Calculo del tiempo en suspension

This commit is contained in:
2026-04-27 12:12:12 +02:00
parent e1450c6e97
commit 4517796ef3
5 changed files with 156 additions and 8 deletions

View File

@@ -269,7 +269,7 @@ export class SimUseCases {
/** /**
* Metodo muy especifico para obtener la fecha e activacion o en su defecto * 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) { private async findActivationDate(actionData: ActionData) {
const iccid = actionData.identifier.identifiers const iccid = actionData.identifier.identifiers
@@ -439,5 +439,22 @@ export class SimUseCases {
}) })
} }
public async getSuspendedTime(iccid: string): Promise<Result<string, { total_milliseconds: number, total_days: number }>> {
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 };
}
}
} }

View File

@@ -10,6 +10,9 @@ import { SimRouter } from "./aplication/Sim.router.js"
import { OrderRepository } from "sim-shared/infrastructure/OrderRepository.js" import { OrderRepository } from "sim-shared/infrastructure/OrderRepository.js"
import { PauseCancelTaskRepository } from "#adapters/PauseCancelTaskRepository.js" import { PauseCancelTaskRepository } from "#adapters/PauseCancelTaskRepository.js"
import express from "express"
import cors from "cors"
async function startWorker() { async function startWorker() {
const rmqClient = await startRMQClient() const rmqClient = await startRMQClient()
@@ -27,19 +30,47 @@ async function startWorker() {
const pauseRepository = new PauseCancelTaskRepository(pgClient) const pauseRepository = new PauseCancelTaskRepository(pgClient)
const simUseCases = new SimUseCases({
httpClient: httpClient,
operationRepository: operationRepository,
orderRepository: orderRepository,
pauseRepository: pauseRepository
})
const simActivationController = new SimController( const simActivationController = new SimController(
rmqClient, rmqClient,
new SimUseCases({ simUseCases
httpClient: httpClient,
operationRepository: operationRepository,
orderRepository: orderRepository,
pauseRepository: pauseRepository
})
) )
const simRouter = new SimRouter(simActivationController, rmqClient) const simRouter = new SimRouter(simActivationController, rmqClient)
// de momento solo una cola por simplificar // de momento solo una cola por simplificar
rmqClient.consume("sim.objenious", simRouter.route) 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() startWorker()

View File

@@ -6,6 +6,7 @@ export interface IOperationsRepository {
createOperation(data: ObjeniousOperation): Promise<Result<string, ObjeniousOperation>> createOperation(data: ObjeniousOperation): Promise<Result<string, ObjeniousOperation>>
updateOperation(data: ObjeniousOperationChange): Promise<Result<string, ObjeniousOperation>> updateOperation(data: ObjeniousOperationChange): Promise<Result<string, ObjeniousOperation>>
getPendingOperations(): Promise<Result<string, ObjeniousOperation[]>> getPendingOperations(): Promise<Result<string, ObjeniousOperation[]>>
getSuspendedTime(iccid: string): Promise<Result<string, { total_milliseconds: number, total_days: number }>>
} }
export type ObjeniousOperation = { export type ObjeniousOperation = {

View File

@@ -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 { ObjeniousOperationsRepository } from "./ObjeniousOperationRepository.js";
import { httpObjClient, postgresClient } from "../config/config.test.js"; import { httpObjClient, postgresClient } from "../config/config.test.js";
import { ObjeniousOperation } from "../domain/operationsRepository.port.js"; import { ObjeniousOperation } from "../domain/operationsRepository.port.js";
@@ -21,6 +21,7 @@ describe("[Integration] Test API requests", () => {
httpObjClient, httpObjClient,
postgresClient postgresClient
) )
const suspend_iccid = "test_suspended_time_iccid";
before(async () => { before(async () => {
await repository.createOperation(correctOperation) await repository.createOperation(correctOperation)
@@ -36,4 +37,40 @@ describe("[Integration] Test API requests", () => {
* - Se ignoran las erroneas * - 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}'`);
});
}) })

View File

@@ -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<Result<string, { total_milliseconds: number, total_days: number }>> {
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
};
}
}
} }