ASN Product Receipt Intake (Logistics / 3PL)
A vertical intake accelerator that lands advanced shipping notice (ASN) XML from a 3PL or
carrier and posts each announced shipment as a purchase-order product receipt through the
Lattice purchase order posting service. customService target style.
The model already publishes the outbound half of this conversation -
MGCLatticeVendorASNLineChangedBusinessEvent tells receiving warehouses and TMS/3PL systems about
inbound freight as it is announced. This bundle closes the inbound half: the 3PL's confirmation
that the freight actually arrived.
Flow
SFTP drop (ASN_*.xml)
-> File Ingestion connection (StorageType=Sftp, FileFormat=Xml, RecordPath=Shipment)
-> one record per <Shipment> element, converted to JSON (FileIngestionService.ConvertXmlToJson)
-> one segment per record
-> Liquid transform (transform.liquid, rendered ONCE PER SEGMENT) -> { service, operation, requestKey, request }
-> MGCLatticeFileIngestServiceInvokeTarget.handleIngestedContent
fills RequestId / LegalEntityId / UserContext / CorrelationId the request leaves empty
-> MGCLatticePurchaseOrderPostingService.postPurchasePackingSlip
-> PurchFormLetter::construct(DocumentStatus::PackingSlip).update(...) => posted product receipt
Target (verified against the AOT)
| Element | Value |
|---|---|
| Service group | MGCLatticeAccountsPayableServiceGroup |
| Service class | MGCLatticePurchaseOrderPostingService |
| Operation | postPurchasePackingSlip (also postPurchasePackingSlipBatch) |
| Request contract | MGCLatticePurchaseOrderPackingReceiptRequestContract |
| Line contract | MGCLatticePurchPostingLineContract |
Request DataMember names used by the mapping - PurchaseOrderId, PackingSlipId, PostingDate,
LegalEntityId, RequestId, CorrelationId - and line members LineNum, ItemId, Qty,
IsSelected are taken directly from the contract classes.
Record granularity (read this before authoring a variant)
The ingestion pipeline splits a file into one segment per record and renders the Liquid template once per segment:
MGCLatticeFileIngestPullparses the file into records (each<Shipment>element, converted to JSON) and writes oneMGCLatticeFileIngestSegmentper record.MGCLatticeFileIngestSegmentServicerendersconnection.LiquidTemplateagainst that single segment'sRawContent.- The Fluid model exposes the record object's members at the top level, so the template reads
{{ PurchaseOrderId }}. There is norecordscollection in scope. MGCLatticeFileIngestServiceInvokeTargetparses exactly one{ service, operation, requestKey, request }envelope per rendered segment, so one record = one service call.
So a bundle must be designed such that one source record is one target document. Here that is
exact: recordPath=Shipment, one <Shipment> = one product receipt. There is no aggregation step
in the pipeline (every record becomes its own segment and its own service call).
XML specifics
recordPath = Shipment selects every <Shipment> element. Each one is converted to JSON with
FileIngestionService.ConvertXmlToJson. Consequences worth knowing:
- The record element itself is dropped, so a
<Shipment>'s children (ShipmentId,PurchaseOrderId,PackingSlipId,ReceiptDate,Lines) are the top-level members the template reads, and the lines areLines.Line. - Every value is a string (
"Quantity": "10"); the template writes numeric members through thenumberfilter, so they reach the contract as JSON numbers (00010becomes10, a blank valuenull, anything else fails the record). - A repeated element becomes a JSON array, but a single occurrence becomes an object. The
template loops over
Lines.Line | as_array, so a shipment with one<Line>works like one with several. - Attributes of an element are plain properties (
<Shipment id="7">givesid), namespace prefixes are dropped, CDATA is plain text, and<Qty unit="ea">10</Qty>givesQtyplusQty_unit.
Run context and idempotency
postPurchasePackingSlip refuses a request without RequestId (a GUID), LegalEntityId and
UserContext. The template writes LegalEntityId from the legalEntityId activation parameter
and names the shipment's stable id in the envelope's requestKey (the element named by the
requestIdSourceField activation parameter, default ShipmentId). The service-invoke target fills
every common member the request leaves empty:
RequestId: a name-based GUID of the connection, the legal entity, the operation and therequestKey. A redelivered ASN, in any file, gets the sameRequestId, and the service replays the posted receipt instead of posting a second one.LegalEntityId: the company the processing batch job runs in.UserContext: the user the processing batch job runs as.CorrelationId: shared by all shipments of one file.
A value the template writes always wins, and requestKey never reaches the request.
Lines
postPurchasePackingSlip receives only the lines the request lists, each with the quantity it
carries (Qty); a request without Lines receives everything on the purchase order that is not yet
received. transform.liquid lists every <Line> of the shipment (through as_array, so a shipment
with one line works like one with several), so a partial shipment, or one purchase order split
across several ASNs, posts exactly what each ASN announces.
Files
manifest.json- bundle manifest (conforms toschema/bundle.schema.json).sample.xml- one ASN file announcing two shipments, two lines each.mapping.json- source-element -> contract-member mapping (header + lines).transform.liquid- Liquid template producing the{ service, operation, requestKey, request }envelope.
Activation
manifest.activation.parameters declares the tenant values this bundle cannot ship: the SFTP
endpoint, the credential (a Key Vault reference - never in the bundle), the pinned host-key
fingerprint, folder paths, poll schedule, the idempotency key field, and the posting legal entity.
Run bundlecatalog plan asn-product-receipt-intake for the ordered provisioning plan and a
parameter template. See Activation. Also enter
MGCLatticePurchaseOrderPostingService.postPurchasePackingSlip in the connection's Allowed
service operations; an empty list allows no operation, and every record is rejected.
Status
preview. The target is real and the dispatch loop is executable.