payment-kit 1.14.29 → 1.14.31
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 +4 -0
- package/api/src/libs/api.ts +23 -0
- package/api/src/libs/subscription.ts +32 -0
- package/api/src/queues/refund.ts +38 -1
- package/api/src/queues/subscription.ts +218 -21
- package/api/src/routes/checkout-sessions.ts +5 -0
- package/api/src/routes/customers.ts +27 -1
- package/api/src/routes/invoices.ts +5 -1
- package/api/src/routes/payment-intents.ts +17 -2
- package/api/src/routes/payment-links.ts +105 -3
- package/api/src/routes/payouts.ts +5 -1
- package/api/src/routes/prices.ts +19 -3
- package/api/src/routes/pricing-table.ts +79 -2
- package/api/src/routes/products.ts +24 -8
- package/api/src/routes/refunds.ts +7 -4
- package/api/src/routes/subscription-items.ts +5 -1
- package/api/src/routes/subscriptions.ts +38 -6
- package/api/src/routes/webhook-endpoints.ts +5 -1
- package/api/src/store/models/subscription.ts +1 -0
- package/api/tests/libs/api.spec.ts +72 -1
- package/api/third.d.ts +2 -0
- package/blocklet.yml +1 -1
- package/package.json +19 -18
- package/src/components/customer/form.tsx +53 -0
- package/src/components/filter-toolbar.tsx +1 -1
- package/src/components/invoice/list.tsx +8 -8
- package/src/components/invoice/table.tsx +42 -36
- package/src/components/metadata/form.tsx +24 -3
- package/src/components/payment-intent/actions.tsx +17 -5
- package/src/components/payment-link/after-pay.tsx +46 -4
- package/src/components/payouts/list.tsx +1 -1
- package/src/components/price/form.tsx +14 -2
- package/src/components/pricing-table/payment-settings.tsx +45 -4
- package/src/components/product/features.tsx +16 -2
- package/src/components/product/form.tsx +28 -4
- package/src/components/subscription/actions/cancel.tsx +10 -0
- package/src/components/subscription/description.tsx +2 -2
- package/src/components/subscription/items/index.tsx +3 -2
- package/src/components/subscription/portal/cancel.tsx +12 -1
- package/src/components/subscription/portal/list.tsx +169 -145
- package/src/hooks/loading.ts +28 -0
- package/src/locales/en.tsx +6 -1
- package/src/locales/zh.tsx +6 -1
- package/src/pages/admin/billing/invoices/detail.tsx +17 -2
- package/src/pages/admin/billing/subscriptions/detail.tsx +4 -0
- package/src/pages/admin/customers/customers/detail.tsx +4 -0
- package/src/pages/admin/customers/customers/index.tsx +1 -1
- package/src/pages/admin/payments/intents/detail.tsx +4 -0
- package/src/pages/admin/payments/payouts/detail.tsx +4 -0
- package/src/pages/admin/payments/refunds/detail.tsx +4 -0
- package/src/pages/admin/products/links/detail.tsx +4 -0
- package/src/pages/admin/products/prices/detail.tsx +4 -0
- package/src/pages/admin/products/pricing-tables/detail.tsx +4 -0
- package/src/pages/admin/products/products/detail.tsx +4 -0
- package/src/pages/checkout/pricing-table.tsx +9 -3
- package/src/pages/customer/index.tsx +28 -17
- package/src/pages/customer/invoice/detail.tsx +27 -16
- package/src/pages/customer/invoice/past-due.tsx +3 -2
- package/src/pages/customer/subscription/detail.tsx +4 -0
package/api/src/index.ts
CHANGED
|
@@ -9,6 +9,9 @@ import dotenv from 'dotenv-flow';
|
|
|
9
9
|
import express, { ErrorRequestHandler, Request, Response } from 'express';
|
|
10
10
|
import morgan from 'morgan';
|
|
11
11
|
|
|
12
|
+
// eslint-disable-next-line import/no-extraneous-dependencies
|
|
13
|
+
import { xss } from 'express-xss-sanitizer';
|
|
14
|
+
|
|
12
15
|
import crons from './crons/index';
|
|
13
16
|
import { ensureStakedForGas } from './integrations/arcblock/stake';
|
|
14
17
|
import { initResourceHandler } from './integrations/blocklet/resource';
|
|
@@ -52,6 +55,7 @@ app.use((req, res, next) => {
|
|
|
52
55
|
});
|
|
53
56
|
app.use(express.urlencoded({ extended: true, limit: '1 mb' }));
|
|
54
57
|
app.use(cors());
|
|
58
|
+
app.use(xss());
|
|
55
59
|
app.use(ensureI18n());
|
|
56
60
|
|
|
57
61
|
const router = express.Router();
|
package/api/src/libs/api.ts
CHANGED
|
@@ -153,3 +153,26 @@ export const BNPositiveValidator = Joi.string().custom((value, helpers) => {
|
|
|
153
153
|
}
|
|
154
154
|
return value;
|
|
155
155
|
}, 'BN Positive Validation');
|
|
156
|
+
|
|
157
|
+
function validateMetadataValue(value: any, helpers: any) {
|
|
158
|
+
if (value && typeof value === 'string' && value.length > 256) {
|
|
159
|
+
return helpers.message({ custom: 'Metadata value should be less than 256 characters' });
|
|
160
|
+
}
|
|
161
|
+
return value;
|
|
162
|
+
}
|
|
163
|
+
export const MetadataSchema = Joi.alternatives()
|
|
164
|
+
.try(
|
|
165
|
+
Joi.object()
|
|
166
|
+
.pattern(Joi.string().max(64), Joi.any().custom(validateMetadataValue, 'Custom Validation'))
|
|
167
|
+
.min(0)
|
|
168
|
+
.allow(null),
|
|
169
|
+
Joi.array()
|
|
170
|
+
.items(
|
|
171
|
+
Joi.object({
|
|
172
|
+
key: Joi.string().max(64).required(),
|
|
173
|
+
value: Joi.any().custom(validateMetadataValue, 'Custom Validation').required(),
|
|
174
|
+
})
|
|
175
|
+
)
|
|
176
|
+
.min(0)
|
|
177
|
+
)
|
|
178
|
+
.optional();
|
|
@@ -712,3 +712,35 @@ export async function getSubscriptionStakeReturnSetup(
|
|
|
712
712
|
lastInvoice,
|
|
713
713
|
};
|
|
714
714
|
}
|
|
715
|
+
|
|
716
|
+
export async function checkRemainingStake(
|
|
717
|
+
paymentMethod: PaymentMethod,
|
|
718
|
+
paymentCurrency: PaymentCurrency,
|
|
719
|
+
address: string,
|
|
720
|
+
amount: string
|
|
721
|
+
): Promise<{ enough: boolean; staked: any; revoked: any }> {
|
|
722
|
+
if (paymentMethod?.type !== 'arcblock') {
|
|
723
|
+
return {
|
|
724
|
+
enough: false,
|
|
725
|
+
staked: '0',
|
|
726
|
+
revoked: '0',
|
|
727
|
+
};
|
|
728
|
+
}
|
|
729
|
+
const client = paymentMethod.getOcapClient();
|
|
730
|
+
const { state } = await client.getStakeState({ address });
|
|
731
|
+
|
|
732
|
+
const staked = state.tokens.find((x: any) => x.address === paymentCurrency.contract);
|
|
733
|
+
const revoked = state.revokedTokens.find((x: any) => x.address === paymentCurrency.contract);
|
|
734
|
+
let total = new BN(0);
|
|
735
|
+
if (staked) {
|
|
736
|
+
total = total.add(new BN(staked.value));
|
|
737
|
+
}
|
|
738
|
+
if (revoked) {
|
|
739
|
+
total = total.add(new BN(revoked.value));
|
|
740
|
+
}
|
|
741
|
+
return {
|
|
742
|
+
enough: total.gte(new BN(amount)),
|
|
743
|
+
staked,
|
|
744
|
+
revoked,
|
|
745
|
+
};
|
|
746
|
+
}
|
package/api/src/queues/refund.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { toStakeAddress } from '@arcblock/did-util';
|
|
2
2
|
import { isRefundReasonSupportedByStripe } from '@api/libs/refund';
|
|
3
|
+
import { checkRemainingStake } from '@api/libs/subscription';
|
|
3
4
|
import { sendErc20ToUser } from '../integrations/ethereum/token';
|
|
4
5
|
import { wallet } from '../libs/auth';
|
|
5
6
|
import CustomError from '../libs/error';
|
|
@@ -296,11 +297,47 @@ const handleStakeReturnJob = async (
|
|
|
296
297
|
if (!arcblockDetail) {
|
|
297
298
|
throw new Error('arcblockDetail info not found');
|
|
298
299
|
}
|
|
300
|
+
|
|
301
|
+
if (refund.amount === '0') {
|
|
302
|
+
logger.warn('stake return aborted because amount is 0', { id: refund.id });
|
|
303
|
+
await refund.update({
|
|
304
|
+
status: 'failed',
|
|
305
|
+
last_attempt_error: {
|
|
306
|
+
type: 'card_error',
|
|
307
|
+
code: '400',
|
|
308
|
+
message: 'stake return amount is 0',
|
|
309
|
+
payment_method_id: paymentMethod.id,
|
|
310
|
+
payment_method_type: paymentMethod.type,
|
|
311
|
+
},
|
|
312
|
+
});
|
|
313
|
+
return;
|
|
314
|
+
}
|
|
299
315
|
const client = paymentMethod.getOcapClient();
|
|
316
|
+
const address = toStakeAddress(customer.did, wallet.address, refund.subscription_id);
|
|
317
|
+
const stakeEnough = await checkRemainingStake(paymentMethod, paymentCurrency, address, refund.amount);
|
|
318
|
+
if (!stakeEnough.enough) {
|
|
319
|
+
logger.warn('Stake return aborted because stake is not enough ', {
|
|
320
|
+
subscription: refund.subscription_id,
|
|
321
|
+
address,
|
|
322
|
+
staked: stakeEnough.staked,
|
|
323
|
+
revoked: stakeEnough.revoked,
|
|
324
|
+
});
|
|
325
|
+
await refund.update({
|
|
326
|
+
status: 'failed',
|
|
327
|
+
last_attempt_error: {
|
|
328
|
+
type: 'card_error',
|
|
329
|
+
code: '400',
|
|
330
|
+
message: 'stake is not enough',
|
|
331
|
+
payment_method_id: paymentMethod.id,
|
|
332
|
+
payment_method_type: paymentMethod.type,
|
|
333
|
+
},
|
|
334
|
+
});
|
|
335
|
+
return;
|
|
336
|
+
}
|
|
300
337
|
const signed = await client.signReturnStakeTx({
|
|
301
338
|
tx: {
|
|
302
339
|
itx: {
|
|
303
|
-
address
|
|
340
|
+
address,
|
|
304
341
|
outputs: [
|
|
305
342
|
{
|
|
306
343
|
owner: arcblockDetail?.receiver,
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { toStakeAddress } from '@arcblock/did-util';
|
|
2
|
-
import { BN } from '@ocap/util';
|
|
3
2
|
import type { LiteralUnion } from 'type-fest';
|
|
4
3
|
|
|
5
4
|
import { ensurePassportRevoked } from '../integrations/blocklet/passport';
|
|
@@ -13,6 +12,7 @@ import { getGasPayerExtra } from '../libs/payment';
|
|
|
13
12
|
import createQueue from '../libs/queue';
|
|
14
13
|
import { getStatementDescriptor } from '../libs/session';
|
|
15
14
|
import {
|
|
15
|
+
checkRemainingStake,
|
|
16
16
|
getSubscriptionCycleAmount,
|
|
17
17
|
getSubscriptionCycleSetup,
|
|
18
18
|
getSubscriptionStakeReturnSetup,
|
|
@@ -328,7 +328,7 @@ const handleStakeSlashAfterCancel = async (subscription: Subscription) => {
|
|
|
328
328
|
|
|
329
329
|
// check the staking
|
|
330
330
|
const client = method.getOcapClient();
|
|
331
|
-
const address = toStakeAddress(customer.did, wallet.address);
|
|
331
|
+
const address = toStakeAddress(customer.did, wallet.address, subscription.id);
|
|
332
332
|
const { state } = await client.getStakeState({ address });
|
|
333
333
|
if (!state || !state.data?.value) {
|
|
334
334
|
logger.warn('Stake slashing aborted because no staking state', {
|
|
@@ -337,7 +337,6 @@ const handleStakeSlashAfterCancel = async (subscription: Subscription) => {
|
|
|
337
337
|
});
|
|
338
338
|
return;
|
|
339
339
|
}
|
|
340
|
-
|
|
341
340
|
// check for staking for this subscription
|
|
342
341
|
const data = JSON.parse(state.data.value || '{}');
|
|
343
342
|
if (!data[subscription.id] && !state.nonce) {
|
|
@@ -350,22 +349,22 @@ const handleStakeSlashAfterCancel = async (subscription: Subscription) => {
|
|
|
350
349
|
}
|
|
351
350
|
|
|
352
351
|
// check for staking for amount
|
|
353
|
-
const
|
|
354
|
-
|
|
355
|
-
let total = new BN(0);
|
|
356
|
-
if (staked) {
|
|
357
|
-
total = total.add(new BN(staked.value));
|
|
358
|
-
}
|
|
359
|
-
if (revoked) {
|
|
360
|
-
total = total.add(new BN(revoked.value));
|
|
361
|
-
}
|
|
362
|
-
const isStakeEnough = total.gte(new BN(invoice.amount_remaining));
|
|
363
|
-
if (!isStakeEnough) {
|
|
352
|
+
const stakeEnough = await checkRemainingStake(method, currency, address, invoice.amount_remaining);
|
|
353
|
+
if (!stakeEnough.enough) {
|
|
364
354
|
logger.warn('Stake slashing aborted because no enough staking', {
|
|
365
355
|
subscription: subscription.id,
|
|
366
356
|
address,
|
|
367
|
-
staked,
|
|
368
|
-
revoked,
|
|
357
|
+
staked: stakeEnough.staked,
|
|
358
|
+
revoked: stakeEnough.revoked,
|
|
359
|
+
});
|
|
360
|
+
return;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
if (invoice.amount_remaining === '0') {
|
|
364
|
+
logger.warn('Stake slashing aborted because amount_remaining is 0', {
|
|
365
|
+
subscription: subscription.id,
|
|
366
|
+
address,
|
|
367
|
+
invoice: invoice.id,
|
|
369
368
|
});
|
|
370
369
|
return;
|
|
371
370
|
}
|
|
@@ -374,7 +373,7 @@ const handleStakeSlashAfterCancel = async (subscription: Subscription) => {
|
|
|
374
373
|
const signed = await client.signSlashStakeTx({
|
|
375
374
|
tx: {
|
|
376
375
|
itx: {
|
|
377
|
-
address: toStakeAddress(customer.did, wallet.address),
|
|
376
|
+
address: toStakeAddress(customer.did, wallet.address, subscription.id),
|
|
378
377
|
outputs: [{ owner: wallet.address, tokens: [{ address: currency.contract, value: invoice.amount_remaining }] }],
|
|
379
378
|
message: 'uncollectible_past_due_invoice',
|
|
380
379
|
data: {
|
|
@@ -450,9 +449,27 @@ const ensureReturnStake = async (subscription: Subscription) => {
|
|
|
450
449
|
logger.info(`Stake return skipped because subscription ${subscription.id} already has stake return records.`);
|
|
451
450
|
return;
|
|
452
451
|
}
|
|
452
|
+
const paymentCurrency = await PaymentCurrency.findByPk(subscription.currency_id);
|
|
453
|
+
if (!paymentCurrency) {
|
|
454
|
+
logger.warn('Stake return skipped because no payment currency', {
|
|
455
|
+
subscription: subscription.id,
|
|
456
|
+
currency: subscription.currency_id,
|
|
457
|
+
});
|
|
458
|
+
return;
|
|
459
|
+
}
|
|
453
460
|
|
|
454
461
|
const result = await getSubscriptionStakeReturnSetup(subscription, address, paymentMethod);
|
|
455
462
|
|
|
463
|
+
const stakeEnough = await checkRemainingStake(paymentMethod, paymentCurrency, address, result.return_amount);
|
|
464
|
+
if (!stakeEnough.enough) {
|
|
465
|
+
logger.warn('Stake return skipped because no enough staking', {
|
|
466
|
+
subscription: subscription.id,
|
|
467
|
+
address,
|
|
468
|
+
staked: stakeEnough.staked,
|
|
469
|
+
revoked: stakeEnough.revoked,
|
|
470
|
+
});
|
|
471
|
+
return;
|
|
472
|
+
}
|
|
456
473
|
if (result.return_amount !== '0') {
|
|
457
474
|
// do the stake return
|
|
458
475
|
const item = await Refund.create({
|
|
@@ -495,6 +512,148 @@ const ensureReturnStake = async (subscription: Subscription) => {
|
|
|
495
512
|
}
|
|
496
513
|
};
|
|
497
514
|
|
|
515
|
+
const slashStakeOnCancel = async (subscription: Subscription) => {
|
|
516
|
+
const paymentMethod = await PaymentMethod.findByPk(subscription.default_payment_method_id);
|
|
517
|
+
if (paymentMethod?.type !== 'arcblock') {
|
|
518
|
+
logger.warn('Stake slashing skipped because payment method not arcblock', {
|
|
519
|
+
subscription: subscription.id,
|
|
520
|
+
paymentMethod: paymentMethod?.id,
|
|
521
|
+
});
|
|
522
|
+
return;
|
|
523
|
+
}
|
|
524
|
+
const customer = await Customer.findByPk(subscription.customer_id);
|
|
525
|
+
if (!customer) {
|
|
526
|
+
logger.warn('Stake slashing skipped because customer not found', {
|
|
527
|
+
subscription: subscription.id,
|
|
528
|
+
customer: subscription.customer_id,
|
|
529
|
+
});
|
|
530
|
+
return;
|
|
531
|
+
}
|
|
532
|
+
const client = paymentMethod.getOcapClient();
|
|
533
|
+
const address = toStakeAddress(customer.did, wallet.address, subscription.id);
|
|
534
|
+
const currency = await PaymentCurrency.findByPk(subscription.currency_id);
|
|
535
|
+
if (!currency) {
|
|
536
|
+
logger.warn('Stake slashing skipped because currency not found', {
|
|
537
|
+
subscription: subscription.id,
|
|
538
|
+
currency: subscription.currency_id,
|
|
539
|
+
});
|
|
540
|
+
return;
|
|
541
|
+
}
|
|
542
|
+
const result = await getSubscriptionStakeReturnSetup(subscription, address, paymentMethod);
|
|
543
|
+
const stakeEnough = await checkRemainingStake(paymentMethod, currency, address, result.return_amount);
|
|
544
|
+
if (!stakeEnough.enough) {
|
|
545
|
+
logger.warn('Stake slashing aborted because no enough staking', {
|
|
546
|
+
subscription: subscription.id,
|
|
547
|
+
address,
|
|
548
|
+
staked: stakeEnough.revoked,
|
|
549
|
+
revoked: stakeEnough.revoked,
|
|
550
|
+
});
|
|
551
|
+
return;
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
if (result.return_amount === '0') {
|
|
555
|
+
logger.warn('Stake slashing aborted because amount_remaining is 0', {
|
|
556
|
+
subscription: subscription.id,
|
|
557
|
+
address,
|
|
558
|
+
amount_remaining: result.return_amount,
|
|
559
|
+
});
|
|
560
|
+
return;
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
// do the slash
|
|
564
|
+
const signed = await client.signSlashStakeTx({
|
|
565
|
+
tx: {
|
|
566
|
+
itx: {
|
|
567
|
+
address: toStakeAddress(customer.did, wallet.address, subscription.id),
|
|
568
|
+
outputs: [{ owner: wallet.address, tokens: [{ address: currency.contract, value: result.return_amount }] }],
|
|
569
|
+
message: 'stake_slash_on_subscription_cancel',
|
|
570
|
+
data: {
|
|
571
|
+
typeUrl: 'json',
|
|
572
|
+
// @ts-ignore
|
|
573
|
+
value: {
|
|
574
|
+
appId: wallet.address,
|
|
575
|
+
reason: 'stake_slash_on_subscription_cancel',
|
|
576
|
+
subscriptionId: subscription.id,
|
|
577
|
+
invoiceId: subscription.latest_invoice_id,
|
|
578
|
+
paymentIntentId: result?.lastInvoice?.payment_intent_id as string,
|
|
579
|
+
},
|
|
580
|
+
},
|
|
581
|
+
},
|
|
582
|
+
},
|
|
583
|
+
wallet,
|
|
584
|
+
});
|
|
585
|
+
// @ts-ignore
|
|
586
|
+
const { buffer } = await client.encodeSlashStakeTx({ tx: signed });
|
|
587
|
+
// @ts-ignore
|
|
588
|
+
const txHash = await client.sendSlashStakeTx({ tx: signed, wallet }, getGasPayerExtra(buffer));
|
|
589
|
+
logger.info('Stake slashing done', {
|
|
590
|
+
subscription: subscription.id,
|
|
591
|
+
amount: result.return_amount,
|
|
592
|
+
address,
|
|
593
|
+
txHash,
|
|
594
|
+
});
|
|
595
|
+
// create new payment intent
|
|
596
|
+
const paymentIntent = await PaymentIntent.create({
|
|
597
|
+
livemode: subscription.livemode,
|
|
598
|
+
amount: result.return_amount,
|
|
599
|
+
amount_received: result.return_amount,
|
|
600
|
+
amount_capturable: '0',
|
|
601
|
+
currency_id: subscription.currency_id,
|
|
602
|
+
customer_id: subscription.customer_id,
|
|
603
|
+
payment_method_id: subscription.default_payment_method_id,
|
|
604
|
+
status: 'succeeded',
|
|
605
|
+
capture_method: 'manual',
|
|
606
|
+
last_payment_error: null,
|
|
607
|
+
description: 'Stake slash on subscription cancel',
|
|
608
|
+
statement_descriptor: result.lastInvoice?.statement_descriptor || getStatementDescriptor([]),
|
|
609
|
+
payment_method_types: ['arcblock'],
|
|
610
|
+
confirmation_method: '',
|
|
611
|
+
payment_details: {
|
|
612
|
+
arcblock: {
|
|
613
|
+
tx_hash: txHash,
|
|
614
|
+
payer: subscription.payment_details?.arcblock?.payer as string,
|
|
615
|
+
type: 'slash',
|
|
616
|
+
},
|
|
617
|
+
},
|
|
618
|
+
});
|
|
619
|
+
logger.info('Payment intent created for stake slash', {
|
|
620
|
+
paymentIntent: paymentIntent.id,
|
|
621
|
+
subscription: subscription.id,
|
|
622
|
+
});
|
|
623
|
+
// 创建一笔账单关联到该订阅
|
|
624
|
+
const { invoice } = await ensureInvoiceAndItems({
|
|
625
|
+
customer,
|
|
626
|
+
currency,
|
|
627
|
+
subscription,
|
|
628
|
+
trialing: false,
|
|
629
|
+
metered: true,
|
|
630
|
+
lineItems: [],
|
|
631
|
+
props: {
|
|
632
|
+
livemode: subscription.livemode,
|
|
633
|
+
description: 'Slash stake',
|
|
634
|
+
statement_descriptor: result.lastInvoice?.statement_descriptor,
|
|
635
|
+
period_start: subscription.canceled_at,
|
|
636
|
+
period_end: subscription.canceled_at,
|
|
637
|
+
|
|
638
|
+
auto_advance: true,
|
|
639
|
+
status: 'paid',
|
|
640
|
+
billing_reason: 'stake_slash_on_subscription_cancel',
|
|
641
|
+
currency_id: subscription.currency_id,
|
|
642
|
+
|
|
643
|
+
total: result.return_amount,
|
|
644
|
+
amount_paid: result.return_amount,
|
|
645
|
+
amount_remaining: '0',
|
|
646
|
+
amount_due: '0',
|
|
647
|
+
|
|
648
|
+
payment_settings: subscription.payment_settings,
|
|
649
|
+
default_payment_method_id: subscription.default_payment_method_id,
|
|
650
|
+
payment_intent_id: paymentIntent.id,
|
|
651
|
+
} as unknown as Invoice,
|
|
652
|
+
});
|
|
653
|
+
logger.info('Invoice created for stake slash', { invoice: invoice.id, subscription: subscription.id });
|
|
654
|
+
paymentIntent.update({ invoice_id: invoice.id });
|
|
655
|
+
};
|
|
656
|
+
|
|
498
657
|
// generate invoice for subscription periodically
|
|
499
658
|
export const handleSubscription = async (job: SubscriptionJob) => {
|
|
500
659
|
logger.info('handle subscription', job);
|
|
@@ -588,6 +747,44 @@ export const startSubscriptionQueue = async () => {
|
|
|
588
747
|
await batchHandleStripeSubscriptions();
|
|
589
748
|
};
|
|
590
749
|
|
|
750
|
+
export const slashStakeQueue = createQueue({
|
|
751
|
+
name: 'slashStake',
|
|
752
|
+
onJob: async (job) => {
|
|
753
|
+
const { subscriptionId } = job;
|
|
754
|
+
const subscription = await Subscription.findByPk(subscriptionId);
|
|
755
|
+
if (!subscription) {
|
|
756
|
+
return;
|
|
757
|
+
}
|
|
758
|
+
await slashStakeOnCancel(subscription);
|
|
759
|
+
},
|
|
760
|
+
options: {
|
|
761
|
+
concurrency: 1,
|
|
762
|
+
maxRetries: 5,
|
|
763
|
+
retryDelay: 1000,
|
|
764
|
+
maxTimeout: 60000,
|
|
765
|
+
enableScheduledJob: true,
|
|
766
|
+
},
|
|
767
|
+
});
|
|
768
|
+
|
|
769
|
+
export const returnStakeQueue = createQueue({
|
|
770
|
+
name: 'returnStake',
|
|
771
|
+
onJob: async (job) => {
|
|
772
|
+
const { subscriptionId } = job;
|
|
773
|
+
const subscription = await Subscription.findByPk(subscriptionId);
|
|
774
|
+
if (!subscription) {
|
|
775
|
+
return;
|
|
776
|
+
}
|
|
777
|
+
await ensureReturnStake(subscription);
|
|
778
|
+
},
|
|
779
|
+
options: {
|
|
780
|
+
concurrency: 1,
|
|
781
|
+
maxRetries: 5,
|
|
782
|
+
retryDelay: 1000,
|
|
783
|
+
maxTimeout: 60000,
|
|
784
|
+
enableScheduledJob: true,
|
|
785
|
+
},
|
|
786
|
+
});
|
|
787
|
+
|
|
591
788
|
export async function addSubscriptionJob(
|
|
592
789
|
subscription: Subscription,
|
|
593
790
|
action: 'cycle' | 'cancel' | 'resume',
|
|
@@ -647,10 +844,10 @@ events.on('customer.subscription.deleted', (subscription: Subscription) => {
|
|
|
647
844
|
});
|
|
648
845
|
// FIXME: ensure invoices that are open or uncollectible are voided
|
|
649
846
|
|
|
650
|
-
if (subscription.cancelation_details?.
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
});
|
|
847
|
+
if (subscription.cancelation_details?.slash_stake) {
|
|
848
|
+
slashStakeQueue.push({ id: `slash-stake-${subscription.id}`, job: { subscriptionId: subscription.id } });
|
|
849
|
+
} else if (subscription.cancelation_details?.return_stake) {
|
|
850
|
+
returnStakeQueue.push({ id: `return-stake-${subscription.id}`, job: { subscriptionId: subscription.id } });
|
|
654
851
|
}
|
|
655
852
|
});
|
|
656
853
|
|
|
@@ -13,6 +13,7 @@ import sortBy from 'lodash/sortBy';
|
|
|
13
13
|
import uniq from 'lodash/uniq';
|
|
14
14
|
import type { WhereOptions } from 'sequelize';
|
|
15
15
|
|
|
16
|
+
import { MetadataSchema } from '@api/libs/api';
|
|
16
17
|
import { checkPassportForPaymentLink } from '../integrations/blocklet/passport';
|
|
17
18
|
import { handleStripePaymentSucceed } from '../integrations/stripe/handlers/payment-intent';
|
|
18
19
|
import { handleStripeSubscriptionSucceed } from '../integrations/stripe/handlers/subscription';
|
|
@@ -1350,6 +1351,10 @@ router.put('/:id', auth, async (req, res) => {
|
|
|
1350
1351
|
|
|
1351
1352
|
const raw = pick(req.body, ['metadata']);
|
|
1352
1353
|
if (raw.metadata) {
|
|
1354
|
+
const { error: metadataError } = MetadataSchema.validate(raw.metadata);
|
|
1355
|
+
if (metadataError) {
|
|
1356
|
+
return res.status(400).json({ error: metadataError });
|
|
1357
|
+
}
|
|
1353
1358
|
raw.metadata = formatMetadata(raw.metadata);
|
|
1354
1359
|
}
|
|
1355
1360
|
|
|
@@ -2,9 +2,10 @@ import { user } from '@blocklet/sdk/lib/middlewares';
|
|
|
2
2
|
import { Router } from 'express';
|
|
3
3
|
import Joi from 'joi';
|
|
4
4
|
import pick from 'lodash/pick';
|
|
5
|
+
import isEmail from 'validator/es/lib/isEmail';
|
|
5
6
|
|
|
6
7
|
import { getStakeSummaryByDid, getTokenSummaryByDid } from '../integrations/arcblock/stake';
|
|
7
|
-
import { createListParamSchema, getWhereFromKvQuery, getWhereFromQuery } from '../libs/api';
|
|
8
|
+
import { createListParamSchema, getWhereFromKvQuery, getWhereFromQuery, MetadataSchema } from '../libs/api';
|
|
8
9
|
import { authenticate } from '../libs/security';
|
|
9
10
|
import { formatMetadata } from '../libs/util';
|
|
10
11
|
import { Customer } from '../store/models/customer';
|
|
@@ -140,6 +141,27 @@ router.get('/:id/summary', auth, async (req, res) => {
|
|
|
140
141
|
}
|
|
141
142
|
});
|
|
142
143
|
|
|
144
|
+
const updateCustomerSchema = Joi.object({
|
|
145
|
+
metadata: MetadataSchema,
|
|
146
|
+
name: Joi.string().min(2).max(30).empty(''),
|
|
147
|
+
email: Joi.string()
|
|
148
|
+
.custom((value, helpers) => {
|
|
149
|
+
if (!isEmail(value)) {
|
|
150
|
+
return helpers.error('any.invalid');
|
|
151
|
+
}
|
|
152
|
+
return value;
|
|
153
|
+
})
|
|
154
|
+
.empty(''),
|
|
155
|
+
phone: Joi.string().empty(''),
|
|
156
|
+
address: Joi.object({
|
|
157
|
+
country: Joi.string().empty(''),
|
|
158
|
+
state: Joi.string().max(50).empty(''),
|
|
159
|
+
city: Joi.string().max(50).empty(''),
|
|
160
|
+
line1: Joi.string().max(100).empty(''),
|
|
161
|
+
line2: Joi.string().max(100).empty(''),
|
|
162
|
+
postal_code: Joi.string().max(20).empty(''),
|
|
163
|
+
}).empty(''),
|
|
164
|
+
}).unknown(true);
|
|
143
165
|
// eslint-disable-next-line consistent-return
|
|
144
166
|
router.put('/:id', authPortal, async (req, res) => {
|
|
145
167
|
try {
|
|
@@ -149,6 +171,10 @@ router.put('/:id', authPortal, async (req, res) => {
|
|
|
149
171
|
}
|
|
150
172
|
|
|
151
173
|
const raw = pick(req.body, ['metadata', 'name', 'email', 'phone', 'address']);
|
|
174
|
+
const { error } = updateCustomerSchema.validate(raw);
|
|
175
|
+
if (error) {
|
|
176
|
+
return res.status(400).json({ error: error.message });
|
|
177
|
+
}
|
|
152
178
|
if (raw.metadata) {
|
|
153
179
|
raw.metadata = formatMetadata(raw.metadata);
|
|
154
180
|
}
|
|
@@ -6,7 +6,7 @@ import { Op } from 'sequelize';
|
|
|
6
6
|
|
|
7
7
|
import { syncStripeInvoice } from '../integrations/stripe/handlers/invoice';
|
|
8
8
|
import { syncStripePayment } from '../integrations/stripe/handlers/payment-intent';
|
|
9
|
-
import { createListParamSchema, getWhereFromKvQuery } from '../libs/api';
|
|
9
|
+
import { createListParamSchema, getWhereFromKvQuery, MetadataSchema } from '../libs/api';
|
|
10
10
|
import { authenticate } from '../libs/security';
|
|
11
11
|
import { expandLineItems } from '../libs/session';
|
|
12
12
|
import { formatMetadata } from '../libs/util';
|
|
@@ -323,6 +323,10 @@ router.put('/:id', authAdmin, async (req, res) => {
|
|
|
323
323
|
|
|
324
324
|
const raw = pick(req.body, ['metadata']);
|
|
325
325
|
if (raw.metadata) {
|
|
326
|
+
const { error } = MetadataSchema.validate(raw.metadata);
|
|
327
|
+
if (error) {
|
|
328
|
+
return res.status(400).json({ error: error.message });
|
|
329
|
+
}
|
|
326
330
|
raw.metadata = formatMetadata(raw.metadata);
|
|
327
331
|
}
|
|
328
332
|
|
|
@@ -6,7 +6,13 @@ import pick from 'lodash/pick';
|
|
|
6
6
|
import { BN, fromTokenToUnit } from '@ocap/util';
|
|
7
7
|
import { syncStripeInvoice } from '../integrations/stripe/handlers/invoice';
|
|
8
8
|
import { syncStripePayment } from '../integrations/stripe/handlers/payment-intent';
|
|
9
|
-
import {
|
|
9
|
+
import {
|
|
10
|
+
BNPositiveValidator,
|
|
11
|
+
createListParamSchema,
|
|
12
|
+
getWhereFromKvQuery,
|
|
13
|
+
getWhereFromQuery,
|
|
14
|
+
MetadataSchema,
|
|
15
|
+
} from '../libs/api';
|
|
10
16
|
import { authenticate } from '../libs/security';
|
|
11
17
|
import { formatMetadata } from '../libs/util';
|
|
12
18
|
import { paymentQueue } from '../queues/payment';
|
|
@@ -200,6 +206,10 @@ router.put('/:id', authAdmin, async (req, res) => {
|
|
|
200
206
|
|
|
201
207
|
const raw = pick(req.body, ['metadata']);
|
|
202
208
|
if (raw.metadata) {
|
|
209
|
+
const { error: metadataError } = MetadataSchema.validate(raw.metadata);
|
|
210
|
+
if (metadataError) {
|
|
211
|
+
return res.status(400).json({ error: `metadata invalid: ${metadataError.message}` });
|
|
212
|
+
}
|
|
203
213
|
raw.metadata = formatMetadata(raw.metadata);
|
|
204
214
|
}
|
|
205
215
|
|
|
@@ -242,7 +252,7 @@ const refundRequestSchema = Joi.object({
|
|
|
242
252
|
reason: Joi.string()
|
|
243
253
|
.valid('duplicate', 'requested_by_customer', 'requested_by_admin', 'fraudulent', 'expired_uncaptured_charge')
|
|
244
254
|
.required(),
|
|
245
|
-
description: Joi.string().required(),
|
|
255
|
+
description: Joi.string().max(200).required(),
|
|
246
256
|
metadata: Joi.object().optional(),
|
|
247
257
|
});
|
|
248
258
|
|
|
@@ -264,6 +274,11 @@ router.put('/:id/refund', authAdmin, async (req, res) => {
|
|
|
264
274
|
throw new Error('PaymentIntent not found');
|
|
265
275
|
}
|
|
266
276
|
|
|
277
|
+
// @ts-ignore
|
|
278
|
+
if (doc.payment_details?.arcblock?.type === 'slash' && doc.paymentMethod?.type === 'arcblock') {
|
|
279
|
+
throw new Error('PaymentIntent refund not supported for slash payment');
|
|
280
|
+
}
|
|
281
|
+
|
|
267
282
|
const invoice = await Invoice.findByPk(doc.invoice_id);
|
|
268
283
|
await syncStripePaymentAndInvoice(doc, invoice, req);
|
|
269
284
|
// @ts-ignore
|