Merge main -> migracion alai

This commit is contained in:
2026-04-29 17:08:30 +02:00
parent 858932f260
commit bb31efb271
43 changed files with 555 additions and 98 deletions

View File

@@ -1,5 +1,4 @@
export const env = {
ENVIRONMENT: process.env.ENVIORMENT,
POSTGRES_USER: process.env.POSTGRES_USER,
POSTGRES_PASSWORD: process.env.POSTGRES_PASSWORD,
POSTGRES_PORT: process.env.POSTGRES_PORT,

View File

@@ -1,8 +0,0 @@
NOS_BASE_URL=localhost
APP_PORT=3001
APP_HOST="0.0.0.0"
ENVIORMENT=development
NOS_ACCESS_TOKEN=2YGhecTr4+uKbVKxaqBlk2edsrHA2OQY
NOS_BASE_URL=https://nosconnectcenter-api.iot-x.com

View File

@@ -13,7 +13,6 @@ try {
}
export const env = {
ENVIRONMENT: process.env.ENVIORMENT,
POSTGRES_USER: process.env.POSTGRES_USER,
POSTGRES_PASSWORD: process.env.POSTGRES_PASSWORD,
POSTGRES_PORT: process.env.POSTGRES_PORT,

View File

@@ -1,4 +1,6 @@
# claves de Objenious
HOST=0.0.0.0
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

View File

@@ -10,6 +10,7 @@ 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";
import { ObjeniousLinesRepository } from "sim-shared/infrastructure/ObjeniousLinesRepository.js";
describe("SimController Integration Tests (Real UseCases)", () => {
let eventBusMock: any;
@@ -32,11 +33,13 @@ describe("SimController Integration Tests (Real UseCases)", () => {
);
const orderRepository = new OrderRepository(postgrClient);
const pauseRepository = new PauseCancelTaskRepository(postgrClient);
const linesRepository = new ObjeniousLinesRepository(postgrClient) // tiene que apuntar a "intranet"
useCases = new SimUseCases({
httpClient: httpInstance,
operationRepository: operationRepository,
orderRepository: orderRepository,
pauseRepository: pauseRepository
pauseRepository: pauseRepository,
objeniousLinesRepository: linesRepository
});
// @ts-expect-error
useCases.findActivationDate = async (data: ActionData) => new Date()

View File

@@ -4,6 +4,9 @@ import { SimUseCases } from "./Sim.usecases.js";
import { SimEvents } from "sim-shared/domain/SimEvents.js";
import { Result } from "sim-shared/domain/Result.js";
import { ActionData } from "#domain/DTOs/objeniousapi.js";
import { Request, Response } from "express"
import { PaginationArgs, QueryPaginationArgs } from "sim-shared/domain/PaginationArgs.js";
import { paginationValidator } from "./httpValidators.js";
/**
* La clase usa generadores de funciones para mantener el contexto
@@ -219,6 +222,36 @@ export class SimController {
}
}
public queryLines() {
const DEFAULT_LIMIT = 1000
const DEFAULT_OFFSET = 0
return async (req: Request, res: Response) => {
const queryParams = req.query
const paginationArgs: QueryPaginationArgs = {
limit: queryParams.limit as string | undefined,
offset: queryParams.offset as string | undefined
}
const validationRes = paginationValidator.validate(paginationArgs)
if (validationRes.error != undefined) {
res.status(402).json(validationRes)
return;
}
const paginationValues = {
limit: (queryParams.limit != undefined) ? Number(queryParams.limit) : DEFAULT_LIMIT,
offset: (queryParams.offset != undefined) ? Number(queryParams.offset) : DEFAULT_OFFSET
}
const status = req.query.status
const queryRes = await this.useCases.getLinesByQuery({ status: status as string | undefined }, paginationValues)
res.json(queryRes)
}
}
/**
* TODO:
* - Loguear motivos de la no validacion

View File

@@ -7,6 +7,9 @@ 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"
import { ObjeniousLinesRepository } from "sim-shared/infrastructure/ObjeniousLinesRepository.js"
import { error } from "node:console"
import { ObjeniousLine, ObjeniousLineDb } from "sim-shared/domain/objeniousLine.js"
// TODO:
// - Pasar a un archivo de DTOs
@@ -17,17 +20,19 @@ export class SimUseCases {
private readonly objeniousRepository: ObjeniousOperationsRepository
private readonly orderRepository: OrderRepository
private readonly pauseRepository: PauseCancelTaskRepository
private readonly objeniousLinesRepository: ObjeniousLinesRepository
constructor(args: {
httpClient: HttpClient,
operationRepository: ObjeniousOperationsRepository,
orderRepository: OrderRepository,
pauseRepository: PauseCancelTaskRepository
pauseRepository: PauseCancelTaskRepository,
objeniousLinesRepository: ObjeniousLinesRepository
}) {
this.httpClient = args.httpClient
this.objeniousRepository = args.operationRepository
this.orderRepository = args.orderRepository
this.pauseRepository = args.pauseRepository
this.objeniousLinesRepository = args.objeniousLinesRepository
}
private async logOperation(data: ObjeniousOperation) {
@@ -119,7 +124,6 @@ export class SimUseCases {
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;
@@ -269,7 +273,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
@@ -278,7 +282,7 @@ export class SimUseCases {
// 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)
//console.log("LineData", lineData.data)
if (lineData.error != undefined) {
console.error(lineData.error)
} else {
@@ -321,13 +325,14 @@ export class SimUseCases {
}
// 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)
@@ -434,10 +439,53 @@ export class SimUseCases {
identifier: terminationData.identifier
},
url: OPERATION_URL,
iccid: terminationData.identifier.identifiers[0], //
iccid: terminationData.identifier.identifiers[0],
operation: "terminate"
})
}
/**
* Calcula el tiempo que una linea ha estado en suspensión
*/
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 };
}
}
/**
* Busqueda de líneas **en nuestro volcado** según una query y con paginacion
*/
public async getLinesByQuery(query: { status?: string | undefined }, pagination: { limit: number, offset: number })
: Promise<Result<string, {
data: ObjeniousLineDb[],
offset: number,
rowCount: number
}>> {
try {
const linesQuery = await this.objeniousLinesRepository.getLinesByStatus(query, pagination)
return {
data: linesQuery,
}
} catch (e) {
return {
error: String(e)
}
}
}
}

View File

@@ -0,0 +1,18 @@
import { BodyValidator, Validator } from "sim-shared/aplication/BodyValidator.js";
import { QueryPaginationArgs } from "sim-shared/domain/PaginationArgs.js";
const limitPositiveOrUndefined = <Validator<QueryPaginationArgs>>{
field: "limit",
validationFunc: (args) => (args.limit == undefined || !isNaN(+args.limit) && parseInt(args.limit) >= 0),
errorMsg: "El campo limit debe ser un numero o undefined (default 0)"
}
const offsetPositiveOrUndefined = <Validator<QueryPaginationArgs>>{
field: "offset",
validationFunc: (args) => (args.offset == undefined || isNaN(+args.offset) && parseInt(args.offset) >= 1),
errorMsg: "El campo offset debe ser un numero o undefined (default 0)"
}
export const paginationValidator = new BodyValidator<QueryPaginationArgs & {}>([
limitPositiveOrUndefined,
offsetPositiveOrUndefined
])

View File

@@ -4,7 +4,10 @@ import path from "node:path";
loadEnvFile(path.join("../../.env")) // Global
loadEnvFile(path.join("./.env")) // base
export const env = {
PORT: parseInt(process.env.OBJENIOUS_CONSUMER_PORT || "3002"),
ENVIRONMENT: process.env.ENVIORMENT,
POSTGRES_USER: process.env.POSTGRES_USER,
POSTGRES_PASSWORD: process.env.POSTGRES_PASSWORD,

View File

@@ -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
})

View File

@@ -10,6 +10,13 @@ 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"
import assert from "node:assert";
import { env } from "#config/env/index.js"
import { ObjeniousLinesRepository } from "sim-shared/infrastructure/ObjeniousLinesRepository.js"
import { postgresClientIntranet } from "#config/intranetPostgresConfig.js"
async function startWorker() {
const rmqClient = await startRMQClient()
@@ -26,20 +33,76 @@ async function startWorker() {
const orderRepository = new OrderRepository(pgClient)
const pauseRepository = new PauseCancelTaskRepository(pgClient)
const simActivationController = new SimController(
rmqClient,
new SimUseCases({
httpClient: httpClient,
operationRepository: operationRepository,
orderRepository: orderRepository,
pauseRepository: pauseRepository
})
const linesRepository = new ObjeniousLinesRepository(
postgresClientIntranet
)
const simRouter = new SimRouter(simActivationController, rmqClient)
const simUseCases = new SimUseCases({
httpClient: httpClient,
operationRepository: operationRepository,
orderRepository: orderRepository,
pauseRepository: pauseRepository,
objeniousLinesRepository: linesRepository
})
const simController = new SimController(
rmqClient,
simUseCases
)
const simRouter = new SimRouter(simController, rmqClient)
// de momento solo una cola por simplificar
rmqClient.consume("sim.objenious", simRouter.route)
// Servidor express
const port = env.PORT
const app = express()
app.use(cors())
app.use(express.json())
app.get("/health", async (req, res) => {
res.json({ ok: "true" })
})
// TODO: meter el template de controller con los validadores
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:{...}} || {error:{...}}
})
/**
* Opciones query:
* - state
*
* Respuestas:
* - OK: data: {
* lines: ObjeniousLineDb[],
* offset: number,
* rowCount: number
* }
*
* - ERR: error: {
* message: string
* }
*/
app.get("/lines", simController.queryLines())
assert.ok(port, "Puerto del servicio no definido")
app.listen(port, () => {
console.log(`[o] HTTP server listening on port ${port}`)
})
}
startWorker()

View File

@@ -13,10 +13,10 @@
"types": "./config/*.ts",
"default": "./config/*.js"
},
"#shared/*.js": {
"sim-shared/*.js": {
"default": "../sim-shared/*.js"
},
"#shared/*": {
"sim-shared/*": {
"default": "../sim-shared/*.js"
},
"#adapters/*.js": {

View File

@@ -1,8 +1,8 @@
import { BodyValidator } from "sim-shared/aplication/BodyValidator.js"
import { OrderUsecases } from "./Order.usecases.js"
import { Request, Response } from "express"
import { PaginationArgs } from "#domain/common.js"
import { idValidator, uuidValidator } from "./httpValidators.js"
import { PaginationArgs } from "sim-shared/domain/PaginationArgs.js"
export class OrderController {
private orderUseCases: OrderUsecases

View File

@@ -1,5 +1,5 @@
import { PaginationArgs } from "#domain/common.js";
import { OrderQuery } from "sim-shared/domain/Order.js";
import { PaginationArgs } from "sim-shared/domain/PaginationArgs.js";
import { OrderRepository } from "sim-shared/infrastructure/OrderRepository.js";

View File

@@ -3,6 +3,7 @@ import path from "node:path";
loadEnvFile(path.join("../../.env")) // Global
//loadEnvFile(path.join("./.env")) // Global
export const env = {
ENVIRONMENT: process.env.ENVIORMENT,
@@ -22,5 +23,7 @@ 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)
CONNECTIONS_URL: String(process.env.CONNECTIONS_URL),
OBJENIOUS_CONSUMER_URL: process.env.OBJENIOUS_CONSUMER_URL,
NOS_CONSUMER_URL: process.env.NOS_CONSUMER_URL,
};

View File

@@ -1,6 +0,0 @@
export type PaginationArgs = {
limit?: number,
offset?: number,
start?: number
}

View File

@@ -6,6 +6,7 @@ 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";
import { franceRoutes } from "#adapters/franceRoutes.http.js";
const PORT = env.API_PORT
const HOSTNAME = "0.0.0.0"
@@ -31,6 +32,9 @@ app.use("/orders", orderRoutes)
app.use("/docs", express.static(path.join(process.cwd(), '../../docs')))
// Rutas especificas para casos especiales como el tiempo de suspension de francia
app.use("/france", franceRoutes)
app.get("/health", (req, res) => {
res.status(200).json({ status: "ok" })
})

View File

@@ -0,0 +1,33 @@
import { Router, Request } from "express"
import { ClientRequest, } from "http"
import { createProxyMiddleware } from "http-proxy-middleware"
import { env } from "#config/env/index.js"
import assert from "node:assert"
const franceRoutes = Router()
const FRANCE_URL = env.OBJENIOUS_CONSUMER_URL
assert.ok(FRANCE_URL, "No se ha definido una URL para el servicio consumidor de Francia")
franceRoutes.use("", createProxyMiddleware({
target: FRANCE_URL,
changeOrigin: true,
pathRewrite: {
'^/france/*': '/'
},
on: {
proxyReq: (proxyReq: ClientRequest, req: Request) => {
/* Debug de las peticiones */
const protocol = req.protocol;
const host = req.get('host');
const originalFullUrl = `${protocol}://${host}${req.originalUrl}`;
const destinationFullUrl = `${FRANCE_URL}${proxyReq.path}`;
//console.log(`[Proxy Req]: ${req.method} ${req.url} -> ${proxyReq.path}`);
}
}
}
))
//orderRoutes.get("/:iccid/suspended-time",)
export { franceRoutes }

View File

@@ -47,5 +47,3 @@ if (env.ENVIRONMENT == "production") {
}
console.log("[i] verificado env")

View File

@@ -6,7 +6,7 @@ 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 { ObjeniousLinesRepository } from "sim-shared/infrastructure/ObjeniousLinesRepository.js"
import { postgresClientIntranet } from "./config/intranetPostgresConfig.js"
import { PauseCancelTaskRepository } from "sim-consumidor-objenious/infrastructure/PauseCancelTaskRepository.js"
import { PauseTerminateTask } from "./tasks/check_pause_terminate.js"
@@ -52,7 +52,8 @@ async function startCron() {
httpClient: httpClient,
operationRepository: operationRepository,
orderRepository: orderRepository,
pauseRepository: pauseRepo
pauseRepository: pauseRepo,
objeniousLinesRepository: objeniousLineRepository
})
const pauseTask = new PauseTerminateTask(
@@ -62,7 +63,7 @@ async function startCron() {
orderRepository
)
await objTask.getPendingOperations()
//await objTask.getPendingOperations()
const PERIODO_PETICIONES = 10 * 60 * 1000
const interval = setInterval(async () => {
try {
@@ -73,6 +74,7 @@ async function startCron() {
}, PERIODO_PETICIONES)
const PERIODO_VOLCADO = 60 * 60 * 1000
//await volcadoLineasTask.loadLines()
const volcadoInterval = setInterval(async () => {
try {
await volcadoLineasTask.loadLines()
@@ -81,7 +83,7 @@ async function startCron() {
}
}, PERIODO_VOLCADO)
await pauseTask.run()
// await pauseTask.run()
const PERIODO_CANCELACIONES = 60 * 60 * 1000;
const clacelacionesInterval = setInterval(async () => {
await pauseTask.run()

View File

@@ -37,6 +37,11 @@ export class CheckObjeniousRequests {
// Todas las validas
const operacionesValidas = pendingOperations.data
.filter((e) => e.request_id != undefined)
.filter((e) => e.operation != "terminate" && e.operation != "suspend")
// Filtrar suspension / terminacion
// Validas sin MassId
const solicitarMassId = operacionesValidas
.filter((e) => e.mass_action_id == undefined)

View File

@@ -66,6 +66,7 @@ export class PauseTerminateTask {
}
while (!lines.done) {
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)
@@ -74,7 +75,6 @@ export class PauseTerminateTask {
lineasActualizadas.push(...lines.value.data)
}
lines = await lineGenerator.next()
}
console.log("Cargado: ", lineasActualizadas)
@@ -126,6 +126,7 @@ export class PauseTerminateTask {
switch (operacionTipo) {
case "suspend":
result = await this.simUsecases.suspend(actionData)()
console.log("SUSPENDED", result)
break;
case "terminate":
result = await this.simUsecases.terminate(actionData)()

View File

@@ -1,5 +1,5 @@
import { lineToCreateLineDto, ObjeniousLine } from "sim-shared/domain/objeniousLine.js";
import { ObjeniousLinesRepository } from "../infranstructure/ObjeniousLinesRepository.js";
import { ObjeniousLinesRepository } from "sim-shared/infrastructure/ObjeniousLinesRepository.js";
import { ObjeniousOperationsRepository } from "sim-shared/infrastructure/ObjeniousOperationRepository.js";
export class TaskVolcadoLineas {

View File

@@ -0,0 +1,14 @@
/**
* Los parametros según entran por la query pueden ser de tipo string
*/
export type QueryPaginationArgs = {
limit?: string,
offset?: string,
start?: string// start = offset; Por compatibilidad, normalmente solo limit y offset
}
export type PaginationArgs = {
limit?: number,
offset?: number,
start?: number // start = offset; Por compatibilidad, normalmente solo limit y offset
}

View File

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

View File

@@ -1,12 +1,12 @@
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 { ObjeniousLinesRepository } from "sim-shared/infrastructure/ObjeniousLinesRepository.js";
import assert from "node:assert";
import { postgresClient } from "../config/config.test.js";
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 pgClient = postgresClient// 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,

View File

@@ -4,7 +4,7 @@
*/
import { createHash } from "node:crypto";
import { PoolClient } from "pg";
import { CreateObjeniousLineDTO } from "sim-shared/domain/objeniousLine.js";
import { CreateObjeniousLineDTO, ObjeniousLineDb } from "sim-shared/domain/objeniousLine.js";
import { PgClient } from "sim-shared/infrastructure/PgClient.js";
export class ObjeniousLinesRepository {
@@ -24,6 +24,58 @@ export class ObjeniousLinesRepository {
}
}
/**
* Hay que hacer la query un poco mas general
*/
public async getLinesByStatus(query: { status?: string | undefined }, pagination: { limit: number, offset: number }) {
// $1 y $2 se reservan para paginación
const values: (string | number)[] = [
pagination.limit,
pagination.offset,
]
const paginationStr = `
LIMIT $1
OFFSET $2
`
const conditionsStr = `
WHERE
raw -> 'status' ->> 'networkStatus' = $3
`
let queryStr = `
SELECT * FROM objenious_lines
`
if (query.status != undefined) {
queryStr = queryStr + conditionsStr
values.push(query.status)
}
queryStr = queryStr + `
${paginationStr};
`
let client: PoolClient | undefined = undefined;
try {
client = await this.pgClient.connect();
const res = await client.query<ObjeniousLineDb>(queryStr, values);
return {
data: res.rows,
offset: pagination.offset,
rowCount: res.rowCount ?? 0,
}
} catch (err) {
console.error('Error en la query:', err);
throw err;
} finally {
if (client != undefined) {
client.release()
}
}
}
public async insertOrUpdate(data: CreateObjeniousLineDTO) {
const query = `
INSERT INTO objenious_lines (

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 { 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}'`);
});
})

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
};
}
}
}

View File

@@ -20,7 +20,6 @@ const order2 = <CreateOrderDTO>{
payload: { iccid: "5678", action: "activate" }
}
describe("Test OrderRepository", {}, (ctx) => {
const orderRepo = new OrderRepository(postgresClient)
let testIds: number[] = []