payment-kit 1.15.1 → 1.15.3
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/crons/payment-stat.ts +1 -0
- package/api/src/index.ts +2 -2
- package/api/src/integrations/arcblock/stake.ts +17 -10
- package/api/src/libs/auth.ts +3 -2
- package/api/src/libs/notification/template/customer-reward-succeeded.ts +15 -8
- package/api/src/libs/notification/template/one-time-payment-succeeded.ts +1 -30
- package/api/src/libs/notification/template/subscription-canceled.ts +45 -23
- package/api/src/libs/notification/template/subscription-refund-succeeded.ts +130 -47
- package/api/src/libs/notification/template/subscription-renewed.ts +10 -2
- package/api/src/libs/notification/template/subscription-stake-slash-succeeded.ts +228 -0
- package/api/src/libs/notification/template/subscription-succeeded.ts +2 -2
- package/api/src/libs/notification/template/subscription-trial-start.ts +7 -10
- package/api/src/libs/notification/template/subscription-trial-will-end.ts +13 -5
- package/api/src/libs/notification/template/subscription-will-renew.ts +41 -29
- package/api/src/libs/payment.ts +53 -1
- package/api/src/libs/subscription.ts +43 -0
- package/api/src/locales/en.ts +24 -0
- package/api/src/locales/zh.ts +22 -0
- package/api/src/queues/invoice.ts +1 -1
- package/api/src/queues/notification.ts +9 -0
- package/api/src/queues/payment.ts +17 -0
- package/api/src/routes/checkout-sessions.ts +13 -1
- package/api/src/routes/payment-stats.ts +3 -3
- package/api/src/routes/subscriptions.ts +26 -6
- package/api/src/store/migrations/20240905-index.ts +100 -0
- package/api/src/store/models/subscription.ts +1 -0
- package/api/tests/libs/payment.spec.ts +168 -0
- package/blocklet.yml +1 -1
- package/package.json +10 -10
- package/src/components/balance-list.tsx +2 -2
- package/src/components/invoice/list.tsx +2 -2
- package/src/components/invoice/table.tsx +1 -1
- package/src/components/payment-intent/list.tsx +1 -1
- package/src/components/payouts/list.tsx +1 -1
- package/src/components/refund/list.tsx +2 -2
- package/src/components/subscription/actions/cancel.tsx +41 -13
- package/src/components/subscription/actions/index.tsx +11 -8
- package/src/components/subscription/actions/slash-stake.tsx +52 -0
- package/src/locales/en.tsx +1 -0
- package/src/locales/zh.tsx +1 -0
- package/src/pages/admin/billing/invoices/detail.tsx +2 -2
- package/src/pages/customer/refund/list.tsx +1 -1
- package/src/pages/customer/subscription/detail.tsx +1 -1
|
@@ -114,7 +114,7 @@ export const handleInvoice = async (job: InvoiceJob) => {
|
|
|
114
114
|
});
|
|
115
115
|
paymentIntent = await PaymentIntent.findByPk(invoice.payment_intent_id);
|
|
116
116
|
if (paymentIntent && paymentIntent.isImmutable() === false) {
|
|
117
|
-
await paymentIntent.update({ status: 'requires_capture' });
|
|
117
|
+
await paymentIntent.update({ status: 'requires_capture', customer_id: invoice.customer_id });
|
|
118
118
|
}
|
|
119
119
|
} else {
|
|
120
120
|
const descriptionMap: any = {
|
|
@@ -51,6 +51,10 @@ import {
|
|
|
51
51
|
SubscriptionWillRenewEmailTemplate,
|
|
52
52
|
SubscriptionWillRenewEmailTemplateOptions,
|
|
53
53
|
} from '../libs/notification/template/subscription-will-renew';
|
|
54
|
+
import {
|
|
55
|
+
SubscriptionStakeSlashSucceededEmailTemplate,
|
|
56
|
+
SubscriptionStakeSlashSucceededEmailTemplateOptions,
|
|
57
|
+
} from '../libs/notification/template/subscription-stake-slash-succeeded';
|
|
54
58
|
import createQueue from '../libs/queue';
|
|
55
59
|
import { CheckoutSession, EventType, Invoice, PaymentLink, Refund, Subscription } from '../store/models';
|
|
56
60
|
|
|
@@ -105,6 +109,11 @@ function getNotificationTemplate(job: NotificationQueueJob): BaseEmailTemplate {
|
|
|
105
109
|
if (job.type === 'customer.reward.succeeded') {
|
|
106
110
|
return new CustomerRewardSucceededEmailTemplate(job.options as CustomerRewardSucceededEmailTemplateOptions);
|
|
107
111
|
}
|
|
112
|
+
if (job.type === 'subscription.stake.slash.succeeded') {
|
|
113
|
+
return new SubscriptionStakeSlashSucceededEmailTemplate(
|
|
114
|
+
job.options as SubscriptionStakeSlashSucceededEmailTemplateOptions
|
|
115
|
+
);
|
|
116
|
+
}
|
|
108
117
|
|
|
109
118
|
throw new Error(`Unknown job type: ${job.type}`);
|
|
110
119
|
}
|
|
@@ -33,6 +33,7 @@ import { Price } from '../store/models/price';
|
|
|
33
33
|
import { Subscription } from '../store/models/subscription';
|
|
34
34
|
import { SubscriptionItem } from '../store/models/subscription-item';
|
|
35
35
|
import type { PaymentError, PaymentSettings } from '../store/models/types';
|
|
36
|
+
import { notificationQueue } from './notification';
|
|
36
37
|
|
|
37
38
|
type PaymentJob = {
|
|
38
39
|
paymentIntentId: string;
|
|
@@ -474,6 +475,22 @@ const handleStakeSlash = async (
|
|
|
474
475
|
},
|
|
475
476
|
});
|
|
476
477
|
await handlePaymentSucceed(paymentIntent, true);
|
|
478
|
+
const jobId = `${paymentIntent.id}-${subscription.id}`;
|
|
479
|
+
const job = await notificationQueue.get(jobId);
|
|
480
|
+
if (job) {
|
|
481
|
+
return;
|
|
482
|
+
}
|
|
483
|
+
notificationQueue.push({
|
|
484
|
+
id: jobId,
|
|
485
|
+
job: {
|
|
486
|
+
type: 'subscription.stake.slash.succeeded',
|
|
487
|
+
options: {
|
|
488
|
+
paymentIntentId: paymentIntent.id,
|
|
489
|
+
subscriptionId: subscription.id,
|
|
490
|
+
invoiceId: invoice.id,
|
|
491
|
+
},
|
|
492
|
+
},
|
|
493
|
+
});
|
|
477
494
|
};
|
|
478
495
|
|
|
479
496
|
export const handlePayment = async (job: PaymentJob) => {
|
|
@@ -47,7 +47,13 @@ import {
|
|
|
47
47
|
import { CHECKOUT_SESSION_TTL, formatAmountPrecisionLimit, formatMetadata, getDataObjectFromQuery } from '../libs/util';
|
|
48
48
|
import { invoiceQueue } from '../queues/invoice';
|
|
49
49
|
import { paymentQueue } from '../queues/payment';
|
|
50
|
-
import
|
|
50
|
+
import {
|
|
51
|
+
Invoice,
|
|
52
|
+
type LineItem,
|
|
53
|
+
type SubscriptionData,
|
|
54
|
+
type TPriceExpanded,
|
|
55
|
+
type TProductExpanded,
|
|
56
|
+
} from '../store/models';
|
|
51
57
|
import { CheckoutSession } from '../store/models/checkout-session';
|
|
52
58
|
import { Customer } from '../store/models/customer';
|
|
53
59
|
import { PaymentCurrency } from '../store/models/payment-currency';
|
|
@@ -814,6 +820,12 @@ router.put('/:id/submit', user, ensureCheckoutSessionOpen, async (req, res) => {
|
|
|
814
820
|
session: checkoutSession.id,
|
|
815
821
|
items: items.map((x) => x.id),
|
|
816
822
|
});
|
|
823
|
+
const invoice = await Invoice.findByPk(subscription?.latest_invoice_id);
|
|
824
|
+
if (invoice && invoice.customer_id !== customer.id) {
|
|
825
|
+
invoice.update({
|
|
826
|
+
customer_id: customer.id,
|
|
827
|
+
});
|
|
828
|
+
}
|
|
817
829
|
} else {
|
|
818
830
|
const recoveredFromId = checkoutSession.subscription_data?.recovered_from || '';
|
|
819
831
|
const recoveredFrom = recoveredFromId ? await Subscription.findByPk(recoveredFromId) : null;
|
|
@@ -47,7 +47,7 @@ router.get('/', auth, async (req, res) => {
|
|
|
47
47
|
if (query.end) {
|
|
48
48
|
where.timestamp[Op.lt] = query.end;
|
|
49
49
|
} else {
|
|
50
|
-
where.timestamp[Op.lt] = dayjs().
|
|
50
|
+
where.timestamp[Op.lt] = dayjs().endOf('day').unix();
|
|
51
51
|
}
|
|
52
52
|
|
|
53
53
|
try {
|
|
@@ -90,7 +90,7 @@ async function getCurrencyLinks(livemode: boolean) {
|
|
|
90
90
|
});
|
|
91
91
|
|
|
92
92
|
return items.reduce((acc, item: any) => {
|
|
93
|
-
if (item.payment_method
|
|
93
|
+
if (item.payment_method?.type === 'arcblock') {
|
|
94
94
|
acc[item.id] = joinURL(
|
|
95
95
|
item.payment_method.settings.arcblock?.explorer_host,
|
|
96
96
|
'accounts',
|
|
@@ -98,7 +98,7 @@ async function getCurrencyLinks(livemode: boolean) {
|
|
|
98
98
|
'tokens'
|
|
99
99
|
);
|
|
100
100
|
}
|
|
101
|
-
if (item.payment_method
|
|
101
|
+
if (item.payment_method?.type === 'ethereum') {
|
|
102
102
|
acc[item.id] = joinURL(item.payment_method.settings.ethereum?.explorer_host, 'address', ethWallet.address);
|
|
103
103
|
}
|
|
104
104
|
return acc;
|
|
@@ -223,6 +223,7 @@ router.get('/:id', authPortal, async (req, res) => {
|
|
|
223
223
|
});
|
|
224
224
|
|
|
225
225
|
const CommentSchema = Joi.string().max(200).empty('').optional();
|
|
226
|
+
const SlashStakeSchema = Joi.string().max(200).required();
|
|
226
227
|
|
|
227
228
|
router.put('/:id/cancel', authPortal, async (req, res) => {
|
|
228
229
|
const { error: commentError } = CommentSchema.validate(req.body?.comment);
|
|
@@ -230,6 +231,15 @@ router.put('/:id/cancel', authPortal, async (req, res) => {
|
|
|
230
231
|
return res.status(400).json({ error: `comment invalid: ${commentError.message}` });
|
|
231
232
|
}
|
|
232
233
|
|
|
234
|
+
const slashStake = req.body?.requestByAdmin && req.body?.staking === 'slash';
|
|
235
|
+
|
|
236
|
+
if (slashStake) {
|
|
237
|
+
const { error: slashReasonError } = SlashStakeSchema.validate(req.body?.slashReason);
|
|
238
|
+
if (slashReasonError) {
|
|
239
|
+
return res.status(400).json({ error: `slash reason invalid: ${slashReasonError.message}` });
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
233
243
|
const subscription = await Subscription.findByPk(req.params.id);
|
|
234
244
|
logger.info('subscription cancel request', { ...req.params, ...req.body });
|
|
235
245
|
|
|
@@ -248,6 +258,7 @@ router.put('/:id/cancel', authPortal, async (req, res) => {
|
|
|
248
258
|
comment = '',
|
|
249
259
|
reason = 'payment_disputed',
|
|
250
260
|
staking = 'none',
|
|
261
|
+
slashReason = 'admin slash',
|
|
251
262
|
} = req.body;
|
|
252
263
|
if (at === 'custom' && dayjs(time).unix() < dayjs().unix()) {
|
|
253
264
|
return res.status(400).json({ error: 'cancel at must be a future timestamp' });
|
|
@@ -258,15 +269,16 @@ router.put('/:id/cancel', authPortal, async (req, res) => {
|
|
|
258
269
|
if ((requestByAdmin && staking === 'proration') || req.body?.cancel_from === 'customer') {
|
|
259
270
|
canReturnStake = true;
|
|
260
271
|
}
|
|
261
|
-
const
|
|
272
|
+
const haveStake = !!subscription.payment_details?.arcblock?.staking?.tx_hash;
|
|
262
273
|
// update cancel at
|
|
263
274
|
const updates: Partial<Subscription> = {
|
|
264
275
|
cancelation_details: {
|
|
265
276
|
comment: comment || `Requested by ${req.user?.role}:${req.user?.did}`,
|
|
266
277
|
reason: reason || 'payment_disputed',
|
|
267
278
|
feedback: feedback || 'other',
|
|
268
|
-
return_stake: canReturnStake,
|
|
269
|
-
slash_stake: slashStake,
|
|
279
|
+
return_stake: canReturnStake && haveStake,
|
|
280
|
+
slash_stake: slashStake && haveStake,
|
|
281
|
+
slash_reason: slashReason,
|
|
270
282
|
},
|
|
271
283
|
};
|
|
272
284
|
const now = dayjs().unix() + 3;
|
|
@@ -278,8 +290,9 @@ router.put('/:id/cancel', authPortal, async (req, res) => {
|
|
|
278
290
|
reason: 'cancellation_requested',
|
|
279
291
|
feedback,
|
|
280
292
|
comment,
|
|
281
|
-
return_stake: canReturnStake,
|
|
282
|
-
slash_stake: slashStake,
|
|
293
|
+
return_stake: canReturnStake && haveStake,
|
|
294
|
+
slash_stake: slashStake && haveStake,
|
|
295
|
+
slash_reason: slashReason,
|
|
283
296
|
};
|
|
284
297
|
updates.canceled_at = now;
|
|
285
298
|
if (inTrialing) {
|
|
@@ -1215,7 +1228,9 @@ router.get('/:id/proration', authPortal, async (req, res) => {
|
|
|
1215
1228
|
|
|
1216
1229
|
const anchor = req.query.time ? dayjs(req.query.time as any).unix() : dayjs().unix();
|
|
1217
1230
|
const result = await getSubscriptionRefundSetup(subscription, anchor);
|
|
1218
|
-
|
|
1231
|
+
if (result.total === '0') {
|
|
1232
|
+
return res.json(null);
|
|
1233
|
+
}
|
|
1219
1234
|
return res.json({
|
|
1220
1235
|
total: result.total,
|
|
1221
1236
|
latest: invoice?.total,
|
|
@@ -1611,6 +1626,10 @@ router.get('/:id/upcoming', authPortal, async (req, res) => {
|
|
|
1611
1626
|
|
|
1612
1627
|
// slash stake
|
|
1613
1628
|
router.put('/:id/slash-stake', auth, async (req, res) => {
|
|
1629
|
+
const { error: slashReasonError } = SlashStakeSchema.validate(req.body?.slashReason);
|
|
1630
|
+
if (slashReasonError) {
|
|
1631
|
+
return res.status(400).json({ error: `slash reason invalid: ${slashReasonError.message}` });
|
|
1632
|
+
}
|
|
1614
1633
|
const subscription = await Subscription.findByPk(req.params.id);
|
|
1615
1634
|
if (!subscription) {
|
|
1616
1635
|
return res.status(404).json({ error: 'Subscription not found' });
|
|
@@ -1636,6 +1655,7 @@ router.put('/:id/slash-stake', auth, async (req, res) => {
|
|
|
1636
1655
|
cancelation_details: {
|
|
1637
1656
|
...subscription.cancelation_details,
|
|
1638
1657
|
slash_stake: true,
|
|
1658
|
+
slash_reason: req.body.slashReason,
|
|
1639
1659
|
},
|
|
1640
1660
|
});
|
|
1641
1661
|
const result = await slashStakeQueue.pushAndWait({
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import type { Migration } from '../migrate';
|
|
2
|
+
|
|
3
|
+
export const up: Migration = async ({ context: queryInterface }) => {
|
|
4
|
+
try {
|
|
5
|
+
await queryInterface.addIndex('subscriptions', ['current_period_start', 'current_period_end'], {
|
|
6
|
+
name: 'idx_subscription_period',
|
|
7
|
+
});
|
|
8
|
+
await queryInterface.addIndex('subscriptions', ['status'], {
|
|
9
|
+
name: 'idx_subscription_status',
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
await queryInterface.addIndex('subscription_items', ['subscription_id', 'price_id'], {
|
|
13
|
+
name: 'idx_subscription_item_subscription_id_price_id',
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
await queryInterface.addIndex('invoices', ['status', 'collection_method'], {
|
|
17
|
+
name: 'idx_invoice_status_collection',
|
|
18
|
+
});
|
|
19
|
+
await queryInterface.addIndex('invoices', ['subscription_id'], {
|
|
20
|
+
name: 'idx_invoice_subscription_id',
|
|
21
|
+
});
|
|
22
|
+
await queryInterface.addIndex('invoices', ['currency_id'], {
|
|
23
|
+
name: 'idx_invoice_currency_id',
|
|
24
|
+
});
|
|
25
|
+
await queryInterface.addIndex('invoices', ['customer_id'], {
|
|
26
|
+
name: 'idx_invoice_customer_id',
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
await queryInterface.addIndex('payment_intents', ['invoice_id'], {
|
|
30
|
+
name: 'idx_payment_intent_invoice_id',
|
|
31
|
+
});
|
|
32
|
+
await queryInterface.addIndex('payment_intents', ['customer_id'], {
|
|
33
|
+
name: 'idx_payment_intent_customer_id',
|
|
34
|
+
});
|
|
35
|
+
await queryInterface.addIndex('payment_intents', ['currency_id'], {
|
|
36
|
+
name: 'idx_payment_intent_currency_id',
|
|
37
|
+
});
|
|
38
|
+
await queryInterface.addIndex('payment_intents', ['status', 'updated_at'], {
|
|
39
|
+
name: 'idx_payment_intent_status_updated_at',
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
await queryInterface.addIndex('webhook_attempts', ['webhook_endpoint_id'], {
|
|
43
|
+
name: 'idx_webhook_attempts_webhook_endpoint_id',
|
|
44
|
+
});
|
|
45
|
+
await queryInterface.addIndex('webhook_attempts', ['event_id'], {
|
|
46
|
+
name: 'idx_webhook_attempts_event_id',
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
await queryInterface.addIndex('usage_records', ['subscription_item_id', 'timestamp'], {
|
|
50
|
+
name: 'idx_usage_records_subscription_item_id_timestamp',
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
await queryInterface.addIndex('webhook_endpoints', ['status', 'livemode'], {
|
|
54
|
+
name: 'idx_webhook_endpoint_status_livemode',
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
await queryInterface.addIndex('payment_stats', ['timestamp', 'currency_id'], {
|
|
58
|
+
name: 'idx_payment_stats_timestamp_currency_id',
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
await queryInterface.addIndex('payouts', ['updated_at', 'status'], {
|
|
62
|
+
name: 'idx_payouts_updated_at_status',
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
await queryInterface.addIndex('refunds', ['status', 'type', 'updated_at'], {
|
|
66
|
+
name: 'idx_refunds_status_type_updated_at',
|
|
67
|
+
});
|
|
68
|
+
await queryInterface.addIndex('refunds', ['subscription_id', 'type'], {
|
|
69
|
+
name: 'idx_refunds_subscription_id_type',
|
|
70
|
+
});
|
|
71
|
+
await queryInterface.addIndex('refunds', ['payment_intent_id', 'type'], {
|
|
72
|
+
name: 'idx_refunds_payment_intent_id_type',
|
|
73
|
+
});
|
|
74
|
+
} catch (error) {
|
|
75
|
+
console.error('Failed to create indexes', error);
|
|
76
|
+
throw error;
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
export const down: Migration = async ({ context: queryInterface }) => {
|
|
80
|
+
await queryInterface.removeIndex('subscriptions', 'idx_subscription_period');
|
|
81
|
+
await queryInterface.removeIndex('subscriptions', 'idx_subscription_status');
|
|
82
|
+
await queryInterface.removeIndex('subscription_items', 'idx_subscription_item_subscription_id_price_id');
|
|
83
|
+
await queryInterface.removeIndex('invoices', 'idx_invoice_status_collection');
|
|
84
|
+
await queryInterface.removeIndex('invoices', 'idx_invoice_subscription_id');
|
|
85
|
+
await queryInterface.removeIndex('invoices', 'idx_invoice_currency_id');
|
|
86
|
+
await queryInterface.removeIndex('invoices', 'idx_invoice_customer_id');
|
|
87
|
+
await queryInterface.removeIndex('payment_intents', 'idx_payment_intent_invoice_id');
|
|
88
|
+
await queryInterface.removeIndex('payment_intents', 'idx_payment_intent_customer_id');
|
|
89
|
+
await queryInterface.removeIndex('payment_intents', 'idx_payment_intent_currency_id');
|
|
90
|
+
await queryInterface.removeIndex('payment_intents', 'idx_payment_intent_status_updated_at');
|
|
91
|
+
await queryInterface.removeIndex('webhook_attempts', 'idx_webhook_attempts_webhook_endpoint_id');
|
|
92
|
+
await queryInterface.removeIndex('webhook_attempts', 'idx_webhook_attempts_event_id');
|
|
93
|
+
await queryInterface.removeIndex('usage_records', 'idx_usage_records_subscription_item_id_timestamp');
|
|
94
|
+
await queryInterface.removeIndex('webhook_endpoints', 'idx_webhook_endpoint_status_livemode');
|
|
95
|
+
await queryInterface.removeIndex('payment_stats', 'idx_payment_stats_timestamp_currency_id');
|
|
96
|
+
await queryInterface.removeIndex('payouts', 'idx_payouts_updated_at_status');
|
|
97
|
+
await queryInterface.removeIndex('refunds', 'idx_refunds_status_type_updated_at');
|
|
98
|
+
await queryInterface.removeIndex('refunds', 'idx_refunds_subscription_id_type');
|
|
99
|
+
await queryInterface.removeIndex('refunds', 'idx_refunds_payment_intent_id_type');
|
|
100
|
+
};
|
|
@@ -63,6 +63,7 @@ export class Subscription extends Model<InferAttributes<Subscription>, InferCrea
|
|
|
63
63
|
reason: LiteralUnion<'cancellation_requested' | 'payment_disputed' | 'payment_failed' | 'stake_revoked', string>;
|
|
64
64
|
return_stake?: boolean;
|
|
65
65
|
slash_stake?: boolean;
|
|
66
|
+
slash_reason?: string;
|
|
66
67
|
};
|
|
67
68
|
|
|
68
69
|
declare billing_cycle_anchor: number;
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import { getPaymentAmountForCycleSubscription } from '../../src/libs/payment';
|
|
2
|
+
import { SubscriptionItem, Price, UsageRecord } from '../../src/store/models'; // 假设这些模型在 models 文件中
|
|
3
|
+
import { getSubscriptionCycleSetup, getSubscriptionCycleAmount } from '../../src/libs/subscription'; // 假设这些工具函数在 utils 文件中
|
|
4
|
+
|
|
5
|
+
jest.mock('../../src/store/models');
|
|
6
|
+
jest.mock('../../src/libs/subscription');
|
|
7
|
+
|
|
8
|
+
describe('getPaymentAmountForCycleSubscription', () => {
|
|
9
|
+
const subscription = {
|
|
10
|
+
id: 'sub_123456789012345678901234',
|
|
11
|
+
livemode: true,
|
|
12
|
+
currency_id: 'tba',
|
|
13
|
+
customer_id: 'cus_123456789012345678',
|
|
14
|
+
current_period_end: 1627689600,
|
|
15
|
+
current_period_start: 1625097600,
|
|
16
|
+
default_payment_method_id: 'pm_123456789012345678901234',
|
|
17
|
+
description: 'Test subscription',
|
|
18
|
+
latest_invoice_id: 'in_123456789012345678901234',
|
|
19
|
+
metadata: { key: 'value' },
|
|
20
|
+
pending_setup_intent: 'seti_123456789012345678901234',
|
|
21
|
+
pending_update: {
|
|
22
|
+
billing_cycle_anchor: 1627689600,
|
|
23
|
+
expires_at: 1627689600,
|
|
24
|
+
subscription_items: [],
|
|
25
|
+
trial_end: 1627689600,
|
|
26
|
+
},
|
|
27
|
+
status: 'active',
|
|
28
|
+
cancel_at_period_end: false,
|
|
29
|
+
cancel_at: 1627689600,
|
|
30
|
+
canceled_at: 1627689600,
|
|
31
|
+
cancelation_details: {
|
|
32
|
+
comment: 'Customer requested cancellation',
|
|
33
|
+
feedback: 'too_expensive',
|
|
34
|
+
reason: 'cancellation_requested',
|
|
35
|
+
return_stake: true,
|
|
36
|
+
slash_stake: false,
|
|
37
|
+
},
|
|
38
|
+
billing_cycle_anchor: 1625097600,
|
|
39
|
+
billing_thresholds: {
|
|
40
|
+
amount_gte: 1000,
|
|
41
|
+
stake_gte: 100,
|
|
42
|
+
reset_billing_cycle_anchor: true,
|
|
43
|
+
},
|
|
44
|
+
collection_method: 'charge_automatically',
|
|
45
|
+
days_until_due: 30,
|
|
46
|
+
days_until_cancel: 7,
|
|
47
|
+
discount_id: 'di_123456789012345678901234',
|
|
48
|
+
next_pending_invoice_item_invoice: 1627689600,
|
|
49
|
+
pause_collection: {
|
|
50
|
+
behavior: 'keep_as_draft',
|
|
51
|
+
resumes_at: 1627689600,
|
|
52
|
+
},
|
|
53
|
+
payment_settings: {
|
|
54
|
+
payment_method_options: {
|
|
55
|
+
card: {
|
|
56
|
+
request_three_d_secure: 'any',
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
payment_method_types: ['card'],
|
|
60
|
+
},
|
|
61
|
+
pending_invoice_item_interval: {
|
|
62
|
+
interval: 'month',
|
|
63
|
+
interval_count: 1,
|
|
64
|
+
},
|
|
65
|
+
schedule_id: 'sch_123456789012345678901234',
|
|
66
|
+
ended_at: 1627689600,
|
|
67
|
+
start_date: 1625097600,
|
|
68
|
+
trial_end: 1627689600,
|
|
69
|
+
trial_start: 1625097600,
|
|
70
|
+
trial_settings: {
|
|
71
|
+
end_behavior: {
|
|
72
|
+
missing_payment_method: 'cancel',
|
|
73
|
+
},
|
|
74
|
+
},
|
|
75
|
+
payment_details: {
|
|
76
|
+
tx_hash: '0x1234567890abcdef',
|
|
77
|
+
},
|
|
78
|
+
proration_behavior: 'always_invoice',
|
|
79
|
+
payment_behavior: 'allow_incomplete',
|
|
80
|
+
service_actions: [
|
|
81
|
+
{
|
|
82
|
+
action: 'activate',
|
|
83
|
+
service: 'service_1',
|
|
84
|
+
},
|
|
85
|
+
],
|
|
86
|
+
_from: 'sub_098765432109876543210987',
|
|
87
|
+
created_at: new Date(),
|
|
88
|
+
updated_at: new Date(),
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
const paymentCurrency = {
|
|
92
|
+
id: 'usd',
|
|
93
|
+
decimal: 18,
|
|
94
|
+
symbol: 'TBA',
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
const subscriptionItems = [
|
|
98
|
+
{ id: 'item_1', price_id: 'price_1', quantity: 1 },
|
|
99
|
+
{ id: 'item_2', price_id: 'price_2', quantity: 2 },
|
|
100
|
+
];
|
|
101
|
+
|
|
102
|
+
const expandedItems = [
|
|
103
|
+
{
|
|
104
|
+
id: 'item_1',
|
|
105
|
+
// @ts-ignore
|
|
106
|
+
price: new Price({
|
|
107
|
+
id: 'price_1',
|
|
108
|
+
active: true,
|
|
109
|
+
product_id: 'prod_1',
|
|
110
|
+
livemode: false,
|
|
111
|
+
type: 'recurring',
|
|
112
|
+
unit_amount: '100000000000000000',
|
|
113
|
+
currency_id: 'tba',
|
|
114
|
+
recurring: {
|
|
115
|
+
usage_type: 'licensed',
|
|
116
|
+
interval: 'month',
|
|
117
|
+
interval_count: 1,
|
|
118
|
+
},
|
|
119
|
+
}),
|
|
120
|
+
quantity: 1,
|
|
121
|
+
},
|
|
122
|
+
{
|
|
123
|
+
id: 'item_2',
|
|
124
|
+
// @ts-ignore
|
|
125
|
+
price: new Price({
|
|
126
|
+
id: 'price_2',
|
|
127
|
+
active: true,
|
|
128
|
+
product_id: 'prod_2',
|
|
129
|
+
livemode: false,
|
|
130
|
+
type: 'recurring',
|
|
131
|
+
currency_id: 'tba',
|
|
132
|
+
unit_amount: '100000000000000000',
|
|
133
|
+
recurring: {
|
|
134
|
+
usage_type: 'metered',
|
|
135
|
+
interval: 'month',
|
|
136
|
+
interval_count: 1,
|
|
137
|
+
aggregate_usage: 'sum',
|
|
138
|
+
},
|
|
139
|
+
}),
|
|
140
|
+
quantity: 2,
|
|
141
|
+
},
|
|
142
|
+
];
|
|
143
|
+
|
|
144
|
+
beforeEach(() => {
|
|
145
|
+
(SubscriptionItem.findAll as jest.Mock).mockResolvedValue(subscriptionItems);
|
|
146
|
+
(Price.expand as jest.Mock).mockResolvedValue(expandedItems);
|
|
147
|
+
(UsageRecord.getSummary as jest.Mock).mockResolvedValue(10);
|
|
148
|
+
(getSubscriptionCycleSetup as jest.Mock).mockReturnValue({
|
|
149
|
+
period: { start: 1625097600, end: 1627689600 },
|
|
150
|
+
cycle: 2592000,
|
|
151
|
+
});
|
|
152
|
+
(getSubscriptionCycleAmount as jest.Mock).mockReturnValue({ total: '100000000000000000' });
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it('should return the total amount for the subscription cycle', async () => {
|
|
156
|
+
// @ts-ignore
|
|
157
|
+
const result = await getPaymentAmountForCycleSubscription(subscription, paymentCurrency);
|
|
158
|
+
expect(result).toBe(0.1);
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it('should return 0 if there are no subscription items', async () => {
|
|
162
|
+
(SubscriptionItem.findAll as jest.Mock).mockResolvedValue([]);
|
|
163
|
+
(Price.expand as jest.Mock).mockResolvedValue([]); // 确保 Price.expand 返回空数组
|
|
164
|
+
// @ts-ignore
|
|
165
|
+
const result = await getPaymentAmountForCycleSubscription(subscription, paymentCurrency);
|
|
166
|
+
expect(result).toBe(0);
|
|
167
|
+
});
|
|
168
|
+
});
|
package/blocklet.yml
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "payment-kit",
|
|
3
|
-
"version": "1.15.
|
|
3
|
+
"version": "1.15.3",
|
|
4
4
|
"scripts": {
|
|
5
5
|
"dev": "blocklet dev --open",
|
|
6
6
|
"eject": "vite eject",
|
|
@@ -45,17 +45,18 @@
|
|
|
45
45
|
"@abtnode/cron": "1.16.30",
|
|
46
46
|
"@arcblock/did": "^1.18.135",
|
|
47
47
|
"@arcblock/did-auth-storage-nedb": "^1.7.1",
|
|
48
|
-
"@arcblock/did-connect": "^2.10.
|
|
48
|
+
"@arcblock/did-connect": "^2.10.30",
|
|
49
49
|
"@arcblock/did-util": "^1.18.135",
|
|
50
50
|
"@arcblock/jwt": "^1.18.135",
|
|
51
|
-
"@arcblock/ux": "2.10.
|
|
51
|
+
"@arcblock/ux": "2.10.28",
|
|
52
52
|
"@arcblock/validator": "^1.18.135",
|
|
53
53
|
"@blocklet/js-sdk": "1.16.30",
|
|
54
54
|
"@blocklet/logger": "1.16.30",
|
|
55
|
-
"@blocklet/payment-react": "1.15.
|
|
55
|
+
"@blocklet/payment-react": "1.15.3",
|
|
56
56
|
"@blocklet/sdk": "1.16.30",
|
|
57
|
-
"@blocklet/ui-react": "^2.10.
|
|
58
|
-
"@blocklet/uploader": "^0.1.
|
|
57
|
+
"@blocklet/ui-react": "^2.10.30",
|
|
58
|
+
"@blocklet/uploader": "^0.1.29",
|
|
59
|
+
"@blocklet/xss": "^0.1.5",
|
|
59
60
|
"@mui/icons-material": "^5.16.6",
|
|
60
61
|
"@mui/lab": "^5.0.0-alpha.173",
|
|
61
62
|
"@mui/material": "^5.16.6",
|
|
@@ -83,7 +84,6 @@
|
|
|
83
84
|
"express": "^4.19.2",
|
|
84
85
|
"express-async-errors": "^3.1.1",
|
|
85
86
|
"express-history-api-fallback": "^2.2.1",
|
|
86
|
-
"express-xss-sanitizer": "^1.2.0",
|
|
87
87
|
"fastq": "^1.17.1",
|
|
88
88
|
"flat": "^5.0.2",
|
|
89
89
|
"google-libphonenumber": "^3.2.38",
|
|
@@ -118,7 +118,7 @@
|
|
|
118
118
|
"devDependencies": {
|
|
119
119
|
"@abtnode/types": "1.16.30",
|
|
120
120
|
"@arcblock/eslint-config-ts": "^0.3.2",
|
|
121
|
-
"@blocklet/payment-types": "1.15.
|
|
121
|
+
"@blocklet/payment-types": "1.15.3",
|
|
122
122
|
"@types/cookie-parser": "^1.4.7",
|
|
123
123
|
"@types/cors": "^2.8.17",
|
|
124
124
|
"@types/debug": "^4.1.12",
|
|
@@ -144,7 +144,7 @@
|
|
|
144
144
|
"typescript": "^4.9.5",
|
|
145
145
|
"vite": "^5.3.5",
|
|
146
146
|
"vite-node": "^2.0.4",
|
|
147
|
-
"vite-plugin-blocklet": "^0.9.
|
|
147
|
+
"vite-plugin-blocklet": "^0.9.4",
|
|
148
148
|
"vite-plugin-node-polyfills": "^0.21.0",
|
|
149
149
|
"vite-plugin-svgr": "^4.2.0",
|
|
150
150
|
"vite-tsconfig-paths": "^4.3.2",
|
|
@@ -160,5 +160,5 @@
|
|
|
160
160
|
"parser": "typescript"
|
|
161
161
|
}
|
|
162
162
|
},
|
|
163
|
-
"gitHead": "
|
|
163
|
+
"gitHead": "e9841f27db3993526f7871e2a0118d89c6958470"
|
|
164
164
|
}
|
|
@@ -29,14 +29,14 @@ export default function BalanceList(props: Props) {
|
|
|
29
29
|
return (
|
|
30
30
|
<Stack
|
|
31
31
|
key={currencyId}
|
|
32
|
-
sx={{ width: '100%', maxWidth: '
|
|
32
|
+
sx={{ width: '100%', maxWidth: '280px' }}
|
|
33
33
|
direction="row"
|
|
34
34
|
spacing={1}
|
|
35
35
|
alignItems="center">
|
|
36
36
|
{props?.showLogo && (
|
|
37
37
|
<Avatar src={currency.logo} alt={currency.symbol} style={{ width: '18px', height: '18px' }} />
|
|
38
38
|
)}
|
|
39
|
-
<Typography sx={{ width: '32px' }} color="text.secondary">
|
|
39
|
+
<Typography sx={{ width: '32px', minWidth: 100 }} color="text.secondary">
|
|
40
40
|
{currency.symbol}
|
|
41
41
|
</Typography>
|
|
42
42
|
<Typography sx={{ flex: 1, textAlign: 'right' }} color="text.primary">
|
|
@@ -148,7 +148,7 @@ export default function InvoiceList({
|
|
|
148
148
|
{
|
|
149
149
|
label: t('common.status'),
|
|
150
150
|
name: 'status',
|
|
151
|
-
width:
|
|
151
|
+
width: 80,
|
|
152
152
|
options: {
|
|
153
153
|
customBodyRenderLite: (_: string, index: number) => {
|
|
154
154
|
const item = data.list[index] as TInvoiceExpanded;
|
|
@@ -229,7 +229,7 @@ export default function InvoiceList({
|
|
|
229
229
|
columns.splice(3, 0, {
|
|
230
230
|
label: t('common.customer'),
|
|
231
231
|
name: 'customer_id',
|
|
232
|
-
width:
|
|
232
|
+
width: 80,
|
|
233
233
|
options: {
|
|
234
234
|
customBodyRenderLite: (_: string, index: number) => {
|
|
235
235
|
const item = data.list[index] as TInvoiceExpanded;
|
|
@@ -35,7 +35,7 @@ type InvoiceSummaryItem = {
|
|
|
35
35
|
};
|
|
36
36
|
|
|
37
37
|
export function getAppliedBalance(invoice: TInvoiceExpanded) {
|
|
38
|
-
if (invoice.paymentMethod
|
|
38
|
+
if (invoice.paymentMethod?.type === 'stripe') {
|
|
39
39
|
const starting = toBN(invoice.starting_balance || '0');
|
|
40
40
|
const ending = toBN(invoice.ending_balance || '0');
|
|
41
41
|
return ending.sub(starting).toString();
|
|
@@ -138,7 +138,7 @@ export default function PaymentList({ customer_id, invoice_id, features }: ListP
|
|
|
138
138
|
{
|
|
139
139
|
label: t('common.status'),
|
|
140
140
|
name: 'status',
|
|
141
|
-
width:
|
|
141
|
+
width: 80,
|
|
142
142
|
options: {
|
|
143
143
|
customBodyRenderLite: (_: string, index: number) => {
|
|
144
144
|
const item = data.list[index] as TPaymentIntentExpanded;
|
|
@@ -130,7 +130,7 @@ export default function PayoutList({ customer_id, payment_intent_id, status, fea
|
|
|
130
130
|
{
|
|
131
131
|
label: t('common.status'),
|
|
132
132
|
name: 'status',
|
|
133
|
-
width:
|
|
133
|
+
width: 80,
|
|
134
134
|
options: {
|
|
135
135
|
customBodyRenderLite: (_: string, index: number) => {
|
|
136
136
|
const item = data.list[index] as TPayoutExpanded;
|
|
@@ -151,7 +151,7 @@ export default function RefundList({
|
|
|
151
151
|
{
|
|
152
152
|
label: t('common.status'),
|
|
153
153
|
name: 'status',
|
|
154
|
-
width:
|
|
154
|
+
width: 80,
|
|
155
155
|
options: {
|
|
156
156
|
filter: true,
|
|
157
157
|
customBodyRenderLite: (_: string, index: number) => {
|
|
@@ -167,7 +167,7 @@ export default function RefundList({
|
|
|
167
167
|
{
|
|
168
168
|
label: t('common.type'),
|
|
169
169
|
name: 'type',
|
|
170
|
-
width:
|
|
170
|
+
width: 80,
|
|
171
171
|
options: {
|
|
172
172
|
filter: true,
|
|
173
173
|
customBodyRenderLite: (_: string, index: number) => {
|