diff --git a/docs/sim-api/Test Order.bru b/docs/sim-api/Test Order.bru new file mode 100644 index 0000000..27b0879 --- /dev/null +++ b/docs/sim-api/Test Order.bru @@ -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 +} diff --git a/docs/sim-objenious/Alarmas disponibles.bru b/docs/sim-objenious/Alarmas disponibles.bru new file mode 100644 index 0000000..f363883 --- /dev/null +++ b/docs/sim-objenious/Alarmas disponibles.bru @@ -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 +} diff --git a/packages/sim-entrada-eventos/aplication/Order.controller.ts b/packages/sim-entrada-eventos/aplication/Order.controller.ts new file mode 100644 index 0000000..efed982 --- /dev/null +++ b/packages/sim-entrada-eventos/aplication/Order.controller.ts @@ -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 + * + * Representa el dato original + *

Representa el dato después del mapeo + */ + public controllerGenerator(args: { + validator?: BodyValidator, + mapBody?: (body: O) => P, + useCase: (args: P) => Promise, + 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; + } + + } + } +} diff --git a/packages/sim-entrada-eventos/aplication/Order.usecases.ts b/packages/sim-entrada-eventos/aplication/Order.usecases.ts new file mode 100644 index 0000000..bad367d --- /dev/null +++ b/packages/sim-entrada-eventos/aplication/Order.usecases.ts @@ -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) + } + } + +} diff --git a/packages/sim-entrada-eventos/aplication/Sim.controller.ts b/packages/sim-entrada-eventos/aplication/Sim.controller.ts index d93e903..90af832 100644 --- a/packages/sim-entrada-eventos/aplication/Sim.controller.ts +++ b/packages/sim-entrada-eventos/aplication/Sim.controller.ts @@ -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(args: { validator?: BodyValidator, mapBody?: (body: O) => P, - useCase: (args: P) => Promise, + useCase: (args: P) => Promise, 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) diff --git a/packages/sim-entrada-eventos/aplication/Sim.usecases.ts b/packages/sim-entrada-eventos/aplication/Sim.usecases.ts index 67427b2..1321047 100644 --- a/packages/sim-entrada-eventos/aplication/Sim.usecases.ts +++ b/packages/sim-entrada-eventos/aplication/Sim.usecases.ts @@ -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 = { - 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(event: SimEvents.general): Promise>> { + if (event.headers?.message_id == undefined) { + return >{ + 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 >{ + 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(order) + return result; + + } + + async test(args: { iccid: string }) { + assert(args.iccid != undefined) + const event = { + 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 }) { diff --git a/packages/sim-entrada-eventos/domain/common.ts b/packages/sim-entrada-eventos/domain/common.ts new file mode 100644 index 0000000..94b32f6 --- /dev/null +++ b/packages/sim-entrada-eventos/domain/common.ts @@ -0,0 +1,6 @@ + +export type PaginationArgs = { + limit?: number, + offset?: number, + start?: number +} diff --git a/packages/sim-entrada-eventos/index.ts b/packages/sim-entrada-eventos/index.ts index 67dab26..c484196 100644 --- a/packages/sim-entrada-eventos/index.ts +++ b/packages/sim-entrada-eventos/index.ts @@ -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" }) diff --git a/packages/sim-entrada-eventos/infrastructure/orderRoutes.http.ts b/packages/sim-entrada-eventos/infrastructure/orderRoutes.http.ts new file mode 100644 index 0000000..71f10be --- /dev/null +++ b/packages/sim-entrada-eventos/infrastructure/orderRoutes.http.ts @@ -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 } + + diff --git a/packages/sim-entrada-eventos/infrastructure/simRoutes.http.ts b/packages/sim-entrada-eventos/infrastructure/simRoutes.http.ts index de9a1f3..abc6b7c 100644 --- a/packages/sim-entrada-eventos/infrastructure/simRoutes.http.ts +++ b/packages/sim-entrada-eventos/infrastructure/simRoutes.http.ts @@ -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()) diff --git a/packages/sim-entrada-eventos/package.json b/packages/sim-entrada-eventos/package.json index 98e5824..8191cc4 100644 --- a/packages/sim-entrada-eventos/package.json +++ b/packages/sim-entrada-eventos/package.json @@ -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" diff --git a/packages/sim-shared/domain/DomainEvent.ts b/packages/sim-shared/domain/DomainEvent.ts index 22f28ae..7a6bc62 100644 --- a/packages/sim-shared/domain/DomainEvent.ts +++ b/packages/sim-shared/domain/DomainEvent.ts @@ -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, diff --git a/packages/sim-shared/domain/Order.ts b/packages/sim-shared/domain/Order.ts index 9a58d5b..32b4d24 100644 --- a/packages/sim-shared/domain/Order.ts +++ b/packages/sim-shared/domain/Order.ts @@ -15,6 +15,16 @@ export type OrderType = | 'reactivate' | 'unknown'; +export const OrderTypeOptions = new Set([ + 'activate', + 'preactivate', + 'cancel', + 'pause', + 'reactivate', + 'unknown' +]) + + // Interfaz para la tabla order_tracking export interface OrderTracking { id: number; @@ -31,6 +41,9 @@ export interface OrderTracking { 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, // 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' >; diff --git a/packages/sim-shared/infrastructure/OrderRepository.test.ts b/packages/sim-shared/infrastructure/OrderRepository.test.ts index d859a01..68575dd 100644 --- a/packages/sim-shared/infrastructure/OrderRepository.test.ts +++ b/packages/sim-shared/infrastructure/OrderRepository.test.ts @@ -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) diff --git a/packages/sim-shared/infrastructure/OrderRepository.ts b/packages/sim-shared/infrastructure/OrderRepository.ts index 79f94a5..b294559 100644 --- a/packages/sim-shared/infrastructure/OrderRepository.ts +++ b/packages/sim-shared/infrastructure/OrderRepository.ts @@ -72,31 +72,41 @@ export class OrderRepository { /** * Busqueda según la id de RabbitMq */ - public async getOrderByQueueId(data: { correlation_id: string }, pool?: PoolClient) { + public async getOrderByQueueId(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>(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(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>(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(data: CreateOrderDTO): Promise>> { 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>(query, values) // TODO comprobar si start_date convierte a Date por defecto, añadir enum de status const result = await this.getFirst(queryPromise) diff --git a/packages/sim-shared/infrastructure/RabbitMQEventBus.ts b/packages/sim-shared/infrastructure/RabbitMQEventBus.ts index f3c97c9..ccee6a9 100644 --- a/packages/sim-shared/infrastructure/RabbitMQEventBus.ts +++ b/packages/sim-shared/infrastructure/RabbitMQEventBus.ts @@ -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; } }