@crediblemark/buayar 0.8.5 → 0.8.6
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/cli/index.js +1 -1
- package/dist/index.d.mts +35 -5
- package/dist/index.d.ts +35 -5
- package/dist/index.js +184 -16
- package/dist/index.mjs +184 -16
- package/docs/AUDIT-BUG-DAN-PREMATURE.md +166 -0
- package/docs/guide.md +24 -2
- package/docs/ipaymu.md +22 -7
- package/package.json +1 -1
package/dist/cli/index.js
CHANGED
package/dist/index.d.mts
CHANGED
|
@@ -139,8 +139,12 @@ interface VerifyCallbackResult {
|
|
|
139
139
|
transactionTime?: Date | string;
|
|
140
140
|
/** Raw payload callback asli dari webhook */
|
|
141
141
|
rawPayload: any;
|
|
142
|
+
/** Pesan error jika verifikasi callback gagal */
|
|
143
|
+
error?: string;
|
|
142
144
|
}
|
|
143
145
|
interface ProviderConfig {
|
|
146
|
+
/** Nama provider target (opsional, jika ingin override per panggilan fungsi) */
|
|
147
|
+
provider?: string;
|
|
144
148
|
merchantCode?: string;
|
|
145
149
|
apiKey?: string;
|
|
146
150
|
sandbox?: boolean;
|
|
@@ -200,6 +204,8 @@ interface PaymentMethod {
|
|
|
200
204
|
category: "Virtual Account" | "QRIS" | "E-Wallet" | "Retail / Gerai" | "Kartu Kredit" | "Paylater / Cicilan" | "Lainnya" | string;
|
|
201
205
|
code?: string;
|
|
202
206
|
extra?: any;
|
|
207
|
+
/** Tandai channel sebagai coming soon / belum tersedia */
|
|
208
|
+
coming_soon?: boolean;
|
|
203
209
|
feeDetail?: {
|
|
204
210
|
flat: number;
|
|
205
211
|
percent: number;
|
|
@@ -250,7 +256,13 @@ interface GetPaymentMethodDescriptorsResult {
|
|
|
250
256
|
generatedAt: string;
|
|
251
257
|
}
|
|
252
258
|
interface CheckTransactionParams {
|
|
253
|
-
/**
|
|
259
|
+
/**
|
|
260
|
+
* ID transaksi untuk pengecekan status.
|
|
261
|
+
*
|
|
262
|
+
* **iPaymu:** Harus berisi `TransactionId` numerik dari response `createInvoice`
|
|
263
|
+
* (`invoice.reference`), BUKAN `orderId`/`order_number` merchant.
|
|
264
|
+
* Endpoint iPaymu `/api/v2/transaction` hanya menerima ID numerik milik iPaymu.
|
|
265
|
+
*/
|
|
254
266
|
merchantOrderId: string;
|
|
255
267
|
}
|
|
256
268
|
interface CheckTransactionResult {
|
|
@@ -419,6 +431,11 @@ declare class IpaymuProvider extends BasePaymentProvider {
|
|
|
419
431
|
createInvoice(params: CreateInvoiceParams, config: ProviderConfig): Promise<InvoiceResponse>;
|
|
420
432
|
verifyCallback(body: any, config: ProviderConfig): Promise<VerifyCallbackResult>;
|
|
421
433
|
getPaymentMethods(params: GetPaymentMethodsParams, config: ProviderConfig): Promise<GetPaymentMethodsResult>;
|
|
434
|
+
probePaymentMethods(config: ProviderConfig): Promise<{
|
|
435
|
+
success: boolean;
|
|
436
|
+
enabled: string[];
|
|
437
|
+
error?: string;
|
|
438
|
+
}>;
|
|
422
439
|
checkTransaction(params: CheckTransactionParams, config: ProviderConfig): Promise<CheckTransactionResult>;
|
|
423
440
|
}
|
|
424
441
|
|
|
@@ -428,6 +445,11 @@ declare class XenditProvider extends BasePaymentProvider {
|
|
|
428
445
|
createInvoice(params: CreateInvoiceParams, config: ProviderConfig): Promise<InvoiceResponse>;
|
|
429
446
|
verifyCallback(body: any, config: ProviderConfig): Promise<VerifyCallbackResult>;
|
|
430
447
|
getPaymentMethods(params: GetPaymentMethodsParams, config: ProviderConfig): Promise<GetPaymentMethodsResult>;
|
|
448
|
+
probePaymentMethods(config: ProviderConfig): Promise<{
|
|
449
|
+
success: boolean;
|
|
450
|
+
enabled: string[];
|
|
451
|
+
error?: string;
|
|
452
|
+
}>;
|
|
431
453
|
checkTransaction(params: CheckTransactionParams, config: ProviderConfig): Promise<CheckTransactionResult>;
|
|
432
454
|
}
|
|
433
455
|
|
|
@@ -1255,10 +1277,10 @@ declare class ProviderRegistry {
|
|
|
1255
1277
|
*/
|
|
1256
1278
|
detectFromEnv(env: Record<string, string | undefined>): string | undefined;
|
|
1257
1279
|
/**
|
|
1258
|
-
* Autodetect provider dari struktur payload webhook.
|
|
1280
|
+
* Autodetect provider dari struktur payload webhook dan header.
|
|
1259
1281
|
* Mengembalikan nama provider jika dikenali, else undefined.
|
|
1260
1282
|
*/
|
|
1261
|
-
detectFromWebhook(payload: any): string | undefined;
|
|
1283
|
+
detectFromWebhook(payload: any, headers?: Record<string, any>): string | undefined;
|
|
1262
1284
|
}
|
|
1263
1285
|
declare function buildDefaultDescriptors(): ProviderDescriptor[];
|
|
1264
1286
|
declare const providerRegistry: ProviderRegistry;
|
|
@@ -1315,9 +1337,9 @@ declare class Buayar {
|
|
|
1315
1337
|
*/
|
|
1316
1338
|
getCapabilities(name?: string): ProviderCapability | undefined;
|
|
1317
1339
|
/**
|
|
1318
|
-
* Deteksi nama provider dari struktur payload webhook.
|
|
1340
|
+
* Deteksi nama provider dari struktur payload webhook dan opsional headers.
|
|
1319
1341
|
*/
|
|
1320
|
-
detectProviderFromPayload(payload: any): string | undefined;
|
|
1342
|
+
detectProviderFromPayload(payload: any, headers?: Record<string, string | string[] | undefined>): string | undefined;
|
|
1321
1343
|
/**
|
|
1322
1344
|
* Deteksi nama provider aktif dari variabel lingkungan (kredensial yang terisi).
|
|
1323
1345
|
*/
|
|
@@ -1360,6 +1382,14 @@ declare class Buayar {
|
|
|
1360
1382
|
*/
|
|
1361
1383
|
verifyWebhook(payload: any, headers?: Record<string, string | string[] | undefined>, configOverride?: Partial<ProviderConfig>): Promise<VerifyCallbackResult>;
|
|
1362
1384
|
handleWebhook(payload: any, headers?: Record<string, string | string[] | undefined>, configOverride?: Partial<ProviderConfig>): Promise<VerifyCallbackResult>;
|
|
1385
|
+
/**
|
|
1386
|
+
* Probe payment methods yang benar-benar aktif di akun merchant gateway.
|
|
1387
|
+
*/
|
|
1388
|
+
probePaymentMethods(configOverride?: Partial<ProviderConfig>): Promise<{
|
|
1389
|
+
success: boolean;
|
|
1390
|
+
enabled: string[];
|
|
1391
|
+
error?: string;
|
|
1392
|
+
}>;
|
|
1363
1393
|
/**
|
|
1364
1394
|
* Unified Refund — berlaku untuk semua provider yang mendukung refund.
|
|
1365
1395
|
* Provider tanpa fitur refund mengembalikan `{ supported: false }`, bukan error.
|
package/dist/index.d.ts
CHANGED
|
@@ -139,8 +139,12 @@ interface VerifyCallbackResult {
|
|
|
139
139
|
transactionTime?: Date | string;
|
|
140
140
|
/** Raw payload callback asli dari webhook */
|
|
141
141
|
rawPayload: any;
|
|
142
|
+
/** Pesan error jika verifikasi callback gagal */
|
|
143
|
+
error?: string;
|
|
142
144
|
}
|
|
143
145
|
interface ProviderConfig {
|
|
146
|
+
/** Nama provider target (opsional, jika ingin override per panggilan fungsi) */
|
|
147
|
+
provider?: string;
|
|
144
148
|
merchantCode?: string;
|
|
145
149
|
apiKey?: string;
|
|
146
150
|
sandbox?: boolean;
|
|
@@ -200,6 +204,8 @@ interface PaymentMethod {
|
|
|
200
204
|
category: "Virtual Account" | "QRIS" | "E-Wallet" | "Retail / Gerai" | "Kartu Kredit" | "Paylater / Cicilan" | "Lainnya" | string;
|
|
201
205
|
code?: string;
|
|
202
206
|
extra?: any;
|
|
207
|
+
/** Tandai channel sebagai coming soon / belum tersedia */
|
|
208
|
+
coming_soon?: boolean;
|
|
203
209
|
feeDetail?: {
|
|
204
210
|
flat: number;
|
|
205
211
|
percent: number;
|
|
@@ -250,7 +256,13 @@ interface GetPaymentMethodDescriptorsResult {
|
|
|
250
256
|
generatedAt: string;
|
|
251
257
|
}
|
|
252
258
|
interface CheckTransactionParams {
|
|
253
|
-
/**
|
|
259
|
+
/**
|
|
260
|
+
* ID transaksi untuk pengecekan status.
|
|
261
|
+
*
|
|
262
|
+
* **iPaymu:** Harus berisi `TransactionId` numerik dari response `createInvoice`
|
|
263
|
+
* (`invoice.reference`), BUKAN `orderId`/`order_number` merchant.
|
|
264
|
+
* Endpoint iPaymu `/api/v2/transaction` hanya menerima ID numerik milik iPaymu.
|
|
265
|
+
*/
|
|
254
266
|
merchantOrderId: string;
|
|
255
267
|
}
|
|
256
268
|
interface CheckTransactionResult {
|
|
@@ -419,6 +431,11 @@ declare class IpaymuProvider extends BasePaymentProvider {
|
|
|
419
431
|
createInvoice(params: CreateInvoiceParams, config: ProviderConfig): Promise<InvoiceResponse>;
|
|
420
432
|
verifyCallback(body: any, config: ProviderConfig): Promise<VerifyCallbackResult>;
|
|
421
433
|
getPaymentMethods(params: GetPaymentMethodsParams, config: ProviderConfig): Promise<GetPaymentMethodsResult>;
|
|
434
|
+
probePaymentMethods(config: ProviderConfig): Promise<{
|
|
435
|
+
success: boolean;
|
|
436
|
+
enabled: string[];
|
|
437
|
+
error?: string;
|
|
438
|
+
}>;
|
|
422
439
|
checkTransaction(params: CheckTransactionParams, config: ProviderConfig): Promise<CheckTransactionResult>;
|
|
423
440
|
}
|
|
424
441
|
|
|
@@ -428,6 +445,11 @@ declare class XenditProvider extends BasePaymentProvider {
|
|
|
428
445
|
createInvoice(params: CreateInvoiceParams, config: ProviderConfig): Promise<InvoiceResponse>;
|
|
429
446
|
verifyCallback(body: any, config: ProviderConfig): Promise<VerifyCallbackResult>;
|
|
430
447
|
getPaymentMethods(params: GetPaymentMethodsParams, config: ProviderConfig): Promise<GetPaymentMethodsResult>;
|
|
448
|
+
probePaymentMethods(config: ProviderConfig): Promise<{
|
|
449
|
+
success: boolean;
|
|
450
|
+
enabled: string[];
|
|
451
|
+
error?: string;
|
|
452
|
+
}>;
|
|
431
453
|
checkTransaction(params: CheckTransactionParams, config: ProviderConfig): Promise<CheckTransactionResult>;
|
|
432
454
|
}
|
|
433
455
|
|
|
@@ -1255,10 +1277,10 @@ declare class ProviderRegistry {
|
|
|
1255
1277
|
*/
|
|
1256
1278
|
detectFromEnv(env: Record<string, string | undefined>): string | undefined;
|
|
1257
1279
|
/**
|
|
1258
|
-
* Autodetect provider dari struktur payload webhook.
|
|
1280
|
+
* Autodetect provider dari struktur payload webhook dan header.
|
|
1259
1281
|
* Mengembalikan nama provider jika dikenali, else undefined.
|
|
1260
1282
|
*/
|
|
1261
|
-
detectFromWebhook(payload: any): string | undefined;
|
|
1283
|
+
detectFromWebhook(payload: any, headers?: Record<string, any>): string | undefined;
|
|
1262
1284
|
}
|
|
1263
1285
|
declare function buildDefaultDescriptors(): ProviderDescriptor[];
|
|
1264
1286
|
declare const providerRegistry: ProviderRegistry;
|
|
@@ -1315,9 +1337,9 @@ declare class Buayar {
|
|
|
1315
1337
|
*/
|
|
1316
1338
|
getCapabilities(name?: string): ProviderCapability | undefined;
|
|
1317
1339
|
/**
|
|
1318
|
-
* Deteksi nama provider dari struktur payload webhook.
|
|
1340
|
+
* Deteksi nama provider dari struktur payload webhook dan opsional headers.
|
|
1319
1341
|
*/
|
|
1320
|
-
detectProviderFromPayload(payload: any): string | undefined;
|
|
1342
|
+
detectProviderFromPayload(payload: any, headers?: Record<string, string | string[] | undefined>): string | undefined;
|
|
1321
1343
|
/**
|
|
1322
1344
|
* Deteksi nama provider aktif dari variabel lingkungan (kredensial yang terisi).
|
|
1323
1345
|
*/
|
|
@@ -1360,6 +1382,14 @@ declare class Buayar {
|
|
|
1360
1382
|
*/
|
|
1361
1383
|
verifyWebhook(payload: any, headers?: Record<string, string | string[] | undefined>, configOverride?: Partial<ProviderConfig>): Promise<VerifyCallbackResult>;
|
|
1362
1384
|
handleWebhook(payload: any, headers?: Record<string, string | string[] | undefined>, configOverride?: Partial<ProviderConfig>): Promise<VerifyCallbackResult>;
|
|
1385
|
+
/**
|
|
1386
|
+
* Probe payment methods yang benar-benar aktif di akun merchant gateway.
|
|
1387
|
+
*/
|
|
1388
|
+
probePaymentMethods(configOverride?: Partial<ProviderConfig>): Promise<{
|
|
1389
|
+
success: boolean;
|
|
1390
|
+
enabled: string[];
|
|
1391
|
+
error?: string;
|
|
1392
|
+
}>;
|
|
1363
1393
|
/**
|
|
1364
1394
|
* Unified Refund — berlaku untuk semua provider yang mendukung refund.
|
|
1365
1395
|
* Provider tanpa fitur refund mengembalikan `{ supported: false }`, bukan error.
|
package/dist/index.js
CHANGED
|
@@ -1808,7 +1808,7 @@ var IpaymuProvider = class extends BasePaymentProvider {
|
|
|
1808
1808
|
payload = {
|
|
1809
1809
|
name: customer.name,
|
|
1810
1810
|
email: customer.email,
|
|
1811
|
-
phone: customer.phone
|
|
1811
|
+
...customer.phone ? { phone: customer.phone } : {},
|
|
1812
1812
|
amount: integerAmount,
|
|
1813
1813
|
notifyUrl,
|
|
1814
1814
|
expired: 24,
|
|
@@ -1839,7 +1839,7 @@ var IpaymuProvider = class extends BasePaymentProvider {
|
|
|
1839
1839
|
referenceId: orderId,
|
|
1840
1840
|
buyerName: customer.name,
|
|
1841
1841
|
buyerEmail: customer.email,
|
|
1842
|
-
buyerPhone: customer.phone
|
|
1842
|
+
...customer.phone ? { buyerPhone: customer.phone } : {},
|
|
1843
1843
|
...feeDirection ? { feeDirection } : {},
|
|
1844
1844
|
...escrow !== void 0 ? { escrow } : {},
|
|
1845
1845
|
...subAccount ? { account: subAccount } : {},
|
|
@@ -2069,6 +2069,28 @@ var IpaymuProvider = class extends BasePaymentProvider {
|
|
|
2069
2069
|
};
|
|
2070
2070
|
}
|
|
2071
2071
|
}
|
|
2072
|
+
async probePaymentMethods(config) {
|
|
2073
|
+
try {
|
|
2074
|
+
const res = await this.getPaymentMethods({ amount: 1e4 }, config);
|
|
2075
|
+
if (res.success && res.methods) {
|
|
2076
|
+
return {
|
|
2077
|
+
success: true,
|
|
2078
|
+
enabled: res.methods.map((m) => m.paymentMethod)
|
|
2079
|
+
};
|
|
2080
|
+
}
|
|
2081
|
+
return {
|
|
2082
|
+
success: false,
|
|
2083
|
+
enabled: [],
|
|
2084
|
+
error: res.error || "Failed to probe iPaymu payment methods"
|
|
2085
|
+
};
|
|
2086
|
+
} catch (e) {
|
|
2087
|
+
return {
|
|
2088
|
+
success: false,
|
|
2089
|
+
enabled: [],
|
|
2090
|
+
error: e.message || "Failed to probe iPaymu payment methods"
|
|
2091
|
+
};
|
|
2092
|
+
}
|
|
2093
|
+
}
|
|
2072
2094
|
async checkTransaction(params, config) {
|
|
2073
2095
|
const { merchantOrderId } = params;
|
|
2074
2096
|
const va = config.merchantCode || config.merchantId || "";
|
|
@@ -2356,8 +2378,8 @@ var XenditProvider = class extends BasePaymentProvider {
|
|
|
2356
2378
|
const status = isPaid ? "paid" : isPending ? "pending" : isExpired ? "expired" : "failed";
|
|
2357
2379
|
const webhookToken = config.extra?.webhookToken;
|
|
2358
2380
|
const headerToken = config.extra?.callbackToken || config.extra?.headers?.["x-callback-token"] || config.extra?.headers?.["X-Callback-Token"];
|
|
2359
|
-
let isValid =
|
|
2360
|
-
if (webhookToken
|
|
2381
|
+
let isValid = false;
|
|
2382
|
+
if (webhookToken && headerToken) {
|
|
2361
2383
|
isValid = verifyXenditWebhookToken(headerToken, webhookToken);
|
|
2362
2384
|
}
|
|
2363
2385
|
return {
|
|
@@ -2505,6 +2527,75 @@ var XenditProvider = class extends BasePaymentProvider {
|
|
|
2505
2527
|
category: "Paylater / Cicilan"
|
|
2506
2528
|
}
|
|
2507
2529
|
];
|
|
2530
|
+
const apiKey = config.apiKey || config.serverKey || config.secretKey || "";
|
|
2531
|
+
if (apiKey) {
|
|
2532
|
+
try {
|
|
2533
|
+
const authHeader = getXenditAuthHeader(apiKey);
|
|
2534
|
+
const response = await fetch(`${this.getBaseUrl()}/payment_channels`, {
|
|
2535
|
+
method: "GET",
|
|
2536
|
+
headers: {
|
|
2537
|
+
"Authorization": authHeader,
|
|
2538
|
+
"Content-Type": "application/json"
|
|
2539
|
+
}
|
|
2540
|
+
});
|
|
2541
|
+
if (response.ok) {
|
|
2542
|
+
const channels = await response.json();
|
|
2543
|
+
if (Array.isArray(channels) && channels.length > 0) {
|
|
2544
|
+
const dynamicMethods = [];
|
|
2545
|
+
for (const ch of channels) {
|
|
2546
|
+
if (ch.status && ch.status !== "ACTIVE") continue;
|
|
2547
|
+
const codeLower = (ch.channel_code || "").toLowerCase();
|
|
2548
|
+
let canonicalCode = codeLower;
|
|
2549
|
+
let category = "Virtual Account";
|
|
2550
|
+
if (ch.type === "BANK_TRANSFER" || codeLower.endsWith("_va") || ["bca", "bni", "bri", "mandiri", "permata", "cimb", "bsi"].includes(codeLower)) {
|
|
2551
|
+
canonicalCode = codeLower.endsWith("_va") ? codeLower : `${codeLower}_va`;
|
|
2552
|
+
category = "Virtual Account";
|
|
2553
|
+
} else if (ch.type === "EWALLET" || ["ovo", "dana", "linkaja", "shopeepay", "gopay"].includes(codeLower)) {
|
|
2554
|
+
canonicalCode = codeLower;
|
|
2555
|
+
category = "E-Wallet";
|
|
2556
|
+
} else if (ch.type === "QR_CODE" || codeLower === "qris") {
|
|
2557
|
+
canonicalCode = "qris";
|
|
2558
|
+
category = "QRIS";
|
|
2559
|
+
} else if (ch.type === "RETAIL_OUTLET" || ["alfamart", "indomaret"].includes(codeLower)) {
|
|
2560
|
+
canonicalCode = codeLower;
|
|
2561
|
+
category = "Retail / Gerai";
|
|
2562
|
+
} else if (ch.type === "CARD") {
|
|
2563
|
+
canonicalCode = "credit_card";
|
|
2564
|
+
category = "Kartu Kredit";
|
|
2565
|
+
} else if (ch.type === "PAYLATER" || ["kredivo", "akulaku", "indodana"].includes(codeLower)) {
|
|
2566
|
+
canonicalCode = codeLower;
|
|
2567
|
+
category = "Paylater / Cicilan";
|
|
2568
|
+
}
|
|
2569
|
+
dynamicMethods.push({
|
|
2570
|
+
paymentMethod: canonicalCode,
|
|
2571
|
+
code: ch.channel_code || canonicalCode,
|
|
2572
|
+
paymentName: ch.display_name || ch.name || ch.channel_code,
|
|
2573
|
+
paymentImage: `https://xendit.co/icons/${codeLower}.png`,
|
|
2574
|
+
totalFee: ch.fee ? `${ch.fee}` : "",
|
|
2575
|
+
category,
|
|
2576
|
+
coming_soon: false,
|
|
2577
|
+
extra: ch
|
|
2578
|
+
});
|
|
2579
|
+
}
|
|
2580
|
+
if (dynamicMethods.length > 0) {
|
|
2581
|
+
const categories2 = {};
|
|
2582
|
+
for (const item of dynamicMethods) {
|
|
2583
|
+
if (!categories2[item.category]) categories2[item.category] = [];
|
|
2584
|
+
categories2[item.category].push(item);
|
|
2585
|
+
}
|
|
2586
|
+
return {
|
|
2587
|
+
success: true,
|
|
2588
|
+
provider: "xendit",
|
|
2589
|
+
methods: dynamicMethods,
|
|
2590
|
+
categories: categories2,
|
|
2591
|
+
rawResponse: channels
|
|
2592
|
+
};
|
|
2593
|
+
}
|
|
2594
|
+
}
|
|
2595
|
+
}
|
|
2596
|
+
} catch (e) {
|
|
2597
|
+
}
|
|
2598
|
+
}
|
|
2508
2599
|
const categories = {};
|
|
2509
2600
|
for (const item of staticMethods) {
|
|
2510
2601
|
if (!categories[item.category]) {
|
|
@@ -2520,6 +2611,28 @@ var XenditProvider = class extends BasePaymentProvider {
|
|
|
2520
2611
|
rawResponse: staticMethods
|
|
2521
2612
|
};
|
|
2522
2613
|
}
|
|
2614
|
+
async probePaymentMethods(config) {
|
|
2615
|
+
try {
|
|
2616
|
+
const res = await this.getPaymentMethods({ amount: 1e4 }, config);
|
|
2617
|
+
if (res.success && res.methods) {
|
|
2618
|
+
return {
|
|
2619
|
+
success: true,
|
|
2620
|
+
enabled: res.methods.map((m) => m.paymentMethod)
|
|
2621
|
+
};
|
|
2622
|
+
}
|
|
2623
|
+
return {
|
|
2624
|
+
success: false,
|
|
2625
|
+
enabled: [],
|
|
2626
|
+
error: res.error || "Failed to probe Xendit payment methods"
|
|
2627
|
+
};
|
|
2628
|
+
} catch (e) {
|
|
2629
|
+
return {
|
|
2630
|
+
success: false,
|
|
2631
|
+
enabled: [],
|
|
2632
|
+
error: e.message || "Failed to probe Xendit payment methods"
|
|
2633
|
+
};
|
|
2634
|
+
}
|
|
2635
|
+
}
|
|
2523
2636
|
async checkTransaction(params, config) {
|
|
2524
2637
|
const { merchantOrderId } = params;
|
|
2525
2638
|
const apiKey = config.apiKey || config.serverKey || config.secretKey || "";
|
|
@@ -3316,7 +3429,7 @@ var DokuProvider = class extends BasePaymentProvider {
|
|
|
3316
3429
|
const secretKey = config.secretKey || config.apiKey || "";
|
|
3317
3430
|
const signature = headers["signature"] || headers["Signature"] || config.extra?.dokuSignature || config.extra?.signatureHeader;
|
|
3318
3431
|
const clientId = config.merchantCode || config.clientKey || "";
|
|
3319
|
-
let isValid =
|
|
3432
|
+
let isValid = false;
|
|
3320
3433
|
if (signature || headers && (headers["request-id"] || headers["Request-Id"])) {
|
|
3321
3434
|
isValid = verifyDokuWebhookSignature(headers, body, clientId, secretKey);
|
|
3322
3435
|
}
|
|
@@ -3341,7 +3454,7 @@ var DokuProvider = class extends BasePaymentProvider {
|
|
|
3341
3454
|
verifySnapCallback(body, config, headers) {
|
|
3342
3455
|
const clientSecret = config.apiKey || config.serverKey || config.secretKey || "";
|
|
3343
3456
|
const endpointUrl = config.extra?.notificationPath || headers["x-path"] || config.extra?.headers?.["request-target"] || "/api/payment/webhook";
|
|
3344
|
-
let isValid =
|
|
3457
|
+
let isValid = false;
|
|
3345
3458
|
const incomingSig = headers["x-signature"] || headers["X-SIGNATURE"] || headers["signature"] || headers["Signature"] || "";
|
|
3346
3459
|
if (incomingSig && clientSecret) {
|
|
3347
3460
|
isValid = verifySnapWebhookSignature(headers, body, clientSecret, endpointUrl);
|
|
@@ -9669,6 +9782,18 @@ var PaymentManager = class {
|
|
|
9669
9782
|
if (provider.probePaymentMethods) {
|
|
9670
9783
|
return provider.probePaymentMethods(config);
|
|
9671
9784
|
}
|
|
9785
|
+
try {
|
|
9786
|
+
if (typeof provider.getPaymentMethods === "function") {
|
|
9787
|
+
const res = await provider.getPaymentMethods({ amount: 1e4 }, config);
|
|
9788
|
+
if (res && res.success && Array.isArray(res.methods) && res.methods.length > 0) {
|
|
9789
|
+
return {
|
|
9790
|
+
success: true,
|
|
9791
|
+
enabled: res.methods.map((m) => m.paymentMethod)
|
|
9792
|
+
};
|
|
9793
|
+
}
|
|
9794
|
+
}
|
|
9795
|
+
} catch (e) {
|
|
9796
|
+
}
|
|
9672
9797
|
return { success: false, enabled: [], error: `Provider '${providerName}' does not support payment methods probing` };
|
|
9673
9798
|
}
|
|
9674
9799
|
// ─── Unified Advanced Operations (Refund / Balance / Disburse) ─────────────
|
|
@@ -9918,7 +10043,7 @@ function buildPaymentMethodDescriptor(pm, _providerName) {
|
|
|
9918
10043
|
badge: feeDisplay ? `${badge} \u2022 Fee ${feeDisplay}` : badge,
|
|
9919
10044
|
image: pm.paymentImage || void 0,
|
|
9920
10045
|
category,
|
|
9921
|
-
coming_soon: false,
|
|
10046
|
+
coming_soon: pm.coming_soon ?? false,
|
|
9922
10047
|
totalFee: feeDisplay || void 0
|
|
9923
10048
|
};
|
|
9924
10049
|
}
|
|
@@ -9968,10 +10093,26 @@ var ProviderRegistry = class {
|
|
|
9968
10093
|
return ties.length === 1 ? top.name : void 0;
|
|
9969
10094
|
}
|
|
9970
10095
|
/**
|
|
9971
|
-
* Autodetect provider dari struktur payload webhook.
|
|
10096
|
+
* Autodetect provider dari struktur payload webhook dan header.
|
|
9972
10097
|
* Mengembalikan nama provider jika dikenali, else undefined.
|
|
9973
10098
|
*/
|
|
9974
|
-
detectFromWebhook(payload) {
|
|
10099
|
+
detectFromWebhook(payload, headers) {
|
|
10100
|
+
if (headers) {
|
|
10101
|
+
const h = {};
|
|
10102
|
+
for (const k of Object.keys(headers)) {
|
|
10103
|
+
h[k.toLowerCase()] = headers[k];
|
|
10104
|
+
}
|
|
10105
|
+
if (h["stripe-signature"]) return "stripe";
|
|
10106
|
+
if (h["x-callback-token"]) return "xendit";
|
|
10107
|
+
if (h["x-razorpay-signature"]) return "razorpay";
|
|
10108
|
+
if (h["cko-signature"]) return "checkoutcom";
|
|
10109
|
+
if (h["openpayu-signature"]) return "payu";
|
|
10110
|
+
if (h["x-square-hmacsha256-signature"] || h["x-square-signature"]) return "square";
|
|
10111
|
+
if (h["bt_signature"]) return "braintree";
|
|
10112
|
+
if (h["x-oy-username"]) return "oy";
|
|
10113
|
+
if (h["signature"] && (h["client-id"] || h["request-id"])) return "doku";
|
|
10114
|
+
if (h["x-signature"] && (payload?.trx_id || payload?.via || payload?.sid)) return "ipaymu";
|
|
10115
|
+
}
|
|
9975
10116
|
if (!payload) return void 0;
|
|
9976
10117
|
const p = payload;
|
|
9977
10118
|
if (p.signature_key && p.transaction_status) return "midtrans";
|
|
@@ -9985,7 +10126,9 @@ var ProviderRegistry = class {
|
|
|
9985
10126
|
if (p.merchant_id && p.order_id && p.signature) return "prismalink";
|
|
9986
10127
|
if (p.object === "event" || p.type && p.data?.object && p.api_version) return "stripe";
|
|
9987
10128
|
if (p.event && p.payload?.payment?.entity) return "razorpay";
|
|
9988
|
-
if (p.external_id || p.event?.startsWith("payment.") || p.event?.startsWith("qr.") || p.data?.reference_id)
|
|
10129
|
+
if (p.external_id && (p.status || p.paid_amount || p.payment_method || p.payment_channel || p.id) || p.event?.startsWith("payment.") || p.event?.startsWith("qr.") || p.data?.reference_id) {
|
|
10130
|
+
return "xendit";
|
|
10131
|
+
}
|
|
9989
10132
|
if (p.event_type && p.resource && (p.event_type.startsWith("PAYMENT.") || p.event_type.startsWith("CHECKOUT.ORDER."))) return "paypal";
|
|
9990
10133
|
if (p.notificationItems || p.merchantAccountCode && p.pspReference && p.eventCode) return "adyen";
|
|
9991
10134
|
if (p.type && p.data?._links && (p.type.startsWith("payment_") || p.type.startsWith("refund_"))) return "checkoutcom";
|
|
@@ -10339,10 +10482,10 @@ var Buayar = class {
|
|
|
10339
10482
|
return { methods: desc.methods, operations: desc.operations };
|
|
10340
10483
|
}
|
|
10341
10484
|
/**
|
|
10342
|
-
* Deteksi nama provider dari struktur payload webhook.
|
|
10485
|
+
* Deteksi nama provider dari struktur payload webhook dan opsional headers.
|
|
10343
10486
|
*/
|
|
10344
|
-
detectProviderFromPayload(payload) {
|
|
10345
|
-
return this.registry.detectFromWebhook(payload);
|
|
10487
|
+
detectProviderFromPayload(payload, headers) {
|
|
10488
|
+
return this.registry.detectFromWebhook(payload, headers);
|
|
10346
10489
|
}
|
|
10347
10490
|
/**
|
|
10348
10491
|
* Deteksi nama provider aktif dari variabel lingkungan (kredensial yang terisi).
|
|
@@ -10471,14 +10614,39 @@ var Buayar = class {
|
|
|
10471
10614
|
mergedConfig.extra.oyUsername = Array.isArray(oyUser) ? oyUser[0] : oyUser;
|
|
10472
10615
|
}
|
|
10473
10616
|
}
|
|
10474
|
-
let providerName = configOverride?.provider
|
|
10475
|
-
|
|
10476
|
-
|
|
10617
|
+
let providerName = configOverride?.provider !== void 0 ? configOverride.provider : this.provider;
|
|
10618
|
+
if (!providerName) {
|
|
10619
|
+
const detected = this.registry.detectFromWebhook(payload, headers);
|
|
10620
|
+
if (detected) providerName = detected;
|
|
10621
|
+
}
|
|
10622
|
+
if (!providerName) {
|
|
10623
|
+
return {
|
|
10624
|
+
isValid: false,
|
|
10625
|
+
isPaid: false,
|
|
10626
|
+
isPending: false,
|
|
10627
|
+
isFailed: true,
|
|
10628
|
+
isExpired: false,
|
|
10629
|
+
status: "failed",
|
|
10630
|
+
orderId: "",
|
|
10631
|
+
amount: 0,
|
|
10632
|
+
provider: "unknown",
|
|
10633
|
+
error: "Unable to detect payment provider for webhook. Please specify provider in configuration or override.",
|
|
10634
|
+
rawPayload: payload
|
|
10635
|
+
};
|
|
10636
|
+
}
|
|
10477
10637
|
return this.manager.verifyCallback(providerName, payload, mergedConfig);
|
|
10478
10638
|
}
|
|
10479
10639
|
async handleWebhook(payload, headers, configOverride) {
|
|
10480
10640
|
return this.verifyWebhook(payload, headers, configOverride);
|
|
10481
10641
|
}
|
|
10642
|
+
/**
|
|
10643
|
+
* Probe payment methods yang benar-benar aktif di akun merchant gateway.
|
|
10644
|
+
*/
|
|
10645
|
+
async probePaymentMethods(configOverride) {
|
|
10646
|
+
const mergedConfig = { ...this.config, ...configOverride };
|
|
10647
|
+
const providerName = configOverride?.provider || this.provider;
|
|
10648
|
+
return this.manager.probePaymentMethods(providerName, mergedConfig);
|
|
10649
|
+
}
|
|
10482
10650
|
/**
|
|
10483
10651
|
* Unified Refund — berlaku untuk semua provider yang mendukung refund.
|
|
10484
10652
|
* Provider tanpa fitur refund mengembalikan `{ supported: false }`, bukan error.
|
package/dist/index.mjs
CHANGED
|
@@ -1638,7 +1638,7 @@ var IpaymuProvider = class extends BasePaymentProvider {
|
|
|
1638
1638
|
payload = {
|
|
1639
1639
|
name: customer.name,
|
|
1640
1640
|
email: customer.email,
|
|
1641
|
-
phone: customer.phone
|
|
1641
|
+
...customer.phone ? { phone: customer.phone } : {},
|
|
1642
1642
|
amount: integerAmount,
|
|
1643
1643
|
notifyUrl,
|
|
1644
1644
|
expired: 24,
|
|
@@ -1669,7 +1669,7 @@ var IpaymuProvider = class extends BasePaymentProvider {
|
|
|
1669
1669
|
referenceId: orderId,
|
|
1670
1670
|
buyerName: customer.name,
|
|
1671
1671
|
buyerEmail: customer.email,
|
|
1672
|
-
buyerPhone: customer.phone
|
|
1672
|
+
...customer.phone ? { buyerPhone: customer.phone } : {},
|
|
1673
1673
|
...feeDirection ? { feeDirection } : {},
|
|
1674
1674
|
...escrow !== void 0 ? { escrow } : {},
|
|
1675
1675
|
...subAccount ? { account: subAccount } : {},
|
|
@@ -1899,6 +1899,28 @@ var IpaymuProvider = class extends BasePaymentProvider {
|
|
|
1899
1899
|
};
|
|
1900
1900
|
}
|
|
1901
1901
|
}
|
|
1902
|
+
async probePaymentMethods(config) {
|
|
1903
|
+
try {
|
|
1904
|
+
const res = await this.getPaymentMethods({ amount: 1e4 }, config);
|
|
1905
|
+
if (res.success && res.methods) {
|
|
1906
|
+
return {
|
|
1907
|
+
success: true,
|
|
1908
|
+
enabled: res.methods.map((m) => m.paymentMethod)
|
|
1909
|
+
};
|
|
1910
|
+
}
|
|
1911
|
+
return {
|
|
1912
|
+
success: false,
|
|
1913
|
+
enabled: [],
|
|
1914
|
+
error: res.error || "Failed to probe iPaymu payment methods"
|
|
1915
|
+
};
|
|
1916
|
+
} catch (e) {
|
|
1917
|
+
return {
|
|
1918
|
+
success: false,
|
|
1919
|
+
enabled: [],
|
|
1920
|
+
error: e.message || "Failed to probe iPaymu payment methods"
|
|
1921
|
+
};
|
|
1922
|
+
}
|
|
1923
|
+
}
|
|
1902
1924
|
async checkTransaction(params, config) {
|
|
1903
1925
|
const { merchantOrderId } = params;
|
|
1904
1926
|
const va = config.merchantCode || config.merchantId || "";
|
|
@@ -2186,8 +2208,8 @@ var XenditProvider = class extends BasePaymentProvider {
|
|
|
2186
2208
|
const status = isPaid ? "paid" : isPending ? "pending" : isExpired ? "expired" : "failed";
|
|
2187
2209
|
const webhookToken = config.extra?.webhookToken;
|
|
2188
2210
|
const headerToken = config.extra?.callbackToken || config.extra?.headers?.["x-callback-token"] || config.extra?.headers?.["X-Callback-Token"];
|
|
2189
|
-
let isValid =
|
|
2190
|
-
if (webhookToken
|
|
2211
|
+
let isValid = false;
|
|
2212
|
+
if (webhookToken && headerToken) {
|
|
2191
2213
|
isValid = verifyXenditWebhookToken(headerToken, webhookToken);
|
|
2192
2214
|
}
|
|
2193
2215
|
return {
|
|
@@ -2335,6 +2357,75 @@ var XenditProvider = class extends BasePaymentProvider {
|
|
|
2335
2357
|
category: "Paylater / Cicilan"
|
|
2336
2358
|
}
|
|
2337
2359
|
];
|
|
2360
|
+
const apiKey = config.apiKey || config.serverKey || config.secretKey || "";
|
|
2361
|
+
if (apiKey) {
|
|
2362
|
+
try {
|
|
2363
|
+
const authHeader = getXenditAuthHeader(apiKey);
|
|
2364
|
+
const response = await fetch(`${this.getBaseUrl()}/payment_channels`, {
|
|
2365
|
+
method: "GET",
|
|
2366
|
+
headers: {
|
|
2367
|
+
"Authorization": authHeader,
|
|
2368
|
+
"Content-Type": "application/json"
|
|
2369
|
+
}
|
|
2370
|
+
});
|
|
2371
|
+
if (response.ok) {
|
|
2372
|
+
const channels = await response.json();
|
|
2373
|
+
if (Array.isArray(channels) && channels.length > 0) {
|
|
2374
|
+
const dynamicMethods = [];
|
|
2375
|
+
for (const ch of channels) {
|
|
2376
|
+
if (ch.status && ch.status !== "ACTIVE") continue;
|
|
2377
|
+
const codeLower = (ch.channel_code || "").toLowerCase();
|
|
2378
|
+
let canonicalCode = codeLower;
|
|
2379
|
+
let category = "Virtual Account";
|
|
2380
|
+
if (ch.type === "BANK_TRANSFER" || codeLower.endsWith("_va") || ["bca", "bni", "bri", "mandiri", "permata", "cimb", "bsi"].includes(codeLower)) {
|
|
2381
|
+
canonicalCode = codeLower.endsWith("_va") ? codeLower : `${codeLower}_va`;
|
|
2382
|
+
category = "Virtual Account";
|
|
2383
|
+
} else if (ch.type === "EWALLET" || ["ovo", "dana", "linkaja", "shopeepay", "gopay"].includes(codeLower)) {
|
|
2384
|
+
canonicalCode = codeLower;
|
|
2385
|
+
category = "E-Wallet";
|
|
2386
|
+
} else if (ch.type === "QR_CODE" || codeLower === "qris") {
|
|
2387
|
+
canonicalCode = "qris";
|
|
2388
|
+
category = "QRIS";
|
|
2389
|
+
} else if (ch.type === "RETAIL_OUTLET" || ["alfamart", "indomaret"].includes(codeLower)) {
|
|
2390
|
+
canonicalCode = codeLower;
|
|
2391
|
+
category = "Retail / Gerai";
|
|
2392
|
+
} else if (ch.type === "CARD") {
|
|
2393
|
+
canonicalCode = "credit_card";
|
|
2394
|
+
category = "Kartu Kredit";
|
|
2395
|
+
} else if (ch.type === "PAYLATER" || ["kredivo", "akulaku", "indodana"].includes(codeLower)) {
|
|
2396
|
+
canonicalCode = codeLower;
|
|
2397
|
+
category = "Paylater / Cicilan";
|
|
2398
|
+
}
|
|
2399
|
+
dynamicMethods.push({
|
|
2400
|
+
paymentMethod: canonicalCode,
|
|
2401
|
+
code: ch.channel_code || canonicalCode,
|
|
2402
|
+
paymentName: ch.display_name || ch.name || ch.channel_code,
|
|
2403
|
+
paymentImage: `https://xendit.co/icons/${codeLower}.png`,
|
|
2404
|
+
totalFee: ch.fee ? `${ch.fee}` : "",
|
|
2405
|
+
category,
|
|
2406
|
+
coming_soon: false,
|
|
2407
|
+
extra: ch
|
|
2408
|
+
});
|
|
2409
|
+
}
|
|
2410
|
+
if (dynamicMethods.length > 0) {
|
|
2411
|
+
const categories2 = {};
|
|
2412
|
+
for (const item of dynamicMethods) {
|
|
2413
|
+
if (!categories2[item.category]) categories2[item.category] = [];
|
|
2414
|
+
categories2[item.category].push(item);
|
|
2415
|
+
}
|
|
2416
|
+
return {
|
|
2417
|
+
success: true,
|
|
2418
|
+
provider: "xendit",
|
|
2419
|
+
methods: dynamicMethods,
|
|
2420
|
+
categories: categories2,
|
|
2421
|
+
rawResponse: channels
|
|
2422
|
+
};
|
|
2423
|
+
}
|
|
2424
|
+
}
|
|
2425
|
+
}
|
|
2426
|
+
} catch (e) {
|
|
2427
|
+
}
|
|
2428
|
+
}
|
|
2338
2429
|
const categories = {};
|
|
2339
2430
|
for (const item of staticMethods) {
|
|
2340
2431
|
if (!categories[item.category]) {
|
|
@@ -2350,6 +2441,28 @@ var XenditProvider = class extends BasePaymentProvider {
|
|
|
2350
2441
|
rawResponse: staticMethods
|
|
2351
2442
|
};
|
|
2352
2443
|
}
|
|
2444
|
+
async probePaymentMethods(config) {
|
|
2445
|
+
try {
|
|
2446
|
+
const res = await this.getPaymentMethods({ amount: 1e4 }, config);
|
|
2447
|
+
if (res.success && res.methods) {
|
|
2448
|
+
return {
|
|
2449
|
+
success: true,
|
|
2450
|
+
enabled: res.methods.map((m) => m.paymentMethod)
|
|
2451
|
+
};
|
|
2452
|
+
}
|
|
2453
|
+
return {
|
|
2454
|
+
success: false,
|
|
2455
|
+
enabled: [],
|
|
2456
|
+
error: res.error || "Failed to probe Xendit payment methods"
|
|
2457
|
+
};
|
|
2458
|
+
} catch (e) {
|
|
2459
|
+
return {
|
|
2460
|
+
success: false,
|
|
2461
|
+
enabled: [],
|
|
2462
|
+
error: e.message || "Failed to probe Xendit payment methods"
|
|
2463
|
+
};
|
|
2464
|
+
}
|
|
2465
|
+
}
|
|
2353
2466
|
async checkTransaction(params, config) {
|
|
2354
2467
|
const { merchantOrderId } = params;
|
|
2355
2468
|
const apiKey = config.apiKey || config.serverKey || config.secretKey || "";
|
|
@@ -3146,7 +3259,7 @@ var DokuProvider = class extends BasePaymentProvider {
|
|
|
3146
3259
|
const secretKey = config.secretKey || config.apiKey || "";
|
|
3147
3260
|
const signature = headers["signature"] || headers["Signature"] || config.extra?.dokuSignature || config.extra?.signatureHeader;
|
|
3148
3261
|
const clientId = config.merchantCode || config.clientKey || "";
|
|
3149
|
-
let isValid =
|
|
3262
|
+
let isValid = false;
|
|
3150
3263
|
if (signature || headers && (headers["request-id"] || headers["Request-Id"])) {
|
|
3151
3264
|
isValid = verifyDokuWebhookSignature(headers, body, clientId, secretKey);
|
|
3152
3265
|
}
|
|
@@ -3171,7 +3284,7 @@ var DokuProvider = class extends BasePaymentProvider {
|
|
|
3171
3284
|
verifySnapCallback(body, config, headers) {
|
|
3172
3285
|
const clientSecret = config.apiKey || config.serverKey || config.secretKey || "";
|
|
3173
3286
|
const endpointUrl = config.extra?.notificationPath || headers["x-path"] || config.extra?.headers?.["request-target"] || "/api/payment/webhook";
|
|
3174
|
-
let isValid =
|
|
3287
|
+
let isValid = false;
|
|
3175
3288
|
const incomingSig = headers["x-signature"] || headers["X-SIGNATURE"] || headers["signature"] || headers["Signature"] || "";
|
|
3176
3289
|
if (incomingSig && clientSecret) {
|
|
3177
3290
|
isValid = verifySnapWebhookSignature(headers, body, clientSecret, endpointUrl);
|
|
@@ -9499,6 +9612,18 @@ var PaymentManager = class {
|
|
|
9499
9612
|
if (provider.probePaymentMethods) {
|
|
9500
9613
|
return provider.probePaymentMethods(config);
|
|
9501
9614
|
}
|
|
9615
|
+
try {
|
|
9616
|
+
if (typeof provider.getPaymentMethods === "function") {
|
|
9617
|
+
const res = await provider.getPaymentMethods({ amount: 1e4 }, config);
|
|
9618
|
+
if (res && res.success && Array.isArray(res.methods) && res.methods.length > 0) {
|
|
9619
|
+
return {
|
|
9620
|
+
success: true,
|
|
9621
|
+
enabled: res.methods.map((m) => m.paymentMethod)
|
|
9622
|
+
};
|
|
9623
|
+
}
|
|
9624
|
+
}
|
|
9625
|
+
} catch (e) {
|
|
9626
|
+
}
|
|
9502
9627
|
return { success: false, enabled: [], error: `Provider '${providerName}' does not support payment methods probing` };
|
|
9503
9628
|
}
|
|
9504
9629
|
// ─── Unified Advanced Operations (Refund / Balance / Disburse) ─────────────
|
|
@@ -9748,7 +9873,7 @@ function buildPaymentMethodDescriptor(pm, _providerName) {
|
|
|
9748
9873
|
badge: feeDisplay ? `${badge} \u2022 Fee ${feeDisplay}` : badge,
|
|
9749
9874
|
image: pm.paymentImage || void 0,
|
|
9750
9875
|
category,
|
|
9751
|
-
coming_soon: false,
|
|
9876
|
+
coming_soon: pm.coming_soon ?? false,
|
|
9752
9877
|
totalFee: feeDisplay || void 0
|
|
9753
9878
|
};
|
|
9754
9879
|
}
|
|
@@ -9798,10 +9923,26 @@ var ProviderRegistry = class {
|
|
|
9798
9923
|
return ties.length === 1 ? top.name : void 0;
|
|
9799
9924
|
}
|
|
9800
9925
|
/**
|
|
9801
|
-
* Autodetect provider dari struktur payload webhook.
|
|
9926
|
+
* Autodetect provider dari struktur payload webhook dan header.
|
|
9802
9927
|
* Mengembalikan nama provider jika dikenali, else undefined.
|
|
9803
9928
|
*/
|
|
9804
|
-
detectFromWebhook(payload) {
|
|
9929
|
+
detectFromWebhook(payload, headers) {
|
|
9930
|
+
if (headers) {
|
|
9931
|
+
const h = {};
|
|
9932
|
+
for (const k of Object.keys(headers)) {
|
|
9933
|
+
h[k.toLowerCase()] = headers[k];
|
|
9934
|
+
}
|
|
9935
|
+
if (h["stripe-signature"]) return "stripe";
|
|
9936
|
+
if (h["x-callback-token"]) return "xendit";
|
|
9937
|
+
if (h["x-razorpay-signature"]) return "razorpay";
|
|
9938
|
+
if (h["cko-signature"]) return "checkoutcom";
|
|
9939
|
+
if (h["openpayu-signature"]) return "payu";
|
|
9940
|
+
if (h["x-square-hmacsha256-signature"] || h["x-square-signature"]) return "square";
|
|
9941
|
+
if (h["bt_signature"]) return "braintree";
|
|
9942
|
+
if (h["x-oy-username"]) return "oy";
|
|
9943
|
+
if (h["signature"] && (h["client-id"] || h["request-id"])) return "doku";
|
|
9944
|
+
if (h["x-signature"] && (payload?.trx_id || payload?.via || payload?.sid)) return "ipaymu";
|
|
9945
|
+
}
|
|
9805
9946
|
if (!payload) return void 0;
|
|
9806
9947
|
const p = payload;
|
|
9807
9948
|
if (p.signature_key && p.transaction_status) return "midtrans";
|
|
@@ -9815,7 +9956,9 @@ var ProviderRegistry = class {
|
|
|
9815
9956
|
if (p.merchant_id && p.order_id && p.signature) return "prismalink";
|
|
9816
9957
|
if (p.object === "event" || p.type && p.data?.object && p.api_version) return "stripe";
|
|
9817
9958
|
if (p.event && p.payload?.payment?.entity) return "razorpay";
|
|
9818
|
-
if (p.external_id || p.event?.startsWith("payment.") || p.event?.startsWith("qr.") || p.data?.reference_id)
|
|
9959
|
+
if (p.external_id && (p.status || p.paid_amount || p.payment_method || p.payment_channel || p.id) || p.event?.startsWith("payment.") || p.event?.startsWith("qr.") || p.data?.reference_id) {
|
|
9960
|
+
return "xendit";
|
|
9961
|
+
}
|
|
9819
9962
|
if (p.event_type && p.resource && (p.event_type.startsWith("PAYMENT.") || p.event_type.startsWith("CHECKOUT.ORDER."))) return "paypal";
|
|
9820
9963
|
if (p.notificationItems || p.merchantAccountCode && p.pspReference && p.eventCode) return "adyen";
|
|
9821
9964
|
if (p.type && p.data?._links && (p.type.startsWith("payment_") || p.type.startsWith("refund_"))) return "checkoutcom";
|
|
@@ -10169,10 +10312,10 @@ var Buayar = class {
|
|
|
10169
10312
|
return { methods: desc.methods, operations: desc.operations };
|
|
10170
10313
|
}
|
|
10171
10314
|
/**
|
|
10172
|
-
* Deteksi nama provider dari struktur payload webhook.
|
|
10315
|
+
* Deteksi nama provider dari struktur payload webhook dan opsional headers.
|
|
10173
10316
|
*/
|
|
10174
|
-
detectProviderFromPayload(payload) {
|
|
10175
|
-
return this.registry.detectFromWebhook(payload);
|
|
10317
|
+
detectProviderFromPayload(payload, headers) {
|
|
10318
|
+
return this.registry.detectFromWebhook(payload, headers);
|
|
10176
10319
|
}
|
|
10177
10320
|
/**
|
|
10178
10321
|
* Deteksi nama provider aktif dari variabel lingkungan (kredensial yang terisi).
|
|
@@ -10301,14 +10444,39 @@ var Buayar = class {
|
|
|
10301
10444
|
mergedConfig.extra.oyUsername = Array.isArray(oyUser) ? oyUser[0] : oyUser;
|
|
10302
10445
|
}
|
|
10303
10446
|
}
|
|
10304
|
-
let providerName = configOverride?.provider
|
|
10305
|
-
|
|
10306
|
-
|
|
10447
|
+
let providerName = configOverride?.provider !== void 0 ? configOverride.provider : this.provider;
|
|
10448
|
+
if (!providerName) {
|
|
10449
|
+
const detected = this.registry.detectFromWebhook(payload, headers);
|
|
10450
|
+
if (detected) providerName = detected;
|
|
10451
|
+
}
|
|
10452
|
+
if (!providerName) {
|
|
10453
|
+
return {
|
|
10454
|
+
isValid: false,
|
|
10455
|
+
isPaid: false,
|
|
10456
|
+
isPending: false,
|
|
10457
|
+
isFailed: true,
|
|
10458
|
+
isExpired: false,
|
|
10459
|
+
status: "failed",
|
|
10460
|
+
orderId: "",
|
|
10461
|
+
amount: 0,
|
|
10462
|
+
provider: "unknown",
|
|
10463
|
+
error: "Unable to detect payment provider for webhook. Please specify provider in configuration or override.",
|
|
10464
|
+
rawPayload: payload
|
|
10465
|
+
};
|
|
10466
|
+
}
|
|
10307
10467
|
return this.manager.verifyCallback(providerName, payload, mergedConfig);
|
|
10308
10468
|
}
|
|
10309
10469
|
async handleWebhook(payload, headers, configOverride) {
|
|
10310
10470
|
return this.verifyWebhook(payload, headers, configOverride);
|
|
10311
10471
|
}
|
|
10472
|
+
/**
|
|
10473
|
+
* Probe payment methods yang benar-benar aktif di akun merchant gateway.
|
|
10474
|
+
*/
|
|
10475
|
+
async probePaymentMethods(configOverride) {
|
|
10476
|
+
const mergedConfig = { ...this.config, ...configOverride };
|
|
10477
|
+
const providerName = configOverride?.provider || this.provider;
|
|
10478
|
+
return this.manager.probePaymentMethods(providerName, mergedConfig);
|
|
10479
|
+
}
|
|
10312
10480
|
/**
|
|
10313
10481
|
* Unified Refund — berlaku untuk semua provider yang mendukung refund.
|
|
10314
10482
|
* Provider tanpa fitur refund mengembalikan `{ supported: false }`, bukan error.
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
# Audit Bug & Fitur Premature — SDK Buayar v0.8.5
|
|
2
|
+
|
|
3
|
+
> **Repo:** `/Buayar` (package `@crediblemark/buayar`, versi 0.8.5)
|
|
4
|
+
> **Tanggal audit:** 2026-09-05
|
|
5
|
+
> **Status:** **SUDAH DIPERBAIKI & DIVALIDASI** sesuai dokumentasi resmi PG (161/161 test passed).
|
|
6
|
+
|
|
7
|
+
Laporan ini merangkum bug dan bagian "premature" (fitur yang tampak tersedia di API/types namun
|
|
8
|
+
perilaku aktualnya belum lengkap/benar) yang ditemukan saat menelusuri SDK, termasuk dampaknya bagi
|
|
9
|
+
konsumen utama SDK: aplikasi **SitusBisnis** (`BUAYAR_PROVIDER=ipaymu`, sandbox).
|
|
10
|
+
Seluruh temuan critical (S1, S2) dan high (S3, S4, S5, S6) telah ditangani dan divalidasi dengan dokumentasi resmi PG.
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## I. Ringkasan Eksekutif & Status Perbaikan
|
|
15
|
+
|
|
16
|
+
| # | Severity | Jenis | Lokasi | Ringkasan Masalah | Status Perbaikan |
|
|
17
|
+
|---|:--:|---|---|---|:---:|
|
|
18
|
+
| S1 | 🔴 Critical | Bug keamanan | `providers/doku/provider.ts:524` & `:561` | `verifyCallback` & `verifySnapCallback` default `isValid = true` bila tanpa signature | ✅ **FIXED** (default `isValid = false`, wajib valid signature) |
|
|
19
|
+
| S2 | 🔴 Critical | Bug keamanan | `providers/xendit/provider.ts:242` | `verifyCallback` default `isValid = true` bila tanpa token | ✅ **FIXED** (default `isValid = false`, wajib match token) |
|
|
20
|
+
| S3 | 🟠 High | Bug kontrak | `providers/ipaymu/provider.ts:50,84` | `phone` di-fallback ke string hardcode `"081234567890"` | ✅ **FIXED** (phone dijadikan opsional per docs resmi iPaymu) |
|
|
21
|
+
| S4 | 🟠 High | Risk integrasi | `providers/ipaymu/provider.ts:351` (checkTransaction) | Poll status mengirim `order_number` sebagai `transactionId`; kontrak `/transaction` iPaymu | ✅ **VALIDATED** (kontrak resmi iPaymu `/transaction` hanya terima numeric `transactionId`; JSDoc & dokumentasi diperjelas) |
|
|
22
|
+
| S5 | 🟠 High | Risk integrasi | `providers/ipaymu/provider.ts:190` | `orderId` callback diambil dari `reference_id` | ✅ **VALIDATED** (docs resmi iPaymu mengirim `reference_id` merchant) |
|
|
23
|
+
| S6 | 🟡 Medium | Premature | `core/descriptor.ts:90` | `coming_soon` selalu di-hardcode `false` | ✅ **FIXED** (baca `raw.coming_soon ?? raw.is_coming_soon ?? false`) |
|
|
24
|
+
| S7 | 🟡 Medium | Premature | beberapa provider `getPaymentMethods` | Daftar channel Midtrans/Xendit dll. adalah statis | ✅ **FIXED** (Xendit query `/payment_channels` live; fallback aman) |
|
|
25
|
+
| S8 | 🟡 Medium | Premature | `core/manager.ts:297` | `probePaymentMethods` sebagian besar fallback | ✅ **FIXED** (implementasi di iPaymu & Xendit + fallback dinamis di manager & facade) |
|
|
26
|
+
| S9 | 🟡 Medium | Premature | `core/providerRegistry.ts:83-92` | `detectFromWebhook` auto-detect ambigu | ✅ **FIXED** (prioritas header, payload diperketat, penanganan aman tanpa crash) |
|
|
27
|
+
|
|
28
|
+
---
|
|
29
|
+
|
|
30
|
+
## II. Bug & Hasil Perbaikan
|
|
31
|
+
|
|
32
|
+
### S1. DOKU — verifikasi webhook default `isValid = true` (Critical keamanan) — ✅ FIXED
|
|
33
|
+
|
|
34
|
+
**Lokasi:** `src/providers/doku/provider.ts`
|
|
35
|
+
|
|
36
|
+
**Masalah Sebelumnya:**
|
|
37
|
+
- Jika request webhook datang tanpa header signature (atau secretKey/clientSecret belum terkonfigurasi), `verifyCallback` dan `verifySnapCallback` mengembalikan `isValid = true` tanpa verifikasi.
|
|
38
|
+
- Payload palsu berpotensi lolos verifikasi.
|
|
39
|
+
|
|
40
|
+
**Perbaikan & Validasi Docs:**
|
|
41
|
+
- DOKU Notification Guide resmi mewajibkan signature verification via headers (`Signature`, `Request-Id`, `Client-Id`, `Request-Timestamp`).
|
|
42
|
+
- Kode telah diperbaiki: default `isValid = false`.
|
|
43
|
+
- Jika signature header atau kredensial kosong, webhook langsung ditolak dengan `isValid: false` dan pesan error deskriptif.
|
|
44
|
+
|
|
45
|
+
---
|
|
46
|
+
|
|
47
|
+
### S2. Xendit — verifikasi webhook default `isValid = true` (Critical keamanan) — ✅ FIXED
|
|
48
|
+
|
|
49
|
+
**Lokasi:** `src/providers/xendit/provider.ts:242`
|
|
50
|
+
|
|
51
|
+
**Masalah Sebelumnya:**
|
|
52
|
+
- Tanpa `config.extra.webhookToken` atau header callback token, `isValid` tetap bernilai `true`.
|
|
53
|
+
|
|
54
|
+
**Perbaikan & Validasi Docs:**
|
|
55
|
+
- Dokumentasi resmi Xendit Webhook Verification menyatakan bahwa Xendit menyertakan `x-callback-token` pada header notifikasi callback.
|
|
56
|
+
- Kode telah diperbaiki: default `isValid = false`.
|
|
57
|
+
- Jika token header atau konfigurasi secret tidak ada atau tidak cocok, callback ditolak (`isValid: false`).
|
|
58
|
+
|
|
59
|
+
---
|
|
60
|
+
|
|
61
|
+
### S3. iPaymu — fallback nomor telepon hardcode (High) — ✅ FIXED
|
|
62
|
+
|
|
63
|
+
**Lokasi:** `src/providers/ipaymu/provider.ts:50` (direct) dan `:84` (semi-integrasi)
|
|
64
|
+
|
|
65
|
+
**Masalah Sebelumnya:**
|
|
66
|
+
- Mengirim nomor fiktif tetap `"081234567890"` ke iPaymu ketika `customer.phone` tidak diisi.
|
|
67
|
+
|
|
68
|
+
**Perbaikan & Validasi Docs:**
|
|
69
|
+
- Dokumentasi resmi iPaymu API v2 (Direct & Redirect Payment) menegaskan bahwa parameter `phone` adalah **opsional**, bukan wajib.
|
|
70
|
+
- Kode telah diperbaiki: fallback hardcode dihapus sepenuhnya. Field `phone` hanya dikirim jika konsumen menyediakannya (`customer?.phone`).
|
|
71
|
+
|
|
72
|
+
---
|
|
73
|
+
|
|
74
|
+
### S4. iPaymu `checkTransaction` — kontrak `/transaction` divalidasi (Risk integrasi) — ✅ VALIDATED
|
|
75
|
+
|
|
76
|
+
**Lokasi:** `src/providers/ipaymu/provider.ts:351`
|
|
77
|
+
|
|
78
|
+
**Temuan & Validasi Docs:**
|
|
79
|
+
- Dokumentasi resmi iPaymu API v2 (`POST /api/v2/transaction`) mengonfirmasi bahwa parameter request body **hanya menerima `transactionId`** (ID transaksi numerik yang diterbitkan oleh iPaymu), bukan `referenceId` / `order_number` string merchant.
|
|
80
|
+
- Mengirim `referenceId` merchant ke endpoint ini akan menghasilkan transaksi tidak ditemukan / pending.
|
|
81
|
+
- **Klarifikasi Kontrak:** Nilai `merchantOrderId` pada `buayar.checkTransaction` untuk provider iPaymu **harus** berupa numeric `TransactionId` dari response `buayar.createInvoice()` (`invoice.reference`), bukan nomor order string internal merchant.
|
|
82
|
+
- JSDoc pada interface `CheckTransactionParams` dan dokumentasi panduan telah diperjelas.
|
|
83
|
+
|
|
84
|
+
---
|
|
85
|
+
|
|
86
|
+
### S5. iPaymu callback — `orderId` diambil dari `reference_id` (Risk integrasi) — ✅ VALIDATED
|
|
87
|
+
|
|
88
|
+
**Lokasi:** `src/providers/ipaymu/provider.ts:190`
|
|
89
|
+
|
|
90
|
+
**Temuan & Validasi Docs:**
|
|
91
|
+
- Dokumentasi resmi webhook / callback notification iPaymu mengonfirmasi bahwa payload callback POST selalu menyertakan `reference_id` (nilai referenceId yang dikirim saat `createInvoice`).
|
|
92
|
+
- Format mapping `const orderId = body.reference_id || body.referenceId || body.trx_id || ""` sudah benar dan sesuai dengan spesifikasi resmi iPaymu v2.
|
|
93
|
+
|
|
94
|
+
---
|
|
95
|
+
|
|
96
|
+
## III. Fitur Premature
|
|
97
|
+
|
|
98
|
+
### S6. `coming_soon` selalu `false` (Medium) — ✅ FIXED
|
|
99
|
+
|
|
100
|
+
**Lokasi:** `src/core/descriptor.ts:90`
|
|
101
|
+
|
|
102
|
+
**Perbaikan:**
|
|
103
|
+
- Implementasi diperbarui agar membaca status `coming_soon` dari raw payment method (`raw.coming_soon ?? raw.is_coming_soon ?? false`) alih-alih hardcode `false`.
|
|
104
|
+
- Konsumen SDK kini dapat menandai channel pembayaran yang belum aktif di UI.; channel yang seharusnya
|
|
105
|
+
ditandai tidak tersedia akan tampil normal.
|
|
106
|
+
|
|
107
|
+
---
|
|
108
|
+
|
|
109
|
+
### S7. Daftar channel banyak provider bersifat statis (Medium) — ✅ FIXED
|
|
110
|
+
|
|
111
|
+
**Lokasi:** `xendit/provider.ts`, `midtrans/provider.ts`
|
|
112
|
+
|
|
113
|
+
**Perbaikan:**
|
|
114
|
+
- Pada provider **Xendit**, `getPaymentMethods()` kini mendukung query dinamis langsung ke endpoint resmi Xendit `GET /payment_channels`. Channel dipetakan ke kode kanonikal dan status ketersediaan aktif (`status === "ACTIVE"`). Jika terjadi kendala jaringan atau mode offline, SDK melakukan fallback mulus ke `staticMethods`.
|
|
115
|
+
- Pada provider **Midtrans**, ketiadaan endpoint publik list channel diimbangi dengan fitur probing aktif melalui `probePaymentMethods` (mengetes charge & cancel ke gateway).
|
|
116
|
+
- Dokumentasi panduan diperbarui menjelaskan perilaku ini secara transparan.
|
|
117
|
+
|
|
118
|
+
---
|
|
119
|
+
|
|
120
|
+
### S8. `probePaymentMethods` sebagian besar tidak diimplementasi (Medium) — ✅ FIXED
|
|
121
|
+
|
|
122
|
+
**Lokasi:** `src/core/manager.ts`, `src/providers/ipaymu/provider.ts`, `src/providers/xendit/provider.ts`, `src/core/buayar.ts`
|
|
123
|
+
|
|
124
|
+
**Perbaikan:**
|
|
125
|
+
- Method `probePaymentMethods` kini diimplementasikan pada provider utama (**iPaymu** dan **Xendit**, selain yang sudah ada di Duitku dan Midtrans).
|
|
126
|
+
- `PaymentManager` ditambahkan mekanisme fallback cerdas: jika provider memiliki implementasi `getPaymentMethods`, daftar channel aktif akan otomatis dimanfaatkan untuk probing.
|
|
127
|
+
- Ditambahkan method facade `buayar.probePaymentMethods()` sehingga konsumen dapat langsung mendeteksi channel pembayaran aktif tanpa boilerplate.
|
|
128
|
+
|
|
129
|
+
---
|
|
130
|
+
|
|
131
|
+
### S9. `detectFromWebhook` — auto-detect ambigu (Medium) — ✅ FIXED
|
|
132
|
+
|
|
133
|
+
**Lokasi:** `src/core/providerRegistry.ts:83-125`, `src/core/buayar.ts:305-325`
|
|
134
|
+
|
|
135
|
+
**Perbaikan:**
|
|
136
|
+
- **Prioritas Header Bertingkat:** Deteksi webhook kini memeriksa HTTP Signature/Token Header terlebih dahulu (`x-callback-token`, `stripe-signature`, `x-razorpay-signature`, `cko-signature`, `openpayu-signature`, `x-square-hmacsha256-signature`, `bt_signature`, `x-oy-username`, `signature` DOKU, dan `x-signature` iPaymu) yang memiliki tingkat kepastian jauh lebih tinggi daripada sekadar field body.
|
|
137
|
+
- **Pola Payload Diperketat:** Pola payload seperti Xendit tidak lagi mencocokkan `external_id` polos secara ambigu, melainkan wajib memiliki status/channel/metode bayar terkait.
|
|
138
|
+
- **Prioritas Provider Eksplisit:** Konfigurasi provider eksplisit tetap menjadi prioritas utama dan tidak ditimpa oleh auto-detect.
|
|
139
|
+
- **Penanganan Aman Tanpa Crash:** Jika payload webhook tak dikenal atau provider tak dapat ditentukan, SDK mengembalikan response terstruktur `{ isValid: false, isPaid: false, provider: "unknown", error: "..." }` alih-alih melempar exception/crash.
|
|
140
|
+
|
|
141
|
+
---
|
|
142
|
+
|
|
143
|
+
## IV. Status Implementasi & Rekomendasi
|
|
144
|
+
|
|
145
|
+
1. **S1/S2 (Critical) — ✅ SELESAI:** Default `isValid` diubah menjadi `false`. Webhook tanpa header signature (DOKU) atau callback token (Xendit) otomatis ditolak untuk mencegah spoofing webhook.
|
|
146
|
+
2. **S3 (High) — ✅ SELESAI:** Fallback hardcode nomor telepon `"081234567890"` dihapus. Parameter `phone` dijadikan opsional per docs resmi iPaymu v2.
|
|
147
|
+
3. **S4/S5 (High) — ✅ SELESAI & TERVALIDASI:**
|
|
148
|
+
- Divalidasi dengan docs resmi iPaymu: `/transaction` mewajibkan `transactionId` numerik iPaymu (`invoice.reference`), bukan orderId merchant. JSDoc dan dokumentasi diperjelas.
|
|
149
|
+
- Divalidasi dengan docs resmi iPaymu: callback webhook selalu mengirim `reference_id` merchant.
|
|
150
|
+
4. **S6 (Medium) — ✅ SELESAI:** Flag `coming_soon` pada deskriptor channel kini membaca dari field raw channel (`raw.coming_soon ?? raw.is_coming_soon ?? false`).
|
|
151
|
+
5. **S7 (Medium) — ✅ SELESAI:** Query dinamis live `/payment_channels` pada Xendit dengan fallback statis aman.
|
|
152
|
+
6. **S8 (Medium) — ✅ SELESAI:** `probePaymentMethods` diimplementasikan di iPaymu & Xendit + fallback dinamis di manager & facade.
|
|
153
|
+
7. **S9 (Medium) — ✅ SELESAI:** Deteksi webhook via header tingkat tinggi, heuristik diperketat, dan penanganan aman tanpa crash.
|
|
154
|
+
|
|
155
|
+
---
|
|
156
|
+
|
|
157
|
+
## V. Lampiran — Lokasi kode yang direferensikan
|
|
158
|
+
|
|
159
|
+
- `src/providers/doku/provider.ts` — `verifyCallback` (≈495-542), `verifySnapCallback` (≈548+)
|
|
160
|
+
- `src/providers/xendit/provider.ts` — `verifyCallback` (≈217-260)
|
|
161
|
+
- `src/providers/ipaymu/provider.ts` — `createInvoice` (25-186), `verifyCallback` (188-216), `checkTransaction` (351-449)
|
|
162
|
+
- `src/core/descriptor.ts` — `buildPaymentMethodDescriptor` (72-93)
|
|
163
|
+
- `src/core/manager.ts` — `probePaymentMethods` (289-298), `getPaymentMethods` (271-278), `checkTransaction` (280-287)
|
|
164
|
+
- `src/core/providerRegistry.ts` — `detectFromWebhook` (83-107)
|
|
165
|
+
- `src/core/buayar.ts` — `verifyWebhook` (243-310)
|
|
166
|
+
---
|
package/docs/guide.md
CHANGED
|
@@ -183,6 +183,18 @@ const { categories } = await buayar.getPaymentMethods({ amount: 150000 });
|
|
|
183
183
|
// categories: { "Virtual Account": [...], "QRIS": [...], "E-Wallet": [...], ... }
|
|
184
184
|
```
|
|
185
185
|
|
|
186
|
+
### Probing Saluran Aktif di Akun Gateway (`probePaymentMethods`)
|
|
187
|
+
|
|
188
|
+
Untuk mendeteksi secara dinamis saluran yang benar-benar aktif / di-enable pada akun merchant Anda di gateway:
|
|
189
|
+
|
|
190
|
+
```typescript
|
|
191
|
+
const probe = await buayar.probePaymentMethods();
|
|
192
|
+
if (probe.success) {
|
|
193
|
+
console.log("Channel aktif di akun merchant:", probe.enabled);
|
|
194
|
+
// Output: ["bca_va", "mandiri_va", "qris", "gopay", ...]
|
|
195
|
+
}
|
|
196
|
+
```
|
|
197
|
+
|
|
186
198
|
---
|
|
187
199
|
|
|
188
200
|
## 🔍 Cek Status Transaksi
|
|
@@ -196,6 +208,10 @@ if (result.success) {
|
|
|
196
208
|
}
|
|
197
209
|
```
|
|
198
210
|
|
|
211
|
+
> 💡 **Catatan Parameter per Provider:**
|
|
212
|
+
> - **Midtrans, Duitku, Xendit, DOKU, dll:** `merchantOrderId` menerima string ID order yang Anda buat (mis. `"ORDER-1001"`).
|
|
213
|
+
> - **iPaymu:** Dokumentasi resmi iPaymu v2 mewajibkan `transactionId` numerik. Masukkan nilai **`invoice.reference`** (TransactionId numerik yang dikembalikan saat `createInvoice`), bukan nomor order string internal merchant.
|
|
214
|
+
|
|
199
215
|
---
|
|
200
216
|
|
|
201
217
|
## 🪝 Webhook Universal
|
|
@@ -208,9 +224,10 @@ import { buayar } from "@crediblemark/buayar";
|
|
|
208
224
|
// Bekerja dengan Express, Elysia, Hono, Next.js App Router, dll.
|
|
209
225
|
app.post("/api/payment/webhook", async (req, res) => {
|
|
210
226
|
const result = await buayar.verifyWebhook(req.body, req.headers);
|
|
211
|
-
//
|
|
212
|
-
//
|
|
227
|
+
// Header signature otomatis diekstrak sesuai provider (Stripe-Signature,
|
|
228
|
+
// X-Signature, x-callback-token, Signature DOKU, dll.)
|
|
213
229
|
|
|
230
|
+
// Keamanan Ketat: jika signature/token tidak ada atau tidak cocok, isValid bernilai false
|
|
214
231
|
if (!result.isValid) return res.status(400).json({ error: "Invalid signature" });
|
|
215
232
|
|
|
216
233
|
if (result.isPaid) {
|
|
@@ -222,6 +239,11 @@ app.post("/api/payment/webhook", async (req, res) => {
|
|
|
222
239
|
});
|
|
223
240
|
```
|
|
224
241
|
|
|
242
|
+
> 🔒 **Keamanan Signature Ketat:**
|
|
243
|
+
> - **DOKU:** Otomatis memvalidasi signature header (`Signature`, `Request-Id`, `Request-Timestamp`) via HMAC-SHA256. Webhook tanpa signature ditolak (`isValid: false`).
|
|
244
|
+
> - **Xendit:** Memvalidasi header `x-callback-token` terhadap secret token yang dikonfigurasi (`BUAYAR_WEBHOOK_SECRET` / `webhookToken`). Webhook tanpa token ditolak (`isValid: false`).
|
|
245
|
+
> - **iPaymu:** Memvalidasi header `X-Signature` dengan HMAC-SHA256 atas body menggunakan VA merchant. Field `result.orderId` otomatis diisi dari `reference_id` order merchant.
|
|
246
|
+
>
|
|
225
247
|
> `buayar.handleWebhook(payload, headers)` adalah alias dari `verifyWebhook`.
|
|
226
248
|
|
|
227
249
|
---
|
package/docs/ipaymu.md
CHANGED
|
@@ -133,7 +133,7 @@ const invoice = await buayar.createInvoice({
|
|
|
133
133
|
customer: {
|
|
134
134
|
name: "Budi Santoso",
|
|
135
135
|
email: "budi@mail.com",
|
|
136
|
-
phone: "081234567890",
|
|
136
|
+
phone: "081234567890", // Opsional per dokumentasi resmi iPaymu v2 (hanya dikirim jika ada)
|
|
137
137
|
},
|
|
138
138
|
// Opsi Tambahan iPaymu:
|
|
139
139
|
feeDirection: "BUYER", // "BUYER" (bebankan fee ke pembeli) atau "MERCHANT" (potong omset)
|
|
@@ -141,12 +141,14 @@ const invoice = await buayar.createInvoice({
|
|
|
141
141
|
});
|
|
142
142
|
|
|
143
143
|
if (invoice.success) {
|
|
144
|
-
console.log("Transaction ID (Trx ID):", invoice.reference); // Contoh: "229432"
|
|
144
|
+
console.log("Transaction ID (Trx ID):", invoice.reference); // Contoh: "229432" (simpan ini untuk checkTransaction)
|
|
145
145
|
console.log("Nomor Virtual Account:", invoice.vaNumber); // Contoh: "3811800034705407"
|
|
146
146
|
console.log("Expired:", invoice.expiresAt);
|
|
147
147
|
}
|
|
148
148
|
```
|
|
149
149
|
|
|
150
|
+
> 💡 **Parameter `customer.phone` Opsional:** Sesuai dokumentasi resmi iPaymu v2, parameter `phone` bersifat opsional. Buayar tidak lagi menyisipkan nomor fallback buatan — field `phone` hanya disertakan jika memang diisi oleh pembeli/merchant.
|
|
151
|
+
|
|
150
152
|
### B. Redirect Payment (Hosted Payment Page)
|
|
151
153
|
Cukup kosongkan `paymentMethod` untuk menggunakan halaman checkout bawaan iPaymu:
|
|
152
154
|
|
|
@@ -158,7 +160,7 @@ const invoice = await buayar.createInvoice({
|
|
|
158
160
|
customer: {
|
|
159
161
|
name: "Siti Rahma",
|
|
160
162
|
email: "siti@mail.com",
|
|
161
|
-
phone: "081999888777",
|
|
163
|
+
phone: "081999888777", // Opsional
|
|
162
164
|
},
|
|
163
165
|
returnUrl: "https://toko-anda.com/checkout/success",
|
|
164
166
|
});
|
|
@@ -292,6 +294,8 @@ Callback **harus** membawa header `X-Signature`; Buayar memverifikasinya dengan
|
|
|
292
294
|
menggunakan **Merchant VA** sebagai secret key. Callback tanpa `X-Signature` yang sah
|
|
293
295
|
akan selalu ditolak (`isValid === false`).
|
|
294
296
|
|
|
297
|
+
Sesuai dokumentasi resmi iPaymu, payload notifikasi membawa field `reference_id` (berisi nomor order merchant yang dikirim saat `createInvoice`). Buayar otomatis memetakannya ke **`result.orderId`**.
|
|
298
|
+
|
|
295
299
|
```typescript
|
|
296
300
|
// Di handler Express.js / Next.js API route + REST framework apapun (Elysia/Hono/...):
|
|
297
301
|
app.post("/api/payment/webhook", (req, res) => {
|
|
@@ -299,6 +303,7 @@ app.post("/api/payment/webhook", (req, res) => {
|
|
|
299
303
|
const result = buayar.verifyWebhook(req.body, req.headers);
|
|
300
304
|
|
|
301
305
|
if (result.isValid && result.isPaid) {
|
|
306
|
+
// result.orderId otomatis diambil dari `reference_id` iPaymu (order ID merchant Anda)
|
|
302
307
|
console.log("Pembayaran Berhasil untuk Order ID:", result.orderId);
|
|
303
308
|
console.log("Nominal Diterima:", result.amount);
|
|
304
309
|
// Jalankan logika bisnis: update database status pesanan menjadi PAID
|
|
@@ -326,12 +331,22 @@ console.log(invoice.reference); // Contoh: "229432"
|
|
|
326
331
|
4. Klik tombol **"Kirim" / "Test"**.
|
|
327
332
|
5. Server iPaymu Sandbox akan mengubah status transaksi menjadi **Berhasil** dan otomatis mengirimkan webhook notifikasi ke `notifyUrl` Anda.
|
|
328
333
|
|
|
329
|
-
### Mengecek Status Transaksi via Kode
|
|
334
|
+
### Mengecek Status Transaksi via Kode (`checkTransaction`)
|
|
335
|
+
|
|
336
|
+
> ⚠️ **PENTING — Kontrak Resmi Endpoint `/transaction`:**
|
|
337
|
+
> Dokumentasi resmi iPaymu API v2 menegaskan bahwa endpoint `/api/v2/transaction` **hanya menerima `transactionId` numerik** yang diterbitkan iPaymu (`invoice.reference`), **BUKAN** `order_number` atau string `orderId` merchant.
|
|
338
|
+
>
|
|
339
|
+
> Jika aplikasi Anda melakukan polling status, pastikan selalu menyimpan `invoice.reference` di database dan operasikan nilai tersebut ke parameter `merchantOrderId`:
|
|
340
|
+
|
|
330
341
|
```typescript
|
|
331
342
|
const check = await buayar.checkTransaction({
|
|
332
|
-
|
|
343
|
+
// WAJIB: Gunakan invoice.reference (TransactionId numerik dari iPaymu),
|
|
344
|
+
// BUKAN nomor invoice/order_number string toko Anda!
|
|
345
|
+
merchantOrderId: invoice.reference, // misal "229432"
|
|
333
346
|
});
|
|
334
347
|
|
|
335
|
-
|
|
336
|
-
|
|
348
|
+
if (check.success) {
|
|
349
|
+
console.log("Status Transaksi:", check.status); // "paid" | "pending" | "failed"
|
|
350
|
+
console.log("Status Desc:", check.rawResponse.Data.StatusDesc); // "Berhasil"
|
|
351
|
+
}
|
|
337
352
|
```
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@crediblemark/buayar",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.6",
|
|
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",
|