payment-kit 1.18.45 → 1.18.47

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.
@@ -23,6 +23,7 @@ import { Product } from '../store/models/product';
23
23
  import { Subscription } from '../store/models/subscription';
24
24
  import { getReturnStakeInvoices, getStakingInvoices, retryUncollectibleInvoices } from '../libs/invoice';
25
25
  import { CheckoutSession, PaymentLink, TInvoiceExpanded } from '../store/models';
26
+ import { mergePaginate, defaultTimeOrderBy, getCachedOrFetch, DataSource } from '../libs/pagination';
26
27
  import logger from '../libs/logger';
27
28
 
28
29
  const router = Router();
@@ -38,6 +39,99 @@ const authPortal = authenticate<Invoice>({
38
39
  },
39
40
  });
40
41
 
42
+ // Helper function: Get staking invoices by subscription ID with error handling
43
+ async function getStakingInvoicesById(subscriptionId: string) {
44
+ try {
45
+ const subscription = await Subscription.findByPk(subscriptionId);
46
+ if (!subscription) return [];
47
+ return await getStakingInvoices(subscription);
48
+ } catch (error) {
49
+ logger.error('Failed to get staking invoices by ID', { error, subscriptionId });
50
+ return [];
51
+ }
52
+ }
53
+
54
+ // Helper function: Get return stake invoices by subscription ID with error handling
55
+ async function getReturnStakeInvoicesById(subscriptionId: string) {
56
+ try {
57
+ const subscription = await Subscription.findByPk(subscriptionId);
58
+ if (!subscription) return [];
59
+ return await getReturnStakeInvoices(subscription);
60
+ } catch (error) {
61
+ logger.error('Failed to get return stake invoices by ID', { error, subscriptionId });
62
+ return [];
63
+ }
64
+ }
65
+
66
+ // Helper function: Create data sources for recover-from subscriptions (with cache)
67
+ function createRecoverFromSubscriptionSources(
68
+ recoverFromSubscriptionIds: string[],
69
+ include_staking: boolean | undefined,
70
+ include_return_staking: boolean | undefined
71
+ ): DataSource<Invoice>[] {
72
+ const sources: DataSource<Invoice>[] = [];
73
+
74
+ for (const id of recoverFromSubscriptionIds) {
75
+ if (include_staking) {
76
+ sources.push({
77
+ count: async () => {
78
+ try {
79
+ const invoices = await getCachedOrFetch(`staking:${id}`, () => getStakingInvoicesById(id));
80
+ return invoices.length;
81
+ } catch (error) {
82
+ logger.error('Failed to count cached staking invoices', { error, subscriptionId: id });
83
+ return 0;
84
+ }
85
+ },
86
+ fetch: async (limit: number, offset: number = 0) => {
87
+ try {
88
+ const invoices = await getCachedOrFetch(`staking:${id}`, () => getStakingInvoicesById(id));
89
+ return invoices.slice(offset, offset + limit);
90
+ } catch (error) {
91
+ logger.error('Failed to fetch cached staking invoices', { error, subscriptionId: id });
92
+ return [];
93
+ }
94
+ },
95
+ meta: {
96
+ type: 'cached' as const,
97
+ estimatedSize: 50,
98
+ cacheable: true,
99
+ },
100
+ });
101
+ }
102
+
103
+ if (include_return_staking) {
104
+ sources.push({
105
+ count: async () => {
106
+ try {
107
+ const invoices = await getCachedOrFetch(`return-staking:${id}`, () => getReturnStakeInvoicesById(id));
108
+ return invoices.length;
109
+ } catch (error) {
110
+ logger.error('Failed to count cached return stake invoices', { error, subscriptionId: id });
111
+ return 0;
112
+ }
113
+ },
114
+ fetch: async (limit: number, offset: number = 0) => {
115
+ try {
116
+ const invoices = await getCachedOrFetch(`return-staking:${id}`, () => getReturnStakeInvoicesById(id));
117
+ return invoices.slice(offset, offset + limit);
118
+ } catch (error) {
119
+ logger.error('Failed to fetch cached return stake invoices', { error, subscriptionId: id });
120
+ return [];
121
+ }
122
+ },
123
+ meta: {
124
+ type: 'cached' as const,
125
+ estimatedSize: 50,
126
+ cacheable: true,
127
+ },
128
+ });
129
+ }
130
+ }
131
+
132
+ return sources;
133
+ }
134
+
41
135
  const schema = createListParamSchema<{
42
136
  status?: string;
43
137
  customer_id?: string;
@@ -61,122 +155,220 @@ const schema = createListParamSchema<{
61
155
  include_overdraft_protection: Joi.boolean().default(true),
62
156
  include_recovered_from: Joi.boolean().empty(false),
63
157
  });
158
+
64
159
  router.get('/', authMine, async (req, res) => {
65
- // eslint-disable-next-line @typescript-eslint/naming-convention
66
- const {
67
- page,
68
- pageSize,
69
- livemode,
70
- status,
71
- ignore_zero,
72
- include_staking,
73
- include_return_staking,
74
- include_overdraft_protection = true,
75
- ...query
76
- } = await schema.validateAsync(req.query, {
77
- stripUnknown: false,
78
- allowUnknown: true,
79
- });
80
- const where = getWhereFromKvQuery(query.q);
160
+ try {
161
+ // eslint-disable-next-line @typescript-eslint/naming-convention
162
+ const {
163
+ page,
164
+ pageSize,
165
+ livemode,
166
+ status,
167
+ ignore_zero,
168
+ include_staking,
169
+ include_return_staking,
170
+ include_overdraft_protection = true,
171
+ ...query
172
+ } = await schema.validateAsync(req.query, {
173
+ stripUnknown: false,
174
+ allowUnknown: true,
175
+ });
81
176
 
82
- if (status) {
83
- where.status = status
84
- .split(',')
85
- .map((x) => x.trim())
86
- .filter(Boolean);
87
- }
88
- if (ignore_zero) {
89
- where.total = { [Op.ne]: '0' };
90
- }
91
- if (query.customer_id) {
92
- where.customer_id = query.customer_id;
93
- }
94
- if (query.currency_id) {
95
- where.currency_id = query.currency_id;
96
- }
97
- if (query.customer_did && isValid(query.customer_did)) {
98
- const customer = await Customer.findOne({ where: { did: query.customer_did } });
99
- if (customer) {
100
- where.customer_id = customer.id;
101
- } else {
102
- res.json({ count: 0, list: [], paging: { page, pageSize } });
103
- return;
177
+ const where = getWhereFromKvQuery(query.q);
178
+
179
+ if (status) {
180
+ where.status = status
181
+ .split(',')
182
+ .map((x) => x.trim())
183
+ .filter(Boolean);
104
184
  }
105
- }
106
- if (query.subscription_id) {
107
- if (query.include_recovered_from) {
108
- where.subscription_id = [query.subscription_id].concat(
109
- await Subscription.getFromSubscriptions(query.subscription_id)
110
- );
111
- } else {
112
- where.subscription_id = query.subscription_id;
185
+ if (ignore_zero) {
186
+ where.total = { [Op.ne]: '0' };
187
+ }
188
+ if (query.customer_id) {
189
+ where.customer_id = query.customer_id;
190
+ }
191
+ if (query.currency_id) {
192
+ where.currency_id = query.currency_id;
193
+ }
194
+ if (query.customer_did && isValid(query.customer_did)) {
195
+ const customer = await Customer.findOne({ where: { did: query.customer_did } });
196
+ if (customer) {
197
+ where.customer_id = customer.id;
198
+ } else {
199
+ res.json({ count: 0, list: [], paging: { page, pageSize } });
200
+ return;
201
+ }
202
+ }
203
+ if (query.subscription_id) {
204
+ if (query.include_recovered_from) {
205
+ where.subscription_id = [query.subscription_id].concat(
206
+ await Subscription.getFromSubscriptions(query.subscription_id)
207
+ );
208
+ } else {
209
+ where.subscription_id = query.subscription_id;
210
+ }
211
+ }
212
+ if (typeof livemode === 'boolean') {
213
+ where.livemode = livemode;
113
214
  }
114
- }
115
- if (typeof livemode === 'boolean') {
116
- where.livemode = livemode;
117
- }
118
215
 
119
- Object.keys(query)
120
- .filter((x) => x.startsWith('metadata.'))
121
- .forEach((key: string) => {
122
- // @ts-ignore
123
- where[key] = query[key];
124
- });
216
+ Object.keys(query)
217
+ .filter((x) => x.startsWith('metadata.'))
218
+ .forEach((key: string) => {
219
+ // @ts-ignore
220
+ where[key] = query[key];
221
+ });
125
222
 
126
- const excludeBillingReasons = ['recharge'];
127
- if (!include_overdraft_protection) {
128
- excludeBillingReasons.push('stake_overdraft_protection');
129
- }
130
- if (!!(include_staking && query.subscription_id) || !include_staking) {
131
- excludeBillingReasons.push('stake');
132
- }
133
- if (excludeBillingReasons.length > 0) {
134
- where.billing_reason = { [Op.notIn]: excludeBillingReasons };
135
- }
136
- try {
137
- const { rows: list, count } = await Invoice.findAndCountAll({
138
- where,
139
- order: getOrder(req.query, [['created_at', query.o === 'asc' ? 'ASC' : 'DESC']]),
140
- offset: (page - 1) * pageSize,
141
- limit: pageSize,
142
- include: [
143
- { model: PaymentCurrency, as: 'paymentCurrency' },
144
- { model: PaymentMethod, as: 'paymentMethod' },
145
- // { model: PaymentIntent, as: 'paymentIntent' },
146
- { model: Subscription, as: 'subscription', attributes: ['id', 'description'] },
147
- { model: Customer, as: 'customer' },
148
- ],
223
+ const excludeBillingReasons = ['recharge'];
224
+ if (!include_overdraft_protection) {
225
+ excludeBillingReasons.push('stake_overdraft_protection');
226
+ }
227
+ if (!!(include_staking && query.subscription_id) || !include_staking) {
228
+ excludeBillingReasons.push('stake');
229
+ }
230
+ if (excludeBillingReasons.length > 0) {
231
+ where.billing_reason = { [Op.notIn]: excludeBillingReasons };
232
+ }
233
+
234
+ // Build data sources with proper metadata
235
+ const sources: DataSource<Invoice>[] = [];
236
+
237
+ // Primary data source: Invoice database records
238
+ sources.push({
239
+ count: () => {
240
+ return Invoice.count({ where });
241
+ },
242
+ fetch: async (limit: number, offset: number = 0) => {
243
+ const result = await Invoice.findAll({
244
+ where,
245
+ order: getOrder(req.query, [['created_at', query.o === 'asc' ? 'ASC' : 'DESC']]),
246
+ limit,
247
+ offset,
248
+ include: [
249
+ { model: PaymentCurrency, as: 'paymentCurrency' },
250
+ { model: PaymentMethod, as: 'paymentMethod' },
251
+ { model: Subscription, as: 'subscription', attributes: ['id', 'description'] },
252
+ { model: Customer, as: 'customer' },
253
+ ],
254
+ });
255
+ return result;
256
+ },
257
+ meta: {
258
+ type: 'database' as const,
259
+ cacheable: false,
260
+ },
149
261
  });
150
262
 
151
- // push staking info as first invoice if we are on the last page
152
- let subscription;
153
- let invoices = list;
154
- if (query.subscription_id && include_staking && page === Math.ceil((count || 1) / pageSize)) {
155
- try {
156
- subscription = await Subscription.findByPk(query.subscription_id);
157
- if (subscription?.payment_details?.arcblock?.staking?.tx_hash) {
158
- const stakingInvoices = await getStakingInvoices(subscription);
159
- let returnStakeInvoices: any[] = [];
160
- if (include_return_staking) {
161
- returnStakeInvoices = await getReturnStakeInvoices(subscription);
162
- }
163
- invoices = [...(stakingInvoices || []), ...(returnStakeInvoices || []), ...list]
164
- .filter(Boolean)
165
- .sort((a, b) =>
166
- query.o === 'asc'
167
- ? new Date(a.created_at).getTime() - new Date(b.created_at).getTime()
168
- : new Date(b.created_at).getTime() - new Date(a.created_at).getTime()
169
- );
263
+ // Handle subscription_id array
264
+ let subscriptionIds: string[] = [];
265
+ if (Array.isArray(where.subscription_id)) {
266
+ subscriptionIds = where.subscription_id;
267
+ } else if (where.subscription_id) {
268
+ subscriptionIds = [where.subscription_id];
269
+ }
270
+
271
+ // Process active subscription (no cache) - only if staking options are enabled
272
+ const activeSubscriptionId = subscriptionIds[0];
273
+ if (include_staking && activeSubscriptionId) {
274
+ const subscription = await Subscription.findByPk(activeSubscriptionId);
275
+ if (subscription?.payment_details?.arcblock?.staking?.tx_hash) {
276
+ // Add staking invoices source
277
+ if (include_staking) {
278
+ sources.push({
279
+ count: async () => {
280
+ try {
281
+ const invoices = await getStakingInvoices(subscription);
282
+ return invoices.length;
283
+ } catch (error) {
284
+ logger.error('Failed to count staking invoices', { error, subscriptionId: activeSubscriptionId });
285
+ return 0;
286
+ }
287
+ },
288
+ fetch: async (limit: number, offset: number = 0) => {
289
+ try {
290
+ const invoices = await getStakingInvoices(subscription);
291
+ return invoices.slice(offset, offset + limit);
292
+ } catch (error) {
293
+ logger.error('Failed to fetch staking invoices', { error, subscriptionId: activeSubscriptionId });
294
+ return [];
295
+ }
296
+ },
297
+ meta: {
298
+ type: 'computed' as const,
299
+ estimatedSize: 50, // Most staking invoices are small
300
+ cacheable: true,
301
+ },
302
+ });
303
+ }
304
+
305
+ // Add return stake invoices source
306
+ if (include_return_staking) {
307
+ sources.push({
308
+ count: async () => {
309
+ try {
310
+ const invoices = await getReturnStakeInvoices(subscription);
311
+ return invoices.length;
312
+ } catch (error) {
313
+ logger.error('Failed to count return stake invoices', { error, subscriptionId: activeSubscriptionId });
314
+ return 0;
315
+ }
316
+ },
317
+ fetch: async (limit: number, offset: number = 0) => {
318
+ try {
319
+ const invoices = await getReturnStakeInvoices(subscription);
320
+ return invoices.slice(offset, offset + limit);
321
+ } catch (error) {
322
+ logger.error('Failed to fetch return stake invoices', { error, subscriptionId: activeSubscriptionId });
323
+ return [];
324
+ }
325
+ },
326
+ meta: {
327
+ type: 'computed' as const,
328
+ estimatedSize: 50, // Return stake invoices are usually fewer
329
+ cacheable: true,
330
+ },
331
+ });
170
332
  }
171
- } catch (err) {
172
- console.error('Failed to include staking record in invoice list', err);
173
333
  }
174
334
  }
175
335
 
176
- res.json({ count, list: invoices, subscription, paging: { page, pageSize } });
336
+ // Process recover-from subscriptions (with cache) - only if staking options are enabled
337
+ const recoverFromSubscriptionIds = subscriptionIds.slice(1);
338
+ if (include_staking && recoverFromSubscriptionIds.length > 0) {
339
+ const recoverFromSources = createRecoverFromSubscriptionSources(
340
+ recoverFromSubscriptionIds,
341
+ include_staking,
342
+ include_return_staking
343
+ );
344
+ sources.push(...recoverFromSources);
345
+ }
346
+
347
+ // Use generic pagination with intelligent strategy
348
+ const result = await mergePaginate(
349
+ sources,
350
+ { page: Number(page), pageSize: Number(pageSize) },
351
+ defaultTimeOrderBy(query.o === 'asc' ? 'asc' : 'desc')
352
+ );
353
+
354
+ res.json({
355
+ count: result.total,
356
+ list: result.data,
357
+ paging: result.paging,
358
+ });
177
359
  } catch (err) {
178
- logger.error(err);
179
- res.json({ count: 0, list: [], paging: { page, pageSize } });
360
+ logger.error('Failed to fetch invoices', {
361
+ error: err,
362
+ page: req.query.page,
363
+ pageSize: req.query.pageSize,
364
+ include_staking: req.query.include_staking,
365
+ subscription_id: req.query.subscription_id,
366
+ });
367
+ res.json({
368
+ count: 0,
369
+ list: [],
370
+ paging: { page: Number(req.query.page || 1), pageSize: Number(req.query.pageSize || 10) },
371
+ });
180
372
  }
181
373
  });
182
374