payment-kit 1.13.79 → 1.13.80
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/crons/subscription-trail-will-end.ts +1 -0
- package/api/src/crons/subscription-will-renew.ts +1 -0
- package/api/src/libs/notification/template/subscription-renewed.ts +3 -0
- package/api/src/libs/notification/template/subscription-succeeded.ts +3 -0
- package/api/src/libs/notification/template/subscription-trial-start.ts +3 -0
- package/api/src/libs/notification/template/subscription-trial-will-end.ts +3 -0
- package/api/src/libs/notification/template/subscription-will-renew.ts +3 -0
- package/api/src/libs/payment.ts +0 -5
- package/api/src/queues/event.ts +1 -1
- package/api/src/queues/invoice.ts +1 -1
- package/api/src/queues/payment.ts +35 -4
- package/api/src/queues/subscription.ts +7 -2
- package/api/src/queues/webhook.ts +2 -2
- package/blocklet.yml +1 -1
- package/package.json +3 -3
- package/src/pages/admin/billing/subscriptions/detail.tsx +16 -2
- package/src/pages/customer/subscription/index.tsx +7 -0
|
@@ -56,6 +56,9 @@ export class SubscriptionRenewedEmailTemplate implements BaseEmailTemplate<Subsc
|
|
|
56
56
|
if (!subscription) {
|
|
57
57
|
throw new Error(`Subscription not found: ${this.options.subscriptionId}`);
|
|
58
58
|
}
|
|
59
|
+
if (subscription.status !== 'active') {
|
|
60
|
+
throw new Error(`Subscription not active: ${this.options.subscriptionId}`);
|
|
61
|
+
}
|
|
59
62
|
|
|
60
63
|
const customer = await Customer.findByPk(subscription.customer_id);
|
|
61
64
|
if (!customer) {
|
|
@@ -57,6 +57,9 @@ export class SubscriptionSucceededEmailTemplate
|
|
|
57
57
|
if (!subscription) {
|
|
58
58
|
throw new Error(`Subscription not found: ${this.options.subscriptionId}`);
|
|
59
59
|
}
|
|
60
|
+
if (subscription.status !== 'active') {
|
|
61
|
+
throw new Error(`Subscription not active: ${this.options.subscriptionId}`);
|
|
62
|
+
}
|
|
60
63
|
|
|
61
64
|
const customer = await Customer.findByPk(subscription.customer_id);
|
|
62
65
|
if (!customer) {
|
|
@@ -49,6 +49,9 @@ export class SubscriptionTrailStartEmailTemplate
|
|
|
49
49
|
if (!subscription) {
|
|
50
50
|
throw new Error(`Subscription not found: ${this.options.subscriptionId}`);
|
|
51
51
|
}
|
|
52
|
+
if (subscription.status !== 'trialing') {
|
|
53
|
+
throw new Error(`Subscription not trialing: ${this.options.subscriptionId}`);
|
|
54
|
+
}
|
|
52
55
|
|
|
53
56
|
const customer = await Customer.findByPk(subscription.customer_id);
|
|
54
57
|
if (!customer) {
|
|
@@ -51,6 +51,9 @@ export class SubscriptionTrailWilEndEmailTemplate
|
|
|
51
51
|
if (!subscription) {
|
|
52
52
|
throw new Error(`Subscription not found: ${this.options.subscriptionId}`);
|
|
53
53
|
}
|
|
54
|
+
if (subscription.status !== 'trialing') {
|
|
55
|
+
throw new Error(`Subscription not trialing: ${this.options.subscriptionId}`);
|
|
56
|
+
}
|
|
54
57
|
|
|
55
58
|
const customer = await Customer.findByPk(subscription.customer_id);
|
|
56
59
|
if (!customer) {
|
|
@@ -51,6 +51,9 @@ export class SubscriptionWillRenewEmailTemplate
|
|
|
51
51
|
if (!subscription) {
|
|
52
52
|
throw new Error(`Subscription not found: ${this.options.subscriptionId}`);
|
|
53
53
|
}
|
|
54
|
+
if (subscription.status !== 'active') {
|
|
55
|
+
throw new Error(`Subscription not active: ${this.options.subscriptionId}`);
|
|
56
|
+
}
|
|
54
57
|
|
|
55
58
|
const customer = await Customer.findByPk(subscription.customer_id);
|
|
56
59
|
if (!customer) {
|
package/api/src/libs/payment.ts
CHANGED
|
@@ -19,7 +19,6 @@ import {
|
|
|
19
19
|
} from '../store/models';
|
|
20
20
|
import type { TPaymentCurrency } from '../store/models/payment-currency';
|
|
21
21
|
import { blocklet, wallet } from './auth';
|
|
22
|
-
import logger from './logger';
|
|
23
22
|
import { OCAP_PAYMENT_TX_TYPE } from './util';
|
|
24
23
|
|
|
25
24
|
export interface SufficientForPaymentResult {
|
|
@@ -162,19 +161,16 @@ export async function getPaymentDetail(userDid: string, invoice: Invoice): Promi
|
|
|
162
161
|
|
|
163
162
|
const paymentIntent = await PaymentIntent.findByPk(invoice.payment_intent_id);
|
|
164
163
|
if (!paymentIntent) {
|
|
165
|
-
logger.error('getPayment.error', { reason: 'NO_PAYMENT_INTENT' });
|
|
166
164
|
return defaultResult;
|
|
167
165
|
}
|
|
168
166
|
|
|
169
167
|
const paymentMethod = await PaymentMethod.findByPk(paymentIntent?.payment_method_id);
|
|
170
168
|
if (!paymentMethod) {
|
|
171
|
-
logger.error('getPayment.error', { reason: 'NO_PAYMENT_METHOD' });
|
|
172
169
|
return defaultResult;
|
|
173
170
|
}
|
|
174
171
|
|
|
175
172
|
const paymentCurrency = await PaymentCurrency.findByPk(paymentIntent.currency_id);
|
|
176
173
|
if (!paymentCurrency) {
|
|
177
|
-
logger.error('getPayment.error', { reason: 'NO_PAYMENT_CURRENCY' });
|
|
178
174
|
return defaultResult;
|
|
179
175
|
}
|
|
180
176
|
|
|
@@ -189,7 +185,6 @@ export async function getPaymentDetail(userDid: string, invoice: Invoice): Promi
|
|
|
189
185
|
|
|
190
186
|
// Do not have enough permission
|
|
191
187
|
if (!result.token) {
|
|
192
|
-
logger.error('getPayment.error', { reason: result.reason });
|
|
193
188
|
return defaultResult;
|
|
194
189
|
}
|
|
195
190
|
|
package/api/src/queues/event.ts
CHANGED
|
@@ -103,7 +103,12 @@ type Updates = {
|
|
|
103
103
|
};
|
|
104
104
|
};
|
|
105
105
|
|
|
106
|
-
export const handlePaymentFailed = async (
|
|
106
|
+
export const handlePaymentFailed = async (
|
|
107
|
+
paymentIntent: PaymentIntent,
|
|
108
|
+
invoice: Invoice,
|
|
109
|
+
error: PaymentError,
|
|
110
|
+
checkSubscriptionStatus = true
|
|
111
|
+
) => {
|
|
107
112
|
const now = dayjs().unix();
|
|
108
113
|
const attemptCount = invoice.attempt_count + 1;
|
|
109
114
|
|
|
@@ -135,7 +140,7 @@ export const handlePaymentFailed = async (paymentIntent: PaymentIntent, invoice:
|
|
|
135
140
|
return updates.retry;
|
|
136
141
|
}
|
|
137
142
|
|
|
138
|
-
if (subscription.status !== 'active') {
|
|
143
|
+
if (checkSubscriptionStatus && subscription.status !== 'active') {
|
|
139
144
|
return updates.retry;
|
|
140
145
|
}
|
|
141
146
|
|
|
@@ -249,6 +254,29 @@ export const handlePayment = async (job: PaymentJob) => {
|
|
|
249
254
|
}
|
|
250
255
|
|
|
251
256
|
const invoice = await Invoice.findByPk(paymentIntent.invoice_id);
|
|
257
|
+
|
|
258
|
+
// check max retry before doing any hard work
|
|
259
|
+
if (invoice && invoice.attempt_count > MAX_RETRY_COUNT) {
|
|
260
|
+
logger.info(`PaymentIntent capture aborted since max retry exceeded: ${paymentIntent.id}`);
|
|
261
|
+
const updates = await handlePaymentFailed(
|
|
262
|
+
paymentIntent,
|
|
263
|
+
invoice,
|
|
264
|
+
{
|
|
265
|
+
type: 'card_error',
|
|
266
|
+
code: 'max_retry_exceeded',
|
|
267
|
+
message: 'max_retry_exceeded',
|
|
268
|
+
},
|
|
269
|
+
false
|
|
270
|
+
);
|
|
271
|
+
await paymentIntent.update(updates.payment);
|
|
272
|
+
await invoice.update(updates.invoice);
|
|
273
|
+
if (!updates.invoice.next_payment_attempt) {
|
|
274
|
+
logger.info(`PaymentIntent job deleted since max retry exceeded: ${paymentIntent.id}`);
|
|
275
|
+
await paymentQueue.delete(paymentIntent.id);
|
|
276
|
+
}
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
|
|
252
280
|
const paymentSettings = invoice?.payment_settings || job.paymentSettings;
|
|
253
281
|
if (!paymentSettings) {
|
|
254
282
|
await paymentIntent.update({ status: 'requires_action' });
|
|
@@ -334,7 +362,7 @@ export const handlePayment = async (job: PaymentJob) => {
|
|
|
334
362
|
// @ts-ignore
|
|
335
363
|
const txHash = await client.sendTransferV2Tx({ tx: signed, wallet, delegator: payer }, getGasPayerExtra(buffer));
|
|
336
364
|
|
|
337
|
-
logger.info(
|
|
365
|
+
logger.info('PaymentIntent capture done', { id: paymentIntent.id, txHash });
|
|
338
366
|
|
|
339
367
|
await paymentIntent.update({
|
|
340
368
|
status: 'succeeded',
|
|
@@ -396,6 +424,9 @@ export const handlePayment = async (job: PaymentJob) => {
|
|
|
396
424
|
runAt: retryAt,
|
|
397
425
|
});
|
|
398
426
|
logger.error('PaymentIntent capture retry scheduled', { id: paymentIntent.id, retryAt });
|
|
427
|
+
} else {
|
|
428
|
+
logger.info('PaymentIntent job deleted since no retry', { id: paymentIntent.id });
|
|
429
|
+
paymentQueue.delete(paymentIntent.id);
|
|
399
430
|
}
|
|
400
431
|
}
|
|
401
432
|
}
|
|
@@ -406,7 +437,7 @@ export const paymentQueue = createQueue<PaymentJob>({
|
|
|
406
437
|
onJob: handlePayment,
|
|
407
438
|
options: {
|
|
408
439
|
concurrency: 1,
|
|
409
|
-
maxRetries:
|
|
440
|
+
maxRetries: 0,
|
|
410
441
|
enableScheduledJob: true,
|
|
411
442
|
},
|
|
412
443
|
});
|
|
@@ -81,6 +81,11 @@ export const handleSubscription = async (job: SubscriptionJob) => {
|
|
|
81
81
|
return;
|
|
82
82
|
}
|
|
83
83
|
|
|
84
|
+
if (subscription.isActive() === false) {
|
|
85
|
+
logger.warn(`Subscription not active: ${job.subscriptionId}, so new invoice is skipped`);
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
|
|
84
89
|
// Do we still have the customer
|
|
85
90
|
const customer = await Customer.findByPk(subscription.customer_id);
|
|
86
91
|
if (!customer) {
|
|
@@ -215,8 +220,8 @@ export const subscriptionQueue = createQueue<SubscriptionJob>({
|
|
|
215
220
|
name: 'subscription',
|
|
216
221
|
onJob: handleSubscription,
|
|
217
222
|
options: {
|
|
218
|
-
concurrency:
|
|
219
|
-
maxRetries:
|
|
223
|
+
concurrency: 5,
|
|
224
|
+
maxRetries: 3,
|
|
220
225
|
enableScheduledJob: true,
|
|
221
226
|
},
|
|
222
227
|
});
|
package/blocklet.yml
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "payment-kit",
|
|
3
|
-
"version": "1.13.
|
|
3
|
+
"version": "1.13.80",
|
|
4
4
|
"scripts": {
|
|
5
5
|
"dev": "COMPONENT_STORE_URL=https://test.store.blocklet.dev blocklet dev",
|
|
6
6
|
"eject": "vite eject",
|
|
@@ -109,7 +109,7 @@
|
|
|
109
109
|
"@abtnode/types": "^1.16.20",
|
|
110
110
|
"@arcblock/eslint-config": "^0.2.4",
|
|
111
111
|
"@arcblock/eslint-config-ts": "^0.2.4",
|
|
112
|
-
"@did-pay/types": "1.13.
|
|
112
|
+
"@did-pay/types": "1.13.80",
|
|
113
113
|
"@types/cookie-parser": "^1.4.6",
|
|
114
114
|
"@types/cors": "^2.8.17",
|
|
115
115
|
"@types/dotenv-flow": "^3.3.3",
|
|
@@ -148,5 +148,5 @@
|
|
|
148
148
|
"parser": "typescript"
|
|
149
149
|
}
|
|
150
150
|
},
|
|
151
|
-
"gitHead": "
|
|
151
|
+
"gitHead": "7ffaad28cb871681cac2b7472307380bee4b0c39"
|
|
152
152
|
}
|
|
@@ -111,20 +111,34 @@ export default function SubscriptionDetail(props: { id: string }) {
|
|
|
111
111
|
value={formatTime(data.start_date ? data.start_date * 1000 : data.created_at)}
|
|
112
112
|
divider
|
|
113
113
|
/>
|
|
114
|
-
{!data.cancel_at && (
|
|
114
|
+
{data.status === 'active' && !data.cancel_at && (
|
|
115
115
|
<InfoMetric
|
|
116
116
|
label={t('admin.subscription.nextInvoice')}
|
|
117
117
|
value={formatTime(data.current_period_end * 1000)}
|
|
118
118
|
divider
|
|
119
119
|
/>
|
|
120
120
|
)}
|
|
121
|
-
{data.cancel_at && (
|
|
121
|
+
{['active', 'trailing'].includes(data.status) && data.cancel_at && (
|
|
122
122
|
<InfoMetric
|
|
123
123
|
label={t('admin.subscription.cancel.schedule')}
|
|
124
124
|
value={formatTime(data.cancel_at * 1000)}
|
|
125
125
|
divider
|
|
126
126
|
/>
|
|
127
127
|
)}
|
|
128
|
+
{data.status !== 'canceled' && data.cancel_at_period_end && (
|
|
129
|
+
<InfoMetric
|
|
130
|
+
label={t('admin.subscription.cancel.schedule')}
|
|
131
|
+
value={formatTime(data.current_period_end * 1000)}
|
|
132
|
+
divider
|
|
133
|
+
/>
|
|
134
|
+
)}
|
|
135
|
+
{data.status === 'canceled' && data.canceled_at && (
|
|
136
|
+
<InfoMetric
|
|
137
|
+
label={t('admin.subscription.cancel.done')}
|
|
138
|
+
value={formatTime(data.canceled_at * 1000)}
|
|
139
|
+
divider
|
|
140
|
+
/>
|
|
141
|
+
)}
|
|
128
142
|
</Stack>
|
|
129
143
|
</Box>
|
|
130
144
|
</Box>
|
|
@@ -84,6 +84,13 @@ export default function CustomerSubscription() {
|
|
|
84
84
|
divider
|
|
85
85
|
/>
|
|
86
86
|
)}
|
|
87
|
+
{data.status !== 'canceled' && data.cancel_at_period_end && (
|
|
88
|
+
<InfoMetric
|
|
89
|
+
label={t('admin.subscription.cancel.schedule')}
|
|
90
|
+
value={formatTime(data.current_period_end * 1000)}
|
|
91
|
+
divider
|
|
92
|
+
/>
|
|
93
|
+
)}
|
|
87
94
|
{data.status === 'canceled' && data.canceled_at && (
|
|
88
95
|
<InfoMetric
|
|
89
96
|
label={t('admin.subscription.cancel.done')}
|