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.
Files changed (51) hide show
  1. package/api/src/crons/metering-subscription-detection.ts +9 -0
  2. package/api/src/integrations/arcblock/nft.ts +1 -0
  3. package/api/src/integrations/blocklet/passport.ts +1 -1
  4. package/api/src/integrations/stripe/handlers/invoice.ts +2 -2
  5. package/api/src/integrations/stripe/handlers/setup-intent.ts +29 -1
  6. package/api/src/integrations/stripe/handlers/subscription.ts +19 -15
  7. package/api/src/integrations/stripe/resource.ts +81 -1
  8. package/api/src/libs/audit.ts +42 -0
  9. package/api/src/libs/invoice.ts +54 -7
  10. package/api/src/libs/notification/index.ts +72 -4
  11. package/api/src/libs/notification/template/base.ts +2 -0
  12. package/api/src/libs/notification/template/subscription-renew-failed.ts +1 -5
  13. package/api/src/libs/notification/template/subscription-renewed.ts +1 -5
  14. package/api/src/libs/notification/template/subscription-succeeded.ts +8 -18
  15. package/api/src/libs/notification/template/subscription-trial-start.ts +2 -10
  16. package/api/src/libs/notification/template/subscription-upgraded.ts +1 -5
  17. package/api/src/libs/payment.ts +47 -14
  18. package/api/src/libs/product.ts +1 -4
  19. package/api/src/libs/session.ts +600 -8
  20. package/api/src/libs/setting.ts +172 -0
  21. package/api/src/libs/subscription.ts +7 -69
  22. package/api/src/libs/ws.ts +5 -0
  23. package/api/src/queues/checkout-session.ts +42 -36
  24. package/api/src/queues/notification.ts +3 -2
  25. package/api/src/queues/payment.ts +33 -6
  26. package/api/src/queues/usage-record.ts +2 -10
  27. package/api/src/routes/checkout-sessions.ts +324 -187
  28. package/api/src/routes/connect/shared.ts +160 -38
  29. package/api/src/routes/connect/subscribe.ts +123 -64
  30. package/api/src/routes/payment-currencies.ts +3 -6
  31. package/api/src/routes/payment-links.ts +11 -1
  32. package/api/src/routes/payment-stats.ts +2 -2
  33. package/api/src/routes/payouts.ts +2 -1
  34. package/api/src/routes/settings.ts +45 -0
  35. package/api/src/routes/subscriptions.ts +1 -2
  36. package/api/src/store/migrations/20250408-subscription-grouping.ts +39 -0
  37. package/api/src/store/migrations/20250419-subscription-grouping.ts +69 -0
  38. package/api/src/store/models/checkout-session.ts +52 -0
  39. package/api/src/store/models/index.ts +1 -0
  40. package/api/src/store/models/payment-link.ts +6 -0
  41. package/api/src/store/models/subscription.ts +8 -6
  42. package/api/src/store/models/types.ts +31 -1
  43. package/api/tests/libs/session.spec.ts +423 -0
  44. package/api/tests/libs/subscription.spec.ts +0 -110
  45. package/blocklet.yml +3 -1
  46. package/package.json +20 -19
  47. package/scripts/sdk.js +486 -155
  48. package/src/locales/en.tsx +1 -1
  49. package/src/locales/zh.tsx +1 -1
  50. package/src/pages/admin/settings/vault-config/edit-form.tsx +1 -1
  51. 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.findOne({ where: { subscription_id: subscription.id } });
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.findOne({ where: { subscription_id: subscription.id } });
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.findOne({ where: { subscription_id: subscription.id } });
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.findOne({ where: { subscription_id: subscription.id } });
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
- if (result.pending_setup_intent && result.pending_setup_intent.status !== 'succeeded') {
20
- logger.warn('subscription can not active because stripe setup not done', {
21
- id: subscription.id,
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.findOne({ where: { payment_intent_id: subscription.id } });
38
- if (checkoutSession) {
39
- await checkoutSession.update({
40
- status: 'complete',
41
- payment_status: 'paid',
42
- payment_details: subscription.payment_details,
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 as any, method, currency);
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
+ }
@@ -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
+ }
@@ -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
- const subscriptionItems = subscription
553
- ? await SubscriptionItem.findAll({ where: { subscription_id: subscription?.id } })
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: invoiceProps.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: any = {
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 { userDid } = await this.template.getContext();
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
  }
@@ -9,4 +9,6 @@ export interface BaseEmailTemplate<C = BaseEmailTemplateContext> {
9
9
  getTemplate(): Promise<BaseEmailTemplateType | null>;
10
10
 
11
11
  getContext(): Promise<C>;
12
+
13
+ options: Record<string, any>;
12
14
  }
@@ -96,11 +96,7 @@ export class SubscriptionRenewFailedEmailTemplate
96
96
  },
97
97
  })) as PaymentCurrency;
98
98
 
99
- const checkoutSession = await CheckoutSession.findOne({
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.findOne({
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 [checkoutSession, invoice] = await Promise.all([
81
- CheckoutSession.findOne({
82
- where: {
83
- subscription_id: subscription.id,
84
- },
85
- }),
86
- Invoice.findOne({
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.findOne({
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.findOne({
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.findOne({
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.findOne({
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);