payment-kit 1.13.149 → 1.13.151

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.
@@ -3,6 +3,7 @@ import merge from 'lodash/merge';
3
3
 
4
4
  import logger from '../../libs/logger';
5
5
  import { getPriceUintAmountByCurrency } from '../../libs/session';
6
+ import { getSubscriptionItemPrice } from '../../libs/subscription';
6
7
  import {
7
8
  Customer,
8
9
  PaymentCurrency,
@@ -244,7 +245,7 @@ export async function ensureStripeSubscription(
244
245
  await Promise.all(
245
246
  stripeSubscription.items.data.map(async (x: any) => {
246
247
  const item = prices.find((y) => y.stripePrice.id === x.price.id);
247
- const price = item.upsell_price || item.price; // local
248
+ const price = getSubscriptionItemPrice(item); // local
248
249
  let exist = await SubscriptionItem.findOne({
249
250
  where: { price_id: price.id, subscription_id: internal.id },
250
251
  });
@@ -76,7 +76,7 @@ export class SubscriptionRefundSucceededEmailTemplate
76
76
  const productName = await getMainProductName(refund.subscription_id!);
77
77
  const at: string = formatTime(refund.created_at);
78
78
 
79
- const paymentInfo: string = `${fromUnitToToken(paymentIntent?.amount, paymentCurrency.decimal)} ${
79
+ const paymentInfo: string = `${fromUnitToToken(paymentIntent?.amount_received || '0', paymentCurrency.decimal)} ${
80
80
  paymentCurrency.symbol
81
81
  }`;
82
82
  const refundInfo: string = `${fromUnitToToken(refund.amount, paymentCurrency.decimal)} ${paymentCurrency.symbol}`;
@@ -97,14 +97,14 @@ export function getSubscriptionCreateSetup(items: TLineItemExpanded[], currencyI
97
97
  let setup = new BN(0);
98
98
 
99
99
  items.forEach((x) => {
100
- const price = x.upsell_price || x.price;
100
+ const price = getSubscriptionItemPrice(x);
101
101
  const unit = getPriceUintAmountByCurrency(price, currencyId);
102
102
  if (price.type === 'one_time' || (price.type === 'recurring' && price.recurring?.usage_type === 'licensed')) {
103
103
  setup = setup.add(new BN(unit).mul(new BN(x.quantity)));
104
104
  }
105
105
  });
106
106
 
107
- const item = items.find((x) => x.price.type === 'recurring');
107
+ const item = items.find((x) => getSubscriptionItemPrice(x).type === 'recurring');
108
108
  const recurring = (item?.upsell_price || item?.price)?.recurring as PriceRecurring;
109
109
  const cycle = getRecurringPeriod(recurring);
110
110
  const trial = trialInDays ? trialInDays * 24 * 60 * 60 * 1000 : 0;
@@ -148,7 +148,9 @@ export function getSubscriptionCycleAmount(items: TLineItemExpanded[], currencyI
148
148
  let amount = new BN(0);
149
149
 
150
150
  items.forEach((x) => {
151
- amount = amount.add(new BN(getPriceUintAmountByCurrency(x.price, currencyId)).mul(new BN(x.quantity)));
151
+ amount = amount.add(
152
+ new BN(getPriceUintAmountByCurrency(getSubscriptionItemPrice(x), currencyId)).mul(new BN(x.quantity))
153
+ );
152
154
  });
153
155
 
154
156
  return {
@@ -156,6 +158,10 @@ export function getSubscriptionCycleAmount(items: TLineItemExpanded[], currencyI
156
158
  };
157
159
  }
158
160
 
161
+ export function getSubscriptionItemPrice(item: TLineItemExpanded) {
162
+ return item.upsell_price || item.price;
163
+ }
164
+
159
165
  export async function createProration(
160
166
  subscription: Subscription,
161
167
  setup: ReturnType<typeof getSubscriptionCreateSetup>,
@@ -175,9 +181,10 @@ export async function createProration(
175
181
  // 1. get last invoice, and invoice items, filter invoice items that are in licensed recurring mode
176
182
  const invoiceItems = await InvoiceItem.findAll({ where: { invoice_id: lastInvoice.id, proration: false } });
177
183
  const invoiceItemsExpanded = await Price.expand(invoiceItems.map((x) => x.toJSON()));
178
- const prorationItems = invoiceItemsExpanded.filter(
179
- (x) => x.price.type === 'recurring' && x.price.recurring?.usage_type === 'licensed'
180
- );
184
+ const prorationItems = invoiceItemsExpanded.filter((x) => {
185
+ const price = getSubscriptionItemPrice(x);
186
+ return price.type === 'recurring' && price.recurring?.usage_type === 'licensed';
187
+ });
181
188
 
182
189
  // 2. calculate proration args based on the filtered invoice items
183
190
  const precision = 10000;
@@ -191,7 +198,8 @@ export async function createProration(
191
198
  let unused = new BN(0);
192
199
  const prorations = await Promise.all(
193
200
  prorationItems.map((x: TLineItemExpanded & { [key: string]: any }) => {
194
- const unitAmount = getPriceUintAmountByCurrency(x.price, subscription.currency_id);
201
+ const price = getSubscriptionItemPrice(x);
202
+ const unitAmount = getPriceUintAmountByCurrency(price, subscription.currency_id);
195
203
  const amount = new BN(unitAmount)
196
204
  .mul(new BN(x.quantity))
197
205
  .mul(new BN(prorationRate))
@@ -206,11 +214,11 @@ export async function createProration(
206
214
  unused = unused.add(new BN(amount));
207
215
 
208
216
  return {
209
- price_id: x.price_id,
217
+ price_id: price.id,
210
218
  amount: `-${amount}`,
211
219
  quantity: x.quantity,
212
220
  // @ts-ignore
213
- description: `Unused time on ${x.price.product.name} after ${dayjs().format('lll')}`,
221
+ description: `Unused time on ${price.product.name} after ${dayjs().format('lll')}`,
214
222
  period: {
215
223
  start: lastInvoice.period_start,
216
224
  end: lastInvoice.period_end,
@@ -78,6 +78,20 @@ export const handleInvoice = async (job: InvoiceJob) => {
78
78
  }
79
79
  }
80
80
 
81
+ if (invoice.payment_intent_id) {
82
+ const paymentIntent = await PaymentIntent.findByPk(invoice.payment_intent_id);
83
+ if (
84
+ paymentIntent &&
85
+ ['requires_action', 'requires_capture', 'requires_payment_method'].includes(paymentIntent.status)
86
+ ) {
87
+ await paymentIntent.update({
88
+ amount_received: '0',
89
+ status: 'succeeded',
90
+ });
91
+ logger.info('invoice payment intent updated', paymentIntent.id);
92
+ }
93
+ }
94
+
81
95
  return;
82
96
  }
83
97
 
@@ -195,6 +195,7 @@ export const refundQueue = createQueue<RefundJob>({
195
195
 
196
196
  export const startRefundQueue = async () => {
197
197
  events.on('refund.created', (refund: Refund) => {
198
+ logger.info('schedule refund', { id: refund.id });
198
199
  refundQueue.push({ id: refund.id, job: { refundId: refund.id } });
199
200
  });
200
201
 
@@ -82,7 +82,7 @@ export const formatCheckoutSession = async (payload: any, throwOnEmptyItems = tr
82
82
  terms_of_service: 'none',
83
83
  },
84
84
  invoice_creation: {
85
- enabled: false,
85
+ enabled: true,
86
86
  },
87
87
  phone_number_collection: {
88
88
  enabled: false,
@@ -25,6 +25,7 @@ import { Product } from '../../store/models/product';
25
25
  import { SetupIntent } from '../../store/models/setup-intent';
26
26
  import { Subscription } from '../../store/models/subscription';
27
27
  import { SubscriptionItem } from '../../store/models/subscription-item';
28
+ import { getSubscriptionItemPrice } from '../../libs/subscription';
28
29
 
29
30
  type Result = {
30
31
  checkoutSession: CheckoutSession;
@@ -446,9 +447,10 @@ export async function ensureInvoiceAndItems({
446
447
  : [];
447
448
 
448
449
  const getLineSetup = (x: TLineItemExpanded) => {
449
- const price = x.upsell_price || x.price;
450
+ const price = getSubscriptionItemPrice(x);
450
451
  if (price.type === 'recurring' && trailing) {
451
452
  return {
453
+ price,
452
454
  amount: '0',
453
455
  // @ts-ignore
454
456
  description: trailing ? `${price.product.name} (trailing)` : price.product.name,
@@ -460,6 +462,7 @@ export async function ensureInvoiceAndItems({
460
462
  }
461
463
 
462
464
  return {
465
+ price,
463
466
  amount: new BN(getPriceUintAmountByCurrency(price, props.currency_id)).mul(new BN(x.quantity)).toString(),
464
467
  // @ts-ignore
465
468
  description: price.product.name,
@@ -470,7 +473,7 @@ export async function ensureInvoiceAndItems({
470
473
  const items = await Promise.all(
471
474
  lineItems.map((x: TLineItemExpanded) => {
472
475
  const setup = getLineSetup(x);
473
- const price = x.upsell_price || x.price;
476
+ const { price } = setup;
474
477
  let { quantity } = x;
475
478
  if (price.type === 'recurring') {
476
479
  if (price.recurring?.usage_type === 'metered' && !metered) {
@@ -489,10 +492,10 @@ export async function ensureInvoiceAndItems({
489
492
  period: setup.period,
490
493
  currency_id: props.currency_id,
491
494
  customer_id: customer.id,
492
- price_id: x.price_id,
495
+ price_id: price.id,
493
496
  invoice_id: invoice.id,
494
497
  subscription_id: subscription?.id,
495
- subscription_item_id: subscriptionItems.find((si) => si.price_id === x.price_id)?.id,
498
+ subscription_item_id: subscriptionItems.find((si) => si.price_id === price.id)?.id,
496
499
  discountable: false,
497
500
  discounts: [],
498
501
  discount_amounts: [],
@@ -22,11 +22,15 @@ const paginationSchema = Joi.object<{
22
22
  pageSize: number;
23
23
  livemode?: boolean;
24
24
  status?: string;
25
+ invoice_id: string;
26
+ subscription_id: string;
25
27
  }>({
26
28
  page: Joi.number().integer().min(1).default(1),
27
29
  pageSize: Joi.number().integer().min(1).max(100).default(20),
28
30
  livemode: Joi.boolean().empty(''),
29
31
  status: Joi.string().empty(''),
32
+ invoice_id: Joi.string().empty(''),
33
+ subscription_id: Joi.string().empty(''),
30
34
  });
31
35
  router.get('/', auth, async (req, res) => {
32
36
  const { page, pageSize, livemode, status, ...query } = await paginationSchema.validateAsync(req.query, {
@@ -38,6 +42,14 @@ router.get('/', auth, async (req, res) => {
38
42
  if (typeof livemode === 'boolean') {
39
43
  where.livemode = livemode;
40
44
  }
45
+
46
+ if (query.invoice_id) {
47
+ where.invoice_id = query.invoice_id;
48
+ }
49
+
50
+ if (query.subscription_id) {
51
+ where.subscription_id = query.subscription_id;
52
+ }
41
53
  if (status) {
42
54
  where.status = status
43
55
  .split(',')
@@ -60,6 +72,7 @@ router.get('/', auth, async (req, res) => {
60
72
  include: [
61
73
  { model: Customer, as: 'customer' },
62
74
  { model: PaymentCurrency, as: 'paymentCurrency' },
75
+ { model: PaymentMethod, as: 'paymentMethod' },
63
76
  // { model: PaymentIntent, as: 'paymentIntent' },
64
77
  // { model: Invoice, as: 'invoice' },
65
78
  // { model: Subscription, as: 'subscription' },
@@ -76,6 +89,7 @@ router.get('/:id', auth, async (req, res) => {
76
89
  { model: Customer, as: 'customer' },
77
90
  { model: PaymentCurrency, as: 'paymentCurrency' },
78
91
  { model: PaymentIntent, as: 'paymentIntent' },
92
+ { model: PaymentMethod, as: 'paymentMethod' },
79
93
  { model: Invoice, as: 'invoice' },
80
94
  { model: Subscription, as: 'subscription' },
81
95
  ],
@@ -312,6 +312,7 @@ router.put('/:id/cancel', authPortal, async (req, res) => {
312
312
  reason: 'requested_by_admin',
313
313
  currency_id: subscription.currency_id,
314
314
  customer_id: subscription.customer_id,
315
+ payment_method_id: subscription.default_payment_method_id,
315
316
  payment_intent_id: result.lastInvoice.payment_intent_id as string,
316
317
  invoice_id: result.lastInvoice.id,
317
318
  subscription_id: subscription.id,
@@ -329,7 +330,7 @@ router.put('/:id/cancel', authPortal, async (req, res) => {
329
330
  unused_period_end: subscription.current_period_end,
330
331
  },
331
332
  });
332
- logger.info('subscription cancel refund done', {
333
+ logger.info('subscription cancel refund created', {
333
334
  ...req.params,
334
335
  ...req.body,
335
336
  ...pick(result, ['total', 'unused']),
@@ -0,0 +1,22 @@
1
+ /* eslint-disable no-await-in-loop */
2
+ import { DataTypes } from 'sequelize';
3
+
4
+ import { Migration, safeApplyColumnChanges } from '../migrate';
5
+
6
+ export const up: Migration = async ({ context }) => {
7
+ await safeApplyColumnChanges(context, {
8
+ refunds: [
9
+ {
10
+ name: 'payment_method_id',
11
+ field: {
12
+ type: DataTypes.STRING(30),
13
+ allowNull: true,
14
+ },
15
+ },
16
+ ],
17
+ });
18
+ };
19
+
20
+ export const down: Migration = async ({ context }) => {
21
+ await context.removeColumn('refunds', 'payment_method_id');
22
+ };
@@ -230,7 +230,7 @@ export type TPricingTableExpanded = TPricingTable & {
230
230
  export type TRefundExpanded = TRefund & {
231
231
  customer: TCustomer;
232
232
  paymentCurrency: TPaymentCurrency;
233
- paymentMethod?: TPaymentMethod;
233
+ paymentMethod: TPaymentMethod;
234
234
  paymentIntent: TPaymentIntent;
235
235
  invoice?: TInvoice;
236
236
  subscription?: TSubscription;
@@ -25,6 +25,7 @@ export class Refund extends Model<InferAttributes<Refund>, InferCreationAttribut
25
25
  declare currency_id: string;
26
26
  declare customer_id: string;
27
27
  declare payment_intent_id: string;
28
+ declare payment_method_id: string;
28
29
  declare invoice_id?: string;
29
30
  declare subscription_id?: string;
30
31
 
@@ -173,21 +174,30 @@ export class Refund extends Model<InferAttributes<Refund>, InferCreationAttribut
173
174
  };
174
175
 
175
176
  public static initialize(sequelize: any) {
176
- this.init(Refund.GENESIS_ATTRIBUTES, {
177
- sequelize,
178
- modelName: 'Refund',
179
- tableName: 'refunds',
180
- createdAt: 'created_at',
181
- updatedAt: 'updated_at',
182
- hooks: {
183
- afterCreate: (model: Refund, options) =>
184
- createEvent('Refund', 'refund.created', model, options).catch(console.error),
185
- afterUpdate: (model: Refund, options) =>
186
- createStatusEvent('Refund', 'refund', { canceled: 'canceled', succeeded: 'succeeded' }, model, options),
187
- afterDestroy: (model: Refund, options) =>
188
- createEvent('Refund', 'refund.deleted', model, options).catch(console.error),
177
+ this.init(
178
+ {
179
+ ...Refund.GENESIS_ATTRIBUTES,
180
+ payment_method_id: {
181
+ type: DataTypes.STRING(30),
182
+ allowNull: true,
183
+ },
189
184
  },
190
- });
185
+ {
186
+ sequelize,
187
+ modelName: 'Refund',
188
+ tableName: 'refunds',
189
+ createdAt: 'created_at',
190
+ updatedAt: 'updated_at',
191
+ hooks: {
192
+ afterCreate: (model: Refund, options) =>
193
+ createEvent('Refund', 'refund.created', model, options).catch(console.error),
194
+ afterUpdate: (model: Refund, options) =>
195
+ createStatusEvent('Refund', 'refund', { canceled: 'canceled', succeeded: 'succeeded' }, model, options),
196
+ afterDestroy: (model: Refund, options) =>
197
+ createEvent('Refund', 'refund.deleted', model, options).catch(console.error),
198
+ },
199
+ }
200
+ );
191
201
  }
192
202
 
193
203
  public static associate(models: any) {
@@ -206,6 +216,11 @@ export class Refund extends Model<InferAttributes<Refund>, InferCreationAttribut
206
216
  foreignKey: 'id',
207
217
  as: 'paymentIntent',
208
218
  });
219
+ this.hasOne(models.PaymentMethod, {
220
+ sourceKey: 'payment_method_id',
221
+ foreignKey: 'id',
222
+ as: 'paymentMethod',
223
+ });
209
224
  this.hasOne(models.Invoice, {
210
225
  sourceKey: 'invoice_id',
211
226
  foreignKey: 'id',
package/blocklet.yml CHANGED
@@ -14,7 +14,7 @@ repository:
14
14
  type: git
15
15
  url: git+https://github.com/blocklet/payment-kit.git
16
16
  specVersion: 1.2.8
17
- version: 1.13.149
17
+ version: 1.13.151
18
18
  logo: logo.png
19
19
  files:
20
20
  - dist
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "payment-kit",
3
- "version": "1.13.149",
3
+ "version": "1.13.151",
4
4
  "scripts": {
5
5
  "dev": "cross-env COMPONENT_STORE_URL=https://test.store.blocklet.dev blocklet dev --open",
6
6
  "eject": "vite eject",
@@ -50,7 +50,7 @@
50
50
  "@arcblock/jwt": "^1.18.110",
51
51
  "@arcblock/ux": "^2.9.29",
52
52
  "@blocklet/logger": "1.16.23",
53
- "@blocklet/payment-react": "1.13.149",
53
+ "@blocklet/payment-react": "1.13.151",
54
54
  "@blocklet/sdk": "1.16.23",
55
55
  "@blocklet/ui-react": "^2.9.29",
56
56
  "@blocklet/uploader": "^0.0.73",
@@ -110,7 +110,7 @@
110
110
  "devDependencies": {
111
111
  "@abtnode/types": "1.16.23",
112
112
  "@arcblock/eslint-config-ts": "^0.2.4",
113
- "@blocklet/payment-types": "1.13.149",
113
+ "@blocklet/payment-types": "1.13.151",
114
114
  "@types/cookie-parser": "^1.4.6",
115
115
  "@types/cors": "^2.8.17",
116
116
  "@types/dotenv-flow": "^3.3.3",
@@ -149,5 +149,5 @@
149
149
  "parser": "typescript"
150
150
  }
151
151
  },
152
- "gitHead": "0edf179eb97dccee7d6ccbee7ffc00d845e4cad4"
152
+ "gitHead": "1697697b65f9372e868fb10d5766edb3144f44f8"
153
153
  }
@@ -62,7 +62,7 @@ export default function InvoiceTable({ invoice, simple }: Props) {
62
62
  </TableCell>
63
63
  )}
64
64
  </TableRow>
65
- {invoice.period_end && invoice.period_start && (
65
+ {invoice.period_end > 0 && invoice.period_start > 0 && (
66
66
  <TableRow sx={{ borderBottom: '1px solid #eee' }}>
67
67
  <TableCell align="left" colSpan={simple ? 4 : 5}>
68
68
  <Typography component="span" variant="body1" color="text.secondary">
@@ -108,7 +108,7 @@ export default function PaymentList({ customer_id, invoice_id, features }: ListP
108
108
  const item = data.list[index] as TPaymentIntentExpanded;
109
109
  return (
110
110
  <Typography component="strong" fontWeight={600}>
111
- {fromUnitToToken(item?.amount, item?.paymentCurrency.decimal)}&nbsp;
111
+ {fromUnitToToken(item?.amount_received, item?.paymentCurrency.decimal)}&nbsp;
112
112
  {item?.paymentCurrency.symbol}
113
113
  </Typography>
114
114
  );
@@ -70,25 +70,6 @@ export default function AfterPay() {
70
70
  )}
71
71
  />
72
72
  )}
73
- <Typography variant="h6" sx={{ fontWeight: 600 }}>
74
- {t('admin.invoices')}
75
- </Typography>
76
- <Controller
77
- name="invoice_creation.enabled"
78
- control={control}
79
- render={({ field }) => (
80
- <FormControlLabel
81
- control={
82
- <Checkbox
83
- checked={getValues().invoice_creation.enabled}
84
- {...field}
85
- onChange={(_, checked) => setValue(field.name, checked)}
86
- />
87
- }
88
- label={t('admin.paymentLink.createInvoice')}
89
- />
90
- )}
91
- />
92
73
  <Controller
93
74
  name="nft_mint_settings.enabled"
94
75
  control={control}
@@ -27,6 +27,7 @@ type SearchProps = {
27
27
  page: number;
28
28
  customer_id?: string;
29
29
  invoice_id?: string;
30
+ subscription_id?: string;
30
31
  };
31
32
 
32
33
  type ListProps = {
@@ -37,6 +38,7 @@ type ListProps = {
37
38
  };
38
39
  customer_id?: string;
39
40
  invoice_id?: string;
41
+ subscription_id?: string;
40
42
  };
41
43
 
42
44
  const getListKey = (props: ListProps) => {
@@ -50,16 +52,17 @@ const getListKey = (props: ListProps) => {
50
52
  return 'refunds';
51
53
  };
52
54
 
53
- PaymentList.defaultProps = {
55
+ RefundList.defaultProps = {
54
56
  features: {
55
57
  customer: true,
56
58
  filter: true,
57
59
  },
58
60
  customer_id: '',
59
61
  invoice_id: '',
62
+ subscription_id: '',
60
63
  };
61
64
 
62
- export default function PaymentList({ customer_id, invoice_id, features }: ListProps) {
65
+ export default function RefundList({ customer_id, invoice_id, subscription_id, features }: ListProps) {
63
66
  const { t } = useLocaleContext();
64
67
  const navigate = useNavigate();
65
68
 
@@ -70,6 +73,7 @@ export default function PaymentList({ customer_id, invoice_id, features }: ListP
70
73
  status: '',
71
74
  customer_id,
72
75
  invoice_id,
76
+ subscription_id,
73
77
  pageSize: persisted.rowsPerPage || 20,
74
78
  page: persisted.page ? persisted.page + 1 : 1,
75
79
  });
@@ -5,6 +5,7 @@ type Props = {
5
5
  title: string;
6
6
  children?: ReactNode;
7
7
  mb?: number;
8
+ mt?: number;
8
9
  };
9
10
 
10
11
  export default function SectionHeader(props: Props) {
@@ -16,7 +17,7 @@ export default function SectionHeader(props: Props) {
16
17
  alignItems="center"
17
18
  flexWrap="wrap"
18
19
  gap={1}
19
- sx={{ mb: props.mb, pb: 1, width: 1, borderBottom: '1px solid #eee' }}>
20
+ sx={{ mb: props.mb, mt: props.mt, pb: 1, width: 1, borderBottom: '1px solid #eee' }}>
20
21
  <Typography variant="h6" sx={{ fontWeight: 600 }}>
21
22
  {props.title}
22
23
  </Typography>
@@ -28,4 +29,5 @@ export default function SectionHeader(props: Props) {
28
29
  SectionHeader.defaultProps = {
29
30
  children: null,
30
31
  mb: 1,
32
+ mt: 1,
31
33
  };
@@ -48,7 +48,7 @@ export function UsageRecordDialog(props: { subscriptionId: string; id: string; o
48
48
  options: {
49
49
  customBodyRenderLite: (_: string, index: number) => {
50
50
  const item = data.list[index] as TUsageRecord;
51
- return formatToDatetime(item.timestamp);
51
+ return formatToDatetime(item.timestamp * 1000);
52
52
  },
53
53
  },
54
54
  },
@@ -2,78 +2,7 @@ import flat from 'flat';
2
2
 
3
3
  export default flat({
4
4
  common: {
5
- id: 'ID',
6
- url: 'URL',
7
- createdAt: 'Created At',
8
- updatedAt: 'Updated At',
9
- resumesAt: 'Resume At',
10
- actions: 'Actions',
11
- options: 'Options',
12
- advanced: 'Advanced options',
13
- login: 'Login to access this page',
14
- settings: 'Settings',
15
- preview: 'Preview',
16
- required: 'Required',
17
- setup: 'Setup',
18
- name: 'Name',
19
- amount: 'Amount',
20
- total: 'Total',
21
- subtotal: 'Subtotal',
22
- status: 'Status',
23
- livemode: 'Test mode',
24
- afterTime: 'After {time}',
25
- timeAgo: '{time} ago',
26
- save: 'Save',
27
- saved: 'Changes saved',
28
- remove: 'Remove',
29
- removed: 'Resource removed',
30
- confirm: 'Confirm',
31
- cancel: 'Cancel',
32
- every: 'every',
33
- per: 'per {interval}',
34
- slash: '/ {interval}',
35
- unit: 'units',
36
- edit: 'Edit',
37
- quantity: 'Quantity',
38
- yes: 'Yes',
39
- no: 'No',
40
- email: 'Email',
41
- did: 'DID',
42
- txHash: 'Transaction',
43
- customer: 'Customer',
44
- custom: 'Custom',
45
- description: 'Description',
46
- statementDescriptor: 'Statement descriptor',
47
- loadMore: 'View more {resource}',
48
- loadingMore: 'Loading more {resource}...',
49
- noMore: 'No more {resource}',
50
- copied: 'Copied',
51
- previous: 'Back',
52
- continue: 'Continue',
53
- qty: 'Qty {count}',
54
- each: '{unit} each',
55
- trial: 'Free for {count} days',
56
- billed: 'billed {rule}',
57
- metered: 'based on usage',
58
- hour: 'hour',
59
- day: 'day',
60
- week: 'week',
61
- month: 'month',
62
- year: 'year',
63
- hourly: 'hourly',
64
- daily: 'daily',
65
- weekly: 'weekly',
66
- monthly: 'monthly',
67
- yearly: 'yearly',
68
- month3: 'every 3 months',
69
- month6: 'every 6 months',
70
- recurring: 'every {count} {interval}',
71
5
  redirecting: 'Redirecting...',
72
- hours: 'hours',
73
- days: 'days',
74
- weeks: 'weeks',
75
- months: 'months',
76
- years: 'years',
77
6
  metadata: {
78
7
  label: 'Metadata',
79
8
  add: 'Add more metadata',
@@ -306,6 +235,7 @@ export default flat({
306
235
  view: 'View payment detail',
307
236
  empty: 'No payment intent',
308
237
  refund: 'Refund payment',
238
+ received: 'Received',
309
239
  },
310
240
  paymentMethod: {
311
241
  _name: 'Payment Method',
@@ -2,78 +2,7 @@ import flat from 'flat';
2
2
 
3
3
  export default flat({
4
4
  common: {
5
- id: 'ID',
6
- url: 'URL',
7
- createdAt: '创建时间',
8
- updatedAt: '更新时间',
9
- resumesAt: '恢复时间',
10
- actions: '操作',
11
- options: '选项',
12
- advanced: '高级选项',
13
- settings: '设置',
14
- preview: '预览',
15
- required: '必填',
16
- setup: '设置',
17
- name: '姓名',
18
- login: '登录以访问此页面',
19
- amount: '金额',
20
- total: '总计',
21
- subtotal: '小计',
22
- status: '状态',
23
- livemode: '测试模式',
24
- afterTime: '在{time}后',
25
- timeAgo: '{time}前',
26
- save: '保存',
27
- saved: '更改已保存',
28
- remove: '删除',
29
- removed: '资源已删除',
30
- confirm: '确认',
31
- cancel: '取消',
32
- every: '每',
33
- per: '每{interval}',
34
- slash: '每{interval}',
35
- unit: '件',
36
- edit: '编辑',
37
- quantity: '数量',
38
- yes: '是',
39
- no: '否',
40
- email: '邮箱',
41
- did: 'DID',
42
- txHash: '交易哈希',
43
- customer: '客户',
44
- custom: '自定义',
45
- description: '描述',
46
- statementDescriptor: '声明描述',
47
- loadMore: '查看更多{resource}',
48
- loadingMore: '正在加载更多{resource}...',
49
- noMore: '没有更多{resource}',
50
- copied: '已复制',
51
- previous: '返回',
52
- continue: '继续',
53
- qty: '{count} 件',
54
- each: '每件 {unit}',
55
- trial: '免费试用 {count} 天',
56
- billed: '{rule}收费',
57
- metered: '按用量',
58
- hour: '小时',
59
- day: '天',
60
- week: '周',
61
- month: '月',
62
- year: '年',
63
- hourly: '按小时',
64
- daily: '按天',
65
- weekly: '按周',
66
- monthly: '按月',
67
- yearly: '按年',
68
- month3: '按季度',
69
- month6: '按半年',
70
- recurring: '每{count}{interval}',
71
5
  redirecting: '跳转中...',
72
- hours: '小时',
73
- days: '天',
74
- weeks: '周',
75
- months: '月',
76
- years: '年',
77
6
  metadata: {
78
7
  label: '元数据',
79
8
  add: '添加更多元数据',
@@ -298,6 +227,7 @@ export default flat({
298
227
  view: '查看支付详情',
299
228
  empty: '没有支付意向',
300
229
  refund: '退款支付',
230
+ received: '实收金额',
301
231
  },
302
232
  paymentMethod: {
303
233
  _name: '支付方式',
@@ -20,6 +20,7 @@ import InvoiceActions from '../../../../components/invoice/action';
20
20
  import InvoiceTable from '../../../../components/invoice/table';
21
21
  import MetadataEditor from '../../../../components/metadata/editor';
22
22
  import PaymentList from '../../../../components/payment-intent/list';
23
+ import RefundList from '../../../../components/refund/list';
23
24
  import SectionHeader from '../../../../components/section/header';
24
25
 
25
26
  const fetchData = (id: string): Promise<TInvoiceExpanded> => {
@@ -145,6 +146,12 @@ export default function InvoiceDetail(props: { id: string }) {
145
146
  <PaymentList features={{ customer: false, toolbar: false }} invoice_id={data.id} />
146
147
  </Box>
147
148
  </Box>
149
+ <Box className="section">
150
+ <SectionHeader title={t('admin.refunds')} mb={0} />
151
+ <Box className="section-body">
152
+ <RefundList features={{ customer: false, toolbar: false }} invoice_id={data.id} />
153
+ </Box>
154
+ </Box>
148
155
  <Box className="section">
149
156
  <SectionHeader title={t('admin.connections')} />
150
157
  <Stack>
@@ -16,6 +16,7 @@ import EventList from '../../../../components/event/list';
16
16
  import InfoRow from '../../../../components/info-row';
17
17
  import InvoiceList from '../../../../components/invoice/list';
18
18
  import MetadataEditor from '../../../../components/metadata/editor';
19
+ import RefundList from '../../../../components/refund/list';
19
20
  import SectionHeader from '../../../../components/section/header';
20
21
  import SubscriptionActions from '../../../../components/subscription/actions';
21
22
  import SubscriptionItemList from '../../../../components/subscription/items';
@@ -205,6 +206,12 @@ export default function SubscriptionDetail(props: { id: string }) {
205
206
  <InvoiceList features={{ customer: true, toolbar: false }} subscription_id={data.id} />
206
207
  </Box>
207
208
  </Box>
209
+ <Box className="section">
210
+ <SectionHeader title={t('admin.refunds')} mb={0} />
211
+ <Box className="section-body">
212
+ <RefundList features={{ customer: true, toolbar: false }} subscription_id={data.id} />
213
+ </Box>
214
+ </Box>
208
215
  <Box className="section">
209
216
  <SectionHeader title={t('admin.events')} />
210
217
  <Box className="section-body">
@@ -1,7 +1,7 @@
1
1
  import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
2
2
  import { PaymentProvider, Switch, usePaymentContext } from '@blocklet/payment-react';
3
3
  import { Box, Chip, Stack } from '@mui/material';
4
- import React, { isValidElement, startTransition } from 'react';
4
+ import React, { Suspense, isValidElement, startTransition } from 'react';
5
5
  import { useNavigate, useParams } from 'react-router-dom';
6
6
 
7
7
  import Layout from '../../components/layout/admin';
@@ -99,7 +99,9 @@ function Admin() {
99
99
  </label>
100
100
  </Stack>
101
101
  </Stack>
102
- <div className="page-content">{isValidElement(TabComponent) ? TabComponent : <TabComponent />}</div>
102
+ <Suspense fallback={<div />}>
103
+ <div className="page-content">{isValidElement(TabComponent) ? TabComponent : <TabComponent />}</div>
104
+ </Suspense>
103
105
  </Layout>
104
106
  );
105
107
  }
@@ -77,7 +77,8 @@ export default function PaymentIntentDetail(props: { id: string }) {
77
77
  const onUpdateMetadata = createUpdater('metadata');
78
78
 
79
79
  const currency = data.paymentCurrency;
80
- const amount = [fromUnitToToken(data?.amount, currency.decimal), currency.symbol].join(' ');
80
+ const received = [fromUnitToToken(data?.amount_received, currency.decimal), currency.symbol].join(' ');
81
+ const total = [fromUnitToToken(data?.amount, currency.decimal), currency.symbol].join(' ');
81
82
 
82
83
  return (
83
84
  <Root direction="column" spacing={4} mb={4}>
@@ -96,7 +97,7 @@ export default function PaymentIntentDetail(props: { id: string }) {
96
97
  <Box mt={2}>
97
98
  <Stack direction="row" justifyContent="space-between" alignItems="center">
98
99
  <Stack direction="row" alignItems="center">
99
- <Amount amount={amount} sx={{ my: 0, fontSize: '2rem', lineHeight: '1rem' }} />
100
+ <Amount amount={received} sx={{ my: 0, fontSize: '2rem', lineHeight: '1rem' }} />
100
101
  <Status label={data.status} color={getPaymentIntentStatusColor(data.status)} sx={{ ml: 2 }} />
101
102
  </Stack>
102
103
  <PaymentIntentActions data={data} variant="normal" />
@@ -117,7 +118,8 @@ export default function PaymentIntentDetail(props: { id: string }) {
117
118
  <Box className="section">
118
119
  <SectionHeader title={t('admin.details')} />
119
120
  <Stack>
120
- <InfoRow label={t('common.amount')} value={amount} />
121
+ <InfoRow label={t('common.amount')} value={total} />
122
+ <InfoRow label={t('admin.paymentIntent.received')} value={received} />
121
123
  <InfoRow
122
124
  label={t('common.status')}
123
125
  value={
@@ -191,16 +193,18 @@ export default function PaymentIntentDetail(props: { id: string }) {
191
193
  <Box className="section">
192
194
  <SectionHeader title={t('admin.connections')} />
193
195
  <Stack>
194
- <InfoRow
195
- label={t('admin.subscription.name')}
196
- value={
197
- data.subscription ? <Link to={`/admin/billing/${data.subscription.id}`}>{data.subscription.id}</Link> : ''
198
- }
199
- />
200
- <InfoRow
201
- label={t('admin.invoice.name')}
202
- value={data.invoice_id ? <Link to={`/admin/billing/${data.invoice_id}`}>{data.invoice_id}</Link> : ''}
203
- />
196
+ {data.subscription && (
197
+ <InfoRow
198
+ label={t('admin.subscription.name')}
199
+ value={<Link to={`/admin/billing/${data.subscription.id}`}>{data.subscription.id}</Link>}
200
+ />
201
+ )}
202
+ {data.invoice_id && (
203
+ <InfoRow
204
+ label={t('admin.invoice.name')}
205
+ value={<Link to={`/admin/billing/${data.invoice_id}`}>{data.invoice_id}</Link>}
206
+ />
207
+ )}
204
208
  </Stack>
205
209
  </Box>
206
210
  <Box className="section">
@@ -52,7 +52,7 @@ export default function CreatePaymentLink() {
52
52
  terms_of_service: 'none',
53
53
  },
54
54
  invoice_creation: {
55
- enabled: false,
55
+ enabled: true,
56
56
  },
57
57
  phone_number_collection: {
58
58
  enabled: false,
@@ -1,10 +1,18 @@
1
1
  /* eslint-disable jsx-a11y/anchor-is-valid */
2
2
  import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
3
3
  import Toast from '@arcblock/ux/lib/Toast';
4
- import { Status, TxLink, api, formatError, formatTime, getInvoiceStatusColor } from '@blocklet/payment-react';
4
+ import {
5
+ PaymentProvider,
6
+ Status,
7
+ TxLink,
8
+ api,
9
+ formatError,
10
+ formatTime,
11
+ getInvoiceStatusColor,
12
+ } from '@blocklet/payment-react';
5
13
  import type { TInvoiceExpanded } from '@blocklet/payment-types';
6
14
  import { ArrowBackOutlined } from '@mui/icons-material';
7
- import { Alert, Box, Button, CircularProgress, Grid, Stack, Typography } from '@mui/material';
15
+ import { Alert, Box, Button, CircularProgress, Stack, Typography } from '@mui/material';
8
16
  import { useRequest, useSetState } from 'ahooks';
9
17
  import { useEffect } from 'react';
10
18
  import { Link, useParams, useSearchParams } from 'react-router-dom';
@@ -14,6 +22,7 @@ import InfoRow from '../../components/info-row';
14
22
  import InvoiceTable from '../../components/invoice/table';
15
23
  import SectionHeader from '../../components/section/header';
16
24
  import { useSessionContext } from '../../contexts/session';
25
+ import CustomerRefundList from './refund/list';
17
26
 
18
27
  const fetchData = (id: string): Promise<TInvoiceExpanded> => {
19
28
  return api.get(`/api/invoices/${id}`).then((res) => res.data);
@@ -23,7 +32,7 @@ const fetchData = (id: string): Promise<TInvoiceExpanded> => {
23
32
  export default function CustomerHome() {
24
33
  const { t } = useLocaleContext();
25
34
  const [searchParams] = useSearchParams();
26
- const { connectApi } = useSessionContext();
35
+ const { session, connectApi } = useSessionContext();
27
36
  const params = useParams<{ id: string }>();
28
37
  const [state, setState] = useSetState({
29
38
  downloading: false,
@@ -89,21 +98,19 @@ export default function CustomerHome() {
89
98
  }
90
99
 
91
100
  return (
92
- <Grid container spacing={3} sx={{ mt: 1 }}>
93
- <Grid item xs={12} md={12}>
101
+ <Box sx={{ maxWidth: '1200px' }}>
102
+ <PaymentProvider session={session} connect={connectApi}>
94
103
  <Link to="/customer">
95
- <Stack direction="row" alignItems="center" sx={{ fontWeight: 'normal' }}>
104
+ <Stack direction="row" alignItems="center" sx={{ fontWeight: 'normal', padding: '10px 0' }}>
96
105
  <ArrowBackOutlined fontSize="small" sx={{ mr: 0.5, color: 'text.secondary' }} />
97
106
  <Typography variant="h6" sx={{ color: 'text.secondary', fontWeight: 'normal' }}>
98
107
  {t('common.previous')}
99
108
  </Typography>
100
109
  </Stack>
101
110
  </Link>
102
- </Grid>
103
- <Grid item xs={12} md={5}>
104
111
  <Box>
105
- <SectionHeader title={t('payment.customer.invoice.summary')} mb={0} />
106
- <Stack sx={{ mt: 1 }}>
112
+ <SectionHeader title={t('payment.customer.invoice.summary')} mb={0} mt={1} />
113
+ <Stack sx={{ mt: 1, display: 'grid', gridTemplateColumns: '50% 50%' }}>
107
114
  <InfoRow label={t('admin.invoice.number')} value={data.number} />
108
115
  <InfoRow label={t('admin.invoice.from')} value={data.statement_descriptor || blocklet.appName} />
109
116
  <InfoRow label={t('admin.invoice.customer')} value={data.customer.name} />
@@ -111,14 +118,12 @@ export default function CustomerHome() {
111
118
  label={t('common.status')}
112
119
  value={<Status label={data.status} color={getInvoiceStatusColor(data.status)} />}
113
120
  />
114
- <InfoRow
115
- label={t('admin.subscription.currentPeriod')}
116
- value={
117
- data.period_start && data.period_end
118
- ? [formatTime(data.period_start * 1000), formatTime(data.period_end * 1000)].join(' ~ ')
119
- : ''
120
- }
121
- />
121
+ {data.period_start > 0 && data.period_end > 0 && (
122
+ <InfoRow
123
+ label={t('admin.subscription.currentPeriod')}
124
+ value={[formatTime(data.period_start * 1000), formatTime(data.period_end * 1000)].join(' ~ ')}
125
+ />
126
+ )}
122
127
  {data.status_transitions?.paid_at && (
123
128
  <InfoRow label={t('admin.invoice.paidAt')} value={formatTime(data.status_transitions.paid_at * 1000)} />
124
129
  )}
@@ -138,9 +143,7 @@ export default function CustomerHome() {
138
143
  />
139
144
  </Stack>
140
145
  </Box>
141
- </Grid>
142
- <Grid item xs={12} md={7}>
143
- <SectionHeader title={t('payment.customer.invoice.details')} mb={0}>
146
+ <SectionHeader title={t('payment.customer.invoice.details')} mb={0} mt={1}>
144
147
  {['open', 'paid', 'uncollectible'].includes(data.status) && (
145
148
  <Button
146
149
  variant="contained"
@@ -160,7 +163,13 @@ export default function CustomerHome() {
160
163
  </Button>
161
164
  )}
162
165
  </Stack>
163
- </Grid>
164
- </Grid>
166
+ <Box className="section">
167
+ <SectionHeader title={t('admin.refunds')} mb={0} mt={0} />
168
+ <Box className="section-body">
169
+ <CustomerRefundList invoice_id={data.id} />
170
+ </Box>
171
+ </Box>
172
+ </PaymentProvider>
173
+ </Box>
165
174
  );
166
175
  }
@@ -0,0 +1,125 @@
1
+ /* eslint-disable react/no-unstable-nested-components */
2
+ import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
3
+ import { Status, TxLink, api, formatToDate, getPaymentIntentStatusColor } from '@blocklet/payment-react';
4
+ import type { Paginated, PaymentDetails, TPaymentIntentExpanded } from '@blocklet/payment-types';
5
+ import { Box, Button, CircularProgress, Stack, Typography } from '@mui/material';
6
+ import { fromUnitToToken } from '@ocap/util';
7
+ import { useInfiniteScroll } from 'ahooks';
8
+
9
+ const groupByDate = (items: TPaymentIntentExpanded[]) => {
10
+ const grouped: { [key: string]: TPaymentIntentExpanded[] } = {};
11
+ items.forEach((item) => {
12
+ const date = new Date(item.created_at).toLocaleDateString();
13
+ if (!grouped[date]) {
14
+ grouped[date] = [];
15
+ }
16
+ grouped[date]?.push(item);
17
+ });
18
+ return grouped;
19
+ };
20
+
21
+ const fetchData = (params: Record<string, any> = {}): Promise<Paginated<TPaymentIntentExpanded>> => {
22
+ const search = new URLSearchParams();
23
+ Object.keys(params).forEach((key) => {
24
+ search.set(key, String(params[key]));
25
+ });
26
+ return api.get(`/api/refunds?${search.toString()}`).then((res) => res.data);
27
+ };
28
+
29
+ type Props = {
30
+ invoice_id: string;
31
+ };
32
+
33
+ const pageSize = 10;
34
+
35
+ export default function CustomerRefundList({ invoice_id }: Props) {
36
+ const { t } = useLocaleContext();
37
+
38
+ const { data, loadMore, loadingMore, loading } = useInfiniteScroll<Paginated<TPaymentIntentExpanded>>(
39
+ (d) => {
40
+ const page = d ? Math.ceil(d.list.length / pageSize) + 1 : 1;
41
+ return fetchData({ page, pageSize, invoice_id });
42
+ },
43
+ {
44
+ reloadDeps: [invoice_id],
45
+ }
46
+ );
47
+
48
+ if (loading || !data) {
49
+ return <CircularProgress />;
50
+ }
51
+
52
+ if (data && data.list.length === 0) {
53
+ return <Typography color="text.secondary">{t('payment.customer.payment.empty')}</Typography>;
54
+ }
55
+
56
+ const hasMore = data && data.list.length < data.count;
57
+
58
+ const grouped = groupByDate(data.list as any);
59
+
60
+ return (
61
+ <Stack direction="column" gap={1} sx={{ mt: 1 }}>
62
+ {Object.entries(grouped).map(([date, payments]) => (
63
+ <Box key={date}>
64
+ <Typography sx={{ fontWeight: 'bold', color: 'text.secondary', mt: 2, mb: 1 }}>{date}</Typography>
65
+ {payments.map((item: any) => {
66
+ return (
67
+ <Stack
68
+ key={item.id}
69
+ direction={{
70
+ xs: 'column',
71
+ sm: 'row',
72
+ }}
73
+ sx={{ my: 1 }}
74
+ gap={{
75
+ xs: 0.5,
76
+ sm: 1.5,
77
+ md: 3,
78
+ }}
79
+ flexWrap="nowrap">
80
+ <Box flex={3}>
81
+ <Typography>{formatToDate(item.created_at)}</Typography>
82
+ </Box>
83
+ <Box flex={2}>
84
+ <Typography textAlign="right">
85
+ {fromUnitToToken(item.amount, item.paymentCurrency.decimal)}&nbsp;
86
+ {item.paymentCurrency.symbol}
87
+ </Typography>
88
+ </Box>
89
+ <Box flex={3}>
90
+ <Status label={item.status} color={getPaymentIntentStatusColor(item.status)} />
91
+ </Box>
92
+ <Box flex={3}>
93
+ <Typography>{item.description || '-'}</Typography>
94
+ </Box>
95
+ <Box flex={3} sx={{ minWidth: '220px' }}>
96
+ {item.payment_details?.arcblock?.tx_hash && (
97
+ <TxLink
98
+ details={item.payment_details as PaymentDetails}
99
+ method={item.paymentMethod}
100
+ mode="customer"
101
+ />
102
+ )}
103
+ </Box>
104
+ </Stack>
105
+ );
106
+ })}
107
+ </Box>
108
+ ))}
109
+ <Box>
110
+ {hasMore && (
111
+ <Button variant="text" type="button" color="inherit" onClick={loadMore} disabled={loadingMore}>
112
+ {loadingMore
113
+ ? t('common.loadingMore', { resource: t('payment.customer.payments') })
114
+ : t('common.loadMore', { resource: t('payment.customer.payments') })}
115
+ </Button>
116
+ )}
117
+ {!hasMore && data.count > pageSize && (
118
+ <Typography color="text.secondary">
119
+ {t('common.noMore', { resource: t('payment.customer.payments') })}
120
+ </Typography>
121
+ )}
122
+ </Box>
123
+ </Stack>
124
+ );
125
+ }