修改了Account类型的支付逻辑,支持多次支付

This commit is contained in:
Xu Chang 2024-05-31 15:27:21 +08:00
parent 507c43d87f
commit 962298ea98
24 changed files with 463 additions and 108 deletions

View File

@ -1,4 +1,7 @@
import { OakInputIllegalException } from 'oak-domain/lib/types';
import { CHECKER_MAX_PRIORITY } from 'oak-domain/lib/types/Trigger';
import { pipeline } from 'oak-domain/lib/utils/executor';
import assert from 'assert';
import { PAY_CHANNEL_ACCOUNT_NAME, PAY_CHANNEL_OFFLINE_NAME } from '../types/PayConfig';
const checkers = [
{
@ -103,6 +106,33 @@ const checkers = [
channel: PAY_CHANNEL_OFFLINE_NAME,
};
}
},
{
// 如果在开始支付或者继续支付过程中paid达到了pricepay的状态可以改为paid
entity: 'pay',
type: 'logical',
action: ['continuePaying', 'startPaying'],
priority: CHECKER_MAX_PRIORITY - 1, // 要超过action矩阵定义的赋state值
checker: (operation, context) => {
const { data, filter } = operation;
assert(filter && typeof filter.id === 'string');
const { paid } = data;
if (paid) {
return pipeline(() => context.select('pay', {
data: {
id: 1,
paid: 1,
price: 1,
},
}, {}), (pays) => {
const [pay] = pays;
const { paid: payPaid, price } = pay;
if (payPaid + paid === price) {
data.iState === 'paid';
}
});
}
}
}
];
export default checkers;

View File

@ -26,7 +26,7 @@ const attrUpdateMatrix = {
}
},
paid: {
actions: ['succeedPaying'],
actions: ['succeedPaying', 'continuePaying'],
},
refunded: {
actions: ['refundPartially', 'refundAll'],

View File

@ -33,7 +33,7 @@ export interface Schema extends EntityShape {
type IAction = 'startPaying' | 'succeedPaying' | 'close' | 'startRefunding' | 'refundAll' | 'refundPartially' | 'stopRefunding';
type IState = 'unpaid' | 'paying' | 'paid' | 'closed' | 'refunding' | 'partiallyRefunded' | 'refunded';
export declare const IActionDef: ActionDef<IAction, IState>;
type Action = IAction | 'closeRefund';
type Action = IAction | 'closeRefund' | 'continuePaying';
export declare const entityDesc: EntityDesc<Schema, Action, '', {
iState: IState;
}>;

View File

@ -74,6 +74,7 @@ export const entityDesc = {
},
action: {
startPaying: '开始支付',
continuePaying: '继续支付',
succeedPaying: '支付成功',
close: '关闭',
startRefunding: '开始退款',
@ -105,6 +106,7 @@ export const entityDesc = {
refundPartially: '',
closeRefund: '',
stopRefunding: '',
continuePaying: '',
},
color: {
iState: {

View File

@ -1,3 +1,4 @@
import { generateNewIdAsync } from 'oak-domain/lib/utils/uuid';
import { DATA_SUBSCRIBER_KEYS } from '../config/constants';
import assert from 'assert';
const triggers = [
@ -16,5 +17,76 @@ const triggers = [
return 1;
},
},
{
name: '当account帐户的avail增加时如果有等待中的pay则继续完成支付',
entity: 'account',
action: ['deposit', 'withdrawBack', 'repay'],
when: 'after',
fn: async ({ operation }, context, option) => {
const { id, filter } = operation;
assert(typeof filter?.id === 'string');
const [account] = await context.select('account', {
data: {
id: 1,
avail: 1,
pay$account: {
$entity: 'pay',
data: {
id: 1,
paid: 1,
price: 1,
},
filter: {
iState: 'paying',
orderId: {
$exists: true,
}
}
}
},
filter: {
id: filter.id,
}
}, {});
const { avail, pay$account: pays } = account;
let rest = avail;
let count = 0;
if (pays && pays.length > 0) {
for (const pay of pays) {
if (rest === 0) {
break;
}
const { price, paid } = pay;
const paid2 = Math.min(price - paid, rest);
await context.operate('pay', {
id: await generateNewIdAsync(),
action: 'continuePaying',
data: {
paid: paid + paid2,
accountOper$entity: [
{
id: await generateNewIdAsync(),
action: 'create',
data: {
id: await generateNewIdAsync(),
type: 'consume',
availPlus: -paid2,
totalPlus: -paid2,
accountId: filter.id,
}
}
]
},
filter: {
id: pay.id,
}
}, {});
rest = rest - paid2;
count++;
}
}
return 1;
},
},
];
export default triggers;

View File

@ -42,7 +42,7 @@ const triggers = [
},
},
{
name: '当account depoist时根据系统配置扣除掉损耗',
name: '当account depoist时根据系统配置扣除掉损耗如果有没支付完整的account类型的pay则继续支付',
entity: 'accountOper',
action: 'create',
when: 'after',
@ -75,27 +75,27 @@ const triggers = [
},
}, {});
const { price, application, channel } = pay;
if (PAY_ORG_CHANNELS.includes(channel)) {
const ratio = getDepositRatio(channel, application);
if (ratio > 0) {
const loss = Math.ceil(price * ratio / 100);
await context.operate('accountOper', {
assert(PAY_ORG_CHANNELS.includes(channel));
const ratio = getDepositRatio(channel, application);
let count = 0;
if (ratio > 0) {
const loss = Math.ceil(price * ratio / 100);
await context.operate('accountOper', {
id: await generateNewIdAsync(),
action: 'create',
data: {
id: await generateNewIdAsync(),
action: 'create',
data: {
id: await generateNewIdAsync(),
type: 'loss',
totalPlus: -loss,
availPlus: -loss,
entity,
entityId,
accountId: data.accountId,
}
}, {});
return 1;
}
type: 'loss',
totalPlus: -loss,
availPlus: -loss,
entity,
entityId,
accountId: data.accountId,
}
}, {});
count++;
}
return 0;
return count;
}
},
];

View File

@ -1,6 +1,7 @@
import { OpSchema as Refund } from "../../oak-app-domain/Refund/Schema";
import { Schema, OpSchema as Pay, UpdateOperationData as PayUpdateData } from "../../oak-app-domain/Pay/Schema";
import PayClazz from "../../types/PayClazz";
import { BRC } from "../../types/RuntimeCxt";
export default class Account implements PayClazz {
refund(refund: Refund): Promise<undefined>;
closeRefund(refund: Refund): Promise<void>;
@ -11,7 +12,7 @@ export default class Account implements PayClazz {
extra?: PayUpdateData | undefined;
}>;
channel: string;
prepay(pay: Schema, data: PayUpdateData): Promise<void>;
prepay(pay: Schema, data: PayUpdateData, context: BRC): Promise<void>;
getState(pay: Pay): Promise<[string, PayUpdateData]>;
close(pay: Pay): Promise<void>;
}

View File

@ -15,28 +15,40 @@ export default class Account {
throw new Error("account类型的pay不需调用此接口");
}
channel = PAY_CHANNEL_ACCOUNT_NAME;
async prepay(pay, data) {
async prepay(pay, data, context) {
const { accountId, price } = pay;
assert(accountId);
/**
* account类型的支付就是直接从account中扣除款项
* 但是注意最多只能把avail扣空
* 如果一个pay没有完全支付等account中充值了会自动继续进行支付
*/
data.iState = 'paid',
data.accountOper$entity = [
{
const [account] = await context.select('account', {
data: {
id: 1,
avail: 1,
},
filter: {
id: accountId,
}
}, { forUpdate: true });
const { avail } = account;
const paid = Math.min(price, avail);
data.accountOper$entity = [
{
id: await generateNewIdAsync(),
action: 'create',
data: {
id: await generateNewIdAsync(),
action: 'create',
data: {
id: await generateNewIdAsync(),
totalPlus: -price,
availPlus: -price,
type: 'consume',
accountId,
},
}
];
totalPlus: -paid,
availPlus: -paid,
type: 'consume',
accountId,
},
}
];
data.meta = {};
data.paid = pay.price;
data.paid = paid;
}
getState(pay) {
throw new Error("account类型的pay不应该需要查询此状态");

View File

@ -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;

View File

@ -1,6 +1,10 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const tslib_1 = require("tslib");
const types_1 = require("oak-domain/lib/types");
const Trigger_1 = require("oak-domain/lib/types/Trigger");
const executor_1 = require("oak-domain/lib/utils/executor");
const assert_1 = tslib_1.__importDefault(require("assert"));
const PayConfig_1 = require("../types/PayConfig");
const checkers = [
{
@ -105,6 +109,33 @@ const checkers = [
channel: PayConfig_1.PAY_CHANNEL_OFFLINE_NAME,
};
}
},
{
// 如果在开始支付或者继续支付过程中paid达到了pricepay的状态可以改为paid
entity: 'pay',
type: 'logical',
action: ['continuePaying', 'startPaying'],
priority: Trigger_1.CHECKER_MAX_PRIORITY - 1, // 要超过action矩阵定义的赋state值
checker: (operation, context) => {
const { data, filter } = operation;
(0, assert_1.default)(filter && typeof filter.id === 'string');
const { paid } = data;
if (paid) {
return (0, executor_1.pipeline)(() => context.select('pay', {
data: {
id: 1,
paid: 1,
price: 1,
},
}, {}), (pays) => {
const [pay] = pays;
const { paid: payPaid, price } = pay;
if (payPaid + paid === price) {
data.iState === 'paid';
}
});
}
}
}
];
exports.default = checkers;

View File

@ -28,7 +28,7 @@ const attrUpdateMatrix = {
}
},
paid: {
actions: ['succeedPaying'],
actions: ['succeedPaying', 'continuePaying'],
},
refunded: {
actions: ['refundPartially', 'refundAll'],

View File

@ -33,7 +33,7 @@ export interface Schema extends EntityShape {
type IAction = 'startPaying' | 'succeedPaying' | 'close' | 'startRefunding' | 'refundAll' | 'refundPartially' | 'stopRefunding';
type IState = 'unpaid' | 'paying' | 'paid' | 'closed' | 'refunding' | 'partiallyRefunded' | 'refunded';
export declare const IActionDef: ActionDef<IAction, IState>;
type Action = IAction | 'closeRefund';
type Action = IAction | 'closeRefund' | 'continuePaying';
export declare const entityDesc: EntityDesc<Schema, Action, '', {
iState: IState;
}>;

View File

@ -77,6 +77,7 @@ exports.entityDesc = {
},
action: {
startPaying: '开始支付',
continuePaying: '继续支付',
succeedPaying: '支付成功',
close: '关闭',
startRefunding: '开始退款',
@ -108,6 +109,7 @@ exports.entityDesc = {
refundPartially: '',
closeRefund: '',
stopRefunding: '',
continuePaying: '',
},
color: {
iState: {

View File

@ -1,6 +1,7 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const tslib_1 = require("tslib");
const uuid_1 = require("oak-domain/lib/utils/uuid");
const constants_1 = require("../config/constants");
const assert_1 = tslib_1.__importDefault(require("assert"));
const triggers = [
@ -19,5 +20,76 @@ const triggers = [
return 1;
},
},
{
name: '当account帐户的avail增加时如果有等待中的pay则继续完成支付',
entity: 'account',
action: ['deposit', 'withdrawBack', 'repay'],
when: 'after',
fn: async ({ operation }, context, option) => {
const { id, filter } = operation;
(0, assert_1.default)(typeof filter?.id === 'string');
const [account] = await context.select('account', {
data: {
id: 1,
avail: 1,
pay$account: {
$entity: 'pay',
data: {
id: 1,
paid: 1,
price: 1,
},
filter: {
iState: 'paying',
orderId: {
$exists: true,
}
}
}
},
filter: {
id: filter.id,
}
}, {});
const { avail, pay$account: pays } = account;
let rest = avail;
let count = 0;
if (pays && pays.length > 0) {
for (const pay of pays) {
if (rest === 0) {
break;
}
const { price, paid } = pay;
const paid2 = Math.min(price - paid, rest);
await context.operate('pay', {
id: await (0, uuid_1.generateNewIdAsync)(),
action: 'continuePaying',
data: {
paid: paid + paid2,
accountOper$entity: [
{
id: await (0, uuid_1.generateNewIdAsync)(),
action: 'create',
data: {
id: await (0, uuid_1.generateNewIdAsync)(),
type: 'consume',
availPlus: -paid2,
totalPlus: -paid2,
accountId: filter.id,
}
}
]
},
filter: {
id: pay.id,
}
}, {});
rest = rest - paid2;
count++;
}
}
return 1;
},
},
];
exports.default = triggers;

View File

@ -45,7 +45,7 @@ const triggers = [
},
},
{
name: '当account depoist时根据系统配置扣除掉损耗',
name: '当account depoist时根据系统配置扣除掉损耗如果有没支付完整的account类型的pay则继续支付',
entity: 'accountOper',
action: 'create',
when: 'after',
@ -78,27 +78,27 @@ const triggers = [
},
}, {});
const { price, application, channel } = pay;
if (PayConfig_1.PAY_ORG_CHANNELS.includes(channel)) {
const ratio = (0, pay_1.getDepositRatio)(channel, application);
if (ratio > 0) {
const loss = Math.ceil(price * ratio / 100);
await context.operate('accountOper', {
(0, assert_1.default)(PayConfig_1.PAY_ORG_CHANNELS.includes(channel));
const ratio = (0, pay_1.getDepositRatio)(channel, application);
let count = 0;
if (ratio > 0) {
const loss = Math.ceil(price * ratio / 100);
await context.operate('accountOper', {
id: await (0, uuid_1.generateNewIdAsync)(),
action: 'create',
data: {
id: await (0, uuid_1.generateNewIdAsync)(),
action: 'create',
data: {
id: await (0, uuid_1.generateNewIdAsync)(),
type: 'loss',
totalPlus: -loss,
availPlus: -loss,
entity,
entityId,
accountId: data.accountId,
}
}, {});
return 1;
}
type: 'loss',
totalPlus: -loss,
availPlus: -loss,
entity,
entityId,
accountId: data.accountId,
}
}, {});
count++;
}
return 0;
return count;
}
},
];

View File

@ -1,6 +1,7 @@
import { OpSchema as Refund } from "../../oak-app-domain/Refund/Schema";
import { Schema, OpSchema as Pay, UpdateOperationData as PayUpdateData } from "../../oak-app-domain/Pay/Schema";
import PayClazz from "../../types/PayClazz";
import { BRC } from "../../types/RuntimeCxt";
export default class Account implements PayClazz {
refund(refund: Refund): Promise<undefined>;
closeRefund(refund: Refund): Promise<void>;
@ -11,7 +12,7 @@ export default class Account implements PayClazz {
extra?: PayUpdateData | undefined;
}>;
channel: string;
prepay(pay: Schema, data: PayUpdateData): Promise<void>;
prepay(pay: Schema, data: PayUpdateData, context: BRC): Promise<void>;
getState(pay: Pay): Promise<[string, PayUpdateData]>;
close(pay: Pay): Promise<void>;
}

View File

@ -18,28 +18,40 @@ class Account {
throw new Error("account类型的pay不需调用此接口");
}
channel = PayConfig_1.PAY_CHANNEL_ACCOUNT_NAME;
async prepay(pay, data) {
async prepay(pay, data, context) {
const { accountId, price } = pay;
(0, assert_1.default)(accountId);
/**
* account类型的支付就是直接从account中扣除款项
* 但是注意最多只能把avail扣空
* 如果一个pay没有完全支付等account中充值了会自动继续进行支付
*/
data.iState = 'paid',
data.accountOper$entity = [
{
const [account] = await context.select('account', {
data: {
id: 1,
avail: 1,
},
filter: {
id: accountId,
}
}, { forUpdate: true });
const { avail } = account;
const paid = Math.min(price, avail);
data.accountOper$entity = [
{
id: await (0, uuid_1.generateNewIdAsync)(),
action: 'create',
data: {
id: await (0, uuid_1.generateNewIdAsync)(),
action: 'create',
data: {
id: await (0, uuid_1.generateNewIdAsync)(),
totalPlus: -price,
availPlus: -price,
type: 'consume',
accountId,
},
}
];
totalPlus: -paid,
availPlus: -paid,
type: 'consume',
accountId,
},
}
];
data.meta = {};
data.paid = pay.price;
data.paid = paid;
}
getState(pay) {
throw new Error("account类型的pay不应该需要查询此状态");

View File

@ -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;

View File

@ -3,6 +3,8 @@ import { EntityDict } from '@project/oak-app-domain';
import { RuntimeCxt } from '../types/RuntimeCxt';
import { OakInputIllegalException } from 'oak-domain/lib/types';
import { generateNewId, generateNewIdAsync } from 'oak-domain/lib/utils/uuid';
import { CHECKER_MAX_PRIORITY } from 'oak-domain/lib/types/Trigger';
import { pipeline } from 'oak-domain/lib/utils/executor';
import assert from 'assert';
import { PAY_CHANNEL_ACCOUNT_NAME, PAY_CHANNEL_OFFLINE_NAME } from '@project/types/PayConfig';
@ -115,6 +117,36 @@ const checkers: Checker<EntityDict, 'pay', RuntimeCxt>[] = [
channel: PAY_CHANNEL_OFFLINE_NAME,
}
}
},
{
// 如果在开始支付或者继续支付过程中paid达到了pricepay的状态可以改为paid
entity: 'pay',
type: 'logical',
action: ['continuePaying', 'startPaying'],
priority: CHECKER_MAX_PRIORITY - 1, // 要超过action矩阵定义的赋state值
checker: (operation, context) => {
const { data, filter } = operation as EntityDict['pay']['Update'];
assert(filter && typeof filter.id === 'string');
const { paid } = data;
if (paid) {
return pipeline(
() => context.select('pay', {
data: {
id: 1,
paid: 1,
price: 1,
},
}, {}),
(pays: EntityDict['pay']['OpSchema'][]) => {
const [ pay ] = pays;
const { paid: payPaid, price } = pay;
if (payPaid + paid === price) {
data.iState === 'paid';
}
}
)
}
}
}
];

View File

@ -30,7 +30,7 @@ const attrUpdateMatrix: AttrUpdateMatrix<EntityDict> = {
}
},
paid: {
actions: ['succeedPaying'],
actions: ['succeedPaying', 'continuePaying'],
},
refunded: {
actions: ['refundPartially', 'refundAll'],

View File

@ -55,7 +55,7 @@ export const IActionDef: ActionDef<IAction, IState> = {
},
is: 'unpaid',
};
type Action = IAction | 'closeRefund';
type Action = IAction | 'closeRefund' | 'continuePaying';
export const entityDesc: EntityDesc<Schema, Action, '', {
iState: IState,
@ -122,6 +122,7 @@ export const entityDesc: EntityDesc<Schema, Action, '', {
},
action: {
startPaying: '开始支付',
continuePaying: '继续支付',
succeedPaying: '支付成功',
close: '关闭',
startRefunding: '开始退款',
@ -153,6 +154,7 @@ export const entityDesc: EntityDesc<Schema, Action, '', {
refundPartially: '',
closeRefund: '',
stopRefunding: '',
continuePaying: '',
},
color: {
iState: {

View File

@ -21,6 +21,80 @@ const triggers: Trigger<EntityDict, 'account', BRC>[] = [
return 1;
},
} as UpdateTriggerInTxn<EntityDict, 'account', BRC>,
{
name: '当account帐户的avail增加时如果有等待中的pay则继续完成支付',
entity: 'account',
action: ['deposit', 'withdrawBack', 'repay'],
when: 'after',
fn: async ({ operation }, context, option) => {
const { id, filter } = operation;
assert(typeof filter?.id === 'string');
const [account] = await context.select('account', {
data: {
id: 1,
avail: 1,
pay$account: {
$entity: 'pay',
data: {
id: 1,
paid: 1,
price: 1,
},
filter: {
iState: 'paying',
orderId: {
$exists: true,
}
}
}
},
filter: {
id: filter!.id,
}
}, {});
const { avail, pay$account: pays } = account;
let rest = avail!;
let count = 0;
if (pays && pays.length > 0) {
for (const pay of pays) {
if (rest === 0) {
break;
}
const { price, paid } = pay;
const paid2 = Math.min(price! - paid!, rest);
await context.operate('pay', {
id: await generateNewIdAsync(),
action: 'continuePaying',
data: {
paid: paid! + paid2,
accountOper$entity: [
{
id: await generateNewIdAsync(),
action: 'create',
data: {
id: await generateNewIdAsync(),
type: 'consume',
availPlus: -paid2,
totalPlus: -paid2,
accountId: filter!.id,
}
}
]
},
filter: {
id: pay!.id!,
}
}, {});
rest = rest - paid2;
count ++;
}
}
return 1;
},
} as UpdateTriggerInTxn<EntityDict, 'account', BRC>,
];
export default triggers;

View File

@ -49,7 +49,7 @@ const triggers: Trigger<EntityDict, 'accountOper', BRC>[] = [
},
} as CreateTriggerInTxn<EntityDict, 'accountOper', BRC>,
{
name: '当account depoist时根据系统配置扣除掉损耗',
name: '当account depoist时根据系统配置扣除掉损耗如果有没支付完整的account类型的pay则继续支付',
entity: 'accountOper',
action: 'create',
when: 'after',
@ -83,30 +83,28 @@ const triggers: Trigger<EntityDict, 'accountOper', BRC>[] = [
}, {});
const { price, application, channel } = pay;
if (PAY_ORG_CHANNELS.includes(channel!)) {
const ratio = getDepositRatio(channel!, application!);
assert(PAY_ORG_CHANNELS.includes(channel!));
const ratio = getDepositRatio(channel!, application!);
if (ratio > 0) {
const loss = Math.ceil(price! * ratio / 100);
await context.operate('accountOper', {
let count = 0;
if (ratio > 0) {
const loss = Math.ceil(price! * ratio / 100);
await context.operate('accountOper', {
id: await generateNewIdAsync(),
action: 'create',
data: {
id: await generateNewIdAsync(),
action: 'create',
data: {
id: await generateNewIdAsync(),
type: 'loss',
totalPlus: -loss,
availPlus: -loss,
entity,
entityId,
accountId: data.accountId!,
}
}, {});
return 1;
}
}
return 0;
type: 'loss',
totalPlus: -loss,
availPlus: -loss,
entity,
entityId,
accountId: data.accountId!,
}
}, {});
count ++;
}
return count;
}
} as CreateTriggerInTxn<EntityDict, 'accountOper', BRC>,
];

View File

@ -4,6 +4,7 @@ import PayClazz from "../../types/PayClazz";
import { PAY_CHANNEL_ACCOUNT_NAME } from '../../types/PayConfig';
import assert from 'assert';
import { generateNewIdAsync } from "oak-domain/lib/utils/uuid";
import { BRC } from "@project/types/RuntimeCxt";
export default class Account implements PayClazz {
async refund(refund: Refund): Promise<undefined> {
@ -20,28 +21,41 @@ export default class Account implements PayClazz {
}
channel = PAY_CHANNEL_ACCOUNT_NAME;
async prepay(pay: Schema, data: PayUpdateData) {
async prepay(pay: Schema, data: PayUpdateData, context: BRC) {
const { accountId, price } = pay;
assert(accountId);
/**
* account类型的支付就是直接从account中扣除款项
* avail扣空
* pay没有完全支付account中充值了会自动继续进行支付
*/
data.iState = 'paid',
const [account] = await context.select('account', {
data: {
id: 1,
avail: 1,
},
filter: {
id: accountId,
}
}, { forUpdate: true });
const { avail } = account;
const paid = Math.min(price!, avail!);
data.accountOper$entity = [
{
id: await generateNewIdAsync(),
action: 'create',
data: {
id: await generateNewIdAsync(),
totalPlus: -price!,
availPlus: -price!,
totalPlus: -paid,
availPlus: -paid,
type: 'consume',
accountId,
},
}
];
data.meta = {};
data.paid = pay.price;
data.paid = paid;
}
getState(pay: Pay): Promise<[string, PayUpdateData]> {