Customer Payment Intake (O2C / lockbox cash application)
A vertical intake accelerator that lands fixed-width bank lockbox remittance files and posts
each remittance as a customer payment journal through the Lattice payment journal service.
customService target style. This is the catalog's exemplar for the FixedWidth source format
and the connection's FixedWidthColumnSpec.
Flow
SFTP drop (LOCKBOX_*.txt)
-> File Ingestion connection (StorageType=Sftp, FileFormat=FixedWidth, FixedWidthColumnSpec=[...])
-> each non-blank line converted to one JSON object (FileIngestionService.ConvertFixedWidthToJson)
-> one segment per line
-> Liquid transform (transform.liquid, rendered ONCE PER SEGMENT) -> { service, operation, requestKey, request }
-> MGCLatticeFileIngestServiceInvokeTarget.handleIngestedContent
fills RequestId / LegalEntityId / UserContext / CorrelationId the request leaves empty
-> MGCLatticePaymentJournalService.postCustomerPaymentJournal
-> LedgerJournalType::CustPayment journal, posted via LedgerJournalCheckPost
Copy fixedWidthColumnSpec from the manifest into the connection's FixedWidthColumnSpec as
is: the parser reads the column names, 0-based start offsets and widths from it.
Target (verified against the AOT)
| Element | Value |
|---|---|
| Service group | MGCLatticeGeneralLedgerServiceGroup |
| Service class | MGCLatticePaymentJournalService |
| Operation | postCustomerPaymentJournal (also postCustomerPaymentJournalBatch) |
| Request contract | MGCLatticePaymentJournalPostRequestContract |
| Line contract | MGCLatticePaymentJournalLineContract |
Request members JournalNameId, Description, PostingDate, LegalEntityId, RequestId,
CorrelationId and line members AccountNum, AmountCurDebit, AmountCurCredit, CurrencyCode,
BankAccountId, MethodOfPayment, Description, TransDate are taken directly from the
contract classes.
Two behaviors worth knowing:
- The operation posts
LedgerJournalType::CustPaymentwithAccountType = LedgerJournalACType::Cust, and validates that the supplied journal name is that type. BankAccountIdis always resolved asLedgerJournalACType::Bank. It is a bank account, never a ledger account.
The fixed-width layout
99 characters per record, start is a 0-based character offset:
| Field | start | width |
|---|---|---|
customer_account | 0 | 10 |
payment_reference | 10 | 16 |
payment_date | 26 | 10 |
amount | 36 | 14 |
currency | 50 | 3 |
invoice_number | 53 | 16 |
remittance_note | 69 | 30 |
The converter trims trailing whitespace only (value.TrimEnd()), so the shipped sample
left-aligns every field. A bank that right-aligns numerics will hand you " 1250.00"; the
transform applies | strip to the amount so that still renders valid JSON. Short/ragged lines are
tolerated - missing characters become empty strings rather than an error.
A positional layout is bank-specific, and a wrong offset produces wrong values silently rather than a parse error.
columnLayoutConfirmedis a required activation parameter for exactly that reason.
Run context and idempotency
postCustomerPaymentJournal refuses a request without RequestId (a GUID), LegalEntityId and
UserContext. The template writes LegalEntityId from the legalEntityId activation parameter
and names the remittance's stable id in the envelope's requestKey (the column named by the
requestIdSourceField activation parameter, default payment_reference). 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 remittance, in any file, gets the sameRequestId, and the service replays the posted journal 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 remittances of one file.
A value the template writes always wins, and requestKey never reaches the request.
Record granularity - one remittance, one journal
The pipeline splits a file into one segment per record and renders the Liquid template once
per segment, and MGCLatticeFileIngestServiceInvokeTarget parses exactly one routing envelope per
segment. So each remittance record posts its own single-line payment journal
(batching.strategy = onePerNLines, bundleSize = 1).
That is a defensible cash-application pattern - each remittance is independently postable and
reversible, and one bad record cannot block the rest of the batch. But a tenant who wants one
journal per lockbox file cannot have it today: there is no aggregation step in the pipeline
(every record becomes its own segment and its own service call). See
productDecisions.
Known gaps
- Posting is not settlement. The journal posts the cash but leaves the customer's invoices
open. Matching cash to invoices is a separate operation,
settleCustomerTransactions. The adapter invokes exactly one operation per segment, so chaining post-then-settle needs either a second connection or an X++ composite operation. - The bank reference has no field of its own. The line contract carries no external line id,
so the transform folds
invoice_numberinto the lineDescription;payment_referenceis the envelope'srequestKey.
Files
manifest.json- bundle manifest (conforms toschema/bundle.schema.json).sample.txt- four 99-character lockbox remittance records.mapping.json- column -> contract-member mapping (header + lines).transform.liquid- Liquid template producing the{ service, operation, requestKey, request }envelope.
Activation
transform.liquid carries __journalNameId__, __bankOffsetAccount__, __methodOfPayment__,
__legalEntityId__ and __requestIdSourceField__ placeholders. Each is declared in
manifest.activation.parameters and is substituted at provisioning time - none is baked into the
bundle, because a journal name, a bank account, a payment method, a company and the bank's
remittance id column are tenant facts. Run bundlecatalog plan customer-payment-intake for the ordered
plan and a parameter template; see Activation. Also enter
MGCLatticePaymentJournalService.postCustomerPaymentJournal 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. Current limitations: a record is
posted but not settled in the same step, and the records of one file cannot be aggregated into one
journal (see Known gaps and Record granularity above).