@escapenavigator/utils 2.0.35 → 2.0.39
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/dist/classify-api-error.d.ts +4 -1
- package/dist/classify-api-error.js +33 -16
- package/dist/classify-api-error.spec.js +31 -14
- package/dist/format-slot-discount/index.d.ts +6 -0
- package/dist/format-slot-discount/index.js +15 -0
- package/dist/get-order-cancelation-date/index.d.ts +2 -1
- package/dist/get-order-cancelation-date/index.js +6 -1
- package/dist/get-slot-discount-amount.d.ts +22 -0
- package/dist/get-slot-discount-amount.js +21 -0
- package/dist/get-slot-discount-amount.spec.d.ts +1 -0
- package/dist/get-slot-discount-amount.spec.js +39 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +3 -0
- package/dist/package-price/index.d.ts +73 -0
- package/dist/package-price/index.js +92 -0
- package/dist/package-price/index.spec.d.ts +1 -0
- package/dist/package-price/index.spec.js +100 -0
- package/dist/role-refund-permissions.d.ts +7 -0
- package/dist/role-refund-permissions.js +9 -0
- package/dist/role-refund-permissions.spec.d.ts +1 -0
- package/dist/role-refund-permissions.spec.js +16 -0
- package/dist/serialize-slot.js +8 -0
- package/dist/slot-representative-price.d.ts +10 -0
- package/dist/slot-representative-price.js +17 -10
- package/dist/tz-date.d.ts +1 -1
- package/dist/tz-date.js +7 -2
- package/package.json +3 -3
|
@@ -23,7 +23,10 @@
|
|
|
23
23
|
*
|
|
24
24
|
* skip → не репортим
|
|
25
25
|
* 304 (etag-success), отменённые axios-запросы (ERR_CANCELED),
|
|
26
|
-
* 401/403 на app/auth (ожидаемый session/role flow)
|
|
26
|
+
* 401/403 на app/auth (ожидаемый session/role flow), а также
|
|
27
|
+
* обрывы связи на стороне клиента: `Network Error`,
|
|
28
|
+
* «timeout of 0ms exceeded» и «Request aborted» (запрос умер
|
|
29
|
+
* до ответа сервера).
|
|
27
30
|
*
|
|
28
31
|
* Группировка (`fingerprint`) делается так, чтобы все события с одинаковым
|
|
29
32
|
* `apiKey + errorCode` сливались в один issue. Иначе Sentry плодит отдельный
|
|
@@ -24,7 +24,10 @@
|
|
|
24
24
|
*
|
|
25
25
|
* skip → не репортим
|
|
26
26
|
* 304 (etag-success), отменённые axios-запросы (ERR_CANCELED),
|
|
27
|
-
* 401/403 на app/auth (ожидаемый session/role flow)
|
|
27
|
+
* 401/403 на app/auth (ожидаемый session/role flow), а также
|
|
28
|
+
* обрывы связи на стороне клиента: `Network Error`,
|
|
29
|
+
* «timeout of 0ms exceeded» и «Request aborted» (запрос умер
|
|
30
|
+
* до ответа сервера).
|
|
28
31
|
*
|
|
29
32
|
* Группировка (`fingerprint`) делается так, чтобы все события с одинаковым
|
|
30
33
|
* `apiKey + errorCode` сливались в один issue. Иначе Sentry плодит отдельный
|
|
@@ -78,20 +81,33 @@ const SKIP_AUTH_STATUSES_ON_APPS = new Set(['app', 'auth']);
|
|
|
78
81
|
*/
|
|
79
82
|
const SKIP_NOT_FOUND_API_KEYS = new Set(['login', 'loginSwitch']);
|
|
80
83
|
const isNetworkErrorMessage = (message) => typeof message === 'string' && /network error/i.test(message);
|
|
84
|
+
/**
|
|
85
|
+
* Сообщения ECONNABORTED, означающие «запрос отменил браузер/ОС», а не
|
|
86
|
+
* «бэк не ответил за отведённый таймаут». Сконфигурированный таймаут виджета
|
|
87
|
+
* («timeout of 25000ms exceeded») этому не соответствует и остаётся critical.
|
|
88
|
+
*/
|
|
89
|
+
const isClientAbortMessage = (message) => typeof message === 'string' && /timeout of 0ms|request aborted/i.test(message);
|
|
81
90
|
function classifyApiError(ctx) {
|
|
82
91
|
const { status, code, apiKey, errorCode, originalErrorCode, message, app } = ctx;
|
|
83
92
|
if (code && SKIP_AXIOS_CODES.has(code)) {
|
|
84
93
|
return { severity: 'skip', level: 'info', fingerprint: null, shouldReport: false };
|
|
85
94
|
}
|
|
86
|
-
//
|
|
87
|
-
//
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
+
// ECONNABORTED, где запрос умер на клиенте, а не на бэке:
|
|
96
|
+
//
|
|
97
|
+
// «timeout of 0ms exceeded» — Safari/iOS обрывает XHR при смене сети или
|
|
98
|
+
// уходе вкладки в фон (timeout у запроса не выставлен, обрыв не наш).
|
|
99
|
+
// Раньше репортили как business/info «для картины мобильной связности»:
|
|
100
|
+
// за месяц это дало десяток issue на 50+ человек
|
|
101
|
+
// (ESCAPE-NAVIGATOR-APP-3J/3K/3M/3H/2P/3G/33), в которых ни одного бага.
|
|
102
|
+
//
|
|
103
|
+
// «Request aborted» — axios@0.21 отдаёт это на `xhr.onabort`, то есть
|
|
104
|
+
// когда браузер сам убил запрос: пользователь ушёл со страницы, закрыл
|
|
105
|
+
// таб или нажал «стоп» до ответа. О здоровье API это не говорит ничего,
|
|
106
|
+
// а в Sentry попадало как critical.
|
|
107
|
+
//
|
|
108
|
+
// Картину связности всё равно лучше видно по метрикам, а не по issue.
|
|
109
|
+
if (code === 'ECONNABORTED' && isClientAbortMessage(message)) {
|
|
110
|
+
return { severity: 'skip', level: 'info', fingerprint: null, shouldReport: false };
|
|
95
111
|
}
|
|
96
112
|
if (typeof status === 'number' && SKIP_HTTP_STATUSES.has(status)) {
|
|
97
113
|
return { severity: 'skip', level: 'info', fingerprint: null, shouldReport: false };
|
|
@@ -155,14 +171,15 @@ function classifyApiError(ctx) {
|
|
|
155
171
|
// code === 'ERR_NETWORK' (axios>=0.27). На проде это почти всегда
|
|
156
172
|
// клиентская связность (мобильный обрыв, смена соты, троттлинг/RKN).
|
|
157
173
|
// На виджете GET'ы уже ретраятся — сюда долетает только финальный фейл.
|
|
174
|
+
//
|
|
175
|
+
// Не репортим: за месяц это ~1000 событий в app/widget/auth, разбитых на
|
|
176
|
+
// десятки issue по apiKey, и ни одно не было багом. Настоящая
|
|
177
|
+
// недоступность бэка видна иначе и этим фильтром не задета: 5xx и
|
|
178
|
+
// сработавший таймаут (25s у виджета) выше классифицируются как critical,
|
|
179
|
+
// а падение домена/сертификата ловится мониторингом со стороны бэкенда.
|
|
158
180
|
const isNetworkError = !status && (code === 'ERR_NETWORK' || (!code && isNetworkErrorMessage(message)));
|
|
159
181
|
if (isNetworkError) {
|
|
160
|
-
return {
|
|
161
|
-
severity: 'business',
|
|
162
|
-
level: 'warning',
|
|
163
|
-
fingerprint: ['api', 'network', apiKey],
|
|
164
|
-
shouldReport: true,
|
|
165
|
-
};
|
|
182
|
+
return { severity: 'skip', level: 'info', fingerprint: null, shouldReport: false };
|
|
166
183
|
}
|
|
167
184
|
// Сюда падают: какие-то странные кейсы без status/кода/сетевого message.
|
|
168
185
|
// Считаем critical, но без агрессивного группирования — пусть Sentry
|
|
@@ -43,6 +43,37 @@ describe('classifyApiError', () => {
|
|
|
43
43
|
const r = (0, classify_api_error_1.classifyApiError)({ ...base, status: 404, app: 'auth' });
|
|
44
44
|
expect(r.shouldReport).toBe(true);
|
|
45
45
|
});
|
|
46
|
+
it('чистый network error → skip (обрыв связи у клиента)', () => {
|
|
47
|
+
expect((0, classify_api_error_1.classifyApiError)({ ...base, code: 'ERR_NETWORK' }).shouldReport).toBe(false);
|
|
48
|
+
expect((0, classify_api_error_1.classifyApiError)({ ...base, message: 'Network Error' }).shouldReport).toBe(false);
|
|
49
|
+
});
|
|
50
|
+
it('мобильный обрыв (timeout of 0ms) → skip', () => {
|
|
51
|
+
const r = (0, classify_api_error_1.classifyApiError)({
|
|
52
|
+
...base,
|
|
53
|
+
code: 'ECONNABORTED',
|
|
54
|
+
message: 'timeout of 0ms exceeded',
|
|
55
|
+
});
|
|
56
|
+
expect(r.severity).toBe('skip');
|
|
57
|
+
expect(r.shouldReport).toBe(false);
|
|
58
|
+
});
|
|
59
|
+
it('уход со страницы до ответа (Request aborted) → skip', () => {
|
|
60
|
+
const r = (0, classify_api_error_1.classifyApiError)({
|
|
61
|
+
...base,
|
|
62
|
+
code: 'ECONNABORTED',
|
|
63
|
+
message: 'Request aborted',
|
|
64
|
+
});
|
|
65
|
+
expect(r.severity).toBe('skip');
|
|
66
|
+
expect(r.shouldReport).toBe(false);
|
|
67
|
+
});
|
|
68
|
+
it('сработавший таймаут виджета (25s) → НЕ skip, это медленный бэк', () => {
|
|
69
|
+
const r = (0, classify_api_error_1.classifyApiError)({
|
|
70
|
+
...base,
|
|
71
|
+
code: 'ECONNABORTED',
|
|
72
|
+
message: 'timeout of 25000ms exceeded',
|
|
73
|
+
});
|
|
74
|
+
expect(r.severity).toBe('critical');
|
|
75
|
+
expect(r.shouldReport).toBe(true);
|
|
76
|
+
});
|
|
46
77
|
it('5xx на login → НЕ skip (это уже настоящий сбой входа)', () => {
|
|
47
78
|
const r = (0, classify_api_error_1.classifyApiError)({
|
|
48
79
|
apiKey: 'login',
|
|
@@ -99,20 +130,6 @@ describe('classifyApiError', () => {
|
|
|
99
130
|
expect(r.severity).toBe('business');
|
|
100
131
|
expect(r.fingerprint).toContain('bookingWindowClosed');
|
|
101
132
|
});
|
|
102
|
-
it('мобильный обрыв (timeout of 0ms) → business/info', () => {
|
|
103
|
-
const r = (0, classify_api_error_1.classifyApiError)({
|
|
104
|
-
...base,
|
|
105
|
-
code: 'ECONNABORTED',
|
|
106
|
-
message: 'timeout of 0ms exceeded',
|
|
107
|
-
});
|
|
108
|
-
expect(r.severity).toBe('business');
|
|
109
|
-
expect(r.level).toBe('info');
|
|
110
|
-
expect(r.fingerprint).toContain('mobile-abort');
|
|
111
|
-
});
|
|
112
|
-
it('чистый network error → business', () => {
|
|
113
|
-
expect((0, classify_api_error_1.classifyApiError)({ ...base, code: 'ERR_NETWORK' }).severity).toBe('business');
|
|
114
|
-
expect((0, classify_api_error_1.classifyApiError)({ ...base, message: 'Network Error' }).severity).toBe('business');
|
|
115
|
-
});
|
|
116
133
|
});
|
|
117
134
|
describe('debug (generic 4xx — шум, но репортим info-level)', () => {
|
|
118
135
|
it.each([
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { SlotDiscountTypeEnum } from '@escapenavigator/types/dist/slot/enum/slot-discount-type.enum';
|
|
2
|
+
/**
|
|
3
|
+
* Скидка слота для показа: «15%» для процентной и «500 руб.» для фиксированной.
|
|
4
|
+
* Без знака «минус» — вызывающий сам решает, как её подать.
|
|
5
|
+
*/
|
|
6
|
+
export declare const formatSlotDiscount: (discount: number, discountType: SlotDiscountTypeEnum | string | null | undefined, currency: string) => string;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.formatSlotDiscount = void 0;
|
|
4
|
+
const format_amount_1 = require("../format-amount");
|
|
5
|
+
const get_slot_discount_amount_1 = require("../get-slot-discount-amount");
|
|
6
|
+
/**
|
|
7
|
+
* Скидка слота для показа: «15%» для процентной и «500 руб.» для фиксированной.
|
|
8
|
+
* Без знака «минус» — вызывающий сам решает, как её подать.
|
|
9
|
+
*/
|
|
10
|
+
const formatSlotDiscount = (discount, discountType, currency) => {
|
|
11
|
+
if ((0, get_slot_discount_amount_1.isFixedSlotDiscount)(discountType))
|
|
12
|
+
return (0, format_amount_1.formatAmount)(discount, currency);
|
|
13
|
+
return `${discount / 100}%`;
|
|
14
|
+
};
|
|
15
|
+
exports.formatSlotDiscount = formatSlotDiscount;
|
|
@@ -3,7 +3,8 @@ type Props = {
|
|
|
3
3
|
utcDate: string;
|
|
4
4
|
minHoursForFreeCanceling: number;
|
|
5
5
|
cancelationRule: QuestroomCancelationTypeEnum;
|
|
6
|
-
|
|
6
|
+
/** Часовой пояс локации; при отсутствии считаем в UTC (см. ниже). */
|
|
7
|
+
timeZone?: string;
|
|
7
8
|
};
|
|
8
9
|
export declare function getOrderCancelationDate({ utcDate, minHoursForFreeCanceling, cancelationRule, timeZone, }: Props): {
|
|
9
10
|
orderCancelationDate: Date;
|
|
@@ -5,7 +5,12 @@ const questroom_cancelation_type_enum_1 = require("@escapenavigator/types/dist/q
|
|
|
5
5
|
const date_1 = require("../date");
|
|
6
6
|
function getOrderCancelationDate({ utcDate, minHoursForFreeCanceling, cancelationRule, timeZone, }) {
|
|
7
7
|
const minHours = cancelationRule === questroom_cancelation_type_enum_1.QuestroomCancelationTypeEnum.NO ? 0 : minHoursForFreeCanceling;
|
|
8
|
-
|
|
8
|
+
// Локация заказа может быть удалена (`questroom.location === null`), и
|
|
9
|
+
// тогда часового пояса просто нет. `utcToZonedTime` на undefined даёт
|
|
10
|
+
// Invalid Date, а `formatDate` на нём кидает — падала вся клиентская
|
|
11
|
+
// страница заказа (ESCAPE-NAVIGATOR-ORDERS-2D). Дата в UTC хуже
|
|
12
|
+
// локальной, но лучше пустого экрана.
|
|
13
|
+
const date = (0, date_1.addHours)((0, date_1.utcToZonedTime)(utcDate, timeZone || 'UTC'), minHours * -1);
|
|
9
14
|
return {
|
|
10
15
|
orderCancelationDate: date,
|
|
11
16
|
orderCancelationHumantDate: (0, date_1.formatDate)(date, 'dd.MM.yyyy, HH:mm'),
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { SlotDiscountTypeEnum } from '@escapenavigator/types/dist/slot/enum/slot-discount-type.enum';
|
|
2
|
+
type SlotDiscountType = SlotDiscountTypeEnum | string | null;
|
|
3
|
+
type Params = {
|
|
4
|
+
/** База начисления скидки в минорных единицах (стоимость игры за игроков). */
|
|
5
|
+
price: number;
|
|
6
|
+
/** `slots.discount`: базисные пункты для процента, минорные единицы для фикс. */
|
|
7
|
+
discount?: number | null;
|
|
8
|
+
discountType?: SlotDiscountType;
|
|
9
|
+
/**
|
|
10
|
+
* Процентная скидка исторически округлялась по-разному: `Math.round` в
|
|
11
|
+
* расчёте заказа и `Math.floor` в «цене от» для расписания. Параметр
|
|
12
|
+
* сохраняет это различие, чтобы правки не сдвигали цены на проде.
|
|
13
|
+
*/
|
|
14
|
+
rounding?: 'round' | 'floor';
|
|
15
|
+
};
|
|
16
|
+
export declare const isFixedSlotDiscount: (discountType?: SlotDiscountType) => boolean;
|
|
17
|
+
/**
|
|
18
|
+
* Сумма скидки слота в минорных единицах. Никогда не превышает базу, поэтому
|
|
19
|
+
* итог не уходит в минус даже если фиксированная скидка больше цены игры.
|
|
20
|
+
*/
|
|
21
|
+
export declare const getSlotDiscountAmount: ({ price, discount, discountType, rounding, }: Params) => number;
|
|
22
|
+
export {};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.getSlotDiscountAmount = exports.isFixedSlotDiscount = void 0;
|
|
4
|
+
const slot_discount_type_enum_1 = require("@escapenavigator/types/dist/slot/enum/slot-discount-type.enum");
|
|
5
|
+
const isFixedSlotDiscount = (discountType) => discountType === slot_discount_type_enum_1.SlotDiscountTypeEnum.FIXED;
|
|
6
|
+
exports.isFixedSlotDiscount = isFixedSlotDiscount;
|
|
7
|
+
/**
|
|
8
|
+
* Сумма скидки слота в минорных единицах. Никогда не превышает базу, поэтому
|
|
9
|
+
* итог не уходит в минус даже если фиксированная скидка больше цены игры.
|
|
10
|
+
*/
|
|
11
|
+
const getSlotDiscountAmount = ({ price, discount, discountType, rounding = 'round', }) => {
|
|
12
|
+
const value = discount || 0;
|
|
13
|
+
const base = price > 0 ? price : 0;
|
|
14
|
+
if (value <= 0 || !base)
|
|
15
|
+
return 0;
|
|
16
|
+
if ((0, exports.isFixedSlotDiscount)(discountType))
|
|
17
|
+
return Math.min(value, base);
|
|
18
|
+
const raw = (base * value) / 10000;
|
|
19
|
+
return Math.min(base, rounding === 'floor' ? Math.floor(raw) : Math.round(raw));
|
|
20
|
+
};
|
|
21
|
+
exports.getSlotDiscountAmount = getSlotDiscountAmount;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const slot_discount_type_enum_1 = require("@escapenavigator/types/dist/slot/enum/slot-discount-type.enum");
|
|
4
|
+
const get_slot_discount_amount_1 = require("./get-slot-discount-amount");
|
|
5
|
+
describe('getSlotDiscountAmount', () => {
|
|
6
|
+
it('treats a missing type as percent (слоты до появления поля)', () => {
|
|
7
|
+
expect((0, get_slot_discount_amount_1.getSlotDiscountAmount)({ price: 10000, discount: 1500 })).toBe(1500);
|
|
8
|
+
expect((0, get_slot_discount_amount_1.getSlotDiscountAmount)({ price: 10000, discount: 1500, discountType: null })).toBe(1500);
|
|
9
|
+
});
|
|
10
|
+
it('rounds percent by half up and by floor on demand', () => {
|
|
11
|
+
expect((0, get_slot_discount_amount_1.getSlotDiscountAmount)({ price: 3333, discount: 1500 })).toBe(500);
|
|
12
|
+
expect((0, get_slot_discount_amount_1.getSlotDiscountAmount)({ price: 3333, discount: 1500, rounding: 'floor' })).toBe(499);
|
|
13
|
+
});
|
|
14
|
+
it('takes a fixed discount as an amount in minor units', () => {
|
|
15
|
+
expect((0, get_slot_discount_amount_1.getSlotDiscountAmount)({
|
|
16
|
+
price: 10000,
|
|
17
|
+
discount: 1500,
|
|
18
|
+
discountType: slot_discount_type_enum_1.SlotDiscountTypeEnum.FIXED,
|
|
19
|
+
})).toBe(1500);
|
|
20
|
+
});
|
|
21
|
+
it('never exceeds the base price', () => {
|
|
22
|
+
expect((0, get_slot_discount_amount_1.getSlotDiscountAmount)({
|
|
23
|
+
price: 3000,
|
|
24
|
+
discount: 500000,
|
|
25
|
+
discountType: slot_discount_type_enum_1.SlotDiscountTypeEnum.FIXED,
|
|
26
|
+
})).toBe(3000);
|
|
27
|
+
expect((0, get_slot_discount_amount_1.getSlotDiscountAmount)({ price: 3000, discount: 20000 })).toBe(3000);
|
|
28
|
+
});
|
|
29
|
+
it('returns 0 for empty input', () => {
|
|
30
|
+
expect((0, get_slot_discount_amount_1.getSlotDiscountAmount)({ price: 0, discount: 1500 })).toBe(0);
|
|
31
|
+
expect((0, get_slot_discount_amount_1.getSlotDiscountAmount)({ price: 10000 })).toBe(0);
|
|
32
|
+
expect((0, get_slot_discount_amount_1.getSlotDiscountAmount)({ price: -100, discount: 1500 })).toBe(0);
|
|
33
|
+
expect((0, get_slot_discount_amount_1.getSlotDiscountAmount)({
|
|
34
|
+
price: 10000,
|
|
35
|
+
discount: -500,
|
|
36
|
+
discountType: slot_discount_type_enum_1.SlotDiscountTypeEnum.FIXED,
|
|
37
|
+
})).toBe(0);
|
|
38
|
+
});
|
|
39
|
+
});
|
package/dist/index.d.ts
CHANGED
|
@@ -8,12 +8,14 @@ export * from './convert-to-options';
|
|
|
8
8
|
export * from './date';
|
|
9
9
|
export * from './enum-to-options';
|
|
10
10
|
export * from './format-flex-duration-label';
|
|
11
|
+
export * from './format-slot-discount';
|
|
11
12
|
export * from './functional-popover-panel-style';
|
|
12
13
|
export * from './get-documents-links';
|
|
13
14
|
export * from './get-full-name';
|
|
14
15
|
export * from './get-handled-error-message';
|
|
15
16
|
export * from './get-order-cancelation-state';
|
|
16
17
|
export * from './get-service-error';
|
|
18
|
+
export * from './get-slot-discount-amount';
|
|
17
19
|
export * from './google-places-link-warnings';
|
|
18
20
|
export * from './google-places-nonquest';
|
|
19
21
|
export * from './image-url';
|
|
@@ -29,6 +31,7 @@ export * from './resolve-calendar-location-id';
|
|
|
29
31
|
export * from './role-chat-permissions';
|
|
30
32
|
export * from './role-comment-permissions';
|
|
31
33
|
export * from './role-export-permissions';
|
|
34
|
+
export * from './role-refund-permissions';
|
|
32
35
|
export * from './serialize-record';
|
|
33
36
|
export * from './serialize-slot';
|
|
34
37
|
export * from './slot-representative-price';
|
package/dist/index.js
CHANGED
|
@@ -24,12 +24,14 @@ __exportStar(require("./convert-to-options"), exports);
|
|
|
24
24
|
__exportStar(require("./date"), exports);
|
|
25
25
|
__exportStar(require("./enum-to-options"), exports);
|
|
26
26
|
__exportStar(require("./format-flex-duration-label"), exports);
|
|
27
|
+
__exportStar(require("./format-slot-discount"), exports);
|
|
27
28
|
__exportStar(require("./functional-popover-panel-style"), exports);
|
|
28
29
|
__exportStar(require("./get-documents-links"), exports);
|
|
29
30
|
__exportStar(require("./get-full-name"), exports);
|
|
30
31
|
__exportStar(require("./get-handled-error-message"), exports);
|
|
31
32
|
__exportStar(require("./get-order-cancelation-state"), exports);
|
|
32
33
|
__exportStar(require("./get-service-error"), exports);
|
|
34
|
+
__exportStar(require("./get-slot-discount-amount"), exports);
|
|
33
35
|
__exportStar(require("./google-places-link-warnings"), exports);
|
|
34
36
|
__exportStar(require("./google-places-nonquest"), exports);
|
|
35
37
|
__exportStar(require("./image-url"), exports);
|
|
@@ -45,6 +47,7 @@ __exportStar(require("./resolve-calendar-location-id"), exports);
|
|
|
45
47
|
__exportStar(require("./role-chat-permissions"), exports);
|
|
46
48
|
__exportStar(require("./role-comment-permissions"), exports);
|
|
47
49
|
__exportStar(require("./role-export-permissions"), exports);
|
|
50
|
+
__exportStar(require("./role-refund-permissions"), exports);
|
|
48
51
|
__exportStar(require("./serialize-record"), exports);
|
|
49
52
|
__exportStar(require("./serialize-slot"), exports);
|
|
50
53
|
__exportStar(require("./slot-representative-price"), exports);
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { PackagePricingModeEnum } from '@escapenavigator/types/dist/packages/package-pricing-mode.enum';
|
|
2
|
+
/**
|
|
3
|
+
* Единый калькулятор цены пакета. Живёт в utils, потому что одна и та же
|
|
4
|
+
* формула нужна в трёх местах: витрина виджета (показать «от»), конструктор
|
|
5
|
+
* (показать итог до брони) и бэкенд (посчитать реальную цену заказа).
|
|
6
|
+
* Расхождение между ними = клиент видит одну сумму, а платит другую.
|
|
7
|
+
*/
|
|
8
|
+
/** Поля пакета, от которых зависит цена. Ровно то, что нужно калькулятору. */
|
|
9
|
+
export type PackagePricingInput = {
|
|
10
|
+
pricingMode: PackagePricingModeEnum;
|
|
11
|
+
price: number;
|
|
12
|
+
weekendPrice?: number | null;
|
|
13
|
+
weekendDays?: number[] | null;
|
|
14
|
+
includedPlayers: number;
|
|
15
|
+
extraPlayerPrice: number;
|
|
16
|
+
};
|
|
17
|
+
export type PackagePriceBreakdown = {
|
|
18
|
+
/** Базовая цена пакета на выбранную дату. */
|
|
19
|
+
base: number;
|
|
20
|
+
/** Сколько игроков сверх включённых в пакет. */
|
|
21
|
+
extraPlayers: number;
|
|
22
|
+
/** Доплата за игроков сверх пакета. */
|
|
23
|
+
extraPlayersTotal: number;
|
|
24
|
+
/** Стоимость выбранных платных допов. */
|
|
25
|
+
addonsTotal: number;
|
|
26
|
+
/** Итог: base + extraPlayersTotal + addonsTotal. */
|
|
27
|
+
total: number;
|
|
28
|
+
/** Цена на человека (для «≈ 1 710 ₽ / чел.»). */
|
|
29
|
+
perPerson: number;
|
|
30
|
+
/** Дата попала в «выходные» по настройкам пакета. */
|
|
31
|
+
isWeekend: boolean;
|
|
32
|
+
};
|
|
33
|
+
/**
|
|
34
|
+
* Индекс дня недели в проектной нумерации: 0 — понедельник … 6 — воскресенье.
|
|
35
|
+
* Совпадает с `availableDays` слотов/апселлов и компонентом `DaysOfWeek`,
|
|
36
|
+
* поэтому НЕ равен `Date.getDay()`.
|
|
37
|
+
*/
|
|
38
|
+
export declare const getProjectWeekday: (date: Date | string) => number;
|
|
39
|
+
export declare const isPackageWeekend: (pkg: Pick<PackagePricingInput, "weekendDays">, date?: Date | string | null) => boolean;
|
|
40
|
+
/** Цена пакета за день без учёта игроков и допов. */
|
|
41
|
+
export declare const getPackageBasePrice: (pkg: PackagePricingInput, date?: Date | string | null) => number;
|
|
42
|
+
/** Диапазон цен пакета по дням недели — для витрины («от / до»). */
|
|
43
|
+
export declare const getPackagePriceRange: (pkg: PackagePricingInput) => {
|
|
44
|
+
min: number;
|
|
45
|
+
max: number;
|
|
46
|
+
hasWeekendPrice: boolean;
|
|
47
|
+
};
|
|
48
|
+
export declare const getPackageExtraPlayers: (pkg: PackagePricingInput, players: number) => number;
|
|
49
|
+
/**
|
|
50
|
+
* Полный расчёт цены пакета в режиме `fixed`.
|
|
51
|
+
*
|
|
52
|
+
* Для `discount`-пакетов возвращает нули по базе: там цену диктуют тарифы
|
|
53
|
+
* слотов, а пакет умеет только скидку — считать её без состава группы нельзя.
|
|
54
|
+
*/
|
|
55
|
+
export declare const calculatePackagePrice: ({ pkg, date, players, addonsTotal, }: {
|
|
56
|
+
pkg: PackagePricingInput;
|
|
57
|
+
date?: Date | string | null;
|
|
58
|
+
players: number;
|
|
59
|
+
addonsTotal?: number;
|
|
60
|
+
}) => PackagePriceBreakdown;
|
|
61
|
+
/**
|
|
62
|
+
* Прайс-мапа `{ игроков: цена }` для заказа-носителя пакета.
|
|
63
|
+
*
|
|
64
|
+
* Заказ хранит снимок тарифа в `order.price`, и `getPlayersPrice` берёт из
|
|
65
|
+
* него значение по текущему числу игроков. Поэтому мы отдаём не одно число,
|
|
66
|
+
* а карту на весь допустимый диапазон: если менеджер потом поменяет состав,
|
|
67
|
+
* пересчёт останется по формуле пакета, а не превратится в ноль.
|
|
68
|
+
*/
|
|
69
|
+
export declare const buildPackagePriceMap: ({ pkg, date, maxPlayers, }: {
|
|
70
|
+
pkg: PackagePricingInput;
|
|
71
|
+
date?: Date | string | null;
|
|
72
|
+
maxPlayers: number;
|
|
73
|
+
}) => Record<string, number>;
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.buildPackagePriceMap = exports.calculatePackagePrice = exports.getPackageExtraPlayers = exports.getPackagePriceRange = exports.getPackageBasePrice = exports.isPackageWeekend = exports.getProjectWeekday = void 0;
|
|
4
|
+
const package_pricing_mode_enum_1 = require("@escapenavigator/types/dist/packages/package-pricing-mode.enum");
|
|
5
|
+
const DEFAULT_WEEKEND_DAYS = [5, 6];
|
|
6
|
+
const toNumber = (value) => {
|
|
7
|
+
const parsed = Number(value);
|
|
8
|
+
return Number.isFinite(parsed) ? parsed : 0;
|
|
9
|
+
};
|
|
10
|
+
/**
|
|
11
|
+
* Индекс дня недели в проектной нумерации: 0 — понедельник … 6 — воскресенье.
|
|
12
|
+
* Совпадает с `availableDays` слотов/апселлов и компонентом `DaysOfWeek`,
|
|
13
|
+
* поэтому НЕ равен `Date.getDay()`.
|
|
14
|
+
*/
|
|
15
|
+
const getProjectWeekday = (date) => {
|
|
16
|
+
const parsed = typeof date === 'string' ? new Date(`${date}T00:00:00`) : date;
|
|
17
|
+
if (Number.isNaN(parsed.getTime()))
|
|
18
|
+
return 0;
|
|
19
|
+
return (parsed.getDay() + 6) % 7;
|
|
20
|
+
};
|
|
21
|
+
exports.getProjectWeekday = getProjectWeekday;
|
|
22
|
+
const isPackageWeekend = (pkg, date) => {
|
|
23
|
+
if (!date)
|
|
24
|
+
return false;
|
|
25
|
+
const weekendDays = pkg.weekendDays?.length ? pkg.weekendDays : DEFAULT_WEEKEND_DAYS;
|
|
26
|
+
return weekendDays.includes((0, exports.getProjectWeekday)(date));
|
|
27
|
+
};
|
|
28
|
+
exports.isPackageWeekend = isPackageWeekend;
|
|
29
|
+
/** Цена пакета за день без учёта игроков и допов. */
|
|
30
|
+
const getPackageBasePrice = (pkg, date) => {
|
|
31
|
+
const weekday = Math.max(0, toNumber(pkg.price));
|
|
32
|
+
const weekend = pkg.weekendPrice == null ? weekday : Math.max(0, toNumber(pkg.weekendPrice));
|
|
33
|
+
return (0, exports.isPackageWeekend)(pkg, date) ? weekend : weekday;
|
|
34
|
+
};
|
|
35
|
+
exports.getPackageBasePrice = getPackageBasePrice;
|
|
36
|
+
/** Диапазон цен пакета по дням недели — для витрины («от / до»). */
|
|
37
|
+
const getPackagePriceRange = (pkg) => {
|
|
38
|
+
const weekday = Math.max(0, toNumber(pkg.price));
|
|
39
|
+
const weekend = pkg.weekendPrice == null ? weekday : Math.max(0, toNumber(pkg.weekendPrice));
|
|
40
|
+
return {
|
|
41
|
+
min: Math.min(weekday, weekend),
|
|
42
|
+
max: Math.max(weekday, weekend),
|
|
43
|
+
hasWeekendPrice: pkg.weekendPrice != null && weekend !== weekday,
|
|
44
|
+
};
|
|
45
|
+
};
|
|
46
|
+
exports.getPackagePriceRange = getPackagePriceRange;
|
|
47
|
+
const getPackageExtraPlayers = (pkg, players) => Math.max(0, Math.floor(toNumber(players)) - Math.max(0, Math.floor(toNumber(pkg.includedPlayers))));
|
|
48
|
+
exports.getPackageExtraPlayers = getPackageExtraPlayers;
|
|
49
|
+
/**
|
|
50
|
+
* Полный расчёт цены пакета в режиме `fixed`.
|
|
51
|
+
*
|
|
52
|
+
* Для `discount`-пакетов возвращает нули по базе: там цену диктуют тарифы
|
|
53
|
+
* слотов, а пакет умеет только скидку — считать её без состава группы нельзя.
|
|
54
|
+
*/
|
|
55
|
+
const calculatePackagePrice = ({ pkg, date, players, addonsTotal = 0, }) => {
|
|
56
|
+
const isFixed = pkg.pricingMode === package_pricing_mode_enum_1.PackagePricingModeEnum.FIXED;
|
|
57
|
+
const base = isFixed ? (0, exports.getPackageBasePrice)(pkg, date) : 0;
|
|
58
|
+
const extraPlayers = isFixed ? (0, exports.getPackageExtraPlayers)(pkg, players) : 0;
|
|
59
|
+
const extraPlayersTotal = extraPlayers * Math.max(0, toNumber(pkg.extraPlayerPrice));
|
|
60
|
+
const safeAddons = Math.max(0, toNumber(addonsTotal));
|
|
61
|
+
const total = base + extraPlayersTotal + safeAddons;
|
|
62
|
+
const guests = Math.max(1, Math.floor(toNumber(players)));
|
|
63
|
+
return {
|
|
64
|
+
base,
|
|
65
|
+
extraPlayers,
|
|
66
|
+
extraPlayersTotal,
|
|
67
|
+
addonsTotal: safeAddons,
|
|
68
|
+
total,
|
|
69
|
+
perPerson: Math.round(total / guests),
|
|
70
|
+
isWeekend: (0, exports.isPackageWeekend)(pkg, date),
|
|
71
|
+
};
|
|
72
|
+
};
|
|
73
|
+
exports.calculatePackagePrice = calculatePackagePrice;
|
|
74
|
+
/**
|
|
75
|
+
* Прайс-мапа `{ игроков: цена }` для заказа-носителя пакета.
|
|
76
|
+
*
|
|
77
|
+
* Заказ хранит снимок тарифа в `order.price`, и `getPlayersPrice` берёт из
|
|
78
|
+
* него значение по текущему числу игроков. Поэтому мы отдаём не одно число,
|
|
79
|
+
* а карту на весь допустимый диапазон: если менеджер потом поменяет состав,
|
|
80
|
+
* пересчёт останется по формуле пакета, а не превратится в ноль.
|
|
81
|
+
*/
|
|
82
|
+
const buildPackagePriceMap = ({ pkg, date, maxPlayers, }) => {
|
|
83
|
+
const base = (0, exports.getPackageBasePrice)(pkg, date);
|
|
84
|
+
const extraPrice = Math.max(0, toNumber(pkg.extraPlayerPrice));
|
|
85
|
+
const limit = Math.max(1, Math.floor(toNumber(maxPlayers)));
|
|
86
|
+
const map = {};
|
|
87
|
+
for (let players = 1; players <= limit; players++) {
|
|
88
|
+
map[players] = base + (0, exports.getPackageExtraPlayers)(pkg, players) * extraPrice;
|
|
89
|
+
}
|
|
90
|
+
return map;
|
|
91
|
+
};
|
|
92
|
+
exports.buildPackagePriceMap = buildPackagePriceMap;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const package_pricing_mode_enum_1 = require("@escapenavigator/types/dist/packages/package-pricing-mode.enum");
|
|
4
|
+
const _1 = require(".");
|
|
5
|
+
/** «Хит» из витрины: 13 590 в будни, 15 590 в выходные, 8 гостей, +800 за гостя. */
|
|
6
|
+
const hit = {
|
|
7
|
+
pricingMode: package_pricing_mode_enum_1.PackagePricingModeEnum.FIXED,
|
|
8
|
+
price: 13590,
|
|
9
|
+
weekendPrice: 15590,
|
|
10
|
+
weekendDays: [5, 6],
|
|
11
|
+
includedPlayers: 8,
|
|
12
|
+
extraPlayerPrice: 800,
|
|
13
|
+
};
|
|
14
|
+
describe('getProjectWeekday', () => {
|
|
15
|
+
it('нумерует с понедельника, а не с воскресенья как Date.getDay', () => {
|
|
16
|
+
expect((0, _1.getProjectWeekday)('2026-08-17')).toBe(0);
|
|
17
|
+
expect((0, _1.getProjectWeekday)('2026-08-22')).toBe(5);
|
|
18
|
+
expect((0, _1.getProjectWeekday)('2026-08-23')).toBe(6);
|
|
19
|
+
});
|
|
20
|
+
});
|
|
21
|
+
describe('isPackageWeekend', () => {
|
|
22
|
+
it('по умолчанию выходные — суббота и воскресенье', () => {
|
|
23
|
+
expect((0, _1.isPackageWeekend)({ weekendDays: [] }, '2026-08-22')).toBe(true);
|
|
24
|
+
expect((0, _1.isPackageWeekend)({ weekendDays: null }, '2026-08-19')).toBe(false);
|
|
25
|
+
});
|
|
26
|
+
it('уважает кастомный набор дней', () => {
|
|
27
|
+
expect((0, _1.isPackageWeekend)({ weekendDays: [4, 5, 6] }, '2026-08-21')).toBe(true);
|
|
28
|
+
});
|
|
29
|
+
it('без даты не угадывает', () => {
|
|
30
|
+
expect((0, _1.isPackageWeekend)(hit, null)).toBe(false);
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
describe('getPackageBasePrice', () => {
|
|
34
|
+
it('берёт цену выходного дня только в выходные', () => {
|
|
35
|
+
expect((0, _1.getPackageBasePrice)(hit, '2026-08-19')).toBe(13590);
|
|
36
|
+
expect((0, _1.getPackageBasePrice)(hit, '2026-08-22')).toBe(15590);
|
|
37
|
+
});
|
|
38
|
+
it('без цены выходного дня цена одна на всю неделю', () => {
|
|
39
|
+
expect((0, _1.getPackageBasePrice)({ ...hit, weekendPrice: null }, '2026-08-22')).toBe(13590);
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
describe('getPackagePriceRange', () => {
|
|
43
|
+
it('отдаёт диапазон для витрины', () => {
|
|
44
|
+
expect((0, _1.getPackagePriceRange)(hit)).toEqual({
|
|
45
|
+
min: 13590,
|
|
46
|
+
max: 15590,
|
|
47
|
+
hasWeekendPrice: true,
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
it('схлопывает диапазон, когда выходная цена совпадает с будней', () => {
|
|
51
|
+
expect((0, _1.getPackagePriceRange)({ ...hit, weekendPrice: 13590 })).toEqual({
|
|
52
|
+
min: 13590,
|
|
53
|
+
max: 13590,
|
|
54
|
+
hasWeekendPrice: false,
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
describe('calculatePackagePrice', () => {
|
|
59
|
+
it('считает 9 гостей в будни как пакет + одна доплата', () => {
|
|
60
|
+
expect((0, _1.calculatePackagePrice)({ pkg: hit, date: '2026-08-19', players: 9, addonsTotal: 1000 })).toEqual({
|
|
61
|
+
base: 13590,
|
|
62
|
+
extraPlayers: 1,
|
|
63
|
+
extraPlayersTotal: 800,
|
|
64
|
+
addonsTotal: 1000,
|
|
65
|
+
total: 15390,
|
|
66
|
+
perPerson: 1710,
|
|
67
|
+
isWeekend: false,
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
it('не берёт доплату, пока гостей не больше включённых', () => {
|
|
71
|
+
const result = (0, _1.calculatePackagePrice)({ pkg: hit, date: '2026-08-19', players: 5 });
|
|
72
|
+
expect(result.extraPlayers).toBe(0);
|
|
73
|
+
expect(result.total).toBe(13590);
|
|
74
|
+
});
|
|
75
|
+
it('в discount-режиме базы у пакета нет — цену диктуют тарифы слотов', () => {
|
|
76
|
+
const result = (0, _1.calculatePackagePrice)({
|
|
77
|
+
pkg: { ...hit, pricingMode: package_pricing_mode_enum_1.PackagePricingModeEnum.DISCOUNT },
|
|
78
|
+
date: '2026-08-22',
|
|
79
|
+
players: 12,
|
|
80
|
+
addonsTotal: 500,
|
|
81
|
+
});
|
|
82
|
+
expect(result.base).toBe(0);
|
|
83
|
+
expect(result.extraPlayersTotal).toBe(0);
|
|
84
|
+
expect(result.total).toBe(500);
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
describe('buildPackagePriceMap', () => {
|
|
88
|
+
it('покрывает весь диапазон, чтобы смена состава не обнуляла заказ', () => {
|
|
89
|
+
const map = (0, _1.buildPackagePriceMap)({ pkg: hit, date: '2026-08-19', maxPlayers: 10 });
|
|
90
|
+
expect(map[1]).toBe(13590);
|
|
91
|
+
expect(map[8]).toBe(13590);
|
|
92
|
+
expect(map[9]).toBe(14390);
|
|
93
|
+
expect(map[10]).toBe(15190);
|
|
94
|
+
expect(Object.keys(map)).toHaveLength(10);
|
|
95
|
+
});
|
|
96
|
+
it('в выходные строится от выходной цены', () => {
|
|
97
|
+
const map = (0, _1.buildPackagePriceMap)({ pkg: hit, date: '2026-08-22', maxPlayers: 9 });
|
|
98
|
+
expect(map[9]).toBe(16390);
|
|
99
|
+
});
|
|
100
|
+
});
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { RoleRO } from '@escapenavigator/types/dist/role/role.ro';
|
|
2
|
+
export type RoleRefundPermissions = Pick<RoleRO, 'totalAccess' | 'canAddRefunds'>;
|
|
3
|
+
/**
|
|
4
|
+
* Право оформлять возвраты (в т.ч. Stripe). Владелец (`totalAccess`) —
|
|
5
|
+
* как будто флаг включён.
|
|
6
|
+
*/
|
|
7
|
+
export declare const canAddRefunds: (role?: RoleRefundPermissions | null) => boolean;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.canAddRefunds = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* Право оформлять возвраты (в т.ч. Stripe). Владелец (`totalAccess`) —
|
|
6
|
+
* как будто флаг включён.
|
|
7
|
+
*/
|
|
8
|
+
const canAddRefunds = (role) => !!role?.totalAccess || !!role?.canAddRefunds;
|
|
9
|
+
exports.canAddRefunds = canAddRefunds;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const role_refund_permissions_1 = require("./role-refund-permissions");
|
|
4
|
+
describe('canAddRefunds', () => {
|
|
5
|
+
it('is false without role or flag', () => {
|
|
6
|
+
expect((0, role_refund_permissions_1.canAddRefunds)()).toBe(false);
|
|
7
|
+
expect((0, role_refund_permissions_1.canAddRefunds)(null)).toBe(false);
|
|
8
|
+
expect((0, role_refund_permissions_1.canAddRefunds)({ totalAccess: false, canAddRefunds: false })).toBe(false);
|
|
9
|
+
});
|
|
10
|
+
it('is true when flag is on', () => {
|
|
11
|
+
expect((0, role_refund_permissions_1.canAddRefunds)({ totalAccess: false, canAddRefunds: true })).toBe(true);
|
|
12
|
+
});
|
|
13
|
+
it('is true for totalAccess even when flag is off', () => {
|
|
14
|
+
expect((0, role_refund_permissions_1.canAddRefunds)({ totalAccess: true, canAddRefunds: false })).toBe(true);
|
|
15
|
+
});
|
|
16
|
+
});
|
package/dist/serialize-slot.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.serializeSlot = exports.serializeSlotOrderData = void 0;
|
|
4
|
+
const slot_discount_type_enum_1 = require("@escapenavigator/types/dist/slot/enum/slot-discount-type.enum");
|
|
4
5
|
const defailt_rule_1 = require("@escapenavigator/types/dist/slot-rule/defailt-rule");
|
|
5
6
|
const buildRuleSummary = (rule) => {
|
|
6
7
|
const { prepayment, prepaymentType, title, minHoursForBooking, minMinutesForBooking, minHoursForFreeCanceling, cancelationRule, minHoursForFullFine, cancelationAmount, } = rule || defailt_rule_1.defaultRule;
|
|
@@ -62,9 +63,16 @@ const serializeSlot = ({ slot, orders = [] }) => ({
|
|
|
62
63
|
tariff: slot.tariff?.price || slot.tariff || {},
|
|
63
64
|
status: slot.status,
|
|
64
65
|
discount: slot.discount ?? 0,
|
|
66
|
+
discountType: slot.discountType ?? slot_discount_type_enum_1.SlotDiscountTypeEnum.PERCENT,
|
|
65
67
|
onlyPhone: slot.onlyPhone ?? false,
|
|
66
68
|
forceOnlineBooking: slot.forceOnlineBooking ?? false,
|
|
67
69
|
ignoreResourceLimit: slot.ignoreResourceLimit ?? false,
|
|
70
|
+
// Пустой список не отдаём вообще: `calendar-day` сериализует до 1000
|
|
71
|
+
// слотов за раз, и `"excludedUpsellingIds":[]` на каждом — это лишние
|
|
72
|
+
// десятки килобайт в ответе ради информации «ничего не запрещено».
|
|
73
|
+
excludedUpsellingIds: slot.excludedUpsellingIds?.length
|
|
74
|
+
? slot.excludedUpsellingIds
|
|
75
|
+
: undefined,
|
|
68
76
|
breakReason: slot.breakReason ?? null,
|
|
69
77
|
availableTeams: slot.availableTeams ?? null,
|
|
70
78
|
numSeatsAvailable: slot.numSeatsAvailable ?? slot.availablePlayers ?? 0,
|
|
@@ -1,7 +1,9 @@
|
|
|
1
|
+
import { SlotDiscountTypeEnum } from '@escapenavigator/types/dist/slot/enum/slot-discount-type.enum';
|
|
1
2
|
type SlotPriceSource = {
|
|
2
3
|
tariff?: unknown;
|
|
3
4
|
basePrice?: number;
|
|
4
5
|
discount?: number;
|
|
6
|
+
discountType?: SlotDiscountTypeEnum | string | null;
|
|
5
7
|
};
|
|
6
8
|
/**
|
|
7
9
|
* Плоская карта цен из тарифа слота.
|
|
@@ -10,6 +12,14 @@ type SlotPriceSource = {
|
|
|
10
12
|
export declare const getTariffPriceMap: (tariff: unknown) => Record<string, number>;
|
|
11
13
|
/** Минимальная положительная цена из тарифа (без скидки слота). */
|
|
12
14
|
export declare const getTariffBasePrice: (tariff: unknown) => number | null;
|
|
15
|
+
/**
|
|
16
|
+
* Цена «от» до и после скидки слота. Обе величины нужны расписанию: одну
|
|
17
|
+
* показываем, вторую зачёркиваем.
|
|
18
|
+
*/
|
|
19
|
+
export declare const getSlotRepresentativePrices: (slot: SlotPriceSource) => {
|
|
20
|
+
base: number;
|
|
21
|
+
discounted: number;
|
|
22
|
+
} | null;
|
|
13
23
|
/** Цена «от» для слота в расписании (с учётом скидки слота). */
|
|
14
24
|
export declare const getSlotRepresentativePrice: (slot: SlotPriceSource) => number | null;
|
|
15
25
|
export {};
|
|
@@ -1,12 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.getSlotRepresentativePrice = exports.getTariffBasePrice = exports.getTariffPriceMap = void 0;
|
|
4
|
-
const
|
|
5
|
-
const value = discount || 0;
|
|
6
|
-
if (!value)
|
|
7
|
-
return base;
|
|
8
|
-
return base - Math.floor((base / 10000) * value);
|
|
9
|
-
};
|
|
3
|
+
exports.getSlotRepresentativePrice = exports.getSlotRepresentativePrices = exports.getTariffBasePrice = exports.getTariffPriceMap = void 0;
|
|
4
|
+
const get_slot_discount_amount_1 = require("./get-slot-discount-amount");
|
|
10
5
|
/**
|
|
11
6
|
* Плоская карта цен из тарифа слота.
|
|
12
7
|
* Принимает и `{ id, price: { "2": 1500 } }` (листинг), и `{ "2": 1500 }` (/slots/details).
|
|
@@ -36,13 +31,25 @@ const getTariffBasePrice = (tariff) => {
|
|
|
36
31
|
return null;
|
|
37
32
|
};
|
|
38
33
|
exports.getTariffBasePrice = getTariffBasePrice;
|
|
39
|
-
/**
|
|
40
|
-
|
|
34
|
+
/**
|
|
35
|
+
* Цена «от» до и после скидки слота. Обе величины нужны расписанию: одну
|
|
36
|
+
* показываем, вторую зачёркиваем.
|
|
37
|
+
*/
|
|
38
|
+
const getSlotRepresentativePrices = (slot) => {
|
|
41
39
|
const base = typeof slot.basePrice === 'number' && slot.basePrice > 0
|
|
42
40
|
? slot.basePrice
|
|
43
41
|
: (0, exports.getTariffBasePrice)(slot.tariff);
|
|
44
42
|
if (base == null)
|
|
45
43
|
return null;
|
|
46
|
-
|
|
44
|
+
const discountAmount = (0, get_slot_discount_amount_1.getSlotDiscountAmount)({
|
|
45
|
+
price: base,
|
|
46
|
+
discount: slot.discount,
|
|
47
|
+
discountType: slot.discountType,
|
|
48
|
+
rounding: 'floor',
|
|
49
|
+
});
|
|
50
|
+
return { base, discounted: base - discountAmount };
|
|
47
51
|
};
|
|
52
|
+
exports.getSlotRepresentativePrices = getSlotRepresentativePrices;
|
|
53
|
+
/** Цена «от» для слота в расписании (с учётом скидки слота). */
|
|
54
|
+
const getSlotRepresentativePrice = (slot) => (0, exports.getSlotRepresentativePrices)(slot)?.discounted ?? null;
|
|
48
55
|
exports.getSlotRepresentativePrice = getSlotRepresentativePrice;
|
package/dist/tz-date.d.ts
CHANGED
package/dist/tz-date.js
CHANGED
|
@@ -14,9 +14,14 @@ exports.convertDateToZonedDate = convertDateToZonedDate;
|
|
|
14
14
|
const convertUTCDateToZonedDate = ({ date, timeZone, offset = 0, format = 'dd.MM.yyyy, HH:mm', showTZ = false, }) => {
|
|
15
15
|
if (!date)
|
|
16
16
|
return '';
|
|
17
|
-
|
|
17
|
+
// Часового пояса может не быть (удалённая локация заказа — см.
|
|
18
|
+
// getOrderCancelationDate): на undefined `utcToZonedTime` даёт Invalid
|
|
19
|
+
// Date, а `formatDate` на нём кидает и роняет весь экран. Показываем
|
|
20
|
+
// время в UTC и честно подписываем его в `showTZ`.
|
|
21
|
+
const tz = timeZone || 'UTC';
|
|
22
|
+
const d = (0, date_1.utcToZonedTime)(date, tz);
|
|
18
23
|
const dt = (0, date_1.addHours)(d, offset);
|
|
19
24
|
const res = (0, date_1.formatDate)(dt, format);
|
|
20
|
-
return showTZ ? `${res} (${
|
|
25
|
+
return showTZ ? `${res} (${tz})` : res;
|
|
21
26
|
};
|
|
22
27
|
exports.convertUTCDateToZonedDate = convertUTCDateToZonedDate;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@escapenavigator/utils",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.39",
|
|
4
4
|
"publishConfig": {
|
|
5
5
|
"access": "public"
|
|
6
6
|
},
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
"test": "jest"
|
|
15
15
|
},
|
|
16
16
|
"dependencies": {
|
|
17
|
-
"@escapenavigator/types": "^2.0.
|
|
17
|
+
"@escapenavigator/types": "^2.0.37",
|
|
18
18
|
"axios": "^0.21.4",
|
|
19
19
|
"class-transformer": "^0.5.1",
|
|
20
20
|
"class-validator": "^0.13.2",
|
|
@@ -29,5 +29,5 @@
|
|
|
29
29
|
"ts-jest": "^29.1.1",
|
|
30
30
|
"typescript": "^5.6"
|
|
31
31
|
},
|
|
32
|
-
"gitHead": "
|
|
32
|
+
"gitHead": "a5a2b7072d29242e6477d13cf07022fca9862b8c"
|
|
33
33
|
}
|