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.
- package/api/src/libs/credit-schedule.ts +866 -0
- package/api/src/queues/credit-consume.ts +4 -2
- package/api/src/queues/credit-grant.ts +385 -5
- package/api/src/queues/notification.ts +13 -7
- package/api/src/queues/subscription.ts +12 -0
- package/api/src/routes/credit-grants.ts +18 -0
- package/api/src/routes/credit-transactions.ts +1 -1
- package/api/src/routes/prices.ts +43 -3
- package/api/src/routes/products.ts +41 -2
- package/api/src/routes/subscriptions.ts +217 -0
- package/api/src/store/migrations/20251225-add-credit-schedule-state.ts +33 -0
- package/api/src/store/models/subscription.ts +9 -0
- package/api/src/store/models/types.ts +42 -0
- package/api/tests/libs/credit-schedule.spec.ts +676 -0
- package/api/tests/libs/subscription.spec.ts +8 -4
- package/blocklet.yml +1 -1
- package/package.json +22 -22
- package/src/components/customer/credit-overview.tsx +1 -1
- package/src/components/price/form.tsx +376 -133
- package/src/components/product/edit-price.tsx +6 -0
- package/src/components/subscription/payment-method-info.tsx +1 -1
- package/src/components/subscription/portal/actions.tsx +9 -2
- package/src/locales/en.tsx +28 -0
- package/src/locales/zh.tsx +28 -0
- package/src/pages/admin/billing/subscriptions/detail.tsx +28 -15
- package/src/pages/admin/products/prices/detail.tsx +114 -0
- package/src/pages/admin/settings/vault-config/index.tsx +1 -1
- package/src/pages/customer/subscription/detail.tsx +28 -8
- package/src/pages/integrations/donations/edit-form.tsx +1 -1
- package/src/pages/integrations/donations/index.tsx +1 -1
- package/vite.config.ts +0 -1
|
@@ -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();
|
package/src/locales/en.tsx
CHANGED
|
@@ -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',
|
package/src/locales/zh.tsx
CHANGED
|
@@ -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 =
|
|
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
|
-
|
|
385
|
-
<
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
<
|
|
390
|
-
customer_id={data.customer_id}
|
|
391
|
-
|
|
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
|
-
|
|
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
|
|
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 =
|
|
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
|
-
|
|
755
|
-
<
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
},
|