@fiado/api-invoker 5.37.0 → 5.39.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/container.config.js +5 -0
- package/bin/index.d.ts +2 -0
- package/bin/index.js +2 -0
- package/bin/retailNotifications/api/RetailNotificationsWebsocketApi.d.ts +17 -0
- package/bin/retailNotifications/api/RetailNotificationsWebsocketApi.js +36 -0
- package/bin/retailNotifications/api/dtos/NotifySubjectRequest.d.ts +18 -0
- package/bin/retailNotifications/api/dtos/NotifySubjectRequest.js +1 -0
- package/bin/retailNotifications/api/dtos/NotifySubjectResult.d.ts +12 -0
- package/bin/retailNotifications/api/dtos/NotifySubjectResult.js +1 -0
- package/bin/retailNotifications/api/interfaces/IRetailNotificationsWebsocketApi.d.ts +43 -0
- package/bin/retailNotifications/api/interfaces/IRetailNotificationsWebsocketApi.js +1 -0
- package/bin/retailNotifications/index.d.ts +4 -0
- package/bin/retailNotifications/index.js +4 -0
- package/bin/transactionProcessor/api/TransactionProcessorApi.d.ts +2 -1
- package/bin/transactionProcessor/api/TransactionProcessorApi.js +4 -0
- package/bin/transactionProcessor/api/interfaces/ITransactionProcessorApi.d.ts +7 -1
- package/bin/webhook-business/index.d.ts +2 -0
- package/bin/webhook-business/index.js +2 -0
- package/bin/webhook-business/queue/WebhookBusinessPublisher.d.ts +6 -0
- package/bin/webhook-business/queue/WebhookBusinessPublisher.js +32 -0
- package/bin/webhook-business/queue/interfaces/IWebhookBusinessPublisher.d.ts +20 -0
- package/bin/webhook-business/queue/interfaces/IWebhookBusinessPublisher.js +1 -0
- package/package.json +2 -2
- package/src/container.config.ts +6 -0
- package/src/index.ts +2 -0
- package/src/retailNotifications/api/RetailNotificationsWebsocketApi.ts +24 -0
- package/src/retailNotifications/api/dtos/NotifySubjectRequest.ts +19 -0
- package/src/retailNotifications/api/dtos/NotifySubjectResult.ts +12 -0
- package/src/retailNotifications/api/interfaces/IRetailNotificationsWebsocketApi.ts +44 -0
- package/src/retailNotifications/index.ts +4 -0
- package/src/transactionProcessor/api/TransactionProcessorApi.ts +10 -1
- package/src/transactionProcessor/api/interfaces/ITransactionProcessorApi.ts +12 -1
- package/src/webhook-business/index.ts +2 -0
- package/src/webhook-business/queue/WebhookBusinessPublisher.ts +26 -0
- package/src/webhook-business/queue/interfaces/IWebhookBusinessPublisher.ts +21 -0
package/bin/container.config.js
CHANGED
|
@@ -128,6 +128,8 @@ import { ResearchSheetsApi } from "./research-sheets/index.js";
|
|
|
128
128
|
import { AuthBusinessApi } from "./auth-business/index.js";
|
|
129
129
|
// KYC verifications business — lectura confirmatoria server-side del KYC por directoryId (SureKeep F2)
|
|
130
130
|
import { KycVerificationsBusinessApi } from "./kyc/index.js";
|
|
131
|
+
// Webhook business — publisher de la cola compartida de entrega de eventos a suscriptores externos
|
|
132
|
+
import { WebhookBusinessPublisher } from "./webhook-business/index.js";
|
|
131
133
|
import { CentralPaymentsWebhookApi } from "./central-payments-webhook/index.js";
|
|
132
134
|
import CollectionEngineApi from "./collection-engine/api/CollectionEngineApi.js";
|
|
133
135
|
import DomainCallbackApi from "./domain-callback/api/DomainCallbackApi.js";
|
|
@@ -274,6 +276,9 @@ export const apiInvokerBindings = new ContainerModule(({ bind }) => {
|
|
|
274
276
|
// KYC verifications business — lectura confirmatoria server-side del KYC por directoryId.
|
|
275
277
|
// La consume el wizard de SureKeep F2 tras el aviso async del stream de `KycVerifications`.
|
|
276
278
|
bind("IKycVerificationsBusinessApi").to(KycVerificationsBusinessApi);
|
|
279
|
+
// Webhook business — lo produce cualquier lambda que tenga algo que avisarle a un externo.
|
|
280
|
+
// El productor necesita WEBHOOK_BUSINESS_QUEUE_URL y sqs:SendMessage sobre la cola.
|
|
281
|
+
bind("IWebhookBusinessPublisher").to(WebhookBusinessPublisher);
|
|
277
282
|
bind("ICentralPaymentsWebhookApi").to(CentralPaymentsWebhookApi);
|
|
278
283
|
// Collection engine — motor de cobro central multitenant (collect/registerCollectible/deregister)
|
|
279
284
|
bind("ICollectionEngineApi").to(CollectionEngineApi);
|
package/bin/index.d.ts
CHANGED
|
@@ -85,6 +85,7 @@ export * from "./datalakeQuery/index.js";
|
|
|
85
85
|
export * from "./retailCustomer/index.js";
|
|
86
86
|
export * from "./retailCatalog/index.js";
|
|
87
87
|
export * from "./retailCards/index.js";
|
|
88
|
+
export * from "./retailNotifications/index.js";
|
|
88
89
|
export * from "./shortlink/index.js";
|
|
89
90
|
export * from "./loanConfig/index.js";
|
|
90
91
|
export * from "./loanOfferings/index.js";
|
|
@@ -111,3 +112,4 @@ export * from "./central-payments-webhook/index.js";
|
|
|
111
112
|
export * from "./collection-engine/index.js";
|
|
112
113
|
export * from "./domain-callback/index.js";
|
|
113
114
|
export * from "./datacultrConnector/index.js";
|
|
115
|
+
export * from "./webhook-business/index.js";
|
package/bin/index.js
CHANGED
|
@@ -85,6 +85,7 @@ export * from "./datalakeQuery/index.js";
|
|
|
85
85
|
export * from "./retailCustomer/index.js";
|
|
86
86
|
export * from "./retailCatalog/index.js";
|
|
87
87
|
export * from "./retailCards/index.js";
|
|
88
|
+
export * from "./retailNotifications/index.js";
|
|
88
89
|
export * from "./shortlink/index.js";
|
|
89
90
|
export * from "./loanConfig/index.js";
|
|
90
91
|
export * from "./loanOfferings/index.js";
|
|
@@ -111,3 +112,4 @@ export * from "./central-payments-webhook/index.js";
|
|
|
111
112
|
export * from "./collection-engine/index.js";
|
|
112
113
|
export * from "./domain-callback/index.js";
|
|
113
114
|
export * from "./datacultrConnector/index.js";
|
|
115
|
+
export * from "./webhook-business/index.js";
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { IHttpRequest } from "@fiado/http-client";
|
|
2
|
+
import { StandardResponse } from "@fiado/gateway-adapter";
|
|
3
|
+
import { IRetailNotificationsWebsocketApi } from "./interfaces/IRetailNotificationsWebsocketApi.js";
|
|
4
|
+
import { NotifySubjectRequest } from "./dtos/NotifySubjectRequest.js";
|
|
5
|
+
import { NotifySubjectResult } from "./dtos/NotifySubjectResult.js";
|
|
6
|
+
/**
|
|
7
|
+
* Publisher HTTP del lambda `retail-notifications-websocket` (SureKeep Fase 2) para su privado M2M de
|
|
8
|
+
* avisos al vendedor. Contrato y semántica completos → `IRetailNotificationsWebsocketApi`.
|
|
9
|
+
*
|
|
10
|
+
* Env var requerida en el consumer: `RETAIL_NOTIFICATIONS_WEBSOCKET_URL`.
|
|
11
|
+
*/
|
|
12
|
+
export default class RetailNotificationsWebsocketApi implements IRetailNotificationsWebsocketApi {
|
|
13
|
+
private httpRequest;
|
|
14
|
+
private readonly baseUrl;
|
|
15
|
+
constructor(httpRequest: IHttpRequest);
|
|
16
|
+
notifySubject(payload: NotifySubjectRequest): Promise<StandardResponse<NotifySubjectResult>>;
|
|
17
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
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-notifications-websocket` (SureKeep Fase 2) para su privado M2M de
|
|
16
|
+
* avisos al vendedor. Contrato y semántica completos → `IRetailNotificationsWebsocketApi`.
|
|
17
|
+
*
|
|
18
|
+
* Env var requerida en el consumer: `RETAIL_NOTIFICATIONS_WEBSOCKET_URL`.
|
|
19
|
+
*/
|
|
20
|
+
let RetailNotificationsWebsocketApi = class RetailNotificationsWebsocketApi {
|
|
21
|
+
httpRequest;
|
|
22
|
+
// El buildspec publica la URL en SSM con "/" final — se normaliza para no armar "//private".
|
|
23
|
+
baseUrl = (process.env.RETAIL_NOTIFICATIONS_WEBSOCKET_URL || "").replace(/\/+$/, "");
|
|
24
|
+
constructor(httpRequest) {
|
|
25
|
+
this.httpRequest = httpRequest;
|
|
26
|
+
}
|
|
27
|
+
async notifySubject(payload) {
|
|
28
|
+
return await this.httpRequest.post(`${this.baseUrl}/private/notifications`, payload);
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
RetailNotificationsWebsocketApi = __decorate([
|
|
32
|
+
injectable(),
|
|
33
|
+
__param(0, inject("IHttpRequest")),
|
|
34
|
+
__metadata("design:paramtypes", [Object])
|
|
35
|
+
], RetailNotificationsWebsocketApi);
|
|
36
|
+
export default RetailNotificationsWebsocketApi;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Catálogo de avisos que el canal WebSocket de SureKeep sabe empujar. Espeja el
|
|
3
|
+
* `NotificationTypeEnum` de `retail-notifications-websocket`, que valida el cuerpo con `@IsEnum`: un tipo
|
|
4
|
+
* fuera del catálogo se rechaza con 400 `INVALID_PAYLOAD`.
|
|
5
|
+
*/
|
|
6
|
+
export type RetailNotificationType = 'SALE_UPDATED';
|
|
7
|
+
/**
|
|
8
|
+
* Lo que viaja por el privado M2M: a qué vendedor avisarle, de qué y sobre cuál de sus ventas.
|
|
9
|
+
* NO lleva ningún dato del comprador — el navegador recibe solo el tipo y el id de la venta, y el
|
|
10
|
+
* detalle se lo pide después al wizard.
|
|
11
|
+
*/
|
|
12
|
+
export interface NotifySubjectRequest {
|
|
13
|
+
/** `sub` de Cognito del vendedor destinatario. Máx. 128 caracteres. */
|
|
14
|
+
subjectId: string;
|
|
15
|
+
type: RetailNotificationType;
|
|
16
|
+
/** Id de la venta, para que el vendedor sepa de cuál de las suyas se trata. Máx. 128 caracteres. */
|
|
17
|
+
sessionId: string;
|
|
18
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resultado de una entrega. `delivered: 0` significa que el vendedor NO tiene ningún socket abierto
|
|
3
|
+
* —tiene la pestaña cerrada— y NO es un error: el llamador no debe reintentar ni alarmar.
|
|
4
|
+
*/
|
|
5
|
+
export interface NotifySubjectResult {
|
|
6
|
+
/** Conexiones a las que llegó el aviso. */
|
|
7
|
+
delivered: number;
|
|
8
|
+
/** Filas de conexiones muertas que el destino limpió de paso. */
|
|
9
|
+
staleRemoved: number;
|
|
10
|
+
/** Conexiones vivas a las que el envío falló. */
|
|
11
|
+
failed: number;
|
|
12
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { StandardResponse } from "@fiado/gateway-adapter";
|
|
2
|
+
import { NotifySubjectRequest } from "../dtos/NotifySubjectRequest.js";
|
|
3
|
+
import { NotifySubjectResult } from "../dtos/NotifySubjectResult.js";
|
|
4
|
+
/**
|
|
5
|
+
* Contrato del publisher HTTP del lambda `retail-notifications-websocket` (SureKeep Fase 2): el canal por
|
|
6
|
+
* el que el portal le avisa al VENDEDOR que su venta cambió de estado.
|
|
7
|
+
*
|
|
8
|
+
* Consumidor previsto:
|
|
9
|
+
* - `retail-wizard-business` → al asentar el desenlace de una venta (stream de la cuenta PCF).
|
|
10
|
+
*
|
|
11
|
+
* ─────────────────────────────────────────────────────────────────────────────
|
|
12
|
+
* SIN TENANT — a diferencia de los clients retail, este NO manda `?tenantId=`.
|
|
13
|
+
* ─────────────────────────────────────────────────────────────────────────────
|
|
14
|
+
* El destino resuelve el silo de SU PROPIO entorno (`resolveTenantId`), nunca del request: el
|
|
15
|
+
* `tenantId` es el hash del GSI de ruteo, así que el aislamiento no puede depender de quien llama.
|
|
16
|
+
*
|
|
17
|
+
* ⚠️ Retorno `StandardResponse<T>` — el consumer lee `result.data`, NO `result.body`.
|
|
18
|
+
*
|
|
19
|
+
* Los DTOs viven acá y no en `@fiado/type-kit` porque el destino tampoco depende de type-kit: sus
|
|
20
|
+
* DTOs son locales. Si algún día el contrato se comparte con un tercer repo, mudarlos a type-kit.
|
|
21
|
+
*
|
|
22
|
+
* Env var requerida en el consumer: `RETAIL_NOTIFICATIONS_WEBSOCKET_URL`.
|
|
23
|
+
* El template.yml del consumer la setea con:
|
|
24
|
+
*
|
|
25
|
+
* RETAIL_NOTIFICATIONS_WEBSOCKET_URL: '{{resolve:ssm:retail-notifications-websocket}}'
|
|
26
|
+
*
|
|
27
|
+
* Convención CLAUDE.md global: SSM key = nombre del lambda owner de la URL.
|
|
28
|
+
*/
|
|
29
|
+
export interface IRetailNotificationsWebsocketApi {
|
|
30
|
+
/**
|
|
31
|
+
* POST `/private/notifications` — empuja un aviso al vendedor por su WebSocket abierto.
|
|
32
|
+
*
|
|
33
|
+
* Semántica de la respuesta, que decide qué hacer ante cada desenlace:
|
|
34
|
+
* · 200 con `delivered: 0` → el vendedor tiene la pestaña cerrada. NO es error, NO reintentar.
|
|
35
|
+
* · 400 `INVALID_PAYLOAD` → el cuerpo está mal (falta un campo, tipo fuera del catálogo).
|
|
36
|
+
* NO reintentar: reintentarlo igual da lo mismo.
|
|
37
|
+
* · 500 `INTERNAL_ERROR` → falló el canal. El request estaba bien y se PUEDE reintentar.
|
|
38
|
+
*
|
|
39
|
+
* Avisar es un efecto de borde: si falla, el aviso se pierde y lo cubre el barrido del wizard.
|
|
40
|
+
* Nunca debe bloquear ni deshacer la operación que lo disparó.
|
|
41
|
+
*/
|
|
42
|
+
notifySubject(payload: NotifySubjectRequest): Promise<StandardResponse<NotifySubjectResult>>;
|
|
43
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export * from "./api/dtos/NotifySubjectRequest.js";
|
|
2
|
+
export * from "./api/dtos/NotifySubjectResult.js";
|
|
3
|
+
export * from "./api/interfaces/IRetailNotificationsWebsocketApi.js";
|
|
4
|
+
export { default as RetailNotificationsWebsocketApi } from "./api/RetailNotificationsWebsocketApi.js";
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export * from "./api/dtos/NotifySubjectRequest.js";
|
|
2
|
+
export * from "./api/dtos/NotifySubjectResult.js";
|
|
3
|
+
export * from "./api/interfaces/IRetailNotificationsWebsocketApi.js";
|
|
4
|
+
export { default as RetailNotificationsWebsocketApi } from "./api/RetailNotificationsWebsocketApi.js";
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { ITransactionProcessorApi } from "./interfaces/ITransactionProcessorApi.js";
|
|
2
2
|
import type { IHttpRequest } from "@fiado/http-client";
|
|
3
|
-
import { AuthorizeCollectorOrderRequest, AuthorizeCollectorOrderResponse, AuthorizeCollectorTransactionRequest, AuthorizeCreditCardPurchaseRequest, AuthorizeSpeiBankTransferNcRequest, AuthorizeSpeiMexBankTransferInRequest, AuthorizeRenewalPreAuthRequest, AuthorizeRenewalPreAuthResponse } from "@fiado/type-kit/bin/transactionProcessor/index.js";
|
|
3
|
+
import { AuthorizeCollectorOrderRequest, AuthorizeCollectorOrderResponse, AuthorizeCollectorTransactionRequest, AuthorizeCreditCardPurchaseRequest, AuthorizeSpeiBankTransferNcRequest, AuthorizeSpeiMexBankTransferInRequest, AuthorizeRenewalPreAuthRequest, AuthorizeRenewalPreAuthResponse, AuthorizeBonusRequest, TransactionProcessorResponse } from "@fiado/type-kit/bin/transactionProcessor/index.js";
|
|
4
4
|
import { ApiGatewayResponse } from "@fiado/gateway-adapter";
|
|
5
5
|
import { ValidateWalletFundingRequest, ValidateWalletFundingResponse, CreditWalletFundingRequest, CreditWalletFundingResponse, ReverseWalletFundingRequest, ReverseWalletFundingResponse, GetWalletFundingLimitRequest, GetWalletFundingLimitResponse } from "@fiado/type-kit/bin/walletFunding/index.js";
|
|
6
6
|
import { RemittanceCreditBackRequest, RemittanceCreditBackResponse } from "@fiado/type-kit/bin/remittance/index.js";
|
|
@@ -24,5 +24,6 @@ export default class TransactionProcessorApi implements ITransactionProcessorApi
|
|
|
24
24
|
walletFundingGetLimit(request: GetWalletFundingLimitRequest): Promise<ApiGatewayResponse<GetWalletFundingLimitResponse>>;
|
|
25
25
|
creditBack(request: RemittanceCreditBackRequest): Promise<FiadoApiResponse<RemittanceCreditBackResponse>>;
|
|
26
26
|
authorizeCollectionMovement(request: AuthorizeCollectionMovementRequest): Promise<FiadoApiResponse<AuthorizeCollectionMovementResponse>>;
|
|
27
|
+
authorizeWelcomeBonus(request: AuthorizeBonusRequest): Promise<FiadoApiResponse<TransactionProcessorResponse>>;
|
|
27
28
|
}
|
|
28
29
|
export {};
|
|
@@ -69,6 +69,10 @@ let TransactionProcessorApi = class TransactionProcessorApi {
|
|
|
69
69
|
const url = `${this.baseUrl}collection/authorize`;
|
|
70
70
|
return await this.httpRequest.post(url, request);
|
|
71
71
|
}
|
|
72
|
+
async authorizeWelcomeBonus(request) {
|
|
73
|
+
const url = `${this.baseUrl}bonus/authorize`;
|
|
74
|
+
return await this.httpRequest.post(url, request);
|
|
75
|
+
}
|
|
72
76
|
};
|
|
73
77
|
TransactionProcessorApi = __decorate([
|
|
74
78
|
injectable(),
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
type FiadoApiResponse<T> = import("@fiado/type-kit/bin/apiResponse/dtos/FiadoApiResponse.js").default<T>;
|
|
2
|
-
import { AuthorizeCollectorOrderRequest, AuthorizeCollectorOrderResponse, AuthorizeCollectorTransactionRequest, AuthorizeCreditCardPurchaseRequest, AuthorizeSpeiBankTransferNcRequest, AuthorizeSpeiMexBankTransferInRequest, AuthorizeRenewalPreAuthRequest, AuthorizeRenewalPreAuthResponse } from "@fiado/type-kit/bin/transactionProcessor/index.js";
|
|
2
|
+
import { AuthorizeCollectorOrderRequest, AuthorizeCollectorOrderResponse, AuthorizeCollectorTransactionRequest, AuthorizeCreditCardPurchaseRequest, AuthorizeSpeiBankTransferNcRequest, AuthorizeSpeiMexBankTransferInRequest, AuthorizeRenewalPreAuthRequest, AuthorizeRenewalPreAuthResponse, AuthorizeBonusRequest, TransactionProcessorResponse } from "@fiado/type-kit/bin/transactionProcessor/index.js";
|
|
3
3
|
import { ApiGatewayResponse } from "@fiado/gateway-adapter";
|
|
4
4
|
import { ValidateWalletFundingRequest, ValidateWalletFundingResponse, CreditWalletFundingRequest, CreditWalletFundingResponse, ReverseWalletFundingRequest, ReverseWalletFundingResponse, GetWalletFundingLimitRequest, GetWalletFundingLimitResponse } from "@fiado/type-kit/bin/walletFunding/index.js";
|
|
5
5
|
import { RemittanceCreditBackRequest, RemittanceCreditBackResponse } from "@fiado/type-kit/bin/remittance/index.js";
|
|
@@ -54,5 +54,11 @@ export interface ITransactionProcessorApi {
|
|
|
54
54
|
* Idempotente por `executionId` (@IdempotencyController).
|
|
55
55
|
*/
|
|
56
56
|
authorizeCollectionMovement(request: AuthorizeCollectionMovementRequest): Promise<FiadoApiResponse<AuthorizeCollectionMovementResponse>>;
|
|
57
|
+
/**
|
|
58
|
+
* Acredita el bono de bienvenida de SureKeep en la wallet PagoConfiado del comprador.
|
|
59
|
+
* Backend: POST /transaction-processor/bonus/authorize (privado VPC).
|
|
60
|
+
* Idempotente por `idempotencyKey` (el `bonusId` del asiento).
|
|
61
|
+
*/
|
|
62
|
+
authorizeWelcomeBonus(request: AuthorizeBonusRequest): Promise<FiadoApiResponse<TransactionProcessorResponse>>;
|
|
57
63
|
}
|
|
58
64
|
export {};
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { WebhookOutboundMessage } from "@fiado/type-kit/bin/webhookBusiness/index.js";
|
|
2
|
+
import { IWebhookBusinessPublisher } from "./interfaces/IWebhookBusinessPublisher.js";
|
|
3
|
+
export declare class WebhookBusinessPublisher implements IWebhookBusinessPublisher {
|
|
4
|
+
private readonly WEBHOOK_BUSINESS_QUEUE_URL;
|
|
5
|
+
publish(message: WebhookOutboundMessage): Promise<void>;
|
|
6
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
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
|
+
import { SendMessageCommand, SQSClient } from "@aws-sdk/client-sqs";
|
|
8
|
+
import { injectable } from "inversify";
|
|
9
|
+
let WebhookBusinessPublisher = class WebhookBusinessPublisher {
|
|
10
|
+
WEBHOOK_BUSINESS_QUEUE_URL = process.env.WEBHOOK_BUSINESS_QUEUE_URL;
|
|
11
|
+
async publish(message) {
|
|
12
|
+
try {
|
|
13
|
+
const client = new SQSClient();
|
|
14
|
+
// Cola ESTÁNDAR (no FIFO): sin MessageGroupId/MessageDeduplicationId ni orden garantizado.
|
|
15
|
+
// El consumidor deduplica por `eventId` del sobre y reordena por `occurredAt`.
|
|
16
|
+
const sendMessageRequest = {
|
|
17
|
+
QueueUrl: this.WEBHOOK_BUSINESS_QUEUE_URL,
|
|
18
|
+
MessageBody: JSON.stringify(message),
|
|
19
|
+
};
|
|
20
|
+
const command = new SendMessageCommand(sendMessageRequest);
|
|
21
|
+
await client.send(command);
|
|
22
|
+
}
|
|
23
|
+
catch (error) {
|
|
24
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
25
|
+
throw new Error(`Error publishing message to WebhookBusinessQueue: ${errorMessage}`);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
WebhookBusinessPublisher = __decorate([
|
|
30
|
+
injectable()
|
|
31
|
+
], WebhookBusinessPublisher);
|
|
32
|
+
export { WebhookBusinessPublisher };
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { WebhookOutboundMessage } from "@fiado/type-kit/bin/webhookBusiness/index.js";
|
|
2
|
+
/**
|
|
3
|
+
* Publisher de Fiado hacia `WebhookBusinessQueue`, que consume `webhook-business` para entregarle
|
|
4
|
+
* el evento a los suscriptores externos que tengan el tag `wh:*` del `directoryId`.
|
|
5
|
+
*
|
|
6
|
+
* Lo produce cualquier lambda de Fiado que tenga algo que avisarle a un externo (la emisión de la
|
|
7
|
+
* cuenta PCF, el dictamen de cumplimiento). Vive aquí —y no como SDK crudo en cada lambda— porque la
|
|
8
|
+
* cola es COMPARTIDA cross-lambda: el estándar Fiado publica a una cola compartida vía Publisher de
|
|
9
|
+
* `@fiado/api-invoker` (mismo patrón que `IRetailKycInboundPublisher`).
|
|
10
|
+
*
|
|
11
|
+
* El productor necesita la env var `WEBHOOK_BUSINESS_QUEUE_URL` en su `template.yml` (la misma que
|
|
12
|
+
* ya declara `webhook-business` del lado consumidor) y permiso `sqs:SendMessage` sobre la cola.
|
|
13
|
+
*
|
|
14
|
+
* El tipo del mensaje es la unión cerrada `WebhookOutboundMessage` del `@fiado/type-kit`: agregar un
|
|
15
|
+
* evento obliga a tocar también el router del consumidor, que es la conversación que tiene que
|
|
16
|
+
* pasar antes de meter un mensaje nuevo en una cola compartida.
|
|
17
|
+
*/
|
|
18
|
+
export interface IWebhookBusinessPublisher {
|
|
19
|
+
publish(message: WebhookOutboundMessage): Promise<void>;
|
|
20
|
+
}
|
|
@@ -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.39.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",
|
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
"@fiado/gateway-adapter": "^3.9.0",
|
|
35
35
|
"@fiado/http-client": "^2.0.1",
|
|
36
36
|
"@fiado/logger": "^1.1.3",
|
|
37
|
-
"@fiado/type-kit": "^3.
|
|
37
|
+
"@fiado/type-kit": "^3.360.0",
|
|
38
38
|
"dotenv": "^16.4.7"
|
|
39
39
|
},
|
|
40
40
|
"peerDependencies": {
|
package/src/container.config.ts
CHANGED
|
@@ -192,6 +192,8 @@ import { IResearchSheetsApi, ResearchSheetsApi } from "./research-sheets/index.j
|
|
|
192
192
|
import { IAuthBusinessApi, AuthBusinessApi } from "./auth-business/index.js";
|
|
193
193
|
// KYC verifications business — lectura confirmatoria server-side del KYC por directoryId (SureKeep F2)
|
|
194
194
|
import { IKycVerificationsBusinessApi, KycVerificationsBusinessApi } from "./kyc/index.js";
|
|
195
|
+
// Webhook business — publisher de la cola compartida de entrega de eventos a suscriptores externos
|
|
196
|
+
import { IWebhookBusinessPublisher, WebhookBusinessPublisher } from "./webhook-business/index.js";
|
|
195
197
|
import { ICentralPaymentsWebhookApi, CentralPaymentsWebhookApi } from "./central-payments-webhook/index.js";
|
|
196
198
|
// Collection engine — cliente del motor de cobro central (collect/registerCollectible/deregister)
|
|
197
199
|
import { ICollectionEngineApi } from "./collection-engine/index.js";
|
|
@@ -366,6 +368,10 @@ export const apiInvokerBindings = new ContainerModule(({ bind }) => {
|
|
|
366
368
|
// KYC verifications business — lectura confirmatoria server-side del KYC por directoryId.
|
|
367
369
|
// La consume el wizard de SureKeep F2 tras el aviso async del stream de `KycVerifications`.
|
|
368
370
|
bind<IKycVerificationsBusinessApi>("IKycVerificationsBusinessApi").to(KycVerificationsBusinessApi);
|
|
371
|
+
|
|
372
|
+
// Webhook business — lo produce cualquier lambda que tenga algo que avisarle a un externo.
|
|
373
|
+
// El productor necesita WEBHOOK_BUSINESS_QUEUE_URL y sqs:SendMessage sobre la cola.
|
|
374
|
+
bind<IWebhookBusinessPublisher>("IWebhookBusinessPublisher").to(WebhookBusinessPublisher);
|
|
369
375
|
bind<ICentralPaymentsWebhookApi>("ICentralPaymentsWebhookApi").to(CentralPaymentsWebhookApi);
|
|
370
376
|
|
|
371
377
|
// Collection engine — motor de cobro central multitenant (collect/registerCollectible/deregister)
|
package/src/index.ts
CHANGED
|
@@ -85,6 +85,7 @@ export * from "./datalakeQuery/index.js";
|
|
|
85
85
|
export * from "./retailCustomer/index.js";
|
|
86
86
|
export * from "./retailCatalog/index.js";
|
|
87
87
|
export * from "./retailCards/index.js";
|
|
88
|
+
export * from "./retailNotifications/index.js";
|
|
88
89
|
export * from "./shortlink/index.js";
|
|
89
90
|
export * from "./loanConfig/index.js";
|
|
90
91
|
export * from "./loanOfferings/index.js";
|
|
@@ -111,3 +112,4 @@ export * from "./central-payments-webhook/index.js";
|
|
|
111
112
|
export * from "./collection-engine/index.js";
|
|
112
113
|
export * from "./domain-callback/index.js";
|
|
113
114
|
export * from "./datacultrConnector/index.js";
|
|
115
|
+
export * from "./webhook-business/index.js";
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { inject, injectable } from "inversify";
|
|
2
|
+
import type { IHttpRequest } from "@fiado/http-client";
|
|
3
|
+
import { StandardResponse } from "@fiado/gateway-adapter";
|
|
4
|
+
import { IRetailNotificationsWebsocketApi } from "./interfaces/IRetailNotificationsWebsocketApi.js";
|
|
5
|
+
import { NotifySubjectRequest } from "./dtos/NotifySubjectRequest.js";
|
|
6
|
+
import { NotifySubjectResult } from "./dtos/NotifySubjectResult.js";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Publisher HTTP del lambda `retail-notifications-websocket` (SureKeep Fase 2) para su privado M2M de
|
|
10
|
+
* avisos al vendedor. Contrato y semántica completos → `IRetailNotificationsWebsocketApi`.
|
|
11
|
+
*
|
|
12
|
+
* Env var requerida en el consumer: `RETAIL_NOTIFICATIONS_WEBSOCKET_URL`.
|
|
13
|
+
*/
|
|
14
|
+
@injectable()
|
|
15
|
+
export default class RetailNotificationsWebsocketApi implements IRetailNotificationsWebsocketApi {
|
|
16
|
+
// El buildspec publica la URL en SSM con "/" final — se normaliza para no armar "//private".
|
|
17
|
+
private readonly baseUrl = (process.env.RETAIL_NOTIFICATIONS_WEBSOCKET_URL || "").replace(/\/+$/, "");
|
|
18
|
+
|
|
19
|
+
constructor(@inject("IHttpRequest") private httpRequest: IHttpRequest) {}
|
|
20
|
+
|
|
21
|
+
async notifySubject(payload: NotifySubjectRequest): Promise<StandardResponse<NotifySubjectResult>> {
|
|
22
|
+
return await this.httpRequest.post(`${this.baseUrl}/private/notifications`, payload);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Catálogo de avisos que el canal WebSocket de SureKeep sabe empujar. Espeja el
|
|
3
|
+
* `NotificationTypeEnum` de `retail-notifications-websocket`, que valida el cuerpo con `@IsEnum`: un tipo
|
|
4
|
+
* fuera del catálogo se rechaza con 400 `INVALID_PAYLOAD`.
|
|
5
|
+
*/
|
|
6
|
+
export type RetailNotificationType = 'SALE_UPDATED';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Lo que viaja por el privado M2M: a qué vendedor avisarle, de qué y sobre cuál de sus ventas.
|
|
10
|
+
* NO lleva ningún dato del comprador — el navegador recibe solo el tipo y el id de la venta, y el
|
|
11
|
+
* detalle se lo pide después al wizard.
|
|
12
|
+
*/
|
|
13
|
+
export interface NotifySubjectRequest {
|
|
14
|
+
/** `sub` de Cognito del vendedor destinatario. Máx. 128 caracteres. */
|
|
15
|
+
subjectId: string;
|
|
16
|
+
type: RetailNotificationType;
|
|
17
|
+
/** Id de la venta, para que el vendedor sepa de cuál de las suyas se trata. Máx. 128 caracteres. */
|
|
18
|
+
sessionId: string;
|
|
19
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resultado de una entrega. `delivered: 0` significa que el vendedor NO tiene ningún socket abierto
|
|
3
|
+
* —tiene la pestaña cerrada— y NO es un error: el llamador no debe reintentar ni alarmar.
|
|
4
|
+
*/
|
|
5
|
+
export interface NotifySubjectResult {
|
|
6
|
+
/** Conexiones a las que llegó el aviso. */
|
|
7
|
+
delivered: number;
|
|
8
|
+
/** Filas de conexiones muertas que el destino limpió de paso. */
|
|
9
|
+
staleRemoved: number;
|
|
10
|
+
/** Conexiones vivas a las que el envío falló. */
|
|
11
|
+
failed: number;
|
|
12
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { StandardResponse } from "@fiado/gateway-adapter";
|
|
2
|
+
import { NotifySubjectRequest } from "../dtos/NotifySubjectRequest.js";
|
|
3
|
+
import { NotifySubjectResult } from "../dtos/NotifySubjectResult.js";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Contrato del publisher HTTP del lambda `retail-notifications-websocket` (SureKeep Fase 2): el canal por
|
|
7
|
+
* el que el portal le avisa al VENDEDOR que su venta cambió de estado.
|
|
8
|
+
*
|
|
9
|
+
* Consumidor previsto:
|
|
10
|
+
* - `retail-wizard-business` → al asentar el desenlace de una venta (stream de la cuenta PCF).
|
|
11
|
+
*
|
|
12
|
+
* ─────────────────────────────────────────────────────────────────────────────
|
|
13
|
+
* SIN TENANT — a diferencia de los clients retail, este NO manda `?tenantId=`.
|
|
14
|
+
* ─────────────────────────────────────────────────────────────────────────────
|
|
15
|
+
* El destino resuelve el silo de SU PROPIO entorno (`resolveTenantId`), nunca del request: el
|
|
16
|
+
* `tenantId` es el hash del GSI de ruteo, así que el aislamiento no puede depender de quien llama.
|
|
17
|
+
*
|
|
18
|
+
* ⚠️ Retorno `StandardResponse<T>` — el consumer lee `result.data`, NO `result.body`.
|
|
19
|
+
*
|
|
20
|
+
* Los DTOs viven acá y no en `@fiado/type-kit` porque el destino tampoco depende de type-kit: sus
|
|
21
|
+
* DTOs son locales. Si algún día el contrato se comparte con un tercer repo, mudarlos a type-kit.
|
|
22
|
+
*
|
|
23
|
+
* Env var requerida en el consumer: `RETAIL_NOTIFICATIONS_WEBSOCKET_URL`.
|
|
24
|
+
* El template.yml del consumer la setea con:
|
|
25
|
+
*
|
|
26
|
+
* RETAIL_NOTIFICATIONS_WEBSOCKET_URL: '{{resolve:ssm:retail-notifications-websocket}}'
|
|
27
|
+
*
|
|
28
|
+
* Convención CLAUDE.md global: SSM key = nombre del lambda owner de la URL.
|
|
29
|
+
*/
|
|
30
|
+
export interface IRetailNotificationsWebsocketApi {
|
|
31
|
+
/**
|
|
32
|
+
* POST `/private/notifications` — empuja un aviso al vendedor por su WebSocket abierto.
|
|
33
|
+
*
|
|
34
|
+
* Semántica de la respuesta, que decide qué hacer ante cada desenlace:
|
|
35
|
+
* · 200 con `delivered: 0` → el vendedor tiene la pestaña cerrada. NO es error, NO reintentar.
|
|
36
|
+
* · 400 `INVALID_PAYLOAD` → el cuerpo está mal (falta un campo, tipo fuera del catálogo).
|
|
37
|
+
* NO reintentar: reintentarlo igual da lo mismo.
|
|
38
|
+
* · 500 `INTERNAL_ERROR` → falló el canal. El request estaba bien y se PUEDE reintentar.
|
|
39
|
+
*
|
|
40
|
+
* Avisar es un efecto de borde: si falla, el aviso se pierde y lo cubre el barrido del wizard.
|
|
41
|
+
* Nunca debe bloquear ni deshacer la operación que lo disparó.
|
|
42
|
+
*/
|
|
43
|
+
notifySubject(payload: NotifySubjectRequest): Promise<StandardResponse<NotifySubjectResult>>;
|
|
44
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export * from "./api/dtos/NotifySubjectRequest.js";
|
|
2
|
+
export * from "./api/dtos/NotifySubjectResult.js";
|
|
3
|
+
export * from "./api/interfaces/IRetailNotificationsWebsocketApi.js";
|
|
4
|
+
export { default as RetailNotificationsWebsocketApi } from "./api/RetailNotificationsWebsocketApi.js";
|
|
@@ -8,7 +8,9 @@ import {
|
|
|
8
8
|
AuthorizeCreditCardPurchaseRequest, AuthorizeSpeiBankTransferNcRequest,
|
|
9
9
|
AuthorizeSpeiMexBankTransferInRequest,
|
|
10
10
|
AuthorizeRenewalPreAuthRequest,
|
|
11
|
-
AuthorizeRenewalPreAuthResponse
|
|
11
|
+
AuthorizeRenewalPreAuthResponse,
|
|
12
|
+
AuthorizeBonusRequest,
|
|
13
|
+
TransactionProcessorResponse
|
|
12
14
|
} from "@fiado/type-kit/bin/transactionProcessor/index.js";
|
|
13
15
|
import { ApiGatewayResponse } from "@fiado/gateway-adapter";
|
|
14
16
|
import {
|
|
@@ -113,4 +115,11 @@ export default class TransactionProcessorApi implements ITransactionProcessorApi
|
|
|
113
115
|
const url = `${this.baseUrl}collection/authorize`;
|
|
114
116
|
return await this.httpRequest.post(url, request);
|
|
115
117
|
}
|
|
118
|
+
|
|
119
|
+
async authorizeWelcomeBonus(
|
|
120
|
+
request: AuthorizeBonusRequest,
|
|
121
|
+
): Promise<FiadoApiResponse<TransactionProcessorResponse>> {
|
|
122
|
+
const url = `${this.baseUrl}bonus/authorize`;
|
|
123
|
+
return await this.httpRequest.post(url, request);
|
|
124
|
+
}
|
|
116
125
|
}
|
|
@@ -7,7 +7,9 @@ import {
|
|
|
7
7
|
AuthorizeSpeiBankTransferNcRequest,
|
|
8
8
|
AuthorizeSpeiMexBankTransferInRequest,
|
|
9
9
|
AuthorizeRenewalPreAuthRequest,
|
|
10
|
-
AuthorizeRenewalPreAuthResponse
|
|
10
|
+
AuthorizeRenewalPreAuthResponse,
|
|
11
|
+
AuthorizeBonusRequest,
|
|
12
|
+
TransactionProcessorResponse
|
|
11
13
|
} from "@fiado/type-kit/bin/transactionProcessor/index.js";
|
|
12
14
|
import { ApiGatewayResponse } from "@fiado/gateway-adapter";
|
|
13
15
|
import {
|
|
@@ -101,4 +103,13 @@ export interface ITransactionProcessorApi {
|
|
|
101
103
|
authorizeCollectionMovement(
|
|
102
104
|
request: AuthorizeCollectionMovementRequest,
|
|
103
105
|
): Promise<FiadoApiResponse<AuthorizeCollectionMovementResponse>>;
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Acredita el bono de bienvenida de SureKeep en la wallet PagoConfiado del comprador.
|
|
109
|
+
* Backend: POST /transaction-processor/bonus/authorize (privado VPC).
|
|
110
|
+
* Idempotente por `idempotencyKey` (el `bonusId` del asiento).
|
|
111
|
+
*/
|
|
112
|
+
authorizeWelcomeBonus(
|
|
113
|
+
request: AuthorizeBonusRequest,
|
|
114
|
+
): Promise<FiadoApiResponse<TransactionProcessorResponse>>;
|
|
104
115
|
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { SendMessageCommand, SendMessageRequest, SQSClient } from "@aws-sdk/client-sqs";
|
|
2
|
+
import { injectable } from "inversify";
|
|
3
|
+
import { WebhookOutboundMessage } from "@fiado/type-kit/bin/webhookBusiness/index.js";
|
|
4
|
+
import { IWebhookBusinessPublisher } from "./interfaces/IWebhookBusinessPublisher.js";
|
|
5
|
+
|
|
6
|
+
@injectable()
|
|
7
|
+
export class WebhookBusinessPublisher implements IWebhookBusinessPublisher {
|
|
8
|
+
private readonly WEBHOOK_BUSINESS_QUEUE_URL = process.env.WEBHOOK_BUSINESS_QUEUE_URL;
|
|
9
|
+
|
|
10
|
+
async publish(message: WebhookOutboundMessage): Promise<void> {
|
|
11
|
+
try {
|
|
12
|
+
const client: SQSClient = new SQSClient();
|
|
13
|
+
// Cola ESTÁNDAR (no FIFO): sin MessageGroupId/MessageDeduplicationId ni orden garantizado.
|
|
14
|
+
// El consumidor deduplica por `eventId` del sobre y reordena por `occurredAt`.
|
|
15
|
+
const sendMessageRequest: SendMessageRequest = {
|
|
16
|
+
QueueUrl: this.WEBHOOK_BUSINESS_QUEUE_URL,
|
|
17
|
+
MessageBody: JSON.stringify(message),
|
|
18
|
+
};
|
|
19
|
+
const command: SendMessageCommand = new SendMessageCommand(sendMessageRequest);
|
|
20
|
+
await client.send(command);
|
|
21
|
+
} catch (error) {
|
|
22
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
23
|
+
throw new Error(`Error publishing message to WebhookBusinessQueue: ${errorMessage}`);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { WebhookOutboundMessage } from "@fiado/type-kit/bin/webhookBusiness/index.js";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Publisher de Fiado hacia `WebhookBusinessQueue`, que consume `webhook-business` para entregarle
|
|
5
|
+
* el evento a los suscriptores externos que tengan el tag `wh:*` del `directoryId`.
|
|
6
|
+
*
|
|
7
|
+
* Lo produce cualquier lambda de Fiado que tenga algo que avisarle a un externo (la emisión de la
|
|
8
|
+
* cuenta PCF, el dictamen de cumplimiento). Vive aquí —y no como SDK crudo en cada lambda— porque la
|
|
9
|
+
* cola es COMPARTIDA cross-lambda: el estándar Fiado publica a una cola compartida vía Publisher de
|
|
10
|
+
* `@fiado/api-invoker` (mismo patrón que `IRetailKycInboundPublisher`).
|
|
11
|
+
*
|
|
12
|
+
* El productor necesita la env var `WEBHOOK_BUSINESS_QUEUE_URL` en su `template.yml` (la misma que
|
|
13
|
+
* ya declara `webhook-business` del lado consumidor) y permiso `sqs:SendMessage` sobre la cola.
|
|
14
|
+
*
|
|
15
|
+
* El tipo del mensaje es la unión cerrada `WebhookOutboundMessage` del `@fiado/type-kit`: agregar un
|
|
16
|
+
* evento obliga a tocar también el router del consumidor, que es la conversación que tiene que
|
|
17
|
+
* pasar antes de meter un mensaje nuevo en una cola compartida.
|
|
18
|
+
*/
|
|
19
|
+
export interface IWebhookBusinessPublisher {
|
|
20
|
+
publish(message: WebhookOutboundMessage): Promise<void>;
|
|
21
|
+
}
|