payment-kit 1.18.2 → 1.18.4

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 (44) hide show
  1. package/api/src/index.ts +4 -21
  2. package/api/src/libs/logger.ts +2 -1
  3. package/api/src/routes/checkout-sessions.ts +7 -7
  4. package/api/src/routes/customers.ts +6 -5
  5. package/api/src/routes/events.ts +3 -2
  6. package/api/src/routes/invoices.ts +4 -3
  7. package/api/src/routes/payment-currencies.ts +2 -2
  8. package/api/src/routes/payment-intents.ts +6 -6
  9. package/api/src/routes/payment-links.ts +1 -1
  10. package/api/src/routes/payment-stats.ts +3 -2
  11. package/api/src/routes/payouts.ts +6 -5
  12. package/api/src/routes/pricing-table.ts +2 -2
  13. package/api/src/routes/settings.ts +251 -2
  14. package/api/src/routes/subscription-items.ts +3 -3
  15. package/api/src/routes/subscriptions.ts +15 -15
  16. package/api/src/routes/usage-records.ts +2 -2
  17. package/api/src/routes/webhook-attempts.ts +2 -1
  18. package/api/src/routes/webhook-endpoints.ts +3 -2
  19. package/api/src/store/migrations/20250211-setting.ts +10 -0
  20. package/api/src/store/migrations/20250214-setting-component.ts +22 -0
  21. package/api/src/store/models/index.ts +6 -1
  22. package/api/src/store/models/setting.ts +84 -0
  23. package/api/src/store/models/types.ts +2 -0
  24. package/blocklet.yml +13 -1
  25. package/package.json +24 -24
  26. package/src/app.tsx +6 -1
  27. package/src/components/invoice/list.tsx +1 -3
  28. package/src/components/payment-intent/list.tsx +5 -6
  29. package/src/components/payouts/list.tsx +3 -3
  30. package/src/components/refund/list.tsx +3 -6
  31. package/src/components/subscription/list.tsx +1 -2
  32. package/src/locales/en.tsx +100 -1
  33. package/src/locales/zh.tsx +96 -0
  34. package/src/pages/admin/customers/customers/index.tsx +27 -16
  35. package/src/pages/admin/developers/webhooks/index.tsx +14 -12
  36. package/src/pages/admin/products/links/index.tsx +22 -15
  37. package/src/pages/admin/products/pricing-tables/index.tsx +16 -14
  38. package/src/pages/admin/products/products/index.tsx +19 -15
  39. package/src/pages/home.tsx +1 -1
  40. package/src/pages/integrations/donations/edit-form.tsx +349 -0
  41. package/src/pages/integrations/donations/index.tsx +360 -0
  42. package/src/pages/integrations/donations/preview.tsx +229 -0
  43. package/src/pages/integrations/index.tsx +80 -0
  44. package/src/pages/integrations/overview.tsx +121 -0
package/api/src/index.ts CHANGED
@@ -6,8 +6,7 @@ import fallback from '@blocklet/sdk/lib/middlewares/fallback';
6
6
  import cookieParser from 'cookie-parser';
7
7
  import cors from 'cors';
8
8
  import dotenv from 'dotenv-flow';
9
- import express, { ErrorRequestHandler, Request, Response } from 'express';
10
- import morgan from 'morgan';
9
+ import express, { ErrorRequestHandler } from 'express';
11
10
 
12
11
  // eslint-disable-next-line import/no-extraneous-dependencies
13
12
  import { xss } from '@blocklet/xss';
@@ -18,7 +17,7 @@ import { ensureStakedForGas } from './integrations/arcblock/stake';
18
17
  import { initResourceHandler } from './integrations/blocklet/resource';
19
18
  import { ensureWebhookRegistered } from './integrations/stripe/setup';
20
19
  import { handlers } from './libs/auth';
21
- import logger, { accessLogStream } from './libs/logger';
20
+ import logger, { setupAccessLogger } from './libs/logger';
22
21
  import { contextMiddleware, ensureI18n } from './libs/middleware';
23
22
  import { initEventBroadcast } from './libs/ws';
24
23
  import { startCheckoutSessionQueue } from './queues/checkout-session';
@@ -49,6 +48,7 @@ initialize(sequelize);
49
48
 
50
49
  export const app = express();
51
50
 
51
+ setupAccessLogger(app);
52
52
  app.set('trust proxy', true);
53
53
  app.use(cookieParser());
54
54
  app.use((req, res, next) => {
@@ -79,23 +79,6 @@ router.use('/api', routes);
79
79
 
80
80
  const isProduction = process.env.BLOCKLET_MODE === 'production';
81
81
 
82
- const accessFormat =
83
- isProduction
84
- ? 'combined'
85
- : (tokens: any, req: Request, res: Response) => [tokens.method(req, res), tokens.url(req, res), tokens.status(req, res), tokens.res(req, res, 'content-length'), '-', tokens['response-time'](req, res), 'ms'].join(' '); // prettier-ignore
86
-
87
- if (isProduction) {
88
- app.use(morgan(accessFormat, { stream: accessLogStream }));
89
- } else {
90
- app.use((req, res, next) => {
91
- if (['/node_modules/.vite', '/src', '/@'].some((p) => req.originalUrl.startsWith(p))) {
92
- return next();
93
- }
94
-
95
- return morgan(accessFormat, { stream: accessLogStream })(req, res, next);
96
- });
97
- }
98
-
99
82
  app.use(contextMiddleware);
100
83
  app.use(router);
101
84
 
@@ -106,7 +89,7 @@ if (isProduction) {
106
89
 
107
90
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
108
91
  app.use(<ErrorRequestHandler>((err, req, res, _next) => {
109
- console.error(err);
92
+ logger.error(err);
110
93
  if (req.accepts('json')) {
111
94
  res.status(500).send({ error: err.message });
112
95
  } else {
@@ -16,4 +16,5 @@ const logger = init('app');
16
16
 
17
17
  export default logger;
18
18
 
19
- export const accessLogStream = createLogger.getAccessLogStream();
19
+ const { setupAccessLogger } = createLogger;
20
+ export { setupAccessLogger };
@@ -522,7 +522,7 @@ export async function startCheckoutSessionFromPaymentLink(id: string, req: Reque
522
522
  customer: req.user ? await Customer.findOne({ where: { did: req.user.did } }) : null,
523
523
  });
524
524
  } catch (err) {
525
- console.error(err);
525
+ logger.error(err);
526
526
  res.status(500).json({ error: err.message });
527
527
  }
528
528
  }
@@ -1135,7 +1135,7 @@ router.put('/:id/upsell', user, ensureCheckoutSessionOpen, async (req, res) => {
1135
1135
  const items = await Price.expand(checkoutSession.line_items, { upsell: true });
1136
1136
  res.json({ ...checkoutSession.toJSON(), line_items: items });
1137
1137
  } catch (err) {
1138
- console.error(err);
1138
+ logger.error(err);
1139
1139
  res.status(500).json({ error: err.message });
1140
1140
  }
1141
1141
  });
@@ -1215,7 +1215,7 @@ router.get('/:id/cross-sell', user, ensureCheckoutSessionOpen, async (req, res)
1215
1215
  // @ts-ignore
1216
1216
  return res.status(result.error ? 400 : 200).json(result);
1217
1217
  } catch (err) {
1218
- console.error(err);
1218
+ logger.error(err);
1219
1219
  res.status(500).json({ error: err.message });
1220
1220
  }
1221
1221
  });
@@ -1261,7 +1261,7 @@ router.put('/:id/cross-sell', user, ensureCheckoutSessionOpen, async (req, res)
1261
1261
  const items = await Price.expand(checkoutSession.line_items, { upsell: true });
1262
1262
  res.json({ ...checkoutSession.toJSON(), line_items: items });
1263
1263
  } catch (err) {
1264
- console.error(err);
1264
+ logger.error(err);
1265
1265
  res.status(500).json({ error: err.message });
1266
1266
  }
1267
1267
  });
@@ -1287,7 +1287,7 @@ router.delete('/:id/cross-sell', user, ensureCheckoutSessionOpen, async (req, re
1287
1287
  const items = await Price.expand(checkoutSession.line_items, { upsell: true });
1288
1288
  res.json({ ...checkoutSession.toJSON(), line_items: items });
1289
1289
  } catch (err) {
1290
- console.error(err);
1290
+ logger.error(err);
1291
1291
  res.status(500).json({ error: err.message });
1292
1292
  }
1293
1293
  });
@@ -1356,7 +1356,7 @@ router.put('/:id/amount', ensureCheckoutSessionOpen, async (req, res) => {
1356
1356
 
1357
1357
  res.json({ ...checkoutSession.toJSON(), line_items: await Price.expand(newItems) });
1358
1358
  } catch (err) {
1359
- console.error(err);
1359
+ logger.error(err);
1360
1360
  res.status(500).json({ error: err.message });
1361
1361
  }
1362
1362
  });
@@ -1454,7 +1454,7 @@ router.get('/', auth, async (req, res) => {
1454
1454
 
1455
1455
  res.json({ count, list: docs });
1456
1456
  } catch (err) {
1457
- console.error(err);
1457
+ logger.error(err);
1458
1458
  res.json({ count: 0, list: [] });
1459
1459
  }
1460
1460
  });
@@ -10,6 +10,7 @@ import { authenticate } from '../libs/security';
10
10
  import { formatMetadata } from '../libs/util';
11
11
  import { Customer } from '../store/models/customer';
12
12
  import { blocklet } from '../libs/auth';
13
+ import logger from '../libs/logger';
13
14
 
14
15
  const router = Router();
15
16
  const auth = authenticate<Customer>({ component: true, roles: ['owner', 'admin'] });
@@ -46,7 +47,7 @@ router.get('/', auth, async (req, res) => {
46
47
 
47
48
  res.json({ count, list, paging: { page, pageSize } });
48
49
  } catch (err) {
49
- console.error(err);
50
+ logger.error(err);
50
51
  res.json({ count: 0, list: [], paging: { page, pageSize } });
51
52
  }
52
53
  });
@@ -103,7 +104,7 @@ router.get('/me', sessionMiddleware(), async (req, res) => {
103
104
  res.json({ ...doc.toJSON(), summary: { ...summary, stake, token }, livemode });
104
105
  }
105
106
  } catch (err) {
106
- console.error('get customer failed', err);
107
+ logger.error('get customer failed', err);
107
108
  res.status(500).json({ error: `Failed to get customer: ${err.message}` });
108
109
  }
109
110
  });
@@ -117,7 +118,7 @@ router.get('/:id', auth, async (req, res) => {
117
118
  res.status(404).json(null);
118
119
  }
119
120
  } catch (err) {
120
- console.error(err);
121
+ logger.error(err);
121
122
  res.status(500).json({ error: `Failed to get customer: ${err.message}` });
122
123
  }
123
124
  });
@@ -137,7 +138,7 @@ router.get('/:id/summary', auth, async (req, res) => {
137
138
  ]);
138
139
  res.json({ ...summary, stake, token });
139
140
  } catch (err) {
140
- console.error(err);
141
+ logger.error(err);
141
142
  res.json(null);
142
143
  }
143
144
  });
@@ -183,7 +184,7 @@ router.put('/:id', authPortal, async (req, res) => {
183
184
  await doc.update(raw);
184
185
  res.json(doc);
185
186
  } catch (err) {
186
- console.error(err);
187
+ logger.error(err);
187
188
  res.json(null);
188
189
  }
189
190
  });
@@ -6,6 +6,7 @@ import { createListParamSchema } from '../libs/api';
6
6
  import { authenticate } from '../libs/security';
7
7
  import { Event } from '../store/models/event';
8
8
  import { blocklet } from '../libs/auth';
9
+ import logger from '../libs/logger';
9
10
 
10
11
  const router = Router();
11
12
  const auth = authenticate<Event>({ component: true, roles: ['owner', 'admin'] });
@@ -49,7 +50,7 @@ router.get('/', auth, async (req, res) => {
49
50
 
50
51
  res.json({ count, list, paging: { page, pageSize } });
51
52
  } catch (err) {
52
- console.error(err);
53
+ logger.error(err);
53
54
  res.json({ count: 0, list: [], paging: { page, pageSize } });
54
55
  }
55
56
  });
@@ -71,7 +72,7 @@ router.get('/:id', auth, async (req, res) => {
71
72
  }
72
73
  return res.status(404).json(null);
73
74
  } catch (err) {
74
- console.error(err);
75
+ logger.error(err);
75
76
  return res.status(400).json({ error: `Failed to get event: ${err.message}` });
76
77
  }
77
78
  });
@@ -23,6 +23,7 @@ import { Product } from '../store/models/product';
23
23
  import { Subscription } from '../store/models/subscription';
24
24
  import { getReturnStakeInvoices, getStakingInvoices } from '../libs/invoice';
25
25
  import { CheckoutSession, PaymentLink, TInvoiceExpanded } from '../store/models';
26
+ import logger from '../libs/logger';
26
27
 
27
28
  const router = Router();
28
29
  const authAdmin = authenticate<Subscription>({ component: true, roles: ['owner', 'admin'] });
@@ -174,7 +175,7 @@ router.get('/', authMine, async (req, res) => {
174
175
 
175
176
  res.json({ count, list: invoices, subscription, paging: { page, pageSize } });
176
177
  } catch (err) {
177
- console.error(err);
178
+ logger.error(err);
178
179
  res.json({ count: 0, list: [], paging: { page, pageSize } });
179
180
  }
180
181
  });
@@ -279,7 +280,7 @@ router.get('/:id', authPortal, async (req, res) => {
279
280
  }
280
281
  return res.status(404).json(null);
281
282
  } catch (err) {
282
- console.error(err);
283
+ logger.error(err);
283
284
  return res.status(500).json({ error: `Failed to get invoice: ${err.message}` });
284
285
  }
285
286
  });
@@ -304,7 +305,7 @@ router.put('/:id', authAdmin, async (req, res) => {
304
305
  await doc.update(raw);
305
306
  res.json(doc);
306
307
  } catch (err) {
307
- console.error(err);
308
+ logger.error(err);
308
309
  res.json(null);
309
310
  }
310
311
  });
@@ -77,7 +77,7 @@ router.post('/', auth, async (req, res) => {
77
77
  });
78
78
  return res.json(currency);
79
79
  } catch (err) {
80
- console.error(err);
80
+ logger.error(err);
81
81
  return res.status(400).json({ error: `${method.type} currency contract verify failed` });
82
82
  }
83
83
  }
@@ -113,7 +113,7 @@ router.post('/', auth, async (req, res) => {
113
113
  });
114
114
  return res.json(currency);
115
115
  } catch (err) {
116
- console.error(err);
116
+ logger.error(err);
117
117
  return res.status(400).json({ error: `${method.type} currency contract verify failed` });
118
118
  }
119
119
  }
@@ -123,7 +123,7 @@ router.get('/', authMine, async (req, res) => {
123
123
 
124
124
  res.json({ count, list, paging: { page, pageSize } });
125
125
  } catch (err) {
126
- console.error(err);
126
+ logger.error(err);
127
127
  res.json({ count: 0, list: [], paging: { page, pageSize } });
128
128
  }
129
129
  });
@@ -191,7 +191,7 @@ router.get('/:id', authPortal, async (req, res) => {
191
191
  res.status(404).json(null);
192
192
  }
193
193
  } catch (err) {
194
- console.error(err);
194
+ logger.error(err);
195
195
  res.status(500).json({ error: `Failed to get payment intent: ${err.message}` });
196
196
  }
197
197
  });
@@ -216,7 +216,7 @@ router.put('/:id', authAdmin, async (req, res) => {
216
216
  await doc.update(raw);
217
217
  res.json(doc);
218
218
  } catch (err) {
219
- console.error(err);
219
+ logger.error(err);
220
220
  res.json(null);
221
221
  }
222
222
  });
@@ -242,7 +242,7 @@ router.get('/:id/retry', authAdmin, async (req, res) => {
242
242
  });
243
243
  res.json(doc);
244
244
  } catch (err) {
245
- console.error(err);
245
+ logger.error(err);
246
246
  res.json(null);
247
247
  }
248
248
  });
@@ -332,7 +332,7 @@ router.put('/:id/refund', authAdmin, async (req, res) => {
332
332
  });
333
333
  res.json(item.toJSON());
334
334
  } catch (err) {
335
- console.error(err);
335
+ logger.error(err);
336
336
  res.status(500).json({ error: `Failed to payment intent refund: ${err.message}` });
337
337
  }
338
338
  });
@@ -370,7 +370,7 @@ router.get('/:id/refundable-amount', authPortal, async (req, res) => {
370
370
  res.status(404).json(null);
371
371
  }
372
372
  } catch (err) {
373
- console.error(err);
373
+ logger.error(err);
374
374
  res.status(500).json({ error: `Failed to get payment intent refundable amount: ${err.message}` });
375
375
  }
376
376
  });
@@ -257,7 +257,7 @@ router.get('/', auth, async (req, res) => {
257
257
 
258
258
  res.json({ count, list, paging: { page, pageSize } });
259
259
  } catch (err) {
260
- console.error(err);
260
+ logger.error(err);
261
261
  res.json({ count: 0, list: [], paging: { page, pageSize } });
262
262
  }
263
263
  });
@@ -21,6 +21,7 @@ import {
21
21
  Subscription,
22
22
  } from '../store/models';
23
23
  import { EVM_CHAIN_TYPES } from '../libs/constants';
24
+ import logger from '../libs/logger';
24
25
 
25
26
  const router = Router();
26
27
  const auth = authenticate<PaymentStat>({ component: true, roles: ['owner', 'admin'] });
@@ -80,7 +81,7 @@ router.get('/', auth, async (req, res) => {
80
81
 
81
82
  res.json({ count, list });
82
83
  } catch (err) {
83
- console.error(err);
84
+ logger.error(err);
84
85
  res.json({ count: 0, list: [] });
85
86
  }
86
87
  });
@@ -132,7 +133,7 @@ router.get('/summary', auth, async (req, res) => {
132
133
  },
133
134
  });
134
135
  } catch (err) {
135
- console.error(err);
136
+ logger.error(err);
136
137
  res.json(null);
137
138
  }
138
139
  });
@@ -14,6 +14,7 @@ import { PaymentMethod } from '../store/models/payment-method';
14
14
  import { Payout } from '../store/models/payout';
15
15
  import { PaymentLink, TPaymentIntentExpanded } from '../store/models';
16
16
  import { CheckoutSession } from '../store/models/checkout-session';
17
+ import logger from '../libs/logger';
17
18
 
18
19
  const router = Router();
19
20
  const authAdmin = authenticate<Payout>({ component: true, roles: ['owner', 'admin'] });
@@ -102,7 +103,7 @@ router.get('/', authMine, async (req, res) => {
102
103
 
103
104
  res.json({ count, list, paging: { page, pageSize } });
104
105
  } catch (err) {
105
- console.error(err);
106
+ logger.error(err);
106
107
  res.json({ count: 0, list: [], paging: { page, pageSize } });
107
108
  }
108
109
  });
@@ -169,7 +170,7 @@ router.get('/mine', sessionMiddleware(), async (req, res) => {
169
170
  );
170
171
  return res.json({ count, list: result, paging: { page, pageSize } });
171
172
  } catch (err) {
172
- console.error(err);
173
+ logger.error(err);
173
174
  return res.status(400).json({ error: err.message });
174
175
  }
175
176
  });
@@ -199,7 +200,7 @@ router.get('/:id', authPortal, async (req, res) => {
199
200
  paymentLink = await PaymentLink.findByPk(checkoutSession.payment_link_id);
200
201
  }
201
202
  } catch (err) {
202
- console.error(err);
203
+ logger.error(err);
203
204
  }
204
205
  }
205
206
  const payout = doc.toJSON();
@@ -217,7 +218,7 @@ router.get('/:id', authPortal, async (req, res) => {
217
218
  }
218
219
  return res.json(payout);
219
220
  } catch (err) {
220
- console.error(err);
221
+ logger.error(err);
221
222
  return res.status(500).json({ error: `Failed to get payout: ${err.message}` });
222
223
  }
223
224
  });
@@ -242,7 +243,7 @@ router.put('/:id', authAdmin, async (req, res) => {
242
243
  await doc.update(raw);
243
244
  res.json(doc);
244
245
  } catch (err) {
245
- console.error(err);
246
+ logger.error(err);
246
247
  res.json(null);
247
248
  }
248
249
  });
@@ -128,7 +128,7 @@ router.get('/', auth, async (req, res) => {
128
128
 
129
129
  res.json({ count, list, paging: { page, pageSize } });
130
130
  } catch (err) {
131
- console.error(err);
131
+ logger.error(err);
132
132
  res.json({ count: 0, list: [], paging: { page, pageSize } });
133
133
  }
134
134
  });
@@ -272,7 +272,7 @@ router.post('/stash', auth, async (req, res) => {
272
272
  }
273
273
  res.json(doc);
274
274
  } catch (err) {
275
- console.error(err);
275
+ logger.error(err);
276
276
  res.status(500).json({ error: err.message });
277
277
  }
278
278
  });
@@ -1,12 +1,18 @@
1
1
  import { Router } from 'express';
2
2
  import pick from 'lodash/pick';
3
- import type { WhereOptions } from 'sequelize';
3
+ import { Op, type WhereOptions } from 'sequelize';
4
4
 
5
+ import Joi from 'joi';
6
+ import { merge } from 'lodash';
5
7
  import { PaymentCurrency } from '../store/models/payment-currency';
6
8
  import { PaymentMethod } from '../store/models/payment-method';
9
+ import { authenticate } from '../libs/security';
10
+ import { Setting } from '../store/models';
11
+ import logger from '../libs/logger';
12
+ import { createListParamSchema, getWhereFromKvQuery } from '../libs/api';
7
13
 
8
14
  const router = Router();
9
-
15
+ const authAdmin = authenticate<Setting>({ component: true, roles: ['owner', 'admin'] });
10
16
  router.get('/', async (req, res) => {
11
17
  const attributes = ['id', 'name', 'symbol', 'decimal', 'logo', 'payment_method_id', 'maximum_precision'];
12
18
  const where: WhereOptions<PaymentMethod> = { livemode: req.livemode, active: true };
@@ -35,4 +41,247 @@ router.get('/', async (req, res) => {
35
41
  });
36
42
  });
37
43
 
44
+ const donateListSchema = createListParamSchema<{
45
+ active?: boolean;
46
+ mountLocation?: string;
47
+ description?: string;
48
+ componentDid?: string;
49
+ }>({
50
+ active: Joi.boolean().empty(''),
51
+ mountLocation: Joi.string().empty(''),
52
+ description: Joi.string().empty(''),
53
+ componentDid: Joi.string().empty(''),
54
+ });
55
+
56
+ router.get('/donate', async (req, res) => {
57
+ try {
58
+ const { page, pageSize, mountLocation, description, componentDid, ...query } = await donateListSchema.validateAsync(
59
+ req.query,
60
+ { stripUnknown: false, allowUnknown: true }
61
+ );
62
+
63
+ const where = {
64
+ type: 'donate',
65
+ ...getWhereFromKvQuery(query.q),
66
+ };
67
+
68
+ if (typeof query.active === 'boolean') {
69
+ where.active = query.active;
70
+ }
71
+ if (mountLocation) {
72
+ where.mount_location = mountLocation;
73
+ }
74
+ if (description) {
75
+ where.description = description;
76
+ }
77
+ if (componentDid) {
78
+ where.component_did = componentDid;
79
+ }
80
+
81
+ const { rows: list, count } = await Setting.findAndCountAll({
82
+ where,
83
+ order: [['created_at', query.o === 'asc' ? 'ASC' : 'DESC']],
84
+ offset: (page - 1) * pageSize,
85
+ limit: pageSize,
86
+ distinct: true,
87
+ });
88
+
89
+ return res.json({ count, list, paging: { page, pageSize } });
90
+ } catch (err) {
91
+ logger.error(err);
92
+ return res.status(400).json({ error: err.message });
93
+ }
94
+ });
95
+
96
+ router.get('/donate/:mountLocationOrId', async (req, res) => {
97
+ try {
98
+ if (!req.params.mountLocationOrId) {
99
+ return res.status(400).json({ error: 'mountLocation is required' });
100
+ }
101
+ const settings = await Setting.findOne({
102
+ where: {
103
+ type: 'donate',
104
+ [Op.or]: [{ mount_location: req.params.mountLocationOrId }, { id: req.params.mountLocationOrId }],
105
+ },
106
+ });
107
+ return res.json(settings);
108
+ } catch (err) {
109
+ logger.error(err);
110
+ return res.status(400).json({ error: err.message });
111
+ }
112
+ });
113
+
114
+ const SettingSchema = Joi.object({
115
+ type: Joi.string().required(),
116
+ mountLocation: Joi.string().required(),
117
+ settings: Joi.object().optional(),
118
+ active: Joi.boolean().optional(),
119
+ description: Joi.string().required(),
120
+ livemode: Joi.boolean().optional(),
121
+ componentDid: Joi.string().optional(),
122
+ }).unknown(true);
123
+ router.post('/', async (req, res) => {
124
+ try {
125
+ const { error } = SettingSchema.validate(req.body);
126
+ if (error) {
127
+ return res.status(400).json({ error: error.message });
128
+ }
129
+ const { type, mountLocation, settings = {}, active = true, description, livemode = true, componentDid } = req.body;
130
+ const raw = {
131
+ type,
132
+ mount_location: mountLocation,
133
+ description,
134
+ active,
135
+ livemode,
136
+ settings: settings || {},
137
+ component_did: componentDid,
138
+ };
139
+ if (type === 'donate') {
140
+ const defaultSetting = {
141
+ amount: {
142
+ presets: ['1', '5', '10'],
143
+ preset: '1',
144
+ minimum: '0.01',
145
+ maximum: '100',
146
+ custom: true,
147
+ },
148
+ btnText: 'Donate',
149
+ historyType: 'avatar',
150
+ };
151
+
152
+ if (settings?.amount) {
153
+ const amountSchema = Joi.object({
154
+ presets: Joi.array()
155
+ .items(Joi.string())
156
+ .when('custom', { is: false, then: Joi.required(), otherwise: Joi.optional() }),
157
+ preset: Joi.string().optional(),
158
+ minimum: Joi.string().when('custom', { is: true, then: Joi.required() }),
159
+ maximum: Joi.string().when('custom', { is: true, then: Joi.required() }),
160
+ custom: Joi.boolean().required(),
161
+ });
162
+
163
+ const { error: amountError } = amountSchema.validate(settings.amount);
164
+ if (amountError) {
165
+ return res.status(400).json({ error: amountError.message });
166
+ }
167
+ }
168
+
169
+ raw.settings = merge({}, defaultSetting, settings);
170
+ if (settings.amount) {
171
+ raw.settings.amount = settings.amount;
172
+ }
173
+ }
174
+ const exist = await Setting.findOne({
175
+ where: {
176
+ type,
177
+ mount_location: mountLocation,
178
+ },
179
+ });
180
+ if (exist) {
181
+ if (exist.component_did !== componentDid) {
182
+ await exist.update({
183
+ component_did: componentDid,
184
+ });
185
+ }
186
+ return res.json(exist);
187
+ }
188
+ const doc = await Setting.create(raw);
189
+ return res.json(doc);
190
+ } catch (err) {
191
+ logger.error(err);
192
+ return res.status(400).json({ error: err.message });
193
+ }
194
+ });
195
+
196
+ const UpdateSettingSchema = Joi.object({
197
+ settings: Joi.object().optional(),
198
+ active: Joi.boolean().optional(),
199
+ description: Joi.string().optional(),
200
+ }).unknown(true);
201
+ router.put('/:mountLocationOrId', authAdmin, async (req, res) => {
202
+ try {
203
+ const { error } = UpdateSettingSchema.validate(req.body);
204
+ if (error) {
205
+ return res.status(400).json({ error: error.message });
206
+ }
207
+ const { settings = {}, active = true, description } = req.body;
208
+ const raw: {
209
+ active: boolean;
210
+ settings: Record<string, any>;
211
+ description?: string;
212
+ } = {
213
+ active,
214
+ settings: settings || {},
215
+ };
216
+ if (description) {
217
+ raw.description = description;
218
+ }
219
+ const setting = await Setting.findOne({
220
+ where: {
221
+ [Op.or]: [
222
+ {
223
+ id: req.params.mountLocationOrId,
224
+ },
225
+ {
226
+ mount_location: req.params.mountLocationOrId,
227
+ },
228
+ ],
229
+ },
230
+ });
231
+ if (!setting) {
232
+ return res.status(404).json({ error: 'Setting not found' });
233
+ }
234
+ raw.settings = merge({}, setting.settings, settings);
235
+ if (setting.type === 'donate') {
236
+ if (settings?.amount) {
237
+ const amountSchema = Joi.object({
238
+ presets: Joi.array()
239
+ .items(Joi.string())
240
+ .when('custom', { is: false, then: Joi.required(), otherwise: Joi.optional() }),
241
+ preset: Joi.string().optional(),
242
+ minimum: Joi.string().when('custom', { is: true, then: Joi.required() }),
243
+ maximum: Joi.string().when('custom', { is: true, then: Joi.required() }),
244
+ custom: Joi.boolean().required(),
245
+ });
246
+
247
+ const { error: amountError } = amountSchema.validate(settings.amount);
248
+ if (amountError) {
249
+ return res.status(400).json({ error: amountError.message });
250
+ }
251
+ raw.settings.amount = settings.amount;
252
+ }
253
+ }
254
+ const doc = await setting.update(raw);
255
+ return res.json(doc);
256
+ } catch (err) {
257
+ logger.error(err);
258
+ return res.status(400).json({ error: err.message });
259
+ }
260
+ });
261
+
262
+ router.delete('/:mountLocationOrId', authAdmin, async (req, res) => {
263
+ try {
264
+ const setting = await Setting.findOne({
265
+ where: {
266
+ [Op.or]: [
267
+ {
268
+ id: req.params.mountLocationOrId,
269
+ },
270
+ {
271
+ mount_location: req.params.mountLocationOrId,
272
+ },
273
+ ],
274
+ },
275
+ });
276
+ if (!setting) {
277
+ return res.status(404).json({ error: `Setting ${req.params.mountLocationOrId} not found` });
278
+ }
279
+ await setting.destroy();
280
+ return res.json({ message: `Setting ${req.params.mountLocationOrId} deleted` });
281
+ } catch (err) {
282
+ logger.error(err);
283
+ return res.status(400).json({ error: err.message });
284
+ }
285
+ });
286
+
38
287
  export default router;