Choose an import policy before reading rows
This guide uses a whole-settlement policy: stage every row, validate the full settlement, and post only after all checks pass. A bad row or missing page holds the whole settlement. Record this rule in the importer so a restart cannot quietly switch to posting good rows first.
A partial-post policy needs a separate design. It must track which rows are posted, which remain held, how each held amount affects the settlement, and how the full totals are later closed. Do not mix that policy with a claim that the whole file was approved. In both designs, staging and validation alone never mark a merchant paid.
Map exact provider fields
Paystack's settlement API lists settlement headers and a separate transaction endpoint. The mapping below uses the documented response keys. It is an adapter for that API, not a promise that every CSV provider uses these fields.
| Provider field | Local use |
|---|---|
| Settlement id | Provider settlement ID, scoped to the authenticated provider account. |
| integration, domain, currency | Expected merchant integration, live/test environment, and currency. |
| status, settlement_date | Provider settlement state and date; neither is a bank statement. |
| total_processed, total_fees | Store the provider's processed and fee totals separately. |
| total_amount, effective_amount, deductions | Preserve each value. Apply a documented deduction rule before comparing net totals. |
| Transaction id, reference, amount, fees | Stable transaction identity, reference, gross value, and transaction fees. |
For NGN, keep money in integer kobo and prove the unit conversion with an adapter fixture. Store large provider transaction IDs without JavaScript number rounding; Paystack specifies unsigned 64-bit transaction IDs. Preserve the raw response alongside normalized values. Do not assume deductions are zero because one sample shows null, or force effective amount to equal processed minus fees when the provider applies other rules.
Prove completeness from an independent set
A file's own row count and totals cannot prove it is complete. A truncated export can have a matching truncated footer. Obtain the expected settlement membership separately: fetch every page from the authenticated settlement transaction endpoint, or use an independently obtained provider manifest for a file-only service. Save the query, account, settlement ID, page count, retrieval time, and raw responses.
Compare the expected and imported transaction ID sets. Report missing IDs, unexpected IDs, and duplicate IDs before checking money totals. Remove no rows merely to make the counts agree. Keep the query scope fixed; date filters or an unfinished page loop can make the expected set incomplete too. Confirm the provider's final settlement state and stable membership rule before approval. Bank receipt is a later cash reconciliation check.
Check source, shape, identity, and totals
Verify the authenticated account or approved delivery path before parsing. Keep the raw file, digest, and parser version. Reject wrong currency, invalid types, unknown columns that change meaning, or amounts outside the agreed schema. OWASP's file upload guide covers safe handling of untrusted files.
Match each transaction to a known captured payment and approved merchant. Compare membership first, then gross values, fees, deductions, and net settlement under the provider adapter's rules. A total can match while the wrong merchant or an unknown transaction is included. A successful charge does not itself prove settlement or bank receipt.
Deduplicate the economic event across settlements
A unique settlement ID plus transaction ID stops a repeat within one settlement. It does not stop the same capture from being credited again in another settlement. Keep an economic posting key scoped to provider account, event type, and event ID, such as capture plus provider transaction ID. Store settlement membership as separate evidence linked to that posting.
If the same capture appears under a second settlement, hold it for review rather than add another merchant credit. Refunds, chargebacks, and fee adjustments each need their own stable event identity and link to the capture. A corrected file changes evidence; it does not automatically create a new money event. Approve and post a named delta journal only when provider evidence proves the correction.
Test two payments and a changed file
Use a synthetic final settlement S-1 with expected transactions A and B. A has ₦10,000 gross and ₦300 fees; B has ₦5,000 gross and ₦150 fees. With no other deductions, gross is ₦15,000, fees are ₦450, and net is ₦14,550. The independent membership endpoint returns both IDs.
- Import only A with a matching one-row footer. The set comparison finds missing B; the whole settlement stays held.
- Import A and B, but repeat A. The duplicate check fails before posting.
- Import a valid S-1 twice. The second run returns the same approval and journal IDs with no new credits.
- Place A in S-2. The economic posting key stops a second capture credit and opens a membership conflict.
- Change B's fee from ₦150 to ₦200 under S-1. Preserve both versions and hold the ₦50 difference for source review.
- Crash during staging, then after posting but before recording the job reply. Recovery resumes validation or returns the existing committed posting.
Case PAY-15 in the synthetic payment test pack covers a corrected export. Extend it with the independent set and cross-settlement tests above. Retain source versions, set differences, totals, approval, and journal IDs. Keep exceptions out of automatic payouts until a named owner resolves them. See reconciliation exceptions and payment gateway testing.