@fiado/api-invoker 5.42.0 → 5.44.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.
- package/bin/document-generator/DocumentGeneratorApi.d.ts +1 -1
- package/bin/document-generator/DocumentGeneratorApi.js +3 -3
- package/bin/index.d.ts +1 -0
- package/bin/index.js +1 -0
- package/bin/legalDocument/api/LegalDocumentApi.d.ts +2 -1
- package/bin/legalDocument/api/LegalDocumentApi.js +7 -0
- package/bin/legalDocument/api/interfaces/ILegalDocumentApi.d.ts +5 -0
- package/bin/loanCredit/api/dtos/LoanCreditAnalyticsBatch.d.ts +16 -3
- package/bin/loanCredit/api/interfaces/ILoanCreditBusinessApi.d.ts +4 -2
- package/bin/retailCatalog/api/RetailCatalogBusinessApi.d.ts +2 -1
- package/bin/retailCatalog/api/RetailCatalogBusinessApi.js +11 -0
- package/bin/retailCatalog/api/interfaces/IRetailCatalogBusinessApi.d.ts +56 -5
- package/bin/retailCatalog/api/interfaces/IRetailCatalogBusinessApi.js +2 -1
- package/bin/retailWizard/api/RetailWizardBusinessApi.d.ts +23 -0
- package/bin/retailWizard/api/RetailWizardBusinessApi.js +45 -0
- package/bin/retailWizard/api/interfaces/IRetailWizardBusinessApi.d.ts +58 -0
- package/bin/retailWizard/api/interfaces/IRetailWizardBusinessApi.js +1 -0
- package/bin/retailWizard/index.d.ts +2 -0
- package/bin/retailWizard/index.js +2 -0
- package/package.json +1 -1
- package/src/document-generator/DocumentGeneratorApi.ts +4 -4
- package/src/index.ts +1 -0
- package/src/legalDocument/api/LegalDocumentApi.ts +12 -1
- package/src/legalDocument/api/interfaces/ILegalDocumentApi.ts +6 -0
- package/src/loanCredit/api/dtos/LoanCreditAnalyticsBatch.ts +16 -3
- package/src/loanCredit/api/interfaces/ILoanCreditBusinessApi.ts +4 -2
- package/src/retailCatalog/api/RetailCatalogBusinessApi.ts +16 -0
- package/src/retailCatalog/api/interfaces/IRetailCatalogBusinessApi.ts +61 -5
- package/src/retailWizard/api/RetailWizardBusinessApi.ts +35 -0
- package/src/retailWizard/api/interfaces/IRetailWizardBusinessApi.ts +62 -0
- package/src/retailWizard/index.ts +2 -0
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { IHttpRequest } from "@fiado/http-client";
|
|
1
|
+
import type { IHttpRequest } from "@fiado/http-client";
|
|
2
2
|
import { IDocumentGeneratorApi } from "./interfaces/IDocumentGeneratorApi.js";
|
|
3
3
|
import { DocumentGenerateRequest } from "@fiado/type-kit/bin/credit/dtos/internal/DocumentGenerateRequest.js";
|
|
4
4
|
import { DocumentGenerateResponse } from "@fiado/type-kit/bin/credit/dtos/internal/DocumentGenerateResponse.js";
|
|
@@ -37,17 +37,17 @@ let DocumentGeneratorApi = class DocumentGeneratorApi {
|
|
|
37
37
|
async getDocument(documentId) {
|
|
38
38
|
const url = `${this.baseUrl}document/${documentId}`;
|
|
39
39
|
const operationName = "documentGetDocument";
|
|
40
|
-
return await this.httpRequest.
|
|
40
|
+
return await this.httpRequest.get(url, undefined, { 'operationName': operationName });
|
|
41
41
|
}
|
|
42
42
|
async getDocumentUrl(documentId) {
|
|
43
43
|
const url = `${this.baseUrl}document/${documentId}/url`;
|
|
44
44
|
const operationName = "documentGetDocumentUrl";
|
|
45
|
-
return await this.httpRequest.
|
|
45
|
+
return await this.httpRequest.get(url, undefined, { 'operationName': operationName });
|
|
46
46
|
}
|
|
47
47
|
async getDocumentByReference(type, referenceId) {
|
|
48
48
|
const url = `${this.baseUrl}document/by-reference/${type}/${referenceId}`;
|
|
49
49
|
const operationName = "documentGetDocumentByReference";
|
|
50
|
-
return await this.httpRequest.
|
|
50
|
+
return await this.httpRequest.get(url, undefined, { 'operationName': operationName });
|
|
51
51
|
}
|
|
52
52
|
};
|
|
53
53
|
DocumentGeneratorApi = __decorate([
|
package/bin/index.d.ts
CHANGED
|
@@ -86,6 +86,7 @@ export * from "./retailCustomer/index.js";
|
|
|
86
86
|
export * from "./retailCatalog/index.js";
|
|
87
87
|
export * from "./retailCards/index.js";
|
|
88
88
|
export * from "./retailNotifications/index.js";
|
|
89
|
+
export * from "./retailWizard/index.js";
|
|
89
90
|
export * from "./shortlink/index.js";
|
|
90
91
|
export * from "./loanConfig/index.js";
|
|
91
92
|
export * from "./loanOfferings/index.js";
|
package/bin/index.js
CHANGED
|
@@ -86,6 +86,7 @@ export * from "./retailCustomer/index.js";
|
|
|
86
86
|
export * from "./retailCatalog/index.js";
|
|
87
87
|
export * from "./retailCards/index.js";
|
|
88
88
|
export * from "./retailNotifications/index.js";
|
|
89
|
+
export * from "./retailWizard/index.js";
|
|
89
90
|
export * from "./shortlink/index.js";
|
|
90
91
|
export * from "./loanConfig/index.js";
|
|
91
92
|
export * from "./loanOfferings/index.js";
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { ILegalDocumentApi } from "./interfaces/ILegalDocumentApi.js";
|
|
2
|
-
import { IHttpRequest } from "@fiado/http-client";
|
|
2
|
+
import type { IHttpRequest } from "@fiado/http-client";
|
|
3
3
|
export declare class LegalDocumentApi implements ILegalDocumentApi {
|
|
4
4
|
private httpRequest;
|
|
5
5
|
private readonly baseUrl;
|
|
6
6
|
constructor(httpRequest: IHttpRequest);
|
|
7
7
|
getPendingSign(directoryId: string): Promise<any>;
|
|
8
8
|
getDirectoryAcceptanceByDirectoryId(directoryId: string): Promise<any>;
|
|
9
|
+
getDocumentByType(type: string): Promise<any>;
|
|
9
10
|
}
|
|
@@ -31,6 +31,13 @@ let LegalDocumentApi = class LegalDocumentApi {
|
|
|
31
31
|
const url = `${this.baseUrl}directories/${directoryId}`;
|
|
32
32
|
return await this.httpRequest.get(url);
|
|
33
33
|
}
|
|
34
|
+
async getDocumentByType(type) {
|
|
35
|
+
if (!type) {
|
|
36
|
+
throw new Error("Document type is required.");
|
|
37
|
+
}
|
|
38
|
+
const url = `${this.baseUrl}documents/types/${encodeURIComponent(type)}`;
|
|
39
|
+
return await this.httpRequest.get(url);
|
|
40
|
+
}
|
|
34
41
|
};
|
|
35
42
|
LegalDocumentApi = __decorate([
|
|
36
43
|
injectable(),
|
|
@@ -1,4 +1,9 @@
|
|
|
1
1
|
export interface ILegalDocumentApi {
|
|
2
2
|
getPendingSign(directoryId: string): Promise<any>;
|
|
3
3
|
getDirectoryAcceptanceByDirectoryId(directoryId: string): Promise<any>;
|
|
4
|
+
/**
|
|
5
|
+
* Documento legal vigente de un tipo (PRIVACY_PAY, etc.).
|
|
6
|
+
* Devuelve el documento con su URL firmada y el vencimiento de esa URL.
|
|
7
|
+
*/
|
|
8
|
+
getDocumentByType(type: string): Promise<any>;
|
|
4
9
|
}
|
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
import { LoanCreditStatusEnum } from "@fiado/type-kit/bin/loanCredit/index.js";
|
|
1
|
+
import { LoanCreditStatusEnum, LoanInstallmentStatusEnum } from "@fiado/type-kit/bin/loanCredit/index.js";
|
|
2
2
|
import { ClientLevelEnum } from "@fiado/type-kit/bin/loanScoring/index.js";
|
|
3
3
|
/** Body de `POST /private/credits/analytics`. Hasta 500 ids por request; el caller pagina. */
|
|
4
4
|
export interface LoanCreditAnalyticsBatchRequest {
|
|
5
5
|
creditIds: string[];
|
|
6
6
|
}
|
|
7
7
|
/**
|
|
8
|
-
* Lo mínimo de un crédito para contar: eje de riesgo, plan, cifras del enganche, estado y
|
|
9
|
-
* CERO PII — ni acreditado, ni nombre, ni teléfono.
|
|
8
|
+
* Lo mínimo de un crédito para contar: eje de riesgo, plan, cifras del enganche, estado, fechas y
|
|
9
|
+
* el estado de su primera cuota. CERO PII — ni acreditado, ni nombre, ni teléfono.
|
|
10
10
|
*/
|
|
11
11
|
export interface LoanCreditAnalyticsItem {
|
|
12
12
|
creditId: string;
|
|
@@ -19,7 +19,15 @@ export interface LoanCreditAnalyticsItem {
|
|
|
19
19
|
equipmentPriceCents: number;
|
|
20
20
|
downPaymentCents: number;
|
|
21
21
|
status: LoanCreditStatusEnum;
|
|
22
|
+
/** Instante de activación; ausente mientras el crédito no se activó. */
|
|
23
|
+
activatedAt?: string;
|
|
22
24
|
createdAt: string;
|
|
25
|
+
/** Estado de la cuota 1; ausente si el crédito no tiene plan o la cuota no se pudo leer. */
|
|
26
|
+
firstInstallmentStatus?: LoanInstallmentStatusEnum;
|
|
27
|
+
/** Vencimiento de la cuota 1 (YYYY-MM-DD); ausente hasta que se activa el crédito. */
|
|
28
|
+
firstInstallmentDueDate?: string;
|
|
29
|
+
/** Instante en que se pagó la cuota 1; ausente mientras no se pagó. */
|
|
30
|
+
firstInstallmentPaidAt?: string;
|
|
23
31
|
}
|
|
24
32
|
/**
|
|
25
33
|
* Los ids que no existen simplemente no vuelven; los que DynamoDB no alcanzó a leer salen
|
|
@@ -28,4 +36,9 @@ export interface LoanCreditAnalyticsItem {
|
|
|
28
36
|
export interface LoanCreditAnalyticsBatchResponse {
|
|
29
37
|
items: LoanCreditAnalyticsItem[];
|
|
30
38
|
unresolvedCreditIds: string[];
|
|
39
|
+
/**
|
|
40
|
+
* Créditos cuya PRIMERA CUOTA no se pudo leer; sus ítems vuelven sin los campos
|
|
41
|
+
* `firstInstallment*`. Opcional: las versiones desplegadas antes de la cosecha no lo mandan.
|
|
42
|
+
*/
|
|
43
|
+
unresolvedFirstInstallmentCreditIds?: string[];
|
|
31
44
|
}
|
|
@@ -59,11 +59,13 @@ export interface ILoanCreditBusinessApi {
|
|
|
59
59
|
getBorrowerByRetailCustomer(retailCustomerId: string, tenantId: string): Promise<StandardResponse<LoanBorrowerResponse>>;
|
|
60
60
|
/**
|
|
61
61
|
* Lee créditos EN LOTE por su id y devuelve solo lo que se cuenta: el eje de riesgo congelado
|
|
62
|
-
* al originar, el plan, las cifras del enganche, el estado
|
|
62
|
+
* al originar, el plan, las cifras del enganche, el estado, las fechas de originación y
|
|
63
|
+
* activación, y el estado de su PRIMERA cuota (la cosecha de primera cuota). **Cero PII.**
|
|
63
64
|
*
|
|
64
65
|
* Se resuelve por BatchGetItem sobre la clave primaria, así que el orden de salida NO es el de
|
|
65
66
|
* entrada. Un id que no existe no vuelve; uno que DynamoDB no alcanzó a leer vuelve nombrado en
|
|
66
|
-
* `unresolvedCreditIds` — nunca se confunde «no pude leerlo» con «no existe».
|
|
67
|
+
* `unresolvedCreditIds` — nunca se confunde «no pude leerlo» con «no existe». El crédito cuya
|
|
68
|
+
* cuota 1 no se pudo leer sale en `unresolvedFirstInstallmentCreditIds`, sin campos de cuota.
|
|
67
69
|
* `400 VALIDATION_ERROR` si el lote pasa de 500 ids o trae uno que no es ULID.
|
|
68
70
|
*/
|
|
69
71
|
batchGetCreditAnalytics(input: LoanCreditAnalyticsBatchRequest, tenantId: string): Promise<StandardResponse<LoanCreditAnalyticsBatchResponse>>;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { IHttpRequest } from "@fiado/http-client";
|
|
2
2
|
import { StandardResponse } from "@fiado/gateway-adapter";
|
|
3
|
-
import { IRetailCatalogBusinessApi, ReleaseReservationBody, ReserveInventoryBody, ReserveInventoryResult, SellInventoryBody, StoreCatalogItem } from "./interfaces/IRetailCatalogBusinessApi.js";
|
|
3
|
+
import { ImeiProductLookupResponse, IRetailCatalogBusinessApi, ReleaseReservationBody, ReserveInventoryBody, ReserveInventoryResult, SellInventoryBody, StoreCatalogItem } from "./interfaces/IRetailCatalogBusinessApi.js";
|
|
4
4
|
/**
|
|
5
5
|
* Publisher HTTP del lambda `retail-catalog-business` (SureKeep Fase 1, pista Retail) para sus
|
|
6
6
|
* endpoints privados del ciclo de venta del IMEI. Contrato y semántica completos →
|
|
@@ -20,6 +20,7 @@ export default class RetailCatalogBusinessApi implements IRetailCatalogBusinessA
|
|
|
20
20
|
constructor(httpRequest: IHttpRequest);
|
|
21
21
|
private tenantHeader;
|
|
22
22
|
getStoreCatalog(storeId: string, issuer: string): Promise<StandardResponse<StoreCatalogItem[]>>;
|
|
23
|
+
getProductsByImei(imeis: string[], tenantId: string): Promise<StandardResponse<ImeiProductLookupResponse>>;
|
|
23
24
|
reserveInventory(imei: string, body: ReserveInventoryBody, issuer: string): Promise<StandardResponse<ReserveInventoryResult>>;
|
|
24
25
|
sellInventory(imei: string, body: SellInventoryBody, issuer: string): Promise<StandardResponse<void>>;
|
|
25
26
|
releaseInventory(imei: string, body: ReleaseReservationBody, issuer: string): Promise<StandardResponse<void>>;
|
|
@@ -11,6 +11,7 @@ var __param = (this && this.__param) || function (paramIndex, decorator) {
|
|
|
11
11
|
return function (target, key) { decorator(target, key, paramIndex); }
|
|
12
12
|
};
|
|
13
13
|
import { inject, injectable } from "inversify";
|
|
14
|
+
import { IMEI_PRODUCT_BATCH_MAX, } from "./interfaces/IRetailCatalogBusinessApi.js";
|
|
14
15
|
/** Header por el que el destino resuelve el silo (issuer Cognito forwardeado por el caller). */
|
|
15
16
|
const TENANT_ISSUER_HEADER = "x-tenant-issuer";
|
|
16
17
|
/**
|
|
@@ -40,6 +41,16 @@ let RetailCatalogBusinessApi = class RetailCatalogBusinessApi {
|
|
|
40
41
|
const url = `${this.baseUrl}/private/stores/${encodeURIComponent(storeId)}/catalog`;
|
|
41
42
|
return await this.httpRequest.get(url, undefined, this.tenantHeader(issuer));
|
|
42
43
|
}
|
|
44
|
+
async getProductsByImei(imeis, tenantId) {
|
|
45
|
+
// Se corta acá: mandar un lote que el destino va a rechazar solo gasta un round-trip.
|
|
46
|
+
if (imeis.length > IMEI_PRODUCT_BATCH_MAX) {
|
|
47
|
+
throw new Error(`El lote admite hasta ${IMEI_PRODUCT_BATCH_MAX} IMEIs y llegaron ${imeis.length}`);
|
|
48
|
+
}
|
|
49
|
+
// `URLSearchParams` codifica la coma del separador; el destino hace `split(',')` sobre el
|
|
50
|
+
// valor ya decodificado por API Gateway.
|
|
51
|
+
const query = new URLSearchParams({ imeis: imeis.join(","), tenantId });
|
|
52
|
+
return await this.httpRequest.get(`${this.baseUrl}/private/products/by-imei?${query.toString()}`);
|
|
53
|
+
}
|
|
43
54
|
async reserveInventory(imei, body, issuer) {
|
|
44
55
|
const url = `${this.baseUrl}/private/inventory/${encodeURIComponent(imei)}/reserve`;
|
|
45
56
|
return await this.httpRequest.put(url, body, this.tenantHeader(issuer));
|
|
@@ -5,12 +5,17 @@ import type { MdmLockModeEnum, MdmProviderEnum } from "@fiado/type-kit/bin/retai
|
|
|
5
5
|
* para sus endpoints privados del ciclo de venta del IMEI, consumidos por el wizard F2.
|
|
6
6
|
*
|
|
7
7
|
* ─────────────────────────────────────────────────────────────────────────────
|
|
8
|
-
* TENANT —
|
|
8
|
+
* TENANT — DOS convenciones conviven acá. Mirá la firma de cada método.
|
|
9
9
|
* ─────────────────────────────────────────────────────────────────────────────
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
* `
|
|
10
|
+
* Los cinco métodos del ciclo de venta del IMEI toman `issuer` y lo mandan en el header
|
|
11
|
+
* `x-tenant-issuer` (`PrivateController.issuerFromHeader` → `TenantContextService.withContext`).
|
|
12
|
+
* `getProductsByImei` toma `tenantId` y lo manda por `?tenantId=`
|
|
13
|
+
* (→ `TenantContextService.withContextByTenantId`), porque su consumidor es un backoffice que no
|
|
14
|
+
* tiene el issuer Cognito. Los privados son `Feature.ANONIMUS` (sin RBAC; la protección es la red
|
|
15
|
+
* VPC), así que el caller DEBE declarar uno de los dos — no hay default.
|
|
16
|
+
*
|
|
17
|
+
* TD: converger los cinco del ciclo de venta a `?tenantId=`, el camino canónico para
|
|
18
|
+
* service-to-service. No se migran acá para no romper al wizard, que ya consume el header.
|
|
14
19
|
*
|
|
15
20
|
* ⚠️ Retorno `StandardResponse<T>` — el consumer lee `result.data`, NO `result.body`.
|
|
16
21
|
* Errores: el destino lanza `DomainError` tipado → el publisher RECHAZA (no devuelve null); cada
|
|
@@ -53,6 +58,28 @@ export interface StoreCatalogItem {
|
|
|
53
58
|
/** Modo de bloqueo pactado; `null` si el producto no lleva MDM. */
|
|
54
59
|
mdmLockMode: MdmLockModeEnum | null;
|
|
55
60
|
}
|
|
61
|
+
/** Techo de IMEIs por request del lote IMEI->producto. Lo declara y lo honra el destino. */
|
|
62
|
+
export declare const IMEI_PRODUCT_BATCH_MAX = 100;
|
|
63
|
+
/** Por que se pudo (o no) resolver el producto de un IMEI del lote. */
|
|
64
|
+
export type ImeiProductLookupStatus = "OK" | "NOT_FOUND" | "UNAVAILABLE" | "INVALID_ID";
|
|
65
|
+
/** Una entrada del lote IMEI->producto (espeja `ImeiProductLookupEntry` del destino). */
|
|
66
|
+
export interface ImeiProductLookupEntry {
|
|
67
|
+
imei: string;
|
|
68
|
+
/**
|
|
69
|
+
* `OK` trae sku/brand/model · `NOT_FOUND` el catalogo no llega al producto (dato POSITIVO) ·
|
|
70
|
+
* `UNAVAILABLE` no se pudo leer ese IMEI, reintentalo · `INVALID_ID` no tiene forma de IMEI.
|
|
71
|
+
*/
|
|
72
|
+
status: ImeiProductLookupStatus;
|
|
73
|
+
sku?: string;
|
|
74
|
+
/** Marca comercial (p. ej. "Apple"). Solo viene con `status: OK`. */
|
|
75
|
+
brand?: string;
|
|
76
|
+
/** Modelo (p. ej. "iPhone 15 Pro"). Solo viene con `status: OK`. */
|
|
77
|
+
model?: string;
|
|
78
|
+
}
|
|
79
|
+
/** Respuesta del lote: UNA entrada por IMEI pedido, en el MISMO orden. */
|
|
80
|
+
export interface ImeiProductLookupResponse {
|
|
81
|
+
items: ImeiProductLookupEntry[];
|
|
82
|
+
}
|
|
56
83
|
/** Resultado de la reserva (espeja `ReserveInventoryResponse` del destino). */
|
|
57
84
|
export interface ReserveInventoryResult {
|
|
58
85
|
reservationExpiresAt: string;
|
|
@@ -74,6 +101,30 @@ export interface ReleaseReservationBody {
|
|
|
74
101
|
export interface IRetailCatalogBusinessApi {
|
|
75
102
|
/** GET `/private/stores/{storeId}/catalog` — catálogo vendible (sku, imei, precio de contado, disponibilidad). */
|
|
76
103
|
getStoreCatalog(storeId: string, issuer: string): Promise<StandardResponse<StoreCatalogItem[]>>;
|
|
104
|
+
/**
|
|
105
|
+
* GET `/private/products/by-imei?imeis=&tenantId=` — resuelve un lote de IMEIs a su producto
|
|
106
|
+
* (sku + marca + modelo). Para los lambdas que del equipo solo guardan el IMEI.
|
|
107
|
+
*
|
|
108
|
+
* ─────────────────────────────────────────────────────────────────────────────
|
|
109
|
+
* ⚠️ El tenant va por `?tenantId=`, NO por el header `x-tenant-issuer` del resto de este publisher.
|
|
110
|
+
* ─────────────────────────────────────────────────────────────────────────────
|
|
111
|
+
* El consumidor es un backoffice que solo tiene el `tenantId` de su contexto; el issuer Cognito
|
|
112
|
+
* no existe de su lado. El destino entra por `TenantContextService.withContextByTenantId`, con las
|
|
113
|
+
* mismas garantías (mismo diccionario, AssumeRole del silo, fail-closed sin tenant).
|
|
114
|
+
*
|
|
115
|
+
* ── El contrato del lote ────────────────────────────────────────────────────────────────────
|
|
116
|
+
* Devuelve UNA entrada por cada IMEI pedido, en el MISMO orden — el caller nunca reconcilia. Los
|
|
117
|
+
* repetidos se consultan una sola vez pero salen repetidos. Techo de `IMEI_PRODUCT_BATCH_MAX`
|
|
118
|
+
* IMEIs por request; el caller parte los lotes mayores.
|
|
119
|
+
*
|
|
120
|
+
* Ningún IMEI tumba el lote: el mal formado sale `INVALID_ID`, el que el catálogo no alcanza a
|
|
121
|
+
* resolver sale `NOT_FOUND` y el que DynamoDB no llegó a leer sale `UNAVAILABLE` (reintentable).
|
|
122
|
+
* Solo un fallo total del destino rechaza la promesa.
|
|
123
|
+
*
|
|
124
|
+
* @throws Error si el lote supera el techo (se corta acá, sin salir a la red) ·
|
|
125
|
+
* `400 IMEI_BATCH_REQUIRED` si `imeis` viene vacío · `400 UNKNOWN_TENANT` sin `tenantId`.
|
|
126
|
+
*/
|
|
127
|
+
getProductsByImei(imeis: string[], tenantId: string): Promise<StandardResponse<ImeiProductLookupResponse>>;
|
|
77
128
|
/** PUT `/private/inventory/{imei}/reserve` — reserva atómica del IMEI para una sesión (AVAILABLE → RESERVED). */
|
|
78
129
|
reserveInventory(imei: string, body: ReserveInventoryBody, issuer: string): Promise<StandardResponse<ReserveInventoryResult>>;
|
|
79
130
|
/** PUT `/private/inventory/{imei}/sell` — vende el IMEI (RESERVED → SOLD). */
|
|
@@ -1 +1,2 @@
|
|
|
1
|
-
|
|
1
|
+
/** Techo de IMEIs por request del lote IMEI->producto. Lo declara y lo honra el destino. */
|
|
2
|
+
export const IMEI_PRODUCT_BATCH_MAX = 100;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { IHttpRequest } from "@fiado/http-client";
|
|
2
|
+
import { StandardResponse } from "@fiado/gateway-adapter";
|
|
3
|
+
import { WizardSessionLookupResponse } from "@fiado/type-kit/bin/retailWizard/index.js";
|
|
4
|
+
import { IRetailWizardBusinessApi } from "./interfaces/IRetailWizardBusinessApi.js";
|
|
5
|
+
/**
|
|
6
|
+
* Publisher HTTP del lambda `retail-wizard-business` (componente 05 SureKeep Fase 2) para sus
|
|
7
|
+
* endpoints privados. Contrato y semantica completos -> `IRetailWizardBusinessApi`.
|
|
8
|
+
*
|
|
9
|
+
* Env var requerida en el consumer: `RETAIL_WIZARD_BUSINESS_URL`.
|
|
10
|
+
* El template.yml del consumer la setea con:
|
|
11
|
+
*
|
|
12
|
+
* RETAIL_WIZARD_BUSINESS_URL: '{{resolve:ssm:retail-wizard-business}}'
|
|
13
|
+
*/
|
|
14
|
+
export default class RetailWizardBusinessApi implements IRetailWizardBusinessApi {
|
|
15
|
+
private httpRequest;
|
|
16
|
+
private readonly baseUrl;
|
|
17
|
+
constructor(httpRequest: IHttpRequest);
|
|
18
|
+
/**
|
|
19
|
+
* Traduce un lote de `wizardSessionId` a tienda / cliente / equipo. Contrato completo (los cuatro
|
|
20
|
+
* estados, el techo de 100 y lo que NO devuelve) -> `IRetailWizardBusinessApi`.
|
|
21
|
+
*/
|
|
22
|
+
getWizardSessionsByIds(wizardSessionIds: string[], tenantId: string): Promise<StandardResponse<WizardSessionLookupResponse>>;
|
|
23
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
2
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
3
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
4
|
+
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;
|
|
5
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
6
|
+
};
|
|
7
|
+
var __metadata = (this && this.__metadata) || function (k, v) {
|
|
8
|
+
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
9
|
+
};
|
|
10
|
+
var __param = (this && this.__param) || function (paramIndex, decorator) {
|
|
11
|
+
return function (target, key) { decorator(target, key, paramIndex); }
|
|
12
|
+
};
|
|
13
|
+
import { inject, injectable } from "inversify";
|
|
14
|
+
/**
|
|
15
|
+
* Publisher HTTP del lambda `retail-wizard-business` (componente 05 SureKeep Fase 2) para sus
|
|
16
|
+
* endpoints privados. Contrato y semantica completos -> `IRetailWizardBusinessApi`.
|
|
17
|
+
*
|
|
18
|
+
* Env var requerida en el consumer: `RETAIL_WIZARD_BUSINESS_URL`.
|
|
19
|
+
* El template.yml del consumer la setea con:
|
|
20
|
+
*
|
|
21
|
+
* RETAIL_WIZARD_BUSINESS_URL: '{{resolve:ssm:retail-wizard-business}}'
|
|
22
|
+
*/
|
|
23
|
+
let RetailWizardBusinessApi = class RetailWizardBusinessApi {
|
|
24
|
+
httpRequest;
|
|
25
|
+
baseUrl = process.env.RETAIL_WIZARD_BUSINESS_URL || "";
|
|
26
|
+
constructor(httpRequest) {
|
|
27
|
+
this.httpRequest = httpRequest;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Traduce un lote de `wizardSessionId` a tienda / cliente / equipo. Contrato completo (los cuatro
|
|
31
|
+
* estados, el techo de 100 y lo que NO devuelve) -> `IRetailWizardBusinessApi`.
|
|
32
|
+
*/
|
|
33
|
+
async getWizardSessionsByIds(wizardSessionIds, tenantId) {
|
|
34
|
+
// `URLSearchParams` codifica la coma del separador; el lambda destino hace `split(',')`
|
|
35
|
+
// sobre el valor ya decodificado por API Gateway.
|
|
36
|
+
const query = new URLSearchParams({ ids: wizardSessionIds.join(","), tenantId });
|
|
37
|
+
return await this.httpRequest.get(`${this.baseUrl}/private/wizard-sessions/batch?${query.toString()}`);
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
RetailWizardBusinessApi = __decorate([
|
|
41
|
+
injectable(),
|
|
42
|
+
__param(0, inject("IHttpRequest")),
|
|
43
|
+
__metadata("design:paramtypes", [Object])
|
|
44
|
+
], RetailWizardBusinessApi);
|
|
45
|
+
export default RetailWizardBusinessApi;
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { StandardResponse } from "@fiado/gateway-adapter";
|
|
2
|
+
import { WizardSessionLookupResponse } from "@fiado/type-kit/bin/retailWizard/index.js";
|
|
3
|
+
/**
|
|
4
|
+
* Contrato del publisher HTTP del lambda `retail-wizard-business` (componente 05 SureKeep Fase 2)
|
|
5
|
+
* para sus endpoints privados (service-to-service, VPC-only).
|
|
6
|
+
*
|
|
7
|
+
* Consumidor previsto: `loan-credit-business`, que guarda `wizardSessionId` en cada credito y sin
|
|
8
|
+
* esto no puede traducirlo a la tienda, el cliente ni el equipo de la venta (columnas de Cartera).
|
|
9
|
+
*
|
|
10
|
+
* ─────────────────────────────────────────────────────────────────────────────
|
|
11
|
+
* `tenantId` es OBLIGATORIO — no es un detalle de firma.
|
|
12
|
+
* ─────────────────────────────────────────────────────────────────────────────
|
|
13
|
+
* Los privados NO llevan token de usuario (`Feature.ANONIMUS`; la proteccion es la red VPC), asi que
|
|
14
|
+
* el lambda destino no tiene `AuthContext.issuer` y NO puede inferir el silo. El caller declara el
|
|
15
|
+
* tenant explicito por query. Sin `tenantId` -> `400 UNKNOWN_TENANT` (fail-closed). Nunca hay
|
|
16
|
+
* default: caer a un tenant seria fail-OPEN y podria devolver datos de otra SOFOM.
|
|
17
|
+
*
|
|
18
|
+
* ⚠️ Patron de retorno: `StandardResponse<T>` — el consumer accede `result.data`, NO `result.body`.
|
|
19
|
+
* `@fiado/http-client` entrega el BODY PELADO; `.statusCode`/`.body` no existen en runtime.
|
|
20
|
+
*
|
|
21
|
+
* Env var requerida en el consumer: `RETAIL_WIZARD_BUSINESS_URL`.
|
|
22
|
+
* El template.yml del consumer la setea con:
|
|
23
|
+
*
|
|
24
|
+
* RETAIL_WIZARD_BUSINESS_URL: '{{resolve:ssm:retail-wizard-business}}'
|
|
25
|
+
*
|
|
26
|
+
* Convencion CLAUDE.md global: SSM key = nombre del lambda owner de la URL.
|
|
27
|
+
*/
|
|
28
|
+
export interface IRetailWizardBusinessApi {
|
|
29
|
+
/**
|
|
30
|
+
* `GET /private/wizard-sessions/batch?ids=` — traduce un lote de `wizardSessionId` a la tienda,
|
|
31
|
+
* el cliente y el equipo de cada venta.
|
|
32
|
+
*
|
|
33
|
+
* ── 🔴 Por que el lote va por SESION y no por venta ──────────────────────────────────────────
|
|
34
|
+
* `<T>RetailSale_GT` tiene 5 GSIs y ninguno por `creditId` ni por `wizardSessionId`: entrar por
|
|
35
|
+
* la venta exigiria un indice nuevo (CloudFormation). La sesion se lee por su `pk` directo.
|
|
36
|
+
*
|
|
37
|
+
* ── El contrato del lote (identico a `GET /borrowers/portfolio`, DEC-010) ────────────────────
|
|
38
|
+
* Devuelve UNA entrada por cada id pedido, en el MISMO orden — el caller nunca reconcilia. Los
|
|
39
|
+
* ids repetidos se consultan una sola vez pero salen repetidos en la respuesta. Techo de 100 ids
|
|
40
|
+
* por request. Un fallo parcial NO tumba el lote: esa entrada sale `UNAVAILABLE` y el resto
|
|
41
|
+
* responde.
|
|
42
|
+
*
|
|
43
|
+
* Cuatro estados por entrada (`WizardSessionLookupStatusEnum`): `OK` se leyo completo ·
|
|
44
|
+
* `NOT_FOUND` no existe (dato POSITIVO) · `UNAVAILABLE` no se pudo leer, reintenta ese id ·
|
|
45
|
+
* `INVALID_ID` no tiene forma de id, nunca reintentar.
|
|
46
|
+
*
|
|
47
|
+
* ── ⚠️ Lo que este endpoint NO devuelve ──────────────────────────────────────────────────────
|
|
48
|
+
* NO trae el NOMBRE del cliente, ni el de la tienda, ni el del vendedor: la sesion del wizard no
|
|
49
|
+
* los guarda (solo ids). El caller los resuelve con `retail-customer` y `retail-org`. Tampoco
|
|
50
|
+
* viaja el telefono, que es PII. La marca y el modelo del equipo SI vienen: la sesion los congela
|
|
51
|
+
* al reservar.
|
|
52
|
+
*
|
|
53
|
+
* @throws `400 WIZARD_SESSION_IDS_REQUIRED` si `ids` viene vacio o ausente ·
|
|
54
|
+
* `400 WIZARD_SESSION_BATCH_TOO_LARGE` si supera los 100 ids ·
|
|
55
|
+
* `400 UNKNOWN_TENANT` sin `tenantId`.
|
|
56
|
+
*/
|
|
57
|
+
getWizardSessionsByIds(wizardSessionIds: string[], tenantId: string): Promise<StandardResponse<WizardSessionLookupResponse>>;
|
|
58
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fiado/api-invoker",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.44.0",
|
|
4
4
|
"description": "Sirve como un puente entre diferentes funciones lambda, facilitando la comunicación entre ellas a través de invocaciones http",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "bin/index.js",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import dotenv from 'dotenv';
|
|
2
2
|
import { inject, injectable } from "inversify";
|
|
3
|
-
import { IHttpRequest } from "@fiado/http-client";
|
|
3
|
+
import type { IHttpRequest } from "@fiado/http-client";
|
|
4
4
|
import { IDocumentGeneratorApi } from "./interfaces/IDocumentGeneratorApi.js";
|
|
5
5
|
import { DocumentGenerateRequest } from "@fiado/type-kit/bin/credit/dtos/internal/DocumentGenerateRequest.js";
|
|
6
6
|
import { DocumentGenerateResponse } from "@fiado/type-kit/bin/credit/dtos/internal/DocumentGenerateResponse.js";
|
|
@@ -34,18 +34,18 @@ export class DocumentGeneratorApi implements IDocumentGeneratorApi {
|
|
|
34
34
|
public async getDocument(documentId: string): Promise<DocumentGenerateResponse> {
|
|
35
35
|
const url = `${this.baseUrl}document/${documentId}`;
|
|
36
36
|
const operationName = "documentGetDocument";
|
|
37
|
-
return await this.httpRequest.
|
|
37
|
+
return await this.httpRequest.get(url, undefined, { 'operationName': operationName });
|
|
38
38
|
}
|
|
39
39
|
|
|
40
40
|
public async getDocumentUrl(documentId: string): Promise<{ url: string; expiresAt: string }> {
|
|
41
41
|
const url = `${this.baseUrl}document/${documentId}/url`;
|
|
42
42
|
const operationName = "documentGetDocumentUrl";
|
|
43
|
-
return await this.httpRequest.
|
|
43
|
+
return await this.httpRequest.get(url, undefined, { 'operationName': operationName });
|
|
44
44
|
}
|
|
45
45
|
|
|
46
46
|
public async getDocumentByReference(type: string, referenceId: string): Promise<DocumentGenerateResponse[]> {
|
|
47
47
|
const url = `${this.baseUrl}document/by-reference/${type}/${referenceId}`;
|
|
48
48
|
const operationName = "documentGetDocumentByReference";
|
|
49
|
-
return await this.httpRequest.
|
|
49
|
+
return await this.httpRequest.get(url, undefined, { 'operationName': operationName });
|
|
50
50
|
}
|
|
51
51
|
}
|
package/src/index.ts
CHANGED
|
@@ -86,6 +86,7 @@ export * from "./retailCustomer/index.js";
|
|
|
86
86
|
export * from "./retailCatalog/index.js";
|
|
87
87
|
export * from "./retailCards/index.js";
|
|
88
88
|
export * from "./retailNotifications/index.js";
|
|
89
|
+
export * from "./retailWizard/index.js";
|
|
89
90
|
export * from "./shortlink/index.js";
|
|
90
91
|
export * from "./loanConfig/index.js";
|
|
91
92
|
export * from "./loanOfferings/index.js";
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { inject, injectable } from "inversify";
|
|
2
2
|
import { ILegalDocumentApi } from "./interfaces/ILegalDocumentApi.js";
|
|
3
|
-
import { IHttpRequest } from "@fiado/http-client";
|
|
3
|
+
import type { IHttpRequest } from "@fiado/http-client";
|
|
4
4
|
|
|
5
5
|
|
|
6
6
|
|
|
@@ -34,5 +34,16 @@ export class LegalDocumentApi implements ILegalDocumentApi {
|
|
|
34
34
|
|
|
35
35
|
return await this.httpRequest.get<any>(url);
|
|
36
36
|
}
|
|
37
|
+
|
|
38
|
+
async getDocumentByType(type: string): Promise<any> {
|
|
39
|
+
|
|
40
|
+
if (!type) {
|
|
41
|
+
throw new Error("Document type is required.")
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const url = `${this.baseUrl}documents/types/${encodeURIComponent(type)}`;
|
|
45
|
+
|
|
46
|
+
return await this.httpRequest.get<any>(url);
|
|
47
|
+
}
|
|
37
48
|
}
|
|
38
49
|
|
|
@@ -5,4 +5,10 @@ export interface ILegalDocumentApi {
|
|
|
5
5
|
getPendingSign(directoryId: string): Promise<any>;
|
|
6
6
|
getDirectoryAcceptanceByDirectoryId(directoryId: string): Promise<any>;
|
|
7
7
|
|
|
8
|
+
/**
|
|
9
|
+
* Documento legal vigente de un tipo (PRIVACY_PAY, etc.).
|
|
10
|
+
* Devuelve el documento con su URL firmada y el vencimiento de esa URL.
|
|
11
|
+
*/
|
|
12
|
+
getDocumentByType(type: string): Promise<any>;
|
|
13
|
+
|
|
8
14
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { LoanCreditStatusEnum } from "@fiado/type-kit/bin/loanCredit/index.js";
|
|
1
|
+
import { LoanCreditStatusEnum, LoanInstallmentStatusEnum } from "@fiado/type-kit/bin/loanCredit/index.js";
|
|
2
2
|
import { ClientLevelEnum } from "@fiado/type-kit/bin/loanScoring/index.js";
|
|
3
3
|
|
|
4
4
|
/** Body de `POST /private/credits/analytics`. Hasta 500 ids por request; el caller pagina. */
|
|
@@ -7,8 +7,8 @@ export interface LoanCreditAnalyticsBatchRequest {
|
|
|
7
7
|
}
|
|
8
8
|
|
|
9
9
|
/**
|
|
10
|
-
* Lo mínimo de un crédito para contar: eje de riesgo, plan, cifras del enganche, estado y
|
|
11
|
-
* CERO PII — ni acreditado, ni nombre, ni teléfono.
|
|
10
|
+
* Lo mínimo de un crédito para contar: eje de riesgo, plan, cifras del enganche, estado, fechas y
|
|
11
|
+
* el estado de su primera cuota. CERO PII — ni acreditado, ni nombre, ni teléfono.
|
|
12
12
|
*/
|
|
13
13
|
export interface LoanCreditAnalyticsItem {
|
|
14
14
|
creditId: string;
|
|
@@ -21,7 +21,15 @@ export interface LoanCreditAnalyticsItem {
|
|
|
21
21
|
equipmentPriceCents: number;
|
|
22
22
|
downPaymentCents: number;
|
|
23
23
|
status: LoanCreditStatusEnum;
|
|
24
|
+
/** Instante de activación; ausente mientras el crédito no se activó. */
|
|
25
|
+
activatedAt?: string;
|
|
24
26
|
createdAt: string;
|
|
27
|
+
/** Estado de la cuota 1; ausente si el crédito no tiene plan o la cuota no se pudo leer. */
|
|
28
|
+
firstInstallmentStatus?: LoanInstallmentStatusEnum;
|
|
29
|
+
/** Vencimiento de la cuota 1 (YYYY-MM-DD); ausente hasta que se activa el crédito. */
|
|
30
|
+
firstInstallmentDueDate?: string;
|
|
31
|
+
/** Instante en que se pagó la cuota 1; ausente mientras no se pagó. */
|
|
32
|
+
firstInstallmentPaidAt?: string;
|
|
25
33
|
}
|
|
26
34
|
|
|
27
35
|
/**
|
|
@@ -31,4 +39,9 @@ export interface LoanCreditAnalyticsItem {
|
|
|
31
39
|
export interface LoanCreditAnalyticsBatchResponse {
|
|
32
40
|
items: LoanCreditAnalyticsItem[];
|
|
33
41
|
unresolvedCreditIds: string[];
|
|
42
|
+
/**
|
|
43
|
+
* Créditos cuya PRIMERA CUOTA no se pudo leer; sus ítems vuelven sin los campos
|
|
44
|
+
* `firstInstallment*`. Opcional: las versiones desplegadas antes de la cosecha no lo mandan.
|
|
45
|
+
*/
|
|
46
|
+
unresolvedFirstInstallmentCreditIds?: string[];
|
|
34
47
|
}
|
|
@@ -106,11 +106,13 @@ export interface ILoanCreditBusinessApi {
|
|
|
106
106
|
|
|
107
107
|
/**
|
|
108
108
|
* Lee créditos EN LOTE por su id y devuelve solo lo que se cuenta: el eje de riesgo congelado
|
|
109
|
-
* al originar, el plan, las cifras del enganche, el estado
|
|
109
|
+
* al originar, el plan, las cifras del enganche, el estado, las fechas de originación y
|
|
110
|
+
* activación, y el estado de su PRIMERA cuota (la cosecha de primera cuota). **Cero PII.**
|
|
110
111
|
*
|
|
111
112
|
* Se resuelve por BatchGetItem sobre la clave primaria, así que el orden de salida NO es el de
|
|
112
113
|
* entrada. Un id que no existe no vuelve; uno que DynamoDB no alcanzó a leer vuelve nombrado en
|
|
113
|
-
* `unresolvedCreditIds` — nunca se confunde «no pude leerlo» con «no existe».
|
|
114
|
+
* `unresolvedCreditIds` — nunca se confunde «no pude leerlo» con «no existe». El crédito cuya
|
|
115
|
+
* cuota 1 no se pudo leer sale en `unresolvedFirstInstallmentCreditIds`, sin campos de cuota.
|
|
114
116
|
* `400 VALIDATION_ERROR` si el lote pasa de 500 ids o trae uno que no es ULID.
|
|
115
117
|
*/
|
|
116
118
|
batchGetCreditAnalytics(
|
|
@@ -2,6 +2,8 @@ import { inject, injectable } from "inversify";
|
|
|
2
2
|
import type { IHttpRequest } from "@fiado/http-client";
|
|
3
3
|
import { StandardResponse } from "@fiado/gateway-adapter";
|
|
4
4
|
import {
|
|
5
|
+
IMEI_PRODUCT_BATCH_MAX,
|
|
6
|
+
ImeiProductLookupResponse,
|
|
5
7
|
IRetailCatalogBusinessApi,
|
|
6
8
|
ReleaseReservationBody,
|
|
7
9
|
ReserveInventoryBody,
|
|
@@ -42,6 +44,20 @@ export default class RetailCatalogBusinessApi implements IRetailCatalogBusinessA
|
|
|
42
44
|
return await this.httpRequest.get(url, undefined, this.tenantHeader(issuer));
|
|
43
45
|
}
|
|
44
46
|
|
|
47
|
+
async getProductsByImei(
|
|
48
|
+
imeis: string[],
|
|
49
|
+
tenantId: string,
|
|
50
|
+
): Promise<StandardResponse<ImeiProductLookupResponse>> {
|
|
51
|
+
// Se corta acá: mandar un lote que el destino va a rechazar solo gasta un round-trip.
|
|
52
|
+
if (imeis.length > IMEI_PRODUCT_BATCH_MAX) {
|
|
53
|
+
throw new Error(`El lote admite hasta ${IMEI_PRODUCT_BATCH_MAX} IMEIs y llegaron ${imeis.length}`);
|
|
54
|
+
}
|
|
55
|
+
// `URLSearchParams` codifica la coma del separador; el destino hace `split(',')` sobre el
|
|
56
|
+
// valor ya decodificado por API Gateway.
|
|
57
|
+
const query = new URLSearchParams({ imeis: imeis.join(","), tenantId });
|
|
58
|
+
return await this.httpRequest.get(`${this.baseUrl}/private/products/by-imei?${query.toString()}`);
|
|
59
|
+
}
|
|
60
|
+
|
|
45
61
|
async reserveInventory(
|
|
46
62
|
imei: string,
|
|
47
63
|
body: ReserveInventoryBody,
|
|
@@ -6,12 +6,17 @@ import type { MdmLockModeEnum, MdmProviderEnum } from "@fiado/type-kit/bin/retai
|
|
|
6
6
|
* para sus endpoints privados del ciclo de venta del IMEI, consumidos por el wizard F2.
|
|
7
7
|
*
|
|
8
8
|
* ─────────────────────────────────────────────────────────────────────────────
|
|
9
|
-
* TENANT —
|
|
9
|
+
* TENANT — DOS convenciones conviven acá. Mirá la firma de cada método.
|
|
10
10
|
* ─────────────────────────────────────────────────────────────────────────────
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
* `
|
|
11
|
+
* Los cinco métodos del ciclo de venta del IMEI toman `issuer` y lo mandan en el header
|
|
12
|
+
* `x-tenant-issuer` (`PrivateController.issuerFromHeader` → `TenantContextService.withContext`).
|
|
13
|
+
* `getProductsByImei` toma `tenantId` y lo manda por `?tenantId=`
|
|
14
|
+
* (→ `TenantContextService.withContextByTenantId`), porque su consumidor es un backoffice que no
|
|
15
|
+
* tiene el issuer Cognito. Los privados son `Feature.ANONIMUS` (sin RBAC; la protección es la red
|
|
16
|
+
* VPC), así que el caller DEBE declarar uno de los dos — no hay default.
|
|
17
|
+
*
|
|
18
|
+
* TD: converger los cinco del ciclo de venta a `?tenantId=`, el camino canónico para
|
|
19
|
+
* service-to-service. No se migran acá para no romper al wizard, que ya consume el header.
|
|
15
20
|
*
|
|
16
21
|
* ⚠️ Retorno `StandardResponse<T>` — el consumer lee `result.data`, NO `result.body`.
|
|
17
22
|
* Errores: el destino lanza `DomainError` tipado → el publisher RECHAZA (no devuelve null); cada
|
|
@@ -62,6 +67,32 @@ export interface StoreCatalogItem {
|
|
|
62
67
|
mdmLockMode: MdmLockModeEnum | null;
|
|
63
68
|
}
|
|
64
69
|
|
|
70
|
+
/** Techo de IMEIs por request del lote IMEI->producto. Lo declara y lo honra el destino. */
|
|
71
|
+
export const IMEI_PRODUCT_BATCH_MAX = 100;
|
|
72
|
+
|
|
73
|
+
/** Por que se pudo (o no) resolver el producto de un IMEI del lote. */
|
|
74
|
+
export type ImeiProductLookupStatus = "OK" | "NOT_FOUND" | "UNAVAILABLE" | "INVALID_ID";
|
|
75
|
+
|
|
76
|
+
/** Una entrada del lote IMEI->producto (espeja `ImeiProductLookupEntry` del destino). */
|
|
77
|
+
export interface ImeiProductLookupEntry {
|
|
78
|
+
imei: string;
|
|
79
|
+
/**
|
|
80
|
+
* `OK` trae sku/brand/model · `NOT_FOUND` el catalogo no llega al producto (dato POSITIVO) ·
|
|
81
|
+
* `UNAVAILABLE` no se pudo leer ese IMEI, reintentalo · `INVALID_ID` no tiene forma de IMEI.
|
|
82
|
+
*/
|
|
83
|
+
status: ImeiProductLookupStatus;
|
|
84
|
+
sku?: string;
|
|
85
|
+
/** Marca comercial (p. ej. "Apple"). Solo viene con `status: OK`. */
|
|
86
|
+
brand?: string;
|
|
87
|
+
/** Modelo (p. ej. "iPhone 15 Pro"). Solo viene con `status: OK`. */
|
|
88
|
+
model?: string;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** Respuesta del lote: UNA entrada por IMEI pedido, en el MISMO orden. */
|
|
92
|
+
export interface ImeiProductLookupResponse {
|
|
93
|
+
items: ImeiProductLookupEntry[];
|
|
94
|
+
}
|
|
95
|
+
|
|
65
96
|
/** Resultado de la reserva (espeja `ReserveInventoryResponse` del destino). */
|
|
66
97
|
export interface ReserveInventoryResult {
|
|
67
98
|
reservationExpiresAt: string;
|
|
@@ -88,6 +119,31 @@ export interface IRetailCatalogBusinessApi {
|
|
|
88
119
|
/** GET `/private/stores/{storeId}/catalog` — catálogo vendible (sku, imei, precio de contado, disponibilidad). */
|
|
89
120
|
getStoreCatalog(storeId: string, issuer: string): Promise<StandardResponse<StoreCatalogItem[]>>;
|
|
90
121
|
|
|
122
|
+
/**
|
|
123
|
+
* GET `/private/products/by-imei?imeis=&tenantId=` — resuelve un lote de IMEIs a su producto
|
|
124
|
+
* (sku + marca + modelo). Para los lambdas que del equipo solo guardan el IMEI.
|
|
125
|
+
*
|
|
126
|
+
* ─────────────────────────────────────────────────────────────────────────────
|
|
127
|
+
* ⚠️ El tenant va por `?tenantId=`, NO por el header `x-tenant-issuer` del resto de este publisher.
|
|
128
|
+
* ─────────────────────────────────────────────────────────────────────────────
|
|
129
|
+
* El consumidor es un backoffice que solo tiene el `tenantId` de su contexto; el issuer Cognito
|
|
130
|
+
* no existe de su lado. El destino entra por `TenantContextService.withContextByTenantId`, con las
|
|
131
|
+
* mismas garantías (mismo diccionario, AssumeRole del silo, fail-closed sin tenant).
|
|
132
|
+
*
|
|
133
|
+
* ── El contrato del lote ────────────────────────────────────────────────────────────────────
|
|
134
|
+
* Devuelve UNA entrada por cada IMEI pedido, en el MISMO orden — el caller nunca reconcilia. Los
|
|
135
|
+
* repetidos se consultan una sola vez pero salen repetidos. Techo de `IMEI_PRODUCT_BATCH_MAX`
|
|
136
|
+
* IMEIs por request; el caller parte los lotes mayores.
|
|
137
|
+
*
|
|
138
|
+
* Ningún IMEI tumba el lote: el mal formado sale `INVALID_ID`, el que el catálogo no alcanza a
|
|
139
|
+
* resolver sale `NOT_FOUND` y el que DynamoDB no llegó a leer sale `UNAVAILABLE` (reintentable).
|
|
140
|
+
* Solo un fallo total del destino rechaza la promesa.
|
|
141
|
+
*
|
|
142
|
+
* @throws Error si el lote supera el techo (se corta acá, sin salir a la red) ·
|
|
143
|
+
* `400 IMEI_BATCH_REQUIRED` si `imeis` viene vacío · `400 UNKNOWN_TENANT` sin `tenantId`.
|
|
144
|
+
*/
|
|
145
|
+
getProductsByImei(imeis: string[], tenantId: string): Promise<StandardResponse<ImeiProductLookupResponse>>;
|
|
146
|
+
|
|
91
147
|
/** PUT `/private/inventory/{imei}/reserve` — reserva atómica del IMEI para una sesión (AVAILABLE → RESERVED). */
|
|
92
148
|
reserveInventory(imei: string, body: ReserveInventoryBody, issuer: string): Promise<StandardResponse<ReserveInventoryResult>>;
|
|
93
149
|
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { inject, injectable } from "inversify";
|
|
2
|
+
import type { IHttpRequest } from "@fiado/http-client";
|
|
3
|
+
import { StandardResponse } from "@fiado/gateway-adapter";
|
|
4
|
+
import { WizardSessionLookupResponse } from "@fiado/type-kit/bin/retailWizard/index.js";
|
|
5
|
+
import { IRetailWizardBusinessApi } from "./interfaces/IRetailWizardBusinessApi.js";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Publisher HTTP del lambda `retail-wizard-business` (componente 05 SureKeep Fase 2) para sus
|
|
9
|
+
* endpoints privados. Contrato y semantica completos -> `IRetailWizardBusinessApi`.
|
|
10
|
+
*
|
|
11
|
+
* Env var requerida en el consumer: `RETAIL_WIZARD_BUSINESS_URL`.
|
|
12
|
+
* El template.yml del consumer la setea con:
|
|
13
|
+
*
|
|
14
|
+
* RETAIL_WIZARD_BUSINESS_URL: '{{resolve:ssm:retail-wizard-business}}'
|
|
15
|
+
*/
|
|
16
|
+
@injectable()
|
|
17
|
+
export default class RetailWizardBusinessApi implements IRetailWizardBusinessApi {
|
|
18
|
+
private readonly baseUrl = process.env.RETAIL_WIZARD_BUSINESS_URL || "";
|
|
19
|
+
|
|
20
|
+
constructor(@inject("IHttpRequest") private httpRequest: IHttpRequest) {}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Traduce un lote de `wizardSessionId` a tienda / cliente / equipo. Contrato completo (los cuatro
|
|
24
|
+
* estados, el techo de 100 y lo que NO devuelve) -> `IRetailWizardBusinessApi`.
|
|
25
|
+
*/
|
|
26
|
+
async getWizardSessionsByIds(
|
|
27
|
+
wizardSessionIds: string[],
|
|
28
|
+
tenantId: string,
|
|
29
|
+
): Promise<StandardResponse<WizardSessionLookupResponse>> {
|
|
30
|
+
// `URLSearchParams` codifica la coma del separador; el lambda destino hace `split(',')`
|
|
31
|
+
// sobre el valor ya decodificado por API Gateway.
|
|
32
|
+
const query = new URLSearchParams({ ids: wizardSessionIds.join(","), tenantId });
|
|
33
|
+
return await this.httpRequest.get(`${this.baseUrl}/private/wizard-sessions/batch?${query.toString()}`);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { StandardResponse } from "@fiado/gateway-adapter";
|
|
2
|
+
import { WizardSessionLookupResponse } from "@fiado/type-kit/bin/retailWizard/index.js";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Contrato del publisher HTTP del lambda `retail-wizard-business` (componente 05 SureKeep Fase 2)
|
|
6
|
+
* para sus endpoints privados (service-to-service, VPC-only).
|
|
7
|
+
*
|
|
8
|
+
* Consumidor previsto: `loan-credit-business`, que guarda `wizardSessionId` en cada credito y sin
|
|
9
|
+
* esto no puede traducirlo a la tienda, el cliente ni el equipo de la venta (columnas de Cartera).
|
|
10
|
+
*
|
|
11
|
+
* ─────────────────────────────────────────────────────────────────────────────
|
|
12
|
+
* `tenantId` es OBLIGATORIO — no es un detalle de firma.
|
|
13
|
+
* ─────────────────────────────────────────────────────────────────────────────
|
|
14
|
+
* Los privados NO llevan token de usuario (`Feature.ANONIMUS`; la proteccion es la red VPC), asi que
|
|
15
|
+
* el lambda destino no tiene `AuthContext.issuer` y NO puede inferir el silo. El caller declara el
|
|
16
|
+
* tenant explicito por query. Sin `tenantId` -> `400 UNKNOWN_TENANT` (fail-closed). Nunca hay
|
|
17
|
+
* default: caer a un tenant seria fail-OPEN y podria devolver datos de otra SOFOM.
|
|
18
|
+
*
|
|
19
|
+
* ⚠️ Patron de retorno: `StandardResponse<T>` — el consumer accede `result.data`, NO `result.body`.
|
|
20
|
+
* `@fiado/http-client` entrega el BODY PELADO; `.statusCode`/`.body` no existen en runtime.
|
|
21
|
+
*
|
|
22
|
+
* Env var requerida en el consumer: `RETAIL_WIZARD_BUSINESS_URL`.
|
|
23
|
+
* El template.yml del consumer la setea con:
|
|
24
|
+
*
|
|
25
|
+
* RETAIL_WIZARD_BUSINESS_URL: '{{resolve:ssm:retail-wizard-business}}'
|
|
26
|
+
*
|
|
27
|
+
* Convencion CLAUDE.md global: SSM key = nombre del lambda owner de la URL.
|
|
28
|
+
*/
|
|
29
|
+
export interface IRetailWizardBusinessApi {
|
|
30
|
+
/**
|
|
31
|
+
* `GET /private/wizard-sessions/batch?ids=` — traduce un lote de `wizardSessionId` a la tienda,
|
|
32
|
+
* el cliente y el equipo de cada venta.
|
|
33
|
+
*
|
|
34
|
+
* ── 🔴 Por que el lote va por SESION y no por venta ──────────────────────────────────────────
|
|
35
|
+
* `<T>RetailSale_GT` tiene 5 GSIs y ninguno por `creditId` ni por `wizardSessionId`: entrar por
|
|
36
|
+
* la venta exigiria un indice nuevo (CloudFormation). La sesion se lee por su `pk` directo.
|
|
37
|
+
*
|
|
38
|
+
* ── El contrato del lote (identico a `GET /borrowers/portfolio`, DEC-010) ────────────────────
|
|
39
|
+
* Devuelve UNA entrada por cada id pedido, en el MISMO orden — el caller nunca reconcilia. Los
|
|
40
|
+
* ids repetidos se consultan una sola vez pero salen repetidos en la respuesta. Techo de 100 ids
|
|
41
|
+
* por request. Un fallo parcial NO tumba el lote: esa entrada sale `UNAVAILABLE` y el resto
|
|
42
|
+
* responde.
|
|
43
|
+
*
|
|
44
|
+
* Cuatro estados por entrada (`WizardSessionLookupStatusEnum`): `OK` se leyo completo ·
|
|
45
|
+
* `NOT_FOUND` no existe (dato POSITIVO) · `UNAVAILABLE` no se pudo leer, reintenta ese id ·
|
|
46
|
+
* `INVALID_ID` no tiene forma de id, nunca reintentar.
|
|
47
|
+
*
|
|
48
|
+
* ── ⚠️ Lo que este endpoint NO devuelve ──────────────────────────────────────────────────────
|
|
49
|
+
* NO trae el NOMBRE del cliente, ni el de la tienda, ni el del vendedor: la sesion del wizard no
|
|
50
|
+
* los guarda (solo ids). El caller los resuelve con `retail-customer` y `retail-org`. Tampoco
|
|
51
|
+
* viaja el telefono, que es PII. La marca y el modelo del equipo SI vienen: la sesion los congela
|
|
52
|
+
* al reservar.
|
|
53
|
+
*
|
|
54
|
+
* @throws `400 WIZARD_SESSION_IDS_REQUIRED` si `ids` viene vacio o ausente ·
|
|
55
|
+
* `400 WIZARD_SESSION_BATCH_TOO_LARGE` si supera los 100 ids ·
|
|
56
|
+
* `400 UNKNOWN_TENANT` sin `tenantId`.
|
|
57
|
+
*/
|
|
58
|
+
getWizardSessionsByIds(
|
|
59
|
+
wizardSessionIds: string[],
|
|
60
|
+
tenantId: string,
|
|
61
|
+
): Promise<StandardResponse<WizardSessionLookupResponse>>;
|
|
62
|
+
}
|