payment-kit 1.14.7 → 1.14.9

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.
@@ -12,9 +12,14 @@ import logger from '../libs/logger';
12
12
  import { getGasPayerExtra } from '../libs/payment';
13
13
  import createQueue from '../libs/queue';
14
14
  import { getStatementDescriptor } from '../libs/session';
15
- import { getSubscriptionCycleAmount, getSubscriptionCycleSetup, shouldCancelSubscription } from '../libs/subscription';
15
+ import {
16
+ getSubscriptionCycleAmount,
17
+ getSubscriptionCycleSetup,
18
+ getSubscriptionStakeReturnSetup,
19
+ shouldCancelSubscription,
20
+ } from '../libs/subscription';
16
21
  import { ensureInvoiceAndItems } from '../routes/connect/shared';
17
- import { PaymentCurrency, PaymentIntent, PaymentMethod, UsageRecord } from '../store/models';
22
+ import { PaymentCurrency, PaymentIntent, PaymentMethod, Refund, UsageRecord } from '../store/models';
18
23
  import { Customer } from '../store/models/customer';
19
24
  import { Invoice } from '../store/models/invoice';
20
25
  import { Price } from '../store/models/price';
@@ -422,6 +427,74 @@ const handleStakeSlashAfterCancel = async (subscription: Subscription) => {
422
427
  });
423
428
  };
424
429
 
430
+ const ensureReturnStake = async (subscription: Subscription) => {
431
+ const paymentMethod = await PaymentMethod.findByPk(subscription.default_payment_method_id);
432
+ if (paymentMethod?.type !== 'arcblock') {
433
+ logger.warn('Stake return skipped because payment method not arcblock', {
434
+ subscription: subscription.id,
435
+ paymentMethod: paymentMethod?.id,
436
+ });
437
+ return;
438
+ }
439
+ const address = subscription?.payment_details?.arcblock?.staking?.address;
440
+ if (!address) {
441
+ logger.warn('Stake return skipped because no staking address', {
442
+ subscription: subscription.id,
443
+ address,
444
+ });
445
+ return;
446
+ }
447
+
448
+ const refunds = await Refund.findAll({ where: { subscription_id: subscription.id, type: 'stake_return' } });
449
+ if (refunds.length > 0) {
450
+ logger.info(`Stake return skipped because subscription ${subscription.id} already has stake return records.`);
451
+ return;
452
+ }
453
+
454
+ const result = await getSubscriptionStakeReturnSetup(subscription, address, paymentMethod);
455
+
456
+ if (result.return_amount !== '0') {
457
+ // do the stake return
458
+ const item = await Refund.create({
459
+ type: 'stake_return',
460
+ livemode: subscription.livemode,
461
+ amount: result.return_amount,
462
+ description: 'stake_return_on_subscription_cancel',
463
+ status: 'pending',
464
+ reason: 'requested_by_admin',
465
+ currency_id: subscription.currency_id,
466
+ customer_id: subscription.customer_id,
467
+ payment_method_id: subscription.default_payment_method_id,
468
+ payment_intent_id: result?.lastInvoice?.payment_intent_id as string,
469
+ subscription_id: subscription.id,
470
+ attempt_count: 0,
471
+ attempted: false,
472
+ next_attempt: 0,
473
+ last_attempt_error: null,
474
+ starting_balance: '0',
475
+ ending_balance: '0',
476
+ starting_token_balance: {},
477
+ ending_token_balance: {},
478
+ payment_details: {
479
+ // @ts-ignore
480
+ arcblock: {
481
+ receiver: result.sender,
482
+ },
483
+ },
484
+ });
485
+ logger.info('Created stake return for canceled subscription', {
486
+ subscription: subscription.id,
487
+ return_amount: result.return_amount,
488
+ item: item.toJSON(),
489
+ });
490
+ } else {
491
+ logger.info('Skipped stake return for canceled subscription', {
492
+ subscription: subscription.id,
493
+ return_amount: result.return_amount,
494
+ });
495
+ }
496
+ };
497
+
425
498
  // generate invoice for subscription periodically
426
499
  export const handleSubscription = async (job: SubscriptionJob) => {
427
500
  logger.info('handle subscription', job);
@@ -572,8 +645,13 @@ events.on('customer.subscription.deleted', (subscription: Subscription) => {
572
645
  ensurePassportRevoked(subscription).catch((err) => {
573
646
  logger.error('ensurePassportRevoked failed', { error: err, subscription: subscription.id });
574
647
  });
575
-
576
648
  // FIXME: ensure invoices that are open or uncollectible are voided
649
+
650
+ if (subscription.cancelation_details?.return_stake) {
651
+ ensureReturnStake(subscription).catch((err) => {
652
+ logger.error('ensureReturnStake failed', { error: err, subscription: subscription.id });
653
+ });
654
+ }
577
655
  });
578
656
 
579
657
  events.on('customer.subscription.upgraded', async (subscription: Subscription) => {
@@ -139,6 +139,15 @@ router.get('/', authMine, async (req, res) => {
139
139
  throw new Error('Invalid currency');
140
140
  }
141
141
  stakeAmount = state.tokens.find((x: any) => x.address === currency?.contract)?.value;
142
+ // stakeAmount should not be zero if nonce exist
143
+ if (!Number(stakeAmount)) {
144
+ const refund = await Refund.findOne({
145
+ where: { subscription_id: subscription.id, status: 'succeeded', type: 'stake_return' },
146
+ });
147
+ if (refund) {
148
+ stakeAmount = refund.amount;
149
+ }
150
+ }
142
151
  }
143
152
  list.push({
144
153
  id: address as string,
@@ -233,12 +233,18 @@ router.put('/:id/cancel', authPortal, async (req, res) => {
233
233
  return res.status(400).json({ error: 'cancel at must be a future timestamp' });
234
234
  }
235
235
 
236
+ let canReturnStake = false;
237
+ const requestByAdmin = ['owner', 'admin'].includes(req.user?.role as string);
238
+ if ((requestByAdmin && staking === 'proration') || req.body?.cancel_from === 'customer') {
239
+ canReturnStake = true;
240
+ }
236
241
  // update cancel at
237
242
  const updates: Partial<Subscription> = {
238
243
  cancelation_details: {
239
244
  comment: comment || `Requested by ${req.user?.role}:${req.user?.did}`,
240
245
  reason: reason || 'payment_disputed',
241
246
  feedback: feedback || 'other',
247
+ return_stake: canReturnStake,
242
248
  },
243
249
  };
244
250
  const now = dayjs().unix() + 3;
@@ -349,66 +355,6 @@ router.put('/:id/cancel', authPortal, async (req, res) => {
349
355
  }
350
356
  }
351
357
 
352
- // trigger stake return
353
- if (staking === 'proration') {
354
- if (['owner', 'admin'].includes(req.user?.role as string) === false) {
355
- return res.status(403).json({ error: 'Not authorized to perform this action' });
356
- }
357
- const paymentMethod = await PaymentMethod.findByPk(subscription.default_payment_method_id);
358
- if (paymentMethod?.type !== 'arcblock') {
359
- return res
360
- .status(400)
361
- .json({ error: `Stake return not supported for subscription with payment method ${paymentMethod?.type}` });
362
- }
363
- const address = subscription?.payment_details?.arcblock?.staking?.address ?? undefined;
364
- if (!address) {
365
- return res.status(400).json({ error: 'Staking not found on subscription payment detail' });
366
- }
367
- const result = await getSubscriptionStakeReturnSetup(subscription, address, paymentMethod);
368
- if (result.return_amount !== '0') {
369
- // do the stake return
370
- const item = await Refund.create({
371
- type: 'stake_return',
372
- livemode: subscription.livemode,
373
- amount: result.return_amount,
374
- description: 'stake_return_on_subscription_cancel',
375
- status: 'pending',
376
- reason: 'requested_by_admin',
377
- currency_id: subscription.currency_id,
378
- customer_id: subscription.customer_id,
379
- payment_method_id: subscription.default_payment_method_id,
380
- payment_intent_id: result?.lastInvoice?.payment_intent_id as string,
381
- subscription_id: subscription.id,
382
- attempt_count: 0,
383
- attempted: false,
384
- next_attempt: 0,
385
- last_attempt_error: null,
386
- starting_balance: '0',
387
- ending_balance: '0',
388
- starting_token_balance: {},
389
- ending_token_balance: {},
390
- payment_details: {
391
- // @ts-ignore
392
- arcblock: {
393
- receiver: result.sender,
394
- },
395
- },
396
- });
397
- logger.info('subscription cancel stake return created', {
398
- ...req.params,
399
- ...req.body,
400
- ...pick(result, ['return_amount']),
401
- item: item.toJSON(),
402
- });
403
- } else {
404
- logger.info('subscription cancel stake return skipped', {
405
- ...req.params,
406
- ...req.body,
407
- ...pick(result, ['return_amount']),
408
- });
409
- }
410
- }
411
-
412
358
  return res.json(subscription);
413
359
  });
414
360
 
@@ -61,6 +61,7 @@ export class Subscription extends Model<InferAttributes<Subscription>, InferCrea
61
61
  string
62
62
  >;
63
63
  reason: LiteralUnion<'cancellation_requested' | 'payment_disputed' | 'payment_failed' | 'stake_revoked', string>;
64
+ return_stake?: boolean;
64
65
  };
65
66
 
66
67
  declare billing_cycle_anchor: 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.14.7
17
+ version: 1.14.9
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.14.7",
3
+ "version": "1.14.9",
4
4
  "scripts": {
5
5
  "dev": "blocklet dev --open",
6
6
  "eject": "vite eject",
@@ -43,31 +43,31 @@
43
43
  },
44
44
  "dependencies": {
45
45
  "@abtnode/cron": "1.16.28",
46
- "@arcblock/did": "^1.18.124",
46
+ "@arcblock/did": "^1.18.127",
47
47
  "@arcblock/did-auth-storage-nedb": "^1.7.1",
48
- "@arcblock/did-connect": "^2.10.3",
49
- "@arcblock/did-util": "^1.18.124",
50
- "@arcblock/jwt": "^1.18.124",
51
- "@arcblock/ux": "^2.10.3",
52
- "@arcblock/validator": "^1.18.124",
48
+ "@arcblock/did-connect": "^2.10.11",
49
+ "@arcblock/did-util": "^1.18.127",
50
+ "@arcblock/jwt": "^1.18.127",
51
+ "@arcblock/ux": "^2.10.11",
52
+ "@arcblock/validator": "^1.18.127",
53
53
  "@blocklet/js-sdk": "1.16.28",
54
54
  "@blocklet/logger": "1.16.28",
55
- "@blocklet/payment-react": "1.14.7",
55
+ "@blocklet/payment-react": "1.14.9",
56
56
  "@blocklet/sdk": "1.16.28",
57
- "@blocklet/ui-react": "^2.10.3",
57
+ "@blocklet/ui-react": "^2.10.11",
58
58
  "@blocklet/uploader": "^0.1.20",
59
- "@mui/icons-material": "^5.15.19",
60
- "@mui/lab": "^5.0.0-alpha.170",
61
- "@mui/material": "^5.15.19",
62
- "@mui/styles": "^5.15.19",
63
- "@mui/system": "^5.15.15",
64
- "@ocap/asset": "^1.18.126",
65
- "@ocap/client": "^1.18.126",
66
- "@ocap/mcrypto": "^1.18.126",
67
- "@ocap/util": "^1.18.126",
68
- "@ocap/wallet": "^1.18.126",
59
+ "@mui/icons-material": "^5.16.6",
60
+ "@mui/lab": "^5.0.0-alpha.173",
61
+ "@mui/material": "^5.16.6",
62
+ "@mui/styles": "^5.16.6",
63
+ "@mui/system": "^5.16.6",
64
+ "@ocap/asset": "^1.18.127",
65
+ "@ocap/client": "^1.18.127",
66
+ "@ocap/mcrypto": "^1.18.127",
67
+ "@ocap/util": "^1.18.127",
68
+ "@ocap/wallet": "^1.18.127",
69
69
  "@react-pdf/renderer": "^3.4.4",
70
- "@stripe/react-stripe-js": "^2.7.1",
70
+ "@stripe/react-stripe-js": "^2.7.3",
71
71
  "@stripe/stripe-js": "^2.4.0",
72
72
  "ahooks": "^3.8.0",
73
73
  "axios": "^1.7.2",
@@ -77,18 +77,18 @@
77
77
  "copy-to-clipboard": "^3.3.3",
78
78
  "cors": "^2.8.5",
79
79
  "date-fns": "^3.6.0",
80
- "dayjs": "^1.11.11",
81
- "debug": "^4.3.5",
80
+ "dayjs": "^1.11.12",
81
+ "debug": "^4.3.6",
82
82
  "dotenv-flow": "^3.3.0",
83
- "ethers": "^6.13.0",
83
+ "ethers": "^6.13.2",
84
84
  "express": "^4.19.2",
85
85
  "express-async-errors": "^3.1.1",
86
86
  "express-history-api-fallback": "^2.2.1",
87
87
  "fastq": "^1.17.1",
88
88
  "flat": "^5.0.2",
89
- "google-libphonenumber": "^3.2.34",
90
- "iframe-resizer-react": "^1.1.0",
91
- "joi": "^17.13.1",
89
+ "google-libphonenumber": "^3.2.38",
90
+ "iframe-resizer-react": "^1.1.1",
91
+ "joi": "^17.13.3",
92
92
  "json-stable-stringify": "^1.1.1",
93
93
  "lodash": "^4.17.21",
94
94
  "morgan": "^1.10.0",
@@ -100,9 +100,9 @@
100
100
  "react": "^18.3.1",
101
101
  "react-dom": "^18.3.1",
102
102
  "react-error-boundary": "^4.0.13",
103
- "react-hook-form": "^7.51.5",
103
+ "react-hook-form": "^7.52.1",
104
104
  "react-international-phone": "^3.1.2",
105
- "react-router-dom": "^6.23.1",
105
+ "react-router-dom": "^6.25.1",
106
106
  "recharts": "^2.12.7",
107
107
  "rimraf": "^3.0.2",
108
108
  "sequelize": "^6.37.3",
@@ -110,7 +110,7 @@
110
110
  "sqlite3": "^5.1.7",
111
111
  "stripe": "^13.11.0",
112
112
  "typewriter-effect": "^2.21.0",
113
- "ufo": "^1.5.3",
113
+ "ufo": "^1.5.4",
114
114
  "umzug": "^3.8.1",
115
115
  "use-bus": "^2.5.2",
116
116
  "validator": "^13.12.0"
@@ -118,16 +118,16 @@
118
118
  "devDependencies": {
119
119
  "@abtnode/types": "1.16.28",
120
120
  "@arcblock/eslint-config-ts": "^0.3.2",
121
- "@blocklet/payment-types": "1.14.7",
121
+ "@blocklet/payment-types": "1.14.9",
122
122
  "@types/cookie-parser": "^1.4.7",
123
123
  "@types/cors": "^2.8.17",
124
124
  "@types/debug": "^4.1.12",
125
125
  "@types/dotenv-flow": "^3.3.3",
126
126
  "@types/express": "^4.17.21",
127
- "@types/node": "^18.19.34",
127
+ "@types/node": "^18.19.42",
128
128
  "@types/react": "^18.3.3",
129
129
  "@types/react-dom": "^18.3.0",
130
- "@vitejs/plugin-react": "^4.3.0",
130
+ "@vitejs/plugin-react": "^4.3.1",
131
131
  "bumpp": "^8.2.1",
132
132
  "cross-env": "^7.0.3",
133
133
  "eslint": "^8.57.0",
@@ -136,15 +136,15 @@
136
136
  "lint-staged": "^12.5.0",
137
137
  "nodemon": "^2.0.22",
138
138
  "npm-run-all": "^4.1.5",
139
- "prettier": "^3.3.2",
139
+ "prettier": "^3.3.3",
140
140
  "prettier-plugin-import-sort": "^0.0.7",
141
- "ts-jest": "^29.1.4",
141
+ "ts-jest": "^29.2.3",
142
142
  "ts-node": "^10.9.2",
143
- "type-fest": "^4.19.0",
143
+ "type-fest": "^4.23.0",
144
144
  "typescript": "^4.9.5",
145
- "vite": "^5.2.12",
146
- "vite-node": "^2.0.1",
147
- "vite-plugin-blocklet": "^0.8.7",
145
+ "vite": "^5.3.5",
146
+ "vite-node": "^2.0.4",
147
+ "vite-plugin-blocklet": "^0.8.11",
148
148
  "vite-plugin-node-polyfills": "^0.21.0",
149
149
  "vite-plugin-svgr": "^4.2.0",
150
150
  "vite-tsconfig-paths": "^4.3.2",
@@ -160,5 +160,5 @@
160
160
  "parser": "typescript"
161
161
  }
162
162
  },
163
- "gitHead": "683097ae01a97a7b303194d68cdabbae24201c14"
163
+ "gitHead": "7157281ad8a7b96a365c5a0b235b982290b0201e"
164
164
  }
@@ -73,7 +73,11 @@ export function SubscriptionActionsInner({ subscription, showExtra, onChange }:
73
73
  try {
74
74
  setState({ loading: true });
75
75
  const sub = await api
76
- .put(`/api/subscriptions/${state.subscription}/cancel`, { at: 'current_period_end', ...getValues().cancel })
76
+ .put(`/api/subscriptions/${state.subscription}/cancel`, {
77
+ at: 'current_period_end',
78
+ ...getValues().cancel,
79
+ cancel_from: 'customer',
80
+ })
77
81
  .then((res) => res.data);
78
82
  setSubscription(sub);
79
83
  Toast.success(t('common.saved'));