@escapenavigator/utils 1.10.134 → 1.10.136

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.
@@ -42,11 +42,11 @@ exports.resolveMinMinutesForBooking = resolveMinMinutesForBooking;
42
42
  const formatBookingLeadTime = (totalMinutes, t) => {
43
43
  const { hours, minutes } = (0, exports.minutesToBookingLeadParts)(totalMinutes);
44
44
  if (hours > 0 && minutes > 0) {
45
- return `${hours} ${t('часов')} ${minutes} ${t('мин')}`;
45
+ return `${hours} ${t('common:часов')} ${minutes} ${t('common:минут')}`;
46
46
  }
47
47
  if (hours > 0) {
48
- return `${hours} ${t('часов')}`;
48
+ return `${hours} ${t('common:часов')}`;
49
49
  }
50
- return `${minutes} ${t('мин')}`;
50
+ return `${minutes} ${t('common:минут')}`;
51
51
  };
52
52
  exports.formatBookingLeadTime = formatBookingLeadTime;
@@ -277,8 +277,7 @@ function walkBlock(node, ctx) {
277
277
  const inline = collectInline(el, [], ctx);
278
278
  if (inline.length === 0)
279
279
  return [];
280
- const hasMeaningful = inline.some((n) => n.type !== 'hardBreak' &&
281
- (n.type !== 'text' || (n.text && n.text.trim() !== '')));
280
+ const hasMeaningful = inline.some((n) => n.type !== 'hardBreak' && (n.type !== 'text' || (n.text && n.text.trim() !== '')));
282
281
  if (!hasMeaningful)
283
282
  return [];
284
283
  return [
@@ -66,7 +66,7 @@ describe('stripHtml', () => {
66
66
  expect((0, strip_html_1.stripHtml)('&lt;tag&gt;')).toBe('<tag>');
67
67
  });
68
68
  it('&quot; / &#39; / &apos; → " и \'', () => {
69
- expect((0, strip_html_1.stripHtml)('&quot;hi&quot; &#39;there&#39; &apos;dude&apos;')).toBe('"hi" \'there\' \'dude\'');
69
+ expect((0, strip_html_1.stripHtml)('&quot;hi&quot; &#39;there&#39; &apos;dude&apos;')).toBe("\"hi\" 'there' 'dude'");
70
70
  });
71
71
  });
72
72
  describe('whitespace', () => {
@@ -13,7 +13,7 @@ const getCurrency = (currency) => {
13
13
  [profile_currency_1.ProfileCurrencyEnum.CZK]: 'Kč', // Чешская крона
14
14
  [profile_currency_1.ProfileCurrencyEnum.PLN]: 'zł', // Польский злотый
15
15
  [profile_currency_1.ProfileCurrencyEnum.HUF]: 'Ft', // Венгерский форинт
16
- [profile_currency_1.ProfileCurrencyEnum.RUB]: '',
16
+ [profile_currency_1.ProfileCurrencyEnum.RUB]: 'руб.',
17
17
  [profile_currency_1.ProfileCurrencyEnum.BGN]: 'лв',
18
18
  [profile_currency_1.ProfileCurrencyEnum.CAD]: 'C$',
19
19
  [profile_currency_1.ProfileCurrencyEnum.AUD]: '$',
@@ -0,0 +1,7 @@
1
+ type TranslateFn = (key: string) => string;
2
+ /**
3
+ * Human-readable flex-slot duration for variation picker tags.
4
+ * Below 60 min: "45 минут"; from 60 min: "1 часов", "1 часов 30 минут".
5
+ */
6
+ export declare const formatFlexDurationLabel: (durationMinutes: number, t: TranslateFn) => string;
7
+ export {};
@@ -0,0 +1,21 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.formatFlexDurationLabel = void 0;
4
+ /**
5
+ * Human-readable flex-slot duration for variation picker tags.
6
+ * Below 60 min: "45 минут"; from 60 min: "1 часов", "1 часов 30 минут".
7
+ */
8
+ const formatFlexDurationLabel = (durationMinutes, t) => {
9
+ const minutesWord = t('common:минут');
10
+ const hoursWord = t('common:часов');
11
+ if (durationMinutes < 60) {
12
+ return `${durationMinutes} ${minutesWord}`;
13
+ }
14
+ const hours = Math.floor(durationMinutes / 60);
15
+ const minutes = durationMinutes % 60;
16
+ if (minutes === 0) {
17
+ return `${hours} ${hoursWord}`;
18
+ }
19
+ return `${hours} ${hoursWord} ${minutes} ${minutesWord}`;
20
+ };
21
+ exports.formatFlexDurationLabel = formatFlexDurationLabel;
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,24 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const index_1 = require("./index");
4
+ const t = (key) => {
5
+ const map = {
6
+ 'common:минут': 'минут',
7
+ 'common:часов': 'часов',
8
+ };
9
+ return map[key] ?? key;
10
+ };
11
+ describe('formatFlexDurationLabel', () => {
12
+ it('formats durations under one hour as minutes only', () => {
13
+ expect((0, index_1.formatFlexDurationLabel)(45, t)).toBe('45 минут');
14
+ expect((0, index_1.formatFlexDurationLabel)(50, t)).toBe('50 минут');
15
+ });
16
+ it('formats whole hours from 60 minutes upward', () => {
17
+ expect((0, index_1.formatFlexDurationLabel)(60, t)).toBe('1 часов');
18
+ expect((0, index_1.formatFlexDurationLabel)(120, t)).toBe('2 часов');
19
+ });
20
+ it('formats hours with remaining minutes', () => {
21
+ expect((0, index_1.formatFlexDurationLabel)(90, t)).toBe('1 часов 30 минут');
22
+ expect((0, index_1.formatFlexDurationLabel)(150, t)).toBe('2 часов 30 минут');
23
+ });
24
+ });
package/dist/index.d.ts CHANGED
@@ -5,6 +5,7 @@ export * from './convert-minutes-to-hhmm';
5
5
  export * from './convert-to-options';
6
6
  export * from './date';
7
7
  export * from './enum-to-options';
8
+ export * from './format-flex-duration-label';
8
9
  export * from './functional-popover-panel-style';
9
10
  export * from './get-documents-links';
10
11
  export * from './get-full-name';
package/dist/index.js CHANGED
@@ -21,6 +21,7 @@ __exportStar(require("./convert-minutes-to-hhmm"), exports);
21
21
  __exportStar(require("./convert-to-options"), exports);
22
22
  __exportStar(require("./date"), exports);
23
23
  __exportStar(require("./enum-to-options"), exports);
24
+ __exportStar(require("./format-flex-duration-label"), exports);
24
25
  __exportStar(require("./functional-popover-panel-style"), exports);
25
26
  __exportStar(require("./get-documents-links"), exports);
26
27
  __exportStar(require("./get-full-name"), exports);
@@ -1,3 +1,4 @@
1
+ import { PromocodeCertificateModeEnum } from '@escapenavigator/types/dist/promocode/emun/promocode-certificate-mode.enum';
1
2
  import { PromocodeNominalRule } from '@escapenavigator/types/dist/promocode/promocode-nominal-rule';
2
3
  type NominalLike = {
3
4
  id: number;
@@ -6,6 +7,11 @@ type NominalLike = {
6
7
  type PromocodeLike = {
7
8
  availableForCertificates?: boolean;
8
9
  certificateNominalRules?: PromocodeNominalRule[];
10
+ /**
11
+ * Режим действия в части сертификатов. Если поле отсутствует —
12
+ * считаем `DISCOUNT` (легаси-семантика).
13
+ */
14
+ certificateMode?: PromocodeCertificateModeEnum | null;
9
15
  };
10
16
  /**
11
17
  * Поиск правила промокода для конкретного номинала. Возвращает запись из
@@ -15,15 +21,34 @@ export declare function findNominalRule(promocode: PromocodeLike | null | undefi
15
21
  /**
16
22
  * Применим ли промокод к данному номиналу: scope охватывает сертификаты и есть
17
23
  * явное правило для `nominal.id`. Если правил нет — промокод к номиналу не
18
- * применяется (магазин показывает базовую скидку номинала).
24
+ * применяется (магазин показывает базовую скидку номинала и не начисляет бонус).
19
25
  */
20
26
  export declare function isPromocodeApplicableToNominal(promocode: PromocodeLike | null | undefined, nominal: NominalLike): boolean;
21
27
  /**
22
- * Эффективная скидка номинала с учётом промокода (если он применим).
28
+ * Режим действия для сертификатов с дефолтом в `DISCOUNT` для старых промокодов
29
+ * без поля `certificateMode`.
30
+ */
31
+ export declare function getCertificateMode(promocode: PromocodeLike | null | undefined): PromocodeCertificateModeEnum;
32
+ /**
33
+ * Эффективная скидка номинала на цену покупки сертификата.
23
34
  * Возвращает значение в минимальных единицах валюты.
24
35
  *
25
- * - Промокод применим к номиналу → скидка из правила.
26
- * - Иначе базовый `nominal.discount` (или 0).
36
+ * - Промокод применим к номиналу и режим = `DISCOUNT` значение из правила.
37
+ * - Режим = `BONUS` или промокод не применим → базовый `nominal.discount` (или 0).
38
+ *
39
+ * Помните: для режима `BONUS` цена не уменьшается, бонус начисляется отдельно
40
+ * (см. `resolveEffectiveNominalBonus`).
27
41
  */
28
42
  export declare function resolveEffectiveNominalDiscount(promocode: PromocodeLike | null | undefined, nominal: NominalLike): number;
43
+ /**
44
+ * Эффективный бонус к балансу сертификата от промокода.
45
+ * Возвращает значение в минимальных единицах валюты.
46
+ *
47
+ * - Промокод применим к номиналу и режим = `BONUS` → значение из правила.
48
+ * - В остальных случаях → 0.
49
+ *
50
+ * Бонус НЕ уменьшает цену покупки — он прибавляется к усваиваемому
51
+ * балансу сертификата (`nominal + bonus - usedAmount`).
52
+ */
53
+ export declare function resolveEffectiveNominalBonus(promocode: PromocodeLike | null | undefined, nominal: NominalLike): number;
29
54
  export {};
@@ -2,7 +2,10 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.findNominalRule = findNominalRule;
4
4
  exports.isPromocodeApplicableToNominal = isPromocodeApplicableToNominal;
5
+ exports.getCertificateMode = getCertificateMode;
5
6
  exports.resolveEffectiveNominalDiscount = resolveEffectiveNominalDiscount;
7
+ exports.resolveEffectiveNominalBonus = resolveEffectiveNominalBonus;
8
+ const promocode_certificate_mode_enum_1 = require("@escapenavigator/types/dist/promocode/emun/promocode-certificate-mode.enum");
6
9
  /**
7
10
  * Поиск правила промокода для конкретного номинала. Возвращает запись из
8
11
  * `certificateNominalRules` или `null`, если правила нет.
@@ -15,7 +18,7 @@ function findNominalRule(promocode, nominalId) {
15
18
  /**
16
19
  * Применим ли промокод к данному номиналу: scope охватывает сертификаты и есть
17
20
  * явное правило для `nominal.id`. Если правил нет — промокод к номиналу не
18
- * применяется (магазин показывает базовую скидку номинала).
21
+ * применяется (магазин показывает базовую скидку номинала и не начисляет бонус).
19
22
  */
20
23
  function isPromocodeApplicableToNominal(promocode, nominal) {
21
24
  if (!promocode?.availableForCertificates)
@@ -23,17 +26,45 @@ function isPromocodeApplicableToNominal(promocode, nominal) {
23
26
  return findNominalRule(promocode, nominal.id) !== null;
24
27
  }
25
28
  /**
26
- * Эффективная скидка номинала с учётом промокода (если он применим).
29
+ * Режим действия для сертификатов с дефолтом в `DISCOUNT` для старых промокодов
30
+ * без поля `certificateMode`.
31
+ */
32
+ function getCertificateMode(promocode) {
33
+ return promocode?.certificateMode ?? promocode_certificate_mode_enum_1.PromocodeCertificateModeEnum.DISCOUNT;
34
+ }
35
+ /**
36
+ * Эффективная скидка номинала на цену покупки сертификата.
27
37
  * Возвращает значение в минимальных единицах валюты.
28
38
  *
29
- * - Промокод применим к номиналу → скидка из правила.
30
- * - Иначе базовый `nominal.discount` (или 0).
39
+ * - Промокод применим к номиналу и режим = `DISCOUNT` значение из правила.
40
+ * - Режим = `BONUS` или промокод не применим → базовый `nominal.discount` (или 0).
41
+ *
42
+ * Помните: для режима `BONUS` цена не уменьшается, бонус начисляется отдельно
43
+ * (см. `resolveEffectiveNominalBonus`).
31
44
  */
32
45
  function resolveEffectiveNominalDiscount(promocode, nominal) {
33
- const rule = isPromocodeApplicableToNominal(promocode, nominal)
34
- ? findNominalRule(promocode, nominal.id)
35
- : null;
46
+ const applicable = isPromocodeApplicableToNominal(promocode, nominal);
47
+ const isDiscountMode = getCertificateMode(promocode) === promocode_certificate_mode_enum_1.PromocodeCertificateModeEnum.DISCOUNT;
48
+ const rule = applicable && isDiscountMode ? findNominalRule(promocode, nominal.id) : null;
36
49
  if (rule)
37
50
  return Math.max(0, rule.discount || 0);
38
51
  return Math.max(0, nominal.discount || 0);
39
52
  }
53
+ /**
54
+ * Эффективный бонус к балансу сертификата от промокода.
55
+ * Возвращает значение в минимальных единицах валюты.
56
+ *
57
+ * - Промокод применим к номиналу и режим = `BONUS` → значение из правила.
58
+ * - В остальных случаях → 0.
59
+ *
60
+ * Бонус НЕ уменьшает цену покупки — он прибавляется к усваиваемому
61
+ * балансу сертификата (`nominal + bonus - usedAmount`).
62
+ */
63
+ function resolveEffectiveNominalBonus(promocode, nominal) {
64
+ if (!isPromocodeApplicableToNominal(promocode, nominal))
65
+ return 0;
66
+ if (getCertificateMode(promocode) !== promocode_certificate_mode_enum_1.PromocodeCertificateModeEnum.BONUS)
67
+ return 0;
68
+ const rule = findNominalRule(promocode, nominal.id);
69
+ return Math.max(0, rule?.discount || 0);
70
+ }
@@ -1,9 +1,23 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ const promocode_certificate_mode_enum_1 = require("@escapenavigator/types/dist/promocode/emun/promocode-certificate-mode.enum");
3
4
  const promocode_nominal_rules_1 = require("./promocode-nominal-rules");
4
5
  const baseNominal = { id: 10, discount: 500 };
5
- const promocodeWithRule = {
6
+ const promocodeDiscountWithRule = {
6
7
  availableForCertificates: true,
8
+ certificateMode: promocode_certificate_mode_enum_1.PromocodeCertificateModeEnum.DISCOUNT,
9
+ certificateNominalRules: [
10
+ { nominalId: 10, discount: 1500 },
11
+ { nominalId: 20, discount: 700 },
12
+ ],
13
+ };
14
+ const promocodeLegacyNoMode = {
15
+ availableForCertificates: true,
16
+ certificateNominalRules: [{ nominalId: 10, discount: 1500 }],
17
+ };
18
+ const promocodeBonusWithRule = {
19
+ availableForCertificates: true,
20
+ certificateMode: promocode_certificate_mode_enum_1.PromocodeCertificateModeEnum.BONUS,
7
21
  certificateNominalRules: [
8
22
  { nominalId: 10, discount: 1500 },
9
23
  { nominalId: 20, discount: 700 },
@@ -11,21 +25,23 @@ const promocodeWithRule = {
11
25
  };
12
26
  const promocodeWithoutRule = {
13
27
  availableForCertificates: true,
28
+ certificateMode: promocode_certificate_mode_enum_1.PromocodeCertificateModeEnum.DISCOUNT,
14
29
  certificateNominalRules: [{ nominalId: 99, discount: 1500 }],
15
30
  };
16
31
  const promocodeBookingsOnly = {
17
32
  availableForCertificates: false,
33
+ certificateMode: promocode_certificate_mode_enum_1.PromocodeCertificateModeEnum.DISCOUNT,
18
34
  certificateNominalRules: [{ nominalId: 10, discount: 1500 }],
19
35
  };
20
36
  describe('findNominalRule', () => {
21
37
  it('returns rule when nominal matches', () => {
22
- expect((0, promocode_nominal_rules_1.findNominalRule)(promocodeWithRule, 10)).toEqual({
38
+ expect((0, promocode_nominal_rules_1.findNominalRule)(promocodeDiscountWithRule, 10)).toEqual({
23
39
  nominalId: 10,
24
40
  discount: 1500,
25
41
  });
26
42
  });
27
43
  it('returns null when nominal not in rules', () => {
28
- expect((0, promocode_nominal_rules_1.findNominalRule)(promocodeWithRule, 999)).toBeNull();
44
+ expect((0, promocode_nominal_rules_1.findNominalRule)(promocodeDiscountWithRule, 999)).toBeNull();
29
45
  });
30
46
  it('returns null on empty/missing data', () => {
31
47
  expect((0, promocode_nominal_rules_1.findNominalRule)(null, 10)).toBeNull();
@@ -36,7 +52,10 @@ describe('findNominalRule', () => {
36
52
  });
37
53
  describe('isPromocodeApplicableToNominal', () => {
38
54
  it('true only when scope=certificates and rule exists', () => {
39
- expect((0, promocode_nominal_rules_1.isPromocodeApplicableToNominal)(promocodeWithRule, baseNominal)).toBe(true);
55
+ expect((0, promocode_nominal_rules_1.isPromocodeApplicableToNominal)(promocodeDiscountWithRule, baseNominal)).toBe(true);
56
+ });
57
+ it('true for bonus-mode promocodes when scope=certificates and rule exists', () => {
58
+ expect((0, promocode_nominal_rules_1.isPromocodeApplicableToNominal)(promocodeBonusWithRule, baseNominal)).toBe(true);
40
59
  });
41
60
  it('false when no rule for nominal', () => {
42
61
  expect((0, promocode_nominal_rules_1.isPromocodeApplicableToNominal)(promocodeWithoutRule, baseNominal)).toBe(false);
@@ -49,8 +68,14 @@ describe('isPromocodeApplicableToNominal', () => {
49
68
  });
50
69
  });
51
70
  describe('resolveEffectiveNominalDiscount', () => {
52
- it('returns rule discount when applicable', () => {
53
- expect((0, promocode_nominal_rules_1.resolveEffectiveNominalDiscount)(promocodeWithRule, baseNominal)).toBe(1500);
71
+ it('returns rule discount when applicable and mode=discount', () => {
72
+ expect((0, promocode_nominal_rules_1.resolveEffectiveNominalDiscount)(promocodeDiscountWithRule, baseNominal)).toBe(1500);
73
+ });
74
+ it('returns rule discount for legacy promocode without certificateMode (defaults to DISCOUNT)', () => {
75
+ expect((0, promocode_nominal_rules_1.resolveEffectiveNominalDiscount)(promocodeLegacyNoMode, baseNominal)).toBe(1500);
76
+ });
77
+ it('returns base discount (not rule) when mode=bonus', () => {
78
+ expect((0, promocode_nominal_rules_1.resolveEffectiveNominalDiscount)(promocodeBonusWithRule, baseNominal)).toBe(500);
54
79
  });
55
80
  it('returns base nominal discount when no rule', () => {
56
81
  expect((0, promocode_nominal_rules_1.resolveEffectiveNominalDiscount)(promocodeWithoutRule, baseNominal)).toBe(500);
@@ -62,6 +87,7 @@ describe('resolveEffectiveNominalDiscount', () => {
62
87
  it('clamps negative rule discount to 0', () => {
63
88
  expect((0, promocode_nominal_rules_1.resolveEffectiveNominalDiscount)({
64
89
  availableForCertificates: true,
90
+ certificateMode: promocode_certificate_mode_enum_1.PromocodeCertificateModeEnum.DISCOUNT,
65
91
  certificateNominalRules: [{ nominalId: 10, discount: -50 }],
66
92
  }, baseNominal)).toBe(0);
67
93
  });
@@ -72,3 +98,39 @@ describe('resolveEffectiveNominalDiscount', () => {
72
98
  expect((0, promocode_nominal_rules_1.resolveEffectiveNominalDiscount)(null, { id: 10 })).toBe(0);
73
99
  });
74
100
  });
101
+ describe('resolveEffectiveNominalBonus', () => {
102
+ it('returns rule value when applicable and mode=bonus', () => {
103
+ expect((0, promocode_nominal_rules_1.resolveEffectiveNominalBonus)(promocodeBonusWithRule, baseNominal)).toBe(1500);
104
+ });
105
+ it('returns 0 when mode=discount', () => {
106
+ expect((0, promocode_nominal_rules_1.resolveEffectiveNominalBonus)(promocodeDiscountWithRule, baseNominal)).toBe(0);
107
+ });
108
+ it('returns 0 for legacy promocode without certificateMode (defaults to DISCOUNT)', () => {
109
+ expect((0, promocode_nominal_rules_1.resolveEffectiveNominalBonus)(promocodeLegacyNoMode, baseNominal)).toBe(0);
110
+ });
111
+ it('returns 0 when no rule for nominal', () => {
112
+ expect((0, promocode_nominal_rules_1.resolveEffectiveNominalBonus)({
113
+ availableForCertificates: true,
114
+ certificateMode: promocode_certificate_mode_enum_1.PromocodeCertificateModeEnum.BONUS,
115
+ certificateNominalRules: [{ nominalId: 99, discount: 1500 }],
116
+ }, baseNominal)).toBe(0);
117
+ });
118
+ it('returns 0 when scope excludes certificates', () => {
119
+ expect((0, promocode_nominal_rules_1.resolveEffectiveNominalBonus)({
120
+ availableForCertificates: false,
121
+ certificateMode: promocode_certificate_mode_enum_1.PromocodeCertificateModeEnum.BONUS,
122
+ certificateNominalRules: [{ nominalId: 10, discount: 1500 }],
123
+ }, baseNominal)).toBe(0);
124
+ });
125
+ it('returns 0 on missing promocode', () => {
126
+ expect((0, promocode_nominal_rules_1.resolveEffectiveNominalBonus)(null, baseNominal)).toBe(0);
127
+ expect((0, promocode_nominal_rules_1.resolveEffectiveNominalBonus)(undefined, baseNominal)).toBe(0);
128
+ });
129
+ it('clamps negative rule value to 0', () => {
130
+ expect((0, promocode_nominal_rules_1.resolveEffectiveNominalBonus)({
131
+ availableForCertificates: true,
132
+ certificateMode: promocode_certificate_mode_enum_1.PromocodeCertificateModeEnum.BONUS,
133
+ certificateNominalRules: [{ nominalId: 10, discount: -100 }],
134
+ }, baseNominal)).toBe(0);
135
+ });
136
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@escapenavigator/utils",
3
- "version": "1.10.134",
3
+ "version": "1.10.136",
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": "^1.10.130",
17
+ "@escapenavigator/types": "^1.10.132",
18
18
  "axios": "^0.21.4",
19
19
  "class-transformer": "^0.5.1",
20
20
  "class-validator": "^0.13.2",
@@ -28,5 +28,5 @@
28
28
  "ts-jest": "^29.1.1",
29
29
  "typescript": "^5.6"
30
30
  },
31
- "gitHead": "1b34f498be3d6fb27dccdd2a3663cef5169bbc91"
31
+ "gitHead": "e27b2a8ad0dcf2e7eda752a57a671cb805c0bc1e"
32
32
  }