Compare commits
19 Commits
WEBINT-334
...
1.5.2
| Author | SHA1 | Date | |
|---|---|---|---|
| c9733113cf | |||
| 4c1d6ac2c4 | |||
| 07e7a0d457 | |||
| 48361ab33f | |||
| a3c7c224b1 | |||
| 324aec3001 | |||
| 05e941710b | |||
| f78a333e1e | |||
| 01c55cba0f | |||
| 10b2ae244c | |||
| 2dba2ebfae | |||
| d7eb4ad326 | |||
| d818441bde | |||
| c91965567d | |||
| d063b47bec | |||
| 6112de297b | |||
| 166c940295 | |||
| 246e4cb83b | |||
| 4517796ef3 |
32
.env
32
.env
@@ -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
2
.gitignore
vendored
@@ -20,3 +20,5 @@ node_modules
|
||||
|
||||
|
||||
dist/*
|
||||
|
||||
.env
|
||||
|
||||
22
README.md
22
README.md
@@ -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.
|
||||
|
||||

|
||||
|
||||
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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
@@ -11,7 +11,7 @@ post {
|
||||
}
|
||||
|
||||
body:form-urlencoded {
|
||||
iccid: 8933201125068887054
|
||||
iccid: 8933201125068890892
|
||||
}
|
||||
|
||||
settings {
|
||||
|
||||
26
docs/sim-api/France Suspended Lines.bru
Normal file
26
docs/sim-api/France Suspended Lines.bru
Normal 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
|
||||
}
|
||||
21
docs/sim-api/France Suspended Time.bru
Normal file
21
docs/sim-api/France Suspended Time.bru
Normal 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
|
||||
}
|
||||
@@ -15,7 +15,7 @@ params:query {
|
||||
}
|
||||
|
||||
body:form-urlencoded {
|
||||
iccid: 8935103196306448300
|
||||
iccid: 8933201125065160331
|
||||
}
|
||||
|
||||
settings {
|
||||
|
||||
@@ -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 .",
|
||||
|
||||
1
packages/_template/config/env/index.ts
vendored
1
packages/_template/config/env/index.ts
vendored
@@ -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,
|
||||
|
||||
@@ -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
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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
|
||||
])
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
})
|
||||
@@ -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()
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { PaginationArgs } from "#domain/common.js";
|
||||
import { PaginationArgs } from "sim-shared/domain/PaginationArgs.js";
|
||||
import { OrderRepository } from "sim-shared/infrastructure/OrderRepository.js";
|
||||
|
||||
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
|
||||
export type PaginationArgs = {
|
||||
limit?: number,
|
||||
offset?: number,
|
||||
start?: number
|
||||
}
|
||||
@@ -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" })
|
||||
})
|
||||
|
||||
@@ -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 }
|
||||
@@ -47,5 +47,3 @@ if (env.ENVIRONMENT == "production") {
|
||||
}
|
||||
|
||||
|
||||
console.log("[i] verificado env")
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)()
|
||||
|
||||
@@ -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 {
|
||||
|
||||
14
packages/sim-shared/domain/PaginationArgs.ts
Normal file
14
packages/sim-shared/domain/PaginationArgs.ts
Normal 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
|
||||
}
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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,
|
||||
@@ -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 (
|
||||
@@ -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}'`);
|
||||
});
|
||||
})
|
||||
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,7 +20,6 @@ const order2 = <CreateOrderDTO>{
|
||||
payload: { iccid: "5678", action: "activate" }
|
||||
}
|
||||
|
||||
|
||||
describe("Test OrderRepository", {}, (ctx) => {
|
||||
const orderRepo = new OrderRepository(postgresClient)
|
||||
let testIds: number[] = []
|
||||
|
||||
Reference in New Issue
Block a user