payment-kit 1.13.15

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 (222) hide show
  1. package/.eslintrc.js +15 -0
  2. package/README.md +3 -0
  3. package/api/dev.ts +6 -0
  4. package/api/hooks/pre-start.js +12 -0
  5. package/api/src/hooks/pre-start.ts +21 -0
  6. package/api/src/index.ts +92 -0
  7. package/api/src/jobs/event.ts +72 -0
  8. package/api/src/jobs/invoice.ts +148 -0
  9. package/api/src/jobs/payment.ts +208 -0
  10. package/api/src/jobs/subscription.ts +301 -0
  11. package/api/src/jobs/webhook.ts +113 -0
  12. package/api/src/libs/audit.ts +73 -0
  13. package/api/src/libs/auth.ts +40 -0
  14. package/api/src/libs/chain/arcblock.ts +13 -0
  15. package/api/src/libs/dayjs.ts +17 -0
  16. package/api/src/libs/env.ts +5 -0
  17. package/api/src/libs/hooks.ts +42 -0
  18. package/api/src/libs/logger.ts +27 -0
  19. package/api/src/libs/middleware.ts +12 -0
  20. package/api/src/libs/payment.ts +53 -0
  21. package/api/src/libs/queue/index.ts +263 -0
  22. package/api/src/libs/queue/store.ts +47 -0
  23. package/api/src/libs/security.ts +95 -0
  24. package/api/src/libs/session.ts +164 -0
  25. package/api/src/libs/util.ts +93 -0
  26. package/api/src/locales/en.ts +3 -0
  27. package/api/src/locales/index.ts +37 -0
  28. package/api/src/locales/zh.ts +3 -0
  29. package/api/src/routes/checkout-sessions.ts +536 -0
  30. package/api/src/routes/connect/collect.ts +109 -0
  31. package/api/src/routes/connect/pay.ts +116 -0
  32. package/api/src/routes/connect/setup.ts +121 -0
  33. package/api/src/routes/connect/shared.ts +410 -0
  34. package/api/src/routes/connect/subscribe.ts +128 -0
  35. package/api/src/routes/customers.ts +70 -0
  36. package/api/src/routes/events.ts +76 -0
  37. package/api/src/routes/index.ts +59 -0
  38. package/api/src/routes/invoices.ts +126 -0
  39. package/api/src/routes/payment-currencies.ts +38 -0
  40. package/api/src/routes/payment-intents.ts +122 -0
  41. package/api/src/routes/payment-links.ts +221 -0
  42. package/api/src/routes/payment-methods.ts +39 -0
  43. package/api/src/routes/prices.ts +134 -0
  44. package/api/src/routes/products.ts +191 -0
  45. package/api/src/routes/settings.ts +33 -0
  46. package/api/src/routes/subscription-items.ts +148 -0
  47. package/api/src/routes/subscriptions.ts +254 -0
  48. package/api/src/routes/usage-records.ts +120 -0
  49. package/api/src/routes/webhook-attempts.ts +57 -0
  50. package/api/src/routes/webhook-endpoints.ts +105 -0
  51. package/api/src/store/migrate.ts +16 -0
  52. package/api/src/store/migrations/20230905-genesis.ts +52 -0
  53. package/api/src/store/migrations/20230911-seeding.ts +145 -0
  54. package/api/src/store/models/checkout-session.ts +395 -0
  55. package/api/src/store/models/coupon.ts +137 -0
  56. package/api/src/store/models/customer.ts +199 -0
  57. package/api/src/store/models/discount.ts +116 -0
  58. package/api/src/store/models/event.ts +111 -0
  59. package/api/src/store/models/index.ts +165 -0
  60. package/api/src/store/models/invoice-item.ts +185 -0
  61. package/api/src/store/models/invoice.ts +492 -0
  62. package/api/src/store/models/job.ts +75 -0
  63. package/api/src/store/models/payment-currency.ts +139 -0
  64. package/api/src/store/models/payment-intent.ts +282 -0
  65. package/api/src/store/models/payment-link.ts +219 -0
  66. package/api/src/store/models/payment-method.ts +169 -0
  67. package/api/src/store/models/price.ts +266 -0
  68. package/api/src/store/models/product.ts +162 -0
  69. package/api/src/store/models/promotion-code.ts +112 -0
  70. package/api/src/store/models/setup-intent.ts +206 -0
  71. package/api/src/store/models/subscription-item.ts +103 -0
  72. package/api/src/store/models/subscription-schedule.ts +157 -0
  73. package/api/src/store/models/subscription.ts +307 -0
  74. package/api/src/store/models/types.ts +406 -0
  75. package/api/src/store/models/usage-record.ts +132 -0
  76. package/api/src/store/models/webhook-attempt.ts +96 -0
  77. package/api/src/store/models/webhook-endpoint.ts +96 -0
  78. package/api/src/store/sequelize.ts +15 -0
  79. package/api/third.d.ts +28 -0
  80. package/blocklet.md +3 -0
  81. package/blocklet.yml +89 -0
  82. package/index.html +14 -0
  83. package/logo.png +0 -0
  84. package/package.json +133 -0
  85. package/public/.gitkeep +0 -0
  86. package/screenshots/.gitkeep +0 -0
  87. package/screenshots/1-subscription.png +0 -0
  88. package/screenshots/2-customer-1.png +0 -0
  89. package/screenshots/3-customer-2.png +0 -0
  90. package/screenshots/4-admin-3.png +0 -0
  91. package/screenshots/5-admin-4.png +0 -0
  92. package/scripts/build-clean.js +6 -0
  93. package/scripts/bump-version.mjs +35 -0
  94. package/src/app.tsx +68 -0
  95. package/src/components/actions.tsx +85 -0
  96. package/src/components/blockchain/tx.tsx +29 -0
  97. package/src/components/checkout/amount.tsx +24 -0
  98. package/src/components/checkout/error.tsx +30 -0
  99. package/src/components/checkout/footer.tsx +12 -0
  100. package/src/components/checkout/form/address.tsx +38 -0
  101. package/src/components/checkout/form/index.tsx +295 -0
  102. package/src/components/checkout/header.tsx +23 -0
  103. package/src/components/checkout/pay.tsx +222 -0
  104. package/src/components/checkout/product-card.tsx +56 -0
  105. package/src/components/checkout/product-item.tsx +37 -0
  106. package/src/components/checkout/skeleton/overview.tsx +21 -0
  107. package/src/components/checkout/skeleton/payment.tsx +35 -0
  108. package/src/components/checkout/success.tsx +183 -0
  109. package/src/components/checkout/summary.tsx +34 -0
  110. package/src/components/collapse.tsx +50 -0
  111. package/src/components/confirm.tsx +55 -0
  112. package/src/components/copyable.tsx +38 -0
  113. package/src/components/currency.tsx +15 -0
  114. package/src/components/customer/actions.tsx +73 -0
  115. package/src/components/data.tsx +20 -0
  116. package/src/components/drawer-form.tsx +77 -0
  117. package/src/components/error-fallback.tsx +7 -0
  118. package/src/components/error.tsx +39 -0
  119. package/src/components/event/list.tsx +217 -0
  120. package/src/components/info-card.tsx +40 -0
  121. package/src/components/info-metric.tsx +35 -0
  122. package/src/components/info-row.tsx +28 -0
  123. package/src/components/input.tsx +40 -0
  124. package/src/components/invoice/action.tsx +94 -0
  125. package/src/components/invoice/list.tsx +225 -0
  126. package/src/components/invoice/table.tsx +110 -0
  127. package/src/components/layout.tsx +70 -0
  128. package/src/components/livemode.tsx +23 -0
  129. package/src/components/metadata/editor.tsx +57 -0
  130. package/src/components/metadata/form.tsx +45 -0
  131. package/src/components/payment-intent/actions.tsx +81 -0
  132. package/src/components/payment-intent/list.tsx +204 -0
  133. package/src/components/payment-link/actions.tsx +114 -0
  134. package/src/components/payment-link/after-pay.tsx +87 -0
  135. package/src/components/payment-link/before-pay.tsx +175 -0
  136. package/src/components/payment-link/item.tsx +135 -0
  137. package/src/components/payment-link/product-select.tsx +66 -0
  138. package/src/components/payment-link/rename.tsx +64 -0
  139. package/src/components/portal/invoice/list.tsx +110 -0
  140. package/src/components/portal/subscription/cancel.tsx +83 -0
  141. package/src/components/portal/subscription/list.tsx +232 -0
  142. package/src/components/price/actions.tsx +21 -0
  143. package/src/components/price/form.tsx +292 -0
  144. package/src/components/product/actions.tsx +125 -0
  145. package/src/components/product/add-price.tsx +59 -0
  146. package/src/components/product/create.tsx +97 -0
  147. package/src/components/product/edit-price.tsx +75 -0
  148. package/src/components/product/edit.tsx +67 -0
  149. package/src/components/product/features.tsx +32 -0
  150. package/src/components/product/form.tsx +76 -0
  151. package/src/components/relative-time.tsx +41 -0
  152. package/src/components/section/header.tsx +29 -0
  153. package/src/components/status.tsx +12 -0
  154. package/src/components/subscription/actions/cancel.tsx +66 -0
  155. package/src/components/subscription/actions/index.tsx +172 -0
  156. package/src/components/subscription/actions/pause.tsx +83 -0
  157. package/src/components/subscription/items/actions.tsx +31 -0
  158. package/src/components/subscription/items/index.tsx +107 -0
  159. package/src/components/subscription/list.tsx +200 -0
  160. package/src/components/switch.tsx +48 -0
  161. package/src/components/table.tsx +66 -0
  162. package/src/components/uploader.tsx +81 -0
  163. package/src/components/webhook/attempts.tsx +149 -0
  164. package/src/contexts/products.tsx +42 -0
  165. package/src/contexts/session.ts +10 -0
  166. package/src/contexts/settings.tsx +54 -0
  167. package/src/env.d.ts +17 -0
  168. package/src/global.css +97 -0
  169. package/src/hooks/mobile.ts +15 -0
  170. package/src/index.tsx +6 -0
  171. package/src/libs/api.ts +19 -0
  172. package/src/libs/dayjs.ts +17 -0
  173. package/src/libs/util.ts +474 -0
  174. package/src/locales/en.tsx +395 -0
  175. package/src/locales/index.tsx +8 -0
  176. package/src/locales/zh.tsx +389 -0
  177. package/src/pages/admin/billing/index.tsx +56 -0
  178. package/src/pages/admin/billing/invoices/detail.tsx +215 -0
  179. package/src/pages/admin/billing/invoices/index.tsx +5 -0
  180. package/src/pages/admin/billing/subscriptions/detail.tsx +237 -0
  181. package/src/pages/admin/billing/subscriptions/index.tsx +5 -0
  182. package/src/pages/admin/customers/customers/detail.tsx +209 -0
  183. package/src/pages/admin/customers/customers/index.tsx +109 -0
  184. package/src/pages/admin/customers/index.tsx +47 -0
  185. package/src/pages/admin/developers/events/detail.tsx +77 -0
  186. package/src/pages/admin/developers/events/index.tsx +5 -0
  187. package/src/pages/admin/developers/index.tsx +60 -0
  188. package/src/pages/admin/developers/logs.tsx +3 -0
  189. package/src/pages/admin/developers/overview.tsx +3 -0
  190. package/src/pages/admin/developers/webhooks/detail.tsx +109 -0
  191. package/src/pages/admin/developers/webhooks/index.tsx +102 -0
  192. package/src/pages/admin/index.tsx +120 -0
  193. package/src/pages/admin/overview.tsx +3 -0
  194. package/src/pages/admin/payments/index.tsx +65 -0
  195. package/src/pages/admin/payments/intents/detail.tsx +205 -0
  196. package/src/pages/admin/payments/intents/index.tsx +5 -0
  197. package/src/pages/admin/payments/links/create.tsx +141 -0
  198. package/src/pages/admin/payments/links/detail.tsx +318 -0
  199. package/src/pages/admin/payments/links/index.tsx +167 -0
  200. package/src/pages/admin/products/coupons/index.tsx +3 -0
  201. package/src/pages/admin/products/index.tsx +81 -0
  202. package/src/pages/admin/products/prices/actions.tsx +151 -0
  203. package/src/pages/admin/products/prices/detail.tsx +203 -0
  204. package/src/pages/admin/products/prices/list.tsx +95 -0
  205. package/src/pages/admin/products/pricing-tables.tsx +3 -0
  206. package/src/pages/admin/products/products/create.tsx +105 -0
  207. package/src/pages/admin/products/products/detail.tsx +246 -0
  208. package/src/pages/admin/products/products/index.tsx +154 -0
  209. package/src/pages/admin/settings/branding.tsx +3 -0
  210. package/src/pages/admin/settings/business.tsx +3 -0
  211. package/src/pages/admin/settings/index.tsx +47 -0
  212. package/src/pages/admin/settings/payment-methods.tsx +80 -0
  213. package/src/pages/checkout/index.tsx +38 -0
  214. package/src/pages/checkout/pay.tsx +89 -0
  215. package/src/pages/customer/index.tsx +93 -0
  216. package/src/pages/customer/invoice.tsx +147 -0
  217. package/src/pages/home.tsx +9 -0
  218. package/tsconfig.api.json +9 -0
  219. package/tsconfig.eslint.json +7 -0
  220. package/tsconfig.json +99 -0
  221. package/tsconfig.types.json +11 -0
  222. package/vite.config.ts +19 -0
@@ -0,0 +1,134 @@
1
+ import { fromTokenToUnit } from '@ocap/util';
2
+ import { Router } from 'express';
3
+ import pick from 'lodash/pick';
4
+
5
+ import { authenticate } from '../libs/security';
6
+ import { PaymentCurrency } from '../store/models/payment-currency';
7
+ import { Price } from '../store/models/price';
8
+ import { Product } from '../store/models/product';
9
+
10
+ const router = Router();
11
+
12
+ const auth = authenticate<Price>({ component: true, roles: ['owner', 'admin'] });
13
+
14
+ // FIXME: @wangshijun use schema validation, validate product exist
15
+ // create price
16
+ // eslint-disable-next-line consistent-return
17
+ router.post('/', auth, async (req, res) => {
18
+ const raw: Price & { model: 'string' } = req.body;
19
+ raw.active = true;
20
+ raw.locked = false;
21
+ raw.livemode = !!req.livemode;
22
+ raw.currency_id = raw.currency_id || req.currency.id;
23
+ raw.created_via = req.user?.via as string;
24
+
25
+ if (!raw.unit_amount) {
26
+ return res.status(400).json({ error: 'unit_amount is required' });
27
+ }
28
+
29
+ const currency = await PaymentCurrency.findByPk(raw.currency_id);
30
+ if (!currency) {
31
+ return res.status(400).json({ error: 'currency not found' });
32
+ }
33
+
34
+ raw.unit_amount = fromTokenToUnit(raw.unit_amount, currency.decimal).toString();
35
+
36
+ const price = await Price.insert(raw);
37
+
38
+ res.json(price);
39
+ });
40
+
41
+ // get price detail
42
+ router.get('/:id', auth, async (req, res) => {
43
+ const price = await Price.findByPkOrLookupKey(req.params.id as string, {
44
+ include: [
45
+ { model: Product, as: 'product' },
46
+ { model: PaymentCurrency, as: 'currency' },
47
+ ],
48
+ });
49
+
50
+ res.json(price);
51
+ });
52
+
53
+ // update price
54
+ router.put('/:id', auth, async (req, res) => {
55
+ const price = await Price.findByPkOrLookupKey(req.params.id as string);
56
+
57
+ if (!price) {
58
+ return res.status(404).json({ error: 'price not found' });
59
+ }
60
+
61
+ if (price.active === false) {
62
+ return res.status(403).json({ error: 'price archived' });
63
+ }
64
+
65
+ const raw: Partial<Price> = Price.format(
66
+ pick(
67
+ req.body,
68
+ price.locked
69
+ ? ['nickname', 'description', 'metadata']
70
+ : ['type', 'model', 'active', 'livemode', 'nickname', 'recurring', 'description', 'tiers', 'unit_amount', 'transform_quantity', 'metadata', 'lookup_key'] // prettier-ignore
71
+ )
72
+ );
73
+
74
+ if (raw.lookup_key) {
75
+ const exist = await Price.findOne({ where: { lookup_key: raw.lookup_key } });
76
+ if (exist && exist.id !== price.id) {
77
+ return res.status(400).json({ error: `lookup_key ${raw.lookup_key} already used by ${exist.id}` });
78
+ }
79
+ }
80
+
81
+ if (raw.unit_amount) {
82
+ const currency = await PaymentCurrency.findByPk(price.currency_id);
83
+ raw.unit_amount = fromTokenToUnit(raw.unit_amount, (currency as PaymentCurrency).decimal).toString();
84
+ }
85
+
86
+ await price.update(Price.format(raw));
87
+
88
+ return res.json(price);
89
+ });
90
+
91
+ // archive
92
+ router.put('/:id/archive', auth, async (req, res) => {
93
+ const price = await Price.findByPkOrLookupKey(req.params.id as string);
94
+
95
+ if (!price) {
96
+ return res.status(404).json({ error: 'price not found' });
97
+ }
98
+
99
+ if (price.active === false) {
100
+ return res.status(403).json({ error: 'price already archived' });
101
+ }
102
+
103
+ if (price.locked) {
104
+ return res.status(403).json({ error: 'price locked' });
105
+ }
106
+
107
+ await price.update({ active: false });
108
+ return res.json(price);
109
+ });
110
+
111
+ // delete price
112
+ router.delete('/:id', auth, async (req, res) => {
113
+ const price = await Price.findByPkOrLookupKey(req.params.id as string);
114
+
115
+ if (!price) {
116
+ return res.status(404).json({ error: 'Can not delete none existing price' });
117
+ }
118
+
119
+ if (price.locked) {
120
+ return res.status(403).json({ error: 'Can not delete locked price' });
121
+ }
122
+
123
+ const product = await Product.findOne({ where: { default_price_id: price.id } });
124
+ if (product) {
125
+ return res
126
+ .status(403)
127
+ .json({ error: `Can not delete price that is used as default price by product: ${product.id}` });
128
+ }
129
+
130
+ await price.destroy();
131
+ return res.json(price);
132
+ });
133
+
134
+ export default router;
@@ -0,0 +1,191 @@
1
+ import { fromTokenToUnit } from '@ocap/util';
2
+ import { Router } from 'express';
3
+ import Joi from 'joi';
4
+ import pick from 'lodash/pick';
5
+ import type { WhereOptions } from 'sequelize';
6
+
7
+ import { authenticate } from '../libs/security';
8
+ import { formatMetadata } from '../libs/util';
9
+ import { PaymentCurrency } from '../store/models/payment-currency';
10
+ import { Price } from '../store/models/price';
11
+ import { Product } from '../store/models/product';
12
+
13
+ const router = Router();
14
+
15
+ const auth = authenticate<Product>({ component: true, roles: ['owner', 'admin'] });
16
+
17
+ // FIXME: @wangshijun use schema validation
18
+ // create product and price
19
+ router.post('/', auth, async (req, res) => {
20
+ const raw: Partial<Product> = pick(req.body, [
21
+ 'name',
22
+ 'type',
23
+ 'description',
24
+ 'images',
25
+ 'metadata',
26
+ 'statement_descriptor',
27
+ 'unit_label',
28
+ 'features',
29
+ 'metadata',
30
+ ]);
31
+ raw.active = true;
32
+ raw.type = raw.type || 'service';
33
+ raw.livemode = !!req.livemode;
34
+ raw.created_via = req.user?.via;
35
+ raw.metadata = formatMetadata(raw.metadata);
36
+
37
+ const product = await Product.create(raw as Product);
38
+
39
+ if (req.body.prices?.length) {
40
+ if (req.body.prices.some((x: any) => !x.unit_amount)) {
41
+ return res.status(400).json({ error: 'unit_amount is required for price' });
42
+ }
43
+
44
+ const currency = await PaymentCurrency.findByPk(req.body.prices[0].currency_id);
45
+ if (!currency) {
46
+ return res.status(400).json({ error: 'currency_id not set for price' });
47
+ }
48
+
49
+ const pricesRaw = req.body.prices.map((price: Price & { model: 'string' }) => {
50
+ price.product_id = product.id;
51
+ price.active = product.active;
52
+ price.livemode = product.livemode;
53
+ price.unit_amount = fromTokenToUnit(price.unit_amount, currency.decimal).toString();
54
+ return price;
55
+ });
56
+
57
+ const prices = await Promise.all(pricesRaw.map((x: Price & { model: 'string' }) => Price.insert(x)));
58
+
59
+ // update default price id
60
+ product.default_price_id = prices[0].id;
61
+ await product.save();
62
+ return res.json({ ...product.toJSON(), prices: prices.map((x) => x.toJSON()) });
63
+ }
64
+
65
+ return res.json({ ...product.toJSON(), prices: [] });
66
+ });
67
+
68
+ // list products and prices
69
+ const paginationSchema = Joi.object<{
70
+ page: number;
71
+ size: number;
72
+ active?: boolean;
73
+ livemode?: boolean;
74
+ }>({
75
+ page: Joi.number().integer().min(1).default(1),
76
+ size: Joi.number().integer().min(1).max(100).default(20),
77
+ active: Joi.boolean().empty(''),
78
+ livemode: Joi.boolean().empty(''),
79
+ });
80
+ router.get('/', auth, async (req, res) => {
81
+ const { page, size, ...query } = await paginationSchema.validateAsync(req.query, { stripUnknown: true });
82
+ const where: WhereOptions<Product> = {};
83
+
84
+ if (typeof query.active === 'boolean') {
85
+ where.active = query.active;
86
+ }
87
+ if (typeof query.livemode === 'boolean') {
88
+ where.livemode = query.livemode;
89
+ }
90
+
91
+ const { rows: list, count } = await Product.findAndCountAll({
92
+ where,
93
+ order: [['created_at', 'DESC']],
94
+ offset: (page - 1) * size,
95
+ limit: size,
96
+ include: [{ model: Price, as: 'prices' }],
97
+ });
98
+
99
+ res.json({ count, list });
100
+ });
101
+
102
+ // get product detail
103
+ router.get('/:id', auth, async (req, res) => {
104
+ const product = await Product.findOne({
105
+ where: { id: req.params.id },
106
+ include: [{ model: Price, as: 'prices', include: [{ model: PaymentCurrency, as: 'currency' }] }],
107
+ });
108
+
109
+ res.json(product);
110
+ });
111
+
112
+ // update product
113
+ router.put('/:id', auth, async (req, res) => {
114
+ const product = await Product.findByPk(req.params.id);
115
+
116
+ if (!product) {
117
+ return res.status(404).json({ error: 'product not found' });
118
+ }
119
+ if (product.active === false) {
120
+ return res.status(403).json({ error: 'product archived' });
121
+ }
122
+ if (product.locked) {
123
+ return res.status(403).json({ error: 'product locked' });
124
+ }
125
+
126
+ const updates: Partial<Product> = pick(req.body, [
127
+ 'name',
128
+ 'description',
129
+ 'images',
130
+ 'metadata',
131
+ 'statement_descriptor',
132
+ 'default_price_id',
133
+ 'unit_label',
134
+ 'features',
135
+ 'metadata',
136
+ ]);
137
+ if (updates.metadata) {
138
+ updates.metadata = formatMetadata(updates.metadata);
139
+ }
140
+ await product.update(updates);
141
+
142
+ return res.json(product);
143
+ });
144
+
145
+ // archive
146
+ router.put('/:id/archive', auth, async (req, res) => {
147
+ const product = await Product.findByPk(req.params.id);
148
+
149
+ if (!product) {
150
+ return res.status(404).json({ error: 'product not found' });
151
+ }
152
+
153
+ if (product.locked) {
154
+ return res.status(403).json({ error: 'product locked' });
155
+ }
156
+
157
+ await product.update({ active: !product.active });
158
+
159
+ // FIXME: deactivate payment-links, pricing-tables
160
+
161
+ return res.json(product);
162
+ });
163
+
164
+ // delete product
165
+ router.delete('/:id', auth, async (req, res) => {
166
+ const product = await Product.findByPk(req.params.id);
167
+
168
+ if (!product) {
169
+ return res.status(404).json({ error: 'product not found' });
170
+ }
171
+
172
+ if (product.active === false) {
173
+ return res.status(403).json({ error: 'product archived' });
174
+ }
175
+
176
+ if (product.locked) {
177
+ return res.status(403).json({ error: 'product locked' });
178
+ }
179
+
180
+ const prices = await Price.findAll({ where: { product_id: product.id } });
181
+ if (prices.some((x) => x.locked)) {
182
+ return res.status(403).json({ error: 'product have prices that is locked' });
183
+ }
184
+
185
+ await product.destroy();
186
+ await Price.destroy({ where: { product_id: product.id } });
187
+
188
+ return res.json(product);
189
+ });
190
+
191
+ export default router;
@@ -0,0 +1,33 @@
1
+ import { Router } from 'express';
2
+ import pick from 'lodash/pick';
3
+ import type { WhereOptions } from 'sequelize';
4
+
5
+ import { PaymentCurrency } from '../store/models/payment-currency';
6
+ import { PaymentMethod } from '../store/models/payment-method';
7
+
8
+ const router = Router();
9
+
10
+ router.get('/', async (req, res) => {
11
+ const where: WhereOptions<PaymentMethod> = {};
12
+
13
+ where.livemode = req.livemode;
14
+
15
+ const methods = await PaymentMethod.findAll({
16
+ where,
17
+ order: [['created_at', 'DESC']],
18
+ include: [{ model: PaymentCurrency, as: 'payment_currencies' }],
19
+ });
20
+
21
+ methods.forEach((method) => {
22
+ // @ts-ignore
23
+ method.currencies = method.payment_currencies?.map((x) => pick(x, ['id', 'name', 'symbol', 'decimal', 'logo']));
24
+ });
25
+
26
+ res.json({
27
+ paymentMethods: methods.map((x) => pick(x, ['id', 'name', 'type', 'logo', 'currencies'])),
28
+ // @ts-ignore
29
+ baseCurrency: methods[0].currencies[0],
30
+ });
31
+ });
32
+
33
+ export default router;
@@ -0,0 +1,148 @@
1
+ import { Router } from 'express';
2
+ import Joi from 'joi';
3
+ import pick from 'lodash/pick';
4
+ import type { WhereOptions } from 'sequelize';
5
+
6
+ import { authenticate } from '../libs/security';
7
+ import { expandLineItems } from '../libs/session';
8
+ import { formatMetadata } from '../libs/util';
9
+ import { Price, Product, SubscriptionItem, UsageRecord } from '../store/models';
10
+
11
+ const router = Router();
12
+ const auth = authenticate<SubscriptionItem>({ component: true, roles: ['owner', 'admin'] });
13
+
14
+ // FIXME: handle payment_behavior, proration_behavior
15
+ // @link https://stripe.com/docs/api/subscription_items/create
16
+ router.post('/', auth, async (req, res) => {
17
+ const raw: Partial<SubscriptionItem> = pick(req.body, [
18
+ 'subscription_id',
19
+ 'price_id',
20
+ 'quantity',
21
+ 'billing_thresholds',
22
+ 'metadata',
23
+ ]);
24
+ const exist = await SubscriptionItem.findOne({
25
+ where: { price_id: raw.price_id, subscription_id: raw.subscription_id },
26
+ });
27
+ if (exist) {
28
+ return res.status(400).json({ error: `SubscriptionItem already exist: ${exist.id}` });
29
+ }
30
+
31
+ raw.livemode = req.livemode;
32
+ if (raw.metadata) {
33
+ raw.metadata = formatMetadata(raw.metadata);
34
+ }
35
+
36
+ const doc = await SubscriptionItem.create(raw as SubscriptionItem);
37
+ return res.json(doc);
38
+ });
39
+
40
+ // @link https://stripe.com/docs/api/subscription_items/list
41
+ const schema = Joi.object<{
42
+ page: number;
43
+ size: number;
44
+ subscription_id: string;
45
+ livemode?: boolean;
46
+ }>({
47
+ page: Joi.number().integer().min(1).default(1),
48
+ size: Joi.number().integer().min(1).max(100).default(20),
49
+ subscription_id: Joi.string().required(),
50
+ livemode: Joi.boolean().empty(''),
51
+ });
52
+ router.get('/', auth, async (req, res) => {
53
+ const { page, size, ...query } = await schema.validateAsync(req.query, { stripUnknown: true });
54
+ const where: WhereOptions<SubscriptionItem> = { subscription_id: query.subscription_id };
55
+
56
+ if (typeof query.livemode === 'boolean') {
57
+ where.livemode = query.livemode;
58
+ }
59
+
60
+ try {
61
+ const { rows, count } = await SubscriptionItem.findAndCountAll({
62
+ where,
63
+ order: [['created_at', 'DESC']],
64
+ offset: (page - 1) * size,
65
+ limit: size,
66
+ include: [],
67
+ });
68
+
69
+ const list = rows.map((x) => x.toJSON());
70
+ const products = (await Product.findAll()).map((x) => x.toJSON());
71
+ const prices = (await Price.findAll()).map((x) => x.toJSON());
72
+
73
+ // @ts-ignore
74
+ expandLineItems(list, products, prices);
75
+
76
+ res.json({ count, list });
77
+ } catch (err) {
78
+ console.error(err);
79
+ res.json({ count: 0, list: [] });
80
+ }
81
+ });
82
+
83
+ // @link https://stripe.com/docs/api/subscription_items/retrieve
84
+ router.get('/:id', auth, async (req, res) => {
85
+ try {
86
+ const doc = await SubscriptionItem.findOne({
87
+ where: { id: req.params.id },
88
+ include: [{ model: Price, as: 'price' }],
89
+ });
90
+ res.json(doc);
91
+ } catch (err) {
92
+ console.error(err);
93
+ res.json(null);
94
+ }
95
+ });
96
+
97
+ router.put('/:id', auth, async (req, res) => {
98
+ const doc = await SubscriptionItem.findByPk(req.params.id);
99
+
100
+ if (!doc) {
101
+ return res.status(404).json({ error: `SubscriptionItem not found: ${req.params.id}` });
102
+ }
103
+
104
+ const updates: Partial<SubscriptionItem> = pick(req.body, ['price_id', 'quantity', 'billing_thresholds', 'metadata']);
105
+ if (updates.price_id) {
106
+ const exist = await SubscriptionItem.findOne({
107
+ where: { price_id: updates.price_id, subscription_id: doc.subscription_id },
108
+ });
109
+ if (exist) {
110
+ return res.status(400).json({ error: `SubscriptionItem already exist: ${exist.id}` });
111
+ }
112
+
113
+ if (!updates.quantity) {
114
+ updates.quantity = 1;
115
+ }
116
+ }
117
+
118
+ if (updates.metadata) {
119
+ updates.metadata = formatMetadata(updates.metadata);
120
+ }
121
+
122
+ await doc.update(updates);
123
+
124
+ return res.json(doc);
125
+ });
126
+
127
+ // TODO: handle proration_behavior
128
+ // @link https://stripe.com/docs/api/subscription_items/delete
129
+ router.delete('/:id', auth, async (req, res) => {
130
+ const doc = await SubscriptionItem.findByPk(req.params.id);
131
+
132
+ if (!doc) {
133
+ return res.status(404).json({ error: 'webhook endpoint not found' });
134
+ }
135
+
136
+ if (req.body.clear_usage) {
137
+ const price = await Price.findByPk(doc.price_id);
138
+ if (price?.recurring?.usage_type === 'metered') {
139
+ await UsageRecord.destroy({ where: { subscription_item_id: doc.id } });
140
+ }
141
+ }
142
+
143
+ await doc.destroy();
144
+
145
+ return res.json(doc);
146
+ });
147
+
148
+ export default router;