remita-checkout-client 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 +0 -0
- package/README.md +138 -0
- package/dist/chunk-MGQQHIIZ.mjs +275 -0
- package/dist/chunk-MGQQHIIZ.mjs.map +1 -0
- package/dist/cli.d.mts +1 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +453 -0
- package/dist/cli.js.map +1 -0
- package/dist/cli.mjs +231 -0
- package/dist/cli.mjs.map +1 -0
- package/dist/index.d.mts +225 -0
- package/dist/index.d.ts +225 -0
- package/dist/index.js +347 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +102 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +58 -0
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
type RemitaEnvironment = "qa" | "demo" | "production";
|
|
2
|
+
declare const ENVIRONMENT_BASE_URLS: Record<RemitaEnvironment, string>;
|
|
3
|
+
interface ResolveEnvironmentOptions {
|
|
4
|
+
/** Explicit override — always wins over prefix detection. */
|
|
5
|
+
environment?: RemitaEnvironment;
|
|
6
|
+
/** Custom base URLs, e.g. to point at a local/staging mirror. */
|
|
7
|
+
baseUrls?: Partial<Record<RemitaEnvironment, string>>;
|
|
8
|
+
}
|
|
9
|
+
interface ResolvedEnvironment {
|
|
10
|
+
environment: RemitaEnvironment;
|
|
11
|
+
baseUrl: string;
|
|
12
|
+
/** False when detection couldn't confidently match a prefix and we fell back to a default. */
|
|
13
|
+
detected: boolean;
|
|
14
|
+
}
|
|
15
|
+
declare function resolveEnvironment(publicKey: string, options?: ResolveEnvironmentOptions): ResolvedEnvironment;
|
|
16
|
+
|
|
17
|
+
interface ApiErrorPayload {
|
|
18
|
+
responseCode?: string;
|
|
19
|
+
responseMessage?: string;
|
|
20
|
+
message?: string;
|
|
21
|
+
[key: string]: unknown;
|
|
22
|
+
}
|
|
23
|
+
declare class RemitaApiError extends Error {
|
|
24
|
+
readonly status: number;
|
|
25
|
+
readonly payload: ApiErrorPayload | undefined;
|
|
26
|
+
constructor(message: string, status: number, payload?: ApiErrorPayload);
|
|
27
|
+
}
|
|
28
|
+
interface PrePaymentRequest {
|
|
29
|
+
firstName: string;
|
|
30
|
+
lastName: string;
|
|
31
|
+
email: string;
|
|
32
|
+
phoneNumber: string;
|
|
33
|
+
paymentIdentifier: string;
|
|
34
|
+
currency: string;
|
|
35
|
+
narration?: string;
|
|
36
|
+
amount: number;
|
|
37
|
+
[key: string]: unknown;
|
|
38
|
+
}
|
|
39
|
+
interface PaymentChannel {
|
|
40
|
+
code: string;
|
|
41
|
+
name: string;
|
|
42
|
+
[key: string]: unknown;
|
|
43
|
+
}
|
|
44
|
+
interface PrePaymentResponse {
|
|
45
|
+
merchantName?: string;
|
|
46
|
+
amount?: number | string;
|
|
47
|
+
currency?: string;
|
|
48
|
+
channels?: PaymentChannel[];
|
|
49
|
+
/** Forwarded from /payment/pre if the API echoes one. */
|
|
50
|
+
transactionId?: string;
|
|
51
|
+
paymentReference?: string;
|
|
52
|
+
[key: string]: unknown;
|
|
53
|
+
}
|
|
54
|
+
interface FeeChargeRequest {
|
|
55
|
+
amount: number;
|
|
56
|
+
channel: string;
|
|
57
|
+
[key: string]: unknown;
|
|
58
|
+
}
|
|
59
|
+
interface FeeChargeResponse {
|
|
60
|
+
fee?: number | string;
|
|
61
|
+
totalAmount?: number | string;
|
|
62
|
+
[key: string]: unknown;
|
|
63
|
+
}
|
|
64
|
+
interface InitiatePaymentRequest {
|
|
65
|
+
channel: string;
|
|
66
|
+
amount: number;
|
|
67
|
+
transactionId?: string;
|
|
68
|
+
[key: string]: unknown;
|
|
69
|
+
}
|
|
70
|
+
interface InitiatePaymentResponse {
|
|
71
|
+
/** Where to POST the next-step authorization payload (OTP, PIN, 3DS fields, etc). */
|
|
72
|
+
actionUrl?: string;
|
|
73
|
+
paymentReference?: string;
|
|
74
|
+
transactionId?: string;
|
|
75
|
+
status?: string;
|
|
76
|
+
[key: string]: unknown;
|
|
77
|
+
}
|
|
78
|
+
type AuthorizePayload = Record<string, unknown>;
|
|
79
|
+
interface AuthorizeResponse {
|
|
80
|
+
status?: string;
|
|
81
|
+
requiresValidation?: boolean;
|
|
82
|
+
[key: string]: unknown;
|
|
83
|
+
}
|
|
84
|
+
interface ValidatePaymentRequest {
|
|
85
|
+
paymentReference?: string;
|
|
86
|
+
transactionId?: string;
|
|
87
|
+
otp?: string;
|
|
88
|
+
[key: string]: unknown;
|
|
89
|
+
}
|
|
90
|
+
interface ValidatePaymentResponse {
|
|
91
|
+
status?: string;
|
|
92
|
+
[key: string]: unknown;
|
|
93
|
+
}
|
|
94
|
+
type PaymentStatus = "APPROVED" | "PENDING" | "FAILED" | (string & {});
|
|
95
|
+
interface VerifyPaymentResponse {
|
|
96
|
+
status?: PaymentStatus;
|
|
97
|
+
paymentReference?: string;
|
|
98
|
+
amount?: number | string;
|
|
99
|
+
[key: string]: unknown;
|
|
100
|
+
}
|
|
101
|
+
interface Bank {
|
|
102
|
+
code: string;
|
|
103
|
+
name: string;
|
|
104
|
+
currency?: string;
|
|
105
|
+
[key: string]: unknown;
|
|
106
|
+
}
|
|
107
|
+
interface BankListResponse {
|
|
108
|
+
banks: Bank[];
|
|
109
|
+
[key: string]: unknown;
|
|
110
|
+
}
|
|
111
|
+
interface UssdCode {
|
|
112
|
+
bankCode: string;
|
|
113
|
+
ussdCode: string;
|
|
114
|
+
[key: string]: unknown;
|
|
115
|
+
}
|
|
116
|
+
interface UssdCodesResponse {
|
|
117
|
+
codes: UssdCode[];
|
|
118
|
+
[key: string]: unknown;
|
|
119
|
+
}
|
|
120
|
+
interface WalletType {
|
|
121
|
+
code: string;
|
|
122
|
+
name: string;
|
|
123
|
+
[key: string]: unknown;
|
|
124
|
+
}
|
|
125
|
+
interface WalletTypesResponse {
|
|
126
|
+
wallets: WalletType[];
|
|
127
|
+
[key: string]: unknown;
|
|
128
|
+
}
|
|
129
|
+
interface StatusStreamEvent {
|
|
130
|
+
status?: PaymentStatus;
|
|
131
|
+
[key: string]: unknown;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
interface StatusStreamOptions {
|
|
135
|
+
onMessage: (event: {
|
|
136
|
+
data: unknown;
|
|
137
|
+
raw: string;
|
|
138
|
+
}) => void;
|
|
139
|
+
onError?: (err: unknown) => void;
|
|
140
|
+
onOpen?: () => void;
|
|
141
|
+
fetchImpl?: typeof fetch;
|
|
142
|
+
/** Optional override; publicKey is sent as a query param by default. */
|
|
143
|
+
sendPublicKeyHeader?: boolean;
|
|
144
|
+
}
|
|
145
|
+
interface StatusStreamHandle {
|
|
146
|
+
close: () => void;
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Opens the payment status SSE stream using a manual fetch + ReadableStream
|
|
150
|
+
* reader rather than the browser-only `EventSource` global, so this works
|
|
151
|
+
* identically in the browser and in Node 18+.
|
|
152
|
+
*/
|
|
153
|
+
declare function openStatusStream(baseUrl: string, trxRef: string, publicKey: string, options: StatusStreamOptions): StatusStreamHandle;
|
|
154
|
+
|
|
155
|
+
interface RemitaCheckoutClientOptions extends ResolveEnvironmentOptions {
|
|
156
|
+
publicKey: string;
|
|
157
|
+
fetchImpl?: typeof fetch;
|
|
158
|
+
timeoutMs?: number;
|
|
159
|
+
/** Extra headers merged after `publicKey` is set (so they can override). */
|
|
160
|
+
defaultHeaders?: Record<string, string>;
|
|
161
|
+
/** Called once if the environment couldn't be confidently detected from the key prefix (falls back to "qa"). */
|
|
162
|
+
onEnvironmentDetectionFallback?: (guessed: "qa") => void;
|
|
163
|
+
}
|
|
164
|
+
declare class RemitaCheckoutClient {
|
|
165
|
+
readonly publicKey: string;
|
|
166
|
+
readonly environment: string;
|
|
167
|
+
readonly baseUrl: string;
|
|
168
|
+
private readonly http;
|
|
169
|
+
constructor(options: RemitaCheckoutClientOptions);
|
|
170
|
+
prePayment(request: PrePaymentRequest): Promise<PrePaymentResponse>;
|
|
171
|
+
calculateFee(request: FeeChargeRequest): Promise<FeeChargeResponse>;
|
|
172
|
+
initiatePayment(request: InitiatePaymentRequest): Promise<InitiatePaymentResponse>;
|
|
173
|
+
/**
|
|
174
|
+
* Posts authorization params (OTP, PIN, 3DS form fields, etc) to the
|
|
175
|
+
* processor-provided actionUrl from initiatePayment().
|
|
176
|
+
*
|
|
177
|
+
* NOTE: CyberSource/Cardinal 3DS endpoints expect
|
|
178
|
+
* `application/x-www-form-urlencoded`. Pass `contentType` if the
|
|
179
|
+
* actionUrl is a 3DS processor endpoint.
|
|
180
|
+
*/
|
|
181
|
+
authorize(actionUrl: string, payload: AuthorizePayload, contentType?: string): Promise<AuthorizeResponse>;
|
|
182
|
+
/**
|
|
183
|
+
* Validates second-factor authorization (e.g. OTP). Per the endpoint docs
|
|
184
|
+
* this may go to a fixed /payment/validate endpoint OR to a dynamic
|
|
185
|
+
* actionUrl — pass `actionUrl` if the preceding step returned one.
|
|
186
|
+
*/
|
|
187
|
+
validatePayment(request: ValidatePaymentRequest, actionUrl?: string): Promise<ValidatePaymentResponse>;
|
|
188
|
+
verifyPayment(paymentRef: string): Promise<VerifyPaymentResponse>;
|
|
189
|
+
/** Opens an SSE stream for real-time status updates. Call the returned `close()` when done. */
|
|
190
|
+
streamPaymentStatus(trxRef: string, options: StatusStreamOptions): StatusStreamHandle;
|
|
191
|
+
getBankList(): Promise<BankListResponse>;
|
|
192
|
+
getUssdCodes(): Promise<UssdCodesResponse>;
|
|
193
|
+
getWalletTypes(): Promise<WalletTypesResponse>;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
type PaymentSessionStage = "idle" | "pre" | "fee" | "initiated" | "authorized" | "validated" | "verified" | "failed";
|
|
197
|
+
/**
|
|
198
|
+
* Sequences the multi-step payment flow (pre → fee → initiate → authorize →
|
|
199
|
+
* validate → verify) and tracks the refs each step hands to the next
|
|
200
|
+
* (paymentReference, transactionId, actionUrl), so callers don't have to
|
|
201
|
+
* thread that state through by hand.
|
|
202
|
+
*/
|
|
203
|
+
declare class PaymentSession {
|
|
204
|
+
private readonly client;
|
|
205
|
+
stage: PaymentSessionStage;
|
|
206
|
+
paymentReference?: string;
|
|
207
|
+
transactionId?: string;
|
|
208
|
+
/** URL returned by initiate() for the *authorize* step. */
|
|
209
|
+
private authorizeActionUrl?;
|
|
210
|
+
/** URL returned by authorize() for the *validate* step, if different. */
|
|
211
|
+
private validateActionUrl?;
|
|
212
|
+
constructor(client: RemitaCheckoutClient);
|
|
213
|
+
pre(request: PrePaymentRequest): Promise<PrePaymentResponse>;
|
|
214
|
+
fee(request: FeeChargeRequest): Promise<FeeChargeResponse>;
|
|
215
|
+
initiate(request: InitiatePaymentRequest): Promise<InitiatePaymentResponse>;
|
|
216
|
+
/**
|
|
217
|
+
* Uses the actionUrl captured from initiate() unless you pass one explicitly.
|
|
218
|
+
* Pass `contentType` for 3DS processor endpoints that require form-encoding.
|
|
219
|
+
*/
|
|
220
|
+
authorize(payload: AuthorizePayload, actionUrlOverride?: string, contentType?: string): Promise<AuthorizeResponse>;
|
|
221
|
+
validate(request: ValidatePaymentRequest, actionUrlOverride?: string): Promise<ValidatePaymentResponse>;
|
|
222
|
+
verify(paymentRefOverride?: string): Promise<VerifyPaymentResponse>;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
export { type ApiErrorPayload, type AuthorizePayload, type AuthorizeResponse, type Bank, type BankListResponse, ENVIRONMENT_BASE_URLS, type FeeChargeRequest, type FeeChargeResponse, type InitiatePaymentRequest, type InitiatePaymentResponse, type PaymentChannel, PaymentSession, type PaymentSessionStage, type PaymentStatus, type PrePaymentRequest, type PrePaymentResponse, RemitaApiError, RemitaCheckoutClient, type RemitaCheckoutClientOptions, type RemitaEnvironment, type ResolveEnvironmentOptions, type ResolvedEnvironment, type StatusStreamEvent, type StatusStreamHandle, type StatusStreamOptions, type UssdCode, type UssdCodesResponse, type ValidatePaymentRequest, type ValidatePaymentResponse, type VerifyPaymentResponse, type WalletType, type WalletTypesResponse, openStatusStream, resolveEnvironment };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,347 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/index.ts
|
|
21
|
+
var index_exports = {};
|
|
22
|
+
__export(index_exports, {
|
|
23
|
+
ENVIRONMENT_BASE_URLS: () => ENVIRONMENT_BASE_URLS,
|
|
24
|
+
PaymentSession: () => PaymentSession,
|
|
25
|
+
RemitaApiError: () => RemitaApiError2,
|
|
26
|
+
RemitaCheckoutClient: () => RemitaCheckoutClient,
|
|
27
|
+
openStatusStream: () => openStatusStream,
|
|
28
|
+
resolveEnvironment: () => resolveEnvironment
|
|
29
|
+
});
|
|
30
|
+
module.exports = __toCommonJS(index_exports);
|
|
31
|
+
|
|
32
|
+
// src/http-client.ts
|
|
33
|
+
var RemitaApiError = class extends Error {
|
|
34
|
+
constructor(message, status, payload) {
|
|
35
|
+
super(message);
|
|
36
|
+
this.status = status;
|
|
37
|
+
this.payload = payload;
|
|
38
|
+
this.name = "RemitaApiError";
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
var HttpClient = class {
|
|
42
|
+
constructor(options) {
|
|
43
|
+
this.baseUrl = options.baseUrl.replace(/\/+$/, "");
|
|
44
|
+
this.fetchImpl = options.fetchImpl ?? (typeof fetch !== "undefined" ? fetch : void 0);
|
|
45
|
+
this.timeoutMs = options.timeoutMs ?? 3e4;
|
|
46
|
+
this.defaultHeaders = options.defaultHeaders ?? {};
|
|
47
|
+
if (!this.fetchImpl) {
|
|
48
|
+
throw new Error("No fetch implementation available for HttpClient.");
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
async request(method, path, opts = {}) {
|
|
52
|
+
const url = opts.absoluteUrl ? opts.absoluteUrl : `${this.baseUrl}${path.startsWith("/") ? "" : "/"}${path}`;
|
|
53
|
+
const headers = {
|
|
54
|
+
Accept: "application/json",
|
|
55
|
+
...this.defaultHeaders,
|
|
56
|
+
...opts.headers
|
|
57
|
+
};
|
|
58
|
+
let body;
|
|
59
|
+
if (opts.body !== void 0) {
|
|
60
|
+
if (opts.contentType === "application/x-www-form-urlencoded") {
|
|
61
|
+
headers["Content-Type"] = opts.contentType;
|
|
62
|
+
body = new URLSearchParams(
|
|
63
|
+
Object.entries(opts.body).map(([k, v]) => [
|
|
64
|
+
k,
|
|
65
|
+
String(v)
|
|
66
|
+
])
|
|
67
|
+
).toString();
|
|
68
|
+
} else {
|
|
69
|
+
headers["Content-Type"] = opts.contentType ?? "application/json";
|
|
70
|
+
body = JSON.stringify(opts.body);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
const controller = new AbortController();
|
|
74
|
+
const timer = this.timeoutMs > 0 ? setTimeout(() => controller.abort(), this.timeoutMs) : void 0;
|
|
75
|
+
try {
|
|
76
|
+
const response = await this.fetchImpl(url, {
|
|
77
|
+
method,
|
|
78
|
+
headers,
|
|
79
|
+
body,
|
|
80
|
+
signal: controller.signal
|
|
81
|
+
});
|
|
82
|
+
const text = await response.text();
|
|
83
|
+
let parsed = text;
|
|
84
|
+
try {
|
|
85
|
+
parsed = text ? JSON.parse(text) : void 0;
|
|
86
|
+
} catch {
|
|
87
|
+
}
|
|
88
|
+
if (!response.ok) {
|
|
89
|
+
const payload = parsed ?? {};
|
|
90
|
+
const message = payload.responseMessage ?? payload.message ?? `Request failed with status ${response.status}`;
|
|
91
|
+
throw new RemitaApiError(message, response.status, payload);
|
|
92
|
+
}
|
|
93
|
+
return parsed;
|
|
94
|
+
} finally {
|
|
95
|
+
if (timer) clearTimeout(timer);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
// src/environment.ts
|
|
101
|
+
var ENVIRONMENT_BASE_URLS = {
|
|
102
|
+
qa: "https://api-checkout-qa.systemspecsng.com/api/v1",
|
|
103
|
+
demo: "https://api-checkout-demo.systemspecsng.com/api/v1",
|
|
104
|
+
production: "https://api-checkout.systemspecsng.com/api/v1"
|
|
105
|
+
};
|
|
106
|
+
var PREFIX_RULES = [
|
|
107
|
+
{ pattern: /^(qa[-_]|pk_qa_)/i, environment: "qa" },
|
|
108
|
+
{ pattern: /^(demo[-_]|pk_demo_|pk_test_)/i, environment: "demo" }
|
|
109
|
+
];
|
|
110
|
+
function resolveEnvironment(publicKey, options = {}) {
|
|
111
|
+
const baseUrls = { ...ENVIRONMENT_BASE_URLS, ...options.baseUrls };
|
|
112
|
+
if (options.environment) {
|
|
113
|
+
return {
|
|
114
|
+
environment: options.environment,
|
|
115
|
+
baseUrl: baseUrls[options.environment],
|
|
116
|
+
detected: true
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
for (const rule of PREFIX_RULES) {
|
|
120
|
+
if (rule.pattern.test(publicKey)) {
|
|
121
|
+
return {
|
|
122
|
+
environment: rule.environment,
|
|
123
|
+
baseUrl: baseUrls[rule.environment],
|
|
124
|
+
detected: true
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
return { environment: "qa", baseUrl: baseUrls.qa, detected: false };
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// src/stream.ts
|
|
132
|
+
function openStatusStream(baseUrl, trxRef, publicKey, options) {
|
|
133
|
+
const fetchImpl = options.fetchImpl ?? (typeof fetch !== "undefined" ? fetch : void 0);
|
|
134
|
+
if (!fetchImpl) {
|
|
135
|
+
throw new Error("No fetch implementation available for streamPaymentStatus.");
|
|
136
|
+
}
|
|
137
|
+
const url = `${baseUrl.replace(/\/+$/, "")}/payment/status/stream/${encodeURIComponent(
|
|
138
|
+
trxRef
|
|
139
|
+
)}?publicKey=${encodeURIComponent(publicKey)}`;
|
|
140
|
+
const headers = { Accept: "text/event-stream" };
|
|
141
|
+
if (options.sendPublicKeyHeader) {
|
|
142
|
+
headers.publicKey = publicKey;
|
|
143
|
+
}
|
|
144
|
+
const controller = new AbortController();
|
|
145
|
+
(async () => {
|
|
146
|
+
try {
|
|
147
|
+
const response = await fetchImpl(url, {
|
|
148
|
+
headers,
|
|
149
|
+
signal: controller.signal
|
|
150
|
+
});
|
|
151
|
+
if (!response.ok || !response.body) {
|
|
152
|
+
throw new Error(`Status stream request failed with status ${response.status}`);
|
|
153
|
+
}
|
|
154
|
+
options.onOpen?.();
|
|
155
|
+
const reader = response.body.getReader();
|
|
156
|
+
const decoder = new TextDecoder();
|
|
157
|
+
let buffer = "";
|
|
158
|
+
while (true) {
|
|
159
|
+
const { done, value } = await reader.read();
|
|
160
|
+
if (done) break;
|
|
161
|
+
buffer += decoder.decode(value, { stream: true });
|
|
162
|
+
const events = buffer.split("\n\n");
|
|
163
|
+
buffer = events.pop() ?? "";
|
|
164
|
+
for (const rawEvent of events) {
|
|
165
|
+
const dataLines = rawEvent.split("\n").filter((line) => line.startsWith("data:")).map((line) => line.slice(5).trim());
|
|
166
|
+
if (dataLines.length === 0) continue;
|
|
167
|
+
const raw = dataLines.join("\n");
|
|
168
|
+
let data = raw;
|
|
169
|
+
try {
|
|
170
|
+
data = JSON.parse(raw);
|
|
171
|
+
} catch {
|
|
172
|
+
}
|
|
173
|
+
options.onMessage({ data, raw });
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
} catch (err) {
|
|
177
|
+
if (err instanceof Error && err.name === "AbortError") return;
|
|
178
|
+
options.onError?.(err);
|
|
179
|
+
}
|
|
180
|
+
})();
|
|
181
|
+
return {
|
|
182
|
+
close: () => controller.abort()
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// src/client.ts
|
|
187
|
+
var RemitaCheckoutClient = class {
|
|
188
|
+
constructor(options) {
|
|
189
|
+
this.publicKey = options.publicKey;
|
|
190
|
+
const resolved = resolveEnvironment(options.publicKey, {
|
|
191
|
+
environment: options.environment,
|
|
192
|
+
baseUrls: options.baseUrls
|
|
193
|
+
});
|
|
194
|
+
if (!resolved.detected) {
|
|
195
|
+
options.onEnvironmentDetectionFallback?.("qa");
|
|
196
|
+
}
|
|
197
|
+
this.environment = resolved.environment;
|
|
198
|
+
this.baseUrl = resolved.baseUrl;
|
|
199
|
+
this.http = new HttpClient({
|
|
200
|
+
baseUrl: resolved.baseUrl,
|
|
201
|
+
fetchImpl: options.fetchImpl,
|
|
202
|
+
timeoutMs: options.timeoutMs,
|
|
203
|
+
defaultHeaders: {
|
|
204
|
+
// publicKey is required as a header on every checkout API call.
|
|
205
|
+
publicKey: options.publicKey,
|
|
206
|
+
...options.defaultHeaders
|
|
207
|
+
}
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
// --- Core payment lifecycle ---------------------------------------------
|
|
211
|
+
prePayment(request) {
|
|
212
|
+
return this.http.request("POST", "/payment/pre", { body: request });
|
|
213
|
+
}
|
|
214
|
+
calculateFee(request) {
|
|
215
|
+
return this.http.request("POST", "/payment/pre/feeCharge", { body: request });
|
|
216
|
+
}
|
|
217
|
+
initiatePayment(request) {
|
|
218
|
+
return this.http.request("POST", "/payment/initiate", { body: request });
|
|
219
|
+
}
|
|
220
|
+
/**
|
|
221
|
+
* Posts authorization params (OTP, PIN, 3DS form fields, etc) to the
|
|
222
|
+
* processor-provided actionUrl from initiatePayment().
|
|
223
|
+
*
|
|
224
|
+
* NOTE: CyberSource/Cardinal 3DS endpoints expect
|
|
225
|
+
* `application/x-www-form-urlencoded`. Pass `contentType` if the
|
|
226
|
+
* actionUrl is a 3DS processor endpoint.
|
|
227
|
+
*/
|
|
228
|
+
authorize(actionUrl, payload, contentType) {
|
|
229
|
+
void contentType;
|
|
230
|
+
return this.http.request("POST", "", {
|
|
231
|
+
body: payload,
|
|
232
|
+
absoluteUrl: actionUrl
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
/**
|
|
236
|
+
* Validates second-factor authorization (e.g. OTP). Per the endpoint docs
|
|
237
|
+
* this may go to a fixed /payment/validate endpoint OR to a dynamic
|
|
238
|
+
* actionUrl — pass `actionUrl` if the preceding step returned one.
|
|
239
|
+
*/
|
|
240
|
+
validatePayment(request, actionUrl) {
|
|
241
|
+
return actionUrl ? this.http.request("POST", "", { body: request, absoluteUrl: actionUrl }) : this.http.request("POST", "/payment/validate", { body: request });
|
|
242
|
+
}
|
|
243
|
+
verifyPayment(paymentRef) {
|
|
244
|
+
return this.http.request("GET", `/payment/verify/${encodeURIComponent(paymentRef)}`);
|
|
245
|
+
}
|
|
246
|
+
/** Opens an SSE stream for real-time status updates. Call the returned `close()` when done. */
|
|
247
|
+
streamPaymentStatus(trxRef, options) {
|
|
248
|
+
return openStatusStream(this.baseUrl, trxRef, this.publicKey, options);
|
|
249
|
+
}
|
|
250
|
+
// --- Channel data / configuration ---------------------------------------
|
|
251
|
+
getBankList() {
|
|
252
|
+
return this.http.request("GET", "/payment/bank-list");
|
|
253
|
+
}
|
|
254
|
+
getUssdCodes() {
|
|
255
|
+
return this.http.request("GET", "/misc/ussd-codes");
|
|
256
|
+
}
|
|
257
|
+
getWalletTypes() {
|
|
258
|
+
return this.http.request("GET", "/misc/wallet-type");
|
|
259
|
+
}
|
|
260
|
+
};
|
|
261
|
+
|
|
262
|
+
// src/session.ts
|
|
263
|
+
var PaymentSession = class {
|
|
264
|
+
constructor(client) {
|
|
265
|
+
this.client = client;
|
|
266
|
+
this.stage = "idle";
|
|
267
|
+
}
|
|
268
|
+
async pre(request) {
|
|
269
|
+
const response = await this.client.prePayment(request);
|
|
270
|
+
this.transactionId = response.transactionId ?? this.transactionId;
|
|
271
|
+
this.paymentReference = response.paymentReference ?? this.paymentReference;
|
|
272
|
+
this.stage = "pre";
|
|
273
|
+
return response;
|
|
274
|
+
}
|
|
275
|
+
async fee(request) {
|
|
276
|
+
const response = await this.client.calculateFee(request);
|
|
277
|
+
this.stage = "fee";
|
|
278
|
+
return response;
|
|
279
|
+
}
|
|
280
|
+
async initiate(request) {
|
|
281
|
+
const response = await this.client.initiatePayment(request);
|
|
282
|
+
this.authorizeActionUrl = response.actionUrl;
|
|
283
|
+
this.validateActionUrl = void 0;
|
|
284
|
+
this.paymentReference = response.paymentReference ?? this.paymentReference;
|
|
285
|
+
this.transactionId = response.transactionId ?? this.transactionId;
|
|
286
|
+
this.stage = "initiated";
|
|
287
|
+
return response;
|
|
288
|
+
}
|
|
289
|
+
/**
|
|
290
|
+
* Uses the actionUrl captured from initiate() unless you pass one explicitly.
|
|
291
|
+
* Pass `contentType` for 3DS processor endpoints that require form-encoding.
|
|
292
|
+
*/
|
|
293
|
+
async authorize(payload, actionUrlOverride, contentType) {
|
|
294
|
+
const url = actionUrlOverride ?? this.authorizeActionUrl;
|
|
295
|
+
if (!url) {
|
|
296
|
+
throw new Error(
|
|
297
|
+
"No actionUrl available \u2014 call initiate() first, or pass actionUrlOverride explicitly."
|
|
298
|
+
);
|
|
299
|
+
}
|
|
300
|
+
const response = await this.client.authorize(url, payload, contentType);
|
|
301
|
+
const next = response.actionUrl;
|
|
302
|
+
if (next) this.validateActionUrl = next;
|
|
303
|
+
this.stage = "authorized";
|
|
304
|
+
return response;
|
|
305
|
+
}
|
|
306
|
+
async validate(request, actionUrlOverride) {
|
|
307
|
+
const url = actionUrlOverride ?? this.validateActionUrl;
|
|
308
|
+
const response = await this.client.validatePayment(request, url);
|
|
309
|
+
this.stage = "validated";
|
|
310
|
+
return response;
|
|
311
|
+
}
|
|
312
|
+
async verify(paymentRefOverride) {
|
|
313
|
+
const ref = paymentRefOverride ?? this.paymentReference ?? this.transactionId;
|
|
314
|
+
if (!ref) {
|
|
315
|
+
throw new Error(
|
|
316
|
+
"No paymentReference or transactionId available \u2014 call initiate() first, or pass it explicitly."
|
|
317
|
+
);
|
|
318
|
+
}
|
|
319
|
+
const response = await this.client.verifyPayment(ref);
|
|
320
|
+
if (response.status === "APPROVED") {
|
|
321
|
+
this.stage = "verified";
|
|
322
|
+
} else if (response.status === "FAILED") {
|
|
323
|
+
this.stage = "failed";
|
|
324
|
+
}
|
|
325
|
+
return response;
|
|
326
|
+
}
|
|
327
|
+
};
|
|
328
|
+
|
|
329
|
+
// src/types.ts
|
|
330
|
+
var RemitaApiError2 = class extends Error {
|
|
331
|
+
constructor(message, status, payload) {
|
|
332
|
+
super(message);
|
|
333
|
+
this.name = "RemitaApiError";
|
|
334
|
+
this.status = status;
|
|
335
|
+
this.payload = payload;
|
|
336
|
+
}
|
|
337
|
+
};
|
|
338
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
339
|
+
0 && (module.exports = {
|
|
340
|
+
ENVIRONMENT_BASE_URLS,
|
|
341
|
+
PaymentSession,
|
|
342
|
+
RemitaApiError,
|
|
343
|
+
RemitaCheckoutClient,
|
|
344
|
+
openStatusStream,
|
|
345
|
+
resolveEnvironment
|
|
346
|
+
});
|
|
347
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/http-client.ts","../src/environment.ts","../src/stream.ts","../src/client.ts","../src/session.ts","../src/types.ts"],"sourcesContent":["export { RemitaCheckoutClient } from \"./client\";\nexport type { RemitaCheckoutClientOptions } from \"./client\";\n\nexport { PaymentSession } from \"./session\";\nexport type { PaymentSessionStage } from \"./session\";\n\nexport {\n resolveEnvironment,\n ENVIRONMENT_BASE_URLS,\n} from \"./environment\";\nexport type {\n RemitaEnvironment,\n ResolveEnvironmentOptions,\n ResolvedEnvironment,\n} from \"./environment\";\n\nexport { openStatusStream } from \"./stream\";\nexport type { StatusStreamOptions, StatusStreamHandle } from \"./stream\";\n\nexport { RemitaApiError } from \"./types\";\nexport type {\n ApiErrorPayload,\n PrePaymentRequest,\n PrePaymentResponse,\n PaymentChannel,\n FeeChargeRequest,\n FeeChargeResponse,\n InitiatePaymentRequest,\n InitiatePaymentResponse,\n AuthorizePayload,\n AuthorizeResponse,\n ValidatePaymentRequest,\n ValidatePaymentResponse,\n PaymentStatus,\n VerifyPaymentResponse,\n Bank,\n BankListResponse,\n UssdCode,\n UssdCodesResponse,\n WalletType,\n WalletTypesResponse,\n StatusStreamEvent,\n} from \"./types\";\n","export interface HttpRequestOptions {\n body?: unknown;\n headers?: Record<string, string>;\n absoluteUrl?: string;\n /** Overrides the default application/json for a single request. */\n contentType?: string;\n}\n\nexport interface HttpClientOptions {\n baseUrl: string;\n fetchImpl?: typeof fetch;\n timeoutMs?: number;\n defaultHeaders?: Record<string, string>;\n}\n\nexport interface ApiErrorPayload {\n responseMessage?: string;\n message?: string;\n [key: string]: unknown;\n}\n\nexport class RemitaApiError extends Error {\n constructor(\n message: string,\n public readonly status: number,\n public readonly payload: ApiErrorPayload\n ) {\n super(message);\n this.name = \"RemitaApiError\";\n }\n}\n\nexport class HttpClient {\n private readonly baseUrl: string;\n private readonly fetchImpl: typeof fetch;\n private readonly timeoutMs: number;\n private readonly defaultHeaders: Record<string, string>;\n\n constructor(options: HttpClientOptions) {\n this.baseUrl = options.baseUrl.replace(/\\/+$/, \"\");\n this.fetchImpl =\n options.fetchImpl ?? (typeof fetch !== \"undefined\" ? fetch : (undefined as never));\n this.timeoutMs = options.timeoutMs ?? 30_000;\n this.defaultHeaders = options.defaultHeaders ?? {};\n\n if (!this.fetchImpl) {\n throw new Error(\"No fetch implementation available for HttpClient.\");\n }\n }\n\n async request<T>(\n method: string,\n path: string,\n opts: HttpRequestOptions = {}\n ): Promise<T> {\n // --- L6 fix: join WITHOUT dropping /api/v1 ---\n // path is expected to start with \"/\" and the baseUrl already ends with\n // \"/api/v1\", so plain concatenation is correct. Do NOT use `new URL(path, base)`.\n const url = opts.absoluteUrl\n ? opts.absoluteUrl\n : `${this.baseUrl}${path.startsWith(\"/\") ? \"\" : \"/\"}${path}`;\n\n const headers: Record<string, string> = {\n Accept: \"application/json\",\n ...this.defaultHeaders,\n ...opts.headers,\n };\n\n let body: BodyInit | undefined;\n if (opts.body !== undefined) {\n if (opts.contentType === \"application/x-www-form-urlencoded\") {\n headers[\"Content-Type\"] = opts.contentType;\n body = new URLSearchParams(\n Object.entries(opts.body as Record<string, unknown>).map(([k, v]) => [\n k,\n String(v),\n ])\n ).toString();\n } else {\n headers[\"Content-Type\"] = opts.contentType ?? \"application/json\";\n body = JSON.stringify(opts.body);\n }\n }\n\n const controller = new AbortController();\n const timer =\n this.timeoutMs > 0 ? setTimeout(() => controller.abort(), this.timeoutMs) : undefined;\n\n try {\n const response = await this.fetchImpl(url, {\n method,\n headers,\n body,\n signal: controller.signal,\n });\n\n const text = await response.text();\n let parsed: unknown = text;\n try {\n parsed = text ? JSON.parse(text) : undefined;\n } catch {\n // leave as raw string\n }\n\n if (!response.ok) {\n const payload = (parsed ?? {}) as ApiErrorPayload;\n const message =\n payload.responseMessage ??\n payload.message ??\n `Request failed with status ${response.status}`;\n throw new RemitaApiError(message, response.status, payload);\n }\n\n return parsed as T;\n } finally {\n if (timer) clearTimeout(timer);\n }\n }\n}","export type RemitaEnvironment = \"qa\" | \"demo\" | \"production\";\n\nexport const ENVIRONMENT_BASE_URLS: Record<RemitaEnvironment, string> = {\n qa: \"https://api-checkout-qa.systemspecsng.com/api/v1\",\n demo: \"https://api-checkout-demo.systemspecsng.com/api/v1\",\n production: \"https://api-checkout.systemspecsng.com/api/v1\",\n};\n\n/**\n * Best-effort prefix-based environment detection from a public key.\n *\n * IMPORTANT: `pk_live_` is deliberately NOT mapped here. Remita's real key\n * format has not been verified, and silently routing a key to production\n * is worse than failing loudly. Callers who want production MUST pass\n * `environment: \"production\"` (or `--env production` on the CLI) explicitly.\n */\nconst PREFIX_RULES: Array<{ pattern: RegExp; environment: RemitaEnvironment }> = [\n { pattern: /^(qa[-_]|pk_qa_)/i, environment: \"qa\" },\n { pattern: /^(demo[-_]|pk_demo_|pk_test_)/i, environment: \"demo\" },\n];\n\nexport interface ResolveEnvironmentOptions {\n /** Explicit override — always wins over prefix detection. */\n environment?: RemitaEnvironment;\n /** Custom base URLs, e.g. to point at a local/staging mirror. */\n baseUrls?: Partial<Record<RemitaEnvironment, string>>;\n}\n\nexport interface ResolvedEnvironment {\n environment: RemitaEnvironment;\n baseUrl: string;\n /** False when detection couldn't confidently match a prefix and we fell back to a default. */\n detected: boolean;\n}\n\nexport function resolveEnvironment(\n publicKey: string,\n options: ResolveEnvironmentOptions = {}\n): ResolvedEnvironment {\n const baseUrls = { ...ENVIRONMENT_BASE_URLS, ...options.baseUrls };\n\n if (options.environment) {\n return {\n environment: options.environment,\n baseUrl: baseUrls[options.environment],\n detected: true,\n };\n }\n\n for (const rule of PREFIX_RULES) {\n if (rule.pattern.test(publicKey)) {\n return {\n environment: rule.environment,\n baseUrl: baseUrls[rule.environment],\n detected: true,\n };\n }\n }\n\n return { environment: \"qa\", baseUrl: baseUrls.qa, detected: false };\n}","export interface StatusStreamOptions {\n onMessage: (event: { data: unknown; raw: string }) => void;\n onError?: (err: unknown) => void;\n onOpen?: () => void;\n fetchImpl?: typeof fetch;\n /** Optional override; publicKey is sent as a query param by default. */\n sendPublicKeyHeader?: boolean;\n}\n\nexport interface StatusStreamHandle {\n close: () => void;\n}\n\n/**\n * Opens the payment status SSE stream using a manual fetch + ReadableStream\n * reader rather than the browser-only `EventSource` global, so this works\n * identically in the browser and in Node 18+.\n */\nexport function openStatusStream(\n baseUrl: string,\n trxRef: string,\n publicKey: string,\n options: StatusStreamOptions\n): StatusStreamHandle {\n const fetchImpl =\n options.fetchImpl ?? (typeof fetch !== \"undefined\" ? fetch : undefined);\n if (!fetchImpl) {\n throw new Error(\"No fetch implementation available for streamPaymentStatus.\");\n }\n\n const url = `${baseUrl.replace(/\\/+$/, \"\")}/payment/status/stream/${encodeURIComponent(\n trxRef\n )}?publicKey=${encodeURIComponent(publicKey)}`;\n\n const headers: Record<string, string> = { Accept: \"text/event-stream\" };\n if (options.sendPublicKeyHeader) {\n headers.publicKey = publicKey;\n }\n\n const controller = new AbortController();\n\n (async () => {\n try {\n const response = await fetchImpl(url, {\n headers,\n signal: controller.signal,\n });\n\n if (!response.ok || !response.body) {\n throw new Error(`Status stream request failed with status ${response.status}`);\n }\n\n options.onOpen?.();\n\n const reader = response.body.getReader();\n const decoder = new TextDecoder();\n let buffer = \"\";\n\n // eslint-disable-next-line no-constant-condition\n while (true) {\n const { done, value } = await reader.read();\n if (done) break;\n\n buffer += decoder.decode(value, { stream: true });\n const events = buffer.split(\"\\n\\n\");\n buffer = events.pop() ?? \"\";\n\n for (const rawEvent of events) {\n const dataLines = rawEvent\n .split(\"\\n\")\n .filter((line) => line.startsWith(\"data:\"))\n .map((line) => line.slice(5).trim());\n\n if (dataLines.length === 0) continue;\n\n const raw = dataLines.join(\"\\n\");\n let data: unknown = raw;\n try {\n data = JSON.parse(raw);\n } catch {\n // not JSON — leave as the raw string\n }\n\n options.onMessage({ data, raw });\n }\n }\n } catch (err) {\n if (err instanceof Error && err.name === \"AbortError\") return;\n options.onError?.(err);\n }\n })();\n\n return {\n close: () => controller.abort(),\n };\n}","import { HttpClient } from \"./http-client\";\nimport { resolveEnvironment } from \"./environment\";\nimport type { ResolveEnvironmentOptions } from \"./environment\";\nimport type {\n PrePaymentRequest,\n PrePaymentResponse,\n FeeChargeRequest,\n FeeChargeResponse,\n InitiatePaymentRequest,\n InitiatePaymentResponse,\n AuthorizePayload,\n AuthorizeResponse,\n ValidatePaymentRequest,\n ValidatePaymentResponse,\n VerifyPaymentResponse,\n BankListResponse,\n UssdCodesResponse,\n WalletTypesResponse,\n} from \"./types\";\nimport { openStatusStream } from \"./stream\";\nimport type { StatusStreamHandle, StatusStreamOptions } from \"./stream\";\n\nexport interface RemitaCheckoutClientOptions extends ResolveEnvironmentOptions {\n publicKey: string;\n fetchImpl?: typeof fetch;\n timeoutMs?: number;\n /** Extra headers merged after `publicKey` is set (so they can override). */\n defaultHeaders?: Record<string, string>;\n /** Called once if the environment couldn't be confidently detected from the key prefix (falls back to \"qa\"). */\n onEnvironmentDetectionFallback?: (guessed: \"qa\") => void;\n}\n\nexport class RemitaCheckoutClient {\n readonly publicKey: string;\n readonly environment: string;\n readonly baseUrl: string;\n private readonly http: HttpClient;\n\n constructor(options: RemitaCheckoutClientOptions) {\n this.publicKey = options.publicKey;\n\n const resolved = resolveEnvironment(options.publicKey, {\n environment: options.environment,\n baseUrls: options.baseUrls,\n });\n\n if (!resolved.detected) {\n options.onEnvironmentDetectionFallback?.(\"qa\");\n }\n\n this.environment = resolved.environment;\n this.baseUrl = resolved.baseUrl;\n\n this.http = new HttpClient({\n baseUrl: resolved.baseUrl,\n fetchImpl: options.fetchImpl,\n timeoutMs: options.timeoutMs,\n defaultHeaders: {\n // publicKey is required as a header on every checkout API call.\n publicKey: options.publicKey,\n ...options.defaultHeaders,\n },\n });\n }\n\n // --- Core payment lifecycle ---------------------------------------------\n\n prePayment(request: PrePaymentRequest): Promise<PrePaymentResponse> {\n return this.http.request(\"POST\", \"/payment/pre\", { body: request });\n }\n\n calculateFee(request: FeeChargeRequest): Promise<FeeChargeResponse> {\n return this.http.request(\"POST\", \"/payment/pre/feeCharge\", { body: request });\n }\n\n initiatePayment(request: InitiatePaymentRequest): Promise<InitiatePaymentResponse> {\n return this.http.request(\"POST\", \"/payment/initiate\", { body: request });\n }\n\n /**\n * Posts authorization params (OTP, PIN, 3DS form fields, etc) to the\n * processor-provided actionUrl from initiatePayment().\n *\n * NOTE: CyberSource/Cardinal 3DS endpoints expect\n * `application/x-www-form-urlencoded`. Pass `contentType` if the\n * actionUrl is a 3DS processor endpoint.\n */\n authorize(\n actionUrl: string,\n payload: AuthorizePayload,\n contentType?: string\n ): Promise<AuthorizeResponse> {\n void contentType;\n return this.http.request(\"POST\", \"\", {\n body: payload,\n absoluteUrl: actionUrl,\n });\n }\n\n /**\n * Validates second-factor authorization (e.g. OTP). Per the endpoint docs\n * this may go to a fixed /payment/validate endpoint OR to a dynamic\n * actionUrl — pass `actionUrl` if the preceding step returned one.\n */\n validatePayment(\n request: ValidatePaymentRequest,\n actionUrl?: string\n ): Promise<ValidatePaymentResponse> {\n return actionUrl\n ? this.http.request(\"POST\", \"\", { body: request, absoluteUrl: actionUrl })\n : this.http.request(\"POST\", \"/payment/validate\", { body: request });\n }\n\n verifyPayment(paymentRef: string): Promise<VerifyPaymentResponse> {\n return this.http.request(\"GET\", `/payment/verify/${encodeURIComponent(paymentRef)}`);\n }\n\n /** Opens an SSE stream for real-time status updates. Call the returned `close()` when done. */\n streamPaymentStatus(trxRef: string, options: StatusStreamOptions): StatusStreamHandle {\n return openStatusStream(this.baseUrl, trxRef, this.publicKey, options);\n }\n\n // --- Channel data / configuration ---------------------------------------\n\n getBankList(): Promise<BankListResponse> {\n return this.http.request(\"GET\", \"/payment/bank-list\");\n }\n\n getUssdCodes(): Promise<UssdCodesResponse> {\n return this.http.request(\"GET\", \"/misc/ussd-codes\");\n }\n\n getWalletTypes(): Promise<WalletTypesResponse> {\n return this.http.request(\"GET\", \"/misc/wallet-type\");\n }\n}","import type { RemitaCheckoutClient } from \"./client\";\nimport type {\n PrePaymentRequest,\n PrePaymentResponse,\n FeeChargeRequest,\n FeeChargeResponse,\n InitiatePaymentRequest,\n InitiatePaymentResponse,\n AuthorizePayload,\n AuthorizeResponse,\n ValidatePaymentRequest,\n ValidatePaymentResponse,\n VerifyPaymentResponse,\n} from \"./types\";\n\nexport type PaymentSessionStage =\n | \"idle\"\n | \"pre\"\n | \"fee\"\n | \"initiated\"\n | \"authorized\"\n | \"validated\"\n | \"verified\"\n | \"failed\";\n\n/**\n * Sequences the multi-step payment flow (pre → fee → initiate → authorize →\n * validate → verify) and tracks the refs each step hands to the next\n * (paymentReference, transactionId, actionUrl), so callers don't have to\n * thread that state through by hand.\n */\nexport class PaymentSession {\n stage: PaymentSessionStage = \"idle\";\n paymentReference?: string;\n transactionId?: string;\n\n /** URL returned by initiate() for the *authorize* step. */\n private authorizeActionUrl?: string;\n /** URL returned by authorize() for the *validate* step, if different. */\n private validateActionUrl?: string;\n\n constructor(private readonly client: RemitaCheckoutClient) {}\n\n async pre(request: PrePaymentRequest): Promise<PrePaymentResponse> {\n const response = await this.client.prePayment(request);\n // Read refs from the RESPONSE, not the request.\n this.transactionId = response.transactionId ?? this.transactionId;\n this.paymentReference = response.paymentReference ?? this.paymentReference;\n this.stage = \"pre\";\n return response;\n }\n\n async fee(request: FeeChargeRequest): Promise<FeeChargeResponse> {\n const response = await this.client.calculateFee(request);\n this.stage = \"fee\";\n return response;\n }\n\n async initiate(request: InitiatePaymentRequest): Promise<InitiatePaymentResponse> {\n const response = await this.client.initiatePayment(request);\n this.authorizeActionUrl = response.actionUrl;\n this.validateActionUrl = undefined; // reset until authorize tells us otherwise\n this.paymentReference = response.paymentReference ?? this.paymentReference;\n this.transactionId = response.transactionId ?? this.transactionId;\n this.stage = \"initiated\";\n return response;\n }\n\n /**\n * Uses the actionUrl captured from initiate() unless you pass one explicitly.\n * Pass `contentType` for 3DS processor endpoints that require form-encoding.\n */\n async authorize(\n payload: AuthorizePayload,\n actionUrlOverride?: string,\n contentType?: string\n ): Promise<AuthorizeResponse> {\n const url = actionUrlOverride ?? this.authorizeActionUrl;\n if (!url) {\n throw new Error(\n \"No actionUrl available — call initiate() first, or pass actionUrlOverride explicitly.\"\n );\n }\n const response = await this.client.authorize(url, payload, contentType);\n\n // If the processor told us where the next validation step lives, use it.\n const next = (response as { actionUrl?: string }).actionUrl;\n if (next) this.validateActionUrl = next;\n\n this.stage = \"authorized\";\n return response;\n }\n\n async validate(\n request: ValidatePaymentRequest,\n actionUrlOverride?: string\n ): Promise<ValidatePaymentResponse> {\n // Prefer an explicit override, then the URL authorize() handed us,\n // then fall through to /payment/validate.\n const url = actionUrlOverride ?? this.validateActionUrl;\n const response = await this.client.validatePayment(request, url);\n this.stage = \"validated\";\n return response;\n }\n\n async verify(paymentRefOverride?: string): Promise<VerifyPaymentResponse> {\n const ref = paymentRefOverride ?? this.paymentReference ?? this.transactionId;\n if (!ref) {\n throw new Error(\n \"No paymentReference or transactionId available — call initiate() first, or pass it explicitly.\"\n );\n }\n const response = await this.client.verifyPayment(ref);\n // Only APPROVED counts as verified. PENDING stays \"validated\".\n if (response.status === \"APPROVED\") {\n this.stage = \"verified\";\n } else if (response.status === \"FAILED\") {\n this.stage = \"failed\";\n }\n return response;\n }\n}","export interface ApiErrorPayload {\n responseCode?: string;\n responseMessage?: string;\n message?: string;\n [key: string]: unknown;\n}\n\nexport class RemitaApiError extends Error {\n readonly status: number;\n readonly payload: ApiErrorPayload | undefined;\n\n constructor(message: string, status: number, payload?: ApiErrorPayload) {\n super(message);\n this.name = \"RemitaApiError\";\n this.status = status;\n this.payload = payload;\n }\n}\n\n/* -------------------------------------------------------------------- *\n * NOTE ON THE TYPES BELOW\n *\n * Field names for PrePaymentRequest are verified against a working QA\n * cURL. The rest remain best-effort based on standard gateway\n * conventions and should be tightened as real DTOs are confirmed.\n *\n * `publicKey` is NEVER part of a request body — it is sent as a header\n * by RemitaCheckoutClient. Do not add it back to any request interface.\n * -------------------------------------------------------------------- */\n\nexport interface PrePaymentRequest {\n firstName: string;\n lastName: string;\n email: string;\n phoneNumber: string;\n paymentIdentifier: string;\n currency: string;\n narration?: string;\n amount: number;\n [key: string]: unknown;\n}\n\nexport interface PaymentChannel {\n code: string;\n name: string;\n [key: string]: unknown;\n}\n\nexport interface PrePaymentResponse {\n merchantName?: string;\n amount?: number | string;\n currency?: string;\n channels?: PaymentChannel[];\n /** Forwarded from /payment/pre if the API echoes one. */\n transactionId?: string;\n paymentReference?: string;\n [key: string]: unknown;\n}\n\nexport interface FeeChargeRequest {\n amount: number;\n channel: string;\n [key: string]: unknown;\n}\n\nexport interface FeeChargeResponse {\n fee?: number | string;\n totalAmount?: number | string;\n [key: string]: unknown;\n}\n\nexport interface InitiatePaymentRequest {\n channel: string;\n amount: number;\n transactionId?: string;\n [key: string]: unknown;\n}\n\nexport interface InitiatePaymentResponse {\n /** Where to POST the next-step authorization payload (OTP, PIN, 3DS fields, etc). */\n actionUrl?: string;\n paymentReference?: string;\n transactionId?: string;\n status?: string;\n [key: string]: unknown;\n}\n\nexport type AuthorizePayload = Record<string, unknown>;\n\nexport interface AuthorizeResponse {\n status?: string;\n requiresValidation?: boolean;\n [key: string]: unknown;\n}\n\nexport interface ValidatePaymentRequest {\n paymentReference?: string;\n transactionId?: string;\n otp?: string;\n [key: string]: unknown;\n}\n\nexport interface ValidatePaymentResponse {\n status?: string;\n [key: string]: unknown;\n}\n\nexport type PaymentStatus = \"APPROVED\" | \"PENDING\" | \"FAILED\" | (string & {});\n\nexport interface VerifyPaymentResponse {\n status?: PaymentStatus;\n paymentReference?: string;\n amount?: number | string;\n [key: string]: unknown;\n}\n\nexport interface Bank {\n code: string;\n name: string;\n currency?: string;\n [key: string]: unknown;\n}\n\nexport interface BankListResponse {\n banks: Bank[];\n [key: string]: unknown;\n}\n\nexport interface UssdCode {\n bankCode: string;\n ussdCode: string;\n [key: string]: unknown;\n}\n\nexport interface UssdCodesResponse {\n codes: UssdCode[];\n [key: string]: unknown;\n}\n\nexport interface WalletType {\n code: string;\n name: string;\n [key: string]: unknown;\n}\n\nexport interface WalletTypesResponse {\n wallets: WalletType[];\n [key: string]: unknown;\n}\n\nexport interface StatusStreamEvent {\n status?: PaymentStatus;\n [key: string]: unknown;\n}"],"mappings":"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","names":["RemitaApiError","RemitaApiError"]}
|