payment-kit 1.18.30 → 1.18.32
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/api/src/crons/metering-subscription-detection.ts +9 -0
- package/api/src/integrations/arcblock/nft.ts +1 -0
- package/api/src/integrations/blocklet/passport.ts +1 -1
- package/api/src/integrations/stripe/handlers/invoice.ts +2 -2
- package/api/src/integrations/stripe/handlers/setup-intent.ts +29 -1
- package/api/src/integrations/stripe/handlers/subscription.ts +19 -15
- package/api/src/integrations/stripe/resource.ts +81 -1
- package/api/src/libs/audit.ts +42 -0
- package/api/src/libs/invoice.ts +54 -7
- package/api/src/libs/notification/index.ts +72 -4
- package/api/src/libs/notification/template/base.ts +2 -0
- package/api/src/libs/notification/template/subscription-renew-failed.ts +1 -5
- package/api/src/libs/notification/template/subscription-renewed.ts +1 -5
- package/api/src/libs/notification/template/subscription-succeeded.ts +8 -18
- package/api/src/libs/notification/template/subscription-trial-start.ts +2 -10
- package/api/src/libs/notification/template/subscription-upgraded.ts +1 -5
- package/api/src/libs/payment.ts +47 -14
- package/api/src/libs/product.ts +1 -4
- package/api/src/libs/session.ts +600 -8
- package/api/src/libs/setting.ts +172 -0
- package/api/src/libs/subscription.ts +7 -69
- package/api/src/libs/ws.ts +5 -0
- package/api/src/queues/checkout-session.ts +42 -36
- package/api/src/queues/notification.ts +3 -2
- package/api/src/queues/payment.ts +33 -6
- package/api/src/queues/usage-record.ts +2 -10
- package/api/src/routes/checkout-sessions.ts +324 -187
- package/api/src/routes/connect/shared.ts +160 -38
- package/api/src/routes/connect/subscribe.ts +123 -64
- package/api/src/routes/payment-currencies.ts +3 -6
- package/api/src/routes/payment-links.ts +11 -1
- package/api/src/routes/payment-stats.ts +2 -2
- package/api/src/routes/payouts.ts +2 -1
- package/api/src/routes/settings.ts +45 -0
- package/api/src/routes/subscriptions.ts +1 -2
- package/api/src/store/migrations/20250408-subscription-grouping.ts +39 -0
- package/api/src/store/migrations/20250419-subscription-grouping.ts +69 -0
- package/api/src/store/models/checkout-session.ts +52 -0
- package/api/src/store/models/index.ts +1 -0
- package/api/src/store/models/payment-link.ts +6 -0
- package/api/src/store/models/subscription.ts +8 -6
- package/api/src/store/models/types.ts +31 -1
- package/api/tests/libs/session.spec.ts +423 -0
- package/api/tests/libs/subscription.spec.ts +0 -110
- package/blocklet.yml +3 -1
- package/package.json +20 -19
- package/scripts/sdk.js +486 -155
- package/src/locales/en.tsx +1 -1
- package/src/locales/zh.tsx +1 -1
- package/src/pages/admin/settings/vault-config/edit-form.tsx +1 -1
- package/src/pages/customer/subscription/change-payment.tsx +8 -3
|
@@ -8,9 +8,9 @@ import { getFastCheckoutAmount } from '../../libs/session';
|
|
|
8
8
|
import { getTxMetadata } from '../../libs/util';
|
|
9
9
|
import { invoiceQueue } from '../../queues/invoice';
|
|
10
10
|
import { addSubscriptionJob } from '../../queues/subscription';
|
|
11
|
-
import type { Invoice, TLineItemExpanded } from '../../store/models';
|
|
11
|
+
import type { Invoice, Subscription, TLineItemExpanded } from '../../store/models';
|
|
12
12
|
import {
|
|
13
|
-
|
|
13
|
+
ensureInvoicesForSubscriptions,
|
|
14
14
|
ensurePaymentIntent,
|
|
15
15
|
executeOcapTransactions,
|
|
16
16
|
getAuthPrincipalClaim,
|
|
@@ -20,6 +20,14 @@ import {
|
|
|
20
20
|
import { ensureStakeInvoice } from '../../libs/invoice';
|
|
21
21
|
import { EVM_CHAIN_TYPES } from '../../libs/constants';
|
|
22
22
|
|
|
23
|
+
const updateInvoices = async (invoices: Invoice[], update: Partial<Invoice>) => {
|
|
24
|
+
await Promise.all(invoices.map((invoice) => invoice.update(update)));
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const updateSubscriptions = async (subscriptions: Subscription[], update: Partial<Subscription>) => {
|
|
28
|
+
await Promise.all(subscriptions.map((subscription) => subscription.update(update)));
|
|
29
|
+
};
|
|
30
|
+
|
|
23
31
|
export default {
|
|
24
32
|
action: 'subscription',
|
|
25
33
|
authPrincipal: false,
|
|
@@ -33,12 +41,16 @@ export default {
|
|
|
33
41
|
onConnect: async (args: CallbackArgs) => {
|
|
34
42
|
const { userDid, userPk, extraParams } = args;
|
|
35
43
|
const { checkoutSessionId, connectedDid, sessionUserDid } = extraParams;
|
|
36
|
-
const {
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
44
|
+
const {
|
|
45
|
+
checkoutSession,
|
|
46
|
+
paymentMethod,
|
|
47
|
+
paymentCurrency,
|
|
48
|
+
subscriptions,
|
|
49
|
+
customer,
|
|
50
|
+
subscription: primarySubscription,
|
|
51
|
+
} = await ensurePaymentIntent(checkoutSessionId, connectedDid || sessionUserDid || userDid);
|
|
52
|
+
if (!subscriptions || subscriptions.length === 0) {
|
|
53
|
+
throw new Error('No subscriptions found for checkoutSession');
|
|
42
54
|
}
|
|
43
55
|
|
|
44
56
|
const now = dayjs().unix();
|
|
@@ -52,6 +64,8 @@ export default {
|
|
|
52
64
|
const minStakeAmount = Number(checkoutSession.subscription_data?.min_stake_amount || 0);
|
|
53
65
|
const fastCheckoutAmount = getFastCheckoutAmount(items, checkoutSession.mode, paymentCurrency.id, trialing);
|
|
54
66
|
const claimsList: any[] = [];
|
|
67
|
+
|
|
68
|
+
const allSubscriptionIds = subscriptions.map((sub) => sub.id);
|
|
55
69
|
if (paymentMethod.type === 'arcblock') {
|
|
56
70
|
const delegation = await isDelegationSufficientForPayment({
|
|
57
71
|
paymentMethod,
|
|
@@ -61,14 +75,19 @@ export default {
|
|
|
61
75
|
});
|
|
62
76
|
|
|
63
77
|
// if we can complete purchase without any wallet interaction
|
|
64
|
-
if
|
|
78
|
+
// we forced to delegate if we can skip stake
|
|
79
|
+
if (delegation.sufficient === false || checkoutSession.subscription_data?.no_stake) {
|
|
65
80
|
claimsList.push({
|
|
66
81
|
signature: await getDelegationTxClaim({
|
|
67
82
|
mode: checkoutSession.mode,
|
|
68
83
|
userDid,
|
|
69
84
|
userPk,
|
|
70
85
|
nonce: checkoutSession.id,
|
|
71
|
-
data: getTxMetadata({
|
|
86
|
+
data: getTxMetadata({
|
|
87
|
+
subscriptionId: primarySubscription?.id,
|
|
88
|
+
subscriptionIds: allSubscriptionIds,
|
|
89
|
+
checkoutSessionId,
|
|
90
|
+
}),
|
|
72
91
|
paymentCurrency,
|
|
73
92
|
paymentMethod,
|
|
74
93
|
trialing,
|
|
@@ -78,17 +97,19 @@ export default {
|
|
|
78
97
|
});
|
|
79
98
|
}
|
|
80
99
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
100
|
+
if (!checkoutSession.subscription_data?.no_stake) {
|
|
101
|
+
claimsList.push({
|
|
102
|
+
prepareTx: await getStakeTxClaim({
|
|
103
|
+
userDid,
|
|
104
|
+
userPk,
|
|
105
|
+
paymentCurrency,
|
|
106
|
+
paymentMethod,
|
|
107
|
+
items,
|
|
108
|
+
subscription: primarySubscription as Subscription,
|
|
109
|
+
subscriptions,
|
|
110
|
+
}),
|
|
111
|
+
});
|
|
112
|
+
}
|
|
92
113
|
|
|
93
114
|
return claimsList;
|
|
94
115
|
}
|
|
@@ -104,7 +125,11 @@ export default {
|
|
|
104
125
|
userDid,
|
|
105
126
|
userPk,
|
|
106
127
|
nonce: checkoutSession.id,
|
|
107
|
-
data: getTxMetadata({
|
|
128
|
+
data: getTxMetadata({
|
|
129
|
+
subscriptionId: primarySubscription?.id,
|
|
130
|
+
subscriptionIds: allSubscriptionIds,
|
|
131
|
+
checkoutSessionId,
|
|
132
|
+
}),
|
|
108
133
|
paymentCurrency,
|
|
109
134
|
paymentMethod,
|
|
110
135
|
trialing,
|
|
@@ -121,7 +146,7 @@ export default {
|
|
|
121
146
|
onAuth: async (args: CallbackArgs) => {
|
|
122
147
|
const { request, userDid, userPk, claims, extraParams, updateSession, step } = args;
|
|
123
148
|
const { checkoutSessionId, connectedDid, sessionUserDid } = extraParams;
|
|
124
|
-
const { checkoutSession, customer, paymentMethod,
|
|
149
|
+
const { checkoutSession, customer, paymentMethod, subscriptions, paymentCurrency } = await ensurePaymentIntent(
|
|
125
150
|
checkoutSessionId,
|
|
126
151
|
connectedDid || sessionUserDid || userDid
|
|
127
152
|
);
|
|
@@ -134,7 +159,9 @@ export default {
|
|
|
134
159
|
},
|
|
135
160
|
});
|
|
136
161
|
const staking = result.find((x: any) => x.claim?.type === 'prepareTx' && x.claim?.meta?.purpose === 'staking');
|
|
137
|
-
const isFinalStep =
|
|
162
|
+
const isFinalStep =
|
|
163
|
+
(paymentMethod.type === 'arcblock' && (staking || checkoutSession.subscription_data?.no_stake)) ||
|
|
164
|
+
paymentMethod.type !== 'arcblock';
|
|
138
165
|
if (!isFinalStep) {
|
|
139
166
|
await updateSession({
|
|
140
167
|
result,
|
|
@@ -151,10 +178,11 @@ export default {
|
|
|
151
178
|
});
|
|
152
179
|
}
|
|
153
180
|
const claimsList = result.map((x: any) => x.claim);
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
throw new Error('Subscription for checkoutSession not found');
|
|
181
|
+
if (!subscriptions || subscriptions.length === 0) {
|
|
182
|
+
throw new Error('No subscriptions found for checkoutSession');
|
|
157
183
|
}
|
|
184
|
+
const primarySubscription = subscriptions[0] as Subscription;
|
|
185
|
+
|
|
158
186
|
const paymentSettings = {
|
|
159
187
|
payment_method_types: [paymentMethod.type],
|
|
160
188
|
payment_method_options: {
|
|
@@ -162,31 +190,44 @@ export default {
|
|
|
162
190
|
},
|
|
163
191
|
};
|
|
164
192
|
const prepareTxExecution = async () => {
|
|
165
|
-
await
|
|
166
|
-
payment_settings: paymentSettings,
|
|
167
|
-
});
|
|
193
|
+
await updateSubscriptions(subscriptions, { payment_settings: paymentSettings });
|
|
168
194
|
};
|
|
169
195
|
|
|
170
|
-
const afterTxExecution = async (
|
|
171
|
-
await
|
|
172
|
-
|
|
173
|
-
|
|
196
|
+
const afterTxExecution = async (invoices: Invoice[], paymentDetails: Record<string, any>) => {
|
|
197
|
+
await updateSubscriptions(subscriptions, { payment_details: { [paymentMethod.type]: paymentDetails } });
|
|
198
|
+
|
|
199
|
+
for (const invoice of invoices) {
|
|
200
|
+
if (invoice) {
|
|
201
|
+
invoiceQueue.pushAndWait({ id: invoice.id, job: { invoiceId: invoice.id, retryOnError: false } });
|
|
202
|
+
}
|
|
174
203
|
}
|
|
175
|
-
|
|
176
|
-
|
|
204
|
+
|
|
205
|
+
await Promise.all(
|
|
206
|
+
subscriptions.map((subscription) => addSubscriptionJob(subscription, 'cycle', false, subscription.trial_end))
|
|
207
|
+
);
|
|
208
|
+
|
|
209
|
+
logger.info('CheckoutSession updated with multiple subscriptions', {
|
|
177
210
|
checkoutSession: checkoutSession.id,
|
|
178
|
-
|
|
211
|
+
subscriptionIds: subscriptions.map((s) => s.id),
|
|
179
212
|
paymentDetails,
|
|
180
213
|
});
|
|
181
214
|
};
|
|
182
215
|
|
|
183
216
|
if (paymentMethod.type === 'arcblock') {
|
|
184
217
|
await prepareTxExecution();
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
218
|
+
|
|
219
|
+
const { invoices } = await ensureInvoicesForSubscriptions({
|
|
220
|
+
checkoutSession,
|
|
221
|
+
customer,
|
|
222
|
+
subscriptions,
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
if (invoices.length === 0) {
|
|
226
|
+
throw new Error('No invoices found for subscriptions');
|
|
188
227
|
}
|
|
189
228
|
|
|
229
|
+
await updateInvoices(invoices, { payment_settings: paymentSettings });
|
|
230
|
+
|
|
190
231
|
const requestArray = result
|
|
191
232
|
.map((item: { stepRequest?: Request }) => item.stepRequest)
|
|
192
233
|
.filter(Boolean) as Request[];
|
|
@@ -199,40 +240,58 @@ export default {
|
|
|
199
240
|
claimsList,
|
|
200
241
|
paymentMethod,
|
|
201
242
|
requestSource,
|
|
202
|
-
|
|
203
|
-
paymentCurrency?.contract
|
|
243
|
+
primarySubscription?.id, // use the primary subscription id
|
|
244
|
+
paymentCurrency?.contract,
|
|
245
|
+
subscriptions.map((s) => s.id).join('-') // use all subscription ids as nonce
|
|
204
246
|
);
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
247
|
+
|
|
248
|
+
// create a stake invoice
|
|
249
|
+
if (stakingAmount && stakingAmount !== '0') {
|
|
250
|
+
await ensureStakeInvoice(
|
|
251
|
+
{
|
|
252
|
+
total: stakingAmount,
|
|
253
|
+
description: 'Stake for subscription',
|
|
254
|
+
checkout_session_id: checkoutSessionId,
|
|
255
|
+
currency_id: paymentCurrency.id,
|
|
256
|
+
metadata: {
|
|
257
|
+
payment_details: {
|
|
258
|
+
arcblock: {
|
|
259
|
+
tx_hash: paymentDetails?.staking?.tx_hash,
|
|
260
|
+
payer: paymentDetails?.payer,
|
|
261
|
+
address: paymentDetails?.staking?.address,
|
|
262
|
+
},
|
|
217
263
|
},
|
|
264
|
+
subscription_ids: subscriptions.map((s) => s.id),
|
|
265
|
+
is_group_stake: subscriptions.length > 1,
|
|
218
266
|
},
|
|
219
267
|
},
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
268
|
+
primarySubscription,
|
|
269
|
+
paymentMethod,
|
|
270
|
+
customer,
|
|
271
|
+
subscriptions
|
|
272
|
+
);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
await afterTxExecution(invoices, paymentDetails);
|
|
226
276
|
|
|
227
277
|
return { hash: paymentDetails.tx_hash };
|
|
228
278
|
}
|
|
229
279
|
|
|
230
280
|
if (EVM_CHAIN_TYPES.includes(paymentMethod.type)) {
|
|
231
281
|
await prepareTxExecution();
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
282
|
+
|
|
283
|
+
const { invoices } = await ensureInvoicesForSubscriptions({
|
|
284
|
+
checkoutSession,
|
|
285
|
+
customer,
|
|
286
|
+
subscriptions,
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
if (invoices.length === 0) {
|
|
290
|
+
throw new Error('No invoices found for subscriptions');
|
|
235
291
|
}
|
|
292
|
+
|
|
293
|
+
await updateInvoices(invoices, { payment_settings: paymentSettings });
|
|
294
|
+
|
|
236
295
|
broadcastEvmTransaction(checkoutSessionId, 'pending', claimsList);
|
|
237
296
|
|
|
238
297
|
const paymentDetails = await executeEvmTransaction('approve', userDid, claimsList, paymentMethod);
|
|
@@ -242,7 +301,7 @@ export default {
|
|
|
242
301
|
paymentMethod.confirmation.block
|
|
243
302
|
)
|
|
244
303
|
.then(async () => {
|
|
245
|
-
await afterTxExecution(
|
|
304
|
+
await afterTxExecution(invoices, paymentDetails);
|
|
246
305
|
broadcastEvmTransaction(checkoutSessionId, 'confirmed', claimsList);
|
|
247
306
|
})
|
|
248
307
|
.catch(console.error);
|
|
@@ -8,7 +8,7 @@ import logger from '../libs/logger';
|
|
|
8
8
|
import { authenticate } from '../libs/security';
|
|
9
9
|
import { PaymentCurrency, TPaymentCurrency } from '../store/models/payment-currency';
|
|
10
10
|
import { PaymentMethod } from '../store/models/payment-method';
|
|
11
|
-
import {
|
|
11
|
+
import { EVM_CHAIN_TYPES } from '../libs/constants';
|
|
12
12
|
import { ethWallet, getVaultAddress, wallet } from '../libs/auth';
|
|
13
13
|
import { resolveAddressChainTypes } from '../libs/util';
|
|
14
14
|
import { depositVaultQueue } from '../queues/payment';
|
|
@@ -243,7 +243,7 @@ const UpdateVaultConfigSchema = Joi.object({
|
|
|
243
243
|
enabled: Joi.boolean().required(),
|
|
244
244
|
deposit_threshold: Joi.number().greater(0).required(),
|
|
245
245
|
withdraw_threshold: Joi.number().min(0).required(),
|
|
246
|
-
buffer_threshold: Joi.number().
|
|
246
|
+
buffer_threshold: Joi.number().min(0).required(),
|
|
247
247
|
});
|
|
248
248
|
router.put('/:id/vault-config', authOwner, async (req, res) => {
|
|
249
249
|
try {
|
|
@@ -269,10 +269,7 @@ router.put('/:id/vault-config', authOwner, async (req, res) => {
|
|
|
269
269
|
enabled: vaultConfig.enabled,
|
|
270
270
|
deposit_threshold: fromTokenToUnit(vaultConfig.deposit_threshold, paymentCurrency.decimal).toString(),
|
|
271
271
|
withdraw_threshold: fromTokenToUnit(vaultConfig.withdraw_threshold, paymentCurrency.decimal).toString(),
|
|
272
|
-
buffer_threshold: fromTokenToUnit(
|
|
273
|
-
vaultConfig.buffer_threshold || VAULT_BUFFER_THRESHOLD,
|
|
274
|
-
paymentCurrency.decimal
|
|
275
|
-
).toString(),
|
|
272
|
+
buffer_threshold: fromTokenToUnit(vaultConfig.buffer_threshold || 0, paymentCurrency.decimal).toString(),
|
|
276
273
|
},
|
|
277
274
|
};
|
|
278
275
|
|
|
@@ -51,6 +51,7 @@ const formatBeforeSave = (payload: any) => {
|
|
|
51
51
|
cross_sell_behavior: 'auto',
|
|
52
52
|
payment_intent_data: null,
|
|
53
53
|
donation_settings: null,
|
|
54
|
+
enable_subscription_grouping: false,
|
|
54
55
|
},
|
|
55
56
|
pick(payload, [
|
|
56
57
|
'name',
|
|
@@ -71,8 +72,15 @@ const formatBeforeSave = (payload: any) => {
|
|
|
71
72
|
'cross_sell_behavior',
|
|
72
73
|
'donation_settings',
|
|
73
74
|
'metadata',
|
|
75
|
+
'enable_subscription_grouping',
|
|
74
76
|
])
|
|
75
77
|
);
|
|
78
|
+
|
|
79
|
+
// TODO: need to support stake subscription
|
|
80
|
+
if (raw.enable_subscription_grouping === true && !raw.subscription_data?.no_stake) {
|
|
81
|
+
throw new Error('Subscription grouping is only supported for stake-free subscriptions');
|
|
82
|
+
}
|
|
83
|
+
|
|
76
84
|
if (raw.after_completion?.type === 'hosted_confirmation') {
|
|
77
85
|
// @ts-ignore
|
|
78
86
|
raw.after_completion.redirect = null;
|
|
@@ -83,7 +91,7 @@ const formatBeforeSave = (payload: any) => {
|
|
|
83
91
|
}
|
|
84
92
|
if (typeof payload.include_free_trial === 'boolean' && !payload.include_free_trial) {
|
|
85
93
|
// @ts-ignore
|
|
86
|
-
raw.subscription_data =
|
|
94
|
+
raw.subscription_data.trial_period_days = 0;
|
|
87
95
|
}
|
|
88
96
|
|
|
89
97
|
if (raw.nft_mint_settings?.enabled) {
|
|
@@ -181,6 +189,7 @@ const PaymentLinkCreateSchema = Joi.object({
|
|
|
181
189
|
.min(0)
|
|
182
190
|
.optional(),
|
|
183
191
|
allow_promotion_codes: Joi.boolean().optional(),
|
|
192
|
+
enable_subscription_grouping: Joi.boolean().optional(),
|
|
184
193
|
nft_mint_settings: Joi.object({
|
|
185
194
|
enabled: Joi.boolean().required(),
|
|
186
195
|
factory: Joi.string().max(40).empty('').optional(),
|
|
@@ -310,6 +319,7 @@ const PaymentLinkUpdateSchema = Joi.object({
|
|
|
310
319
|
.min(0)
|
|
311
320
|
.optional(),
|
|
312
321
|
allow_promotion_codes: Joi.boolean().optional(),
|
|
322
|
+
enable_subscription_grouping: Joi.boolean().optional(),
|
|
313
323
|
nft_mint_settings: Joi.object({
|
|
314
324
|
enabled: Joi.boolean().required(),
|
|
315
325
|
factory: Joi.string().max(40).empty('').optional(),
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Router } from 'express';
|
|
2
2
|
import Joi from 'joi';
|
|
3
|
-
import { Op } from 'sequelize';
|
|
3
|
+
import { Op, type WhereOptions } from 'sequelize';
|
|
4
4
|
import { joinURL } from 'ufo';
|
|
5
5
|
|
|
6
6
|
import { getPaymentStat } from '../crons/payment-stat';
|
|
@@ -33,7 +33,7 @@ const schema = createListParamSchema<{ currency_id?: string; start?: number; end
|
|
|
33
33
|
});
|
|
34
34
|
router.get('/', auth, async (req, res) => {
|
|
35
35
|
const { page, pageSize, ...query } = await schema.validateAsync(req.query, { stripUnknown: true });
|
|
36
|
-
const where:
|
|
36
|
+
const where: WhereOptions = {};
|
|
37
37
|
|
|
38
38
|
if (typeof query.livemode === 'boolean') {
|
|
39
39
|
where.livemode = query.livemode;
|
|
@@ -4,6 +4,7 @@ import Joi from 'joi';
|
|
|
4
4
|
import pick from 'lodash/pick';
|
|
5
5
|
|
|
6
6
|
import sessionMiddleware from '@blocklet/sdk/lib/middlewares/session';
|
|
7
|
+
import type { WhereOptions } from 'sequelize';
|
|
7
8
|
import { createListParamSchema, getOrder, getWhereFromKvQuery, MetadataSchema } from '../libs/api';
|
|
8
9
|
import { authenticate } from '../libs/security';
|
|
9
10
|
import { formatMetadata } from '../libs/util';
|
|
@@ -136,7 +137,7 @@ router.get('/mine', sessionMiddleware(), async (req, res) => {
|
|
|
136
137
|
throw new Error(`Customer not found: ${req.user?.did}`);
|
|
137
138
|
}
|
|
138
139
|
|
|
139
|
-
const where:
|
|
140
|
+
const where: WhereOptions = { customer_id: customer.id };
|
|
140
141
|
if (currencyId) {
|
|
141
142
|
where.currency_id = currencyId;
|
|
142
143
|
}
|
|
@@ -171,6 +171,18 @@ router.post('/', async (req, res) => {
|
|
|
171
171
|
raw.settings.amount = settings.amount;
|
|
172
172
|
}
|
|
173
173
|
}
|
|
174
|
+
if (type === 'notification') {
|
|
175
|
+
const notificationSchema = Joi.object({
|
|
176
|
+
self_handle: Joi.boolean().required().default(false),
|
|
177
|
+
include_events: Joi.array().items(Joi.string()).optional(),
|
|
178
|
+
exclude_events: Joi.array().items(Joi.string()).optional(),
|
|
179
|
+
});
|
|
180
|
+
const { error: notificationError, value: notificationSettings } = notificationSchema.validate(settings);
|
|
181
|
+
if (notificationError) {
|
|
182
|
+
return res.status(400).json({ error: notificationError.message });
|
|
183
|
+
}
|
|
184
|
+
raw.settings = notificationSettings;
|
|
185
|
+
}
|
|
174
186
|
const exist = await Setting.findOne({
|
|
175
187
|
where: {
|
|
176
188
|
type,
|
|
@@ -251,6 +263,18 @@ router.put('/:mountLocationOrId', authAdmin, async (req, res) => {
|
|
|
251
263
|
raw.settings.amount = settings.amount;
|
|
252
264
|
}
|
|
253
265
|
}
|
|
266
|
+
if (setting.type === 'notification') {
|
|
267
|
+
const notificationSchema = Joi.object({
|
|
268
|
+
self_handle: Joi.boolean().required().default(false),
|
|
269
|
+
include_events: Joi.array().items(Joi.string()).optional(),
|
|
270
|
+
exclude_events: Joi.array().items(Joi.string()).optional(),
|
|
271
|
+
});
|
|
272
|
+
const { error: notificationError, value: notificationSettings } = notificationSchema.validate(settings);
|
|
273
|
+
if (notificationError) {
|
|
274
|
+
return res.status(400).json({ error: notificationError.message });
|
|
275
|
+
}
|
|
276
|
+
raw.settings = notificationSettings;
|
|
277
|
+
}
|
|
254
278
|
const doc = await setting.update(raw);
|
|
255
279
|
return res.json(doc);
|
|
256
280
|
} catch (err) {
|
|
@@ -284,4 +308,25 @@ router.delete('/:mountLocationOrId', authAdmin, async (req, res) => {
|
|
|
284
308
|
}
|
|
285
309
|
});
|
|
286
310
|
|
|
311
|
+
router.get('/:mountLocationOrId', authAdmin, async (req, res) => {
|
|
312
|
+
try {
|
|
313
|
+
const setting = await Setting.findOne({
|
|
314
|
+
where: {
|
|
315
|
+
[Op.or]: [
|
|
316
|
+
{
|
|
317
|
+
id: req.params.mountLocationOrId,
|
|
318
|
+
},
|
|
319
|
+
{
|
|
320
|
+
mount_location: req.params.mountLocationOrId,
|
|
321
|
+
},
|
|
322
|
+
],
|
|
323
|
+
},
|
|
324
|
+
});
|
|
325
|
+
return res.json(setting);
|
|
326
|
+
} catch (err) {
|
|
327
|
+
logger.error(err);
|
|
328
|
+
return res.status(400).json({ error: err.message });
|
|
329
|
+
}
|
|
330
|
+
});
|
|
331
|
+
|
|
287
332
|
export default router;
|
|
@@ -15,12 +15,11 @@ import dayjs from '../libs/dayjs';
|
|
|
15
15
|
import logger from '../libs/logger';
|
|
16
16
|
import { isDelegationSufficientForPayment } from '../libs/payment';
|
|
17
17
|
import { authenticate } from '../libs/security';
|
|
18
|
-
import { expandLineItems, getFastCheckoutAmount, isLineItemAligned } from '../libs/session';
|
|
18
|
+
import { expandLineItems, getFastCheckoutAmount, getSubscriptionCreateSetup, isLineItemAligned } from '../libs/session';
|
|
19
19
|
import {
|
|
20
20
|
createProration,
|
|
21
21
|
finalizeSubscriptionUpdate,
|
|
22
22
|
getPastInvoicesAmount,
|
|
23
|
-
getSubscriptionCreateSetup,
|
|
24
23
|
getSubscriptionPaymentAddress,
|
|
25
24
|
getSubscriptionRefundSetup,
|
|
26
25
|
getSubscriptionStakeReturnSetup,
|
|
@@ -0,0 +1,39 @@
|
|
|
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
|
+
checkout_sessions: [
|
|
8
|
+
{
|
|
9
|
+
name: 'enable_subscription_grouping',
|
|
10
|
+
field: {
|
|
11
|
+
type: DataTypes.BOOLEAN,
|
|
12
|
+
defaultValue: false,
|
|
13
|
+
},
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
name: 'subscription_groups',
|
|
17
|
+
field: {
|
|
18
|
+
type: DataTypes.JSON,
|
|
19
|
+
allowNull: true,
|
|
20
|
+
},
|
|
21
|
+
},
|
|
22
|
+
],
|
|
23
|
+
payment_links: [
|
|
24
|
+
{
|
|
25
|
+
name: 'enable_subscription_grouping',
|
|
26
|
+
field: {
|
|
27
|
+
type: DataTypes.BOOLEAN,
|
|
28
|
+
defaultValue: false,
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
],
|
|
32
|
+
});
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
export const down: Migration = async ({ context }) => {
|
|
36
|
+
await context.removeColumn('checkout_sessions', 'enable_subscription_grouping');
|
|
37
|
+
await context.removeColumn('checkout_sessions', 'subscription_groups');
|
|
38
|
+
await context.removeColumn('payment_links', 'enable_subscription_grouping');
|
|
39
|
+
};
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { DataTypes, QueryTypes } from 'sequelize';
|
|
2
|
+
import { Migration, safeApplyColumnChanges } from '../migrate';
|
|
3
|
+
|
|
4
|
+
export const up: Migration = async ({ context }) => {
|
|
5
|
+
try {
|
|
6
|
+
// 添加 success_subscription_count 字段
|
|
7
|
+
await safeApplyColumnChanges(context, {
|
|
8
|
+
checkout_sessions: [
|
|
9
|
+
{
|
|
10
|
+
name: 'success_subscription_count',
|
|
11
|
+
field: {
|
|
12
|
+
type: DataTypes.INTEGER,
|
|
13
|
+
defaultValue: 0,
|
|
14
|
+
},
|
|
15
|
+
},
|
|
16
|
+
],
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
const sessions = (await context.sequelize.query(
|
|
20
|
+
`SELECT id, subscription_id, subscription_groups
|
|
21
|
+
FROM checkout_sessions
|
|
22
|
+
WHERE payment_status = 'paid' AND (mode = 'subscription' OR mode = 'setup')`,
|
|
23
|
+
{ type: QueryTypes.SELECT }
|
|
24
|
+
)) as any[];
|
|
25
|
+
|
|
26
|
+
if (sessions.length > 0) {
|
|
27
|
+
// 收集更新数据
|
|
28
|
+
const updates: { id: string; count: number }[] = [];
|
|
29
|
+
for (const session of sessions) {
|
|
30
|
+
let count = 0;
|
|
31
|
+
if (session.subscription_groups) {
|
|
32
|
+
try {
|
|
33
|
+
const groups = JSON.parse(session.subscription_groups);
|
|
34
|
+
count = Object.keys(groups).length;
|
|
35
|
+
} catch (e) {
|
|
36
|
+
console.warn(`Failed to parse subscription_groups for session ${session.id}`);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (count === 0 && session.subscription_id) {
|
|
41
|
+
count = 1;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (count > 0) {
|
|
45
|
+
updates.push({ id: session.id, count });
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// 使用事务确保原子性
|
|
50
|
+
await context.sequelize.transaction(async (t) => {
|
|
51
|
+
for (const update of updates) {
|
|
52
|
+
// eslint-disable-next-line no-await-in-loop
|
|
53
|
+
await context.sequelize.query('UPDATE checkout_sessions SET success_subscription_count = ? WHERE id = ?', {
|
|
54
|
+
replacements: [update.count, update.id],
|
|
55
|
+
transaction: t,
|
|
56
|
+
type: QueryTypes.UPDATE,
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
} catch (error) {
|
|
62
|
+
console.error('Migration failed:', error);
|
|
63
|
+
throw error;
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
export const down: Migration = async ({ context }) => {
|
|
68
|
+
await context.removeColumn('checkout_sessions', 'success_subscription_count');
|
|
69
|
+
};
|