diff --git a/packages/sim-consumidor-objenious/aplication/Sim.usecases.ts b/packages/sim-consumidor-objenious/aplication/Sim.usecases.ts index 8fc34ee..7549beb 100644 --- a/packages/sim-consumidor-objenious/aplication/Sim.usecases.ts +++ b/packages/sim-consumidor-objenious/aplication/Sim.usecases.ts @@ -269,7 +269,7 @@ 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 + * la actual para saber cuando se va a completar el periodo de test de una linea */ private async findActivationDate(actionData: ActionData) { const iccid = actionData.identifier.identifiers @@ -439,5 +439,22 @@ export class SimUseCases { }) } + public async getSuspendedTime(iccid: string): Promise> { + try { + const result = await this.objeniousRepository.getSuspendedTime(iccid); + if (result.error !== undefined) { + return { error: result.error as string, data: undefined }; + } + return { + data: { + total_milliseconds: result.data!.total_milliseconds, + total_days: result.data!.total_days + } + }; + } catch (error) { + console.error("[Sim.usecases] Error getting suspended time", error); + return { error: "Error getting suspended time", data: undefined }; + } + } } diff --git a/packages/sim-consumidor-objenious/index.ts b/packages/sim-consumidor-objenious/index.ts index 01e9d17..0ac4483 100644 --- a/packages/sim-consumidor-objenious/index.ts +++ b/packages/sim-consumidor-objenious/index.ts @@ -10,6 +10,9 @@ import { SimRouter } from "./aplication/Sim.router.js" import { OrderRepository } from "sim-shared/infrastructure/OrderRepository.js" import { PauseCancelTaskRepository } from "#adapters/PauseCancelTaskRepository.js" +import express from "express" +import cors from "cors" + async function startWorker() { const rmqClient = await startRMQClient() @@ -27,19 +30,47 @@ async function startWorker() { const pauseRepository = new PauseCancelTaskRepository(pgClient) + const simUseCases = new SimUseCases({ + httpClient: httpClient, + operationRepository: operationRepository, + orderRepository: orderRepository, + pauseRepository: pauseRepository + }) + const simActivationController = new SimController( rmqClient, - new SimUseCases({ - httpClient: httpClient, - operationRepository: operationRepository, - orderRepository: orderRepository, - pauseRepository: pauseRepository - }) + simUseCases ) const simRouter = new SimRouter(simActivationController, rmqClient) // de momento solo una cola por simplificar rmqClient.consume("sim.objenious", simRouter.route) + + // Express Server para endpoints sincrónicos + const app = express() + app.use(cors()) + app.use(express.json()) + + app.get("/lines/:iccid/suspended-time", async (req, res) => { + const iccid = req.params.iccid + if (!iccid) { + res.status(400).json({ error: "iccid is required" }) + return + } + + const result = await simUseCases.getSuspendedTime(iccid) + if (result.error !== undefined) { + res.status(500).json({ error: result.error }) + return + } + + res.json(result.data) + }) + + const port = process.env.PORT || 3001 + app.listen(port, () => { + console.log(`[o] HTTP server listening on port ${port}`) + }) } startWorker() diff --git a/packages/sim-shared/domain/operationsRepository.port.ts b/packages/sim-shared/domain/operationsRepository.port.ts index 898fd10..6bf1f3b 100644 --- a/packages/sim-shared/domain/operationsRepository.port.ts +++ b/packages/sim-shared/domain/operationsRepository.port.ts @@ -6,6 +6,7 @@ export interface IOperationsRepository { createOperation(data: ObjeniousOperation): Promise> updateOperation(data: ObjeniousOperationChange): Promise> getPendingOperations(): Promise> + getSuspendedTime(iccid: string): Promise> } export type ObjeniousOperation = { diff --git a/packages/sim-shared/infrastructure/ObjeniousOperationRepository.test.ts b/packages/sim-shared/infrastructure/ObjeniousOperationRepository.test.ts index aa80a27..86e6a3a 100644 --- a/packages/sim-shared/infrastructure/ObjeniousOperationRepository.test.ts +++ b/packages/sim-shared/infrastructure/ObjeniousOperationRepository.test.ts @@ -1,4 +1,4 @@ -import { before, describe, it } from "node:test"; +import { before, after, 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"; @@ -21,6 +21,7 @@ describe("[Integration] Test API requests", () => { httpObjClient, postgresClient ) + const suspend_iccid = "test_suspended_time_iccid"; before(async () => { await repository.createOperation(correctOperation) @@ -36,4 +37,40 @@ describe("[Integration] Test API requests", () => { * - Se ignoran las erroneas */ }) + + it("Calculates suspended time accurately", async () => { + // Se crean registros con un iccid concocido + await postgresClient.query(` + INSERT INTO objenious_operation (operation, iccids, status, error, start_date, end_date) VALUES + ('suspend', $1, 'finished', NULL, NOW() - INTERVAL '3 days', NOW() - INTERVAL '3 days'), + ('reactivate', $1, 'finished', NULL, NOW() - INTERVAL '2 days', NOW() - INTERVAL '2 days'), + ('suspend', $1, 'finished', NULL, NOW() - INTERVAL '1 day', NOW() - INTERVAL '1 day'); + `, [suspend_iccid]); + + const result = await repository.getSuspendedTime(suspend_iccid); + + if (result.error) { + throw new Error("Query returned an error: " + result.error); + } + + // Se esperan mas o menos 2 dias para cada periodo, total 4 (Puede cambiar un poco por zona horaria) + // 2 dias en ms + const expectedApproxMs = 2 * 24 * 60 * 60 * 1000; + const msDiff = Math.abs(result.data!.total_milliseconds - expectedApproxMs); + + // Margen de 5s + if (msDiff > 5000) { + throw new Error(`Expected approx ${expectedApproxMs} ms, got ${result.data!.total_milliseconds}`); + } + + // como se incluye el dia de sespension los dias van a variar de 2 a 3 + if (result.data!.total_days < 2) { + throw new Error("total_days should be at least 2"); + } + }); + + after(async () => { + // Eliminacion de los iccid de periodo de suspensiones + await postgresClient.query(`DELETE FROM objenious_operation WHERE iccids = '${suspend_iccid}'`); + }); }) diff --git a/packages/sim-shared/infrastructure/ObjeniousOperationRepository.ts b/packages/sim-shared/infrastructure/ObjeniousOperationRepository.ts index 4346ff7..9adc9ef 100644 --- a/packages/sim-shared/infrastructure/ObjeniousOperationRepository.ts +++ b/packages/sim-shared/infrastructure/ObjeniousOperationRepository.ts @@ -231,4 +231,66 @@ export class ObjeniousOperationsRepository implements IOperationsRepository { } } } + + /** + * Obtiene el tiempo en suspensión de una linea en miliseguntos y dias efetivos + * Todo el calculo se hace en postgres. Puede que haga falta traer las transiciones + * que normalmente son pocas, para hacer filtros personalizados. + */ + async getSuspendedTime(iccid: string): Promise> { + const query = ` + WITH ordered_events AS ( + -- 1. Selecciona y normaliza los eventos relevantes del historial + -- Se define el 'estado' final (suspendido vs activo) basado en la operación + SELECT operation, end_date, + CASE WHEN operation = 'suspend' THEN 'suspended' ELSE 'active' END as state + FROM objenious_operation + WHERE iccids = $1 AND status = 'finished' AND error IS NULL + AND operation IN ('suspend', 'activate', 'reactivate', 'terminate') + ORDER BY end_date + ), + state_transitions AS ( + -- 2. Detecta cambios de estado comparando con la fila anterior (LAG) + SELECT state, end_date, + LAG(state) OVER (ORDER BY end_date) as prev_state + FROM ordered_events + ), + filtered_transitions AS ( + -- 3. Filtra solo las filas donde el estado realmente ha cambaido + -- Se obtiene la fecha de inicio del siguiente intervalo (LEAD) + SELECT state, end_date, + LEAD(end_date) OVER (ORDER BY end_date) as next_end_date + FROM state_transitions + WHERE state IS DISTINCT FROM prev_state + ), + intervals AS ( + -- 4. Calcula la duración de los periodos en los que el estado fue 'suspended' + -- Se usa NOW() para el intervalo abierto (último estado hasta hoy) + SELECT EXTRACT(EPOCH FROM (COALESCE(next_end_date, NOW() at time zone 'utc') - end_date)) * 1000 as ms_duration, + (COALESCE(next_end_date, NOW() at time zone 'utc')::date - end_date::date) + 1 as days_duration + FROM filtered_transitions + WHERE state = 'suspended' + ) + -- 5. Suma total de tiempo en estado de suspensión + SELECT COALESCE(SUM(ms_duration)::bigint, 0) as total_milliseconds, + COALESCE(SUM(days_duration), 0) as total_days + FROM intervals; + `; + + try { + const { rows } = await this.pgClient.query<{ total_milliseconds: string, total_days: string }>(query, [iccid]); + return { + data: { + total_milliseconds: parseFloat(rows[0].total_milliseconds), + total_days: parseInt(rows[0].total_days) + } + }; + } catch (e) { + console.error("Error calculating suspended time", e); + return { + error: String(e), + data: undefined + }; + } + } }