Design and implement your transaction flow
Prerequisites¶
To run this tutorial, you must have a running instance with (i) an assigned application profile and (ii) the P6 Demo App installed.
If you are not familiar with the P6 Demo App, please read this section.
Design the transaction flow¶
There are a few questions to be answered in order to design a transaction flow, such as:
- Where will the transaction originate from (it could be retrieved from the blockchain, or received from an internal system like an ERP, or from a trading partner via a protocol like AS2 or SFTP…)
- What will the source format of the transaction / business document be and what shall its target format be
- What transaction attributes / details should be captured from the business document (for example: total amount, number of line items,…)
- Which statuses will the transaction take and what will dictate a status change
- Will specific statuses trigger a workflow and if so, what actions would be presented to the assignee(s)
Let us take the Demo App as an example:
Step & Description | Source & Target system(s) | Business document | P6 transaction(s) | Workflow |
---|---|---|---|---|
Step 1 - A Request for Quotation (RFQ) is created every 10 minutes and stored in a smart contract | P6 -> Smart contract | UBL RFQ created and stored in smart contract | No transaction created on P6 | - |
Step 2 - The UBL RFQ is retrieved from the smart contract and an RFQ Transaction is created on P6 | Smart contract -> P6 | UBL RFQ stored as is in the instance file system | RFQ transaction created, with technical status set to “Received” | - |
Step 3 - The received RFQ triggers a P6 workflow, which offers two options to the supplier user: either (a) provide a Quote or (b) decline the RFQ | P6 -> P6 | - | - | Workflow task created |
Step 4a - The Supplier user decides to provide a Quote in reply to the received RFQ | P6 -> Smart contract | UBL Quote created and stored both on the instance file system and the smart contract | RFQ tech status set to “Handled”, functional one to “Quote provided” Quote transaction created, with tech status set to “Sent” |
- |
Step 4b - The Supplier user decides to decline the RFQ and can enter a reason | P6 -> Smart contract | No business doc created but the RFQ status is set to “Declined” in the smart contract | RFQ tech status set to “Handled”, functional one to “Declined” | - |
Step 5 - The Buyer receives the Quote via the blockchain and generates a Purchase Order matching the Quote | Smart contract -> P6 | UBL Order created and stored on the file system | - | - |
Step 6 - The received PO triggers a P6 workflow, which offers two options to the supplier user: either (a) accept the Order or (b) reject it | P6 -> P6 | - | PO transaction created and tech status set to “Received” | Workflow task created and email notification sent to the assignee(s) |
Step 7a - The Supplier user decides to accept the PO and can enter a Sales Order Number | P6 -> Smart contract | - | PO transaction tech status set to “Handled” and functional status set to “Accepted” | - |
Step 7b - The Supplier user decides to reject the PO and can enter a rejection reason | P6 -> P6 | - | PO transaction tech status set to “Handled” and functional status set to “Rejected” | - |
In this tutorial, we will extend the scenario of the Demo App and trigger a workflow that will offer the Supplier user the ability to notify the buyer of the shipment date for a specific purchase order.
If the user opts for sending a confirmation, an email with PO and shipment details will be posted and the transaction information will be updated accordingly.
Else, no email will be sent but the transaction information will be nonetheless updated to reflect the user’s choice and the shipment date.
So, here are the few steps we will want to add:
Step & Description | Source & Target system(s) | Business document | P6 transaction(s) | Workflow |
---|---|---|---|---|
Step 8 - Each “Accepted” PO triggers a workflow, which offers two options to the supplier user: either (a) Confirm shipment to the buyer - via email - or (b) Skip shipment confirmation | P6 -> P6 | - | - | Workflow task created |
Step 9a - The Supplier user decides to confirm shipment (and enters ship date and email details) | P6 -> Email | - | PO transaction functional status set to “Shipment confirmation sent”, email sent to buyer (with copy to the user who handled the workflow task) | - |
Step 9b - The Supplier user decides to skip shipment confirmation (but enters the ship date) | P6 -> P6 | - | PO transaction functional status set to “Shipment confirmation not sent” | - |
Implement the transaction flow¶
The implementation of the designed flow will usually involve Scripts, Routes, Workflow Steps - and potentially other types of service items.
If you need a refresh on how the P6 Demo App is implemented, you may read again this section, which details the various service items leveraged at each step of the transaction flow.
Now, let us implement steps 8 and 9a/9b…
Add a Route Deployment Script¶
We shall start by creating a route - an endpoint that the Demo App will call.
So click on the ‘Routes’ menu entry, duplicate the route deployment script called ‘RoutingRulesForPurchaseOrders’ and keep the same name (so just hit the green ‘Duplicate’ icon and validate with the ‘Enter’ key).
Note
Service items are identified by their application key and name, so two service items can have the same name as long as they don’t have the same app key
Open the route deployment script you created via duplication and make the required changes to the script so it looks as follows:
p6.camel.getCtx().addRoutes(new RouteBuilder() {
void configure() {
from('direct:p6router.ExtendPurchaseOrder')
.choice()
.when(xpath("/TransactionInfo/FunctionalStatusCode='Accepted'"))
.setHeader("platform6.request.action").constant("invoke")
.setHeader("status").constant("Shipment to be confirmed")
.setHeader("step").constant("HandleShipmentConfirmation")
.setHeader("appkey").constant("")
.setHeader("flowname").constant("UUID")
.setHeader("script").constant("CustomWFStepBuilder")
.to("p6route://platform6.workflowsteps")
.end()
.routeId('Extend p6_demo Routing rules for Purchase Orders')
.description("Extend p6_demo Routing rules for Purchase Orders")
}
})
When called and if the transaction FunctionalStatusCode is ‘Accepted’, this route will trigger the ‘HandleShipmentConfirmation’ workflow step.
Alternatively, instead of duplicating the ‘RoutingRulesForPurchaseOrders’ script from p6_demo, you can can also create it from scratch. To do so, hit the ‘+ Add’ button and fill in the form as follows:
Add a Workflow Step¶
We will now create the workflow step that will offer two option to the workflow assignees, either to confirm or not confirm shipment. So, click on the ‘Workflow Steps’ menu entry, then hit the ‘+Add button’, enter ‘HandleShipmentConfirmation’ as the name and the following XML description:
<WorkflowStep enabled="true">
<Description>
<EN>Shipment - Confirm or not confirm</EN>
<FR>Expédition - Confirmer ou pas</FR>
</Description>
<AllowTransactionEdit>ASSIGNEE</AllowTransactionEdit>
<TransactionDataModel>p6_demo.TransactionInfo</TransactionDataModel>
<ViewNames>
<Item>p6_demo.Transactions</Item>
<WorkItem>p6_demo.Workflow Tasks</WorkItem>
</ViewNames>
<AllowRecall>false</AllowRecall>
<AllowApproverDelegation>false</AllowApproverDelegation>
<!-- This section determines if workflow assignees will receive notification emails and, if so, which email template to use -->
<!-- To send emails out, you first need to add an email profile by following this guide:
https://documentation.amalto.com/platform6/latest/reference/built-in-services/email/email-guide/
then set the flag below to 'true' -->
<SendEmails>false</SendEmails>
<EmailTemplate modelScript="p6_demo.WFHandlePO-BuildEmail">file://${B2BOX_DATA}/resources/templates/p6demo_POReview.ftl</EmailTemplate>
<!-- This is the Time To Live for the workflow task, expressed in number of minutes. A value of 0 means never expire. -->
<Ttl id="expire">120</Ttl>
<WorkflowTaskEnhancer script="p6_demo.WFWorkflowTaskEnhancer"/>
<!-- This is where the workflow assignees are defined, in particular to where they are in the org structure and which permission they shall have -->
<Assignee name="DemoApp" path="/${INSTANCE_ID}" type="UNIT" scope="*=*">
<Label>
<EN>Supplier</EN>
<FR>Fournisseur</FR>
</Label>
</Assignee>
<StatusLabels>
<Label name="Shipment to be confirmed" >
<EN>Shipment to be confirmed</EN>
<FR>Expédition à confirmer</FR>
</Label>
</StatusLabels>
<!-- This is the section where the various actions offered are defined. -->
<Actions>
<Action id="confirm" status="Shipment confirmed" type="ACTION" stop="true" script="WFHandleShipmentConf-HandleActionConfirmShipment">
<Style>icon:fa-check,btn:btn-success</Style>
<Label>
<EN>Confirm shipment</EN>
<FR>Confirmer expédition</FR>
</Label>
<Parameter>
<Name>recipient</Name>
<Label>
<EN>To</EN>
<FR>To</FR>
</Label>
<Mandatory>true</Mandatory>
<InputType>TEXT</InputType>
<DefaultValue>purchasing-dept@buyercorp.com</DefaultValue>
</Parameter>
<Parameter>
<Name>carboncopy</Name>
<Label>
<EN>Cc</EN>
<FR>Cc</FR>
</Label>
<Mandatory>true</Mandatory>
<InputType>TEXT</InputType>
<DefaultValue></DefaultValue>
</Parameter>
<Parameter>
<Name>shipmentdate</Name>
<Label>
<EN>Shipment date</EN>
<FR>Date d'expédition</FR>
</Label>
<Mandatory>true</Mandatory>
<InputType>TEXT</InputType>
<DefaultValue></DefaultValue>
</Parameter>
<Parameter>
<Name>files</Name>
<Label>
<EN>Attachments</EN>
<FR>Pièces jointes</FR>
</Label>
<Mandatory>false</Mandatory>
<InputType>FILES</InputType>
</Parameter>
</Action>
<Action id="notconfirm" status="Shipment confirmation not sent" type="ACTION" stop="true" script="WFHandleShipmentConf-HandleActionSkipConfirmation">
<Style>icon:fa-times,btn:btn-warning,color:warning</Style>
<Label>
<EN>Skip shipment confirmation</EN>
<FR>Ne pas confirmer l'expédition</FR>
</Label>
<Parameter>
<Name>shipmentdate</Name>
<Label>
<EN>Shipment date</EN>
<FR>Date d'expédition</FR>
</Label>
<Mandatory>true</Mandatory>
<InputType>TEXT</InputType>
<DefaultValue></DefaultValue>
</Parameter>
</Action>
<Action id="expire" status="EXPIRED" type="EXPIRE" display="false">
<Expiry error="false"/>
</Action>
</Actions>
</WorkflowStep>
Save this workflow step, then stop and restart the service. The Workflow Steps service compiles all step definitions and caches them for runtime efficiency. To force your new workflow step to be compiled, a service restart is needed (using the stop and start buttons, top right of the page).
Add Scripts¶
The ‘HandleShipmentConfirmation’ workflow step we just created references two scripts, each linked to one of the actions offered to the workflow assignee.
The next step is to create the script that will be called in case the workflow assignee chooses the ‘Confirm shipment’ action, then completes and submits the workflow task.
Click on the ‘Scripts’ menu entry, then on the green ‘+ Add’ button.
Enter the name and description of the script:
- Name: WFHandleShipmentConf-HandleActionConfirmShipment
- Description: Handle Accepted PO TransactionInfo updates for a Shipment Confirmation - Script called by the HandleShipmentConfirmation Workflow Step
After hitting ‘Save’, you will get the following screen:
Here is the groovy script to be inserted:
import groovy.json.*
import java.text.SimpleDateFormat
import org.apache.commons.io.FileUtils
def dataType = p6.pipeline.get 'platform6.request.dataType'
def itemIds = p6.pipeline.get 'platform6.request.ids'
def recipient = p6.pipeline.get 'recipient'
def shipmentdate = p6.pipeline.get 'shipmentdate'
def cc = p6.pipeline.get 'carboncopy'
def itemPk = p6.transaction.buildPK(dataType, itemIds)
def transactionInfoContent = p6.transaction.exists(itemPk)
XmlSlurper slurper = new XmlSlurper()
def transactionInfo = slurper.parseText(transactionInfoContent)
def currentDate = new Date()
SimpleDateFormat transaction_sdf = new SimpleDateFormat("yyyyMMdd'T'HH:mm:ss")
def formatedDate = transaction_sdf.format(currentDate)
transactionInfo.FunctionalStatusCode = 'Shipment confirmation sent'
transactionInfo.FunctionalStatusMessage = 'Shipment confirmation emailed to '+recipient+' with shipment date: '+shipmentdate
transactionInfo.FunctionalStatusDate = formatedDate
def transactionId = transactionInfo.Id.text()
def ipk = p6.transaction.buildPK('TransactionInfo', transactionId)
p6.transaction.save(groovy.xml.XmlUtil.serialize(transactionInfo), 'p6_demo.TransactionInfo', ipk)
def params = [ 'subject': 'Shipment confirmation', 'cc' : cc ]
def attachments = null
def poFile = p6.uri.fileFromUrl(transactionInfo.TargetDocumentURI.text())
def purchaseOrder = FileUtils.readFileToString(poFile, 'UTF-8')
def html_body = p6.xslt.process("PO XML to ShipmentConfirmation HTML", p6.resource.get('PO_to_ShipmentConfirmation'), purchaseOrder)
html_body = html_body.replace('REPLACESHIPMENTDATE', shipmentdate)
p6.email.sendHtmlEmail( 'demo@platform6.io', recipient, html_body, params, attachments )
Now, you have probably noticed that the groovy script references a resource called ‘PO_to_ShipmentConfirmation’. This resource is an XSLT that creates the body of the Shipment Confirmation email notification to be sent. So, after you’ve created the main groovy script, you will have to add a resource. In order to do so, click the ‘+’ button:
Then enter the resource name, select its type and save:
You can then copy & paste the XSLT resource:
<?xml version="1.0"?>
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:root="urn:oasis:names:specification:ubl:schema:xsd:Order-2"
xmlns:cac="urn:oasis:names:specification:ubl:schema:xsd:CommonAggregateComponents-2"
xmlns:cbc="urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2"
version="2.0">
<xsl:output method="html" version="4.0" encoding="UTF-8" indent="yes"/>
<xsl:template match="/">
<HTML>
<HEAD><TITLE/></HEAD>
<BODY>
<H1></H1>
Dear Client, <br/><br/>We are glad to confirm your order <xsl:value-of select="root:Order/cbc:ID"/> is scheduled to be shipped on REPLACESHIPMENTDATE.<br/><br/>
<br/><strong>Shipping details</strong>
<br/>- Shipping to: <xsl:apply-templates select="//cac:BuyerCustomerParty/cac:Party/cac:PostalAddress"/>
<br/><br/>- Items shipped:
<xsl:apply-templates select="//cac:OrderLine"/>
<br/><br/>Thank you for your business!
</BODY>
</HTML>
</xsl:template>
<xsl:template match="cac:PostalAddress">
<br/><DIV><xsl:value-of select="cbc:StreetName"/><xsl:text> </xsl:text><xsl:value-of select="cbc:PostalZone"/><xsl:text> </xsl:text><xsl:value-of select="cbc:CityName"/></DIV>
</xsl:template>
<xsl:template match="cac:OrderLine">
<br/><DIV><xsl:value-of select="cac:LineItem/cac:Item/cbc:Description"/><xsl:text> </xsl:text><xsl:value-of select="cac:LineItem/cbc:Quantity"/><xsl:text> </xsl:text><xsl:value-of select="cac:LineItem/cbc:Quantity/@unitCode"/></DIV>
</xsl:template>
</xsl:stylesheet>
The next step is to create the script that will be called in case the workflow assignee chooses the ‘Skip shipment confirmation’ action, then completes and submits the workflow task.
- Name: WFHandleShipmentConf-HandleActionSkipConfirmation
- Description: Handle Accepted PO TransactionInfo updates for a Shipment Confirmation - Script called by the HandleShipmentConfirmation Workflow Step
Here are the groovy script details:
import groovy.json.*
import java.text.SimpleDateFormat
def dataType = p6.pipeline.get 'platform6.request.dataType'
def itemIds = p6.pipeline.get 'platform6.request.ids'
def shipmentdate = p6.pipeline.get 'shipmentdate'
def itemPk = p6.transaction.buildPK(dataType, itemIds)
def transactionInfoContent = p6.transaction.exists(itemPk)
XmlSlurper slurper = new XmlSlurper()
def transactionInfo = slurper.parseText(transactionInfoContent)
def currentDate = new Date()
SimpleDateFormat transaction_sdf = new SimpleDateFormat("yyyyMMdd'T'HH:mm:ss")
def formatedDate = transaction_sdf.format(currentDate)
transactionInfo.FunctionalStatusCode = 'Shipment confirmation not sent'
transactionInfo.FunctionalStatusMessage = 'Shipment date: '+shipmentdate
transactionInfo.FunctionalStatusDate = formatedDate
def transactionId = transactionInfo.Id.text()
def ipk = p6.transaction.buildPK('TransactionInfo', transactionId)
p6.transaction.save(groovy.xml.XmlUtil.serialize(transactionInfo), 'p6_demo.TransactionInfo', ipk)
We will now add a script that will adapt the workflow step definition “on the fly”. Create a script named ‘CustomWFStepBuilder’, as follows:
import java.text.SimpleDateFormat
def workflowStep = p6.pipeline.getXml 'templateStepXml'
def dataType = p6.pipeline.get 'platform6.request.dataType'
def itemIds = p6.pipeline.get 'platform6.request.ids'
def itemPk = p6.transaction.buildPK(dataType, itemIds)
def transactionInfoContent = p6.transaction.exists(itemPk)
XmlSlurper slurper = new XmlSlurper()
def transactionInfo = slurper.parseText(transactionInfoContent)
// SET Default shipmentdate parameters to current date
def currentDate = new Date()
SimpleDateFormat transaction_sdf = new SimpleDateFormat("yyyy-MM-dd")
def formatedDate = transaction_sdf.format(currentDate)
def myAction = workflowStep.Actions.Action.find{it.@id == 'confirm'}
def myParameter = myAction.Parameter.find{it.Name == 'shipmentdate'}
myParameter.DefaultValue = formatedDate
def mySecondAction = workflowStep.Actions.Action.find{it.@id == 'notconfirm'}
def myOtherParameter = mySecondAction.Parameter.find{it.Name == 'shipmentdate'}
myOtherParameter.DefaultValue = formatedDate
def stepXml = groovy.xml.XmlUtil.serialize( workflowStep )
p6.pipeline.put 'stepXml', stepXml
Deploy the Route¶
Go back to the ‘Routes’ page, open the ‘RoutingRulesForPurchaseOrders’ deployment script created earlier, and hit the ‘Start job’ button - in order to actually deploy the route.
Expand the ‘ACTIVE ROUTES’ top panel (which is collapsed by default), and you will see a new ‘Extend p6_demo Routing rules for Purchase Orders’ route, which just started.
Update p6_demo script to call your endpoint¶
Go to the ‘Scripts’ page, open the ‘p6_demo.WFHandlePO-HandleActionAcceptOrder’ script and edit it so it calls the ‘direct:p6router.ExtendPurchaseOrder’ endpoint we created initially in the ‘RoutingRulesForPurchaseOrders’ route.
To do so, you just need to change the last line of the script:
//p6.transaction.saveAndRoute(groovy.xml.XmlUtil.serialize(transactionInfo), 'p6_demo.TransactionInfo', ipk, 'direct:p6router.p6_demo_Dispatcher')
p6.transaction.saveAndRoute(groovy.xml.XmlUtil.serialize(transactionInfo), 'p6_demo.TransactionInfo', ipk, 'direct:p6router.ExtendPurchaseOrder')
Once done, restart the Scripts service.
That’s it. You’ve added steps to the Demo scenario.