@dryinov8/zumbopay-ts 1.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/LICENSE +21 -0
- package/README.md +375 -0
- package/dist/chunk-YULBG3E6.mjs +338 -0
- package/dist/chunk-YULBG3E6.mjs.map +1 -0
- package/dist/client-CO8dBkq7.d.mts +188 -0
- package/dist/client-CO8dBkq7.d.ts +188 -0
- package/dist/index.d.mts +35 -0
- package/dist/index.d.ts +35 -0
- package/dist/index.js +400 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +36 -0
- package/dist/index.mjs.map +1 -0
- package/dist/react/index.d.mts +46 -0
- package/dist/react/index.d.ts +46 -0
- package/dist/react/index.js +755 -0
- package/dist/react/index.js.map +1 -0
- package/dist/react/index.mjs +393 -0
- package/dist/react/index.mjs.map +1 -0
- package/package.json +80 -0
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
type MobileOperator = 'mpesa' | 'emola' | 'mkesh' | 'unknown';
|
|
2
|
+
type PaymentStatus = 'pending' | 'processing' | 'success' | 'succeeded' | 'completed' | 'failed' | 'declined' | 'cancelled' | 'expired' | 'disabled';
|
|
3
|
+
interface ZumboPayWallets {
|
|
4
|
+
mpesa?: string;
|
|
5
|
+
emola?: string;
|
|
6
|
+
mkesh?: string;
|
|
7
|
+
card?: string;
|
|
8
|
+
[key: string]: string | undefined;
|
|
9
|
+
}
|
|
10
|
+
interface ZumboPayConfig {
|
|
11
|
+
/**
|
|
12
|
+
* ZumboPay Secret API Key
|
|
13
|
+
*/
|
|
14
|
+
apiKey: string;
|
|
15
|
+
/**
|
|
16
|
+
* ZumboPay Merchant ID
|
|
17
|
+
*/
|
|
18
|
+
merchantId: string;
|
|
19
|
+
/**
|
|
20
|
+
* Base API URL (defaults to https://zumbopay.com/api/public/v1)
|
|
21
|
+
*/
|
|
22
|
+
baseUrl?: string;
|
|
23
|
+
/**
|
|
24
|
+
* Webhook signing secret for HMAC verification
|
|
25
|
+
*/
|
|
26
|
+
webhookSecret?: string;
|
|
27
|
+
/**
|
|
28
|
+
* Pre-configured Wallet UUIDs for specific channels
|
|
29
|
+
*/
|
|
30
|
+
wallets?: ZumboPayWallets;
|
|
31
|
+
/**
|
|
32
|
+
* Global kill-switch / activation toggle.
|
|
33
|
+
* If false, all checkout and STK push operations are gracefully silenced
|
|
34
|
+
* and return a disabled status without making external network calls.
|
|
35
|
+
* Defaults to true (or checks process.env.ZUMBOPAY_ENABLED when available).
|
|
36
|
+
*/
|
|
37
|
+
enabled?: boolean;
|
|
38
|
+
/**
|
|
39
|
+
* Request timeout in milliseconds (defaults to 15000)
|
|
40
|
+
*/
|
|
41
|
+
timeout?: number;
|
|
42
|
+
}
|
|
43
|
+
interface StkPushRequest {
|
|
44
|
+
/**
|
|
45
|
+
* Amount in Principal currency units (e.g. 150.00 MZN)
|
|
46
|
+
*/
|
|
47
|
+
amount: number;
|
|
48
|
+
/**
|
|
49
|
+
* Mozambican phone number (Vodacom, Movitel, or Tmcel)
|
|
50
|
+
*/
|
|
51
|
+
phone: string;
|
|
52
|
+
/**
|
|
53
|
+
* Commercial transaction reference / order ID
|
|
54
|
+
*/
|
|
55
|
+
reference?: string;
|
|
56
|
+
/**
|
|
57
|
+
* Customer full name
|
|
58
|
+
*/
|
|
59
|
+
customerName?: string;
|
|
60
|
+
/**
|
|
61
|
+
* Specific Wallet UUID (optional, will be auto-resolved from phone if omitted)
|
|
62
|
+
*/
|
|
63
|
+
walletId?: string;
|
|
64
|
+
/**
|
|
65
|
+
* Description / note
|
|
66
|
+
*/
|
|
67
|
+
description?: string;
|
|
68
|
+
}
|
|
69
|
+
interface StkPushResponse {
|
|
70
|
+
success: boolean;
|
|
71
|
+
status: PaymentStatus;
|
|
72
|
+
reference: string | null;
|
|
73
|
+
message: string;
|
|
74
|
+
raw?: Record<string, unknown>;
|
|
75
|
+
}
|
|
76
|
+
interface CheckoutRequest {
|
|
77
|
+
/**
|
|
78
|
+
* Amount in Principal currency units (e.g. 500.00 MZN)
|
|
79
|
+
*/
|
|
80
|
+
amount: number;
|
|
81
|
+
/**
|
|
82
|
+
* Payment title / product name
|
|
83
|
+
*/
|
|
84
|
+
title: string;
|
|
85
|
+
/**
|
|
86
|
+
* Currency code (defaults to MZN)
|
|
87
|
+
*/
|
|
88
|
+
currency?: string;
|
|
89
|
+
/**
|
|
90
|
+
* Commercial transaction reference / order ID
|
|
91
|
+
*/
|
|
92
|
+
reference?: string;
|
|
93
|
+
/**
|
|
94
|
+
* Return URL after successful payment
|
|
95
|
+
*/
|
|
96
|
+
returnUrl?: string;
|
|
97
|
+
/**
|
|
98
|
+
* Cancel URL if user aborts
|
|
99
|
+
*/
|
|
100
|
+
cancelUrl?: string;
|
|
101
|
+
/**
|
|
102
|
+
* Allowed payment channels (defaults to ['card', 'mpesa', 'emola', 'mkesh'])
|
|
103
|
+
*/
|
|
104
|
+
channels?: string[];
|
|
105
|
+
/**
|
|
106
|
+
* Specific Wallet UUID
|
|
107
|
+
*/
|
|
108
|
+
walletId?: string;
|
|
109
|
+
/**
|
|
110
|
+
* Additional metadata
|
|
111
|
+
*/
|
|
112
|
+
metadata?: Record<string, unknown>;
|
|
113
|
+
}
|
|
114
|
+
interface CheckoutResponse {
|
|
115
|
+
success: boolean;
|
|
116
|
+
checkoutUrl: string | null;
|
|
117
|
+
reference: string | null;
|
|
118
|
+
message: string;
|
|
119
|
+
raw?: Record<string, unknown>;
|
|
120
|
+
}
|
|
121
|
+
interface TransactionStatusResponse {
|
|
122
|
+
success: boolean;
|
|
123
|
+
status: PaymentStatus;
|
|
124
|
+
paid: boolean;
|
|
125
|
+
reference: string;
|
|
126
|
+
amount?: number;
|
|
127
|
+
currency?: string;
|
|
128
|
+
channel?: string;
|
|
129
|
+
paidAt?: string | null;
|
|
130
|
+
raw?: Record<string, unknown>;
|
|
131
|
+
}
|
|
132
|
+
interface WalletItem {
|
|
133
|
+
id: string;
|
|
134
|
+
wallet_code?: string;
|
|
135
|
+
name?: string;
|
|
136
|
+
method?: string;
|
|
137
|
+
currency?: string;
|
|
138
|
+
balance?: number;
|
|
139
|
+
is_active?: boolean;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
declare class ZumboPayClient {
|
|
143
|
+
private readonly config;
|
|
144
|
+
private cachedWallets;
|
|
145
|
+
private walletsCacheExpiresAt;
|
|
146
|
+
constructor(config: ZumboPayConfig);
|
|
147
|
+
/**
|
|
148
|
+
* Returns whether the ZumboPay client is currently active.
|
|
149
|
+
* If false, all mutations (STK push, checkouts) are gracefully silenced.
|
|
150
|
+
*/
|
|
151
|
+
isEnabled(): boolean;
|
|
152
|
+
/**
|
|
153
|
+
* Programmatically enable or disable the gateway (kill-switch toggle).
|
|
154
|
+
*/
|
|
155
|
+
setEnabled(enabled: boolean): void;
|
|
156
|
+
/**
|
|
157
|
+
* Returns configured base URL.
|
|
158
|
+
*/
|
|
159
|
+
getBaseUrl(): string;
|
|
160
|
+
/**
|
|
161
|
+
* Resolves a valid Wallet UUID for a given operator or channel.
|
|
162
|
+
*/
|
|
163
|
+
resolveWalletId(channel: 'mpesa' | 'emola' | 'mkesh' | 'card'): Promise<string | undefined>;
|
|
164
|
+
/**
|
|
165
|
+
* Resolves the proper wallet ID for a Mozambican phone number.
|
|
166
|
+
*/
|
|
167
|
+
resolveWalletIdForPhone(phone: string): Promise<string | undefined>;
|
|
168
|
+
/**
|
|
169
|
+
* Initiates a direct STK Push prompt to a mobile phone (M-Pesa, e-Mola, mKesh).
|
|
170
|
+
*/
|
|
171
|
+
stkPush(request: StkPushRequest): Promise<StkPushResponse>;
|
|
172
|
+
/**
|
|
173
|
+
* Creates a Hosted Checkout URL for credit/debit card & multicanal payments.
|
|
174
|
+
*/
|
|
175
|
+
createCheckout(request: CheckoutRequest): Promise<CheckoutResponse>;
|
|
176
|
+
/**
|
|
177
|
+
* Queries status of an existing charge/checkout transaction.
|
|
178
|
+
*/
|
|
179
|
+
getStatus(referenceOrId: string): Promise<TransactionStatusResponse>;
|
|
180
|
+
/**
|
|
181
|
+
* Lists all wallets associated with the merchant account (cached for 10 minutes).
|
|
182
|
+
*/
|
|
183
|
+
listWallets(): Promise<WalletItem[]>;
|
|
184
|
+
private getHeaders;
|
|
185
|
+
private fetchWithTimeout;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
export { type CheckoutRequest as C, type MobileOperator as M, type PaymentStatus as P, type StkPushRequest as S, type TransactionStatusResponse as T, type WalletItem as W, ZumboPayClient as Z, type CheckoutResponse as a, type StkPushResponse as b, type ZumboPayConfig as c, type ZumboPayWallets as d };
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { M as MobileOperator } from './client-CO8dBkq7.mjs';
|
|
2
|
+
export { C as CheckoutRequest, a as CheckoutResponse, P as PaymentStatus, S as StkPushRequest, b as StkPushResponse, T as TransactionStatusResponse, W as WalletItem, Z as ZumboPayClient, c as ZumboPayConfig, d as ZumboPayWallets } from './client-CO8dBkq7.mjs';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Normalizes any Mozambican telephone string into the standard 12-digit format `258XXXXXXXXX`.
|
|
6
|
+
* Removes spaces, hyphens, parentheses, and prefixes (+258, 258, 0).
|
|
7
|
+
*/
|
|
8
|
+
declare function normalizePhone(phone: string): string;
|
|
9
|
+
/**
|
|
10
|
+
* Detects mobile network operator from phone number:
|
|
11
|
+
* - Vodacom (M-Pesa): 84, 85
|
|
12
|
+
* - Movitel (e-Mola): 86, 87
|
|
13
|
+
* - Tmcel (mKesh): 82, 83
|
|
14
|
+
*/
|
|
15
|
+
declare function detectOperator(phone: string): MobileOperator;
|
|
16
|
+
/**
|
|
17
|
+
* Returns human-readable label for a mobile operator.
|
|
18
|
+
*/
|
|
19
|
+
declare function getOperatorLabel(operator: MobileOperator): string;
|
|
20
|
+
/**
|
|
21
|
+
* Validates whether the number is a valid 9-digit Mozambican mobile number (with or without 258).
|
|
22
|
+
*/
|
|
23
|
+
declare function isValidMozPhone(phone: string): boolean;
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Validates HMAC-SHA256 webhook signature sent by ZumboPay.
|
|
27
|
+
* Resists timing attacks using constant-time buffer comparison.
|
|
28
|
+
*
|
|
29
|
+
* @param rawBody - Raw body string or Buffer from the webhook request
|
|
30
|
+
* @param receivedSignature - Signature sent in `X-Signature`, `Signature`, or `X-ZumboPay-Signature` header
|
|
31
|
+
* @param secret - Webhook secret configured in ZumboPay dashboard
|
|
32
|
+
*/
|
|
33
|
+
declare function verifyWebhookSignature(rawBody: string | Buffer, receivedSignature: string, secret: string): boolean;
|
|
34
|
+
|
|
35
|
+
export { MobileOperator, detectOperator, getOperatorLabel, isValidMozPhone, normalizePhone, verifyWebhookSignature };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { M as MobileOperator } from './client-CO8dBkq7.js';
|
|
2
|
+
export { C as CheckoutRequest, a as CheckoutResponse, P as PaymentStatus, S as StkPushRequest, b as StkPushResponse, T as TransactionStatusResponse, W as WalletItem, Z as ZumboPayClient, c as ZumboPayConfig, d as ZumboPayWallets } from './client-CO8dBkq7.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Normalizes any Mozambican telephone string into the standard 12-digit format `258XXXXXXXXX`.
|
|
6
|
+
* Removes spaces, hyphens, parentheses, and prefixes (+258, 258, 0).
|
|
7
|
+
*/
|
|
8
|
+
declare function normalizePhone(phone: string): string;
|
|
9
|
+
/**
|
|
10
|
+
* Detects mobile network operator from phone number:
|
|
11
|
+
* - Vodacom (M-Pesa): 84, 85
|
|
12
|
+
* - Movitel (e-Mola): 86, 87
|
|
13
|
+
* - Tmcel (mKesh): 82, 83
|
|
14
|
+
*/
|
|
15
|
+
declare function detectOperator(phone: string): MobileOperator;
|
|
16
|
+
/**
|
|
17
|
+
* Returns human-readable label for a mobile operator.
|
|
18
|
+
*/
|
|
19
|
+
declare function getOperatorLabel(operator: MobileOperator): string;
|
|
20
|
+
/**
|
|
21
|
+
* Validates whether the number is a valid 9-digit Mozambican mobile number (with or without 258).
|
|
22
|
+
*/
|
|
23
|
+
declare function isValidMozPhone(phone: string): boolean;
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Validates HMAC-SHA256 webhook signature sent by ZumboPay.
|
|
27
|
+
* Resists timing attacks using constant-time buffer comparison.
|
|
28
|
+
*
|
|
29
|
+
* @param rawBody - Raw body string or Buffer from the webhook request
|
|
30
|
+
* @param receivedSignature - Signature sent in `X-Signature`, `Signature`, or `X-ZumboPay-Signature` header
|
|
31
|
+
* @param secret - Webhook secret configured in ZumboPay dashboard
|
|
32
|
+
*/
|
|
33
|
+
declare function verifyWebhookSignature(rawBody: string | Buffer, receivedSignature: string, secret: string): boolean;
|
|
34
|
+
|
|
35
|
+
export { MobileOperator, detectOperator, getOperatorLabel, isValidMozPhone, normalizePhone, verifyWebhookSignature };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,400 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name in all)
|
|
10
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
+
};
|
|
12
|
+
var __copyProps = (to, from, except, desc) => {
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
+
for (let key of __getOwnPropNames(from))
|
|
15
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
+
}
|
|
18
|
+
return to;
|
|
19
|
+
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
28
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
|
+
|
|
30
|
+
// src/index.ts
|
|
31
|
+
var src_exports = {};
|
|
32
|
+
__export(src_exports, {
|
|
33
|
+
ZumboPayClient: () => ZumboPayClient,
|
|
34
|
+
detectOperator: () => detectOperator,
|
|
35
|
+
getOperatorLabel: () => getOperatorLabel,
|
|
36
|
+
isValidMozPhone: () => isValidMozPhone,
|
|
37
|
+
normalizePhone: () => normalizePhone,
|
|
38
|
+
verifyWebhookSignature: () => verifyWebhookSignature
|
|
39
|
+
});
|
|
40
|
+
module.exports = __toCommonJS(src_exports);
|
|
41
|
+
|
|
42
|
+
// src/phone.ts
|
|
43
|
+
function normalizePhone(phone) {
|
|
44
|
+
let cleaned = phone.replace(/\D/g, "");
|
|
45
|
+
if (cleaned.startsWith("0")) {
|
|
46
|
+
cleaned = cleaned.slice(1);
|
|
47
|
+
}
|
|
48
|
+
if (cleaned.length === 9) {
|
|
49
|
+
cleaned = "258" + cleaned;
|
|
50
|
+
}
|
|
51
|
+
return cleaned;
|
|
52
|
+
}
|
|
53
|
+
function detectOperator(phone) {
|
|
54
|
+
const normalized = normalizePhone(phone);
|
|
55
|
+
if (normalized.length !== 12 || !normalized.startsWith("258")) {
|
|
56
|
+
return "unknown";
|
|
57
|
+
}
|
|
58
|
+
const prefix = normalized.slice(3, 5);
|
|
59
|
+
if (prefix === "84" || prefix === "85") {
|
|
60
|
+
return "mpesa";
|
|
61
|
+
}
|
|
62
|
+
if (prefix === "86" || prefix === "87") {
|
|
63
|
+
return "emola";
|
|
64
|
+
}
|
|
65
|
+
if (prefix === "82" || prefix === "83") {
|
|
66
|
+
return "mkesh";
|
|
67
|
+
}
|
|
68
|
+
return "unknown";
|
|
69
|
+
}
|
|
70
|
+
function getOperatorLabel(operator) {
|
|
71
|
+
switch (operator) {
|
|
72
|
+
case "mpesa":
|
|
73
|
+
return "Vodacom M-Pesa";
|
|
74
|
+
case "emola":
|
|
75
|
+
return "Movitel e-Mola";
|
|
76
|
+
case "mkesh":
|
|
77
|
+
return "Tmcel mKesh";
|
|
78
|
+
default:
|
|
79
|
+
return "Desconhecida";
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
function isValidMozPhone(phone) {
|
|
83
|
+
return detectOperator(phone) !== "unknown";
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// src/webhook.ts
|
|
87
|
+
var import_node_crypto = __toESM(require("crypto"));
|
|
88
|
+
function verifyWebhookSignature(rawBody, receivedSignature, secret) {
|
|
89
|
+
if (!receivedSignature || !secret) {
|
|
90
|
+
return false;
|
|
91
|
+
}
|
|
92
|
+
const cleanReceived = receivedSignature.replace(/^(sha256=|v1=)/i, "").trim();
|
|
93
|
+
try {
|
|
94
|
+
const expected = import_node_crypto.default.createHmac("sha256", secret).update(typeof rawBody === "string" ? rawBody : rawBody.toString("utf8")).digest("hex");
|
|
95
|
+
const expectedBuf = Buffer.from(expected, "hex");
|
|
96
|
+
const receivedBuf = Buffer.from(cleanReceived, "hex");
|
|
97
|
+
if (expectedBuf.length !== receivedBuf.length) {
|
|
98
|
+
return false;
|
|
99
|
+
}
|
|
100
|
+
return import_node_crypto.default.timingSafeEqual(expectedBuf, receivedBuf);
|
|
101
|
+
} catch {
|
|
102
|
+
return false;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// src/client.ts
|
|
107
|
+
var ZumboPayClient = class {
|
|
108
|
+
config;
|
|
109
|
+
cachedWallets = null;
|
|
110
|
+
walletsCacheExpiresAt = 0;
|
|
111
|
+
constructor(config) {
|
|
112
|
+
const envWallets = {};
|
|
113
|
+
let defaultEnabled = true;
|
|
114
|
+
if (typeof process !== "undefined" && process?.env) {
|
|
115
|
+
if (process.env.ZUMBOPAY_ENABLED !== void 0) {
|
|
116
|
+
defaultEnabled = process.env.ZUMBOPAY_ENABLED === "true" || process.env.ZUMBOPAY_ENABLED === "1";
|
|
117
|
+
}
|
|
118
|
+
if (process.env.ZUMBOPAY_WALLET_MPESA) {
|
|
119
|
+
envWallets.mpesa = process.env.ZUMBOPAY_WALLET_MPESA;
|
|
120
|
+
}
|
|
121
|
+
if (process.env.ZUMBOPAY_WALLET_EMOLA) {
|
|
122
|
+
envWallets.emola = process.env.ZUMBOPAY_WALLET_EMOLA;
|
|
123
|
+
}
|
|
124
|
+
if (process.env.ZUMBOPAY_WALLET_MKESH) {
|
|
125
|
+
envWallets.mkesh = process.env.ZUMBOPAY_WALLET_MKESH;
|
|
126
|
+
}
|
|
127
|
+
if (process.env.ZUMBOPAY_WALLET_CARD) {
|
|
128
|
+
envWallets.card = process.env.ZUMBOPAY_WALLET_CARD;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
this.config = {
|
|
132
|
+
apiKey: config.apiKey,
|
|
133
|
+
merchantId: config.merchantId,
|
|
134
|
+
baseUrl: config.baseUrl?.replace(/\/+$/, "") || "https://zumbopay.com/api/public/v1",
|
|
135
|
+
webhookSecret: config.webhookSecret,
|
|
136
|
+
wallets: {
|
|
137
|
+
...envWallets,
|
|
138
|
+
...config.wallets || {}
|
|
139
|
+
},
|
|
140
|
+
enabled: config.enabled !== void 0 ? config.enabled : defaultEnabled,
|
|
141
|
+
timeout: config.timeout || 15e3
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Returns whether the ZumboPay client is currently active.
|
|
146
|
+
* If false, all mutations (STK push, checkouts) are gracefully silenced.
|
|
147
|
+
*/
|
|
148
|
+
isEnabled() {
|
|
149
|
+
return Boolean(this.config.enabled);
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Programmatically enable or disable the gateway (kill-switch toggle).
|
|
153
|
+
*/
|
|
154
|
+
setEnabled(enabled) {
|
|
155
|
+
this.config.enabled = enabled;
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Returns configured base URL.
|
|
159
|
+
*/
|
|
160
|
+
getBaseUrl() {
|
|
161
|
+
return this.config.baseUrl;
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Resolves a valid Wallet UUID for a given operator or channel.
|
|
165
|
+
*/
|
|
166
|
+
async resolveWalletId(channel) {
|
|
167
|
+
if (this.config.wallets[channel]) {
|
|
168
|
+
return this.config.wallets[channel];
|
|
169
|
+
}
|
|
170
|
+
const wallets = await this.listWallets();
|
|
171
|
+
const match = wallets.find(
|
|
172
|
+
(w) => w.is_active !== false && (w.method?.toLowerCase() === channel || w.wallet_code?.toLowerCase().includes(channel) || w.name?.toLowerCase().includes(channel))
|
|
173
|
+
);
|
|
174
|
+
if (match) {
|
|
175
|
+
return match.id;
|
|
176
|
+
}
|
|
177
|
+
const active = wallets.find((w) => w.is_active !== false);
|
|
178
|
+
return active?.id;
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* Resolves the proper wallet ID for a Mozambican phone number.
|
|
182
|
+
*/
|
|
183
|
+
async resolveWalletIdForPhone(phone) {
|
|
184
|
+
const operator = detectOperator(phone);
|
|
185
|
+
if (operator === "unknown") {
|
|
186
|
+
return void 0;
|
|
187
|
+
}
|
|
188
|
+
return this.resolveWalletId(operator);
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* Initiates a direct STK Push prompt to a mobile phone (M-Pesa, e-Mola, mKesh).
|
|
192
|
+
*/
|
|
193
|
+
async stkPush(request) {
|
|
194
|
+
if (!this.isEnabled()) {
|
|
195
|
+
return {
|
|
196
|
+
success: false,
|
|
197
|
+
status: "disabled",
|
|
198
|
+
reference: null,
|
|
199
|
+
message: "O gateway de pagamento ZumboPay est\xE1 temporariamente desativado."
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
const normalizedPhone = normalizePhone(request.phone);
|
|
203
|
+
const walletId = request.walletId || await this.resolveWalletIdForPhone(normalizedPhone);
|
|
204
|
+
const sourceId = request.reference || `stk-${Date.now()}`;
|
|
205
|
+
const payload = {
|
|
206
|
+
phone: normalizedPhone,
|
|
207
|
+
amount: request.amount,
|
|
208
|
+
wallet_id: walletId,
|
|
209
|
+
reference: sourceId,
|
|
210
|
+
customer_name: request.customerName,
|
|
211
|
+
description: request.description
|
|
212
|
+
};
|
|
213
|
+
try {
|
|
214
|
+
const res = await this.fetchWithTimeout("/charges", {
|
|
215
|
+
method: "POST",
|
|
216
|
+
headers: this.getHeaders(),
|
|
217
|
+
body: JSON.stringify(payload)
|
|
218
|
+
});
|
|
219
|
+
const data = await res.json().catch(() => ({}));
|
|
220
|
+
if (res.ok) {
|
|
221
|
+
const status = (data.data?.status || data.status || "pending").toLowerCase();
|
|
222
|
+
const code = data.code || data.data?.code;
|
|
223
|
+
const isSuccess = status === "success" || status === "succeeded" || status === "completed" || code === "INS-0";
|
|
224
|
+
return {
|
|
225
|
+
success: true,
|
|
226
|
+
status: isSuccess ? "success" : status,
|
|
227
|
+
reference: data.data?.reference || sourceId,
|
|
228
|
+
message: isSuccess ? "Pagamento efetuado com sucesso!" : "Pedido de pagamento enviado para o seu telem\xF3vel. Por favor confirme com o seu PIN.",
|
|
229
|
+
raw: data
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
return {
|
|
233
|
+
success: false,
|
|
234
|
+
status: "declined",
|
|
235
|
+
reference: null,
|
|
236
|
+
message: data.error?.message || data.message || "O pagamento foi recusado ou expirou no telem\xF3vel.",
|
|
237
|
+
raw: data
|
|
238
|
+
};
|
|
239
|
+
} catch (err) {
|
|
240
|
+
const error = err;
|
|
241
|
+
if (error.name === "AbortError" || error.message.includes("timeout")) {
|
|
242
|
+
return {
|
|
243
|
+
success: true,
|
|
244
|
+
status: "pending",
|
|
245
|
+
reference: sourceId,
|
|
246
|
+
message: "O pedido foi enviado. Por favor verifique o telem\xF3vel e confirme com o PIN.",
|
|
247
|
+
raw: { timeout: true }
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
return {
|
|
251
|
+
success: false,
|
|
252
|
+
status: "failed",
|
|
253
|
+
reference: null,
|
|
254
|
+
message: error.message || "Falha na comunica\xE7\xE3o com o ZumboPay."
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
/**
|
|
259
|
+
* Creates a Hosted Checkout URL for credit/debit card & multicanal payments.
|
|
260
|
+
*/
|
|
261
|
+
async createCheckout(request) {
|
|
262
|
+
if (!this.isEnabled()) {
|
|
263
|
+
return {
|
|
264
|
+
success: false,
|
|
265
|
+
checkoutUrl: null,
|
|
266
|
+
reference: null,
|
|
267
|
+
message: "O gateway de pagamento ZumboPay est\xE1 temporariamente desativado."
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
const walletId = request.walletId || await this.resolveWalletId("card") || await this.resolveWalletId("mpesa");
|
|
271
|
+
const sourceId = request.reference || `chk-${Date.now()}`;
|
|
272
|
+
const payload = {
|
|
273
|
+
title: request.title,
|
|
274
|
+
amount: request.amount,
|
|
275
|
+
currency: request.currency || "MZN",
|
|
276
|
+
channels: request.channels || ["card", "mpesa", "emola", "mkesh"],
|
|
277
|
+
wallet_id: walletId,
|
|
278
|
+
reference: sourceId,
|
|
279
|
+
return_url: request.returnUrl,
|
|
280
|
+
redirect_url: request.returnUrl,
|
|
281
|
+
cancel_url: request.cancelUrl,
|
|
282
|
+
metadata: request.metadata
|
|
283
|
+
};
|
|
284
|
+
try {
|
|
285
|
+
const res = await this.fetchWithTimeout("/checkouts", {
|
|
286
|
+
method: "POST",
|
|
287
|
+
headers: this.getHeaders(),
|
|
288
|
+
body: JSON.stringify(payload)
|
|
289
|
+
});
|
|
290
|
+
const data = await res.json().catch(() => ({}));
|
|
291
|
+
if (res.ok) {
|
|
292
|
+
const checkoutUrl = data.data?.checkout_url || data.checkout_url || data.data?.url || data.url || null;
|
|
293
|
+
return {
|
|
294
|
+
success: true,
|
|
295
|
+
checkoutUrl,
|
|
296
|
+
reference: data.data?.reference || sourceId,
|
|
297
|
+
message: "Sess\xE3o de checkout criada com sucesso.",
|
|
298
|
+
raw: data
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
return {
|
|
302
|
+
success: false,
|
|
303
|
+
checkoutUrl: null,
|
|
304
|
+
reference: null,
|
|
305
|
+
message: data.error?.message || data.message || "N\xE3o foi poss\xEDvel gerar a p\xE1gina de checkout.",
|
|
306
|
+
raw: data
|
|
307
|
+
};
|
|
308
|
+
} catch (err) {
|
|
309
|
+
const error = err;
|
|
310
|
+
return {
|
|
311
|
+
success: false,
|
|
312
|
+
checkoutUrl: null,
|
|
313
|
+
reference: null,
|
|
314
|
+
message: error.message || "Erro de conex\xE3o ao criar checkout."
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
/**
|
|
319
|
+
* Queries status of an existing charge/checkout transaction.
|
|
320
|
+
*/
|
|
321
|
+
async getStatus(referenceOrId) {
|
|
322
|
+
const res = await this.fetchWithTimeout(
|
|
323
|
+
`/charges/${encodeURIComponent(referenceOrId)}`,
|
|
324
|
+
{
|
|
325
|
+
method: "GET",
|
|
326
|
+
headers: this.getHeaders()
|
|
327
|
+
}
|
|
328
|
+
);
|
|
329
|
+
const data = await res.json().catch(() => ({}));
|
|
330
|
+
const payload = data.data || data;
|
|
331
|
+
const status = (payload.status || "pending").toLowerCase();
|
|
332
|
+
const isPaid = status === "success" || status === "succeeded" || status === "completed" || payload.is_paid === true;
|
|
333
|
+
return {
|
|
334
|
+
success: res.ok,
|
|
335
|
+
status,
|
|
336
|
+
paid: isPaid,
|
|
337
|
+
reference: payload.reference || referenceOrId,
|
|
338
|
+
amount: payload.amount,
|
|
339
|
+
currency: payload.currency || "MZN",
|
|
340
|
+
channel: payload.channel,
|
|
341
|
+
paidAt: payload.paid_at || payload.updated_at || null,
|
|
342
|
+
raw: data
|
|
343
|
+
};
|
|
344
|
+
}
|
|
345
|
+
/**
|
|
346
|
+
* Lists all wallets associated with the merchant account (cached for 10 minutes).
|
|
347
|
+
*/
|
|
348
|
+
async listWallets() {
|
|
349
|
+
const now = Date.now();
|
|
350
|
+
if (this.cachedWallets && now < this.walletsCacheExpiresAt) {
|
|
351
|
+
return this.cachedWallets;
|
|
352
|
+
}
|
|
353
|
+
try {
|
|
354
|
+
const res = await this.fetchWithTimeout("/wallets", {
|
|
355
|
+
method: "GET",
|
|
356
|
+
headers: this.getHeaders()
|
|
357
|
+
});
|
|
358
|
+
if (res.ok) {
|
|
359
|
+
const data = await res.json();
|
|
360
|
+
this.cachedWallets = data.data || data || [];
|
|
361
|
+
this.walletsCacheExpiresAt = now + 10 * 60 * 1e3;
|
|
362
|
+
return this.cachedWallets;
|
|
363
|
+
}
|
|
364
|
+
return [];
|
|
365
|
+
} catch {
|
|
366
|
+
return [];
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
getHeaders() {
|
|
370
|
+
return {
|
|
371
|
+
Authorization: `Bearer ${this.config.apiKey}`,
|
|
372
|
+
"X-Merchant-Id": this.config.merchantId,
|
|
373
|
+
"Content-Type": "application/json",
|
|
374
|
+
Accept: "application/json"
|
|
375
|
+
};
|
|
376
|
+
}
|
|
377
|
+
async fetchWithTimeout(endpoint, options) {
|
|
378
|
+
const controller = new AbortController();
|
|
379
|
+
const timer = setTimeout(() => controller.abort(), this.config.timeout);
|
|
380
|
+
const url = `${this.config.baseUrl}${endpoint}`;
|
|
381
|
+
try {
|
|
382
|
+
return await fetch(url, {
|
|
383
|
+
...options,
|
|
384
|
+
signal: controller.signal
|
|
385
|
+
});
|
|
386
|
+
} finally {
|
|
387
|
+
clearTimeout(timer);
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
};
|
|
391
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
392
|
+
0 && (module.exports = {
|
|
393
|
+
ZumboPayClient,
|
|
394
|
+
detectOperator,
|
|
395
|
+
getOperatorLabel,
|
|
396
|
+
isValidMozPhone,
|
|
397
|
+
normalizePhone,
|
|
398
|
+
verifyWebhookSignature
|
|
399
|
+
});
|
|
400
|
+
//# sourceMappingURL=index.js.map
|