payment-kit 1.13.159 → 1.13.161

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 (39) hide show
  1. package/api/src/hooks/pre-flight.ts +0 -3
  2. package/api/src/index.ts +4 -2
  3. package/api/src/integrations/stripe/handlers/invoice.ts +6 -0
  4. package/api/src/integrations/stripe/handlers/setup-intent.ts +53 -6
  5. package/api/src/integrations/stripe/handlers/subscription.ts +5 -3
  6. package/api/src/integrations/stripe/resource.ts +12 -4
  7. package/api/src/libs/subscription.ts +12 -1
  8. package/api/src/routes/checkout-sessions.ts +0 -1
  9. package/api/src/routes/connect/change-payment.ts +106 -0
  10. package/api/src/routes/connect/{update.ts → change-plan.ts} +1 -1
  11. package/api/src/routes/connect/setup.ts +5 -3
  12. package/api/src/routes/connect/shared.ts +50 -11
  13. package/api/src/routes/invoices.ts +24 -0
  14. package/api/src/routes/payment-intents.ts +24 -0
  15. package/api/src/routes/refunds.ts +24 -0
  16. package/api/src/routes/subscriptions.ts +254 -6
  17. package/api/src/store/migrate.ts +1 -1
  18. package/api/src/store/models/setup-intent.ts +2 -5
  19. package/blocklet.yml +1 -1
  20. package/package.json +14 -14
  21. package/src/app.tsx +14 -4
  22. package/src/components/metadata/list.tsx +25 -0
  23. package/src/components/price/currency-select.tsx +1 -1
  24. package/src/components/subscription/portal/actions.tsx +4 -6
  25. package/src/libs/util.ts +7 -21
  26. package/src/pages/admin/billing/invoices/detail.tsx +2 -10
  27. package/src/pages/admin/billing/subscriptions/detail.tsx +2 -9
  28. package/src/pages/admin/customers/customers/detail.tsx +3 -3
  29. package/src/pages/admin/payments/intents/detail.tsx +2 -10
  30. package/src/pages/admin/payments/links/detail.tsx +6 -14
  31. package/src/pages/admin/payments/refunds/detail.tsx +2 -10
  32. package/src/pages/admin/products/prices/detail.tsx +6 -13
  33. package/src/pages/admin/products/pricing-tables/detail.tsx +6 -14
  34. package/src/pages/admin/products/products/detail.tsx +6 -14
  35. package/src/pages/customer/invoice/past-due.tsx +49 -15
  36. package/src/pages/customer/subscription/change-payment.tsx +362 -0
  37. package/src/pages/customer/subscription/{update.tsx → change-plan.tsx} +26 -37
  38. package/src/pages/customer/subscription/detail.tsx +19 -7
  39. package/api/src/libs/hooks.ts +0 -25
@@ -2,13 +2,10 @@ import '@blocklet/sdk/lib/error-handler';
2
2
 
3
3
  import dotenv from 'dotenv-flow';
4
4
 
5
- import { ensureSqliteBinaryFile } from '../libs/hooks';
6
-
7
5
  dotenv.config({ silent: true });
8
6
 
9
7
  (async () => {
10
8
  try {
11
- await ensureSqliteBinaryFile();
12
9
  await import('../store/migrate').then((m) => m.default());
13
10
  process.exit(0);
14
11
  } catch (err) {
package/api/src/index.ts CHANGED
@@ -24,11 +24,12 @@ import { startPaymentQueue } from './queues/payment';
24
24
  import { startRefundQueue } from './queues/refund';
25
25
  import { startSubscriptionQueue } from './queues/subscription';
26
26
  import routes from './routes';
27
+ import changePaymentHandlers from './routes/connect/change-payment';
28
+ import changePlanHandlers from './routes/connect/change-plan';
27
29
  import collectHandlers from './routes/connect/collect';
28
30
  import payHandlers from './routes/connect/pay';
29
31
  import setupHandlers from './routes/connect/setup';
30
32
  import subscribeHandlers from './routes/connect/subscribe';
31
- import updateHandlers from './routes/connect/update';
32
33
  import { initialize } from './store/models';
33
34
  import { sequelize } from './store/sequelize';
34
35
 
@@ -56,7 +57,8 @@ handlers.attach(Object.assign({ app: router }, collectHandlers));
56
57
  handlers.attach(Object.assign({ app: router }, payHandlers));
57
58
  handlers.attach(Object.assign({ app: router }, setupHandlers));
58
59
  handlers.attach(Object.assign({ app: router }, subscribeHandlers));
59
- handlers.attach(Object.assign({ app: router }, updateHandlers));
60
+ handlers.attach(Object.assign({ app: router }, changePaymentHandlers));
61
+ handlers.attach(Object.assign({ app: router }, changePlanHandlers));
60
62
 
61
63
  router.use('/api', routes);
62
64
 
@@ -3,6 +3,7 @@ import pick from 'lodash/pick';
3
3
  import pWaitFor from 'p-wait-for';
4
4
  import type Stripe from 'stripe';
5
5
 
6
+ import { getLock } from '../../../libs/lock';
6
7
  import logger from '../../../libs/logger';
7
8
  import {
8
9
  CheckoutSession,
@@ -72,11 +73,15 @@ export async function syncStripeInvoice(invoice: Invoice) {
72
73
  }
73
74
 
74
75
  export async function ensureStripeInvoice(stripeInvoice: any, subscription: Subscription, client: Stripe) {
76
+ const lock = getLock(`mirror-stripe-invoice-${stripeInvoice.id}`);
77
+ await lock.acquire();
78
+
75
79
  const customer = await Customer.findByPk(subscription.customer_id);
76
80
  const checkoutSession = await CheckoutSession.findOne({ where: { subscription_id: subscription.id } });
77
81
 
78
82
  let invoice = await Invoice.findOne({ where: { 'metadata.stripe_id': stripeInvoice.id } });
79
83
  if (invoice) {
84
+ lock.release();
80
85
  return invoice;
81
86
  }
82
87
 
@@ -175,6 +180,7 @@ export async function ensureStripeInvoice(stripeInvoice: any, subscription: Subs
175
180
  })
176
181
  );
177
182
 
183
+ lock.release();
178
184
  return invoice;
179
185
  }
180
186
 
@@ -1,11 +1,9 @@
1
1
  import type Stripe from 'stripe';
2
2
 
3
3
  import logger from '../../../libs/logger';
4
- import { CheckoutSession, Subscription, TEventExpanded } from '../../../store/models';
4
+ import { CheckoutSession, SetupIntent, Subscription, TEventExpanded } from '../../../store/models';
5
5
 
6
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
7
- export async function handleSetupIntentEvent(event: TEventExpanded, _: Stripe) {
8
- const stripeIntentId = event.data.object.id;
6
+ async function handleSubscriptionOnSetupSucceeded(event: TEventExpanded, stripeIntentId: string) {
9
7
  const subscription = await Subscription.findOne({
10
8
  where: { 'payment_details.stripe.setup_intent_id': stripeIntentId },
11
9
  });
@@ -15,8 +13,6 @@ export async function handleSetupIntentEvent(event: TEventExpanded, _: Stripe) {
15
13
  return;
16
14
  }
17
15
 
18
- logger.info('received setup intent event', { id: event.id, type: event.type, subscriptionId: subscription.id });
19
-
20
16
  if (event.type === 'setup_intent.succeeded') {
21
17
  if (subscription.status === 'incomplete') {
22
18
  await subscription.start();
@@ -40,3 +36,54 @@ export async function handleSetupIntentEvent(event: TEventExpanded, _: Stripe) {
40
36
  // FIXME:
41
37
  }
42
38
  }
39
+
40
+ async function handleSetupIntentOnSetupSucceeded(event: TEventExpanded, stripeIntentId: string) {
41
+ const setupIntent = await SetupIntent.findOne({
42
+ where: { 'setup_details.stripe.setup_intent_id': stripeIntentId },
43
+ });
44
+
45
+ if (!setupIntent) {
46
+ logger.warn('local subscription not found for setup intent', { id: event.id, type: event.type, stripeIntentId });
47
+ return;
48
+ }
49
+
50
+ if (event.type === 'setup_intent.succeeded') {
51
+ setupIntent.update({
52
+ status: 'succeeded',
53
+ last_setup_error: null,
54
+ payment_method_types: ['stripe'],
55
+ });
56
+
57
+ if (
58
+ setupIntent.metadata?.subscription_id &&
59
+ setupIntent.metadata?.from_currency &&
60
+ setupIntent.metadata?.to_currency
61
+ ) {
62
+ const subscription = await Subscription.findByPk(setupIntent.metadata.subscription_id);
63
+ if (subscription) {
64
+ await subscription.update({
65
+ currency_id: setupIntent.currency_id,
66
+ default_payment_method_id: setupIntent.payment_method_id,
67
+ payment_settings: {
68
+ payment_method_types: ['stripe'],
69
+ payment_method_options: {},
70
+ },
71
+ });
72
+ logger.info('subscription payment changed to stripe on stripe setup intent succeeded', {
73
+ subscriptionId: subscription.id,
74
+ setupIntentId: setupIntent.id,
75
+ stripeIntentId,
76
+ });
77
+ }
78
+ }
79
+ }
80
+ }
81
+
82
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
83
+ export async function handleSetupIntentEvent(event: TEventExpanded, _: Stripe) {
84
+ const stripeIntentId = event.data.object.id;
85
+ logger.info('received setup intent event', { id: event.id, type: event.type, stripeIntentId });
86
+
87
+ await handleSubscriptionOnSetupSucceeded(event, stripeIntentId);
88
+ await handleSetupIntentOnSetupSucceeded(event, stripeIntentId);
89
+ }
@@ -43,9 +43,11 @@ export async function handleSubscriptionEvent(event: TEventExpanded, _: Stripe)
43
43
  return;
44
44
  }
45
45
 
46
- await subscription.update(
47
- pick(event.data.object, ['cancel_at', 'cancel_at_period_end', 'canceled_at', 'pause_collection'])
48
- );
46
+ const fields = ['cancel_at', 'cancel_at_period_end', 'canceled_at'];
47
+ if (subscription.payment_settings?.payment_method_types?.includes('stripe')) {
48
+ fields.push('pause_collection');
49
+ }
50
+ await subscription.update(pick(event.data.object, fields));
49
51
  return;
50
52
  }
51
53
 
@@ -192,7 +192,8 @@ export async function ensureStripeSubscription(
192
192
  method: PaymentMethod,
193
193
  currency: PaymentCurrency,
194
194
  items: TLineItemExpanded[],
195
- trialInDays: number = 0
195
+ trialInDays: number = 0,
196
+ trialEnds: number = 0
196
197
  ) {
197
198
  const client = method.getStripeClient();
198
199
 
@@ -226,12 +227,11 @@ export async function ensureStripeSubscription(
226
227
  .filter((x) => x.price.type !== 'recurring')
227
228
  .map((x) => ({ price: x.stripePrice.id, quantity: x.quantity }));
228
229
 
229
- stripeSubscription = await client.subscriptions.create({
230
+ const props: any = {
230
231
  currency: currency.symbol.toLowerCase(),
231
232
  customer: customer.id,
232
233
  items: recurringItems,
233
234
  add_invoice_items: onetimeItems,
234
- trial_period_days: trialInDays,
235
235
  payment_behavior: 'default_incomplete',
236
236
  payment_settings: { save_default_payment_method: 'on_subscription' },
237
237
  metadata: {
@@ -239,7 +239,15 @@ export async function ensureStripeSubscription(
239
239
  id: internal.id,
240
240
  },
241
241
  expand: ['latest_invoice.payment_intent', 'pending_setup_intent'],
242
- });
242
+ };
243
+
244
+ if (trialInDays) {
245
+ props.trial_period_days = trialInDays;
246
+ } else if (trialEnds) {
247
+ props.trial_end = trialEnds;
248
+ }
249
+
250
+ stripeSubscription = await client.subscriptions.create(props);
243
251
  logger.info('stripe subscription created', { local: internal.id, remote: stripeSubscription.id });
244
252
 
245
253
  await Promise.all(
@@ -16,7 +16,7 @@ import {
16
16
  } from '../store/models';
17
17
  import dayjs from './dayjs';
18
18
  import logger from './logger';
19
- import { getPriceUintAmountByCurrency, getRecurringPeriod } from './session';
19
+ import { getPriceCurrencyOptions, getPriceUintAmountByCurrency, getRecurringPeriod } from './session';
20
20
  import { getConnectQueryParam } from './util';
21
21
 
22
22
  export function getCustomerSubscriptionPageUrl({
@@ -315,3 +315,14 @@ export function formatSubscriptionProduct(items: TLineItemExpanded[], maxLength
315
315
  names.slice(0, maxLength).join(', ') + (names.length > maxLength ? ` and ${names.length - maxLength} more` : '')
316
316
  );
317
317
  }
318
+
319
+ export async function canChangePaymentMethod(subscriptionId: string) {
320
+ const item = await SubscriptionItem.findOne({ where: { subscription_id: subscriptionId } });
321
+ const expanded = await Price.findOne({ where: { id: item!.price_id } });
322
+ return expanded && getPriceCurrencyOptions(expanded).length > 1;
323
+ }
324
+
325
+ export async function expandSubscriptionItems(subscriptionId: string) {
326
+ const items = await SubscriptionItem.findAll({ where: { subscription_id: subscriptionId } });
327
+ return Price.expand(items.map((x) => x.toJSON()));
328
+ }
@@ -615,7 +615,6 @@ router.put('/:id/submit', user, ensureCheckoutSessionOpen, async (req, res) => {
615
615
  intent: setupIntent.id,
616
616
  });
617
617
  } else {
618
- // ensure payment intent
619
618
  setupIntent = await SetupIntent.create({
620
619
  livemode: !!checkoutSession.livemode,
621
620
  customer_id: customer.id,
@@ -0,0 +1,106 @@
1
+ import { toTypeInfo } from '@arcblock/did';
2
+ import type { Transaction } from '@ocap/client';
3
+ import { fromPublicKey } from '@ocap/wallet';
4
+
5
+ import type { CallbackArgs } from '../../libs/auth';
6
+ import { getGasPayerExtra } from '../../libs/payment';
7
+ import { getTxMetadata } from '../../libs/util';
8
+ import type { TLineItemExpanded } from '../../store/models';
9
+ import { ensureChangePaymentContext, getAuthPrincipalClaim, getDelegationTxClaim } from './shared';
10
+
11
+ export default {
12
+ action: 'change-payment',
13
+ authPrincipal: false,
14
+ claims: {
15
+ authPrincipal: async ({ extraParams }: CallbackArgs) => {
16
+ const { paymentMethod } = await ensureChangePaymentContext(extraParams.subscriptionId);
17
+ return getAuthPrincipalClaim(paymentMethod, 'continue');
18
+ },
19
+ },
20
+ onConnect: async ({ userDid, userPk, extraParams }: CallbackArgs) => {
21
+ const { subscriptionId } = extraParams;
22
+ const { subscription, paymentMethod, paymentCurrency } = await ensureChangePaymentContext(subscriptionId);
23
+
24
+ if (paymentMethod.type === 'arcblock') {
25
+ // @ts-ignore
26
+ const items = subscription!.items as TLineItemExpanded[];
27
+
28
+ return {
29
+ signature: await getDelegationTxClaim({
30
+ mode: 'setup',
31
+ userDid,
32
+ userPk,
33
+ nonce: subscription!.id,
34
+ data: getTxMetadata({ subscriptionId: subscription!.id }),
35
+ paymentCurrency,
36
+ paymentMethod,
37
+ trial: false,
38
+ items,
39
+ }),
40
+ };
41
+ }
42
+
43
+ throw new Error(`Payment method ${paymentMethod.type} not supported`);
44
+ },
45
+
46
+ onAuth: async ({ userDid, userPk, claims, extraParams }: CallbackArgs) => {
47
+ const { subscriptionId } = extraParams;
48
+ const { subscription, setupIntent, paymentCurrency, paymentMethod } = await ensureChangePaymentContext(
49
+ subscriptionId
50
+ );
51
+
52
+ if (paymentMethod.type === 'arcblock') {
53
+ await subscription?.update({
54
+ payment_settings: {
55
+ payment_method_types: ['arcblock'],
56
+ payment_method_options: {
57
+ arcblock: { payer: userDid },
58
+ },
59
+ },
60
+ });
61
+
62
+ const client = paymentMethod.getOcapClient();
63
+ const claim = claims.find((x) => x.type === 'signature');
64
+
65
+ // execute the delegate tx
66
+ const tx: Partial<Transaction> = client.decodeTx(claim.origin);
67
+ tx.signature = claim.sig;
68
+
69
+ // @ts-ignore
70
+ const { buffer } = await client.encodeDelegateTx({ tx });
71
+ const txHash = await client.sendDelegateTx(
72
+ // @ts-ignore
73
+ { tx, wallet: fromPublicKey(userPk, toTypeInfo(userDid)) },
74
+ getGasPayerExtra(buffer)
75
+ );
76
+
77
+ await setupIntent.update({
78
+ status: 'succeeded',
79
+ last_setup_error: null,
80
+ payment_method_types: ['arcblock'],
81
+ payment_method_options: {
82
+ arcblock: { payer: userDid },
83
+ },
84
+ setup_details: {
85
+ arcblock: {
86
+ tx_hash: txHash,
87
+ payer: userDid,
88
+ },
89
+ },
90
+ });
91
+
92
+ await subscription?.update({
93
+ currency_id: paymentCurrency.id,
94
+ default_payment_method_id: paymentMethod.id,
95
+ payment_settings: {
96
+ payment_method_types: [paymentMethod.type],
97
+ payment_method_options: {},
98
+ },
99
+ });
100
+
101
+ return { hash: txHash };
102
+ }
103
+
104
+ throw new Error(`Payment method ${paymentMethod.type} not supported`);
105
+ },
106
+ };
@@ -11,7 +11,7 @@ import type { TLineItemExpanded } from '../../store/models';
11
11
  import { ensureSubscription, getAuthPrincipalClaim, getDelegationTxClaim } from './shared';
12
12
 
13
13
  export default {
14
- action: 'update',
14
+ action: 'change-plan',
15
15
  authPrincipal: false,
16
16
  claims: {
17
17
  authPrincipal: async ({ extraParams }: CallbackArgs) => {
@@ -87,14 +87,16 @@ export default {
87
87
 
88
88
  await setupIntent.update({
89
89
  status: 'succeeded',
90
- payment_method_types: ['arcblock'],
91
90
  last_setup_error: null,
91
+ payment_method_types: ['arcblock'],
92
92
  payment_method_options: {
93
93
  arcblock: { payer: userDid },
94
94
  },
95
95
  setup_details: {
96
- tx_hash: txHash,
97
- payer: userDid,
96
+ arcblock: {
97
+ tx_hash: txHash,
98
+ payer: userDid,
99
+ },
98
100
  },
99
101
  });
100
102
 
@@ -11,12 +11,11 @@ import dayjs from '../../libs/dayjs';
11
11
  import logger from '../../libs/logger';
12
12
  import { getTokenLimitsForDelegation } from '../../libs/payment';
13
13
  import {
14
- expandLineItems,
15
14
  getFastCheckoutAmount,
16
15
  getPriceUintAmountByCurrency,
17
16
  getStatementDescriptor,
18
17
  } from '../../libs/session';
19
- import { getSubscriptionItemPrice } from '../../libs/subscription';
18
+ import { expandSubscriptionItems, getSubscriptionItemPrice } from '../../libs/subscription';
20
19
  import type { TLineItemExpanded } from '../../store/models';
21
20
  import { CheckoutSession } from '../../store/models/checkout-session';
22
21
  import { Customer } from '../../store/models/customer';
@@ -26,7 +25,6 @@ import { PaymentCurrency } from '../../store/models/payment-currency';
26
25
  import { PaymentIntent } from '../../store/models/payment-intent';
27
26
  import { PaymentMethod } from '../../store/models/payment-method';
28
27
  import { Price } from '../../store/models/price';
29
- import { Product } from '../../store/models/product';
30
28
  import { SetupIntent } from '../../store/models/setup-intent';
31
29
  import { Subscription } from '../../store/models/subscription';
32
30
  import { SubscriptionItem } from '../../store/models/subscription-item';
@@ -709,15 +707,8 @@ export async function ensureSubscription(subscriptionId: string): Promise<Result
709
707
  throw new Error(`Payment method ${paymentMethod.type} should not be here`);
710
708
  }
711
709
 
712
- const items = (await SubscriptionItem.findAll({ where: { subscription_id: subscription.id } })).map((x) =>
713
- x.toJSON()
714
- );
715
- const products = (await Product.findAll()).map((x) => x.toJSON());
716
- const prices = (await Price.findAll()).map((x) => x.toJSON());
717
- // @ts-ignore
718
- expandLineItems(items, products, prices);
719
710
  // @ts-ignore
720
- subscription.items = await Price.expand(items);
711
+ subscription.items = await expandSubscriptionItems(subscription.id);
721
712
 
722
713
  return {
723
714
  // @ts-ignore
@@ -729,3 +720,51 @@ export async function ensureSubscription(subscriptionId: string): Promise<Result
729
720
  invoice,
730
721
  };
731
722
  }
723
+
724
+ export async function ensureChangePaymentContext(subscriptionId: string) {
725
+ const subscription = await Subscription.findByPk(subscriptionId);
726
+ if (!subscription) {
727
+ throw new Error(`Subscription not found: ${subscriptionId}`);
728
+ }
729
+
730
+ const context = subscription.metadata?.changePayment || {};
731
+ if (!context || !context.setup_intent_id) {
732
+ throw new Error(`Change payment context not found: ${subscriptionId}`);
733
+ }
734
+
735
+ const setupIntent = await SetupIntent.findByPk(context.setup_intent_id);
736
+ if (!setupIntent) {
737
+ throw new Error(`SetupIntent not found for subscription payment change ${subscriptionId}`);
738
+ }
739
+ if (setupIntent.status === 'succeeded') {
740
+ throw new Error(`SetupIntent completed for subscription payment change ${subscriptionId}`);
741
+ }
742
+
743
+ const paymentCurrencyId = setupIntent.currency_id;
744
+ const paymentMethodId = setupIntent.payment_method_id;
745
+
746
+ const [paymentMethod, paymentCurrency] = await Promise.all([
747
+ PaymentMethod.findByPk(paymentMethodId),
748
+ PaymentCurrency.findByPk(paymentCurrencyId),
749
+ ]);
750
+ if (!paymentMethod) {
751
+ throw new Error(`Payment method not found for SetupIntent ${setupIntent.id}`);
752
+ }
753
+ if (!paymentCurrency) {
754
+ throw new Error(`Payment currency not found for SetupIntent ${setupIntent.id}`);
755
+ }
756
+
757
+ if (['arcblock', 'ethereum'].includes(paymentMethod.type) === false) {
758
+ throw new Error(`Payment method ${paymentMethod.type} should not be here`);
759
+ }
760
+
761
+ // @ts-ignore
762
+ subscription.items = await expandSubscriptionItems(subscription.id);
763
+
764
+ return {
765
+ subscription,
766
+ setupIntent,
767
+ paymentMethod,
768
+ paymentCurrency,
769
+ };
770
+ }
@@ -1,12 +1,14 @@
1
1
  import { isValid } from '@arcblock/did';
2
2
  import { Router } from 'express';
3
3
  import Joi from 'joi';
4
+ import pick from 'lodash/pick';
4
5
  import type { WhereOptions } from 'sequelize';
5
6
 
6
7
  import { syncStripeInvoice } from '../integrations/stripe/handlers/invoice';
7
8
  import { getWhereFromKvQuery } from '../libs/api';
8
9
  import { authenticate } from '../libs/security';
9
10
  import { expandLineItems } from '../libs/session';
11
+ import { formatMetadata } from '../libs/util';
10
12
  import { Customer } from '../store/models/customer';
11
13
  import { Invoice } from '../store/models/invoice';
12
14
  import { InvoiceItem } from '../store/models/invoice-item';
@@ -18,6 +20,7 @@ import { Product } from '../store/models/product';
18
20
  import { Subscription } from '../store/models/subscription';
19
21
 
20
22
  const router = Router();
23
+ const authAdmin = authenticate<Subscription>({ component: true, roles: ['owner', 'admin'] });
21
24
  const authMine = authenticate<Subscription>({ component: true, roles: ['owner', 'admin'], mine: true });
22
25
  const authPortal = authenticate<Invoice>({
23
26
  component: true,
@@ -177,4 +180,25 @@ router.get('/:id', authPortal, async (req, res) => {
177
180
  }
178
181
  });
179
182
 
183
+ // eslint-disable-next-line consistent-return
184
+ router.put('/:id', authAdmin, async (req, res) => {
185
+ try {
186
+ const doc = await Invoice.findByPk(req.params.id as string);
187
+ if (!doc) {
188
+ return res.status(404).json({ error: 'Invoice not found' });
189
+ }
190
+
191
+ const raw = pick(req.body, ['metadata']);
192
+ if (raw.metadata) {
193
+ raw.metadata = formatMetadata(raw.metadata);
194
+ }
195
+
196
+ await doc.update(raw);
197
+ res.json(doc);
198
+ } catch (err) {
199
+ console.error(err);
200
+ res.json(null);
201
+ }
202
+ });
203
+
180
204
  export default router;
@@ -1,11 +1,13 @@
1
1
  import { isValid } from '@arcblock/did';
2
2
  import { Router } from 'express';
3
3
  import Joi from 'joi';
4
+ import pick from 'lodash/pick';
4
5
  import type { WhereOptions } from 'sequelize';
5
6
 
6
7
  import { syncStripPayment } from '../integrations/stripe/handlers/payment-intent';
7
8
  import { getWhereFromKvQuery, getWhereFromQuery } from '../libs/api';
8
9
  import { authenticate } from '../libs/security';
10
+ import { formatMetadata } from '../libs/util';
9
11
  import { CheckoutSession } from '../store/models/checkout-session';
10
12
  import { Customer } from '../store/models/customer';
11
13
  import { Invoice } from '../store/models/invoice';
@@ -15,6 +17,7 @@ import { PaymentMethod } from '../store/models/payment-method';
15
17
  import { Subscription } from '../store/models/subscription';
16
18
 
17
19
  const router = Router();
20
+ const authAdmin = authenticate<Subscription>({ component: true, roles: ['owner', 'admin'] });
18
21
  const authMine = authenticate<Subscription>({ component: true, roles: ['owner', 'admin'], mine: true });
19
22
  const authPortal = authenticate<PaymentIntent>({
20
23
  component: true,
@@ -178,4 +181,25 @@ router.get('/:id', authPortal, async (req, res) => {
178
181
  }
179
182
  });
180
183
 
184
+ // eslint-disable-next-line consistent-return
185
+ router.put('/:id', authAdmin, async (req, res) => {
186
+ try {
187
+ const doc = await PaymentIntent.findByPk(req.params.id as string);
188
+ if (!doc) {
189
+ return res.status(404).json({ error: 'PaymentIntent not found' });
190
+ }
191
+
192
+ const raw = pick(req.body, ['metadata']);
193
+ if (raw.metadata) {
194
+ raw.metadata = formatMetadata(raw.metadata);
195
+ }
196
+
197
+ await doc.update(raw);
198
+ res.json(doc);
199
+ } catch (err) {
200
+ console.error(err);
201
+ res.json(null);
202
+ }
203
+ });
204
+
181
205
  export default router;
@@ -1,9 +1,11 @@
1
1
  /* eslint-disable consistent-return */
2
2
  import { Router } from 'express';
3
3
  import Joi from 'joi';
4
+ import pick from 'lodash/pick';
4
5
  import type { WhereOptions } from 'sequelize';
5
6
 
6
7
  import { authenticate } from '../libs/security';
8
+ import { formatMetadata } from '../libs/util';
7
9
  import {
8
10
  Customer,
9
11
  Invoice,
@@ -15,6 +17,7 @@ import {
15
17
  } from '../store/models';
16
18
 
17
19
  const router = Router();
20
+ const authAdmin = authenticate<Invoice>({ component: true, roles: ['owner', 'admin'] });
18
21
  const auth = authenticate<Invoice>({
19
22
  component: true,
20
23
  roles: ['owner', 'admin'],
@@ -119,4 +122,25 @@ router.get('/:id', auth, async (req, res) => {
119
122
  return res.status(404).json(null);
120
123
  });
121
124
 
125
+ // eslint-disable-next-line consistent-return
126
+ router.put('/:id', authAdmin, async (req, res) => {
127
+ try {
128
+ const doc = await Refund.findByPk(req.params.id as string);
129
+ if (!doc) {
130
+ return res.status(404).json({ error: 'Refund not found' });
131
+ }
132
+
133
+ const raw = pick(req.body, ['metadata']);
134
+ if (raw.metadata) {
135
+ raw.metadata = formatMetadata(raw.metadata);
136
+ }
137
+
138
+ await doc.update(raw);
139
+ res.json(doc);
140
+ } catch (err) {
141
+ console.error(err);
142
+ res.json(null);
143
+ }
144
+ });
145
+
122
146
  export default router;