payment-kit 1.18.30 → 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/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/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 +47 -14
- 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 +33 -6
- 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 +3 -6
- 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 +31 -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 +20 -19
- package/scripts/sdk.js +486 -155
- package/src/locales/en.tsx +1 -1
- package/src/locales/zh.tsx +1 -1
- package/src/pages/admin/settings/vault-config/edit-form.tsx +1 -1
- package/src/pages/customer/subscription/change-payment.tsx +8 -3
|
@@ -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);
|
|
@@ -18,7 +18,6 @@ import {
|
|
|
18
18
|
getDueUnit,
|
|
19
19
|
getMaxRetryCount,
|
|
20
20
|
getMinRetryMail,
|
|
21
|
-
getSubscriptionCreateSetup,
|
|
22
21
|
getSubscriptionStakeAddress,
|
|
23
22
|
isSubscriptionOverdraftProtectionEnabled,
|
|
24
23
|
shouldCancelSubscription,
|
|
@@ -41,6 +40,7 @@ import { Lock } from '../store/models';
|
|
|
41
40
|
import { ensureOverdraftProtectionPrice } from '../libs/overdraft-protection';
|
|
42
41
|
import createQueue from '../libs/queue';
|
|
43
42
|
import { CHARGE_SUPPORTED_CHAIN_TYPES, EVM_CHAIN_TYPES } from '../libs/constants';
|
|
43
|
+
import { getCheckoutSessionSubscriptionIds, getSubscriptionCreateSetup } from '../libs/session';
|
|
44
44
|
|
|
45
45
|
type PaymentJob = {
|
|
46
46
|
paymentIntentId: string;
|
|
@@ -232,6 +232,19 @@ export const handlePaymentSucceed = async (
|
|
|
232
232
|
if (isEmpty(result)) {
|
|
233
233
|
// reset billing cycle anchor and cancel_* if we are recovering from payment failed
|
|
234
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
|
+
}
|
|
235
248
|
const subscriptionItems = await SubscriptionItem.findAll({ where: { subscription_id: subscription.id } });
|
|
236
249
|
const lineItems = await Price.expand(subscriptionItems.map((x) => x.toJSON()));
|
|
237
250
|
const setup = getSubscriptionCreateSetup(lineItems, subscription.currency_id, 0);
|
|
@@ -288,11 +301,24 @@ export const handlePaymentSucceed = async (
|
|
|
288
301
|
checkoutSessionId: checkoutSession.id,
|
|
289
302
|
});
|
|
290
303
|
});
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
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
|
+
}
|
|
296
322
|
logger.info(`CheckoutSession ${checkoutSession.id} updated on payment done ${paymentIntent.id}`);
|
|
297
323
|
}
|
|
298
324
|
}
|
|
@@ -510,6 +536,7 @@ export const handlePaymentFailed = async (
|
|
|
510
536
|
logger.warn('Subscription moved to past_due after payment failed', {
|
|
511
537
|
subscription: subscription.id,
|
|
512
538
|
payment: paymentIntent.id,
|
|
539
|
+
cancelUpdates,
|
|
513
540
|
gracePeriodStart,
|
|
514
541
|
graceDuration,
|
|
515
542
|
dueUnit,
|
|
@@ -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', {
|