Skip to main content

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)​

ElementValue
Service groupMGCLatticeGeneralLedgerServiceGroup
Service classMGCLatticePaymentJournalService
OperationpostCustomerPaymentJournal (also postCustomerPaymentJournalBatch)
Request contractMGCLatticePaymentJournalPostRequestContract
Line contractMGCLatticePaymentJournalLineContract

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::CustPayment with AccountType = LedgerJournalACType::Cust, and validates that the supplied journal name is that type.
  • BankAccountId is always resolved as LedgerJournalACType::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:

Fieldstartwidth
customer_account010
payment_reference1016
payment_date2610
amount3614
currency503
invoice_number5316
remittance_note6930

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. columnLayoutConfirmed is 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 the requestKey. A redelivered remittance, in any file, gets the same RequestId, 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​

  1. 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.
  2. The bank reference has no field of its own. The line contract carries no external line id, so the transform folds invoice_number into the line Description; payment_reference is the envelope's requestKey.

Files​

  • manifest.json - bundle manifest (conforms to schema/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).