@escapenavigator/utils 2.0.2 → 2.0.4
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/booking-page-url/index.d.ts +49 -0
- package/dist/booking-page-url/index.js +94 -0
- package/dist/booking-page-url/index.test.d.ts +1 -0
- package/dist/booking-page-url/index.test.js +35 -0
- package/dist/format-promocode-discount/index.d.ts +7 -0
- package/dist/format-promocode-discount/index.js +20 -0
- package/dist/get-order-cancelation-state/index.d.ts +9 -1
- package/dist/get-order-cancelation-state/index.js +6 -4
- package/dist/get-order-cancelation-state/index.test.js +92 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/mask-contacts.d.ts +15 -0
- package/dist/mask-contacts.js +46 -0
- package/dist/role-contact-permissions.d.ts +7 -0
- package/dist/role-contact-permissions.js +9 -0
- package/dist/sanitize-html.d.ts +11 -0
- package/dist/sanitize-html.js +44 -0
- package/dist/utm-touchpoints.d.ts +9 -0
- package/package.json +4 -3
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Утилиты для standalone booking-страницы (orders-домен) и кнопки
|
|
3
|
+
* «Записаться», которая ведёт на неё с сайта клиента.
|
|
4
|
+
*
|
|
5
|
+
* Задача: сохранить UTM, click-id и (опционально) consent при переходе
|
|
6
|
+
* с клиентского сайта на orders.escapenavigator.com/booking/:uuid.
|
|
7
|
+
*/
|
|
8
|
+
export declare const BOOKING_TRACKING_QUERY_PARAMS: readonly ["utm_source", "utm_medium", "utm_campaign", "utm_term", "utm_content", "en_campaign_id", "en_link_id", "gclid", "fbclid", "ttclid", "msclkid"];
|
|
9
|
+
export type BookingTrackingQueryParam = (typeof BOOKING_TRACKING_QUERY_PARAMS)[number];
|
|
10
|
+
export type BookingTrackingParams = Partial<Record<BookingTrackingQueryParam, string>>;
|
|
11
|
+
export type BookingConsentParams = {
|
|
12
|
+
analytics?: boolean;
|
|
13
|
+
ads?: boolean;
|
|
14
|
+
};
|
|
15
|
+
export type BuildBookingPageUrlOptions = {
|
|
16
|
+
ordersDomain: string;
|
|
17
|
+
widgetUuid: string;
|
|
18
|
+
testMode?: boolean;
|
|
19
|
+
lang?: string;
|
|
20
|
+
tracking?: BookingTrackingParams;
|
|
21
|
+
consent?: BookingConsentParams;
|
|
22
|
+
};
|
|
23
|
+
/**
|
|
24
|
+
* Собирает tracking-параметры из URLSearchParams (или строки query).
|
|
25
|
+
* Пустые значения отбрасываются.
|
|
26
|
+
*/
|
|
27
|
+
export declare function collectTrackingParamsFromSearch(search: string | URLSearchParams): BookingTrackingParams;
|
|
28
|
+
/**
|
|
29
|
+
* Добавляет tracking/consent-параметры к URL. Существующие ключи в baseUrl
|
|
30
|
+
* не перезаписываются (приоритет у уже заданных в ссылке).
|
|
31
|
+
*/
|
|
32
|
+
export declare function appendBookingParamsToUrl(baseUrl: string, options?: {
|
|
33
|
+
tracking?: BookingTrackingParams;
|
|
34
|
+
consent?: BookingConsentParams;
|
|
35
|
+
}): string;
|
|
36
|
+
/**
|
|
37
|
+
* Базовый URL standalone booking-страницы:
|
|
38
|
+
* `{ordersDomain}/booking/{widgetUuid}` + testMode/lang/tracking/consent.
|
|
39
|
+
*/
|
|
40
|
+
export declare function buildBookingPageUrl(options: BuildBookingPageUrlOptions): string;
|
|
41
|
+
/**
|
|
42
|
+
* HTML onclick для кнопки «Записаться»: открывает booking-страницу через
|
|
43
|
+
* `EscapeNavigator.openBookingPage` (декорирует UTM + GA linker).
|
|
44
|
+
*/
|
|
45
|
+
export declare function buildBookingButtonOnclick(baseUrl: string): string;
|
|
46
|
+
/**
|
|
47
|
+
* Snippet analytics-bridge для `<head>` сайта клиента (iframe/Wix).
|
|
48
|
+
*/
|
|
49
|
+
export declare function buildAnalyticsBridgeScriptTag(widgetDomain: string): string;
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Утилиты для standalone booking-страницы (orders-домен) и кнопки
|
|
4
|
+
* «Записаться», которая ведёт на неё с сайта клиента.
|
|
5
|
+
*
|
|
6
|
+
* Задача: сохранить UTM, click-id и (опционально) consent при переходе
|
|
7
|
+
* с клиентского сайта на orders.escapenavigator.com/booking/:uuid.
|
|
8
|
+
*/
|
|
9
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
10
|
+
exports.BOOKING_TRACKING_QUERY_PARAMS = void 0;
|
|
11
|
+
exports.collectTrackingParamsFromSearch = collectTrackingParamsFromSearch;
|
|
12
|
+
exports.appendBookingParamsToUrl = appendBookingParamsToUrl;
|
|
13
|
+
exports.buildBookingPageUrl = buildBookingPageUrl;
|
|
14
|
+
exports.buildBookingButtonOnclick = buildBookingButtonOnclick;
|
|
15
|
+
exports.buildAnalyticsBridgeScriptTag = buildAnalyticsBridgeScriptTag;
|
|
16
|
+
exports.BOOKING_TRACKING_QUERY_PARAMS = [
|
|
17
|
+
'utm_source',
|
|
18
|
+
'utm_medium',
|
|
19
|
+
'utm_campaign',
|
|
20
|
+
'utm_term',
|
|
21
|
+
'utm_content',
|
|
22
|
+
'en_campaign_id',
|
|
23
|
+
'en_link_id',
|
|
24
|
+
'gclid',
|
|
25
|
+
'fbclid',
|
|
26
|
+
'ttclid',
|
|
27
|
+
'msclkid',
|
|
28
|
+
];
|
|
29
|
+
const normalizeOrdersDomain = (ordersDomain) => ordersDomain.replace(/\/+$/, '');
|
|
30
|
+
/**
|
|
31
|
+
* Собирает tracking-параметры из URLSearchParams (или строки query).
|
|
32
|
+
* Пустые значения отбрасываются.
|
|
33
|
+
*/
|
|
34
|
+
function collectTrackingParamsFromSearch(search) {
|
|
35
|
+
const params = typeof search === 'string' ? new URLSearchParams(search) : search;
|
|
36
|
+
const out = {};
|
|
37
|
+
for (const key of exports.BOOKING_TRACKING_QUERY_PARAMS) {
|
|
38
|
+
const value = params.get(key)?.trim();
|
|
39
|
+
if (value)
|
|
40
|
+
out[key] = value;
|
|
41
|
+
}
|
|
42
|
+
return out;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Добавляет tracking/consent-параметры к URL. Существующие ключи в baseUrl
|
|
46
|
+
* не перезаписываются (приоритет у уже заданных в ссылке).
|
|
47
|
+
*/
|
|
48
|
+
function appendBookingParamsToUrl(baseUrl, options) {
|
|
49
|
+
const url = new URL(baseUrl);
|
|
50
|
+
if (options?.tracking) {
|
|
51
|
+
for (const [key, value] of Object.entries(options.tracking)) {
|
|
52
|
+
if (!value || url.searchParams.has(key))
|
|
53
|
+
continue;
|
|
54
|
+
url.searchParams.set(key, value);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
if (options?.consent?.analytics && !url.searchParams.has('en_consent_analytics')) {
|
|
58
|
+
url.searchParams.set('en_consent_analytics', '1');
|
|
59
|
+
}
|
|
60
|
+
if (options?.consent?.ads && !url.searchParams.has('en_consent_ads')) {
|
|
61
|
+
url.searchParams.set('en_consent_ads', '1');
|
|
62
|
+
}
|
|
63
|
+
return url.toString();
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Базовый URL standalone booking-страницы:
|
|
67
|
+
* `{ordersDomain}/booking/{widgetUuid}` + testMode/lang/tracking/consent.
|
|
68
|
+
*/
|
|
69
|
+
function buildBookingPageUrl(options) {
|
|
70
|
+
const { ordersDomain, widgetUuid, testMode, lang, tracking, consent } = options;
|
|
71
|
+
const base = `${normalizeOrdersDomain(ordersDomain)}/booking/${encodeURIComponent(widgetUuid)}`;
|
|
72
|
+
const url = new URL(base);
|
|
73
|
+
if (testMode) {
|
|
74
|
+
url.searchParams.set('testMode', 'true');
|
|
75
|
+
}
|
|
76
|
+
if (lang) {
|
|
77
|
+
url.searchParams.set('lang', lang);
|
|
78
|
+
}
|
|
79
|
+
return appendBookingParamsToUrl(url.toString(), { tracking, consent });
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* HTML onclick для кнопки «Записаться»: открывает booking-страницу через
|
|
83
|
+
* `EscapeNavigator.openBookingPage` (декорирует UTM + GA linker).
|
|
84
|
+
*/
|
|
85
|
+
function buildBookingButtonOnclick(baseUrl) {
|
|
86
|
+
return `event.preventDefault();window.EscapeNavigator&&window.EscapeNavigator.openBookingPage?window.EscapeNavigator.openBookingPage('${baseUrl.replace(/'/g, "\\'")}'):window.open('${baseUrl.replace(/'/g, "\\'")}','_blank','noopener');return false;`;
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Snippet analytics-bridge для `<head>` сайта клиента (iframe/Wix).
|
|
90
|
+
*/
|
|
91
|
+
function buildAnalyticsBridgeScriptTag(widgetDomain) {
|
|
92
|
+
const src = `${widgetDomain.replace(/\/+$/, '')}/analytics-bridge.js`;
|
|
93
|
+
return `<script src="${src}" async></script>`;
|
|
94
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const index_1 = require("./index");
|
|
4
|
+
describe('collectTrackingParamsFromSearch', () => {
|
|
5
|
+
it('collects UTM and click-id params', () => {
|
|
6
|
+
const params = (0, index_1.collectTrackingParamsFromSearch)('?utm_source=google&gclid=abc123&foo=bar');
|
|
7
|
+
expect(params).toEqual({
|
|
8
|
+
utm_source: 'google',
|
|
9
|
+
gclid: 'abc123',
|
|
10
|
+
});
|
|
11
|
+
});
|
|
12
|
+
});
|
|
13
|
+
describe('appendBookingParamsToUrl', () => {
|
|
14
|
+
it('does not overwrite existing query keys', () => {
|
|
15
|
+
const url = (0, index_1.appendBookingParamsToUrl)('https://orders.example.com/booking/u1?utm_source=keep', {
|
|
16
|
+
tracking: { utm_source: 'new', gclid: 'x' },
|
|
17
|
+
consent: { analytics: true },
|
|
18
|
+
});
|
|
19
|
+
expect(url).toContain('utm_source=keep');
|
|
20
|
+
expect(url).toContain('gclid=x');
|
|
21
|
+
expect(url).toContain('en_consent_analytics=1');
|
|
22
|
+
});
|
|
23
|
+
});
|
|
24
|
+
describe('buildBookingPageUrl', () => {
|
|
25
|
+
it('builds booking URL with test mode and lang', () => {
|
|
26
|
+
const url = (0, index_1.buildBookingPageUrl)({
|
|
27
|
+
ordersDomain: 'https://orders.escapenavigator.com/',
|
|
28
|
+
widgetUuid: 'W123',
|
|
29
|
+
testMode: true,
|
|
30
|
+
lang: 'de',
|
|
31
|
+
tracking: { utm_medium: 'email' },
|
|
32
|
+
});
|
|
33
|
+
expect(url).toBe('https://orders.escapenavigator.com/booking/W123?testMode=true&lang=de&utm_medium=email');
|
|
34
|
+
});
|
|
35
|
+
});
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { PromocodeTypeEnum } from '@escapenavigator/types/dist/promocode/emun/promocode-type.enum';
|
|
2
|
+
/**
|
|
3
|
+
* Format promocode discount for display.
|
|
4
|
+
* Fixed amounts are stored in minor units (cents); percent values are stored
|
|
5
|
+
* as hundredths of a percent (1500 → 15%).
|
|
6
|
+
*/
|
|
7
|
+
export declare const formatPromocodeDiscount: (type: PromocodeTypeEnum, discount: number, currency: string, clientDescription?: string | null) => string;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.formatPromocodeDiscount = void 0;
|
|
4
|
+
const promocode_type_enum_1 = require("@escapenavigator/types/dist/promocode/emun/promocode-type.enum");
|
|
5
|
+
const format_amount_1 = require("../format-amount");
|
|
6
|
+
/**
|
|
7
|
+
* Format promocode discount for display.
|
|
8
|
+
* Fixed amounts are stored in minor units (cents); percent values are stored
|
|
9
|
+
* as hundredths of a percent (1500 → 15%).
|
|
10
|
+
*/
|
|
11
|
+
const formatPromocodeDiscount = (type, discount, currency, clientDescription) => {
|
|
12
|
+
if (type === promocode_type_enum_1.PromocodeTypeEnum.PROMOTIONAL) {
|
|
13
|
+
return clientDescription?.trim() || '—';
|
|
14
|
+
}
|
|
15
|
+
if (type === promocode_type_enum_1.PromocodeTypeEnum.FIXED) {
|
|
16
|
+
return (0, format_amount_1.formatAmount)(discount, currency);
|
|
17
|
+
}
|
|
18
|
+
return `${discount / 100}%`;
|
|
19
|
+
};
|
|
20
|
+
exports.formatPromocodeDiscount = formatPromocodeDiscount;
|
|
@@ -22,6 +22,14 @@ type Props = {
|
|
|
22
22
|
payed: number;
|
|
23
23
|
minimalPrice: number;
|
|
24
24
|
slotDiscount: number;
|
|
25
|
+
/**
|
|
26
|
+
* Суммарная стоимость невозвратных допуслуг заказа (напр. страховка от
|
|
27
|
+
* отмены). Удерживается венью независимо от штрафа и таймингов отмены,
|
|
28
|
+
* поэтому вычитается из суммы к возврату (с полом 0). Неоплаченная часть
|
|
29
|
+
* остаётся долгом клиента и учитывается в total заказа (см. updatePrice),
|
|
30
|
+
* поэтому здесь в chargeAmount не добавляется.
|
|
31
|
+
*/
|
|
32
|
+
nonRefundableAmount?: number;
|
|
25
33
|
};
|
|
26
34
|
export declare enum FineDescriptions {
|
|
27
35
|
FINE_TON_SETUP = "descriptionFineNotSetup",
|
|
@@ -35,7 +43,7 @@ export declare const isCancelationAllowed: ({ date, minHoursForFreeCanceling, }:
|
|
|
35
43
|
date: string;
|
|
36
44
|
minHoursForFreeCanceling: number;
|
|
37
45
|
}) => boolean;
|
|
38
|
-
export declare function getOrderCancelationState({ cancelationReason, cancelationRule, cancelationAmount, transactions, date, returnCertificates, forceFine, certificates, minHoursForFreeCanceling, minHoursForFullFine, playersPrice, slotDiscount, payed, minimalPrice, }: Props): {
|
|
46
|
+
export declare function getOrderCancelationState({ cancelationReason, cancelationRule, cancelationAmount, transactions, date, returnCertificates, forceFine, certificates, minHoursForFreeCanceling, minHoursForFullFine, playersPrice, slotDiscount, payed, minimalPrice, nonRefundableAmount, }: Props): {
|
|
39
47
|
fineAmount: number;
|
|
40
48
|
returnAmount: number;
|
|
41
49
|
allowedCancelation: boolean;
|
|
@@ -22,7 +22,9 @@ var FineDescriptions;
|
|
|
22
22
|
})(FineDescriptions || (exports.FineDescriptions = FineDescriptions = {}));
|
|
23
23
|
const isCancelationAllowed = ({ date, minHoursForFreeCanceling = 0, }) => (0, date_1.differenceInHours)(new Date(date), new Date()) >= minHoursForFreeCanceling;
|
|
24
24
|
exports.isCancelationAllowed = isCancelationAllowed;
|
|
25
|
-
function getOrderCancelationState({ cancelationReason, cancelationRule, cancelationAmount, transactions, date, returnCertificates, forceFine, certificates, minHoursForFreeCanceling, minHoursForFullFine, playersPrice, slotDiscount = 0, payed, minimalPrice, }) {
|
|
25
|
+
function getOrderCancelationState({ cancelationReason, cancelationRule, cancelationAmount, transactions, date, returnCertificates, forceFine, certificates, minHoursForFreeCanceling, minHoursForFullFine, playersPrice, slotDiscount = 0, payed, minimalPrice, nonRefundableAmount = 0, }) {
|
|
26
|
+
// Удерживаем стоимость невозвратных услуг из суммы к возврату (пол 0).
|
|
27
|
+
const withholdNonRefundable = (amount) => Math.max(0, +(amount - nonRefundableAmount).toFixed(0));
|
|
26
28
|
const halfFineCancelation = cancelationRule !== questroom_cancelation_type_enum_1.QuestroomCancelationTypeEnum.NO &&
|
|
27
29
|
cancelationReason !== order_cancel_reson_enum_1.OrderCancelReasonEnum.QUESTROOM &&
|
|
28
30
|
!(0, exports.isCancelationAllowed)({
|
|
@@ -69,7 +71,7 @@ function getOrderCancelationState({ cancelationReason, cancelationRule, cancelat
|
|
|
69
71
|
return {
|
|
70
72
|
fineAmount: 0,
|
|
71
73
|
chargeAmount: 0,
|
|
72
|
-
returnAmount: receivedTransactionsAmount,
|
|
74
|
+
returnAmount: withholdNonRefundable(receivedTransactionsAmount),
|
|
73
75
|
allowedCancelation,
|
|
74
76
|
type: fineAmount ? FineDescriptions.NO_FINE : FineDescriptions.FINE_TON_SETUP,
|
|
75
77
|
};
|
|
@@ -78,7 +80,7 @@ function getOrderCancelationState({ cancelationReason, cancelationRule, cancelat
|
|
|
78
80
|
return {
|
|
79
81
|
fineAmount: 0,
|
|
80
82
|
chargeAmount: 0,
|
|
81
|
-
returnAmount: receivedTransactionsAmount,
|
|
83
|
+
returnAmount: withholdNonRefundable(receivedTransactionsAmount),
|
|
82
84
|
allowedCancelation,
|
|
83
85
|
type: autoReturtAvailable ? FineDescriptions.AUTO_RETURN : FineDescriptions.HAND_RETURN,
|
|
84
86
|
};
|
|
@@ -96,7 +98,7 @@ function getOrderCancelationState({ cancelationReason, cancelationRule, cancelat
|
|
|
96
98
|
};
|
|
97
99
|
}
|
|
98
100
|
if (chargeAmount < 0) {
|
|
99
|
-
const returnAmount = +(chargeAmount * -1).toFixed(0);
|
|
101
|
+
const returnAmount = withholdNonRefundable(+(chargeAmount * -1).toFixed(0));
|
|
100
102
|
const allowAutoReturn = !slotDiscount && autoReturtAvailable && receivedTransactionsAmount >= returnAmount;
|
|
101
103
|
return {
|
|
102
104
|
type: allowAutoReturn ? FineDescriptions.AUTO_RETURN : FineDescriptions.HAND_RETURN,
|
|
@@ -253,4 +253,96 @@ describe('getOrderCancelationState function', () => {
|
|
|
253
253
|
allowedCancelation: false,
|
|
254
254
|
});
|
|
255
255
|
});
|
|
256
|
+
test('should withhold non-refundable amount from return on in-time cancelation', () => {
|
|
257
|
+
const result = (0, _1.getOrderCancelationState)({
|
|
258
|
+
cancelationReason: order_cancel_reson_enum_1.OrderCancelReasonEnum.CLIENT,
|
|
259
|
+
slotDiscount: 0,
|
|
260
|
+
cancelationAmount: 0,
|
|
261
|
+
minHoursForFullFine: 0,
|
|
262
|
+
cancelationRule: questroom_cancelation_type_enum_1.QuestroomCancelationTypeEnum.FULL_PRICE,
|
|
263
|
+
transactions: [
|
|
264
|
+
{
|
|
265
|
+
amount: 120,
|
|
266
|
+
type: transaction_type_enum_1.TransactionTypeEnum.INSIDE,
|
|
267
|
+
},
|
|
268
|
+
],
|
|
269
|
+
date: (0, date_1.addHours)(new Date(), 50).toISOString(),
|
|
270
|
+
returnCertificates: true,
|
|
271
|
+
certificates: [],
|
|
272
|
+
minHoursForFreeCanceling: 48,
|
|
273
|
+
playersPrice: 100,
|
|
274
|
+
payed: 120,
|
|
275
|
+
minimalPrice: 50,
|
|
276
|
+
nonRefundableAmount: 20,
|
|
277
|
+
});
|
|
278
|
+
expect(result).toEqual({
|
|
279
|
+
type: 'descriptionFineWithReturn',
|
|
280
|
+
fineAmount: 0,
|
|
281
|
+
returnAmount: 100,
|
|
282
|
+
chargeAmount: 0,
|
|
283
|
+
allowedCancelation: true,
|
|
284
|
+
});
|
|
285
|
+
});
|
|
286
|
+
test('should withhold non-refundable amount on late cancelation with overpayment', () => {
|
|
287
|
+
const result = (0, _1.getOrderCancelationState)({
|
|
288
|
+
cancelationReason: order_cancel_reson_enum_1.OrderCancelReasonEnum.CLIENT,
|
|
289
|
+
slotDiscount: 0,
|
|
290
|
+
minHoursForFullFine: 0,
|
|
291
|
+
cancelationAmount: 0,
|
|
292
|
+
cancelationRule: questroom_cancelation_type_enum_1.QuestroomCancelationTypeEnum.HALF_PRICE,
|
|
293
|
+
transactions: [
|
|
294
|
+
{
|
|
295
|
+
amount: 120,
|
|
296
|
+
type: transaction_type_enum_1.TransactionTypeEnum.SUCCEEDED,
|
|
297
|
+
},
|
|
298
|
+
],
|
|
299
|
+
date: new Date().toISOString(),
|
|
300
|
+
returnCertificates: true,
|
|
301
|
+
certificates: [],
|
|
302
|
+
minHoursForFreeCanceling: 48,
|
|
303
|
+
playersPrice: 100,
|
|
304
|
+
payed: 120,
|
|
305
|
+
minimalPrice: 50,
|
|
306
|
+
nonRefundableAmount: 20,
|
|
307
|
+
});
|
|
308
|
+
// fine = 50 (half of quest), refund base = 120 - 50 = 70, minus
|
|
309
|
+
// non-refundable 20 → 50.
|
|
310
|
+
expect(result).toEqual({
|
|
311
|
+
type: 'descriptionFineWithAutoReturn',
|
|
312
|
+
fineAmount: 50,
|
|
313
|
+
returnAmount: 50,
|
|
314
|
+
chargeAmount: 0,
|
|
315
|
+
allowedCancelation: false,
|
|
316
|
+
});
|
|
317
|
+
});
|
|
318
|
+
test('should floor return at 0 when non-refundable amount exceeds refund', () => {
|
|
319
|
+
const result = (0, _1.getOrderCancelationState)({
|
|
320
|
+
cancelationReason: order_cancel_reson_enum_1.OrderCancelReasonEnum.CLIENT,
|
|
321
|
+
slotDiscount: 0,
|
|
322
|
+
minHoursForFullFine: 0,
|
|
323
|
+
cancelationAmount: 0,
|
|
324
|
+
cancelationRule: questroom_cancelation_type_enum_1.QuestroomCancelationTypeEnum.FULL_PRICE,
|
|
325
|
+
transactions: [
|
|
326
|
+
{
|
|
327
|
+
amount: 120,
|
|
328
|
+
type: transaction_type_enum_1.TransactionTypeEnum.INSIDE,
|
|
329
|
+
},
|
|
330
|
+
],
|
|
331
|
+
date: (0, date_1.addHours)(new Date(), 50).toISOString(),
|
|
332
|
+
returnCertificates: true,
|
|
333
|
+
certificates: [],
|
|
334
|
+
minHoursForFreeCanceling: 48,
|
|
335
|
+
playersPrice: 100,
|
|
336
|
+
payed: 120,
|
|
337
|
+
minimalPrice: 50,
|
|
338
|
+
nonRefundableAmount: 200,
|
|
339
|
+
});
|
|
340
|
+
expect(result).toEqual({
|
|
341
|
+
type: 'descriptionFineWithReturn',
|
|
342
|
+
fineAmount: 0,
|
|
343
|
+
returnAmount: 0,
|
|
344
|
+
chargeAmount: 0,
|
|
345
|
+
allowedCancelation: true,
|
|
346
|
+
});
|
|
347
|
+
});
|
|
256
348
|
});
|
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
|
@@ -16,6 +16,7 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
|
16
16
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
17
|
__exportStar(require("./agreement-config"), exports);
|
|
18
18
|
__exportStar(require("./booking-lead-time"), exports);
|
|
19
|
+
__exportStar(require("./booking-page-url"), exports);
|
|
19
20
|
__exportStar(require("./classify-api-error"), exports);
|
|
20
21
|
__exportStar(require("./convert-hhmm-to-minutes"), exports);
|
|
21
22
|
__exportStar(require("./convert-minutes-to-hhmm"), exports);
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/** Символ-заполнитель для маскированных контактов клиента. */
|
|
2
|
+
export declare const MASK_CHAR = "\u2022";
|
|
3
|
+
/**
|
|
4
|
+
* Маскирует email, оставляя первый символ локальной части и домен:
|
|
5
|
+
* `john.doe@mail.com` → `j•••@mail.com`. Пустые/невалидные значения
|
|
6
|
+
* возвращаются как есть (нечего маскировать).
|
|
7
|
+
*/
|
|
8
|
+
export declare const maskEmail: (email?: string | null) => string;
|
|
9
|
+
/**
|
|
10
|
+
* Маскирует телефон, оставляя последние 2 цифры:
|
|
11
|
+
* `+4915112345678` → `+49•••••••78`-подобный вид. Сохраняет ведущий `+`.
|
|
12
|
+
*/
|
|
13
|
+
export declare const maskPhone: (phone?: string | null) => string;
|
|
14
|
+
/** Похоже ли значение на уже замаскированное (содержит символ-заполнитель). */
|
|
15
|
+
export declare const isMaskedContact: (value?: string | null) => boolean;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.isMaskedContact = exports.maskPhone = exports.maskEmail = exports.MASK_CHAR = void 0;
|
|
4
|
+
/** Символ-заполнитель для маскированных контактов клиента. */
|
|
5
|
+
exports.MASK_CHAR = '•';
|
|
6
|
+
/**
|
|
7
|
+
* Маскирует email, оставляя первый символ локальной части и домен:
|
|
8
|
+
* `john.doe@mail.com` → `j•••@mail.com`. Пустые/невалидные значения
|
|
9
|
+
* возвращаются как есть (нечего маскировать).
|
|
10
|
+
*/
|
|
11
|
+
const maskEmail = (email) => {
|
|
12
|
+
if (!email)
|
|
13
|
+
return email ?? '';
|
|
14
|
+
const trimmed = String(email).trim();
|
|
15
|
+
const at = trimmed.indexOf('@');
|
|
16
|
+
if (at <= 0) {
|
|
17
|
+
// Нет валидной локальной части — маскируем целиком.
|
|
18
|
+
return exports.MASK_CHAR.repeat(Math.max(trimmed.length, 3));
|
|
19
|
+
}
|
|
20
|
+
const local = trimmed.slice(0, at);
|
|
21
|
+
const domain = trimmed.slice(at);
|
|
22
|
+
const firstChar = local[0];
|
|
23
|
+
return `${firstChar}${exports.MASK_CHAR.repeat(3)}${domain}`;
|
|
24
|
+
};
|
|
25
|
+
exports.maskEmail = maskEmail;
|
|
26
|
+
/**
|
|
27
|
+
* Маскирует телефон, оставляя последние 2 цифры:
|
|
28
|
+
* `+4915112345678` → `+49•••••••78`-подобный вид. Сохраняет ведущий `+`.
|
|
29
|
+
*/
|
|
30
|
+
const maskPhone = (phone) => {
|
|
31
|
+
if (!phone)
|
|
32
|
+
return phone ?? '';
|
|
33
|
+
const trimmed = String(phone).trim();
|
|
34
|
+
const hasPlus = trimmed.startsWith('+');
|
|
35
|
+
const digits = trimmed.replace(/\D/g, '');
|
|
36
|
+
if (digits.length <= 2) {
|
|
37
|
+
return `${hasPlus ? '+' : ''}${exports.MASK_CHAR.repeat(Math.max(digits.length, 3))}`;
|
|
38
|
+
}
|
|
39
|
+
const visible = digits.slice(-2);
|
|
40
|
+
const maskedCount = digits.length - 2;
|
|
41
|
+
return `${hasPlus ? '+' : ''}${exports.MASK_CHAR.repeat(maskedCount)}${visible}`;
|
|
42
|
+
};
|
|
43
|
+
exports.maskPhone = maskPhone;
|
|
44
|
+
/** Похоже ли значение на уже замаскированное (содержит символ-заполнитель). */
|
|
45
|
+
const isMaskedContact = (value) => !!value && value.includes(exports.MASK_CHAR);
|
|
46
|
+
exports.isMaskedContact = isMaskedContact;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { RoleRO } from '@escapenavigator/types/dist/role/role.ro';
|
|
2
|
+
export type RoleContactPermissions = Pick<RoleRO, 'totalAccess' | 'canRevealClientContacts'>;
|
|
3
|
+
/**
|
|
4
|
+
* Право видеть полные контакты клиента (телефон/email) без маскировки.
|
|
5
|
+
* Владелец (`totalAccess`) видит всегда; остальным нужен явный флаг.
|
|
6
|
+
*/
|
|
7
|
+
export declare const canRevealClientContacts: (role?: RoleContactPermissions | null) => boolean;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.canRevealClientContacts = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* Право видеть полные контакты клиента (телефон/email) без маскировки.
|
|
6
|
+
* Владелец (`totalAccess`) видит всегда; остальным нужен явный флаг.
|
|
7
|
+
*/
|
|
8
|
+
const canRevealClientContacts = (role) => !!role?.totalAccess || !!role?.canRevealClientContacts;
|
|
9
|
+
exports.canRevealClientContacts = canRevealClientContacts;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Единственная точка XSS-санитизации HTML перед `dangerouslySetInnerHTML`.
|
|
3
|
+
*
|
|
4
|
+
* `sanitizeQuillHtml` исторически только снимала цвета/пробелы и НЕ была защитой
|
|
5
|
+
* от XSS (сохраняла `<script>`/`<style>`, не резала `onerror=`/`javascript:`).
|
|
6
|
+
* Здесь через DOMPurify вырезаем скрипты, inline-обработчики событий и опасные
|
|
7
|
+
* URL-схемы, оставляя обычное форматирование (Quill-HTML договоров, писем,
|
|
8
|
+
* контента квестов). `isomorphic-dompurify` работает и в браузере, и в Node
|
|
9
|
+
* (SSR/тесты), поэтому утилита безопасна в общем пакете.
|
|
10
|
+
*/
|
|
11
|
+
export declare function sanitizeHtml(html: string | null | undefined): string;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.sanitizeHtml = sanitizeHtml;
|
|
7
|
+
const isomorphic_dompurify_1 = __importDefault(require("isomorphic-dompurify"));
|
|
8
|
+
/**
|
|
9
|
+
* Единственная точка XSS-санитизации HTML перед `dangerouslySetInnerHTML`.
|
|
10
|
+
*
|
|
11
|
+
* `sanitizeQuillHtml` исторически только снимала цвета/пробелы и НЕ была защитой
|
|
12
|
+
* от XSS (сохраняла `<script>`/`<style>`, не резала `onerror=`/`javascript:`).
|
|
13
|
+
* Здесь через DOMPurify вырезаем скрипты, inline-обработчики событий и опасные
|
|
14
|
+
* URL-схемы, оставляя обычное форматирование (Quill-HTML договоров, писем,
|
|
15
|
+
* контента квестов). `isomorphic-dompurify` работает и в браузере, и в Node
|
|
16
|
+
* (SSR/тесты), поэтому утилита безопасна в общем пакете.
|
|
17
|
+
*/
|
|
18
|
+
function sanitizeHtml(html) {
|
|
19
|
+
if (!html)
|
|
20
|
+
return '';
|
|
21
|
+
return isomorphic_dompurify_1.default.sanitize(html, {
|
|
22
|
+
// Ссылки в контенте открываем в новой вкладке — сохраняем `target`,
|
|
23
|
+
// а `rel="noopener"` навешиваем хуком ниже.
|
|
24
|
+
ADD_ATTR: ['target'],
|
|
25
|
+
// `<style>` НЕ запрещаем: DOMPurify санитизирует CSS (убирает @import,
|
|
26
|
+
// expression и т.п.), а email-превью на нём держат вёрстку. Режем именно
|
|
27
|
+
// XSS-векторы: скрипты, встраивания и формы (фишинг).
|
|
28
|
+
FORBID_TAGS: ['script', 'iframe', 'object', 'embed', 'form'],
|
|
29
|
+
FORBID_ATTR: ['srcdoc'],
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
// Любой `<a target="_blank">` получает `rel="noopener noreferrer"` — защита от
|
|
33
|
+
// tabnabbing и утечки referrer из пользовательского HTML. Не используем
|
|
34
|
+
// `instanceof Element`: в Node (jsdom через isomorphic-dompurify) глобального
|
|
35
|
+
// `Element` нет, поэтому проверяем `nodeName` и наличие методов.
|
|
36
|
+
isomorphic_dompurify_1.default.addHook('afterSanitizeAttributes', (node) => {
|
|
37
|
+
const el = node;
|
|
38
|
+
if (el.nodeName === 'A' &&
|
|
39
|
+
typeof el.getAttribute === 'function' &&
|
|
40
|
+
typeof el.setAttribute === 'function' &&
|
|
41
|
+
el.getAttribute('target') === '_blank') {
|
|
42
|
+
el.setAttribute('rel', 'noopener noreferrer');
|
|
43
|
+
}
|
|
44
|
+
});
|
|
@@ -18,6 +18,15 @@ export type UtmFields = {
|
|
|
18
18
|
* session storage.
|
|
19
19
|
*/
|
|
20
20
|
enLinkId?: string | null;
|
|
21
|
+
/**
|
|
22
|
+
* Рекламные click-id (last-click, как UTM). В touchpoint-нормализацию
|
|
23
|
+
* НЕ входят — используются только для записи в денорм-колонки заказа /
|
|
24
|
+
* продажи сертификата (серверная атрибуция конверсий).
|
|
25
|
+
*/
|
|
26
|
+
gclid?: string | null;
|
|
27
|
+
fbclid?: string | null;
|
|
28
|
+
ttclid?: string | null;
|
|
29
|
+
msclkid?: string | null;
|
|
21
30
|
};
|
|
22
31
|
/**
|
|
23
32
|
* Нормализация одного UTM-поля для индекса/поиска: trim + lowercase.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@escapenavigator/utils",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.4",
|
|
4
4
|
"publishConfig": {
|
|
5
5
|
"access": "public"
|
|
6
6
|
},
|
|
@@ -14,11 +14,12 @@
|
|
|
14
14
|
"test": "jest"
|
|
15
15
|
},
|
|
16
16
|
"dependencies": {
|
|
17
|
-
"@escapenavigator/types": "^2.0.
|
|
17
|
+
"@escapenavigator/types": "^2.0.4",
|
|
18
18
|
"axios": "^0.21.4",
|
|
19
19
|
"class-transformer": "^0.5.1",
|
|
20
20
|
"class-validator": "^0.13.2",
|
|
21
21
|
"i18next": "^21.6.4",
|
|
22
|
+
"isomorphic-dompurify": "^3.18.0",
|
|
22
23
|
"libphonenumber-js": "^1.12.24",
|
|
23
24
|
"quill-delta": "^5.1.0"
|
|
24
25
|
},
|
|
@@ -28,5 +29,5 @@
|
|
|
28
29
|
"ts-jest": "^29.1.1",
|
|
29
30
|
"typescript": "^5.6"
|
|
30
31
|
},
|
|
31
|
-
"gitHead": "
|
|
32
|
+
"gitHead": "c5d3da52ae3e82c63f099daf59730f2e374df66a"
|
|
32
33
|
}
|