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.
- package/.eslintrc.js +15 -0
- package/README.md +3 -0
- package/api/dev.ts +6 -0
- package/api/hooks/pre-start.js +12 -0
- package/api/src/hooks/pre-start.ts +21 -0
- package/api/src/index.ts +92 -0
- package/api/src/jobs/event.ts +72 -0
- package/api/src/jobs/invoice.ts +148 -0
- package/api/src/jobs/payment.ts +208 -0
- package/api/src/jobs/subscription.ts +301 -0
- package/api/src/jobs/webhook.ts +113 -0
- package/api/src/libs/audit.ts +73 -0
- package/api/src/libs/auth.ts +40 -0
- package/api/src/libs/chain/arcblock.ts +13 -0
- package/api/src/libs/dayjs.ts +17 -0
- package/api/src/libs/env.ts +5 -0
- package/api/src/libs/hooks.ts +42 -0
- package/api/src/libs/logger.ts +27 -0
- package/api/src/libs/middleware.ts +12 -0
- package/api/src/libs/payment.ts +53 -0
- package/api/src/libs/queue/index.ts +263 -0
- package/api/src/libs/queue/store.ts +47 -0
- package/api/src/libs/security.ts +95 -0
- package/api/src/libs/session.ts +164 -0
- package/api/src/libs/util.ts +93 -0
- package/api/src/locales/en.ts +3 -0
- package/api/src/locales/index.ts +37 -0
- package/api/src/locales/zh.ts +3 -0
- package/api/src/routes/checkout-sessions.ts +536 -0
- package/api/src/routes/connect/collect.ts +109 -0
- package/api/src/routes/connect/pay.ts +116 -0
- package/api/src/routes/connect/setup.ts +121 -0
- package/api/src/routes/connect/shared.ts +410 -0
- package/api/src/routes/connect/subscribe.ts +128 -0
- package/api/src/routes/customers.ts +70 -0
- package/api/src/routes/events.ts +76 -0
- package/api/src/routes/index.ts +59 -0
- package/api/src/routes/invoices.ts +126 -0
- package/api/src/routes/payment-currencies.ts +38 -0
- package/api/src/routes/payment-intents.ts +122 -0
- package/api/src/routes/payment-links.ts +221 -0
- package/api/src/routes/payment-methods.ts +39 -0
- package/api/src/routes/prices.ts +134 -0
- package/api/src/routes/products.ts +191 -0
- package/api/src/routes/settings.ts +33 -0
- package/api/src/routes/subscription-items.ts +148 -0
- package/api/src/routes/subscriptions.ts +254 -0
- package/api/src/routes/usage-records.ts +120 -0
- package/api/src/routes/webhook-attempts.ts +57 -0
- package/api/src/routes/webhook-endpoints.ts +105 -0
- package/api/src/store/migrate.ts +16 -0
- package/api/src/store/migrations/20230905-genesis.ts +52 -0
- package/api/src/store/migrations/20230911-seeding.ts +145 -0
- package/api/src/store/models/checkout-session.ts +395 -0
- package/api/src/store/models/coupon.ts +137 -0
- package/api/src/store/models/customer.ts +199 -0
- package/api/src/store/models/discount.ts +116 -0
- package/api/src/store/models/event.ts +111 -0
- package/api/src/store/models/index.ts +165 -0
- package/api/src/store/models/invoice-item.ts +185 -0
- package/api/src/store/models/invoice.ts +492 -0
- package/api/src/store/models/job.ts +75 -0
- package/api/src/store/models/payment-currency.ts +139 -0
- package/api/src/store/models/payment-intent.ts +282 -0
- package/api/src/store/models/payment-link.ts +219 -0
- package/api/src/store/models/payment-method.ts +169 -0
- package/api/src/store/models/price.ts +266 -0
- package/api/src/store/models/product.ts +162 -0
- package/api/src/store/models/promotion-code.ts +112 -0
- package/api/src/store/models/setup-intent.ts +206 -0
- package/api/src/store/models/subscription-item.ts +103 -0
- package/api/src/store/models/subscription-schedule.ts +157 -0
- package/api/src/store/models/subscription.ts +307 -0
- package/api/src/store/models/types.ts +406 -0
- package/api/src/store/models/usage-record.ts +132 -0
- package/api/src/store/models/webhook-attempt.ts +96 -0
- package/api/src/store/models/webhook-endpoint.ts +96 -0
- package/api/src/store/sequelize.ts +15 -0
- package/api/third.d.ts +28 -0
- package/blocklet.md +3 -0
- package/blocklet.yml +89 -0
- package/index.html +14 -0
- package/logo.png +0 -0
- package/package.json +133 -0
- package/public/.gitkeep +0 -0
- package/screenshots/.gitkeep +0 -0
- package/screenshots/1-subscription.png +0 -0
- package/screenshots/2-customer-1.png +0 -0
- package/screenshots/3-customer-2.png +0 -0
- package/screenshots/4-admin-3.png +0 -0
- package/screenshots/5-admin-4.png +0 -0
- package/scripts/build-clean.js +6 -0
- package/scripts/bump-version.mjs +35 -0
- package/src/app.tsx +68 -0
- package/src/components/actions.tsx +85 -0
- package/src/components/blockchain/tx.tsx +29 -0
- package/src/components/checkout/amount.tsx +24 -0
- package/src/components/checkout/error.tsx +30 -0
- package/src/components/checkout/footer.tsx +12 -0
- package/src/components/checkout/form/address.tsx +38 -0
- package/src/components/checkout/form/index.tsx +295 -0
- package/src/components/checkout/header.tsx +23 -0
- package/src/components/checkout/pay.tsx +222 -0
- package/src/components/checkout/product-card.tsx +56 -0
- package/src/components/checkout/product-item.tsx +37 -0
- package/src/components/checkout/skeleton/overview.tsx +21 -0
- package/src/components/checkout/skeleton/payment.tsx +35 -0
- package/src/components/checkout/success.tsx +183 -0
- package/src/components/checkout/summary.tsx +34 -0
- package/src/components/collapse.tsx +50 -0
- package/src/components/confirm.tsx +55 -0
- package/src/components/copyable.tsx +38 -0
- package/src/components/currency.tsx +15 -0
- package/src/components/customer/actions.tsx +73 -0
- package/src/components/data.tsx +20 -0
- package/src/components/drawer-form.tsx +77 -0
- package/src/components/error-fallback.tsx +7 -0
- package/src/components/error.tsx +39 -0
- package/src/components/event/list.tsx +217 -0
- package/src/components/info-card.tsx +40 -0
- package/src/components/info-metric.tsx +35 -0
- package/src/components/info-row.tsx +28 -0
- package/src/components/input.tsx +40 -0
- package/src/components/invoice/action.tsx +94 -0
- package/src/components/invoice/list.tsx +225 -0
- package/src/components/invoice/table.tsx +110 -0
- package/src/components/layout.tsx +70 -0
- package/src/components/livemode.tsx +23 -0
- package/src/components/metadata/editor.tsx +57 -0
- package/src/components/metadata/form.tsx +45 -0
- package/src/components/payment-intent/actions.tsx +81 -0
- package/src/components/payment-intent/list.tsx +204 -0
- package/src/components/payment-link/actions.tsx +114 -0
- package/src/components/payment-link/after-pay.tsx +87 -0
- package/src/components/payment-link/before-pay.tsx +175 -0
- package/src/components/payment-link/item.tsx +135 -0
- package/src/components/payment-link/product-select.tsx +66 -0
- package/src/components/payment-link/rename.tsx +64 -0
- package/src/components/portal/invoice/list.tsx +110 -0
- package/src/components/portal/subscription/cancel.tsx +83 -0
- package/src/components/portal/subscription/list.tsx +232 -0
- package/src/components/price/actions.tsx +21 -0
- package/src/components/price/form.tsx +292 -0
- package/src/components/product/actions.tsx +125 -0
- package/src/components/product/add-price.tsx +59 -0
- package/src/components/product/create.tsx +97 -0
- package/src/components/product/edit-price.tsx +75 -0
- package/src/components/product/edit.tsx +67 -0
- package/src/components/product/features.tsx +32 -0
- package/src/components/product/form.tsx +76 -0
- package/src/components/relative-time.tsx +41 -0
- package/src/components/section/header.tsx +29 -0
- package/src/components/status.tsx +12 -0
- package/src/components/subscription/actions/cancel.tsx +66 -0
- package/src/components/subscription/actions/index.tsx +172 -0
- package/src/components/subscription/actions/pause.tsx +83 -0
- package/src/components/subscription/items/actions.tsx +31 -0
- package/src/components/subscription/items/index.tsx +107 -0
- package/src/components/subscription/list.tsx +200 -0
- package/src/components/switch.tsx +48 -0
- package/src/components/table.tsx +66 -0
- package/src/components/uploader.tsx +81 -0
- package/src/components/webhook/attempts.tsx +149 -0
- package/src/contexts/products.tsx +42 -0
- package/src/contexts/session.ts +10 -0
- package/src/contexts/settings.tsx +54 -0
- package/src/env.d.ts +17 -0
- package/src/global.css +97 -0
- package/src/hooks/mobile.ts +15 -0
- package/src/index.tsx +6 -0
- package/src/libs/api.ts +19 -0
- package/src/libs/dayjs.ts +17 -0
- package/src/libs/util.ts +474 -0
- package/src/locales/en.tsx +395 -0
- package/src/locales/index.tsx +8 -0
- package/src/locales/zh.tsx +389 -0
- package/src/pages/admin/billing/index.tsx +56 -0
- package/src/pages/admin/billing/invoices/detail.tsx +215 -0
- package/src/pages/admin/billing/invoices/index.tsx +5 -0
- package/src/pages/admin/billing/subscriptions/detail.tsx +237 -0
- package/src/pages/admin/billing/subscriptions/index.tsx +5 -0
- package/src/pages/admin/customers/customers/detail.tsx +209 -0
- package/src/pages/admin/customers/customers/index.tsx +109 -0
- package/src/pages/admin/customers/index.tsx +47 -0
- package/src/pages/admin/developers/events/detail.tsx +77 -0
- package/src/pages/admin/developers/events/index.tsx +5 -0
- package/src/pages/admin/developers/index.tsx +60 -0
- package/src/pages/admin/developers/logs.tsx +3 -0
- package/src/pages/admin/developers/overview.tsx +3 -0
- package/src/pages/admin/developers/webhooks/detail.tsx +109 -0
- package/src/pages/admin/developers/webhooks/index.tsx +102 -0
- package/src/pages/admin/index.tsx +120 -0
- package/src/pages/admin/overview.tsx +3 -0
- package/src/pages/admin/payments/index.tsx +65 -0
- package/src/pages/admin/payments/intents/detail.tsx +205 -0
- package/src/pages/admin/payments/intents/index.tsx +5 -0
- package/src/pages/admin/payments/links/create.tsx +141 -0
- package/src/pages/admin/payments/links/detail.tsx +318 -0
- package/src/pages/admin/payments/links/index.tsx +167 -0
- package/src/pages/admin/products/coupons/index.tsx +3 -0
- package/src/pages/admin/products/index.tsx +81 -0
- package/src/pages/admin/products/prices/actions.tsx +151 -0
- package/src/pages/admin/products/prices/detail.tsx +203 -0
- package/src/pages/admin/products/prices/list.tsx +95 -0
- package/src/pages/admin/products/pricing-tables.tsx +3 -0
- package/src/pages/admin/products/products/create.tsx +105 -0
- package/src/pages/admin/products/products/detail.tsx +246 -0
- package/src/pages/admin/products/products/index.tsx +154 -0
- package/src/pages/admin/settings/branding.tsx +3 -0
- package/src/pages/admin/settings/business.tsx +3 -0
- package/src/pages/admin/settings/index.tsx +47 -0
- package/src/pages/admin/settings/payment-methods.tsx +80 -0
- package/src/pages/checkout/index.tsx +38 -0
- package/src/pages/checkout/pay.tsx +89 -0
- package/src/pages/customer/index.tsx +93 -0
- package/src/pages/customer/invoice.tsx +147 -0
- package/src/pages/home.tsx +9 -0
- package/tsconfig.api.json +9 -0
- package/tsconfig.eslint.json +7 -0
- package/tsconfig.json +99 -0
- package/tsconfig.types.json +11 -0
- 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;
|