108 Commits

Author SHA1 Message Date
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
7a7dc33724 Error de prod 2026-03-05 10:30:43 +01:00
7743bd1f0d Migraciones a mano de momento 2026-03-05 10:17:26 +01:00
2897d7aa3c Probando a añadir el registro desde jenkins 2026-03-05 10:10:12 +01:00
0fd7eafcf3 Eliminado el clone 2026-03-05 09:09:02 +01:00
71253d216e Registry local 2026-03-05 09:04:50 +01:00
aeea6cfefd Probando con clone 2026-03-04 16:49:07 +01:00
e8eb925834 Sin paso 2026-03-04 15:54:13 +01:00
7cf9cc60e6 Test jenkins 2026-03-04 15:52:43 +01:00
1e9818d430 Yarn lock 2026-03-04 14:03:11 +01:00
39c0e87758 Mejora de las orders y actualizacion docs 2026-03-04 13:51:24 +01:00
5771972e2a Revesriendo cambio del docker 2026-03-02 17:19:07 +01:00
ea13403dc3 Error https 2026-03-02 17:16:34 +01:00
8d9a9b84b8 Cambiando el lock a mano 2026-03-02 17:15:04 +01:00
9b92f3506b Ya no hace falta la eliminacion explicita 2026-03-02 16:57:46 +01:00
1798118f6b Sin yarn.lock que copiar 2026-03-02 16:51:12 +01:00
eba2b8c569 Ya con la eliminacion del lock 2026-03-02 16:48:18 +01:00
b6b2cf6cc8 El inmutable 2026-03-02 16:46:29 +01:00
a0faa2d105 Jenkins 2026-03-02 16:45:35 +01:00
d323f804fc No copiar el lock 2026-03-02 16:41:37 +01:00
978454754c Eliminado yarn lock 2026-03-02 16:29:35 +01:00
b6091b15da docker con clean del cache 2026-03-02 16:23:05 +01:00
a6794a061b Yarn install 2026-03-02 16:01:00 +01:00
fafea3ce04 http 2026-03-02 15:55:32 +01:00
992f639f35 Prueba con otra url para gitea 2026-03-02 15:38:42 +01:00
f57309b06a Preparado despliegue 2026-03-02 15:07:30 +01:00
3be2b8f20d Nuevo nombre del container 2026-03-02 14:57:43 +01:00
4853fec7ff Fix de gestion de orders
Proceso de cancelacion verificado
2026-02-27 13:43:09 +01:00
04a6e50b7a Orders en todas las etapas 2026-02-27 11:16:45 +01:00
8ca3d095e6 Fix suspension && paso a plantilla de caso de uso 2026-02-26 17:47:32 +01:00
ca1144b55c Orders en los consumidores y gestion de los demas casos de uso 2026-02-26 17:30:32 +01:00
18422fbe38 Order para pause, activate y terminate 2026-02-25 17:42:16 +01:00
f221035c8b Visualizacion via api de las operaciones pendientes 2026-02-25 17:23:22 +01:00
02c80cd503 Orders con endpoints para monitorizacion 2026-02-25 12:20:52 +01:00
c416114c50 Arrglos por el cambio de nombre 2026-02-24 12:44:19 +01:00
e329b36933 Orders para test y flujo de migraciones mas simple 2026-02-24 11:27:47 +01:00
5c64c84e2a Todos los test de orders pasan 2026-02-23 13:35:36 +01:00
fc319372be Integracion completa de las migraciones 2026-02-23 12:04:21 +01:00
12dae135b5 Scripts de inicio con migraciones 2026-02-20 10:59:15 +01:00
b208c9c301 Preparando proceso de despliegue local para que se parezca al de
desarrollo, problema de las migraciones
2026-02-20 10:47:28 +01:00
1583ae539e Organizadas las migraciones para el despliegue 2026-02-19 17:24:47 +01:00
b6ec37c339 cambio de nombre por proposito 2026-02-17 17:24:13 +01:00
459523666f Mejora migraciones con tabla de versiones 2026-02-17 17:22:20 +01:00
8427613114 Intento de migraciones con script generador 2026-02-17 13:46:16 +01:00
5d3465fd97 Test para todo el repositorio de orders 2026-02-17 09:33:51 +01:00
39a2622cb1 base de datos de orders con repositorio y test 2026-02-16 17:31:20 +01:00
0a42e4776d Merge branch 'main' into seguimiento-tareas 2026-02-13 10:57:54 +01:00
44fea21a56 Fix de api-key y mejora del control de versiones 2026-02-13 10:55:19 +01:00
8a53fb6749 iccid en lista 2026-02-12 09:41:00 +01:00
3a55e5f057 Caracteres de escaoe en el iccid 2026-02-12 08:56:11 +01:00
6bc1340930 operation 2026-02-11 20:24:33 +01:00
2299d5f735 Body 2026-02-11 20:08:22 +01:00
e454a3fee1 url 2026-02-11 19:56:46 +01:00
672de92e5b Fix url 2026-02-11 19:53:55 +01:00
404f142c7e Inicio del cron 2026-02-11 19:46:33 +01:00
8fcf592b88 URLURL 2026-02-11 19:43:54 +01:00
d91e7e746c Config 2026-02-11 19:34:31 +01:00
f176788267 Created deployment 2026-02-11 18:53:40 +01:00
c4721328f8 Created deployment 2026-02-11 18:52:12 +01:00
e878c9a27e Created deployment 2026-02-11 18:41:02 +01:00
d17e399a4c Created deployment 2026-02-11 18:21:49 +01:00
068ec20a9b Merge branch 'main' of git.savefamilygps.net:SaveFamily/sf-sim 2026-02-11 18:11:13 +01:00
b32e1af71c error env 2026-02-11 18:11:10 +01:00
fe5b492218 Error de la ruta de env 2026-02-11 18:10:39 +01:00
a217914ec6 Created deployment 2026-02-11 18:07:54 +01:00
669abd3c43 Created deployment 2026-02-11 17:59:46 +01:00
43cef09f89 Created deployment 2026-02-11 17:56:40 +01:00
0a9470b1d8 Created deployment 2026-02-11 17:52:22 +01:00
6c6916c4cb Created deployment 2026-02-11 17:48:29 +01:00
39f8708fca Created deployment 2026-02-11 17:46:01 +01:00
4df80f6171 Created deployment 2026-02-11 17:45:36 +01:00
5296f58cef Created deployment 2026-02-11 17:44:44 +01:00
ecd5190713 Merge branch 'main' of git.savefamilygps.net:SaveFamily/sf-sim 2026-02-11 17:25:18 +01:00
71e0ec959c Vuelta atras del entrypoint 2026-02-11 17:24:28 +01:00
c40876af77 Merge branch 'main' of git.savefamilygps.net:SaveFamily/sf-sim 2026-02-11 17:12:51 +01:00
1888b82425 Created deployment 2026-02-11 17:11:23 +01:00
54e670cb87 Errata 2026-02-11 17:09:23 +01:00
166abf8b16 Error de tipado 2026-02-11 15:41:03 +01:00
4a58595870 ñadir numero en la respuesta de las activaciones 2026-02-11 15:14:15 +01:00
261342a629 Prueba despliegue 2026-02-11 13:41:45 +01:00
4d778e96ea Problema de despliegue 2026-02-11 13:29:46 +01:00
05ea4e76cc Notificacion cuando finalice una activacion 2026-02-11 13:20:04 +01:00
46ac54f7ab Seguimiento de ordenes desde la ingesta 2026-02-11 12:19:16 +01:00
61c91b6548 Created deployment 2026-02-11 11:33:12 +01:00
2d926d2583 Created deployment 2026-02-10 17:51:44 +01:00
25bc40840e Created deployment 2026-02-10 17:47:15 +01:00
aed7624e1e Created deployment 2026-02-10 17:43:31 +01:00
3ce820ef64 Created deployment 2026-02-10 17:39:45 +01:00
2c9bf9dd93 Mejora del commit anterior 2026-02-10 17:28:32 +01:00
19b2958a9c Intento de mejorar el proceso de validacion de los controladores 2026-02-10 17:26:04 +01:00
a39b84e107 Validaciones para los endpints 2026-02-10 15:57:03 +01:00
98 changed files with 3556 additions and 2706 deletions

14
.env
View File

@@ -1,4 +1,5 @@
PORT=3000
API_HOSTNAME=0.0.0.0
RABBITMQ_USER=guest
RABBITMQ_PASSWORD=guest
@@ -13,11 +14,16 @@ RABBITMQ_SECURE=false
RABBITMQ_VHOST=sim-vhost
# Hay cosas que unificar de varios servicios
POSTGRES_DB=postgres
POSTGRES_DATABASE=postgres
#POSTGRES_HOST=postgresql-sim
POSTGRES_HOST=localhost
POSTGRES_PORT=5432
DEV_POSTGRES_PORT=5432
POSTGRES_DB=postgres
POSTGRES_DATABASE=postgres
POSTGRES_PORT=5433
POSTGRES_USER=postgres
POSTGRES_PASSWORD=1234
# Para el postgres local para generar el script de resultado de migraciones
PGHOST=localhost
PGUSER=alvar
PGPASSWORD=alvar
PGPORT=5433

View File

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

View File

@@ -1,4 +1,3 @@
#/bin/bash
rm deployment/database/init.sql
cat deployment/database/*.sql >deployment/database/init.sql
docker compose -f deployment/local/docker/docker-compose.yaml --project-directory ./ build

View File

@@ -1,7 +1,8 @@
# stage base para coordinar las fases de build y ejecucion
FROM node:22-alpine AS base
WORKDIR /usr/local/app
COPY ./package.json ./yarn.lock ./
COPY ./package.json ./
#COPY ./package.json ./yarn.lock ./
RUN corepack enable && \
corepack prepare yarn@4.12.0 --activate
# copia el codigo en general

View File

@@ -4,16 +4,18 @@ CREATE TYPE status_enum AS ENUM ('noRequestID','noMassID','running','finished','
-- Tabla para gestionar las peticiones de cambio de objenious.
-- Para una o mas lineas se pueden lanzar operacione que no sabemos
-- con certeza cuando van a terminar.
-- Estas tablas está fuertemente ligadas al sistema que usa la plataforma
-- de objenioius y no debe unsarse para otra compañia.
CREATE TABLE if not exists objenious_operation (
id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
retry_count INT DEFAULT 0,
max_retry INT DEFAULT 5,
max_date_retry TIMESTAMP DEFAULT NULL,
retry_count INT DEFAULT 0, -- No implementado en codigo
max_retry INT DEFAULT 5, -- No implementado en codigo
max_date_retry TIMESTAMP DEFAULT NULL, -- No implementado en codigo
iccids TEXT,
request_id TEXT,
mass_action_id TEXT,
operation TEXT NOT NULL,
start_date TIMESTAMP NOT NULL DEFAULT now(),
start_date TIMESTAMP NOT NULL DEFAULT now(),
last_change_date TIMESTAMP NOT NULL DEFAULT now(),
end_date TIMESTAMP,
error TEXT,
@@ -24,7 +26,7 @@ CREATE TABLE if not exists objenious_operation (
-- operaciones pendientes para revisar
CREATE INDEX IF NOT EXISTS pending_operations
ON objenious_operation(start_date)
WHERE end_date IS NULL;
WHERE end_date IS NULL;
CREATE TABLE if not exists objenious_operation_change (
id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY,

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,106 @@
#!/bin/bash
# --- Para que siempre se ejecute en el mismo path
cd "$(dirname "$0")"
# --- Configuración por defecto ---
MIGRATIONS_DIR="./migrations"
OUTPUT_FILE_PREFIX="esquema_final"
DB_NAME="temp_schema_build_$(date +%s)"
# --- Función de Ayuda ---
usage() {
echo "Uso: $0 -v <version> [-e <ruta_env>]"
echo " -v Versión semántica objetivo (ej: 1.2.0)"
echo " -e (Opcional) Ruta al archivo .env para cargar variables"
echo " Los archivos de verions tienen que tener el formato x.x.x_descripcion.sql (Es importante la _ para serpar las partes) "
exit 1
}
# --- Procesar Argumentos (Flags) ---
# v: obligatorio
# e: opcionar
while getopts "v:e:" opt; do
case $opt in
v) TARGET_VERSION="$OPTARG" ;;
e) ENV_PATH="$OPTARG" ;;
*) usage ;;
esac
done
# Validar que la versión esté presente
if [ -z "$TARGET_VERSION" ]; then
echo "Error: La versión es obligatoria."
usage
fi
# --- Cargar variables de entorno ---
if [ ! -z "$ENV_PATH" ]; then
if [ -f "$ENV_PATH" ]; then
echo "~> Cargando configuración desde: $ENV_PATH"
# Exporta automáticamente las variables definidas en el archivo
set -o allexport
source "$ENV_PATH"
set +o allexport
else
echo "Error: No se encontró el archivo .env en: $ENV_PATH"
exit 1
fi
else
echo "!> No se especificó archivo .env, usando variables del sistema actual"
fi
# echo "Debug: Usuario es '$PGUSER'"
# echo "Debug: Host es '$PGHOST'"
# echo "Debug: Password es '$PGPASSWORD'" # Cuidado con mostrar esto
# --- Función de limpieza (Safety Net) ---
cleanup() {
echo "~> Limpiando: Eliminando base de datos temporal '$DB_NAME'"
# Usamos las variables de conexión cargadas (si las hay)
dropdb $DB_NAME --if-exists 2>/dev/null
}
trap cleanup EXIT
# --- Inicio del Proceso ---
echo "~> Iniciando build para versión: $TARGET_VERSION"
# 1. Crear BD temporal
# Nota: Si tu .env tiene PGHOST, la BD se creará allí. Si no, en localhost.
createdb $DB_NAME
# 2. Ejecutar script base (si existe)
rm -rf init.sql
cat base/*.sql >init.sql
if [ -f "init.sql" ]; then
echo "~> Ejecutando init.sql..."
psql -d $DB_NAME -f init.sql >/dev/null
fi
# 3. Iterar y filtrar migraciones
echo "~> Aplicando migraciones hasta la versión $TARGET_VERSION..."
for f in $(ls $MIGRATIONS_DIR/*.sql | sort -V); do
FILENAME=$(basename "$f")
# Extraer versión (Asume formato V1.0.0_desc.sql o 1.0.0_desc.sql)
FILE_VER=$(echo "$FILENAME" | sed -E 's/^V//' | awk -F_ '{print $1}')
# Comparación semántica
echo "comparando $TARGET_VERSION con $FILE_VER"
LOWEST=$(echo -e "$TARGET_VERSION\n$FILE_VER" | sort -V | head -n1)
if [ "$LOWEST" == "$FILE_VER" ] || [ "$FILE_VER" == "$TARGET_VERSION" ]; then
echo "~> Aplicando: $FILENAME ($FILE_VER)"
psql -d $DB_NAME -f "$f" >/dev/null
else
echo "~> Saltando: $FILENAME ($FILE_VER) - Mayor que objetivo"
fi
done
# 4. Generar nombre de archivo de salida
OUTPUT_FILE="${OUTPUT_FILE_PREFIX}_v${TARGET_VERSION}.sql"
# 5. Extraer el esquema FINAL
echo "~> Generando $OUTPUT_FILE ---"
pg_dump -d $DB_NAME -s --no-owner --no-privileges >$OUTPUT_FILE
echo "o> Esquema guardado en $OUTPUT_FILE"

View File

@@ -1,150 +0,0 @@
-- eliminar los drop para prod
drop domain if exists imei_type cascade;
CREATE DOMAIN imei_type as varchar(15);
drop domain if exists iccid_type cascade;
CREATE DOMAIN iccid_type as varchar(22);
drop domain if exists imsi_type cascade;
CREATE DOMAIN imsi_type as varchar(15);
CREATE table if not exists sim_cards (
id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
imei imei_type,
iccid iccid_type,
imsi imsi_type,
user_id BIGINT,
subscription_id BIGINT,
created_at TIMESTAMP,
last_update TIMESTAMP,
deleted_at TIMESTAMP
);
CREATE TABLE if not exists sim_envio (
id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
codigo_origen TEXT,
codigo_distrito TEXT,
pedido_id BIGINT,
sim_id BIGINT,
fecha_envio TIMESTAMP,
fecha_email TIMESTAMP,
is_preactivado BOOLEAN,
fecha_devolucion TIMESTAMP,
created_at TIMESTAMP,
CONSTRAINT fk_sim_id
FOREIGN KEY(sim_id) REFERENCES sim_cards(id)
);
-- Mock, No es parte de SIMs
CREATE TABLE if not exists sf_subscription (
id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY
);
-- No habria que meterle las propiedades del tipo de subscripcion
CREATE TABLE if not exists sim_subscription_types (
id INT PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
subscription TEXT NOT NULL,
created_at TIMESTAMP,
updated_at TIMESTAMP,
deleted_at TIMESTAMP
);
CREATE TABLE if not exists sim_company (
id INT PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
name TEXT,
created_at TIMESTAMP,
updated_at TIMESTAMP,
deleted_at TIMESTAMP
);
CREATE TABLE sim_subscription (
id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
company_id INT,
subscription_type_id INT,
sim_id BIGINT,
order_id BIGINT,
created_at TIMESTAMP,
updated_at TIMESTAMP,
deleted_at TIMESTAMP,
CONSTRAINT fk_sim_id
FOREIGN KEY(sim_id) REFERENCES sim_cards(id),
CONSTRAINT fk_company_id
FOREIGN KEY(company_id) REFERENCES sim_company(id),
CONSTRAINT fk_subscription_type_id
FOREIGN KEY(subscription_type_id) REFERENCES sim_subscription_types(id)
);
CREATE TABLE if not exists sim_subscription_operations (
id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
sim_id BIGINT,
operation_type TEXT NOT NULL,
happened_at TIMESTAMP,
CONSTRAINT valid_operations CHECK (
operation_type in ('free','preactivate','activate','pause','cancel')
),
CONSTRAINT fk_subscription_id
FOREIGN KEY(sim_id)
REFERENCES sim_subscription(id)
);
-- Se supone que indica un cambio
CREATE TABLE sim_subscription_historic (
id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
subscription_id BIGINT,
iccid iccid_type,
company_id INT
);
CREATE TYPE status_enum AS ENUM ('noRequestID','noMassID','running','finished','error','other');
-- Tabla para gestionar las peticiones de cambio de objenious.
-- Para una o mas lineas se pueden lanzar operacione que no sabemos
-- con certeza cuando van a terminar.
CREATE TABLE if not exists objenious_operation (
id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
retry_count INT DEFAULT 0,
max_retry INT DEFAULT 5,
max_date_retry TIMESTAMP DEFAULT NULL,
iccids TEXT,
request_id TEXT,
mass_action_id TEXT,
operation TEXT NOT NULL,
start_date TIMESTAMP NOT NULL DEFAULT now(),
last_change_date TIMESTAMP NOT NULL DEFAULT now(),
end_date TIMESTAMP,
error TEXT,
status status_enum,
objenious_status TEXT
);
-- operaciones pendientes para revisar
CREATE INDEX IF NOT EXISTS pending_operations
ON objenious_operation(start_date)
WHERE end_date IS NULL;
CREATE TABLE if not exists objenious_operation_change (
id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
operation_id BIGINT,
creation_date TIMESTAMP NOT NULL DEFAULT now(),
error TEXT,
new_status status_enum,
previous_status status_enum,
new_objenious_status TEXT,
previous_objenious_status TEXT,
new_request_id TEXT,
new_mass_action_id TEXT,
CONSTRAINT fk_operation_id
FOREIGN KEY(operation_id) REFERENCES objenious_operation(id)
);
CREATE INDEX operation_change
ON objenious_operation_change(operation_id);

View File

@@ -0,0 +1,48 @@
CREATE TYPE status_enum AS ENUM ('noRequestID','noMassID','running','finished','error','other');
-- Tabla para gestionar las peticiones de cambio de objenious.
-- Para una o mas lineas se pueden lanzar operacione que no sabemos
-- con certeza cuando van a terminar.
-- Estas tablas está fuertemente ligadas al sistema que usa la plataforma
-- de objenioius y no debe unsarse para otra compañia.
CREATE TABLE if not exists objenious_operation (
id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
retry_count INT DEFAULT 0, -- No implementado en codigo
max_retry INT DEFAULT 5, -- No implementado en codigo
max_date_retry TIMESTAMP DEFAULT NULL, -- No implementado en codigo
iccids TEXT,
request_id TEXT,
mass_action_id TEXT,
operation TEXT NOT NULL,
start_date TIMESTAMP NOT NULL DEFAULT now(),
last_change_date TIMESTAMP NOT NULL DEFAULT now(),
end_date TIMESTAMP,
error TEXT,
status status_enum,
objenious_status TEXT
);
-- operaciones pendientes para revisar
CREATE INDEX IF NOT EXISTS pending_operations
ON objenious_operation(start_date)
WHERE end_date IS NULL;
CREATE TABLE if not exists objenious_operation_change (
id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
operation_id BIGINT,
creation_date TIMESTAMP NOT NULL DEFAULT now(),
error TEXT,
new_status status_enum,
previous_status status_enum,
new_objenious_status TEXT,
previous_objenious_status TEXT,
new_request_id TEXT,
new_mass_action_id TEXT,
CONSTRAINT fk_operation_id
FOREIGN KEY(operation_id) REFERENCES objenious_operation(id)
);
CREATE INDEX operation_change
ON objenious_operation_change(operation_id);

View File

@@ -0,0 +1,67 @@
-- Tablas para el seguimiento de las operaciones de SIM sin importar
-- la cmpañia.
DO $$ BEGIN
CREATE TYPE order_types AS ENUM ('activate','preactivate','cancel','pause','reactivate','unknown');
CREATE TYPE order_status AS ENUM (
'pending', -- Mensaje creado/enviado a RabbitMQ
'running', -- Consumidor ha cogido el mensaje (opcional)
'finished', -- Procesado correctamente
'failed', -- Falló, pero podría reintentarse (Pasar a delay?)
'dlx' -- Falló definitivamente y está en Dead Letter Exchange
);
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
CREATE TABLE IF NOT EXISTS order_tracking (
id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
correlation_id VARCHAR(255) NOT NULL, -- ID compartido con RabbitMQ (message_id)
exchange VARCHAR(100), -- Exchange al que se envia (de momento solo hay 1 principal sin contar delay y dlx)
routing_key VARCHAR(100), -- Routing key del mensaje
order_type order_types NOT NULL DEFAULT 'unknown',
payload JSONB, -- Duda si es optimo guardar la copia, es útil en caso de fallo
-- Campos de reintentos?
status order_status NOT NULL DEFAULT 'pending',
retry_count INT DEFAULT 0,
error_message TEXT, -- Razón del fallo
error_stacktrace TEXT,
start_date TIMESTAMP NOT NULL DEFAULT (now() at time zone 'utc'),
update_date TIMESTAMP NOT NULL DEFAULT (now() at time zone 'utc'),
finish_date TIMESTAMP
);
-- Busqueda según id de rabbit
CREATE INDEX IF NOT EXISTS idx_order_correlation
ON order_tracking(correlation_id);
-- Ordenenes que todavia no han finalizado
CREATE INDEX IF NOT EXISTS pending_orders
ON order_tracking(start_date)
WHERE order_tracking.finish_date IS NULL;
CREATE TABLE IF NOT EXISTS order_history(
id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
order_id BIGINT NOT NULL,
previous_status order_status NOT NULL, -- Siempre hay un estado anterior, para casos excepcioneale "unknown"
new_status order_status NOT NULL,
change_reason TEXT,
change_date TIMESTAMP NOT NULL DEFAULT (now() at time zone 'utc'),
CONSTRAINT fk_order_id
FOREIGN KEY(order_id)
REFERENCES order_tracking(id)
ON DELETE CASCADE
);
-- fk de order
CREATE INDEX IF NOT EXISTS idx_order_id
ON order_history(order_id);
-- busquedas por fecha
CREATE INDEX IF NOT EXISTS idx_order_change_date
ON order_history(change_date);

View File

@@ -0,0 +1,12 @@
/*
* Fechas modificadas para que todas sean en base a 'UTC'
* */
ALTER TABLE objenious_operation
ALTER COLUMN start_date SET DEFAULT (now() at time zone 'utc'),
ALTER COLUMN last_change_date SET DEFAULT (now() at time zone 'utc');
ALTER TABLE objenious_operation_change
ALTER COLUMN creation_date SET DEFAULT (now() at time zone 'utc');

View File

@@ -0,0 +1,30 @@
/*
* Fechas modificadas para que se puedan hacer query en base a la zona horaria objetivo
* SELECT col_date at time zone 'cet' -- devuleve la fecha en esa zona
* SELECT col_date -- devuleve la fecha en UTC con el offset de la zona horaria
*
* */
ALTER TABLE objenious_operation
ALTER COLUMN start_date SET DATA TYPE TIMESTAMP WITH TIME ZONE,
ALTER COLUMN start_date SET DEFAULT now(),
ALTER COLUMN last_change_date SET DATA TYPE TIMESTAMP WITH TIME ZONE,
ALTER COLUMN last_change_date SET DEFAULT now(),
ALTER COLUMN end_date SET DATA TYPE TIMESTAMP WITH TIME ZONE;
ALTER TABLE objenious_operation_change
ALTER COLUMN creation_date SET DATA TYPE TIMESTAMP WITH TIME ZONE,
ALTER COLUMN creation_date SET DEFAULT now();
ALTER TABLE order_tracking
ALTER COLUMN start_date SET DATA TYPE TIMESTAMP WITH TIME ZONE,
ALTER COLUMN start_date SET DEFAULT now(),
ALTER COLUMN update_date SET DATA TYPE TIMESTAMP WITH TIME ZONE,
ALTER COLUMN update_date SET DEFAULT now(),
ALTER COLUMN finish_date SET DATA TYPE TIMESTAMP WITH TIME ZONE;
ALTER TABLE order_history
ALTER COLUMN change_date SET DATA TYPE TIMESTAMP WITH TIME ZONE,
ALTER COLUMN change_date SET DEFAULT now();

View File

@@ -0,0 +1,10 @@
/**
* A que endpoint actualizar el estado de los order, si se especificase.
* Se asume que siempre se usa POST.
* Se separa host de enpoint para dejar host como default el origen de la
* peticion anterior y poder hacer filtrados
*/
ALTER TABLE order_tracking
ADD COLUMN webhook_host TEXT,
ADD COLUMN webhook_endpoint TEXT;

View File

@@ -0,0 +1,7 @@
/**
* En la tabla de orders de objenious no hay forma de saber a a que mensaje está Solicitando
* cada operación.
*/
ALTER TABLE objenious_operation
ADD COLUMN correlation_id TEXT;

View File

@@ -1,23 +1,23 @@
# --- Release image ---
FROM node:22-alpine AS release
RUN apk --no-cache add git
WORKDIR /home/node/app
# Copiar paquetes compilados del monorepo
RUN corepack enable
COPY ./dist/packages ./packages
COPY ./.yarnrc.yml ./
COPY ./docs ./docs
# Instalar dependencias de produccion
COPY ./package.json ./
COPY ./package-lock.json ./
RUN npm ci --omit=dev
# Symlinks para modulos compartidos del workspace
RUN mkdir -p packages/node_modules && \
ln -sf ../sim-shared packages/node_modules/sim-shared && \
ln -sf ../sim-consumidor-objenious packages/node_modules/sim-consumidor-objenious
RUN yarn install
RUN mkdir -p dist && ln -sf ../packages dist/packages
# Script de inicio para los 3 servicios
COPY ./entrypoint.sh ./
RUN chmod +x entrypoint.sh
EXPOSE ${PORT:-3000}
ENTRYPOINT ["./entrypoint.sh"]

View File

@@ -23,16 +23,32 @@ services:
timeout: 5s
retries: 5
environment:
RABBITMQ_DEFAULT_USER: ${RABBITMQ_USER}
RABBITMQ_DEFAULT_PASS: ${RABBITMQ_PASSWORD}
RABBITMQ_USER: ${RABBITMQ_USER}
RABBITMQ_PASSWORD: ${RABBITMQ_PASSWORD}
entrypoint: ["bash", "/usr/local/bin/docker-entrypoint-wrapper.sh"]
command: ["rabbitmq-server"]
volumes:
- ./rabbit/docker-entrypoint-wrapper.sh:/usr/local/bin/docker-entrypoint-wrapper.sh:ro
- ./rabbitmq_plugins/enabled_plugins:/etc/rabbitmq/enabled_plugins:ro
- ./rabbit/rabbitmq.conf:/etc/rabbitmq/rabbitmq.conf:ro
- ./rabbit/definitions.json:/etc/rabbitmq/definitions.json:ro
- ./rabbit/definitions.json:/etc/rabbitmq/definitions.template.json:ro
networks:
- internal
- proxy
labels:
- "io.portainer.accesscontrol.teams=develop"
- "traefik.enable=true"
- "traefik.http.routers.sf-sims-rabbitmq.entrypoints=web"
- "traefik.http.routers.sf-sims-rabbitmq.rule=Host(`sf-sims-rabbitmq.savefamilygps.net`)"
- "traefik.http.middlewares.sf-sims-rabbitmq-https-redirect.redirectscheme.scheme=https"
- "traefik.http.routers.sf-sims-rabbitmq.middlewares=sf-sims-rabbitmq-https-redirect"
- "traefik.http.routers.sf-sims-rabbitmq-secure.entrypoints=websecure"
- "traefik.http.routers.sf-sims-rabbitmq-secure.rule=Host(`sf-sims-rabbitmq.savefamilygps.net`)"
- "traefik.http.routers.sf-sims-rabbitmq-secure.tls=true"
- "traefik.http.routers.sf-sims-rabbitmq-secure.service=sf-sims-rabbitmq"
- "traefik.http.routers.sf-sims-rabbitmq-secure.tls.certresolver=myresolver"
- "traefik.http.services.sf-sims-rabbitmq.loadbalancer.server.port=15672"
- "traefik.docker.network=proxy"
sf-sims-api:
container_name: sf-sims-api
@@ -48,8 +64,6 @@ services:
depends_on:
rabbitmq-sim-broker:
condition: service_healthy
postgresql-sim:
condition: service_healthy
networks:
- savefamily
- proxy
@@ -60,8 +74,14 @@ services:
- ./.env:/home/node/app/.env:ro
- ./sim-consumidor-objenious.env:/home/node/app/packages/sim-consumidor-objenious/.env:ro
- ./sim-objenious-cron.env:/home/node/app/packages/sim-objenious-cron/.env:ro
- ./obj.pem:/home/node/app/packages/sim-consumidor-objenious/obj.pem:ro
- ./obj.pem:/home/node/app/packages/sim-objenious-cron/obj.pem:ro
healthcheck:
test: ["CMD-SHELL", "wget -qO- http://localhost:${PORT:-3000}/health || exit 1"]
test:
[
"CMD-SHELL",
'node -e "fetch(''http://localhost:'' + (process.env.PORT || 3000) + ''/health'').then(r => { if (!r.ok) process.exit(1) }).catch(() => process.exit(1))"',
]
interval: 10s
timeout: 5s
retries: 5
@@ -78,26 +98,5 @@ services:
- "traefik.http.routers.sf-sims-secure.tls=true"
- "traefik.http.routers.sf-sims-secure.service=sf-sims"
- "traefik.http.routers.sf-sims-secure.tls.certresolver=myresolver"
- "traefik.http.routers.sf-sims-secure.middlewares=tailscale-ipwhitelist@file"
- "traefik.http.services.sf-sims.loadbalancer.server.port=${PORT}"
- "traefik.docker.network=proxy"
postgresql-sim:
image: postgres:16.1
env_file:
- ./.env
ports:
- 5432
volumes:
- ./postgres:/var/lib/postgres/data
- ./database/init.sql:/docker-entrypoint-initdb.d/init.sql
healthcheck:
test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}"]
interval: 5s
retries: 5
start_period: 5s
timeout: 5s
networks:
- internal
labels:
- "io.portainer.accesscontrol.teams=develop"

View File

@@ -1,9 +1,4 @@
#!/bin/sh
cd /home
# Cada servicio necesita CWD en su directorio de paquete
# para que loadEnvFile("../../.env") y loadEnvFile("./.env") resuelvan correctamente
(cd /home/node/app/packages/sim-objenious-cron && node index.js) &
(cd /home/node/app/packages/sim-consumidor-objenious && node index.js) &
(cd /home/node/app/packages/sim-consumidor-nos && node index.js) &
cd /home/node/app/packages/sim-entrada-eventos && exec node index.js
cd /home/node/app && yarn start

View File

@@ -22,6 +22,7 @@ pipeline {
}
stage("🧱 Building") {
steps {
sh 'rm -rf dist/'
sh 'yarn run build'
}
}
@@ -49,12 +50,21 @@ pipeline {
cleanRemote: false,
execCommand: "ln -sf $BASE_REMOTE_PATH/vault/savefamily/sf-sims/sim-objenious-cron.env $APP_REMOTE_PATH/sim-objenious-cron.env"
),
sshTransfer(
cleanRemote: false,
execCommand: "ln -sf $BASE_REMOTE_PATH/vault/savefamily/sf-sims/obj.pem $APP_REMOTE_PATH/obj.pem"
),
sshTransfer(
cleanRemote: false,
remoteDirectory: "$APP_REMOTE_PATH",
sourceFiles: "dist/**/*",
excludes: "dist/**/node_modules/**"
),
sshTransfer(
cleanRemote: false,
remoteDirectory: "$APP_REMOTE_PATH",
sourceFiles: "docs/**/*",
),
sshTransfer(
cleanRemote: false,
remoteDirectory: "$APP_REMOTE_PATH",
@@ -64,8 +74,8 @@ pipeline {
sshTransfer(
cleanRemote: false,
remoteDirectory: "$APP_REMOTE_PATH",
sourceFiles: "deployment/rabbit/**/*",
removePrefix: "deployment",
sourceFiles: "deployment/develop/rabbit/**/*",
removePrefix: "deployment/develop",
),
sshTransfer(
cleanRemote: false,
@@ -86,7 +96,7 @@ pipeline {
sshTransfer(
cleanRemote: false,
remoteDirectory: "$APP_REMOTE_PATH",
sourceFiles: "package-lock.json",
sourceFiles: ".yarnrc.yml",
),
sshTransfer(
cleanRemote: false,

View File

@@ -0,0 +1,90 @@
{
"rabbit_version": "4.2.2",
"rabbitmq_version": "4.2.2",
"product_name": "RabbitMQ",
"product_version": "4.2.2",
"users": [
{
"name": "RABBITMQ_USER_PLACEHOLDER",
"password": "RABBITMQ_PASSWORD_PLACEHOLDER",
"tags": ["administrator"]
}
],
"vhosts": [
{
"name": "sim-vhost"
}
],
"permissions": [
{
"user": "RABBITMQ_USER_PLACEHOLDER",
"vhost": "sim-vhost",
"configure": ".*",
"write": ".*",
"read": ".*"
}
],
"topic_permissions": [],
"parameters": [],
"global_parameters": [
{
"name": "cluster_name",
"value": "rabbit@a8d5c6e08439"
},
{
"name": "internal_cluster_id",
"value": "rabbitmq-cluster-id-gXeBLbsUC2W2tU0Bx_QY_w"
}
],
"policies": [
{
"vhost": "sim-vhost",
"name": "pol.sim.dlx",
"pattern": "sim.*",
"apply-to": "queues",
"definition": {
"dead-letter-exchange": "sim.dlx"
},
"priority": 7
}
],
"exchanges": [
{
"name": "sim.exchange",
"vhost": "sim-vhost",
"type": "topic",
"durable": true,
"auto_delete": false,
"internal": false,
"argurments": {}
},
{
"name": "sim.dlx",
"vhost": "sim-vhost",
"type": "topic",
"durable": true,
"auto_delete": false,
"internal": false,
"argurments": {}
}
],
"queues": [
{
"name": "sim.logs",
"vhost": "sim-vhost",
"durable": true,
"auto_delete": false,
"arguments": {}
}
],
"bindings": [
{
"source": "sim.exchange",
"vhost": "sim-vhost",
"destination": "sim.logs",
"destination_type": "queue",
"routing_key": "sim.#",
"arguments": {}
}
]
}

View File

@@ -0,0 +1,12 @@
#!/bin/bash
set -eu
# Substitute env vars into definitions template before RabbitMQ starts.
# RabbitMQ 4.x skips default user creation when definitions.json is loaded,
# so the user must be defined in the JSON itself.
sed \
-e "s|RABBITMQ_USER_PLACEHOLDER|${RABBITMQ_USER}|g" \
-e "s|RABBITMQ_PASSWORD_PLACEHOLDER|${RABBITMQ_PASSWORD}|g" \
/etc/rabbitmq/definitions.template.json > /etc/rabbitmq/definitions.json
exec docker-entrypoint.sh "$@"

View File

@@ -0,0 +1,4 @@
management.load_definitions = /etc/rabbitmq/definitions.json
default_vhost = sim-vhost
default_queue_type = quorum

View File

@@ -0,0 +1,28 @@
# Stage base para coordinar las fases de build y ejecucion
FROM node:22-alpine AS base
# Hace falta para la herramienta de migraciones, cuando se publique se
# sustituira por el paquete de npm
RUN apk --no-cache add git
WORKDIR /usr/local/app
RUN corepack enable && \
corepack prepare yarn@4.12.0 --activate
COPY ./package.json ./yarn.lock ./
COPY ./packages ./packages
# copia el codigo en general
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
RUN yarn install && \
yarn cache clean && \
yarn build && \
chmod +x start.sh
EXPOSE ${PORT}
ENTRYPOINT [ "./start.sh" ]

View File

@@ -24,14 +24,15 @@ services:
RABBITMQ_DEFAULT_PASS: ${RABBITMQ_PASSWORD}
volumes:
- ./rabbitmq_plugins/enabled_plugins:/etc/rabbitmq/enabled_plugins:ro
- ./deployment/rabbit/rabbitmq.conf:/etc/rabbitmq/rabbitmq.conf:ro
- ./deployment/rabbit/definitions.json:/etc/rabbitmq/definitions.json:ro
- ./deployment/local/rabbit/rabbitmq.conf:/etc/rabbitmq/rabbitmq.conf:ro
- ./deployment/local/rabbit/definitions.json:/etc/rabbitmq/definitions.json:ro
sim-gateway:
container_name: sim-gateway
sf-sims-api:
container_name: sf-sims-api
image: sf-sims-api
build:
context: ./
dockerfile: deployment/Dockerfile.dev
dockerfile: deployment/local/docker/Dockerfile.dev
args:
PORT: "${PORT:-3000}"
develop:
@@ -39,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:
@@ -46,19 +50,31 @@ services:
env_file:
- .env
restart: unless-stopped
healthcheck:
test:
[
"CMD-SHELL",
'node -e "fetch(''http://localhost:'' + (process.env.PORT || 3000) + ''/health'').then(r => { if (!r.ok) process.exit(1) }).catch(() => process.exit(1))"',
]
interval: 10s
timeout: 5s
retries: 5
start_period: 15s
depends_on:
rabbitmq-sim-broker:
condition: service_healthy
postgresql-sim:
condition: service_healthy
postgresql-sim:
container_name: postgresql-sim
image: postgres:16.1
env_file:
- .env
ports:
- "5432:${DEV_POSTGRES_PORT}"
- "${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

View File

@@ -1,9 +1,10 @@
#!/bin/bash
cd /mnt/docker-storage/containers/savefamily/sf-shopify-orders
# cd /mnt/docker-storage/containers/savefamily/sf-shopify-orders
cd /mnt/docker-storage/containers/savefamily/sf-sims-api
docker stop sf-shopify-orders-api || true
docker rm sf-shopify-orders-api || true
docker rmi sf-shopify-orders-api || true
docker stop sf-sims-api || true
docker rm sf-sims-api || true
docker rmi sf-sims-api || true
docker compose -f docker-compose.yaml up --build -d

View File

@@ -0,0 +1,3 @@
#!/bin/sh
echo "Lanzando migraciones e iniciando servidor"
yarn migrate && yarn start

View File

@@ -1,6 +1,3 @@
default_user = guest
default_pass = guest
listeners.tcp.default = 5672
management.tcp.port = 15672

File diff suppressed because one or more lines are too long

View File

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

View File

@@ -0,0 +1,16 @@
meta {
name: Activation Email Health
type: http
seq: 8
}
post {
url: https://sf-sim-activation.savefamily.net/health
body: none
auth: inherit
}
settings {
encodeUrl: true
timeout: 0
}

View File

@@ -0,0 +1,42 @@
meta {
name: Activation Email
type: http
seq: 6
}
post {
url: https://sf-sim-activation.savefamily.net/send-activation-mail
body: json
auth: inherit
}
headers {
x-apikey-sim-activation: 9e48c4ac-1ab0-4397-b3f3-6c239200dfe6
}
body:json {
{
"id": "11",
"retry_count": 0,
"max_retry": null,
"max_date_retry": null,
"iccids": [
"8933201125068886080"
],
"request_id": "14362",
"mass_action_id": "5208468",
"operation": "activate",
"start_date": "2026-02-13T11:08:42.499Z",
"last_change_date": "2026-02-16T09:24:36.073Z",
"end_date": "2026-02-16T09:24:36.073Z",
"error": null,
"status": "finished",
"objenious_status": "Terminé",
"msisdn": "33764399870"
}
}
settings {
encodeUrl: true
timeout: 0
}

View File

@@ -11,10 +11,45 @@ post {
}
body:form-urlencoded {
iccid: 8933201124059176320
iccid: 8933201125068886692
}
settings {
encodeUrl: true
timeout: 0
}
docs {
El endpoint recibe como body
```
{
iccid: string,
update_webhook?: string
}
```
`update_webhook` está en desarrollo, pero será donde se mande la actualizacion de la cancelación cuando haya una respuesta de la API externa.
Si la llamada tiene exito devuelve:
``` json
{
data: {
iccid: string,
message_id: string,
operation: "cancelation"
}
}
```
message_id se usará para la llamada /orders/message_id/}{message_id}
Si la llamada falla devolvera:
```json
{
errors: {
msg: string
... (campos extra de gestion del error)
}
}
```
}

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

@@ -0,0 +1,16 @@
meta {
name: Get pending orders
type: http
seq: 11
}
get {
url: {{baseurl}}/orders/pending
body: none
auth: inherit
}
settings {
encodeUrl: true
timeout: 0
}

View File

@@ -0,0 +1,16 @@
meta {
name: Order by id
type: http
seq: 9
}
get {
url: {{baseurl}}/orders/
body: none
auth: inherit
}
settings {
encodeUrl: true
timeout: 0
}

View File

@@ -0,0 +1,20 @@
meta {
name: Orders by message_id
type: http
seq: 12
}
get {
url: {{baseurl}}/orders/message_id/019c93d3-014a-711d-b958-03dd629be78d
body: none
auth: inherit
}
params:query {
~message_id: 019c93d3-014a-711d-b958-03dd629be78d
}
settings {
encodeUrl: true
timeout: 0
}

View File

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

View File

@@ -0,0 +1,34 @@
docs {
Los endpoint tienen unos campos comunes de entrada:
```ts
{
iccid: string,
update_webhook?: string
}
```
`update_webhook` está en desarrollo, pero será donde se mande la actualizacion de la cancelación cuando haya una respuesta de la API externa.
Si la llamada tiene exito devuelve:
```ts
{
data: {
iccid: string,
message_id: string,
operation: string,
}
}
```
message_id se usará para la llamada /orders/message_id/}{message_id}
Si la llamada falla devolvera:
```ts
{
errors: {
msg: string
... (campos extra de gestion del error)
}
}
```
}

View File

@@ -0,0 +1,3 @@
vars {
baseurl: https://sf-sims.savefamilygps.net
}

View File

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

View File

@@ -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: 14111
params.id: 15102
}
settings {

View File

@@ -0,0 +1,33 @@
meta {
name: Unit action by id copy
type: http
seq: 19
}
get {
url: https://api-getway.objenious.com/ws/actions/massActions/{{id}}
body: formUrlEncoded
auth: bearer
}
auth:bearer {
token: {{ws-access-token-partenaire}}
}
body:json {
{
"identifier": {
"identifiers": ["8933201124059175967"],
"identifierType": "ICCID"
}
}
}
vars:pre-request {
id: 5192767
}
settings {
encodeUrl: true
timeout: 0
}

1843
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,5 +1,6 @@
{
"name": "sim-eventos",
"version": "1.0.0",
"packageManager": "yarn@4.12.0",
"workspaces": [
"packages/*"
@@ -14,9 +15,11 @@
"lint": "eslint .",
"lint:fix": "eslint --fix .",
"format": "prettier --write .",
"format:check": "prettier --check ."
"format:check": "prettier --check .",
"migrate": "yarn db-migrate -e .env -m deployment/database/migrations -t 99.0.0"
},
"dependencies": {
"@sf-alvar/db-migrate": "1.0.3",
"@tsconfig/node22": "^22.0.5",
"amqp-connection-manager": "^5.0.0",
"amqplib": "^0.10.9",
@@ -26,6 +29,7 @@
"express": "^5.2.1",
"pg": "^8.18.0",
"typescript": "^5.9.3",
"uuidv7": "^1.1.0",
"vite": "^7.3.1",
"vite-tsconfig-paths": "^6.0.5"
},

View File

@@ -1,3 +1,3 @@
console.log("Template")
console.log(new Date().toISOString())
export default {}

View File

@@ -3,20 +3,3 @@ RABBITMQ_USER=guest
RABBITMQ_PASSWORD=guest
ENVIORMENT=development
RABBITMQ_HOST=rabbitmq-sim-broker
#RABBITMQ_HOST=localhost
RABBITMQ_PORT=5672
RABBITMQ_USER=guest
RABBITMQ_PASSWORD=guest
RABBITMQ_SECURE=false
RABBITMQ_VHOST=sim-vhost
# Hay cosas que unificar de varios servicios
POSTGRES_DB=postgres
POSTGRES_DATABASE=postres
POSTGRES_HOST=postgresql-sim-1
POSTGRES_PORT=5432
DEV_POSTGRES_PORT=5432
POSTGRES_USER=postgres
POSTGRES_PASSWORD=1234

View File

@@ -9,8 +9,8 @@ export const env = {
POSTGRES_HOST: process.env.POSTGRES_HOST,
POSTGRES_DATABASE: process.env.POSTGRES_DATABASE,
RABBITMQ_HOST: String(process.env.RABBITMQ_HOST ?? "localhost"),
RABBITMQ_USER: String(process.env.RABBITMQ_USER ?? "guest"),
RABBITMQ_PASSWORD: String(process.env.RABBITMQ_PASSWORD ?? "guest"),
RABBITMQ_USER: String(process.env.RABBITMQ_USER ?? "test"),
RABBITMQ_PASSWORD: String(process.env.RABBITMQ_PASSWORD ?? "test"),
RABBITMQ_EXCHANGE: String(process.env.RABBITMQ_EXCHANGE ?? "/"),
RABBITMQ_PORT: parseInt(process.env.RABBITMQ_PORT ?? "5672"),
RABBITMQ_MODULENAME: process.env.MODULENAME,

View File

@@ -5,4 +5,6 @@ OBJ_CLI_ASSERTION=XOc7FtwXD8hUX2SFVX94XSty8wkOmChkwDNF09O_aIxPubMDdFUdCDCB4zpzSI
OBJ_CLIENT_ID=savefamily_rest_ws
OBJ_KID=xNfbMiyL1ORXGP8lElhcv8nVaG3EJKye4Lc1YoN3I1E
OBJ_BASE_URL=https://api-getway.objenious.com/ws
OBJ_CUSTOMER_CODE=9.49411.10
//OBJ_BASE_URL=https://api-getway.objenious.com/ws/test

View File

@@ -3,6 +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";
/**
* La clase usa generadores de funciones para mantener el contexto
@@ -64,6 +65,8 @@ export class SimController {
}
public activate() {
const DUE_DATE_SECONDS = 2 * 60
return async (msg: ConsumeMessage) => {
let msgData;
try {
@@ -80,9 +83,10 @@ export class SimController {
throw new Error("Error activando la sim, no se ha especificado la oferta")
}
this.tryUseCase(msg, this.useCases.activate({
dueDate: this.genDueDate(2 * 60).toISOString(),
customerAccountCode: "9.49411.10", // TODO: Al .env
const resp = await this.tryUseCase(msg, this.useCases.activate({
correlation_id: msgData.headers?.message_id,
dueDate: this.genDueDate(DUE_DATE_SECONDS).toISOString(),
customerAccountCode: "9.49411.10",
identifier: {
identifierType: "ICCID",
identifiers: [iccid]
@@ -92,6 +96,11 @@ export class SimController {
services: []
}
}))
// TODO:
// - Crear un registro de operación
// - Si ha salido bien id de operación -> webhook?
// - Si ha salido mal notificar solo cuando se manda a dlx ??
}
}
@@ -109,7 +118,8 @@ export class SimController {
}
const iccid = msgData.payload.iccid
this.tryUseCase(msg, this.useCases.preActivate({
const res = await this.tryUseCase(msg, this.useCases.preActivate({
correlation_id: msgData.headers?.message_id,
dueDate: this.genDueDate(2 * 60).toISOString(),
identifier: {
identifierType: "ICCID",
@@ -135,7 +145,8 @@ export class SimController {
}
const iccid = msgData.payload.iccid
this.tryUseCase(msg, this.useCases.suspend({
const res = await this.tryUseCase(msg, this.useCases.reActivate({
correlation_id: msgData.headers?.message_id,
dueDate: this.genDueDate(2 * 60).toISOString(),
identifier: {
identifierType: "ICCID",
@@ -160,7 +171,8 @@ export class SimController {
}
const iccid = msgData.payload.iccid
this.tryUseCase(msg, this.useCases.suspend({
const res = await this.tryUseCase(msg, this.useCases.suspend({
correlation_id: msgData.headers?.message_id,
dueDate: this.genDueDate(2 * 60).toISOString(),
identifier: {
identifierType: "ICCID",
@@ -184,8 +196,9 @@ export class SimController {
return Promise.reject("Mensaje invalido")
}
const iccid = msgData.payload.iccid
console.log("Mensaje procesado", String(msgData))
this.tryUseCase(msg, this.useCases.terminate({
console.log("Mensaje procesado", msgData)
const res = await this.tryUseCase(msg, this.useCases.terminate({
correlation_id: msgData.headers?.message_id,
dueDate: this.genDueDate(2 * 60).toISOString(),
identifier: {
identifierType: "ICCID",
@@ -213,3 +226,4 @@ export class SimController {
return dueDate
}
}

View File

@@ -18,7 +18,7 @@ export class SimRouter {
this.routes = new Map([
["activate", this.simController.activate()],
["pause", this.simController.suspend()],
["cancel", this.simController.terminate()], // terminate
["cancel", this.simController.terminate()],
["reActivate", this.simController.reActivate()],
["preActivate", this.simController.preActivate()]
]);
@@ -27,6 +27,8 @@ export class SimRouter {
/**
* 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) {

View File

@@ -3,21 +3,26 @@ import { HttpClient } from "sim-shared/infrastructure/HTTPClient.js"
import { AxiosError } from "axios"
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"
// TODO:
// - Pasar a un archivo de DTOs
// - Mucha repeticion por funcion, deberia hacer una plantilla
export class SimUseCases {
private readonly httpClient: HttpClient
private readonly operationRepository: OperationsRepositoryPort
private readonly orderRepository: OrderRepository
constructor(args: {
httpClient: HttpClient,
operationRepository: OperationsRepositoryPort
operationRepository: OperationsRepositoryPort,
orderRepository: OrderRepository
}) {
this.httpClient = args.httpClient
this.operationRepository = args.operationRepository
this.orderRepository = args.orderRepository
}
private async logOperation(data: ObjeniousOperation) {
@@ -26,11 +31,89 @@ export class SimUseCases {
})
}
/**
* Garantiza el flujo de todos los casos de uso de:
* - Petición según la acción
* - Control de errores
* - Siempre devuelve un Result
* - Almacena la operacion en la base de datos
* - Actualiza el estado del order
*
* Necesita:
* - Mas control según el codigo de error
*/
private generateUseCase<
PAYLOAD,
RESPONSETYPE extends { requestId: string }
>(args: {
correlation_id?: string,
url: string,
operation: string,
operationPayload: PAYLOAD,
iccid: string
onError?: (_: any) => void
// on code response??
}): () => Promise<Result<string, boolean>> {
return async () => {
const req = this.httpClient.client.post<RESPONSETYPE>(args.url, {
...args.operationPayload
})
try {
const response = await req;
if (response.status == 200) {
assert(response.data.requestId != undefined)
// Creacion de la operacion inicial, antes de tener los datos
const operation: ObjeniousOperation = {
operation: args.operation,
iccids: String(args.iccid),
status: "noMassID",
request_id: response.data.requestId
}
this.logOperation(operation)
.then().catch(e => console.error(e))
if (args.correlation_id != undefined) {
this.orderRepository.updateOrder({
correlation_id: args.correlation_id!,
new_status: "running", // Siempre es runing la primera vez que se consume
})
.then(e => console.log("Order actualizado: ", e))
.catch(e => console.error("Error actualizando order", args.correlation_id))
}
return <Result<string, boolean>>{
error: undefined,
data: true
}
} else {
return {
error: String(response.status),
data: undefined
}
}
} catch (error) {
console.error(`[Sim.usecase] Error ${args.operation}`, (error as AxiosError).response?.status)
return {
error: "Error general de la peticion",
data: undefined
}
}
}
}
public activate(activationData: ActivationData): () => Promise<Result<string, boolean>> {
const OPERATION_URL = "/actions/activateLine"
return async () => {
const req = this.httpClient.client.post(OPERATION_URL, {
...activationData
dueDate: activationData.dueDate,
identifier: activationData.identifier,
customerAccountCode: activationData.customerAccountCode,
offer: activationData.offer
})
try {
@@ -41,7 +124,7 @@ export class SimUseCases {
const operation: ObjeniousOperation = {
operation: "activate",
iccids: activationData.identifier.identifiers,
iccids: String(activationData.identifier.identifiers),
status: "noMassID",
request_id: response.data.requestId
}
@@ -52,8 +135,6 @@ export class SimUseCases {
error: undefined,
data: true
}
} else {
// muy mejorable el control de errores
return {
@@ -83,8 +164,9 @@ export class SimUseCases {
if (resp.status == 200) {
console.log("Sim preactivada con exito", resp.data)
const operation: ObjeniousOperation = {
correlation_id: preActivateData.correlation_id,
operation: "preActivate",
iccids: preActivateData.identifier.identifiers,
iccids: String(preActivateData.identifier.identifiers),
status: "noMassID",
request_id: resp.data.requestId
}
@@ -97,14 +179,14 @@ export class SimUseCases {
} else {
return <Result<string, boolean>>{
error: String(resp.status),
data: true
data: undefined
}
}
} catch (error) {
console.error("Error preactivacion", preActivateData)
return <Result<string, boolean>>{
error: "Error preactivando la sim" + preActivateData.identifier,
data: true
data: undefined
}
}
}
@@ -118,17 +200,25 @@ export class SimUseCases {
})
try {
const e = await req
console.log("Sim reactivada con exito", e.data)
return <Result<string, boolean>>{
error: undefined,
data: true
const response = await req
if (response.status == 200) {
console.log("[o] Sim solicitud de reactivacion ", response.data)
return <Result<string, boolean>>{
error: undefined,
data: true
}
} else {
return {
error: String(response.status),
data: undefined
}
}
} catch (error) {
console.error("Error reactivacion", error)
console.error("[x] Error reactivacion", (error as AxiosError).response?.status)
return <Result<string, boolean>>{
error: "Error reactivando la sim" + pauseData.identifier,
data: true
data: undefined
}
}
}
@@ -136,53 +226,30 @@ export class SimUseCases {
public suspend(suspendData: ActionData): () => Promise<Result<string, boolean>> {
const OPERATION_URL = "/actions/suspendLine"
return async () => {
const req = this.httpClient.client.post(OPERATION_URL, {
...suspendData
})
try {
const e = await req
console.log("Sim pausada/suspendida con exito", e.data)
return <Result<string, boolean>>{
error: undefined,
data: true
}
} catch (error) {
console.error("[Pausa Use case] Error pausa")
return {
error: "Error general pausando/suspendiendo la sim" + suspendData.identifier,
data: undefined
}
}
}
return this.generateUseCase({
correlation_id: suspendData.correlation_id,
operationPayload: {
dueDate: suspendData.dueDate,
identifier: suspendData.identifier
},
url: OPERATION_URL,
iccid: suspendData.identifier.identifiers[0], //
operation: "suspend"
})
}
public terminate(terminationData: ActionData): () => Promise<Result<string, boolean>> {
const OPERATION_URL = "/actions/terminateLine"
return async () => {
const req = this.httpClient.client.post(OPERATION_URL, {
...terminationData
})
// TODO: para cuando estemos listos.
throw new Error("Peticion no reversible desactivada de momento")
try {
const e = await req
console.log("Sim cancelada con exito", e.data)
return <Result<string, boolean>>{
error: undefined,
data: true
}
} catch (error) {
console.error("Error pausa", error)
return <Result<string, boolean>>{
error: "Error cancelando/terminate la sim" + terminationData.identifier,
data: undefined
}
}
}
return this.generateUseCase({
correlation_id: terminationData.correlation_id,
operationPayload: {
dueDate: terminationData.dueDate,
identifier: terminationData.identifier
},
url: OPERATION_URL,
iccid: terminationData.identifier.identifiers[0], //
operation: "terminate"
})
}

View File

@@ -12,8 +12,8 @@ export const env = {
POSTGRES_HOST: process.env.POSTGRES_HOST,
POSTGRES_DATABASE: process.env.POSTGRES_DATABASE,
RABBITMQ_HOST: String(process.env.RABBITMQ_HOST ?? "localhost"),
RABBITMQ_USER: String(process.env.RABBITMQ_USER ?? "guest"),
RABBITMQ_PASSWORD: String(process.env.RABBITMQ_PASSWORD ?? "guest"),
RABBITMQ_USER: String(process.env.RABBITMQ_USER ?? "test"),
RABBITMQ_PASSWORD: String(process.env.RABBITMQ_PASSWORD ?? "test"),
RABBITMQ_EXCHANGE: String(process.env.RABBITMQ_EXCHANGE ?? "/"),
RABBITMQ_PORT: parseInt(process.env.RABBITMQ_PORT ?? "5672"),
RABBITMQ_MODULENAME: process.env.MODULENAME,
@@ -28,7 +28,7 @@ export const env = {
OBJ_CLI_ASSERTION: String(process.env.OBJ_CLI_ASSERTION),
OBJ_CLIENT_ID: String(process.env.OBJ_CLIENT_ID),
OBJ_KID: String(process.env.OBJ_KID),
OBJ_BASE_URL: String(process.env.OBJ_BASE_URL)
OBJ_BASE_URL: String(process.env.OBJ_BASE_URL),
OBJ_CUSTOMER_CODE: String(process.env.OBJ_CUSTOMER_CODE)
};

View File

@@ -1,5 +1,6 @@
export type ActionData = {
correlation_id?: string;
dueDate: string, // isodate
filter?: {} // no se si hace falta
identifier: {

View File

@@ -1,5 +1,5 @@
import { OperationsRepository } from "sim-shared/infrastructure/OperationRepository.js"
import { ObjeniousOperationsRepository } from "sim-shared/infrastructure/ObjeniousOperationRepository.js"
import { startRMQClient } from "#config/eventBus.config.js"
import { httpInstance } from "#config/httpClient.config.js"
import { pgPool } from "#config/postgreConfig.js"
@@ -7,6 +7,7 @@ import { PgClient } from "sim-shared/infrastructure/PgClient.js"
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"
async function startWorker() {
const rmqClient = await startRMQClient()
@@ -17,13 +18,15 @@ async function startWorker() {
await pgClient.checkDatabaseConnection()
const operationRepository = new OperationsRepository(pgClient)
const operationRepository = new ObjeniousOperationsRepository(pgClient)
const orderRepository = new OrderRepository(pgClient)
const simActivationController = new SimController(
rmqClient,
new SimUseCases({
httpClient: httpClient,
operationRepository: operationRepository
operationRepository: operationRepository,
orderRepository: orderRepository
})
)
const simRouter = new SimRouter(simActivationController, rmqClient)

View File

@@ -68,7 +68,6 @@
"cors": "*",
"dotenv": "*",
"express": "*",
"sim-consumidor-objenious": "sim-consumidor-objenious:*",
"sim-shared": "sim-shared:*",
"typescript": "*"
},

View File

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

View File

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

View File

@@ -1,164 +1,181 @@
import { Request, Response } from "express"
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"
// Partiendo del caracter 3 2 de pais + 2 de compañia
// Metiendolo a la BDD podria ser mas dinamico pero perderia
// tiempo de query
// Puede que esté bien crear un endpoint para administrarlo
const COMPAÑIASICCID = new Map<string, string>(
[
["3490", "alai"],
["3510", "nos"],
["3320", "objenious"]
])
export class SimController {
private simUseCases: SimUsecases
constructor(args: {
simUseCases: SimUsecases
simUseCases: SimUsecases,
}) {
this.simUseCases = args.simUseCases
this.activation = this.activation.bind(this)
}
public preactivation() {
/**
* TODO:
* En proceso, tiene varios problemas
*
* Abstrae el proceso de
* Peticion -> validacion del body -> map del body -> useCase -> OK/ERR
*
* <O> Representa el dato original
* <P> Representa el dato después del mapeo
*/
public controllerGenerator<O extends Object, P extends Object>(args: {
validator?: BodyValidator<O>,
mapBody?: (body: O) => P,
useCase: (args: P) => Promise<any>,
onError: (args: O | P, error: string) => void,
onSuccess: (args: P) => void,
}) {
return async (req: Request, res: Response) => {
const valido = this.validateBody(req.body, res)
if (valido == false) return;
const body = req.body
const { iccid } = req.body
const compañia = this.compañiaFromIccid(iccid)
// 1. Validacion del body
if (args.validator != undefined) {
const validationResult = args.validator.validate(body)
if (validationResult.error != undefined) {
res.status(422).json({
errors: {
...validationResult.error
}
})
args.onError(body, validationResult.error.msg)
return 1;
}
}
if (compañia == undefined) {
res.status(500).json({
// 2. Transformacion del body
// TODO: sustituir el try cach
let data: P = body;
try {
if (args.mapBody != undefined)
data = args.mapBody(body)
} catch (e) {
res.status(422).json({
errors: {
msg: "El iccid no pertenece a una compañia conocida"
msg: "Error parseando el body: " + e
}
})
return;
args.onError(body, String(e))
return 1;
}
// 3. Aplicacion del UseCase
// TODO: todos los use cases tienen que pasar a devolver un Result<>
const usecaseResult = await args.useCase(data) // no deberia hacer falta el trycatch
try {
await this.simUseCases.preActivation({ iccid, compañia })
res.status(200).json({
iccid: iccid,
operation: "activation"
}).send()
} catch (err) {
console.error("Error activando la sim ", req.body)
// 4. Casos de error del usecase
if (usecaseResult.error != undefined) {
// 4.1 Error del caso de uso
res.status(500).json({
errors: {
msg: "Error general de activation"
...usecaseResult.error
}
}).send()
return;
args.onError(body, usecaseResult.error ?? "Error indefinido")
return 1;
}
// 5. Se devuelve al usuario el caso de exito
res.status(200).json(
usecaseResult.data
).send()
args.onSuccess(usecaseResult.data)
return 0;
}
}
public test() {
return this.controllerGenerator<{ iccid: string, offer: string }, { iccid: string }>({
validator: iccidValidator,
useCase: (args) => this.simUseCases.test(args),
onError: (data, error) => console.error(error),
onSuccess: (data) => {
console.log("OK", data)
}
})
}
public preactivation() {
return this.controllerGenerator<{ iccid: string, offer: string }, { iccid: string, offer: string, compañia: string }>({
validator: activationValidator,
mapBody: (b) => {
const { iccid, offer } = b
const compañia = companyFromIccid(iccid)
return { iccid, compañia, offer }
},
useCase: (args) => this.simUseCases.preActivation(args),
onError: (d, e) => console.error("[x] Error preactivation: ", d, e),
onSuccess: console.log
})
}
public activation() {
return async (req: Request, res: Response) => {
const valido = this.validateBody(req.body, res)
if (valido == false) return; // Si no es valido ya se ha enviado el error
const { iccid, offer } = req.body
const compañia = this.compañiaFromIccid(iccid)
if (compañia == undefined) {
res.status(500).json({
errors: {
msg: "El iccid no pertenece a una compañia conocida"
}
})
return;
}
try {
await this.simUseCases.activation({ iccid, compañia, offer })
res.status(200).json({
iccid: iccid,
operation: "activation"
}).send()
} catch (err) {
console.error("Error activando la sim ", req.body)
res.status(500).json({
errors: {
msg: "Error general de activation"
}
}).send()
return;
}
}
return this.controllerGenerator<{ iccid: string, offer: string }, { iccid: string, offer: string, compañia: string }>({
validator: activationValidator,
mapBody: (b) => {
const { iccid, offer } = b
const compañia = companyFromIccid(iccid)
return { iccid, compañia, offer }
},
useCase: (args) => this.simUseCases.activation(args),
onError: (d, e) => console.error("[x] Error activacion: ", d, e),
onSuccess: console.log
})
}
public cancelation() {
return async (req: Request, res: Response) => {
const valido = this.validateBody(req.body, res)
if (valido == false) return; // Si no es valido ya se ha enviado el error
const { iccid } = req.body
const compañia = this.compañiaFromIccid(iccid)
try {
await this.simUseCases.cancelation({ iccid, compañia })
res.status(200).json({
iccid: iccid,
operation: "cancelation"
})
} catch (err) {
console.error("Error cancelando la sim ", req.body)
res.status(500).json({
errors: {
msg: "Error general de cancelacion"
}
})
}
}
return this.controllerGenerator<{ iccid: string }, { iccid: string, compañia: string }>({
validator: iccidValidator,
mapBody: (b) => {
const { iccid } = b
const compañia = companyFromIccid(iccid)
return { iccid, compañia }
},
useCase: (args) => this.simUseCases.cancelation(args),
// TODO: Meter en los mensajes el nombre de la operacion
onError: (d, e) => console.error("[x] Error cancelacion: ", d, e),
onSuccess: console.log
})
}
public pause() {
return async (req: Request, res: Response) => {
const valido = this.validateBody(req.body, res)
return this.controllerGenerator<{ iccid: string }, { iccid: string, compañia: string }>({
validator: iccidValidator,
mapBody: (b) => {
const { iccid } = b
const compañia = companyFromIccid(iccid)
return { iccid, compañia }
},
useCase: (args) => this.simUseCases.pause(args),
onError: (d, e) => console.error("[x] Error pausa: ", d, e),
onSuccess: console.log
})
if (valido == false) return; // Si no es valido ya se ha enviado el error
const { iccid } = req.body
const compañia = this.compañiaFromIccid(iccid)
try {
await this.simUseCases.pause({ iccid, compañia })
res.status(200).json({
iccid: iccid,
operation: "cancelation"
})
} catch (err) {
console.error("Error pausando la sim ", req.body)
res.status(500).json({
errors: {
msg: "Error pausando la sim"
}
})
}
}
}
public free() {
return async (req: Request, res: Response) => {
const valido = this.validateBody(req.body, res)
if (valido == false) return; // Si no es valido ya se ha enviado el error
try {
iccidValidator.validate(req.body)
} catch (e) {
res.status(422).json({
errors: {
msg: e
}
})
}
const { iccid } = req.body
const compañia = this.compañiaFromIccid(iccid)
const compañia = companyFromIccid(iccid)
try {
await this.simUseCases.cancelation({ iccid, compañia })
@@ -178,14 +195,19 @@ export class SimController {
}
public save() {
return async (req: Request, res: Response) => {
const valido = this.validateBody(req.body, res)
if (valido == false) return; // Si no es valido ya se ha enviado el error
try {
iccidValidator.validate(req.body)
} catch (e) {
res.status(422).json({
errors: {
msg: e
}
})
}
const { iccid } = req.body
const compañia = this.compañiaFromIccid(iccid)
const compañia = companyFromIccid(iccid)
try {
await this.simUseCases.cancelation({ iccid, compañia })
@@ -203,40 +225,4 @@ export class SimController {
}
}
}
private validateBody(body: any, res: Response) {
const { iccid } = body
let errors = {}
let valid = true
if (iccid == undefined) {
res.status(400)
errors = {
...errors,
iccid: "El iccid es undefined"
}
valid = false
}
if (valid == false) {
res.json({
errors: errors
})
}
return valid;
}
/**
* A partir del iccid completo devuelve la compañia a la que pertenece
* @throws Error si no hay una compañia definida en COMPAÑIASICCID con el codigo
*/
private compañiaFromIccid(iccid: string) {
const caracteresCommpañia = iccid.slice(2, 6)
const compañia = COMPAÑIASICCID.get(caracteresCommpañia)
if (compañia == undefined) throw new Error("El la compañia es desconocida: " + caracteresCommpañia)
return compañia
}
}

View File

@@ -1,23 +1,89 @@
import { OrderRepository } from "sim-shared/infrastructure/OrderRepository.js";
import { Result } from "sim-shared/domain/Result.js";
import assert from "node:assert";
import { EventBus } from "sim-shared/domain/EventBus.port";
import { SimEvents } from "sim-shared/domain/SimEvents";
import { uuidv7 } from "uuidv7";
import { CreateOrderDTO, OrderTracking, OrderType, OrderTypeOptions } from "sim-shared/domain/Order.js";
/**
* TODO:
* - Conexion con la BDD
* - Conexion con RabbitMQ
* - Pasar a clase cuando existan las conexiones
* Casos de uso de tarjetas sim. Garantiza que todos los metodos usan el mismo bus de mensajes
* y repositorio de registro de las ordenes.
*/
export class SimUsecases {
private eventBus: EventBus
private eventBus: EventBus;
private orderRepository: OrderRepository;
constructor(args: {
eventBus: EventBus
eventBus: EventBus,
orderRepository: OrderRepository
}
) {
this.eventBus = args.eventBus
this.orderRepository = args.orderRepository
}
/**
* Añade un id de mensaje (correlation_id en la base de datos) a los mensajes que van a entrar en la cola
*/
private addMessage_id(event: SimEvents.general): SimEvents.general & { headers: { message_id: string } } {
const uuid = uuidv7()
return {
...event,
headers: {
...event.headers,
message_id: uuid
}
}
}
/**
* El tipo T es el tipo del payload del Order
*/
private async saveOrder<T extends any>(event: SimEvents.general): Promise<Result<string, OrderTracking<T>>> {
if (event.headers?.message_id == undefined) {
return <Result<string, any>>{
error: "El evento no tiene una cabecera message_id definido"
}
}
const orderType = (event.key.split(".")[2] as OrderType ?? "unknown")
// Estoy pensando en la posibilidad de pasarlo a unknown
if (!OrderTypeOptions.has(orderType)) {
return <Result<string, any>>{
error: `El evento no tiene un tipo valido: ${orderType} no existe como tipo valido`
}
}
const order: CreateOrderDTO = {
correlation_id: event.headers.message_id,
order_type: orderType,
routing_key: event.key,
payload: event
}
const result = await this.orderRepository.createOrder<T>(order)
return result;
}
async test(args: { iccid: string }) {
assert(args.iccid != undefined)
const event = <SimEvents.general>{
key: `sim.test.unknown`,
payload: {
iccid: args.iccid
}
}
const eventWithId = this.addMessage_id(event)
const publish = await this.eventBus.publish([eventWithId])
await this.saveOrder(eventWithId)
return eventWithId
}
/**
* TODO:
* Crea una nueva sim de la que no se tenia registro anteriormente
* Si ya existia se modifican los campos pero no se hace un cambio
* de estado.
@@ -34,8 +100,8 @@ export class SimUsecases {
return this.eventBus.publish([activationEvent])
}
async activation(args: { iccid: string, compañia: string, offer: string }) {
async activation(args: { iccid: string, compañia: string, offer: string }):
Promise<Result<string, { iccid: string, message_id: string, operation: "activation" }>> {
const activationEvent = <SimEvents.activation>{
key: `sim.${args.compañia}.activate`,
payload: {
@@ -43,11 +109,29 @@ export class SimUsecases {
offer: args.offer
}
}
console.log("[d] Activation ", activationEvent)
return this.eventBus.publish([activationEvent])
const activationWithId = this.addMessage_id(activationEvent)
console.log("[d] Activation ", activationWithId)
await this.eventBus.publish([activationWithId])
const createdOrder = await this.saveOrder<SimEvents.activation>(activationWithId)
if (createdOrder.error != undefined) {
console.error(createdOrder.error)
return {
error: createdOrder.error
}
}
return {
data: {
iccid: args.iccid,
operation: "activation",
message_id: createdOrder.data?.correlation_id
}
}
}
async preActivation(args: { iccid: string, compañia: string }) {
async preActivation(args: { iccid: string, compañia: string }):
Promise<Result<string, { iccid: string, message_id: string, operation: "preactivation" }>> {
const preActivationEvent = <SimEvents.preActivation>{
key: `sim.${args.compañia}.preActivate`,
@@ -56,22 +140,56 @@ export class SimUsecases {
}
}
console.log("[d] Pre - activation ", preActivationEvent)
return this.eventBus.publish([preActivationEvent])
await this.eventBus.publish([preActivationEvent])
const preactivationWithId = this.addMessage_id(preActivationEvent)
const createdOrder = await this.saveOrder<SimEvents.preActivation>(preactivationWithId)
if (createdOrder.error != undefined) {
console.error(createdOrder.error)
return {
error: createdOrder.error
}
}
return {
data: {
iccid: args.iccid,
operation: "preactivation",
message_id: createdOrder.data?.correlation_id
}
}
}
/**
* Para objenious es terminate
*/
async cancelation(args: { iccid: string, compañia: string }) {
async cancelation(args: { iccid: string, compañia: string }):
Promise<Result<string, { iccid: string, message_id: string, operation: "cancelation" }>> {
const activationEvent = <SimEvents.cancel>{
const cancelationEvent = <SimEvents.cancel>{
key: `sim.${args.compañia}.cancel`,
payload: {
iccid: args.iccid
}
}
console.log("[d] Cancelation ", activationEvent)
return this.eventBus.publish([activationEvent])
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 {
error: savedOrder.error
}
}
return {
data: {
iccid: args.iccid,
message_id: savedOrder.data.correlation_id,
operation: "cancelation"
}
}
}
// alias por si acaso
public terminate = this.cancelation;
@@ -79,16 +197,36 @@ export class SimUsecases {
/**
* alias de bloquear / suspender en objenious
*/
async pause(args: { iccid: string, compañia: string }) {
const cancelationEvent = <SimEvents.pause>{
async pause(args: { iccid: string, compañia: string }):
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)
return this.eventBus.publish([cancelationEvent])
if (savedOrder.error != undefined) {
console.error(savedOrder.error)
return {
error: savedOrder.error
}
}
return {
data: {
iccid: args.iccid,
message_id: savedOrder.data.correlation_id,
operation: "pause"
}
}
}
async free(args: { iccid: string, compañia: string }) {
const cancelationEvent = <SimEvents.free>{
key: `sim.${args.compañia}.free`,

View File

@@ -0,0 +1,39 @@
import { describe, it } from "node:test";
import { iccidValidator } from "./httpValidators.js";
import assert from "node:assert";
describe("test validators", () => {
it("should validate 19 char iccid", () => {
const validBody = {
iccid: "8933201125068886692"
}
const res = iccidValidator.validate(validBody)
assert(res.error == undefined)
}),
// TODO: Nada de esto es valido, a partir de ahora los validadores no lanzan excepcion sino Result
it("shouldnt validate empty string iccid", () => {
const validBody = {
iccid: ""
}
assert
.throws(() => iccidValidator.validate(validBody), { message: "La longitud del iccid es incorrecta debera ser de 19 caracteres" })
}),
it("shouldnt validate >19 char iccid", () => {
const validBody = {
iccid: "893320112506888669212345"
}
assert
.throws(() => iccidValidator.validate(validBody), { message: "La longitud del iccid es incorrecta debera ser de 19 caracteres" })
}),
it("shouldnt validate <19 char iccid", () => {
const validBody = {
iccid: "8933201125"
}
assert
.throws(() => iccidValidator.validate(validBody), { message: "La longitud del iccid es incorrecta debera ser de 19 caracteres" })
})
})

View File

@@ -0,0 +1,84 @@
import { companyFromIccid } from "#domain/companies.js";
import { BodyValidator, Validator } from "sim-shared/aplication/BodyValidator.js";
const offers = new Map([
["mensual", "SAVEFAMILY1"],
["anual", "SAVEFAMILY2"],
["SAVEFAMILY1", "SAVEFAMILY1"],
["SAVEFAMILY2", "SAVEFAMILY2"],
])
const iccidLongitudValidator = <Validator<{ iccid: string }>>{
field: "iccid",
errorMsg: "La longitud del iccid es incorrecta debera ser de 19 caracteres",
validationFunc: (a: { iccid: string }) => a.iccid.length == 19,
}
const iccidRequired = <Validator<{ iccid: string }>>{
field: "iccid",
errorMsg: "El iccid debe estara definido",
validationFunc: (a: { iccid: string }) => a.iccid != undefined,
}
const iccidWithValidCompany = <Validator<{ iccid: string }>>{
field: "iccid",
errorMsg: "El iccid no corresponde a una compañia registrada",
validationFunc: (a: { iccid: string }) => companyFromIccid(a.iccid) != undefined,
}
const offerExists = <Validator<{ offer: string }>>{
field: "offer",
errorMsg: "La oferta introducida no es valida",
validationFunc: (a: { offer: string }) => offers.has(a.offer),
}
const isUuidv7 = <Validator<{ correlation_id?: string }>>{
field: "correlation_id",
errorMsg: "El uuid no es un uuidv7 valido",
validationFunc: (a) => a.correlation_id != undefined && a.correlation_id.length < 36
}
const definedId = <Validator<{ id?: number }>>{
field: "id",
errorMsg: "El id no se ha definido",
validationFunc: (e) => e.id != undefined
}
const isIntegerId = <Validator<{ id?: number }>>{
field: "id",
errorMsg: "El id no se ha definido",
validationFunc: (e) => Number.isInteger(e.id)
}
const validNumericId = <Validator<{ id?: number }>>{
field: "id",
errorMsg: "El id introducido no es un numero >= 0",
validationFunc: (e) => e.id! >= 0
}
export const activationValidator = new BodyValidator<{ iccid: string, offer: string }>(
[
iccidRequired,
iccidLongitudValidator,
iccidWithValidCompany,
offerExists,
]
)
export const iccidValidator = new BodyValidator<{ iccid: string }>(
[
iccidRequired,
iccidLongitudValidator,
iccidWithValidCompany,
]
)
export const uuidValidator = new BodyValidator<{ correlation_id?: string }>([
isUuidv7
])
export const idValidator = new BodyValidator<{ id?: number }>([
definedId,
isIntegerId,
validNumericId
])

View File

@@ -1,7 +1,8 @@
import { loadEnvFile } from "node:process";
import path from "node:path";
loadEnvFile(path.join(import.meta.dirname, "../../../../.env"))
loadEnvFile(path.join("../../.env")) // Global
export const env = {
ENVIRONMENT: process.env.ENVIORMENT,
@@ -12,8 +13,8 @@ export const env = {
POSTGRES_HOST: process.env.POSTGRES_HOST,
POSTGRES_DATABASE: process.env.POSTGRES_DATABASE,
RABBITMQ_HOST: String(process.env.RABBITMQ_HOST ?? "localhost"),
RABBITMQ_USER: String(process.env.RABBITMQ_USER ?? "guest"),
RABBITMQ_PASSWORD: String(process.env.RABBITMQ_PASSWORD ?? "guest"),
RABBITMQ_USER: String(process.env.RABBITMQ_USER ?? "test"),
RABBITMQ_PASSWORD: String(process.env.RABBITMQ_PASSWORD ?? "test"),
RABBITMQ_EXCHANGE: String(process.env.RABBITMQ_EXCHANGE ?? "/"),
RABBITMQ_PORT: parseInt(process.env.RABBITMQ_PORT ?? "5672"),
RABBITMQ_MODULENAME: process.env.MODULENAME,

View File

@@ -0,0 +1,18 @@
import { Pool } from 'pg';
import { PgClient } from 'sim-shared/infrastructure/PgClient.js'
import { env } from './env/index.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) || 5432,
});
export const postgresClient = new PgClient({
pool: pgPool
})

View File

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

View File

@@ -0,0 +1,22 @@
// Partiendo del caracter 3 2 de pais + 2 de compañia
// Metiendolo a la BDD podria ser mas dinamico pero perderia
// tiempo de query
// Puede que esté bien crear un endpoint para administrarlo
export const COMPANYICCID = new Map<string, string>(
[
["3490", "alai"],
["3510", "nos"],
["3320", "objenious"]
])
/**
* A partir del iccid completo devuelve la compañia a la que pertenece
* @throws Error si no hay una compañia definida en COMPAÑIASICCID con el codigo
*/
export function companyFromIccid(iccid: string) {
const caracteresCommpañia = iccid.slice(2, 6)
const compañia = COMPANYICCID.get(caracteresCommpañia)
if (compañia == undefined) throw new Error("El la compañia es desconocida: " + caracteresCommpañia)
return compañia
}

View File

@@ -1,8 +1,10 @@
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";
const PORT = env.API_PORT
const HOSTNAME = "0.0.0.0"
@@ -24,6 +26,9 @@ app.use(express.json());
app.use(express.urlencoded({ extended: true }));
app.use("/sim", simRoutes)
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" })

View File

@@ -0,0 +1,40 @@
/**
* Rutas para consultar el estado de los order
*/
import { OrderRepository } from "sim-shared/infrastructure/OrderRepository.js"
import { Router } from "express"
import { postgresClient } from '#config/postgreConfig.js';
import { OrderController } from "../aplication/Order.controller.js";
import { OrderUsecases } from "../aplication/Order.usecases.js";
const orderRoutes = Router()
// orderRepository no se trata como singleton
const orderRepository = new OrderRepository(postgresClient)
const orderUseCases = new OrderUsecases({
orderRepository: orderRepository
})
const orderController = new OrderController({
orderUseCases: orderUseCases
})
/**
* Todas las orders, o un resumen, admite filtros
* por:
* - status
* - fecha inicio
* - fecha fin
* - pendientes
* */
orderRoutes.get("/", (req, res) => { res.send("ok") })
orderRoutes.get("/message_id/:correlation_id", orderController.getByQueueId())
/** Operaciones pendientes */
orderRoutes.get("/pending", orderController.getPending())
/** Order por id (uuid del mensaje) */
orderRoutes.get("/:id", orderController.getById())
export { orderRoutes }

View File

@@ -2,17 +2,22 @@ import { rabbitmqEventBus } from '#config/eventBusConfig.js';
import { SimUsecases } from '../aplication/Sim.usecases.js';
import { SimController } from '../aplication/Sim.controller.js';
import { Router } from 'express';
import { OrderRepository } from 'sim-shared/infrastructure/OrderRepository.js';
import { postgresClient } from '#config/postgreConfig.js';
const simRoutes = Router()
const orderRepository = new OrderRepository(postgresClient)
const simUseCases = new SimUsecases({
eventBus: rabbitmqEventBus
eventBus: rabbitmqEventBus,
orderRepository: orderRepository
})
const simController = new SimController({
simUseCases: simUseCases
})
// TODO: status de todos los proyectos
simRoutes.get("/status", () => { })
simRoutes.post("/save", simController.save())
@@ -25,6 +30,8 @@ simRoutes.post("/pause", simController.pause())
simRoutes.post("/cancel", simController.cancelation())
simRoutes.post("/test", simController.test())
// Proceso especifico de ALAI para liberar sims canceladas
simRoutes.post("/free", simController.free())

View File

@@ -13,12 +13,6 @@
"types": "./config/*.ts",
"default": "./config/*.js"
},
"#shared/*.js": {
"default": "../sim-shared/*.js"
},
"#shared/*": {
"default": "../sim-shared/*.js"
},
"#adapters/*.js": {
"types": "./infrastructure/*.ts",
"default": "./infrastructure/*.js"
@@ -45,7 +39,7 @@
}
},
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"test": "node --import tsx --test ./**/*.test.ts",
"build": "tsc --build && tsc-alias -p tsconfig.json && cp package.json ../../dist/packages/sim-entrada-eventos/",
"dev": "tsx watch index.ts",
"start": "node ../../dist/packages/sim-entrada-eventos/index.js"
@@ -59,6 +53,7 @@
"cors": "*",
"dotenv": "*",
"express": "*",
"sim-shared": "sim-shared:*",
"typescript": "*"
},
"devDependencies": {

View File

@@ -5,4 +5,8 @@ OBJ_CLI_ASSERTION=XOc7FtwXD8hUX2SFVX94XSty8wkOmChkwDNF09O_aIxPubMDdFUdCDCB4zpzSI
OBJ_CLIENT_ID=savefamily_rest_ws
OBJ_KID=xNfbMiyL1ORXGP8lElhcv8nVaG3EJKye4Lc1YoN3I1E
OBJ_BASE_URL=https://api-getway.objenious.com/ws
//OBJ_BASE_URL=https://api-getway.objenious.com/ws/test
# OBJ_BASE_URL=https://api-getway.objenious.com/ws/test
# 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

@@ -1,5 +1,6 @@
import { loadEnvFile } from "node:process";
import path from "node:path";
import assert from "node:assert";
loadEnvFile(path.join("../../.env")) // Global
loadEnvFile(path.join("./.env")) // base
@@ -12,9 +13,9 @@ export const env = {
POSTGRES_HOST: process.env.POSTGRES_HOST,
POSTGRES_DATABASE: process.env.POSTGRES_DATABASE,
RABBITMQ_HOST: String(process.env.RABBITMQ_HOST ?? "localhost"),
RABBITMQ_USER: String(process.env.RABBITMQ_USER ?? "guest"),
RABBITMQ_PASSWORD: String(process.env.RABBITMQ_PASSWORD ?? "guest"),
RABBITMQ_EXCHANGE: String(process.env.RABBITMQ_EXCHANGE ?? "/"),
RABBITMQ_USER: String(process.env.RABBITMQ_USER),
RABBITMQ_PASSWORD: String(process.env.RABBITMQ_PASSWORD),
RABBITMQ_EXCHANGE: String(process.env.RABBITMQ_EXCHANGE),
RABBITMQ_PORT: parseInt(process.env.RABBITMQ_PORT ?? "5672"),
RABBITMQ_MODULENAME: process.env.MODULENAME,
RABBITMQ_TTL: process.env.RABBITMQ_TTL,
@@ -28,7 +29,21 @@ export const env = {
OBJ_CLI_ASSERTION: String(process.env.OBJ_CLI_ASSERTION),
OBJ_CLIENT_ID: String(process.env.OBJ_CLIENT_ID),
OBJ_KID: String(process.env.OBJ_KID),
OBJ_BASE_URL: String(process.env.OBJ_BASE_URL)
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)
};
// 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)
if (env.ENVIRONMENT == "production") {
assert(env.RABBITMQ_PASSWORD != "guest")
assert(env.RABBITMQ_HOST != "localhost")
}

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: "postgres",
password: env.POSTGRES_PASSWORD,
port: Number(env.POSTGRES_PORT) || 5432,
});
export const postgresClientIntranet = new PgClient({
pool: pgPoolIntranet
})

View File

@@ -1,4 +1,4 @@
import { Pool, QueryResult } from 'pg';
import { Pool } from 'pg';
import { PgClient } from 'sim-shared/infrastructure/PgClient.js'
import { env } from './env/index.js';

View File

@@ -3,7 +3,11 @@ import { pgPool } from "./config/postgreConfig.js"
import { PgClient } from "sim-shared/infrastructure/PgClient.js"
import { httpInstance } from "./config/httpClient.config.js"
import { CheckObjeniousRequests } from "./tasks/check_objenious_request.js"
import { OperationsRepository } from "sim-shared/infrastructure/OperationRepository.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"
async function startCron() {
const commonSettings = {
@@ -13,30 +17,42 @@ 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 OperationsRepository(pgClient)
const operationRepository = new ObjeniousOperationsRepository(pgClient)
const orderRepository = new OrderRepository(pgClient)
const objeniousLineRepository = new ObjeniousLinesRepository(postgresClientIntranet)
const objTask = new CheckObjeniousRequests(
operationRepository,
httpClient
orderRepository,
httpClient,
)
const interval = setInterval(async () => {
console.log("Updating...")
await objTask.getPendingOperations()
console.log("Update finished")
}, 60 * 1000)
/*
const task = cron.createTask("* * * * *", async () => {
}
, {
...commonSettings,
name: "Test"
})
*/
const volcadoLineasTask = new TaskVolcadoLineas(httpClient, objeniousLineRepository)
const PERIODO_PETICIONES = 10 * 60 * 60
const interval = setInterval(async () => {
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 volcadoLineasTask.loadLines()
//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,7 +31,7 @@
}
},
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"test": "node --import tsx --test ./**/*.test.ts",
"build": "tsc --build && tsc-alias -p tsconfig.json && cp package.json ../../dist/packages/sim-objenious-cron/",
"dev": "tsx watch index.ts",
"start": "node ../../dist/packages/sim-objenious-cron/index.js"

View File

@@ -1 +0,0 @@
export const task = async () => console.log("Background " + new Date().toISOString())

View File

@@ -1,9 +1,13 @@
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";
export class CheckObjeniousRequests {
constructor(
private readonly operationsRepository: IOperationsRepository,
private readonly orderRepository: OrderRepository,
private readonly httpClient: HttpClient
) {
}
@@ -12,59 +16,59 @@ 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()
if (pendingOperations.error != undefined) {
throw new Error("Error obteniendo las tareas pendientes " + pendingOperations.error)
}
if (pendingOperations.data == undefined || pendingOperations.data.length == 0) {
//Nada pendiente
console.log("[cron] No hay operaciones pendientes de Objenious")
return;
}
// 2. Clasificación de las tareas pendientes
// Erroneas => no se les ha dado un request_id, no se pueden comprobar
const erroneas = pendingOperations.data
.filter((e) => e.request_id == undefined)
// Todas las validas
const operacionesValidas = pendingOperations.data
.filter((e) => e.request_id != undefined)
// Validas sin MassId
const solicitarMassId = operacionesValidas
.filter((e) => e.mass_action_id == undefined)
// Validas con MassId
const consultarEstado = pendingOperations.data
.filter(e => e.mass_action_id != undefined)
// TODO: Validas sin/con massID que lleven mucho tiempo sin actualizarse
console.log("validas", operacionesValidas)
console.log("Solicitando mass id para", solicitarMassId)
console.log("[cron] Solicitando mass id para", solicitarMassId.map(e => e.id))
const newMassActions = await this.getMassIdFromRequest(solicitarMassId)
const merged = [...newMassActions || [], ...consultarEstado]
console.log("Solicitando status para", merged)
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;
if (requestList.length == 0) return 0;
const operationsList = structuredClone(requestList)
const PATH = "/actions/massActions/"
const updated = []
const iccids = operationsList
.map(e => e.iccids)
.flat()
const mass_actions = operationsList
.filter(e => e.mass_action_id != undefined)
const iccidSet = new Set<string>(iccids)
console.log("iccidSet", iccidSet)
// 1. Una peticion por cada accion a comprobar
// Las peticiones por iccid u otro filtro tardan ~50s
for (const originalAction of mass_actions) {
@@ -79,27 +83,28 @@ export class CheckObjeniousRequests {
try {
res = await req
} catch (e) {
console.error("Error comprobando el estado de ", originalAction)
console.error("Error: ", e)
console.error("[cron] Error comprobando el estado de ", originalAction)
console.error("[cron] Error: ", e)
return;
}
const { data } = res
console.log("Estado de : ", originalAction.mass_action_id, originalAction.iccids)
console.log("[cron] Estado de : ", originalAction.mass_action_id, originalAction.iccids)
console.log(res.status, data)
if (res.status != 200 || data == undefined) {
console.error("Error buscando los massActions")
console.error("[cron] Error buscando los massActions")
continue;
}
// 2. Se comprueba si ha habido un cambio de estado
const { id, status, info } = data
if (status != originalAction.objenious_status) {
console.log(status, "!=", originalAction.objenious_status)
console.log("Actualizando", originalAction, status)
const hasStatusChanged = status != originalAction.objenious_status
if (hasStatusChanged) {
console.log("[cron] Actualizando", originalAction.id, originalAction.iccids, status)
/** Status convertido al que se usa en la aplicacion */
const uorStatus = this.mapStatus(status)
const updateData: ObjeniousOperationChange = {
operation_id: originalAction.id!,
@@ -109,21 +114,59 @@ export class CheckObjeniousRequests {
previous_status: originalAction.status
}
originalAction.status = uorStatus;
originalAction.objenious_status = status;
originalAction.last_change_date = new Date().toISOString()
const updatedAction = structuredClone(originalAction)
updatedAction.status = uorStatus;
updatedAction.objenious_status = status;
updatedAction.last_change_date = new Date().toISOString()
updatedAction.end_date = originalAction.last_change_date
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])
const msisdn = lineData.content[0].identifier.msisdn
if (originalAction.correlation_id != undefined) {
this.orderRepository.finishOrder({ correlation_id: originalAction.correlation_id })
.then(e => console.log("[o] Finalizada order", e))
.catch(e => {
console.error("[x] Error finalizando la order ", e)
console.error(e)
})
}
if (originalAction.operation == "activation") {
this.notifyFinalization({
...originalAction,
msisdn
})
// TODO la accion no siempre es activacion!
.then(e => {
console.log("[o] Notificada la activacion de ", originalAction.iccids)
})
.catch(e => {
console.error("[x] Error enviando la activacion de ", originalAction)
console.error(e)
})
}
}
if (info != undefined) {
updateData.info = info
}
try {
console.log("Subiendo un update")
console.log("[i] Subiendo un update")
console.log(updateData)
await this.operationsRepository.updateOperation(updateData)
updated.push(originalAction)
} catch (e) {
console.error("Error actualizando el estado de ", originalAction, e)
console.error("[x] Error actualizando el estado de ", originalAction, e)
return;
}
}
@@ -137,13 +180,34 @@ export class CheckObjeniousRequests {
// ! Importante las claves siempre en minuscula, los valores son cammelCase
const equivalentMap = new Map<string, StatusEnum>([
["en cours", "running"],
["terminé", "finished"]
["terminé", "finished"],
["annulé", "finished"]
])
const res = equivalentMap.get(sanitizedStatus)
if (res == undefined) return "running"
return res
}
private async getLineData(iccids: string) {
const PATH = "/lines"
const req = this.httpClient.client.get(PATH, {
params: {
pageSize: 100, // no hace fata
"identifier.identifierType": "ICCID",
"identifier.identifiers": iccids
}
})
try {
const res = await req
return res.data
} catch (e) {
console.error("Error obteniendo datos de la sim")
throw new Error(String(e))
}
}
/**
* Refrescar los requests hasta que conseguir una Id de mass action
* Como no se puede consultar por
@@ -191,10 +255,26 @@ export class CheckObjeniousRequests {
console.log("Error actualizando el estado de ", request)
continue;
}
}
// 3. Se devuelve la lista de los requests con las actualizaciones
return operationsList
}
/**
* Se devuelve la respuesta de una operacion completa de objenious
* al servicio que manda los mails
*/
private async notifyFinalization(operation: ObjeniousOperation & { msisdn: string }) {
const req = axios.post(env.NOTIFICATION_URL, {
...operation,
iccids: [operation.iccids]
}, {
headers: {
"x-apikey-sim-activation": env.SIM_ACTIVATION_API_KEY
}
})
await req
}
}

View File

@@ -0,0 +1,133 @@
import assert from "node:assert";
import { lineToCreateLineDto, ObjeniousLine, ObjeniousLineResponse } from "sim-shared/domain/objeniousLine.js";
import { tryCatch, Result } from "sim-shared/domain/Result.js";
import { HttpClient } from "sim-shared/infrastructure/HTTPClient.js";
import { ObjeniousLinesRepository } from "../infranstructure/ObjeniousLinesRepository.js";
import { AxiosResponse } from "axios";
import { constants } from "node:buffer";
const MAX_PAGE_SIZE = 100
export class TaskVolcadoLineas {
constructor(
private readonly httpClient: HttpClient,
private readonly linesRepository: ObjeniousLinesRepository,
) {
}
/**
* Mover al repo
*/
private async * getLinesByStatus(args?: {
pageSize?: number,
pageNumber?: number,
status?: string
}): AsyncGenerator<Result<string, ObjeniousLine[]>, Result<string, ObjeniousLine[]>, any> {
const path = "/lines"
const pageSize = args?.pageSize ?? 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> = {}
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("Params", params)
console.log(`[i] Cargando pagina ${currentPage} de ${totalPages ?? "(desc)"}`)
const nextPage = await tryCatch<AxiosResponse<ObjeniousLineResponse>>(this.httpClient.client.get(path, {
params: params
}))
if (nextPage.error != undefined) {
console.error(nextPage.error.msg)
return {
error: nextPage.error.msg.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: []
}
}
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.getLinesByStatus()
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) {
console.log()
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

@@ -0,0 +1,35 @@
import { Result } from "../domain/Result.js"
export type Validator<T extends Object> = {
field: keyof T,
errorMsg: string,
validationFunc: (obj: T) => boolean
}
/**
* Ejecuta una lista de validadores en orden, si alguno
* falla devuelve un Error
*/
export class BodyValidator<T extends Object> {
validatorList: Validator<T>[] = []
constructor(
validators: Validator<T>[]
) {
this.validatorList = validators
}
public validate(obj: T): Result<{ msg: string, field: string }, boolean> {
for (const validator of this.validatorList) {
if (validator.validationFunc(obj) == false)
return {
error: {
msg: validator.errorMsg,
field: String(validator.field)
}
}
}
return {
data: true
};
}
}

View File

@@ -0,0 +1,27 @@
/**
* !Importate
* Configuración unicamente para lanzar los test, este código no debe de ejecutarse
* en produccion
*/
import { env, loadEnvFile } from "node:process";
import { Pool } from "pg";
import { PgClient } from "../infrastructure/PgClient.js";
console.warn("[i!] Se está corriendo codigo de test")
loadEnvFile("../../.env") // Global
// 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) || 5432,
});
export const postgresClient = new PgClient({
pool: pgPool
})
console.warn(`[T] TEST DB : ${env.POSTGRES_DATABASE}@${env.POSTGRES_HOST}`)

View File

@@ -7,9 +7,11 @@ export type DomainEventType = string
export type DomainEvent = {
key: string,
payload: Object,
options: Object,
occurredOn: Date,
payload: object,
headers?: object & {
message_id?: string
},
occurredOn?: Date,
}
export interface DomainEventSubscriber<T extends DomainEvent> {

View File

@@ -2,7 +2,7 @@ import { ConsumeMessage } from "amqplib";
import { DomainEvent, DomainEventSubscriber } from "./DomainEvent.js";
export interface EventBus {
publish(events: Array<DomainEvent>): Promise<void>;
publish(events: Array<DomainEvent>): Promise<{ success: DomainEvent[], error: DomainEvent[] }>;
// Sacado de NEKI, posiblemente no haga falta
addSubscribers(subscribers: Array<DomainEventSubscriber<DomainEvent>>): void;

View File

@@ -0,0 +1,85 @@
// Reemplaza al enum OrderStatus
export type OrderStatus =
| 'pending'
| 'running'
| 'finished'
| 'failed'
| 'dlx';
// Reemplaza al enum OrderTypes
export type OrderType =
| 'activate'
| 'preactivate'
| 'cancel'
| 'pause'
| 'reactivate'
| 'unknown';
export const OrderTypeOptions = new Set<OrderType>([
'activate',
'preactivate',
'cancel',
'pause',
'reactivate',
'unknown'
])
// Interfaz para la tabla order_tracking
export interface OrderTracking<T> {
id: number;
correlation_id: string;
exchange?: string | null;
routing_key?: string | null;
order_type: OrderType;
payload?: Record<string, T> | null; // Por no especificar el tipo del json hasta que no se cree
status: OrderStatus;
retry_count: number;
error_message?: string | null;
error_stacktrace?: string | null;
/* TODO: Importante decidir si trabajar con fecha y tener que crear los objetos o seguir como string */
start_date: string | Date;
update_date: string | Date;
finish_date?: string | Date | null;
// desde la 1.1.0
webhook_host?: string | null;
webhook_endpoint?: string | null;
}
// Interfaz para la tabla order_history
export interface OrderHistory {
id: number;
order_id: number;
previous_status: OrderStatus;
new_status: OrderStatus;
change_reason?: string | null;
change_date: Date;
}
// Tipo útil para la creación (Omitiendo campos generados por la DB)
export type CreateOrderDTO = Pick<
OrderTracking<any>, // Aqui realmente no importan los campos
'correlation_id' | 'exchange' | 'routing_key' | 'order_type' | 'payload' | 'webhook_host' | 'webhook_endpoint'
>;
export type UpdateOrderDTO =
(
{ id: number, correlation_id?: never } |
{ id?: never, correlation_id: string }
)
&
{
new_status: OrderStatus,
reason?: string
}
export type FinishOrderDTO =
(
{ id: number, correlation_id?: never } |
{ id?: never, correlation_id: string }
)
&
{
reason?: string
}

View File

@@ -1,7 +1,31 @@
export type Success<D> = {
error?: undefined | null,
data: D
}
export type Failure<E = Error> = {
data?: undefined | null,
error: E
}
/**
* Result<Error,Data>
*/
export type Result<E, D> = {
error: E | undefined,
data: D | undefined
export type Result<E, D> = Failure<E> | Success<D>
export async function tryCatch<T>(func: Promise<T>): Promise<Result<{ msg: Error }, T>> {
try {
const res = await func;
return {
data: res
}
} catch (e: unknown) {
return {
error: {
msg: e as Error
}
}
}
}

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: string, //"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

@@ -10,14 +10,16 @@ export interface IOperationsRepository {
export type ObjeniousOperation = {
id?: number;
/** Uuid del mensaje asociado a la operacion */
correlation_id?: string;
operation: string;
retry_count?: number;
max_retry?: number;
max_date_retry?: Date | null;
iccids: string[];
max_date_retry?: string | null;
iccids: string; // Deberia ser string[] pero no parseo la lista de iccids
request_id?: string;
mass_action_id?: string;
end_date?: Date | null;
end_date?: string | null;
error?: string | null;
status: StatusEnum;
objenious_status?: string;

View File

@@ -2,7 +2,7 @@ import { IOperationsRepository, ObjeniousOperation, ObjeniousOperationChange } f
import { Result } from "sim-shared/domain/Result.js";
import { PgClient } from "sim-shared/infrastructure/PgClient.js";
export class OperationsRepository implements IOperationsRepository {
export class ObjeniousOperationsRepository implements IOperationsRepository {
constructor(
private readonly pgClient: PgClient
@@ -12,10 +12,9 @@ export class OperationsRepository implements IOperationsRepository {
async createOperation(data: ObjeniousOperation): Promise<Result<string, ObjeniousOperation>> {
const query = `
INSERT INTO objenious_operation (operation, iccids, status, max_retry, request_id)
VALUES ($1, $2, $3, $4, $5)
VALUES ($1, $2, $3, $4, $5)
RETURNING *`;
const iccids = JSON.stringify(data.iccids)
const values = [data.operation, iccids, data.status, data.max_retry, data.request_id];
const values = [data.operation, data.iccids, data.status, data.max_retry, data.request_id];
const { rows } = await this.pgClient.query(query, values);
return <Result<string, ObjeniousOperation>>{
data: rows[0]
@@ -46,8 +45,8 @@ export class OperationsRepository implements IOperationsRepository {
error = COALESCE($3,error),
request_id = COALESCE($4, request_id),
mass_action_id = COALESCE($5, mass_action_id),
last_change_date = now(),
end_date = CASE WHEN $2 IN ('finished') THEN now() ELSE end_date END,
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,
objenious_status = $6
WHERE id = $1`;

View File

@@ -0,0 +1,172 @@
import { before, describe, it } from "node:test";
import { OrderRepository } from "./OrderRepository.js";
import { CreateOrderDTO } from "../domain/Order.js";
import { postgresClient } from "../config/config.test.js";
import assert from "node:assert";
const order1 = <CreateOrderDTO>{
correlation_id: "fakeRMQid-1234",
exchange: "fake.ex",
routing_key: "test.order.idk",
order_type: "activate",
payload: { iccid: "1234", action: "activate" }
}
const order2 = <CreateOrderDTO>{
correlation_id: "fakeRMQid-5678",
exchange: "fake.ex",
routing_key: "test.order.idk",
order_type: "activate",
payload: { iccid: "5678", action: "activate" }
}
describe("Test OrderRepository", {}, (ctx) => {
const orderRepo = new OrderRepository(postgresClient)
let testIds: number[] = []
before(async () => {
// Order1
const result1 = await orderRepo.createOrder(order1)
assert(result1.data != undefined)
testIds.push(result1.data.id)
// Order2 -> Para el test de crearOrder
// const result2 = await orderRepo.createOrder(order2)
// assert(result2.data != undefined)
// testIds.push(result2.data.id)
})
it("Insert new Order", async () => {
const newOrder = order1
const result = await orderRepo.createOrder(newOrder)
assert(result.error == undefined)
assert(result.data != undefined)
const order = result.data!
assert(order.id != undefined)
assert(order.correlation_id == newOrder.correlation_id)
assert(order.status == 'pending')
console.log("[T] Creada Order", typeof (result.data.start_date))
})
it("Find by valid id should return the order", async () => {
const result = await orderRepo.getOrderById({ id: testIds[0]! })
assert(result.error == undefined)
assert(result.data != undefined)
const order = result.data
assert(order.id == testIds[0])
assert(order.correlation_id == order1.correlation_id)
})
it("Find by correlation id should return a valid order", async () => {
const result = await orderRepo.getOrderByQueueId({ correlation_id: order1.correlation_id })
assert(result.error == undefined)
assert(result.data != undefined)
const order = result.data
assert(order.correlation_id == order1.correlation_id)
})
it("Get pending orders should return all pending orders in ASC order", async () => {
// We already have 'testId' from before block
// Insert two more orders
const orderA = { ...order1, correlation_id: "pending-A" }
const orderB = { ...order1, correlation_id: "pending-B" }
const resA = await orderRepo.createOrder(orderA)
const resB = await orderRepo.createOrder(orderB)
assert(resA.data != undefined)
assert(resB.data != undefined)
const idA = resA.data.id
const idB = resB.data.id
const result = await orderRepo.getPendingOrders()
assert(result.error == undefined)
assert(Array.isArray(result.data))
const ids = result.data.map(o => o.id)
assert(ids.includes(testIds[0]!))
assert(ids.includes(idA))
assert(ids.includes(idB))
// Verify ordering (ASC by start_date, which maps to ID order in this sequential test)
const indexTest = result.data.findIndex(o => o.id === testIds[0])
const indexA = result.data.findIndex(o => o.id === idA)
const indexB = result.data.findIndex(o => o.id === idB)
assert(indexTest < indexA)
assert(indexA < indexB)
})
it("Update order status should change status and add history", async () => {
const newStatus = "running"
const reason = "Test update"
const result = await orderRepo.updateOrder({ id: testIds[0]!, new_status: newStatus, reason: reason })
assert(result.error == undefined)
assert(result.data != undefined)
assert(result.data.status === newStatus)
})
it("Finish order should set status to finished and set finish_date", async () => {
const result = await orderRepo.finishOrder({ id: testIds[0]!, reason: "Test finish" })
assert(result.error == undefined)
assert(result.data != undefined)
assert(result.data.status === "finished")
assert(result.data.finish_date != null)
})
it("Error order (failed) should increment retry_count and set status", async () => {
// Create another order for this test
const order2 = { ...order1, correlation_id: "fake-error-test" }
const createResult = await orderRepo.createOrder(order2)
assert(createResult.data != undefined)
const errTestId = createResult.data.id
const result = await orderRepo.errorOrder({
id: errTestId,
status: "failed",
reason: "Test failure",
error: "Some error",
stackTrace: "Some stack"
})
assert(result.error == undefined)
assert(result.data != undefined)
assert(result.data.status === "failed")
assert(result.data.retry_count > 0)
assert(result.data.finish_date == null)
})
it("Error order (dlx) should set finish_date", async () => {
// Create another order for this test
const order3 = { ...order1, correlation_id: "fake-dlx-test" }
const createResult = await orderRepo.createOrder(order3)
assert(createResult.data != undefined)
const dlxTestId = createResult.data.id
const result = await orderRepo.errorOrder({
id: dlxTestId,
status: "dlx",
reason: "Test DLX",
error: "Fatal error",
stackTrace: "Fatal stack"
})
assert(result.error == undefined)
assert(result.data != undefined)
assert(result.data.status === "dlx")
assert(result.data.finish_date != null)
})
})

View File

@@ -0,0 +1,446 @@
/**
* TODO: Usar
*/
import { PoolClient, QueryResult, QueryResultRow } from "pg";
import { CreateOrderDTO, FinishOrderDTO, OrderTracking, UpdateOrderDTO } from "../domain/Order.js";
import { Result } 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*.
* Las *Order* son seguimientos de operaciones que han entrado correctamente a cualquier cola
* de mensajes independientemente del pais/empresa objetivo de la tarjeta.
*
* Todas las operaciones devuelven un tipo Result<Error,Data> para gestionar los errores
* de acceso a la BDD, para las operaciones correctas se devuleve Error = undefined, para
* las erroneas Data = undefined.
*/
export class OrderRepository {
constructor(
private readonly pgClient: PgClient
) {
}
/**
* Comprobacion de la query y devolucion del primer resulado
* Garantiza la gestion de errores
*/
private async getFirst<T extends QueryResultRow>(queryPromise: Promise<QueryResult<T>>) {
try {
const queryResult = await queryPromise
return <Result<string, T>>{
data: queryResult.rows[0]
}
} catch (e) {
return <Result<string, T>>{
error: e as string
}
}
}
/**
* Se asume que se va a devolver una lista del tipo T
*/
private async getAll<T extends QueryResultRow>(queryPromise: Promise<QueryResult<T>>) {
try {
const queryResult = await queryPromise
return <Result<string, T[]>>{
data: queryResult.rows
}
} catch (e) {
return <Result<string, T[]>>{
error: e as string
}
}
}
/**
* El tipo <T> representa el contenido del mensaje de los order
*/
public async getOrderById<T>(data: { id: number }): Promise<Result<string, OrderTracking<T>>> {
const query = `
SELECT * FROM order_tracking
WHERE id = $1
`
const values = [data.id]
const queryPromise = this.pgClient.query<OrderTracking<T>>(query, values)
const result = await this.getFirst(queryPromise);
return result
}
/**
* Busqueda según la id de RabbitMq
*/
public async getOrderByQueueId<T>(data: { correlation_id: string }, pool?: PoolClient) {
const query = `
SELECT * FROM order_tracking
WHERE correlation_id = $1
`
const values = [data.correlation_id]
const queryPromise = this.pgClient.query<OrderTracking<T>>(query, values)
const result = await this.getFirst(queryPromise);
return result
}
/**
* Operaciones que no han concluido con filtros de limit, offset y start
* @param options ()
* @returns
*/
public async getPendingOrders<T>(options?: {
limit?: number,
offset?: number,
start?: number // id de inicio
}) {
const client = await this.pgClient.connect();
const offsetFragment = (options?.offset != undefined) ? `OFFSET ${options?.offset}` : ""
const limitFragment = (options?.limit != undefined) ? `LIMIT ${options?.limit}` : ""
const startFragment = (options?.start != undefined) ? `AND id >= ${options?.start}` : ""
const query = `
SELECT * FROM order_tracking
WHERE finish_date IS NULL
${startFragment}
ORDER BY start_date ASC
${offsetFragment}
${limitFragment}
`
const values: string[] = []
const queryPromise = client.query<OrderTracking<T>>(query, values)
const result = await this.getAll(queryPromise)
client.release()
return result
}
public async createOrder<T extends any>(data: CreateOrderDTO): Promise<Result<string, OrderTracking<T>>> {
const client = await this.pgClient.connect();
await client.query("BEGIN")
const query = `
INSERT INTO order_tracking (
correlation_id,
exchange,
routing_key,
order_type,
payload,
status,
webhook_host,
webhook_endpoint
)
VALUES (
$1, -- correlation_id
$2, -- exchange
$3, -- routing_key
$4, -- order_type (ej: 'activate')
$5, -- payload (json object)
'pending',
$6, -- webhook_host,
$7 -- webhook_endpoint
)
RETURNING
id,
correlation_id,
exchange,
routing_key,
order_type,
payload,
status,
webhook_host,
webhook_endpoint
`
const values = [data.correlation_id, data.exchange, data.routing_key, data.order_type, data.payload, data.webhook_host, data.webhook_endpoint]
const queryPromise = client.query<OrderTracking<T>>(query, values)
// TODO comprobar si start_date convierte a Date por defecto, añadir enum de status
const result = await this.getFirst(queryPromise)
if (result.error == undefined) {
await client.query("COMMIT")
} else {
await client.query("ROLLBACK")
}
client.release()
return result
}
/**
* Actualizacion "correcta" del estado de un order
*/
public async updateOrder(args: UpdateOrderDTO): Promise<Result<string, OrderTracking<any>>> {
// XOR id o correlation_id
assert((args.id != undefined) != (args.correlation_id != undefined))
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 ${idType} = $1
`
const vCurrentOrder = [idValue]
const currentOrderResult = await this.getFirst(client.query<OrderTracking<any>>(qCurrentOrder, vCurrentOrder))
const orderId = currentOrderResult.data?.id
if (orderId == undefined) {
return {
error: "El order a actualizar no existe " + idType + ": " + idValue
}
}
if (currentOrderResult.error != undefined) {
await client.query("ROLLBACK")
client.release()
return currentOrderResult
}
const currentOrder = currentOrderResult.data!
// 2. Si todo ok se actualiza el order
const uOrderTracking = `
UPDATE order_tracking
SET
status = $2::order_status,
update_date = (now() at time zone 'utc')
WHERE id = $1
RETURNING id, status, update_date;
`
const vOrderTracking = [orderId, args.new_status]
const updatedOrderResult = await this.getFirst(
client.query<{ id: number, status: string, update_date: string }>(uOrderTracking, vOrderTracking)
)
if (updatedOrderResult.error != undefined) {
await client.query("ROLLBACK")
client.release()
return updatedOrderResult
}
// 3. Si todo ok se añade una entradad de order_history con los datos modificados
const iOrderHistory = `
INSERT INTO order_history (
order_id,
previous_status,
new_status,
change_reason
)
VALUES (
$1, -- ID de la orden
$2, -- Estado anterior
$3::order_status, -- Nuevo estado
$4 -- Razón (ej: "Consumer processed successfully" o "RabbitMQ NACK")
)
RETURNING id;
`
const vOrderHistory = [orderId, currentOrder.status, args.new_status, args.reason]
const newOrderHistoryResult = await this.getFirst(
client.query<{ id: number }>(iOrderHistory, vOrderHistory)
)
if (newOrderHistoryResult.error != undefined) {
await client.query("ROLLBACK")
client.release()
return newOrderHistoryResult
}
await client.query("COMMIT")
const updatedOrder = await this.getFirst(
client.query<OrderTracking<any>>(qCurrentOrder, vCurrentOrder)
)
client.release()
return updatedOrder
}
public async finishOrder(args: FinishOrderDTO) {
const client = await this.pgClient.connect();
assert((args.id != undefined) != (args.correlation_id != undefined))
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 ${idType} = $1
`
const vCurrentOrder = [idValue]
const currentOrderResult = await this.getFirst(client.query<OrderTracking<any>>(qCurrentOrder, vCurrentOrder))
const orderId = currentOrderResult.data?.id
if (orderId == undefined) {
return {
error: "El order a actualizar no existe " + idType + ": " + idValue
}
}
if (currentOrderResult.error != undefined) {
await client.query("ROLLBACK")
client.release()
return currentOrderResult
}
const currentOrder = currentOrderResult.data!
// 2. Si todo ok se actualiza el order
const uOrderTracking = `
UPDATE order_tracking
SET
status = 'finished',
update_date = (now() at time zone 'utc'),
finish_date = (now() at time zone 'utc')
WHERE id = $1
RETURNING id, status, update_date;
`
const vOrderTracking = [orderId]
const updatedOrderResult = await this.getFirst(
client.query<{ id: number, status: string, update_date: string }>(uOrderTracking, vOrderTracking)
)
if (updatedOrderResult.error != undefined) {
await client.query("ROLLBACK")
client.release()
return updatedOrderResult
}
// 3. Si todo ok se guardo un nuevo registro de history
const iOrderHistory = `
INSERT INTO order_history (
order_id,
previous_status,
new_status,
change_reason
)
VALUES (
$1, -- ID de la orden
$2, -- Estado anterior
'finished',
$3 -- Siempre "finished successfully" a no ser que se especifique otra razón
)
RETURNING id;
`
const vOrderHistory = [orderId, currentOrder.status, args.reason ?? "finished successfully"]
const newOrderHistoryResult = await this.getFirst(
client.query<{ id: number }>(iOrderHistory, vOrderHistory)
)
if (newOrderHistoryResult.error != undefined) {
await client.query("ROLLBACK")
client.release()
return newOrderHistoryResult
}
await client.query("COMMIT")
const updatedOrder = await this.getFirst(
client.query<OrderTracking<any>>(qCurrentOrder, vCurrentOrder)
)
client.release()
return updatedOrder
}
// TODO: tema de poder filtrar por correlation_id
public async errorOrder(args: {
id: number,
status: "failed" | "dlx",
reason: string,
error?: string,
stackTrace?: string
}) {
const client = await this.pgClient.connect();
await client.query('BEGIN');
// 1. Se consulta la order de base
const qCurrentOrder = `
SELECT * FROM order_tracking
WHERE id = $1
`
const vCurrentOrder = [args.id]
const currentOrderResult = await this.getFirst(client.query<OrderTracking<any>>(qCurrentOrder, vCurrentOrder))
if (currentOrderResult.error != undefined) {
await client.query("ROLLBACK")
client.release()
return currentOrderResult
}
const currentOrder = currentOrderResult.data!
// 3. Si todo ok se actualiza el order
// Si el status es dlx se asume que ha terminado y no va a reintentarse
// Si es failed se asume que se ha movido a la cola de delay y en algún momento se va a reintentar
const uOrderTracking = `
UPDATE order_tracking
SET
status = $2::order_status,
update_date = (now() at time zone 'utc'),
finish_date = CASE WHEN $2::order_status = 'dlx' THEN (now() at time zone 'utc') ELSE null END,
retry_count = retry_count + 1,
error_message = $3,
error_stacktrace = $4
WHERE id = $1
RETURNING id, status, update_date;
`
const vOrderTracking = [args.id, args.status, args.error, args.stackTrace]
const updatedOrderResult = await this.getFirst(
client.query<{ id: number, status: string, update_date: string }>(uOrderTracking, vOrderTracking)
)
if (updatedOrderResult.error != undefined) {
await client.query("ROLLBACK")
client.release()
return updatedOrderResult
}
// 3. Si todo ok se guardo un nuevo registro de history
const iOrderHistory = `
INSERT INTO order_history (
order_id,
previous_status,
new_status,
change_reason
)
VALUES (
$1, -- ID de la orden
$2, -- Estado anterior
$3::order_status, -- En este caso particular 'dlx' o 'failed'
$4 -- En este caso el motivo de fallo completo
)
RETURNING id;
`
const vOrderHistory = [args.id, currentOrder.status, args.status, args.reason]
const newOrderHistoryResult = await this.getFirst(
client.query<{ id: number }>(iOrderHistory, vOrderHistory)
)
if (newOrderHistoryResult.error != undefined) {
await client.query("ROLLBACK")
client.release()
return newOrderHistoryResult
}
await client.query("COMMIT")
const updatedOrder = await this.getFirst(
client.query<OrderTracking<any>>(qCurrentOrder, vCurrentOrder)
)
client.release()
return updatedOrder
}
}

View File

@@ -17,6 +17,11 @@ export class RabbitMQEventBus implements EventBus {
private buildStructure?: (chan: Channel) => Promise<void>
private maxRetry: number = 0
connection?: AmqpConnectionManager
channel?: ChannelWrapper
connected: Boolean = false
private connectionOptions: RMQConnectionParams
constructor(args: {
connectionParams: RMQConnectionParams,
buildStructure?: (chan: Channel) => Promise<void>,
@@ -73,11 +78,6 @@ export class RabbitMQEventBus implements EventBus {
//return this.channel.nack(msg, false, requeue)
}
connection?: AmqpConnectionManager
channel?: ChannelWrapper
connected: Boolean = false
private connectionOptions: RMQConnectionParams
public async connect() {
@@ -96,28 +96,39 @@ export class RabbitMQEventBus implements EventBus {
} catch (e) {
console.error("[RMQ] Error estableciendo la conexion con el servidor", e)
}
}
publish(events: DomainEvent[]): Promise<void> {
return new Promise((res, rej) => {
publish(events: DomainEvent[]): Promise<{ success: DomainEvent[], error: DomainEvent[] }> {
return new Promise(async (res, rej) => {
const successEvents: DomainEvent[] = []
const errorEvents: DomainEvent[] = []
try {
for (const event of events) {
const exchange = "sim.exchange"
const routingKey = event.key
const content = Buffer.from(JSON.stringify(event))
this.channel?.publish(exchange, routingKey, content, {}, (err, ok) => {
const isPublished = await this.channel?.publish(exchange, routingKey, content, {
headers: {
...event.headers
}
}, (err, ok) => {
if (err == undefined) {
console.log("Evento publicado ", event)
} else {
console.error("Error publicando", event)
}
})
// Hay que revisarlo pero en principio la libreria se encarga que el mensaje se publique
// si o si
successEvents.push(event)
}
return res()
return res({
success: successEvents,
error: errorEvents
})
} catch (err) {
return rej(err)
}
@@ -161,7 +172,8 @@ export class RabbitMQEventBus implements EventBus {
if (this.connection == undefined) throw new Error("[RMQ] Intentando crear un canal sin una conexion")
const channel = this.connection.createChannel({
setup: async (channel: Channel) => {
confirm: true,
setup: async (channel: ConfirmChannel) => {
// Exchanges comunes a todos
channel.assertExchange("sim.exchange", "topic", { durable: true })
channel.assertExchange("sim.dlx", "topic", { durable: true })
@@ -195,6 +207,6 @@ export class RabbitMQEventBus implements EventBus {
Promise.reject(error);
});
return channel as ChannelWrapper;
return channel;
}
}

View File

@@ -4,6 +4,14 @@
"version": "1.0.0",
"description": "",
"exports": {
"./aplication/*.js": {
"types": "./aplication/*.ts",
"default": "./aplication/*.js"
},
"./aplication/*": {
"types": "./aplication/*.ts",
"default": "./aplication/*.js"
},
"./infrastructure/*.js": {
"types": "./infrastructure/*.ts",
"default": "./infrastructure/*.js"
@@ -30,7 +38,7 @@
}
},
"scripts": {
"test": "echo \"Error: no test specified\" ",
"test": "node --import tsx --test ./**/*.test.ts",
"dev": "echo \" Shared no es un modulo ejecutable \" ",
"build": "tsc --build && tsc-alias -p tsconfig.json && cp package.json ../../dist/packages/sim-shared/"
},

View File

@@ -1,4 +1,3 @@
#/bin/bash
rm deployment/database/init.sql
cat deployment/database/*.sql >deployment/database/init.sql
docker compose -f deployment/local/docker/docker-compose.yaml --project-directory ./ up --watch

582
yarn.lock

File diff suppressed because it is too large Load Diff