payment-kit 1.18.34 → 1.18.36
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.
- package/api/src/crons/subscription-trial-will-end.ts +1 -1
- package/api/src/index.ts +2 -0
- package/api/src/libs/did-space.ts +235 -0
- package/api/src/libs/util.ts +33 -1
- package/api/src/queues/payment.ts +7 -4
- package/api/src/queues/space.ts +661 -0
- package/api/src/queues/subscription.ts +1 -1
- package/api/src/routes/customers.ts +47 -0
- package/api/src/store/models/invoice.ts +12 -3
- package/api/tests/libs/util.spec.ts +215 -1
- package/blocklet.yml +2 -1
- package/package.json +10 -9
- package/scripts/sdk.js +58 -2
- package/src/components/customer/link.tsx +32 -57
- package/src/components/invoice/list.tsx +1 -1
- package/src/components/payouts/list.tsx +8 -2
- package/src/libs/util.ts +0 -13
- package/src/pages/admin/customers/customers/detail.tsx +2 -2
- package/src/pages/admin/customers/customers/index.tsx +1 -2
- package/src/pages/admin/payments/payouts/detail.tsx +1 -1
- package/src/pages/customer/recharge/account.tsx +2 -1
- package/src/pages/customer/recharge/subscription.tsx +2 -1
- package/src/pages/customer/subscription/embed.tsx +1 -0
|
@@ -0,0 +1,661 @@
|
|
|
1
|
+
import { getUrl } from '@blocklet/sdk';
|
|
2
|
+
import { literal, Op } from 'sequelize';
|
|
3
|
+
import { events } from '../libs/event';
|
|
4
|
+
import logger from '../libs/logger';
|
|
5
|
+
import createQueue from '../libs/queue';
|
|
6
|
+
import { uploadBillingInfo, BillingInfo, getEndpointAndSpaceDid } from '../libs/did-space';
|
|
7
|
+
import {
|
|
8
|
+
Customer,
|
|
9
|
+
EVMChainType,
|
|
10
|
+
Invoice,
|
|
11
|
+
Job,
|
|
12
|
+
PaymentCurrency,
|
|
13
|
+
PaymentIntent,
|
|
14
|
+
PaymentMethod,
|
|
15
|
+
PaymentMethodSettings,
|
|
16
|
+
Refund,
|
|
17
|
+
Subscription,
|
|
18
|
+
TInvoiceExpanded,
|
|
19
|
+
TRefundExpanded,
|
|
20
|
+
} from '../store/models';
|
|
21
|
+
import { CHARGE_SUPPORTED_CHAIN_TYPES } from '../libs/constants';
|
|
22
|
+
import env from '../libs/env';
|
|
23
|
+
import dayjs from '../libs/dayjs';
|
|
24
|
+
import { getExplorerTxUrl } from '../libs/util';
|
|
25
|
+
|
|
26
|
+
// Types
|
|
27
|
+
export type SpaceUploadType = 'invoice' | 'refund' | 'customer' | 'customerInvoiceChunk' | 'customerRefundChunk';
|
|
28
|
+
export type SpaceUploadData = {
|
|
29
|
+
invoice: { id: string };
|
|
30
|
+
refund: { id: string };
|
|
31
|
+
customer: { id: string };
|
|
32
|
+
customerInvoiceChunk: {
|
|
33
|
+
customerId: string;
|
|
34
|
+
chunkIndex: number;
|
|
35
|
+
totalChunks: number;
|
|
36
|
+
ids: string[];
|
|
37
|
+
processTime: number;
|
|
38
|
+
};
|
|
39
|
+
customerRefundChunk: {
|
|
40
|
+
customerId: string;
|
|
41
|
+
chunkIndex: number;
|
|
42
|
+
totalChunks: number;
|
|
43
|
+
ids: string[];
|
|
44
|
+
processTime: number;
|
|
45
|
+
};
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
export type SpaceUploadJob = {
|
|
49
|
+
type: SpaceUploadType;
|
|
50
|
+
data: SpaceUploadData[SpaceUploadType];
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
// Utility functions
|
|
54
|
+
const categoryMap = {
|
|
55
|
+
stake: 'stake',
|
|
56
|
+
slash_stake: 'slashStake',
|
|
57
|
+
overdraft_protection: 'fee',
|
|
58
|
+
stake_overdraft_protection: 'stake',
|
|
59
|
+
recharge: 'recharge',
|
|
60
|
+
return_stake: 'returnStake',
|
|
61
|
+
} as const;
|
|
62
|
+
|
|
63
|
+
const getBillingCategory = (billingReason: string): string => {
|
|
64
|
+
if (
|
|
65
|
+
billingReason.includes('stake') ||
|
|
66
|
+
billingReason.includes('recharge') ||
|
|
67
|
+
billingReason === 'overdraft_protection'
|
|
68
|
+
) {
|
|
69
|
+
return categoryMap[billingReason as keyof typeof categoryMap] || 'payment';
|
|
70
|
+
}
|
|
71
|
+
return 'payment';
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
const createSubscriptionInfo = (subscription: Subscription) => ({
|
|
75
|
+
type: 'subscription' as const,
|
|
76
|
+
id: subscription.id,
|
|
77
|
+
name: subscription.description || subscription.id,
|
|
78
|
+
status: subscription.status,
|
|
79
|
+
period_start: subscription.current_period_start,
|
|
80
|
+
period_end: subscription.current_period_end,
|
|
81
|
+
link: getUrl(`customer/subscription/${subscription.id}`),
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
const handleInvoicePaid = async (invoiceId: string) => {
|
|
85
|
+
const invoice = (await Invoice.findByPk(invoiceId, {
|
|
86
|
+
include: [
|
|
87
|
+
{
|
|
88
|
+
model: Customer,
|
|
89
|
+
as: 'customer',
|
|
90
|
+
attributes: ['did'],
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
model: PaymentCurrency,
|
|
94
|
+
as: 'paymentCurrency',
|
|
95
|
+
attributes: ['symbol', 'decimal'],
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
model: PaymentMethod,
|
|
99
|
+
as: 'paymentMethod',
|
|
100
|
+
attributes: ['type', 'settings'],
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
model: Subscription,
|
|
104
|
+
as: 'subscription',
|
|
105
|
+
attributes: ['id', 'description', 'status', 'current_period_start', 'current_period_end'],
|
|
106
|
+
},
|
|
107
|
+
{
|
|
108
|
+
model: PaymentIntent,
|
|
109
|
+
as: 'paymentIntent',
|
|
110
|
+
},
|
|
111
|
+
],
|
|
112
|
+
})) as TInvoiceExpanded | null;
|
|
113
|
+
|
|
114
|
+
// Validation
|
|
115
|
+
if (!invoice) {
|
|
116
|
+
logger.info('Upload invoice skipped because invoice not found:', { invoiceId });
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (invoice.metadata?.did_space_uploaded) {
|
|
121
|
+
logger.info('Upload invoice skipped because invoice already uploaded:', { invoiceId });
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const paymentDetails = invoice.paymentIntent?.payment_details || invoice.metadata?.payment_details;
|
|
126
|
+
if (!paymentDetails) {
|
|
127
|
+
logger.info('Upload invoice skipped because payment details not found:', { invoiceId });
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (!CHARGE_SUPPORTED_CHAIN_TYPES.includes(invoice?.paymentMethod?.type)) {
|
|
132
|
+
logger.info('Upload invoice skipped because payment method is not supported:', {
|
|
133
|
+
invoiceId,
|
|
134
|
+
paymentMethod: invoice?.paymentMethod?.type,
|
|
135
|
+
});
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const txHash = paymentDetails[invoice.paymentMethod?.type].tx_hash;
|
|
140
|
+
if (!txHash) {
|
|
141
|
+
logger.info('Upload invoice skipped because tx hash not found:', { invoiceId });
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
let spaceDid = null;
|
|
146
|
+
let endpoint = null;
|
|
147
|
+
try {
|
|
148
|
+
const result = await getEndpointAndSpaceDid(invoice.customer?.did);
|
|
149
|
+
spaceDid = result.spaceDid;
|
|
150
|
+
endpoint = result.endpoint;
|
|
151
|
+
} catch (error) {
|
|
152
|
+
logger.info('Customer space endpoint not available:', {
|
|
153
|
+
invoiceId,
|
|
154
|
+
did: invoice.customer?.did,
|
|
155
|
+
error: (error as Error).message,
|
|
156
|
+
});
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const methodInfo = (invoice.paymentMethod?.settings as PaymentMethodSettings)?.[
|
|
161
|
+
invoice.paymentMethod?.type as 'arcblock' | EVMChainType
|
|
162
|
+
];
|
|
163
|
+
// Create billing info
|
|
164
|
+
const billInfo: BillingInfo = {
|
|
165
|
+
tx_hash: txHash,
|
|
166
|
+
timestamp: dayjs(invoice.updated_at).unix(),
|
|
167
|
+
invoice_id: invoice.id,
|
|
168
|
+
category: getBillingCategory(invoice.billing_reason),
|
|
169
|
+
description: invoice.description || 'Payment received',
|
|
170
|
+
amount: invoice.amount_paid,
|
|
171
|
+
currency: {
|
|
172
|
+
symbol: invoice.paymentCurrency?.symbol,
|
|
173
|
+
decimal: invoice.paymentCurrency?.decimal || 18,
|
|
174
|
+
type: invoice.paymentMethod?.type || '',
|
|
175
|
+
chain_id: methodInfo?.chain_id || '',
|
|
176
|
+
explorer_host: methodInfo?.explorer_host || '',
|
|
177
|
+
explorer_tx_url: methodInfo?.explorer_host
|
|
178
|
+
? getExplorerTxUrl({
|
|
179
|
+
explorerHost: methodInfo?.explorer_host,
|
|
180
|
+
txHash,
|
|
181
|
+
type: invoice.paymentMethod?.type || '',
|
|
182
|
+
})
|
|
183
|
+
: '',
|
|
184
|
+
},
|
|
185
|
+
customer_did: invoice.customer?.did,
|
|
186
|
+
link: getUrl(`customer/invoice/${invoice.id}`),
|
|
187
|
+
app_pid: env.appPid,
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
if (invoice.subscription_id && invoice.subscription) {
|
|
191
|
+
billInfo.related = createSubscriptionInfo(invoice.subscription as Subscription);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Upload billing info
|
|
195
|
+
const result = await uploadBillingInfo(invoice.customer?.did, billInfo, endpoint);
|
|
196
|
+
if (result) {
|
|
197
|
+
// @ts-ignore
|
|
198
|
+
await invoice.update({
|
|
199
|
+
metadata: {
|
|
200
|
+
...invoice.metadata,
|
|
201
|
+
did_space_uploaded: true,
|
|
202
|
+
did_space: spaceDid,
|
|
203
|
+
},
|
|
204
|
+
});
|
|
205
|
+
logger.info('Successfully uploaded paid invoice:', {
|
|
206
|
+
invoiceId: invoice.id,
|
|
207
|
+
txHash,
|
|
208
|
+
customerDid: invoice.customer?.did,
|
|
209
|
+
});
|
|
210
|
+
} else {
|
|
211
|
+
logger.error('Failed to upload paid invoice:', {
|
|
212
|
+
invoiceId: invoice.id,
|
|
213
|
+
txHash,
|
|
214
|
+
customerDid: invoice.customer?.did,
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
};
|
|
218
|
+
|
|
219
|
+
const handleRefundPaid = async (refundId: string) => {
|
|
220
|
+
const refund = (await Refund.findByPk(refundId, {
|
|
221
|
+
include: [
|
|
222
|
+
{ model: Customer, as: 'customer', attributes: ['did'] },
|
|
223
|
+
{
|
|
224
|
+
model: PaymentMethod,
|
|
225
|
+
as: 'paymentMethod',
|
|
226
|
+
attributes: ['type', 'settings'],
|
|
227
|
+
},
|
|
228
|
+
{
|
|
229
|
+
model: PaymentIntent,
|
|
230
|
+
as: 'paymentIntent',
|
|
231
|
+
},
|
|
232
|
+
{
|
|
233
|
+
model: PaymentCurrency,
|
|
234
|
+
as: 'paymentCurrency',
|
|
235
|
+
attributes: ['symbol', 'decimal'],
|
|
236
|
+
},
|
|
237
|
+
],
|
|
238
|
+
})) as TRefundExpanded | null;
|
|
239
|
+
if (!refund) {
|
|
240
|
+
logger.info('Upload refund skipped because refund not found:', { refundId });
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
if (refund.metadata?.did_space_uploaded) {
|
|
245
|
+
logger.info('Upload refund skipped because refund already uploaded:', { refundId });
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
if (!CHARGE_SUPPORTED_CHAIN_TYPES.includes(refund.paymentMethod?.type)) {
|
|
250
|
+
logger.info('Upload refund skipped because payment method is not supported:', {
|
|
251
|
+
refundId,
|
|
252
|
+
paymentMethod: refund.paymentMethod?.type,
|
|
253
|
+
});
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
// @ts-ignore
|
|
257
|
+
const txHash = refund?.payment_details?.[refund.paymentMethod?.type]?.tx_hash;
|
|
258
|
+
if (!txHash) {
|
|
259
|
+
logger.info('Upload refund skipped because tx hash not found:', { refundId });
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
let spaceDid = null;
|
|
264
|
+
let endpoint = null;
|
|
265
|
+
try {
|
|
266
|
+
const result = await getEndpointAndSpaceDid(refund.customer?.did);
|
|
267
|
+
spaceDid = result.spaceDid;
|
|
268
|
+
endpoint = result.endpoint;
|
|
269
|
+
} catch (error) {
|
|
270
|
+
logger.info('Customer space endpoint not available:', {
|
|
271
|
+
refundId,
|
|
272
|
+
did: refund.customer?.did,
|
|
273
|
+
error: (error as Error).message,
|
|
274
|
+
});
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
let invoice = null;
|
|
279
|
+
if (refund.invoice_id) {
|
|
280
|
+
invoice = await Invoice.findByPk(refund.invoice_id, {
|
|
281
|
+
include: [{ model: Customer, as: 'customer', attributes: ['did'] }],
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
const methodInfo = (refund.paymentMethod?.settings as PaymentMethodSettings)?.[
|
|
285
|
+
refund.paymentMethod?.type as 'arcblock' | EVMChainType
|
|
286
|
+
];
|
|
287
|
+
const billInfo: BillingInfo = {
|
|
288
|
+
tx_hash: txHash,
|
|
289
|
+
timestamp: dayjs(refund.updated_at).unix(),
|
|
290
|
+
invoice_id: refund.id,
|
|
291
|
+
category: 'refund',
|
|
292
|
+
description: refund.description || 'Refund',
|
|
293
|
+
amount: refund.amount,
|
|
294
|
+
currency: {
|
|
295
|
+
symbol: refund.paymentCurrency?.symbol,
|
|
296
|
+
decimal: refund.paymentCurrency?.decimal || 18,
|
|
297
|
+
type: refund.paymentMethod?.type || '',
|
|
298
|
+
chain_id: methodInfo?.chain_id || '',
|
|
299
|
+
explorer_host: methodInfo?.explorer_host || '',
|
|
300
|
+
explorer_tx_url: methodInfo?.explorer_host
|
|
301
|
+
? getExplorerTxUrl({
|
|
302
|
+
explorerHost: methodInfo?.explorer_host,
|
|
303
|
+
txHash,
|
|
304
|
+
type: refund.paymentMethod?.type || '',
|
|
305
|
+
})
|
|
306
|
+
: '',
|
|
307
|
+
},
|
|
308
|
+
customer_did: refund.customer?.did,
|
|
309
|
+
link: getUrl(`customer/invoice/${refund.invoice_id}`),
|
|
310
|
+
app_pid: env.appPid,
|
|
311
|
+
};
|
|
312
|
+
if (invoice) {
|
|
313
|
+
billInfo.related = {
|
|
314
|
+
type: 'invoice',
|
|
315
|
+
id: invoice.id,
|
|
316
|
+
name: invoice.description || invoice.id,
|
|
317
|
+
link: getUrl(`customer/invoice/${invoice.id}`),
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
const result = await uploadBillingInfo(refund.customer?.did, billInfo, endpoint);
|
|
322
|
+
if (result) {
|
|
323
|
+
// @ts-ignore
|
|
324
|
+
await refund.update({
|
|
325
|
+
metadata: {
|
|
326
|
+
...refund.metadata,
|
|
327
|
+
did_space_uploaded: true,
|
|
328
|
+
did_space: spaceDid,
|
|
329
|
+
},
|
|
330
|
+
});
|
|
331
|
+
logger.info('Successfully uploaded paid refund:', {
|
|
332
|
+
refundId: refund.id,
|
|
333
|
+
txHash,
|
|
334
|
+
customerDid: refund.customer?.did,
|
|
335
|
+
});
|
|
336
|
+
} else {
|
|
337
|
+
logger.error('Failed to upload paid refund:', { refundId: refund.id, txHash, customerDid: refund.customer?.did });
|
|
338
|
+
}
|
|
339
|
+
};
|
|
340
|
+
|
|
341
|
+
/**
|
|
342
|
+
* create batch tasks for invoice or refund
|
|
343
|
+
* @param type 'invoice' | 'refund'
|
|
344
|
+
* @param customerId customer id
|
|
345
|
+
* @param records records list
|
|
346
|
+
* @param now current timestamp
|
|
347
|
+
* @returns number of new tasks created
|
|
348
|
+
*/
|
|
349
|
+
const createBatchTasks = (
|
|
350
|
+
type: 'invoice' | 'refund',
|
|
351
|
+
customerId: string,
|
|
352
|
+
records: { id: string; created_at?: Date }[],
|
|
353
|
+
now: number
|
|
354
|
+
): { tasksCreated: number; totalRecords: number } => {
|
|
355
|
+
if (records.length === 0) {
|
|
356
|
+
return { tasksCreated: 0, totalRecords: 0 };
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
const recordIds = records.map((record) => record.id);
|
|
360
|
+
const recordLength = recordIds.length;
|
|
361
|
+
const batchSize = 30;
|
|
362
|
+
const chunks = Math.ceil(recordLength / batchSize);
|
|
363
|
+
let tasksCreated = 0;
|
|
364
|
+
|
|
365
|
+
for (let i = 0; i < chunks; i++) {
|
|
366
|
+
const start = i * batchSize;
|
|
367
|
+
const end = Math.min(start + batchSize, recordLength);
|
|
368
|
+
const chunkRecordIds = recordIds.slice(start, end);
|
|
369
|
+
const processTime = new Date(records?.[start]?.created_at || now).getTime();
|
|
370
|
+
const chunkId = `space-${customerId}-${type}-chunk-${i}-${processTime}`;
|
|
371
|
+
|
|
372
|
+
spaceQueue.push({
|
|
373
|
+
id: chunkId,
|
|
374
|
+
job: {
|
|
375
|
+
type: type === 'invoice' ? 'customerInvoiceChunk' : 'customerRefundChunk',
|
|
376
|
+
data: {
|
|
377
|
+
customerId,
|
|
378
|
+
chunkIndex: i,
|
|
379
|
+
totalChunks: chunks,
|
|
380
|
+
ids: chunkRecordIds,
|
|
381
|
+
processTime,
|
|
382
|
+
},
|
|
383
|
+
},
|
|
384
|
+
delay: 60 * (i + 1), // add 1 minute delay for each chunk
|
|
385
|
+
});
|
|
386
|
+
|
|
387
|
+
tasksCreated++;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
logger.info(`Created ${type} chunk tasks`, {
|
|
391
|
+
customerId,
|
|
392
|
+
chunks,
|
|
393
|
+
totalRecords: recordLength,
|
|
394
|
+
});
|
|
395
|
+
|
|
396
|
+
return {
|
|
397
|
+
tasksCreated,
|
|
398
|
+
totalRecords: recordLength,
|
|
399
|
+
};
|
|
400
|
+
};
|
|
401
|
+
|
|
402
|
+
const syncCustomerBillingToSpace = async (customerId: string) => {
|
|
403
|
+
try {
|
|
404
|
+
const now = Date.now();
|
|
405
|
+
const customer = await Customer.findByPkOrDid(customerId);
|
|
406
|
+
if (!customer) {
|
|
407
|
+
logger.info('Customer not found:', { customerId });
|
|
408
|
+
return;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
try {
|
|
412
|
+
await getEndpointAndSpaceDid(customer.did);
|
|
413
|
+
} catch (error) {
|
|
414
|
+
logger.info('Customer space endpoint not available:', {
|
|
415
|
+
customerId,
|
|
416
|
+
did: customer.did,
|
|
417
|
+
error: (error as Error).message,
|
|
418
|
+
});
|
|
419
|
+
return;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
const validMethods = await PaymentMethod.findAll({
|
|
423
|
+
where: {
|
|
424
|
+
type: { [Op.in]: CHARGE_SUPPORTED_CHAIN_TYPES },
|
|
425
|
+
},
|
|
426
|
+
attributes: ['id'],
|
|
427
|
+
});
|
|
428
|
+
|
|
429
|
+
if (!validMethods.length) {
|
|
430
|
+
logger.info('No valid payment methods found', { customerId });
|
|
431
|
+
return;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
const validMethodIds = validMethods.map((method) => method.id);
|
|
435
|
+
|
|
436
|
+
let invoiceProcessTime = 0;
|
|
437
|
+
let refundProcessTime = 0;
|
|
438
|
+
try {
|
|
439
|
+
// get the latest process time from the queue
|
|
440
|
+
const [invoiceProcessQueue, refundProcessQueue] = await Promise.all([
|
|
441
|
+
Job.findOne({
|
|
442
|
+
where: {
|
|
443
|
+
queue: 'did-space',
|
|
444
|
+
id: { [Op.like]: `space-${customerId}-invoice-chunk-%` },
|
|
445
|
+
cancelled: false,
|
|
446
|
+
},
|
|
447
|
+
order: [['created_at', 'DESC']],
|
|
448
|
+
}),
|
|
449
|
+
Job.findOne({
|
|
450
|
+
where: {
|
|
451
|
+
queue: 'did-space',
|
|
452
|
+
id: { [Op.like]: `space-${customerId}-refund-chunk-%` },
|
|
453
|
+
cancelled: false,
|
|
454
|
+
},
|
|
455
|
+
order: [['created_at', 'DESC']],
|
|
456
|
+
}),
|
|
457
|
+
]);
|
|
458
|
+
|
|
459
|
+
invoiceProcessTime = invoiceProcessQueue?.job?.data?.processTime ?? 0;
|
|
460
|
+
refundProcessTime = refundProcessQueue?.job?.data?.processTime ?? 0;
|
|
461
|
+
logger.info('Invoice and refund process time', {
|
|
462
|
+
customerId,
|
|
463
|
+
invoiceProcessTime,
|
|
464
|
+
refundProcessTime,
|
|
465
|
+
});
|
|
466
|
+
} catch (error) {
|
|
467
|
+
logger.error('Failed to get invoice and refund process time', { customerId, error });
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
let [invoices, refunds] = await Promise.all([
|
|
471
|
+
Invoice.findAll({
|
|
472
|
+
where: {
|
|
473
|
+
status: 'paid',
|
|
474
|
+
'metadata.did_space_uploaded': { [Op.not]: true },
|
|
475
|
+
customer_id: customer.id,
|
|
476
|
+
default_payment_method_id: { [Op.in]: validMethodIds },
|
|
477
|
+
// only process invoices after the latest process time
|
|
478
|
+
created_at: { [Op.gte]: invoiceProcessTime },
|
|
479
|
+
},
|
|
480
|
+
attributes: ['id', 'metadata', 'payment_intent_id', 'created_at'],
|
|
481
|
+
order: [['created_at', 'DESC']],
|
|
482
|
+
}),
|
|
483
|
+
Refund.findAll({
|
|
484
|
+
where: {
|
|
485
|
+
status: 'succeeded',
|
|
486
|
+
'metadata.did_space_uploaded': { [Op.not]: true },
|
|
487
|
+
customer_id: customer.id,
|
|
488
|
+
payment_method_id: { [Op.in]: validMethodIds },
|
|
489
|
+
[Op.and]: [literal('payment_details IS NOT NULL')],
|
|
490
|
+
// only process refunds after the latest process time
|
|
491
|
+
created_at: { [Op.gte]: refundProcessTime },
|
|
492
|
+
},
|
|
493
|
+
attributes: ['id', 'metadata', 'payment_details', 'created_at'],
|
|
494
|
+
order: [['created_at', 'DESC']],
|
|
495
|
+
}),
|
|
496
|
+
]);
|
|
497
|
+
|
|
498
|
+
invoices = invoices.filter((x) => x.metadata?.payment_details || x.payment_intent_id);
|
|
499
|
+
refunds = refunds.filter((x) => x.payment_details);
|
|
500
|
+
|
|
501
|
+
const invoicesLength = invoices.length;
|
|
502
|
+
const refundsLength = refunds.length;
|
|
503
|
+
logger.info('Found records to upload:', {
|
|
504
|
+
customerId,
|
|
505
|
+
did: customer.did,
|
|
506
|
+
invoiceCount: invoicesLength,
|
|
507
|
+
refundCount: refundsLength,
|
|
508
|
+
});
|
|
509
|
+
|
|
510
|
+
if (invoicesLength === 0 && refundsLength === 0) {
|
|
511
|
+
logger.info('No records to process', { customerId });
|
|
512
|
+
return;
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
const invoiceChunks = createBatchTasks('invoice', customerId, invoices, now);
|
|
516
|
+
|
|
517
|
+
const refundChunks = createBatchTasks('refund', customerId, refunds, now);
|
|
518
|
+
|
|
519
|
+
logger.info('Completed creating all chunk tasks', {
|
|
520
|
+
customerId,
|
|
521
|
+
invoiceChunks: invoiceChunks.tasksCreated,
|
|
522
|
+
refundChunks: refundChunks.tasksCreated,
|
|
523
|
+
totalRecords: invoicesLength + refundsLength,
|
|
524
|
+
});
|
|
525
|
+
} catch (error) {
|
|
526
|
+
logger.error('Failed to create chunk tasks:', {
|
|
527
|
+
customerId,
|
|
528
|
+
error: error instanceof Error ? error.message : String(error),
|
|
529
|
+
});
|
|
530
|
+
}
|
|
531
|
+
};
|
|
532
|
+
|
|
533
|
+
const handleCustomerInvoiceChunk = async (data: SpaceUploadData['customerInvoiceChunk']) => {
|
|
534
|
+
const { customerId, chunkIndex, totalChunks, ids: invoiceIds } = data;
|
|
535
|
+
|
|
536
|
+
logger.info('Processing invoice chunk', {
|
|
537
|
+
customerId,
|
|
538
|
+
chunkIndex: chunkIndex + 1,
|
|
539
|
+
totalChunks,
|
|
540
|
+
invoiceCount: invoiceIds.length,
|
|
541
|
+
});
|
|
542
|
+
|
|
543
|
+
try {
|
|
544
|
+
await Promise.all(
|
|
545
|
+
invoiceIds.map((invoiceId) =>
|
|
546
|
+
handleInvoicePaid(invoiceId).catch((error) => {
|
|
547
|
+
logger.error('Failed to process invoice:', {
|
|
548
|
+
customerId,
|
|
549
|
+
invoiceId,
|
|
550
|
+
error: error.message,
|
|
551
|
+
});
|
|
552
|
+
})
|
|
553
|
+
)
|
|
554
|
+
);
|
|
555
|
+
|
|
556
|
+
logger.info('Completed invoice chunk processing', {
|
|
557
|
+
customerId,
|
|
558
|
+
chunkIndex: chunkIndex + 1,
|
|
559
|
+
totalChunks,
|
|
560
|
+
processedInvoices: invoiceIds.length,
|
|
561
|
+
});
|
|
562
|
+
} catch (error) {
|
|
563
|
+
logger.error('Invoice chunk processing failed:', {
|
|
564
|
+
customerId,
|
|
565
|
+
chunkIndex,
|
|
566
|
+
error: error instanceof Error ? error.message : String(error),
|
|
567
|
+
});
|
|
568
|
+
}
|
|
569
|
+
};
|
|
570
|
+
|
|
571
|
+
const handleCustomerRefundChunk = async (data: SpaceUploadData['customerRefundChunk']) => {
|
|
572
|
+
const { customerId, chunkIndex, totalChunks, ids: refundIds } = data;
|
|
573
|
+
|
|
574
|
+
logger.info('Processing refund chunk', {
|
|
575
|
+
customerId,
|
|
576
|
+
chunkIndex: chunkIndex + 1,
|
|
577
|
+
totalChunks,
|
|
578
|
+
refundCount: refundIds.length,
|
|
579
|
+
});
|
|
580
|
+
|
|
581
|
+
try {
|
|
582
|
+
await Promise.all(
|
|
583
|
+
refundIds.map((refundId) =>
|
|
584
|
+
handleRefundPaid(refundId).catch((error) => {
|
|
585
|
+
logger.error('Failed to process refund:', {
|
|
586
|
+
customerId,
|
|
587
|
+
refundId,
|
|
588
|
+
error: error.message,
|
|
589
|
+
});
|
|
590
|
+
})
|
|
591
|
+
)
|
|
592
|
+
);
|
|
593
|
+
|
|
594
|
+
logger.info('Completed refund chunk processing', {
|
|
595
|
+
customerId,
|
|
596
|
+
chunkIndex: chunkIndex + 1,
|
|
597
|
+
totalChunks,
|
|
598
|
+
processedRefunds: refundIds.length,
|
|
599
|
+
});
|
|
600
|
+
} catch (error) {
|
|
601
|
+
logger.error('Refund chunk processing failed:', {
|
|
602
|
+
customerId,
|
|
603
|
+
chunkIndex,
|
|
604
|
+
error: error instanceof Error ? error.message : String(error),
|
|
605
|
+
});
|
|
606
|
+
}
|
|
607
|
+
};
|
|
608
|
+
|
|
609
|
+
const handlers = {
|
|
610
|
+
invoice: (data: SpaceUploadData['invoice']) => handleInvoicePaid(data.id),
|
|
611
|
+
refund: (data: SpaceUploadData['refund']) => handleRefundPaid(data.id),
|
|
612
|
+
customer: (data: SpaceUploadData['customer']) => syncCustomerBillingToSpace(data.id),
|
|
613
|
+
customerInvoiceChunk: (data: SpaceUploadData['customerInvoiceChunk']) => handleCustomerInvoiceChunk(data),
|
|
614
|
+
customerRefundChunk: (data: SpaceUploadData['customerRefundChunk']) => handleCustomerRefundChunk(data),
|
|
615
|
+
};
|
|
616
|
+
|
|
617
|
+
export const handleSpaceUpload = async (job: SpaceUploadJob) => {
|
|
618
|
+
logger.info('Starting to handle space upload', job);
|
|
619
|
+
const handler = handlers[job.type];
|
|
620
|
+
if (!handler) {
|
|
621
|
+
logger.error('No handler found for job type', { job });
|
|
622
|
+
return;
|
|
623
|
+
}
|
|
624
|
+
// @ts-ignore
|
|
625
|
+
await handler(job.data);
|
|
626
|
+
};
|
|
627
|
+
|
|
628
|
+
export const spaceQueue = createQueue<SpaceUploadJob>({
|
|
629
|
+
name: 'did-space',
|
|
630
|
+
onJob: handleSpaceUpload,
|
|
631
|
+
options: {
|
|
632
|
+
concurrency: 5,
|
|
633
|
+
maxRetries: 3,
|
|
634
|
+
enableScheduledJob: true,
|
|
635
|
+
},
|
|
636
|
+
});
|
|
637
|
+
|
|
638
|
+
spaceQueue.on('failed', ({ id, job, error }) => {
|
|
639
|
+
logger.error('Space upload job failed', { id, job, error });
|
|
640
|
+
});
|
|
641
|
+
|
|
642
|
+
export const startUploadBillingInfoListener = () => {
|
|
643
|
+
events.on('invoice.paid', (invoice) => {
|
|
644
|
+
spaceQueue.push({
|
|
645
|
+
id: `space-${invoice.id}`,
|
|
646
|
+
job: {
|
|
647
|
+
type: 'invoice',
|
|
648
|
+
data: { id: invoice.id },
|
|
649
|
+
},
|
|
650
|
+
});
|
|
651
|
+
});
|
|
652
|
+
|
|
653
|
+
events.on('refund.succeeded', (refund) => {
|
|
654
|
+
spaceQueue.push({
|
|
655
|
+
id: `space-${refund.id}`,
|
|
656
|
+
job: { type: 'refund', data: { id: refund.id } },
|
|
657
|
+
});
|
|
658
|
+
});
|
|
659
|
+
|
|
660
|
+
logger.info('Space upload listeners started');
|
|
661
|
+
};
|
|
@@ -133,7 +133,7 @@ const doHandleSubscriptionInvoice = async ({
|
|
|
133
133
|
const usageReportStart = usageStart || start - offset;
|
|
134
134
|
const usageReportEnd = usageEnd || end - offset;
|
|
135
135
|
|
|
136
|
-
if (subscription.status !== 'trialing') {
|
|
136
|
+
if (subscription.status !== 'trialing' && reason !== 'recover') {
|
|
137
137
|
// check if usage report is empty
|
|
138
138
|
const usageReportEmpty = await checkUsageReportEmpty(subscription, usageReportStart, usageReportEnd);
|
|
139
139
|
if (usageReportEmpty) {
|