mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-09-01 12:23:26 +00:00
fix(payments): the Ethiopian rails settle birr only
PROVIDER_CURRENCIES was permissive for everything except CBE_BILL, on the reasoning that the adapters pass input.currency through verbatim and nothing in them refuses a currency. That was the wrong place to look. The configuration says otherwise, and so does the domain: Telebirr is Ethio Telecom, CBE Birr is a Commercial Bank of Ethiopia wallet, and neither settles Djiboutian francs. The table as written would have let a DJF freight invoice be routed to one of them. payments.service.ts already states the rule above its chargeCurrency resolution — "WAAFI/DMONEY settle in DJF, CARD in USD, Ethiopian wallets in ETB" — and fix-payment-method-currency.ts corrected exactly WAAFI to DJF and CARD to USD, deliberately leaving the Ethiopian methods on the schema's ETB default. eBirr is configured as one of those: seeded region ETHIOPIA, never corrected. So TELEBIRR, CBE_BIRR, CBE_BILL and EBIRR are ETB-only. WAAFI, DMONEY and CAC_BANK stay open rather than pinned to DJF — that is their configured settlement currency, not a refusal of anything else, and passenger converts to each method's own currency before initiating regardless. CARD stays open too; its /100 minor-unit handling is a separate problem. Adds the spec that was missing: DJF must not reach a birr rail.
This commit is contained in:
@@ -0,0 +1,49 @@
|
||||
import {
|
||||
PROVIDER_CURRENCIES,
|
||||
ProviderMethod,
|
||||
providerSupportsCurrency,
|
||||
} from "@edr/types";
|
||||
|
||||
/**
|
||||
* Which gateway may be handed which currency. This started out permissive for every
|
||||
* provider but CBE_BILL, which would have let a DJF freight invoice be routed to Telebirr
|
||||
* or CBE Birr — Ethiopian birr rails that cannot settle francs.
|
||||
*/
|
||||
describe("provider settlement currencies", () => {
|
||||
const ETB_ONLY = [
|
||||
ProviderMethod.TELEBIRR,
|
||||
ProviderMethod.CBE_BIRR,
|
||||
ProviderMethod.CBE_BILL,
|
||||
ProviderMethod.EBIRR,
|
||||
];
|
||||
|
||||
it.each(ETB_ONLY)("refuses DJF on %s", (provider) => {
|
||||
expect(providerSupportsCurrency(provider, "DJF")).toBe(false);
|
||||
expect(providerSupportsCurrency(provider, "USD")).toBe(false);
|
||||
expect(providerSupportsCurrency(provider, "ETB")).toBe(true);
|
||||
});
|
||||
|
||||
it.each([ProviderMethod.WAAFI, ProviderMethod.DMONEY, ProviderMethod.CAC_BANK])(
|
||||
"settles DJF on %s",
|
||||
(provider) => {
|
||||
expect(providerSupportsCurrency(provider, "DJF")).toBe(true);
|
||||
},
|
||||
);
|
||||
|
||||
it("matches case-insensitively — currency columns are free strings", () => {
|
||||
expect(providerSupportsCurrency(ProviderMethod.CAC_BANK, "djf")).toBe(true);
|
||||
expect(providerSupportsCurrency(ProviderMethod.TELEBIRR, "djf")).toBe(false);
|
||||
});
|
||||
|
||||
it("refuses a currency the platform does not bill in at all", () => {
|
||||
expect(providerSupportsCurrency(ProviderMethod.CAC_BANK, "EUR")).toBe(false);
|
||||
expect(providerSupportsCurrency(ProviderMethod.CAC_BANK, null)).toBe(false);
|
||||
expect(providerSupportsCurrency(ProviderMethod.CAC_BANK, undefined)).toBe(false);
|
||||
});
|
||||
|
||||
it("covers every provider, so a new one cannot default to unrestricted", () => {
|
||||
for (const provider of Object.values(ProviderMethod)) {
|
||||
expect(PROVIDER_CURRENCIES[provider]?.length).toBeGreaterThan(0);
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -32,7 +32,6 @@ export enum ProviderMethod {
|
||||
CBE_BILL = "CBE_BILL",
|
||||
}
|
||||
|
||||
|
||||
export type PaymentPlatform = "web" | "mobile" | "inapp";
|
||||
|
||||
export type ClientAction =
|
||||
@@ -141,7 +140,6 @@ export enum PaymentReferenceType {
|
||||
SUPPLEMENTARY_CHARGE = "SUPPLEMENTARY_CHARGE",
|
||||
}
|
||||
|
||||
|
||||
/** Body of `POST /payments/initiate` on the payment service (internal, service-authenticated). */
|
||||
export interface InitiatePaymentRequest {
|
||||
service: PaymentService;
|
||||
@@ -186,7 +184,7 @@ export interface ConfirmPaymentRequest {
|
||||
}
|
||||
|
||||
/** Response of `POST /payments/initiate` and shape of intent lookups. */
|
||||
export type PaymentIntentSnapshot ={
|
||||
export type PaymentIntentSnapshot = {
|
||||
intentId: string;
|
||||
service: PaymentService;
|
||||
referenceType: PaymentReferenceType;
|
||||
@@ -211,7 +209,7 @@ export type PaymentIntentSnapshot ={
|
||||
* for state decisions — the state machine drives `status`.
|
||||
*/
|
||||
providerResponse?: Record<string, unknown>;
|
||||
}
|
||||
};
|
||||
|
||||
export type PaymentEventType = "payment.succeeded" | "payment.failed";
|
||||
|
||||
@@ -261,10 +259,7 @@ export type PaymentCurrency = (typeof PAYMENT_CURRENCIES)[number];
|
||||
|
||||
/** Narrowing guard for the free-string `currency` columns the ORMs hand back. */
|
||||
export function isPaymentCurrency(value: unknown): value is PaymentCurrency {
|
||||
return (
|
||||
typeof value === "string" &&
|
||||
(PAYMENT_CURRENCIES as readonly string[]).includes(value)
|
||||
);
|
||||
return typeof value === "string" && (PAYMENT_CURRENCIES as readonly string[]).includes(value);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -280,25 +275,34 @@ export const CURRENCY_DECIMALS: Record<PaymentCurrency, number> = {
|
||||
/**
|
||||
* Which currencies each gateway will settle.
|
||||
*
|
||||
* Only CBE_BILL carries a restriction we can actually evidence: CBE settles ETB and the
|
||||
* adapter says so (docs/cbe/CBE_IMPLEMENTATION_PLAN.md D8). Every other adapter passes
|
||||
* `input.currency` to the provider verbatim — eBirr, Waafi and D-Money all carry an explicit
|
||||
* "charge exactly the currency the caller already converted to" comment — so no other row can
|
||||
* be tightened without inventing a rule the code does not have.
|
||||
* The Ethiopian rails settle birr and nothing else — Telebirr is Ethio Telecom, CBE Birr
|
||||
* and CBE Bill are Commercial Bank of Ethiopia, and eBirr is configured as an Ethiopian
|
||||
* method too (seeded `region: ETHIOPIA`, and `fix-payment-method-currency.ts` deliberately
|
||||
* left it on the schema's ETB default while correcting Waafi). Handing any of them a DJF
|
||||
* order is a routing mistake, not a conversion the provider will make for us.
|
||||
*
|
||||
* ponytail: permissive on purpose. Settlement currency is really a property of the merchant
|
||||
* account, not the provider brand; tighten a row here once ops confirms what its merchant
|
||||
* account actually accepts, rather than guessing now and breaking a live flow.
|
||||
* The Djibouti-side rails are the ones that settle francs: Waafi and D-Money are configured
|
||||
* to DJF, and CAC Bank both defaults to it and enforces DJF's 10–100,000 bounds in its own
|
||||
* adapter. They stay open to the other currencies rather than pinned to DJF — nothing in the
|
||||
* code says they refuse them, and passenger already converts to each method's own settlement
|
||||
* currency before it initiates, so it never sends them anything else anyway.
|
||||
*
|
||||
* CARD is left unrestricted: card gateways are normally multi-currency, and its `/100`
|
||||
* minor-unit handling is a separate pre-existing problem from currency routing.
|
||||
*
|
||||
* ponytail: settlement currency is really a property of the merchant account. Tighten a row
|
||||
* when ops confirms one, and leave it open rather than guessing — a guess here either
|
||||
* refuses a live payment or routes one to a rail that cannot take it.
|
||||
*/
|
||||
export const PROVIDER_CURRENCIES: Record<ProviderMethod, readonly PaymentCurrency[]> = {
|
||||
[ProviderMethod.CBE_BILL]: ["ETB"],
|
||||
[ProviderMethod.CBE_BIRR]: PAYMENT_CURRENCIES,
|
||||
[ProviderMethod.TELEBIRR]: PAYMENT_CURRENCIES,
|
||||
[ProviderMethod.CARD]: PAYMENT_CURRENCIES,
|
||||
[ProviderMethod.EBIRR]: PAYMENT_CURRENCIES,
|
||||
[ProviderMethod.CBE_BIRR]: ["ETB"],
|
||||
[ProviderMethod.TELEBIRR]: ["ETB"],
|
||||
[ProviderMethod.EBIRR]: ["ETB"],
|
||||
[ProviderMethod.WAAFI]: PAYMENT_CURRENCIES,
|
||||
[ProviderMethod.DMONEY]: PAYMENT_CURRENCIES,
|
||||
[ProviderMethod.CAC_BANK]: PAYMENT_CURRENCIES,
|
||||
[ProviderMethod.CARD]: PAYMENT_CURRENCIES,
|
||||
};
|
||||
|
||||
/** Whether `provider` can settle `currency`. Unknown currencies are never settleable. */
|
||||
|
||||
Reference in New Issue
Block a user