@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.
package/README.md CHANGED
@@ -1,167 +1,217 @@
1
1
  # FlexPay TypeScript
2
2
 
3
- ![npm](https://img.shields.io/npm/v/@ngandu-dev/flexpay?style=flat-square)
4
- ![npm](https://img.shields.io/npm/dt/@ngandu-dev/flexpay?style=flat-square)
5
- [![Quality](https://github.com/ngandu-dev/flexpay/actions/workflows/quality.yml/badge.svg?branch=main)](https://github.com/ngandu-dev/flexpay/actions/workflows/quality.yml)
6
- [![Tests](https://github.com/ngandu-dev/flexpay/actions/workflows/test.yml/badge.svg?branch=main)](https://github.com/ngandu-dev/flexpay/actions/workflows/test.yml)
7
- ![GitHub](https://img.shields.io/github/license/ngandu-dev/flexpay?style=flat-square)
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 privacy reasons, FlexPay's original documentation cannot be shared without written permission.
10
- For credentials and implementation details, contact [FlexPay](https://flexpay.cd).
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
- Add the TypeScript client to your application's dependencies:
26
-
27
- ```bash
28
- npm install @ngandu-dev/flexpay
11
+ ```sh
12
+ npm install @ngandu-dev/flexpay@^3
29
13
  ```
30
14
 
31
- ## Quick start
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
- - **Step 1**. Contact FlexPay to get a Merchant Account
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
- Then use these credentials to authenticate your client
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 as Flexpay } from "@ngandu-dev/flexpay";
24
+ import { Client, productionBaseUrl } from "@ngandu-dev/flexpay";
43
25
 
44
- const flexpay = new Flexpay("merchant_code", "token", "dev"); // use "prod" for production
26
+ const client = new Client(
27
+ { merchant: "MERCHANT_CODE", token: "YOUR_TOKEN" },
28
+ productionBaseUrl,
29
+ );
45
30
  ```
46
31
 
47
- ### Local simulator or custom endpoints
32
+ `productionBaseUrl` is a frozen value containing:
48
33
 
49
- Pass optional versioned prefixes as the fourth constructor argument:
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
- const flexpay = new Flexpay("TESTMERCHANT", "test-token", "dev", {
53
- apiBaseUrl: "http://localhost:8080/api/rest/v1",
54
- cardBaseUrl: "http://localhost:8080/v1.1",
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
- `apiBaseUrl` is shared by mobile payments, payouts, and status checks; the client appends
59
- `/paymentService`, `/merchantPayOutService`, or `/check/{orderNumber}`. `cardBaseUrl` gets `/pay`
60
- appended. Include the version path in each prefix, and omit the final service path.
61
- Trailing slashes are removed. Each omitted option keeps its selected `dev`/`prod` default,
62
- so existing constructor calls continue to work. Overrides must be absolute HTTP(S) URLs without
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
- ## Usage
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
- ### Create a Payment Request
67
+ ## Initiate payments
70
68
 
71
- ```typescript
72
- import type { CardRequest, MobileRequest } from "@ngandu-dev/flexpay";
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
- const mobile = {
75
- amount: 10, // 10 USD
76
- currency: "USD",
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
- reference: "card_order_12345",
87
- description: "your_transaction_description",
88
- callbackUrl: "https://example.com/flexpay/callback",
89
- approveUrl: "https://example.com/flexpay/approved",
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
- ```typescript
120
- const webhook = flexpay.handleCallback(req.body);
121
- flexpay.isSuccessful(webhook); // true or false
122
- ```
81
+ if (!mobile.accepted || !mobile.orderNumber) {
82
+ throw new Error(mobile.message || "Payment initiation was not accepted");
83
+ }
123
84
 
124
- ### Check Transaction state
125
- You don't trust webhook ? you can always check the transaction state by providing the order number.
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
- ```typescript
128
- if (!mobileResponse.orderNumber) {
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
- const tx = await flexpay.check(mobileResponse.orderNumber);
133
- flexpay.isSuccessful(tx); // true or false
99
+ // Store card.orderNumber, then redirect the customer to card.url.
134
100
  ```
135
101
 
136
- ## Migrating to `@ngandu-dev/flexpay`
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
- Replace the dependency and every import with `@ngandu-dev/flexpay`. Version 2 is a clean package
139
- move and does not provide an alias for the old scope.
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
- ## Development
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
- Install dependencies with `bun install`, then run `bun run quality` before opening a pull request.
144
- See [CONTRIBUTING.md](CONTRIBUTING.md) for the complete contribution workflow.
113
+ ## Read transaction outcomes
145
114
 
146
- ## Testing
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
- ## Security
118
+ const check = await client.check(mobile.orderNumber);
156
119
 
157
- Please report vulnerabilities privately as described in [SECURITY.md](SECURITY.md).
120
+ if (check.successful) {
121
+ // The lookup succeeded and the nested transaction has a success status.
122
+ }
158
123
 
159
- ## License
124
+ // incomingBody is the unknown JSON body received by your callback endpoint.
125
+ const callback = parseCallback(incomingBody);
160
126
 
161
- Released under the [MIT License](LICENSE).
127
+ if (callback.successful) {
128
+ // The parsed callback claims success. Authenticate its sender in your application.
129
+ }
130
+ ```
162
131
 
163
- ## Contributors
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
- <a href="https://github.com/ngandu-dev/flexpay/graphs/contributors" title="Show all contributors">
166
- <img src="https://contrib.rocks/image?repo=ngandu-dev/flexpay" alt="Contributors" />
167
- </a>
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).