payment-kit 1.13.73 → 1.13.74

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 (65) hide show
  1. package/api/src/{schedule → crons}/base.ts +1 -1
  2. package/api/src/index.ts +7 -7
  3. package/api/src/integrations/stripe/handlers/customer.ts +24 -0
  4. package/api/src/integrations/stripe/handlers/index.ts +4 -0
  5. package/api/src/integrations/stripe/handlers/payment-intent.ts +1 -1
  6. package/api/src/integrations/stripe/resource.ts +1 -1
  7. package/api/src/libs/audit.ts +34 -28
  8. package/api/src/libs/payment.ts +26 -0
  9. package/api/src/libs/queue/index.ts +18 -1
  10. package/api/src/libs/queue/store.ts +6 -5
  11. package/api/src/libs/session.ts +13 -12
  12. package/api/src/libs/subscription.ts +26 -0
  13. package/api/src/libs/util.ts +5 -1
  14. package/api/src/{jobs → queues}/checkout-session.ts +11 -0
  15. package/api/src/{jobs → queues}/invoice.ts +15 -6
  16. package/api/src/{jobs → queues}/payment.ts +182 -30
  17. package/api/src/{jobs → queues}/subscription.ts +36 -104
  18. package/api/src/{jobs → queues}/webhook.ts +2 -0
  19. package/api/src/routes/checkout-sessions.ts +68 -19
  20. package/api/src/routes/connect/collect.ts +2 -2
  21. package/api/src/routes/connect/pay.ts +1 -1
  22. package/api/src/routes/connect/setup.ts +2 -2
  23. package/api/src/routes/connect/shared.ts +94 -45
  24. package/api/src/routes/connect/subscribe.ts +3 -3
  25. package/api/src/routes/pricing-table.ts +2 -0
  26. package/api/src/routes/subscription-items.ts +1 -1
  27. package/api/src/routes/subscriptions.ts +434 -13
  28. package/api/src/store/migrate.ts +0 -1
  29. package/api/src/store/migrations/20231204-subupdate.ts +50 -0
  30. package/api/src/store/models/checkout-session.ts +4 -0
  31. package/api/src/store/models/customer.ts +52 -15
  32. package/api/src/store/models/invoice-item.ts +6 -1
  33. package/api/src/store/models/invoice.ts +41 -22
  34. package/api/src/store/models/payment-intent.ts +4 -0
  35. package/api/src/store/models/setup-intent.ts +4 -0
  36. package/api/src/store/models/subscription-item.ts +0 -4
  37. package/api/src/store/models/subscription.ts +77 -44
  38. package/api/src/store/models/types.ts +1 -0
  39. package/api/src/store/sequelize.ts +6 -0
  40. package/api/third.d.ts +2 -0
  41. package/blocklet.yml +1 -1
  42. package/jest.config.js +14 -0
  43. package/package.json +24 -19
  44. package/src/components/blockchain/tx.tsx +20 -11
  45. package/src/components/checkout/form/index.tsx +1 -1
  46. package/src/components/invoice/table.tsx +58 -19
  47. package/src/components/layout/admin.tsx +17 -5
  48. package/src/components/portal/invoice/list.tsx +12 -8
  49. package/src/components/portal/subscription/list.tsx +114 -77
  50. package/src/components/subscription/status.tsx +21 -19
  51. package/src/global.css +4 -0
  52. package/src/locales/en.tsx +14 -1
  53. package/src/locales/zh.tsx +14 -0
  54. package/src/pages/admin/customers/customers/detail.tsx +47 -3
  55. package/src/pages/admin/overview.tsx +21 -1
  56. package/src/pages/admin/payments/intents/detail.tsx +12 -3
  57. package/src/pages/customer/invoice.tsx +15 -1
  58. package/src/pages/customer/subscription/index.tsx +9 -2
  59. package/tests/api/libs/subscription.spec.ts +45 -0
  60. /package/api/src/{schedule → crons}/index.ts +0 -0
  61. /package/api/src/{schedule → crons}/interface/diff.ts +0 -0
  62. /package/api/src/{schedule → crons}/subscription-trail-will-end.ts +0 -0
  63. /package/api/src/{schedule → crons}/subscription-will-renew.ts +0 -0
  64. /package/api/src/{jobs → queues}/event.ts +0 -0
  65. /package/api/src/{jobs → queues}/notification.ts +0 -0
@@ -68,8 +68,10 @@ export class Invoice extends Model<InferAttributes<Invoice>, InferCreationAttrib
68
68
  declare amount_remaining: string;
69
69
  declare amount_shipping: string;
70
70
 
71
- declare ending_balance: string;
72
71
  declare starting_balance: string;
72
+ declare ending_balance: string;
73
+ declare starting_token_balance?: Record<string, string>; // token balances
74
+ declare ending_token_balance?: Record<string, string>; // token balances
73
75
 
74
76
  declare attempt_count: number;
75
77
  declare attempted: boolean;
@@ -427,29 +429,42 @@ export class Invoice extends Model<InferAttributes<Invoice>, InferCreationAttrib
427
429
  };
428
430
 
429
431
  public static initialize(sequelize: any) {
430
- this.init(Invoice.GENESIS_ATTRIBUTES, {
431
- sequelize,
432
- modelName: 'Invoice',
433
- tableName: 'invoices',
434
- createdAt: 'created_at',
435
- updatedAt: 'updated_at',
436
- hooks: {
437
- afterCreate: (model: Invoice, options) =>
438
- createEvent('Invoice', 'invoice.created', model, options).catch(console.error),
439
- afterUpdate: (model: Invoice, options) => {
440
- createEvent('Invoice', 'invoice.updated', model, options).catch(console.error);
441
- createStatusEvent(
442
- 'Invoice',
443
- 'invoice',
444
- { open: 'finalized', void: 'voided', paid: 'paid', uncollectible: 'marked_uncollectible' },
445
- model,
446
- options
447
- ).catch(console.error);
432
+ this.init(
433
+ {
434
+ ...Invoice.GENESIS_ATTRIBUTES,
435
+ starting_token_balance: {
436
+ type: DataTypes.JSON,
437
+ defaultValue: {},
438
+ },
439
+ ending_token_balance: {
440
+ type: DataTypes.JSON,
441
+ defaultValue: {},
448
442
  },
449
- afterDestroy: (model: Invoice, options) =>
450
- createEvent('Invoice', 'invoice.deleted', model, options).catch(console.error),
451
443
  },
452
- });
444
+ {
445
+ sequelize,
446
+ modelName: 'Invoice',
447
+ tableName: 'invoices',
448
+ createdAt: 'created_at',
449
+ updatedAt: 'updated_at',
450
+ hooks: {
451
+ afterCreate: (model: Invoice, options) =>
452
+ createEvent('Invoice', 'invoice.created', model, options).catch(console.error),
453
+ afterUpdate: (model: Invoice, options) => {
454
+ createEvent('Invoice', 'invoice.updated', model, options).catch(console.error);
455
+ createStatusEvent(
456
+ 'Invoice',
457
+ 'invoice',
458
+ { open: 'finalized', void: 'voided', paid: 'paid', uncollectible: 'marked_uncollectible' },
459
+ model,
460
+ options
461
+ ).catch(console.error);
462
+ },
463
+ afterDestroy: (model: Invoice, options) =>
464
+ createEvent('Invoice', 'invoice.deleted', model, options).catch(console.error),
465
+ },
466
+ }
467
+ );
453
468
  }
454
469
 
455
470
  public static associate(models: any) {
@@ -489,6 +504,10 @@ export class Invoice extends Model<InferAttributes<Invoice>, InferCreationAttrib
489
504
  as: 'lines',
490
505
  });
491
506
  }
507
+
508
+ public isImmutable() {
509
+ return ['paid', 'void'].includes(this.status);
510
+ }
492
511
  }
493
512
 
494
513
  export type TInvoice = InferAttributes<Invoice>;
@@ -276,6 +276,10 @@ export class PaymentIntent extends Model<InferAttributes<PaymentIntent>, InferCr
276
276
  as: 'customer',
277
277
  });
278
278
  }
279
+
280
+ public isImmutable() {
281
+ return ['canceled', 'succeeded'].includes(this.status);
282
+ }
279
283
  }
280
284
 
281
285
  export type TPaymentIntent = InferAttributes<PaymentIntent>;
@@ -201,6 +201,10 @@ export class SetupIntent extends Model<InferAttributes<SetupIntent>, InferCreati
201
201
  as: 'customer',
202
202
  });
203
203
  }
204
+
205
+ public isImmutable() {
206
+ return ['canceled', 'succeeded'].includes(this.status);
207
+ }
204
208
  }
205
209
 
206
210
  export type TSetupIntent = InferAttributes<SetupIntent>;
@@ -24,10 +24,6 @@ export class SubscriptionItem extends Model<InferAttributes<SubscriptionItem>, I
24
24
  usage_gte: number;
25
25
  };
26
26
 
27
- // Fields exists on creation
28
- // declare proration_behavior: LiteralUnion<'always_invoice' | 'create_prorations' | 'none', string>;
29
- // declare payment_behavior: LiteralUnion<'allow_incomplete' | 'error_if_incomplete' | 'pending_if_incomplete', string>;
30
-
31
27
  // TODO: following fields not supported
32
28
  // tax_rates
33
29
 
@@ -38,7 +38,7 @@ export class Subscription extends Model<InferAttributes<Subscription>, InferCrea
38
38
  };
39
39
 
40
40
  declare status: LiteralUnion<
41
- 'active' | 'past_due' | 'unpaid' | 'canceled' | 'incomplete' | 'incomplete_expired' | 'trialing' | 'paused',
41
+ 'active' | 'past_due' | 'paused' | 'canceled' | 'incomplete' | 'incomplete_expired' | 'trialing',
42
42
  string
43
43
  >;
44
44
 
@@ -101,6 +101,9 @@ export class Subscription extends Model<InferAttributes<Subscription>, InferCrea
101
101
  // 3rd party payment tx hash
102
102
  declare payment_details?: PaymentDetails;
103
103
 
104
+ declare proration_behavior?: LiteralUnion<'always_invoice' | 'create_prorations' | 'none', string>;
105
+ declare payment_behavior?: LiteralUnion<'allow_incomplete' | 'error_if_incomplete' | 'pending_if_incomplete', string>;
106
+
104
107
  // TODO: following fields not supported
105
108
  // application
106
109
  // application_fee_percent
@@ -164,7 +167,7 @@ export class Subscription extends Model<InferAttributes<Subscription>, InferCrea
164
167
  allowNull: true,
165
168
  },
166
169
  status: {
167
- type: DataTypes.ENUM('active', 'canceled', 'incomplete', 'incomplete_expired', 'past_due', 'trialing', 'unpaid'),
170
+ type: DataTypes.ENUM('active', 'canceled', 'incomplete', 'incomplete_expired', 'past_due', 'trialing', 'paused'),
168
171
  allowNull: false,
169
172
  },
170
173
  cancel_at: {
@@ -260,52 +263,74 @@ export class Subscription extends Model<InferAttributes<Subscription>, InferCrea
260
263
  };
261
264
 
262
265
  public static initialize(sequelize: any) {
263
- this.init(Subscription.GENESIS_ATTRIBUTES, {
264
- sequelize,
265
- modelName: 'Subscription',
266
- tableName: 'subscriptions',
267
- createdAt: 'created_at',
268
- updatedAt: 'updated_at',
269
- hooks: {
270
- afterCreate: (model: Subscription, options) => {
271
- createEvent('Subscription', 'customer.subscription.created', model, options).catch(console.error);
266
+ this.init(
267
+ {
268
+ ...Subscription.GENESIS_ATTRIBUTES,
269
+ proration_behavior: {
270
+ type: DataTypes.ENUM('always_invoice', 'create_prorations', 'none'),
271
+ defaultValue: 'none',
272
+ },
273
+ payment_behavior: {
274
+ type: DataTypes.ENUM(
275
+ 'default_incomplete',
276
+ 'allow_incomplete',
277
+ 'error_if_incomplete',
278
+ 'pending_if_incomplete'
279
+ ),
280
+ defaultValue: 'default_incomplete',
272
281
  },
273
- afterUpdate: async (model: Subscription, options) => {
274
- createEvent('Subscription', 'customer.subscription.updated', model, options).catch(console.error);
275
-
276
- if (model.trail_start) {
277
- const previousLatestInvoiceId = model.previous('latest_invoice_id');
278
-
279
- if (!previousLatestInvoiceId && model.latest_invoice_id) {
280
- createEvent('Subscription', 'customer.subscription.trial_start', model, options).catch(console.error);
281
- } else if (
282
- previousLatestInvoiceId &&
283
- model.latest_invoice_id &&
284
- previousLatestInvoiceId !== model.latest_invoice_id
285
- ) {
286
- const count: number = await Invoice.count({
287
- where: {
288
- subscription_id: model.id,
289
- },
290
- });
291
- if (count === 2) {
292
- // 当且仅当 有试用期的订阅更新了 && 恰好有 2 次发票,此时订阅的试用期刚好结束
293
- createEvent('Subscription', 'customer.subscription.trial_end', model, options).catch(console.error);
282
+ },
283
+ {
284
+ sequelize,
285
+ modelName: 'Subscription',
286
+ tableName: 'subscriptions',
287
+ createdAt: 'created_at',
288
+ updatedAt: 'updated_at',
289
+ hooks: {
290
+ afterCreate: (model: Subscription, options) => {
291
+ createEvent('Subscription', 'customer.subscription.created', model, options).catch(console.error);
292
+ },
293
+ afterUpdate: async (model: Subscription, options) => {
294
+ createEvent('Subscription', 'customer.subscription.updated', model, options).catch(console.error);
295
+
296
+ if (model.trail_start) {
297
+ const previousLatestInvoiceId = model.previous('latest_invoice_id');
298
+
299
+ if (!previousLatestInvoiceId && model.latest_invoice_id) {
300
+ createEvent('Subscription', 'customer.subscription.trial_start', model, options).catch(console.error);
301
+ } else if (
302
+ previousLatestInvoiceId &&
303
+ model.latest_invoice_id &&
304
+ previousLatestInvoiceId !== model.latest_invoice_id
305
+ ) {
306
+ const count: number = await Invoice.count({
307
+ where: {
308
+ subscription_id: model.id,
309
+ },
310
+ });
311
+ if (count === 2) {
312
+ // 当且仅当 有试用期的订阅更新了 && 恰好有 2 次发票,此时订阅的试用期刚好结束
313
+ createEvent('Subscription', 'customer.subscription.trial_end', model, options).catch(console.error);
314
+ }
294
315
  }
316
+ } else if (!model.previous('latest_invoice_id') && model.latest_invoice_id) {
317
+ createEvent('Subscription', 'customer.subscription.started', model, options).catch(console.error);
295
318
  }
296
- } else if (!model.previous('latest_invoice_id') && model.latest_invoice_id) {
297
- createEvent('Subscription', 'customer.subscription.started', model, options).catch(console.error);
298
- }
299
-
300
- createStatusEvent('Subscription', 'customer.subscription', { canceled: 'deleted' }, model, options).catch(
301
- console.error
302
- );
303
- createCustomEvent('Subscription', 'customer.subscription', getSubscriptionEventType, model, options).catch(
304
- console.error
305
- );
319
+
320
+ createStatusEvent(
321
+ 'Subscription',
322
+ 'customer.subscription',
323
+ { canceled: 'deleted', past_due: 'past_due' },
324
+ model,
325
+ options
326
+ ).catch(console.error);
327
+ createCustomEvent('Subscription', 'customer.subscription', getSubscriptionEventType, model, options).catch(
328
+ console.error
329
+ );
330
+ },
306
331
  },
307
- },
308
- });
332
+ }
333
+ );
309
334
  }
310
335
 
311
336
  public static associate(models: any) {
@@ -329,6 +354,14 @@ export class Subscription extends Model<InferAttributes<Subscription>, InferCrea
329
354
  as: 'items',
330
355
  });
331
356
  }
357
+
358
+ public isImmutable() {
359
+ return ['canceled', 'incomplete_expired'].includes(this.status);
360
+ }
361
+
362
+ public isActive() {
363
+ return ['active', 'trialing'].includes(this.status);
364
+ }
332
365
  }
333
366
 
334
367
  export type TSubscription = InferAttributes<Subscription>;
@@ -399,6 +399,7 @@ export type EventType = LiteralUnion<
399
399
  | 'customer.subscription.created'
400
400
  | 'customer.subscription.deleted'
401
401
  | 'customer.subscription.paused'
402
+ | 'customer.subscription.past_due'
402
403
  | 'customer.subscription.pending_update_applied'
403
404
  | 'customer.subscription.pending_update_expired'
404
405
  | 'customer.subscription.resumed'
@@ -1,12 +1,18 @@
1
+ /* eslint-disable react-hooks/rules-of-hooks */
1
2
  // NOTE: add next line to keep sqlite3 in the bundle
2
3
  import 'sqlite3';
3
4
 
4
5
  import { join } from 'path';
5
6
 
7
+ import CLS from 'cls-hooked';
6
8
  import { Sequelize } from 'sequelize';
7
9
 
8
10
  import env from '../libs/env';
9
11
 
12
+ const namespace = CLS.createNamespace('payment-kit');
13
+
14
+ Sequelize.useCLS(namespace);
15
+
10
16
  // eslint-disable-next-line import/prefer-default-export
11
17
  export const sequelize = new Sequelize({
12
18
  dialect: 'sqlite',
package/api/third.d.ts CHANGED
@@ -10,6 +10,8 @@ declare module 'morgan';
10
10
 
11
11
  declare module 'flat';
12
12
 
13
+ declare module 'cls-hooked';
14
+
13
15
  namespace Express {
14
16
  interface Request {
15
17
  user?: {
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.73
17
+ version: 1.13.74
18
18
  logo: logo.png
19
19
  files:
20
20
  - dist
package/jest.config.js ADDED
@@ -0,0 +1,14 @@
1
+ /** @type {import('ts-jest').JestConfigWithTsJest} */
2
+ module.exports = {
3
+ verbose: true,
4
+ preset: 'ts-jest',
5
+ testEnvironment: 'node',
6
+ collectCoverage: true,
7
+ coverageDirectory: 'coverage',
8
+ clearMocks: true,
9
+ transform: {
10
+ '^.+\\.ts?$': 'ts-jest',
11
+ },
12
+ testMatch: ['**/tests/**/*.spec.ts'],
13
+ collectCoverageFrom: ['api/src/**/*.ts'],
14
+ };
package/package.json CHANGED
@@ -1,12 +1,14 @@
1
1
  {
2
2
  "name": "payment-kit",
3
- "version": "1.13.73",
3
+ "version": "1.13.74",
4
4
  "scripts": {
5
5
  "dev": "COMPONENT_STORE_URL=https://test.store.blocklet.dev blocklet dev",
6
6
  "eject": "vite eject",
7
7
  "lint": "tsc --noEmit && eslint src api/src --ext .mjs,.js,.jsx,.ts,.tsx",
8
8
  "lint:fix": "npm run lint -- --fix",
9
9
  "format": "prettier -w src",
10
+ "test": "jest",
11
+ "coverage": "npm run test -- --coverage",
10
12
  "start": "cross-env NODE_ENV=development nodemon api/dev.ts -w api",
11
13
  "clean": "node scripts/build-clean.js",
12
14
  "bundle": "tsc --noEmit && npm run bundle:client && npm run bundle:api",
@@ -40,32 +42,33 @@
40
42
  ]
41
43
  },
42
44
  "dependencies": {
43
- "@abtnode/cron": "^1.16.19",
44
- "@arcblock/did": "^1.18.103",
45
+ "@abtnode/cron": "^1.16.20",
46
+ "@arcblock/did": "^1.18.106",
45
47
  "@arcblock/did-auth-storage-nedb": "^1.7.1",
46
- "@arcblock/did-connect": "^2.8.20",
47
- "@arcblock/did-util": "^1.18.103",
48
- "@arcblock/jwt": "^1.18.103",
49
- "@arcblock/ux": "^2.8.20",
50
- "@blocklet/logger": "^1.16.19",
51
- "@blocklet/sdk": "^1.16.19",
52
- "@blocklet/ui-react": "^2.8.20",
53
- "@blocklet/uploader": "^0.0.38",
48
+ "@arcblock/did-connect": "^2.8.23",
49
+ "@arcblock/did-util": "^1.18.106",
50
+ "@arcblock/jwt": "^1.18.106",
51
+ "@arcblock/ux": "^2.8.23",
52
+ "@blocklet/logger": "^1.16.20",
53
+ "@blocklet/sdk": "^1.16.20",
54
+ "@blocklet/ui-react": "^2.8.23",
55
+ "@blocklet/uploader": "^0.0.55",
54
56
  "@mui/icons-material": "^5.14.19",
55
57
  "@mui/lab": "^5.0.0-alpha.155",
56
58
  "@mui/material": "^5.14.20",
57
59
  "@mui/styles": "^5.14.20",
58
60
  "@mui/system": "^5.14.20",
59
- "@ocap/asset": "^1.18.103",
60
- "@ocap/client": "^1.18.103",
61
- "@ocap/mcrypto": "^1.18.103",
62
- "@ocap/util": "^1.18.103",
63
- "@ocap/wallet": "^1.18.103",
61
+ "@ocap/asset": "^1.18.106",
62
+ "@ocap/client": "^1.18.106",
63
+ "@ocap/mcrypto": "^1.18.106",
64
+ "@ocap/util": "^1.18.106",
65
+ "@ocap/wallet": "^1.18.106",
64
66
  "@stripe/react-stripe-js": "^2.4.0",
65
67
  "@stripe/stripe-js": "^2.2.0",
66
68
  "ahooks": "^3.7.8",
67
69
  "axios": "^0.27.2",
68
70
  "body-parser": "^1.20.2",
71
+ "cls-hooked": "^4.2.2",
69
72
  "cookie-parser": "^1.4.6",
70
73
  "copy-to-clipboard": "^3.3.3",
71
74
  "cors": "^2.8.5",
@@ -103,10 +106,10 @@
103
106
  "validator": "^13.11.0"
104
107
  },
105
108
  "devDependencies": {
106
- "@abtnode/types": "^1.16.19",
109
+ "@abtnode/types": "^1.16.20",
107
110
  "@arcblock/eslint-config": "^0.2.4",
108
111
  "@arcblock/eslint-config-ts": "^0.2.4",
109
- "@did-pay/types": "1.13.73",
112
+ "@did-pay/types": "1.13.74",
110
113
  "@types/cookie-parser": "^1.4.6",
111
114
  "@types/cors": "^2.8.17",
112
115
  "@types/dotenv-flow": "^3.3.3",
@@ -119,11 +122,13 @@
119
122
  "cross-env": "^7.0.3",
120
123
  "eslint": "^8.55.0",
121
124
  "import-sort-style-module": "^6.0.0",
125
+ "jest": "^29.7.0",
122
126
  "lint-staged": "^12.5.0",
123
127
  "nodemon": "^2.0.22",
124
128
  "npm-run-all": "^4.1.5",
125
129
  "prettier": "^2.8.8",
126
130
  "prettier-plugin-import-sort": "^0.0.7",
131
+ "ts-jest": "^29.1.1",
127
132
  "ts-node": "^10.9.1",
128
133
  "type-fest": "^4.8.3",
129
134
  "typescript": "^4.9.5",
@@ -143,5 +148,5 @@
143
148
  "parser": "typescript"
144
149
  }
145
150
  },
146
- "gitHead": "544382d01feaed289ddaf2066b701a6ecf253370"
151
+ "gitHead": "7b0f74ea85cd7e16334a3dbed6e0bd66f359066e"
147
152
  }
@@ -4,19 +4,19 @@ import { Link, Stack, Typography } from '@mui/material';
4
4
  import { joinURL } from 'ufo';
5
5
 
6
6
  const getTxLink = (method: TPaymentMethod, details: PaymentDetails) => {
7
- if (method.type === 'arcblock') {
7
+ if (method.type === 'arcblock' && details.arcblock?.tx_hash) {
8
8
  return {
9
9
  link: joinURL(method.settings.arcblock?.explorer_host as string, '/txs', details.arcblock?.tx_hash as string),
10
10
  text: details.arcblock?.tx_hash as string,
11
11
  };
12
12
  }
13
- if (method.type === 'bitcoin') {
13
+ if (method.type === 'bitcoin' && details.bitcoin?.tx_hash) {
14
14
  return {
15
15
  link: joinURL(method.settings.bitcoin?.explorer_host as string, '/tx', details.bitcoin?.tx_hash as string),
16
16
  text: details.bitcoin?.tx_hash as string,
17
17
  };
18
18
  }
19
- if (method.type === 'ethereum') {
19
+ if (method.type === 'ethereum' && details.ethereum?.tx_hash) {
20
20
  return {
21
21
  link: joinURL(method.settings.ethereum?.explorer_host as string, '/tx', details.ethereum?.tx_hash as string),
22
22
  text: details.ethereum?.tx_hash as string,
@@ -55,14 +55,23 @@ export default function TxLink(props: {
55
55
  }
56
56
 
57
57
  const { text, link } = getTxLink(props.method, props.details);
58
+
59
+ if (link) {
60
+ return (
61
+ <Link href={link} target="_blank" rel="noopener noreferrer">
62
+ <Stack component="span" direction="row" alignItems="center" spacing={1}>
63
+ <Typography component="span" color="primary">
64
+ {text.length > 40 ? [text.slice(0, 8), text.slice(-8)].join('...') : text}
65
+ </Typography>
66
+ <OpenInNewOutlined fontSize="small" />
67
+ </Stack>
68
+ </Link>
69
+ );
70
+ }
71
+
58
72
  return (
59
- <Link href={link} target="_blank" rel="noopener noreferrer">
60
- <Stack component="span" direction="row" alignItems="center" spacing={1}>
61
- <Typography component="span" color="primary">
62
- {text.length > 40 ? [text.slice(0, 8), text.slice(-8)].join('...') : text}
63
- </Typography>
64
- <OpenInNewOutlined fontSize="small" />
65
- </Stack>
66
- </Link>
73
+ <Typography component="small" color="text.secondary">
74
+ None
75
+ </Typography>
67
76
  );
68
77
  }
@@ -204,7 +204,7 @@ export default function PaymentForm({
204
204
 
205
205
  if (['arcblock', 'ethereum'].includes(method.type)) {
206
206
  setState({ paying: true });
207
- if (result.data.delegation.sufficient) {
207
+ if (result.data.balance?.sufficient || result.data.delegation?.sufficient) {
208
208
  await handleConnected();
209
209
  } else {
210
210
  connectApi.open({
@@ -1,6 +1,8 @@
1
+ import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
1
2
  import type { TInvoiceExpanded } from '@did-pay/types';
2
3
  import { Table, TableBody, TableCell, TableHead, TableRow, Typography } from '@mui/material';
3
4
  import { styled } from '@mui/system';
5
+ import { toBN } from '@ocap/util';
4
6
 
5
7
  import { formatAmount, formatToDate, getPriceUintAmountByCurrency } from '../../libs/util';
6
8
  import LineItemActions from '../subscription/items/actions';
@@ -14,20 +16,44 @@ InvoiceTable.defaultProps = {
14
16
  simple: false,
15
17
  };
16
18
 
19
+ export function getAppliedBalance(invoice: TInvoiceExpanded) {
20
+ if (invoice.paymentMethod.type === 'stripe') {
21
+ const starting = toBN(invoice.starting_balance);
22
+ const ending = toBN(invoice.ending_balance);
23
+ return ending.sub(starting).toString();
24
+ }
25
+
26
+ if (
27
+ invoice.starting_token_balance &&
28
+ invoice.starting_token_balance[invoice.paymentCurrency.id] &&
29
+ invoice.ending_token_balance &&
30
+ invoice.ending_token_balance[invoice.paymentCurrency.id]
31
+ ) {
32
+ const starting = toBN(invoice.starting_token_balance[invoice.paymentCurrency.id]);
33
+ const ending = toBN(invoice.ending_token_balance[invoice.paymentCurrency.id]);
34
+ return ending.sub(starting).toString();
35
+ }
36
+
37
+ return '0';
38
+ }
39
+
17
40
  export default function InvoiceTable({ invoice, simple }: Props) {
41
+ const { t } = useLocaleContext();
42
+ const appliedBalance = getAppliedBalance(invoice);
43
+
18
44
  return (
19
45
  <StyledTable>
20
46
  <TableHead>
21
47
  <TableRow sx={{ borderBottom: '1px solid #eee' }}>
22
48
  <TableCell sx={{ textTransform: 'none', fontWeight: 'normal' }}>Description</TableCell>
23
49
  <TableCell sx={{ textTransform: 'none', fontWeight: 'normal', width: 80 }} align="right">
24
- Quantity
50
+ {t('common.quantity')}
25
51
  </TableCell>
26
52
  <TableCell sx={{ textTransform: 'none', fontWeight: 'normal', width: 120 }} align="right">
27
- Unit Price
53
+ {t('customer.invoice.unitPrice')}
28
54
  </TableCell>
29
55
  <TableCell sx={{ textTransform: 'none', fontWeight: 'normal', width: 100 }} align="right">
30
- Amount
56
+ {t('common.amount')}
31
57
  </TableCell>
32
58
  {!simple && (
33
59
  <TableCell sx={{ textTransform: 'none', fontWeight: 'normal', width: 50 }} align="right">
@@ -54,10 +80,9 @@ export default function InvoiceTable({ invoice, simple }: Props) {
54
80
  </TableCell>
55
81
  <TableCell align="right">{line.quantity}</TableCell>
56
82
  <TableCell align="right">
57
- {formatAmount(
58
- getPriceUintAmountByCurrency(line.price, invoice.paymentCurrency),
59
- invoice.paymentCurrency.decimal
60
- )}
83
+ {!line.proration
84
+ ? formatAmount(getPriceUintAmountByCurrency(line.price, invoice.paymentCurrency), invoice.paymentCurrency.decimal) // prettier-ignore
85
+ : ''}
61
86
  </TableCell>
62
87
  <TableCell align="right">{formatAmount(line.amount, invoice.paymentCurrency.decimal)}</TableCell>
63
88
  {!simple && (
@@ -69,7 +94,7 @@ export default function InvoiceTable({ invoice, simple }: Props) {
69
94
  ))}
70
95
  <TableRow>
71
96
  <TableCell colSpan={3} align="right" sx={{ fontWeight: 600 }}>
72
- Subtotal
97
+ {t('common.subtotal')}
73
98
  </TableCell>
74
99
  <TableCell align="right" sx={{ fontWeight: 600 }}>
75
100
  {formatAmount(invoice.subtotal, invoice.paymentCurrency.decimal)}
@@ -78,25 +103,39 @@ export default function InvoiceTable({ invoice, simple }: Props) {
78
103
  </TableRow>
79
104
  <TableRow sx={{ borderBottom: '1px solid #eee' }}>
80
105
  <TableCell colSpan={3} align="right" sx={{ fontWeight: 600 }}>
81
- Total
106
+ {t('common.total')}
82
107
  </TableCell>
83
108
  <TableCell align="right" sx={{ fontWeight: 600 }}>
84
109
  {formatAmount(invoice.total, invoice.paymentCurrency.decimal)}
85
110
  </TableCell>
86
111
  <TableCell>&nbsp;</TableCell>
87
112
  </TableRow>
88
- <TableRow>
89
- <TableCell colSpan={3} align="right" sx={{ fontWeight: 600, color: 'text.secondary' }}>
90
- Amount Paid
91
- </TableCell>
92
- <TableCell align="right" sx={{ fontWeight: 600 }}>
93
- -{formatAmount(invoice.amount_paid, invoice.paymentCurrency.decimal)}
94
- </TableCell>
95
- <TableCell>&nbsp;</TableCell>
96
- </TableRow>
113
+ {invoice.amount_paid !== '0' && (
114
+ <TableRow>
115
+ <TableCell colSpan={3} align="right" sx={{ fontWeight: 600, color: 'text.secondary' }}>
116
+ {t('customer.invoice.amountPaid')}
117
+ </TableCell>
118
+ <TableCell align="right" sx={{ fontWeight: 600 }}>
119
+ {invoice.amount_paid === '0' ? '' : '-'}
120
+ {formatAmount(invoice.amount_paid, invoice.paymentCurrency.decimal)}
121
+ </TableCell>
122
+ <TableCell>&nbsp;</TableCell>
123
+ </TableRow>
124
+ )}
125
+ {appliedBalance !== '0' && (
126
+ <TableRow>
127
+ <TableCell colSpan={3} align="right" sx={{ fontWeight: 600, color: 'text.secondary' }}>
128
+ {t('customer.invoice.amountApplied')}
129
+ </TableCell>
130
+ <TableCell align="right" sx={{ fontWeight: 600 }}>
131
+ {formatAmount(appliedBalance, invoice.paymentCurrency.decimal)}
132
+ </TableCell>
133
+ <TableCell>&nbsp;</TableCell>
134
+ </TableRow>
135
+ )}
97
136
  <TableRow>
98
137
  <TableCell colSpan={3} align="right" sx={{ fontWeight: 600 }}>
99
- Amount Due
138
+ {t('customer.invoice.amountDue')}
100
139
  </TableCell>
101
140
  <TableCell align="right" sx={{ fontWeight: 600 }}>
102
141
  {formatAmount(invoice.amount_remaining, invoice.paymentCurrency.decimal)}