payment-kit 1.20.11 → 1.20.13

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 (92) hide show
  1. package/api/src/crons/index.ts +8 -0
  2. package/api/src/index.ts +2 -0
  3. package/api/src/integrations/stripe/handlers/invoice.ts +63 -5
  4. package/api/src/integrations/stripe/handlers/payment-intent.ts +1 -0
  5. package/api/src/integrations/stripe/resource.ts +253 -2
  6. package/api/src/libs/currency.ts +31 -0
  7. package/api/src/libs/discount/coupon.ts +1061 -0
  8. package/api/src/libs/discount/discount.ts +349 -0
  9. package/api/src/libs/discount/nft.ts +239 -0
  10. package/api/src/libs/discount/redemption.ts +636 -0
  11. package/api/src/libs/discount/vc.ts +73 -0
  12. package/api/src/libs/env.ts +1 -0
  13. package/api/src/libs/invoice.ts +44 -10
  14. package/api/src/libs/math-utils.ts +6 -0
  15. package/api/src/libs/price.ts +43 -0
  16. package/api/src/libs/session.ts +242 -57
  17. package/api/src/libs/subscription.ts +2 -6
  18. package/api/src/libs/vendor-util/adapters/launcher-adapter.ts +1 -1
  19. package/api/src/libs/vendor-util/adapters/types.ts +1 -0
  20. package/api/src/libs/vendor-util/fulfillment.ts +1 -1
  21. package/api/src/queues/auto-recharge.ts +1 -1
  22. package/api/src/queues/discount-status.ts +200 -0
  23. package/api/src/queues/subscription.ts +98 -5
  24. package/api/src/queues/usage-record.ts +1 -1
  25. package/api/src/queues/vendors/fulfillment-coordinator.ts +1 -29
  26. package/api/src/queues/vendors/return-processor.ts +184 -0
  27. package/api/src/queues/vendors/return-scanner.ts +119 -0
  28. package/api/src/queues/vendors/status-check.ts +1 -1
  29. package/api/src/routes/auto-recharge-configs.ts +5 -3
  30. package/api/src/routes/checkout-sessions.ts +755 -64
  31. package/api/src/routes/connect/change-payment.ts +6 -1
  32. package/api/src/routes/connect/change-plan.ts +6 -1
  33. package/api/src/routes/connect/setup.ts +6 -1
  34. package/api/src/routes/connect/shared.ts +80 -9
  35. package/api/src/routes/connect/subscribe.ts +12 -2
  36. package/api/src/routes/coupons.ts +518 -0
  37. package/api/src/routes/index.ts +4 -0
  38. package/api/src/routes/invoices.ts +44 -3
  39. package/api/src/routes/meter-events.ts +2 -1
  40. package/api/src/routes/payment-currencies.ts +1 -0
  41. package/api/src/routes/promotion-codes.ts +482 -0
  42. package/api/src/routes/subscriptions.ts +23 -2
  43. package/api/src/routes/vendor.ts +89 -2
  44. package/api/src/store/migrations/20250904-discount.ts +136 -0
  45. package/api/src/store/migrations/20250910-timestamp-fields.ts +116 -0
  46. package/api/src/store/migrations/20250916-add-description-fields.ts +30 -0
  47. package/api/src/store/migrations/20250918-add-vendor-extends.ts +20 -0
  48. package/api/src/store/models/checkout-session.ts +17 -2
  49. package/api/src/store/models/coupon.ts +144 -4
  50. package/api/src/store/models/discount.ts +23 -10
  51. package/api/src/store/models/index.ts +13 -2
  52. package/api/src/store/models/product-vendor.ts +6 -0
  53. package/api/src/store/models/promotion-code.ts +295 -18
  54. package/api/src/store/models/types.ts +30 -1
  55. package/api/tests/libs/session.spec.ts +48 -27
  56. package/blocklet.yml +1 -1
  57. package/package.json +20 -20
  58. package/src/app.tsx +2 -0
  59. package/src/components/customer/link.tsx +1 -1
  60. package/src/components/discount/discount-info.tsx +178 -0
  61. package/src/components/invoice/table.tsx +140 -48
  62. package/src/components/invoice-pdf/styles.ts +6 -0
  63. package/src/components/invoice-pdf/template.tsx +59 -33
  64. package/src/components/metadata/form.tsx +14 -5
  65. package/src/components/payment-link/actions.tsx +42 -0
  66. package/src/components/price/form.tsx +91 -65
  67. package/src/components/product/vendor-config.tsx +5 -3
  68. package/src/components/promotion/active-redemptions.tsx +534 -0
  69. package/src/components/promotion/currency-multi-select.tsx +350 -0
  70. package/src/components/promotion/currency-restrictions.tsx +117 -0
  71. package/src/components/promotion/product-select.tsx +292 -0
  72. package/src/components/promotion/promotion-code-form.tsx +534 -0
  73. package/src/components/subscription/portal/list.tsx +6 -1
  74. package/src/components/subscription/vendor-service-list.tsx +13 -2
  75. package/src/locales/en.tsx +227 -0
  76. package/src/locales/zh.tsx +222 -1
  77. package/src/pages/admin/billing/subscriptions/detail.tsx +5 -0
  78. package/src/pages/admin/products/coupons/applicable-products.tsx +166 -0
  79. package/src/pages/admin/products/coupons/create.tsx +612 -0
  80. package/src/pages/admin/products/coupons/detail.tsx +538 -0
  81. package/src/pages/admin/products/coupons/edit.tsx +127 -0
  82. package/src/pages/admin/products/coupons/index.tsx +210 -3
  83. package/src/pages/admin/products/index.tsx +22 -3
  84. package/src/pages/admin/products/products/detail.tsx +12 -2
  85. package/src/pages/admin/products/promotion-codes/actions.tsx +103 -0
  86. package/src/pages/admin/products/promotion-codes/create.tsx +235 -0
  87. package/src/pages/admin/products/promotion-codes/detail.tsx +416 -0
  88. package/src/pages/admin/products/promotion-codes/list.tsx +247 -0
  89. package/src/pages/admin/products/promotion-codes/verification-config.tsx +327 -0
  90. package/src/pages/admin/products/vendors/index.tsx +17 -5
  91. package/src/pages/customer/subscription/detail.tsx +5 -0
  92. package/vite.config.ts +4 -3
@@ -0,0 +1,534 @@
1
+ import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
2
+ import {
3
+ Box,
4
+ TextField,
5
+ FormControl,
6
+ Select,
7
+ MenuItem,
8
+ FormControlLabel,
9
+ Checkbox,
10
+ Typography,
11
+ Chip,
12
+ Stack,
13
+ InputAdornment,
14
+ Button,
15
+ Divider,
16
+ Autocomplete,
17
+ Avatar,
18
+ } from '@mui/material';
19
+ import { Controller, useFormContext, useWatch } from 'react-hook-form';
20
+ import { FormInput, FormLabel, api, getCustomerAvatar } from '@blocklet/payment-react';
21
+ import { useRequest } from 'ahooks';
22
+ import { useEffect } from 'react';
23
+ import CurrencyMultiSelect from './currency-multi-select';
24
+
25
+ export type PromotionCodeData = {
26
+ code: string;
27
+ description?: string;
28
+ active: boolean;
29
+ max_redemptions?: number;
30
+ expires_at?: string;
31
+ verification_type: 'code' | 'nft' | 'vc' | 'user_restricted';
32
+ limit_number_redemptions?: boolean;
33
+ add_expiration_date?: boolean;
34
+ restrictions?: {
35
+ first_time_transaction?: boolean;
36
+ require_minimum_amount?: boolean;
37
+ minimum_amount?: number;
38
+ minimum_amount_currency?: string;
39
+ currency_options?: Record<string, { minimum_amount: number }>;
40
+ };
41
+ customer_dids?: string[];
42
+ nft_addresses?: string[];
43
+ nft_tags?: string[];
44
+ trusted_issuers?: string[];
45
+ trusted_parents?: string[];
46
+ min_balance?: number;
47
+ vc_roles?: string[];
48
+ vc_trusted_issuers?: string[];
49
+ };
50
+
51
+ export const DEFAULT_PROMOTION_CODE: PromotionCodeData = {
52
+ code: '',
53
+ description: '',
54
+ active: true,
55
+ verification_type: 'code',
56
+ limit_number_redemptions: false,
57
+ add_expiration_date: false,
58
+ restrictions: {
59
+ first_time_transaction: false,
60
+ require_minimum_amount: false,
61
+ currency_options: {},
62
+ },
63
+ customer_dids: [],
64
+ nft_addresses: [],
65
+ nft_tags: [],
66
+ trusted_issuers: [],
67
+ trusted_parents: [],
68
+ min_balance: 1,
69
+ vc_roles: [],
70
+ vc_trusted_issuers: [],
71
+ };
72
+
73
+ type PromotionCodeFormProps = {
74
+ prefix?: string;
75
+ showCodeInput?: boolean;
76
+ showExpandedOptions?: boolean;
77
+ couponType?: 'percentage' | 'fixed_amount';
78
+ availableCurrencies?: Array<{
79
+ id: string;
80
+ symbol: string;
81
+ name: string;
82
+ }>;
83
+ };
84
+
85
+ const fetchCustomers = (): Promise<{ list: any[]; count: number }> => {
86
+ return api.get('/api/customers?limit=1000').then((res: any) => res.data);
87
+ };
88
+
89
+ export default function PromotionCodeForm({
90
+ prefix = '',
91
+ showCodeInput = false,
92
+ showExpandedOptions = true,
93
+ availableCurrencies = [],
94
+ }: Omit<PromotionCodeFormProps, 'couponType'>) {
95
+ const { t } = useLocaleContext();
96
+ const { control, setValue, watch } = useFormContext();
97
+
98
+ const getFieldName = (name: string) => (prefix ? `${prefix}.${name}` : name);
99
+
100
+ const { data: customersData } = useRequest(fetchCustomers);
101
+
102
+ const watchVerificationType = useWatch({ control, name: getFieldName('verification_type') });
103
+ const watchLimitNumberRedemptions = useWatch({ control, name: getFieldName('limit_number_redemptions') });
104
+ const watchAddExpirationDate = useWatch({ control, name: getFieldName('add_expiration_date') });
105
+ const watchRequireMinimumAmount = useWatch({ control, name: getFieldName('restrictions.require_minimum_amount') });
106
+ const watchCurrencyOptions = useWatch({ control, name: getFieldName('restrictions.currency_options') });
107
+
108
+ const customers = customersData?.list || [];
109
+
110
+ // sync currency_options first currency to base fields
111
+ useEffect(() => {
112
+ if (watchRequireMinimumAmount && watchCurrencyOptions && typeof watchCurrencyOptions === 'object') {
113
+ const currencyEntries = Object.entries(watchCurrencyOptions);
114
+ if (currencyEntries.length > 0) {
115
+ const firstEntry = currencyEntries[0];
116
+ if (firstEntry) {
117
+ const [firstCurrencyId, firstCurrencyData] = firstEntry;
118
+ const amount = (firstCurrencyData as any).minimum_amount || 0;
119
+ setValue(getFieldName('restrictions.minimum_amount'), amount, { shouldValidate: false });
120
+ setValue(getFieldName('restrictions.minimum_amount_currency'), firstCurrencyId, { shouldValidate: false });
121
+ }
122
+ }
123
+ } else if (!watchRequireMinimumAmount) {
124
+ setValue(getFieldName('restrictions.minimum_amount'), undefined, { shouldValidate: false });
125
+ setValue(getFieldName('restrictions.minimum_amount_currency'), undefined, { shouldValidate: false });
126
+ }
127
+ }, [watchRequireMinimumAmount, watchCurrencyOptions, setValue, getFieldName]);
128
+
129
+ const generatePromotionCode = () => {
130
+ return Math.random().toString(36).substring(2, 12).toUpperCase();
131
+ };
132
+
133
+ const getCustomerDisplayName = (customer: any) => {
134
+ return customer.name || customer.email || customer.did || customer.id;
135
+ };
136
+
137
+ const handleArrayChange = (fieldName: string, value: string) => {
138
+ const currentValue = watch(getFieldName(fieldName)) || [];
139
+ if (value.trim()) {
140
+ const newArray = [...currentValue, value.trim()];
141
+ setValue(getFieldName(fieldName), newArray);
142
+ }
143
+ };
144
+
145
+ const handleArrayRemove = (fieldName: string, index: number) => {
146
+ const currentValue = watch(getFieldName(fieldName)) || [];
147
+ const newArray = currentValue.filter((_: any, i: number) => i !== index);
148
+ setValue(getFieldName(fieldName), newArray);
149
+ };
150
+
151
+ const renderArrayField = (fieldName: string, label: string, placeholder: string) => {
152
+ const currentValue = watch(getFieldName(fieldName)) || [];
153
+
154
+ return (
155
+ <Box sx={{ position: 'relative' }}>
156
+ <TextField
157
+ label={label}
158
+ placeholder={placeholder}
159
+ fullWidth
160
+ size="small"
161
+ onKeyPress={(e) => {
162
+ if (e.key === 'Enter') {
163
+ e.preventDefault();
164
+ const { value } = e.target as HTMLInputElement;
165
+ handleArrayChange(fieldName, value);
166
+ (e.target as HTMLInputElement).value = '';
167
+ }
168
+ }}
169
+ />
170
+ <Box
171
+ sx={{
172
+ position: 'absolute',
173
+ top: 8,
174
+ right: 12,
175
+ fontSize: '0.75rem',
176
+ color: 'text.secondary',
177
+ pointerEvents: 'none',
178
+ zIndex: 1,
179
+ }}>
180
+ Press Enter
181
+ </Box>
182
+ {currentValue.length > 0 && (
183
+ <Stack direction="row" spacing={1} sx={{ mt: 1, flexWrap: 'wrap' }}>
184
+ {currentValue.map((item: string, index: number) => (
185
+ <Chip key={item} label={item} onDelete={() => handleArrayRemove(fieldName, index)} size="small" />
186
+ ))}
187
+ </Stack>
188
+ )}
189
+ </Box>
190
+ );
191
+ };
192
+
193
+ const renderMultiCurrencyMinimumAmount = () => {
194
+ return (
195
+ <Box>
196
+ <Typography
197
+ variant="body2"
198
+ sx={{
199
+ color: 'text.secondary',
200
+ mb: 2,
201
+ }}>
202
+ {t('admin.coupon.minimumAmountHelp')}
203
+ </Typography>
204
+ <CurrencyMultiSelect
205
+ prefix={getFieldName('restrictions')}
206
+ baseCurrencyFieldName="minimum_amount_currency"
207
+ currencyOptionsFieldName="currency_options"
208
+ unitAmountFieldName="minimum_amount"
209
+ objectMode
210
+ showBaseCurrency={false}
211
+ />
212
+ </Box>
213
+ );
214
+ };
215
+
216
+ const renderCustomerSelection = () => {
217
+ const currentValue = watch(getFieldName('customer_dids')) || [];
218
+ const selectedOptions: (any | string)[] = [];
219
+
220
+ customers.forEach((customer: any) => {
221
+ if (currentValue.includes(customer.did) || currentValue.includes(customer.id)) {
222
+ selectedOptions.push(customer);
223
+ }
224
+ });
225
+
226
+ currentValue.forEach((did: string) => {
227
+ if (!customers.some((c: any) => c.did === did || c.id === did)) {
228
+ selectedOptions.push(did);
229
+ }
230
+ });
231
+
232
+ return (
233
+ <Box>
234
+ <Autocomplete
235
+ multiple
236
+ options={customers}
237
+ getOptionLabel={getCustomerDisplayName}
238
+ value={selectedOptions}
239
+ onChange={(_, newValue) => {
240
+ const dids = newValue.map((item: any) => {
241
+ if (typeof item === 'string') {
242
+ return item;
243
+ }
244
+ return item.did || item.id;
245
+ });
246
+ setValue(getFieldName('customer_dids'), dids);
247
+ }}
248
+ freeSolo
249
+ renderInput={(params) => (
250
+ <TextField
251
+ {...params}
252
+ label={t('admin.coupon.customerDids')}
253
+ placeholder={t('admin.coupon.customerDidsPlaceholder')}
254
+ size="small"
255
+ helperText={t('admin.coupon.customerDidsHelp')}
256
+ />
257
+ )}
258
+ renderOption={(props, option) => (
259
+ <Box component="li" {...props} sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
260
+ <Avatar
261
+ src={getCustomerAvatar(
262
+ option.did,
263
+ option?.updated_at ? new Date(option.updated_at).toISOString() : undefined
264
+ )}
265
+ sx={{ width: 24, height: 24 }}
266
+ alt={getCustomerDisplayName(option)}
267
+ />
268
+ <Typography variant="body2">{getCustomerDisplayName(option)}</Typography>
269
+ </Box>
270
+ )}
271
+ renderTags={(tagValues, getTagProps) =>
272
+ tagValues.map((option, index) => {
273
+ const isCustomer = typeof option === 'object';
274
+ const displayLabel = isCustomer ? getCustomerDisplayName(option) : option;
275
+
276
+ return (
277
+ <Chip
278
+ variant="outlined"
279
+ label={displayLabel}
280
+ size="small"
281
+ color={isCustomer ? 'primary' : 'default'}
282
+ avatar={
283
+ isCustomer ? (
284
+ <Avatar
285
+ src={getCustomerAvatar(
286
+ option.did,
287
+ option?.updated_at ? new Date(option.updated_at).toISOString() : undefined
288
+ )}
289
+ sx={{ width: 20, height: 20 }}
290
+ alt={getCustomerDisplayName(option)}
291
+ />
292
+ ) : undefined
293
+ }
294
+ {...getTagProps({ index })}
295
+ key={isCustomer ? option.id : option}
296
+ />
297
+ );
298
+ })
299
+ }
300
+ filterOptions={(options, params) => {
301
+ const filtered = options.filter((option) =>
302
+ getCustomerDisplayName(option).toLowerCase().includes(params.inputValue.toLowerCase())
303
+ );
304
+
305
+ const { inputValue } = params;
306
+ const isExisting = options.some((option) => getCustomerDisplayName(option) === inputValue);
307
+ if (inputValue !== '' && !isExisting && inputValue.length > 10) {
308
+ filtered.push(inputValue as any);
309
+ }
310
+
311
+ return filtered;
312
+ }}
313
+ />
314
+ </Box>
315
+ );
316
+ };
317
+
318
+ return (
319
+ <Box sx={{ display: 'flex', flexDirection: 'column', gap: 3 }}>
320
+ {showCodeInput && (
321
+ <Box sx={{ display: 'flex', gap: 2, alignItems: 'flex-end' }}>
322
+ <FormInput
323
+ name={getFieldName('code')}
324
+ label={t('admin.coupon.codeOptional')}
325
+ placeholder="FRIENDS20"
326
+ helperText={t('admin.coupon.codeHelp')}
327
+ InputProps={{
328
+ endAdornment: (
329
+ <InputAdornment position="end">
330
+ <Button
331
+ size="small"
332
+ onClick={() => {
333
+ setValue(getFieldName('code'), generatePromotionCode());
334
+ }}>
335
+ {t('admin.coupon.generate')}
336
+ </Button>
337
+ </InputAdornment>
338
+ ),
339
+ }}
340
+ />
341
+ </Box>
342
+ )}
343
+
344
+ <FormInput
345
+ name={getFieldName('description')}
346
+ label={t('admin.coupon.desc')}
347
+ placeholder={t('admin.coupon.desc')}
348
+ multiline
349
+ minRows={2}
350
+ maxRows={4}
351
+ slotProps={{
352
+ htmlInput: {
353
+ maxLength: 250,
354
+ },
355
+ }}
356
+ />
357
+
358
+ <FormControl component="fieldset" fullWidth>
359
+ <FormLabel component="legend" sx={{ mb: 1, fontSize: '0.875rem', fontWeight: 500 }}>
360
+ {t('admin.coupon.verificationType')}
361
+ </FormLabel>
362
+ <Controller
363
+ name={getFieldName('verification_type')}
364
+ control={control}
365
+ render={({ field }) => (
366
+ <Select {...field} size="small" displayEmpty>
367
+ <MenuItem value="code">{t('admin.coupon.codeOnly')}</MenuItem>
368
+ <MenuItem value="nft" disabled>
369
+ {t('admin.coupon.nftVerification')}
370
+ </MenuItem>
371
+ <MenuItem value="vc" disabled>
372
+ {t('admin.coupon.vcVerification')}
373
+ </MenuItem>
374
+ <MenuItem value="user_restricted">{t('admin.coupon.userWhitelist')}</MenuItem>
375
+ </Select>
376
+ )}
377
+ />
378
+ </FormControl>
379
+
380
+ {watchVerificationType === 'nft' && (
381
+ <Box sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
382
+ <Typography variant="h6">{t('admin.coupon.nftSettings')}</Typography>
383
+ {renderArrayField('nft_addresses', t('admin.coupon.nftAddresses'), t('admin.coupon.nftAddressesPlaceholder'))}
384
+ {renderArrayField('nft_tags', t('admin.coupon.nftTags'), t('admin.coupon.nftTagsPlaceholder'))}
385
+ {renderArrayField(
386
+ 'trusted_issuers',
387
+ t('admin.coupon.trustedIssuers'),
388
+ t('admin.coupon.trustedIssuersPlaceholder')
389
+ )}
390
+ {renderArrayField(
391
+ 'trusted_parents',
392
+ t('admin.coupon.trustedParents'),
393
+ t('admin.coupon.trustedParentsPlaceholder')
394
+ )}
395
+
396
+ <FormInput
397
+ name={getFieldName('min_balance')}
398
+ label={t('admin.coupon.minBalance')}
399
+ type="number"
400
+ rules={{ min: 1 }}
401
+ />
402
+ </Box>
403
+ )}
404
+
405
+ {watchVerificationType === 'vc' && (
406
+ <Box sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
407
+ <Typography variant="h6">{t('admin.coupon.vcSettings')}</Typography>
408
+ {renderArrayField('vc_roles', t('admin.coupon.requiredRoles'), t('admin.coupon.requiredRolesPlaceholder'))}
409
+ {renderArrayField(
410
+ 'vc_trusted_issuers',
411
+ t('admin.coupon.trustedVcIssuers'),
412
+ t('admin.coupon.trustedVcIssuersPlaceholder')
413
+ )}
414
+ </Box>
415
+ )}
416
+
417
+ {watchVerificationType === 'user_restricted' && (
418
+ <Box sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
419
+ <Typography variant="h6">{t('admin.coupon.userWhitelistSettings')}</Typography>
420
+ {renderCustomerSelection()}
421
+ </Box>
422
+ )}
423
+
424
+ {showExpandedOptions && (
425
+ <>
426
+ <Divider />
427
+ <Box sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
428
+ <Controller
429
+ name={getFieldName('restrictions.first_time_transaction')}
430
+ control={control}
431
+ render={({ field }) => (
432
+ <FormControlLabel
433
+ control={<Checkbox {...field} checked={field.value || false} />}
434
+ label={t('admin.coupon.eligibleFirstTime')}
435
+ sx={{
436
+ alignItems: 'flex-start',
437
+ }}
438
+ />
439
+ )}
440
+ />
441
+
442
+ <Controller
443
+ name={getFieldName('limit_number_redemptions')}
444
+ control={control}
445
+ render={({ field }) => (
446
+ <FormControlLabel
447
+ control={<Checkbox {...field} checked={field.value} />}
448
+ label={t('admin.coupon.limitNumberRedemptions')}
449
+ sx={{
450
+ alignItems: 'flex-start',
451
+ }}
452
+ />
453
+ )}
454
+ />
455
+
456
+ {watchLimitNumberRedemptions && (
457
+ <FormInput
458
+ name={getFieldName('max_redemptions')}
459
+ label={t('admin.coupon.maxRedemptions')}
460
+ type="number"
461
+ slotProps={{
462
+ input: {
463
+ endAdornment: <InputAdornment position="end">{t('admin.coupon.times')}</InputAdornment>,
464
+ },
465
+ }}
466
+ />
467
+ )}
468
+
469
+ <Controller
470
+ name={getFieldName('add_expiration_date')}
471
+ control={control}
472
+ render={({ field }) => (
473
+ <FormControlLabel
474
+ control={<Checkbox {...field} checked={field.value} />}
475
+ label={t('admin.coupon.addExpirationDate')}
476
+ sx={{
477
+ alignItems: 'flex-start',
478
+ }}
479
+ />
480
+ )}
481
+ />
482
+
483
+ {watchAddExpirationDate && (
484
+ <FormInput
485
+ name={getFieldName('expires_at')}
486
+ label={t('admin.coupon.expiresAt')}
487
+ type="datetime-local"
488
+ InputLabelProps={{ shrink: true }}
489
+ />
490
+ )}
491
+
492
+ <Controller
493
+ name={getFieldName('restrictions.require_minimum_amount')}
494
+ control={control}
495
+ render={({ field }) => (
496
+ <FormControlLabel
497
+ control={
498
+ <Checkbox
499
+ {...field}
500
+ checked={field.value || false}
501
+ onChange={(e) => {
502
+ field.onChange(e.target.checked);
503
+ if (e.target.checked) {
504
+ const firstCurrency = availableCurrencies[0];
505
+ if (firstCurrency) {
506
+ setValue(
507
+ getFieldName('restrictions.currency_options'),
508
+ {
509
+ [firstCurrency.id]: { minimum_amount: 0 },
510
+ },
511
+ { shouldValidate: true }
512
+ );
513
+ }
514
+ } else {
515
+ setValue(getFieldName('restrictions.currency_options'), {}, { shouldValidate: true });
516
+ }
517
+ }}
518
+ />
519
+ }
520
+ label={t('admin.coupon.requireMinimumOrder')}
521
+ sx={{
522
+ alignItems: 'flex-start',
523
+ }}
524
+ />
525
+ )}
526
+ />
527
+
528
+ {watchRequireMinimumAmount && renderMultiCurrencyMinimumAmount()}
529
+ </Box>
530
+ </>
531
+ )}
532
+ </Box>
533
+ );
534
+ }
@@ -218,7 +218,12 @@ export default function CurrentSubscriptions({
218
218
  </AvatarGroup>
219
219
  )}
220
220
 
221
- <Stack direction="column" spacing={0.25} width="100%">
221
+ <Stack
222
+ direction="column"
223
+ spacing={0.25}
224
+ sx={{
225
+ width: '100%',
226
+ }}>
222
227
  <SubscriptionDescription
223
228
  subscription={subscription}
224
229
  hideSubscription
@@ -54,8 +54,19 @@ export default function VendorServiceList({ vendorServices, subscriptionId }: Ve
54
54
  },
55
55
  transition: 'background-color 0.2s ease',
56
56
  }}>
57
- <Stack direction="row" justifyContent="space-between" alignItems="flex-start">
58
- <Stack direction="row" alignItems="center" spacing={1} flex={1}>
57
+ <Stack
58
+ direction="row"
59
+ sx={{
60
+ justifyContent: 'space-between',
61
+ alignItems: 'flex-start',
62
+ }}>
63
+ <Stack
64
+ direction="row"
65
+ spacing={1}
66
+ sx={{
67
+ alignItems: 'center',
68
+ flex: 1,
69
+ }}>
59
70
  <Box
60
71
  sx={{
61
72
  width: 8,