payment-kit 1.13.150 → 1.13.152

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 (39) hide show
  1. package/api/src/libs/notification/template/subscription-refund-succeeded.ts +1 -1
  2. package/api/src/queues/invoice.ts +14 -0
  3. package/api/src/queues/refund.ts +1 -0
  4. package/api/src/routes/checkout-sessions.ts +9 -1
  5. package/api/src/routes/connect/shared.ts +1 -0
  6. package/api/src/routes/customers.ts +22 -1
  7. package/api/src/routes/refunds.ts +14 -0
  8. package/api/src/routes/subscriptions.ts +2 -1
  9. package/api/src/store/migrations/20240225-refund-ext.ts +22 -0
  10. package/api/src/store/models/customer.ts +21 -0
  11. package/api/src/store/models/index.ts +1 -1
  12. package/api/src/store/models/invoice.ts +37 -1
  13. package/api/src/store/models/payment-intent.ts +24 -2
  14. package/api/src/store/models/refund.ts +53 -16
  15. package/api/src/store/models/types.ts +2 -0
  16. package/blocklet.yml +1 -1
  17. package/package.json +4 -4
  18. package/src/app.tsx +10 -0
  19. package/src/components/balance-list.tsx +43 -0
  20. package/src/components/info-metric.tsx +2 -2
  21. package/src/components/invoice/table.tsx +1 -1
  22. package/src/components/payment-intent/list.tsx +1 -1
  23. package/src/components/payment-link/after-pay.tsx +0 -19
  24. package/src/components/refund/list.tsx +6 -2
  25. package/src/components/section/header.tsx +3 -1
  26. package/src/components/subscription/items/usage-records.tsx +1 -1
  27. package/src/global.css +0 -1
  28. package/src/locales/en.tsx +2 -71
  29. package/src/locales/zh.tsx +2 -71
  30. package/src/pages/admin/billing/invoices/detail.tsx +7 -0
  31. package/src/pages/admin/billing/subscriptions/detail.tsx +7 -0
  32. package/src/pages/admin/customers/customers/detail.tsx +49 -32
  33. package/src/pages/admin/index.tsx +4 -2
  34. package/src/pages/admin/payments/intents/detail.tsx +17 -13
  35. package/src/pages/admin/payments/links/create.tsx +1 -1
  36. package/src/pages/customer/index.tsx +55 -9
  37. package/src/pages/customer/invoice/past-due.tsx +77 -0
  38. package/src/pages/customer/invoice.tsx +58 -56
  39. package/src/pages/customer/refund/list.tsx +125 -0
@@ -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}`;
@@ -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,
@@ -504,6 +504,14 @@ router.put('/:id/submit', user, ensureCheckoutSessionOpen, async (req, res) => {
504
504
 
505
505
  await customer.update(updates);
506
506
  }
507
+ const canMakeNewPurchase = await customer.canMakeNewPurchase(checkoutSession.invoice_id);
508
+ if (!canMakeNewPurchase) {
509
+ return res.status(403).json({
510
+ code: 'CUSTOMER_LIMITED',
511
+ error: 'Customer can not make new purchase, maybe you have unpaid invoices from previous purchases',
512
+ });
513
+ }
514
+
507
515
  await checkoutSession.update({ customer_id: customer.id, customer_did: req.user.did });
508
516
 
509
517
  // payment intent is only created when checkout session is in payment mode
@@ -532,6 +532,7 @@ export async function ensureInvoiceForCollect(invoiceId: string) {
532
532
  throw new Error(`Payment intent already canceled for invoice ${invoiceId}`);
533
533
  }
534
534
  if (paymentIntent.status === 'succeeded') {
535
+ await invoice.update({ status: 'paid' });
535
536
  throw new Error(`Payment intent already succeeded for invoice ${invoiceId}`);
536
537
  }
537
538
 
@@ -102,7 +102,12 @@ router.get('/me', user(), async (req, res) => {
102
102
 
103
103
  try {
104
104
  const doc = await Customer.findByPkOrDid(req.user.did as string);
105
- res.json(doc);
105
+ if (!doc) {
106
+ res.status(404).json({ error: 'Customer not found' });
107
+ } else {
108
+ const summary = await doc.getSummary();
109
+ res.json({ ...doc.toJSON(), summary });
110
+ }
106
111
  } catch (err) {
107
112
  console.error(err);
108
113
  res.json(null);
@@ -119,6 +124,22 @@ router.get('/:id', auth, async (req, res) => {
119
124
  }
120
125
  });
121
126
 
127
+ router.get('/:id/summary', auth, async (req, res) => {
128
+ try {
129
+ const doc = await Customer.findByPkOrDid(req.params.id as string);
130
+ if (!doc) {
131
+ res.status(404).json({ error: 'Customer not found' });
132
+ return;
133
+ }
134
+
135
+ const result = await doc.getSummary();
136
+ res.json(result);
137
+ } catch (err) {
138
+ console.error(err);
139
+ res.json(null);
140
+ }
141
+ });
142
+
122
143
  // eslint-disable-next-line consistent-return
123
144
  router.put('/:id', authPortal, async (req, res) => {
124
145
  try {
@@ -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
+ };
@@ -154,6 +154,27 @@ export class Customer extends Model<InferAttributes<Customer>, InferCreationAttr
154
154
  return `${this.invoice_prefix}-${padStart(sequence.toString(), 4, '0')}`;
155
155
  }
156
156
 
157
+ public async getSummary() {
158
+ const { PaymentIntent, Refund, Invoice } = this.sequelize.models;
159
+ const [paid, due, refunded] = await Promise.all([
160
+ // @ts-ignore
161
+ PaymentIntent!.getPaidAmountByCustomer(this.id),
162
+ // @ts-ignore
163
+ Invoice!.getUncollectibleAmountByCustomer(this.id),
164
+ // @ts-ignore
165
+ Refund!.getRefundAmountByCustomer(this.id),
166
+ ]);
167
+
168
+ return { paid, due, refunded };
169
+ }
170
+
171
+ public async canMakeNewPurchase(excludedInvoiceId: string = '') {
172
+ const { Invoice } = this.sequelize.models;
173
+ // @ts-ignore
174
+ const result = await Invoice!.getUncollectibleAmountByCustomer(this.id, excludedInvoiceId);
175
+ return Object.entries(result).every(([, amount]) => new BN(amount).lte(new BN(0)));
176
+ }
177
+
157
178
  public getBalanceToApply(currencyId: string, amount: string) {
158
179
  const tokens = this.token_balance || {};
159
180
  const balance = tokens[currencyId] || '0';
@@ -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;
@@ -1,6 +1,15 @@
1
1
  /* eslint-disable @typescript-eslint/indent */
2
2
  /* eslint-disable @typescript-eslint/lines-between-class-members */
3
- import { CreationOptional, DataTypes, InferAttributes, InferCreationAttributes, Model } from 'sequelize';
3
+ import { BN } from '@ocap/util';
4
+ import {
5
+ CreationOptional,
6
+ DataTypes,
7
+ InferAttributes,
8
+ InferCreationAttributes,
9
+ Model,
10
+ Op,
11
+ WhereOptions,
12
+ } from 'sequelize';
4
13
  import type { LiteralUnion } from 'type-fest';
5
14
 
6
15
  import { createEvent, createStatusEvent } from '../../libs/audit';
@@ -9,6 +18,7 @@ import type {
9
18
  CustomerAddress,
10
19
  CustomerShipping,
11
20
  DiscountAmount,
21
+ GroupedBN,
12
22
  PaymentError,
13
23
  PaymentSettings,
14
24
  SimpleCustomField,
@@ -511,6 +521,32 @@ export class Invoice extends Model<InferAttributes<Invoice>, InferCreationAttrib
511
521
  public isImmutable() {
512
522
  return ['paid', 'void'].includes(this.status);
513
523
  }
524
+
525
+ public static async getUncollectibleAmountByCustomer(
526
+ customerId: string,
527
+ excludedInvoiceId?: string
528
+ ): Promise<GroupedBN> {
529
+ const where: WhereOptions<Invoice> = {
530
+ status: 'uncollectible',
531
+ customer_id: customerId,
532
+ amount_remaining: { [Op.gt]: '0' },
533
+ };
534
+ if (excludedInvoiceId) {
535
+ where.id = { [Op.not]: excludedInvoiceId };
536
+ }
537
+ const invoices = await Invoice.findAll({ where });
538
+
539
+ return invoices.reduce((acc: GroupedBN, invoice) => {
540
+ const key = invoice.currency_id;
541
+ if (!acc[key]) {
542
+ acc[key] = '0';
543
+ }
544
+
545
+ acc[key] = new BN(acc[key]).add(new BN(invoice.amount_remaining)).toString();
546
+
547
+ return acc;
548
+ }, {});
549
+ }
514
550
  }
515
551
 
516
552
  export type TInvoice = InferAttributes<Invoice>;
@@ -1,11 +1,12 @@
1
1
  /* eslint-disable @typescript-eslint/lines-between-class-members */
2
2
  /* eslint-disable @typescript-eslint/indent */
3
- import { CreationOptional, DataTypes, InferAttributes, InferCreationAttributes, Model } from 'sequelize';
3
+ import { BN } from '@ocap/util';
4
+ import { CreationOptional, DataTypes, InferAttributes, InferCreationAttributes, Model, Op } from 'sequelize';
4
5
  import type { LiteralUnion } from 'type-fest';
5
6
 
6
7
  import { createEvent, createStatusEvent } from '../../libs/audit';
7
8
  import { createIdGenerator } from '../../libs/util';
8
- import type { PaymentDetails, PaymentError } from './types';
9
+ import type { GroupedBN, PaymentDetails, PaymentError } from './types';
9
10
 
10
11
  export const nextPaymentIntentId = createIdGenerator('pi', 24);
11
12
 
@@ -280,6 +281,27 @@ export class PaymentIntent extends Model<InferAttributes<PaymentIntent>, InferCr
280
281
  public isImmutable() {
281
282
  return ['canceled', 'succeeded'].includes(this.status);
282
283
  }
284
+
285
+ public static async getPaidAmountByCustomer(customerId: string): Promise<GroupedBN> {
286
+ const payments = await PaymentIntent.findAll({
287
+ where: {
288
+ status: 'succeeded',
289
+ customer_id: customerId,
290
+ amount_received: { [Op.gt]: '0' },
291
+ },
292
+ });
293
+
294
+ return payments.reduce((acc: GroupedBN, payment) => {
295
+ const key = payment.currency_id;
296
+ if (!acc[key]) {
297
+ acc[key] = '0';
298
+ }
299
+
300
+ acc[key] = new BN(acc[key]).add(new BN(payment.amount_received)).toString();
301
+
302
+ return acc;
303
+ }, {});
304
+ }
283
305
  }
284
306
 
285
307
  export type TPaymentIntent = InferAttributes<PaymentIntent>;
@@ -1,11 +1,12 @@
1
1
  /* eslint-disable @typescript-eslint/lines-between-class-members */
2
2
  /* eslint-disable @typescript-eslint/indent */
3
- import { CreationOptional, DataTypes, InferAttributes, InferCreationAttributes, Model } from 'sequelize';
3
+ import { BN } from '@ocap/util';
4
+ import { CreationOptional, DataTypes, InferAttributes, InferCreationAttributes, Model, Op } from 'sequelize';
4
5
  import type { LiteralUnion } from 'type-fest';
5
6
 
6
7
  import { createEvent, createStatusEvent } from '../../libs/audit';
7
8
  import { createIdGenerator } from '../../libs/util';
8
- import type { PaymentDetails, PaymentError } from './types';
9
+ import type { GroupedBN, PaymentDetails, PaymentError } from './types';
9
10
 
10
11
  export const nextRefundId = createIdGenerator('re', 24);
11
12
 
@@ -25,6 +26,7 @@ export class Refund extends Model<InferAttributes<Refund>, InferCreationAttribut
25
26
  declare currency_id: string;
26
27
  declare customer_id: string;
27
28
  declare payment_intent_id: string;
29
+ declare payment_method_id: string;
28
30
  declare invoice_id?: string;
29
31
  declare subscription_id?: string;
30
32
 
@@ -173,21 +175,30 @@ export class Refund extends Model<InferAttributes<Refund>, InferCreationAttribut
173
175
  };
174
176
 
175
177
  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),
178
+ this.init(
179
+ {
180
+ ...Refund.GENESIS_ATTRIBUTES,
181
+ payment_method_id: {
182
+ type: DataTypes.STRING(30),
183
+ allowNull: true,
184
+ },
189
185
  },
190
- });
186
+ {
187
+ sequelize,
188
+ modelName: 'Refund',
189
+ tableName: 'refunds',
190
+ createdAt: 'created_at',
191
+ updatedAt: 'updated_at',
192
+ hooks: {
193
+ afterCreate: (model: Refund, options) =>
194
+ createEvent('Refund', 'refund.created', model, options).catch(console.error),
195
+ afterUpdate: (model: Refund, options) =>
196
+ createStatusEvent('Refund', 'refund', { canceled: 'canceled', succeeded: 'succeeded' }, model, options),
197
+ afterDestroy: (model: Refund, options) =>
198
+ createEvent('Refund', 'refund.deleted', model, options).catch(console.error),
199
+ },
200
+ }
201
+ );
191
202
  }
192
203
 
193
204
  public static associate(models: any) {
@@ -206,6 +217,11 @@ export class Refund extends Model<InferAttributes<Refund>, InferCreationAttribut
206
217
  foreignKey: 'id',
207
218
  as: 'paymentIntent',
208
219
  });
220
+ this.hasOne(models.PaymentMethod, {
221
+ sourceKey: 'payment_method_id',
222
+ foreignKey: 'id',
223
+ as: 'paymentMethod',
224
+ });
209
225
  this.hasOne(models.Invoice, {
210
226
  sourceKey: 'invoice_id',
211
227
  foreignKey: 'id',
@@ -221,6 +237,27 @@ export class Refund extends Model<InferAttributes<Refund>, InferCreationAttribut
221
237
  public isImmutable() {
222
238
  return ['canceled', 'succeeded'].includes(this.status);
223
239
  }
240
+
241
+ public static async getRefundAmountByCustomer(customerId: string, status: string = 'succeeded'): Promise<GroupedBN> {
242
+ const refunds = await Refund.findAll({
243
+ where: {
244
+ status,
245
+ customer_id: customerId,
246
+ amount: { [Op.gt]: '0' },
247
+ },
248
+ });
249
+
250
+ return refunds.reduce((acc: GroupedBN, refund) => {
251
+ const key = refund.currency_id;
252
+ if (!acc[key]) {
253
+ acc[key] = '0';
254
+ }
255
+
256
+ acc[key] = new BN(acc[key]).add(new BN(refund.amount)).toString();
257
+
258
+ return acc;
259
+ }, {});
260
+ }
224
261
  }
225
262
 
226
263
  export type TRefund = InferAttributes<Refund>;
@@ -1,6 +1,8 @@
1
1
  /* eslint-disable @typescript-eslint/indent */
2
2
  import type { LiteralUnion } from 'type-fest';
3
3
 
4
+ export type GroupedBN = { [currencyId: string]: string };
5
+
4
6
  export type Pagination<T = any> = T & {
5
7
  // offset based
6
8
  page?: number;
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.150
17
+ version: 1.13.152
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.150",
3
+ "version": "1.13.152",
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.150",
53
+ "@blocklet/payment-react": "1.13.152",
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.150",
113
+ "@blocklet/payment-types": "1.13.152",
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": "b65548baa2ab6bd40aec35b2a6594bfe6dbb7ee9"
152
+ "gitHead": "5d6d2b6a8c0422e1055ddcec4e3179fd734cdbc5"
153
153
  }
package/src/app.tsx CHANGED
@@ -21,6 +21,7 @@ const CheckoutPage = React.lazy(() => import('./pages/checkout'));
21
21
  const AdminPage = React.lazy(() => import('./pages/admin'));
22
22
  const CustomerHome = React.lazy(() => import('./pages/customer/index'));
23
23
  const CustomerInvoice = React.lazy(() => import('./pages/customer/invoice'));
24
+ const CustomerInvoicePastDue = React.lazy(() => import('./pages/customer/invoice/past-due'));
24
25
  const CustomerSubscriptionDetail = React.lazy(() => import('./pages/customer/subscription/detail'));
25
26
  const CustomerSubscriptionUpdate = React.lazy(() => import('./pages/customer/subscription/update'));
26
27
 
@@ -79,6 +80,15 @@ function App() {
79
80
  }
80
81
  />
81
82
  <Route key="subscription-embed" path="/customer/embed/subscription" element={<MiniInvoiceList />} />,
83
+ <Route
84
+ key="customer-due"
85
+ path="/customer/invoice/past-due"
86
+ element={
87
+ <Layout>
88
+ <CustomerInvoicePastDue />
89
+ </Layout>
90
+ }
91
+ />
82
92
  <Route
83
93
  key="customer-invoice"
84
94
  path="/customer/invoice/:id"
@@ -0,0 +1,43 @@
1
+ import { formatAmount, usePaymentContext } from '@blocklet/payment-react';
2
+ import type { GroupedBN } from '@blocklet/payment-types';
3
+ import { Stack, Typography } from '@mui/material';
4
+ import flatten from 'lodash/flatten';
5
+ import isEmpty from 'lodash/isEmpty';
6
+
7
+ type Props = {
8
+ data?: GroupedBN;
9
+ };
10
+
11
+ export default function BalanceList(props: Props) {
12
+ const { settings } = usePaymentContext();
13
+ const currencies = flatten(settings.paymentMethods.map((method) => method.payment_currencies));
14
+
15
+ if (isEmpty(props.data)) {
16
+ return <Typography>None</Typography>;
17
+ }
18
+
19
+ return (
20
+ <Stack direction="column" alignItems="flex-start" sx={{ width: '100%' }}>
21
+ {Object.entries(props.data).map(([currencyId, amount]) => {
22
+ const currency = currencies.find((c) => c.id === currencyId);
23
+ if (!currency) {
24
+ return null;
25
+ }
26
+ return (
27
+ <Stack key={currencyId} sx={{ width: '100%' }} direction="row" spacing={1}>
28
+ <Typography sx={{ flex: 1 }} color="text.primary">
29
+ {formatAmount(amount, currency.decimal)}
30
+ </Typography>
31
+ <Typography sx={{ width: '32px' }} color="text.secondary">
32
+ {currency.symbol}
33
+ </Typography>
34
+ </Stack>
35
+ );
36
+ })}
37
+ </Stack>
38
+ );
39
+ }
40
+
41
+ BalanceList.defaultProps = {
42
+ data: {},
43
+ };
@@ -12,7 +12,7 @@ export default function InfoMetric(props: Props) {
12
12
  return (
13
13
  <>
14
14
  <Stack direction="column" alignItems="flex-start">
15
- <Typography variant="body1" mb={1} color="text.secondary">
15
+ <Typography component="div" variant="body1" mb={1} color="text.secondary">
16
16
  {props.label}
17
17
  {!!props.tip && (
18
18
  <Tooltip title={props.tip}>
@@ -20,7 +20,7 @@ export default function InfoMetric(props: Props) {
20
20
  </Tooltip>
21
21
  )}
22
22
  </Typography>
23
- <Typography variant="body1" color="text.primary">
23
+ <Typography component="div" variant="body1" color="text.primary" sx={{ width: '100%' }}>
24
24
  {props.value}
25
25
  </Typography>
26
26
  </Stack>
@@ -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}