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
@@ -33,8 +33,31 @@ import { SubscriptionTrialWillEndSchedule } from './subscription-trial-will-end'
33
33
  import { SubscriptionWillCanceledSchedule } from './subscription-will-canceled';
34
34
  import { SubscriptionWillRenewSchedule } from './subscription-will-renew';
35
35
  import { scheduleVendorReturnScan } from '../queues/vendors/return-scanner';
36
+ import { getRetentionConfig } from '../libs/archive/config';
37
+ import { recoverFromCrash } from '../libs/archive/executor';
38
+ import { enqueueArchiveJob } from '../queues/archive';
36
39
 
37
40
  function init() {
41
+ // Recover any stale archive jobs from previous crash/restart
42
+ recoverFromCrash().catch((error) => {
43
+ logger.error('archive crash recovery error during init', { error });
44
+ });
45
+
46
+ const retentionConfig = getRetentionConfig();
47
+ const archiveJobs =
48
+ retentionConfig.enabled && retentionConfig.schedule.enabled
49
+ ? [
50
+ {
51
+ name: 'data.retention.archive',
52
+ time: retentionConfig.schedule.cron,
53
+ fn: () =>
54
+ enqueueArchiveJob({
55
+ triggeredBy: 'cron',
56
+ }),
57
+ options: { runOnInit: false },
58
+ },
59
+ ]
60
+ : [];
38
61
  Cron.init({
39
62
  context: {},
40
63
  jobs: [
@@ -131,6 +154,7 @@ function init() {
131
154
  fn: () => scheduleVendorReturnScan(),
132
155
  options: { runOnInit: false },
133
156
  },
157
+ ...archiveJobs,
134
158
  ],
135
159
  onError: (error: Error, name: string) => {
136
160
  logger.error('run job failed', { name, error });
@@ -0,0 +1,254 @@
1
+ import config from '@blocklet/sdk/lib/config';
2
+ import Joi from 'joi';
3
+
4
+ import logger from '../logger';
5
+
6
+ export type RetentionPreferences = {
7
+ retentionEnabled?: boolean;
8
+ scheduleEnabled?: boolean;
9
+ scheduleHour?: number;
10
+ batchSize?: number;
11
+ minFreeDiskMB?: number;
12
+ };
13
+
14
+ export type RetentionPolicyConfig = {
15
+ enabled: boolean;
16
+ defaults: {
17
+ retentionDays: number;
18
+ batchSize: number;
19
+ };
20
+ schedule: {
21
+ enabled: boolean;
22
+ hour: number;
23
+ cron: string;
24
+ };
25
+ storage: {
26
+ minFreeDiskMB: number;
27
+ maxArchiveFiles: number;
28
+ };
29
+ tables: Record<string, TableRetentionPolicy>;
30
+ };
31
+
32
+ export type TableRetentionPolicy = {
33
+ enabled: boolean;
34
+ retentionDays: number;
35
+ allowRestore?: boolean;
36
+ archivableStatuses?: string[];
37
+ excludeConditions?: {
38
+ hasActiveSubscription?: boolean;
39
+ statuses?: string[];
40
+ customCondition?: string;
41
+ };
42
+ /**
43
+ * Cascade relations: child tables to archive together with this table.
44
+ * Format: { childTable: foreignKeyField }
45
+ * Example: { invoice_items: 'invoice_id' } means when archiving an invoice,
46
+ * also archive all invoice_items where invoice_id matches.
47
+ */
48
+ cascadeRelations?: Record<string, string>;
49
+ };
50
+
51
+ const preferencesSchema = Joi.object<RetentionPreferences>({
52
+ retentionEnabled: Joi.boolean().default(false),
53
+ scheduleEnabled: Joi.boolean().default(true),
54
+ scheduleHour: Joi.number().integer().min(0).max(23).default(2),
55
+ batchSize: Joi.number().integer().min(100).max(500).default(500),
56
+ minFreeDiskMB: Joi.number().integer().min(100).default(1000),
57
+ });
58
+
59
+ const DEFAULT_TABLE_POLICIES: Record<string, TableRetentionPolicy> = {
60
+ meter_events: {
61
+ enabled: true,
62
+ retentionDays: 90,
63
+ archivableStatuses: ['completed', 'canceled'],
64
+ excludeConditions: {
65
+ statuses: ['pending', 'processing', 'requires_action', 'requires_capture'],
66
+ hasActiveSubscription: true,
67
+ },
68
+ },
69
+ credit_transactions: {
70
+ enabled: true,
71
+ retentionDays: 90,
72
+ excludeConditions: {
73
+ hasActiveSubscription: true,
74
+ },
75
+ },
76
+ events: {
77
+ enabled: true,
78
+ retentionDays: 90,
79
+ excludeConditions: {
80
+ customCondition: 'pending_webhooks > 0',
81
+ },
82
+ },
83
+ webhook_attempts: {
84
+ enabled: true,
85
+ retentionDays: 90,
86
+ },
87
+ jobs: {
88
+ enabled: false,
89
+ retentionDays: -1,
90
+ },
91
+ payment_intents: {
92
+ enabled: true,
93
+ retentionDays: 730,
94
+ archivableStatuses: ['succeeded', 'canceled'],
95
+ excludeConditions: {
96
+ statuses: [
97
+ 'requires_payment_method',
98
+ 'requires_confirmation',
99
+ 'requires_action',
100
+ 'processing',
101
+ 'requires_capture',
102
+ ],
103
+ hasActiveSubscription: true,
104
+ },
105
+ allowRestore: false,
106
+ },
107
+ invoices: {
108
+ enabled: true,
109
+ retentionDays: 730,
110
+ archivableStatuses: ['paid', 'void', 'uncollectible'],
111
+ excludeConditions: {
112
+ statuses: ['draft', 'open'],
113
+ hasActiveSubscription: true,
114
+ },
115
+ allowRestore: false,
116
+ cascadeRelations: {
117
+ invoice_items: 'invoice_id',
118
+ },
119
+ },
120
+ refunds: {
121
+ enabled: true,
122
+ retentionDays: 730,
123
+ archivableStatuses: ['succeeded', 'failed', 'canceled'],
124
+ excludeConditions: {
125
+ statuses: ['pending', 'requires_action'],
126
+ hasActiveSubscription: true,
127
+ },
128
+ allowRestore: false,
129
+ },
130
+ payouts: {
131
+ enabled: true,
132
+ retentionDays: 730,
133
+ archivableStatuses: ['paid', 'failed', 'canceled', 'reverted'],
134
+ excludeConditions: {
135
+ statuses: ['pending', 'in_transit', 'deferred'],
136
+ hasActiveSubscription: true,
137
+ },
138
+ allowRestore: false,
139
+ },
140
+ subscriptions: {
141
+ enabled: true,
142
+ retentionDays: 365,
143
+ archivableStatuses: ['canceled', 'incomplete_expired'],
144
+ excludeConditions: {
145
+ statuses: ['active', 'past_due', 'trialing', 'paused', 'incomplete'],
146
+ },
147
+ allowRestore: false,
148
+ cascadeRelations: {
149
+ subscription_items: 'subscription_id',
150
+ subscription_schedules: 'subscription_id',
151
+ },
152
+ },
153
+ credit_grants: {
154
+ enabled: true,
155
+ retentionDays: 365,
156
+ archivableStatuses: ['depleted', 'expired', 'voided'],
157
+ excludeConditions: {
158
+ statuses: ['pending', 'granted'],
159
+ hasActiveSubscription: true,
160
+ },
161
+ allowRestore: false,
162
+ },
163
+ products: {
164
+ enabled: false,
165
+ retentionDays: -1,
166
+ },
167
+ prices: {
168
+ enabled: false,
169
+ retentionDays: -1,
170
+ },
171
+ coupons: {
172
+ enabled: false,
173
+ retentionDays: -1,
174
+ },
175
+ checkout_sessions: {
176
+ enabled: true,
177
+ retentionDays: 365,
178
+ archivableStatuses: ['complete', 'expired'],
179
+ excludeConditions: {
180
+ statuses: ['open'],
181
+ hasActiveSubscription: true,
182
+ },
183
+ },
184
+ customers: {
185
+ enabled: false,
186
+ retentionDays: -1,
187
+ },
188
+ discounts: {
189
+ enabled: true,
190
+ retentionDays: 365,
191
+ excludeConditions: {
192
+ hasActiveSubscription: true,
193
+ },
194
+ allowRestore: false,
195
+ },
196
+ usage_records: {
197
+ enabled: true,
198
+ retentionDays: 90,
199
+ excludeConditions: {
200
+ hasActiveSubscription: true,
201
+ },
202
+ allowRestore: false,
203
+ },
204
+ price_quotes: {
205
+ enabled: true,
206
+ retentionDays: 90,
207
+ archivableStatuses: ['expired', 'used'],
208
+ excludeConditions: {
209
+ statuses: ['active', 'pending'],
210
+ },
211
+ allowRestore: false,
212
+ },
213
+ setup_intents: {
214
+ enabled: true,
215
+ retentionDays: 365,
216
+ archivableStatuses: ['succeeded', 'canceled'],
217
+ excludeConditions: {
218
+ statuses: ['requires_payment_method', 'requires_confirmation', 'requires_action', 'processing'],
219
+ },
220
+ allowRestore: false,
221
+ },
222
+ };
223
+
224
+ export const getArchiveCronExpression = (hour: number) => `0 0 ${hour} * * 3`;
225
+
226
+ export function getRetentionConfig(): RetentionPolicyConfig {
227
+ const prefs = (config.env?.preferences || {}) as RetentionPreferences;
228
+ const { value: rawValue, error } = preferencesSchema.validate(prefs, { stripUnknown: true });
229
+ // On validation error, log and fall back to schema defaults to enforce constraints (e.g. batchSize max)
230
+ const value = error ? preferencesSchema.validate({}, { stripUnknown: true }).value : rawValue;
231
+ if (error) {
232
+ logger.error('validate retention preferences error', { error });
233
+ }
234
+
235
+ const scheduleHour = value?.scheduleHour ?? 2;
236
+
237
+ return {
238
+ enabled: value?.retentionEnabled ?? false,
239
+ defaults: {
240
+ retentionDays: 365,
241
+ batchSize: value?.batchSize ?? 500,
242
+ },
243
+ schedule: {
244
+ enabled: value?.scheduleEnabled ?? true,
245
+ hour: scheduleHour,
246
+ cron: getArchiveCronExpression(scheduleHour),
247
+ },
248
+ storage: {
249
+ minFreeDiskMB: value?.minFreeDiskMB ?? 1000, // 1GB min free space (yearly archives can be large)
250
+ maxArchiveFiles: 10, // Keep 10 years of archive files (one per year)
251
+ },
252
+ tables: DEFAULT_TABLE_POLICIES,
253
+ };
254
+ }