diff --git a/src/checkers/account.ts b/src/checkers/account.ts new file mode 100644 index 00000000..b10ba5ee --- /dev/null +++ b/src/checkers/account.ts @@ -0,0 +1,8 @@ +import { Checker } from 'oak-domain/lib/types/Auth'; +import { EntityDict } from '../oak-app-domain'; +import { RuntimeCxt } from '../types/RuntimeCxt'; + +const checkers: Checker[] = [ +]; + +export default checkers; diff --git a/src/checkers/accountOper.ts b/src/checkers/accountOper.ts new file mode 100644 index 00000000..a02964aa --- /dev/null +++ b/src/checkers/accountOper.ts @@ -0,0 +1,79 @@ +import assert from 'assert'; +import { Checker } from 'oak-domain/lib/types/Auth'; +import { EntityDict } from '../oak-app-domain'; +import { RuntimeCxt } from '../types/RuntimeCxt'; +import { OakInputIllegalException } from 'oak-domain/lib/types'; + +const checkers: Checker[] = [ + { + entity: 'accountOper', + action: 'create', + type: 'row', + filter: { + account: { + ableState: 'enabled', + }, + }, + errMsg: 'account.update.accountDisabled', + }, + { + entity: 'accountOper', + action: 'create', + type: 'data', + checker(data, context) { + assert(!(data instanceof Array)); + const { type, totalPlus, availPlus } = data; + if (typeof totalPlus !== 'number') { + throw new OakInputIllegalException('accountOper', ['totalPlus'], 'accountOper中的totalPlus不是数字'); + } + if (typeof availPlus !== 'number') { + throw new OakInputIllegalException('accountOper', ['availPlus'], 'accountOper中的availPlus不是数字'); + } + switch (type) { + case 'consume': { + if (totalPlus >= 0 || availPlus > 0 || totalPlus > availPlus) { + throw new OakInputIllegalException('accountOper', ['availPlus'], 'accountOper为consume时,其totalPlus/availPlus必须为0或负数,且totalPlus的绝对值要更大'); + } + break; + } + case 'deposit': { + if (totalPlus < 0 || availPlus < 0 || totalPlus !== availPlus) { + throw new OakInputIllegalException('accountOper', ['availPlus'], 'accountOper为deposit时,其totalPlus/availPlus必须为正数且相等'); + } + break; + } + case 'loan': { + if (totalPlus !== 0 || availPlus >= 0) { + throw new OakInputIllegalException('accountOper', ['availPlus'], 'accountOper为loan时,其totalPlus必须为0,且availPlus必须为负数'); + } + break; + } + case 'repay': { + if (totalPlus !== 0 || availPlus <= 0) { + throw new OakInputIllegalException('accountOper', ['availPlus'], 'accountOper为repay时,其totalPlus必须为0,且availPlus必须为正数'); + } + break; + } + case 'withdraw': { + if (totalPlus >= 0 || availPlus >= 0 || totalPlus !== availPlus) { + throw new OakInputIllegalException('accountOper', ['availPlus'], 'accountOper为withdraw时,其totalPlus和availPlus必须为不相等的负数'); + } + break; + } + case 'withdrawBack': { + if (totalPlus <= 0 || availPlus <= 0 || totalPlus !== availPlus) { + throw new OakInputIllegalException('accountOper', ['availPlus'], 'accountOper为withdraw时,其totalPlus和availPlus必须为不相等的正数'); + } + break; + } + default: { + assert(false); + break; + } + } + + } + } +]; + +export default checkers; diff --git a/src/checkers/index.ts b/src/checkers/index.ts index 32fb152d..9437739c 100644 --- a/src/checkers/index.ts +++ b/src/checkers/index.ts @@ -1,9 +1,10 @@ import { EntityDict } from '@oak-app-domain'; import { Checker } from 'oak-domain/lib/types'; import { RuntimeCxt } from '../types/RuntimeCxt'; +import aoCheckers from './accountOper'; const checkers = [ - + ...aoCheckers, ] as Checker[]; export default checkers; diff --git a/src/checkers/pay.ts b/src/checkers/pay.ts new file mode 100644 index 00000000..96ed4fbd --- /dev/null +++ b/src/checkers/pay.ts @@ -0,0 +1,100 @@ +import { Checker } from 'oak-domain/lib/types/Auth'; +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 assert from 'assert'; +import { PAY_CHANNEL_ACCOUNT_ID } from '@project/config/constants'; + +const checkers: Checker[] = [ + { + entity: 'pay', + type: 'logical', + action: 'create', + checker(operation) { + const { data } = operation as EntityDict['pay']['CreateSingle']; + data.refunded = 0; + data.paid = 0; + }, + }, + { + entity: 'pay', + type: 'data', + action: 'create', + checker(data) { + const { channelId, price, orderId, accountId } = data as EntityDict['pay']['CreateSingle']['data']; + if (price! <= 0) { + throw new OakInputIllegalException('pay', ['price'], '支付金额必须大于0'); + } + if (!orderId) { + // 充值类订单 + if (!accountId) { + throw new OakInputIllegalException('pay', ['accountId'], '充值类支付必须指定accountId'); + } + else if (channelId === PAY_CHANNEL_ACCOUNT_ID) { + throw new OakInputIllegalException('pay', ['channelId'], '充值类支付不能使用帐户支付'); + } + } + else { + if (channelId === PAY_CHANNEL_ACCOUNT_ID) { + if (!accountId) { + throw new OakInputIllegalException('pay', ['accountId'], '使用account支付必须指向accountId'); + } + } + } + } + }, + { + entity: 'pay', + type: 'logicalData', + action: 'create', + checker(operation, context) { + const { data } = operation as EntityDict['pay']['CreateSingle']; + const { orderId, price } = data; + + if (orderId) { + // 所有已经支付和正在支付的pay之和不能超过订单总和 + const order = context.select('order', { + data: { + id: 1, + price: 1, + pay$order: { + $entity: 'pay', + data: { + id: 1, + price: 1, + }, + filter: { + iState: { + $in: ['paying', 'paid'], + } + } + } + }, + filter: { + id: orderId, + } + }, {}); + + const checkPays = (order: Partial) => { + const { price: orderPrice, pay$order: pays } = order; + let pricePaying = 0; + pays!.forEach( + (pay) => pricePaying += pay.price! + ); + if (pricePaying + price! > orderPrice!) { + throw new OakInputIllegalException('pay', ['price'], 'pay.create.priceOverflow'); + } + }; + if (order instanceof Promise) { + return order.then( + ([o]) => checkPays(o) + ); + } + return checkPays(order[0]); + } + } + } +]; + +export default checkers; diff --git a/src/checkers/store.ts b/src/checkers/store.ts deleted file mode 100644 index 14606669..00000000 --- a/src/checkers/store.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { EntityDict } from '@oak-app-domain'; -import { Checker } from 'oak-domain/lib/types'; -import { RuntimeCxt } from '../types/RuntimeCxt'; -import { checkAttributesNotNull } from 'oak-domain/lib/utils/validator'; -import { CreateOperationData as CreateStoreData } from '@oak-app-domain/Store/Schema'; - -export const checkers: Checker[] = [ - { - type: 'data', - action: 'create', - entity: 'store', - checker: (data, context) => { - if (data instanceof Array) { - data.forEach((ele) => { - checkAttributesNotNull('store', data, [ - 'coordinate', - 'name', - 'addrDetail', - 'areaId', - ]); - }); - } else { - checkAttributesNotNull('store', data, [ - 'coordinate', - 'name', - 'addrDetail', - 'areaId', - ]); - } - return 0; - }, - }, -]; diff --git a/src/config/constants.ts b/src/config/constants.ts index 75ad8976..71b8b8d9 100644 --- a/src/config/constants.ts +++ b/src/config/constants.ts @@ -1 +1,6 @@ -export const DATA_SUBSCRIBER_KEYS = {}; \ No newline at end of file +export const DATA_SUBSCRIBER_KEYS = {}; + +export const PAY_CHANNEL_ACCOUNT_ID = 'payChannelAccountId'; +export const PAY_CHANNEL_ACCOUNT_CODE = 'ACCOUNT'; +export const PAY_CHANNEL_OFFLINE_ID = 'payChannelOfflineId'; +export const PAY_CHANNEL_OFFLINE_CODE = 'OFFLINE'; \ No newline at end of file diff --git a/src/data/actionAuth.ts b/src/data/actionAuth.ts deleted file mode 100644 index 3bafae47..00000000 --- a/src/data/actionAuth.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { CreateOperationData as ActionAuth } from '@oak-app-domain/ActionAuth/Schema'; - - -const actionAuths: ActionAuth[] = [ - -]; - -export default actionAuths; diff --git a/src/data/i18n.ts b/src/data/i18n.ts deleted file mode 100644 index 4ed525c1..00000000 --- a/src/data/i18n.ts +++ /dev/null @@ -1,5 +0,0 @@ -// 本文件为自动编译产生,请勿直接修改 - -import { CreateOperationData as I18n } from "../oak-app-domain/I18n/Schema"; -const i18ns: I18n[] = []; -export default i18ns; \ No newline at end of file diff --git a/src/data/index.ts b/src/data/index.ts index 510e8ec3..0b395653 100644 --- a/src/data/index.ts +++ b/src/data/index.ts @@ -1,13 +1,7 @@ import { relations } from '@project/oak-app-domain/Relation'; -import actionAuth from './actionAuth'; -import relationAuth from './relationAuth'; -import path from './path'; -import i18n from './i18n'; +import payChannel from './payChannel'; export default { relation: relations, - actionAuth, - relationAuth, - path, - i18n, + payChannel, }; diff --git a/src/data/path.ts b/src/data/path.ts deleted file mode 100644 index 5e459cfa..00000000 --- a/src/data/path.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { CreateOperationData as Path } from '@oak-app-domain/Path/Schema'; - -const paths: Path[] = []; - -export default paths; diff --git a/src/data/payChannel.ts b/src/data/payChannel.ts new file mode 100644 index 00000000..dbcbc7e0 --- /dev/null +++ b/src/data/payChannel.ts @@ -0,0 +1,16 @@ +import { PAY_CHANNEL_ACCOUNT_CODE, PAY_CHANNEL_ACCOUNT_ID, PAY_CHANNEL_OFFLINE_CODE, PAY_CHANNEL_OFFLINE_ID } from '@project/config/constants'; +import { CreateOperationData as PayChannel } from '../oak-app-domain/PayChannel/Schema'; +const data: PayChannel[] = [ + { + id: PAY_CHANNEL_ACCOUNT_ID, + code: PAY_CHANNEL_ACCOUNT_CODE, + svg: 'todo url', + }, + { + id: PAY_CHANNEL_OFFLINE_ID, + code: PAY_CHANNEL_OFFLINE_CODE, + svg: 'todo url', + } +]; + +export default data; \ No newline at end of file diff --git a/src/data/relationAuth.ts b/src/data/relationAuth.ts deleted file mode 100644 index 0ae57eda..00000000 --- a/src/data/relationAuth.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { CreateOperationData as RelationAuth } from '@oak-app-domain/RelationAuth/Schema'; - - -const relationAuths: RelationAuth[] = [ - -] - -export default relationAuths; \ No newline at end of file diff --git a/src/entities/Account.ts b/src/entities/Account.ts new file mode 100644 index 00000000..bce98583 --- /dev/null +++ b/src/entities/Account.ts @@ -0,0 +1,68 @@ +import { + String, + Text, + Price, + Boolean, + Datetime, +} from 'oak-domain/lib/types/DataType'; +import { AbleAction, AbleState, makeAbleActionDef } from 'oak-domain/lib/actions/action'; +import { EntityShape } from 'oak-domain/lib/types/Entity'; +import { EntityDesc, ActionDef } from 'oak-domain/lib/types'; + +export interface Schema extends EntityShape { + total: Price; + avail: Price; +}; + +type IAction = 'deposit' | 'withdraw' | 'withdrawBack' | 'consume' | 'loan' | 'repay'; + +export type Action = AbleAction | IAction; +export const AbleActionDef: ActionDef = makeAbleActionDef('enabled'); + +export const entityDesc: EntityDesc = { + locales: { + zh_CN: { + name: '帐户', + attr: { + total: '总余额', + avail: '可用余额', + ableState: '状态', + }, + action: { + enable: '启用', + disable: '禁用', + deposit: '充值', + withdraw: '提现', + withdrawBack: '提现退还', + consume: '消费', + loan: '抵押', + repay: '偿还', + }, + v: { + ableState: { + enabled: '可用的', + disabled: '禁用的', + } + } + }, + }, + style: { + icon: { + enable: '', + disable: '', + deposit: '', + withdraw: '', + consume: '', + loan: '', + repay: '', + }, + color: { + ableState: { + enabled: '#008000', + disabled: '#A9A9A9' + } + } + } +}; diff --git a/src/entities/AccountOper.ts b/src/entities/AccountOper.ts new file mode 100644 index 00000000..2e212f58 --- /dev/null +++ b/src/entities/AccountOper.ts @@ -0,0 +1,63 @@ +import { + String, + Text, + Price, + Boolean, + 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 Account } from './Account'; + +type Type = 'deposit' | 'withdraw' | 'consume' | 'loan' | 'repay' | 'withdrawBack'; + +export interface Schema extends EntityShape { + account: Account; + type: Type; + totalPlus: Price; + availPlus: Price; + entity: String<32>; + entityId: String<64>; +}; + +export const entityDesc: EntityDesc = { + locales: { + zh_CN: { + name: '帐号操作', + attr: { + account: '帐号', + type: '类型', + totalPlus: '余额变化', + availPlus: '可用余额变化', + entity: '关联对象', + entityId: '关联对象Id', + }, + v: { + type: { + deposit: '充值', + withdraw: '提现', + consume: '消费', + loan: '抵押', + repay: '偿还', + withdrawBack: '提现失败', + }, + }, + }, + }, + style: { + color: { + type: { + deposit: '#3498DB', + withdraw: '#F7DC6F', + consume: '#A569BD', + loan: '#CD6155', + repay: '#82E0AA', + } + } + }, + configuration: { + actionType: 'appendOnly', + } +} \ No newline at end of file diff --git a/src/entities/Pay.ts b/src/entities/Pay.ts index 8d95b245..3daa4e14 100644 --- a/src/entities/Pay.ts +++ b/src/entities/Pay.ts @@ -9,35 +9,40 @@ import { EntityShape } from 'oak-domain/lib/types/Entity'; import { EntityDesc, ActionDef } from 'oak-domain/lib/types'; import { Schema as Order } from './Order'; import { Schema as PayChannel } from './PayChannel'; +import { Schema as Account } from './Account'; +import { Schema as AccountOper } from './AccountOper'; +/** + * 约定:充值类pay,其orderId为null,accountId指向要充值的帐户(channel不能是ACCOUNT) + * 订单类pay,其orderId指向订单,accountId指向付款的帐户(channel必须是ACCOUNT) + */ export interface Schema extends EntityShape { price: Price; paid: Price; refunded: Price; - order: Order; channel: PayChannel; timeoutAt: Datetime; forbidRefundAt?: Datetime; + account?: Account; + order?: Order; + opers: AccountOper[]; }; -type IAction = 'startPaying' | 'payAll' | 'payPartially' | 'payNone' | 'timeout' | 'cancel' | 'startRefunding' | 'refundAll' | 'refundPartially' | 'refundNone'; -type IState = 'paid' | 'unPaid' | 'timeout' | 'cancelled' | 'paying' | 'partiallyPaid' | 'paid' | 'refunding' | 'partiallyRefunded' | 'refunded'; +type IAction = 'startPaying' | 'succeedPaying' | 'startClosing' | 'succeedClosing' | 'startRefunding' | 'refundAll' | 'refundPartially'; +type IState = 'unpaid' | 'paying' | 'paid' | 'closing' | 'closed' | 'refunding' | 'partiallyRefunded' | 'refunded'; export const IActionDef: ActionDef = { stm: { - startPaying: ['unPaid', 'paying'], - payAll: [['unPaid', 'paying', 'partiallyPaid'], 'paid'], - payPartially: [['unPaid', 'paying'], 'partiallyPaid'], - payNone: ['paying', 'unPaid'], - timeout: ['unPaid', 'timeout'], - cancel: ['unPaid', 'cancelled'], - startRefunding: [['paid', 'partiallyPaid'], 'refunding'], - refundAll: [['paid', 'refunding', 'partiallyPaid', 'partiallyRefunded'], 'refunded'], - refundPartially: [['paid', 'refunding', 'partiallyPaid', 'partiallyRefunded'], 'partiallyRefunded'], - refundNone: ['refunding', 'paid'], + startPaying: [['unpaid', 'paying'], 'paying'], + succeedPaying: [['unpaid', 'paying'], 'paid'], + startClosing: ['paying', 'closing'], + succeedClosing: [['closing', 'paying'], 'closed'], + startRefunding: [['paid', 'partiallyRefunded', 'refunding'], 'refunding'], + refundAll: [['paid', 'refunding', 'partiallyRefunded'], 'refunded'], + refundPartially: [['paid', 'refunding', 'partiallyRefunded'], 'partiallyRefunded'], }, - is: 'unPaid', + is: 'unpaid', }; type Action = IAction; @@ -67,6 +72,8 @@ export const entityDesc: EntityDesc = { + stm: { + succeed: ['withdrawing', 'successful'], + fail: ['withdrawing', 'failed'], + }, + is: 'withdrawing', +}; +type Action = IAction; + +export const entityDesc: EntityDesc = { + locales: { + zh_CN: { + name: '提现', + attr: { + account: '帐户', + price: '金额', + channel: '途径', + iState: '状态', + opers: '被关联帐户操作', + }, + v: { + iState: { + withdrawing: '提现中', + successful: '成功的', + failed: '失败的', + }, + }, + action: { + succeed: '成功', + fail: '失败', + }, + }, + }, + style: { + icon: { + succeed: '', + fail: '', + }, + color: { + iState: { + withdrawing: '#D2B4DE', + successful: '#2E86C1', + failed: '#D6DBDF', + } + } + } +} \ No newline at end of file diff --git a/src/entities/WithdrawChannel.ts b/src/entities/WithdrawChannel.ts new file mode 100644 index 00000000..a71f275e --- /dev/null +++ b/src/entities/WithdrawChannel.ts @@ -0,0 +1,44 @@ +import { + String, + Text, + Price, + Boolean, + 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 PayChannel } from './PayChannel'; + +export interface Schema extends EntityShape { + name: String<64>; + code: String<64>; + data: Object; + payChannel?: PayChannel; +}; + +export const entityDesc: EntityDesc = { + locales: { + zh_CN: { + name: '提现途径', + attr: { + name: '姓名', + code: '帐号', + data: 'metadata', + payChannel: '关联支付途径', + } + }, + }, + indexes: [ + { + name: 'code_uniqe', + attributes: [ + { + name: 'code', + } + ], + config: { + unique: true, + }, + }, + ], +}; diff --git a/src/initialize.ts b/src/initialize.ts deleted file mode 100644 index 34c4a48d..00000000 --- a/src/initialize.ts +++ /dev/null @@ -1,4 +0,0 @@ -import initialize from './initialize.dev'; -export default initialize; - -console.log('不应该走到这里'); diff --git a/src/triggers/accountOper.ts b/src/triggers/accountOper.ts new file mode 100644 index 00000000..a1810ce3 --- /dev/null +++ b/src/triggers/accountOper.ts @@ -0,0 +1,46 @@ +import { CreateTriggerInTxn, Trigger } from 'oak-domain/lib/types/Trigger'; +import { generateNewIdAsync } from 'oak-domain/lib/utils/uuid'; +import { EntityDict } from '../oak-app-domain'; +import BackendRuntimeContext from '@project/context/BackendRuntimeContext'; +import assert from 'assert'; + +const triggers: Trigger[] = [ + { + name: '当生成accountOper时,修改account中的值', + entity: 'accountOper', + action: 'create', + when: 'after', + fn: async ({ operation }, context, option) => { + const { data } = operation; + assert(!(data instanceof Array)); + const { accountId, totalPlus, availPlus, type } = data; + const [account] = await context.select('account', { + data: { + id: 1, + total: 1, + avail: 1, + }, + filter: { + id: accountId!, + } + }, { forUpdate: true }); + + const { total, avail } = account; + await context.operate('account', { + id: await generateNewIdAsync(), + action: type!, + data: { + total: total! + totalPlus!, + avail: avail! + availPlus!, + }, + filter: { + id: accountId!, + } + }, option); + + return 1; + }, + } as CreateTriggerInTxn +]; + +export default triggers; \ No newline at end of file diff --git a/src/triggers/index.ts b/src/triggers/index.ts index f4a0d0a0..1e8928ed 100644 --- a/src/triggers/index.ts +++ b/src/triggers/index.ts @@ -1,10 +1,12 @@ import { EntityDict } from '@oak-app-domain'; import { Trigger } from 'oak-domain/lib/types'; import { BackendRuntimeContext } from '../context/BackendRuntimeContext'; - +import aoTriggers from './accountOper'; +import payTriggers from './pay'; const triggers = [ - + ...aoTriggers, + ...payTriggers, ] as Trigger[]; export default triggers; diff --git a/src/triggers/order.ts b/src/triggers/order.ts new file mode 100644 index 00000000..88403096 --- /dev/null +++ b/src/triggers/order.ts @@ -0,0 +1,9 @@ +import { CreateTriggerInTxn, Trigger, UpdateTriggerInTxn } from 'oak-domain/lib/types/Trigger'; +import { generateNewIdAsync } from 'oak-domain/lib/utils/uuid'; +import { EntityDict } from '@project/oak-app-domain'; +import BackendRuntimeContext from '@project/context/BackendRuntimeContext'; +import assert from 'assert'; +import { OperateOption } from 'oak-domain/lib/types'; + +const triggers: Trigger[] = [ +]; \ No newline at end of file diff --git a/src/triggers/pay.ts b/src/triggers/pay.ts new file mode 100644 index 00000000..83b51505 --- /dev/null +++ b/src/triggers/pay.ts @@ -0,0 +1,239 @@ +import { CreateTriggerInTxn, Trigger, UpdateTriggerInTxn } from 'oak-domain/lib/types/Trigger'; +import { generateNewIdAsync } from 'oak-domain/lib/utils/uuid'; +import { EntityDict } from '@project/oak-app-domain'; +import BackendRuntimeContext from '@project/context/BackendRuntimeContext'; +import assert from 'assert'; +import { OperateOption } from 'oak-domain/lib/types'; + +async function changeOrderStateByPay( + filter: NonNullable, + context: BackendRuntimeContext, + option: OperateOption) { + const orders = await context.select('order', { + data: { + id: 1, + iState: 1, + price: 1, + paid: 1, + refunded: 1, + pay$order: { + $entity: 'pay', + data: { + id: 1, + price: 1, + paid: 1, + refunded: 1, + iState: 1, + }, + }, + }, + filter, + }, { dontCollect: true }); + assert(orders.length === 1); + const [{ id: orderId, pay$order: pays, price: orderPrice, + paid: orderPaid, refunded: orderRefunded, iState: orderIState }] = orders; + + let hasPaying = false, hasRefunding = false; + let payPaid = 0, payRefunded = 0; + for (const pay of pays!) { + const { price, iState, paid, refunded } = pay; + switch (iState!) { + case 'paying': { + hasPaying = true; + break; + } + case 'refunding': { + hasRefunding = true; + break; + } + default: { + break; + } + } + if (paid) { + payPaid += paid; + } + if (refunded) { + payRefunded += refunded; + } + } + + if (hasPaying) { + // 一定是在支付状态 + assert(!hasRefunding && payRefunded === 0 && payPaid < orderPrice!); + if (orderIState !== 'paying' || orderPaid !== payPaid) { + await context.operate('order', { + id: await generateNewIdAsync(), + action: 'startPaying', + data: { + paid: payPaid, + }, + id: orderId!, + }, option); + return 1; + } + } + else if (hasRefunding) { + assert(!hasPaying && payPaid === orderPrice && payPaid === orderPaid && payRefunded < orderPrice); + if (orderIState !== 'refunding' || orderRefunded !== payRefunded) { + await context.operate('order', { + id: await generateNewIdAsync(), + action: 'startRefunding', + data: { + refunded: payRefunded, + }, + id: orderId!, + }, option); + return 1; + } + } + else if (payRefunded) { + // 已经在退款流程当中 + assert(payPaid === orderPrice && payPaid === orderPaid); + const iState = payPaid === orderPrice ? 'refunded' : 'partiallyRefunded'; + if (orderIState !== iState || orderRefunded !== payRefunded) { + const action = payPaid === orderPrice ? 'refundAll' : 'refundPartially'; + await context.operate('order', { + id: await generateNewIdAsync(), + action, + data: { + refunded: payRefunded, + }, + id: orderId!, + }, option); + return 1; + } + } + else if (payPaid) { + // 在支付流程当中 + assert(orderRefunded === 0); + const iState = payPaid === orderPrice ? 'paid' : 'partiallyPaid'; + if (orderIState !== iState || orderPaid !== payPaid) { + const action = payPaid === orderPrice ? 'payAll' : 'payPartially'; + await context.operate('order', { + id: await generateNewIdAsync(), + action, + data: { + paid: payPaid, + }, + id: orderId!, + }, option); + return 1; + } + } + else { + const iState = unpaid; + assert(orderRefunded === 0 && orderPaid === 0); + if (orderIState !== iState) { + await context.operate('order', { + id: await generateNewIdAsync(), + action: 'payNone', + data: { + }, + id: orderId!, + }, option); + return 1; + } + } +} + +const triggers: Trigger[] = [ + { + name: '当生成pay时,自动开始支付流程', + entity: 'pay', + action: 'create', + when: 'after', + fn: async ({ operation }, context, option) => { + const { data } = operation; + assert(!(data instanceof Array)); + const { accountId, price, orderId, id } = data; + if (orderId) { + if (accountId) { + // 使用帐户支付,直接成功并扣款 + await context.operate('pay', { + id: await generateNewIdAsync(), + action: 'payAll', + data: { + accountOper$entity: [ + { + id: await generateNewIdAsync(), + action: 'create', + data: { + id: await generateNewIdAsync(), + totalPlus: -price!, + availPlus: -price!, + }, + } + ] + }, + filter: { + id, + } + }, option); + return 1; + } + } + + // 其余情况都是进入paying流程 + await context.operate('pay', { + id: await generateNewIdAsync(), + action: 'startPaying', + data: { + }, + filter: { + id, + } + }, option); + + return 1; + }, + } as CreateTriggerInTxn, + { + name: '当pay的状态发生变化时,修改相应的order的状态或者account的状态', + entity: 'pay', + action: ['startPaying', 'succeedPaying', 'startClosing', 'succeedClosing', 'startRefunding', + 'refundAll', 'refundPartially'], + when: 'after', + fn: async ({ operation }, context, option) => { + const { filter, action } = operation as EntityDict['pay']['Update']; + assert(typeof filter!.id === 'string'); + + const [pay] = await context.select('pay', { + data: { + id: 1, + orderId: 1, + accountId: 1, + price: 1, + }, + filter, + }, { dontCollect: true }); + const { orderId, accountId } = pay; + + if (orderId) { + return await changeOrderStateByPay({ id: orderId }, context, option); + } + else { + assert(accountId); + // 如果是支付成功,则增加帐户余额,其它暂时不支持(提现暂时设计成不走退款) + if (action === 'succeedPaying') { + const payPrice = pay.price!; + await context.operate('accountOper', { + id: await generateNewIdAsync(), + action: 'create', + data: { + id: await generateNewIdAsync(), + totalPlus: payPrice, + availPlus: payPrice, + entity: 'pay', + entityId: pay.id!, + } + }, option); + return 1; + } + return 0; + } + }, + } as UpdateTriggerInTxn, +]; + +export default triggers; \ No newline at end of file diff --git a/src/watchers/index.ts b/src/watchers/index.ts index 30bcf040..52686251 100644 --- a/src/watchers/index.ts +++ b/src/watchers/index.ts @@ -1,8 +1,11 @@ import { Watcher } from 'oak-domain/lib/types'; import { EntityDict } from '@oak-app-domain'; import { BackendRuntimeContext } from '../context/BackendRuntimeContext'; +import orderWatchers from './order'; -const watchers = [] as Watcher< +const watchers = [ + ...orderWatchers, +] as Watcher< EntityDict, keyof EntityDict, BackendRuntimeContext diff --git a/src/watchers/order.ts b/src/watchers/order.ts new file mode 100644 index 00000000..2cbef952 --- /dev/null +++ b/src/watchers/order.ts @@ -0,0 +1,22 @@ +import { BBWatcher } from 'oak-domain/lib/types/Watcher'; +import { EntityDict } from '@project/oak-app-domain'; + +const watchers: BBWatcher[] = [ + { + name: '使过期的order结束', + entity: 'order', + filter: () => { + const now = Date.now(); + return { + iState: 'unpaid', + timeoutAt: { + $lte: now, + }, + }; + }, + action: 'timeout', + actionData: {}, + } +]; + +export default watchers; \ No newline at end of file