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.
Files changed (43) hide show
  1. package/api/src/crons/payment-stat.ts +1 -0
  2. package/api/src/index.ts +2 -2
  3. package/api/src/integrations/arcblock/stake.ts +17 -10
  4. package/api/src/libs/auth.ts +3 -2
  5. package/api/src/libs/notification/template/customer-reward-succeeded.ts +15 -8
  6. package/api/src/libs/notification/template/one-time-payment-succeeded.ts +1 -30
  7. package/api/src/libs/notification/template/subscription-canceled.ts +45 -23
  8. package/api/src/libs/notification/template/subscription-refund-succeeded.ts +130 -47
  9. package/api/src/libs/notification/template/subscription-renewed.ts +10 -2
  10. package/api/src/libs/notification/template/subscription-stake-slash-succeeded.ts +228 -0
  11. package/api/src/libs/notification/template/subscription-succeeded.ts +2 -2
  12. package/api/src/libs/notification/template/subscription-trial-start.ts +7 -10
  13. package/api/src/libs/notification/template/subscription-trial-will-end.ts +13 -5
  14. package/api/src/libs/notification/template/subscription-will-renew.ts +41 -29
  15. package/api/src/libs/payment.ts +53 -1
  16. package/api/src/libs/subscription.ts +43 -0
  17. package/api/src/locales/en.ts +24 -0
  18. package/api/src/locales/zh.ts +22 -0
  19. package/api/src/queues/invoice.ts +1 -1
  20. package/api/src/queues/notification.ts +9 -0
  21. package/api/src/queues/payment.ts +17 -0
  22. package/api/src/routes/checkout-sessions.ts +13 -1
  23. package/api/src/routes/payment-stats.ts +3 -3
  24. package/api/src/routes/subscriptions.ts +26 -6
  25. package/api/src/store/migrations/20240905-index.ts +100 -0
  26. package/api/src/store/models/subscription.ts +1 -0
  27. package/api/tests/libs/payment.spec.ts +168 -0
  28. package/blocklet.yml +1 -1
  29. package/package.json +10 -10
  30. package/src/components/balance-list.tsx +2 -2
  31. package/src/components/invoice/list.tsx +2 -2
  32. package/src/components/invoice/table.tsx +1 -1
  33. package/src/components/payment-intent/list.tsx +1 -1
  34. package/src/components/payouts/list.tsx +1 -1
  35. package/src/components/refund/list.tsx +2 -2
  36. package/src/components/subscription/actions/cancel.tsx +41 -13
  37. package/src/components/subscription/actions/index.tsx +11 -8
  38. package/src/components/subscription/actions/slash-stake.tsx +52 -0
  39. package/src/locales/en.tsx +1 -0
  40. package/src/locales/zh.tsx +1 -0
  41. package/src/pages/admin/billing/invoices/detail.tsx +2 -2
  42. package/src/pages/customer/refund/list.tsx +1 -1
  43. 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 type { LineItem, SubscriptionData, TPriceExpanded, TProductExpanded } from '../store/models';
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().startOf('day').unix();
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.type === 'arcblock') {
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.type === 'ethereum') {
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 slashStake = requestByAdmin && staking === 'slash';
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
@@ -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.15.1
17
+ version: 1.15.3
18
18
  logo: logo.png
19
19
  files:
20
20
  - dist
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "payment-kit",
3
- "version": "1.15.1",
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.25",
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.24",
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.1",
55
+ "@blocklet/payment-react": "1.15.3",
56
56
  "@blocklet/sdk": "1.16.30",
57
- "@blocklet/ui-react": "^2.10.25",
58
- "@blocklet/uploader": "^0.1.27",
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.1",
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.3",
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": "ff06b03c0e25d6c4d9ea8f5495592c4f5c20829b"
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: '200px' }}
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: 60,
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: 60,
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.type === 'stripe') {
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: 60,
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: 60,
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: 60,
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: 60,
170
+ width: 80,
171
171
  options: {
172
172
  filter: true,
173
173
  customBodyRenderLite: (_: string, index: number) => {