退款的逻辑和界面

This commit is contained in:
Xu Chang 2024-05-29 21:05:10 +08:00
parent 3908215be5
commit 5b5a7f1b70
139 changed files with 3982 additions and 280 deletions

View File

@ -1,4 +1,5 @@
import { BRC } from '../types/RuntimeCxt';
export declare function getAccountPayRefunds(params: {
accountId: string;
price: number;
}, context: BRC): Promise<Omit<import("../oak-app-domain/Refund/Schema").CreateOperationData, "entity" | "entityId">[]>;

View File

@ -1,4 +1,4 @@
import { getAccountPayRefunds as getAprsFn } from '../utils/pay';
export async function getAccountPayRefunds(params, context) {
return getAprsFn(context, params.accountId);
return getAprsFn(context, params.accountId, params.price);
}

View File

@ -2,5 +2,6 @@ declare const _default: (props: import("oak-frontend-base").ReactComponentProps<
depositMinCent: number;
depositMaxCent: number;
onDepositPayId: (payId: string) => void;
onWithdraw: () => void;
}>) => React.ReactElement;
export default _default;

View File

@ -52,6 +52,7 @@ export default OakComponent({
depositMinCent: 0,
depositMaxCent: 1000000,
onDepositPayId: (payId) => undefined,
onWithdraw: () => undefined,
},
formData({ data }) {
const unfinishedPayId = data?.pay$account?.[0]?.id;
@ -158,6 +159,9 @@ export default OakComponent({
const { onDepositPayId } = this.props;
const { unfinishedPayId } = this.state;
onDepositPayId && onDepositPayId(unfinishedPayId);
},
onWithdrawClick() {
this.props.onWithdraw();
}
},
data: {

View File

@ -24,6 +24,7 @@
<l-button
type="success"
size="long"
bind:lintap="onWithdrawClick"
>
{{t('account:action.withdraw')}}
</l-button>

View File

@ -7,6 +7,7 @@ export default function Render(props: WebComponentProps<EntityDict, 'account', f
depositMinCent: number;
unfinishedPayId?: string;
onDepositPayId: (payId: string) => void;
onWithdraw: () => void;
depositOpen: boolean;
ufOpen: boolean;
depPrice: number | null;

View File

@ -5,7 +5,7 @@ import classNames from 'classnames';
import { CentToString } from 'oak-domain/lib/utils/money';
import AccountDeposit from '../deposit';
export default function Render(props) {
const { account, depositMaxCent, unfinishedPayId, onDepositPayId, depositOpen, depositMinCent, ufOpen, depPrice, depositChannel, depositMeta, depositing } = props.data;
const { account, depositMaxCent, unfinishedPayId, onDepositPayId, depositOpen, depositMinCent, ufOpen, depPrice, depositChannel, depositMeta, depositing, onWithdraw, } = props.data;
const { t, createDepositPay, setMessage, setDepositOpen, setUfOpen, setDepPrice, setDepositChannel, setDepositMeta, setDepositing, onDepositClick, onDepositModalClose, onUfModalClose } = props.methods;
if (account) {
const { total, avail, '#oakLegalActions': legalActions, accountOper$account: opers } = account;
@ -40,10 +40,7 @@ export default function Render(props) {
</Button>
</div>}
{legalActions?.includes('withdraw') && <div className={Styles.item}>
<Button block onClick={() => setMessage({
type: 'warning',
content: '尚未实现'
})}>
<Button block onClick={() => onWithdraw()}>
{t('account:action.withdraw')}
</Button>
</div>}

View File

@ -7,6 +7,7 @@ export default function Render(props: WebComponentProps<EntityDict, 'account', f
depositMinCent: number;
unfinishedPayId?: string;
onDepositPayId: (payId: string) => void;
onWithdraw: () => void;
depositOpen: boolean;
ufOpen: boolean;
depPrice: number | null;

View File

@ -7,7 +7,7 @@ import AccountDeposit from '../deposit';
import AccountOperList from '../../accountOper/pure/List.pc';
import { AccountBookOutlined, ScheduleOutlined } from '@ant-design/icons';
export default function Render(props) {
const { account, depositMaxCent, unfinishedPayId, onDepositPayId, depositOpen, depositMinCent, ufOpen, depPrice, depositChannel, depositMeta, depositing } = props.data;
const { account, depositMaxCent, unfinishedPayId, onDepositPayId, depositOpen, depositMinCent, ufOpen, depPrice, depositChannel, depositMeta, depositing, onWithdraw, } = props.data;
const { t, createDepositPay, setMessage, setDepositOpen, setUfOpen, setDepPrice, setDepositChannel, setDepositMeta, setDepositing, onDepositClick, onDepositModalClose, } = props.methods;
if (account) {
const { total, avail, '#oakLegalActions': legalActions, accountOper$account: opers } = account;
@ -16,10 +16,7 @@ export default function Render(props) {
{legalActions?.includes('deposit') && <Button type="primary" disabled={ufOpen || depositOpen} onClick={() => onDepositClick()}>
{t('account:action.deposit')}
</Button>}
{legalActions?.includes('withdraw') && <Button onClick={() => setMessage({
type: 'warning',
content: '尚未实现'
})}>
{legalActions?.includes('withdraw') && <Button onClick={() => onWithdraw()}>
{t('account:action.withdraw')}
</Button>}
</Flex>}>

View File

@ -0,0 +1,8 @@
import { EntityDict } from "../../../oak-app-domain";
declare const _default: (props: import("oak-frontend-base").ReactComponentProps<EntityDict, keyof EntityDict, boolean, {
accountId: string;
withdrawAccountFilter: import("../../../oak-app-domain/WithdrawAccount/Schema").Filter | undefined;
onNewWithdrawAccount: () => void;
onCreateWithdraw: (id: string) => void;
}>) => React.ReactElement;
export default _default;

View File

@ -0,0 +1,211 @@
import { ToCent, ToYuan, ThousandCont } from "oak-domain/lib/utils/money";
import assert from "assert";
import { RefundExceedMax } from "../../../types/Exception";
import { generateNewIdAsync } from "oak-domain/lib/utils/uuid";
export default OakComponent({
properties: {
accountId: '',
withdrawAccountFilter: {},
onNewWithdrawAccount: () => undefined,
onCreateWithdraw: (id) => undefined,
},
formData({ features }) {
const { accountId, withdrawAccountFilter } = this.props;
const [account] = features.cache.get('account', {
data: {
id: 1,
avail: 1,
refundable: 1,
},
filter: {
id: accountId,
}
});
const withdrawAccounts = features.cache.get('withdrawAccount', {
data: {
id: 1,
org: 1,
name: 1,
code: 1,
channel: {
id: 1,
name: 1,
lossRatio: 1,
}
},
filter: withdrawAccountFilter
});
const { avail, refundable } = account;
const refundAmount = Math.min(avail, refundable);
const manualAmount = avail - refundAmount;
const { value, withdrawData } = this.state;
const refundData = withdrawData && withdrawData.refund$entity?.map(ele => ele.data).map((refund) => {
const { meta, price, loss } = refund;
const { refundLossRatio, refundLossFloor, channel } = meta;
return {
lossExp: refundLossRatio ? this.t('refund.lossExp.ratio', { ratio: refundLossRatio }) : (refundLossFloor ? this.t(`refund.lossExp.floor.${refundLossFloor}`) : this.t('refund.lossExp.none')),
channel: this.t(`payChannel::${channel}`),
priceYuan: ThousandCont(ToYuan(price), 2),
lossYuan: ThousandCont(ToYuan(loss), 2),
finalYuan: ThousandCont(ToYuan(price - loss), 2),
};
});
const withdrawExactPrice = ThousandCont(ToYuan(withdrawData ? withdrawData.price - withdrawData.loss : 0), 2);
return {
executale: value > 0,
account,
withdrawAccounts,
refundAmount,
refundAmountYuan: ToYuan(refundAmount),
manualAmount,
manualAmountYuan: ToYuan(manualAmount),
avail,
availYuan: ToYuan(avail),
withdrawMethod: refundData && refundData.length ? 'refund' : undefined,
refundData,
withdrawExactPrice,
};
},
features: ['cache'],
lifetimes: {
ready() {
const { withdrawAccountFilter } = this.props;
this.features.cache.refresh('withdrawAccount', {
data: {
id: 1,
org: 1,
name: 1,
code: 1,
channel: {
id: 1,
name: 1,
lossRatio: 1,
}
},
filter: withdrawAccountFilter
});
}
},
data: {
value: 0,
valueYuan: 0,
showMethodHelp: false,
showLossHelp: false,
withdrawData: null,
},
methods: {
setValue(valueYuan) {
let valueYuan2 = typeof valueYuan === 'string' ? parseInt(valueYuan) : valueYuan;
const value = valueYuan2 ? ToCent(valueYuan2) : null;
this.setState({ value, valueYuan: valueYuan2 }, () => this.reRender());
},
switchHelp(type) {
switch (type) {
case 'method': {
this.setState({
showMethodHelp: !this.state.showMethodHelp,
showLossHelp: false,
});
break;
}
case 'loss': {
this.setState({
showMethodHelp: false,
showLossHelp: !this.state.showLossHelp,
});
break;
}
}
},
async createWithdrawData() {
const { value, refundAmount, avail, manualAmount } = this.state;
if (value > avail) {
this.setMessage({
type: 'error',
content: this.t('error.overflow'),
});
}
else if (refundAmount) {
if (value > refundAmount) {
this.setMessage({
type: 'error',
content: this.t('error.overflowRefundAmount'),
});
}
else {
try {
const { result: refundData } = (await this.features.cache.exec('getAccountPayRefunds', {
accountId: this.props.accountId,
price: value,
}));
let loss = 0;
(refundData).forEach((ele) => {
loss += ele.loss || 0;
});
this.setState({
withdrawData: {
id: await generateNewIdAsync(),
accountId: this.props.accountId,
price: value,
loss,
refund$entity: await Promise.all(refundData.map(async (data) => ({
id: await generateNewIdAsync(),
action: 'create',
data,
}))),
creatorId: this.features.token.getUserId(),
},
}, () => this.reRender());
}
catch (err) {
if (err instanceof RefundExceedMax) {
this.features.cache.refresh('account', {
data: {
id: 1,
avail: 1,
refundable: 1,
},
filter: {
id: this.props.accountId,
}
});
}
throw err;
}
}
}
else {
assert(manualAmount);
if (value > manualAmount) {
this.setMessage({
type: 'error',
content: this.t('error.overflowManualAmount'),
});
}
else {
console.warn('还没实现');
}
}
},
clearWithdrawData() {
this.setState({
withdrawData: null,
}, () => this.reRender());
},
async createWithdraw() {
const { withdrawData } = this.state;
assert(withdrawData);
const id = withdrawData.id;
await this.features.cache.exec('operate', {
entity: 'withdraw',
operation: {
id: await generateNewIdAsync(),
data: withdrawData,
action: 'create',
}
});
const { onCreateWithdraw } = this.props;
onCreateWithdraw && onCreateWithdraw(id);
}
}
});

View File

@ -0,0 +1 @@
{}

View File

@ -0,0 +1,39 @@
{
"label": "提现金额",
"placeholder": "请输入提现金额",
"tips": {
"1-1": "当前帐户可用余额",
"1-2": "元,其中:",
"2-1": "系统自动退款额度",
"2-2": "元",
"3-1": "人工划款额度",
"3-2": "元",
"fill": "全部提现"
},
"helps": {
"label": {
"method": "提现途径说明",
"loss": "提现手续费说明"
},
"content": {
"method": "提现时,帐户中可以退款的金额部分优现从充值的途径返回。若充值途径已经不能退款,则由人工划款到用户指定的提现账户中。一次提现不能同时使用两种提现方法,需要先将自动退款额度退完,剩余部分再申请人工划款",
"loss": "因支付途径损耗,提现过程可能存在一定的手续费,其中,自动退款的手续费额度由相应的充值途径决定,人工划款的手续费额度由相应的提现途径决定。当您申请退款确认后,可以看到相应的手续费额度"
}
},
"error": {
"overflow": "提现额度不能超过帐户可用额度",
"overflowRefundAmount": "请先提现自动退款部分的额度",
"overflowManualAmount": "提现额度不能超过人工划款的额度"
},
"refund": {
"lossExp": {
"ratio": "%{ratio}%",
"floor": {
"1": "无",
"2": "按角取整",
"3": "按元取整"
},
"none": "无"
}
}
}

32
es/components/withdraw/create/web.d.ts vendored Normal file
View File

@ -0,0 +1,32 @@
import React from 'react';
import { EntityDict } from "../../../oak-app-domain";
import { WebComponentProps } from "oak-frontend-base";
export default function render(props: WebComponentProps<EntityDict, 'withdraw', false, {
account: EntityDict['account']['OpSchema'];
value: number;
valueYuan: number;
refundAmount: number;
manualAmount: number;
refundAmountYuan: number;
manualAmountYuan: number;
avail: number;
availYuan: number;
showMethodHelp: boolean;
showLossHelp: boolean;
executale: boolean;
withdrawMethod?: 'refund' | 'channel';
refundData?: ({
lossExp: string;
channel: string;
priceYuan: string;
lossYuan: string;
finalYuan: string;
})[];
withdrawExactPrice: string;
}, {
setValue: (v: string | number | null) => void;
switchHelp: (type: 'method' | 'loss') => void;
createWithdrawData: () => void;
clearWithdrawData: () => void;
createWithdraw: () => void;
}>): React.JSX.Element | undefined;

View File

@ -0,0 +1,89 @@
import React from 'react';
import { Button, Input } from 'antd-mobile';
import { DownOutlined, UpOutlined } from '@ant-design/icons';
import Styles from './web.module.less';
import classNames from 'classnames';
import WithdrawDetail from '../dry/Detail.mobile';
export default function render(props) {
const { account, value, refundAmount, manualAmount, avail, availYuan, valueYuan, manualAmountYuan, refundAmountYuan, showMethodHelp, showLossHelp, executale, withdrawMethod, refundData, withdrawExactPrice } = props.data;
const { t, setValue, switchHelp, createWithdrawData, clearWithdrawData, createWithdraw } = props.methods;
if (refundData) {
return (<div className={Styles.container}>
<WithdrawDetail withdrawExactPrice={withdrawExactPrice} withdrawMethod={withdrawMethod} refundData={refundData} t={t} step={0}/>
<div className={Styles.btns}>
<Button className={Styles.btn} block onClick={clearWithdrawData}>
{t('common::action.cancel')}
</Button>
<Button className={Styles.btn} block color="primary" onClick={createWithdraw}>
{t('common::action.commit')}
</Button>
</div>
</div>);
}
if (account) {
if (avail > 0) {
return (<div className={Styles.main}>
<div className={Styles.label}>
{t('label')}
</div>
<div className={Styles.input}>
<span className={Styles.symbol}>{t('common::pay.symbol')}</span>
<Input autoFocus type="number" min={0} max={availYuan} placeholder={t('placeholder')} value={valueYuan ? `${valueYuan}` : undefined} onChange={(value) => setValue(value)} style={{ '--font-size': '28px' }}/>
</div>
<div className={Styles.tips}>
<div className={Styles.tipLine}>
<span>{t('tips.1-1')}</span>
<span className={Styles.value}>{availYuan}</span>
<span>{t('tips.1-2')}</span>
</div>
{refundAmount > 0 && <div className={Styles.tipLine}>
<span>{t('tips.2-1')}</span>
<span className={Styles.value}>
{refundAmountYuan}
</span>
<span>{t('tips.2-2')}</span>
<span className={classNames(Styles.value, Styles.clickable)} onClick={() => setValue(refundAmountYuan)}>
{t('tips.fill')}
</span>
</div>}
{manualAmount > 0 && <div className={Styles.tipLine}>
<span>{t('tips.3-1')}</span>
{refundAmount === 0 ? <span className={Styles.value} onClick={() => setValue(manualAmountYuan)}>
{manualAmountYuan}
</span> : <span className={Styles.value}>
{manualAmountYuan}
</span>}
<span>{t('tips.3-2')}</span>
{refundAmount === 0 && <span className={classNames(Styles.value, Styles.clickable)} onClick={() => setValue(manualAmountYuan)}>
{t('tips.fill')}
</span>}
</div>}
</div>
<div className={Styles.helps}>
<Button size="small" className={Styles.label2} fill="none" onClick={() => switchHelp('method')}>
<span className={showMethodHelp ? Styles.up : Styles.down} style={{ marginRight: 8 }}>
{showMethodHelp ? <UpOutlined /> : <DownOutlined />}
</span>
<span className={showMethodHelp ? Styles.up : Styles.down}>
{t('helps.label.method')}
</span>
</Button>
{showMethodHelp && <div className={Styles.content2}>{t('helps.content.method')}</div>}
<Button size="small" className={Styles.label2} fill="none" onClick={() => switchHelp('loss')}>
<span className={showLossHelp ? Styles.up : Styles.down} style={{ marginRight: 8 }}>
{showLossHelp ? <UpOutlined /> : <DownOutlined />}
</span>
<span className={showLossHelp ? Styles.up : Styles.down}>
{t('helps.label.loss')}
</span>
</Button>
{showLossHelp && <div className={Styles.content2}>{t('helps.content.loss')}</div>}
</div>
<div style={{ flex: 1 }}/>
<Button className={Styles.btn} block color="primary" disabled={!executale} onClick={() => createWithdrawData()}>
{t('common::confirm')}
</Button>
</div>);
}
}
}

View File

@ -0,0 +1,104 @@
.main {
background-color: var(--oak-bg-color-container);
height: calc(100% - 44px);
border-radius: 4px;
padding: 22px;
display: flex;
flex-direction: column;
.label {
margin-top: 18px;
font-weight: bold;
}
.input {
margin-top: 12px;
display: flex;
flex-direction: row;
align-items: baseline;
font-size: 28px;
border-bottom: solid 0.1px silver;
.symbol {
margin-right: 6px;
}
}
.tips {
margin-top: 12px;
.tipLine {
margin-top: 8px;
color: dimgrey;
font-size: x-small;
.value {
color: var(--oak-color-primary);
font-weight: bolder;
margin-left: 2px;
margin-right: 2px;
}
.clickable {
text-decoration: underline;
cursor: pointer;
margin-left: 8px;
}
}
}
.helps {
margin-top: 22px;
display: flex;
flex-direction: column;
align-items: flex-start;
button {
span {
font-size: small;
}
.up {
color: blue;
}
.down {
color: skyblue;
}
}
.label2 {
color: value(--oak-color-primary)
}
.content2 {
margin-bottom: 8px;
margin-left: 22px;
color: slategrey;
border-radius: 4px;
padding: 8px;
}
}
.btn {
align-self: flex-end;
}
}
.container {
height: 100%;
display: flex;
flex-direction: column;
align-items: stretch;
background-color: var(--oak-bg-color-container);
.btns {
display: flex;
flex-direction: row;
padding: 8px;
.btn {
flex: 1;
margin-left: 8px;
border-radius: 0px;
}
}
}

View File

@ -0,0 +1,32 @@
import React from 'react';
import { EntityDict } from "../../../oak-app-domain";
import { WebComponentProps } from "oak-frontend-base";
export default function render(props: WebComponentProps<EntityDict, 'withdraw', false, {
account: EntityDict['account']['OpSchema'];
value: number;
valueYuan: number;
refundAmount: number;
manualAmount: number;
refundAmountYuan: number;
manualAmountYuan: number;
avail: number;
availYuan: number;
showMethodHelp: boolean;
showLossHelp: boolean;
executale: boolean;
withdrawMethod?: 'refund' | 'channel';
refundData?: ({
lossExp: string;
channel: string;
priceYuan: string;
lossYuan: string;
finalYuan: string;
})[];
withdrawExactPrice: string;
}, {
setValue: (v: number | null) => void;
switchHelp: (type: 'method' | 'loss') => void;
createWithdrawData: () => void;
clearWithdrawData: () => void;
createWithdraw: () => void;
}>): React.JSX.Element | undefined;

View File

@ -0,0 +1,71 @@
import React from 'react';
import { Button, Alert, InputNumber } from 'antd';
import { DownOutlined, UpOutlined } from '@ant-design/icons';
import Styles from './web.pc.module.less';
import classNames from 'classnames';
import WithdrawDetail from '../dry/Detail.pc';
export default function render(props) {
const { account, value, refundAmount, manualAmount, avail, availYuan, valueYuan, manualAmountYuan, refundAmountYuan, showMethodHelp, showLossHelp, executale, withdrawMethod, refundData, withdrawExactPrice } = props.data;
const { t, setValue, switchHelp, createWithdrawData, clearWithdrawData, createWithdraw } = props.methods;
if (refundData) {
return (<div className={Styles.container}>
<WithdrawDetail withdrawExactPrice={withdrawExactPrice} withdrawMethod={withdrawMethod} refundData={refundData} t={t} step={0}/>
<div style={{ flex: 1 }}/>
<div className={Styles.btns}>
<Button className={Styles.btn} size="large" onClick={clearWithdrawData}>
{t('common::action.cancel')}
</Button>
<Button className={Styles.btn} size="large" type="primary" onClick={createWithdraw}>
{t('common::action.commit')}
</Button>
</div>
</div>);
}
if (account) {
if (avail > 0) {
return (<div className={Styles.main}>
<div className={Styles.label}>
{t('label')}
</div>
<InputNumber autoFocus addonBefore={t('common::pay.symbol')} className={Styles.input} size="large" value={valueYuan || null} onChange={(value) => setValue(value)} placeholder={t('placeholder')}/>
<div className={Styles.tips}>
<div className={Styles.tipLine}>
<span>{t('tips.1-1')}</span>
<span className={Styles.value}>{availYuan}</span>
<span>{t('tips.1-2')}</span>
</div>
{refundAmount > 0 && <div className={Styles.tipLine}>
<span>{t('tips.2-1')}</span>
<span className={classNames(Styles.value, Styles.clickable)} onClick={() => setValue(refundAmountYuan)}>
{refundAmountYuan}
</span>
<span>{t('tips.2-2')}</span>
</div>}
{manualAmount > 0 && <div className={Styles.tipLine}>
<span>{t('tips.3-1')}</span>
{refundAmount === 0 ? <span className={classNames(Styles.value, Styles.clickable)} onClick={() => setValue(manualAmountYuan)}>
{manualAmountYuan}
</span> : <span className={Styles.value}>
{manualAmountYuan}
</span>}
<span>{t('tips.3-2')}</span>
</div>}
</div>
<div className={Styles.helps}>
<Button size="small" className={Styles.label2} type="link" icon={showMethodHelp ? <UpOutlined /> : <DownOutlined />} onClick={() => switchHelp('method')}>
{t('helps.label.method')}
</Button>
{showMethodHelp && <Alert className={Styles.content2} type="info" message={t('helps.content.method')}/>}
<Button size="small" className={Styles.label2} type="link" icon={showLossHelp ? <UpOutlined /> : <DownOutlined />} onClick={() => switchHelp('loss')}>
{t('helps.label.loss')}
</Button>
{showLossHelp && <Alert className={Styles.content2} type="info" message={t('helps.content.loss')}/>}
</div>
<div style={{ flex: 1 }}/>
<Button className={Styles.btn} size="large" type="primary" disabled={!executale} onClick={() => createWithdrawData()}>
{t('common::confirm')}
</Button>
</div>);
}
}
}

View File

@ -0,0 +1,79 @@
.main {
background-color: var(--oak-bg-color-container);
height: 100%;
border-radius: 4px;
padding: 22px;
display: flex;
flex-direction: column;
.label {
margin-top: 18px;
font-size: smaller;
font-weight: bold;
}
.input {
margin-top: 12px;
}
.tips {
margin-top: 12px;
.tipLine {
margin-top: 8px;
color: dimgrey;
font-size: x-small;
.value {
color: var(--oak-color-primary);
font-weight: bolder;
margin-left: 2px;
margin-right: 2px;
}
.clickable {
text-decoration: underline;
cursor: pointer;
}
}
}
.helps {
margin-top: 22px;
display: flex;
flex-direction: column;
align-items: flex-start;
.label2 {
color: value(--oak-color-primary)
}
.content2 {
margin-top: 8px;
}
}
.btn {
align-self: flex-end;
}
}
.container {
height: 100%;
display: flex;
flex-direction: column;
align-items: stretch;
background-color: var(--oak-bg-color-container);
.btns {
display: flex;
flex-direction: row;
justify-content: flex-end;
padding: 8px;
.btn {
margin-left: 8px;
border-radius: 0px;
}
}
}

View File

@ -0,0 +1,4 @@
/// <reference types="wechat-miniprogram" />
import { EntityDict } from "../../../oak-app-domain";
declare const _default: (props: import("oak-frontend-base").ReactComponentProps<EntityDict, "withdraw", false, WechatMiniprogram.Component.DataOption>) => React.ReactElement;
export default _default;

View File

@ -0,0 +1,59 @@
import { ToYuan, ThousandCont } from "oak-domain/lib/utils/money";
import dayJs from 'dayjs';
export default OakComponent({
entity: 'withdraw',
isList: false,
projection: {
id: 1,
price: 1,
loss: 1,
dealLoss: 1,
dealPrice: 1,
iState: 1,
withdrawAccount: {
id: 1,
name: 1,
},
reason: 1,
meta: 1,
refund$entity: {
$entity: 'refund',
data: {
id: 1,
price: 1,
loss: 1,
meta: 1,
iState: 1,
reason: 1,
$$updateAt$$: 1,
},
},
$$createAt$$: 1,
},
formData({ data }) {
const refundData = data && (data.refund$entity).map((refund) => {
const { meta, price, loss, iState, $$updateAt$$, reason } = refund;
const { refundLossRatio, refundLossFloor, channel } = meta;
return {
iState,
iStateColor: this.features.style.getColor('refund', 'iState', iState),
lossExp: refundLossRatio ? this.t('refund.lossExp.ratio', { ratio: refundLossRatio }) : (refundLossFloor ? this.t(`refund.lossExp.floor.${refundLossFloor}`) : this.t('refund.lossExp.none')),
channel: this.t(`payChannel::${channel}`),
priceYuan: ThousandCont(ToYuan(price), 2),
lossYuan: ThousandCont(ToYuan(loss), 2),
finalYuan: ThousandCont(ToYuan(price - loss), 2),
successAt: iState === 'refunded' && dayJs($$updateAt$$).format('YYYY-MM-DD HH:mm'),
reason,
};
});
const withdrawExactPrice = ThousandCont(ToYuan(data ? data.price - data.loss : 0), 2);
return {
createAtStr: data && dayJs(data.$$createAt$$).format('YYYY-MM-DD HH:mm'),
withdrawExactPrice,
refundData,
withdrawMethod: refundData && refundData.length ? 'refund' : undefined,
step: data?.iState === 'withdrawing' ? 1 : 2,
failed: data?.iState === 'failed',
};
}
});

View File

@ -0,0 +1 @@
{}

View File

@ -0,0 +1,13 @@
{
"refund": {
"lossExp": {
"ratio": "%{ratio}%",
"floor": {
"1": "无",
"2": "按角取整",
"3": "按元取整"
},
"none": "无"
}
}
}

19
es/components/withdraw/detail/web.d.ts vendored Normal file
View File

@ -0,0 +1,19 @@
import React from 'react';
import { EntityDict } from "../../../oak-app-domain";
import { WebComponentProps } from "oak-frontend-base";
export default function render(props: WebComponentProps<EntityDict, 'withdraw', false, {
createAtStr: string;
withdrawMethod?: 'refund' | 'channel';
refundData?: ({
lossExp: string;
channel: string;
priceYuan: string;
lossYuan: string;
finalYuan: string;
iState: EntityDict['refund']['OpSchema']['iState'];
iStateColor: string;
})[];
withdrawExactPrice: string;
step: 1 | 2;
failed?: boolean;
}>): React.JSX.Element | null;

View File

@ -0,0 +1,13 @@
import React from 'react';
import WithdrawDetail from '../dry/Detail.mobile';
import Styles from './web.module.less';
export default function render(props) {
const { withdrawExactPrice, withdrawMethod, refundData, createAtStr, step, failed } = props.data;
const { t } = props.methods;
if (refundData) {
return (<div className={Styles.container}>
<WithdrawDetail withdrawExactPrice={withdrawExactPrice} withdrawMethod={withdrawMethod} refundData={refundData} t={t} step={step} createAt={createAtStr} failed={failed}/>
</div>);
}
return null;
}

View File

@ -0,0 +1,8 @@
.container {
height: 100%;
display: flex;
flex-direction: column;
align-items: stretch;
background-color: var(--oak-bg-color-container);
}

View File

@ -0,0 +1,19 @@
import React from 'react';
import { EntityDict } from "../../../oak-app-domain";
import { WebComponentProps } from "oak-frontend-base";
export default function render(props: WebComponentProps<EntityDict, 'withdraw', false, {
createAtStr: string;
withdrawMethod?: 'refund' | 'channel';
refundData?: ({
lossExp: string;
channel: string;
priceYuan: string;
lossYuan: string;
finalYuan: string;
iState: EntityDict['refund']['OpSchema']['iState'];
iStateColor: string;
})[];
withdrawExactPrice: string;
step: 1 | 2;
failed?: boolean;
}>): React.JSX.Element | null;

View File

@ -0,0 +1,13 @@
import React from 'react';
import WithdrawDetail from '../dry/Detail.pc';
import Styles from './web.pc.module.less';
export default function render(props) {
const { withdrawExactPrice, withdrawMethod, refundData, createAtStr, step, failed } = props.data;
const { t } = props.methods;
if (refundData) {
return (<div className={Styles.container}>
<WithdrawDetail withdrawExactPrice={withdrawExactPrice} withdrawMethod={withdrawMethod} refundData={refundData} t={t} step={step} createAt={createAtStr} failed={failed}/>
</div>);
}
return null;
}

View File

@ -0,0 +1,8 @@
.container {
height: 100%;
display: flex;
flex-direction: column;
align-items: stretch;
background-color: var(--oak-bg-color-container);
}

14
es/components/withdraw/dry/Detail.d.ts vendored Normal file
View File

@ -0,0 +1,14 @@
/// <reference types="react" />
export default function Detail(props: {
withdrawMethod?: 'refund' | 'channel';
refundData: ({
lossExp: string;
channel: string;
priceYuan: number;
lossYuan: number;
finalYuan: number;
})[];
withdrawExactPrice: string;
t: (k: string, p?: any) => string;
step: 0 | 1 | 2;
}): import("react").JSX.Element;

View File

@ -0,0 +1,71 @@
import { Steps } from 'antd-mobile';
import { FileAddOutlined, FieldTimeOutlined, CheckCircleOutlined } from '@ant-design/icons';
import Styles from './Detail.pc.module.less';
import classNames from 'classnames';
export default function Detail(props) {
const { withdrawExactPrice, withdrawMethod, refundData, t, step } = props;
const H = (<>
<div className={Styles.header}>
<div className={Styles.label}>
{t('header.label')}
</div>
<div className={Styles.value}>
<span className={Styles.symbol}>{t('common::pay.symbol')}</span>
<span>{withdrawExactPrice}</span>
</div>
</div>
<div className={Styles.step}>
<Steps labelPlacement="vertical" current={step} items={[
{
title: <span className={step === 0 ? classNames(Styles.label, Styles.active) : Styles.label}>{t('steps.1.title')}</span>,
icon: <FileAddOutlined className={step === 0 ? Styles.active : undefined}/>
},
{
title: <span className={step === 1 ? classNames(Styles.label, Styles.active) : Styles.label}>{t('steps.2.title')}</span>,
icon: <FieldTimeOutlined className={step === 1 ? Styles.active : undefined}/>,
description: t(`method.v.${withdrawMethod}`)
},
{
title: <span className={step === 2 ? classNames(Styles.label, Styles.active) : Styles.label}>{t('steps.3.title')}</span>,
icon: <CheckCircleOutlined className={step === 2 ? Styles.active : undefined}/>
}
]}/>
</div>
</>);
return (<>
{H}
<div className={Styles.refunds}>
{refundData.map((data) => (<div className={Styles.refundItem}>
<div className={Styles.item}>
<span className={Styles.label}>{t('refund.label.0')}</span>
<span className={Styles.value}>
<span className={Styles.symbol}>{t('common::pay.symbol')}</span>
{data.priceYuan}
</span>
</div>
<div className={Styles.item}>
<span className={Styles.label}>{t('refund.label.1')}</span>
<span className={Styles.value}>
<span className={Styles.symbol}>{t('common::pay.symbol')}</span>
{data.finalYuan}
</span>
</div>
<div className={Styles.item}>
<span className={Styles.label}>{t('refund.label.2')}</span>
<span className={Styles.value}>
<span className={Styles.symbol}>{t('common::pay.symbol')}</span>
{data.lossYuan}
</span>
</div>
<div className={Styles.item}>
<span className={Styles.label}>{t('refund.label.3')}</span>
<span className={Styles.value}>{data.lossExp}</span>
</div>
<div className={Styles.item}>
<span className={Styles.label}>{t('refund.label.4')}</span>
<span className={Styles.value}>{data.channel}</span>
</div>
</div>))}
</div>
</>);
}

View File

@ -0,0 +1,21 @@
/// <reference types="react" />
import { EntityDict } from '../../../oak-app-domain';
export default function Detail(props: {
createAt?: string;
withdrawMethod?: 'refund' | 'channel';
refundData: ({
lossExp: string;
channel: string;
priceYuan: string;
lossYuan: string;
finalYuan: string;
iState?: EntityDict['refund']['OpSchema']['iState'];
iStateColor?: string;
successAt?: string;
reason?: string;
})[];
withdrawExactPrice: string;
t: (k: string, p?: any) => string;
step: 0 | 1 | 2;
failed?: boolean;
}): import("react").JSX.Element;

View File

@ -0,0 +1,73 @@
import { Tag, Steps } from 'antd-mobile';
import Styles from './Detail.module.less';
import classNames from 'classnames';
export default function Detail(props) {
const { withdrawExactPrice, withdrawMethod, refundData, t, step, createAt, failed } = props;
return (<>
<div className={Styles.header}>
<div className={Styles.label}>
{t('header.label')}
</div>
<div className={Styles.value}>
<span className={Styles.symbol}>{t('common::pay.symbol')}</span>
<span>{withdrawExactPrice}</span>
</div>
</div>
<div className={Styles.scroll}>
<div className={Styles.step}>
<Steps current={step}>
<Steps.Step title={<span className={step >= 0 ? classNames(Styles.label, Styles.active) : Styles.label}>{t('steps.1.title')}</span>} description={<span className={step >= 0 ? classNames(Styles.label, Styles.active) : Styles.label}>{createAt}</span>} key="1"/>
<Steps.Step title={<span className={step >= 1 ? classNames(Styles.label, Styles.active) : Styles.label}>{t('steps.2.title')}</span>} description={<span className={step >= 1 ? classNames(Styles.label, Styles.active) : Styles.label}>{t(`method.v.${withdrawMethod}`)}</span>} key="2"/>
<Steps.Step title={<span className={step >= 2 ? classNames(Styles.label, failed ? Styles.failed : Styles.success) : Styles.label}>{failed ? t('steps.3.failed') : t('steps.3.title')}</span>} key="3"/>
</Steps>
</div>
{refundData.map((data, idx) => (<div className={Styles.refundItem} key={idx}>
{data.iState && <div className={Styles.item}>
<span className={Styles.label}>{t('refund:attr.iState')}</span>
<span className={Styles.value}>
<Tag color={data.iStateColor}>
{t(`refund:v.iState.${data.iState}`)}
</Tag>
</span>
</div>}
<div className={Styles.item}>
<span className={Styles.label}>{t('refund.label.0')}</span>
<span className={Styles.value}>
<span className={Styles.symbol}>{t('common::pay.symbol')}</span>
{data.priceYuan}
</span>
</div>
<div className={Styles.item}>
<span className={Styles.label}>{t('refund.label.1')}</span>
<span className={Styles.value}>
<span className={Styles.symbol}>{t('common::pay.symbol')}</span>
{data.finalYuan}
</span>
</div>
<div className={Styles.item}>
<span className={Styles.label}>{t('refund.label.2')}</span>
<span className={Styles.value}>
<span className={Styles.symbol}>{t('common::pay.symbol')}</span>
{data.lossYuan}
</span>
</div>
<div className={Styles.item}>
<span className={Styles.label}>{t('refund.label.3')}</span>
<span className={Styles.value}>{data.lossExp}</span>
</div>
<div className={Styles.item}>
<span className={Styles.label}>{t('refund.label.4')}</span>
<span className={Styles.value}>{data.channel}</span>
</div>
{!!data.successAt && <div className={Styles.item}>
<span className={Styles.label}>{t('refund.label.successAt')}</span>
<span className={Styles.value}>{data.successAt}</span>
</div>}
{!!data.reason && <div className={Styles.item}>
<span className={Styles.label}>{t('refund.label.reason')}</span>
<span className={Styles.value}>{data.reason}</span>
</div>}
</div>))}
</div>
</>);
}

View File

@ -0,0 +1,79 @@
.header {
background-color: var(--oak-color-primary);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 120px;
.label {
align-self: flex-start;
margin-left: 36px;
font-size: small;
color: white;
margin-bottom: 8px;
}
.value {
font-size: x-large;
font-weight: bolder;
color: white;
.symbol {
margin-right: 6px;
}
}
}
.scroll {
flex: 1;
overflow-y: scroll;
}
.step {
margin-top: 25px;
margin-bottom: 15px;
.label {
font-size: small;
}
.active {
color: var(--oak-color-primary);
}
.success {
color: green;
}
.failed {
color: red;
}
}
.refundItem {
margin: 12px;
padding: 8px;
font-size: small;
border-top: solid 0.1px silver;
.item {
margin-top: 4px;
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
.label {
color: var(--oak-text-color-secondary);
}
.value {
color: var(--oak-text-color-primary);
.symbol {
margin-right: 4px;
}
}
}
}

View File

@ -0,0 +1,21 @@
/// <reference types="react" />
import { EntityDict } from '../../../oak-app-domain';
export default function Detail(props: {
createAt?: string;
withdrawMethod?: 'refund' | 'channel';
refundData: ({
lossExp: string;
channel: string;
priceYuan: string;
lossYuan: string;
finalYuan: string;
iState?: EntityDict['refund']['OpSchema']['iState'];
iStateColor?: string;
successAt?: string;
reason?: string;
})[];
withdrawExactPrice: string;
t: (k: string, p?: any) => string;
step: 0 | 1 | 2;
failed?: boolean;
}): import("react").JSX.Element;

View File

@ -0,0 +1,89 @@
import { Steps, Tag } from 'antd';
import { CloseCircleOutlined, FileAddOutlined, FieldTimeOutlined, CheckCircleOutlined } from '@ant-design/icons';
import Styles from './Detail.pc.module.less';
export default function Detail(props) {
const { withdrawExactPrice, withdrawMethod, refundData, t, step, createAt, failed } = props;
const H = (<>
<div className={Styles.header}>
<div className={Styles.label}>
{t('header.label')}
</div>
<div className={Styles.value}>
<span className={Styles.symbol}>{t('common::pay.symbol')}</span>
<span>{withdrawExactPrice}</span>
</div>
</div>
<div className={Styles.step}>
<Steps labelPlacement="vertical" current={step} items={[
{
title: t('steps.1.title'),
icon: <FileAddOutlined />,
description: createAt,
},
{
title: t('steps.2.title'),
icon: <FieldTimeOutlined />,
description: t(`method.v.${withdrawMethod}`)
},
{
title: failed ? <span style={{ color: 'red' }}>{t('steps.3.failed')}</span> : <span style={{ color: 'green' }}>{t('steps.3.title')}</span>,
icon: failed ? <CloseCircleOutlined style={{ color: 'red' }}/> : <CheckCircleOutlined style={{ color: 'green' }}/>
}
]}/>
</div>
</>);
return (<>
{H}
<div className={Styles.refunds}>
{refundData.map((data, idx) => (<div className={Styles.refundItem} key={idx}>
{data.iState && <div className={Styles.item}>
<span className={Styles.label}>{t('refund:attr.iState')}</span>
<span className={Styles.value}>
<Tag color={data.iStateColor} style={{
marginInlineEnd: 0,
}}>
{t(`refund:v.iState.${data.iState}`)}
</Tag>
</span>
</div>}
<div className={Styles.item}>
<span className={Styles.label}>{t('refund.label.0')}</span>
<span className={Styles.value}>
<span className={Styles.symbol}>{t('common::pay.symbol')}</span>
{data.priceYuan}
</span>
</div>
<div className={Styles.item}>
<span className={Styles.label}>{t('refund.label.1')}</span>
<span className={Styles.value}>
<span className={Styles.symbol}>{t('common::pay.symbol')}</span>
{data.finalYuan}
</span>
</div>
<div className={Styles.item}>
<span className={Styles.label}>{t('refund.label.2')}</span>
<span className={Styles.value}>
<span className={Styles.symbol}>{t('common::pay.symbol')}</span>
{data.lossYuan}
</span>
</div>
<div className={Styles.item}>
<span className={Styles.label}>{t('refund.label.3')}</span>
<span className={Styles.value}>{data.lossExp}</span>
</div>
<div className={Styles.item}>
<span className={Styles.label}>{t('refund.label.4')}</span>
<span className={Styles.value}>{data.channel}</span>
</div>
{!!data.successAt && <div className={Styles.item}>
<span className={Styles.label}>{t('refund.label.successAt')}</span>
<span className={Styles.value}>{data.successAt}</span>
</div>}
{!!data.reason && <div className={Styles.item}>
<span className={Styles.label}>{t('refund.label.reason')}</span>
<span className={Styles.value}>{data.reason}</span>
</div>}
</div>))}
</div>
</>);
}

View File

@ -0,0 +1,80 @@
.header {
background-color: var(--oak-color-primary);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 120px;
.label {
align-self: flex-start;
margin-left: 36px;
font-size: small;
color: white;
margin-bottom: 8px;
}
.value {
font-size: x-large;
font-weight: bolder;
color: white;
.symbol {
margin-right: 6px;
}
}
}
.step {
margin-top: 25px;
margin-bottom: 15px;
.label {
font-size: small;
}
.active {
// color: var(--oak-color-primary);
}
:global {
.ant-steps-item-content {
margin-top: 0px !important;
}
}
}
.refunds {
display: flex;
flex-direction: row;
flex-wrap: wrap;
}
.refundItem {
width: 188px;
margin: 12px;
padding: 8px;
font-size: small;
border: solid 0.1px var(--oak-color-info);
border-radius: 4px;
.item {
margin-top: 4px;
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
.label {
color: var(--oak-text-color-secondary);
}
.value {
color: var(--oak-text-color-primary);
.symbol {
margin-right: 4px;
}
}
}
}

View File

@ -0,0 +1,35 @@
{
"header": {
"label": "预计到帐金额"
},
"steps": {
"1": {
"title": "发起提现申请"
},
"2": {
"title": "处理中"
},
"3": {
"title": "到帐成功",
"failed": "提现失败"
}
},
"method": {
"label": "提现方法",
"v": {
"refund": "原路退款",
"manual": "人工划拨"
}
},
"refund": {
"label": {
"0": "申请金额",
"1": "到帐金额",
"2": "手续费",
"3": "手续费率",
"4": "到帐通道",
"successAt": "到帐时间",
"reason": "异常原因"
}
}
}

View File

@ -37,6 +37,17 @@ const attrUpdateMatrix = {
forbidRefundAt: {
actions: ['succeedPaying'],
}
},
refund: {
externalId: {
actions: ['update'],
filter: {
iState: 'refunding',
},
},
reason: {
actions: ['failRefunding', 'makeAbnormal'],
}
}
};
export default attrUpdateMatrix;

View File

@ -244,6 +244,114 @@ const i18ns = [
}
}
},
{
id: "6a1df36d072d367121b49dfc665b4100",
namespace: "oak-pay-business-c-withdraw-create",
language: "zh-CN",
module: "oak-pay-business",
position: "src/components/withdraw/create",
data: {
"label": "提现金额",
"placeholder": "请输入提现金额",
"tips": {
"1-1": "当前帐户可用余额",
"1-2": "元,其中:",
"2-1": "系统自动退款额度",
"2-2": "元",
"3-1": "人工划款额度",
"3-2": "元",
"fill": "全部提现"
},
"helps": {
"label": {
"method": "提现途径说明",
"loss": "提现手续费说明"
},
"content": {
"method": "提现时,帐户中可以退款的金额部分优现从充值的途径返回。若充值途径已经不能退款,则由人工划款到用户指定的提现账户中。一次提现不能同时使用两种提现方法,需要先将自动退款额度退完,剩余部分再申请人工划款",
"loss": "因支付途径损耗,提现过程可能存在一定的手续费,其中,自动退款的手续费额度由相应的充值途径决定,人工划款的手续费额度由相应的提现途径决定。当您申请退款确认后,可以看到相应的手续费额度"
}
},
"error": {
"overflow": "提现额度不能超过帐户可用额度",
"overflowRefundAmount": "请先提现自动退款部分的额度",
"overflowManualAmount": "提现额度不能超过人工划款的额度"
},
"refund": {
"lossExp": {
"ratio": "%{ratio}%",
"floor": {
"1": "无",
"2": "按角取整",
"3": "按元取整"
},
"none": "无"
}
}
}
},
{
id: "a80b0aea4e216e86ec8d07cd59750d1b",
namespace: "oak-pay-business-c-withdraw-detail",
language: "zh-CN",
module: "oak-pay-business",
position: "src/components/withdraw/detail",
data: {
"refund": {
"lossExp": {
"ratio": "%{ratio}%",
"floor": {
"1": "无",
"2": "按角取整",
"3": "按元取整"
},
"none": "无"
}
}
}
},
{
id: "999905578f10b092c9c9c6666fbffe1f",
namespace: "oak-pay-business-c-withdraw-dry",
language: "zh-CN",
module: "oak-pay-business",
position: "src/components/withdraw/dry",
data: {
"header": {
"label": "预计到帐金额"
},
"steps": {
"1": {
"title": "发起提现申请"
},
"2": {
"title": "处理中"
},
"3": {
"title": "到帐成功",
"failed": "提现失败"
}
},
"method": {
"label": "提现方法",
"v": {
"refund": "原路退款",
"manual": "人工划拨"
}
},
"refund": {
"label": {
"0": "申请金额",
"1": "到帐金额",
"2": "手续费",
"3": "手续费率",
"4": "到帐通道",
"successAt": "到帐时间",
"reason": "异常原因"
}
}
}
},
{
id: "37b438f926095fc0c5de592f6d23e912",
namespace: "oak-pay-business-c-withdrawChannel-list",

View File

@ -30,7 +30,7 @@ export interface Schema extends EntityShape {
phantom3?: Int<4>;
phantom4?: Int<8>;
}
type IAction = 'startPaying' | 'succeedPaying' | 'close' | 'startRefunding' | 'refundAll' | 'refundPartially';
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';

View File

@ -7,6 +7,7 @@ export const IActionDef = {
startRefunding: [['paid', 'partiallyRefunded', 'refunding'], 'refunding'],
refundAll: [['paid', 'refunding', 'partiallyRefunded'], 'refunded'],
refundPartially: [['paid', 'refunding', 'partiallyRefunded'], 'partiallyRefunded'],
stopRefunding: ['refunding', 'paid'],
},
is: 'unpaid',
};
@ -78,7 +79,8 @@ export const entityDesc = {
startRefunding: '开始退款',
refundAll: '完全退款',
refundPartially: '部分退款',
closeRefund: '禁止退款'
closeRefund: '禁止退款',
stopRefunding: '停止退款',
},
v: {
iState: {
@ -102,6 +104,7 @@ export const entityDesc = {
refundAll: '',
refundPartially: '',
closeRefund: '',
stopRefunding: '',
},
color: {
iState: {

View File

@ -12,7 +12,7 @@ export interface Schema extends EntityShape {
externalId?: String<64>;
price: Price;
creator: User;
reason: Text;
reason?: Text;
}
type IState = 'refunding' | 'refunded' | 'failed' | 'abnormal';
type IAction = 'succeedRefunding' | 'failRefunding' | 'makeAbnormal';

View File

@ -10,6 +10,8 @@ export interface Schema extends EntityShape {
account: Account;
price: Price;
loss: Price;
dealPrice: Price;
dealLoss: Price;
withdrawAccount?: WithdrawAccount;
opers: AccountOper[];
creator: User;
@ -17,8 +19,8 @@ export interface Schema extends EntityShape {
meta?: Object;
refunds: Refund[];
}
type IState = 'withdrawing' | 'successful' | 'failed' | 'applying';
type IAction = 'succeed' | 'fail';
type IState = 'withdrawing' | 'successful' | 'partiallySuccessful' | 'failed' | 'applying';
type IAction = 'succeed' | 'fail' | 'succeedPartially';
export declare const IActionDef: ActionDef<IAction, IState>;
type Action = IAction;
export declare const entityDesc: EntityDesc<Schema, Action, '', {

View File

@ -2,7 +2,8 @@
export const IActionDef = {
stm: {
succeed: [['withdrawing', 'applying'], 'successful'],
fail: ['applying', 'failed'],
fail: [['withdrawing', 'applying'], 'failed'],
succeedPartially: [['withdrawing', 'applying'], 'partiallySuccessful']
},
is: 'withdrawing',
};
@ -14,6 +15,8 @@ export const entityDesc = {
account: '帐户',
price: '金额',
loss: "损耗",
dealPrice: '完成金额',
dealLoss: '完成损耗',
withdrawAccount: '提现帐户',
iState: '状态',
opers: '被关联帐户操作',
@ -26,12 +29,14 @@ export const entityDesc = {
iState: {
withdrawing: '提现中',
successful: '成功的',
partiallySuccessful: '部分成功的',
failed: '失败的',
applying: '申请中'
},
},
action: {
succeed: '成功',
succeedPartially: '部分成功',
fail: '失败',
},
},
@ -40,11 +45,13 @@ export const entityDesc = {
icon: {
succeed: '',
fail: '',
succeedPartially: '',
},
color: {
iState: {
withdrawing: '#D2B4DE',
successful: '#2E86C1',
partiallySuccessful: '#A9DFBF',
failed: '#D6DBDF',
applying: '#52BE80',
}

View File

@ -1,4 +1,4 @@
import { String } from 'oak-domain/lib/types/DataType';
import { String, Boolean } from 'oak-domain/lib/types/DataType';
import { EntityShape } from 'oak-domain/lib/types/Entity';
import { EntityDesc } from 'oak-domain/lib/types';
import { Schema as WithdrawChannel } from './WithdrawChannel';
@ -10,5 +10,6 @@ export interface Schema extends EntityShape {
channel: WithdrawChannel;
entity: String<32>;
entityId: String<64>;
isDefault: Boolean;
}
export declare const entityDesc: EntityDesc<Schema>;

View File

@ -10,7 +10,8 @@ export const entityDesc = {
data: 'metadata',
channel: '提现通道',
entity: '关联对象',
entityId: '关联对象Id'
entityId: '关联对象Id',
isDefault: '是否默认'
},
},
},

View File

@ -1,6 +1,6 @@
import { ActionDef } from "oak-domain/lib/types/Action";
import { GenericAction } from "oak-domain/lib/actions/action";
export type IAction = 'startPaying' | 'succeedPaying' | 'close' | 'startRefunding' | 'refundAll' | 'refundPartially' | string;
export type IAction = 'startPaying' | 'succeedPaying' | 'close' | 'startRefunding' | 'refundAll' | 'refundPartially' | 'stopRefunding' | string;
export type IState = 'unpaid' | 'paying' | 'paid' | 'closed' | 'refunding' | 'partiallyRefunded' | 'refunded' | string;
export declare const IActionDef: ActionDef<IAction, IState>;
export type ParticularAction = IAction | 'closeRefund';

View File

@ -6,10 +6,11 @@ export const IActionDef = {
startRefunding: [['paid', 'partiallyRefunded', 'refunding'], 'refunding'],
refundAll: [['paid', 'refunding', 'partiallyRefunded'], 'refunded'],
refundPartially: [['paid', 'refunding', 'partiallyRefunded'], 'partiallyRefunded'],
stopRefunding: ['refunding', 'paid'],
},
is: 'unpaid',
};
export const actions = ["count", "stat", "download", "select", "aggregate", "create", "remove", "update", "startPaying", "succeedPaying", "close", "startRefunding", "refundAll", "refundPartially", "closeRefund"];
export const actions = ["count", "stat", "download", "select", "aggregate", "create", "remove", "update", "startPaying", "succeedPaying", "close", "startRefunding", "refundAll", "refundPartially", "stopRefunding", "closeRefund"];
export const actionDefDict = {
iState: IActionDef
};

View File

@ -7,6 +7,7 @@ export const style = {
refundAll: '',
refundPartially: '',
closeRefund: '',
stopRefunding: '',
},
color: {
iState: {

View File

@ -1 +1 @@
{ "name": "订单", "attr": { "price": "应支付金额", "paid": "已支付金额", "refunded": "已退款金额", "iState": "支付状态", "channel": "支付渠道", "order": "所属订单", "timeoutAt": "过期时间", "forbidRefundAt": "停止退款时间", "refundable": "是否可退款", "account": "充值帐户", "meta": "支付metadata", "externalId": "外部订单Id", "opers": "被关联帐户操作", "application": "关联应用", "creator": "创建者", "phantom1": "索引项一", "phantom2": "索引项二", "phantom3": "索引项三", "phantom4": "索引项四" }, "action": { "startPaying": "开始支付", "succeedPaying": "支付成功", "close": "关闭", "startRefunding": "开始退款", "refundAll": "完全退款", "refundPartially": "部分退款", "closeRefund": "禁止退款" }, "v": { "iState": { "unpaid": "待付款", "paying": "支付中", "paid": "已付款", "closed": "已关闭", "refunding": "退款中", "refunded": "已退款", "partiallyRefunded": "已部分退款" } } }
{ "name": "订单", "attr": { "price": "应支付金额", "paid": "已支付金额", "refunded": "已退款金额", "iState": "支付状态", "channel": "支付渠道", "order": "所属订单", "timeoutAt": "过期时间", "forbidRefundAt": "停止退款时间", "refundable": "是否可退款", "account": "充值帐户", "meta": "支付metadata", "externalId": "外部订单Id", "opers": "被关联帐户操作", "application": "关联应用", "creator": "创建者", "phantom1": "索引项一", "phantom2": "索引项二", "phantom3": "索引项三", "phantom4": "索引项四" }, "action": { "startPaying": "开始支付", "succeedPaying": "支付成功", "close": "关闭", "startRefunding": "开始退款", "refundAll": "完全退款", "refundPartially": "部分退款", "closeRefund": "禁止退款", "stopRefunding": "停止退款" }, "v": { "iState": { "unpaid": "待付款", "paying": "支付中", "paid": "已付款", "closed": "已关闭", "refunding": "退款中", "refunded": "已退款", "partiallyRefunded": "已部分退款" } } }

View File

@ -16,7 +16,7 @@ export type OpSchema = EntityShape & {
externalId?: String<64> | null;
price: Price;
creatorId: ForeignKey<"user">;
reason: Text;
reason?: Text | null;
iState?: IState | null;
};
export type OpAttr = keyof OpSchema;
@ -29,7 +29,7 @@ export type Schema = EntityShape & {
externalId?: String<64> | null;
price: Price;
creatorId: ForeignKey<"user">;
reason: Text;
reason?: Text | null;
iState?: IState | null;
pay: Pay.Schema;
creator: User.Schema;

View File

@ -44,7 +44,6 @@ export const desc = {
ref: "user"
},
reason: {
notNull: true,
type: "text"
},
iState: {

View File

@ -1,7 +1,7 @@
import { ActionDef } from "oak-domain/lib/types/Action";
import { GenericAction } from "oak-domain/lib/actions/action";
export type IState = 'withdrawing' | 'successful' | 'failed' | 'applying' | string;
export type IAction = 'succeed' | 'fail' | string;
export type IState = 'withdrawing' | 'successful' | 'partiallySuccessful' | 'failed' | 'applying' | string;
export type IAction = 'succeed' | 'fail' | 'succeedPartially' | string;
export declare const IActionDef: ActionDef<IAction, IState>;
export type ParticularAction = IAction;
export declare const actions: string[];

View File

@ -1,11 +1,12 @@
export const IActionDef = {
stm: {
succeed: [['withdrawing', 'applying'], 'successful'],
fail: ['applying', 'failed'],
fail: [['withdrawing', 'applying'], 'failed'],
succeedPartially: [['withdrawing', 'applying'], 'partiallySuccessful']
},
is: 'withdrawing',
};
export const actions = ["count", "stat", "download", "select", "aggregate", "create", "remove", "update", "succeed", "fail"];
export const actions = ["count", "stat", "download", "select", "aggregate", "create", "remove", "update", "succeed", "fail", "succeedPartially"];
export const actionDefDict = {
iState: IActionDef
};

View File

@ -15,6 +15,8 @@ export type OpSchema = EntityShape & {
accountId: ForeignKey<"account">;
price: Price;
loss: Price;
dealPrice: Price;
dealLoss: Price;
withdrawAccountId?: ForeignKey<"withdrawAccount"> | null;
creatorId: ForeignKey<"user">;
reason?: Text | null;
@ -26,6 +28,8 @@ export type Schema = EntityShape & {
accountId: ForeignKey<"account">;
price: Price;
loss: Price;
dealPrice: Price;
dealLoss: Price;
withdrawAccountId?: ForeignKey<"withdrawAccount"> | null;
creatorId: ForeignKey<"user">;
reason?: Text | null;
@ -54,6 +58,8 @@ type AttrFilter = {
account: Account.Filter;
price: Q_NumberValue;
loss: Q_NumberValue;
dealPrice: Q_NumberValue;
dealLoss: Q_NumberValue;
withdrawAccountId: Q_StringValue;
withdrawAccount: WithdrawAccount.Filter;
creatorId: Q_StringValue;
@ -78,6 +84,8 @@ export type Projection = {
account?: Account.Projection;
price?: number;
loss?: number;
dealPrice?: number;
dealLoss?: number;
withdrawAccountId?: number;
withdrawAccount?: WithdrawAccount.Projection;
creatorId?: number;
@ -138,6 +146,10 @@ export type SortAttr = {
price: number;
} | {
loss: number;
} | {
dealPrice: number;
} | {
dealLoss: number;
} | {
withdrawAccountId: number;
} | {

View File

@ -14,6 +14,14 @@ export const desc = {
notNull: true,
type: "money"
},
dealPrice: {
notNull: true,
type: "money"
},
dealLoss: {
notNull: true,
type: "money"
},
withdrawAccountId: {
type: "ref",
ref: "withdrawAccount"
@ -31,7 +39,7 @@ export const desc = {
},
iState: {
type: "enum",
enumeration: ["withdrawing", "successful", "failed", "applying"]
enumeration: ["withdrawing", "successful", "partiallySuccessful", "failed", "applying"]
}
},
actionType: "crud",

View File

@ -2,11 +2,13 @@ export const style = {
icon: {
succeed: '',
fail: '',
succeedPartially: '',
},
color: {
iState: {
withdrawing: '#D2B4DE',
successful: '#2E86C1',
partiallySuccessful: '#A9DFBF',
failed: '#D6DBDF',
applying: '#52BE80',
}

View File

@ -1 +1 @@
{ "name": "提现", "attr": { "account": "帐户", "price": "金额", "loss": "损耗", "withdrawAccount": "提现帐户", "iState": "状态", "opers": "被关联帐户操作", "reason": "原因", "meta": "metadata", "creator": "创建者", "refunds": "退款" }, "v": { "iState": { "withdrawing": "提现中", "successful": "成功的", "failed": "失败的", "applying": "申请中" } }, "action": { "succeed": "成功", "fail": "失败" } }
{ "name": "提现", "attr": { "account": "帐户", "price": "金额", "loss": "损耗", "dealPrice": "完成金额", "dealLoss": "完成损耗", "withdrawAccount": "提现帐户", "iState": "状态", "opers": "被关联帐户操作", "reason": "原因", "meta": "metadata", "creator": "创建者", "refunds": "退款" }, "v": { "iState": { "withdrawing": "提现中", "successful": "成功的", "partiallySuccessful": "部分成功的", "failed": "失败的", "applying": "申请中" } }, "action": { "succeed": "成功", "succeedPartially": "部分成功", "fail": "失败" } }

View File

@ -1,9 +1,9 @@
import { ForeignKey } from "oak-domain/lib/types/DataType";
import { Q_DateValue, Q_NumberValue, Q_StringValue, Q_EnumValue, NodeId, MakeFilter, ExprOp, ExpressionKey, SubQueryPredicateMetadata } from "oak-domain/lib/types/Demand";
import { Q_DateValue, Q_BooleanValue, Q_NumberValue, Q_StringValue, Q_EnumValue, NodeId, MakeFilter, ExprOp, ExpressionKey, SubQueryPredicateMetadata } from "oak-domain/lib/types/Demand";
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 { GenericAction } from "oak-domain/lib/actions/action";
import { String } from "oak-domain/lib/types/DataType";
import { String, Boolean } from "oak-domain/lib/types/DataType";
import * as WithdrawChannel from "../WithdrawChannel/Schema";
import * as User from "../User/Schema";
import * as Withdraw from "../Withdraw/Schema";
@ -17,6 +17,7 @@ export type OpSchema = EntityShape & {
channelId: ForeignKey<"withdrawChannel">;
entity: "user" | string;
entityId: String<64>;
isDefault: Boolean;
};
export type OpAttr = keyof OpSchema;
export type Schema = EntityShape & {
@ -27,6 +28,7 @@ export type Schema = EntityShape & {
channelId: ForeignKey<"withdrawChannel">;
entity: "user" | string;
entityId: String<64>;
isDefault: Boolean;
channel: WithdrawChannel.Schema;
user?: User.Schema;
withdraw$withdrawAccount?: Array<Withdraw.Schema>;
@ -51,6 +53,7 @@ type AttrFilter = {
channel: WithdrawChannel.Filter;
entity: Q_EnumValue<"user" | string>;
entityId: Q_StringValue;
isDefault: Q_BooleanValue;
user: User.Filter;
withdraw$withdrawAccount: Withdraw.Filter & SubQueryPredicateMetadata;
modiEntity$entity: ModiEntity.Filter & SubQueryPredicateMetadata;
@ -72,6 +75,7 @@ export type Projection = {
channel?: WithdrawChannel.Projection;
entity?: number;
entityId?: number;
isDefault?: number;
user?: User.Projection;
withdraw$withdrawAccount?: Withdraw.Selection & {
$entity: "withdraw";
@ -123,6 +127,8 @@ export type SortAttr = {
entity: number;
} | {
entityId: number;
} | {
isDefault: number;
} | {
user: User.SortAttr;
} | {

View File

@ -44,6 +44,10 @@ export const desc = {
params: {
length: 64
}
},
isDefault: {
notNull: true,
type: "boolean"
}
},
actionType: "crud",

View File

@ -1 +1 @@
{ "name": "提现帐号", "attr": { "org": "机构", "name": "姓名", "code": "帐号", "data": "metadata", "channel": "提现通道", "entity": "关联对象", "entityId": "关联对象Id" } }
{ "name": "提现帐号", "attr": { "org": "机构", "name": "姓名", "code": "帐号", "data": "metadata", "channel": "提现通道", "entity": "关联对象", "entityId": "关联对象Id", "isDefault": "是否默认" } }

View File

@ -226,7 +226,7 @@ const triggers = [
}
else {
assert(accountId);
// 如果是支付成功,则增加帐户余额,其它暂时不支持(提现还未设计)
// 如果是支付成功,则增加帐户余额
if (action === 'succeedPaying') {
const payPrice = pay.price;
await context.operate('accountOper', {
@ -306,8 +306,7 @@ const triggers = [
when: 'before',
fn: async ({ operation }, context, option) => {
const { filter, action, id } = operation;
assert(typeof filter.id === 'string');
const [pay] = await context.select('pay', {
const pays = await context.select('pay', {
data: {
id: 1,
orderId: 1,
@ -317,25 +316,30 @@ const triggers = [
},
filter,
}, { dontCollect: true });
const { orderId, accountId, price, refunded } = pay;
assert(!orderId);
if (price - refunded > 0) {
// 减少account上可以refund的部分
await context.operate('accountOper', {
id: await generateNewIdAsync(),
action: 'create',
data: {
let count = 0;
for (const pay of pays) {
const { orderId, accountId, price, refunded } = pay;
assert(!orderId);
if (price - refunded > 0) {
// 减少account上可以refund的部分
await context.operate('accountOper', {
id: await generateNewIdAsync(),
type: 'cutoffRefundable',
availPlus: 0,
totalPlus: 0,
refundablePlus: refunded - price,
accountId: accountId,
},
}, {});
return 1;
action: 'create',
data: {
id: await generateNewIdAsync(),
type: 'cutoffRefundable',
availPlus: 0,
totalPlus: 0,
refundablePlus: refunded - price,
accountId: accountId,
entity: 'pay',
entityId: pay.id,
},
}, {});
count++;
}
}
return 0;
return count;
},
},
];

View File

@ -1,6 +1,72 @@
import { generateNewId, generateNewIdAsync } from 'oak-domain/lib/utils/uuid';
import { getPayClazz } from '../utils/payClazz';
import assert from 'assert';
/**
* 当refund完成或失败时如果关联有提现去更新提现的状态
* @param context
* @param refunds
*/
async function changeWithdrawStateByRefunds(context, refunds) {
const withdraws = await context.select('withdraw', {
data: {
id: 1,
iState: 1,
price: 1,
refund$entity: {
$entity: 'refund',
data: {
id: 1,
price: 1,
iState: 1,
loss: 1,
},
filter: {
iState: 'refunded',
},
},
},
filter: {
id: {
$in: refunds.filter(ele => ele.entity === 'withdraw').map(ele => ele.entityId),
}
}
}, {});
let count = 0;
for (const withdraw of withdraws) {
const { price, iState, refund$entity: relatedRefunds } = withdraw;
assert(iState === 'withdrawing');
let dealPrice = 0;
let dealLoss = 0;
let allRefundsOver = true;
for (const refund2 of relatedRefunds) {
if (refund2.iState === 'refunding') {
allRefundsOver = false;
break;
}
if (refund2.iState === 'refunded') {
dealPrice += refund2.price;
dealLoss += refund2.loss;
}
}
if (!allRefundsOver) {
continue;
}
const action = dealPrice === price ? 'succeed' : 'partiallySucceed';
await context.operate('withdraw', {
id: await generateNewIdAsync(),
action,
data: {
dealPrice,
dealLoss,
},
filter: {
id: withdraw.id,
}
}, {});
count++;
}
return count;
}
const triggers = [
{
entity: 'refund',
@ -33,7 +99,19 @@ const triggers = [
const { id, price, pay } = refund;
const { price: payPrice, refunded, channel, applicationId } = pay;
const payClazz = await getPayClazz(applicationId, channel, context);
await payClazz.refund(refund);
const data = await payClazz.refund(refund);
if (data) {
const closeFn = context.openRootMode();
await context.operate('refund', {
id: await generateNewIdAsync(),
data,
action: 'update',
filter: {
id,
}
}, { dontCollect: true });
closeFn();
}
}
},
},
@ -66,7 +144,7 @@ const triggers = [
}, {});
for (const refund of refunds) {
const { id, price, iState, pay } = refund;
assert(iState === 'refunding' && pay.iState === 'refunding');
assert(iState === 'refunded' && pay.iState === 'refunding');
const { price: payPrice, refunded } = pay;
const refunded2 = refunded + price;
assert(refunded2 <= payPrice, '退款金额不应高于pay的总金额');
@ -82,43 +160,51 @@ const triggers = [
}
}, {});
}
const withdraws = await context.select('withdraw', {
return refunds.length + await changeWithdrawStateByRefunds(context, refunds);
}
},
{
entity: 'refund',
action: 'failRefunding',
when: 'after',
name: '退款失败时更新对应的pay状态以及对应的withdraw状态',
fn: async ({ operation }, context) => {
const { filter } = operation;
const refunds = await context.select('refund', {
data: {
id: 1,
iState: 1,
price: 1,
refund$entity: {
$entity: 'refund',
data: {
id: 1,
price: 1,
iState: 1,
},
filter: {
iState: 'refunded',
},
iState: 1,
entity: 1,
entityId: 1,
pay: {
id: 1,
price: 1,
iState: 1,
refunded: 1,
channel: 1,
applicationId: 1,
orderId: 1,
accountId: 1,
},
},
filter: {
id: {
$in: refunds.filter(ele => ele.entity === 'withdraw').map(ele => ele.entityId),
}
}
filter,
}, {});
const successfulWithdraws = withdraws.filter(({ price, iState, refund$entity: relatedRefunds }) => {
assert(iState === 'refunding');
let refundedPrice = 0;
relatedRefunds?.forEach(({ price }) => refundedPrice += price);
return relatedRefunds === price;
});
if (successfulWithdraws.length > 0) {
await context.operate('withdraw', {
for (const refund of refunds) {
const { id, iState, pay } = refund;
assert(iState === 'failed' && pay.iState === 'refunding');
const { refunded } = pay;
const action = refunded === 0 ? 'stopRefunding' : 'refundPartially';
await context.operate('pay', {
id: await generateNewIdAsync(),
action: 'succeed',
action,
data: {},
filter: {
id: pay.id,
}
}, {});
}
return refunds.length + successfulWithdraws.length;
return refunds.length + await changeWithdrawStateByRefunds(context, refunds);
}
},
{

View File

@ -12,19 +12,43 @@ const triggers = [
assert(!(data instanceof Array));
const { withdrawAccountId, accountId, price } = data;
if (!withdrawAccountId) {
// 没有指定渠道则尝试走退款
const refundData = await getAccountPayRefunds(context, accountId, price);
data.refund$entity = refundData.map((data) => ({
id: generateNewId(),
action: 'create',
data,
}));
// 没有指定渠道则走退款前台通过getAccountPayRefunds的aspect去获得refund数据并挂载
if (!data.refund$entity) {
const refundData = await getAccountPayRefunds(context, accountId, price);
data.refund$entity = refundData.map((data) => ({
id: generateNewId(),
action: 'create',
data,
}));
let loss = 0;
data.refund$entity.forEach((ele) => {
const { data } = ele;
loss += data.loss || 0;
});
data.loss = loss;
}
data.iState = 'withdrawing';
}
else {
// 否则走渠道提现,暂时假设为人工操作
const [withdrawAccount] = await context.select('withdrawAccount', {
data: {
id: 1,
channel: {
id: 1,
lossRatio: 1,
},
},
filter: {
id: withdrawAccountId,
}
}, { dontCollect: true });
const { channel } = withdrawAccount;
const { lossRatio } = channel;
data.loss = lossRatio ? Math.ceil(data.price * lossRatio / 100) : 0;
data.iState = 'applying';
}
data.dealPrice = data.dealLoss = 0;
data.creatorId = context.getCurrentUserId();
data.accountOper$entity = [
{
@ -56,13 +80,14 @@ const triggers = [
accountId: 1,
iState: 1,
price: 1,
dealPrice: 1,
withdrawAccountId: 1,
},
filter,
}, {});
for (const withdraw of withdraws) {
const { accountId, withdrawAccountId, price } = withdraw;
assert(withdrawAccountId); // 只有走渠道的提现才可以失败
const { accountId, price, dealPrice } = withdraw;
assert(dealPrice === 0);
await context.operate('accountOper', {
id: await generateNewIdAsync(),
action: 'create',
@ -77,6 +102,42 @@ const triggers = [
}
return withdraws.length;
},
},
{
name: '当withdraw部分成功时将差价部分还回到帐户中',
entity: 'withdraw',
action: 'succeedPartially',
when: 'after',
fn: async ({ operation }, context) => {
const { filter, data } = operation;
const withdraws = await context.select('withdraw', {
data: {
id: 1,
accountId: 1,
iState: 1,
price: 1,
dealPrice: 1,
withdrawAccountId: 1,
},
filter,
}, {});
for (const withdraw of withdraws) {
const { accountId, price, dealPrice } = withdraw;
assert(price > dealPrice && dealPrice > 0);
await context.operate('accountOper', {
id: await generateNewIdAsync(),
action: 'create',
data: {
id: await generateNewIdAsync(),
accountId,
type: 'withdrawBack',
totalPlus: price - dealPrice,
availPlus: price - dealPrice,
},
}, {});
}
return withdraws.length;
}
}
];
export default triggers;

View File

@ -104,7 +104,10 @@ export async function getAccountPayRefunds(context, accountId, totalPrice) {
data: {
id: 1,
price: 1,
paid: 1,
refundable: 1,
refunded: 1,
channel: 1,
},
filter: {
refundable: true,
@ -165,6 +168,11 @@ export async function getAccountPayRefunds(context, accountId, totalPrice) {
creatorId: context.getCurrentUserId(),
payId: pay.id,
iState: 'refunding',
meta: {
refundLossRatio,
refundLossFloor,
channel,
}
});
price2 += refundPrice;
if (totalPrice && price2 === totalPrice) {

View File

@ -10,16 +10,18 @@ export default class WechatPay {
this.channel = channel;
}
async refund(refund) {
return;
return {
externalId: Math.random().toString(),
};
}
async closeRefund(refund) {
return;
}
async getRefundState(refund) {
const r = Math.random();
if (r < 0.3) {
/* if (r < 0.3) {
return ['refunding', {}];
}
} */
return ['refunded', {}];
}
decodePayNotification(params, body) {
@ -74,7 +76,10 @@ export default class WechatPay {
else if (r > 0.95) {
return ['closed', {}];
}
return ['paid', {}];
return ['paid', {
forbidRefundAt: Date.now() + 24 * 3600 * 1000,
refundable: true,
}];
}
async close(pay) {
}

View File

@ -1,7 +1,9 @@
import orderWatchers from './order';
import payWatchers from './pay';
import refundWatchers from './refund';
const watchers = [
...orderWatchers,
...payWatchers,
...refundWatchers,
];
export default watchers;

View File

@ -10,7 +10,7 @@ const watchers = [
filter: async () => {
const now = Date.now();
return {
iState: 'refund',
iState: 'refunding',
$$updateAt$$: {
$lte: now - QUERY_PAYING_STATE_GAP,
},
@ -40,7 +40,6 @@ const watchers = [
let action = 'succeedRefunding';
switch (iState) {
case 'refunded': {
// action = 'close';
break;
}
case 'failed': {

View File

@ -1,4 +1,5 @@
import { BRC } from '../types/RuntimeCxt';
export declare function getAccountPayRefunds(params: {
accountId: string;
price: number;
}, context: BRC): Promise<Omit<import("../oak-app-domain/Refund/Schema").CreateOperationData, "entity" | "entityId">[]>;

View File

@ -3,6 +3,6 @@ Object.defineProperty(exports, "__esModule", { value: true });
exports.getAccountPayRefunds = void 0;
const pay_1 = require("../utils/pay");
async function getAccountPayRefunds(params, context) {
return (0, pay_1.getAccountPayRefunds)(context, params.accountId);
return (0, pay_1.getAccountPayRefunds)(context, params.accountId, params.price);
}
exports.getAccountPayRefunds = getAccountPayRefunds;

View File

@ -39,6 +39,17 @@ const attrUpdateMatrix = {
forbidRefundAt: {
actions: ['succeedPaying'],
}
},
refund: {
externalId: {
actions: ['update'],
filter: {
iState: 'refunding',
},
},
reason: {
actions: ['failRefunding', 'makeAbnormal'],
}
}
};
exports.default = attrUpdateMatrix;

View File

@ -246,6 +246,114 @@ const i18ns = [
}
}
},
{
id: "6a1df36d072d367121b49dfc665b4100",
namespace: "oak-pay-business-c-withdraw-create",
language: "zh-CN",
module: "oak-pay-business",
position: "src/components/withdraw/create",
data: {
"label": "提现金额",
"placeholder": "请输入提现金额",
"tips": {
"1-1": "当前帐户可用余额",
"1-2": "元,其中:",
"2-1": "系统自动退款额度",
"2-2": "元",
"3-1": "人工划款额度",
"3-2": "元",
"fill": "全部提现"
},
"helps": {
"label": {
"method": "提现途径说明",
"loss": "提现手续费说明"
},
"content": {
"method": "提现时,帐户中可以退款的金额部分优现从充值的途径返回。若充值途径已经不能退款,则由人工划款到用户指定的提现账户中。一次提现不能同时使用两种提现方法,需要先将自动退款额度退完,剩余部分再申请人工划款",
"loss": "因支付途径损耗,提现过程可能存在一定的手续费,其中,自动退款的手续费额度由相应的充值途径决定,人工划款的手续费额度由相应的提现途径决定。当您申请退款确认后,可以看到相应的手续费额度"
}
},
"error": {
"overflow": "提现额度不能超过帐户可用额度",
"overflowRefundAmount": "请先提现自动退款部分的额度",
"overflowManualAmount": "提现额度不能超过人工划款的额度"
},
"refund": {
"lossExp": {
"ratio": "%{ratio}%",
"floor": {
"1": "无",
"2": "按角取整",
"3": "按元取整"
},
"none": "无"
}
}
}
},
{
id: "a80b0aea4e216e86ec8d07cd59750d1b",
namespace: "oak-pay-business-c-withdraw-detail",
language: "zh-CN",
module: "oak-pay-business",
position: "src/components/withdraw/detail",
data: {
"refund": {
"lossExp": {
"ratio": "%{ratio}%",
"floor": {
"1": "无",
"2": "按角取整",
"3": "按元取整"
},
"none": "无"
}
}
}
},
{
id: "999905578f10b092c9c9c6666fbffe1f",
namespace: "oak-pay-business-c-withdraw-dry",
language: "zh-CN",
module: "oak-pay-business",
position: "src/components/withdraw/dry",
data: {
"header": {
"label": "预计到帐金额"
},
"steps": {
"1": {
"title": "发起提现申请"
},
"2": {
"title": "处理中"
},
"3": {
"title": "到帐成功",
"failed": "提现失败"
}
},
"method": {
"label": "提现方法",
"v": {
"refund": "原路退款",
"manual": "人工划拨"
}
},
"refund": {
"label": {
"0": "申请金额",
"1": "到帐金额",
"2": "手续费",
"3": "手续费率",
"4": "到帐通道",
"successAt": "到帐时间",
"reason": "异常原因"
}
}
}
},
{
id: "37b438f926095fc0c5de592f6d23e912",
namespace: "oak-pay-business-c-withdrawChannel-list",

View File

@ -30,7 +30,7 @@ export interface Schema extends EntityShape {
phantom3?: Int<4>;
phantom4?: Int<8>;
}
type IAction = 'startPaying' | 'succeedPaying' | 'close' | 'startRefunding' | 'refundAll' | 'refundPartially';
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';

View File

@ -10,6 +10,7 @@ exports.IActionDef = {
startRefunding: [['paid', 'partiallyRefunded', 'refunding'], 'refunding'],
refundAll: [['paid', 'refunding', 'partiallyRefunded'], 'refunded'],
refundPartially: [['paid', 'refunding', 'partiallyRefunded'], 'partiallyRefunded'],
stopRefunding: ['refunding', 'paid'],
},
is: 'unpaid',
};
@ -81,7 +82,8 @@ exports.entityDesc = {
startRefunding: '开始退款',
refundAll: '完全退款',
refundPartially: '部分退款',
closeRefund: '禁止退款'
closeRefund: '禁止退款',
stopRefunding: '停止退款',
},
v: {
iState: {
@ -105,6 +107,7 @@ exports.entityDesc = {
refundAll: '',
refundPartially: '',
closeRefund: '',
stopRefunding: '',
},
color: {
iState: {

View File

@ -12,7 +12,7 @@ export interface Schema extends EntityShape {
externalId?: String<64>;
price: Price;
creator: User;
reason: Text;
reason?: Text;
}
type IState = 'refunding' | 'refunded' | 'failed' | 'abnormal';
type IAction = 'succeedRefunding' | 'failRefunding' | 'makeAbnormal';

View File

@ -10,6 +10,8 @@ export interface Schema extends EntityShape {
account: Account;
price: Price;
loss: Price;
dealPrice: Price;
dealLoss: Price;
withdrawAccount?: WithdrawAccount;
opers: AccountOper[];
creator: User;
@ -17,8 +19,8 @@ export interface Schema extends EntityShape {
meta?: Object;
refunds: Refund[];
}
type IState = 'withdrawing' | 'successful' | 'failed' | 'applying';
type IAction = 'succeed' | 'fail';
type IState = 'withdrawing' | 'successful' | 'partiallySuccessful' | 'failed' | 'applying';
type IAction = 'succeed' | 'fail' | 'succeedPartially';
export declare const IActionDef: ActionDef<IAction, IState>;
type Action = IAction;
export declare const entityDesc: EntityDesc<Schema, Action, '', {

View File

@ -5,7 +5,8 @@ exports.entityDesc = exports.IActionDef = void 0;
exports.IActionDef = {
stm: {
succeed: [['withdrawing', 'applying'], 'successful'],
fail: ['applying', 'failed'],
fail: [['withdrawing', 'applying'], 'failed'],
succeedPartially: [['withdrawing', 'applying'], 'partiallySuccessful']
},
is: 'withdrawing',
};
@ -17,6 +18,8 @@ exports.entityDesc = {
account: '帐户',
price: '金额',
loss: "损耗",
dealPrice: '完成金额',
dealLoss: '完成损耗',
withdrawAccount: '提现帐户',
iState: '状态',
opers: '被关联帐户操作',
@ -29,12 +32,14 @@ exports.entityDesc = {
iState: {
withdrawing: '提现中',
successful: '成功的',
partiallySuccessful: '部分成功的',
failed: '失败的',
applying: '申请中'
},
},
action: {
succeed: '成功',
succeedPartially: '部分成功',
fail: '失败',
},
},
@ -43,11 +48,13 @@ exports.entityDesc = {
icon: {
succeed: '',
fail: '',
succeedPartially: '',
},
color: {
iState: {
withdrawing: '#D2B4DE',
successful: '#2E86C1',
partiallySuccessful: '#A9DFBF',
failed: '#D6DBDF',
applying: '#52BE80',
}

View File

@ -1,4 +1,4 @@
import { String } from 'oak-domain/lib/types/DataType';
import { String, Boolean } from 'oak-domain/lib/types/DataType';
import { EntityShape } from 'oak-domain/lib/types/Entity';
import { EntityDesc } from 'oak-domain/lib/types';
import { Schema as WithdrawChannel } from './WithdrawChannel';
@ -10,5 +10,6 @@ export interface Schema extends EntityShape {
channel: WithdrawChannel;
entity: String<32>;
entityId: String<64>;
isDefault: Boolean;
}
export declare const entityDesc: EntityDesc<Schema>;

View File

@ -13,7 +13,8 @@ exports.entityDesc = {
data: 'metadata',
channel: '提现通道',
entity: '关联对象',
entityId: '关联对象Id'
entityId: '关联对象Id',
isDefault: '是否默认'
},
},
},

View File

@ -1,6 +1,6 @@
import { ActionDef } from "oak-domain/lib/types/Action";
import { GenericAction } from "oak-domain/lib/actions/action";
export type IAction = 'startPaying' | 'succeedPaying' | 'close' | 'startRefunding' | 'refundAll' | 'refundPartially' | string;
export type IAction = 'startPaying' | 'succeedPaying' | 'close' | 'startRefunding' | 'refundAll' | 'refundPartially' | 'stopRefunding' | string;
export type IState = 'unpaid' | 'paying' | 'paid' | 'closed' | 'refunding' | 'partiallyRefunded' | 'refunded' | string;
export declare const IActionDef: ActionDef<IAction, IState>;
export type ParticularAction = IAction | 'closeRefund';

View File

@ -9,10 +9,11 @@ exports.IActionDef = {
startRefunding: [['paid', 'partiallyRefunded', 'refunding'], 'refunding'],
refundAll: [['paid', 'refunding', 'partiallyRefunded'], 'refunded'],
refundPartially: [['paid', 'refunding', 'partiallyRefunded'], 'partiallyRefunded'],
stopRefunding: ['refunding', 'paid'],
},
is: 'unpaid',
};
exports.actions = ["count", "stat", "download", "select", "aggregate", "create", "remove", "update", "startPaying", "succeedPaying", "close", "startRefunding", "refundAll", "refundPartially", "closeRefund"];
exports.actions = ["count", "stat", "download", "select", "aggregate", "create", "remove", "update", "startPaying", "succeedPaying", "close", "startRefunding", "refundAll", "refundPartially", "stopRefunding", "closeRefund"];
exports.actionDefDict = {
iState: exports.IActionDef
};

View File

@ -10,6 +10,7 @@ exports.style = {
refundAll: '',
refundPartially: '',
closeRefund: '',
stopRefunding: '',
},
color: {
iState: {

View File

@ -1 +1 @@
{ "name": "订单", "attr": { "price": "应支付金额", "paid": "已支付金额", "refunded": "已退款金额", "iState": "支付状态", "channel": "支付渠道", "order": "所属订单", "timeoutAt": "过期时间", "forbidRefundAt": "停止退款时间", "refundable": "是否可退款", "account": "充值帐户", "meta": "支付metadata", "externalId": "外部订单Id", "opers": "被关联帐户操作", "application": "关联应用", "creator": "创建者", "phantom1": "索引项一", "phantom2": "索引项二", "phantom3": "索引项三", "phantom4": "索引项四" }, "action": { "startPaying": "开始支付", "succeedPaying": "支付成功", "close": "关闭", "startRefunding": "开始退款", "refundAll": "完全退款", "refundPartially": "部分退款", "closeRefund": "禁止退款" }, "v": { "iState": { "unpaid": "待付款", "paying": "支付中", "paid": "已付款", "closed": "已关闭", "refunding": "退款中", "refunded": "已退款", "partiallyRefunded": "已部分退款" } } }
{ "name": "订单", "attr": { "price": "应支付金额", "paid": "已支付金额", "refunded": "已退款金额", "iState": "支付状态", "channel": "支付渠道", "order": "所属订单", "timeoutAt": "过期时间", "forbidRefundAt": "停止退款时间", "refundable": "是否可退款", "account": "充值帐户", "meta": "支付metadata", "externalId": "外部订单Id", "opers": "被关联帐户操作", "application": "关联应用", "creator": "创建者", "phantom1": "索引项一", "phantom2": "索引项二", "phantom3": "索引项三", "phantom4": "索引项四" }, "action": { "startPaying": "开始支付", "succeedPaying": "支付成功", "close": "关闭", "startRefunding": "开始退款", "refundAll": "完全退款", "refundPartially": "部分退款", "closeRefund": "禁止退款", "stopRefunding": "停止退款" }, "v": { "iState": { "unpaid": "待付款", "paying": "支付中", "paid": "已付款", "closed": "已关闭", "refunding": "退款中", "refunded": "已退款", "partiallyRefunded": "已部分退款" } } }

View File

@ -16,7 +16,7 @@ export type OpSchema = EntityShape & {
externalId?: String<64> | null;
price: Price;
creatorId: ForeignKey<"user">;
reason: Text;
reason?: Text | null;
iState?: IState | null;
};
export type OpAttr = keyof OpSchema;
@ -29,7 +29,7 @@ export type Schema = EntityShape & {
externalId?: String<64> | null;
price: Price;
creatorId: ForeignKey<"user">;
reason: Text;
reason?: Text | null;
iState?: IState | null;
pay: Pay.Schema;
creator: User.Schema;

View File

@ -47,7 +47,6 @@ exports.desc = {
ref: "user"
},
reason: {
notNull: true,
type: "text"
},
iState: {

View File

@ -1,7 +1,7 @@
import { ActionDef } from "oak-domain/lib/types/Action";
import { GenericAction } from "oak-domain/lib/actions/action";
export type IState = 'withdrawing' | 'successful' | 'failed' | 'applying' | string;
export type IAction = 'succeed' | 'fail' | string;
export type IState = 'withdrawing' | 'successful' | 'partiallySuccessful' | 'failed' | 'applying' | string;
export type IAction = 'succeed' | 'fail' | 'succeedPartially' | string;
export declare const IActionDef: ActionDef<IAction, IState>;
export type ParticularAction = IAction;
export declare const actions: string[];

View File

@ -4,11 +4,12 @@ exports.actionDefDict = exports.actions = exports.IActionDef = void 0;
exports.IActionDef = {
stm: {
succeed: [['withdrawing', 'applying'], 'successful'],
fail: ['applying', 'failed'],
fail: [['withdrawing', 'applying'], 'failed'],
succeedPartially: [['withdrawing', 'applying'], 'partiallySuccessful']
},
is: 'withdrawing',
};
exports.actions = ["count", "stat", "download", "select", "aggregate", "create", "remove", "update", "succeed", "fail"];
exports.actions = ["count", "stat", "download", "select", "aggregate", "create", "remove", "update", "succeed", "fail", "succeedPartially"];
exports.actionDefDict = {
iState: exports.IActionDef
};

View File

@ -15,6 +15,8 @@ export type OpSchema = EntityShape & {
accountId: ForeignKey<"account">;
price: Price;
loss: Price;
dealPrice: Price;
dealLoss: Price;
withdrawAccountId?: ForeignKey<"withdrawAccount"> | null;
creatorId: ForeignKey<"user">;
reason?: Text | null;
@ -26,6 +28,8 @@ export type Schema = EntityShape & {
accountId: ForeignKey<"account">;
price: Price;
loss: Price;
dealPrice: Price;
dealLoss: Price;
withdrawAccountId?: ForeignKey<"withdrawAccount"> | null;
creatorId: ForeignKey<"user">;
reason?: Text | null;
@ -54,6 +58,8 @@ type AttrFilter = {
account: Account.Filter;
price: Q_NumberValue;
loss: Q_NumberValue;
dealPrice: Q_NumberValue;
dealLoss: Q_NumberValue;
withdrawAccountId: Q_StringValue;
withdrawAccount: WithdrawAccount.Filter;
creatorId: Q_StringValue;
@ -78,6 +84,8 @@ export type Projection = {
account?: Account.Projection;
price?: number;
loss?: number;
dealPrice?: number;
dealLoss?: number;
withdrawAccountId?: number;
withdrawAccount?: WithdrawAccount.Projection;
creatorId?: number;
@ -138,6 +146,10 @@ export type SortAttr = {
price: number;
} | {
loss: number;
} | {
dealPrice: number;
} | {
dealLoss: number;
} | {
withdrawAccountId: number;
} | {

View File

@ -17,6 +17,14 @@ exports.desc = {
notNull: true,
type: "money"
},
dealPrice: {
notNull: true,
type: "money"
},
dealLoss: {
notNull: true,
type: "money"
},
withdrawAccountId: {
type: "ref",
ref: "withdrawAccount"
@ -34,7 +42,7 @@ exports.desc = {
},
iState: {
type: "enum",
enumeration: ["withdrawing", "successful", "failed", "applying"]
enumeration: ["withdrawing", "successful", "partiallySuccessful", "failed", "applying"]
}
},
actionType: "crud",

View File

@ -5,11 +5,13 @@ exports.style = {
icon: {
succeed: '',
fail: '',
succeedPartially: '',
},
color: {
iState: {
withdrawing: '#D2B4DE',
successful: '#2E86C1',
partiallySuccessful: '#A9DFBF',
failed: '#D6DBDF',
applying: '#52BE80',
}

View File

@ -1 +1 @@
{ "name": "提现", "attr": { "account": "帐户", "price": "金额", "loss": "损耗", "withdrawAccount": "提现帐户", "iState": "状态", "opers": "被关联帐户操作", "reason": "原因", "meta": "metadata", "creator": "创建者", "refunds": "退款" }, "v": { "iState": { "withdrawing": "提现中", "successful": "成功的", "failed": "失败的", "applying": "申请中" } }, "action": { "succeed": "成功", "fail": "失败" } }
{ "name": "提现", "attr": { "account": "帐户", "price": "金额", "loss": "损耗", "dealPrice": "完成金额", "dealLoss": "完成损耗", "withdrawAccount": "提现帐户", "iState": "状态", "opers": "被关联帐户操作", "reason": "原因", "meta": "metadata", "creator": "创建者", "refunds": "退款" }, "v": { "iState": { "withdrawing": "提现中", "successful": "成功的", "partiallySuccessful": "部分成功的", "failed": "失败的", "applying": "申请中" } }, "action": { "succeed": "成功", "succeedPartially": "部分成功", "fail": "失败" } }

View File

@ -1,9 +1,9 @@
import { ForeignKey } from "oak-domain/lib/types/DataType";
import { Q_DateValue, Q_NumberValue, Q_StringValue, Q_EnumValue, NodeId, MakeFilter, ExprOp, ExpressionKey, SubQueryPredicateMetadata } from "oak-domain/lib/types/Demand";
import { Q_DateValue, Q_BooleanValue, Q_NumberValue, Q_StringValue, Q_EnumValue, NodeId, MakeFilter, ExprOp, ExpressionKey, SubQueryPredicateMetadata } from "oak-domain/lib/types/Demand";
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 { GenericAction } from "oak-domain/lib/actions/action";
import { String } from "oak-domain/lib/types/DataType";
import { String, Boolean } from "oak-domain/lib/types/DataType";
import * as WithdrawChannel from "../WithdrawChannel/Schema";
import * as User from "../User/Schema";
import * as Withdraw from "../Withdraw/Schema";
@ -17,6 +17,7 @@ export type OpSchema = EntityShape & {
channelId: ForeignKey<"withdrawChannel">;
entity: "user" | string;
entityId: String<64>;
isDefault: Boolean;
};
export type OpAttr = keyof OpSchema;
export type Schema = EntityShape & {
@ -27,6 +28,7 @@ export type Schema = EntityShape & {
channelId: ForeignKey<"withdrawChannel">;
entity: "user" | string;
entityId: String<64>;
isDefault: Boolean;
channel: WithdrawChannel.Schema;
user?: User.Schema;
withdraw$withdrawAccount?: Array<Withdraw.Schema>;
@ -51,6 +53,7 @@ type AttrFilter = {
channel: WithdrawChannel.Filter;
entity: Q_EnumValue<"user" | string>;
entityId: Q_StringValue;
isDefault: Q_BooleanValue;
user: User.Filter;
withdraw$withdrawAccount: Withdraw.Filter & SubQueryPredicateMetadata;
modiEntity$entity: ModiEntity.Filter & SubQueryPredicateMetadata;
@ -72,6 +75,7 @@ export type Projection = {
channel?: WithdrawChannel.Projection;
entity?: number;
entityId?: number;
isDefault?: number;
user?: User.Projection;
withdraw$withdrawAccount?: Withdraw.Selection & {
$entity: "withdraw";
@ -123,6 +127,8 @@ export type SortAttr = {
entity: number;
} | {
entityId: number;
} | {
isDefault: number;
} | {
user: User.SortAttr;
} | {

View File

@ -47,6 +47,10 @@ exports.desc = {
params: {
length: 64
}
},
isDefault: {
notNull: true,
type: "boolean"
}
},
actionType: "crud",

View File

@ -1 +1 @@
{ "name": "提现帐号", "attr": { "org": "机构", "name": "姓名", "code": "帐号", "data": "metadata", "channel": "提现通道", "entity": "关联对象", "entityId": "关联对象Id" } }
{ "name": "提现帐号", "attr": { "org": "机构", "name": "姓名", "code": "帐号", "data": "metadata", "channel": "提现通道", "entity": "关联对象", "entityId": "关联对象Id", "isDefault": "是否默认" } }

View File

@ -229,7 +229,7 @@ const triggers = [
}
else {
(0, assert_1.default)(accountId);
// 如果是支付成功,则增加帐户余额,其它暂时不支持(提现还未设计)
// 如果是支付成功,则增加帐户余额
if (action === 'succeedPaying') {
const payPrice = pay.price;
await context.operate('accountOper', {
@ -309,8 +309,7 @@ const triggers = [
when: 'before',
fn: async ({ operation }, context, option) => {
const { filter, action, id } = operation;
(0, assert_1.default)(typeof filter.id === 'string');
const [pay] = await context.select('pay', {
const pays = await context.select('pay', {
data: {
id: 1,
orderId: 1,
@ -320,25 +319,30 @@ const triggers = [
},
filter,
}, { dontCollect: true });
const { orderId, accountId, price, refunded } = pay;
(0, assert_1.default)(!orderId);
if (price - refunded > 0) {
// 减少account上可以refund的部分
await context.operate('accountOper', {
id: await (0, uuid_1.generateNewIdAsync)(),
action: 'create',
data: {
let count = 0;
for (const pay of pays) {
const { orderId, accountId, price, refunded } = pay;
(0, assert_1.default)(!orderId);
if (price - refunded > 0) {
// 减少account上可以refund的部分
await context.operate('accountOper', {
id: await (0, uuid_1.generateNewIdAsync)(),
type: 'cutoffRefundable',
availPlus: 0,
totalPlus: 0,
refundablePlus: refunded - price,
accountId: accountId,
},
}, {});
return 1;
action: 'create',
data: {
id: await (0, uuid_1.generateNewIdAsync)(),
type: 'cutoffRefundable',
availPlus: 0,
totalPlus: 0,
refundablePlus: refunded - price,
accountId: accountId,
entity: 'pay',
entityId: pay.id,
},
}, {});
count++;
}
}
return 0;
return count;
},
},
];

View File

@ -4,6 +4,72 @@ const tslib_1 = require("tslib");
const uuid_1 = require("oak-domain/lib/utils/uuid");
const payClazz_1 = require("../utils/payClazz");
const assert_1 = tslib_1.__importDefault(require("assert"));
/**
* 当refund完成或失败时如果关联有提现去更新提现的状态
* @param context
* @param refunds
*/
async function changeWithdrawStateByRefunds(context, refunds) {
const withdraws = await context.select('withdraw', {
data: {
id: 1,
iState: 1,
price: 1,
refund$entity: {
$entity: 'refund',
data: {
id: 1,
price: 1,
iState: 1,
loss: 1,
},
filter: {
iState: 'refunded',
},
},
},
filter: {
id: {
$in: refunds.filter(ele => ele.entity === 'withdraw').map(ele => ele.entityId),
}
}
}, {});
let count = 0;
for (const withdraw of withdraws) {
const { price, iState, refund$entity: relatedRefunds } = withdraw;
(0, assert_1.default)(iState === 'withdrawing');
let dealPrice = 0;
let dealLoss = 0;
let allRefundsOver = true;
for (const refund2 of relatedRefunds) {
if (refund2.iState === 'refunding') {
allRefundsOver = false;
break;
}
if (refund2.iState === 'refunded') {
dealPrice += refund2.price;
dealLoss += refund2.loss;
}
}
if (!allRefundsOver) {
continue;
}
const action = dealPrice === price ? 'succeed' : 'partiallySucceed';
await context.operate('withdraw', {
id: await (0, uuid_1.generateNewIdAsync)(),
action,
data: {
dealPrice,
dealLoss,
},
filter: {
id: withdraw.id,
}
}, {});
count++;
}
return count;
}
const triggers = [
{
entity: 'refund',
@ -36,7 +102,19 @@ const triggers = [
const { id, price, pay } = refund;
const { price: payPrice, refunded, channel, applicationId } = pay;
const payClazz = await (0, payClazz_1.getPayClazz)(applicationId, channel, context);
await payClazz.refund(refund);
const data = await payClazz.refund(refund);
if (data) {
const closeFn = context.openRootMode();
await context.operate('refund', {
id: await (0, uuid_1.generateNewIdAsync)(),
data,
action: 'update',
filter: {
id,
}
}, { dontCollect: true });
closeFn();
}
}
},
},
@ -69,7 +147,7 @@ const triggers = [
}, {});
for (const refund of refunds) {
const { id, price, iState, pay } = refund;
(0, assert_1.default)(iState === 'refunding' && pay.iState === 'refunding');
(0, assert_1.default)(iState === 'refunded' && pay.iState === 'refunding');
const { price: payPrice, refunded } = pay;
const refunded2 = refunded + price;
(0, assert_1.default)(refunded2 <= payPrice, '退款金额不应高于pay的总金额');
@ -85,43 +163,51 @@ const triggers = [
}
}, {});
}
const withdraws = await context.select('withdraw', {
return refunds.length + await changeWithdrawStateByRefunds(context, refunds);
}
},
{
entity: 'refund',
action: 'failRefunding',
when: 'after',
name: '退款失败时更新对应的pay状态以及对应的withdraw状态',
fn: async ({ operation }, context) => {
const { filter } = operation;
const refunds = await context.select('refund', {
data: {
id: 1,
iState: 1,
price: 1,
refund$entity: {
$entity: 'refund',
data: {
id: 1,
price: 1,
iState: 1,
},
filter: {
iState: 'refunded',
},
iState: 1,
entity: 1,
entityId: 1,
pay: {
id: 1,
price: 1,
iState: 1,
refunded: 1,
channel: 1,
applicationId: 1,
orderId: 1,
accountId: 1,
},
},
filter: {
id: {
$in: refunds.filter(ele => ele.entity === 'withdraw').map(ele => ele.entityId),
}
}
filter,
}, {});
const successfulWithdraws = withdraws.filter(({ price, iState, refund$entity: relatedRefunds }) => {
(0, assert_1.default)(iState === 'refunding');
let refundedPrice = 0;
relatedRefunds?.forEach(({ price }) => refundedPrice += price);
return relatedRefunds === price;
});
if (successfulWithdraws.length > 0) {
await context.operate('withdraw', {
for (const refund of refunds) {
const { id, iState, pay } = refund;
(0, assert_1.default)(iState === 'failed' && pay.iState === 'refunding');
const { refunded } = pay;
const action = refunded === 0 ? 'stopRefunding' : 'refundPartially';
await context.operate('pay', {
id: await (0, uuid_1.generateNewIdAsync)(),
action: 'succeed',
action,
data: {},
filter: {
id: pay.id,
}
}, {});
}
return refunds.length + successfulWithdraws.length;
return refunds.length + await changeWithdrawStateByRefunds(context, refunds);
}
},
{

View File

@ -15,19 +15,43 @@ const triggers = [
(0, assert_1.default)(!(data instanceof Array));
const { withdrawAccountId, accountId, price } = data;
if (!withdrawAccountId) {
// 没有指定渠道则尝试走退款
const refundData = await (0, pay_1.getAccountPayRefunds)(context, accountId, price);
data.refund$entity = refundData.map((data) => ({
id: (0, uuid_1.generateNewId)(),
action: 'create',
data,
}));
// 没有指定渠道则走退款前台通过getAccountPayRefunds的aspect去获得refund数据并挂载
if (!data.refund$entity) {
const refundData = await (0, pay_1.getAccountPayRefunds)(context, accountId, price);
data.refund$entity = refundData.map((data) => ({
id: (0, uuid_1.generateNewId)(),
action: 'create',
data,
}));
let loss = 0;
data.refund$entity.forEach((ele) => {
const { data } = ele;
loss += data.loss || 0;
});
data.loss = loss;
}
data.iState = 'withdrawing';
}
else {
// 否则走渠道提现,暂时假设为人工操作
const [withdrawAccount] = await context.select('withdrawAccount', {
data: {
id: 1,
channel: {
id: 1,
lossRatio: 1,
},
},
filter: {
id: withdrawAccountId,
}
}, { dontCollect: true });
const { channel } = withdrawAccount;
const { lossRatio } = channel;
data.loss = lossRatio ? Math.ceil(data.price * lossRatio / 100) : 0;
data.iState = 'applying';
}
data.dealPrice = data.dealLoss = 0;
data.creatorId = context.getCurrentUserId();
data.accountOper$entity = [
{
@ -59,13 +83,14 @@ const triggers = [
accountId: 1,
iState: 1,
price: 1,
dealPrice: 1,
withdrawAccountId: 1,
},
filter,
}, {});
for (const withdraw of withdraws) {
const { accountId, withdrawAccountId, price } = withdraw;
(0, assert_1.default)(withdrawAccountId); // 只有走渠道的提现才可以失败
const { accountId, price, dealPrice } = withdraw;
(0, assert_1.default)(dealPrice === 0);
await context.operate('accountOper', {
id: await (0, uuid_1.generateNewIdAsync)(),
action: 'create',
@ -80,6 +105,42 @@ const triggers = [
}
return withdraws.length;
},
},
{
name: '当withdraw部分成功时将差价部分还回到帐户中',
entity: 'withdraw',
action: 'succeedPartially',
when: 'after',
fn: async ({ operation }, context) => {
const { filter, data } = operation;
const withdraws = await context.select('withdraw', {
data: {
id: 1,
accountId: 1,
iState: 1,
price: 1,
dealPrice: 1,
withdrawAccountId: 1,
},
filter,
}, {});
for (const withdraw of withdraws) {
const { accountId, price, dealPrice } = withdraw;
(0, assert_1.default)(price > dealPrice && dealPrice > 0);
await context.operate('accountOper', {
id: await (0, uuid_1.generateNewIdAsync)(),
action: 'create',
data: {
id: await (0, uuid_1.generateNewIdAsync)(),
accountId,
type: 'withdrawBack',
totalPlus: price - dealPrice,
availPlus: price - dealPrice,
},
}, {});
}
return withdraws.length;
}
}
];
exports.default = triggers;

View File

@ -110,7 +110,10 @@ async function getAccountPayRefunds(context, accountId, totalPrice) {
data: {
id: 1,
price: 1,
paid: 1,
refundable: 1,
refunded: 1,
channel: 1,
},
filter: {
refundable: true,
@ -171,6 +174,11 @@ async function getAccountPayRefunds(context, accountId, totalPrice) {
creatorId: context.getCurrentUserId(),
payId: pay.id,
iState: 'refunding',
meta: {
refundLossRatio,
refundLossFloor,
channel,
}
});
price2 += refundPrice;
if (totalPrice && price2 === totalPrice) {

View File

@ -14,16 +14,18 @@ class WechatPay {
this.channel = channel;
}
async refund(refund) {
return;
return {
externalId: Math.random().toString(),
};
}
async closeRefund(refund) {
return;
}
async getRefundState(refund) {
const r = Math.random();
if (r < 0.3) {
/* if (r < 0.3) {
return ['refunding', {}];
}
} */
return ['refunded', {}];
}
decodePayNotification(params, body) {
@ -78,7 +80,10 @@ class WechatPay {
else if (r > 0.95) {
return ['closed', {}];
}
return ['paid', {}];
return ['paid', {
forbidRefundAt: Date.now() + 24 * 3600 * 1000,
refundable: true,
}];
}
async close(pay) {
}

Some files were not shown because too many files have changed in this diff Show More