@momorail/pawapay 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Momorail contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,118 @@
1
+ import { IdempotencyStore, PaymentProvider, ProviderCapabilities, CollectionInput, ProviderCallOptions, Transaction, DisbursementInput, TransactionRef, RawWebhook, WebhookEvent, Operator } from '@momorail/core';
2
+
3
+ type PawapayMode = 'live' | 'sandbox';
4
+ interface PawapayOptions {
5
+ /** pawaPay API token (JWT) — sent as `Authorization: Bearer <token>`. */
6
+ apiToken: string;
7
+ /** `'sandbox'` (default) or `'live'` — selects the base URL. */
8
+ mode?: PawapayMode;
9
+ /** Override the API base URL (tests). */
10
+ baseUrl?: string;
11
+ /** Per-request timeout in ms. Default 15000. */
12
+ timeoutMs?: number;
13
+ /** Injectable fetch implementation. Default the global `fetch`. */
14
+ fetch?: typeof fetch;
15
+ /** Id generator for `depositId` / `payoutId` (UUID v4). Default `crypto.randomUUID`. */
16
+ generateId?: () => string;
17
+ /** Where the adapter remembers created transactions. Default: in-memory, per process. */
18
+ idempotencyStore?: IdempotencyStore;
19
+ }
20
+ /**
21
+ * pawaPay adapter — direct mobile-money deposits and payouts across Sub-Saharan
22
+ * Africa. The API is asynchronous: `POST /v2/deposits` returns `ACCEPTED` and the
23
+ * transaction stays `processing` until a callback or a `GET` poll reports
24
+ * `COMPLETED` / `FAILED`. The customer approves the deposit via their operator's
25
+ * own prompt, so there is no `checkoutUrl`.
26
+ *
27
+ * pawaPay ids are merchant-generated UUIDs, so the adapter mints a `depositId` /
28
+ * `payoutId`, uses it as `Transaction.id`, keeps a `reference → Transaction`
29
+ * cache, and sets `clientReferenceId` + `metadata` so the caller reference is
30
+ * recoverable across processes.
31
+ */
32
+ declare class PawapayProvider implements PaymentProvider {
33
+ #private;
34
+ readonly id = "pawapay";
35
+ constructor(options: PawapayOptions);
36
+ capabilities(): ProviderCapabilities;
37
+ collection(input: CollectionInput, options?: ProviderCallOptions): Promise<Transaction>;
38
+ disbursement(input: DisbursementInput, options?: ProviderCallOptions): Promise<Transaction>;
39
+ getTransaction(ref: TransactionRef, options?: ProviderCallOptions): Promise<Transaction>;
40
+ parseWebhook(raw: RawWebhook): Promise<WebhookEvent>;
41
+ }
42
+
43
+ /**
44
+ * Raw pawaPay v2 Merchant API shapes. Only the fields Momorail reads are
45
+ * modelled; the untouched payload is always returned on `Transaction.providerRaw`.
46
+ *
47
+ * @see https://docs.pawapay.io/v2/api-reference
48
+ */
49
+ interface PawapayFailureReason {
50
+ failureCode?: string;
51
+ failureMessage?: string;
52
+ }
53
+ interface PawapayMmoAccount {
54
+ type: 'MMO';
55
+ accountDetails: {
56
+ phoneNumber: string;
57
+ provider: string;
58
+ };
59
+ }
60
+ /** Synchronous response to `POST /v2/deposits` and `POST /v2/payouts`. */
61
+ interface PawapayAcceptResponse {
62
+ depositId?: string;
63
+ payoutId?: string;
64
+ /** `ACCEPTED` | `REJECTED` | `DUPLICATE_IGNORED` */
65
+ status: string;
66
+ created?: string;
67
+ failureReason?: PawapayFailureReason;
68
+ }
69
+ /** `GET /v2/deposits/{depositId}` / `GET /v2/payouts/{payoutId}`. */
70
+ interface PawapayTransactionResponse {
71
+ depositId?: string;
72
+ payoutId?: string;
73
+ /** `ACCEPTED` | `PROCESSING` | `IN_RECONCILIATION` | `COMPLETED` | `FAILED` */
74
+ status: string;
75
+ amount?: string;
76
+ currency?: string;
77
+ payer?: PawapayMmoAccount;
78
+ recipient?: PawapayMmoAccount;
79
+ customerMessage?: string;
80
+ clientReferenceId?: string;
81
+ providerTransactionId?: string;
82
+ created?: string;
83
+ failureReason?: PawapayFailureReason;
84
+ metadata?: Array<Record<string, unknown>>;
85
+ }
86
+
87
+ /**
88
+ * Momorail operator → pawaPay `provider` code. pawaPay scopes providers by
89
+ * ISO-3166 alpha-3 country. Confirm the exact codes for your markets against
90
+ * pawaPay's `GET /v2/active-conf`.
91
+ */
92
+ declare const PAWAPAY_PROVIDER: Partial<Record<Operator, string>>;
93
+
94
+ /**
95
+ * pawaPay signs callbacks with RFC-9421 HTTP Message Signatures using an
96
+ * asymmetric key (ECDSA / RSASSA-PSS). Full verification needs the public key
97
+ * from pawaPay's Public Keys endpoint and an RFC-9421 implementation — a planned
98
+ * follow-up.
99
+ *
100
+ * What this adapter checks today is the `Content-Digest` header: a SHA-256 /
101
+ * SHA-512 hash of the raw body. It proves the body was not altered in transit,
102
+ * and the adapter then re-fetches the authoritative status from the API, so a
103
+ * forged callback only triggers a status re-check.
104
+ */
105
+ declare function verifyContentDigest(headers: Record<string, string | string[] | undefined>, rawBody: string): void;
106
+ interface PawapayCallback {
107
+ depositId?: string;
108
+ payoutId?: string;
109
+ refundId?: string;
110
+ status?: string;
111
+ [key: string]: unknown;
112
+ }
113
+ declare function parseCallbackBody(body: string | Uint8Array | Record<string, unknown>): {
114
+ fields: PawapayCallback;
115
+ raw: string;
116
+ };
117
+
118
+ export { PAWAPAY_PROVIDER, type PawapayAcceptResponse, type PawapayCallback, type PawapayMode, type PawapayOptions, PawapayProvider, type PawapayTransactionResponse, parseCallbackBody, verifyContentDigest };
package/dist/index.js ADDED
@@ -0,0 +1,384 @@
1
+ // src/provider.ts
2
+ import {
3
+ IdempotencyCache,
4
+ MemoryIdempotencyStore,
5
+ ValidationError,
6
+ isOperator,
7
+ webhookEventTypeForStatus
8
+ } from "@momorail/core";
9
+
10
+ // src/http.ts
11
+ import {
12
+ AuthError,
13
+ ProviderUnavailableError,
14
+ RateLimitError,
15
+ TransactionNotFoundError
16
+ } from "@momorail/core";
17
+ var PROVIDER_ID = "pawapay";
18
+ var SANDBOX_BASE_URL = "https://api.sandbox.pawapay.io";
19
+ var PRODUCTION_BASE_URL = "https://api.pawapay.io";
20
+ var HttpClient = class {
21
+ #baseUrl;
22
+ #timeoutMs;
23
+ #fetch;
24
+ #apiToken;
25
+ constructor(options) {
26
+ this.#baseUrl = options.baseUrl.replace(/\/$/, "");
27
+ this.#timeoutMs = options.timeoutMs;
28
+ this.#fetch = options.fetch;
29
+ this.#apiToken = options.apiToken;
30
+ }
31
+ postJson(path, body, signal) {
32
+ return this.#request("POST", path, body, signal);
33
+ }
34
+ getJson(path, signal) {
35
+ return this.#request("GET", path, void 0, signal);
36
+ }
37
+ async #request(method, path, body, signal) {
38
+ const timeout = AbortSignal.timeout(this.#timeoutMs);
39
+ const composite = signal ? AbortSignal.any([signal, timeout]) : timeout;
40
+ const headers = {
41
+ accept: "application/json",
42
+ authorization: `Bearer ${this.#apiToken}`
43
+ };
44
+ if (method === "POST") headers["content-type"] = "application/json";
45
+ let response;
46
+ try {
47
+ response = await this.#fetch(`${this.#baseUrl}${path}`, {
48
+ method,
49
+ headers,
50
+ body: method === "POST" ? JSON.stringify(body) : void 0,
51
+ signal: composite
52
+ });
53
+ } catch (cause) {
54
+ if (signal?.aborted) throw cause;
55
+ throw new ProviderUnavailableError("pawaPay request failed to complete", {
56
+ provider: PROVIDER_ID,
57
+ cause
58
+ });
59
+ }
60
+ if (response.status === 401 || response.status === 403) {
61
+ throw new AuthError(`pawaPay rejected the API token (${response.status})`, {
62
+ provider: PROVIDER_ID
63
+ });
64
+ }
65
+ if (response.status === 404) {
66
+ throw new TransactionNotFoundError("pawaPay has no such transaction", {
67
+ provider: PROVIDER_ID
68
+ });
69
+ }
70
+ if (response.status === 429) {
71
+ throw new RateLimitError("pawaPay throttled the request", { provider: PROVIDER_ID });
72
+ }
73
+ const raw = await response.text();
74
+ let parsed;
75
+ try {
76
+ parsed = raw ? JSON.parse(raw) : {};
77
+ } catch (cause) {
78
+ throw new ProviderUnavailableError("pawaPay returned a non-JSON response", {
79
+ provider: PROVIDER_ID,
80
+ providerRaw: raw,
81
+ cause
82
+ });
83
+ }
84
+ if (response.status >= 500) {
85
+ throw new ProviderUnavailableError(`pawaPay responded ${response.status}`, {
86
+ provider: PROVIDER_ID,
87
+ providerRaw: parsed
88
+ });
89
+ }
90
+ return parsed;
91
+ }
92
+ };
93
+
94
+ // src/mapping.ts
95
+ import {
96
+ MomorailError,
97
+ ProviderUnavailableError as ProviderUnavailableError2
98
+ } from "@momorail/core";
99
+ var PAWAPAY_PROVIDER = {
100
+ orange_ci: "ORANGE_CIV",
101
+ mtn_ci: "MTN_MOMO_CIV",
102
+ moov_ci: "MOOV_CIV",
103
+ wave_ci: "WAVE_CIV",
104
+ orange_sn: "ORANGE_SEN",
105
+ free_sn: "FREE_SEN",
106
+ wave_sn: "WAVE_SEN",
107
+ orange_ml: "ORANGE_MLI",
108
+ moov_ml: "MOOV_MLI",
109
+ orange_bf: "ORANGE_BFA",
110
+ moov_bf: "MOOV_BFA",
111
+ mtn_bj: "MTN_MOMO_BEN",
112
+ moov_bj: "MOOV_BEN",
113
+ orange_ne: "ORANGE_NER",
114
+ moov_ne: "MOOV_NER",
115
+ moov_tg: "MOOV_TGO",
116
+ togocom_tg: "TOGOCOM_TGO",
117
+ orange_cd: "ORANGE_COD",
118
+ airtel_cd: "AIRTEL_COD",
119
+ vodacom_cd: "VODACOM_COD"
120
+ };
121
+ function mapTransactionStatus(status) {
122
+ switch (status?.toUpperCase()) {
123
+ case "COMPLETED":
124
+ return "succeeded";
125
+ case "FAILED":
126
+ return "failed";
127
+ case "ACCEPTED":
128
+ case "PROCESSING":
129
+ case "IN_RECONCILIATION":
130
+ return "processing";
131
+ default:
132
+ return "unknown";
133
+ }
134
+ }
135
+ var FAILURE_REASONS = {
136
+ INSUFFICIENT_BALANCE: "insufficient_funds",
137
+ PAYER_LIMIT_REACHED: "limit_exceeded",
138
+ RECIPIENT_LIMIT_REACHED: "limit_exceeded",
139
+ PAYER_NOT_FOUND: "invalid_number",
140
+ RECIPIENT_NOT_FOUND: "invalid_number",
141
+ INVALID_PAYER_FORMAT: "invalid_number",
142
+ UNSPECIFIED_FAILURE: "provider_error",
143
+ PROVIDER_TEMPORARILY_UNAVAILABLE: "provider_error",
144
+ PAYER_DECLINED: "user_declined",
145
+ USER_CANCELLED: "user_declined",
146
+ TRANSACTION_TIMED_OUT: "timeout"
147
+ };
148
+ function mapFailureReason(reason) {
149
+ return FAILURE_REASONS[reason?.failureCode ?? ""] ?? "other";
150
+ }
151
+ function rejectedError(res) {
152
+ const code = res.failureReason?.failureCode ?? "REJECTED";
153
+ const message = res.failureReason?.failureMessage ?? "pawaPay rejected the request";
154
+ if (/UNAVAILABLE|TEMPORARILY/.test(code)) {
155
+ return new ProviderUnavailableError2(`pawaPay is unavailable (${code}: ${message})`, {
156
+ provider: PROVIDER_ID,
157
+ providerRaw: res
158
+ });
159
+ }
160
+ return new MomorailError(`pawaPay rejected the request (${code}: ${message})`, "provider_error", {
161
+ provider: PROVIDER_ID,
162
+ providerRaw: res
163
+ });
164
+ }
165
+
166
+ // src/webhook.ts
167
+ import { createHash } from "crypto";
168
+ import { WebhookVerificationError } from "@momorail/core";
169
+ function verifyContentDigest(headers, rawBody) {
170
+ const header = headerValue(headers, "content-digest");
171
+ if (!header) return;
172
+ const match = /(sha-256|sha-512)=:([A-Za-z0-9+/=]+):/i.exec(header);
173
+ if (!match) {
174
+ throw new WebhookVerificationError("Unparseable Content-Digest on pawaPay callback", {
175
+ provider: PROVIDER_ID
176
+ });
177
+ }
178
+ const algo = match[1]?.toLowerCase() === "sha-512" ? "sha512" : "sha256";
179
+ const expected = createHash(algo).update(rawBody, "utf8").digest("base64");
180
+ if (match[2] !== expected) {
181
+ throw new WebhookVerificationError("pawaPay callback body does not match its Content-Digest", {
182
+ provider: PROVIDER_ID
183
+ });
184
+ }
185
+ }
186
+ function parseCallbackBody(body) {
187
+ if (typeof body === "string") {
188
+ return { fields: JSON.parse(body || "{}"), raw: body };
189
+ }
190
+ if (body instanceof Uint8Array) {
191
+ const raw = new TextDecoder().decode(body);
192
+ return { fields: JSON.parse(raw || "{}"), raw };
193
+ }
194
+ return { fields: body, raw: JSON.stringify(body) };
195
+ }
196
+ function headerValue(headers, name) {
197
+ const found = headers[name] ?? headers[name.toLowerCase()] ?? Object.entries(headers).find(([k]) => k.toLowerCase() === name.toLowerCase())?.[1];
198
+ return Array.isArray(found) ? found[0] : found;
199
+ }
200
+
201
+ // src/provider.ts
202
+ var DEFAULT_TIMEOUT_MS = 15e3;
203
+ var REFERENCE_KEY = "momorailReference";
204
+ var CAPABILITIES = {
205
+ collection: true,
206
+ disbursement: true,
207
+ // pawaPay looks a transaction up by the merchant-generated deposit/payout id.
208
+ lookupByReference: false,
209
+ // Direct deposits push a prompt to the customer's phone — no hosted page.
210
+ hostedCheckout: false,
211
+ operators: Object.keys(PAWAPAY_PROVIDER),
212
+ currencies: ["XOF"]
213
+ };
214
+ var PawapayProvider = class {
215
+ id = PROVIDER_ID;
216
+ #http;
217
+ #generateId;
218
+ #cache;
219
+ constructor(options) {
220
+ if (!options.apiToken?.trim()) {
221
+ throw new ValidationError('PawapayProvider requires a non-empty "apiToken"');
222
+ }
223
+ const mode = options.mode ?? "sandbox";
224
+ this.#generateId = options.generateId ?? (() => crypto.randomUUID());
225
+ this.#cache = new IdempotencyCache(
226
+ options.idempotencyStore ?? new MemoryIdempotencyStore(),
227
+ this.id
228
+ );
229
+ this.#http = new HttpClient({
230
+ baseUrl: options.baseUrl ?? (mode === "live" ? PRODUCTION_BASE_URL : SANDBOX_BASE_URL),
231
+ timeoutMs: options.timeoutMs ?? DEFAULT_TIMEOUT_MS,
232
+ fetch: options.fetch ?? globalThis.fetch,
233
+ apiToken: options.apiToken
234
+ });
235
+ }
236
+ capabilities() {
237
+ return CAPABILITIES;
238
+ }
239
+ async collection(input, options) {
240
+ return this.#initiate("collection", input, input.customer, options);
241
+ }
242
+ async disbursement(input, options) {
243
+ return this.#initiate("disbursement", input, input.recipient, options);
244
+ }
245
+ async getTransaction(ref, options) {
246
+ const known = (ref.id ? await this.#cache.byId(ref.id) : void 0) ?? (ref.reference ? await this.#cache.byReference(ref.reference) : void 0);
247
+ const id = ref.id ?? known?.id;
248
+ if (!id) {
249
+ throw new ValidationError(
250
+ "pawaPay looks a transaction up by its deposit/payout id (the provider id); this reference is not known to this instance",
251
+ { provider: this.id }
252
+ );
253
+ }
254
+ const type = known?.type ?? "collection";
255
+ const path = type === "disbursement" ? `/v2/payouts/${id}` : `/v2/deposits/${id}`;
256
+ const res = await this.#http.getJson(path, options?.signal);
257
+ return await this.#toTransaction(type, res, { known, id });
258
+ }
259
+ async parseWebhook(raw) {
260
+ const { fields, raw: rawBody } = parseCallbackBody(raw.body);
261
+ verifyContentDigest(raw.headers, rawBody);
262
+ const depositId = fields.depositId;
263
+ const payoutId = fields.payoutId;
264
+ const id = depositId ?? payoutId;
265
+ if (!id) {
266
+ throw new ValidationError("pawaPay callback carries neither a depositId nor a payoutId", {
267
+ provider: this.id
268
+ });
269
+ }
270
+ const known = await this.#cache.byId(id);
271
+ const type = payoutId ? "disbursement" : "collection";
272
+ const path = type === "disbursement" ? `/v2/payouts/${id}` : `/v2/deposits/${id}`;
273
+ const res = await this.#http.getJson(path);
274
+ const transaction = await this.#toTransaction(type, res, { known, id });
275
+ return {
276
+ type: webhookEventTypeForStatus(transaction.status),
277
+ transaction,
278
+ providerRaw: fields
279
+ };
280
+ }
281
+ async #initiate(type, input, party, options) {
282
+ const operator = party.operator;
283
+ if (!operator || !isOperator(operator)) {
284
+ throw new ValidationError(`pawaPay ${type} needs a known operator`, { provider: this.id });
285
+ }
286
+ const providerCode = PAWAPAY_PROVIDER[operator];
287
+ if (!providerCode) {
288
+ throw new ValidationError(`pawaPay has no provider code for operator "${operator}"`, {
289
+ provider: this.id
290
+ });
291
+ }
292
+ const cached = await this.#cache.byReference(input.reference);
293
+ if (cached) return structuredClone(cached);
294
+ const id = this.#generateId();
295
+ const account = {
296
+ type: "MMO",
297
+ accountDetails: { phoneNumber: party.phone.replace(/\D/g, ""), provider: providerCode }
298
+ };
299
+ const body = {
300
+ [type === "disbursement" ? "payoutId" : "depositId"]: id,
301
+ amount: String(input.amount.amount),
302
+ currency: input.amount.currency,
303
+ [type === "disbursement" ? "recipient" : "payer"]: account,
304
+ customerMessage: customerMessage(input.description),
305
+ clientReferenceId: input.reference,
306
+ metadata: [
307
+ { [REFERENCE_KEY]: input.reference },
308
+ ...Object.entries(input.metadata ?? {}).map(([k, v]) => ({ [k]: v }))
309
+ ]
310
+ };
311
+ const path = type === "disbursement" ? "/v2/payouts" : "/v2/deposits";
312
+ const res = await this.#http.postJson(path, body, options?.signal);
313
+ if (res.status === "REJECTED") throw rejectedError(res);
314
+ if (res.status === "DUPLICATE_IGNORED") {
315
+ const fetched = await this.#http.getJson(
316
+ `${path}/${id}`,
317
+ options?.signal
318
+ );
319
+ return await this.#toTransaction(type, fetched, { id, input, operator, party });
320
+ }
321
+ const now = res.created ?? (/* @__PURE__ */ new Date()).toISOString();
322
+ const txn = {
323
+ provider: this.id,
324
+ id,
325
+ reference: input.reference,
326
+ type,
327
+ status: "processing",
328
+ amount: input.amount,
329
+ operator,
330
+ customer: party,
331
+ providerRaw: res,
332
+ createdAt: now,
333
+ updatedAt: now
334
+ };
335
+ await this.#cache.remember(txn);
336
+ return structuredClone(txn);
337
+ }
338
+ async #toTransaction(type, res, ctx) {
339
+ const status = mapTransactionStatus(res.status);
340
+ const echoed = metadataReference(res.metadata) ?? res.clientReferenceId;
341
+ const now = (/* @__PURE__ */ new Date()).toISOString();
342
+ const txn = {
343
+ provider: this.id,
344
+ id: ctx.id,
345
+ reference: echoed ?? ctx.known?.reference ?? ctx.input?.reference ?? ctx.id,
346
+ type,
347
+ status,
348
+ amount: ctx.known?.amount ?? ctx.input?.amount ?? coerceAmount(res.amount, res.currency),
349
+ operator: ctx.known?.operator ?? ctx.operator,
350
+ customer: ctx.known?.customer ?? ctx.party,
351
+ failureReason: status === "failed" ? mapFailureReason(res.failureReason) : void 0,
352
+ providerRaw: res,
353
+ createdAt: ctx.known?.createdAt ?? res.created ?? now,
354
+ updatedAt: now
355
+ };
356
+ await this.#cache.remember(txn);
357
+ return structuredClone(txn);
358
+ }
359
+ };
360
+ function customerMessage(description) {
361
+ const cleaned = (description ?? "Payment").replace(/[^a-zA-Z0-9]/g, "");
362
+ return cleaned.length >= 4 ? cleaned.slice(0, 22) : "Payment";
363
+ }
364
+ function metadataReference(metadata) {
365
+ for (const entry of metadata ?? []) {
366
+ const value = entry[REFERENCE_KEY];
367
+ if (typeof value === "string") return value;
368
+ }
369
+ return void 0;
370
+ }
371
+ function coerceAmount(raw, currency) {
372
+ const amount = raw ? Number.parseInt(raw, 10) : 0;
373
+ return {
374
+ amount: Number.isFinite(amount) ? amount : 0,
375
+ currency: currency === "XAF" ? "XAF" : "XOF"
376
+ };
377
+ }
378
+ export {
379
+ PAWAPAY_PROVIDER,
380
+ PawapayProvider,
381
+ parseCallbackBody,
382
+ verifyContentDigest
383
+ };
384
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/provider.ts","../src/http.ts","../src/mapping.ts","../src/webhook.ts"],"sourcesContent":["import {\n type CollectionInput,\n type DisbursementInput,\n IdempotencyCache,\n type IdempotencyStore,\n MemoryIdempotencyStore,\n type Money,\n type Operator,\n type Party,\n type PaymentProvider,\n type ProviderCallOptions,\n type ProviderCapabilities,\n type RawWebhook,\n type Transaction,\n type TransactionRef,\n type TransactionType,\n ValidationError,\n type WebhookEvent,\n isOperator,\n webhookEventTypeForStatus,\n} from '@momorail/core';\nimport { HttpClient, PRODUCTION_BASE_URL, PROVIDER_ID, SANDBOX_BASE_URL } from './http.js';\nimport {\n PAWAPAY_PROVIDER,\n mapFailureReason,\n mapTransactionStatus,\n rejectedError,\n} from './mapping.js';\nimport type {\n PawapayAcceptResponse,\n PawapayMmoAccount,\n PawapayTransactionResponse,\n} from './types.js';\nimport { parseCallbackBody, verifyContentDigest } from './webhook.js';\n\nconst DEFAULT_TIMEOUT_MS = 15_000;\nconst REFERENCE_KEY = 'momorailReference';\n\nexport type PawapayMode = 'live' | 'sandbox';\n\nexport interface PawapayOptions {\n /** pawaPay API token (JWT) — sent as `Authorization: Bearer <token>`. */\n apiToken: string;\n /** `'sandbox'` (default) or `'live'` — selects the base URL. */\n mode?: PawapayMode;\n /** Override the API base URL (tests). */\n baseUrl?: string;\n /** Per-request timeout in ms. Default 15000. */\n timeoutMs?: number;\n /** Injectable fetch implementation. Default the global `fetch`. */\n fetch?: typeof fetch;\n /** Id generator for `depositId` / `payoutId` (UUID v4). Default `crypto.randomUUID`. */\n generateId?: () => string;\n /** Where the adapter remembers created transactions. Default: in-memory, per process. */\n idempotencyStore?: IdempotencyStore;\n}\n\nconst CAPABILITIES: ProviderCapabilities = {\n collection: true,\n disbursement: true,\n // pawaPay looks a transaction up by the merchant-generated deposit/payout id.\n lookupByReference: false,\n // Direct deposits push a prompt to the customer's phone — no hosted page.\n hostedCheckout: false,\n operators: Object.keys(PAWAPAY_PROVIDER) as Operator[],\n currencies: ['XOF'],\n};\n\n/**\n * pawaPay adapter — direct mobile-money deposits and payouts across Sub-Saharan\n * Africa. The API is asynchronous: `POST /v2/deposits` returns `ACCEPTED` and the\n * transaction stays `processing` until a callback or a `GET` poll reports\n * `COMPLETED` / `FAILED`. The customer approves the deposit via their operator's\n * own prompt, so there is no `checkoutUrl`.\n *\n * pawaPay ids are merchant-generated UUIDs, so the adapter mints a `depositId` /\n * `payoutId`, uses it as `Transaction.id`, keeps a `reference → Transaction`\n * cache, and sets `clientReferenceId` + `metadata` so the caller reference is\n * recoverable across processes.\n */\nexport class PawapayProvider implements PaymentProvider {\n readonly id = PROVIDER_ID;\n\n readonly #http: HttpClient;\n readonly #generateId: () => string;\n readonly #cache: IdempotencyCache;\n\n constructor(options: PawapayOptions) {\n if (!options.apiToken?.trim()) {\n throw new ValidationError('PawapayProvider requires a non-empty \"apiToken\"');\n }\n const mode: PawapayMode = options.mode ?? 'sandbox';\n this.#generateId = options.generateId ?? (() => crypto.randomUUID());\n this.#cache = new IdempotencyCache(\n options.idempotencyStore ?? new MemoryIdempotencyStore(),\n this.id,\n );\n this.#http = new HttpClient({\n baseUrl: options.baseUrl ?? (mode === 'live' ? PRODUCTION_BASE_URL : SANDBOX_BASE_URL),\n timeoutMs: options.timeoutMs ?? DEFAULT_TIMEOUT_MS,\n fetch: options.fetch ?? globalThis.fetch,\n apiToken: options.apiToken,\n });\n }\n\n capabilities(): ProviderCapabilities {\n return CAPABILITIES;\n }\n\n async collection(input: CollectionInput, options?: ProviderCallOptions): Promise<Transaction> {\n return this.#initiate('collection', input, input.customer, options);\n }\n\n async disbursement(\n input: DisbursementInput,\n options?: ProviderCallOptions,\n ): Promise<Transaction> {\n return this.#initiate('disbursement', input, input.recipient, options);\n }\n\n async getTransaction(ref: TransactionRef, options?: ProviderCallOptions): Promise<Transaction> {\n const known =\n (ref.id ? await this.#cache.byId(ref.id) : undefined) ??\n (ref.reference ? await this.#cache.byReference(ref.reference) : undefined);\n\n const id = ref.id ?? known?.id;\n if (!id) {\n throw new ValidationError(\n 'pawaPay looks a transaction up by its deposit/payout id (the provider id); this reference is not known to this instance',\n { provider: this.id },\n );\n }\n\n const type: TransactionType = known?.type ?? 'collection';\n const path = type === 'disbursement' ? `/v2/payouts/${id}` : `/v2/deposits/${id}`;\n const res = await this.#http.getJson<PawapayTransactionResponse>(path, options?.signal);\n\n return await this.#toTransaction(type, res, { known, id });\n }\n\n async parseWebhook(raw: RawWebhook): Promise<WebhookEvent> {\n const { fields, raw: rawBody } = parseCallbackBody(raw.body);\n verifyContentDigest(raw.headers, rawBody);\n\n const depositId = fields.depositId;\n const payoutId = fields.payoutId;\n const id = depositId ?? payoutId;\n if (!id) {\n throw new ValidationError('pawaPay callback carries neither a depositId nor a payoutId', {\n provider: this.id,\n });\n }\n\n const known = await this.#cache.byId(id);\n const type: TransactionType = payoutId ? 'disbursement' : 'collection';\n const path = type === 'disbursement' ? `/v2/payouts/${id}` : `/v2/deposits/${id}`;\n const res = await this.#http.getJson<PawapayTransactionResponse>(path);\n const transaction = await this.#toTransaction(type, res, { known, id });\n\n return {\n type: webhookEventTypeForStatus(transaction.status),\n transaction,\n providerRaw: fields,\n };\n }\n\n async #initiate(\n type: TransactionType,\n input: CollectionInput | DisbursementInput,\n party: Party,\n options?: ProviderCallOptions,\n ): Promise<Transaction> {\n const operator = party.operator;\n if (!operator || !isOperator(operator)) {\n throw new ValidationError(`pawaPay ${type} needs a known operator`, { provider: this.id });\n }\n const providerCode = PAWAPAY_PROVIDER[operator];\n if (!providerCode) {\n throw new ValidationError(`pawaPay has no provider code for operator \"${operator}\"`, {\n provider: this.id,\n });\n }\n\n const cached = await this.#cache.byReference(input.reference);\n if (cached) return structuredClone(cached);\n\n const id = this.#generateId();\n const account: PawapayMmoAccount = {\n type: 'MMO',\n accountDetails: { phoneNumber: party.phone.replace(/\\D/g, ''), provider: providerCode },\n };\n const body = {\n [type === 'disbursement' ? 'payoutId' : 'depositId']: id,\n amount: String(input.amount.amount),\n currency: input.amount.currency,\n [type === 'disbursement' ? 'recipient' : 'payer']: account,\n customerMessage: customerMessage(input.description),\n clientReferenceId: input.reference,\n metadata: [\n { [REFERENCE_KEY]: input.reference },\n ...Object.entries(input.metadata ?? {}).map(([k, v]) => ({ [k]: v })),\n ],\n };\n\n const path = type === 'disbursement' ? '/v2/payouts' : '/v2/deposits';\n const res = await this.#http.postJson<PawapayAcceptResponse>(path, body, options?.signal);\n\n if (res.status === 'REJECTED') throw rejectedError(res);\n if (res.status === 'DUPLICATE_IGNORED') {\n const fetched = await this.#http.getJson<PawapayTransactionResponse>(\n `${path}/${id}`,\n options?.signal,\n );\n return await this.#toTransaction(type, fetched, { id, input, operator, party });\n }\n\n // ACCEPTED — the transaction is now processing.\n const now = res.created ?? new Date().toISOString();\n const txn: Transaction = {\n provider: this.id,\n id,\n reference: input.reference,\n type,\n status: 'processing',\n amount: input.amount,\n operator,\n customer: party,\n providerRaw: res,\n createdAt: now,\n updatedAt: now,\n };\n await this.#cache.remember(txn);\n return structuredClone(txn);\n }\n\n async #toTransaction(\n type: TransactionType,\n res: PawapayTransactionResponse,\n ctx: {\n known?: Transaction;\n id: string;\n input?: CollectionInput | DisbursementInput;\n operator?: Transaction['operator'];\n party?: Party;\n },\n ): Promise<Transaction> {\n const status = mapTransactionStatus(res.status);\n const echoed = metadataReference(res.metadata) ?? res.clientReferenceId;\n const now = new Date().toISOString();\n\n const txn: Transaction = {\n provider: this.id,\n id: ctx.id,\n reference: echoed ?? ctx.known?.reference ?? ctx.input?.reference ?? ctx.id,\n type,\n status,\n amount: ctx.known?.amount ?? ctx.input?.amount ?? coerceAmount(res.amount, res.currency),\n operator: ctx.known?.operator ?? ctx.operator,\n customer: ctx.known?.customer ?? ctx.party,\n failureReason: status === 'failed' ? mapFailureReason(res.failureReason) : undefined,\n providerRaw: res,\n createdAt: ctx.known?.createdAt ?? res.created ?? now,\n updatedAt: now,\n };\n await this.#cache.remember(txn);\n return structuredClone(txn);\n }\n}\n\nfunction customerMessage(description: string | undefined): string {\n const cleaned = (description ?? 'Payment').replace(/[^a-zA-Z0-9]/g, '');\n return cleaned.length >= 4 ? cleaned.slice(0, 22) : 'Payment';\n}\n\nfunction metadataReference(\n metadata: Array<Record<string, unknown>> | undefined,\n): string | undefined {\n for (const entry of metadata ?? []) {\n const value = entry[REFERENCE_KEY];\n if (typeof value === 'string') return value;\n }\n return undefined;\n}\n\nfunction coerceAmount(raw: string | undefined, currency: string | undefined): Money {\n const amount = raw ? Number.parseInt(raw, 10) : 0;\n return {\n amount: Number.isFinite(amount) ? amount : 0,\n currency: currency === 'XAF' ? 'XAF' : 'XOF',\n };\n}\n","import {\n AuthError,\n ProviderUnavailableError,\n RateLimitError,\n TransactionNotFoundError,\n} from '@momorail/core';\n\nexport const PROVIDER_ID = 'pawapay';\nexport const SANDBOX_BASE_URL = 'https://api.sandbox.pawapay.io';\nexport const PRODUCTION_BASE_URL = 'https://api.pawapay.io';\n\nexport type FetchLike = typeof fetch;\n\nexport interface HttpClientOptions {\n baseUrl: string;\n timeoutMs: number;\n fetch: FetchLike;\n apiToken: string;\n}\n\n/** JSON helper for the pawaPay v2 API: Bearer auth, timeout, and error mapping. */\nexport class HttpClient {\n readonly #baseUrl: string;\n readonly #timeoutMs: number;\n readonly #fetch: FetchLike;\n readonly #apiToken: string;\n\n constructor(options: HttpClientOptions) {\n this.#baseUrl = options.baseUrl.replace(/\\/$/, '');\n this.#timeoutMs = options.timeoutMs;\n this.#fetch = options.fetch;\n this.#apiToken = options.apiToken;\n }\n\n postJson<T>(path: string, body: unknown, signal?: AbortSignal): Promise<T> {\n return this.#request<T>('POST', path, body, signal);\n }\n\n getJson<T>(path: string, signal?: AbortSignal): Promise<T> {\n return this.#request<T>('GET', path, undefined, signal);\n }\n\n async #request<T>(\n method: 'GET' | 'POST',\n path: string,\n body: unknown,\n signal?: AbortSignal,\n ): Promise<T> {\n const timeout = AbortSignal.timeout(this.#timeoutMs);\n const composite = signal ? AbortSignal.any([signal, timeout]) : timeout;\n\n const headers: Record<string, string> = {\n accept: 'application/json',\n authorization: `Bearer ${this.#apiToken}`,\n };\n if (method === 'POST') headers['content-type'] = 'application/json';\n\n let response: Response;\n try {\n response = await this.#fetch(`${this.#baseUrl}${path}`, {\n method,\n headers,\n body: method === 'POST' ? JSON.stringify(body) : undefined,\n signal: composite,\n });\n } catch (cause) {\n if (signal?.aborted) throw cause;\n throw new ProviderUnavailableError('pawaPay request failed to complete', {\n provider: PROVIDER_ID,\n cause,\n });\n }\n\n if (response.status === 401 || response.status === 403) {\n throw new AuthError(`pawaPay rejected the API token (${response.status})`, {\n provider: PROVIDER_ID,\n });\n }\n if (response.status === 404) {\n throw new TransactionNotFoundError('pawaPay has no such transaction', {\n provider: PROVIDER_ID,\n });\n }\n if (response.status === 429) {\n throw new RateLimitError('pawaPay throttled the request', { provider: PROVIDER_ID });\n }\n\n const raw = await response.text();\n let parsed: unknown;\n try {\n parsed = raw ? JSON.parse(raw) : {};\n } catch (cause) {\n throw new ProviderUnavailableError('pawaPay returned a non-JSON response', {\n provider: PROVIDER_ID,\n providerRaw: raw,\n cause,\n });\n }\n\n if (response.status >= 500) {\n throw new ProviderUnavailableError(`pawaPay responded ${response.status}`, {\n provider: PROVIDER_ID,\n providerRaw: parsed,\n });\n }\n\n return parsed as T;\n }\n}\n","import {\n type FailureReason,\n MomorailError,\n type Operator,\n ProviderUnavailableError,\n type TransactionStatus,\n} from '@momorail/core';\nimport { PROVIDER_ID } from './http.js';\nimport type { PawapayAcceptResponse, PawapayFailureReason } from './types.js';\n\n/**\n * Momorail operator → pawaPay `provider` code. pawaPay scopes providers by\n * ISO-3166 alpha-3 country. Confirm the exact codes for your markets against\n * pawaPay's `GET /v2/active-conf`.\n */\nexport const PAWAPAY_PROVIDER: Partial<Record<Operator, string>> = {\n orange_ci: 'ORANGE_CIV',\n mtn_ci: 'MTN_MOMO_CIV',\n moov_ci: 'MOOV_CIV',\n wave_ci: 'WAVE_CIV',\n orange_sn: 'ORANGE_SEN',\n free_sn: 'FREE_SEN',\n wave_sn: 'WAVE_SEN',\n orange_ml: 'ORANGE_MLI',\n moov_ml: 'MOOV_MLI',\n orange_bf: 'ORANGE_BFA',\n moov_bf: 'MOOV_BFA',\n mtn_bj: 'MTN_MOMO_BEN',\n moov_bj: 'MOOV_BEN',\n orange_ne: 'ORANGE_NER',\n moov_ne: 'MOOV_NER',\n moov_tg: 'MOOV_TGO',\n togocom_tg: 'TOGOCOM_TGO',\n orange_cd: 'ORANGE_COD',\n airtel_cd: 'AIRTEL_COD',\n vodacom_cd: 'VODACOM_COD',\n};\n\n/** Map a pawaPay transaction status (from `GET`) onto a Momorail status. */\nexport function mapTransactionStatus(status: string | undefined): TransactionStatus {\n switch (status?.toUpperCase()) {\n case 'COMPLETED':\n return 'succeeded';\n case 'FAILED':\n return 'failed';\n case 'ACCEPTED':\n case 'PROCESSING':\n case 'IN_RECONCILIATION':\n return 'processing';\n default:\n return 'unknown';\n }\n}\n\nconst FAILURE_REASONS: Record<string, FailureReason> = {\n INSUFFICIENT_BALANCE: 'insufficient_funds',\n PAYER_LIMIT_REACHED: 'limit_exceeded',\n RECIPIENT_LIMIT_REACHED: 'limit_exceeded',\n PAYER_NOT_FOUND: 'invalid_number',\n RECIPIENT_NOT_FOUND: 'invalid_number',\n INVALID_PAYER_FORMAT: 'invalid_number',\n UNSPECIFIED_FAILURE: 'provider_error',\n PROVIDER_TEMPORARILY_UNAVAILABLE: 'provider_error',\n PAYER_DECLINED: 'user_declined',\n USER_CANCELLED: 'user_declined',\n TRANSACTION_TIMED_OUT: 'timeout',\n};\n\nexport function mapFailureReason(reason: PawapayFailureReason | undefined): FailureReason {\n return FAILURE_REASONS[reason?.failureCode ?? ''] ?? 'other';\n}\n\n/** Turn a synchronous `REJECTED` response into the matching `MomorailError`. */\nexport function rejectedError(res: PawapayAcceptResponse): MomorailError {\n const code = res.failureReason?.failureCode ?? 'REJECTED';\n const message = res.failureReason?.failureMessage ?? 'pawaPay rejected the request';\n if (/UNAVAILABLE|TEMPORARILY/.test(code)) {\n return new ProviderUnavailableError(`pawaPay is unavailable (${code}: ${message})`, {\n provider: PROVIDER_ID,\n providerRaw: res,\n });\n }\n return new MomorailError(`pawaPay rejected the request (${code}: ${message})`, 'provider_error', {\n provider: PROVIDER_ID,\n providerRaw: res,\n });\n}\n","import { createHash } from 'node:crypto';\nimport { WebhookVerificationError } from '@momorail/core';\nimport { PROVIDER_ID } from './http.js';\n\n/**\n * pawaPay signs callbacks with RFC-9421 HTTP Message Signatures using an\n * asymmetric key (ECDSA / RSASSA-PSS). Full verification needs the public key\n * from pawaPay's Public Keys endpoint and an RFC-9421 implementation — a planned\n * follow-up.\n *\n * What this adapter checks today is the `Content-Digest` header: a SHA-256 /\n * SHA-512 hash of the raw body. It proves the body was not altered in transit,\n * and the adapter then re-fetches the authoritative status from the API, so a\n * forged callback only triggers a status re-check.\n */\nexport function verifyContentDigest(\n headers: Record<string, string | string[] | undefined>,\n rawBody: string,\n): void {\n const header = headerValue(headers, 'content-digest');\n if (!header) return; // absent — nothing to check; status is re-fetched anyway\n\n // Format: `sha-256=:<base64>:` (possibly multiple, comma-separated)\n const match = /(sha-256|sha-512)=:([A-Za-z0-9+/=]+):/i.exec(header);\n if (!match) {\n throw new WebhookVerificationError('Unparseable Content-Digest on pawaPay callback', {\n provider: PROVIDER_ID,\n });\n }\n const algo = match[1]?.toLowerCase() === 'sha-512' ? 'sha512' : 'sha256';\n const expected = createHash(algo).update(rawBody, 'utf8').digest('base64');\n if (match[2] !== expected) {\n throw new WebhookVerificationError('pawaPay callback body does not match its Content-Digest', {\n provider: PROVIDER_ID,\n });\n }\n}\n\nexport interface PawapayCallback {\n depositId?: string;\n payoutId?: string;\n refundId?: string;\n status?: string;\n [key: string]: unknown;\n}\n\nexport function parseCallbackBody(body: string | Uint8Array | Record<string, unknown>): {\n fields: PawapayCallback;\n raw: string;\n} {\n if (typeof body === 'string') {\n return { fields: JSON.parse(body || '{}') as PawapayCallback, raw: body };\n }\n if (body instanceof Uint8Array) {\n const raw = new TextDecoder().decode(body);\n return { fields: JSON.parse(raw || '{}') as PawapayCallback, raw };\n }\n return { fields: body as PawapayCallback, raw: JSON.stringify(body) };\n}\n\nfunction headerValue(\n headers: Record<string, string | string[] | undefined>,\n name: string,\n): string | undefined {\n const found =\n headers[name] ??\n headers[name.toLowerCase()] ??\n Object.entries(headers).find(([k]) => k.toLowerCase() === name.toLowerCase())?.[1];\n return Array.isArray(found) ? found[0] : found;\n}\n"],"mappings":"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","names":["ProviderUnavailableError","ProviderUnavailableError"]}
package/package.json ADDED
@@ -0,0 +1,58 @@
1
+ {
2
+ "name": "@momorail/pawapay",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "description": "pawaPay adapter for Momorail (deposits, payouts, callbacks) across Sub-Saharan Africa",
6
+ "keywords": [
7
+ "mobile-money",
8
+ "payments",
9
+ "west-africa",
10
+ "africa",
11
+ "orange-money",
12
+ "mtn-momo",
13
+ "wave",
14
+ "xof",
15
+ "byo-keys",
16
+ "typescript",
17
+ "pawapay",
18
+ "adapter"
19
+ ],
20
+ "license": "MIT",
21
+ "author": "Boukymen <boukymen@gmail.com>",
22
+ "repository": {
23
+ "type": "git",
24
+ "url": "git+https://github.com/Boukymen/momorail.git",
25
+ "directory": "packages/pawapay"
26
+ },
27
+ "homepage": "https://github.com/Boukymen/momorail/tree/main/packages/pawapay#readme",
28
+ "bugs": "https://github.com/Boukymen/momorail/issues",
29
+ "main": "./dist/index.js",
30
+ "types": "./dist/index.d.ts",
31
+ "exports": {
32
+ ".": {
33
+ "types": "./dist/index.d.ts",
34
+ "import": "./dist/index.js"
35
+ }
36
+ },
37
+ "files": [
38
+ "dist"
39
+ ],
40
+ "publishConfig": {
41
+ "access": "public"
42
+ },
43
+ "dependencies": {
44
+ "@momorail/core": "0.1.0"
45
+ },
46
+ "devDependencies": {
47
+ "msw": "^2.7.0",
48
+ "tsup": "^8.3.5",
49
+ "typescript": "^5.7.2",
50
+ "vitest": "^2.1.8",
51
+ "@momorail/conformance": "0.1.0"
52
+ },
53
+ "scripts": {
54
+ "build": "tsup",
55
+ "typecheck": "tsc --noEmit",
56
+ "test": "vitest run"
57
+ }
58
+ }