payment-kit 1.18.30 → 1.18.32
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
|
@@ -36,6 +36,12 @@ interface MeteringSubscriptionDetectionContext {
|
|
|
36
36
|
|
|
37
37
|
export class MeteringSubscriptionDetectionTemplate implements BaseEmailTemplate<MeteringSubscriptionDetectionContext> {
|
|
38
38
|
private timeRange: { start: number; end: number };
|
|
39
|
+
options: {
|
|
40
|
+
timeRange: {
|
|
41
|
+
start: number;
|
|
42
|
+
end: number;
|
|
43
|
+
};
|
|
44
|
+
};
|
|
39
45
|
|
|
40
46
|
constructor() {
|
|
41
47
|
const end = dayjs();
|
|
@@ -44,6 +50,9 @@ export class MeteringSubscriptionDetectionTemplate implements BaseEmailTemplate<
|
|
|
44
50
|
start: start.unix(),
|
|
45
51
|
end: end.unix(),
|
|
46
52
|
};
|
|
53
|
+
this.options = {
|
|
54
|
+
timeRange: this.timeRange,
|
|
55
|
+
};
|
|
47
56
|
}
|
|
48
57
|
|
|
49
58
|
private async getAbnormalSubscriptions(meteringInvoiceIds: string[], meteringSubscriptionIds: string[]) {
|
|
@@ -108,6 +108,7 @@ export async function mintNftForCheckoutSession(id: string) {
|
|
|
108
108
|
},
|
|
109
109
|
});
|
|
110
110
|
|
|
111
|
+
// FIXME: checkoutSession has multiple subscriptions?
|
|
111
112
|
if (checkoutSession.subscription_id) {
|
|
112
113
|
const subscription = await Subscription.findByPk(checkoutSession.subscription_id);
|
|
113
114
|
if (subscription && subscription.metadata?.nft_address) {
|
|
@@ -109,7 +109,7 @@ export async function ensurePassportRevoked(subscription: Subscription) {
|
|
|
109
109
|
return;
|
|
110
110
|
}
|
|
111
111
|
|
|
112
|
-
const checkoutSession = await CheckoutSession.
|
|
112
|
+
const checkoutSession = await CheckoutSession.findBySubscriptionId(subscription.id);
|
|
113
113
|
if (!checkoutSession) {
|
|
114
114
|
logger.warn('checkoutSession for subscription not found', info);
|
|
115
115
|
return;
|
|
@@ -99,7 +99,7 @@ export async function ensureStripeInvoice(stripeInvoice: any, subscription: Subs
|
|
|
99
99
|
await lock.acquire();
|
|
100
100
|
|
|
101
101
|
const customer = await Customer.findByPk(subscription.customer_id);
|
|
102
|
-
const checkoutSession = await CheckoutSession.
|
|
102
|
+
const checkoutSession = await CheckoutSession.findBySubscriptionId(subscription.id);
|
|
103
103
|
|
|
104
104
|
let invoice = await Invoice.findOne({ where: { 'metadata.stripe_id': stripeInvoice.id } });
|
|
105
105
|
if (invoice) {
|
|
@@ -256,7 +256,7 @@ export async function handleStripeInvoiceCreated(event: TEventExpanded, client:
|
|
|
256
256
|
});
|
|
257
257
|
|
|
258
258
|
const customer = await Customer.findByPk(subscription.customer_id);
|
|
259
|
-
const checkoutSession = await CheckoutSession.
|
|
259
|
+
const checkoutSession = await CheckoutSession.findBySubscriptionId(subscription.id);
|
|
260
260
|
|
|
261
261
|
if (stripeInvoice.billing_reason === 'subscription_cycle') {
|
|
262
262
|
// check if usage report is empty
|
|
@@ -2,6 +2,7 @@ import type Stripe from 'stripe';
|
|
|
2
2
|
|
|
3
3
|
import logger from '../../../libs/logger';
|
|
4
4
|
import { CheckoutSession, Lock, SetupIntent, Subscription, TEventExpanded } from '../../../store/models';
|
|
5
|
+
import { updateGroupSubscriptionsPaymentMethod } from '../resource';
|
|
5
6
|
|
|
6
7
|
async function handleSubscriptionOnSetupSucceeded(event: TEventExpanded, stripeIntentId: string) {
|
|
7
8
|
const subscription = await Subscription.findOne({
|
|
@@ -19,7 +20,7 @@ async function handleSubscriptionOnSetupSucceeded(event: TEventExpanded, stripeI
|
|
|
19
20
|
logger.info('subscription become active on stripe intent succeeded', subscription.id);
|
|
20
21
|
}
|
|
21
22
|
|
|
22
|
-
const checkoutSession = await CheckoutSession.
|
|
23
|
+
const checkoutSession = await CheckoutSession.findBySubscriptionId(subscription.id);
|
|
23
24
|
if (checkoutSession && checkoutSession.status === 'open') {
|
|
24
25
|
await checkoutSession.update({ status: 'complete', payment_status: 'no_payment_required' });
|
|
25
26
|
logger.info('checkout session become complete on stripe intent succeeded', checkoutSession.id);
|
|
@@ -81,6 +82,32 @@ async function handleSetupIntentOnSetupSucceeded(event: TEventExpanded, stripeIn
|
|
|
81
82
|
}
|
|
82
83
|
}
|
|
83
84
|
|
|
85
|
+
async function handleCheckoutSessionOnSetupSucceeded(event: TEventExpanded, stripeIntentId: string) {
|
|
86
|
+
const checkoutSession = await CheckoutSession.findOne({
|
|
87
|
+
where: { 'payment_details.stripe.setup_intent_id': stripeIntentId },
|
|
88
|
+
});
|
|
89
|
+
if (!checkoutSession) {
|
|
90
|
+
logger.warn('local checkout session not found for setup intent', {
|
|
91
|
+
id: event.id,
|
|
92
|
+
type: event.type,
|
|
93
|
+
stripeIntentId,
|
|
94
|
+
});
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
logger.info('received setup intent event', { id: event.id, data: event.data, stripeIntentId, checkoutSession });
|
|
99
|
+
|
|
100
|
+
if (event.type === 'setup_intent.succeeded') {
|
|
101
|
+
const stripePaymentMethod = event.data.object.payment_method;
|
|
102
|
+
if (stripePaymentMethod && checkoutSession.subscription_groups) {
|
|
103
|
+
await updateGroupSubscriptionsPaymentMethod({
|
|
104
|
+
stripePaymentMethodId: stripePaymentMethod,
|
|
105
|
+
subscriptionIds: Object.values(checkoutSession.subscription_groups),
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
84
111
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
85
112
|
export async function handleSetupIntentEvent(event: TEventExpanded, _: Stripe) {
|
|
86
113
|
const stripeIntentId = event.data.object.id;
|
|
@@ -88,4 +115,5 @@ export async function handleSetupIntentEvent(event: TEventExpanded, _: Stripe) {
|
|
|
88
115
|
|
|
89
116
|
await handleSubscriptionOnSetupSucceeded(event, stripeIntentId);
|
|
90
117
|
await handleSetupIntentOnSetupSucceeded(event, stripeIntentId);
|
|
118
|
+
await handleCheckoutSessionOnSetupSucceeded(event, stripeIntentId);
|
|
91
119
|
}
|
|
@@ -4,6 +4,7 @@ import type Stripe from 'stripe';
|
|
|
4
4
|
import logger from '../../../libs/logger';
|
|
5
5
|
import { finalizeStripeSubscriptionUpdate } from '../../../libs/subscription';
|
|
6
6
|
import { CheckoutSession, PaymentMethod, Subscription, TEventExpanded } from '../../../store/models';
|
|
7
|
+
import { getCheckoutSessionSubscriptionIds } from '../../../libs/session';
|
|
7
8
|
|
|
8
9
|
export async function handleStripeSubscriptionSucceed(subscription: Subscription, status: string) {
|
|
9
10
|
if (!subscription.payment_details?.stripe?.subscription_id) {
|
|
@@ -16,14 +17,9 @@ export async function handleStripeSubscriptionSucceed(subscription: Subscription
|
|
|
16
17
|
const result: any = await client.subscriptions.retrieve(subscription.payment_details.stripe.subscription_id, {
|
|
17
18
|
expand: ['latest_invoice.payment_intent', 'pending_setup_intent'],
|
|
18
19
|
});
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
status: result.pending_setup_intent.status,
|
|
23
|
-
});
|
|
24
|
-
return;
|
|
25
|
-
}
|
|
26
|
-
if (result.latest_invoice?.payment_intent && result.latest_invoice.payment_intent !== 'succeeded') {
|
|
20
|
+
const paymentIntent = result.latest_invoice?.payment_intent;
|
|
21
|
+
const paymentIntentStatus = typeof paymentIntent === 'string' ? paymentIntent : paymentIntent?.status;
|
|
22
|
+
if (result.latest_invoice?.payment_intent && paymentIntentStatus !== 'succeeded') {
|
|
27
23
|
logger.warn('subscription can not active because stripe payment not done', {
|
|
28
24
|
id: subscription.id,
|
|
29
25
|
status: result.latest_invoice.payment_intent.status,
|
|
@@ -34,14 +30,22 @@ export async function handleStripeSubscriptionSucceed(subscription: Subscription
|
|
|
34
30
|
await subscription.update({ status });
|
|
35
31
|
logger.info('subscription become active on stripe event', { id: subscription.id, status: subscription.status });
|
|
36
32
|
|
|
37
|
-
const checkoutSession = await CheckoutSession.
|
|
38
|
-
if (checkoutSession) {
|
|
39
|
-
await checkoutSession.
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
33
|
+
const checkoutSession = await CheckoutSession.findBySubscriptionId(subscription.id);
|
|
34
|
+
if (checkoutSession && checkoutSession.status === 'open') {
|
|
35
|
+
await checkoutSession.increment('success_subscription_count', { by: 1 });
|
|
36
|
+
await checkoutSession.reload();
|
|
37
|
+
const subscriptionIds = getCheckoutSessionSubscriptionIds(checkoutSession);
|
|
38
|
+
if (checkoutSession.success_subscription_count === subscriptionIds.length) {
|
|
39
|
+
await checkoutSession.update({
|
|
40
|
+
status: 'complete',
|
|
41
|
+
payment_status: 'paid',
|
|
42
|
+
payment_details: paymentIntent?.payment_details || null,
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
logger.info('checkout session become complete on stripe event', {
|
|
46
|
+
id: checkoutSession.id,
|
|
47
|
+
subscriptionId: subscription.id,
|
|
43
48
|
});
|
|
44
|
-
logger.info('checkout session become complete on stripe event', { id: checkoutSession.id });
|
|
45
49
|
}
|
|
46
50
|
}
|
|
47
51
|
|
|
@@ -11,6 +11,7 @@ import { getPriceUintAmountByCurrency } from '../../libs/session';
|
|
|
11
11
|
import { getSubscriptionItemPrice } from '../../libs/subscription';
|
|
12
12
|
import { sleep } from '../../libs/util';
|
|
13
13
|
import {
|
|
14
|
+
CheckoutSession,
|
|
14
15
|
Customer,
|
|
15
16
|
Invoice,
|
|
16
17
|
PaymentCurrency,
|
|
@@ -239,6 +240,31 @@ export async function ensureStripePaymentIntent(
|
|
|
239
240
|
return stripeIntent;
|
|
240
241
|
}
|
|
241
242
|
|
|
243
|
+
export async function ensureStripeSetupIntentForCheckoutSession(
|
|
244
|
+
checkoutSession: CheckoutSession,
|
|
245
|
+
method: PaymentMethod,
|
|
246
|
+
metadata: Record<string, string>
|
|
247
|
+
) {
|
|
248
|
+
const client = method.getStripeClient();
|
|
249
|
+
const customer = await ensureStripePaymentCustomer(checkoutSession, method);
|
|
250
|
+
const setupIntent = await client.setupIntents.create({
|
|
251
|
+
customer: customer.id,
|
|
252
|
+
payment_method_types: ['card'],
|
|
253
|
+
usage: 'off_session',
|
|
254
|
+
metadata,
|
|
255
|
+
});
|
|
256
|
+
await checkoutSession.update({
|
|
257
|
+
payment_details: {
|
|
258
|
+
...(checkoutSession.payment_details || {}),
|
|
259
|
+
stripe: {
|
|
260
|
+
...(checkoutSession.payment_details?.stripe || {}),
|
|
261
|
+
setup_intent_id: setupIntent.id,
|
|
262
|
+
},
|
|
263
|
+
},
|
|
264
|
+
});
|
|
265
|
+
return setupIntent;
|
|
266
|
+
}
|
|
267
|
+
|
|
242
268
|
export async function ensureStripeSubscription(
|
|
243
269
|
internal: Subscription,
|
|
244
270
|
method: PaymentMethod,
|
|
@@ -260,7 +286,7 @@ export async function ensureStripeSubscription(
|
|
|
260
286
|
const prices = await Promise.all(
|
|
261
287
|
items.map(async (x: any) => {
|
|
262
288
|
const price = x.upsell_price || x.price;
|
|
263
|
-
x.stripePrice = await ensureStripePrice(price
|
|
289
|
+
x.stripePrice = await ensureStripePrice(price, method, currency);
|
|
264
290
|
return x;
|
|
265
291
|
})
|
|
266
292
|
);
|
|
@@ -543,3 +569,57 @@ export async function batchHandleStripePayments() {
|
|
|
543
569
|
await sleep(1000);
|
|
544
570
|
}
|
|
545
571
|
}
|
|
572
|
+
|
|
573
|
+
export async function updateGroupSubscriptionsPaymentMethod(params: {
|
|
574
|
+
stripePaymentMethodId: string;
|
|
575
|
+
subscriptionIds: string[];
|
|
576
|
+
}) {
|
|
577
|
+
const { stripePaymentMethodId, subscriptionIds } = params;
|
|
578
|
+
try {
|
|
579
|
+
const updates = subscriptionIds.map(async (subId) => {
|
|
580
|
+
const subscription = (await Subscription.findByPk(subId, {
|
|
581
|
+
include: [{ model: PaymentMethod, as: 'paymentMethod' }],
|
|
582
|
+
})) as Subscription & { paymentMethod: PaymentMethod };
|
|
583
|
+
if (!subscription || !subscription.paymentMethod || !subscription.payment_details?.stripe?.subscription_id) {
|
|
584
|
+
return null;
|
|
585
|
+
}
|
|
586
|
+
const client = subscription.paymentMethod.getStripeClient();
|
|
587
|
+
let stripeSub = await client.subscriptions.retrieve(subscription.payment_details.stripe.subscription_id, {
|
|
588
|
+
expand: ['latest_invoice', 'latest_invoice.payment_intent'],
|
|
589
|
+
});
|
|
590
|
+
if (stripeSub.status !== 'incomplete') {
|
|
591
|
+
return null;
|
|
592
|
+
}
|
|
593
|
+
const updatedSub = await client.subscriptions.update(stripeSub.id, {
|
|
594
|
+
default_payment_method: stripePaymentMethodId,
|
|
595
|
+
payment_behavior: 'default_incomplete',
|
|
596
|
+
});
|
|
597
|
+
|
|
598
|
+
stripeSub = await client.subscriptions.retrieve(updatedSub.id, {
|
|
599
|
+
expand: ['latest_invoice', 'latest_invoice.payment_intent'],
|
|
600
|
+
});
|
|
601
|
+
// if the latest invoice is not paid, pay it
|
|
602
|
+
// @ts-ignore
|
|
603
|
+
if (stripeSub.latest_invoice && stripeSub.latest_invoice?.status !== 'paid') {
|
|
604
|
+
// @ts-ignore
|
|
605
|
+
await client.invoices.pay(stripeSub.latest_invoice.id);
|
|
606
|
+
}
|
|
607
|
+
return updatedSub;
|
|
608
|
+
});
|
|
609
|
+
|
|
610
|
+
const updatedSubscriptions = await Promise.all(updates);
|
|
611
|
+
logger.info('Updated group subscriptions payment method', {
|
|
612
|
+
paymentMethod: stripePaymentMethodId,
|
|
613
|
+
subscriptions: subscriptionIds,
|
|
614
|
+
});
|
|
615
|
+
|
|
616
|
+
return updatedSubscriptions;
|
|
617
|
+
} catch (error) {
|
|
618
|
+
logger.error('Failed to update group subscriptions payment method', {
|
|
619
|
+
error,
|
|
620
|
+
paymentMethod: stripePaymentMethodId,
|
|
621
|
+
subscriptionIds,
|
|
622
|
+
});
|
|
623
|
+
throw error;
|
|
624
|
+
}
|
|
625
|
+
}
|
package/api/src/libs/audit.ts
CHANGED
|
@@ -118,3 +118,45 @@ export async function createCustomEvent(
|
|
|
118
118
|
events.emit('event.created', { id: event.id });
|
|
119
119
|
events.emit(event.type, data.object);
|
|
120
120
|
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* 创建自定义事件,无需依赖模型对象
|
|
124
|
+
* @param type 完整的事件类型,格式为 prefix.suffix
|
|
125
|
+
* @param objectType 对象类型
|
|
126
|
+
* @param objectId 对象ID
|
|
127
|
+
* @param data 事件数据
|
|
128
|
+
* @param options 额外选项
|
|
129
|
+
*/
|
|
130
|
+
export async function createFlexibleEvent(
|
|
131
|
+
type: string,
|
|
132
|
+
objectType: string,
|
|
133
|
+
objectId: string,
|
|
134
|
+
data: Record<string, any>,
|
|
135
|
+
options: {
|
|
136
|
+
livemode?: boolean;
|
|
137
|
+
requestedBy?: string;
|
|
138
|
+
metadata?: Record<string, any>;
|
|
139
|
+
} = {}
|
|
140
|
+
) {
|
|
141
|
+
const { livemode = false, requestedBy, metadata = {} } = options;
|
|
142
|
+
|
|
143
|
+
const event = await Event.create({
|
|
144
|
+
type,
|
|
145
|
+
api_version: API_VERSION,
|
|
146
|
+
livemode,
|
|
147
|
+
object_id: objectId,
|
|
148
|
+
object_type: objectType,
|
|
149
|
+
data,
|
|
150
|
+
request: {
|
|
151
|
+
id: '',
|
|
152
|
+
idempotency_key: '',
|
|
153
|
+
requested_by: requestedBy || context.getRequestedBy() || 'system',
|
|
154
|
+
},
|
|
155
|
+
metadata,
|
|
156
|
+
pending_webhooks: 99, // force all events goto the event queue
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
events.emit('event.created', { id: event.id });
|
|
160
|
+
events.emit(type, data);
|
|
161
|
+
return event;
|
|
162
|
+
}
|
package/api/src/libs/invoice.ts
CHANGED
|
@@ -3,7 +3,7 @@ import type { LiteralUnion } from 'type-fest';
|
|
|
3
3
|
import { withQuery } from 'ufo';
|
|
4
4
|
|
|
5
5
|
import { BN, fromUnitToToken } from '@ocap/util';
|
|
6
|
-
import { Op } from 'sequelize';
|
|
6
|
+
import { Op, type WhereOptions } from 'sequelize';
|
|
7
7
|
import { cloneDeep, pick } from 'lodash';
|
|
8
8
|
import {
|
|
9
9
|
Customer,
|
|
@@ -523,6 +523,7 @@ export async function ensureInvoiceAndItems({
|
|
|
523
523
|
customer,
|
|
524
524
|
currency,
|
|
525
525
|
subscription,
|
|
526
|
+
subscriptions,
|
|
526
527
|
props,
|
|
527
528
|
lineItems,
|
|
528
529
|
trialing,
|
|
@@ -532,6 +533,7 @@ export async function ensureInvoiceAndItems({
|
|
|
532
533
|
customer: Customer;
|
|
533
534
|
currency: PaymentCurrency;
|
|
534
535
|
subscription?: Subscription;
|
|
536
|
+
subscriptions?: Subscription[];
|
|
535
537
|
props: TInvoice;
|
|
536
538
|
lineItems: TLineItemExpanded[];
|
|
537
539
|
trialing: boolean; // do we have trialing
|
|
@@ -549,9 +551,16 @@ export async function ensureInvoiceAndItems({
|
|
|
549
551
|
}
|
|
550
552
|
|
|
551
553
|
// get subscription items
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
554
|
+
let subscriptionItems: SubscriptionItem[] = [];
|
|
555
|
+
if (subscriptions && subscriptions.length > 0) {
|
|
556
|
+
subscriptionItems = await SubscriptionItem.findAll({
|
|
557
|
+
where: { subscription_id: { [Op.in]: subscriptions.map((s) => s.id) } },
|
|
558
|
+
});
|
|
559
|
+
} else if (subscription) {
|
|
560
|
+
subscriptionItems = await SubscriptionItem.findAll({
|
|
561
|
+
where: { subscription_id: subscription?.id },
|
|
562
|
+
});
|
|
563
|
+
}
|
|
555
564
|
|
|
556
565
|
function getLineSetup(x: TLineItemExpanded) {
|
|
557
566
|
const price = getSubscriptionItemPrice(x);
|
|
@@ -794,13 +803,25 @@ export async function ensureStakeInvoice(
|
|
|
794
803
|
},
|
|
795
804
|
subscription: Subscription,
|
|
796
805
|
paymentMethod: PaymentMethod,
|
|
797
|
-
customer: Customer
|
|
806
|
+
customer: Customer,
|
|
807
|
+
allSubscriptions?: Subscription[]
|
|
798
808
|
) {
|
|
799
809
|
if (paymentMethod.type !== 'arcblock') {
|
|
800
810
|
return;
|
|
801
811
|
}
|
|
802
812
|
|
|
803
813
|
try {
|
|
814
|
+
const isMultiSubscription = allSubscriptions && allSubscriptions.length > 1;
|
|
815
|
+
const metadata = {
|
|
816
|
+
...(invoiceProps.metadata || {}),
|
|
817
|
+
};
|
|
818
|
+
|
|
819
|
+
if (isMultiSubscription) {
|
|
820
|
+
metadata.all_subscription_ids = allSubscriptions.map((s) => s.id);
|
|
821
|
+
metadata.is_shared_stake = true;
|
|
822
|
+
metadata.primary_subscription_id = subscription.id;
|
|
823
|
+
}
|
|
824
|
+
|
|
804
825
|
const { invoice } = await createInvoiceWithItems({
|
|
805
826
|
customer,
|
|
806
827
|
subscription,
|
|
@@ -817,7 +838,7 @@ export async function ensureStakeInvoice(
|
|
|
817
838
|
amount_remaining: '0',
|
|
818
839
|
default_payment_method_id: paymentMethod.id,
|
|
819
840
|
checkout_session_id: invoiceProps?.checkout_session_id || '',
|
|
820
|
-
metadata
|
|
841
|
+
metadata,
|
|
821
842
|
auto_advance: false,
|
|
822
843
|
paid: true,
|
|
823
844
|
paid_out_of_band: false,
|
|
@@ -839,6 +860,32 @@ export async function ensureStakeInvoice(
|
|
|
839
860
|
}
|
|
840
861
|
}
|
|
841
862
|
|
|
863
|
+
export async function getStakeInvoiceForSubscription(subscriptionId: string, currencyId: string) {
|
|
864
|
+
const where: WhereOptions = {
|
|
865
|
+
[Op.or]: [
|
|
866
|
+
{ subscription_id: subscriptionId },
|
|
867
|
+
{ 'metadata.all_subscription_ids': { [Op.contains]: [subscriptionId] } },
|
|
868
|
+
],
|
|
869
|
+
billing_reason: 'stake',
|
|
870
|
+
status: 'paid',
|
|
871
|
+
};
|
|
872
|
+
|
|
873
|
+
if (currencyId) {
|
|
874
|
+
where.currency_id = currencyId;
|
|
875
|
+
}
|
|
876
|
+
try {
|
|
877
|
+
const invoice = await Invoice.findOne({ where });
|
|
878
|
+
return invoice;
|
|
879
|
+
} catch (error) {
|
|
880
|
+
logger.error('getStakeInvoiceForSubscription: find invoice failed', {
|
|
881
|
+
error,
|
|
882
|
+
subscriptionId,
|
|
883
|
+
currencyId,
|
|
884
|
+
});
|
|
885
|
+
return null;
|
|
886
|
+
}
|
|
887
|
+
}
|
|
888
|
+
|
|
842
889
|
// mark overdraft protection invoice as void after payment
|
|
843
890
|
export async function handleOverdraftProtectionInvoiceAfterPayment(invoice: Invoice) {
|
|
844
891
|
try {
|
|
@@ -960,7 +1007,7 @@ export async function retryUncollectibleInvoices(options: {
|
|
|
960
1007
|
|
|
961
1008
|
const { customerId, subscriptionId, invoiceId, invoiceIds, currencyId } = options;
|
|
962
1009
|
|
|
963
|
-
const where:
|
|
1010
|
+
const where: WhereOptions = {
|
|
964
1011
|
status: { [Op.in]: ['uncollectible'] },
|
|
965
1012
|
payment_intent_id: { [Op.ne]: null },
|
|
966
1013
|
};
|
|
@@ -1,12 +1,20 @@
|
|
|
1
1
|
import { Notification as BlockletNotification } from '@blocklet/sdk';
|
|
2
2
|
|
|
3
3
|
import type { BaseEmailTemplate, BaseEmailTemplateType } from './template/base';
|
|
4
|
+
import { CheckoutSession, Invoice, Subscription } from '../../store/models';
|
|
5
|
+
import { getNotificationSettings, shouldSendSystemNotification } from '../setting';
|
|
6
|
+
import logger from '../logger';
|
|
7
|
+
import { events } from '../event';
|
|
8
|
+
import { createFlexibleEvent } from '../audit';
|
|
4
9
|
|
|
5
10
|
export class Notification {
|
|
6
11
|
template: BaseEmailTemplate;
|
|
7
|
-
|
|
8
|
-
constructor(template: BaseEmailTemplate) {
|
|
12
|
+
type?: string;
|
|
13
|
+
constructor(template: BaseEmailTemplate, type?: string) {
|
|
9
14
|
this.template = template;
|
|
15
|
+
if (type) {
|
|
16
|
+
this.type = type;
|
|
17
|
+
}
|
|
10
18
|
}
|
|
11
19
|
|
|
12
20
|
async send() {
|
|
@@ -16,8 +24,68 @@ export class Notification {
|
|
|
16
24
|
return;
|
|
17
25
|
}
|
|
18
26
|
|
|
19
|
-
const
|
|
20
|
-
|
|
27
|
+
const context = await this.template.getContext();
|
|
28
|
+
const { userDid } = context;
|
|
29
|
+
try {
|
|
30
|
+
const entity = await this.getEntityFromOptions(this.template.options);
|
|
31
|
+
if (entity) {
|
|
32
|
+
const settings = await getNotificationSettings(entity);
|
|
33
|
+
const shouldSend = shouldSendSystemNotification(this.type as string, settings);
|
|
34
|
+
if (!shouldSend) {
|
|
35
|
+
logger.info('Notification will not be sent', {
|
|
36
|
+
type: this.type,
|
|
37
|
+
entity,
|
|
38
|
+
});
|
|
39
|
+
try {
|
|
40
|
+
await createFlexibleEvent('manual.notification', 'notification', entity.id, {
|
|
41
|
+
type: this.type,
|
|
42
|
+
data: {
|
|
43
|
+
entity,
|
|
44
|
+
userDid,
|
|
45
|
+
context,
|
|
46
|
+
},
|
|
47
|
+
});
|
|
48
|
+
} catch (error) {
|
|
49
|
+
logger.error('Create flexible event error', error);
|
|
50
|
+
events.emit('manual.notification', {
|
|
51
|
+
type: this.type,
|
|
52
|
+
data: {
|
|
53
|
+
entity,
|
|
54
|
+
userDid,
|
|
55
|
+
context,
|
|
56
|
+
},
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
} catch (error) {
|
|
63
|
+
logger.error('Check notification settings error', error);
|
|
64
|
+
}
|
|
21
65
|
await BlockletNotification.sendToUser(userDid, template as any);
|
|
22
66
|
}
|
|
67
|
+
private async getEntityFromOptions(options: Record<string, any>) {
|
|
68
|
+
if (options.subscriptionId) {
|
|
69
|
+
const subscription = await Subscription.findByPk(options.subscriptionId);
|
|
70
|
+
if (subscription) {
|
|
71
|
+
return { subscription, id: subscription.id };
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (options.invoiceId) {
|
|
76
|
+
const invoice = await Invoice.findByPk(options.invoiceId);
|
|
77
|
+
if (invoice) {
|
|
78
|
+
return { invoice, id: invoice.id };
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (options.checkoutSessionId) {
|
|
83
|
+
const checkoutSession = await CheckoutSession.findByPk(options.checkoutSessionId);
|
|
84
|
+
if (checkoutSession) {
|
|
85
|
+
return { checkoutSession, id: checkoutSession.id };
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
23
91
|
}
|
|
@@ -96,11 +96,7 @@ export class SubscriptionRenewFailedEmailTemplate
|
|
|
96
96
|
},
|
|
97
97
|
})) as PaymentCurrency;
|
|
98
98
|
|
|
99
|
-
const checkoutSession = await CheckoutSession.
|
|
100
|
-
where: {
|
|
101
|
-
subscription_id: subscription.id,
|
|
102
|
-
},
|
|
103
|
-
});
|
|
99
|
+
const checkoutSession = await CheckoutSession.findBySubscriptionId(subscription.id);
|
|
104
100
|
|
|
105
101
|
const userDid: string = customer.did;
|
|
106
102
|
const locale = await getUserLocale(userDid);
|
|
@@ -87,11 +87,7 @@ export class SubscriptionRenewedEmailTemplate implements BaseEmailTemplate<Subsc
|
|
|
87
87
|
},
|
|
88
88
|
})) as PaymentCurrency;
|
|
89
89
|
|
|
90
|
-
const checkoutSession = await CheckoutSession.
|
|
91
|
-
where: {
|
|
92
|
-
subscription_id: subscription.id,
|
|
93
|
-
},
|
|
94
|
-
});
|
|
90
|
+
const checkoutSession = await CheckoutSession.findBySubscriptionId(subscription.id);
|
|
95
91
|
const userDid: string = customer.did;
|
|
96
92
|
const locale = await getUserLocale(userDid);
|
|
97
93
|
const productName = await getMainProductName(subscription.id);
|
|
@@ -77,19 +77,13 @@ export class SubscriptionSucceededEmailTemplate
|
|
|
77
77
|
|
|
78
78
|
await pWaitFor(
|
|
79
79
|
async () => {
|
|
80
|
-
const
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
where: {
|
|
88
|
-
subscription_id: subscription.id,
|
|
89
|
-
},
|
|
90
|
-
order: [['created_at', 'ASC']],
|
|
91
|
-
}),
|
|
92
|
-
]);
|
|
80
|
+
const invoice = await Invoice.findOne({
|
|
81
|
+
where: {
|
|
82
|
+
subscription_id: subscription.id,
|
|
83
|
+
},
|
|
84
|
+
order: [['created_at', 'ASC']],
|
|
85
|
+
});
|
|
86
|
+
const checkoutSession = await CheckoutSession.findBySubscriptionId(subscription.id);
|
|
93
87
|
|
|
94
88
|
return Boolean(
|
|
95
89
|
['disabled', 'minted', 'sent', 'error'].includes(checkoutSession?.nft_mint_status as string) &&
|
|
@@ -106,11 +100,7 @@ export class SubscriptionSucceededEmailTemplate
|
|
|
106
100
|
},
|
|
107
101
|
})) as PaymentCurrency;
|
|
108
102
|
|
|
109
|
-
const checkoutSession = await CheckoutSession.
|
|
110
|
-
where: {
|
|
111
|
-
subscription_id: subscription.id,
|
|
112
|
-
},
|
|
113
|
-
});
|
|
103
|
+
const checkoutSession = await CheckoutSession.findBySubscriptionId(subscription.id);
|
|
114
104
|
|
|
115
105
|
const userDid: string = customer.did;
|
|
116
106
|
const locale = await getUserLocale(userDid);
|
|
@@ -78,11 +78,7 @@ export class SubscriptionTrialStartEmailTemplate
|
|
|
78
78
|
|
|
79
79
|
await pWaitFor(
|
|
80
80
|
async () => {
|
|
81
|
-
const checkoutSession = await CheckoutSession.
|
|
82
|
-
where: {
|
|
83
|
-
subscription_id: subscription.id,
|
|
84
|
-
},
|
|
85
|
-
});
|
|
81
|
+
const checkoutSession = await CheckoutSession.findBySubscriptionId(subscription.id);
|
|
86
82
|
return ['minted', 'sent', 'error', 'disabled'].includes(checkoutSession?.nft_mint_status as string);
|
|
87
83
|
},
|
|
88
84
|
{ timeout: 1000 * 10, interval: 1000 }
|
|
@@ -101,11 +97,7 @@ export class SubscriptionTrialStartEmailTemplate
|
|
|
101
97
|
|
|
102
98
|
const oneTimeProductInfo = await getOneTimeProductInfo(subscription.latest_invoice_id as string, paymentCurrency);
|
|
103
99
|
|
|
104
|
-
const checkoutSession = await CheckoutSession.
|
|
105
|
-
where: {
|
|
106
|
-
subscription_id: subscription.id,
|
|
107
|
-
},
|
|
108
|
-
});
|
|
100
|
+
const checkoutSession = await CheckoutSession.findBySubscriptionId(subscription.id);
|
|
109
101
|
|
|
110
102
|
const userDid: string = customer.did;
|
|
111
103
|
const locale = await getUserLocale(userDid);
|
|
@@ -76,11 +76,7 @@ export class SubscriptionUpgradedEmailTemplate implements BaseEmailTemplate<Subs
|
|
|
76
76
|
},
|
|
77
77
|
})) as PaymentCurrency;
|
|
78
78
|
|
|
79
|
-
const checkoutSession = await CheckoutSession.
|
|
80
|
-
where: {
|
|
81
|
-
subscription_id: subscription.id,
|
|
82
|
-
},
|
|
83
|
-
});
|
|
79
|
+
const checkoutSession = await CheckoutSession.findBySubscriptionId(subscription.id);
|
|
84
80
|
|
|
85
81
|
const userDid: string = customer.did;
|
|
86
82
|
const locale = await getUserLocale(userDid);
|