Orders con endpoints para monitorizacion

This commit is contained in:
2026-02-25 12:20:52 +01:00
parent c416114c50
commit 02c80cd503
16 changed files with 373 additions and 63 deletions

View File

@@ -0,0 +1,21 @@
meta {
name: Test Order
type: http
seq: 9
}
post {
url: {{baseurl}}/sim/test
body: formUrlEncoded
auth: inherit
}
body:form-urlencoded {
iccid: 8933201125065160999
offer: SAVEFAMILY1
}
settings {
encodeUrl: true
timeout: 0
}

View File

@@ -0,0 +1,38 @@
meta {
name: Alarmas disponibles
type: http
seq: 20
}
get {
url: https://api-getway.objenious.com/ws/alarms
body: formUrlEncoded
auth: bearer
}
auth:bearer {
token: {{ws-access-token-partenaire}}
}
body:json {
{
"identifier": {
"identifiers": ["8933201124059175967"],
"identifierType": "ICCID"
}
}
}
body:form-urlencoded {
~identifier.identifierType: "ICCID"
~identifier.identifiers: ["8933201124059175967"]
}
vars:pre-request {
~id: 5187320
}
settings {
encodeUrl: true
timeout: 0
}

View File

@@ -0,0 +1,110 @@
import { BodyValidator } from "sim-shared/aplication/BodyValidator.js"
import { OrderUsecases } from "./Order.usecases.js"
import { Request, Response } from "express"
import { PaginationArgs } from "#domain/common.js"
export class OrderController {
private orderUseCases: OrderUsecases
constructor(args: {
orderUseCases: OrderUsecases
}) {
this.orderUseCases = args.orderUseCases
}
public getById(args: { id: number }) {
return this.controllerGenerator<{ id: number }, { id: number }>({
validator: undefined,
useCase: this.orderUseCases.getById(args),
onError: (data, error) => { console.error(error) },
onSuccess: (data) => console.log(data)
})
}
public getPending(args: PaginationArgs) {
return this.controllerGenerator<{ id: number }, { id: number }>({
validator: undefined,
useCase: this.orderUseCases.getPending(args),
onError: (data, error) => { console.error(error) },
onSuccess: (data) => console.log(data)
})
}
public getByQueueId(args: { message_id: string }) {
return this.controllerGenerator({
validator: undefined,
useCase: this.orderUseCases.getByQueueId(args),
onError: (data, error) => { console.error(error) },
onSuccess: (data) => console.log(data)
})
}
/**
* TODO:
* - En proceso de validacion, tiene varios problemas
* - Está copiado, planteado inyectarlo
*
* Abstrae el proceso de
* Peticion -> validacion del body -> map del body -> useCase -> OK/ERR
*
* <O> Representa el dato original
* <P> Representa el dato después del mapeo
*/
public controllerGenerator<O extends Object, P extends Object>(args: {
validator?: BodyValidator<O>,
mapBody?: (body: O) => P,
useCase: (args: P) => Promise<any>,
onError: (args: O | P, error: string) => void,
onSuccess: (args: P) => void,
}) {
return async (req: Request, res: Response) => {
const body = req.body
// 1. Validacion del body
try {
if (args.validator != undefined)
args.validator.validate(body)
} catch (e) {
if (args.onError != undefined) args.onError(body, e as string)
res.status(422).json({
errors: {
msg: e
}
})
}
// 2. Transformacion del body
let data: P = body;
try {
if (args.mapBody != undefined)
data = args.mapBody(body)
} catch (e) {
res.status(422).json({
errors: {
msg: "Error parseando el body: " + e
}
})
}
// 3. Aplicacion del UseCase
try {
const usecaseResult = await args.useCase(data)
// 4. Se devuelve al usuario el caso de exito
res.status(200).json(
usecaseResult
).send()
args.onSuccess(data)
} catch (err) {
// 4.1 Error del caso de uso
res.status(500).json({
errors: {
msg: "Error general:" + err
}
}).send()
return;
}
}
}
}

View File

@@ -0,0 +1,37 @@
import { PaginationArgs } from "#domain/common.js";
import { OrderRepository } from "sim-shared/infrastructure/OrderRepository.js";
export class OrderUsecases {
private orderRepository: OrderRepository;
constructor(args: {
orderRepository: OrderRepository
}
) {
this.orderRepository = args.orderRepository
}
public getById(args: {
id: number
}) {
return async () => {
return await this.orderRepository.getOrderById(args)
}
}
public getByQueueId(args: {
message_id: string
}) {
return async () => {
return await this.orderRepository.getOrderByQueueId(args)
}
}
public getPending(args: PaginationArgs & {
}) {
return async () => {
return await this.orderRepository.getPendingOrders(args)
}
}
}

View File

@@ -3,7 +3,6 @@ import { SimUsecases } from "./Sim.usecases.js"
import { activationValidator, iccidValidator } from "./httpValidators.js"
import { companyFromIccid } from "#domain/companies.js"
import { BodyValidator } from "sim-shared/aplication/BodyValidator.js"
import { error } from "node:console"
export class SimController {
@@ -30,7 +29,7 @@ export class SimController {
public controllerGenerator<O extends Object, P extends Object>(args: {
validator?: BodyValidator<O>,
mapBody?: (body: O) => P,
useCase: (args: P) => Promise<void>,
useCase: (args: P) => Promise<any>,
onError: (args: O | P, error: string) => void,
onSuccess: (args: P) => void,
}) {
@@ -66,10 +65,13 @@ export class SimController {
// 3. Aplicacion del UseCase
try {
const usecaseResult = await args.useCase(data)
// 4. Se devuelve al usuario el caso de exito
res.status(200).json(
usecaseResult
).send()
args.onSuccess(data)
} catch (err) {
// 4.1 Error del caso de uso
res.status(500).json({
errors: {
msg: "Error general:" + err
@@ -77,13 +79,14 @@ export class SimController {
}).send()
return;
}
}
}
public preactivationTest() {
return this.controllerGenerator({
public test() {
return this.controllerGenerator<{ iccid: string, offer: string }, { iccid: string }>({
validator: iccidValidator,
useCase: this.simUseCases.test,
useCase: (args) => this.simUseCases.test(args),
onError: (data, error) => console.error(error),
onSuccess: (data) => {
console.log("OK", data)

View File

@@ -1,9 +1,10 @@
import { OrderRepository } from "sim-shared/infrastructure/OrderRepository.js";
import { Result } from "sim-shared/domain/Result.js";
import assert from "node:assert";
import { EventBus } from "sim-shared/domain/EventBus.port";
import { SimEvents } from "sim-shared/domain/SimEvents";
import { uuidv7 } from "uuidv7";
import { CreateOrderDTO, OrderType } from "sim-shared/domain/Order.js";
import { CreateOrderDTO, OrderTracking, OrderType, OrderTypeOptions } from "sim-shared/domain/Order.js";
/**
* Casos de uso de tarjetas sim. Garantiza que todos los metodos usan el mismo bus de mensajes
@@ -22,40 +23,61 @@ export class SimUsecases {
this.orderRepository = args.orderRepository
}
async test(args: { iccid: string }) {
assert(args.iccid != undefined)
private addMessage_id(event: SimEvents.general): SimEvents.general & { headers: { message_id: string } } {
const uuid = uuidv7()
const event = <SimEvents.general>{
key: `sim.test.test`,
payload: {
iccid: args.iccid
},
return {
...event,
headers: {
...event.headers,
message_id: uuid
}
}
}
const publish = await this.eventBus.publish([event])
/**
* TODO:
* De momento solo para mensajes publicados de 1 en 1 y si se les ha añadido cabecera
* Si se ha saltado el proceso de añadir un ID no se
*/
if (publish.success.length == 1) {
if (event.headers?.message_id != undefined) {
const orderType = (event.key.split(".")[2] as OrderType ?? "unknown")
assert(orderType)
const order: CreateOrderDTO = {
correlation_id: event.headers.message_id,
order_type: orderType,
routing_key: event.key,
payload: event
}
this.orderRepository.createOrder(order)
/**
* El tipo T es el tipo del payload del Order
*/
private async saveOrder<T extends any>(event: SimEvents.general): Promise<Result<string, OrderTracking<T>>> {
if (event.headers?.message_id == undefined) {
return <Result<string, any>>{
error: "El evento no tiene una cabecera message_id definido"
}
}
const orderType = (event.key.split(".")[2] as OrderType ?? "unknown")
// Estoy pensando en la posibilidad de pasarlo a unknown
if (!OrderTypeOptions.has(orderType)) {
return <Result<string, any>>{
error: `El evento no tiene un tipo valido: ${orderType} no existe como tipo valido`
}
}
const order: CreateOrderDTO = {
correlation_id: event.headers.message_id,
order_type: orderType,
routing_key: event.key,
payload: event
}
const result = await this.orderRepository.createOrder<T>(order)
return result;
}
async test(args: { iccid: string }) {
assert(args.iccid != undefined)
const event = <SimEvents.general>{
key: `sim.test.unknown`,
payload: {
iccid: args.iccid
}
}
const eventWithId = this.addMessage_id(event)
const publish = await this.eventBus.publish([eventWithId])
await this.saveOrder(eventWithId)
return eventWithId
}
/**
@@ -85,8 +107,11 @@ export class SimUsecases {
offer: args.offer
}
}
console.log("[d] Activation ", activationEvent)
return this.eventBus.publish([activationEvent])
const activationWithId = this.addMessage_id(activationEvent)
console.log("[d] Activation ", activationWithId)
await this.eventBus.publish([activationWithId])
this.saveOrder(activationWithId)
}
async preActivation(args: { iccid: string, compañia: string }) {

View File

@@ -0,0 +1,6 @@
export type PaginationArgs = {
limit?: number,
offset?: number,
start?: number
}

View File

@@ -3,6 +3,7 @@ import cors from 'cors';
import { simRoutes } from "./infrastructure/simRoutes.http.js"
import { rabbitmqEventBus } from '#config/eventBusConfig.js';
import { env } from "#config/env/index.js"
import { orderRoutes } from "#adapters/orderRoutes.http.js";
const PORT = env.API_PORT
const HOSTNAME = "0.0.0.0"
@@ -24,6 +25,7 @@ app.use(express.json());
app.use(express.urlencoded({ extended: true }));
app.use("/sim", simRoutes)
app.use("/orders", orderRoutes)
app.get("/health", (req, res) => {
res.status(200).json({ status: "ok" })

View File

@@ -0,0 +1,31 @@
/**
* Rutas para consultar el estado de los order
*/
import { OrderRepository } from "sim-shared/infrastructure/OrderRepository.js"
import { Router } from "express"
import { postgresClient } from '#config/postgreConfig.js';
const orderRoutes = Router()
// orderRepository no se trata como singleton
const orderRepository = new OrderRepository(postgresClient)
/**
* Todas las orders, o un resumen, admite filtros
* por:
* - status
* - fecha inicio
* - fecha fin
* - pendientes
* */
orderRoutes.get("/")
/** Order por id (uuid del mensaje) */
orderRoutes.get("/{id}")
orderRoutes.get("/{status}")
export { orderRoutes }

View File

@@ -30,6 +30,8 @@ simRoutes.post("/pause", simController.pause())
simRoutes.post("/cancel", simController.cancelation())
simRoutes.post("/test", simController.test())
// Proceso especifico de ALAI para liberar sims canceladas
simRoutes.post("/free", simController.free())

View File

@@ -13,12 +13,6 @@
"types": "./config/*.ts",
"default": "./config/*.js"
},
"#shared/*.js": {
"default": "../sim-shared/*.js"
},
"#shared/*": {
"default": "../sim-shared/*.js"
},
"#adapters/*.js": {
"types": "./infrastructure/*.ts",
"default": "./infrastructure/*.js"

View File

@@ -7,8 +7,8 @@ export type DomainEventType = string
export type DomainEvent = {
key: string,
payload: Object,
headers?: Object & {
payload: object,
headers?: object & {
message_id?: string
},
occurredOn?: Date,

View File

@@ -15,6 +15,16 @@ export type OrderType =
| 'reactivate'
| 'unknown';
export const OrderTypeOptions = new Set<OrderType>([
'activate',
'preactivate',
'cancel',
'pause',
'reactivate',
'unknown'
])
// Interfaz para la tabla order_tracking
export interface OrderTracking<T> {
id: number;
@@ -31,6 +41,9 @@ export interface OrderTracking<T> {
start_date: string | Date;
update_date: string | Date;
finish_date?: string | Date | null;
// desde la 1.1.0
webhook_host?: string | null;
webhook_endpoint?: string | null;
}
// Interfaz para la tabla order_history
@@ -46,5 +59,5 @@ export interface OrderHistory {
// Tipo útil para la creación (Omitiendo campos generados por la DB)
export type CreateOrderDTO = Pick<
OrderTracking<any>, // Aqui realmente no importan los campos
'correlation_id' | 'exchange' | 'routing_key' | 'order_type' | 'payload'
'correlation_id' | 'exchange' | 'routing_key' | 'order_type' | 'payload' | 'webhook_host' | 'webhook_endpoint'
>;

View File

@@ -63,7 +63,7 @@ describe("Test OrderRepository", {}, (ctx) => {
})
it("Find by correlation id should return a valid order", async () => {
const result = await orderRepo.getOrderByQueueId({ correlation_id: order1.correlation_id })
const result = await orderRepo.getOrderByQueueId({ message_id: order1.correlation_id })
assert(result.error == undefined)
assert(result.data != undefined)

View File

@@ -72,31 +72,41 @@ export class OrderRepository {
/**
* Busqueda según la id de RabbitMq
*/
public async getOrderByQueueId<T>(data: { correlation_id: string }, pool?: PoolClient) {
public async getOrderByQueueId<T>(data: { message_id: string }, pool?: PoolClient) {
const query = `
SELECT * FROM order_tracking
WHERE correlation_id = $1
`
const values = [data.correlation_id]
const values = [data.message_id]
const queryPromise = this.pgClient.query<OrderTracking<T>>(query, values)
const result = await this.getFirst(queryPromise);
return result
}
/**
*/
* Operaciones que no han concluido con filtros de limit, offset y start
* @param options ()
* @returns
*/
public async getPendingOrders<T>(options?: {
limit?: number
limit?: number,
offset?: number,
start?: number // id de inicio
}) {
const client = await this.pgClient.connect();
const offsetFragment = (options?.offset != undefined) ? `OFFSET ${options?.offset}` : ""
const limitFragment = (options?.limit != undefined) ? `LIMIT ${options?.limit}` : ""
const startFragment = (options?.start != undefined) ? `AND id >= ${options?.start}` : ""
const query = `
SELECT * FROM order_tracking
WHERE finish_date IS NULL
WHERE finish_date IS NULL
${startFragment}
ORDER BY start_date ASC
${offsetFragment}
${limitFragment}
`
if (options?.limit != undefined) {
}
const values: string[] = []
const queryPromise = client.query<OrderTracking<T>>(query, values)
const result = await this.getAll(queryPromise)
@@ -104,7 +114,7 @@ export class OrderRepository {
return result
}
public async createOrder(data: CreateOrderDTO) {
public async createOrder<T extends any>(data: CreateOrderDTO): Promise<Result<string, OrderTracking<T>>> {
const client = await this.pgClient.connect();
await client.query("BEGIN")
const query = `
@@ -114,7 +124,9 @@ export class OrderRepository {
routing_key,
order_type,
payload,
status
status,
webhook_host,
webhook_endpoint
)
VALUES (
$1, -- correlation_id
@@ -122,12 +134,23 @@ export class OrderRepository {
$3, -- routing_key
$4, -- order_type (ej: 'activate')
$5, -- payload (json object)
'pending'
'pending',
$6, -- webhook_host,
$7 -- webhook_endpoint
)
RETURNING id, correlation_id, status, start_date;
RETURNING
id,
correlation_id,
exchange,
routing_key,
order_type,
payload,
status,
webhook_host,
webhook_endpoint
`
const values = [data.correlation_id, data.exchange, data.routing_key, data.order_type, data.payload]
const queryPromise = client.query<{ id: number, correlation_id: string, status: string, start_date: string }>(query, values)
const values = [data.correlation_id, data.exchange, data.routing_key, data.order_type, data.payload, data.webhook_host, data.webhook_endpoint]
const queryPromise = client.query<OrderTracking<T>>(query, values)
// TODO comprobar si start_date convierte a Date por defecto, añadir enum de status
const result = await this.getFirst(queryPromise)

View File

@@ -107,20 +107,24 @@ export class RabbitMQEventBus implements EventBus {
const exchange = "sim.exchange"
const routingKey = event.key
const content = Buffer.from(JSON.stringify(event))
await this.channel?.publish(exchange, routingKey, content, {
const isPublished = await this.channel?.publish(exchange, routingKey, content, {
headers: {
...event.headers
}
}, (err, ok) => {
if (err == undefined) {
console.log("Evento publicado ", event)
successEvents.push(event)
} else {
console.error("Error publicando", event)
errorEvents.push(event)
}
})
// Hay que revisarlo pero en principio la libreria se encarga que el mensaje se publique
// si o si
successEvents.push(event)
}
return res({
success: successEvents,
error: errorEvents
@@ -168,7 +172,8 @@ export class RabbitMQEventBus implements EventBus {
if (this.connection == undefined) throw new Error("[RMQ] Intentando crear un canal sin una conexion")
const channel = this.connection.createChannel({
setup: async (channel: Channel) => {
confirm: true,
setup: async (channel: ConfirmChannel) => {
// Exchanges comunes a todos
channel.assertExchange("sim.exchange", "topic", { durable: true })
channel.assertExchange("sim.dlx", "topic", { durable: true })
@@ -202,6 +207,6 @@ export class RabbitMQEventBus implements EventBus {
Promise.reject(error);
});
return channel as ChannelWrapper;
return channel;
}
}