payment-kit 1.15.1 → 1.15.3

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 (43) hide show
  1. package/api/src/crons/payment-stat.ts +1 -0
  2. package/api/src/index.ts +2 -2
  3. package/api/src/integrations/arcblock/stake.ts +17 -10
  4. package/api/src/libs/auth.ts +3 -2
  5. package/api/src/libs/notification/template/customer-reward-succeeded.ts +15 -8
  6. package/api/src/libs/notification/template/one-time-payment-succeeded.ts +1 -30
  7. package/api/src/libs/notification/template/subscription-canceled.ts +45 -23
  8. package/api/src/libs/notification/template/subscription-refund-succeeded.ts +130 -47
  9. package/api/src/libs/notification/template/subscription-renewed.ts +10 -2
  10. package/api/src/libs/notification/template/subscription-stake-slash-succeeded.ts +228 -0
  11. package/api/src/libs/notification/template/subscription-succeeded.ts +2 -2
  12. package/api/src/libs/notification/template/subscription-trial-start.ts +7 -10
  13. package/api/src/libs/notification/template/subscription-trial-will-end.ts +13 -5
  14. package/api/src/libs/notification/template/subscription-will-renew.ts +41 -29
  15. package/api/src/libs/payment.ts +53 -1
  16. package/api/src/libs/subscription.ts +43 -0
  17. package/api/src/locales/en.ts +24 -0
  18. package/api/src/locales/zh.ts +22 -0
  19. package/api/src/queues/invoice.ts +1 -1
  20. package/api/src/queues/notification.ts +9 -0
  21. package/api/src/queues/payment.ts +17 -0
  22. package/api/src/routes/checkout-sessions.ts +13 -1
  23. package/api/src/routes/payment-stats.ts +3 -3
  24. package/api/src/routes/subscriptions.ts +26 -6
  25. package/api/src/store/migrations/20240905-index.ts +100 -0
  26. package/api/src/store/models/subscription.ts +1 -0
  27. package/api/tests/libs/payment.spec.ts +168 -0
  28. package/blocklet.yml +1 -1
  29. package/package.json +10 -10
  30. package/src/components/balance-list.tsx +2 -2
  31. package/src/components/invoice/list.tsx +2 -2
  32. package/src/components/invoice/table.tsx +1 -1
  33. package/src/components/payment-intent/list.tsx +1 -1
  34. package/src/components/payouts/list.tsx +1 -1
  35. package/src/components/refund/list.tsx +2 -2
  36. package/src/components/subscription/actions/cancel.tsx +41 -13
  37. package/src/components/subscription/actions/index.tsx +11 -8
  38. package/src/components/subscription/actions/slash-stake.tsx +52 -0
  39. package/src/locales/en.tsx +1 -0
  40. package/src/locales/zh.tsx +1 -0
  41. package/src/pages/admin/billing/invoices/detail.tsx +2 -2
  42. package/src/pages/customer/refund/list.tsx +1 -1
  43. package/src/pages/customer/subscription/detail.tsx +1 -1
@@ -36,7 +36,8 @@ export default function SubscriptionCancelForm({ data }: { data: TSubscriptionEx
36
36
  setValue('cancel.refund', 'none');
37
37
  }
38
38
  }, [cancelAt, cancelTime]); // eslint-disable-line
39
-
39
+ const stakingDisabled = !staking || staking?.return_amount === '0';
40
+ const refundDisabled = !refund || refund.total === '0';
40
41
  const isCustom = cancelAt === 'custom';
41
42
  const { decimal, symbol } = data.paymentCurrency;
42
43
 
@@ -95,15 +96,15 @@ export default function SubscriptionCancelForm({ data }: { data: TSubscriptionEx
95
96
  <RadioGroup>
96
97
  <FormControlLabel
97
98
  value="none"
98
- disabled={loading || !refund}
99
- onClick={() => !(loading || !refund) && setValue('cancel.refund', 'none')}
99
+ disabled={loading || refundDisabled}
100
+ onClick={() => !(loading || refundDisabled) && setValue('cancel.refund', 'none')}
100
101
  control={<Radio checked={refundType === 'none'} />}
101
102
  label={t('admin.subscription.cancel.refund.none')}
102
103
  />
103
104
  <FormControlLabel
104
105
  value="last"
105
- disabled={loading || !refund}
106
- onClick={() => !(loading || !refund) && setValue('cancel.refund', 'last')}
106
+ disabled={loading || refundDisabled}
107
+ onClick={() => !(loading || refundDisabled) && setValue('cancel.refund', 'last')}
107
108
  control={<Radio checked={refundType === 'last'} />}
108
109
  label={t('admin.subscription.cancel.refund.last', {
109
110
  symbol,
@@ -112,8 +113,8 @@ export default function SubscriptionCancelForm({ data }: { data: TSubscriptionEx
112
113
  />
113
114
  <FormControlLabel
114
115
  value="proration"
115
- disabled={loading || !refund}
116
- onClick={() => !(loading || !refund) && setValue('cancel.refund', 'proration')}
116
+ disabled={loading || refundDisabled}
117
+ onClick={() => !(loading || refundDisabled) && setValue('cancel.refund', 'proration')}
117
118
  control={<Radio checked={refundType === 'proration'} />}
118
119
  label={t('admin.subscription.cancel.refund.proration', {
119
120
  symbol,
@@ -132,15 +133,15 @@ export default function SubscriptionCancelForm({ data }: { data: TSubscriptionEx
132
133
  <RadioGroup>
133
134
  <FormControlLabel
134
135
  value="none"
135
- disabled={loading || !staking}
136
- onClick={() => !(loading || !staking) && setValue('cancel.staking', 'none')}
136
+ disabled={loading || stakingDisabled}
137
+ onClick={() => !(loading || stakingDisabled) && setValue('cancel.staking', 'none')}
137
138
  control={<Radio checked={stakingType === 'none'} />}
138
139
  label={t('admin.subscription.cancel.staking.none')}
139
140
  />
140
141
  <FormControlLabel
141
142
  value="proration"
142
- disabled={loading || !staking}
143
- onClick={() => !(loading || !staking) && setValue('cancel.staking', 'proration')}
143
+ disabled={loading || stakingDisabled}
144
+ onClick={() => !(loading || stakingDisabled) && setValue('cancel.staking', 'proration')}
144
145
  control={<Radio checked={stakingType === 'proration'} />}
145
146
  label={t('admin.subscription.cancel.staking.proration', {
146
147
  unused: formatAmount(staking?.return_amount || '0', decimal),
@@ -149,8 +150,8 @@ export default function SubscriptionCancelForm({ data }: { data: TSubscriptionEx
149
150
  />
150
151
  <FormControlLabel
151
152
  value="slash"
152
- disabled={loading || !staking}
153
- onClick={() => !(loading || !staking) && setValue('cancel.staking', 'slash')}
153
+ disabled={loading || stakingDisabled}
154
+ onClick={() => !(loading || stakingDisabled) && setValue('cancel.staking', 'slash')}
154
155
  control={<Radio checked={stakingType === 'slash'} />}
155
156
  label={t('admin.subscription.cancel.staking.slash', {
156
157
  unused: formatAmount(staking?.slash_amount || '0', decimal),
@@ -159,6 +160,33 @@ export default function SubscriptionCancelForm({ data }: { data: TSubscriptionEx
159
160
  />
160
161
  </RadioGroup>
161
162
  </Stack>
163
+ {stakingType === 'slash' && (
164
+ <Controller
165
+ name="cancel.slashReason"
166
+ control={control}
167
+ rules={{
168
+ required: t('common.required'),
169
+ validate: (value) => value.trim() !== '' || t('common.required'),
170
+ }}
171
+ render={({ field }) => (
172
+ <TextField
173
+ {...field}
174
+ variant="outlined"
175
+ size="small"
176
+ fullWidth
177
+ multiline
178
+ minRows={2}
179
+ maxRows={4}
180
+ placeholder={t('admin.subscription.cancel.staking.slashReason')}
181
+ error={!!(formState.errors as any)?.cancel?.slashReason}
182
+ helperText={(formState.errors as any)?.cancel?.slashReason?.message}
183
+ inputProps={{
184
+ maxLength: 200,
185
+ }}
186
+ />
187
+ )}
188
+ />
189
+ )}
162
190
  </>
163
191
  )}
164
192
  </Root>
@@ -12,6 +12,7 @@ import Actions from '../../actions';
12
12
  import ClickBoundary from '../../click-boundary';
13
13
  import SubscriptionCancelForm from './cancel';
14
14
  import SubscriptionPauseForm from './pause';
15
+ import SlashStakeForm from './slash-stake';
15
16
 
16
17
  type Props = {
17
18
  data: TSubscriptionExpanded;
@@ -30,7 +31,7 @@ const fetchStakingData = (id: string, time: string): Promise<{ return_amount: st
30
31
  function SubscriptionActionsInner({ data, variant, onChange }: Props) {
31
32
  const { t } = useLocaleContext();
32
33
  const navigate = useNavigate();
33
- const { reset, getValues, setError } = useFormContext();
34
+ const { reset, getValues, setError, handleSubmit } = useFormContext();
34
35
  const [state, setState] = useSetState({
35
36
  action: '',
36
37
  loading: false,
@@ -69,7 +70,8 @@ function SubscriptionActionsInner({ data, variant, onChange }: Props) {
69
70
 
70
71
  try {
71
72
  setState({ loading: true });
72
- await api.put(`/api/subscriptions/${data.id}/${action}`, values[action] || {}).then((res) => res.data);
73
+ const key = action === 'slash-stake' ? 'slashStake' : action;
74
+ await api.put(`/api/subscriptions/${data.id}/${action}`, values[key] || {}).then((res) => res.data);
73
75
  Toast.success(t('common.saved'));
74
76
  onChange(state.action);
75
77
  } catch (err) {
@@ -148,7 +150,7 @@ function SubscriptionActionsInner({ data, variant, onChange }: Props) {
148
150
  <Actions variant={variant} actions={actions} onOpenCallback={fetchStakeResultAsync} />
149
151
  {state.action === 'cancel' && (
150
152
  <ConfirmDialog
151
- onConfirm={createHandler('cancel')}
153
+ onConfirm={handleSubmit(createHandler('cancel'))}
152
154
  onCancel={handleCancel}
153
155
  title={t('admin.subscription.cancel.title')}
154
156
  message={<SubscriptionCancelForm data={data} />}
@@ -175,13 +177,10 @@ function SubscriptionActionsInner({ data, variant, onChange }: Props) {
175
177
  )}
176
178
  {state.action === 'slashStake' && (
177
179
  <ConfirmDialog
178
- onConfirm={createHandler('slash-stake')}
180
+ onConfirm={handleSubmit(createHandler('slash-stake'))}
179
181
  onCancel={handleCancel}
180
182
  title={t('admin.subscription.cancel.staking.slashTitle')}
181
- message={t('admin.subscription.cancel.staking.slashTip', {
182
- unused: stakeValue,
183
- symbol: data.paymentCurrency?.symbol,
184
- })}
183
+ message={<SlashStakeForm data={data} stakeValue={stakeValue} />}
185
184
  loading={state.loading}
186
185
  />
187
186
  )}
@@ -197,12 +196,16 @@ export default function SubscriptionActions(props: Props) {
197
196
  time: '',
198
197
  refund: 'none',
199
198
  staking: 'none',
199
+ slashReason: '',
200
200
  },
201
201
  pause: {
202
202
  type: 'never',
203
203
  resumesAt: '',
204
204
  behavior: 'void',
205
205
  },
206
+ slashStake: {
207
+ slashReason: '',
208
+ },
206
209
  },
207
210
  });
208
211
 
@@ -0,0 +1,52 @@
1
+ import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
2
+ import type { TSubscriptionExpanded } from '@blocklet/payment-types';
3
+ import { Box, TextField, styled } from '@mui/material';
4
+ import { Controller, useFormContext } from 'react-hook-form';
5
+
6
+ export default function SlashStakeForm({ data, stakeValue }: { data: TSubscriptionExpanded; stakeValue: string }) {
7
+ const { t } = useLocaleContext();
8
+ const { control, formState } = useFormContext();
9
+
10
+ return (
11
+ <Root sx={{ width: 400 }}>
12
+ {t('admin.subscription.cancel.staking.slashTip', {
13
+ unused: stakeValue,
14
+ symbol: data.paymentCurrency?.symbol,
15
+ })}
16
+ <Controller
17
+ name="slashStake.slashReason"
18
+ control={control}
19
+ rules={{
20
+ required: t('common.required'),
21
+ validate: (value) => {
22
+ return value.trim() !== '' || t('common.required');
23
+ },
24
+ }}
25
+ render={({ field }) => (
26
+ <TextField
27
+ {...field}
28
+ variant="outlined"
29
+ size="small"
30
+ fullWidth
31
+ multiline
32
+ minRows={2}
33
+ maxRows={4}
34
+ placeholder={t('admin.subscription.cancel.staking.slashReason')}
35
+ error={!!(formState.errors as any)?.slashStake?.slashReason}
36
+ helperText={(formState.errors as any)?.slashStake?.slashReason?.message}
37
+ inputProps={{
38
+ maxLength: 200,
39
+ }}
40
+ sx={{ mt: 1 }}
41
+ />
42
+ )}
43
+ />
44
+ </Root>
45
+ );
46
+ }
47
+
48
+ const Root = styled(Box)`
49
+ .form-title {
50
+ width: 60px;
51
+ }
52
+ `;
@@ -469,6 +469,7 @@ export default flat({
469
469
  none: 'No return or slash',
470
470
  proration: 'Return Remaining Stake {unused}{symbol}',
471
471
  slash: 'Slash Remaining Stake {unused}{symbol}',
472
+ slashReason: 'Slash Reason',
472
473
  slashTip:
473
474
  'The remaining stake of this subscription {unused}{symbol} will be slashed, please confirm to continue?',
474
475
  slashTitle: 'Slash stake',
@@ -459,6 +459,7 @@ export default flat({
459
459
  none: '不退还 / 罚没质押',
460
460
  proration: '退还剩余部分 {unused}{symbol}',
461
461
  slash: '罚没剩余部分 {unused}{symbol}',
462
+ slashReason: '罚没原因',
462
463
  slashTip: '该订阅剩余的质押部分 {unused}{symbol} 将被罚没, 请确认是否继续?',
463
464
  slashTitle: '罚没质押',
464
465
  },
@@ -289,7 +289,7 @@ export default function InvoiceDetail(props: { id: string }) {
289
289
  />
290
290
  <InfoRow
291
291
  label={t('admin.paymentMethod._name')}
292
- value={<Currency logo={data.paymentMethod.logo} name={data.paymentMethod.name} />}
292
+ value={<Currency logo={data.paymentMethod?.logo} name={data.paymentMethod?.name} />}
293
293
  direction={InfoDirection}
294
294
  alignItems={InfoAlignItems}
295
295
  />
@@ -298,7 +298,7 @@ export default function InvoiceDetail(props: { id: string }) {
298
298
  value={
299
299
  <Currency
300
300
  logo={data.paymentCurrency.logo}
301
- name={`${data.paymentCurrency.symbol} (${data.paymentMethod.name})`}
301
+ name={`${data.paymentCurrency.symbol} (${data.paymentMethod?.name})`}
302
302
  />
303
303
  }
304
304
  direction={InfoDirection}
@@ -95,7 +95,7 @@ const RefundTable = memo(({ invoice_id }: Props) => {
95
95
  {
96
96
  label: t('common.type'),
97
97
  name: 'type',
98
- width: 60,
98
+ width: 80,
99
99
  options: {
100
100
  filter: true,
101
101
  customBodyRenderLite: (_: string, index: number) => {
@@ -201,7 +201,7 @@ export default function CustomerSubscriptionDetail() {
201
201
  />
202
202
  <InfoRow
203
203
  label={t('admin.paymentMethod._name')}
204
- value={<Currency logo={data.paymentMethod.logo} name={data.paymentMethod.name} />}
204
+ value={<Currency logo={data.paymentMethod?.logo} name={data.paymentMethod?.name} />}
205
205
  alignItems={InfoAlignItems}
206
206
  direction={InfoDirection}
207
207
  />