Template alai
This commit is contained in:
7
docs/sim-alai/environments/local.yml
Normal file
7
docs/sim-alai/environments/local.yml
Normal file
@@ -0,0 +1,7 @@
|
||||
name: local
|
||||
color: "#2E8A54"
|
||||
variables:
|
||||
- name: baseurl
|
||||
value: http://localhost:3001
|
||||
- secret: true
|
||||
name: token
|
||||
7
docs/sim-alai/environments/prod.yml
Normal file
7
docs/sim-alai/environments/prod.yml
Normal file
@@ -0,0 +1,7 @@
|
||||
name: prod
|
||||
color: "#CE4F3B"
|
||||
variables:
|
||||
- name: baseurl
|
||||
value: https://nosconnectcenter-api.iot-x.com
|
||||
- secret: true
|
||||
name: token
|
||||
10
docs/sim-alai/opencollection.yml
Normal file
10
docs/sim-alai/opencollection.yml
Normal file
@@ -0,0 +1,10 @@
|
||||
opencollection: 1.0.0
|
||||
|
||||
info:
|
||||
name: sim-alai
|
||||
bundled: false
|
||||
extensions:
|
||||
bruno:
|
||||
ignore:
|
||||
- node_modules
|
||||
- .git
|
||||
@@ -11,7 +11,7 @@ post {
|
||||
}
|
||||
|
||||
body:form-urlencoded {
|
||||
iccid: 8935103196306448300
|
||||
iccid: 8933201125065156057
|
||||
offer: SAVEFAMILY1
|
||||
}
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ post {
|
||||
}
|
||||
|
||||
body:form-urlencoded {
|
||||
iccid: 8933201125068887054
|
||||
iccid: 8933201125068889894
|
||||
}
|
||||
|
||||
settings {
|
||||
|
||||
6
packages/sim-consumidor-alai/.env
Normal file
6
packages/sim-consumidor-alai/.env
Normal file
@@ -0,0 +1,6 @@
|
||||
APP_PORT=3002
|
||||
APP_HOST="0.0.0.0"
|
||||
|
||||
ENVIORMENT=development
|
||||
|
||||
ALAI_CERTIFICATES_DIR=./certificates/
|
||||
178
packages/sim-consumidor-alai/aplication/SimAlai.controller.ts
Normal file
178
packages/sim-consumidor-alai/aplication/SimAlai.controller.ts
Normal file
@@ -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<T extends any>
|
||||
(msg: ConsumeMessage, usecase: () => Promise<Result<string, T>>): Promise<Result<string, T>> {
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
79
packages/sim-consumidor-alai/aplication/SimAlai.router.ts
Normal file
79
packages/sim-consumidor-alai/aplication/SimAlai.router.ts
Normal 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 { 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<Result<string, any>>)
|
||||
|
||||
export class SimNosRouter {
|
||||
private readonly routes: Map<string, FuncType>;
|
||||
|
||||
constructor(
|
||||
private readonly simController: SimAlaiController,
|
||||
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];
|
||||
}
|
||||
}
|
||||
156
packages/sim-consumidor-alai/aplication/SimAlai.usecases.ts
Normal file
156
packages/sim-consumidor-alai/aplication/SimAlai.usecases.ts
Normal 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 { 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<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 {}
|
||||
}
|
||||
*/
|
||||
}
|
||||
51
packages/sim-consumidor-alai/aplication/SslService.ts
Normal file
51
packages/sim-consumidor-alai/aplication/SslService.ts
Normal file
@@ -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<string, P12Cert> {
|
||||
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<string, SSLCert> {
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
39
packages/sim-consumidor-alai/aplication/httpValidators.ts
Normal file
39
packages/sim-consumidor-alai/aplication/httpValidators.ts
Normal 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,
|
||||
]
|
||||
)
|
||||
|
||||
40
packages/sim-consumidor-alai/config/env/env.ts
vendored
Normal file
40
packages/sim-consumidor-alai/config/env/env.ts
vendored
Normal file
@@ -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)
|
||||
};
|
||||
|
||||
72
packages/sim-consumidor-alai/config/eventBus.config.ts
Normal file
72
packages/sim-consumidor-alai/config/eventBus.config.ts
Normal 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 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
|
||||
}
|
||||
18
packages/sim-consumidor-alai/config/postgreConfig.ts
Normal file
18
packages/sim-consumidor-alai/config/postgreConfig.ts
Normal 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
|
||||
})
|
||||
1
packages/sim-consumidor-alai/domain/AlaiAPI.ts
Normal file
1
packages/sim-consumidor-alai/domain/AlaiAPI.ts
Normal file
@@ -0,0 +1 @@
|
||||
|
||||
55
packages/sim-consumidor-alai/index.ts
Normal file
55
packages/sim-consumidor-alai/index.ts
Normal file
@@ -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 {}
|
||||
@@ -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<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
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
177
packages/sim-consumidor-alai/infrastructure/AlaiRepository.ts
Normal file
177
packages/sim-consumidor-alai/infrastructure/AlaiRepository.ts
Normal file
@@ -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<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
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
71
packages/sim-consumidor-alai/package.json
Normal file
71
packages/sim-consumidor-alai/package.json
Normal file
@@ -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": "*"
|
||||
}
|
||||
}
|
||||
5
packages/sim-consumidor-alai/readme.md
Normal file
5
packages/sim-consumidor-alai/readme.md
Normal file
@@ -0,0 +1,5 @@
|
||||
# Alai
|
||||
|
||||
## Particularidades de las operaciones de Alai
|
||||
|
||||
TODO: Copiar de obsidian
|
||||
41
packages/sim-consumidor-alai/tsconfig.json
Normal file
41
packages/sim-consumidor-alai/tsconfig.json
Normal file
@@ -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"
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user