@escapenavigator/types 1.10.107 → 1.10.121

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.
@@ -1,9 +1,27 @@
1
1
  export declare enum AgbTypeEnum {
2
- REGISTRATION_NEW_PROFILE = "registration_new_profile",
2
+ /** Политика обработки персональных данных. */
3
+ PRIVACY_POLICY = "privacy_policy",
4
+ /** Импринт / юр. реквизиты Escape Navigator. */
3
5
  IMPRINT = "imprint",
6
+ /** Cookies-уведомление. */
4
7
  COOKIES = "cookies",
5
- PRIVACY_POLICY = "privacy_policy",
6
- AGREEMENT = "agreement",
8
+ /** Договор с квестом на участие в агрегаторе. */
7
9
  PARTNER_PROGRAMM = "partner_programm",
8
- CUSTOMERS_AGREEEMENT = "customers_agreement"
10
+ /** Договор с квестом на SaaS-подписку Escape Navigator. */
11
+ AGREEMENT = "agreement",
12
+ /**
13
+ * Шаблон клиентского соглашения квеста с плейсхолдерами вида
14
+ * `{{legalTitle}}`, `{{phoneForCustomers}}` и т.д. Не показывается в
15
+ * публичной странице `/documents/:type/:country` в orders, используется
16
+ * в app для генерации terms of services конкретного оператора
17
+ * (см. `ClientAgreement` → `TemplateGeneratorModal`).
18
+ */
19
+ TEMPLATE = "template"
9
20
  }
21
+ /**
22
+ * Список типов документов, которые публикуются на странице `/documents/:type/:country`
23
+ * в orders. `TEMPLATE` сюда не входит — это внутренний инструмент app для
24
+ * генерации клиентского соглашения оператора.
25
+ */
26
+ export declare const PUBLIC_DOCUMENT_TYPES: readonly [AgbTypeEnum.PRIVACY_POLICY, AgbTypeEnum.IMPRINT, AgbTypeEnum.COOKIES, AgbTypeEnum.PARTNER_PROGRAMM, AgbTypeEnum.AGREEMENT];
27
+ export declare const isPublicDocumentType: (type: AgbTypeEnum) => boolean;
@@ -1,13 +1,38 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.AgbTypeEnum = void 0;
3
+ exports.isPublicDocumentType = exports.PUBLIC_DOCUMENT_TYPES = exports.AgbTypeEnum = void 0;
4
4
  var AgbTypeEnum;
5
5
  (function (AgbTypeEnum) {
6
- AgbTypeEnum["REGISTRATION_NEW_PROFILE"] = "registration_new_profile";
6
+ /** Политика обработки персональных данных. */
7
+ AgbTypeEnum["PRIVACY_POLICY"] = "privacy_policy";
8
+ /** Импринт / юр. реквизиты Escape Navigator. */
7
9
  AgbTypeEnum["IMPRINT"] = "imprint";
10
+ /** Cookies-уведомление. */
8
11
  AgbTypeEnum["COOKIES"] = "cookies";
9
- AgbTypeEnum["PRIVACY_POLICY"] = "privacy_policy";
10
- AgbTypeEnum["AGREEMENT"] = "agreement";
12
+ /** Договор с квестом на участие в агрегаторе. */
11
13
  AgbTypeEnum["PARTNER_PROGRAMM"] = "partner_programm";
12
- AgbTypeEnum["CUSTOMERS_AGREEEMENT"] = "customers_agreement";
14
+ /** Договор с квестом на SaaS-подписку Escape Navigator. */
15
+ AgbTypeEnum["AGREEMENT"] = "agreement";
16
+ /**
17
+ * Шаблон клиентского соглашения квеста с плейсхолдерами вида
18
+ * `{{legalTitle}}`, `{{phoneForCustomers}}` и т.д. Не показывается в
19
+ * публичной странице `/documents/:type/:country` в orders, используется
20
+ * в app для генерации terms of services конкретного оператора
21
+ * (см. `ClientAgreement` → `TemplateGeneratorModal`).
22
+ */
23
+ AgbTypeEnum["TEMPLATE"] = "template";
13
24
  })(AgbTypeEnum || (exports.AgbTypeEnum = AgbTypeEnum = {}));
25
+ /**
26
+ * Список типов документов, которые публикуются на странице `/documents/:type/:country`
27
+ * в orders. `TEMPLATE` сюда не входит — это внутренний инструмент app для
28
+ * генерации клиентского соглашения оператора.
29
+ */
30
+ exports.PUBLIC_DOCUMENT_TYPES = [
31
+ AgbTypeEnum.PRIVACY_POLICY,
32
+ AgbTypeEnum.IMPRINT,
33
+ AgbTypeEnum.COOKIES,
34
+ AgbTypeEnum.PARTNER_PROGRAMM,
35
+ AgbTypeEnum.AGREEMENT,
36
+ ];
37
+ const isPublicDocumentType = (type) => exports.PUBLIC_DOCUMENT_TYPES.includes(type);
38
+ exports.isPublicDocumentType = isPublicDocumentType;
@@ -1,4 +1,6 @@
1
+ import { CountriesEnum } from '../shared/enum/countries.enum';
2
+ import { AgbTypeEnum } from './agb-type.enum';
1
3
  export declare class GetForOrderServiceDto {
2
- type: string;
3
- country: string;
4
+ type: AgbTypeEnum;
5
+ country: CountriesEnum;
4
6
  }
@@ -10,15 +10,17 @@ var __metadata = (this && this.__metadata) || function (k, v) {
10
10
  };
11
11
  Object.defineProperty(exports, "__esModule", { value: true });
12
12
  exports.GetForOrderServiceDto = void 0;
13
- const is_not_blank_1 = require("../shared/is-not-blank");
13
+ const class_validator_1 = require("class-validator");
14
+ const countries_enum_1 = require("../shared/enum/countries.enum");
15
+ const agb_type_enum_1 = require("./agb-type.enum");
14
16
  class GetForOrderServiceDto {
15
17
  }
16
18
  exports.GetForOrderServiceDto = GetForOrderServiceDto;
17
19
  __decorate([
18
- (0, is_not_blank_1.IsNotBlank)(),
20
+ (0, class_validator_1.IsEnum)(agb_type_enum_1.AgbTypeEnum),
19
21
  __metadata("design:type", String)
20
22
  ], GetForOrderServiceDto.prototype, "type", void 0);
21
23
  __decorate([
22
- (0, is_not_blank_1.IsNotBlank)(),
24
+ (0, class_validator_1.IsEnum)(countries_enum_1.CountriesEnum),
23
25
  __metadata("design:type", String)
24
26
  ], GetForOrderServiceDto.prototype, "country", void 0);
@@ -0,0 +1,26 @@
1
+ import { OpenapiCreatePreorderDto } from '../orders/openapi-create-preorder.dto';
2
+ /**
3
+ * Атомарное создание сразу нескольких preorder-ов одной группой в один
4
+ * HTTP-запрос. Конкретно адресован app-flow «забронировать N игр за раз»:
5
+ * виджет умеет делать это циклом из N×POST /preorder + attachToOrderTokens,
6
+ * но это:
7
+ * • N×network round-trip (юзер увидит лоадер на каждый);
8
+ * • N×race-window между preorder и group attach;
9
+ * • при фейле i-го preorder клиент остаётся с зомбированными первыми i-1.
10
+ *
11
+ * Batch endpoint решает всё это: одна валидация, одна точка отказа,
12
+ * compensation rollback всех успевших создаться ордеров при любой
13
+ * ошибке. Бэкенд гарантирует, что либо группа собрана целиком, либо
14
+ * ни одного ордера не осталось.
15
+ *
16
+ * Ограничения:
17
+ * • `items.length` в [2, 5] — одиночный preorder идёт через
18
+ * POST /openapi/orders/preorder (там нет смысла создавать группу);
19
+ * верхний лимит — `OrderGroupService.ORDER_GROUP_SIZE_LIMIT`.
20
+ * • Все items должны иметь идентичные client-fields (name/phone/email/...).
21
+ * Бэкенд это проверяет и бросает 400 при mismatch, чтобы не создать
22
+ * несколько разных клиентов.
23
+ */
24
+ export declare class OpenapiBatchCreatePreorderDto {
25
+ items: OpenapiCreatePreorderDto[];
26
+ }
@@ -0,0 +1,48 @@
1
+ "use strict";
2
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
3
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
4
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
5
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
6
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
7
+ };
8
+ var __metadata = (this && this.__metadata) || function (k, v) {
9
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
10
+ };
11
+ Object.defineProperty(exports, "__esModule", { value: true });
12
+ exports.OpenapiBatchCreatePreorderDto = void 0;
13
+ const class_transformer_1 = require("class-transformer");
14
+ const class_validator_1 = require("class-validator");
15
+ const openapi_create_preorder_dto_1 = require("../orders/openapi-create-preorder.dto");
16
+ /**
17
+ * Атомарное создание сразу нескольких preorder-ов одной группой в один
18
+ * HTTP-запрос. Конкретно адресован app-flow «забронировать N игр за раз»:
19
+ * виджет умеет делать это циклом из N×POST /preorder + attachToOrderTokens,
20
+ * но это:
21
+ * • N×network round-trip (юзер увидит лоадер на каждый);
22
+ * • N×race-window между preorder и group attach;
23
+ * • при фейле i-го preorder клиент остаётся с зомбированными первыми i-1.
24
+ *
25
+ * Batch endpoint решает всё это: одна валидация, одна точка отказа,
26
+ * compensation rollback всех успевших создаться ордеров при любой
27
+ * ошибке. Бэкенд гарантирует, что либо группа собрана целиком, либо
28
+ * ни одного ордера не осталось.
29
+ *
30
+ * Ограничения:
31
+ * • `items.length` в [2, 5] — одиночный preorder идёт через
32
+ * POST /openapi/orders/preorder (там нет смысла создавать группу);
33
+ * верхний лимит — `OrderGroupService.ORDER_GROUP_SIZE_LIMIT`.
34
+ * • Все items должны иметь идентичные client-fields (name/phone/email/...).
35
+ * Бэкенд это проверяет и бросает 400 при mismatch, чтобы не создать
36
+ * несколько разных клиентов.
37
+ */
38
+ class OpenapiBatchCreatePreorderDto {
39
+ }
40
+ exports.OpenapiBatchCreatePreorderDto = OpenapiBatchCreatePreorderDto;
41
+ __decorate([
42
+ (0, class_validator_1.ArrayMinSize)(2),
43
+ (0, class_validator_1.ArrayMaxSize)(5),
44
+ (0, class_validator_1.ValidateNested)({ each: true }),
45
+ (0, class_transformer_1.Type)(() => openapi_create_preorder_dto_1.OpenapiCreatePreorderDto),
46
+ (0, class_transformer_1.Expose)(),
47
+ __metadata("design:type", Array)
48
+ ], OpenapiBatchCreatePreorderDto.prototype, "items", void 0);
@@ -39,10 +39,15 @@ declare class CustomFiled {
39
39
  }
40
40
  declare class Profile {
41
41
  id: number;
42
+ /**
43
+ * Уникальный slug компании. Используется виджетом для построения публичных
44
+ * ссылок на страницу клиентского соглашения оператора
45
+ * (`{ORDERS_DOMAIN}/company/:slug/terms-of-services`).
46
+ */
47
+ slug: string;
42
48
  phoneForCustomers: string;
43
49
  country: CountriesEnum;
44
50
  bookingFields: WidgetBookingFiledEnum[];
45
- agreementLink: string;
46
51
  title: string;
47
52
  availableLanguages: Languages[];
48
53
  language: Languages;
@@ -115,6 +115,10 @@ __decorate([
115
115
  (0, class_transformer_1.Expose)(),
116
116
  __metadata("design:type", Number)
117
117
  ], Profile.prototype, "id", void 0);
118
+ __decorate([
119
+ (0, class_transformer_1.Expose)(),
120
+ __metadata("design:type", String)
121
+ ], Profile.prototype, "slug", void 0);
118
122
  __decorate([
119
123
  (0, class_transformer_1.Expose)(),
120
124
  __metadata("design:type", String)
@@ -127,10 +131,6 @@ __decorate([
127
131
  (0, class_transformer_1.Expose)(),
128
132
  __metadata("design:type", Array)
129
133
  ], Profile.prototype, "bookingFields", void 0);
130
- __decorate([
131
- (0, class_transformer_1.Expose)(),
132
- __metadata("design:type", String)
133
- ], Profile.prototype, "agreementLink", void 0);
134
134
  __decorate([
135
135
  (0, class_transformer_1.Expose)(),
136
136
  __metadata("design:type", String)
@@ -0,0 +1,26 @@
1
+ import { CountriesEnum } from '../../shared/enum/countries.enum';
2
+ /**
3
+ * Срез данных компании, нужный админу на странице "Profile agreements".
4
+ * Не тянем полный `AdminProfileRO` намеренно — чтобы пагинированный список
5
+ * не падал в Mb при сотнях профилей.
6
+ */
7
+ export declare class AdminProfileAgbProfile {
8
+ id: number;
9
+ title: string;
10
+ slug: string;
11
+ country: CountriesEnum;
12
+ }
13
+ /**
14
+ * Запись клиентского соглашения профиля для админ-таблицы. `length` —
15
+ * число символов чистого текста (после strip HTML), считается на бэке,
16
+ * чтобы фронт не тащил весь HTML только ради подсчёта.
17
+ */
18
+ export declare class AdminProfileAgbRO {
19
+ id: number;
20
+ createdAt: Date;
21
+ text: string;
22
+ /** Длина чистого текста (без HTML-тегов) — используется в фильтрах
23
+ * и для подсветки «короткого/пустого» соглашения в таблице. */
24
+ length: number;
25
+ profile?: AdminProfileAgbProfile;
26
+ }
@@ -0,0 +1,67 @@
1
+ "use strict";
2
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
3
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
4
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
5
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
6
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
7
+ };
8
+ var __metadata = (this && this.__metadata) || function (k, v) {
9
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
10
+ };
11
+ Object.defineProperty(exports, "__esModule", { value: true });
12
+ exports.AdminProfileAgbRO = exports.AdminProfileAgbProfile = void 0;
13
+ const class_transformer_1 = require("class-transformer");
14
+ const countries_enum_1 = require("../../shared/enum/countries.enum");
15
+ /**
16
+ * Срез данных компании, нужный админу на странице "Profile agreements".
17
+ * Не тянем полный `AdminProfileRO` намеренно — чтобы пагинированный список
18
+ * не падал в Mb при сотнях профилей.
19
+ */
20
+ class AdminProfileAgbProfile {
21
+ }
22
+ exports.AdminProfileAgbProfile = AdminProfileAgbProfile;
23
+ __decorate([
24
+ (0, class_transformer_1.Expose)(),
25
+ __metadata("design:type", Number)
26
+ ], AdminProfileAgbProfile.prototype, "id", void 0);
27
+ __decorate([
28
+ (0, class_transformer_1.Expose)(),
29
+ __metadata("design:type", String)
30
+ ], AdminProfileAgbProfile.prototype, "title", void 0);
31
+ __decorate([
32
+ (0, class_transformer_1.Expose)(),
33
+ __metadata("design:type", String)
34
+ ], AdminProfileAgbProfile.prototype, "slug", void 0);
35
+ __decorate([
36
+ (0, class_transformer_1.Expose)(),
37
+ __metadata("design:type", String)
38
+ ], AdminProfileAgbProfile.prototype, "country", void 0);
39
+ /**
40
+ * Запись клиентского соглашения профиля для админ-таблицы. `length` —
41
+ * число символов чистого текста (после strip HTML), считается на бэке,
42
+ * чтобы фронт не тащил весь HTML только ради подсчёта.
43
+ */
44
+ class AdminProfileAgbRO {
45
+ }
46
+ exports.AdminProfileAgbRO = AdminProfileAgbRO;
47
+ __decorate([
48
+ (0, class_transformer_1.Expose)(),
49
+ __metadata("design:type", Number)
50
+ ], AdminProfileAgbRO.prototype, "id", void 0);
51
+ __decorate([
52
+ (0, class_transformer_1.Expose)(),
53
+ __metadata("design:type", Date)
54
+ ], AdminProfileAgbRO.prototype, "createdAt", void 0);
55
+ __decorate([
56
+ (0, class_transformer_1.Expose)(),
57
+ __metadata("design:type", String)
58
+ ], AdminProfileAgbRO.prototype, "text", void 0);
59
+ __decorate([
60
+ (0, class_transformer_1.Expose)(),
61
+ __metadata("design:type", Number)
62
+ ], AdminProfileAgbRO.prototype, "length", void 0);
63
+ __decorate([
64
+ (0, class_transformer_1.Expose)(),
65
+ (0, class_transformer_1.Type)(() => AdminProfileAgbProfile),
66
+ __metadata("design:type", AdminProfileAgbProfile)
67
+ ], AdminProfileAgbRO.prototype, "profile", void 0);
@@ -1,6 +1,12 @@
1
1
  export type ProfileAgbRO = {
2
2
  id: number;
3
3
  text: string;
4
+ /**
5
+ * Дата последнего редактирования. Показывается на публичной странице
6
+ * соглашения оператора как «Редакция от …», чтобы клиент видел
7
+ * актуальность версии договора.
8
+ */
9
+ updatedAt?: Date | string;
4
10
  profile?: {
5
11
  title?: string;
6
12
  logo?: string;
@@ -32,11 +32,13 @@ export declare class ProfileRO {
32
32
  legalTitle: string;
33
33
  legalAddress?: string;
34
34
  legalTaxId?: string;
35
+ /** ОГРН/ОГРНИП — для RU-юрлиц. Используется как плейсхолдер `{{ogrn}}`
36
+ * в TEMPLATE-документах при генерации клиентского соглашения. */
37
+ ogrn?: string;
35
38
  phoneForCustomers: string;
36
39
  logo: string;
37
40
  mainEmail: string;
38
41
  site: string;
39
- agreementLink: string;
40
42
  crm: string;
41
43
  slug: string;
42
44
  language: Languages;
@@ -111,6 +111,10 @@ __decorate([
111
111
  (0, class_transformer_1.Expose)(),
112
112
  __metadata("design:type", String)
113
113
  ], ProfileRO.prototype, "legalTaxId", void 0);
114
+ __decorate([
115
+ (0, class_transformer_1.Expose)(),
116
+ __metadata("design:type", String)
117
+ ], ProfileRO.prototype, "ogrn", void 0);
114
118
  __decorate([
115
119
  (0, class_transformer_1.Expose)(),
116
120
  __metadata("design:type", String)
@@ -127,10 +131,6 @@ __decorate([
127
131
  (0, class_transformer_1.Expose)(),
128
132
  __metadata("design:type", String)
129
133
  ], ProfileRO.prototype, "site", void 0);
130
- __decorate([
131
- (0, class_transformer_1.Expose)(),
132
- __metadata("design:type", String)
133
- ], ProfileRO.prototype, "agreementLink", void 0);
134
134
  __decorate([
135
135
  (0, class_transformer_1.Expose)(),
136
136
  __metadata("design:type", String)
@@ -4,6 +4,7 @@ export declare class UpdateCurrentBaseDto {
4
4
  legalTitle: string;
5
5
  legalAddress?: string;
6
6
  legalTaxId?: string;
7
+ ogrn?: string;
7
8
  tax?: number;
8
9
  taxIncluded?: boolean;
9
10
  phoneForCustomers: string;
@@ -37,6 +37,11 @@ __decorate([
37
37
  (0, class_transformer_1.Expose)(),
38
38
  __metadata("design:type", String)
39
39
  ], UpdateCurrentBaseDto.prototype, "legalTaxId", void 0);
40
+ __decorate([
41
+ (0, class_validator_1.IsOptional)(),
42
+ (0, class_transformer_1.Expose)(),
43
+ __metadata("design:type", String)
44
+ ], UpdateCurrentBaseDto.prototype, "ogrn", void 0);
40
45
  __decorate([
41
46
  (0, class_validator_1.IsOptional)(),
42
47
  (0, class_validator_1.IsNumber)(),
@@ -1,6 +1,5 @@
1
1
  import { UpdateCurrentBaseDto } from './update-current-base.dto';
2
2
  export declare class UpdateCurrentDto extends UpdateCurrentBaseDto {
3
- agreementLink: string;
4
3
  site: string;
5
4
  logo: string;
6
5
  gaEnabled: boolean;
@@ -16,11 +16,6 @@ const update_current_base_dto_1 = require("./update-current-base.dto");
16
16
  class UpdateCurrentDto extends update_current_base_dto_1.UpdateCurrentBaseDto {
17
17
  }
18
18
  exports.UpdateCurrentDto = UpdateCurrentDto;
19
- __decorate([
20
- (0, class_validator_1.IsUrl)(),
21
- (0, class_transformer_1.Expose)(),
22
- __metadata("design:type", String)
23
- ], UpdateCurrentDto.prototype, "agreementLink", void 0);
24
19
  __decorate([
25
20
  (0, class_validator_1.IsUrl)(),
26
21
  (0, class_transformer_1.Expose)(),
@@ -2,8 +2,12 @@
2
2
  * Сводный отчёт по продажам сертификатов за выбранный период.
3
3
  *
4
4
  * Все денежные суммы — в минимальных единицах валюты (копейки/центы).
5
- * Базовая фильтрация на бэке: `code != NOT_GENERATED_CODE` (висящие
6
- * корзины не учитываем) + `paidAt` в окне `periods`.
5
+ *
6
+ * Когорта периода = `code != NOT_GENERATED_CODE` + `annul = false` +
7
+ * `paidAt ∈ [start, end)`. ВСЕ агрегаты overview (включая остаток
8
+ * номиналов, количество полностью использованных и аннулированных)
9
+ * считаются по одной и той же когорте — это снимает прежний разнобой,
10
+ * когда `outstandingBalance` суммировался по всей истории профиля.
7
11
  *
8
12
  * Семантика финансовых полей:
9
13
  * - gross — стоимость по прайсу (SUM(total)). Не выручка.
@@ -11,13 +15,19 @@
11
15
  * - discount — сумма ручных скидок (SUM(discountTotal)).
12
16
  * - promocode — сумма промокодов (SUM(promocodeTotal)).
13
17
  * - refunded — реально возвращённые клиенту деньги (SUM(refundedAmount)).
18
+ * - nominalSold — суммарный номинал когорты (SUM(nominal)).
19
+ * - nominalRedeemed — сколько из этого номинала уже потрачено в заказах
20
+ * (SUM(usedAmount)).
21
+ * - outstandingBalance — остаток номиналов когорты у ещё не закрытых
22
+ * сертификатов (SUM(balance) WHERE used = false).
23
+ * - redemptionRate — целое число процентов, nominalRedeemed / nominalSold.
14
24
  */
15
25
  export type CertificatesOverview = {
16
26
  /** Сколько сертификатов выписано (paid) за период. */
17
27
  soldCount: number;
18
- /** Сколько аннулировано в этом же периоде (по annulledAt). */
28
+ /** Сколько из когорты уже аннулировано (annul = true к моменту запроса). */
19
29
  annulledCount: number;
20
- /** Сколько использовано полностью (по fullyUsedAt). */
30
+ /** Сколько из когорты уже полностью погашено (fullyUsedAt IS NOT NULL). */
21
31
  fullyUsedCount: number;
22
32
  /** Сколько ушло в подарок (isGift = true) среди soldCount. */
23
33
  giftCount: number;
@@ -28,8 +38,14 @@ export type CertificatesOverview = {
28
38
  refunded: number;
29
39
  /** Средний чек по cash: cash / soldCount, 0 если нет продаж. */
30
40
  avgCheck: number;
31
- /** Не использованный остаток номиналов: SUM(balance) у не-аннулированных. */
41
+ /** Суммарный номинал по когорте: SUM(nominal). */
42
+ nominalSold: number;
43
+ /** Сколько номинала уже потрачено в заказах: SUM(usedAmount). */
44
+ nominalRedeemed: number;
45
+ /** Остаток номиналов когорты у не закрытых сертификатов: SUM(balance) WHERE used = false. */
32
46
  outstandingBalance: number;
47
+ /** Доля погашения номинала, целое число процентов (0..100). */
48
+ redemptionRate: number;
33
49
  };
34
50
  export type CertificatesDynamicRow = {
35
51
  period: string;
@@ -66,6 +82,52 @@ export type CertificatesByUtmRow = {
66
82
  soldCount: number;
67
83
  cash: number;
68
84
  };
85
+ /**
86
+ * Бакеты «через сколько дней после покупки сертификат…»
87
+ * - d0 — в день покупки
88
+ * - d1_7 — 1..7 дней
89
+ * - d8_30 — 8..30 дней
90
+ * - d31_90 — 31..90 дней
91
+ * - d91_180 — 91..180 дней
92
+ * - d181_365 — 181..365 дней
93
+ * - d_over_365 — больше года
94
+ * - not_yet — ещё не активирован / не погашен полностью
95
+ */
96
+ export type CertificatesVelocityBucket = 'd0' | 'd1_7' | 'd8_30' | 'd31_90' | 'd91_180' | 'd181_365' | 'd_over_365' | 'not_yet';
97
+ export type CertificatesVelocityBucketRow = {
98
+ bucket: CertificatesVelocityBucket;
99
+ /** Сколько сертификатов когорты попало в бакет. */
100
+ count: number;
101
+ };
102
+ /**
103
+ * Скорость использования сертификатов когорты периода.
104
+ *
105
+ * Когорта = `code != NOT_GENERATED_CODE` + `annul = false` +
106
+ * `paidAt ∈ [start, end)`. Источники дат — `firstUsedAt`, `fullyUsedAt`,
107
+ * `expireDate`. Никаких джоинов — считаем по плоской `certificatesales`.
108
+ */
109
+ export type CertificatesRedemptionVelocity = {
110
+ /** Распределение времени до первого применения. */
111
+ firstUseBuckets: CertificatesVelocityBucketRow[];
112
+ /** Распределение времени до полного погашения. */
113
+ fullRedeemBuckets: CertificatesVelocityBucketRow[];
114
+ /** Медианное число дней от покупки до первого применения. */
115
+ medianDaysToFirstUse: number | null;
116
+ /** Медианное число дней от покупки до полного погашения. */
117
+ medianDaysToFullRedeem: number | null;
118
+ /** Активировано в течение 7 дней после покупки. */
119
+ activatedWithin7d: number;
120
+ /** Активировано в течение 30 дней после покупки. */
121
+ activatedWithin30d: number;
122
+ /** Полностью погашено в течение 30 дней после покупки. */
123
+ fullyRedeemedWithin30d: number;
124
+ /** Полностью погашено в течение 90 дней после покупки. */
125
+ fullyRedeemedWithin90d: number;
126
+ /** «Спящие» — куплены больше 90 дней назад и ни разу не применены. */
127
+ dormantOver90d: number;
128
+ /** Сгорели (expireDate < сегодня) и не были полностью использованы. */
129
+ expiredUnused: number;
130
+ };
69
131
  export type CertificatesReportRO = {
70
132
  overview: CertificatesOverview;
71
133
  dynamic: CertificatesDynamicRow[];
@@ -73,4 +135,5 @@ export type CertificatesReportRO = {
73
135
  bySource: CertificatesBySourceRow[];
74
136
  byDelivery: CertificatesByDeliveryRow[];
75
137
  byUtm: CertificatesByUtmRow[];
138
+ redemption: CertificatesRedemptionVelocity;
76
139
  };