payment-kit 1.16.17 → 1.16.19
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/index.ts +1 -1
- package/api/src/hooks/pre-start.ts +2 -0
- package/api/src/index.ts +2 -0
- package/api/src/integrations/arcblock/stake.ts +7 -1
- package/api/src/integrations/stripe/resource.ts +1 -1
- package/api/src/libs/env.ts +12 -0
- package/api/src/libs/event.ts +8 -0
- package/api/src/libs/invoice.ts +585 -3
- package/api/src/libs/notification/template/subscription-succeeded.ts +1 -2
- package/api/src/libs/notification/template/subscription-trial-will-end.ts +2 -2
- package/api/src/libs/notification/template/subscription-will-renew.ts +6 -2
- package/api/src/libs/notification/template/subscription.overdraft-protection.exhausted.ts +139 -0
- package/api/src/libs/overdraft-protection.ts +86 -0
- package/api/src/libs/payment.ts +1 -65
- package/api/src/libs/queue/index.ts +0 -1
- package/api/src/libs/subscription.ts +532 -2
- package/api/src/libs/util.ts +4 -0
- package/api/src/locales/en.ts +5 -0
- package/api/src/locales/zh.ts +5 -0
- package/api/src/queues/event.ts +3 -2
- package/api/src/queues/invoice.ts +28 -3
- package/api/src/queues/notification.ts +25 -3
- package/api/src/queues/payment.ts +154 -3
- package/api/src/queues/refund.ts +2 -2
- package/api/src/queues/subscription.ts +215 -4
- package/api/src/queues/webhook.ts +1 -0
- package/api/src/routes/connect/change-payment.ts +1 -1
- package/api/src/routes/connect/change-plan.ts +1 -1
- package/api/src/routes/connect/overdraft-protection.ts +120 -0
- package/api/src/routes/connect/recharge.ts +2 -1
- package/api/src/routes/connect/setup.ts +1 -1
- package/api/src/routes/connect/shared.ts +117 -350
- package/api/src/routes/connect/subscribe.ts +1 -1
- package/api/src/routes/customers.ts +2 -2
- package/api/src/routes/invoices.ts +9 -4
- package/api/src/routes/subscriptions.ts +172 -2
- package/api/src/store/migrate.ts +9 -10
- package/api/src/store/migrations/20240905-index.ts +95 -60
- package/api/src/store/migrations/20241203-overdraft-protection.ts +25 -0
- package/api/src/store/migrations/20241216-update-overdraft-protection.ts +30 -0
- package/api/src/store/models/customer.ts +2 -2
- package/api/src/store/models/invoice.ts +7 -0
- package/api/src/store/models/lock.ts +7 -0
- package/api/src/store/models/subscription.ts +15 -0
- package/api/src/store/sequelize.ts +6 -1
- package/blocklet.yml +1 -1
- package/package.json +23 -23
- package/src/components/customer/overdraft-protection.tsx +367 -0
- package/src/components/event/list.tsx +3 -4
- package/src/components/product/edit-price.tsx +2 -2
- package/src/components/subscription/actions/cancel.tsx +3 -0
- package/src/components/subscription/portal/actions.tsx +324 -77
- package/src/components/uploader.tsx +31 -26
- package/src/env.d.ts +1 -0
- package/src/hooks/subscription.ts +30 -0
- package/src/libs/env.ts +4 -0
- package/src/locales/en.tsx +41 -0
- package/src/locales/zh.tsx +37 -0
- package/src/pages/admin/billing/invoices/detail.tsx +16 -15
- package/src/pages/admin/index.tsx +3 -1
- package/src/pages/admin/products/prices/detail.tsx +1 -1
- package/src/pages/admin/products/products/detail.tsx +6 -2
- package/src/pages/customer/index.tsx +7 -2
- package/src/pages/customer/invoice/detail.tsx +29 -5
- package/src/pages/customer/invoice/past-due.tsx +18 -4
- package/src/pages/customer/recharge.tsx +2 -4
- package/src/pages/customer/subscription/change-payment.tsx +7 -1
- package/src/pages/customer/subscription/detail.tsx +69 -51
- package/tsconfig.json +0 -5
- package/api/tests/libs/payment.spec.ts +0 -168
|
@@ -65,6 +65,10 @@ import {
|
|
|
65
65
|
BillingDiscrepancyEmailTemplate,
|
|
66
66
|
BillingDiscrepancyEmailTemplateOptions,
|
|
67
67
|
} from '../libs/notification/template/billing-discrepancy';
|
|
68
|
+
import {
|
|
69
|
+
OverdraftProtectionExhaustedEmailTemplate,
|
|
70
|
+
OverdraftProtectionExhaustedEmailTemplateOptions,
|
|
71
|
+
} from '../libs/notification/template/subscription.overdraft-protection.exhausted';
|
|
68
72
|
|
|
69
73
|
export type NotificationQueueJobOptions = any;
|
|
70
74
|
|
|
@@ -75,7 +79,8 @@ export type NotificationQueueJobType =
|
|
|
75
79
|
| 'customer.subscription.will_canceled'
|
|
76
80
|
| 'customer.reward.succeeded'
|
|
77
81
|
| 'usage.report.empty'
|
|
78
|
-
| 'billing.discrepancy'
|
|
82
|
+
| 'billing.discrepancy'
|
|
83
|
+
| 'subscription.overdraftProtection.exhausted';
|
|
79
84
|
|
|
80
85
|
export type NotificationQueueJob = {
|
|
81
86
|
type: NotificationQueueJobType;
|
|
@@ -130,6 +135,11 @@ function getNotificationTemplate(job: NotificationQueueJob): BaseEmailTemplate {
|
|
|
130
135
|
job.options as SubscriptionStakeSlashSucceededEmailTemplateOptions
|
|
131
136
|
);
|
|
132
137
|
}
|
|
138
|
+
if (job.type === 'subscription.overdraftProtection.exhausted') {
|
|
139
|
+
return new OverdraftProtectionExhaustedEmailTemplate(
|
|
140
|
+
job.options as OverdraftProtectionExhaustedEmailTemplateOptions
|
|
141
|
+
);
|
|
142
|
+
}
|
|
133
143
|
|
|
134
144
|
throw new Error(`Unknown job type: ${job.type}`);
|
|
135
145
|
}
|
|
@@ -234,8 +244,8 @@ export async function startNotificationQueue() {
|
|
|
234
244
|
});
|
|
235
245
|
});
|
|
236
246
|
|
|
237
|
-
events.on('customer.subscription.renew_failed', async (subscription: Subscription) => {
|
|
238
|
-
const invoice = await Invoice.findByPk(subscription.latest_invoice_id);
|
|
247
|
+
events.on('customer.subscription.renew_failed', async (subscription: Subscription, { invoiceId }) => {
|
|
248
|
+
const invoice = await Invoice.findByPk(invoiceId || subscription.latest_invoice_id);
|
|
239
249
|
|
|
240
250
|
logger.info('events.on', 'customer.subscription.renew_failed', {
|
|
241
251
|
subscriptionId: subscription.id,
|
|
@@ -317,4 +327,16 @@ export async function startNotificationQueue() {
|
|
|
317
327
|
},
|
|
318
328
|
});
|
|
319
329
|
});
|
|
330
|
+
|
|
331
|
+
events.on('subscription.overdraft_protection.exhausted', (subscription: Subscription) => {
|
|
332
|
+
notificationQueue.push({
|
|
333
|
+
id: `subscription.overdraftProtection.exhausted.${subscription.id}`,
|
|
334
|
+
job: {
|
|
335
|
+
type: 'subscription.overdraftProtection.exhausted',
|
|
336
|
+
options: {
|
|
337
|
+
subscriptionId: subscription.id,
|
|
338
|
+
},
|
|
339
|
+
},
|
|
340
|
+
});
|
|
341
|
+
});
|
|
320
342
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import isEmpty from 'lodash/isEmpty';
|
|
2
2
|
|
|
3
|
+
import { BN } from '@ocap/util';
|
|
3
4
|
import { ensureStakedForGas } from '../integrations/arcblock/stake';
|
|
4
5
|
import { transferErc20FromUser } from '../integrations/ethereum/token';
|
|
5
6
|
import { createEvent } from '../libs/audit';
|
|
@@ -9,7 +10,6 @@ import CustomError from '../libs/error';
|
|
|
9
10
|
import { events } from '../libs/event';
|
|
10
11
|
import logger from '../libs/logger';
|
|
11
12
|
import { getGasPayerExtra, isDelegationSufficientForPayment } from '../libs/payment';
|
|
12
|
-
import createQueue from '../libs/queue';
|
|
13
13
|
import {
|
|
14
14
|
checkRemainingStake,
|
|
15
15
|
getDaysUntilCancel,
|
|
@@ -19,6 +19,7 @@ import {
|
|
|
19
19
|
getMinRetryMail,
|
|
20
20
|
getSubscriptionCreateSetup,
|
|
21
21
|
getSubscriptionStakeAddress,
|
|
22
|
+
isSubscriptionOverdraftProtectionEnabled,
|
|
22
23
|
shouldCancelSubscription,
|
|
23
24
|
} from '../libs/subscription';
|
|
24
25
|
import { MAX_RETRY_COUNT, MIN_RETRY_MAIL, getNextRetry } from '../libs/util';
|
|
@@ -34,6 +35,10 @@ import { Subscription } from '../store/models/subscription';
|
|
|
34
35
|
import { SubscriptionItem } from '../store/models/subscription-item';
|
|
35
36
|
import type { PaymentError, PaymentSettings } from '../store/models/types';
|
|
36
37
|
import { notificationQueue } from './notification';
|
|
38
|
+
import { ensureOverdraftProtectionInvoiceAndItems } from '../libs/invoice';
|
|
39
|
+
import { Lock } from '../store/models';
|
|
40
|
+
import { ensureOverdraftProtectionPrice } from '../libs/overdraft-protection';
|
|
41
|
+
import createQueue from '../libs/queue';
|
|
37
42
|
|
|
38
43
|
type PaymentJob = {
|
|
39
44
|
paymentIntentId: string;
|
|
@@ -240,6 +245,59 @@ export const handlePaymentSucceed = async (
|
|
|
240
245
|
}
|
|
241
246
|
};
|
|
242
247
|
|
|
248
|
+
export const doOverdraftProtection = async (
|
|
249
|
+
paymentIntent: PaymentIntent,
|
|
250
|
+
subscription: Subscription,
|
|
251
|
+
customer: Customer,
|
|
252
|
+
relatedInvoice?: Invoice
|
|
253
|
+
) => {
|
|
254
|
+
const { enabled } = await isSubscriptionOverdraftProtectionEnabled(subscription);
|
|
255
|
+
if (!enabled) {
|
|
256
|
+
logger.warn('overdraft protection is not enabled', {
|
|
257
|
+
subscription: subscription.id,
|
|
258
|
+
});
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
const existProtectedInvoice = await Invoice.findOne({
|
|
262
|
+
where: {
|
|
263
|
+
subscription_id: subscription.id,
|
|
264
|
+
billing_reason: 'overdraft_protection',
|
|
265
|
+
'metadata.payment_intent_id': paymentIntent.id,
|
|
266
|
+
},
|
|
267
|
+
});
|
|
268
|
+
if (existProtectedInvoice) {
|
|
269
|
+
logger.info('overdraft protection invoice already exists', {
|
|
270
|
+
invoice: existProtectedInvoice.id,
|
|
271
|
+
});
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
try {
|
|
275
|
+
const { invoice, items } = await ensureOverdraftProtectionInvoiceAndItems({
|
|
276
|
+
customer,
|
|
277
|
+
subscription,
|
|
278
|
+
paymentIntent,
|
|
279
|
+
props: {
|
|
280
|
+
period_start: subscription.current_period_start,
|
|
281
|
+
period_end: subscription.current_period_end,
|
|
282
|
+
metadata: {
|
|
283
|
+
payment_intent_id: paymentIntent.id,
|
|
284
|
+
invoice_id: relatedInvoice?.id || '',
|
|
285
|
+
},
|
|
286
|
+
},
|
|
287
|
+
});
|
|
288
|
+
logger.info('ensure overdraft protection invoice and items done', {
|
|
289
|
+
invoice: invoice.id,
|
|
290
|
+
items: items.map((x) => x.id),
|
|
291
|
+
});
|
|
292
|
+
} catch (error) {
|
|
293
|
+
logger.error('ensure overdraft protection invoice and items failed', {
|
|
294
|
+
error,
|
|
295
|
+
subscription: subscription.id,
|
|
296
|
+
});
|
|
297
|
+
throw error;
|
|
298
|
+
}
|
|
299
|
+
};
|
|
300
|
+
|
|
243
301
|
type Updates = {
|
|
244
302
|
retry: {
|
|
245
303
|
payment: Partial<PaymentIntent>;
|
|
@@ -323,6 +381,49 @@ export const handlePaymentFailed = async (
|
|
|
323
381
|
return updates.terminate;
|
|
324
382
|
}
|
|
325
383
|
|
|
384
|
+
if (invoice.billing_reason === 'overdraft_protection') {
|
|
385
|
+
// overdraft protection invoice is always terminated
|
|
386
|
+
return updates.terminate;
|
|
387
|
+
}
|
|
388
|
+
// check overdraft protection, if protected, no need to check due
|
|
389
|
+
const customer = await Customer.findByPk(invoice.customer_id);
|
|
390
|
+
|
|
391
|
+
const { enabled: enableOverdraftProtection, unused: unusedAmount } =
|
|
392
|
+
await isSubscriptionOverdraftProtectionEnabled(subscription);
|
|
393
|
+
const { price } = await ensureOverdraftProtectionPrice(subscription.livemode);
|
|
394
|
+
const invoicePrice = (price.currency_options || []).find((x: any) => x.currency_id === paymentIntent?.currency_id);
|
|
395
|
+
|
|
396
|
+
if (subscription.overdraft_protection?.enabled && !enableOverdraftProtection) {
|
|
397
|
+
// overdraft protection is enabled but not enough
|
|
398
|
+
const lockKey = `${subscription.id}-${paymentIntent.currency_id}-overdraft-protection-exhausted`;
|
|
399
|
+
const isLock = await Lock.isLocked(lockKey);
|
|
400
|
+
if (!isLock) {
|
|
401
|
+
await Lock.acquire(lockKey, subscription.current_period_end);
|
|
402
|
+
createEvent('Subscription', 'subscription.overdraft_protection.exhausted', subscription).catch(console.error);
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
if (
|
|
407
|
+
enableOverdraftProtection &&
|
|
408
|
+
unusedAmount !== '0' &&
|
|
409
|
+
new BN(unusedAmount).gte(new BN(invoicePrice?.unit_amount || '0'))
|
|
410
|
+
) {
|
|
411
|
+
logger.info('do overdraft protection', {
|
|
412
|
+
subscription: subscription.id,
|
|
413
|
+
payment: paymentIntent.id,
|
|
414
|
+
unusedAmount,
|
|
415
|
+
});
|
|
416
|
+
try {
|
|
417
|
+
await doOverdraftProtection(paymentIntent, subscription, customer!, invoice);
|
|
418
|
+
return updates.terminate;
|
|
419
|
+
} catch (err) {
|
|
420
|
+
logger.error('do overdraft protection failed', {
|
|
421
|
+
error: err,
|
|
422
|
+
subscription: subscription.id,
|
|
423
|
+
});
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
|
|
326
427
|
if (subscription.isImmutable()) {
|
|
327
428
|
logger.info('Subscription is immutable, no need to check due', { subscription: subscription.id });
|
|
328
429
|
return updates.terminate;
|
|
@@ -543,6 +644,32 @@ export const handlePayment = async (job: PaymentJob) => {
|
|
|
543
644
|
|
|
544
645
|
const invoice = await Invoice.findByPk(paymentIntent.invoice_id);
|
|
545
646
|
|
|
647
|
+
if (invoice?.status === 'void') {
|
|
648
|
+
await paymentIntent.update({
|
|
649
|
+
status: 'canceled',
|
|
650
|
+
canceled_at: dayjs().unix(),
|
|
651
|
+
cancellation_reason: 'void_invoice',
|
|
652
|
+
});
|
|
653
|
+
logger.info('PaymentIntent capture skipped because invoice is void', { id: paymentIntent.id });
|
|
654
|
+
return;
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
if (invoice && invoice.subscription_id) {
|
|
658
|
+
const subscription = await Subscription.findByPk(invoice.subscription_id);
|
|
659
|
+
if (
|
|
660
|
+
subscription &&
|
|
661
|
+
subscription.isActive() &&
|
|
662
|
+
subscription.overdraft_protection?.enabled &&
|
|
663
|
+
invoice?.billing_reason === 'overdraft_protection'
|
|
664
|
+
) {
|
|
665
|
+
logger.info('PaymentIntent capture skipped because of overdraft protection', {
|
|
666
|
+
id: paymentIntent.id,
|
|
667
|
+
subscription: subscription.id,
|
|
668
|
+
});
|
|
669
|
+
return;
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
|
|
546
673
|
// check max retry before doing any hard work
|
|
547
674
|
if (invoice && invoice.attempt_count >= MAX_RETRY_COUNT) {
|
|
548
675
|
logger.info('PaymentIntent capture aborted since max retry exceeded', { id: paymentIntent.id });
|
|
@@ -740,7 +867,9 @@ export const handlePayment = async (job: PaymentJob) => {
|
|
|
740
867
|
invoiceId: invoice.id,
|
|
741
868
|
});
|
|
742
869
|
if (!subscription.isImmutable()) {
|
|
743
|
-
createEvent('Subscription', 'customer.subscription.renew_failed', subscription
|
|
870
|
+
createEvent('Subscription', 'customer.subscription.renew_failed', subscription, {
|
|
871
|
+
invoiceId: invoice.id,
|
|
872
|
+
});
|
|
744
873
|
}
|
|
745
874
|
}
|
|
746
875
|
}
|
|
@@ -773,7 +902,6 @@ export const paymentQueue = createQueue<PaymentJob>({
|
|
|
773
902
|
});
|
|
774
903
|
|
|
775
904
|
export const startPaymentQueue = async () => {
|
|
776
|
-
// Restore previous payments
|
|
777
905
|
const payments = await PaymentIntent.findAll({
|
|
778
906
|
where: {
|
|
779
907
|
status: ['requires_capture', 'processing'],
|
|
@@ -804,3 +932,26 @@ events.on('payment_intent.succeeded', async (paymentIntent: PaymentIntent) => {
|
|
|
804
932
|
ensureStakedForGas();
|
|
805
933
|
}
|
|
806
934
|
});
|
|
935
|
+
|
|
936
|
+
events.on('payment.queued', async (id, job, args = {}) => {
|
|
937
|
+
const { sync, ...extraArgs } = args;
|
|
938
|
+
if (sync) {
|
|
939
|
+
try {
|
|
940
|
+
await paymentQueue.pushAndWait({
|
|
941
|
+
id,
|
|
942
|
+
job,
|
|
943
|
+
...extraArgs,
|
|
944
|
+
});
|
|
945
|
+
events.emit('payment.queued.done');
|
|
946
|
+
} catch (error) {
|
|
947
|
+
logger.error('Error in payment.queued', { id, job, error });
|
|
948
|
+
events.emit('payment.queued.error', error);
|
|
949
|
+
}
|
|
950
|
+
return;
|
|
951
|
+
}
|
|
952
|
+
paymentQueue.push({
|
|
953
|
+
id,
|
|
954
|
+
job,
|
|
955
|
+
...extraArgs,
|
|
956
|
+
});
|
|
957
|
+
});
|
package/api/src/queues/refund.ts
CHANGED
|
@@ -347,13 +347,13 @@ const handleStakeReturnJob = async (
|
|
|
347
347
|
tokens: [{ address: paymentCurrency.contract, value: refund.amount }],
|
|
348
348
|
},
|
|
349
349
|
],
|
|
350
|
-
message: 'stake_return_on_subscription_cancel',
|
|
350
|
+
message: refund.description || 'stake_return_on_subscription_cancel',
|
|
351
351
|
data: {
|
|
352
352
|
typeUrl: 'json',
|
|
353
353
|
// @ts-ignore
|
|
354
354
|
value: {
|
|
355
355
|
appId: wallet.address,
|
|
356
|
-
reason: 'subscription_cancel',
|
|
356
|
+
reason: refund?.metadata?.reason || 'subscription_cancel',
|
|
357
357
|
subscriptionId: refund.subscription_id,
|
|
358
358
|
},
|
|
359
359
|
},
|
|
@@ -21,9 +21,12 @@ import {
|
|
|
21
21
|
getSubscriptionStakeAddress,
|
|
22
22
|
getSubscriptionStakeReturnSetup,
|
|
23
23
|
getSubscriptionStakeSlashSetup,
|
|
24
|
+
isSubscriptionOverdraftProtectionEnabled,
|
|
25
|
+
returnOverdraftProtectionStake,
|
|
24
26
|
shouldCancelSubscription,
|
|
27
|
+
slashOverdraftProtectionStake,
|
|
25
28
|
} from '../libs/subscription';
|
|
26
|
-
import { ensureInvoiceAndItems } from '../
|
|
29
|
+
import { ensureInvoiceAndItems } from '../libs/invoice';
|
|
27
30
|
import { PaymentCurrency, PaymentIntent, PaymentMethod, Refund, SetupIntent, UsageRecord } from '../store/models';
|
|
28
31
|
import { Customer } from '../store/models/customer';
|
|
29
32
|
import { Invoice } from '../store/models/invoice';
|
|
@@ -87,7 +90,18 @@ const doHandleSubscriptionInvoice = async ({
|
|
|
87
90
|
billing_reason: `subscription_${reason}`,
|
|
88
91
|
},
|
|
89
92
|
});
|
|
90
|
-
|
|
93
|
+
let existOverdraftProtection;
|
|
94
|
+
if (previous) {
|
|
95
|
+
existOverdraftProtection = await Invoice.findOne({
|
|
96
|
+
where: {
|
|
97
|
+
subscription_id: subscription.id,
|
|
98
|
+
billing_reason: 'overdraft_protection',
|
|
99
|
+
'metadata.invoice_id': previous.id,
|
|
100
|
+
},
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (previous && previous.isImmutable() === false && !existOverdraftProtection) {
|
|
91
105
|
logger.warn(`Invoice for current period ${previous.id} not paid for subscription ${subscription.id}`);
|
|
92
106
|
return null;
|
|
93
107
|
}
|
|
@@ -747,6 +761,102 @@ const ensureRefundOnCancel = async (subscription: Subscription) => {
|
|
|
747
761
|
});
|
|
748
762
|
};
|
|
749
763
|
|
|
764
|
+
const ensureReturnOverdraftProtectionStake = async (subscription: Subscription, paymentCurrencyId?: string) => {
|
|
765
|
+
const { enabled, remaining, revokedStake } = await isSubscriptionOverdraftProtectionEnabled(
|
|
766
|
+
subscription,
|
|
767
|
+
paymentCurrencyId
|
|
768
|
+
);
|
|
769
|
+
if (!enabled && remaining === '0' && revokedStake === '0') {
|
|
770
|
+
logger.info('Return overdraft protection stake skipped because no remaining', {
|
|
771
|
+
subscription: subscription.id,
|
|
772
|
+
remaining,
|
|
773
|
+
revokedStake,
|
|
774
|
+
});
|
|
775
|
+
return;
|
|
776
|
+
}
|
|
777
|
+
const paymentCurrency = await PaymentCurrency.findByPk(paymentCurrencyId || subscription.currency_id);
|
|
778
|
+
if (!paymentCurrency) {
|
|
779
|
+
logger.warn('Return overdraft protection stake skipped because currency not found', {
|
|
780
|
+
subscription: subscription.id,
|
|
781
|
+
currency: subscription.currency_id,
|
|
782
|
+
});
|
|
783
|
+
return;
|
|
784
|
+
}
|
|
785
|
+
const paymentMethod = await PaymentMethod.findByPk(paymentCurrency.payment_method_id);
|
|
786
|
+
if (!paymentMethod) {
|
|
787
|
+
logger.warn('Return overdraft protection stake skipped because payment method not found', {
|
|
788
|
+
subscription: subscription.id,
|
|
789
|
+
paymentMethod: paymentCurrency.payment_method_id,
|
|
790
|
+
});
|
|
791
|
+
return;
|
|
792
|
+
}
|
|
793
|
+
if (paymentMethod?.type !== 'arcblock') {
|
|
794
|
+
logger.info('Return overdraft protection stake skipped because payment method is not arcblock', {
|
|
795
|
+
subscription: subscription.id,
|
|
796
|
+
paymentMethod: paymentMethod?.type,
|
|
797
|
+
});
|
|
798
|
+
return;
|
|
799
|
+
}
|
|
800
|
+
await returnOverdraftProtectionStake(subscription, paymentCurrencyId);
|
|
801
|
+
logger.info('Overdraft protection stake returned', { subscription: subscription.id });
|
|
802
|
+
if (subscription.overdraft_protection?.enabled) {
|
|
803
|
+
await subscription.update({
|
|
804
|
+
// @ts-ignore
|
|
805
|
+
overdraft_protection: {
|
|
806
|
+
...(subscription.overdraft_protection || {}),
|
|
807
|
+
enabled: false,
|
|
808
|
+
},
|
|
809
|
+
});
|
|
810
|
+
logger.info('Overdraft protection disabled', { subscription: subscription.id });
|
|
811
|
+
}
|
|
812
|
+
};
|
|
813
|
+
|
|
814
|
+
const ensureSlashOverdraftProtectionStake = async (subscription: Subscription) => {
|
|
815
|
+
const { remaining, revokedStake } = await isSubscriptionOverdraftProtectionEnabled(subscription);
|
|
816
|
+
if (remaining === '0' && revokedStake === '0') {
|
|
817
|
+
logger.info('Slash overdraft protection stake skipped because no remaining', {
|
|
818
|
+
subscription: subscription.id,
|
|
819
|
+
remaining,
|
|
820
|
+
revokedStake,
|
|
821
|
+
});
|
|
822
|
+
return;
|
|
823
|
+
}
|
|
824
|
+
const paymentCurrency = await PaymentCurrency.findByPk(subscription.currency_id);
|
|
825
|
+
if (!paymentCurrency) {
|
|
826
|
+
logger.warn('Slash overdraft protection stake skipped because currency not found', {
|
|
827
|
+
subscription: subscription.id,
|
|
828
|
+
currency: subscription.currency_id,
|
|
829
|
+
});
|
|
830
|
+
return;
|
|
831
|
+
}
|
|
832
|
+
const paymentMethod = await PaymentMethod.findByPk(paymentCurrency.payment_method_id);
|
|
833
|
+
if (!paymentMethod) {
|
|
834
|
+
logger.warn('Slash overdraft protection stake skipped because payment method not found', {
|
|
835
|
+
subscription: subscription.id,
|
|
836
|
+
paymentMethod: paymentCurrency.payment_method_id,
|
|
837
|
+
});
|
|
838
|
+
return;
|
|
839
|
+
}
|
|
840
|
+
if (paymentMethod?.type !== 'arcblock') {
|
|
841
|
+
logger.info('Slash overdraft protection stake skipped because payment method is not arcblock', {
|
|
842
|
+
subscription: subscription.id,
|
|
843
|
+
paymentMethod: paymentMethod?.type,
|
|
844
|
+
});
|
|
845
|
+
return;
|
|
846
|
+
}
|
|
847
|
+
await slashOverdraftProtectionStake(subscription);
|
|
848
|
+
logger.info('Overdraft protection stake slashed', { subscription: subscription.id });
|
|
849
|
+
if (subscription.overdraft_protection?.enabled) {
|
|
850
|
+
await subscription.update({
|
|
851
|
+
// @ts-ignore
|
|
852
|
+
overdraft_protection: {
|
|
853
|
+
...(subscription.overdraft_protection || {}),
|
|
854
|
+
enabled: false,
|
|
855
|
+
},
|
|
856
|
+
});
|
|
857
|
+
}
|
|
858
|
+
};
|
|
859
|
+
|
|
750
860
|
// generate invoice for subscription periodically
|
|
751
861
|
export const handleSubscription = async (job: SubscriptionJob) => {
|
|
752
862
|
logger.info('handle subscription', job);
|
|
@@ -780,6 +890,12 @@ export const handleSubscription = async (job: SubscriptionJob) => {
|
|
|
780
890
|
});
|
|
781
891
|
|
|
782
892
|
if (previousStatus === 'past_due' && job.action === 'cancel') {
|
|
893
|
+
// slash overdraft protection stake
|
|
894
|
+
await slashOverdraftProtectionQueue.pushAndWait({
|
|
895
|
+
id: `slash-overdraft-protection-${subscription.id}`,
|
|
896
|
+
job: { subscriptionId: subscription.id },
|
|
897
|
+
});
|
|
898
|
+
logger.info('Overdraft protection stake slash job scheduled', { subscription: subscription.id });
|
|
783
899
|
await handleStakeSlashAfterCancel(subscription);
|
|
784
900
|
}
|
|
785
901
|
return;
|
|
@@ -924,6 +1040,44 @@ export const subscriptionCancelRefund = createQueue({
|
|
|
924
1040
|
},
|
|
925
1041
|
});
|
|
926
1042
|
|
|
1043
|
+
export const returnOverdraftProtectionQueue = createQueue({
|
|
1044
|
+
name: 'returnOverdraftProtection',
|
|
1045
|
+
onJob: async (job) => {
|
|
1046
|
+
const { subscriptionId, paymentCurrencyId } = job;
|
|
1047
|
+
const subscription = await Subscription.findByPk(subscriptionId);
|
|
1048
|
+
if (!subscription) {
|
|
1049
|
+
return;
|
|
1050
|
+
}
|
|
1051
|
+
await ensureReturnOverdraftProtectionStake(subscription, paymentCurrencyId);
|
|
1052
|
+
},
|
|
1053
|
+
options: {
|
|
1054
|
+
concurrency: 1,
|
|
1055
|
+
maxRetries: 5,
|
|
1056
|
+
retryDelay: 1000,
|
|
1057
|
+
maxTimeout: 60000,
|
|
1058
|
+
enableScheduledJob: true,
|
|
1059
|
+
},
|
|
1060
|
+
});
|
|
1061
|
+
|
|
1062
|
+
export const slashOverdraftProtectionQueue = createQueue({
|
|
1063
|
+
name: 'slashOverdraftProtection',
|
|
1064
|
+
onJob: async (job) => {
|
|
1065
|
+
const { subscriptionId } = job;
|
|
1066
|
+
const subscription = await Subscription.findByPk(subscriptionId);
|
|
1067
|
+
if (!subscription) {
|
|
1068
|
+
return;
|
|
1069
|
+
}
|
|
1070
|
+
await ensureSlashOverdraftProtectionStake(subscription);
|
|
1071
|
+
},
|
|
1072
|
+
options: {
|
|
1073
|
+
concurrency: 1,
|
|
1074
|
+
maxRetries: 5,
|
|
1075
|
+
retryDelay: 1000,
|
|
1076
|
+
maxTimeout: 60000,
|
|
1077
|
+
enableScheduledJob: true,
|
|
1078
|
+
},
|
|
1079
|
+
});
|
|
1080
|
+
|
|
927
1081
|
export async function addSubscriptionJob(
|
|
928
1082
|
subscription: Subscription,
|
|
929
1083
|
action: 'cycle' | 'cancel' | 'resume',
|
|
@@ -978,12 +1132,11 @@ events.on('customer.subscription.recovered', async (subscription: Subscription)
|
|
|
978
1132
|
await handleSubscriptionAfterRecover(doc!);
|
|
979
1133
|
});
|
|
980
1134
|
|
|
981
|
-
events.on('customer.subscription.deleted', (subscription: Subscription) => {
|
|
1135
|
+
events.on('customer.subscription.deleted', async (subscription: Subscription) => {
|
|
982
1136
|
ensurePassportRevoked(subscription).catch((err) => {
|
|
983
1137
|
logger.error('ensurePassportRevoked failed', { error: err, subscription: subscription.id });
|
|
984
1138
|
});
|
|
985
1139
|
// FIXME: ensure invoices that are open or uncollectible are voided
|
|
986
|
-
|
|
987
1140
|
if (
|
|
988
1141
|
subscription.cancelation_details?.refund &&
|
|
989
1142
|
['last', 'proration'].includes(subscription.cancelation_details.refund)
|
|
@@ -993,6 +1146,15 @@ events.on('customer.subscription.deleted', (subscription: Subscription) => {
|
|
|
993
1146
|
job: { subscriptionId: subscription.id },
|
|
994
1147
|
});
|
|
995
1148
|
}
|
|
1149
|
+
const { remaining, revokedStake } = await isSubscriptionOverdraftProtectionEnabled(subscription);
|
|
1150
|
+
if (remaining !== '0' || revokedStake !== '0') {
|
|
1151
|
+
// need return overdraft protection stake
|
|
1152
|
+
await returnOverdraftProtectionQueue.pushAndWait({
|
|
1153
|
+
id: `return-overdraft-protection-${subscription.id}`,
|
|
1154
|
+
job: { subscriptionId: subscription.id },
|
|
1155
|
+
});
|
|
1156
|
+
logger.info('Overdraft protection stake return job scheduled', { subscription: subscription.id });
|
|
1157
|
+
}
|
|
996
1158
|
if (subscription.cancelation_details?.slash_stake) {
|
|
997
1159
|
slashStakeQueue.push({
|
|
998
1160
|
id: `slash-stake-${subscription.id}`,
|
|
@@ -1016,6 +1178,25 @@ events.on('customer.stake.revoked', async ({ subscriptionId, tx }: { subscriptio
|
|
|
1016
1178
|
if (!subscription) {
|
|
1017
1179
|
return;
|
|
1018
1180
|
}
|
|
1181
|
+
|
|
1182
|
+
const { address } = tx.tx.itxJson;
|
|
1183
|
+
if (address === subscription.overdraft_protection?.payment_details?.arcblock?.staking?.address) {
|
|
1184
|
+
// revoke overdraft protection stake
|
|
1185
|
+
await subscription.update({
|
|
1186
|
+
// @ts-ignore
|
|
1187
|
+
overdraft_protection: {
|
|
1188
|
+
...(subscription.overdraft_protection || {}),
|
|
1189
|
+
enabled: false,
|
|
1190
|
+
},
|
|
1191
|
+
});
|
|
1192
|
+
slashOverdraftProtectionQueue.push({
|
|
1193
|
+
id: `slash-overdraft-protection-${subscription.id}`,
|
|
1194
|
+
job: { subscriptionId: subscription.id },
|
|
1195
|
+
});
|
|
1196
|
+
logger.info('Overdraft protection stake slash job scheduled', { subscription: subscription.id });
|
|
1197
|
+
return;
|
|
1198
|
+
}
|
|
1199
|
+
|
|
1019
1200
|
if (subscription.isActive() === false) {
|
|
1020
1201
|
return;
|
|
1021
1202
|
}
|
|
@@ -1030,7 +1211,18 @@ events.on('customer.stake.revoked', async ({ subscriptionId, tx }: { subscriptio
|
|
|
1030
1211
|
feedback: 'other',
|
|
1031
1212
|
comment: `Revoked by ${tx.tx.from} with tx ${tx.hash}`,
|
|
1032
1213
|
},
|
|
1214
|
+
// 关闭透支保护
|
|
1215
|
+
// @ts-ignore
|
|
1216
|
+
overdraft_protection: {
|
|
1217
|
+
...(subscription.overdraft_protection || {}),
|
|
1218
|
+
enabled: false,
|
|
1219
|
+
},
|
|
1220
|
+
});
|
|
1221
|
+
slashOverdraftProtectionQueue.push({
|
|
1222
|
+
id: `slash-overdraft-protection-${subscription.id}`,
|
|
1223
|
+
job: { subscriptionId: subscription.id },
|
|
1033
1224
|
});
|
|
1225
|
+
logger.info('Overdraft protection stake slash job scheduled', { subscription: subscription.id });
|
|
1034
1226
|
await new SubscriptionWillCanceledSchedule().reScheduleSubscriptionTasks([subscription]);
|
|
1035
1227
|
await addSubscriptionJob(subscription, 'cancel', true, subscription.current_period_end);
|
|
1036
1228
|
});
|
|
@@ -1069,5 +1261,24 @@ events.on('setup_intent.succeeded', async (setupIntent: SetupIntent) => {
|
|
|
1069
1261
|
paymentCurrencyId: setupIntent.metadata?.from_currency,
|
|
1070
1262
|
});
|
|
1071
1263
|
}
|
|
1264
|
+
const { remaining, revokedStake } = await isSubscriptionOverdraftProtectionEnabled(
|
|
1265
|
+
subscription,
|
|
1266
|
+
setupIntent.metadata?.from_currency
|
|
1267
|
+
);
|
|
1268
|
+
|
|
1269
|
+
if (remaining !== '0' || revokedStake !== '0') {
|
|
1270
|
+
try {
|
|
1271
|
+
returnOverdraftProtectionQueue.push({
|
|
1272
|
+
id: `return-overdraft-protection-${subscription.id}`,
|
|
1273
|
+
job: { subscriptionId: subscription.id, paymentCurrencyId: setupIntent.metadata?.from_currency },
|
|
1274
|
+
});
|
|
1275
|
+
logger.info('Overdraft protection stake return job scheduled', {
|
|
1276
|
+
subscription: subscription.id,
|
|
1277
|
+
paymentCurrencyId: setupIntent.metadata?.from_currency,
|
|
1278
|
+
});
|
|
1279
|
+
} catch (error) {
|
|
1280
|
+
logger.error('create return overdraft protection stake job failed', { error, subscription: subscription.id });
|
|
1281
|
+
}
|
|
1282
|
+
}
|
|
1072
1283
|
}
|
|
1073
1284
|
});
|
|
@@ -103,6 +103,7 @@ export const handleWebhook = async (job: WebhookJob) => {
|
|
|
103
103
|
id: getWebhookJobId(event.id, webhook.id),
|
|
104
104
|
job: { eventId: event.id, webhookId: webhook.id },
|
|
105
105
|
runAt: getNextRetry(retryCount),
|
|
106
|
+
persist: false,
|
|
106
107
|
});
|
|
107
108
|
logger.info('scheduled webhook job', { ...job, retryCount });
|
|
108
109
|
});
|
|
@@ -4,13 +4,13 @@ import { getTxMetadata } from '../../libs/util';
|
|
|
4
4
|
import { Lock, type TLineItemExpanded } from '../../store/models';
|
|
5
5
|
import {
|
|
6
6
|
ensureChangePaymentContext,
|
|
7
|
-
ensureStakeInvoice,
|
|
8
7
|
executeOcapTransactions,
|
|
9
8
|
getAuthPrincipalClaim,
|
|
10
9
|
getDelegationTxClaim,
|
|
11
10
|
getStakeTxClaim,
|
|
12
11
|
updateStripeSubscriptionAfterChangePayment,
|
|
13
12
|
} from './shared';
|
|
13
|
+
import { ensureStakeInvoice } from '../../libs/invoice';
|
|
14
14
|
|
|
15
15
|
export default {
|
|
16
16
|
action: 'change-payment',
|
|
@@ -8,13 +8,13 @@ import { invoiceQueue } from '../../queues/invoice';
|
|
|
8
8
|
import { addSubscriptionJob, subscriptionQueue } from '../../queues/subscription';
|
|
9
9
|
import type { TLineItemExpanded } from '../../store/models';
|
|
10
10
|
import {
|
|
11
|
-
ensureStakeInvoice,
|
|
12
11
|
ensureSubscription,
|
|
13
12
|
executeOcapTransactions,
|
|
14
13
|
getAuthPrincipalClaim,
|
|
15
14
|
getDelegationTxClaim,
|
|
16
15
|
getStakeTxClaim,
|
|
17
16
|
} from './shared';
|
|
17
|
+
import { ensureStakeInvoice } from '../../libs/invoice';
|
|
18
18
|
|
|
19
19
|
export default {
|
|
20
20
|
action: 'change-plan',
|