diff --git a/.env b/.env index 681f736..d72f207 100644 --- a/.env +++ b/.env @@ -20,10 +20,13 @@ POSTGRES_DB=postgres POSTGRES_DATABASE=postgres POSTGRES_PORT=5433 POSTGRES_USER=postgres -POSTGRES_PASSWORD=1234 +POSTGRES_PASSWORD='1234' # Para el postgres local para generar el script de resultado de migraciones PGHOST=localhost PGUSER=alvar PGPASSWORD=alvar PGPORT=5433 + +# Proxy +CONNECTIONS_URL=https://sim-connections.savefamilygps.net diff --git a/deployment/database/base/xx-volcado-objenious.sql b/deployment/database/base/xx-volcado-objenious.sql new file mode 100644 index 0000000..4417b5c --- /dev/null +++ b/deployment/database/base/xx-volcado-objenious.sql @@ -0,0 +1,20 @@ +CREATE table if not exists objenious_lines ( + id INT PRIMARY KEY GENERATED ALWAYS AS IDENTITY, + simId BIGINT UNIQUE, + status TEXT, + iccid TEXT NOT NULL, + msisdn TEXT, + imei TEXT, + imeiChangeDate TIMESTAMPTZ, + offerCode TEXT, + preactivationDate TIMESTAMPTZ, -- No viene con hora + activationDate TIMESTAMPTZ, + commercialStatus TEXT, + commercialStatusDate TIMESTAMPTZ, + billingStatus TEXT, + billingStatusChangeDate TIMESTAMPTZ, + billingActivationDate TIMESTAMPTZ, + createDate TIMESTAMPTZ, + raw JSONB, + hash TEXT +) diff --git a/deployment/database/migrations/1.2.0_Cola-pausa-cancelacion.sql b/deployment/database/migrations/1.2.0_Cola-pausa-cancelacion.sql new file mode 100644 index 0000000..3d0791e --- /dev/null +++ b/deployment/database/migrations/1.2.0_Cola-pausa-cancelacion.sql @@ -0,0 +1,32 @@ +/** +* Para la tarea WEBINT-328-Pausas-cacelaciones. +* Almacena las pausas/cancelaciones que no se han podido hacer porque la linea esta en +* "Test" +*/ + + +DO $$ BEGIN + CREATE TYPE SUSPENDTERMINATE AS ENUM ('suspend','terminate'); +EXCEPTION + WHEN duplicate_object THEN null; +END $$; + +CREATE TABLE IF NOT EXISTS pause_cancel_tasks ( + id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY, + iccid TEXT NOT NULL, + operation_type SUSPENDTERMINATE, + last_checked TIMESTAMPTZ, -- Última vez que se ha comprobado que no esté en test + activation_date TIMESTAMPTZ, -- Fecha de activacion para comprobar si ha pasdo un mes + next_check TIMESTAMPTZ, -- Si se ha comprobado se asignará la siguiente fecha de revision + + completed_date TIMESTAMPTZ, -- Cuando se ha completado, para bien o mal. + error TEXT, + action_data JSONB -- datos de la operacion original. +); + +-- Indice de las tareas que no han terminado +CREATE INDEX idx_pause_cancel_tasks_pending +ON pause_cancel_tasks (next_check) +WHERE completed_date IS NULL; + + diff --git a/deployment/develop/docker/Dockerfile b/deployment/develop/docker/Dockerfile index ca0a124..3623791 100644 --- a/deployment/develop/docker/Dockerfile +++ b/deployment/develop/docker/Dockerfile @@ -8,6 +8,8 @@ RUN corepack enable COPY ./dist/packages ./packages COPY ./.yarnrc.yml ./ COPY ./docs ./docs +# Para las migraciones +COPY ./deployment ./deployment COPY ./package.json ./ diff --git a/deployment/develop/docker/entrypoint.sh b/deployment/develop/docker/entrypoint.sh index 416704d..2fdb3cb 100644 --- a/deployment/develop/docker/entrypoint.sh +++ b/deployment/develop/docker/entrypoint.sh @@ -1,4 +1,6 @@ #!/bin/sh cd /home -cd /home/node/app && yarn start +cd /home/node/app +yarn migrate +yarn start diff --git a/deployment/develop/jenkinsfile.groovy b/deployment/develop/jenkinsfile.groovy index 3b69366..291e0fe 100644 --- a/deployment/develop/jenkinsfile.groovy +++ b/deployment/develop/jenkinsfile.groovy @@ -69,7 +69,6 @@ pipeline { cleanRemote: false, remoteDirectory: "$APP_REMOTE_PATH", sourceFiles: "deployment/database/**/*", - removePrefix: "deployment", ), sshTransfer( cleanRemote: false, diff --git a/docs/sim-api/Activate.bru b/docs/sim-api/Activate.bru index 0d4eda1..55487be 100644 --- a/docs/sim-api/Activate.bru +++ b/docs/sim-api/Activate.bru @@ -11,7 +11,7 @@ post { } body:form-urlencoded { - iccid: 8933201125068886692 + iccid: 8933201125065160380 offer: SAVEFAMILY1 } diff --git a/docs/sim-api/Cancel.bru b/docs/sim-api/Cancel.bru index 4c5584e..642091a 100644 --- a/docs/sim-api/Cancel.bru +++ b/docs/sim-api/Cancel.bru @@ -11,7 +11,7 @@ post { } body:form-urlencoded { - iccid: 8933201125068886692 + iccid: 8933201125068890074 } settings { diff --git a/docs/sim-api/Pause.bru b/docs/sim-api/Pause.bru index cef1ed7..4a88e4d 100644 --- a/docs/sim-api/Pause.bru +++ b/docs/sim-api/Pause.bru @@ -15,7 +15,7 @@ params:query { } body:form-urlencoded { - iccid: 8933201125065160414 + iccid: 8933201125068886700 } settings { diff --git a/docs/sim-api/ReActivate.bru b/docs/sim-api/ReActivate.bru new file mode 100644 index 0000000..50308ab --- /dev/null +++ b/docs/sim-api/ReActivate.bru @@ -0,0 +1,21 @@ +meta { + name: ReActivate + type: http + seq: 13 +} + +post { + url: {{baseurl}}/sim/reActivate + body: formUrlEncoded + auth: inherit +} + +body:form-urlencoded { + iccid: 8933201125065160380 + ~offer: SAVEFAMILY1 +} + +settings { + encodeUrl: true + timeout: 0 +} diff --git a/docs/sim-api/environments/local.bru b/docs/sim-api/environments/local.bru index 79faa0b..fab4907 100644 --- a/docs/sim-api/environments/local.bru +++ b/docs/sim-api/environments/local.bru @@ -1,3 +1,4 @@ vars { baseurl: http://localhost:3000 } +color: #2E8A54 diff --git a/docs/sim-api/environments/prod.bru b/docs/sim-api/environments/prod.bru index 1ef2e33..d6e2471 100644 --- a/docs/sim-api/environments/prod.bru +++ b/docs/sim-api/environments/prod.bru @@ -1,3 +1,4 @@ vars { baseurl: https://sf-sims.savefamilygps.net } +color: #CE4F3B diff --git a/docs/sim-api/environments/simconnections.bru b/docs/sim-api/environments/simconnections.bru new file mode 100644 index 0000000..d7e876f --- /dev/null +++ b/docs/sim-api/environments/simconnections.bru @@ -0,0 +1,4 @@ +vars { + baseurl: http://sim-connections.savefamilygps.net +} +color: #C77A0F diff --git a/docs/sim-api/test proxy.bru b/docs/sim-api/test proxy.bru new file mode 100644 index 0000000..d4e17b0 --- /dev/null +++ b/docs/sim-api/test proxy.bru @@ -0,0 +1,20 @@ +meta { + name: test proxy + type: http + seq: 14 +} + +get { + url: {{baseurl}}/simconnections/alai/select?iccid=1111111111111111111 + body: none + auth: inherit +} + +params:query { + iccid: 1111111111111111111 +} + +settings { + encodeUrl: true + timeout: 0 +} diff --git a/docs/sim-objenious/Appel lines.bru b/docs/sim-objenious/Appel lines.bru index e10670f..afc428d 100644 --- a/docs/sim-objenious/Appel lines.bru +++ b/docs/sim-objenious/Appel lines.bru @@ -5,16 +5,16 @@ meta { } get { - url: https://api-getway.objenious.com/ws/lines?pageSize=10&identifier.identifierType=ICCID&identifier.identifiers=8933201125065160455 + url: https://api-getway.objenious.com/ws/lines?pageSize=1000&simStatus=ACTIVATED body: formUrlEncoded auth: bearer } params:query { - pageSize: 10 - identifier.identifierType: ICCID - identifier.identifiers: 8933201125065160455 - ~simStatus: ACTIVATED + pageSize: 1000 + simStatus: ACTIVATED + ~identifier.identifierType: ICCID + ~identifier.identifiers: 8933201125065160455 } auth:bearer { diff --git a/docs/sim-objenious/Get request by ID.bru b/docs/sim-objenious/Get request by ID.bru index bda2942..f4ebaff 100644 --- a/docs/sim-objenious/Get request by ID.bru +++ b/docs/sim-objenious/Get request by ID.bru @@ -37,7 +37,7 @@ body:form-urlencoded { } vars:pre-request { - params.id: 14557 + params.id: 15102 } settings { diff --git a/docs/sim-objenious/Mass action list.bru b/docs/sim-objenious/Mass action list.bru index b83a34a..ebbec4b 100644 --- a/docs/sim-objenious/Mass action list.bru +++ b/docs/sim-objenious/Mass action list.bru @@ -5,13 +5,13 @@ meta { } get { - url: {{actionsUrl}}/massActions?massActionId=5192767 + url: {{actionsUrl}}/massActions?massActionId=5363116 body: formUrlEncoded auth: bearer } params:query { - massActionId: 5192767 + massActionId: 5363116 ~identifier.identifierType: ICCID ~identifier.identifiers: 8933201125065160463,8933201125065160422 } diff --git a/package.json b/package.json index d40554c..2a43600 100644 --- a/package.json +++ b/package.json @@ -19,7 +19,7 @@ "migrate": "yarn db-migrate -e .env -m deployment/database/migrations -t 99.0.0" }, "dependencies": { - "@sf-alvar/db-migrate": "1.0.3", + "@sf-alvar/db-migrate": "1.0.6", "@tsconfig/node22": "^22.0.5", "amqp-connection-manager": "^5.0.0", "amqplib": "^0.10.9", diff --git a/packages/sim-consumidor-nos/.env b/packages/sim-consumidor-nos/.env index 6979d6b..597b9fa 100644 --- a/packages/sim-consumidor-nos/.env +++ b/packages/sim-consumidor-nos/.env @@ -1,5 +1,3 @@ -PORT=3000 -RABBITMQ_USER=guest -RABBITMQ_PASSWORD=guest +NOS_BASE_URL=localhost ENVIORMENT=development diff --git a/packages/sim-consumidor-nos/config/env/index.ts b/packages/sim-consumidor-nos/config/env/index.ts index 021873e..4aecfbb 100644 --- a/packages/sim-consumidor-nos/config/env/index.ts +++ b/packages/sim-consumidor-nos/config/env/index.ts @@ -1,5 +1,16 @@ import { loadEnvFile } from "node:process"; -loadEnvFile("../../.env") +import path from "node:path"; + +try { + loadEnvFile(path.join("./.env")) // base +} catch (e) { + console.error("Error cargando el .env desde ./.env") +} +try { + loadEnvFile(path.join("../../.env")) // Global +} catch (e) { + console.error("Error cargando el .env desde ../../.env") +} export const env = { ENVIRONMENT: process.env.ENVIORMENT, @@ -18,5 +29,8 @@ export const env = { RABBITMQ_SECURE: process.env.RABBITMQ_SECURE, RABBITMQ_RETRY_INTERVAL: process.env.RABBITMQ_INTERVAL, RABBITMQ_VHOST: String(process.env.RABBITMQ_VHOST), + + // ESPECIFICO NOS + NOS_BASE_URL: String(process.env.NOS_BASE_URL) }; diff --git a/packages/sim-consumidor-nos/config/eventBus.config.ts b/packages/sim-consumidor-nos/config/eventBus.config.ts new file mode 100644 index 0000000..5c84601 --- /dev/null +++ b/packages/sim-consumidor-nos/config/eventBus.config.ts @@ -0,0 +1,69 @@ +import { RabbitMQEventBus, RMQConnectionParams } from "sim-shared/infrastructure/RabbitMQEventBus.js" +import { Channel } from "amqp-connection-manager" +import { env } from "./env/index.js" + +const rmqUser = env.RABBITMQ_USER +const rmqPass = env.RABBITMQ_PASSWORD +const rmqHost = env.RABBITMQ_HOST +const rmqPort = Number(env.RABBITMQ_PORT) +const rmqSecure = false +const rmqVhost = env.RABBITMQ_VHOST + +export const rmqConnOptions = { + username: rmqUser, + password: rmqPass, + vhost: rmqVhost, + hostname: rmqHost, + port: rmqPort, + secure: rmqSecure, +} + +export const rabbitmqEventBus = new RabbitMQEventBus({ + connectionParams: rmqConnOptions, + buildStructure: buildQueues, + maxRetry: 5 +}) + +async function buildQueues(channel: Channel) { + const QUEUES = { + NOS: "sim.nos", + NOSDLX: "sim.nos.dlx", + NOSDEL: "sim.nos.delayed", + } + + const EXCHANGES = { + MAIN: "sim.exchange", + DLX: "sim.ex.nos.dlx", + DEL: "sim.ex.nos.delayed" + } + + const DELAY = 10 * 1000 + const BASE_NOS_KEY = "sim.nos.#" + + await channel.assertExchange(EXCHANGES.DEL, "topic") + await channel.assertExchange(EXCHANGES.DLX, "topic") + await channel.assertExchange(EXCHANGES.MAIN, "topic") + + await channel.assertQueue(QUEUES.NOS) + await channel.assertQueue(QUEUES.NOSDLX) + await channel.assertQueue(QUEUES.NOSDEL, { + durable: true, + arguments: { + 'x-message-ttl': DELAY, + 'x-dead-letter-exchange': EXCHANGES.MAIN, + } + }) + + // Cola dead-letter + await channel.bindQueue(QUEUES.NOSDLX, EXCHANGES.DLX, "sim.nos.#") + // Cola delay + await channel.bindQueue(QUEUES.NOSDEL, EXCHANGES.DEL, BASE_NOS_KEY) + // Cola nos -> main exchange + await channel.bindQueue(QUEUES.NOS, EXCHANGES.MAIN, BASE_NOS_KEY) + +} + +export async function startRMQClient() { + await rabbitmqEventBus.connect() + return rabbitmqEventBus +} diff --git a/packages/sim-consumidor-nos/config/eventBusConfig.ts b/packages/sim-consumidor-nos/config/eventBusConfig.ts deleted file mode 100644 index 9a0e1f3..0000000 --- a/packages/sim-consumidor-nos/config/eventBusConfig.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { RabbitMQEventBus, RMQConnectionParams } from "sim-shared/infrastructure/RabbitMQEventBus.js" -import { env } from "./env" - -const rmqUser = env.RABBITMQ_USER -const rmqPass = env.RABBITMQ_PASSWORD -const rmqHost = env.RABBITMQ_HOST -const rmqPort = Number(env.RABBITMQ_PORT) -const rmqSecure = false -const rmqVhost = env.RABBITMQ_VHOST - -export const rmqConnOptions = { - username: rmqUser, - password: rmqPass, - vhost: rmqVhost, - hostname: rmqHost, - port: rmqPort, - secure: rmqSecure, -} - -export const rabbitmqEventBus = new RabbitMQEventBus({ - connectionParams: rmqConnOptions -}) - -export async function startRMQClient() { - await rabbitmqEventBus.connect().catch(async e => { - console.error("Error en la conexion RMQ") - await rabbitmqEventBus.connect() - }) - - // Bindings especificos, deberia meterlos en la clase - try { - await rabbitmqEventBus.channel?.assertQueue("sim.nos") - } catch { - console.log("[i] Cola de sims de nos creada") - await rabbitmqEventBus.channel?.bindQueue("sim.nos", "sim.exchange", "sim.nos.*") - } - - return rabbitmqEventBus -} diff --git a/packages/sim-consumidor-nos/index.ts b/packages/sim-consumidor-nos/index.ts index 36cce2a..3214b9d 100644 --- a/packages/sim-consumidor-nos/index.ts +++ b/packages/sim-consumidor-nos/index.ts @@ -1,5 +1,5 @@ -import { startRMQClient } from "#config/eventBusConfig" +import { startRMQClient } from "#config/eventBus.config.js" import { SimNosController } from "./aplication/SimNOS.controller.js" async function startWorker() { diff --git a/packages/sim-consumidor-nos/package.json b/packages/sim-consumidor-nos/package.json index ee4549f..40057f5 100644 --- a/packages/sim-consumidor-nos/package.json +++ b/packages/sim-consumidor-nos/package.json @@ -7,7 +7,8 @@ "test": "echo \"Error: no test specified\" && exit 1", "build": "yarn tsc --project tsconfig.json && yarn tsc-alias && cp package.json ../../dist/packages/sim-consumidor-nos/", "esbuild": "esbuild index.ts --platform=node", - "start": "node ../../dist/packages/sim-consumidor-nos/index.js" + "start": "node ../../dist/packages/sim-consumidor-nos/index.js", + "dev": "tsx watch index.ts" }, "author": "", "license": "ISC", diff --git a/packages/sim-consumidor-objenious/aplication/Sim.controller.test.ts b/packages/sim-consumidor-objenious/aplication/Sim.controller.test.ts new file mode 100644 index 0000000..b408723 --- /dev/null +++ b/packages/sim-consumidor-objenious/aplication/Sim.controller.test.ts @@ -0,0 +1,118 @@ +import { describe, it, beforeEach, mock, after } from "node:test"; +import assert from "node:assert"; +import { SimController } from "./Sim.controller.js"; +import { EventBus } from "sim-shared/domain/EventBus.port.js"; +import { SimUseCases } from "./Sim.usecases.js"; +import { ConsumeMessage } from "amqplib"; +import { postgrClient, pgPool } from "#config/postgreConfig.js"; +import { httpInstance } from "#config/httpClient.config.js"; +import { OrderRepository } from "sim-shared/infrastructure/OrderRepository.js"; +import { PauseCancelTaskRepository } from "#adapters/PauseCancelTaskRepository.js"; +import { ObjeniousOperationsRepository } from "sim-shared/infrastructure/ObjeniousOperationRepository.js"; +import { ActionData } from "#domain/DTOs/objeniousapi.js"; + +describe("SimController Integration Tests (Real UseCases)", () => { + let eventBusMock: any; + let controller: SimController; + let useCases: SimUseCases; + + beforeEach(() => { + // Mock ONLY the event bus as requested + eventBusMock = { + publish: mock.fn(), + addSubscribers: mock.fn(), + consume: mock.fn(), + ack: mock.fn(async () => { }), + nack: mock.fn(async () => { }), + }; + + const operationRepository = new ObjeniousOperationsRepository( + httpInstance, + postgrClient, + ); + const orderRepository = new OrderRepository(postgrClient); + const pauseRepository = new PauseCancelTaskRepository(postgrClient); + useCases = new SimUseCases({ + httpClient: httpInstance, + operationRepository: operationRepository, + orderRepository: orderRepository, + pauseRepository: pauseRepository + }); + // @ts-expect-error + useCases.findActivationDate = async (data: ActionData) => new Date() + + controller = new SimController(eventBusMock as unknown as EventBus, useCases); + }); + + const createMockMsg = (payload: any): ConsumeMessage => { + return { + content: Buffer.from(JSON.stringify(payload)), + fields: {}, + properties: { + headers: { + message_id: "test-correlation-id" + } + }, + } as unknown as ConsumeMessage; + }; + + after(async () => { + await pgPool.end(); + }); + + describe("suspend", () => { + it("should call stage_suspend and interact with DB and EventBus", async () => { + const iccid = "test-iccid-suspend-" + Date.now(); + const msg = createMockMsg({ + key: "sim.test.pause", + payload: { + iccid: iccid + }, + headers: { + message_id: "correlation-suspend-" + iccid + } + }); + + const handler = controller.suspend(); + await handler(msg); + + // Verify that it reached the stage_suspend logic (which adds to pauseRepository) + // We can query the DB or check if ACK was called + assert.strictEqual(eventBusMock.ack.mock.callCount(), 1, "Message should be ACKed on success"); + assert.strictEqual(eventBusMock.nack.mock.callCount(), 0, "Message should not be NACKed"); + }); + }); + + describe("terminate", () => { + it("should call stage_terminate and interact with DB and EventBus", async () => { + const iccid = "test-iccid-terminate-" + Date.now(); + const msg = createMockMsg({ + key: "sim.test.pause", + payload: { + iccid: iccid + }, + headers: { + message_id: "correlation-terminate-" + iccid + } + }); + + const handler = controller.terminate(); + await handler(msg); + + assert.strictEqual(eventBusMock.ack.mock.callCount(), 1, "Message should be ACKed on success"); + assert.strictEqual(eventBusMock.nack.mock.callCount(), 0, "Message should not be NACKed"); + }); + }); + + describe("Error Handling", () => { + it("should nack if message is invalid", async () => { + const msg = { + content: Buffer.from("invalid json"), + fields: {}, + properties: {}, + } as unknown as ConsumeMessage; + const handler = controller.suspend(); + await assert.rejects(handler(msg), "Error de suspension consumiendo el mensaje no es valido"); + }); + }); +}); diff --git a/packages/sim-consumidor-objenious/aplication/Sim.controller.ts b/packages/sim-consumidor-objenious/aplication/Sim.controller.ts index 7c60673..08bc354 100644 --- a/packages/sim-consumidor-objenious/aplication/Sim.controller.ts +++ b/packages/sim-consumidor-objenious/aplication/Sim.controller.ts @@ -3,7 +3,7 @@ import { ConsumeMessage } from "amqplib"; import { SimUseCases } from "./Sim.usecases.js"; import { SimEvents } from "sim-shared/domain/SimEvents.js"; import { Result } from "sim-shared/domain/Result.js"; -import { env } from "#config/env/index.js"; +import { ActionData } from "#domain/DTOs/objeniousapi.js"; /** * La clase usa generadores de funciones para mantener el contexto @@ -37,6 +37,7 @@ export class SimController { } catch (error) { console.error('Error al decodificar JSON:', error); + console.error(Buffer.from(msg.content).toString(("utf8"))) // Aquí podrías decidir devolver el string crudo o null return undefined; } @@ -157,6 +158,9 @@ export class SimController { } } + /** + * Lo mismo que pause + */ public suspend() { return async (msg: ConsumeMessage) => { let msgData; @@ -171,14 +175,18 @@ export class SimController { } const iccid = msgData.payload.iccid - const res = await this.tryUseCase(msg, this.useCases.suspend({ + const suspendData: ActionData = { correlation_id: msgData.headers?.message_id, dueDate: this.genDueDate(2 * 60).toISOString(), identifier: { identifierType: "ICCID", - identifiers: [iccid] + identifiers: [iccid] // Por algún motivo solo he puesto un iccd por identifier } - })) + } + const useCaseRes = await this.tryUseCase(msg, this.useCases.stage_suspend(suspendData)) + /* + const res = await this.tryUseCase(msg, this.useCases.suspend(actionData)) + */ } } @@ -195,16 +203,20 @@ export class SimController { if (msgData == undefined) { return Promise.reject("Mensaje invalido") } + const iccid = msgData.payload.iccid - console.log("Mensaje procesado", msgData) - const res = await this.tryUseCase(msg, this.useCases.terminate({ + const terminateActionData: ActionData = { correlation_id: msgData.headers?.message_id, dueDate: this.genDueDate(2 * 60).toISOString(), identifier: { identifierType: "ICCID", identifiers: [iccid] } - })) + } + + //const res = await this.tryUseCase(msg, this.useCases.terminate(terminateActionData)) + const res = await this.tryUseCase(msg, this.useCases.stage_terminate(terminateActionData)) + } } diff --git a/packages/sim-consumidor-objenious/aplication/Sim.router.ts b/packages/sim-consumidor-objenious/aplication/Sim.router.ts index 79a001e..7a34e29 100644 --- a/packages/sim-consumidor-objenious/aplication/Sim.router.ts +++ b/packages/sim-consumidor-objenious/aplication/Sim.router.ts @@ -19,7 +19,7 @@ export class SimRouter { ["activate", this.simController.activate()], ["pause", this.simController.suspend()], ["cancel", this.simController.terminate()], - ["reActivate", this.simController.reActivate()], + ["reactivate", this.simController.reActivate()], ["preActivate", this.simController.preActivate()] ]); } diff --git a/packages/sim-consumidor-objenious/aplication/Sim.usecases.ts b/packages/sim-consumidor-objenious/aplication/Sim.usecases.ts index f7c79a5..8fc34ee 100644 --- a/packages/sim-consumidor-objenious/aplication/Sim.usecases.ts +++ b/packages/sim-consumidor-objenious/aplication/Sim.usecases.ts @@ -5,6 +5,8 @@ import { Result } from "sim-shared/domain/Result.js" import { ObjeniousOperation, IOperationsRepository as OperationsRepositoryPort } from "sim-shared/domain/operationsRepository.port.js" import assert from "node:assert" import { OrderRepository } from "sim-shared/infrastructure/OrderRepository.js" +import { CreatePauseCancelTaskDTO, PauseCancelTaskRepository } from "#adapters/PauseCancelTaskRepository.js" +import { ObjeniousOperationsRepository } from "sim-shared/infrastructure/ObjeniousOperationRepository.js" // TODO: // - Pasar a un archivo de DTOs @@ -12,21 +14,24 @@ import { OrderRepository } from "sim-shared/infrastructure/OrderRepository.js" export class SimUseCases { private readonly httpClient: HttpClient - private readonly operationRepository: OperationsRepositoryPort + private readonly objeniousRepository: ObjeniousOperationsRepository private readonly orderRepository: OrderRepository + private readonly pauseRepository: PauseCancelTaskRepository constructor(args: { httpClient: HttpClient, - operationRepository: OperationsRepositoryPort, - orderRepository: OrderRepository + operationRepository: ObjeniousOperationsRepository, + orderRepository: OrderRepository, + pauseRepository: PauseCancelTaskRepository }) { this.httpClient = args.httpClient - this.operationRepository = args.operationRepository + this.objeniousRepository = args.operationRepository this.orderRepository = args.orderRepository + this.pauseRepository = args.pauseRepository } private async logOperation(data: ObjeniousOperation) { - await this.operationRepository.createOperation({ + await this.objeniousRepository.createOperation({ ...data }) } @@ -70,11 +75,14 @@ export class SimUseCases { operation: args.operation, iccids: String(args.iccid), status: "noMassID", - request_id: response.data.requestId + request_id: response.data.requestId, + correlation_id: args.correlation_id } + // TODO: Esto tiene poco sentido si la operacion ya se + // tenia que haber creado en el generador this.logOperation(operation) - .then().catch(e => console.error(e)) + .then().catch(e => console.error("Error login operation", e)) if (args.correlation_id != undefined) { this.orderRepository.updateOrder({ @@ -89,7 +97,6 @@ export class SimUseCases { error: undefined, data: true } - } else { return { error: String(response.status), @@ -109,6 +116,15 @@ export class SimUseCases { public activate(activationData: ActivationData): () => Promise> { const OPERATION_URL = "/actions/activateLine" return async () => { + const iccid = activationData.identifier.identifiers + // Comporbación excepcional para saber si la linea está suspendida + const statusLinea = await this.objeniousRepository.getLinesAPI("ICCID", [String(iccid)]) + console.log("statusLinea, ", iccid, statusLinea) + if (statusLinea.data != undefined && statusLinea.data[0].status.networkStatus == "SUSPENDED") { + const res = await this.reActivate(activationData)() + return res; + } + const req = this.httpClient.client.post(OPERATION_URL, { dueDate: activationData.dueDate, identifier: activationData.identifier, @@ -192,16 +208,29 @@ export class SimUseCases { } } - public reActivate(pauseData: ActionData): () => Promise> { + public reActivate(reactivateData: ActionData): () => Promise> { const OPERATION_URL = "/actions/reactivateLine" return async () => { const req = this.httpClient.client.post(OPERATION_URL, { - ...pauseData + ...reactivateData }) try { const response = await req + // Creacion de la operacion inicial, antes de tener los datos + const operation: ObjeniousOperation = { + operation: "reactivate", + iccids: reactivateData.identifier.identifiers[0], + status: "noMassID", + request_id: response.data.requestId, + correlation_id: reactivateData.correlation_id + } + + // TODO: Esto tiene poco sentido si la operacion ya se + // tenia que haber creado en el generador + this.logOperation(operation) + .then().catch(e => console.error("Error login operation", e)) if (response.status == 200) { console.log("[o] Sim solicitud de reactivacion ", response.data) return >{ @@ -217,7 +246,7 @@ export class SimUseCases { } catch (error) { console.error("[x] Error reactivacion", (error as AxiosError).response?.status) return >{ - error: "Error reactivando la sim" + pauseData.identifier, + error: "Error reactivando la sim" + reactivateData.identifier, data: undefined } } @@ -238,6 +267,164 @@ export class SimUseCases { }) } + /** + * Metodo muy especifico para obtener la fecha e activacion o en su defecto + * la actual para aber cuando se va a completar el periodo de test de una linea + */ + private async findActivationDate(actionData: ActionData) { + const iccid = actionData.identifier.identifiers + const lineData = await this.objeniousRepository.getLinesAPI("ICCID", iccid) + let activationDate = new Date() + // Si no se pueden sacar datos de la linea guardo momentaneamente el error + // pero no se cancela la operacion, el error puede ser de objenious y no nos + // puede afectar + console.log("LineData", lineData.data) + if (lineData.error != undefined) { + console.error(lineData.error) + } else { + const activationDateStr = lineData.data[0].status.activationDate + if (activationDateStr != undefined && activationDateStr != "") { + activationDate = new Date(activationDateStr) + } + } + return activationDate + } + + + /** + * Paso previo a la suspension para evitar errores cuando el billing es test + */ + public stage_suspend(suspendData: ActionData): () => Promise> { + return async (): Promise> => { + const correlation_id = suspendData.correlation_id + const iccid = suspendData.identifier.identifiers + + + const operation: ObjeniousOperation = { + operation: "suspend", + iccids: iccid[0], + status: "running", + correlation_id: correlation_id + } + // No se registra hasta que no pase por la tabla de pausas + // this.logOperation(operation) + // .then().catch(e => console.error("Error login operation", e)) + + const fail = (error: string) => { + console.error("[Sim.usecases]", error) + if (correlation_id != undefined) { + this.orderRepository.updateOrder({ + correlation_id: correlation_id, + new_status: "failed" + }) + } + } + + // TODO REGISTRAR EL ORDER + if (correlation_id != undefined) { + await this.orderRepository.createOrder({ + correlation_id: correlation_id, + order_type: "pause" + }) + } + + let activationDate; + try { + activationDate = await this.findActivationDate(suspendData) + } catch (e) { + return { + error: String(e) + } + } + const newTask: CreatePauseCancelTaskDTO = { + iccid: iccid[0], + activation_date: activationDate, + next_check: undefined, // Que se haga instantaneamente al ser la primera + operation_type: "suspend", + action_data: suspendData + } + + const taskCreated = await this.pauseRepository.addTask(newTask) + + // Caso que la task no se pueda crear en la BDD + if (taskCreated.error != undefined) { + fail(taskCreated.error) + return { + error: taskCreated.error + } + } + + // Caso que se haya creado en la BDD + if (correlation_id != undefined) { + this.orderRepository.updateOrder({ + correlation_id: correlation_id, + new_status: "running" + }) + } + + return { + data: true + } + } + } + + /** + * Paso previo a la suspension para evitar errores cuando el billing es test + */ + public stage_terminate(terminateData: ActionData): () => Promise> { + return async (): Promise> => { + const correlation_id = terminateData.correlation_id + const iccid = terminateData.identifier.identifiers[0] + + const activationDate = await this.findActivationDate(terminateData) + const newTask: CreatePauseCancelTaskDTO = { + iccid: iccid, + activation_date: activationDate, + next_check: undefined, // Que se haga instantaneamente al ser la primera + operation_type: "terminate", + action_data: terminateData + } + + const taskCreated = await this.pauseRepository.addTask(newTask) + + const operation: ObjeniousOperation = { + operation: "terminate", + iccids: iccid, + status: "running", + correlation_id: correlation_id + } + + /** + this.logOperation(operation) + .then().catch(e => console.error("Error login operation", e)) + */ + // Caso que la task no se pueda crear en la BDD + if (taskCreated.error != undefined) { + console.error("[Sim.usecases]", taskCreated.error) + if (correlation_id != undefined) { + this.orderRepository.updateOrder({ + correlation_id: correlation_id, + new_status: "failed" + }) + } + return { + error: taskCreated.error + } + } + + // Caso que se haya creado en la BDD + if (correlation_id != undefined) { + this.orderRepository.updateOrder({ + correlation_id: correlation_id, + new_status: "running" + }) + } + + return { + data: true + } + } + } public terminate(terminationData: ActionData): () => Promise> { const OPERATION_URL = "/actions/terminateLine" return this.generateUseCase({ diff --git a/packages/sim-consumidor-objenious/config/eventBus.config.ts b/packages/sim-consumidor-objenious/config/eventBus.config.ts index 4c5db47..81f583d 100644 --- a/packages/sim-consumidor-objenious/config/eventBus.config.ts +++ b/packages/sim-consumidor-objenious/config/eventBus.config.ts @@ -27,8 +27,8 @@ export const rabbitmqEventBus = new RabbitMQEventBus({ async function buildQueues(channel: Channel) { const QUEUES = { OBJ: "sim.objenious", - DLX: "sim.objenious.dlx", - DEL: "sim.objenious.delayed" + OBJDLX: "sim.objenious.dlx", + OBJDEL: "sim.objenious.delayed", } const EXCHANGES = { @@ -45,8 +45,8 @@ async function buildQueues(channel: Channel) { await channel.assertExchange(EXCHANGES.MAIN, "topic") await channel.assertQueue(QUEUES.OBJ) - await channel.assertQueue(QUEUES.DLX) - await channel.assertQueue(QUEUES.DEL, { + await channel.assertQueue(QUEUES.OBJDLX) + await channel.assertQueue(QUEUES.OBJDEL, { durable: true, arguments: { 'x-message-ttl': DELAY, diff --git a/packages/sim-consumidor-objenious/config/httpClient.config.ts b/packages/sim-consumidor-objenious/config/httpClient.config.ts index 6d37725..427b6e8 100644 --- a/packages/sim-consumidor-objenious/config/httpClient.config.ts +++ b/packages/sim-consumidor-objenious/config/httpClient.config.ts @@ -1,6 +1,6 @@ import { HttpClient } from "sim-shared/infrastructure/HTTPClient.js" -import { JWTService } from "../aplication/JWT.service.js" import { env } from "./env/index.js" +import { jwtService } from "./jwtService.config.js" const OBJ_BASE_URL = env.OBJ_BASE_URL @@ -9,5 +9,5 @@ export const httpInstance = new HttpClient({ headers: { "content-type": " application/json; charset=utf-8" }, - jwtManager: new JWTService() + jwtManager: jwtService }) diff --git a/packages/sim-consumidor-objenious/config/jwtService.config.ts b/packages/sim-consumidor-objenious/config/jwtService.config.ts new file mode 100644 index 0000000..3d9bf78 --- /dev/null +++ b/packages/sim-consumidor-objenious/config/jwtService.config.ts @@ -0,0 +1,59 @@ +import { GrantAccessRequestBody, JWTService } from "sim-shared/aplication/JWT.service.js" +import { env } from "./env/index.js" +import { JWTHeader } from "sim-shared/domain/JWT.js" + + +const PRIVATE_KEY_PATH = env.OBJ_PEM_PATH + +const GET_TOKEN_URL = "https://idp.docapost.io/auth/realms/GETWAY/protocol/openid-connect/token" +const REFRESH_TOKEN_URL = GET_TOKEN_URL + +const DEFAULT_BODY: GrantAccessRequestBody = { + grant_type: "client_credentials", + client_id: env.OBJ_CLIENT_ID, + client_assertion_type: "urn:ietf:params:oauth:client-assertion-type:jwt-bearer", + client_assertion: env.OBJ_CLI_ASSERTION +} + + +const DEFAULT_HEADERS = { + "content-type": "application/x-www-form-urlencoded" +} + +const DEFAULT_HEADERS_JWT = { + alg: "RS256", + typ: "JWT", + kid: env.OBJ_KID, +} + +const DEFAULT_DATA_JWT = { + sub: env.OBJ_CLIENT_ID, + iss: env.OBJ_CLIENT_ID, + aud: "https://idp.docapost.io/auth/realms/GETWAY", + jti: Date.now().toString(), + +} + +function addIATHeaders(authHeaders: Object) { + const headers = { + ...authHeaders, + sub: env.OBJ_CLIENT_ID, + iss: env.OBJ_CLIENT_ID, + aud: GET_TOKEN_URL, + jti: Date.now().toString(), + iat: Math.floor(Date.now() / 1000), + exp: Math.floor(Date.now() / 1000) + 5 * 60, + } + return headers +} + +export const jwtService = new JWTService({ + transformJWTHeaders: addIATHeaders, + defaultHeaders: DEFAULT_HEADERS, + defaultBody: DEFAULT_BODY, + defaultJWTHeaders: DEFAULT_HEADERS_JWT, + defaultJWTPayload: DEFAULT_DATA_JWT, + privateKeyPath: PRIVATE_KEY_PATH, + tokenUrl: GET_TOKEN_URL, + refreshTokenUrl: REFRESH_TOKEN_URL +}) diff --git a/packages/sim-consumidor-objenious/index.ts b/packages/sim-consumidor-objenious/index.ts index c2d836c..01e9d17 100644 --- a/packages/sim-consumidor-objenious/index.ts +++ b/packages/sim-consumidor-objenious/index.ts @@ -8,6 +8,7 @@ import { SimUseCases } from "./aplication/Sim.usecases.js" import { SimController } from "./aplication/Sim.controller.js" import { SimRouter } from "./aplication/Sim.router.js" import { OrderRepository } from "sim-shared/infrastructure/OrderRepository.js" +import { PauseCancelTaskRepository } from "#adapters/PauseCancelTaskRepository.js" async function startWorker() { const rmqClient = await startRMQClient() @@ -18,15 +19,21 @@ async function startWorker() { await pgClient.checkDatabaseConnection() - const operationRepository = new ObjeniousOperationsRepository(pgClient) + const operationRepository = new ObjeniousOperationsRepository( + httpClient, + pgClient, + ) const orderRepository = new OrderRepository(pgClient) + const pauseRepository = new PauseCancelTaskRepository(pgClient) + const simActivationController = new SimController( rmqClient, new SimUseCases({ httpClient: httpClient, operationRepository: operationRepository, - orderRepository: orderRepository + orderRepository: orderRepository, + pauseRepository: pauseRepository }) ) const simRouter = new SimRouter(simActivationController, rmqClient) diff --git a/packages/sim-consumidor-objenious/infrastructure/PauseCancelTaskRepository.test.ts b/packages/sim-consumidor-objenious/infrastructure/PauseCancelTaskRepository.test.ts new file mode 100644 index 0000000..a1e48e4 --- /dev/null +++ b/packages/sim-consumidor-objenious/infrastructure/PauseCancelTaskRepository.test.ts @@ -0,0 +1,72 @@ +import { after, before, describe, it } from "node:test"; +import { CreatePauseCancelTaskDTO, PauseCancelTaskRepository } from "./PauseCancelTaskRepository.js"; +import { postgrClient } from "#config/postgreConfig.js"; +import assert from "node:assert"; + +const testTask: CreatePauseCancelTaskDTO = { + iccid: "1234", + operation_type: "suspend", + activation_date: new Date(), + next_check: new Date(), + action_data: { + dueDate: new Date().toString(), + correlation_id: "12223", + identifier: { + identifiers: ["1234"], + identifierType: "ICCID" + } + } +} + +describe("Test PauseCancelTaskRepository - DB", () => { + + const createdIds: number[] = []; + const pauseRepo = new PauseCancelTaskRepository(postgrClient) + + before(() => { + }) + + after(() => { + }) + + it("Should create a task", async () => { + const created = await pauseRepo.addTask(testTask) + assert.ok(created != undefined, "A value must be returned always") + assert.ok(created.error == undefined, "Should not return a error") + assert.ok(created.data != undefined, "Data must be returned") + createdIds.push(created.data.id) + }) + + it("Should update a existing task", async () => { + const updated = await pauseRepo.updateTask({ + id: createdIds[0], + next_check: new Date() + }) + + assert.ok(updated != undefined, "A value must be returned always") + assert.ok(updated.error == undefined, "Should not return a error") + assert.ok(updated.data != undefined, "Data must be returned") + }) + + it("Should finish a existing task", async () => { + const finish = await pauseRepo.finishTask({ + id: createdIds[0], + error: "ok" + }) + + assert.ok(finish != undefined, "A value must be returned always") + assert.ok(finish.error == undefined, "Should not return a error") + assert.ok(finish.data != undefined, "Data must be returned") + }) + + it("Should get at least 1 pending task", async () => { + const created = await pauseRepo.addTask(testTask) + const pending = await pauseRepo.getPending() + + assert.ok(pending != undefined, "A value must be returned always") + assert.ok(pending.error == undefined, "Should not return a error") + assert.ok(pending.data != undefined, "Data must be returned") + + console.log("--> ", pending.data[0]) + }) +}) diff --git a/packages/sim-consumidor-objenious/infrastructure/PauseCancelTaskRepository.ts b/packages/sim-consumidor-objenious/infrastructure/PauseCancelTaskRepository.ts new file mode 100644 index 0000000..e8bb1da --- /dev/null +++ b/packages/sim-consumidor-objenious/infrastructure/PauseCancelTaskRepository.ts @@ -0,0 +1,126 @@ +import { Result } from "sim-shared/domain/Result.js"; +import { QueryResult } from "pg"; +import { PgClient } from "sim-shared/infrastructure/PgClient.js"; +import { AxiosError } from "axios"; +import { ActionData } from "#domain/DTOs/objeniousapi.js"; + +export type PauseCancelTask = { + id: number; + iccid: string; + operation_type: "suspend" | "terminate", + last_checked?: Date | null; + activation_date?: Date | null; + next_check?: Date | null; + completed_date?: Date | null; + error?: string | null; + action_data: ActionData +} + +export type CreatePauseCancelTaskDTO = Pick +export type UpdatePauseCancelTaskDTO = Pick +export type FinishPauseCancelTaskDTO = Pick + +/** + * Repositorio para compensar los problemas de cacelcaiones/pausas de objenious a + * la hora aplicarlo sobre una linea con el billing a test. + */ +export class PauseCancelTaskRepository { + constructor( + private readonly pgClient: PgClient + ) { + } + + /** + * Obtiene las siguientes que se pueden lanzar, puede haber más pero + * estan pendientes + */ + public async getPending(): Promise> { + const sql = ` + SELECT * FROM pause_cancel_tasks + WHERE completed_date IS NULL + AND (next_check <= NOW() OR next_check IS NULL) + ORDER BY id ASC; + `; + + try { + const res: QueryResult = await this.pgClient.query(sql); + return { + data: res.rows + } + } catch (e) { + return { + error: (e as AxiosError).message + } + } + } + + public async addTask(task: CreatePauseCancelTaskDTO): Promise> { + + const sql = ` + INSERT INTO pause_cancel_tasks (iccid, activation_date, next_check, last_checked, operation_type, action_data) + VALUES ($1, $2, $3, now(), $4, $5) + RETURNING *; + `; + try { + const values = [task.iccid, task.activation_date, task.next_check, task.operation_type, JSON.stringify(task.action_data)]; + const res: QueryResult = await this.pgClient.query(sql, values); + return { + data: res.rows[0] + } + } catch (e) { + return { + error: (e as AxiosError).message + } + } + } + + /** + * Se ha vuelto a comprobar la tarea pero sigue en test + */ + public async updateTask(updateData: UpdatePauseCancelTaskDTO): Promise> { + + const sql = ` + UPDATE pause_cancel_tasks + SET last_checked = now(), next_check = $1 + WHERE id = $2 + RETURNING *; + `; + try { + const res = await this.pgClient.query(sql, [updateData.next_check, updateData.id]); + return { + data: res.rows[0] + } + } catch (e) { + return { + error: (e as AxiosError).message + } + + } + } + + + /** + * La tarea ha termiando bien o mal + */ + public async finishTask(finishData: FinishPauseCancelTaskDTO) { + const sql = ` + UPDATE pause_cancel_tasks + SET completed_date = NOW(), error = $1 + WHERE id = $2 + RETURNING *; + `; + + try { + const res = await this.pgClient.query(sql, [finishData.error, finishData.id]); + return { + data: res.rows[0] + } + } catch (e) { + return { + error: (e as AxiosError).message + } + + } + } + +} diff --git a/packages/sim-consumidor-objenious/package.json b/packages/sim-consumidor-objenious/package.json index 135c24e..5fc7f6e 100644 --- a/packages/sim-consumidor-objenious/package.json +++ b/packages/sim-consumidor-objenious/package.json @@ -53,7 +53,7 @@ } }, "scripts": { - "test": "echo \"Error: no test specified\" && exit 1", + "test": "node --import tsx --test ./**/*.test.ts", "dev": "tsx watch index.ts", "build": "tsc --build && yarn tsc-alias -p tsconfig.json && cp .env package.json ../../dist/packages/sim-consumidor-objenious/", "start": "node ../../dist/packages/sim-consumidor-objenious/index.js", diff --git a/packages/sim-entrada-eventos/aplication/Sim.controller.ts b/packages/sim-entrada-eventos/aplication/Sim.controller.ts index 0c69c8b..62ebfc4 100644 --- a/packages/sim-entrada-eventos/aplication/Sim.controller.ts +++ b/packages/sim-entrada-eventos/aplication/Sim.controller.ts @@ -36,7 +36,6 @@ export class SimController { }) { return async (req: Request, res: Response) => { const body = req.body - // 1. Validacion del body if (args.validator != undefined) { const validationResult = args.validator.validate(body) @@ -132,6 +131,21 @@ export class SimController { }) } + + public reActivation() { + return this.controllerGenerator<{ iccid: string, offer: string }, { iccid: string, offer: string, compañia: string }>({ + validator: iccidValidator, + mapBody: (b) => { + const { iccid, offer } = b + const compañia = companyFromIccid(iccid) + return { iccid, compañia, offer } + }, + useCase: (args) => this.simUseCases.reActivation(args), + onError: (d, e) => console.error("[x] Error reactivacion: ", d, e), + onSuccess: console.log + }) + } + public cancelation() { return this.controllerGenerator<{ iccid: string }, { iccid: string, compañia: string }>({ validator: iccidValidator, diff --git a/packages/sim-entrada-eventos/aplication/Sim.usecases.ts b/packages/sim-entrada-eventos/aplication/Sim.usecases.ts index 22e1812..a81fb3d 100644 --- a/packages/sim-entrada-eventos/aplication/Sim.usecases.ts +++ b/packages/sim-entrada-eventos/aplication/Sim.usecases.ts @@ -130,6 +130,36 @@ export class SimUsecases { } } + async reActivation(args: { iccid: string, compañia: string, offer: string }): + Promise> { + const activationEvent = { + key: `sim.${args.compañia}.reactivate`, + payload: { + iccid: args.iccid, + offer: args.offer + } + } + const activationWithId = this.addMessage_id(activationEvent) + console.log("[d] Reactivation ", activationWithId) + await this.eventBus.publish([activationWithId]) + const createdOrder = await this.saveOrder(activationWithId) + + if (createdOrder.error != undefined) { + console.error(createdOrder.error) + return { + error: createdOrder.error + } + } + + return { + data: { + iccid: args.iccid, + operation: "reactivate", + message_id: createdOrder.data?.correlation_id + } + } + } + async preActivation(args: { iccid: string, compañia: string }): Promise> { @@ -174,8 +204,10 @@ export class SimUsecases { const cancelationWithId = this.addMessage_id(cancelationEvent) console.log("[d] Cancelation ", cancelationWithId) + await this.eventBus.publish([cancelationWithId]) const savedOrder = await this.saveOrder(cancelationWithId) + if (savedOrder.error != undefined) { console.error(savedOrder.error) return { @@ -205,11 +237,12 @@ export class SimUsecases { iccid: args.iccid } } + const pauseWithId = this.addMessage_id(pauseEvent) console.log("[d] Pause", pauseWithId) await this.eventBus.publish([pauseWithId]) - await this.saveOrder(pauseWithId) - const savedOrder = await this.saveOrder(pauseWithId) + //await this.saveOrder(pauseWithId) + const savedOrder = await this.saveOrder(pauseWithId) if (savedOrder.error != undefined) { console.error(savedOrder.error) diff --git a/packages/sim-entrada-eventos/config/env/index.ts b/packages/sim-entrada-eventos/config/env/index.ts index 3923ba0..c8d6370 100644 --- a/packages/sim-entrada-eventos/config/env/index.ts +++ b/packages/sim-entrada-eventos/config/env/index.ts @@ -22,4 +22,5 @@ export const env = { RABBITMQ_SECURE: process.env.RABBITMQ_SECURE, RABBITMQ_RETRY_INTERVAL: process.env.RABBITMQ_INTERVAL, RABBITMQ_VHOST: String(process.env.RABBITMQ_VHOST), + CONNECTIONS_URL: String(process.env.CONNECTIONS_URL) }; diff --git a/packages/sim-entrada-eventos/index.ts b/packages/sim-entrada-eventos/index.ts index 39ea518..cd3953b 100644 --- a/packages/sim-entrada-eventos/index.ts +++ b/packages/sim-entrada-eventos/index.ts @@ -5,6 +5,7 @@ import { simRoutes } from "./infrastructure/simRoutes.http.js" import { rabbitmqEventBus } from '#config/eventBusConfig.js'; import { env } from "#config/env/index.js" import { orderRoutes } from "#adapters/orderRoutes.http.js"; +import { connectionsRoutes } from "#adapters/simconnectionsRoutes.js"; const PORT = env.API_PORT const HOSTNAME = "0.0.0.0" @@ -26,6 +27,7 @@ app.use(express.json()); app.use(express.urlencoded({ extended: true })); app.use("/sim", simRoutes) +app.use("/simconnections", connectionsRoutes) app.use("/orders", orderRoutes) app.use("/docs", express.static(path.join(process.cwd(), '../../docs'))) diff --git a/packages/sim-entrada-eventos/infrastructure/simRoutes.http.ts b/packages/sim-entrada-eventos/infrastructure/simRoutes.http.ts index abc6b7c..9276ea6 100644 --- a/packages/sim-entrada-eventos/infrastructure/simRoutes.http.ts +++ b/packages/sim-entrada-eventos/infrastructure/simRoutes.http.ts @@ -23,6 +23,7 @@ simRoutes.get("/status", () => { }) simRoutes.post("/save", simController.save()) simRoutes.post("/activate", simController.activation()) +simRoutes.post("/reActivate", simController.reActivation()) simRoutes.post("/preActivate", simController.preactivation()) @@ -35,4 +36,5 @@ simRoutes.post("/test", simController.test()) // Proceso especifico de ALAI para liberar sims canceladas simRoutes.post("/free", simController.free()) + export { simRoutes } diff --git a/packages/sim-entrada-eventos/infrastructure/simconnectionsRoutes.ts b/packages/sim-entrada-eventos/infrastructure/simconnectionsRoutes.ts new file mode 100644 index 0000000..3f04022 --- /dev/null +++ b/packages/sim-entrada-eventos/infrastructure/simconnectionsRoutes.ts @@ -0,0 +1,87 @@ +import { env } from "#config/env/index.js" +import { Router } from "express" +import { ClientRequest, IncomingMessage } from "http" +import { createProxyMiddleware } from "http-proxy-middleware" +import { Request } from "express" + +export const connectionsRoutes = Router() + +const CONNECTIONS_URL = env.CONNECTIONS_URL// TODO: Meter al ENV +//const CONNECTIONS_URL = "http://sf-nfc-server.savefamilygps.net" + +console.log("CONNURL: ", CONNECTIONS_URL) + +connectionsRoutes.use("", createProxyMiddleware({ + target: CONNECTIONS_URL, + changeOrigin: true, + pathRewrite: { + '^/': "/simconnections/" + }, + on: { + proxyReq: (proxyReq: ClientRequest, req: Request) => { + const protocol = req.protocol; + const host = req.get('host'); + const originalFullUrl = `${protocol}://${host}${req.originalUrl}`; + const destinationFullUrl = `${CONNECTIONS_URL}${proxyReq.path}`; + /* + constnsole.log('──────────────────────────────────────────────────'); + console.log(`[PROXY_DEBUG]`); + console.log(` ENTRADA: ${originalFullUrl}`); + console.log(` MÉTODO : ${req.method}`); + console.log(` DESTINO: ${destinationFullUrl}`); + console.log('──────────────────────────────────────────────────'); + */ + console.log(`[Proxy Req]: ${req.method} ${req.url} -> ${proxyReq.path}`); + }, + proxyRes: (proxyRes, req, res) => { + console.log(`[Proxy Res] Status: ${proxyRes.statusCode} desde ${req.url}`); + }, + error: (err, req, res) => { + console.error('[Proxy Error]:', err); + + // Validamos que 'res' tenga el método 'status' (típico de Express Response) + if ('status' in res) { + //@ts-ignore + res.status(500).json({ message: 'Error interno en el Gateway' }); + } + }, + } +})) + + + +// Rutas +/** +connectionsRoutes.post('/simconnections/alai/preactivate',); +connectionsRoutes.get('/simconnections/alai/pause',); +connectionsRoutes.post('/simconnections/alai/terminate',); +connectionsRoutes.get('/simconnections/alai/pauseByPhone',); +connectionsRoutes.get('/simconnections/alai/active',); +connectionsRoutes.get('/simconnections/alai/change_orderid',); +connectionsRoutes.get('/simconnections/alai/select',); +connectionsRoutes.get('/simconnections/alai/select-iccid',); +connectionsRoutes.get('/simconnections/alai/selectFromDb',); +connectionsRoutes.get('/simconnections/alai/selectPage',); +connectionsRoutes.post('/simconnections/alai/schedulePause',); +connectionsRoutes.get('/simconnections/shopify/getbyWP',); +connectionsRoutes.get('/simconnections/shopify/getbyWPS',); + +/// + +connectionsRoutes.get('/simconnections/sim/associate',); +connectionsRoutes.post('/simconnections/sim/search',); +connectionsRoutes.post('/simconnections/sim/historic',); +connectionsRoutes.post('/simconnections/sim/update',); + +/// + +connectionsRoutes.post('/simconnections/nos/activate',); +connectionsRoutes.get('/simconnections/nos/select',); +connectionsRoutes.get('/simconnections/nos/selectPage',); + +//Unificación +connectionsRoutes.post('/simconnections/sim/active',); // True false +connectionsRoutes.patch('/simconnections/sim/pause',); +connectionsRoutes.get('/simconnections/sim/select',); +connectionsRoutes.get('/simconnections/sim/select-phone',); +**/ diff --git a/packages/sim-entrada-eventos/package.json b/packages/sim-entrada-eventos/package.json index b59bdc9..ed10c87 100644 --- a/packages/sim-entrada-eventos/package.json +++ b/packages/sim-entrada-eventos/package.json @@ -53,6 +53,7 @@ "cors": "*", "dotenv": "*", "express": "*", + "http-proxy-middleware": "^3.0.5", "sim-shared": "sim-shared:*", "typescript": "*" }, diff --git a/packages/sim-objenious-cron/.env b/packages/sim-objenious-cron/.env index 4df9ec5..1121f37 100644 --- a/packages/sim-objenious-cron/.env +++ b/packages/sim-objenious-cron/.env @@ -7,6 +7,6 @@ OBJ_KID=xNfbMiyL1ORXGP8lElhcv8nVaG3EJKye4Lc1YoN3I1E OBJ_BASE_URL=https://api-getway.objenious.com/ws # OBJ_BASE_URL=https://api-getway.objenious.com/ws/test -# NOTIFICATION_URL="https://sf-sim-activation.savefamilygps.net/send-activation-mail" -NOTIFICATION_URL="localhost" +NOTIFICATION_URL="https://sf-sim-activation.savefamilygps.net/send-activation-mail" +# NOTIFICATION_URL="localhost" SIM_ACTIVATION_API_KEY=9e48c4ac-1ab0-4397-b3f3-6c239200dfe6 diff --git a/packages/sim-objenious-cron/config/env/index.ts b/packages/sim-objenious-cron/config/env/index.ts index 229480e..bf1b5f9 100644 --- a/packages/sim-objenious-cron/config/env/index.ts +++ b/packages/sim-objenious-cron/config/env/index.ts @@ -31,15 +31,15 @@ export const env = { OBJ_KID: String(process.env.OBJ_KID), OBJ_BASE_URL: String(process.env.OBJ_BASE_URL), - NOTIFICATION_URL: String(process.env.NOTIFICATION_URL), - SIM_ACTIVATION_API_KEY: String(process.env.SIM_ACTIVATION_API_KEY) + NOTIFICATION_URL: String(process.env.NOTIFICATION_URL ?? ""), + SIM_ACTIVATION_API_KEY: String(process.env.SIM_ACTIVATION_API_KEY ?? "") }; // assert las partes criticas assert(env.RABBITMQ_PASSWORD != undefined) assert(env.RABBITMQ_USER != undefined) -assert(env.SIM_ACTIVATION_API_KEY != undefined) -assert(env.NOTIFICATION_URL != undefined) +assert(env.SIM_ACTIVATION_API_KEY != "") +assert(env.NOTIFICATION_URL != "") if (env.ENVIRONMENT == "production") { assert(env.RABBITMQ_PASSWORD != "guest") @@ -47,3 +47,5 @@ if (env.ENVIRONMENT == "production") { } +console.log("[i] verificado env") + diff --git a/packages/sim-objenious-cron/config/httpClient.config.ts b/packages/sim-objenious-cron/config/httpClient.config.ts index 89fe5d1..c526054 100644 --- a/packages/sim-objenious-cron/config/httpClient.config.ts +++ b/packages/sim-objenious-cron/config/httpClient.config.ts @@ -1,6 +1,7 @@ import { HttpClient } from "sim-shared/infrastructure/HTTPClient.js" import { env } from "./env/index.js" -import { JWTService } from "packages/sim-consumidor-objenious/aplication/JWT.service.js" +import { jwtService } from "./jwtService.config.js" + const OBJ_BASE_URL = env.OBJ_BASE_URL @@ -9,5 +10,5 @@ export const httpInstance = new HttpClient({ headers: { "content-type": " application/json; charset=utf-8" }, - jwtManager: new JWTService() + jwtManager: jwtService }) diff --git a/packages/sim-objenious-cron/config/intranetPostgresConfig.ts b/packages/sim-objenious-cron/config/intranetPostgresConfig.ts new file mode 100644 index 0000000..7797136 --- /dev/null +++ b/packages/sim-objenious-cron/config/intranetPostgresConfig.ts @@ -0,0 +1,20 @@ +/** + * Cliente de postgres para la intranet. Se usa solo porque hace falta para el + * volcado de datos, si se usa en mas partes algo estás haciendo mal. + */ + +import { Pool } from 'pg'; +import { PgClient } from 'sim-shared/infrastructure/PgClient.js' +import { env } from './env/index.js'; + +export const pgPoolIntranet = new Pool({ + user: env.POSTGRES_USER, + host: env.POSTGRES_HOST, + database: "intranet", + password: env.POSTGRES_PASSWORD, + port: Number(env.POSTGRES_PORT) || 5432, +}); + +export const postgresClientIntranet = new PgClient({ + pool: pgPoolIntranet +}) diff --git a/packages/sim-objenious-cron/config/jwtService.config.ts b/packages/sim-objenious-cron/config/jwtService.config.ts new file mode 100644 index 0000000..3d9bf78 --- /dev/null +++ b/packages/sim-objenious-cron/config/jwtService.config.ts @@ -0,0 +1,59 @@ +import { GrantAccessRequestBody, JWTService } from "sim-shared/aplication/JWT.service.js" +import { env } from "./env/index.js" +import { JWTHeader } from "sim-shared/domain/JWT.js" + + +const PRIVATE_KEY_PATH = env.OBJ_PEM_PATH + +const GET_TOKEN_URL = "https://idp.docapost.io/auth/realms/GETWAY/protocol/openid-connect/token" +const REFRESH_TOKEN_URL = GET_TOKEN_URL + +const DEFAULT_BODY: GrantAccessRequestBody = { + grant_type: "client_credentials", + client_id: env.OBJ_CLIENT_ID, + client_assertion_type: "urn:ietf:params:oauth:client-assertion-type:jwt-bearer", + client_assertion: env.OBJ_CLI_ASSERTION +} + + +const DEFAULT_HEADERS = { + "content-type": "application/x-www-form-urlencoded" +} + +const DEFAULT_HEADERS_JWT = { + alg: "RS256", + typ: "JWT", + kid: env.OBJ_KID, +} + +const DEFAULT_DATA_JWT = { + sub: env.OBJ_CLIENT_ID, + iss: env.OBJ_CLIENT_ID, + aud: "https://idp.docapost.io/auth/realms/GETWAY", + jti: Date.now().toString(), + +} + +function addIATHeaders(authHeaders: Object) { + const headers = { + ...authHeaders, + sub: env.OBJ_CLIENT_ID, + iss: env.OBJ_CLIENT_ID, + aud: GET_TOKEN_URL, + jti: Date.now().toString(), + iat: Math.floor(Date.now() / 1000), + exp: Math.floor(Date.now() / 1000) + 5 * 60, + } + return headers +} + +export const jwtService = new JWTService({ + transformJWTHeaders: addIATHeaders, + defaultHeaders: DEFAULT_HEADERS, + defaultBody: DEFAULT_BODY, + defaultJWTHeaders: DEFAULT_HEADERS_JWT, + defaultJWTPayload: DEFAULT_DATA_JWT, + privateKeyPath: PRIVATE_KEY_PATH, + tokenUrl: GET_TOKEN_URL, + refreshTokenUrl: REFRESH_TOKEN_URL +}) diff --git a/packages/sim-objenious-cron/index.ts b/packages/sim-objenious-cron/index.ts index e9cf2ef..b624356 100644 --- a/packages/sim-objenious-cron/index.ts +++ b/packages/sim-objenious-cron/index.ts @@ -5,6 +5,12 @@ import { httpInstance } from "./config/httpClient.config.js" import { CheckObjeniousRequests } from "./tasks/check_objenious_request.js" import { ObjeniousOperationsRepository } from "sim-shared/infrastructure/ObjeniousOperationRepository.js" import { OrderRepository } from "sim-shared/infrastructure/OrderRepository.js" +import { TaskVolcadoLineas } from "./tasks/volcado_lineas.js" +import { ObjeniousLinesRepository } from "./infranstructure/ObjeniousLinesRepository.js" +import { postgresClientIntranet } from "./config/intranetPostgresConfig.js" +import { PauseCancelTaskRepository } from "packages/sim-consumidor-objenious/infrastructure/PauseCancelTaskRepository.js" +import { PauseTerminateTask } from "./tasks/check_pause_terminate.js" +import { SimUseCases } from "packages/sim-consumidor-objenious/aplication/Sim.usecases.js" async function startCron() { const commonSettings = { @@ -14,10 +20,16 @@ async function startCron() { const httpClient = httpInstance const pgClient = new PgClient({ pool: pgPool }) + + console.log("[i] Comprobando conexion con la BDD ") await pgClient.checkDatabaseConnection() - await pgClient.checkDatabaseConnection() - const operationRepository = new ObjeniousOperationsRepository(pgClient) + + const operationRepository = new ObjeniousOperationsRepository( + httpClient, + pgClient, + ) const orderRepository = new OrderRepository(pgClient) + const objeniousLineRepository = new ObjeniousLinesRepository(postgresClientIntranet) const objTask = new CheckObjeniousRequests( operationRepository, @@ -25,23 +37,56 @@ async function startCron() { httpClient, ) + const objeniosRepo = new ObjeniousOperationsRepository( + httpClient, + pgClient + ) + + const volcadoLineasTask = new TaskVolcadoLineas( + objeniousLineRepository, + objeniosRepo + ) + + const pauseRepo = new PauseCancelTaskRepository(pgClient) + const simUsecases = new SimUseCases({ + httpClient: httpClient, + operationRepository: operationRepository, + orderRepository: orderRepository, + pauseRepository: pauseRepo + }) + + const pauseTask = new PauseTerminateTask( + objeniosRepo, + pauseRepo, + simUsecases, + orderRepository + ) + await objTask.getPendingOperations() - + const PERIODO_PETICIONES = 10 * 60 * 1000 const interval = setInterval(async () => { - console.log("Updating...") - await objTask.getPendingOperations() - console.log("Update finished") - }, 10 * 60 * 1000) - /* - const task = cron.createTask("* * * * *", async () => { - } - , { - ...commonSettings, - name: "Test" - }) -*/ + try { + await objTask.getPendingOperations() + } catch (e) { + console.error("[x] Error de actualizacion de las lineas ") + } + }, PERIODO_PETICIONES) + + const PERIODO_VOLCADO = 60 * 60 * 1000 + const volcadoInterval = setInterval(async () => { + try { + await volcadoLineasTask.loadLines() + } catch (e) { + console.error("[x] Volcado de lineas de Objenious Fallido", e) + } + }, PERIODO_VOLCADO) + + await pauseTask.run() + const PERIODO_CANCELACIONES = 60 * 60 * 1000; + const clacelacionesInterval = setInterval(async () => { + await pauseTask.run() + }, PERIODO_CANCELACIONES) - //await objTask.getPendingOperations() } diff --git a/packages/sim-objenious-cron/infranstructure/ObjeniousLinesRepository.test.ts b/packages/sim-objenious-cron/infranstructure/ObjeniousLinesRepository.test.ts new file mode 100644 index 0000000..7fa5623 --- /dev/null +++ b/packages/sim-objenious-cron/infranstructure/ObjeniousLinesRepository.test.ts @@ -0,0 +1,59 @@ +import test, { after, before, describe } from "node:test"; +import { CreateObjeniousLineDTO } from "sim-shared/domain/objeniousLine.js"; +import { ObjeniousLinesRepository } from "./ObjeniousLinesRepository.js"; +import { postgrClient } from "../config/postgreConfig.js"; +import assert from "node:assert"; + +describe("Line insertion test", async () => { + //const pgClient = postgreClientIntranet + const pgClient = postgrClient // En prod hay que usar el de Intrantet para usar la otra base de datos + const lineRepository = new ObjeniousLinesRepository(pgClient) + const lineaTest: CreateObjeniousLineDTO = { + simId: 1234, + iccid: "9999999999999", + msisdn: "34654674732", + imei: "219789481293", + imeiChangeDate: new Date(), + offerCode: "SAVEFAMILY1", + status: "ACTIVATED", + preactivationDate: new Date(), + activationDate: new Date(), + commercialStatus: "test", + commercialStatusDate: new Date(), + billingStatus: "test", + billingStatusChangeDate: new Date(), + billingActivationDate: new Date(), + createDate: new Date(), + raw: { test: "test" } as any // Para este test no hace falta + } + + // Clean up before and after tests to ensure isolation + const cleanup = async () => { + await pgClient.query("DELETE FROM objenious_lines WHERE simId = 1234"); + }; + + before(async () => { + await cleanup() + }) + + after(async () => { + await cleanup() + }) + + test("Should insert new line", async () => { + const res = await lineRepository.insertOrUpdate(lineaTest) + assert.ok(res != undefined, "The line wasn't created") + }) + + test("Should not update a line if the hash is the same", async () => { + const res = await lineRepository.insertOrUpdate(lineaTest) + assert.ok(res == undefined, "The line have been updated") + }) + + test("Should update a line if the hash changes", async () => { + const updated = structuredClone(lineaTest) + lineaTest.billingActivationDate = new Date() + const res = await lineRepository.insertOrUpdate(lineaTest) + assert.ok(res != undefined, "The line have been updated") + }) +}) diff --git a/packages/sim-objenious-cron/infranstructure/ObjeniousLinesRepository.ts b/packages/sim-objenious-cron/infranstructure/ObjeniousLinesRepository.ts new file mode 100644 index 0000000..4bba217 --- /dev/null +++ b/packages/sim-objenious-cron/infranstructure/ObjeniousLinesRepository.ts @@ -0,0 +1,112 @@ +/** + * Repositorio para el volcado de lineas de objenious en intranet + * solo para uso en el volcado. + */ +import { createHash } from "node:crypto"; +import { PoolClient } from "pg"; +import { CreateObjeniousLineDTO } from "sim-shared/domain/objeniousLine.js"; +import { PgClient } from "sim-shared/infrastructure/PgClient.js"; + +export class ObjeniousLinesRepository { + constructor( + private pgClient: PgClient + ) { + } + + private generateLineHash(data: CreateObjeniousLineDTO) { + try { + const lineStr = JSON.stringify(data) + const hash = createHash("sha256").update(lineStr).digest("base64url") + return hash + } catch (e) { + console.error("[x] Error generando el hash de la linea", data) + return undefined + } + } + + public async insertOrUpdate(data: CreateObjeniousLineDTO) { + const query = ` + INSERT INTO objenious_lines ( + simId, + iccid, + msisdn, + imei, + imeiChangeDate, + offerCode, + status, + preactivationDate, + activationDate, + commercialStatus, + commercialStatusDate, + billingStatus, + billingStatusChangeDate, + billingActivationDate, + createDate, + raw, + hash + ) VALUES ( + $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17 + ) + ON CONFLICT (simId) + DO UPDATE SET + iccid = EXCLUDED.iccid, + msisdn = EXCLUDED.msisdn, + imei = EXCLUDED.imei, + imeiChangeDate = EXCLUDED.imeiChangeDate, + offerCode = EXCLUDED.offerCode, + status = EXCLUDED.status, + preactivationDate = EXCLUDED.preactivationDate, + activationDate = EXCLUDED.activationDate, + commercialStatus = EXCLUDED.commercialStatus, + commercialStatusDate = EXCLUDED.commercialStatusDate, + billingStatus = EXCLUDED.billingStatus, + billingStatusChangeDate = EXCLUDED.billingStatusChangeDate, + billingActivationDate = EXCLUDED.billingActivationDate, + raw = EXCLUDED.raw, + hash = EXCLUDED.hash + WHERE objenious_lines.hash IS DISTINCT FROM EXCLUDED.hash + RETURNING id; + `; + + const lineHash = this.generateLineHash(data) + + if (lineHash == undefined) { + console.error("[x] Ignorando linea ", data) + return; + } + + const values = [ + data.simId, + data.iccid, + data.msisdn, + data.imei, + data.imeiChangeDate, + data.offerCode, + data.status, + data.preactivationDate, + data.activationDate, + data.commercialStatus, + data.commercialStatusDate, + data.billingStatus, + data.billingStatusChangeDate, + data.billingActivationDate, + data.createDate || new Date(), // Default a ahora si no viene + JSON.stringify(data.raw), // El driver de pg requiere string o el objeto directo para JSONB + lineHash + ]; + + let client: PoolClient | undefined = undefined; + try { + client = await this.pgClient.connect(); + const res = await client.query<{ id: number }>(query, values); + return res.rows[0]; + } catch (err) { + console.error('Error en la inserción:', err); + throw err; + } finally { + if (client != undefined) { + client.release() + } + } + } +} diff --git a/packages/sim-objenious-cron/package.json b/packages/sim-objenious-cron/package.json index bf8da38..6e0d860 100644 --- a/packages/sim-objenious-cron/package.json +++ b/packages/sim-objenious-cron/package.json @@ -5,20 +5,6 @@ "description": "", "main": "index.ts", "imports": { - "#config/*.js": { - "types": "./config/*.ts", - "default": "./config/*.js" - }, - "#config/*": { - "types": "./config/*.ts", - "default": "./config/*.js" - }, - "#shared/*.js": { - "default": "../sim-shared/*.js" - }, - "#shared/*": { - "default": "../sim-shared/*.js" - }, "#adapters/*.js": { "types": "./infrastructure/*.ts", "default": "./infrastructure/*.js" @@ -45,8 +31,8 @@ } }, "scripts": { - "test": "echo \"Error: no test specified\" && exit 1", - "build": "tsc --build && tsc-alias -p tsconfig.json && cp package.json ../../dist/packages/sim-objenious-cron/", + "test": "node --import tsx --test ./**/*.test.ts", + "build": "tsc --build && tsc-alias -p tsconfig.json && cp .env package.json ../../dist/packages/sim-objenious-cron/", "dev": "tsx watch index.ts", "start": "node ../../dist/packages/sim-objenious-cron/index.js" }, diff --git a/packages/sim-objenious-cron/tasks/check_objenious_request.ts b/packages/sim-objenious-cron/tasks/check_objenious_request.ts index 49c1f2a..ed33bb8 100644 --- a/packages/sim-objenious-cron/tasks/check_objenious_request.ts +++ b/packages/sim-objenious-cron/tasks/check_objenious_request.ts @@ -1,12 +1,13 @@ -import { env } from "#config/env/index.js"; +import { env } from "../config/env/index.js"; import { OrderRepository } from "sim-shared/infrastructure/OrderRepository.js"; import axios from "axios"; import { IOperationsRepository, Objenious, ObjeniousOperation, ObjeniousOperationChange, StatusEnum } from "sim-shared/domain/operationsRepository.port.js"; import { HttpClient } from "sim-shared/infrastructure/HTTPClient.js"; +import { ObjeniousOperationsRepository } from "packages/sim-shared/infrastructure/ObjeniousOperationRepository.js"; export class CheckObjeniousRequests { constructor( - private readonly operationsRepository: IOperationsRepository, + private readonly operationsRepository: ObjeniousOperationsRepository, private readonly orderRepository: OrderRepository, private readonly httpClient: HttpClient ) { @@ -16,6 +17,7 @@ export class CheckObjeniousRequests { * TODO: meter a una funcion a parte task con los 3 pasos */ public async getPendingOperations() { + console.log("[i] Inicio revision de peticiones") // 1. Se obtienen todas las operaciones pendientes de la BDD const pendingOperations = await this.operationsRepository.getPendingOperations() @@ -49,11 +51,14 @@ export class CheckObjeniousRequests { console.log("[cron] Solicitando status para", merged.map(e => e.id)) const result = await this.getMassActionsStatus(merged) + + console.log("[o] Revisión de eventos completa") } /** * Para una lista de operaciones **con mass_action_id** se comprueba si han tenido alguna actualizacion * Devuelve el numero de operaciones comprobadas. + * TODO: Esto va en un repositorio */ private async getMassActionsStatus(requestList: ObjeniousOperation[]) { if (requestList.length == 0) return 0; @@ -119,9 +124,6 @@ export class CheckObjeniousRequests { if (uorStatus == "finished") { console.log(" ****> Status", uorStatus) - if (uorStatus != "finished") { - console.error("!!! Notificando estado no finished") - } const targetIccids = originalAction.iccids const lineData = await this.getLineData(targetIccids) console.log("[i] lineData", lineData.content[0]) @@ -136,7 +138,7 @@ export class CheckObjeniousRequests { }) } - if (originalAction.operation == "activation") { + if (originalAction.operation == "activate") { this.notifyFinalization({ ...originalAction, msisdn @@ -215,7 +217,7 @@ export class CheckObjeniousRequests { const PATH = "/actions/requests/" const operationsList = structuredClone(requestList) - + // TODO: El for es gigantesco hay que simplificar partes for (const request of operationsList) { if (request.id == undefined) continue; @@ -228,13 +230,50 @@ export class CheckObjeniousRequests { try { res = await req } catch (e) { - console.error("Error comprobando el estado de ", request, e) - //todo actualizar el estado para incluir el error + console.error("[x] Error comprobando el estado de ", request, e) + continue; + } + + // 2. Casos de error o id no generada + if (res.data.massActionIds.length == 0) { + // Si no hay es que *puede* que haya un problema o no se ha generado todavia + const reports = res.data.actionRequestReports + // Se entiende que no hay report ni id = está a la espera + if (reports.length == 0) continue; + + // ! Hay minimo un report -> se considera error y se para + const updateData: ObjeniousOperationChange = { + operation_id: request.id, + new_status: "error", + error: JSON.stringify(reports[0].actionRequestReportDataDTOs) + } + + const updateRes = await this.operationsRepository.updateOperation(updateData) + if (updateRes.error != undefined) { + console.error("[x] Error actualizando el estado de la operacion", updateData.error) + } + + if (request.correlation_id != undefined) { + this.orderRepository.errorOrder({ + correlation_id: request.correlation_id, + status: "failed", + error: "MassId no obtenida", + reason: "MassId no obtenida", + stackTrace: JSON.stringify(reports[0].actionRequestReportDataDTOs) + }).then(e => { + if (e.error != undefined) { + console.error("[x] Error actualizando el estado del Order con correlation_id: ", request.correlation_id) + console.error(e.error) + } + }).catch(e => { + console.error("[x] Error actualizando el estado del Order con correlation_id: ", request.correlation_id) + }) + } continue; } - // 2. Modificacion del massId si ha habido un cambio const massActionId = res.data.massActionIds[0] + // 3. Modificacion del massId si ha habido un cambio try { if (res.status == 200 && res.data != undefined && massActionId != undefined) { const updateData: ObjeniousOperationChange = { @@ -248,7 +287,7 @@ export class CheckObjeniousRequests { request.mass_action_id = String(massActionId) } } catch (e) { - console.log("Error actualizando el estado de ", request) + console.log("[x] Error actualizando el estado de ", request) continue; } } @@ -262,6 +301,8 @@ export class CheckObjeniousRequests { * al servicio que manda los mails */ private async notifyFinalization(operation: ObjeniousOperation & { msisdn: string }) { + console.log("[i] Enviando activacion a", env.NOTIFICATION_URL) + console.log("[i] Operation", operation) const req = axios.post(env.NOTIFICATION_URL, { ...operation, iccids: [operation.iccids] @@ -270,7 +311,17 @@ export class CheckObjeniousRequests { "x-apikey-sim-activation": env.SIM_ACTIVATION_API_KEY } }) - await req + try { + const res = await req + if (res.status != 200) { + console.error("[x] Error enviando el mail de confirmacion para ", operation, " status ", res.status, res.statusText) + } + } catch (e) { + console.error("[x] Error enviando el mail de confirmacion para ", operation) + console.error(e) + } + + } } diff --git a/packages/sim-objenious-cron/tasks/check_pause_terminate.ts b/packages/sim-objenious-cron/tasks/check_pause_terminate.ts new file mode 100644 index 0000000..230c45d --- /dev/null +++ b/packages/sim-objenious-cron/tasks/check_pause_terminate.ts @@ -0,0 +1,191 @@ +import { ObjeniousLine } from "sim-shared/domain/objeniousLine.js"; +import { PauseCancelTaskRepository } from "sim-consumidor-objenious/infrastructure/PauseCancelTaskRepository.js"; +import { ObjeniousOperationsRepository } from "sim-shared/infrastructure/ObjeniousOperationRepository.js"; +import { SimUseCases } from "sim-consumidor-objenious/aplication/Sim.usecases.js"; +import { OrderRepository } from "packages/sim-shared/infrastructure/OrderRepository.js"; + +const logger = +{ + log: (...data: any[]) => console.log("[i] [TaskPauseTerminate]", ...data), + error: (...data: any[]) => console.error("[x] [TaskPauseTerminate] ", ...data), +} + + +export class PauseTerminateTask { + constructor( + private readonly objeniousRepo: ObjeniousOperationsRepository, + private readonly pauseRepo: PauseCancelTaskRepository, + private readonly simUsecases: SimUseCases, + private readonly orderRepo: OrderRepository + ) { + } + + public async run() { + const finError = (err: any) => { + logger.error("Finalizado con errores proceso de comprobacion de lineas en pausa o canceladas") + logger.error(err) + } + + const finExito = () => { + logger.log("Finalizado con exito proceso de comprobacion de lineas en pausa o canceladas") + } + try { + logger.log("Iniciando proceso de comprobacion de lineas en pausa o canceladas") + + // 1. Se comprueba cuantas peticiones hay qye revisar + const peticionesRevisar = await this.pauseRepo.getPending() + + if (peticionesRevisar.error != undefined) { + finError(peticionesRevisar.error) + return 1; + } + + logger.log(`Se van a revisar ${peticionesRevisar.data?.length} peticiones`) + if (peticionesRevisar.data == undefined || peticionesRevisar.data.length == 0) { + finExito() + return 0; + } + + + // 2. Se comprueba que alguna de las lineas haya dejado de estar en estado de test + const iccids = peticionesRevisar.data.map(e => e.iccid) + const lineasActualizadas: ObjeniousLine[] = [] + + const lineGenerator = this.objeniousRepo.getLinesByStatusAPI({ + iccids: iccids + }) + + let lines = await lineGenerator.next() + + if (lines.value.error != undefined || lines.value.data == undefined) { + logger.error("Error cargando las lineas", lines.value.error) + finError(lines.value.error) + return 1; + } else { + lineasActualizadas.push(...lines.value.data) + } + + while (!lines.done) { + if (lines.value.error != undefined || lines.value.data == undefined) { + logger.error("Error cargando las lineas", lines.value.error) + finError(lines.value.error) + return 1; + } else { + lineasActualizadas.push(...lines.value.data) + } + + lines = await lineGenerator.next() + } + + console.log("Cargado: ", lineasActualizadas) + + // 3. Se separan las lineas que se tienen que actualizar al no ser test + // y las que se tienen que reencolar al ser test + const lineasNoTest = lineasActualizadas.filter(e => e.status.billingStatus != "TEST") + const lineasTest = lineasActualizadas.filter(e => e.status.billingStatus == "TEST") + + // 4. Las lineas de test se reencolan + // El proximo reintento es en 1 dia + const proximoReintento = new Date() + proximoReintento.setDate(new Date().getDate() + 1) + + // 5. Reintentos en 1 dia + for (const linea of lineasTest) { + const lineaId = peticionesRevisar.data + .find(e => e.iccid == linea.identifier.iccid)?.id + + if (lineaId == undefined) continue; // Esto puede ser un problema si se generaliza + + this.pauseRepo.updateTask({ + id: lineaId, + next_check: proximoReintento + }) + } + + // 6. Operaciones de pausa/cancelacion definitiva + for (const linea of lineasNoTest) { + const operacion = peticionesRevisar.data + .find(e => e.iccid == linea.identifier.iccid) + + if (operacion == undefined) continue; + const dueDate = new Date() + dueDate.setMinutes(new Date().getMinutes() + 15) + + const operacionTipo = operacion.operation_type + const actionData = operacion.action_data + const correlation_id = operacion.action_data.correlation_id + actionData.dueDate = dueDate.toISOString() + + switch (linea.status.billingStatus) { + case "ACTIVATED": + let result = null; + + // Se termina el proceso aqui pero pasa a ser una operación de + // objenious por lo que puede fallar y quedaria registrado en + // la tabla objenious_operation + switch (operacionTipo) { + case "suspend": + result = await this.simUsecases.suspend(actionData)() + break; + case "terminate": + result = await this.simUsecases.terminate(actionData)() + break; + default: + break; + } + + if (result == undefined) { + logger.error("Operacion desconocida", operacion) + } else if (result?.error != undefined) { + // error usecase + logger.error(result.error) + await this.pauseRepo.finishTask({ + id: operacion.id, + error: result.error + }) + if (correlation_id != undefined) + await this.orderRepo.errorOrder({ + correlation_id: correlation_id, + status: "dlx", + reason: result.error + }) + } else { + // ok + await this.pauseRepo.finishTask({ id: operacion.id }) + if (correlation_id != undefined) + await this.orderRepo.finishOrder({ correlation_id }) + } + + break; + case "CANCELED": + await this.pauseRepo.finishTask({ + id: operacion.id, + error: "billingStatus is CANCELED" + }) + if (correlation_id != undefined) + await this.orderRepo.finishOrder({ correlation_id }) + break; + case "SUSPENDED": + await this.pauseRepo.finishTask({ + id: operacion.id, + error: "billingStatus is SUSPENDED" + }) + if (correlation_id != undefined) + await this.orderRepo.finishOrder({ correlation_id }) + break; + case "TEST": + // No puede ser + default: + logger.error("billingStatus desconocido", linea.status.billingStatus) + + } + } + + finExito() + } catch (e) { + finError(e) + } + + return 0 + } +} diff --git a/packages/sim-objenious-cron/tasks/volcado_lineas.ts b/packages/sim-objenious-cron/tasks/volcado_lineas.ts new file mode 100644 index 0000000..d06eb87 --- /dev/null +++ b/packages/sim-objenious-cron/tasks/volcado_lineas.ts @@ -0,0 +1,54 @@ +import { lineToCreateLineDto, ObjeniousLine } from "sim-shared/domain/objeniousLine.js"; +import { ObjeniousLinesRepository } from "../infranstructure/ObjeniousLinesRepository.js"; +import { ObjeniousOperationsRepository } from "sim-shared/infrastructure/ObjeniousOperationRepository.js"; + +export class TaskVolcadoLineas { + constructor( + private readonly linesRepository: ObjeniousLinesRepository, + private readonly objeniousRepository: ObjeniousOperationsRepository + ) { + } + + + private async saveLines(lines: ObjeniousLine[]) { + const linesToCreate = lines.map(lineToCreateLineDto) + let created: number[] = [] + + + for (const line of linesToCreate) { + // Si es lento pasar a Promise.all + const res = await this.linesRepository.insertOrUpdate(line) + if (res?.id != undefined) + created.push(res.id) + } + } + + public async loadLines() { + console.log("[i] Iniciando task de volcado de lineas de Objenious") + // Carga todas las lineas en memoria, hay que comprobar que no se gaste demasiada + + const linesIterator = this.objeniousRepository.getLinesByStatusAPI({ + pageSize: 100 + }) + let lines = await linesIterator.next() + + if (lines.value.error != undefined || lines.value.data == undefined) { + console.error("[x] Error cargando las lineas a volcar", lines.value.error) + return; + } + + await this.saveLines(lines.value.data) + + while (!lines.done) { + lines = await linesIterator.next() + if (lines.value.error != undefined || lines.value.data == undefined) { + console.error("[x] Error cargando las lineas a volcar", lines.value.error) + return; + } + await this.saveLines(lines.value.data) + } + + console.log("[i] Terminado task de volcado de lineas de Objenious") + } + +} diff --git a/packages/sim-consumidor-objenious/aplication/JWT.service.test.ts b/packages/sim-shared/aplication/JWT.service.test.ts similarity index 61% rename from packages/sim-consumidor-objenious/aplication/JWT.service.test.ts rename to packages/sim-shared/aplication/JWT.service.test.ts index d7f2f43..1d37d7e 100644 --- a/packages/sim-consumidor-objenious/aplication/JWT.service.test.ts +++ b/packages/sim-shared/aplication/JWT.service.test.ts @@ -1,16 +1,16 @@ import { test, describe } from "vitest" -import { JWTService } from "./JWT.service.js" +import { jwtService } from "../config/jwtService.config.js" describe("Tokens Objenious", () => { - const jwtService = new JWTService() + const jwt = jwtService test("Solicicitud normal de auth", async () => { - const token = await jwtService.getAccessToken() + const token = await jwt.getAccessToken() console.log("acceso objenious", token) }), test("Solicicitud de refresh de auth", async () => { - const token = await jwtService.tryRefreshToken() + const token = await jwt.tryRefreshToken() console.log("acceso refresh objenious", token) }) }) diff --git a/packages/sim-consumidor-objenious/aplication/JWT.service.ts b/packages/sim-shared/aplication/JWT.service.ts similarity index 66% rename from packages/sim-consumidor-objenious/aplication/JWT.service.ts rename to packages/sim-shared/aplication/JWT.service.ts index 038cfb8..9ef739e 100644 --- a/packages/sim-consumidor-objenious/aplication/JWT.service.ts +++ b/packages/sim-shared/aplication/JWT.service.ts @@ -4,24 +4,24 @@ * el cliente HTTP */ -import { env } from "#config/env/index.js"; import fs from "fs" import { JWTToken, JWTHeader, - IJWTService + IJWTService, + JWTPayload } from "sim-shared/domain/JWT.js" import axios, { AxiosError } from "axios"; -type GrantAccessRequestBody = { +export type GrantAccessRequestBody = { grant_type: string, client_id: string, client_assertion_type: string, client_assertion: string } -type TokensRequestResponse = { +export type TokensRequestResponse = { "access_token": string, "expires_in": number, "refresh_token": string @@ -32,41 +32,6 @@ type TokensRequestResponse = { "scope": string } - -const PRIVATE_KEY_PATH = env.OBJ_PEM_PATH - -const GET_TOKEN_URL = "https://idp.docapost.io/auth/realms/GETWAY/protocol/openid-connect/token" -const REFRESH_TOKEN_URL = GET_TOKEN_URL - -const DEFAULT_BODY: GrantAccessRequestBody = { - grant_type: "client_credentials", - client_id: env.OBJ_CLIENT_ID, - client_assertion_type: "urn:ietf:params:oauth:client-assertion-type:jwt-bearer", - client_assertion: env.OBJ_CLI_ASSERTION -} - -const REFRESH_BODY = { - ...DEFAULT_BODY, - grant_type: "refresh_token", -} - -const DEFAULT_HEADERS = { - "content-type": "application/x-www-form-urlencoded" -} - -function addIATHeaders(authHeaders: Object) { - const headers = { - ...authHeaders, - sub: env.OBJ_CLIENT_ID, - iss: env.OBJ_CLIENT_ID, - aud: GET_TOKEN_URL, - jti: Date.now().toString(), - iat: Math.floor(Date.now() / 1000), - exp: Math.floor(Date.now() / 1000) + 5 * 60, - } - return headers -} - export type ObjeniousTokenBody = any /** @@ -82,27 +47,54 @@ export class JWTService implements IJWTService { public authToken: JWTToken | undefined; private refreshToken?: JWTToken | undefined; - constructor(args?: { + // http + private transformHeaders?: (_: Object) => JWTHeader; + private defaultHttpHeaders: Record; + private defaultBody: Record; + + // jwt + private defaultJWTHeaders: JWTHeader; + private defaultJWTPayload: JWTPayload; + private privateKeyPath: string; + private tokenUrl: string; + private refreshTokenUrl: string; + + + constructor(args: { token?: string // si se partiese de un token existente, - refreshToken?: string + refreshToken?: string, + transformJWTHeaders?: (_: Object) => JWTHeader, + defaultHeaders: Record, + defaultBody: Record, + defaultJWTHeaders: JWTHeader, + defaultJWTPayload: JWTPayload, + privateKeyPath: string, + tokenUrl: string, + refreshTokenUrl: string }) { if (args?.token != undefined) this.authToken = new JWTToken(args.token) if (args?.refreshToken != undefined) this.refreshToken = new JWTToken(args.refreshToken) + if (args?.transformJWTHeaders != undefined) this.transformHeaders = args.transformJWTHeaders + + this.defaultHttpHeaders = args.defaultHeaders; + this.defaultBody = args.defaultBody; + + this.defaultJWTHeaders = args.defaultJWTHeaders; + this.defaultJWTPayload = args.defaultJWTPayload; + this.privateKeyPath = args.privateKeyPath; + + this.tokenUrl = args.tokenUrl; + this.refreshTokenUrl = args.refreshTokenUrl; } private buildJwtBody() { - const jwtHeaders = { - alg: "RS256", - typ: "JWT", - kid: env.OBJ_KID - } - const jwtData = addIATHeaders({ - sub: env.OBJ_CLIENT_ID, - iss: env.OBJ_CLIENT_ID, - aud: "https://idp.docapost.io/auth/realms/GETWAY", - jti: Date.now().toString(), - }) - const key = fs.readFileSync(PRIVATE_KEY_PATH, "utf8") + const jwtHeaders = this.defaultJWTHeaders + + const jwtData = (this.transformHeaders) ? + this.transformHeaders(this.defaultJWTPayload) : + this.defaultJWTPayload; + + const key = fs.readFileSync(this.privateKeyPath, "utf8") const token = JWTToken.fromParts({ header: jwtHeaders, payload: jwtData, @@ -116,14 +108,16 @@ export class JWTService implements IJWTService { public async getNewAuthToken() { const bodyWithtoken = { - ...DEFAULT_BODY, + ...this.defaultBody, client_assertion: this.buildJwtBody() } - const req = axios.post(GET_TOKEN_URL, + const headers = (this.transformHeaders) ? this.transformHeaders(this.defaultHttpHeaders) : this.defaultHttpHeaders; + + const req = axios.post(this.tokenUrl, bodyWithtoken, { - headers: addIATHeaders(DEFAULT_HEADERS) + headers: headers } ) @@ -166,16 +160,21 @@ export class JWTService implements IJWTService { if (this.refreshToken == undefined) throw new Error("El refreshToken no está definido") if (this.refreshToken.isExpired()) throw new Error("El refreshToken ha expirado") + const refreshBody = { + ...this.defaultBody, + grant_type: "refresh_token", + } + const body = { - ...REFRESH_BODY, + ...refreshBody, client_assertion: this.buildJwtBody(), refresh_token: this.refreshToken.rawToken } - const req = axios.post(REFRESH_TOKEN_URL, + const req = axios.post(this.refreshTokenUrl, body, { - headers: DEFAULT_HEADERS + headers: this.defaultHttpHeaders } ) diff --git a/packages/sim-shared/config/config.test.ts b/packages/sim-shared/config/config.test.ts index 22fb083..b8e2c7c 100644 --- a/packages/sim-shared/config/config.test.ts +++ b/packages/sim-shared/config/config.test.ts @@ -7,9 +7,12 @@ import { env, loadEnvFile } from "node:process"; import { Pool } from "pg"; import { PgClient } from "../infrastructure/PgClient.js"; +import { HttpClient } from "../infrastructure/HTTPClient.js"; +import { jwtService } from "./jwtService.config.js"; console.warn("[i!] Se está corriendo codigo de test") loadEnvFile("../../.env") // Global +loadEnvFile("./test.env") // Local // se hace una por servicio. export const pgPool = new Pool({ @@ -24,4 +27,14 @@ export const postgresClient = new PgClient({ pool: pgPool }) +const OBJ_BASE_URL = "https://api-getway.objenious.com/ws" +export const httpObjClient = new HttpClient({ + baseURL: OBJ_BASE_URL, + headers: { + "content-type": " application/json; charset=utf-8" + }, + jwtManager: jwtService +}) + + console.warn(`[T] TEST DB : ${env.POSTGRES_DATABASE}@${env.POSTGRES_HOST}`) diff --git a/packages/sim-shared/config/jwtService.config.ts b/packages/sim-shared/config/jwtService.config.ts new file mode 100644 index 0000000..c2500b8 --- /dev/null +++ b/packages/sim-shared/config/jwtService.config.ts @@ -0,0 +1,67 @@ +import assert from "assert" +import { env, loadEnvFile } from "process" +import { GrantAccessRequestBody, JWTService } from "sim-shared/aplication/JWT.service.js" +import { JWTHeader } from "sim-shared/domain/JWT.js" + +loadEnvFile("../../.env") // Global +loadEnvFile("./test.env") // Local + +assert(env.OBJ_CLIENT_ID != undefined) +assert(env.OBJ_CLI_ASSERTION != undefined) +assert(env.OBJ_PEM_PATH != undefined) + +const PRIVATE_KEY_PATH = env.OBJ_PEM_PATH + +const GET_TOKEN_URL = "https://idp.docapost.io/auth/realms/GETWAY/protocol/openid-connect/token" +const REFRESH_TOKEN_URL = GET_TOKEN_URL + + +const DEFAULT_BODY: GrantAccessRequestBody = { + grant_type: "client_credentials", + client_id: env.OBJ_CLIENT_ID, + client_assertion_type: "urn:ietf:params:oauth:client-assertion-type:jwt-bearer", + client_assertion: env.OBJ_CLI_ASSERTION +} + + +const DEFAULT_HEADERS = { + "content-type": "application/x-www-form-urlencoded" +} + +const DEFAULT_HEADERS_JWT = { + alg: "RS256", + typ: "JWT", + kid: env.OBJ_KID, +} + +const DEFAULT_DATA_JWT = { + sub: env.OBJ_CLIENT_ID, + iss: env.OBJ_CLIENT_ID, + aud: "https://idp.docapost.io/auth/realms/GETWAY", + jti: Date.now().toString(), + +} + +function addIATHeaders(authHeaders: Object) { + const headers = { + ...authHeaders, + sub: env.OBJ_CLIENT_ID, + iss: env.OBJ_CLIENT_ID, + aud: GET_TOKEN_URL, + jti: Date.now().toString(), + iat: Math.floor(Date.now() / 1000), + exp: Math.floor(Date.now() / 1000) + 5 * 60, + } + return headers +} + +export const jwtService = new JWTService({ + transformJWTHeaders: addIATHeaders, + defaultHeaders: DEFAULT_HEADERS, + defaultBody: DEFAULT_BODY, + defaultJWTHeaders: DEFAULT_HEADERS_JWT, + defaultJWTPayload: DEFAULT_DATA_JWT, + privateKeyPath: PRIVATE_KEY_PATH, + tokenUrl: GET_TOKEN_URL, + refreshTokenUrl: REFRESH_TOKEN_URL +}) diff --git a/packages/sim-shared/domain/Order.ts b/packages/sim-shared/domain/Order.ts index 30ec573..df1c2f3 100644 --- a/packages/sim-shared/domain/Order.ts +++ b/packages/sim-shared/domain/Order.ts @@ -62,11 +62,14 @@ export type CreateOrderDTO = Pick< 'correlation_id' | 'exchange' | 'routing_key' | 'order_type' | 'payload' | 'webhook_host' | 'webhook_endpoint' >; -export type UpdateOrderDTO = +type IdOrCorrelationID = ( { id: number, correlation_id?: never } | { id?: never, correlation_id: string } ) + +export type UpdateOrderDTO = + IdOrCorrelationID & { new_status: OrderStatus, @@ -74,12 +77,21 @@ export type UpdateOrderDTO = } export type FinishOrderDTO = - ( - { id: number, correlation_id?: never } | - { id?: never, correlation_id: string } - ) + IdOrCorrelationID & { - reason?: string + reason?: string, + end_date?: Date } +export type ErrorOrderDTO = + IdOrCorrelationID + & + { + status: "failed" | "dlx", + reason: string, + error?: string, + stackTrace?: string + } + + diff --git a/packages/sim-shared/domain/Result.ts b/packages/sim-shared/domain/Result.ts index a8bda25..0381424 100644 --- a/packages/sim-shared/domain/Result.ts +++ b/packages/sim-shared/domain/Result.ts @@ -14,7 +14,7 @@ export type Failure = { */ export type Result = Failure | Success -export async function tryCatch(func: Promise): Promise> { +export async function tryCatch(func: Promise): Promise> { try { const res = await func; return { @@ -22,9 +22,8 @@ export async function tryCatch(func: Promise): Promise; + +export function lineToCreateLineDto(line: ObjeniousLine): CreateObjeniousLineDTO { + + const dateOrNull = (data: string | null) => { + if (data == null) return null; + return new Date(data) + } + + const transformed: CreateObjeniousLineDTO = { + simId: line.identifier.simId, + iccid: line.identifier.iccid, + msisdn: line.identifier.msisdn, + imei: line.identifier.imei, + imeiChangeDate: new Date(line.device.imeiChangeDate), + offerCode: line.offer.code, + status: line.status.status, + preactivationDate: dateOrNull(line.status.preactivationDate), + activationDate: dateOrNull(line.status.activationDate), + commercialStatus: line.status.commercialStatus, + commercialStatusDate: dateOrNull(line.status.commercialStatusDate), + billingStatus: line.status.billingStatus, + billingStatusChangeDate: dateOrNull(line.status.activationDate), + billingActivationDate: dateOrNull(line.status.activationDate), + createDate: dateOrNull(line.status.activationDate), + raw: line + } + + return transformed; +} diff --git a/packages/sim-shared/domain/operationsRepository.port.ts b/packages/sim-shared/domain/operationsRepository.port.ts index 7af02c2..898fd10 100644 --- a/packages/sim-shared/domain/operationsRepository.port.ts +++ b/packages/sim-shared/domain/operationsRepository.port.ts @@ -12,7 +12,7 @@ export type ObjeniousOperation = { id?: number; /** Uuid del mensaje asociado a la operacion */ correlation_id?: string; - operation: string; + operation: "activate" | "suspend" | "terminate" | string; // TODO: completar y actualizar retry_count?: number; max_retry?: number; max_date_retry?: string | null; @@ -27,8 +27,7 @@ export type ObjeniousOperation = { } export type ObjeniousOperationChange = { - id?: number; - operation_id: number; + id?: number; operation_id: number; info?: string | null; error?: string | null; new_status: StatusEnum; @@ -46,10 +45,34 @@ export namespace Objenious { created: string, status: "NEW" | "RUNNING" | "OK" | "KO" | "REPLAYED" | "CANCELLED" | "CLOSED" | "DISABLED", statusDate: string, - actionType: "PREACTIVATION_AND_ACTIVATION" | string, // todo: añadir el resto - massActionIds: number[] + actionType: ActionType + massActionIds: number[], + actionRequestReports: + { + requestId: string, + actionRequestReportDataDTOs: [ + { + data: string, + newData: string | null, + iccid: string, + dataStatus: DataStatus + } + ] + }[], } + export type DataStatus = "DATA_INVALID_FORMAT" | "DATA_NOT_FOUND" | "DATA_NOT_ACTIVATED" | "SERVICE_DATA_NOT_ACTIVATED" | + "DATA_WRONG_STATUS" | "DATA_NOT_AUTHORIZED" | "DATA_CUSTOMER_ACCOUNT_NOT_AUTHORIZED" | "DATA_AMBIGUOUS" | + "NEW_DATA_INVALID_FORMAT" | "NEW_DATA_ALREADY_EXISTS" | "DUPLICATE_DATA" | "DATA_TERMINATION_VALIDATED" | + "DATA_TERMINATION_SECURISED" | "MAX_ALARM_INSTANCE" | "MAX_ALARM_INSTANCE_TO_CATCH_UP" | + "ACTIVATED_LINE_CANNOT_BE_TRANSFERED" | "ESIM_WRONG_STEP" | "ESIM_WRONG_PAIRED_VALUE" | + "ESIM_WRONG_DOWNLOAD_STATE" | "ESIM_WRONG_STATUS" | "ESIM_WRONG_FAMILY" | "ESIM_WRONG_CATEGORY" | + "ENTITY_STATUS_NOT_AUTHORIZED" | "LONG_LIFE_NOT_ALLOWED" | "RCARD_NOT_COMPATIBLE" | "APN_NOT_FOUND" | + "APN_OR_DNN_NOT_FOUND" | "APN_CONFIGURATION_NOT_FOUND" | "APN_CONFIGURATION_INVALID_PARAMETER_FILE" | + "IP_NOT_AVAILABLE" | "RADIUS_FIELD_LENGTH_NOT_ALLOWED" | "RADIUS_LOGIN_OR_PASSWORD_NOT_FOUND" | "RADIUS_PASSWORD_NOT_ALLOWED" | + "RADIUS_LOGIN_NOT_ALLOWED" | "NETWORK_NOT_ACTIVATED" | "CHANGE_CUSTOMER_ACCOUNT_NOT_AllOWED" | "CHANGE_OFFER_NOT_ALLOWED" | + "SIM_NOT_EUICC" | "OFFER_NOT_WSF_PALIER_FLOTTE_FR" + export type ActionType = "PREACTIVATION" | "PREACTIVATION_ACTIVATION" | "ACTIVATION" | "STATUS_CHANGE" | "ICCID_CHANGE" | "EUICC_NOTIFICATION" | "EUICC_AUDIT" | "MSISDN_CHANGE" | "ALARM_SETTING" diff --git a/packages/sim-shared/infrastructure/ObjeniousOperationRepository.test.ts b/packages/sim-shared/infrastructure/ObjeniousOperationRepository.test.ts new file mode 100644 index 0000000..aa80a27 --- /dev/null +++ b/packages/sim-shared/infrastructure/ObjeniousOperationRepository.test.ts @@ -0,0 +1,39 @@ +import { before, describe, it } from "node:test"; +import { ObjeniousOperationsRepository } from "./ObjeniousOperationRepository.js"; +import { httpObjClient, postgresClient } from "../config/config.test.js"; +import { ObjeniousOperation } from "../domain/operationsRepository.port.js"; + +const correctOperation: ObjeniousOperation = { + iccids: "test", + operation: "activate", + status: "finished" +} + +const errorOperation: ObjeniousOperation = { + iccids: "test", + operation: "terminate", + status: "error", + error: "mensaje de error" +} + +describe("[Integration] Test API requests", () => { + const repository = new ObjeniousOperationsRepository( + httpObjClient, + postgresClient + ) + + before(async () => { + await repository.createOperation(correctOperation) + await repository.createOperation(errorOperation) + }) + + it("Read last operation by line", () => { + /** + * Objetivo: + * - Cuando se va a hacer una operacion de sim hay que cancelarla directamente si: + * - Ya hay una en curso del mismo tipo. + * - Ya ha terminado una del mismo tipo. + * - Se ignoran las erroneas + */ + }) +}) diff --git a/packages/sim-shared/infrastructure/ObjeniousOperationRepository.ts b/packages/sim-shared/infrastructure/ObjeniousOperationRepository.ts index 134da3f..4346ff7 100644 --- a/packages/sim-shared/infrastructure/ObjeniousOperationRepository.ts +++ b/packages/sim-shared/infrastructure/ObjeniousOperationRepository.ts @@ -1,14 +1,139 @@ import { IOperationsRepository, ObjeniousOperation, ObjeniousOperationChange } from "sim-shared/domain/operationsRepository.port.js"; -import { Result } from "sim-shared/domain/Result.js"; +import { Result, tryCatch } from "sim-shared/domain/Result.js"; import { PgClient } from "sim-shared/infrastructure/PgClient.js"; +import { ObjeniousLine, ObjeniousLineResponse } from "../domain/objeniousLine.js"; +import { HttpClient } from "./HTTPClient.js"; +import assert from "node:assert"; +import { AxiosResponse } from "axios"; export class ObjeniousOperationsRepository implements IOperationsRepository { constructor( + private http: HttpClient, private readonly pgClient: PgClient ) { } + /** + * Consulta el estado de una o mas lineas directamente a la API de Objenious + * TODO: No hay paginacion como en getLinesByStatusAPI + */ + public async getLinesAPI( + identifierType: "ICCID" | "IMSI" | "IMEI" | "MSISDN" | "REFERENCE", + identifiers: string[] + ): Promise> { + if (identifiers.length == 0) { + return { + data: [] + } + } + + // Comprobar < MAX_PAGE_SIZE (Poco probable) + + const path = "/lines" + const params = { + "identifier.identifierType": identifierType, + "identifier.identifiers": identifiers.toString() + } + + const req = this.http.client.get(path, { + params: params + }) + + const res = await tryCatch(req) + + if (res.error != undefined) { + return { + error: res.error?.message + } + } + + const lines = res.data.data.content + + return { + data: lines + } + } + + + private MAX_PAGE_SIZE = 1000 + public async * getLinesByStatusAPI(args?: { + pageSize?: number, + pageNumber?: number, + status?: string, + iccids?: string[] + }): AsyncGenerator, Result, any> { + + const path = "/lines" + const pageSize = args?.pageSize ?? this.MAX_PAGE_SIZE; + + let currentPage = args?.pageNumber ?? 0; + let totalPages: number | undefined = undefined; // Como limite de paginas, igual es pasarse pero hasta que se lea + + const params: Record = {} + + // Si se va a filtrar por iccids especificamente, en un futuro habra que ampliar el tipo de filtros + if (args?.iccids != undefined) { + params["identifier.identifierType"] = "ICCID" + params["identifier.identifiers"] = args.iccids.toString() + } + + const loadNextLine = async (page: number): Promise> => { + if (args?.status != undefined) params["simStatus"] = args.status + params["pageSize"] = pageSize + params["pageNumber"] = page + console.log(`[i] Cargando pagina ${currentPage} de ${totalPages ?? "(desc)"}`) + const nextPage = await tryCatch>(this.http.client.get(path, { + params: params + })) + + if (nextPage.error != undefined) { + console.error(nextPage.error) + return { + error: nextPage.error.message + } + } + + // Se aumenta para la siguiente ejecucion + console.log(`[i] Página ${currentPage} completa, total: ${nextPage.data.data.totalPages}`) + totalPages = nextPage.data.data.totalPages + + return { + data: nextPage.data.data.content + } + + } + + // El inicio se ejecuta siempre + const lines = await loadNextLine(currentPage) + + if (lines.error != undefined) { + console.error("[x] Error obteniendo las lineas, cancelando operación"); + return { + error: "Error cargando lineas" + } + } + + currentPage++; + + yield { + data: lines.data + } + + // Copia para evitar bucles infinitos por error de la api + const maxPages = totalPages + assert.ok(maxPages != undefined, "No se ha defindo el numero de paginas") // Nunca deberia pasar pero así se evitan bucles infnitos + console.log("maxPages", maxPages) + for (let i = currentPage; i < maxPages!; i++) { + console.log("Bucle i:", i, "page: ", currentPage) + yield await loadNextLine(currentPage); + currentPage++; + } + + return { + data: [] + } + } async createOperation(data: ObjeniousOperation): Promise> { const query = ` INSERT INTO objenious_operation (operation, iccids, status, max_retry, request_id) @@ -21,6 +146,20 @@ export class ObjeniousOperationsRepository implements IOperationsRepository { } } + + async getLastOperationOfLine(iccid: string) { + const query = ` + SELECT * FROM public.objenious_operation + WHERE iccids = $1 and error is null + ORDER BY id asc limit 1 + ` + const values = [iccid]; + const { rows } = await this.pgClient.query(query, values); + return >{ + data: rows[0] + } + } + async updateOperation(data: ObjeniousOperationChange): Promise> { const client = await this.pgClient.connect(); const { @@ -46,7 +185,7 @@ export class ObjeniousOperationsRepository implements IOperationsRepository { request_id = COALESCE($4, request_id), mass_action_id = COALESCE($5, mass_action_id), last_change_date = now() at time zone 'utc', - end_date = CASE WHEN $2 IN ('finished') THEN now() at time zone 'utc' ELSE end_date END, + end_date = CASE WHEN $2 IN ('finished','error') THEN now() at time zone 'utc' ELSE end_date END, objenious_status = $6 WHERE id = $1`; diff --git a/packages/sim-shared/infrastructure/OrderRepository.test.ts b/packages/sim-shared/infrastructure/OrderRepository.test.ts index 8b33947..4636fe3 100644 --- a/packages/sim-shared/infrastructure/OrderRepository.test.ts +++ b/packages/sim-shared/infrastructure/OrderRepository.test.ts @@ -27,7 +27,7 @@ describe("Test OrderRepository", {}, (ctx) => { before(async () => { // Order1 const result1 = await orderRepo.createOrder(order1) - assert(result1.data != undefined) + assert.ok(result1.data != undefined, result1.error as string) testIds.push(result1.data.id) // Order2 -> Para el test de crearOrder diff --git a/packages/sim-shared/infrastructure/OrderRepository.ts b/packages/sim-shared/infrastructure/OrderRepository.ts index a6c23b5..bab9436 100644 --- a/packages/sim-shared/infrastructure/OrderRepository.ts +++ b/packages/sim-shared/infrastructure/OrderRepository.ts @@ -2,11 +2,10 @@ * TODO: Usar */ import { PoolClient, QueryResult, QueryResultRow } from "pg"; -import { CreateOrderDTO, FinishOrderDTO, OrderTracking, UpdateOrderDTO } from "../domain/Order.js"; -import { Result } from "../domain/Result.js"; +import { CreateOrderDTO, ErrorOrderDTO, FinishOrderDTO, OrderTracking, UpdateOrderDTO } from "../domain/Order.js"; +import { Result, tryCatch } from "../domain/Result.js"; import { PgClient } from "./PgClient.js"; import assert from "node:assert"; -import { error } from "node:console"; /** * Agrupa todas las operaciones de *Order*. @@ -19,9 +18,8 @@ import { error } from "node:console"; */ export class OrderRepository { constructor( - private readonly pgClient: PgClient + private readonly pgClient: PgClient, ) { - } /** @@ -57,6 +55,8 @@ export class OrderRepository { } } + + /** * El tipo representa el contenido del mensaje de los order */ @@ -191,6 +191,8 @@ export class OrderRepository { const orderId = currentOrderResult.data?.id if (orderId == undefined) { + await client.query("ROLLBACK") + client.release() return { error: "El order a actualizar no existe " + idType + ": " + idValue } @@ -261,7 +263,6 @@ export class OrderRepository { return updatedOrder } - public async finishOrder(args: FinishOrderDTO) { const client = await this.pgClient.connect(); assert((args.id != undefined) != (args.correlation_id != undefined)) @@ -281,6 +282,8 @@ export class OrderRepository { const orderId = currentOrderResult.data?.id if (orderId == undefined) { + await client.query("ROLLBACK") + client.release() return { error: "El order a actualizar no existe " + idType + ": " + idValue } @@ -299,8 +302,8 @@ export class OrderRepository { UPDATE order_tracking SET status = 'finished', - update_date = (now() at time zone 'utc'), - finish_date = (now() at time zone 'utc') + update_date = now(), + finish_date = now() WHERE id = $1 RETURNING id, status, update_date; ` @@ -353,22 +356,19 @@ export class OrderRepository { } // TODO: tema de poder filtrar por correlation_id - public async errorOrder(args: { - id: number, - status: "failed" | "dlx", - reason: string, - error?: string, - stackTrace?: string - }) { + public async errorOrder(args: ErrorOrderDTO): Promise>> { const client = await this.pgClient.connect(); await client.query('BEGIN'); + const idType = ('id' in args) ? "id" : "correlation_id" + const idValue = (args.id != undefined) ? args.id : args.correlation_id + // 1. Se consulta la order de base const qCurrentOrder = ` SELECT * FROM order_tracking - WHERE id = $1 + WHERE ${idType} = $1 ` - const vCurrentOrder = [args.id] + const vCurrentOrder = [idValue] const currentOrderResult = await this.getFirst(client.query>(qCurrentOrder, vCurrentOrder)) @@ -378,6 +378,7 @@ export class OrderRepository { return currentOrderResult } + const id = currentOrderResult.data.id // Saco el id para evitar busacr por correlation_id que es mas lento const currentOrder = currentOrderResult.data! // 3. Si todo ok se actualiza el order @@ -395,7 +396,7 @@ export class OrderRepository { WHERE id = $1 RETURNING id, status, update_date; ` - const vOrderTracking = [args.id, args.status, args.error, args.stackTrace] + const vOrderTracking = [id, args.status, args.error, args.stackTrace] const updatedOrderResult = await this.getFirst( client.query<{ id: number, status: string, update_date: string }>(uOrderTracking, vOrderTracking) ) diff --git a/packages/sim-shared/test.env b/packages/sim-shared/test.env new file mode 100644 index 0000000..1e5983d --- /dev/null +++ b/packages/sim-shared/test.env @@ -0,0 +1,14 @@ +## ENV PARA DATOS DE TEST - shared nunca se lanza en produccion + +# claves de Objenious +OBJ_PEM_PATH=./obj.pem +OBJ_AUTHORIZATION=XOc7FtwXD8hUX2SFVX94XSty8wkOmChkwDNF09O_aIxPubMDdFUdCDCB4zpzSIxi8nOcTg7r_LM_nmd5qm7uLbksf_XArjI8iAyhjKz_2BAXPhmvKs4Fc9f3vv5LDfCVrPB9lP8P7rJ66_qnWs4jvhLQxSfn29m96hgXeCf8oySdIDUjN2q9Js3KAS5LL52Ri6ryvUeO1PvMhaPQMWRqoHIqTV1wPfPtiqQwcjUPmu5GeW164Kq1JLgV3KaGzfCZ9Qv9lbv30EJrukXxWuLCAhBS0kzrBXZoWvf2pb9uh3Am_93_dDxiIGQfIap9ZU_m8ZD1HPgvZOMCY6ZkxQconQ +OBJ_CLI_ASSERTION=XOc7FtwXD8hUX2SFVX94XSty8wkOmChkwDNF09O_aIxPubMDdFUdCDCB4zpzSIxi8nOcTg7r_LM_nmd5qm7uLbksf_XArjI8iAyhjKz_2BAXPhmvKs4Fc9f3vv5LDfCVrPB9lP8P7rJ66_qnWs4jvhLQxSfn29m96hgXeCf8oySdIDUjN2q9Js3KAS5LL52Ri6ryvUeO1PvMhaPQMWRqoHIqTV1wPfPtiqQwcjUPmu5GeW164Kq1JLgV3KaGzfCZ9Qv9lbv30EJrukXxWuLCAhBS0kzrBXZoWvf2pb9uh3Am_93_dDxiIGQfIap9ZU_m8ZD1HPgvZOMCY6ZkxQconQ +OBJ_CLIENT_ID=savefamily_rest_ws +OBJ_KID=xNfbMiyL1ORXGP8lElhcv8nVaG3EJKye4Lc1YoN3I1E +OBJ_BASE_URL=https://api-getway.objenious.com/ws +# OBJ_BASE_URL=https://api-getway.objenious.com/ws/test + +NOTIFICATION_URL="https://sf-sim-activation.savefamilygps.net/send-activation-mail" +# NOTIFICATION_URL="localhost" +SIM_ACTIVATION_API_KEY=9e48c4ac-1ab0-4397-b3f3-6c239200dfe6 diff --git a/yarn.lock b/yarn.lock index 19cf892..719cdff 100644 --- a/yarn.lock +++ b/yarn.lock @@ -452,15 +452,15 @@ __metadata: languageName: node linkType: hard -"@sf-alvar/db-migrate@npm:1.0.3": - version: 1.0.3 - resolution: "@sf-alvar/db-migrate@npm:1.0.3::__archiveUrl=https%3A%2F%2Fgit.savefamilygps.net%2Fapi%2Fpackages%2FSaveFamily%2Fnpm%2F%2540sf-alvar%252Fdb-migrate%2F-%2F1.0.3%2Fdb-migrate-1.0.3.tgz" +"@sf-alvar/db-migrate@npm:1.0.6": + version: 1.0.6 + resolution: "@sf-alvar/db-migrate@npm:1.0.6::__archiveUrl=https%3A%2F%2Fgit.savefamilygps.net%2Fapi%2Fpackages%2FSaveFamily%2Fnpm%2F%2540sf-alvar%252Fdb-migrate%2F-%2F1.0.6%2Fdb-migrate-1.0.6.tgz" dependencies: pg: "npm:^8.18.0" yargs: "npm:^18.0.0" bin: db-migrate: lib/index.js - checksum: 10/2b5745a5ce60456fc7fee1e6a8580978a520fedd8abbbc695557847cdf2b36aa5e1d795721ad35bc151fc9373dfa023bde73d6f43ba412b17293a1822c09fe6b + checksum: 10/070f1388ff1c6fd2d24c3139d779e871bc0db94f11dd2013aa7eb5728e3c21e594bac0f4d46f8f3132391a9903cca56d5c864862c622d70f24e0db0ffcbbbf0e languageName: node linkType: hard @@ -576,6 +576,15 @@ __metadata: languageName: node linkType: hard +"@types/http-proxy@npm:^1.17.15": + version: 1.17.17 + resolution: "@types/http-proxy@npm:1.17.17" + dependencies: + "@types/node": "npm:*" + checksum: 10/893e46e12be576baa471cf2fc13a4f0e413eaf30a5850de8fdbea3040e138ad4171234c59b986cf7137ff20a1582b254bf0c44cfd715d5ed772e1ab94dd75cd1 + languageName: node + linkType: hard + "@types/methods@npm:^1.1.4": version: 1.1.4 resolution: "@types/methods@npm:1.1.4" @@ -1154,7 +1163,7 @@ __metadata: languageName: node linkType: hard -"debug@npm:4, debug@npm:^4.1.1, debug@npm:^4.3.4, debug@npm:^4.3.7, debug@npm:^4.4.0, debug@npm:^4.4.3": +"debug@npm:4, debug@npm:^4.1.1, debug@npm:^4.3.4, debug@npm:^4.3.6, debug@npm:^4.3.7, debug@npm:^4.4.0, debug@npm:^4.4.3": version: 4.4.3 resolution: "debug@npm:4.4.3" dependencies: @@ -1413,6 +1422,13 @@ __metadata: languageName: node linkType: hard +"eventemitter3@npm:^4.0.0": + version: 4.0.7 + resolution: "eventemitter3@npm:4.0.7" + checksum: 10/8030029382404942c01d0037079f1b1bc8fed524b5849c237b80549b01e2fc49709e1d0c557fa65ca4498fc9e24cff1475ef7b855121fcc15f9d61f93e282346 + languageName: node + linkType: hard + "expect-type@npm:^1.2.2": version: 1.3.0 resolution: "expect-type@npm:1.3.0" @@ -1527,6 +1543,16 @@ __metadata: languageName: node linkType: hard +"follow-redirects@npm:^1.0.0": + version: 1.16.0 + resolution: "follow-redirects@npm:1.16.0" + peerDependenciesMeta: + debug: + optional: true + checksum: 10/3fbe3d80b3b544c22705d837aa5d4a0d07a740d913534a2620b0a004c610af4148e3b58723536dd099aaa1c9d3a155964bde9665d6e5cb331460809a1fc572fd + languageName: node + linkType: hard + "follow-redirects@npm:^1.15.11": version: 1.15.11 resolution: "follow-redirects@npm:1.15.11" @@ -1788,6 +1814,31 @@ __metadata: languageName: node linkType: hard +"http-proxy-middleware@npm:^3.0.5": + version: 3.0.5 + resolution: "http-proxy-middleware@npm:3.0.5" + dependencies: + "@types/http-proxy": "npm:^1.17.15" + debug: "npm:^4.3.6" + http-proxy: "npm:^1.18.1" + is-glob: "npm:^4.0.3" + is-plain-object: "npm:^5.0.0" + micromatch: "npm:^4.0.8" + checksum: 10/83c1956be6451a5f4a2f3c7b3d84085dbd47e1efb5bb684c1ed668a6606c18c7c07be823b0dbba1326955b64cf88de2672492940b0b48d140215fbdb06105c9a + languageName: node + linkType: hard + +"http-proxy@npm:^1.18.1": + version: 1.18.1 + resolution: "http-proxy@npm:1.18.1" + dependencies: + eventemitter3: "npm:^4.0.0" + follow-redirects: "npm:^1.0.0" + requires-port: "npm:^1.0.0" + checksum: 10/2489e98aba70adbfd8b9d41ed1ff43528be4598c88616c558b109a09eaffe4bb35e551b6c75ac42ed7d948bb7530a22a2be6ef4f0cecacb5927be139f4274594 + languageName: node + linkType: hard + "https-proxy-agent@npm:^7.0.1": version: 7.0.6 resolution: "https-proxy-agent@npm:7.0.6" @@ -1865,7 +1916,7 @@ __metadata: languageName: node linkType: hard -"is-glob@npm:^4.0.1, is-glob@npm:~4.0.1": +"is-glob@npm:^4.0.1, is-glob@npm:^4.0.3, is-glob@npm:~4.0.1": version: 4.0.3 resolution: "is-glob@npm:4.0.3" dependencies: @@ -1881,6 +1932,13 @@ __metadata: languageName: node linkType: hard +"is-plain-object@npm:^5.0.0": + version: 5.0.0 + resolution: "is-plain-object@npm:5.0.0" + checksum: 10/e32d27061eef62c0847d303125440a38660517e586f2f3db7c9d179ae5b6674ab0f469d519b2e25c147a1a3bc87156d0d5f4d8821e0ce4a9ee7fe1fcf11ce45c + languageName: node + linkType: hard + "is-promise@npm:^4.0.0": version: 4.0.0 resolution: "is-promise@npm:4.0.0" @@ -2842,6 +2900,7 @@ __metadata: cors: "npm:*" dotenv: "npm:*" express: "npm:*" + http-proxy-middleware: "npm:^3.0.5" prettier: "npm:*" sim-shared: "sim-shared:*" supertest: "npm:*" @@ -2856,7 +2915,7 @@ __metadata: version: 0.0.0-use.local resolution: "sim-eventos@workspace:." dependencies: - "@sf-alvar/db-migrate": "npm:1.0.3" + "@sf-alvar/db-migrate": "npm:1.0.6" "@tsconfig/node22": "npm:^22.0.5" "@types/amqplib": "npm:^0.10.8" "@types/cors": "npm:^2.8.19"