payment-kit 1.18.54 → 1.18.55
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/libs/queue/index.ts +4 -1
- package/api/src/queues/payment.ts +21 -3
- package/api/src/queues/subscription.ts +246 -111
- package/api/src/routes/connect/change-payment.ts +6 -5
- package/api/src/routes/connect/shared.ts +6 -2
- package/api/src/routes/connect/subscribe.ts +6 -4
- package/blocklet.yml +1 -1
- package/package.json +17 -17
|
@@ -8,6 +8,7 @@ import { nanoid } from 'nanoid';
|
|
|
8
8
|
import { AsyncLocalStorage } from 'async_hooks';
|
|
9
9
|
import logger from '../logger';
|
|
10
10
|
import { sleep, tryWithTimeout } from '../util';
|
|
11
|
+
import dayjs from '../dayjs';
|
|
11
12
|
import createQueueStore from './store';
|
|
12
13
|
import { Job } from '../../store/models/job';
|
|
13
14
|
import { sequelize } from '../../store/sequelize';
|
|
@@ -112,6 +113,7 @@ export default function createQueue<T = any>({ name, onJob, options = defaults }
|
|
|
112
113
|
queueEvents.emit(e, data);
|
|
113
114
|
jobEvents.emit(e, data);
|
|
114
115
|
};
|
|
116
|
+
const now = dayjs().unix();
|
|
115
117
|
|
|
116
118
|
if (!job) {
|
|
117
119
|
throw new Error('Can not queue empty job');
|
|
@@ -134,7 +136,8 @@ export default function createQueue<T = any>({ name, onJob, options = defaults }
|
|
|
134
136
|
attrs.delay = delay;
|
|
135
137
|
attrs.will_run_at = Date.now() + delay * 1000;
|
|
136
138
|
}
|
|
137
|
-
if (runAt) {
|
|
139
|
+
if (runAt && runAt > now) {
|
|
140
|
+
// 如果 runAt 大于当前时间,则延迟执行,否则直接执行
|
|
138
141
|
attrs.delay = 1;
|
|
139
142
|
attrs.will_run_at = runAt * 1000;
|
|
140
143
|
}
|
|
@@ -42,6 +42,7 @@ import createQueue from '../libs/queue';
|
|
|
42
42
|
import { CHARGE_SUPPORTED_CHAIN_TYPES, EVM_CHAIN_TYPES } from '../libs/constants';
|
|
43
43
|
import { getCheckoutSessionSubscriptionIds, getSubscriptionCreateSetup } from '../libs/session';
|
|
44
44
|
import { syncStripeSubscriptionAfterRecovery } from '../integrations/stripe/handlers/subscription';
|
|
45
|
+
import { getLock } from '../libs/lock';
|
|
45
46
|
|
|
46
47
|
type PaymentJob = {
|
|
47
48
|
paymentIntentId: string;
|
|
@@ -798,11 +799,26 @@ export const handlePayment = async (job: PaymentJob) => {
|
|
|
798
799
|
// try payment capture and reschedule on error
|
|
799
800
|
logger.info('PaymentIntent capture attempt', { id: paymentIntent.id, attempt: invoice?.attempt_count });
|
|
800
801
|
let result;
|
|
802
|
+
|
|
803
|
+
// Use lock to prevent race condition with subscription queue
|
|
804
|
+
const lock = getLock(`payment-${paymentIntent.id}`);
|
|
805
|
+
|
|
801
806
|
try {
|
|
802
|
-
await
|
|
803
|
-
logger.
|
|
804
|
-
|
|
807
|
+
await lock.acquire();
|
|
808
|
+
logger.debug('Acquired lock for payment processing', {
|
|
809
|
+
paymentIntent: paymentIntent.id,
|
|
810
|
+
invoice: invoice?.id,
|
|
805
811
|
});
|
|
812
|
+
|
|
813
|
+
// Re-check payment intent status after acquiring lock
|
|
814
|
+
await paymentIntent.reload();
|
|
815
|
+
if (paymentIntent.status === 'succeeded') {
|
|
816
|
+
logger.info('PaymentIntent already succeeded, skipping', { id: paymentIntent.id });
|
|
817
|
+
return;
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
await paymentIntent.update({ status: 'processing', last_payment_error: null });
|
|
821
|
+
|
|
806
822
|
if (paymentMethod.type === 'arcblock') {
|
|
807
823
|
if (invoice?.billing_reason === 'slash_stake') {
|
|
808
824
|
await handleStakeSlash(invoice, paymentIntent, paymentMethod, customer, paymentCurrency);
|
|
@@ -984,6 +1000,8 @@ export const handlePayment = async (job: PaymentJob) => {
|
|
|
984
1000
|
paymentQueue.delete(paymentIntent.id);
|
|
985
1001
|
}
|
|
986
1002
|
}
|
|
1003
|
+
} finally {
|
|
1004
|
+
await lock.release();
|
|
987
1005
|
}
|
|
988
1006
|
};
|
|
989
1007
|
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { LiteralUnion } from 'type-fest';
|
|
2
2
|
|
|
3
|
+
import { Op } from 'sequelize';
|
|
3
4
|
import { createEvent } from '../libs/audit';
|
|
4
5
|
import { ensurePassportRevoked } from '../integrations/blocklet/passport';
|
|
5
6
|
import { batchHandleStripeSubscriptions } from '../integrations/stripe/resource';
|
|
@@ -8,7 +9,7 @@ import dayjs from '../libs/dayjs';
|
|
|
8
9
|
import { events } from '../libs/event';
|
|
9
10
|
import { getLock } from '../libs/lock';
|
|
10
11
|
import logger from '../libs/logger';
|
|
11
|
-
import { getGasPayerExtra } from '../libs/payment';
|
|
12
|
+
import { getGasPayerExtra, isDelegationSufficientForPayment } from '../libs/payment';
|
|
12
13
|
import createQueue from '../libs/queue';
|
|
13
14
|
import { getStatementDescriptor } from '../libs/session';
|
|
14
15
|
import {
|
|
@@ -240,9 +241,17 @@ const handleSubscriptionBeforeCancel = async (subscription: Subscription) => {
|
|
|
240
241
|
});
|
|
241
242
|
|
|
242
243
|
if (invoice) {
|
|
243
|
-
// schedule invoice
|
|
244
|
-
|
|
245
|
-
|
|
244
|
+
// Just schedule the invoice processing, don't wait for completion
|
|
245
|
+
// The actual payment will be handled by handleFinalInvoicePayment during stake operations
|
|
246
|
+
await invoiceQueue.pushAndWait({
|
|
247
|
+
id: invoice.id,
|
|
248
|
+
job: { invoiceId: invoice.id, retryOnError: false },
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
logger.info('Final invoice job scheduled for async processing', {
|
|
252
|
+
invoice: invoice.id,
|
|
253
|
+
subscription: subscription.id,
|
|
254
|
+
});
|
|
246
255
|
|
|
247
256
|
// persist invoice id
|
|
248
257
|
await subscription.update({ latest_invoice_id: invoice.id });
|
|
@@ -341,9 +350,94 @@ const handleSubscriptionAfterRecover = async (subscription: Subscription) => {
|
|
|
341
350
|
logger.info(`Subscription job scheduled for next billing cycle after recover: ${subscription.id}`);
|
|
342
351
|
};
|
|
343
352
|
|
|
344
|
-
|
|
353
|
+
/**
|
|
354
|
+
* Handle final metered invoice payment before stake operations
|
|
355
|
+
* Checks user balance and slashes stake if insufficient funds
|
|
356
|
+
*/
|
|
357
|
+
const handleFinalInvoicePayment = async (
|
|
358
|
+
subscription: Subscription,
|
|
359
|
+
paymentMethod: PaymentMethod,
|
|
360
|
+
paymentCurrency: PaymentCurrency
|
|
361
|
+
) => {
|
|
362
|
+
// Check if there's any unpaid final metered invoice
|
|
363
|
+
const lastInvoice = await Invoice.findOne({
|
|
364
|
+
where: {
|
|
365
|
+
subscription_id: subscription.id,
|
|
366
|
+
billing_reason: 'subscription_cancel',
|
|
367
|
+
status: { [Op.in]: ['open', 'uncollectible'] },
|
|
368
|
+
amount_remaining: { [Op.gt]: '0' },
|
|
369
|
+
},
|
|
370
|
+
order: [['created_at', 'DESC']],
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
if (!lastInvoice) {
|
|
374
|
+
logger.info('No unpaid final invoice found, skipping payment handling', {
|
|
375
|
+
subscription: subscription.id,
|
|
376
|
+
});
|
|
377
|
+
return;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
// Check payment status and handle accordingly
|
|
381
|
+
const paymentIntent = await PaymentIntent.findByPk(lastInvoice.payment_intent_id);
|
|
382
|
+
if (!paymentIntent) {
|
|
383
|
+
logger.warn('PaymentIntent not found for final invoice', {
|
|
384
|
+
subscription: subscription.id,
|
|
385
|
+
invoice: lastInvoice.id,
|
|
386
|
+
});
|
|
387
|
+
return;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
// If payment already succeeded, skip processing
|
|
391
|
+
if (paymentIntent.status === 'succeeded') {
|
|
392
|
+
logger.info('Final invoice already paid successfully, skipping', {
|
|
393
|
+
subscription: subscription.id,
|
|
394
|
+
invoice: lastInvoice.id,
|
|
395
|
+
paymentIntentStatus: paymentIntent.status,
|
|
396
|
+
});
|
|
397
|
+
return;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
const customer = await Customer.findByPk(subscription.customer_id);
|
|
401
|
+
if (!customer) {
|
|
402
|
+
logger.warn('Final invoice settlement skipped because customer not found', {
|
|
403
|
+
subscription: subscription.id,
|
|
404
|
+
invoice: lastInvoice.id,
|
|
405
|
+
});
|
|
406
|
+
return;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
logger.info('Found unpaid final invoice, checking user delegation balance', {
|
|
410
|
+
subscription: subscription.id,
|
|
411
|
+
invoice: lastInvoice.id,
|
|
412
|
+
amount: lastInvoice.amount_remaining,
|
|
413
|
+
});
|
|
414
|
+
|
|
415
|
+
const paymentSettings = lastInvoice.payment_settings;
|
|
416
|
+
const payer = paymentSettings?.payment_method_options?.arcblock?.payer as string;
|
|
417
|
+
|
|
418
|
+
const hasSufficientBalance = await isDelegationSufficientForPayment({
|
|
419
|
+
paymentMethod,
|
|
420
|
+
paymentCurrency,
|
|
421
|
+
userDid: payer || customer.did,
|
|
422
|
+
amount: lastInvoice.amount_remaining,
|
|
423
|
+
});
|
|
424
|
+
|
|
425
|
+
if (!hasSufficientBalance.sufficient) {
|
|
426
|
+
logger.info('User has insufficient balance, slashing stake to pay final invoice', {
|
|
427
|
+
subscription: subscription.id,
|
|
428
|
+
invoice: lastInvoice.id,
|
|
429
|
+
userBalance: hasSufficientBalance,
|
|
430
|
+
});
|
|
431
|
+
|
|
432
|
+
await handleStakeSlashAfterCancel(subscription, true);
|
|
433
|
+
|
|
434
|
+
logger.info('Stake slashed for final invoice, proceeding with remaining stake return');
|
|
435
|
+
}
|
|
436
|
+
};
|
|
437
|
+
|
|
438
|
+
export const handleStakeSlashAfterCancel = async (subscription: Subscription, forceSlash: boolean = false) => {
|
|
345
439
|
const invoice = await Invoice.findByPk(subscription.latest_invoice_id);
|
|
346
|
-
if (!invoice || invoice.status !== 'uncollectible') {
|
|
440
|
+
if (!invoice || (invoice.status !== 'uncollectible' && !forceSlash)) {
|
|
347
441
|
logger.warn('Stake slashing aborted because invoice status', {
|
|
348
442
|
subscription: subscription.id,
|
|
349
443
|
invoice: invoice?.id,
|
|
@@ -379,125 +473,155 @@ const handleStakeSlashAfterCancel = async (subscription: Subscription) => {
|
|
|
379
473
|
return;
|
|
380
474
|
}
|
|
381
475
|
const paymentIntent = await PaymentIntent.findByPk(invoice.payment_intent_id);
|
|
382
|
-
if (!paymentIntent
|
|
383
|
-
logger.warn('Stake slashing aborted because payment intent', {
|
|
476
|
+
if (!paymentIntent) {
|
|
477
|
+
logger.warn('Stake slashing aborted because payment intent not found', {
|
|
384
478
|
subscription: subscription.id,
|
|
385
|
-
|
|
386
|
-
status: paymentIntent?.status,
|
|
479
|
+
invoice: invoice.id,
|
|
387
480
|
});
|
|
388
481
|
return;
|
|
389
482
|
}
|
|
390
483
|
|
|
391
|
-
//
|
|
392
|
-
const
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
logger.
|
|
484
|
+
// Use lock to prevent race condition with payment queue
|
|
485
|
+
const lock = getLock(`payment-${paymentIntent.id}`);
|
|
486
|
+
|
|
487
|
+
try {
|
|
488
|
+
await lock.acquire();
|
|
489
|
+
logger.debug('Acquired lock for stake slashing', {
|
|
397
490
|
subscription: subscription.id,
|
|
398
|
-
|
|
491
|
+
paymentIntent: paymentIntent.id,
|
|
399
492
|
});
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
493
|
+
|
|
494
|
+
// Re-check payment intent status after acquiring lock
|
|
495
|
+
await paymentIntent.reload();
|
|
496
|
+
if (paymentIntent.status === 'succeeded') {
|
|
497
|
+
logger.info('Payment already succeeded, skipping stake slashing', {
|
|
498
|
+
subscription: subscription.id,
|
|
499
|
+
paymentIntent: paymentIntent.id,
|
|
500
|
+
status: paymentIntent.status,
|
|
501
|
+
});
|
|
502
|
+
return;
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
// check the staking
|
|
506
|
+
const client = method.getOcapClient();
|
|
507
|
+
const address = await getSubscriptionStakeAddress(subscription, customer.did);
|
|
508
|
+
const { state } = await client.getStakeState({ address });
|
|
509
|
+
if (!state || !state.data?.value) {
|
|
510
|
+
logger.warn('Stake slashing aborted because no staking state', {
|
|
511
|
+
subscription: subscription.id,
|
|
512
|
+
address,
|
|
513
|
+
});
|
|
514
|
+
return;
|
|
515
|
+
}
|
|
516
|
+
// check for staking for this subscription
|
|
517
|
+
const data = JSON.parse(state.data.value || '{}');
|
|
518
|
+
if (!data[subscription.id] && !state.nonce) {
|
|
519
|
+
logger.warn('Stake slashing aborted because no staking for subscription', {
|
|
520
|
+
subscription: subscription.id,
|
|
521
|
+
address,
|
|
522
|
+
data,
|
|
523
|
+
});
|
|
524
|
+
return;
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
// check for staking for amount
|
|
528
|
+
const stakeEnough = await checkRemainingStake(method, currency, address, invoice.amount_remaining);
|
|
529
|
+
if (!stakeEnough.enough) {
|
|
530
|
+
logger.warn('Stake slashing aborted because no enough staking', {
|
|
531
|
+
subscription: subscription.id,
|
|
532
|
+
address,
|
|
533
|
+
staked: stakeEnough.staked,
|
|
534
|
+
revoked: stakeEnough.revoked,
|
|
535
|
+
});
|
|
536
|
+
return;
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
if (invoice.amount_remaining === '0') {
|
|
540
|
+
logger.warn('Stake slashing aborted because amount_remaining is 0', {
|
|
541
|
+
subscription: subscription.id,
|
|
542
|
+
address,
|
|
543
|
+
invoice: invoice.id,
|
|
544
|
+
});
|
|
545
|
+
return;
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
// do the slash
|
|
549
|
+
const signed = await client.signSlashStakeTx({
|
|
550
|
+
tx: {
|
|
551
|
+
itx: {
|
|
552
|
+
address,
|
|
553
|
+
outputs: [
|
|
554
|
+
{ owner: wallet.address, tokens: [{ address: currency.contract, value: invoice.amount_remaining }] },
|
|
555
|
+
],
|
|
556
|
+
message: 'uncollectible_past_due_invoice',
|
|
557
|
+
data: {
|
|
558
|
+
typeUrl: 'json',
|
|
559
|
+
// @ts-ignore
|
|
560
|
+
value: {
|
|
561
|
+
appId: wallet.address,
|
|
562
|
+
reason: 'subscription_cancel',
|
|
563
|
+
subscriptionId: subscription.id,
|
|
564
|
+
invoiceId: invoice.id,
|
|
565
|
+
paymentIntentId: paymentIntent.id,
|
|
566
|
+
},
|
|
567
|
+
},
|
|
568
|
+
},
|
|
569
|
+
},
|
|
570
|
+
wallet,
|
|
571
|
+
});
|
|
572
|
+
// @ts-ignore
|
|
573
|
+
const { buffer } = await client.encodeSlashStakeTx({ tx: signed });
|
|
574
|
+
// @ts-ignore
|
|
575
|
+
const txHash = await client.sendSlashStakeTx({ tx: signed, wallet }, getGasPayerExtra(buffer));
|
|
576
|
+
logger.info('Stake slashing done', {
|
|
406
577
|
subscription: subscription.id,
|
|
578
|
+
amount: invoice.amount_remaining,
|
|
407
579
|
address,
|
|
408
|
-
|
|
580
|
+
txHash,
|
|
581
|
+
invoice: invoice.id,
|
|
409
582
|
});
|
|
410
|
-
return;
|
|
411
|
-
}
|
|
412
583
|
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
584
|
+
await paymentIntent.update({
|
|
585
|
+
status: 'succeeded',
|
|
586
|
+
amount_received: invoice.amount_remaining,
|
|
587
|
+
capture_method: 'manual',
|
|
588
|
+
last_payment_error: null,
|
|
589
|
+
payment_details: {
|
|
590
|
+
arcblock: {
|
|
591
|
+
tx_hash: txHash,
|
|
592
|
+
payer: getSubscriptionPaymentAddress(subscription, 'arcblock'),
|
|
593
|
+
type: 'slash',
|
|
594
|
+
},
|
|
595
|
+
},
|
|
596
|
+
});
|
|
597
|
+
logger.info('PaymentIntent updated after stake slash', {
|
|
417
598
|
subscription: subscription.id,
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
599
|
+
paymentIntent: paymentIntent.id,
|
|
600
|
+
status: 'succeeded',
|
|
601
|
+
});
|
|
602
|
+
await invoice.update({
|
|
603
|
+
paid: true,
|
|
604
|
+
status: 'paid',
|
|
605
|
+
amount_paid: paymentIntent.amount,
|
|
606
|
+
amount_remaining: '0',
|
|
607
|
+
attempt_count: invoice.attempt_count + 1,
|
|
608
|
+
attempted: true,
|
|
609
|
+
status_transitions: { ...invoice.status_transitions, paid_at: dayjs().unix() },
|
|
421
610
|
});
|
|
422
|
-
return;
|
|
423
|
-
}
|
|
424
611
|
|
|
425
|
-
|
|
426
|
-
logger.warn('Stake slashing aborted because amount_remaining is 0', {
|
|
612
|
+
logger.info('Invoice updated after stake slash', {
|
|
427
613
|
subscription: subscription.id,
|
|
428
|
-
address,
|
|
429
614
|
invoice: invoice.id,
|
|
615
|
+
status: 'paid',
|
|
616
|
+
});
|
|
617
|
+
} finally {
|
|
618
|
+
// Always release the lock
|
|
619
|
+
await lock.release();
|
|
620
|
+
logger.debug('Released lock for stake slashing', {
|
|
621
|
+
subscription: subscription.id,
|
|
622
|
+
paymentIntent: paymentIntent.id,
|
|
430
623
|
});
|
|
431
|
-
return;
|
|
432
624
|
}
|
|
433
|
-
|
|
434
|
-
// do the slash
|
|
435
|
-
const signed = await client.signSlashStakeTx({
|
|
436
|
-
tx: {
|
|
437
|
-
itx: {
|
|
438
|
-
address,
|
|
439
|
-
outputs: [{ owner: wallet.address, tokens: [{ address: currency.contract, value: invoice.amount_remaining }] }],
|
|
440
|
-
message: 'uncollectible_past_due_invoice',
|
|
441
|
-
data: {
|
|
442
|
-
typeUrl: 'json',
|
|
443
|
-
// @ts-ignore
|
|
444
|
-
value: {
|
|
445
|
-
appId: wallet.address,
|
|
446
|
-
reason: 'subscription_cancel',
|
|
447
|
-
subscriptionId: subscription.id,
|
|
448
|
-
invoiceId: invoice.id,
|
|
449
|
-
paymentIntentId: paymentIntent.id,
|
|
450
|
-
},
|
|
451
|
-
},
|
|
452
|
-
},
|
|
453
|
-
},
|
|
454
|
-
wallet,
|
|
455
|
-
});
|
|
456
|
-
// @ts-ignore
|
|
457
|
-
const { buffer } = await client.encodeSlashStakeTx({ tx: signed });
|
|
458
|
-
// @ts-ignore
|
|
459
|
-
const txHash = await client.sendSlashStakeTx({ tx: signed, wallet }, getGasPayerExtra(buffer));
|
|
460
|
-
logger.info('Stake slashing done', {
|
|
461
|
-
subscription: subscription.id,
|
|
462
|
-
amount: invoice.amount_remaining,
|
|
463
|
-
address,
|
|
464
|
-
txHash,
|
|
465
|
-
invoice: invoice.id,
|
|
466
|
-
});
|
|
467
|
-
|
|
468
|
-
await paymentIntent.update({
|
|
469
|
-
status: 'succeeded',
|
|
470
|
-
amount_received: invoice.amount_remaining,
|
|
471
|
-
capture_method: 'manual',
|
|
472
|
-
last_payment_error: null,
|
|
473
|
-
payment_details: {
|
|
474
|
-
arcblock: {
|
|
475
|
-
tx_hash: txHash,
|
|
476
|
-
payer: getSubscriptionPaymentAddress(subscription, 'arcblock'),
|
|
477
|
-
type: 'slash',
|
|
478
|
-
},
|
|
479
|
-
},
|
|
480
|
-
});
|
|
481
|
-
logger.info('PaymentIntent updated after stake slash', {
|
|
482
|
-
subscription: subscription.id,
|
|
483
|
-
paymentIntent: paymentIntent.id,
|
|
484
|
-
status: 'succeeded',
|
|
485
|
-
});
|
|
486
|
-
await invoice.update({
|
|
487
|
-
paid: true,
|
|
488
|
-
status: 'paid',
|
|
489
|
-
amount_paid: paymentIntent.amount,
|
|
490
|
-
amount_remaining: '0',
|
|
491
|
-
attempt_count: invoice.attempt_count + 1,
|
|
492
|
-
attempted: true,
|
|
493
|
-
status_transitions: { ...invoice.status_transitions, paid_at: dayjs().unix() },
|
|
494
|
-
});
|
|
495
|
-
|
|
496
|
-
logger.info('Invoice updated after stake slash', {
|
|
497
|
-
subscription: subscription.id,
|
|
498
|
-
invoice: invoice.id,
|
|
499
|
-
status: 'paid',
|
|
500
|
-
});
|
|
501
625
|
};
|
|
502
626
|
|
|
503
627
|
const ensureReturnStake = async (subscription: Subscription, paymentCurrencyId?: string, stakingAddress?: string) => {
|
|
@@ -528,6 +652,9 @@ const ensureReturnStake = async (subscription: Subscription, paymentCurrencyId?:
|
|
|
528
652
|
return;
|
|
529
653
|
}
|
|
530
654
|
|
|
655
|
+
// Handle any unpaid final invoice before stake return
|
|
656
|
+
await handleFinalInvoicePayment(subscription, paymentMethod, paymentCurrency);
|
|
657
|
+
|
|
531
658
|
const result = await getSubscriptionStakeReturnSetup(subscription, address, paymentMethod, paymentCurrencyId);
|
|
532
659
|
|
|
533
660
|
const stakeEnough = await checkRemainingStake(paymentMethod, paymentCurrency, address, result.return_amount);
|
|
@@ -562,7 +689,7 @@ const ensureReturnStake = async (subscription: Subscription, paymentCurrencyId?:
|
|
|
562
689
|
invoice_id: invoice?.id,
|
|
563
690
|
customer_id: subscription.customer_id,
|
|
564
691
|
payment_method_id: paymentMethod.id,
|
|
565
|
-
payment_intent_id:
|
|
692
|
+
payment_intent_id: invoice?.payment_intent_id || '',
|
|
566
693
|
subscription_id: subscription.id,
|
|
567
694
|
attempt_count: 0,
|
|
568
695
|
attempted: false,
|
|
@@ -959,6 +1086,13 @@ export const startSubscriptionQueue = async () => {
|
|
|
959
1086
|
return;
|
|
960
1087
|
}
|
|
961
1088
|
if (['past_due', 'paused'].includes(x.status)) {
|
|
1089
|
+
const willCancel = x.cancel_at || x.cancel_at_period_end;
|
|
1090
|
+
if (x.status === 'past_due' && willCancel) {
|
|
1091
|
+
const existingJob = await subscriptionQueue.get(`cancel-${x.id}`);
|
|
1092
|
+
if (!existingJob) {
|
|
1093
|
+
await addSubscriptionJob(x, 'cancel', true, x.cancel_at || x.current_period_end);
|
|
1094
|
+
}
|
|
1095
|
+
}
|
|
962
1096
|
logger.info(`skip add cycle subscription job because status is ${x.status}`, {
|
|
963
1097
|
subscription: x.id,
|
|
964
1098
|
action: 'cycle',
|
|
@@ -1086,10 +1220,11 @@ export async function addSubscriptionJob(
|
|
|
1086
1220
|
sync?: boolean
|
|
1087
1221
|
) {
|
|
1088
1222
|
const fn = sync ? 'pushAndWait' : 'push';
|
|
1089
|
-
const
|
|
1090
|
-
const
|
|
1223
|
+
const cycleJobId = subscription.id;
|
|
1224
|
+
const jobId = action === 'cycle' ? cycleJobId : `${action}-${subscription.id}`;
|
|
1225
|
+
const cycleJob = await subscriptionQueue.get(cycleJobId);
|
|
1091
1226
|
if (replace && cycleJob) {
|
|
1092
|
-
await subscriptionQueue.delete(
|
|
1227
|
+
await subscriptionQueue.delete(cycleJobId);
|
|
1093
1228
|
logger.info(`subscription cycle job replaced with ${action} job`, { subscription: subscription.id });
|
|
1094
1229
|
}
|
|
1095
1230
|
if (action === 'cycle') {
|
|
@@ -45,8 +45,8 @@ export default {
|
|
|
45
45
|
amount: fastCheckoutAmount,
|
|
46
46
|
});
|
|
47
47
|
const needDelegation = delegation.sufficient === false;
|
|
48
|
-
const
|
|
49
|
-
if (needDelegation ||
|
|
48
|
+
const requiredStake = !subscription.billing_thresholds?.no_stake;
|
|
49
|
+
if (needDelegation || !requiredStake) {
|
|
50
50
|
claimsList.push({
|
|
51
51
|
signature: await getDelegationTxClaim({
|
|
52
52
|
mode: 'setup',
|
|
@@ -59,11 +59,12 @@ export default {
|
|
|
59
59
|
trialing,
|
|
60
60
|
billingThreshold,
|
|
61
61
|
items,
|
|
62
|
+
requiredStake,
|
|
62
63
|
}),
|
|
63
64
|
});
|
|
64
65
|
}
|
|
65
66
|
|
|
66
|
-
if (
|
|
67
|
+
if (requiredStake) {
|
|
67
68
|
claimsList.push({
|
|
68
69
|
prepareTx: await getStakeTxClaim({
|
|
69
70
|
userDid,
|
|
@@ -109,7 +110,7 @@ export default {
|
|
|
109
110
|
const { setupIntent, subscription, paymentMethod, paymentCurrency, customer } =
|
|
110
111
|
await ensureChangePaymentContext(subscriptionId);
|
|
111
112
|
|
|
112
|
-
const
|
|
113
|
+
const requiredStake = !subscription.billing_thresholds?.no_stake;
|
|
113
114
|
|
|
114
115
|
const result = request?.context?.store?.result || [];
|
|
115
116
|
result.push({
|
|
@@ -123,7 +124,7 @@ export default {
|
|
|
123
124
|
// 判断是否为最后一步
|
|
124
125
|
const staking = result.find((x: any) => x.claim?.type === 'prepareTx' && x.claim?.meta?.purpose === 'staking');
|
|
125
126
|
const isFinalStep =
|
|
126
|
-
(paymentMethod.type === 'arcblock' && (staking ||
|
|
127
|
+
(paymentMethod.type === 'arcblock' && (staking || !requiredStake)) || paymentMethod.type !== 'arcblock';
|
|
127
128
|
|
|
128
129
|
if (!isFinalStep) {
|
|
129
130
|
await updateSession({
|
|
@@ -691,6 +691,7 @@ export async function getDelegationTxClaim({
|
|
|
691
691
|
paymentMethod,
|
|
692
692
|
trialing = false,
|
|
693
693
|
billingThreshold = 0,
|
|
694
|
+
requiredStake = true,
|
|
694
695
|
}: {
|
|
695
696
|
userDid: string;
|
|
696
697
|
userPk: string;
|
|
@@ -702,6 +703,7 @@ export async function getDelegationTxClaim({
|
|
|
702
703
|
paymentMethod: PaymentMethod;
|
|
703
704
|
trialing: boolean;
|
|
704
705
|
billingThreshold?: number;
|
|
706
|
+
requiredStake?: boolean;
|
|
705
707
|
}) {
|
|
706
708
|
const amount = getFastCheckoutAmount(items, mode, paymentCurrency.id);
|
|
707
709
|
const address = toDelegateAddress(userDid, wallet.address);
|
|
@@ -713,8 +715,8 @@ export async function getDelegationTxClaim({
|
|
|
713
715
|
billingThreshold,
|
|
714
716
|
paymentMethod,
|
|
715
717
|
paymentCurrency,
|
|
718
|
+
requiredStake
|
|
716
719
|
});
|
|
717
|
-
|
|
718
720
|
if (mode === 'delegation') {
|
|
719
721
|
tokenRequirements = [];
|
|
720
722
|
}
|
|
@@ -1003,6 +1005,7 @@ export type TokenRequirementArgs = {
|
|
|
1003
1005
|
paymentCurrency: PaymentCurrency;
|
|
1004
1006
|
trialing: boolean;
|
|
1005
1007
|
billingThreshold: number;
|
|
1008
|
+
requiredStake?: boolean;
|
|
1006
1009
|
};
|
|
1007
1010
|
|
|
1008
1011
|
export async function getTokenRequirements({
|
|
@@ -1012,6 +1015,7 @@ export async function getTokenRequirements({
|
|
|
1012
1015
|
paymentCurrency,
|
|
1013
1016
|
trialing = false,
|
|
1014
1017
|
billingThreshold = 0,
|
|
1018
|
+
requiredStake
|
|
1015
1019
|
}: TokenRequirementArgs) {
|
|
1016
1020
|
const tokenRequirements = [];
|
|
1017
1021
|
let amount = getFastCheckoutAmount(items, mode, paymentCurrency.id, !!trialing);
|
|
@@ -1039,7 +1043,7 @@ export async function getTokenRequirements({
|
|
|
1039
1043
|
}
|
|
1040
1044
|
|
|
1041
1045
|
// Add stake requirement to token requirement
|
|
1042
|
-
if ((paymentMethod.type === 'arcblock' && mode !== 'delegation') || mode === 'setup') {
|
|
1046
|
+
if (requiredStake && ((paymentMethod.type === 'arcblock' && mode !== 'delegation') || mode === 'setup')) {
|
|
1043
1047
|
const staking = getSubscriptionStakeSetup(
|
|
1044
1048
|
items,
|
|
1045
1049
|
paymentCurrency.id,
|
|
@@ -66,6 +66,7 @@ export default {
|
|
|
66
66
|
const claimsList: any[] = [];
|
|
67
67
|
|
|
68
68
|
const allSubscriptionIds = subscriptions.map((sub) => sub.id);
|
|
69
|
+
const requiredStake = !checkoutSession.subscription_data?.no_stake;
|
|
69
70
|
if (paymentMethod.type === 'arcblock') {
|
|
70
71
|
const delegation = await isDelegationSufficientForPayment({
|
|
71
72
|
paymentMethod,
|
|
@@ -76,7 +77,7 @@ export default {
|
|
|
76
77
|
|
|
77
78
|
// if we can complete purchase without any wallet interaction
|
|
78
79
|
// we forced to delegate if we can skip stake
|
|
79
|
-
if (delegation.sufficient === false ||
|
|
80
|
+
if (delegation.sufficient === false || !requiredStake) {
|
|
80
81
|
claimsList.push({
|
|
81
82
|
signature: await getDelegationTxClaim({
|
|
82
83
|
mode: checkoutSession.mode,
|
|
@@ -93,11 +94,12 @@ export default {
|
|
|
93
94
|
trialing,
|
|
94
95
|
billingThreshold: Math.max(minStakeAmount, billingThreshold),
|
|
95
96
|
items,
|
|
97
|
+
requiredStake,
|
|
96
98
|
}),
|
|
97
99
|
});
|
|
98
100
|
}
|
|
99
101
|
|
|
100
|
-
if (
|
|
102
|
+
if (requiredStake) {
|
|
101
103
|
claimsList.push({
|
|
102
104
|
prepareTx: await getStakeTxClaim({
|
|
103
105
|
userDid,
|
|
@@ -162,10 +164,10 @@ export default {
|
|
|
162
164
|
headers: request?.headers,
|
|
163
165
|
},
|
|
164
166
|
});
|
|
167
|
+
const requiredStake = !checkoutSession.subscription_data?.no_stake;
|
|
165
168
|
const staking = result.find((x: any) => x.claim?.type === 'prepareTx' && x.claim?.meta?.purpose === 'staking');
|
|
166
169
|
const isFinalStep =
|
|
167
|
-
(paymentMethod.type === 'arcblock' && (staking ||
|
|
168
|
-
paymentMethod.type !== 'arcblock';
|
|
170
|
+
(paymentMethod.type === 'arcblock' && (staking || !requiredStake)) || paymentMethod.type !== 'arcblock';
|
|
169
171
|
if (!isFinalStep) {
|
|
170
172
|
await updateSession({
|
|
171
173
|
result,
|
package/blocklet.yml
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "payment-kit",
|
|
3
|
-
"version": "1.18.
|
|
3
|
+
"version": "1.18.55",
|
|
4
4
|
"scripts": {
|
|
5
5
|
"dev": "blocklet dev --open",
|
|
6
6
|
"eject": "vite eject",
|
|
@@ -45,30 +45,30 @@
|
|
|
45
45
|
},
|
|
46
46
|
"dependencies": {
|
|
47
47
|
"@abtnode/cron": "^1.16.44",
|
|
48
|
-
"@arcblock/did": "^1.20.
|
|
48
|
+
"@arcblock/did": "^1.20.14",
|
|
49
49
|
"@arcblock/did-auth-storage-nedb": "^1.7.1",
|
|
50
|
-
"@arcblock/did-connect": "^2.13.
|
|
51
|
-
"@arcblock/did-util": "^1.20.
|
|
52
|
-
"@arcblock/jwt": "^1.20.
|
|
53
|
-
"@arcblock/ux": "^2.13.
|
|
54
|
-
"@arcblock/validator": "^1.20.
|
|
55
|
-
"@blocklet/did-space-js": "^1.0.
|
|
50
|
+
"@arcblock/did-connect": "^2.13.66",
|
|
51
|
+
"@arcblock/did-util": "^1.20.14",
|
|
52
|
+
"@arcblock/jwt": "^1.20.14",
|
|
53
|
+
"@arcblock/ux": "^2.13.66",
|
|
54
|
+
"@arcblock/validator": "^1.20.14",
|
|
55
|
+
"@blocklet/did-space-js": "^1.0.60",
|
|
56
56
|
"@blocklet/js-sdk": "^1.16.44",
|
|
57
57
|
"@blocklet/logger": "^1.16.44",
|
|
58
|
-
"@blocklet/payment-react": "1.18.
|
|
58
|
+
"@blocklet/payment-react": "1.18.55",
|
|
59
59
|
"@blocklet/sdk": "^1.16.44",
|
|
60
|
-
"@blocklet/ui-react": "^2.13.
|
|
60
|
+
"@blocklet/ui-react": "^2.13.66",
|
|
61
61
|
"@blocklet/uploader": "^0.1.95",
|
|
62
62
|
"@blocklet/xss": "^0.1.36",
|
|
63
63
|
"@mui/icons-material": "^5.16.6",
|
|
64
64
|
"@mui/lab": "^5.0.0-alpha.173",
|
|
65
65
|
"@mui/material": "^5.16.6",
|
|
66
66
|
"@mui/system": "^5.16.6",
|
|
67
|
-
"@ocap/asset": "^1.20.
|
|
68
|
-
"@ocap/client": "^1.20.
|
|
69
|
-
"@ocap/mcrypto": "^1.20.
|
|
70
|
-
"@ocap/util": "^1.20.
|
|
71
|
-
"@ocap/wallet": "^1.20.
|
|
67
|
+
"@ocap/asset": "^1.20.14",
|
|
68
|
+
"@ocap/client": "^1.20.14",
|
|
69
|
+
"@ocap/mcrypto": "^1.20.14",
|
|
70
|
+
"@ocap/util": "^1.20.14",
|
|
71
|
+
"@ocap/wallet": "^1.20.14",
|
|
72
72
|
"@stripe/react-stripe-js": "^2.7.3",
|
|
73
73
|
"@stripe/stripe-js": "^2.4.0",
|
|
74
74
|
"ahooks": "^3.8.0",
|
|
@@ -123,7 +123,7 @@
|
|
|
123
123
|
"devDependencies": {
|
|
124
124
|
"@abtnode/types": "^1.16.44",
|
|
125
125
|
"@arcblock/eslint-config-ts": "^0.3.3",
|
|
126
|
-
"@blocklet/payment-types": "1.18.
|
|
126
|
+
"@blocklet/payment-types": "1.18.55",
|
|
127
127
|
"@types/cookie-parser": "^1.4.7",
|
|
128
128
|
"@types/cors": "^2.8.17",
|
|
129
129
|
"@types/debug": "^4.1.12",
|
|
@@ -169,5 +169,5 @@
|
|
|
169
169
|
"parser": "typescript"
|
|
170
170
|
}
|
|
171
171
|
},
|
|
172
|
-
"gitHead": "
|
|
172
|
+
"gitHead": "87893b50ad72026312cd6b5d35e7f6f1181ad53a"
|
|
173
173
|
}
|