Appearance
Internal Transfers Flow
Move funds between accounts with full audit trail via double-entry journals. Replaces the deprecated account adjustments endpoint.
Overview
| Attribute | Value |
|---|---|
| Trigger | HTTP POST /v1/internal-transfers |
| Handler | src/handler/createInternalTransferHandler.ts |
| Service | src/service/transfers/InternalTransferService.ts |
| Interface | src/repository/IInternalTransferService.ts |
| Delegates to | AccountingService.accountingPreparer() with category ADJUSTMENT |
An internal transfer moves a specified amount from one account (from_account_number) to another (to_account_number), generating one ADJUSTMENT journal and the corresponding ledger entries. The transfer_type is a user-friendly category that maps to an underlying AdjustmentTypeEnum value stored in the journal metadata for audit purposes.
The transfer model is directional: the source account's balance decreases by the amount and the destination's balance increases by the amount, regardless of each account's accounting nature (ASSET, LIABILITY, REVENUE, EXPENSE, EQUITY). The accounting service handles the underlying DEBIT/CREDIT mapping per account type — see Accounting Integration below.
Flow Diagram
Request
POST /v1/internal-transfers
| Field | Type | Required | Description |
|---|---|---|---|
from_account_number | string | Yes | Source account number — its balance will decrease |
to_account_number | string | Yes | Destination account number — its balance will increase |
amount | number | Yes | Transfer amount (minimum 0.01) |
transfer_type | TransferTypeEnum | Yes | User-facing reason for the transfer |
description | string | Yes | Human-readable description for the audit trail |
reference | string | No | External reference ID (ticket number, internal note) |
Transfer Types
Each transfer type maps to an AdjustmentTypeEnum value stored in the journal metadata. The mapping is defined in TRANSFER_TYPE_TO_ADJUSTMENT_TYPE in InternalTransferService.ts.
| Transfer Type | Maps To (AdjustmentType) | Use Case |
|---|---|---|
PAYMENT_CORRECTION | MANUAL_ADJUSTMENT | Fix an incorrect payment amount or allocation |
FEE_ADJUSTMENT | FEE_CORRECTION | Correct a fee that was over/under-charged |
RESERVE_MOVEMENT | RESERVE_ADJUSTMENT | Move funds to/from rolling reserve accounts |
SERVICE_CREDIT | SERVICE_COMPENSATION | Credit a merchant for a service issue |
BALANCE_RECONCILIATION | BALANCE_RECONCILIATION | Reconcile a balance discrepancy |
TAX_ADJUSTMENT | TAX_CORRECTION | Correct a tax (IVA) calculation |
PRIOR_PERIOD_CORRECTION | PRIOR_PERIOD_CORRECTION | Load historical balance or correct a prior accounting period (source must be PRIOR_PERIOD_ADJUSTMENT equity) |
Response
| Field | Type | Description |
|---|---|---|
from_account_number | string | Source account |
to_account_number | string | Destination account |
amount | number | Transferred amount |
transfer_type | string | Transfer type used |
description | string | Description provided |
message | string | Confirmation message |
journals | IJournal[] | Created adjustment journals |
entries | ILedgerEntry[] | Created ledger entries (only when ledger_entries is enabled in the business config) |
Validations
- Source account exists: Queried via
account_number-indexGSI - Destination account exists: Queried via
AccountService.getAccountByAccountNumber() - Different accounts: Source and destination must have different account numbers (E004)
- Same currency: Both accounts must have the same
currency_code(E004) - Minimum amount: Must be >= 0.01 (schema validation)
Accounting Integration
The transfer always delegates to AccountingService.accountingPreparer() with these hardcoded values:
| Parameter | Value | Notes |
|---|---|---|
category | ADJUSTMENT | Routes to accountingAdjustmentPreparer |
adjustment_type | Mapped from transfer_type | Stored in journal metadata |
effect | DECREASE | Always — see explanation below |
account.PK / account.SK | Source account's PK/SK | Identifies the "target" of the adjustment |
contra_account_number | Destination account number | The other side of the entry |
gross_amount | Transfer amount | |
process_id | UUID v7 | Auto-generated |
entity_id | Source account's entity_id | |
currency_code | Shared currency |
Why effect is always DECREASE
Internal transfers do NOT expose an increase/decrease choice to the caller. The model is purely directional: from→to. The service hardcodes effect: "DECREASE" for the source account, which the AdjustmentEntriesGenerator translates to the correct ledger entry type based on each account's accounting nature:
| Source account type | Nature | effect: DECREASE produces | Effect on source balance |
|---|---|---|---|
| ASSET | Debit-normal | CREDIT entry on source | Balance decreases |
| EXPENSE | Debit-normal | CREDIT entry on source | Balance decreases |
| LIABILITY | Credit-normal | DEBIT entry on source | Balance decreases |
| REVENUE | Credit-normal | DEBIT entry on source | Balance decreases |
| EQUITY | Credit-normal | DEBIT entry on source | Balance decreases |
The contra (destination) account always receives the opposite entry type, which always increases its balance.
Net effect: source balance −= amount; destination balance += amount. The user/caller does not need to think about debits or credits — they think in terms of "from" and "to". The accounting service hides the double-entry math.
This is implemented in src/service/accounting/entries/AdjustmentEntriesGenerator.ts (mapEffectToEntryType).
Output
The accountingAdjustmentPreparer produces:
- One ADJUSTMENT journal with
process_id,entity_id,gross_amount,description, and metadata containingprocess_type,transfer_type, and optionalreference - Two ledger entries (one DEBIT, one CREDIT — order varies based on the source/destination types) when
ledger_entriesis enabled in the business config - Entries are sent to
AccountUpdateQueue(FIFO,MessageGroupId = account.id) for atomic balance updates
Use Cases
Payment Correction
A payment was allocated to the wrong business account.
Example: Move $500 MXN from BUSINESS_PAYABLE of merchant A back to ACQUIRER_RECEIVABLE of the acquirer that processed it.
Fee Adjustment
A merchant was overcharged on processing fees.
Example: Refund $50 MXN from PROCESSING_FEES_REVENUE to merchant's BUSINESS_PAYABLE.
Reserve Movement
Manually move funds in/out of a merchant's rolling reserve.
Example: Release $200 MXN from the merchant's RESERVE_PAYABLE to the merchant's BUSINESS_PAYABLE ahead of the scheduled rolling reserve job.
Service Credit
Compensate a merchant for a platform issue or SLA breach.
Example: Credit $100 MXN from ACQUIRER_FEES_EXPENSE (or another platform expense account) to merchant's BUSINESS_PAYABLE.
Balance Reconciliation
Fix a discrepancy found during reconciliation.
Example: Move $25 MXN from one merchant account to another to fix a misallocation discovered during month-end recon.
Tax Adjustment
Correct an IVA/VAT calculation error.
Example: Move $16 MXN from BUSINESS_PAYABLE to VAT_PAYABLE after discovering IVA was under-collected.
Prior Period Correction
Inject historical balance into an account that had no operational source for it.
Example: A merchant onboarded with $5,000 MXN of pre-existing balance from a legacy system. The amount does not come from any operational account (acquirer, fees, reserve), so the source must be the platform's equity account.
From: PRIOR_PERIOD_ADJUSTMENT (PLATFORM, EQUITY, MXN)
To: BUSINESS_PAYABLE of the merchant (MXN)
Amount: 5000
Transfer type: PRIOR_PERIOD_CORRECTION
Description: "Historical balance load — merchant onboarded from legacy system"Resulting balances:
PRIOR_PERIOD_ADJUSTMENT_MXN: 0 → −5000 (equity absorbs the historical correction)- Merchant
BUSINESS_PAYABLE: 0 → +5000 (merchant now reflects the legacy balance)
This is the only valid pattern for injecting funds into a merchant account without an operational source. Other transfer types require both the source and destination to be real operational accounts.
A
PRIOR_PERIOD_ADJUSTMENTaccount must exist for each currency before this transfer type can be used. It lives at the platform level (entity_id = "T1") and is created once as part of platform setup.
Migration from Adjustments
This endpoint replaces the deprecated POST /v1/accounts/{account_number}/adjustments. Key differences:
| Aspect | Old (Adjustments) | New (Transfers) |
|---|---|---|
| Endpoint | /v1/accounts/{account_number}/adjustments | /v1/internal-transfers |
| Scope | Single account with effect: INCREASE | DECREASE | Two accounts (source + destination), direction implicit |
| User-facing categories | None (raw adjustment types) | 7 transfer types mapped to adjustment types |
| Audit trail | Adjustment type only | Adjustment type + transfer type + reference + description |
| Handler | createAccountAdjustmentHandler (deleted) | createInternalTransferHandler |
| Service | AdjustmentsService (deleted) | InternalTransferService |
The old effect field is no longer needed because the source/destination direction encodes the same information: the source always decreases, the destination always increases. The accounting service computes the correct DEBIT/CREDIT entries automatically per account type.