Transfer Currency Normalization Layer¶
Table of contents¶
- Overview
- Backend storage format
- User input modes
- Design rationale
- Implementation architecture
- Normalization calculations
- Example transformation
- Error handling
- Testing
- Related documentation
Overview¶
The transfer currency normalization layer lets users specify currency for either the from_account or the to_account when creating transfers. It normalizes inputs to the backend standard format before validation and persistence.
Backend Storage Format¶
The Transfer table stores currency information in this format:
| Field | Description |
|---|---|
currency |
Always equals from_account currency |
currency_amount |
Amount in from_account currency |
amount |
Amount in to_account currency |
The backend enforces currency = from_account to keep conversion semantics unambiguous.
User Input Modes¶
Users can specify transfer amounts in three ways:
1. Amount Only, Inference¶
Specify only the amount field. The system infers currency based on account types:
{
"from_account": "TWH - Personal", // SGD, base currency
"to_account": "TWH IB USD", // USD
"amount": "200.00" // Inferred as SGD, base currency present
}
Inference rules:
- If base currency in either account -> user amount is in base currency
- If base in neither account -> user amount is in from_account currency
2. From-Currency Explicit, Pass Through¶
Specify currency + currency_amount matching the from_account:
{
"from_account": "TWH IB USD", // USD
"to_account": "TWH - Personal", // SGD
"currency": "USD", // Matches from_account
"currency_amount": "150.00" // from_amount
}
Behavior: Already in backend format -> passes through unchanged.
3. To-Currency Explicit, Normalized¶
Specify currency + currency_amount matching the to_account:
{
"from_account": "TWH - Personal", // SGD, base currency
"to_account": "TWH IB USD", // USD
"currency": "USD", // Matches to_account
"currency_amount": "100.00" // to_amount
}
Behavior: Normalized to backend format:
- Interpret
currency_amountasto_amount, 100.00 USD - Calculate
from_amountusing the inverse rate,from_amount = to_amount / forex_rate - Convert to standard form:
currency= "SGD", from_accountcurrency_amount= calculated from_amountamount= 100.00, to_amount
Design Rationale¶
Why Normalize to from_account?¶
The backend constraint currency = from_account provides:
- Unambiguous semantics: Always clear which account's currency is the reference
- Consistent querying: Filtering by currency always uses from_account perspective
- Simplified validation: Single constraint rule instead of multiple cases
Why Accept Both Specifications?¶
User convenience:
- From-account specification: Natural when user thinks "I'm sending X USD"
- To-account specification: Natural when user thinks "Recipient gets Y EUR"
- Either works: System handles both transparently
The normalization layer provides user flexibility while maintaining backend consistency.
Implementation Architecture¶
Layer 1: User Input, Flexible¶
Location: JSON parsing in CLI, cli/transfer.py
Accepts:
amountonly, optionalcurrency+currency_amount, optional and can match either account
Layer 2: Normalization, Conversion¶
Location: client.py, _infer_currency_for_transfer()
Logic:
-
If
currency+currency_amountspecified:- Validate:
currencymust match either from_account or to_account - If matches from_account, pass through
- If matches to_account, calculate inverse and swap to backend format
- Validate:
-
If only
amountspecified:- Apply inference rules, base currency priority
-
Return normalized TransferDTO
Layer 3: Validation, Constraint Enforcement¶
Location: client.py, _validate_transfer_currency_constraint()
Enforces: currency must equal from_account currency, backend constraint
This validation runs after normalization, so it always sees backend-formatted data.
Persistence stores backend-formatted data via repository insert_transfer.
Normalization Calculations¶
Same Currency Accounts¶
Foreign to Base Currency¶
Inverse, when normalizing to_amount to from_amount:
Base to Foreign Currency¶
Inverse, when normalizing to_amount to from_amount:
Foreign to Foreign Currency¶
Inverse, when normalizing to_amount to from_amount:
Example Transformation¶
User Input, to_account currency:
{
"date": "2026-02-22",
"from_account": "TWH - Personal", // SGD, base currency, rate = 1.0
"to_account": "TWH IB USD", // USD, rate = 0.74
"currency": "USD",
"currency_amount": "100.00",
"notes": "Normalized transfer"
}
Normalization Steps:
- Identify:
currency= USD matchesto_account, not from_account - Interpret:
currency_amount100.00 is the to_amount in USD - Calculate from_amount:
- Conversion: foreign USD to base SGD
- Formula:
from_amount = to_amount / forex_rate -
Calculation:
100.00 / 0.74 = 135.14SGD -
Normalize to backend format:
Error Handling¶
Over-Specification Error¶
Cannot specify both amount and currency_amount:
Error message:
Cannot specify both 'amount' and 'currency_amount'.
Provide either 'amount' alone for inference or 'currency' with 'currency_amount'.
Invalid Currency Error¶
Currency must match one of the transfer accounts:
{
"from_account": "TWH - Personal", // SGD
"to_account": "TWH IB USD", // USD
"currency": "EUR", // ERROR: matches neither
"currency_amount": "50.00"
}
Error message:
Transfer currency must match either from_account or to_account currency.
from_account uses SGD, to_account uses USD, but transfer specifies EUR.
Testing¶
Integration Test Coverage¶
The normalization layer is tested in:
tests/integration/test_currency_constraints.py::test_transfer_currency_must_match_from_account
This test verifies:
- ✅ To-currency specification is normalized correctly
- ✅ From-currency specification passes through unchanged
- ✅ Invalid currencies, matching neither account, are rejected
UAT Test Cases¶
Comprehensive batch transfer tests in:
Test coverage:
- Items 1-4: Amount-only inference, 4 cases
- Items 5-7: Explicit from-currency, 3 cases
- Items 8-10: Explicit to-currency with normalization, 3 cases
- Item 11: Invalid date format, parsing error test
See Test cases for detailed test case documentation.
Related Documentation¶
- Design, transfer currency semantics
- Schema, transfer table structure
- Test Strategy, inference testing approach
- Test Cases, UAT coverage