payment-kit 1.25.8 → 1.25.10

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.
@@ -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
+ }