@crediblemark/buayar 0.5.1 → 0.6.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/dist/index.d.mts +137 -1
- package/dist/index.d.ts +137 -1
- package/dist/index.js +514 -25
- package/dist/index.mjs +488 -8
- package/package.json +1 -1
package/dist/index.mjs
CHANGED
|
@@ -2413,12 +2413,221 @@ Digest:${digest}`;
|
|
|
2413
2413
|
return safeCompare(incomingSignature, expectedSignature);
|
|
2414
2414
|
}
|
|
2415
2415
|
|
|
2416
|
+
// src/providers/doku/snap.ts
|
|
2417
|
+
import crypto3 from "crypto";
|
|
2418
|
+
function snapTimestamp(date = /* @__PURE__ */ new Date()) {
|
|
2419
|
+
const wib = new Date(date.getTime() + 7 * 60 * 60 * 1e3);
|
|
2420
|
+
return wib.toISOString().slice(0, 19) + "+07:00";
|
|
2421
|
+
}
|
|
2422
|
+
function snapUtcTimestamp(date = /* @__PURE__ */ new Date()) {
|
|
2423
|
+
return date.toISOString().slice(0, 19) + "Z";
|
|
2424
|
+
}
|
|
2425
|
+
function minifyJson(obj) {
|
|
2426
|
+
return JSON.stringify(obj);
|
|
2427
|
+
}
|
|
2428
|
+
function sha256Hex(body) {
|
|
2429
|
+
const raw = typeof body === "string" ? body : minifyJson(body);
|
|
2430
|
+
return crypto3.createHash("sha256").update(raw).digest("hex").toLowerCase();
|
|
2431
|
+
}
|
|
2432
|
+
function generateSnapSymmetricSignature(clientSecret, method, endpointUrl, accessToken, body, timestamp) {
|
|
2433
|
+
const hash = sha256Hex(body);
|
|
2434
|
+
const stringToSign = `${method}:${endpointUrl}:${accessToken}:${hash}:${timestamp}`;
|
|
2435
|
+
return crypto3.createHmac("sha512", clientSecret).update(stringToSign).digest("base64");
|
|
2436
|
+
}
|
|
2437
|
+
function generateSnapAsymmetricSignature(privateKey, clientId, timestamp) {
|
|
2438
|
+
const stringToSign = `${clientId}|${timestamp}`;
|
|
2439
|
+
const key = crypto3.createPrivateKey(normalizePem(privateKey));
|
|
2440
|
+
return crypto3.sign("RSA-SHA256", Buffer.from(stringToSign, "utf8"), key).toString("base64");
|
|
2441
|
+
}
|
|
2442
|
+
function normalizePem(key) {
|
|
2443
|
+
const trimmed = key.trim();
|
|
2444
|
+
if (trimmed.includes("-----BEGIN")) {
|
|
2445
|
+
return trimmed;
|
|
2446
|
+
}
|
|
2447
|
+
return [
|
|
2448
|
+
"-----BEGIN PRIVATE KEY-----",
|
|
2449
|
+
trimmed.replace(/\s+/g, "").match(/.{1,64}/g)?.join("\n") || trimmed,
|
|
2450
|
+
"-----END PRIVATE KEY-----"
|
|
2451
|
+
].join("\n");
|
|
2452
|
+
}
|
|
2453
|
+
function snapExternalId(prefix = "") {
|
|
2454
|
+
return `${prefix}${Date.now()}${Math.floor(Math.random() * 1e3)}`;
|
|
2455
|
+
}
|
|
2456
|
+
function verifySnapWebhookSignature(headers, body, clientSecret, endpointUrl = "/api/payment/webhook") {
|
|
2457
|
+
if (!clientSecret) return false;
|
|
2458
|
+
const incoming = headers["x-signature"] || headers["X-SIGNATURE"] || headers["signature"] || headers["Signature"] || "";
|
|
2459
|
+
if (!incoming) return false;
|
|
2460
|
+
const timestamp = headers["x-timestamp"] || headers["X-TIMESTAMP"] || headers["timestamp"] || headers["Timestamp"] || snapTimestamp();
|
|
2461
|
+
const computed = generateSnapSymmetricSignature(
|
|
2462
|
+
clientSecret,
|
|
2463
|
+
"POST",
|
|
2464
|
+
endpointUrl,
|
|
2465
|
+
"",
|
|
2466
|
+
body,
|
|
2467
|
+
timestamp
|
|
2468
|
+
);
|
|
2469
|
+
return safeCompare(incoming, computed);
|
|
2470
|
+
}
|
|
2471
|
+
|
|
2472
|
+
// src/clients/snap.ts
|
|
2473
|
+
var SnapClient = class {
|
|
2474
|
+
options;
|
|
2475
|
+
token = null;
|
|
2476
|
+
tokenExpiry = 0;
|
|
2477
|
+
constructor(options) {
|
|
2478
|
+
this.options = {
|
|
2479
|
+
clientId: options.clientId || options.merchantCode || "",
|
|
2480
|
+
clientSecret: options.clientSecret || options.apiKey || options.serverKey || options.secretKey || "",
|
|
2481
|
+
privateKey: options.privateKey,
|
|
2482
|
+
sandbox: !!options.sandbox,
|
|
2483
|
+
merchantId: options.merchantId,
|
|
2484
|
+
terminalId: options.terminalId,
|
|
2485
|
+
partnerServiceId: options.partnerServiceId,
|
|
2486
|
+
channelId: options.channelId || "H2H"
|
|
2487
|
+
};
|
|
2488
|
+
}
|
|
2489
|
+
get baseUrl() {
|
|
2490
|
+
return this.options.sandbox ? "https://api-sandbox.doku.com" : "https://api.doku.com";
|
|
2491
|
+
}
|
|
2492
|
+
get isConfigured() {
|
|
2493
|
+
return Boolean(this.options.clientId && this.options.clientSecret);
|
|
2494
|
+
}
|
|
2495
|
+
/**
|
|
2496
|
+
* Get (cached) B2B access token. Mints a new one when expired.
|
|
2497
|
+
*/
|
|
2498
|
+
async getAccessToken() {
|
|
2499
|
+
const now = Date.now();
|
|
2500
|
+
if (this.token && now < this.tokenExpiry) {
|
|
2501
|
+
return this.token;
|
|
2502
|
+
}
|
|
2503
|
+
const { clientId, clientSecret, privateKey } = this.options;
|
|
2504
|
+
if (!privateKey) {
|
|
2505
|
+
throw new Error("DOKU SNAP: RSA privateKey required to obtain B2B access token");
|
|
2506
|
+
}
|
|
2507
|
+
const endpoint = "/authorization/v1/access-token/b2b";
|
|
2508
|
+
const timestamp = snapUtcTimestamp();
|
|
2509
|
+
const signature = generateSnapAsymmetricSignature(privateKey, clientId, timestamp);
|
|
2510
|
+
const response = await fetch(this.baseUrl + endpoint, {
|
|
2511
|
+
method: "POST",
|
|
2512
|
+
headers: {
|
|
2513
|
+
"Content-Type": "application/json",
|
|
2514
|
+
"X-CLIENT-KEY": clientId,
|
|
2515
|
+
"X-TIMESTAMP": timestamp,
|
|
2516
|
+
"X-SIGNATURE": signature
|
|
2517
|
+
},
|
|
2518
|
+
body: JSON.stringify({ grantType: "client_credentials" })
|
|
2519
|
+
});
|
|
2520
|
+
const text = await response.text();
|
|
2521
|
+
let data = null;
|
|
2522
|
+
try {
|
|
2523
|
+
data = JSON.parse(text);
|
|
2524
|
+
} catch {
|
|
2525
|
+
}
|
|
2526
|
+
if (!response.ok || !data?.accessToken) {
|
|
2527
|
+
throw new Error(data?.responseMessage || data?.message || `DOKU SNAP get-token failed: HTTP ${response.status} - ${text}`);
|
|
2528
|
+
}
|
|
2529
|
+
if (!clientSecret) {
|
|
2530
|
+
}
|
|
2531
|
+
this.token = data.accessToken;
|
|
2532
|
+
const expiresIn = Number(data.expiresIn || 900);
|
|
2533
|
+
this.tokenExpiry = now + (expiresIn - 30) * 1e3;
|
|
2534
|
+
return this.token;
|
|
2535
|
+
}
|
|
2536
|
+
clearToken() {
|
|
2537
|
+
this.token = null;
|
|
2538
|
+
this.tokenExpiry = 0;
|
|
2539
|
+
}
|
|
2540
|
+
/**
|
|
2541
|
+
* Perform a signed SNAP transaction request.
|
|
2542
|
+
* @param method GET/POST
|
|
2543
|
+
* @param endpoint request-target path (e.g. /virtual-accounts/...)
|
|
2544
|
+
* @param body request body (object)
|
|
2545
|
+
* @param opts extra headers (X-DEVICE-ID, X-IP-ADDRESS, etc.)
|
|
2546
|
+
*/
|
|
2547
|
+
async request(method, endpoint, body, opts = {}) {
|
|
2548
|
+
const { clientId, clientSecret, channelId } = this.options;
|
|
2549
|
+
if (!clientSecret) {
|
|
2550
|
+
throw new Error("DOKU SNAP: clientSecret (Secret Key) required for transaction signing");
|
|
2551
|
+
}
|
|
2552
|
+
const accessToken = await this.getAccessToken();
|
|
2553
|
+
const timestamp = snapTimestamp();
|
|
2554
|
+
const externalId = opts.externalId || snapExternalId();
|
|
2555
|
+
const payload = body === void 0 ? "" : body;
|
|
2556
|
+
const signature = generateSnapSymmetricSignature(
|
|
2557
|
+
clientSecret,
|
|
2558
|
+
method,
|
|
2559
|
+
endpoint,
|
|
2560
|
+
accessToken,
|
|
2561
|
+
payload,
|
|
2562
|
+
timestamp
|
|
2563
|
+
);
|
|
2564
|
+
const headers = {
|
|
2565
|
+
"Content-Type": "application/json",
|
|
2566
|
+
"X-PARTNER-ID": clientId,
|
|
2567
|
+
"X-EXTERNAL-ID": externalId,
|
|
2568
|
+
"X-TIMESTAMP": timestamp,
|
|
2569
|
+
"X-SIGNATURE": signature,
|
|
2570
|
+
"CHANNEL-ID": channelId || "H2H",
|
|
2571
|
+
"Authorization": `Bearer ${accessToken}`,
|
|
2572
|
+
...opts.deviceId ? { "X-DEVICE-ID": opts.deviceId } : {},
|
|
2573
|
+
...opts.ipAddress ? { "X-IP-ADDRESS": opts.ipAddress } : {},
|
|
2574
|
+
...opts.extraHeaders || {}
|
|
2575
|
+
};
|
|
2576
|
+
const response = await fetch(this.baseUrl + endpoint, {
|
|
2577
|
+
method,
|
|
2578
|
+
headers,
|
|
2579
|
+
body: method === "POST" && body !== void 0 ? JSON.stringify(body) : void 0
|
|
2580
|
+
});
|
|
2581
|
+
const text = await response.text();
|
|
2582
|
+
let data = null;
|
|
2583
|
+
try {
|
|
2584
|
+
data = JSON.parse(text);
|
|
2585
|
+
} catch {
|
|
2586
|
+
}
|
|
2587
|
+
if (!response.ok) {
|
|
2588
|
+
const err = data?.responseMessage || data?.error?.message || data?.message || `HTTP ${response.status}`;
|
|
2589
|
+
throw new Error(`${err}${data?.responseCode ? ` (${data.responseCode})` : ""}`);
|
|
2590
|
+
}
|
|
2591
|
+
return data;
|
|
2592
|
+
}
|
|
2593
|
+
};
|
|
2594
|
+
|
|
2416
2595
|
// src/providers/doku/provider.ts
|
|
2417
2596
|
var DokuProvider = class extends BasePaymentProvider {
|
|
2418
2597
|
name = "doku";
|
|
2419
2598
|
getBaseUrl(sandbox) {
|
|
2420
2599
|
return sandbox ? "https://api-sandbox.doku.com" : "https://api.doku.com";
|
|
2421
2600
|
}
|
|
2601
|
+
/**
|
|
2602
|
+
* DETEKSI mode integrasi DOKU:
|
|
2603
|
+
* - SNAP : kredensial baru (Client ID `doku_...` + Secret Key `SK-...`) + opsional RSA privateKey
|
|
2604
|
+
* untuk Get Token B2B. Diaktifkan via extra.snap / extra.dokuMode="snap" / doku_ prefix.
|
|
2605
|
+
* - Legacy: Jokul v2 (Client-Id + Signature HMAC-SHA256), default jika bukan SNAP.
|
|
2606
|
+
*/
|
|
2607
|
+
isSnap(config) {
|
|
2608
|
+
if (config.extra?.snap === true || config.extra?.snap === "true") return true;
|
|
2609
|
+
if (config.extra?.dokuMode === "snap") return true;
|
|
2610
|
+
const clientId = String(config.merchantCode || config.merchantId || config.clientKey || "").trim().toLowerCase();
|
|
2611
|
+
return /^doku[_:-]/.test(clientId);
|
|
2612
|
+
}
|
|
2613
|
+
buildSnap(config) {
|
|
2614
|
+
const clientId = config.merchantCode || config.merchantId || config.clientKey || "";
|
|
2615
|
+
const clientSecret = config.apiKey || config.serverKey || config.secretKey || "";
|
|
2616
|
+
return new SnapClient({
|
|
2617
|
+
clientId,
|
|
2618
|
+
clientSecret,
|
|
2619
|
+
privateKey: config.privateKey,
|
|
2620
|
+
sandbox: !!config.sandbox,
|
|
2621
|
+
merchantId: config.extra?.merchantId || config.projectId || "",
|
|
2622
|
+
terminalId: config.extra?.terminalId || "",
|
|
2623
|
+
partnerServiceId: config.extra?.partnerServiceId || "",
|
|
2624
|
+
channelId: config.extra?.channelId || "H2H"
|
|
2625
|
+
});
|
|
2626
|
+
}
|
|
2627
|
+
/** Helper: jumlah integer → format SNAP 2-desimal (".00"). */
|
|
2628
|
+
snapAmount(amount, currency = "IDR") {
|
|
2629
|
+
return { value: amount.toFixed(2), currency };
|
|
2630
|
+
}
|
|
2422
2631
|
async createInvoice(params, config) {
|
|
2423
2632
|
const { orderId, amount, productDetails, customer, returnUrl } = params;
|
|
2424
2633
|
const clientId = config.merchantCode || config.merchantId || config.clientKey || "";
|
|
@@ -2429,6 +2638,9 @@ var DokuProvider = class extends BasePaymentProvider {
|
|
|
2429
2638
|
const isDirect = !!dokuMethod;
|
|
2430
2639
|
const baseUrl = this.getBaseUrl(sandbox);
|
|
2431
2640
|
try {
|
|
2641
|
+
if (this.isSnap(config)) {
|
|
2642
|
+
return await this.createSnapInvoice(params, config, baseUrl);
|
|
2643
|
+
}
|
|
2432
2644
|
if (isDirect) {
|
|
2433
2645
|
const endpoint = dokuMethod.endpoint;
|
|
2434
2646
|
const url = `${baseUrl}${endpoint}`;
|
|
@@ -2616,7 +2828,178 @@ var DokuProvider = class extends BasePaymentProvider {
|
|
|
2616
2828
|
};
|
|
2617
2829
|
}
|
|
2618
2830
|
}
|
|
2831
|
+
/**
|
|
2832
|
+
* DOKU SNAP: buat transaksi (Create VA / Generate QRIS / e-Wallet Payment).
|
|
2833
|
+
* Autentikasi via B2B token + symmetric HMAC-SHA512 signature.
|
|
2834
|
+
*/
|
|
2835
|
+
async createSnapInvoice(params, config, baseUrl) {
|
|
2836
|
+
const { orderId, amount, productDetails, customer, returnUrl } = params;
|
|
2837
|
+
const snap = this.buildSnap(config);
|
|
2838
|
+
const integerAmount = Math.round(amount);
|
|
2839
|
+
const method = String(params.paymentMethod || "").toLowerCase();
|
|
2840
|
+
const bankMap = {
|
|
2841
|
+
bca: "VIRTUAL_ACCOUNT_BCA",
|
|
2842
|
+
mandiri: "VIRTUAL_ACCOUNT_BANK_MANDIRI",
|
|
2843
|
+
bri: "VIRTUAL_ACCOUNT_BRI",
|
|
2844
|
+
bni: "VIRTUAL_ACCOUNT_BNI",
|
|
2845
|
+
permata: "VIRTUAL_ACCOUNT_BANK_PERMATA",
|
|
2846
|
+
cimb: "VIRTUAL_ACCOUNT_BANK_CIMB",
|
|
2847
|
+
danamon: "VIRTUAL_ACCOUNT_BANK_DANAMON",
|
|
2848
|
+
bsi: "VIRTUAL_ACCOUNT_BANK_SYARIAH_MANDIRI",
|
|
2849
|
+
sinarmas: "VIRTUAL_ACCOUNT_SINARMAS",
|
|
2850
|
+
bjb: "VIRTUAL_ACCOUNT_BANK_BJB",
|
|
2851
|
+
btn: "VIRTUAL_ACCOUNT_BTN",
|
|
2852
|
+
bnc: "VIRTUAL_ACCOUNT_BNC"
|
|
2853
|
+
};
|
|
2854
|
+
const isVa = method.includes("_va");
|
|
2855
|
+
const isQris = method === "qris" || method.includes("qris");
|
|
2856
|
+
try {
|
|
2857
|
+
if (isVa) {
|
|
2858
|
+
return await this.snapCreateVA(params, config, snap, baseUrl, bankMap);
|
|
2859
|
+
}
|
|
2860
|
+
if (isQris) {
|
|
2861
|
+
return await this.snapGenerateQRIS(params, config, snap, baseUrl);
|
|
2862
|
+
}
|
|
2863
|
+
return await this.snapEWalletPayment(params, config, snap, baseUrl);
|
|
2864
|
+
} catch (e) {
|
|
2865
|
+
return {
|
|
2866
|
+
success: false,
|
|
2867
|
+
provider: "doku",
|
|
2868
|
+
orderId,
|
|
2869
|
+
amount: integerAmount,
|
|
2870
|
+
rawResponse: null,
|
|
2871
|
+
error: e.message || "Failed to make SNAP request to DOKU API"
|
|
2872
|
+
};
|
|
2873
|
+
}
|
|
2874
|
+
}
|
|
2875
|
+
/** Create Virtual Account (SNAP) — DOKU Generate Payment Code. */
|
|
2876
|
+
async snapCreateVA(params, config, snap, baseUrl, bankMap) {
|
|
2877
|
+
const { orderId, amount, productDetails, customer } = params;
|
|
2878
|
+
const method = String(params.paymentMethod || "").toLowerCase();
|
|
2879
|
+
const bankKey = method.replace(/_va$/, "");
|
|
2880
|
+
const channel = config.extra?.vaChannel || bankMap[bankKey] || `VIRTUAL_ACCOUNT_${bankKey.toUpperCase()}`;
|
|
2881
|
+
const partnerServiceId = (config.extra?.partnerServiceId || "").replace(/\s/g, "");
|
|
2882
|
+
const customerNo = (config.extra?.customerNo || "").slice(0, 20) || String(Date.now()).slice(-12);
|
|
2883
|
+
const virtualAccountNo = (partnerServiceId + customerNo).slice(0, 28);
|
|
2884
|
+
const reusable = config.extra?.reusableStatus === true;
|
|
2885
|
+
const currency = params.currency || "IDR";
|
|
2886
|
+
const body = {
|
|
2887
|
+
partnerServiceId,
|
|
2888
|
+
customerNo,
|
|
2889
|
+
virtualAccountNo,
|
|
2890
|
+
virtualAccountName: customer.name || "Customer",
|
|
2891
|
+
virtualAccountEmail: customer.email,
|
|
2892
|
+
virtualAccountPhone: customer.phone,
|
|
2893
|
+
trxId: orderId,
|
|
2894
|
+
totalAmount: this.snapAmount(amount, currency),
|
|
2895
|
+
virtualAccountTrxType: "C",
|
|
2896
|
+
additionalInfo: {
|
|
2897
|
+
channel,
|
|
2898
|
+
virtualAccountConfig: { reusableStatus: reusable }
|
|
2899
|
+
}
|
|
2900
|
+
};
|
|
2901
|
+
if (params.providerParams) {
|
|
2902
|
+
Object.assign(body.additionalInfo.virtualAccountConfig, params.providerParams);
|
|
2903
|
+
}
|
|
2904
|
+
if (config.extra?.expiredDate) body.expiredDate = config.extra.expiredDate;
|
|
2905
|
+
const endpoint = "/virtual-accounts/bi-snap-va/v1.1/transfer-va/create-va";
|
|
2906
|
+
const data = await snap.request("POST", endpoint, body);
|
|
2907
|
+
const vaData = data.virtualAccountData || {};
|
|
2908
|
+
return {
|
|
2909
|
+
success: true,
|
|
2910
|
+
provider: "doku",
|
|
2911
|
+
orderId: vaData.trxId || orderId,
|
|
2912
|
+
amount: Number(vaData.totalAmount?.value || amount) || Math.round(amount),
|
|
2913
|
+
reference: vaData.virtualAccountNo || orderId,
|
|
2914
|
+
vaNumber: vaData.virtualAccountNo,
|
|
2915
|
+
vaBank: bankKey.toUpperCase(),
|
|
2916
|
+
paymentUrl: vaData.additionalInfo?.howToPayPage,
|
|
2917
|
+
expiresAt: vaData.expiredDate ? new Date(vaData.expiredDate) : void 0,
|
|
2918
|
+
rawResponse: data
|
|
2919
|
+
};
|
|
2920
|
+
}
|
|
2921
|
+
/** Generate QRIS (SNAP) — dynamic QRIS MPM. */
|
|
2922
|
+
async snapGenerateQRIS(params, config, snap, baseUrl) {
|
|
2923
|
+
const { orderId, amount } = params;
|
|
2924
|
+
const currency = params.currency || "IDR";
|
|
2925
|
+
const merchantId = config.extra?.merchantId || config.projectId || "";
|
|
2926
|
+
const terminalId = config.extra?.terminalId || "0001";
|
|
2927
|
+
const body = {
|
|
2928
|
+
partnerReferenceNo: orderId,
|
|
2929
|
+
amount: this.snapAmount(amount, currency),
|
|
2930
|
+
merchantId,
|
|
2931
|
+
terminalId,
|
|
2932
|
+
validityPeriod: config.extra?.validityPeriod || new Date(Date.now() + 3600 * 1e3).toISOString(),
|
|
2933
|
+
additionalInfo: {
|
|
2934
|
+
postalCode: config.extra?.postalCode || "10110",
|
|
2935
|
+
feeType: 1
|
|
2936
|
+
}
|
|
2937
|
+
};
|
|
2938
|
+
if (params.providerParams) {
|
|
2939
|
+
Object.assign(body.additionalInfo, params.providerParams);
|
|
2940
|
+
}
|
|
2941
|
+
const endpoint = "/snap-adapter/b2b/v1.0/qr/qr-mpm-generate";
|
|
2942
|
+
const data = await snap.request("POST", endpoint, body);
|
|
2943
|
+
return {
|
|
2944
|
+
success: true,
|
|
2945
|
+
provider: "doku",
|
|
2946
|
+
orderId: data.partnerReferenceNo || orderId,
|
|
2947
|
+
amount: Number(data.amount?.value || amount) || Math.round(amount),
|
|
2948
|
+
reference: data.referenceNo || orderId,
|
|
2949
|
+
qrString: data.qrContent,
|
|
2950
|
+
rawResponse: data
|
|
2951
|
+
};
|
|
2952
|
+
}
|
|
2953
|
+
/** e-Wallet payment (SNAP) — DANA / OVO / ShopeePay via payment-host-to-host. */
|
|
2954
|
+
async snapEWalletPayment(params, config, snap, baseUrl) {
|
|
2955
|
+
const { orderId, amount } = params;
|
|
2956
|
+
const method = String(params.paymentMethod || "").toLowerCase();
|
|
2957
|
+
const currency = params.currency || "IDR";
|
|
2958
|
+
const returnUrl = params.returnUrl || config.returnUrl || "";
|
|
2959
|
+
const channelMap = {
|
|
2960
|
+
dana: "EMONEY_DANA_SNAP",
|
|
2961
|
+
ovo: "EMONEY_OVO_SNAP",
|
|
2962
|
+
shopeepay: "EMONEY_SHOPEEPAY_SNAP"
|
|
2963
|
+
};
|
|
2964
|
+
const channel = config.extra?.ewalletChannel || channelMap[method] || `EMONEY_${method.toUpperCase()}_SNAP`;
|
|
2965
|
+
const body = {
|
|
2966
|
+
partnerReferenceNo: orderId,
|
|
2967
|
+
amount: this.snapAmount(amount, currency),
|
|
2968
|
+
pointOfInitiation: "pc",
|
|
2969
|
+
urlParam: {
|
|
2970
|
+
url: returnUrl || "https://example.com/return",
|
|
2971
|
+
type: "PAY_RETURN",
|
|
2972
|
+
isDeepLink: "N"
|
|
2973
|
+
},
|
|
2974
|
+
additionalInfo: {
|
|
2975
|
+
channel,
|
|
2976
|
+
orderTitle: params.productDetails || `Pembayaran ${orderId}`,
|
|
2977
|
+
supportDeepLinkCheckoutUrl: "false"
|
|
2978
|
+
}
|
|
2979
|
+
};
|
|
2980
|
+
if (params.providerParams) {
|
|
2981
|
+
Object.assign(body.additionalInfo, params.providerParams);
|
|
2982
|
+
}
|
|
2983
|
+
const endpoint = "/direct-debit/core/v1/debit/payment-host-to-host";
|
|
2984
|
+
const data = await snap.request("POST", endpoint, body, {
|
|
2985
|
+
deviceId: config.extra?.deviceId,
|
|
2986
|
+
ipAddress: config.extra?.ipAddress
|
|
2987
|
+
});
|
|
2988
|
+
return {
|
|
2989
|
+
success: true,
|
|
2990
|
+
provider: "doku",
|
|
2991
|
+
orderId: data.partnerReferenceNo || orderId,
|
|
2992
|
+
amount: Number(data.amount?.value || amount) || Math.round(amount),
|
|
2993
|
+
reference: data.partnerReferenceNo || orderId,
|
|
2994
|
+
paymentUrl: data.webRedirectUrl || data.paymentUrl,
|
|
2995
|
+
rawResponse: data
|
|
2996
|
+
};
|
|
2997
|
+
}
|
|
2619
2998
|
async verifyCallback(body, config) {
|
|
2999
|
+
const headers = config.extra?.headers || {};
|
|
3000
|
+
if (this.isSnap(config)) {
|
|
3001
|
+
return this.verifySnapCallback(body, config, headers);
|
|
3002
|
+
}
|
|
2620
3003
|
const rawStatus = (body.transaction?.status || body.status || "").toUpperCase();
|
|
2621
3004
|
const isPaid = rawStatus === "SUCCESS" || rawStatus === "PAID" || rawStatus === "SETTLED";
|
|
2622
3005
|
const isPending = rawStatus === "PENDING";
|
|
@@ -2626,7 +3009,6 @@ var DokuProvider = class extends BasePaymentProvider {
|
|
|
2626
3009
|
const amount = body.order?.amount || body.amount || 0;
|
|
2627
3010
|
const status = isPaid ? "paid" : isPending ? "pending" : isExpired ? "expired" : "failed";
|
|
2628
3011
|
const secretKey = config.secretKey || config.apiKey || "";
|
|
2629
|
-
const headers = config.extra?.headers || {};
|
|
2630
3012
|
const signature = headers["signature"] || headers["Signature"] || config.extra?.dokuSignature || config.extra?.signatureHeader;
|
|
2631
3013
|
const clientId = config.merchantCode || config.clientKey || "";
|
|
2632
3014
|
let isValid = true;
|
|
@@ -2647,6 +3029,40 @@ var DokuProvider = class extends BasePaymentProvider {
|
|
|
2647
3029
|
rawPayload: body
|
|
2648
3030
|
};
|
|
2649
3031
|
}
|
|
3032
|
+
/**
|
|
3033
|
+
* Verifikasi webhook / notifikasi DOKU SNAP.
|
|
3034
|
+
* Signature dibangun dengan symmetric HMAC-SHA512 (AccessToken kosong).
|
|
3035
|
+
*/
|
|
3036
|
+
verifySnapCallback(body, config, headers) {
|
|
3037
|
+
const clientSecret = config.apiKey || config.serverKey || config.secretKey || "";
|
|
3038
|
+
const endpointUrl = config.extra?.notificationPath || headers["x-path"] || config.extra?.headers?.["request-target"] || "/api/payment/webhook";
|
|
3039
|
+
let isValid = true;
|
|
3040
|
+
const incomingSig = headers["x-signature"] || headers["X-SIGNATURE"] || headers["signature"] || headers["Signature"] || "";
|
|
3041
|
+
if (incomingSig && clientSecret) {
|
|
3042
|
+
isValid = verifySnapWebhookSignature(headers, body, clientSecret, endpointUrl);
|
|
3043
|
+
}
|
|
3044
|
+
const explicitStatus = String(body.transactionStatus || body.status || body.latestTransactionStatus || "").toUpperCase();
|
|
3045
|
+
const isPaid = explicitStatus === "SUCCESS" || explicitStatus === "PAID" || explicitStatus === "SETTLED" || explicitStatus === "00" || !explicitStatus && Boolean(body.paidAmount?.value ?? body.totalAmount?.value);
|
|
3046
|
+
const isPending = explicitStatus === "PENDING" || explicitStatus === "11" || explicitStatus === "ONGOING";
|
|
3047
|
+
const isFailed = explicitStatus === "FAILED" || explicitStatus === "DECLINED" || Boolean(explicitStatus) && !isPaid && !isPending && explicitStatus !== "00";
|
|
3048
|
+
const isExpired = explicitStatus === "EXPIRED";
|
|
3049
|
+
const orderId = body.trxId || body.partnerReferenceNo || body.originalPartnerReferenceNo || body.order?.invoice_number || body.invoice_number || body.order_id || "";
|
|
3050
|
+
const paidValue = body.paidAmount?.value ?? body.totalAmount?.value ?? body.amount?.value ?? body.amount ?? 0;
|
|
3051
|
+
const status = isPaid ? "paid" : isPending ? "pending" : isExpired ? "expired" : "failed";
|
|
3052
|
+
return {
|
|
3053
|
+
isValid,
|
|
3054
|
+
provider: "doku",
|
|
3055
|
+
orderId: String(orderId),
|
|
3056
|
+
amount: Number(paidValue) || 0,
|
|
3057
|
+
status,
|
|
3058
|
+
isPaid,
|
|
3059
|
+
isPending,
|
|
3060
|
+
isFailed,
|
|
3061
|
+
isExpired,
|
|
3062
|
+
statusCode: explicitStatus || "SUCCESS",
|
|
3063
|
+
rawPayload: body
|
|
3064
|
+
};
|
|
3065
|
+
}
|
|
2650
3066
|
async getPaymentMethods(params, config) {
|
|
2651
3067
|
const staticMethods = [
|
|
2652
3068
|
{
|
|
@@ -2806,6 +3222,9 @@ var DokuProvider = class extends BasePaymentProvider {
|
|
|
2806
3222
|
const clientId = config.merchantCode || config.merchantId || config.clientKey || "";
|
|
2807
3223
|
const secretKey = config.apiKey || config.serverKey || config.secretKey || "";
|
|
2808
3224
|
const sandbox = !!config.sandbox;
|
|
3225
|
+
if (this.isSnap(config)) {
|
|
3226
|
+
return this.snapCheckTransaction(params, config, clientId);
|
|
3227
|
+
}
|
|
2809
3228
|
const endpoint = `/orders/v1/status/${merchantOrderId}`;
|
|
2810
3229
|
const url = `${this.getBaseUrl(sandbox)}${endpoint}`;
|
|
2811
3230
|
const headers = generateDokuHeaders(clientId, secretKey, endpoint);
|
|
@@ -2883,6 +3302,58 @@ var DokuProvider = class extends BasePaymentProvider {
|
|
|
2883
3302
|
};
|
|
2884
3303
|
}
|
|
2885
3304
|
}
|
|
3305
|
+
/** Cek status transaksi SNAP (menggunakan Query QRIS bila ref berasal dari QRIS). */
|
|
3306
|
+
async snapCheckTransaction(params, config, clientId) {
|
|
3307
|
+
const { merchantOrderId } = params;
|
|
3308
|
+
const snap = this.buildSnap(config);
|
|
3309
|
+
try {
|
|
3310
|
+
const body = {
|
|
3311
|
+
originalPartnerReferenceNo: merchantOrderId,
|
|
3312
|
+
serviceCode: "47",
|
|
3313
|
+
merchantId: config.extra?.merchantId || config.projectId || ""
|
|
3314
|
+
};
|
|
3315
|
+
const data = await snap.request("POST", "/snap-adapter/b2b/v1.0/qr/qr-mpm-query", body);
|
|
3316
|
+
const txStatus = String(data.latestTransactionStatus || "").toUpperCase();
|
|
3317
|
+
const isPaid = txStatus === "SUCCESS" || txStatus === "00" || txStatus === "PAID" || txStatus === "SETTLED";
|
|
3318
|
+
const isPending = txStatus === "PENDING" || txStatus === "ONGOING" || txStatus === "11";
|
|
3319
|
+
const isExpired = txStatus === "EXPIRED";
|
|
3320
|
+
const isFailed = txStatus === "FAILED" || txStatus === "DECLINED" || Boolean(txStatus) && !isPaid && !isPending && !isExpired;
|
|
3321
|
+
const status = isPaid ? "paid" : isPending ? "pending" : isExpired ? "expired" : "failed";
|
|
3322
|
+
return {
|
|
3323
|
+
success: true,
|
|
3324
|
+
provider: "doku",
|
|
3325
|
+
orderId: data.originalPartnerReferenceNo || merchantOrderId,
|
|
3326
|
+
reference: data.originalReferenceNo || "",
|
|
3327
|
+
amount: Number(data.amount?.value || 0),
|
|
3328
|
+
statusCode: txStatus || String(data.responseCode || ""),
|
|
3329
|
+
status,
|
|
3330
|
+
isPaid,
|
|
3331
|
+
isPending,
|
|
3332
|
+
isFailed,
|
|
3333
|
+
isExpired,
|
|
3334
|
+
statusMessage: txStatus || data.responseMessage || "",
|
|
3335
|
+
paymentType: "QRIS",
|
|
3336
|
+
rawResponse: data
|
|
3337
|
+
};
|
|
3338
|
+
} catch (e) {
|
|
3339
|
+
return {
|
|
3340
|
+
success: false,
|
|
3341
|
+
provider: "doku",
|
|
3342
|
+
orderId: merchantOrderId,
|
|
3343
|
+
reference: "",
|
|
3344
|
+
amount: 0,
|
|
3345
|
+
statusCode: "ERROR",
|
|
3346
|
+
status: "failed",
|
|
3347
|
+
isPaid: false,
|
|
3348
|
+
isPending: false,
|
|
3349
|
+
isFailed: true,
|
|
3350
|
+
isExpired: false,
|
|
3351
|
+
statusMessage: e.message || "Failed to check SNAP transaction status",
|
|
3352
|
+
error: e.message || "Failed to check SNAP transaction status",
|
|
3353
|
+
rawResponse: null
|
|
3354
|
+
};
|
|
3355
|
+
}
|
|
3356
|
+
}
|
|
2886
3357
|
};
|
|
2887
3358
|
|
|
2888
3359
|
// src/providers/prismalink/signature.ts
|
|
@@ -3276,15 +3747,15 @@ var PrismalinkProvider = class extends BasePaymentProvider {
|
|
|
3276
3747
|
};
|
|
3277
3748
|
|
|
3278
3749
|
// src/providers/faspay/signature.ts
|
|
3279
|
-
import
|
|
3750
|
+
import crypto4 from "crypto";
|
|
3280
3751
|
function generateFaspaySignature(userId, password, billNo) {
|
|
3281
|
-
const md5Hash =
|
|
3282
|
-
return
|
|
3752
|
+
const md5Hash = crypto4.createHash("md5").update(`${userId}${password}${billNo}`).digest("hex");
|
|
3753
|
+
return crypto4.createHash("sha1").update(md5Hash).digest("hex");
|
|
3283
3754
|
}
|
|
3284
3755
|
function verifyFaspaySignature(userId, password, billNo, paymentStatusCode, incomingSignature) {
|
|
3285
3756
|
if (!incomingSignature || !password) return false;
|
|
3286
|
-
const md5Hash =
|
|
3287
|
-
const computed =
|
|
3757
|
+
const md5Hash = crypto4.createHash("md5").update(`${userId}${password}${billNo}${paymentStatusCode}`).digest("hex");
|
|
3758
|
+
const computed = crypto4.createHash("sha1").update(md5Hash).digest("hex");
|
|
3288
3759
|
const simpleComputed = generateFaspaySignature(userId, password, billNo);
|
|
3289
3760
|
return safeCompare(incomingSignature, computed) || safeCompare(incomingSignature, simpleComputed);
|
|
3290
3761
|
}
|
|
@@ -3697,10 +4168,10 @@ var FaspayProvider = class extends BasePaymentProvider {
|
|
|
3697
4168
|
};
|
|
3698
4169
|
|
|
3699
4170
|
// src/providers/finpay/signature.ts
|
|
3700
|
-
import
|
|
4171
|
+
import crypto5 from "crypto";
|
|
3701
4172
|
function generateFinpaySignature(merchantId, orderId, amount, merchantKey) {
|
|
3702
4173
|
const data = `${merchantId}%${orderId}%${Math.round(amount)}%${merchantKey}`;
|
|
3703
|
-
return
|
|
4174
|
+
return crypto5.createHmac("sha512", merchantKey).update(data).digest("hex");
|
|
3704
4175
|
}
|
|
3705
4176
|
function verifyFinpaySignature(merchantId, orderId, amount, merchantKey, incomingSignature) {
|
|
3706
4177
|
if (!incomingSignature || !merchantKey) return false;
|
|
@@ -9442,6 +9913,7 @@ export {
|
|
|
9442
9913
|
PrismalinkProvider,
|
|
9443
9914
|
RazorpayClient,
|
|
9444
9915
|
RazorpayProvider,
|
|
9916
|
+
SnapClient,
|
|
9445
9917
|
SquareClient,
|
|
9446
9918
|
SquareProvider,
|
|
9447
9919
|
StripeClient,
|
|
@@ -9465,6 +9937,8 @@ export {
|
|
|
9465
9937
|
generateNicepayToken,
|
|
9466
9938
|
generateOyHeaders,
|
|
9467
9939
|
generatePrismalinkSignature,
|
|
9940
|
+
generateSnapAsymmetricSignature,
|
|
9941
|
+
generateSnapSymmetricSignature,
|
|
9468
9942
|
getDuitkuInquirySignatures,
|
|
9469
9943
|
getDuitkuPaymentMethodsSignature,
|
|
9470
9944
|
getDuitkuStatusSignatures,
|
|
@@ -9472,6 +9946,7 @@ export {
|
|
|
9472
9946
|
getXenditAuthHeader,
|
|
9473
9947
|
hmacSha256,
|
|
9474
9948
|
md5,
|
|
9949
|
+
minifyJson,
|
|
9475
9950
|
parseCoreChargeResponse,
|
|
9476
9951
|
paymentManager,
|
|
9477
9952
|
resolveConfigFromEnv,
|
|
@@ -9479,7 +9954,11 @@ export {
|
|
|
9479
9954
|
serializePaypalParams,
|
|
9480
9955
|
serializeStripeParams,
|
|
9481
9956
|
sha256,
|
|
9957
|
+
sha256Hex,
|
|
9482
9958
|
sha512,
|
|
9959
|
+
snapExternalId,
|
|
9960
|
+
snapTimestamp,
|
|
9961
|
+
snapUtcTimestamp,
|
|
9483
9962
|
toCanonicalPaymentMethod,
|
|
9484
9963
|
toDokuPaymentMethod,
|
|
9485
9964
|
toDuitkuPaymentMethod,
|
|
@@ -9505,6 +9984,7 @@ export {
|
|
|
9505
9984
|
verifyPayuWebhook,
|
|
9506
9985
|
verifyPrismalinkSignature,
|
|
9507
9986
|
verifyRazorpayWebhook,
|
|
9987
|
+
verifySnapWebhookSignature,
|
|
9508
9988
|
verifySquareWebhook,
|
|
9509
9989
|
verifyStripeWebhook,
|
|
9510
9990
|
verifyTwoCheckoutWebhook,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@crediblemark/buayar",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.1",
|
|
4
4
|
"description": "Unified Payment Gateway SDK for Node.js & TypeScript — 19 providers (Midtrans, Xendit, Duitku, Stripe, PayPal, Adyen, Razorpay, Square, Checkout.com, PayU, Braintree, 2Checkout, DOKU, iPaymu, PrismaLink, Faspay, Finpay, Nicepay, OY!) with zero-code switching via .env",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"module": "./dist/index.mjs",
|