payment-kit 1.19.18 → 1.19.20
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/index.ts +3 -1
- package/api/src/integrations/ethereum/tx.ts +11 -0
- package/api/src/integrations/stripe/handlers/invoice.ts +26 -6
- package/api/src/integrations/stripe/handlers/setup-intent.ts +34 -2
- package/api/src/integrations/stripe/resource.ts +185 -1
- package/api/src/libs/invoice.ts +2 -1
- package/api/src/libs/session.ts +6 -1
- package/api/src/queues/auto-recharge.ts +343 -0
- package/api/src/queues/credit-consume.ts +15 -1
- package/api/src/queues/credit-grant.ts +15 -0
- package/api/src/queues/payment.ts +14 -1
- package/api/src/queues/space.ts +1 -0
- package/api/src/routes/auto-recharge-configs.ts +454 -0
- package/api/src/routes/connect/auto-recharge-auth.ts +182 -0
- package/api/src/routes/connect/recharge-account.ts +72 -10
- package/api/src/routes/connect/setup.ts +5 -3
- package/api/src/routes/connect/shared.ts +45 -4
- package/api/src/routes/customers.ts +10 -6
- package/api/src/routes/index.ts +2 -0
- package/api/src/routes/invoices.ts +10 -1
- package/api/src/routes/meters.ts +1 -1
- package/api/src/routes/payment-currencies.ts +129 -0
- package/api/src/store/migrate.ts +20 -0
- package/api/src/store/migrations/20250821-auto-recharge-config.ts +38 -0
- package/api/src/store/models/auto-recharge-config.ts +225 -0
- package/api/src/store/models/credit-grant.ts +1 -1
- package/api/src/store/models/customer.ts +1 -0
- package/api/src/store/models/index.ts +3 -0
- package/api/src/store/models/invoice.ts +2 -1
- package/api/src/store/models/payment-currency.ts +10 -2
- package/api/src/store/models/types.ts +11 -0
- package/blocklet.yml +1 -1
- package/package.json +18 -17
- package/src/components/customer/credit-overview.tsx +103 -18
- package/src/components/customer/overdraft-protection.tsx +5 -5
- package/src/components/info-metric.tsx +11 -2
- package/src/components/invoice/recharge.tsx +8 -2
- package/src/components/metadata/form.tsx +29 -27
- package/src/components/meter/form.tsx +1 -2
- package/src/components/price/form.tsx +39 -26
- package/src/components/product/form.tsx +1 -2
- package/src/locales/en.tsx +15 -0
- package/src/locales/zh.tsx +14 -0
- package/src/pages/admin/billing/meters/detail.tsx +18 -0
- package/src/pages/admin/customers/customers/credit-grant/detail.tsx +10 -0
- package/src/pages/admin/products/prices/actions.tsx +42 -2
- package/src/pages/admin/products/products/create.tsx +1 -2
- package/src/pages/admin/settings/vault-config/edit-form.tsx +8 -8
- package/src/pages/customer/credit-grant/detail.tsx +9 -1
- package/src/pages/customer/recharge/account.tsx +14 -7
- package/src/pages/customer/recharge/subscription.tsx +4 -4
- package/vite.config.ts +26 -1
|
@@ -0,0 +1,454 @@
|
|
|
1
|
+
import { Router } from 'express';
|
|
2
|
+
import Joi from 'joi';
|
|
3
|
+
|
|
4
|
+
import { CustomError } from '@blocklet/error';
|
|
5
|
+
import { Op } from 'sequelize';
|
|
6
|
+
import sessionMiddleware from '@blocklet/sdk/lib/middlewares/session';
|
|
7
|
+
import { BN, fromTokenToUnit, fromUnitToToken } from '@ocap/util';
|
|
8
|
+
import {
|
|
9
|
+
AutoRechargeConfig,
|
|
10
|
+
Customer,
|
|
11
|
+
EVMChainType,
|
|
12
|
+
PaymentCurrency,
|
|
13
|
+
PaymentMethod,
|
|
14
|
+
Price,
|
|
15
|
+
Product,
|
|
16
|
+
TPriceExpanded,
|
|
17
|
+
} from '../store/models';
|
|
18
|
+
import { isDelegationSufficientForPayment } from '../libs/payment';
|
|
19
|
+
import { getPriceUintAmountByCurrency, validateStripePaymentAmounts } from '../libs/session';
|
|
20
|
+
import { ensureStripeSetupIntentForAutoRecharge } from '../integrations/stripe/resource';
|
|
21
|
+
import logger from '../libs/logger';
|
|
22
|
+
|
|
23
|
+
const router = Router();
|
|
24
|
+
|
|
25
|
+
const createConfigSchema = Joi.object({
|
|
26
|
+
customer_id: Joi.string().required(),
|
|
27
|
+
enabled: Joi.boolean().default(false),
|
|
28
|
+
threshold: Joi.number().default(0),
|
|
29
|
+
currency_id: Joi.string().required(),
|
|
30
|
+
recharge_currency_id: Joi.string().optional(),
|
|
31
|
+
payment_method_id: Joi.string().optional(),
|
|
32
|
+
price_id: Joi.string().optional(),
|
|
33
|
+
quantity: Joi.number().integer().min(1).default(1),
|
|
34
|
+
change_payment_method: Joi.boolean().default(false),
|
|
35
|
+
daily_limits: Joi.object({
|
|
36
|
+
max_attempts: Joi.number().integer().min(0).default(0).optional(),
|
|
37
|
+
max_amount: Joi.number().min(0).default(0).optional(),
|
|
38
|
+
}).optional(),
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
const getConfigSchema = Joi.object({
|
|
42
|
+
currency_id: Joi.string().optional(),
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
// Helper functions for validation and retrieval
|
|
46
|
+
async function ensureCustomerExists(customerId: string): Promise<Customer> {
|
|
47
|
+
const customer = await Customer.findByPkOrDid(customerId);
|
|
48
|
+
if (!customer) {
|
|
49
|
+
throw new CustomError(404, 'Customer not found');
|
|
50
|
+
}
|
|
51
|
+
return customer;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async function ensureCurrencyWithRechargeConfig(currencyId: string): Promise<PaymentCurrency> {
|
|
55
|
+
const currency = await PaymentCurrency.scope('withRechargeConfig').findByPk(currencyId);
|
|
56
|
+
if (!currency) {
|
|
57
|
+
throw new CustomError(404, 'Payment currency not found');
|
|
58
|
+
}
|
|
59
|
+
if (!currency.recharge_config?.base_price_id) {
|
|
60
|
+
throw new CustomError(404, 'No top-up package found');
|
|
61
|
+
}
|
|
62
|
+
return currency;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async function ensurePaymentMethodExists(paymentMethodId: string): Promise<PaymentMethod> {
|
|
66
|
+
const paymentMethod = await PaymentMethod.findByPk(paymentMethodId);
|
|
67
|
+
if (!paymentMethod) {
|
|
68
|
+
throw new CustomError(400, `Payment method not found: ${paymentMethodId}`);
|
|
69
|
+
}
|
|
70
|
+
return paymentMethod;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Apply balance check results to configuration
|
|
74
|
+
async function applyBalanceCheckResults(
|
|
75
|
+
config: AutoRechargeConfig,
|
|
76
|
+
balanceResult: any,
|
|
77
|
+
paymentMethod: PaymentMethod,
|
|
78
|
+
enableIfSufficient = false
|
|
79
|
+
): Promise<void> {
|
|
80
|
+
const updates: any = {};
|
|
81
|
+
|
|
82
|
+
if (balanceResult.stripeContext) {
|
|
83
|
+
updates.payment_details = {
|
|
84
|
+
stripe: {
|
|
85
|
+
setup_intent_id: balanceResult.stripeContext.id,
|
|
86
|
+
customer_id: balanceResult.stripeContext.stripe_customer_id,
|
|
87
|
+
},
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const existingPayer =
|
|
92
|
+
config.payment_settings?.payment_method_options?.[
|
|
93
|
+
paymentMethod.type as keyof typeof config.payment_settings.payment_method_options
|
|
94
|
+
]?.payer;
|
|
95
|
+
|
|
96
|
+
if (balanceResult.sufficient && balanceResult.payer && balanceResult.payer !== existingPayer) {
|
|
97
|
+
updates.payment_settings = {
|
|
98
|
+
payment_method_options: {
|
|
99
|
+
...(config.payment_settings?.payment_method_options || {}),
|
|
100
|
+
[paymentMethod.type]: {
|
|
101
|
+
...(config.payment_settings?.payment_method_options?.[
|
|
102
|
+
paymentMethod.type as keyof typeof config.payment_settings.payment_method_options
|
|
103
|
+
] || {}),
|
|
104
|
+
payer: balanceResult.payer,
|
|
105
|
+
},
|
|
106
|
+
},
|
|
107
|
+
payment_method_types: [paymentMethod.type],
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
if (enableIfSufficient) {
|
|
111
|
+
updates.enabled = true;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (Object.keys(updates).length > 0) {
|
|
116
|
+
await config.update(updates);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Get auto-recharge configuration for a customer
|
|
121
|
+
router.get('/customer/:customerId', sessionMiddleware({ accessKey: true }), async (req, res) => {
|
|
122
|
+
const { error, value } = getConfigSchema.validate(req.query, {
|
|
123
|
+
stripUnknown: true,
|
|
124
|
+
});
|
|
125
|
+
if (error) {
|
|
126
|
+
throw new CustomError(400, error.message || 'Validation error');
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (!req.user) {
|
|
130
|
+
throw new CustomError(403, 'Please login to continue');
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
let { customerId } = req.params;
|
|
134
|
+
if (!customerId) {
|
|
135
|
+
customerId = req.user?.did;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const { currency_id: currencyId } = value;
|
|
139
|
+
const customer = await ensureCustomerExists(customerId);
|
|
140
|
+
|
|
141
|
+
if (customer.did !== req.user?.did && !['admin', 'owner'].includes(req.user?.role)) {
|
|
142
|
+
throw new CustomError(403, 'You are not allowed to access this resource');
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const where: any = {
|
|
146
|
+
customer_id: customer.id, // Use customer.id instead of customerId from params
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
if (currencyId) {
|
|
150
|
+
where.currency_id = currencyId;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const currency = await ensureCurrencyWithRechargeConfig(currencyId);
|
|
154
|
+
|
|
155
|
+
const config = (await AutoRechargeConfig.findOne({
|
|
156
|
+
where,
|
|
157
|
+
include: [
|
|
158
|
+
{
|
|
159
|
+
model: PaymentCurrency,
|
|
160
|
+
as: 'rechargeCurrency',
|
|
161
|
+
required: false,
|
|
162
|
+
attributes: ['id', 'name', 'symbol', 'type', 'decimal', 'logo'],
|
|
163
|
+
},
|
|
164
|
+
{
|
|
165
|
+
model: PaymentMethod,
|
|
166
|
+
as: 'paymentMethod',
|
|
167
|
+
attributes: ['id', 'type', 'name', 'active'],
|
|
168
|
+
},
|
|
169
|
+
],
|
|
170
|
+
})) as AutoRechargeConfig & { rechargeCurrency: PaymentCurrency; paymentMethod: PaymentMethod };
|
|
171
|
+
const price = await Price.findByPk(currency.recharge_config?.base_price_id, {
|
|
172
|
+
include: [{ model: Product, as: 'product' }],
|
|
173
|
+
});
|
|
174
|
+
if (!price) {
|
|
175
|
+
throw new CustomError(404, 'Base price not found');
|
|
176
|
+
}
|
|
177
|
+
if (!config) {
|
|
178
|
+
const newConfig = await AutoRechargeConfig.create({
|
|
179
|
+
customer_id: customer.id, // Use customer.id instead of customerId from params
|
|
180
|
+
currency_id: currencyId,
|
|
181
|
+
livemode: currency.livemode,
|
|
182
|
+
enabled: false,
|
|
183
|
+
threshold: currency.minimum_payment_amount,
|
|
184
|
+
price_id: price.id,
|
|
185
|
+
quantity: 1,
|
|
186
|
+
recharge_currency_id: price.currency_id,
|
|
187
|
+
});
|
|
188
|
+
return res.json({
|
|
189
|
+
...newConfig.toJSON(),
|
|
190
|
+
currency,
|
|
191
|
+
price,
|
|
192
|
+
threshold: fromUnitToToken(currency.minimum_payment_amount, currency.decimal),
|
|
193
|
+
customer,
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
if (config.price_id !== price.id) {
|
|
198
|
+
await config.update({
|
|
199
|
+
price_id: price.id,
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
return res.json({
|
|
203
|
+
...config.toJSON(),
|
|
204
|
+
daily_limits: {
|
|
205
|
+
max_attempts: config.daily_limits?.max_attempts || 0,
|
|
206
|
+
max_amount: fromUnitToToken(config.daily_limits?.max_amount || '0', config.rechargeCurrency?.decimal || 2),
|
|
207
|
+
},
|
|
208
|
+
currency,
|
|
209
|
+
price,
|
|
210
|
+
threshold: fromUnitToToken(config.threshold || 0, currency.decimal),
|
|
211
|
+
customer,
|
|
212
|
+
});
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
// Retrieve specific auto-recharge configuration by ID
|
|
216
|
+
router.get('/retrieve/:id', sessionMiddleware({ accessKey: true }), async (req, res) => {
|
|
217
|
+
const config = await AutoRechargeConfig.findByPk(req.params.id);
|
|
218
|
+
if (!config) {
|
|
219
|
+
throw new CustomError(404, 'Auto recharge config not found');
|
|
220
|
+
}
|
|
221
|
+
const paymentMethod = await PaymentMethod.findByPk(config.payment_method_id);
|
|
222
|
+
if (!paymentMethod) {
|
|
223
|
+
throw new CustomError(404, 'Payment method not found');
|
|
224
|
+
}
|
|
225
|
+
return res.json({
|
|
226
|
+
...config.toJSON(),
|
|
227
|
+
paymentMethod,
|
|
228
|
+
});
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
async function checkSufficientBalance({
|
|
232
|
+
price,
|
|
233
|
+
quantity = 1,
|
|
234
|
+
rechargeCurrency,
|
|
235
|
+
userDid,
|
|
236
|
+
customer,
|
|
237
|
+
autoRechargeConfig,
|
|
238
|
+
forceReauthorize = false,
|
|
239
|
+
}: {
|
|
240
|
+
price: Price;
|
|
241
|
+
quantity: number;
|
|
242
|
+
rechargeCurrency: PaymentCurrency;
|
|
243
|
+
userDid: string;
|
|
244
|
+
customer: Customer;
|
|
245
|
+
autoRechargeConfig: AutoRechargeConfig;
|
|
246
|
+
forceReauthorize?: boolean;
|
|
247
|
+
}) {
|
|
248
|
+
const priceAmount = await getPriceUintAmountByCurrency(price, rechargeCurrency.id);
|
|
249
|
+
const amount = new BN(priceAmount).mul(new BN(quantity));
|
|
250
|
+
const paymentMethod = await PaymentMethod.findByPk(rechargeCurrency.payment_method_id);
|
|
251
|
+
|
|
252
|
+
if (!paymentMethod) {
|
|
253
|
+
throw new CustomError(400, `Payment method not found: ${rechargeCurrency.payment_method_id}`);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
const payer =
|
|
257
|
+
autoRechargeConfig.payment_settings?.payment_method_options?.[
|
|
258
|
+
paymentMethod?.type as 'arcblock' | EVMChainType | 'stripe'
|
|
259
|
+
]?.payer || userDid;
|
|
260
|
+
|
|
261
|
+
if (paymentMethod.type === 'stripe') {
|
|
262
|
+
if (forceReauthorize || !autoRechargeConfig.payment_settings?.payment_method_options?.stripe?.payer) {
|
|
263
|
+
const setupIntent = await ensureStripeSetupIntentForAutoRecharge(
|
|
264
|
+
customer,
|
|
265
|
+
paymentMethod,
|
|
266
|
+
autoRechargeConfig,
|
|
267
|
+
forceReauthorize
|
|
268
|
+
);
|
|
269
|
+
|
|
270
|
+
return {
|
|
271
|
+
sufficient: false,
|
|
272
|
+
stripeContext: {
|
|
273
|
+
type: 'setup_intent',
|
|
274
|
+
id: setupIntent.id,
|
|
275
|
+
client_secret: setupIntent.client_secret,
|
|
276
|
+
intent_type: 'setup_intent',
|
|
277
|
+
status: setupIntent.status,
|
|
278
|
+
stripe_customer_id: setupIntent.customer as string,
|
|
279
|
+
},
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
return {
|
|
283
|
+
sufficient: true,
|
|
284
|
+
payer,
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
const delegation = await isDelegationSufficientForPayment({
|
|
288
|
+
paymentMethod,
|
|
289
|
+
paymentCurrency: rechargeCurrency,
|
|
290
|
+
userDid: payer,
|
|
291
|
+
amount: amount.toString(),
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
return {
|
|
295
|
+
sufficient: delegation.sufficient,
|
|
296
|
+
delegation,
|
|
297
|
+
payer,
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// Submit auto-recharge configuration
|
|
302
|
+
router.post('/submit', async (req, res) => {
|
|
303
|
+
const { error, value } = createConfigSchema.validate(req.body, {
|
|
304
|
+
stripUnknown: true,
|
|
305
|
+
});
|
|
306
|
+
if (error) {
|
|
307
|
+
throw new CustomError(400, error.message || 'Validation error');
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
const { customer_id: customerId, ...configData } = value;
|
|
311
|
+
|
|
312
|
+
// Validate all required entities
|
|
313
|
+
const customer = await ensureCustomerExists(customerId);
|
|
314
|
+
const currency = await PaymentCurrency.scope('withRechargeConfig').findByPk(value.currency_id);
|
|
315
|
+
if (!currency) {
|
|
316
|
+
throw new CustomError(400, `Currency not found: ${value.currency_id}`);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
if (currency.recharge_config?.base_price_id && currency.recharge_config?.base_price_id !== value.price_id) {
|
|
320
|
+
throw new CustomError(400, 'Price is not the base price');
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
const rechargeCurrency = await PaymentCurrency.findByPk(value.recharge_currency_id);
|
|
324
|
+
if (!rechargeCurrency) {
|
|
325
|
+
throw new CustomError(400, `Recharge currency not found: ${value.recharge_currency_id}`);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
const paymentMethod = await ensurePaymentMethodExists(rechargeCurrency.payment_method_id);
|
|
329
|
+
|
|
330
|
+
const price = (await Price.findOne({
|
|
331
|
+
where: {
|
|
332
|
+
[Op.or]: [{ id: value.price_id }, { lookup_key: value.price_id }],
|
|
333
|
+
active: true,
|
|
334
|
+
},
|
|
335
|
+
include: [{ model: Product, as: 'product' }],
|
|
336
|
+
})) as TPriceExpanded | null;
|
|
337
|
+
|
|
338
|
+
if (!price) {
|
|
339
|
+
throw new CustomError(400, `Active price not found: ${value.price_id}`);
|
|
340
|
+
}
|
|
341
|
+
let { threshold, daily_limits: dailyLimits } = configData;
|
|
342
|
+
if (threshold) {
|
|
343
|
+
threshold = fromTokenToUnit(parseFloat(threshold).toFixed(currency.decimal), currency.decimal).toString();
|
|
344
|
+
}
|
|
345
|
+
if (dailyLimits) {
|
|
346
|
+
dailyLimits = {
|
|
347
|
+
max_attempts: dailyLimits.max_attempts || 0,
|
|
348
|
+
max_amount: fromTokenToUnit(
|
|
349
|
+
parseFloat(dailyLimits.max_amount || '0').toFixed(rechargeCurrency.decimal),
|
|
350
|
+
rechargeCurrency.decimal
|
|
351
|
+
).toString(),
|
|
352
|
+
};
|
|
353
|
+
}
|
|
354
|
+
if (paymentMethod.type === 'stripe' && configData.enabled) {
|
|
355
|
+
const priceAmount = getPriceUintAmountByCurrency(price, rechargeCurrency.id);
|
|
356
|
+
if (!priceAmount) {
|
|
357
|
+
throw new CustomError(400, 'Price amount is not valid');
|
|
358
|
+
}
|
|
359
|
+
const result = validateStripePaymentAmounts(
|
|
360
|
+
new BN(priceAmount).mul(new BN(Number(configData.quantity || '0'))).toString(),
|
|
361
|
+
rechargeCurrency
|
|
362
|
+
);
|
|
363
|
+
if (!result) {
|
|
364
|
+
throw new CustomError(400, 'Auto Top-up amount must be greater or equal to 0.5 USD');
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
const existingConfig = await AutoRechargeConfig.findOne({
|
|
369
|
+
where: {
|
|
370
|
+
customer_id: customer.id,
|
|
371
|
+
currency_id: value.currency_id,
|
|
372
|
+
},
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
if (existingConfig) {
|
|
376
|
+
if (!configData.enabled) {
|
|
377
|
+
await existingConfig.update({
|
|
378
|
+
enabled: false,
|
|
379
|
+
});
|
|
380
|
+
return res.json({
|
|
381
|
+
...existingConfig.toJSON(),
|
|
382
|
+
paymentMethod,
|
|
383
|
+
customer,
|
|
384
|
+
});
|
|
385
|
+
}
|
|
386
|
+
// if exist, update it
|
|
387
|
+
await existingConfig.update({
|
|
388
|
+
...configData,
|
|
389
|
+
currency_id: value.currency_id,
|
|
390
|
+
recharge_currency_id: value.recharge_currency_id,
|
|
391
|
+
price_id: price.id,
|
|
392
|
+
payment_method_id: rechargeCurrency.payment_method_id,
|
|
393
|
+
threshold: threshold ?? existingConfig.threshold,
|
|
394
|
+
daily_limits: dailyLimits,
|
|
395
|
+
});
|
|
396
|
+
const balanceResult = await checkSufficientBalance({
|
|
397
|
+
price: price as unknown as Price,
|
|
398
|
+
quantity: value.quantity,
|
|
399
|
+
rechargeCurrency,
|
|
400
|
+
userDid: customer.did,
|
|
401
|
+
customer,
|
|
402
|
+
autoRechargeConfig: existingConfig,
|
|
403
|
+
forceReauthorize: value.change_payment_method,
|
|
404
|
+
});
|
|
405
|
+
// Update payment details and settings based on balance check result
|
|
406
|
+
await applyBalanceCheckResults(existingConfig, balanceResult, paymentMethod);
|
|
407
|
+
return res.json({
|
|
408
|
+
...existingConfig.toJSON(),
|
|
409
|
+
balanceResult,
|
|
410
|
+
paymentMethod,
|
|
411
|
+
customer,
|
|
412
|
+
});
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
const config = await AutoRechargeConfig.create({
|
|
416
|
+
customer_id: customer.id, // Use customer.id instead of customerId from params
|
|
417
|
+
currency_id: value.currency_id,
|
|
418
|
+
recharge_currency_id: value.recharge_currency_id,
|
|
419
|
+
price_id: price.id,
|
|
420
|
+
livemode: currency.livemode,
|
|
421
|
+
enabled: configData.enabled ?? false,
|
|
422
|
+
threshold: threshold || '0',
|
|
423
|
+
quantity: Number(configData.quantity || '1'),
|
|
424
|
+
payment_method_id: rechargeCurrency.payment_method_id,
|
|
425
|
+
daily_limits: dailyLimits,
|
|
426
|
+
});
|
|
427
|
+
|
|
428
|
+
logger.info('Auto recharge config created', {
|
|
429
|
+
configId: config.id,
|
|
430
|
+
customerId: customer.id,
|
|
431
|
+
currencyId: value.currency_id,
|
|
432
|
+
});
|
|
433
|
+
|
|
434
|
+
const balanceResult = await checkSufficientBalance({
|
|
435
|
+
price: price as unknown as Price,
|
|
436
|
+
quantity: value.quantity,
|
|
437
|
+
rechargeCurrency,
|
|
438
|
+
userDid: customer.did,
|
|
439
|
+
customer,
|
|
440
|
+
autoRechargeConfig: config as AutoRechargeConfig,
|
|
441
|
+
forceReauthorize: value.change_payment_method,
|
|
442
|
+
});
|
|
443
|
+
|
|
444
|
+
// Update payment details and settings based on balance check result
|
|
445
|
+
await applyBalanceCheckResults(config, balanceResult, paymentMethod, true);
|
|
446
|
+
return res.json({
|
|
447
|
+
...config.toJSON(),
|
|
448
|
+
balanceResult,
|
|
449
|
+
paymentMethod,
|
|
450
|
+
customer,
|
|
451
|
+
});
|
|
452
|
+
});
|
|
453
|
+
|
|
454
|
+
export default router;
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import {
|
|
2
|
+
broadcastAutoRechargeEvmTransaction,
|
|
3
|
+
executeEvmTransaction,
|
|
4
|
+
waitForEvmTxConfirm,
|
|
5
|
+
} from '../../integrations/ethereum/tx';
|
|
6
|
+
import type { CallbackArgs } from '../../libs/auth';
|
|
7
|
+
import logger from '../../libs/logger';
|
|
8
|
+
import { getTxMetadata } from '../../libs/util';
|
|
9
|
+
import {
|
|
10
|
+
ensureAutoRechargeAuthorization,
|
|
11
|
+
executeOcapTransactions,
|
|
12
|
+
getAuthPrincipalClaim,
|
|
13
|
+
getDelegationTxClaim,
|
|
14
|
+
} from './shared';
|
|
15
|
+
import { EVM_CHAIN_TYPES } from '../../libs/constants';
|
|
16
|
+
import { Price } from '../../store/models';
|
|
17
|
+
|
|
18
|
+
export default {
|
|
19
|
+
action: 'auto-recharge-auth',
|
|
20
|
+
authPrincipal: false,
|
|
21
|
+
persistentDynamicClaims: true,
|
|
22
|
+
claims: {
|
|
23
|
+
authPrincipal: async ({ extraParams }: CallbackArgs) => {
|
|
24
|
+
const { paymentMethod } = await ensureAutoRechargeAuthorization(extraParams.autoRechargeConfigId);
|
|
25
|
+
return getAuthPrincipalClaim(paymentMethod, 'auto-recharge-auth');
|
|
26
|
+
},
|
|
27
|
+
},
|
|
28
|
+
onConnect: async (args: CallbackArgs) => {
|
|
29
|
+
const { userDid, userPk, extraParams } = args;
|
|
30
|
+
const { autoRechargeConfigId } = extraParams;
|
|
31
|
+
const { autoRechargeConfig, paymentMethod, paymentCurrency } =
|
|
32
|
+
await ensureAutoRechargeAuthorization(autoRechargeConfigId);
|
|
33
|
+
const claimsList: any[] = [];
|
|
34
|
+
const expandedItems = await Price.expand(
|
|
35
|
+
[{ price_id: autoRechargeConfig.price_id, quantity: autoRechargeConfig.quantity }],
|
|
36
|
+
{
|
|
37
|
+
product: true,
|
|
38
|
+
}
|
|
39
|
+
);
|
|
40
|
+
if (paymentMethod.type === 'arcblock') {
|
|
41
|
+
claimsList.push({
|
|
42
|
+
signature: await getDelegationTxClaim({
|
|
43
|
+
mode: 'auto-recharge-auth',
|
|
44
|
+
userDid,
|
|
45
|
+
userPk,
|
|
46
|
+
nonce: autoRechargeConfig.id,
|
|
47
|
+
data: getTxMetadata({
|
|
48
|
+
autoRechargeConfigId,
|
|
49
|
+
}),
|
|
50
|
+
paymentCurrency,
|
|
51
|
+
paymentMethod,
|
|
52
|
+
trialing: false,
|
|
53
|
+
billingThreshold: 0,
|
|
54
|
+
items: expandedItems,
|
|
55
|
+
requiredStake: false,
|
|
56
|
+
}),
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
return claimsList;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (EVM_CHAIN_TYPES.includes(paymentMethod.type)) {
|
|
63
|
+
if (!paymentCurrency.contract) {
|
|
64
|
+
throw new Error(`Payment currency ${paymentMethod.type}:${paymentCurrency.id} does not support subscription`);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
claimsList.push({
|
|
68
|
+
signature: await getDelegationTxClaim({
|
|
69
|
+
mode: 'auto-recharge-auth',
|
|
70
|
+
userDid,
|
|
71
|
+
userPk,
|
|
72
|
+
nonce: autoRechargeConfig.id,
|
|
73
|
+
data: getTxMetadata({
|
|
74
|
+
autoRechargeConfigId,
|
|
75
|
+
}),
|
|
76
|
+
paymentCurrency,
|
|
77
|
+
paymentMethod,
|
|
78
|
+
trialing: false,
|
|
79
|
+
billingThreshold: 0,
|
|
80
|
+
items: expandedItems,
|
|
81
|
+
requiredStake: false,
|
|
82
|
+
}),
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
return claimsList;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
throw new Error(`subscription: Payment method ${paymentMethod.type} not supported`);
|
|
89
|
+
},
|
|
90
|
+
onAuth: async (args: CallbackArgs) => {
|
|
91
|
+
const { request, userDid, userPk, claims, extraParams, updateSession, step } = args;
|
|
92
|
+
const { autoRechargeConfigId } = extraParams;
|
|
93
|
+
const { autoRechargeConfig, paymentMethod, paymentCurrency } =
|
|
94
|
+
await ensureAutoRechargeAuthorization(autoRechargeConfigId);
|
|
95
|
+
const result = request?.context?.store?.result || [];
|
|
96
|
+
result.push({
|
|
97
|
+
step,
|
|
98
|
+
claim: claims?.[0],
|
|
99
|
+
stepRequest: {
|
|
100
|
+
headers: request?.headers,
|
|
101
|
+
},
|
|
102
|
+
});
|
|
103
|
+
try {
|
|
104
|
+
await updateSession({
|
|
105
|
+
result: [],
|
|
106
|
+
});
|
|
107
|
+
} catch (error) {
|
|
108
|
+
logger.error('updateSession', {
|
|
109
|
+
error,
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
const claimsList = result.map((x: any) => x.claim);
|
|
113
|
+
|
|
114
|
+
const paymentSettings = {
|
|
115
|
+
payment_method_types: [paymentMethod.type],
|
|
116
|
+
payment_method_options: {
|
|
117
|
+
...(autoRechargeConfig.payment_settings?.payment_method_options || {}),
|
|
118
|
+
[paymentMethod.type]: { payer: userDid },
|
|
119
|
+
},
|
|
120
|
+
};
|
|
121
|
+
const prepareTxExecution = async () => {
|
|
122
|
+
await autoRechargeConfig.update({ payment_settings: paymentSettings });
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
const afterTxExecution = async (paymentDetails: Record<string, any>) => {
|
|
126
|
+
await autoRechargeConfig.update({
|
|
127
|
+
payment_details: { ...autoRechargeConfig.payment_details, [paymentMethod.type]: paymentDetails },
|
|
128
|
+
enabled: true,
|
|
129
|
+
});
|
|
130
|
+
logger.info('enable auto recharge success', {
|
|
131
|
+
autoRechargeConfigId,
|
|
132
|
+
});
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
if (paymentMethod.type === 'arcblock') {
|
|
136
|
+
await prepareTxExecution();
|
|
137
|
+
|
|
138
|
+
const requestArray = result
|
|
139
|
+
.map((item: { stepRequest?: Request }) => item.stepRequest)
|
|
140
|
+
.filter(Boolean) as Request[];
|
|
141
|
+
|
|
142
|
+
const requestSource = requestArray.length > 0 ? requestArray : request;
|
|
143
|
+
|
|
144
|
+
const { stakingAmount, ...paymentDetails } = await executeOcapTransactions(
|
|
145
|
+
userDid,
|
|
146
|
+
userPk,
|
|
147
|
+
claimsList,
|
|
148
|
+
paymentMethod,
|
|
149
|
+
requestSource,
|
|
150
|
+
'',
|
|
151
|
+
paymentCurrency?.contract,
|
|
152
|
+
autoRechargeConfig.id
|
|
153
|
+
);
|
|
154
|
+
|
|
155
|
+
await afterTxExecution(paymentDetails);
|
|
156
|
+
|
|
157
|
+
return { hash: paymentDetails.tx_hash };
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if (EVM_CHAIN_TYPES.includes(paymentMethod.type)) {
|
|
161
|
+
await prepareTxExecution();
|
|
162
|
+
|
|
163
|
+
broadcastAutoRechargeEvmTransaction(autoRechargeConfigId, 'pending', claimsList);
|
|
164
|
+
|
|
165
|
+
const paymentDetails = await executeEvmTransaction('approve', userDid, claimsList, paymentMethod);
|
|
166
|
+
waitForEvmTxConfirm(
|
|
167
|
+
paymentMethod.getEvmClient(),
|
|
168
|
+
Number(paymentDetails.block_height),
|
|
169
|
+
paymentMethod.confirmation.block
|
|
170
|
+
)
|
|
171
|
+
.then(async () => {
|
|
172
|
+
await afterTxExecution(paymentDetails);
|
|
173
|
+
broadcastAutoRechargeEvmTransaction(autoRechargeConfigId, 'confirmed', claimsList);
|
|
174
|
+
})
|
|
175
|
+
.catch(console.error);
|
|
176
|
+
|
|
177
|
+
return { hash: paymentDetails.tx_hash };
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
throw new Error(`Payment method ${paymentMethod.type} not supported`);
|
|
181
|
+
},
|
|
182
|
+
};
|