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.
Files changed (52) hide show
  1. package/api/src/crons/base.ts +69 -7
  2. package/api/src/crons/subscription-trial-will-end.ts +20 -5
  3. package/api/src/crons/subscription-will-canceled.ts +22 -6
  4. package/api/src/crons/subscription-will-renew.ts +13 -4
  5. package/api/src/index.ts +4 -1
  6. package/api/src/integrations/arcblock/stake.ts +27 -0
  7. package/api/src/libs/audit.ts +4 -1
  8. package/api/src/libs/context.ts +48 -0
  9. package/api/src/libs/invoice.ts +2 -2
  10. package/api/src/libs/middleware.ts +39 -1
  11. package/api/src/libs/notification/template/subscription-canceled.ts +4 -0
  12. package/api/src/libs/notification/template/subscription-trial-will-end.ts +12 -34
  13. package/api/src/libs/notification/template/subscription-will-canceled.ts +82 -48
  14. package/api/src/libs/notification/template/subscription-will-renew.ts +16 -45
  15. package/api/src/libs/time.ts +13 -0
  16. package/api/src/libs/util.ts +17 -0
  17. package/api/src/locales/en.ts +12 -2
  18. package/api/src/locales/zh.ts +11 -2
  19. package/api/src/queues/checkout-session.ts +15 -0
  20. package/api/src/queues/event.ts +13 -4
  21. package/api/src/queues/invoice.ts +21 -3
  22. package/api/src/queues/payment.ts +3 -0
  23. package/api/src/queues/refund.ts +3 -0
  24. package/api/src/queues/subscription.ts +107 -2
  25. package/api/src/queues/usage-record.ts +4 -0
  26. package/api/src/queues/webhook.ts +9 -0
  27. package/api/src/routes/checkout-sessions.ts +40 -2
  28. package/api/src/routes/connect/recharge.ts +143 -0
  29. package/api/src/routes/connect/shared.ts +25 -0
  30. package/api/src/routes/customers.ts +2 -2
  31. package/api/src/routes/donations.ts +5 -1
  32. package/api/src/routes/events.ts +9 -4
  33. package/api/src/routes/payment-links.ts +40 -20
  34. package/api/src/routes/prices.ts +17 -4
  35. package/api/src/routes/products.ts +21 -2
  36. package/api/src/routes/refunds.ts +20 -3
  37. package/api/src/routes/subscription-items.ts +39 -2
  38. package/api/src/routes/subscriptions.ts +77 -40
  39. package/api/src/routes/usage-records.ts +29 -0
  40. package/api/src/store/models/event.ts +1 -0
  41. package/api/src/store/models/subscription.ts +2 -0
  42. package/api/tests/libs/time.spec.ts +54 -0
  43. package/blocklet.yml +1 -1
  44. package/package.json +19 -19
  45. package/src/app.tsx +10 -0
  46. package/src/components/subscription/actions/cancel.tsx +30 -9
  47. package/src/components/subscription/actions/index.tsx +11 -3
  48. package/src/components/webhook/attempts.tsx +122 -3
  49. package/src/locales/en.tsx +13 -0
  50. package/src/locales/zh.tsx +13 -0
  51. package/src/pages/customer/recharge.tsx +417 -0
  52. 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('create payment link error', err);
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
- await doc.update(formatBeforeSave(Object.assign({}, doc.dataValues, req.body)));
339
-
340
- res.json(doc);
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
- // if (doc.locked) {
356
- // return res.status(403).json({ error: 'payment link locked' });
357
- // }
358
-
359
- await doc.update({ active: false });
360
- return res.json(doc);
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
- // if (doc.locked) {
376
- // return res.status(403).json({ error: 'payment link locked' });
377
- // }
378
-
379
- await doc.destroy();
380
- return res.json(doc);
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
- console.error(err);
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
  });
@@ -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
- console.error(err);
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
- await doc.update(Price.formatBeforeSave(updates));
350
-
351
- return res.json(await getExpandedPrice(req.params.id as string));
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('create refund error', err);
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
- console.error(err);
247
- res.json(null);
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 perform this action' });
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
- const item = await Refund.create({
391
- type: 'refund',
392
- livemode: subscription.livemode,
393
- amount: refund === 'last' ? result.total : result.unused,
394
- description: 'refund_transfer_on_subscription_cancel',
395
- status: 'pending',
396
- reason: 'requested_by_admin',
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 skipped', {
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('subscription deleted', { subscription: req.params.id });
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
 
@@ -24,6 +24,7 @@ export class Event extends Model<InferAttributes<Event>, InferCreationAttributes
24
24
  declare request: {
25
25
  id: string;
26
26
  idempotency_key: string;
27
+ requested_by?: string;
27
28
  };
28
29
 
29
30
  // Number of webhooks that have yet to be successfully delivered
@@ -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
+ });