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