payment-kit 1.23.10 → 1.24.0
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/libs/invoice.ts +9 -2
- package/api/src/queues/credit-consume.ts +109 -35
- 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/meter-events.ts +0 -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/meter-event.ts +1 -1
- 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 +6 -6
- package/src/components/price/form.tsx +376 -133
- package/src/components/product/edit-price.tsx +6 -0
- 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/customer/subscription/detail.tsx +28 -8
package/blocklet.yml
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "payment-kit",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.24.0",
|
|
4
4
|
"scripts": {
|
|
5
5
|
"dev": "blocklet dev --open",
|
|
6
6
|
"prelint": "npm run types",
|
|
@@ -59,9 +59,9 @@
|
|
|
59
59
|
"@blocklet/error": "^0.3.5",
|
|
60
60
|
"@blocklet/js-sdk": "^1.17.8-beta-20260104-120132-cb5b1914",
|
|
61
61
|
"@blocklet/logger": "^1.17.8-beta-20260104-120132-cb5b1914",
|
|
62
|
-
"@blocklet/payment-broker-client": "1.
|
|
63
|
-
"@blocklet/payment-react": "1.
|
|
64
|
-
"@blocklet/payment-vendor": "1.
|
|
62
|
+
"@blocklet/payment-broker-client": "1.24.0",
|
|
63
|
+
"@blocklet/payment-react": "1.24.0",
|
|
64
|
+
"@blocklet/payment-vendor": "1.24.0",
|
|
65
65
|
"@blocklet/sdk": "^1.17.8-beta-20260104-120132-cb5b1914",
|
|
66
66
|
"@blocklet/ui-react": "^3.3.10",
|
|
67
67
|
"@blocklet/uploader": "^0.3.19",
|
|
@@ -131,7 +131,7 @@
|
|
|
131
131
|
"devDependencies": {
|
|
132
132
|
"@abtnode/types": "^1.17.8-beta-20260104-120132-cb5b1914",
|
|
133
133
|
"@arcblock/eslint-config-ts": "^0.3.3",
|
|
134
|
-
"@blocklet/payment-types": "1.
|
|
134
|
+
"@blocklet/payment-types": "1.24.0",
|
|
135
135
|
"@types/cookie-parser": "^1.4.9",
|
|
136
136
|
"@types/cors": "^2.8.19",
|
|
137
137
|
"@types/debug": "^4.1.12",
|
|
@@ -178,5 +178,5 @@
|
|
|
178
178
|
"parser": "typescript"
|
|
179
179
|
}
|
|
180
180
|
},
|
|
181
|
-
"gitHead": "
|
|
181
|
+
"gitHead": "f3fad0829a1377422d51f9b6445a3502d0e94719"
|
|
182
182
|
}
|
|
@@ -767,145 +767,388 @@ export default function PriceForm({ prefix = '', simple = false, productType = u
|
|
|
767
767
|
expanded={isLocked}
|
|
768
768
|
style={{ width: INPUT_WIDTH }}>
|
|
769
769
|
<Box sx={{ width: INPUT_WIDTH, mb: 2, pl: 2, pr: 1 }}>
|
|
770
|
-
{/*
|
|
770
|
+
{/* 发放模式选择 - 仅对订阅类型显示 */}
|
|
771
|
+
{isRecurring && (
|
|
772
|
+
<Controller
|
|
773
|
+
name={getFieldName('metadata')}
|
|
774
|
+
control={control}
|
|
775
|
+
render={({ field }) => {
|
|
776
|
+
const deliveryMode = field.value?.credit_config?.schedule?.enabled ? 'periodic' : 'once';
|
|
777
|
+
return (
|
|
778
|
+
<Box sx={{ width: '100%', mb: 2 }}>
|
|
779
|
+
<FormLabel>{t('admin.creditProduct.deliveryMode.label')}</FormLabel>
|
|
780
|
+
<ToggleButtonGroup
|
|
781
|
+
value={deliveryMode}
|
|
782
|
+
exclusive
|
|
783
|
+
fullWidth
|
|
784
|
+
size="small"
|
|
785
|
+
onChange={(_, newMode) => {
|
|
786
|
+
if (!newMode) return;
|
|
787
|
+
const metadata = field.value || {};
|
|
788
|
+
const creditConfig = metadata.credit_config || {};
|
|
789
|
+
if (newMode === 'periodic') {
|
|
790
|
+
creditConfig.schedule = {
|
|
791
|
+
enabled: true,
|
|
792
|
+
delivery_mode: 'schedule',
|
|
793
|
+
interval_value: 1,
|
|
794
|
+
interval_unit: 'day',
|
|
795
|
+
first_grant_timing: 'immediate',
|
|
796
|
+
expire_with_next_grant: true,
|
|
797
|
+
};
|
|
798
|
+
// Clear one-time fields
|
|
799
|
+
delete creditConfig.credit_amount;
|
|
800
|
+
delete creditConfig.valid_duration_value;
|
|
801
|
+
delete creditConfig.valid_duration_unit;
|
|
802
|
+
} else {
|
|
803
|
+
delete creditConfig.schedule;
|
|
804
|
+
}
|
|
805
|
+
metadata.credit_config = creditConfig;
|
|
806
|
+
field.onChange(metadata);
|
|
807
|
+
}}>
|
|
808
|
+
<ToggleButton value="once">{t('admin.creditProduct.deliveryMode.once')}</ToggleButton>
|
|
809
|
+
<ToggleButton value="periodic">{t('admin.creditProduct.deliveryMode.periodic')}</ToggleButton>
|
|
810
|
+
</ToggleButtonGroup>
|
|
811
|
+
</Box>
|
|
812
|
+
);
|
|
813
|
+
}}
|
|
814
|
+
/>
|
|
815
|
+
)}
|
|
816
|
+
|
|
817
|
+
{/* 一次性发放配置 */}
|
|
771
818
|
<Controller
|
|
772
819
|
name={getFieldName('metadata')}
|
|
773
820
|
control={control}
|
|
774
|
-
render={({ field }) =>
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
821
|
+
render={({ field }) => {
|
|
822
|
+
const isPeriodicMode = isRecurring && field.value?.credit_config?.schedule?.enabled;
|
|
823
|
+
if (isPeriodicMode) return <Box />;
|
|
824
|
+
|
|
825
|
+
return (
|
|
826
|
+
<>
|
|
827
|
+
{/* Credit 数量配置 */}
|
|
828
|
+
<Box sx={{ width: '100%', mb: 2 }}>
|
|
829
|
+
<FormLabel required>{t('admin.creditProduct.creditAmount.label')}</FormLabel>
|
|
830
|
+
<TextField
|
|
831
|
+
size="small"
|
|
832
|
+
fullWidth
|
|
833
|
+
type="number"
|
|
834
|
+
placeholder={t('admin.creditProduct.creditAmount.placeholder')}
|
|
835
|
+
value={field.value?.credit_config?.credit_amount || ''}
|
|
836
|
+
disabled={isLocked}
|
|
837
|
+
onChange={(e) => {
|
|
838
|
+
const metadata = field.value || {};
|
|
839
|
+
const creditConfig = metadata.credit_config || {};
|
|
840
|
+
if (e.target.value) {
|
|
841
|
+
creditConfig.credit_amount = e.target.value;
|
|
842
|
+
} else {
|
|
843
|
+
delete creditConfig.credit_amount;
|
|
844
|
+
}
|
|
845
|
+
metadata.credit_config = creditConfig;
|
|
846
|
+
field.onChange(metadata);
|
|
847
|
+
}}
|
|
848
|
+
slotProps={{
|
|
849
|
+
input: {
|
|
850
|
+
endAdornment: (
|
|
851
|
+
<InputAdornment position="end">
|
|
852
|
+
<CurrencySelect
|
|
853
|
+
mode="selected"
|
|
854
|
+
hasSelected={(c) => currencies.fields.some((x: any) => x.currency_id === c.id)}
|
|
855
|
+
currencyFilter={(c) => c.type === 'credit'}
|
|
856
|
+
onSelect={(currencyId) => {
|
|
857
|
+
const metadata = field.value || {};
|
|
858
|
+
const creditConfig = metadata.credit_config || {};
|
|
859
|
+
creditConfig.currency_id = currencyId;
|
|
860
|
+
metadata.credit_config = creditConfig;
|
|
861
|
+
field.onChange(metadata);
|
|
862
|
+
}}
|
|
863
|
+
value={field.value?.credit_config?.currency_id || creditCurrencies?.[0]?.id}
|
|
864
|
+
disabled={isLocked}
|
|
865
|
+
hideMethod
|
|
866
|
+
selectSX={{ '.MuiOutlinedInput-notchedOutline': { border: 'none' } }}
|
|
867
|
+
/>
|
|
868
|
+
</InputAdornment>
|
|
869
|
+
),
|
|
870
|
+
},
|
|
871
|
+
htmlInput: {
|
|
872
|
+
min: 0,
|
|
873
|
+
step: 'any',
|
|
874
|
+
},
|
|
875
|
+
}}
|
|
876
|
+
/>
|
|
877
|
+
<Typography
|
|
878
|
+
variant="caption"
|
|
879
|
+
sx={{
|
|
880
|
+
color: 'text.secondary',
|
|
881
|
+
display: 'block',
|
|
882
|
+
mt: 0.5,
|
|
883
|
+
}}>
|
|
884
|
+
{t('admin.creditProduct.creditAmount.description')}
|
|
885
|
+
</Typography>
|
|
886
|
+
</Box>
|
|
887
|
+
|
|
888
|
+
{/* 可用时长配置 */}
|
|
889
|
+
<Box sx={{ width: '100%', mb: 2 }}>
|
|
890
|
+
<FormLabel tooltip={t('admin.creditProduct.validDuration.help')}>
|
|
891
|
+
{t('admin.creditProduct.validDuration.label')}
|
|
892
|
+
</FormLabel>
|
|
893
|
+
<Stack
|
|
894
|
+
direction="row"
|
|
895
|
+
spacing={1}
|
|
896
|
+
sx={{
|
|
897
|
+
alignItems: 'center',
|
|
898
|
+
}}>
|
|
899
|
+
<TextField
|
|
900
|
+
size="small"
|
|
901
|
+
type="number"
|
|
902
|
+
sx={{ flex: 1 }}
|
|
903
|
+
value={field.value?.credit_config?.valid_duration_value ?? '0'}
|
|
904
|
+
placeholder="0"
|
|
905
|
+
disabled={isLocked}
|
|
906
|
+
onChange={(e) => {
|
|
907
|
+
const metadata = field.value || {};
|
|
908
|
+
const creditConfig = metadata.credit_config || {};
|
|
909
|
+
const value = +e.target.value;
|
|
910
|
+
if (!Number.isNaN(value) && value >= 0) {
|
|
911
|
+
creditConfig.valid_duration_value = +value;
|
|
912
|
+
} else {
|
|
913
|
+
delete creditConfig.valid_duration_value;
|
|
914
|
+
}
|
|
915
|
+
metadata.credit_config = creditConfig;
|
|
916
|
+
field.onChange(metadata);
|
|
917
|
+
}}
|
|
918
|
+
slotProps={{
|
|
919
|
+
htmlInput: {
|
|
920
|
+
min: 0,
|
|
921
|
+
step: 0.1,
|
|
922
|
+
},
|
|
923
|
+
}}
|
|
924
|
+
/>
|
|
925
|
+
<Select
|
|
926
|
+
size="small"
|
|
927
|
+
sx={{ minWidth: 120 }}
|
|
928
|
+
disabled={isLocked}
|
|
929
|
+
value={field.value?.credit_config?.valid_duration_unit || 'days'}
|
|
930
|
+
onChange={(e) => {
|
|
931
|
+
const metadata = field.value || {};
|
|
932
|
+
const creditConfig = metadata.credit_config || {};
|
|
933
|
+
creditConfig.valid_duration_unit = e.target.value;
|
|
934
|
+
metadata.credit_config = creditConfig;
|
|
935
|
+
field.onChange(metadata);
|
|
936
|
+
}}>
|
|
937
|
+
{!livemode && (
|
|
938
|
+
<MenuItem value="hours">{t('admin.creditProduct.validDuration.hours')}</MenuItem>
|
|
939
|
+
)}
|
|
940
|
+
<MenuItem value="days">{t('admin.creditProduct.validDuration.days')}</MenuItem>
|
|
941
|
+
<MenuItem value="weeks">{t('admin.creditProduct.validDuration.weeks')}</MenuItem>
|
|
942
|
+
<MenuItem value="months">{t('admin.creditProduct.validDuration.months')}</MenuItem>
|
|
943
|
+
<MenuItem value="years">{t('admin.creditProduct.validDuration.years')}</MenuItem>
|
|
944
|
+
</Select>
|
|
945
|
+
</Stack>
|
|
946
|
+
<Typography
|
|
947
|
+
variant="caption"
|
|
948
|
+
sx={{
|
|
949
|
+
color: 'text.secondary',
|
|
950
|
+
display: 'block',
|
|
951
|
+
mt: 0.5,
|
|
952
|
+
}}>
|
|
953
|
+
{t('admin.creditProduct.validDuration.description')}
|
|
954
|
+
</Typography>
|
|
955
|
+
</Box>
|
|
956
|
+
</>
|
|
957
|
+
);
|
|
958
|
+
}}
|
|
835
959
|
/>
|
|
836
960
|
|
|
837
|
-
{/*
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
sx={{
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
961
|
+
{/* 周期性发放配置 */}
|
|
962
|
+
{isRecurring && (
|
|
963
|
+
<Controller
|
|
964
|
+
name={getFieldName('metadata')}
|
|
965
|
+
control={control}
|
|
966
|
+
render={({ field }) => {
|
|
967
|
+
const isPeriodicMode = field.value?.credit_config?.schedule?.enabled;
|
|
968
|
+
if (!isPeriodicMode) return <Box />;
|
|
969
|
+
|
|
970
|
+
const expireWithNextGrant = field.value?.credit_config?.schedule?.expire_with_next_grant ?? true;
|
|
971
|
+
|
|
972
|
+
return (
|
|
973
|
+
<Stack spacing={2} sx={{ mb: 2 }}>
|
|
974
|
+
{/* 发放间隔 */}
|
|
975
|
+
<Box>
|
|
976
|
+
<FormLabel>{t('admin.creditProduct.schedule.interval.label')}</FormLabel>
|
|
977
|
+
<Stack direction="row" spacing={1} alignItems="center">
|
|
978
|
+
<TextField
|
|
979
|
+
size="small"
|
|
980
|
+
type="number"
|
|
981
|
+
sx={{ flex: 1 }}
|
|
982
|
+
value={field.value?.credit_config?.schedule?.interval_value || 1}
|
|
983
|
+
onChange={(e) => {
|
|
984
|
+
const metadata = field.value || {};
|
|
985
|
+
const creditConfig = metadata.credit_config || {};
|
|
986
|
+
const schedule = creditConfig.schedule || {};
|
|
987
|
+
schedule.interval_value = Math.max(0.01, parseFloat(e.target.value) || 1);
|
|
988
|
+
creditConfig.schedule = schedule;
|
|
989
|
+
metadata.credit_config = creditConfig;
|
|
990
|
+
field.onChange(metadata);
|
|
991
|
+
}}
|
|
992
|
+
slotProps={{ htmlInput: { min: 0.01, step: 0.01 } }}
|
|
993
|
+
/>
|
|
994
|
+
<Select
|
|
995
|
+
size="small"
|
|
996
|
+
sx={{ minWidth: 100 }}
|
|
997
|
+
value={field.value?.credit_config?.schedule?.interval_unit || 'day'}
|
|
998
|
+
onChange={(e) => {
|
|
999
|
+
const metadata = field.value || {};
|
|
1000
|
+
const creditConfig = metadata.credit_config || {};
|
|
1001
|
+
const schedule = creditConfig.schedule || {};
|
|
1002
|
+
schedule.interval_unit = e.target.value;
|
|
1003
|
+
creditConfig.schedule = schedule;
|
|
1004
|
+
metadata.credit_config = creditConfig;
|
|
1005
|
+
field.onChange(metadata);
|
|
1006
|
+
}}>
|
|
1007
|
+
{!livemode && (
|
|
1008
|
+
<MenuItem value="hour">{t('admin.creditProduct.schedule.interval.hour')}</MenuItem>
|
|
1009
|
+
)}
|
|
1010
|
+
<MenuItem value="day">{t('admin.creditProduct.schedule.interval.day')}</MenuItem>
|
|
1011
|
+
<MenuItem value="week">{t('admin.creditProduct.schedule.interval.week')}</MenuItem>
|
|
1012
|
+
<MenuItem value="month">{t('admin.creditProduct.schedule.interval.month')}</MenuItem>
|
|
1013
|
+
</Select>
|
|
1014
|
+
</Stack>
|
|
1015
|
+
</Box>
|
|
1016
|
+
|
|
1017
|
+
{/* 每次发放数量(带货币选择) */}
|
|
1018
|
+
<Box>
|
|
1019
|
+
<FormLabel required>{t('admin.creditProduct.schedule.amountPerGrant.label')}</FormLabel>
|
|
1020
|
+
<TextField
|
|
1021
|
+
size="small"
|
|
1022
|
+
type="number"
|
|
1023
|
+
fullWidth
|
|
1024
|
+
required
|
|
1025
|
+
value={field.value?.credit_config?.schedule?.amount_per_grant || ''}
|
|
1026
|
+
onChange={(e) => {
|
|
1027
|
+
const metadata = field.value || {};
|
|
1028
|
+
const creditConfig = metadata.credit_config || {};
|
|
1029
|
+
const schedule = creditConfig.schedule || {};
|
|
1030
|
+
schedule.amount_per_grant = e.target.value;
|
|
1031
|
+
creditConfig.schedule = schedule;
|
|
1032
|
+
metadata.credit_config = creditConfig;
|
|
1033
|
+
field.onChange(metadata);
|
|
1034
|
+
}}
|
|
1035
|
+
slotProps={{
|
|
1036
|
+
input: {
|
|
1037
|
+
endAdornment: (
|
|
1038
|
+
<InputAdornment position="end">
|
|
1039
|
+
<CurrencySelect
|
|
1040
|
+
mode="selected"
|
|
1041
|
+
hasSelected={(c) => currencies.fields.some((x: any) => x.currency_id === c.id)}
|
|
1042
|
+
currencyFilter={(c) => c.type === 'credit'}
|
|
1043
|
+
onSelect={(currencyId) => {
|
|
1044
|
+
const metadata = field.value || {};
|
|
1045
|
+
const creditConfig = metadata.credit_config || {};
|
|
1046
|
+
creditConfig.currency_id = currencyId;
|
|
1047
|
+
metadata.credit_config = creditConfig;
|
|
1048
|
+
field.onChange(metadata);
|
|
1049
|
+
}}
|
|
1050
|
+
value={field.value?.credit_config?.currency_id || creditCurrencies?.[0]?.id}
|
|
1051
|
+
disabled={isLocked}
|
|
1052
|
+
hideMethod
|
|
1053
|
+
selectSX={{ '.MuiOutlinedInput-notchedOutline': { border: 'none' } }}
|
|
1054
|
+
/>
|
|
1055
|
+
</InputAdornment>
|
|
1056
|
+
),
|
|
1057
|
+
},
|
|
1058
|
+
htmlInput: { min: 0, step: 'any' },
|
|
1059
|
+
}}
|
|
1060
|
+
/>
|
|
1061
|
+
<Typography variant="caption" sx={{ color: 'text.secondary', display: 'block', mt: 0.5 }}>
|
|
1062
|
+
{t('admin.creditProduct.schedule.amountPerGrant.description')}
|
|
1063
|
+
</Typography>
|
|
1064
|
+
</Box>
|
|
1065
|
+
|
|
1066
|
+
{/* 可用时长配置 */}
|
|
1067
|
+
<Box>
|
|
1068
|
+
<FormLabel tooltip={t('admin.creditProduct.validDuration.help')}>
|
|
1069
|
+
{t('admin.creditProduct.validDuration.label')}
|
|
1070
|
+
</FormLabel>
|
|
1071
|
+
<Stack direction="row" spacing={1} sx={{ alignItems: 'center' }}>
|
|
1072
|
+
<TextField
|
|
1073
|
+
size="small"
|
|
1074
|
+
type="number"
|
|
1075
|
+
sx={{ flex: 1 }}
|
|
1076
|
+
value={field.value?.credit_config?.valid_duration_value ?? '0'}
|
|
1077
|
+
placeholder="0"
|
|
1078
|
+
disabled={isLocked || expireWithNextGrant}
|
|
1079
|
+
onChange={(e) => {
|
|
1080
|
+
const metadata = field.value || {};
|
|
1081
|
+
const creditConfig = metadata.credit_config || {};
|
|
1082
|
+
const value = +e.target.value;
|
|
1083
|
+
if (!Number.isNaN(value) && value >= 0) {
|
|
1084
|
+
creditConfig.valid_duration_value = +value;
|
|
1085
|
+
} else {
|
|
1086
|
+
delete creditConfig.valid_duration_value;
|
|
1087
|
+
}
|
|
1088
|
+
metadata.credit_config = creditConfig;
|
|
1089
|
+
field.onChange(metadata);
|
|
1090
|
+
}}
|
|
1091
|
+
slotProps={{ htmlInput: { min: 0, step: 0.1 } }}
|
|
1092
|
+
/>
|
|
1093
|
+
<Select
|
|
1094
|
+
size="small"
|
|
1095
|
+
sx={{ minWidth: 120 }}
|
|
1096
|
+
disabled={isLocked || expireWithNextGrant}
|
|
1097
|
+
value={field.value?.credit_config?.valid_duration_unit || 'days'}
|
|
1098
|
+
onChange={(e) => {
|
|
1099
|
+
const metadata = field.value || {};
|
|
1100
|
+
const creditConfig = metadata.credit_config || {};
|
|
1101
|
+
creditConfig.valid_duration_unit = e.target.value;
|
|
1102
|
+
metadata.credit_config = creditConfig;
|
|
1103
|
+
field.onChange(metadata);
|
|
1104
|
+
}}>
|
|
1105
|
+
{!livemode && (
|
|
1106
|
+
<MenuItem value="hours">{t('admin.creditProduct.validDuration.hours')}</MenuItem>
|
|
1107
|
+
)}
|
|
1108
|
+
<MenuItem value="days">{t('admin.creditProduct.validDuration.days')}</MenuItem>
|
|
1109
|
+
<MenuItem value="weeks">{t('admin.creditProduct.validDuration.weeks')}</MenuItem>
|
|
1110
|
+
<MenuItem value="months">{t('admin.creditProduct.validDuration.months')}</MenuItem>
|
|
1111
|
+
<MenuItem value="years">{t('admin.creditProduct.validDuration.years')}</MenuItem>
|
|
1112
|
+
</Select>
|
|
1113
|
+
</Stack>
|
|
1114
|
+
<Typography variant="caption" sx={{ color: 'text.secondary', display: 'block', mt: 0.5 }}>
|
|
1115
|
+
{t('admin.creditProduct.validDuration.description')}
|
|
1116
|
+
</Typography>
|
|
1117
|
+
</Box>
|
|
1118
|
+
|
|
1119
|
+
{/* 随下次发放过期 */}
|
|
1120
|
+
<FormControlLabel
|
|
1121
|
+
sx={{ alignItems: 'flex-start' }}
|
|
1122
|
+
control={
|
|
1123
|
+
<Checkbox
|
|
1124
|
+
checked={expireWithNextGrant}
|
|
1125
|
+
onChange={(_, checked) => {
|
|
1126
|
+
const metadata = field.value || {};
|
|
1127
|
+
const creditConfig = metadata.credit_config || {};
|
|
1128
|
+
const schedule = creditConfig.schedule || {};
|
|
1129
|
+
schedule.expire_with_next_grant = checked;
|
|
1130
|
+
creditConfig.schedule = schedule;
|
|
1131
|
+
metadata.credit_config = creditConfig;
|
|
1132
|
+
field.onChange(metadata);
|
|
1133
|
+
}}
|
|
1134
|
+
/>
|
|
867
1135
|
}
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
const metadata = field.value || {};
|
|
885
|
-
const creditConfig = metadata.credit_config || {};
|
|
886
|
-
creditConfig.valid_duration_unit = e.target.value;
|
|
887
|
-
metadata.credit_config = creditConfig;
|
|
888
|
-
field.onChange(metadata);
|
|
889
|
-
}}>
|
|
890
|
-
{!livemode && <MenuItem value="hours">{t('admin.creditProduct.validDuration.hours')}</MenuItem>}
|
|
891
|
-
<MenuItem value="days">{t('admin.creditProduct.validDuration.days')}</MenuItem>
|
|
892
|
-
<MenuItem value="weeks">{t('admin.creditProduct.validDuration.weeks')}</MenuItem>
|
|
893
|
-
<MenuItem value="months">{t('admin.creditProduct.validDuration.months')}</MenuItem>
|
|
894
|
-
<MenuItem value="years">{t('admin.creditProduct.validDuration.years')}</MenuItem>
|
|
895
|
-
</Select>
|
|
896
|
-
</Stack>
|
|
897
|
-
<Typography
|
|
898
|
-
variant="caption"
|
|
899
|
-
sx={{
|
|
900
|
-
color: 'text.secondary',
|
|
901
|
-
display: 'block',
|
|
902
|
-
mt: 0.5,
|
|
903
|
-
}}>
|
|
904
|
-
{t('admin.creditProduct.validDuration.description')}
|
|
905
|
-
</Typography>
|
|
906
|
-
</Box>
|
|
907
|
-
)}
|
|
908
|
-
/>
|
|
1136
|
+
label={
|
|
1137
|
+
<Box>
|
|
1138
|
+
<Typography sx={{ color: 'text.primary' }}>
|
|
1139
|
+
{t('admin.creditProduct.schedule.expireWithNextGrant.label')}
|
|
1140
|
+
</Typography>
|
|
1141
|
+
<Typography variant="caption" sx={{ color: 'text.secondary' }}>
|
|
1142
|
+
{t('admin.creditProduct.schedule.expireWithNextGrant.description')}
|
|
1143
|
+
</Typography>
|
|
1144
|
+
</Box>
|
|
1145
|
+
}
|
|
1146
|
+
/>
|
|
1147
|
+
</Stack>
|
|
1148
|
+
);
|
|
1149
|
+
}}
|
|
1150
|
+
/>
|
|
1151
|
+
)}
|
|
909
1152
|
|
|
910
1153
|
{/* 关联特定价格 */}
|
|
911
1154
|
<Controller
|
|
@@ -63,6 +63,12 @@ export default function EditPrice({ price, loading, onSave, onCancel, productTyp
|
|
|
63
63
|
return;
|
|
64
64
|
}
|
|
65
65
|
handleSubmit(async (formData: any) => {
|
|
66
|
+
// Workaround: handleSubmit may not include metadata when Controller returns empty element
|
|
67
|
+
// Use getValues('metadata') as fallback
|
|
68
|
+
if (formData.metadata === undefined) {
|
|
69
|
+
formData.metadata = methods.getValues('metadata');
|
|
70
|
+
}
|
|
71
|
+
|
|
66
72
|
if (
|
|
67
73
|
Number(formData.quantity_available) > 0 &&
|
|
68
74
|
Number(formData.quantity_available) < Number(formData.quantity_sold)
|
|
@@ -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();
|