diff --git a/es/components/AbstractComponents.d.ts b/es/components/AbstractComponents.d.ts index 14d10d0c..5e13736d 100644 --- a/es/components/AbstractComponents.d.ts +++ b/es/components/AbstractComponents.d.ts @@ -2,6 +2,7 @@ * 抽象组件在业务层根据EntityDict的重新声明 * by Xc 20230807 */ +import React from 'react'; import { EntityDict } from '../oak-app-domain'; import { ReactComponentProps, ColumnProps, RowWithActions, OakExtraActionProps, OakAbsAttrDef, onActionFnDef, ListButtonProps, OakAbsAttrUpsertDef } from 'oak-frontend-base'; declare const FilterPanel: (props: ReactComponentProps; reason?: Text | null; + successAt?: Datetime | null; iState?: IState | null; }; export type OpAttr = keyof OpSchema; @@ -30,6 +31,7 @@ export type Schema = EntityShape & { price: Price; creatorId: ForeignKey<"user">; reason?: Text | null; + successAt?: Datetime | null; iState?: IState | null; withdraw?: Withdraw.Schema | null; pay: Pay.Schema; @@ -57,6 +59,7 @@ type AttrFilter = { creatorId: Q_StringValue; creator: User.Filter; reason: Q_StringValue; + successAt: Q_DateValue; iState: Q_EnumValue; sysAccountOper$refund: SysAccountOper.Filter & SubQueryPredicateMetadata; accountOper$entity: AccountOper.Filter & SubQueryPredicateMetadata; @@ -80,6 +83,7 @@ export type Projection = { creatorId?: number; creator?: User.Projection; reason?: number; + successAt?: number; iState?: number; sysAccountOper$refund?: SysAccountOper.Selection & { $entity: "sysAccountOper"; @@ -134,6 +138,8 @@ export type SortAttr = { creator: User.SortAttr; } | { reason: number; +} | { + successAt: number; } | { iState: number; } | { diff --git a/es/oak-app-domain/Refund/Storage.js b/es/oak-app-domain/Refund/Storage.js index 0a1adfa1..3795e023 100644 --- a/es/oak-app-domain/Refund/Storage.js +++ b/es/oak-app-domain/Refund/Storage.js @@ -35,6 +35,9 @@ export const desc = { reason: { type: "text" }, + successAt: { + type: "datetime" + }, iState: { type: "enum", enumeration: ["refunding", "successful", "failed"] diff --git a/es/oak-app-domain/Refund/locales/zh_CN.json b/es/oak-app-domain/Refund/locales/zh_CN.json index c44716b4..d53886ab 100644 --- a/es/oak-app-domain/Refund/locales/zh_CN.json +++ b/es/oak-app-domain/Refund/locales/zh_CN.json @@ -1 +1 @@ -{ "name": "帐户", "attr": { "pay": "关联支付", "price": "价格", "loss": "损耗", "withdraw": "关联提现", "meta": "metadata", "externalId": "外部退款流水号", "iState": "状态", "creator": "创建者", "reason": "原因", "opers": "相关账户操作" }, "action": { "succeed": "退款成功", "fail": "退款失败" }, "v": { "iState": { "refunding": "退款中", "successful": "退款成功", "failed": "退款失败" } } } +{ "name": "帐户", "attr": { "pay": "关联支付", "price": "价格", "loss": "损耗", "withdraw": "关联提现", "meta": "metadata", "externalId": "外部退款流水号", "iState": "状态", "creator": "创建者", "reason": "原因", "opers": "相关账户操作", "successAt": "退款成功时间" }, "action": { "succeed": "退款成功", "fail": "退款失败" }, "v": { "iState": { "refunding": "退款中", "successful": "退款成功", "failed": "退款失败" } } } diff --git a/es/triggers/refund.js b/es/triggers/refund.js index bd78fdf1..3951edec 100644 --- a/es/triggers/refund.js +++ b/es/triggers/refund.js @@ -322,7 +322,7 @@ const triggers = [ return; } } - const data = await payClazz.refund(refund); + const data = await payClazz.refund(refund, context); if (data) { assert(data.externalId); const closeFn = context.openRootMode(); diff --git a/es/triggers/withdraw.d.ts b/es/triggers/withdraw.d.ts index e69f8d6e..979a44c8 100644 --- a/es/triggers/withdraw.d.ts +++ b/es/triggers/withdraw.d.ts @@ -6,6 +6,6 @@ import { BRC } from '../types/RuntimeCxt'; * @param context * @param refunds */ -export declare function updateWithdrawState(context: BRC, id: string): Promise<0 | 1>; +export declare function updateWithdrawState(context: BRC, id: string): Promise<1 | 0>; declare const triggers: Trigger[]; export default triggers; diff --git a/es/types/Exception.d.ts b/es/types/Exception.d.ts index 46a5ad34..2b2e4c78 100644 --- a/es/types/Exception.d.ts +++ b/es/types/Exception.d.ts @@ -1,6 +1,6 @@ import { OakException, OpRecord } from 'oak-domain/lib/types'; import { EntityDict } from '../oak-app-domain/index'; -export declare class ExternalPrePayException extends OakException { +export declare class ExternalPayUtilException extends OakException { reason: any; constructor(reason: any, message?: string); getSerialData(): { diff --git a/es/types/Exception.js b/es/types/Exception.js index f0c81e2f..1d0a098b 100644 --- a/es/types/Exception.js +++ b/es/types/Exception.js @@ -1,9 +1,9 @@ import { OakException } from 'oak-domain/lib/types'; import makeDepedentException from './DependentExceptions'; -export class ExternalPrePayException extends OakException { +export class ExternalPayUtilException extends OakException { reason; constructor(reason, message) { - super(message || '调用外部支付预下单接口失败'); + super(message || '调用外部支付渠道接口失败'); this.reason = reason; } getSerialData() { @@ -28,7 +28,7 @@ export function makeException(msg) { const { name, message } = data; switch (name) { case 'ExternalPrePayException': { - exception = new ExternalPrePayException(data.reason, message); + exception = new ExternalPayUtilException(data.reason, message); break; } case 'RefundExceedMax': { diff --git a/es/types/PayClazz.d.ts b/es/types/PayClazz.d.ts index b7b82191..f0aa1d4f 100644 --- a/es/types/PayClazz.d.ts +++ b/es/types/PayClazz.d.ts @@ -1,6 +1,7 @@ import { EntityDict } from '../oak-app-domain'; import { BRC } from '../types/RuntimeCxt'; type IState = EntityDict['pay']['OpSchema']['iState']; +type RefundIState = EntityDict['refund']['OpSchema']['iState']; export default interface PayClazz { getAccountEntity(): [string, string]; getAccountAmount(context: BRC): Promise; @@ -16,8 +17,13 @@ export default interface PayClazz { extra?: EntityDict['pay']['Update']['data']; price?: number; }>; - refund(refund: EntityDict['refund']['OpSchema']): Promise; - closeRefund(refund: EntityDict['refund']['OpSchema']): Promise; + decodeRefundNotification(params: Record, body: any): Promise<{ + refundId: string; + iState: RefundIState; + extra?: EntityDict['refund']['Update']['data']; + price?: number; + }>; + refund(refund: EntityDict['refund']['Schema'], context: BRC): Promise; getRefundableAt(successAt: number): number; getRefundState(refund: EntityDict['refund']['OpSchema']): Promise<[EntityDict['refund']['OpSchema']['iState'], EntityDict['refund']['Update']['data'] | undefined]>; } diff --git a/es/utils/payClazz/Account.d.ts b/es/utils/payClazz/Account.d.ts index 46d91978..a1f1489e 100644 --- a/es/utils/payClazz/Account.d.ts +++ b/es/utils/payClazz/Account.d.ts @@ -9,13 +9,18 @@ export default class Account implements PayClazz { calcPayTax(): [number, string, string]; getRefundableAt(successAt: number): number; refund(refund: EntityDict['refund']['Schema']): Promise; - closeRefund(refund: EntityDict['refund']['Schema']): Promise; getRefundState(refund: EntityDict['refund']['Schema']): Promise<[EntityDict['refund']['Schema']['iState'], undefined]>; decodePayNotification(params: Record, body: any): Promise<{ payId: string; iState: string | null | undefined; extra?: EntityDict['pay']['Update']['data'] | undefined; }>; + decodeRefundNotification(params: Record, body: any): Promise<{ + refundId: string; + iState: string | null | undefined; + extra?: EntityDict['refund']['Update']['data'] | undefined; + price?: number | undefined; + }>; prepay(pay: EntityDict['pay']['Schema'], data: EntityDict['pay']['Update']['data'], context: BRC): Promise; getState(pay: EntityDict['pay']['Schema']): Promise<[string, EntityDict['pay']['Update']['data']]>; close(pay: EntityDict['pay']['Schema']): Promise; diff --git a/es/utils/payClazz/Account.js b/es/utils/payClazz/Account.js index 2c2944ba..104e49d3 100644 --- a/es/utils/payClazz/Account.js +++ b/es/utils/payClazz/Account.js @@ -23,15 +23,15 @@ export default class Account { async refund(refund) { return; } - async closeRefund(refund) { - return; - } async getRefundState(refund) { return ['refuding', undefined]; } decodePayNotification(params, body) { throw new Error("account类型的pay不需调用此接口"); } + decodeRefundNotification(params, body) { + throw new Error("account类型的pay不需调用此接口"); + } async prepay(pay, data, context) { const { entity, entityId, price } = pay; assert(entity === 'account' && entityId); diff --git a/es/utils/payClazz/Offline.d.ts b/es/utils/payClazz/Offline.d.ts index 5065983c..bb1d76ba 100644 --- a/es/utils/payClazz/Offline.d.ts +++ b/es/utils/payClazz/Offline.d.ts @@ -13,13 +13,18 @@ export default class Offline implements PayClazz { calcPayTax(price: number): [number, string, string]; getRefundableAt(successTime: number): number; refund(refund: RefundOpSchema): Promise; - closeRefund(refund: RefundOpSchema): Promise; getRefundState(refund: RefundOpSchema): Promise<[PayOpSchema['iState'], PayUpdateData | undefined]>; decodePayNotification(params: Record, body: any): Promise<{ payId: string; iState: string | null | undefined; extra?: PayUpdateData | undefined; }>; + decodeRefundNotification(params: Record, body: any): Promise<{ + refundId: string; + iState: string | null | undefined; + extra?: EntityDict['refund']['Update']['data'] | undefined; + price?: number | undefined; + }>; prepay(pay: PayOpSchema, data: PayUpdateData, context: BRC): Promise; getState(pay: PayOpSchema): Promise<[string, PayUpdateData]>; close(pay: PayOpSchema): Promise; diff --git a/es/utils/payClazz/Offline.js b/es/utils/payClazz/Offline.js index e3946cab..0ff1c664 100644 --- a/es/utils/payClazz/Offline.js +++ b/es/utils/payClazz/Offline.js @@ -49,10 +49,6 @@ export default class Offline { // 啥也不做 return; } - async closeRefund(refund) { - // 啥也不做 - return; - } async getRefundState(refund) { const { iState } = refund; assert(iState === 'refunding'); @@ -61,6 +57,9 @@ export default class Offline { decodePayNotification(params, body) { throw new Error("offline类型的pay不需调用此接口"); } + decodeRefundNotification(params, body) { + throw new Error("offline类型的pay不需调用此接口"); + } async prepay(pay, data, context) { data.phantom3 = Math.ceil(Math.random() * 1000000); return; diff --git a/es/utils/payClazz/WechatPay/WechatPay.d.ts b/es/utils/payClazz/WechatPay/WechatPay.d.ts index ee6a145c..d968a9dc 100644 --- a/es/utils/payClazz/WechatPay/WechatPay.d.ts +++ b/es/utils/payClazz/WechatPay/WechatPay.d.ts @@ -3,7 +3,7 @@ import PayClazz from "../../../types/PayClazz"; import { BRC } from "../../../types/RuntimeCxt"; import { WechatPay as WechatPaySDK } from 'wechat-pay-nodejs'; import { EntityDict } from "../../../oak-app-domain"; -import { OpSchema as OpRefund, UpdateOperationData as RefundUpdateData } from "../../../oak-app-domain/Refund/Schema"; +import { OpSchema as OpRefund, UpdateOperationData as RefundUpdateData, Schema as Refund } from "../../../oak-app-domain/Refund/Schema"; import { Schema as WpProduct } from '../../../oak-app-domain/WpProduct/Schema'; import WechatPayDebug from './WechatPay.debug'; export default class WechatPay extends WechatPayDebug implements PayClazz { @@ -19,10 +19,9 @@ export default class WechatPay extends WechatPayDebug implements PayClazz { static MIN_REFUND_DAYS_GAP: number; static DEFAULT_REFUND_DAYS_GAP: number; constructor(wpProduct: WpProduct, appId: string); - refund(refund: OpRefund): Promise; - closeRefund(refund: OpRefund): Promise; - getRefundState(refund: OpRefund): Promise<[string | null | undefined, PayUpdateData | undefined]>; - private analyzePrepayResult; + refund(refund: Refund, context: BRC): Promise; + getRefundState(refund: OpRefund): Promise<[string | null | undefined, RefundUpdateData | undefined]>; + private analyzeResult; private caclRefundDeadline; prepay(pay: Pay, data: PayUpdateData, context: BRC): Promise; getState(pay: OpPay): Promise<[EntityDict['pay']['OpSchema']['iState'], PayUpdateData]>; @@ -32,5 +31,11 @@ export default class WechatPay extends WechatPayDebug implements PayClazz { iState: EntityDict['pay']['OpSchema']['iState']; extra?: PayUpdateData; }>; + decodeRefundNotification(params: Record, body: any): Promise<{ + refundId: string; + iState: string | null | undefined; + extra?: RefundUpdateData | undefined; + price?: number | undefined; + }>; getRefundableAt(successAt: number): number; } diff --git a/es/utils/payClazz/WechatPay/WechatPay.debug.d.ts b/es/utils/payClazz/WechatPay/WechatPay.debug.d.ts index ff86d018..e7270e20 100644 --- a/es/utils/payClazz/WechatPay/WechatPay.debug.d.ts +++ b/es/utils/payClazz/WechatPay/WechatPay.debug.d.ts @@ -8,13 +8,18 @@ export declare function registerGetPayStateResult(payState: NonNullable, body: any): Promise<{ + refundId: string; + iState: string | null | undefined; + extra?: RefundUpdateData | undefined; + price?: number | undefined; + }>; getAccountEntity(): [string, string]; getAccountAmount(context: BRC): Promise; calcTransferTax(price: number): [number, string, string]; calcRefundTax(price: number): [number, string, string]; calcPayTax(price: number): [number, string, string]; - refund(refund: Refund): Promise; - closeRefund(refund: Refund): Promise; + refund(refund: Refund, context: BRC): Promise; getRefundState(refund: Refund): Promise<[string | null | undefined, PayUpdateData | undefined]>; decodePayNotification(params: Record, body: any): Promise<{ payId: string; diff --git a/es/utils/payClazz/WechatPay/WechatPay.debug.js b/es/utils/payClazz/WechatPay/WechatPay.debug.js index 310948e9..d04352b3 100644 --- a/es/utils/payClazz/WechatPay/WechatPay.debug.js +++ b/es/utils/payClazz/WechatPay/WechatPay.debug.js @@ -8,6 +8,9 @@ export default class WechatPay { constructor(wpProduct) { this.wpProduct = wpProduct; } + decodeRefundNotification(params, body) { + throw new Error('Method not implemented.'); + } getAccountEntity() { return ['wpAccount', this.wpProduct.wpAccountId]; } @@ -49,14 +52,11 @@ export default class WechatPay { assert(typeof taxLossRatio === 'number', '微信渠道的手续费率未配置'); return [Math.round(price * taxLossRatio / 100), 'wpAccount', wpProduct.wpAccountId]; } - async refund(refund) { + async refund(refund, context) { return { externalId: Math.random().toString(), }; } - async closeRefund(refund) { - return; - } async getRefundState(refund) { const r = Math.random(); if (r < 0.5) { diff --git a/es/utils/payClazz/WechatPay/WechatPay.js b/es/utils/payClazz/WechatPay/WechatPay.js index 7ca2bf88..772f51f7 100644 --- a/es/utils/payClazz/WechatPay/WechatPay.js +++ b/es/utils/payClazz/WechatPay/WechatPay.js @@ -2,7 +2,7 @@ import fs from 'fs'; import dayJs from 'dayjs'; import { WechatPay as WechatPaySDK } from 'wechat-pay-nodejs'; import { compressTo32, decompressFrom32 } from 'oak-domain/lib/utils/uuid'; -import { ExternalPrePayException } from "../../../types/Exception"; +import { ExternalPayUtilException } from "../../../types/Exception"; import assert from "assert"; import { omit } from "oak-domain/lib/utils/lodash"; import WechatPayDebug from './WechatPay.debug'; @@ -14,6 +14,12 @@ const TRADE_STATE_MATRIX = { 'PAYERROR': 'closed', 'REVOKED': 'closed', }; +const REFUND_STATE_MATRIX = { + 'SUCCESS': "successful", + 'CLOSED': "failed", + "ABNORMAL": "refunding", + "PROCESSING": 'refunding', +}; export default class WechatPay extends WechatPayDebug { wechatPay; refundGapDays; @@ -45,20 +51,45 @@ export default class WechatPay extends WechatPayDebug { cert_public_content: fs.readFileSync(this.publicKeyFilePath), }); } - refund(refund) { - throw new Error("Method not implemented."); + 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: compressTo32(pay.id), + out_refund_no: compressTo32(id), + notify_url: refundNotifyUrl, + amount: { + refund: price, + total: pay.price, + currency: 'CNY', + } + }); + const { refund_id } = this.analyzeResult(result); + return { + externalId: refund_id, + }; } - closeRefund(refund) { - throw new Error("Method not implemented."); + async getRefundState(refund) { + const result = await this.wechatPay.queryRefundByOutRefundNo(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: dayJs(success_time).millisecond() } : undefined]; } - getRefundState(refund) { - throw new Error("Method not implemented."); - } - analyzePrepayResult(result) { + analyzeResult(result) { const { success, data, } = result; if (!success) { console.error(JSON.stringify(result)); - throw new ExternalPrePayException(result); + throw new ExternalPayUtilException(result); } return data; } @@ -88,7 +119,7 @@ export default class WechatPay extends WechatPayDebug { total: pay.price, }, }); - const { code_url } = this.analyzePrepayResult(result); + const { code_url } = this.analyzeResult(result); data.externalId = code_url; data.meta = { codeUrl: code_url, @@ -119,7 +150,7 @@ export default class WechatPay extends WechatPayDebug { openid: wechatUser.openId, } }); - const prepayMeta = this.analyzePrepayResult(result); + const prepayMeta = this.analyzeResult(result); const prepayId = prepayMeta.package.slice(11); // `prepay_id=${prepay_id}` data.externalId = prepayId; data.meta = { @@ -152,7 +183,7 @@ export default class WechatPay extends WechatPayDebug { * USERPAYING:用户支付中(仅付款码支付会返回) * PAYERROR:支付失败(仅付款码支付会返回) */ - const { trade_state: tradeState, success_time } = this.analyzePrepayResult(result); + const { trade_state: tradeState, success_time } = this.analyzeResult(result); const iState = TRADE_STATE_MATRIX[tradeState]; assert(iState); const updateData = { @@ -168,7 +199,7 @@ export default class WechatPay extends WechatPayDebug { async close(pay) { const outTradeNo = compressTo32(pay.id); const result = await this.wechatPay.closeOrder(outTradeNo); - this.analyzePrepayResult(result); + this.analyzeResult(result); } async decodePayNotification(params, body) { const { resource } = body; @@ -285,6 +316,59 @@ export default class WechatPay extends WechatPayDebug { 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 = decompressFrom32(out_refund_no); + assert(mchid === this.mchId); + const iState = REFUND_STATE_MATRIX[refund_status]; + assert(iState); + const extra = { + meta: omit(result, ['mchid',]), + }; + if (iState === 'successful') { + extra.successAt = success_time; + } + return { + refundId, + iState, + extra, + }; + } getRefundableAt(successAt) { return this.caclRefundDeadline(successAt); } diff --git a/es/utils/payClazz/WechatPay/index.d.ts b/es/utils/payClazz/WechatPay/index.d.ts index eeba2b4b..ce1d041a 100644 --- a/es/utils/payClazz/WechatPay/index.d.ts +++ b/es/utils/payClazz/WechatPay/index.d.ts @@ -1,4 +1,4 @@ import WechatPay from './WechatPay'; import WechatPayDebug from './WechatPay.debug'; -declare const _default: typeof WechatPayDebug | typeof WechatPay; +declare const _default: typeof WechatPay | typeof WechatPayDebug; export default _default; diff --git a/lib/aspects/withdraw.d.ts b/lib/aspects/withdraw.d.ts index 2ea225ee..8ad7334d 100644 --- a/lib/aspects/withdraw.d.ts +++ b/lib/aspects/withdraw.d.ts @@ -12,8 +12,8 @@ export declare function getWithdrawCreateData(params: { creatorId: string; creator?: import("../oak-app-domain/User/Schema").UpdateOperation | undefined; } & { - refund$withdraw?: import("oak-domain/lib/types").Operation, Omit> | import("oak-domain/lib/types").Operation<"create", Omit[]> | (import("oak-domain/lib/types").Operation, Omit> | import("oak-domain/lib/types").Operation<"create", Omit>)[] | undefined; - withdrawTransfer$withdraw?: import("oak-domain/lib/types").Operation, Omit> | import("oak-domain/lib/types").Operation<"create", Omit[]> | (import("oak-domain/lib/types").Operation, Omit> | import("oak-domain/lib/types").Operation<"create", Omit>)[] | undefined; + refund$withdraw?: import("oak-domain/lib/types").Operation, Omit> | import("oak-domain/lib/types").Operation<"create", Omit[]> | (import("oak-domain/lib/types").Operation, Omit> | import("oak-domain/lib/types").Operation<"create", Omit>)[] | undefined; + withdrawTransfer$withdraw?: import("oak-domain/lib/types").Operation, Omit> | import("oak-domain/lib/types").Operation<"create", Omit[]> | (import("oak-domain/lib/types").Operation, Omit> | import("oak-domain/lib/types").Operation<"create", Omit>)[] | undefined; modiEntity$entity?: import("oak-domain/lib/types").Operation<"create", Omit[]> | import("oak-domain/lib/types").Operation<"create", Omit>[] | undefined; operEntity$entity?: import("oak-domain/lib/types").Operation<"create", Omit[]> | import("oak-domain/lib/types").Operation<"create", Omit>[] | undefined; accountOper$entity?: import("oak-domain/lib/types").Operation<"create", Omit[]> | import("oak-domain/lib/types").Operation<"create", Omit>[] | undefined; @@ -26,8 +26,8 @@ export declare function getWithdrawCreateData(params: { creator?: undefined; creatorId: string; } & { - refund$withdraw?: import("oak-domain/lib/types").Operation, Omit> | import("oak-domain/lib/types").Operation<"create", Omit[]> | (import("oak-domain/lib/types").Operation, Omit> | import("oak-domain/lib/types").Operation<"create", Omit>)[] | undefined; - withdrawTransfer$withdraw?: import("oak-domain/lib/types").Operation, Omit> | import("oak-domain/lib/types").Operation<"create", Omit[]> | (import("oak-domain/lib/types").Operation, Omit> | import("oak-domain/lib/types").Operation<"create", Omit>)[] | undefined; + refund$withdraw?: import("oak-domain/lib/types").Operation, Omit> | import("oak-domain/lib/types").Operation<"create", Omit[]> | (import("oak-domain/lib/types").Operation, Omit> | import("oak-domain/lib/types").Operation<"create", Omit>)[] | undefined; + withdrawTransfer$withdraw?: import("oak-domain/lib/types").Operation, Omit> | import("oak-domain/lib/types").Operation<"create", Omit[]> | (import("oak-domain/lib/types").Operation, Omit> | import("oak-domain/lib/types").Operation<"create", Omit>)[] | undefined; modiEntity$entity?: import("oak-domain/lib/types").Operation<"create", Omit[]> | import("oak-domain/lib/types").Operation<"create", Omit>[] | undefined; operEntity$entity?: import("oak-domain/lib/types").Operation<"create", Omit[]> | import("oak-domain/lib/types").Operation<"create", Omit>[] | undefined; accountOper$entity?: import("oak-domain/lib/types").Operation<"create", Omit[]> | import("oak-domain/lib/types").Operation<"create", Omit>[] | undefined; @@ -40,8 +40,8 @@ export declare function getWithdrawCreateData(params: { creatorId: string; creator?: import("../oak-app-domain/User/Schema").UpdateOperation | undefined; } & { - refund$withdraw?: import("oak-domain/lib/types").Operation, Omit> | import("oak-domain/lib/types").Operation<"create", Omit[]> | (import("oak-domain/lib/types").Operation, Omit> | import("oak-domain/lib/types").Operation<"create", Omit>)[] | undefined; - withdrawTransfer$withdraw?: import("oak-domain/lib/types").Operation, Omit> | import("oak-domain/lib/types").Operation<"create", Omit[]> | (import("oak-domain/lib/types").Operation, Omit> | import("oak-domain/lib/types").Operation<"create", Omit>)[] | undefined; + refund$withdraw?: import("oak-domain/lib/types").Operation, Omit> | import("oak-domain/lib/types").Operation<"create", Omit[]> | (import("oak-domain/lib/types").Operation, Omit> | import("oak-domain/lib/types").Operation<"create", Omit>)[] | undefined; + withdrawTransfer$withdraw?: import("oak-domain/lib/types").Operation, Omit> | import("oak-domain/lib/types").Operation<"create", Omit[]> | (import("oak-domain/lib/types").Operation, Omit> | import("oak-domain/lib/types").Operation<"create", Omit>)[] | undefined; modiEntity$entity?: import("oak-domain/lib/types").Operation<"create", Omit[]> | import("oak-domain/lib/types").Operation<"create", Omit>[] | undefined; operEntity$entity?: import("oak-domain/lib/types").Operation<"create", Omit[]> | import("oak-domain/lib/types").Operation<"create", Omit>[] | undefined; accountOper$entity?: import("oak-domain/lib/types").Operation<"create", Omit[]> | import("oak-domain/lib/types").Operation<"create", Omit>[] | undefined; @@ -54,8 +54,8 @@ export declare function getWithdrawCreateData(params: { creator?: undefined; creatorId: string; } & { - refund$withdraw?: import("oak-domain/lib/types").Operation, Omit> | import("oak-domain/lib/types").Operation<"create", Omit[]> | (import("oak-domain/lib/types").Operation, Omit> | import("oak-domain/lib/types").Operation<"create", Omit>)[] | undefined; - withdrawTransfer$withdraw?: import("oak-domain/lib/types").Operation, Omit> | import("oak-domain/lib/types").Operation<"create", Omit[]> | (import("oak-domain/lib/types").Operation, Omit> | import("oak-domain/lib/types").Operation<"create", Omit>)[] | undefined; + refund$withdraw?: import("oak-domain/lib/types").Operation, Omit> | import("oak-domain/lib/types").Operation<"create", Omit[]> | (import("oak-domain/lib/types").Operation, Omit> | import("oak-domain/lib/types").Operation<"create", Omit>)[] | undefined; + withdrawTransfer$withdraw?: import("oak-domain/lib/types").Operation, Omit> | import("oak-domain/lib/types").Operation<"create", Omit[]> | (import("oak-domain/lib/types").Operation, Omit> | import("oak-domain/lib/types").Operation<"create", Omit>)[] | undefined; modiEntity$entity?: import("oak-domain/lib/types").Operation<"create", Omit[]> | import("oak-domain/lib/types").Operation<"create", Omit>[] | undefined; operEntity$entity?: import("oak-domain/lib/types").Operation<"create", Omit[]> | import("oak-domain/lib/types").Operation<"create", Omit>[] | undefined; accountOper$entity?: import("oak-domain/lib/types").Operation<"create", Omit[]> | import("oak-domain/lib/types").Operation<"create", Omit>[] | undefined; diff --git a/lib/context/BackendRuntimeContext.d.ts b/lib/context/BackendRuntimeContext.d.ts index e8536354..11c2072c 100644 --- a/lib/context/BackendRuntimeContext.d.ts +++ b/lib/context/BackendRuntimeContext.d.ts @@ -15,7 +15,7 @@ export declare class BackendRuntimeContext | undefined; + config?: number | import("oak-domain/lib/types").JsonProjection | undefined; style?: number | import("oak-domain/lib/types").JsonProjection | undefined; domainId?: number | undefined; domain?: import("../oak-app-domain/Domain/Schema").Projection | undefined; diff --git a/lib/entities/Refund.d.ts b/lib/entities/Refund.d.ts index d5831cb7..782860a0 100644 --- a/lib/entities/Refund.d.ts +++ b/lib/entities/Refund.d.ts @@ -1,4 +1,4 @@ -import { String, Text, Price } from 'oak-domain/lib/types/DataType'; +import { String, Text, Price, Datetime } from 'oak-domain/lib/types/DataType'; import { EntityShape } from 'oak-domain/lib/types/Entity'; import { EntityDesc, ActionDef } from 'oak-domain/lib/types'; import { Schema as User } from './User'; @@ -14,6 +14,7 @@ export interface Schema extends EntityShape { price: Price; creator: User; reason?: Text; + successAt?: Datetime; opers: AccountOper[]; } export type IAction = 'succeed' | 'fail'; diff --git a/lib/entities/Refund.js b/lib/entities/Refund.js index 99f9e8d8..b4ec7276 100644 --- a/lib/entities/Refund.js +++ b/lib/entities/Refund.js @@ -23,7 +23,8 @@ exports.entityDesc = { iState: '状态', creator: '创建者', reason: '原因', - opers: '相关账户操作' + opers: '相关账户操作', + successAt: '退款成功时间' }, action: { succeed: '退款成功', diff --git a/lib/oak-app-domain/Refund/Schema.d.ts b/lib/oak-app-domain/Refund/Schema.d.ts index a1f72e9a..cc1e401d 100644 --- a/lib/oak-app-domain/Refund/Schema.d.ts +++ b/lib/oak-app-domain/Refund/Schema.d.ts @@ -3,7 +3,7 @@ import { Q_DateValue, Q_NumberValue, Q_StringValue, Q_EnumValue, NodeId, MakeFil import { OneOf } from "oak-domain/lib/types/Polyfill"; import { FormCreateData, FormUpdateData, DeduceAggregation, Operation as OakOperation, Selection as OakSelection, MakeAction as OakMakeAction, AggregationResult, EntityShape } from "oak-domain/lib/types/Entity"; import { Action, ParticularAction, IState } from "./Action"; -import { Price, String, Text } from "oak-domain/lib/types/DataType"; +import { Price, String, Text, Datetime } from "oak-domain/lib/types/DataType"; import * as Withdraw from "../Withdraw/Schema"; import * as Pay from "../Pay/Schema"; import * as User from "../User/Schema"; @@ -18,6 +18,7 @@ export type OpSchema = EntityShape & { price: Price; creatorId: ForeignKey<"user">; reason?: Text | null; + successAt?: Datetime | null; iState?: IState | null; }; export type OpAttr = keyof OpSchema; @@ -30,6 +31,7 @@ export type Schema = EntityShape & { price: Price; creatorId: ForeignKey<"user">; reason?: Text | null; + successAt?: Datetime | null; iState?: IState | null; withdraw?: Withdraw.Schema | null; pay: Pay.Schema; @@ -57,6 +59,7 @@ type AttrFilter = { creatorId: Q_StringValue; creator: User.Filter; reason: Q_StringValue; + successAt: Q_DateValue; iState: Q_EnumValue; sysAccountOper$refund: SysAccountOper.Filter & SubQueryPredicateMetadata; accountOper$entity: AccountOper.Filter & SubQueryPredicateMetadata; @@ -80,6 +83,7 @@ export type Projection = { creatorId?: number; creator?: User.Projection; reason?: number; + successAt?: number; iState?: number; sysAccountOper$refund?: SysAccountOper.Selection & { $entity: "sysAccountOper"; @@ -134,6 +138,8 @@ export type SortAttr = { creator: User.SortAttr; } | { reason: number; +} | { + successAt: number; } | { iState: number; } | { diff --git a/lib/oak-app-domain/Refund/Storage.js b/lib/oak-app-domain/Refund/Storage.js index bcdd8fb9..35e3489e 100644 --- a/lib/oak-app-domain/Refund/Storage.js +++ b/lib/oak-app-domain/Refund/Storage.js @@ -38,6 +38,9 @@ exports.desc = { reason: { type: "text" }, + successAt: { + type: "datetime" + }, iState: { type: "enum", enumeration: ["refunding", "successful", "failed"] diff --git a/lib/oak-app-domain/Refund/locales/zh_CN.json b/lib/oak-app-domain/Refund/locales/zh_CN.json index c44716b4..d53886ab 100644 --- a/lib/oak-app-domain/Refund/locales/zh_CN.json +++ b/lib/oak-app-domain/Refund/locales/zh_CN.json @@ -1 +1 @@ -{ "name": "帐户", "attr": { "pay": "关联支付", "price": "价格", "loss": "损耗", "withdraw": "关联提现", "meta": "metadata", "externalId": "外部退款流水号", "iState": "状态", "creator": "创建者", "reason": "原因", "opers": "相关账户操作" }, "action": { "succeed": "退款成功", "fail": "退款失败" }, "v": { "iState": { "refunding": "退款中", "successful": "退款成功", "failed": "退款失败" } } } +{ "name": "帐户", "attr": { "pay": "关联支付", "price": "价格", "loss": "损耗", "withdraw": "关联提现", "meta": "metadata", "externalId": "外部退款流水号", "iState": "状态", "creator": "创建者", "reason": "原因", "opers": "相关账户操作", "successAt": "退款成功时间" }, "action": { "succeed": "退款成功", "fail": "退款失败" }, "v": { "iState": { "refunding": "退款中", "successful": "退款成功", "failed": "退款失败" } } } diff --git a/lib/triggers/refund.js b/lib/triggers/refund.js index 4a4902da..2ad03b53 100644 --- a/lib/triggers/refund.js +++ b/lib/triggers/refund.js @@ -325,7 +325,7 @@ const triggers = [ return; } } - const data = await payClazz.refund(refund); + const data = await payClazz.refund(refund, context); if (data) { (0, assert_1.default)(data.externalId); const closeFn = context.openRootMode(); diff --git a/lib/types/Exception.d.ts b/lib/types/Exception.d.ts index 46a5ad34..2b2e4c78 100644 --- a/lib/types/Exception.d.ts +++ b/lib/types/Exception.d.ts @@ -1,6 +1,6 @@ import { OakException, OpRecord } from 'oak-domain/lib/types'; import { EntityDict } from '../oak-app-domain/index'; -export declare class ExternalPrePayException extends OakException { +export declare class ExternalPayUtilException extends OakException { reason: any; constructor(reason: any, message?: string); getSerialData(): { diff --git a/lib/types/Exception.js b/lib/types/Exception.js index 8dd28001..c7e0148d 100644 --- a/lib/types/Exception.js +++ b/lib/types/Exception.js @@ -1,13 +1,13 @@ "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -exports.makeException = exports.RefundExceedMax = exports.ExternalPrePayException = void 0; +exports.makeException = exports.RefundExceedMax = exports.ExternalPayUtilException = void 0; const tslib_1 = require("tslib"); const types_1 = require("oak-domain/lib/types"); const DependentExceptions_1 = tslib_1.__importDefault(require("./DependentExceptions")); -class ExternalPrePayException extends types_1.OakException { +class ExternalPayUtilException extends types_1.OakException { reason; constructor(reason, message) { - super(message || '调用外部支付预下单接口失败'); + super(message || '调用外部支付渠道接口失败'); this.reason = reason; } getSerialData() { @@ -18,7 +18,7 @@ class ExternalPrePayException extends types_1.OakException { }; } } -exports.ExternalPrePayException = ExternalPrePayException; +exports.ExternalPayUtilException = ExternalPayUtilException; class RefundExceedMax extends types_1.OakException { constructor(message) { super(message || '可退款的总额不足'); @@ -34,7 +34,7 @@ function makeException(msg) { const { name, message } = data; switch (name) { case 'ExternalPrePayException': { - exception = new ExternalPrePayException(data.reason, message); + exception = new ExternalPayUtilException(data.reason, message); break; } case 'RefundExceedMax': { diff --git a/lib/types/PayClazz.d.ts b/lib/types/PayClazz.d.ts index b7b82191..f0aa1d4f 100644 --- a/lib/types/PayClazz.d.ts +++ b/lib/types/PayClazz.d.ts @@ -1,6 +1,7 @@ import { EntityDict } from '../oak-app-domain'; import { BRC } from '../types/RuntimeCxt'; type IState = EntityDict['pay']['OpSchema']['iState']; +type RefundIState = EntityDict['refund']['OpSchema']['iState']; export default interface PayClazz { getAccountEntity(): [string, string]; getAccountAmount(context: BRC): Promise; @@ -16,8 +17,13 @@ export default interface PayClazz { extra?: EntityDict['pay']['Update']['data']; price?: number; }>; - refund(refund: EntityDict['refund']['OpSchema']): Promise; - closeRefund(refund: EntityDict['refund']['OpSchema']): Promise; + decodeRefundNotification(params: Record, body: any): Promise<{ + refundId: string; + iState: RefundIState; + extra?: EntityDict['refund']['Update']['data']; + price?: number; + }>; + refund(refund: EntityDict['refund']['Schema'], context: BRC): Promise; getRefundableAt(successAt: number): number; getRefundState(refund: EntityDict['refund']['OpSchema']): Promise<[EntityDict['refund']['OpSchema']['iState'], EntityDict['refund']['Update']['data'] | undefined]>; } diff --git a/lib/utils/application.d.ts b/lib/utils/application.d.ts index 260f0950..e7c4620e 100644 --- a/lib/utils/application.d.ts +++ b/lib/utils/application.d.ts @@ -14,7 +14,7 @@ export declare const mergedProjection: { type?: number | undefined; systemId?: number | undefined; system?: import("../oak-app-domain/System/Schema").Projection | undefined; - config?: number | import("oak-domain/lib/types").JsonProjection | undefined; + config?: number | import("oak-domain/lib/types").JsonProjection | undefined; style?: number | import("oak-domain/lib/types").JsonProjection | undefined; domainId?: number | undefined; domain?: import("../oak-app-domain/Domain/Schema").Projection | undefined; diff --git a/lib/utils/payClazz/Account.d.ts b/lib/utils/payClazz/Account.d.ts index 46d91978..a1f1489e 100644 --- a/lib/utils/payClazz/Account.d.ts +++ b/lib/utils/payClazz/Account.d.ts @@ -9,13 +9,18 @@ export default class Account implements PayClazz { calcPayTax(): [number, string, string]; getRefundableAt(successAt: number): number; refund(refund: EntityDict['refund']['Schema']): Promise; - closeRefund(refund: EntityDict['refund']['Schema']): Promise; getRefundState(refund: EntityDict['refund']['Schema']): Promise<[EntityDict['refund']['Schema']['iState'], undefined]>; decodePayNotification(params: Record, body: any): Promise<{ payId: string; iState: string | null | undefined; extra?: EntityDict['pay']['Update']['data'] | undefined; }>; + decodeRefundNotification(params: Record, body: any): Promise<{ + refundId: string; + iState: string | null | undefined; + extra?: EntityDict['refund']['Update']['data'] | undefined; + price?: number | undefined; + }>; prepay(pay: EntityDict['pay']['Schema'], data: EntityDict['pay']['Update']['data'], context: BRC): Promise; getState(pay: EntityDict['pay']['Schema']): Promise<[string, EntityDict['pay']['Update']['data']]>; close(pay: EntityDict['pay']['Schema']): Promise; diff --git a/lib/utils/payClazz/Account.js b/lib/utils/payClazz/Account.js index 8257252f..f50cabd5 100644 --- a/lib/utils/payClazz/Account.js +++ b/lib/utils/payClazz/Account.js @@ -26,15 +26,15 @@ class Account { async refund(refund) { return; } - async closeRefund(refund) { - return; - } async getRefundState(refund) { return ['refuding', undefined]; } decodePayNotification(params, body) { throw new Error("account类型的pay不需调用此接口"); } + decodeRefundNotification(params, body) { + throw new Error("account类型的pay不需调用此接口"); + } async prepay(pay, data, context) { const { entity, entityId, price } = pay; (0, assert_1.default)(entity === 'account' && entityId); diff --git a/lib/utils/payClazz/Offline.d.ts b/lib/utils/payClazz/Offline.d.ts index 5065983c..bb1d76ba 100644 --- a/lib/utils/payClazz/Offline.d.ts +++ b/lib/utils/payClazz/Offline.d.ts @@ -13,13 +13,18 @@ export default class Offline implements PayClazz { calcPayTax(price: number): [number, string, string]; getRefundableAt(successTime: number): number; refund(refund: RefundOpSchema): Promise; - closeRefund(refund: RefundOpSchema): Promise; getRefundState(refund: RefundOpSchema): Promise<[PayOpSchema['iState'], PayUpdateData | undefined]>; decodePayNotification(params: Record, body: any): Promise<{ payId: string; iState: string | null | undefined; extra?: PayUpdateData | undefined; }>; + decodeRefundNotification(params: Record, body: any): Promise<{ + refundId: string; + iState: string | null | undefined; + extra?: EntityDict['refund']['Update']['data'] | undefined; + price?: number | undefined; + }>; prepay(pay: PayOpSchema, data: PayUpdateData, context: BRC): Promise; getState(pay: PayOpSchema): Promise<[string, PayUpdateData]>; close(pay: PayOpSchema): Promise; diff --git a/lib/utils/payClazz/Offline.js b/lib/utils/payClazz/Offline.js index fd2e4894..6157b3e0 100644 --- a/lib/utils/payClazz/Offline.js +++ b/lib/utils/payClazz/Offline.js @@ -52,10 +52,6 @@ class Offline { // 啥也不做 return; } - async closeRefund(refund) { - // 啥也不做 - return; - } async getRefundState(refund) { const { iState } = refund; (0, assert_1.default)(iState === 'refunding'); @@ -64,6 +60,9 @@ class Offline { decodePayNotification(params, body) { throw new Error("offline类型的pay不需调用此接口"); } + decodeRefundNotification(params, body) { + throw new Error("offline类型的pay不需调用此接口"); + } async prepay(pay, data, context) { data.phantom3 = Math.ceil(Math.random() * 1000000); return; diff --git a/lib/utils/payClazz/WechatPay/WechatPay.d.ts b/lib/utils/payClazz/WechatPay/WechatPay.d.ts index ee6a145c..d968a9dc 100644 --- a/lib/utils/payClazz/WechatPay/WechatPay.d.ts +++ b/lib/utils/payClazz/WechatPay/WechatPay.d.ts @@ -3,7 +3,7 @@ import PayClazz from "../../../types/PayClazz"; import { BRC } from "../../../types/RuntimeCxt"; import { WechatPay as WechatPaySDK } from 'wechat-pay-nodejs'; import { EntityDict } from "../../../oak-app-domain"; -import { OpSchema as OpRefund, UpdateOperationData as RefundUpdateData } from "../../../oak-app-domain/Refund/Schema"; +import { OpSchema as OpRefund, UpdateOperationData as RefundUpdateData, Schema as Refund } from "../../../oak-app-domain/Refund/Schema"; import { Schema as WpProduct } from '../../../oak-app-domain/WpProduct/Schema'; import WechatPayDebug from './WechatPay.debug'; export default class WechatPay extends WechatPayDebug implements PayClazz { @@ -19,10 +19,9 @@ export default class WechatPay extends WechatPayDebug implements PayClazz { static MIN_REFUND_DAYS_GAP: number; static DEFAULT_REFUND_DAYS_GAP: number; constructor(wpProduct: WpProduct, appId: string); - refund(refund: OpRefund): Promise; - closeRefund(refund: OpRefund): Promise; - getRefundState(refund: OpRefund): Promise<[string | null | undefined, PayUpdateData | undefined]>; - private analyzePrepayResult; + refund(refund: Refund, context: BRC): Promise; + getRefundState(refund: OpRefund): Promise<[string | null | undefined, RefundUpdateData | undefined]>; + private analyzeResult; private caclRefundDeadline; prepay(pay: Pay, data: PayUpdateData, context: BRC): Promise; getState(pay: OpPay): Promise<[EntityDict['pay']['OpSchema']['iState'], PayUpdateData]>; @@ -32,5 +31,11 @@ export default class WechatPay extends WechatPayDebug implements PayClazz { iState: EntityDict['pay']['OpSchema']['iState']; extra?: PayUpdateData; }>; + decodeRefundNotification(params: Record, body: any): Promise<{ + refundId: string; + iState: string | null | undefined; + extra?: RefundUpdateData | undefined; + price?: number | undefined; + }>; getRefundableAt(successAt: number): number; } diff --git a/lib/utils/payClazz/WechatPay/WechatPay.debug.d.ts b/lib/utils/payClazz/WechatPay/WechatPay.debug.d.ts index ff86d018..e7270e20 100644 --- a/lib/utils/payClazz/WechatPay/WechatPay.debug.d.ts +++ b/lib/utils/payClazz/WechatPay/WechatPay.debug.d.ts @@ -8,13 +8,18 @@ export declare function registerGetPayStateResult(payState: NonNullable, body: any): Promise<{ + refundId: string; + iState: string | null | undefined; + extra?: RefundUpdateData | undefined; + price?: number | undefined; + }>; getAccountEntity(): [string, string]; getAccountAmount(context: BRC): Promise; calcTransferTax(price: number): [number, string, string]; calcRefundTax(price: number): [number, string, string]; calcPayTax(price: number): [number, string, string]; - refund(refund: Refund): Promise; - closeRefund(refund: Refund): Promise; + refund(refund: Refund, context: BRC): Promise; getRefundState(refund: Refund): Promise<[string | null | undefined, PayUpdateData | undefined]>; decodePayNotification(params: Record, body: any): Promise<{ payId: string; diff --git a/lib/utils/payClazz/WechatPay/WechatPay.debug.js b/lib/utils/payClazz/WechatPay/WechatPay.debug.js index 517fcf35..e18dfc80 100644 --- a/lib/utils/payClazz/WechatPay/WechatPay.debug.js +++ b/lib/utils/payClazz/WechatPay/WechatPay.debug.js @@ -13,6 +13,9 @@ class WechatPay { constructor(wpProduct) { this.wpProduct = wpProduct; } + decodeRefundNotification(params, body) { + throw new Error('Method not implemented.'); + } getAccountEntity() { return ['wpAccount', this.wpProduct.wpAccountId]; } @@ -54,14 +57,11 @@ class WechatPay { (0, assert_1.default)(typeof taxLossRatio === 'number', '微信渠道的手续费率未配置'); return [Math.round(price * taxLossRatio / 100), 'wpAccount', wpProduct.wpAccountId]; } - async refund(refund) { + async refund(refund, context) { return { externalId: Math.random().toString(), }; } - async closeRefund(refund) { - return; - } async getRefundState(refund) { const r = Math.random(); if (r < 0.5) { diff --git a/lib/utils/payClazz/WechatPay/WechatPay.js b/lib/utils/payClazz/WechatPay/WechatPay.js index 09134243..b7efa449 100644 --- a/lib/utils/payClazz/WechatPay/WechatPay.js +++ b/lib/utils/payClazz/WechatPay/WechatPay.js @@ -17,6 +17,12 @@ const TRADE_STATE_MATRIX = { '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; @@ -48,20 +54,45 @@ class WechatPay extends WechatPay_debug_1.default { cert_public_content: fs_1.default.readFileSync(this.publicKeyFilePath), }); } - refund(refund) { - throw new Error("Method not implemented."); + 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, + }; } - closeRefund(refund) { - throw new Error("Method not implemented."); + 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).millisecond() } : undefined]; } - getRefundState(refund) { - throw new Error("Method not implemented."); - } - analyzePrepayResult(result) { + analyzeResult(result) { const { success, data, } = result; if (!success) { console.error(JSON.stringify(result)); - throw new Exception_1.ExternalPrePayException(result); + throw new Exception_1.ExternalPayUtilException(result); } return data; } @@ -91,7 +122,7 @@ class WechatPay extends WechatPay_debug_1.default { total: pay.price, }, }); - const { code_url } = this.analyzePrepayResult(result); + const { code_url } = this.analyzeResult(result); data.externalId = code_url; data.meta = { codeUrl: code_url, @@ -122,7 +153,7 @@ class WechatPay extends WechatPay_debug_1.default { openid: wechatUser.openId, } }); - const prepayMeta = this.analyzePrepayResult(result); + const prepayMeta = this.analyzeResult(result); const prepayId = prepayMeta.package.slice(11); // `prepay_id=${prepay_id}` data.externalId = prepayId; data.meta = { @@ -155,7 +186,7 @@ class WechatPay extends WechatPay_debug_1.default { * USERPAYING:用户支付中(仅付款码支付会返回) * PAYERROR:支付失败(仅付款码支付会返回) */ - const { trade_state: tradeState, success_time } = this.analyzePrepayResult(result); + const { trade_state: tradeState, success_time } = this.analyzeResult(result); const iState = TRADE_STATE_MATRIX[tradeState]; (0, assert_1.default)(iState); const updateData = { @@ -171,7 +202,7 @@ class WechatPay extends WechatPay_debug_1.default { async close(pay) { const outTradeNo = (0, uuid_1.compressTo32)(pay.id); const result = await this.wechatPay.closeOrder(outTradeNo); - this.analyzePrepayResult(result); + this.analyzeResult(result); } async decodePayNotification(params, body) { const { resource } = body; @@ -288,6 +319,59 @@ class WechatPay extends WechatPay_debug_1.default { 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 = success_time; + } + return { + refundId, + iState, + extra, + }; + } getRefundableAt(successAt) { return this.caclRefundDeadline(successAt); } diff --git a/src/components/AbstractComponents.ts b/src/components/AbstractComponents.ts index 5070e39a..4d1d18a8 100644 --- a/src/components/AbstractComponents.ts +++ b/src/components/AbstractComponents.ts @@ -2,8 +2,9 @@ * 抽象组件在业务层根据EntityDict的重新声明 * by Xc 20230807 */ -// @ts-nocheck +// @ts-nocheck +import React from 'react'; import { EntityDict } from '../oak-app-domain'; import { TableProps, PaginationProps } from 'antd'; diff --git a/src/entities/Refund.ts b/src/entities/Refund.ts index e88b2077..fd11d498 100644 --- a/src/entities/Refund.ts +++ b/src/entities/Refund.ts @@ -22,6 +22,7 @@ export interface Schema extends EntityShape { price: Price; creator: User; reason?: Text; + successAt?: Datetime; opers: AccountOper[]; }; @@ -53,7 +54,8 @@ export const entityDesc: EntityDesc[] = [ } - const data = await payClazz.refund(refund as EntityDict['refund']['Schema']); + const data = await payClazz.refund(refund as EntityDict['refund']['Schema'], context); if (data) { assert(data.externalId); const closeFn = context.openRootMode(); diff --git a/src/types/Exception.ts b/src/types/Exception.ts index 53f73f35..58cab1b5 100644 --- a/src/types/Exception.ts +++ b/src/types/Exception.ts @@ -2,10 +2,10 @@ import { OakException, OakUserException, OpRecord } from 'oak-domain/lib/types'; import { EntityDict } from '@oak-app-domain'; import makeDepedentException from './DependentExceptions'; -export class ExternalPrePayException extends OakException { +export class ExternalPayUtilException extends OakException { reason: any; constructor(reason: any, message?: string) { - super(message || '调用外部支付预下单接口失败'); + super(message || '调用外部支付渠道接口失败'); this.reason = reason; } @@ -35,7 +35,7 @@ export function makeException(msg: string | object) { const { name, message } = data; switch (name) { case 'ExternalPrePayException': { - exception = new ExternalPrePayException(data.reason, message); + exception = new ExternalPayUtilException(data.reason, message); break; } case 'RefundExceedMax': { diff --git a/src/types/PayClazz.ts b/src/types/PayClazz.ts index c118ef0a..837c34f6 100644 --- a/src/types/PayClazz.ts +++ b/src/types/PayClazz.ts @@ -1,6 +1,7 @@ import { EntityDict } from '../oak-app-domain'; import { BRC } from '../types/RuntimeCxt'; type IState = EntityDict['pay']['OpSchema']['iState']; +type RefundIState = EntityDict['refund']['OpSchema']['iState']; export default interface PayClazz { getAccountEntity(): [string, string]; @@ -26,10 +27,15 @@ export default interface PayClazz { price?: number; }>; - // refund相关的待定 - refund(refund: EntityDict['refund']['OpSchema']): Promise; + decodeRefundNotification(params: Record, body: any): Promise<{ + refundId: string, + iState: RefundIState, + extra?: EntityDict['refund']['Update']['data'], + price?: number; + }> - closeRefund(refund: EntityDict['refund']['OpSchema']): Promise; + // refund相关的待定 + refund(refund: EntityDict['refund']['Schema'], context: BRC): Promise; getRefundableAt(successAt: number): number; diff --git a/src/utils/payClazz/Account.ts b/src/utils/payClazz/Account.ts index e9942909..7a008de5 100644 --- a/src/utils/payClazz/Account.ts +++ b/src/utils/payClazz/Account.ts @@ -27,9 +27,6 @@ export default class Account implements PayClazz { async refund(refund: EntityDict['refund']['Schema']): Promise { return; } - async closeRefund(refund: EntityDict['refund']['Schema']): Promise { - return; - } async getRefundState(refund: EntityDict['refund']['Schema']): Promise<[EntityDict['refund']['Schema']['iState'], undefined]> { return ['refuding', undefined]; } @@ -41,6 +38,10 @@ export default class Account implements PayClazz { throw new Error("account类型的pay不需调用此接口"); } + decodeRefundNotification(params: Record, body: any): Promise<{ refundId: string; iState: string | null | undefined; extra?: EntityDict['refund']['Update']['data'] | undefined; price?: number | undefined; }> { + throw new Error("account类型的pay不需调用此接口"); + } + async prepay(pay: EntityDict['pay']['Schema'], data: EntityDict['pay']['Update']['data'], context: BRC) { const { entity, entityId, price } = pay; assert(entity === 'account' && entityId); diff --git a/src/utils/payClazz/Offline.ts b/src/utils/payClazz/Offline.ts index 0daee141..9c11d7c9 100644 --- a/src/utils/payClazz/Offline.ts +++ b/src/utils/payClazz/Offline.ts @@ -60,10 +60,6 @@ export default class Offline implements PayClazz { // 啥也不做 return; } - async closeRefund(refund: RefundOpSchema): Promise { - // 啥也不做 - return; - } async getRefundState(refund: RefundOpSchema): Promise<[PayOpSchema['iState'], PayUpdateData | undefined]> { const { iState } = refund; assert(iState === 'refunding'); @@ -72,6 +68,9 @@ export default class Offline implements PayClazz { decodePayNotification(params: Record, body: any): Promise<{ payId: string; iState: string | null | undefined; extra?: PayUpdateData | undefined; }> { throw new Error("offline类型的pay不需调用此接口"); } + decodeRefundNotification(params: Record, body: any): Promise<{ refundId: string; iState: string | null | undefined; extra?: EntityDict['refund']['Update']['data'] | undefined; price?: number | undefined; }> { + throw new Error("offline类型的pay不需调用此接口"); + } async prepay(pay: PayOpSchema, data: PayUpdateData, context: BRC): Promise { data.phantom3 = Math.ceil(Math.random() * 1000000); return; diff --git a/src/utils/payClazz/WechatPay/WechatPay.debug.ts b/src/utils/payClazz/WechatPay/WechatPay.debug.ts index 3344ddd8..2d41c7a5 100644 --- a/src/utils/payClazz/WechatPay/WechatPay.debug.ts +++ b/src/utils/payClazz/WechatPay/WechatPay.debug.ts @@ -17,6 +17,9 @@ export default class WechatPay implements PayClazz { constructor(wpProduct: WpProduct) { this.wpProduct = wpProduct; } + decodeRefundNotification(params: Record, body: any): Promise<{ refundId: string; iState: string | null | undefined; extra?: RefundUpdateData | undefined; price?: number | undefined; }> { + throw new Error('Method not implemented.'); + } getAccountEntity(): [string, string] { return ['wpAccount', this.wpProduct.wpAccountId]; } @@ -61,14 +64,11 @@ export default class WechatPay implements PayClazz { assert(typeof taxLossRatio === 'number', '微信渠道的手续费率未配置'); return [Math.round(price * taxLossRatio / 100), 'wpAccount', wpProduct.wpAccountId!]; } - async refund(refund: Refund): Promise { + async refund(refund: Refund, context: BRC): Promise { return { externalId: Math.random().toString(), }; } - async closeRefund(refund: Refund): Promise { - return; - } async getRefundState(refund: Refund): Promise<[string | null | undefined, PayUpdateData | undefined]> { const r = Math.random(); if (r < 0.5) { diff --git a/src/utils/payClazz/WechatPay/WechatPay.ts b/src/utils/payClazz/WechatPay/WechatPay.ts index 73ac3361..4206b250 100644 --- a/src/utils/payClazz/WechatPay/WechatPay.ts +++ b/src/utils/payClazz/WechatPay/WechatPay.ts @@ -1,12 +1,12 @@ import { OpSchema as OpPay, Schema as Pay, UpdateOperationData as PayUpdateData } from "../../../oak-app-domain/Pay/Schema"; import PayClazz from "../../../types/PayClazz"; -import fs from 'fs'; +import fs, { stat } from 'fs'; import dayJs from 'dayjs'; import { BRC } from "../../../types/RuntimeCxt"; import { WechatPay as WechatPaySDK } from 'wechat-pay-nodejs'; import { compressTo32, decompressFrom32 } from 'oak-domain/lib/utils/uuid'; -import { ExternalPrePayException } from "../../../types/Exception"; +import { ExternalPayUtilException } from "../../../types/Exception"; import { ApiResult, QueryOrderResult } from "wechat-pay-nodejs/typings"; import assert from "assert"; import { EntityDict } from "../../../oak-app-domain"; @@ -24,6 +24,13 @@ const TRADE_STATE_MATRIX: Record = { + 'SUCCESS': "successful", + 'CLOSED': "failed", + "ABNORMAL": "refunding", + "PROCESSING": 'refunding', +}; + export default class WechatPay extends WechatPayDebug implements PayClazz { wechatPay: WechatPaySDK; @@ -61,24 +68,51 @@ export default class WechatPay extends WechatPayDebug implements PayClazz { }); } - refund(refund: OpRefund): Promise { - throw new Error("Method not implemented."); + async refund(refund: Refund, context: BRC): Promise { + 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: compressTo32(pay.id), + out_refund_no: compressTo32(id), + notify_url: refundNotifyUrl, + amount: { + refund: price!, + total: pay.price!, + currency: 'CNY', + } + }); + const { refund_id } = this.analyzeResult(result); + return { + externalId: refund_id, + }; } - closeRefund(refund: OpRefund): Promise { - throw new Error("Method not implemented."); - } - getRefundState(refund: OpRefund): Promise<[string | null | undefined, PayUpdateData | undefined]> { - throw new Error("Method not implemented."); + async getRefundState(refund: OpRefund): Promise<[string | null | undefined, RefundUpdateData | undefined]> { + const result = await this.wechatPay.queryRefundByOutRefundNo(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: dayJs(success_time).millisecond() } : undefined]; } - private analyzePrepayResult(result: ApiResult) { + private analyzeResult(result: ApiResult) { const { success, data, } = result; if (!success) { console.error(JSON.stringify(result)); - throw new ExternalPrePayException(result); + throw new ExternalPayUtilException(result); } return data!; } @@ -110,7 +144,7 @@ export default class WechatPay extends WechatPayDebug implements PayClazz { total: pay.price, }, }); - const { code_url } = this.analyzePrepayResult(result); + const { code_url } = this.analyzeResult(result); data.externalId = code_url; data.meta = { codeUrl: code_url, @@ -141,7 +175,7 @@ export default class WechatPay extends WechatPayDebug implements PayClazz { openid: wechatUser.openId!, } }); - const prepayMeta = this.analyzePrepayResult(result); + const prepayMeta = this.analyzeResult(result); const prepayId = prepayMeta.package.slice(11); // `prepay_id=${prepay_id}` data.externalId = prepayId; data.meta = { @@ -176,7 +210,7 @@ export default class WechatPay extends WechatPayDebug implements PayClazz { * USERPAYING:用户支付中(仅付款码支付会返回) * PAYERROR:支付失败(仅付款码支付会返回) */ - const { trade_state: tradeState, success_time } = this.analyzePrepayResult(result) as QueryOrderResult & { + const { trade_state: tradeState, success_time } = this.analyzeResult(result) as QueryOrderResult & { trade_state: string; }; const iState: EntityDict['pay']['OpSchema']['iState'] = TRADE_STATE_MATRIX[tradeState]; @@ -195,7 +229,7 @@ export default class WechatPay extends WechatPayDebug implements PayClazz { async close(pay: OpPay): Promise { const outTradeNo = compressTo32(pay.id); const result = await this.wechatPay.closeOrder(outTradeNo); - this.analyzePrepayResult(result); + this.analyzeResult(result); } async decodePayNotification(params: Record, body: any): Promise<{ @@ -338,6 +372,82 @@ export default class WechatPay extends WechatPayDebug implements PayClazz { extra, }; } + + async decodeRefundNotification(params: Record, body: any): Promise<{ refundId: string; iState: string | null | undefined; extra?: RefundUpdateData | undefined; price?: number | undefined; }> { + const { + resource + } = body as { + resource: { + algorithm: string; + ciphertext: string; + associated_data: string; + original_type: string; + nonce: string; + }; + }; + 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 = decompressFrom32(out_refund_no); + assert(mchid === this.mchId); + const iState = REFUND_STATE_MATRIX[refund_status]; + assert(iState); + + const extra: RefundUpdateData = { + meta: omit(result, ['mchid', ]), + }; + + if (iState === 'successful') { + extra.successAt = success_time; + } + + return { + refundId, + iState, + extra, + }; + } getRefundableAt(successAt: number): number { return this.caclRefundDeadline(successAt);