telebirr-nodejs 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +181 -0
- package/dist/index.cjs +489 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +104 -0
- package/dist/index.d.ts +104 -0
- package/dist/index.js +489 -0
- package/dist/index.js.map +1 -0
- package/package.json +39 -0
- package/tsconfig.json +21 -0
- package/tsup.config.ts +10 -0
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
type TelebirrMode = "simulate" | "sandbox" | "production";
|
|
2
|
+
type IntegrationOption = "C2B" | "B2B";
|
|
3
|
+
interface TelebirrConfig {
|
|
4
|
+
appId: string;
|
|
5
|
+
appSecret: string;
|
|
6
|
+
merchantAppId: string;
|
|
7
|
+
merchantCode: string;
|
|
8
|
+
privateKey: string;
|
|
9
|
+
notifyUrl: string;
|
|
10
|
+
redirectUrl: string;
|
|
11
|
+
mode: TelebirrMode;
|
|
12
|
+
http: boolean;
|
|
13
|
+
integrationOption: IntegrationOption;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
interface GenerateCheckoutUrlInput {
|
|
17
|
+
title: string;
|
|
18
|
+
amount: string;
|
|
19
|
+
merchOrderId?: string;
|
|
20
|
+
callbackInfo?: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
type TelebirrTradeStatus = "PAY_SUCCESS" | "PAY_FAILED" | "WAIT_PAY" | "ORDER_CLOSED" | "PAYING" | "ACCEPTED" | "REFUNDING" | "REFUND_SUCCESS" | "REFUND_FAILED";
|
|
24
|
+
interface QueryOrderResponse {
|
|
25
|
+
result: "SUCCESS" | "FAIL";
|
|
26
|
+
code: string;
|
|
27
|
+
msg: string;
|
|
28
|
+
sign: string;
|
|
29
|
+
sign_type: "SHA256WithRSA";
|
|
30
|
+
nonce_str: string;
|
|
31
|
+
biz_content: {
|
|
32
|
+
merch_order_id: string;
|
|
33
|
+
order_status: TelebirrTradeStatus;
|
|
34
|
+
payment_order_id: string;
|
|
35
|
+
trans_time: string;
|
|
36
|
+
trans_currency: "ETB";
|
|
37
|
+
total_amount: string;
|
|
38
|
+
trans_id: string;
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
interface RefundInput {
|
|
43
|
+
merchOrderId: string;
|
|
44
|
+
refundRequestNo: string;
|
|
45
|
+
amount: string;
|
|
46
|
+
refundReason?: string;
|
|
47
|
+
}
|
|
48
|
+
type TelebirrRefundStatus = "REFUND_SUCCESS" | "REFUNDING" | "REFUND_FAILED" | "REFUND_DUPLICATED";
|
|
49
|
+
interface RefundResponse {
|
|
50
|
+
result: "SUCCESS" | "FAIL";
|
|
51
|
+
code: string;
|
|
52
|
+
msg: string;
|
|
53
|
+
sign: string;
|
|
54
|
+
sign_type: "SHA256WithRSA";
|
|
55
|
+
nonce_str: string;
|
|
56
|
+
biz_content: {
|
|
57
|
+
/** Short code registered by a merchant with Mobile Money */
|
|
58
|
+
merch_code: string;
|
|
59
|
+
/** Order ID on the merchant side */
|
|
60
|
+
merch_order_id: string;
|
|
61
|
+
/** Original transaction order ID on the payment side */
|
|
62
|
+
trans_order_id: string;
|
|
63
|
+
/** Refund transaction order ID on the payment side */
|
|
64
|
+
refund_order_id: string;
|
|
65
|
+
/** Refund amount */
|
|
66
|
+
refund_amount: string;
|
|
67
|
+
/** Refund currency (e.g. ETB) */
|
|
68
|
+
refund_currency: string;
|
|
69
|
+
/** Refund status */
|
|
70
|
+
refund_status: TelebirrRefundStatus;
|
|
71
|
+
/**
|
|
72
|
+
* Refund success timestamp.
|
|
73
|
+
* Only present when refund_status === "REFUND_SUCCESS"
|
|
74
|
+
*/
|
|
75
|
+
refund_time?: string;
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
declare class TelebirrClient {
|
|
80
|
+
private token?;
|
|
81
|
+
private config;
|
|
82
|
+
constructor(config: TelebirrConfig);
|
|
83
|
+
getFabricToken(): Promise<any>;
|
|
84
|
+
private createCheckoutUrl;
|
|
85
|
+
preOrder(input: GenerateCheckoutUrlInput): Promise<string | void>;
|
|
86
|
+
queryOrder(input: string): Promise<QueryOrderResponse | void>;
|
|
87
|
+
refundOrder(input: RefundInput): Promise<RefundResponse | void>;
|
|
88
|
+
private static readonly TOKEN_EXPIRY_SAFETY_WINDOW_MS;
|
|
89
|
+
private isTokenExpired;
|
|
90
|
+
private parseTelebirrDate;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
interface KeyPairOptions {
|
|
94
|
+
dir?: string;
|
|
95
|
+
privateKeyName?: string;
|
|
96
|
+
publicKeyName?: string;
|
|
97
|
+
overwrite?: boolean;
|
|
98
|
+
}
|
|
99
|
+
declare function generateKeys(options?: KeyPairOptions): {
|
|
100
|
+
privateKeyPath: string;
|
|
101
|
+
publicKeyPath: string;
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
export { TelebirrClient, type TelebirrConfig, generateKeys };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
type TelebirrMode = "simulate" | "sandbox" | "production";
|
|
2
|
+
type IntegrationOption = "C2B" | "B2B";
|
|
3
|
+
interface TelebirrConfig {
|
|
4
|
+
appId: string;
|
|
5
|
+
appSecret: string;
|
|
6
|
+
merchantAppId: string;
|
|
7
|
+
merchantCode: string;
|
|
8
|
+
privateKey: string;
|
|
9
|
+
notifyUrl: string;
|
|
10
|
+
redirectUrl: string;
|
|
11
|
+
mode: TelebirrMode;
|
|
12
|
+
http: boolean;
|
|
13
|
+
integrationOption: IntegrationOption;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
interface GenerateCheckoutUrlInput {
|
|
17
|
+
title: string;
|
|
18
|
+
amount: string;
|
|
19
|
+
merchOrderId?: string;
|
|
20
|
+
callbackInfo?: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
type TelebirrTradeStatus = "PAY_SUCCESS" | "PAY_FAILED" | "WAIT_PAY" | "ORDER_CLOSED" | "PAYING" | "ACCEPTED" | "REFUNDING" | "REFUND_SUCCESS" | "REFUND_FAILED";
|
|
24
|
+
interface QueryOrderResponse {
|
|
25
|
+
result: "SUCCESS" | "FAIL";
|
|
26
|
+
code: string;
|
|
27
|
+
msg: string;
|
|
28
|
+
sign: string;
|
|
29
|
+
sign_type: "SHA256WithRSA";
|
|
30
|
+
nonce_str: string;
|
|
31
|
+
biz_content: {
|
|
32
|
+
merch_order_id: string;
|
|
33
|
+
order_status: TelebirrTradeStatus;
|
|
34
|
+
payment_order_id: string;
|
|
35
|
+
trans_time: string;
|
|
36
|
+
trans_currency: "ETB";
|
|
37
|
+
total_amount: string;
|
|
38
|
+
trans_id: string;
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
interface RefundInput {
|
|
43
|
+
merchOrderId: string;
|
|
44
|
+
refundRequestNo: string;
|
|
45
|
+
amount: string;
|
|
46
|
+
refundReason?: string;
|
|
47
|
+
}
|
|
48
|
+
type TelebirrRefundStatus = "REFUND_SUCCESS" | "REFUNDING" | "REFUND_FAILED" | "REFUND_DUPLICATED";
|
|
49
|
+
interface RefundResponse {
|
|
50
|
+
result: "SUCCESS" | "FAIL";
|
|
51
|
+
code: string;
|
|
52
|
+
msg: string;
|
|
53
|
+
sign: string;
|
|
54
|
+
sign_type: "SHA256WithRSA";
|
|
55
|
+
nonce_str: string;
|
|
56
|
+
biz_content: {
|
|
57
|
+
/** Short code registered by a merchant with Mobile Money */
|
|
58
|
+
merch_code: string;
|
|
59
|
+
/** Order ID on the merchant side */
|
|
60
|
+
merch_order_id: string;
|
|
61
|
+
/** Original transaction order ID on the payment side */
|
|
62
|
+
trans_order_id: string;
|
|
63
|
+
/** Refund transaction order ID on the payment side */
|
|
64
|
+
refund_order_id: string;
|
|
65
|
+
/** Refund amount */
|
|
66
|
+
refund_amount: string;
|
|
67
|
+
/** Refund currency (e.g. ETB) */
|
|
68
|
+
refund_currency: string;
|
|
69
|
+
/** Refund status */
|
|
70
|
+
refund_status: TelebirrRefundStatus;
|
|
71
|
+
/**
|
|
72
|
+
* Refund success timestamp.
|
|
73
|
+
* Only present when refund_status === "REFUND_SUCCESS"
|
|
74
|
+
*/
|
|
75
|
+
refund_time?: string;
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
declare class TelebirrClient {
|
|
80
|
+
private token?;
|
|
81
|
+
private config;
|
|
82
|
+
constructor(config: TelebirrConfig);
|
|
83
|
+
getFabricToken(): Promise<any>;
|
|
84
|
+
private createCheckoutUrl;
|
|
85
|
+
preOrder(input: GenerateCheckoutUrlInput): Promise<string | void>;
|
|
86
|
+
queryOrder(input: string): Promise<QueryOrderResponse | void>;
|
|
87
|
+
refundOrder(input: RefundInput): Promise<RefundResponse | void>;
|
|
88
|
+
private static readonly TOKEN_EXPIRY_SAFETY_WINDOW_MS;
|
|
89
|
+
private isTokenExpired;
|
|
90
|
+
private parseTelebirrDate;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
interface KeyPairOptions {
|
|
94
|
+
dir?: string;
|
|
95
|
+
privateKeyName?: string;
|
|
96
|
+
publicKeyName?: string;
|
|
97
|
+
overwrite?: boolean;
|
|
98
|
+
}
|
|
99
|
+
declare function generateKeys(options?: KeyPairOptions): {
|
|
100
|
+
privateKeyPath: string;
|
|
101
|
+
publicKeyPath: string;
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
export { TelebirrClient, type TelebirrConfig, generateKeys };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,489 @@
|
|
|
1
|
+
// src/utils/credentials.ts
|
|
2
|
+
import fs2 from "fs";
|
|
3
|
+
import { randomUUID, randomBytes } from "crypto";
|
|
4
|
+
import { customAlphabet } from "nanoid";
|
|
5
|
+
|
|
6
|
+
// src/utils/keys.ts
|
|
7
|
+
import { generateKeyPairSync } from "crypto";
|
|
8
|
+
import fs from "fs";
|
|
9
|
+
import path from "path";
|
|
10
|
+
function generateKeys(options = {}) {
|
|
11
|
+
const {
|
|
12
|
+
dir = process.cwd(),
|
|
13
|
+
privateKeyName = "telebirr_private.pem",
|
|
14
|
+
publicKeyName = "telebirr_public.pem",
|
|
15
|
+
overwrite = false
|
|
16
|
+
} = options;
|
|
17
|
+
const privateKeyPath = path.join(dir, privateKeyName);
|
|
18
|
+
const publicKeyPath = path.join(dir, publicKeyName);
|
|
19
|
+
const exists = fs.existsSync(privateKeyPath) && fs.existsSync(publicKeyPath);
|
|
20
|
+
if (exists && !overwrite) {
|
|
21
|
+
return { privateKeyPath, publicKeyPath };
|
|
22
|
+
}
|
|
23
|
+
const { privateKey, publicKey } = generateKeyPairSync("rsa", {
|
|
24
|
+
modulusLength: 2048,
|
|
25
|
+
publicKeyEncoding: {
|
|
26
|
+
type: "pkcs1",
|
|
27
|
+
format: "pem"
|
|
28
|
+
},
|
|
29
|
+
privateKeyEncoding: {
|
|
30
|
+
type: "pkcs1",
|
|
31
|
+
format: "pem"
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
fs.writeFileSync(privateKeyPath, privateKey, { mode: 384 });
|
|
35
|
+
fs.writeFileSync(publicKeyPath, publicKey);
|
|
36
|
+
return { privateKeyPath, publicKeyPath };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// src/utils/credentials.ts
|
|
40
|
+
var numeric16 = customAlphabet("0123456789", 16);
|
|
41
|
+
var numeric6 = customAlphabet("0123456789", 6);
|
|
42
|
+
function generateCredentials() {
|
|
43
|
+
const { privateKeyPath } = generateKeys();
|
|
44
|
+
const merchantPrivateKey = fs2.readFileSync(privateKeyPath, "utf8");
|
|
45
|
+
return {
|
|
46
|
+
fabricAppId: randomUUID(),
|
|
47
|
+
fabricAppSecret: randomBytes(16).toString("hex"),
|
|
48
|
+
merchantAppId: numeric16(),
|
|
49
|
+
merchantCode: numeric6(),
|
|
50
|
+
merchantPrivateKey
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// src/services/requestToken.ts
|
|
55
|
+
import http from "http";
|
|
56
|
+
import https from "https";
|
|
57
|
+
|
|
58
|
+
// src/constants/urls.ts
|
|
59
|
+
var TELEBIRR_URLS = {
|
|
60
|
+
sandbox: {
|
|
61
|
+
apiBase: "https://developerportal.ethiotelebirr.et:38443/apiaccess/payment/gateway",
|
|
62
|
+
webBase: "https://developerportal.ethiotelebirr.et:38443/payment/web/paygate?"
|
|
63
|
+
},
|
|
64
|
+
production: {
|
|
65
|
+
apiBase: "https://telebirrappcube.ethiomobilemoney.et:38443/apiaccess/payment/gateway",
|
|
66
|
+
webBase: "https://telebirrappcube.ethiomobilemoney.et:38443/payment/web/paygate?"
|
|
67
|
+
},
|
|
68
|
+
simulate: {
|
|
69
|
+
apiBase: "http://localhost:3000",
|
|
70
|
+
webBase: "http://localhost:3000/web/?"
|
|
71
|
+
}
|
|
72
|
+
// simulate: {
|
|
73
|
+
// apiBase: "https://telebirr-node-simulator.onrender.com",
|
|
74
|
+
// webBase: "https://telebirr-node-simulator.onrender.com/web/?",
|
|
75
|
+
// },
|
|
76
|
+
};
|
|
77
|
+
var CHECKOUT_OTHER_PARAMS = "&version=1.0&trade_type=Checkout";
|
|
78
|
+
|
|
79
|
+
// src/services/requestToken.ts
|
|
80
|
+
function requestToken(config) {
|
|
81
|
+
const isHttps = TELEBIRR_URLS[config.mode].apiBase.startsWith("https://");
|
|
82
|
+
const client = isHttps ? https : http;
|
|
83
|
+
return new Promise((resolve, reject) => {
|
|
84
|
+
const req = client.request(
|
|
85
|
+
` ${TELEBIRR_URLS[config.mode].apiBase}/payment/v1/token`,
|
|
86
|
+
{
|
|
87
|
+
method: "POST",
|
|
88
|
+
headers: {
|
|
89
|
+
"Content-Type": "application/json",
|
|
90
|
+
"X-APP-Key": config.appId
|
|
91
|
+
},
|
|
92
|
+
...isHttps && { rejectUnauthorized: false }
|
|
93
|
+
},
|
|
94
|
+
(res) => {
|
|
95
|
+
let body = "";
|
|
96
|
+
res.on("data", (chunk) => body += chunk);
|
|
97
|
+
res.on("end", () => resolve(body));
|
|
98
|
+
}
|
|
99
|
+
);
|
|
100
|
+
req.on("error", reject);
|
|
101
|
+
req.write(JSON.stringify({ appSecret: config.appSecret }));
|
|
102
|
+
req.end();
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// src/services/requestCreateOrder.ts
|
|
107
|
+
import http2 from "http";
|
|
108
|
+
import https2 from "https";
|
|
109
|
+
|
|
110
|
+
// src/utils/nonce.ts
|
|
111
|
+
import { randomBytes as randomBytes2 } from "crypto";
|
|
112
|
+
function createNonceStr() {
|
|
113
|
+
const bytes = Math.ceil(16);
|
|
114
|
+
return randomBytes2(bytes).toString("hex").slice(0, 32);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// src/utils/timestamp.ts
|
|
118
|
+
function createTimestamp() {
|
|
119
|
+
return Math.floor(Date.now() / 1e3).toString();
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// src/utils/signature.ts
|
|
123
|
+
import crypto from "crypto";
|
|
124
|
+
function flattenParams(params) {
|
|
125
|
+
const flat = {};
|
|
126
|
+
for (const key in params) {
|
|
127
|
+
const value = params[key];
|
|
128
|
+
if (value === void 0 || value === null || value === "" || key === "sign" || key === "signType" || key === "sign_type") {
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
if (key === "biz_content" && typeof value === "object") {
|
|
132
|
+
for (const bizKey in value) {
|
|
133
|
+
const bizValue = value[bizKey];
|
|
134
|
+
if (bizValue !== void 0 && bizValue !== null && bizValue !== "") {
|
|
135
|
+
flat[bizKey] = String(bizValue);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
} else {
|
|
139
|
+
flat[key] = String(value);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
return flat;
|
|
143
|
+
}
|
|
144
|
+
function buildSignString(params) {
|
|
145
|
+
const flat = flattenParams(params);
|
|
146
|
+
return Object.keys(flat).sort().map((key) => `${key}=${flat[key]}`).join("&");
|
|
147
|
+
}
|
|
148
|
+
function signRequest(data, privateKey) {
|
|
149
|
+
const signString = buildSignString(data);
|
|
150
|
+
return crypto.createSign("RSA-SHA256").update(signString, "utf8").sign(privateKey, "base64");
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// src/services/requestCreateOrder.ts
|
|
154
|
+
function requestCreateOrder(fabricToken, input, config) {
|
|
155
|
+
const reqBody = {
|
|
156
|
+
timestamp: createTimestamp(),
|
|
157
|
+
nonce_str: createNonceStr(),
|
|
158
|
+
method: "payment.preorder",
|
|
159
|
+
version: "1.0",
|
|
160
|
+
biz_content: {
|
|
161
|
+
appid: config.merchantAppId,
|
|
162
|
+
merch_code: config.merchantCode,
|
|
163
|
+
merch_order_id: input.merchOrderId,
|
|
164
|
+
notify_url: config.notifyUrl,
|
|
165
|
+
redirect_url: config.redirectUrl,
|
|
166
|
+
trade_type: "Checkout",
|
|
167
|
+
title: input.title,
|
|
168
|
+
total_amount: input.amount,
|
|
169
|
+
trans_currency: "ETB",
|
|
170
|
+
timeout_express: "120m",
|
|
171
|
+
business_type: "BuyGoods",
|
|
172
|
+
payee_type: "3000",
|
|
173
|
+
payee_identifier: config.merchantCode,
|
|
174
|
+
payee_identifier_type: "04",
|
|
175
|
+
callback_info: "From web"
|
|
176
|
+
}
|
|
177
|
+
};
|
|
178
|
+
reqBody.sign = signRequest(reqBody, config.privateKey);
|
|
179
|
+
reqBody.sign_type = "SHA256WithRSA";
|
|
180
|
+
const payload = JSON.stringify(reqBody);
|
|
181
|
+
const baseUrl = TELEBIRR_URLS[config.mode].apiBase;
|
|
182
|
+
const isHttps = baseUrl.startsWith("https://");
|
|
183
|
+
const client = isHttps ? https2 : http2;
|
|
184
|
+
return new Promise((resolve, reject) => {
|
|
185
|
+
const req = client.request(
|
|
186
|
+
`${baseUrl}/payment/v1/merchant/preOrder`,
|
|
187
|
+
{
|
|
188
|
+
method: "POST",
|
|
189
|
+
headers: {
|
|
190
|
+
"Content-Type": "application/json",
|
|
191
|
+
"Content-Length": Buffer.byteLength(payload),
|
|
192
|
+
"X-APP-Key": config.appId,
|
|
193
|
+
Authorization: fabricToken
|
|
194
|
+
},
|
|
195
|
+
...isHttps && { rejectUnauthorized: false }
|
|
196
|
+
},
|
|
197
|
+
(res) => {
|
|
198
|
+
let raw = "";
|
|
199
|
+
res.on("data", (chunk) => {
|
|
200
|
+
raw += chunk;
|
|
201
|
+
});
|
|
202
|
+
res.on("end", () => {
|
|
203
|
+
const status = res.statusCode || 0;
|
|
204
|
+
let parsed = raw;
|
|
205
|
+
try {
|
|
206
|
+
parsed = JSON.parse(raw);
|
|
207
|
+
} catch {
|
|
208
|
+
}
|
|
209
|
+
if (status < 200 || status >= 300) {
|
|
210
|
+
return reject({
|
|
211
|
+
message: "Telebirr preorder request failed",
|
|
212
|
+
status,
|
|
213
|
+
data: parsed,
|
|
214
|
+
headers: res.headers
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
resolve({
|
|
218
|
+
data: parsed,
|
|
219
|
+
status,
|
|
220
|
+
headers: res.headers
|
|
221
|
+
});
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
);
|
|
225
|
+
req.on("error", (err) => {
|
|
226
|
+
reject({
|
|
227
|
+
message: "Telebirr preorder network error",
|
|
228
|
+
cause: err,
|
|
229
|
+
code: err.code
|
|
230
|
+
});
|
|
231
|
+
});
|
|
232
|
+
req.write(payload);
|
|
233
|
+
req.end();
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// src/services/requestRefundOrder.ts
|
|
238
|
+
import http3 from "http";
|
|
239
|
+
import https3 from "https";
|
|
240
|
+
function requestRefund(fabricToken, input, config) {
|
|
241
|
+
const reqBody = {
|
|
242
|
+
timestamp: createTimestamp(),
|
|
243
|
+
nonce_str: createNonceStr(),
|
|
244
|
+
method: "payment.refund",
|
|
245
|
+
version: "1.0",
|
|
246
|
+
biz_content: {
|
|
247
|
+
appid: config.merchantAppId,
|
|
248
|
+
merch_code: config.merchantCode,
|
|
249
|
+
merch_order_id: input.merchOrderId,
|
|
250
|
+
trans_currency: "ETB",
|
|
251
|
+
actual_amount: input.amount,
|
|
252
|
+
refund_request_no: input.refundRequestNo,
|
|
253
|
+
refund_reason: input.refundReason
|
|
254
|
+
}
|
|
255
|
+
};
|
|
256
|
+
reqBody.sign = signRequest(reqBody, config.privateKey);
|
|
257
|
+
reqBody.sign_type = "SHA256WithRSA";
|
|
258
|
+
const payload = JSON.stringify(reqBody);
|
|
259
|
+
const baseUrl = TELEBIRR_URLS[config.mode].apiBase;
|
|
260
|
+
const isHttps = baseUrl.startsWith("https://");
|
|
261
|
+
const client = isHttps ? https3 : http3;
|
|
262
|
+
return new Promise((resolve, reject) => {
|
|
263
|
+
const req = client.request(
|
|
264
|
+
`${baseUrl}/payment/v1/merchant/refund`,
|
|
265
|
+
{
|
|
266
|
+
method: "POST",
|
|
267
|
+
headers: {
|
|
268
|
+
"Content-Type": "application/json",
|
|
269
|
+
"Content-Length": Buffer.byteLength(payload),
|
|
270
|
+
"X-APP-Key": config.appId,
|
|
271
|
+
Authorization: fabricToken
|
|
272
|
+
},
|
|
273
|
+
...isHttps && { rejectUnauthorized: false }
|
|
274
|
+
},
|
|
275
|
+
(res) => {
|
|
276
|
+
let raw = "";
|
|
277
|
+
res.on("data", (chunk) => {
|
|
278
|
+
raw += chunk;
|
|
279
|
+
});
|
|
280
|
+
res.on("end", () => {
|
|
281
|
+
const status = res.statusCode || 0;
|
|
282
|
+
let parsed = raw;
|
|
283
|
+
try {
|
|
284
|
+
parsed = JSON.parse(raw);
|
|
285
|
+
} catch {
|
|
286
|
+
}
|
|
287
|
+
if (status < 200 || status >= 300) {
|
|
288
|
+
return reject({
|
|
289
|
+
message: "Telebirr refund request failed",
|
|
290
|
+
status,
|
|
291
|
+
data: parsed,
|
|
292
|
+
headers: res.headers
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
resolve({
|
|
296
|
+
data: parsed,
|
|
297
|
+
status,
|
|
298
|
+
headers: res.headers
|
|
299
|
+
});
|
|
300
|
+
});
|
|
301
|
+
}
|
|
302
|
+
);
|
|
303
|
+
req.on("error", (err) => {
|
|
304
|
+
reject({
|
|
305
|
+
message: "Telebirr refund network error",
|
|
306
|
+
cause: err,
|
|
307
|
+
code: err.code
|
|
308
|
+
});
|
|
309
|
+
});
|
|
310
|
+
req.write(payload);
|
|
311
|
+
req.end();
|
|
312
|
+
});
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// src/services/requestQueryOrder.ts
|
|
316
|
+
import http4 from "http";
|
|
317
|
+
import https4 from "https";
|
|
318
|
+
function requestQueryOrder(fabricToken, merchOrderId, config) {
|
|
319
|
+
const reqBody = {
|
|
320
|
+
timestamp: createTimestamp(),
|
|
321
|
+
nonce_str: createNonceStr(),
|
|
322
|
+
method: "payment.queryorder",
|
|
323
|
+
version: "1.0",
|
|
324
|
+
biz_content: {
|
|
325
|
+
appid: config.merchantAppId,
|
|
326
|
+
merch_code: config.merchantCode,
|
|
327
|
+
merch_order_id: merchOrderId
|
|
328
|
+
}
|
|
329
|
+
};
|
|
330
|
+
reqBody.sign = signRequest(reqBody, config.privateKey);
|
|
331
|
+
reqBody.sign_type = "SHA256WithRSA";
|
|
332
|
+
const payload = JSON.stringify(reqBody);
|
|
333
|
+
const baseUrl = TELEBIRR_URLS[config.mode].apiBase;
|
|
334
|
+
const isHttps = baseUrl.startsWith("https://");
|
|
335
|
+
const client = isHttps ? https4 : http4;
|
|
336
|
+
return new Promise((resolve, reject) => {
|
|
337
|
+
const req = client.request(
|
|
338
|
+
`${baseUrl}/payment/v1/merchant/queryOrder`,
|
|
339
|
+
{
|
|
340
|
+
method: "POST",
|
|
341
|
+
headers: {
|
|
342
|
+
"Content-Type": "application/json",
|
|
343
|
+
"Content-Length": Buffer.byteLength(payload),
|
|
344
|
+
"X-APP-Key": config.appId,
|
|
345
|
+
Authorization: fabricToken
|
|
346
|
+
},
|
|
347
|
+
...isHttps && { rejectUnauthorized: false }
|
|
348
|
+
},
|
|
349
|
+
(res) => {
|
|
350
|
+
let raw = "";
|
|
351
|
+
res.on("data", (chunk) => {
|
|
352
|
+
raw += chunk;
|
|
353
|
+
});
|
|
354
|
+
res.on("end", () => {
|
|
355
|
+
const status = res.statusCode || 0;
|
|
356
|
+
let parsed = raw;
|
|
357
|
+
try {
|
|
358
|
+
parsed = JSON.parse(raw);
|
|
359
|
+
} catch {
|
|
360
|
+
}
|
|
361
|
+
if (status < 200 || status >= 300) {
|
|
362
|
+
return reject({
|
|
363
|
+
message: "Telebirr queryOrder request failed",
|
|
364
|
+
status,
|
|
365
|
+
data: parsed,
|
|
366
|
+
headers: res.headers
|
|
367
|
+
});
|
|
368
|
+
}
|
|
369
|
+
resolve({
|
|
370
|
+
data: parsed,
|
|
371
|
+
status,
|
|
372
|
+
headers: res.headers
|
|
373
|
+
});
|
|
374
|
+
});
|
|
375
|
+
}
|
|
376
|
+
);
|
|
377
|
+
req.on("error", (err) => {
|
|
378
|
+
reject({
|
|
379
|
+
message: "Telebirr queryOrder network error",
|
|
380
|
+
cause: err,
|
|
381
|
+
code: err.code
|
|
382
|
+
});
|
|
383
|
+
});
|
|
384
|
+
req.write(payload);
|
|
385
|
+
req.end();
|
|
386
|
+
});
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
// src/client.ts
|
|
390
|
+
var TelebirrClient = class _TelebirrClient {
|
|
391
|
+
token;
|
|
392
|
+
config;
|
|
393
|
+
constructor(config) {
|
|
394
|
+
if (config.mode === "simulate") {
|
|
395
|
+
const creds = generateCredentials();
|
|
396
|
+
this.config = {
|
|
397
|
+
...config,
|
|
398
|
+
appId: config.appId ?? creds.fabricAppId,
|
|
399
|
+
appSecret: config.appSecret ?? creds.fabricAppSecret,
|
|
400
|
+
merchantAppId: config.merchantAppId ?? creds.merchantAppId,
|
|
401
|
+
merchantCode: config.merchantCode ?? creds.merchantCode,
|
|
402
|
+
privateKey: creds.merchantPrivateKey
|
|
403
|
+
};
|
|
404
|
+
} else {
|
|
405
|
+
this.config = config;
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
async getFabricToken() {
|
|
409
|
+
if (this.token && !this.isTokenExpired(this.token)) {
|
|
410
|
+
return this.token;
|
|
411
|
+
}
|
|
412
|
+
let token = await requestToken(this.config);
|
|
413
|
+
token = JSON.parse(token);
|
|
414
|
+
if (!token) return;
|
|
415
|
+
this.token = token;
|
|
416
|
+
return token;
|
|
417
|
+
}
|
|
418
|
+
createCheckoutUrl(prepayId) {
|
|
419
|
+
const map = {
|
|
420
|
+
appid: this.config.merchantAppId,
|
|
421
|
+
merch_code: this.config.merchantCode,
|
|
422
|
+
nonce_str: createNonceStr(),
|
|
423
|
+
prepay_id: prepayId,
|
|
424
|
+
timestamp: createTimestamp()
|
|
425
|
+
};
|
|
426
|
+
const sign = signRequest(map, this.config.privateKey);
|
|
427
|
+
const rawRequest = [
|
|
428
|
+
`appid=${map.appid}`,
|
|
429
|
+
`merch_code=${map.merch_code}`,
|
|
430
|
+
`nonce_str=${map.nonce_str}`,
|
|
431
|
+
`prepay_id=${map.prepay_id}`,
|
|
432
|
+
`timestamp=${map.timestamp}`,
|
|
433
|
+
`sign=${sign}`,
|
|
434
|
+
`sign_type=SHA256WithRSA`
|
|
435
|
+
].join("&");
|
|
436
|
+
const webBase = TELEBIRR_URLS[this.config.mode].webBase;
|
|
437
|
+
return webBase + rawRequest + CHECKOUT_OTHER_PARAMS;
|
|
438
|
+
}
|
|
439
|
+
async preOrder(input) {
|
|
440
|
+
const token = await this.getFabricToken();
|
|
441
|
+
if (!token) return;
|
|
442
|
+
let response = await requestCreateOrder(
|
|
443
|
+
token.token,
|
|
444
|
+
input,
|
|
445
|
+
this.config
|
|
446
|
+
);
|
|
447
|
+
if (!response) return;
|
|
448
|
+
return this.createCheckoutUrl(response.data.biz_content.prepay_id);
|
|
449
|
+
}
|
|
450
|
+
async queryOrder(input) {
|
|
451
|
+
let token = await this.getFabricToken();
|
|
452
|
+
if (!token) return;
|
|
453
|
+
const response = await requestQueryOrder(
|
|
454
|
+
token.token,
|
|
455
|
+
input,
|
|
456
|
+
this.config
|
|
457
|
+
);
|
|
458
|
+
return response.data;
|
|
459
|
+
}
|
|
460
|
+
async refundOrder(input) {
|
|
461
|
+
const token = await this.getFabricToken();
|
|
462
|
+
if (!token) return;
|
|
463
|
+
const response = await requestRefund(token.token, input, this.config);
|
|
464
|
+
return response.data;
|
|
465
|
+
}
|
|
466
|
+
static TOKEN_EXPIRY_SAFETY_WINDOW_MS = 5 * 60 * 1e3;
|
|
467
|
+
isTokenExpired(token) {
|
|
468
|
+
if (!token) {
|
|
469
|
+
return true;
|
|
470
|
+
}
|
|
471
|
+
const now = Date.now();
|
|
472
|
+
const expiry = this.parseTelebirrDate(token.expirationDate).getTime();
|
|
473
|
+
return now >= expiry - _TelebirrClient.TOKEN_EXPIRY_SAFETY_WINDOW_MS;
|
|
474
|
+
}
|
|
475
|
+
parseTelebirrDate(value) {
|
|
476
|
+
const year = Number(value.slice(0, 4));
|
|
477
|
+
const month = Number(value.slice(4, 6)) - 1;
|
|
478
|
+
const day = Number(value.slice(6, 8));
|
|
479
|
+
const hour = Number(value.slice(8, 10));
|
|
480
|
+
const minute = Number(value.slice(10, 12));
|
|
481
|
+
const second = Number(value.slice(12, 14));
|
|
482
|
+
return new Date(Date.UTC(year, month, day, hour, minute, second));
|
|
483
|
+
}
|
|
484
|
+
};
|
|
485
|
+
export {
|
|
486
|
+
TelebirrClient,
|
|
487
|
+
generateKeys
|
|
488
|
+
};
|
|
489
|
+
//# sourceMappingURL=index.js.map
|