204 Commits
1.0.0 ... main

Author SHA1 Message Date
3f7c052572 config antigua 2026-05-13 17:30:51 +02:00
3d9e2a6e9b Nuevo lock 2026-05-13 17:27:28 +02:00
f94ee799d0 Problema de tipado 2026-05-13 17:20:31 +02:00
d1e5892a0d Mejorado el control de errores para ALAI 2026-05-13 17:08:19 +02:00
b14464da39 Bug preactivacion si id de operacion 2026-05-13 13:45:28 +02:00
cf6204e231 Problema de select tarjetas no preactivadas 2026-05-13 13:12:28 +02:00
9d8788db39 Preactivacion y activacion alai 2026-05-13 12:46:30 +02:00
79ceb74604 Activación con externalId 2026-05-12 17:36:22 +02:00
d02b1d48bf Correccion id de subscripcion de alai 2026-05-12 17:25:26 +02:00
1fb50323b0 Fix error imei undefined 2026-05-12 16:19:24 +02:00
985c85da59 Colas persistentes 2026-05-12 13:37:48 +02:00
23c61097a8 Env por defecto para NOS 2026-05-12 13:18:24 +02:00
5372045bd7 Error path 2026-05-12 12:51:06 +02:00
72d61b8376 Error path 2026-05-12 12:04:32 +02:00
526c094494 Crear dir de certificados 2026-05-12 11:13:14 +02:00
474f7b7c68 nombre 2026-05-12 08:56:37 +02:00
f8692e3e2e cert 2026-05-12 08:46:08 +02:00
62715fae34 Fix jenkisfile 2026-05-12 08:34:23 +02:00
f8678a68bb compose 2026-05-11 17:37:35 +02:00
8f8ae22f23 tado p12 2026-05-11 17:35:47 +02:00
2611147eb3 debug 2026-05-11 17:25:48 +02:00
b0b3badd5c test path 2026-05-11 16:59:16 +02:00
2b812098fb Ruta 2026-05-11 16:56:29 +02:00
3146efec64 Faltaba alai 2026-05-11 16:49:38 +02:00
44e4b98e35 errata 2026-05-11 15:54:13 +02:00
6adb4d5c95 Codigo no usable 2026-05-11 15:52:40 +02:00
bcb1a28164 build limpia 2026-05-11 15:30:51 +02:00
d5798602e2 Error nombre paquete 2026-05-11 15:16:09 +02:00
1f94a89520 Ordern correcto de operaciones 2026-05-11 13:08:35 +02:00
44bbc8f17c Scripts distintos para prod 2026-05-11 12:51:13 +02:00
9e6877c329 Cambiado a cp -P para mantener symlink 2026-05-11 12:42:47 +02:00
d5883ba75e Links simbolicos para .env 2026-05-11 12:36:29 +02:00
421d0aa705 Probando con links simbolicos 2026-05-11 12:24:29 +02:00
69b5958296 Merge branch 'main' of git.savefamilygps.net:SaveFamily/sf-sim 2026-05-11 12:16:36 +02:00
86478b1073 Comillas 2026-05-11 12:15:37 +02:00
b9e3e1784f Merge pull request 'WEBINT-335_migracion_alai' (#4) from WEBINT-335_migracion_alai into main
Reviewed-on: #4
2026-05-11 08:12:54 +00:00
976cf1c3d2 delete 2026-05-11 10:10:17 +02:00
e4ba1576e5 delete env 2026-05-11 10:09:16 +02:00
4baa9f708f Certificado p12 2026-05-11 10:05:26 +02:00
6fb25e6055 Ignore p12 certificado 2026-05-11 09:23:49 +02:00
63698ee1aa Misma info de sim para el /select 2026-05-08 14:49:43 +02:00
410f659db0 Sim comun para nos 2026-05-08 12:06:24 +02:00
08c972e720 Todos los datos de Alai para agrupar 2026-05-07 16:19:18 +02:00
c4e4d87303 Imei from subscription, SIM común 2026-05-07 13:53:02 +02:00
9c74fb9a7b Funcionan todos los select, pendiente el general 2026-05-07 11:37:35 +02:00
1d7c2b2946 puertos 2026-05-07 10:36:16 +02:00
d2c86396b1 Organizacion docs 2026-05-07 09:20:26 +02:00
3cf5c3695e Pruebas a los endpoints internos 2026-05-06 16:59:09 +02:00
7dda25fbfb Router para el select de cualquier sim 2026-05-06 16:47:52 +02:00
eefb7c5a79 pruebas 2026-05-05 17:31:08 +02:00
13944a64d2 ontroladores y rutas 2026-05-05 17:01:24 +02:00
07e60690ab Ajuste de preactivacion con externalID 2026-05-05 14:24:24 +02:00
036ae20ac3 Subscripciones de ALAI 2026-05-05 13:12:31 +02:00
189de6c0fb La conexión con alai funciona 2026-05-04 15:12:53 +02:00
113d9f3786 Debug para tokens de Alai 2026-05-04 13:39:54 +02:00
331d920379 Operaciones basicas ALAI 2026-05-04 09:37:06 +02:00
a615fc2b81 Agente https para certificados 2026-04-30 17:41:49 +02:00
f98097d11d Estructura para el token de alai
cabecera automatica de bearer para todas las requests a alai
2026-04-30 15:49:59 +02:00
3e76c3c931 Merge branch 'main' into WEBINT-335_migracion_alai 2026-04-29 17:12:55 +02:00
bb31efb271 Merge main -> migracion alai 2026-04-29 17:08:30 +02:00
c9733113cf Persistencia de rabbit 2026-04-29 13:25:46 +02:00
4c1d6ac2c4 No se crean los enlaces simbolicos en start 2026-04-29 13:19:47 +02:00
07e7a0d457 .env de NOS en el compose 2026-04-29 12:47:59 +02:00
48361ab33f El .env no se deberia mover en el paso de build 2026-04-29 12:24:27 +02:00
a3c7c224b1 Copia del env de nos 2026-04-29 11:26:00 +02:00
324aec3001 Merge branch 'WEBINT-338_tiempo_suspension' 2026-04-28 17:25:09 +02:00
05e941710b Limpieza 2026-04-28 17:24:28 +02:00
f78a333e1e Merge pull request 'WEBINT-338_tiempo_suspension' (#2) from WEBINT-338_tiempo_suspension into main
Reviewed-on: #2
2026-04-28 13:40:03 +00:00
01c55cba0f No se traquea el .env 2026-04-28 15:35:00 +02:00
10b2ae244c ignore 2026-04-28 15:33:08 +02:00
2dba2ebfae Query de sims por networkStatus y tiempo de suspension 2026-04-28 15:23:27 +02:00
d7eb4ad326 Gateway para francia funciona 2026-04-27 17:24:40 +02:00
d818441bde Docu 2026-04-27 16:30:22 +02:00
c91965567d Actualizacion docs 2026-04-27 15:17:20 +02:00
d063b47bec Merge branch 'main' into WEBINT-338_tiempo_suspension 2026-04-27 14:00:08 +02:00
6112de297b Bug duplicados solo en la primera linea 2026-04-27 13:46:34 +02:00
166c940295 Fix 2026-04-27 13:32:55 +02:00
246e4cb83b Comentarios 2026-04-27 13:03:47 +02:00
4517796ef3 Calculo del tiempo en suspension 2026-04-27 12:12:12 +02:00
858932f260 Test 2026-04-27 11:11:47 +02:00
a84f600fa2 Test orders 2026-04-27 11:05:09 +02:00
4e02ea021d Docs orders 2026-04-27 09:33:55 +02:00
9ec127433d Template alai 2026-04-23 13:18:50 +02:00
e1450c6e97 POST -> PATCH 2026-04-22 15:22:37 +02:00
e40a19bbfb Bug de correlation_id undefined 2026-04-22 13:31:24 +02:00
fbdb64f3a1 Arreglo de bugs ordes 2026-04-22 12:59:23 +02:00
9a29f49669 Fix orders nos 2026-04-22 12:31:46 +02:00
c2081191ae Trazabilidad de nos, arreglo de orders 2026-04-21 17:39:09 +02:00
f0f3827fd0 Registro del estado/resultado de las operaciones de NOS 2026-04-21 15:51:16 +02:00
ee8f84bc57 Typescript 6 2026-04-21 13:33:01 +02:00
f95677d503 Error pageNOS 2026-04-21 12:50:54 +02:00
59b0b57ec2 Merge branch 'main' into WEBINT-334-migracion-nos 2026-04-21 10:47:08 +02:00
9174b0b6a4 Nombres de colas 2026-04-21 10:22:53 +02:00
e62c49ce91 Endpoints NOS 2026-04-21 10:11:21 +02:00
32990b4dcd Controladores y rutas 2026-04-17 15:49:53 +02:00
da2413002b Repositorio de nos completo 2026-04-17 14:06:41 +02:00
fdbb81ba64 Inicio port NOS 2026-04-16 17:46:32 +02:00
964ea6add9 Inicio migracion NOS 2026-04-16 12:44:31 +02:00
602878acf4 console log de debug 2026-04-16 12:01:31 +02:00
0aa52feaac Proxy 2026-04-16 11:51:30 +02:00
15b70309da Fix networkStatus 2026-04-15 15:11:13 +02:00
7001fccbf7 Problema de creacion de operacion de pausa/cancelacion 2026-04-15 13:50:20 +02:00
cffee785b2 logs 2026-04-15 12:48:08 +02:00
33d260310c Reactivate cuando la linea esté suspendida 2026-04-15 12:47:26 +02:00
e359acc1d5 Validaciones pausa instantaneas 2026-04-15 11:47:33 +02:00
bb4bce4a6d fix 2026-04-15 11:11:55 +02:00
eac74ef0cd Error key evento 2026-04-15 10:31:21 +02:00
1dc4eb5648 validador 2026-04-15 10:28:52 +02:00
a35a6c2b60 Error endpoint 2026-04-15 10:27:58 +02:00
1f78f4a3e1 Periodo pruebas reducidio 2026-04-15 10:18:49 +02:00
1e98559f3a Fix 2026-04-15 10:17:36 +02:00
ef0f860b9d Erro handler 2026-04-14 16:20:15 +02:00
0bff55379f tab 2026-04-14 16:15:01 +02:00
4d34308a13 Bug de logueo de operacion 2026-04-14 16:07:17 +02:00
70bf73b0a4 Error query 2026-04-10 11:11:12 +02:00
e3849d8217 Archivos de migraciones 2026-04-09 12:48:36 +02:00
d9854a12a8 Version de migrate 2026-04-09 12:31:06 +02:00
48d387a8da Migraciones antes de lanzar start 2026-04-09 12:10:40 +02:00
93d3e13793 Merge pull request 'WEBINT-328-Pausas-cacelaciones' (#1) from WEBINT-328-Pausas-cacelaciones into main
Reviewed-on: #1
2026-04-09 10:03:47 +00:00
031f5d5cf0 nombre de columna con mayus 2026-04-09 11:59:06 +02:00
047669bab2 Acabados de corregir bugs 2026-04-09 11:53:49 +02:00
5ea5939e3a Bug de finaliazacion de tareas erroneas 2026-04-09 09:08:11 +02:00
7ff3f13af4 Funcionan las suspensiones 2026-04-08 17:37:47 +02:00
a9589f578b Solucionado cierrre de pool para test 2026-04-08 14:47:57 +02:00
a27e4b30d2 Cron completo y mejora de logs 2026-04-08 13:48:57 +02:00
4168949b9e Endpoint preparados 2026-04-08 10:08:54 +02:00
e6ff54a15d Usecases 2026-04-07 17:43:17 +02:00
3956797020 Las operaciones basicas del repositorio de pause/cancel funcionan y
tienen test
2026-04-07 15:40:19 +02:00
7d88359263 Refactor de jwt y base de la bdd de pausas-cancelaciones 2026-04-07 13:20:31 +02:00
1b6da651a6 Ajustado el periodo de comprobaciones 2026-03-27 12:47:10 +01:00
9b305f887f Test .env ajustado 2026-03-27 12:24:20 +01:00
9506b9e28e Error de nombre de activacion 2026-03-27 10:59:15 +01:00
61c0edca07 Logs del envio 2026-03-27 10:52:03 +01:00
9470b5605d Pribando el env 2026-03-27 10:50:03 +01:00
9d63d23754 Mejor gestion de errores para los order 2026-03-26 12:21:28 +01:00
a95655a2a6 Completada la tarea de volcado 2026-03-26 09:29:09 +01:00
025801a689 Repositorio de lineas funciona 2026-03-25 11:51:14 +01:00
28880c4d99 Lineas activas e insertar cada una 2026-03-24 17:27:52 +01:00
5bb3bc554b doc 2026-03-24 11:20:59 +01:00
cfb907b840 Copy en jenkins 2026-03-11 12:34:41 +01:00
d5d7953fd2 Endpoint para documentacion 2026-03-11 12:31:17 +01:00
96298aab25 Docs en HTML 2026-03-11 11:35:16 +01:00
c17cca1e81 Sobreescribia el registerSobreescribia el register 2026-03-10 10:43:56 +01:00
7264efcf79 Errata 2026-03-10 10:42:32 +01:00
8934bcd603 Copia yarnrc 2026-03-10 10:39:39 +01:00
bdd08dbc56 Copiar yarnrc a docker 2026-03-10 10:37:26 +01:00
7d47fde806 Solucionado problema db-migrate 2026-03-10 10:21:53 +01:00
ad207fb732 db-migrate 2026-03-10 09:34:17 +01:00
bd9081b5bc hardcodeado el customerAccountCode 2026-03-06 11:18:30 +01:00
a429e9d14a Errata customer 2026-03-06 11:13:47 +01:00
81eb986313 Error de tipado 2026-03-06 11:06:15 +01:00
58bedc42f1 Bug de correlation_id en las llamadas a objenious 2026-03-06 11:02:18 +01:00
b97f422261 Prod 2026-03-05 10:33:38 +01:00
7a7dc33724 Error de prod 2026-03-05 10:30:43 +01:00
7743bd1f0d Migraciones a mano de momento 2026-03-05 10:17:26 +01:00
2897d7aa3c Probando a añadir el registro desde jenkins 2026-03-05 10:10:12 +01:00
0fd7eafcf3 Eliminado el clone 2026-03-05 09:09:02 +01:00
71253d216e Registry local 2026-03-05 09:04:50 +01:00
aeea6cfefd Probando con clone 2026-03-04 16:49:07 +01:00
e8eb925834 Sin paso 2026-03-04 15:54:13 +01:00
7cf9cc60e6 Test jenkins 2026-03-04 15:52:43 +01:00
1e9818d430 Yarn lock 2026-03-04 14:03:11 +01:00
39c0e87758 Mejora de las orders y actualizacion docs 2026-03-04 13:51:24 +01:00
5771972e2a Revesriendo cambio del docker 2026-03-02 17:19:07 +01:00
ea13403dc3 Error https 2026-03-02 17:16:34 +01:00
8d9a9b84b8 Cambiando el lock a mano 2026-03-02 17:15:04 +01:00
9b92f3506b Ya no hace falta la eliminacion explicita 2026-03-02 16:57:46 +01:00
1798118f6b Sin yarn.lock que copiar 2026-03-02 16:51:12 +01:00
eba2b8c569 Ya con la eliminacion del lock 2026-03-02 16:48:18 +01:00
b6b2cf6cc8 El inmutable 2026-03-02 16:46:29 +01:00
a0faa2d105 Jenkins 2026-03-02 16:45:35 +01:00
d323f804fc No copiar el lock 2026-03-02 16:41:37 +01:00
978454754c Eliminado yarn lock 2026-03-02 16:29:35 +01:00
b6091b15da docker con clean del cache 2026-03-02 16:23:05 +01:00
a6794a061b Yarn install 2026-03-02 16:01:00 +01:00
fafea3ce04 http 2026-03-02 15:55:32 +01:00
992f639f35 Prueba con otra url para gitea 2026-03-02 15:38:42 +01:00
f57309b06a Preparado despliegue 2026-03-02 15:07:30 +01:00
3be2b8f20d Nuevo nombre del container 2026-03-02 14:57:43 +01:00
4853fec7ff Fix de gestion de orders
Proceso de cancelacion verificado
2026-02-27 13:43:09 +01:00
04a6e50b7a Orders en todas las etapas 2026-02-27 11:16:45 +01:00
8ca3d095e6 Fix suspension && paso a plantilla de caso de uso 2026-02-26 17:47:32 +01:00
ca1144b55c Orders en los consumidores y gestion de los demas casos de uso 2026-02-26 17:30:32 +01:00
18422fbe38 Order para pause, activate y terminate 2026-02-25 17:42:16 +01:00
f221035c8b Visualizacion via api de las operaciones pendientes 2026-02-25 17:23:22 +01:00
02c80cd503 Orders con endpoints para monitorizacion 2026-02-25 12:20:52 +01:00
c416114c50 Arrglos por el cambio de nombre 2026-02-24 12:44:19 +01:00
e329b36933 Orders para test y flujo de migraciones mas simple 2026-02-24 11:27:47 +01:00
5c64c84e2a Todos los test de orders pasan 2026-02-23 13:35:36 +01:00
fc319372be Integracion completa de las migraciones 2026-02-23 12:04:21 +01:00
12dae135b5 Scripts de inicio con migraciones 2026-02-20 10:59:15 +01:00
b208c9c301 Preparando proceso de despliegue local para que se parezca al de
desarrollo, problema de las migraciones
2026-02-20 10:47:28 +01:00
1583ae539e Organizadas las migraciones para el despliegue 2026-02-19 17:24:47 +01:00
b6ec37c339 cambio de nombre por proposito 2026-02-17 17:24:13 +01:00
459523666f Mejora migraciones con tabla de versiones 2026-02-17 17:22:20 +01:00
8427613114 Intento de migraciones con script generador 2026-02-17 13:46:16 +01:00
5d3465fd97 Test para todo el repositorio de orders 2026-02-17 09:33:51 +01:00
39a2622cb1 base de datos de orders con repositorio y test 2026-02-16 17:31:20 +01:00
0a42e4776d Merge branch 'main' into seguimiento-tareas 2026-02-13 10:57:54 +01:00
44fea21a56 Fix de api-key y mejora del control de versiones 2026-02-13 10:55:19 +01:00
46ac54f7ab Seguimiento de ordenes desde la ingesta 2026-02-11 12:19:16 +01:00
2c9bf9dd93 Mejora del commit anterior 2026-02-10 17:28:32 +01:00
19b2958a9c Intento de mejorar el proceso de validacion de los controladores 2026-02-10 17:26:04 +01:00
a39b84e107 Validaciones para los endpints 2026-02-10 15:57:03 +01:00
219 changed files with 10414 additions and 3731 deletions

23
.env
View File

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

6
.gitignore vendored
View File

@@ -16,7 +16,11 @@ node_modules
#!.yarn/cache
.pnp.*
# Certificados
*.pem
*.p12
*.key
dist/*
.env

View File

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

View File

@@ -12,13 +12,13 @@ La compañia a la que pertenece cada peticion y por tanto el servicio que lo va
## Decisiones pendientes
- [x] La capa worker según acción y la de operaciones de proveedores, se podrían unir en una sola con un enrutamiento por acción y compañía, pasando de tener claves `sim.[acción]` a `sim.[compañia].[acción]`. *Se ha aplicado el cambio ahora las routing keys tienen la estructura `sim.[compañia].[acción]`*
- [x] La estructura de RMQ se genera por medio del JSON, igual habría que definir cada cola en el worker que la consuma para poder añadir workers sin parar el RMQ. *Se ha aplicado el cambio, ahora solo se define en el json el broker principal para garantizar que exita sin servicios consumidores. Sin embargo tal como estan estructurdos los proyectos no es posible reiniciar solo un servicio*
- [x] La capa worker según acción y la de operaciones de proveedores, se podrían unir en una sola con un enrutamiento por acción y compañía, pasando de tener claves `sim.[acción]` a `sim.[compañia].[acción]`. _Se ha aplicado el cambio ahora las routing keys tienen la estructura `sim.[compañia].[acción]`_
- [x] La estructura de RMQ se genera por medio del JSON, igual habría que definir cada cola en el worker que la consuma para poder añadir workers sin parar el RMQ. _Se ha aplicado el cambio, ahora solo se define en el json el broker principal para garantizar que exita sin servicios consumidores. Sin embargo tal como estan estructurdos los proyectos no es posible reiniciar solo un servicio_
- [ ] Versionado de la API.
- [x] Método para sacar la compañía a partir del iccid, o buscar en la BDD si no es posible. *De momento es un objeto Map en el servicio de gateway*
- [ ] Cola de mensajes que no se han podido procesar. Distinguir según error de red; se reintenta; o error del propio mensaje; se envía a la cola de errores. v2 Se ha creado una cola de delay pero no se distingue el tipo de error, despues de n reintentos el mensaje va a la cola de dead-letter.
- [ ] Seguimiento de las peticiones de Objenious, por cada peticion hay qye hacer un seguimiento del request y de los mass action para saber si las activaciones han tenido exito. Habria que crear otra cola para consultar cada x tiempo o mejor un cron?
- [ ] Actualizar en la base de datos el estado de las peticiones de las sim y añadir el número de telefono cuando se activen o cuando se cumpla una accion.
- [x] Método para sacar la compañía a partir del iccid, o buscar en la BDD si no es posible. _De momento es un objeto Map en el servicio de gateway_
- [ ] Cola de mensajes que no se han podido procesar. Distinguir según error de red; se reintenta; o error del propio mensaje; se envía a la cola de errores. v2 Se ha creado una cola de delay pero no se distingue el tipo de error, después de n reintentos el mensaje va a la cola de dead-letter.
- [x] Seguimiento de las peticiones de Objenious, por cada peticion hay qye hacer un seguimiento del request y de los mass action para saber si las activaciones han tenido exito. Habria que crear otra cola para consultar cada x tiempo o mejor un cron?
- [x] Actualizar en la base de datos el estado de las peticiones de las sim y añadir el número de telefono cuando se activen o cuando se cumpla una accion.
## Versión con consumidores basados en la compañia
@@ -32,8 +32,15 @@ OBJENIOUS (33)2011a
## Diagrama de las colas de Rabbitmq
Actualmente la topologia de las colas consiste en un exchage principal que recibe todos los mensajes y los redistribuye en las colas de cada empresa y a la de logs. Para evitar reintentos de mensajes instantaneos, que podrian ser inutiles si algún servicio se ha caido, se ha añadido una cola de delay que alamcena los mesajes fallidos durante n segundos antes de ser reenviados al exchange principal. Si despues de n reintentos el mensaje sigue fallando se envia a la cola de dead-letter para ser procesado manualmente.
Actualmente la topología de las colas consiste en un exchage principal que recibe todos los mensajes y los redistribuye en las colas de cada empresa y a la de logs. Para evitar reintentos de mensajes instantáneos, que podrían ser inútiles si algún servicio se ha caído, se ha añadido una cola de delay que almacena los mensajes fallidos durante n segundos antes de ser reenviados al exchange principal. Si después de n reintentos el mensaje sigue fallando se envía a la cola de dead-letter para ser procesado manualmente.
![img](./imgs/diagrama-rabbit.png)
La decisión del numero de reintentos y la cola de dlx se hace en los servicios, con una configuracion global en shared.
La decisión del numero de reintentos y la cola de dlx se hace en los servicios, con una configuración global en shared.
## Puertos internos para comunicaciones entre sub-servicios
- **3000**: Gateway (sim-entrada-eventos)
- **3001**: Consumidor NOS (sim-consumidor-nos)
- **3002**: Consumidor Objenious (sim-consumidor-objenious)
- **3003**: Consumidor Alai (sim-consumidor-alai)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,16 +1,18 @@
# --- Release image ---
FROM node:22-alpine AS release
RUN apk --no-cache add git
WORKDIR /home/node/app
RUN corepack enable
COPY ./dist/packages ./packages
COPY ./.yarnrc.yml ./
COPY ./docs ./docs
# Para las migraciones
COPY ./deployment ./deployment
COPY ./package.json ./
# Force node-modules linker (no .yarnrc.yml in build context)
RUN echo 'nodeLinker: node-modules' > .yarnrc.yml
RUN yarn install
RUN mkdir -p dist && ln -sf ../packages dist/packages
@@ -19,4 +21,5 @@ COPY ./entrypoint.sh ./
RUN chmod +x entrypoint.sh
EXPOSE ${PORT:-3000}
ENTRYPOINT ["./entrypoint.sh"]

View File

@@ -6,6 +6,8 @@ networks:
external: true
internal:
driver: bridge
volumes:
rabbitmq_data:
services:
rabbitmq-sim-broker:
@@ -28,6 +30,7 @@ services:
entrypoint: ["bash", "/usr/local/bin/docker-entrypoint-wrapper.sh"]
command: ["rabbitmq-server"]
volumes:
- rabbitmq_data:/var/lib/rabbitmq
- ./rabbit/docker-entrypoint-wrapper.sh:/usr/local/bin/docker-entrypoint-wrapper.sh:ro
- ./rabbitmq_plugins/enabled_plugins:/etc/rabbitmq/enabled_plugins:ro
- ./rabbit/rabbitmq.conf:/etc/rabbitmq/rabbitmq.conf:ro
@@ -72,7 +75,10 @@ services:
- ${PORT}
volumes:
- ./.env:/home/node/app/.env:ro
- ./sim-consumidor-nos.env:/home/node/app/packages/sim-consumidor-nos/.env:ro
- ./sim-consumidor-alai.env:/home/node/app/packages/sim-consumidor-alai/.env:ro
- ./sim-consumidor-objenious.env:/home/node/app/packages/sim-consumidor-objenious/.env:ro
- ./wsaccess_alaisecure_com_cert_client_new.p12:/home/node/app/packages/sim-consumidor-alai/certificates/wsaccess_alaisecure_com_cert_client_new.p12:ro
- ./sim-objenious-cron.env:/home/node/app/packages/sim-objenious-cron/.env:ro
- ./obj.pem:/home/node/app/packages/sim-consumidor-objenious/obj.pem:ro
- ./obj.pem:/home/node/app/packages/sim-objenious-cron/obj.pem:ro

View File

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

View File

@@ -22,8 +22,8 @@ pipeline {
}
stage("🧱 Building") {
steps {
sh 'rm -rf dist/'
sh 'yarn run build'
sh 'rm -rf dist/'
sh 'yarn run build:prod'
}
}
stage("🏗 Deploying") {
@@ -38,14 +38,40 @@ pipeline {
cleanRemote: false,
execCommand: "mkdir -p $APP_REMOTE_PATH"
),
sshTransfer(
cleanRemote: false,
execCommand: "rm -rf $APP_REMOTE_PATH/dist"
),
sshTransfer(
cleanRemote: false,
execCommand: "ls -la $BASE_REMOTE_PATH/vault/savefamily/sf-sims/"
),
sshTransfer(
cleanRemote: false,
remoteDirectory: "$APP_REMOTE_PATH",
sourceFiles: "dist/**/*",
excludes: "dist/**/node_modules/**"
),
sshTransfer(
cleanRemote: false,
execCommand: "ln -sf $BASE_REMOTE_PATH/vault/savefamily/sf-sims/.env $APP_REMOTE_PATH/.env"
),
sshTransfer(
cleanRemote: false,
execCommand: "ln -sf $BASE_REMOTE_PATH/vault/savefamily/sf-sims/wsaccess_alaisecure_com_cert_client_new.p12 $APP_REMOTE_PATH/wsaccess_alaisecure_com_cert_client_new.p12"
),
sshTransfer(
cleanRemote: false,
execCommand: "ln -sf $BASE_REMOTE_PATH/vault/savefamily/sf-sims/sim-consumidor-objenious.env $APP_REMOTE_PATH/sim-consumidor-objenious.env"
),
sshTransfer(
cleanRemote: false,
execCommand: "ln -sf $BASE_REMOTE_PATH/vault/savefamily/sf-sims/sim-consumidor-nos.env $APP_REMOTE_PATH/sim-consumidor-nos.env"
),
sshTransfer(
cleanRemote: false,
execCommand: "ln -sf $BASE_REMOTE_PATH/vault/savefamily/sf-sims/sim-consumidor-alai.env $APP_REMOTE_PATH/sim-consumidor-alai.env"
),
sshTransfer(
cleanRemote: false,
execCommand: "ln -sf $BASE_REMOTE_PATH/vault/savefamily/sf-sims/sim-objenious-cron.env $APP_REMOTE_PATH/sim-objenious-cron.env"
@@ -57,14 +83,12 @@ pipeline {
sshTransfer(
cleanRemote: false,
remoteDirectory: "$APP_REMOTE_PATH",
sourceFiles: "dist/**/*",
excludes: "dist/**/node_modules/**"
sourceFiles: "docs/**/*",
),
sshTransfer(
cleanRemote: false,
remoteDirectory: "$APP_REMOTE_PATH",
sourceFiles: "deployment/database/**/*",
removePrefix: "deployment",
),
sshTransfer(
cleanRemote: false,
@@ -88,6 +112,11 @@ pipeline {
remoteDirectory: "$APP_REMOTE_PATH",
sourceFiles: "package.json",
),
sshTransfer(
cleanRemote: false,
remoteDirectory: "$APP_REMOTE_PATH",
sourceFiles: ".yarnrc.yml",
),
sshTransfer(
cleanRemote: false,
execCommand: "sh $APP_REMOTE_PATH/rebuild.sh"

View File

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

View File

@@ -0,0 +1,28 @@
# Stage base para coordinar las fases de build y ejecucion
FROM node:22-alpine AS base
# Hace falta para la herramienta de migraciones, cuando se publique se
# sustituira por el paquete de npm
RUN apk --no-cache add git
WORKDIR /usr/local/app
RUN corepack enable && \
corepack prepare yarn@4.12.0 --activate
COPY ./package.json ./yarn.lock ./
COPY ./packages ./packages
# copia el codigo en general
COPY tsconfig*.json ./
COPY .env* ./
COPY ./.yarnrc.yml ./
COPY ./docs ./docs
COPY ./deployment/local/docker/start.sh ./
# Copiar el archivo de migrations? porque ahora no creo que se esté lanzando nada
COPY ./deployment/database/migrations ./deployment/database/migrations
RUN yarn install && \
yarn cache clean && \
yarn build && \
chmod +x start.sh
EXPOSE ${PORT}
ENTRYPOINT [ "./start.sh" ]

View File

@@ -7,6 +7,7 @@ networks:
services:
rabbitmq-sim-broker:
container_name: rabbitmq-sim-broker
hostname: rabbitmq-sim
image: "rabbitmq:4.2.2-management"
ports:
- "5672:5672"
@@ -23,15 +24,17 @@ services:
RABBITMQ_DEFAULT_USER: ${RABBITMQ_USER}
RABBITMQ_DEFAULT_PASS: ${RABBITMQ_PASSWORD}
volumes:
- ./rabbitmq-data/:/var/lib/rabbitmq/
- ./rabbitmq_plugins/enabled_plugins:/etc/rabbitmq/enabled_plugins:ro
- ./deployment/rabbit/rabbitmq.conf:/etc/rabbitmq/rabbitmq.conf:ro
- ./deployment/rabbit/definitions.json:/etc/rabbitmq/definitions.json:ro
- ./deployment/local/rabbit/rabbitmq.conf:/etc/rabbitmq/rabbitmq.conf:ro
- ./deployment/local/rabbit/definitions.json:/etc/rabbitmq/definitions.json:ro
sim-gateway:
container_name: sim-gateway
sf-sims-api:
container_name: sf-sims-api
image: sf-sims-api
build:
context: ./
dockerfile: deployment/Dockerfile.dev
dockerfile: deployment/local/docker/Dockerfile.dev
args:
PORT: "${PORT:-3000}"
develop:
@@ -39,6 +42,9 @@ services:
- path: ./packages
action: sync
target: /usr/local/app/packages
- path: ./docs
action: sync
target: /usr/local/app/docs
- path: ./package.json
action: rebuild
ports:
@@ -46,19 +52,31 @@ services:
env_file:
- .env
restart: unless-stopped
healthcheck:
test:
[
"CMD-SHELL",
'node -e "fetch(''http://localhost:'' + (process.env.PORT || 3000) + ''/health'').then(r => { if (!r.ok) process.exit(1) }).catch(() => process.exit(1))"',
]
interval: 10s
timeout: 5s
retries: 5
start_period: 15s
depends_on:
rabbitmq-sim-broker:
condition: service_healthy
postgresql-sim:
condition: service_healthy
postgresql-sim:
container_name: postgresql-sim
image: postgres:16.1
env_file:
- .env
ports:
- "5432:${DEV_POSTGRES_PORT}"
- "${POSTGRES_PORT}:${POSTGRES_PORT}"
volumes:
- ./sql-data/:/var/lib/postgres/data
- ./deployment/database/init.sql:/docker-entrypoint-initdb.d/init.sql
healthcheck:
test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}"]
interval: 5s

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,32 @@
info:
name: Change External ID
type: http
seq: 7
http:
method: GET
url: "{{baseurl}}/v1/subscription/{{subscription}}?action=MODIFY"
params:
- name: action
value: MODIFY
type: query
body:
type: json
data: |-
{
"externalID":""
}
auth:
type: bearer
token: "{{alai_token}}"
runtime:
variables:
- name: subscription
value: asdasdasd
settings:
encodeUrl: true
timeout: 0
followRedirects: true
maxRedirects: 5

View File

@@ -0,0 +1,22 @@
info:
name: IMEI of subscription
type: http
seq: 5
http:
method: GET
url: "{{baseurl}}/v1/subscription/{{subscription}}/imei"
auth:
type: bearer
token: "{{alai_token}}"
runtime:
variables:
- name: subscription
value: SID1848557_TS1766417781101_0
settings:
encodeUrl: true
timeout: 0
followRedirects: true
maxRedirects: 5

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

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

View File

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

22
docs/sim-alai/SIM.yml Normal file
View File

@@ -0,0 +1,22 @@
info:
name: SIM
type: http
seq: 4
http:
method: GET
url: "{{baseurl}}/v1/sim/{{iccid}}"
auth:
type: bearer
token: "{{alai_token}}"
runtime:
variables:
- name: iccid
value: "8934909001500561503"
settings:
encodeUrl: true
timeout: 0
followRedirects: true
maxRedirects: 5

View File

@@ -0,0 +1,22 @@
info:
name: Subscription
type: http
seq: 4
http:
method: GET
url: "{{baseurl}}/v1/subscription/{{subscription}}"
auth:
type: bearer
token: "{{alai_token}}"
runtime:
variables:
- name: subscription
value: SID1776275_TS1759238704226_0
settings:
encodeUrl: true
timeout: 0
followRedirects: true
maxRedirects: 5

Binary file not shown.

View File

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

View File

@@ -0,0 +1,15 @@
name: prod
color: "#CE4F3B"
variables:
- name: baseurl
value: https://wsaccess.alaisecure.com/bssrest
- name: username
value: palomaibanez
- name: password
value: palomaibanez123
- secret: true
name: certPasswd
- name: brandId
value: savefamily
- name: alai_token
value: eyJhbGciOiJIUzM4NCJ9.eyJiciI6InNhdmVmYW1pbHkiLCJpcCI6Ijg4LjE1LjE1Ny4xNjciLCJzdWIiOiJwYWxvbWFpYmFuZXoiLCJzIjoiRVdTMTY0YWJhYWRlNjA3ZDAyIiwicG9zIjoic2F2ZWZhbWlseUNhYyIsImlkV3NVc2VyIjoiODYiLCJpc012bmEiOmZhbHNlLCJkb21haW4iOiJBbGFpfHNhdmVmYW1pbHkiLCJpYXQiOjE3NzgxNTEzMzYsImV4cCI6MTc3ODE2MjEzNn0.zCFBJJsa0Krc7n5vUFF00z9Tq7m0dRlCGzs2Od67jaLCCn-mnIyyU424PkazacRW

View File

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

File diff suppressed because one or more lines are too long

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,20 @@
meta {
name: Preactivate
type: http
seq: 2
}
get {
url: {{baseAlai}}/preactivate?iccid=8934909001400027654
body: none
auth: inherit
}
params:query {
iccid: 8934909001400027654
}
settings {
encodeUrl: true
timeout: 0
}

View File

@@ -0,0 +1,20 @@
meta {
name: Select SIM
type: http
seq: 1
}
get {
url: {{baseAlai}}/select/?iccid=8934909001500561503
body: none
auth: inherit
}
params:query {
iccid: 8934909001500561503
}
settings {
encodeUrl: true
timeout: 0
}

View File

@@ -0,0 +1,8 @@
meta {
name: Alai
seq: 14
}
auth {
mode: inherit
}

View File

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

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

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

View File

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

View File

@@ -0,0 +1,16 @@
meta {
name: Select
type: http
seq: 1
}
get {
url: {{baseurl}}/portugal/select
body: none
auth: inherit
}
settings {
encodeUrl: true
timeout: 0
}

View File

@@ -0,0 +1,8 @@
meta {
name: Nos
seq: 15
}
auth {
mode: inherit
}

View File

@@ -0,0 +1,26 @@
meta {
name: France Suspended Lines
type: http
seq: 16
}
get {
url: {{baseurl}}/france/lines?status=SUSPENDED&limit=100
body: none
auth: inherit
}
params:query {
status: SUSPENDED
limit: 100
}
vars:pre-request {
iccid: 8933201125065160331
~baseurl: http://localhost:3002
}
settings {
encodeUrl: true
timeout: 0
}

View File

@@ -0,0 +1,21 @@
meta {
name: France Suspended Time
type: http
seq: 15
}
get {
url: {{baseurl}}/france/lines/{{iccid}}/suspended-time
body: none
auth: inherit
}
vars:pre-request {
iccid: 8933201125065160331
~baseurl: http://localhost:3002
}
settings {
encodeUrl: true
timeout: 0
}

View File

@@ -0,0 +1,8 @@
meta {
name: Objenious
seq: 16
}
auth {
mode: inherit
}

View File

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

View File

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

View File

@@ -0,0 +1,16 @@
meta {
name: Orders by message_id
type: http
seq: 12
}
get {
url: {{baseurl}}/orders/message_id/019dbeaf-8abb-7783-8b51-94fbd9f0b0df
body: none
auth: inherit
}
settings {
encodeUrl: true
timeout: 0
}

View File

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

View File

@@ -1,7 +1,7 @@
meta {
name: Pause
type: http
seq: 1
seq: 5
}
post {
@@ -15,7 +15,7 @@ params:query {
}
body:form-urlencoded {
iccid: 8933201125065160414
iccid: 8933201125065160331
}
settings {

View File

@@ -1,7 +1,7 @@
meta {
name: Preactivate
type: http
seq: 1
seq: 6
}
post {
@@ -15,7 +15,9 @@ params:query {
}
body:form-urlencoded {
iccid: 8933201125065160380
iccid: 8934909001500954922
offer: mensual
orderId: test
}
settings {

View File

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

20
docs/sim-api/Select.bru Normal file
View File

@@ -0,0 +1,20 @@
meta {
name: Select
type: http
seq: 13
}
get {
url: {{baseurl}}/sim/select?iccid=8935103196306448300
body: none
auth: inherit
}
params:query {
iccid: 8935103196306448300
}
settings {
encodeUrl: true
timeout: 0
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,24 @@
meta {
name: Alarm by id
type: http
seq: 2
}
get {
url: {{baseUrl}}alarms/{{alarmId}}
body: none
auth: bearer
}
auth:bearer {
token: {{ws-access-token-partenaire}}
}
vars:pre-request {
alarmId: 2439
}
settings {
encodeUrl: true
timeout: 0
}

View File

@@ -0,0 +1,28 @@
meta {
name: Alerts
type: http
seq: 3
}
get {
url: {{baseUrl}}alarms/alerts?pageNumber=100
body: none
auth: bearer
}
params:query {
pageNumber: 100
}
auth:bearer {
token: {{ws-access-token-partenaire}}
}
vars:pre-request {
alarmId: 2439
}
settings {
encodeUrl: true
timeout: 0
}

View File

@@ -0,0 +1,20 @@
meta {
name: All Alarms
type: http
seq: 1
}
get {
url: {{baseUrl}}alarms
body: none
auth: bearer
}
auth:bearer {
token: {{ws-access-token-partenaire}}
}
settings {
encodeUrl: true
timeout: 0
}

View File

@@ -0,0 +1,8 @@
meta {
name: Alarms
seq: 21
}
auth {
mode: inherit
}

View File

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

View File

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

View File

@@ -0,0 +1,38 @@
meta {
name: Consumption details
type: http
seq: 21
}
get {
url: https://api-getway.objenious.com/ws/diagXL/massHistories
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: ["8933201125068889373"]
}
vars:pre-request {
~id: 5187320
}
settings {
encodeUrl: true
timeout: 0
}

View File

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

View File

@@ -0,0 +1,41 @@
meta {
name: Line by iccid
type: http
seq: 22
}
get {
url: https://api-getway.objenious.com/ws/lines?pageSize=1000&simStatus=ACTIVATED
body: formUrlEncoded
auth: bearer
}
params:query {
pageSize: 1000
simStatus: ACTIVATED
~identifier.identifierType: ICCID
~identifier.identifiers: 8933201125065160455
}
auth:bearer {
token: {{ws-access-token-partenaire}}
}
body:json {
{
"identifier": {
"identifiers": ["8933201124059175967"],
"identifierType": "ICCID"
}
}
}
body:form-urlencoded {
~identifier.identifierType: "ICCID"
~identifier.identifiers: ["8933201124059175967"]
}
settings {
encodeUrl: true
timeout: 0
}

View File

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

1843
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

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

View File

@@ -1,5 +1,4 @@
export const env = {
ENVIRONMENT: process.env.ENVIORMENT,
POSTGRES_USER: process.env.POSTGRES_USER,
POSTGRES_PASSWORD: process.env.POSTGRES_PASSWORD,
POSTGRES_PORT: process.env.POSTGRES_PORT,

View File

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

View File

@@ -0,0 +1,43 @@
import { AlaiRepository } from "#infrastructure/AlaiRepository.js";
import { JWTToken } from "sim-shared/domain/JWT.js";
import { JWTProvider } from "sim-shared/infrastructure/HTTPClient.js";
import { httpsAgent } from "#config/httpsAgent.js";
export class AlaiTokenManager implements JWTProvider<{}> {
isRefreshing: boolean = false;
authToken: JWTToken<{}> | undefined;
private async getNewAuthToken() {
// TODO: Si no funcionase hay que reprogramar los mensajes para ser
// consumidos mas tarde.
const res = await AlaiRepository.login(httpsAgent);
if (res.error != undefined) {
console.error("Error obteniendo el token de ALAI", res.error)
} else {
console.log("Obtenido token de ALAI: ", res)
this.authToken = new JWTToken(res.data.accessToken)
}
}
public tryRefreshToken(): Promise<JWTToken<{}>> {
// En Alai no existe el concepto de refresh, se solicita otro token nuevo
return this.getAccessToken()
};
public async getAccessToken(): Promise<JWTToken<{}>> {
// Caso 1: El token actual es valido
if (this.authToken != undefined && !this.authToken.isExpired()) {
return this.authToken
} else {
// Caso 2: El token actual no existe o ha expirado
await this.getNewAuthToken()
}
// Si después de todo no se ha generado el token es un error catastrofico
if (this.authToken == undefined) throw new Error("Error obteniendo tokens de auth")
return this.authToken
};
}

View File

@@ -0,0 +1,50 @@
import { JWTToken } from "sim-shared/domain/JWT.js";
import { JWTProvider } from "sim-shared/infrastructure/HTTPClient.js";
import { LegacyJWTTokenRepository } from "#infrastructure/LegacyJWTTokensRepository.js";
import { env } from "#config/env/env.js";
const tokenDir = String(env.ALAI_CERTIFICATES_DIR)
const tokenFile = ".debugToken"
/**
* Usa un token guardado a mano en archivo para no gastar tokens de Alai
*/
export class DebugTokenManager implements JWTProvider<{}> {
isRefreshing: boolean = false;
authToken: JWTToken<{}> | undefined;
private async getNewAuthToken() {
// TODO: Si no funcionase hay que reprogramar los mensajes para ser
// consumidos mas tarde.
const res = LegacyJWTTokenRepository.getTokenFromFile(tokenDir, tokenFile)
if (res.error != undefined) {
console.error("Error obteniendo el token de ALAI", res.error)
} else {
this.authToken = new JWTToken(res.data)
console.log("[d] Token DEBUG: ", this.authToken)
}
}
public tryRefreshToken(): Promise<JWTToken<{}>> {
// En Alai no existe el concepto de refresh, se solicita otro token nuevo
return this.getAccessToken()
};
public async getAccessToken(): Promise<JWTToken<{}>> {
// Caso 1: El token actual es valido
if (this.authToken != undefined && !this.authToken.isExpired()) {
return this.authToken
} else {
// Caso 2: El token actual no existe o ha expirado
await this.getNewAuthToken()
}
// Si después de todo no se ha generado el token es un error catastrofico
if (this.authToken == undefined) throw new Error("Error obteniendo tokens de auth")
return this.authToken
};
}

View File

@@ -0,0 +1,225 @@
import { ConsumeMessage } from "amqplib";
import { Request, Response } from "express"
import { SimAlaiUsecases } from "./SimAlai.usecases.js";
import { EventBus } from "sim-shared/domain/EventBus.port.js";
import { Result } from "sim-shared/domain/Result.js";
import { SimEvents } from "sim-shared/domain/SimEvents.js";
import { iccidValidator } from "./httpValidators.js";
import { alaiSimToCommonSim } from "#domain/transformers.js";
type ErrorUsecase = {
msg: string,
stackTrace?: string
}
export class SimAlaiController {
constructor(
private uscases: SimAlaiUsecases,
private eventBus: EventBus,
) {
}
private validateMsg(msg: ConsumeMessage | null) {
if (msg == undefined) return false;
const msgData = this.decodeMsg(msg) as SimEvents.general
if (msgData == undefined || msgData.payload == undefined) throw new Error("Mensaje invalido")
return msgData;
}
private decodeMsg(msg: ConsumeMessage): object | undefined {
if (msg.content == undefined) {
console.warn('[Sim.controller] Mensaje vacío');
return undefined;
}
try {
// Convertir el Buffer a String (UTF-8)
const contentJson = JSON.parse(Buffer.from(msg.content).toString('utf8'))
return contentJson;
} catch (error) {
console.error('Error al decodificar JSON:', error);
console.error(Buffer.from(msg.content).toString(("utf8")))
// Aquí podrías decidir devolver el string crudo o null
return undefined;
}
}
/**
* Metodo duplicado se puede generalizar la a una clase sharedController con las funciones basicas
* TODO: meter un check de 429
*/
private async tryUseCase<T extends any>
(msg: ConsumeMessage, usecase: () => Promise<Result<ErrorUsecase, T>>): Promise<Result<ErrorUsecase, T>> {
try {
const result = await usecase()
if (result.error == undefined) {
await this.eventBus.ack(msg)
return result
} else {
console.error("Error procesando el caso de uso (Alai)", result.error)
this.eventBus.nack(msg)
return result
}
} catch (e) {
console.error("Error general procesando el caso de uso (Alai)")
this.eventBus.nack(msg)
return {
error: {
msg: String(e),
stackTrace: String(e)
}
}
}
}
public activate() {
return async (msg: ConsumeMessage) => {
console.log("[i] Evento activate ", msg.fields)
const data = this.validateMsg(msg) as SimEvents.activation
const iccid = data.payload.iccid
const correlation_id = data.headers?.message_id
const externalId = data.payload.orderId
const res = await this.tryUseCase(msg, this.uscases.activate({
iccid: iccid,
correlation_id: correlation_id,
}))
return res;
}
}
public preactivate() {
return async (msg: ConsumeMessage) => {
console.log("[i] Evento preactivate ", msg)
const data = this.validateMsg(msg) as SimEvents.preActivation
const iccid = data.payload.iccid
const correlation_id = data.headers?.message_id
const externalId = data.payload.orderId
console.log("MSG:", data, data.headers)
const res = await this.tryUseCase(msg, this.uscases.preactivate({
iccid: iccid,
correlation_id: correlation_id,
externalId: externalId
}))
return res;
}
}
public suspend() {
return async (msg: ConsumeMessage) => {
console.log("Evento suspend ", msg.fields)
const data = this.validateMsg(msg) as SimEvents.suspend
const iccid = data.payload.iccid
const correlation_id = data.headers?.message_id
const res = await this.tryUseCase(msg, this.uscases.suspend({
iccid: iccid,
correlation_id: correlation_id
}))
return res;
}
}
public reActivate() {
return async (msg: ConsumeMessage) => {
console.log("Evento reActivate ", msg.fields)
const data = this.validateMsg(msg) as SimEvents.reActivation
const iccid = data.payload.iccid
const correlation_id = data.headers?.message_id
const res = await this.tryUseCase(msg, this.uscases.reactivate({
iccid: iccid,
correlation_id: correlation_id
}))
return res;
}
}
public terminate() {
return async (msg: ConsumeMessage) => {
console.log("Evento reActivate ", msg.fields, msg)
const data = this.validateMsg(msg) as SimEvents.reActivation
const iccid = data.payload.iccid
const correlation_id = data.headers?.message_id
const res = await this.tryUseCase(msg, this.uscases.terminate({
iccid: iccid,
correlation_id: correlation_id
}))
return res;
}
}
/**
* Select especificamente por REST para evitar pasar por las colas.
* La respuesta es instantanea no se tiene que registrar como operación.
*/
public selectREST() {
return async (req: Request, res: Response) => {
const { query } = req
const body = { iccid: query.iccid as string }
console.log("Evento select", body)
const validateBody = iccidValidator.validate(body);
if (validateBody.error != undefined) {
res.status(422).json(validateBody)
return;
}
const iccid: string | string[] = body.iccid
if (Array.isArray(iccid)) {
// TODO: Automatizar la paginacion
//const usecaseRes = this.uscases.selectMany({ iccid })
} else {
//const usecaseRes = await this.uscases.selectOne(iccid)
const usecaseRes = await this.uscases.selectCompleteSim(iccid)
if (usecaseRes.error != undefined) {
res.status(500).json(usecaseRes)
return;
} else {
const { sim, subscription, imei } = usecaseRes.data
const simData = alaiSimToCommonSim(sim, subscription, imei)
res.send(simData)
return;
}
}
res.status(200).json(validateBody)
}
}
/**
public selectPageREST() {
return async (req: Request, res: Response) => {
const { offset, limit, filter, orderBy } = req.query
const params = {
offset: (offset != undefined) ? Number(offset) : undefined,
limit: (limit != undefined) ? Number(limit) : undefined,
filter: (filter != undefined) ? String(filter) : undefined,
orderBy: (orderBy != undefined) ? String(orderBy) : undefined
}
const usecaseRes = await this.uscases.selectPage(params)
if (usecaseRes.error != undefined) {
res.status(500).json(usecaseRes)
return;
} else {
res.status(200).send(usecaseRes.data)
return;
}
}
}
**/
}

View File

@@ -0,0 +1,79 @@
/**
* Dirige cada mensaje dependiendo de el tipo de acción que contenga
* Podría hacerse con varias colas, pero así se controla mejor que
* las operaciones se hagan de 1 en 1.
*/
import { ConsumeMessage } from "amqplib";
import { EventBus } from "sim-shared/domain/EventBus.port.js";
import { Result } from "sim-shared/domain/Result.js";
import { SimAlaiController } from "./SimAlai.controller.js";
type FuncType = ((m: ConsumeMessage) => Promise<Result<{ msg: string, stackTrace?: string }, any>>)
export class SimAlaiRouter {
private readonly routes: Map<string, FuncType>;
// WIP
constructor(
private readonly simController: SimAlaiController,
private readonly eventBus: EventBus
) {
this.routes = new Map<string, FuncType>([
["activate", this.simController.activate()],
["pause", this.simController.suspend()],
["reactivate", this.simController.reActivate()],
["cancel", this.simController.terminate()],
["preactivate", this.simController.preactivate()]
]);
}
/**
* Enruta el mensaje a la acción correspondiente basándose en la routing key
* TODO: No estoy seguro que deba meter el nack aqui
* - De moemento el ack-nack se gestiona en los controller, por si acaso hay casos
* limite en
*/
public route = async (msg: ConsumeMessage | null): Promise<void> => {
if (!msg) {
console.error("[Router] Mensaje vacío");
return;
}
const action = this.extractAction(msg);
if (!action) {
console.error("[Router] La routing key no tiene una acción definida", msg.fields.routingKey);
this.eventBus.nack(msg)
return;
}
const handler = this.routes.get(action);
if (!handler) {
console.error(`[Router] La acción '${action}' no tiene un controlador asociado`);
this.eventBus.nack(msg)
return;
}
try {
console.log("[Router] Ejecutando operación:", action);
// El controlador devuelve una función (thunk) que debe ser ejecutada
const executeParams = handler(msg);
if (typeof executeParams === "function") {
const res = await executeParams;
}
} catch (error) {
console.error(`[Router] Error al ejecutar la operación '${action}':`, error);
this.eventBus.nack(msg)
}
};
private extractAction(msg: ConsumeMessage): string | undefined {
// Se asume que la acción está en la tercera posición: domain.compañia.accion
return msg.fields.routingKey.split(".")[2];
}
}

View File

@@ -0,0 +1,302 @@
/**
* Documentación de referencia:
* https://pelion-help.iot-x.com/nos/en-US/Content/API/APIReference/API%20Reference.htm?tocpath=_____7
*
* En nos el correlation_id ya va a ser obligatorio en todos los mensajes
*
* TODO:
* - Control de errores más preciso
*
*/
import { AlaiAPI } from "#domain/AlaiAPI.js";
import { AlaiRepository } from "#infrastructure/AlaiRepository.js";
import { ErrorOrderDTO, FinishOrderDTO, UpdateOrderDTO } from "sim-shared/domain/Order.js";
import { Result } from "sim-shared/domain/Result.js";
import { HttpClient } from "sim-shared/infrastructure/HTTPClient.js";
import { OrderRepository } from "sim-shared/infrastructure/OrderRepository.js";
export class SimAlaiUsecases {
constructor(
private httpClient: HttpClient,
private alaiRepository: AlaiRepository,
private orderRepository: OrderRepository
) {
}
private async setRunning(correlation_id: string) {
const updateData: UpdateOrderDTO = {
new_status: "running",
correlation_id: correlation_id
}
const order = await this.orderRepository.updateOrder(updateData)
return order
}
private async setFinished(correlation_id: string) {
// En NOS el updateOrder se hace con el correlation_id que viene en la cabecera del
// mensaje consumido
const updateData: FinishOrderDTO = {
correlation_id: correlation_id
}
const order = await this.orderRepository.finishOrder(updateData)
return order
}
private async setFailed(correlation_id: string, reason: string, stackTrace?: string) {
// En NOS el updateOrder se hace con el correlation_id que viene en la cabecera del
// mensaje consumido
const updateData: ErrorOrderDTO = {
status: "failed",
correlation_id: correlation_id,
reason: reason,
error: reason,
stackTrace: stackTrace
}
console.log("SET FAILED DATA:", updateData)
const order = await this.orderRepository.errorOrder(updateData)
console.log("SET FAILED RES:", order)
return order
}
/**
* Gestiona el ciclo de vida de una petición. No aplica
* a peticiones de lectura (no pasan por la cola y no generan un order)
*/
public usecaseTemplate<T, R>(
func: (_: T) => Promise<Result<{ msg: string, stackTrace?: string }, R>>,
args: T,
correlation_id?: string | undefined
) {
return async (): Promise<Result<{ msg: string, stackTrace?: string }, R>> => {
// Operacion pending -> running
if (correlation_id != undefined)
this.setRunning(correlation_id)
.then()
.catch(e => console.error("Error actualizando el order", e))
else
console.warn("[!] Se ha lanzado una caso de uso sin correlation_id")
try {
const res = await func(args)
if (res.error != undefined) {
console.log("Error peticion: ", res, correlation_id)
if (correlation_id != undefined)
this.setFailed(correlation_id, res.error.msg, res.error.stackTrace)
.then(e => console.log("failed", e))
.catch(e => console.error(e))
return res;
} else {
if (correlation_id != undefined)
this.setFinished(correlation_id).then()
return res;
}
} catch (e) {
if (correlation_id != undefined)
this.setFailed(correlation_id, "Error general de operacion de SIM (NOS) ", String(e)).then()
return {
error: {
msg: "Error general de operacion de SIM (NOS) " + String(e)
}
}
}
}
}
public activate(args: {
iccid: string,
correlation_id: string | undefined,
}) {
return this.usecaseTemplate(async (iccid /*iccid*/) => {
const sim = await this.alaiRepository.getSimByICCID(iccid)
if (sim.error != undefined) {
return sim
}
if (sim.data == undefined) {
return {
error: {
msg: `La sim ${iccid} no se ha encontrado`
}
}
}
const subscriptionId = sim.data.subscription!.id
if (subscriptionId == undefined) {
return {
error: {
msg: `La sim ${iccid} no tiene un id de subscripción`
}
}
}
const activationRes = await this.alaiRepository.activateSubscription(subscriptionId)
return activationRes
}, args.iccid, args.correlation_id)
}
public preactivate(args: {
iccid: string,
correlation_id: string | undefined,
externalId: string | undefined // Por compatibilidad
}) {
const inputargs = {
iccid: args.iccid,
externalId: args.externalId
}
return this.usecaseTemplate(async (args) => {
const order = await this.alaiRepository.createOrder()
if (order.error != undefined) {
return order
}
const orderId = order.data.id
const reserve = await this.alaiRepository.createReserve(orderId, args.iccid)
if (reserve.error != undefined) {
return reserve
}
const applyOrder = await this.alaiRepository.applyOrder(orderId)
if (applyOrder.error != undefined) {
// TODO: gestion del error
// reusar el orderId
return applyOrder
}
const preactivatedSim = await this.alaiRepository.getSimByICCID(args.iccid)
if (preactivatedSim.error != undefined) {
return preactivatedSim
}
// TODO: Controlar sim no encotrada (No deberia pasar)
const subscriptionId = preactivatedSim.data!.subscription!.id
if (args.externalId) {
const externalIdAdded = await this.alaiRepository.changeExternalId(subscriptionId, args.externalId)
if (externalIdAdded.error != undefined) {
return externalIdAdded
}
}
// En connections acaba buscando el numero.
const subscription = await this.alaiRepository.getSubscriptionById(subscriptionId)
return subscription
}, inputargs, args.correlation_id)
}
public suspend(args: {
iccid: string,
correlation_id: string | undefined
}) {
return this.usecaseTemplate(async (args) => {
const subscription = await this.alaiRepository.getSimByICCID(args.iccid)
if (subscription.error != undefined) {
return subscription
}
// TODO: Controlar que no se encuentre la subscription
const subscriptionid = subscription.data?.subscription?.id
const suspension = this.alaiRepository.pauseSubscription(subscriptionid!)
return suspension
}, args, args.correlation_id)
}
public reactivate(args: {
iccid: string,
correlation_id: string | undefined
}) {
return this.usecaseTemplate(async (args) => {
const subscription = await this.alaiRepository.getSimByICCID(args.iccid)
if (subscription.error != undefined) {
return subscription
}
const subscriptionid = subscription.data?.subscription?.id
// TODO: Controlar que no se encuentre la subscription
const suspension = this.alaiRepository.unPauseSubscription(subscriptionid!)
return suspension
}, args, args.correlation_id)
}
public terminate(args: {
iccid: string,
correlation_id: string | undefined
}) {
return this.usecaseTemplate(async (args) => {
const subscription = await this.alaiRepository.getSimByICCID(args.iccid)
if (subscription.error != undefined) {
return subscription
}
// TODO: Controlar que no se encuentre la subscription
const suspension = this.alaiRepository.terminateSubscription(subscription.data!.id)
return suspension
}, args, args.correlation_id)
}
public async selectOne(iccid: string) {
const sim = await this.alaiRepository.getSimByICCID(iccid)
return sim
}
/**
* Para sacar los datos de una liena hay que sacar sim -> subscripcion -> imei
* son 3 llamadas distintas.
*/
public async selectCompleteSim(iccid: string): Promise<Result<{ msg: string, stackTrace?: string }, {
sim: AlaiAPI.Sim,
subscription?: AlaiAPI.Subscription,
imei?: AlaiAPI.GetImeiSubscriptionDTO
}>> {
const sim = await this.alaiRepository.getSimByICCID(iccid)
if (sim.error != undefined) {
return sim
}
if (sim.data == undefined) {
return {
error: {
msg: `La sim ${iccid} no se ha encontrado`
}
}
}
// En este caso la tarjeta no se ha preactivado, por lo que no tiene subscripcion
if (sim.data.subscription == undefined) {
return {
data: {
sim: sim.data,
subscription: undefined,
imei: undefined
}
}
}
const subscriptionId = sim.data.subscription.id
const subscription = await this.alaiRepository.getSubscriptionById(subscriptionId)
if (subscription.error != undefined) {
return subscription
}
const imei = await this.alaiRepository.getImeiFromSubscription(subscriptionId)
if (imei.error != undefined) {
return imei
}
return {
data: {
sim: sim.data!,
subscription: subscription.data!,
imei: imei.data!
}
}
}
}

View File

@@ -0,0 +1,55 @@
import fs from "fs";
import path from "path";
import { Result } from "sim-shared/domain/Result.js";
export type P12Cert = {
cainfo: string,
p12cert: string
}
export type SSLCert = {
cainfo: string,
sslcert: string,
keypem: string
}
/**
* TODO:
* - Se ha usado https.Agent en su lugar, eliminar si no se usa
*/
export class SSLCertificateLoader {
constructor(
private certificatesDir: string,
) {
}
public loadCertificatesP12(caFile: string, certFile: string): Result<string, P12Cert> {
try {
const cainfo = fs.readFileSync(path.resolve(this.certificatesDir, caFile)).toString();
const p12cert = fs.readFileSync(path.resolve(this.certificatesDir, certFile)).toString();
return { data: { cainfo, p12cert } };
} catch (e) {
console.error("[x] Error cargando los certificados P12", e)
return {
error: String(e)
}
}
}
public loadCertificatesSSL(caFile: string, certFile: string, keyFile: string): Result<string, SSLCert> {
try {
const cainfo = fs.readFileSync(path.resolve(this.certificatesDir, caFile)).toString();
const sslcert = fs.readFileSync(path.resolve(this.certificatesDir, certFile), { encoding: null }).toString();
const keypem = fs.readFileSync(path.resolve(this.certificatesDir, keyFile), { encoding: null }).toString();
return { data: { cainfo, sslcert, keypem } };
} catch (e) {
console.error("[x] Error cargando los certificados SSL", e)
return {
error: String(e)
}
}
}
}

View File

@@ -0,0 +1,39 @@
import { BodyValidator, Validator } from "sim-shared/aplication/BodyValidator.js";
const iccidNotNull = <Validator<{ iccid: unknown }>>{
field: "iccid",
errorMsg: "El iccid no está definido",
validationFunc: (a: { iccid: unknown }) => {
return (a.iccid != null && a.iccid != undefined)
}
}
const iccidValueOrArray = <Validator<{ iccid: unknown }>>{
field: "iccid",
errorMsg: "El iccid debe de ser un único valor o una lista",
validationFunc: (a: { iccid: unknown }) => {
return (typeof a.iccid == "string" || Array.isArray(a.iccid))
}
}
const iccidLongitudValidator = <Validator<{ iccid: string | string[] }>>{
field: "iccid",
errorMsg: "La longitud del iccid/s es incorrecta debera ser de 19 caracteres",
validationFunc: (a: { iccid: string | string[] }) => {
if (Array.isArray(a.iccid)) {
const res = (a.iccid as string[]).filter(e => e.length != 19)
if (res.length > 0) return false;
} else {
return (a.iccid as string).length == 19
}
},
}
export const iccidValidator = new BodyValidator<{ iccid: string | string[] }>(
[
iccidNotNull,
iccidValueOrArray,
iccidLongitudValidator,
]
)

View File

@@ -0,0 +1 @@
eyJhbGciOiJIUzM4NCJ9.eyJiciI6InNhdmVmYW1pbHkiLCJpcCI6Ijg4LjE1LjE1Ny4xNjciLCJzdWIiOiJwYWxvbWFpYmFuZXoiLCJzIjoiRVdTMTY3MzRhYTM2MDY1M2EwIiwicG9zIjoic2F2ZWZhbWlseUNhYyIsImlkV3NVc2VyIjoiODYiLCJpc012bmEiOmZhbHNlLCJkb21haW4iOiJBbGFpfHNhdmVmYW1pbHkiLCJpYXQiOjE3Nzg2ODQ0NjIsImV4cCI6MTc3ODY5NTI2Mn0.wMWgjaOErm5clang7ErYzREU56okgpXWzq1zihT4lOfUDRQ005r-nCHJu7rpilj1

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