@gomani/pay
v0.7.0
Published
Mobile-money-first payments: a provider-agnostic, idempotent, offline-tolerant payment abstraction.
Maintainers
Readme
@gomani/pay
Mobile-money-first, provider-agnostic payments for Gomani (P9/P10). Payments are an integrated,
pre-optimised concern: an idempotent, offline-tolerant engine over a pluggable PaymentProvider, a
web-standard endpoint to mount, and low-literacy UI primitives — with mobile money (USSD / STK-push,
deep links) as the first-class case and cards secondary.
Posture: pass-through. The app holds no funds and never touches card data — money moves between the payer and the provider, and KYC / fraud / settlement / licensing are the provider's job. Compliance rests with the developer and their provider; Gomani carries the integration burden.
The guarantee
The idempotency key is the contract: the same key is the same payment forever. A dropped connection mid-payment can be retried freely and will never double-charge or lose state, because:
- the record is persisted before the provider is called (a crash leaves a recoverable intent);
- a repeated
initiatewith a known key reconciles rather than re-charging; - a terminal state is final — a replayed webhook or a late status poll is a no-op, so the fulfilment hook fires exactly once.
Server (@gomani/pay)
import {
createPaymentEngine,
createPaymentHandler,
createMockProvider, // swap for createMobileMoneyProvider({ baseUrl, apiKey, webhookSecret })
} from '@gomani/pay';
const engine = createPaymentEngine({
provider: createMockProvider(),
onTransition: (record) => {
if (record.status === 'succeeded') grantAccess(record.reference); // fires once
},
});
// In app/server.ts — mounts /_gomani/pay/{initiate,status,webhook}:
const pay = createPaymentHandler(engine);
export default (request: Request) => pay(request);Providers: createMockProvider (local default, for dev/tests — simulates a USSD/STK approval and can
mint signed webhooks), createMobileMoneyProvider (a generic STK-push/USSD adapter scaffold), and
createAggregatorProvider (a corridor-collapsing adapter that routes by MSISDN prefix). Real
endpoints and secrets are configuration; fetch is injected via the provider context.
Bring your own storage by implementing PaymentStore (an in-memory one ships; Postgres is a drop-in).
Client (@gomani/pay/client)
import { createPaymentClient, PaymentStatusView } from '@gomani/pay/client';
const client = createPaymentClient(); // talks to /_gomani/pay
client.pay({
amount: { amount: 150000, currency: 'MWK' },
reference: order.id,
customer: { phone },
});
// render PaymentStatusView({ record: client.payment.value }) in a reactive regioncreatePaymentClient derives a stable idempotency key from the order reference (persisted before the
first request), initiates, and polls status until the payment settles — pausing while offline and
resuming when connectivity returns. PaymentStatusView renders the low-literacy status card, including a
big tap-to-dial USSD code when the payer must act.
See examples/shop for a complete checkout.
