@momorail/ligdicash 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 +21 -0
- package/dist/index.d.ts +104 -0
- package/dist/index.js +403 -0
- package/dist/index.js.map +1 -0
- package/package.json +58 -0
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.
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { IdempotencyStore, PaymentProvider, ProviderCapabilities, CollectionInput, ProviderCallOptions, Transaction, DisbursementInput, TransactionRef, RawWebhook, WebhookEvent } from '@momorail/core';
|
|
2
|
+
|
|
3
|
+
interface LigdicashOptions {
|
|
4
|
+
/** API Key from the LigdiCash project (sent as the `Apikey` header). */
|
|
5
|
+
apiKey: string;
|
|
6
|
+
/** API Token from the LigdiCash project (sent as `Authorization: Bearer <token>`). */
|
|
7
|
+
apiToken: string;
|
|
8
|
+
/** Store name shown on the hosted invoice. */
|
|
9
|
+
storeName: string;
|
|
10
|
+
/** Store website, shown on the invoice. Optional. */
|
|
11
|
+
storeUrl?: string;
|
|
12
|
+
/** Fallback callback URL when a collection doesn't set `callbackUrl`. */
|
|
13
|
+
callbackUrl?: string;
|
|
14
|
+
/** Fallback success redirect. */
|
|
15
|
+
returnUrl?: string;
|
|
16
|
+
/** Fallback cancel redirect. */
|
|
17
|
+
cancelUrl?: string;
|
|
18
|
+
/** Override the API base URL (tests). Default `https://app.ligdicash.com`. */
|
|
19
|
+
baseUrl?: string;
|
|
20
|
+
/** Per-request timeout in ms. Default 15000. */
|
|
21
|
+
timeoutMs?: number;
|
|
22
|
+
/** Injectable fetch implementation. Default the global `fetch`. */
|
|
23
|
+
fetch?: typeof fetch;
|
|
24
|
+
/** Where the adapter remembers created transactions. Default: in-memory, per process. */
|
|
25
|
+
idempotencyStore?: IdempotencyStore;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* LigdiCash adapter — the redirect ("LAR") checkout flow. Implements collection
|
|
29
|
+
* (hosted checkout invoice), status via `confirm`, and callback normalisation.
|
|
30
|
+
*
|
|
31
|
+
* LigdiCash confirms an invoice by its **token** only, so the adapter keeps a
|
|
32
|
+
* per-instance cache from `reference` to the created `Transaction`, and stashes
|
|
33
|
+
* the caller's `reference` in `custom_data` so `confirm` and the callback echo it
|
|
34
|
+
* back across processes.
|
|
35
|
+
*/
|
|
36
|
+
declare class LigdicashProvider implements PaymentProvider {
|
|
37
|
+
#private;
|
|
38
|
+
readonly id = "ligdicash";
|
|
39
|
+
constructor(options: LigdicashOptions);
|
|
40
|
+
capabilities(): ProviderCapabilities;
|
|
41
|
+
collection(input: CollectionInput, options?: ProviderCallOptions): Promise<Transaction>;
|
|
42
|
+
disbursement(_input: DisbursementInput, _options?: ProviderCallOptions): Promise<Transaction>;
|
|
43
|
+
getTransaction(ref: TransactionRef, options?: ProviderCallOptions): Promise<Transaction>;
|
|
44
|
+
parseWebhook(raw: RawWebhook): Promise<WebhookEvent>;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Raw LigdiCash Payment API shapes (`/pay/v01`). Only the fields Momorail reads
|
|
49
|
+
* are modelled; the untouched payload is always returned on
|
|
50
|
+
* `Transaction.providerRaw`.
|
|
51
|
+
*
|
|
52
|
+
* @see https://developers.ligdicash.com/api1
|
|
53
|
+
*/
|
|
54
|
+
/** `POST /pay/v01/redirect/checkout-invoice/create` */
|
|
55
|
+
interface LigdicashCreateResponse {
|
|
56
|
+
/** `"00"` on success. */
|
|
57
|
+
response_code: string;
|
|
58
|
+
response_text?: string;
|
|
59
|
+
description?: string;
|
|
60
|
+
/** The invoice token used to confirm the transaction later. */
|
|
61
|
+
token?: string;
|
|
62
|
+
/** Alias returned by some responses; treated the same as `token`. */
|
|
63
|
+
response_token?: string;
|
|
64
|
+
/** Link to LigdiCash docs explaining a non-`00` code. */
|
|
65
|
+
wiki?: string;
|
|
66
|
+
}
|
|
67
|
+
/** `GET /pay/v01/redirect/checkout-invoice/confirm` */
|
|
68
|
+
interface LigdicashConfirmResponse {
|
|
69
|
+
response_code: string;
|
|
70
|
+
response_text?: string;
|
|
71
|
+
wiki?: string;
|
|
72
|
+
/** `completed` | `pending` | `in progress` | `cancelled` | `nocompleted` | `failed` */
|
|
73
|
+
status?: string;
|
|
74
|
+
montant?: number | string;
|
|
75
|
+
custom_data?: Record<string, unknown> | string;
|
|
76
|
+
customer?: {
|
|
77
|
+
name?: string;
|
|
78
|
+
phone?: string;
|
|
79
|
+
email?: string;
|
|
80
|
+
};
|
|
81
|
+
external_id?: string;
|
|
82
|
+
operator_id?: string;
|
|
83
|
+
operator_name?: string;
|
|
84
|
+
}
|
|
85
|
+
/** Fields LigdiCash POSTs to the callback URL. Parsed leniently (JSON or form). */
|
|
86
|
+
type LigdicashCallbackFields = Record<string, string | undefined>;
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Parse a LigdiCash callback body. LigdiCash POSTs the callback URL on each
|
|
90
|
+
* payment event; the body is JSON or `application/x-www-form-urlencoded`.
|
|
91
|
+
*
|
|
92
|
+
* NOTE: LigdiCash callbacks are **not cryptographically signed**. The documented
|
|
93
|
+
* pattern is to re-check each callback against the invoice token stored at
|
|
94
|
+
* creation — which is what `LigdicashProvider.parseWebhook` does. A forged
|
|
95
|
+
* callback therefore only triggers an authoritative status re-check, never a
|
|
96
|
+
* trusted state change.
|
|
97
|
+
*/
|
|
98
|
+
declare function parseCallbackBody(body: string | Uint8Array | Record<string, unknown>): LigdicashCallbackFields;
|
|
99
|
+
/** Read the invoice token from a parsed callback, however LigdiCash spelled it. */
|
|
100
|
+
declare function callbackToken(fields: LigdicashCallbackFields): string | undefined;
|
|
101
|
+
/** Read the caller reference (LigdiCash echoes it as `external_id` / `custom_data`). */
|
|
102
|
+
declare function callbackReference(fields: LigdicashCallbackFields): string | undefined;
|
|
103
|
+
|
|
104
|
+
export { type LigdicashCallbackFields, type LigdicashConfirmResponse, type LigdicashCreateResponse, type LigdicashOptions, LigdicashProvider, callbackReference, callbackToken, parseCallbackBody };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,403 @@
|
|
|
1
|
+
// src/provider.ts
|
|
2
|
+
import {
|
|
3
|
+
IdempotencyCache,
|
|
4
|
+
MemoryIdempotencyStore,
|
|
5
|
+
MomorailError as MomorailError2,
|
|
6
|
+
TransactionNotFoundError as TransactionNotFoundError2,
|
|
7
|
+
ValidationError,
|
|
8
|
+
isOperator,
|
|
9
|
+
webhookEventTypeForStatus
|
|
10
|
+
} from "@momorail/core";
|
|
11
|
+
|
|
12
|
+
// src/http.ts
|
|
13
|
+
import { ProviderUnavailableError, RateLimitError } from "@momorail/core";
|
|
14
|
+
|
|
15
|
+
// src/mapping.ts
|
|
16
|
+
import {
|
|
17
|
+
AuthError,
|
|
18
|
+
MomorailError,
|
|
19
|
+
TransactionNotFoundError
|
|
20
|
+
} from "@momorail/core";
|
|
21
|
+
var PROVIDER_ID = "ligdicash";
|
|
22
|
+
var OK = "00";
|
|
23
|
+
var AUTH_HINT = /apikey|api[\s-]?key|token invalide|non autoris|unauthor|authenticat/i;
|
|
24
|
+
var NOT_FOUND_HINT = /introuvable|invalide|not[\s_]*found|no such|inexistant|aucune facture/i;
|
|
25
|
+
function mapConfirmStatus(res) {
|
|
26
|
+
switch (res.status?.toLowerCase().trim()) {
|
|
27
|
+
case "completed":
|
|
28
|
+
case "reussi":
|
|
29
|
+
case "succes":
|
|
30
|
+
return "succeeded";
|
|
31
|
+
case "pending":
|
|
32
|
+
case "in progress":
|
|
33
|
+
case "en cours":
|
|
34
|
+
return "processing";
|
|
35
|
+
case "cancelled":
|
|
36
|
+
case "canceled":
|
|
37
|
+
case "annule":
|
|
38
|
+
return "cancelled";
|
|
39
|
+
case "nocompleted":
|
|
40
|
+
case "failed":
|
|
41
|
+
case "echec":
|
|
42
|
+
return "failed";
|
|
43
|
+
default:
|
|
44
|
+
return res.response_code === OK ? "processing" : "unknown";
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
function mapFailureReason(res) {
|
|
48
|
+
const text = `${res.response_text ?? ""}`.toLowerCase();
|
|
49
|
+
if (/solde|insufficient|fund/.test(text)) return "insufficient_funds";
|
|
50
|
+
if (/annul|cancel/.test(text)) return "user_declined";
|
|
51
|
+
if (/expir|delai/.test(text)) return "timeout";
|
|
52
|
+
return "other";
|
|
53
|
+
}
|
|
54
|
+
function ligdicashError(responseCode, message, providerRaw) {
|
|
55
|
+
const opts = { provider: PROVIDER_ID, providerRaw };
|
|
56
|
+
if (AUTH_HINT.test(message)) {
|
|
57
|
+
return new AuthError(`LigdiCash rejected the credentials (${responseCode}: ${message})`, opts);
|
|
58
|
+
}
|
|
59
|
+
if (NOT_FOUND_HINT.test(message)) {
|
|
60
|
+
return new TransactionNotFoundError(
|
|
61
|
+
`LigdiCash has no such invoice (${responseCode}: ${message})`,
|
|
62
|
+
opts
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
return new MomorailError(`LigdiCash error (${responseCode}: ${message})`, "provider_error", opts);
|
|
66
|
+
}
|
|
67
|
+
function isOk(responseCode) {
|
|
68
|
+
return responseCode === OK;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// src/http.ts
|
|
72
|
+
var HttpClient = class {
|
|
73
|
+
#baseUrl;
|
|
74
|
+
#timeoutMs;
|
|
75
|
+
#fetch;
|
|
76
|
+
#authHeaders;
|
|
77
|
+
constructor(options) {
|
|
78
|
+
this.#baseUrl = options.baseUrl.replace(/\/$/, "");
|
|
79
|
+
this.#timeoutMs = options.timeoutMs;
|
|
80
|
+
this.#fetch = options.fetch;
|
|
81
|
+
this.#authHeaders = options.authHeaders;
|
|
82
|
+
}
|
|
83
|
+
postJson(path, body, signal) {
|
|
84
|
+
return this.#request("POST", path, body, signal);
|
|
85
|
+
}
|
|
86
|
+
getJson(path, signal) {
|
|
87
|
+
return this.#request("GET", path, void 0, signal);
|
|
88
|
+
}
|
|
89
|
+
async #request(method, path, body, signal) {
|
|
90
|
+
const timeout = AbortSignal.timeout(this.#timeoutMs);
|
|
91
|
+
const composite = signal ? AbortSignal.any([signal, timeout]) : timeout;
|
|
92
|
+
const headers = { accept: "application/json", ...this.#authHeaders };
|
|
93
|
+
if (method === "POST") headers["content-type"] = "application/json";
|
|
94
|
+
let response;
|
|
95
|
+
try {
|
|
96
|
+
response = await this.#fetch(`${this.#baseUrl}${path}`, {
|
|
97
|
+
method,
|
|
98
|
+
headers,
|
|
99
|
+
body: method === "POST" ? JSON.stringify(body) : void 0,
|
|
100
|
+
signal: composite
|
|
101
|
+
});
|
|
102
|
+
} catch (cause) {
|
|
103
|
+
if (signal?.aborted) throw cause;
|
|
104
|
+
throw new ProviderUnavailableError("LigdiCash request failed to complete", {
|
|
105
|
+
provider: PROVIDER_ID,
|
|
106
|
+
cause
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
if (response.status === 429) {
|
|
110
|
+
throw new RateLimitError("LigdiCash throttled the request", { provider: PROVIDER_ID });
|
|
111
|
+
}
|
|
112
|
+
const raw = await response.text();
|
|
113
|
+
let parsed;
|
|
114
|
+
try {
|
|
115
|
+
parsed = raw ? JSON.parse(raw) : {};
|
|
116
|
+
} catch (cause) {
|
|
117
|
+
throw new ProviderUnavailableError("LigdiCash returned a non-JSON response", {
|
|
118
|
+
provider: PROVIDER_ID,
|
|
119
|
+
providerRaw: raw,
|
|
120
|
+
cause
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
if (response.status >= 500) {
|
|
124
|
+
throw new ProviderUnavailableError(`LigdiCash responded ${response.status}`, {
|
|
125
|
+
provider: PROVIDER_ID,
|
|
126
|
+
providerRaw: parsed
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
return parsed;
|
|
130
|
+
}
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
// src/webhook.ts
|
|
134
|
+
function parseCallbackBody(body) {
|
|
135
|
+
if (typeof body === "string" || body instanceof Uint8Array) {
|
|
136
|
+
const raw = typeof body === "string" ? body : new TextDecoder().decode(body);
|
|
137
|
+
const trimmed = raw.trim();
|
|
138
|
+
if (trimmed.startsWith("{") || trimmed.startsWith("[")) {
|
|
139
|
+
return flatten(JSON.parse(trimmed));
|
|
140
|
+
}
|
|
141
|
+
return Object.fromEntries(new URLSearchParams(raw).entries());
|
|
142
|
+
}
|
|
143
|
+
return flatten(body);
|
|
144
|
+
}
|
|
145
|
+
function callbackToken(fields) {
|
|
146
|
+
return fields.token ?? fields.invoice_token ?? fields.invoiceToken ?? fields.response_token ?? fields["transaction.token"];
|
|
147
|
+
}
|
|
148
|
+
function callbackReference(fields) {
|
|
149
|
+
return fields.external_id ?? fields["custom_data.momorail_reference"] ?? fields.momorail_reference ?? fields["transaction.external_id"];
|
|
150
|
+
}
|
|
151
|
+
function flatten(input, prefix = "") {
|
|
152
|
+
const out = {};
|
|
153
|
+
for (const [key, value] of Object.entries(input)) {
|
|
154
|
+
const path = prefix ? `${prefix}.${key}` : key;
|
|
155
|
+
if (value && typeof value === "object" && !Array.isArray(value)) {
|
|
156
|
+
Object.assign(out, flatten(value, path));
|
|
157
|
+
} else if (value != null) {
|
|
158
|
+
out[path] = String(value);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
return out;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// src/provider.ts
|
|
165
|
+
var DEFAULT_BASE_URL = "https://app.ligdicash.com";
|
|
166
|
+
var DEFAULT_TIMEOUT_MS = 15e3;
|
|
167
|
+
var REFERENCE_KEY = "momorail_reference";
|
|
168
|
+
var CAPABILITIES = {
|
|
169
|
+
collection: true,
|
|
170
|
+
// Payout API (POST /pay/v01/straight/payout) exists but its body shape needs
|
|
171
|
+
// sandbox verification — planned for a later release.
|
|
172
|
+
disbursement: false,
|
|
173
|
+
// LigdiCash confirms an invoice by its token only.
|
|
174
|
+
lookupByReference: false,
|
|
175
|
+
hostedCheckout: true,
|
|
176
|
+
operators: [
|
|
177
|
+
"orange_bf",
|
|
178
|
+
"moov_bf",
|
|
179
|
+
"orange_ci",
|
|
180
|
+
"mtn_ci",
|
|
181
|
+
"moov_ci",
|
|
182
|
+
"wave_ci",
|
|
183
|
+
"orange_ml",
|
|
184
|
+
"moov_ml",
|
|
185
|
+
"wave_ml",
|
|
186
|
+
"orange_ne",
|
|
187
|
+
"moov_ne",
|
|
188
|
+
"moov_tg",
|
|
189
|
+
"togocom_tg",
|
|
190
|
+
"mtn_bj",
|
|
191
|
+
"moov_bj",
|
|
192
|
+
"orange_sn",
|
|
193
|
+
"free_sn",
|
|
194
|
+
"wave_sn",
|
|
195
|
+
"orange_cd",
|
|
196
|
+
"airtel_cd",
|
|
197
|
+
"vodacom_cd"
|
|
198
|
+
],
|
|
199
|
+
currencies: ["XOF"]
|
|
200
|
+
};
|
|
201
|
+
var LigdicashProvider = class {
|
|
202
|
+
id = PROVIDER_ID;
|
|
203
|
+
#storeName;
|
|
204
|
+
#storeUrl;
|
|
205
|
+
#callbackUrl;
|
|
206
|
+
#returnUrl;
|
|
207
|
+
#cancelUrl;
|
|
208
|
+
#http;
|
|
209
|
+
#cache;
|
|
210
|
+
constructor(options) {
|
|
211
|
+
for (const key of ["apiKey", "apiToken", "storeName"]) {
|
|
212
|
+
if (!options[key]?.trim()) {
|
|
213
|
+
throw new ValidationError(`LigdicashProvider requires a non-empty "${key}"`);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
this.#storeName = options.storeName;
|
|
217
|
+
this.#storeUrl = options.storeUrl ?? "";
|
|
218
|
+
this.#callbackUrl = options.callbackUrl;
|
|
219
|
+
this.#returnUrl = options.returnUrl;
|
|
220
|
+
this.#cancelUrl = options.cancelUrl;
|
|
221
|
+
this.#cache = new IdempotencyCache(
|
|
222
|
+
options.idempotencyStore ?? new MemoryIdempotencyStore(),
|
|
223
|
+
this.id
|
|
224
|
+
);
|
|
225
|
+
this.#http = new HttpClient({
|
|
226
|
+
baseUrl: options.baseUrl ?? DEFAULT_BASE_URL,
|
|
227
|
+
timeoutMs: options.timeoutMs ?? DEFAULT_TIMEOUT_MS,
|
|
228
|
+
fetch: options.fetch ?? globalThis.fetch,
|
|
229
|
+
authHeaders: {
|
|
230
|
+
Apikey: options.apiKey,
|
|
231
|
+
Authorization: `Bearer ${options.apiToken}`
|
|
232
|
+
}
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
capabilities() {
|
|
236
|
+
return CAPABILITIES;
|
|
237
|
+
}
|
|
238
|
+
async collection(input, options) {
|
|
239
|
+
if (input.customer.operator !== void 0 && !isOperator(input.customer.operator)) {
|
|
240
|
+
throw new ValidationError(`Unknown operator: ${input.customer.operator}`, {
|
|
241
|
+
provider: this.id
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
const cached = await this.#cache.byReference(input.reference);
|
|
245
|
+
if (cached) return structuredClone(cached);
|
|
246
|
+
const callbackUrl = input.callbackUrl ?? this.#callbackUrl;
|
|
247
|
+
if (!callbackUrl) {
|
|
248
|
+
throw new ValidationError(
|
|
249
|
+
"LigdiCash needs a callback URL: pass collection input.callbackUrl or set LigdicashOptions.callbackUrl",
|
|
250
|
+
{ provider: this.id }
|
|
251
|
+
);
|
|
252
|
+
}
|
|
253
|
+
const description = input.description ?? `Payment ${input.reference}`;
|
|
254
|
+
const body = {
|
|
255
|
+
commande: {
|
|
256
|
+
invoice: {
|
|
257
|
+
items: [
|
|
258
|
+
{
|
|
259
|
+
name: description,
|
|
260
|
+
description,
|
|
261
|
+
quantity: 1,
|
|
262
|
+
unit_price: input.amount.amount,
|
|
263
|
+
total_price: input.amount.amount
|
|
264
|
+
}
|
|
265
|
+
],
|
|
266
|
+
total_amount: input.amount.amount,
|
|
267
|
+
devise: input.amount.currency,
|
|
268
|
+
description,
|
|
269
|
+
customer: "",
|
|
270
|
+
customer_firstname: input.customer.firstName ?? "",
|
|
271
|
+
customer_lastname: input.customer.lastName ?? "",
|
|
272
|
+
customer_email: input.customer.email ?? "",
|
|
273
|
+
external_id: input.reference,
|
|
274
|
+
otp: ""
|
|
275
|
+
},
|
|
276
|
+
store: { name: this.#storeName, website_url: this.#storeUrl },
|
|
277
|
+
actions: pruneUndefined({
|
|
278
|
+
cancel_url: input.cancelUrl ?? this.#cancelUrl,
|
|
279
|
+
return_url: input.returnUrl ?? this.#returnUrl,
|
|
280
|
+
callback_url: callbackUrl
|
|
281
|
+
}),
|
|
282
|
+
custom_data: { [REFERENCE_KEY]: input.reference, ...input.metadata }
|
|
283
|
+
}
|
|
284
|
+
};
|
|
285
|
+
const res = await this.#http.postJson(
|
|
286
|
+
"/pay/v01/redirect/checkout-invoice/create",
|
|
287
|
+
body,
|
|
288
|
+
options?.signal
|
|
289
|
+
);
|
|
290
|
+
const token = res.response_token ?? res.token;
|
|
291
|
+
if (!isOk(res.response_code) || !token) {
|
|
292
|
+
throw ligdicashError(res.response_code, res.response_text ?? "create failed", res);
|
|
293
|
+
}
|
|
294
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
295
|
+
const txn = {
|
|
296
|
+
provider: this.id,
|
|
297
|
+
id: token,
|
|
298
|
+
reference: input.reference,
|
|
299
|
+
type: "collection",
|
|
300
|
+
status: "pending",
|
|
301
|
+
amount: input.amount,
|
|
302
|
+
operator: input.customer.operator,
|
|
303
|
+
customer: input.customer,
|
|
304
|
+
checkoutUrl: isUrl(res.response_text) ? res.response_text : `${DEFAULT_BASE_URL}/pay/v01/redirect/checkout-invoice/pay-with-token/${token}`,
|
|
305
|
+
providerRaw: res,
|
|
306
|
+
createdAt: now,
|
|
307
|
+
updatedAt: now
|
|
308
|
+
};
|
|
309
|
+
await this.#cache.remember(txn);
|
|
310
|
+
return structuredClone(txn);
|
|
311
|
+
}
|
|
312
|
+
async disbursement(_input, _options) {
|
|
313
|
+
throw new MomorailError2(
|
|
314
|
+
"LigdiCash disbursement is not supported by this adapter yet (POST /pay/v01/straight/payout, planned for a later release)",
|
|
315
|
+
"not_supported",
|
|
316
|
+
{ provider: this.id }
|
|
317
|
+
);
|
|
318
|
+
}
|
|
319
|
+
async getTransaction(ref, options) {
|
|
320
|
+
const known = (ref.id ? await this.#cache.byId(ref.id) : void 0) ?? (ref.reference ? await this.#cache.byReference(ref.reference) : void 0);
|
|
321
|
+
const token = ref.id ?? known?.id;
|
|
322
|
+
if (!token) {
|
|
323
|
+
throw new ValidationError(
|
|
324
|
+
"LigdiCash looks a transaction up by its invoice token (the provider id); this reference is not known to this instance",
|
|
325
|
+
{ provider: this.id }
|
|
326
|
+
);
|
|
327
|
+
}
|
|
328
|
+
const res = await this.#http.getJson(
|
|
329
|
+
`/pay/v01/redirect/checkout-invoice/confirm/?invoiceToken=${encodeURIComponent(token)}`,
|
|
330
|
+
options?.signal
|
|
331
|
+
);
|
|
332
|
+
const status = mapConfirmStatus(res);
|
|
333
|
+
if (!isOk(res.response_code) && status === "unknown") {
|
|
334
|
+
throw ligdicashError(res.response_code, res.response_text ?? "unknown invoice", res);
|
|
335
|
+
}
|
|
336
|
+
const echoed = typeof res.custom_data === "object" ? res.custom_data?.[REFERENCE_KEY] : void 0;
|
|
337
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
338
|
+
const txn = {
|
|
339
|
+
provider: this.id,
|
|
340
|
+
id: token,
|
|
341
|
+
reference: (typeof echoed === "string" ? echoed : void 0) ?? res.external_id ?? known?.reference ?? ref.reference ?? token,
|
|
342
|
+
type: "collection",
|
|
343
|
+
status,
|
|
344
|
+
amount: known?.amount ?? coerceAmount(res.montant),
|
|
345
|
+
operator: known?.operator,
|
|
346
|
+
customer: known?.customer,
|
|
347
|
+
checkoutUrl: known?.checkoutUrl,
|
|
348
|
+
failureReason: status === "failed" ? mapFailureReason(res) : void 0,
|
|
349
|
+
providerRaw: res,
|
|
350
|
+
createdAt: known?.createdAt ?? now,
|
|
351
|
+
updatedAt: now
|
|
352
|
+
};
|
|
353
|
+
await this.#cache.remember(txn);
|
|
354
|
+
return structuredClone(txn);
|
|
355
|
+
}
|
|
356
|
+
async parseWebhook(raw) {
|
|
357
|
+
const fields = parseCallbackBody(raw.body);
|
|
358
|
+
const token = callbackToken(fields);
|
|
359
|
+
const reference = callbackReference(fields);
|
|
360
|
+
if (!token && !reference) {
|
|
361
|
+
throw new ValidationError(
|
|
362
|
+
"LigdiCash callback carries neither an invoice token nor a reference",
|
|
363
|
+
{
|
|
364
|
+
provider: this.id
|
|
365
|
+
}
|
|
366
|
+
);
|
|
367
|
+
}
|
|
368
|
+
const known = reference ? await this.#cache.byReference(reference) : void 0;
|
|
369
|
+
const lookupToken = token ?? known?.id;
|
|
370
|
+
if (!lookupToken) {
|
|
371
|
+
throw new TransactionNotFoundError2("LigdiCash callback does not resolve to a known invoice", {
|
|
372
|
+
provider: this.id
|
|
373
|
+
});
|
|
374
|
+
}
|
|
375
|
+
const transaction = await this.getTransaction({ id: lookupToken });
|
|
376
|
+
return {
|
|
377
|
+
type: webhookEventTypeForStatus(transaction.status),
|
|
378
|
+
transaction,
|
|
379
|
+
providerRaw: fields
|
|
380
|
+
};
|
|
381
|
+
}
|
|
382
|
+
};
|
|
383
|
+
function isUrl(value) {
|
|
384
|
+
return typeof value === "string" && /^https?:\/\//i.test(value);
|
|
385
|
+
}
|
|
386
|
+
function coerceAmount(raw) {
|
|
387
|
+
const amount = typeof raw === "string" ? Number.parseInt(raw, 10) : raw ?? 0;
|
|
388
|
+
return { amount: Number.isFinite(amount) ? Math.trunc(amount) : 0, currency: "XOF" };
|
|
389
|
+
}
|
|
390
|
+
function pruneUndefined(obj) {
|
|
391
|
+
const out = {};
|
|
392
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
393
|
+
if (value !== void 0) out[key] = value;
|
|
394
|
+
}
|
|
395
|
+
return out;
|
|
396
|
+
}
|
|
397
|
+
export {
|
|
398
|
+
LigdicashProvider,
|
|
399
|
+
callbackReference,
|
|
400
|
+
callbackToken,
|
|
401
|
+
parseCallbackBody
|
|
402
|
+
};
|
|
403
|
+
//# 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 MomorailError,\n type Money,\n type PaymentProvider,\n type ProviderCallOptions,\n type ProviderCapabilities,\n type RawWebhook,\n type Transaction,\n TransactionNotFoundError,\n type TransactionRef,\n ValidationError,\n type WebhookEvent,\n isOperator,\n webhookEventTypeForStatus,\n} from '@momorail/core';\nimport { HttpClient } from './http.js';\nimport {\n PROVIDER_ID,\n isOk,\n ligdicashError,\n mapConfirmStatus,\n mapFailureReason,\n} from './mapping.js';\nimport type { LigdicashConfirmResponse, LigdicashCreateResponse } from './types.js';\nimport { callbackReference, callbackToken, parseCallbackBody } from './webhook.js';\n\nconst DEFAULT_BASE_URL = 'https://app.ligdicash.com';\nconst DEFAULT_TIMEOUT_MS = 15_000;\nconst REFERENCE_KEY = 'momorail_reference';\n\nexport interface LigdicashOptions {\n /** API Key from the LigdiCash project (sent as the `Apikey` header). */\n apiKey: string;\n /** API Token from the LigdiCash project (sent as `Authorization: Bearer <token>`). */\n apiToken: string;\n /** Store name shown on the hosted invoice. */\n storeName: string;\n /** Store website, shown on the invoice. Optional. */\n storeUrl?: string;\n /** Fallback callback URL when a collection doesn't set `callbackUrl`. */\n callbackUrl?: string;\n /** Fallback success redirect. */\n returnUrl?: string;\n /** Fallback cancel redirect. */\n cancelUrl?: string;\n /** Override the API base URL (tests). Default `https://app.ligdicash.com`. */\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 /** Where the adapter remembers created transactions. Default: in-memory, per process. */\n idempotencyStore?: IdempotencyStore;\n}\n\nconst CAPABILITIES: ProviderCapabilities = {\n collection: true,\n // Payout API (POST /pay/v01/straight/payout) exists but its body shape needs\n // sandbox verification — planned for a later release.\n disbursement: false,\n // LigdiCash confirms an invoice by its token only.\n lookupByReference: false,\n hostedCheckout: true,\n operators: [\n 'orange_bf',\n 'moov_bf',\n 'orange_ci',\n 'mtn_ci',\n 'moov_ci',\n 'wave_ci',\n 'orange_ml',\n 'moov_ml',\n 'wave_ml',\n 'orange_ne',\n 'moov_ne',\n 'moov_tg',\n 'togocom_tg',\n 'mtn_bj',\n 'moov_bj',\n 'orange_sn',\n 'free_sn',\n 'wave_sn',\n 'orange_cd',\n 'airtel_cd',\n 'vodacom_cd',\n ],\n currencies: ['XOF'],\n};\n\n/**\n * LigdiCash adapter — the redirect (\"LAR\") checkout flow. Implements collection\n * (hosted checkout invoice), status via `confirm`, and callback normalisation.\n *\n * LigdiCash confirms an invoice by its **token** only, so the adapter keeps a\n * per-instance cache from `reference` to the created `Transaction`, and stashes\n * the caller's `reference` in `custom_data` so `confirm` and the callback echo it\n * back across processes.\n */\nexport class LigdicashProvider implements PaymentProvider {\n readonly id = PROVIDER_ID;\n\n readonly #storeName: string;\n readonly #storeUrl: string;\n readonly #callbackUrl: string | undefined;\n readonly #returnUrl: string | undefined;\n readonly #cancelUrl: string | undefined;\n readonly #http: HttpClient;\n readonly #cache: IdempotencyCache;\n\n constructor(options: LigdicashOptions) {\n for (const key of ['apiKey', 'apiToken', 'storeName'] as const) {\n if (!options[key]?.trim()) {\n throw new ValidationError(`LigdicashProvider requires a non-empty \"${key}\"`);\n }\n }\n this.#storeName = options.storeName;\n this.#storeUrl = options.storeUrl ?? '';\n this.#callbackUrl = options.callbackUrl;\n this.#returnUrl = options.returnUrl;\n this.#cancelUrl = options.cancelUrl;\n this.#cache = new IdempotencyCache(\n options.idempotencyStore ?? new MemoryIdempotencyStore(),\n this.id,\n );\n this.#http = new HttpClient({\n baseUrl: options.baseUrl ?? DEFAULT_BASE_URL,\n timeoutMs: options.timeoutMs ?? DEFAULT_TIMEOUT_MS,\n fetch: options.fetch ?? globalThis.fetch,\n authHeaders: {\n Apikey: options.apiKey,\n Authorization: `Bearer ${options.apiToken}`,\n },\n });\n }\n\n capabilities(): ProviderCapabilities {\n return CAPABILITIES;\n }\n\n async collection(input: CollectionInput, options?: ProviderCallOptions): Promise<Transaction> {\n if (input.customer.operator !== undefined && !isOperator(input.customer.operator)) {\n throw new ValidationError(`Unknown operator: ${input.customer.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 callbackUrl = input.callbackUrl ?? this.#callbackUrl;\n if (!callbackUrl) {\n throw new ValidationError(\n 'LigdiCash needs a callback URL: pass collection input.callbackUrl or set LigdicashOptions.callbackUrl',\n { provider: this.id },\n );\n }\n\n const description = input.description ?? `Payment ${input.reference}`;\n const body = {\n commande: {\n invoice: {\n items: [\n {\n name: description,\n description,\n quantity: 1,\n unit_price: input.amount.amount,\n total_price: input.amount.amount,\n },\n ],\n total_amount: input.amount.amount,\n devise: input.amount.currency,\n description,\n customer: '',\n customer_firstname: input.customer.firstName ?? '',\n customer_lastname: input.customer.lastName ?? '',\n customer_email: input.customer.email ?? '',\n external_id: input.reference,\n otp: '',\n },\n store: { name: this.#storeName, website_url: this.#storeUrl },\n actions: pruneUndefined({\n cancel_url: input.cancelUrl ?? this.#cancelUrl,\n return_url: input.returnUrl ?? this.#returnUrl,\n callback_url: callbackUrl,\n }),\n custom_data: { [REFERENCE_KEY]: input.reference, ...input.metadata },\n },\n };\n\n const res = await this.#http.postJson<LigdicashCreateResponse>(\n '/pay/v01/redirect/checkout-invoice/create',\n body,\n options?.signal,\n );\n const token = res.response_token ?? res.token;\n if (!isOk(res.response_code) || !token) {\n throw ligdicashError(res.response_code, res.response_text ?? 'create failed', res);\n }\n\n const now = new Date().toISOString();\n const txn: Transaction = {\n provider: this.id,\n id: token,\n reference: input.reference,\n type: 'collection',\n status: 'pending',\n amount: input.amount,\n operator: input.customer.operator,\n customer: input.customer,\n checkoutUrl: isUrl(res.response_text)\n ? res.response_text\n : `${DEFAULT_BASE_URL}/pay/v01/redirect/checkout-invoice/pay-with-token/${token}`,\n providerRaw: res,\n createdAt: now,\n updatedAt: now,\n };\n\n await this.#cache.remember(txn);\n return structuredClone(txn);\n }\n\n async disbursement(\n _input: DisbursementInput,\n _options?: ProviderCallOptions,\n ): Promise<Transaction> {\n throw new MomorailError(\n 'LigdiCash disbursement is not supported by this adapter yet (POST /pay/v01/straight/payout, planned for a later release)',\n 'not_supported',\n { provider: this.id },\n );\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 token = ref.id ?? known?.id;\n if (!token) {\n throw new ValidationError(\n 'LigdiCash looks a transaction up by its invoice token (the provider id); this reference is not known to this instance',\n { provider: this.id },\n );\n }\n\n const res = await this.#http.getJson<LigdicashConfirmResponse>(\n `/pay/v01/redirect/checkout-invoice/confirm/?invoiceToken=${encodeURIComponent(token)}`,\n options?.signal,\n );\n\n const status = mapConfirmStatus(res);\n if (!isOk(res.response_code) && status === 'unknown') {\n throw ligdicashError(res.response_code, res.response_text ?? 'unknown invoice', res);\n }\n\n const echoed =\n typeof res.custom_data === 'object' ? res.custom_data?.[REFERENCE_KEY] : undefined;\n const now = new Date().toISOString();\n const txn: Transaction = {\n provider: this.id,\n id: token,\n reference:\n (typeof echoed === 'string' ? echoed : undefined) ??\n res.external_id ??\n known?.reference ??\n ref.reference ??\n token,\n type: 'collection',\n status,\n amount: known?.amount ?? coerceAmount(res.montant),\n operator: known?.operator,\n customer: known?.customer,\n checkoutUrl: known?.checkoutUrl,\n failureReason: status === 'failed' ? mapFailureReason(res) : undefined,\n providerRaw: res,\n createdAt: known?.createdAt ?? now,\n updatedAt: now,\n };\n\n await this.#cache.remember(txn);\n return structuredClone(txn);\n }\n\n async parseWebhook(raw: RawWebhook): Promise<WebhookEvent> {\n const fields = parseCallbackBody(raw.body);\n const token = callbackToken(fields);\n const reference = callbackReference(fields);\n if (!token && !reference) {\n throw new ValidationError(\n 'LigdiCash callback carries neither an invoice token nor a reference',\n {\n provider: this.id,\n },\n );\n }\n\n // LigdiCash callbacks are unsigned — re-confirm for the authoritative status.\n const known = reference ? await this.#cache.byReference(reference) : undefined;\n const lookupToken = token ?? known?.id;\n if (!lookupToken) {\n throw new TransactionNotFoundError('LigdiCash callback does not resolve to a known invoice', {\n provider: this.id,\n });\n }\n\n const transaction = await this.getTransaction({ id: lookupToken });\n return {\n type: webhookEventTypeForStatus(transaction.status),\n transaction,\n providerRaw: fields,\n };\n }\n}\n\nfunction isUrl(value: string | undefined): value is string {\n return typeof value === 'string' && /^https?:\\/\\//i.test(value);\n}\n\nfunction coerceAmount(raw: number | string | undefined): Money {\n const amount = typeof raw === 'string' ? Number.parseInt(raw, 10) : (raw ?? 0);\n return { amount: Number.isFinite(amount) ? Math.trunc(amount) : 0, currency: 'XOF' };\n}\n\nfunction pruneUndefined<T extends object>(obj: T): { [K in keyof T]: T[K] } {\n const out = {} as { [K in keyof T]: T[K] };\n for (const [key, value] of Object.entries(obj)) {\n if (value !== undefined) out[key as keyof T] = value as T[keyof T];\n }\n return out;\n}\n","import { ProviderUnavailableError, RateLimitError } from '@momorail/core';\nimport { PROVIDER_ID } from './mapping.js';\n\nexport type FetchLike = typeof fetch;\n\nexport interface HttpClientOptions {\n baseUrl: string;\n timeoutMs: number;\n fetch: FetchLike;\n /** `Apikey` and `Authorization` headers, sent on every request. */\n authHeaders: Record<string, string>;\n}\n\n/**\n * Minimal JSON helper for the LigdiCash Payment API: attaches the `Apikey` /\n * `Authorization` headers, enforces a timeout, and maps transport-level failures\n * onto `MomorailError`s.\n */\nexport class HttpClient {\n readonly #baseUrl: string;\n readonly #timeoutMs: number;\n readonly #fetch: FetchLike;\n readonly #authHeaders: Record<string, string>;\n\n constructor(options: HttpClientOptions) {\n this.#baseUrl = options.baseUrl.replace(/\\/$/, '');\n this.#timeoutMs = options.timeoutMs;\n this.#fetch = options.fetch;\n this.#authHeaders = options.authHeaders;\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> = { accept: 'application/json', ...this.#authHeaders };\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('LigdiCash request failed to complete', {\n provider: PROVIDER_ID,\n cause,\n });\n }\n\n if (response.status === 429) {\n throw new RateLimitError('LigdiCash 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('LigdiCash 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(`LigdiCash responded ${response.status}`, {\n provider: PROVIDER_ID,\n providerRaw: parsed,\n });\n }\n\n return parsed as T;\n }\n}\n","import {\n AuthError,\n type FailureReason,\n MomorailError,\n TransactionNotFoundError,\n type TransactionStatus,\n} from '@momorail/core';\nimport type { LigdicashConfirmResponse } from './types.js';\n\nexport const PROVIDER_ID = 'ligdicash';\n\nconst OK = '00';\nconst AUTH_HINT = /apikey|api[\\s-]?key|token invalide|non autoris|unauthor|authenticat/i;\nconst NOT_FOUND_HINT = /introuvable|invalide|not[\\s_]*found|no such|inexistant|aucune facture/i;\n\n/** Map a LigdiCash `status` string onto a Momorail status. */\nexport function mapConfirmStatus(res: LigdicashConfirmResponse): TransactionStatus {\n switch (res.status?.toLowerCase().trim()) {\n case 'completed':\n case 'reussi':\n case 'succes':\n return 'succeeded';\n case 'pending':\n case 'in progress':\n case 'en cours':\n return 'processing';\n case 'cancelled':\n case 'canceled':\n case 'annule':\n return 'cancelled';\n case 'nocompleted':\n case 'failed':\n case 'echec':\n return 'failed';\n default:\n return res.response_code === OK ? 'processing' : 'unknown';\n }\n}\n\nexport function mapFailureReason(res: LigdicashConfirmResponse): FailureReason {\n const text = `${res.response_text ?? ''}`.toLowerCase();\n if (/solde|insufficient|fund/.test(text)) return 'insufficient_funds';\n if (/annul|cancel/.test(text)) return 'user_declined';\n if (/expir|delai/.test(text)) return 'timeout';\n return 'other';\n}\n\n/**\n * LigdiCash usually returns HTTP 200 with `response_code !== \"00\"` and a message\n * in `response_text` (plus a `wiki` link). Translate that into a `MomorailError`.\n */\nexport function ligdicashError(\n responseCode: string,\n message: string,\n providerRaw: unknown,\n): MomorailError {\n const opts = { provider: PROVIDER_ID, providerRaw } as const;\n\n if (AUTH_HINT.test(message)) {\n return new AuthError(`LigdiCash rejected the credentials (${responseCode}: ${message})`, opts);\n }\n if (NOT_FOUND_HINT.test(message)) {\n return new TransactionNotFoundError(\n `LigdiCash has no such invoice (${responseCode}: ${message})`,\n opts,\n );\n }\n return new MomorailError(`LigdiCash error (${responseCode}: ${message})`, 'provider_error', opts);\n}\n\nexport function isOk(responseCode: string): boolean {\n return responseCode === OK;\n}\n","import type { LigdicashCallbackFields } from './types.js';\n\n/**\n * Parse a LigdiCash callback body. LigdiCash POSTs the callback URL on each\n * payment event; the body is JSON or `application/x-www-form-urlencoded`.\n *\n * NOTE: LigdiCash callbacks are **not cryptographically signed**. The documented\n * pattern is to re-check each callback against the invoice token stored at\n * creation — which is what `LigdicashProvider.parseWebhook` does. A forged\n * callback therefore only triggers an authoritative status re-check, never a\n * trusted state change.\n */\nexport function parseCallbackBody(\n body: string | Uint8Array | Record<string, unknown>,\n): LigdicashCallbackFields {\n if (typeof body === 'string' || body instanceof Uint8Array) {\n const raw = typeof body === 'string' ? body : new TextDecoder().decode(body);\n const trimmed = raw.trim();\n if (trimmed.startsWith('{') || trimmed.startsWith('[')) {\n return flatten(JSON.parse(trimmed) as Record<string, unknown>);\n }\n return Object.fromEntries(new URLSearchParams(raw).entries());\n }\n return flatten(body);\n}\n\n/** Read the invoice token from a parsed callback, however LigdiCash spelled it. */\nexport function callbackToken(fields: LigdicashCallbackFields): string | undefined {\n return (\n fields.token ??\n fields.invoice_token ??\n fields.invoiceToken ??\n fields.response_token ??\n fields['transaction.token']\n );\n}\n\n/** Read the caller reference (LigdiCash echoes it as `external_id` / `custom_data`). */\nexport function callbackReference(fields: LigdicashCallbackFields): string | undefined {\n return (\n fields.external_id ??\n fields['custom_data.momorail_reference'] ??\n fields.momorail_reference ??\n fields['transaction.external_id']\n );\n}\n\nfunction flatten(input: Record<string, unknown>, prefix = ''): LigdicashCallbackFields {\n const out: LigdicashCallbackFields = {};\n for (const [key, value] of Object.entries(input)) {\n const path = prefix ? `${prefix}.${key}` : key;\n if (value && typeof value === 'object' && !Array.isArray(value)) {\n Object.assign(out, flatten(value as Record<string, unknown>, path));\n } else if (value != null) {\n out[path] = String(value);\n }\n }\n return out;\n}\n"],"mappings":"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","names":["MomorailError","TransactionNotFoundError","MomorailError","TransactionNotFoundError"]}
|
package/package.json
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@momorail/ligdicash",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "LigdiCash adapter for Momorail (redirect checkout, status, callback)",
|
|
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
|
+
"ligdicash",
|
|
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/ligdicash"
|
|
26
|
+
},
|
|
27
|
+
"homepage": "https://github.com/Boukymen/momorail/tree/main/packages/ligdicash#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
|
+
}
|