36 Commits

Author SHA1 Message Date
e1450c6e97 POST -> PATCH 2026-04-22 15:22:37 +02:00
e40a19bbfb Bug de correlation_id undefined 2026-04-22 13:31:24 +02:00
fbdb64f3a1 Arreglo de bugs ordes 2026-04-22 12:59:23 +02:00
9a29f49669 Fix orders nos 2026-04-22 12:31:46 +02:00
c2081191ae Trazabilidad de nos, arreglo de orders 2026-04-21 17:39:09 +02:00
f0f3827fd0 Registro del estado/resultado de las operaciones de NOS 2026-04-21 15:51:16 +02:00
ee8f84bc57 Typescript 6 2026-04-21 13:33:01 +02:00
f95677d503 Error pageNOS 2026-04-21 12:50:54 +02:00
59b0b57ec2 Merge branch 'main' into WEBINT-334-migracion-nos 2026-04-21 10:47:08 +02:00
9174b0b6a4 Nombres de colas 2026-04-21 10:22:53 +02:00
e62c49ce91 Endpoints NOS 2026-04-21 10:11:21 +02:00
32990b4dcd Controladores y rutas 2026-04-17 15:49:53 +02:00
da2413002b Repositorio de nos completo 2026-04-17 14:06:41 +02:00
fdbb81ba64 Inicio port NOS 2026-04-16 17:46:32 +02:00
964ea6add9 Inicio migracion NOS 2026-04-16 12:44:31 +02:00
602878acf4 console log de debug 2026-04-16 12:01:31 +02:00
0aa52feaac Proxy 2026-04-16 11:51:30 +02:00
15b70309da Fix networkStatus 2026-04-15 15:11:13 +02:00
7001fccbf7 Problema de creacion de operacion de pausa/cancelacion 2026-04-15 13:50:20 +02:00
cffee785b2 logs 2026-04-15 12:48:08 +02:00
33d260310c Reactivate cuando la linea esté suspendida 2026-04-15 12:47:26 +02:00
e359acc1d5 Validaciones pausa instantaneas 2026-04-15 11:47:33 +02:00
bb4bce4a6d fix 2026-04-15 11:11:55 +02:00
eac74ef0cd Error key evento 2026-04-15 10:31:21 +02:00
1dc4eb5648 validador 2026-04-15 10:28:52 +02:00
a35a6c2b60 Error endpoint 2026-04-15 10:27:58 +02:00
1f78f4a3e1 Periodo pruebas reducidio 2026-04-15 10:18:49 +02:00
1e98559f3a Fix 2026-04-15 10:17:36 +02:00
ef0f860b9d Erro handler 2026-04-14 16:20:15 +02:00
0bff55379f tab 2026-04-14 16:15:01 +02:00
4d34308a13 Bug de logueo de operacion 2026-04-14 16:07:17 +02:00
70bf73b0a4 Error query 2026-04-10 11:11:12 +02:00
e3849d8217 Archivos de migraciones 2026-04-09 12:48:36 +02:00
d9854a12a8 Version de migrate 2026-04-09 12:31:06 +02:00
48d387a8da Migraciones antes de lanzar start 2026-04-09 12:10:40 +02:00
93d3e13793 Merge pull request 'WEBINT-328-Pausas-cacelaciones' (#1) from WEBINT-328-Pausas-cacelaciones into main
Reviewed-on: #1
2026-04-09 10:03:47 +00:00
69 changed files with 2499 additions and 944 deletions

5
.env
View File

@@ -20,10 +20,13 @@ POSTGRES_DB=postgres
POSTGRES_DATABASE=postgres POSTGRES_DATABASE=postgres
POSTGRES_PORT=5433 POSTGRES_PORT=5433
POSTGRES_USER=postgres POSTGRES_USER=postgres
POSTGRES_PASSWORD=1234 POSTGRES_PASSWORD='1234'
# Para el postgres local para generar el script de resultado de migraciones # Para el postgres local para generar el script de resultado de migraciones
PGHOST=localhost PGHOST=localhost
PGUSER=alvar PGUSER=alvar
PGPASSWORD=alvar PGPASSWORD=alvar
PGPORT=5433 PGPORT=5433
# Proxy
CONNECTIONS_URL=https://sim-connections.savefamilygps.net

View File

@@ -8,6 +8,8 @@ RUN corepack enable
COPY ./dist/packages ./packages COPY ./dist/packages ./packages
COPY ./.yarnrc.yml ./ COPY ./.yarnrc.yml ./
COPY ./docs ./docs COPY ./docs ./docs
# Para las migraciones
COPY ./deployment ./deployment
COPY ./package.json ./ COPY ./package.json ./

View File

@@ -1,4 +1,6 @@
#!/bin/sh #!/bin/sh
cd /home cd /home
cd /home/node/app && yarn start cd /home/node/app
yarn migrate
yarn start

View File

@@ -69,7 +69,6 @@ pipeline {
cleanRemote: false, cleanRemote: false,
remoteDirectory: "$APP_REMOTE_PATH", remoteDirectory: "$APP_REMOTE_PATH",
sourceFiles: "deployment/database/**/*", sourceFiles: "deployment/database/**/*",
removePrefix: "deployment",
), ),
sshTransfer( sshTransfer(
cleanRemote: false, cleanRemote: false,

View File

@@ -11,7 +11,7 @@ post {
} }
body:form-urlencoded { body:form-urlencoded {
iccid: 8933201125068886692 iccid: 8935103196306448300
offer: SAVEFAMILY1 offer: SAVEFAMILY1
} }

View File

@@ -11,7 +11,7 @@ post {
} }
body:form-urlencoded { body:form-urlencoded {
iccid: 8933201125068886692 iccid: 8933201125068887054
} }
settings { settings {

View File

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

View File

@@ -0,0 +1,21 @@
meta {
name: ReActivate
type: http
seq: 13
}
post {
url: {{baseurl}}/sim/reActivate
body: formUrlEncoded
auth: inherit
}
body:form-urlencoded {
iccid: 8935103196306448300
~offer: SAVEFAMILY1
}
settings {
encodeUrl: true
timeout: 0
}

View File

@@ -0,0 +1,4 @@
vars {
baseurl: http://sim-connections.savefamilygps.net
}
color: #C77A0F

View File

@@ -0,0 +1,20 @@
meta {
name: test proxy
type: http
seq: 14
}
get {
url: {{baseurl}}/simconnections/alai/select?iccid=1111111111111111111
body: none
auth: inherit
}
params:query {
iccid: 1111111111111111111
}
settings {
encodeUrl: true
timeout: 0
}

View File

@@ -0,0 +1,23 @@
info:
name: Select Page
type: http
seq: 6
http:
method: GET
url: "{{baseurl}}/selectPage"
params:
- name: iccid
value: "8935103196306448300"
type: query
disabled: true
body:
type: json
data: ""
auth: inherit
settings:
encodeUrl: true
timeout: 0
followRedirects: true
maxRedirects: 5

25
docs/sim-nos/Select.yml Normal file
View File

@@ -0,0 +1,25 @@
info:
name: Select
type: http
seq: 5
http:
method: GET
url: "{{baseurl}}/select?iccid=8935103196306448300"
params:
- name: iccid
value: "8935103196306448300"
type: query
body:
type: json
data: |-
{
"iccid": "8933201125068890066"
}
auth: inherit
settings:
encodeUrl: true
timeout: 0
followRedirects: true
maxRedirects: 5

View File

@@ -0,0 +1,7 @@
name: local
color: "#2E8A54"
variables:
- name: baseurl
value: http://localhost:3001
- secret: true
name: token

View File

@@ -0,0 +1,7 @@
name: prod
color: "#CE4F3B"
variables:
- name: baseurl
value: https://nosconnectcenter-api.iot-x.com
- secret: true
name: token

View File

@@ -0,0 +1,10 @@
opencollection: 1.0.0
info:
name: sim-nos
bundled: false
extensions:
bruno:
ignore:
- node_modules
- .git

View File

@@ -0,0 +1,22 @@
info:
name: subscriber actions
type: http
seq: 1
http:
method: GET
url: "{{baseurl}}/subscribers/{{iccid}}/actions"
auth:
type: bearer
token: "{{token}}"
runtime:
variables:
- name: iccid
value: "8935103196306448300"
settings:
encodeUrl: true
timeout: 0
followRedirects: true
maxRedirects: 5

View File

@@ -0,0 +1,22 @@
info:
name: subscriber info
type: http
seq: 2
http:
method: GET
url: "{{baseurl}}/subscribers/{{iccid}}"
auth:
type: bearer
token: "{{token}}"
runtime:
variables:
- name: iccid
value: "8935103196306448300"
settings:
encodeUrl: true
timeout: 0
followRedirects: true
maxRedirects: 5

View File

@@ -0,0 +1,22 @@
info:
name: subscriber products available
type: http
seq: 4
http:
method: GET
url: "{{baseurl}}/subscribers/{{iccid}}/products/available"
auth:
type: bearer
token: "{{token}}"
runtime:
variables:
- name: iccid
value: "8935103196306448300"
settings:
encodeUrl: true
timeout: 0
followRedirects: true
maxRedirects: 5

View File

@@ -0,0 +1,22 @@
info:
name: subscribers
type: http
seq: 3
http:
method: GET
url: "{{baseurl}}/subscribers"
auth:
type: bearer
token: "{{token}}"
runtime:
variables:
- name: iccid
value: "8935103196306448300"
settings:
encodeUrl: true
timeout: 0
followRedirects: true
maxRedirects: 5

View File

@@ -5,13 +5,13 @@ meta {
} }
get { get {
url: {{actionsUrl}}/massActions?massActionId=5192767 url: {{actionsUrl}}/massActions?massActionId=5363116
body: formUrlEncoded body: formUrlEncoded
auth: bearer auth: bearer
} }
params:query { params:query {
massActionId: 5192767 massActionId: 5363116
~identifier.identifierType: ICCID ~identifier.identifierType: ICCID
~identifier.identifiers: 8933201125065160463,8933201125065160422 ~identifier.identifiers: 8933201125065160463,8933201125065160422
} }

View File

@@ -7,11 +7,11 @@
], ],
"scripts": { "scripts": {
"test": "vitest watch", "test": "vitest watch",
"build": "yarn workspaces foreach -A --exclude sim-consumidor-nos run build && cp .env dist/ && yarn setup:runtime", "build": "rm -rf ./dist && yarn workspaces foreach -Api run build && cp .env dist/ && 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", "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 --exclude sim-consumidor-nos run start", "start": "yarn setup:runtime && yarn workspaces foreach -Apiv run start",
"typecheck": "npx tsc --noEmit", "typecheck": "npx tsc --noEmit",
"dev": "yarn workspaces foreach -Apiv --exclude sim-consumidor-nos run dev ", "dev": "yarn workspaces foreach -Apiv --exclude sim-objenious-cron run dev",
"lint": "eslint .", "lint": "eslint .",
"lint:fix": "eslint --fix .", "lint:fix": "eslint --fix .",
"format": "prettier --write .", "format": "prettier --write .",
@@ -19,7 +19,7 @@
"migrate": "yarn db-migrate -e .env -m deployment/database/migrations -t 99.0.0" "migrate": "yarn db-migrate -e .env -m deployment/database/migrations -t 99.0.0"
}, },
"dependencies": { "dependencies": {
"@sf-alvar/db-migrate": "1.0.3", "@sf-alvar/db-migrate": "1.0.6",
"@tsconfig/node22": "^22.0.5", "@tsconfig/node22": "^22.0.5",
"amqp-connection-manager": "^5.0.0", "amqp-connection-manager": "^5.0.0",
"amqplib": "^0.10.9", "amqplib": "^0.10.9",
@@ -28,7 +28,7 @@
"dotenv": "^17.2.3", "dotenv": "^17.2.3",
"express": "^5.2.1", "express": "^5.2.1",
"pg": "^8.18.0", "pg": "^8.18.0",
"typescript": "^5.9.3", "typescript": "^6.0.3",
"uuidv7": "^1.1.0", "uuidv7": "^1.1.0",
"vite": "^7.3.1", "vite": "^7.3.1",
"vite-tsconfig-paths": "^6.0.5" "vite-tsconfig-paths": "^6.0.5"

View File

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

View File

@@ -1,65 +1,178 @@
import { EventBus } from "sim-shared/domain/EventBus.port.js";
import { ConsumeMessage } from "amqplib"; import { ConsumeMessage } from "amqplib";
import { Request, Response } from "express"
import { SimNosUsecases } from "./SimNOS.usecases.js";
import { EventBus } from "sim-shared/domain/EventBus.port.js";
import { Result } from "sim-shared/domain/Result.js";
import { SimEvents } from "sim-shared/domain/SimEvents.js";
import { iccidValidator } from "./httpValidators.js";
export class SimNosController { export class SimNosController {
private eventBus: EventBus;
private activationUseCases: any;
private routes = new Map<string, () => void>([
["activate", async () => { console.log("caso de uso activate") }],
["pause", async () => { console.log("caso de uso pause") }],
["cancel", async () => { console.log("caso de uso cancel") }],
])
constructor( constructor(
eventBus: EventBus private uscases: SimNosUsecases,
private eventBus: EventBus,
) { ) {
this.eventBus = eventBus
// No se si hay un sistema mejor
// convertor en const () => {} para conservar el contexto??
this.recibeMsg = this.recibeMsg.bind(this)
} }
public async recibeMsg(msg: ConsumeMessage | null) { private validateMsg(msg: ConsumeMessage | null) {
if (!this.validateActivationMsg(msg)) { if (msg == undefined) return false;
throw new Error("Error consumiendo el mensaje no es valido") const msgData = this.decodeMsg(msg) as SimEvents.general
if (msgData == undefined || msgData.payload == undefined) throw new Error("Mensaje invalido")
return msgData;
} }
msg = msg! private decodeMsg(msg: ConsumeMessage): object | undefined {
if (msg.content == undefined) {
const msgParsed = JSON.parse(String(msg.content)) console.warn('[Sim.controller] Mensaje vacío');
const msgKey = msg.fields.routingKey.split(".") return undefined;
const accion = msgKey[2]
if (accion == undefined) {
console.error("La routingKey es incorrecta: " + accion)
this.eventBus.nack(msg)
return;
}
if (this.routes.get(accion) == undefined) {
console.error("No hay una ruta definida para la accion")
this.eventBus.nack(msg)
return;
} }
try { try {
this.routes.get(accion)!() // Convertir el Buffer a String (UTF-8)
} catch (err) { const contentJson = JSON.parse(Buffer.from(msg.content).toString('utf8'))
console.log("Error procesando el mensaje") return contentJson;
this.eventBus.nack(msg)
} finally { } catch (error) {
this.eventBus.ack(msg) console.error('Error al decodificar JSON:', error);
console.error(Buffer.from(msg.content).toString(("utf8")))
// Aquí podrías decidir devolver el string crudo o null
return undefined;
} }
} }
/** /**
* TODO: * Metodo duplicado se puede generalizar la a una clase sharedController con las funciones basicas
* - Loguear motivos de la no validacion
*/ */
private validateActivationMsg(msg: ConsumeMessage | null) { private async tryUseCase<T extends any>
if (msg == undefined) return false; (msg: ConsumeMessage, usecase: () => Promise<Result<string, T>>): Promise<Result<string, T>> {
return true; try {
const result = await usecase()
if (result.error == undefined) {
await this.eventBus.ack(msg)
return result
} else {
console.error("Error procesando el caso de uso (NOS)", result.error)
this.eventBus.nack(msg)
return result
}
} catch (e) {
console.error("Error general procesando el caso de uso (NOS)")
this.eventBus.nack(msg)
return {
error: String(e)
}
}
}
public activate() {
return async (msg: ConsumeMessage) => {
console.log("[i] Evento activate ", msg.fields)
const data = this.validateMsg(msg) as SimEvents.activation
const iccid = data.payload.iccid
const correlation_id = data.headers?.message_id
const res = await this.tryUseCase(msg, this.uscases.activate({
iccid: iccid,
correlation_id: correlation_id
}))
return res;
}
}
public suspend() {
return async (msg: ConsumeMessage) => {
console.log("Evento suspend ", msg.fields)
const data = this.validateMsg(msg) as SimEvents.suspend
const iccid = data.payload.iccid
const correlation_id = data.headers?.message_id
const res = await this.tryUseCase(msg, this.uscases.suspend({
iccid: iccid,
correlation_id: correlation_id
}))
return res;
}
}
public terminate() {
return async (msg: ConsumeMessage) => {
console.log("Evento termiante no soportado ", msg.fields)
}
}
public reActivate() {
return async (msg: ConsumeMessage) => {
console.log("Evento reActivate ", msg.fields)
const data = this.validateMsg(msg) as SimEvents.reActivation
const iccid = data.payload.iccid
const correlation_id = data.headers?.message_id
const res = await this.tryUseCase(msg, this.uscases.reactivate({
iccid: iccid,
correlation_id: correlation_id
}))
return res;
}
}
/**
* Select especificamente por REST para evitar pasar por las colas.
* La respuesta es instantanea no se tiene que registrar como operación.
*/
public selectREST() {
return async (req: Request, res: Response) => {
const { query } = req
const body = { iccid: query.iccid as string }
console.log("Evento select", body)
const validateBody = iccidValidator.validate(body);
if (validateBody.error != undefined) {
res.status(402).json(validateBody)
return;
}
const iccid: string | string[] = body.iccid
if (Array.isArray(iccid)) {
// TODO: Automatizar la paginacion
//const usecaseRes = this.uscases.selectMany({ iccid })
} else {
const usecaseRes = await this.uscases.selectOne({ iccid })
if (usecaseRes.error != undefined) {
res.status(500).json(usecaseRes)
return;
} else {
res.send(usecaseRes.data)
return;
}
}
res.status(200).json(validateBody)
}
}
public selectPageREST() {
return async (req: Request, res: Response) => {
const { offset, limit, filter, orderBy } = req.query
const params = {
offset: (offset != undefined) ? Number(offset) : undefined,
limit: (limit != undefined) ? Number(limit) : undefined,
filter: (filter != undefined) ? String(filter) : undefined,
orderBy: (orderBy != undefined) ? String(orderBy) : undefined
}
const usecaseRes = await this.uscases.selectPage(params)
if (usecaseRes.error != undefined) {
res.status(500).json(usecaseRes)
return;
} else {
res.status(200).send(usecaseRes.data)
return;
}
}
} }
} }

View File

@@ -0,0 +1,79 @@
/**
* Dirige cada mensaje dependiendo de el tipo de acción que contenga
* Podría hacerse con varias colas, pero así se controla mejor que
* las operaciones se hagan de 1 en 1.
*/
import { ConsumeMessage } from "amqplib";
import { SimNosController } from "./SimNOS.controller.js";
import { EventBus } from "sim-shared/domain/EventBus.port.js";
import { Result } from "sim-shared/domain/Result.js";
type FuncType = ((m: ConsumeMessage) => Promise<Result<string, any>>)
export class SimNosRouter {
private readonly routes: Map<string, FuncType>;
constructor(
private readonly simController: SimNosController,
private readonly eventBus: EventBus
) {
this.routes = new Map<string, FuncType>([
//["select", undefined],
["activate", this.simController.activate()],
["pause", this.simController.suspend()],
["reactivate", this.simController.reActivate()],
//["cancel", this.simController.terminate()],
//["preActivate", this.simController.preActivate()]
]);
}
/**
* Enruta el mensaje a la acción correspondiente basándose en la routing key
* TODO: No estoy seguro que deba meter el nack aqui
* - De moemento el ack-nack se gestiona en los controller, por si acaso hay casos
* limite en
*/
public route = async (msg: ConsumeMessage | null): Promise<void> => {
if (!msg) {
console.error("[Router] Mensaje vacío");
return;
}
const action = this.extractAction(msg);
if (!action) {
console.error("[Router] La routing key no tiene una acción definida", msg.fields.routingKey);
this.eventBus.nack(msg)
return;
}
const handler = this.routes.get(action);
if (!handler) {
console.error(`[Router] La acción '${action}' no tiene un controlador asociado`);
this.eventBus.nack(msg)
return;
}
try {
console.log("[Router] Ejecutando operación:", action);
// El controlador devuelve una función (thunk) que debe ser ejecutada
const executeParams = handler(msg);
if (typeof executeParams === "function") {
const res = await executeParams;
}
} catch (error) {
console.error(`[Router] Error al ejecutar la operación '${action}':`, error);
this.eventBus.nack(msg)
}
};
private extractAction(msg: ConsumeMessage): string | undefined {
// Se asume que la acción está en la tercera posición: domain.compañia.accion
return msg.fields.routingKey.split(".")[2];
}
}

View File

@@ -0,0 +1,156 @@
/**
* Documentación de referencia:
* https://pelion-help.iot-x.com/nos/en-US/Content/API/APIReference/API%20Reference.htm?tocpath=_____7
*
* En nos el correlation_id ya va a ser obligatorio en todos los mensajes
*
* TODO:
* - Control de errores más preciso
*
*/
import { NosHttpClient } from "#infrastructure/NosHttpClient.js";
import { NosRepository } from "#infrastructure/NosRepository.js";
import { ErrorOrderDTO, FinishOrderDTO, UpdateOrderDTO } from "sim-shared/domain/Order.js";
import { Result } from "sim-shared/domain/Result.js";
import { OrderRepository } from "sim-shared/infrastructure/OrderRepository.js";
export class SimNosUsecases {
constructor(
private httpClient: NosHttpClient,
private nosRepository: NosRepository,
private orderRepository: OrderRepository
) {
}
private async setRunning(correlation_id: string) {
// En NOS el updateOrder se hace con el correlation_id que viene en la cabecera del
// mensaje consumido
const updateData: UpdateOrderDTO = {
new_status: "running",
correlation_id: correlation_id
}
const order = await this.orderRepository.updateOrder(updateData)
return order
}
private async setFinished(correlation_id: string) {
// En NOS el updateOrder se hace con el correlation_id que viene en la cabecera del
// mensaje consumido
const updateData: FinishOrderDTO = {
correlation_id: correlation_id
}
const order = await this.orderRepository.finishOrder(updateData)
return order
}
private async setFailed(correlation_id: string, reason: string, detail?: string) {
// En NOS el updateOrder se hace con el correlation_id que viene en la cabecera del
// mensaje consumido
const updateData: ErrorOrderDTO = {
status: "failed",
correlation_id: correlation_id,
reason: reason,
error: reason,
stackTrace: detail
}
console.log("SET FAILED DATA:", updateData)
const order = await this.orderRepository.errorOrder(updateData)
console.log("SET FAILED RES:", order)
return order
}
public usecaseTemplate<T, R>(
func: (_: T) => Promise<Result<string, R>>,
args: T,
correlation_id?: string | undefined
) {
return async () => {
// Operacion pending -> running
if (correlation_id != undefined)
this.setRunning(correlation_id)
.then()
.catch(e => console.error("Error actualizando el order", e))
try {
const res = await func(args)
if (res.error != undefined) {
console.log("Error peticion: ", res, correlation_id)
if (correlation_id != undefined)
this.setFailed(correlation_id, res.error)
.then(e => console.log("failed", e))
.catch(e => console.error(e))
return res;
} else {
if (correlation_id != undefined)
this.setFinished(correlation_id).then()
return res;
}
} catch (e) {
if (correlation_id != undefined)
this.setFailed(correlation_id, "Error general de operacion de SIM (NOS) ", String(e)).then()
return {
error: "Error general de operacion de SIM (NOS) " + String(e)
}
}
}
}
public activate(args: {
iccid: string,
correlation_id?: string
}) {
return this.usecaseTemplate(
(args) => this.nosRepository.activateSim(args), args.iccid, args.correlation_id)
}
public suspend(args: {
iccid: string,
correlation_id?: string
}) {
return this.usecaseTemplate(
(args) => this.nosRepository.bar(args), args.iccid, args.correlation_id)
}
public reactivate(args: {
iccid: string,
correlation_id?: string
}) {
return this.usecaseTemplate(
(args) => this.nosRepository.unbar(args), args.iccid, args.correlation_id)
}
public terminate(args: { iccid: string }) {
throw new Error("No hay termination para NOS")
}
/* Importante: Las operaciones de lectua no dejan registro en orders */
public async selectOne(args: {
iccid: string
}) {
const res = await this.nosRepository.getLineInfo(args.iccid)
return res
}
public async selectPage(args: {
offset?: number,
limit?: number,
filter?: string,
orderBy?: string
}) {
const res = await this.nosRepository.getLinePage(args)
return res
}
/**
public selectMany(args: {
iccid: string[]
}) {
return {}
}
*/
}

View File

@@ -0,0 +1,39 @@
import { BodyValidator, Validator } from "sim-shared/aplication/BodyValidator.js";
const iccidNotNull = <Validator<{ iccid: unknown }>>{
field: "iccid",
errorMsg: "El iccid no está definido",
validationFunc: (a: { iccid: unknown }) => {
return (a.iccid != null && a.iccid != undefined)
}
}
const iccidValueOrArray = <Validator<{ iccid: unknown }>>{
field: "iccid",
errorMsg: "El iccid debe de ser un único valor o una lista",
validationFunc: (a: { iccid: unknown }) => {
return (typeof a.iccid == "string" || Array.isArray(a.iccid))
}
}
const iccidLongitudValidator = <Validator<{ iccid: string | string[] }>>{
field: "iccid",
errorMsg: "La longitud del iccid/s es incorrecta debera ser de 19 caracteres",
validationFunc: (a: { iccid: string | string[] }) => {
if (Array.isArray(a.iccid)) {
const res = (a.iccid as string[]).filter(e => e.length != 19)
if (res.length > 0) return false;
} else {
return (a.iccid as string).length == 19
}
},
}
export const iccidValidator = new BodyValidator<{ iccid: string | string[] }>(
[
iccidNotNull,
iccidValueOrArray,
iccidLongitudValidator,
]
)

View File

@@ -1,5 +1,16 @@
import { loadEnvFile } from "node:process"; import { loadEnvFile } from "node:process";
loadEnvFile("../../.env") import path from "node:path";
try {
loadEnvFile(path.join("./.env")) // base
} catch (e) {
console.error("Error cargando el .env desde ./.env")
}
try {
loadEnvFile(path.join("../../.env")) // Global
} catch (e) {
console.error("Error cargando el .env desde ../../.env")
}
export const env = { export const env = {
ENVIRONMENT: process.env.ENVIORMENT, ENVIRONMENT: process.env.ENVIORMENT,
@@ -18,5 +29,12 @@ export const env = {
RABBITMQ_SECURE: process.env.RABBITMQ_SECURE, RABBITMQ_SECURE: process.env.RABBITMQ_SECURE,
RABBITMQ_RETRY_INTERVAL: process.env.RABBITMQ_INTERVAL, RABBITMQ_RETRY_INTERVAL: process.env.RABBITMQ_INTERVAL,
RABBITMQ_VHOST: String(process.env.RABBITMQ_VHOST), RABBITMQ_VHOST: String(process.env.RABBITMQ_VHOST),
APP_PORT: Number(process.env.APP_PORT),
APP_HOST: String(process.env.APP_HOST),
// ESPECIFICO NOS
NOS_BASE_URL: String(process.env.NOS_BASE_URL),
NOS_ACCESS_TOKEN: String(process.env.NOS_ACCESS_TOKEN)
}; };

View File

@@ -0,0 +1,72 @@
import { RabbitMQEventBus, RMQConnectionParams } from "sim-shared/infrastructure/RabbitMQEventBus.js"
import { Channel } from "amqp-connection-manager"
import { env } from "./env/env.js"
const rmqUser = env.RABBITMQ_USER
const rmqPass = env.RABBITMQ_PASSWORD
const rmqHost = env.RABBITMQ_HOST
const rmqPort = Number(env.RABBITMQ_PORT)
const rmqSecure = false
const rmqVhost = env.RABBITMQ_VHOST
export const rmqConnOptions = <RMQConnectionParams>{
username: rmqUser,
password: rmqPass,
vhost: rmqVhost,
hostname: rmqHost,
port: rmqPort,
secure: rmqSecure,
}
const QUEUES = {
NOS: "sim.nos",
NOSDLX: "sim.nos.dlx",
NOSDEL: "sim.nos.delayed",
}
const EXCHANGES = {
MAIN: "sim.exchange",
DLX: "sim.ex.nos.dlx",
DEL: "sim.ex.nos.delayed"
}
export const rabbitmqEventBus = new RabbitMQEventBus({
connectionParams: rmqConnOptions,
buildStructure: buildQueues,
maxRetry: 2,
delayedExchange: EXCHANGES.DEL,
dlxExchange: EXCHANGES.DLX
})
async function buildQueues(channel: Channel) {
const DELAY = 10 * 1000
const BASE_NOS_KEY = "sim.nos.#"
await channel.assertExchange(EXCHANGES.DEL, "topic")
await channel.assertExchange(EXCHANGES.DLX, "topic")
await channel.assertExchange(EXCHANGES.MAIN, "topic")
await channel.assertQueue(QUEUES.NOS)
await channel.assertQueue(QUEUES.NOSDLX)
await channel.assertQueue(QUEUES.NOSDEL, {
durable: true,
arguments: {
'x-message-ttl': DELAY,
'x-dead-letter-exchange': EXCHANGES.MAIN,
}
})
// Cola dead-letter
await channel.bindQueue(QUEUES.NOSDLX, EXCHANGES.DLX, "sim.nos.#")
// Cola delay
await channel.bindQueue(QUEUES.NOSDEL, EXCHANGES.DEL, BASE_NOS_KEY)
// Cola nos -> main exchange
await channel.bindQueue(QUEUES.NOS, EXCHANGES.MAIN, BASE_NOS_KEY)
}
export async function startRMQClient() {
await rabbitmqEventBus.connect()
return rabbitmqEventBus
}

View File

@@ -1,39 +0,0 @@
import { RabbitMQEventBus, RMQConnectionParams } from "sim-shared/infrastructure/RabbitMQEventBus.js"
import { env } from "./env"
const rmqUser = env.RABBITMQ_USER
const rmqPass = env.RABBITMQ_PASSWORD
const rmqHost = env.RABBITMQ_HOST
const rmqPort = Number(env.RABBITMQ_PORT)
const rmqSecure = false
const rmqVhost = env.RABBITMQ_VHOST
export const rmqConnOptions = <RMQConnectionParams>{
username: rmqUser,
password: rmqPass,
vhost: rmqVhost,
hostname: rmqHost,
port: rmqPort,
secure: rmqSecure,
}
export const rabbitmqEventBus = new RabbitMQEventBus({
connectionParams: rmqConnOptions
})
export async function startRMQClient() {
await rabbitmqEventBus.connect().catch(async e => {
console.error("Error en la conexion RMQ")
await rabbitmqEventBus.connect()
})
// Bindings especificos, deberia meterlos en la clase
try {
await rabbitmqEventBus.channel?.assertQueue("sim.nos")
} catch {
console.log("[i] Cola de sims de nos creada")
await rabbitmqEventBus.channel?.bindQueue("sim.nos", "sim.exchange", "sim.nos.*")
}
return rabbitmqEventBus
}

View File

@@ -0,0 +1,18 @@
import { Pool, QueryResult } from 'pg';
import { PgClient } from 'sim-shared/infrastructure/PgClient.js'
import { env } from './env/env.js';
// Configuracion de la conexion a la BDD, deberia ser la
// Misma para todos los servicios pero hasta que se unifique todo
// se hace una por servicio.
export const pgPool = new Pool({
user: env.POSTGRES_USER,
host: env.POSTGRES_HOST,
database: env.POSTGRES_DATABASE,
password: env.POSTGRES_PASSWORD,
port: Number(env.POSTGRES_PORT) || 5433,
});
export const pgClient = new PgClient({
pool: pgPool
})

View File

@@ -0,0 +1,131 @@
export namespace NosApi {
export type ActivationData = {
/**
The unique physical subscriber identifier:
Cellular - the ICCID
Non - IP - the EUI
Satellite - the IMEI
*/
physicalId: string,
/**
example: 447000000001
The unique network subscriber identifier:
Cellular subscriber - the MSISDN
Non - IP subscriber - the Device EUI
Satellite subscriber - the Subscription ID
*/
subscriberId: string,
/**
example: 9999
If the subscriber uses Circuit Switched Data(CSD), this field displays its data number.If the subscriber does not use CSD, this field is null.
*/
dataNumber: string
/**
example: 172.0.0.1
The subscriber IP address.
*/
ip: string
/**
example: 234150000000001
The subscriber IMSI.
*/
imsi: string
}
type OkResponse<T> = {
error?: undefined | null,
content: T
}
type ErrorResponse<E = GeneralError> = {
content?: undefined | null,
error: E
}
type GeneralError = {
children?: string[],
code: string,
message: string
}
export type ActivateResponseOK = OkResponse<ActivationData>
export type ActivateResponseError = ErrorResponse
export type ActivateResponse = ActivateResponseOK | ActivateResponseError
export type LineDataResponseOK = OkResponse<LineData>
export type LineDataResponseError = ErrorResponse
export type LineDataResponse = LineDataResponseOK | LineDataResponseError
export type PageDataResponseOk = OkResponse<LineData[]>
export type PageDataResponseError = OkResponse<LineData[]>
export type PageResponse = PageDataResponseOk | PageDataResponseError
export type LineData = {
physicalId: string
subscriberId: string
imsi: string
nickname: string
operatorCode: string
tariffName: string
lineRental: number
contractLength: number
isBarred: boolean
isActive: boolean
terminateDate: any
groupId: number
subscriberType: any
connectionDate: string
expiryDate: string
networkState: NetworkState
billingState: BillingState
operatorName: string
imei: string
dataUsage?: number
eid?: string
smdpProvider?: string
related?: {
parent: RelatedItem,
profiles: RelatedItem[]
}
}
type RelatedItem = {
physicalId: string
isEnabledOnParent: boolean
}
export type NetworkState = {
currentStateId: number
currentState: string
isTransferring: boolean
lastTransferred: number
isOnline: boolean
lastSeenOnline: number
}
export type BillingState = {
currentStateId: number
currentState: string
}
export type BarData = {
product: string
description: string
enabled: boolean
}
export type BarResponseOk = OkResponse<BarData>
export type BarResponseError = ErrorResponse
export type BarResponse = BarResponseOk | BarResponseError
}

View File

@@ -1,14 +1,74 @@
import express from "express"
import { startRMQClient } from "#config/eventBusConfig" import cors from 'cors';
import { SimNosRouter } from "./aplication/SimNOS.router.js"
import { SimNosController } from "./aplication/SimNOS.controller.js" import { SimNosController } from "./aplication/SimNOS.controller.js"
import { SimNosUsecases } from "./aplication/SimNOS.usecases.js"
import { NosHttpClient } from "./infrastructure/NosHttpClient.js"
import { env } from "#config/env/env.js"
import { NosRepository } from "./infrastructure/NosRepository.js"
import { OrderRepository } from "sim-shared/infrastructure/OrderRepository.js";
import { pgClient } from "#config/postgreConfig.js";
import { startRMQClient } from "#config/eventBus.config.js";
const RMQ_QUEUE = "sim.nos"
const NOS_BASE_URL = env.NOS_BASE_URL
const PORT = env.APP_PORT
const HOSTNAME = env.APP_HOST
async function startWorker() { async function startWorker() {
// Instancia de dependencias
const rmqClient = await startRMQClient() const rmqClient = await startRMQClient()
const nosHttpClient = new NosHttpClient(
NOS_BASE_URL
)
const nosRepository = new NosRepository(
nosHttpClient
)
const orderRepository = new OrderRepository(
pgClient
)
const simUsecases = new SimNosUsecases(
nosHttpClient,
nosRepository,
orderRepository
)
const simController = new SimNosController( const simController = new SimNosController(
simUsecases,
rmqClient rmqClient
) )
rmqClient.consume("sim.nos", simController.recibeMsg) const simRouter = new SimNosRouter(
simController,
rmqClient
)
// RMQ
rmqClient.consume(RMQ_QUEUE, simRouter.route)
.then(() => console.log("Cliente rmq creado con exito"))
.catch(e => console.error("Error conectando con RABBITMQ", e))
// Express
const app = express()
app.use(cors());
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
app.get("/select", simController.selectREST())
app.get("/selectPage", simController.selectPageREST())
app.listen(PORT, HOSTNAME, (e) => {
if (e == undefined) {
console.log("[o] Servidor iniciado en el puerto %d", PORT)
} else {
console.error("Error express ", e)
}
})
} }
startWorker() startWorker()

View File

@@ -0,0 +1,41 @@
import axios, { AxiosInstance } from "axios";
import { env } from "#config/env/env.js"
export class NosHttpClient {
public client: AxiosInstance;
constructor(
private baseURL: string,
//private jwtManager: JWTProvider<any>
) {
this.client = axios.create({
baseURL: baseURL
})
// Interceptor para los headers fijos
this.client.interceptors.request.use(
async (config) => {
// Configuracion especifica de NOS (El token simepre es el mismo?)
const token = env.NOS_ACCESS_TOKEN;
config.headers.Authorization = `Bearer ${token}`
config.headers.set("content-type", "application/json")
return config
},
(error) => Promise.reject(error)
)
}
get post() {
return this.client.post
}
get patch() {
return this.client.patch
}
get get() {
return this.client.get
}
}

View File

@@ -0,0 +1,177 @@
import { Result } from "sim-shared/domain/Result.js";
import { NosHttpClient } from "./NosHttpClient.js";
import { NosApi } from "#domain/NosAPI.js";
import axios, { AxiosError, AxiosResponse } from "axios";
export class NosRepository {
constructor(
private httpClient: NosHttpClient
) {
}
/**
* E => Tipo de error
* T => Tipo de dato para cod 200
*
* TODO:
* - Mejor gestion de los errores
* - E no se aplica todavia por no hacer la transformacion del error
*/
private async manageNosRequest<E, T>(promise: Promise<AxiosResponse<T>>): Promise<Result<string, T>> {
try {
const res = await promise
return {
data: res.data
}
} catch (e) {
if (axios.isAxiosError(e)) {
const error = e as AxiosError
return {
error: error.code + " : " + String(error.response?.statusText)
}
} else {
return {
error: String(e)
}
}
}
}
public async getLineInfo(iccid: string): Promise<Result<string, NosApi.LineData>> {
const PATH = "/subscribers/" + iccid
console.log("PAth", PATH)
const lineRequest = this.httpClient.get<NosApi.LineDataResponseOK>(PATH)
const lineResponse = await this.manageNosRequest<string, NosApi.LineDataResponseOK>(lineRequest)
if (lineResponse.error != undefined) {
return lineResponse
} else {
return {
data: lineResponse.data.content
}
}
}
/**
* El metodo de NOS de paginar las lineas
* maximo por pagina 100, default 25
* no devuelve el offset ni el numero de elementos restantes
* hay que llevar la cuenta
*/
public async getLinePage(args: {
limit?: number,
offset?: number,
filter?: string,
orderBy?: string
}): Promise<Result<string, any>> {
const PATH = "/subscribers"
const LIMIT = 100
const options = {
limit: args.limit ?? LIMIT,
offset: args.offset ?? 0,
filter: args.filter,
orderBy: args.orderBy
}
const pageRequest = this.httpClient.get(PATH, {
params: options
})
const pageResponse = await this.manageNosRequest<string, any>(pageRequest)
if (pageResponse.error != undefined) {
return pageResponse
} else {
return {
data: pageResponse.data.content
}
}
}
public async getLinesInfo(iccid: string[]) /*Promise<Result<string, NosApi.LineData>>*/ {
throw new Error("NOS no permite buscar iccid en bulk, se puede hacer un apaño pero está en proceso")
const PATH = "/subscribers"
const LIMIT = 100
const steps = Math.ceil(iccid.length / LIMIT)
const options = {
limit: LIMIT,
offset: 0,
}
const req = this.httpClient.post<NosApi.LineDataResponseOK>(PATH)
const resp = await this.manageNosRequest<string, NosApi.LineDataResponseOK>(req)
if (resp.error != undefined) {
return resp
} else {
return {
//@ts-expect-error
data: resp.data.content
}
}
}
public async activateSim(iccid: string): Promise<Result<string, NosApi.ActivationData>> {
const PATH = '/provisioning'
const PRODUCT_ID = 1330 // No se que es, preguntar a Ivan
const data = {
productSetId: PRODUCT_ID
}
const req = this.httpClient.post<NosApi.ActivateResponseOK>(PATH, data)
const resp = await this.manageNosRequest<string, NosApi.ActivateResponseOK>(req)
if (resp.error != undefined) {
return resp
} else {
return {
data: resp.data.content
}
}
}
/**
* "A bar is a service provisioning action that results in a subscriber being blocked from accessing an operator's network. The bar remains in place until the operator is sent an unbar request."
* Se entiende que un "bar" es una suspension temporal
*/
public async bar(iccid: string) {
const PATH = `/subscribers/${iccid}/products`
const data = {
product: "BAR DN TOTAL",
action: "enable"
}
const req = this.httpClient.patch<NosApi.BarResponseOk>(PATH, data)
const resp = await this.manageNosRequest<string, NosApi.BarResponseOk>(req)
if (resp.error != undefined) {
return resp
} else {
return {
data: resp.data.content
}
}
}
public async unbar(iccid: string) {
const PATH = `/subscribers/${iccid}/products`
const data = {
product: "BAR DN TOTAL",
action: "disable"
}
const req = this.httpClient.patch<NosApi.BarResponseOk>(PATH, data)
const resp = await this.manageNosRequest<string, NosApi.BarResponseOk>(req)
if (resp.error != undefined) {
return resp
} else {
return {
data: resp.data.content
}
}
}
}

View File

@@ -1,17 +1,8 @@
{ {
"name": "sim-consumidor-nos", "name": "sim-consumidor-nos",
"version": "1.0.0", "type": "module",
"description": "consumidor generico de eventos de NOS", "description": "consumidor generico de eventos de NOS",
"main": "index.ts", "main": "index.ts",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"build": "yarn tsc --project tsconfig.json && yarn tsc-alias && cp package.json ../../dist/packages/sim-consumidor-nos/",
"esbuild": "esbuild index.ts --platform=node",
"start": "node ../../dist/packages/sim-consumidor-nos/index.js"
},
"author": "",
"license": "ISC",
"packageManager": "yarn@4.12.0",
"imports": { "imports": {
"#config/*.js": { "#config/*.js": {
"types": "./config/*.ts", "types": "./config/*.ts",
@@ -21,13 +12,13 @@
"types": "./config/*.ts", "types": "./config/*.ts",
"default": "./config/*.js" "default": "./config/*.js"
}, },
"#adapters/*.js": { "#infrastructure/*.js": {
"types": "./adapters/*.ts", "types": "./infrastructure/*.ts",
"default": "./adapters/*.js" "default": "./infrastructure/*.js"
}, },
"#adapters/*": { "#infrastructure/*": {
"types": "./adapters/*.ts", "types": "./infrastructure/*.ts",
"default": "./adapters/*.js" "default": "./infrastructure/*.js"
}, },
"#domain/*.js": { "#domain/*.js": {
"types": "./domain/*.ts", "types": "./domain/*.ts",
@@ -37,29 +28,32 @@
"types": "./domain/*.ts", "types": "./domain/*.ts",
"default": "./domain/*.js" "default": "./domain/*.js"
}, },
"#ports/*.js": { "#aplication/*.js": {
"types": "./ports/*.ts", "types": "./aplication/*.ts",
"default": "./ports/*.js" "default": "./aplication/*.js"
}, },
"#ports/*": { "#aplication/*": {
"types": "./ports/*.ts", "types": "./aplication/*.ts",
"default": "./ports/*.js" "default": "./aplication/*.js"
},
"#tests/*.js": {
"types": "./__tests__/*.ts",
"default": "./__tests__/*.js"
},
"#tests/*": {
"types": "./__tests__/*.ts",
"default": "./__tests__/*.js"
} }
}, },
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"build": "yarn tsc --project tsconfig.json && yarn tsc-alias && cp package.json ../../dist/packages/sim-consumidor-nos/",
"esbuild": "esbuild index.ts --platform=node",
"start": "node ../../dist/packages/sim-consumidor-nos/index.js",
"dev": "tsx watch index.ts"
},
"author": "",
"license": "ISC",
"packageManager": "yarn@4.12.0",
"dependencies": { "dependencies": {
"@tsconfig/node22": "*", "@tsconfig/node22": "*",
"amqplib": "^0.10.9", "amqplib": "^0.10.9",
"cors": "*", "cors": "*",
"dotenv": "*", "dotenv": "*",
"express": "*", "express": "*",
"sim-shared": "sim-shared:*",
"typescript": "*" "typescript": "*"
}, },
"devDependencies": { "devDependencies": {
@@ -70,6 +64,7 @@
"@types/supertest": "*", "@types/supertest": "*",
"prettier": "*", "prettier": "*",
"supertest": "*", "supertest": "*",
"tsc-alias": "^1.8.16",
"tsx": "*", "tsx": "*",
"vitest": "*" "vitest": "*"
} }

View File

@@ -0,0 +1,11 @@
# NOS
## Particularidades de las operaciones de NOS
- Documentación de la API: [DOC](https://pelion-help.iot-x.com/nos/en/Content/API/APIReference/API%20Reference.htm?tocpath=_____7)
- No se necesita la pre-activación de las SIM.
- La suspensión y reactivación se llama "bar" y "unbar".
- El token de Authentication dura exactamente 1 año, solo se puede refrescar
desde la web.
- En la documentación la URL de la API es <https://nos-api.iot-x.com> pero la
de producción es <https://nosconnectcenter-api.iot-x.com>.

View File

@@ -0,0 +1,5 @@
## ENV PARA DATOS DE TEST - shared nunca se lanza en produccion
NOTIFICATION_URL="https://sf-sim-activation.savefamilygps.net/send-activation-mail"
# NOTIFICATION_URL="localhost"
SIM_ACTIVATION_API_KEY=9e48c4ac-1ab0-4397-b3f3-6c239200dfe6

View File

@@ -2,14 +2,38 @@
"extends": "../../tsconfig.json", "extends": "../../tsconfig.json",
"compilerOptions": { "compilerOptions": {
"outDir": "../../dist", "outDir": "../../dist",
"baseUrl": ".", "rootDir": "../../",
"paths": {
"#config/*": [
"./config/*"
],
"#infrastructure/*": [
"./infrastructure/*"
],
"#domain/*": [
"./domain/*"
],
"#aplication/*": [
"./aplication/*"
],
"config/*": [
"./config/*"
],
"infrastructure/*": [
"./infrastructure/*"
],
"domain/*": [
"./domain/*"
]
}
}, },
"exclude": [ "exclude": [
"node_modules" "node_modules"
], ],
"include": [ "include": [
"**/*.ts", "**/*.ts",
"src/**/*.d.ts" "**/*.d.ts",
"../../packages/sim-shared/**/*.ts"
], ],
"files": [ "files": [
"index.ts" "index.ts"

View File

@@ -21,7 +21,6 @@ export class SimController {
) { ) {
this.eventBus = eventBus this.eventBus = eventBus
this.useCases = useCases this.useCases = useCases
} }
private decodeMsg(msg: ConsumeMessage): object | undefined { private decodeMsg(msg: ConsumeMessage): object | undefined {
@@ -109,7 +108,7 @@ export class SimController {
return async (msg: ConsumeMessage) => { return async (msg: ConsumeMessage) => {
let msgData; let msgData;
try { try {
msgData = this.validateMsg(msg) as SimEvents.pause msgData = this.validateMsg(msg) as SimEvents.suspend
} catch (e) { } catch (e) {
throw new Error("Error de preactivacion consumiendo el mensaje no es valido" + String(e)) throw new Error("Error de preactivacion consumiendo el mensaje no es valido" + String(e))
} }
@@ -136,7 +135,7 @@ export class SimController {
return async (msg: ConsumeMessage) => { return async (msg: ConsumeMessage) => {
let msgData; let msgData;
try { try {
msgData = this.validateMsg(msg) as SimEvents.pause msgData = this.validateMsg(msg) as SimEvents.suspend
} catch (e) { } catch (e) {
throw new Error("Error de reactivacion consumiendo el mensaje no es valido" + String(e)) throw new Error("Error de reactivacion consumiendo el mensaje no es valido" + String(e))
} }
@@ -165,7 +164,7 @@ export class SimController {
return async (msg: ConsumeMessage) => { return async (msg: ConsumeMessage) => {
let msgData; let msgData;
try { try {
msgData = this.validateMsg(msg) as SimEvents.pause msgData = this.validateMsg(msg) as SimEvents.suspend
} catch (e) { } catch (e) {
throw new Error("Error de suspension consumiendo el mensaje no es valido" + String(e)) throw new Error("Error de suspension consumiendo el mensaje no es valido" + String(e))
} }
@@ -195,7 +194,7 @@ export class SimController {
return async (msg: ConsumeMessage) => { return async (msg: ConsumeMessage) => {
let msgData; let msgData;
try { try {
msgData = this.validateMsg(msg) as SimEvents.pause msgData = this.validateMsg(msg) as SimEvents.suspend
} catch (e) { } catch (e) {
throw new Error("Error consumiendo el mensaje no es valido" + String(e)) throw new Error("Error consumiendo el mensaje no es valido" + String(e))
} }

View File

@@ -19,7 +19,7 @@ export class SimRouter {
["activate", this.simController.activate()], ["activate", this.simController.activate()],
["pause", this.simController.suspend()], ["pause", this.simController.suspend()],
["cancel", this.simController.terminate()], ["cancel", this.simController.terminate()],
["reActivate", this.simController.reActivate()], ["reactivate", this.simController.reActivate()],
["preActivate", this.simController.preActivate()] ["preActivate", this.simController.preActivate()]
]); ]);
} }

View File

@@ -75,13 +75,14 @@ export class SimUseCases {
operation: args.operation, operation: args.operation,
iccids: String(args.iccid), iccids: String(args.iccid),
status: "noMassID", status: "noMassID",
request_id: response.data.requestId request_id: response.data.requestId,
correlation_id: args.correlation_id
} }
// TODO: Esto tiene poco sentido si la operacion ya se // TODO: Esto tiene poco sentido si la operacion ya se
// tenia que haber creado en el generador // tenia que haber creado en el generador
this.logOperation(operation) this.logOperation(operation)
.then().catch(e => console.error(e)) .then().catch(e => console.error("Error login operation", e))
if (args.correlation_id != undefined) { if (args.correlation_id != undefined) {
this.orderRepository.updateOrder({ this.orderRepository.updateOrder({
@@ -115,6 +116,15 @@ export class SimUseCases {
public activate(activationData: ActivationData): () => Promise<Result<string, boolean>> { public activate(activationData: ActivationData): () => Promise<Result<string, boolean>> {
const OPERATION_URL = "/actions/activateLine" const OPERATION_URL = "/actions/activateLine"
return async () => { return async () => {
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;
}
const req = this.httpClient.client.post(OPERATION_URL, { const req = this.httpClient.client.post(OPERATION_URL, {
dueDate: activationData.dueDate, dueDate: activationData.dueDate,
identifier: activationData.identifier, identifier: activationData.identifier,
@@ -198,16 +208,29 @@ export class SimUseCases {
} }
} }
public reActivate(pauseData: ActionData): () => Promise<Result<string, boolean>> { public reActivate(reactivateData: ActionData): () => Promise<Result<string, boolean>> {
const OPERATION_URL = "/actions/reactivateLine" const OPERATION_URL = "/actions/reactivateLine"
return async () => { return async () => {
const req = this.httpClient.client.post(OPERATION_URL, { const req = this.httpClient.client.post(OPERATION_URL, {
...pauseData ...reactivateData
}) })
try { try {
const response = await req const response = await req
// Creacion de la operacion inicial, antes de tener los datos
const operation: ObjeniousOperation = {
operation: "reactivate",
iccids: reactivateData.identifier.identifiers[0],
status: "noMassID",
request_id: response.data.requestId,
correlation_id: reactivateData.correlation_id
}
// TODO: Esto tiene poco sentido si la operacion ya se
// tenia que haber creado en el generador
this.logOperation(operation)
.then().catch(e => console.error("Error login operation", e))
if (response.status == 200) { if (response.status == 200) {
console.log("[o] Sim solicitud de reactivacion ", response.data) console.log("[o] Sim solicitud de reactivacion ", response.data)
return <Result<string, boolean>>{ return <Result<string, boolean>>{
@@ -223,7 +246,7 @@ export class SimUseCases {
} catch (error) { } catch (error) {
console.error("[x] Error reactivacion", (error as AxiosError).response?.status) console.error("[x] Error reactivacion", (error as AxiosError).response?.status)
return <Result<string, boolean>>{ return <Result<string, boolean>>{
error: "Error reactivando la sim" + pauseData.identifier, error: "Error reactivando la sim" + reactivateData.identifier,
data: undefined data: undefined
} }
} }
@@ -276,6 +299,17 @@ export class SimUseCases {
const correlation_id = suspendData.correlation_id const correlation_id = suspendData.correlation_id
const iccid = suspendData.identifier.identifiers const iccid = suspendData.identifier.identifiers
const operation: ObjeniousOperation = {
operation: "suspend",
iccids: iccid[0],
status: "running",
correlation_id: correlation_id
}
// No se registra hasta que no pase por la tabla de pausas
// this.logOperation(operation)
// .then().catch(e => console.error("Error login operation", e))
const fail = (error: string) => { const fail = (error: string) => {
console.error("[Sim.usecases]", error) console.error("[Sim.usecases]", error)
if (correlation_id != undefined) { if (correlation_id != undefined) {
@@ -286,6 +320,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; let activationDate;
try { try {
activationDate = await this.findActivationDate(suspendData) activationDate = await this.findActivationDate(suspendData)
@@ -332,10 +374,11 @@ export class SimUseCases {
public stage_terminate(terminateData: ActionData): () => Promise<Result<string, boolean>> { public stage_terminate(terminateData: ActionData): () => Promise<Result<string, boolean>> {
return async (): Promise<Result<string, boolean>> => { return async (): Promise<Result<string, boolean>> => {
const correlation_id = terminateData.correlation_id const correlation_id = terminateData.correlation_id
const iccid = terminateData.identifier.identifiers[0]
const activationDate = await this.findActivationDate(terminateData) const activationDate = await this.findActivationDate(terminateData)
const newTask: CreatePauseCancelTaskDTO = { const newTask: CreatePauseCancelTaskDTO = {
iccid: terminateData.identifier.identifiers[0], iccid: iccid,
activation_date: activationDate, activation_date: activationDate,
next_check: undefined, // Que se haga instantaneamente al ser la primera next_check: undefined, // Que se haga instantaneamente al ser la primera
operation_type: "terminate", operation_type: "terminate",
@@ -344,6 +387,17 @@ export class SimUseCases {
const taskCreated = await this.pauseRepository.addTask(newTask) const taskCreated = await this.pauseRepository.addTask(newTask)
const operation: ObjeniousOperation = {
operation: "terminate",
iccids: iccid,
status: "running",
correlation_id: correlation_id
}
/**
this.logOperation(operation)
.then().catch(e => console.error("Error login operation", e))
*/
// Caso que la task no se pueda crear en la BDD // Caso que la task no se pueda crear en la BDD
if (taskCreated.error != undefined) { if (taskCreated.error != undefined) {
console.error("[Sim.usecases]", taskCreated.error) console.error("[Sim.usecases]", taskCreated.error)

View File

@@ -18,24 +18,26 @@ export const rmqConnOptions = <RMQConnectionParams>{
secure: rmqSecure, secure: rmqSecure,
} }
export const rabbitmqEventBus = new RabbitMQEventBus({ export const QUEUES = {
connectionParams: rmqConnOptions,
buildStructure: buildQueues,
maxRetry: 5
})
async function buildQueues(channel: Channel) {
const QUEUES = {
OBJ: "sim.objenious", OBJ: "sim.objenious",
DLX: "sim.objenious.dlx", OBJDLX: "sim.objenious.dlx",
DEL: "sim.objenious.delayed" OBJDEL: "sim.objenious.delayed",
} }
const EXCHANGES = { export const EXCHANGES = {
MAIN: "sim.exchange", MAIN: "sim.exchange",
DLX: "sim.ex.objenious.dlx", DLX: "sim.ex.objenious.dlx",
DEL: "sim.ex.objenious.delayed" DEL: "sim.ex.objenious.delayed"
} }
export const rabbitmqEventBus = new RabbitMQEventBus({
connectionParams: rmqConnOptions,
buildStructure: buildQueues,
maxRetry: 5,
delayedExchange: EXCHANGES.DEL,
dlxExchange: EXCHANGES.DLX
})
async function buildQueues(channel: Channel) {
const DELAY = 10 * 1000 const DELAY = 10 * 1000
const BASE_OBENIOUS_KEY = "sim.objenious.#" const BASE_OBENIOUS_KEY = "sim.objenious.#"
@@ -45,8 +47,8 @@ async function buildQueues(channel: Channel) {
await channel.assertExchange(EXCHANGES.MAIN, "topic") await channel.assertExchange(EXCHANGES.MAIN, "topic")
await channel.assertQueue(QUEUES.OBJ) await channel.assertQueue(QUEUES.OBJ)
await channel.assertQueue(QUEUES.DLX) await channel.assertQueue(QUEUES.OBJDLX)
await channel.assertQueue(QUEUES.DEL, { await channel.assertQueue(QUEUES.OBJDEL, {
durable: true, durable: true,
arguments: { arguments: {
'x-message-ttl': DELAY, 'x-message-ttl': DELAY,
@@ -55,9 +57,9 @@ async function buildQueues(channel: Channel) {
}) })
// Cola dead-letter // Cola dead-letter
await channel.bindQueue(QUEUES.DLX, EXCHANGES.DLX, "sim.objenious.#") await channel.bindQueue(QUEUES.OBJDLX, EXCHANGES.DLX, "sim.objenious.#")
// Cola delay // Cola delay
await channel.bindQueue(QUEUES.DEL, EXCHANGES.DEL, BASE_OBENIOUS_KEY) await channel.bindQueue(QUEUES.OBJDEL, EXCHANGES.DEL, BASE_OBENIOUS_KEY)
// Cola objenious -> main exchange // Cola objenious -> main exchange
await channel.bindQueue(QUEUES.OBJ, EXCHANGES.MAIN, BASE_OBENIOUS_KEY) await channel.bindQueue(QUEUES.OBJ, EXCHANGES.MAIN, BASE_OBENIOUS_KEY)

View File

@@ -57,7 +57,7 @@ export class PauseCancelTaskRepository {
public async addTask(task: CreatePauseCancelTaskDTO): Promise<Result<string, PauseCancelTask>> { public async addTask(task: CreatePauseCancelTaskDTO): Promise<Result<string, PauseCancelTask>> {
const sql = ` const sql = `
INSERT INTO pause_cancel_tasks (iccid, activation_date, next_check, last_checked, operation_type, actionData) INSERT INTO pause_cancel_tasks (iccid, activation_date, next_check, last_checked, operation_type, action_data)
VALUES ($1, $2, $3, now(), $4, $5) VALUES ($1, $2, $3, now(), $4, $5)
RETURNING *; RETURNING *;
`; `;
@@ -124,3 +124,5 @@ export class PauseCancelTaskRepository {
} }
} }
export default PauseCancelTask

View File

@@ -9,6 +9,10 @@
], ],
"include": [ "include": [
"**/*.ts", "**/*.ts",
"**/*.d.ts",
"../../packages/sim-shared/**/*.ts" "../../packages/sim-shared/**/*.ts"
],
"files": [
"index.ts"
] ]
} }

View File

@@ -3,7 +3,7 @@ import { SimUsecases } from "./Sim.usecases.js"
import { activationValidator, iccidValidator } from "./httpValidators.js" import { activationValidator, iccidValidator } from "./httpValidators.js"
import { companyFromIccid } from "#domain/companies.js" import { companyFromIccid } from "#domain/companies.js"
import { BodyValidator } from "sim-shared/aplication/BodyValidator.js" import { BodyValidator } from "sim-shared/aplication/BodyValidator.js"
import { tryCatch } from "packages/sim-shared/domain/Result.js" import { tryCatch } from "sim-shared/domain/Result.js"
export class SimController { export class SimController {
@@ -131,6 +131,21 @@ export class SimController {
}) })
} }
public reActivation() {
return this.controllerGenerator<{ iccid: string, offer: string }, { iccid: string, offer: string, compañia: string }>({
validator: iccidValidator,
mapBody: (b) => {
const { iccid, offer } = b
const compañia = companyFromIccid(iccid)
return { iccid, compañia, offer }
},
useCase: (args) => this.simUseCases.reActivation(args),
onError: (d, e) => console.error("[x] Error reactivacion: ", d, e),
onSuccess: console.log
})
}
public cancelation() { public cancelation() {
return this.controllerGenerator<{ iccid: string }, { iccid: string, compañia: string }>({ return this.controllerGenerator<{ iccid: string }, { iccid: string, compañia: string }>({
validator: iccidValidator, validator: iccidValidator,

View File

@@ -1,10 +1,10 @@
import { OrderRepository } from "sim-shared/infrastructure/OrderRepository.js"; import { OrderRepository } from "sim-shared/infrastructure/OrderRepository.js";
import { Result } from "sim-shared/domain/Result.js"; import { Result } from "sim-shared/domain/Result.js";
import assert from "node:assert"; import assert from "node:assert";
import { EventBus } from "sim-shared/domain/EventBus.port"; import { SimEvents } from "sim-shared/domain/SimEvents.js";
import { SimEvents } from "sim-shared/domain/SimEvents";
import { uuidv7 } from "uuidv7"; import { uuidv7 } from "uuidv7";
import { CreateOrderDTO, OrderTracking, OrderType, OrderTypeOptions } from "sim-shared/domain/Order.js"; import { CreateOrderDTO, OrderTracking, OrderType, OrderTypeOptions } from "sim-shared/domain/Order.js";
import { EventBus } from "sim-shared/domain/EventBus.port.js";
/** /**
* Casos de uso de tarjetas sim. Garantiza que todos los metodos usan el mismo bus de mensajes * Casos de uso de tarjetas sim. Garantiza que todos los metodos usan el mismo bus de mensajes
@@ -130,6 +130,36 @@ export class SimUsecases {
} }
} }
async reActivation(args: { iccid: string, compañia: string, offer: string }):
Promise<Result<string, { iccid: string, message_id: string, operation: "reactivate" }>> {
const activationEvent = <SimEvents.activation>{
key: `sim.${args.compañia}.reactivate`,
payload: {
iccid: args.iccid,
offer: args.offer
}
}
const activationWithId = this.addMessage_id(activationEvent)
console.log("[d] Reactivation ", activationWithId)
await this.eventBus.publish([activationWithId])
const createdOrder = await this.saveOrder<SimEvents.reActivation>(activationWithId)
if (createdOrder.error != undefined) {
console.error(createdOrder.error)
return {
error: createdOrder.error
}
}
return {
data: {
iccid: args.iccid,
operation: "reactivate",
message_id: createdOrder.data?.correlation_id
}
}
}
async preActivation(args: { iccid: string, compañia: string }): async preActivation(args: { iccid: string, compañia: string }):
Promise<Result<string, { iccid: string, message_id: string, operation: "preactivation" }>> { Promise<Result<string, { iccid: string, message_id: string, operation: "preactivation" }>> {
@@ -174,8 +204,10 @@ export class SimUsecases {
const cancelationWithId = this.addMessage_id(cancelationEvent) const cancelationWithId = this.addMessage_id(cancelationEvent)
console.log("[d] Cancelation ", cancelationWithId) console.log("[d] Cancelation ", cancelationWithId)
await this.eventBus.publish([cancelationWithId]) await this.eventBus.publish([cancelationWithId])
const savedOrder = await this.saveOrder(cancelationWithId) const savedOrder = await this.saveOrder(cancelationWithId)
if (savedOrder.error != undefined) { if (savedOrder.error != undefined) {
console.error(savedOrder.error) console.error(savedOrder.error)
return { return {
@@ -205,11 +237,12 @@ export class SimUsecases {
iccid: args.iccid iccid: args.iccid
} }
} }
const pauseWithId = this.addMessage_id(pauseEvent) const pauseWithId = this.addMessage_id(pauseEvent)
console.log("[d] Pause", pauseWithId) console.log("[d] Pause", pauseWithId)
await this.eventBus.publish([pauseWithId]) await this.eventBus.publish([pauseWithId])
await this.saveOrder(pauseWithId) //await this.saveOrder(pauseWithId)
const savedOrder = await this.saveOrder(pauseWithId) const savedOrder = await this.saveOrder<SimEvents.pause>(pauseWithId)
if (savedOrder.error != undefined) { if (savedOrder.error != undefined) {
console.error(savedOrder.error) console.error(savedOrder.error)

View File

@@ -22,4 +22,5 @@ export const env = {
RABBITMQ_SECURE: process.env.RABBITMQ_SECURE, RABBITMQ_SECURE: process.env.RABBITMQ_SECURE,
RABBITMQ_RETRY_INTERVAL: process.env.RABBITMQ_INTERVAL, RABBITMQ_RETRY_INTERVAL: process.env.RABBITMQ_INTERVAL,
RABBITMQ_VHOST: String(process.env.RABBITMQ_VHOST), RABBITMQ_VHOST: String(process.env.RABBITMQ_VHOST),
CONNECTIONS_URL: String(process.env.CONNECTIONS_URL)
}; };

View File

@@ -18,7 +18,10 @@ export const rmqConnOptions = <RMQConnectionParams>{
} }
export const rabbitmqEventBus = new RabbitMQEventBus({ export const rabbitmqEventBus = new RabbitMQEventBus({
connectionParams: rmqConnOptions connectionParams: rmqConnOptions,
// La entrada de eventos no tiene que definir exchanges de dlx o delay pero es obligatorio
delayedExchange: "-",
dlxExchange: "-"
}) })
export async function startRMQClient() { export async function startRMQClient() {

View File

@@ -5,6 +5,7 @@ import { simRoutes } from "./infrastructure/simRoutes.http.js"
import { rabbitmqEventBus } from '#config/eventBusConfig.js'; import { rabbitmqEventBus } from '#config/eventBusConfig.js';
import { env } from "#config/env/index.js" import { env } from "#config/env/index.js"
import { orderRoutes } from "#adapters/orderRoutes.http.js"; import { orderRoutes } from "#adapters/orderRoutes.http.js";
import { connectionsRoutes } from "#adapters/simconnectionsRoutes.js";
const PORT = env.API_PORT const PORT = env.API_PORT
const HOSTNAME = "0.0.0.0" const HOSTNAME = "0.0.0.0"
@@ -18,7 +19,6 @@ rabbitmqEventBus.connect()
console.error("[!] El cliente RMQ no se ha podido iniciar", e) console.error("[!] El cliente RMQ no se ha podido iniciar", e)
}) })
// Middleware // Middleware
app.use(cors()); app.use(cors());
@@ -26,6 +26,7 @@ app.use(express.json());
app.use(express.urlencoded({ extended: true })); app.use(express.urlencoded({ extended: true }));
app.use("/sim", simRoutes) app.use("/sim", simRoutes)
app.use("/simconnections", connectionsRoutes)
app.use("/orders", orderRoutes) app.use("/orders", orderRoutes)
app.use("/docs", express.static(path.join(process.cwd(), '../../docs'))) app.use("/docs", express.static(path.join(process.cwd(), '../../docs')))
@@ -37,4 +38,5 @@ app.get("/health", (req, res) => {
app.listen(PORT, HOSTNAME, () => { app.listen(PORT, HOSTNAME, () => {
console.log("[o] Servidor iniciado en el puerto %d", PORT) console.log("[o] Servidor iniciado en el puerto %d", PORT)
}) })
export default {} export default {}

View File

@@ -1,4 +1,4 @@
import { rabbitmqEventBus } from '#config/eventBusConfig.js'; import { rabbitmqEventBus } from '../config/eventBusConfig.js';
import { SimUsecases } from '../aplication/Sim.usecases.js'; import { SimUsecases } from '../aplication/Sim.usecases.js';
import { SimController } from '../aplication/Sim.controller.js'; import { SimController } from '../aplication/Sim.controller.js';
import { Router } from 'express'; import { Router } from 'express';
@@ -23,6 +23,7 @@ simRoutes.get("/status", () => { })
simRoutes.post("/save", simController.save()) simRoutes.post("/save", simController.save())
simRoutes.post("/activate", simController.activation()) simRoutes.post("/activate", simController.activation())
simRoutes.post("/reActivate", simController.reActivation())
simRoutes.post("/preActivate", simController.preactivation()) simRoutes.post("/preActivate", simController.preactivation())
@@ -35,4 +36,5 @@ simRoutes.post("/test", simController.test())
// Proceso especifico de ALAI para liberar sims canceladas // Proceso especifico de ALAI para liberar sims canceladas
simRoutes.post("/free", simController.free()) simRoutes.post("/free", simController.free())
export { simRoutes } export { simRoutes }

View File

@@ -0,0 +1,87 @@
import { env } from "#config/env/index.js"
import { Router } from "express"
import { ClientRequest, IncomingMessage } from "http"
import { createProxyMiddleware } from "http-proxy-middleware"
import { Request } from "express"
export const connectionsRoutes = Router()
const CONNECTIONS_URL = env.CONNECTIONS_URL// TODO: Meter al ENV
//const CONNECTIONS_URL = "http://sf-nfc-server.savefamilygps.net"
console.log("CONNURL: ", CONNECTIONS_URL)
connectionsRoutes.use("", createProxyMiddleware({
target: CONNECTIONS_URL,
changeOrigin: true,
pathRewrite: {
'^/': "/simconnections/"
},
on: {
proxyReq: (proxyReq: ClientRequest, req: Request) => {
const protocol = req.protocol;
const host = req.get('host');
const originalFullUrl = `${protocol}://${host}${req.originalUrl}`;
const destinationFullUrl = `${CONNECTIONS_URL}${proxyReq.path}`;
/*
constnsole.log('──────────────────────────────────────────────────');
console.log(`[PROXY_DEBUG]`);
console.log(` ENTRADA: ${originalFullUrl}`);
console.log(` MÉTODO : ${req.method}`);
console.log(` DESTINO: ${destinationFullUrl}`);
console.log('──────────────────────────────────────────────────');
*/
console.log(`[Proxy Req]: ${req.method} ${req.url} -> ${proxyReq.path}`);
},
proxyRes: (proxyRes, req, res) => {
console.log(`[Proxy Res] Status: ${proxyRes.statusCode} desde ${req.url}`);
},
error: (err, req, res) => {
console.error('[Proxy Error]:', err);
// Validamos que 'res' tenga el método 'status' (típico de Express Response)
if ('status' in res) {
//@ts-ignore
res.status(500).json({ message: 'Error interno en el Gateway' });
}
},
}
}))
// Rutas
/**
connectionsRoutes.post('/simconnections/alai/preactivate',);
connectionsRoutes.get('/simconnections/alai/pause',);
connectionsRoutes.post('/simconnections/alai/terminate',);
connectionsRoutes.get('/simconnections/alai/pauseByPhone',);
connectionsRoutes.get('/simconnections/alai/active',);
connectionsRoutes.get('/simconnections/alai/change_orderid',);
connectionsRoutes.get('/simconnections/alai/select',);
connectionsRoutes.get('/simconnections/alai/select-iccid',);
connectionsRoutes.get('/simconnections/alai/selectFromDb',);
connectionsRoutes.get('/simconnections/alai/selectPage',);
connectionsRoutes.post('/simconnections/alai/schedulePause',);
connectionsRoutes.get('/simconnections/shopify/getbyWP',);
connectionsRoutes.get('/simconnections/shopify/getbyWPS',);
///
connectionsRoutes.get('/simconnections/sim/associate',);
connectionsRoutes.post('/simconnections/sim/search',);
connectionsRoutes.post('/simconnections/sim/historic',);
connectionsRoutes.post('/simconnections/sim/update',);
///
connectionsRoutes.post('/simconnections/nos/activate',);
connectionsRoutes.get('/simconnections/nos/select',);
connectionsRoutes.get('/simconnections/nos/selectPage',);
//Unificación
connectionsRoutes.post('/simconnections/sim/active',); // True false
connectionsRoutes.patch('/simconnections/sim/pause',);
connectionsRoutes.get('/simconnections/sim/select',);
connectionsRoutes.get('/simconnections/sim/select-phone',);
**/

View File

@@ -53,6 +53,7 @@
"cors": "*", "cors": "*",
"dotenv": "*", "dotenv": "*",
"express": "*", "express": "*",
"http-proxy-middleware": "^3.0.5",
"sim-shared": "sim-shared:*", "sim-shared": "sim-shared:*",
"typescript": "*" "typescript": "*"
}, },

View File

@@ -9,6 +9,7 @@
], ],
"include": [ "include": [
"**/*.ts", "**/*.ts",
"**/*.d.ts",
"../../packages/sim-shared/**/*.ts" "../../packages/sim-shared/**/*.ts"
], ],
"files": [ "files": [

View File

@@ -8,9 +8,9 @@ import { OrderRepository } from "sim-shared/infrastructure/OrderRepository.js"
import { TaskVolcadoLineas } from "./tasks/volcado_lineas.js" import { TaskVolcadoLineas } from "./tasks/volcado_lineas.js"
import { ObjeniousLinesRepository } from "./infranstructure/ObjeniousLinesRepository.js" import { ObjeniousLinesRepository } from "./infranstructure/ObjeniousLinesRepository.js"
import { postgresClientIntranet } from "./config/intranetPostgresConfig.js" import { postgresClientIntranet } from "./config/intranetPostgresConfig.js"
import { PauseCancelTaskRepository } from "packages/sim-consumidor-objenious/infrastructure/PauseCancelTaskRepository.js" import { PauseCancelTaskRepository } from "sim-consumidor-objenious/infrastructure/PauseCancelTaskRepository.js"
import { PauseTerminateTask } from "./tasks/check_pause_terminate.js" import { PauseTerminateTask } from "./tasks/check_pause_terminate.js"
import { SimUseCases } from "packages/sim-consumidor-objenious/aplication/Sim.usecases.js" import { SimUseCases } from "sim-consumidor-objenious/aplication/Sim.usecases.js"
async function startCron() { async function startCron() {
const commonSettings = { const commonSettings = {
@@ -62,6 +62,7 @@ async function startCron() {
orderRepository orderRepository
) )
await objTask.getPendingOperations()
const PERIODO_PETICIONES = 10 * 60 * 1000 const PERIODO_PETICIONES = 10 * 60 * 1000
const interval = setInterval(async () => { const interval = setInterval(async () => {
try { try {
@@ -80,7 +81,6 @@ async function startCron() {
} }
}, PERIODO_VOLCADO) }, PERIODO_VOLCADO)
await pauseTask.run() await pauseTask.run()
const PERIODO_CANCELACIONES = 60 * 60 * 1000; const PERIODO_CANCELACIONES = 60 * 60 * 1000;
const clacelacionesInterval = setInterval(async () => { const clacelacionesInterval = setInterval(async () => {

View File

@@ -3,7 +3,7 @@ import { OrderRepository } from "sim-shared/infrastructure/OrderRepository.js";
import axios from "axios"; import axios from "axios";
import { IOperationsRepository, Objenious, ObjeniousOperation, ObjeniousOperationChange, StatusEnum } from "sim-shared/domain/operationsRepository.port.js"; import { IOperationsRepository, Objenious, ObjeniousOperation, ObjeniousOperationChange, StatusEnum } from "sim-shared/domain/operationsRepository.port.js";
import { HttpClient } from "sim-shared/infrastructure/HTTPClient.js"; import { HttpClient } from "sim-shared/infrastructure/HTTPClient.js";
import { ObjeniousOperationsRepository } from "packages/sim-shared/infrastructure/ObjeniousOperationRepository.js"; import { ObjeniousOperationsRepository } from "sim-shared/infrastructure/ObjeniousOperationRepository.js";
export class CheckObjeniousRequests { export class CheckObjeniousRequests {
constructor( constructor(

View File

@@ -2,7 +2,7 @@ import { ObjeniousLine } from "sim-shared/domain/objeniousLine.js";
import { PauseCancelTaskRepository } from "sim-consumidor-objenious/infrastructure/PauseCancelTaskRepository.js"; import { PauseCancelTaskRepository } from "sim-consumidor-objenious/infrastructure/PauseCancelTaskRepository.js";
import { ObjeniousOperationsRepository } from "sim-shared/infrastructure/ObjeniousOperationRepository.js"; import { ObjeniousOperationsRepository } from "sim-shared/infrastructure/ObjeniousOperationRepository.js";
import { SimUseCases } from "sim-consumidor-objenious/aplication/Sim.usecases.js"; import { SimUseCases } from "sim-consumidor-objenious/aplication/Sim.usecases.js";
import { OrderRepository } from "packages/sim-shared/infrastructure/OrderRepository.js"; import { OrderRepository } from "sim-shared/infrastructure/OrderRepository.js";
const logger = const logger =
{ {
@@ -118,9 +118,11 @@ export class PauseTerminateTask {
switch (linea.status.billingStatus) { switch (linea.status.billingStatus) {
case "ACTIVATED": case "ACTIVATED":
let exito = false;
let result = null; let result = null;
// IMPORTANTE COMRPOBAR EL DUE DATE
// Se termina el proceso aqui pero pasa a ser una operación de
// objenious por lo que puede fallar y quedaria registrado en
// la tabla objenious_operation
switch (operacionTipo) { switch (operacionTipo) {
case "suspend": case "suspend":
result = await this.simUsecases.suspend(actionData)() result = await this.simUsecases.suspend(actionData)()

View File

@@ -9,6 +9,7 @@
], ],
"include": [ "include": [
"**/*.ts", "**/*.ts",
"**/*.d.ts",
"../../packages/sim-shared/**/*.ts", "../../packages/sim-shared/**/*.ts",
"../../packages/sim-consumidor-objenious/**/*.ts" "../../packages/sim-consumidor-objenious/**/*.ts"
] ]

View File

@@ -80,7 +80,8 @@ export type FinishOrderDTO =
IdOrCorrelationID IdOrCorrelationID
& &
{ {
reason?: string reason?: string,
end_date?: Date
} }
export type ErrorOrderDTO = export type ErrorOrderDTO =

View File

@@ -24,7 +24,7 @@ export namespace SimEvents {
} }
export type reActivation = DomainEvent & { export type reActivation = DomainEvent & {
key: `sim.${string}.reActivate`, key: `sim.${string}.reactivate`,
payload: { payload: {
iccid: string iccid: string
}, },
@@ -47,6 +47,7 @@ export namespace SimEvents {
options: { options: {
} }
} }
export type suspend = pause
export type free = DomainEvent & { export type free = DomainEvent & {
key: `sim.${string}.free`, key: `sim.${string}.free`,

View File

@@ -12,7 +12,7 @@ export type ObjeniousOperation = {
id?: number; id?: number;
/** Uuid del mensaje asociado a la operacion */ /** Uuid del mensaje asociado a la operacion */
correlation_id?: string; correlation_id?: string;
operation: "activate" | string; // TODO: completar y actualizar operation: "activate" | "suspend" | "terminate" | string; // TODO: completar y actualizar
retry_count?: number; retry_count?: number;
max_retry?: number; max_retry?: number;
max_date_retry?: string | null; max_date_retry?: string | null;
@@ -27,8 +27,7 @@ export type ObjeniousOperation = {
} }
export type ObjeniousOperationChange = { export type ObjeniousOperationChange = {
id?: number; id?: number; operation_id: number;
operation_id: number;
info?: string | null; info?: string | null;
error?: string | null; error?: string | null;
new_status: StatusEnum; new_status: StatusEnum;

View File

@@ -1,6 +1,20 @@
import { describe, it } from "node:test"; import { before, describe, it } from "node:test";
import { ObjeniousOperationsRepository } from "./ObjeniousOperationRepository.js"; import { ObjeniousOperationsRepository } from "./ObjeniousOperationRepository.js";
import { httpObjClient, postgresClient } from "../config/config.test.js"; import { httpObjClient, postgresClient } from "../config/config.test.js";
import { ObjeniousOperation } from "../domain/operationsRepository.port.js";
const correctOperation: ObjeniousOperation = {
iccids: "test",
operation: "activate",
status: "finished"
}
const errorOperation: ObjeniousOperation = {
iccids: "test",
operation: "terminate",
status: "error",
error: "mensaje de error"
}
describe("[Integration] Test API requests", () => { describe("[Integration] Test API requests", () => {
const repository = new ObjeniousOperationsRepository( const repository = new ObjeniousOperationsRepository(
@@ -8,7 +22,18 @@ describe("[Integration] Test API requests", () => {
postgresClient postgresClient
) )
it("Read /lines with multiple iccids", () => { before(async () => {
await repository.createOperation(correctOperation)
await repository.createOperation(errorOperation)
})
it("Read last operation by line", () => {
/**
* Objetivo:
* - Cuando se va a hacer una operacion de sim hay que cancelarla directamente si:
* - Ya hay una en curso del mismo tipo.
* - Ya ha terminado una del mismo tipo.
* - Se ignoran las erroneas
*/
}) })
}) })

View File

@@ -146,6 +146,20 @@ export class ObjeniousOperationsRepository implements IOperationsRepository {
} }
} }
async getLastOperationOfLine(iccid: string) {
const query = `
SELECT * FROM public.objenious_operation
WHERE iccids = $1 and error is null
ORDER BY id asc limit 1
`
const values = [iccid];
const { rows } = await this.pgClient.query(query, values);
return <Result<string, ObjeniousOperation>>{
data: rows[0]
}
}
async updateOperation(data: ObjeniousOperationChange): Promise<Result<string, ObjeniousOperation>> { async updateOperation(data: ObjeniousOperationChange): Promise<Result<string, ObjeniousOperation>> {
const client = await this.pgClient.connect(); const client = await this.pgClient.connect();
const { const {

View File

@@ -302,8 +302,8 @@ export class OrderRepository {
UPDATE order_tracking UPDATE order_tracking
SET SET
status = 'finished', status = 'finished',
update_date = (now() at time zone 'utc'), update_date = now(),
finish_date = (now() at time zone 'utc') finish_date = now()
WHERE id = $1 WHERE id = $1
RETURNING id, status, update_date; RETURNING id, status, update_date;
` `
@@ -380,6 +380,7 @@ export class OrderRepository {
const id = currentOrderResult.data.id // Saco el id para evitar busacr por correlation_id que es mas lento const id = currentOrderResult.data.id // Saco el id para evitar busacr por correlation_id que es mas lento
const currentOrder = currentOrderResult.data! const currentOrder = currentOrderResult.data!
//console.log("Current Order", currentOrder)
// 3. Si todo ok se actualiza el order // 3. Si todo ok se actualiza el order
// Si el status es dlx se asume que ha terminado y no va a reintentarse // Si el status es dlx se asume que ha terminado y no va a reintentarse
@@ -401,6 +402,8 @@ export class OrderRepository {
client.query<{ id: number, status: string, update_date: string }>(uOrderTracking, vOrderTracking) client.query<{ id: number, status: string, update_date: string }>(uOrderTracking, vOrderTracking)
) )
console.log("updatedOrderResult", updatedOrderResult)
if (updatedOrderResult.error != undefined) { if (updatedOrderResult.error != undefined) {
await client.query("ROLLBACK") await client.query("ROLLBACK")
client.release() client.release()
@@ -423,7 +426,7 @@ export class OrderRepository {
) )
RETURNING id; RETURNING id;
` `
const vOrderHistory = [args.id, currentOrder.status, args.status, args.reason] const vOrderHistory = [currentOrder.id, currentOrder.status, args.status, args.reason]
const newOrderHistoryResult = await this.getFirst( const newOrderHistoryResult = await this.getFirst(
client.query<{ id: number }>(iOrderHistory, vOrderHistory) client.query<{ id: number }>(iOrderHistory, vOrderHistory)
) )

View File

@@ -21,15 +21,22 @@ export class RabbitMQEventBus implements EventBus {
channel?: ChannelWrapper channel?: ChannelWrapper
connected: Boolean = false connected: Boolean = false
private delayedExchange: string;
private dlxExchange: string;
private connectionOptions: RMQConnectionParams private connectionOptions: RMQConnectionParams
constructor(args: { constructor(args: {
connectionParams: RMQConnectionParams, connectionParams: RMQConnectionParams,
buildStructure?: (chan: Channel) => Promise<void>, buildStructure?: (chan: Channel) => Promise<void>,
maxRetry?: number maxRetry?: number,
delayedExchange: string,
dlxExchange: string
}) { }) {
this.connectionOptions = args.connectionParams this.connectionOptions = args.connectionParams
if (args.buildStructure != undefined) this.buildStructure = args.buildStructure if (args.buildStructure != undefined) this.buildStructure = args.buildStructure
if (args.maxRetry != undefined) this.maxRetry = args.maxRetry if (args.maxRetry != undefined) this.maxRetry = args.maxRetry
this.delayedExchange = args.delayedExchange
this.dlxExchange = args.dlxExchange
} }
async consume(queue: string, callback: (msg: ConsumeMessage | null) => void) { async consume(queue: string, callback: (msg: ConsumeMessage | null) => void) {
@@ -50,23 +57,25 @@ export class RabbitMQEventBus implements EventBus {
async nack(msg: ConsumeMessage, requeue?: boolean) { async nack(msg: ConsumeMessage, requeue?: boolean) {
if (this.channel == undefined) throw new Error("[RMQ] Canal no iniciallizado"); if (this.channel == undefined) throw new Error("[RMQ] Canal no iniciallizado");
console.log("NACK: ", msg.properties.headers) console.log("[i] NACK: ", msg.properties.headers)
const headers = msg.properties.headers || {} const headers = msg.properties.headers || {}
const numberRetry = headers['x-retry-count'] || 0 const numberRetry = headers['x-retry-count'] || 0
const routingKey = msg.fields.routingKey const routingKey = msg.fields.routingKey
if (numberRetry < this.maxRetry) { if (numberRetry < this.maxRetry) {
console.log("Delaying") console.log("[i] Delaying ")
await this.channel.publish("sim.ex.objenious.delayed", routingKey, msg.content, { // "sim.ex.objenious.delayed"
await this.channel.publish(this.delayedExchange, routingKey, msg.content, {
headers: { headers: {
...headers, ...headers,
'x-retry-count': numberRetry + 1 'x-retry-count': numberRetry + 1
} }
}) })
} else { } else {
console.log("DeadLetter") console.log("[i] DeadLetter")
await this.channel.publish("sim.ex.objenious.dlx", routingKey, msg.content, { //"sim.ex.objenious.dlx"
await this.channel.publish(this.dlxExchange, routingKey, msg.content, {
headers: { headers: {
...headers ...headers
} }

View File

@@ -9,6 +9,6 @@
], ],
"include": [ "include": [
"**/*.ts", "**/*.ts",
"../../packages/sim-shared/**/*.ts", "**/*.d.ts",
] ]
} }

View File

@@ -3,7 +3,7 @@
"compilerOptions": { "compilerOptions": {
"outDir": "dist", "outDir": "dist",
"rootDir": "./", "rootDir": "./",
"baseUrl": "./", //"baseUrl": "./", //eliminar para v6
"composite": true, "composite": true,
"esModuleInterop": true, "esModuleInterop": true,
"sourceMap": true, "sourceMap": true,
@@ -12,6 +12,11 @@
"resolveJsonModule": true, "resolveJsonModule": true,
"module": "nodenext", "module": "nodenext",
"moduleResolution": "nodenext", "moduleResolution": "nodenext",
"paths": {
"sim-consumidor-objenious": [
"./packages/sim-consumidor-objenious/*"
]
}
}, },
"include": [ "include": [
"**/*.ts", "**/*.ts",
@@ -20,5 +25,5 @@
], ],
"exclude": [ "exclude": [
"dist" "dist"
] ],
} }

1661
yarn.lock

File diff suppressed because it is too large Load Diff