@escapenavigator/utils 2.0.50 → 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.
@@ -43,12 +43,32 @@ export declare const baseCategory: (categories: TariffCategory[]) => TariffCateg
43
43
  * категория, занимающая место в составе, а детская — надбавка сверх группы.
44
44
  */
45
45
  export declare const categoriesFromLegacy: (config: CompositionPricingConfig) => TariffCategory[];
46
+ /**
47
+ * В тарифе есть строки кроме базовых взрослых: льготные места или надбавка
48
+ * сверх группы. Для такого состава пикер рисует степперы, а в сумме прячет
49
+ * «за человека» — она уже стоит на каждой строке.
50
+ */
51
+ export declare const hasExtraCategories: (categories: TariffCategory[] | null | undefined) => boolean;
52
+ /**
53
+ * Дописывает в уже сохранённые категории те строки, которые ещё живут только
54
+ * в легаси-полях (`price.child`, `reduced*`).
55
+ *
56
+ * Нужно рефилу: тариф, который успели открыть в новом редакторе, получил
57
+ * `[BASE]` и больше не попадает в «пустой список», хотя детская цена на нём
58
+ * ещё есть. Идемпотентен: уже перенесённые детская/льгота не дублируются.
59
+ */
60
+ export declare const mergeLegacyCategories: (existing: TariffCategory[] | null | undefined, config: CompositionPricingConfig) => TariffCategory[];
46
61
  /**
47
62
  * Категории, по которым считать цену: собственные, если тариф уже переведён,
48
63
  * иначе собранные из легаси-полей. Без базовой категории список бессмысленен
49
64
  * (не от чего считать доли) — такой тоже уходит в легаси-ветку.
50
65
  */
51
66
  export declare const resolveCategories: (config: CompositionPricingConfig) => TariffCategory[];
67
+ /**
68
+ * «2 взрослых, 3 студента» — подпись под общим числом человек, когда в тарифе
69
+ * есть льготные или надбавочные строки. Без них раскладывать нечего.
70
+ */
71
+ export declare const formatCompositionBreakdown: (composition: OrderComposition | null | undefined, categories: TariffCategory[] | null | undefined) => string;
52
72
  /** Нормализованный состав: целые неотрицательные числа, лишние ключи выброшены. */
53
73
  export declare const normalizeComposition: (composition: OrderComposition | null | undefined, categories: TariffCategory[]) => OrderComposition;
54
74
  /** Гостей всего: и те, что занимают место в сетке, и надбавочные. */
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.legacyCountsFromComposition = exports.legacyFromCategories = exports.compositionFromLegacy = exports.getMaxDiscountedSeats = exports.getCompositionPrice = exports.getCategorySeatPrice = exports.getCategorySeatDiscount = exports.getPerPersonAmount = exports.getLevelAmount = exports.getPricingHeads = exports.getTotalHeads = exports.normalizeComposition = exports.resolveCategories = exports.categoriesFromLegacy = exports.baseCategory = void 0;
3
+ exports.legacyCountsFromComposition = exports.legacyFromCategories = exports.compositionFromLegacy = exports.getMaxDiscountedSeats = exports.getCompositionPrice = exports.getCategorySeatPrice = exports.getCategorySeatDiscount = exports.getPerPersonAmount = exports.getLevelAmount = exports.getPricingHeads = exports.getTotalHeads = exports.normalizeComposition = exports.formatCompositionBreakdown = exports.resolveCategories = exports.mergeLegacyCategories = exports.hasExtraCategories = exports.categoriesFromLegacy = exports.baseCategory = void 0;
4
4
  const tariff_category_mode_enum_1 = require("@escapenavigator/types/dist/tariff/enum/tariff-category-mode.enum");
5
5
  const tariff_reduced_rate_mode_enum_1 = require("@escapenavigator/types/dist/tariff/enum/tariff-reduced-rate-mode.enum");
6
6
  const tariff_category_1 = require("@escapenavigator/types/dist/tariff/tariff-category");
@@ -63,6 +63,43 @@ const categoriesFromLegacy = (config) => {
63
63
  return categories;
64
64
  };
65
65
  exports.categoriesFromLegacy = categoriesFromLegacy;
66
+ /**
67
+ * В тарифе есть строки кроме базовых взрослых: льготные места или надбавка
68
+ * сверх группы. Для такого состава пикер рисует степперы, а в сумме прячет
69
+ * «за человека» — она уже стоит на каждой строке.
70
+ */
71
+ const hasExtraCategories = (categories) => (categories || []).some((category) => category.mode !== tariff_category_mode_enum_1.TariffCategoryModeEnum.BASE);
72
+ exports.hasExtraCategories = hasExtraCategories;
73
+ /**
74
+ * Дописывает в уже сохранённые категории те строки, которые ещё живут только
75
+ * в легаси-полях (`price.child`, `reduced*`).
76
+ *
77
+ * Нужно рефилу: тариф, который успели открыть в новом редакторе, получил
78
+ * `[BASE]` и больше не попадает в «пустой список», хотя детская цена на нём
79
+ * ещё есть. Идемпотентен: уже перенесённые детская/льгота не дублируются.
80
+ */
81
+ const mergeLegacyCategories = (existing, config) => {
82
+ const fromLegacy = (0, exports.categoriesFromLegacy)(config);
83
+ const own = (existing || []).filter((category) => !!category?.key);
84
+ if (!own.length || !(0, exports.baseCategory)(own))
85
+ return fromLegacy;
86
+ const keys = new Set(own.map((category) => category.key));
87
+ const hasAddOn = own.some((category) => !category.countsInPrice);
88
+ const hasReduced = own.some((category) => category.countsInPrice && category.mode !== tariff_category_mode_enum_1.TariffCategoryModeEnum.BASE);
89
+ const extras = fromLegacy.filter((category) => {
90
+ if (category.mode === tariff_category_mode_enum_1.TariffCategoryModeEnum.BASE)
91
+ return false;
92
+ if (keys.has(category.key))
93
+ return false;
94
+ if (!category.countsInPrice && hasAddOn)
95
+ return false;
96
+ if (category.countsInPrice && hasReduced)
97
+ return false;
98
+ return true;
99
+ });
100
+ return extras.length ? [...own, ...extras] : own;
101
+ };
102
+ exports.mergeLegacyCategories = mergeLegacyCategories;
66
103
  /**
67
104
  * Категории, по которым считать цену: собственные, если тариф уже переведён,
68
105
  * иначе собранные из легаси-полей. Без базовой категории список бессмысленен
@@ -73,6 +110,25 @@ const resolveCategories = (config) => {
73
110
  return own.length && (0, exports.baseCategory)(own) ? own : (0, exports.categoriesFromLegacy)(config);
74
111
  };
75
112
  exports.resolveCategories = resolveCategories;
113
+ /**
114
+ * «2 взрослых, 3 студента» — подпись под общим числом человек, когда в тарифе
115
+ * есть льготные или надбавочные строки. Без них раскладывать нечего.
116
+ */
117
+ const formatCompositionBreakdown = (composition, categories) => {
118
+ if (!(0, exports.hasExtraCategories)(categories))
119
+ return '';
120
+ return (categories || [])
121
+ .map((category) => {
122
+ const count = positiveInt(composition?.[category.key]);
123
+ const title = category.title?.trim();
124
+ if (!count || !title)
125
+ return '';
126
+ return `${count} ${title}`;
127
+ })
128
+ .filter(Boolean)
129
+ .join(', ');
130
+ };
131
+ exports.formatCompositionBreakdown = formatCompositionBreakdown;
76
132
  /** Нормализованный состав: целые неотрицательные числа, лишние ключи выброшены. */
77
133
  const normalizeComposition = (composition, categories) => {
78
134
  const counts = {};
@@ -2,8 +2,8 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  const tariff_category_mode_enum_1 = require("@escapenavigator/types/dist/tariff/enum/tariff-category-mode.enum");
4
4
  const tariff_reduced_rate_mode_enum_1 = require("@escapenavigator/types/dist/tariff/enum/tariff-reduced-rate-mode.enum");
5
- const get_players_price_1 = require("./get-players-price");
6
5
  const composition_price_1 = require("./composition-price");
6
+ const get_players_price_1 = require("./get-players-price");
7
7
  /** Прайс реальной комнаты (Paris / Strange): база 98 € за двоих. */
8
8
  const PARIS = {
9
9
  2: 9800,
@@ -69,7 +69,12 @@ describe('фиксированная скидка с человека', () => {
69
69
  });
70
70
  it('на минимальной группе скидка упирается в пол и выбор не блокирует', () => {
71
71
  expect(price(0, 2)).toBe(9800);
72
- expect((0, composition_price_1.getMaxDiscountedSeats)({ price: PARIS, categories: [adults, students], composition: { adult: 0, student: 2 }, minGameAmount: 9800 }, 'student')).toBe(0);
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);
73
78
  });
74
79
  });
75
80
  describe('фиксированная цена за человека', () => {
@@ -234,8 +239,7 @@ describe('совместимость с прежним форматом сост
234
239
  };
235
240
  it('тариф без категорий раскладывается на них автоматически', () => {
236
241
  const categories = (0, composition_price_1.categoriesFromLegacy)(legacyTariff);
237
- expect(categories.map((category) => [category.key, category.mode, category.countsInPrice]))
238
- .toEqual([
242
+ expect(categories.map((category) => [category.key, category.mode, category.countsInPrice])).toEqual([
239
243
  ['adult', tariff_category_mode_enum_1.TariffCategoryModeEnum.BASE, true],
240
244
  ['reduced', tariff_category_mode_enum_1.TariffCategoryModeEnum.DELTA, true],
241
245
  ['child', tariff_category_mode_enum_1.TariffCategoryModeEnum.FIXED, false],
@@ -271,12 +275,95 @@ describe('совместимость с прежним форматом сост
271
275
  it('несколько льготных категорий схлопываются в одно число льготных мест', () => {
272
276
  const categories = [
273
277
  adults,
274
- { key: 'student', title: '', mode: tariff_category_mode_enum_1.TariffCategoryModeEnum.DELTA, value: 500, countsInPrice: true },
275
- { key: 'senior', title: '', mode: tariff_category_mode_enum_1.TariffCategoryModeEnum.PERCENT, value: 2500, countsInPrice: true },
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
+ },
276
292
  ];
277
293
  expect((0, composition_price_1.legacyCountsFromComposition)({ adult: 1, student: 2, senior: 1 }, categories)).toEqual({ players: 4, children: 0, reducedPlayers: 3 });
278
294
  });
279
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
+ });
280
367
  describe('проекция категорий обратно в легаси-поля', () => {
281
368
  const kids = {
282
369
  key: 'kid',
@@ -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
+ };
@@ -22,9 +22,7 @@ const toCompositionSource = (order) => {
22
22
  return {
23
23
  ...config,
24
24
  categories,
25
- composition: hasComposition
26
- ? order.composition
27
- : (0, composition_price_1.compositionFromLegacy)(order, categories),
25
+ composition: hasComposition ? order.composition : (0, composition_price_1.compositionFromLegacy)(order, categories),
28
26
  };
29
27
  };
30
28
  const getPlayersPrice = (order) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@escapenavigator/utils",
3
- "version": "2.0.50",
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.48",
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": "480174116c4a107d2db7e0a4ae92fd0b4e0c63e5"
32
+ "gitHead": "6566478707369811402ca08e2193bf0ead7a1172"
33
33
  }