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

32
.env
View File

@@ -1,32 +0,0 @@
PORT=3000
API_HOSTNAME=0.0.0.0
RABBITMQ_USER=guest
RABBITMQ_PASSWORD=guest
ENVIORMENT=development
#RABBITMQ_HOST=rabbitmq-sim-broker
RABBITMQ_HOST=localhost
RABBITMQ_PORT=5672
RABBITMQ_USER=guest
RABBITMQ_PASSWORD=guest
RABBITMQ_SECURE=false
RABBITMQ_VHOST=sim-vhost
# Hay cosas que unificar de varios servicios
#POSTGRES_HOST=postgresql-sim
POSTGRES_HOST=localhost
POSTGRES_DB=postgres
POSTGRES_DATABASE=postgres
POSTGRES_PORT=5433
POSTGRES_USER=postgres
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

2
.gitignore vendored
View File

@@ -20,3 +20,5 @@ node_modules
dist/*
.env

View File

@@ -12,13 +12,13 @@ La compañia a la que pertenece cada peticion y por tanto el servicio que lo va
## Decisiones pendientes
- [x] La capa worker según acción y la de operaciones de proveedores, se podrían unir en una sola con un enrutamiento por acción y compañía, pasando de tener claves `sim.[acción]` a `sim.[compañia].[acción]`. *Se ha aplicado el cambio ahora las routing keys tienen la estructura `sim.[compañia].[acción]`*
- [x] La estructura de RMQ se genera por medio del JSON, igual habría que definir cada cola en el worker que la consuma para poder añadir workers sin parar el RMQ. *Se ha aplicado el cambio, ahora solo se define en el json el broker principal para garantizar que exita sin servicios consumidores. Sin embargo tal como estan estructurdos los proyectos no es posible reiniciar solo un servicio*
- [x] La capa worker según acción y la de operaciones de proveedores, se podrían unir en una sola con un enrutamiento por acción y compañía, pasando de tener claves `sim.[acción]` a `sim.[compañia].[acción]`. _Se ha aplicado el cambio ahora las routing keys tienen la estructura `sim.[compañia].[acción]`_
- [x] La estructura de RMQ se genera por medio del JSON, igual habría que definir cada cola en el worker que la consuma para poder añadir workers sin parar el RMQ. _Se ha aplicado el cambio, ahora solo se define en el json el broker principal para garantizar que exita sin servicios consumidores. Sin embargo tal como estan estructurdos los proyectos no es posible reiniciar solo un servicio_
- [ ] Versionado de la API.
- [x] Método para sacar la compañía a partir del iccid, o buscar en la BDD si no es posible. *De momento es un objeto Map en el servicio de gateway*
- [ ] Cola de mensajes que no se han podido procesar. Distinguir según error de red; se reintenta; o error del propio mensaje; se envía a la cola de errores. v2 Se ha creado una cola de delay pero no se distingue el tipo de error, despues de n reintentos el mensaje va a la cola de dead-letter.
- [ ] Seguimiento de las peticiones de Objenious, por cada peticion hay qye hacer un seguimiento del request y de los mass action para saber si las activaciones han tenido exito. Habria que crear otra cola para consultar cada x tiempo o mejor un cron?
- [ ] Actualizar en la base de datos el estado de las peticiones de las sim y añadir el número de telefono cuando se activen o cuando se cumpla una accion.
- [x] Método para sacar la compañía a partir del iccid, o buscar en la BDD si no es posible. _De momento es un objeto Map en el servicio de gateway_
- [ ] Cola de mensajes que no se han podido procesar. Distinguir según error de red; se reintenta; o error del propio mensaje; se envía a la cola de errores. v2 Se ha creado una cola de delay pero no se distingue el tipo de error, después de n reintentos el mensaje va a la cola de dead-letter.
- [x] Seguimiento de las peticiones de Objenious, por cada peticion hay qye hacer un seguimiento del request y de los mass action para saber si las activaciones han tenido exito. Habria que crear otra cola para consultar cada x tiempo o mejor un cron?
- [x] Actualizar en la base de datos el estado de las peticiones de las sim y añadir el número de telefono cuando se activen o cuando se cumpla una accion.
## Versión con consumidores basados en la compañia
@@ -32,8 +32,14 @@ OBJENIOUS (33)2011a
## Diagrama de las colas de Rabbitmq
Actualmente la topologia de las colas consiste en un exchage principal que recibe todos los mensajes y los redistribuye en las colas de cada empresa y a la de logs. Para evitar reintentos de mensajes instantaneos, que podrian ser inutiles si algún servicio se ha caido, se ha añadido una cola de delay que alamcena los mesajes fallidos durante n segundos antes de ser reenviados al exchange principal. Si despues de n reintentos el mensaje sigue fallando se envia a la cola de dead-letter para ser procesado manualmente.
Actualmente la topología de las colas consiste en un exchage principal que recibe todos los mensajes y los redistribuye en las colas de cada empresa y a la de logs. Para evitar reintentos de mensajes instantáneos, que podrían ser inútiles si algún servicio se ha caído, se ha añadido una cola de delay que almacena los mensajes fallidos durante n segundos antes de ser reenviados al exchange principal. Si después de n reintentos el mensaje sigue fallando se envía a la cola de dead-letter para ser procesado manualmente.
![img](./imgs/diagrama-rabbit.png)
La decisión del numero de reintentos y la cola de dlx se hace en los servicios, con una configuracion global en shared.
La decisión del numero de reintentos y la cola de dlx se hace en los servicios, con una configuración global en shared.
## Puertos internos para comunicaciones entre sub-servicios
- **3000**: Gateway (sim-entrada-eventos)
- **3001**: Consumidor NOS (sim-consumidor-nos)
- **3002**: Consumidor Objenious (sim-consumidor-objenious)

View File

@@ -72,6 +72,7 @@ services:
- ${PORT}
volumes:
- ./.env:/home/node/app/.env:ro
- ./sim-consumidor-nos.env:/home/node/app/packages/sim-consumidor-nos/.env:ro
- ./sim-consumidor-objenious.env:/home/node/app/packages/sim-consumidor-objenious/.env:ro
- ./sim-objenious-cron.env:/home/node/app/packages/sim-objenious-cron/.env:ro
- ./obj.pem:/home/node/app/packages/sim-consumidor-objenious/obj.pem:ro

View File

@@ -46,6 +46,10 @@ pipeline {
cleanRemote: false,
execCommand: "ln -sf $BASE_REMOTE_PATH/vault/savefamily/sf-sims/sim-consumidor-objenious.env $APP_REMOTE_PATH/sim-consumidor-objenious.env"
),
sshTransfer(
cleanRemote: false,
execCommand: "ln -sf $BASE_REMOTE_PATH/vault/savefamily/sf-sims/sim-consumidor-nos.env $APP_REMOTE_PATH/sim-consumidor-nos.env"
),
sshTransfer(
cleanRemote: false,
execCommand: "ln -sf $BASE_REMOTE_PATH/vault/savefamily/sf-sims/sim-objenious-cron.env $APP_REMOTE_PATH/sim-objenious-cron.env"

View File

@@ -7,6 +7,7 @@ networks:
services:
rabbitmq-sim-broker:
container_name: rabbitmq-sim-broker
hostname: rabbitmq-sim
image: "rabbitmq:4.2.2-management"
ports:
- "5672:5672"
@@ -23,6 +24,7 @@ services:
RABBITMQ_DEFAULT_USER: ${RABBITMQ_USER}
RABBITMQ_DEFAULT_PASS: ${RABBITMQ_PASSWORD}
volumes:
- ./rabbitmq-data/:/var/lib/rabbitmq/
- ./rabbitmq_plugins/enabled_plugins:/etc/rabbitmq/enabled_plugins:ro
- ./deployment/local/rabbit/rabbitmq.conf:/etc/rabbitmq/rabbitmq.conf:ro
- ./deployment/local/rabbit/definitions.json:/etc/rabbitmq/definitions.json:ro

File diff suppressed because one or more lines are too long

View File

@@ -11,7 +11,11 @@ post {
}
body:form-urlencoded {
<<<<<<< HEAD
iccid: 8933201125068889894
=======
iccid: 8933201125068890892
>>>>>>> main
}
settings {

View File

@@ -0,0 +1,26 @@
meta {
name: France Suspended Lines
type: http
seq: 17
}
get {
url: {{baseurl}}/france/lines?status=SUSPENDED&limit=100
body: none
auth: inherit
}
params:query {
status: SUSPENDED
limit: 100
}
vars:pre-request {
iccid: 8933201125065160331
~baseurl: http://localhost:3002
}
settings {
encodeUrl: true
timeout: 0
}

View File

@@ -0,0 +1,21 @@
meta {
name: France Suspended Time
type: http
seq: 15
}
get {
url: {{baseurl}}/france/lines/{{iccid}}/suspended-time
body: none
auth: inherit
}
vars:pre-request {
iccid: 8933201125065160331
~baseurl: http://localhost:3002
}
settings {
encodeUrl: true
timeout: 0
}

View File

@@ -15,7 +15,7 @@ params:query {
}
body:form-urlencoded {
iccid: 8935103196306448300
iccid: 8933201125065160331
}
settings {

View File

@@ -0,0 +1,38 @@
meta {
name: Consumption details
type: http
seq: 21
}
get {
url: https://api-getway.objenious.com/ws/diagXL/massHistories
body: formUrlEncoded
auth: bearer
}
auth:bearer {
token: {{ws-access-token-partenaire}}
}
body:json {
{
"identifier": {
"identifiers": ["8933201124059175967"],
"identifierType": "ICCID"
}
}
}
body:form-urlencoded {
identifier.identifierType: "ICCID"
identifier.identifiers: ["8933201125068889373"]
}
vars:pre-request {
~id: 5187320
}
settings {
encodeUrl: true
timeout: 0
}

View File

@@ -7,11 +7,11 @@
],
"scripts": {
"test": "vitest watch",
"build": "rm -rf ./dist && yarn workspaces foreach -Api run build && cp .env dist/ && yarn setup:runtime",
"build": "rm -rf ./dist && yarn workspaces foreach -Api run build && yarn setup:runtime",
"setup:runtime": "mkdir -p dist/packages/node_modules && ln -sf ../sim-shared dist/packages/node_modules/sim-shared && ln -sf ../sf-consumidor-objenious dist/packages/node_modules/sim-consumidor-objenious",
"start": "yarn setup:runtime && yarn workspaces foreach -Apiv run start",
"start": "yarn workspaces foreach -Apiv run start",
"typecheck": "npx tsc --noEmit",
"dev": "yarn workspaces foreach -Apiv --exclude sim-objenious-cron run dev",
"dev": "yarn workspaces foreach -Apiv run dev",
"lint": "eslint .",
"lint:fix": "eslint --fix .",
"format": "prettier --write .",

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[] = []