@herberthtk/yo-payments-api 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/CHANGELOG.md +6 -0
- package/LICENSE +21 -0
- package/README.md +236 -0
- package/certs/Yo_Uganda_Public_Certificate.crt +28 -0
- package/certs/Yo_Uganda_Public_Sandbox_Certificate.crt +35 -0
- package/dist/index.cjs +857 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +391 -0
- package/dist/index.d.ts +391 -0
- package/dist/index.js +828 -0
- package/dist/index.js.map +1 -0
- package/package.json +80 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,828 @@
|
|
|
1
|
+
// src/errors.ts
|
|
2
|
+
var YoAPIError = class extends Error {
|
|
3
|
+
/** HTTP status code when the failure came with an HTTP response. */
|
|
4
|
+
status;
|
|
5
|
+
/** Truncated response body (up to 500 chars) when one was received. */
|
|
6
|
+
body;
|
|
7
|
+
constructor(message, options) {
|
|
8
|
+
super(message, options?.cause !== void 0 ? { cause: options.cause } : void 0);
|
|
9
|
+
this.name = "YoAPIError";
|
|
10
|
+
this.status = options?.status;
|
|
11
|
+
this.body = options?.body;
|
|
12
|
+
}
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
// src/YoAPI.ts
|
|
16
|
+
import { createHash, createPrivateKey, sign as rsaSign, verify as rsaVerify } from "crypto";
|
|
17
|
+
import { readFileSync as readFileSync2 } from "fs";
|
|
18
|
+
import { join as join2 } from "path";
|
|
19
|
+
|
|
20
|
+
// src/constants.ts
|
|
21
|
+
import { dirname, join } from "path";
|
|
22
|
+
import { fileURLToPath } from "url";
|
|
23
|
+
|
|
24
|
+
// src/embeddedCerts.ts
|
|
25
|
+
var YO_UGANDA_SANDBOX_CERTIFICATE = "-----BEGIN CERTIFICATE-----\n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\n-----END CERTIFICATE-----\n";
|
|
26
|
+
var YO_UGANDA_PRODUCTION_CERTIFICATE = "-----BEGIN CERTIFICATE-----\nMIIEvTCCA6WgAwIBAgIJAN3e7VqDg5zQMA0GCSqGSIb3DQEBBQUAMIGaMQswCQYD\nVQQGEwJVRzEQMA4GA1UECBMHS2FtcGFsYTEQMA4GA1UEBxMHS2FtcGFsYTEbMBkG\nA1UECgwSWW8hIFVnYW5kYSBMaW1pdGVkMRUwEwYDVQQLDAxZbyEgUGF5bWVudHMx\nFTATBgNVBAMTDHd3dy55by5jby51ZzEcMBoGCSqGSIb3DQEJARYNaW5mb0B5by5j\nby51ZzAeFw0xMzA4MDkwNTQyMTRaFw0yMzA4MDcwNTQyMTRaMIGaMQswCQYDVQQG\nEwJVRzEQMA4GA1UECBMHS2FtcGFsYTEQMA4GA1UEBxMHS2FtcGFsYTEbMBkGA1UE\nCgwSWW8hIFVnYW5kYSBMaW1pdGVkMRUwEwYDVQQLDAxZbyEgUGF5bWVudHMxFTAT\nBgNVBAMTDHd3dy55by5jby51ZzEcMBoGCSqGSIb3DQEJARYNaW5mb0B5by5jby51\nZzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAPPo+N67Z56ebScXJ9tX\ntFpSNBNNyDlqU/X8bqouZjWuvxpWOI4xZkPKXi0t205ooVbQL/+962NASjJRrouQ\nIUhJq7xhwb+KKcWyFpA25742mNgaxeZJa9iofiHeKotBvHz6pswuqa2gXAyTTmYf\nj6BOIFhDeUffOjfJYbzACy7WLtbK6VIRSTHypQY+zMQluw1euyY8524GYzf8E+c5\n9qjIa5YY5PPianvvR25VDNRCm0Z6GPolhIGvYPUWHFZx+HtU8xoZumi5Kddvipew\nuujxNVBRyQ8bVRoYxKKuDMFHiXA6V01oPzSOtfPK7JI+rd2JFU7dQgbFxTXI9+Qx\n2yUCAwEAAaOCAQIwgf8wHQYDVR0OBBYEFPj0nwwE8lJByx243yV6cfXbTKbhMIHP\nBgNVHSMEgccwgcSAFPj0nwwE8lJByx243yV6cfXbTKbhoYGgpIGdMIGaMQswCQYD\nVQQGEwJVRzEQMA4GA1UECBMHS2FtcGFsYTEQMA4GA1UEBxMHS2FtcGFsYTEbMBkG\nA1UECgwSWW8hIFVnYW5kYSBMaW1pdGVkMRUwEwYDVQQLDAxZbyEgUGF5bWVudHMx\nFTATBgNVBAMTDHd3dy55by5jby51ZzEcMBoGCSqGSIb3DQEJARYNaW5mb0B5by5j\nby51Z4IJAN3e7VqDg5zQMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEFBQADggEB\nAGCaUMHBxGVtVsA8xMDWknjH6hV9yuca3s0qRrOoMfM7nyOjeYtUNgZlsLxuX2n3\nFhoeK9DUBvIKVSlVfO5SXgsXyWKG54YFEkZ8D50Krsyl5NCfaAJezkQ0MNdtpG98\nwlD/cYa6C6DC/s1eilUbI5QqaxLo+EFy5VuHQ8tAuxJbNTVPMW9GvTjxofeMUnug\nSxUMDqHmEkzbQV7yCBVqf3yi4XOM4/6B7Tr6gaandpuR+v2XaKl4SOf8G5svn96g\nKn+Bk8p6rlBWAl+5hWxHWi4dkjiLsk8q+aeKh6ibwYtRjEt/sbWTgJAZjI1mTT8d\nwsLYlL7k1O3wCjUeMQzi274=\n-----END CERTIFICATE-----\n";
|
|
27
|
+
|
|
28
|
+
// src/constants.ts
|
|
29
|
+
var SANDBOX_URL = "https://sandbox.yo.co.ug/services/yopaymentsdev/task.php";
|
|
30
|
+
var PRODUCTION_URL = "https://paymentsapi1.yo.co.ug/ybs/task.php";
|
|
31
|
+
var PUBLIC_KEY_FILE_FOR_SANDBOX = "Yo_Uganda_Public_Sandbox_Certificate.crt";
|
|
32
|
+
var PUBLIC_KEY_FILE_FOR_PRODUCTION = "Yo_Uganda_Public_Certificate.crt";
|
|
33
|
+
var DEFAULT_MAX_RESPONSE_BYTES = 1024 * 1024;
|
|
34
|
+
var MODULE_DIR = resolveModuleDir();
|
|
35
|
+
var CERTS_DIR = join(MODULE_DIR, "..", "certs");
|
|
36
|
+
function resolveModuleDir() {
|
|
37
|
+
try {
|
|
38
|
+
const metaUrl = import.meta?.url;
|
|
39
|
+
if (typeof metaUrl === "string" && metaUrl.length > 0) {
|
|
40
|
+
return dirname(fileURLToPath(metaUrl));
|
|
41
|
+
}
|
|
42
|
+
} catch {
|
|
43
|
+
}
|
|
44
|
+
return process.cwd();
|
|
45
|
+
}
|
|
46
|
+
function defaultVerificationCertificate(mode) {
|
|
47
|
+
return mode === "sandbox" ? YO_UGANDA_SANDBOX_CERTIFICATE : YO_UGANDA_PRODUCTION_CERTIFICATE;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// src/http.ts
|
|
51
|
+
async function postXml(url, xml, options) {
|
|
52
|
+
const init = {
|
|
53
|
+
method: "POST",
|
|
54
|
+
body: xml,
|
|
55
|
+
headers: {
|
|
56
|
+
"Content-Type": "text/xml",
|
|
57
|
+
"Content-transfer-encoding": "text",
|
|
58
|
+
"Content-Length": String(Buffer.byteLength(xml))
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
if (options.timeoutMs > 0) {
|
|
62
|
+
init.signal = AbortSignal.timeout(options.timeoutMs);
|
|
63
|
+
}
|
|
64
|
+
if (!options.verifyTls) {
|
|
65
|
+
init.tls = { rejectUnauthorized: false };
|
|
66
|
+
}
|
|
67
|
+
let res;
|
|
68
|
+
try {
|
|
69
|
+
res = await fetch(url, init);
|
|
70
|
+
} catch (error) {
|
|
71
|
+
throw new YoAPIError(
|
|
72
|
+
`Request to the Yo! Payments gateway failed: ${error?.message ?? error}`,
|
|
73
|
+
{ cause: error }
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
const text = await readBoundedText(res, options.maxResponseBytes);
|
|
77
|
+
if (!res.ok) {
|
|
78
|
+
throw new YoAPIError(`Yo! Payments gateway responded with HTTP ${res.status}`, {
|
|
79
|
+
status: res.status,
|
|
80
|
+
body: text.slice(0, 500)
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
return text;
|
|
84
|
+
}
|
|
85
|
+
async function readBoundedText(res, limit) {
|
|
86
|
+
const declared = res.headers.get("content-length");
|
|
87
|
+
if (declared !== null && Number(declared) > limit) {
|
|
88
|
+
throw new YoAPIError(
|
|
89
|
+
`Yo! Payments gateway response (${declared} bytes) exceeds the limit of ${limit} bytes`
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
if (res.body === null) {
|
|
93
|
+
return "";
|
|
94
|
+
}
|
|
95
|
+
const reader = res.body.getReader();
|
|
96
|
+
const chunks = [];
|
|
97
|
+
let size = 0;
|
|
98
|
+
try {
|
|
99
|
+
for (; ; ) {
|
|
100
|
+
const { done, value } = await reader.read();
|
|
101
|
+
if (done) break;
|
|
102
|
+
size += value.byteLength;
|
|
103
|
+
if (size > limit) {
|
|
104
|
+
await reader.cancel().catch(() => void 0);
|
|
105
|
+
throw new YoAPIError(
|
|
106
|
+
`Yo! Payments gateway response exceeds the limit of ${limit} bytes`
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
chunks.push(value);
|
|
110
|
+
}
|
|
111
|
+
} finally {
|
|
112
|
+
reader.releaseLock();
|
|
113
|
+
}
|
|
114
|
+
return Buffer.concat(chunks).toString("utf-8");
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// src/keys.ts
|
|
118
|
+
import { createPublicKey } from "crypto";
|
|
119
|
+
import { readFileSync, statSync } from "fs";
|
|
120
|
+
var publicKeyCache = /* @__PURE__ */ new Map();
|
|
121
|
+
function loadPublicKeyCached(filePath, fallbackPem) {
|
|
122
|
+
const fromFile = loadFromFile(filePath);
|
|
123
|
+
if (fromFile !== null) return fromFile;
|
|
124
|
+
if (fallbackPem === void 0) return null;
|
|
125
|
+
try {
|
|
126
|
+
return createPublicKey(fallbackPem);
|
|
127
|
+
} catch {
|
|
128
|
+
return null;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
function loadFromFile(filePath) {
|
|
132
|
+
let size;
|
|
133
|
+
let mtimeMs;
|
|
134
|
+
try {
|
|
135
|
+
const stat = statSync(filePath);
|
|
136
|
+
size = stat.size;
|
|
137
|
+
mtimeMs = stat.mtimeMs;
|
|
138
|
+
} catch {
|
|
139
|
+
return null;
|
|
140
|
+
}
|
|
141
|
+
const cached = publicKeyCache.get(filePath);
|
|
142
|
+
if (cached !== void 0 && cached.size === size && cached.mtimeMs === mtimeMs) {
|
|
143
|
+
return cached.key;
|
|
144
|
+
}
|
|
145
|
+
try {
|
|
146
|
+
const key = createPublicKey(readFileSync(filePath, "utf-8"));
|
|
147
|
+
publicKeyCache.set(filePath, { size, mtimeMs, key });
|
|
148
|
+
return key;
|
|
149
|
+
} catch {
|
|
150
|
+
return null;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// src/xml.ts
|
|
155
|
+
import { XMLParser, XMLValidator } from "fast-xml-parser";
|
|
156
|
+
var XML_HEADER = '<?xml version="1.0" encoding="UTF-8"?>';
|
|
157
|
+
var responseParser = new XMLParser({
|
|
158
|
+
ignoreAttributes: true,
|
|
159
|
+
parseTagValue: false,
|
|
160
|
+
ignoreDeclaration: true,
|
|
161
|
+
ignorePiTags: true
|
|
162
|
+
});
|
|
163
|
+
function el(tag, value) {
|
|
164
|
+
return `<${tag}>${value}</${tag}>`;
|
|
165
|
+
}
|
|
166
|
+
function opt(tag, value) {
|
|
167
|
+
if (value === null || value === void 0 || value === "") return "";
|
|
168
|
+
return el(tag, value);
|
|
169
|
+
}
|
|
170
|
+
function str(value) {
|
|
171
|
+
if (value === void 0 || value === null) return "";
|
|
172
|
+
if (typeof value === "object") return "";
|
|
173
|
+
return String(value);
|
|
174
|
+
}
|
|
175
|
+
function setIfNonEmpty(target, key, value) {
|
|
176
|
+
if (value !== "" && value !== "0") target[key] = value;
|
|
177
|
+
}
|
|
178
|
+
function setIfNotNull(target, key, value) {
|
|
179
|
+
if (value === void 0 || value === null) return;
|
|
180
|
+
if (typeof value === "object") return;
|
|
181
|
+
if (String(value) !== "") target[key] = String(value);
|
|
182
|
+
}
|
|
183
|
+
function asArray(value) {
|
|
184
|
+
if (value === void 0 || value === null) return [];
|
|
185
|
+
return Array.isArray(value) ? value : [value];
|
|
186
|
+
}
|
|
187
|
+
function asRecord(value) {
|
|
188
|
+
if (value === void 0 || value === null || typeof value !== "object") return {};
|
|
189
|
+
return value;
|
|
190
|
+
}
|
|
191
|
+
function parseGatewayResponse(responseXml) {
|
|
192
|
+
const validation = XMLValidator.validate(responseXml);
|
|
193
|
+
if (validation !== true) {
|
|
194
|
+
throw new YoAPIError(`Invalid XML response from the Yo! Payments gateway: ${validation.err.msg}`, {
|
|
195
|
+
body: responseXml.slice(0, 500)
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
const doc = asRecord(responseParser.parse(responseXml));
|
|
199
|
+
if (doc.Response !== void 0) return doc;
|
|
200
|
+
for (const value of Object.values(doc)) {
|
|
201
|
+
const node = asRecord(value);
|
|
202
|
+
if (node.Response !== void 0) return node;
|
|
203
|
+
}
|
|
204
|
+
throw new YoAPIError("Yo! Payments gateway response did not contain a <Response> node", {
|
|
205
|
+
body: responseXml.slice(0, 500)
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// src/YoAPI.ts
|
|
210
|
+
var YoAPI = class {
|
|
211
|
+
/** The Yo! Payments API Username. Required. */
|
|
212
|
+
username;
|
|
213
|
+
/** The Yo! Payments API Password. Required. */
|
|
214
|
+
password;
|
|
215
|
+
/** Whether the gateway connection is held open until the request completes. Default "FALSE". */
|
|
216
|
+
nonBlocking = "FALSE";
|
|
217
|
+
/** An externally agreed reference (e.g. an invoice number). */
|
|
218
|
+
externalReference = null;
|
|
219
|
+
/** A reference code related to another Yo! Payments system transaction. */
|
|
220
|
+
internalReference = null;
|
|
221
|
+
/** Text appended to the confirmation SMS sent by the mobile money provider. */
|
|
222
|
+
providerReferenceText = null;
|
|
223
|
+
/** URL notified as soon as funds are successfully deposited into your account. */
|
|
224
|
+
instantNotificationUrl = null;
|
|
225
|
+
/** URL notified as soon as a deposit request fails. */
|
|
226
|
+
failureNotificationUrl = null;
|
|
227
|
+
/** May be required to authenticate certain deposit requests. */
|
|
228
|
+
authenticationSignatureBase64 = null;
|
|
229
|
+
/** "PULL" or "PUSH". Default "PULL". */
|
|
230
|
+
depositTransactionType = "PULL";
|
|
231
|
+
/** The URL API requests are submitted to. */
|
|
232
|
+
yoUrl = PRODUCTION_URL;
|
|
233
|
+
/** Certificate used to verify IPN signatures (sandbox or production). */
|
|
234
|
+
publicKeyFile;
|
|
235
|
+
/** Whether publicKeyFile is still the bundled default (enables the embedded-cert fallback). */
|
|
236
|
+
publicKeyFileIsDefault = true;
|
|
237
|
+
transactionLimitAccountIdentifier = null;
|
|
238
|
+
/** Unique nonce per request, required when public key authentication is enabled. */
|
|
239
|
+
publicKeyAuthenticationNonce = null;
|
|
240
|
+
/** Base64 RSA signature over SHA1(username+amount+account+narrative+external_ref+nonce). */
|
|
241
|
+
publicKeyAuthenticationSignatureBase64 = null;
|
|
242
|
+
/** Location of the private key used to sign the public key authentication signature. */
|
|
243
|
+
privateKeyFileLocation = null;
|
|
244
|
+
/**
|
|
245
|
+
* Private key PEM content used to sign the public key authentication signature.
|
|
246
|
+
* Prefer this over a file location on serverless/bundled hosts where the
|
|
247
|
+
* filesystem is ephemeral (e.g. Vercel). Takes precedence when both are set.
|
|
248
|
+
*/
|
|
249
|
+
privateKeyContent = null;
|
|
250
|
+
mode;
|
|
251
|
+
/** Request timeout in milliseconds (PHP library uses curl timeout 120s). <= 0 means no timeout, like curl. */
|
|
252
|
+
timeoutMs = 12e4;
|
|
253
|
+
/**
|
|
254
|
+
* Whether to verify the gateway TLS certificate. Default true.
|
|
255
|
+
* The PHP library disables peer verification; this port verifies by default and
|
|
256
|
+
* only skips verification when explicitly opted out via setTlsVerificationEnabled(false).
|
|
257
|
+
*/
|
|
258
|
+
verifyTls = true;
|
|
259
|
+
/** Maximum accepted gateway response body in bytes (default 1 MiB). */
|
|
260
|
+
maxResponseBytes = DEFAULT_MAX_RESPONSE_BYTES;
|
|
261
|
+
constructor(username, password, mode = "production") {
|
|
262
|
+
this.username = username;
|
|
263
|
+
this.password = password;
|
|
264
|
+
this.mode = mode;
|
|
265
|
+
if (mode === "sandbox") {
|
|
266
|
+
this.yoUrl = SANDBOX_URL;
|
|
267
|
+
this.publicKeyFile = join2(CERTS_DIR, PUBLIC_KEY_FILE_FOR_SANDBOX);
|
|
268
|
+
} else {
|
|
269
|
+
this.yoUrl = PRODUCTION_URL;
|
|
270
|
+
this.publicKeyFile = join2(CERTS_DIR, PUBLIC_KEY_FILE_FOR_PRODUCTION);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
/** Returns the mode ("production" or "sandbox") this instance was created with. */
|
|
274
|
+
getMode() {
|
|
275
|
+
return this.mode;
|
|
276
|
+
}
|
|
277
|
+
/** Set the API Username. */
|
|
278
|
+
setUsername(username) {
|
|
279
|
+
this.username = username;
|
|
280
|
+
}
|
|
281
|
+
/** Returns the API Username. */
|
|
282
|
+
getUsername() {
|
|
283
|
+
return this.username;
|
|
284
|
+
}
|
|
285
|
+
/** Set the API Password. */
|
|
286
|
+
setPassword(password) {
|
|
287
|
+
this.password = password;
|
|
288
|
+
}
|
|
289
|
+
/** Returns the API Password. */
|
|
290
|
+
getPassword() {
|
|
291
|
+
return this.password;
|
|
292
|
+
}
|
|
293
|
+
/** Set the URL to submit API requests to. */
|
|
294
|
+
setUrl(url) {
|
|
295
|
+
this.yoUrl = url;
|
|
296
|
+
}
|
|
297
|
+
/** Returns the URL API requests are submitted to. */
|
|
298
|
+
getUrl() {
|
|
299
|
+
return this.yoUrl;
|
|
300
|
+
}
|
|
301
|
+
/** Set the path of the certificate used to verify IPN signatures. */
|
|
302
|
+
setPublicKeyFileUrl(publicKeyFileUrl) {
|
|
303
|
+
this.publicKeyFile = publicKeyFileUrl;
|
|
304
|
+
this.publicKeyFileIsDefault = false;
|
|
305
|
+
}
|
|
306
|
+
/** Returns the path of the certificate used to verify IPN signatures. */
|
|
307
|
+
getPublicKeyFileUrl() {
|
|
308
|
+
return this.publicKeyFile;
|
|
309
|
+
}
|
|
310
|
+
/** Set the NonBlocking variable: "TRUE" for non-blocking API requests. */
|
|
311
|
+
setNonblocking(nonblocking) {
|
|
312
|
+
this.nonBlocking = nonblocking;
|
|
313
|
+
}
|
|
314
|
+
/** Returns the NonBlocking variable. */
|
|
315
|
+
getNonblocking() {
|
|
316
|
+
return this.nonBlocking;
|
|
317
|
+
}
|
|
318
|
+
/** Set the External Reference used when submitting payment requests. */
|
|
319
|
+
setExternalReference(externalReference) {
|
|
320
|
+
this.externalReference = externalReference;
|
|
321
|
+
}
|
|
322
|
+
/** Returns the externalReference variable. */
|
|
323
|
+
getExternalReference() {
|
|
324
|
+
return this.externalReference;
|
|
325
|
+
}
|
|
326
|
+
/** Set the Internal Reference used when submitting payment requests. */
|
|
327
|
+
setInternalReference(internalReference) {
|
|
328
|
+
this.internalReference = internalReference;
|
|
329
|
+
}
|
|
330
|
+
/** Returns the internalReference variable. */
|
|
331
|
+
getInternalReference() {
|
|
332
|
+
return this.internalReference;
|
|
333
|
+
}
|
|
334
|
+
/** Set the Provider Reference Text used when submitting payment requests. */
|
|
335
|
+
setProviderReferenceText(providerReferenceText) {
|
|
336
|
+
this.providerReferenceText = providerReferenceText;
|
|
337
|
+
}
|
|
338
|
+
/** Returns the providerReferenceText variable. */
|
|
339
|
+
getProviderReferenceText() {
|
|
340
|
+
return this.providerReferenceText;
|
|
341
|
+
}
|
|
342
|
+
/** Set the Instant Notification URL (useful for non-blocking requests). */
|
|
343
|
+
setInstantNotificationUrl(instantNotificationUrl) {
|
|
344
|
+
this.instantNotificationUrl = instantNotificationUrl;
|
|
345
|
+
}
|
|
346
|
+
/** Returns the instantNotificationUrl variable. */
|
|
347
|
+
getInstantNotificationUrl() {
|
|
348
|
+
return this.instantNotificationUrl;
|
|
349
|
+
}
|
|
350
|
+
/** Set the Failure Notification URL (useful for non-blocking requests). */
|
|
351
|
+
setFailureNotificationUrl(failureNotificationUrl) {
|
|
352
|
+
this.failureNotificationUrl = failureNotificationUrl;
|
|
353
|
+
}
|
|
354
|
+
/** Returns the failureNotificationUrl variable. */
|
|
355
|
+
getFailureNotificationUrl() {
|
|
356
|
+
return this.failureNotificationUrl;
|
|
357
|
+
}
|
|
358
|
+
/** Set the Authentication Signature Base64. */
|
|
359
|
+
setAuthenticationSignatureBase64(authenticationSignatureBase64) {
|
|
360
|
+
this.authenticationSignatureBase64 = authenticationSignatureBase64;
|
|
361
|
+
}
|
|
362
|
+
/** Returns the Authentication Signature Base64 variable. */
|
|
363
|
+
getAuthenticationSignatureBase64() {
|
|
364
|
+
return this.authenticationSignatureBase64;
|
|
365
|
+
}
|
|
366
|
+
/** Set the Deposit Transaction Type ("PULL" or "PUSH") used by acTransactionCheckStatus. */
|
|
367
|
+
setDepositTransactionType(depositTransactionType) {
|
|
368
|
+
this.depositTransactionType = depositTransactionType;
|
|
369
|
+
}
|
|
370
|
+
/** Returns the Deposit Transaction Type variable. */
|
|
371
|
+
getDepositTransactionType() {
|
|
372
|
+
return this.depositTransactionType;
|
|
373
|
+
}
|
|
374
|
+
/** Set the Transaction Limit Account Identifier (refer to your account administrator). */
|
|
375
|
+
setTransactionLimitAccountIdentifier(transactionLimitAccountIdentifier) {
|
|
376
|
+
this.transactionLimitAccountIdentifier = transactionLimitAccountIdentifier;
|
|
377
|
+
}
|
|
378
|
+
/** Returns the Transaction Limit Account Identifier variable. */
|
|
379
|
+
getTransactionLimitAccountIdentifier() {
|
|
380
|
+
return this.transactionLimitAccountIdentifier;
|
|
381
|
+
}
|
|
382
|
+
/** Set the Public Key Authentication Nonce (refer to your account administrator). */
|
|
383
|
+
setPublicKeyAuthenticationNonce(publicKeyAuthenticationNonce) {
|
|
384
|
+
this.publicKeyAuthenticationNonce = publicKeyAuthenticationNonce;
|
|
385
|
+
}
|
|
386
|
+
/** Returns the Public Key Authentication Nonce variable. */
|
|
387
|
+
getPublicKeyAuthenticationNonce() {
|
|
388
|
+
return this.publicKeyAuthenticationNonce;
|
|
389
|
+
}
|
|
390
|
+
/** Set the Public Key Authentication Base64-Encoded Signature (refer to your account administrator). */
|
|
391
|
+
setPublicKeyAuthenticationSignatureBase64(publicKeyAuthenticationSignatureBase64) {
|
|
392
|
+
this.publicKeyAuthenticationSignatureBase64 = publicKeyAuthenticationSignatureBase64;
|
|
393
|
+
}
|
|
394
|
+
/** Returns the Public Key Authentication Base64-Encoded Signature variable. */
|
|
395
|
+
getPublicKeyAuthenticationSignatureBase64() {
|
|
396
|
+
return this.publicKeyAuthenticationSignatureBase64;
|
|
397
|
+
}
|
|
398
|
+
/** Set the location of the private key used to sign the public key authentication signature. */
|
|
399
|
+
setPrivateKeyFileLocation(privateKeyFileLocation) {
|
|
400
|
+
this.privateKeyFileLocation = privateKeyFileLocation;
|
|
401
|
+
}
|
|
402
|
+
/** Returns the Private Key File variable. */
|
|
403
|
+
getPrivateKeyFileLocation() {
|
|
404
|
+
return this.privateKeyFileLocation;
|
|
405
|
+
}
|
|
406
|
+
/**
|
|
407
|
+
* Set the private key PEM content directly (alternative to setPrivateKeyFileLocation).
|
|
408
|
+
* Useful where key files are unavailable, e.g. serverless deployments reading
|
|
409
|
+
* the key from an environment variable. Takes precedence when both are set.
|
|
410
|
+
*/
|
|
411
|
+
setPrivateKeyContent(privateKeyContent) {
|
|
412
|
+
this.privateKeyContent = privateKeyContent;
|
|
413
|
+
}
|
|
414
|
+
/** Returns the Private Key PEM content variable. */
|
|
415
|
+
getPrivateKeyContent() {
|
|
416
|
+
return this.privateKeyContent;
|
|
417
|
+
}
|
|
418
|
+
/** Set the request timeout in milliseconds. Values <= 0 disable the timeout (like PHP curl timeout 0). */
|
|
419
|
+
setTimeout(timeoutMs) {
|
|
420
|
+
this.timeoutMs = timeoutMs;
|
|
421
|
+
}
|
|
422
|
+
/** Returns the request timeout in milliseconds. */
|
|
423
|
+
getTimeout() {
|
|
424
|
+
return this.timeoutMs;
|
|
425
|
+
}
|
|
426
|
+
/**
|
|
427
|
+
* Enable or disable verification of the gateway TLS certificate (default enabled).
|
|
428
|
+
* Disable only for testing against endpoints with self-signed certificates —
|
|
429
|
+
* the PHP library always skips verification.
|
|
430
|
+
* Note: the underlying mechanism is a Bun fetch extension; on Node.js, disabling
|
|
431
|
+
* verification additionally requires NODE_TLS_REJECT_UNAUTHORIZED=0 in the environment.
|
|
432
|
+
*/
|
|
433
|
+
setTlsVerificationEnabled(enabled) {
|
|
434
|
+
this.verifyTls = enabled;
|
|
435
|
+
}
|
|
436
|
+
/** Returns whether gateway TLS certificate verification is enabled. */
|
|
437
|
+
getTlsVerificationEnabled() {
|
|
438
|
+
return this.verifyTls;
|
|
439
|
+
}
|
|
440
|
+
/** Set the maximum accepted gateway response body in bytes (default 1048576). */
|
|
441
|
+
setMaxResponseBytes(maxResponseBytes) {
|
|
442
|
+
this.maxResponseBytes = maxResponseBytes;
|
|
443
|
+
}
|
|
444
|
+
/** Returns the maximum accepted gateway response body in bytes. */
|
|
445
|
+
getMaxResponseBytes() {
|
|
446
|
+
return this.maxResponseBytes;
|
|
447
|
+
}
|
|
448
|
+
/**
|
|
449
|
+
* Request Mobile Money User to deposit funds into your account.
|
|
450
|
+
* Shortly after submitting, the mobile money user receives an on-screen prompt to
|
|
451
|
+
* authorize the transfer. Not supported by all mobile money operator networks.
|
|
452
|
+
* @param msisdn the mobile money phone number in the format 256772123456
|
|
453
|
+
* @param amount the amount to deposit into your account (fractions supported)
|
|
454
|
+
* @param narrative the reason for the mobile money user to deposit funds
|
|
455
|
+
*/
|
|
456
|
+
async acDepositFunds(msisdn, amount, narrative) {
|
|
457
|
+
const xml = this.requestXml(
|
|
458
|
+
this.authXml() + el("Method", "acdepositfunds") + el("NonBlocking", this.nonBlocking) + el("Account", msisdn) + el("Amount", amount) + el("Narrative", narrative) + opt("ExternalReference", this.externalReference) + opt("InternalReference", this.internalReference) + opt("ProviderReferenceText", this.providerReferenceText) + opt("InstantNotificationUrl", this.instantNotificationUrl) + opt("FailureNotificationUrl", this.failureNotificationUrl) + opt("AuthenticationSignatureBase64", this.authenticationSignatureBase64)
|
|
459
|
+
);
|
|
460
|
+
const response = asRecord((await this.parseResponse(xml)).Response);
|
|
461
|
+
const result = {
|
|
462
|
+
Status: str(response.Status),
|
|
463
|
+
StatusCode: str(response.StatusCode),
|
|
464
|
+
StatusMessage: str(response.StatusMessage),
|
|
465
|
+
TransactionStatus: str(response.TransactionStatus)
|
|
466
|
+
};
|
|
467
|
+
setIfNonEmpty(result, "ErrorMessageCode", str(response.ErrorMessageCode));
|
|
468
|
+
setIfNonEmpty(result, "ErrorMessage", str(response.ErrorMessage));
|
|
469
|
+
setIfNonEmpty(result, "TransactionReference", str(response.TransactionReference));
|
|
470
|
+
setIfNonEmpty(result, "MNOTransactionReferenceId", str(response.MNOTransactionReferenceId));
|
|
471
|
+
setIfNonEmpty(result, "IssuedReceiptNumber", str(response.IssuedReceiptNumber));
|
|
472
|
+
return result;
|
|
473
|
+
}
|
|
474
|
+
/**
|
|
475
|
+
* Check the status of a transaction that was earlier submitted for processing.
|
|
476
|
+
* Particularly useful when NonBlocking is "TRUE".
|
|
477
|
+
* @param transactionReference the gateway reference uniquely identifying the transaction
|
|
478
|
+
* @param privateTransactionReference the External Reference used to carry out the transaction
|
|
479
|
+
*/
|
|
480
|
+
async acTransactionCheckStatus(transactionReference, privateTransactionReference = null) {
|
|
481
|
+
const xml = this.requestXml(
|
|
482
|
+
this.authXml() + el("Method", "actransactioncheckstatus") + opt("TransactionReference", transactionReference) + opt("PrivateTransactionReference", privateTransactionReference) + el("DepositTransactionType", this.depositTransactionType)
|
|
483
|
+
);
|
|
484
|
+
const response = asRecord((await this.parseResponse(xml)).Response);
|
|
485
|
+
const result = {
|
|
486
|
+
Status: str(response.Status),
|
|
487
|
+
StatusCode: str(response.StatusCode),
|
|
488
|
+
StatusMessage: str(response.StatusMessage),
|
|
489
|
+
TransactionStatus: str(response.TransactionStatus)
|
|
490
|
+
};
|
|
491
|
+
setIfNonEmpty(result, "ErrorMessageCode", str(response.ErrorMessageCode));
|
|
492
|
+
setIfNonEmpty(result, "ErrorMessage", str(response.ErrorMessage));
|
|
493
|
+
setIfNonEmpty(result, "TransactionReference", str(response.TransactionReference));
|
|
494
|
+
setIfNonEmpty(result, "MNOTransactionReferenceId", str(response.MNOTransactionReferenceId));
|
|
495
|
+
setIfNonEmpty(result, "Amount", str(response.Amount));
|
|
496
|
+
setIfNonEmpty(result, "AmountFormatted", str(response.AmountFormatted));
|
|
497
|
+
setIfNonEmpty(result, "CurrencyCode", str(response.CurrencyCode));
|
|
498
|
+
setIfNonEmpty(result, "TransactionInitiationDate", str(response.TransactionInitiationDate));
|
|
499
|
+
setIfNonEmpty(result, "TransactionCompletionDate", str(response.TransactionCompletionDate));
|
|
500
|
+
setIfNonEmpty(result, "IssuedReceiptNumber", str(response.IssuedReceiptNumber));
|
|
501
|
+
return result;
|
|
502
|
+
}
|
|
503
|
+
/**
|
|
504
|
+
* Transfer funds from your Payment Account to another Yo! Payments Account.
|
|
505
|
+
* @param currencyCode e.g. "UGX-MTNMM", "UGX-MTNAT", "UGX-WTLAT", "UGX-OULAT", "UGX-AIRAT"
|
|
506
|
+
* @param amount the amount to be transferred
|
|
507
|
+
* @param beneficiaryAccount account number of the beneficiary Yo! Payments user
|
|
508
|
+
* @param beneficiaryEmail email address of the recipient of funds
|
|
509
|
+
* @param narrative textual narrative about the transaction
|
|
510
|
+
*/
|
|
511
|
+
async acInternalTransfer(currencyCode, amount, beneficiaryAccount, beneficiaryEmail, narrative) {
|
|
512
|
+
const xml = this.requestXml(
|
|
513
|
+
this.authXml() + el("Method", "acinternaltransfer") + el("CurrencyCode", currencyCode) + el("Amount", amount) + el("BeneficiaryAccount", beneficiaryAccount) + el("BeneficiaryEmail", beneficiaryEmail) + el("Narrative", narrative) + opt("InternalReference", this.internalReference) + opt("ExternalReference", this.externalReference)
|
|
514
|
+
);
|
|
515
|
+
const response = asRecord((await this.parseResponse(xml)).Response);
|
|
516
|
+
const result = {
|
|
517
|
+
Status: str(response.Status),
|
|
518
|
+
StatusCode: str(response.StatusCode),
|
|
519
|
+
StatusMessage: str(response.StatusMessage),
|
|
520
|
+
TransactionStatus: str(response.TransactionStatus)
|
|
521
|
+
};
|
|
522
|
+
setIfNonEmpty(result, "ErrorMessageCode", str(response.ErrorMessageCode));
|
|
523
|
+
setIfNonEmpty(result, "ErrorMessage", str(response.ErrorMessage));
|
|
524
|
+
setIfNonEmpty(result, "TransactionReference", str(response.TransactionReference));
|
|
525
|
+
setIfNonEmpty(result, "MNOTransactionReferenceId", str(response.MNOTransactionReferenceId));
|
|
526
|
+
setIfNonEmpty(result, "IssuedReceiptNumber", str(response.IssuedReceiptNumber));
|
|
527
|
+
return result;
|
|
528
|
+
}
|
|
529
|
+
/**
|
|
530
|
+
* Get the current balance of your Yo! Payments Account.
|
|
531
|
+
* The returned object contains an array of balances (including airtime).
|
|
532
|
+
*/
|
|
533
|
+
async acAcctBalance() {
|
|
534
|
+
const xml = this.requestXml(this.authXml() + el("Method", "acacctbalance"));
|
|
535
|
+
const response = asRecord((await this.parseResponse(xml)).Response);
|
|
536
|
+
const result = {
|
|
537
|
+
Status: str(response.Status),
|
|
538
|
+
StatusCode: str(response.StatusCode),
|
|
539
|
+
balance: []
|
|
540
|
+
};
|
|
541
|
+
const currencies = asArray(asRecord(asRecord(response.Balance).Currency));
|
|
542
|
+
for (const currency of currencies) {
|
|
543
|
+
const node = asRecord(currency);
|
|
544
|
+
result.balance.push({ code: str(node.Code), balance: str(node.Balance) });
|
|
545
|
+
}
|
|
546
|
+
setIfNonEmpty(result, "StatusMessage", str(response.StatusMessage));
|
|
547
|
+
setIfNonEmpty(result, "ErrorMessageCode", str(response.ErrorMessageCode));
|
|
548
|
+
setIfNonEmpty(result, "ErrorMessage", str(response.ErrorMessage));
|
|
549
|
+
return result;
|
|
550
|
+
}
|
|
551
|
+
/**
|
|
552
|
+
* Return transactions carried out on your account for a certain period of time.
|
|
553
|
+
* @param startDate format YYYY-MM-DD HH:MM:SS
|
|
554
|
+
* @param endDate format YYYY-MM-DD HH:MM:SS
|
|
555
|
+
* @param transactionStatus e.g. "FAILED", "PENDING", "INDETERMINATE", "SUCCEEDED", "FAILED,SUCCEEDED"
|
|
556
|
+
* @param currencyCode e.g. "UGX-MTNMM", "UGX-WARIDMM", "UGX-MTNAT", "UGX-WTLAT", "UGX-OULAT", "UGX-AIRAT"
|
|
557
|
+
* @param resultSetLimit a value of 0 returns all; default gateway limit = 15
|
|
558
|
+
* @param transactionEntryDesignation "TRANSACTION", "CHARGES" or "ANY"
|
|
559
|
+
* @param externalReference filter using this external reference
|
|
560
|
+
*/
|
|
561
|
+
async acGetMinistatement(startDate = null, endDate = null, transactionStatus = null, currencyCode = null, resultSetLimit = null, transactionEntryDesignation = "ANY", externalReference = null) {
|
|
562
|
+
const xml = this.requestXml(
|
|
563
|
+
this.authXml() + el("Method", "acgetministatement") + opt("StartDate", startDate) + opt("EndDate", endDate) + opt("TransactionStatus", transactionStatus) + opt("CurrencyCode", currencyCode) + opt("ResultSetLimit", resultSetLimit) + el("TransactionEntryDesignation", transactionEntryDesignation) + opt("ExternalReference", externalReference)
|
|
564
|
+
);
|
|
565
|
+
const response = asRecord((await this.parseResponse(xml)).Response);
|
|
566
|
+
const result = {
|
|
567
|
+
Status: str(response.Status),
|
|
568
|
+
StatusCode: str(response.StatusCode),
|
|
569
|
+
TotalTransactions: str(response.TotalTransactions),
|
|
570
|
+
ReturnedTransactions: str(response.ReturnedTransactions),
|
|
571
|
+
Transactions: []
|
|
572
|
+
};
|
|
573
|
+
const transactions = asArray(asRecord(response.Transactions).Transaction);
|
|
574
|
+
for (const transaction of transactions) {
|
|
575
|
+
const node = asRecord(transaction);
|
|
576
|
+
const detail = {
|
|
577
|
+
TransactionSystemId: str(node.TransactionSystemId),
|
|
578
|
+
TransactionReference: str(node.TransactionReference),
|
|
579
|
+
TransactionStatus: str(node.TransactionStatus),
|
|
580
|
+
InitiationDate: str(node.InitiationDate),
|
|
581
|
+
CompletionDate: str(node.CompletionDate),
|
|
582
|
+
NarrativeBase64: str(asArray(node.NarrativeBase64)[0]),
|
|
583
|
+
Currency: str(node.Currency),
|
|
584
|
+
Amount: str(node.Amount),
|
|
585
|
+
Balance: str(node.Balance),
|
|
586
|
+
GeneralType: str(node.GeneralType),
|
|
587
|
+
DetailedType: str(node.DetailedType),
|
|
588
|
+
BeneficiaryBase64: str(node.BeneficiaryBase64),
|
|
589
|
+
SenderBase64: str(node.SenderBase64),
|
|
590
|
+
TransactionEntryDesignation: str(node.TransactionEntryDesignation)
|
|
591
|
+
};
|
|
592
|
+
setIfNonEmpty(detail, "BeneficiaryMsisdn", str(node.BeneficiaryMsisdn));
|
|
593
|
+
setIfNonEmpty(detail, "SenderMsisdn", str(node.SenderMsisdn));
|
|
594
|
+
setIfNonEmpty(detail, "Base64TransactionExternalReference", str(node.Base64TransactionExternalReference));
|
|
595
|
+
result.Transactions.push(detail);
|
|
596
|
+
}
|
|
597
|
+
setIfNonEmpty(result, "ErrorMessageCode", str(response.ErrorMessageCode));
|
|
598
|
+
setIfNonEmpty(result, "ErrorMessage", str(response.ErrorMessage));
|
|
599
|
+
return result;
|
|
600
|
+
}
|
|
601
|
+
/**
|
|
602
|
+
* Send airtime to a mobile phone user.
|
|
603
|
+
* @param msisdn the mobile phone number in the format 256772123456
|
|
604
|
+
* @param amount the amount of airtime to be sent to the mobile user
|
|
605
|
+
* @param narrative textual narrative about the transfer
|
|
606
|
+
*/
|
|
607
|
+
async acSendAirtimeMobile(msisdn, amount, narrative) {
|
|
608
|
+
const xml = this.requestXml(
|
|
609
|
+
this.authXml() + el("Method", "acsendairtimemobile") + el("NonBlocking", this.nonBlocking) + el("Account", msisdn) + el("Amount", amount) + el("Narrative", narrative) + opt("ExternalReference", this.externalReference) + opt("InternalReference", this.internalReference) + opt("ProviderReferenceText", this.providerReferenceText)
|
|
610
|
+
);
|
|
611
|
+
const response = asRecord((await this.parseResponse(xml)).Response);
|
|
612
|
+
const result = {
|
|
613
|
+
Status: str(response.Status),
|
|
614
|
+
StatusCode: str(response.StatusCode),
|
|
615
|
+
StatusMessage: str(response.StatusMessage),
|
|
616
|
+
TransactionStatus: str(response.TransactionStatus)
|
|
617
|
+
};
|
|
618
|
+
setIfNotNull(result, "ErrorMessageCode", response.ErrorMessageCode);
|
|
619
|
+
setIfNotNull(result, "ErrorMessage", response.ErrorMessage);
|
|
620
|
+
setIfNotNull(result, "TransactionReference", response.TransactionReference);
|
|
621
|
+
setIfNotNull(result, "MNOTransactionReferenceId", response.MNOTransactionReferenceId);
|
|
622
|
+
setIfNotNull(result, "IssuedReceiptNumber", response.IssuedReceiptNumber);
|
|
623
|
+
return result;
|
|
624
|
+
}
|
|
625
|
+
/**
|
|
626
|
+
* Send airtime from your Yo! Payments account to another Yo! Payments user account.
|
|
627
|
+
* @param currencyCode e.g. "UGX-MTNAT", "UGX-WTLAT", "UGX-OULAT", "UGX-AIRAT"
|
|
628
|
+
* @param amount the amount of airtime to be sent to the beneficiary Yo! Payments user
|
|
629
|
+
* @param beneficiaryAccount the beneficiary Yo! Payments account number
|
|
630
|
+
* @param beneficiaryEmail the beneficiary email address
|
|
631
|
+
* @param narrative textual narrative about the transfer
|
|
632
|
+
*/
|
|
633
|
+
async acSendAirtimeInternal(currencyCode, amount, beneficiaryAccount, beneficiaryEmail, narrative) {
|
|
634
|
+
const xml = this.requestXml(
|
|
635
|
+
this.authXml() + el("Method", "acsendairtimeinternal") + el("CurrencyCode", currencyCode) + el("Amount", amount) + el("BeneficiaryAccount", beneficiaryAccount) + el("BeneficiaryEmail", beneficiaryEmail) + el("Narrative", narrative) + opt("InternalReference", this.internalReference) + opt("ExternalReference", this.externalReference)
|
|
636
|
+
);
|
|
637
|
+
const response = asRecord((await this.parseResponse(xml)).Response);
|
|
638
|
+
const result = {
|
|
639
|
+
Status: str(response.Status),
|
|
640
|
+
StatusCode: str(response.StatusCode),
|
|
641
|
+
StatusMessage: str(response.StatusMessage),
|
|
642
|
+
TransactionStatus: str(response.TransactionStatus)
|
|
643
|
+
};
|
|
644
|
+
setIfNotNull(result, "ErrorMessageCode", response.ErrorMessageCode);
|
|
645
|
+
setIfNotNull(result, "ErrorMessage", response.ErrorMessage);
|
|
646
|
+
setIfNotNull(result, "TransactionReference", response.TransactionReference);
|
|
647
|
+
setIfNotNull(result, "MNOTransactionReferenceId", response.MNOTransactionReferenceId);
|
|
648
|
+
setIfNotNull(result, "IssuedReceiptNumber", response.IssuedReceiptNumber);
|
|
649
|
+
return result;
|
|
650
|
+
}
|
|
651
|
+
/**
|
|
652
|
+
* Withdraw funds from your Yo! Payments Account to a mobile money user.
|
|
653
|
+
* Handle with care: if compromised, it can lead to withdrawal of funds from your account.
|
|
654
|
+
* Requires permission granted by the issuance of an API Access Letter.
|
|
655
|
+
* @param msisdn the mobile money phone number in the format 256772123456
|
|
656
|
+
* @param amount the amount to withdraw from your account (fractions supported)
|
|
657
|
+
* @param narrative the reason for withdrawal of funds from your account
|
|
658
|
+
*/
|
|
659
|
+
async acWithdrawFunds(msisdn, amount, narrative) {
|
|
660
|
+
const xml = this.requestXml(
|
|
661
|
+
this.authXml() + el("Method", "acwithdrawfunds") + el("NonBlocking", this.nonBlocking) + el("Account", msisdn) + el("Amount", amount) + el("Narrative", narrative) + opt("ExternalReference", this.externalReference) + opt("InternalReference", this.internalReference) + opt("ProviderReferenceText", this.providerReferenceText) + opt("TransactionLimitAccountIdentifier", this.transactionLimitAccountIdentifier) + opt("PublicKeyAuthenticationNonce", this.publicKeyAuthenticationNonce) + opt("PublicKeyAuthenticationSignatureBase64", this.publicKeyAuthenticationSignatureBase64)
|
|
662
|
+
);
|
|
663
|
+
const response = asRecord((await this.parseResponse(xml)).Response);
|
|
664
|
+
const result = {
|
|
665
|
+
Status: str(response.Status),
|
|
666
|
+
StatusCode: str(response.StatusCode),
|
|
667
|
+
StatusMessage: str(response.StatusMessage),
|
|
668
|
+
TransactionStatus: str(response.TransactionStatus)
|
|
669
|
+
};
|
|
670
|
+
setIfNonEmpty(result, "ErrorMessageCode", str(response.ErrorMessageCode));
|
|
671
|
+
setIfNonEmpty(result, "ErrorMessage", str(response.ErrorMessage));
|
|
672
|
+
setIfNonEmpty(result, "TransactionReference", str(response.TransactionReference));
|
|
673
|
+
setIfNonEmpty(result, "MNOTransactionReferenceId", str(response.MNOTransactionReferenceId));
|
|
674
|
+
setIfNonEmpty(result, "IssuedReceiptNumber", str(response.IssuedReceiptNumber));
|
|
675
|
+
return result;
|
|
676
|
+
}
|
|
677
|
+
/**
|
|
678
|
+
* Purchase airtime using your Mobile Money Credit.
|
|
679
|
+
* @param airtimeCurrencyCode e.g. "UGX-MTNAT", "UGX-AIRAT", "UGX-OULAT", "UGX-UTLAT", "UGX-SMTAT"
|
|
680
|
+
* @param amount the amount to spend (fractions supported)
|
|
681
|
+
*/
|
|
682
|
+
async acUserPurchaseAirtimestock(airtimeCurrencyCode, amount) {
|
|
683
|
+
const xml = this.requestXml(
|
|
684
|
+
this.authXml() + el("Method", "acuserpurchaseairtimestock") + el("AirtimeCurrencyCode", airtimeCurrencyCode) + el("Amount", amount) + // The PHP library sends externalReference inside a TransactionReference tag; kept for parity.
|
|
685
|
+
opt("TransactionReference", this.externalReference)
|
|
686
|
+
);
|
|
687
|
+
const response = asRecord((await this.parseResponse(xml)).Response);
|
|
688
|
+
const result = {
|
|
689
|
+
Status: str(response.Status),
|
|
690
|
+
StatusCode: str(response.StatusCode)
|
|
691
|
+
};
|
|
692
|
+
setIfNonEmpty(result, "StatusMessage", str(response.StatusMessage));
|
|
693
|
+
setIfNonEmpty(result, "TransactionReference", str(response.TransactionReference));
|
|
694
|
+
setIfNonEmpty(result, "TotalCurrencyDebited", str(response.TotalCurrencyDebited));
|
|
695
|
+
setIfNonEmpty(result, "CommissionAmount", str(response.CommissionAmount));
|
|
696
|
+
setIfNonEmpty(result, "ErrorMessageCode", str(response.ErrorMessageCode));
|
|
697
|
+
setIfNonEmpty(result, "ErrorMessage", str(response.ErrorMessage));
|
|
698
|
+
return result;
|
|
699
|
+
}
|
|
700
|
+
/**
|
|
701
|
+
* Obtain the name of a phone number before paying out funds.
|
|
702
|
+
* Only available for MTN Uganda and Airtel Uganda networks; requires permission
|
|
703
|
+
* from support@yo.co.ug.
|
|
704
|
+
* @param msisdn the phone number in the format 2567XXXXXXXXXX
|
|
705
|
+
*/
|
|
706
|
+
async acGetMsisdnKycInfo(msisdn) {
|
|
707
|
+
const xml = this.requestXml(this.authXml() + el("Method", "acgetmsisdnkycinfo") + el("Msisdn", msisdn));
|
|
708
|
+
const response = asRecord((await this.parseResponse(xml)).Response);
|
|
709
|
+
const result = {
|
|
710
|
+
Status: str(response.Status),
|
|
711
|
+
StatusCode: str(response.StatusCode)
|
|
712
|
+
};
|
|
713
|
+
setIfNonEmpty(result, "StatusMessage", str(response.StatusMessage));
|
|
714
|
+
const names = asRecord(asRecord(asRecord(response.AccountInformation).PersonalInformation).Names);
|
|
715
|
+
setIfNonEmpty(result, "FirstName", str(names.FirstName));
|
|
716
|
+
setIfNonEmpty(result, "MiddleName", str(names.MiddleName));
|
|
717
|
+
setIfNonEmpty(result, "Surname", str(names.Surname));
|
|
718
|
+
return result;
|
|
719
|
+
}
|
|
720
|
+
/**
|
|
721
|
+
* Decode and verify a successful payment notification (IPN) POSTed to your
|
|
722
|
+
* Instant Notification URL. Pass the parsed form body of the request.
|
|
723
|
+
*/
|
|
724
|
+
receivePaymentNotification(body) {
|
|
725
|
+
return {
|
|
726
|
+
is_verified: this.verifyPaymentNotification(body),
|
|
727
|
+
date_time: body.date_time ?? "",
|
|
728
|
+
amount: body.amount ?? "",
|
|
729
|
+
narrative: body.narrative ?? "",
|
|
730
|
+
network_ref: body.network_ref ?? "",
|
|
731
|
+
external_ref: body.external_ref ?? "",
|
|
732
|
+
msisdn: body.msisdn ?? ""
|
|
733
|
+
};
|
|
734
|
+
}
|
|
735
|
+
/**
|
|
736
|
+
* Decode and verify a failed payment notification POSTed to your
|
|
737
|
+
* Failure Notification URL. Pass the parsed form body of the request.
|
|
738
|
+
*/
|
|
739
|
+
receivePaymentFailureNotification(body) {
|
|
740
|
+
return {
|
|
741
|
+
is_verified: this.verifyPaymentFailureNotification(body),
|
|
742
|
+
failed_transaction_reference: body.failed_transaction_reference ?? "",
|
|
743
|
+
transaction_init_date: body.transaction_init_date ?? ""
|
|
744
|
+
};
|
|
745
|
+
}
|
|
746
|
+
/**
|
|
747
|
+
* Calculate the Public Key Authentication Signature required by some payout requests.
|
|
748
|
+
* Sets publicKeyAuthenticationSignatureBase64 on success.
|
|
749
|
+
* @param msisdn the account the funds will be pushed to
|
|
750
|
+
* @param amount the transaction amount
|
|
751
|
+
* @param narrative the transaction narrative
|
|
752
|
+
*/
|
|
753
|
+
generatePublicKeyAuthenticationSignature(msisdn, amount, narrative) {
|
|
754
|
+
if (!this.publicKeyAuthenticationNonce) {
|
|
755
|
+
throw new Error("Public key authentication nonce is not set. Please set it to continue");
|
|
756
|
+
}
|
|
757
|
+
if (!this.privateKeyFileLocation && this.privateKeyContent === null) {
|
|
758
|
+
throw new Error("Private key file location cannot be NULL");
|
|
759
|
+
}
|
|
760
|
+
let privateKeyPem = this.privateKeyContent;
|
|
761
|
+
if (privateKeyPem === null) {
|
|
762
|
+
try {
|
|
763
|
+
privateKeyPem = readFileSync2(this.privateKeyFileLocation, "utf-8");
|
|
764
|
+
} catch {
|
|
765
|
+
throw new Error(
|
|
766
|
+
`Private key file could not be opened. Confirm your file location ${this.privateKeyFileLocation}`
|
|
767
|
+
);
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
let privateKey;
|
|
771
|
+
try {
|
|
772
|
+
privateKey = createPrivateKey(privateKeyPem);
|
|
773
|
+
} catch {
|
|
774
|
+
throw new Error("Private key is invalid");
|
|
775
|
+
}
|
|
776
|
+
const data = this.username + String(amount) + msisdn + narrative + (this.externalReference ?? "") + this.publicKeyAuthenticationNonce;
|
|
777
|
+
const sha1Hex = createHash("sha1").update(data).digest("hex");
|
|
778
|
+
const signature = rsaSign("sha1", Buffer.from(sha1Hex, "utf-8"), privateKey);
|
|
779
|
+
this.publicKeyAuthenticationSignatureBase64 = signature.toString("base64");
|
|
780
|
+
}
|
|
781
|
+
/** POST raw XML to the gateway and return the XML response body. */
|
|
782
|
+
async getXmlResponse(xml) {
|
|
783
|
+
return postXml(this.yoUrl, xml, {
|
|
784
|
+
timeoutMs: this.timeoutMs,
|
|
785
|
+
verifyTls: this.verifyTls,
|
|
786
|
+
maxResponseBytes: this.maxResponseBytes
|
|
787
|
+
});
|
|
788
|
+
}
|
|
789
|
+
/** Verify the RSA-SHA256 signature on a payment notification against the Yo public certificate. */
|
|
790
|
+
verifyPaymentNotification(body) {
|
|
791
|
+
const data = (body.date_time ?? "") + (body.amount ?? "") + (body.narrative ?? "") + (body.network_ref ?? "") + (body.external_ref ?? "") + (body.msisdn ?? "");
|
|
792
|
+
return this.verifySignature(data, body.signature);
|
|
793
|
+
}
|
|
794
|
+
/** Verify the RSA-SHA256 signature on a payment failure notification against the Yo public certificate. */
|
|
795
|
+
verifyPaymentFailureNotification(body) {
|
|
796
|
+
const data = (body.failed_transaction_reference ?? "") + (body.transaction_init_date ?? "");
|
|
797
|
+
return this.verifySignature(data, body.verification);
|
|
798
|
+
}
|
|
799
|
+
verifySignature(data, signatureBase64) {
|
|
800
|
+
if (!signatureBase64) return false;
|
|
801
|
+
const publicKey = loadPublicKeyCached(
|
|
802
|
+
this.publicKeyFile,
|
|
803
|
+
this.publicKeyFileIsDefault ? defaultVerificationCertificate(this.mode) : void 0
|
|
804
|
+
);
|
|
805
|
+
if (publicKey === null) return false;
|
|
806
|
+
try {
|
|
807
|
+
return rsaVerify("sha256", Buffer.from(data, "utf-8"), publicKey, Buffer.from(signatureBase64, "base64"));
|
|
808
|
+
} catch {
|
|
809
|
+
return false;
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
authXml() {
|
|
813
|
+
return el("APIUsername", this.username) + el("APIPassword", this.password);
|
|
814
|
+
}
|
|
815
|
+
requestXml(body) {
|
|
816
|
+
return `${XML_HEADER}<AutoCreate><Request>${body}</Request></AutoCreate>`;
|
|
817
|
+
}
|
|
818
|
+
/** POST the request XML to the gateway and return the parsed envelope that holds the <Response> node. */
|
|
819
|
+
async parseResponse(requestXml) {
|
|
820
|
+
return parseGatewayResponse(await this.getXmlResponse(requestXml));
|
|
821
|
+
}
|
|
822
|
+
};
|
|
823
|
+
export {
|
|
824
|
+
YoAPI,
|
|
825
|
+
YoAPIError,
|
|
826
|
+
YoAPI as default
|
|
827
|
+
};
|
|
828
|
+
//# sourceMappingURL=index.js.map
|