Payment processes and batches

What it is

Payments to suppliers are released in batches (called "processes" in the routing and "fund transfer batches" in the UI). A batch is created with selection criteria, filled with the matching payments, reviewed and approved payment by payment, locked, submitted, and finally produces a payment file sent to the bank.

This is the highest-consequence area of the application: a mistake here moves real money.

Where it lives

Concern Location

Routes

processes/processes-routing.routes.ts — two route tables, /processes and the misspelled /proceses

Components

processes/process-list/, process-add/, process-edit/, process-batches/, supplier-payments-details/, pending-edit/

Services

processes/service/payment-batch-repository-service.ts (implements PaymentBatchesAbstractService), payment-batch-downloader.service.ts (+ abstract + mock), processes/service/pending-review.service.ts

Models

processes/model/supplier-payment-batch-model.ts, supplier-payment-batch-list-model.ts, supplier-payment-model.ts

Balance approval

processes/balance-approval/ — see balance approval

Routes

Route Component

/processes/list

ProcessListComponent — all batches.

/processes/:processKey/batches

ProcessBatchesComponent.

/processes/:processKey/new

ProcessAddComponent.

/processes/:processKey/edit

ProcessEditComponent.

/processes/:processKey/:paymentKey

SupplierPaymentsDetailsComponent — one payment inside a batch.

/processes/:supKey

PendingEditComponent.

/processes/approval

BalanceApprovalComponent.

/proceses/approval

BalanceApprovalComponent again, in a second route table.

Route ordering hazard. In the /processes table, :processKey/:paymentKey and :supKey are both declared before approval, so /processes/approval matches :supKey and renders PendingEditComponent, not balance approval. That is why the sidebar links to /proceses/approval — the second, misspelled table exists to work around it.

Do not "tidy" either table without checking bookmarks, the sidebar, and any deep links in emails or runbooks.

The sidebar’s Payments group links to /processes/supplier-payments/batches and /proceses/approval. The first does not match any declared route path literally — it resolves through :processKey/batches with processKey = 'supplier-payments', i.e. a literal string used as a key. Worth knowing before changing the batches route.

Batch creation

SupplierPaymentNewBatchModel is the creation payload, and its fields are the selection criteria:

Field Meaning

name

Batch label.

plannedPaymentTime

When the batch is intended to pay.

reviewers

Who must review it.

locked

Whether it starts locked.

scheduledPayments

Include scheduled payments.

instantAccess

Include Instant Access payments.

proposedAdvanceLine

Include proposed advance-line disbursements.

debitPayments

This is a debit batch (money in) rather than credit (money out).

addAchPmts, addEfsPmts, addPpmPmts, addWirPmts, addLlpPmts

Which payment types to include: ACH, EFS card, PPM, wire, LLP.

paymentBatchFeeCode

Optional fee code applied to the batch.

SupplierPaymentBatchModel adds the runtime view: paymentBatchKey, paymentTypeCounts, paymentTotal, numPayments, numApproved, numCancelled, numHold, numSentToBank, submitted, locked, internalTransfers.

PaymentBatchRepositoryService.getItems post-processes paymentTypeCounts into a human-readable paymentTypes string by concatenating the type names.

Batch endpoints

All on internalApiUrl:

Endpoint Purpose

GET paymentBatches

List (with HttpParams for paging/filtering). Returns { results, total }.

GET paymentBatches/:batchId

One batch.

POST paymentBatches

Create from SupplierPaymentNewBatchModel.

PUT paymentBatches/:batchId

Update.

POST paymentBatches/:batchId/action

Perform a workflow action (SupplierPaymentBatchAction) — approve, submit, cancel and similar transitions.

POST paymentBatches/:batchId/unlock

Unlock a locked batch.

GET paymentBatches/:batchId/status

SupplierPaymentWorkflowStatusModel — where the batch is in its workflow.

GET paymentBatches/:batchId/payments

The credit payments in the batch.

GET paymentBatches/:batchId/get_debit_payments

The debit payments — a different endpoint, selected by an isDebit flag on getPayments.

PUT paymentBatches/:batchId/payments/:paymentId

Update one payment (hold, approve, amend).

DELETE paymentBatches/:batchId/payments/:paymentId

Remove a payment from the batch.

GET paymentBatches/:batchId/paymentFile?withAch=true

The bank payment file (PaymentBatchDownloaderService).

saveItem on the repository is a stub returning null with a // [SM] TODO — batch updates go through updateBatch, not the generic abstract-service method.

Serialisation quirk

Every write method calls SerializeKeysTo(CamelCase) before serialising — so batches are sent in camelCase, unlike most of the API which expects snake_case. Because the casing setting is global in cerialize, this leaves CamelCase active for whatever serialises next. See data modelling.

Payment normalisation

getPayments fills in empty objects for missing paymentOutcome and outcomeReason so templates can bind without null checks:

if (isNullOrUndefined(item.paymentOutcome)) { item.paymentOutcome = {}; }
if (isNullOrUndefined(item.outcomeReason))  { item.outcomeReason  = {}; }

Batch lifecycle

  create                fill                  review
┌──────────────┐   ┌───────────────┐   ┌────────────────────────┐
│ POST         │──►│ backend selects│──►│ per-payment approve /  │
│ paymentBatches│  │ matching pmts  │   │ hold / cancel / remove │
│ (criteria)   │   │ by criteria    │   │ PUT|DELETE …/payments  │
└──────────────┘   └───────────────┘   └───────────┬────────────┘
                                                   │
                    lock ◄─────── unlock ──────────┤
                     │        POST …/unlock         │
                     ▼                              ▼
            ┌────────────────┐          ┌───────────────────────┐
            │ POST …/action  │────────► │ submitted             │
            │ (approve/submit)│         │ numSentToBank grows   │
            └────────────────┘          └──────────┬────────────┘
                                                   ▼
                                        ┌───────────────────────┐
                                        │ GET …/paymentFile     │
                                        │ ?withAch=true         │
                                        │ → file-saver download │
                                        └───────────────────────┘

The authoritative state is GET …/status; the counters on the batch model (numApproved / numCancelled / numHold / numSentToBank) are the summary view.

The payment file

PaymentBatchDownloaderService builds paymentBatches/:batchId/paymentFile?withAch=true and appends a timestamp (addTimestampToUrl) to defeat caching, then hands the response to file-saver. There is an abstract base (PaymentBatchDownloaderAbstractService) and a mock, so specs can exercise the flow without downloading.

Pending review and pending edits

Two related-but-distinct things:

PendingReviewService

GET pending_review/ and PUT /pending_review/:supplierKey with { supplier_transfer_approval_ml_key, treasury_approved }. This is the balance approval queue — see balance approval.

PendingEditComponent

Routed at /processes/:supKey, this is the pending-edit review — supplier changes flagged pendApprovNeeded that require a second approver. Note it is also what /processes/approval accidentally resolves to.

PendingReviewService.updatePendingReview builds its URL as ${this.apiUrl}/pending_review/${supplierKey} while apiUrl already ends in a slash, producing a double slash. BalanceApprovalService builds the same path correctly. Two services, two spellings, same endpoint.

Permissions

The /processes and /proceses tables use the standard deny clause (except: [REST_UW_ACCESS, ADVANCE_AUDITORS]) — the second table without NgxPermissionsGuard at all, only RouteAccessResolver and FeatureFlagsGeneralGuard.

Meaningful control is by permission code enforced in components and the backend: INT_PMT_ADMIN (administer payments), INT_PMT_REVIEW (review payments), MANAGE_PMT, FINANCE. See the permission catalogue.

/proceses/approval has no permission guard on the route. Anyone who passes authentication and reaches that URL renders the balance-approval component; the data calls behind it are what actually enforce access. Treat the route guard gap as a UX inconsistency, not an authorisation hole — but do not rely on the route for gating.

Error handling

PaymentBatchRepositoryService is unusual in toasting from the service layer — each method catches, toasts a specific message ("Cannot fetch Payment Batches", "Cannot store Payment Batch", "Cannot update Payment Batch", "Cannot make Payment Batch Action", "Cannot unlock Payment Batch", "Cannot update Payment", "Cannot delete Payment", "Cannot fetch Payment status") and rethrows.

Combined with PendingInterceptorService, which toasts any 5xx, a server error during a batch operation produces two toasts. Expected, not a bug. See error handling.

Gotchas

Issue Detail

/proceses is intentional

The misspelling is load-bearing — it is how balance approval is reachable.

supplier-payments is a literal processKey

The sidebar’s batches link relies on it.

Debit vs credit are different endpoints

payments vs get_debit_payments, selected by a boolean argument.

camelCase serialisation

Batch writes flip the global cerialize casing.

saveItem is a stub

Returns null.

Double slashes in URLs

PendingReviewService.updatePendingReview.

Fixture JSON in the module

bbbb.json, payments.mock.json, processes-batches.mock.json sit in processes/ and are test data, not runtime config.