From 9ec127433dbe92d5f9476eb0d44841e434d596f0 Mon Sep 17 00:00:00 2001 From: Alvar San Martin Date: Thu, 23 Apr 2026 13:18:50 +0200 Subject: [PATCH] Template alai --- docs/sim-alai/environments/local.yml | 7 + docs/sim-alai/environments/prod.yml | 7 + docs/sim-alai/opencollection.yml | 10 + docs/sim-api/Activate.bru | 2 +- docs/sim-api/Cancel.bru | 2 +- packages/sim-consumidor-alai/.env | 6 + .../aplication/SimAlai.controller.ts | 178 ++++++++++++++++++ .../aplication/SimAlai.router.ts | 79 ++++++++ .../aplication/SimAlai.usecases.ts | 156 +++++++++++++++ .../aplication/SslService.ts | 51 +++++ .../aplication/httpValidators.ts | 39 ++++ .../sim-consumidor-alai/config/env/env.ts | 40 ++++ .../config/eventBus.config.ts | 72 +++++++ .../config/postgreConfig.ts | 18 ++ .../sim-consumidor-alai/domain/AlaiAPI.ts | 1 + packages/sim-consumidor-alai/index.ts | 55 ++++++ .../infrastructure/AlaiHttpClient.ts | 41 ++++ .../infrastructure/AlaiJwtService.ts | 0 .../infrastructure/AlaiRepository.test.ts | 0 .../infrastructure/AlaiRepository.ts | 177 +++++++++++++++++ packages/sim-consumidor-alai/package.json | 71 +++++++ packages/sim-consumidor-alai/readme.md | 5 + packages/sim-consumidor-alai/tsconfig.json | 41 ++++ 23 files changed, 1056 insertions(+), 2 deletions(-) create mode 100644 docs/sim-alai/environments/local.yml create mode 100644 docs/sim-alai/environments/prod.yml create mode 100644 docs/sim-alai/opencollection.yml create mode 100644 packages/sim-consumidor-alai/.env create mode 100644 packages/sim-consumidor-alai/aplication/SimAlai.controller.ts create mode 100644 packages/sim-consumidor-alai/aplication/SimAlai.router.ts create mode 100644 packages/sim-consumidor-alai/aplication/SimAlai.usecases.ts create mode 100644 packages/sim-consumidor-alai/aplication/SslService.ts create mode 100644 packages/sim-consumidor-alai/aplication/httpValidators.ts create mode 100644 packages/sim-consumidor-alai/config/env/env.ts create mode 100644 packages/sim-consumidor-alai/config/eventBus.config.ts create mode 100644 packages/sim-consumidor-alai/config/postgreConfig.ts create mode 100644 packages/sim-consumidor-alai/domain/AlaiAPI.ts create mode 100644 packages/sim-consumidor-alai/index.ts create mode 100644 packages/sim-consumidor-alai/infrastructure/AlaiHttpClient.ts create mode 100644 packages/sim-consumidor-alai/infrastructure/AlaiJwtService.ts create mode 100644 packages/sim-consumidor-alai/infrastructure/AlaiRepository.test.ts create mode 100644 packages/sim-consumidor-alai/infrastructure/AlaiRepository.ts create mode 100644 packages/sim-consumidor-alai/package.json create mode 100644 packages/sim-consumidor-alai/readme.md create mode 100644 packages/sim-consumidor-alai/tsconfig.json diff --git a/docs/sim-alai/environments/local.yml b/docs/sim-alai/environments/local.yml new file mode 100644 index 0000000..678d231 --- /dev/null +++ b/docs/sim-alai/environments/local.yml @@ -0,0 +1,7 @@ +name: local +color: "#2E8A54" +variables: + - name: baseurl + value: http://localhost:3001 + - secret: true + name: token diff --git a/docs/sim-alai/environments/prod.yml b/docs/sim-alai/environments/prod.yml new file mode 100644 index 0000000..080fcff --- /dev/null +++ b/docs/sim-alai/environments/prod.yml @@ -0,0 +1,7 @@ +name: prod +color: "#CE4F3B" +variables: + - name: baseurl + value: https://nosconnectcenter-api.iot-x.com + - secret: true + name: token diff --git a/docs/sim-alai/opencollection.yml b/docs/sim-alai/opencollection.yml new file mode 100644 index 0000000..297aed6 --- /dev/null +++ b/docs/sim-alai/opencollection.yml @@ -0,0 +1,10 @@ +opencollection: 1.0.0 + +info: + name: sim-alai +bundled: false +extensions: + bruno: + ignore: + - node_modules + - .git diff --git a/docs/sim-api/Activate.bru b/docs/sim-api/Activate.bru index 9b392e4..bbc6a60 100644 --- a/docs/sim-api/Activate.bru +++ b/docs/sim-api/Activate.bru @@ -11,7 +11,7 @@ post { } body:form-urlencoded { - iccid: 8935103196306448300 + iccid: 8933201125065156057 offer: SAVEFAMILY1 } diff --git a/docs/sim-api/Cancel.bru b/docs/sim-api/Cancel.bru index 5632418..3e31352 100644 --- a/docs/sim-api/Cancel.bru +++ b/docs/sim-api/Cancel.bru @@ -11,7 +11,7 @@ post { } body:form-urlencoded { - iccid: 8933201125068887054 + iccid: 8933201125068889894 } settings { diff --git a/packages/sim-consumidor-alai/.env b/packages/sim-consumidor-alai/.env new file mode 100644 index 0000000..1a67319 --- /dev/null +++ b/packages/sim-consumidor-alai/.env @@ -0,0 +1,6 @@ +APP_PORT=3002 +APP_HOST="0.0.0.0" + +ENVIORMENT=development + +ALAI_CERTIFICATES_DIR=./certificates/ diff --git a/packages/sim-consumidor-alai/aplication/SimAlai.controller.ts b/packages/sim-consumidor-alai/aplication/SimAlai.controller.ts new file mode 100644 index 0000000..1c4aac4 --- /dev/null +++ b/packages/sim-consumidor-alai/aplication/SimAlai.controller.ts @@ -0,0 +1,178 @@ +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 SimAlaiController { + + constructor( + private uscases: SimNosUsecases, + private eventBus: EventBus, + ) { + } + + private validateMsg(msg: ConsumeMessage | null) { + if (msg == undefined) return false; + const msgData = this.decodeMsg(msg) as SimEvents.general + if (msgData == undefined || msgData.payload == undefined) throw new Error("Mensaje invalido") + return msgData; + } + + private decodeMsg(msg: ConsumeMessage): object | undefined { + if (msg.content == undefined) { + console.warn('[Sim.controller] Mensaje vacío'); + return undefined; + } + + try { + // Convertir el Buffer a String (UTF-8) + const contentJson = JSON.parse(Buffer.from(msg.content).toString('utf8')) + return contentJson; + + } catch (error) { + 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; + } + } + + /** + * Metodo duplicado se puede generalizar la a una clase sharedController con las funciones basicas + */ + private async tryUseCase + (msg: ConsumeMessage, usecase: () => Promise>): Promise> { + 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; + } + } + } +} + + diff --git a/packages/sim-consumidor-alai/aplication/SimAlai.router.ts b/packages/sim-consumidor-alai/aplication/SimAlai.router.ts new file mode 100644 index 0000000..01fd517 --- /dev/null +++ b/packages/sim-consumidor-alai/aplication/SimAlai.router.ts @@ -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 { EventBus } from "sim-shared/domain/EventBus.port.js"; +import { Result } from "sim-shared/domain/Result.js"; +import { SimAlaiController } from "./SimAlai.controller.js"; + +type FuncType = ((m: ConsumeMessage) => Promise>) + +export class SimNosRouter { + private readonly routes: Map; + + constructor( + private readonly simController: SimAlaiController, + private readonly eventBus: EventBus + ) { + this.routes = new Map([ + //["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 => { + 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]; + } +} diff --git a/packages/sim-consumidor-alai/aplication/SimAlai.usecases.ts b/packages/sim-consumidor-alai/aplication/SimAlai.usecases.ts new file mode 100644 index 0000000..96707ee --- /dev/null +++ b/packages/sim-consumidor-alai/aplication/SimAlai.usecases.ts @@ -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 { AlaiHttpClient } from "#infrastructure/NosHttpClient.js"; +import { NosRepository } from "#infrastructure/AlaiRepository.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 SimAlaiUsecases { + 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( + func: (_: T) => Promise>, + 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 {} + } +*/ +} diff --git a/packages/sim-consumidor-alai/aplication/SslService.ts b/packages/sim-consumidor-alai/aplication/SslService.ts new file mode 100644 index 0000000..7846367 --- /dev/null +++ b/packages/sim-consumidor-alai/aplication/SslService.ts @@ -0,0 +1,51 @@ +import fs from "fs"; +import path from "path"; +import { Result } from "sim-shared/domain/Result.js"; + +export type P12Cert = { + cainfo: string, + p12cert: string +} + +export type SSLCert = { + cainfo: string, + sslcert: string, + keypem: string +} + +export class SSLCertificateLoader { + + constructor( + private certificatesDir: string, + ) { + } + + public loadCertificatesP12(caFile: string, certFile: string): Result { + try { + const cainfo = fs.readFileSync(path.resolve(this.certificatesDir, caFile)).toString(); + const p12cert = fs.readFileSync(path.resolve(this.certificatesDir, certFile)).toString(); + return { data: { cainfo, p12cert } }; + } catch (e) { + console.error("[x] Error cargando los certificados P12", e) + return { + error: String(e) + } + } + } + + public loadCertificatesSSL(caFile: string, certFile: string, keyFile: string): Result { + try { + const cainfo = fs.readFileSync(path.resolve(this.certificatesDir, caFile)).toString(); + const sslcert = fs.readFileSync(path.resolve(this.certificatesDir, certFile), { encoding: null }).toString(); + const keypem = fs.readFileSync(path.resolve(this.certificatesDir, keyFile), { encoding: null }).toString(); + return { data: { cainfo, sslcert, keypem } }; + } catch (e) { + console.error("[x] Error cargando los certificados SSL", e) + return { + error: String(e) + } + } + } + +} + diff --git a/packages/sim-consumidor-alai/aplication/httpValidators.ts b/packages/sim-consumidor-alai/aplication/httpValidators.ts new file mode 100644 index 0000000..b452272 --- /dev/null +++ b/packages/sim-consumidor-alai/aplication/httpValidators.ts @@ -0,0 +1,39 @@ +import { BodyValidator, Validator } from "sim-shared/aplication/BodyValidator.js"; + +const iccidNotNull = >{ + field: "iccid", + errorMsg: "El iccid no está definido", + validationFunc: (a: { iccid: unknown }) => { + return (a.iccid != null && a.iccid != undefined) + } +} + +const iccidValueOrArray = >{ + 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 = >{ + 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, + ] +) + diff --git a/packages/sim-consumidor-alai/config/env/env.ts b/packages/sim-consumidor-alai/config/env/env.ts new file mode 100644 index 0000000..80f0352 --- /dev/null +++ b/packages/sim-consumidor-alai/config/env/env.ts @@ -0,0 +1,40 @@ +import { loadEnvFile } from "node:process"; +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 = { + ENVIRONMENT: process.env.ENVIORMENT, + POSTGRES_USER: process.env.POSTGRES_USER, + POSTGRES_PASSWORD: process.env.POSTGRES_PASSWORD, + POSTGRES_PORT: process.env.POSTGRES_PORT, + POSTGRES_HOST: process.env.POSTGRES_HOST, + POSTGRES_DATABASE: process.env.POSTGRES_DATABASE, + RABBITMQ_HOST: String(process.env.RABBITMQ_HOST ?? "localhost"), + RABBITMQ_USER: String(process.env.RABBITMQ_USER ?? "test"), + RABBITMQ_PASSWORD: String(process.env.RABBITMQ_PASSWORD ?? "test"), + RABBITMQ_EXCHANGE: String(process.env.RABBITMQ_EXCHANGE ?? "/"), + RABBITMQ_PORT: parseInt(process.env.RABBITMQ_PORT ?? "5672"), + RABBITMQ_MODULENAME: process.env.MODULENAME, + RABBITMQ_TTL: process.env.RABBITMQ_TTL, + RABBITMQ_SECURE: process.env.RABBITMQ_SECURE, + RABBITMQ_RETRY_INTERVAL: process.env.RABBITMQ_INTERVAL, + 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) +}; + diff --git a/packages/sim-consumidor-alai/config/eventBus.config.ts b/packages/sim-consumidor-alai/config/eventBus.config.ts new file mode 100644 index 0000000..c794b99 --- /dev/null +++ b/packages/sim-consumidor-alai/config/eventBus.config.ts @@ -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 = { + username: rmqUser, + password: rmqPass, + vhost: rmqVhost, + hostname: rmqHost, + port: rmqPort, + secure: rmqSecure, +} + + +const BASE_ALAI_KEY = "sim.alai.#" +const QUEUES = { + MAIN: "sim.alai", + DLX: "sim.alai.dlx", + DELAY: "sim.alai.delayed", +} + +const EXCHANGES = { + MAIN: "sim.exchange", + DLX: "sim.ex.alai.dlx", + DEL: "sim.ex.alai.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 + + await channel.assertExchange(EXCHANGES.DEL, "topic") + await channel.assertExchange(EXCHANGES.DLX, "topic") + await channel.assertExchange(EXCHANGES.MAIN, "topic") + + await channel.assertQueue(QUEUES.MAIN) + await channel.assertQueue(QUEUES.DLX) + await channel.assertQueue(QUEUES.DELAY, { + durable: true, + arguments: { + 'x-message-ttl': DELAY, + 'x-dead-letter-exchange': EXCHANGES.MAIN, + } + }) + + // Cola dead-letter + await channel.bindQueue(QUEUES.DLX, EXCHANGES.DLX, BASE_ALAI_KEY) + // Cola delay + await channel.bindQueue(QUEUES.DELAY, EXCHANGES.DEL, BASE_ALAI_KEY) + // Cola nos -> main exchange + await channel.bindQueue(QUEUES.MAIN, EXCHANGES.MAIN, BASE_ALAI_KEY) +} + +export async function startRMQClient() { + await rabbitmqEventBus.connect() + return rabbitmqEventBus +} diff --git a/packages/sim-consumidor-alai/config/postgreConfig.ts b/packages/sim-consumidor-alai/config/postgreConfig.ts new file mode 100644 index 0000000..9c67604 --- /dev/null +++ b/packages/sim-consumidor-alai/config/postgreConfig.ts @@ -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 +}) diff --git a/packages/sim-consumidor-alai/domain/AlaiAPI.ts b/packages/sim-consumidor-alai/domain/AlaiAPI.ts new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/packages/sim-consumidor-alai/domain/AlaiAPI.ts @@ -0,0 +1 @@ + diff --git a/packages/sim-consumidor-alai/index.ts b/packages/sim-consumidor-alai/index.ts new file mode 100644 index 0000000..d2a4527 --- /dev/null +++ b/packages/sim-consumidor-alai/index.ts @@ -0,0 +1,55 @@ +import express from "express" +import cors from 'cors'; +import { env } from "#config/env/env.js" +import { pgClient } from "#config/postgreConfig.js"; +import { startRMQClient } from "#config/eventBus.config.js"; +import { SimNosRouter } from "#aplication/SimAlai.router.js"; + +const RMQ_QUEUE = "sim.alai" +const NOS_BASE_URL = env.NOS_BASE_URL +const PORT = env.APP_PORT +const HOSTNAME = env.APP_HOST + +async function startWorker() { + // Instancia de dependencias + + const rmqClient = await startRMQClient() + + 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() + .then(e => { + console.log("[o] Worker de SIM de NOS iniciado") + }) + .catch(e => { + console.log("[x] Error iniciando worker de SIM de NOS") + }) + +export default {} diff --git a/packages/sim-consumidor-alai/infrastructure/AlaiHttpClient.ts b/packages/sim-consumidor-alai/infrastructure/AlaiHttpClient.ts new file mode 100644 index 0000000..5e12de7 --- /dev/null +++ b/packages/sim-consumidor-alai/infrastructure/AlaiHttpClient.ts @@ -0,0 +1,41 @@ +import axios, { AxiosInstance } from "axios"; +import { env } from "#config/env/env.js" + +export class AlaiHttpClient { + public client: AxiosInstance; + + constructor( + private baseURL: string, + //private jwtManager: JWTProvider + ) { + 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 + } + + +} diff --git a/packages/sim-consumidor-alai/infrastructure/AlaiJwtService.ts b/packages/sim-consumidor-alai/infrastructure/AlaiJwtService.ts new file mode 100644 index 0000000..e69de29 diff --git a/packages/sim-consumidor-alai/infrastructure/AlaiRepository.test.ts b/packages/sim-consumidor-alai/infrastructure/AlaiRepository.test.ts new file mode 100644 index 0000000..e69de29 diff --git a/packages/sim-consumidor-alai/infrastructure/AlaiRepository.ts b/packages/sim-consumidor-alai/infrastructure/AlaiRepository.ts new file mode 100644 index 0000000..9f55269 --- /dev/null +++ b/packages/sim-consumidor-alai/infrastructure/AlaiRepository.ts @@ -0,0 +1,177 @@ +import { Result } from "sim-shared/domain/Result.js"; +import { NosHttpClient } from "./AlaiHttpClient.js"; +import { NosApi } from "#domain/AlaiAPI.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(promise: Promise>): Promise> { + 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> { + const PATH = "/subscribers/" + iccid + console.log("PAth", PATH) + const lineRequest = this.httpClient.get(PATH) + const lineResponse = await this.manageNosRequest(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> { + 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(pageRequest) + if (pageResponse.error != undefined) { + return pageResponse + } else { + return { + data: pageResponse.data.content + } + } + } + + public async getLinesInfo(iccid: string[]) /*Promise>*/ { + 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(PATH) + const resp = await this.manageNosRequest(req) + + if (resp.error != undefined) { + return resp + } else { + return { + //@ts-expect-error + data: resp.data.content + } + } + } + + public async activateSim(iccid: string): Promise> { + const PATH = '/provisioning' + const PRODUCT_ID = 1330 // No se que es, preguntar a Ivan + const data = { + productSetId: PRODUCT_ID + } + + const req = this.httpClient.post(PATH, data) + const resp = await this.manageNosRequest(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(PATH, data) + const resp = await this.manageNosRequest(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(PATH, data) + const resp = await this.manageNosRequest(req) + + if (resp.error != undefined) { + return resp + } else { + return { + data: resp.data.content + } + } + + } + +} diff --git a/packages/sim-consumidor-alai/package.json b/packages/sim-consumidor-alai/package.json new file mode 100644 index 0000000..3005079 --- /dev/null +++ b/packages/sim-consumidor-alai/package.json @@ -0,0 +1,71 @@ +{ + "name": "sim-consumidor-alai", + "type": "module", + "description": "consumidor generico de eventos de alai", + "main": "index.ts", + "imports": { + "#config/*.js": { + "types": "./config/*.ts", + "default": "./config/*.js" + }, + "#config/*": { + "types": "./config/*.ts", + "default": "./config/*.js" + }, + "#infrastructure/*.js": { + "types": "./infrastructure/*.ts", + "default": "./infrastructure/*.js" + }, + "#infrastructure/*": { + "types": "./infrastructure/*.ts", + "default": "./infrastructure/*.js" + }, + "#domain/*.js": { + "types": "./domain/*.ts", + "default": "./domain/*.js" + }, + "#domain/*": { + "types": "./domain/*.ts", + "default": "./domain/*.js" + }, + "#aplication/*.js": { + "types": "./aplication/*.ts", + "default": "./aplication/*.js" + }, + "#aplication/*": { + "types": "./aplication/*.ts", + "default": "./aplication/*.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": { + "@tsconfig/node22": "*", + "amqplib": "^0.10.9", + "cors": "*", + "dotenv": "*", + "express": "*", + "sim-shared": "sim-shared:*", + "typescript": "*" + }, + "devDependencies": { + "@types/amqplib": "^0.10.8", + "@types/cors": "*", + "@types/express": "*", + "@types/node": "*", + "@types/supertest": "*", + "prettier": "*", + "supertest": "*", + "tsc-alias": "^1.8.16", + "tsx": "*", + "vitest": "*" + } +} diff --git a/packages/sim-consumidor-alai/readme.md b/packages/sim-consumidor-alai/readme.md new file mode 100644 index 0000000..750d04f --- /dev/null +++ b/packages/sim-consumidor-alai/readme.md @@ -0,0 +1,5 @@ +# Alai + +## Particularidades de las operaciones de Alai + +TODO: Copiar de obsidian diff --git a/packages/sim-consumidor-alai/tsconfig.json b/packages/sim-consumidor-alai/tsconfig.json new file mode 100644 index 0000000..119ff00 --- /dev/null +++ b/packages/sim-consumidor-alai/tsconfig.json @@ -0,0 +1,41 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "outDir": "../../dist", + "rootDir": "../../", + "paths": { + "#config/*": [ + "./config/*" + ], + "#infrastructure/*": [ + "./infrastructure/*" + ], + "#domain/*": [ + "./domain/*" + ], + "#aplication/*": [ + "./aplication/*" + ], + "config/*": [ + "./config/*" + ], + "infrastructure/*": [ + "./infrastructure/*" + ], + "domain/*": [ + "./domain/*" + ] + } + }, + "exclude": [ + "node_modules" + ], + "include": [ + "**/*.ts", + "**/*.d.ts", + "../../packages/sim-shared/**/*.ts" + ], + "files": [ + "index.ts" + ] +} \ No newline at end of file