payment-kit 1.18.29 → 1.18.31
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 +8 -0
- package/api/src/crons/metering-subscription-detection.ts +9 -0
- package/api/src/integrations/arcblock/nft.ts +1 -0
- package/api/src/integrations/blocklet/passport.ts +1 -1
- package/api/src/integrations/stripe/handlers/invoice.ts +2 -2
- package/api/src/integrations/stripe/handlers/setup-intent.ts +29 -1
- package/api/src/integrations/stripe/handlers/subscription.ts +19 -15
- package/api/src/integrations/stripe/resource.ts +81 -1
- package/api/src/libs/audit.ts +42 -0
- package/api/src/libs/constants.ts +2 -0
- package/api/src/libs/env.ts +2 -2
- package/api/src/libs/invoice.ts +54 -7
- package/api/src/libs/notification/index.ts +72 -4
- package/api/src/libs/notification/template/base.ts +2 -0
- package/api/src/libs/notification/template/subscription-renew-failed.ts +1 -5
- package/api/src/libs/notification/template/subscription-renewed.ts +1 -5
- package/api/src/libs/notification/template/subscription-succeeded.ts +8 -18
- package/api/src/libs/notification/template/subscription-trial-start.ts +2 -10
- package/api/src/libs/notification/template/subscription-upgraded.ts +1 -5
- package/api/src/libs/payment.ts +48 -8
- package/api/src/libs/product.ts +1 -4
- package/api/src/libs/session.ts +600 -8
- package/api/src/libs/setting.ts +172 -0
- package/api/src/libs/subscription.ts +7 -69
- package/api/src/libs/ws.ts +5 -0
- package/api/src/queues/checkout-session.ts +42 -36
- package/api/src/queues/notification.ts +3 -2
- package/api/src/queues/payment.ts +56 -8
- package/api/src/queues/usage-record.ts +2 -10
- package/api/src/routes/checkout-sessions.ts +324 -187
- package/api/src/routes/connect/shared.ts +160 -38
- package/api/src/routes/connect/subscribe.ts +123 -64
- package/api/src/routes/payment-currencies.ts +11 -0
- package/api/src/routes/payment-links.ts +11 -1
- package/api/src/routes/payment-stats.ts +2 -2
- package/api/src/routes/payouts.ts +2 -1
- package/api/src/routes/settings.ts +45 -0
- package/api/src/routes/subscriptions.ts +1 -2
- package/api/src/store/migrations/20250408-subscription-grouping.ts +39 -0
- package/api/src/store/migrations/20250419-subscription-grouping.ts +69 -0
- package/api/src/store/models/checkout-session.ts +52 -0
- package/api/src/store/models/index.ts +1 -0
- package/api/src/store/models/payment-link.ts +6 -0
- package/api/src/store/models/subscription.ts +8 -6
- package/api/src/store/models/types.ts +32 -1
- package/api/tests/libs/session.spec.ts +423 -0
- package/api/tests/libs/subscription.spec.ts +0 -110
- package/blocklet.yml +3 -1
- package/package.json +25 -24
- package/scripts/sdk.js +486 -155
- package/src/locales/en.tsx +4 -0
- package/src/locales/zh.tsx +3 -0
- package/src/pages/admin/settings/vault-config/edit-form.tsx +58 -3
- package/src/pages/admin/settings/vault-config/index.tsx +35 -1
- package/src/pages/customer/subscription/change-payment.tsx +8 -3
- package/src/pages/integrations/overview.tsx +1 -1
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import { Op } from 'sequelize';
|
|
2
|
+
import { CheckoutSession, Invoice, Setting, Subscription } from '../store/models';
|
|
3
|
+
import type { EventType, NotificationSetting } from '../store/models/types';
|
|
4
|
+
import logger from './logger';
|
|
5
|
+
|
|
6
|
+
const notificationSettingKey = 'notification_settings';
|
|
7
|
+
const settingIdKey = 'setting_id';
|
|
8
|
+
|
|
9
|
+
export const getSettingForNotification = async (settingKey: string | null): Promise<NotificationSetting | null> => {
|
|
10
|
+
if (!settingKey) {
|
|
11
|
+
return null;
|
|
12
|
+
}
|
|
13
|
+
try {
|
|
14
|
+
const setting = await Setting.findOne({
|
|
15
|
+
where: {
|
|
16
|
+
type: 'notification',
|
|
17
|
+
[Op.or]: [
|
|
18
|
+
{
|
|
19
|
+
id: settingKey,
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
component_did: settingKey,
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
mount_location: settingKey,
|
|
26
|
+
},
|
|
27
|
+
],
|
|
28
|
+
},
|
|
29
|
+
});
|
|
30
|
+
return (setting?.settings || null) as NotificationSetting | null;
|
|
31
|
+
} catch (error) {
|
|
32
|
+
logger.error('getSettingForNotification error', error);
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export function getDirectNotificationSetting(obj: {
|
|
38
|
+
metadata?: Record<string, any>;
|
|
39
|
+
subscription_data?: Record<string, any>;
|
|
40
|
+
}): NotificationSetting | null {
|
|
41
|
+
if (obj?.metadata?.[notificationSettingKey]) {
|
|
42
|
+
return obj.metadata[notificationSettingKey];
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (obj?.subscription_data?.[notificationSettingKey]) {
|
|
46
|
+
return obj.subscription_data[notificationSettingKey];
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function getSettingIdFromObject(obj: { metadata?: Record<string, any> }): string | null {
|
|
53
|
+
return obj?.metadata?.[settingIdKey] || null;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export async function getNotificationSettingFromSubscription(
|
|
57
|
+
subscription: Subscription
|
|
58
|
+
): Promise<NotificationSetting | null> {
|
|
59
|
+
if (!subscription) {
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
const directSetting = getDirectNotificationSetting(subscription);
|
|
63
|
+
if (directSetting) {
|
|
64
|
+
return directSetting;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const settingId = getSettingIdFromObject(subscription);
|
|
68
|
+
if (settingId) {
|
|
69
|
+
return getSettingForNotification(settingId);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const checkoutSession = await CheckoutSession.findBySubscriptionId(subscription.id);
|
|
73
|
+
return checkoutSession ? getNotificationSettingFromCheckoutSession(checkoutSession) : null;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export async function getNotificationSettingFromCheckoutSession(
|
|
77
|
+
checkoutSession: CheckoutSession
|
|
78
|
+
): Promise<NotificationSetting | null> {
|
|
79
|
+
if (!checkoutSession) {
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
const directSetting = getDirectNotificationSetting(checkoutSession);
|
|
83
|
+
if (directSetting) {
|
|
84
|
+
return directSetting;
|
|
85
|
+
}
|
|
86
|
+
const settingId = getSettingIdFromObject(checkoutSession);
|
|
87
|
+
if (settingId) {
|
|
88
|
+
const settings = await getSettingForNotification(settingId);
|
|
89
|
+
return settings || null;
|
|
90
|
+
}
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export async function getNotificationSettingFromInvoice(invoice: Invoice): Promise<NotificationSetting | null> {
|
|
95
|
+
if (!invoice) {
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
const directSetting = getDirectNotificationSetting(invoice);
|
|
99
|
+
if (directSetting) {
|
|
100
|
+
return directSetting;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const settingId = getSettingIdFromObject(invoice);
|
|
104
|
+
if (settingId) {
|
|
105
|
+
return getSettingForNotification(settingId);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (invoice.subscription_id) {
|
|
109
|
+
const subscription = await Subscription.findByPk(invoice.subscription_id);
|
|
110
|
+
if (subscription) {
|
|
111
|
+
return getNotificationSettingFromSubscription(subscription);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (invoice.checkout_session_id) {
|
|
116
|
+
const checkoutSession = await CheckoutSession.findByPk(invoice.checkout_session_id);
|
|
117
|
+
if (checkoutSession) {
|
|
118
|
+
return getNotificationSettingFromCheckoutSession(checkoutSession);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return null;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export type NotificationSettingsProps = {
|
|
125
|
+
subscription?: Subscription;
|
|
126
|
+
invoice?: Invoice;
|
|
127
|
+
checkoutSession?: CheckoutSession;
|
|
128
|
+
};
|
|
129
|
+
export async function getNotificationSettings({
|
|
130
|
+
subscription,
|
|
131
|
+
invoice,
|
|
132
|
+
checkoutSession,
|
|
133
|
+
}: NotificationSettingsProps): Promise<NotificationSetting | null> {
|
|
134
|
+
if (subscription) {
|
|
135
|
+
const settings = await getNotificationSettingFromSubscription(subscription);
|
|
136
|
+
return settings || null;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (checkoutSession) {
|
|
140
|
+
const settings = await getNotificationSettingFromCheckoutSession(checkoutSession);
|
|
141
|
+
return settings || null;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (invoice) {
|
|
145
|
+
const settings = await getNotificationSettingFromInvoice(invoice);
|
|
146
|
+
return settings || null;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return null;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export function shouldSendSystemNotification(eventType: string, settings?: NotificationSetting | null): boolean {
|
|
153
|
+
if (!settings) {
|
|
154
|
+
return true;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const { include_events: includeEvents, exclude_events: excludeEvents, self_handle: selfHandle } = settings;
|
|
158
|
+
if (!selfHandle) {
|
|
159
|
+
// if self_handle is false, then the notification is sent by the system
|
|
160
|
+
return true;
|
|
161
|
+
}
|
|
162
|
+
if (includeEvents && includeEvents.length > 0) {
|
|
163
|
+
const isIncluded = includeEvents.includes(eventType as EventType);
|
|
164
|
+
return !isIncluded;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if (excludeEvents && excludeEvents.length > 0) {
|
|
168
|
+
const isExcluded = excludeEvents.includes(eventType as EventType);
|
|
169
|
+
return isExcluded;
|
|
170
|
+
}
|
|
171
|
+
return !selfHandle;
|
|
172
|
+
}
|
|
@@ -30,7 +30,12 @@ import { createEvent } from './audit';
|
|
|
30
30
|
import dayjs from './dayjs';
|
|
31
31
|
import env from './env';
|
|
32
32
|
import logger from './logger';
|
|
33
|
-
import {
|
|
33
|
+
import {
|
|
34
|
+
getPriceCurrencyOptions,
|
|
35
|
+
getPriceUintAmountByCurrency,
|
|
36
|
+
getRecurringPeriod,
|
|
37
|
+
getSubscriptionCreateSetup,
|
|
38
|
+
} from './session';
|
|
34
39
|
import { getConnectQueryParam, getCustomerStakeAddress } from './util';
|
|
35
40
|
import { wallet } from './auth';
|
|
36
41
|
import { getGasPayerExtra } from './payment';
|
|
@@ -184,66 +189,6 @@ export function getSubscriptionStakeSetup(items: TLineItemExpanded[], currencyId
|
|
|
184
189
|
return staking;
|
|
185
190
|
}
|
|
186
191
|
|
|
187
|
-
export function getSubscriptionCreateSetup(
|
|
188
|
-
items: TLineItemExpanded[],
|
|
189
|
-
currencyId: string,
|
|
190
|
-
trialInDays = 0,
|
|
191
|
-
trialEnd = 0
|
|
192
|
-
) {
|
|
193
|
-
let setup = new BN(0);
|
|
194
|
-
|
|
195
|
-
items.forEach((x) => {
|
|
196
|
-
const price = getSubscriptionItemPrice(x);
|
|
197
|
-
const unit = getPriceUintAmountByCurrency(price, currencyId);
|
|
198
|
-
const amount = new BN(unit).mul(new BN(x.quantity));
|
|
199
|
-
if (price.type === 'recurring') {
|
|
200
|
-
if (price.recurring?.usage_type === 'licensed') {
|
|
201
|
-
setup = setup.add(amount);
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
|
-
if (price.type === 'one_time') {
|
|
205
|
-
setup = setup.add(amount);
|
|
206
|
-
}
|
|
207
|
-
});
|
|
208
|
-
|
|
209
|
-
const now = dayjs().unix();
|
|
210
|
-
const item = items.find((x) => getSubscriptionItemPrice(x).type === 'recurring');
|
|
211
|
-
const recurring = (item?.upsell_price || item?.price)?.recurring as PriceRecurring;
|
|
212
|
-
const cycle = getRecurringPeriod(recurring);
|
|
213
|
-
|
|
214
|
-
let trialStartAt = 0;
|
|
215
|
-
let trialEndAt = 0;
|
|
216
|
-
if (+trialEnd && trialEnd > now) {
|
|
217
|
-
trialStartAt = now;
|
|
218
|
-
trialEndAt = trialEnd;
|
|
219
|
-
} else if (trialInDays) {
|
|
220
|
-
trialStartAt = now;
|
|
221
|
-
trialEndAt = dayjs().add(trialInDays, 'day').unix();
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
const periodStart = trialStartAt || now;
|
|
225
|
-
const periodEnd = trialEndAt || dayjs().add(cycle, 'millisecond').unix();
|
|
226
|
-
|
|
227
|
-
return {
|
|
228
|
-
recurring,
|
|
229
|
-
cycle: {
|
|
230
|
-
duration: cycle,
|
|
231
|
-
anchor: periodEnd,
|
|
232
|
-
},
|
|
233
|
-
trial: {
|
|
234
|
-
start: trialStartAt,
|
|
235
|
-
end: trialEndAt,
|
|
236
|
-
},
|
|
237
|
-
period: {
|
|
238
|
-
start: periodStart,
|
|
239
|
-
end: periodEnd,
|
|
240
|
-
},
|
|
241
|
-
amount: {
|
|
242
|
-
setup: setup.toString(),
|
|
243
|
-
},
|
|
244
|
-
};
|
|
245
|
-
}
|
|
246
|
-
|
|
247
192
|
export function getSubscriptionCycleSetup(recurring: PriceRecurring, previousPeriodEnd: number) {
|
|
248
193
|
const cycle = getRecurringPeriod(recurring);
|
|
249
194
|
|
|
@@ -262,7 +207,7 @@ export function getSubscriptionCycleAmount(items: TLineItemExpanded[], currencyI
|
|
|
262
207
|
|
|
263
208
|
items.forEach((x) => {
|
|
264
209
|
amount = amount.add(
|
|
265
|
-
new BN(getPriceUintAmountByCurrency(getSubscriptionItemPrice(x), currencyId)).mul(new BN(x.quantity))
|
|
210
|
+
new BN(getPriceUintAmountByCurrency(getSubscriptionItemPrice(x) as any, currencyId)).mul(new BN(x.quantity))
|
|
266
211
|
);
|
|
267
212
|
});
|
|
268
213
|
|
|
@@ -458,13 +403,6 @@ export function shouldCancelSubscription(
|
|
|
458
403
|
return false;
|
|
459
404
|
}
|
|
460
405
|
|
|
461
|
-
export function formatSubscriptionProduct(items: TLineItemExpanded[], maxLength = 3) {
|
|
462
|
-
const names = items.map((x) => x.price?.product?.name).filter(Boolean);
|
|
463
|
-
return (
|
|
464
|
-
names.slice(0, maxLength).join(', ') + (names.length > maxLength ? ` and ${names.length - maxLength} more` : '')
|
|
465
|
-
);
|
|
466
|
-
}
|
|
467
|
-
|
|
468
406
|
export async function canChangePaymentMethod(subscriptionId: string) {
|
|
469
407
|
const item = await SubscriptionItem.findOne({ where: { subscription_id: subscriptionId } });
|
|
470
408
|
const expanded = await Price.findOne({ where: { id: item!.price_id } });
|
package/api/src/libs/ws.ts
CHANGED
|
@@ -88,4 +88,9 @@ export function initEventBroadcast() {
|
|
|
88
88
|
events.on('customer.subscription.trial_end', (data: Subscription, extraParams?: Record<string, any>) => {
|
|
89
89
|
broadcast('customer.subscription.trial_end', data, extraParams);
|
|
90
90
|
});
|
|
91
|
+
|
|
92
|
+
// notification events
|
|
93
|
+
events.on('manual.notification', (data: Record<string, any>) => {
|
|
94
|
+
broadcast('manual.notification', data);
|
|
95
|
+
});
|
|
91
96
|
}
|
|
@@ -20,6 +20,7 @@ import {
|
|
|
20
20
|
Subscription,
|
|
21
21
|
SubscriptionItem,
|
|
22
22
|
} from '../store/models';
|
|
23
|
+
import { getCheckoutSessionSubscriptionIds } from '../libs/session';
|
|
23
24
|
|
|
24
25
|
type CheckoutSessionJob = {
|
|
25
26
|
id: string;
|
|
@@ -194,44 +195,49 @@ events.on('checkout.session.expired', async (checkoutSession: CheckoutSession) =
|
|
|
194
195
|
});
|
|
195
196
|
}
|
|
196
197
|
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
const
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
198
|
+
const subscriptionIds = getCheckoutSessionSubscriptionIds(checkoutSession);
|
|
199
|
+
if (subscriptionIds.length > 0) {
|
|
200
|
+
const subscriptions = await Subscription.findAll({ where: { id: { [Op.in]: subscriptionIds } } });
|
|
201
|
+
await Promise.all(
|
|
202
|
+
subscriptions.map(async (subscription) => {
|
|
203
|
+
const stripeSubscriptionId = subscription?.payment_details?.stripe?.subscription_id;
|
|
204
|
+
if (subscription && stripeSubscriptionId) {
|
|
205
|
+
const method = await PaymentMethod.findByPk(subscription.default_payment_method_id);
|
|
206
|
+
if (method?.type === 'stripe') {
|
|
207
|
+
const client = method.getStripeClient();
|
|
208
|
+
try {
|
|
209
|
+
await client.subscriptions.cancel(stripeSubscriptionId, {
|
|
210
|
+
prorate: false,
|
|
211
|
+
invoice_now: false,
|
|
212
|
+
cancellation_details: {
|
|
213
|
+
comment: 'checkout_session_expired',
|
|
214
|
+
feedback: 'unused',
|
|
215
|
+
},
|
|
216
|
+
});
|
|
217
|
+
logger.info('Stripe Subscription for checkout session canceled on expire', {
|
|
218
|
+
checkoutSession: checkoutSession.id,
|
|
219
|
+
subscription: checkoutSession.subscription_id,
|
|
220
|
+
stripeSubscription: stripeSubscriptionId,
|
|
221
|
+
});
|
|
222
|
+
} catch (err) {
|
|
223
|
+
logger.error('Stripe Subscription for checkout session cancel failed on expire', {
|
|
224
|
+
checkoutSession: checkoutSession.id,
|
|
225
|
+
subscription: checkoutSession.subscription_id,
|
|
226
|
+
stripeSubscription: stripeSubscriptionId,
|
|
227
|
+
error: err,
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
}
|
|
225
231
|
}
|
|
226
|
-
}
|
|
227
|
-
}
|
|
228
232
|
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
233
|
+
await SubscriptionItem.destroy({ where: { subscription_id: subscription.id } });
|
|
234
|
+
await Subscription.destroy({ where: { id: subscription.id } });
|
|
235
|
+
logger.info('Subscription and SubscriptionItem for checkout session deleted on expire', {
|
|
236
|
+
checkoutSession: checkoutSession.id,
|
|
237
|
+
subscription: subscription.id,
|
|
238
|
+
});
|
|
239
|
+
})
|
|
240
|
+
);
|
|
235
241
|
}
|
|
236
242
|
|
|
237
243
|
// update price lock status
|
|
@@ -235,7 +235,8 @@ function getNotificationTemplate(job: NotificationQueueJob): BaseEmailTemplate {
|
|
|
235
235
|
async function handleNotificationJob(job: NotificationQueueJob): Promise<void> {
|
|
236
236
|
try {
|
|
237
237
|
const template = getNotificationTemplate(job);
|
|
238
|
-
|
|
238
|
+
|
|
239
|
+
await new Notification(template, job.type).send();
|
|
239
240
|
logger.info('handleImmediateNotificationJob.success', { job });
|
|
240
241
|
} catch (error) {
|
|
241
242
|
logger.error('handleImmediateNotificationJob.error', error);
|
|
@@ -384,7 +385,7 @@ function getAggregatedNotificationTemplate(job: AggregatedNotificationJob): Base
|
|
|
384
385
|
async function handleAggregatedNotificationJob(job: AggregatedNotificationJob): Promise<void> {
|
|
385
386
|
try {
|
|
386
387
|
const template = await getAggregatedNotificationTemplate(job);
|
|
387
|
-
await new Notification(template).send();
|
|
388
|
+
await new Notification(template, job.type).send();
|
|
388
389
|
logger.info('handleAggregatedNotificationJob.success', { job });
|
|
389
390
|
} catch (error) {
|
|
390
391
|
logger.error('handleAggregatedNotificationJob.error', error);
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import isEmpty from 'lodash/isEmpty';
|
|
2
2
|
|
|
3
3
|
import { BN } from '@ocap/util';
|
|
4
|
+
import pAll from 'p-all';
|
|
4
5
|
import { ensureStakedForGas } from '../integrations/arcblock/stake';
|
|
5
6
|
import { transferErc20FromUser } from '../integrations/ethereum/token';
|
|
6
7
|
import { createEvent } from '../libs/audit';
|
|
@@ -17,7 +18,6 @@ import {
|
|
|
17
18
|
getDueUnit,
|
|
18
19
|
getMaxRetryCount,
|
|
19
20
|
getMinRetryMail,
|
|
20
|
-
getSubscriptionCreateSetup,
|
|
21
21
|
getSubscriptionStakeAddress,
|
|
22
22
|
isSubscriptionOverdraftProtectionEnabled,
|
|
23
23
|
shouldCancelSubscription,
|
|
@@ -39,7 +39,8 @@ import { ensureOverdraftProtectionInvoiceAndItems } from '../libs/invoice';
|
|
|
39
39
|
import { Lock } from '../store/models';
|
|
40
40
|
import { ensureOverdraftProtectionPrice } from '../libs/overdraft-protection';
|
|
41
41
|
import createQueue from '../libs/queue';
|
|
42
|
-
import { EVM_CHAIN_TYPES } from '../libs/constants';
|
|
42
|
+
import { CHARGE_SUPPORTED_CHAIN_TYPES, EVM_CHAIN_TYPES } from '../libs/constants';
|
|
43
|
+
import { getCheckoutSessionSubscriptionIds, getSubscriptionCreateSetup } from '../libs/session';
|
|
43
44
|
|
|
44
45
|
type PaymentJob = {
|
|
45
46
|
paymentIntentId: string;
|
|
@@ -231,6 +232,19 @@ export const handlePaymentSucceed = async (
|
|
|
231
232
|
if (isEmpty(result)) {
|
|
232
233
|
// reset billing cycle anchor and cancel_* if we are recovering from payment failed
|
|
233
234
|
if (subscription.cancel_at && subscription.cancel_at !== subscription.current_period_end) {
|
|
235
|
+
const now = dayjs().unix();
|
|
236
|
+
if (now <= subscription.current_period_end) {
|
|
237
|
+
// if payment succeeds before current_period_end, we should active this subscription
|
|
238
|
+
await subscription.update({
|
|
239
|
+
status: 'active',
|
|
240
|
+
cancel_at: 0,
|
|
241
|
+
cancel_at_period_end: false,
|
|
242
|
+
// @ts-ignore
|
|
243
|
+
cancelation_details: null,
|
|
244
|
+
});
|
|
245
|
+
logger.info(`Subscription ${subscription.id} recovered on payment done ${paymentIntent.id}: cancel rest`);
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
234
248
|
const subscriptionItems = await SubscriptionItem.findAll({ where: { subscription_id: subscription.id } });
|
|
235
249
|
const lineItems = await Price.expand(subscriptionItems.map((x) => x.toJSON()));
|
|
236
250
|
const setup = getSubscriptionCreateSetup(lineItems, subscription.currency_id, 0);
|
|
@@ -287,11 +301,24 @@ export const handlePaymentSucceed = async (
|
|
|
287
301
|
checkoutSessionId: checkoutSession.id,
|
|
288
302
|
});
|
|
289
303
|
});
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
304
|
+
if (['subscription', 'setup'].includes(checkoutSession.mode) && invoice.subscription_id) {
|
|
305
|
+
await checkoutSession.increment('success_subscription_count', { by: 1 });
|
|
306
|
+
await checkoutSession.reload();
|
|
307
|
+
const subscriptionIds = getCheckoutSessionSubscriptionIds(checkoutSession);
|
|
308
|
+
if (checkoutSession.success_subscription_count === subscriptionIds.length) {
|
|
309
|
+
await checkoutSession.update({
|
|
310
|
+
status: 'complete',
|
|
311
|
+
payment_status: 'paid',
|
|
312
|
+
payment_details: paymentIntent.payment_details,
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
} else {
|
|
316
|
+
await checkoutSession.update({
|
|
317
|
+
status: 'complete',
|
|
318
|
+
payment_status: 'paid',
|
|
319
|
+
payment_details: paymentIntent.payment_details,
|
|
320
|
+
});
|
|
321
|
+
}
|
|
295
322
|
logger.info(`CheckoutSession ${checkoutSession.id} updated on payment done ${paymentIntent.id}`);
|
|
296
323
|
}
|
|
297
324
|
}
|
|
@@ -509,6 +536,7 @@ export const handlePaymentFailed = async (
|
|
|
509
536
|
logger.warn('Subscription moved to past_due after payment failed', {
|
|
510
537
|
subscription: subscription.id,
|
|
511
538
|
payment: paymentIntent.id,
|
|
539
|
+
cancelUpdates,
|
|
512
540
|
gracePeriodStart,
|
|
513
541
|
graceDuration,
|
|
514
542
|
dueUnit,
|
|
@@ -802,14 +830,16 @@ export const handlePayment = async (job: PaymentJob) => {
|
|
|
802
830
|
wallet,
|
|
803
831
|
delegator: result.delegator,
|
|
804
832
|
});
|
|
833
|
+
logger.info('PaymentIntent signed', { signed });
|
|
805
834
|
// @ts-ignore
|
|
806
835
|
const { buffer } = await client.encodeTransferV2Tx({ tx: signed });
|
|
836
|
+
logger.info('PaymentIntent buffer', { buffer, gas: getGasPayerExtra(buffer) });
|
|
807
837
|
const txHash = await client.sendTransferV2Tx(
|
|
808
838
|
// @ts-ignore
|
|
809
839
|
{ tx: signed, wallet, delegator: result.delegator },
|
|
810
840
|
getGasPayerExtra(buffer)
|
|
811
841
|
);
|
|
812
|
-
|
|
842
|
+
logger.info('PaymentIntent txHash', { txHash });
|
|
813
843
|
logger.info('PaymentIntent capture done', { id: paymentIntent.id, txHash });
|
|
814
844
|
|
|
815
845
|
await paymentIntent.update({
|
|
@@ -1022,3 +1052,21 @@ events.on('payment.queued', async (id, job, args = {}) => {
|
|
|
1022
1052
|
events.emit('payment.queued.error', { id, job, error });
|
|
1023
1053
|
}
|
|
1024
1054
|
});
|
|
1055
|
+
|
|
1056
|
+
export async function startDepositVaultQueue() {
|
|
1057
|
+
logger.debug('startDepositVaultQueue');
|
|
1058
|
+
const paymentCurrencies = (await PaymentCurrency.scope('withVaultConfig').findAll({
|
|
1059
|
+
include: [{ model: PaymentMethod, as: 'payment_method' }],
|
|
1060
|
+
})) as (PaymentCurrency & { payment_method: PaymentMethod })[];
|
|
1061
|
+
await pAll(
|
|
1062
|
+
paymentCurrencies.map((x) => {
|
|
1063
|
+
if (CHARGE_SUPPORTED_CHAIN_TYPES.includes(x.payment_method.type) && x.vault_config?.enabled === true) {
|
|
1064
|
+
depositVaultQueue.push({ id: `deposit-vault-${x.id}`, job: { currencyId: x.id } });
|
|
1065
|
+
}
|
|
1066
|
+
return async () => {};
|
|
1067
|
+
}),
|
|
1068
|
+
{
|
|
1069
|
+
concurrency: 5,
|
|
1070
|
+
}
|
|
1071
|
+
);
|
|
1072
|
+
}
|
|
@@ -5,15 +5,7 @@ import { getLock } from '../libs/lock';
|
|
|
5
5
|
import logger from '../libs/logger';
|
|
6
6
|
import createQueue from '../libs/queue';
|
|
7
7
|
import { getPriceUintAmountByCurrency } from '../libs/session';
|
|
8
|
-
import {
|
|
9
|
-
Invoice,
|
|
10
|
-
PaymentCurrency,
|
|
11
|
-
Price,
|
|
12
|
-
SubscriptionItem,
|
|
13
|
-
TLineItemExpanded,
|
|
14
|
-
TPrice,
|
|
15
|
-
UsageRecord,
|
|
16
|
-
} from '../store/models';
|
|
8
|
+
import { Invoice, PaymentCurrency, Price, SubscriptionItem, TLineItemExpanded, UsageRecord } from '../store/models';
|
|
17
9
|
import { Subscription } from '../store/models/subscription';
|
|
18
10
|
import { invoiceQueue } from './invoice';
|
|
19
11
|
import { handleSubscriptionInvoice } from './subscription';
|
|
@@ -103,7 +95,7 @@ export const doHandleUsageRecord = async (job: UsageRecordJob) => {
|
|
|
103
95
|
});
|
|
104
96
|
// @ts-ignore
|
|
105
97
|
const quantity = expanded?.price.transformQuantity(rawQuantity);
|
|
106
|
-
const unitAmount = getPriceUintAmountByCurrency(expanded?.price
|
|
98
|
+
const unitAmount = expanded?.price ? getPriceUintAmountByCurrency(expanded?.price, subscription.currency_id) : 0;
|
|
107
99
|
const totalAmount = new BN(quantity).mul(new BN(unitAmount));
|
|
108
100
|
const threshold = fromTokenToUnit(subscription.billing_thresholds.amount_gte, currency.decimal);
|
|
109
101
|
logger.info('SubscriptionItem Usage check', {
|