payment-kit 1.14.30 → 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.
Files changed (58) hide show
  1. package/api/src/index.ts +4 -0
  2. package/api/src/libs/api.ts +23 -0
  3. package/api/src/libs/subscription.ts +32 -0
  4. package/api/src/queues/refund.ts +38 -1
  5. package/api/src/queues/subscription.ts +218 -21
  6. package/api/src/routes/checkout-sessions.ts +5 -0
  7. package/api/src/routes/customers.ts +27 -1
  8. package/api/src/routes/invoices.ts +5 -1
  9. package/api/src/routes/payment-intents.ts +17 -2
  10. package/api/src/routes/payment-links.ts +105 -3
  11. package/api/src/routes/payouts.ts +5 -1
  12. package/api/src/routes/prices.ts +19 -3
  13. package/api/src/routes/pricing-table.ts +79 -2
  14. package/api/src/routes/products.ts +24 -8
  15. package/api/src/routes/refunds.ts +7 -4
  16. package/api/src/routes/subscription-items.ts +5 -1
  17. package/api/src/routes/subscriptions.ts +25 -5
  18. package/api/src/routes/webhook-endpoints.ts +5 -1
  19. package/api/src/store/models/subscription.ts +1 -0
  20. package/api/tests/libs/api.spec.ts +72 -1
  21. package/api/third.d.ts +2 -0
  22. package/blocklet.yml +1 -1
  23. package/package.json +19 -18
  24. package/src/components/customer/form.tsx +53 -0
  25. package/src/components/filter-toolbar.tsx +1 -1
  26. package/src/components/invoice/list.tsx +8 -8
  27. package/src/components/invoice/table.tsx +42 -36
  28. package/src/components/metadata/form.tsx +24 -3
  29. package/src/components/payment-intent/actions.tsx +17 -5
  30. package/src/components/payment-link/after-pay.tsx +46 -4
  31. package/src/components/payouts/list.tsx +1 -1
  32. package/src/components/price/form.tsx +14 -2
  33. package/src/components/pricing-table/payment-settings.tsx +45 -4
  34. package/src/components/product/features.tsx +16 -2
  35. package/src/components/product/form.tsx +28 -4
  36. package/src/components/subscription/actions/cancel.tsx +10 -0
  37. package/src/components/subscription/description.tsx +2 -2
  38. package/src/components/subscription/items/index.tsx +3 -2
  39. package/src/components/subscription/portal/cancel.tsx +12 -1
  40. package/src/components/subscription/portal/list.tsx +6 -5
  41. package/src/locales/en.tsx +6 -1
  42. package/src/locales/zh.tsx +6 -1
  43. package/src/pages/admin/billing/invoices/detail.tsx +17 -2
  44. package/src/pages/admin/billing/subscriptions/detail.tsx +4 -0
  45. package/src/pages/admin/customers/customers/detail.tsx +4 -0
  46. package/src/pages/admin/customers/customers/index.tsx +1 -1
  47. package/src/pages/admin/payments/intents/detail.tsx +4 -0
  48. package/src/pages/admin/payments/payouts/detail.tsx +4 -0
  49. package/src/pages/admin/payments/refunds/detail.tsx +4 -0
  50. package/src/pages/admin/products/links/detail.tsx +4 -0
  51. package/src/pages/admin/products/prices/detail.tsx +4 -0
  52. package/src/pages/admin/products/pricing-tables/detail.tsx +4 -0
  53. package/src/pages/admin/products/products/detail.tsx +4 -0
  54. package/src/pages/checkout/pricing-table.tsx +9 -3
  55. package/src/pages/customer/index.tsx +28 -17
  56. package/src/pages/customer/invoice/detail.tsx +27 -16
  57. package/src/pages/customer/invoice/past-due.tsx +3 -2
  58. 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();
@@ -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
+ }
@@ -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: toStakeAddress(customer.did, wallet.address, refund.subscription_id),
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 staked = state.tokens.find((x: any) => x.address === currency.contract);
354
- const revoked = state.revokedTokens.find((x: any) => x.address === currency.contract);
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?.return_stake) {
651
- ensureReturnStake(subscription).catch((err) => {
652
- logger.error('ensureReturnStake failed', { error: err, subscription: subscription.id });
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 { BNPositiveValidator, createListParamSchema, getWhereFromKvQuery, getWhereFromQuery } from '../libs/api';
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