payment-kit 1.15.20 → 1.15.22
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/base.ts +69 -7
- package/api/src/crons/subscription-trial-will-end.ts +20 -5
- package/api/src/crons/subscription-will-canceled.ts +22 -6
- package/api/src/crons/subscription-will-renew.ts +13 -4
- package/api/src/index.ts +4 -1
- package/api/src/integrations/arcblock/stake.ts +27 -0
- package/api/src/libs/audit.ts +4 -1
- package/api/src/libs/context.ts +48 -0
- package/api/src/libs/invoice.ts +2 -2
- package/api/src/libs/middleware.ts +39 -1
- package/api/src/libs/notification/template/subscription-canceled.ts +4 -0
- package/api/src/libs/notification/template/subscription-trial-will-end.ts +12 -34
- package/api/src/libs/notification/template/subscription-will-canceled.ts +82 -48
- package/api/src/libs/notification/template/subscription-will-renew.ts +16 -45
- package/api/src/libs/time.ts +13 -0
- package/api/src/libs/util.ts +17 -0
- package/api/src/locales/en.ts +12 -2
- package/api/src/locales/zh.ts +11 -2
- package/api/src/queues/checkout-session.ts +15 -0
- package/api/src/queues/event.ts +13 -4
- package/api/src/queues/invoice.ts +21 -3
- package/api/src/queues/payment.ts +3 -0
- package/api/src/queues/refund.ts +3 -0
- package/api/src/queues/subscription.ts +107 -2
- package/api/src/queues/usage-record.ts +4 -0
- package/api/src/queues/webhook.ts +9 -0
- package/api/src/routes/checkout-sessions.ts +40 -2
- package/api/src/routes/connect/recharge.ts +143 -0
- package/api/src/routes/connect/shared.ts +25 -0
- package/api/src/routes/customers.ts +2 -2
- package/api/src/routes/donations.ts +5 -1
- package/api/src/routes/events.ts +9 -4
- package/api/src/routes/payment-links.ts +40 -20
- package/api/src/routes/prices.ts +17 -4
- package/api/src/routes/products.ts +21 -2
- package/api/src/routes/refunds.ts +20 -3
- package/api/src/routes/subscription-items.ts +39 -2
- package/api/src/routes/subscriptions.ts +77 -40
- package/api/src/routes/usage-records.ts +29 -0
- package/api/src/store/models/event.ts +1 -0
- package/api/src/store/models/subscription.ts +2 -0
- package/api/tests/libs/time.spec.ts +54 -0
- package/blocklet.yml +1 -1
- package/package.json +19 -19
- package/src/app.tsx +10 -0
- package/src/components/subscription/actions/cancel.tsx +30 -9
- package/src/components/subscription/actions/index.tsx +11 -3
- package/src/components/webhook/attempts.tsx +122 -3
- package/src/locales/en.tsx +13 -0
- package/src/locales/zh.tsx +13 -0
- package/src/pages/customer/recharge.tsx +417 -0
- package/src/pages/customer/subscription/detail.tsx +38 -20
|
@@ -194,6 +194,7 @@ router.post('/', auth, async (req, res) => {
|
|
|
194
194
|
try {
|
|
195
195
|
const { error } = PaymentLinkCreateSchema.validate(req.body);
|
|
196
196
|
if (error) {
|
|
197
|
+
logger.warn('Payment link create request invalid', { error: error.message, body: req.body });
|
|
197
198
|
res.status(400).json({ error: `Payment link create request invalid: ${error.message}` });
|
|
198
199
|
return;
|
|
199
200
|
}
|
|
@@ -204,9 +205,10 @@ router.post('/', auth, async (req, res) => {
|
|
|
204
205
|
currency_id: req.body.currency_id || req.currency.id,
|
|
205
206
|
metadata: formatMetadata(req.body.metadata),
|
|
206
207
|
});
|
|
208
|
+
logger.info('Payment link created successfully', { id: result.id, user: req.user?.did });
|
|
207
209
|
res.json(result);
|
|
208
210
|
} catch (err) {
|
|
209
|
-
logger.error('
|
|
211
|
+
logger.error('Create payment link error', { error: err.message, stack: err.stack, body: req.body });
|
|
210
212
|
res.status(400).json({ error: err.message });
|
|
211
213
|
}
|
|
212
214
|
});
|
|
@@ -321,23 +323,33 @@ const PaymentLinkUpdateSchema = Joi.object({
|
|
|
321
323
|
router.put('/:id', auth, async (req, res) => {
|
|
322
324
|
const { error } = PaymentLinkUpdateSchema.validate(req.body);
|
|
323
325
|
if (error) {
|
|
326
|
+
logger.warn('Payment link update request invalid', { error: error.message, id: req.params.id, body: req.body });
|
|
324
327
|
return res.status(400).json({ error: `Payment link update request invalid: ${error.message}` });
|
|
325
328
|
}
|
|
326
329
|
const doc = await PaymentLink.findByPk(req.params.id);
|
|
327
330
|
|
|
328
331
|
if (!doc) {
|
|
332
|
+
logger.warn('Payment link not found for update', { id: req.params.id });
|
|
329
333
|
return res.status(404).json({ error: 'payment link not found' });
|
|
330
334
|
}
|
|
331
335
|
if (doc.active === false) {
|
|
336
|
+
logger.warn('Attempt to update archived payment link', { id: req.params.id });
|
|
332
337
|
return res.status(403).json({ error: 'payment link archived' });
|
|
333
338
|
}
|
|
334
|
-
// if (doc.locked) {
|
|
335
|
-
// return res.status(403).json({ error: 'payment link locked' });
|
|
336
|
-
// }
|
|
337
339
|
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
340
|
+
try {
|
|
341
|
+
await doc.update(formatBeforeSave(Object.assign({}, doc.dataValues, req.body)));
|
|
342
|
+
logger.info('Payment link updated successfully', { id: req.params.id, user: req.user?.did });
|
|
343
|
+
res.json(doc);
|
|
344
|
+
} catch (err) {
|
|
345
|
+
logger.error('Update payment link error', {
|
|
346
|
+
error: err.message,
|
|
347
|
+
stack: err.stack,
|
|
348
|
+
id: req.params.id,
|
|
349
|
+
body: req.body,
|
|
350
|
+
});
|
|
351
|
+
res.status(500).json({ error: 'Failed to update payment link' });
|
|
352
|
+
}
|
|
341
353
|
});
|
|
342
354
|
|
|
343
355
|
// archive
|
|
@@ -345,19 +357,23 @@ router.put('/:id/archive', auth, async (req, res) => {
|
|
|
345
357
|
const doc = await PaymentLink.findByPk(req.params.id);
|
|
346
358
|
|
|
347
359
|
if (!doc) {
|
|
360
|
+
logger.warn('Payment link not found for archiving', { id: req.params.id });
|
|
348
361
|
return res.status(404).json({ error: 'payment link not found' });
|
|
349
362
|
}
|
|
350
363
|
|
|
351
364
|
if (doc.active === false) {
|
|
365
|
+
logger.warn('Attempt to archive already archived payment link', { id: req.params.id });
|
|
352
366
|
return res.status(403).json({ error: 'payment link already archived' });
|
|
353
367
|
}
|
|
354
368
|
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
369
|
+
try {
|
|
370
|
+
await doc.update({ active: false });
|
|
371
|
+
logger.info('Payment link archived successfully', { id: req.params.id, user: req.user?.did });
|
|
372
|
+
return res.json(doc);
|
|
373
|
+
} catch (err) {
|
|
374
|
+
logger.error('Archive payment link error', { error: err.message, stack: err.stack, id: req.params.id });
|
|
375
|
+
return res.status(500).json({ error: 'Failed to archive payment link' });
|
|
376
|
+
}
|
|
361
377
|
});
|
|
362
378
|
|
|
363
379
|
// delete
|
|
@@ -372,12 +388,14 @@ router.delete('/:id', auth, async (req, res) => {
|
|
|
372
388
|
return res.status(403).json({ error: 'payment link archived' });
|
|
373
389
|
}
|
|
374
390
|
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
391
|
+
try {
|
|
392
|
+
await doc.destroy();
|
|
393
|
+
logger.info('Payment link deleted successfully', { id: req.params.id, user: req.user?.did });
|
|
394
|
+
return res.json(doc);
|
|
395
|
+
} catch (err) {
|
|
396
|
+
logger.error('Delete payment link error', { error: err.message, stack: err.stack, id: req.params.id });
|
|
397
|
+
return res.status(500).json({ error: 'Failed to delete payment link' });
|
|
398
|
+
}
|
|
381
399
|
});
|
|
382
400
|
|
|
383
401
|
router.post('/stash', auth, async (req, res) => {
|
|
@@ -393,12 +411,14 @@ router.post('/stash', auth, async (req, res) => {
|
|
|
393
411
|
let doc = await PaymentLink.findByPk(raw.id);
|
|
394
412
|
if (doc) {
|
|
395
413
|
await doc.update({ ...formatBeforeSave(req.body), livemode: raw.livemode });
|
|
414
|
+
logger.info('Stashed payment link updated', { id: raw.id, user: req.user?.did });
|
|
396
415
|
} else {
|
|
397
416
|
doc = await PaymentLink.create(raw as PaymentLink);
|
|
417
|
+
logger.info('New stashed payment link created', { id: raw.id, user: req.user?.did });
|
|
398
418
|
}
|
|
399
419
|
res.json(doc);
|
|
400
420
|
} catch (err) {
|
|
401
|
-
|
|
421
|
+
logger.error('Stash payment link error', { error: err.message, stack: err.stack, body: req.body });
|
|
402
422
|
res.status(500).json({ error: err.message });
|
|
403
423
|
}
|
|
404
424
|
});
|
package/api/src/routes/prices.ts
CHANGED
|
@@ -217,9 +217,10 @@ router.post('/', auth, async (req, res) => {
|
|
|
217
217
|
quantity_sold: 0,
|
|
218
218
|
});
|
|
219
219
|
|
|
220
|
+
logger.info(`Price created: ${result?.id}`, { priceId: result?.id, requestedBy: req.user?.did });
|
|
220
221
|
res.json(result);
|
|
221
222
|
} catch (err) {
|
|
222
|
-
|
|
223
|
+
logger.error('Error creating price', { error: err.message, request: req.body });
|
|
223
224
|
res.status(400).json({ error: err.message });
|
|
224
225
|
}
|
|
225
226
|
});
|
|
@@ -346,9 +347,14 @@ router.put('/:id', auth, async (req, res) => {
|
|
|
346
347
|
}
|
|
347
348
|
}
|
|
348
349
|
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
350
|
+
try {
|
|
351
|
+
await doc.update(Price.formatBeforeSave(updates));
|
|
352
|
+
logger.info(`Price updated: ${req.params.id}`, { priceId: req.params.id, updates, requestedBy: req.user?.did });
|
|
353
|
+
return res.json(await getExpandedPrice(req.params.id as string));
|
|
354
|
+
} catch (err) {
|
|
355
|
+
logger.error('Error updating price', { error: err.message, request: req.body });
|
|
356
|
+
return res.status(400).json({ error: err.message });
|
|
357
|
+
}
|
|
352
358
|
});
|
|
353
359
|
|
|
354
360
|
// archive
|
|
@@ -369,6 +375,7 @@ router.put('/:id/archive', auth, async (req, res) => {
|
|
|
369
375
|
|
|
370
376
|
await price.update({ active: false });
|
|
371
377
|
|
|
378
|
+
logger.info(`Price archived: ${req.params.id}`, { priceId: req.params.id, requestedBy: req.user?.did });
|
|
372
379
|
return res.json(await getExpandedPrice(req.params.id as string));
|
|
373
380
|
});
|
|
374
381
|
|
|
@@ -431,6 +438,12 @@ router.put('/:id/inventory', auth, async (req, res) => {
|
|
|
431
438
|
}
|
|
432
439
|
await price.increment('quantity_sold', { by: req.body.quantity });
|
|
433
440
|
}
|
|
441
|
+
logger.info(`Price inventory updated: ${req.params.id}`, {
|
|
442
|
+
priceId: req.params.id,
|
|
443
|
+
action: req.body.action,
|
|
444
|
+
quantity: req.body.quantity,
|
|
445
|
+
requestedBy: req.user?.did,
|
|
446
|
+
});
|
|
434
447
|
return res.json(await getExpandedPrice(req.params.id as string));
|
|
435
448
|
} catch (err) {
|
|
436
449
|
logger.error('update price inventory error', err);
|
|
@@ -161,6 +161,12 @@ router.post('/', auth, async (req, res) => {
|
|
|
161
161
|
currency_id: req.currency.id,
|
|
162
162
|
metadata: formatMetadata(req.body.metadata),
|
|
163
163
|
});
|
|
164
|
+
logger.info('Product and prices created', {
|
|
165
|
+
productId: result.id,
|
|
166
|
+
name: result.name,
|
|
167
|
+
priceCount: result.prices.length,
|
|
168
|
+
requestedBy: req.user?.did,
|
|
169
|
+
});
|
|
164
170
|
res.json(result);
|
|
165
171
|
} catch (err) {
|
|
166
172
|
logger.error('create product error', err);
|
|
@@ -300,7 +306,11 @@ router.put('/:id', auth, async (req, res) => {
|
|
|
300
306
|
updates.metadata = formatMetadata(updates.metadata);
|
|
301
307
|
}
|
|
302
308
|
await product.update(updates);
|
|
303
|
-
|
|
309
|
+
logger.info('Product updated', {
|
|
310
|
+
productId: product.id,
|
|
311
|
+
updatedFields: Object.keys(updates),
|
|
312
|
+
requestedBy: req.user?.did,
|
|
313
|
+
});
|
|
304
314
|
return res.json(await Product.expand(req.params.id as string));
|
|
305
315
|
});
|
|
306
316
|
|
|
@@ -346,7 +356,10 @@ router.delete('/:id', auth, async (req, res) => {
|
|
|
346
356
|
|
|
347
357
|
await product.destroy();
|
|
348
358
|
await Price.destroy({ where: { product_id: product.id } });
|
|
349
|
-
|
|
359
|
+
logger.info('Product and associated prices deleted', {
|
|
360
|
+
productId: product.id,
|
|
361
|
+
requestedBy: req.user?.did,
|
|
362
|
+
});
|
|
350
363
|
return res.json(product);
|
|
351
364
|
} catch (err) {
|
|
352
365
|
logger.error('delete product error', err);
|
|
@@ -415,6 +428,12 @@ router.post('/batch-price-update', auth, async (req, res) => {
|
|
|
415
428
|
})
|
|
416
429
|
);
|
|
417
430
|
|
|
431
|
+
logger.info('Batch price update completed', {
|
|
432
|
+
updatedCount: updated.length,
|
|
433
|
+
dryRun,
|
|
434
|
+
factor,
|
|
435
|
+
requestedBy: req.user?.did,
|
|
436
|
+
});
|
|
418
437
|
return res.json(updated);
|
|
419
438
|
} catch (err) {
|
|
420
439
|
logger.error('batch price update error', err);
|
|
@@ -169,10 +169,16 @@ router.post('/', authAdmin, async (req, res) => {
|
|
|
169
169
|
...req.params,
|
|
170
170
|
...req.body,
|
|
171
171
|
result: item.toJSON(),
|
|
172
|
+
requestedBy: req.user?.did,
|
|
172
173
|
});
|
|
173
174
|
res.json(item);
|
|
174
175
|
} catch (err) {
|
|
175
|
-
logger.error('
|
|
176
|
+
logger.error('Create refund failed', {
|
|
177
|
+
error: err.message,
|
|
178
|
+
stack: err.stack,
|
|
179
|
+
requestBody: req.body,
|
|
180
|
+
requestedBy: req.user?.did,
|
|
181
|
+
});
|
|
176
182
|
res.status(400).json({ error: err.message });
|
|
177
183
|
}
|
|
178
184
|
});
|
|
@@ -241,10 +247,21 @@ router.put('/:id', authAdmin, async (req, res) => {
|
|
|
241
247
|
}
|
|
242
248
|
|
|
243
249
|
await doc.update(raw);
|
|
250
|
+
logger.info('Refund updated', {
|
|
251
|
+
refundId: doc.id,
|
|
252
|
+
updatedFields: Object.keys(raw),
|
|
253
|
+
requestedBy: req.user?.did,
|
|
254
|
+
});
|
|
244
255
|
res.json(doc);
|
|
245
256
|
} catch (err) {
|
|
246
|
-
|
|
247
|
-
|
|
257
|
+
logger.error('Update refund failed', {
|
|
258
|
+
refundId: req.params.id,
|
|
259
|
+
error: err.message,
|
|
260
|
+
stack: err.stack,
|
|
261
|
+
requestBody: req.body,
|
|
262
|
+
requestedBy: req.user?.did,
|
|
263
|
+
});
|
|
264
|
+
res.status(500).json({ error: 'Internal server error' });
|
|
248
265
|
}
|
|
249
266
|
});
|
|
250
267
|
|
|
@@ -11,6 +11,7 @@ import { formatMetadata } from '../libs/util';
|
|
|
11
11
|
import { Price, Product, Subscription, SubscriptionItem, UsageRecord } from '../store/models';
|
|
12
12
|
import { forwardUsageRecordToStripe } from '../integrations/stripe/resource';
|
|
13
13
|
import { usageRecordQueue } from '../queues/usage-record';
|
|
14
|
+
import logger from '../libs/logger';
|
|
14
15
|
|
|
15
16
|
const router = Router();
|
|
16
17
|
const auth = authenticate<SubscriptionItem>({ component: true, roles: ['owner', 'admin'] });
|
|
@@ -42,6 +43,13 @@ router.post('/', auth, async (req, res) => {
|
|
|
42
43
|
}
|
|
43
44
|
|
|
44
45
|
const doc = await SubscriptionItem.create(raw as SubscriptionItem);
|
|
46
|
+
logger.info('SubscriptionItem created', {
|
|
47
|
+
id: doc.id,
|
|
48
|
+
subscriptionId: doc.subscription_id,
|
|
49
|
+
priceId: doc.price_id,
|
|
50
|
+
quantity: doc.quantity,
|
|
51
|
+
requestedBy: req.user?.did,
|
|
52
|
+
});
|
|
45
53
|
return res.json(doc);
|
|
46
54
|
});
|
|
47
55
|
|
|
@@ -135,7 +143,12 @@ router.put('/:id', auth, async (req, res) => {
|
|
|
135
143
|
}
|
|
136
144
|
|
|
137
145
|
await doc.update(updates);
|
|
138
|
-
|
|
146
|
+
logger.info('SubscriptionItem updated', {
|
|
147
|
+
id: doc.id,
|
|
148
|
+
subscriptionId: doc.subscription_id,
|
|
149
|
+
updatedFields: Object.keys(updates),
|
|
150
|
+
requestedBy: req.user?.did,
|
|
151
|
+
});
|
|
139
152
|
return res.json(doc);
|
|
140
153
|
});
|
|
141
154
|
|
|
@@ -156,7 +169,12 @@ router.delete('/:id', auth, async (req, res) => {
|
|
|
156
169
|
}
|
|
157
170
|
|
|
158
171
|
await doc.destroy();
|
|
159
|
-
|
|
172
|
+
logger.info('SubscriptionItem deleted', {
|
|
173
|
+
id: doc.id,
|
|
174
|
+
subscriptionId: doc.subscription_id,
|
|
175
|
+
clearUsage: req.body.clear_usage,
|
|
176
|
+
requestedBy: req.user?.did,
|
|
177
|
+
});
|
|
160
178
|
return res.json(doc);
|
|
161
179
|
});
|
|
162
180
|
|
|
@@ -198,11 +216,23 @@ router.post('/:id/add-usage-quantity', auth, async (req, res) => {
|
|
|
198
216
|
timestamp: now,
|
|
199
217
|
} as UsageRecord);
|
|
200
218
|
|
|
219
|
+
logger.info('Usage quantity added', {
|
|
220
|
+
subscriptionItemId: subscriptionItem.id,
|
|
221
|
+
subscriptionId: subscription.id,
|
|
222
|
+
quantity,
|
|
223
|
+
timestamp: now,
|
|
224
|
+
requestedBy: req.user?.did,
|
|
225
|
+
});
|
|
226
|
+
|
|
201
227
|
if (subscription.billing_thresholds?.amount_gte) {
|
|
202
228
|
usageRecordQueue.push({
|
|
203
229
|
id: `usage-${subscription.id}`,
|
|
204
230
|
job: { subscriptionId: subscription.id, subscriptionItemId: subscriptionItem.id },
|
|
205
231
|
});
|
|
232
|
+
logger.info('Usage record pushed to queue', {
|
|
233
|
+
subscriptionId: subscription.id,
|
|
234
|
+
subscriptionItemId: subscriptionItem.id,
|
|
235
|
+
});
|
|
206
236
|
}
|
|
207
237
|
|
|
208
238
|
await forwardUsageRecordToStripe(subscriptionItem, {
|
|
@@ -210,6 +240,13 @@ router.post('/:id/add-usage-quantity', auth, async (req, res) => {
|
|
|
210
240
|
timestamp: now,
|
|
211
241
|
action: 'increment',
|
|
212
242
|
});
|
|
243
|
+
logger.info('Usage record forwarded to Stripe', {
|
|
244
|
+
subscriptionItemId: subscriptionItem.id,
|
|
245
|
+
quantity,
|
|
246
|
+
timestamp: now,
|
|
247
|
+
action: 'increment',
|
|
248
|
+
});
|
|
249
|
+
|
|
213
250
|
return res.json(usageRecord);
|
|
214
251
|
} catch (err) {
|
|
215
252
|
console.error(err);
|
|
@@ -38,7 +38,6 @@ import { PaymentMethod } from '../store/models/payment-method';
|
|
|
38
38
|
import { Price } from '../store/models/price';
|
|
39
39
|
import { PricingTable } from '../store/models/pricing-table';
|
|
40
40
|
import { Product } from '../store/models/product';
|
|
41
|
-
import { Refund } from '../store/models/refund';
|
|
42
41
|
import { SetupIntent } from '../store/models/setup-intent';
|
|
43
42
|
import { Subscription, TSubscription } from '../store/models/subscription';
|
|
44
43
|
import { SubscriptionItem } from '../store/models/subscription-item';
|
|
@@ -46,6 +45,8 @@ import type { LineItem, ServiceAction, SubscriptionUpdateItem } from '../store/m
|
|
|
46
45
|
import { UsageRecord } from '../store/models/usage-record';
|
|
47
46
|
import { cleanupInvoiceAndItems, ensureInvoiceAndItems } from './connect/shared';
|
|
48
47
|
import { createUsageRecordQueryFn } from './usage-records';
|
|
48
|
+
import { SubscriptionWillCanceledSchedule } from '../crons/subscription-will-canceled';
|
|
49
|
+
import { getTokenByAddress } from '../integrations/arcblock/stake';
|
|
49
50
|
|
|
50
51
|
const router = Router();
|
|
51
52
|
const auth = authenticate<Subscription>({ component: true, roles: ['owner', 'admin'] });
|
|
@@ -377,58 +378,43 @@ router.put('/:id/cancel', authPortal, async (req, res) => {
|
|
|
377
378
|
}
|
|
378
379
|
}
|
|
379
380
|
|
|
380
|
-
await subscription.update(updates);
|
|
381
|
-
|
|
382
381
|
// trigger refund
|
|
383
382
|
if (updates.cancel_at < subscription.current_period_end && refund !== 'none') {
|
|
384
383
|
if (['owner', 'admin'].includes(req.user?.role as string) === false) {
|
|
385
|
-
return res.status(403).json({ error: 'Not authorized to
|
|
384
|
+
return res.status(403).json({ error: 'Not authorized to refund' });
|
|
386
385
|
}
|
|
387
|
-
|
|
388
386
|
const result = await getSubscriptionRefundSetup(subscription, updates.cancel_at);
|
|
389
387
|
if (result.unused !== '0') {
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
currency_id: subscription.currency_id,
|
|
398
|
-
customer_id: subscription.customer_id,
|
|
399
|
-
payment_method_id: subscription.default_payment_method_id,
|
|
400
|
-
payment_intent_id: result.lastInvoice.payment_intent_id as string,
|
|
401
|
-
invoice_id: result.lastInvoice.id,
|
|
402
|
-
subscription_id: subscription.id,
|
|
403
|
-
attempt_count: 0,
|
|
404
|
-
attempted: false,
|
|
405
|
-
next_attempt: 0,
|
|
406
|
-
last_attempt_error: null,
|
|
407
|
-
starting_balance: '0',
|
|
408
|
-
ending_balance: '0',
|
|
409
|
-
starting_token_balance: {},
|
|
410
|
-
ending_token_balance: {},
|
|
411
|
-
metadata: {
|
|
412
|
-
requested_by: req.user?.did,
|
|
413
|
-
unused_period_start: refund === 'last' ? subscription.current_period_start : updates.cancel_at,
|
|
414
|
-
unused_period_end: subscription.current_period_end,
|
|
415
|
-
},
|
|
416
|
-
});
|
|
417
|
-
logger.info('subscription cancel refund created', {
|
|
388
|
+
// @ts-ignore
|
|
389
|
+
updates.cancelation_details = {
|
|
390
|
+
...(updates.cancelation_details || {}),
|
|
391
|
+
refund,
|
|
392
|
+
requested_by: req.user?.did,
|
|
393
|
+
};
|
|
394
|
+
logger.info('subscription cancel with refund', {
|
|
418
395
|
...req.params,
|
|
419
396
|
...req.body,
|
|
397
|
+
refund,
|
|
420
398
|
...pick(result, ['total', 'unused']),
|
|
421
|
-
item: item.toJSON(),
|
|
422
399
|
});
|
|
423
400
|
} else {
|
|
424
|
-
logger.info('subscription cancel refund
|
|
401
|
+
logger.info('subscription cancel no refund', {
|
|
425
402
|
...req.params,
|
|
426
403
|
...req.body,
|
|
427
404
|
...pick(result, ['total', 'unused']),
|
|
428
405
|
});
|
|
429
406
|
}
|
|
430
407
|
}
|
|
431
|
-
|
|
408
|
+
await subscription.update(updates);
|
|
409
|
+
await new SubscriptionWillCanceledSchedule().reScheduleSubscriptionTasks([subscription]);
|
|
410
|
+
logger.info('Update subscription for cancel request successful', {
|
|
411
|
+
subscriptionId: subscription.id,
|
|
412
|
+
customerId: subscription.customer_id,
|
|
413
|
+
reason: req.body.reason,
|
|
414
|
+
cancelAt: subscription.cancel_at,
|
|
415
|
+
requestedBy: req.user?.did,
|
|
416
|
+
updates,
|
|
417
|
+
});
|
|
432
418
|
return res.json(subscription);
|
|
433
419
|
});
|
|
434
420
|
|
|
@@ -455,7 +441,7 @@ router.put('/:id/recover', authPortal, async (req, res) => {
|
|
|
455
441
|
}
|
|
456
442
|
|
|
457
443
|
await doc.update({ cancel_at_period_end: false, cancel_at: 0, canceled_at: 0 });
|
|
458
|
-
|
|
444
|
+
await new SubscriptionWillCanceledSchedule().deleteScheduleSubscriptionJobs([doc]);
|
|
459
445
|
// reschedule jobs
|
|
460
446
|
subscriptionQueue
|
|
461
447
|
.delete(`cancel-${doc.id}`)
|
|
@@ -1126,7 +1112,11 @@ router.put('/:id', authPortal, async (req, res) => {
|
|
|
1126
1112
|
}
|
|
1127
1113
|
}
|
|
1128
1114
|
}
|
|
1129
|
-
|
|
1115
|
+
logger.info('Subscription updated successfully', {
|
|
1116
|
+
subscriptionId: subscription.id,
|
|
1117
|
+
updatedFields: Object.keys(updates),
|
|
1118
|
+
newStatus: subscription.status,
|
|
1119
|
+
});
|
|
1130
1120
|
return res.json({ ...subscription.toJSON(), connectAction });
|
|
1131
1121
|
} catch (err) {
|
|
1132
1122
|
console.error(err);
|
|
@@ -1645,8 +1635,16 @@ router.delete('/:id', auth, async (req, res) => {
|
|
|
1645
1635
|
await UsageRecord.destroy({ where: { subscription_item_id: items.map((x) => x.id) } });
|
|
1646
1636
|
await SubscriptionItem.destroy({ where: { subscription_id: doc.id } });
|
|
1647
1637
|
await doc.destroy();
|
|
1648
|
-
logger.info('
|
|
1649
|
-
|
|
1638
|
+
logger.info('Subscription deleted successfully', {
|
|
1639
|
+
subscriptionId: req.params.id,
|
|
1640
|
+
deletedRelatedRecords: {
|
|
1641
|
+
invoiceItems: await InvoiceItem.count({ where: { subscription_id: doc.id } }),
|
|
1642
|
+
invoices: await Invoice.count({ where: { subscription_id: doc.id } }),
|
|
1643
|
+
usageRecords: await UsageRecord.count({ where: { subscription_item_id: items.map((x) => x.id) } }),
|
|
1644
|
+
subscriptionItems: items.length,
|
|
1645
|
+
},
|
|
1646
|
+
requestedBy: req.user?.did,
|
|
1647
|
+
});
|
|
1650
1648
|
return res.json(doc);
|
|
1651
1649
|
});
|
|
1652
1650
|
|
|
@@ -1712,6 +1710,12 @@ router.put('/:id/slash-stake', auth, async (req, res) => {
|
|
|
1712
1710
|
return res.status(400).json({ error: 'Staking not found on subscription payment detail' });
|
|
1713
1711
|
}
|
|
1714
1712
|
try {
|
|
1713
|
+
logger.warn('Stake slash initiated', {
|
|
1714
|
+
subscriptionId: subscription.id,
|
|
1715
|
+
slashReason: req.body.slashReason,
|
|
1716
|
+
requestedBy: req.user?.did,
|
|
1717
|
+
});
|
|
1718
|
+
|
|
1715
1719
|
await subscription.update({
|
|
1716
1720
|
// @ts-ignore
|
|
1717
1721
|
cancelation_details: {
|
|
@@ -1724,10 +1728,43 @@ router.put('/:id/slash-stake', auth, async (req, res) => {
|
|
|
1724
1728
|
id: `slash-stake-${subscription.id}`,
|
|
1725
1729
|
job: { subscriptionId: subscription.id },
|
|
1726
1730
|
});
|
|
1731
|
+
logger.info('Stake slash scheduled successfully', {
|
|
1732
|
+
subscriptionId: subscription.id,
|
|
1733
|
+
result,
|
|
1734
|
+
slashReason: req.body.slashReason,
|
|
1735
|
+
requestedBy: req.user?.did,
|
|
1736
|
+
stakingAddress: address,
|
|
1737
|
+
});
|
|
1727
1738
|
return res.json(result);
|
|
1728
1739
|
} catch (err) {
|
|
1729
1740
|
logger.error('subscription slash stake failed', { subscription: subscription.id, error: err });
|
|
1730
1741
|
return res.status(400).json({ error: err.message });
|
|
1731
1742
|
}
|
|
1732
1743
|
});
|
|
1744
|
+
|
|
1745
|
+
// get payer token
|
|
1746
|
+
router.get('/:id/payer-token', authMine, async (req, res) => {
|
|
1747
|
+
const subscription = await Subscription.findByPk(req.params.id);
|
|
1748
|
+
if (!subscription) {
|
|
1749
|
+
return res.status(400).json({ error: `Subscription(${req.params.id}) not found` });
|
|
1750
|
+
}
|
|
1751
|
+
const paymentMethod = await PaymentMethod.findByPk(subscription.default_payment_method_id);
|
|
1752
|
+
if (!paymentMethod) {
|
|
1753
|
+
return res.status(400).json({ error: `Payment method(${subscription.default_payment_method_id}) not found` });
|
|
1754
|
+
}
|
|
1755
|
+
|
|
1756
|
+
const paymentCurrency = await PaymentCurrency.findByPk(subscription.currency_id);
|
|
1757
|
+
if (!paymentCurrency) {
|
|
1758
|
+
return res.status(400).json({ error: `Payment currency(${subscription.currency_id}) not found` });
|
|
1759
|
+
}
|
|
1760
|
+
|
|
1761
|
+
// @ts-ignore
|
|
1762
|
+
const paymentAddress = subscription.payment_details?.[paymentMethod.type]?.payer ?? undefined;
|
|
1763
|
+
if (!paymentAddress && ['ethereum', 'arcblock'].includes(paymentMethod.type)) {
|
|
1764
|
+
return res.status(400).json({ error: `Payer not found on subscription payment detail: ${subscription.id}` });
|
|
1765
|
+
}
|
|
1766
|
+
|
|
1767
|
+
const token = await getTokenByAddress(paymentAddress, paymentMethod, paymentCurrency);
|
|
1768
|
+
return res.json({ token, paymentAddress });
|
|
1769
|
+
});
|
|
1733
1770
|
export default router;
|
|
@@ -45,17 +45,36 @@ router.post('/', auth, async (req, res) => {
|
|
|
45
45
|
});
|
|
46
46
|
if (doc) {
|
|
47
47
|
if (doc.billed) {
|
|
48
|
+
logger.info('UsageRecord updated', {
|
|
49
|
+
subscriptionItemId: raw.subscription_item_id,
|
|
50
|
+
timestamp: raw.timestamp,
|
|
51
|
+
newQuantity: raw.quantity,
|
|
52
|
+
});
|
|
48
53
|
return res.status(400).json({ error: 'UsageRecord is immutable because already billed' });
|
|
49
54
|
}
|
|
50
55
|
if (req.body.action === 'increment') {
|
|
51
56
|
await doc.increment('quantity', { by: raw.quantity });
|
|
57
|
+
logger.info('UsageRecord incremented', {
|
|
58
|
+
subscriptionItemId: raw.subscription_item_id,
|
|
59
|
+
timestamp: raw.timestamp,
|
|
60
|
+
incrementBy: raw.quantity,
|
|
61
|
+
});
|
|
52
62
|
} else {
|
|
53
63
|
if (subscription.billing_thresholds?.amount_gte) {
|
|
64
|
+
logger.warn('Invalid action for subscription with billing_thresholds', {
|
|
65
|
+
subscriptionId: subscription.id,
|
|
66
|
+
action: req.body.action,
|
|
67
|
+
});
|
|
54
68
|
return res
|
|
55
69
|
.status(400)
|
|
56
70
|
.json({ error: 'UsageRecord action must be `increment` for subscriptions with billing_thresholds' });
|
|
57
71
|
}
|
|
58
72
|
await doc.update({ quantity: raw.quantity });
|
|
73
|
+
logger.info('UsageRecord updated', {
|
|
74
|
+
subscriptionItemId: raw.subscription_item_id,
|
|
75
|
+
timestamp: raw.timestamp,
|
|
76
|
+
newQuantity: raw.quantity,
|
|
77
|
+
});
|
|
59
78
|
}
|
|
60
79
|
} else {
|
|
61
80
|
raw.livemode = req.livemode;
|
|
@@ -67,6 +86,10 @@ router.post('/', auth, async (req, res) => {
|
|
|
67
86
|
id: `usage-${subscription.id}`,
|
|
68
87
|
job: { subscriptionId: subscription.id, subscriptionItemId: item.id },
|
|
69
88
|
});
|
|
89
|
+
logger.info('UsageRecord pushed to queue', {
|
|
90
|
+
subscriptionId: subscription.id,
|
|
91
|
+
subscriptionItemId: item.id,
|
|
92
|
+
});
|
|
70
93
|
}
|
|
71
94
|
|
|
72
95
|
await forwardUsageRecordToStripe(item, {
|
|
@@ -75,6 +98,12 @@ router.post('/', auth, async (req, res) => {
|
|
|
75
98
|
action: req.body.action,
|
|
76
99
|
});
|
|
77
100
|
|
|
101
|
+
logger.info('UsageRecord forwarded to Stripe', {
|
|
102
|
+
subscriptionItemId: item.id,
|
|
103
|
+
quantity: Number(raw.quantity),
|
|
104
|
+
timestamp: raw.timestamp,
|
|
105
|
+
action: req.body.action,
|
|
106
|
+
});
|
|
78
107
|
return res.json(doc);
|
|
79
108
|
});
|
|
80
109
|
|
|
@@ -64,6 +64,8 @@ export class Subscription extends Model<InferAttributes<Subscription>, InferCrea
|
|
|
64
64
|
return_stake?: boolean;
|
|
65
65
|
slash_stake?: boolean;
|
|
66
66
|
slash_reason?: string;
|
|
67
|
+
refund?: LiteralUnion<'last' | 'proration' | 'none', string>;
|
|
68
|
+
requested_by?: string;
|
|
67
69
|
};
|
|
68
70
|
|
|
69
71
|
declare billing_cycle_anchor: number;
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { getSimplifyDuration } from '../../src/libs/time';
|
|
2
|
+
|
|
3
|
+
describe('getSimplifyDuration', () => {
|
|
4
|
+
const testCasesEn = [
|
|
5
|
+
{ ms: 30 * 1000, expected: '30 seconds' },
|
|
6
|
+
{ ms: 34 * 60 * 1000 + 30 * 1000, expected: '34 minutes' },
|
|
7
|
+
{ ms: 59 * 60 * 1000, expected: '59 minutes' },
|
|
8
|
+
{ ms: 60 * 60 * 1000, expected: '1 hour' },
|
|
9
|
+
{ ms: 23 * 60 * 60 * 1000, expected: '23 hours' },
|
|
10
|
+
{ ms: 24 * 60 * 60 * 1000, expected: '1 day' },
|
|
11
|
+
{ ms: 24 * 60 * 60 * 1000 + 20 * 60 * 1000 + 30 * 1000, expected: '1 day' },
|
|
12
|
+
{ ms: 25 * 24 * 60 * 60 * 1000 + 30 * 60 * 1000 + 10 * 1000, expected: '25 days' },
|
|
13
|
+
{ ms: 32 * 24 * 60 * 60 * 1000 + 30 * 60 * 1000 + 12 * 1000, expected: '32 days' },
|
|
14
|
+
{ ms: 60 * 24 * 60 * 60 * 1000 + 20 * 60 * 1000, expected: '60 days' },
|
|
15
|
+
{ ms: 366 * 24 * 60 * 60 * 1000 + 20 * 60 * 1000, expected: '1 year' },
|
|
16
|
+
];
|
|
17
|
+
|
|
18
|
+
const testCasesZh = [
|
|
19
|
+
{ ms: 30 * 1000, expected: '30秒' },
|
|
20
|
+
{ ms: 34 * 60 * 1000 + 30 * 1000, expected: '34分钟' },
|
|
21
|
+
{ ms: 59 * 60 * 1000, expected: '59分钟' },
|
|
22
|
+
{ ms: 60 * 60 * 1000, expected: '1小时' },
|
|
23
|
+
{ ms: 23 * 60 * 60 * 1000, expected: '23小时' },
|
|
24
|
+
{ ms: 24 * 60 * 60 * 1000, expected: '1天' },
|
|
25
|
+
{ ms: 24 * 60 * 60 * 1000 + 20 * 60 * 1000 + 30 * 1000, expected: '1天' },
|
|
26
|
+
{ ms: 25 * 24 * 60 * 60 * 1000 + 30 * 60 * 1000 + 10 * 1000, expected: '25天' },
|
|
27
|
+
{ ms: 32 * 24 * 60 * 60 * 1000 + 30 * 60 * 1000 + 12 * 1000, expected: '32天' },
|
|
28
|
+
{ ms: 60 * 24 * 60 * 60 * 1000 + 20 * 60 * 1000, expected: '60天' },
|
|
29
|
+
{ ms: 366 * 24 * 60 * 60 * 1000 + 20 * 60 * 1000, expected: '1年' },
|
|
30
|
+
];
|
|
31
|
+
|
|
32
|
+
test.each(testCasesEn)('should return $expected for $ms milliseconds in English', ({ ms, expected }) => {
|
|
33
|
+
expect(getSimplifyDuration(ms, 'en')).toBe(expected);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
test.each(testCasesZh)('should return $expected for $ms milliseconds in Chinese', ({ ms, expected }) => {
|
|
37
|
+
expect(getSimplifyDuration(ms, 'zh')).toBe(expected);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
test('should handle zero', () => {
|
|
41
|
+
expect(getSimplifyDuration(0, 'en')).toBe('0ms');
|
|
42
|
+
expect(getSimplifyDuration(0, 'zh')).toBe('0毫秒');
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
test('should handle negative values', () => {
|
|
46
|
+
expect(getSimplifyDuration(-24 * 60 * 60 * 1000, 'en')).toBe('-1 days');
|
|
47
|
+
expect(getSimplifyDuration(-24 * 60 * 60 * 1000, 'zh')).toBe('-1天');
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test('should handle large number of days', () => {
|
|
51
|
+
expect(getSimplifyDuration(1000 * 24 * 60 * 60 * 1000, 'en')).toBe('2 years');
|
|
52
|
+
expect(getSimplifyDuration(1000 * 24 * 60 * 60 * 1000, 'zh')).toBe('2年');
|
|
53
|
+
});
|
|
54
|
+
});
|