@escapenavigator/utils 2.0.49 → 2.1.0

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.
@@ -0,0 +1,453 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const tariff_category_mode_enum_1 = require("@escapenavigator/types/dist/tariff/enum/tariff-category-mode.enum");
4
+ const tariff_reduced_rate_mode_enum_1 = require("@escapenavigator/types/dist/tariff/enum/tariff-reduced-rate-mode.enum");
5
+ const composition_price_1 = require("./composition-price");
6
+ const get_players_price_1 = require("./get-players-price");
7
+ /** Прайс реальной комнаты (Paris / Strange): база 98 € за двоих. */
8
+ const PARIS = {
9
+ 2: 9800,
10
+ 3: 12000,
11
+ 4: 14000,
12
+ 5: 16000,
13
+ 6: 18000,
14
+ 7: 18900,
15
+ 8: 20000,
16
+ 9: 20700,
17
+ 10: 22000,
18
+ 11: 24200,
19
+ 12: 26400,
20
+ };
21
+ const adults = {
22
+ key: 'adult',
23
+ title: 'Взрослые',
24
+ mode: tariff_category_mode_enum_1.TariffCategoryModeEnum.BASE,
25
+ value: 0,
26
+ countsInPrice: true,
27
+ };
28
+ describe('фиксированная скидка с человека', () => {
29
+ const students = {
30
+ key: 'student',
31
+ title: 'Студенты',
32
+ mode: tariff_category_mode_enum_1.TariffCategoryModeEnum.DELTA,
33
+ value: 500,
34
+ countsInPrice: true,
35
+ };
36
+ const price = (adult, student) => (0, composition_price_1.getCompositionPrice)({
37
+ price: PARIS,
38
+ categories: [adults, students],
39
+ composition: { adult, student },
40
+ minGameAmount: 9800,
41
+ });
42
+ /**
43
+ * Льготная колонка венью целиком: суммы взяты из их прайс-листа. Раньше
44
+ * этот же результат давала связка `reducedRate` + `reducedPlayers` — так
45
+ * проверяем, что перевод на категории не сдвинул ни одной цены.
46
+ */
47
+ it.each([
48
+ [3, 10500],
49
+ [4, 12000],
50
+ [5, 13500],
51
+ [6, 15000],
52
+ [7, 15400],
53
+ [8, 16000],
54
+ [9, 16200],
55
+ [10, 17000],
56
+ [11, 18700],
57
+ [12, 20400],
58
+ ])('вся группа из %i студентов стоит %i', (heads, expected) => {
59
+ expect(price(0, heads)).toBe(expected);
60
+ });
61
+ it('смешанный состав считается по головам', () => {
62
+ // Четверо за 140 €, из них двое студентов по −5 €.
63
+ expect(price(2, 2)).toBe(13000);
64
+ });
65
+ it('состав определяет уровень сетки независимо от категорий', () => {
66
+ // 1 + 3 и 4 + 0 — один и тот же уровень price[4].
67
+ expect(price(1, 3)).toBe(14000 - 1500);
68
+ expect(price(4, 0)).toBe(14000);
69
+ });
70
+ it('на минимальной группе скидка упирается в пол и выбор не блокирует', () => {
71
+ expect(price(0, 2)).toBe(9800);
72
+ expect((0, composition_price_1.getMaxDiscountedSeats)({
73
+ price: PARIS,
74
+ categories: [adults, students],
75
+ composition: { adult: 0, student: 2 },
76
+ minGameAmount: 9800,
77
+ }, 'student')).toBe(0);
78
+ });
79
+ });
80
+ describe('фиксированная цена за человека', () => {
81
+ // Прайс из письма венью: 88 € за двоих (он же минимум), 108 € за трёх.
82
+ const price = { 2: 8800, 3: 10800, 4: 12000 };
83
+ const reduced = {
84
+ key: 'reduced',
85
+ title: 'Школьники',
86
+ mode: tariff_category_mode_enum_1.TariffCategoryModeEnum.FIXED,
87
+ value: 2200,
88
+ countsInPrice: true,
89
+ };
90
+ const total = (adult, school) => (0, composition_price_1.getCompositionPrice)({
91
+ price,
92
+ categories: [adults, reduced],
93
+ composition: { adult, reduced: school },
94
+ minGameAmount: 8800,
95
+ });
96
+ it('на троих одно льготное место даёт 94 €', () => {
97
+ expect(total(2, 1)).toBe(9400);
98
+ });
99
+ it('на четверых льготными могут быть все — 88 €', () => {
100
+ expect(total(0, 4)).toBe(8800);
101
+ });
102
+ it('смешанный состав: 2 обычных по 30 € + 2 льготных по 22 €', () => {
103
+ expect(total(2, 2)).toBe(10400);
104
+ });
105
+ it('не даёт скидки там, где обычная цена уже опустилась до льготной', () => {
106
+ // На 10 человек доля и так 22 € — категория ничего не меняет.
107
+ expect((0, composition_price_1.getCompositionPrice)({
108
+ price: PARIS,
109
+ categories: [adults, reduced],
110
+ composition: { adult: 0, reduced: 10 },
111
+ minGameAmount: 9800,
112
+ })).toBe(22000);
113
+ });
114
+ });
115
+ describe('процентная скидка', () => {
116
+ const seniors = {
117
+ key: 'senior',
118
+ title: 'Пенсионеры',
119
+ mode: tariff_category_mode_enum_1.TariffCategoryModeEnum.PERCENT,
120
+ value: 2000,
121
+ countsInPrice: true,
122
+ };
123
+ it('снимает свой процент с доли гостя', () => {
124
+ // price[4] = 140 €, доля 35 €, −20 % = 7 € с каждого пенсионера.
125
+ expect((0, composition_price_1.getCompositionPrice)({
126
+ price: PARIS,
127
+ categories: [adults, seniors],
128
+ composition: { adult: 3, senior: 1 },
129
+ })).toBe(13300);
130
+ expect((0, composition_price_1.getCompositionPrice)({
131
+ price: PARIS,
132
+ categories: [adults, seniors],
133
+ composition: { adult: 0, senior: 4 },
134
+ })).toBe(11200);
135
+ });
136
+ it('сто процентов делают гостя бесплатным, но место в сетке он занимает', () => {
137
+ const free = { ...seniors, value: 10000 };
138
+ expect((0, composition_price_1.getCompositionPrice)({
139
+ price: PARIS,
140
+ categories: [adults, free],
141
+ composition: { adult: 2, senior: 2 },
142
+ })).toBe(7000);
143
+ });
144
+ it('процент не уводит цену ниже минимальной стоимости игры', () => {
145
+ expect((0, composition_price_1.getCompositionPrice)({
146
+ price: PARIS,
147
+ categories: [adults, { ...seniors, value: 10000 }],
148
+ composition: { adult: 2, senior: 2 },
149
+ minGameAmount: 9800,
150
+ })).toBe(9800);
151
+ });
152
+ });
153
+ describe('категории сверх групповой цены', () => {
154
+ const kids = {
155
+ key: 'kid',
156
+ title: 'Дети до 7 лет',
157
+ mode: tariff_category_mode_enum_1.TariffCategoryModeEnum.FIXED,
158
+ value: 0,
159
+ countsInPrice: false,
160
+ };
161
+ it('бесплатные дети не платят и не двигают уровень сетки', () => {
162
+ expect((0, composition_price_1.getCompositionPrice)({
163
+ price: PARIS,
164
+ categories: [adults, kids],
165
+ composition: { adult: 4, kid: 3 },
166
+ })).toBe(14000);
167
+ });
168
+ it('платные дети идут надбавкой к цене группы', () => {
169
+ expect((0, composition_price_1.getCompositionPrice)({
170
+ price: PARIS,
171
+ categories: [adults, { ...kids, value: 1000 }],
172
+ composition: { adult: 4, kid: 2 },
173
+ })).toBe(16000);
174
+ });
175
+ it('недоступный состав не считается, но надбавка остаётся', () => {
176
+ // Уровня на 20 человек в прайсе нет: минимум к такому составу не
177
+ // применяется, иначе отключённое число игроков начало бы стоить денег.
178
+ expect((0, composition_price_1.getCompositionPrice)({
179
+ price: PARIS,
180
+ categories: [adults, { ...kids, value: 1000 }],
181
+ composition: { adult: 20, kid: 1 },
182
+ minGameAmount: 9800,
183
+ })).toBe(1000);
184
+ });
185
+ });
186
+ describe('смешанный состав из нескольких категорий', () => {
187
+ const categories = [
188
+ adults,
189
+ {
190
+ key: 'student',
191
+ title: 'Студенты',
192
+ mode: tariff_category_mode_enum_1.TariffCategoryModeEnum.DELTA,
193
+ value: 500,
194
+ countsInPrice: true,
195
+ },
196
+ {
197
+ key: 'senior',
198
+ title: 'Пенсионеры',
199
+ mode: tariff_category_mode_enum_1.TariffCategoryModeEnum.PERCENT,
200
+ value: 2500,
201
+ countsInPrice: true,
202
+ },
203
+ {
204
+ key: 'kid',
205
+ title: 'Дети до 7 лет',
206
+ mode: tariff_category_mode_enum_1.TariffCategoryModeEnum.FIXED,
207
+ value: 0,
208
+ countsInPrice: false,
209
+ },
210
+ ];
211
+ it('считает все категории в одной броне', () => {
212
+ // Уровень по головам в сетке = 2 + 1 + 1 = 4 → 140 €, доля 35 €.
213
+ // Студент −5 €, пенсионер −25 % = −8.75 → 9 €, ребёнок бесплатно.
214
+ expect((0, composition_price_1.getCompositionPrice)({
215
+ price: PARIS,
216
+ categories,
217
+ composition: { adult: 2, student: 1, senior: 1, kid: 2 },
218
+ })).toBe(14000 - 500 - 875);
219
+ });
220
+ it('лимит категории режет состав', () => {
221
+ const limited = categories.map((category) => category.key === 'kid' ? { ...category, value: 1000, max: 1 } : category);
222
+ expect((0, composition_price_1.getCompositionPrice)({
223
+ price: PARIS,
224
+ categories: limited,
225
+ composition: { adult: 4, kid: 5 },
226
+ })).toBe(15000);
227
+ });
228
+ });
229
+ describe('совместимость с прежним форматом состава', () => {
230
+ const legacyTariff = {
231
+ price: { ...PARIS, child: 1000 },
232
+ legacy: {
233
+ reducedRateMode: tariff_reduced_rate_mode_enum_1.TariffReducedRateModeEnum.DELTA,
234
+ reducedRate: 500,
235
+ reducedTitle: 'Школьники',
236
+ childPriceDescription: 'До 7 лет',
237
+ },
238
+ minGameAmount: 9800,
239
+ };
240
+ it('тариф без категорий раскладывается на них автоматически', () => {
241
+ const categories = (0, composition_price_1.categoriesFromLegacy)(legacyTariff);
242
+ expect(categories.map((category) => [category.key, category.mode, category.countsInPrice])).toEqual([
243
+ ['adult', tariff_category_mode_enum_1.TariffCategoryModeEnum.BASE, true],
244
+ ['reduced', tariff_category_mode_enum_1.TariffCategoryModeEnum.DELTA, true],
245
+ ['child', tariff_category_mode_enum_1.TariffCategoryModeEnum.FIXED, false],
246
+ ]);
247
+ });
248
+ it('старые поля состава дают ту же цену, что и категории', () => {
249
+ const legacyPrice = (0, get_players_price_1.getPlayersPrice)({
250
+ price: legacyTariff.price,
251
+ players: 4,
252
+ children: 2,
253
+ reducedPlayers: 2,
254
+ reducedRateMode: tariff_reduced_rate_mode_enum_1.TariffReducedRateModeEnum.DELTA,
255
+ reducedRate: 500,
256
+ minGameAmount: 9800,
257
+ });
258
+ expect(legacyPrice).toBe((0, composition_price_1.getCompositionPrice)({
259
+ ...legacyTariff,
260
+ categories: (0, composition_price_1.categoriesFromLegacy)(legacyTariff),
261
+ composition: { adult: 2, reduced: 2, child: 2 },
262
+ }));
263
+ expect(legacyPrice).toBe(14000 - 1000 + 2000);
264
+ });
265
+ it('состав переводится в старые поля и обратно без потерь', () => {
266
+ const categories = (0, composition_price_1.categoriesFromLegacy)(legacyTariff);
267
+ const composition = (0, composition_price_1.compositionFromLegacy)({ players: 4, children: 2, reducedPlayers: 3 }, categories);
268
+ expect(composition).toEqual({ adult: 1, reduced: 3, child: 2 });
269
+ expect((0, composition_price_1.legacyCountsFromComposition)(composition, categories)).toEqual({
270
+ players: 4,
271
+ children: 2,
272
+ reducedPlayers: 3,
273
+ });
274
+ });
275
+ it('несколько льготных категорий схлопываются в одно число льготных мест', () => {
276
+ const categories = [
277
+ adults,
278
+ {
279
+ key: 'student',
280
+ title: '',
281
+ mode: tariff_category_mode_enum_1.TariffCategoryModeEnum.DELTA,
282
+ value: 500,
283
+ countsInPrice: true,
284
+ },
285
+ {
286
+ key: 'senior',
287
+ title: '',
288
+ mode: tariff_category_mode_enum_1.TariffCategoryModeEnum.PERCENT,
289
+ value: 2500,
290
+ countsInPrice: true,
291
+ },
292
+ ];
293
+ expect((0, composition_price_1.legacyCountsFromComposition)({ adult: 1, student: 2, senior: 1 }, categories)).toEqual({ players: 4, children: 0, reducedPlayers: 3 });
294
+ });
295
+ });
296
+ describe('дозапись легаси-строк в уже сохранённые категории', () => {
297
+ const baseOnly = [adults];
298
+ const childLegacy = {
299
+ price: { ...PARIS, child: 1500 },
300
+ legacy: { childPriceDescription: 'До 7 лет' },
301
+ };
302
+ it('в пустой список кладёт полный разбор легаси', () => {
303
+ expect((0, composition_price_1.mergeLegacyCategories)([], childLegacy).map((category) => category.key)).toEqual([
304
+ 'adult',
305
+ 'child',
306
+ ]);
307
+ });
308
+ it('к одной базе дописывает детскую надбавку из price.child', () => {
309
+ expect((0, composition_price_1.mergeLegacyCategories)(baseOnly, childLegacy).map((category) => [
310
+ category.key,
311
+ category.countsInPrice,
312
+ ])).toEqual([
313
+ ['adult', true],
314
+ ['child', false],
315
+ ]);
316
+ });
317
+ it('не дублирует уже перенесённую детскую строку', () => {
318
+ const already = (0, composition_price_1.mergeLegacyCategories)(baseOnly, childLegacy);
319
+ expect((0, composition_price_1.mergeLegacyCategories)(already, childLegacy)).toEqual(already);
320
+ });
321
+ it('не трогает тариф, у которого льготы уже свои, а детской цены нет', () => {
322
+ const withStudents = [
323
+ adults,
324
+ {
325
+ key: 'student',
326
+ title: 'Студенты',
327
+ mode: tariff_category_mode_enum_1.TariffCategoryModeEnum.DELTA,
328
+ value: 500,
329
+ countsInPrice: true,
330
+ },
331
+ ];
332
+ expect((0, composition_price_1.mergeLegacyCategories)(withStudents, {
333
+ price: PARIS,
334
+ legacy: {
335
+ reducedRateMode: tariff_reduced_rate_mode_enum_1.TariffReducedRateModeEnum.DELTA,
336
+ reducedRate: 500,
337
+ },
338
+ })).toEqual(withStudents);
339
+ });
340
+ });
341
+ describe('подпись состава', () => {
342
+ const categories = [
343
+ { ...adults, title: 'Взрослые' },
344
+ {
345
+ key: 'student',
346
+ title: 'Студенты',
347
+ mode: tariff_category_mode_enum_1.TariffCategoryModeEnum.DELTA,
348
+ value: 500,
349
+ countsInPrice: true,
350
+ },
351
+ {
352
+ key: 'school',
353
+ title: 'Школьник',
354
+ mode: tariff_category_mode_enum_1.TariffCategoryModeEnum.FIXED,
355
+ value: 1500,
356
+ countsInPrice: true,
357
+ },
358
+ ];
359
+ it('для тарифа без льготных строк пустая — раскладывать нечего', () => {
360
+ expect((0, composition_price_1.hasExtraCategories)([adults])).toBe(false);
361
+ expect((0, composition_price_1.formatCompositionBreakdown)({ adult: 4 }, [adults])).toBe('');
362
+ });
363
+ it('склеивает ненулевые строки', () => {
364
+ expect((0, composition_price_1.formatCompositionBreakdown)({ adult: 2, student: 3, school: 0 }, categories)).toBe('2 Взрослые, 3 Студенты');
365
+ });
366
+ });
367
+ describe('проекция категорий обратно в легаси-поля', () => {
368
+ const kids = {
369
+ key: 'kid',
370
+ title: 'До 7 лет',
371
+ description: 'Бесплатно',
372
+ mode: tariff_category_mode_enum_1.TariffCategoryModeEnum.FIXED,
373
+ value: 0,
374
+ countsInPrice: false,
375
+ };
376
+ it('надбавочная категория становится детской ценой тарифа', () => {
377
+ expect((0, composition_price_1.legacyFromCategories)([adults, kids], PARIS)).toMatchObject({
378
+ childPrice: 0,
379
+ childPriceDescription: 'Бесплатно',
380
+ reducedRateMode: null,
381
+ reducedRate: 0,
382
+ });
383
+ });
384
+ it('фикс и дельта переносятся один в один', () => {
385
+ const students = {
386
+ key: 'student',
387
+ title: 'Студенты',
388
+ mode: tariff_category_mode_enum_1.TariffCategoryModeEnum.DELTA,
389
+ value: 500,
390
+ countsInPrice: true,
391
+ };
392
+ expect((0, composition_price_1.legacyFromCategories)([adults, students], PARIS)).toMatchObject({
393
+ reducedRateMode: tariff_reduced_rate_mode_enum_1.TariffReducedRateModeEnum.DELTA,
394
+ reducedRate: 500,
395
+ reducedTitle: 'Студенты',
396
+ });
397
+ expect((0, composition_price_1.legacyFromCategories)([adults, { ...students, mode: tariff_category_mode_enum_1.TariffCategoryModeEnum.FIXED, value: 2200 }], PARIS)).toMatchObject({
398
+ reducedRateMode: tariff_reduced_rate_mode_enum_1.TariffReducedRateModeEnum.FIXED,
399
+ reducedRate: 2200,
400
+ });
401
+ });
402
+ it('процент превращается в самую осторожную фиксированную скидку', () => {
403
+ const seniors = {
404
+ key: 'senior',
405
+ title: 'Пенсионеры',
406
+ mode: tariff_category_mode_enum_1.TariffCategoryModeEnum.PERCENT,
407
+ value: 2500,
408
+ countsInPrice: true,
409
+ };
410
+ // Доля гостя минимальна на группе из 12 (26400 / 12 = 2200), значит и
411
+ // 25% от неё — 550 — это наименьшая скидка, которую даёт эта строка.
412
+ // Старый клиент недосчитает скидку, но не продаст игру дешевле нужного.
413
+ const legacy = (0, composition_price_1.legacyFromCategories)([adults, seniors], PARIS);
414
+ expect(legacy).toMatchObject({
415
+ reducedRateMode: tariff_reduced_rate_mode_enum_1.TariffReducedRateModeEnum.DELTA,
416
+ reducedRate: 550,
417
+ });
418
+ const onSmallestGroup = (0, composition_price_1.getCompositionPrice)({
419
+ price: PARIS,
420
+ categories: [adults, seniors],
421
+ composition: { adult: 1, senior: 1 },
422
+ });
423
+ expect(onSmallestGroup).toBeLessThanOrEqual(9800 - legacy.reducedRate);
424
+ });
425
+ it('тариф без льготных строк обнуляет ставку, а не оставляет прежнюю', () => {
426
+ expect((0, composition_price_1.legacyFromCategories)([adults], PARIS)).toEqual({
427
+ childPrice: null,
428
+ childPriceDescription: null,
429
+ reducedRateMode: null,
430
+ reducedRate: 0,
431
+ reducedTitle: null,
432
+ reducedPriceDescription: null,
433
+ });
434
+ });
435
+ });
436
+ describe('цена строки категории', () => {
437
+ it('меняется вместе с составом — доля гостя падает на больших группах', () => {
438
+ const students = {
439
+ key: 'student',
440
+ title: '',
441
+ mode: tariff_category_mode_enum_1.TariffCategoryModeEnum.DELTA,
442
+ value: 500,
443
+ countsInPrice: true,
444
+ };
445
+ expect((0, composition_price_1.getCategorySeatPrice)(adults, (0, composition_price_1.getPerPersonAmount)(PARIS, 2))).toBe(4900);
446
+ expect((0, composition_price_1.getCategorySeatPrice)(adults, (0, composition_price_1.getPerPersonAmount)(PARIS, 6))).toBe(3000);
447
+ expect((0, composition_price_1.getCategorySeatPrice)(students, (0, composition_price_1.getPerPersonAmount)(PARIS, 6))).toBe(2500);
448
+ });
449
+ it('на недоступном уровне цены нет', () => {
450
+ expect((0, composition_price_1.getPerPersonAmount)(PARIS, 20)).toBe(0);
451
+ expect((0, composition_price_1.getCategorySeatPrice)(adults, (0, composition_price_1.getPerPersonAmount)(PARIS, 20))).toBe(0);
452
+ });
453
+ });
@@ -13,3 +13,12 @@ export declare const functionalPopoverPanelStyle: {
13
13
  };
14
14
  export type FunctionalPopoverPanelStyle = typeof functionalPopoverPanelStyle;
15
15
  export declare const functionalPopoverPanel: (extra?: Record<string, string | number>) => Record<string, string | number>;
16
+ /**
17
+ * Пикер состава со степперами льготных строк не влезает в стандартные 300px:
18
+ * название, цена и «+/−» стоят в один ряд.
19
+ */
20
+ export declare const playersPickerPopoverStyle: {
21
+ readonly width: 420;
22
+ readonly maxWidth: "92vw";
23
+ readonly overflow: "auto";
24
+ };
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.functionalPopoverPanel = exports.functionalPopoverPanelStyle = void 0;
3
+ exports.playersPickerPopoverStyle = exports.functionalPopoverPanel = exports.functionalPopoverPanelStyle = void 0;
4
4
  /**
5
5
  * Shared chrome for interactive Popover panels (pickers, calendars, menus).
6
6
  * Use on the inner content wrapper (`ref={popoverRef}`), not on Tooltip.
@@ -19,3 +19,12 @@ const functionalPopoverPanel = (extra) => ({
19
19
  ...extra,
20
20
  });
21
21
  exports.functionalPopoverPanel = functionalPopoverPanel;
22
+ /**
23
+ * Пикер состава со степперами льготных строк не влезает в стандартные 300px:
24
+ * название, цена и «+/−» стоят в один ряд.
25
+ */
26
+ exports.playersPickerPopoverStyle = {
27
+ width: 420,
28
+ maxWidth: '92vw',
29
+ overflow: 'auto',
30
+ };
@@ -1,8 +1,13 @@
1
+ import { OrderComposition, TariffCategory } from '@escapenavigator/types/dist/tariff/tariff-category';
1
2
  import { ReducedPricingConfig } from './reduced-price';
2
3
  /**
3
- * Источник цены игры. Кроме прайс-карты и состава принимает снимок льготной
4
- * ставки и минимальной стоимости — у `OrderEntity` эти поля есть, поэтому
5
- * заказы получают правильную цену без правок на каждом вызове.
4
+ * Источник цены игры. Кроме прайс-карты и состава принимает снимок условий
5
+ * тарифа — у `OrderEntity` эти поля есть, поэтому заказы получают правильную
6
+ * цену без правок на каждом вызове.
7
+ *
8
+ * Состав задаётся либо категориями (`composition` + `categories`), либо
9
+ * полями старого формата. Второе оставлено ради не обновлённых клиентов:
10
+ * внутри всё равно считает один движок — `composition-price`.
6
11
  */
7
12
  export type GamePriceSource = ReducedPricingConfig & {
8
13
  price: Record<string, number>;
@@ -10,6 +15,13 @@ export type GamePriceSource = ReducedPricingConfig & {
10
15
  children: number;
11
16
  /** Сколько мест в составе идёт по льготной ставке (школьники и т.п.). */
12
17
  reducedPlayers?: number | null;
18
+ /** Категории участников тарифа (или их снимок на брони). */
19
+ categories?: TariffCategory[] | null;
20
+ /** Состав по категориям. Приоритетнее полей старого формата. */
21
+ composition?: OrderComposition | null;
22
+ reducedTitle?: string | null;
23
+ reducedPriceDescription?: string | null;
24
+ childPriceDescription?: string | null;
13
25
  };
14
26
  export declare const getPlayersPrice: (order: GamePriceSource) => number;
15
27
  /**
@@ -1,25 +1,34 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.getMaxGameDiscount = exports.getPlayersPrice = void 0;
4
+ const composition_price_1 = require("./composition-price");
4
5
  const reduced_price_1 = require("./reduced-price");
6
+ /** Приводит источник любого поколения к аргументам движка категорий. */
7
+ const toCompositionSource = (order) => {
8
+ const config = {
9
+ price: order.price,
10
+ categories: order.categories,
11
+ minGameAmount: order.minGameAmount,
12
+ legacy: {
13
+ reducedRateMode: order.reducedRateMode,
14
+ reducedRate: order.reducedRate,
15
+ reducedTitle: order.reducedTitle,
16
+ reducedPriceDescription: order.reducedPriceDescription,
17
+ childPriceDescription: order.childPriceDescription,
18
+ },
19
+ };
20
+ const categories = (0, composition_price_1.resolveCategories)(config);
21
+ const hasComposition = !!order.composition && Object.keys(order.composition).length > 0;
22
+ return {
23
+ ...config,
24
+ categories,
25
+ composition: hasComposition ? order.composition : (0, composition_price_1.compositionFromLegacy)(order, categories),
26
+ };
27
+ };
5
28
  const getPlayersPrice = (order) => {
6
29
  if (!order?.price)
7
30
  return 0;
8
- const groupAmount = Number(order.price[order.players]) || 0;
9
- const childAmount = !Number.isNaN(+order.price.child) && order.children
10
- ? order.price.child * order.children
11
- : 0;
12
- // Цены на такой состав нет (или она 0 — «состав недоступен»). Заказ по
13
- // нему не считается, и минимальная стоимость игры к нему не применяется:
14
- // иначе отключённое число игроков внезапно начало бы стоить пол выручки.
15
- if (groupAmount <= 0)
16
- return childAmount;
17
- // Льготные места вычитаются из групповой цены, а не считаются отдельной
18
- // суммой по головам: групповой итог остаётся ровно из прайса, а неровное
19
- // деление (108 / 3) не размазывается копейками.
20
- const seats = Math.min(Math.max(0, Math.floor(Number(order.reducedPlayers) || 0)), order.players);
21
- const reducedDiscount = Math.min(seats * (0, reduced_price_1.getReducedSeatDiscount)(order), groupAmount);
22
- return (0, reduced_price_1.applyMinGameAmount)(groupAmount - reducedDiscount + childAmount, order);
31
+ return (0, composition_price_1.getCompositionPrice)(toCompositionSource(order));
23
32
  };
24
33
  exports.getPlayersPrice = getPlayersPrice;
25
34
  /**
@@ -1,12 +1,17 @@
1
1
  import { PromocodeTypeEnum } from '@escapenavigator/types/dist/promocode/emun/promocode-type.enum';
2
+ import { OrderComposition, TariffCategory } from '@escapenavigator/types/dist/tariff/tariff-category';
2
3
  import { ReducedPricingConfig } from './reduced-price';
3
- export declare const getPromocodeDiscount: ({ slotId, slotDiscount, slotDiscountAmount, total, promocodeType, promocodeDiscount, price, players, children, reducedPlayers, reducedRateMode, reducedRate, minGameAmount, }: ReducedPricingConfig & {
4
+ export declare const getPromocodeDiscount: ({ slotId, slotDiscount, slotDiscountAmount, total, promocodeType, promocodeDiscount, price, players, children, reducedPlayers, composition, categories, reducedRateMode, reducedRate, minGameAmount, }: ReducedPricingConfig & {
4
5
  slotId: number;
5
6
  children?: number;
6
7
  total: number;
7
8
  players: number;
8
9
  /** Сколько мест идёт по льготной ставке (снимок с заказа). */
9
10
  reducedPlayers?: number | null;
11
+ /** Состав по категориям. Приоритетнее полей выше. */
12
+ composition?: OrderComposition | null;
13
+ /** Категории тарифа (или их снимок на брони). */
14
+ categories?: TariffCategory[] | null;
10
15
  /**
11
16
  * Легаси-параметр: часть вызовов передаёт сумму, часть — ставку в bps.
12
17
  * Используется только процентными типами (вычитание из total).
@@ -4,13 +4,15 @@ exports.getPromocodeDiscount = void 0;
4
4
  const promocode_type_enum_1 = require("@escapenavigator/types/dist/promocode/emun/promocode-type.enum");
5
5
  const get_players_price_1 = require("./get-players-price");
6
6
  const reduced_price_1 = require("./reduced-price");
7
- const getPromocodeDiscount = ({ slotId, slotDiscount, slotDiscountAmount = 0, total, promocodeType, promocodeDiscount, price, players, children = 0, reducedPlayers = 0, reducedRateMode = null, reducedRate = 0, minGameAmount = 0, }) => {
7
+ const getPromocodeDiscount = ({ slotId, slotDiscount, slotDiscountAmount = 0, total, promocodeType, promocodeDiscount, price, players, children = 0, reducedPlayers = 0, composition, categories, reducedRateMode = null, reducedRate = 0, minGameAmount = 0, }) => {
8
8
  const orderTotal = slotId ? total - slotDiscount : total;
9
9
  const game = {
10
10
  price,
11
11
  players,
12
12
  children,
13
13
  reducedPlayers,
14
+ composition,
15
+ categories,
14
16
  reducedRateMode,
15
17
  reducedRate,
16
18
  minGameAmount,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@escapenavigator/utils",
3
- "version": "2.0.49",
3
+ "version": "2.1.0",
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.47",
17
+ "@escapenavigator/types": "^2.1.0",
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": "9eaab88a1509930c7715945ce519eaeba879f550"
32
+ "gitHead": "6566478707369811402ca08e2193bf0ead7a1172"
33
33
  }