@ngandu-dev/flexpay 2.0.4 → 3.0.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.
@@ -0,0 +1,345 @@
1
+ # Upgrade to FlexPay TypeScript 3.0.0
2
+
3
+ Version 3 moves mobile-money and card collections to FlexPay's corporate API and changes the public
4
+ client, status, and response contracts. This guide covers migration from version 2. Current usage
5
+ examples are in the [README](README.md).
6
+
7
+ ## Configure explicit destinations
8
+
9
+ Replace positional merchant/token arguments with a `Credential` object and a required `BaseUrl`.
10
+ Remove imports of `Environment`, `EnvironmentType`, and `BaseUrls`. There is no default environment
11
+ or destination. Node.js 24 or newer remains required.
12
+
13
+ Before:
14
+
15
+ ```ts
16
+ import { Client } from "@ngandu-dev/flexpay";
17
+
18
+ const client = new Client("MERCHANT_CODE", "YOUR_TOKEN", "prod");
19
+ ```
20
+
21
+ After:
22
+
23
+ ```ts
24
+ import { Client, type Credential, productionBaseUrl } from "@ngandu-dev/flexpay";
25
+
26
+ const credential = {
27
+ merchant: "MERCHANT_CODE",
28
+ token: "YOUR_TOKEN",
29
+ } satisfies Credential;
30
+
31
+ const client = new Client(credential, productionBaseUrl);
32
+ ```
33
+
34
+ Replace `apiBaseUrl` and `cardBaseUrl` overrides with all three `BaseUrl` properties, even if your
35
+ application uses only one operation. Prefixes include the API version and omit the service path.
36
+
37
+ Before:
38
+
39
+ ```ts
40
+ const localClient = new Client("TESTMERCHANT", "test-token", "dev", {
41
+ apiBaseUrl: "http://localhost:8080/api/rest/v1",
42
+ cardBaseUrl: "http://localhost:8080/v1.1",
43
+ });
44
+ ```
45
+
46
+ After:
47
+
48
+ ```ts
49
+ import { Client, type BaseUrl } from "@ngandu-dev/flexpay";
50
+
51
+ const baseUrl = {
52
+ payment: "http://localhost:8080/api/rest/v1",
53
+ check: "http://localhost:8080/api/rest/v1",
54
+ payout: "http://localhost:8080/api/rest/v1",
55
+ } satisfies BaseUrl;
56
+
57
+ const localClient = new Client(
58
+ { merchant: "TESTMERCHANT", token: "test-token" },
59
+ baseUrl,
60
+ );
61
+ ```
62
+
63
+ `productionBaseUrl` contains these prefixes:
64
+
65
+ | Property | Prefix |
66
+ | --- | --- |
67
+ | `payment` | `https://corporateapi.flexpay.cd/api/rest/v1` |
68
+ | `check` | `https://apicheck.flexpaie.com/api/rest/v1` |
69
+ | `payout` | `https://backend.flexpay.cd/api/rest/v1` |
70
+
71
+ FlexPay has not supplied a corporate sandbox URL. Configure your simulator explicitly instead of
72
+ relying on the removed `dev` environment. Both mobile and card now use `/paymentService`. Checks
73
+ use the separate check prefix. Payouts retain `/merchantPayOutService` and the existing wire protocol.
74
+
75
+ Configuration schemas reject unknown fields and blank credentials. Base URLs must be absolute
76
+ HTTP(S) prefixes without credentials, query strings, fragments, backslashes, or internal whitespace
77
+ and control characters. Surrounding whitespace and trailing slashes are removed from prefixes.
78
+ Merchant/token contents are preserved. The client retains immutable validated snapshots.
79
+
80
+ An optional third constructor argument accepts a Fetch-compatible function. It must honor the
81
+ provided abort signal and `redirect: "manual"` behavior.
82
+
83
+ ## Replace payment dispatch and removed request fields
84
+
85
+ Replace `pay()` with `mobile()` or `card()`. Request schemas reject unknown fields rather than
86
+ silently stripping them. Remove obsolete fields from runtime objects as well as TypeScript types.
87
+
88
+ For mobile requests, remove `description`, `approveUrl`, `cancelUrl`, and `declineUrl`.
89
+
90
+ Before:
91
+
92
+ ```ts
93
+ const mobile = await client.pay({
94
+ amount: 10,
95
+ currency: "USD",
96
+ phone: "243810000001",
97
+ reference: "mobile-order-123",
98
+ callbackUrl: "https://example.com/payments/callback",
99
+ description: "Order payment",
100
+ approveUrl: "https://example.com/payments/approved",
101
+ });
102
+ ```
103
+
104
+ After:
105
+
106
+ ```ts
107
+ const mobile = await client.mobile({
108
+ amount: 10,
109
+ currency: "USD",
110
+ phone: "243810000001",
111
+ reference: "mobile-order-123",
112
+ callbackUrl: "https://example.com/payments/callback",
113
+ });
114
+
115
+ if (!mobile.accepted || !mobile.orderNumber) {
116
+ throw new Error(mobile.message || "Payment initiation was not accepted");
117
+ }
118
+ ```
119
+
120
+ For card requests, remove `description` and `homeUrl`. The 25-character reference cap is removed.
121
+
122
+ Before:
123
+
124
+ ```ts
125
+ const card = await client.card({
126
+ amount: 10,
127
+ currency: "USD",
128
+ reference: "card-order-123",
129
+ description: "Order payment",
130
+ homeUrl: "https://example.com",
131
+ callbackUrl: "https://example.com/payments/callback",
132
+ approveUrl: "https://example.com/payments/approved",
133
+ cancelUrl: "https://example.com/payments/cancelled",
134
+ declineUrl: "https://example.com/payments/declined",
135
+ });
136
+ ```
137
+
138
+ After:
139
+
140
+ ```ts
141
+ const card = await client.card({
142
+ amount: 10,
143
+ currency: "USD",
144
+ reference: "card-order-123",
145
+ callbackUrl: "https://example.com/payments/callback",
146
+ approveUrl: "https://example.com/payments/approved",
147
+ cancelUrl: "https://example.com/payments/cancelled",
148
+ declineUrl: "https://example.com/payments/declined",
149
+ });
150
+
151
+ if (!card.accepted || !card.orderNumber || !card.url) {
152
+ throw new Error(card.message || "Card checkout is unavailable");
153
+ }
154
+
155
+ // Store card.orderNumber, then redirect the customer to card.url.
156
+ ```
157
+
158
+ Public fields remain camelCase. The SDK writes string types `"1"`/`"2"`, `callback_url`, and the
159
+ card redirect fields in snake_case. Authorization is sent only in the bearer header. Remove any
160
+ application-side dependency on body authorization, `home_url`, description, or the old card `/pay`
161
+ endpoint. Payout input fields remain `amount`, `currency`, `phone`, `reference`, and `callbackUrl`;
162
+ its wire body still uses numeric type `1` and `callbackUrl`.
163
+
164
+ Phones now require exactly 12 digits, rather than any 12 characters. Amounts must be positive finite
165
+ numbers. References, callback strings, and redirect strings must contain non-whitespace content;
166
+ the SDK does not validate callback/redirect strings as absolute URLs. Check identifiers cannot be
167
+ blank or exactly `.` or `..`. Request validation does not silently trim or rewrite your values.
168
+
169
+ ## Separate acceptance from completion
170
+
171
+ Remove `client.isSuccessful()`. Use the result property for the operation you performed:
172
+
173
+ | Operation | Property | Meaning |
174
+ | --- | --- | --- |
175
+ | `mobile()`, `card()`, `payout()` | `accepted` | The provider returned initiation code 0. |
176
+ | `check()` | `successful` | Both lookup code and nested transaction status are 0. |
177
+ | `parseCallback()` | `successful` | The supplied callback claims code 0. |
178
+
179
+ An accepted initiation can still have a missing or empty order number or checkout URL. Check these
180
+ fields before using them. Acceptance does not confirm collection of funds.
181
+
182
+ Before:
183
+
184
+ ```ts
185
+ const check = await client.check("ORDER_NUMBER");
186
+
187
+ if (client.isSuccessful(check)) {
188
+ // Version 2 checked only the top-level lookup code.
189
+ }
190
+ ```
191
+
192
+ After:
193
+
194
+ ```ts
195
+ const check = await client.check("ORDER_NUMBER");
196
+
197
+ if (check.successful) {
198
+ // The lookup succeeded and the nested transaction has a success status.
199
+ }
200
+ ```
201
+
202
+ An observed live card check returned lookup code `0`, transaction status `4`, and a message saying
203
+ the operator declined that order. Version 3 preserves status `4` and returns `successful: false`.
204
+ This observation does not establish a global mapping for status `4`. A paid transaction with
205
+ nested status `0` and the provider's pending-status code remain unverified.
206
+
207
+ ## Migrate status comparisons and serialization
208
+
209
+ `Status` is now an immutable value object. Every response `code` and transaction `status` is a
210
+ `Status` instance. Remove `Status.SUCCESS`, `Status.FAILURE`, and direct numeric comparisons.
211
+
212
+ Before:
213
+
214
+ ```ts
215
+ import { Status } from "@ngandu-dev/flexpay";
216
+
217
+ const hasSuccessCode = check.code === Status.SUCCESS;
218
+ const hasFailureCode = check.transaction?.status === Status.FAILURE;
219
+ ```
220
+
221
+ After:
222
+
223
+ ```ts
224
+ const lookupSucceeded = check.code.isSuccessful();
225
+ const hasKnownFailureCode = check.transaction?.status.isFailure() === true;
226
+ const rawTransactionStatus = check.transaction?.status.value;
227
+ ```
228
+
229
+ `isSuccessful()` recognizes only `0`; `isFailure()` recognizes only `1`. Other safe integer codes
230
+ remain intact without an inferred state. For example, status `4` makes both methods return `false`.
231
+ Never treat `!status.isFailure()` as proof of payment success. Use `check.successful` for the full
232
+ check outcome, not `check.code.isSuccessful()` alone.
233
+
234
+ Use `new Status(integer)` for application-owned values. `StatusSchema` parses provider integers or
235
+ canonical integer strings into `Status`; it does not accept an already-normalized `Status` object.
236
+
237
+ JSON output changes with the value object:
238
+
239
+ ```ts
240
+ import { Status } from "@ngandu-dev/flexpay";
241
+
242
+ JSON.stringify(new Status(0)); // {"value":0}
243
+ ```
244
+
245
+ Serializing a whole SDK response preserves these nested objects. `JSON.parse` does not restore
246
+ their methods. If your persistence or API contract requires numeric codes, map `.value` explicitly:
247
+
248
+ ```ts
249
+ const storedCheck = {
250
+ code: check.code.value,
251
+ message: check.message,
252
+ transaction:
253
+ check.transaction === null
254
+ ? null
255
+ : { ...check.transaction, status: check.transaction.status.value },
256
+ };
257
+
258
+ const storedJson = JSON.stringify(storedCheck);
259
+ ```
260
+
261
+ To read that numeric representation back into SDK objects, parse it with `CheckResponseSchema`:
262
+
263
+ ```ts
264
+ import { CheckResponseSchema } from "@ngandu-dev/flexpay";
265
+
266
+ const restoredCheck = CheckResponseSchema.parse(JSON.parse(storedJson));
267
+ ```
268
+
269
+ ## Update response types and callback handling
270
+
271
+ `MobileResponse` now contains only `code`, `message`, `orderNumber`, and `accepted`.
272
+ `PayoutResponse` has the same shape. `CardResponse` adds nullable `url`. Reporting fields belong
273
+ to the new `CallbackResponse`, which has `successful`, amounts, phone, currency, timestamps,
274
+ channel, reference, order number, and `providerReference`.
275
+
276
+ Replace `client.handleCallback()` with standalone `parseCallback()`. No client or credential is
277
+ needed to parse a callback.
278
+
279
+ Before:
280
+
281
+ ```ts
282
+ const callback = client.handleCallback(incomingBody);
283
+ const claimedSuccess = client.isSuccessful(callback);
284
+ ```
285
+
286
+ After:
287
+
288
+ ```ts
289
+ import { parseCallback } from "@ngandu-dev/flexpay";
290
+
291
+ const callback = parseCallback(incomingBody);
292
+ const claimedSuccess = callback.successful;
293
+ ```
294
+
295
+ The parser validates payload structure and reports the payload's claim. It does not authenticate
296
+ the sender or authorize fulfillment. Keep that verification in your application.
297
+
298
+ Only the provider's wire field `provider_reference` populates `callback.providerReference`.
299
+ An input containing only the old camelCase alias `providerReference` leaves the output field null;
300
+ provider response objects permit extra fields, so the alias does not raise a validation error.
301
+
302
+ Normalized results, nested transactions, and statuses are frozen and readonly. Optional wire
303
+ fields such as card URL and nested transaction order number now normalize to required nullable
304
+ output properties. Missing messages normalize to an empty string. An empty order number remains
305
+ an empty string. Do not mutate a result or rely on `undefined` for these fields.
306
+
307
+ Update hand-built typed mocks to include `Status` instances, derived outcome properties, and all
308
+ normalized nullable fields. Exported schemas can build consistent fixtures from wire data:
309
+
310
+ ```ts
311
+ import { MobileResponseSchema } from "@ngandu-dev/flexpay";
312
+
313
+ const mobileResponse = MobileResponseSchema.parse({
314
+ code: "0",
315
+ message: "Accepted",
316
+ orderNumber: "TEST-ORDER",
317
+ });
318
+ ```
319
+
320
+ Do not pass a serialized SDK response containing `{ "value": 0 }` into a provider-response schema.
321
+ Use raw numeric/string wire codes, or explicitly restore the numeric representation as shown above.
322
+
323
+ ## Update error handling and retry assumptions
324
+
325
+ HTTP response parsing now occurs inside the transport. Malformed JSON or an invalid response
326
+ structure raises `NetworkException` with `type: "invalid_response"`, rather than a Zod error.
327
+ HTTP status and Content-Type remain available in the diagnostic. Parsed HTTP failures use
328
+ `type: "http_error"`; provider error bodies, error messages, and raw fetch/parser causes are no
329
+ longer copied into the exception. HTTP 200 business rejection codes remain decoded result objects.
330
+
331
+ The exception classes retain their HTTP mapping: 401/429 use `AccountException`, other 4xx use
332
+ `ClientException`, and 5xx use `ServerException`. Direct request/configuration validation and
333
+ `parseCallback` still raise Zod errors. Invalid check identifiers raise `Error`; invalid direct
334
+ `Status` construction raises `RangeError`.
335
+
336
+ Payment and payout POSTs are sent once. Only GET checks retry transient network, timeout, or 5xx
337
+ failures, up to three retries after the original attempt. Each attempt has a 30-second deadline.
338
+ Default waits are 500, 1,000, and 2,000 milliseconds. A numeric `Retry-After` of at most two seconds
339
+ can increase a wait; a longer, date-formatted, or invalid value suppresses retry. Known 4xx
340
+ responses do not retry, even if reading the body later fails.
341
+
342
+ Redirects now raise `NetworkException` with `type: "unexpected_redirect"` and are not followed.
343
+ Timeouts use `type: "timeout"`; unknown fetch failures use `type: "network_error"`. Unfinished
344
+ responses are closed when an attempt ends. A transport failure does not establish whether a
345
+ payment was accepted. Reconcile the original transaction before attempting another collection.