@ngandu-dev/flexpay 2.0.5 → 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.
- package/README.md +175 -125
- package/UPGRADE_3.0.0.md +345 -0
- package/dist/index.d.mts +186 -118
- package/dist/index.d.ts +186 -118
- package/dist/index.js +1 -1
- package/dist/index.mjs +1 -1
- package/package.json +6 -5
package/README.md
CHANGED
|
@@ -1,167 +1,217 @@
|
|
|
1
1
|
# FlexPay TypeScript
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
[](https://github.com/ngandu-dev/flexpay/actions/workflows/quality.yml)
|
|
6
|
-
[](https://github.com/ngandu-dev/flexpay/actions/workflows/test.yml)
|
|
7
|
-

|
|
3
|
+
TypeScript client for FlexPay corporate mobile-money and card collections, transaction checks,
|
|
4
|
+
callback parsing, and the existing payout API.
|
|
8
5
|
|
|
9
|
-
For
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
## Features
|
|
13
|
-
|
|
14
|
-
- Typed mobile-money and card payment requests
|
|
15
|
-
- Transaction status checks and callback validation
|
|
16
|
-
- Development and production API environments with optional custom base URLs
|
|
17
|
-
- ESM and CommonJS distributions with TypeScript declarations
|
|
18
|
-
|
|
19
|
-
## Requirements
|
|
20
|
-
|
|
21
|
-
- Node.js 24 or newer
|
|
6
|
+
For credentials and the original provider documentation, contact [FlexPay](https://flexpay.cd).
|
|
7
|
+
This package requires Node.js 24 or newer and ships ESM, CommonJS, and TypeScript declarations.
|
|
22
8
|
|
|
23
9
|
## Installation
|
|
24
10
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
```bash
|
|
28
|
-
npm install @ngandu-dev/flexpay
|
|
11
|
+
```sh
|
|
12
|
+
npm install @ngandu-dev/flexpay@^3
|
|
29
13
|
```
|
|
30
14
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
### Authentication
|
|
15
|
+
These examples use the version 3 API.
|
|
16
|
+
For version 2 migration steps, see [UPGRADE_3.0.0.md](UPGRADE_3.0.0.md).
|
|
34
17
|
|
|
35
|
-
|
|
36
|
-
You will receive a Merchant Form to complete in order to provide your business details and preferred Cash out Wallet or Banking Details.
|
|
37
|
-
- **Step 2**. Once the paperwork is completed, you will be issued with Live and Sandbox Accounts (Merchant Code and Authorization token)
|
|
18
|
+
## Configure a client
|
|
38
19
|
|
|
39
|
-
|
|
20
|
+
Every client requires a `Credential` and a `BaseUrl` with exactly three versioned prefixes:
|
|
21
|
+
`payment`, `check`, and `payout`. There is no environment switch or implicit production fallback.
|
|
40
22
|
|
|
41
23
|
```ts
|
|
42
|
-
import { Client
|
|
24
|
+
import { Client, productionBaseUrl } from "@ngandu-dev/flexpay";
|
|
43
25
|
|
|
44
|
-
const
|
|
26
|
+
const client = new Client(
|
|
27
|
+
{ merchant: "MERCHANT_CODE", token: "YOUR_TOKEN" },
|
|
28
|
+
productionBaseUrl,
|
|
29
|
+
);
|
|
45
30
|
```
|
|
46
31
|
|
|
47
|
-
|
|
32
|
+
`productionBaseUrl` is a frozen value containing:
|
|
48
33
|
|
|
49
|
-
|
|
34
|
+
| Property | Prefix | Operations |
|
|
35
|
+
| --- | --- | --- |
|
|
36
|
+
| `payment` | `https://corporateapi.flexpay.cd/api/rest/v1` | `/paymentService` for mobile and card |
|
|
37
|
+
| `check` | `https://apicheck.flexpaie.com/api/rest/v1` | `/check/{orderNumber}` |
|
|
38
|
+
| `payout` | `https://backend.flexpay.cd/api/rest/v1` | `/merchantPayOutService` |
|
|
39
|
+
|
|
40
|
+
FlexPay has not supplied a corporate sandbox URL. Point every prefix at your local simulator
|
|
41
|
+
when testing. Include the version path but omit the service path.
|
|
50
42
|
|
|
51
43
|
```ts
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
44
|
+
import { Client, type BaseUrl } from "@ngandu-dev/flexpay";
|
|
45
|
+
|
|
46
|
+
const localBaseUrl = {
|
|
47
|
+
payment: "http://localhost:8080/api/rest/v1",
|
|
48
|
+
check: "http://localhost:8080/api/rest/v1",
|
|
49
|
+
payout: "http://localhost:8080/api/rest/v1",
|
|
50
|
+
} satisfies BaseUrl;
|
|
51
|
+
|
|
52
|
+
const localClient = new Client(
|
|
53
|
+
{ merchant: "TESTMERCHANT", token: "test-token" },
|
|
54
|
+
localBaseUrl,
|
|
55
|
+
);
|
|
56
56
|
```
|
|
57
57
|
|
|
58
|
-
`
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
credentials, query strings, or fragments; invalid values throw during construction.
|
|
64
|
-
The optional `BaseUrls` type is exported from the package root. `Environment` also accepts these
|
|
65
|
-
options as its second argument when generating endpoint URLs directly.
|
|
58
|
+
`BaseUrlSchema` validates all three prefixes, removes surrounding whitespace and trailing slashes,
|
|
59
|
+
and freezes the result. Prefixes must be absolute HTTP(S) URLs without credentials, query strings,
|
|
60
|
+
fragments, or internal whitespace/control characters. Unknown configuration fields are rejected.
|
|
61
|
+
`CredentialSchema` rejects blank merchant codes and tokens without modifying their contents.
|
|
62
|
+
The client retains validated snapshots, so later caller mutations cannot change its configuration.
|
|
66
63
|
|
|
67
|
-
|
|
64
|
+
An optional third constructor argument accepts a Fetch-compatible function for testing or an
|
|
65
|
+
application-owned HTTP integration. It must honor the supplied abort signal and `redirect: "manual"`.
|
|
68
66
|
|
|
69
|
-
|
|
67
|
+
## Initiate payments
|
|
70
68
|
|
|
71
|
-
|
|
72
|
-
|
|
69
|
+
Use explicit methods. Both collections send JSON to the corporate payment endpoint with a bearer
|
|
70
|
+
header. Public input is camelCase; the client writes the documented snake_case wire fields.
|
|
73
71
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
phone: "243999999999",
|
|
78
|
-
reference: "mobile_order_12345",
|
|
79
|
-
description: "your_transaction_description",
|
|
80
|
-
callbackUrl: "https://example.com/flexpay/callback",
|
|
81
|
-
} satisfies MobileRequest;
|
|
82
|
-
|
|
83
|
-
const card = {
|
|
84
|
-
amount: 10, // 10 USD
|
|
72
|
+
```ts
|
|
73
|
+
const mobile = await client.mobile({
|
|
74
|
+
amount: 10,
|
|
85
75
|
currency: "USD",
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
callbackUrl: "https://example.com/
|
|
89
|
-
|
|
90
|
-
cancelUrl: "https://example.com/flexpay/cancelled",
|
|
91
|
-
declineUrl: "https://example.com/flexpay/declined",
|
|
92
|
-
homeUrl: "https://example.com",
|
|
93
|
-
} satisfies CardRequest;
|
|
94
|
-
```
|
|
95
|
-
|
|
96
|
-
> **Note**: we highly recommend your `callbacks` urls to be unique for each transaction.
|
|
97
|
-
|
|
98
|
-
### Mobile Payment
|
|
99
|
-
Once called, Flexpay will send a payment request to the user's mobile money account, and the user will have to confirm the payment on their phone.
|
|
100
|
-
after that the payment will be processed and the callback url will be called with the transaction details.
|
|
101
|
-
|
|
102
|
-
```typescript
|
|
103
|
-
const mobileResponse = await flexpay.pay(mobile);
|
|
104
|
-
```
|
|
105
|
-
|
|
106
|
-
### Visa Card Payment
|
|
107
|
-
You can set up card payment via VPOS features, which is typically used for online payments.
|
|
108
|
-
it's a gateway that allows you to accept payments from your customers using their credit cards.
|
|
109
|
-
|
|
110
|
-
```typescript
|
|
111
|
-
const cardResponse = await flexpay.pay(card);
|
|
112
|
-
// redirect to cardResponse.url to complete the payment
|
|
113
|
-
```
|
|
114
|
-
|
|
115
|
-
#### **handling callback (callbackUrl, approveUrl, cancelUrl, declineUrl)**
|
|
116
|
-
Flexpay will send a POST request to the defined callbackUrl and the response will contain the transaction details.
|
|
117
|
-
you can use the following code to validate the incoming payload.
|
|
76
|
+
phone: "243810000001",
|
|
77
|
+
reference: "mobile-order-123",
|
|
78
|
+
callbackUrl: "https://example.com/payments/callback",
|
|
79
|
+
});
|
|
118
80
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
```
|
|
81
|
+
if (!mobile.accepted || !mobile.orderNumber) {
|
|
82
|
+
throw new Error(mobile.message || "Payment initiation was not accepted");
|
|
83
|
+
}
|
|
123
84
|
|
|
124
|
-
|
|
125
|
-
|
|
85
|
+
const card = await client.card({
|
|
86
|
+
amount: 10,
|
|
87
|
+
currency: "USD",
|
|
88
|
+
reference: "card-order-123",
|
|
89
|
+
callbackUrl: "https://example.com/payments/callback",
|
|
90
|
+
approveUrl: "https://example.com/payments/approved",
|
|
91
|
+
cancelUrl: "https://example.com/payments/cancelled",
|
|
92
|
+
declineUrl: "https://example.com/payments/declined",
|
|
93
|
+
});
|
|
126
94
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
throw new Error("FlexPay did not return an order number");
|
|
95
|
+
if (!card.accepted || !card.orderNumber || !card.url) {
|
|
96
|
+
throw new Error(card.message || "Card checkout is unavailable");
|
|
130
97
|
}
|
|
131
98
|
|
|
132
|
-
|
|
133
|
-
flexpay.isSuccessful(tx); // true or false
|
|
99
|
+
// Store card.orderNumber, then redirect the customer to card.url.
|
|
134
100
|
```
|
|
135
101
|
|
|
136
|
-
|
|
102
|
+
`accepted` reports request acceptance, not collection of funds. Wait for the transaction outcome
|
|
103
|
+
before fulfilling an order. Card checkout URLs and order numbers can be null or empty on rejection.
|
|
137
104
|
|
|
138
|
-
|
|
139
|
-
|
|
105
|
+
Inputs reject unknown fields. Amounts must be positive finite numbers, phone numbers must contain
|
|
106
|
+
exactly 12 digits, and references and callback/redirect strings must not be blank. References have
|
|
107
|
+
no client-imposed length cap.
|
|
140
108
|
|
|
141
|
-
|
|
109
|
+
`client.payout()` accepts `amount`, `currency`, `phone`, `reference`, and `callbackUrl`. Its response
|
|
110
|
+
has `accepted`. Payouts retain the existing provider protocol, including the `callbackUrl` wire field;
|
|
111
|
+
the corporate collection document does not replace that protocol.
|
|
142
112
|
|
|
143
|
-
|
|
144
|
-
See [CONTRIBUTING.md](CONTRIBUTING.md) for the complete contribution workflow.
|
|
113
|
+
## Read transaction outcomes
|
|
145
114
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
Run `bun run test` for the test suite or `bun run test:coverage` for a coverage report.
|
|
149
|
-
|
|
150
|
-
## Contributing
|
|
151
|
-
|
|
152
|
-
Contributions are welcome. Please read [CONTRIBUTING.md](CONTRIBUTING.md) and follow our
|
|
153
|
-
[Code of Conduct](CODE_OF_CONDUCT.md).
|
|
115
|
+
```ts
|
|
116
|
+
import { parseCallback } from "@ngandu-dev/flexpay";
|
|
154
117
|
|
|
155
|
-
|
|
118
|
+
const check = await client.check(mobile.orderNumber);
|
|
156
119
|
|
|
157
|
-
|
|
120
|
+
if (check.successful) {
|
|
121
|
+
// The lookup succeeded and the nested transaction has a success status.
|
|
122
|
+
}
|
|
158
123
|
|
|
159
|
-
|
|
124
|
+
// incomingBody is the unknown JSON body received by your callback endpoint.
|
|
125
|
+
const callback = parseCallback(incomingBody);
|
|
160
126
|
|
|
161
|
-
|
|
127
|
+
if (callback.successful) {
|
|
128
|
+
// The parsed callback claims success. Authenticate its sender in your application.
|
|
129
|
+
}
|
|
130
|
+
```
|
|
162
131
|
|
|
163
|
-
|
|
132
|
+
`parseCallback` requires no client or credentials. It validates the payload; it does not authenticate
|
|
133
|
+
the sender, establish webhook trust, or fulfill an order. `CallbackResponse` preserves amounts,
|
|
134
|
+
phone, currency, timestamps, channel, reference, order number, and `providerReference`. The wire
|
|
135
|
+
field is `provider_reference`; a pre-normalized `providerReference` input alias is not supported.
|
|
136
|
+
|
|
137
|
+
Every response `code` and transaction `status` is an immutable `Status` instance.
|
|
138
|
+
`status.value` preserves the numeric provider code. `status.isSuccessful()`
|
|
139
|
+
means code `0`, and `status.isFailure()` means code `1`. Other integer codes are retained without
|
|
140
|
+
assigning them a meaning. For example, a simulator status of `2` makes both methods return `false`.
|
|
141
|
+
|
|
142
|
+
Initiation results expose `accepted`; callback and check results expose `successful`. Check success
|
|
143
|
+
requires both a successful lookup code and a successful nested transaction status. A missing,
|
|
144
|
+
failed, pending, or unknown transaction cannot produce `successful: true`. Response objects,
|
|
145
|
+
nested transactions, and their `Status` objects are frozen so these derived outcomes cannot become stale.
|
|
146
|
+
|
|
147
|
+
Live checks have verified both the unknown-order rejection and an existing card order with lookup
|
|
148
|
+
code `0`, nested status `4`, and a message stating that the operator declined that order. The client
|
|
149
|
+
preserves status `4` and returns `successful: false`; this observation does not assign status `4` a
|
|
150
|
+
global meaning. The response omitted the nested order number, which normalizes to `null`. A paid
|
|
151
|
+
transaction with nested status `0` and the upstream pending-status code remain unverified.
|
|
152
|
+
|
|
153
|
+
## Failure behavior
|
|
154
|
+
|
|
155
|
+
Each HTTP attempt has a 30-second deadline. Payment and payout POST requests are sent once.
|
|
156
|
+
GET checks can retry three times after transient network, timeout, or server failures. Default delays
|
|
157
|
+
are 500, 1,000, and 2,000 milliseconds. On a 5xx response, a numeric `Retry-After` of at most two seconds
|
|
158
|
+
raises the delay when it exceeds the default. A longer, date-formatted, or invalid `Retry-After`
|
|
159
|
+
stops retries instead of retrying earlier than requested. The four attempts and three bounded waits
|
|
160
|
+
take at most 126 seconds, plus event-loop scheduling and processing time. No automatic retry follows
|
|
161
|
+
a 4xx response or malformed HTTP 200 response.
|
|
162
|
+
|
|
163
|
+
Redirects are not followed. A 3xx response raises `NetworkException` with `type: "unexpected_redirect"`.
|
|
164
|
+
This prevents the transport from forwarding a bearer token or replaying a POST to another location.
|
|
165
|
+
A timeout raises `NetworkException` with `type: "timeout"`.
|
|
166
|
+
|
|
167
|
+
Unreadable JSON and structurally invalid provider responses raise `NetworkException` with
|
|
168
|
+
`type: "invalid_response"`, HTTP status, and Content-Type. HTTP classifications are retained:
|
|
169
|
+
401/429 use `AccountException`, other 4xx use `ClientException`, and 5xx use `ServerException`.
|
|
170
|
+
HTTP failures containing JSON use `type: "http_error"`. Provider error bodies and raw fetch/parser
|
|
171
|
+
causes are not copied into these exceptions. Local request/configuration validation and
|
|
172
|
+
`parseCallback` report Zod validation errors.
|
|
173
|
+
|
|
174
|
+
A transport failure does not prove that a payment was rejected. Reconcile the original transaction
|
|
175
|
+
before attempting another collection.
|
|
176
|
+
|
|
177
|
+
## Design and tests
|
|
178
|
+
|
|
179
|
+
`Client` coordinates four provider operations. Configuration schemas own input validation and
|
|
180
|
+
immutable endpoint/credential snapshots. Request schemas and wire factories own request shape;
|
|
181
|
+
response schemas own normalization and distinguish acceptance from completion. `Status` owns the
|
|
182
|
+
meaning of a numeric provider code. `HttpTransport` owns bearer authentication, deadlines,
|
|
183
|
+
redirect policy, response validation failures, and bounded read retries.
|
|
184
|
+
|
|
185
|
+
For a card collection, the client validates input, creates the wire body, and asks the transport
|
|
186
|
+
for a response parsed by the card schema. That schema computes `accepted` from its `Status`.
|
|
187
|
+
For an HTML 403, the transport returns a `ClientException` marked `invalid_response`, sends no
|
|
188
|
+
second POST, and excludes the HTML from the diagnostic. The caller owns reconciliation.
|
|
189
|
+
|
|
190
|
+
```sh
|
|
191
|
+
bun install --frozen-lockfile
|
|
192
|
+
bun run format
|
|
193
|
+
bun run quality
|
|
194
|
+
bun run test:coverage
|
|
195
|
+
bun audit
|
|
196
|
+
```
|
|
164
197
|
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
198
|
+
`quality` runs formatting checks, type checks, tests, builds, and package/export validation.
|
|
199
|
+
Tests exercise injected Fetch implementations, exact wire payloads, immutable statuses,
|
|
200
|
+
acceptance/completion semantics, deadlines, redirects, and error classifications.
|
|
201
|
+
|
|
202
|
+
`src/__tests__/fixtures/corporate.json` contains deterministic simulator examples. Its pending code
|
|
203
|
+
`2` is a simulator convention, not a confirmed provider mapping. The provider rejection fixtures
|
|
204
|
+
record controlled observations from October 29, 2026. Mobile `callback_url` and `callbackUrl` probes
|
|
205
|
+
both advanced past missing-callback validation to an invalid-amount rejection. Card string probes
|
|
206
|
+
were inconclusive because absent callbacks also reached the amount error. Object-field controls
|
|
207
|
+
supported recognition of both spellings during input handling but did not verify initiation or
|
|
208
|
+
callback delivery. Serialization therefore keeps the documented `callback_url` field.
|
|
209
|
+
`provider-existing-order.json` preserves an observed declined-card check shape with an anonymized
|
|
210
|
+
order/reference and a synthetic timestamp in the observed format.
|
|
211
|
+
No automated test uses real credentials or submits a provider payment.
|
|
212
|
+
|
|
213
|
+
## Contributing and security
|
|
214
|
+
|
|
215
|
+
See [CONTRIBUTING.md](CONTRIBUTING.md) for the contribution workflow and
|
|
216
|
+
[SECURITY.md](SECURITY.md) for private vulnerability reporting. Contributions follow the
|
|
217
|
+
[Code of Conduct](CODE_OF_CONDUCT.md). Released under the [MIT License](LICENSE).
|