380 lines
15 KiB
JavaScript
380 lines
15 KiB
JavaScript
"use strict";
|
||
Object.defineProperty(exports, "__esModule", { value: true });
|
||
const tslib_1 = require("tslib");
|
||
const fs_1 = tslib_1.__importDefault(require("fs"));
|
||
const dayjs_1 = tslib_1.__importDefault(require("dayjs"));
|
||
const wechat_pay_nodejs_1 = require("wechat-pay-nodejs");
|
||
const uuid_1 = require("oak-domain/lib/utils/uuid");
|
||
const Exception_1 = require("../../../types/Exception");
|
||
const assert_1 = tslib_1.__importDefault(require("assert"));
|
||
const lodash_1 = require("oak-domain/lib/utils/lodash");
|
||
const WechatPay_debug_1 = tslib_1.__importDefault(require("./WechatPay.debug"));
|
||
const TRADE_STATE_MATRIX = {
|
||
'SUCCESS': 'paid',
|
||
'USERPAYING': 'paying',
|
||
'NOTPAY': 'paying',
|
||
'CLOSED': 'closed',
|
||
'PAYERROR': 'closed',
|
||
'REVOKED': 'closed',
|
||
};
|
||
const REFUND_STATE_MATRIX = {
|
||
'SUCCESS': "successful",
|
||
'CLOSED': "failed",
|
||
"ABNORMAL": "refunding",
|
||
"PROCESSING": 'refunding',
|
||
};
|
||
class WechatPay extends WechatPay_debug_1.default {
|
||
wechatPay;
|
||
refundGapDays;
|
||
mchId;
|
||
publicKeyFilePath;
|
||
privateKeyFilePath;
|
||
apiV3Key;
|
||
payNotifyUrl;
|
||
refundNotifyUrl;
|
||
static MAX_REFUND_DAYS_GAP = 365;
|
||
static MIN_REFUND_DAYS_GAP = 7;
|
||
static DEFAULT_REFUND_DAYS_GAP = 300;
|
||
constructor(wpProduct, appId) {
|
||
super(wpProduct);
|
||
const { wpAccount } = wpProduct;
|
||
this.wpProduct = wpProduct;
|
||
// 这此不记也行,懒得改了
|
||
this.refundGapDays = wpAccount.refundGapDays;
|
||
this.mchId = wpAccount.mchId;
|
||
this.publicKeyFilePath = wpAccount.publicKeyFilePath;
|
||
this.privateKeyFilePath = wpAccount.privateKeyFilePath;
|
||
this.apiV3Key = wpAccount.apiV3Key;
|
||
this.payNotifyUrl = wpAccount.wechatPay.payNotifyUrl;
|
||
this.refundNotifyUrl = wpAccount.wechatPay.refundNotifyUrl;
|
||
this.wechatPay = new wechat_pay_nodejs_1.WechatPay({
|
||
appid: appId,
|
||
mchid: this.mchId,
|
||
cert_private_content: fs_1.default.readFileSync(this.privateKeyFilePath),
|
||
cert_public_content: fs_1.default.readFileSync(this.publicKeyFilePath),
|
||
});
|
||
}
|
||
async refund(refund, context) {
|
||
const { id, price, pay } = refund;
|
||
const serverUrl = context.composeAccessPath();
|
||
const endpoint = this.refundNotifyUrl;
|
||
const refundNotifyUrl = `${serverUrl}/endpoint/${endpoint}/${pay.id}`;
|
||
const result = await this.wechatPay.createRefund({
|
||
out_trade_no: (0, uuid_1.compressTo32)(pay.id),
|
||
out_refund_no: (0, uuid_1.compressTo32)(id),
|
||
notify_url: refundNotifyUrl,
|
||
amount: {
|
||
refund: price,
|
||
total: pay.price,
|
||
currency: 'CNY',
|
||
}
|
||
});
|
||
const { refund_id } = this.analyzeResult(result);
|
||
return {
|
||
externalId: refund_id,
|
||
};
|
||
}
|
||
async getRefundState(refund) {
|
||
const result = await this.wechatPay.queryRefundByOutRefundNo((0, uuid_1.compressTo32)(refund.id));
|
||
const { status, success_time } = this.analyzeResult(result);
|
||
/**
|
||
* 【退款状态】 退款到银行发现用户的卡作废或者冻结了,导致原路退款银行卡失败,可前往商户平台(pay.weixin.qq.com)-交易中心,手动处理此笔退款。
|
||
可选取值:
|
||
SUCCESS: 退款成功
|
||
CLOSED: 退款关闭
|
||
PROCESSING: 退款处理中
|
||
ABNORMAL: 退款异常
|
||
*/
|
||
const iState = REFUND_STATE_MATRIX[status];
|
||
return [iState, success_time ? { successAt: (0, dayjs_1.default)(success_time).valueOf() } : undefined];
|
||
}
|
||
analyzeResult(result) {
|
||
const { success, data, } = result;
|
||
if (!success) {
|
||
console.error(JSON.stringify(result));
|
||
throw new Exception_1.ExternalPayUtilException(result);
|
||
}
|
||
return data;
|
||
}
|
||
caclRefundDeadline(successTime) {
|
||
let gapDays = this.refundGapDays || WechatPay.DEFAULT_REFUND_DAYS_GAP;
|
||
if (gapDays > WechatPay.MAX_REFUND_DAYS_GAP) {
|
||
gapDays = WechatPay.MAX_REFUND_DAYS_GAP;
|
||
}
|
||
if (gapDays < WechatPay.MIN_REFUND_DAYS_GAP) {
|
||
gapDays = WechatPay.MIN_REFUND_DAYS_GAP;
|
||
}
|
||
return (0, dayjs_1.default)(successTime).add(gapDays, 'day').subtract(12, 'hour').valueOf();
|
||
}
|
||
async prepay(pay, data, context) {
|
||
const applicationId = context.getApplicationId();
|
||
const userId = context.getCurrentUserId();
|
||
const serverUrl = context.composeAccessPath();
|
||
const endpoint = this.payNotifyUrl;
|
||
const payNotifyUrl = `${serverUrl}/endpoint/${endpoint}/${pay.id}`;
|
||
switch (this.wpProduct.type) {
|
||
case 'native': {
|
||
const result = await this.wechatPay.prepayNative({
|
||
description: pay.order?.desc || pay.orderId ? '订单支付' : '帐户充值',
|
||
out_trade_no: (0, uuid_1.compressTo32)(pay.id),
|
||
notify_url: payNotifyUrl,
|
||
amount: {
|
||
total: pay.price,
|
||
},
|
||
});
|
||
const { code_url } = this.analyzeResult(result);
|
||
data.externalId = code_url;
|
||
data.meta = {
|
||
codeUrl: code_url,
|
||
};
|
||
break;
|
||
}
|
||
case 'jsapi':
|
||
case 'mp': {
|
||
const [wechatUser] = await context.select('wechatUser', {
|
||
data: {
|
||
id: 1,
|
||
openId: 1,
|
||
},
|
||
filter: {
|
||
applicationId,
|
||
userId,
|
||
}
|
||
}, { dontCollect: true });
|
||
(0, assert_1.default)(wechatUser, `user【${userId}】找不到相应的openId,无法小程序支付`);
|
||
const result = await this.wechatPay.prepayJsapi({
|
||
description: pay.order?.desc || pay.orderId ? '订单支付' : '帐户充值',
|
||
out_trade_no: (0, uuid_1.compressTo32)(pay.id),
|
||
notify_url: payNotifyUrl,
|
||
amount: {
|
||
total: pay.price,
|
||
},
|
||
payer: {
|
||
openid: wechatUser.openId,
|
||
}
|
||
});
|
||
const prepayMeta = this.analyzeResult(result);
|
||
const prepayId = prepayMeta.package.slice(11); // `prepay_id=${prepay_id}`
|
||
data.externalId = prepayId;
|
||
data.meta = {
|
||
...pay.meta,
|
||
prepayMeta,
|
||
payId: userId,
|
||
payOpenId: wechatUser.openId,
|
||
};
|
||
break;
|
||
}
|
||
default: {
|
||
throw new Error('本支付方法还没来的及实现呢');
|
||
}
|
||
}
|
||
}
|
||
async getState(pay) {
|
||
const outTradeNo = (0, uuid_1.compressTo32)(pay.id);
|
||
const result = await this.wechatPay.queryOrderByOutTradeNo(outTradeNo);
|
||
// 返回结果中没有声明这个域
|
||
/**
|
||
* trade_state
|
||
必填
|
||
string(32)
|
||
【交易状态】 交易状态,枚举值:
|
||
* SUCCESS:支付成功
|
||
* REFUND:转入退款
|
||
* NOTPAY:未支付
|
||
* CLOSED:已关闭
|
||
* REVOKED:已撤销(仅付款码支付会返回)
|
||
* USERPAYING:用户支付中(仅付款码支付会返回)
|
||
* PAYERROR:支付失败(仅付款码支付会返回)
|
||
*/
|
||
const { trade_state: tradeState, success_time } = this.analyzeResult(result);
|
||
const iState = TRADE_STATE_MATRIX[tradeState];
|
||
(0, assert_1.default)(iState);
|
||
const updateData = {
|
||
meta: {
|
||
getState: (0, lodash_1.omit)(result, ['mchid', 'appid', 'out_trade_no']),
|
||
},
|
||
};
|
||
if (iState === 'paid') {
|
||
updateData.successAt = (0, dayjs_1.default)(success_time).valueOf();
|
||
}
|
||
return [iState, updateData];
|
||
}
|
||
async close(pay) {
|
||
const outTradeNo = (0, uuid_1.compressTo32)(pay.id);
|
||
const result = await this.wechatPay.closeOrder(outTradeNo);
|
||
this.analyzeResult(result);
|
||
}
|
||
async decodePayNotification(params, body) {
|
||
const { resource } = body;
|
||
if (process.env.NODE_ENV !== 'production') {
|
||
console.log('decodePayNotification-resource', JSON.stringify(resource));
|
||
}
|
||
const { ciphertext, nonce, associated_data } = resource;
|
||
const result2 = this.wechatPay.decryptAesGcm({
|
||
ciphertext,
|
||
nonce,
|
||
apiV3Key: this.apiV3Key,
|
||
associatedData: associated_data,
|
||
});
|
||
const result = JSON.parse(result2);
|
||
if (process.env.NODE_ENV !== 'production') {
|
||
console.log('decodePayNotification-decrypt', JSON.stringify(result));
|
||
}
|
||
/**
|
||
* {
|
||
"transaction_id":"1217752501201407033233368018",
|
||
"amount":{
|
||
"payer_total":100,
|
||
"total":100,
|
||
"currency":"CNY",
|
||
"payer_currency":"CNY"
|
||
},
|
||
"mchid":"1230000109",
|
||
"trade_state":"SUCCESS",
|
||
"bank_type":"CMC",
|
||
"promotion_detail":[
|
||
{
|
||
"amount":100,
|
||
"wechatpay_contribute":0,
|
||
"coupon_id":"109519",
|
||
"scope":"GLOBAL",
|
||
"merchant_contribute":0,
|
||
"name":"单品惠-6",
|
||
"other_contribute":0,
|
||
"currency":"CNY",
|
||
"stock_id":"931386",
|
||
"goods_detail":[
|
||
{
|
||
"goods_remark":"商品备注信息",
|
||
"quantity":1,
|
||
"discount_amount":1,
|
||
"goods_id":"M1006",
|
||
"unit_price":100
|
||
},
|
||
{
|
||
"goods_remark":"商品备注信息",
|
||
"quantity":1,
|
||
"discount_amount":1,
|
||
"goods_id":"M1006",
|
||
"unit_price":100
|
||
}
|
||
]
|
||
},
|
||
{
|
||
"amount":100,
|
||
"wechatpay_contribute":0,
|
||
"coupon_id":"109519",
|
||
"scope":"GLOBAL",
|
||
"merchant_contribute":0,
|
||
"name":"单品惠-6",
|
||
"other_contribute":0,
|
||
"currency":"CNY",
|
||
"stock_id":"931386",
|
||
"goods_detail":[
|
||
{
|
||
"goods_remark":"商品备注信息",
|
||
"quantity":1,
|
||
"discount_amount":1,
|
||
"goods_id":"M1006",
|
||
"unit_price":100
|
||
},
|
||
{
|
||
"goods_remark":"商品备注信息",
|
||
"quantity":1,
|
||
"discount_amount":1,
|
||
"goods_id":"M1006",
|
||
"unit_price":100
|
||
}
|
||
]
|
||
}
|
||
],
|
||
"success_time":"2018-06-08T10:34:56+08:00",
|
||
"payer":{
|
||
"openid":"oUpF8uMuAJO_M2pxb1Q9zNjWeS6o"
|
||
},
|
||
"out_trade_no":"1217752501201407033233368018",
|
||
"AppID":"wxd678efh567hg6787",
|
||
"trade_state_desc":"支付成功",
|
||
"trade_type":"MICROPAY",
|
||
"attach":"自定义数据",
|
||
"scene_info":{
|
||
"device_id":"013467007045764"
|
||
}
|
||
}
|
||
*/
|
||
const { out_trade_no, mchid, trade_state, success_time, } = result;
|
||
const payId = (0, uuid_1.decompressFrom32)(out_trade_no);
|
||
(0, assert_1.default)(mchid === this.mchId);
|
||
const iState = TRADE_STATE_MATRIX[trade_state];
|
||
(0, assert_1.default)(iState);
|
||
const extra = {
|
||
meta: (0, lodash_1.omit)(result, ['mchid',]),
|
||
};
|
||
if (iState === 'paid') {
|
||
extra.successAt = (0, dayjs_1.default)(success_time).valueOf();
|
||
}
|
||
return {
|
||
payId,
|
||
iState,
|
||
extra,
|
||
};
|
||
}
|
||
async decodeRefundNotification(params, body) {
|
||
const { resource } = body;
|
||
if (process.env.NODE_ENV !== 'production') {
|
||
console.log('decodeRefundNotification-resource', JSON.stringify(resource));
|
||
}
|
||
const { ciphertext, nonce, associated_data } = resource;
|
||
const result2 = this.wechatPay.decryptAesGcm({
|
||
ciphertext,
|
||
nonce,
|
||
apiV3Key: this.apiV3Key,
|
||
associatedData: associated_data,
|
||
});
|
||
const result = JSON.parse(result2);
|
||
if (process.env.NODE_ENV !== 'production') {
|
||
console.log('decodeRefundNotification-decrypt', JSON.stringify(result));
|
||
}
|
||
/**
|
||
* 对resource对象进行解密后,得到的资源对象示例
|
||
* https://pay.weixin.qq.com/docs/merchant/apis/jsapi-payment/refund-result-notice.html
|
||
* {
|
||
"mchid": "1900000100",
|
||
"transaction_id": "1008450740201411110005820873",
|
||
"out_trade_no": "20150806125346",
|
||
"refund_id": "50200207182018070300011301001",
|
||
"out_refund_no": "7752501201407033233368018",
|
||
"refund_status": "SUCCESS",
|
||
"success_time": "2018-06-08T10:34:56+08:00",
|
||
"user_received_account": "招商银行信用卡0403",
|
||
"amount" : {
|
||
"total": 999,
|
||
"refund": 999,
|
||
"payer_total": 999,
|
||
"payer_refund": 999
|
||
}
|
||
}
|
||
*/
|
||
const { out_refund_no, mchid, refund_status, success_time, } = result;
|
||
const refundId = (0, uuid_1.decompressFrom32)(out_refund_no);
|
||
(0, assert_1.default)(mchid === this.mchId);
|
||
const iState = REFUND_STATE_MATRIX[refund_status];
|
||
(0, assert_1.default)(iState);
|
||
const extra = {
|
||
meta: (0, lodash_1.omit)(result, ['mchid',]),
|
||
};
|
||
if (iState === 'successful') {
|
||
extra.successAt = (0, dayjs_1.default)(success_time).valueOf();
|
||
}
|
||
return {
|
||
refundId,
|
||
iState,
|
||
extra,
|
||
};
|
||
}
|
||
getRefundableAt(successAt) {
|
||
return this.caclRefundDeadline(successAt);
|
||
}
|
||
}
|
||
exports.default = WechatPay;
|