@fiado/type-kit 3.517.0 → 3.519.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.
Files changed (46) hide show
  1. package/_test_/unit/retailWizard/fixtures/saleWithoutPhone.strict.ts +57 -0
  2. package/_test_/unit/retailWizard/saleWithoutPhone.test.ts +46 -0
  3. package/_test_/unit/retailWizard/saleWithoutPhoneLines.test.ts +82 -0
  4. package/_test_/unit/rewardsEligibility/backofficeBarrel.test.ts +1 -0
  5. package/_test_/unit/rewardsEligibility/events.test.ts +1 -1
  6. package/_test_/unit/rewardsEligibility/generateRulesRequest.test.ts +50 -0
  7. package/_test_/unit/rewardsEligibility/generateRulesResponse.test.ts +12 -0
  8. package/_test_/unit/rewardsEligibility/ruleCatalogRich.test.ts +18 -0
  9. package/_test_/unit/rewardsEligibility/rulesAiEnums.test.ts +15 -0
  10. package/bin/retailCommission/dtos/CommissionAccrual.d.ts +2 -1
  11. package/bin/retailWizard/dtos/Sale.d.ts +3 -2
  12. package/bin/retailWizard/dtos/WizardLineProduct.d.ts +3 -0
  13. package/bin/retailWizard/events/RetailCommissionInboundMessage.d.ts +2 -0
  14. package/bin/retailWizard/events/RetailCommissionInboundMessage.js +6 -0
  15. package/bin/retailWizard/events/SaleCompletedV1.d.ts +3 -2
  16. package/bin/retailWizard/events/SaleCompletedV2.d.ts +2 -1
  17. package/bin/rewardsEligibility/constants/BackofficeLimits.d.ts +4 -0
  18. package/bin/rewardsEligibility/constants/BackofficeLimits.js +5 -1
  19. package/bin/rewardsEligibility/dtos/backoffice/GenerateRulesRequest.d.ts +17 -0
  20. package/bin/rewardsEligibility/dtos/backoffice/GenerateRulesRequest.js +44 -0
  21. package/bin/rewardsEligibility/dtos/backoffice/GenerateRulesResponse.d.ts +18 -0
  22. package/bin/rewardsEligibility/dtos/backoffice/GenerateRulesResponse.js +12 -0
  23. package/bin/rewardsEligibility/dtos/backoffice/RuleCatalogResponse.d.ts +6 -1
  24. package/bin/rewardsEligibility/dtos/backoffice/RulesChatTurn.d.ts +6 -0
  25. package/bin/rewardsEligibility/dtos/backoffice/RulesChatTurn.js +32 -0
  26. package/bin/rewardsEligibility/enums/RewardsEligibilityErrorCodeEnum.d.ts +11 -1
  27. package/bin/rewardsEligibility/enums/RewardsEligibilityErrorCodeEnum.js +10 -0
  28. package/bin/rewardsEligibility/enums/RulesChatRoleEnum.d.ts +5 -0
  29. package/bin/rewardsEligibility/enums/RulesChatRoleEnum.js +9 -0
  30. package/bin/rewardsEligibility/index.d.ts +4 -0
  31. package/bin/rewardsEligibility/index.js +4 -0
  32. package/package.json +1 -1
  33. package/src/retailCommission/dtos/CommissionAccrual.ts +2 -1
  34. package/src/retailWizard/dtos/Sale.ts +3 -2
  35. package/src/retailWizard/dtos/WizardLineProduct.ts +3 -0
  36. package/src/retailWizard/events/RetailCommissionInboundMessage.ts +6 -0
  37. package/src/retailWizard/events/SaleCompletedV1.ts +3 -2
  38. package/src/retailWizard/events/SaleCompletedV2.ts +2 -1
  39. package/src/rewardsEligibility/constants/BackofficeLimits.ts +4 -0
  40. package/src/rewardsEligibility/dtos/backoffice/GenerateRulesRequest.ts +33 -0
  41. package/src/rewardsEligibility/dtos/backoffice/GenerateRulesResponse.ts +17 -0
  42. package/src/rewardsEligibility/dtos/backoffice/RuleCatalogResponse.ts +6 -2
  43. package/src/rewardsEligibility/dtos/backoffice/RulesChatTurn.ts +17 -0
  44. package/src/rewardsEligibility/enums/RewardsEligibilityErrorCodeEnum.ts +10 -0
  45. package/src/rewardsEligibility/enums/RulesChatRoleEnum.ts +5 -0
  46. package/src/rewardsEligibility/index.ts +4 -0
@@ -0,0 +1,57 @@
1
+ import { SaleTypeEnum } from '../../../../src/retailWizard/enums/SaleTypeEnum';
2
+ import { SaleFinancierEnum } from '../../../../src/retailWizard/enums/SaleFinancierEnum';
3
+ import type { Sale } from '../../../../src/retailWizard/dtos/Sale';
4
+ import type { SaleCompletedV1 } from '../../../../src/retailWizard/events/SaleCompletedV1';
5
+ import type { SaleCompletedV2 } from '../../../../src/retailWizard/events/SaleCompletedV2';
6
+ import type { CommissionSaleInput } from '../../../../src/retailCommission/dtos/CommissionAccrual';
7
+
8
+ // Venta de contado sin teléfono; el test lo compila con strict, como los consumidores.
9
+ export const saleSku: Sale['sku'] = null;
10
+
11
+ export const completedV1: SaleCompletedV1 = {
12
+ saleId: 's-1',
13
+ folio: 'LG-T001-VTA-abcde-2026W40',
14
+ retailerId: 'r-1',
15
+ storeId: 'st-1',
16
+ retailCustomerId: 'c-1',
17
+ sku: null,
18
+ imei: null,
19
+ amountCents: 35000,
20
+ type: SaleTypeEnum.CASH,
21
+ soldAt: '2026-10-08T12:00:00.000Z',
22
+ occurredAt: '2026-10-08T12:00:00.000Z',
23
+ };
24
+
25
+ export const completedV2: SaleCompletedV2 = {
26
+ saleId: 's-1',
27
+ folio: 'LG-T001-VTA-abcde-2026W40',
28
+ retailerId: 'r-1',
29
+ storeId: 'st-1',
30
+ sellerRetailUserId: 'u-1',
31
+ retailCustomerId: 'c-1',
32
+ sku: null,
33
+ brand: null,
34
+ imei: null,
35
+ amountCents: 35000,
36
+ equipmentPriceCents: null,
37
+ downPaymentCents: null,
38
+ creditId: null,
39
+ type: SaleTypeEnum.CASH,
40
+ externalProvider: null,
41
+ soldAt: '2026-10-08T12:00:00.000Z',
42
+ occurredAt: '2026-10-08T12:00:00.000Z',
43
+ };
44
+
45
+ export const commissionInput: CommissionSaleInput = {
46
+ saleId: 's-1',
47
+ sellerId: 'u-1',
48
+ retailerId: 'r-1',
49
+ storeId: 'st-1',
50
+ sku: null,
51
+ brand: null,
52
+ equipmentPriceCents: null,
53
+ amountCents: 35000,
54
+ financier: Object.values(SaleFinancierEnum)[0],
55
+ paymentType: 'CASH',
56
+ soldAt: '2026-10-08T12:00:00.000Z',
57
+ };
@@ -0,0 +1,46 @@
1
+ import 'reflect-metadata';
2
+ import * as path from 'path';
3
+ import * as ts from 'typescript';
4
+ import {
5
+ commissionInput,
6
+ completedV1,
7
+ completedV2,
8
+ saleSku,
9
+ } from './fixtures/saleWithoutPhone.strict';
10
+
11
+ const FIXTURE = path.join(__dirname, 'fixtures', 'saleWithoutPhone.strict.ts');
12
+
13
+ // Los consumidores compilan con strict; sin strictNullChecks null cabe en string y no prueba nada.
14
+ function strictDiagnostics(): string[] {
15
+ const program = ts.createProgram([FIXTURE], {
16
+ strict: true,
17
+ noEmit: true,
18
+ skipLibCheck: true,
19
+ experimentalDecorators: true,
20
+ emitDecoratorMetadata: true,
21
+ target: ts.ScriptTarget.ES2020,
22
+ module: ts.ModuleKind.CommonJS,
23
+ moduleResolution: ts.ModuleResolutionKind.Node10,
24
+ });
25
+ return ts
26
+ .getPreEmitDiagnostics(program)
27
+ .filter((d) => d.file?.fileName === FIXTURE)
28
+ .map((d) => ts.flattenDiagnosticMessageText(d.messageText, '\n'));
29
+ }
30
+
31
+ describe('venta sin teléfono — sku nullable', () => {
32
+ it('Sale, SaleCompletedV1/V2 y CommissionSaleInput aceptan sku null en strict', () => {
33
+ expect(strictDiagnostics()).toEqual([]);
34
+ });
35
+
36
+ it('el payload viaja con sku null', () => {
37
+ expect(saleSku).toBeNull();
38
+ expect(completedV2.sku).toBeNull();
39
+ expect(commissionInput.sku).toBeNull();
40
+ });
41
+
42
+ it('SaleCompletedV1 omite equipmentPriceCents sin equipo', () => {
43
+ expect(completedV1.sku).toBeNull();
44
+ expect(completedV1).not.toHaveProperty('equipmentPriceCents');
45
+ });
46
+ });
@@ -0,0 +1,82 @@
1
+ import 'reflect-metadata';
2
+ import { plainToInstance } from 'class-transformer';
3
+ import { validateSync, type ValidationError } from 'class-validator';
4
+ import {
5
+ LinePaymentTypeEnum,
6
+ RetailCommissionInboundMessage,
7
+ RetailCommissionInboundMessageTypeEnum,
8
+ SaleTypeEnum,
9
+ type WizardLineProduct,
10
+ } from '../../../src/retailWizard';
11
+ import { ItemTypeEnum, ProductCategoryEnum, TrackingModeEnum } from '../../../src/retailCatalog';
12
+
13
+ function paths(errors: ValidationError[], prefix = ''): string[] {
14
+ return errors.flatMap((e) => {
15
+ const p = prefix ? `${prefix}.${e.property}` : e.property;
16
+ return e.children && e.children.length > 0 ? paths(e.children, p) : [p];
17
+ });
18
+ }
19
+
20
+ function errorsOf(payload: object): string[] {
21
+ return paths(validateSync(plainToInstance(RetailCommissionInboundMessage, payload) as object));
22
+ }
23
+
24
+ describe('venta sin teléfono — phoneCount en el mensaje a comisiones', () => {
25
+ const kit = {
26
+ lineId: 'L1',
27
+ sku: 'KIT-1',
28
+ qty: 1,
29
+ netCents: 349900,
30
+ itemType: ItemTypeEnum.PRODUCT,
31
+ category: ProductCategoryEnum.ACCESSORY,
32
+ tier: null,
33
+ brand: null,
34
+ paymentType: LinePaymentTypeEnum.CASH,
35
+ };
36
+ const message = (line: object) => ({
37
+ type: RetailCommissionInboundMessageTypeEnum.SALE_COMPLETED,
38
+ tenantId: 't',
39
+ retailerId: 'ret',
40
+ storeId: 'st',
41
+ saleId: 'sale-1',
42
+ sessionId: 'ses-1',
43
+ sellerRetailUserId: 'seller-1',
44
+ saleType: SaleTypeEnum.CASH,
45
+ externalProvider: null,
46
+ creditId: null,
47
+ soldAt: '2026-10-08T20:00:00.000Z',
48
+ occurredAt: '2026-10-08T20:00:00.000Z',
49
+ lines: [line],
50
+ });
51
+
52
+ it('un mensaje viejo sin phoneCount sigue validando', () => expect(errorsOf(message(kit))).toEqual([]));
53
+
54
+ it('acepta phoneCount en 0 y positivo', () => {
55
+ expect(errorsOf(message({ ...kit, phoneCount: 0 }))).toEqual([]);
56
+ expect(errorsOf(message({ ...kit, phoneCount: 2 }))).toEqual([]);
57
+ });
58
+
59
+ it('rechaza phoneCount negativo o no entero', () => {
60
+ expect(errorsOf(message({ ...kit, phoneCount: -1 }))).toEqual(['lines.0.phoneCount']);
61
+ expect(errorsOf(message({ ...kit, phoneCount: 1.5 }))).toEqual(['lines.0.phoneCount']);
62
+ });
63
+ });
64
+
65
+ describe('venta sin teléfono — itemType en la ficha de la línea de sesión', () => {
66
+ const base: WizardLineProduct = {
67
+ brand: 'Genérico',
68
+ model: 'Funda',
69
+ capacity: null,
70
+ color: null,
71
+ imageUrl: null,
72
+ tier: null,
73
+ category: ProductCategoryEnum.ACCESSORY,
74
+ trackingMode: TrackingModeEnum.QUANTITY,
75
+ };
76
+
77
+ it('acepta itemType y también su ausencia', () => {
78
+ const service: WizardLineProduct = { ...base, itemType: ItemTypeEnum.SERVICE };
79
+ expect(service.itemType).toBe(ItemTypeEnum.SERVICE);
80
+ expect(base.itemType).toBeUndefined();
81
+ });
82
+ });
@@ -10,6 +10,7 @@ describe('barrel rewardsEligibility incluye el backoffice', () => {
10
10
  'CreateRunRequest', 'PopulationRunRequest', 'ApproveRunRequest', 'QuotaImpactResponse', 'RunResponse', 'RunListResponse',
11
11
  'RunResultResponse', 'RunResultListResponse', 'CreateRunResponse', 'MetricsPointResponse', 'FailedConditionStatResponse', 'MetricsQuery',
12
12
  'MetricsDimensionEnum', 'RuleValueTypeEnum', 'ELIGIBILITY_ID_PATTERN', 'EVENT_KEY_PATTERN', 'REQUEST_ID_PATTERN', 'MAX_QUOTAS_PER_CAMPAIGN',
13
+ 'RulesChatRoleEnum', 'RulesChatTurn', 'GenerateRulesRequest', 'GenerateRulesResponse', 'RULES_CHAT_TURN_MAX_LENGTH', 'RULES_CHAT_MAX_TURNS_HARD',
13
14
  ])('exporta %s', (name) => {
14
15
  expect((kit as Record<string, object | string | number | RegExp>)[name]).toBeDefined();
15
16
  });
@@ -77,7 +77,7 @@ describe('enums de rewardsEligibility (valores que viajan por HTTP)', () => {
77
77
  expect(Object.values(TriggerTypeEnum).sort()).toEqual(['CRON', 'EVENT', 'MANUAL']);
78
78
  const codes = Object.values(RewardsEligibilityErrorCodeEnum);
79
79
  expect(codes).toEqual(expect.arrayContaining(['EVENT_TYPE_NOT_FOUND', 'INPUT_INVALID', 'RUN_NOT_FOUND']));
80
- expect(codes).toHaveLength(30);
80
+ expect(codes).toHaveLength(35);
81
81
  });
82
82
  });
83
83
 
@@ -0,0 +1,50 @@
1
+ import 'reflect-metadata';
2
+ import { plainToInstance } from 'class-transformer';
3
+ import { validate } from 'class-validator';
4
+ import { InputFieldTypeEnum } from '../../../src/dynamicForm/enums/InputFieldTypeEnum';
5
+ import { GenerateRulesRequest } from '../../../src/rewardsEligibility/dtos/backoffice/GenerateRulesRequest';
6
+
7
+ const parse = async (body: object) => {
8
+ const value = plainToInstance(GenerateRulesRequest, body, { excludeExtraneousValues: true });
9
+ return { value, errors: await validate(value) };
10
+ };
11
+ const input = { key: 'buyerDirectoryId', type: InputFieldTypeEnum.DIRECTORY, label: 'Comprador', required: true };
12
+ const turn = (content: string, role = 'user') => ({ role, content });
13
+ const valid = { history: [turn('cuentas de menos de 7 días')], inputs: [input] };
14
+
15
+ describe('GenerateRulesRequest', () => {
16
+ it('acepta un chat con reglas actuales y el contrato de entrada', async () => {
17
+ const { errors, value } = await parse({ ...valid, currentRules: { match: 'ALL', groups: [] } });
18
+ expect(errors).toEqual([]);
19
+ expect(value.history[0]?.content).toBe('cuentas de menos de 7 días');
20
+ expect(value.currentRules).toEqual({ match: 'ALL', groups: [] });
21
+ });
22
+
23
+ it('acepta historial vacío y contrato vacío (el lambda decide con AI_CHAT_HISTORY_EMPTY)', async () => {
24
+ expect((await parse({ history: [], inputs: [] })).errors).toEqual([]);
25
+ });
26
+
27
+ it('rechaza un rol inventado', async () => {
28
+ expect((await parse({ ...valid, history: [turn('hola', 'system')] })).errors.length).toBeGreaterThan(0);
29
+ });
30
+
31
+ it('rechaza un turno vacío y uno de más de 4000 caracteres', async () => {
32
+ expect((await parse({ ...valid, history: [turn('')] })).errors.length).toBeGreaterThan(0);
33
+ expect((await parse({ ...valid, history: [turn('x'.repeat(4001))] })).errors.length).toBeGreaterThan(0);
34
+ expect((await parse({ ...valid, history: [turn('x'.repeat(4000))] })).errors).toEqual([]);
35
+ });
36
+
37
+ it('rechaza más de 50 turnos (tope de seguridad del DTO)', async () => {
38
+ expect((await parse({ ...valid, history: Array.from({ length: 51 }, () => turn('a')) })).errors.length).toBeGreaterThan(0);
39
+ });
40
+
41
+ it('rechaza inputs que no es un arreglo y currentRules que no es un objeto', async () => {
42
+ expect((await parse({ ...valid, inputs: 'x' })).errors.length).toBeGreaterThan(0);
43
+ expect((await parse({ ...valid, currentRules: 'x' })).errors.length).toBeGreaterThan(0);
44
+ });
45
+
46
+ it('descarta propiedades que no son del contrato', async () => {
47
+ const { value } = await parse({ ...valid, hack: true });
48
+ expect('hack' in value).toBe(false);
49
+ });
50
+ });
@@ -0,0 +1,12 @@
1
+ import type { GenerateRulesResponse } from '../../../src/rewardsEligibility/dtos/backoffice/GenerateRulesResponse';
2
+
3
+ describe('GenerateRulesResponse (forma pública)', () => {
4
+ it('las tres salidas posibles del chat se pueden armar con la forma pública', () => {
5
+ const ready: GenerateRulesResponse = { explanation: 'Cuentas de hasta 7 días.', rules: { match: 'ALL', groups: [] }, valid: true };
6
+ const question: GenerateRulesResponse = { explanation: '¿A qué grupo te refieres?', valid: true };
7
+ const failed: GenerateRulesResponse = { explanation: 'No pude.', valid: false, problems: ['rules.groups[0].conditions[0].field: campo desconocido'] };
8
+ expect(ready.rules).toBeDefined();
9
+ expect(question.rules).toBeUndefined();
10
+ expect(failed.problems).toHaveLength(1);
11
+ });
12
+ });
@@ -0,0 +1,18 @@
1
+ import { RuleValueTypeEnum } from '../../../src/rewardsEligibility/enums/RuleValueTypeEnum';
2
+ import type { RuleCatalogFieldResponse } from '../../../src/rewardsEligibility/dtos/backoffice/RuleCatalogResponse';
3
+
4
+ describe('RuleCatalogFieldResponse enriquecido', () => {
5
+ it('un campo puede traer descripción, frases de ejemplo y descripción por opción', () => {
6
+ const field: RuleCatalogFieldResponse = {
7
+ field: 'status', label: 'Estado de la cuenta', valueType: RuleValueTypeEnum.STRING, operators: ['EQUALS'],
8
+ description: 'Estado operativo del directorio del usuario.',
9
+ examplePhrases: ['usuarios activos'],
10
+ options: [{ value: 'ACTIVE', label: 'ACTIVE', description: 'cuenta operativa' }],
11
+ };
12
+ expect(field.options?.[0]?.description).toBe('cuenta operativa');
13
+ });
14
+ it('sigue siendo válido sin los campos nuevos', () => {
15
+ const field: RuleCatalogFieldResponse = { field: 'x', label: 'x', valueType: RuleValueTypeEnum.NUMBER, operators: ['GT'] };
16
+ expect(field.description).toBeUndefined();
17
+ });
18
+ });
@@ -0,0 +1,15 @@
1
+ import { RewardsEligibilityErrorCodeEnum } from '../../../src/rewardsEligibility/enums/RewardsEligibilityErrorCodeEnum';
2
+ import { RulesChatRoleEnum } from '../../../src/rewardsEligibility/enums/RulesChatRoleEnum';
3
+
4
+ describe('enums de la generación de reglas con IA', () => {
5
+ it('los roles del chat son user y assistant (los mismos del prompt de benefits)', () => {
6
+ expect(Object.values(RulesChatRoleEnum)).toEqual(['user', 'assistant']);
7
+ });
8
+
9
+ it.each(['AI_CHAT_HISTORY_EMPTY', 'AI_CHAT_HISTORY_TOO_LONG', 'AI_GENERATION_FAILED', 'AI_RESPONSE_UNPARSEABLE', 'AI_GENERATION_TIMEOUT'] as const)(
10
+ 'el código %s existe y su valor es su nombre',
11
+ (code) => {
12
+ expect(RewardsEligibilityErrorCodeEnum[code]).toBe(code);
13
+ },
14
+ );
15
+ });
@@ -69,7 +69,8 @@ export interface CommissionSaleInput {
69
69
  sellerId: string;
70
70
  retailerId: string;
71
71
  storeId: string;
72
- sku: string;
72
+ /** `null` en la venta sin teléfono: se comisiona solo por accesorios. */
73
+ sku: string | null;
73
74
  brand: string | null;
74
75
  /**
75
76
  * 🔴 **La base de comisión es el PRECIO DEL EQUIPO, no lo cobrado en caja.**
@@ -20,8 +20,9 @@ export interface Sale {
20
20
  wizardSessionId: string;
21
21
  type: SaleTypeEnum;
22
22
  externalProvider: ExternalProviderEnum | null;
23
- sku: string;
24
- /** Líneas vendidas; `sku` e `imei` siguen siendo los del teléfono financiado o de la línea principal. */
23
+ /** SKU del teléfono de la venta; `null` en la venta sin teléfono (en DDB se omite). */
24
+ sku: string | null;
25
+ /** Líneas vendidas; `sku` e `imei` son los del teléfono, o `null` si la venta no lleva teléfono. */
25
26
  lines?: SaleLine[];
26
27
  /** SKUs de líneas y componentes, para filtrar ventas por SKU. */
27
28
  skus?: string[];
@@ -1,3 +1,4 @@
1
+ import type { ItemTypeEnum } from '../../retailCatalog/enums/ItemTypeEnum';
1
2
  import type { ProductCategoryEnum } from '../../retailCatalog/enums/ProductCategoryEnum';
2
3
  import type { ProductTierEnum } from '../../retailCatalog/enums/ProductTierEnum';
3
4
  import type { TrackingModeEnum } from '../../retailCatalog/enums/TrackingModeEnum';
@@ -12,4 +13,6 @@ export interface WizardLineProduct {
12
13
  tier: ProductTierEnum | null;
13
14
  category: ProductCategoryEnum;
14
15
  trackingMode: TrackingModeEnum;
16
+ /** Producto físico o servicio; ausente en líneas viejas (el consumidor lo toma como PRODUCT). */
17
+ itemType?: ItemTypeEnum;
15
18
  }
@@ -21,6 +21,8 @@ export declare class RetailCommissionInboundLine {
21
21
  brand: string | null;
22
22
  paymentType: LinePaymentTypeEnum;
23
23
  role?: WizardLineRoleEnum;
24
+ /** Teléfonos que trae la línea (kit), congelados al cerrar; ausente en mensajes viejos. */
25
+ phoneCount?: number;
24
26
  }
25
27
  /** Venta cerrada o cancelada que el wizard publica en RetailCommissionInboundQueue. */
26
28
  export declare class RetailCommissionInboundMessage {
@@ -72,6 +72,12 @@ __decorate([
72
72
  (0, class_validator_1.IsEnum)(WizardLineRoleEnum_1.WizardLineRoleEnum),
73
73
  __metadata("design:type", String)
74
74
  ], RetailCommissionInboundLine.prototype, "role", void 0);
75
+ __decorate([
76
+ (0, class_validator_1.IsOptional)(),
77
+ (0, class_validator_1.IsInt)(),
78
+ (0, class_validator_1.Min)(0),
79
+ __metadata("design:type", Number)
80
+ ], RetailCommissionInboundLine.prototype, "phoneCount", void 0);
75
81
  /** Venta cerrada o cancelada que el wizard publica en RetailCommissionInboundQueue. */
76
82
  class RetailCommissionInboundMessage {
77
83
  }
@@ -10,7 +10,8 @@ export interface SaleCompletedV1 {
10
10
  retailerId: string;
11
11
  storeId: string;
12
12
  retailCustomerId: string;
13
- sku: string;
13
+ /** `null` en la venta sin teléfono. */
14
+ sku: string | null;
14
15
  imei: string | null;
15
16
  /**
16
17
  * Lo cobrado en caja al cerrar: enganche + lo no financiado (en contado, el total).
@@ -22,7 +23,7 @@ export interface SaleCompletedV1 {
22
23
  * sin `type` un crédito con $1,300 de enganche era indistinguible de un contado de $1,300.
23
24
  */
24
25
  type?: SaleTypeEnum;
25
- /** Precio de lista del equipo; en contado coincide con `amountCents`. */
26
+ /** Precio de lista del equipo; en contado coincide con `amountCents`. Sin equipo se omite. */
26
27
  equipmentPriceCents?: number;
27
28
  /** Enganche cobrado (solo `type=CREDIT`). */
28
29
  downPaymentCents?: number;
@@ -23,7 +23,8 @@ export interface SaleCompletedV2 {
23
23
  /** Quién vendió — el eje del cálculo de comisión. Es lo que más falta en la V1. */
24
24
  sellerRetailUserId: string;
25
25
  retailCustomerId: string;
26
- sku: string;
26
+ /** `null` en la venta sin teléfono. */
27
+ sku: string | null;
27
28
  /** Marca del equipo, del snapshot congelado al cerrar. `null` si no se pudo resolver. */
28
29
  brand: string | null;
29
30
  imei: string | null;
@@ -10,3 +10,7 @@ export declare const REQUEST_ID_PATTERN: RegExp;
10
10
  export declare const MAX_QUOTAS_PER_CAMPAIGN = 9;
11
11
  /** Tope de `limit` de los listados paginados. */
12
12
  export declare const LIST_PAGE_MAX = 100;
13
+ /** Largo máximo del texto de un turno del chat que genera reglas. */
14
+ export declare const RULES_CHAT_TURN_MAX_LENGTH = 4000;
15
+ /** Tope de seguridad de turnos que acepta el DTO (el límite funcional, 10, lo aplica el lambda con `AI_CHAT_HISTORY_TOO_LONG`). */
16
+ export declare const RULES_CHAT_MAX_TURNS_HARD = 50;
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.LIST_PAGE_MAX = exports.MAX_QUOTAS_PER_CAMPAIGN = exports.REQUEST_ID_PATTERN = exports.DATE_PATTERN = exports.EVENT_KEY_PATTERN = exports.ELIGIBILITY_ID_PATTERN = void 0;
3
+ exports.RULES_CHAT_MAX_TURNS_HARD = exports.RULES_CHAT_TURN_MAX_LENGTH = exports.LIST_PAGE_MAX = exports.MAX_QUOTAS_PER_CAMPAIGN = exports.REQUEST_ID_PATTERN = exports.DATE_PATTERN = exports.EVENT_KEY_PATTERN = exports.ELIGIBILITY_ID_PATTERN = void 0;
4
4
  /** Id de campaña, evaluación (minúsculas, dígitos y guion; 2 a 64; sin `#` ni `:` porque entran en llaves compuestas y en la idempotencyKey). */
5
5
  exports.ELIGIBILITY_ID_PATTERN = /^[a-z0-9][a-z0-9-]{0,62}[a-z0-9]$/;
6
6
  /** `eventKey` de un tipo de evento: MAYÚSCULAS con guion bajo (ej. `PHONE_SALES_CODE_REDEEMED`). */
@@ -13,3 +13,7 @@ exports.REQUEST_ID_PATTERN = /^[A-Za-z0-9_-]{8,64}$/;
13
13
  exports.MAX_QUOTAS_PER_CAMPAIGN = 9;
14
14
  /** Tope de `limit` de los listados paginados. */
15
15
  exports.LIST_PAGE_MAX = 100;
16
+ /** Largo máximo del texto de un turno del chat que genera reglas. */
17
+ exports.RULES_CHAT_TURN_MAX_LENGTH = 4000;
18
+ /** Tope de seguridad de turnos que acepta el DTO (el límite funcional, 10, lo aplica el lambda con `AI_CHAT_HISTORY_TOO_LONG`). */
19
+ exports.RULES_CHAT_MAX_TURNS_HARD = 50;
@@ -0,0 +1,17 @@
1
+ import { JsonValue } from '../../../common';
2
+ import { InputField } from '../../../dynamicForm/dtos/InputField';
3
+ import { RulesChatTurn } from './RulesChatTurn';
4
+ /**
5
+ * `POST /rules/generate`: el chat del editor de reglas. El lambda arma el prompt con el catálogo de fuentes y ESTE contrato de entrada
6
+ * (qué sujetos `DIRECTORY` y qué campos de `input` existen), genera `{ match, groups[] }` y lo valida con el mismo `validateRule` de la evaluación.
7
+ */
8
+ export declare class GenerateRulesRequest {
9
+ /** Del turno más viejo al más nuevo; el último es del admin (`user`). Entre 1 y 10 turnos (el lambda responde `AI_CHAT_HISTORY_EMPTY` / `AI_CHAT_HISTORY_TOO_LONG`). */
10
+ history: RulesChatTurn[];
11
+ /** Las reglas que el editor tiene ahora (`{ match, groups[] }`), para refinarlas. Ausente = regla nueva. */
12
+ currentRules?: {
13
+ [key: string]: JsonValue;
14
+ };
15
+ /** Contrato de entrada de la evaluación que se está editando (`InputField[]`, dominio `dynamicForm`): de aquí salen los `subject` y los campos de la fuente `input`. */
16
+ inputs: InputField[];
17
+ }
@@ -0,0 +1,44 @@
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.GenerateRulesRequest = void 0;
13
+ const class_transformer_1 = require("class-transformer");
14
+ const class_validator_1 = require("class-validator");
15
+ const BackofficeLimits_1 = require("../../constants/BackofficeLimits");
16
+ const RulesChatTurn_1 = require("./RulesChatTurn");
17
+ /**
18
+ * `POST /rules/generate`: el chat del editor de reglas. El lambda arma el prompt con el catálogo de fuentes y ESTE contrato de entrada
19
+ * (qué sujetos `DIRECTORY` y qué campos de `input` existen), genera `{ match, groups[] }` y lo valida con el mismo `validateRule` de la evaluación.
20
+ */
21
+ class GenerateRulesRequest {
22
+ }
23
+ exports.GenerateRulesRequest = GenerateRulesRequest;
24
+ __decorate([
25
+ (0, class_transformer_1.Expose)(),
26
+ (0, class_validator_1.IsArray)(),
27
+ (0, class_validator_1.ArrayMaxSize)(BackofficeLimits_1.RULES_CHAT_MAX_TURNS_HARD),
28
+ (0, class_validator_1.ValidateNested)({ each: true }),
29
+ (0, class_transformer_1.Type)(() => RulesChatTurn_1.RulesChatTurn),
30
+ __metadata("design:type", Array)
31
+ ], GenerateRulesRequest.prototype, "history", void 0);
32
+ __decorate([
33
+ (0, class_transformer_1.Expose)(),
34
+ (0, class_validator_1.IsOptional)(),
35
+ (0, class_validator_1.IsObject)(),
36
+ __metadata("design:type", Object)
37
+ ], GenerateRulesRequest.prototype, "currentRules", void 0);
38
+ __decorate([
39
+ (0, class_transformer_1.Expose)(),
40
+ (0, class_validator_1.IsArray)(),
41
+ (0, class_validator_1.ArrayMaxSize)(30),
42
+ (0, class_validator_1.IsObject)({ each: true }),
43
+ __metadata("design:type", Array)
44
+ ], GenerateRulesRequest.prototype, "inputs", void 0);
@@ -0,0 +1,18 @@
1
+ import { JsonValue } from '../../../common';
2
+ /**
3
+ * Respuesta de `POST /rules/generate`. Tres casos:
4
+ * - Reglas listas: `valid: true` + `rules` (ya validadas contra el catálogo y los inputs: el editor puede cargarlas tal cual).
5
+ * - La IA pidió una aclaración (o no podía con lo pedido): `valid: true`, SIN `rules`; la pregunta va en `explanation`.
6
+ * - La IA generó reglas que no pasaron la validación ni tras el reintento: `valid: false`, SIN `rules`, con `problems`.
7
+ */
8
+ export declare class GenerateRulesResponse {
9
+ /** Qué hizo (o qué preguntó) la IA, en español. Siempre viene. */
10
+ explanation: string;
11
+ /** `{ match, groups[] }` de `@fiado/rules-engine`. */
12
+ rules?: {
13
+ [key: string]: JsonValue;
14
+ };
15
+ valid: boolean;
16
+ /** Mensajes legibles de la última salida inválida. Solo con `valid: false`. */
17
+ problems?: string[];
18
+ }
@@ -0,0 +1,12 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.GenerateRulesResponse = void 0;
4
+ /**
5
+ * Respuesta de `POST /rules/generate`. Tres casos:
6
+ * - Reglas listas: `valid: true` + `rules` (ya validadas contra el catálogo y los inputs: el editor puede cargarlas tal cual).
7
+ * - La IA pidió una aclaración (o no podía con lo pedido): `valid: true`, SIN `rules`; la pregunta va en `explanation`.
8
+ * - La IA generó reglas que no pasaron la validación ni tras el reintento: `valid: false`, SIN `rules`, con `problems`.
9
+ */
10
+ class GenerateRulesResponse {
11
+ }
12
+ exports.GenerateRulesResponse = GenerateRulesResponse;
@@ -9,11 +9,16 @@ export declare class RuleCatalogFieldResponse {
9
9
  operators: string[];
10
10
  /** Params del campo (ej. ventana y moneda de `sumAmount`), como formulario dinámico. */
11
11
  params?: InputField[];
12
- /** Valores posibles si es un enum. */
12
+ /** Valores posibles si es un enum (con qué significa cada uno, si se sabe). */
13
13
  options?: {
14
14
  value: string;
15
15
  label: string;
16
+ description?: string;
16
17
  }[];
18
+ /** Qué es este campo, en una frase (tooltip del editor). */
19
+ description?: string;
20
+ /** Frases de ejemplo que típicamente piden este campo (ayuda del chat de IA). */
21
+ examplePhrases?: string[];
17
22
  }
18
23
  export declare class RuleCatalogSourceResponse {
19
24
  source: string;
@@ -0,0 +1,6 @@
1
+ import { RulesChatRoleEnum } from '../../enums/RulesChatRoleEnum';
2
+ /** Un turno del chat que genera reglas con IA. */
3
+ export declare class RulesChatTurn {
4
+ role: RulesChatRoleEnum;
5
+ content: string;
6
+ }
@@ -0,0 +1,32 @@
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.RulesChatTurn = void 0;
13
+ const class_transformer_1 = require("class-transformer");
14
+ const class_validator_1 = require("class-validator");
15
+ const BackofficeLimits_1 = require("../../constants/BackofficeLimits");
16
+ const RulesChatRoleEnum_1 = require("../../enums/RulesChatRoleEnum");
17
+ /** Un turno del chat que genera reglas con IA. */
18
+ class RulesChatTurn {
19
+ }
20
+ exports.RulesChatTurn = RulesChatTurn;
21
+ __decorate([
22
+ (0, class_transformer_1.Expose)(),
23
+ (0, class_validator_1.IsEnum)(RulesChatRoleEnum_1.RulesChatRoleEnum),
24
+ __metadata("design:type", String)
25
+ ], RulesChatTurn.prototype, "role", void 0);
26
+ __decorate([
27
+ (0, class_transformer_1.Expose)(),
28
+ (0, class_validator_1.IsString)(),
29
+ (0, class_validator_1.IsNotEmpty)(),
30
+ (0, class_validator_1.MaxLength)(BackofficeLimits_1.RULES_CHAT_TURN_MAX_LENGTH),
31
+ __metadata("design:type", String)
32
+ ], RulesChatTurn.prototype, "content", void 0);
@@ -49,5 +49,15 @@ export declare enum RewardsEligibilityErrorCodeEnum {
49
49
  /** 400: el rango pedido excede el tope de filas de la exportación. */
50
50
  EXPORT_TOO_LARGE = "EXPORT_TOO_LARGE",
51
51
  /** 503: la tabla de métricas no responde o no existe aún. */
52
- METRICS_UNAVAILABLE = "METRICS_UNAVAILABLE"
52
+ METRICS_UNAVAILABLE = "METRICS_UNAVAILABLE",
53
+ /** 400: `POST /rules/generate` sin ningún turno de chat. */
54
+ AI_CHAT_HISTORY_EMPTY = "AI_CHAT_HISTORY_EMPTY",
55
+ /** 400: el chat tiene más de 10 turnos (se acota el prompt). */
56
+ AI_CHAT_HISTORY_TOO_LONG = "AI_CHAT_HISTORY_TOO_LONG",
57
+ /** 503: el motor de IA no respondió o falló. Reintentar. */
58
+ AI_GENERATION_FAILED = "AI_GENERATION_FAILED",
59
+ /** 503: la IA respondió algo que no se pudo leer como `{ explanation, rules }`. Reintentar. */
60
+ AI_RESPONSE_UNPARSEABLE = "AI_RESPONSE_UNPARSEABLE",
61
+ /** 503: la IA no contestó dentro del presupuesto de tiempo (el gateway corta a los 29 s). Reintentar. */
62
+ AI_GENERATION_TIMEOUT = "AI_GENERATION_TIMEOUT"
53
63
  }
@@ -55,4 +55,14 @@ var RewardsEligibilityErrorCodeEnum;
55
55
  RewardsEligibilityErrorCodeEnum["EXPORT_TOO_LARGE"] = "EXPORT_TOO_LARGE";
56
56
  /** 503: la tabla de métricas no responde o no existe aún. */
57
57
  RewardsEligibilityErrorCodeEnum["METRICS_UNAVAILABLE"] = "METRICS_UNAVAILABLE";
58
+ /** 400: `POST /rules/generate` sin ningún turno de chat. */
59
+ RewardsEligibilityErrorCodeEnum["AI_CHAT_HISTORY_EMPTY"] = "AI_CHAT_HISTORY_EMPTY";
60
+ /** 400: el chat tiene más de 10 turnos (se acota el prompt). */
61
+ RewardsEligibilityErrorCodeEnum["AI_CHAT_HISTORY_TOO_LONG"] = "AI_CHAT_HISTORY_TOO_LONG";
62
+ /** 503: el motor de IA no respondió o falló. Reintentar. */
63
+ RewardsEligibilityErrorCodeEnum["AI_GENERATION_FAILED"] = "AI_GENERATION_FAILED";
64
+ /** 503: la IA respondió algo que no se pudo leer como `{ explanation, rules }`. Reintentar. */
65
+ RewardsEligibilityErrorCodeEnum["AI_RESPONSE_UNPARSEABLE"] = "AI_RESPONSE_UNPARSEABLE";
66
+ /** 503: la IA no contestó dentro del presupuesto de tiempo (el gateway corta a los 29 s). Reintentar. */
67
+ RewardsEligibilityErrorCodeEnum["AI_GENERATION_TIMEOUT"] = "AI_GENERATION_TIMEOUT";
58
68
  })(RewardsEligibilityErrorCodeEnum || (exports.RewardsEligibilityErrorCodeEnum = RewardsEligibilityErrorCodeEnum = {}));
@@ -0,0 +1,5 @@
1
+ /** Quién habló en un turno del chat que genera reglas con IA (`POST /rules/generate`). */
2
+ export declare enum RulesChatRoleEnum {
3
+ USER = "user",
4
+ ASSISTANT = "assistant"
5
+ }
@@ -0,0 +1,9 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.RulesChatRoleEnum = void 0;
4
+ /** Quién habló en un turno del chat que genera reglas con IA (`POST /rules/generate`). */
5
+ var RulesChatRoleEnum;
6
+ (function (RulesChatRoleEnum) {
7
+ RulesChatRoleEnum["USER"] = "user";
8
+ RulesChatRoleEnum["ASSISTANT"] = "assistant";
9
+ })(RulesChatRoleEnum || (exports.RulesChatRoleEnum = RulesChatRoleEnum = {}));
@@ -31,6 +31,7 @@ export * from './dtos/EvaluationReward';
31
31
  export * from './dtos/EvaluationTrigger';
32
32
  export * from './enums/MetricsDimensionEnum';
33
33
  export * from './enums/RuleValueTypeEnum';
34
+ export * from './enums/RulesChatRoleEnum';
34
35
  export * from './constants/BackofficeLimits';
35
36
  export * from './dtos/backoffice/CampaignRequest';
36
37
  export * from './dtos/backoffice/CampaignStatusRequest';
@@ -51,6 +52,9 @@ export * from './dtos/backoffice/SimulateResponse';
51
52
  export * from './dtos/backoffice/RuleCatalogResponse';
52
53
  export * from './dtos/backoffice/PopulationSourceResponse';
53
54
  export * from './dtos/backoffice/InputTypeResponse';
55
+ export * from './dtos/backoffice/RulesChatTurn';
56
+ export * from './dtos/backoffice/GenerateRulesRequest';
57
+ export * from './dtos/backoffice/GenerateRulesResponse';
54
58
  export * from './dtos/backoffice/CreateRunRequest';
55
59
  export * from './dtos/backoffice/PopulationRunRequest';
56
60
  export * from './dtos/backoffice/ApproveRunRequest';
@@ -53,6 +53,7 @@ __exportStar(require("./dtos/EvaluationTrigger"), exports);
53
53
  //Backoffice (fase 3): enums, constantes y DTOs
54
54
  __exportStar(require("./enums/MetricsDimensionEnum"), exports);
55
55
  __exportStar(require("./enums/RuleValueTypeEnum"), exports);
56
+ __exportStar(require("./enums/RulesChatRoleEnum"), exports);
56
57
  __exportStar(require("./constants/BackofficeLimits"), exports);
57
58
  __exportStar(require("./dtos/backoffice/CampaignRequest"), exports);
58
59
  __exportStar(require("./dtos/backoffice/CampaignStatusRequest"), exports);
@@ -73,6 +74,9 @@ __exportStar(require("./dtos/backoffice/SimulateResponse"), exports);
73
74
  __exportStar(require("./dtos/backoffice/RuleCatalogResponse"), exports);
74
75
  __exportStar(require("./dtos/backoffice/PopulationSourceResponse"), exports);
75
76
  __exportStar(require("./dtos/backoffice/InputTypeResponse"), exports);
77
+ __exportStar(require("./dtos/backoffice/RulesChatTurn"), exports);
78
+ __exportStar(require("./dtos/backoffice/GenerateRulesRequest"), exports);
79
+ __exportStar(require("./dtos/backoffice/GenerateRulesResponse"), exports);
76
80
  __exportStar(require("./dtos/backoffice/CreateRunRequest"), exports);
77
81
  __exportStar(require("./dtos/backoffice/PopulationRunRequest"), exports);
78
82
  __exportStar(require("./dtos/backoffice/ApproveRunRequest"), exports);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fiado/type-kit",
3
- "version": "3.517.0",
3
+ "version": "3.519.0",
4
4
  "description": "",
5
5
  "main": "bin/index.js",
6
6
  "types": "bin/index.d.ts",
@@ -72,7 +72,8 @@ export interface CommissionSaleInput {
72
72
  sellerId: string;
73
73
  retailerId: string;
74
74
  storeId: string;
75
- sku: string;
75
+ /** `null` en la venta sin teléfono: se comisiona solo por accesorios. */
76
+ sku: string | null;
76
77
  brand: string | null;
77
78
  /**
78
79
  * 🔴 **La base de comisión es el PRECIO DEL EQUIPO, no lo cobrado en caja.**
@@ -21,8 +21,9 @@ export interface Sale {
21
21
  wizardSessionId: string;
22
22
  type: SaleTypeEnum;
23
23
  externalProvider: ExternalProviderEnum | null;
24
- sku: string;
25
- /** Líneas vendidas; `sku` e `imei` siguen siendo los del teléfono financiado o de la línea principal. */
24
+ /** SKU del teléfono de la venta; `null` en la venta sin teléfono (en DDB se omite). */
25
+ sku: string | null;
26
+ /** Líneas vendidas; `sku` e `imei` son los del teléfono, o `null` si la venta no lleva teléfono. */
26
27
  lines?: SaleLine[];
27
28
  /** SKUs de líneas y componentes, para filtrar ventas por SKU. */
28
29
  skus?: string[];
@@ -1,3 +1,4 @@
1
+ import type { ItemTypeEnum } from '../../retailCatalog/enums/ItemTypeEnum';
1
2
  import type { ProductCategoryEnum } from '../../retailCatalog/enums/ProductCategoryEnum';
2
3
  import type { ProductTierEnum } from '../../retailCatalog/enums/ProductTierEnum';
3
4
  import type { TrackingModeEnum } from '../../retailCatalog/enums/TrackingModeEnum';
@@ -13,4 +14,6 @@ export interface WizardLineProduct {
13
14
  tier: ProductTierEnum | null;
14
15
  category: ProductCategoryEnum;
15
16
  trackingMode: TrackingModeEnum;
17
+ /** Producto físico o servicio; ausente en líneas viejas (el consumidor lo toma como PRODUCT). */
18
+ itemType?: ItemTypeEnum;
16
19
  }
@@ -62,6 +62,12 @@ export class RetailCommissionInboundLine {
62
62
  @IsOptional()
63
63
  @IsEnum(WizardLineRoleEnum)
64
64
  role?: WizardLineRoleEnum;
65
+
66
+ /** Teléfonos que trae la línea (kit), congelados al cerrar; ausente en mensajes viejos. */
67
+ @IsOptional()
68
+ @IsInt()
69
+ @Min(0)
70
+ phoneCount?: number;
65
71
  }
66
72
 
67
73
  /** Venta cerrada o cancelada que el wizard publica en RetailCommissionInboundQueue. */
@@ -11,7 +11,8 @@ export interface SaleCompletedV1 {
11
11
  retailerId: string;
12
12
  storeId: string;
13
13
  retailCustomerId: string;
14
- sku: string;
14
+ /** `null` en la venta sin teléfono. */
15
+ sku: string | null;
15
16
  imei: string | null;
16
17
  /**
17
18
  * Lo cobrado en caja al cerrar: enganche + lo no financiado (en contado, el total).
@@ -23,7 +24,7 @@ export interface SaleCompletedV1 {
23
24
  * sin `type` un crédito con $1,300 de enganche era indistinguible de un contado de $1,300.
24
25
  */
25
26
  type?: SaleTypeEnum;
26
- /** Precio de lista del equipo; en contado coincide con `amountCents`. */
27
+ /** Precio de lista del equipo; en contado coincide con `amountCents`. Sin equipo se omite. */
27
28
  equipmentPriceCents?: number;
28
29
  /** Enganche cobrado (solo `type=CREDIT`). */
29
30
  downPaymentCents?: number;
@@ -24,7 +24,8 @@ export interface SaleCompletedV2 {
24
24
  /** Quién vendió — el eje del cálculo de comisión. Es lo que más falta en la V1. */
25
25
  sellerRetailUserId: string;
26
26
  retailCustomerId: string;
27
- sku: string;
27
+ /** `null` en la venta sin teléfono. */
28
+ sku: string | null;
28
29
  /** Marca del equipo, del snapshot congelado al cerrar. `null` si no se pudo resolver. */
29
30
  brand: string | null;
30
31
  imei: string | null;
@@ -10,3 +10,7 @@ export const REQUEST_ID_PATTERN = /^[A-Za-z0-9_-]{8,64}$/;
10
10
  export const MAX_QUOTAS_PER_CAMPAIGN = 9;
11
11
  /** Tope de `limit` de los listados paginados. */
12
12
  export const LIST_PAGE_MAX = 100;
13
+ /** Largo máximo del texto de un turno del chat que genera reglas. */
14
+ export const RULES_CHAT_TURN_MAX_LENGTH = 4000;
15
+ /** Tope de seguridad de turnos que acepta el DTO (el límite funcional, 10, lo aplica el lambda con `AI_CHAT_HISTORY_TOO_LONG`). */
16
+ export const RULES_CHAT_MAX_TURNS_HARD = 50;
@@ -0,0 +1,33 @@
1
+ import { Expose, Type } from 'class-transformer';
2
+ import { ArrayMaxSize, IsArray, IsObject, IsOptional, ValidateNested } from 'class-validator';
3
+ import { JsonValue } from '../../../common';
4
+ import { InputField } from '../../../dynamicForm/dtos/InputField';
5
+ import { RULES_CHAT_MAX_TURNS_HARD } from '../../constants/BackofficeLimits';
6
+ import { RulesChatTurn } from './RulesChatTurn';
7
+
8
+ /**
9
+ * `POST /rules/generate`: el chat del editor de reglas. El lambda arma el prompt con el catálogo de fuentes y ESTE contrato de entrada
10
+ * (qué sujetos `DIRECTORY` y qué campos de `input` existen), genera `{ match, groups[] }` y lo valida con el mismo `validateRule` de la evaluación.
11
+ */
12
+ export class GenerateRulesRequest {
13
+ /** Del turno más viejo al más nuevo; el último es del admin (`user`). Entre 1 y 10 turnos (el lambda responde `AI_CHAT_HISTORY_EMPTY` / `AI_CHAT_HISTORY_TOO_LONG`). */
14
+ @Expose()
15
+ @IsArray()
16
+ @ArrayMaxSize(RULES_CHAT_MAX_TURNS_HARD)
17
+ @ValidateNested({ each: true })
18
+ @Type(() => RulesChatTurn)
19
+ history: RulesChatTurn[];
20
+
21
+ /** Las reglas que el editor tiene ahora (`{ match, groups[] }`), para refinarlas. Ausente = regla nueva. */
22
+ @Expose()
23
+ @IsOptional()
24
+ @IsObject()
25
+ currentRules?: { [key: string]: JsonValue };
26
+
27
+ /** Contrato de entrada de la evaluación que se está editando (`InputField[]`, dominio `dynamicForm`): de aquí salen los `subject` y los campos de la fuente `input`. */
28
+ @Expose()
29
+ @IsArray()
30
+ @ArrayMaxSize(30)
31
+ @IsObject({ each: true })
32
+ inputs: InputField[];
33
+ }
@@ -0,0 +1,17 @@
1
+ import { JsonValue } from '../../../common';
2
+
3
+ /**
4
+ * Respuesta de `POST /rules/generate`. Tres casos:
5
+ * - Reglas listas: `valid: true` + `rules` (ya validadas contra el catálogo y los inputs: el editor puede cargarlas tal cual).
6
+ * - La IA pidió una aclaración (o no podía con lo pedido): `valid: true`, SIN `rules`; la pregunta va en `explanation`.
7
+ * - La IA generó reglas que no pasaron la validación ni tras el reintento: `valid: false`, SIN `rules`, con `problems`.
8
+ */
9
+ export class GenerateRulesResponse {
10
+ /** Qué hizo (o qué preguntó) la IA, en español. Siempre viene. */
11
+ explanation: string;
12
+ /** `{ match, groups[] }` de `@fiado/rules-engine`. */
13
+ rules?: { [key: string]: JsonValue };
14
+ valid: boolean;
15
+ /** Mensajes legibles de la última salida inválida. Solo con `valid: false`. */
16
+ problems?: string[];
17
+ }
@@ -10,8 +10,12 @@ export class RuleCatalogFieldResponse {
10
10
  operators: string[];
11
11
  /** Params del campo (ej. ventana y moneda de `sumAmount`), como formulario dinámico. */
12
12
  params?: InputField[];
13
- /** Valores posibles si es un enum. */
14
- options?: { value: string; label: string }[];
13
+ /** Valores posibles si es un enum (con qué significa cada uno, si se sabe). */
14
+ options?: { value: string; label: string; description?: string }[];
15
+ /** Qué es este campo, en una frase (tooltip del editor). */
16
+ description?: string;
17
+ /** Frases de ejemplo que típicamente piden este campo (ayuda del chat de IA). */
18
+ examplePhrases?: string[];
15
19
  }
16
20
 
17
21
  export class RuleCatalogSourceResponse {
@@ -0,0 +1,17 @@
1
+ import { Expose } from 'class-transformer';
2
+ import { IsEnum, IsNotEmpty, IsString, MaxLength } from 'class-validator';
3
+ import { RULES_CHAT_TURN_MAX_LENGTH } from '../../constants/BackofficeLimits';
4
+ import { RulesChatRoleEnum } from '../../enums/RulesChatRoleEnum';
5
+
6
+ /** Un turno del chat que genera reglas con IA. */
7
+ export class RulesChatTurn {
8
+ @Expose()
9
+ @IsEnum(RulesChatRoleEnum)
10
+ role: RulesChatRoleEnum;
11
+
12
+ @Expose()
13
+ @IsString()
14
+ @IsNotEmpty()
15
+ @MaxLength(RULES_CHAT_TURN_MAX_LENGTH)
16
+ content: string;
17
+ }
@@ -52,4 +52,14 @@ export enum RewardsEligibilityErrorCodeEnum {
52
52
  EXPORT_TOO_LARGE = 'EXPORT_TOO_LARGE',
53
53
  /** 503: la tabla de métricas no responde o no existe aún. */
54
54
  METRICS_UNAVAILABLE = 'METRICS_UNAVAILABLE',
55
+ /** 400: `POST /rules/generate` sin ningún turno de chat. */
56
+ AI_CHAT_HISTORY_EMPTY = 'AI_CHAT_HISTORY_EMPTY',
57
+ /** 400: el chat tiene más de 10 turnos (se acota el prompt). */
58
+ AI_CHAT_HISTORY_TOO_LONG = 'AI_CHAT_HISTORY_TOO_LONG',
59
+ /** 503: el motor de IA no respondió o falló. Reintentar. */
60
+ AI_GENERATION_FAILED = 'AI_GENERATION_FAILED',
61
+ /** 503: la IA respondió algo que no se pudo leer como `{ explanation, rules }`. Reintentar. */
62
+ AI_RESPONSE_UNPARSEABLE = 'AI_RESPONSE_UNPARSEABLE',
63
+ /** 503: la IA no contestó dentro del presupuesto de tiempo (el gateway corta a los 29 s). Reintentar. */
64
+ AI_GENERATION_TIMEOUT = 'AI_GENERATION_TIMEOUT',
55
65
  }
@@ -0,0 +1,5 @@
1
+ /** Quién habló en un turno del chat que genera reglas con IA (`POST /rules/generate`). */
2
+ export enum RulesChatRoleEnum {
3
+ USER = 'user',
4
+ ASSISTANT = 'assistant',
5
+ }
@@ -42,6 +42,7 @@ export * from './dtos/EvaluationTrigger';
42
42
  //Backoffice (fase 3): enums, constantes y DTOs
43
43
  export * from './enums/MetricsDimensionEnum';
44
44
  export * from './enums/RuleValueTypeEnum';
45
+ export * from './enums/RulesChatRoleEnum';
45
46
  export * from './constants/BackofficeLimits';
46
47
  export * from './dtos/backoffice/CampaignRequest';
47
48
  export * from './dtos/backoffice/CampaignStatusRequest';
@@ -62,6 +63,9 @@ export * from './dtos/backoffice/SimulateResponse';
62
63
  export * from './dtos/backoffice/RuleCatalogResponse';
63
64
  export * from './dtos/backoffice/PopulationSourceResponse';
64
65
  export * from './dtos/backoffice/InputTypeResponse';
66
+ export * from './dtos/backoffice/RulesChatTurn';
67
+ export * from './dtos/backoffice/GenerateRulesRequest';
68
+ export * from './dtos/backoffice/GenerateRulesResponse';
65
69
  export * from './dtos/backoffice/CreateRunRequest';
66
70
  export * from './dtos/backoffice/PopulationRunRequest';
67
71
  export * from './dtos/backoffice/ApproveRunRequest';