payment-kit 1.13.163 → 1.13.165
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/api/src/index.ts +2 -0
- package/api/src/locales/zh.ts +1 -1
- package/api/src/queues/payment.ts +1 -1
- package/api/src/routes/connect/collect-batch.ts +125 -0
- package/api/src/routes/connect/shared.ts +32 -0
- package/api/src/routes/subscriptions.ts +20 -0
- package/api/src/store/models/customer.ts +4 -4
- package/api/src/store/models/invoice.ts +38 -22
- package/api/src/store/models/subscription.ts +1 -1
- package/api/src/store/models/types.ts +1 -0
- package/api/tests/libs/util.spec.ts +4 -9
- package/blocklet.yml +2 -2
- package/package.json +4 -4
- package/src/components/filter-toolbar.tsx +5 -13
- package/src/components/invoice/list.tsx +19 -9
- package/src/components/payment-intent/list.tsx +37 -7
- package/src/components/refund/list.tsx +12 -2
- package/src/components/subscription/list.tsx +11 -2
- package/src/components/subscription/portal/actions.tsx +46 -11
- package/src/locales/en.tsx +4 -0
- package/src/locales/zh.tsx +27 -23
- package/src/pages/admin/billing/invoices/detail.tsx +10 -0
- package/src/pages/admin/billing/invoices/index.tsx +1 -1
- package/src/pages/admin/payments/index.tsx +1 -1
- package/src/pages/customer/invoice/past-due.tsx +25 -6
- package/src/pages/customer/subscription/detail.tsx +1 -1
package/api/src/index.ts
CHANGED
|
@@ -27,6 +27,7 @@ import routes from './routes';
|
|
|
27
27
|
import changePaymentHandlers from './routes/connect/change-payment';
|
|
28
28
|
import changePlanHandlers from './routes/connect/change-plan';
|
|
29
29
|
import collectHandlers from './routes/connect/collect';
|
|
30
|
+
import collectBatchHandlers from './routes/connect/collect-batch';
|
|
30
31
|
import payHandlers from './routes/connect/pay';
|
|
31
32
|
import setupHandlers from './routes/connect/setup';
|
|
32
33
|
import subscribeHandlers from './routes/connect/subscribe';
|
|
@@ -54,6 +55,7 @@ app.use(ensureI18n());
|
|
|
54
55
|
|
|
55
56
|
const router = express.Router();
|
|
56
57
|
handlers.attach(Object.assign({ app: router }, collectHandlers));
|
|
58
|
+
handlers.attach(Object.assign({ app: router }, collectBatchHandlers));
|
|
57
59
|
handlers.attach(Object.assign({ app: router }, payHandlers));
|
|
58
60
|
handlers.attach(Object.assign({ app: router }, setupHandlers));
|
|
59
61
|
handlers.attach(Object.assign({ app: router }, subscribeHandlers));
|
package/api/src/locales/zh.ts
CHANGED
|
@@ -77,7 +77,7 @@ export const handlePaymentSucceed = async (paymentIntent: PaymentIntent) => {
|
|
|
77
77
|
logger.info(`Subscription ${subscription.id} updated on payment done ${invoice.id}`);
|
|
78
78
|
} else if (subscription.status === 'past_due' && subscription.cancelation_details?.reason === 'payment_failed') {
|
|
79
79
|
// ensure no uncollectible amount before recovering from payment failed
|
|
80
|
-
const result = await Invoice.
|
|
80
|
+
const [result] = await Invoice.getUncollectibleAmount({ subscriptionId: subscription.id });
|
|
81
81
|
if (isEmpty(result)) {
|
|
82
82
|
// reset billing cycle anchor and cancel_* if we are recovering from payment failed
|
|
83
83
|
if (subscription.cancel_at && subscription.cancel_at !== subscription.current_period_end) {
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/* eslint-disable no-await-in-loop */
|
|
2
|
+
import type { Transaction } from '@ocap/client';
|
|
3
|
+
import { fromAddress } from '@ocap/wallet';
|
|
4
|
+
|
|
5
|
+
import type { CallbackArgs } from '../../libs/auth';
|
|
6
|
+
import { wallet } from '../../libs/auth';
|
|
7
|
+
import { getGasPayerExtra } from '../../libs/payment';
|
|
8
|
+
import { getTxMetadata } from '../../libs/util';
|
|
9
|
+
import { invoiceQueue } from '../../queues/invoice';
|
|
10
|
+
import { handlePaymentSucceed, paymentQueue } from '../../queues/payment';
|
|
11
|
+
import { PaymentIntent } from '../../store/models';
|
|
12
|
+
import { ensureSubscriptionForCollectBatch, getAuthPrincipalClaim } from './shared';
|
|
13
|
+
|
|
14
|
+
// Used to collect uncollectible invoices for a subscription and currency
|
|
15
|
+
export default {
|
|
16
|
+
action: 'collect-batch',
|
|
17
|
+
authPrincipal: false,
|
|
18
|
+
claims: {
|
|
19
|
+
authPrincipal: async ({ extraParams }: CallbackArgs) => {
|
|
20
|
+
const { paymentMethod } = await ensureSubscriptionForCollectBatch(
|
|
21
|
+
extraParams.subscriptionId,
|
|
22
|
+
extraParams.currencyId
|
|
23
|
+
);
|
|
24
|
+
return getAuthPrincipalClaim(paymentMethod, 'pay');
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
onConnect: async ({ userDid, userPk, extraParams }: CallbackArgs) => {
|
|
28
|
+
const { subscriptionId, currencyId } = extraParams;
|
|
29
|
+
const { amount, invoices, paymentCurrency, paymentMethod } = await ensureSubscriptionForCollectBatch(
|
|
30
|
+
subscriptionId,
|
|
31
|
+
currencyId
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
if (paymentMethod.type === 'arcblock') {
|
|
35
|
+
const tokens = [{ address: paymentCurrency.contract as string, value: amount }];
|
|
36
|
+
// @ts-ignore
|
|
37
|
+
const itx: TransferV3Tx = {
|
|
38
|
+
outputs: [{ owner: wallet.address, tokens, assets: [] }],
|
|
39
|
+
data: getTxMetadata({
|
|
40
|
+
subscriptionId,
|
|
41
|
+
currencyId,
|
|
42
|
+
invoices,
|
|
43
|
+
}),
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const claims: { [key: string]: object } = {
|
|
47
|
+
prepareTx: {
|
|
48
|
+
type: 'TransferV3Tx',
|
|
49
|
+
description: `Pay all past due invoices for ${subscriptionId}`,
|
|
50
|
+
partialTx: { from: userDid, pk: userPk, itx },
|
|
51
|
+
requirement: { tokens },
|
|
52
|
+
chainInfo: {
|
|
53
|
+
host: paymentMethod.settings?.arcblock?.api_host as string,
|
|
54
|
+
id: paymentMethod.settings?.arcblock?.chain_id as string,
|
|
55
|
+
},
|
|
56
|
+
},
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
return claims;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
throw new Error(`Payment method ${paymentMethod.type} not supported`);
|
|
63
|
+
},
|
|
64
|
+
onAuth: async ({ userDid, claims, extraParams }: CallbackArgs) => {
|
|
65
|
+
const { subscriptionId, currencyId } = extraParams;
|
|
66
|
+
const { invoices, paymentMethod } = await ensureSubscriptionForCollectBatch(subscriptionId, currencyId);
|
|
67
|
+
|
|
68
|
+
if (paymentMethod.type === 'arcblock') {
|
|
69
|
+
const client = paymentMethod.getOcapClient();
|
|
70
|
+
const claim = claims.find((x) => x.type === 'prepareTx');
|
|
71
|
+
|
|
72
|
+
const tx: Partial<Transaction> = client.decodeTx(claim.finalTx);
|
|
73
|
+
if (claim.delegator && claim.from) {
|
|
74
|
+
tx.delegator = claim.delegator;
|
|
75
|
+
tx.from = claim.from;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// @ts-ignore
|
|
79
|
+
const { buffer } = await client.encodeTransferV3Tx({ tx });
|
|
80
|
+
const txHash = await client.sendTransferV3Tx(
|
|
81
|
+
// @ts-ignore
|
|
82
|
+
{ tx, wallet: fromAddress(userDid) },
|
|
83
|
+
getGasPayerExtra(buffer)
|
|
84
|
+
);
|
|
85
|
+
|
|
86
|
+
const paymentIntents = await PaymentIntent.findAll({
|
|
87
|
+
where: {
|
|
88
|
+
invoice_id: invoices,
|
|
89
|
+
},
|
|
90
|
+
});
|
|
91
|
+
for (const paymentIntent of paymentIntents) {
|
|
92
|
+
await paymentIntent.update({
|
|
93
|
+
status: 'succeeded',
|
|
94
|
+
amount_received: paymentIntent.amount,
|
|
95
|
+
capture_method: 'manual',
|
|
96
|
+
last_payment_error: null,
|
|
97
|
+
payment_details: {
|
|
98
|
+
arcblock: {
|
|
99
|
+
tx_hash: txHash,
|
|
100
|
+
payer: userDid,
|
|
101
|
+
},
|
|
102
|
+
},
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
await handlePaymentSucceed(paymentIntent);
|
|
106
|
+
|
|
107
|
+
// cleanup the queue
|
|
108
|
+
let exist = await paymentQueue.get(paymentIntent.id);
|
|
109
|
+
if (exist) {
|
|
110
|
+
await paymentQueue.delete(paymentIntent.id);
|
|
111
|
+
}
|
|
112
|
+
if (paymentIntent.invoice_id) {
|
|
113
|
+
exist = await invoiceQueue.get(paymentIntent.invoice_id);
|
|
114
|
+
if (exist) {
|
|
115
|
+
await invoiceQueue.delete(paymentIntent.invoice_id);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return { hash: txHash };
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
throw new Error(`Payment method ${paymentMethod.type} not supported`);
|
|
124
|
+
},
|
|
125
|
+
};
|
|
@@ -4,6 +4,7 @@ import { toTypeInfo } from '@arcblock/did';
|
|
|
4
4
|
import { toDelegateAddress } from '@arcblock/did-util';
|
|
5
5
|
import { BN } from '@ocap/util';
|
|
6
6
|
import { fromPublicKey } from '@ocap/wallet';
|
|
7
|
+
import isEmpty from 'lodash/isEmpty';
|
|
7
8
|
|
|
8
9
|
import { estimateMaxGasForTx, hasStakedForGas } from '../../integrations/blockchain/stake';
|
|
9
10
|
import { blocklet, wallet } from '../../libs/auth';
|
|
@@ -768,3 +769,34 @@ export async function ensureChangePaymentContext(subscriptionId: string) {
|
|
|
768
769
|
paymentCurrency,
|
|
769
770
|
};
|
|
770
771
|
}
|
|
772
|
+
|
|
773
|
+
export async function ensureSubscriptionForCollectBatch(subscriptionId: string, currencyId: string) {
|
|
774
|
+
const subscription = await Subscription.findByPk(subscriptionId);
|
|
775
|
+
if (!subscription) {
|
|
776
|
+
throw new Error(`Subscription ${subscriptionId} not found when prepare batch collect`);
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
const paymentCurrency = await PaymentCurrency.findByPk(currencyId);
|
|
780
|
+
if (!paymentCurrency) {
|
|
781
|
+
throw new Error(`PaymentCurrency ${currencyId} not found when prepare batch collect`);
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
const [summary, detail] = await Invoice.getUncollectibleAmount({
|
|
785
|
+
subscriptionId: subscription.id,
|
|
786
|
+
customerId: subscription.customer_id,
|
|
787
|
+
currencyId,
|
|
788
|
+
});
|
|
789
|
+
if (isEmpty(summary) || !summary[currencyId] || summary[currencyId] === '0') {
|
|
790
|
+
throw new Error(`No uncollectible invoice found for subscription ${subscriptionId} to batch collect`);
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
const paymentMethod = await PaymentMethod.findByPk(paymentCurrency.payment_method_id);
|
|
794
|
+
|
|
795
|
+
return {
|
|
796
|
+
subscription,
|
|
797
|
+
paymentCurrency: paymentCurrency as PaymentCurrency,
|
|
798
|
+
paymentMethod: paymentMethod as PaymentMethod,
|
|
799
|
+
amount: summary[currencyId],
|
|
800
|
+
invoices: detail[currencyId],
|
|
801
|
+
};
|
|
802
|
+
}
|
|
@@ -1375,4 +1375,24 @@ router.get('/:id/usage-records', authPortal, (req, res) => {
|
|
|
1375
1375
|
createUsageRecordQueryFn(req.doc)(req, res);
|
|
1376
1376
|
});
|
|
1377
1377
|
|
|
1378
|
+
// Get invoice summary
|
|
1379
|
+
router.get('/:id/summary', authPortal, async (req, res) => {
|
|
1380
|
+
try {
|
|
1381
|
+
const subscription = await Subscription.findByPk(req.params.id);
|
|
1382
|
+
if (!subscription) {
|
|
1383
|
+
return res.status(404).json({ error: 'Subscription not found' });
|
|
1384
|
+
}
|
|
1385
|
+
|
|
1386
|
+
const [summary] = await Invoice.getUncollectibleAmount({
|
|
1387
|
+
subscriptionId: subscription.id,
|
|
1388
|
+
currencyId: subscription.currency_id,
|
|
1389
|
+
customerId: subscription.customer_id,
|
|
1390
|
+
});
|
|
1391
|
+
return res.json(summary);
|
|
1392
|
+
} catch (err) {
|
|
1393
|
+
console.error(err);
|
|
1394
|
+
return res.status(400).json({ error: err.message });
|
|
1395
|
+
}
|
|
1396
|
+
});
|
|
1397
|
+
|
|
1378
1398
|
export default router;
|
|
@@ -161,11 +161,11 @@ export class Customer extends Model<InferAttributes<Customer>, InferCreationAttr
|
|
|
161
161
|
|
|
162
162
|
public async getSummary() {
|
|
163
163
|
const { PaymentIntent, Refund, Invoice } = this.sequelize.models;
|
|
164
|
-
const [paid, due, refunded] = await Promise.all([
|
|
164
|
+
const [paid, [due], refunded] = await Promise.all([
|
|
165
165
|
// @ts-ignore
|
|
166
166
|
PaymentIntent!.getPaidAmountByCustomer(this.id),
|
|
167
167
|
// @ts-ignore
|
|
168
|
-
Invoice!.
|
|
168
|
+
Invoice!.getUncollectibleAmount({ customerId: this.id }),
|
|
169
169
|
// @ts-ignore
|
|
170
170
|
Refund!.getRefundAmountByCustomer(this.id),
|
|
171
171
|
]);
|
|
@@ -176,8 +176,8 @@ export class Customer extends Model<InferAttributes<Customer>, InferCreationAttr
|
|
|
176
176
|
public async canMakeNewPurchase(excludedInvoiceId: string = '') {
|
|
177
177
|
const { Invoice } = this.sequelize.models;
|
|
178
178
|
// @ts-ignore
|
|
179
|
-
const
|
|
180
|
-
return Object.entries(
|
|
179
|
+
const [summary] = await Invoice!.getUncollectibleAmount({ customerId: this.id, excludedInvoiceId });
|
|
180
|
+
return Object.entries(summary).every(([, amount]) => new BN(amount).lte(new BN(0)));
|
|
181
181
|
}
|
|
182
182
|
|
|
183
183
|
public getBalanceToApply(currencyId: string, amount: string) {
|
|
@@ -19,6 +19,7 @@ import type {
|
|
|
19
19
|
CustomerShipping,
|
|
20
20
|
DiscountAmount,
|
|
21
21
|
GroupedBN,
|
|
22
|
+
GroupedStrList,
|
|
22
23
|
PaymentError,
|
|
23
24
|
PaymentSettings,
|
|
24
25
|
SimpleCustomField,
|
|
@@ -523,44 +524,59 @@ export class Invoice extends Model<InferAttributes<Invoice>, InferCreationAttrib
|
|
|
523
524
|
return ['paid', 'void'].includes(this.status);
|
|
524
525
|
}
|
|
525
526
|
|
|
526
|
-
|
|
527
|
+
private static async _getUncollectibleAmount(where: WhereOptions<Invoice>): Promise<[GroupedBN, GroupedStrList]> {
|
|
527
528
|
const invoices = await Invoice.findAll({ where });
|
|
528
|
-
|
|
529
|
+
const summary: GroupedBN = {};
|
|
530
|
+
const detail: GroupedStrList = {};
|
|
531
|
+
|
|
532
|
+
invoices.forEach((invoice) => {
|
|
529
533
|
const key = invoice.currency_id;
|
|
530
|
-
if (!
|
|
531
|
-
|
|
534
|
+
if (!summary[key]) {
|
|
535
|
+
summary[key] = '0';
|
|
536
|
+
}
|
|
537
|
+
if (!detail[key]) {
|
|
538
|
+
detail[key] = [];
|
|
532
539
|
}
|
|
533
540
|
|
|
534
|
-
|
|
541
|
+
summary[key] = new BN(summary[key]).add(new BN(invoice.amount_remaining)).toString();
|
|
542
|
+
detail[key]!.push(invoice.id);
|
|
543
|
+
});
|
|
535
544
|
|
|
536
|
-
|
|
537
|
-
}, {});
|
|
545
|
+
return [summary, detail];
|
|
538
546
|
}
|
|
539
547
|
|
|
540
|
-
public static
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
+
public static getUncollectibleAmount({
|
|
549
|
+
customerId,
|
|
550
|
+
subscriptionId,
|
|
551
|
+
currencyId,
|
|
552
|
+
excludedInvoiceId,
|
|
553
|
+
}: {
|
|
554
|
+
customerId?: string;
|
|
555
|
+
subscriptionId?: string;
|
|
556
|
+
currencyId?: string;
|
|
557
|
+
excludedInvoiceId?: string;
|
|
558
|
+
}) {
|
|
559
|
+
if (!customerId && !subscriptionId) {
|
|
560
|
+
throw new Error('customerId or subscriptionId is required for getUncollectibleAmount');
|
|
548
561
|
}
|
|
549
|
-
|
|
550
|
-
return Invoice.getUncollectibleAmount(where);
|
|
551
|
-
}
|
|
552
|
-
|
|
553
|
-
public static getUncollectibleAmountBySubscription(subscriptionId: string, excludedInvoiceId?: string) {
|
|
554
562
|
const where: WhereOptions<Invoice> = {
|
|
555
563
|
status: ['uncollectible'],
|
|
556
|
-
subscription_id: subscriptionId,
|
|
557
564
|
amount_remaining: { [Op.gt]: '0' },
|
|
558
565
|
};
|
|
566
|
+
if (currencyId) {
|
|
567
|
+
where.currency_id = currencyId;
|
|
568
|
+
}
|
|
569
|
+
if (customerId) {
|
|
570
|
+
where.customer_id = customerId;
|
|
571
|
+
}
|
|
572
|
+
if (subscriptionId) {
|
|
573
|
+
where.subscription_id = subscriptionId;
|
|
574
|
+
}
|
|
559
575
|
if (excludedInvoiceId) {
|
|
560
576
|
where.id = { [Op.not]: excludedInvoiceId };
|
|
561
577
|
}
|
|
562
578
|
|
|
563
|
-
return
|
|
579
|
+
return this._getUncollectibleAmount(where);
|
|
564
580
|
}
|
|
565
581
|
}
|
|
566
582
|
|
|
@@ -322,7 +322,7 @@ export class Subscription extends Model<InferAttributes<Subscription>, InferCrea
|
|
|
322
322
|
},
|
|
323
323
|
});
|
|
324
324
|
if (count === 2) {
|
|
325
|
-
// 当且仅当 有试用期的订阅更新了 && 恰好有 2
|
|
325
|
+
// 当且仅当 有试用期的订阅更新了 && 恰好有 2 次账单,此时订阅的试用期刚好结束
|
|
326
326
|
createEvent('Subscription', 'customer.subscription.trial_end', model, options).catch(console.error);
|
|
327
327
|
}
|
|
328
328
|
}
|
|
@@ -1,17 +1,16 @@
|
|
|
1
1
|
import { Op } from 'sequelize';
|
|
2
|
+
|
|
3
|
+
import { getWhereFromKvQuery } from '../../src/libs/api';
|
|
2
4
|
import dayjs from '../../src/libs/dayjs';
|
|
3
5
|
import {
|
|
4
6
|
createCodeGenerator,
|
|
5
7
|
createIdGenerator,
|
|
6
8
|
formatMetadata,
|
|
7
9
|
getDataObjectFromQuery,
|
|
8
|
-
|
|
9
10
|
getNextRetry,
|
|
10
11
|
tryWithTimeout,
|
|
11
12
|
} from '../../src/libs/util';
|
|
12
13
|
|
|
13
|
-
import { getWhereFromKvQuery } from '../../src/libs/api'
|
|
14
|
-
|
|
15
14
|
describe('createIdGenerator', () => {
|
|
16
15
|
it('should return a function that generates an ID with the specified prefix and size', () => {
|
|
17
16
|
const generateId = createIdGenerator('test', 10);
|
|
@@ -199,11 +198,7 @@ describe('getWhereFromKvQuery', () => {
|
|
|
199
198
|
const q = 'like-status:succ like-number:1533';
|
|
200
199
|
const result = getWhereFromKvQuery(q);
|
|
201
200
|
expect(result).toEqual({
|
|
202
|
-
[Op.or]: [
|
|
203
|
-
{ status: { [Op.like]: `%succ%` } },
|
|
204
|
-
{ number: { [Op.like]: `%1533%` } }
|
|
205
|
-
],
|
|
201
|
+
[Op.or]: [{ status: { [Op.like]: '%succ%' } }, { number: { [Op.like]: '%1533%' } }],
|
|
206
202
|
});
|
|
207
203
|
});
|
|
208
|
-
|
|
209
|
-
});
|
|
204
|
+
});
|
package/blocklet.yml
CHANGED
|
@@ -14,7 +14,7 @@ repository:
|
|
|
14
14
|
type: git
|
|
15
15
|
url: git+https://github.com/blocklet/payment-kit.git
|
|
16
16
|
specVersion: 1.2.8
|
|
17
|
-
version: 1.13.
|
|
17
|
+
version: 1.13.165
|
|
18
18
|
logo: logo.png
|
|
19
19
|
files:
|
|
20
20
|
- dist
|
|
@@ -93,7 +93,7 @@ navigation:
|
|
|
93
93
|
- id: billing
|
|
94
94
|
title:
|
|
95
95
|
en: Billing
|
|
96
|
-
zh:
|
|
96
|
+
zh: 我的账单
|
|
97
97
|
icon: ion:receipt-outline
|
|
98
98
|
link: /customer
|
|
99
99
|
section:
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "payment-kit",
|
|
3
|
-
"version": "1.13.
|
|
3
|
+
"version": "1.13.165",
|
|
4
4
|
"scripts": {
|
|
5
5
|
"dev": "cross-env COMPONENT_STORE_URL=https://test.store.blocklet.dev blocklet dev --open",
|
|
6
6
|
"eject": "vite eject",
|
|
@@ -50,7 +50,7 @@
|
|
|
50
50
|
"@arcblock/jwt": "^1.18.110",
|
|
51
51
|
"@arcblock/ux": "^2.9.39",
|
|
52
52
|
"@blocklet/logger": "1.16.23",
|
|
53
|
-
"@blocklet/payment-react": "1.13.
|
|
53
|
+
"@blocklet/payment-react": "1.13.165",
|
|
54
54
|
"@blocklet/sdk": "1.16.23",
|
|
55
55
|
"@blocklet/ui-react": "^2.9.39",
|
|
56
56
|
"@blocklet/uploader": "^0.0.74",
|
|
@@ -110,7 +110,7 @@
|
|
|
110
110
|
"devDependencies": {
|
|
111
111
|
"@abtnode/types": "1.16.23",
|
|
112
112
|
"@arcblock/eslint-config-ts": "^0.2.4",
|
|
113
|
-
"@blocklet/payment-types": "1.13.
|
|
113
|
+
"@blocklet/payment-types": "1.13.165",
|
|
114
114
|
"@types/cookie-parser": "^1.4.6",
|
|
115
115
|
"@types/cors": "^2.8.17",
|
|
116
116
|
"@types/dotenv-flow": "^3.3.3",
|
|
@@ -149,5 +149,5 @@
|
|
|
149
149
|
"parser": "typescript"
|
|
150
150
|
}
|
|
151
151
|
},
|
|
152
|
-
"gitHead": "
|
|
152
|
+
"gitHead": "24fadab6620ca13856393399efd6a1dffc431d66"
|
|
153
153
|
}
|
|
@@ -40,7 +40,7 @@ type Props = {
|
|
|
40
40
|
setSearch: (x: any) => void;
|
|
41
41
|
};
|
|
42
42
|
|
|
43
|
-
export default function
|
|
43
|
+
export default function FilterToolbar(props: Props) {
|
|
44
44
|
const { setSearch, search, status, currency } = props;
|
|
45
45
|
const isProduct = window.location.pathname.includes('product');
|
|
46
46
|
const handleSearch = (obj: any) => {
|
|
@@ -71,7 +71,7 @@ const defaultProps = {
|
|
|
71
71
|
currency: false,
|
|
72
72
|
};
|
|
73
73
|
|
|
74
|
-
|
|
74
|
+
FilterToolbar.defaultProps = defaultProps;
|
|
75
75
|
SearchStatus.defaultProps = defaultProps;
|
|
76
76
|
SearchCurrency.defaultProps = defaultProps;
|
|
77
77
|
SearchProducts.defaultProps = defaultProps;
|
|
@@ -218,7 +218,7 @@ function SearchCustomers({ setSearch }: Pick<Props, 'setSearch'>) {
|
|
|
218
218
|
'like-phone': text,
|
|
219
219
|
},
|
|
220
220
|
page: 1,
|
|
221
|
-
pageSize:
|
|
221
|
+
pageSize: 10,
|
|
222
222
|
}).then((data: any) => {
|
|
223
223
|
setCustomers(data.list);
|
|
224
224
|
});
|
|
@@ -315,7 +315,7 @@ function SearchProducts({ setSearch }: Pick<Props, 'setSearch'>) {
|
|
|
315
315
|
[key]: str,
|
|
316
316
|
},
|
|
317
317
|
page: 1,
|
|
318
|
-
pageSize:
|
|
318
|
+
pageSize: 10,
|
|
319
319
|
});
|
|
320
320
|
});
|
|
321
321
|
}, [price]);
|
|
@@ -371,7 +371,7 @@ function SearchProducts({ setSearch }: Pick<Props, 'setSearch'>) {
|
|
|
371
371
|
const Root = styled(Box)`
|
|
372
372
|
.table-toolbar-left {
|
|
373
373
|
display: flex;
|
|
374
|
-
align-items:
|
|
374
|
+
align-items: center;
|
|
375
375
|
}
|
|
376
376
|
|
|
377
377
|
.table-toolbar-left section {
|
|
@@ -396,10 +396,6 @@ const Root = styled(Box)`
|
|
|
396
396
|
padding: 0 3px;
|
|
397
397
|
}
|
|
398
398
|
|
|
399
|
-
.custom-toobar-title-inner {
|
|
400
|
-
line-height: normal;
|
|
401
|
-
}
|
|
402
|
-
|
|
403
399
|
.status-options {
|
|
404
400
|
position: absolute;
|
|
405
401
|
box-shadow: 0px 5px 5px -3px rgba(0, 0, 0, 0.2), 0px 8px 10px 1px rgba(0, 0, 0, 0.14),
|
|
@@ -419,8 +415,4 @@ const Root = styled(Box)`
|
|
|
419
415
|
list-style: none;
|
|
420
416
|
line-height: normal;
|
|
421
417
|
}
|
|
422
|
-
|
|
423
|
-
.custom-toobar-title-inner span {
|
|
424
|
-
overflow: auto;
|
|
425
|
-
}
|
|
426
418
|
`;
|
|
@@ -9,7 +9,7 @@ import { useEffect, useState } from 'react';
|
|
|
9
9
|
import { useNavigate } from 'react-router-dom';
|
|
10
10
|
|
|
11
11
|
import CustomerLink from '../customer/link';
|
|
12
|
-
import
|
|
12
|
+
import FilterToolbar from '../filter-toolbar';
|
|
13
13
|
import Table from '../table';
|
|
14
14
|
import InvoiceActions from './action';
|
|
15
15
|
|
|
@@ -41,6 +41,7 @@ type ListProps = {
|
|
|
41
41
|
features?: {
|
|
42
42
|
customer?: boolean;
|
|
43
43
|
toolbar?: boolean;
|
|
44
|
+
filter?: boolean;
|
|
44
45
|
footer?: boolean;
|
|
45
46
|
};
|
|
46
47
|
customer_id?: string;
|
|
@@ -68,6 +69,7 @@ const getListKey = (props: ListProps) => {
|
|
|
68
69
|
InvoiceList.defaultProps = {
|
|
69
70
|
features: {
|
|
70
71
|
customer: true,
|
|
72
|
+
filter: true,
|
|
71
73
|
},
|
|
72
74
|
customer_id: '',
|
|
73
75
|
subscription_id: '',
|
|
@@ -150,20 +152,21 @@ export default function InvoiceList({ customer_id, subscription_id, features, st
|
|
|
150
152
|
},
|
|
151
153
|
},
|
|
152
154
|
{
|
|
153
|
-
label: t('
|
|
154
|
-
name: '
|
|
155
|
+
label: t('common.createdAt'),
|
|
156
|
+
name: 'created_at',
|
|
155
157
|
options: {
|
|
156
|
-
|
|
157
|
-
|
|
158
|
+
sort: true,
|
|
159
|
+
responsive: 'vertical',
|
|
160
|
+
customBodyRender: (e: string) => {
|
|
161
|
+
return formatTime(e);
|
|
158
162
|
},
|
|
159
163
|
},
|
|
160
164
|
},
|
|
161
165
|
{
|
|
162
|
-
label: t('common.
|
|
163
|
-
name: '
|
|
166
|
+
label: t('common.updatedAt'),
|
|
167
|
+
name: 'updated_at',
|
|
164
168
|
options: {
|
|
165
169
|
sort: true,
|
|
166
|
-
responsive: 'vertical',
|
|
167
170
|
customBodyRender: (e: string) => {
|
|
168
171
|
return formatTime(e);
|
|
169
172
|
},
|
|
@@ -260,7 +263,14 @@ export default function InvoiceList({ customer_id, subscription_id, features, st
|
|
|
260
263
|
toolbar={features?.toolbar}
|
|
261
264
|
footer={features?.footer}
|
|
262
265
|
title={
|
|
263
|
-
|
|
266
|
+
features?.filter && (
|
|
267
|
+
<FilterToolbar
|
|
268
|
+
setSearch={setSearch}
|
|
269
|
+
search={search}
|
|
270
|
+
status={['draft', 'open', 'void', 'paid', 'uncollectible']}
|
|
271
|
+
currency
|
|
272
|
+
/>
|
|
273
|
+
)
|
|
264
274
|
}
|
|
265
275
|
/>
|
|
266
276
|
);
|
|
@@ -10,7 +10,7 @@ import { useNavigate } from 'react-router-dom';
|
|
|
10
10
|
|
|
11
11
|
import { debounce } from '../../libs/util';
|
|
12
12
|
import CustomerLink from '../customer/link';
|
|
13
|
-
import
|
|
13
|
+
import FilterToolbar from '../filter-toolbar';
|
|
14
14
|
import Table from '../table';
|
|
15
15
|
import PaymentIntentActions from './actions';
|
|
16
16
|
|
|
@@ -42,6 +42,7 @@ type ListProps = {
|
|
|
42
42
|
features?: {
|
|
43
43
|
customer?: boolean;
|
|
44
44
|
toolbar?: boolean;
|
|
45
|
+
filter?: boolean;
|
|
45
46
|
footer?: boolean;
|
|
46
47
|
};
|
|
47
48
|
customer_id?: string;
|
|
@@ -107,8 +108,15 @@ export default function PaymentList({ customer_id, invoice_id, features }: ListP
|
|
|
107
108
|
customBodyRenderLite: (_: string, index: number) => {
|
|
108
109
|
const item = data.list[index] as TPaymentIntentExpanded;
|
|
109
110
|
return (
|
|
110
|
-
<Typography
|
|
111
|
-
|
|
111
|
+
<Typography
|
|
112
|
+
component="strong"
|
|
113
|
+
sx={{ color: item.amount_received === '0' ? 'warning.main' : 'inherit' }}
|
|
114
|
+
fontWeight={600}>
|
|
115
|
+
{fromUnitToToken(
|
|
116
|
+
item.amount_received === '0' ? item.amount : item.amount_received,
|
|
117
|
+
item?.paymentCurrency.decimal
|
|
118
|
+
)}
|
|
119
|
+
|
|
112
120
|
{item?.paymentCurrency.symbol}
|
|
113
121
|
</Typography>
|
|
114
122
|
);
|
|
@@ -120,7 +128,6 @@ export default function PaymentList({ customer_id, invoice_id, features }: ListP
|
|
|
120
128
|
name: 'status',
|
|
121
129
|
width: 60,
|
|
122
130
|
options: {
|
|
123
|
-
filter: true,
|
|
124
131
|
customBodyRenderLite: (_: string, index: number) => {
|
|
125
132
|
const item = data.list[index] as TPaymentIntentExpanded;
|
|
126
133
|
return <Status label={item.status} color={getPaymentIntentStatusColor(item.status)} />;
|
|
@@ -131,7 +138,6 @@ export default function PaymentList({ customer_id, invoice_id, features }: ListP
|
|
|
131
138
|
label: t('common.description'),
|
|
132
139
|
name: 'description',
|
|
133
140
|
options: {
|
|
134
|
-
filter: true,
|
|
135
141
|
customBodyRenderLite: (_: string, index: number) => {
|
|
136
142
|
const item = data.list[index] as TPaymentIntentExpanded;
|
|
137
143
|
return item.description || item.id;
|
|
@@ -143,7 +149,16 @@ export default function PaymentList({ customer_id, invoice_id, features }: ListP
|
|
|
143
149
|
name: 'created_at',
|
|
144
150
|
options: {
|
|
145
151
|
sort: true,
|
|
146
|
-
|
|
152
|
+
customBodyRender: (e: string) => {
|
|
153
|
+
return formatTime(e);
|
|
154
|
+
},
|
|
155
|
+
},
|
|
156
|
+
},
|
|
157
|
+
{
|
|
158
|
+
label: t('common.updatedAt'),
|
|
159
|
+
name: 'updated_at',
|
|
160
|
+
options: {
|
|
161
|
+
sort: true,
|
|
147
162
|
customBodyRender: (e: string) => {
|
|
148
163
|
return formatTime(e);
|
|
149
164
|
},
|
|
@@ -230,7 +245,22 @@ export default function PaymentList({ customer_id, invoice_id, features }: ListP
|
|
|
230
245
|
toolbar={features?.toolbar}
|
|
231
246
|
footer={features?.footer}
|
|
232
247
|
title={
|
|
233
|
-
|
|
248
|
+
features?.filter && (
|
|
249
|
+
<FilterToolbar
|
|
250
|
+
setSearch={setSearch}
|
|
251
|
+
search={search}
|
|
252
|
+
status={[
|
|
253
|
+
'requires_payment_method',
|
|
254
|
+
'requires_confirmation',
|
|
255
|
+
'requires_action',
|
|
256
|
+
'processing',
|
|
257
|
+
'requires_capture',
|
|
258
|
+
'canceled',
|
|
259
|
+
'succeeded',
|
|
260
|
+
]}
|
|
261
|
+
currency
|
|
262
|
+
/>
|
|
263
|
+
)
|
|
234
264
|
}
|
|
235
265
|
/>
|
|
236
266
|
);
|
|
@@ -9,7 +9,7 @@ import { useEffect, useState } from 'react';
|
|
|
9
9
|
import { useNavigate } from 'react-router-dom';
|
|
10
10
|
|
|
11
11
|
import CustomerLink from '../customer/link';
|
|
12
|
-
import
|
|
12
|
+
import FilterToolbar from '../filter-toolbar';
|
|
13
13
|
import Table from '../table';
|
|
14
14
|
import RefundActions from './actions';
|
|
15
15
|
|
|
@@ -42,6 +42,7 @@ type ListProps = {
|
|
|
42
42
|
features?: {
|
|
43
43
|
customer?: boolean;
|
|
44
44
|
toolbar?: boolean;
|
|
45
|
+
filter?: boolean;
|
|
45
46
|
footer?: boolean;
|
|
46
47
|
};
|
|
47
48
|
customer_id?: string;
|
|
@@ -231,7 +232,16 @@ export default function RefundList({ customer_id, invoice_id, subscription_id, f
|
|
|
231
232
|
}}
|
|
232
233
|
toolbar={features?.toolbar}
|
|
233
234
|
footer={features?.footer}
|
|
234
|
-
title={
|
|
235
|
+
title={
|
|
236
|
+
features?.filter && (
|
|
237
|
+
<FilterToolbar
|
|
238
|
+
setSearch={setSearch}
|
|
239
|
+
search={search}
|
|
240
|
+
status={['requires_action', 'pending', 'failed', 'canceled', 'succeeded']}
|
|
241
|
+
currency
|
|
242
|
+
/>
|
|
243
|
+
)
|
|
244
|
+
}
|
|
235
245
|
/>
|
|
236
246
|
);
|
|
237
247
|
}
|
|
@@ -8,7 +8,7 @@ import { useEffect, useState } from 'react';
|
|
|
8
8
|
import { useNavigate } from 'react-router-dom';
|
|
9
9
|
|
|
10
10
|
import CustomerLink from '../customer/link';
|
|
11
|
-
import
|
|
11
|
+
import FilterToolbar from '../filter-toolbar';
|
|
12
12
|
import Table from '../table';
|
|
13
13
|
import SubscriptionActions from './actions';
|
|
14
14
|
import SubscriptionDescription from './description';
|
|
@@ -41,6 +41,7 @@ type ListProps = {
|
|
|
41
41
|
features?: {
|
|
42
42
|
customer?: boolean;
|
|
43
43
|
toolbar?: boolean;
|
|
44
|
+
filter?: boolean;
|
|
44
45
|
footer?: boolean;
|
|
45
46
|
};
|
|
46
47
|
customer_id?: string;
|
|
@@ -222,7 +223,15 @@ export default function SubscriptionList({ customer_id, features, status }: List
|
|
|
222
223
|
}}
|
|
223
224
|
toolbar={features?.toolbar}
|
|
224
225
|
footer={features?.footer}
|
|
225
|
-
title={
|
|
226
|
+
title={
|
|
227
|
+
features?.filter && (
|
|
228
|
+
<FilterToolbar
|
|
229
|
+
setSearch={setSearch}
|
|
230
|
+
search={search}
|
|
231
|
+
status={['active', 'canceled', 'incomplete', 'incomplete_expired', 'past_due', 'trialing', 'paused']}
|
|
232
|
+
/>
|
|
233
|
+
)
|
|
234
|
+
}
|
|
226
235
|
/>
|
|
227
236
|
);
|
|
228
237
|
}
|
|
@@ -5,6 +5,7 @@ import { ConfirmDialog, api, formatError, formatToDate, getSubscriptionAction }
|
|
|
5
5
|
import type { TSubscriptionExpanded } from '@blocklet/payment-types';
|
|
6
6
|
import { Button, Link, Stack } from '@mui/material';
|
|
7
7
|
import { useRequest, useSetState } from 'ahooks';
|
|
8
|
+
import isEmpty from 'lodash/isEmpty';
|
|
8
9
|
import { FormProvider, useForm, useFormContext } from 'react-hook-form';
|
|
9
10
|
import { useNavigate } from 'react-router-dom';
|
|
10
11
|
|
|
@@ -12,29 +13,52 @@ import CustomerCancelForm from './cancel';
|
|
|
12
13
|
|
|
13
14
|
type Props = {
|
|
14
15
|
subscription: TSubscriptionExpanded;
|
|
15
|
-
|
|
16
|
+
showExtra?: boolean;
|
|
16
17
|
onChange: (action?: string) => any | Promise<any>;
|
|
17
18
|
};
|
|
18
19
|
|
|
19
20
|
SubscriptionActions.defaultProps = {
|
|
20
|
-
|
|
21
|
+
showExtra: false,
|
|
21
22
|
};
|
|
22
23
|
|
|
23
|
-
const
|
|
24
|
-
|
|
25
|
-
|
|
24
|
+
const fetchExtraActions = async ({
|
|
25
|
+
id,
|
|
26
|
+
showExtra,
|
|
27
|
+
}: {
|
|
28
|
+
id: string;
|
|
29
|
+
showExtra: boolean;
|
|
30
|
+
}): Promise<{ changePlan: boolean; batchPay: string }> => {
|
|
31
|
+
if (!showExtra) {
|
|
32
|
+
return Promise.resolve({ changePlan: false, batchPay: '' });
|
|
26
33
|
}
|
|
27
34
|
|
|
28
|
-
|
|
35
|
+
const [changePlan, batchPay] = await Promise.all([
|
|
36
|
+
api
|
|
37
|
+
.get(`/api/subscriptions/${id}/change-plan`)
|
|
38
|
+
.then((res) => !!res.data)
|
|
39
|
+
.catch(() => false),
|
|
40
|
+
api
|
|
41
|
+
.get(`/api/subscriptions/${id}/summary`)
|
|
42
|
+
.then((res) => {
|
|
43
|
+
if (!isEmpty(res.data) && Object.keys(res.data).length === 1) {
|
|
44
|
+
return Object.keys(res.data)[0] as string;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return '';
|
|
48
|
+
})
|
|
49
|
+
.catch(() => ''),
|
|
50
|
+
]);
|
|
51
|
+
|
|
52
|
+
return { changePlan, batchPay };
|
|
29
53
|
};
|
|
30
54
|
|
|
31
|
-
export function SubscriptionActionsInner({ subscription,
|
|
55
|
+
export function SubscriptionActionsInner({ subscription, showExtra, onChange }: Props) {
|
|
32
56
|
const { t, locale } = useLocaleContext();
|
|
33
57
|
const { reset, getValues } = useFormContext();
|
|
34
58
|
const navigate = useNavigate();
|
|
35
59
|
const action = getSubscriptionAction(subscription);
|
|
36
60
|
|
|
37
|
-
const { data } = useRequest(() =>
|
|
61
|
+
const { data: extraActions } = useRequest(() => fetchExtraActions({ id: subscription.id, showExtra: !!showExtra }));
|
|
38
62
|
|
|
39
63
|
const [state, setState] = useSetState({
|
|
40
64
|
action: '',
|
|
@@ -75,7 +99,7 @@ export function SubscriptionActionsInner({ subscription, showUpdate, onChange }:
|
|
|
75
99
|
|
|
76
100
|
return (
|
|
77
101
|
<Stack direction="row" alignItems="center" spacing={1}>
|
|
78
|
-
{action && (
|
|
102
|
+
{!extraActions?.batchPay && action && (
|
|
79
103
|
<Button
|
|
80
104
|
variant={action.variant as any}
|
|
81
105
|
color={action.color as any}
|
|
@@ -93,7 +117,7 @@ export function SubscriptionActionsInner({ subscription, showUpdate, onChange }:
|
|
|
93
117
|
{t(`payment.customer.${action.action}.button`)}
|
|
94
118
|
</Button>
|
|
95
119
|
)}
|
|
96
|
-
{
|
|
120
|
+
{extraActions?.changePlan && (
|
|
97
121
|
<Button
|
|
98
122
|
variant="contained"
|
|
99
123
|
color="primary"
|
|
@@ -104,6 +128,17 @@ export function SubscriptionActionsInner({ subscription, showUpdate, onChange }:
|
|
|
104
128
|
{t('payment.customer.changePlan.button')}
|
|
105
129
|
</Button>
|
|
106
130
|
)}
|
|
131
|
+
{!!extraActions?.batchPay && (
|
|
132
|
+
<Button
|
|
133
|
+
variant="contained"
|
|
134
|
+
color="info"
|
|
135
|
+
size="small"
|
|
136
|
+
onClick={() => {
|
|
137
|
+
navigate(`/customer/invoice/past-due?subscription=${subscription.id}¤cy=${extraActions.batchPay}`);
|
|
138
|
+
}}>
|
|
139
|
+
{t('admin.subscription.batchPay.button')}
|
|
140
|
+
</Button>
|
|
141
|
+
)}
|
|
107
142
|
{subscription.service_actions?.map((x) => (
|
|
108
143
|
// @ts-ignore
|
|
109
144
|
<Button
|
|
@@ -160,5 +195,5 @@ export default function SubscriptionActions(props: Props) {
|
|
|
160
195
|
}
|
|
161
196
|
|
|
162
197
|
SubscriptionActionsInner.defaultProps = {
|
|
163
|
-
|
|
198
|
+
showExtra: false,
|
|
164
199
|
};
|
package/src/locales/en.tsx
CHANGED
|
@@ -231,6 +231,7 @@ export default flat({
|
|
|
231
231
|
customer: 'Customer portal',
|
|
232
232
|
},
|
|
233
233
|
paymentIntent: {
|
|
234
|
+
list: 'Payments',
|
|
234
235
|
name: 'Payment',
|
|
235
236
|
view: 'View payment detail',
|
|
236
237
|
empty: 'No payment intent',
|
|
@@ -385,6 +386,9 @@ export default flat({
|
|
|
385
386
|
vary: 'Varies with usage',
|
|
386
387
|
used: 'Unit used',
|
|
387
388
|
},
|
|
389
|
+
batchPay: {
|
|
390
|
+
button: 'Pay due invoices',
|
|
391
|
+
},
|
|
388
392
|
},
|
|
389
393
|
customer: {
|
|
390
394
|
view: 'View customer',
|
package/src/locales/zh.tsx
CHANGED
|
@@ -21,8 +21,8 @@ export default flat({
|
|
|
21
21
|
coupons: '优惠券',
|
|
22
22
|
pricing: '定价',
|
|
23
23
|
pricingTables: '价格表',
|
|
24
|
-
billing: '
|
|
25
|
-
invoices: '
|
|
24
|
+
billing: '订阅和账单',
|
|
25
|
+
invoices: '账单',
|
|
26
26
|
subscriptions: '订阅',
|
|
27
27
|
developers: '开发者工具箱',
|
|
28
28
|
webhooks: '钩子',
|
|
@@ -53,7 +53,7 @@ export default flat({
|
|
|
53
53
|
removeTip: '删除将隐藏此产品不再允许新购买。确定要删除此产品吗?',
|
|
54
54
|
archived: '此产品已存档',
|
|
55
55
|
archivedTip:
|
|
56
|
-
'
|
|
56
|
+
'此产品无法添加到新的账单、订阅、支付链接或定价表。具有此产品的任何现有订阅将保持活动状态,直到取消,任何现有的支付链接或定价表将被停用。',
|
|
57
57
|
locked: '此产品已锁定,因为至少有一个价格用于订阅或支付。',
|
|
58
58
|
image: {
|
|
59
59
|
label: '图片',
|
|
@@ -71,7 +71,7 @@ export default flat({
|
|
|
71
71
|
description: {
|
|
72
72
|
label: '描述',
|
|
73
73
|
required: '产品描述是必需的',
|
|
74
|
-
placeholder: '
|
|
74
|
+
placeholder: '在结账、账单页面显示的产品描述',
|
|
75
75
|
},
|
|
76
76
|
statement_descriptor: {
|
|
77
77
|
label: '声明描述',
|
|
@@ -183,7 +183,7 @@ export default flat({
|
|
|
183
183
|
mintNft: '购买完成时铸造 NFT',
|
|
184
184
|
mintNftFrom: '从下面的 NFT 集合铸造',
|
|
185
185
|
noConfirmPage: '不显示确认页面',
|
|
186
|
-
createInvoice: '
|
|
186
|
+
createInvoice: '为相关付款创建账单。',
|
|
187
187
|
adjustable: '可调整数量',
|
|
188
188
|
adjustableQuantity: '允许客户调整数量',
|
|
189
189
|
noProducts: '支付链接必须至少有一个产品',
|
|
@@ -223,10 +223,11 @@ export default flat({
|
|
|
223
223
|
customer: '客户门户',
|
|
224
224
|
},
|
|
225
225
|
paymentIntent: {
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
226
|
+
list: '付款记录',
|
|
227
|
+
name: '付款记录',
|
|
228
|
+
view: '查看付款详情',
|
|
229
|
+
empty: '没有付款记录',
|
|
230
|
+
refund: '退款',
|
|
230
231
|
received: '实收金额',
|
|
231
232
|
},
|
|
232
233
|
paymentMethod: {
|
|
@@ -297,11 +298,11 @@ export default flat({
|
|
|
297
298
|
pendingWebhooks: '待处理的钩子',
|
|
298
299
|
},
|
|
299
300
|
invoice: {
|
|
300
|
-
view: '
|
|
301
|
-
name: '
|
|
302
|
-
from: '
|
|
303
|
-
empty: '
|
|
304
|
-
number: '
|
|
301
|
+
view: '查看账单',
|
|
302
|
+
name: '账单',
|
|
303
|
+
from: '账单来自',
|
|
304
|
+
empty: '没有账单',
|
|
305
|
+
number: '账单编号',
|
|
305
306
|
dueDate: '截止日期',
|
|
306
307
|
finalizedAt: '已完成时间',
|
|
307
308
|
paidAt: '支付日期',
|
|
@@ -309,8 +310,8 @@ export default flat({
|
|
|
309
310
|
customer: '开具给',
|
|
310
311
|
billing: '计费方式',
|
|
311
312
|
download: '下载PDF',
|
|
312
|
-
edit: '
|
|
313
|
-
duplicate: '
|
|
313
|
+
edit: '编辑账单',
|
|
314
|
+
duplicate: '复制账单',
|
|
314
315
|
},
|
|
315
316
|
subscription: {
|
|
316
317
|
view: '查看订阅',
|
|
@@ -323,11 +324,11 @@ export default flat({
|
|
|
323
324
|
trailEnd: '试用期结束于{date}',
|
|
324
325
|
discount: '折扣',
|
|
325
326
|
startedAt: '开始于',
|
|
326
|
-
nextInvoice: '
|
|
327
|
+
nextInvoice: '下一张账单',
|
|
327
328
|
itemId: '订阅项目ID',
|
|
328
329
|
update: '更新订阅',
|
|
329
330
|
resume: '恢复付款',
|
|
330
|
-
resumeTip: '
|
|
331
|
+
resumeTip: '您确定要继续收款吗?此订阅的未来账单将继续付款。',
|
|
331
332
|
cancel: {
|
|
332
333
|
schedule: '计划取消',
|
|
333
334
|
title: '取消订阅',
|
|
@@ -356,12 +357,12 @@ export default flat({
|
|
|
356
357
|
custom: '暂停到自定义日期',
|
|
357
358
|
},
|
|
358
359
|
behavior: {
|
|
359
|
-
title: '
|
|
360
|
-
keep_as_draft: '
|
|
360
|
+
title: '账单行为',
|
|
361
|
+
keep_as_draft: '保留账单为草稿',
|
|
361
362
|
keep_as_draft_tip: '对于目前提供服务但等待收款的企业。',
|
|
362
|
-
mark_uncollectible: '
|
|
363
|
+
mark_uncollectible: '将账单标记为不可收款',
|
|
363
364
|
mark_uncollectible_tip: '对于目前提供免费服务的企业。',
|
|
364
|
-
void: '
|
|
365
|
+
void: '作废账单',
|
|
365
366
|
voidTip: '对于目前不提供服务的企业。',
|
|
366
367
|
},
|
|
367
368
|
until: {
|
|
@@ -376,6 +377,9 @@ export default flat({
|
|
|
376
377
|
vary: '随使用情况变化',
|
|
377
378
|
used: '已使用单位',
|
|
378
379
|
},
|
|
380
|
+
batchPay: {
|
|
381
|
+
button: '批量付款',
|
|
382
|
+
},
|
|
379
383
|
},
|
|
380
384
|
customer: {
|
|
381
385
|
view: '查看客户',
|
|
@@ -387,7 +391,7 @@ export default flat({
|
|
|
387
391
|
name: '名称',
|
|
388
392
|
email: '电子邮件',
|
|
389
393
|
phone: '电话',
|
|
390
|
-
invoicePrefix: '
|
|
394
|
+
invoicePrefix: '账单前缀',
|
|
391
395
|
balance: '余额 ({currency})',
|
|
392
396
|
address: {
|
|
393
397
|
label: '地址',
|
|
@@ -132,6 +132,16 @@ export default function InvoiceDetail(props: { id: string }) {
|
|
|
132
132
|
label={t('admin.paymentCurrency.name')}
|
|
133
133
|
value={<Currency logo={data.paymentCurrency.logo} name={data.paymentCurrency.symbol} />}
|
|
134
134
|
/>
|
|
135
|
+
{data.subscription && (
|
|
136
|
+
<InfoRow
|
|
137
|
+
label={t('admin.subscription.name')}
|
|
138
|
+
value={
|
|
139
|
+
<Link to={`/admin/billing/${data.subscription.id}`}>
|
|
140
|
+
{data.subscription.description || data.subscription.id}
|
|
141
|
+
</Link>
|
|
142
|
+
}
|
|
143
|
+
/>
|
|
144
|
+
)}
|
|
135
145
|
</Stack>
|
|
136
146
|
</Box>
|
|
137
147
|
<Box className="section">
|
|
@@ -41,7 +41,7 @@ export default function PaymentIndex() {
|
|
|
41
41
|
// @ts-ignore
|
|
42
42
|
const TabComponent = pages[page] || pages.intents;
|
|
43
43
|
const tabs = [
|
|
44
|
-
{ label: t('admin.
|
|
44
|
+
{ label: t('admin.paymentIntent.list'), value: 'intents' },
|
|
45
45
|
{ label: t('admin.refunds'), value: 'refunds' },
|
|
46
46
|
{ label: t('admin.paymentLinks'), value: 'links' },
|
|
47
47
|
];
|
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
|
|
2
2
|
import Toast from '@arcblock/ux/lib/Toast';
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
CustomerInvoiceList,
|
|
5
|
+
PaymentProvider,
|
|
6
|
+
formatError,
|
|
7
|
+
getPrefix,
|
|
8
|
+
usePaymentContext,
|
|
9
|
+
} from '@blocklet/payment-react';
|
|
4
10
|
import type { TCustomerExpanded } from '@blocklet/payment-types';
|
|
5
11
|
import { ArrowBackOutlined } from '@mui/icons-material';
|
|
6
12
|
import { Alert, Box, Button, CircularProgress, Stack, Typography } from '@mui/material';
|
|
@@ -18,7 +24,7 @@ const fetchData = (): Promise<TCustomerExpanded> => {
|
|
|
18
24
|
return api.get('/api/customers/me').then((res) => res.data);
|
|
19
25
|
};
|
|
20
26
|
|
|
21
|
-
export
|
|
27
|
+
export function CustomerInvoicePastDue() {
|
|
22
28
|
const { t } = useLocaleContext();
|
|
23
29
|
const { events } = useSessionContext();
|
|
24
30
|
const { connect } = usePaymentContext();
|
|
@@ -49,12 +55,13 @@ export default function CustomerInvoicePastDue() {
|
|
|
49
55
|
}
|
|
50
56
|
|
|
51
57
|
const subscriptionId = params.get('subscription') || '';
|
|
58
|
+
const currencyId = params.get('currency') || '';
|
|
52
59
|
const handleBatchPay = () => {
|
|
53
60
|
connect.open({
|
|
54
61
|
containerEl: undefined as unknown as Element,
|
|
55
62
|
action: 'collect-batch',
|
|
56
63
|
prefix: joinURL(window.location.origin, getPrefix(), '/api/did'),
|
|
57
|
-
extraParams: { subscriptionId },
|
|
64
|
+
extraParams: { subscriptionId, currencyId },
|
|
58
65
|
onSuccess: () => {
|
|
59
66
|
connect.close();
|
|
60
67
|
},
|
|
@@ -83,9 +90,9 @@ export default function CustomerInvoicePastDue() {
|
|
|
83
90
|
<Alert severity="info">{t('payment.customer.pastDue.warning')}</Alert>
|
|
84
91
|
<Box className="section">
|
|
85
92
|
<SectionHeader title={t('payment.customer.pastDue.invoices')} mb={0}>
|
|
86
|
-
{subscriptionId &&
|
|
87
|
-
<Button size="small" variant="contained" color="
|
|
88
|
-
{t('
|
|
93
|
+
{subscriptionId && currencyId && (
|
|
94
|
+
<Button size="small" variant="contained" color="info" onClick={handleBatchPay}>
|
|
95
|
+
{t('admin.subscription.batchPay.button')}
|
|
89
96
|
</Button>
|
|
90
97
|
)}
|
|
91
98
|
</SectionHeader>
|
|
@@ -93,9 +100,11 @@ export default function CustomerInvoicePastDue() {
|
|
|
93
100
|
<CustomerInvoiceList
|
|
94
101
|
customer_id={data.id}
|
|
95
102
|
subscription_id={subscriptionId}
|
|
103
|
+
currency_id={currencyId}
|
|
96
104
|
pageSize={100}
|
|
97
105
|
status="uncollectible"
|
|
98
106
|
target="_blank"
|
|
107
|
+
action="pay"
|
|
99
108
|
/>
|
|
100
109
|
</Box>
|
|
101
110
|
</Box>
|
|
@@ -109,3 +118,13 @@ const Root = styled(Stack)`
|
|
|
109
118
|
text-decoration: underline;
|
|
110
119
|
}
|
|
111
120
|
`;
|
|
121
|
+
|
|
122
|
+
export default function PastDueWrapper() {
|
|
123
|
+
const { session, connectApi } = useSessionContext();
|
|
124
|
+
|
|
125
|
+
return (
|
|
126
|
+
<PaymentProvider session={session} connect={connectApi}>
|
|
127
|
+
<CustomerInvoicePastDue />
|
|
128
|
+
</PaymentProvider>
|
|
129
|
+
);
|
|
130
|
+
}
|
|
@@ -60,7 +60,7 @@ export default function CustomerSubscriptionDetail() {
|
|
|
60
60
|
<SubscriptionDescription subscription={data} variant="h5" />
|
|
61
61
|
<SubscriptionStatus subscription={data} sx={{ ml: 1 }} />
|
|
62
62
|
</Stack>
|
|
63
|
-
<SubscriptionActions subscription={data} onChange={() => refresh()}
|
|
63
|
+
<SubscriptionActions subscription={data} onChange={() => refresh()} showExtra />
|
|
64
64
|
</Stack>
|
|
65
65
|
<Stack
|
|
66
66
|
className="section-body"
|