payment-kit 1.20.11 → 1.20.12
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/index.ts +2 -0
- package/api/src/integrations/stripe/handlers/invoice.ts +63 -5
- package/api/src/integrations/stripe/handlers/payment-intent.ts +1 -0
- package/api/src/integrations/stripe/resource.ts +253 -2
- package/api/src/libs/currency.ts +31 -0
- package/api/src/libs/discount/coupon.ts +1061 -0
- package/api/src/libs/discount/discount.ts +349 -0
- package/api/src/libs/discount/nft.ts +239 -0
- package/api/src/libs/discount/redemption.ts +636 -0
- package/api/src/libs/discount/vc.ts +73 -0
- package/api/src/libs/invoice.ts +44 -10
- package/api/src/libs/math-utils.ts +6 -0
- package/api/src/libs/price.ts +43 -0
- package/api/src/libs/session.ts +242 -57
- package/api/src/libs/subscription.ts +2 -6
- package/api/src/queues/auto-recharge.ts +1 -1
- package/api/src/queues/discount-status.ts +200 -0
- package/api/src/queues/subscription.ts +98 -5
- package/api/src/queues/usage-record.ts +1 -1
- package/api/src/routes/auto-recharge-configs.ts +5 -3
- package/api/src/routes/checkout-sessions.ts +755 -64
- package/api/src/routes/connect/change-payment.ts +6 -1
- package/api/src/routes/connect/change-plan.ts +6 -1
- package/api/src/routes/connect/setup.ts +6 -1
- package/api/src/routes/connect/shared.ts +80 -9
- package/api/src/routes/connect/subscribe.ts +12 -2
- package/api/src/routes/coupons.ts +518 -0
- package/api/src/routes/index.ts +4 -0
- package/api/src/routes/invoices.ts +44 -3
- package/api/src/routes/meter-events.ts +2 -1
- package/api/src/routes/payment-currencies.ts +1 -0
- package/api/src/routes/promotion-codes.ts +482 -0
- package/api/src/routes/subscriptions.ts +23 -2
- package/api/src/store/migrations/20250904-discount.ts +136 -0
- package/api/src/store/migrations/20250910-timestamp-fields.ts +116 -0
- package/api/src/store/migrations/20250916-add-description-fields.ts +30 -0
- package/api/src/store/models/checkout-session.ts +12 -0
- package/api/src/store/models/coupon.ts +144 -4
- package/api/src/store/models/discount.ts +23 -10
- package/api/src/store/models/index.ts +13 -2
- package/api/src/store/models/promotion-code.ts +295 -18
- package/api/src/store/models/types.ts +30 -1
- package/api/tests/libs/session.spec.ts +48 -27
- package/blocklet.yml +1 -1
- package/package.json +20 -20
- package/src/app.tsx +2 -0
- package/src/components/customer/link.tsx +1 -1
- package/src/components/discount/discount-info.tsx +178 -0
- package/src/components/invoice/table.tsx +140 -48
- package/src/components/invoice-pdf/styles.ts +6 -0
- package/src/components/invoice-pdf/template.tsx +59 -33
- package/src/components/metadata/form.tsx +14 -5
- package/src/components/payment-link/actions.tsx +42 -0
- package/src/components/price/form.tsx +91 -65
- package/src/components/product/vendor-config.tsx +5 -3
- package/src/components/promotion/active-redemptions.tsx +534 -0
- package/src/components/promotion/currency-multi-select.tsx +350 -0
- package/src/components/promotion/currency-restrictions.tsx +117 -0
- package/src/components/promotion/product-select.tsx +292 -0
- package/src/components/promotion/promotion-code-form.tsx +534 -0
- package/src/components/subscription/portal/list.tsx +6 -1
- package/src/components/subscription/vendor-service-list.tsx +13 -2
- package/src/locales/en.tsx +227 -0
- package/src/locales/zh.tsx +222 -1
- package/src/pages/admin/billing/subscriptions/detail.tsx +5 -0
- package/src/pages/admin/products/coupons/applicable-products.tsx +166 -0
- package/src/pages/admin/products/coupons/create.tsx +612 -0
- package/src/pages/admin/products/coupons/detail.tsx +538 -0
- package/src/pages/admin/products/coupons/edit.tsx +127 -0
- package/src/pages/admin/products/coupons/index.tsx +210 -3
- package/src/pages/admin/products/index.tsx +22 -3
- package/src/pages/admin/products/products/detail.tsx +12 -2
- package/src/pages/admin/products/promotion-codes/actions.tsx +103 -0
- package/src/pages/admin/products/promotion-codes/create.tsx +235 -0
- package/src/pages/admin/products/promotion-codes/detail.tsx +416 -0
- package/src/pages/admin/products/promotion-codes/list.tsx +247 -0
- package/src/pages/admin/products/promotion-codes/verification-config.tsx +327 -0
- package/src/pages/admin/products/vendors/index.tsx +17 -5
- package/src/pages/customer/subscription/detail.tsx +5 -0
- package/vite.config.ts +4 -3
|
@@ -0,0 +1,350 @@
|
|
|
1
|
+
import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
|
|
2
|
+
import { usePaymentContext, findCurrency } from '@blocklet/payment-react';
|
|
3
|
+
import { Box, TextField, Stack, IconButton, InputAdornment } from '@mui/material';
|
|
4
|
+
import { DeleteOutlineOutlined } from '@mui/icons-material';
|
|
5
|
+
import { Controller, useFieldArray, useFormContext } from 'react-hook-form';
|
|
6
|
+
import CurrencySelect from '../price/currency-select';
|
|
7
|
+
|
|
8
|
+
type CurrencyMultiSelectProps = {
|
|
9
|
+
prefix?: string;
|
|
10
|
+
baseCurrencyFieldName?: string;
|
|
11
|
+
currencyOptionsFieldName?: string;
|
|
12
|
+
unitAmountFieldName?: string;
|
|
13
|
+
objectMode?: boolean; // Support object format for currency_options
|
|
14
|
+
showBaseCurrency?: boolean; // Whether to show the base currency field
|
|
15
|
+
disabled?: boolean;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export default function CurrencyMultiSelect({
|
|
19
|
+
prefix = '',
|
|
20
|
+
baseCurrencyFieldName = 'currency_id',
|
|
21
|
+
currencyOptionsFieldName = 'currency_options',
|
|
22
|
+
unitAmountFieldName = 'amount_off',
|
|
23
|
+
objectMode = false,
|
|
24
|
+
showBaseCurrency = true,
|
|
25
|
+
disabled = false,
|
|
26
|
+
}: CurrencyMultiSelectProps) {
|
|
27
|
+
const { t } = useLocaleContext();
|
|
28
|
+
const { settings } = usePaymentContext();
|
|
29
|
+
const {
|
|
30
|
+
control,
|
|
31
|
+
setValue,
|
|
32
|
+
watch,
|
|
33
|
+
formState: { errors },
|
|
34
|
+
trigger,
|
|
35
|
+
} = useFormContext();
|
|
36
|
+
|
|
37
|
+
const getFieldName = (name: string) => (prefix ? `${prefix}.${name}` : name);
|
|
38
|
+
|
|
39
|
+
const currencies = useFieldArray({
|
|
40
|
+
control,
|
|
41
|
+
name: getFieldName(currencyOptionsFieldName),
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
const baseCurrencyId = watch(getFieldName(baseCurrencyFieldName));
|
|
45
|
+
const currencyOptionsObject = watch(getFieldName(currencyOptionsFieldName));
|
|
46
|
+
|
|
47
|
+
// Object mode helpers
|
|
48
|
+
const getObjectCurrencies = () => {
|
|
49
|
+
if (!objectMode || !currencyOptionsObject || typeof currencyOptionsObject !== 'object') {
|
|
50
|
+
return [];
|
|
51
|
+
}
|
|
52
|
+
return Object.entries(currencyOptionsObject).map(([currencyId, data]) => ({
|
|
53
|
+
currency_id: currencyId,
|
|
54
|
+
[unitAmountFieldName]: (data as any)[unitAmountFieldName] || 0,
|
|
55
|
+
}));
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
const addObjectCurrency = (currencyId: string) => {
|
|
59
|
+
const current = currencyOptionsObject || {};
|
|
60
|
+
setValue(
|
|
61
|
+
getFieldName(currencyOptionsFieldName),
|
|
62
|
+
{
|
|
63
|
+
...current,
|
|
64
|
+
[currencyId]: { [unitAmountFieldName]: 0 },
|
|
65
|
+
},
|
|
66
|
+
{ shouldValidate: true }
|
|
67
|
+
);
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
const removeObjectCurrency = (currencyId: string) => {
|
|
71
|
+
const current = currencyOptionsObject || {};
|
|
72
|
+
const updated = { ...current };
|
|
73
|
+
delete updated[currencyId];
|
|
74
|
+
setValue(getFieldName(currencyOptionsFieldName), updated, { shouldValidate: true });
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
const updateObjectCurrency = (oldCurrencyId: string, newCurrencyId: string) => {
|
|
78
|
+
const current = currencyOptionsObject || {};
|
|
79
|
+
const updated = { ...current };
|
|
80
|
+
const data = updated[oldCurrencyId];
|
|
81
|
+
delete updated[oldCurrencyId];
|
|
82
|
+
updated[newCurrencyId] = data;
|
|
83
|
+
setValue(getFieldName(currencyOptionsFieldName), updated, { shouldValidate: true });
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
// Use appropriate data source based on mode
|
|
87
|
+
const currencyList = objectMode ? getObjectCurrencies() : currencies.fields;
|
|
88
|
+
|
|
89
|
+
const getFieldError = (name: string) => {
|
|
90
|
+
const names = name?.split('.');
|
|
91
|
+
return names.reduce((prev, curr) => prev?.[curr], errors as any);
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
const validateAmount = (v: number) => {
|
|
95
|
+
if (Number(v) <= 0) {
|
|
96
|
+
return t('admin.price.unit_amount.positive');
|
|
97
|
+
}
|
|
98
|
+
return true;
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
const handleRemoveCurrency = async (indexOrCurrencyId: number | string) => {
|
|
102
|
+
if (objectMode) {
|
|
103
|
+
removeObjectCurrency(indexOrCurrencyId as string);
|
|
104
|
+
} else {
|
|
105
|
+
await currencies.remove(indexOrCurrencyId as number);
|
|
106
|
+
trigger(getFieldName(baseCurrencyFieldName));
|
|
107
|
+
}
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
const handleCurrencyChange = (indexOrOldCurrencyId: number | string, currencyId: string) => {
|
|
111
|
+
if (objectMode) {
|
|
112
|
+
updateObjectCurrency(indexOrOldCurrencyId as string, currencyId);
|
|
113
|
+
} else {
|
|
114
|
+
const index = indexOrOldCurrencyId as number;
|
|
115
|
+
const update = {
|
|
116
|
+
currency_id: currencyId,
|
|
117
|
+
};
|
|
118
|
+
// @ts-ignore
|
|
119
|
+
if (currencies?.fields?.[index]?.currency) {
|
|
120
|
+
// @ts-ignore
|
|
121
|
+
update.currency = findCurrency(settings.paymentMethods, currencyId);
|
|
122
|
+
}
|
|
123
|
+
currencies.update(index, {
|
|
124
|
+
...currencies.fields[index],
|
|
125
|
+
...update,
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
const handleAddCurrency = (currencyId: string) => {
|
|
131
|
+
if (objectMode) {
|
|
132
|
+
addObjectCurrency(currencyId);
|
|
133
|
+
} else {
|
|
134
|
+
currencies.append({ currency_id: currencyId, [unitAmountFieldName.split('.').pop() || 'amount_off']: 0 });
|
|
135
|
+
}
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
return (
|
|
139
|
+
<Stack spacing={2} sx={{ width: '100%' }}>
|
|
140
|
+
{/* Base Currency */}
|
|
141
|
+
{showBaseCurrency && (
|
|
142
|
+
<Box sx={{ width: '100%' }}>
|
|
143
|
+
<Controller
|
|
144
|
+
name={getFieldName(unitAmountFieldName)}
|
|
145
|
+
control={control}
|
|
146
|
+
rules={{
|
|
147
|
+
required: t('admin.price.unit_amount.required'),
|
|
148
|
+
validate: (v) => {
|
|
149
|
+
return validateAmount(v);
|
|
150
|
+
},
|
|
151
|
+
}}
|
|
152
|
+
disabled={disabled}
|
|
153
|
+
render={({ field }) => (
|
|
154
|
+
<TextField
|
|
155
|
+
{...field}
|
|
156
|
+
type="number"
|
|
157
|
+
size="small"
|
|
158
|
+
fullWidth
|
|
159
|
+
disabled={disabled}
|
|
160
|
+
error={!!getFieldError(getFieldName(unitAmountFieldName))}
|
|
161
|
+
helperText={getFieldError(getFieldName(unitAmountFieldName))?.message}
|
|
162
|
+
slotProps={{
|
|
163
|
+
input: {
|
|
164
|
+
endAdornment: (
|
|
165
|
+
<InputAdornment position="end">
|
|
166
|
+
<CurrencySelect
|
|
167
|
+
mode="selected"
|
|
168
|
+
hasSelected={(currency) =>
|
|
169
|
+
currencies.fields.some((x: any) => x.currency_id === currency.id) ||
|
|
170
|
+
currency.id === baseCurrencyId
|
|
171
|
+
}
|
|
172
|
+
currencyFilter={(c) => c.type !== 'credit'}
|
|
173
|
+
onSelect={(currencyId) => {
|
|
174
|
+
const index = currencies.fields.findIndex((x: any) => x.currency_id === baseCurrencyId);
|
|
175
|
+
if (index > -1) {
|
|
176
|
+
handleCurrencyChange(index, currencyId);
|
|
177
|
+
}
|
|
178
|
+
setValue(getFieldName('currency'), findCurrency(settings.paymentMethods, currencyId), {
|
|
179
|
+
shouldValidate: true,
|
|
180
|
+
});
|
|
181
|
+
setValue(getFieldName(baseCurrencyFieldName), currencyId, { shouldValidate: true });
|
|
182
|
+
}}
|
|
183
|
+
value={baseCurrencyId}
|
|
184
|
+
disabled={disabled}
|
|
185
|
+
selectSX={{ '.MuiOutlinedInput-notchedOutline': { border: 'none' } }}
|
|
186
|
+
/>
|
|
187
|
+
</InputAdornment>
|
|
188
|
+
),
|
|
189
|
+
},
|
|
190
|
+
}}
|
|
191
|
+
onChange={(e) => {
|
|
192
|
+
const { value } = e.target;
|
|
193
|
+
field.onChange(value);
|
|
194
|
+
if (objectMode) {
|
|
195
|
+
const current = currencyOptionsObject || {};
|
|
196
|
+
setValue(getFieldName(currencyOptionsFieldName), {
|
|
197
|
+
...current,
|
|
198
|
+
[baseCurrencyId]: {
|
|
199
|
+
...(current[baseCurrencyId] || {}),
|
|
200
|
+
[unitAmountFieldName]: value,
|
|
201
|
+
},
|
|
202
|
+
});
|
|
203
|
+
} else {
|
|
204
|
+
const index = currencies.fields.findIndex((x: any) => x.currency_id === baseCurrencyId);
|
|
205
|
+
if (index === -1) {
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
setValue(
|
|
209
|
+
getFieldName(`${currencyOptionsFieldName}.${index}.${unitAmountFieldName.split('.').pop()}`),
|
|
210
|
+
value,
|
|
211
|
+
{
|
|
212
|
+
shouldValidate: true,
|
|
213
|
+
}
|
|
214
|
+
);
|
|
215
|
+
}
|
|
216
|
+
}}
|
|
217
|
+
/>
|
|
218
|
+
)}
|
|
219
|
+
/>
|
|
220
|
+
</Box>
|
|
221
|
+
)}
|
|
222
|
+
|
|
223
|
+
{/* Additional Currencies */}
|
|
224
|
+
{currencyList.filter((x: any) => !showBaseCurrency || x.currency_id !== baseCurrencyId).length > 0 && (
|
|
225
|
+
<Stack spacing={1.5} sx={{ width: '100%' }}>
|
|
226
|
+
{currencyList.map((item: any, index: number) => {
|
|
227
|
+
if (showBaseCurrency && item.currency_id === baseCurrencyId) {
|
|
228
|
+
return null;
|
|
229
|
+
}
|
|
230
|
+
const fieldName = objectMode
|
|
231
|
+
? getFieldName(`${currencyOptionsFieldName}.${item.currency_id}.${unitAmountFieldName}`)
|
|
232
|
+
: getFieldName(`${currencyOptionsFieldName}.${index}.${unitAmountFieldName.split('.').pop()}`);
|
|
233
|
+
const currency = findCurrency(settings.paymentMethods, item.currency_id);
|
|
234
|
+
return (
|
|
235
|
+
<Stack
|
|
236
|
+
key={item.currency_id}
|
|
237
|
+
direction="row"
|
|
238
|
+
spacing={1}
|
|
239
|
+
sx={{
|
|
240
|
+
alignItems: 'start',
|
|
241
|
+
}}>
|
|
242
|
+
<Box sx={{ flex: 1 }}>
|
|
243
|
+
<Controller
|
|
244
|
+
name={fieldName}
|
|
245
|
+
control={control}
|
|
246
|
+
rules={{
|
|
247
|
+
required: t('admin.price.unit_amount.required'),
|
|
248
|
+
validate: (v) => {
|
|
249
|
+
return validateAmount(v);
|
|
250
|
+
},
|
|
251
|
+
}}
|
|
252
|
+
disabled={disabled}
|
|
253
|
+
render={({ field }) => (
|
|
254
|
+
<TextField
|
|
255
|
+
{...field}
|
|
256
|
+
type="number"
|
|
257
|
+
size="small"
|
|
258
|
+
fullWidth
|
|
259
|
+
sx={{ minWidth: '300px' }}
|
|
260
|
+
disabled={disabled}
|
|
261
|
+
error={!!getFieldError(fieldName)}
|
|
262
|
+
helperText={getFieldError(fieldName)?.message as string}
|
|
263
|
+
onChange={(e) => {
|
|
264
|
+
const { value } = e.target;
|
|
265
|
+
field.onChange(value);
|
|
266
|
+
if (objectMode) {
|
|
267
|
+
const current = currencyOptionsObject || {};
|
|
268
|
+
setValue(
|
|
269
|
+
getFieldName(currencyOptionsFieldName),
|
|
270
|
+
{
|
|
271
|
+
...current,
|
|
272
|
+
[item.currency_id]: {
|
|
273
|
+
...(current[item.currency_id] || {}),
|
|
274
|
+
[unitAmountFieldName]: parseFloat(value) || 0,
|
|
275
|
+
},
|
|
276
|
+
},
|
|
277
|
+
{ shouldValidate: true }
|
|
278
|
+
);
|
|
279
|
+
trigger(fieldName);
|
|
280
|
+
}
|
|
281
|
+
}}
|
|
282
|
+
slotProps={{
|
|
283
|
+
input: {
|
|
284
|
+
endAdornment: (
|
|
285
|
+
<InputAdornment position="end">
|
|
286
|
+
<CurrencySelect
|
|
287
|
+
mode="selected"
|
|
288
|
+
hasSelected={(c) =>
|
|
289
|
+
currencyList.some((x: any) => x.currency_id === c.id) ||
|
|
290
|
+
(showBaseCurrency && c.id === baseCurrencyId)
|
|
291
|
+
}
|
|
292
|
+
currencyFilter={(c) => c.type !== 'credit'}
|
|
293
|
+
onSelect={(currencyId) => {
|
|
294
|
+
if (objectMode) {
|
|
295
|
+
handleCurrencyChange(item.currency_id, currencyId);
|
|
296
|
+
} else {
|
|
297
|
+
const cIndex = currencies.fields.findIndex(
|
|
298
|
+
(x: any) => x.currency_id === currency?.id
|
|
299
|
+
);
|
|
300
|
+
if (cIndex > -1) {
|
|
301
|
+
handleCurrencyChange(cIndex, currencyId);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
}}
|
|
305
|
+
value={currency?.id!}
|
|
306
|
+
disabled={disabled}
|
|
307
|
+
selectSX={{ '.MuiOutlinedInput-notchedOutline': { border: 'none' } }}
|
|
308
|
+
/>
|
|
309
|
+
</InputAdornment>
|
|
310
|
+
),
|
|
311
|
+
},
|
|
312
|
+
}}
|
|
313
|
+
/>
|
|
314
|
+
)}
|
|
315
|
+
/>
|
|
316
|
+
</Box>
|
|
317
|
+
{!disabled && (
|
|
318
|
+
<IconButton
|
|
319
|
+
size="small"
|
|
320
|
+
disabled={disabled}
|
|
321
|
+
onClick={() => handleRemoveCurrency(objectMode ? item.currency_id : index)}
|
|
322
|
+
sx={{ mt: 0.5, ml: -1 }}>
|
|
323
|
+
<DeleteOutlineOutlined color="error" sx={{ opacity: 0.75 }} />
|
|
324
|
+
</IconButton>
|
|
325
|
+
)}
|
|
326
|
+
</Stack>
|
|
327
|
+
);
|
|
328
|
+
})}
|
|
329
|
+
</Stack>
|
|
330
|
+
)}
|
|
331
|
+
|
|
332
|
+
{/* Add more currencies */}
|
|
333
|
+
{!disabled && (
|
|
334
|
+
<Box sx={{ width: '100%' }}>
|
|
335
|
+
<CurrencySelect
|
|
336
|
+
mode="waiting"
|
|
337
|
+
hasSelected={(currency) =>
|
|
338
|
+
currencyList.some((x: any) => x.currency_id === currency.id) ||
|
|
339
|
+
(showBaseCurrency && currency.id === baseCurrencyId)
|
|
340
|
+
}
|
|
341
|
+
currencyFilter={(c) => c.type !== 'credit'}
|
|
342
|
+
onSelect={handleAddCurrency}
|
|
343
|
+
value=""
|
|
344
|
+
width="100%"
|
|
345
|
+
/>
|
|
346
|
+
</Box>
|
|
347
|
+
)}
|
|
348
|
+
</Stack>
|
|
349
|
+
);
|
|
350
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
|
|
2
|
+
import { findCurrency, usePaymentContext } from '@blocklet/payment-react';
|
|
3
|
+
import { Box, Typography, Stack } from '@mui/material';
|
|
4
|
+
import { styled } from '@mui/system';
|
|
5
|
+
import { fromUnitToToken } from '@ocap/util';
|
|
6
|
+
|
|
7
|
+
const CurrencyRow = styled(Stack)(({ theme }) => ({
|
|
8
|
+
flexDirection: 'row',
|
|
9
|
+
justifyContent: 'space-between',
|
|
10
|
+
alignItems: 'center',
|
|
11
|
+
padding: theme.spacing(1.5, 2),
|
|
12
|
+
backgroundColor: theme.palette.grey[50],
|
|
13
|
+
borderRadius: theme.shape.borderRadius,
|
|
14
|
+
marginBottom: theme.spacing(1),
|
|
15
|
+
'&:last-child': {
|
|
16
|
+
marginBottom: 0,
|
|
17
|
+
},
|
|
18
|
+
}));
|
|
19
|
+
|
|
20
|
+
const CurrencyLabel = styled(Typography)(({ theme }) => ({
|
|
21
|
+
fontWeight: 500,
|
|
22
|
+
color: theme.palette.text.primary,
|
|
23
|
+
textTransform: 'uppercase',
|
|
24
|
+
fontSize: '0.875rem',
|
|
25
|
+
}));
|
|
26
|
+
|
|
27
|
+
const AmountValue = styled(Typography)(({ theme }) => ({
|
|
28
|
+
color: theme.palette.text.secondary,
|
|
29
|
+
fontSize: '0.875rem',
|
|
30
|
+
fontFamily: 'monospace',
|
|
31
|
+
}));
|
|
32
|
+
|
|
33
|
+
const HeaderRow = styled(Stack)(({ theme }) => ({
|
|
34
|
+
flexDirection: 'row',
|
|
35
|
+
justifyContent: 'space-between',
|
|
36
|
+
alignItems: 'center',
|
|
37
|
+
padding: theme.spacing(1, 2),
|
|
38
|
+
marginBottom: theme.spacing(1),
|
|
39
|
+
borderBottom: `1px solid ${theme.palette.divider}`,
|
|
40
|
+
}));
|
|
41
|
+
|
|
42
|
+
const HeaderLabel = styled(Typography)(({ theme }) => ({
|
|
43
|
+
fontWeight: 600,
|
|
44
|
+
color: theme.palette.text.primary,
|
|
45
|
+
fontSize: '0.75rem',
|
|
46
|
+
letterSpacing: '0.5px',
|
|
47
|
+
}));
|
|
48
|
+
|
|
49
|
+
type Props = {
|
|
50
|
+
currencyOptions: Record<string, string | number | { minimum_amount: string } | { amount_off: string }>;
|
|
51
|
+
type?: 'coupon' | 'promotion_code';
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
export default function CurrencyRestrictions({ currencyOptions, type = 'coupon' }: Props) {
|
|
55
|
+
const { t } = useLocaleContext();
|
|
56
|
+
const { settings } = usePaymentContext();
|
|
57
|
+
|
|
58
|
+
if (!currencyOptions || Object.keys(currencyOptions).length === 0) {
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const currencyEntries = Object.entries(currencyOptions);
|
|
63
|
+
|
|
64
|
+
return (
|
|
65
|
+
<Box>
|
|
66
|
+
<HeaderRow>
|
|
67
|
+
<HeaderLabel>{t('admin.coupon.currency')}</HeaderLabel>
|
|
68
|
+
<HeaderLabel>
|
|
69
|
+
{type === 'coupon' ? t('admin.coupon.discountAmount') : t('admin.coupon.minimumAmount')}
|
|
70
|
+
</HeaderLabel>
|
|
71
|
+
</HeaderRow>
|
|
72
|
+
<Stack>
|
|
73
|
+
{currencyEntries.map(([currencyId, data]) => {
|
|
74
|
+
const currency = findCurrency(settings.paymentMethods, currencyId) || {
|
|
75
|
+
id: currencyId,
|
|
76
|
+
symbol: currencyId.toUpperCase(),
|
|
77
|
+
name: currencyId.toUpperCase(),
|
|
78
|
+
decimal: 18,
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
// Handle different data formats
|
|
82
|
+
let displayAmount: string;
|
|
83
|
+
|
|
84
|
+
if (typeof data === 'string') {
|
|
85
|
+
// Token amount string format (from stored currency_options/restrictions)
|
|
86
|
+
// Convert back to user-friendly display format
|
|
87
|
+
displayAmount = fromUnitToToken(data, currency.decimal);
|
|
88
|
+
} else if (typeof data === 'number') {
|
|
89
|
+
// Direct number format (unit amount) - display as-is
|
|
90
|
+
displayAmount = data.toLocaleString();
|
|
91
|
+
} else if (typeof data === 'object' && data !== null) {
|
|
92
|
+
if ('minimum_amount' in data) {
|
|
93
|
+
// Promotion code format with minimum_amount
|
|
94
|
+
displayAmount = fromUnitToToken((data as { minimum_amount: string }).minimum_amount, currency.decimal);
|
|
95
|
+
} else if ('amount_off' in data) {
|
|
96
|
+
// Coupon format with amount_off (token string)
|
|
97
|
+
displayAmount = fromUnitToToken((data as { amount_off: string }).amount_off, currency.decimal);
|
|
98
|
+
} else {
|
|
99
|
+
// Fallback for unknown object format
|
|
100
|
+
displayAmount = '0';
|
|
101
|
+
}
|
|
102
|
+
} else {
|
|
103
|
+
// Fallback
|
|
104
|
+
displayAmount = '0';
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return (
|
|
108
|
+
<CurrencyRow key={currencyId}>
|
|
109
|
+
<CurrencyLabel>{currency.symbol}</CurrencyLabel>
|
|
110
|
+
<AmountValue>{displayAmount}</AmountValue>
|
|
111
|
+
</CurrencyRow>
|
|
112
|
+
);
|
|
113
|
+
})}
|
|
114
|
+
</Stack>
|
|
115
|
+
</Box>
|
|
116
|
+
);
|
|
117
|
+
}
|