4 Commits

Author SHA1 Message Date
7c2e86779a Pausa 2026-04-07 10:08:21 +02:00
1780cf1743 Pausa 2026-04-07 10:06:45 +02:00
10104c202f Merge branch 'main' into alarmas-objenious 2026-04-07 09:05:39 +02:00
ef8222ae30 Tipos y plantill 2026-03-24 13:25:03 +01:00
192 changed files with 1586 additions and 7104 deletions

29
.env Normal file
View File

@@ -0,0 +1,29 @@
PORT=3000
API_HOSTNAME=0.0.0.0
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_HOST=postgresql-sim
POSTGRES_HOST=localhost
POSTGRES_DB=postgres
POSTGRES_DATABASE=postgres
POSTGRES_PORT=5433
POSTGRES_USER=postgres
POSTGRES_PASSWORD=1234
# Para el postgres local para generar el script de resultado de migraciones
PGHOST=localhost
PGUSER=alvar
PGPASSWORD=alvar
PGPORT=5433

6
.gitignore vendored
View File

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

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, 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.
- [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.
## Versión con consumidores basados en la compañia
@@ -32,15 +32,8 @@ OBJENIOUS (33)2011a
## Diagrama de las colas de Rabbitmq
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.
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.
![img](./imgs/diagrama-rabbit.png)
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)
La decisión del numero de reintentos y la cola de dlx se hace en los servicios, con una configuracion global en shared.

View File

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

View File

@@ -1,32 +0,0 @@
/**
* 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

@@ -8,8 +8,6 @@ RUN corepack enable
COPY ./dist/packages ./packages
COPY ./.yarnrc.yml ./
COPY ./docs ./docs
# Para las migraciones
COPY ./deployment ./deployment
COPY ./package.json ./

View File

@@ -6,8 +6,6 @@ networks:
external: true
internal:
driver: bridge
volumes:
rabbitmq_data:
services:
rabbitmq-sim-broker:
@@ -30,7 +28,6 @@ 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
@@ -75,10 +72,7 @@ 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,6 +1,4 @@
#!/bin/sh
cd /home
cd /home/node/app
yarn migrate
yarn start
cd /home/node/app && yarn start

View File

@@ -23,7 +23,7 @@ pipeline {
stage("🧱 Building") {
steps {
sh 'rm -rf dist/'
sh 'yarn run build:prod'
sh 'yarn run build'
}
}
stage("🏗 Deploying") {
@@ -38,40 +38,14 @@ 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"
@@ -80,6 +54,12 @@ pipeline {
cleanRemote: false,
execCommand: "ln -sf $BASE_REMOTE_PATH/vault/savefamily/sf-sims/obj.pem $APP_REMOTE_PATH/obj.pem"
),
sshTransfer(
cleanRemote: false,
remoteDirectory: "$APP_REMOTE_PATH",
sourceFiles: "dist/**/*",
excludes: "dist/**/node_modules/**"
),
sshTransfer(
cleanRemote: false,
remoteDirectory: "$APP_REMOTE_PATH",
@@ -89,6 +69,7 @@ pipeline {
cleanRemote: false,
remoteDirectory: "$APP_REMOTE_PATH",
sourceFiles: "deployment/database/**/*",
removePrefix: "deployment",
),
sshTransfer(
cleanRemote: false,

View File

@@ -7,9 +7,7 @@
{
"name": "RABBITMQ_USER_PLACEHOLDER",
"password": "RABBITMQ_PASSWORD_PLACEHOLDER",
"tags": [
"administrator"
]
"tags": ["administrator"]
}
],
"vhosts": [

View File

@@ -7,7 +7,6 @@ networks:
services:
rabbitmq-sim-broker:
container_name: rabbitmq-sim-broker
hostname: rabbitmq-sim
image: "rabbitmq:4.2.2-management"
ports:
- "5672:5672"
@@ -24,7 +23,6 @@ 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/local/rabbit/rabbitmq.conf:/etc/rabbitmq/rabbitmq.conf:ro
- ./deployment/local/rabbit/definitions.json:/etc/rabbitmq/definitions.json:ro

View File

@@ -1,32 +0,0 @@
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

@@ -1,22 +0,0 @@
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

View File

@@ -1,51 +0,0 @@
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

@@ -1,16 +0,0 @@
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

View File

@@ -1,22 +0,0 @@
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

@@ -1,22 +0,0 @@
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

View File

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

View File

@@ -1,15 +0,0 @@
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

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

View File

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

View File

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

View File

@@ -1,20 +0,0 @@
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

@@ -1,20 +0,0 @@
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

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

View File

@@ -1,7 +1,7 @@
meta {
name: Cancel
type: http
seq: 4
seq: 1
}
post {
@@ -11,7 +11,7 @@ post {
}
body:form-urlencoded {
iccid: 8933201125068889894
iccid: 8933201125068886692
}
settings {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,26 +0,0 @@
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

@@ -1,21 +0,0 @@
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

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

View File

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

View File

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

View File

@@ -1,45 +0,0 @@
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: 5
seq: 1
}
post {
@@ -15,7 +15,7 @@ params:query {
}
body:form-urlencoded {
iccid: 8933201125065160331
iccid: 8933201125065160414
}
settings {

View File

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

View File

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

View File

@@ -1,20 +0,0 @@
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

@@ -1,7 +1,7 @@
meta {
name: Test Order
type: http
seq: 10
seq: 9
}
post {

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,20 +0,0 @@
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

@@ -1,23 +0,0 @@
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

View File

@@ -1,25 +0,0 @@
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

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

View File

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

View File

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

View File

@@ -1,22 +0,0 @@
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

@@ -1,22 +0,0 @@
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

@@ -1,22 +0,0 @@
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

@@ -1,22 +0,0 @@
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

@@ -1,24 +0,0 @@
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

@@ -1,28 +0,0 @@
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

@@ -1,20 +0,0 @@
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

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

View File

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

View File

@@ -1,38 +0,0 @@
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

@@ -1,41 +0,0 @@
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=5363116
url: {{actionsUrl}}/massActions?massActionId=5192767
body: formUrlEncoded
auth: bearer
}
params:query {
massActionId: 5363116
massActionId: 5192767
~identifier.identifierType: ICCID
~identifier.identifiers: 8933201125065160463,8933201125065160422
}

View File

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

View File

@@ -1,4 +1,5 @@
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

@@ -0,0 +1 @@
PORT=3001

View File

@@ -0,0 +1,18 @@
import { Request, Response } from "express"
export class AlarmsController {
constructor() {
// Alarm usecases
}
public recibe() {
return (req: Request, res: Response) => {
const body = req.body()
const header = req.headers
}
}
}

View File

@@ -0,0 +1,14 @@
import { PgClient } from "sim-shared/infrastructure/PgClient.js";
export class AlarmUsecases {
constructor(
private alarmRepository: any
) {
}
public saveAlarm() {
}
}

View File

@@ -1,18 +1,9 @@
import { loadEnvFile } from "node:process";
import path from "node:path";
try {
loadEnvFile(path.join("./.env")) // base
} catch (e) {
console.error("Error cargando el .env desde ./.env")
}
try {
loadEnvFile(path.join("../../.env")) // Global
} catch (e) {
console.error("Error cargando el .env desde ../../.env")
}
loadEnvFile("../../.env")
export const env = {
PORT: Number(process.env.PORT),
ENVIRONMENT: process.env.ENVIORMENT,
POSTGRES_USER: process.env.POSTGRES_USER,
POSTGRES_PASSWORD: process.env.POSTGRES_PASSWORD,
POSTGRES_PORT: process.env.POSTGRES_PORT,
@@ -28,12 +19,5 @@ export const env = {
RABBITMQ_SECURE: process.env.RABBITMQ_SECURE,
RABBITMQ_RETRY_INTERVAL: process.env.RABBITMQ_INTERVAL,
RABBITMQ_VHOST: String(process.env.RABBITMQ_VHOST),
APP_PORT: Number(process.env.NOS_PORT ?? 3001),
APP_HOST: String(process.env.NOS_HOST ?? "0.0.0.0"),
// ESPECIFICO NOS
NOS_BASE_URL: String(process.env.NOS_BASE_URL),
NOS_ACCESS_TOKEN: String(process.env.NOS_ACCESS_TOKEN)
};

View File

@@ -1,6 +1,6 @@
import { Pool, QueryResult } from 'pg';
import { Pool } from 'pg';
import { PgClient } from 'sim-shared/infrastructure/PgClient.js'
import { env } from './env/env.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
@@ -10,9 +10,9 @@ export const pgPool = new Pool({
host: env.POSTGRES_HOST,
database: env.POSTGRES_DATABASE,
password: env.POSTGRES_PASSWORD,
port: Number(env.POSTGRES_PORT) || 5433,
port: Number(env.POSTGRES_PORT) || 5432,
});
export const pgClient = new PgClient({
export const postgresClient = new PgClient({
pool: pgPool
})

View File

@@ -0,0 +1,62 @@
export type ObjeniousAlarmType =
"PLMN_CHANGE" |
"COUNTRY_CHANGE" |
"OVER_CONSUMPTION_VOLUME" |
"UNDER_CONSUMPTION_VOLUME" |
"IMEI_CHANGE" |
"OVER_CONSUMPTION_VOLUME" |
string
export type ObjeniousAlarm = {
alarmId: Number,
emissionDate: Date //"07/02/2019 14:44:00"
alarmType: ObjeniousAlarmType,
lineInfos: {
msisdn: string,
iccid: string
},
change?: ObjeniousChangeTypes,
usage?: ObjeniousUsageTypes
}
export type ObjeniousChangeTypes = ChangePayload | ImeiChangePayload;
export type ObjeniousUsageTypes = UsageAlarmPayload | OverConsumptionPayload;
/**
* PLMN_CHANGE
* COUNTRY_CHANGE
* STATUS_CHANGE
* */
export type ChangePayload = {
previous: string,
current: string
}
/**
* IMEI_CHANGE
*/
export type ImeiChangePayload = {
previous: string,//"42947662490276",
current: string, //"74845559130697",
previousManufacturer: string, //"u-blox AG",
currentManufacturer: string, //"SierraWireless"
}
/**
* OVER_CONSUMPTION_VOLUME
* UNDER_CONSUMPTION_VOLUME
*/
export type UsageAlarmPayload = {
type: string,
threshold: string,
trigerValue: string
}
/**
* OVER_CONSUMPTION_VOLUME
*/
export type OverConsumptionPayload = {
type: string, //"data-sms-smsIN-voiceOUT",
threshold: string, // "500-200-100-10",
triggerValue: string, // "600-300-101-15"
}

View File

@@ -0,0 +1,29 @@
import express from "express"
import cors from 'cors';
import { env } from "#config/env/index.js"
import { AlarmsController } from "aplication/Alarms.controller";
const PORT = env.PORT
const HOSTNAME = "0.0.0.0"
const app = express()
// Instancias
const alarmsController = new AlarmsController();
// Middleware
app.use(cors());
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
app.use("/fr/alarms", alarmsController.recibe())
app.get("/health", (req, res) => {
res.status(200).json({ status: "ok" })
})
app.listen(PORT, HOSTNAME, () => {
console.log("[o] Servidor iniciado en el puerto %d", PORT)
})
export default {}

View File

@@ -0,0 +1,10 @@
import { PgClient } from "sim-shared/infrastructure/PgClient.js";
export class AlarmsRepository {
constructor(private pgClient: PgClient) {
}
public createAlarm() {
}
}

View File

@@ -0,0 +1,76 @@
{
"name": "sim-alarmas-objenious",
"version": "1.0.0",
"description": "Recibe las alarmas de los webhook de objenious",
"main": "index.ts",
"scripts": {
"test": "node --import tsx --test ./**/*.test.ts",
"build": "tsc --build && tsc-alias -p tsconfig.json && cp package.json ../../dist/packages/sim-alarmas-objenious/",
"dev": "tsx watch index.ts",
"start": "node ../../dist/packages/sim-alarmas-objenious/index.js"
},
"author": "",
"license": "ISC",
"packageManager": "yarn@4.12.0",
"imports": {
"#config/*.js": {
"types": "./config/*.ts",
"default": "./config/*.js"
},
"#config/*": {
"types": "./config/*.ts",
"default": "./config/*.js"
},
"#adapters/*.js": {
"types": "./adapters/*.ts",
"default": "./adapters/*.js"
},
"#adapters/*": {
"types": "./adapters/*.ts",
"default": "./adapters/*.js"
},
"#domain/*.js": {
"types": "./domain/*.ts",
"default": "./domain/*.js"
},
"#domain/*": {
"types": "./domain/*.ts",
"default": "./domain/*.js"
},
"#ports/*.js": {
"types": "./ports/*.ts",
"default": "./ports/*.js"
},
"#ports/*": {
"types": "./ports/*.ts",
"default": "./ports/*.js"
},
"#tests/*.js": {
"types": "./__tests__/*.ts",
"default": "./__tests__/*.js"
},
"#tests/*": {
"types": "./__tests__/*.ts",
"default": "./__tests__/*.js"
}
},
"dependencies": {
"@tsconfig/node22": "*",
"amqplib": "^0.10.9",
"cors": "*",
"dotenv": "*",
"express": "*",
"typescript": "*"
},
"devDependencies": {
"@types/amqplib": "^0.10.8",
"@types/cors": "*",
"@types/express": "*",
"@types/node": "*",
"@types/supertest": "*",
"prettier": "*",
"supertest": "*",
"tsx": "*",
"vitest": "*"
}
}

View File

@@ -0,0 +1,17 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"outDir": "../../dist",
"baseUrl": ".",
},
"exclude": [
"node_modules"
],
"include": [
"**/*.ts",
"src/**/*.d.ts"
],
"files": [
"index.ts"
]
}

View File

@@ -1,43 +0,0 @@
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

@@ -1,50 +0,0 @@
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

@@ -1,195 +0,0 @@
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";
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<string, T>>): Promise<Result<string, T>> {
try {
const result = await usecase()
if (result.error == undefined) {
await this.eventBus.ack(msg)
return result
} else {
console.error("Error procesando el caso de uso (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: String(e)
}
}
}
public activate() {
return async (msg: ConsumeMessage) => {
console.log("[i] Evento activate ", msg.fields)
const data = this.validateMsg(msg) as SimEvents.activation
const iccid = data.payload.iccid
const correlation_id = data.headers?.message_id
const res = await this.tryUseCase(msg, this.uscases.activate({
iccid: iccid,
correlation_id: correlation_id
}))
return res;
}
}
public suspend() {
return async (msg: ConsumeMessage) => {
console.log("Evento suspend ", msg.fields)
const data = this.validateMsg(msg) as SimEvents.suspend
const iccid = data.payload.iccid
const correlation_id = data.headers?.message_id
const res = await this.tryUseCase(msg, this.uscases.suspend({
iccid: iccid,
correlation_id: correlation_id
}))
return res;
}
}
public 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)
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

@@ -1,79 +0,0 @@
/**
* 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<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.activate()]
]);
}
/**
* 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

@@ -1,264 +0,0 @@
/**
* 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 { ConsumeMessage } from "amqplib";
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";
import { isOmittedExpression } from "typescript";
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, detail?: string) {
// En NOS el updateOrder se hace con el correlation_id que viene en la cabecera del
// mensaje consumido
const updateData: ErrorOrderDTO = {
status: "failed",
correlation_id: correlation_id,
reason: reason,
error: reason,
stackTrace: detail
}
console.log("SET FAILED DATA:", updateData)
const order = await this.orderRepository.errorOrder(updateData)
console.log("SET FAILED RES:", order)
return order
}
/**
* 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<string, R>>,
args: T,
correlation_id?: string | undefined
) {
return async (): Promise<Result<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)
.then(e => console.log("failed", e))
.catch(e => console.error(e))
return res;
} else {
if (correlation_id != undefined)
this.setFinished(correlation_id).then()
return res;
}
} catch (e) {
if (correlation_id != undefined)
this.setFailed(correlation_id, "Error general de operacion de SIM (NOS) ", String(e)).then()
return {
error: "Error general de operacion de SIM (NOS) " + String(e)
}
}
}
}
public activate(args: {
iccid: string,
correlation_id: string | undefined
}) {
return this.usecaseTemplate(async (iccid /*iccid*/) => {
const order = await this.alaiRepository.createOrder()
if (order.error != undefined) {
// Falla el crearse un order (problema de servidor, token, etc)
console.error(order.error)
return order
}
const reserved = await this.alaiRepository.createReserve(order.data.id, iccid)
return reserved
}, 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 suspension = this.alaiRepository.pauseSubscription(subscription.data!.id)
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
}
// TODO: Controlar que no se encuentre la subscription
const suspension = this.alaiRepository.unPauseSubscription(subscription.data!.id)
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<string, {
sim: AlaiAPI.Sim,
subscription: AlaiAPI.Subscription,
imei: AlaiAPI.GetImeiSubscriptionDTO
}>> {
const sim = await this.alaiRepository.getSimByICCID(iccid)
if (sim.error != undefined) {
return sim
}
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

@@ -1,55 +0,0 @@
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

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

View File

@@ -1 +0,0 @@
eyJhbGciOiJIUzM4NCJ9.eyJiciI6InNhdmVmYW1pbHkiLCJpcCI6Ijg4LjE1LjE1Ny4xNjciLCJzdWIiOiJwYWxvbWFpYmFuZXoiLCJzIjoiRVdTMTY0NmFmNjNlZGUyMjgzIiwicG9zIjoic2F2ZWZhbWlseUNhYyIsImlkV3NVc2VyIjoiODYiLCJpc012bmEiOmZhbHNlLCJkb21haW4iOiJBbGFpfHNhdmVmYW1pbHkiLCJpYXQiOjE3Nzc4OTk3MzcsImV4cCI6MTc3NzkxMDUzN30.PvTTRhUpKlslGOerQsLY4RLBXdQ5FIVvUKb_1ZK4b2Zggt04KZhwX0d-XoLAcP93

View File

@@ -1,61 +0,0 @@
import { loadEnvFile } from "node:process";
import path from "node:path";
import assert from "node:assert";
try {
loadEnvFile(path.join("../../.env")) // Global
} catch (e) {
console.error("Error cargando el .env desde ../../.env")
}
try {
loadEnvFile(path.join("./.env")) // base
} catch (e) {
console.error("Error cargando el .env desde ./.env")
}
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,
POSTGRES_HOST: process.env.POSTGRES_HOST,
POSTGRES_DATABASE: process.env.POSTGRES_DATABASE,
RABBITMQ_HOST: String(process.env.RABBITMQ_HOST ?? "localhost"),
RABBITMQ_USER: String(process.env.RABBITMQ_USER ?? "test"),
RABBITMQ_PASSWORD: String(process.env.RABBITMQ_PASSWORD ?? "test"),
RABBITMQ_EXCHANGE: String(process.env.RABBITMQ_EXCHANGE ?? "/"),
RABBITMQ_PORT: parseInt(process.env.RABBITMQ_PORT ?? "5672"),
RABBITMQ_MODULENAME: process.env.MODULENAME,
RABBITMQ_TTL: process.env.RABBITMQ_TTL,
RABBITMQ_SECURE: process.env.RABBITMQ_SECURE,
RABBITMQ_RETRY_INTERVAL: process.env.RABBITMQ_INTERVAL,
RABBITMQ_VHOST: String(process.env.RABBITMQ_VHOST),
ALAI_PORT: parseInt(process.env.ALAI_PORT ?? "3002"),
ALAI_HOST: String(process.env.ALAI_HOST),
// ESPECIFICO ALAI
ALAI_API_URL: process.env.ALAI_API_URL,
ALAI_CERTIFICATES_DIR: process.env.ALAI_CERTIFICATES_DIR,
ALAI_CERTIFICATE_NAME: process.env.ALAI_CERTIFICATE_NAME,
ALAI_CERTIFICATE_PASSWORD: process.env.ALAI_CERTIFICATE_PASSWORD,
ALAI_USERNAME: process.env.ALAI_USERNAME,
ALAI_PASSWORD: process.env.ALAI_PASSWORD,
ALAI_BRANDID: process.env.ALAI_BRANDID,
ALAI_PACKAGE: process.env.ALAI_PACKAGE,
ALAI_SUBSCRIBER_ID: process.env.ALAI_SUBSCRIBER_ID
};
assert.ok(env.ALAI_SUBSCRIBER_ID != undefined, "ALAI_SUBSCRIBER_ID no definido")
assert.ok(env.ALAI_PACKAGE != undefined, "ALAI_PACKAGE no definido")
assert.ok(env.ALAI_USERNAME != undefined, "ALAI_USERNAME no definido")
assert.ok(env.ALAI_PASSWORD != undefined, "ALAI_PASSWORD no definido")
assert.ok(env.ALAI_BRANDID != undefined, "ALAI_BRANDID no definido")
assert.ok(env.ALAI_API_URL != undefined, "ALAI_API_URL no definido")
assert.ok(env.ALAI_CERTIFICATE_NAME != undefined, "ALAI_CERTIFICATE_NAME no definido")
assert.ok(env.ALAI_CERTIFICATES_DIR != undefined, "ALAI_CERTIFICATES_DIR no definido")
assert.ok(env.ALAI_CERTIFICATE_PASSWORD != undefined, "ALAI_CERTIFICATE_PASSWORD no definido")

View File

@@ -1,72 +0,0 @@
import { RabbitMQEventBus, RMQConnectionParams } from "sim-shared/infrastructure/RabbitMQEventBus.js"
import { Channel } from "amqp-connection-manager"
import { env } from "./env/env.js"
const rmqUser = env.RABBITMQ_USER
const rmqPass = env.RABBITMQ_PASSWORD
const rmqHost = env.RABBITMQ_HOST
const rmqPort = Number(env.RABBITMQ_PORT)
const rmqSecure = false
const rmqVhost = env.RABBITMQ_VHOST
export const rmqConnOptions = <RMQConnectionParams>{
username: rmqUser,
password: rmqPass,
vhost: rmqVhost,
hostname: rmqHost,
port: rmqPort,
secure: rmqSecure,
}
const BASE_ALAI_KEY = "sim.alai.#"
const QUEUES = {
MAIN: "sim.alai",
DLX: "sim.alai.dlx",
DELAY: "sim.alai.delayed",
}
const EXCHANGES = {
MAIN: "sim.exchange",
DLX: "sim.ex.alai.dlx",
DEL: "sim.ex.alai.delayed"
}
export const rabbitmqEventBus = new RabbitMQEventBus({
connectionParams: rmqConnOptions,
buildStructure: buildQueues,
maxRetry: 2,
delayedExchange: EXCHANGES.DEL,
dlxExchange: EXCHANGES.DLX
})
async function buildQueues(channel: Channel) {
const DELAY = 10 * 1000
await channel.assertExchange(EXCHANGES.DEL, "topic")
await channel.assertExchange(EXCHANGES.DLX, "topic")
await channel.assertExchange(EXCHANGES.MAIN, "topic")
await channel.assertQueue(QUEUES.MAIN)
await channel.assertQueue(QUEUES.DLX)
await channel.assertQueue(QUEUES.DELAY, {
durable: true,
arguments: {
'x-message-ttl': DELAY,
'x-dead-letter-exchange': EXCHANGES.MAIN,
}
})
// Cola dead-letter
await channel.bindQueue(QUEUES.DLX, EXCHANGES.DLX, BASE_ALAI_KEY)
// Cola delay
await channel.bindQueue(QUEUES.DELAY, EXCHANGES.DEL, BASE_ALAI_KEY)
// Cola nos -> main exchange
await channel.bindQueue(QUEUES.MAIN, EXCHANGES.MAIN, BASE_ALAI_KEY)
}
export async function startRMQClient() {
await rabbitmqEventBus.connect()
return rabbitmqEventBus
}

View File

@@ -1,17 +0,0 @@
import { HttpClient } from "sim-shared/infrastructure/HTTPClient.js"
import { AlaiTokenManager } from "#aplication/AlaiTokenManager.js"
import { env } from "#config/env/env.js";
import { httpsAgent } from "./httpsAgent.js"
import { DebugTokenManager } from "#aplication/DebugTokenManager.js";
const tokenManager = new AlaiTokenManager()
//const debugTokenManagr = new DebugTokenManager()
//console.error("USANDO DebugTokenManager! Eliminar en prod")
export const alaiHttp = new HttpClient({
baseURL: env.ALAI_API_URL as string,
headers: {},
jwtManager: tokenManager,
httpsAgent: httpsAgent
})

View File

@@ -1,14 +0,0 @@
import fs from 'fs';
import https from 'https';
import { env } from './env/env.js';
import path from 'path';
const certificatesDir = String(env.ALAI_CERTIFICATES_DIR)
const certificateName = String(env.ALAI_CERTIFICATE_NAME)
const certificatePassword = String(env.ALAI_CERTIFICATE_PASSWORD)
export const httpsAgent = new https.Agent({
pfx: fs.readFileSync(path.join(certificatesDir, certificateName)),
passphrase: certificatePassword
});

View File

@@ -1,294 +0,0 @@
import { StringMappingType } from "typescript"
export namespace AlaiAPI {
export type LoginResponseDTO = {
accessToken: string,
tokenType: string,
refreshToken: string,
expiresIn: string // isodate
}
/**
Hardcodeado en:
sf-sim-connections/context/infrastructure/api/alaiService.js
const data = {
type: "RETAIL",
salesChannel: "OWN_CALLCENTER",
status: "CONFIRMED",
packages: [{ id: "Tarifa_250MB_100MIN_5SMS" }],
subscriber: { id: "16216" }
};
*/
export type CreateOrderDTO = {
type: "RETAIL" | string,
salesChannel: "OWN_CALLCENTER" | string,
status: "CONFIRMED" | string,
packages: { id: "Tarifa_250MB_100MIN_5SMS" | string }[],
subscriber: {
id: string
}
}
type OrderPackage = {
id: string,
name: string,
packagePrices: unknown,
packageInstance: {
id: string,
name: string,
links: Link[]
}
}
type Link = {
rel: string,
href: string,
hreflang: string,
media: string,
title: string,
type: string,
deprecation: string,
profile: string,
name: string
}
export type UpdateSubscriptionDTO = {
location: string
}
export type ApplyOrderDTO = UpdateSubscriptionDTO
export type Subscription = {
id: string,
name: string,
domain: string,
status: Status,
networkStatus: NetworkStatus,
type: "RETAIL" | string,
portabilityStatus: "NO_PORTABILITY" | string,
billingType: string,
creationDate: string, // ISODATE
firstActivationDate: string, // ISODATE
terminationDate: string, // ISODATE
balance: number,
balanceExpirationDate: string, // ISODATE
lastTrafficDate: string, // ISODATE
externalName: string,
language: string,
ntwID: string,
publicIdentity: string,
externalID: string,
lastMsisdnID: string,
msisdn: {
id: string,
name: string,
links: Link[]
},
lastIccID: string,
priceplan: {
id: string,
name: string,
pricePlanName: string
},
salesData: {
salesChannel: string,
salesPerson: string,
},
address: {
country: string,
state: string,
county: string,
city: string,
street: string,
postalCode: string,
number: string,
description: string,
neighborhood: string,
typeSettlement: string,
normalized: boolean,
externalID: string,
externalType: string,
spainSpecial: {
externalRefList:
{
refId: string,
refType: string
}[],
streetType: number,
ineCityCode: string,
ineSingularEntityCode: string,
floor: string,
door: string,
apartmentNumber: string,
staircaseNumber: string,
streetNrLast: string,
streetNrLastSuffix: string,
subUnitNumber: string,
buildingName: string,
homeID: string
},
iranSpecial: unknown,
mexicoSpecial: unknown,
brazilSpecial: unknown,
},
msisdnList: {
id: string,
name: string,
links: Link[]
}[],
terminalList: {
id: string,
name: string,
links: Link[]
}[]
}
export type CreateOrderResponseDTO = {
id: string,
name: string,
domain: string,
orderCode: string,
externalID: string,
type: string,
status: string,
saleStatus: string,
distributionStatus: string,
description: string,
salesChannel: string,
salesPerson: string,
deliveryType: string,
distributionInfo: {
providerID: string,
providerReference: string,
providerTracking: string,
cashOnDelivery: boolean,
prepaidShipping: boolean,
description: string,
events: {
status: string,
observations: string,
date: string | Date,
expectedDeliveryDate: string | Date,
completedDeliveryDate: string | Date,
}[]
},
packages: OrderPackage[],
subscription: {
id: string,
name: string,
links: Link[]
}
subscriber: {
id: string,
name: string,
links: Link[]
}
brand: {
id: string,
name: string,
links: Link[]
}
pos: {
id: string,
name: string,
links: Link[]
}
links: Link[]
}
export type NetworkStatus =
"ACTIVE" |
"BLOCKED" |
"DEACTIVATE" |
"FRAUD" |
"PRE_ACTIVE"
export type Status =
"ABORTED" |
"ACTIVE" |
"BLOCKEDCORE" |
"BLOCKEDFRAUD" |
"CANCELLED" |
"CONFIGURING" |
"DELETED" |
"PRE_ACTIVE" |
"TERMINATED"
export type Sim = {
id: string,
name: string,
simCode: string,
puk: string,
puk2: string,
pin: string,
pin2: string,
status: string,
storeStatus: string,
statusEsim: string,
pool: {
id: string,
name: string,
links: Link[]
},
sourcePool: {
id: string,
name: string,
links: Link[]
},
subscription: {
id: string,
name: string,
links: Link[]
},
imsi: {
id: string,
name: string,
links: Link[]
},
msisdn: {
id: string,
name: string,
links: Link[]
},
distributedPos: {
id: string,
name: string,
links: Link[]
},
pkgi: {
id: string,
name: string,
links: Link[]
},
links: Link[]
}
export type GetImeiSubscriptionDTO = {
links: Link[],
content: {
id: string,
sim: {
id: string,
links: Link[]
},
imsi: string,
lastChange: string, //ISODATE
lastUpdate: string, //ISODATE
model: string,
subscription: {
id: string,
links: Link[]
},
links: Link[]
}[],
page: {
size: number,
totalElements: number,
totalPages: number,
number: number
}
}
}

View File

@@ -1,47 +0,0 @@
import { Result } from "sim-shared/domain/Result.js";
import { AlaiAPI } from "./AlaiAPI.js";
import { CommonSim } from "sim-shared/domain/CommonSim.js";
const alaiStates = new Map<AlaiAPI.Status, CommonSim<any>["billing_status"]>([
["ABORTED", "SUSPENDED"],
["ACTIVE", "ACTIVE"],
["BLOCKEDCORE", "SUSPENDED"],
["BLOCKEDFRAUD", "SUSPENDED"],
["CANCELLED", "TERMINATED"],
["CONFIGURING", "SUSPENDED"],
["DELETED", "TERMINATED"],
["PRE_ACTIVE", "PREACTIVATED"],
["TERMINATED", "TERMINATED"]
])
const alaiNetworkStates = new Map<AlaiAPI.NetworkStatus, CommonSim<any>["network_status"]>([
["ACTIVE", "ACTIVE"],
["PRE_ACTIVE", "PREACTIVATED"],
["BLOCKED", "SUSPENDED"],
["DEACTIVATE", "SUSPENDED"],
["FRAUD", "TERMINATED"]
])
export function alaiSimToCommonSim(alaiSim: AlaiAPI.Sim, alaiSubscription: AlaiAPI.Subscription, imeiSubscription: AlaiAPI.GetImeiSubscriptionDTO):
Result<string, CommonSim<AlaiAPI.Subscription>> {
const status = alaiStates.get(alaiSubscription.status) ?? "UNKNOWN"
const networkStatus = alaiNetworkStates.get(alaiSubscription.networkStatus) ?? "UNKNOWN"
const commonSim: CommonSim<AlaiAPI.Subscription> = {
company: "ALAI",
tariff: alaiSubscription.name,
iccid: alaiSim.id,
msisdn: alaiSubscription.lastMsisdnID,
billing_status: status,
network_status: networkStatus,
raw: alaiSubscription,
imei: imeiSubscription.content[0].id ?? "0",
preactivation_date: new Date(alaiSubscription.creationDate),
activation_date: new Date(alaiSubscription.firstActivationDate)
}
return {
data: commonSim
}
}

View File

@@ -1,78 +0,0 @@
import express from "express"
import cors from 'cors';
import { env } from "#config/env/env.js"
import { startRMQClient } from "#config/eventBus.config.js";
import { SimAlaiRouter } from "#aplication/SimAlai.router.js";
import { SimAlaiController } from "#aplication/SimAlai.controller.js";
import { SimAlaiUsecases } from "#aplication/SimAlai.usecases.js";
import { alaiHttp } from "#config/httpClient.config.js";
import { AlaiRepository } from "#infrastructure/AlaiRepository.js";
import { OrderRepository } from "sim-shared/infrastructure/OrderRepository.js";
import { pgClient } from "#config/postgreConfig.js";
const RMQ_QUEUE = "sim.alai"
const PORT = env.ALAI_PORT
const HOSTNAME = env.ALAI_HOST
async function startWorker() {
// Instancia de dependencias
const rmqClient = await startRMQClient()
const orderRepository = new OrderRepository(pgClient)
const alaiRepository = new AlaiRepository(alaiHttp)
const alaiUsecases = new SimAlaiUsecases(
alaiHttp,
alaiRepository,
orderRepository
)
const alaiController = new SimAlaiController(
alaiUsecases,
rmqClient
)
const simRouter = new SimAlaiRouter(
alaiController,
rmqClient
)
// RMQ
rmqClient.consume(RMQ_QUEUE, simRouter.route)
.then(() => console.log("Cliente rmq creado con exito"))
.catch(e => console.error("Error conectando con RABBITMQ", e))
// Express
const app = express()
app.use(cors());
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
// WIP
app.get("/select", alaiController.selectREST())
app.get("/health",
(req, res) => res.json({
ok: "alai"
}))
//app.get("/selectPage", alaiController.selectPageREST())
app.listen(PORT, HOSTNAME, (e) => {
if (e == undefined) {
console.log("[o] Servidor (Alai) iniciado en el puerto %d", PORT)
} else {
console.error("Error express ", e)
}
})
}
startWorker()
.then(e => {
console.log("[o] Worker de SIM de Alai iniciado")
})
.catch(e => {
console.log("[x] Error iniciando worker de SIM de Alai", e)
})
export default {}

View File

@@ -1,190 +0,0 @@
import { AlaiAPI } from "#domain/AlaiAPI.js";
import axios, { AxiosError, AxiosResponse } from "axios";
import { Result } from "sim-shared/domain/Result.js";
import { env } from "#config/env/env.js";
import { HttpClient } from "sim-shared/infrastructure/HTTPClient.js";
import https from "https"
export class AlaiRepository {
constructor(
private httpClient: HttpClient
) {
}
private async manageRequest<E, T>(promiseReq: Promise<AxiosResponse<T>>): Promise<Result<string, T>> {
try {
const res = await promiseReq
return {
data: res.data
}
} catch (e) {
if (axios.isAxiosError(e)) {
const error = e as AxiosError
return {
error: error.code + " : " + String(error.response?.statusText)
}
} else {
return {
error: String(e)
}
}
}
}
public static async login(httpsAgent: https.Agent): Promise<Result<string, AlaiAPI.LoginResponseDTO>> {
const alaiUrl = env.ALAI_API_URL
const endpoint = "/v1/auth/login"
const fullUrl = alaiUrl + endpoint
const data = {
"username": env.ALAI_USERNAME,
"password": env.ALAI_PASSWORD,
"brandID": env.ALAI_BRANDID
}
try {
const loginRes = await axios.post<AlaiAPI.LoginResponseDTO>(fullUrl, data, { httpsAgent })
return {
data: loginRes.data
}
} catch (e) {
if (axios.isAxiosError(e)) {
const error = e as AxiosError
return {
error: error.code + " : " + String(error.response?.statusText)
}
} else {
return {
error: String(e)
}
}
}
}
/**
* Los orders son la unidad que envuelve las peticiones para garantizar ideponencia.
*/
public async createOrder() {
// POST
const endpoint = "/v1/order"
const data: AlaiAPI.CreateOrderDTO = {
type: "RETAIL",
salesChannel: "OWN_CALLCENTER",
status: "CONFIRMED",
packages: [{ id: env.ALAI_PACKAGE as string }],
subscriber: { id: env.ALAI_SUBSCRIBER_ID as string }
}
const promReq = this.httpClient.post<AlaiAPI.CreateOrderResponseDTO>(endpoint, data)
const res = await this.manageRequest(promReq)
return res
}
public async applyOrder(orderId: string) {
const endpoint = `/v1/order/${orderId}`
const params = new URLSearchParams([
["action", "APPLY"]
])
const promReq = this.httpClient.patch<AlaiAPI.ApplyOrderDTO>(endpoint, undefined, { params: params })
const res = await this.manageRequest(promReq)
return res
}
/*
* Ver estado del order para debug
*/
public async getOrder(orderId: string) {
const endpoint = `/v1/order/${orderId}`
const promReq = this.httpClient.post<AlaiAPI.CreateOrderResponseDTO>(endpoint, undefined)
const res = await this.manageRequest(promReq)
return res
}
/**
* Antes se usaba PATCH /v1/sim/{iccid}/{orderId} pero en la docu ha pasado a POST
*/
public async createReserve(orderId: string, iccid: string): Promise<Result<string, AlaiAPI.CreateOrderResponseDTO>> {
const endpoint = `/v1/sim/${iccid}/order/${orderId}`
// Crear la reserva no usa datos en el body
const promReq = this.httpClient.post<AlaiAPI.CreateOrderResponseDTO>(endpoint, undefined)
const res = await this.manageRequest(promReq)
return res
}
/**
* IMPORTANTE:
* - En el campo subscription viene el id para los cambios de estado, no se hacen
* sobre la sim, sino sobre la subscription
*/
public async getSimByICCID(iccid: string) {
const endpoint = `/v1/sim/${iccid}`
const promReq = this.httpClient.get<AlaiAPI.Sim | undefined>(endpoint, undefined)
const res = await this.manageRequest(promReq)
return res
}
public async pauseSubscription(subscriptionId: string) {
const endpoint = `/v1/subscription/${subscriptionId}`
// En teoria ahora se usa ["action", "BLOCK"] pero no he probado
const params = new URLSearchParams([
["action", "CHANGE_STATUS"]
])
const data = {
status: "BLOCKEDCORE"
}
const promReq = this.httpClient.patch<AlaiAPI.UpdateSubscriptionDTO | undefined>(endpoint, data, { params: params })
const res = await this.manageRequest(promReq)
return res
}
public async unPauseSubscription(subscriptionId: string) {
const endpoint = `/v1/subscription/${subscriptionId}`
// En teoria ahora se usa ["action", "UNBLOCK"] pero no he probado
const params = new URLSearchParams([
["action", "CHANGE_STATUS"]
])
const data = {
status: "ACTIVE"
}
const promReq = this.httpClient.patch<AlaiAPI.UpdateSubscriptionDTO | undefined>(endpoint, data, { params: params })
const res = await this.manageRequest(promReq)
return res
}
public async terminateSubscription(subscriptionId: string) {
const endpoint = `/v1/subscription/${subscriptionId}`
// Esta llamada si es de acuerdo a la docu
const params = new URLSearchParams([
["action", "TERMINATE"]
])
const promReq = this.httpClient.patch<AlaiAPI.UpdateSubscriptionDTO | undefined>(endpoint, undefined, { params: params })
const res = await this.manageRequest(promReq)
return res
}
public async changeExternalId(subscriptionId: string, externalId: string) {
const endpoint = `/v1/subscription/${subscriptionId}`
// Esta llamada si es de acuerdo a la docu
const params = new URLSearchParams([
["action", "MODIFY"]
])
const data = {
externalID: externalId
}
const promReq = this.httpClient.patch<AlaiAPI.UpdateSubscriptionDTO | undefined>(endpoint, data, { params: params })
const res = await this.manageRequest(promReq)
return res
}
public async getSubscriptionById(subscriptionId: string) {
const endpoint = `/v1/subscription/${subscriptionId}`
const promReq = this.httpClient.get<AlaiAPI.Subscription | undefined>(endpoint)
const res = await this.manageRequest(promReq)
return res
}
public async getImeiFromSubscription(subscriptionId: string) {
const endpoint = `/v1/subscription/${subscriptionId}/imei`
const promReq = this.httpClient.get<AlaiAPI.GetImeiSubscriptionDTO | undefined>(endpoint)
const res = await this.manageRequest(promReq)
return res
}
}

View File

@@ -1,65 +0,0 @@
import path from "path";
import fs from 'fs';
import { Result } from "sim-shared/domain/Result.js";
import { PgClient } from "sim-shared/infrastructure/PgClient.js";
type TokenLine = {
id: number,
url: string,
user_name: string,
pass: string,
brand_id: string,
cert_file: string,
key_file: string,
ca_file: string,
p12_file: string,
cert_password: string,
token: string
}
/**
* Repositorio para usar los tokens guardados en la bdd de intranet o en un archivo
*/
export class LegacyJWTTokenRepository {
constructor(
// En prod (no deberia usarse) tiene que apuntar a intranet
private pgClient: PgClient
) {
}
public async getTokenFromDB(): Promise<Result<string, string>> {
const query = "SELECT * FROM alai_api_credentials;"
try {
const res = await this.pgClient.query<TokenLine>(query)
if (res.rowCount == 0) {
return {
error: "Error recuperando el token actual"
}
} else {
return {
data: res.rows[0].token
}
}
} catch (e) {
return {
error: String(e)
}
}
}
public static getTokenFromFile(dir: string, file: string): Result<string, string> {
try {
const tokenPath = path.join(dir, file)
const fileContent = fs.readFileSync(tokenPath).toString()
return {
data: fileContent
}
} catch (e) {
return {
error: "[d!] No se ha podido leer el archivo del token de debug" + String(e)
}
}
}
}

View File

@@ -1,72 +0,0 @@
{
"name": "sim-consumidor-alai",
"type": "module",
"description": "consumidor generico de eventos de alai",
"main": "index.ts",
"imports": {
"#config/*.js": {
"types": "./config/*.ts",
"default": "./config/*.js"
},
"#config/*": {
"types": "./config/*.ts",
"default": "./config/*.js"
},
"#infrastructure/*.js": {
"types": "./infrastructure/*.ts",
"default": "./infrastructure/*.js"
},
"#infrastructure/*": {
"types": "./infrastructure/*.ts",
"default": "./infrastructure/*.js"
},
"#domain/*.js": {
"types": "./domain/*.ts",
"default": "./domain/*.js"
},
"#domain/*": {
"types": "./domain/*.ts",
"default": "./domain/*.js"
},
"#aplication/*.js": {
"types": "./aplication/*.ts",
"default": "./aplication/*.js"
},
"#aplication/*": {
"types": "./aplication/*.ts",
"default": "./aplication/*.js"
}
},
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"build": "yarn tsc --project tsconfig.json && yarn tsc-alias && cp -P .env package.json ../../dist/packages/sim-consumidor-alai/ && cp -r certificates/ ../../dist/packages/sim-consumidor-alai/",
"build:prod": "yarn tsc --project tsconfig.json && yarn tsc-alias && cp -P package.json ../../dist/packages/sim-consumidor-alai/ && mkdir ../../dist/packages/sim-consumidor-alai/certificates",
"esbuild": "esbuild index.ts --platform=node",
"start": "node ../../dist/packages/sim-consumidor-alai/index.js",
"dev": "tsx watch index.ts"
},
"author": "",
"license": "ISC",
"packageManager": "yarn@4.12.0",
"dependencies": {
"@tsconfig/node22": "*",
"amqplib": "^0.10.9",
"cors": "*",
"dotenv": "*",
"express": "*",
"sim-shared": "sim-shared:*",
"typescript": "*"
},
"devDependencies": {
"@types/amqplib": "^0.10.8",
"@types/cors": "*",
"@types/express": "*",
"@types/node": "*",
"@types/supertest": "*",
"prettier": "*",
"supertest": "*",
"tsc-alias": "^1.8.16",
"tsx": "*",
"vitest": "*"
}
}

View File

@@ -1,5 +0,0 @@
# Alai
## Particularidades de las operaciones de Alai
TODO: Copiar de obsidian

View File

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

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