payment-kit 1.13.210 → 1.13.211

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 (46) hide show
  1. package/api/src/libs/api.ts +2 -2
  2. package/api/src/libs/session.ts +90 -4
  3. package/api/src/queues/payment.ts +61 -1
  4. package/api/src/routes/checkout-sessions.ts +61 -10
  5. package/api/src/routes/connect/collect.ts +44 -37
  6. package/api/src/routes/connect/pay.ts +40 -29
  7. package/api/src/routes/connect/setup.ts +39 -33
  8. package/api/src/routes/connect/shared.ts +3 -1
  9. package/api/src/routes/donations.ts +157 -0
  10. package/api/src/routes/index.ts +4 -0
  11. package/api/src/routes/payment-intents.ts +2 -2
  12. package/api/src/routes/payment-links.ts +8 -3
  13. package/api/src/routes/payouts.ts +151 -0
  14. package/api/src/routes/products.ts +24 -6
  15. package/api/src/routes/usage-records.ts +6 -3
  16. package/api/src/store/migrations/20240408-payout.ts +36 -0
  17. package/api/src/store/models/checkout-session.ts +5 -0
  18. package/api/src/store/models/customer.ts +6 -1
  19. package/api/src/store/models/index.ts +12 -0
  20. package/api/src/store/models/payment-intent.ts +38 -26
  21. package/api/src/store/models/payment-link.ts +8 -1
  22. package/api/src/store/models/payout.ts +243 -0
  23. package/api/src/store/models/types.ts +39 -0
  24. package/api/tests/libs/session.spec.ts +101 -0
  25. package/blocklet.yml +1 -1
  26. package/package.json +17 -16
  27. package/src/components/info-card.tsx +5 -5
  28. package/src/components/invoice/list.tsx +2 -0
  29. package/src/components/invoice/table.tsx +1 -1
  30. package/src/components/payment-intent/list.tsx +2 -0
  31. package/src/components/payouts/actions.tsx +43 -0
  32. package/src/components/payouts/list.tsx +255 -0
  33. package/src/components/refund/list.tsx +2 -0
  34. package/src/components/subscription/list.tsx +2 -0
  35. package/src/libs/util.ts +4 -1
  36. package/src/locales/en.tsx +7 -0
  37. package/src/locales/zh.tsx +6 -0
  38. package/src/pages/admin/customers/customers/index.tsx +2 -2
  39. package/src/pages/admin/payments/index.tsx +7 -0
  40. package/src/pages/admin/payments/intents/detail.tsx +7 -0
  41. package/src/pages/admin/payments/payouts/detail.tsx +204 -0
  42. package/src/pages/admin/payments/payouts/index.tsx +5 -0
  43. package/src/pages/admin/products/links/index.tsx +2 -2
  44. package/src/pages/admin/products/prices/detail.tsx +2 -1
  45. package/src/pages/admin/products/pricing-tables/index.tsx +2 -2
  46. package/src/pages/admin/products/products/index.tsx +2 -2
@@ -99,42 +99,48 @@ export default {
99
99
  }
100
100
 
101
101
  if (paymentMethod.type === 'arcblock') {
102
- const paymentSettings = {
103
- payment_method_types: ['arcblock'],
104
- payment_method_options: {
105
- arcblock: { payer: userDid },
106
- },
107
- };
108
- await setupIntent.update({ status: 'processing' });
109
- await subscription.update({ payment_settings: paymentSettings });
110
- if (invoice) {
111
- await invoice.update({ payment_settings: paymentSettings });
112
- }
102
+ try {
103
+ const paymentSettings = {
104
+ payment_method_types: ['arcblock'],
105
+ payment_method_options: {
106
+ arcblock: { payer: userDid },
107
+ },
108
+ };
109
+ await setupIntent.update({ status: 'processing' });
110
+ await subscription.update({ payment_settings: paymentSettings });
111
+ if (invoice) {
112
+ await invoice.update({ payment_settings: paymentSettings });
113
+ }
113
114
 
114
- const paymentDetails = await executeOcapTransactions(userDid, userPk, claims, paymentMethod);
115
- await setupIntent.update({
116
- status: 'succeeded',
117
- last_setup_error: null,
118
- setup_details: { arcblock: paymentDetails },
119
- ...paymentSettings,
120
- });
121
- await subscription.update({
122
- status: subscription.trial_end ? 'trialing' : 'active',
123
- payment_details: { arcblock: paymentDetails },
124
- });
115
+ const paymentDetails = await executeOcapTransactions(userDid, userPk, claims, paymentMethod);
116
+ await setupIntent.update({
117
+ status: 'succeeded',
118
+ last_setup_error: null,
119
+ setup_details: { arcblock: paymentDetails },
120
+ ...paymentSettings,
121
+ });
122
+ await subscription.update({
123
+ status: subscription.trial_end ? 'trialing' : 'active',
124
+ payment_details: { arcblock: paymentDetails },
125
+ });
125
126
 
126
- await checkoutSession.update({ status: 'complete', payment_status: 'paid' });
127
- if (invoice) {
128
- invoiceQueue.pushAndWait({ id: invoice.id, job: { invoiceId: invoice.id, retryOnError: false } });
129
- }
130
- await addSubscriptionJob(subscription, 'cycle', false, subscription.trial_end);
131
- logger.info('CheckoutSession updated on setup done', {
132
- checkoutSession: checkoutSession.id,
133
- setupIntent: setupIntent.id,
134
- paymentDetails,
135
- });
127
+ await checkoutSession.update({ status: 'complete', payment_status: 'paid' });
128
+ if (invoice) {
129
+ invoiceQueue.pushAndWait({ id: invoice.id, job: { invoiceId: invoice.id, retryOnError: false } });
130
+ }
131
+ await addSubscriptionJob(subscription, 'cycle', false, subscription.trial_end);
132
+ logger.info('CheckoutSession updated on setup done', {
133
+ checkoutSession: checkoutSession.id,
134
+ setupIntent: setupIntent.id,
135
+ paymentDetails,
136
+ });
136
137
 
137
- return { hash: paymentDetails.tx_hash };
138
+ return { hash: paymentDetails.tx_hash };
139
+ } catch (err) {
140
+ logger.error('Failed to finalize setup', { setupIntent: setupIntent.id, error: err });
141
+ await setupIntent.update({ status: 'requires_capture' });
142
+ return {};
143
+ }
138
144
  }
139
145
 
140
146
  throw new Error(`Payment method ${paymentMethod.type} not supported`);
@@ -454,7 +454,9 @@ export async function ensureInvoiceAndItems({
454
454
 
455
455
  return {
456
456
  price,
457
- amount: new BN(getPriceUintAmountByCurrency(price, props.currency_id)).mul(new BN(x.quantity)).toString(),
457
+ amount:
458
+ x.custom_amount ||
459
+ new BN(getPriceUintAmountByCurrency(price, props.currency_id)).mul(new BN(x.quantity)).toString(),
458
460
  // @ts-ignore
459
461
  description: price.product.name,
460
462
  period: undefined,
@@ -0,0 +1,157 @@
1
+ import { Joi } from '@arcblock/validator';
2
+ import { Router } from 'express';
3
+
4
+ import { createListParamSchema } from '../libs/api';
5
+ import logger from '../libs/logger';
6
+ import { CheckoutSession } from '../store/models/checkout-session';
7
+ import { Customer } from '../store/models/customer';
8
+ import { PaymentLink } from '../store/models/payment-link';
9
+ import { PaymentMethod } from '../store/models/payment-method';
10
+ import { Price } from '../store/models/price';
11
+ import type { DonationSettings } from '../store/models/types';
12
+ import { createPaymentLink } from './payment-links';
13
+ import { createProductAndPrices } from './products';
14
+
15
+ const router = Router();
16
+
17
+ // FIXME: add more custom validation here for amount
18
+ const donationSchema = Joi.object<DonationSettings>({
19
+ target: Joi.string().max(128).required(),
20
+ title: Joi.string().required(),
21
+ description: Joi.string().required(),
22
+ reference: Joi.string().required(),
23
+ beneficiaries: Joi.array()
24
+ .items(
25
+ Joi.object({
26
+ address: Joi.DID().required(),
27
+ share: Joi.number().positive().required(),
28
+ memo: Joi.string().max(64).optional(),
29
+ })
30
+ )
31
+ .max(8)
32
+ .optional()
33
+ .default([]),
34
+ amount: Joi.object({
35
+ presets: Joi.array().items(Joi.number().positive()).optional().default([]),
36
+ preset: Joi.number().positive().optional(),
37
+ minimum: Joi.number().positive().optional(),
38
+ maximum: Joi.number().positive().optional(),
39
+ custom: Joi.boolean().optional().default(true),
40
+ }),
41
+ message: Joi.object({
42
+ success: Joi.string().optional(),
43
+ summary: Joi.string().optional(),
44
+ }),
45
+ });
46
+ // prepare donation payment links
47
+ router.post('/', async (req, res) => {
48
+ try {
49
+ const payload = await donationSchema.validateAsync(req.body, { stripUnknown: true, convert: true });
50
+ const link = await PaymentLink.findOne({ where: { 'donation_settings.target': payload.target } });
51
+ if (link) {
52
+ await link.update({
53
+ name: payload.title,
54
+ submit_type: 'donate',
55
+ donation_settings: payload,
56
+ after_completion: {
57
+ type: 'hosted_confirmation',
58
+ hosted_confirmation: {
59
+ custom_message: payload.message?.success || '',
60
+ },
61
+ },
62
+ });
63
+ res.json(link.toJSON());
64
+ return;
65
+ }
66
+
67
+ let price = await Price.findByPkOrLookupKey(payload.target);
68
+ if (!price) {
69
+ const result = await createProductAndPrices({
70
+ type: 'service',
71
+ livemode: req.livemode,
72
+ name: payload.title,
73
+ description: payload.description,
74
+ currency_id: req.currency.id,
75
+ prices: [
76
+ {
77
+ type: 'one_time',
78
+ unit_amount: '0',
79
+ billing_schema: 'per_unit',
80
+ lookup_key: payload.target,
81
+ custom_unit_amount: {
82
+ presets: payload.amount.presets || [],
83
+ preset: payload.amount.preset || null,
84
+ maximum: payload.amount.maximum || null,
85
+ minimum: payload.amount.minimum || '0',
86
+ },
87
+ },
88
+ ],
89
+ metadata: {},
90
+ });
91
+
92
+ price = result.prices[0] as Price;
93
+ }
94
+
95
+ const result = await createPaymentLink({
96
+ livemode: !!req.livemode,
97
+ created_via: req.user?.via,
98
+ currency_id: req.currency.id,
99
+ name: payload.title,
100
+ submit_type: 'donate',
101
+ line_items: [{ price_id: price.id, quantity: 1 }],
102
+ donation_settings: payload,
103
+ after_completion: {
104
+ type: 'hosted_confirmation',
105
+ hosted_confirmation: {
106
+ custom_message: payload.message?.success || '',
107
+ },
108
+ },
109
+ });
110
+ res.json(result);
111
+ } catch (err) {
112
+ logger.error('prepare payment link for donation', err);
113
+ res.status(400).json({ error: err.message });
114
+ }
115
+ });
116
+
117
+ // get donations by target
118
+ const paginationSchema = createListParamSchema<{ target: string }>({ target: Joi.string().required() }, 20);
119
+ router.get('/', async (req, res) => {
120
+ try {
121
+ const { page, pageSize, target } = await paginationSchema.validateAsync(req.query, {
122
+ convert: true,
123
+ stripUnknown: true,
124
+ });
125
+ const { rows, count } = await CheckoutSession.findAndCountAll({
126
+ where: { payment_link_id: target, status: 'complete' },
127
+ attributes: [
128
+ 'id',
129
+ 'customer_id',
130
+ 'customer_did',
131
+ 'amount_total',
132
+ 'payment_intent_id',
133
+ 'payment_details',
134
+ 'created_at',
135
+ 'updated_at',
136
+ ],
137
+ order: [['created_at', 'DESC']],
138
+ offset: (page - 1) * pageSize,
139
+ include: [{ model: Customer, as: 'customer', attributes: ['id', 'did', 'name'] }],
140
+ limit: pageSize,
141
+ });
142
+
143
+ const method = await PaymentMethod.findByPk(req.currency.payment_method_id);
144
+
145
+ res.json({
146
+ supporters: rows,
147
+ currency: req.currency,
148
+ method,
149
+ total: count,
150
+ paging: { page, pageSize },
151
+ });
152
+ } catch (err) {
153
+ res.status(400).json({ error: err.message, supporters: [] });
154
+ }
155
+ });
156
+
157
+ export default router;
@@ -3,6 +3,7 @@ import { Router } from 'express';
3
3
  import { PaymentCurrency } from '../store/models/payment-currency';
4
4
  import checkoutSessions from './checkout-sessions';
5
5
  import customers from './customers';
6
+ import donations from './donations';
6
7
  import events from './events';
7
8
  import stripe from './integrations/stripe';
8
9
  import invoices from './invoices';
@@ -11,6 +12,7 @@ import paymentCurrencies from './payment-currencies';
11
12
  import paymentIntents from './payment-intents';
12
13
  import paymentLinks from './payment-links';
13
14
  import paymentMethods from './payment-methods';
15
+ import payouts from './payouts';
14
16
  import prices from './prices';
15
17
  import pricingTables from './pricing-table';
16
18
  import products from './products';
@@ -46,6 +48,7 @@ router.use(async (req, _, next) => {
46
48
 
47
49
  router.use('/checkout-sessions', checkoutSessions);
48
50
  router.use('/customers', customers);
51
+ router.use('/donations', donations);
49
52
  router.use('/events', events);
50
53
  router.use('/invoices', invoices);
51
54
  router.use('/integrations/stripe', stripe);
@@ -57,6 +60,7 @@ router.use('/payment-currencies', paymentCurrencies);
57
60
  router.use('/prices', prices);
58
61
  router.use('/pricing-tables', pricingTables);
59
62
  router.use('/products', products);
63
+ router.use('/payouts', payouts);
60
64
  router.use('/redirect', redirect);
61
65
  router.use('/refunds', refunds);
62
66
  router.use('/settings', settings);
@@ -17,8 +17,8 @@ import { PaymentMethod } from '../store/models/payment-method';
17
17
  import { Subscription } from '../store/models/subscription';
18
18
 
19
19
  const router = Router();
20
- const authAdmin = authenticate<Subscription>({ component: true, roles: ['owner', 'admin'] });
21
- const authMine = authenticate<Subscription>({ component: true, roles: ['owner', 'admin'], mine: true });
20
+ const authAdmin = authenticate<PaymentIntent>({ component: true, roles: ['owner', 'admin'] });
21
+ const authMine = authenticate<PaymentIntent>({ component: true, roles: ['owner', 'admin'], mine: true });
22
22
  const authPortal = authenticate<PaymentIntent>({
23
23
  component: true,
24
24
  roles: ['owner', 'admin'],
@@ -1,5 +1,5 @@
1
+ import { Joi } from '@arcblock/validator';
1
2
  import { Router } from 'express';
2
- import Joi from 'joi';
3
3
  import pick from 'lodash/pick';
4
4
  import type { WhereOptions } from 'sequelize';
5
5
 
@@ -31,7 +31,7 @@ const formatBeforeSave = (payload: any) => {
31
31
  terms_of_service: 'none',
32
32
  },
33
33
  invoice_creation: {
34
- enabled: false,
34
+ enabled: true,
35
35
  },
36
36
  phone_number_collection: {
37
37
  enabled: false,
@@ -47,6 +47,7 @@ const formatBeforeSave = (payload: any) => {
47
47
  },
48
48
  submit_type: 'pay',
49
49
  cross_sell_behavior: 'auto',
50
+ donation_settings: null,
50
51
  },
51
52
  pick(payload, [
52
53
  'name',
@@ -64,6 +65,7 @@ const formatBeforeSave = (payload: any) => {
64
65
  'subscription_data',
65
66
  'nft_mint_settings',
66
67
  'cross_sell_behavior',
68
+ 'donation_settings',
67
69
  'metadata',
68
70
  ])
69
71
  );
@@ -110,6 +112,10 @@ export async function createPaymentLink(payload: any) {
110
112
  }
111
113
 
112
114
  const items = await Price.expand(raw.line_items);
115
+ if (items.find((x) => x.price.custom_unit_amount) && items.length > 1) {
116
+ throw new Error('Multiple items with custom unit amount are not supported in payment link');
117
+ }
118
+
113
119
  for (let i = 0; i < items.length; i++) {
114
120
  const result = isLineItemAligned(items, i);
115
121
  if (result.currency === false) {
@@ -128,7 +134,6 @@ export async function createPaymentLink(payload: any) {
128
134
  }
129
135
 
130
136
  // FIXME: @wangshijun use schema validation
131
- // eslint-disable-next-line consistent-return
132
137
  router.post('/', auth, async (req, res) => {
133
138
  try {
134
139
  const result = await createPaymentLink({
@@ -0,0 +1,151 @@
1
+ import { isValid } from '@arcblock/did';
2
+ import { Router } from 'express';
3
+ import Joi from 'joi';
4
+ import pick from 'lodash/pick';
5
+
6
+ import { createListParamSchema, getWhereFromKvQuery } from '../libs/api';
7
+ import { authenticate } from '../libs/security';
8
+ import { formatMetadata } from '../libs/util';
9
+ import { Customer } from '../store/models/customer';
10
+ import { PaymentCurrency } from '../store/models/payment-currency';
11
+ import { PaymentIntent } from '../store/models/payment-intent';
12
+ import { PaymentMethod } from '../store/models/payment-method';
13
+ import { Payout } from '../store/models/payout';
14
+
15
+ const router = Router();
16
+ const authAdmin = authenticate<Payout>({ component: true, roles: ['owner', 'admin'] });
17
+ const authMine = authenticate<Payout>({ component: true, roles: ['owner', 'admin'], mine: true });
18
+ const authPortal = authenticate<Payout>({
19
+ component: true,
20
+ roles: ['owner', 'admin'],
21
+ record: {
22
+ // @ts-ignore
23
+ model: Payout,
24
+ field: 'customer_id',
25
+ },
26
+ });
27
+
28
+ // list payment intents
29
+ const paginationSchema = createListParamSchema<{
30
+ status?: string;
31
+ payment_intent_id?: string;
32
+ customer_id?: string;
33
+ customer_did?: string;
34
+ currency_id?: string;
35
+ }>({
36
+ status: Joi.string().empty(''),
37
+ payment_intent_id: Joi.string().empty(''),
38
+ customer_id: Joi.string().empty(''),
39
+ customer_did: Joi.string().empty(''),
40
+ currency_id: Joi.string().empty(''),
41
+ });
42
+ router.get('/', authMine, async (req, res) => {
43
+ const { page, pageSize, status, livemode, ...query } = await paginationSchema.validateAsync(req.query, {
44
+ stripUnknown: false,
45
+ allowUnknown: true,
46
+ });
47
+ const where = getWhereFromKvQuery(query.q);
48
+
49
+ if (status) {
50
+ where.status = status
51
+ .split(',')
52
+ .map((x) => x.trim())
53
+ .filter(Boolean);
54
+ }
55
+ if (query.customer_id) {
56
+ where.customer_id = query.customer_id;
57
+ }
58
+ if (query.customer_did && isValid(query.customer_did)) {
59
+ const customer = await Customer.findOne({ where: { did: query.customer_did } });
60
+ if (customer) {
61
+ where.customer_id = customer.id;
62
+ } else {
63
+ res.json({ count: 0, list: [] });
64
+ return;
65
+ }
66
+ }
67
+ if (query.payment_intent_id) {
68
+ where.payment_intent_id = query.payment_intent_id;
69
+ }
70
+
71
+ if (query.currency_id) {
72
+ where.currency_id = query.currency_id;
73
+ }
74
+
75
+ if (typeof livemode === 'boolean') {
76
+ where.livemode = livemode;
77
+ }
78
+
79
+ Object.keys(query)
80
+ .filter((x) => x.startsWith('metadata.'))
81
+ .forEach((key: string) => {
82
+ // @ts-ignore
83
+ where[key] = query[key];
84
+ });
85
+
86
+ try {
87
+ const { rows: list, count } = await Payout.findAndCountAll({
88
+ where,
89
+ order: [['created_at', query.o === 'asc' ? 'ASC' : 'DESC']],
90
+ offset: (page - 1) * pageSize,
91
+ limit: pageSize,
92
+ include: [
93
+ { model: PaymentCurrency, as: 'paymentCurrency' },
94
+ { model: PaymentMethod, as: 'paymentMethod' },
95
+ { model: PaymentIntent, as: 'paymentIntent' },
96
+ { model: Customer, as: 'customer' },
97
+ ],
98
+ });
99
+
100
+ res.json({ count, list, paging: { page, pageSize } });
101
+ } catch (err) {
102
+ console.error(err);
103
+ res.json({ count: 0, list: [], paging: { page, pageSize } });
104
+ }
105
+ });
106
+
107
+ router.get('/:id', authPortal, async (req, res) => {
108
+ try {
109
+ const doc = await Payout.findOne({
110
+ where: { id: req.params.id },
111
+ include: [
112
+ { model: PaymentCurrency, as: 'paymentCurrency' },
113
+ { model: PaymentMethod, as: 'paymentMethod' },
114
+ { model: PaymentIntent, as: 'paymentIntent' },
115
+ { model: Customer, as: 'customer' },
116
+ ],
117
+ });
118
+
119
+ if (doc) {
120
+ res.json({ ...doc.toJSON() });
121
+ } else {
122
+ res.status(404).json(null);
123
+ }
124
+ } catch (err) {
125
+ console.error(err);
126
+ res.status(500).json({ error: `Failed to get payout: ${err.message}` });
127
+ }
128
+ });
129
+
130
+ // eslint-disable-next-line consistent-return
131
+ router.put('/:id', authAdmin, async (req, res) => {
132
+ try {
133
+ const doc = await Payout.findByPk(req.params.id as string);
134
+ if (!doc) {
135
+ return res.status(404).json({ error: 'Payout not found' });
136
+ }
137
+
138
+ const raw = pick(req.body, ['metadata']);
139
+ if (raw.metadata) {
140
+ raw.metadata = formatMetadata(raw.metadata);
141
+ }
142
+
143
+ await doc.update(raw);
144
+ res.json(doc);
145
+ } catch (err) {
146
+ console.error(err);
147
+ res.json(null);
148
+ }
149
+ });
150
+
151
+ export default router;
@@ -10,6 +10,7 @@ import { formatMetadata } from '../libs/util';
10
10
  import { PaymentCurrency } from '../store/models/payment-currency';
11
11
  import { Price } from '../store/models/price';
12
12
  import { Product } from '../store/models/product';
13
+ import type { CustomUnitAmount } from '../store/models/types';
13
14
 
14
15
  const router = Router();
15
16
 
@@ -36,8 +37,8 @@ export async function createProductAndPrices(payload: any) {
36
37
 
37
38
  const product = await Product.create(raw as Product);
38
39
  if (Array.isArray(payload.prices) && payload.prices.length) {
39
- if (payload.prices.some((x: any) => !x.unit_amount)) {
40
- throw new Error('unit_amount is required for price');
40
+ if (payload.prices.some((x: any) => !x.unit_amount && !x.custom_unit_amount)) {
41
+ throw new Error('unit_amount or custom_unit_amount is required for price');
41
42
  }
42
43
 
43
44
  const currencies = await PaymentCurrency.findAll({ where: { active: true } });
@@ -51,10 +52,27 @@ export async function createProductAndPrices(payload: any) {
51
52
  if (!currency) {
52
53
  throw new Error(`currency ${price.currency_id} used in price not found or inactive`);
53
54
  }
54
- if (!price.unit_amount) {
55
- throw new Error('price.unit_amount is required');
55
+ if (price.custom_unit_amount) {
56
+ // @ts-ignore
57
+ ['preset', 'maximum', 'minimum'].forEach((key: keyof CustomUnitAmount) => {
58
+ if (price.custom_unit_amount?.[key]) {
59
+ price.custom_unit_amount[key] = fromTokenToUnit(
60
+ price.custom_unit_amount[key] as string,
61
+ currency.decimal
62
+ ).toString();
63
+ }
64
+ });
65
+ if (Array.isArray(price.custom_unit_amount.presets)) {
66
+ price.custom_unit_amount.presets = price.custom_unit_amount.presets.map((x) =>
67
+ fromTokenToUnit(x, currency.decimal).toString()
68
+ );
69
+ }
70
+ } else {
71
+ if (!price.unit_amount) {
72
+ throw new Error('price.unit_amount is required');
73
+ }
74
+ price.unit_amount = fromTokenToUnit(price.unit_amount, currency.decimal).toString();
56
75
  }
57
- price.unit_amount = fromTokenToUnit(price.unit_amount, currency.decimal).toString();
58
76
 
59
77
  if (Array.isArray(price.currency_options)) {
60
78
  price.currency_options = Price.formatCurrencies(price.currency_options, currencies);
@@ -66,7 +84,7 @@ export async function createProductAndPrices(payload: any) {
66
84
  currency_id: price.currency_id,
67
85
  unit_amount: price.unit_amount,
68
86
  tiers: null,
69
- custom_unit_amount: null,
87
+ custom_unit_amount: price.custom_unit_amount,
70
88
  });
71
89
  }
72
90
 
@@ -78,9 +78,12 @@ router.post('/', auth, async (req, res) => {
78
78
  // @link https://stripe.com/docs/api/usage_records/subscription_item_summary_list
79
79
  const schema = createListParamSchema<{
80
80
  subscription_item_id: string;
81
- }>({
82
- subscription_item_id: Joi.string().required(),
83
- });
81
+ }>(
82
+ {
83
+ subscription_item_id: Joi.string().required(),
84
+ },
85
+ 100
86
+ );
84
87
  router.get('/summary', auth, async (req, res) => {
85
88
  const { page, pageSize, ...query } = await schema.validateAsync(req.query, { stripUnknown: true });
86
89
 
@@ -0,0 +1,36 @@
1
+ import { DataTypes } from 'sequelize';
2
+
3
+ import { Migration, safeApplyColumnChanges } from '../migrate';
4
+ import models from '../models';
5
+
6
+ export const up: Migration = async ({ context }) => {
7
+ await context.createTable('payouts', models.Payout.GENESIS_ATTRIBUTES);
8
+ await safeApplyColumnChanges(context, {
9
+ payment_links: [
10
+ {
11
+ name: 'donation_settings',
12
+ field: {
13
+ type: DataTypes.JSON,
14
+ allowNull: true,
15
+ defaultValue: {},
16
+ },
17
+ },
18
+ ],
19
+ payment_intents: [
20
+ {
21
+ name: 'beneficiaries',
22
+ field: {
23
+ type: DataTypes.JSON,
24
+ allowNull: true,
25
+ defaultValue: [],
26
+ },
27
+ },
28
+ ],
29
+ });
30
+ };
31
+
32
+ export const down: Migration = async ({ context }) => {
33
+ await context.dropTable('payouts');
34
+ await context.removeColumn('payment_links', 'donation_settings');
35
+ await context.removeColumn('payment_intents', 'beneficiaries');
36
+ };
@@ -463,6 +463,11 @@ export class CheckoutSession extends Model<InferAttributes<CheckoutSession>, Inf
463
463
  foreignKey: 'id',
464
464
  as: 'currency',
465
465
  });
466
+ this.hasOne(models.Customer, {
467
+ sourceKey: 'customer_id',
468
+ foreignKey: 'id',
469
+ as: 'customer',
470
+ });
466
471
  }
467
472
 
468
473
  public static findByPkOrClientRefId(id: string, options: FindOptions<CheckoutSession> = {}) {
@@ -15,10 +15,11 @@ import {
15
15
  import { createEvent } from '../../libs/audit';
16
16
  import CustomError from '../../libs/error';
17
17
  import { getLock } from '../../libs/lock';
18
- import { createIdGenerator } from '../../libs/util';
18
+ import { createCodeGenerator, createIdGenerator } from '../../libs/util';
19
19
  import type { CustomerAddress, CustomerShipping } from './types';
20
20
 
21
21
  export const nextCustomerId = createIdGenerator('cus', 14);
22
+ export const nextInvoicePrefix = createCodeGenerator('', 8);
22
23
 
23
24
  // eslint-disable-next-line prettier/prettier
24
25
  export class Customer extends Model<InferAttributes<Customer>, InferCreationAttributes<Customer>> {
@@ -271,6 +272,10 @@ export class Customer extends Model<InferAttributes<Customer>, InferCreationAttr
271
272
  ...options,
272
273
  });
273
274
  }
275
+
276
+ public static getInvoicePrefix() {
277
+ return nextInvoicePrefix();
278
+ }
274
279
  }
275
280
 
276
281
  export type TCustomer = InferAttributes<Customer>;