Compare commits
42 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 39c0e87758 | |||
| 5771972e2a | |||
| ea13403dc3 | |||
| 8d9a9b84b8 | |||
| 9b92f3506b | |||
| 1798118f6b | |||
| eba2b8c569 | |||
| b6b2cf6cc8 | |||
| a0faa2d105 | |||
| d323f804fc | |||
| 978454754c | |||
| b6091b15da | |||
| a6794a061b | |||
| fafea3ce04 | |||
| 992f639f35 | |||
| f57309b06a | |||
| 3be2b8f20d | |||
| 4853fec7ff | |||
| 04a6e50b7a | |||
| 8ca3d095e6 | |||
| ca1144b55c | |||
| 18422fbe38 | |||
| f221035c8b | |||
| 02c80cd503 | |||
| c416114c50 | |||
| e329b36933 | |||
| 5c64c84e2a | |||
| fc319372be | |||
| 12dae135b5 | |||
| b208c9c301 | |||
| 1583ae539e | |||
| b6ec37c339 | |||
| 459523666f | |||
| 8427613114 | |||
| 5d3465fd97 | |||
| 39a2622cb1 | |||
| 0a42e4776d | |||
| 44fea21a56 | |||
| 46ac54f7ab | |||
| 2c9bf9dd93 | |||
| 19b2958a9c | |||
| a39b84e107 |
14
.env
14
.env
@@ -1,4 +1,5 @@
|
|||||||
PORT=3000
|
PORT=3000
|
||||||
|
API_HOSTNAME=0.0.0.0
|
||||||
RABBITMQ_USER=guest
|
RABBITMQ_USER=guest
|
||||||
RABBITMQ_PASSWORD=guest
|
RABBITMQ_PASSWORD=guest
|
||||||
|
|
||||||
@@ -13,11 +14,16 @@ RABBITMQ_SECURE=false
|
|||||||
RABBITMQ_VHOST=sim-vhost
|
RABBITMQ_VHOST=sim-vhost
|
||||||
|
|
||||||
# Hay cosas que unificar de varios servicios
|
# Hay cosas que unificar de varios servicios
|
||||||
POSTGRES_DB=postgres
|
|
||||||
POSTGRES_DATABASE=postgres
|
|
||||||
#POSTGRES_HOST=postgresql-sim
|
#POSTGRES_HOST=postgresql-sim
|
||||||
POSTGRES_HOST=localhost
|
POSTGRES_HOST=localhost
|
||||||
POSTGRES_PORT=5432
|
POSTGRES_DB=postgres
|
||||||
DEV_POSTGRES_PORT=5432
|
POSTGRES_DATABASE=postgres
|
||||||
|
POSTGRES_PORT=5433
|
||||||
POSTGRES_USER=postgres
|
POSTGRES_USER=postgres
|
||||||
POSTGRES_PASSWORD=1234
|
POSTGRES_PASSWORD=1234
|
||||||
|
|
||||||
|
# Para el postgres local para generar el script de resultado de migraciones
|
||||||
|
PGHOST=localhost
|
||||||
|
PGUSER=alvar
|
||||||
|
PGPASSWORD=alvar
|
||||||
|
PGPORT=5433
|
||||||
|
|||||||
@@ -1,4 +1,10 @@
|
|||||||
#/bin/bash
|
#/bin/bash
|
||||||
rm deployment/database/init.sql
|
rm deployment/database/init.sql
|
||||||
cat deployment/database/*.sql >deployment/database/init.sql
|
# cat deployment/database/*.sql >deployment/database/init.sql
|
||||||
|
cp deployment/database/esquema_final* deployment/database/init.sql
|
||||||
|
|
||||||
|
# compatibilidad con postgresql < 17
|
||||||
|
sed -i '/\\restrict/d' deployment/database/init.sql
|
||||||
|
sed -i '/\\unrestrict/d' deployment/database/init.sql
|
||||||
|
|
||||||
docker compose -f deployment/local/docker/docker-compose.yaml --project-directory ./ build
|
docker compose -f deployment/local/docker/docker-compose.yaml --project-directory ./ build
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
# stage base para coordinar las fases de build y ejecucion
|
# stage base para coordinar las fases de build y ejecucion
|
||||||
FROM node:22-alpine AS base
|
FROM node:22-alpine AS base
|
||||||
WORKDIR /usr/local/app
|
WORKDIR /usr/local/app
|
||||||
COPY ./package.json ./yarn.lock ./
|
COPY ./package.json ./
|
||||||
|
#COPY ./package.json ./yarn.lock ./
|
||||||
RUN corepack enable && \
|
RUN corepack enable && \
|
||||||
corepack prepare yarn@4.12.0 --activate
|
corepack prepare yarn@4.12.0 --activate
|
||||||
# copia el codigo en general
|
# copia el codigo en general
|
||||||
|
|||||||
@@ -4,16 +4,18 @@ CREATE TYPE status_enum AS ENUM ('noRequestID','noMassID','running','finished','
|
|||||||
-- Tabla para gestionar las peticiones de cambio de objenious.
|
-- Tabla para gestionar las peticiones de cambio de objenious.
|
||||||
-- Para una o mas lineas se pueden lanzar operacione que no sabemos
|
-- Para una o mas lineas se pueden lanzar operacione que no sabemos
|
||||||
-- con certeza cuando van a terminar.
|
-- 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 (
|
CREATE TABLE if not exists objenious_operation (
|
||||||
id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
|
id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
|
||||||
retry_count INT DEFAULT 0,
|
retry_count INT DEFAULT 0, -- No implementado en codigo
|
||||||
max_retry INT DEFAULT 5,
|
max_retry INT DEFAULT 5, -- No implementado en codigo
|
||||||
max_date_retry TIMESTAMP DEFAULT NULL,
|
max_date_retry TIMESTAMP DEFAULT NULL, -- No implementado en codigo
|
||||||
iccids TEXT,
|
iccids TEXT,
|
||||||
request_id TEXT,
|
request_id TEXT,
|
||||||
mass_action_id TEXT,
|
mass_action_id TEXT,
|
||||||
operation TEXT NOT NULL,
|
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(),
|
last_change_date TIMESTAMP NOT NULL DEFAULT now(),
|
||||||
end_date TIMESTAMP,
|
end_date TIMESTAMP,
|
||||||
error TEXT,
|
error TEXT,
|
||||||
@@ -24,7 +26,7 @@ CREATE TABLE if not exists objenious_operation (
|
|||||||
-- operaciones pendientes para revisar
|
-- operaciones pendientes para revisar
|
||||||
CREATE INDEX IF NOT EXISTS pending_operations
|
CREATE INDEX IF NOT EXISTS pending_operations
|
||||||
ON objenious_operation(start_date)
|
ON objenious_operation(start_date)
|
||||||
WHERE end_date IS NULL;
|
WHERE end_date IS NULL;
|
||||||
|
|
||||||
CREATE TABLE if not exists objenious_operation_change (
|
CREATE TABLE if not exists objenious_operation_change (
|
||||||
id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
|
id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
|
||||||
106
deployment/database/generateSchema.sh
Executable file
106
deployment/database/generateSchema.sh
Executable 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"
|
||||||
@@ -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);
|
|
||||||
|
|||||||
48
deployment/database/migrations/0.1.0_objenious.sql
Normal file
48
deployment/database/migrations/0.1.0_objenious.sql
Normal 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);
|
||||||
67
deployment/database/migrations/1.0.0_orders.sql
Normal file
67
deployment/database/migrations/1.0.0_orders.sql
Normal 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);
|
||||||
|
|
||||||
12
deployment/database/migrations/1.0.1_utc.sql
Normal file
12
deployment/database/migrations/1.0.1_utc.sql
Normal 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');
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
30
deployment/database/migrations/1.0.2_timezones.sql
Normal file
30
deployment/database/migrations/1.0.2_timezones.sql
Normal 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();
|
||||||
|
|
||||||
10
deployment/database/migrations/1.1.0_webhook-order.sql
Normal file
10
deployment/database/migrations/1.1.0_webhook-order.sql
Normal 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;
|
||||||
@@ -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;
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
# --- Release image ---
|
# --- Release image ---
|
||||||
FROM node:22-alpine AS release
|
FROM node:22-alpine AS release
|
||||||
|
RUN apk --no-cache add git
|
||||||
WORKDIR /home/node/app
|
WORKDIR /home/node/app
|
||||||
|
|
||||||
RUN corepack enable
|
RUN corepack enable
|
||||||
@@ -11,7 +12,7 @@ COPY ./package.json ./
|
|||||||
# Force node-modules linker (no .yarnrc.yml in build context)
|
# Force node-modules linker (no .yarnrc.yml in build context)
|
||||||
RUN echo 'nodeLinker: node-modules' > .yarnrc.yml
|
RUN echo 'nodeLinker: node-modules' > .yarnrc.yml
|
||||||
|
|
||||||
RUN yarn install
|
RUN yarn install
|
||||||
|
|
||||||
RUN mkdir -p dist && ln -sf ../packages dist/packages
|
RUN mkdir -p dist && ln -sf ../packages dist/packages
|
||||||
|
|
||||||
@@ -19,4 +20,5 @@ COPY ./entrypoint.sh ./
|
|||||||
RUN chmod +x entrypoint.sh
|
RUN chmod +x entrypoint.sh
|
||||||
|
|
||||||
EXPOSE ${PORT:-3000}
|
EXPOSE ${PORT:-3000}
|
||||||
|
|
||||||
ENTRYPOINT ["./entrypoint.sh"]
|
ENTRYPOINT ["./entrypoint.sh"]
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ pipeline {
|
|||||||
sh 'npm install -g yarn'
|
sh 'npm install -g yarn'
|
||||||
sh 'corepack enable'
|
sh 'corepack enable'
|
||||||
sh 'corepack prepare yarn@4.12.0 --activate'
|
sh 'corepack prepare yarn@4.12.0 --activate'
|
||||||
sh 'yarn install --immutable'
|
sh 'yarn install'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
stage("🧱 Building") {
|
stage("🧱 Building") {
|
||||||
|
|||||||
27
deployment/local/docker/Dockerfile.dev
Normal file
27
deployment/local/docker/Dockerfile.dev
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
# 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 ./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" ]
|
||||||
|
|
||||||
|
|
||||||
@@ -24,14 +24,15 @@ services:
|
|||||||
RABBITMQ_DEFAULT_PASS: ${RABBITMQ_PASSWORD}
|
RABBITMQ_DEFAULT_PASS: ${RABBITMQ_PASSWORD}
|
||||||
volumes:
|
volumes:
|
||||||
- ./rabbitmq_plugins/enabled_plugins:/etc/rabbitmq/enabled_plugins:ro
|
- ./rabbitmq_plugins/enabled_plugins:/etc/rabbitmq/enabled_plugins:ro
|
||||||
- ./deployment/rabbit/rabbitmq.conf:/etc/rabbitmq/rabbitmq.conf:ro
|
- ./deployment/local/rabbit/rabbitmq.conf:/etc/rabbitmq/rabbitmq.conf:ro
|
||||||
- ./deployment/rabbit/definitions.json:/etc/rabbitmq/definitions.json:ro
|
- ./deployment/local/rabbit/definitions.json:/etc/rabbitmq/definitions.json:ro
|
||||||
|
|
||||||
sim-gateway:
|
sf-sims-api:
|
||||||
container_name: sim-gateway
|
container_name: sf-sims-api
|
||||||
|
image: sf-sims-api
|
||||||
build:
|
build:
|
||||||
context: ./
|
context: ./
|
||||||
dockerfile: deployment/Dockerfile.dev
|
dockerfile: deployment/local/docker/Dockerfile.dev
|
||||||
args:
|
args:
|
||||||
PORT: "${PORT:-3000}"
|
PORT: "${PORT:-3000}"
|
||||||
develop:
|
develop:
|
||||||
@@ -46,16 +47,29 @@ services:
|
|||||||
env_file:
|
env_file:
|
||||||
- .env
|
- .env
|
||||||
restart: unless-stopped
|
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:
|
depends_on:
|
||||||
rabbitmq-sim-broker:
|
rabbitmq-sim-broker:
|
||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
|
postgresql-sim:
|
||||||
|
condition: service_healthy
|
||||||
|
|
||||||
postgresql-sim:
|
postgresql-sim:
|
||||||
|
container_name: postgresql-sim
|
||||||
image: postgres:16.1
|
image: postgres:16.1
|
||||||
env_file:
|
env_file:
|
||||||
- .env
|
- .env
|
||||||
ports:
|
ports:
|
||||||
- "5432:${DEV_POSTGRES_PORT}"
|
- "${POSTGRES_PORT}:${POSTGRES_PORT}"
|
||||||
volumes:
|
volumes:
|
||||||
- ./sql-data/:/var/lib/postgres/data
|
- ./sql-data/:/var/lib/postgres/data
|
||||||
- ./deployment/database/init.sql:/docker-entrypoint-initdb.d/init.sql
|
- ./deployment/database/init.sql:/docker-entrypoint-initdb.d/init.sql
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
#!/bin/bash
|
#!/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 stop sf-sims-api || true
|
||||||
docker rm sf-shopify-orders-api || true
|
docker rm sf-sims-api || true
|
||||||
docker rmi sf-shopify-orders-api || true
|
docker rmi sf-sims-api || true
|
||||||
|
|
||||||
docker compose -f docker-compose.yaml up --build -d
|
docker compose -f docker-compose.yaml up --build -d
|
||||||
|
|||||||
3
deployment/local/docker/start.sh
Normal file
3
deployment/local/docker/start.sh
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
echo "Lanzando migraciones e iniciando servidor"
|
||||||
|
yarn migrate && yarn start
|
||||||
@@ -1,6 +1,3 @@
|
|||||||
default_user = guest
|
|
||||||
default_pass = guest
|
|
||||||
|
|
||||||
listeners.tcp.default = 5672
|
listeners.tcp.default = 5672
|
||||||
management.tcp.port = 15672
|
management.tcp.port = 15672
|
||||||
|
|
||||||
@@ -11,7 +11,7 @@ post {
|
|||||||
}
|
}
|
||||||
|
|
||||||
body:form-urlencoded {
|
body:form-urlencoded {
|
||||||
iccid: 8933201125065160406
|
iccid: 8933201125068886692
|
||||||
offer: SAVEFAMILY1
|
offer: SAVEFAMILY1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
16
docs/sim-api/Activation Email Health.bru
Normal file
16
docs/sim-api/Activation Email Health.bru
Normal 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
|
||||||
|
}
|
||||||
42
docs/sim-api/Activation Email.bru
Normal file
42
docs/sim-api/Activation Email.bru
Normal 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
|
||||||
|
}
|
||||||
@@ -11,10 +11,45 @@ post {
|
|||||||
}
|
}
|
||||||
|
|
||||||
body:form-urlencoded {
|
body:form-urlencoded {
|
||||||
iccid: 8933201124059176320
|
iccid: 8933201125068886692
|
||||||
}
|
}
|
||||||
|
|
||||||
settings {
|
settings {
|
||||||
encodeUrl: true
|
encodeUrl: true
|
||||||
timeout: 0
|
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/Get pending orders.bru
Normal file
16
docs/sim-api/Get pending orders.bru
Normal 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
|
||||||
|
}
|
||||||
16
docs/sim-api/Order by id.bru
Normal file
16
docs/sim-api/Order by id.bru
Normal 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
|
||||||
|
}
|
||||||
20
docs/sim-api/Orders by message_id.bru
Normal file
20
docs/sim-api/Orders by message_id.bru
Normal 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
|
||||||
|
}
|
||||||
21
docs/sim-api/Test Order.bru
Normal file
21
docs/sim-api/Test Order.bru
Normal 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
|
||||||
|
}
|
||||||
34
docs/sim-api/collection.bru
Normal file
34
docs/sim-api/collection.bru
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
}
|
||||||
38
docs/sim-objenious/Alarmas disponibles.bru
Normal file
38
docs/sim-objenious/Alarmas disponibles.bru
Normal 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
|
||||||
|
}
|
||||||
@@ -37,7 +37,7 @@ body:form-urlencoded {
|
|||||||
}
|
}
|
||||||
|
|
||||||
vars:pre-request {
|
vars:pre-request {
|
||||||
params.id: 14111
|
params.id: 14557
|
||||||
}
|
}
|
||||||
|
|
||||||
settings {
|
settings {
|
||||||
|
|||||||
1843
package-lock.json
generated
1843
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "sim-eventos",
|
"name": "sim-eventos",
|
||||||
|
"version": "1.0.0",
|
||||||
"packageManager": "yarn@4.12.0",
|
"packageManager": "yarn@4.12.0",
|
||||||
"workspaces": [
|
"workspaces": [
|
||||||
"packages/*"
|
"packages/*"
|
||||||
@@ -14,7 +15,8 @@
|
|||||||
"lint": "eslint .",
|
"lint": "eslint .",
|
||||||
"lint:fix": "eslint --fix .",
|
"lint:fix": "eslint --fix .",
|
||||||
"format": "prettier --write .",
|
"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": {
|
"dependencies": {
|
||||||
"@tsconfig/node22": "^22.0.5",
|
"@tsconfig/node22": "^22.0.5",
|
||||||
@@ -22,10 +24,12 @@
|
|||||||
"amqplib": "^0.10.9",
|
"amqplib": "^0.10.9",
|
||||||
"axios": "^1.13.3",
|
"axios": "^1.13.3",
|
||||||
"cors": "^2.8.5",
|
"cors": "^2.8.5",
|
||||||
|
"db-migrate": "http://gitea:3000/alvarsanmartin/herramienta-migracion.git",
|
||||||
"dotenv": "^17.2.3",
|
"dotenv": "^17.2.3",
|
||||||
"express": "^5.2.1",
|
"express": "^5.2.1",
|
||||||
"pg": "^8.18.0",
|
"pg": "^8.18.0",
|
||||||
"typescript": "^5.9.3",
|
"typescript": "^5.9.3",
|
||||||
|
"uuidv7": "^1.1.0",
|
||||||
"vite": "^7.3.1",
|
"vite": "^7.3.1",
|
||||||
"vite-tsconfig-paths": "^6.0.5"
|
"vite-tsconfig-paths": "^6.0.5"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
console.log("Template")
|
console.log(new Date().toISOString())
|
||||||
|
|
||||||
export default {}
|
export default {}
|
||||||
|
|||||||
@@ -3,20 +3,3 @@ RABBITMQ_USER=guest
|
|||||||
RABBITMQ_PASSWORD=guest
|
RABBITMQ_PASSWORD=guest
|
||||||
|
|
||||||
ENVIORMENT=development
|
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
|
|
||||||
|
|||||||
@@ -5,4 +5,6 @@ OBJ_CLI_ASSERTION=XOc7FtwXD8hUX2SFVX94XSty8wkOmChkwDNF09O_aIxPubMDdFUdCDCB4zpzSI
|
|||||||
OBJ_CLIENT_ID=savefamily_rest_ws
|
OBJ_CLIENT_ID=savefamily_rest_ws
|
||||||
OBJ_KID=xNfbMiyL1ORXGP8lElhcv8nVaG3EJKye4Lc1YoN3I1E
|
OBJ_KID=xNfbMiyL1ORXGP8lElhcv8nVaG3EJKye4Lc1YoN3I1E
|
||||||
OBJ_BASE_URL=https://api-getway.objenious.com/ws
|
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
|
//OBJ_BASE_URL=https://api-getway.objenious.com/ws/test
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { ConsumeMessage } from "amqplib";
|
|||||||
import { SimUseCases } from "./Sim.usecases.js";
|
import { SimUseCases } from "./Sim.usecases.js";
|
||||||
import { SimEvents } from "sim-shared/domain/SimEvents.js";
|
import { SimEvents } from "sim-shared/domain/SimEvents.js";
|
||||||
import { Result } from "sim-shared/domain/Result.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
|
* La clase usa generadores de funciones para mantener el contexto
|
||||||
@@ -64,6 +65,8 @@ export class SimController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public activate() {
|
public activate() {
|
||||||
|
const DUE_DATE_SECONDS = 2 * 60
|
||||||
|
|
||||||
return async (msg: ConsumeMessage) => {
|
return async (msg: ConsumeMessage) => {
|
||||||
let msgData;
|
let msgData;
|
||||||
try {
|
try {
|
||||||
@@ -80,9 +83,10 @@ export class SimController {
|
|||||||
throw new Error("Error activando la sim, no se ha especificado la oferta")
|
throw new Error("Error activando la sim, no se ha especificado la oferta")
|
||||||
}
|
}
|
||||||
|
|
||||||
this.tryUseCase(msg, this.useCases.activate({
|
const resp = await this.tryUseCase(msg, this.useCases.activate({
|
||||||
dueDate: this.genDueDate(2 * 60).toISOString(),
|
correlation_id: msgData.headers?.message_id,
|
||||||
customerAccountCode: "9.49411.10", // TODO: Al .env
|
dueDate: this.genDueDate(DUE_DATE_SECONDS).toISOString(),
|
||||||
|
customerAccountCode: env.OBJ_CUSTOMER_CODE,
|
||||||
identifier: {
|
identifier: {
|
||||||
identifierType: "ICCID",
|
identifierType: "ICCID",
|
||||||
identifiers: [iccid]
|
identifiers: [iccid]
|
||||||
@@ -92,6 +96,11 @@ export class SimController {
|
|||||||
services: []
|
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
|
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(),
|
dueDate: this.genDueDate(2 * 60).toISOString(),
|
||||||
identifier: {
|
identifier: {
|
||||||
identifierType: "ICCID",
|
identifierType: "ICCID",
|
||||||
@@ -135,7 +145,8 @@ export class SimController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const iccid = msgData.payload.iccid
|
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(),
|
dueDate: this.genDueDate(2 * 60).toISOString(),
|
||||||
identifier: {
|
identifier: {
|
||||||
identifierType: "ICCID",
|
identifierType: "ICCID",
|
||||||
@@ -160,7 +171,8 @@ export class SimController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const iccid = msgData.payload.iccid
|
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(),
|
dueDate: this.genDueDate(2 * 60).toISOString(),
|
||||||
identifier: {
|
identifier: {
|
||||||
identifierType: "ICCID",
|
identifierType: "ICCID",
|
||||||
@@ -184,8 +196,9 @@ export class SimController {
|
|||||||
return Promise.reject("Mensaje invalido")
|
return Promise.reject("Mensaje invalido")
|
||||||
}
|
}
|
||||||
const iccid = msgData.payload.iccid
|
const iccid = msgData.payload.iccid
|
||||||
console.log("Mensaje procesado", String(msgData))
|
console.log("Mensaje procesado", msgData)
|
||||||
this.tryUseCase(msg, this.useCases.terminate({
|
const res = await this.tryUseCase(msg, this.useCases.terminate({
|
||||||
|
correlation_id: msgData.headers?.message_id,
|
||||||
dueDate: this.genDueDate(2 * 60).toISOString(),
|
dueDate: this.genDueDate(2 * 60).toISOString(),
|
||||||
identifier: {
|
identifier: {
|
||||||
identifierType: "ICCID",
|
identifierType: "ICCID",
|
||||||
@@ -213,3 +226,4 @@ export class SimController {
|
|||||||
return dueDate
|
return dueDate
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ export class SimRouter {
|
|||||||
this.routes = new Map([
|
this.routes = new Map([
|
||||||
["activate", this.simController.activate()],
|
["activate", this.simController.activate()],
|
||||||
["pause", this.simController.suspend()],
|
["pause", this.simController.suspend()],
|
||||||
["cancel", this.simController.terminate()], // terminate
|
["cancel", this.simController.terminate()],
|
||||||
["reActivate", this.simController.reActivate()],
|
["reActivate", this.simController.reActivate()],
|
||||||
["preActivate", this.simController.preActivate()]
|
["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
|
* Enruta el mensaje a la acción correspondiente basándose en la routing key
|
||||||
* TODO: No estoy seguro que deba meter el nack aqui
|
* 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> => {
|
public route = async (msg: ConsumeMessage | null): Promise<void> => {
|
||||||
if (!msg) {
|
if (!msg) {
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ import { HttpClient } from "sim-shared/infrastructure/HTTPClient.js"
|
|||||||
import { AxiosError } from "axios"
|
import { AxiosError } from "axios"
|
||||||
import { Result } from "sim-shared/domain/Result.js"
|
import { Result } from "sim-shared/domain/Result.js"
|
||||||
import { ObjeniousOperation, IOperationsRepository as OperationsRepositoryPort } from "sim-shared/domain/operationsRepository.port.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:
|
// TODO:
|
||||||
// - Pasar a un archivo de DTOs
|
// - Pasar a un archivo de DTOs
|
||||||
@@ -11,12 +13,16 @@ import { ObjeniousOperation, IOperationsRepository as OperationsRepositoryPort }
|
|||||||
export class SimUseCases {
|
export class SimUseCases {
|
||||||
private readonly httpClient: HttpClient
|
private readonly httpClient: HttpClient
|
||||||
private readonly operationRepository: OperationsRepositoryPort
|
private readonly operationRepository: OperationsRepositoryPort
|
||||||
|
private readonly orderRepository: OrderRepository
|
||||||
|
|
||||||
constructor(args: {
|
constructor(args: {
|
||||||
httpClient: HttpClient,
|
httpClient: HttpClient,
|
||||||
operationRepository: OperationsRepositoryPort
|
operationRepository: OperationsRepositoryPort,
|
||||||
|
orderRepository: OrderRepository
|
||||||
}) {
|
}) {
|
||||||
this.httpClient = args.httpClient
|
this.httpClient = args.httpClient
|
||||||
this.operationRepository = args.operationRepository
|
this.operationRepository = args.operationRepository
|
||||||
|
this.orderRepository = args.orderRepository
|
||||||
}
|
}
|
||||||
|
|
||||||
private async logOperation(data: ObjeniousOperation) {
|
private async logOperation(data: ObjeniousOperation) {
|
||||||
@@ -25,6 +31,81 @@ 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>> {
|
public activate(activationData: ActivationData): () => Promise<Result<string, boolean>> {
|
||||||
const OPERATION_URL = "/actions/activateLine"
|
const OPERATION_URL = "/actions/activateLine"
|
||||||
return async () => {
|
return async () => {
|
||||||
@@ -51,8 +132,6 @@ export class SimUseCases {
|
|||||||
error: undefined,
|
error: undefined,
|
||||||
data: true
|
data: true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
// muy mejorable el control de errores
|
// muy mejorable el control de errores
|
||||||
return {
|
return {
|
||||||
@@ -82,6 +161,7 @@ export class SimUseCases {
|
|||||||
if (resp.status == 200) {
|
if (resp.status == 200) {
|
||||||
console.log("Sim preactivada con exito", resp.data)
|
console.log("Sim preactivada con exito", resp.data)
|
||||||
const operation: ObjeniousOperation = {
|
const operation: ObjeniousOperation = {
|
||||||
|
correlation_id: preActivateData.correlation_id,
|
||||||
operation: "preActivate",
|
operation: "preActivate",
|
||||||
iccids: String(preActivateData.identifier.identifiers),
|
iccids: String(preActivateData.identifier.identifiers),
|
||||||
status: "noMassID",
|
status: "noMassID",
|
||||||
@@ -96,14 +176,14 @@ export class SimUseCases {
|
|||||||
} else {
|
} else {
|
||||||
return <Result<string, boolean>>{
|
return <Result<string, boolean>>{
|
||||||
error: String(resp.status),
|
error: String(resp.status),
|
||||||
data: true
|
data: undefined
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error preactivacion", preActivateData)
|
console.error("Error preactivacion", preActivateData)
|
||||||
return <Result<string, boolean>>{
|
return <Result<string, boolean>>{
|
||||||
error: "Error preactivando la sim" + preActivateData.identifier,
|
error: "Error preactivando la sim" + preActivateData.identifier,
|
||||||
data: true
|
data: undefined
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -117,17 +197,25 @@ export class SimUseCases {
|
|||||||
})
|
})
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const e = await req
|
const response = await req
|
||||||
console.log("Sim reactivada con exito", e.data)
|
|
||||||
return <Result<string, boolean>>{
|
if (response.status == 200) {
|
||||||
error: undefined,
|
console.log("[o] Sim solicitud de reactivacion ", response.data)
|
||||||
data: true
|
return <Result<string, boolean>>{
|
||||||
|
error: undefined,
|
||||||
|
data: true
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return {
|
||||||
|
error: String(response.status),
|
||||||
|
data: undefined
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error reactivacion", error)
|
console.error("[x] Error reactivacion", (error as AxiosError).response?.status)
|
||||||
return <Result<string, boolean>>{
|
return <Result<string, boolean>>{
|
||||||
error: "Error reactivando la sim" + pauseData.identifier,
|
error: "Error reactivando la sim" + pauseData.identifier,
|
||||||
data: true
|
data: undefined
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -135,53 +223,24 @@ export class SimUseCases {
|
|||||||
|
|
||||||
public suspend(suspendData: ActionData): () => Promise<Result<string, boolean>> {
|
public suspend(suspendData: ActionData): () => Promise<Result<string, boolean>> {
|
||||||
const OPERATION_URL = "/actions/suspendLine"
|
const OPERATION_URL = "/actions/suspendLine"
|
||||||
return async () => {
|
return this.generateUseCase({
|
||||||
const req = this.httpClient.client.post(OPERATION_URL, {
|
correlation_id: suspendData.correlation_id,
|
||||||
...suspendData
|
operationPayload: suspendData,
|
||||||
})
|
url: OPERATION_URL,
|
||||||
|
iccid: suspendData.identifier.identifiers[0], //
|
||||||
try {
|
operation: "suspend"
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public terminate(terminationData: ActionData): () => Promise<Result<string, boolean>> {
|
public terminate(terminationData: ActionData): () => Promise<Result<string, boolean>> {
|
||||||
const OPERATION_URL = "/actions/terminateLine"
|
const OPERATION_URL = "/actions/terminateLine"
|
||||||
return async () => {
|
return this.generateUseCase({
|
||||||
const req = this.httpClient.client.post(OPERATION_URL, {
|
correlation_id: terminationData.correlation_id,
|
||||||
...terminationData
|
operationPayload: terminationData,
|
||||||
})
|
url: OPERATION_URL,
|
||||||
|
iccid: terminationData.identifier.identifiers[0], //
|
||||||
// TODO: para cuando estemos listos.
|
operation: "terminate"
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ export const env = {
|
|||||||
OBJ_CLI_ASSERTION: String(process.env.OBJ_CLI_ASSERTION),
|
OBJ_CLI_ASSERTION: String(process.env.OBJ_CLI_ASSERTION),
|
||||||
OBJ_CLIENT_ID: String(process.env.OBJ_CLIENT_ID),
|
OBJ_CLIENT_ID: String(process.env.OBJ_CLIENT_ID),
|
||||||
OBJ_KID: String(process.env.OBJ_KID),
|
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)
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
|
|
||||||
export type ActionData = {
|
export type ActionData = {
|
||||||
|
correlation_id?: string;
|
||||||
dueDate: string, // isodate
|
dueDate: string, // isodate
|
||||||
filter?: {} // no se si hace falta
|
filter?: {} // no se si hace falta
|
||||||
identifier: {
|
identifier: {
|
||||||
|
|||||||
@@ -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 { startRMQClient } from "#config/eventBus.config.js"
|
||||||
import { httpInstance } from "#config/httpClient.config.js"
|
import { httpInstance } from "#config/httpClient.config.js"
|
||||||
import { pgPool } from "#config/postgreConfig.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 { SimUseCases } from "./aplication/Sim.usecases.js"
|
||||||
import { SimController } from "./aplication/Sim.controller.js"
|
import { SimController } from "./aplication/Sim.controller.js"
|
||||||
import { SimRouter } from "./aplication/Sim.router.js"
|
import { SimRouter } from "./aplication/Sim.router.js"
|
||||||
|
import { OrderRepository } from "sim-shared/infrastructure/OrderRepository.js"
|
||||||
|
|
||||||
async function startWorker() {
|
async function startWorker() {
|
||||||
const rmqClient = await startRMQClient()
|
const rmqClient = await startRMQClient()
|
||||||
@@ -17,13 +18,15 @@ async function startWorker() {
|
|||||||
|
|
||||||
await pgClient.checkDatabaseConnection()
|
await pgClient.checkDatabaseConnection()
|
||||||
|
|
||||||
const operationRepository = new OperationsRepository(pgClient)
|
const operationRepository = new ObjeniousOperationsRepository(pgClient)
|
||||||
|
const orderRepository = new OrderRepository(pgClient)
|
||||||
|
|
||||||
const simActivationController = new SimController(
|
const simActivationController = new SimController(
|
||||||
rmqClient,
|
rmqClient,
|
||||||
new SimUseCases({
|
new SimUseCases({
|
||||||
httpClient: httpClient,
|
httpClient: httpClient,
|
||||||
operationRepository: operationRepository
|
operationRepository: operationRepository,
|
||||||
|
orderRepository: orderRepository
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
const simRouter = new SimRouter(simActivationController, rmqClient)
|
const simRouter = new SimRouter(simActivationController, rmqClient)
|
||||||
|
|||||||
@@ -68,7 +68,6 @@
|
|||||||
"cors": "*",
|
"cors": "*",
|
||||||
"dotenv": "*",
|
"dotenv": "*",
|
||||||
"express": "*",
|
"express": "*",
|
||||||
"sim-consumidor-objenious": "sim-consumidor-objenious:*",
|
|
||||||
"sim-shared": "sim-shared:*",
|
"sim-shared": "sim-shared:*",
|
||||||
"typescript": "*"
|
"typescript": "*"
|
||||||
},
|
},
|
||||||
|
|||||||
127
packages/sim-entrada-eventos/aplication/Order.controller.ts
Normal file
127
packages/sim-entrada-eventos/aplication/Order.controller.ts
Normal 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
39
packages/sim-entrada-eventos/aplication/Order.usecases.ts
Normal file
39
packages/sim-entrada-eventos/aplication/Order.usecases.ts
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -1,164 +1,181 @@
|
|||||||
import { Request, Response } from "express"
|
import { Request, Response } from "express"
|
||||||
import { SimUsecases } from "./Sim.usecases.js"
|
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 {
|
export class SimController {
|
||||||
private simUseCases: SimUsecases
|
private simUseCases: SimUsecases
|
||||||
|
|
||||||
constructor(args: {
|
constructor(args: {
|
||||||
simUseCases: SimUsecases
|
simUseCases: SimUsecases,
|
||||||
}) {
|
}) {
|
||||||
this.simUseCases = args.simUseCases
|
this.simUseCases = args.simUseCases
|
||||||
|
|
||||||
this.activation = this.activation.bind(this)
|
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) => {
|
return async (req: Request, res: Response) => {
|
||||||
const valido = this.validateBody(req.body, res)
|
const body = req.body
|
||||||
if (valido == false) return;
|
|
||||||
|
|
||||||
const { iccid } = req.body
|
// 1. Validacion del body
|
||||||
const compañia = this.compañiaFromIccid(iccid)
|
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) {
|
// 2. Transformacion del body
|
||||||
res.status(500).json({
|
// 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: {
|
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 {
|
// 4. Casos de error del usecase
|
||||||
await this.simUseCases.preActivation({ iccid, compañia })
|
if (usecaseResult.error != undefined) {
|
||||||
|
// 4.1 Error del caso de uso
|
||||||
res.status(200).json({
|
|
||||||
iccid: iccid,
|
|
||||||
operation: "activation"
|
|
||||||
}).send()
|
|
||||||
} catch (err) {
|
|
||||||
console.error("Error activando la sim ", req.body)
|
|
||||||
res.status(500).json({
|
res.status(500).json({
|
||||||
errors: {
|
errors: {
|
||||||
msg: "Error general de activation"
|
...usecaseResult.error
|
||||||
}
|
}
|
||||||
}).send()
|
}).send()
|
||||||
return;
|
args.onError(body, usecaseResult.error.msg.message)
|
||||||
|
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() {
|
public activation() {
|
||||||
return async (req: Request, res: Response) => {
|
return this.controllerGenerator<{ iccid: string, offer: string }, { iccid: string, offer: string, compañia: string }>({
|
||||||
const valido = this.validateBody(req.body, res)
|
validator: activationValidator,
|
||||||
|
mapBody: (b) => {
|
||||||
if (valido == false) return; // Si no es valido ya se ha enviado el error
|
const { iccid, offer } = b
|
||||||
|
const compañia = companyFromIccid(iccid)
|
||||||
const { iccid, offer } = req.body
|
return { iccid, compañia, offer }
|
||||||
|
},
|
||||||
const compañia = this.compañiaFromIccid(iccid)
|
useCase: (args) => this.simUseCases.activation(args),
|
||||||
|
onError: (d, e) => console.error("[x] Error activacion: ", d, e),
|
||||||
if (compañia == undefined) {
|
onSuccess: console.log
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public cancelation() {
|
public cancelation() {
|
||||||
return async (req: Request, res: Response) => {
|
return this.controllerGenerator<{ iccid: string }, { iccid: string, compañia: string }>({
|
||||||
const valido = this.validateBody(req.body, res)
|
validator: iccidValidator,
|
||||||
|
mapBody: (b) => {
|
||||||
if (valido == false) return; // Si no es valido ya se ha enviado el error
|
const { iccid } = b
|
||||||
|
const compañia = companyFromIccid(iccid)
|
||||||
const { iccid } = req.body
|
return { iccid, compañia }
|
||||||
const compañia = this.compañiaFromIccid(iccid)
|
},
|
||||||
|
useCase: (args) => this.simUseCases.cancelation(args),
|
||||||
try {
|
// TODO: Meter en los mensajes el nombre de la operacion
|
||||||
await this.simUseCases.cancelation({ iccid, compañia })
|
onError: (d, e) => console.error("[x] Error cancelacion: ", d, e),
|
||||||
res.status(200).json({
|
onSuccess: console.log
|
||||||
iccid: iccid,
|
})
|
||||||
operation: "cancelation"
|
|
||||||
})
|
|
||||||
} catch (err) {
|
|
||||||
console.error("Error cancelando la sim ", req.body)
|
|
||||||
res.status(500).json({
|
|
||||||
errors: {
|
|
||||||
msg: "Error general de cancelacion"
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public pause() {
|
public pause() {
|
||||||
return async (req: Request, res: Response) => {
|
return this.controllerGenerator<{ iccid: string }, { iccid: string, compañia: string }>({
|
||||||
const valido = this.validateBody(req.body, res)
|
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() {
|
public free() {
|
||||||
return async (req: Request, res: Response) => {
|
return async (req: Request, res: Response) => {
|
||||||
const valido = this.validateBody(req.body, res)
|
try {
|
||||||
|
iccidValidator.validate(req.body)
|
||||||
if (valido == false) return; // Si no es valido ya se ha enviado el error
|
} catch (e) {
|
||||||
|
res.status(422).json({
|
||||||
|
errors: {
|
||||||
|
msg: e
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
const { iccid } = req.body
|
const { iccid } = req.body
|
||||||
const compañia = this.compañiaFromIccid(iccid)
|
const compañia = companyFromIccid(iccid)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await this.simUseCases.cancelation({ iccid, compañia })
|
await this.simUseCases.cancelation({ iccid, compañia })
|
||||||
@@ -178,14 +195,19 @@ export class SimController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public save() {
|
public save() {
|
||||||
|
|
||||||
return async (req: Request, res: Response) => {
|
return async (req: Request, res: Response) => {
|
||||||
const valido = this.validateBody(req.body, res)
|
try {
|
||||||
|
iccidValidator.validate(req.body)
|
||||||
if (valido == false) return; // Si no es valido ya se ha enviado el error
|
} catch (e) {
|
||||||
|
res.status(422).json({
|
||||||
|
errors: {
|
||||||
|
msg: e
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
const { iccid } = req.body
|
const { iccid } = req.body
|
||||||
const compañia = this.compañiaFromIccid(iccid)
|
const compañia = companyFromIccid(iccid)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await this.simUseCases.cancelation({ iccid, compañia })
|
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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 { EventBus } from "sim-shared/domain/EventBus.port";
|
||||||
import { SimEvents } from "sim-shared/domain/SimEvents";
|
import { SimEvents } from "sim-shared/domain/SimEvents";
|
||||||
|
import { uuidv7 } from "uuidv7";
|
||||||
|
import { CreateOrderDTO, OrderTracking, OrderType, OrderTypeOptions } from "sim-shared/domain/Order.js";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* TODO:
|
* Casos de uso de tarjetas sim. Garantiza que todos los metodos usan el mismo bus de mensajes
|
||||||
* - Conexion con la BDD
|
* y repositorio de registro de las ordenes.
|
||||||
* - Conexion con RabbitMQ
|
|
||||||
* - Pasar a clase cuando existan las conexiones
|
|
||||||
*/
|
*/
|
||||||
export class SimUsecases {
|
export class SimUsecases {
|
||||||
private eventBus: EventBus
|
private eventBus: EventBus;
|
||||||
|
private orderRepository: OrderRepository;
|
||||||
|
|
||||||
constructor(args: {
|
constructor(args: {
|
||||||
eventBus: EventBus
|
eventBus: EventBus,
|
||||||
|
orderRepository: OrderRepository
|
||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
this.eventBus = args.eventBus
|
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
|
* 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
|
* Si ya existia se modifican los campos pero no se hace un cambio
|
||||||
* de estado.
|
* de estado.
|
||||||
@@ -34,8 +100,8 @@ export class SimUsecases {
|
|||||||
return this.eventBus.publish([activationEvent])
|
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>{
|
const activationEvent = <SimEvents.activation>{
|
||||||
key: `sim.${args.compañia}.activate`,
|
key: `sim.${args.compañia}.activate`,
|
||||||
payload: {
|
payload: {
|
||||||
@@ -43,11 +109,29 @@ export class SimUsecases {
|
|||||||
offer: args.offer
|
offer: args.offer
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
console.log("[d] Activation ", activationEvent)
|
const activationWithId = this.addMessage_id(activationEvent)
|
||||||
return this.eventBus.publish([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>{
|
const preActivationEvent = <SimEvents.preActivation>{
|
||||||
key: `sim.${args.compañia}.preActivate`,
|
key: `sim.${args.compañia}.preActivate`,
|
||||||
@@ -56,22 +140,56 @@ export class SimUsecases {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
console.log("[d] Pre - activation ", preActivationEvent)
|
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
|
* 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`,
|
key: `sim.${args.compañia}.cancel`,
|
||||||
payload: {
|
payload: {
|
||||||
iccid: args.iccid
|
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
|
// alias por si acaso
|
||||||
public terminate = this.cancelation;
|
public terminate = this.cancelation;
|
||||||
@@ -79,16 +197,36 @@ export class SimUsecases {
|
|||||||
/**
|
/**
|
||||||
* alias de bloquear / suspender en objenious
|
* alias de bloquear / suspender en objenious
|
||||||
*/
|
*/
|
||||||
async pause(args: { iccid: string, compañia: string }) {
|
async pause(args: { iccid: string, compañia: string }):
|
||||||
const cancelationEvent = <SimEvents.pause>{
|
Promise<Result<string, { iccid: string, message_id: string, operation: "cancelation" }>> {
|
||||||
|
const pauseEvent = <SimEvents.pause>{
|
||||||
key: `sim.${args.compañia}.pause`,
|
key: `sim.${args.compañia}.pause`,
|
||||||
payload: {
|
payload: {
|
||||||
iccid: args.iccid
|
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: "cancelation"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async free(args: { iccid: string, compañia: string }) {
|
async free(args: { iccid: string, compañia: string }) {
|
||||||
const cancelationEvent = <SimEvents.free>{
|
const cancelationEvent = <SimEvents.free>{
|
||||||
key: `sim.${args.compañia}.free`,
|
key: `sim.${args.compañia}.free`,
|
||||||
|
|||||||
@@ -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" })
|
||||||
|
})
|
||||||
|
})
|
||||||
84
packages/sim-entrada-eventos/aplication/httpValidators.ts
Normal file
84
packages/sim-entrada-eventos/aplication/httpValidators.ts
Normal 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
|
||||||
|
])
|
||||||
18
packages/sim-entrada-eventos/config/postgreConfig.ts
Normal file
18
packages/sim-entrada-eventos/config/postgreConfig.ts
Normal 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
|
||||||
|
})
|
||||||
6
packages/sim-entrada-eventos/domain/common.ts
Normal file
6
packages/sim-entrada-eventos/domain/common.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
|
||||||
|
export type PaginationArgs = {
|
||||||
|
limit?: number,
|
||||||
|
offset?: number,
|
||||||
|
start?: number
|
||||||
|
}
|
||||||
22
packages/sim-entrada-eventos/domain/companies.ts
Normal file
22
packages/sim-entrada-eventos/domain/companies.ts
Normal 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
|
||||||
|
}
|
||||||
@@ -3,6 +3,7 @@ import cors from 'cors';
|
|||||||
import { simRoutes } from "./infrastructure/simRoutes.http.js"
|
import { simRoutes } from "./infrastructure/simRoutes.http.js"
|
||||||
import { rabbitmqEventBus } from '#config/eventBusConfig.js';
|
import { rabbitmqEventBus } from '#config/eventBusConfig.js';
|
||||||
import { env } from "#config/env/index.js"
|
import { env } from "#config/env/index.js"
|
||||||
|
import { orderRoutes } from "#adapters/orderRoutes.http.js";
|
||||||
|
|
||||||
const PORT = env.API_PORT
|
const PORT = env.API_PORT
|
||||||
const HOSTNAME = "0.0.0.0"
|
const HOSTNAME = "0.0.0.0"
|
||||||
@@ -24,6 +25,7 @@ app.use(express.json());
|
|||||||
app.use(express.urlencoded({ extended: true }));
|
app.use(express.urlencoded({ extended: true }));
|
||||||
|
|
||||||
app.use("/sim", simRoutes)
|
app.use("/sim", simRoutes)
|
||||||
|
app.use("/orders", orderRoutes)
|
||||||
|
|
||||||
app.get("/health", (req, res) => {
|
app.get("/health", (req, res) => {
|
||||||
res.status(200).json({ status: "ok" })
|
res.status(200).json({ status: "ok" })
|
||||||
|
|||||||
@@ -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 }
|
||||||
|
|
||||||
@@ -2,17 +2,22 @@ import { rabbitmqEventBus } from '#config/eventBusConfig.js';
|
|||||||
import { SimUsecases } from '../aplication/Sim.usecases.js';
|
import { SimUsecases } from '../aplication/Sim.usecases.js';
|
||||||
import { SimController } from '../aplication/Sim.controller.js';
|
import { SimController } from '../aplication/Sim.controller.js';
|
||||||
import { Router } from 'express';
|
import { Router } from 'express';
|
||||||
|
import { OrderRepository } from 'sim-shared/infrastructure/OrderRepository.js';
|
||||||
|
import { postgresClient } from '#config/postgreConfig.js';
|
||||||
|
|
||||||
const simRoutes = Router()
|
const simRoutes = Router()
|
||||||
|
const orderRepository = new OrderRepository(postgresClient)
|
||||||
|
|
||||||
const simUseCases = new SimUsecases({
|
const simUseCases = new SimUsecases({
|
||||||
eventBus: rabbitmqEventBus
|
eventBus: rabbitmqEventBus,
|
||||||
|
orderRepository: orderRepository
|
||||||
})
|
})
|
||||||
|
|
||||||
const simController = new SimController({
|
const simController = new SimController({
|
||||||
simUseCases: simUseCases
|
simUseCases: simUseCases
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// TODO: status de todos los proyectos
|
||||||
simRoutes.get("/status", () => { })
|
simRoutes.get("/status", () => { })
|
||||||
|
|
||||||
simRoutes.post("/save", simController.save())
|
simRoutes.post("/save", simController.save())
|
||||||
@@ -25,6 +30,8 @@ simRoutes.post("/pause", simController.pause())
|
|||||||
|
|
||||||
simRoutes.post("/cancel", simController.cancelation())
|
simRoutes.post("/cancel", simController.cancelation())
|
||||||
|
|
||||||
|
simRoutes.post("/test", simController.test())
|
||||||
|
|
||||||
// Proceso especifico de ALAI para liberar sims canceladas
|
// Proceso especifico de ALAI para liberar sims canceladas
|
||||||
simRoutes.post("/free", simController.free())
|
simRoutes.post("/free", simController.free())
|
||||||
|
|
||||||
|
|||||||
@@ -13,12 +13,6 @@
|
|||||||
"types": "./config/*.ts",
|
"types": "./config/*.ts",
|
||||||
"default": "./config/*.js"
|
"default": "./config/*.js"
|
||||||
},
|
},
|
||||||
"#shared/*.js": {
|
|
||||||
"default": "../sim-shared/*.js"
|
|
||||||
},
|
|
||||||
"#shared/*": {
|
|
||||||
"default": "../sim-shared/*.js"
|
|
||||||
},
|
|
||||||
"#adapters/*.js": {
|
"#adapters/*.js": {
|
||||||
"types": "./infrastructure/*.ts",
|
"types": "./infrastructure/*.ts",
|
||||||
"default": "./infrastructure/*.js"
|
"default": "./infrastructure/*.js"
|
||||||
@@ -45,7 +39,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"scripts": {
|
"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/",
|
"build": "tsc --build && tsc-alias -p tsconfig.json && cp package.json ../../dist/packages/sim-entrada-eventos/",
|
||||||
"dev": "tsx watch index.ts",
|
"dev": "tsx watch index.ts",
|
||||||
"start": "node ../../dist/packages/sim-entrada-eventos/index.js"
|
"start": "node ../../dist/packages/sim-entrada-eventos/index.js"
|
||||||
@@ -59,6 +53,7 @@
|
|||||||
"cors": "*",
|
"cors": "*",
|
||||||
"dotenv": "*",
|
"dotenv": "*",
|
||||||
"express": "*",
|
"express": "*",
|
||||||
|
"sim-shared": "sim-shared:*",
|
||||||
"typescript": "*"
|
"typescript": "*"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ OBJ_CLI_ASSERTION=XOc7FtwXD8hUX2SFVX94XSty8wkOmChkwDNF09O_aIxPubMDdFUdCDCB4zpzSI
|
|||||||
OBJ_CLIENT_ID=savefamily_rest_ws
|
OBJ_CLIENT_ID=savefamily_rest_ws
|
||||||
OBJ_KID=xNfbMiyL1ORXGP8lElhcv8nVaG3EJKye4Lc1YoN3I1E
|
OBJ_KID=xNfbMiyL1ORXGP8lElhcv8nVaG3EJKye4Lc1YoN3I1E
|
||||||
OBJ_BASE_URL=https://api-getway.objenious.com/ws
|
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="https://sf-sim-activation.savefamilygps.net/send-activation-mail"
|
||||||
|
NOTIFICATION_URL="localhost"
|
||||||
|
SIM_ACTIVATION_API_KEY=9e48c4ac-1ab0-4397-b3f3-6c239200dfe6
|
||||||
|
|||||||
22
packages/sim-objenious-cron/config/env/index.ts
vendored
22
packages/sim-objenious-cron/config/env/index.ts
vendored
@@ -1,5 +1,6 @@
|
|||||||
import { loadEnvFile } from "node:process";
|
import { loadEnvFile } from "node:process";
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
|
import assert from "node:assert";
|
||||||
|
|
||||||
loadEnvFile(path.join("../../.env")) // Global
|
loadEnvFile(path.join("../../.env")) // Global
|
||||||
loadEnvFile(path.join("./.env")) // base
|
loadEnvFile(path.join("./.env")) // base
|
||||||
@@ -12,9 +13,9 @@ export const env = {
|
|||||||
POSTGRES_HOST: process.env.POSTGRES_HOST,
|
POSTGRES_HOST: process.env.POSTGRES_HOST,
|
||||||
POSTGRES_DATABASE: process.env.POSTGRES_DATABASE,
|
POSTGRES_DATABASE: process.env.POSTGRES_DATABASE,
|
||||||
RABBITMQ_HOST: String(process.env.RABBITMQ_HOST ?? "localhost"),
|
RABBITMQ_HOST: String(process.env.RABBITMQ_HOST ?? "localhost"),
|
||||||
RABBITMQ_USER: String(process.env.RABBITMQ_USER ?? "guest"),
|
RABBITMQ_USER: String(process.env.RABBITMQ_USER),
|
||||||
RABBITMQ_PASSWORD: String(process.env.RABBITMQ_PASSWORD ?? "guest"),
|
RABBITMQ_PASSWORD: String(process.env.RABBITMQ_PASSWORD),
|
||||||
RABBITMQ_EXCHANGE: String(process.env.RABBITMQ_EXCHANGE ?? "/"),
|
RABBITMQ_EXCHANGE: String(process.env.RABBITMQ_EXCHANGE),
|
||||||
RABBITMQ_PORT: parseInt(process.env.RABBITMQ_PORT ?? "5672"),
|
RABBITMQ_PORT: parseInt(process.env.RABBITMQ_PORT ?? "5672"),
|
||||||
RABBITMQ_MODULENAME: process.env.MODULENAME,
|
RABBITMQ_MODULENAME: process.env.MODULENAME,
|
||||||
RABBITMQ_TTL: process.env.RABBITMQ_TTL,
|
RABBITMQ_TTL: process.env.RABBITMQ_TTL,
|
||||||
@@ -30,6 +31,19 @@ export const env = {
|
|||||||
OBJ_KID: String(process.env.OBJ_KID),
|
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)
|
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")
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Pool, QueryResult } from 'pg';
|
import { Pool } from 'pg';
|
||||||
import { PgClient } from 'sim-shared/infrastructure/PgClient.js'
|
import { PgClient } from 'sim-shared/infrastructure/PgClient.js'
|
||||||
import { env } from './env/index.js';
|
import { env } from './env/index.js';
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,8 @@ import { pgPool } from "./config/postgreConfig.js"
|
|||||||
import { PgClient } from "sim-shared/infrastructure/PgClient.js"
|
import { PgClient } from "sim-shared/infrastructure/PgClient.js"
|
||||||
import { httpInstance } from "./config/httpClient.config.js"
|
import { httpInstance } from "./config/httpClient.config.js"
|
||||||
import { CheckObjeniousRequests } from "./tasks/check_objenious_request.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"
|
||||||
|
|
||||||
async function startCron() {
|
async function startCron() {
|
||||||
const commonSettings = {
|
const commonSettings = {
|
||||||
@@ -15,19 +16,22 @@ async function startCron() {
|
|||||||
const pgClient = new PgClient({ pool: pgPool })
|
const pgClient = new PgClient({ pool: pgPool })
|
||||||
await pgClient.checkDatabaseConnection()
|
await pgClient.checkDatabaseConnection()
|
||||||
await pgClient.checkDatabaseConnection()
|
await pgClient.checkDatabaseConnection()
|
||||||
const operationRepository = new OperationsRepository(pgClient)
|
const operationRepository = new ObjeniousOperationsRepository(pgClient)
|
||||||
|
const orderRepository = new OrderRepository(pgClient)
|
||||||
|
|
||||||
const objTask = new CheckObjeniousRequests(
|
const objTask = new CheckObjeniousRequests(
|
||||||
operationRepository,
|
operationRepository,
|
||||||
httpClient
|
orderRepository,
|
||||||
|
httpClient,
|
||||||
)
|
)
|
||||||
|
|
||||||
await objTask.getPendingOperations()
|
await objTask.getPendingOperations()
|
||||||
|
|
||||||
const interval = setInterval(async () => {
|
const interval = setInterval(async () => {
|
||||||
console.log("Updating...")
|
console.log("Updating...")
|
||||||
await objTask.getPendingOperations()
|
await objTask.getPendingOperations()
|
||||||
console.log("Update finished")
|
console.log("Update finished")
|
||||||
}, 60 * 1000)
|
}, 10 * 60 * 1000)
|
||||||
/*
|
/*
|
||||||
const task = cron.createTask("* * * * *", async () => {
|
const task = cron.createTask("* * * * *", async () => {
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
export const task = async () => console.log("Background " + new Date().toISOString())
|
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import { env } from "#config/env/index.js";
|
import { env } from "#config/env/index.js";
|
||||||
|
import { OrderRepository } from "sim-shared/infrastructure/OrderRepository.js";
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import { IOperationsRepository, Objenious, ObjeniousOperation, ObjeniousOperationChange, StatusEnum } from "sim-shared/domain/operationsRepository.port.js";
|
import { IOperationsRepository, Objenious, ObjeniousOperation, ObjeniousOperationChange, StatusEnum } from "sim-shared/domain/operationsRepository.port.js";
|
||||||
import { HttpClient } from "sim-shared/infrastructure/HTTPClient.js";
|
import { HttpClient } from "sim-shared/infrastructure/HTTPClient.js";
|
||||||
@@ -6,6 +7,7 @@ import { HttpClient } from "sim-shared/infrastructure/HTTPClient.js";
|
|||||||
export class CheckObjeniousRequests {
|
export class CheckObjeniousRequests {
|
||||||
constructor(
|
constructor(
|
||||||
private readonly operationsRepository: IOperationsRepository,
|
private readonly operationsRepository: IOperationsRepository,
|
||||||
|
private readonly orderRepository: OrderRepository,
|
||||||
private readonly httpClient: HttpClient
|
private readonly httpClient: HttpClient
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
@@ -14,43 +16,47 @@ export class CheckObjeniousRequests {
|
|||||||
* TODO: meter a una funcion a parte task con los 3 pasos
|
* TODO: meter a una funcion a parte task con los 3 pasos
|
||||||
*/
|
*/
|
||||||
public async getPendingOperations() {
|
public async getPendingOperations() {
|
||||||
|
// 1. Se obtienen todas las operaciones pendientes de la BDD
|
||||||
const pendingOperations = await this.operationsRepository.getPendingOperations()
|
const pendingOperations = await this.operationsRepository.getPendingOperations()
|
||||||
|
|
||||||
|
|
||||||
if (pendingOperations.error != undefined) {
|
if (pendingOperations.error != undefined) {
|
||||||
throw new Error("Error obteniendo las tareas pendientes " + pendingOperations.error)
|
throw new Error("Error obteniendo las tareas pendientes " + pendingOperations.error)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (pendingOperations.data == undefined || pendingOperations.data.length == 0) {
|
if (pendingOperations.data == undefined || pendingOperations.data.length == 0) {
|
||||||
//Nada pendiente
|
console.log("[cron] No hay operaciones pendientes de Objenious")
|
||||||
return;
|
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
|
const erroneas = pendingOperations.data
|
||||||
.filter((e) => e.request_id == undefined)
|
.filter((e) => e.request_id == undefined)
|
||||||
|
// Todas las validas
|
||||||
const operacionesValidas = pendingOperations.data
|
const operacionesValidas = pendingOperations.data
|
||||||
.filter((e) => e.request_id != undefined)
|
.filter((e) => e.request_id != undefined)
|
||||||
|
// Validas sin MassId
|
||||||
const solicitarMassId = operacionesValidas
|
const solicitarMassId = operacionesValidas
|
||||||
.filter((e) => e.mass_action_id == undefined)
|
.filter((e) => e.mass_action_id == undefined)
|
||||||
|
// Validas con MassId
|
||||||
const consultarEstado = pendingOperations.data
|
const consultarEstado = pendingOperations.data
|
||||||
.filter(e => e.mass_action_id != undefined)
|
.filter(e => e.mass_action_id != undefined)
|
||||||
|
// TODO: Validas sin/con massID que lleven mucho tiempo sin actualizarse
|
||||||
|
|
||||||
console.log("[cron] Solicitando mass id para", solicitarMassId.map(e => e.id))
|
console.log("[cron] Solicitando mass id para", solicitarMassId.map(e => e.id))
|
||||||
|
|
||||||
const newMassActions = await this.getMassIdFromRequest(solicitarMassId)
|
const newMassActions = await this.getMassIdFromRequest(solicitarMassId)
|
||||||
|
|
||||||
const merged = [...newMassActions || [], ...consultarEstado]
|
const merged = [...newMassActions || [], ...consultarEstado]
|
||||||
|
|
||||||
console.log("[cron] Solicitando status para", merged.map(e => e.id))
|
console.log("[cron] Solicitando status para", merged.map(e => e.id))
|
||||||
|
|
||||||
const result = await this.getMassActionsStatus(merged)
|
const result = await this.getMassActionsStatus(merged)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Para una lista de operaciones **con mass_action_id** se comprueba si han tenido alguna actualizacion
|
||||||
|
* Devuelve el numero de operaciones comprobadas.
|
||||||
|
*/
|
||||||
private async getMassActionsStatus(requestList: ObjeniousOperation[]) {
|
private async getMassActionsStatus(requestList: ObjeniousOperation[]) {
|
||||||
if (requestList.length == 0) return;
|
if (requestList.length == 0) return 0;
|
||||||
|
|
||||||
const operationsList = structuredClone(requestList)
|
const operationsList = structuredClone(requestList)
|
||||||
const PATH = "/actions/massActions/"
|
const PATH = "/actions/massActions/"
|
||||||
@@ -91,8 +97,10 @@ export class CheckObjeniousRequests {
|
|||||||
// 2. Se comprueba si ha habido un cambio de estado
|
// 2. Se comprueba si ha habido un cambio de estado
|
||||||
const { id, status, info } = data
|
const { id, status, info } = data
|
||||||
|
|
||||||
if (status != originalAction.objenious_status) {
|
const hasStatusChanged = status != originalAction.objenious_status
|
||||||
|
if (hasStatusChanged) {
|
||||||
console.log("[cron] Actualizando", originalAction.id, originalAction.iccids, status)
|
console.log("[cron] Actualizando", originalAction.id, originalAction.iccids, status)
|
||||||
|
/** Status convertido al que se usa en la aplicacion */
|
||||||
const uorStatus = this.mapStatus(status)
|
const uorStatus = this.mapStatus(status)
|
||||||
const updateData: ObjeniousOperationChange = {
|
const updateData: ObjeniousOperationChange = {
|
||||||
operation_id: originalAction.id!,
|
operation_id: originalAction.id!,
|
||||||
@@ -102,29 +110,46 @@ export class CheckObjeniousRequests {
|
|||||||
previous_status: originalAction.status
|
previous_status: originalAction.status
|
||||||
}
|
}
|
||||||
|
|
||||||
originalAction.status = uorStatus;
|
const updatedAction = structuredClone(originalAction)
|
||||||
originalAction.objenious_status = status;
|
|
||||||
originalAction.last_change_date = new Date().toISOString()
|
updatedAction.status = uorStatus;
|
||||||
originalAction.end_date = originalAction.last_change_date
|
updatedAction.objenious_status = status;
|
||||||
console.log(" ----> Status", uorStatus)
|
updatedAction.last_change_date = new Date().toISOString()
|
||||||
if (uorStatus /*== "finished"*/) {
|
updatedAction.end_date = originalAction.last_change_date
|
||||||
|
|
||||||
|
if (uorStatus == "finished") {
|
||||||
console.log(" ****> Status", uorStatus)
|
console.log(" ****> Status", uorStatus)
|
||||||
|
if (uorStatus != "finished") {
|
||||||
|
console.error("!!! Notificando estado no finished")
|
||||||
|
}
|
||||||
const targetIccids = originalAction.iccids
|
const targetIccids = originalAction.iccids
|
||||||
const lineData = await this.getLineData(targetIccids)
|
const lineData = await this.getLineData(targetIccids)
|
||||||
console.log("lineData", lineData.content[0])
|
console.log("[i] lineData", lineData.content[0])
|
||||||
const msisdn = lineData.content[0].identifier.msisdn
|
const msisdn = lineData.content[0].identifier.msisdn
|
||||||
|
|
||||||
this.notifyFinalization({
|
if (originalAction.correlation_id != undefined) {
|
||||||
...originalAction,
|
this.orderRepository.finishOrder({ correlation_id: originalAction.correlation_id })
|
||||||
msisdn
|
.then(e => console.log("[o] Finalizada order", e))
|
||||||
})
|
.catch(e => {
|
||||||
.then(e => {
|
console.error("[x] Error finalizando la order ", e)
|
||||||
console.log("Notificada la activacion de ", originalAction.iccids)
|
console.error(e)
|
||||||
})
|
})
|
||||||
.catch(e => {
|
}
|
||||||
console.error("Error enviando la activacion de ", originalAction)
|
|
||||||
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) {
|
if (info != undefined) {
|
||||||
@@ -132,12 +157,12 @@ export class CheckObjeniousRequests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
console.log("Subiendo un update")
|
console.log("[i] Subiendo un update")
|
||||||
console.log(updateData)
|
console.log(updateData)
|
||||||
await this.operationsRepository.updateOperation(updateData)
|
await this.operationsRepository.updateOperation(updateData)
|
||||||
updated.push(originalAction)
|
updated.push(originalAction)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("Error actualizando el estado de ", originalAction, e)
|
console.error("[x] Error actualizando el estado de ", originalAction, e)
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -151,7 +176,8 @@ export class CheckObjeniousRequests {
|
|||||||
// ! Importante las claves siempre en minuscula, los valores son cammelCase
|
// ! Importante las claves siempre en minuscula, los valores son cammelCase
|
||||||
const equivalentMap = new Map<string, StatusEnum>([
|
const equivalentMap = new Map<string, StatusEnum>([
|
||||||
["en cours", "running"],
|
["en cours", "running"],
|
||||||
["terminé", "finished"]
|
["terminé", "finished"],
|
||||||
|
["annulé", "finished"]
|
||||||
])
|
])
|
||||||
const res = equivalentMap.get(sanitizedStatus)
|
const res = equivalentMap.get(sanitizedStatus)
|
||||||
if (res == undefined) return "running"
|
if (res == undefined) return "running"
|
||||||
@@ -236,29 +262,15 @@ export class CheckObjeniousRequests {
|
|||||||
* al servicio que manda los mails
|
* al servicio que manda los mails
|
||||||
*/
|
*/
|
||||||
private async notifyFinalization(operation: ObjeniousOperation & { msisdn: string }) {
|
private async notifyFinalization(operation: ObjeniousOperation & { msisdn: string }) {
|
||||||
console.log("Notificada, ", operation)
|
|
||||||
|
|
||||||
const req = axios.post(env.NOTIFICATION_URL, {
|
const req = axios.post(env.NOTIFICATION_URL, {
|
||||||
...operation,
|
...operation,
|
||||||
iccids: [operation.iccids]
|
iccids: [operation.iccids]
|
||||||
}, {
|
}, {
|
||||||
headers: {
|
headers: {
|
||||||
"x-apikey-sim-activation": "9e48c4ac-1ab0-4397-b3f3-6c239200dfe6"
|
"x-apikey-sim-activation": env.SIM_ACTIVATION_API_KEY
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
/*
|
|
||||||
const req = this.httpClient.client.post<any>("",
|
|
||||||
{ operation: operation },
|
|
||||||
{
|
|
||||||
headers: {
|
|
||||||
"x-apikey-sim-activation": "9e48c4ac-1ab0-4397-b3f3-6c239200dfe6"
|
|
||||||
},
|
|
||||||
baseURL: env.NOTIFICATION_URL
|
|
||||||
}
|
|
||||||
|
|
||||||
)*/
|
|
||||||
await req
|
await req
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
35
packages/sim-shared/aplication/BodyValidator.ts
Normal file
35
packages/sim-shared/aplication/BodyValidator.ts
Normal 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
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
27
packages/sim-shared/config/config.test.ts
Normal file
27
packages/sim-shared/config/config.test.ts
Normal 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}`)
|
||||||
@@ -7,9 +7,11 @@ export type DomainEventType = string
|
|||||||
|
|
||||||
export type DomainEvent = {
|
export type DomainEvent = {
|
||||||
key: string,
|
key: string,
|
||||||
payload: Object,
|
payload: object,
|
||||||
options: Object,
|
headers?: object & {
|
||||||
occurredOn: Date,
|
message_id?: string
|
||||||
|
},
|
||||||
|
occurredOn?: Date,
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DomainEventSubscriber<T extends DomainEvent> {
|
export interface DomainEventSubscriber<T extends DomainEvent> {
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { ConsumeMessage } from "amqplib";
|
|||||||
import { DomainEvent, DomainEventSubscriber } from "./DomainEvent.js";
|
import { DomainEvent, DomainEventSubscriber } from "./DomainEvent.js";
|
||||||
|
|
||||||
export interface EventBus {
|
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
|
// Sacado de NEKI, posiblemente no haga falta
|
||||||
addSubscribers(subscribers: Array<DomainEventSubscriber<DomainEvent>>): void;
|
addSubscribers(subscribers: Array<DomainEventSubscriber<DomainEvent>>): void;
|
||||||
|
|
||||||
|
|||||||
85
packages/sim-shared/domain/Order.ts
Normal file
85
packages/sim-shared/domain/Order.ts
Normal 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
|
||||||
|
}
|
||||||
|
|
||||||
@@ -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>
|
* Result<Error,Data>
|
||||||
*/
|
*/
|
||||||
export type Result<E, D> = {
|
export type Result<E, D> = Failure<E> | Success<D>
|
||||||
error: E | undefined,
|
|
||||||
data: D | undefined
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ export interface IOperationsRepository {
|
|||||||
|
|
||||||
export type ObjeniousOperation = {
|
export type ObjeniousOperation = {
|
||||||
id?: number;
|
id?: number;
|
||||||
|
/** Uuid del mensaje asociado a la operacion */
|
||||||
|
correlation_id?: string;
|
||||||
operation: string;
|
operation: string;
|
||||||
retry_count?: number;
|
retry_count?: number;
|
||||||
max_retry?: number;
|
max_retry?: number;
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { IOperationsRepository, ObjeniousOperation, ObjeniousOperationChange } f
|
|||||||
import { Result } from "sim-shared/domain/Result.js";
|
import { Result } from "sim-shared/domain/Result.js";
|
||||||
import { PgClient } from "sim-shared/infrastructure/PgClient.js";
|
import { PgClient } from "sim-shared/infrastructure/PgClient.js";
|
||||||
|
|
||||||
export class OperationsRepository implements IOperationsRepository {
|
export class ObjeniousOperationsRepository implements IOperationsRepository {
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private readonly pgClient: PgClient
|
private readonly pgClient: PgClient
|
||||||
@@ -12,7 +12,7 @@ export class OperationsRepository implements IOperationsRepository {
|
|||||||
async createOperation(data: ObjeniousOperation): Promise<Result<string, ObjeniousOperation>> {
|
async createOperation(data: ObjeniousOperation): Promise<Result<string, ObjeniousOperation>> {
|
||||||
const query = `
|
const query = `
|
||||||
INSERT INTO objenious_operation (operation, iccids, status, max_retry, request_id)
|
INSERT INTO objenious_operation (operation, iccids, status, max_retry, request_id)
|
||||||
VALUES ($1, $2, $3, $4, $5)
|
VALUES ($1, $2, $3, $4, $5)
|
||||||
RETURNING *`;
|
RETURNING *`;
|
||||||
const values = [data.operation, data.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);
|
const { rows } = await this.pgClient.query(query, values);
|
||||||
@@ -45,8 +45,8 @@ export class OperationsRepository implements IOperationsRepository {
|
|||||||
error = COALESCE($3,error),
|
error = COALESCE($3,error),
|
||||||
request_id = COALESCE($4, request_id),
|
request_id = COALESCE($4, request_id),
|
||||||
mass_action_id = COALESCE($5, mass_action_id),
|
mass_action_id = COALESCE($5, mass_action_id),
|
||||||
last_change_date = now(),
|
last_change_date = now() at time zone 'utc',
|
||||||
end_date = CASE WHEN $2 IN ('finished') THEN now() ELSE end_date END,
|
end_date = CASE WHEN $2 IN ('finished') THEN now() at time zone 'utc' ELSE end_date END,
|
||||||
objenious_status = $6
|
objenious_status = $6
|
||||||
WHERE id = $1`;
|
WHERE id = $1`;
|
||||||
|
|
||||||
172
packages/sim-shared/infrastructure/OrderRepository.test.ts
Normal file
172
packages/sim-shared/infrastructure/OrderRepository.test.ts
Normal 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)
|
||||||
|
})
|
||||||
|
})
|
||||||
446
packages/sim-shared/infrastructure/OrderRepository.ts
Normal file
446
packages/sim-shared/infrastructure/OrderRepository.ts
Normal 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
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -17,6 +17,11 @@ export class RabbitMQEventBus implements EventBus {
|
|||||||
private buildStructure?: (chan: Channel) => Promise<void>
|
private buildStructure?: (chan: Channel) => Promise<void>
|
||||||
private maxRetry: number = 0
|
private maxRetry: number = 0
|
||||||
|
|
||||||
|
connection?: AmqpConnectionManager
|
||||||
|
channel?: ChannelWrapper
|
||||||
|
connected: Boolean = false
|
||||||
|
|
||||||
|
private connectionOptions: RMQConnectionParams
|
||||||
constructor(args: {
|
constructor(args: {
|
||||||
connectionParams: RMQConnectionParams,
|
connectionParams: RMQConnectionParams,
|
||||||
buildStructure?: (chan: Channel) => Promise<void>,
|
buildStructure?: (chan: Channel) => Promise<void>,
|
||||||
@@ -73,11 +78,6 @@ export class RabbitMQEventBus implements EventBus {
|
|||||||
//return this.channel.nack(msg, false, requeue)
|
//return this.channel.nack(msg, false, requeue)
|
||||||
}
|
}
|
||||||
|
|
||||||
connection?: AmqpConnectionManager
|
|
||||||
channel?: ChannelWrapper
|
|
||||||
connected: Boolean = false
|
|
||||||
|
|
||||||
private connectionOptions: RMQConnectionParams
|
|
||||||
|
|
||||||
public async connect() {
|
public async connect() {
|
||||||
|
|
||||||
@@ -96,28 +96,39 @@ export class RabbitMQEventBus implements EventBus {
|
|||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("[RMQ] Error estableciendo la conexion con el servidor", e)
|
console.error("[RMQ] Error estableciendo la conexion con el servidor", e)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
publish(events: DomainEvent[]): Promise<void> {
|
publish(events: DomainEvent[]): Promise<{ success: DomainEvent[], error: DomainEvent[] }> {
|
||||||
|
return new Promise(async (res, rej) => {
|
||||||
return new Promise((res, rej) => {
|
const successEvents: DomainEvent[] = []
|
||||||
|
const errorEvents: DomainEvent[] = []
|
||||||
try {
|
try {
|
||||||
for (const event of events) {
|
for (const event of events) {
|
||||||
const exchange = "sim.exchange"
|
const exchange = "sim.exchange"
|
||||||
const routingKey = event.key
|
const routingKey = event.key
|
||||||
const content = Buffer.from(JSON.stringify(event))
|
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) {
|
if (err == undefined) {
|
||||||
console.log("Evento publicado ", event)
|
console.log("Evento publicado ", event)
|
||||||
} else {
|
} else {
|
||||||
console.error("Error publicando", event)
|
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) {
|
} catch (err) {
|
||||||
return rej(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")
|
if (this.connection == undefined) throw new Error("[RMQ] Intentando crear un canal sin una conexion")
|
||||||
const channel = this.connection.createChannel({
|
const channel = this.connection.createChannel({
|
||||||
setup: async (channel: Channel) => {
|
confirm: true,
|
||||||
|
setup: async (channel: ConfirmChannel) => {
|
||||||
// Exchanges comunes a todos
|
// Exchanges comunes a todos
|
||||||
channel.assertExchange("sim.exchange", "topic", { durable: true })
|
channel.assertExchange("sim.exchange", "topic", { durable: true })
|
||||||
channel.assertExchange("sim.dlx", "topic", { durable: true })
|
channel.assertExchange("sim.dlx", "topic", { durable: true })
|
||||||
@@ -195,6 +207,6 @@ export class RabbitMQEventBus implements EventBus {
|
|||||||
Promise.reject(error);
|
Promise.reject(error);
|
||||||
});
|
});
|
||||||
|
|
||||||
return channel as ChannelWrapper;
|
return channel;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,14 @@
|
|||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"description": "",
|
"description": "",
|
||||||
"exports": {
|
"exports": {
|
||||||
|
"./aplication/*.js": {
|
||||||
|
"types": "./aplication/*.ts",
|
||||||
|
"default": "./aplication/*.js"
|
||||||
|
},
|
||||||
|
"./aplication/*": {
|
||||||
|
"types": "./aplication/*.ts",
|
||||||
|
"default": "./aplication/*.js"
|
||||||
|
},
|
||||||
"./infrastructure/*.js": {
|
"./infrastructure/*.js": {
|
||||||
"types": "./infrastructure/*.ts",
|
"types": "./infrastructure/*.ts",
|
||||||
"default": "./infrastructure/*.js"
|
"default": "./infrastructure/*.js"
|
||||||
@@ -30,7 +38,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "echo \"Error: no test specified\" ",
|
"test": "node --import tsx --test ./**/*.test.ts",
|
||||||
"dev": "echo \" Shared no es un modulo ejecutable \" ",
|
"dev": "echo \" Shared no es un modulo ejecutable \" ",
|
||||||
"build": "tsc --build && tsc-alias -p tsconfig.json && cp package.json ../../dist/packages/sim-shared/"
|
"build": "tsc --build && tsc-alias -p tsconfig.json && cp package.json ../../dist/packages/sim-shared/"
|
||||||
},
|
},
|
||||||
|
|||||||
10
run.local.sh
10
run.local.sh
@@ -1,4 +1,12 @@
|
|||||||
#/bin/bash
|
#/bin/bash
|
||||||
rm deployment/database/init.sql
|
rm deployment/database/init.sql
|
||||||
cat deployment/database/*.sql >deployment/database/init.sql
|
|
||||||
|
# init sql debe juntar todos los scripts de "base" (sin contar migraciones)
|
||||||
|
cat deployment/database/base/*.sql >deployment/database/init.sql
|
||||||
|
#cp deployment/database/esquema_final* deployment/database/init.sql
|
||||||
|
|
||||||
|
# compatibilidad con postgresql < 17
|
||||||
|
sed -i '/\\restrict/d' deployment/database/init.sql
|
||||||
|
sed -i '/\\unrestrict/d' deployment/database/init.sql
|
||||||
|
|
||||||
docker compose -f deployment/local/docker/docker-compose.yaml --project-directory ./ up --watch
|
docker compose -f deployment/local/docker/docker-compose.yaml --project-directory ./ up --watch
|
||||||
|
|||||||
Reference in New Issue
Block a user