payment-kit 1.18.46 → 1.18.48
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/index.ts +2 -0
- package/api/src/libs/pagination.ts +406 -0
- package/api/src/libs/subscription.ts +1 -1
- package/api/src/routes/connect/re-stake.ts +116 -0
- package/api/src/routes/connect/setup.ts +5 -0
- package/api/src/routes/connect/shared.ts +79 -0
- package/api/src/routes/invoices.ts +295 -103
- package/api/src/routes/subscriptions.ts +90 -2
- package/api/tests/libs/pagination.spec.ts +576 -0
- package/blocklet.yml +1 -1
- package/package.json +10 -10
- package/scripts/sdk.js +7 -0
- package/src/components/subscription/portal/actions.tsx +39 -57
- package/src/components/subscription/portal/list.tsx +36 -26
- package/src/libs/util.ts +14 -0
- package/src/pages/admin/billing/subscriptions/detail.tsx +2 -10
- package/src/pages/customer/invoice/past-due.tsx +1 -0
|
@@ -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
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
.
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
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
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
where.
|
|
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
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
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
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
{
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
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
|
-
//
|
|
152
|
-
let
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
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
|
-
|
|
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(
|
|
179
|
-
|
|
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
|
|
|
@@ -17,6 +17,7 @@ import { isDelegationSufficientForPayment } from '../libs/payment';
|
|
|
17
17
|
import { authenticate } from '../libs/security';
|
|
18
18
|
import { expandLineItems, getFastCheckoutAmount, getSubscriptionCreateSetup, isLineItemAligned } from '../libs/session';
|
|
19
19
|
import {
|
|
20
|
+
checkRemainingStake,
|
|
20
21
|
createProration,
|
|
21
22
|
finalizeSubscriptionUpdate,
|
|
22
23
|
getPastInvoicesAmount,
|
|
@@ -33,6 +34,7 @@ import { invoiceQueue } from '../queues/invoice';
|
|
|
33
34
|
import {
|
|
34
35
|
addSubscriptionJob,
|
|
35
36
|
returnOverdraftProtectionQueue,
|
|
37
|
+
returnStakeQueue,
|
|
36
38
|
slashOverdraftProtectionQueue,
|
|
37
39
|
slashStakeQueue,
|
|
38
40
|
subscriptionQueue,
|
|
@@ -426,6 +428,42 @@ router.put('/:id/cancel', authPortal, async (req, res) => {
|
|
|
426
428
|
return res.json(subscription);
|
|
427
429
|
});
|
|
428
430
|
|
|
431
|
+
router.get('/:id/recover-info', authPortal, async (req, res) => {
|
|
432
|
+
const doc = await Subscription.findByPk(req.params.id);
|
|
433
|
+
|
|
434
|
+
if (!doc) {
|
|
435
|
+
return res.status(404).json({ error: 'Subscription not found' });
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
const paymentMethod = await PaymentMethod.findByPk(doc.default_payment_method_id);
|
|
439
|
+
const paymentCurrency = await PaymentCurrency.findByPk(doc.currency_id);
|
|
440
|
+
|
|
441
|
+
let needStake = false;
|
|
442
|
+
let revokedStake = '0';
|
|
443
|
+
|
|
444
|
+
if (paymentMethod?.type === 'arcblock' && paymentCurrency) {
|
|
445
|
+
const address = doc.payment_details?.arcblock?.staking?.address;
|
|
446
|
+
if (address) {
|
|
447
|
+
try {
|
|
448
|
+
const { revoked } = await checkRemainingStake(paymentMethod, paymentCurrency, address, '0');
|
|
449
|
+
const cancelReason = doc.cancelation_details?.reason;
|
|
450
|
+
if (revoked && revoked !== '0' && cancelReason === 'stake_revoked') {
|
|
451
|
+
needStake = true;
|
|
452
|
+
revokedStake = revoked;
|
|
453
|
+
}
|
|
454
|
+
} catch (err) {
|
|
455
|
+
console.error(`Failed to check remaining stake for subscription ${doc.id}`, { error: err });
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
return res.json({
|
|
461
|
+
subscription: doc,
|
|
462
|
+
needStake,
|
|
463
|
+
revokedStake,
|
|
464
|
+
});
|
|
465
|
+
});
|
|
466
|
+
|
|
429
467
|
router.put('/:id/recover', authPortal, async (req, res) => {
|
|
430
468
|
const doc = await Subscription.findByPk(req.params.id);
|
|
431
469
|
|
|
@@ -442,13 +480,43 @@ router.put('/:id/recover', authPortal, async (req, res) => {
|
|
|
442
480
|
return res.status(400).json({ error: 'Subscription not recoverable from cancellation' });
|
|
443
481
|
}
|
|
444
482
|
|
|
483
|
+
const paymentMethod = await PaymentMethod.findByPk(doc.default_payment_method_id);
|
|
484
|
+
if (!paymentMethod) {
|
|
485
|
+
return res.status(400).json({ error: 'Payment method not found' });
|
|
486
|
+
}
|
|
487
|
+
const paymentCurrency = await PaymentCurrency.findByPk(doc.currency_id);
|
|
488
|
+
if (!paymentCurrency) {
|
|
489
|
+
return res.status(400).json({ error: 'Payment currency not found' });
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
// check if need stake
|
|
493
|
+
if (paymentMethod.type === 'arcblock') {
|
|
494
|
+
const address = doc.payment_details?.arcblock?.staking?.address;
|
|
495
|
+
if (address) {
|
|
496
|
+
try {
|
|
497
|
+
const { revoked } = await checkRemainingStake(paymentMethod, paymentCurrency, address, '0');
|
|
498
|
+
const cancelReason = doc.cancelation_details?.reason;
|
|
499
|
+
if (revoked && revoked !== '0' && cancelReason === 'stake_revoked') {
|
|
500
|
+
return res.json({
|
|
501
|
+
needStake: true,
|
|
502
|
+
subscription: doc,
|
|
503
|
+
revoked,
|
|
504
|
+
});
|
|
505
|
+
}
|
|
506
|
+
} catch (err) {
|
|
507
|
+
logger.error('subscription recover failed to check remaining stake', { error: err });
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
|
|
445
512
|
if (doc.cancel_at_period_end) {
|
|
446
513
|
await updateStripeSubscription(doc, { cancel_at_period_end: false });
|
|
447
514
|
} else {
|
|
448
515
|
await updateStripeSubscription(doc, { cancel_at: null });
|
|
449
516
|
}
|
|
450
517
|
|
|
451
|
-
|
|
518
|
+
// @ts-ignore
|
|
519
|
+
await doc.update({ cancel_at_period_end: false, cancel_at: 0, canceled_at: 0, cancelation_details: null });
|
|
452
520
|
await new SubscriptionWillCanceledSchedule().deleteScheduleSubscriptionJobs([doc]);
|
|
453
521
|
// reschedule jobs
|
|
454
522
|
subscriptionQueue
|
|
@@ -457,7 +525,7 @@ router.put('/:id/recover', authPortal, async (req, res) => {
|
|
|
457
525
|
.catch((err) => logger.error('subscription cancel job failed to cancel', { error: err }));
|
|
458
526
|
await addSubscriptionJob(doc, 'cycle');
|
|
459
527
|
|
|
460
|
-
return res.json(doc);
|
|
528
|
+
return res.json({ subscription: doc });
|
|
461
529
|
});
|
|
462
530
|
|
|
463
531
|
router.put('/:id/pause', auth, async (req, res) => {
|
|
@@ -520,6 +588,26 @@ router.put('/:id/resume', auth, async (req, res) => {
|
|
|
520
588
|
return res.json(doc);
|
|
521
589
|
});
|
|
522
590
|
|
|
591
|
+
router.put('/:id/return-stake', authPortal, async (req, res) => {
|
|
592
|
+
const doc = await Subscription.findByPk(req.params.id);
|
|
593
|
+
if (!doc) {
|
|
594
|
+
return res.status(404).json({ error: 'Subscription not found' });
|
|
595
|
+
}
|
|
596
|
+
if (doc.status !== 'canceled') {
|
|
597
|
+
return res.status(400).json({ error: 'Subscription is not canceled' });
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
if (!doc.payment_details?.arcblock?.staking?.tx_hash) {
|
|
601
|
+
return res.status(400).json({ error: 'No staking transaction found in subscription' });
|
|
602
|
+
}
|
|
603
|
+
returnStakeQueue.push({ id: `return-stake-${doc.id}`, job: { subscriptionId: doc.id } });
|
|
604
|
+
logger.info('Subscription return stake job scheduled', {
|
|
605
|
+
jobId: `return-stake-${doc.id}`,
|
|
606
|
+
subscription: doc.id,
|
|
607
|
+
});
|
|
608
|
+
return res.json({ success: true, subscriptionId: doc.id });
|
|
609
|
+
});
|
|
610
|
+
|
|
523
611
|
const isValidSubscriptionItemChange = (item: SubscriptionUpdateItem) => {
|
|
524
612
|
if (item.deleted) {
|
|
525
613
|
if (!item.id) {
|