70 Commits

Author SHA1 Message Date
e1450c6e97 POST -> PATCH 2026-04-22 15:22:37 +02:00
e40a19bbfb Bug de correlation_id undefined 2026-04-22 13:31:24 +02:00
fbdb64f3a1 Arreglo de bugs ordes 2026-04-22 12:59:23 +02:00
9a29f49669 Fix orders nos 2026-04-22 12:31:46 +02:00
c2081191ae Trazabilidad de nos, arreglo de orders 2026-04-21 17:39:09 +02:00
f0f3827fd0 Registro del estado/resultado de las operaciones de NOS 2026-04-21 15:51:16 +02:00
ee8f84bc57 Typescript 6 2026-04-21 13:33:01 +02:00
f95677d503 Error pageNOS 2026-04-21 12:50:54 +02:00
59b0b57ec2 Merge branch 'main' into WEBINT-334-migracion-nos 2026-04-21 10:47:08 +02:00
9174b0b6a4 Nombres de colas 2026-04-21 10:22:53 +02:00
e62c49ce91 Endpoints NOS 2026-04-21 10:11:21 +02:00
32990b4dcd Controladores y rutas 2026-04-17 15:49:53 +02:00
da2413002b Repositorio de nos completo 2026-04-17 14:06:41 +02:00
fdbb81ba64 Inicio port NOS 2026-04-16 17:46:32 +02:00
964ea6add9 Inicio migracion NOS 2026-04-16 12:44:31 +02:00
602878acf4 console log de debug 2026-04-16 12:01:31 +02:00
0aa52feaac Proxy 2026-04-16 11:51:30 +02:00
15b70309da Fix networkStatus 2026-04-15 15:11:13 +02:00
7001fccbf7 Problema de creacion de operacion de pausa/cancelacion 2026-04-15 13:50:20 +02:00
cffee785b2 logs 2026-04-15 12:48:08 +02:00
33d260310c Reactivate cuando la linea esté suspendida 2026-04-15 12:47:26 +02:00
e359acc1d5 Validaciones pausa instantaneas 2026-04-15 11:47:33 +02:00
bb4bce4a6d fix 2026-04-15 11:11:55 +02:00
eac74ef0cd Error key evento 2026-04-15 10:31:21 +02:00
1dc4eb5648 validador 2026-04-15 10:28:52 +02:00
a35a6c2b60 Error endpoint 2026-04-15 10:27:58 +02:00
1f78f4a3e1 Periodo pruebas reducidio 2026-04-15 10:18:49 +02:00
1e98559f3a Fix 2026-04-15 10:17:36 +02:00
ef0f860b9d Erro handler 2026-04-14 16:20:15 +02:00
0bff55379f tab 2026-04-14 16:15:01 +02:00
4d34308a13 Bug de logueo de operacion 2026-04-14 16:07:17 +02:00
70bf73b0a4 Error query 2026-04-10 11:11:12 +02:00
e3849d8217 Archivos de migraciones 2026-04-09 12:48:36 +02:00
d9854a12a8 Version de migrate 2026-04-09 12:31:06 +02:00
48d387a8da Migraciones antes de lanzar start 2026-04-09 12:10:40 +02:00
93d3e13793 Merge pull request 'WEBINT-328-Pausas-cacelaciones' (#1) from WEBINT-328-Pausas-cacelaciones into main
Reviewed-on: #1
2026-04-09 10:03:47 +00:00
031f5d5cf0 nombre de columna con mayus 2026-04-09 11:59:06 +02:00
047669bab2 Acabados de corregir bugs 2026-04-09 11:53:49 +02:00
5ea5939e3a Bug de finaliazacion de tareas erroneas 2026-04-09 09:08:11 +02:00
7ff3f13af4 Funcionan las suspensiones 2026-04-08 17:37:47 +02:00
a9589f578b Solucionado cierrre de pool para test 2026-04-08 14:47:57 +02:00
a27e4b30d2 Cron completo y mejora de logs 2026-04-08 13:48:57 +02:00
4168949b9e Endpoint preparados 2026-04-08 10:08:54 +02:00
e6ff54a15d Usecases 2026-04-07 17:43:17 +02:00
3956797020 Las operaciones basicas del repositorio de pause/cancel funcionan y
tienen test
2026-04-07 15:40:19 +02:00
7d88359263 Refactor de jwt y base de la bdd de pausas-cancelaciones 2026-04-07 13:20:31 +02:00
1b6da651a6 Ajustado el periodo de comprobaciones 2026-03-27 12:47:10 +01:00
9b305f887f Test .env ajustado 2026-03-27 12:24:20 +01:00
9506b9e28e Error de nombre de activacion 2026-03-27 10:59:15 +01:00
61c0edca07 Logs del envio 2026-03-27 10:52:03 +01:00
9470b5605d Pribando el env 2026-03-27 10:50:03 +01:00
9d63d23754 Mejor gestion de errores para los order 2026-03-26 12:21:28 +01:00
a95655a2a6 Completada la tarea de volcado 2026-03-26 09:29:09 +01:00
025801a689 Repositorio de lineas funciona 2026-03-25 11:51:14 +01:00
28880c4d99 Lineas activas e insertar cada una 2026-03-24 17:27:52 +01:00
5bb3bc554b doc 2026-03-24 11:20:59 +01:00
cfb907b840 Copy en jenkins 2026-03-11 12:34:41 +01:00
d5d7953fd2 Endpoint para documentacion 2026-03-11 12:31:17 +01:00
96298aab25 Docs en HTML 2026-03-11 11:35:16 +01:00
c17cca1e81 Sobreescribia el registerSobreescribia el register 2026-03-10 10:43:56 +01:00
7264efcf79 Errata 2026-03-10 10:42:32 +01:00
8934bcd603 Copia yarnrc 2026-03-10 10:39:39 +01:00
bdd08dbc56 Copiar yarnrc a docker 2026-03-10 10:37:26 +01:00
7d47fde806 Solucionado problema db-migrate 2026-03-10 10:21:53 +01:00
ad207fb732 db-migrate 2026-03-10 09:34:17 +01:00
bd9081b5bc hardcodeado el customerAccountCode 2026-03-06 11:18:30 +01:00
a429e9d14a Errata customer 2026-03-06 11:13:47 +01:00
81eb986313 Error de tipado 2026-03-06 11:06:15 +01:00
58bedc42f1 Bug de correlation_id en las llamadas a objenious 2026-03-06 11:02:18 +01:00
b97f422261 Prod 2026-03-05 10:33:38 +01:00
105 changed files with 4407 additions and 1125 deletions

5
.env
View File

@@ -20,10 +20,13 @@ POSTGRES_DB=postgres
POSTGRES_DATABASE=postgres
POSTGRES_PORT=5433
POSTGRES_USER=postgres
POSTGRES_PASSWORD=1234
POSTGRES_PASSWORD='1234'
# Para el postgres local para generar el script de resultado de migraciones
PGHOST=localhost
PGUSER=alvar
PGPASSWORD=alvar
PGPORT=5433
# Proxy
CONNECTIONS_URL=https://sim-connections.savefamilygps.net

View File

@@ -4,4 +4,8 @@ enableGlobalCache: false
nodeLinker: node-modules
npmRegistryServer: "https://git.savefamilygps.net/api/packages/alvarsanmartin/npm/"
npmScopes:
sf-alvar:
npmRegistryServer: "https://git.savefamilygps.net/api/packages/SaveFamily/npm/"
npmRegistryServer: "https://registry.npmjs.org/"

View File

@@ -1,10 +1,3 @@
#/bin/bash
rm deployment/database/init.sql
# cat deployment/database/*.sql >deployment/database/init.sql
cp deployment/database/esquema_final* deployment/database/init.sql
# compatibilidad con postgresql < 17
sed -i '/\\restrict/d' deployment/database/init.sql
sed -i '/\\unrestrict/d' deployment/database/init.sql
docker compose -f deployment/local/docker/docker-compose.yaml --project-directory ./ build

View File

@@ -0,0 +1,20 @@
CREATE table if not exists objenious_lines (
id INT PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
simId BIGINT UNIQUE,
status TEXT,
iccid TEXT NOT NULL,
msisdn TEXT,
imei TEXT,
imeiChangeDate TIMESTAMPTZ,
offerCode TEXT,
preactivationDate TIMESTAMPTZ, -- No viene con hora
activationDate TIMESTAMPTZ,
commercialStatus TEXT,
commercialStatusDate TIMESTAMPTZ,
billingStatus TEXT,
billingStatusChangeDate TIMESTAMPTZ,
billingActivationDate TIMESTAMPTZ,
createDate TIMESTAMPTZ,
raw JSONB,
hash TEXT
)

View File

@@ -0,0 +1,32 @@
/**
* Para la tarea WEBINT-328-Pausas-cacelaciones.
* Almacena las pausas/cancelaciones que no se han podido hacer porque la linea esta en
* "Test"
*/
DO $$ BEGIN
CREATE TYPE SUSPENDTERMINATE AS ENUM ('suspend','terminate');
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
CREATE TABLE IF NOT EXISTS pause_cancel_tasks (
id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
iccid TEXT NOT NULL,
operation_type SUSPENDTERMINATE,
last_checked TIMESTAMPTZ, -- Última vez que se ha comprobado que no esté en test
activation_date TIMESTAMPTZ, -- Fecha de activacion para comprobar si ha pasdo un mes
next_check TIMESTAMPTZ, -- Si se ha comprobado se asignará la siguiente fecha de revision
completed_date TIMESTAMPTZ, -- Cuando se ha completado, para bien o mal.
error TEXT,
action_data JSONB -- datos de la operacion original.
);
-- Indice de las tareas que no han terminado
CREATE INDEX idx_pause_cancel_tasks_pending
ON pause_cancel_tasks (next_check)
WHERE completed_date IS NULL;

View File

@@ -6,12 +6,13 @@ WORKDIR /home/node/app
RUN corepack enable
COPY ./dist/packages ./packages
COPY ./.yarnrc.yml ./
COPY ./docs ./docs
# Para las migraciones
COPY ./deployment ./deployment
COPY ./package.json ./
# Force node-modules linker (no .yarnrc.yml in build context)
RUN echo 'nodeLinker: node-modules' > .yarnrc.yml
RUN yarn install
RUN mkdir -p dist && ln -sf ../packages dist/packages

View File

@@ -1,4 +1,6 @@
#!/bin/sh
cd /home
cd /home/node/app && yarn start
cd /home/node/app
yarn migrate
yarn start

View File

@@ -60,11 +60,15 @@ pipeline {
sourceFiles: "dist/**/*",
excludes: "dist/**/node_modules/**"
),
sshTransfer(
cleanRemote: false,
remoteDirectory: "$APP_REMOTE_PATH",
sourceFiles: "docs/**/*",
),
sshTransfer(
cleanRemote: false,
remoteDirectory: "$APP_REMOTE_PATH",
sourceFiles: "deployment/database/**/*",
removePrefix: "deployment",
),
sshTransfer(
cleanRemote: false,
@@ -88,6 +92,11 @@ pipeline {
remoteDirectory: "$APP_REMOTE_PATH",
sourceFiles: "package.json",
),
sshTransfer(
cleanRemote: false,
remoteDirectory: "$APP_REMOTE_PATH",
sourceFiles: ".yarnrc.yml",
),
sshTransfer(
cleanRemote: false,
execCommand: "sh $APP_REMOTE_PATH/rebuild.sh"

View File

@@ -14,6 +14,7 @@ COPY ./packages ./packages
COPY tsconfig*.json ./
COPY .env* ./
COPY ./.yarnrc.yml ./
COPY ./docs ./docs
COPY ./deployment/local/docker/start.sh ./
# Copiar el archivo de migrations? porque ahora no creo que se esté lanzando nada
COPY ./deployment/database/migrations ./deployment/database/migrations

View File

@@ -40,6 +40,9 @@ services:
- path: ./packages
action: sync
target: /usr/local/app/packages
- path: ./docs
action: sync
target: /usr/local/app/docs
- path: ./package.json
action: rebuild
ports:
@@ -72,7 +75,6 @@ services:
- "${POSTGRES_PORT}:${POSTGRES_PORT}"
volumes:
- ./sql-data/:/var/lib/postgres/data
- ./deployment/database/init.sql:/docker-entrypoint-initdb.d/init.sql
healthcheck:
test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}"]
interval: 5s

File diff suppressed because one or more lines are too long

View File

@@ -11,7 +11,7 @@ post {
}
body:form-urlencoded {
iccid: 8933201125068886692
iccid: 8935103196306448300
offer: SAVEFAMILY1
}

View File

@@ -11,7 +11,7 @@ post {
}
body:form-urlencoded {
iccid: 8933201125068886692
iccid: 8933201125068887054
}
settings {

16
docs/sim-api/Docs.bru Normal file
View File

@@ -0,0 +1,16 @@
meta {
name: Docs
type: http
seq: 12
}
get {
url: {{baseurl}}/docs/sim-api-documentation.html
body: none
auth: inherit
}
settings {
encodeUrl: true
timeout: 0
}

View File

@@ -15,7 +15,7 @@ params:query {
}
body:form-urlencoded {
iccid: 8933201125065160414
iccid: 8935103196306448300
}
settings {

View File

@@ -0,0 +1,21 @@
meta {
name: ReActivate
type: http
seq: 13
}
post {
url: {{baseurl}}/sim/reActivate
body: formUrlEncoded
auth: inherit
}
body:form-urlencoded {
iccid: 8935103196306448300
~offer: SAVEFAMILY1
}
settings {
encodeUrl: true
timeout: 0
}

View File

@@ -1,3 +1,4 @@
vars {
baseurl: http://localhost:3000
}
color: #2E8A54

View File

@@ -1,3 +1,4 @@
vars {
baseurl: https://sf-sims.savefamilygps.net
}
color: #CE4F3B

View File

@@ -0,0 +1,4 @@
vars {
baseurl: http://sim-connections.savefamilygps.net
}
color: #C77A0F

View File

@@ -0,0 +1,20 @@
meta {
name: test proxy
type: http
seq: 14
}
get {
url: {{baseurl}}/simconnections/alai/select?iccid=1111111111111111111
body: none
auth: inherit
}
params:query {
iccid: 1111111111111111111
}
settings {
encodeUrl: true
timeout: 0
}

View File

@@ -0,0 +1,23 @@
info:
name: Select Page
type: http
seq: 6
http:
method: GET
url: "{{baseurl}}/selectPage"
params:
- name: iccid
value: "8935103196306448300"
type: query
disabled: true
body:
type: json
data: ""
auth: inherit
settings:
encodeUrl: true
timeout: 0
followRedirects: true
maxRedirects: 5

25
docs/sim-nos/Select.yml Normal file
View File

@@ -0,0 +1,25 @@
info:
name: Select
type: http
seq: 5
http:
method: GET
url: "{{baseurl}}/select?iccid=8935103196306448300"
params:
- name: iccid
value: "8935103196306448300"
type: query
body:
type: json
data: |-
{
"iccid": "8933201125068890066"
}
auth: inherit
settings:
encodeUrl: true
timeout: 0
followRedirects: true
maxRedirects: 5

View File

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

View File

@@ -0,0 +1,7 @@
name: prod
color: "#CE4F3B"
variables:
- name: baseurl
value: https://nosconnectcenter-api.iot-x.com
- secret: true
name: token

View File

@@ -0,0 +1,10 @@
opencollection: 1.0.0
info:
name: sim-nos
bundled: false
extensions:
bruno:
ignore:
- node_modules
- .git

View File

@@ -0,0 +1,22 @@
info:
name: subscriber actions
type: http
seq: 1
http:
method: GET
url: "{{baseurl}}/subscribers/{{iccid}}/actions"
auth:
type: bearer
token: "{{token}}"
runtime:
variables:
- name: iccid
value: "8935103196306448300"
settings:
encodeUrl: true
timeout: 0
followRedirects: true
maxRedirects: 5

View File

@@ -0,0 +1,22 @@
info:
name: subscriber info
type: http
seq: 2
http:
method: GET
url: "{{baseurl}}/subscribers/{{iccid}}"
auth:
type: bearer
token: "{{token}}"
runtime:
variables:
- name: iccid
value: "8935103196306448300"
settings:
encodeUrl: true
timeout: 0
followRedirects: true
maxRedirects: 5

View File

@@ -0,0 +1,22 @@
info:
name: subscriber products available
type: http
seq: 4
http:
method: GET
url: "{{baseurl}}/subscribers/{{iccid}}/products/available"
auth:
type: bearer
token: "{{token}}"
runtime:
variables:
- name: iccid
value: "8935103196306448300"
settings:
encodeUrl: true
timeout: 0
followRedirects: true
maxRedirects: 5

View File

@@ -0,0 +1,22 @@
info:
name: subscribers
type: http
seq: 3
http:
method: GET
url: "{{baseurl}}/subscribers"
auth:
type: bearer
token: "{{token}}"
runtime:
variables:
- name: iccid
value: "8935103196306448300"
settings:
encodeUrl: true
timeout: 0
followRedirects: true
maxRedirects: 5

View File

@@ -5,16 +5,16 @@ meta {
}
get {
url: https://api-getway.objenious.com/ws/lines?pageSize=10&identifier.identifierType=ICCID&identifier.identifiers=8933201125065160455
url: https://api-getway.objenious.com/ws/lines?pageSize=1000&simStatus=ACTIVATED
body: formUrlEncoded
auth: bearer
}
params:query {
pageSize: 10
identifier.identifierType: ICCID
identifier.identifiers: 8933201125065160455
~simStatus: ACTIVATED
pageSize: 1000
simStatus: ACTIVATED
~identifier.identifierType: ICCID
~identifier.identifiers: 8933201125065160455
}
auth:bearer {

View File

@@ -37,7 +37,7 @@ body:form-urlencoded {
}
vars:pre-request {
params.id: 14557
params.id: 15102
}
settings {

View File

@@ -5,13 +5,13 @@ meta {
}
get {
url: {{actionsUrl}}/massActions?massActionId=5192767
url: {{actionsUrl}}/massActions?massActionId=5363116
body: formUrlEncoded
auth: bearer
}
params:query {
massActionId: 5192767
massActionId: 5363116
~identifier.identifierType: ICCID
~identifier.identifiers: 8933201125065160463,8933201125065160422
}

View File

@@ -7,11 +7,11 @@
],
"scripts": {
"test": "vitest watch",
"build": "yarn workspaces foreach -A --exclude sim-consumidor-nos run build && cp .env dist/ && yarn setup:runtime",
"build": "rm -rf ./dist && yarn workspaces foreach -Api run build && cp .env dist/ && yarn setup:runtime",
"setup:runtime": "mkdir -p dist/packages/node_modules && ln -sf ../sim-shared dist/packages/node_modules/sim-shared && ln -sf ../sf-consumidor-objenious dist/packages/node_modules/sim-consumidor-objenious",
"start": "yarn setup:runtime && yarn workspaces foreach -Apiv --exclude sim-consumidor-nos run start",
"start": "yarn setup:runtime && yarn workspaces foreach -Apiv run start",
"typecheck": "npx tsc --noEmit",
"dev": "yarn workspaces foreach -Apiv --exclude sim-consumidor-nos run dev ",
"dev": "yarn workspaces foreach -Apiv --exclude sim-objenious-cron run dev",
"lint": "eslint .",
"lint:fix": "eslint --fix .",
"format": "prettier --write .",
@@ -19,6 +19,7 @@
"migrate": "yarn db-migrate -e .env -m deployment/database/migrations -t 99.0.0"
},
"dependencies": {
"@sf-alvar/db-migrate": "1.0.6",
"@tsconfig/node22": "^22.0.5",
"amqp-connection-manager": "^5.0.0",
"amqplib": "^0.10.9",
@@ -27,7 +28,7 @@
"dotenv": "^17.2.3",
"express": "^5.2.1",
"pg": "^8.18.0",
"typescript": "^5.9.3",
"typescript": "^6.0.3",
"uuidv7": "^1.1.0",
"vite": "^7.3.1",
"vite-tsconfig-paths": "^6.0.5"

View File

@@ -1,5 +1,8 @@
PORT=3000
RABBITMQ_USER=guest
RABBITMQ_PASSWORD=guest
NOS_BASE_URL=localhost
APP_PORT=3001
APP_HOST="0.0.0.0"
ENVIORMENT=development
NOS_ACCESS_TOKEN=2YGhecTr4+uKbVKxaqBlk2edsrHA2OQY
NOS_BASE_URL=https://nosconnectcenter-api.iot-x.com

View File

@@ -1,65 +1,178 @@
import { EventBus } from "sim-shared/domain/EventBus.port.js";
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 SimNosController {
private eventBus: EventBus;
private activationUseCases: any;
private routes = new Map<string, () => void>([
["activate", async () => { console.log("caso de uso activate") }],
["pause", async () => { console.log("caso de uso pause") }],
["cancel", async () => { console.log("caso de uso cancel") }],
])
constructor(
eventBus: EventBus
private uscases: SimNosUsecases,
private eventBus: EventBus,
) {
this.eventBus = eventBus
// No se si hay un sistema mejor
// convertor en const () => {} para conservar el contexto??
this.recibeMsg = this.recibeMsg.bind(this)
}
public async recibeMsg(msg: ConsumeMessage | null) {
if (!this.validateActivationMsg(msg)) {
throw new Error("Error consumiendo el mensaje no es valido")
}
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;
}
msg = msg!
const msgParsed = JSON.parse(String(msg.content))
const msgKey = msg.fields.routingKey.split(".")
const accion = msgKey[2]
if (accion == undefined) {
console.error("La routingKey es incorrecta: " + accion)
this.eventBus.nack(msg)
return;
}
if (this.routes.get(accion) == undefined) {
console.error("No hay una ruta definida para la accion")
this.eventBus.nack(msg)
return;
private decodeMsg(msg: ConsumeMessage): object | undefined {
if (msg.content == undefined) {
console.warn('[Sim.controller] Mensaje vacío');
return undefined;
}
try {
this.routes.get(accion)!()
} catch (err) {
console.log("Error procesando el mensaje")
this.eventBus.nack(msg)
} finally {
this.eventBus.ack(msg)
// 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;
}
}
/**
* TODO:
* - Loguear motivos de la no validacion
* 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.
*/
private validateActivationMsg(msg: ConsumeMessage | null) {
if (msg == undefined) return false;
return true;
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;
}
}
}
}

View 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 { SimNosController } from "./SimNOS.controller.js";
import { EventBus } from "sim-shared/domain/EventBus.port.js";
import { Result } from "sim-shared/domain/Result.js";
type FuncType = ((m: ConsumeMessage) => Promise<Result<string, any>>)
export class SimNosRouter {
private readonly routes: Map<string, FuncType>;
constructor(
private readonly simController: SimNosController,
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];
}
}

View 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 { NosHttpClient } from "#infrastructure/NosHttpClient.js";
import { NosRepository } from "#infrastructure/NosRepository.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 SimNosUsecases {
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 {}
}
*/
}

View 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,
]
)

View File

@@ -1,5 +1,16 @@
import { loadEnvFile } from "node:process";
loadEnvFile("../../.env")
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,
@@ -18,5 +29,12 @@ export const env = {
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)
};

View 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 QUEUES = {
NOS: "sim.nos",
NOSDLX: "sim.nos.dlx",
NOSDEL: "sim.nos.delayed",
}
const EXCHANGES = {
MAIN: "sim.exchange",
DLX: "sim.ex.nos.dlx",
DEL: "sim.ex.nos.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
const BASE_NOS_KEY = "sim.nos.#"
await channel.assertExchange(EXCHANGES.DEL, "topic")
await channel.assertExchange(EXCHANGES.DLX, "topic")
await channel.assertExchange(EXCHANGES.MAIN, "topic")
await channel.assertQueue(QUEUES.NOS)
await channel.assertQueue(QUEUES.NOSDLX)
await channel.assertQueue(QUEUES.NOSDEL, {
durable: true,
arguments: {
'x-message-ttl': DELAY,
'x-dead-letter-exchange': EXCHANGES.MAIN,
}
})
// Cola dead-letter
await channel.bindQueue(QUEUES.NOSDLX, EXCHANGES.DLX, "sim.nos.#")
// Cola delay
await channel.bindQueue(QUEUES.NOSDEL, EXCHANGES.DEL, BASE_NOS_KEY)
// Cola nos -> main exchange
await channel.bindQueue(QUEUES.NOS, EXCHANGES.MAIN, BASE_NOS_KEY)
}
export async function startRMQClient() {
await rabbitmqEventBus.connect()
return rabbitmqEventBus
}

View File

@@ -1,39 +0,0 @@
import { RabbitMQEventBus, RMQConnectionParams } from "sim-shared/infrastructure/RabbitMQEventBus.js"
import { env } from "./env"
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,
}
export const rabbitmqEventBus = new RabbitMQEventBus({
connectionParams: rmqConnOptions
})
export async function startRMQClient() {
await rabbitmqEventBus.connect().catch(async e => {
console.error("Error en la conexion RMQ")
await rabbitmqEventBus.connect()
})
// Bindings especificos, deberia meterlos en la clase
try {
await rabbitmqEventBus.channel?.assertQueue("sim.nos")
} catch {
console.log("[i] Cola de sims de nos creada")
await rabbitmqEventBus.channel?.bindQueue("sim.nos", "sim.exchange", "sim.nos.*")
}
return rabbitmqEventBus
}

View 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
})

View File

@@ -0,0 +1,131 @@
export namespace NosApi {
export type ActivationData = {
/**
The unique physical subscriber identifier:
Cellular - the ICCID
Non - IP - the EUI
Satellite - the IMEI
*/
physicalId: string,
/**
example: 447000000001
The unique network subscriber identifier:
Cellular subscriber - the MSISDN
Non - IP subscriber - the Device EUI
Satellite subscriber - the Subscription ID
*/
subscriberId: string,
/**
example: 9999
If the subscriber uses Circuit Switched Data(CSD), this field displays its data number.If the subscriber does not use CSD, this field is null.
*/
dataNumber: string
/**
example: 172.0.0.1
The subscriber IP address.
*/
ip: string
/**
example: 234150000000001
The subscriber IMSI.
*/
imsi: string
}
type OkResponse<T> = {
error?: undefined | null,
content: T
}
type ErrorResponse<E = GeneralError> = {
content?: undefined | null,
error: E
}
type GeneralError = {
children?: string[],
code: string,
message: string
}
export type ActivateResponseOK = OkResponse<ActivationData>
export type ActivateResponseError = ErrorResponse
export type ActivateResponse = ActivateResponseOK | ActivateResponseError
export type LineDataResponseOK = OkResponse<LineData>
export type LineDataResponseError = ErrorResponse
export type LineDataResponse = LineDataResponseOK | LineDataResponseError
export type PageDataResponseOk = OkResponse<LineData[]>
export type PageDataResponseError = OkResponse<LineData[]>
export type PageResponse = PageDataResponseOk | PageDataResponseError
export type LineData = {
physicalId: string
subscriberId: string
imsi: string
nickname: string
operatorCode: string
tariffName: string
lineRental: number
contractLength: number
isBarred: boolean
isActive: boolean
terminateDate: any
groupId: number
subscriberType: any
connectionDate: string
expiryDate: string
networkState: NetworkState
billingState: BillingState
operatorName: string
imei: string
dataUsage?: number
eid?: string
smdpProvider?: string
related?: {
parent: RelatedItem,
profiles: RelatedItem[]
}
}
type RelatedItem = {
physicalId: string
isEnabledOnParent: boolean
}
export type NetworkState = {
currentStateId: number
currentState: string
isTransferring: boolean
lastTransferred: number
isOnline: boolean
lastSeenOnline: number
}
export type BillingState = {
currentStateId: number
currentState: string
}
export type BarData = {
product: string
description: string
enabled: boolean
}
export type BarResponseOk = OkResponse<BarData>
export type BarResponseError = ErrorResponse
export type BarResponse = BarResponseOk | BarResponseError
}

View File

@@ -1,14 +1,74 @@
import { startRMQClient } from "#config/eventBusConfig"
import express from "express"
import cors from 'cors';
import { SimNosRouter } from "./aplication/SimNOS.router.js"
import { SimNosController } from "./aplication/SimNOS.controller.js"
import { SimNosUsecases } from "./aplication/SimNOS.usecases.js"
import { NosHttpClient } from "./infrastructure/NosHttpClient.js"
import { env } from "#config/env/env.js"
import { NosRepository } from "./infrastructure/NosRepository.js"
import { OrderRepository } from "sim-shared/infrastructure/OrderRepository.js";
import { pgClient } from "#config/postgreConfig.js";
import { startRMQClient } from "#config/eventBus.config.js";
const RMQ_QUEUE = "sim.nos"
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 nosHttpClient = new NosHttpClient(
NOS_BASE_URL
)
const nosRepository = new NosRepository(
nosHttpClient
)
const orderRepository = new OrderRepository(
pgClient
)
const simUsecases = new SimNosUsecases(
nosHttpClient,
nosRepository,
orderRepository
)
const simController = new SimNosController(
simUsecases,
rmqClient
)
rmqClient.consume("sim.nos", simController.recibeMsg)
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()

View File

@@ -0,0 +1,41 @@
import axios, { AxiosInstance } from "axios";
import { env } from "#config/env/env.js"
export class NosHttpClient {
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
}
}

View File

@@ -0,0 +1,177 @@
import { Result } from "sim-shared/domain/Result.js";
import { NosHttpClient } from "./NosHttpClient.js";
import { NosApi } from "#domain/NosAPI.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
}
}
}
}

View File

@@ -1,17 +1,8 @@
{
"name": "sim-consumidor-nos",
"version": "1.0.0",
"type": "module",
"description": "consumidor generico de eventos de NOS",
"main": "index.ts",
"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"
},
"author": "",
"license": "ISC",
"packageManager": "yarn@4.12.0",
"imports": {
"#config/*.js": {
"types": "./config/*.ts",
@@ -21,13 +12,13 @@
"types": "./config/*.ts",
"default": "./config/*.js"
},
"#adapters/*.js": {
"types": "./adapters/*.ts",
"default": "./adapters/*.js"
"#infrastructure/*.js": {
"types": "./infrastructure/*.ts",
"default": "./infrastructure/*.js"
},
"#adapters/*": {
"types": "./adapters/*.ts",
"default": "./adapters/*.js"
"#infrastructure/*": {
"types": "./infrastructure/*.ts",
"default": "./infrastructure/*.js"
},
"#domain/*.js": {
"types": "./domain/*.ts",
@@ -37,29 +28,32 @@
"types": "./domain/*.ts",
"default": "./domain/*.js"
},
"#ports/*.js": {
"types": "./ports/*.ts",
"default": "./ports/*.js"
"#aplication/*.js": {
"types": "./aplication/*.ts",
"default": "./aplication/*.js"
},
"#ports/*": {
"types": "./ports/*.ts",
"default": "./ports/*.js"
},
"#tests/*.js": {
"types": "./__tests__/*.ts",
"default": "./__tests__/*.js"
},
"#tests/*": {
"types": "./__tests__/*.ts",
"default": "./__tests__/*.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": {
@@ -70,6 +64,7 @@
"@types/supertest": "*",
"prettier": "*",
"supertest": "*",
"tsc-alias": "^1.8.16",
"tsx": "*",
"vitest": "*"
}

View File

@@ -0,0 +1,11 @@
# NOS
## Particularidades de las operaciones de NOS
- Documentación de la API: [DOC](https://pelion-help.iot-x.com/nos/en/Content/API/APIReference/API%20Reference.htm?tocpath=_____7)
- No se necesita la pre-activación de las SIM.
- La suspensión y reactivación se llama "bar" y "unbar".
- El token de Authentication dura exactamente 1 año, solo se puede refrescar
desde la web.
- En la documentación la URL de la API es <https://nos-api.iot-x.com> pero la
de producción es <https://nosconnectcenter-api.iot-x.com>.

View File

@@ -0,0 +1,5 @@
## ENV PARA DATOS DE TEST - shared nunca se lanza en produccion
NOTIFICATION_URL="https://sf-sim-activation.savefamilygps.net/send-activation-mail"
# NOTIFICATION_URL="localhost"
SIM_ACTIVATION_API_KEY=9e48c4ac-1ab0-4397-b3f3-6c239200dfe6

View File

@@ -2,16 +2,40 @@
"extends": "../../tsconfig.json",
"compilerOptions": {
"outDir": "../../dist",
"baseUrl": ".",
"rootDir": "../../",
"paths": {
"#config/*": [
"./config/*"
],
"#infrastructure/*": [
"./infrastructure/*"
],
"#domain/*": [
"./domain/*"
],
"#aplication/*": [
"./aplication/*"
],
"config/*": [
"./config/*"
],
"infrastructure/*": [
"./infrastructure/*"
],
"domain/*": [
"./domain/*"
]
}
},
"exclude": [
"node_modules"
],
"include": [
"**/*.ts",
"src/**/*.d.ts"
"**/*.d.ts",
"../../packages/sim-shared/**/*.ts"
],
"files": [
"index.ts"
]
}
}

View File

@@ -0,0 +1,118 @@
import { describe, it, beforeEach, mock, after } from "node:test";
import assert from "node:assert";
import { SimController } from "./Sim.controller.js";
import { EventBus } from "sim-shared/domain/EventBus.port.js";
import { SimUseCases } from "./Sim.usecases.js";
import { ConsumeMessage } from "amqplib";
import { postgrClient, pgPool } from "#config/postgreConfig.js";
import { httpInstance } from "#config/httpClient.config.js";
import { OrderRepository } from "sim-shared/infrastructure/OrderRepository.js";
import { PauseCancelTaskRepository } from "#adapters/PauseCancelTaskRepository.js";
import { ObjeniousOperationsRepository } from "sim-shared/infrastructure/ObjeniousOperationRepository.js";
import { ActionData } from "#domain/DTOs/objeniousapi.js";
describe("SimController Integration Tests (Real UseCases)", () => {
let eventBusMock: any;
let controller: SimController;
let useCases: SimUseCases;
beforeEach(() => {
// Mock ONLY the event bus as requested
eventBusMock = {
publish: mock.fn(),
addSubscribers: mock.fn(),
consume: mock.fn(),
ack: mock.fn(async () => { }),
nack: mock.fn(async () => { }),
};
const operationRepository = new ObjeniousOperationsRepository(
httpInstance,
postgrClient,
);
const orderRepository = new OrderRepository(postgrClient);
const pauseRepository = new PauseCancelTaskRepository(postgrClient);
useCases = new SimUseCases({
httpClient: httpInstance,
operationRepository: operationRepository,
orderRepository: orderRepository,
pauseRepository: pauseRepository
});
// @ts-expect-error
useCases.findActivationDate = async (data: ActionData) => new Date()
controller = new SimController(eventBusMock as unknown as EventBus, useCases);
});
const createMockMsg = (payload: any): ConsumeMessage => {
return {
content: Buffer.from(JSON.stringify(payload)),
fields: {},
properties: {
headers: {
message_id: "test-correlation-id"
}
},
} as unknown as ConsumeMessage;
};
after(async () => {
await pgPool.end();
});
describe("suspend", () => {
it("should call stage_suspend and interact with DB and EventBus", async () => {
const iccid = "test-iccid-suspend-" + Date.now();
const msg = createMockMsg({
key: "sim.test.pause",
payload: {
iccid: iccid
},
headers: {
message_id: "correlation-suspend-" + iccid
}
});
const handler = controller.suspend();
await handler(msg);
// Verify that it reached the stage_suspend logic (which adds to pauseRepository)
// We can query the DB or check if ACK was called
assert.strictEqual(eventBusMock.ack.mock.callCount(), 1, "Message should be ACKed on success");
assert.strictEqual(eventBusMock.nack.mock.callCount(), 0, "Message should not be NACKed");
});
});
describe("terminate", () => {
it("should call stage_terminate and interact with DB and EventBus", async () => {
const iccid = "test-iccid-terminate-" + Date.now();
const msg = createMockMsg({
key: "sim.test.pause",
payload: {
iccid: iccid
},
headers: {
message_id: "correlation-terminate-" + iccid
}
});
const handler = controller.terminate();
await handler(msg);
assert.strictEqual(eventBusMock.ack.mock.callCount(), 1, "Message should be ACKed on success");
assert.strictEqual(eventBusMock.nack.mock.callCount(), 0, "Message should not be NACKed");
});
});
describe("Error Handling", () => {
it("should nack if message is invalid", async () => {
const msg = {
content: Buffer.from("invalid json"),
fields: {},
properties: {},
} as unknown as ConsumeMessage;
const handler = controller.suspend();
await assert.rejects(handler(msg), "Error de suspension consumiendo el mensaje no es valido");
});
});
});

View File

@@ -3,7 +3,7 @@ import { ConsumeMessage } from "amqplib";
import { SimUseCases } from "./Sim.usecases.js";
import { SimEvents } from "sim-shared/domain/SimEvents.js";
import { Result } from "sim-shared/domain/Result.js";
import { env } from "#config/env/index.js";
import { ActionData } from "#domain/DTOs/objeniousapi.js";
/**
* La clase usa generadores de funciones para mantener el contexto
@@ -21,7 +21,6 @@ export class SimController {
) {
this.eventBus = eventBus
this.useCases = useCases
}
private decodeMsg(msg: ConsumeMessage): object | undefined {
@@ -37,6 +36,7 @@ export class SimController {
} 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;
}
@@ -86,7 +86,7 @@ export class SimController {
const resp = await this.tryUseCase(msg, this.useCases.activate({
correlation_id: msgData.headers?.message_id,
dueDate: this.genDueDate(DUE_DATE_SECONDS).toISOString(),
customerAccountCode: env.OBJ_CUSTOMER_CODE,
customerAccountCode: "9.49411.10",
identifier: {
identifierType: "ICCID",
identifiers: [iccid]
@@ -108,7 +108,7 @@ export class SimController {
return async (msg: ConsumeMessage) => {
let msgData;
try {
msgData = this.validateMsg(msg) as SimEvents.pause
msgData = this.validateMsg(msg) as SimEvents.suspend
} catch (e) {
throw new Error("Error de preactivacion consumiendo el mensaje no es valido" + String(e))
}
@@ -135,7 +135,7 @@ export class SimController {
return async (msg: ConsumeMessage) => {
let msgData;
try {
msgData = this.validateMsg(msg) as SimEvents.pause
msgData = this.validateMsg(msg) as SimEvents.suspend
} catch (e) {
throw new Error("Error de reactivacion consumiendo el mensaje no es valido" + String(e))
}
@@ -157,11 +157,14 @@ export class SimController {
}
}
/**
* Lo mismo que pause
*/
public suspend() {
return async (msg: ConsumeMessage) => {
let msgData;
try {
msgData = this.validateMsg(msg) as SimEvents.pause
msgData = this.validateMsg(msg) as SimEvents.suspend
} catch (e) {
throw new Error("Error de suspension consumiendo el mensaje no es valido" + String(e))
}
@@ -171,14 +174,18 @@ export class SimController {
}
const iccid = msgData.payload.iccid
const res = await this.tryUseCase(msg, this.useCases.suspend({
const suspendData: ActionData = {
correlation_id: msgData.headers?.message_id,
dueDate: this.genDueDate(2 * 60).toISOString(),
identifier: {
identifierType: "ICCID",
identifiers: [iccid]
identifiers: [iccid] // Por algún motivo solo he puesto un iccd por identifier
}
}))
}
const useCaseRes = await this.tryUseCase(msg, this.useCases.stage_suspend(suspendData))
/*
const res = await this.tryUseCase(msg, this.useCases.suspend(actionData))
*/
}
}
@@ -187,7 +194,7 @@ export class SimController {
return async (msg: ConsumeMessage) => {
let msgData;
try {
msgData = this.validateMsg(msg) as SimEvents.pause
msgData = this.validateMsg(msg) as SimEvents.suspend
} catch (e) {
throw new Error("Error consumiendo el mensaje no es valido" + String(e))
}
@@ -195,16 +202,20 @@ export class SimController {
if (msgData == undefined) {
return Promise.reject("Mensaje invalido")
}
const iccid = msgData.payload.iccid
console.log("Mensaje procesado", msgData)
const res = await this.tryUseCase(msg, this.useCases.terminate({
const terminateActionData: ActionData = {
correlation_id: msgData.headers?.message_id,
dueDate: this.genDueDate(2 * 60).toISOString(),
identifier: {
identifierType: "ICCID",
identifiers: [iccid]
}
}))
}
//const res = await this.tryUseCase(msg, this.useCases.terminate(terminateActionData))
const res = await this.tryUseCase(msg, this.useCases.stage_terminate(terminateActionData))
}
}

View File

@@ -19,7 +19,7 @@ export class SimRouter {
["activate", this.simController.activate()],
["pause", this.simController.suspend()],
["cancel", this.simController.terminate()],
["reActivate", this.simController.reActivate()],
["reactivate", this.simController.reActivate()],
["preActivate", this.simController.preActivate()]
]);
}

View File

@@ -5,6 +5,8 @@ import { Result } from "sim-shared/domain/Result.js"
import { ObjeniousOperation, IOperationsRepository as OperationsRepositoryPort } from "sim-shared/domain/operationsRepository.port.js"
import assert from "node:assert"
import { OrderRepository } from "sim-shared/infrastructure/OrderRepository.js"
import { CreatePauseCancelTaskDTO, PauseCancelTaskRepository } from "#adapters/PauseCancelTaskRepository.js"
import { ObjeniousOperationsRepository } from "sim-shared/infrastructure/ObjeniousOperationRepository.js"
// TODO:
// - Pasar a un archivo de DTOs
@@ -12,21 +14,24 @@ import { OrderRepository } from "sim-shared/infrastructure/OrderRepository.js"
export class SimUseCases {
private readonly httpClient: HttpClient
private readonly operationRepository: OperationsRepositoryPort
private readonly objeniousRepository: ObjeniousOperationsRepository
private readonly orderRepository: OrderRepository
private readonly pauseRepository: PauseCancelTaskRepository
constructor(args: {
httpClient: HttpClient,
operationRepository: OperationsRepositoryPort,
orderRepository: OrderRepository
operationRepository: ObjeniousOperationsRepository,
orderRepository: OrderRepository,
pauseRepository: PauseCancelTaskRepository
}) {
this.httpClient = args.httpClient
this.operationRepository = args.operationRepository
this.objeniousRepository = args.operationRepository
this.orderRepository = args.orderRepository
this.pauseRepository = args.pauseRepository
}
private async logOperation(data: ObjeniousOperation) {
await this.operationRepository.createOperation({
await this.objeniousRepository.createOperation({
...data
})
}
@@ -70,11 +75,14 @@ export class SimUseCases {
operation: args.operation,
iccids: String(args.iccid),
status: "noMassID",
request_id: response.data.requestId
request_id: response.data.requestId,
correlation_id: args.correlation_id
}
// TODO: Esto tiene poco sentido si la operacion ya se
// tenia que haber creado en el generador
this.logOperation(operation)
.then().catch(e => console.error(e))
.then().catch(e => console.error("Error login operation", e))
if (args.correlation_id != undefined) {
this.orderRepository.updateOrder({
@@ -89,7 +97,6 @@ export class SimUseCases {
error: undefined,
data: true
}
} else {
return {
error: String(response.status),
@@ -109,8 +116,20 @@ export class SimUseCases {
public activate(activationData: ActivationData): () => Promise<Result<string, boolean>> {
const OPERATION_URL = "/actions/activateLine"
return async () => {
const iccid = activationData.identifier.identifiers
// Comporbación excepcional para saber si la linea está suspendida
const statusLinea = await this.objeniousRepository.getLinesAPI("ICCID", [String(iccid)])
console.log("statusLinea, ", iccid, statusLinea)
if (statusLinea.data != undefined && statusLinea.data[0].status.networkStatus == "SUSPENDED") {
const res = await this.reActivate(activationData)()
return res;
}
const req = this.httpClient.client.post(OPERATION_URL, {
...activationData
dueDate: activationData.dueDate,
identifier: activationData.identifier,
customerAccountCode: activationData.customerAccountCode,
offer: activationData.offer
})
try {
@@ -189,16 +208,29 @@ export class SimUseCases {
}
}
public reActivate(pauseData: ActionData): () => Promise<Result<string, boolean>> {
public reActivate(reactivateData: ActionData): () => Promise<Result<string, boolean>> {
const OPERATION_URL = "/actions/reactivateLine"
return async () => {
const req = this.httpClient.client.post(OPERATION_URL, {
...pauseData
...reactivateData
})
try {
const response = await req
// Creacion de la operacion inicial, antes de tener los datos
const operation: ObjeniousOperation = {
operation: "reactivate",
iccids: reactivateData.identifier.identifiers[0],
status: "noMassID",
request_id: response.data.requestId,
correlation_id: reactivateData.correlation_id
}
// TODO: Esto tiene poco sentido si la operacion ya se
// tenia que haber creado en el generador
this.logOperation(operation)
.then().catch(e => console.error("Error login operation", e))
if (response.status == 200) {
console.log("[o] Sim solicitud de reactivacion ", response.data)
return <Result<string, boolean>>{
@@ -214,7 +246,7 @@ export class SimUseCases {
} catch (error) {
console.error("[x] Error reactivacion", (error as AxiosError).response?.status)
return <Result<string, boolean>>{
error: "Error reactivando la sim" + pauseData.identifier,
error: "Error reactivando la sim" + reactivateData.identifier,
data: undefined
}
}
@@ -225,18 +257,182 @@ export class SimUseCases {
const OPERATION_URL = "/actions/suspendLine"
return this.generateUseCase({
correlation_id: suspendData.correlation_id,
operationPayload: suspendData,
operationPayload: {
dueDate: suspendData.dueDate,
identifier: suspendData.identifier
},
url: OPERATION_URL,
iccid: suspendData.identifier.identifiers[0], //
operation: "suspend"
})
}
/**
* Metodo muy especifico para obtener la fecha e activacion o en su defecto
* la actual para aber cuando se va a completar el periodo de test de una linea
*/
private async findActivationDate(actionData: ActionData) {
const iccid = actionData.identifier.identifiers
const lineData = await this.objeniousRepository.getLinesAPI("ICCID", iccid)
let activationDate = new Date()
// Si no se pueden sacar datos de la linea guardo momentaneamente el error
// pero no se cancela la operacion, el error puede ser de objenious y no nos
// puede afectar
console.log("LineData", lineData.data)
if (lineData.error != undefined) {
console.error(lineData.error)
} else {
const activationDateStr = lineData.data[0].status.activationDate
if (activationDateStr != undefined && activationDateStr != "") {
activationDate = new Date(activationDateStr)
}
}
return activationDate
}
/**
* Paso previo a la suspension para evitar errores cuando el billing es test
*/
public stage_suspend(suspendData: ActionData): () => Promise<Result<string, boolean>> {
return async (): Promise<Result<string, boolean>> => {
const correlation_id = suspendData.correlation_id
const iccid = suspendData.identifier.identifiers
const operation: ObjeniousOperation = {
operation: "suspend",
iccids: iccid[0],
status: "running",
correlation_id: correlation_id
}
// No se registra hasta que no pase por la tabla de pausas
// this.logOperation(operation)
// .then().catch(e => console.error("Error login operation", e))
const fail = (error: string) => {
console.error("[Sim.usecases]", error)
if (correlation_id != undefined) {
this.orderRepository.updateOrder({
correlation_id: correlation_id,
new_status: "failed"
})
}
}
// TODO REGISTRAR EL ORDER
if (correlation_id != undefined) {
await this.orderRepository.createOrder({
correlation_id: correlation_id,
order_type: "pause"
})
}
let activationDate;
try {
activationDate = await this.findActivationDate(suspendData)
} catch (e) {
return {
error: String(e)
}
}
const newTask: CreatePauseCancelTaskDTO = {
iccid: iccid[0],
activation_date: activationDate,
next_check: undefined, // Que se haga instantaneamente al ser la primera
operation_type: "suspend",
action_data: suspendData
}
const taskCreated = await this.pauseRepository.addTask(newTask)
// Caso que la task no se pueda crear en la BDD
if (taskCreated.error != undefined) {
fail(taskCreated.error)
return {
error: taskCreated.error
}
}
// Caso que se haya creado en la BDD
if (correlation_id != undefined) {
this.orderRepository.updateOrder({
correlation_id: correlation_id,
new_status: "running"
})
}
return {
data: true
}
}
}
/**
* Paso previo a la suspension para evitar errores cuando el billing es test
*/
public stage_terminate(terminateData: ActionData): () => Promise<Result<string, boolean>> {
return async (): Promise<Result<string, boolean>> => {
const correlation_id = terminateData.correlation_id
const iccid = terminateData.identifier.identifiers[0]
const activationDate = await this.findActivationDate(terminateData)
const newTask: CreatePauseCancelTaskDTO = {
iccid: iccid,
activation_date: activationDate,
next_check: undefined, // Que se haga instantaneamente al ser la primera
operation_type: "terminate",
action_data: terminateData
}
const taskCreated = await this.pauseRepository.addTask(newTask)
const operation: ObjeniousOperation = {
operation: "terminate",
iccids: iccid,
status: "running",
correlation_id: correlation_id
}
/**
this.logOperation(operation)
.then().catch(e => console.error("Error login operation", e))
*/
// Caso que la task no se pueda crear en la BDD
if (taskCreated.error != undefined) {
console.error("[Sim.usecases]", taskCreated.error)
if (correlation_id != undefined) {
this.orderRepository.updateOrder({
correlation_id: correlation_id,
new_status: "failed"
})
}
return {
error: taskCreated.error
}
}
// Caso que se haya creado en la BDD
if (correlation_id != undefined) {
this.orderRepository.updateOrder({
correlation_id: correlation_id,
new_status: "running"
})
}
return {
data: true
}
}
}
public terminate(terminationData: ActionData): () => Promise<Result<string, boolean>> {
const OPERATION_URL = "/actions/terminateLine"
return this.generateUseCase({
correlation_id: terminationData.correlation_id,
operationPayload: terminationData,
operationPayload: {
dueDate: terminationData.dueDate,
identifier: terminationData.identifier
},
url: OPERATION_URL,
iccid: terminationData.identifier.identifiers[0], //
operation: "terminate"

View File

@@ -18,24 +18,26 @@ export const rmqConnOptions = <RMQConnectionParams>{
secure: rmqSecure,
}
export const QUEUES = {
OBJ: "sim.objenious",
OBJDLX: "sim.objenious.dlx",
OBJDEL: "sim.objenious.delayed",
}
export const EXCHANGES = {
MAIN: "sim.exchange",
DLX: "sim.ex.objenious.dlx",
DEL: "sim.ex.objenious.delayed"
}
export const rabbitmqEventBus = new RabbitMQEventBus({
connectionParams: rmqConnOptions,
buildStructure: buildQueues,
maxRetry: 5
maxRetry: 5,
delayedExchange: EXCHANGES.DEL,
dlxExchange: EXCHANGES.DLX
})
async function buildQueues(channel: Channel) {
const QUEUES = {
OBJ: "sim.objenious",
DLX: "sim.objenious.dlx",
DEL: "sim.objenious.delayed"
}
const EXCHANGES = {
MAIN: "sim.exchange",
DLX: "sim.ex.objenious.dlx",
DEL: "sim.ex.objenious.delayed"
}
const DELAY = 10 * 1000
const BASE_OBENIOUS_KEY = "sim.objenious.#"
@@ -45,8 +47,8 @@ async function buildQueues(channel: Channel) {
await channel.assertExchange(EXCHANGES.MAIN, "topic")
await channel.assertQueue(QUEUES.OBJ)
await channel.assertQueue(QUEUES.DLX)
await channel.assertQueue(QUEUES.DEL, {
await channel.assertQueue(QUEUES.OBJDLX)
await channel.assertQueue(QUEUES.OBJDEL, {
durable: true,
arguments: {
'x-message-ttl': DELAY,
@@ -55,9 +57,9 @@ async function buildQueues(channel: Channel) {
})
// Cola dead-letter
await channel.bindQueue(QUEUES.DLX, EXCHANGES.DLX, "sim.objenious.#")
await channel.bindQueue(QUEUES.OBJDLX, EXCHANGES.DLX, "sim.objenious.#")
// Cola delay
await channel.bindQueue(QUEUES.DEL, EXCHANGES.DEL, BASE_OBENIOUS_KEY)
await channel.bindQueue(QUEUES.OBJDEL, EXCHANGES.DEL, BASE_OBENIOUS_KEY)
// Cola objenious -> main exchange
await channel.bindQueue(QUEUES.OBJ, EXCHANGES.MAIN, BASE_OBENIOUS_KEY)

View File

@@ -1,6 +1,6 @@
import { HttpClient } from "sim-shared/infrastructure/HTTPClient.js"
import { JWTService } from "../aplication/JWT.service.js"
import { env } from "./env/index.js"
import { jwtService } from "./jwtService.config.js"
const OBJ_BASE_URL = env.OBJ_BASE_URL
@@ -9,5 +9,5 @@ export const httpInstance = new HttpClient({
headers: {
"content-type": " application/json; charset=utf-8"
},
jwtManager: new JWTService()
jwtManager: jwtService
})

View File

@@ -0,0 +1,59 @@
import { GrantAccessRequestBody, JWTService } from "sim-shared/aplication/JWT.service.js"
import { env } from "./env/index.js"
import { JWTHeader } from "sim-shared/domain/JWT.js"
const PRIVATE_KEY_PATH = env.OBJ_PEM_PATH
const GET_TOKEN_URL = "https://idp.docapost.io/auth/realms/GETWAY/protocol/openid-connect/token"
const REFRESH_TOKEN_URL = GET_TOKEN_URL
const DEFAULT_BODY: GrantAccessRequestBody = {
grant_type: "client_credentials",
client_id: env.OBJ_CLIENT_ID,
client_assertion_type: "urn:ietf:params:oauth:client-assertion-type:jwt-bearer",
client_assertion: env.OBJ_CLI_ASSERTION
}
const DEFAULT_HEADERS = {
"content-type": "application/x-www-form-urlencoded"
}
const DEFAULT_HEADERS_JWT = {
alg: "RS256",
typ: "JWT",
kid: env.OBJ_KID,
}
const DEFAULT_DATA_JWT = {
sub: env.OBJ_CLIENT_ID,
iss: env.OBJ_CLIENT_ID,
aud: "https://idp.docapost.io/auth/realms/GETWAY",
jti: Date.now().toString(),
}
function addIATHeaders(authHeaders: Object) {
const headers = <JWTHeader>{
...authHeaders,
sub: env.OBJ_CLIENT_ID,
iss: env.OBJ_CLIENT_ID,
aud: GET_TOKEN_URL,
jti: Date.now().toString(),
iat: Math.floor(Date.now() / 1000),
exp: Math.floor(Date.now() / 1000) + 5 * 60,
}
return headers
}
export const jwtService = new JWTService({
transformJWTHeaders: addIATHeaders,
defaultHeaders: DEFAULT_HEADERS,
defaultBody: DEFAULT_BODY,
defaultJWTHeaders: DEFAULT_HEADERS_JWT,
defaultJWTPayload: DEFAULT_DATA_JWT,
privateKeyPath: PRIVATE_KEY_PATH,
tokenUrl: GET_TOKEN_URL,
refreshTokenUrl: REFRESH_TOKEN_URL
})

View File

@@ -8,6 +8,7 @@ import { SimUseCases } from "./aplication/Sim.usecases.js"
import { SimController } from "./aplication/Sim.controller.js"
import { SimRouter } from "./aplication/Sim.router.js"
import { OrderRepository } from "sim-shared/infrastructure/OrderRepository.js"
import { PauseCancelTaskRepository } from "#adapters/PauseCancelTaskRepository.js"
async function startWorker() {
const rmqClient = await startRMQClient()
@@ -18,15 +19,21 @@ async function startWorker() {
await pgClient.checkDatabaseConnection()
const operationRepository = new ObjeniousOperationsRepository(pgClient)
const operationRepository = new ObjeniousOperationsRepository(
httpClient,
pgClient,
)
const orderRepository = new OrderRepository(pgClient)
const pauseRepository = new PauseCancelTaskRepository(pgClient)
const simActivationController = new SimController(
rmqClient,
new SimUseCases({
httpClient: httpClient,
operationRepository: operationRepository,
orderRepository: orderRepository
orderRepository: orderRepository,
pauseRepository: pauseRepository
})
)
const simRouter = new SimRouter(simActivationController, rmqClient)

View File

@@ -0,0 +1,72 @@
import { after, before, describe, it } from "node:test";
import { CreatePauseCancelTaskDTO, PauseCancelTaskRepository } from "./PauseCancelTaskRepository.js";
import { postgrClient } from "#config/postgreConfig.js";
import assert from "node:assert";
const testTask: CreatePauseCancelTaskDTO = {
iccid: "1234",
operation_type: "suspend",
activation_date: new Date(),
next_check: new Date(),
action_data: {
dueDate: new Date().toString(),
correlation_id: "12223",
identifier: {
identifiers: ["1234"],
identifierType: "ICCID"
}
}
}
describe("Test PauseCancelTaskRepository - DB", () => {
const createdIds: number[] = [];
const pauseRepo = new PauseCancelTaskRepository(postgrClient)
before(() => {
})
after(() => {
})
it("Should create a task", async () => {
const created = await pauseRepo.addTask(testTask)
assert.ok(created != undefined, "A value must be returned always")
assert.ok(created.error == undefined, "Should not return a error")
assert.ok(created.data != undefined, "Data must be returned")
createdIds.push(created.data.id)
})
it("Should update a existing task", async () => {
const updated = await pauseRepo.updateTask({
id: createdIds[0],
next_check: new Date()
})
assert.ok(updated != undefined, "A value must be returned always")
assert.ok(updated.error == undefined, "Should not return a error")
assert.ok(updated.data != undefined, "Data must be returned")
})
it("Should finish a existing task", async () => {
const finish = await pauseRepo.finishTask({
id: createdIds[0],
error: "ok"
})
assert.ok(finish != undefined, "A value must be returned always")
assert.ok(finish.error == undefined, "Should not return a error")
assert.ok(finish.data != undefined, "Data must be returned")
})
it("Should get at least 1 pending task", async () => {
const created = await pauseRepo.addTask(testTask)
const pending = await pauseRepo.getPending()
assert.ok(pending != undefined, "A value must be returned always")
assert.ok(pending.error == undefined, "Should not return a error")
assert.ok(pending.data != undefined, "Data must be returned")
console.log("--> ", pending.data[0])
})
})

View File

@@ -0,0 +1,128 @@
import { Result } from "sim-shared/domain/Result.js";
import { QueryResult } from "pg";
import { PgClient } from "sim-shared/infrastructure/PgClient.js";
import { AxiosError } from "axios";
import { ActionData } from "#domain/DTOs/objeniousapi.js";
export type PauseCancelTask = {
id: number;
iccid: string;
operation_type: "suspend" | "terminate",
last_checked?: Date | null;
activation_date?: Date | null;
next_check?: Date | null;
completed_date?: Date | null;
error?: string | null;
action_data: ActionData
}
export type CreatePauseCancelTaskDTO = Pick<PauseCancelTask, "iccid" | "activation_date" | "next_check" | "operation_type" | "action_data">
export type UpdatePauseCancelTaskDTO = Pick<PauseCancelTask, "id" | "next_check">
export type FinishPauseCancelTaskDTO = Pick<PauseCancelTask, "id" | "error">
/**
* Repositorio para compensar los problemas de cacelcaiones/pausas de objenious a
* la hora aplicarlo sobre una linea con el billing a test.
*/
export class PauseCancelTaskRepository {
constructor(
private readonly pgClient: PgClient
) {
}
/**
* Obtiene las siguientes que se pueden lanzar, puede haber más pero
* estan pendientes
*/
public async getPending(): Promise<Result<string, PauseCancelTask[]>> {
const sql = `
SELECT * FROM pause_cancel_tasks
WHERE completed_date IS NULL
AND (next_check <= NOW() OR next_check IS NULL)
ORDER BY id ASC;
`;
try {
const res: QueryResult<PauseCancelTask> = await this.pgClient.query(sql);
return {
data: res.rows
}
} catch (e) {
return {
error: (e as AxiosError).message
}
}
}
public async addTask(task: CreatePauseCancelTaskDTO): Promise<Result<string, PauseCancelTask>> {
const sql = `
INSERT INTO pause_cancel_tasks (iccid, activation_date, next_check, last_checked, operation_type, action_data)
VALUES ($1, $2, $3, now(), $4, $5)
RETURNING *;
`;
try {
const values = [task.iccid, task.activation_date, task.next_check, task.operation_type, JSON.stringify(task.action_data)];
const res: QueryResult<PauseCancelTask> = await this.pgClient.query(sql, values);
return {
data: res.rows[0]
}
} catch (e) {
return {
error: (e as AxiosError).message
}
}
}
/**
* Se ha vuelto a comprobar la tarea pero sigue en test
*/
public async updateTask(updateData: UpdatePauseCancelTaskDTO): Promise<Result<string, PauseCancelTask>> {
const sql = `
UPDATE pause_cancel_tasks
SET last_checked = now(), next_check = $1
WHERE id = $2
RETURNING *;
`;
try {
const res = await this.pgClient.query<PauseCancelTask>(sql, [updateData.next_check, updateData.id]);
return {
data: res.rows[0]
}
} catch (e) {
return {
error: (e as AxiosError).message
}
}
}
/**
* La tarea ha termiando bien o mal
*/
public async finishTask(finishData: FinishPauseCancelTaskDTO) {
const sql = `
UPDATE pause_cancel_tasks
SET completed_date = NOW(), error = $1
WHERE id = $2
RETURNING *;
`;
try {
const res = await this.pgClient.query(sql, [finishData.error, finishData.id]);
return {
data: res.rows[0]
}
} catch (e) {
return {
error: (e as AxiosError).message
}
}
}
}
export default PauseCancelTask

View File

@@ -53,7 +53,7 @@
}
},
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"test": "node --import tsx --test ./**/*.test.ts",
"dev": "tsx watch index.ts",
"build": "tsc --build && yarn tsc-alias -p tsconfig.json && cp .env package.json ../../dist/packages/sim-consumidor-objenious/",
"start": "node ../../dist/packages/sim-consumidor-objenious/index.js",

View File

@@ -9,6 +9,10 @@
],
"include": [
"**/*.ts",
"**/*.d.ts",
"../../packages/sim-shared/**/*.ts"
],
"files": [
"index.ts"
]
}

View File

@@ -3,7 +3,7 @@ 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 { tryCatch } from "packages/sim-shared/domain/Result.js"
import { tryCatch } from "sim-shared/domain/Result.js"
export class SimController {
@@ -36,7 +36,6 @@ export class SimController {
}) {
return async (req: Request, res: Response) => {
const body = req.body
// 1. Validacion del body
if (args.validator != undefined) {
const validationResult = args.validator.validate(body)
@@ -79,7 +78,7 @@ export class SimController {
...usecaseResult.error
}
}).send()
args.onError(body, usecaseResult.error?.msg?.message ?? "Error indefinido")
args.onError(body, usecaseResult.error ?? "Error indefinido")
return 1;
}
@@ -132,6 +131,21 @@ export class SimController {
})
}
public reActivation() {
return this.controllerGenerator<{ iccid: string, offer: string }, { iccid: string, offer: string, compañia: string }>({
validator: iccidValidator,
mapBody: (b) => {
const { iccid, offer } = b
const compañia = companyFromIccid(iccid)
return { iccid, compañia, offer }
},
useCase: (args) => this.simUseCases.reActivation(args),
onError: (d, e) => console.error("[x] Error reactivacion: ", d, e),
onSuccess: console.log
})
}
public cancelation() {
return this.controllerGenerator<{ iccid: string }, { iccid: string, compañia: string }>({
validator: iccidValidator,

View File

@@ -1,10 +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 { SimEvents } from "sim-shared/domain/SimEvents.js";
import { uuidv7 } from "uuidv7";
import { CreateOrderDTO, OrderTracking, OrderType, OrderTypeOptions } from "sim-shared/domain/Order.js";
import { EventBus } from "sim-shared/domain/EventBus.port.js";
/**
* Casos de uso de tarjetas sim. Garantiza que todos los metodos usan el mismo bus de mensajes
@@ -130,6 +130,36 @@ export class SimUsecases {
}
}
async reActivation(args: { iccid: string, compañia: string, offer: string }):
Promise<Result<string, { iccid: string, message_id: string, operation: "reactivate" }>> {
const activationEvent = <SimEvents.activation>{
key: `sim.${args.compañia}.reactivate`,
payload: {
iccid: args.iccid,
offer: args.offer
}
}
const activationWithId = this.addMessage_id(activationEvent)
console.log("[d] Reactivation ", activationWithId)
await this.eventBus.publish([activationWithId])
const createdOrder = await this.saveOrder<SimEvents.reActivation>(activationWithId)
if (createdOrder.error != undefined) {
console.error(createdOrder.error)
return {
error: createdOrder.error
}
}
return {
data: {
iccid: args.iccid,
operation: "reactivate",
message_id: createdOrder.data?.correlation_id
}
}
}
async preActivation(args: { iccid: string, compañia: string }):
Promise<Result<string, { iccid: string, message_id: string, operation: "preactivation" }>> {
@@ -174,8 +204,10 @@ export class SimUsecases {
const cancelationWithId = this.addMessage_id(cancelationEvent)
console.log("[d] Cancelation ", cancelationWithId)
await this.eventBus.publish([cancelationWithId])
const savedOrder = await this.saveOrder(cancelationWithId)
if (savedOrder.error != undefined) {
console.error(savedOrder.error)
return {
@@ -198,18 +230,19 @@ export class SimUsecases {
* alias de bloquear / suspender en objenious
*/
async pause(args: { iccid: string, compañia: string }):
Promise<Result<string, { iccid: string, message_id: string, operation: "cancelation" }>> {
Promise<Result<string, { iccid: string, message_id: string, operation: "pause" }>> {
const pauseEvent = <SimEvents.pause>{
key: `sim.${args.compañia}.pause`,
payload: {
iccid: args.iccid
}
}
const pauseWithId = this.addMessage_id(pauseEvent)
console.log("[d] Pause", pauseWithId)
await this.eventBus.publish([pauseWithId])
await this.saveOrder(pauseWithId)
const savedOrder = await this.saveOrder(pauseWithId)
//await this.saveOrder(pauseWithId)
const savedOrder = await this.saveOrder<SimEvents.pause>(pauseWithId)
if (savedOrder.error != undefined) {
console.error(savedOrder.error)
@@ -222,7 +255,7 @@ export class SimUsecases {
data: {
iccid: args.iccid,
message_id: savedOrder.data.correlation_id,
operation: "cancelation"
operation: "pause"
}
}
}

View File

@@ -22,4 +22,5 @@ export const env = {
RABBITMQ_SECURE: process.env.RABBITMQ_SECURE,
RABBITMQ_RETRY_INTERVAL: process.env.RABBITMQ_INTERVAL,
RABBITMQ_VHOST: String(process.env.RABBITMQ_VHOST),
CONNECTIONS_URL: String(process.env.CONNECTIONS_URL)
};

View File

@@ -18,7 +18,10 @@ export const rmqConnOptions = <RMQConnectionParams>{
}
export const rabbitmqEventBus = new RabbitMQEventBus({
connectionParams: rmqConnOptions
connectionParams: rmqConnOptions,
// La entrada de eventos no tiene que definir exchanges de dlx o delay pero es obligatorio
delayedExchange: "-",
dlxExchange: "-"
})
export async function startRMQClient() {

View File

@@ -1,9 +1,11 @@
import express from "express"
import cors from 'cors';
import path from 'path';
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";
import { connectionsRoutes } from "#adapters/simconnectionsRoutes.js";
const PORT = env.API_PORT
const HOSTNAME = "0.0.0.0"
@@ -17,7 +19,6 @@ rabbitmqEventBus.connect()
console.error("[!] El cliente RMQ no se ha podido iniciar", e)
})
// Middleware
app.use(cors());
@@ -25,8 +26,11 @@ app.use(express.json());
app.use(express.urlencoded({ extended: true }));
app.use("/sim", simRoutes)
app.use("/simconnections", connectionsRoutes)
app.use("/orders", orderRoutes)
app.use("/docs", express.static(path.join(process.cwd(), '../../docs')))
app.get("/health", (req, res) => {
res.status(200).json({ status: "ok" })
})
@@ -34,4 +38,5 @@ app.get("/health", (req, res) => {
app.listen(PORT, HOSTNAME, () => {
console.log("[o] Servidor iniciado en el puerto %d", PORT)
})
export default {}

View File

@@ -1,4 +1,4 @@
import { rabbitmqEventBus } from '#config/eventBusConfig.js';
import { rabbitmqEventBus } from '../config/eventBusConfig.js';
import { SimUsecases } from '../aplication/Sim.usecases.js';
import { SimController } from '../aplication/Sim.controller.js';
import { Router } from 'express';
@@ -23,6 +23,7 @@ simRoutes.get("/status", () => { })
simRoutes.post("/save", simController.save())
simRoutes.post("/activate", simController.activation())
simRoutes.post("/reActivate", simController.reActivation())
simRoutes.post("/preActivate", simController.preactivation())
@@ -35,4 +36,5 @@ simRoutes.post("/test", simController.test())
// Proceso especifico de ALAI para liberar sims canceladas
simRoutes.post("/free", simController.free())
export { simRoutes }

View File

@@ -0,0 +1,87 @@
import { env } from "#config/env/index.js"
import { Router } from "express"
import { ClientRequest, IncomingMessage } from "http"
import { createProxyMiddleware } from "http-proxy-middleware"
import { Request } from "express"
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)
connectionsRoutes.use("", createProxyMiddleware({
target: CONNECTIONS_URL,
changeOrigin: true,
pathRewrite: {
'^/': "/simconnections/"
},
on: {
proxyReq: (proxyReq: ClientRequest, req: Request) => {
const protocol = req.protocol;
const host = req.get('host');
const originalFullUrl = `${protocol}://${host}${req.originalUrl}`;
const destinationFullUrl = `${CONNECTIONS_URL}${proxyReq.path}`;
/*
constnsole.log('──────────────────────────────────────────────────');
console.log(`[PROXY_DEBUG]`);
console.log(` ENTRADA: ${originalFullUrl}`);
console.log(` MÉTODO : ${req.method}`);
console.log(` DESTINO: ${destinationFullUrl}`);
console.log('──────────────────────────────────────────────────');
*/
console.log(`[Proxy Req]: ${req.method} ${req.url} -> ${proxyReq.path}`);
},
proxyRes: (proxyRes, req, res) => {
console.log(`[Proxy Res] Status: ${proxyRes.statusCode} desde ${req.url}`);
},
error: (err, req, res) => {
console.error('[Proxy Error]:', err);
// Validamos que 'res' tenga el método 'status' (típico de Express Response)
if ('status' in res) {
//@ts-ignore
res.status(500).json({ message: 'Error interno en el Gateway' });
}
},
}
}))
// Rutas
/**
connectionsRoutes.post('/simconnections/alai/preactivate',);
connectionsRoutes.get('/simconnections/alai/pause',);
connectionsRoutes.post('/simconnections/alai/terminate',);
connectionsRoutes.get('/simconnections/alai/pauseByPhone',);
connectionsRoutes.get('/simconnections/alai/active',);
connectionsRoutes.get('/simconnections/alai/change_orderid',);
connectionsRoutes.get('/simconnections/alai/select',);
connectionsRoutes.get('/simconnections/alai/select-iccid',);
connectionsRoutes.get('/simconnections/alai/selectFromDb',);
connectionsRoutes.get('/simconnections/alai/selectPage',);
connectionsRoutes.post('/simconnections/alai/schedulePause',);
connectionsRoutes.get('/simconnections/shopify/getbyWP',);
connectionsRoutes.get('/simconnections/shopify/getbyWPS',);
///
connectionsRoutes.get('/simconnections/sim/associate',);
connectionsRoutes.post('/simconnections/sim/search',);
connectionsRoutes.post('/simconnections/sim/historic',);
connectionsRoutes.post('/simconnections/sim/update',);
///
connectionsRoutes.post('/simconnections/nos/activate',);
connectionsRoutes.get('/simconnections/nos/select',);
connectionsRoutes.get('/simconnections/nos/selectPage',);
//Unificación
connectionsRoutes.post('/simconnections/sim/active',); // True false
connectionsRoutes.patch('/simconnections/sim/pause',);
connectionsRoutes.get('/simconnections/sim/select',);
connectionsRoutes.get('/simconnections/sim/select-phone',);
**/

View File

@@ -53,6 +53,7 @@
"cors": "*",
"dotenv": "*",
"express": "*",
"http-proxy-middleware": "^3.0.5",
"sim-shared": "sim-shared:*",
"typescript": "*"
},

View File

@@ -9,6 +9,7 @@
],
"include": [
"**/*.ts",
"**/*.d.ts",
"../../packages/sim-shared/**/*.ts"
],
"files": [

View File

@@ -7,6 +7,6 @@ OBJ_KID=xNfbMiyL1ORXGP8lElhcv8nVaG3EJKye4Lc1YoN3I1E
OBJ_BASE_URL=https://api-getway.objenious.com/ws
# OBJ_BASE_URL=https://api-getway.objenious.com/ws/test
# NOTIFICATION_URL="https://sf-sim-activation.savefamilygps.net/send-activation-mail"
NOTIFICATION_URL="localhost"
NOTIFICATION_URL="https://sf-sim-activation.savefamilygps.net/send-activation-mail"
# NOTIFICATION_URL="localhost"
SIM_ACTIVATION_API_KEY=9e48c4ac-1ab0-4397-b3f3-6c239200dfe6

View File

@@ -31,15 +31,15 @@ export const env = {
OBJ_KID: String(process.env.OBJ_KID),
OBJ_BASE_URL: String(process.env.OBJ_BASE_URL),
NOTIFICATION_URL: String(process.env.NOTIFICATION_URL),
SIM_ACTIVATION_API_KEY: String(process.env.SIM_ACTIVATION_API_KEY)
NOTIFICATION_URL: String(process.env.NOTIFICATION_URL ?? ""),
SIM_ACTIVATION_API_KEY: String(process.env.SIM_ACTIVATION_API_KEY ?? "")
};
// assert las partes criticas
assert(env.RABBITMQ_PASSWORD != undefined)
assert(env.RABBITMQ_USER != undefined)
assert(env.SIM_ACTIVATION_API_KEY != undefined)
assert(env.NOTIFICATION_URL != undefined)
assert(env.SIM_ACTIVATION_API_KEY != "")
assert(env.NOTIFICATION_URL != "")
if (env.ENVIRONMENT == "production") {
assert(env.RABBITMQ_PASSWORD != "guest")
@@ -47,3 +47,5 @@ if (env.ENVIRONMENT == "production") {
}
console.log("[i] verificado env")

View File

@@ -1,6 +1,7 @@
import { HttpClient } from "sim-shared/infrastructure/HTTPClient.js"
import { env } from "./env/index.js"
import { JWTService } from "packages/sim-consumidor-objenious/aplication/JWT.service.js"
import { jwtService } from "./jwtService.config.js"
const OBJ_BASE_URL = env.OBJ_BASE_URL
@@ -9,5 +10,5 @@ export const httpInstance = new HttpClient({
headers: {
"content-type": " application/json; charset=utf-8"
},
jwtManager: new JWTService()
jwtManager: jwtService
})

View File

@@ -0,0 +1,20 @@
/**
* Cliente de postgres para la intranet. Se usa solo porque hace falta para el
* volcado de datos, si se usa en mas partes algo estás haciendo mal.
*/
import { Pool } from 'pg';
import { PgClient } from 'sim-shared/infrastructure/PgClient.js'
import { env } from './env/index.js';
export const pgPoolIntranet = new Pool({
user: env.POSTGRES_USER,
host: env.POSTGRES_HOST,
database: "intranet",
password: env.POSTGRES_PASSWORD,
port: Number(env.POSTGRES_PORT) || 5432,
});
export const postgresClientIntranet = new PgClient({
pool: pgPoolIntranet
})

View File

@@ -0,0 +1,59 @@
import { GrantAccessRequestBody, JWTService } from "sim-shared/aplication/JWT.service.js"
import { env } from "./env/index.js"
import { JWTHeader } from "sim-shared/domain/JWT.js"
const PRIVATE_KEY_PATH = env.OBJ_PEM_PATH
const GET_TOKEN_URL = "https://idp.docapost.io/auth/realms/GETWAY/protocol/openid-connect/token"
const REFRESH_TOKEN_URL = GET_TOKEN_URL
const DEFAULT_BODY: GrantAccessRequestBody = {
grant_type: "client_credentials",
client_id: env.OBJ_CLIENT_ID,
client_assertion_type: "urn:ietf:params:oauth:client-assertion-type:jwt-bearer",
client_assertion: env.OBJ_CLI_ASSERTION
}
const DEFAULT_HEADERS = {
"content-type": "application/x-www-form-urlencoded"
}
const DEFAULT_HEADERS_JWT = {
alg: "RS256",
typ: "JWT",
kid: env.OBJ_KID,
}
const DEFAULT_DATA_JWT = {
sub: env.OBJ_CLIENT_ID,
iss: env.OBJ_CLIENT_ID,
aud: "https://idp.docapost.io/auth/realms/GETWAY",
jti: Date.now().toString(),
}
function addIATHeaders(authHeaders: Object) {
const headers = <JWTHeader>{
...authHeaders,
sub: env.OBJ_CLIENT_ID,
iss: env.OBJ_CLIENT_ID,
aud: GET_TOKEN_URL,
jti: Date.now().toString(),
iat: Math.floor(Date.now() / 1000),
exp: Math.floor(Date.now() / 1000) + 5 * 60,
}
return headers
}
export const jwtService = new JWTService({
transformJWTHeaders: addIATHeaders,
defaultHeaders: DEFAULT_HEADERS,
defaultBody: DEFAULT_BODY,
defaultJWTHeaders: DEFAULT_HEADERS_JWT,
defaultJWTPayload: DEFAULT_DATA_JWT,
privateKeyPath: PRIVATE_KEY_PATH,
tokenUrl: GET_TOKEN_URL,
refreshTokenUrl: REFRESH_TOKEN_URL
})

View File

@@ -5,6 +5,12 @@ import { httpInstance } from "./config/httpClient.config.js"
import { CheckObjeniousRequests } from "./tasks/check_objenious_request.js"
import { ObjeniousOperationsRepository } from "sim-shared/infrastructure/ObjeniousOperationRepository.js"
import { OrderRepository } from "sim-shared/infrastructure/OrderRepository.js"
import { TaskVolcadoLineas } from "./tasks/volcado_lineas.js"
import { ObjeniousLinesRepository } from "./infranstructure/ObjeniousLinesRepository.js"
import { postgresClientIntranet } from "./config/intranetPostgresConfig.js"
import { PauseCancelTaskRepository } from "sim-consumidor-objenious/infrastructure/PauseCancelTaskRepository.js"
import { PauseTerminateTask } from "./tasks/check_pause_terminate.js"
import { SimUseCases } from "sim-consumidor-objenious/aplication/Sim.usecases.js"
async function startCron() {
const commonSettings = {
@@ -14,10 +20,16 @@ async function startCron() {
const httpClient = httpInstance
const pgClient = new PgClient({ pool: pgPool })
console.log("[i] Comprobando conexion con la BDD ")
await pgClient.checkDatabaseConnection()
await pgClient.checkDatabaseConnection()
const operationRepository = new ObjeniousOperationsRepository(pgClient)
const operationRepository = new ObjeniousOperationsRepository(
httpClient,
pgClient,
)
const orderRepository = new OrderRepository(pgClient)
const objeniousLineRepository = new ObjeniousLinesRepository(postgresClientIntranet)
const objTask = new CheckObjeniousRequests(
operationRepository,
@@ -25,23 +37,56 @@ async function startCron() {
httpClient,
)
const objeniosRepo = new ObjeniousOperationsRepository(
httpClient,
pgClient
)
const volcadoLineasTask = new TaskVolcadoLineas(
objeniousLineRepository,
objeniosRepo
)
const pauseRepo = new PauseCancelTaskRepository(pgClient)
const simUsecases = new SimUseCases({
httpClient: httpClient,
operationRepository: operationRepository,
orderRepository: orderRepository,
pauseRepository: pauseRepo
})
const pauseTask = new PauseTerminateTask(
objeniosRepo,
pauseRepo,
simUsecases,
orderRepository
)
await objTask.getPendingOperations()
const PERIODO_PETICIONES = 10 * 60 * 1000
const interval = setInterval(async () => {
console.log("Updating...")
await objTask.getPendingOperations()
console.log("Update finished")
}, 10 * 60 * 1000)
/*
const task = cron.createTask("* * * * *", async () => {
}
, {
...commonSettings,
name: "Test"
})
*/
try {
await objTask.getPendingOperations()
} catch (e) {
console.error("[x] Error de actualizacion de las lineas ")
}
}, PERIODO_PETICIONES)
const PERIODO_VOLCADO = 60 * 60 * 1000
const volcadoInterval = setInterval(async () => {
try {
await volcadoLineasTask.loadLines()
} catch (e) {
console.error("[x] Volcado de lineas de Objenious Fallido", e)
}
}, PERIODO_VOLCADO)
await pauseTask.run()
const PERIODO_CANCELACIONES = 60 * 60 * 1000;
const clacelacionesInterval = setInterval(async () => {
await pauseTask.run()
}, PERIODO_CANCELACIONES)
//await objTask.getPendingOperations()
}

View File

@@ -0,0 +1,59 @@
import test, { after, before, describe } from "node:test";
import { CreateObjeniousLineDTO } from "sim-shared/domain/objeniousLine.js";
import { ObjeniousLinesRepository } from "./ObjeniousLinesRepository.js";
import { postgrClient } from "../config/postgreConfig.js";
import assert from "node:assert";
describe("Line insertion test", async () => {
//const pgClient = postgreClientIntranet
const pgClient = postgrClient // En prod hay que usar el de Intrantet para usar la otra base de datos
const lineRepository = new ObjeniousLinesRepository(pgClient)
const lineaTest: CreateObjeniousLineDTO = {
simId: 1234,
iccid: "9999999999999",
msisdn: "34654674732",
imei: "219789481293",
imeiChangeDate: new Date(),
offerCode: "SAVEFAMILY1",
status: "ACTIVATED",
preactivationDate: new Date(),
activationDate: new Date(),
commercialStatus: "test",
commercialStatusDate: new Date(),
billingStatus: "test",
billingStatusChangeDate: new Date(),
billingActivationDate: new Date(),
createDate: new Date(),
raw: { test: "test" } as any // Para este test no hace falta
}
// Clean up before and after tests to ensure isolation
const cleanup = async () => {
await pgClient.query("DELETE FROM objenious_lines WHERE simId = 1234");
};
before(async () => {
await cleanup()
})
after(async () => {
await cleanup()
})
test("Should insert new line", async () => {
const res = await lineRepository.insertOrUpdate(lineaTest)
assert.ok(res != undefined, "The line wasn't created")
})
test("Should not update a line if the hash is the same", async () => {
const res = await lineRepository.insertOrUpdate(lineaTest)
assert.ok(res == undefined, "The line have been updated")
})
test("Should update a line if the hash changes", async () => {
const updated = structuredClone(lineaTest)
lineaTest.billingActivationDate = new Date()
const res = await lineRepository.insertOrUpdate(lineaTest)
assert.ok(res != undefined, "The line have been updated")
})
})

View File

@@ -0,0 +1,112 @@
/**
* Repositorio para el volcado de lineas de objenious en intranet
* solo para uso en el volcado.
*/
import { createHash } from "node:crypto";
import { PoolClient } from "pg";
import { CreateObjeniousLineDTO } from "sim-shared/domain/objeniousLine.js";
import { PgClient } from "sim-shared/infrastructure/PgClient.js";
export class ObjeniousLinesRepository {
constructor(
private pgClient: PgClient
) {
}
private generateLineHash(data: CreateObjeniousLineDTO) {
try {
const lineStr = JSON.stringify(data)
const hash = createHash("sha256").update(lineStr).digest("base64url")
return hash
} catch (e) {
console.error("[x] Error generando el hash de la linea", data)
return undefined
}
}
public async insertOrUpdate(data: CreateObjeniousLineDTO) {
const query = `
INSERT INTO objenious_lines (
simId,
iccid,
msisdn,
imei,
imeiChangeDate,
offerCode,
status,
preactivationDate,
activationDate,
commercialStatus,
commercialStatusDate,
billingStatus,
billingStatusChangeDate,
billingActivationDate,
createDate,
raw,
hash
) VALUES (
$1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17
)
ON CONFLICT (simId)
DO UPDATE SET
iccid = EXCLUDED.iccid,
msisdn = EXCLUDED.msisdn,
imei = EXCLUDED.imei,
imeiChangeDate = EXCLUDED.imeiChangeDate,
offerCode = EXCLUDED.offerCode,
status = EXCLUDED.status,
preactivationDate = EXCLUDED.preactivationDate,
activationDate = EXCLUDED.activationDate,
commercialStatus = EXCLUDED.commercialStatus,
commercialStatusDate = EXCLUDED.commercialStatusDate,
billingStatus = EXCLUDED.billingStatus,
billingStatusChangeDate = EXCLUDED.billingStatusChangeDate,
billingActivationDate = EXCLUDED.billingActivationDate,
raw = EXCLUDED.raw,
hash = EXCLUDED.hash
WHERE objenious_lines.hash IS DISTINCT FROM EXCLUDED.hash
RETURNING id;
`;
const lineHash = this.generateLineHash(data)
if (lineHash == undefined) {
console.error("[x] Ignorando linea ", data)
return;
}
const values = [
data.simId,
data.iccid,
data.msisdn,
data.imei,
data.imeiChangeDate,
data.offerCode,
data.status,
data.preactivationDate,
data.activationDate,
data.commercialStatus,
data.commercialStatusDate,
data.billingStatus,
data.billingStatusChangeDate,
data.billingActivationDate,
data.createDate || new Date(), // Default a ahora si no viene
JSON.stringify(data.raw), // El driver de pg requiere string o el objeto directo para JSONB
lineHash
];
let client: PoolClient | undefined = undefined;
try {
client = await this.pgClient.connect();
const res = await client.query<{ id: number }>(query, values);
return res.rows[0];
} catch (err) {
console.error('Error en la inserción:', err);
throw err;
} finally {
if (client != undefined) {
client.release()
}
}
}
}

View File

@@ -5,20 +5,6 @@
"description": "",
"main": "index.ts",
"imports": {
"#config/*.js": {
"types": "./config/*.ts",
"default": "./config/*.js"
},
"#config/*": {
"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"
@@ -45,8 +31,8 @@
}
},
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"build": "tsc --build && tsc-alias -p tsconfig.json && cp package.json ../../dist/packages/sim-objenious-cron/",
"test": "node --import tsx --test ./**/*.test.ts",
"build": "tsc --build && tsc-alias -p tsconfig.json && cp .env package.json ../../dist/packages/sim-objenious-cron/",
"dev": "tsx watch index.ts",
"start": "node ../../dist/packages/sim-objenious-cron/index.js"
},

View File

@@ -1,12 +1,13 @@
import { env } from "#config/env/index.js";
import { env } from "../config/env/index.js";
import { OrderRepository } from "sim-shared/infrastructure/OrderRepository.js";
import axios from "axios";
import { IOperationsRepository, Objenious, ObjeniousOperation, ObjeniousOperationChange, StatusEnum } from "sim-shared/domain/operationsRepository.port.js";
import { HttpClient } from "sim-shared/infrastructure/HTTPClient.js";
import { ObjeniousOperationsRepository } from "sim-shared/infrastructure/ObjeniousOperationRepository.js";
export class CheckObjeniousRequests {
constructor(
private readonly operationsRepository: IOperationsRepository,
private readonly operationsRepository: ObjeniousOperationsRepository,
private readonly orderRepository: OrderRepository,
private readonly httpClient: HttpClient
) {
@@ -16,6 +17,7 @@ export class CheckObjeniousRequests {
* TODO: meter a una funcion a parte task con los 3 pasos
*/
public async getPendingOperations() {
console.log("[i] Inicio revision de peticiones")
// 1. Se obtienen todas las operaciones pendientes de la BDD
const pendingOperations = await this.operationsRepository.getPendingOperations()
@@ -49,11 +51,14 @@ export class CheckObjeniousRequests {
console.log("[cron] Solicitando status para", merged.map(e => e.id))
const result = await this.getMassActionsStatus(merged)
console.log("[o] Revisión de eventos completa")
}
/**
* Para una lista de operaciones **con mass_action_id** se comprueba si han tenido alguna actualizacion
* Devuelve el numero de operaciones comprobadas.
* TODO: Esto va en un repositorio
*/
private async getMassActionsStatus(requestList: ObjeniousOperation[]) {
if (requestList.length == 0) return 0;
@@ -119,9 +124,6 @@ export class CheckObjeniousRequests {
if (uorStatus == "finished") {
console.log(" ****> Status", uorStatus)
if (uorStatus != "finished") {
console.error("!!! Notificando estado no finished")
}
const targetIccids = originalAction.iccids
const lineData = await this.getLineData(targetIccids)
console.log("[i] lineData", lineData.content[0])
@@ -136,7 +138,7 @@ export class CheckObjeniousRequests {
})
}
if (originalAction.operation == "activation") {
if (originalAction.operation == "activate") {
this.notifyFinalization({
...originalAction,
msisdn
@@ -215,7 +217,7 @@ export class CheckObjeniousRequests {
const PATH = "/actions/requests/"
const operationsList = structuredClone(requestList)
// TODO: El for es gigantesco hay que simplificar partes
for (const request of operationsList) {
if (request.id == undefined) continue;
@@ -228,13 +230,50 @@ export class CheckObjeniousRequests {
try {
res = await req
} catch (e) {
console.error("Error comprobando el estado de ", request, e)
//todo actualizar el estado para incluir el error
console.error("[x] Error comprobando el estado de ", request, e)
continue;
}
// 2. Casos de error o id no generada
if (res.data.massActionIds.length == 0) {
// Si no hay es que *puede* que haya un problema o no se ha generado todavia
const reports = res.data.actionRequestReports
// Se entiende que no hay report ni id = está a la espera
if (reports.length == 0) continue;
// ! Hay minimo un report -> se considera error y se para
const updateData: ObjeniousOperationChange = {
operation_id: request.id,
new_status: "error",
error: JSON.stringify(reports[0].actionRequestReportDataDTOs)
}
const updateRes = await this.operationsRepository.updateOperation(updateData)
if (updateRes.error != undefined) {
console.error("[x] Error actualizando el estado de la operacion", updateData.error)
}
if (request.correlation_id != undefined) {
this.orderRepository.errorOrder({
correlation_id: request.correlation_id,
status: "failed",
error: "MassId no obtenida",
reason: "MassId no obtenida",
stackTrace: JSON.stringify(reports[0].actionRequestReportDataDTOs)
}).then(e => {
if (e.error != undefined) {
console.error("[x] Error actualizando el estado del Order con correlation_id: ", request.correlation_id)
console.error(e.error)
}
}).catch(e => {
console.error("[x] Error actualizando el estado del Order con correlation_id: ", request.correlation_id)
})
}
continue;
}
// 2. Modificacion del massId si ha habido un cambio
const massActionId = res.data.massActionIds[0]
// 3. Modificacion del massId si ha habido un cambio
try {
if (res.status == 200 && res.data != undefined && massActionId != undefined) {
const updateData: ObjeniousOperationChange = {
@@ -248,7 +287,7 @@ export class CheckObjeniousRequests {
request.mass_action_id = String(massActionId)
}
} catch (e) {
console.log("Error actualizando el estado de ", request)
console.log("[x] Error actualizando el estado de ", request)
continue;
}
}
@@ -262,6 +301,8 @@ export class CheckObjeniousRequests {
* al servicio que manda los mails
*/
private async notifyFinalization(operation: ObjeniousOperation & { msisdn: string }) {
console.log("[i] Enviando activacion a", env.NOTIFICATION_URL)
console.log("[i] Operation", operation)
const req = axios.post(env.NOTIFICATION_URL, {
...operation,
iccids: [operation.iccids]
@@ -270,7 +311,17 @@ export class CheckObjeniousRequests {
"x-apikey-sim-activation": env.SIM_ACTIVATION_API_KEY
}
})
await req
try {
const res = await req
if (res.status != 200) {
console.error("[x] Error enviando el mail de confirmacion para ", operation, " status ", res.status, res.statusText)
}
} catch (e) {
console.error("[x] Error enviando el mail de confirmacion para ", operation)
console.error(e)
}
}
}

View File

@@ -0,0 +1,191 @@
import { ObjeniousLine } from "sim-shared/domain/objeniousLine.js";
import { PauseCancelTaskRepository } from "sim-consumidor-objenious/infrastructure/PauseCancelTaskRepository.js";
import { ObjeniousOperationsRepository } from "sim-shared/infrastructure/ObjeniousOperationRepository.js";
import { SimUseCases } from "sim-consumidor-objenious/aplication/Sim.usecases.js";
import { OrderRepository } from "sim-shared/infrastructure/OrderRepository.js";
const logger =
{
log: (...data: any[]) => console.log("[i] [TaskPauseTerminate]", ...data),
error: (...data: any[]) => console.error("[x] [TaskPauseTerminate] ", ...data),
}
export class PauseTerminateTask {
constructor(
private readonly objeniousRepo: ObjeniousOperationsRepository,
private readonly pauseRepo: PauseCancelTaskRepository,
private readonly simUsecases: SimUseCases,
private readonly orderRepo: OrderRepository
) {
}
public async run() {
const finError = (err: any) => {
logger.error("Finalizado con errores proceso de comprobacion de lineas en pausa o canceladas")
logger.error(err)
}
const finExito = () => {
logger.log("Finalizado con exito proceso de comprobacion de lineas en pausa o canceladas")
}
try {
logger.log("Iniciando proceso de comprobacion de lineas en pausa o canceladas")
// 1. Se comprueba cuantas peticiones hay qye revisar
const peticionesRevisar = await this.pauseRepo.getPending()
if (peticionesRevisar.error != undefined) {
finError(peticionesRevisar.error)
return 1;
}
logger.log(`Se van a revisar ${peticionesRevisar.data?.length} peticiones`)
if (peticionesRevisar.data == undefined || peticionesRevisar.data.length == 0) {
finExito()
return 0;
}
// 2. Se comprueba que alguna de las lineas haya dejado de estar en estado de test
const iccids = peticionesRevisar.data.map(e => e.iccid)
const lineasActualizadas: ObjeniousLine[] = []
const lineGenerator = this.objeniousRepo.getLinesByStatusAPI({
iccids: iccids
})
let lines = await lineGenerator.next()
if (lines.value.error != undefined || lines.value.data == undefined) {
logger.error("Error cargando las lineas", lines.value.error)
finError(lines.value.error)
return 1;
} else {
lineasActualizadas.push(...lines.value.data)
}
while (!lines.done) {
if (lines.value.error != undefined || lines.value.data == undefined) {
logger.error("Error cargando las lineas", lines.value.error)
finError(lines.value.error)
return 1;
} else {
lineasActualizadas.push(...lines.value.data)
}
lines = await lineGenerator.next()
}
console.log("Cargado: ", lineasActualizadas)
// 3. Se separan las lineas que se tienen que actualizar al no ser test
// y las que se tienen que reencolar al ser test
const lineasNoTest = lineasActualizadas.filter(e => e.status.billingStatus != "TEST")
const lineasTest = lineasActualizadas.filter(e => e.status.billingStatus == "TEST")
// 4. Las lineas de test se reencolan
// El proximo reintento es en 1 dia
const proximoReintento = new Date()
proximoReintento.setDate(new Date().getDate() + 1)
// 5. Reintentos en 1 dia
for (const linea of lineasTest) {
const lineaId = peticionesRevisar.data
.find(e => e.iccid == linea.identifier.iccid)?.id
if (lineaId == undefined) continue; // Esto puede ser un problema si se generaliza
this.pauseRepo.updateTask({
id: lineaId,
next_check: proximoReintento
})
}
// 6. Operaciones de pausa/cancelacion definitiva
for (const linea of lineasNoTest) {
const operacion = peticionesRevisar.data
.find(e => e.iccid == linea.identifier.iccid)
if (operacion == undefined) continue;
const dueDate = new Date()
dueDate.setMinutes(new Date().getMinutes() + 15)
const operacionTipo = operacion.operation_type
const actionData = operacion.action_data
const correlation_id = operacion.action_data.correlation_id
actionData.dueDate = dueDate.toISOString()
switch (linea.status.billingStatus) {
case "ACTIVATED":
let result = null;
// Se termina el proceso aqui pero pasa a ser una operación de
// objenious por lo que puede fallar y quedaria registrado en
// la tabla objenious_operation
switch (operacionTipo) {
case "suspend":
result = await this.simUsecases.suspend(actionData)()
break;
case "terminate":
result = await this.simUsecases.terminate(actionData)()
break;
default:
break;
}
if (result == undefined) {
logger.error("Operacion desconocida", operacion)
} else if (result?.error != undefined) {
// error usecase
logger.error(result.error)
await this.pauseRepo.finishTask({
id: operacion.id,
error: result.error
})
if (correlation_id != undefined)
await this.orderRepo.errorOrder({
correlation_id: correlation_id,
status: "dlx",
reason: result.error
})
} else {
// ok
await this.pauseRepo.finishTask({ id: operacion.id })
if (correlation_id != undefined)
await this.orderRepo.finishOrder({ correlation_id })
}
break;
case "CANCELED":
await this.pauseRepo.finishTask({
id: operacion.id,
error: "billingStatus is CANCELED"
})
if (correlation_id != undefined)
await this.orderRepo.finishOrder({ correlation_id })
break;
case "SUSPENDED":
await this.pauseRepo.finishTask({
id: operacion.id,
error: "billingStatus is SUSPENDED"
})
if (correlation_id != undefined)
await this.orderRepo.finishOrder({ correlation_id })
break;
case "TEST":
// No puede ser
default:
logger.error("billingStatus desconocido", linea.status.billingStatus)
}
}
finExito()
} catch (e) {
finError(e)
}
return 0
}
}

View File

@@ -0,0 +1,54 @@
import { lineToCreateLineDto, ObjeniousLine } from "sim-shared/domain/objeniousLine.js";
import { ObjeniousLinesRepository } from "../infranstructure/ObjeniousLinesRepository.js";
import { ObjeniousOperationsRepository } from "sim-shared/infrastructure/ObjeniousOperationRepository.js";
export class TaskVolcadoLineas {
constructor(
private readonly linesRepository: ObjeniousLinesRepository,
private readonly objeniousRepository: ObjeniousOperationsRepository
) {
}
private async saveLines(lines: ObjeniousLine[]) {
const linesToCreate = lines.map(lineToCreateLineDto)
let created: number[] = []
for (const line of linesToCreate) {
// Si es lento pasar a Promise.all
const res = await this.linesRepository.insertOrUpdate(line)
if (res?.id != undefined)
created.push(res.id)
}
}
public async loadLines() {
console.log("[i] Iniciando task de volcado de lineas de Objenious")
// Carga todas las lineas en memoria, hay que comprobar que no se gaste demasiada
const linesIterator = this.objeniousRepository.getLinesByStatusAPI({
pageSize: 100
})
let lines = await linesIterator.next()
if (lines.value.error != undefined || lines.value.data == undefined) {
console.error("[x] Error cargando las lineas a volcar", lines.value.error)
return;
}
await this.saveLines(lines.value.data)
while (!lines.done) {
lines = await linesIterator.next()
if (lines.value.error != undefined || lines.value.data == undefined) {
console.error("[x] Error cargando las lineas a volcar", lines.value.error)
return;
}
await this.saveLines(lines.value.data)
}
console.log("[i] Terminado task de volcado de lineas de Objenious")
}
}

View File

@@ -9,6 +9,7 @@
],
"include": [
"**/*.ts",
"**/*.d.ts",
"../../packages/sim-shared/**/*.ts",
"../../packages/sim-consumidor-objenious/**/*.ts"
]

View File

@@ -1,16 +1,16 @@
import { test, describe } from "vitest"
import { JWTService } from "./JWT.service.js"
import { jwtService } from "../config/jwtService.config.js"
describe("Tokens Objenious", () => {
const jwtService = new JWTService()
const jwt = jwtService
test("Solicicitud normal de auth", async () => {
const token = await jwtService.getAccessToken()
const token = await jwt.getAccessToken()
console.log("acceso objenious", token)
}),
test("Solicicitud de refresh de auth", async () => {
const token = await jwtService.tryRefreshToken()
const token = await jwt.tryRefreshToken()
console.log("acceso refresh objenious", token)
})
})

View File

@@ -4,24 +4,24 @@
* el cliente HTTP
*/
import { env } from "#config/env/index.js";
import fs from "fs"
import {
JWTToken,
JWTHeader,
IJWTService
IJWTService,
JWTPayload
} from "sim-shared/domain/JWT.js"
import axios, { AxiosError } from "axios";
type GrantAccessRequestBody = {
export type GrantAccessRequestBody = {
grant_type: string,
client_id: string,
client_assertion_type: string,
client_assertion: string
}
type TokensRequestResponse = {
export type TokensRequestResponse = {
"access_token": string,
"expires_in": number,
"refresh_token": string
@@ -32,41 +32,6 @@ type TokensRequestResponse = {
"scope": string
}
const PRIVATE_KEY_PATH = env.OBJ_PEM_PATH
const GET_TOKEN_URL = "https://idp.docapost.io/auth/realms/GETWAY/protocol/openid-connect/token"
const REFRESH_TOKEN_URL = GET_TOKEN_URL
const DEFAULT_BODY: GrantAccessRequestBody = {
grant_type: "client_credentials",
client_id: env.OBJ_CLIENT_ID,
client_assertion_type: "urn:ietf:params:oauth:client-assertion-type:jwt-bearer",
client_assertion: env.OBJ_CLI_ASSERTION
}
const REFRESH_BODY = {
...DEFAULT_BODY,
grant_type: "refresh_token",
}
const DEFAULT_HEADERS = {
"content-type": "application/x-www-form-urlencoded"
}
function addIATHeaders(authHeaders: Object) {
const headers = <JWTHeader>{
...authHeaders,
sub: env.OBJ_CLIENT_ID,
iss: env.OBJ_CLIENT_ID,
aud: GET_TOKEN_URL,
jti: Date.now().toString(),
iat: Math.floor(Date.now() / 1000),
exp: Math.floor(Date.now() / 1000) + 5 * 60,
}
return headers
}
export type ObjeniousTokenBody = any
/**
@@ -82,27 +47,54 @@ export class JWTService implements IJWTService<ObjeniousTokenBody> {
public authToken: JWTToken<ObjeniousTokenBody> | undefined;
private refreshToken?: JWTToken<ObjeniousTokenBody> | undefined;
constructor(args?: {
// http
private transformHeaders?: (_: Object) => JWTHeader;
private defaultHttpHeaders: Record<string, string>;
private defaultBody: Record<string, string>;
// jwt
private defaultJWTHeaders: JWTHeader;
private defaultJWTPayload: JWTPayload<any>;
private privateKeyPath: string;
private tokenUrl: string;
private refreshTokenUrl: string;
constructor(args: {
token?: string // si se partiese de un token existente,
refreshToken?: string
refreshToken?: string,
transformJWTHeaders?: (_: Object) => JWTHeader,
defaultHeaders: Record<string, string>,
defaultBody: Record<string, string>,
defaultJWTHeaders: JWTHeader,
defaultJWTPayload: JWTPayload<any>,
privateKeyPath: string,
tokenUrl: string,
refreshTokenUrl: string
}) {
if (args?.token != undefined) this.authToken = new JWTToken(args.token)
if (args?.refreshToken != undefined) this.refreshToken = new JWTToken(args.refreshToken)
if (args?.transformJWTHeaders != undefined) this.transformHeaders = args.transformJWTHeaders
this.defaultHttpHeaders = args.defaultHeaders;
this.defaultBody = args.defaultBody;
this.defaultJWTHeaders = args.defaultJWTHeaders;
this.defaultJWTPayload = args.defaultJWTPayload;
this.privateKeyPath = args.privateKeyPath;
this.tokenUrl = args.tokenUrl;
this.refreshTokenUrl = args.refreshTokenUrl;
}
private buildJwtBody() {
const jwtHeaders = {
alg: "RS256",
typ: "JWT",
kid: env.OBJ_KID
}
const jwtData = addIATHeaders({
sub: env.OBJ_CLIENT_ID,
iss: env.OBJ_CLIENT_ID,
aud: "https://idp.docapost.io/auth/realms/GETWAY",
jti: Date.now().toString(),
})
const key = fs.readFileSync(PRIVATE_KEY_PATH, "utf8")
const jwtHeaders = this.defaultJWTHeaders
const jwtData = (this.transformHeaders) ?
this.transformHeaders(this.defaultJWTPayload) :
this.defaultJWTPayload;
const key = fs.readFileSync(this.privateKeyPath, "utf8")
const token = JWTToken.fromParts({
header: jwtHeaders,
payload: jwtData,
@@ -116,14 +108,16 @@ export class JWTService implements IJWTService<ObjeniousTokenBody> {
public async getNewAuthToken() {
const bodyWithtoken = {
...DEFAULT_BODY,
...this.defaultBody,
client_assertion: this.buildJwtBody()
}
const req = axios.post(GET_TOKEN_URL,
const headers = (this.transformHeaders) ? this.transformHeaders(this.defaultHttpHeaders) : this.defaultHttpHeaders;
const req = axios.post(this.tokenUrl,
bodyWithtoken,
{
headers: addIATHeaders(DEFAULT_HEADERS)
headers: headers
}
)
@@ -166,16 +160,21 @@ export class JWTService implements IJWTService<ObjeniousTokenBody> {
if (this.refreshToken == undefined) throw new Error("El refreshToken no está definido")
if (this.refreshToken.isExpired()) throw new Error("El refreshToken ha expirado")
const refreshBody = {
...this.defaultBody,
grant_type: "refresh_token",
}
const body = {
...REFRESH_BODY,
...refreshBody,
client_assertion: this.buildJwtBody(),
refresh_token: this.refreshToken.rawToken
}
const req = axios.post(REFRESH_TOKEN_URL,
const req = axios.post(this.refreshTokenUrl,
body,
{
headers: DEFAULT_HEADERS
headers: this.defaultHttpHeaders
}
)

View File

@@ -7,9 +7,12 @@
import { env, loadEnvFile } from "node:process";
import { Pool } from "pg";
import { PgClient } from "../infrastructure/PgClient.js";
import { HttpClient } from "../infrastructure/HTTPClient.js";
import { jwtService } from "./jwtService.config.js";
console.warn("[i!] Se está corriendo codigo de test")
loadEnvFile("../../.env") // Global
loadEnvFile("./test.env") // Local
// se hace una por servicio.
export const pgPool = new Pool({
@@ -24,4 +27,14 @@ export const postgresClient = new PgClient({
pool: pgPool
})
const OBJ_BASE_URL = "https://api-getway.objenious.com/ws"
export const httpObjClient = new HttpClient({
baseURL: OBJ_BASE_URL,
headers: {
"content-type": " application/json; charset=utf-8"
},
jwtManager: jwtService
})
console.warn(`[T] TEST DB : ${env.POSTGRES_DATABASE}@${env.POSTGRES_HOST}`)

View File

@@ -0,0 +1,67 @@
import assert from "assert"
import { env, loadEnvFile } from "process"
import { GrantAccessRequestBody, JWTService } from "sim-shared/aplication/JWT.service.js"
import { JWTHeader } from "sim-shared/domain/JWT.js"
loadEnvFile("../../.env") // Global
loadEnvFile("./test.env") // Local
assert(env.OBJ_CLIENT_ID != undefined)
assert(env.OBJ_CLI_ASSERTION != undefined)
assert(env.OBJ_PEM_PATH != undefined)
const PRIVATE_KEY_PATH = env.OBJ_PEM_PATH
const GET_TOKEN_URL = "https://idp.docapost.io/auth/realms/GETWAY/protocol/openid-connect/token"
const REFRESH_TOKEN_URL = GET_TOKEN_URL
const DEFAULT_BODY: GrantAccessRequestBody = {
grant_type: "client_credentials",
client_id: env.OBJ_CLIENT_ID,
client_assertion_type: "urn:ietf:params:oauth:client-assertion-type:jwt-bearer",
client_assertion: env.OBJ_CLI_ASSERTION
}
const DEFAULT_HEADERS = {
"content-type": "application/x-www-form-urlencoded"
}
const DEFAULT_HEADERS_JWT = {
alg: "RS256",
typ: "JWT",
kid: env.OBJ_KID,
}
const DEFAULT_DATA_JWT = {
sub: env.OBJ_CLIENT_ID,
iss: env.OBJ_CLIENT_ID,
aud: "https://idp.docapost.io/auth/realms/GETWAY",
jti: Date.now().toString(),
}
function addIATHeaders(authHeaders: Object) {
const headers = <JWTHeader>{
...authHeaders,
sub: env.OBJ_CLIENT_ID,
iss: env.OBJ_CLIENT_ID,
aud: GET_TOKEN_URL,
jti: Date.now().toString(),
iat: Math.floor(Date.now() / 1000),
exp: Math.floor(Date.now() / 1000) + 5 * 60,
}
return headers
}
export const jwtService = new JWTService({
transformJWTHeaders: addIATHeaders,
defaultHeaders: DEFAULT_HEADERS,
defaultBody: DEFAULT_BODY,
defaultJWTHeaders: DEFAULT_HEADERS_JWT,
defaultJWTPayload: DEFAULT_DATA_JWT,
privateKeyPath: PRIVATE_KEY_PATH,
tokenUrl: GET_TOKEN_URL,
refreshTokenUrl: REFRESH_TOKEN_URL
})

View File

@@ -62,11 +62,14 @@ export type CreateOrderDTO = Pick<
'correlation_id' | 'exchange' | 'routing_key' | 'order_type' | 'payload' | 'webhook_host' | 'webhook_endpoint'
>;
export type UpdateOrderDTO =
type IdOrCorrelationID =
(
{ id: number, correlation_id?: never } |
{ id?: never, correlation_id: string }
)
export type UpdateOrderDTO =
IdOrCorrelationID
&
{
new_status: OrderStatus,
@@ -74,12 +77,21 @@ export type UpdateOrderDTO =
}
export type FinishOrderDTO =
(
{ id: number, correlation_id?: never } |
{ id?: never, correlation_id: string }
)
IdOrCorrelationID
&
{
reason?: string
reason?: string,
end_date?: Date
}
export type ErrorOrderDTO =
IdOrCorrelationID
&
{
status: "failed" | "dlx",
reason: string,
error?: string,
stackTrace?: string
}

View File

@@ -14,7 +14,7 @@ export type Failure<E = Error> = {
*/
export type Result<E, D> = Failure<E> | Success<D>
export async function tryCatch<T>(func: Promise<T>): Promise<Result<{ msg: Error }, T>> {
export async function tryCatch<T>(func: Promise<T>): Promise<Result<Error, T>> {
try {
const res = await func;
return {
@@ -22,9 +22,8 @@ export async function tryCatch<T>(func: Promise<T>): Promise<Result<{ msg: Error
}
} catch (e: unknown) {
return {
error: {
msg: e as Error
}
error: e as Error
}
}
}

View File

@@ -24,7 +24,7 @@ export namespace SimEvents {
}
export type reActivation = DomainEvent & {
key: `sim.${string}.reActivate`,
key: `sim.${string}.reactivate`,
payload: {
iccid: string
},
@@ -47,6 +47,7 @@ export namespace SimEvents {
options: {
}
}
export type suspend = pause
export type free = DomainEvent & {
key: `sim.${string}.free`,

View File

@@ -0,0 +1,144 @@
export type ObjeniousLineResponse = {
content: ObjeniousLine[],
offset: number,
pageNumber: number,
pageSize: number,
paged: boolean,
totalPages: number,
totalElements: number
}
export type ObjeniousLine = {
identifier: {
simId: number,
iccid: string,
imsi: string,
msisdn: string,
amsisdn?: string,
imei: string
},
simCardType: {
code: string,
description: string
},
device: {
imei: string,
imeiChangeDate: string, //Fecha iso
deviceReference?: string | null,
manufacturer?: string | null,
},
customerAccount: {
code: string,
label: string,
address: {
address1: string,
address2: string,
address3: string,
zipCode: string,
city: string,
country: string,
state?: string | null
}
},
offer: {
code: string,
description: string,
},
party: {
name: string,
code: string,
contractReference: string,
partyType: string,
},
lineCustomFields: {
custom1: {
label: string | null,
value: string | null
},
custom2: {
label: string | null,
value: string | null
},
custom3: {
label: string | null,
value: string | null
},
custom4: {
label: string | null,
value: string | null
},
custom5: {
label: string | null,
value: string | null
},
custom6: {
label: string | null,
value: string | null
}
},
status: {
status: string,
preactivationDate: string | null, //"2026-03-17",
activationDate: string | null, //"2026-03-17T11:04:11.408+00:00",
commercialStatus: string, //"test",
commercialStatusDate: string, //"2026-03-17T11:41:01.493+00:00",
networkStatus: string, // "ACTIVATED",
billingStatus: "ACTIVATED" | "SUSPENDED" | "CANCELED" | "TEST",
billingStatusChangeDate: string | null, // "2026-03-17T11:01:00.276+00:00",
billingActivationDate: string | null //,
createdDate: string | null,//"2026-01-30T01:50:02.060+00:00"
},
services: string | null
};
export type ObjeniousLineDb = {
id: number;
simId?: number;
iccid: string;
msisdn?: string;
imei?: string;
imeiChangeDate?: Date;
offerCode?: string;
status?: string;
preactivationDate?: Date | null;
activationDate?: Date | null;
commercialStatus?: string;
commercialStatusDate?: Date | null;
billingStatus?: string;
billingStatusChangeDate?: Date | null;
billingActivationDate?: Date | null;
createDate?: Date | null;
raw: ObjeniousLine;
}
// DTO para inserción (omite el ID autogenerado)
export type CreateObjeniousLineDTO = Omit<ObjeniousLineDb, 'id'>;
export function lineToCreateLineDto(line: ObjeniousLine): CreateObjeniousLineDTO {
const dateOrNull = (data: string | null) => {
if (data == null) return null;
return new Date(data)
}
const transformed: CreateObjeniousLineDTO = {
simId: line.identifier.simId,
iccid: line.identifier.iccid,
msisdn: line.identifier.msisdn,
imei: line.identifier.imei,
imeiChangeDate: new Date(line.device.imeiChangeDate),
offerCode: line.offer.code,
status: line.status.status,
preactivationDate: dateOrNull(line.status.preactivationDate),
activationDate: dateOrNull(line.status.activationDate),
commercialStatus: line.status.commercialStatus,
commercialStatusDate: dateOrNull(line.status.commercialStatusDate),
billingStatus: line.status.billingStatus,
billingStatusChangeDate: dateOrNull(line.status.activationDate),
billingActivationDate: dateOrNull(line.status.activationDate),
createDate: dateOrNull(line.status.activationDate),
raw: line
}
return transformed;
}

View File

@@ -12,7 +12,7 @@ export type ObjeniousOperation = {
id?: number;
/** Uuid del mensaje asociado a la operacion */
correlation_id?: string;
operation: string;
operation: "activate" | "suspend" | "terminate" | string; // TODO: completar y actualizar
retry_count?: number;
max_retry?: number;
max_date_retry?: string | null;
@@ -27,8 +27,7 @@ export type ObjeniousOperation = {
}
export type ObjeniousOperationChange = {
id?: number;
operation_id: number;
id?: number; operation_id: number;
info?: string | null;
error?: string | null;
new_status: StatusEnum;
@@ -46,10 +45,34 @@ export namespace Objenious {
created: string,
status: "NEW" | "RUNNING" | "OK" | "KO" | "REPLAYED" | "CANCELLED" | "CLOSED" | "DISABLED",
statusDate: string,
actionType: "PREACTIVATION_AND_ACTIVATION" | string, // todo: añadir el resto
massActionIds: number[]
actionType: ActionType
massActionIds: number[],
actionRequestReports:
{
requestId: string,
actionRequestReportDataDTOs: [
{
data: string,
newData: string | null,
iccid: string,
dataStatus: DataStatus
}
]
}[],
}
export type DataStatus = "DATA_INVALID_FORMAT" | "DATA_NOT_FOUND" | "DATA_NOT_ACTIVATED" | "SERVICE_DATA_NOT_ACTIVATED" |
"DATA_WRONG_STATUS" | "DATA_NOT_AUTHORIZED" | "DATA_CUSTOMER_ACCOUNT_NOT_AUTHORIZED" | "DATA_AMBIGUOUS" |
"NEW_DATA_INVALID_FORMAT" | "NEW_DATA_ALREADY_EXISTS" | "DUPLICATE_DATA" | "DATA_TERMINATION_VALIDATED" |
"DATA_TERMINATION_SECURISED" | "MAX_ALARM_INSTANCE" | "MAX_ALARM_INSTANCE_TO_CATCH_UP" |
"ACTIVATED_LINE_CANNOT_BE_TRANSFERED" | "ESIM_WRONG_STEP" | "ESIM_WRONG_PAIRED_VALUE" |
"ESIM_WRONG_DOWNLOAD_STATE" | "ESIM_WRONG_STATUS" | "ESIM_WRONG_FAMILY" | "ESIM_WRONG_CATEGORY" |
"ENTITY_STATUS_NOT_AUTHORIZED" | "LONG_LIFE_NOT_ALLOWED" | "RCARD_NOT_COMPATIBLE" | "APN_NOT_FOUND" |
"APN_OR_DNN_NOT_FOUND" | "APN_CONFIGURATION_NOT_FOUND" | "APN_CONFIGURATION_INVALID_PARAMETER_FILE" |
"IP_NOT_AVAILABLE" | "RADIUS_FIELD_LENGTH_NOT_ALLOWED" | "RADIUS_LOGIN_OR_PASSWORD_NOT_FOUND" | "RADIUS_PASSWORD_NOT_ALLOWED" |
"RADIUS_LOGIN_NOT_ALLOWED" | "NETWORK_NOT_ACTIVATED" | "CHANGE_CUSTOMER_ACCOUNT_NOT_AllOWED" | "CHANGE_OFFER_NOT_ALLOWED" |
"SIM_NOT_EUICC" | "OFFER_NOT_WSF_PALIER_FLOTTE_FR"
export type ActionType = "PREACTIVATION" | "PREACTIVATION_ACTIVATION" | "ACTIVATION" |
"STATUS_CHANGE" | "ICCID_CHANGE" | "EUICC_NOTIFICATION"
| "EUICC_AUDIT" | "MSISDN_CHANGE" | "ALARM_SETTING"

View File

@@ -0,0 +1,39 @@
import { before, describe, it } from "node:test";
import { ObjeniousOperationsRepository } from "./ObjeniousOperationRepository.js";
import { httpObjClient, postgresClient } from "../config/config.test.js";
import { ObjeniousOperation } from "../domain/operationsRepository.port.js";
const correctOperation: ObjeniousOperation = {
iccids: "test",
operation: "activate",
status: "finished"
}
const errorOperation: ObjeniousOperation = {
iccids: "test",
operation: "terminate",
status: "error",
error: "mensaje de error"
}
describe("[Integration] Test API requests", () => {
const repository = new ObjeniousOperationsRepository(
httpObjClient,
postgresClient
)
before(async () => {
await repository.createOperation(correctOperation)
await repository.createOperation(errorOperation)
})
it("Read last operation by line", () => {
/**
* Objetivo:
* - Cuando se va a hacer una operacion de sim hay que cancelarla directamente si:
* - Ya hay una en curso del mismo tipo.
* - Ya ha terminado una del mismo tipo.
* - Se ignoran las erroneas
*/
})
})

View File

@@ -1,14 +1,139 @@
import { IOperationsRepository, ObjeniousOperation, ObjeniousOperationChange } from "sim-shared/domain/operationsRepository.port.js";
import { Result } from "sim-shared/domain/Result.js";
import { Result, tryCatch } from "sim-shared/domain/Result.js";
import { PgClient } from "sim-shared/infrastructure/PgClient.js";
import { ObjeniousLine, ObjeniousLineResponse } from "../domain/objeniousLine.js";
import { HttpClient } from "./HTTPClient.js";
import assert from "node:assert";
import { AxiosResponse } from "axios";
export class ObjeniousOperationsRepository implements IOperationsRepository {
constructor(
private http: HttpClient,
private readonly pgClient: PgClient
) {
}
/**
* Consulta el estado de una o mas lineas directamente a la API de Objenious
* TODO: No hay paginacion como en getLinesByStatusAPI
*/
public async getLinesAPI(
identifierType: "ICCID" | "IMSI" | "IMEI" | "MSISDN" | "REFERENCE",
identifiers: string[]
): Promise<Result<string, ObjeniousLine[]>> {
if (identifiers.length == 0) {
return {
data: []
}
}
// Comprobar < MAX_PAGE_SIZE (Poco probable)
const path = "/lines"
const params = {
"identifier.identifierType": identifierType,
"identifier.identifiers": identifiers.toString()
}
const req = this.http.client.get<ObjeniousLineResponse>(path, {
params: params
})
const res = await tryCatch(req)
if (res.error != undefined) {
return {
error: res.error?.message
}
}
const lines = res.data.data.content
return {
data: lines
}
}
private MAX_PAGE_SIZE = 1000
public async * getLinesByStatusAPI(args?: {
pageSize?: number,
pageNumber?: number,
status?: string,
iccids?: string[]
}): AsyncGenerator<Result<string, ObjeniousLine[]>, Result<string, ObjeniousLine[]>, any> {
const path = "/lines"
const pageSize = args?.pageSize ?? this.MAX_PAGE_SIZE;
let currentPage = args?.pageNumber ?? 0;
let totalPages: number | undefined = undefined; // Como limite de paginas, igual es pasarse pero hasta que se lea
const params: Record<string, string | number> = {}
// Si se va a filtrar por iccids especificamente, en un futuro habra que ampliar el tipo de filtros
if (args?.iccids != undefined) {
params["identifier.identifierType"] = "ICCID"
params["identifier.identifiers"] = args.iccids.toString()
}
const loadNextLine = async (page: number): Promise<Result<string, ObjeniousLine[]>> => {
if (args?.status != undefined) params["simStatus"] = args.status
params["pageSize"] = pageSize
params["pageNumber"] = page
console.log(`[i] Cargando pagina ${currentPage} de ${totalPages ?? "(desc)"}`)
const nextPage = await tryCatch<AxiosResponse<ObjeniousLineResponse>>(this.http.client.get(path, {
params: params
}))
if (nextPage.error != undefined) {
console.error(nextPage.error)
return {
error: nextPage.error.message
}
}
// Se aumenta para la siguiente ejecucion
console.log(`[i] Página ${currentPage} completa, total: ${nextPage.data.data.totalPages}`)
totalPages = nextPage.data.data.totalPages
return {
data: nextPage.data.data.content
}
}
// El inicio se ejecuta siempre
const lines = await loadNextLine(currentPage)
if (lines.error != undefined) {
console.error("[x] Error obteniendo las lineas, cancelando operación");
return {
error: "Error cargando lineas"
}
}
currentPage++;
yield {
data: lines.data
}
// Copia para evitar bucles infinitos por error de la api
const maxPages = totalPages
assert.ok(maxPages != undefined, "No se ha defindo el numero de paginas") // Nunca deberia pasar pero así se evitan bucles infnitos
console.log("maxPages", maxPages)
for (let i = currentPage; i < maxPages!; i++) {
console.log("Bucle i:", i, "page: ", currentPage)
yield await loadNextLine(currentPage);
currentPage++;
}
return {
data: []
}
}
async createOperation(data: ObjeniousOperation): Promise<Result<string, ObjeniousOperation>> {
const query = `
INSERT INTO objenious_operation (operation, iccids, status, max_retry, request_id)
@@ -21,6 +146,20 @@ export class ObjeniousOperationsRepository implements IOperationsRepository {
}
}
async getLastOperationOfLine(iccid: string) {
const query = `
SELECT * FROM public.objenious_operation
WHERE iccids = $1 and error is null
ORDER BY id asc limit 1
`
const values = [iccid];
const { rows } = await this.pgClient.query(query, values);
return <Result<string, ObjeniousOperation>>{
data: rows[0]
}
}
async updateOperation(data: ObjeniousOperationChange): Promise<Result<string, ObjeniousOperation>> {
const client = await this.pgClient.connect();
const {
@@ -46,7 +185,7 @@ export class ObjeniousOperationsRepository implements IOperationsRepository {
request_id = COALESCE($4, request_id),
mass_action_id = COALESCE($5, mass_action_id),
last_change_date = now() at time zone 'utc',
end_date = CASE WHEN $2 IN ('finished') THEN now() at time zone 'utc' ELSE end_date END,
end_date = CASE WHEN $2 IN ('finished','error') THEN now() at time zone 'utc' ELSE end_date END,
objenious_status = $6
WHERE id = $1`;

View File

@@ -27,7 +27,7 @@ describe("Test OrderRepository", {}, (ctx) => {
before(async () => {
// Order1
const result1 = await orderRepo.createOrder(order1)
assert(result1.data != undefined)
assert.ok(result1.data != undefined, result1.error as string)
testIds.push(result1.data.id)
// Order2 -> Para el test de crearOrder

View File

@@ -2,11 +2,10 @@
* TODO: Usar
*/
import { PoolClient, QueryResult, QueryResultRow } from "pg";
import { CreateOrderDTO, FinishOrderDTO, OrderTracking, UpdateOrderDTO } from "../domain/Order.js";
import { Result } from "../domain/Result.js";
import { CreateOrderDTO, ErrorOrderDTO, FinishOrderDTO, OrderTracking, UpdateOrderDTO } from "../domain/Order.js";
import { Result, tryCatch } from "../domain/Result.js";
import { PgClient } from "./PgClient.js";
import assert from "node:assert";
import { error } from "node:console";
/**
* Agrupa todas las operaciones de *Order*.
@@ -19,9 +18,8 @@ import { error } from "node:console";
*/
export class OrderRepository {
constructor(
private readonly pgClient: PgClient
private readonly pgClient: PgClient,
) {
}
/**
@@ -57,6 +55,8 @@ export class OrderRepository {
}
}
/**
* El tipo <T> representa el contenido del mensaje de los order
*/
@@ -191,6 +191,8 @@ export class OrderRepository {
const orderId = currentOrderResult.data?.id
if (orderId == undefined) {
await client.query("ROLLBACK")
client.release()
return {
error: "El order a actualizar no existe " + idType + ": " + idValue
}
@@ -261,7 +263,6 @@ export class OrderRepository {
return updatedOrder
}
public async finishOrder(args: FinishOrderDTO) {
const client = await this.pgClient.connect();
assert((args.id != undefined) != (args.correlation_id != undefined))
@@ -281,6 +282,8 @@ export class OrderRepository {
const orderId = currentOrderResult.data?.id
if (orderId == undefined) {
await client.query("ROLLBACK")
client.release()
return {
error: "El order a actualizar no existe " + idType + ": " + idValue
}
@@ -299,8 +302,8 @@ export class OrderRepository {
UPDATE order_tracking
SET
status = 'finished',
update_date = (now() at time zone 'utc'),
finish_date = (now() at time zone 'utc')
update_date = now(),
finish_date = now()
WHERE id = $1
RETURNING id, status, update_date;
`
@@ -353,22 +356,19 @@ export class OrderRepository {
}
// TODO: tema de poder filtrar por correlation_id
public async errorOrder(args: {
id: number,
status: "failed" | "dlx",
reason: string,
error?: string,
stackTrace?: string
}) {
public async errorOrder(args: ErrorOrderDTO): Promise<Result<string, OrderTracking<any>>> {
const client = await this.pgClient.connect();
await client.query('BEGIN');
const idType = ('id' in args) ? "id" : "correlation_id"
const idValue = (args.id != undefined) ? args.id : args.correlation_id
// 1. Se consulta la order de base
const qCurrentOrder = `
SELECT * FROM order_tracking
WHERE id = $1
WHERE ${idType} = $1
`
const vCurrentOrder = [args.id]
const vCurrentOrder = [idValue]
const currentOrderResult = await this.getFirst(client.query<OrderTracking<any>>(qCurrentOrder, vCurrentOrder))
@@ -378,7 +378,9 @@ export class OrderRepository {
return currentOrderResult
}
const id = currentOrderResult.data.id // Saco el id para evitar busacr por correlation_id que es mas lento
const currentOrder = currentOrderResult.data!
//console.log("Current Order", currentOrder)
// 3. Si todo ok se actualiza el order
// Si el status es dlx se asume que ha terminado y no va a reintentarse
@@ -395,11 +397,13 @@ export class OrderRepository {
WHERE id = $1
RETURNING id, status, update_date;
`
const vOrderTracking = [args.id, args.status, args.error, args.stackTrace]
const vOrderTracking = [id, args.status, args.error, args.stackTrace]
const updatedOrderResult = await this.getFirst(
client.query<{ id: number, status: string, update_date: string }>(uOrderTracking, vOrderTracking)
)
console.log("updatedOrderResult", updatedOrderResult)
if (updatedOrderResult.error != undefined) {
await client.query("ROLLBACK")
client.release()
@@ -422,7 +426,7 @@ export class OrderRepository {
)
RETURNING id;
`
const vOrderHistory = [args.id, currentOrder.status, args.status, args.reason]
const vOrderHistory = [currentOrder.id, currentOrder.status, args.status, args.reason]
const newOrderHistoryResult = await this.getFirst(
client.query<{ id: number }>(iOrderHistory, vOrderHistory)
)

View File

@@ -21,15 +21,22 @@ export class RabbitMQEventBus implements EventBus {
channel?: ChannelWrapper
connected: Boolean = false
private delayedExchange: string;
private dlxExchange: string;
private connectionOptions: RMQConnectionParams
constructor(args: {
connectionParams: RMQConnectionParams,
buildStructure?: (chan: Channel) => Promise<void>,
maxRetry?: number
maxRetry?: number,
delayedExchange: string,
dlxExchange: string
}) {
this.connectionOptions = args.connectionParams
if (args.buildStructure != undefined) this.buildStructure = args.buildStructure
if (args.maxRetry != undefined) this.maxRetry = args.maxRetry
this.delayedExchange = args.delayedExchange
this.dlxExchange = args.dlxExchange
}
async consume(queue: string, callback: (msg: ConsumeMessage | null) => void) {
@@ -50,23 +57,25 @@ export class RabbitMQEventBus implements EventBus {
async nack(msg: ConsumeMessage, requeue?: boolean) {
if (this.channel == undefined) throw new Error("[RMQ] Canal no iniciallizado");
console.log("NACK: ", msg.properties.headers)
console.log("[i] NACK: ", msg.properties.headers)
const headers = msg.properties.headers || {}
const numberRetry = headers['x-retry-count'] || 0
const routingKey = msg.fields.routingKey
if (numberRetry < this.maxRetry) {
console.log("Delaying")
await this.channel.publish("sim.ex.objenious.delayed", routingKey, msg.content, {
console.log("[i] Delaying ")
// "sim.ex.objenious.delayed"
await this.channel.publish(this.delayedExchange, routingKey, msg.content, {
headers: {
...headers,
'x-retry-count': numberRetry + 1
}
})
} else {
console.log("DeadLetter")
await this.channel.publish("sim.ex.objenious.dlx", routingKey, msg.content, {
console.log("[i] DeadLetter")
//"sim.ex.objenious.dlx"
await this.channel.publish(this.dlxExchange, routingKey, msg.content, {
headers: {
...headers
}

Some files were not shown because too many files have changed in this diff Show More