payment-kit 1.13.33 → 1.13.35
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/libs/session.ts +1 -1
- package/api/src/routes/prices.ts +123 -48
- package/api/src/routes/products.ts +21 -15
- package/api/src/store/models/price.ts +1 -1
- package/blocklet.yml +1 -1
- package/package.json +3 -3
- package/src/components/checkout/product-item.tsx +5 -5
- package/src/components/metadata/editor.tsx +0 -2
- package/src/components/price/upsell-select.tsx +3 -1
- package/src/components/price/upsell.tsx +1 -1
- package/src/libs/util.ts +36 -2
- package/src/locales/zh.tsx +3 -3
- package/src/pages/admin/products/prices/actions.tsx +1 -1
package/api/src/libs/session.ts
CHANGED
|
@@ -80,7 +80,7 @@ export function getCheckoutAmount(items: TLineItemExpanded[], currency: TPayment
|
|
|
80
80
|
|
|
81
81
|
export function getRecurringPeriod(recurring: PriceRecurring) {
|
|
82
82
|
const { interval } = recurring;
|
|
83
|
-
const count = recurring.interval_count || 1;
|
|
83
|
+
const count = +recurring.interval_count || 1;
|
|
84
84
|
const dayInMs = 24 * 60 * 60 * 1000;
|
|
85
85
|
|
|
86
86
|
switch (interval) {
|
package/api/src/routes/prices.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { fromTokenToUnit } from '@ocap/util';
|
|
2
2
|
import { Router } from 'express';
|
|
3
|
+
import Joi from 'joi';
|
|
3
4
|
import pick from 'lodash/pick';
|
|
5
|
+
import type { WhereOptions } from 'sequelize';
|
|
4
6
|
|
|
5
7
|
import { authenticate } from '../libs/security';
|
|
6
8
|
import { canUpsell } from '../libs/session';
|
|
@@ -12,7 +14,33 @@ const router = Router();
|
|
|
12
14
|
|
|
13
15
|
const auth = authenticate<Price>({ component: true, roles: ['owner', 'admin'] });
|
|
14
16
|
|
|
15
|
-
|
|
17
|
+
export async function getExpandedPrice(id: string) {
|
|
18
|
+
const price = await Price.findByPkOrLookupKey(id, {
|
|
19
|
+
include: [
|
|
20
|
+
{ model: Product, as: 'product' },
|
|
21
|
+
{ model: PaymentCurrency, as: 'currency' },
|
|
22
|
+
],
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
if (price) {
|
|
26
|
+
const currencies = await PaymentCurrency.findAll();
|
|
27
|
+
const doc = Price.formatAfterRead(price.toJSON(), currencies);
|
|
28
|
+
|
|
29
|
+
if (doc.upsell?.upsells_to_id) {
|
|
30
|
+
const to = await Price.findByPk(doc.upsell.upsells_to_id);
|
|
31
|
+
if (to) {
|
|
32
|
+
// @ts-ignore
|
|
33
|
+
doc.upsell.upsells_to = Price.formatAfterRead(to.toJSON(), currencies);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return doc;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// FIXME: @wangshijun use schema validation
|
|
16
44
|
// create price
|
|
17
45
|
// eslint-disable-next-line consistent-return
|
|
18
46
|
router.post('/', auth, async (req, res) => {
|
|
@@ -23,14 +51,17 @@ router.post('/', auth, async (req, res) => {
|
|
|
23
51
|
raw.currency_id = raw.currency_id || req.currency.id;
|
|
24
52
|
raw.created_via = req.user?.via as string;
|
|
25
53
|
|
|
54
|
+
if (!raw.product_id) {
|
|
55
|
+
return res.status(400).json({ error: 'product_id is required to create a price' });
|
|
56
|
+
}
|
|
57
|
+
|
|
26
58
|
if (!raw.unit_amount) {
|
|
27
59
|
return res.status(400).json({ error: 'price unit_amount is required' });
|
|
28
60
|
}
|
|
29
61
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
];
|
|
62
|
+
const product = await Product.findByPk(raw.product_id);
|
|
63
|
+
if (!product) {
|
|
64
|
+
return res.status(400).json({ error: `product ${raw.product_id} not found for price` });
|
|
34
65
|
}
|
|
35
66
|
|
|
36
67
|
const currencies = await PaymentCurrency.findAll({ where: { active: true } });
|
|
@@ -38,39 +69,29 @@ router.post('/', auth, async (req, res) => {
|
|
|
38
69
|
if (!currency) {
|
|
39
70
|
return res.status(400).json({ error: `currency used in price or not active: ${raw.currency_id}` });
|
|
40
71
|
}
|
|
41
|
-
|
|
72
|
+
|
|
73
|
+
if (Array.isArray(raw.currency_options) === false) {
|
|
74
|
+
raw.currency_options = [];
|
|
75
|
+
}
|
|
76
|
+
if (raw.currency_options.some((x) => x.currency_id === raw.currency_id) === false) {
|
|
77
|
+
raw.currency_options.unshift({
|
|
78
|
+
currency_id: raw.currency_id,
|
|
79
|
+
unit_amount: raw.unit_amount,
|
|
80
|
+
tiers: null,
|
|
81
|
+
custom_unit_amount: null,
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
|
|
42
85
|
raw.currency_options = Price.formatCurrencies(raw.currency_options, currencies);
|
|
86
|
+
raw.unit_amount = fromTokenToUnit(raw.unit_amount, currency.decimal).toString();
|
|
43
87
|
|
|
44
88
|
const price = await Price.insert(raw);
|
|
45
|
-
|
|
46
|
-
res.json(price);
|
|
89
|
+
res.json(await getExpandedPrice(price.id as string));
|
|
47
90
|
});
|
|
48
91
|
|
|
49
92
|
// get price detail
|
|
50
93
|
router.get('/:id', auth, async (req, res) => {
|
|
51
|
-
|
|
52
|
-
include: [
|
|
53
|
-
{ model: Product, as: 'product' },
|
|
54
|
-
{ model: PaymentCurrency, as: 'currency' },
|
|
55
|
-
],
|
|
56
|
-
});
|
|
57
|
-
|
|
58
|
-
if (price) {
|
|
59
|
-
const currencies = await PaymentCurrency.findAll();
|
|
60
|
-
const doc = Price.formatAfterRead(price.toJSON(), currencies);
|
|
61
|
-
|
|
62
|
-
if (doc.upsell?.upsells_to_id) {
|
|
63
|
-
const to = await Price.findByPk(doc.upsell.upsells_to_id);
|
|
64
|
-
if (to) {
|
|
65
|
-
// @ts-ignore
|
|
66
|
-
doc.upsell.upsells_to = Price.formatAfterRead(to.toJSON(), currencies);
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
res.json(doc);
|
|
71
|
-
} else {
|
|
72
|
-
res.json(null);
|
|
73
|
-
}
|
|
94
|
+
res.json(await getExpandedPrice(req.params.id as string));
|
|
74
95
|
});
|
|
75
96
|
|
|
76
97
|
router.get('/:id/upsell', auth, async (req, res) => {
|
|
@@ -107,7 +128,7 @@ router.put('/:id', auth, async (req, res) => {
|
|
|
107
128
|
pick(
|
|
108
129
|
req.body,
|
|
109
130
|
price.locked
|
|
110
|
-
? ['nickname', 'description', 'metadata', 'upsell']
|
|
131
|
+
? ['nickname', 'description', 'metadata', 'currency_options', 'upsell']
|
|
111
132
|
: ['type', 'model', 'active', 'livemode', 'nickname', 'recurring', 'description', 'tiers', 'unit_amount', 'transform_quantity', 'metadata', 'lookup_key', 'currency_options', 'upsell'] // prettier-ignore
|
|
112
133
|
)
|
|
113
134
|
);
|
|
@@ -120,17 +141,16 @@ router.put('/:id', auth, async (req, res) => {
|
|
|
120
141
|
}
|
|
121
142
|
|
|
122
143
|
const currencies = await PaymentCurrency.findAll({ where: { active: true } });
|
|
144
|
+
const currency = currencies.find((x) => x.id === price.currency_id);
|
|
145
|
+
if (!currency) {
|
|
146
|
+
return res.status(400).json({ error: `currency used in price not found or not active: ${price.currency_id}` });
|
|
147
|
+
}
|
|
123
148
|
if (updates.unit_amount) {
|
|
124
|
-
const currency = currencies.find((x) => x.id === price.currency_id);
|
|
125
|
-
if (!currency) {
|
|
126
|
-
return res.status(400).json({ error: `currency used in price not found or not active: ${price.currency_id}` });
|
|
127
|
-
}
|
|
128
149
|
updates.unit_amount = fromTokenToUnit(updates.unit_amount, currency.decimal).toString();
|
|
129
150
|
}
|
|
130
151
|
if (updates.currency_options) {
|
|
131
152
|
updates.currency_options = Price.formatCurrencies(updates.currency_options, currencies);
|
|
132
|
-
|
|
133
|
-
if (!base) {
|
|
153
|
+
if (updates.currency_options.some((x) => x.currency_id === price.currency_id) === false) {
|
|
134
154
|
updates.currency_options.unshift({
|
|
135
155
|
currency_id: price.currency_id,
|
|
136
156
|
unit_amount: price.unit_amount,
|
|
@@ -139,21 +159,16 @@ router.put('/:id', auth, async (req, res) => {
|
|
|
139
159
|
});
|
|
140
160
|
}
|
|
141
161
|
if (updates.unit_amount) {
|
|
142
|
-
const
|
|
143
|
-
if (
|
|
144
|
-
|
|
162
|
+
const base = price.currency_options.find((x) => x.currency_id === price.currency_id);
|
|
163
|
+
if (base) {
|
|
164
|
+
base.unit_amount = updates.unit_amount;
|
|
145
165
|
}
|
|
146
166
|
}
|
|
147
|
-
} else if (updates.unit_amount) {
|
|
148
|
-
const exist = price.currency_options.find((x) => x.currency_id === price.currency_id);
|
|
149
|
-
if (exist) {
|
|
150
|
-
exist.unit_amount = updates.unit_amount;
|
|
151
|
-
}
|
|
152
167
|
}
|
|
153
168
|
|
|
154
169
|
await price.update(Price.formatBeforeSave(updates));
|
|
155
170
|
|
|
156
|
-
return res.json(
|
|
171
|
+
return res.json(await getExpandedPrice(req.params.id as string));
|
|
157
172
|
});
|
|
158
173
|
|
|
159
174
|
// archive
|
|
@@ -173,7 +188,8 @@ router.put('/:id/archive', auth, async (req, res) => {
|
|
|
173
188
|
}
|
|
174
189
|
|
|
175
190
|
await price.update({ active: false });
|
|
176
|
-
|
|
191
|
+
|
|
192
|
+
return res.json(await getExpandedPrice(req.params.id as string));
|
|
177
193
|
});
|
|
178
194
|
|
|
179
195
|
// delete price
|
|
@@ -199,4 +215,63 @@ router.delete('/:id', auth, async (req, res) => {
|
|
|
199
215
|
return res.json(price);
|
|
200
216
|
});
|
|
201
217
|
|
|
218
|
+
// list products and prices
|
|
219
|
+
const paginationSchema = Joi.object<{
|
|
220
|
+
page: number;
|
|
221
|
+
pageSize: number;
|
|
222
|
+
livemode?: boolean;
|
|
223
|
+
active?: boolean;
|
|
224
|
+
type?: string;
|
|
225
|
+
currency_id?: string;
|
|
226
|
+
product_id?: string;
|
|
227
|
+
lookup_key?: string;
|
|
228
|
+
}>({
|
|
229
|
+
page: Joi.number().integer().min(1).default(1),
|
|
230
|
+
pageSize: Joi.number().integer().min(1).max(100).default(20),
|
|
231
|
+
livemode: Joi.boolean().empty(''),
|
|
232
|
+
active: Joi.boolean().empty(''),
|
|
233
|
+
type: Joi.string().empty(''),
|
|
234
|
+
currency_id: Joi.string().empty(''),
|
|
235
|
+
product_id: Joi.string().empty(''),
|
|
236
|
+
lookup_key: Joi.string().empty(''),
|
|
237
|
+
});
|
|
238
|
+
router.get('/', auth, async (req, res) => {
|
|
239
|
+
const { page, pageSize, active, livemode, ...query } = await paginationSchema.validateAsync(req.query, {
|
|
240
|
+
stripUnknown: false,
|
|
241
|
+
allowUnknown: true,
|
|
242
|
+
});
|
|
243
|
+
const where: WhereOptions<Price> = {};
|
|
244
|
+
|
|
245
|
+
if (typeof active === 'boolean') {
|
|
246
|
+
where.active = active;
|
|
247
|
+
}
|
|
248
|
+
if (typeof livemode === 'boolean') {
|
|
249
|
+
where.livemode = livemode;
|
|
250
|
+
}
|
|
251
|
+
['type', 'currency_id', 'product_id', 'lookup_key'].forEach((key: string) => {
|
|
252
|
+
// @ts-ignore
|
|
253
|
+
if (query[key]) {
|
|
254
|
+
// @ts-ignore
|
|
255
|
+
where[key] = query[key].split(',').map((x: string) => x.trim()).filter(Boolean); // prettier-ignore
|
|
256
|
+
}
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
Object.keys(query)
|
|
260
|
+
.filter((x) => x.startsWith('recurring.'))
|
|
261
|
+
.forEach((key: string) => {
|
|
262
|
+
// @ts-ignore
|
|
263
|
+
where[key] = query[key];
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
const { rows, count } = await Price.findAndCountAll({
|
|
267
|
+
where,
|
|
268
|
+
attributes: ['id'],
|
|
269
|
+
order: [['created_at', 'DESC']],
|
|
270
|
+
offset: (page - 1) * pageSize,
|
|
271
|
+
limit: pageSize,
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
res.json({ count, list: await Promise.all(rows.map((x) => getExpandedPrice(x.id))) });
|
|
275
|
+
});
|
|
276
|
+
|
|
202
277
|
export default router;
|
|
@@ -63,12 +63,14 @@ router.post('/', auth, async (req, res) => {
|
|
|
63
63
|
} else {
|
|
64
64
|
price.currency_options = [];
|
|
65
65
|
}
|
|
66
|
-
price.currency_options.
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
66
|
+
if (price.currency_options.some((x) => x.currency_id === price.currency_id) === false) {
|
|
67
|
+
price.currency_options.unshift({
|
|
68
|
+
currency_id: price.currency_id,
|
|
69
|
+
unit_amount: price.unit_amount,
|
|
70
|
+
tiers: null,
|
|
71
|
+
custom_unit_amount: null,
|
|
72
|
+
});
|
|
73
|
+
}
|
|
72
74
|
|
|
73
75
|
return price;
|
|
74
76
|
});
|
|
@@ -138,10 +140,9 @@ router.get('/', auth, async (req, res) => {
|
|
|
138
140
|
res.json({ count, list });
|
|
139
141
|
});
|
|
140
142
|
|
|
141
|
-
|
|
142
|
-
router.get('/:id', auth, async (req, res) => {
|
|
143
|
+
export async function getExpandedProduct(id: string) {
|
|
143
144
|
const product = await Product.findOne({
|
|
144
|
-
where: { id
|
|
145
|
+
where: { id },
|
|
145
146
|
include: [
|
|
146
147
|
{ model: Price, as: 'prices', include: [{ model: PaymentCurrency, as: 'currency' }] },
|
|
147
148
|
{ model: Price, as: 'default_price' },
|
|
@@ -169,10 +170,16 @@ router.get('/:id', auth, async (req, res) => {
|
|
|
169
170
|
];
|
|
170
171
|
}
|
|
171
172
|
}
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
res.json(null);
|
|
173
|
+
|
|
174
|
+
return doc;
|
|
175
175
|
}
|
|
176
|
+
|
|
177
|
+
return null;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// get product detail
|
|
181
|
+
router.get('/:id', auth, async (req, res) => {
|
|
182
|
+
res.json(await getExpandedProduct(req.params.id as string));
|
|
176
183
|
});
|
|
177
184
|
|
|
178
185
|
// update product
|
|
@@ -204,7 +211,7 @@ router.put('/:id', auth, async (req, res) => {
|
|
|
204
211
|
}
|
|
205
212
|
await product.update(updates);
|
|
206
213
|
|
|
207
|
-
return res.json(
|
|
214
|
+
return res.json(await getExpandedProduct(req.params.id as string));
|
|
208
215
|
});
|
|
209
216
|
|
|
210
217
|
// archive
|
|
@@ -221,8 +228,7 @@ router.put('/:id/archive', auth, async (req, res) => {
|
|
|
221
228
|
await product.update({ active: !product.active });
|
|
222
229
|
|
|
223
230
|
// FIXME: deactivate payment-links, pricing-tables
|
|
224
|
-
|
|
225
|
-
return res.json(product);
|
|
231
|
+
return res.json(await getExpandedProduct(req.params.id as string));
|
|
226
232
|
});
|
|
227
233
|
|
|
228
234
|
// delete product
|
|
@@ -355,7 +355,7 @@ export class Price extends Model<InferAttributes<Price>, InferCreationAttributes
|
|
|
355
355
|
}
|
|
356
356
|
|
|
357
357
|
// @ts-ignore
|
|
358
|
-
return this.create(this.formatBeforeSave(price));
|
|
358
|
+
return this.create(this.formatBeforeSave({ model: 'standard', ...price }));
|
|
359
359
|
}
|
|
360
360
|
|
|
361
361
|
public static findByPkOrLookupKey(id: string, options: FindOptions<Price> = {}) {
|
package/blocklet.yml
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "payment-kit",
|
|
3
|
-
"version": "1.13.
|
|
3
|
+
"version": "1.13.35",
|
|
4
4
|
"scripts": {
|
|
5
5
|
"dev": "blocklet dev",
|
|
6
6
|
"eject": "vite eject",
|
|
@@ -103,7 +103,7 @@
|
|
|
103
103
|
"@abtnode/types": "1.16.17-beta-952ef53d",
|
|
104
104
|
"@arcblock/eslint-config": "^0.2.4",
|
|
105
105
|
"@arcblock/eslint-config-ts": "^0.2.4",
|
|
106
|
-
"@did-pay/types": "1.13.
|
|
106
|
+
"@did-pay/types": "1.13.35",
|
|
107
107
|
"@types/cookie-parser": "^1.4.4",
|
|
108
108
|
"@types/cors": "^2.8.14",
|
|
109
109
|
"@types/dotenv-flow": "^3.3.1",
|
|
@@ -140,5 +140,5 @@
|
|
|
140
140
|
"parser": "typescript"
|
|
141
141
|
}
|
|
142
142
|
},
|
|
143
|
-
"gitHead": "
|
|
143
|
+
"gitHead": "25a3e75f4894d8aefb016605548c58645c0ca194"
|
|
144
144
|
}
|
|
@@ -61,12 +61,12 @@ export default function ProductItem({ item, session, currency, onUpsell, onDowns
|
|
|
61
61
|
onChange={() => onUpsell(item.price_id, item.price.upsell?.upsells_to_id)}
|
|
62
62
|
/>
|
|
63
63
|
{t('checkout.upsell.save', {
|
|
64
|
-
recurring: formatRecurring(item.price.upsell.upsells_to.recurring as PriceRecurring),
|
|
64
|
+
recurring: t(`common.${formatRecurring(item.price.upsell.upsells_to.recurring as PriceRecurring)}`),
|
|
65
65
|
})}
|
|
66
|
-
<Status label={t('checkout.upsell.off', { saving })} color="primary" variant="outlined" sx={{ ml:
|
|
66
|
+
<Status label={t('checkout.upsell.off', { saving })} color="primary" variant="outlined" sx={{ ml: 1 }} />
|
|
67
67
|
</Typography>
|
|
68
68
|
<Typography component="span" sx={{ fontSize: 12 }}>
|
|
69
|
-
{formatPrice(item.price.upsell.upsells_to, currency)}
|
|
69
|
+
{formatPrice(item.price.upsell.upsells_to, currency, item.price.product?.unit_label)}
|
|
70
70
|
</Typography>
|
|
71
71
|
</Stack>
|
|
72
72
|
)}
|
|
@@ -88,11 +88,11 @@ export default function ProductItem({ item, session, currency, onUpsell, onDowns
|
|
|
88
88
|
onChange={() => onDownsell(item.upsell_price_id)}
|
|
89
89
|
/>
|
|
90
90
|
{t('checkout.upsell.revert', {
|
|
91
|
-
recurring: formatRecurring(item.price.recurring as PriceRecurring),
|
|
91
|
+
recurring: t(`common.${formatRecurring(item.price.recurring as PriceRecurring)}`),
|
|
92
92
|
})}
|
|
93
93
|
</Typography>
|
|
94
94
|
<Typography component="span" sx={{ fontSize: 12 }}>
|
|
95
|
-
{formatPrice(item.price, currency)}
|
|
95
|
+
{formatPrice(item.price, currency, item.price.product?.unit_label)}
|
|
96
96
|
</Typography>
|
|
97
97
|
</Stack>
|
|
98
98
|
)}
|
|
@@ -20,8 +20,6 @@ export default function MetadataEditor({
|
|
|
20
20
|
const metadata = data.metadata || {};
|
|
21
21
|
const methods = useForm<any>({
|
|
22
22
|
defaultValues: {
|
|
23
|
-
...data,
|
|
24
|
-
// @ts-ignore
|
|
25
23
|
metadata: Object.keys(metadata).map((key: string) => ({ key, value: metadata[key] })),
|
|
26
24
|
},
|
|
27
25
|
});
|
|
@@ -12,13 +12,14 @@ import AddPrice from '../product/add-price';
|
|
|
12
12
|
type Props = {
|
|
13
13
|
price: TPriceExpanded;
|
|
14
14
|
onSelect: Function;
|
|
15
|
+
onAdd: Function;
|
|
15
16
|
};
|
|
16
17
|
|
|
17
18
|
const fetchData = (id: string): Promise<TPriceExpanded[]> => {
|
|
18
19
|
return api.get(`/api/prices/${id}/upsell`).then((res) => res.data);
|
|
19
20
|
};
|
|
20
21
|
|
|
21
|
-
export default function UpsellSelect({ price, onSelect }: Props) {
|
|
22
|
+
export default function UpsellSelect({ price, onSelect, onAdd }: Props) {
|
|
22
23
|
const { t } = useLocaleContext();
|
|
23
24
|
const [state, setState] = useSetState({ loading: false, adding: false, action: '' });
|
|
24
25
|
|
|
@@ -47,6 +48,7 @@ export default function UpsellSelect({ price, onSelect }: Props) {
|
|
|
47
48
|
setState({ adding: true });
|
|
48
49
|
await api.post('/api/prices', { ...formData, product_id: price.product_id });
|
|
49
50
|
Toast.success(t('common.saved'));
|
|
51
|
+
onAdd();
|
|
50
52
|
} catch (err) {
|
|
51
53
|
console.error(err);
|
|
52
54
|
Toast.error(formatError(err));
|
|
@@ -58,7 +58,7 @@ export function UpsellForm({ data, onChange }: { data: TPriceExpanded; onChange:
|
|
|
58
58
|
);
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
-
return <UpsellSelect price={data} onSelect={onSelectUpsell} />;
|
|
61
|
+
return <UpsellSelect price={data} onSelect={onSelectUpsell} onAdd={onChange} />;
|
|
62
62
|
}
|
|
63
63
|
|
|
64
64
|
export default function PriceUpsell({ data, onChange }: { data: TPriceExpanded; onChange: Function }) {
|
package/src/libs/util.ts
CHANGED
|
@@ -332,9 +332,36 @@ export function formatPaymentLinkPricing(link: TPaymentLinkExpanded, currency: T
|
|
|
332
332
|
currency
|
|
333
333
|
);
|
|
334
334
|
}
|
|
335
|
+
export function getRecurringPeriod(recurring: PriceRecurring) {
|
|
336
|
+
const { interval } = recurring;
|
|
337
|
+
const count = +recurring.interval_count || 1;
|
|
338
|
+
const dayInMs = 24 * 60 * 60 * 1000;
|
|
339
|
+
|
|
340
|
+
switch (interval) {
|
|
341
|
+
case 'hour':
|
|
342
|
+
return 60 * 60 * 1000;
|
|
343
|
+
case 'day':
|
|
344
|
+
return count * dayInMs;
|
|
345
|
+
case 'week':
|
|
346
|
+
return count * 7 * dayInMs;
|
|
347
|
+
case 'month':
|
|
348
|
+
return count * 30 * dayInMs;
|
|
349
|
+
case 'year':
|
|
350
|
+
return count * 365 * dayInMs;
|
|
351
|
+
default:
|
|
352
|
+
throw new Error(`Unsupported recurring interval: ${interval}`);
|
|
353
|
+
}
|
|
354
|
+
}
|
|
335
355
|
|
|
336
356
|
export function formatUpsellSaving(session: TCheckoutSessionExpanded, currency: TPaymentCurrency) {
|
|
337
357
|
const items = session.line_items as TLineItemExpanded[];
|
|
358
|
+
if (items[0]?.upsell_price_id) {
|
|
359
|
+
return '0';
|
|
360
|
+
}
|
|
361
|
+
if (!items[0]?.price.upsell?.upsells_to) {
|
|
362
|
+
return '0';
|
|
363
|
+
}
|
|
364
|
+
|
|
338
365
|
const from = getCheckoutAmount(items, currency, false, false);
|
|
339
366
|
const to = getCheckoutAmount(
|
|
340
367
|
items.map((x) => ({
|
|
@@ -347,11 +374,18 @@ export function formatUpsellSaving(session: TCheckoutSessionExpanded, currency:
|
|
|
347
374
|
true
|
|
348
375
|
);
|
|
349
376
|
|
|
350
|
-
const
|
|
377
|
+
const fromRecurring = items[0].price?.recurring as PriceRecurring;
|
|
378
|
+
const toRecurring = items[0].price?.upsell?.upsells_to?.recurring as PriceRecurring;
|
|
379
|
+
if (!fromRecurring || !toRecurring) {
|
|
380
|
+
return '0';
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
const factor = Math.floor(getRecurringPeriod(toRecurring) / getRecurringPeriod(fromRecurring));
|
|
384
|
+
|
|
351
385
|
const before = new BN(from.total).mul(new BN(factor));
|
|
352
386
|
const after = new BN(to.total);
|
|
353
387
|
|
|
354
|
-
return Number(before.sub(after).mul(new BN(100)).div(
|
|
388
|
+
return Number(before.sub(after).mul(new BN(100)).div(before).toString()).toFixed(0);
|
|
355
389
|
}
|
|
356
390
|
|
|
357
391
|
export function formatCheckoutHeadlines(
|
package/src/locales/zh.tsx
CHANGED
|
@@ -501,15 +501,15 @@ export default flat({
|
|
|
501
501
|
},
|
|
502
502
|
customer: {
|
|
503
503
|
name: '姓名',
|
|
504
|
-
email: '
|
|
504
|
+
email: '邮件',
|
|
505
505
|
phone: '电话',
|
|
506
506
|
phonePlaceholder: '电话号码',
|
|
507
507
|
phoneTip: '以防需要与您联系有关您的订单',
|
|
508
508
|
},
|
|
509
509
|
upsell: {
|
|
510
|
-
save: '使用{recurring}
|
|
510
|
+
save: '使用{recurring}计费方式',
|
|
511
511
|
revert: '切换到{recurring}计费方式',
|
|
512
|
-
off: '{saving}
|
|
512
|
+
off: '省 {saving}%',
|
|
513
513
|
},
|
|
514
514
|
expired: {
|
|
515
515
|
title: '链接已过期',
|
|
@@ -26,7 +26,7 @@ export default function PriceActions(props: Props) {
|
|
|
26
26
|
|
|
27
27
|
const canEdit = props.data.active;
|
|
28
28
|
const canArchive = props.data.active;
|
|
29
|
-
const canRemove = !props.data.locked && props.setAsDefault;
|
|
29
|
+
const canRemove = !props.data.locked && !props.setAsDefault;
|
|
30
30
|
|
|
31
31
|
const [state, setState] = useSetState({
|
|
32
32
|
action: '',
|