@madapay/alefa 0.0.0-stage → 0.1.1
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 +176 -0
- package/README.md +65 -2
- package/dist/index.cjs +608 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +159 -0
- package/dist/index.d.ts +159 -0
- package/dist/index.js +595 -0
- package/dist/index.js.map +1 -0
- package/package.json +47 -3
package/dist/index.js
ADDED
|
@@ -0,0 +1,595 @@
|
|
|
1
|
+
import { createHmac, timingSafeEqual } from 'crypto';
|
|
2
|
+
import { ProviderTimeoutError, ProviderUnavailableError, AuthenticationError, PaymentNotFoundError, DuplicatePaymentError, ValidationError, validateCurrency, validateAmount, toNationalFormat, executeWithRetry, WebhookVerificationError } from '@madapay/core';
|
|
3
|
+
|
|
4
|
+
// src/index.ts
|
|
5
|
+
var PROVIDER = "alefa";
|
|
6
|
+
function safeProviderError(payload) {
|
|
7
|
+
if (typeof payload !== "object" || payload === null) return {};
|
|
8
|
+
const p = payload;
|
|
9
|
+
const out = {};
|
|
10
|
+
for (const k of ["code", "error", "message", "detail"]) {
|
|
11
|
+
if (typeof p[k] === "string") out[`provider_${k}`] = p[k].slice(0, 200);
|
|
12
|
+
}
|
|
13
|
+
return out;
|
|
14
|
+
}
|
|
15
|
+
async function alefaRequest(opts) {
|
|
16
|
+
const controller = new AbortController();
|
|
17
|
+
const timer = setTimeout(() => controller.abort(), opts.timeoutMs);
|
|
18
|
+
const onAbort = () => controller.abort();
|
|
19
|
+
opts.signal?.addEventListener("abort", onAbort, { once: true });
|
|
20
|
+
let response;
|
|
21
|
+
try {
|
|
22
|
+
response = await opts.fetchImpl(opts.url, {
|
|
23
|
+
method: opts.method,
|
|
24
|
+
headers: opts.headers,
|
|
25
|
+
body: opts.body === void 0 ? void 0 : JSON.stringify(opts.body),
|
|
26
|
+
signal: controller.signal
|
|
27
|
+
});
|
|
28
|
+
} catch {
|
|
29
|
+
if (opts.signal?.aborted) throw new Error("Operation aborted by caller");
|
|
30
|
+
const timedOut = controller.signal.aborted;
|
|
31
|
+
if (opts.uncertainOnFailure || timedOut) {
|
|
32
|
+
throw new ProviderTimeoutError(
|
|
33
|
+
timedOut ? `Alefa request timed out after ${opts.timeoutMs}ms` : "Alefa request failed after dispatch; outcome is uncertain (network error)",
|
|
34
|
+
{ timeoutMs: opts.timeoutMs },
|
|
35
|
+
PROVIDER
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
throw new ProviderUnavailableError("Alefa is unreachable", void 0, PROVIDER);
|
|
39
|
+
} finally {
|
|
40
|
+
clearTimeout(timer);
|
|
41
|
+
opts.signal?.removeEventListener("abort", onAbort);
|
|
42
|
+
}
|
|
43
|
+
const text = await response.text().catch(() => "");
|
|
44
|
+
let payload;
|
|
45
|
+
try {
|
|
46
|
+
payload = text ? JSON.parse(text) : void 0;
|
|
47
|
+
} catch {
|
|
48
|
+
payload = void 0;
|
|
49
|
+
}
|
|
50
|
+
if (response.ok) {
|
|
51
|
+
if (payload === void 0) {
|
|
52
|
+
throw opts.uncertainOnFailure ? new ProviderTimeoutError(
|
|
53
|
+
"Alefa returned an unreadable success response; outcome is uncertain",
|
|
54
|
+
{ status: response.status },
|
|
55
|
+
PROVIDER
|
|
56
|
+
) : new ProviderUnavailableError(
|
|
57
|
+
"Alefa returned an unreadable response",
|
|
58
|
+
void 0,
|
|
59
|
+
PROVIDER
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
return payload;
|
|
63
|
+
}
|
|
64
|
+
const details = { status: response.status, ...safeProviderError(payload) };
|
|
65
|
+
const s = response.status;
|
|
66
|
+
if (s === 401 || s === 403) {
|
|
67
|
+
throw new AuthenticationError("Alefa rejected credentials", details, PROVIDER);
|
|
68
|
+
}
|
|
69
|
+
if (s === 404) {
|
|
70
|
+
throw new PaymentNotFoundError("Alefa payment or resource not found", details, PROVIDER);
|
|
71
|
+
}
|
|
72
|
+
if (s === 409) {
|
|
73
|
+
throw new DuplicatePaymentError(
|
|
74
|
+
"Alefa idempotency conflict: request reference already processed or conflicting payload",
|
|
75
|
+
details,
|
|
76
|
+
PROVIDER
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
if (s === 400 || s === 422) {
|
|
80
|
+
const errorMsg = (typeof payload?.message === "string" ? payload.message : void 0) || "Alefa rejected request validation";
|
|
81
|
+
throw new ValidationError(errorMsg, "VALIDATION_FAILED", details, PROVIDER);
|
|
82
|
+
}
|
|
83
|
+
if (s >= 500) {
|
|
84
|
+
throw new ProviderUnavailableError(`Alefa service unavailable (HTTP ${s})`, details, PROVIDER);
|
|
85
|
+
}
|
|
86
|
+
throw new ProviderUnavailableError(
|
|
87
|
+
`Alefa returned unexpected status HTTP ${s}`,
|
|
88
|
+
details,
|
|
89
|
+
PROVIDER
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// src/status.ts
|
|
94
|
+
function mapAlefaStatus(rawStatus) {
|
|
95
|
+
if (typeof rawStatus !== "string" || rawStatus.trim() === "") {
|
|
96
|
+
return { status: "processing", known: false };
|
|
97
|
+
}
|
|
98
|
+
const s = rawStatus.trim().toUpperCase();
|
|
99
|
+
switch (s) {
|
|
100
|
+
case "CREATED":
|
|
101
|
+
case "INIT":
|
|
102
|
+
case "INITIAL":
|
|
103
|
+
return { status: "created", known: true };
|
|
104
|
+
case "PENDING":
|
|
105
|
+
case "WAITING":
|
|
106
|
+
case "AWAITING_ACTION":
|
|
107
|
+
case "ACTION_REQUIRED":
|
|
108
|
+
case "PENDING_ACTION":
|
|
109
|
+
case "PENDING_CUSTOMER_ACTION":
|
|
110
|
+
return { status: "pending", known: true };
|
|
111
|
+
case "PROCESSING":
|
|
112
|
+
case "IN_PROGRESS":
|
|
113
|
+
case "IN_FLIGHT":
|
|
114
|
+
return { status: "processing", known: true };
|
|
115
|
+
case "SUCCEEDED":
|
|
116
|
+
case "SUCCESS":
|
|
117
|
+
case "COMPLETED":
|
|
118
|
+
case "PAID":
|
|
119
|
+
return { status: "succeeded", known: true };
|
|
120
|
+
case "FAILED":
|
|
121
|
+
case "DECLINED":
|
|
122
|
+
case "REJECTED":
|
|
123
|
+
case "ERROR":
|
|
124
|
+
return { status: "failed", known: true };
|
|
125
|
+
case "EXPIRED":
|
|
126
|
+
case "TIMEOUT":
|
|
127
|
+
case "TIMED_OUT":
|
|
128
|
+
return { status: "expired", known: true };
|
|
129
|
+
case "CANCELLED":
|
|
130
|
+
case "CANCELED":
|
|
131
|
+
return { status: "cancelled", known: true };
|
|
132
|
+
default:
|
|
133
|
+
return { status: "processing", known: false };
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
var ALEFA_CONTRACT_METADATA = {
|
|
137
|
+
contractVersion: "Alefa Payment Gateway API v1.0.0",
|
|
138
|
+
reviewDate: "2026-10-31",
|
|
139
|
+
source: "https://alefa.mg/docs",
|
|
140
|
+
operatingEntity: "Alefa SAS (Antananarivo, Madagascar)",
|
|
141
|
+
indicativeCommissionPercent: 2.5,
|
|
142
|
+
settlementFeeAriary: 0
|
|
143
|
+
};
|
|
144
|
+
var alefaSandboxNumbers = {
|
|
145
|
+
mvola: "+261340000001",
|
|
146
|
+
mvola_qr: "+261340000002",
|
|
147
|
+
mvola_expired: "+261340000003",
|
|
148
|
+
mvola_failed: "+261340000004",
|
|
149
|
+
orange_money: "+261320000001",
|
|
150
|
+
orange_money_ussd: "+261320000002",
|
|
151
|
+
orange_money_expired: "+261320000003",
|
|
152
|
+
orange_money_failed: "+261320000004",
|
|
153
|
+
airtel_money: "+261330000001",
|
|
154
|
+
airtel_money_redirect: "+261330000002",
|
|
155
|
+
airtel_money_expired: "+261330000003",
|
|
156
|
+
airtel_money_failed: "+261330000004"
|
|
157
|
+
};
|
|
158
|
+
var alefaQrActionFixture = {
|
|
159
|
+
type: "qr_code",
|
|
160
|
+
qrData: "alefa://pay/tx_alefa_qr_12345?amount=10000&cur=MGA&ref=ORDER-QR-001",
|
|
161
|
+
qrImageUrl: "https://api.sandbox.alefa.mg/qr/tx_alefa_qr_12345.png",
|
|
162
|
+
expiresAt: "2026-10-31T20:00:00.000Z"
|
|
163
|
+
};
|
|
164
|
+
var alefaUssdActionFixture = {
|
|
165
|
+
type: "ussd",
|
|
166
|
+
ussdCode: "#144*31*654321#",
|
|
167
|
+
instructions: "Composez #144*31*654321# depuis votre mobile Orange Money et validez avec votre code secret.",
|
|
168
|
+
expiresAt: "2026-10-31T19:30:00.000Z"
|
|
169
|
+
};
|
|
170
|
+
var alefaRedirectActionFixture = {
|
|
171
|
+
type: "redirect_url",
|
|
172
|
+
url: "https://pay.sandbox.alefa.mg/checkout/tx_alefa_red_12345",
|
|
173
|
+
returnUrl: "https://merchant.mg/checkout/complete"
|
|
174
|
+
};
|
|
175
|
+
function createMockAlefaPaymentResponse(overrides = {}) {
|
|
176
|
+
return {
|
|
177
|
+
transactionId: overrides.transactionId ?? `alefa_tx_${Date.now()}`,
|
|
178
|
+
reference: overrides.reference ?? `ORDER_${Date.now()}`,
|
|
179
|
+
status: overrides.status ?? "PENDING",
|
|
180
|
+
amount: overrides.amount ?? 1e4,
|
|
181
|
+
currency: "MGA",
|
|
182
|
+
operator: overrides.operator ?? "mvola",
|
|
183
|
+
customerPhone: overrides.customerPhone ?? "0340000001",
|
|
184
|
+
checkoutUrl: overrides.checkoutUrl ?? "https://pay.sandbox.alefa.mg/checkout/mock",
|
|
185
|
+
action: overrides.action ?? alefaQrActionFixture,
|
|
186
|
+
expiresAt: overrides.expiresAt ?? new Date(Date.now() + 30 * 60 * 1e3).toISOString(),
|
|
187
|
+
createdAt: overrides.createdAt ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
188
|
+
updatedAt: overrides.updatedAt ?? (/* @__PURE__ */ new Date()).toISOString()
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
function createMockAlefaWebhook(payload, secret) {
|
|
192
|
+
const fullPayload = {
|
|
193
|
+
eventId: payload.eventId ?? `evt_alefa_${Date.now()}`,
|
|
194
|
+
eventType: payload.eventType ?? "payment.succeeded",
|
|
195
|
+
transactionId: payload.transactionId ?? `alefa_tx_${Date.now()}`,
|
|
196
|
+
reference: payload.reference ?? `ORDER_${Date.now()}`,
|
|
197
|
+
status: payload.status ?? "SUCCEEDED",
|
|
198
|
+
amount: payload.amount ?? 1e4,
|
|
199
|
+
currency: "MGA",
|
|
200
|
+
operator: payload.operator ?? "mvola",
|
|
201
|
+
timestamp: payload.timestamp ?? (/* @__PURE__ */ new Date()).toISOString()
|
|
202
|
+
};
|
|
203
|
+
const bodyString = JSON.stringify(fullPayload);
|
|
204
|
+
const rawBody = new TextEncoder().encode(bodyString);
|
|
205
|
+
const signature = createHmac("sha256", secret).update(rawBody).digest("hex");
|
|
206
|
+
return {
|
|
207
|
+
rawBody,
|
|
208
|
+
headers: {
|
|
209
|
+
"content-type": "application/json",
|
|
210
|
+
"x-alefa-signature": signature,
|
|
211
|
+
"x-alefa-timestamp": fullPayload.timestamp
|
|
212
|
+
},
|
|
213
|
+
payload: fullPayload
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// src/index.ts
|
|
218
|
+
var ALEFA_BASE_URLS = {
|
|
219
|
+
sandbox: "https://api.sandbox.alefa.mg",
|
|
220
|
+
production: "https://api.alefa.mg"
|
|
221
|
+
};
|
|
222
|
+
var PROVIDER2 = "alefa";
|
|
223
|
+
var SUPPORTED_METHODS = ["mvola", "orange_money", "airtel_money"];
|
|
224
|
+
var MIN_AMOUNT_MGA = 100;
|
|
225
|
+
var MAX_AMOUNT_MGA = 5e6;
|
|
226
|
+
function validateConfig(config) {
|
|
227
|
+
if (!config || typeof config.apiKey !== "string" || config.apiKey.trim() === "") {
|
|
228
|
+
throw new AuthenticationError("Alefa apiKey is required", void 0, PROVIDER2);
|
|
229
|
+
}
|
|
230
|
+
if (config.environment !== "sandbox" && config.environment !== "production") {
|
|
231
|
+
throw new ValidationError(
|
|
232
|
+
"Alefa environment must be explicitly 'sandbox' or 'production'",
|
|
233
|
+
"VALIDATION_FAILED",
|
|
234
|
+
void 0,
|
|
235
|
+
PROVIDER2
|
|
236
|
+
);
|
|
237
|
+
}
|
|
238
|
+
const baseUrl = (config.baseUrl ?? ALEFA_BASE_URLS[config.environment]).replace(/\/+$/, "");
|
|
239
|
+
if (!baseUrl.startsWith("https://")) {
|
|
240
|
+
throw new ValidationError(
|
|
241
|
+
"Alefa baseUrl must use https",
|
|
242
|
+
"VALIDATION_FAILED",
|
|
243
|
+
void 0,
|
|
244
|
+
PROVIDER2
|
|
245
|
+
);
|
|
246
|
+
}
|
|
247
|
+
const looksSandbox = /sandbox|test|dev/i.test(new URL(baseUrl).hostname);
|
|
248
|
+
if (config.environment === "production" && looksSandbox) {
|
|
249
|
+
throw new ValidationError(
|
|
250
|
+
"Alefa production environment configured with a sandbox base URL",
|
|
251
|
+
"VALIDATION_FAILED",
|
|
252
|
+
void 0,
|
|
253
|
+
PROVIDER2
|
|
254
|
+
);
|
|
255
|
+
}
|
|
256
|
+
if (config.environment === "sandbox" && !looksSandbox) {
|
|
257
|
+
throw new ValidationError(
|
|
258
|
+
"Alefa sandbox environment configured with a non-sandbox base URL",
|
|
259
|
+
"VALIDATION_FAILED",
|
|
260
|
+
void 0,
|
|
261
|
+
PROVIDER2
|
|
262
|
+
);
|
|
263
|
+
}
|
|
264
|
+
return baseUrl;
|
|
265
|
+
}
|
|
266
|
+
function asRecord(value) {
|
|
267
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
268
|
+
throw new ValidationError(
|
|
269
|
+
"Unexpected Alefa response format",
|
|
270
|
+
"VALIDATION_FAILED",
|
|
271
|
+
void 0,
|
|
272
|
+
PROVIDER2
|
|
273
|
+
);
|
|
274
|
+
}
|
|
275
|
+
return value;
|
|
276
|
+
}
|
|
277
|
+
function parseCustomerAction(rawAction) {
|
|
278
|
+
if (typeof rawAction !== "object" || rawAction === null) return void 0;
|
|
279
|
+
const a = rawAction;
|
|
280
|
+
if (a.type === "qr_code" && typeof a.qrData === "string") {
|
|
281
|
+
return {
|
|
282
|
+
type: "qr_code",
|
|
283
|
+
qrData: a.qrData,
|
|
284
|
+
qrImageUrl: typeof a.qrImageUrl === "string" ? a.qrImageUrl : void 0,
|
|
285
|
+
expiresAt: typeof a.expiresAt === "string" ? a.expiresAt : void 0
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
if (a.type === "ussd" && typeof a.ussdCode === "string") {
|
|
289
|
+
return {
|
|
290
|
+
type: "ussd",
|
|
291
|
+
ussdCode: a.ussdCode,
|
|
292
|
+
instructions: typeof a.instructions === "string" ? a.instructions : void 0,
|
|
293
|
+
expiresAt: typeof a.expiresAt === "string" ? a.expiresAt : void 0
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
if (a.type === "redirect_url" && typeof a.url === "string") {
|
|
297
|
+
return {
|
|
298
|
+
type: "redirect_url",
|
|
299
|
+
url: a.url,
|
|
300
|
+
returnUrl: typeof a.returnUrl === "string" ? a.returnUrl : void 0
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
return void 0;
|
|
304
|
+
}
|
|
305
|
+
var AlefaPaymentProvider = class {
|
|
306
|
+
id = PROVIDER2;
|
|
307
|
+
name = "Alefa";
|
|
308
|
+
apiKey;
|
|
309
|
+
webhookSecret;
|
|
310
|
+
baseUrl;
|
|
311
|
+
environment;
|
|
312
|
+
callbackUrl;
|
|
313
|
+
defaultValidityMinutes;
|
|
314
|
+
timeoutMs;
|
|
315
|
+
maxCreationRetries;
|
|
316
|
+
retryInitialDelayMs;
|
|
317
|
+
fetchImpl;
|
|
318
|
+
onUnknownStatus;
|
|
319
|
+
constructor(config) {
|
|
320
|
+
this.baseUrl = validateConfig(config);
|
|
321
|
+
this.apiKey = config.apiKey;
|
|
322
|
+
this.environment = config.environment;
|
|
323
|
+
this.webhookSecret = config.webhookSecret;
|
|
324
|
+
this.callbackUrl = config.callbackUrl;
|
|
325
|
+
this.defaultValidityMinutes = config.defaultValidityMinutes ?? 30;
|
|
326
|
+
this.timeoutMs = config.timeoutMs ?? 15e3;
|
|
327
|
+
this.maxCreationRetries = config.maxCreationRetries ?? 2;
|
|
328
|
+
this.retryInitialDelayMs = config.retryInitialDelayMs ?? 200;
|
|
329
|
+
this.fetchImpl = config.fetch ?? ((url, init) => fetch(url, init));
|
|
330
|
+
this.onUnknownStatus = config.onUnknownStatus;
|
|
331
|
+
}
|
|
332
|
+
/** Never exposes credentials or secrets. */
|
|
333
|
+
toJSON() {
|
|
334
|
+
return { id: this.id, environment: this.environment, baseUrl: this.baseUrl };
|
|
335
|
+
}
|
|
336
|
+
/**
|
|
337
|
+
* Capability truth table (D-07, ALEFA-01 through ALEFA-04).
|
|
338
|
+
* QR/USSD customer actions and hosted checkout are enabled.
|
|
339
|
+
* Direct refunds, payouts, payment links, and fee quotes remain unsupported.
|
|
340
|
+
*/
|
|
341
|
+
getCapabilities() {
|
|
342
|
+
return {
|
|
343
|
+
payment: true,
|
|
344
|
+
asynchronousConfirmation: true,
|
|
345
|
+
synchronousConfirmation: false,
|
|
346
|
+
webhooks: true,
|
|
347
|
+
polling: true,
|
|
348
|
+
refunds: false,
|
|
349
|
+
payouts: false,
|
|
350
|
+
paymentLinks: false,
|
|
351
|
+
hostedCheckout: true,
|
|
352
|
+
feeQuote: false,
|
|
353
|
+
cancellation: false,
|
|
354
|
+
customerActions: true,
|
|
355
|
+
supportedMethods: [...SUPPORTED_METHODS],
|
|
356
|
+
syncConfirmation: false,
|
|
357
|
+
asyncConfirmation: true,
|
|
358
|
+
webhookEvents: true,
|
|
359
|
+
feeCalculation: false
|
|
360
|
+
};
|
|
361
|
+
}
|
|
362
|
+
headers(extra = {}) {
|
|
363
|
+
return {
|
|
364
|
+
Authorization: `Bearer ${this.apiKey}`,
|
|
365
|
+
"Content-Type": "application/json",
|
|
366
|
+
Accept: "application/json",
|
|
367
|
+
...extra
|
|
368
|
+
};
|
|
369
|
+
}
|
|
370
|
+
async createPayment(request) {
|
|
371
|
+
if (!SUPPORTED_METHODS.includes(request.paymentMethod)) {
|
|
372
|
+
throw new ValidationError(
|
|
373
|
+
`Alefa does not support payment method '${request.paymentMethod}'. Supported: ${SUPPORTED_METHODS.join(", ")}`,
|
|
374
|
+
"VALIDATION_FAILED",
|
|
375
|
+
{ method: request.paymentMethod },
|
|
376
|
+
PROVIDER2
|
|
377
|
+
);
|
|
378
|
+
}
|
|
379
|
+
validateCurrency(request.currency);
|
|
380
|
+
validateAmount(request.amount, {
|
|
381
|
+
min: MIN_AMOUNT_MGA,
|
|
382
|
+
max: MAX_AMOUNT_MGA,
|
|
383
|
+
providerName: PROVIDER2
|
|
384
|
+
});
|
|
385
|
+
if (!request.idempotencyKey) {
|
|
386
|
+
throw new ValidationError(
|
|
387
|
+
"Alefa payment creation requires an idempotency key",
|
|
388
|
+
"VALIDATION_FAILED",
|
|
389
|
+
void 0,
|
|
390
|
+
PROVIDER2
|
|
391
|
+
);
|
|
392
|
+
}
|
|
393
|
+
const customerPhone = toNationalFormat(request.customer.phone);
|
|
394
|
+
const preferredAction = request.providerData?.preferredAction || "auto";
|
|
395
|
+
const validityMinutes = typeof request.providerData?.validityMinutes === "number" ? request.providerData.validityMinutes : this.defaultValidityMinutes;
|
|
396
|
+
const body = {
|
|
397
|
+
amount: request.amount,
|
|
398
|
+
currency: request.currency,
|
|
399
|
+
operator: request.paymentMethod,
|
|
400
|
+
customer: { phone: customerPhone },
|
|
401
|
+
reference: request.reference,
|
|
402
|
+
preferredAction,
|
|
403
|
+
validityMinutes
|
|
404
|
+
};
|
|
405
|
+
if (this.callbackUrl) body.callbackUrl = this.callbackUrl;
|
|
406
|
+
if (request.description) body.description = request.description;
|
|
407
|
+
if (request.returnUrls) body.returnUrls = request.returnUrls;
|
|
408
|
+
if (request.metadata) body.metadata = request.metadata;
|
|
409
|
+
const raw = await executeWithRetry(
|
|
410
|
+
() => alefaRequest({
|
|
411
|
+
method: "POST",
|
|
412
|
+
url: `${this.baseUrl}/api/v1/payments`,
|
|
413
|
+
headers: this.headers({ "Idempotency-Key": request.idempotencyKey }),
|
|
414
|
+
body,
|
|
415
|
+
timeoutMs: this.timeoutMs,
|
|
416
|
+
fetchImpl: this.fetchImpl,
|
|
417
|
+
uncertainOnFailure: true
|
|
418
|
+
}),
|
|
419
|
+
{ maxRetries: this.maxCreationRetries, initialDelayMs: this.retryInitialDelayMs }
|
|
420
|
+
);
|
|
421
|
+
const p = asRecord(raw);
|
|
422
|
+
const providerReference = String(p.transactionId || p.paymentReference || p.id || "");
|
|
423
|
+
if (!providerReference) {
|
|
424
|
+
throw new ValidationError(
|
|
425
|
+
"Alefa response missing transactionId",
|
|
426
|
+
"VALIDATION_FAILED",
|
|
427
|
+
void 0,
|
|
428
|
+
PROVIDER2
|
|
429
|
+
);
|
|
430
|
+
}
|
|
431
|
+
const { status, known } = mapAlefaStatus(p.status);
|
|
432
|
+
if (!known) {
|
|
433
|
+
this.onUnknownStatus?.(String(p.status), request.reference);
|
|
434
|
+
}
|
|
435
|
+
const action = parseCustomerAction(p.action);
|
|
436
|
+
return {
|
|
437
|
+
providerReference,
|
|
438
|
+
reference: typeof p.reference === "string" ? p.reference : request.reference,
|
|
439
|
+
amount: typeof p.amount === "number" ? p.amount : request.amount,
|
|
440
|
+
currency: "MGA",
|
|
441
|
+
status,
|
|
442
|
+
paymentMethod: request.paymentMethod,
|
|
443
|
+
rawProviderStatus: typeof p.status === "string" ? p.status : void 0,
|
|
444
|
+
checkoutUrl: typeof p.checkoutUrl === "string" ? p.checkoutUrl : void 0,
|
|
445
|
+
action,
|
|
446
|
+
expiresAt: typeof p.expiresAt === "string" ? p.expiresAt : void 0
|
|
447
|
+
};
|
|
448
|
+
}
|
|
449
|
+
async getPayment(providerReferenceOrLookup) {
|
|
450
|
+
let identifier;
|
|
451
|
+
let isMerchantRef = false;
|
|
452
|
+
if (typeof providerReferenceOrLookup === "string") {
|
|
453
|
+
identifier = providerReferenceOrLookup.trim();
|
|
454
|
+
} else {
|
|
455
|
+
if (providerReferenceOrLookup.providerReference) {
|
|
456
|
+
identifier = providerReferenceOrLookup.providerReference.trim();
|
|
457
|
+
} else if (providerReferenceOrLookup.reference) {
|
|
458
|
+
identifier = providerReferenceOrLookup.reference.trim();
|
|
459
|
+
isMerchantRef = true;
|
|
460
|
+
} else if (providerReferenceOrLookup.id) {
|
|
461
|
+
identifier = providerReferenceOrLookup.id.trim();
|
|
462
|
+
} else {
|
|
463
|
+
throw new ValidationError(
|
|
464
|
+
"Invalid payment lookup selector",
|
|
465
|
+
"INVALID_SELECTOR",
|
|
466
|
+
void 0,
|
|
467
|
+
PROVIDER2
|
|
468
|
+
);
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
if (!identifier) {
|
|
472
|
+
throw new ValidationError(
|
|
473
|
+
"Empty payment identifier provided",
|
|
474
|
+
"INVALID_SELECTOR",
|
|
475
|
+
void 0,
|
|
476
|
+
PROVIDER2
|
|
477
|
+
);
|
|
478
|
+
}
|
|
479
|
+
const url = isMerchantRef ? `${this.baseUrl}/api/v1/payments?reference=${encodeURIComponent(identifier)}` : `${this.baseUrl}/api/v1/payments/${encodeURIComponent(identifier)}`;
|
|
480
|
+
const raw = await alefaRequest({
|
|
481
|
+
method: "GET",
|
|
482
|
+
url,
|
|
483
|
+
headers: this.headers(),
|
|
484
|
+
timeoutMs: this.timeoutMs,
|
|
485
|
+
fetchImpl: this.fetchImpl,
|
|
486
|
+
uncertainOnFailure: false
|
|
487
|
+
});
|
|
488
|
+
const p = asRecord(raw);
|
|
489
|
+
const providerReference = String(p.transactionId || p.paymentReference || p.id || identifier);
|
|
490
|
+
const { status, known } = mapAlefaStatus(p.status);
|
|
491
|
+
if (!known) {
|
|
492
|
+
this.onUnknownStatus?.(String(p.status), providerReference);
|
|
493
|
+
}
|
|
494
|
+
const action = parseCustomerAction(p.action);
|
|
495
|
+
const operatorMethod = typeof p.operator === "string" && SUPPORTED_METHODS.includes(p.operator) ? p.operator : "mvola";
|
|
496
|
+
return {
|
|
497
|
+
providerReference,
|
|
498
|
+
reference: typeof p.reference === "string" ? p.reference : providerReference,
|
|
499
|
+
amount: typeof p.amount === "number" ? p.amount : 0,
|
|
500
|
+
currency: "MGA",
|
|
501
|
+
status,
|
|
502
|
+
paymentMethod: operatorMethod,
|
|
503
|
+
rawProviderStatus: typeof p.status === "string" ? p.status : void 0,
|
|
504
|
+
checkoutUrl: typeof p.checkoutUrl === "string" ? p.checkoutUrl : void 0,
|
|
505
|
+
action,
|
|
506
|
+
expiresAt: typeof p.expiresAt === "string" ? p.expiresAt : void 0
|
|
507
|
+
};
|
|
508
|
+
}
|
|
509
|
+
async verifyWebhook(request) {
|
|
510
|
+
if (!request || !(request.rawBody instanceof Uint8Array)) {
|
|
511
|
+
throw new WebhookVerificationError(
|
|
512
|
+
"Webhook verification requires unmodified rawBody as a Uint8Array (D-04)",
|
|
513
|
+
void 0,
|
|
514
|
+
PROVIDER2
|
|
515
|
+
);
|
|
516
|
+
}
|
|
517
|
+
if (!this.webhookSecret) {
|
|
518
|
+
throw new WebhookVerificationError(
|
|
519
|
+
"Alefa webhook verification requires a configured webhookSecret",
|
|
520
|
+
void 0,
|
|
521
|
+
PROVIDER2
|
|
522
|
+
);
|
|
523
|
+
}
|
|
524
|
+
const headerSig = request.headers["x-alefa-signature"] || request.headers["X-Alefa-Signature"] || request.headers["x-signature"];
|
|
525
|
+
if (!headerSig || typeof headerSig !== "string") {
|
|
526
|
+
throw new WebhookVerificationError(
|
|
527
|
+
"Missing required X-Alefa-Signature header on webhook request",
|
|
528
|
+
void 0,
|
|
529
|
+
PROVIDER2
|
|
530
|
+
);
|
|
531
|
+
}
|
|
532
|
+
const timestampHeader = request.headers["x-alefa-timestamp"] || request.headers["X-Alefa-Timestamp"];
|
|
533
|
+
if (typeof timestampHeader === "string") {
|
|
534
|
+
const webhookTime = new Date(timestampHeader).getTime();
|
|
535
|
+
const now = Date.now();
|
|
536
|
+
const maxDriftMs = 3e5;
|
|
537
|
+
if (Number.isNaN(webhookTime) || Math.abs(now - webhookTime) > maxDriftMs) {
|
|
538
|
+
throw new WebhookVerificationError(
|
|
539
|
+
`Alefa webhook timestamp drift exceeds ${maxDriftMs / 1e3}s tolerance`,
|
|
540
|
+
void 0,
|
|
541
|
+
PROVIDER2
|
|
542
|
+
);
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
const computedSig = createHmac("sha256", this.webhookSecret).update(request.rawBody).digest("hex");
|
|
546
|
+
const expectedBuf = Buffer.from(computedSig, "hex");
|
|
547
|
+
const actualBuf = Buffer.from(headerSig.trim(), "hex");
|
|
548
|
+
if (expectedBuf.length !== actualBuf.length || !timingSafeEqual(expectedBuf, actualBuf)) {
|
|
549
|
+
throw new WebhookVerificationError(
|
|
550
|
+
"Alefa webhook signature verification failed: invalid HMAC digest",
|
|
551
|
+
void 0,
|
|
552
|
+
PROVIDER2
|
|
553
|
+
);
|
|
554
|
+
}
|
|
555
|
+
let payload;
|
|
556
|
+
try {
|
|
557
|
+
const text = new TextDecoder().decode(request.rawBody);
|
|
558
|
+
payload = asRecord(JSON.parse(text));
|
|
559
|
+
} catch {
|
|
560
|
+
throw new WebhookVerificationError(
|
|
561
|
+
"Alefa webhook body is not valid JSON after signature validation",
|
|
562
|
+
void 0,
|
|
563
|
+
PROVIDER2
|
|
564
|
+
);
|
|
565
|
+
}
|
|
566
|
+
const transactionId = String(
|
|
567
|
+
payload.transactionId || payload.paymentReference || payload.id || ""
|
|
568
|
+
);
|
|
569
|
+
if (!transactionId) {
|
|
570
|
+
throw new WebhookVerificationError(
|
|
571
|
+
"Alefa webhook payload missing transactionId or payment reference",
|
|
572
|
+
void 0,
|
|
573
|
+
PROVIDER2
|
|
574
|
+
);
|
|
575
|
+
}
|
|
576
|
+
const { status, known } = mapAlefaStatus(payload.status);
|
|
577
|
+
if (!known) {
|
|
578
|
+
this.onUnknownStatus?.(String(payload.status), transactionId);
|
|
579
|
+
}
|
|
580
|
+
const eventId = String(payload.eventId || payload.id || `evt_${Date.now()}`);
|
|
581
|
+
const occurredAt = typeof payload.timestamp === "string" ? payload.timestamp : (/* @__PURE__ */ new Date()).toISOString();
|
|
582
|
+
return {
|
|
583
|
+
providerEventId: eventId,
|
|
584
|
+
providerReference: transactionId,
|
|
585
|
+
reference: typeof payload.reference === "string" ? payload.reference : void 0,
|
|
586
|
+
status,
|
|
587
|
+
occurredAt,
|
|
588
|
+
rawProviderStatus: typeof payload.status === "string" ? payload.status : void 0
|
|
589
|
+
};
|
|
590
|
+
}
|
|
591
|
+
};
|
|
592
|
+
|
|
593
|
+
export { ALEFA_BASE_URLS, ALEFA_CONTRACT_METADATA, AlefaPaymentProvider, AlefaPaymentProvider as AlefaProvider, alefaQrActionFixture, alefaRedirectActionFixture, alefaRequest, alefaSandboxNumbers, alefaUssdActionFixture, createMockAlefaPaymentResponse, createMockAlefaWebhook, mapAlefaStatus };
|
|
594
|
+
//# sourceMappingURL=index.js.map
|
|
595
|
+
//# sourceMappingURL=index.js.map
|