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.
- package/api/src/crons/index.ts +1 -1
- package/api/src/hooks/pre-start.ts +2 -0
- package/api/src/index.ts +2 -0
- package/api/src/integrations/arcblock/stake.ts +7 -1
- package/api/src/integrations/stripe/resource.ts +1 -1
- package/api/src/libs/env.ts +12 -0
- package/api/src/libs/event.ts +8 -0
- package/api/src/libs/invoice.ts +585 -3
- package/api/src/libs/notification/template/subscription-succeeded.ts +1 -2
- package/api/src/libs/notification/template/subscription-trial-will-end.ts +2 -2
- package/api/src/libs/notification/template/subscription-will-renew.ts +6 -2
- package/api/src/libs/notification/template/subscription.overdraft-protection.exhausted.ts +139 -0
- package/api/src/libs/overdraft-protection.ts +85 -0
- package/api/src/libs/payment.ts +1 -65
- package/api/src/libs/queue/index.ts +0 -1
- package/api/src/libs/subscription.ts +532 -2
- package/api/src/libs/util.ts +4 -0
- package/api/src/locales/en.ts +5 -0
- package/api/src/locales/zh.ts +5 -0
- package/api/src/queues/event.ts +3 -2
- package/api/src/queues/invoice.ts +28 -3
- package/api/src/queues/notification.ts +25 -3
- package/api/src/queues/payment.ts +154 -3
- package/api/src/queues/refund.ts +2 -2
- package/api/src/queues/subscription.ts +215 -4
- package/api/src/queues/webhook.ts +1 -0
- package/api/src/routes/connect/change-payment.ts +1 -1
- package/api/src/routes/connect/change-plan.ts +1 -1
- package/api/src/routes/connect/overdraft-protection.ts +120 -0
- package/api/src/routes/connect/recharge.ts +2 -1
- package/api/src/routes/connect/setup.ts +1 -1
- package/api/src/routes/connect/shared.ts +117 -350
- package/api/src/routes/connect/subscribe.ts +1 -1
- package/api/src/routes/customers.ts +2 -2
- package/api/src/routes/invoices.ts +9 -4
- package/api/src/routes/subscriptions.ts +172 -2
- package/api/src/store/migrate.ts +9 -10
- package/api/src/store/migrations/20240905-index.ts +95 -60
- package/api/src/store/migrations/20241203-overdraft-protection.ts +25 -0
- package/api/src/store/migrations/20241216-update-overdraft-protection.ts +30 -0
- package/api/src/store/models/customer.ts +2 -2
- package/api/src/store/models/invoice.ts +7 -0
- package/api/src/store/models/lock.ts +7 -0
- package/api/src/store/models/subscription.ts +15 -0
- package/api/src/store/sequelize.ts +6 -1
- package/blocklet.yml +1 -1
- package/package.json +23 -23
- package/src/components/customer/overdraft-protection.tsx +367 -0
- package/src/components/event/list.tsx +3 -4
- package/src/components/subscription/actions/cancel.tsx +3 -0
- package/src/components/subscription/portal/actions.tsx +324 -77
- package/src/components/uploader.tsx +31 -26
- package/src/env.d.ts +1 -0
- package/src/hooks/subscription.ts +30 -0
- package/src/libs/env.ts +4 -0
- package/src/locales/en.tsx +41 -0
- package/src/locales/zh.tsx +37 -0
- package/src/pages/admin/billing/invoices/detail.tsx +16 -15
- package/src/pages/customer/index.tsx +7 -2
- package/src/pages/customer/invoice/detail.tsx +29 -5
- package/src/pages/customer/invoice/past-due.tsx +18 -4
- package/src/pages/customer/recharge.tsx +2 -4
- package/src/pages/customer/subscription/change-payment.tsx +7 -1
- package/src/pages/customer/subscription/detail.tsx +69 -51
- package/tsconfig.json +0 -5
- 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
|
-
|
|
220
|
-
|
|
221
|
-
|
|
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 {
|
|
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 '
|
|
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;
|
package/api/src/store/migrate.ts
CHANGED
|
@@ -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
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
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
|
|
13
|
-
|
|
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
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
await queryInterface
|
|
23
|
-
|
|
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
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
await
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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
|
|
50
|
-
|
|
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
|
|
54
|
-
|
|
55
|
-
|
|
75
|
+
await createIndexIfNotExists(
|
|
76
|
+
queryInterface,
|
|
77
|
+
'webhook_endpoints',
|
|
78
|
+
['status', 'livemode'],
|
|
79
|
+
'idx_webhook_endpoint_status_livemode'
|
|
80
|
+
);
|
|
56
81
|
|
|
57
|
-
await
|
|
58
|
-
|
|
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
|
|
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
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
await
|
|
72
|
-
|
|
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;');
|