Proceso completo de solicitud de activacion

This commit is contained in:
2026-01-29 12:57:51 +01:00
parent 4acc04fb51
commit 68ae3aea57
14 changed files with 138 additions and 107 deletions

View File

@@ -10,12 +10,8 @@ post {
auth: inherit auth: inherit
} }
params:query {
:
}
body:form-urlencoded { body:form-urlencoded {
iccid: 12339912344 iccid: 8933201124059175967
} }
settings { settings {

View File

@@ -11,7 +11,7 @@ export namespace SimEvents {
} }
export type activation = DomainEvent & { export type activation = DomainEvent & {
key: "sim.activation", key: `sim.${string}.activation`,
payload: { payload: {
iccid: string iccid: string
}, },
@@ -20,7 +20,7 @@ export namespace SimEvents {
} }
export type cancelation = DomainEvent & { export type cancelation = DomainEvent & {
key: "sim.cancelation", key: `sim.${string}.cancelation`,
payload: { payload: {
iccid: string iccid: string
}, },

View File

@@ -30,12 +30,12 @@ export class HttpClient {
async (config) => { async (config) => {
// Idealmente estas condiciones no deberian de darse si mantenemos el // Idealmente estas condiciones no deberian de darse si mantenemos el
// token valido de forma preventiva // token valido de forma preventiva
const token = this.jwtManager.authToken?.rawToken const token = await this.jwtManager.getAccessToken()
if (token == undefined) throw new Error("No se ha obtenido el token para la peticion") if (token == undefined) throw new Error("No se ha obtenido el token para la peticion")
config.headers.Authorization = `Bearer ${this.jwtManager.authToken!.rawToken}` config.headers.Authorization = `Bearer ${this.jwtManager.authToken!.rawToken}`
console.log("request completa", config.headers, config.data) console.log("request completa", config.data)
return config; return config;
}, },
(error) => Promise.reject(error) (error) => Promise.reject(error)
@@ -49,7 +49,7 @@ export class HttpClient {
async (error) => { async (error) => {
// TODO: Esta parte no tiene tipos, hay que asegurar el error // TODO: Esta parte no tiene tipos, hay que asegurar el error
const req = error.config const req = error.config
console.error("[http] Error en la respuesta ", error) console.error("[http] Error en la respuesta ", error.response.data)
if (error.response?.status == 401) { if (error.response?.status == 401) {
this.jwtManager.getAccessToken() this.jwtManager.getAccessToken()
} }

View File

@@ -122,7 +122,6 @@ export class RabbitMQEventBus implements EventBus {
protected async createChannel() { protected async createChannel() {
if (this.connection == undefined) throw new Error("[RMQ] Intentando crear un canal sin una conexion") if (this.connection == undefined) throw new Error("[RMQ] Intentando crear un canal sin una conexion")
const channel = this.connection.createChannel({ const channel = this.connection.createChannel({
json: true,
setup: (channel: Channel) => { setup: (channel: Channel) => {
// Exchanges comunes a todos // Exchanges comunes a todos
channel.assertExchange("sim.exchange", "topic", { durable: true }) channel.assertExchange("sim.exchange", "topic", { durable: true })

View File

@@ -4,3 +4,4 @@ OBJ_AUTHORIZATION=XOc7FtwXD8hUX2SFVX94XSty8wkOmChkwDNF09O_aIxPubMDdFUdCDCB4zpzSI
OBJ_CLI_ASSERTION=XOc7FtwXD8hUX2SFVX94XSty8wkOmChkwDNF09O_aIxPubMDdFUdCDCB4zpzSIxi8nOcTg7r_LM_nmd5qm7uLbksf_XArjI8iAyhjKz_2BAXPhmvKs4Fc9f3vv5LDfCVrPB9lP8P7rJ66_qnWs4jvhLQxSfn29m96hgXeCf8oySdIDUjN2q9Js3KAS5LL52Ri6ryvUeO1PvMhaPQMWRqoHIqTV1wPfPtiqQwcjUPmu5GeW164Kq1JLgV3KaGzfCZ9Qv9lbv30EJrukXxWuLCAhBS0kzrBXZoWvf2pb9uh3Am_93_dDxiIGQfIap9ZU_m8ZD1HPgvZOMCY6ZkxQconQ OBJ_CLI_ASSERTION=XOc7FtwXD8hUX2SFVX94XSty8wkOmChkwDNF09O_aIxPubMDdFUdCDCB4zpzSIxi8nOcTg7r_LM_nmd5qm7uLbksf_XArjI8iAyhjKz_2BAXPhmvKs4Fc9f3vv5LDfCVrPB9lP8P7rJ66_qnWs4jvhLQxSfn29m96hgXeCf8oySdIDUjN2q9Js3KAS5LL52Ri6ryvUeO1PvMhaPQMWRqoHIqTV1wPfPtiqQwcjUPmu5GeW164Kq1JLgV3KaGzfCZ9Qv9lbv30EJrukXxWuLCAhBS0kzrBXZoWvf2pb9uh3Am_93_dDxiIGQfIap9ZU_m8ZD1HPgvZOMCY6ZkxQconQ
OBJ_CLIENT_ID=savefamily_rest_ws OBJ_CLIENT_ID=savefamily_rest_ws
OBJ_KID=xNfbMiyL1ORXGP8lElhcv8nVaG3EJKye4Lc1YoN3I1E OBJ_KID=xNfbMiyL1ORXGP8lElhcv8nVaG3EJKye4Lc1YoN3I1E
OBJ_BASE_URL=https://api-getway.objenious.com/ws

View File

@@ -118,11 +118,7 @@ export class JWTService {
return token return token
} }
public async getAccessToken() { public async getNewTokens() {
if (this.authToken != undefined && !this.authToken.isExpired()) {
console.warn("Se está intentado conseguir un token sin expirar el anterior")
}
const bodyWithtoken = { const bodyWithtoken = {
...DEFAULT_BODY, ...DEFAULT_BODY,
client_assertion: this.buildJwtBody() client_assertion: this.buildJwtBody()
@@ -151,6 +147,25 @@ export class JWTService {
} }
} }
public async getAccessToken() {
// Caso 1: El token actual es valido
if (this.authToken != undefined && !this.authToken.isExpired()) {
return this.authToken
}
// Caso 2: El token ha expirado pero se puede refrescar
if (this.authToken?.isExpired() && this.refreshToken != undefined && !this.refreshToken.isExpired()) {
await this.tryRefreshToken()
}
// Caso 3: Ningún token es valido
await this.getNewTokens()
if (this.authToken == undefined) throw new Error("Error obteniendo tokens de auth")
return this.authToken
}
public async tryRefreshToken() { public async tryRefreshToken() {
if (this.refreshToken == undefined) throw new Error("El refreshToken no está definido") if (this.refreshToken == undefined) throw new Error("El refreshToken no está definido")
if (this.refreshToken.isExpired()) throw new Error("El refreshToken ha expirado") if (this.refreshToken.isExpired()) throw new Error("El refreshToken ha expirado")

View File

@@ -1,7 +1,14 @@
import { EventBus } from "#shared/domain/EventBus.port"; import { EventBus } from "#shared/domain/EventBus.port";
import { ConsumeMessage } from "amqplib"; import { ConsumeMessage } from "amqplib";
import { SimUseCases } from "./Sim.usecases"; import { SimUseCases } from "./Sim.usecases";
import { SimEvents } from "#shared/domain/SimEvents";
/**
* La clase usa generadores de funciones para mantener el contexto
* el proceso se hace en 2 partes:
* Controlador - handlers -> Router -> Ejecucion
*
*/
export class SimController { export class SimController {
private eventBus: EventBus; private eventBus: EventBus;
private useCases: SimUseCases private useCases: SimUseCases
@@ -15,33 +22,61 @@ export class SimController {
} }
public async activateSim(msg: ConsumeMessage | null) { private decodeMsg(msg: ConsumeMessage): object | undefined {
return async () => { if (msg.content == undefined) {
if (!this.validateActivationMsg(msg)) { console.warn('[Sim.controller] Mensaje vacío');
throw new Error("Error consumiendo el mensaje no es valido") return undefined;
} }
const msgData = Buffer.from(JSON.parse(msg?.content.toString("utf-8") || "{}").data)
console.log("Mensaje procesado", String(msgData))
// Caso de uso de activaciones try {
await this.useCases.activate({ // Convertir el Buffer a String (UTF-8)
dueDate: new Date().toString(), const contentJson = JSON.parse(Buffer.from(msg.content).toString('utf8'))
identifier: { return contentJson;
identifierType: "ICCID",
identifiers: ["1234"] } catch (error) {
} console.error('Error al decodificar JSON:', error);
})() // Aquí podrías decidir devolver el string crudo o null
// TODO: comprobar el resultado de la opreacion return undefined;
this.eventBus.ack(msg!)
} }
} }
public async pauseSim(msg: ConsumeMessage | null) { public activateSim() {
return async () => { return async (msg: ConsumeMessage) => {
if (!this.validateActivationMsg(msg)) { if (!this.validateActivationMsg(msg)) {
throw new Error("Error consumiendo el mensaje no es valido") throw new Error("Error consumiendo el mensaje no es valido")
} }
const msgData = Buffer.from(JSON.parse(msg?.content.toString("utf-8") || "{}").data) const msgData = this.decodeMsg(msg) as SimEvents.activation
if (msgData == undefined || msgData.payload == undefined) Promise.reject("Mensaje invalido")
console.log("Mensaje procesado", msgData?.toString())
// TODO: Añadir un validador del mensaje
const iccid = msgData.payload.iccid
try {
// Caso de uso de activaciones
await this.useCases.activate({
dueDate: this.genDueDate(2 * 60).toISOString(),
identifier: {
identifierType: "ICCID",
identifiers: [iccid]
}
})()
this.eventBus.ack(msg)
} catch (e) {
this.eventBus.nack(msg)
}
}
}
public pauseSim() {
return async (msg: ConsumeMessage) => {
if (!this.validateActivationMsg(msg)) {
throw new Error("Error consumiendo el mensaje no es valido")
}
const msgData = this.decodeMsg(msg)
if (msgData == undefined) Promise.reject("Mensaje invalido")
console.log("Mensaje procesado", String(msgData)) console.log("Mensaje procesado", String(msgData))
} }
} }
@@ -54,4 +89,10 @@ export class SimController {
if (msg == undefined) return false; if (msg == undefined) return false;
return true; return true;
} }
private genDueDate(secondsDue: number) {
const now = Date.now() + secondsDue * 1000
const dueDate = new Date(now)
return dueDate
}
} }

View File

@@ -1,48 +1,62 @@
/** /**
* Dirige cada mensaje dependiendo de el tipo de accion que contenga * 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 { ConsumeMessage } from "amqplib";
import { SimController } from "./Sim.controller"; import { SimController } from "./Sim.controller";
export class SimRouter { export class SimRouter {
private simController: SimController private readonly routes: Map<string, (m: ConsumeMessage) => Promise<any>>;
private routeMap: Map<string, (m: ConsumeMessage | null) => void> = new Map() constructor(private readonly simController: SimController) {
this.routes = new Map([
constructor(simController: SimController) { ["activate", this.simController.activateSim()],
this.simController = simController ["pause", this.simController.pauseSim()],
]);
// No me gusta que se defina en el constructor
this.routeMap = new Map([
["activate", this.simController.activateSim],
["pause", this.simController.pauseSim],
])
this.route = this.route.bind(this)
} }
public route(msg: ConsumeMessage | null) { /**
if (msg == undefined) { * Enruta el mensaje a la acción correspondiente basándose en la routing key
console.error("Mensaje vacio") */
public route = async (msg: ConsumeMessage | null): Promise<void> => {
if (!msg) {
console.error("[Router] Mensaje vacío");
return; return;
} }
const routingKey = msg.fields.routingKey const action = this.extractAction(msg);
const action = routingKey.split(".")[2]
if (action == undefined) { if (!action) {
console.error("La routing key no tiene una accion definida") console.error("[Router] La routing key no tiene una acción definida", msg.fields.routingKey);
console.error(msg.fields) return;
} }
const accionEjecutable = this.routeMap.get(action) const handler = this.routes.get(action);
if (accionEjecutable == undefined) { if (!handler) {
console.error("La accion del mensaje no tiene un controlador asociado") console.error(`[Router] La acción '${action}' no tiene un controlador asociado`);
} else { return;
console.log("Ejecutado operacion", action)
} }
try {
console.log("[Router] Ejecutando operación:", action);
// El controlador devuelve una función (thunk) que debe ser ejecutada
const executeParams = await handler(msg);
if (typeof executeParams === "function") {
await executeParams();
}
} catch (error) {
console.error(`[Router] Error al ejecutar la operación '${action}':`, error);
}
};
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

@@ -15,7 +15,7 @@ export class SimUseCases {
} }
public activate(activationData: ActivationData) { public activate(activationData: ActivationData) {
const OPERATION_URL = "/actions/preactivate" const OPERATION_URL = "/actions/preactivateLine"
return async () => { return async () => {
const req = this.httpClient.client.post(OPERATION_URL, { const req = this.httpClient.client.post(OPERATION_URL, {
...activationData ...activationData
@@ -23,7 +23,7 @@ export class SimUseCases {
try { try {
const e = await req const e = await req
console.log("Activacion con exito", e.data) console.log("Activacion con exito", e.data.response)
} catch (error) { } catch (error) {
console.error("Error activando ", error) console.error("Error activando ", error)
} }

View File

@@ -1,27 +0,0 @@
import { ActivationData } from "#domain/DTOs/objeniousapi"
import { HttpClient } from "#shared/infrastructure/HTTPClient"
export class SimActivationUseCase {
private httpClient: HttpClient
constructor(args: {
httpClient: HttpClient
}) {
this.httpClient = args.httpClient
}
public async run(activationData: ActivationData,) {
const req = this.httpClient.client.post("/actions/preactivate", {
...activationData
})
try {
const e = await req
console.log("Activacion con exito", e.data)
} catch (error) {
console.error("Error activando ", error)
}
}
}

View File

@@ -27,7 +27,8 @@ export const env = {
OBJ_AUTHOIZATION: String(process.env.OBJ_ATHORIZATION), OBJ_AUTHOIZATION: String(process.env.OBJ_ATHORIZATION),
OBJ_CLI_ASSERTION: String(process.env.OBJ_CLI_ASSERTION), OBJ_CLI_ASSERTION: String(process.env.OBJ_CLI_ASSERTION),
OBJ_CLIENT_ID: String(process.env.OBJ_CLIENT_ID), OBJ_CLIENT_ID: String(process.env.OBJ_CLIENT_ID),
OBJ_KID: String(process.env.OBJ_KID) OBJ_KID: String(process.env.OBJ_KID),
OBJ_BASE_URL: String(process.env.OBJ_BASE_URL)
}; };

View File

@@ -1,12 +1,13 @@
import { JWTService } from "aplication/JWT.service" import { JWTService } from "aplication/JWT.service"
import { HttpClient } from "#shared/infrastructure/HTTPClient" import { HttpClient } from "#shared/infrastructure/HTTPClient"
import { env } from "./env"
// TODO: mover a shared/infrastructure para usar en el resto de servicios const OBJ_BASE_URL = env.OBJ_BASE_URL
export const httpInstance = new HttpClient({ export const httpInstance = new HttpClient({
baseURL: "https://api-getway.objenious.com/ws/", baseURL: OBJ_BASE_URL,
headers: { headers: {
"content-type": "application/x-www-form-urlencoded" "content-type": " application/json; charset=utf-8"
}, },
jwtManager: new JWTService() jwtManager: new JWTService()
}) })

View File

@@ -10,7 +10,7 @@ const COMPAÑIASICCID = new Map<string, string>(
[ [
["3490", "alai"], ["3490", "alai"],
["3510", "nos"], ["3510", "nos"],
["3399", "objenious"] ["3320", "objenious"]
]) ])
export class SimController { export class SimController {

View File

@@ -46,16 +46,6 @@ export class SimUsecases {
return this.eventBus.publish([activationEvent]) return this.eventBus.publish([activationEvent])
} }
async cancelation(args: { iccid: string }) {
const cancelationEvent = <SimEvents.cancelation>{
key: "sim.cancelation",
payload: {
iccid: args.iccid
}
}
return this.eventBus.publish([cancelationEvent])
}
async pause(args: { iccid: string }) { async pause(args: { iccid: string }) {
const cancelationEvent = <SimEvents.pause>{ const cancelationEvent = <SimEvents.pause>{