payment-kit 1.16.16 → 1.16.18

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 (66) hide show
  1. package/api/src/crons/index.ts +1 -1
  2. package/api/src/hooks/pre-start.ts +2 -0
  3. package/api/src/index.ts +2 -0
  4. package/api/src/integrations/arcblock/stake.ts +7 -1
  5. package/api/src/integrations/stripe/resource.ts +1 -1
  6. package/api/src/libs/env.ts +12 -0
  7. package/api/src/libs/event.ts +8 -0
  8. package/api/src/libs/invoice.ts +585 -3
  9. package/api/src/libs/notification/template/subscription-succeeded.ts +1 -2
  10. package/api/src/libs/notification/template/subscription-trial-will-end.ts +2 -2
  11. package/api/src/libs/notification/template/subscription-will-renew.ts +6 -2
  12. package/api/src/libs/notification/template/subscription.overdraft-protection.exhausted.ts +139 -0
  13. package/api/src/libs/overdraft-protection.ts +85 -0
  14. package/api/src/libs/payment.ts +1 -65
  15. package/api/src/libs/queue/index.ts +0 -1
  16. package/api/src/libs/subscription.ts +532 -2
  17. package/api/src/libs/util.ts +4 -0
  18. package/api/src/locales/en.ts +5 -0
  19. package/api/src/locales/zh.ts +5 -0
  20. package/api/src/queues/event.ts +3 -2
  21. package/api/src/queues/invoice.ts +28 -3
  22. package/api/src/queues/notification.ts +25 -3
  23. package/api/src/queues/payment.ts +154 -3
  24. package/api/src/queues/refund.ts +2 -2
  25. package/api/src/queues/subscription.ts +215 -4
  26. package/api/src/queues/webhook.ts +1 -0
  27. package/api/src/routes/connect/change-payment.ts +1 -1
  28. package/api/src/routes/connect/change-plan.ts +1 -1
  29. package/api/src/routes/connect/overdraft-protection.ts +120 -0
  30. package/api/src/routes/connect/recharge.ts +2 -1
  31. package/api/src/routes/connect/setup.ts +1 -1
  32. package/api/src/routes/connect/shared.ts +117 -350
  33. package/api/src/routes/connect/subscribe.ts +1 -1
  34. package/api/src/routes/customers.ts +2 -2
  35. package/api/src/routes/invoices.ts +9 -4
  36. package/api/src/routes/subscriptions.ts +172 -2
  37. package/api/src/store/migrate.ts +9 -10
  38. package/api/src/store/migrations/20240905-index.ts +95 -60
  39. package/api/src/store/migrations/20241203-overdraft-protection.ts +25 -0
  40. package/api/src/store/migrations/20241216-update-overdraft-protection.ts +30 -0
  41. package/api/src/store/models/customer.ts +2 -2
  42. package/api/src/store/models/invoice.ts +7 -0
  43. package/api/src/store/models/lock.ts +7 -0
  44. package/api/src/store/models/subscription.ts +15 -0
  45. package/api/src/store/sequelize.ts +6 -1
  46. package/blocklet.yml +1 -1
  47. package/package.json +23 -23
  48. package/src/components/customer/overdraft-protection.tsx +367 -0
  49. package/src/components/event/list.tsx +3 -4
  50. package/src/components/subscription/actions/cancel.tsx +3 -0
  51. package/src/components/subscription/portal/actions.tsx +324 -77
  52. package/src/components/uploader.tsx +31 -26
  53. package/src/env.d.ts +1 -0
  54. package/src/hooks/subscription.ts +30 -0
  55. package/src/libs/env.ts +4 -0
  56. package/src/locales/en.tsx +41 -0
  57. package/src/locales/zh.tsx +37 -0
  58. package/src/pages/admin/billing/invoices/detail.tsx +16 -15
  59. package/src/pages/customer/index.tsx +7 -2
  60. package/src/pages/customer/invoice/detail.tsx +29 -5
  61. package/src/pages/customer/invoice/past-due.tsx +18 -4
  62. package/src/pages/customer/recharge.tsx +2 -4
  63. package/src/pages/customer/subscription/change-payment.tsx +7 -1
  64. package/src/pages/customer/subscription/detail.tsx +69 -51
  65. package/tsconfig.json +0 -5
  66. package/api/tests/libs/payment.spec.ts +0 -168
@@ -0,0 +1,139 @@
1
+ /* eslint-disable prettier/prettier */
2
+ import { translate } from '../../../locales';
3
+ import { Customer, Subscription } from '../../../store/models';
4
+ import type { BaseEmailTemplate, BaseEmailTemplateType } from './base';
5
+ import { getUserLocale } from '../../../integrations/blocklet/notification';
6
+ import { getCustomerSubscriptionPageUrl, isSubscriptionOverdraftProtectionEnabled } from '../../subscription';
7
+ import { getMainProductName } from '../../product';
8
+
9
+ export interface OverdraftProtectionExhaustedEmailTemplateOptions {
10
+ subscriptionId: string;
11
+ }
12
+
13
+ interface OverdraftProtectionExhaustedEmailTemplateContext {
14
+ locale: string;
15
+ userDid: string;
16
+ viewSubscriptionLink: string;
17
+ productName: string;
18
+ subscriptionId: string;
19
+ }
20
+
21
+ export class OverdraftProtectionExhaustedEmailTemplate
22
+ implements BaseEmailTemplate<OverdraftProtectionExhaustedEmailTemplateContext> {
23
+ options: OverdraftProtectionExhaustedEmailTemplateOptions;
24
+
25
+ constructor(options: OverdraftProtectionExhaustedEmailTemplateOptions) {
26
+ this.options = options;
27
+ }
28
+
29
+ async getContext(): Promise<OverdraftProtectionExhaustedEmailTemplateContext> {
30
+ const { subscriptionId } = this.options;
31
+
32
+ const subscription: Subscription | null = await Subscription.findByPk(subscriptionId);
33
+ if (!subscription) {
34
+ throw new Error(`Subscription not found: ${subscriptionId}`);
35
+ }
36
+
37
+ const customer: Customer | null = await Customer.findByPk(subscription.customer_id);
38
+ if (!customer) {
39
+ throw new Error(`Customer not found: ${subscription.customer_id}`);
40
+ }
41
+
42
+ if (!subscription.overdraft_protection?.enabled) {
43
+ throw new Error(`Overdraft protection not enabled for subscription: ${subscriptionId}`);
44
+ }
45
+
46
+ const { enabled } = await isSubscriptionOverdraftProtectionEnabled(subscription);
47
+ if (enabled) {
48
+ throw new Error(`Overdraft protection enabled for subscription: ${subscriptionId}`);
49
+ }
50
+
51
+ const productName = await getMainProductName(subscription.id);
52
+ const userDid = customer.did;
53
+ const locale = await getUserLocale(userDid);
54
+ const viewSubscriptionLink = getCustomerSubscriptionPageUrl({
55
+ subscriptionId: subscription.id,
56
+ locale,
57
+ userDid,
58
+ });
59
+ return {
60
+ userDid,
61
+ locale,
62
+ viewSubscriptionLink,
63
+ productName,
64
+ subscriptionId,
65
+ };
66
+ }
67
+
68
+ async getTemplate(): Promise<BaseEmailTemplateType> {
69
+ const { locale, userDid, viewSubscriptionLink, productName, subscriptionId } = await this.getContext();
70
+
71
+ const template: BaseEmailTemplateType = {
72
+ title: translate('notification.overdraftProtectionExhausted.title', locale),
73
+ body: translate('notification.overdraftProtectionExhausted.body', locale, {
74
+ productName
75
+ }),
76
+ attachments: [
77
+ {
78
+ type: 'section',
79
+ fields: [
80
+ {
81
+ type: 'text',
82
+ data: {
83
+ type: 'plain',
84
+ color: '#9397A1',
85
+ text: translate('notification.common.account', locale),
86
+ },
87
+ },
88
+ {
89
+ type: 'text',
90
+ data: {
91
+ type: 'plain',
92
+ text: userDid,
93
+ },
94
+ },
95
+ {
96
+ type: 'text',
97
+ data: {
98
+ type: 'plain',
99
+ color: '#9397A1',
100
+ text: translate('notification.common.product', locale),
101
+ },
102
+ },
103
+ {
104
+ type: 'text',
105
+ data: {
106
+ type: 'plain',
107
+ text: productName,
108
+ },
109
+ },
110
+ {
111
+ type: 'text',
112
+ data: {
113
+ type: 'plain',
114
+ color: '#9397A1',
115
+ text: translate('notification.common.subscriptionId', locale),
116
+ },
117
+ },
118
+ {
119
+ type: 'text',
120
+ data: {
121
+ type: 'plain',
122
+ text: subscriptionId,
123
+ },
124
+ },
125
+ ].filter(Boolean),
126
+ },
127
+ ],
128
+ actions: [
129
+ {
130
+ name: translate('notification.common.viewSubscription', locale),
131
+ title: translate('notification.common.viewSubscription', locale),
132
+ link: viewSubscriptionLink,
133
+ },
134
+ ].filter(Boolean),
135
+ };
136
+
137
+ return template;
138
+ }
139
+ }
@@ -0,0 +1,85 @@
1
+ import { createProductAndPrices } from '../routes/products';
2
+ import { PaymentCurrency, PaymentMethod, Price, Product } from '../store/models';
3
+ import logger from './logger';
4
+
5
+ export const overdraftProtectionKey = 'overdraft-protection';
6
+
7
+ export async function ensureOverdraftProtectionPrice(livemode = true) {
8
+ try {
9
+ const exist = await Price.findOne({ where: { lookup_key: overdraftProtectionKey, livemode } });
10
+ if (exist) {
11
+ const product = await Product.findByPk(exist.product_id);
12
+ return {
13
+ product,
14
+ price: exist,
15
+ };
16
+ }
17
+ const baseCurrency = await PaymentCurrency.findOne({ where: { is_base_currency: true, livemode } });
18
+ const currencies = (await PaymentCurrency.findAll({
19
+ where: { livemode },
20
+ include: [{ model: PaymentMethod, as: 'payment_method' }],
21
+ })) as (PaymentCurrency & { payment_method: PaymentMethod })[];
22
+
23
+ const currencyOptions = currencies
24
+ .filter((currency) => currency.payment_method?.type === 'arcblock')
25
+ .map((currency) => ({ currency_id: currency.id, unit_amount: '1' }));
26
+
27
+ const result = await createProductAndPrices({
28
+ type: 'service',
29
+ name: 'Overdraft Protection',
30
+ description:
31
+ 'If you purchase this service, the overdue subscription will be protected and not be ended due to overdue',
32
+ images: [],
33
+ statement_descriptor: '',
34
+ unit_label: '',
35
+ features: [],
36
+ livemode,
37
+ prices: [
38
+ {
39
+ livemode,
40
+ locked: true,
41
+ model: 'standard',
42
+ billing_scheme: '',
43
+ currency_id: baseCurrency?.id,
44
+ nickname: '',
45
+ type: 'one_time',
46
+ unit_amount: '1',
47
+ lookup_key: overdraftProtectionKey,
48
+ recurring: {},
49
+ transform_quantity: { divide_by: 1, round: 'up' },
50
+ tiers: [],
51
+ metadata: [],
52
+ custom_unit_amount: null,
53
+ currency_options: currencyOptions,
54
+ tiers_mode: null,
55
+ quantity_available: 0,
56
+ quantity_limit_per_checkout: 0,
57
+ },
58
+ ],
59
+ metadata: [],
60
+ });
61
+ logger.info('overdraft protection price created', {
62
+ productId: result.id,
63
+ priceId: result.prices?.[0]?.id,
64
+ });
65
+ return {
66
+ product: result,
67
+ price: result.prices?.[0],
68
+ };
69
+ } catch (err) {
70
+ logger.error('create overdraft protection price error', err);
71
+ return {
72
+ product: null,
73
+ price: null,
74
+ };
75
+ }
76
+ }
77
+
78
+ export async function ensureCreateOverdraftProtectionPrices() {
79
+ try {
80
+ await ensureOverdraftProtectionPrice(true);
81
+ await ensureOverdraftProtectionPrice(false);
82
+ } catch (err) {
83
+ logger.error('ensure overdraft protection prices error', err);
84
+ }
85
+ }
@@ -10,23 +10,11 @@ import cloneDeep from 'lodash/cloneDeep';
10
10
  import type { LiteralUnion } from 'type-fest';
11
11
 
12
12
  import { fetchErc20Allowance, fetchErc20Balance, fetchEtherBalance } from '../integrations/ethereum/token';
13
- import {
14
- Invoice,
15
- PaymentCurrency,
16
- PaymentIntent,
17
- PaymentMethod,
18
- SubscriptionItem,
19
- TCustomer,
20
- TLineItemExpanded,
21
- Subscription,
22
- Price,
23
- UsageRecord,
24
- } from '../store/models';
13
+ import { Invoice, PaymentCurrency, PaymentIntent, PaymentMethod, TCustomer, TLineItemExpanded } from '../store/models';
25
14
  import type { TPaymentCurrency } from '../store/models/payment-currency';
26
15
  import { blocklet, ethWallet, wallet } from './auth';
27
16
  import logger from './logger';
28
17
  import { OCAP_PAYMENT_TX_TYPE } from './util';
29
- import { getSubscriptionCycleAmount, getSubscriptionCycleSetup } from './subscription';
30
18
 
31
19
  export interface SufficientForPaymentResult {
32
20
  sufficient: boolean;
@@ -356,55 +344,3 @@ export async function isBalanceSufficientForRefund(args: {
356
344
 
357
345
  throw new Error(`isBalanceSufficientForRefund: Payment method ${paymentMethod.type} not supported`);
358
346
  }
359
-
360
- export async function getPaymentAmountForCycleSubscription(
361
- subscription: Subscription,
362
- paymentCurrency: PaymentCurrency
363
- ) {
364
- const subscriptionItems = await SubscriptionItem.findAll({ where: { subscription_id: subscription.id } });
365
- if (subscriptionItems.length === 0) {
366
- logger.info('subscription items not found in getPaymentAmountForCycleSubscription', {
367
- subscription: subscription.id,
368
- });
369
- return 0;
370
- }
371
- let expandedItems = await Price.expand(
372
- subscriptionItems.map((x) => ({ id: x.id, price_id: x.price_id, quantity: x.quantity })),
373
- { product: true }
374
- );
375
- if (expandedItems.length === 0) {
376
- logger.info('expanded items not found in getPaymentAmountForCycleSubscription', {
377
- subscription: subscription.id,
378
- });
379
- return 0;
380
- }
381
- const previousPeriodEnd =
382
- subscription.status === 'trialing' ? subscription.trial_end : subscription.current_period_end;
383
- const setup = getSubscriptionCycleSetup(subscription.pending_invoice_item_interval, previousPeriodEnd as number);
384
- // get usage summaries for this billing cycle
385
- expandedItems = await Promise.all(
386
- expandedItems.map(async (x: any) => {
387
- // For metered billing, we need to get usage summary for this billing cycle
388
- // @link https://stripe.com/docs/products-prices/pricing-models#usage-types
389
- if (x.price.recurring?.usage_type === 'metered') {
390
- const rawQuantity = await UsageRecord.getSummary({
391
- id: x.id,
392
- start: setup.period.start - setup.cycle / 1000,
393
- end: setup.period.end - setup.cycle / 1000,
394
- method: x.price.recurring?.aggregate_usage,
395
- dryRun: true,
396
- });
397
- x.quantity = x.price.transformQuantity(rawQuantity);
398
- // record raw quantity in metadata
399
- x.metadata = x.metadata || {};
400
- x.metadata.quantity = rawQuantity;
401
- }
402
- return x;
403
- })
404
- );
405
- if (expandedItems.length > 0) {
406
- const amount = getSubscriptionCycleAmount(expandedItems, paymentCurrency.id);
407
- return +fromUnitToToken(amount?.total || '0', paymentCurrency.decimal);
408
- }
409
- return 0;
410
- }
@@ -147,7 +147,6 @@ export default function createQueue<T = any>({ name, onJob, options = defaults }
147
147
  })
148
148
  .catch((err) => {
149
149
  console.error(err);
150
- logger.error('Can not add scheduled job to store', { jobId, job, attrs, error: err });
151
150
  });
152
151
 
153
152
  // @ts-ignore