payment-kit 1.16.17 → 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
@@ -96,7 +96,7 @@ router.get('/me', sessionMiddleware(), async (req, res) => {
96
96
  }
97
97
  } else {
98
98
  const [summary, stake, token] = await Promise.all([
99
- doc.getSummary(),
99
+ doc.getSummary(livemode),
100
100
  getStakeSummaryByDid(doc.did, livemode),
101
101
  getTokenSummaryByDid(doc.did, livemode),
102
102
  ]);
@@ -131,7 +131,7 @@ router.get('/:id/summary', auth, async (req, res) => {
131
131
  }
132
132
 
133
133
  const [summary, stake, token] = await Promise.all([
134
- doc.getSummary(),
134
+ doc.getSummary(doc.livemode),
135
135
  getStakeSummaryByDid(doc.did, doc.livemode),
136
136
  getTokenSummaryByDid(doc.did, doc.livemode),
137
137
  ]);
@@ -216,13 +216,18 @@ router.get('/:id', authPortal, async (req, res) => {
216
216
  const prices = (await Price.findAll()).map((x) => x.toJSON());
217
217
  // @ts-ignore
218
218
  expandLineItems(json.lines, products, prices);
219
- res.json(json);
220
- } else {
221
- res.status(404).json(null);
219
+ if (doc.metadata?.invoice_id) {
220
+ const relatedInvoice = await Invoice.findByPk(doc.metadata.invoice_id, {
221
+ attributes: ['id', 'number', 'status', 'billing_reason'],
222
+ });
223
+ return res.json({ ...json, relatedInvoice });
224
+ }
225
+ return res.json(json);
222
226
  }
227
+ return res.status(404).json(null);
223
228
  } catch (err) {
224
229
  console.error(err);
225
- res.status(500).json({ error: `Failed to get invoice: ${err.message}` });
230
+ return res.status(500).json({ error: `Failed to get invoice: ${err.message}` });
226
231
  }
227
232
  });
228
233
 
@@ -24,16 +24,25 @@ import { expandLineItems, getFastCheckoutAmount, isLineItemAligned } from '../li
24
24
  import {
25
25
  createProration,
26
26
  finalizeSubscriptionUpdate,
27
+ getPastInvoicesAmount,
27
28
  getSubscriptionCreateSetup,
28
29
  getSubscriptionPaymentAddress,
29
30
  getSubscriptionRefundSetup,
30
31
  getSubscriptionStakeReturnSetup,
31
32
  getSubscriptionStakeSlashSetup,
33
+ getSubscriptionUnpaidInvoicesCount,
32
34
  getUpcomingInvoiceAmount,
35
+ isSubscriptionOverdraftProtectionEnabled,
33
36
  } from '../libs/subscription';
34
37
  import { MAX_SUBSCRIPTION_ITEM_COUNT, formatMetadata } from '../libs/util';
35
38
  import { invoiceQueue } from '../queues/invoice';
36
- import { addSubscriptionJob, slashStakeQueue, subscriptionQueue } from '../queues/subscription';
39
+ import {
40
+ addSubscriptionJob,
41
+ returnOverdraftProtectionQueue,
42
+ slashOverdraftProtectionQueue,
43
+ slashStakeQueue,
44
+ subscriptionQueue,
45
+ } from '../queues/subscription';
37
46
  import type { TLineItemExpanded } from '../store/models';
38
47
  import { Customer } from '../store/models/customer';
39
48
  import { Invoice } from '../store/models/invoice';
@@ -50,10 +59,11 @@ import { Subscription, TSubscription } from '../store/models/subscription';
50
59
  import { SubscriptionItem } from '../store/models/subscription-item';
51
60
  import type { LineItem, ServiceAction, SubscriptionUpdateItem } from '../store/models/types';
52
61
  import { UsageRecord } from '../store/models/usage-record';
53
- import { cleanupInvoiceAndItems, ensureInvoiceAndItems } from './connect/shared';
62
+ import { cleanupInvoiceAndItems, ensureInvoiceAndItems } from '../libs/invoice';
54
63
  import { createUsageRecordQueryFn } from './usage-records';
55
64
  import { SubscriptionWillCanceledSchedule } from '../crons/subscription-will-canceled';
56
65
  import { getTokenByAddress } from '../integrations/arcblock/stake';
66
+ import { ensureOverdraftProtectionPrice } from '../libs/overdraft-protection';
57
67
 
58
68
  const router = Router();
59
69
  const auth = authenticate<Subscription>({ component: true, roles: ['owner', 'admin'] });
@@ -1735,6 +1745,41 @@ router.get('/:id/upcoming', authPortal, async (req, res) => {
1735
1745
  }
1736
1746
  });
1737
1747
 
1748
+ router.get('/:id/cycle-amount', authPortal, async (req, res) => {
1749
+ const subscription = await Subscription.findByPk(req.params.id);
1750
+ if (!subscription) {
1751
+ return res.status(404).json({ error: 'Subscription not found' });
1752
+ }
1753
+ const currency = await PaymentCurrency.findByPk(subscription.currency_id);
1754
+ if (!currency) {
1755
+ return res.status(404).json({ error: 'Currency not found' });
1756
+ }
1757
+ // get upcoming invoice
1758
+ const result = await getUpcomingInvoiceAmount(subscription.id);
1759
+ // get past invoices
1760
+ const pastMaxAmount = await getPastInvoicesAmount(subscription.id, 'max');
1761
+
1762
+ // return max amount
1763
+ const nextAmount = new BN(result.amount === '0' ? result.minExpectedAmount : result.amount);
1764
+
1765
+ const maxAmount = new BN(pastMaxAmount.amount).lte(new BN(nextAmount)) ? nextAmount : pastMaxAmount.amount;
1766
+
1767
+ if (req.query?.overdraftProtection) {
1768
+ const { price } = await ensureOverdraftProtectionPrice(subscription.livemode);
1769
+ const invoicePrice = price.currency_options.find((x: any) => x.currency_id === subscription?.currency_id);
1770
+ const gas = invoicePrice?.unit_amount;
1771
+ return res.json({
1772
+ amount: new BN(maxAmount).add(new BN(gas)).toString(),
1773
+ gas,
1774
+ currency,
1775
+ });
1776
+ }
1777
+ return res.json({
1778
+ amount: maxAmount,
1779
+ currency,
1780
+ });
1781
+ });
1782
+
1738
1783
  // slash stake
1739
1784
  router.put('/:id/slash-stake', auth, async (req, res) => {
1740
1785
  const { error: slashReasonError } = SlashStakeSchema.validate(req.body?.slashReason);
@@ -1959,4 +2004,129 @@ router.get('/:id/delegation', authPortal, async (req, res) => {
1959
2004
  return res.json(null);
1960
2005
  }
1961
2006
  });
2007
+
2008
+ router.get('/:id/overdraft-protection', authPortal, async (req, res) => {
2009
+ const subscription = await Subscription.findByPk(req.params.id);
2010
+ if (!subscription) {
2011
+ return res.status(404).json({ error: 'Subscription not found' });
2012
+ }
2013
+ try {
2014
+ const { enabled, remaining, unused, used, shouldPay } =
2015
+ await isSubscriptionOverdraftProtectionEnabled(subscription);
2016
+ const upcoming = await getUpcomingInvoiceAmount(req.params.id as string);
2017
+ const { price } = await ensureOverdraftProtectionPrice(subscription.livemode);
2018
+ const invoicePrice = price.currency_options.find((x: any) => x.currency_id === subscription?.currency_id);
2019
+ const gas = invoicePrice?.unit_amount;
2020
+ return res.json({
2021
+ enabled,
2022
+ remaining,
2023
+ unused,
2024
+ used,
2025
+ upcoming,
2026
+ gas,
2027
+ shouldPay,
2028
+ });
2029
+ } catch (err) {
2030
+ console.error(err);
2031
+ return res.status(400).json({ error: err.message });
2032
+ }
2033
+ });
2034
+
2035
+ const overdraftProtectionSchema = createListParamSchema<{
2036
+ amount: string;
2037
+ enabled: boolean;
2038
+ return_stake: boolean;
2039
+ }>({
2040
+ amount: Joi.number().empty(0).optional(),
2041
+ enabled: Joi.boolean().required(),
2042
+ return_stake: Joi.boolean().empty(false).optional(),
2043
+ });
2044
+ // 订阅保护
2045
+ router.post('/:id/overdraft-protection', authPortal, async (req, res) => {
2046
+ try {
2047
+ const {
2048
+ error: overdraftProtectionError,
2049
+ value: { amount, return_stake: returnStake, enabled },
2050
+ } = overdraftProtectionSchema.validate(req.body);
2051
+ if (overdraftProtectionError) {
2052
+ return res.status(400).json({ error: `Overdraft protection invalid: ${overdraftProtectionError.message}` });
2053
+ }
2054
+ const subscription = await Subscription.findByPk(req.params.id);
2055
+ if (!subscription) {
2056
+ return res.status(404).json({ error: 'Subscription not found' });
2057
+ }
2058
+ const customer = await Customer.findByPkOrDid(req.user?.did as string);
2059
+ if (!customer) {
2060
+ return res.status(404).json({ error: 'Customer not found' });
2061
+ }
2062
+ const { remaining, used } = await isSubscriptionOverdraftProtectionEnabled(subscription);
2063
+ if (returnStake && remaining !== '0' && !enabled) {
2064
+ // disable overdraft protection
2065
+ await subscription.update({
2066
+ // @ts-ignore
2067
+ overdraft_protection: {
2068
+ ...(subscription.overdraft_protection || {}),
2069
+ enabled: false,
2070
+ },
2071
+ });
2072
+ returnOverdraftProtectionQueue.push({
2073
+ id: `return-overdraft-protection-${subscription.id}`,
2074
+ job: { subscriptionId: subscription.id },
2075
+ });
2076
+ logger.info('Return overdraft protection stake scheduled', {
2077
+ subscriptionId: subscription.id,
2078
+ requestedBy: req.user?.did,
2079
+ });
2080
+ return res.json({
2081
+ open: false,
2082
+ overdraft_protection: subscription.overdraft_protection,
2083
+ });
2084
+ }
2085
+ if (remaining !== '0' && used !== '0' && !enabled) {
2086
+ // slash stake
2087
+ slashOverdraftProtectionQueue.push({
2088
+ id: `slash-overdraft-protection-${subscription.id}`,
2089
+ job: { subscriptionId: subscription.id },
2090
+ });
2091
+ logger.info('Slash overdraft protection stake scheduled', {
2092
+ subscriptionId: subscription.id,
2093
+ requestedBy: req.user?.did,
2094
+ });
2095
+ }
2096
+ await subscription.update({
2097
+ // @ts-ignore
2098
+ overdraft_protection: {
2099
+ ...subscription.overdraft_protection,
2100
+ enabled,
2101
+ },
2102
+ });
2103
+ if (enabled && Number(amount) > 0) {
2104
+ return res.json({
2105
+ open: true,
2106
+ amount,
2107
+ overdraft_protection: subscription.overdraft_protection,
2108
+ });
2109
+ }
2110
+ if (enabled) {
2111
+ // release the exhausted lock, so that the notification can be sent again if overdraft protection exhausted
2112
+ await Lock.release(`${subscription.id}-${subscription.currency_id}-overdraft-protection-exhausted`);
2113
+ }
2114
+ return res.json({
2115
+ open: false,
2116
+ overdraft_protection: subscription.overdraft_protection,
2117
+ });
2118
+ } catch (err) {
2119
+ console.error(err);
2120
+ return res.status(400).json({ error: err.message });
2121
+ }
2122
+ });
2123
+
2124
+ router.get('/:id/unpaid-invoices', authPortal, async (req, res) => {
2125
+ const subscription = await Subscription.findByPk(req.params.id);
2126
+ if (!subscription) {
2127
+ return res.status(404).json({ error: 'Subscription not found' });
2128
+ }
2129
+ const count = await getSubscriptionUnpaidInvoicesCount(subscription);
2130
+ return res.json({ count });
2131
+ });
1962
2132
  export default router;
@@ -9,16 +9,15 @@ import { sequelize } from './sequelize';
9
9
  const umzug = new Umzug({
10
10
  migrations: {
11
11
  glob: ['**/migrations/*.{ts,js}', { cwd: __dirname }],
12
- // @FIXME: @wangshijun jianchao这边的注释了才能 blocklet dev 成功
13
- // resolve: ({ name, path, context }) => {
14
- // // eslint-disable-next-line import/no-dynamic-require, global-require
15
- // const migration = require(path!);
16
- // return {
17
- // name: name.replace(/\.ts$/, '.js'),
18
- // up: () => migration.up({ context }),
19
- // down: () => migration.down({ context }),
20
- // };
21
- // },
12
+ resolve: ({ name, path, context }) => {
13
+ // eslint-disable-next-line import/no-dynamic-require, global-require
14
+ const migration = require(path!);
15
+ return {
16
+ name: name.replace(/\.ts$/, '.js'),
17
+ up: () => migration.up({ context }),
18
+ down: () => migration.down({ context }),
19
+ };
20
+ },
22
21
  },
23
22
  context: sequelize.getQueryInterface(),
24
23
  storage: new SequelizeStorage({ sequelize }),
@@ -1,76 +1,111 @@
1
+ import type { QueryInterface } from 'sequelize';
1
2
  import type { Migration } from '../migrate';
2
3
 
4
+ const indexExists = async (table: string, indexName: string, queryInterface: QueryInterface) => {
5
+ const indexes = await queryInterface.showIndex(table);
6
+ return indexes && Array.isArray(indexes) && indexes.some((index: { name: string }) => index.name === indexName);
7
+ };
8
+
9
+ const createIndexIfNotExists = async (
10
+ queryInterface: QueryInterface,
11
+ table: string,
12
+ columns: string[],
13
+ indexName: string
14
+ ) => {
15
+ if (await indexExists(table, indexName, queryInterface)) {
16
+ /* eslint-disable no-console */
17
+ console.log(`Index ${indexName} already exists on ${table}, skipping...`);
18
+ return;
19
+ }
20
+ await queryInterface.addIndex(table, columns, { name: indexName });
21
+ };
22
+
3
23
  export const up: Migration = async ({ context: queryInterface }) => {
4
24
  try {
5
- await queryInterface.addIndex('subscriptions', ['current_period_start', 'current_period_end'], {
6
- name: 'idx_subscription_period',
7
- });
8
- await queryInterface.addIndex('subscriptions', ['status'], {
9
- name: 'idx_subscription_status',
10
- });
25
+ await createIndexIfNotExists(
26
+ queryInterface,
27
+ 'subscriptions',
28
+ ['current_period_start', 'current_period_end'],
29
+ 'idx_subscription_period'
30
+ );
31
+ await createIndexIfNotExists(queryInterface, 'subscriptions', ['status'], 'idx_subscription_status');
11
32
 
12
- await queryInterface.addIndex('subscription_items', ['subscription_id', 'price_id'], {
13
- name: 'idx_subscription_item_subscription_id_price_id',
14
- });
33
+ await createIndexIfNotExists(
34
+ queryInterface,
35
+ 'subscription_items',
36
+ ['subscription_id', 'price_id'],
37
+ 'idx_subscription_item_subscription_id_price_id'
38
+ );
15
39
 
16
- await queryInterface.addIndex('invoices', ['status', 'collection_method'], {
17
- name: 'idx_invoice_status_collection',
18
- });
19
- await queryInterface.addIndex('invoices', ['subscription_id'], {
20
- name: 'idx_invoice_subscription_id',
21
- });
22
- await queryInterface.addIndex('invoices', ['currency_id'], {
23
- name: 'idx_invoice_currency_id',
24
- });
25
- await queryInterface.addIndex('invoices', ['customer_id'], {
26
- name: 'idx_invoice_customer_id',
27
- });
40
+ await createIndexIfNotExists(
41
+ queryInterface,
42
+ 'invoices',
43
+ ['status', 'collection_method'],
44
+ 'idx_invoice_status_collection'
45
+ );
46
+ await createIndexIfNotExists(queryInterface, 'invoices', ['subscription_id'], 'idx_invoice_subscription_id');
47
+ await createIndexIfNotExists(queryInterface, 'invoices', ['currency_id'], 'idx_invoice_currency_id');
48
+ await createIndexIfNotExists(queryInterface, 'invoices', ['customer_id'], 'idx_invoice_customer_id');
28
49
 
29
- await queryInterface.addIndex('payment_intents', ['invoice_id'], {
30
- name: 'idx_payment_intent_invoice_id',
31
- });
32
- await queryInterface.addIndex('payment_intents', ['customer_id'], {
33
- name: 'idx_payment_intent_customer_id',
34
- });
35
- await queryInterface.addIndex('payment_intents', ['currency_id'], {
36
- name: 'idx_payment_intent_currency_id',
37
- });
38
- await queryInterface.addIndex('payment_intents', ['status', 'updated_at'], {
39
- name: 'idx_payment_intent_status_updated_at',
40
- });
50
+ await createIndexIfNotExists(queryInterface, 'payment_intents', ['invoice_id'], 'idx_payment_intent_invoice_id');
51
+ await createIndexIfNotExists(queryInterface, 'payment_intents', ['customer_id'], 'idx_payment_intent_customer_id');
52
+ await createIndexIfNotExists(queryInterface, 'payment_intents', ['currency_id'], 'idx_payment_intent_currency_id');
53
+ await createIndexIfNotExists(
54
+ queryInterface,
55
+ 'payment_intents',
56
+ ['status', 'updated_at'],
57
+ 'idx_payment_intent_status_updated_at'
58
+ );
41
59
 
42
- await queryInterface.addIndex('webhook_attempts', ['webhook_endpoint_id'], {
43
- name: 'idx_webhook_attempts_webhook_endpoint_id',
44
- });
45
- await queryInterface.addIndex('webhook_attempts', ['event_id'], {
46
- name: 'idx_webhook_attempts_event_id',
47
- });
60
+ await createIndexIfNotExists(
61
+ queryInterface,
62
+ 'webhook_attempts',
63
+ ['webhook_endpoint_id'],
64
+ 'idx_webhook_attempts_webhook_endpoint_id'
65
+ );
66
+ await createIndexIfNotExists(queryInterface, 'webhook_attempts', ['event_id'], 'idx_webhook_attempts_event_id');
48
67
 
49
- await queryInterface.addIndex('usage_records', ['subscription_item_id', 'timestamp'], {
50
- name: 'idx_usage_records_subscription_item_id_timestamp',
51
- });
68
+ await createIndexIfNotExists(
69
+ queryInterface,
70
+ 'usage_records',
71
+ ['subscription_item_id', 'timestamp'],
72
+ 'idx_usage_records_subscription_item_id_timestamp'
73
+ );
52
74
 
53
- await queryInterface.addIndex('webhook_endpoints', ['status', 'livemode'], {
54
- name: 'idx_webhook_endpoint_status_livemode',
55
- });
75
+ await createIndexIfNotExists(
76
+ queryInterface,
77
+ 'webhook_endpoints',
78
+ ['status', 'livemode'],
79
+ 'idx_webhook_endpoint_status_livemode'
80
+ );
56
81
 
57
- await queryInterface.addIndex('payment_stats', ['timestamp', 'currency_id'], {
58
- name: 'idx_payment_stats_timestamp_currency_id',
59
- });
82
+ await createIndexIfNotExists(
83
+ queryInterface,
84
+ 'payment_stats',
85
+ ['timestamp', 'currency_id'],
86
+ 'idx_payment_stats_timestamp_currency_id'
87
+ );
60
88
 
61
- await queryInterface.addIndex('payouts', ['updated_at', 'status'], {
62
- name: 'idx_payouts_updated_at_status',
63
- });
89
+ await createIndexIfNotExists(queryInterface, 'payouts', ['updated_at', 'status'], 'idx_payouts_updated_at_status');
64
90
 
65
- await queryInterface.addIndex('refunds', ['status', 'type', 'updated_at'], {
66
- name: 'idx_refunds_status_type_updated_at',
67
- });
68
- await queryInterface.addIndex('refunds', ['subscription_id', 'type'], {
69
- name: 'idx_refunds_subscription_id_type',
70
- });
71
- await queryInterface.addIndex('refunds', ['payment_intent_id', 'type'], {
72
- name: 'idx_refunds_payment_intent_id_type',
73
- });
91
+ await createIndexIfNotExists(
92
+ queryInterface,
93
+ 'refunds',
94
+ ['status', 'type', 'updated_at'],
95
+ 'idx_refunds_status_type_updated_at'
96
+ );
97
+ await createIndexIfNotExists(
98
+ queryInterface,
99
+ 'refunds',
100
+ ['subscription_id', 'type'],
101
+ 'idx_refunds_subscription_id_type'
102
+ );
103
+ await createIndexIfNotExists(
104
+ queryInterface,
105
+ 'refunds',
106
+ ['payment_intent_id', 'type'],
107
+ 'idx_refunds_payment_intent_id_type'
108
+ );
74
109
  } catch (error) {
75
110
  console.error('Failed to create indexes', error);
76
111
  throw error;
@@ -0,0 +1,25 @@
1
+ import { DataTypes } from 'sequelize';
2
+
3
+ import { Migration, safeApplyColumnChanges } from '../migrate';
4
+
5
+ export const up: Migration = async ({ context }) => {
6
+ await safeApplyColumnChanges(context, {
7
+ customers: [
8
+ {
9
+ name: 'overdraft_protection',
10
+ field: {
11
+ type: DataTypes.JSON,
12
+ defaultValue: JSON.stringify({
13
+ status: 'inactive',
14
+ remaining_quantity: 0,
15
+ total: 0,
16
+ }),
17
+ },
18
+ },
19
+ ],
20
+ });
21
+ };
22
+
23
+ export const down: Migration = async ({ context }) => {
24
+ await context.removeColumn('customers', 'overdraft_protection');
25
+ };
@@ -0,0 +1,30 @@
1
+ import { DataTypes } from 'sequelize';
2
+
3
+ import { Migration, safeApplyColumnChanges } from '../migrate';
4
+
5
+ export const up: Migration = async ({ context }) => {
6
+ // 如果customer存在overdraft_protection,则删除
7
+ const columns = await context.describeTable('customers');
8
+ if (columns.overdraft_protection) {
9
+ await context.removeColumn('customers', 'overdraft_protection');
10
+ }
11
+ await safeApplyColumnChanges(context, {
12
+ subscriptions: [
13
+ {
14
+ name: 'overdraft_protection',
15
+ field: {
16
+ type: DataTypes.JSON,
17
+ defaultValue: JSON.stringify({
18
+ enabled: false,
19
+ payment_method_id: null,
20
+ payment_details: null,
21
+ }),
22
+ },
23
+ },
24
+ ],
25
+ });
26
+ };
27
+
28
+ export const down: Migration = async ({ context }) => {
29
+ await context.removeColumn('subscriptions', 'overdraft_protection');
30
+ };
@@ -162,13 +162,13 @@ export class Customer extends Model<InferAttributes<Customer>, InferCreationAttr
162
162
  return `${this.invoice_prefix}-${padStart(sequence.toString(), 4, '0')}`;
163
163
  }
164
164
 
165
- public async getSummary() {
165
+ public async getSummary(livemode?: boolean) {
166
166
  const { PaymentIntent, Refund, Invoice } = this.sequelize.models;
167
167
  const [paid, [due], refunded] = await Promise.all([
168
168
  // @ts-ignore
169
169
  PaymentIntent!.getPaidAmountByCustomer(this.id),
170
170
  // @ts-ignore
171
- Invoice!.getUncollectibleAmount({ customerId: this.id }),
171
+ Invoice!.getUncollectibleAmount({ customerId: this.id, livemode }),
172
172
  // @ts-ignore
173
173
  Refund!.getRefundAmountByCustomer(this.id),
174
174
  ]);
@@ -61,6 +61,8 @@ export class Invoice extends Model<InferAttributes<Invoice>, InferCreationAttrib
61
61
  | 'upcoming'
62
62
  | 'slash_stake'
63
63
  | 'stake'
64
+ | 'overdraft_protection'
65
+ | 'stake_overdraft_protection'
64
66
  | 'recharge',
65
67
  string
66
68
  >;
@@ -554,11 +556,13 @@ export class Invoice extends Model<InferAttributes<Invoice>, InferCreationAttrib
554
556
  subscriptionId,
555
557
  currencyId,
556
558
  excludedInvoiceId,
559
+ livemode,
557
560
  }: {
558
561
  customerId?: string;
559
562
  subscriptionId?: string;
560
563
  currencyId?: string;
561
564
  excludedInvoiceId?: string;
565
+ livemode?: boolean;
562
566
  }) {
563
567
  if (!customerId && !subscriptionId) {
564
568
  throw new Error('customerId or subscriptionId is required for getUncollectibleAmount');
@@ -579,6 +583,9 @@ export class Invoice extends Model<InferAttributes<Invoice>, InferCreationAttrib
579
583
  if (excludedInvoiceId) {
580
584
  where.id = { [Op.not]: excludedInvoiceId };
581
585
  }
586
+ if (typeof livemode === 'boolean') {
587
+ where.livemode = livemode;
588
+ }
582
589
 
583
590
  return this._getUncollectibleAmount(where);
584
591
  }
@@ -76,6 +76,13 @@ export class Lock extends Model<InferAttributes<Lock>, InferCreationAttributes<L
76
76
  return !!exist && exist.release_at > now;
77
77
  }
78
78
 
79
+ public static async release(id: string) {
80
+ const exist = await this.findByPk(id);
81
+ if (exist) {
82
+ await exist.update({ release_at: dayjs().unix() });
83
+ }
84
+ }
85
+
79
86
  public static associate() {}
80
87
  }
81
88
 
@@ -130,6 +130,12 @@ export class Subscription extends Model<InferAttributes<Subscription>, InferCrea
130
130
  declare created_at: CreationOptional<Date>;
131
131
  declare updated_at: CreationOptional<Date>;
132
132
 
133
+ declare overdraft_protection?: {
134
+ enabled: boolean;
135
+ payment_method_id: string;
136
+ payment_details: PaymentDetails;
137
+ };
138
+
133
139
  public static readonly GENESIS_ATTRIBUTES = {
134
140
  id: {
135
141
  type: DataTypes.STRING(30),
@@ -309,6 +315,15 @@ export class Subscription extends Model<InferAttributes<Subscription>, InferCrea
309
315
  type: DataTypes.STRING(40),
310
316
  allowNull: true,
311
317
  },
318
+ overdraft_protection: {
319
+ type: DataTypes.JSON,
320
+ allowNull: true,
321
+ defaultValue: JSON.stringify({
322
+ enabled: false,
323
+ payment_method_id: null,
324
+ payment_details: null,
325
+ }),
326
+ },
312
327
  },
313
328
  {
314
329
  sequelize,
@@ -7,7 +7,7 @@ import { join } from 'path';
7
7
  import CLS from 'cls-hooked';
8
8
  import { Sequelize } from 'sequelize';
9
9
 
10
- import env from '../libs/env';
10
+ import env, { sequelizeOptionsPoolIdle, sequelizeOptionsPoolMax, sequelizeOptionsPoolMin } from '../libs/env';
11
11
 
12
12
  const namespace = CLS.createNamespace('payment-kit');
13
13
 
@@ -18,6 +18,11 @@ export const sequelize = new Sequelize({
18
18
  dialect: 'sqlite',
19
19
  logging: process.env.SQL_LOG === '1',
20
20
  storage: join(env.dataDir, 'payment-kit.db'),
21
+ pool: {
22
+ min: sequelizeOptionsPoolMin,
23
+ max: sequelizeOptionsPoolMax,
24
+ idle: sequelizeOptionsPoolIdle,
25
+ },
21
26
  });
22
27
 
23
28
  sequelize.query('pragma journal_mode = WAL;');
package/blocklet.yml CHANGED
@@ -14,7 +14,7 @@ repository:
14
14
  type: git
15
15
  url: git+https://github.com/blocklet/payment-kit.git
16
16
  specVersion: 1.2.8
17
- version: 1.16.17
17
+ version: 1.16.18
18
18
  logo: logo.png
19
19
  files:
20
20
  - dist