Skip to content

Internal Transfers Flow

Move funds between accounts with full audit trail via double-entry journals. Replaces the deprecated account adjustments endpoint.


Overview

AttributeValue
TriggerHTTP POST /v1/internal-transfers
Handlersrc/handler/createInternalTransferHandler.ts
Servicesrc/service/transfers/InternalTransferService.ts
Interfacesrc/repository/IInternalTransferService.ts
Delegates toAccountingService.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

FieldTypeRequiredDescription
from_account_numberstringYesSource account number — its balance will decrease
to_account_numberstringYesDestination account number — its balance will increase
amountnumberYesTransfer amount (minimum 0.01)
transfer_typeTransferTypeEnumYesUser-facing reason for the transfer
descriptionstringYesHuman-readable description for the audit trail
referencestringNoExternal 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 TypeMaps To (AdjustmentType)Use Case
PAYMENT_CORRECTIONMANUAL_ADJUSTMENTFix an incorrect payment amount or allocation
FEE_ADJUSTMENTFEE_CORRECTIONCorrect a fee that was over/under-charged
RESERVE_MOVEMENTRESERVE_ADJUSTMENTMove funds to/from rolling reserve accounts
SERVICE_CREDITSERVICE_COMPENSATIONCredit a merchant for a service issue
BALANCE_RECONCILIATIONBALANCE_RECONCILIATIONReconcile a balance discrepancy
TAX_ADJUSTMENTTAX_CORRECTIONCorrect a tax (IVA) calculation
PRIOR_PERIOD_CORRECTIONPRIOR_PERIOD_CORRECTIONLoad historical balance or correct a prior accounting period (source must be PRIOR_PERIOD_ADJUSTMENT equity)

Response

FieldTypeDescription
from_account_numberstringSource account
to_account_numberstringDestination account
amountnumberTransferred amount
transfer_typestringTransfer type used
descriptionstringDescription provided
messagestringConfirmation message
journalsIJournal[]Created adjustment journals
entriesILedgerEntry[]Created ledger entries (only when ledger_entries is enabled in the business config)

Validations

  1. Source account exists: Queried via account_number-index GSI
  2. Destination account exists: Queried via AccountService.getAccountByAccountNumber()
  3. Different accounts: Source and destination must have different account numbers (E004)
  4. Same currency: Both accounts must have the same currency_code (E004)
  5. Minimum amount: Must be >= 0.01 (schema validation)

Accounting Integration

The transfer always delegates to AccountingService.accountingPreparer() with these hardcoded values:

ParameterValueNotes
categoryADJUSTMENTRoutes to accountingAdjustmentPreparer
adjustment_typeMapped from transfer_typeStored in journal metadata
effectDECREASEAlways — see explanation below
account.PK / account.SKSource account's PK/SKIdentifies the "target" of the adjustment
contra_account_numberDestination account numberThe other side of the entry
gross_amountTransfer amount
process_idUUID v7Auto-generated
entity_idSource account's entity_id
currency_codeShared 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 typeNatureeffect: DECREASE producesEffect on source balance
ASSETDebit-normalCREDIT entry on sourceBalance decreases
EXPENSEDebit-normalCREDIT entry on sourceBalance decreases
LIABILITYCredit-normalDEBIT entry on sourceBalance decreases
REVENUECredit-normalDEBIT entry on sourceBalance decreases
EQUITYCredit-normalDEBIT entry on sourceBalance 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 containing process_type, transfer_type, and optional reference
  • Two ledger entries (one DEBIT, one CREDIT — order varies based on the source/destination types) when ledger_entries is 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_ADJUSTMENT account 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:

AspectOld (Adjustments)New (Transfers)
Endpoint/v1/accounts/{account_number}/adjustments/v1/internal-transfers
ScopeSingle account with effect: INCREASE | DECREASETwo accounts (source + destination), direction implicit
User-facing categoriesNone (raw adjustment types)7 transfer types mapped to adjustment types
Audit trailAdjustment type onlyAdjustment type + transfer type + reference + description
HandlercreateAccountAdjustmentHandler (deleted)createInternalTransferHandler
ServiceAdjustmentsService (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.

Vecnet — Build Spec v0.2 · Obsidian Terminal