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.
- package/api/src/queues/subscription.ts +81 -3
- package/api/src/routes/invoices.ts +9 -0
- package/api/src/routes/subscriptions.ts +6 -60
- package/api/src/store/models/subscription.ts +1 -0
- package/blocklet.yml +1 -1
- package/package.json +39 -39
- package/src/components/subscription/portal/actions.tsx +5 -1
|
@@ -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 {
|
|
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
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "payment-kit",
|
|
3
|
-
"version": "1.14.
|
|
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.
|
|
46
|
+
"@arcblock/did": "^1.18.127",
|
|
47
47
|
"@arcblock/did-auth-storage-nedb": "^1.7.1",
|
|
48
|
-
"@arcblock/did-connect": "^2.10.
|
|
49
|
-
"@arcblock/did-util": "^1.18.
|
|
50
|
-
"@arcblock/jwt": "^1.18.
|
|
51
|
-
"@arcblock/ux": "^2.10.
|
|
52
|
-
"@arcblock/validator": "^1.18.
|
|
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.
|
|
55
|
+
"@blocklet/payment-react": "1.14.9",
|
|
56
56
|
"@blocklet/sdk": "1.16.28",
|
|
57
|
-
"@blocklet/ui-react": "^2.10.
|
|
57
|
+
"@blocklet/ui-react": "^2.10.11",
|
|
58
58
|
"@blocklet/uploader": "^0.1.20",
|
|
59
|
-
"@mui/icons-material": "^5.
|
|
60
|
-
"@mui/lab": "^5.0.0-alpha.
|
|
61
|
-
"@mui/material": "^5.
|
|
62
|
-
"@mui/styles": "^5.
|
|
63
|
-
"@mui/system": "^5.
|
|
64
|
-
"@ocap/asset": "^1.18.
|
|
65
|
-
"@ocap/client": "^1.18.
|
|
66
|
-
"@ocap/mcrypto": "^1.18.
|
|
67
|
-
"@ocap/util": "^1.18.
|
|
68
|
-
"@ocap/wallet": "^1.18.
|
|
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.
|
|
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.
|
|
81
|
-
"debug": "^4.3.
|
|
80
|
+
"dayjs": "^1.11.12",
|
|
81
|
+
"debug": "^4.3.6",
|
|
82
82
|
"dotenv-flow": "^3.3.0",
|
|
83
|
-
"ethers": "^6.13.
|
|
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.
|
|
90
|
-
"iframe-resizer-react": "^1.1.
|
|
91
|
-
"joi": "^17.13.
|
|
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.
|
|
103
|
+
"react-hook-form": "^7.52.1",
|
|
104
104
|
"react-international-phone": "^3.1.2",
|
|
105
|
-
"react-router-dom": "^6.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
139
|
+
"prettier": "^3.3.3",
|
|
140
140
|
"prettier-plugin-import-sort": "^0.0.7",
|
|
141
|
-
"ts-jest": "^29.
|
|
141
|
+
"ts-jest": "^29.2.3",
|
|
142
142
|
"ts-node": "^10.9.2",
|
|
143
|
-
"type-fest": "^4.
|
|
143
|
+
"type-fest": "^4.23.0",
|
|
144
144
|
"typescript": "^4.9.5",
|
|
145
|
-
"vite": "^5.
|
|
146
|
-
"vite-node": "^2.0.
|
|
147
|
-
"vite-plugin-blocklet": "^0.8.
|
|
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": "
|
|
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`, {
|
|
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'));
|