@rytass/payments-adapter-ctbc-micro-fast-pay
Version:
Rytass Payment Gateway
770 lines (767 loc) • 32.5 kB
JavaScript
import { desEcbEncryptHex } from './ctbc-crypto-core.js';
import * as soap from 'soap';
import { OrderState } from '@rytass/payments';
import { debugPayment } from './ctbc-payment.js';
class CTBCAEGateway {
serverConfig = {};
response = {};
constructor(config){
this.serverConfig = config;
}
/**
* AMEX 查詢功能
* 對應 PHP amex.php 的 Inquiry 方法
*/ async inquiry(params) {
this.response = {
count: 0,
mac: '',
errCode: '',
errDesc: '',
poDetails: []
};
if (!this.checkServer(this.serverConfig)) {
return this.toPosInquiryResponse(this.response);
}
const requestData = {};
const merIdResult = this.checkMerId(params.merId);
if (!merIdResult) {
return this.toPosInquiryResponse(this.response);
}
requestData.merId = merIdResult;
const lidmResult = this.checkLidm(params.lidm);
if (!lidmResult) {
return this.toPosInquiryResponse(this.response);
}
requestData.lidm = lidmResult;
if (params.xid) {
// XID 是可選的,只有在提供時才需要驗證
const xidResult = this.checkXid(params.xid);
if (!xidResult) {
return this.toPosInquiryResponse(this.response);
}
requestData.xid = xidResult;
} else {
// 根據 Java ParametersChecker.inquiry(),XID 可以為空
// 如果沒有提供 XID,我們可以不設置或設置為空字串
requestData.xid = ''; // 或者可以不設置這個欄位
}
// 查詢 MAC:merId(12,左補0) + lidm(20,右補空白)
{
const macString = requestData.merId.padStart(12, '0') + requestData.lidm.padEnd(20, ' ');
requestData.mac = params.IN_MAC_KEY ? this.createMac(macString, params.IN_MAC_KEY) : '';
}
const startTime = Date.now();
try {
const wsdlUrl = this.serverConfig.wsdlUrl;
const soapClient = await soap.createClientAsync(wsdlUrl, {
wsdl_options: this.serverConfig.sslOptions
});
const client = soapClient;
// 根據 WSDL,這是 RPC style SOAP,參數需要包裝在 request 對象中
const rpcRequest = {
request: requestData
};
// 調用 SOAP 方法
const soapResponse = await new Promise((resolve, reject)=>{
const call = (name, payload)=>client[name](payload, (err, result)=>err ? reject(err) : resolve(result));
if (typeof client['Inquiry'] === 'function') {
call('Inquiry', rpcRequest);
} else if (typeof client['inquiry'] === 'function') {
call('inquiry', rpcRequest);
} else {
console.error('Available SOAP methods:', Object.keys(client));
reject(new Error('SOAP method "Inquiry" not found in WSDL'));
}
});
debugPayment('AMEX SOAP Inquiry soapResponse:', soapResponse);
// 處理 RPC style SOAP 回應
const inquiryResult = soapResponse.inquiryReturn ?? soapResponse;
Object.assign(this.response, inquiryResult);
debugPayment('AMEX SOAP Inquiry this.response:', this.response);
// 處理 SOAP 回應
if (inquiryResult.count !== undefined && inquiryResult.count !== null) {
const c = inquiryResult.count;
this.response.count = typeof c === 'number' ? c : 0;
}
if (inquiryResult.errCode !== undefined && inquiryResult.errCode !== null) {
this.response.errCode = String(inquiryResult.errCode);
}
if (inquiryResult.errDesc !== undefined && inquiryResult.errDesc !== null) {
this.response.errDesc = String(inquiryResult.errDesc);
}
// mac 於下方檢核後以 'Y'/'F'/'N' 形式寫入
const pd = inquiryResult.poDetails;
if (pd) {
this.response.poDetails = Array.isArray(pd) ? pd.map((detail)=>({
aetId: detail.aetid || detail.aetId,
authCode: detail.authCode,
termSeq: detail.termSeq,
authAmt: detail.purchAmt ? String(detail.purchAmt) : detail.authAmt || '',
currency: detail.currency || 'TWD',
status: detail.status,
txnType: detail.txnType,
expDate: detail.expDate,
pan: detail.pan,
xid: detail.xid,
lidm: detail.lidm
})) : [
{
aetId: pd.aetid || pd.aetId,
authCode: pd.authCode,
termSeq: pd.termSeq,
authAmt: pd.purchAmt ? String(pd.purchAmt) : pd.authAmt || '',
currency: pd.currency || 'TWD',
status: pd.status,
txnType: pd.txnType,
expDate: pd.expDate,
pan: pd.pan,
xid: pd.xid,
lidm: pd.lidm
}
];
}
const responseSMac = this.sprintf('%08d', this.response.count ?? 0) + this.padOrTruncate(this.response.errCode || '', 8);
const recvMac = inquiryResult?.mac?.toString() || '';
this.checkMac(responseSMac, params.IN_MAC_KEY || '', recvMac);
const endTime = Date.now();
this.checkTimeOut(startTime, endTime);
} catch (error) {
console.error('AMEX SOAP Inquiry failed:', error);
this.response.errCode = 'SOAP_ERROR';
this.response.errDesc = `SOAP communication failed: ${error instanceof Error ? error.message : String(error)}`;
}
return this.toPosInquiryResponse(this.response);
}
/**
* AMEX 退款功能
* 對應 PHP amex.php 的 Cred 方法
*/ async refund(params) {
this.response = {
aetId: '',
xid: '',
credAmt: '',
unCredAmt: '',
capBatchId: '',
capBatchSeq: '',
errCode: '',
errDesc: '',
mac: ''
};
if (!this.checkServer(this.serverConfig)) {
return this.toPosInquiryResponse(this.response);
}
const requestData = {};
const merIdResult = this.checkMerId(params.merId);
if (!merIdResult) {
return this.toPosInquiryResponse(this.response);
}
requestData.merId = merIdResult;
const xidResult = this.checkXid(params.xid);
if (!xidResult) {
return this.toPosInquiryResponse(this.response);
}
requestData.xid = xidResult;
const purchAmtResult = this.checkCredAmt(params.purchAmt);
if (!purchAmtResult) {
return this.toPosInquiryResponse(this.response);
}
requestData.credAmt = purchAmtResult;
// 退款 MAC:merId(12,左補0) + credAmt(12,左補0) + xid(12,右補空白) + lidm(20,右補空白)
const lidmResult = this.checkLidm(params.lidm);
if (!lidmResult) {
return this.toPosInquiryResponse(this.response);
}
requestData.lidm = lidmResult;
const sMac = requestData.merId.padStart(12, '0') + String(requestData.credAmt).padStart(12, '0') + requestData.xid.padEnd(12, ' ') + requestData.lidm.padEnd(20, ' ');
// 按 PHP 規格只送 mac,不送 IN_MAC_KEY
requestData.mac = this.createMac(sMac, params.IN_MAC_KEY);
const startTime = Date.now();
try {
const wsdlUrl = this.serverConfig.wsdlUrl;
const soapClient = await soap.createClientAsync(wsdlUrl, {
wsdl_options: this.serverConfig.sslOptions
});
const client = soapClient;
const soapResponse = await new Promise((resolve, reject)=>{
const call = (name, payload)=>client[name](payload, (err, result)=>err ? reject(err) : resolve(result));
if (typeof client['Cred'] === 'function') {
call('Cred', {
request: requestData
});
} else if (typeof client['cred'] === 'function') {
call('cred', {
request: requestData
});
} else if (typeof client['refund'] === 'function') {
call('refund', requestData);
} else {
reject(new Error('SOAP method "Cred"/"refund" not found in WSDL'));
}
});
debugPayment('AMEX SOAP Refund soapResponse:', soapResponse);
// 處理退款回應(不同實作大小寫可能不同)
const refundResult = soapResponse.credReturn ?? soapResponse;
Object.assign(this.response, refundResult);
debugPayment('AMEX SOAP Refund this.response:', this.response);
// 檢核回應 MAC:aetId(16,右補空白) + xid(16,右補空白) + errCode(8,右補空白) + credAmt(16,左補0)
const responseSMac = this.padOrTruncate(this.response.aetId || '', 16) + this.padOrTruncate(this.response.xid || '', 16) + this.padOrTruncate(this.response.errCode || '', 8) + this.sprintf('%016d', parseInt(this.response.credAmt || '0', 10));
const recvMac = String(refundResult?.['mac'] ?? '');
this.checkMac(responseSMac, params.IN_MAC_KEY || '', recvMac);
const endTime = Date.now();
this.checkTimeOut(startTime, endTime);
} catch (error) {
console.error('AMEX SOAP Refund failed:', error);
this.response.errCode = 'SOAP_ERROR';
this.response.errDesc = `SOAP communication failed: ${error instanceof Error ? error.message : String(error)}`;
}
return this.toPosInquiryResponse(this.response);
}
/**
* AMEX 授權取消 (AuthRev)
*/ async authRev(params) {
this.response = {
aetId: '',
xid: '',
termSeq: '',
errCode: '',
errDesc: '',
mac: ''
};
if (!this.checkServer(this.serverConfig)) {
return this.toPosInquiryResponse(this.response);
}
const requestData = {};
const merIdResult = this.checkMerId(params.merId);
if (!merIdResult) return this.toPosInquiryResponse(this.response);
requestData.merId = merIdResult;
const xidResult = this.checkXid(params.xid);
if (!xidResult) return this.toPosInquiryResponse(this.response);
requestData.xid = xidResult;
const lidmResult = this.checkLidm(params.lidm);
if (!lidmResult) return this.toPosInquiryResponse(this.response);
requestData.lidm = lidmResult;
// sMac: merId(12,0) + xid(12,' ') + lidm(24,' ')
const sMac = requestData.merId.padStart(12, '0') + requestData.xid.padEnd(12, ' ') + this.padOrTruncate(requestData.lidm, 24);
requestData.mac = this.createMac(sMac, params.IN_MAC_KEY);
const startTime = Date.now();
try {
const wsdlUrl = this.serverConfig.wsdlUrl;
const soapClient = await soap.createClientAsync(wsdlUrl, {
wsdl_options: this.serverConfig.sslOptions
});
const client = soapClient;
const soapResponse = await new Promise((resolve, reject)=>{
const call = (name, payload)=>client[name](payload, (err, result)=>err ? reject(err) : resolve(result));
if (typeof client['authRev'] === 'function') {
call('authRev', {
request: requestData
});
} else if (typeof client['AuthRev'] === 'function') {
call('AuthRev', {
request: requestData
});
} else {
reject(new Error('SOAP method "AuthRev" not found in WSDL'));
}
});
debugPayment('AMEX SOAP AuthRev soapResponse:', soapResponse);
const authRevReturn = soapResponse.authRevReturn ?? soapResponse;
Object.assign(this.response, authRevReturn);
debugPayment('AMEX SOAP AuthRev this.response:', this.response);
// Response sMac: aetId(16) + xid(16) + errCode(8)
const responseSMac = this.padOrTruncate(this.response.aetId || '', 16) + this.padOrTruncate(this.response.xid || '', 16) + this.padOrTruncate(this.response.errCode || '', 8);
const recvMac = String(authRevReturn?.['mac'] ?? '');
this.checkMac(responseSMac, params.IN_MAC_KEY || '', recvMac);
const endTime = Date.now();
this.checkTimeOut(startTime, endTime);
} catch (error) {
console.error('AMEX SOAP AuthRev failed:', error);
this.response.errCode = 'SOAP_ERROR';
this.response.errDesc = `SOAP communication failed: ${error instanceof Error ? error.message : String(error)}`;
}
return this.toPosInquiryResponse(this.response);
}
/**
* AMEX 請款取消 (CapRev)
*/ async capRev(params) {
this.response = {
aetId: '',
xid: '',
errCode: '',
errDesc: '',
mac: ''
};
if (!this.checkServer(this.serverConfig)) {
return this.toPosInquiryResponse(this.response);
}
const requestData = {};
const merIdResult = this.checkMerId(params.merId);
if (!merIdResult) return this.toPosInquiryResponse(this.response);
requestData.merId = merIdResult;
const xidResult = this.checkXid(params.xid);
if (!xidResult) return this.toPosInquiryResponse(this.response);
requestData.xid = xidResult;
const lidmResult = this.checkLidm(params.lidm);
if (!lidmResult) return this.toPosInquiryResponse(this.response);
requestData.lidm = lidmResult;
// sMac: merId(12,0) + xid(12,' ') + lidm(24,' ')
const sMac = requestData.merId.padStart(12, '0') + requestData.xid.padEnd(12, ' ') + this.padOrTruncate(requestData.lidm, 24);
requestData.mac = this.createMac(sMac, params.IN_MAC_KEY);
const startTime = Date.now();
try {
const wsdlUrl = this.serverConfig.wsdlUrl;
const soapClient = await soap.createClientAsync(wsdlUrl, {
wsdl_options: this.serverConfig.sslOptions
});
const client = soapClient;
const soapResponse = await new Promise((resolve, reject)=>{
const call = (name, payload)=>client[name](payload, (err, result)=>err ? reject(err) : resolve(result));
if (typeof client['CapRev'] === 'function') {
call('CapRev', {
request: requestData
});
} else if (typeof client['capRev'] === 'function') {
call('capRev', {
request: requestData
});
} else {
reject(new Error('SOAP method "CapRev" not found in WSDL'));
}
});
debugPayment('AMEX SOAP CapRev soapResponse:', soapResponse);
const capRevReturn = soapResponse.capRevReturn ?? soapResponse;
Object.assign(this.response, capRevReturn);
debugPayment('AMEX SOAP CapRev this.response:', this.response);
// Response sMac: aetId(16) + xid(16) + errCode(8)
const responseSMac = this.padOrTruncate(this.response.aetId || '', 16) + this.padOrTruncate(this.response.xid || '', 16) + this.padOrTruncate(this.response.errCode || '', 8);
const recvMac = String(capRevReturn?.['mac'] ?? '');
this.checkMac(responseSMac, params.IN_MAC_KEY || '', recvMac);
const endTime = Date.now();
this.checkTimeOut(startTime, endTime);
} catch (error) {
console.error('AMEX SOAP CapRev failed:', error);
this.response.errCode = 'SOAP_ERROR';
this.response.errDesc = `SOAP communication failed: ${error instanceof Error ? error.message : String(error)}`;
}
return this.toPosInquiryResponse(this.response);
}
/**
* AMEX 取消退款 (CredRev)
*/ async cancelRefund(params) {
this.response = {
aetId: '',
xid: '',
errCode: '',
errDesc: '',
mac: ''
};
if (!this.checkServer(this.serverConfig)) {
return this.toPosInquiryResponse(this.response);
}
const requestData = {};
const merIdResult = this.checkMerId(params.merId);
if (!merIdResult) return this.toPosInquiryResponse(this.response);
requestData.merId = merIdResult;
const xidResult = this.checkXid(params.xid);
if (!xidResult) return this.toPosInquiryResponse(this.response);
requestData.xid = xidResult;
const lidmResult = this.checkLidm(params.lidm);
if (!lidmResult) return this.toPosInquiryResponse(this.response);
requestData.lidm = lidmResult;
const capBatchIdResult = this.checkCapBatchId(params.capBatchId);
if (!capBatchIdResult) return this.toPosInquiryResponse(this.response);
requestData.capBatchId = capBatchIdResult;
const capBatchSeqResult = this.checkCapBatchSeq(params.capBatchSeq);
if (!capBatchSeqResult) return this.toPosInquiryResponse(this.response);
requestData.capBatchSeq = capBatchSeqResult;
// sMac: merId(12,0) + capBatchSeq(12,space) + xid(12,space) + lidm(20,space)
const sMac = requestData.merId.padStart(12, '0') + requestData.capBatchSeq.padEnd(12, ' ') + requestData.xid.padEnd(12, ' ') + requestData.lidm.padEnd(20, ' ');
requestData.mac = this.createMac(sMac, params.IN_MAC_KEY);
const startTime = Date.now();
try {
const wsdlUrl = this.serverConfig.wsdlUrl;
const soapClient = await soap.createClientAsync(wsdlUrl, {
wsdl_options: this.serverConfig.sslOptions
});
const client = soapClient;
const soapResponse = await new Promise((resolve, reject)=>{
const call = (name, payload)=>client[name](payload, (err, result)=>err ? reject(err) : resolve(result));
if (typeof client['CredRev'] === 'function') {
call('CredRev', {
request: requestData
});
} else if (typeof client['credRev'] === 'function') {
call('credRev', {
request: requestData
});
} else {
reject(new Error('SOAP method "CredRev" not found in WSDL'));
}
});
debugPayment('AMEX SOAP Cancel Refund soapResponse:', soapResponse);
const credRevReturn = soapResponse.credRevReturn ?? soapResponse;
Object.assign(this.response, credRevReturn);
debugPayment('AMEX SOAP Cancel Refund this.response:', this.response);
// Response MAC: aetId(16,space) + xid(16,space) + errCode(8,space)
const responseSMac = this.padOrTruncate(this.response.aetId || '', 16) + this.padOrTruncate(this.response.xid || '', 16) + this.padOrTruncate(this.response.errCode || '', 8);
const recvMac = String(credRevReturn?.['mac'] ?? '');
this.checkMac(responseSMac, params.IN_MAC_KEY || '', recvMac);
const endTime = Date.now();
this.checkTimeOut(startTime, endTime);
} catch (error) {
console.error('AMEX SOAP Cancel Refund failed:', error);
this.response.errCode = 'SOAP_ERROR';
this.response.errDesc = `SOAP communication failed: ${error instanceof Error ? error.message : String(error)}`;
}
return this.toPosInquiryResponse(this.response);
}
// 驗證伺服器設定
checkServer(serverConfig) {
this.serverConfig = serverConfig;
if (!serverConfig.wsdlUrl) {
return false;
}
return true;
}
// 驗證商戶 ID - 根據 Java ParametersChecker
checkMerId(merId) {
if (!merId || merId.length === 0) {
this.response.errCode = 'I124';
this.response.errDesc = 'MERID錯誤';
return false;
}
// Java 版本: 1-12 字符
if (merId.length < 1 || merId.length > 12) {
this.response.errCode = 'I124';
this.response.errDesc = 'MERID錯誤';
return false;
}
return merId;
}
// 驗證 LIDM - 根據 Java ParametersChecker
checkLidm(lidm) {
if (!lidm || lidm.length === 0) {
this.response.errCode = 'I131';
this.response.errDesc = 'LIDM錯誤';
return false;
}
// Java 版本: 1-19 字符,只能包含數字和字母
if (lidm.length < 1 || lidm.length > 19) {
this.response.errCode = 'I131';
this.response.errDesc = 'LIDM錯誤';
return false;
}
// 檢查是否只包含數字和字母
const alphanumericPattern = /^[0-9a-zA-Z]+$/;
if (!alphanumericPattern.test(lidm)) {
this.response.errCode = 'I131';
this.response.errDesc = 'LIDM錯誤';
return false;
}
return lidm;
}
// 驗證 XID
checkXid(xid) {
if (!xid || xid.length === 0) {
this.response.errCode = 'I130';
this.response.errDesc = 'XID錯誤';
return false;
}
// 根據 Java ParametersChecker,XID 長度應該在 1-12 字符之間
if (xid.length < 1 || xid.length > 12) {
this.response.errCode = 'I130';
this.response.errDesc = 'XID錯誤';
return false;
}
return xid;
}
checkCapBatchId(capBatchId) {
const id = (capBatchId || '').trim();
if (id && id.length === 8) return id;
this.response.errCode = 'I134';
this.response.errDesc = 'capBatchId參數錯誤';
return false;
}
checkCapBatchSeq(capBatchSeq) {
const seq = (capBatchSeq || '').trim();
if (seq && seq.length === 12) return seq;
this.response.errCode = 'I135';
this.response.errDesc = 'capBatchSeq參數錯誤';
return false;
}
// ——— POS-like response mappers ———
mapErrCodeToPos(code) {
if (!code) return code;
// AMEX success A000 => POS success 00
return code === 'A000' ? '00' : code;
}
toPosInquiryResponse(amex) {
const detail = Array.isArray(amex.poDetails) ? amex.poDetails[0] : amex.poDetails?.[0];
const ErrCode = this.mapErrCodeToPos(amex.errCode) || '';
const state = amex.txnType && [
'AU',
'BQ',
'RV'
].includes(amex.txnType) ? OrderState.COMMITTED : amex.txnType && [
'VD',
'RF',
'BV'
].includes(amex.txnType) ? OrderState.REFUNDED : amex.capBatchId && amex.capBatchSeq ? OrderState.REFUNDED : OrderState.FAILED;
const resp = {
RespCode: ErrCode === '00' ? '0' : '1',
ErrCode,
ERRDESC: amex.errDesc,
XID: detail?.xid ?? amex.xid,
AuthCode: detail?.authCode ?? '',
AuthAmt: detail?.purchAmt ? detail.purchAmt : detail?.authAmt,
PAN: detail?.pan,
ECI: undefined,
QueryCode: undefined,
currency: detail?.currency === '901' ? 'TWD' : detail?.currency,
// 方便上層判斷 AE 狀態
txnType: detail?.txnType,
status: detail?.status,
CurrentState: state,
capBatchId: amex.capBatchId ?? '',
capBatchSeq: amex.capBatchSeq ?? '',
aetId: detail?.aetId ?? amex.aetId
};
return resp;
}
// 驗證退款金額
checkCredAmt(credAmt) {
if (credAmt <= 0) {
this.response.errCode = 'PARAM_ERROR';
this.response.errDesc = 'Credit amount must be greater than 0';
return false;
}
if (credAmt > 999999) {
this.response.errCode = 'PARAM_ERROR';
this.response.errDesc = 'Credit amount too large';
return false;
}
return credAmt;
}
// 驗證 MAC Key(允許 0/8/24;0 表示不送 mac)
checkInMacKey(macKey) {
if (macKey.length === 0) return false;
if (macKey.length === 8) return true;
if (macKey.length === 24) return true;
this.response.errCode = 'PARAM_ERROR';
this.response.errDesc = 'MAC Key length must be 0, 8 or 24 characters';
return false;
}
// 創建 MAC - 3DES/ECB,滿 8 bytes 不補,否則補 PKCS#5
createMac(data, macKey) {
if (!this.checkInMacKey(macKey)) return '';
try {
return desEcbEncryptHex(data, macKey);
} catch (_err) {
return '';
}
}
// 驗證 MAC,並將結果記錄到 this.response.mac(Y/F/N)
checkMac(data, macKey, receivedMac) {
if (!macKey) {
this.response.mac = 'N';
return false;
}
if (!receivedMac) {
this.response.mac = 'F';
return false;
}
const expectedMac = this.createMac(data, macKey);
const ok = expectedMac === receivedMac.toUpperCase();
this.response.mac = ok ? 'Y' : 'F';
return ok;
}
// 格式化字串(類似 PHP 的 sprintf)
sprintf(format, ...args) {
let i = 0;
return format.replace(/%(?:0(\d+))?[sd%]/g, (match, width)=>{
if (match === '%%') return '%';
if (i < args.length) {
const arg = args[i++];
if (match === '%s') return String(arg);
if (match.includes('d')) {
const num = String(parseInt(String(arg), 10) || 0);
if (width) {
return num.padStart(parseInt(width, 10), '0');
}
return num;
}
}
return match;
});
}
// 填充或截斷字串
padOrTruncate(str, length) {
if (str.length > length) {
return str.substring(0, length);
}
return str.padEnd(length, ' ');
}
// 檢查超時
checkTimeOut(startTime, endTime) {
const elapsed = endTime - startTime;
if (elapsed > (this.serverConfig.timeout || 30000)) {
console.warn(`SOAP request took ${elapsed}ms, which exceeds timeout threshold`);
}
}
}
/**
* AMEX 查詢函數
*/ async function amexInquiry(config, params) {
const gateway = new CTBCAEGateway(config);
return gateway.inquiry(params);
}
/**
* AMEX 退款函數
*/ async function amexRefund(config, params) {
const gateway = new CTBCAEGateway(config);
return gateway.refund(params);
}
/**
* AMEX 取消退款函數 (CredRev)
*/ async function amexCancelRefund(config, params) {
const gateway = new CTBCAEGateway(config);
return gateway.cancelRefund(params);
}
/**
* AMEX 授權取消 (AuthRev)
*/ async function amexAuthRev(config, params) {
const gateway = new CTBCAEGateway(config);
return gateway.authRev(params);
}
/**
* AMEX 請款取消 (CapRev)
*/ async function amexCapRev(config, params) {
const gateway = new CTBCAEGateway(config);
return gateway.capRev(params);
}
function getAmexNextActionFromInquiry(inquiryResp) {
const obj = inquiryResp;
const txnType = obj['txnType'];
const statusCode = obj['status'];
// statusCode 可能值:
// 2空格: 等待授權回應
// TO: 授權交易逾時
// AP: 交易核准
// VD: 訂單取消
// DC: 交易拒絕
// B1: 準備產生(退貨)請款檔
// B2: (退貨)請款檔產生中
// B3: 已產生(退貨)請款檔
// B4: 請款資料匯入中
// B5: 請款成功
// B6: 請款失敗
// RV: 退貨取消
const pendingStatuses = [
'B2',
'B3',
'B4',
' '
];
const forbiddenStatuses = [
'DC',
'TO',
'RV'
];
const finishedStatuses = [
'VD'
];
// txnType 可能值:
// ◼ AU -授權交易
// ◼ VD -取消授權
// ◼ BQ -請款(轉入請款檔)
// ◼ BV -請款取消
// ◼ RF -退貨交易
// ◼ RV -退款取消
if (txnType === 'AU') {
if (statusCode) {
if (pendingStatuses.includes(statusCode)) return 'Pending';
if (forbiddenStatuses.includes(statusCode)) return 'Forbidden';
if (finishedStatuses.includes(statusCode)) return 'None';
if (statusCode === 'AP') return 'AuthRev';
if (statusCode === 'B1') return 'CapRev';
if (statusCode === 'B5') return 'Refund';
if (statusCode === 'B6') return 'Failed';
}
return 'None';
} else if (txnType === 'VD' || txnType === 'BV' || txnType === 'RV') {
return 'None';
} else if (txnType === 'BQ') {
return 'Pending';
} else if (txnType === 'RF') {
return 'Forbidden';
}
// Default to do nothing
return 'None';
}
async function amexSmartCancelOrRefund(config, params) {
const inquiry = await amexInquiry(config, {
merId: params.merId,
lidm: params.lidm,
xid: params.xid,
IN_MAC_KEY: params.IN_MAC_KEY
});
const action = getAmexNextActionFromInquiry(inquiry);
debugPayment(`Determined AMEX action: ${action}, parameters:`, params);
let response;
if (action === 'AuthRev') {
response = await amexAuthRev(config, {
merId: params.merId,
xid: params.xid,
lidm: params.lidm,
purchAmt: params.purchAmt,
orgAmt: params.orgAmt,
IN_MAC_KEY: params.IN_MAC_KEY
});
debugPayment('amexSmartCancelOrRefund AuthRev response:', response);
} else if (action === 'CapRev') {
response = await amexCapRev(config, {
merId: params.merId,
xid: params.xid,
lidm: params.lidm,
purchAmt: params.purchAmt,
orgAmt: params.orgAmt,
IN_MAC_KEY: params.IN_MAC_KEY
});
debugPayment('amexSmartCancelOrRefund CapRev response:', response);
response = await amexAuthRev(config, {
merId: params.merId,
xid: params.xid,
lidm: params.lidm,
purchAmt: params.purchAmt,
orgAmt: params.orgAmt,
IN_MAC_KEY: params.IN_MAC_KEY
});
debugPayment('amexSmartCancelOrRefund AuthRev after CapRev response:', response);
} else if (action === 'Refund') {
response = await amexRefund(config, {
merId: params.merId,
xid: params.xid,
lidm: params.lidm,
IN_MAC_KEY: params.IN_MAC_KEY,
purchAmt: params.purchAmt,
orgAmt: params.orgAmt
});
debugPayment('amexSmartCancelOrRefund Refund response:', response);
} else if (action === 'Pending') {
throw new Error('Transaction is still pending, cannot proceed with cancellation or refund.');
} else if (action === 'Forbidden') {
throw new Error('Transaction is in a forbidden state for cancellation or refund.');
} else if (action === 'Failed') {
throw new Error('Transaction has failed, cannot proceed with cancellation or refund.');
} else {
response = {
...inquiry
};
}
return {
action,
response,
inquiry
};
}
export { CTBCAEGateway, amexAuthRev, amexCancelRefund, amexCapRev, amexInquiry, amexRefund, amexSmartCancelOrRefund, getAmexNextActionFromInquiry };