payment-kit 1.23.11 → 1.24.1

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 (31) hide show
  1. package/api/src/libs/credit-schedule.ts +866 -0
  2. package/api/src/queues/credit-consume.ts +4 -2
  3. package/api/src/queues/credit-grant.ts +385 -5
  4. package/api/src/queues/notification.ts +13 -7
  5. package/api/src/queues/subscription.ts +12 -0
  6. package/api/src/routes/credit-grants.ts +18 -0
  7. package/api/src/routes/credit-transactions.ts +1 -1
  8. package/api/src/routes/prices.ts +43 -3
  9. package/api/src/routes/products.ts +41 -2
  10. package/api/src/routes/subscriptions.ts +217 -0
  11. package/api/src/store/migrations/20251225-add-credit-schedule-state.ts +33 -0
  12. package/api/src/store/models/subscription.ts +9 -0
  13. package/api/src/store/models/types.ts +42 -0
  14. package/api/tests/libs/credit-schedule.spec.ts +676 -0
  15. package/api/tests/libs/subscription.spec.ts +8 -4
  16. package/blocklet.yml +1 -1
  17. package/package.json +22 -22
  18. package/src/components/customer/credit-overview.tsx +1 -1
  19. package/src/components/price/form.tsx +376 -133
  20. package/src/components/product/edit-price.tsx +6 -0
  21. package/src/components/subscription/payment-method-info.tsx +1 -1
  22. package/src/components/subscription/portal/actions.tsx +9 -2
  23. package/src/locales/en.tsx +28 -0
  24. package/src/locales/zh.tsx +28 -0
  25. package/src/pages/admin/billing/subscriptions/detail.tsx +28 -15
  26. package/src/pages/admin/products/prices/detail.tsx +114 -0
  27. package/src/pages/admin/settings/vault-config/index.tsx +1 -1
  28. package/src/pages/customer/subscription/detail.tsx +28 -8
  29. package/src/pages/integrations/donations/edit-form.tsx +1 -1
  30. package/src/pages/integrations/donations/index.tsx +1 -1
  31. package/vite.config.ts +0 -1
@@ -1,4 +1,4 @@
1
- import { Button } from '@arcblock/ux';
1
+ import Button from '@arcblock/ux/lib/Button';
2
2
  import DID from '@arcblock/ux/lib/DID';
3
3
  import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
4
4
  import Toast from '@arcblock/ux/lib/Toast';
@@ -153,8 +153,15 @@ export function SubscriptionActionsInner({
153
153
  return true;
154
154
  };
155
155
 
156
+ const isScheduledCreditPrice = (price: any) => {
157
+ const schedule = price?.metadata?.credit_config?.schedule;
158
+ return schedule?.enabled && schedule?.delivery_mode === 'schedule';
159
+ };
160
+
161
+ const hasScheduledCredit = (subscription.items || []).some((item: any) => isScheduledCreditPrice(item.price));
162
+
156
163
  const { data: changePlanAvailable = false } = useRequest(() => fetchChangePlan(subscription.id), {
157
- ready: !!showExtra && isActive(subscription),
164
+ ready: !!showExtra && isActive(subscription) && !hasScheduledCredit,
158
165
  });
159
166
 
160
167
  const { data: batchPayAvailable = '' } = useRequest(() => fetchBatchPay(subscription.id), {
@@ -491,7 +498,7 @@ export function SubscriptionActionsInner({
491
498
  },
492
499
  {
493
500
  key: 'changePlan',
494
- show: changePlanAvailable,
501
+ show: changePlanAvailable && !hasScheduledCredit,
495
502
  label: action?.text || t('payment.customer.changePlan.button'),
496
503
  onClick: (e) => {
497
504
  e?.stopPropagation();
@@ -7,6 +7,7 @@ export default flat({
7
7
  estimated: 'Estimated',
8
8
  total: 'Total',
9
9
  active: 'Active',
10
+ every: 'Every',
10
11
  inactive: 'Inactive',
11
12
  metadata: {
12
13
  label: 'Metadata',
@@ -416,6 +417,11 @@ export default flat({
416
417
  defaultNickname: 'Credit Recharge',
417
418
  unitLabel: 'units',
418
419
  settings: 'Credit Settings',
420
+ configTitle: 'Credit Configuration',
421
+ expirationTime: {
422
+ label: 'Expiration',
423
+ expireWithNextGrant: 'Expire with Next Grant',
424
+ },
419
425
  name: {
420
426
  placeholder: 'Enter Credit product name',
421
427
  },
@@ -451,6 +457,28 @@ export default flat({
451
457
  help: 'Usage order when user has multiple Credits, lower numbers have higher priority',
452
458
  description: 'Range: 0-100, 0 is highest priority, 50 is default value',
453
459
  },
460
+ deliveryMode: {
461
+ label: 'Delivery Mode',
462
+ once: 'One-time',
463
+ periodic: 'Periodic',
464
+ },
465
+ schedule: {
466
+ interval: {
467
+ label: 'Grant Frequency',
468
+ hour: 'hours',
469
+ day: 'days',
470
+ week: 'weeks',
471
+ month: 'months',
472
+ },
473
+ amountPerGrant: {
474
+ label: 'Amount per Grant',
475
+ description: 'Fixed Credit amount per grant',
476
+ },
477
+ expireWithNextGrant: {
478
+ label: 'Expire with Next Grant',
479
+ description: 'This Credit expires when the next grant is issued (refresh mode)',
480
+ },
481
+ },
454
482
  },
455
483
  meterEvent: {
456
484
  title: 'Meter Event Details',
@@ -7,6 +7,7 @@ export default flat({
7
7
  estimated: '预估',
8
8
  total: '总量',
9
9
  active: '生效中',
10
+ every: '每',
10
11
  metadata: {
11
12
  label: '元数据',
12
13
  description: '添加自定义键值对以存储有关此计量器的其他信息。',
@@ -414,6 +415,11 @@ export default flat({
414
415
  defaultNickname: 'Credit 充值',
415
416
  unitLabel: '单位',
416
417
  settings: 'Credit 设置',
418
+ configTitle: 'Credit 配置',
419
+ expirationTime: {
420
+ label: '过期时间',
421
+ expireWithNextGrant: '随下次发放过期',
422
+ },
417
423
  name: {
418
424
  placeholder: '输入 Credit 产品名称',
419
425
  },
@@ -448,6 +454,28 @@ export default flat({
448
454
  help: '当用户有多个 Credit 时的使用顺序,数字越小优先级越高',
449
455
  description: '范围:0-100,0 为最高优先级,50 为默认值',
450
456
  },
457
+ deliveryMode: {
458
+ label: '发放模式',
459
+ once: '一次性发放',
460
+ periodic: '周期性发放',
461
+ },
462
+ schedule: {
463
+ interval: {
464
+ label: '发放频率',
465
+ hour: '小时',
466
+ day: '天',
467
+ week: '周',
468
+ month: '月',
469
+ },
470
+ amountPerGrant: {
471
+ label: '每次发放数量',
472
+ description: '每次发放固定数量的 Credit',
473
+ },
474
+ expireWithNextGrant: {
475
+ label: '随下次发放过期',
476
+ description: '本次 Credit 将在下一次发放时自动过期',
477
+ },
478
+ },
451
479
  },
452
480
  meterEvent: {
453
481
  title: '计量事件详情',
@@ -8,6 +8,7 @@ import {
8
8
  formatTime,
9
9
  useMobile,
10
10
  hasDelegateTxHash,
11
+ CreditGrantsList,
11
12
  CreditTransactionsList,
12
13
  } from '@blocklet/payment-react';
13
14
  import type { TProduct, TSubscriptionExpanded } from '@blocklet/payment-types';
@@ -89,7 +90,10 @@ export default function SubscriptionDetail(props: { id: string }) {
89
90
  setState((prev) => ({ editing: { ...prev.editing, metadata: true } }));
90
91
  };
91
92
 
92
- const isCredit = data.paymentCurrency.type === 'credit';
93
+ const isCredit =
94
+ data?.serviceType === 'credit' ||
95
+ data?.paymentCurrency?.type === 'credit' ||
96
+ data?.items?.some((item) => item?.price?.product?.type === 'credit' || item?.price?.metadata?.credit_config);
93
97
 
94
98
  return (
95
99
  <Root direction="column" spacing={2.5} sx={{ mb: 4 }}>
@@ -379,22 +383,31 @@ export default function SubscriptionDetail(props: { id: string }) {
379
383
  </>
380
384
  );
381
385
  })()}
382
- <Divider />
386
+ <Divider sx={{ my: 1.5 }} />
383
387
  {isCredit ? (
384
- <Box className="section">
385
- <Typography variant="h3" className="section-header">
386
- {t('admin.creditTransactions.title')}
387
- </Typography>
388
- <Box className="section-body">
389
- <CreditTransactionsList
390
- customer_id={data.customer_id}
391
- subscription_id={data.id}
392
- showAdminColumns
393
- showTimeFilter
394
- mode="dashboard"
395
- />
388
+ <>
389
+ <Box className="section">
390
+ <Typography variant="h3" className="section-header">
391
+ {t('admin.creditGrants.tab')}
392
+ </Typography>
393
+ <Box className="section-body">
394
+ <CreditGrantsList customer_id={data.customer_id} subscription_id={data.id} mode="dashboard" />
395
+ </Box>
396
396
  </Box>
397
- </Box>
397
+ <Box className="section">
398
+ <Typography variant="h3" className="section-header">
399
+ {t('admin.creditTransactions.title')}
400
+ </Typography>
401
+ <Box className="section-body">
402
+ <CreditTransactionsList
403
+ customer_id={data.customer_id}
404
+ subscription_id={data.id}
405
+ includeGrants
406
+ mode="dashboard"
407
+ />
408
+ </Box>
409
+ </Box>
410
+ </>
398
411
  ) : (
399
412
  <>
400
413
  <Box className="section">
@@ -310,6 +310,120 @@ export default function PriceDetail(props: { id: string }) {
310
310
  </Box>
311
311
 
312
312
  <Divider />
313
+ {data.metadata?.credit_config && (
314
+ <>
315
+ <Box className="section">
316
+ <SectionHeader title={t('admin.creditProduct.configTitle')} />
317
+ <InfoRowGroup
318
+ sx={{
319
+ display: 'grid',
320
+ gridTemplateColumns: {
321
+ xs: 'repeat(1, 1fr)',
322
+ xl: 'repeat(2, 1fr)',
323
+ },
324
+ '@container (min-width: 1000px)': {
325
+ gridTemplateColumns: 'repeat(2, 1fr)',
326
+ },
327
+ '.info-row-wrapper': {
328
+ gap: 1,
329
+ flexDirection: {
330
+ xs: 'column',
331
+ xl: 'row',
332
+ },
333
+ alignItems: {
334
+ xs: 'flex-start',
335
+ xl: 'center',
336
+ },
337
+ '@container (min-width: 1000px)': {
338
+ flexDirection: 'row',
339
+ alignItems: 'center',
340
+ },
341
+ },
342
+ }}>
343
+ {(() => {
344
+ const creditConfig = data.metadata.credit_config;
345
+ const isScheduleEnabled = creditConfig.schedule?.enabled;
346
+ const creditCurrencyItem = data.currency_options?.find(
347
+ (item: any) => item.currency_id === creditConfig.currency_id
348
+ );
349
+ const creditCurrency = creditCurrencyItem ? (creditCurrencyItem as any).currency : null;
350
+
351
+ return (
352
+ <>
353
+ {/* 周期性发放的字段 */}
354
+ {isScheduleEnabled &&
355
+ creditConfig.schedule?.interval_value &&
356
+ creditConfig.schedule?.interval_unit && (
357
+ <InfoRow
358
+ label={t('admin.creditProduct.schedule.interval.label')}
359
+ value={`${t('common.every')} ${creditConfig.schedule.interval_value} ${t(`admin.creditProduct.schedule.interval.${creditConfig.schedule.interval_unit}`)}`}
360
+ />
361
+ )}
362
+ {isScheduleEnabled && creditConfig.schedule?.amount_per_grant && (
363
+ <InfoRow
364
+ label={t('admin.creditProduct.schedule.amountPerGrant.label')}
365
+ value={
366
+ creditCurrency ? (
367
+ <Stack direction="row" spacing={0.5} alignItems="center">
368
+ <span>{creditConfig.schedule.amount_per_grant}</span>
369
+ <Currency logo={creditCurrency.logo} name={creditCurrency.symbol} size={16} />
370
+ </Stack>
371
+ ) : (
372
+ creditConfig.schedule.amount_per_grant
373
+ )
374
+ }
375
+ />
376
+ )}
377
+ {/* 一次性发放的字段 */}
378
+ {!isScheduleEnabled && creditConfig.credit_amount && (
379
+ <InfoRow
380
+ label={t('admin.creditProduct.creditAmount.label')}
381
+ value={
382
+ creditCurrency ? (
383
+ <Stack direction="row" spacing={0.5} alignItems="center">
384
+ <span>{creditConfig.credit_amount}</span>
385
+ <Currency logo={creditCurrency.logo} name={creditCurrency.symbol} size={16} />
386
+ </Stack>
387
+ ) : (
388
+ creditConfig.credit_amount
389
+ )
390
+ }
391
+ />
392
+ )}
393
+ {/* 过期时间 */}
394
+ <InfoRow
395
+ label={t('admin.creditProduct.expirationTime.label')}
396
+ value={(() => {
397
+ if (isScheduleEnabled && creditConfig.schedule?.expire_with_next_grant) {
398
+ return t('admin.creditProduct.expirationTime.expireWithNextGrant');
399
+ }
400
+ const durationValue = creditConfig.valid_duration_value;
401
+ const durationUnit = creditConfig.valid_duration_unit;
402
+ if (durationValue && durationValue > 0 && durationUnit) {
403
+ return `${durationValue} ${t(`admin.creditProduct.validDuration.${durationUnit}`)}`;
404
+ }
405
+ return '-';
406
+ })()}
407
+ />
408
+ {/* 使用优先级 */}
409
+ {creditConfig.priority !== undefined && (
410
+ <InfoRow label={t('admin.creditProduct.priority.label')} value={creditConfig.priority} />
411
+ )}
412
+ {/* 关联价格 */}
413
+ {creditConfig.applicable_prices && creditConfig.applicable_prices.length > 0 && (
414
+ <InfoRow
415
+ label={t('admin.creditProduct.associatedPrices.label')}
416
+ value={creditConfig.applicable_prices.length}
417
+ />
418
+ )}
419
+ </>
420
+ );
421
+ })()}
422
+ </InfoRowGroup>
423
+ </Box>
424
+ <Divider />
425
+ </>
426
+ )}
313
427
  {isCreditMetered(data) && (
314
428
  <>
315
429
  <Box className="section">
@@ -6,7 +6,7 @@ import { useRequest, useSetState } from 'ahooks';
6
6
  import Empty from '@arcblock/ux/lib/Empty';
7
7
  import { HelpOutline } from '@mui/icons-material';
8
8
  import type { TPaymentCurrencyExpanded } from '@blocklet/payment-types';
9
- import { Toast } from '@arcblock/ux';
9
+ import Toast from '@arcblock/ux/lib/Toast';
10
10
  import { styled } from '@mui/system';
11
11
  import { useSessionContext } from '../../../../contexts/session';
12
12
 
@@ -2,6 +2,7 @@
2
2
  import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
3
3
  import {
4
4
  api,
5
+ CreditGrantsList,
5
6
  CreditTransactionsList,
6
7
  CustomerInvoiceList,
7
8
  formatBNStr,
@@ -129,7 +130,10 @@ export default function CustomerSubscriptionDetail() {
129
130
  return <CircularProgress />;
130
131
  }
131
132
 
132
- const isCredit = data?.paymentCurrency?.type === 'credit';
133
+ const isCredit =
134
+ data?.serviceType === 'credit' ||
135
+ data?.paymentCurrency?.type === 'credit' ||
136
+ data?.items?.some((item) => item?.price?.product?.type === 'credit' || item?.price?.metadata?.credit_config);
133
137
  const showOverdraftProtection =
134
138
  data?.paymentMethod?.type === 'arcblock' &&
135
139
  !overdraftProtectionLoading &&
@@ -751,14 +755,30 @@ export default function CustomerSubscriptionDetail() {
751
755
  })()}
752
756
  <Box className="divider" />
753
757
  {isCredit ? (
754
- <Box className="section">
755
- <Typography variant="h3" className="section-header">
756
- {t('admin.creditTransactions.title')}
757
- </Typography>
758
- <Box className="section-body">
759
- <CreditTransactionsList customer_id={data.customer_id} subscription_id={data.id} showAdminColumns={false} />
758
+ <>
759
+ <Divider />
760
+ <Box className="section">
761
+ <Typography variant="h3" className="section-header">
762
+ {t('admin.creditGrants.tab')}
763
+ </Typography>
764
+ <Box className="section-body">
765
+ <CreditGrantsList customer_id={data.customer_id} subscription_id={data.id} mode="portal" />
766
+ </Box>
760
767
  </Box>
761
- </Box>
768
+ <Box className="section">
769
+ <Typography variant="h3" className="section-header">
770
+ {t('admin.creditTransactions.title')}
771
+ </Typography>
772
+ <Box className="section-body">
773
+ <CreditTransactionsList
774
+ customer_id={data.customer_id}
775
+ subscription_id={data.id}
776
+ showAdminColumns={false}
777
+ includeGrants
778
+ />
779
+ </Box>
780
+ </Box>
781
+ </>
762
782
  ) : (
763
783
  <Box className="section">
764
784
  <Typography variant="h3" className="section-header">
@@ -13,7 +13,7 @@ import {
13
13
  ToggleButton,
14
14
  } from '@mui/material';
15
15
  import { api, formatError, clearDonateCache } from '@blocklet/payment-react';
16
- import { Toast } from '@arcblock/ux';
16
+ import Toast from '@arcblock/ux/lib/Toast';
17
17
  import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
18
18
  import type { TSetting } from '@blocklet/payment-types';
19
19
  import { useState, useEffect, useRef } from 'react';
@@ -1,5 +1,5 @@
1
1
  /* eslint-disable react/no-unstable-nested-components */
2
- import { Toast } from '@arcblock/ux';
2
+ import Toast from '@arcblock/ux/lib/Toast';
3
3
  import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
4
4
  import { api, formatError, Table, ConfirmDialog, Switch, clearDonateCache, getPrefix } from '@blocklet/payment-react';
5
5
  import { Alert, Typography, Button, CircularProgress, Stack, Avatar, Box, IconButton, Tooltip } from '@mui/material';
package/vite.config.ts CHANGED
@@ -98,7 +98,6 @@ export default defineConfig(({ mode }) => {
98
98
  moment: ['moment', 'moment-timezone'],
99
99
  utils: ['dayjs', 'numbro', 'bn.js'],
100
100
  hooks: ['ahooks', 'use-bus'],
101
- lottie: ['lottie-web', 'react-lottie-player', '@lottiefiles/react-lottie-player'],
102
101
  'vendor-arcblock': ['@arcblock/did-connect-react', '@arcblock/ux'],
103
102
  'vendor-blocklet': ['@blocklet/ui-react'],
104
103
  },