Worked batch example

A synthetic batch has three rows: ₦1,000, ₦2,000, and ₦3,000. The approval screen says three staff and ₦6,000 total. After approval, change the last account number while keeping the amount unchanged. The batch hash changes and the release fails. Restore the approved file; the provider accepts two rows and rejects the ₦3,000 row. The payout record shows ₦3,000 accepted, ₦3,000 under review, and no new request for the first two rows on retry. The amounts are invented; the assertion is that every approved row has one final outcome.

Freeze the approved batch

Validate the upload first: parse every row, reject duplicate staff IDs within the batch, check amount totals, and display the exact rows to approvers. Hash a stable, normalized representation so changing a file name does not change the approval, but changing a bank account does.

Checks to run

  1. Hash the normalized batch and bind approvals to that hash, amount total, and item count.
  2. Reject self-approval and require fresh approval after any row changes.
  3. Test duplicate uploads, one bad account, partial provider acceptance, and release retry.

Approval matrix

Try a file with one changed account number after first approval, a changed amount after second approval, and a duplicated staff row. Each change must invalidate prior approvals. Test an approver who also uploaded the file; the release should be blocked by separation of duties.

Run a provider response with 199 accepted rows and one rejected row. After confirming final rejection, retry that row under the provider’s rule while keeping the approved original and row outcomes linked.

One more boundary test

Test bank account normalization. Two rows may use different spacing or bank-code formatting but point to the same destination. The batch validator should apply one canonical format before duplicate checks and hashing. Keep the original text for audit, but approve the canonical destination that will actually be sent. Also test an approver whose role is revoked after first approval and before release. The release rule must say whether that approval remains valid and enforce it consistently.

Show exactly what was approved

Build batch P-01 with 100 workers and a total of ₦2,000,000. Store a digest of the canonical payee, account, and amount rows before approval. After two reviewers sign, change one account while keeping the same total. Release must fail because the approved digest no longer matches the batch. Test deleting a row, adding a row, changing the bank code, and reordering rows. Reordering should follow the chosen canonical format, so the team must define whether order matters. Keep the source file, parsed rows, digest, reviewer IDs, and provider response. The receipt must link to the exact approved version.

Classify each payroll row before retry

After partial release, divide rows into confirmed paid, confirmed failed, and unknown. Paid rows must not be sent again. Query unknown rows by their original references before any replacement. A confirmed failed row can follow the provider’s retry rule, but correcting its bank account changes the approved payload and needs fresh approval. Keep the original row and failure; record the corrected version as a linked replacement after ruling out an earlier payment. In the three-row example, mark the 3,000 NGN row confirmed rejected before retrying it. A timeout is not a rejection. Reconcile the batch to all row states, including unresolved amounts.

Primary sources

Next step

Approve a test payroll file, change one account number, and confirm that release rejects the changed batch. Read the related guide. For a review of your own system, request a security review.