Docs orders

This commit is contained in:
2026-04-27 09:33:55 +02:00
parent 9ec127433d
commit 4e02ea021d
32 changed files with 349 additions and 40 deletions

View File

@@ -24,7 +24,6 @@ CREATE TABLE IF NOT EXISTS order_tracking (
payload JSONB, -- Duda si es optimo guardar la copia, es útil en caso de fallo
-- Campos de reintentos?
status order_status NOT NULL DEFAULT 'pending',
retry_count INT DEFAULT 0,
error_message TEXT, -- Razón del fallo

51
docs/sim-alai/Login.yml Normal file
View File

@@ -0,0 +1,51 @@
info:
name: Login
type: http
seq: 2
http:
method: POST
url: "{{baseurl}}/v1/auth/login"
body:
type: json
data: |-
{
"username": "{{username}}",
"password": "{{password}}",
"brandID": "{{brandId}}"
}
auth: inherit
runtime:
scripts:
- type: after-response
code: |-
const data = res.getBody();
if (data.staus != 200) {
console.error("Error de login: ", data)
return 1;
}
if (data && data.accessToken) {
bru.setEnvVar("alai_token", data.accessToken);
if (data.tokenType) {
bru.setEnvVar("alai_token_type", data.tokenType);
}
console.log("Token guardado correctamente");
} else {
console.error("No se pudo encontrar el accessToken en la respuesta");
}
settings:
encodeUrl: true
timeout: 0
followRedirects: true
maxRedirects: 5
docs: |-
Necesita un certificado p12 (PFX) y la contraseña asociada para efectuar la operacion.
Collection Settings => ClientCertificates => Add Certificate

View File

@@ -0,0 +1,15 @@
info:
name: New Order
type: http
seq: 3
http:
method: POST
url: "{{baseurl}}/v1/order"
auth: inherit
settings:
encodeUrl: true
timeout: 0
followRedirects: true
maxRedirects: 5

Binary file not shown.

View File

@@ -2,6 +2,6 @@ name: local
color: "#2E8A54"
variables:
- name: baseurl
value: http://localhost:3001
value: http://localhost:3002
- secret: true
name: token

View File

@@ -2,6 +2,12 @@ name: prod
color: "#CE4F3B"
variables:
- name: baseurl
value: https://nosconnectcenter-api.iot-x.com
value: https://wsaccess.alaisecure.com/bssrest
- name: username
value: palomaibanez
- name: password
value: palomaibanez123
- secret: true
name: token
name: certPasswd
- name: brandId
value: savefamily

View File

@@ -2,6 +2,22 @@ opencollection: 1.0.0
info:
name: sim-alai
config:
proxy:
inherit: true
config:
protocol: http
hostname: ""
port: ""
auth:
username: ""
password: ""
bypassProxy: ""
clientCertificates:
- domain: wsaccess.alaisecure.com
type: pkcs12
pkcs12FilePath: certificates\alai_cert.p12
passphrase: iHaaek+zyzWz6cH6rg==
bundled: false
extensions:
bruno:

View File

@@ -6,16 +6,80 @@ meta {
post {
url: {{baseurl}}/sim/activate
body: formUrlEncoded
body: json
auth: inherit
}
body:json {
{
"iccid": "1234"
}
}
body:form-urlencoded {
iccid: 8933201125065156057
offer: SAVEFAMILY1
iccid: 123
offer: mensual
}
settings {
encodeUrl: true
timeout: 0
}
docs {
Campos de entrada:
```ts
// Header requerido
// > content-type:application/x-www-form-urlencoded
// > content-type:application/json
// Cualquiera de los 2 es valido
// Esquema body
{
iccid: string,
offer: "mensual" | "anual" | "SAVEFAMILY1" | "SAVEFAMILY2"
webhook?: string,
}
```
En el campo `offer` "mensual" equivale a "SAVEFAMILY2" y "anual" a "SAVEFAMILY1" porque se mantien los códigos de Oferta de Objenious por compatibilidad pero se espera usar "mensual" y "anual" y hacer la conversión en el servicio de cada proveedor.
Para las llamadas al webhook se va a usar siempre el metodo `POST`, ahora mismo no se firman los mensajes. Se introduce la URL completa tal que `https://dominion.com/v1/endpoint`.
Respuestas:
- **200**: OK
``` ts
// Esquema
{
iccid: string,
operation: string,
message_id: string, //uuidv7
}
```
``` json
// Ejemplo
{
"iccid": "89332011250651xxxxx",
"operation": "activation",
"message_id": "019dbeaf-8abb-7783-8b51-94fbd9f0b0df"
}
```
*iccid*: Confirmación del iccid enviado.
*operation*: Confirmación de la operacion que se ha aplicado.
*message_id*: Id de la operación, para consultar en orders.
> A futuro se va a incluir un campo `"ref":[]` para añadir los enlaces a las consultas de la operación. El body va a permitir tambien json.
- **402**: Algún campo es incorrecto
Se indica que campo es incorrecto, si hubiese mas de uno solo aparecería el primero en comprobarse.
```json
"errors": {
"msg": "La longitud del iccid es incorrecta debera ser de 19 caracteres",
"field": "iccid"
}
```
- **500**: Error general
Ha ocurrido un error imprevisto durante la
}

View File

@@ -1,7 +1,7 @@
meta {
name: Activation Email
type: http
seq: 6
seq: 7
}
post {

View File

@@ -1,7 +1,7 @@
meta {
name: Cancel
type: http
seq: 1
seq: 3
}
post {

View File

@@ -1,7 +1,7 @@
meta {
name: Docs
type: http
seq: 12
seq: 10
}
get {

View File

@@ -1,7 +1,7 @@
meta {
name: Health
type: http
seq: 5
seq: 6
}
get {

View File

@@ -1,7 +1,7 @@
meta {
name: Get pending orders
type: http
seq: 11
seq: 10
}
get {

View File

@@ -5,7 +5,7 @@ meta {
}
get {
url: {{baseurl}}/orders/
url: {{baseurl}}/orders/019dbeaf-8abb-7783-8b51-94fbd9f0b0df
body: none
auth: inherit
}

View File

@@ -5,15 +5,11 @@ meta {
}
get {
url: {{baseurl}}/orders/message_id/019c93d3-014a-711d-b958-03dd629be78d
url: {{baseurl}}/orders/message_id/019dbeaf-8abb-7783-8b51-94fbd9f0b0df
body: none
auth: inherit
}
params:query {
~message_id: 019c93d3-014a-711d-b958-03dd629be78d
}
settings {
encodeUrl: true
timeout: 0

View File

@@ -0,0 +1,44 @@
meta {
name: Orders
}
auth {
mode: inherit
}
docs {
# Orders
Los *order* representan ordenes que se hacen al servidor y representan en que estado se encuentran las peticiones. Los *order* se generan cuando se solicita una operacion y devuelven su identificador en el campo `message_id` de todas las respuestas a peticiones que requieran cambios. Los identificadores de `order` son UUIDv7, aunque tambien tienen asociado un id tradicional BIGINT en la BDD.
## Ciclo de vida
Cuando se crea un *order* comienza en estado `pending`, inicando que ha entrado en la cola y está pendiente de iniciarse; una vez se ha consumido por un servicio pasa a estado `running` indicando que la operacion asociada al *order* ha comenzado, el order continuara en este estado durante un tiempo indefinido (pueden pasar semanas para algunos casos), hasta que la tara finalize correctamente o con errores. En el caso que la tarea finalize con éxito el *order* pasará a estado `finished`, en caso de que haya habido un error el estado será `failed` y se almacenará el error en los campos `error_message` y opcionalemente en `error_stacktrace` según gravedad del error.
- Caso normal
`pending` -> `running` -> `finished`
- Error durante el consumo
`pending` -> `failed`
- Error durante la operacion
`pending` -> `running` -> `failed`
## Endpoints
Estan sujetos a cambios en cuanto a mostrar información
- [WIP]**GET** /orders?{query}
Devuelve todos los orders con un campo que tenga el valor especificado en la query
- **GET** /orders/{id}
Devuelve el order objetivo según su UUID de mensaje (No según el uuid de mensaje)
- **GET** /orders/base_id/{id}
Devuelve el id según su id de la bdd, no es el metodo normal de usar la api
- **GET** /orders/pending
Devuelve todas las order que no hayan finalizado
}

View File

@@ -1,7 +1,7 @@
meta {
name: Pause
type: http
seq: 1
seq: 4
}
post {

View File

@@ -1,7 +1,7 @@
meta {
name: Preactivate
type: http
seq: 1
seq: 5
}
post {

View File

@@ -1,7 +1,7 @@
meta {
name: ReActivate
type: http
seq: 13
seq: 11
}
post {

View File

@@ -1,5 +1,6 @@
docs {
Los endpoint tienen unos campos comunes de entrada:
Todos los endpoint tienen unos campos comunes de entrada:
```ts
{
iccid: string,

View File

@@ -1,7 +1,7 @@
meta {
name: test proxy
type: http
seq: 14
seq: 12
}
get {

View File

@@ -1,6 +1,6 @@
import { ConsumeMessage } from "amqplib";
import { Request, Response } from "express"
import { SimNosUsecases } from "./SimNOS.usecases.js";
import { SimAlaiUsecases } from "./SimAlai.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";
@@ -9,7 +9,7 @@ import { iccidValidator } from "./httpValidators.js";
export class SimAlaiController {
constructor(
private uscases: SimNosUsecases,
private uscases: SimAlaiUsecases,
private eventBus: EventBus,
) {
}
@@ -51,12 +51,12 @@ export class SimAlaiController {
await this.eventBus.ack(msg)
return result
} else {
console.error("Error procesando el caso de uso (NOS)", result.error)
console.error("Error procesando el caso de uso (Alai)", result.error)
this.eventBus.nack(msg)
return result
}
} catch (e) {
console.error("Error general procesando el caso de uso (NOS)")
console.error("Error general procesando el caso de uso (Alai)")
this.eventBus.nack(msg)
return {
error: String(e)

View File

View File

@@ -32,14 +32,23 @@ export class OrderController {
}
public getByQueueId() {
return this.controllerGenerator<{ correlation_id: string }, { correlation_id: string }>({
return this.controllerGenerator<{ uuid: string }, { correlation_id: string }>({
validator: uuidValidator,
mapBody: (e) => ({ correlation_id: e.uuid }),
useCase: this.orderUseCases.getByQueueId(),
onError: (data, error) => { console.error(error) },
onSuccess: (data) => console.log(data)
})
}
public getByQuery() {
return this.controllerGenerator({
validator: undefined,
useCase: this.orderUseCases.getByQuery(),
onError: (data, error) => { console.error(error) },
onSuccess: (data) => console.log(data)
})
}
/**
* TODO:
@@ -77,7 +86,7 @@ export class OrderController {
})
}
// 2. Transformacion del body
// 2. Transformacion del body O => P
let data: P = body;
try {
if (args.mapBody != undefined)

View File

@@ -1,4 +1,5 @@
import { PaginationArgs } from "#domain/common.js";
import { OrderQuery } from "sim-shared/domain/Order.js";
import { OrderRepository } from "sim-shared/infrastructure/OrderRepository.js";
@@ -36,4 +37,10 @@ export class OrderUsecases {
}
}
// WIP
public getByQuery() {
return async (args: OrderQuery) => {
return await this.orderRepository.getOrdersByQuery(args)
}
}
}

View File

@@ -32,10 +32,10 @@ const offerExists = <Validator<{ offer: string }>>{
validationFunc: (a: { offer: string }) => offers.has(a.offer),
}
const isUuidv7 = <Validator<{ correlation_id?: string }>>{
field: "correlation_id",
const isUuidv7 = <Validator<{ uuid?: string }>>{
field: "uuid",
errorMsg: "El uuid no es un uuidv7 valido",
validationFunc: (a) => a.correlation_id != undefined && a.correlation_id.length < 36
validationFunc: (a) => a.uuid != undefined && a.uuid.length < 36
}
const definedId = <Validator<{ id?: number }>>{
@@ -73,7 +73,7 @@ export const iccidValidator = new BodyValidator<{ iccid: string }>(
]
)
export const uuidValidator = new BodyValidator<{ correlation_id?: string }>([
export const uuidValidator = new BodyValidator<{ uuid?: string }>([
isUuidv7
])

View File

@@ -28,13 +28,17 @@ const orderController = new OrderController({
* */
orderRoutes.get("/", (req, res) => { res.send("ok") })
orderRoutes.get("/message_id/:correlation_id", orderController.getByQueueId())
/*
* Ahora es el id de bdd
* */
orderRoutes.get("/message_id/:id", orderController.getById())
/** Operaciones pendientes */
orderRoutes.get("/pending", orderController.getPending())
/** Order por id (uuid del mensaje) */
orderRoutes.get("/:id", orderController.getById())
// TODO: falla
orderRoutes.get("/:id", orderController.getByQueueId())
export { orderRoutes }

View File

@@ -9,8 +9,7 @@ export const connectionsRoutes = Router()
const CONNECTIONS_URL = env.CONNECTIONS_URL// TODO: Meter al ENV
//const CONNECTIONS_URL = "http://sf-nfc-server.savefamilygps.net"
console.log("CONNURL: ", CONNECTIONS_URL)
//console.log("CONNURL: ", CONNECTIONS_URL)
connectionsRoutes.use("", createProxyMiddleware({
target: CONNECTIONS_URL,
changeOrigin: true,

View File

@@ -94,4 +94,19 @@ export type ErrorOrderDTO =
stackTrace?: string
}
/*
* Se considera cada entrada de conditions como un filtro sobre un campo
* cada fila se podrá expresar como campo:filtro
* ```json
* {
* "value": "-gte 200" // Un valor >= 200
* "text": "-eq 'busqueda' " // El campo tiene que ser exactamente 'busqueada'
* }
* ```
* TODO: sacar opciones de paginación
* */
export type OrderQuery = {
conditions: Record<string, string>,
limit?: number | undefined,
offset?: number | undefined,
}

View File

@@ -1,8 +1,9 @@
import { before, describe, it } from "node:test";
import { OrderRepository } from "./OrderRepository.js";
import { CreateOrderDTO } from "../domain/Order.js";
import { CreateOrderDTO, OrderQuery } from "../domain/Order.js";
import { postgresClient } from "../config/config.test.js";
import assert from "node:assert";
import { Query } from "pg";
const order1 = <CreateOrderDTO>{
correlation_id: "fakeRMQid-1234",
@@ -169,4 +170,16 @@ describe("Test OrderRepository", {}, (ctx) => {
assert(result.data.status === "dlx")
assert(result.data.finish_date != null)
})
it("Query generates with parameters", async () => {
const params: OrderQuery = {
conditions: {
status: "-eq 'pending'"
}
}
//@ts-expect-error
const res = orderRepo.generateTableQuery("test", params)
console.log("Query:", res)
assert.ok(res != undefined)
})
})

View File

@@ -2,7 +2,7 @@
* TODO: Usar
*/
import { PoolClient, QueryResult, QueryResultRow } from "pg";
import { CreateOrderDTO, ErrorOrderDTO, FinishOrderDTO, OrderTracking, UpdateOrderDTO } from "../domain/Order.js";
import { CreateOrderDTO, ErrorOrderDTO, FinishOrderDTO, OrderQuery, OrderTracking, UpdateOrderDTO } from "../domain/Order.js";
import { Result, tryCatch } from "../domain/Result.js";
import { PgClient } from "./PgClient.js";
import assert from "node:assert";
@@ -55,7 +55,77 @@ export class OrderRepository {
}
}
/**
* Mapeo de prefijos a operadores SQL
*/
private OPERATOR_MAP: Record<string, string> = {
"-eq": "=",
"-neq": "!=",
"-gt": ">",
"-gte": ">=",
"-lt": "<",
"-lte": "<=",
"-like": "LIKE",
};
/**
* Tabla general para sacar datos de la tabla en base a unas condiciones
* TODO:
* - Dar la opción de generar los campos a devolver en vez de *
* - Garantizar el numero de parametros de respuesta
*/
private generateTableQuery(table: string, query: OrderQuery) {
const { conditions, limit, offset } = query;
const whereClauses: string[] = [];
const queryValues: any[] = [];
let paramIndex = 1; // Para los parametros de PostgreSQL ($1, $2) (que empiezan por 1)
for (const [column, filter] of Object.entries(conditions)) {
const match = filter.match(/^(-\w+)\s+(.+)$/);
if (match) {
const [_, prefix, value] = match;
const operator = this.OPERATOR_MAP[prefix];
if (operator) {
// Eliminación de comillas
const cleanValue = value.replace(/^'|'$/g, "");
whereClauses.push(`${column} ${operator} $${paramIndex}`);
queryValues.push(operator === "LIKE" ? `%${cleanValue}%` : cleanValue);
paramIndex++;
}
}
}
// 2. Query completa
// TODO: Cambair el * por parametros
let sql = `SELECT * FROM ${table}`;
if (whereClauses.length > 0) {
sql += ` WHERE ${whereClauses.join(" AND ")}`;
}
// 3. Paginacion
if (limit !== undefined) {
sql += ` LIMIT ${Number(limit)}`;
}
if (offset !== undefined) {
sql += ` OFFSET ${Number(offset)}`;
}
return {
sql,
values: queryValues,
};
}
public async getOrdersByQuery(args: OrderQuery) {
const query = this.generateTableQuery('order_tracking', args)
const queryPromise = this.pgClient.query<OrderTracking<Record<string, any>>>(query.sql, query.values)
const result = await this.getAll(queryPromise)
}
/**
* El tipo <T> representa el contenido del mensaje de los order