payment-kit 1.13.39 → 1.13.40
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/api/src/routes/checkout-sessions.ts +183 -47
- package/api/src/routes/payment-links.ts +2 -0
- package/api/src/routes/pricing-table.ts +23 -6
- package/api/src/routes/products.ts +4 -40
- package/api/src/routes/redirect.ts +4 -2
- package/api/src/store/migrations/20231030-crosssell.ts +23 -0
- package/api/src/store/models/checkout-session.ts +11 -5
- package/api/src/store/models/index.ts +9 -1
- package/api/src/store/models/payment-link.ts +6 -0
- package/api/src/store/models/price.ts +23 -18
- package/api/src/store/models/product.ts +73 -14
- package/api/src/store/models/types.ts +3 -0
- package/blocklet.yml +1 -1
- package/package.json +10 -10
- package/src/components/checkout/pay.tsx +23 -0
- package/src/components/checkout/product-item.tsx +30 -20
- package/src/components/checkout/summary.tsx +112 -4
- package/src/components/payment-link/before-pay.tsx +16 -0
- package/src/components/price/upsell-select.tsx +7 -2
- package/src/components/pricing-table/payment-settings.tsx +16 -0
- package/src/components/pricing-table/product-settings.tsx +1 -0
- package/src/components/product/cross-sell-select.tsx +51 -0
- package/src/components/product/cross-sell.tsx +83 -0
- package/src/locales/en.tsx +11 -0
- package/src/locales/zh.tsx +11 -0
- package/src/pages/admin/payments/links/create.tsx +1 -0
- package/src/pages/admin/products/products/detail.tsx +7 -0
- package/src/pages/checkout/pay.tsx +3 -5
- package/src/pages/checkout/pricing-table.tsx +1 -1
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/* eslint-disable consistent-return */
|
|
2
2
|
import { getUrl } from '@blocklet/sdk/lib/component';
|
|
3
3
|
import userMiddleware from '@blocklet/sdk/lib/middlewares/user';
|
|
4
|
-
import { Request, Response, Router } from 'express';
|
|
4
|
+
import { NextFunction, Request, Response, Router } from 'express';
|
|
5
5
|
import cloneDeep from 'lodash/cloneDeep';
|
|
6
6
|
import merge from 'lodash/merge';
|
|
7
7
|
import omit from 'lodash/omit';
|
|
@@ -32,14 +32,15 @@ import {
|
|
|
32
32
|
isLineItemAligned,
|
|
33
33
|
} from '../libs/session';
|
|
34
34
|
import { createCodeGenerator, formatMetadata } from '../libs/util';
|
|
35
|
-
import type {
|
|
35
|
+
import type { TPriceExpanded, TProductExpanded } from '../store/models';
|
|
36
36
|
import { CheckoutSession } from '../store/models/checkout-session';
|
|
37
37
|
import { Customer } from '../store/models/customer';
|
|
38
|
-
import { PaymentCurrency } from '../store/models/payment-currency';
|
|
38
|
+
import { PaymentCurrency, TPaymentCurrency } from '../store/models/payment-currency';
|
|
39
39
|
import { PaymentIntent } from '../store/models/payment-intent';
|
|
40
40
|
import { PaymentLink } from '../store/models/payment-link';
|
|
41
41
|
import { PaymentMethod } from '../store/models/payment-method';
|
|
42
42
|
import { Price } from '../store/models/price';
|
|
43
|
+
import { Product } from '../store/models/product';
|
|
43
44
|
import { SetupIntent } from '../store/models/setup-intent';
|
|
44
45
|
import { Subscription } from '../store/models/subscription';
|
|
45
46
|
import { SubscriptionItem } from '../store/models/subscription-item';
|
|
@@ -91,6 +92,7 @@ export const formatCheckoutSession = async (payload: any, throwOnEmptyItems = tr
|
|
|
91
92
|
},
|
|
92
93
|
payment_intent_data: {},
|
|
93
94
|
submit_type: 'pay',
|
|
95
|
+
cross_sell_behavior: 'auto',
|
|
94
96
|
},
|
|
95
97
|
pick(payload, [
|
|
96
98
|
'currency_id',
|
|
@@ -98,6 +100,7 @@ export const formatCheckoutSession = async (payload: any, throwOnEmptyItems = tr
|
|
|
98
100
|
'line_items',
|
|
99
101
|
'allow_promotion_codes',
|
|
100
102
|
'consent_collection',
|
|
103
|
+
'cross_sell_behavior',
|
|
101
104
|
'custom_fields',
|
|
102
105
|
'customer_creation',
|
|
103
106
|
'invoice_creation',
|
|
@@ -193,6 +196,81 @@ export async function getCheckoutSessionAmounts(checkoutSession: CheckoutSession
|
|
|
193
196
|
};
|
|
194
197
|
}
|
|
195
198
|
|
|
199
|
+
export async function ensureCheckoutSessionOpen(req: Request, res: Response, next: NextFunction) {
|
|
200
|
+
const doc = await CheckoutSession.findByPk(req.params.id);
|
|
201
|
+
|
|
202
|
+
if (!doc) {
|
|
203
|
+
return res.status(404).json({ error: 'Checkout session not found' });
|
|
204
|
+
}
|
|
205
|
+
if (doc.status === 'complete') {
|
|
206
|
+
return res.status(403).json({ error: 'Checkout session completed' });
|
|
207
|
+
}
|
|
208
|
+
if (doc.status === 'expired') {
|
|
209
|
+
return res.status(403).json({ error: 'Checkout session already expired' });
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
req.doc = doc;
|
|
213
|
+
|
|
214
|
+
next();
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
export async function getCrossSellItem(checkoutSession: CheckoutSession) {
|
|
218
|
+
// FIXME: perhaps we can support cross sell even if the current session have multiple items
|
|
219
|
+
if (checkoutSession.line_items.length > 1) {
|
|
220
|
+
return { error: 'Cross sell not supported for checkoutSession with multiple line items' };
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const items = await Price.expand(checkoutSession.line_items, { upsell: false });
|
|
224
|
+
const item = items.find(
|
|
225
|
+
(x) => x.upsell_price?.product?.cross_sell?.cross_sells_to_id || x.price.product.cross_sell?.cross_sells_to_id
|
|
226
|
+
);
|
|
227
|
+
if (!item) {
|
|
228
|
+
return { error: 'Cross sell not configured for all line item products' };
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const to = (await Product.expand(
|
|
232
|
+
item.upsell_price?.product?.cross_sell.cross_sells_to_id || item.price.product.cross_sell.cross_sells_to_id
|
|
233
|
+
)) as TProductExpanded;
|
|
234
|
+
if (!to) {
|
|
235
|
+
return { error: 'Cross sell configure not valid anymore' };
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
const from = item.upsell_price || item.price;
|
|
239
|
+
|
|
240
|
+
const toCrossSellProduct = (price: TPriceExpanded) => ({
|
|
241
|
+
...price,
|
|
242
|
+
product: omit(to, ['prices', 'default_price', 'cross_sell']),
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
// one_time can only cross sell to one_time
|
|
246
|
+
if (from.type === 'one_time') {
|
|
247
|
+
const oneTime = to.prices.find((x) => x.type === 'one_time');
|
|
248
|
+
if (oneTime) {
|
|
249
|
+
return toCrossSellProduct(oneTime);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// recurring can cross sell to matching recurring or one_time
|
|
254
|
+
if (from.type === 'recurring') {
|
|
255
|
+
const recurring = to.prices.find(
|
|
256
|
+
(x) =>
|
|
257
|
+
x.type === 'recurring' &&
|
|
258
|
+
x.recurring?.interval === from.recurring?.interval &&
|
|
259
|
+
x.recurring?.interval_count === from.recurring?.interval_count
|
|
260
|
+
);
|
|
261
|
+
if (recurring) {
|
|
262
|
+
return toCrossSellProduct(recurring);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
const oneTime = to.prices.find((x) => x.type === 'one_time');
|
|
266
|
+
if (oneTime) {
|
|
267
|
+
return toCrossSellProduct(oneTime);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
return { error: 'Cross sell not suitable' };
|
|
272
|
+
}
|
|
273
|
+
|
|
196
274
|
// create checkout session
|
|
197
275
|
router.post('/', auth, async (req, res) => {
|
|
198
276
|
const raw: Partial<CheckoutSession> = await formatCheckoutSession(req.body);
|
|
@@ -324,22 +402,23 @@ router.get('/retrieve/:id', user, async (req, res) => {
|
|
|
324
402
|
});
|
|
325
403
|
|
|
326
404
|
// submit order
|
|
327
|
-
router.put('/:id/submit', user, async (req, res) => {
|
|
405
|
+
router.put('/:id/submit', user, ensureCheckoutSessionOpen, async (req, res) => {
|
|
328
406
|
try {
|
|
329
407
|
if (!req.user) {
|
|
330
408
|
return res.status(403).json({ error: 'Please login to continue' });
|
|
331
409
|
}
|
|
332
410
|
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
411
|
+
const checkoutSession = req.doc as CheckoutSession;
|
|
412
|
+
|
|
413
|
+
// validate cross sell
|
|
414
|
+
if (checkoutSession.cross_sell_behavior === 'required') {
|
|
415
|
+
if (checkoutSession.line_items.some((x) => x.cross_sell) === false) {
|
|
416
|
+
const result = await getCrossSellItem(checkoutSession);
|
|
417
|
+
// @ts-ignore
|
|
418
|
+
if (result.id) {
|
|
419
|
+
return res.status(400).json({ error: 'Please select cross sell product to continue' });
|
|
420
|
+
}
|
|
421
|
+
}
|
|
343
422
|
}
|
|
344
423
|
|
|
345
424
|
// validate payment settings
|
|
@@ -694,19 +773,10 @@ router.put('/:id/submit', user, async (req, res) => {
|
|
|
694
773
|
});
|
|
695
774
|
|
|
696
775
|
// upsell
|
|
697
|
-
router.put('/:id/upsell', user, async (req, res) => {
|
|
776
|
+
router.put('/:id/upsell', user, ensureCheckoutSessionOpen, async (req, res) => {
|
|
698
777
|
try {
|
|
699
778
|
// validate session
|
|
700
|
-
const checkoutSession =
|
|
701
|
-
if (!checkoutSession) {
|
|
702
|
-
return res.status(404).json({ error: 'Checkout session not found' });
|
|
703
|
-
}
|
|
704
|
-
if (checkoutSession.status === 'complete') {
|
|
705
|
-
return res.status(403).json({ error: 'Checkout session completed' });
|
|
706
|
-
}
|
|
707
|
-
if (checkoutSession.status === 'expired') {
|
|
708
|
-
return res.status(403).json({ error: 'Checkout session expired' });
|
|
709
|
-
}
|
|
779
|
+
const checkoutSession = req.doc as CheckoutSession;
|
|
710
780
|
|
|
711
781
|
if (checkoutSession.line_items) {
|
|
712
782
|
// validate line items
|
|
@@ -749,19 +819,10 @@ router.put('/:id/upsell', user, async (req, res) => {
|
|
|
749
819
|
}
|
|
750
820
|
});
|
|
751
821
|
|
|
752
|
-
router.put('/:id/downsell', user, async (req, res) => {
|
|
822
|
+
router.put('/:id/downsell', user, ensureCheckoutSessionOpen, async (req, res) => {
|
|
753
823
|
try {
|
|
754
824
|
// validate session
|
|
755
|
-
const checkoutSession =
|
|
756
|
-
if (!checkoutSession) {
|
|
757
|
-
return res.status(404).json({ error: 'Checkout session not found' });
|
|
758
|
-
}
|
|
759
|
-
if (checkoutSession.status === 'complete') {
|
|
760
|
-
return res.status(403).json({ error: 'Checkout session completed' });
|
|
761
|
-
}
|
|
762
|
-
if (checkoutSession.status === 'expired') {
|
|
763
|
-
return res.status(403).json({ error: 'Checkout session expired' });
|
|
764
|
-
}
|
|
825
|
+
const checkoutSession = req.doc as CheckoutSession;
|
|
765
826
|
|
|
766
827
|
// validate from
|
|
767
828
|
const from = await Price.findByPk(req.body.from);
|
|
@@ -793,22 +854,97 @@ router.put('/:id/downsell', user, async (req, res) => {
|
|
|
793
854
|
});
|
|
794
855
|
|
|
795
856
|
// eslint-disable-next-line consistent-return
|
|
796
|
-
router.put('/:id/expire', auth, async (req, res) => {
|
|
797
|
-
const doc =
|
|
857
|
+
router.put('/:id/expire', auth, ensureCheckoutSessionOpen, async (req, res) => {
|
|
858
|
+
const doc = req.doc as CheckoutSession;
|
|
798
859
|
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
860
|
+
await doc.update({ status: 'expired', expires_at: dayjs().unix() });
|
|
861
|
+
|
|
862
|
+
res.json(doc);
|
|
863
|
+
});
|
|
864
|
+
|
|
865
|
+
// Return the expanded price to cross-sell-to for the checkout session
|
|
866
|
+
router.get('/:id/cross-sell', user, ensureCheckoutSessionOpen, async (req, res) => {
|
|
867
|
+
try {
|
|
868
|
+
const checkoutSession = req.doc as CheckoutSession;
|
|
869
|
+
const result = await getCrossSellItem(checkoutSession);
|
|
870
|
+
// @ts-ignore
|
|
871
|
+
return res.status(result.error ? 400 : 200).json(result);
|
|
872
|
+
} catch (err) {
|
|
873
|
+
console.error(err);
|
|
874
|
+
res.status(500).json({ error: err.message });
|
|
804
875
|
}
|
|
805
|
-
|
|
806
|
-
|
|
876
|
+
});
|
|
877
|
+
|
|
878
|
+
router.put('/:id/cross-sell', user, ensureCheckoutSessionOpen, async (req, res) => {
|
|
879
|
+
try {
|
|
880
|
+
const checkoutSession = req.doc as CheckoutSession;
|
|
881
|
+
if (!req.body.to) {
|
|
882
|
+
return res.status(400).json({ error: 'Cross sell item is required' });
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
const result = await getCrossSellItem(checkoutSession);
|
|
886
|
+
// @ts-ignore
|
|
887
|
+
if (result.error) {
|
|
888
|
+
return res.status(400).json(result);
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
// @ts-ignore
|
|
892
|
+
const to = result as TPriceExpanded;
|
|
893
|
+
if (to.id !== req.body.to) {
|
|
894
|
+
return res.status(400).json({ error: 'Cross sell item does not match' });
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
if (checkoutSession.line_items) {
|
|
898
|
+
const index = checkoutSession.line_items.findIndex((x) => x.upsell_price_id === to.id);
|
|
899
|
+
if (index > -1) {
|
|
900
|
+
return res.status(400).json({ error: 'Cross sell item already exist' });
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
const items = cloneDeep(checkoutSession.line_items);
|
|
904
|
+
items.push({
|
|
905
|
+
price_id: to.id,
|
|
906
|
+
quantity: 1,
|
|
907
|
+
cross_sell: true,
|
|
908
|
+
});
|
|
909
|
+
await checkoutSession.update({ line_items: items });
|
|
910
|
+
logger.info('CheckoutSession updated on add cross-sell', { id: req.params.id, crossSell: to.id });
|
|
911
|
+
|
|
912
|
+
// recalculate amount
|
|
913
|
+
await checkoutSession.update(await getCheckoutSessionAmounts(checkoutSession));
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
const items = await Price.expand(checkoutSession.line_items, { upsell: true });
|
|
917
|
+
res.json({ ...checkoutSession.toJSON(), line_items: items });
|
|
918
|
+
} catch (err) {
|
|
919
|
+
console.error(err);
|
|
920
|
+
res.status(500).json({ error: err.message });
|
|
807
921
|
}
|
|
922
|
+
});
|
|
808
923
|
|
|
809
|
-
|
|
924
|
+
router.delete('/:id/cross-sell', user, ensureCheckoutSessionOpen, async (req, res) => {
|
|
925
|
+
try {
|
|
926
|
+
const checkoutSession = req.doc as CheckoutSession;
|
|
927
|
+
if (checkoutSession.line_items) {
|
|
928
|
+
const index = checkoutSession.line_items.findIndex((x) => x.cross_sell);
|
|
929
|
+
if (index === -1) {
|
|
930
|
+
return res.status(400).json({ error: 'Cross sell item not exist' });
|
|
931
|
+
}
|
|
810
932
|
|
|
811
|
-
|
|
933
|
+
const items = cloneDeep(checkoutSession.line_items);
|
|
934
|
+
items.splice(index, 1);
|
|
935
|
+
await checkoutSession.update({ line_items: items });
|
|
936
|
+
logger.info('CheckoutSession updated on remove cross-sell', { id: req.params.id });
|
|
937
|
+
|
|
938
|
+
// recalculate amount
|
|
939
|
+
await checkoutSession.update(await getCheckoutSessionAmounts(checkoutSession));
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
const items = await Price.expand(checkoutSession.line_items, { upsell: true });
|
|
943
|
+
res.json({ ...checkoutSession.toJSON(), line_items: items });
|
|
944
|
+
} catch (err) {
|
|
945
|
+
console.error(err);
|
|
946
|
+
res.status(500).json({ error: err.message });
|
|
947
|
+
}
|
|
812
948
|
});
|
|
813
949
|
|
|
814
950
|
export default router;
|
|
@@ -44,6 +44,7 @@ const formatBeforeSave = (payload: any) => {
|
|
|
44
44
|
factory: '',
|
|
45
45
|
},
|
|
46
46
|
submit_type: 'pay',
|
|
47
|
+
cross_sell_behavior: 'auto',
|
|
47
48
|
},
|
|
48
49
|
pick(payload, [
|
|
49
50
|
'name',
|
|
@@ -60,6 +61,7 @@ const formatBeforeSave = (payload: any) => {
|
|
|
60
61
|
'submit_type',
|
|
61
62
|
'subscription_data',
|
|
62
63
|
'nft_mint_settings',
|
|
64
|
+
'cross_sell_behavior',
|
|
63
65
|
'metadata',
|
|
64
66
|
])
|
|
65
67
|
);
|
|
@@ -6,6 +6,7 @@ import uniq from 'lodash/uniq';
|
|
|
6
6
|
import { Op, WhereOptions } from 'sequelize';
|
|
7
7
|
|
|
8
8
|
import { checkPassportForPricingTable } from '../integrations/blocklet/passport';
|
|
9
|
+
import logger from '../libs/logger';
|
|
9
10
|
import { authenticate } from '../libs/security';
|
|
10
11
|
import { isLineItemCurrencyAligned } from '../libs/session';
|
|
11
12
|
import { formatMetadata } from '../libs/util';
|
|
@@ -68,21 +69,23 @@ const formatPricingTable = (payload: any) => {
|
|
|
68
69
|
factory: '',
|
|
69
70
|
},
|
|
70
71
|
submit_type: 'auto',
|
|
72
|
+
cross_sell_behavior: 'auto',
|
|
71
73
|
},
|
|
72
74
|
pick(x, [
|
|
73
|
-
'product_id',
|
|
74
|
-
'price_id',
|
|
75
|
-
'is_highlight',
|
|
76
|
-
'highlight_text',
|
|
77
75
|
'adjustable_quantity',
|
|
78
76
|
'after_completion',
|
|
79
77
|
'allow_promotion_codes',
|
|
78
|
+
'billing_address_collection',
|
|
80
79
|
'consent_collection',
|
|
80
|
+
'cross_sell_behavior',
|
|
81
81
|
'custom_fields',
|
|
82
|
+
'highlight_text',
|
|
83
|
+
'is_highlight',
|
|
84
|
+
'nft_mint_settings',
|
|
82
85
|
'phone_number_collection',
|
|
83
|
-
'
|
|
86
|
+
'price_id',
|
|
87
|
+
'product_id',
|
|
84
88
|
'submit_type',
|
|
85
|
-
'nft_mint_settings',
|
|
86
89
|
'subscription_data',
|
|
87
90
|
])
|
|
88
91
|
);
|
|
@@ -330,6 +333,7 @@ router.post('/:id/checkout/:priceId', async (req, res) => {
|
|
|
330
333
|
'phone_number_collection',
|
|
331
334
|
'billing_address_collection',
|
|
332
335
|
'submit_type',
|
|
336
|
+
'cross_sell_behavior',
|
|
333
337
|
'nft_mint_settings',
|
|
334
338
|
'subscription_data',
|
|
335
339
|
]),
|
|
@@ -347,6 +351,19 @@ router.post('/:id/checkout/:priceId', async (req, res) => {
|
|
|
347
351
|
if (req.query.redirect) {
|
|
348
352
|
raw.success_url = req.query.redirect as string;
|
|
349
353
|
raw.cancel_url = req.query.redirect as string;
|
|
354
|
+
logger.info('use redirect from query when checkout from pricing table', { v: raw.success_url });
|
|
355
|
+
}
|
|
356
|
+
if (req.query.cross_sell_behavior) {
|
|
357
|
+
raw.cross_sell_behavior = req.query.cross_sell_behavior as string;
|
|
358
|
+
logger.info('use cross_sell_behavior from query when checkout from pricing table', { v: raw.cross_sell_behavior });
|
|
359
|
+
}
|
|
360
|
+
if (req.query.nft_mint_factory) {
|
|
361
|
+
raw.nft_mint_settings = {
|
|
362
|
+
enabled: true,
|
|
363
|
+
behavior: 'per_checkout_session',
|
|
364
|
+
factory: req.query.nft_mint_factory as string,
|
|
365
|
+
};
|
|
366
|
+
logger.info('use nft_mint_settings from query when checkout from pricing table', { v: raw.nft_mint_settings });
|
|
350
367
|
}
|
|
351
368
|
|
|
352
369
|
const session = await CheckoutSession.create(raw as any);
|
|
@@ -140,46 +140,9 @@ router.get('/', auth, async (req, res) => {
|
|
|
140
140
|
res.json({ count, list });
|
|
141
141
|
});
|
|
142
142
|
|
|
143
|
-
export async function getExpandedProduct(id: string) {
|
|
144
|
-
const product = await Product.findOne({
|
|
145
|
-
where: { id },
|
|
146
|
-
include: [
|
|
147
|
-
{ model: Price, as: 'prices', include: [{ model: PaymentCurrency, as: 'currency' }] },
|
|
148
|
-
{ model: Price, as: 'default_price' },
|
|
149
|
-
],
|
|
150
|
-
});
|
|
151
|
-
|
|
152
|
-
if (product) {
|
|
153
|
-
const doc = product.toJSON();
|
|
154
|
-
const currencies = await PaymentCurrency.findAll();
|
|
155
|
-
// @ts-ignore
|
|
156
|
-
for (const price of doc.prices) {
|
|
157
|
-
if (Array.isArray(price.currency_options)) {
|
|
158
|
-
price.currency_options.forEach((x: any) => {
|
|
159
|
-
x.currency = currencies.find((c) => c.id === x.currency_id);
|
|
160
|
-
});
|
|
161
|
-
const base = price.currency_options.find((x: any) => x.currency_id === price.currency_id);
|
|
162
|
-
if (!base) {
|
|
163
|
-
price.currency_options.unshift(
|
|
164
|
-
pick(price, ['currency_id', 'unit_amount', 'tiers', 'custom_unit_amount', 'currency'])
|
|
165
|
-
);
|
|
166
|
-
}
|
|
167
|
-
} else {
|
|
168
|
-
price.currency_options = [
|
|
169
|
-
pick(price, ['currency_id', 'unit_amount', 'tiers', 'custom_unit_amount', 'currency']),
|
|
170
|
-
];
|
|
171
|
-
}
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
return doc;
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
return null;
|
|
178
|
-
}
|
|
179
|
-
|
|
180
143
|
// get product detail
|
|
181
144
|
router.get('/:id', auth, async (req, res) => {
|
|
182
|
-
res.json(await
|
|
145
|
+
res.json(await Product.expand(req.params.id as string));
|
|
183
146
|
});
|
|
184
147
|
|
|
185
148
|
// update product
|
|
@@ -205,13 +168,14 @@ router.put('/:id', auth, async (req, res) => {
|
|
|
205
168
|
'features',
|
|
206
169
|
'nft_factory',
|
|
207
170
|
'metadata',
|
|
171
|
+
'cross_sell',
|
|
208
172
|
]);
|
|
209
173
|
if (updates.metadata) {
|
|
210
174
|
updates.metadata = formatMetadata(updates.metadata);
|
|
211
175
|
}
|
|
212
176
|
await product.update(updates);
|
|
213
177
|
|
|
214
|
-
return res.json(await
|
|
178
|
+
return res.json(await Product.expand(req.params.id as string));
|
|
215
179
|
});
|
|
216
180
|
|
|
217
181
|
// archive
|
|
@@ -228,7 +192,7 @@ router.put('/:id/archive', auth, async (req, res) => {
|
|
|
228
192
|
await product.update({ active: !product.active });
|
|
229
193
|
|
|
230
194
|
// FIXME: deactivate payment-links, pricing-tables
|
|
231
|
-
return res.json(await
|
|
195
|
+
return res.json(await Product.expand(req.params.id as string));
|
|
232
196
|
});
|
|
233
197
|
|
|
234
198
|
// delete product
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import qs from 'querystring';
|
|
2
|
+
|
|
1
3
|
import { getUrl } from '@blocklet/sdk/lib/component';
|
|
2
4
|
import { Router } from 'express';
|
|
3
5
|
|
|
@@ -6,10 +8,10 @@ const router = Router();
|
|
|
6
8
|
router.get('/checkout/:entryId', (req, res) => {
|
|
7
9
|
const { entryId } = req.params;
|
|
8
10
|
if (entryId.startsWith('plink_')) {
|
|
9
|
-
return res.redirect(getUrl(`/checkout/pay/${entryId}
|
|
11
|
+
return res.redirect(getUrl(`/checkout/pay/${entryId}?${qs.stringify(req.query as any)}`));
|
|
10
12
|
}
|
|
11
13
|
if (entryId.startsWith('prctbl_')) {
|
|
12
|
-
return res.redirect(getUrl(`/checkout/pricing-table/${entryId}
|
|
14
|
+
return res.redirect(getUrl(`/checkout/pricing-table/${entryId}?${qs.stringify(req.query as any)}`));
|
|
13
15
|
}
|
|
14
16
|
|
|
15
17
|
return res.redirect(getUrl('/'));
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { DataTypes } from 'sequelize';
|
|
2
|
+
|
|
3
|
+
import type { Migration } from '../migrate';
|
|
4
|
+
|
|
5
|
+
export const up: Migration = async ({ context }) => {
|
|
6
|
+
await context.addColumn('products', 'cross_sell', { type: DataTypes.JSON, allowNull: true });
|
|
7
|
+
await context.addColumn('checkout_sessions', 'cross_sell_behavior', {
|
|
8
|
+
type: DataTypes.ENUM('auto', 'required'),
|
|
9
|
+
defaultValue: 'auto',
|
|
10
|
+
});
|
|
11
|
+
await context.addColumn('payment_links', 'cross_sell_behavior', {
|
|
12
|
+
type: DataTypes.ENUM('auto', 'required'),
|
|
13
|
+
defaultValue: 'auto',
|
|
14
|
+
});
|
|
15
|
+
await context.bulkUpdate('checkout_sessions', { cross_sell_behavior: 'auto' }, {});
|
|
16
|
+
await context.bulkUpdate('payment_links', { cross_sell_behavior: 'auto' }, {});
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export const down: Migration = async ({ context }) => {
|
|
20
|
+
await context.removeColumn('products', 'cross_sell');
|
|
21
|
+
await context.removeColumn('checkout_sessions', 'cross_sell_behavior');
|
|
22
|
+
await context.removeColumn('payment_links', 'cross_sell_behavior');
|
|
23
|
+
};
|
|
@@ -184,6 +184,8 @@ export class CheckoutSession extends Model<InferAttributes<CheckoutSession>, Inf
|
|
|
184
184
|
declare nft_mint_settings?: NftMintSettings;
|
|
185
185
|
declare nft_mint_details?: NftMintDetails;
|
|
186
186
|
|
|
187
|
+
declare cross_sell_behavior?: LiteralUnion<'auto' | 'required', string>;
|
|
188
|
+
|
|
187
189
|
// FIXME: Only exist on creation
|
|
188
190
|
// declare discounts?: {
|
|
189
191
|
// coupon?: string;
|
|
@@ -266,11 +268,6 @@ export class CheckoutSession extends Model<InferAttributes<CheckoutSession>, Inf
|
|
|
266
268
|
type: DataTypes.ENUM('paid', 'unpaid', 'no_payment_required'),
|
|
267
269
|
allowNull: false,
|
|
268
270
|
},
|
|
269
|
-
nft_mint_status: {
|
|
270
|
-
type: DataTypes.ENUM('disabled', 'pending', 'minted', 'sent', 'error'),
|
|
271
|
-
defaultValue: 'disabled',
|
|
272
|
-
allowNull: false,
|
|
273
|
-
},
|
|
274
271
|
line_items: {
|
|
275
272
|
type: DataTypes.JSON,
|
|
276
273
|
allowNull: false,
|
|
@@ -399,6 +396,11 @@ export class CheckoutSession extends Model<InferAttributes<CheckoutSession>, Inf
|
|
|
399
396
|
type: DataTypes.JSON,
|
|
400
397
|
allowNull: true,
|
|
401
398
|
},
|
|
399
|
+
nft_mint_status: {
|
|
400
|
+
type: DataTypes.ENUM('disabled', 'pending', 'minted', 'sent', 'error'),
|
|
401
|
+
defaultValue: 'disabled',
|
|
402
|
+
allowNull: false,
|
|
403
|
+
},
|
|
402
404
|
nft_mint_settings: {
|
|
403
405
|
type: DataTypes.JSON,
|
|
404
406
|
allowNull: true,
|
|
@@ -407,6 +409,10 @@ export class CheckoutSession extends Model<InferAttributes<CheckoutSession>, Inf
|
|
|
407
409
|
type: DataTypes.JSON,
|
|
408
410
|
allowNull: true,
|
|
409
411
|
},
|
|
412
|
+
cross_sell_behavior: {
|
|
413
|
+
type: DataTypes.ENUM('auto', 'required'),
|
|
414
|
+
defaultValue: 'auto',
|
|
415
|
+
},
|
|
410
416
|
},
|
|
411
417
|
{
|
|
412
418
|
sequelize,
|
|
@@ -97,7 +97,15 @@ export type TPriceExpanded = TPrice & {
|
|
|
97
97
|
|
|
98
98
|
export type TLineItemExpanded = LineItem & { price: TPriceExpanded; upsell_price: TPriceExpanded };
|
|
99
99
|
|
|
100
|
-
export type TProductExpanded = TProduct & {
|
|
100
|
+
export type TProductExpanded = TProduct & {
|
|
101
|
+
object: 'price';
|
|
102
|
+
prices: TPriceExpanded[];
|
|
103
|
+
default_price: TPriceExpanded;
|
|
104
|
+
cross_sell?: {
|
|
105
|
+
cross_sells_to: TProductExpanded;
|
|
106
|
+
cross_sells_to_id: string;
|
|
107
|
+
};
|
|
108
|
+
};
|
|
101
109
|
|
|
102
110
|
export type TPaymentLinkExpanded = TPaymentLink & { object: 'payment_link'; line_items: TLineItemExpanded[] };
|
|
103
111
|
|
|
@@ -73,6 +73,8 @@ export class PaymentLink extends Model<InferAttributes<PaymentLink>, InferCreati
|
|
|
73
73
|
|
|
74
74
|
declare nft_mint_settings?: NftMintSettings;
|
|
75
75
|
|
|
76
|
+
declare cross_sell_behavior?: LiteralUnion<'auto' | 'required', string>;
|
|
77
|
+
|
|
76
78
|
declare metadata?: Record<string, any>;
|
|
77
79
|
|
|
78
80
|
// TODO: following fields not supported
|
|
@@ -199,6 +201,10 @@ export class PaymentLink extends Model<InferAttributes<PaymentLink>, InferCreati
|
|
|
199
201
|
type: DataTypes.JSON,
|
|
200
202
|
allowNull: true,
|
|
201
203
|
},
|
|
204
|
+
cross_sell_behavior: {
|
|
205
|
+
type: DataTypes.ENUM('auto', 'required'),
|
|
206
|
+
defaultValue: 'auto',
|
|
207
|
+
},
|
|
202
208
|
},
|
|
203
209
|
{
|
|
204
210
|
sequelize,
|
|
@@ -160,10 +160,6 @@ export class Price extends Model<InferAttributes<Price>, InferCreationAttributes
|
|
|
160
160
|
type: DataTypes.JSON,
|
|
161
161
|
defaultValue: [],
|
|
162
162
|
},
|
|
163
|
-
upsell: {
|
|
164
|
-
type: DataTypes.JSON,
|
|
165
|
-
allowNull: true,
|
|
166
|
-
},
|
|
167
163
|
created_at: {
|
|
168
164
|
type: DataTypes.DATE,
|
|
169
165
|
defaultValue: DataTypes.NOW,
|
|
@@ -181,21 +177,30 @@ export class Price extends Model<InferAttributes<Price>, InferCreationAttributes
|
|
|
181
177
|
|
|
182
178
|
// eslint-disable-next-line @typescript-eslint/no-shadow
|
|
183
179
|
public static initialize(sequelize: any) {
|
|
184
|
-
this.init(
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
afterCreate: (model: Price, options) =>
|
|
192
|
-
createEvent('Price', 'price.created', model, options).catch(console.error),
|
|
193
|
-
afterUpdate: (model: Price, options) =>
|
|
194
|
-
createEvent('Price', 'price.updated', model, options).catch(console.error),
|
|
195
|
-
afterDestroy: (model: Price, options) =>
|
|
196
|
-
createEvent('Price', 'price.deleted', model, options).catch(console.error),
|
|
180
|
+
this.init(
|
|
181
|
+
{
|
|
182
|
+
...Price.GENESIS_ATTRIBUTES,
|
|
183
|
+
upsell: {
|
|
184
|
+
type: DataTypes.JSON,
|
|
185
|
+
allowNull: true,
|
|
186
|
+
},
|
|
197
187
|
},
|
|
198
|
-
|
|
188
|
+
{
|
|
189
|
+
sequelize,
|
|
190
|
+
modelName: 'Price',
|
|
191
|
+
tableName: 'prices',
|
|
192
|
+
createdAt: 'created_at',
|
|
193
|
+
updatedAt: 'updated_at',
|
|
194
|
+
hooks: {
|
|
195
|
+
afterCreate: (model: Price, options) =>
|
|
196
|
+
createEvent('Price', 'price.created', model, options).catch(console.error),
|
|
197
|
+
afterUpdate: (model: Price, options) =>
|
|
198
|
+
createEvent('Price', 'price.updated', model, options).catch(console.error),
|
|
199
|
+
afterDestroy: (model: Price, options) =>
|
|
200
|
+
createEvent('Price', 'price.deleted', model, options).catch(console.error),
|
|
201
|
+
},
|
|
202
|
+
}
|
|
203
|
+
);
|
|
199
204
|
}
|
|
200
205
|
|
|
201
206
|
public static associate(models: any) {
|