payment-kit 1.25.8 → 1.26.0

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 (115) hide show
  1. package/api/src/crons/index.ts +24 -0
  2. package/api/src/libs/archive/config.ts +254 -0
  3. package/api/src/libs/archive/executor.ts +729 -0
  4. package/api/src/libs/archive/index.ts +7 -0
  5. package/api/src/libs/archive/lock.ts +50 -0
  6. package/api/src/libs/archive/policy.ts +55 -0
  7. package/api/src/libs/archive/query.ts +136 -0
  8. package/api/src/libs/archive/snapshot.ts +291 -0
  9. package/api/src/libs/archive/store.ts +200 -0
  10. package/api/src/libs/session.ts +43 -25
  11. package/api/src/queues/archive.ts +32 -0
  12. package/api/src/queues/subscription.ts +3 -1
  13. package/api/src/routes/archive.ts +176 -0
  14. package/api/src/routes/checkout-sessions.ts +50 -34
  15. package/api/src/routes/index.ts +2 -0
  16. package/api/src/routes/meters.ts +28 -0
  17. package/api/src/routes/payment-stats.ts +167 -20
  18. package/api/src/store/migrations/20260203-archive.ts +12 -0
  19. package/api/src/store/migrations/20260204-revenue-snapshot.ts +19 -0
  20. package/api/src/store/models/archive-lock.ts +55 -0
  21. package/api/src/store/models/archive-metadata.ts +132 -0
  22. package/api/src/store/models/index.ts +9 -0
  23. package/api/src/store/models/revenue-snapshot.ts +110 -0
  24. package/api/tests/libs/archive-config.spec.ts +185 -0
  25. package/api/tests/libs/archive-executor.spec.ts +678 -0
  26. package/api/tests/libs/archive-lock.spec.ts +130 -0
  27. package/api/tests/libs/archive-policy.spec.ts +255 -0
  28. package/api/tests/libs/archive-query.spec.ts +267 -0
  29. package/api/tests/libs/archive-store.spec.ts +159 -0
  30. package/blocklet.prefs.json +187 -0
  31. package/blocklet.yml +2 -1
  32. package/package.json +10 -10
  33. package/src/components/customer/actions.tsx +1 -1
  34. package/src/components/customer/credit-overview.tsx +3 -1
  35. package/src/components/customer/overdraft-protection.tsx +1 -1
  36. package/src/components/event/list.tsx +1 -1
  37. package/src/components/filter-toolbar.tsx +2 -2
  38. package/src/components/invoice/action.tsx +3 -3
  39. package/src/components/invoice/list.tsx +1 -1
  40. package/src/components/invoice/recharge.tsx +2 -2
  41. package/src/components/meter/add-usage-dialog.tsx +1 -1
  42. package/src/components/passport/actions.tsx +1 -1
  43. package/src/components/passport/assign.tsx +1 -1
  44. package/src/components/payment-currency/add.tsx +1 -1
  45. package/src/components/payment-currency/edit.tsx +1 -1
  46. package/src/components/payment-intent/actions.tsx +4 -4
  47. package/src/components/payment-intent/list.tsx +1 -1
  48. package/src/components/payment-link/actions.tsx +4 -4
  49. package/src/components/payment-link/item.tsx +1 -1
  50. package/src/components/payouts/list.tsx +1 -1
  51. package/src/components/payouts/portal/list.tsx +1 -1
  52. package/src/components/price/upsell-select.tsx +1 -1
  53. package/src/components/price/upsell.tsx +2 -2
  54. package/src/components/pricing-table/actions.tsx +3 -3
  55. package/src/components/pricing-table/product-item.tsx +1 -1
  56. package/src/components/product/actions.tsx +3 -3
  57. package/src/components/product/create.tsx +1 -1
  58. package/src/components/product/cross-sell.tsx +2 -2
  59. package/src/components/promotion/active-redemptions.tsx +1 -1
  60. package/src/components/refund/list.tsx +1 -1
  61. package/src/components/subscription/actions/index.tsx +1 -1
  62. package/src/components/subscription/items/usage-records.tsx +4 -2
  63. package/src/components/subscription/list.tsx +1 -1
  64. package/src/components/subscription/metrics.tsx +3 -3
  65. package/src/components/subscription/portal/actions.tsx +15 -12
  66. package/src/components/subscription/portal/list.tsx +1 -1
  67. package/src/components/webhook/attempts.tsx +4 -4
  68. package/src/hooks/subscription.ts +2 -2
  69. package/src/locales/en.tsx +4 -0
  70. package/src/locales/zh.tsx +4 -0
  71. package/src/pages/admin/billing/meter-events/index.tsx +3 -3
  72. package/src/pages/admin/billing/meters/index.tsx +1 -1
  73. package/src/pages/admin/billing/overdue/index.tsx +2 -2
  74. package/src/pages/admin/billing/subscriptions/detail.tsx +2 -2
  75. package/src/pages/admin/customers/customers/credit-grant/detail.tsx +1 -1
  76. package/src/pages/admin/customers/customers/credit-transaction/detail.tsx +1 -1
  77. package/src/pages/admin/customers/customers/detail.tsx +4 -4
  78. package/src/pages/admin/developers/events/detail.tsx +1 -1
  79. package/src/pages/admin/developers/webhooks/detail.tsx +1 -1
  80. package/src/pages/admin/developers/webhooks/index.tsx +1 -1
  81. package/src/pages/admin/overview.tsx +2 -0
  82. package/src/pages/admin/payments/intents/detail.tsx +2 -2
  83. package/src/pages/admin/payments/payouts/detail.tsx +2 -2
  84. package/src/pages/admin/payments/refunds/detail.tsx +2 -2
  85. package/src/pages/admin/products/coupons/detail.tsx +1 -1
  86. package/src/pages/admin/products/coupons/index.tsx +1 -1
  87. package/src/pages/admin/products/exchange-rate-providers/index.tsx +1 -1
  88. package/src/pages/admin/products/links/create.tsx +1 -1
  89. package/src/pages/admin/products/links/detail.tsx +2 -2
  90. package/src/pages/admin/products/links/index.tsx +1 -1
  91. package/src/pages/admin/products/passports/index.tsx +1 -1
  92. package/src/pages/admin/products/prices/actions.tsx +4 -4
  93. package/src/pages/admin/products/prices/detail.tsx +2 -2
  94. package/src/pages/admin/products/pricing-tables/create.tsx +1 -1
  95. package/src/pages/admin/products/pricing-tables/detail.tsx +2 -2
  96. package/src/pages/admin/products/pricing-tables/index.tsx +1 -1
  97. package/src/pages/admin/products/products/index.tsx +1 -1
  98. package/src/pages/admin/products/promotion-codes/actions.tsx +2 -2
  99. package/src/pages/admin/products/promotion-codes/detail.tsx +2 -2
  100. package/src/pages/admin/products/promotion-codes/list.tsx +1 -1
  101. package/src/pages/admin/settings/payment-methods/create.tsx +1 -1
  102. package/src/pages/admin/settings/payment-methods/edit.tsx +1 -1
  103. package/src/pages/admin/settings/payment-methods/index.tsx +2 -2
  104. package/src/pages/admin/tax/detail.tsx +2 -2
  105. package/src/pages/admin/tax/list.tsx +1 -1
  106. package/src/pages/checkout/pay.tsx +2 -2
  107. package/src/pages/customer/index.tsx +1 -1
  108. package/src/pages/customer/invoice/past-due.tsx +1 -1
  109. package/src/pages/customer/payout/detail.tsx +1 -1
  110. package/src/pages/customer/refund/list.tsx +1 -1
  111. package/src/pages/customer/subscription/change-payment.tsx +2 -2
  112. package/src/pages/customer/subscription/change-plan.tsx +3 -3
  113. package/src/pages/customer/subscription/detail.tsx +3 -3
  114. package/src/pages/integrations/donations/index.tsx +1 -1
  115. package/vite.config.ts +3 -1
@@ -137,6 +137,34 @@ router.post('/', auth, async (req, res) => {
137
137
  }
138
138
  });
139
139
 
140
+ // Public endpoint: only returns safe fields, no auth required
141
+ const PUBLIC_METER_FIELDS = ['id', 'name', 'event_name', 'status', 'unit', 'description', 'currency_id'] as const;
142
+
143
+ router.get('/public/:id', async (req, res) => {
144
+ try {
145
+ const meter = await Meter.findOne({
146
+ where: {
147
+ [Op.or]: [{ id: req.params.id }, { event_name: req.params.id }],
148
+ },
149
+ include: [{ model: PaymentCurrency, as: 'paymentCurrency' }],
150
+ });
151
+
152
+ if (!meter) {
153
+ return res.status(404).json({ error: 'Meter not found' });
154
+ }
155
+
156
+ return res.json({
157
+ ...pick(meter.toJSON(), PUBLIC_METER_FIELDS),
158
+ paymentCurrency: (meter as any).paymentCurrency
159
+ ? pick((meter as any).paymentCurrency.toJSON(), ['id', 'name', 'symbol', 'decimal', 'logo', 'type'])
160
+ : null,
161
+ });
162
+ } catch (err) {
163
+ logger.error('get public meter failed', { error: err?.message, meterId: req.params.id });
164
+ return res.status(400).json({ error: err?.message });
165
+ }
166
+ });
167
+
140
168
  router.get('/:id', auth, async (req, res) => {
141
169
  try {
142
170
  const meter = await Meter.findOne({
@@ -11,6 +11,7 @@ import { ethWallet, wallet } from '../libs/auth';
11
11
  import dayjs from '../libs/dayjs';
12
12
  import { authenticate } from '../libs/security';
13
13
  import {
14
+ CreditGrant,
14
15
  EVMChainType,
15
16
  Invoice,
16
17
  InvoiceItem,
@@ -20,6 +21,7 @@ import {
20
21
  PaymentStat,
21
22
  Payout,
22
23
  Refund,
24
+ RevenueSnapshot,
23
25
  Subscription,
24
26
  } from '../store/models';
25
27
  import { EVM_CHAIN_TYPES } from '../libs/constants';
@@ -121,7 +123,9 @@ async function getCurrencyLinks(livemode: boolean) {
121
123
 
122
124
  type RevenueStats = {
123
125
  totalRevenue: string;
126
+ refundAmount: string;
124
127
  promotionCost: string;
128
+ creditGrantCost: string;
125
129
  vendorCost: string;
126
130
  taxedRevenue: string;
127
131
  netRevenue: string;
@@ -129,7 +133,9 @@ type RevenueStats = {
129
133
 
130
134
  const ZERO_REVENUE_STATS: RevenueStats = {
131
135
  totalRevenue: '0',
136
+ refundAmount: '0',
132
137
  promotionCost: '0',
138
+ creditGrantCost: '0',
133
139
  vendorCost: '0',
134
140
  taxedRevenue: '0',
135
141
  netRevenue: '0',
@@ -202,6 +208,62 @@ async function getTaxedInvoiceIds(
202
208
 
203
209
  async function getRevenueStats(livemode: boolean, currencyId?: string, start?: number, end?: number) {
204
210
  const currencyFilter = currencyId ? [currencyId] : undefined;
211
+ const byCurrency: Record<string, RevenueStats> = {};
212
+
213
+ // Step 1: Query snapshots within the date range
214
+ const snapshotWhere: WhereOptions = { livemode };
215
+ if (currencyId) {
216
+ snapshotWhere.currency_id = currencyId;
217
+ }
218
+ if (start || end) {
219
+ snapshotWhere.timestamp = {};
220
+ if (start) {
221
+ (snapshotWhere.timestamp as any)[Op.gte] = start;
222
+ }
223
+ if (end) {
224
+ (snapshotWhere.timestamp as any)[Op.lt] = end;
225
+ }
226
+ }
227
+
228
+ const snapshots = await RevenueSnapshot.findAll({ where: snapshotWhere });
229
+
230
+ // Track the latest snapshot end timestamp to avoid double counting
231
+ // Each snapshot's timestamp is month start, so the data covers [timestamp, timestamp + 1 month)
232
+ let latestSnapshotEnd = 0;
233
+
234
+ for (const snapshot of snapshots) {
235
+ const id = snapshot.currency_id;
236
+ if (!byCurrency[id]) {
237
+ byCurrency[id] = { ...ZERO_REVENUE_STATS };
238
+ }
239
+ const entry = byCurrency[id]!;
240
+ entry.totalRevenue = new BN(entry.totalRevenue).add(new BN(snapshot.total_revenue || '0')).toString();
241
+ entry.refundAmount = new BN(entry.refundAmount).add(new BN(snapshot.refund_amount || '0')).toString();
242
+ entry.promotionCost = new BN(entry.promotionCost).add(new BN(snapshot.promotion_cost || '0')).toString();
243
+ entry.creditGrantCost = new BN(entry.creditGrantCost).add(new BN(snapshot.credit_grant_cost || '0')).toString();
244
+ entry.vendorCost = new BN(entry.vendorCost).add(new BN(snapshot.vendor_cost || '0')).toString();
245
+ entry.taxedRevenue = new BN(entry.taxedRevenue).add(new BN(snapshot.taxed_revenue || '0')).toString();
246
+ entry.netRevenue = new BN(entry.netRevenue).add(new BN(snapshot.net_revenue || '0')).toString();
247
+
248
+ // Calculate the end of this snapshot's coverage (start of next month)
249
+ const snapshotEndTimestamp = dayjs.unix(snapshot.timestamp).add(1, 'month').startOf('month').unix();
250
+ if (snapshotEndTimestamp > latestSnapshotEnd) {
251
+ latestSnapshotEnd = snapshotEndTimestamp;
252
+ }
253
+ }
254
+
255
+ // Step 2: Query live data only for time AFTER the latest snapshot
256
+ // This prevents double counting data that's already in snapshots
257
+ const liveDataStart = latestSnapshotEnd > 0 ? Math.max(latestSnapshotEnd, start || 0) : start;
258
+ const liveDataEnd = end;
259
+
260
+ // Skip live data query if the requested range is entirely covered by snapshots
261
+ if (liveDataStart && liveDataEnd && liveDataStart >= liveDataEnd) {
262
+ if (Object.keys(byCurrency).length === 0 && currencyId) {
263
+ byCurrency[currencyId] = { ...ZERO_REVENUE_STATS };
264
+ }
265
+ return byCurrency;
266
+ }
205
267
 
206
268
  const invoiceWhere: WhereOptions = {
207
269
  livemode,
@@ -214,13 +276,13 @@ async function getRevenueStats(livemode: boolean, currencyId?: string, start?: n
214
276
  invoiceWhere.currency_id = invoiceCurrencyFilter;
215
277
  }
216
278
 
217
- if (start || end) {
279
+ if (liveDataStart || liveDataEnd) {
218
280
  invoiceWhere.created_at = {};
219
- if (start) {
220
- invoiceWhere.created_at[Op.gte] = new Date(start * 1000);
281
+ if (liveDataStart) {
282
+ invoiceWhere.created_at[Op.gte] = new Date(liveDataStart * 1000);
221
283
  }
222
- if (end) {
223
- invoiceWhere.created_at[Op.lt] = new Date(end * 1000);
284
+ if (liveDataEnd) {
285
+ invoiceWhere.created_at[Op.lt] = new Date(liveDataEnd * 1000);
224
286
  }
225
287
  }
226
288
 
@@ -229,7 +291,7 @@ async function getRevenueStats(livemode: boolean, currencyId?: string, start?: n
229
291
  where: invoiceWhere,
230
292
  attributes: ['id', 'total', 'total_discount_amounts', 'currency_id'],
231
293
  }),
232
- getTaxedInvoiceIds(livemode, currencyFilter, start, end),
294
+ getTaxedInvoiceIds(livemode, currencyFilter, liveDataStart, liveDataEnd),
233
295
  ]);
234
296
 
235
297
  const totalRevenueByCurrency: Record<string, string> = {};
@@ -255,6 +317,7 @@ async function getRevenueStats(livemode: boolean, currencyId?: string, start?: n
255
317
  }
256
318
  });
257
319
 
320
+ // Query payouts for live data period
258
321
  const payoutWhere: WhereOptions = {
259
322
  livemode,
260
323
  status: { [Op.in]: ['paid', 'deferred'] },
@@ -266,13 +329,13 @@ async function getRevenueStats(livemode: boolean, currencyId?: string, start?: n
266
329
  payoutWhere.currency_id = payoutCurrencyFilter;
267
330
  }
268
331
 
269
- if (start || end) {
332
+ if (liveDataStart || liveDataEnd) {
270
333
  payoutWhere.created_at = {};
271
- if (start) {
272
- payoutWhere.created_at[Op.gte] = new Date(start * 1000);
334
+ if (liveDataStart) {
335
+ payoutWhere.created_at[Op.gte] = new Date(liveDataStart * 1000);
273
336
  }
274
- if (end) {
275
- payoutWhere.created_at[Op.lt] = new Date(end * 1000);
337
+ if (liveDataEnd) {
338
+ payoutWhere.created_at[Op.lt] = new Date(liveDataEnd * 1000);
276
339
  }
277
340
  }
278
341
 
@@ -286,32 +349,116 @@ async function getRevenueStats(livemode: boolean, currencyId?: string, start?: n
286
349
  addToMap(vendorCostByCurrency, payout.currency_id, payout.amount || '0');
287
350
  });
288
351
 
352
+ // Query refunds for live data period
353
+ const refundWhere: WhereOptions = {
354
+ livemode,
355
+ status: 'succeeded',
356
+ };
357
+ const refundCurrencyFilter = buildCurrencyFilter(currencyFilter);
358
+ if (refundCurrencyFilter) {
359
+ refundWhere.currency_id = refundCurrencyFilter;
360
+ }
361
+ if (liveDataStart || liveDataEnd) {
362
+ refundWhere.created_at = {};
363
+ if (liveDataStart) {
364
+ refundWhere.created_at[Op.gte] = new Date(liveDataStart * 1000);
365
+ }
366
+ if (liveDataEnd) {
367
+ refundWhere.created_at[Op.lt] = new Date(liveDataEnd * 1000);
368
+ }
369
+ }
370
+
371
+ const refunds = await Refund.findAll({
372
+ where: refundWhere,
373
+ attributes: ['amount', 'currency_id'],
374
+ });
375
+
376
+ const refundAmountByCurrency: Record<string, string> = {};
377
+ refunds.forEach((refund) => {
378
+ addToMap(refundAmountByCurrency, refund.currency_id, refund.amount || '0');
379
+ });
380
+
381
+ // Query credit grants for live data period
382
+ // Include granted (active), depleted, and expired - all have consumed portions
383
+ // Exclude: pending (not yet active), voided (revoked, not a cost)
384
+ // Cost = amount - remaining_amount (the actually consumed portion)
385
+ const creditGrantWhere: WhereOptions = {
386
+ livemode,
387
+ status: { [Op.in]: ['granted', 'depleted', 'expired'] },
388
+ };
389
+ const creditGrantCurrencyFilter = buildCurrencyFilter(currencyFilter);
390
+ if (creditGrantCurrencyFilter) {
391
+ creditGrantWhere.currency_id = creditGrantCurrencyFilter;
392
+ }
393
+ if (liveDataStart || liveDataEnd) {
394
+ creditGrantWhere.created_at = {};
395
+ if (liveDataStart) {
396
+ creditGrantWhere.created_at[Op.gte] = new Date(liveDataStart * 1000);
397
+ }
398
+ if (liveDataEnd) {
399
+ creditGrantWhere.created_at[Op.lt] = new Date(liveDataEnd * 1000);
400
+ }
401
+ }
402
+
403
+ const creditGrants = await CreditGrant.findAll({
404
+ where: creditGrantWhere,
405
+ attributes: ['amount', 'remaining_amount', 'currency_id'],
406
+ });
407
+
408
+ const creditGrantCostByCurrency: Record<string, string> = {};
409
+ creditGrants.forEach((grant) => {
410
+ // Cost = consumed amount = total amount - remaining amount
411
+ const consumed = new BN(grant.amount || '0').sub(new BN(grant.remaining_amount || '0'));
412
+ addToMap(creditGrantCostByCurrency, grant.currency_id, consumed.toString());
413
+ });
414
+
415
+ // Merge live data with snapshot data
289
416
  const currencySet = new Set<string>(currencyFilter || []);
290
- [totalRevenueByCurrency, promotionCostByCurrency, taxedRevenueByCurrency, vendorCostByCurrency].forEach((map) => {
417
+ [
418
+ totalRevenueByCurrency,
419
+ promotionCostByCurrency,
420
+ taxedRevenueByCurrency,
421
+ vendorCostByCurrency,
422
+ refundAmountByCurrency,
423
+ creditGrantCostByCurrency,
424
+ ].forEach((map) => {
291
425
  Object.keys(map).forEach((id) => currencySet.add(id));
292
426
  });
427
+ Object.keys(byCurrency).forEach((id) => currencySet.add(id));
293
428
 
294
429
  const currencyList = Array.from(currencySet);
295
- const byCurrency: Record<string, RevenueStats> = {};
296
430
 
297
431
  currencyList.forEach((id) => {
298
- const totalRevenue = totalRevenueByCurrency[id] || '0';
299
- const promotionCost = promotionCostByCurrency[id] || '0';
300
- const taxedRevenue = taxedRevenueByCurrency[id] || '0';
301
- const vendorCost = vendorCostByCurrency[id] || '0';
432
+ const snapshotData = byCurrency[id] || ZERO_REVENUE_STATS;
433
+ const liveTotal = totalRevenueByCurrency[id] || '0';
434
+ const liveRefund = refundAmountByCurrency[id] || '0';
435
+ const livePromotion = promotionCostByCurrency[id] || '0';
436
+ const liveCreditGrant = creditGrantCostByCurrency[id] || '0';
437
+ const liveTaxed = taxedRevenueByCurrency[id] || '0';
438
+ const liveVendor = vendorCostByCurrency[id] || '0';
439
+
440
+ const totalRevenue = new BN(snapshotData.totalRevenue).add(new BN(liveTotal)).toString();
441
+ const refundAmount = new BN(snapshotData.refundAmount).add(new BN(liveRefund)).toString();
442
+ const promotionCost = new BN(snapshotData.promotionCost).add(new BN(livePromotion)).toString();
443
+ const creditGrantCost = new BN(snapshotData.creditGrantCost).add(new BN(liveCreditGrant)).toString();
444
+ const taxedRevenue = new BN(snapshotData.taxedRevenue).add(new BN(liveTaxed)).toString();
445
+ const vendorCost = new BN(snapshotData.vendorCost).add(new BN(liveVendor)).toString();
446
+ // Net Revenue = Total Revenue - Refunds
447
+ const netRevenue = new BN(totalRevenue).sub(new BN(refundAmount)).toString();
302
448
 
303
449
  const includeEntry =
304
- hasNonZeroValue([totalRevenue, promotionCost, taxedRevenue, vendorCost]) || Boolean(currencyFilter);
450
+ hasNonZeroValue([totalRevenue, refundAmount, promotionCost, creditGrantCost, taxedRevenue, vendorCost]) ||
451
+ Boolean(currencyFilter);
305
452
 
306
453
  if (!includeEntry) {
307
454
  return;
308
455
  }
309
456
 
310
- const netRevenue = new BN(totalRevenue).sub(new BN(vendorCost)).toString();
311
-
312
457
  byCurrency[id] = {
313
458
  totalRevenue,
459
+ refundAmount,
314
460
  promotionCost,
461
+ creditGrantCost,
315
462
  vendorCost,
316
463
  taxedRevenue,
317
464
  netRevenue,
@@ -0,0 +1,12 @@
1
+ import type { Migration } from '../migrate';
2
+ import models from '../models';
3
+
4
+ export const up: Migration = async ({ context }) => {
5
+ await context.createTable('archive_metadata', models.ArchiveMetadata.GENESIS_ATTRIBUTES);
6
+ await context.createTable('archive_locks', models.ArchiveLock.GENESIS_ATTRIBUTES);
7
+ };
8
+
9
+ export const down: Migration = async ({ context }) => {
10
+ await context.dropTable('archive_metadata');
11
+ await context.dropTable('archive_locks');
12
+ };
@@ -0,0 +1,19 @@
1
+ import type { Migration } from '../migrate';
2
+ import { createIndexIfNotExists } from '../migrate';
3
+ import models from '../models';
4
+
5
+ export const up: Migration = async ({ context }) => {
6
+ await context.createTable('revenue_snapshots', models.RevenueSnapshot.GENESIS_ATTRIBUTES);
7
+ await createIndexIfNotExists(
8
+ context,
9
+ 'revenue_snapshots',
10
+ ['timestamp', 'currency_id', 'livemode', 'period_type'],
11
+ 'idx_revenue_snapshots_unique',
12
+ { unique: true }
13
+ );
14
+ };
15
+
16
+ export const down: Migration = async ({ context }) => {
17
+ await context.removeIndex('revenue_snapshots', 'idx_revenue_snapshots_unique');
18
+ await context.dropTable('revenue_snapshots');
19
+ };
@@ -0,0 +1,55 @@
1
+ /* eslint-disable @typescript-eslint/lines-between-class-members */
2
+ import { CreationOptional, DataTypes, InferAttributes, InferCreationAttributes, Model } from 'sequelize';
3
+
4
+ export class ArchiveLock extends Model<InferAttributes<ArchiveLock>, InferCreationAttributes<ArchiveLock>> {
5
+ declare id: CreationOptional<string>;
6
+ declare locked_by?: string | null;
7
+ declare locked_at?: number | null;
8
+ declare expires_at?: number | null;
9
+ declare created_at: CreationOptional<Date>;
10
+ declare updated_at: CreationOptional<Date>;
11
+
12
+ public static readonly GENESIS_ATTRIBUTES = {
13
+ id: {
14
+ type: DataTypes.STRING(40),
15
+ primaryKey: true,
16
+ allowNull: false,
17
+ },
18
+ locked_by: {
19
+ type: DataTypes.STRING(64),
20
+ allowNull: true,
21
+ },
22
+ locked_at: {
23
+ type: DataTypes.INTEGER,
24
+ allowNull: true,
25
+ },
26
+ expires_at: {
27
+ type: DataTypes.INTEGER,
28
+ allowNull: true,
29
+ },
30
+ created_at: {
31
+ type: DataTypes.DATE,
32
+ defaultValue: DataTypes.NOW,
33
+ allowNull: false,
34
+ },
35
+ updated_at: {
36
+ type: DataTypes.DATE,
37
+ defaultValue: DataTypes.NOW,
38
+ allowNull: false,
39
+ },
40
+ };
41
+
42
+ public static initialize(sequelize: any) {
43
+ this.init(ArchiveLock.GENESIS_ATTRIBUTES, {
44
+ sequelize,
45
+ modelName: 'ArchiveLock',
46
+ tableName: 'archive_locks',
47
+ createdAt: 'created_at',
48
+ updatedAt: 'updated_at',
49
+ });
50
+ }
51
+
52
+ public static associate() {}
53
+ }
54
+
55
+ export type TArchiveLock = InferAttributes<ArchiveLock>;
@@ -0,0 +1,132 @@
1
+ /* eslint-disable @typescript-eslint/lines-between-class-members */
2
+ import { CreationOptional, DataTypes, InferAttributes, InferCreationAttributes, Model } from 'sequelize';
3
+
4
+ export class ArchiveMetadata extends Model<InferAttributes<ArchiveMetadata>, InferCreationAttributes<ArchiveMetadata>> {
5
+ declare id: CreationOptional<string>;
6
+ declare archive_file: string;
7
+ declare created_at: CreationOptional<Date>;
8
+ declare updated_at: CreationOptional<Date>;
9
+
10
+ declare date_range_start: number;
11
+ declare date_range_end: number;
12
+
13
+ declare tables: Record<
14
+ string,
15
+ {
16
+ archived_count: number;
17
+ deleted_count: number;
18
+ failed_count: number;
19
+ failed_ids?: string[];
20
+ }
21
+ >;
22
+ declare total_records: number;
23
+
24
+ declare checksum?: string;
25
+ declare file_size?: number;
26
+
27
+ declare duration_ms?: number;
28
+ declare triggered_by: 'cron' | 'manual';
29
+ declare triggered_by_user_id?: string;
30
+ declare status: 'completed' | 'failed' | 'in_progress';
31
+ declare error?: string;
32
+
33
+ declare query_count: number;
34
+ declare query_actor_ids?: string[];
35
+ declare last_queried_at?: number;
36
+
37
+ public static readonly GENESIS_ATTRIBUTES = {
38
+ id: {
39
+ type: DataTypes.STRING(40),
40
+ primaryKey: true,
41
+ allowNull: false,
42
+ },
43
+ archive_file: {
44
+ type: DataTypes.STRING(128),
45
+ allowNull: false,
46
+ },
47
+ date_range_start: {
48
+ type: DataTypes.INTEGER,
49
+ allowNull: false,
50
+ },
51
+ date_range_end: {
52
+ type: DataTypes.INTEGER,
53
+ allowNull: false,
54
+ },
55
+ tables: {
56
+ type: DataTypes.JSON,
57
+ allowNull: false,
58
+ defaultValue: {},
59
+ },
60
+ total_records: {
61
+ type: DataTypes.INTEGER,
62
+ allowNull: false,
63
+ defaultValue: 0,
64
+ },
65
+ checksum: {
66
+ type: DataTypes.STRING(128),
67
+ allowNull: true,
68
+ },
69
+ file_size: {
70
+ type: DataTypes.INTEGER,
71
+ allowNull: true,
72
+ },
73
+ duration_ms: {
74
+ type: DataTypes.INTEGER,
75
+ allowNull: true,
76
+ },
77
+ triggered_by: {
78
+ type: DataTypes.ENUM('cron', 'manual'),
79
+ allowNull: false,
80
+ },
81
+ triggered_by_user_id: {
82
+ type: DataTypes.STRING(64),
83
+ allowNull: true,
84
+ },
85
+ status: {
86
+ type: DataTypes.ENUM('completed', 'failed', 'in_progress'),
87
+ allowNull: false,
88
+ },
89
+ error: {
90
+ type: DataTypes.TEXT,
91
+ allowNull: true,
92
+ },
93
+ query_count: {
94
+ type: DataTypes.INTEGER,
95
+ allowNull: false,
96
+ defaultValue: 0,
97
+ },
98
+ query_actor_ids: {
99
+ type: DataTypes.JSON,
100
+ allowNull: true,
101
+ defaultValue: [],
102
+ },
103
+ last_queried_at: {
104
+ type: DataTypes.INTEGER,
105
+ allowNull: true,
106
+ },
107
+ created_at: {
108
+ type: DataTypes.DATE,
109
+ defaultValue: DataTypes.NOW,
110
+ allowNull: false,
111
+ },
112
+ updated_at: {
113
+ type: DataTypes.DATE,
114
+ defaultValue: DataTypes.NOW,
115
+ allowNull: false,
116
+ },
117
+ };
118
+
119
+ public static initialize(sequelize: any) {
120
+ this.init(ArchiveMetadata.GENESIS_ATTRIBUTES, {
121
+ sequelize,
122
+ modelName: 'ArchiveMetadata',
123
+ tableName: 'archive_metadata',
124
+ createdAt: 'created_at',
125
+ updatedAt: 'updated_at',
126
+ });
127
+ }
128
+
129
+ public static associate() {}
130
+ }
131
+
132
+ export type TArchiveMetadata = InferAttributes<ArchiveMetadata>;
@@ -36,6 +36,9 @@ import { ProductVendor } from './product-vendor';
36
36
  import { TaxRate } from './tax-rate';
37
37
  import { ExchangeRateProvider } from './exchange-rate-provider';
38
38
  import { PriceQuote } from './price-quote';
39
+ import { ArchiveMetadata } from './archive-metadata';
40
+ import { ArchiveLock } from './archive-lock';
41
+ import { RevenueSnapshot } from './revenue-snapshot';
39
42
 
40
43
  const models = {
41
44
  CheckoutSession,
@@ -75,6 +78,9 @@ const models = {
75
78
  TaxRate,
76
79
  ExchangeRateProvider,
77
80
  PriceQuote,
81
+ ArchiveMetadata,
82
+ ArchiveLock,
83
+ RevenueSnapshot,
78
84
  };
79
85
 
80
86
  export function initialize(sequelize: any) {
@@ -128,6 +134,9 @@ export * from './product-vendor';
128
134
  export * from './tax-rate';
129
135
  export * from './exchange-rate-provider';
130
136
  export * from './price-quote';
137
+ export * from './archive-metadata';
138
+ export * from './archive-lock';
139
+ export * from './revenue-snapshot';
131
140
 
132
141
  export type TPriceExpanded = TPrice & {
133
142
  object: 'price';
@@ -0,0 +1,110 @@
1
+ /* eslint-disable @typescript-eslint/lines-between-class-members */
2
+ import { CreationOptional, DataTypes, InferAttributes, InferCreationAttributes, Model } from 'sequelize';
3
+
4
+ import { createIdGenerator } from '../../libs/util';
5
+
6
+ const nextId = createIdGenerator('rs', 24);
7
+
8
+ export class RevenueSnapshot extends Model<InferAttributes<RevenueSnapshot>, InferCreationAttributes<RevenueSnapshot>> {
9
+ declare id: CreationOptional<string>;
10
+ declare livemode: boolean;
11
+ declare currency_id: string;
12
+
13
+ declare timestamp: number;
14
+ declare period_type: 'monthly' | 'quarterly';
15
+
16
+ declare total_revenue: string;
17
+ declare refund_amount: string;
18
+ declare promotion_cost: string;
19
+ declare credit_grant_cost: string;
20
+ declare vendor_cost: string;
21
+ declare taxed_revenue: string;
22
+ declare net_revenue: string;
23
+
24
+ declare archive_metadata_id: CreationOptional<string>;
25
+
26
+ declare created_at: CreationOptional<Date>;
27
+ declare updated_at: CreationOptional<Date>;
28
+
29
+ public static readonly GENESIS_ATTRIBUTES = {
30
+ id: {
31
+ type: DataTypes.STRING(30),
32
+ primaryKey: true,
33
+ allowNull: false,
34
+ defaultValue: nextId,
35
+ },
36
+ livemode: {
37
+ type: DataTypes.BOOLEAN,
38
+ allowNull: false,
39
+ },
40
+ currency_id: {
41
+ type: DataTypes.STRING(16),
42
+ allowNull: false,
43
+ },
44
+ timestamp: {
45
+ type: DataTypes.INTEGER,
46
+ allowNull: false,
47
+ },
48
+ period_type: {
49
+ type: DataTypes.ENUM('monthly', 'quarterly'),
50
+ allowNull: false,
51
+ defaultValue: 'monthly',
52
+ },
53
+ total_revenue: {
54
+ type: DataTypes.STRING(64),
55
+ defaultValue: '0',
56
+ },
57
+ refund_amount: {
58
+ type: DataTypes.STRING(64),
59
+ defaultValue: '0',
60
+ },
61
+ promotion_cost: {
62
+ type: DataTypes.STRING(64),
63
+ defaultValue: '0',
64
+ },
65
+ credit_grant_cost: {
66
+ type: DataTypes.STRING(64),
67
+ defaultValue: '0',
68
+ },
69
+ vendor_cost: {
70
+ type: DataTypes.STRING(64),
71
+ defaultValue: '0',
72
+ },
73
+ taxed_revenue: {
74
+ type: DataTypes.STRING(64),
75
+ defaultValue: '0',
76
+ },
77
+ net_revenue: {
78
+ type: DataTypes.STRING(64),
79
+ defaultValue: '0',
80
+ },
81
+ archive_metadata_id: {
82
+ type: DataTypes.STRING(40),
83
+ allowNull: true,
84
+ },
85
+ created_at: {
86
+ type: DataTypes.DATE,
87
+ defaultValue: DataTypes.NOW,
88
+ allowNull: false,
89
+ },
90
+ updated_at: {
91
+ type: DataTypes.DATE,
92
+ defaultValue: DataTypes.NOW,
93
+ allowNull: false,
94
+ },
95
+ };
96
+
97
+ public static initialize(sequelize: any) {
98
+ this.init(RevenueSnapshot.GENESIS_ATTRIBUTES, {
99
+ sequelize,
100
+ modelName: 'RevenueSnapshot',
101
+ tableName: 'revenue_snapshots',
102
+ createdAt: 'created_at',
103
+ updatedAt: 'updated_at',
104
+ });
105
+ }
106
+
107
+ public static associate() {}
108
+ }
109
+
110
+ export type TRevenueSnapshot = InferAttributes<RevenueSnapshot>;