2 Commits

186 changed files with 1652 additions and 5723 deletions

32
.env Normal file
View File

@@ -0,0 +1,32 @@
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
# Proxy
CONNECTIONS_URL=https://sim-connections.savefamilygps.net

6
.gitignore vendored
View File

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

View File

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

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

@@ -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 payload JSONB, -- Duda si es optimo guardar la copia, es útil en caso de fallo
-- Campos de reintentos? -- Campos de reintentos?
status order_status NOT NULL DEFAULT 'pending', status order_status NOT NULL DEFAULT 'pending',
retry_count INT DEFAULT 0, retry_count INT DEFAULT 0,
error_message TEXT, -- Razón del fallo error_message TEXT, -- Razón del fallo

View File

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

View File

@@ -23,7 +23,7 @@ pipeline {
stage("🧱 Building") { stage("🧱 Building") {
steps { steps {
sh 'rm -rf dist/' sh 'rm -rf dist/'
sh 'yarn run build:prod' sh 'yarn run build'
} }
} }
stage("🏗 Deploying") { stage("🏗 Deploying") {
@@ -38,40 +38,14 @@ pipeline {
cleanRemote: false, cleanRemote: false,
execCommand: "mkdir -p $APP_REMOTE_PATH" 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( sshTransfer(
cleanRemote: false, cleanRemote: false,
execCommand: "ln -sf $BASE_REMOTE_PATH/vault/savefamily/sf-sims/.env $APP_REMOTE_PATH/.env" 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( sshTransfer(
cleanRemote: false, cleanRemote: false,
execCommand: "ln -sf $BASE_REMOTE_PATH/vault/savefamily/sf-sims/sim-consumidor-objenious.env $APP_REMOTE_PATH/sim-consumidor-objenious.env" 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( sshTransfer(
cleanRemote: false, cleanRemote: false,
execCommand: "ln -sf $BASE_REMOTE_PATH/vault/savefamily/sf-sims/sim-objenious-cron.env $APP_REMOTE_PATH/sim-objenious-cron.env" 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, cleanRemote: false,
execCommand: "ln -sf $BASE_REMOTE_PATH/vault/savefamily/sf-sims/obj.pem $APP_REMOTE_PATH/obj.pem" 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( sshTransfer(
cleanRemote: false, cleanRemote: false,
remoteDirectory: "$APP_REMOTE_PATH", remoteDirectory: "$APP_REMOTE_PATH",

View File

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

View File

@@ -7,7 +7,6 @@ networks:
services: services:
rabbitmq-sim-broker: rabbitmq-sim-broker:
container_name: rabbitmq-sim-broker container_name: rabbitmq-sim-broker
hostname: rabbitmq-sim
image: "rabbitmq:4.2.2-management" image: "rabbitmq:4.2.2-management"
ports: ports:
- "5672:5672" - "5672:5672"
@@ -24,7 +23,6 @@ services:
RABBITMQ_DEFAULT_USER: ${RABBITMQ_USER} RABBITMQ_DEFAULT_USER: ${RABBITMQ_USER}
RABBITMQ_DEFAULT_PASS: ${RABBITMQ_PASSWORD} RABBITMQ_DEFAULT_PASS: ${RABBITMQ_PASSWORD}
volumes: volumes:
- ./rabbitmq-data/:/var/lib/rabbitmq/
- ./rabbitmq_plugins/enabled_plugins:/etc/rabbitmq/enabled_plugins:ro - ./rabbitmq_plugins/enabled_plugins:/etc/rabbitmq/enabled_plugins:ro
- ./deployment/local/rabbit/rabbitmq.conf:/etc/rabbitmq/rabbitmq.conf:ro - ./deployment/local/rabbit/rabbitmq.conf:/etc/rabbitmq/rabbitmq.conf:ro
- ./deployment/local/rabbit/definitions.json:/etc/rabbitmq/definitions.json: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 { post {
url: {{baseurl}}/sim/activate url: {{baseurl}}/sim/activate
body: json body: formUrlEncoded
auth: inherit auth: inherit
} }
body:json {
{
"iccid": "8934909001500561503"
}
}
body:form-urlencoded { body:form-urlencoded {
iccid: 123 iccid: 8933201125065160380
offer: mensual offer: SAVEFAMILY1
} }
settings { settings {
encodeUrl: true encodeUrl: true
timeout: 0 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 { meta {
name: Activation Email Health name: Activation Email Health
type: http type: http
seq: 9 seq: 8
} }
post { post {

View File

@@ -1,7 +1,7 @@
meta { meta {
name: Activation Email name: Activation Email
type: http type: http
seq: 8 seq: 6
} }
post { 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 { meta {
name: Cancel name: Cancel
type: http type: http
seq: 4 seq: 1
} }
post { post {
@@ -11,7 +11,7 @@ post {
} }
body:form-urlencoded { body:form-urlencoded {
iccid: 8933201125068889894 iccid: 8933201125068890074
} }
settings { settings {

View File

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

View File

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

View File

@@ -1,7 +1,7 @@
meta { meta {
name: Health name: Health
type: http type: http
seq: 7 seq: 5
} }
get { 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 { get {
url: {{baseurl}}/orders/019dbeaf-8abb-7783-8b51-94fbd9f0b0df url: {{baseurl}}/orders/
body: none body: none
auth: inherit auth: inherit
} }

View File

@@ -5,11 +5,15 @@ meta {
} }
get { get {
url: {{baseurl}}/orders/message_id/019dbeaf-8abb-7783-8b51-94fbd9f0b0df url: {{baseurl}}/orders/message_id/019c93d3-014a-711d-b958-03dd629be78d
body: none body: none
auth: inherit auth: inherit
} }
params:query {
~message_id: 019c93d3-014a-711d-b958-03dd629be78d
}
settings { settings {
encodeUrl: true encodeUrl: true
timeout: 0 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 { meta {
name: Pause name: Pause
type: http type: http
seq: 5 seq: 1
} }
post { post {
@@ -15,7 +15,7 @@ params:query {
} }
body:form-urlencoded { body:form-urlencoded {
iccid: 8933201125065160331 iccid: 8933201125068886700
} }
settings { settings {

View File

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

View File

@@ -1,7 +1,7 @@
meta { meta {
name: ReActivate name: ReActivate
type: http type: http
seq: 12 seq: 13
} }
post { post {
@@ -11,7 +11,7 @@ post {
} }
body:form-urlencoded { body:form-urlencoded {
iccid: 8934909001500561503 iccid: 8933201125065160380
~offer: SAVEFAMILY1 ~offer: SAVEFAMILY1
} }

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 { meta {
name: Test Order name: Test Order
type: http type: http
seq: 10 seq: 9
} }
post { post {

View File

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

View File

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

View File

@@ -1,7 +1,7 @@
meta { meta {
name: test proxy name: test proxy
type: http type: http
seq: 13 seq: 14
} }
get { get {

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

@@ -1,38 +0,0 @@
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,16 +5,16 @@ meta {
} }
get { 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 body: formUrlEncoded
auth: bearer auth: bearer
} }
params:query { params:query {
identifier.identifierType: ICCID pageSize: 1000
identifier.identifiers: 8933201125065160455 simStatus: ACTIVATED
~pageSize: 1000 ~identifier.identifierType: ICCID
~simStatus: ACTIVATED ~identifier.identifiers: 8933201125065160455
} }
auth:bearer { 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

@@ -1,18 +1,17 @@
{ {
"name": "sim-eventos", "name": "sim-eventos",
"version": "1.0.0", "version": "1.0.0",
"packageManager": "yarn@4.14.1", "packageManager": "yarn@4.12.0",
"workspaces": [ "workspaces": [
"packages/*" "packages/*"
], ],
"scripts": { "scripts": {
"test": "vitest watch", "test": "vitest watch",
"build": "rm -rf ./dist && yarn workspaces foreach -Api run build && yarn setup:runtime", "build": "yarn workspaces foreach -A --exclude sim-consumidor-nos run build && cp .env dist/ && 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", "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", "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": "eslint .",
"lint:fix": "eslint --fix .", "lint:fix": "eslint --fix .",
"format": "prettier --write .", "format": "prettier --write .",
@@ -29,7 +28,7 @@
"dotenv": "^17.2.3", "dotenv": "^17.2.3",
"express": "^5.2.1", "express": "^5.2.1",
"pg": "^8.18.0", "pg": "^8.18.0",
"typescript": "^6.0.3", "typescript": "^5.9.3",
"uuidv7": "^1.1.0", "uuidv7": "^1.1.0",
"vite": "^7.3.1", "vite": "^7.3.1",
"vite-tsconfig-paths": "^6.0.5" "vite-tsconfig-paths": "^6.0.5"

View File

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

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,225 +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";
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

@@ -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<{ 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

@@ -1,302 +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 { 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

@@ -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.eyJiciI6InNhdmVmYW1pbHkiLCJpcCI6Ijg4LjE1LjE1Ny4xNjciLCJzdWIiOiJwYWxvbWFpYmFuZXoiLCJzIjoiRVdTMTY3MzRhYTM2MDY1M2EwIiwicG9zIjoic2F2ZWZhbWlseUNhYyIsImlkV3NVc2VyIjoiODYiLCJpc012bmEiOmZhbHNlLCJkb21haW4iOiJBbGFpfHNhdmVmYW1pbHkiLCJpYXQiOjE3Nzg2ODQ0NjIsImV4cCI6MTc3ODY5NTI2Mn0.wMWgjaOErm5clang7ErYzREU56okgpXWzq1zihT4lOfUDRQ005r-nCHJu7rpilj1

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,20 +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: {
"content-type": "application/json"
},
jwtManager: tokenManager,
//jwtManager: debugTokenManagr,
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,18 +0,0 @@
import { Pool, QueryResult } from 'pg';
import { PgClient } from 'sim-shared/infrastructure/PgClient.js'
import { env } from './env/env.js';
// Configuracion de la conexion a la BDD, deberia ser la
// Misma para todos los servicios pero hasta que se unifique todo
// se hace una por servicio.
export const pgPool = new Pool({
user: env.POSTGRES_USER,
host: env.POSTGRES_HOST,
database: env.POSTGRES_DATABASE,
password: env.POSTGRES_PASSWORD,
port: Number(env.POSTGRES_PORT) || 5433,
});
export const pgClient = new PgClient({
pool: pgPool
})

View File

@@ -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,62 +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<
{
sim: AlaiAPI.Sim,
subscription?: AlaiAPI.Subscription,
imeiSubscription?: AlaiAPI.GetImeiSubscriptionDTO
}
>> {
const billingStatus = (alaiSubscription == undefined) ? "AVAILABLE" : alaiStates.get(alaiSubscription?.status ?? "") ?? "UNKNOWN"
const networkStatus = (alaiSubscription == undefined) ? "AVAILABLE" : alaiNetworkStates.get(alaiSubscription.networkStatus) ?? "UNKNOWN"
const commonSim: CommonSim<{
sim: AlaiAPI.Sim,
subscription?: AlaiAPI.Subscription,
imeiSubscription?: AlaiAPI.GetImeiSubscriptionDTO
}> = {
company: "ALAI",
tariff: alaiSubscription?.name,
iccid: alaiSim.id,
msisdn: alaiSubscription?.lastMsisdnID,
billing_status: billingStatus,
network_status: networkStatus,
raw: {
subscription: alaiSubscription,
sim: alaiSim,
imeiSubscription: imeiSubscription
},
imei: imeiSubscription?.content[0]?.id ?? "0",
preactivation_date: (alaiSubscription != undefined) ? new Date(alaiSubscription.creationDate) : undefined,
activation_date: (alaiSubscription != undefined) ? new Date(alaiSubscription.firstActivationDate) : undefined
}
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,228 +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"
type ErrorRepo = {
msg: string,
stackTrace?: string
}
export class AlaiRepository {
constructor(
private httpClient: HttpClient
) {
}
private async manageRequest<E, T>(promiseReq: Promise<AxiosResponse<T>>): Promise<Result<ErrorRepo, T>> {
try {
const res = await promiseReq
return {
data: res.data
}
} catch (e) {
if (axios.isAxiosError(e)) {
console.log("ERROR REQUEST ", e.response)
const error = e as AxiosError
return {
error: {
msg: error.code + " : " + String(error.response?.statusText),
stackTrace: JSON.stringify(error.response?.data)
}
}
} else {
return {
error: {
msg: String(e),
stackTrace: String(e)
}
}
}
}
}
public static async login(httpsAgent: https.Agent): Promise<Result<ErrorRepo, 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: {
msg: error.code + " : " + String(error.response?.statusText),
stackTrace: String(error)
}
}
} else {
return {
error: {
msg: 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<ErrorRepo, 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 activateSubscription(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 unPauseSubscription(subscriptionId: string) {
const endpoint = `/v1/subscription/${subscriptionId}`
// En teoria ahora se usa ["action", "UNBLOCK"] pero no he probado
const params = new URLSearchParams([
["action", "UNBLOCK"]
])
const rawParams = {
"action": "UNBLOCK"
}
const data = {
status: "ACTIVE"
}
const promReq = this.httpClient.patch<AlaiAPI.UpdateSubscriptionDTO | undefined>(endpoint, undefined, { params: rawParams })
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,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"
]
}

View File

@@ -0,0 +1,3 @@
NOS_BASE_URL=localhost
ENVIORMENT=development

View File

@@ -1,185 +1,65 @@
import { ConsumeMessage } from "amqplib";
import { Request, Response } from "express"
import { SimNosUsecases } from "./SimNOS.usecases.js";
import { EventBus } from "sim-shared/domain/EventBus.port.js"; import { EventBus } from "sim-shared/domain/EventBus.port.js";
import { Result } from "sim-shared/domain/Result.js"; import { ConsumeMessage } from "amqplib";
import { SimEvents } from "sim-shared/domain/SimEvents.js";
import { iccidValidator } from "./httpValidators.js";
import { error } from "node:console";
import { nosSimToCommonSim } from "#domain/transformers.js";
export class SimNosController { export class SimNosController {
private eventBus: EventBus;
private activationUseCases: any;
private routes = new Map<string, () => void>([
["activate", async () => { console.log("caso de uso activate") }],
["pause", async () => { console.log("caso de uso pause") }],
["cancel", async () => { console.log("caso de uso cancel") }],
])
constructor( constructor(
private uscases: SimNosUsecases, eventBus: EventBus
private eventBus: EventBus,
) { ) {
this.eventBus = eventBus
// No se si hay un sistema mejor
// convertor en const () => {} para conservar el contexto??
this.recibeMsg = this.recibeMsg.bind(this)
} }
private validateMsg(msg: ConsumeMessage | null) { public async recibeMsg(msg: ConsumeMessage | null) {
if (msg == undefined) return false; if (!this.validateActivationMsg(msg)) {
const msgData = this.decodeMsg(msg) as SimEvents.general throw new Error("Error consumiendo el mensaje no es valido")
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 { msg = msg!
// Convertir el Buffer a String (UTF-8)
const contentJson = JSON.parse(Buffer.from(msg.content).toString('utf8'))
return contentJson;
} catch (error) { const msgParsed = JSON.parse(String(msg.content))
console.error('Error al decodificar JSON:', error); const msgKey = msg.fields.routingKey.split(".")
console.error(Buffer.from(msg.content).toString(("utf8"))) const accion = msgKey[2]
// Aquí podrías decidir devolver el string crudo o null
return undefined;
}
}
/** if (accion == undefined) {
* Metodo duplicado se puede generalizar la a una clase sharedController con las funciones basicas console.error("La routingKey es incorrecta: " + accion)
*/
private async tryUseCase<T extends any>
(msg: ConsumeMessage, usecase: () => Promise<Result<string, T>>): Promise<Result<string, T>> {
try {
const result = await usecase()
if (result.error == undefined) {
await this.eventBus.ack(msg)
return result
} else {
console.error("Error procesando el caso de uso (NOS)", result.error)
this.eventBus.nack(msg)
return result
}
} catch (e) {
console.error("Error general procesando el caso de uso (NOS)")
this.eventBus.nack(msg) this.eventBus.nack(msg)
return { return;
error: String(e)
}
} }
}
public activate() { if (this.routes.get(accion) == undefined) {
return async (msg: ConsumeMessage) => { console.error("No hay una ruta definida para la accion")
console.log("[i] Evento activate ", msg.fields) this.eventBus.nack(msg)
const data = this.validateMsg(msg) as SimEvents.activation return;
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() { try {
return async (msg: ConsumeMessage) => { this.routes.get(accion)!()
console.log("Evento suspend ", msg.fields) } catch (err) {
const data = this.validateMsg(msg) as SimEvents.suspend console.log("Error procesando el mensaje")
const iccid = data.payload.iccid this.eventBus.nack(msg)
const correlation_id = data.headers?.message_id } finally {
const res = await this.tryUseCase(msg, this.uscases.suspend({ this.eventBus.ack(msg)
iccid: iccid,
correlation_id: correlation_id
}))
return res;
}
}
public terminate() {
return async (msg: ConsumeMessage) => {
console.log("Evento termiante no soportado ", msg.fields)
}
}
public reActivate() {
return async (msg: ConsumeMessage) => {
console.log("Evento reActivate ", msg.fields)
const data = this.validateMsg(msg) as SimEvents.reActivation
const iccid = data.payload.iccid
const correlation_id = data.headers?.message_id
const res = await this.tryUseCase(msg, this.uscases.reactivate({
iccid: iccid,
correlation_id: correlation_id
}))
return res;
} }
} }
/** /**
* Select especificamente por REST para evitar pasar por las colas. * TODO:
* La respuesta es instantanea no se tiene que registrar como operación. * - Loguear motivos de la no validacion
*/ */
public selectREST() { private validateActivationMsg(msg: ConsumeMessage | null) {
return async (req: Request, res: Response) => { if (msg == undefined) return false;
const { query } = req return true;
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 })
if (usecaseRes.error != undefined) {
res.status(500).json(usecaseRes)
return;
} else {
const simComun = nosSimToCommonSim(usecaseRes.data)
res.status(200).send({ data: simComun })
return;
}
}
res.status(501).json({
errors: {
msg: "No está implementada la busqueda por lista de iccid"
}
})
}
}
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)
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 { SimNosController } from "./SimNOS.controller.js";
import { EventBus } from "sim-shared/domain/EventBus.port.js";
import { Result } from "sim-shared/domain/Result.js";
type FuncType = ((m: ConsumeMessage) => Promise<Result<string, any>>)
export class SimNosRouter {
private readonly routes: Map<string, FuncType>;
constructor(
private readonly simController: SimNosController,
private readonly eventBus: EventBus
) {
this.routes = new Map<string, FuncType>([
//["select", undefined],
["activate", this.simController.activate()],
["pause", this.simController.suspend()],
["reactivate", this.simController.reActivate()],
//["cancel", this.simController.terminate()],
//["preActivate", this.simController.preActivate()]
]);
}
/**
* Enruta el mensaje a la acción correspondiente basándose en la routing key
* TODO: No estoy seguro que deba meter el nack aqui
* - De moemento el ack-nack se gestiona en los controller, por si acaso hay casos
* limite en
*/
public route = async (msg: ConsumeMessage | null): Promise<void> => {
if (!msg) {
console.error("[Router] Mensaje vacío");
return;
}
const action = this.extractAction(msg);
if (!action) {
console.error("[Router] La routing key no tiene una acción definida", msg.fields.routingKey);
this.eventBus.nack(msg)
return;
}
const handler = this.routes.get(action);
if (!handler) {
console.error(`[Router] La acción '${action}' no tiene un controlador asociado`);
this.eventBus.nack(msg)
return;
}
try {
console.log("[Router] Ejecutando operación:", action);
// El controlador devuelve una función (thunk) que debe ser ejecutada
const executeParams = handler(msg);
if (typeof executeParams === "function") {
const res = await executeParams;
}
} catch (error) {
console.error(`[Router] Error al ejecutar la operación '${action}':`, error);
this.eventBus.nack(msg)
}
};
private extractAction(msg: ConsumeMessage): string | undefined {
// Se asume que la acción está en la tercera posición: domain.compañia.accion
return msg.fields.routingKey.split(".")[2];
}
}

View File

@@ -1,156 +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 { NosHttpClient } from "#infrastructure/NosHttpClient.js";
import { NosRepository } from "#infrastructure/NosRepository.js";
import { ErrorOrderDTO, FinishOrderDTO, UpdateOrderDTO } from "sim-shared/domain/Order.js";
import { Result } from "sim-shared/domain/Result.js";
import { OrderRepository } from "sim-shared/infrastructure/OrderRepository.js";
export class SimNosUsecases {
constructor(
private httpClient: NosHttpClient,
private nosRepository: NosRepository,
private orderRepository: OrderRepository
) {
}
private async setRunning(correlation_id: string) {
// En NOS el updateOrder se hace con el correlation_id que viene en la cabecera del
// mensaje consumido
const updateData: UpdateOrderDTO = {
new_status: "running",
correlation_id: correlation_id
}
const order = await this.orderRepository.updateOrder(updateData)
return order
}
private async setFinished(correlation_id: string) {
// En NOS el updateOrder se hace con el correlation_id que viene en la cabecera del
// mensaje consumido
const updateData: FinishOrderDTO = {
correlation_id: correlation_id
}
const order = await this.orderRepository.finishOrder(updateData)
return order
}
private async setFailed(correlation_id: string, reason: string, detail?: string) {
// En NOS el updateOrder se hace con el correlation_id que viene en la cabecera del
// mensaje consumido
const updateData: ErrorOrderDTO = {
status: "failed",
correlation_id: correlation_id,
reason: reason,
error: reason,
stackTrace: detail
}
console.log("SET FAILED DATA:", updateData)
const order = await this.orderRepository.errorOrder(updateData)
console.log("SET FAILED RES:", order)
return order
}
public usecaseTemplate<T, R>(
func: (_: T) => Promise<Result<string, R>>,
args: T,
correlation_id?: string | undefined
) {
return async () => {
// Operacion pending -> running
if (correlation_id != undefined)
this.setRunning(correlation_id)
.then()
.catch(e => console.error("Error actualizando el order", e))
try {
const res = await func(args)
if (res.error != undefined) {
console.log("Error peticion: ", res, correlation_id)
if (correlation_id != undefined)
this.setFailed(correlation_id, res.error)
.then(e => console.log("failed", e))
.catch(e => console.error(e))
return res;
} else {
if (correlation_id != undefined)
this.setFinished(correlation_id).then()
return res;
}
} catch (e) {
if (correlation_id != undefined)
this.setFailed(correlation_id, "Error general de operacion de SIM (NOS) ", String(e)).then()
return {
error: "Error general de operacion de SIM (NOS) " + String(e)
}
}
}
}
public activate(args: {
iccid: string,
correlation_id?: string
}) {
return this.usecaseTemplate(
(args) => this.nosRepository.activateSim(args), args.iccid, args.correlation_id)
}
public suspend(args: {
iccid: string,
correlation_id?: string
}) {
return this.usecaseTemplate(
(args) => this.nosRepository.bar(args), args.iccid, args.correlation_id)
}
public reactivate(args: {
iccid: string,
correlation_id?: string
}) {
return this.usecaseTemplate(
(args) => this.nosRepository.unbar(args), args.iccid, args.correlation_id)
}
public terminate(args: { iccid: string }) {
throw new Error("No hay termination para NOS")
}
/* Importante: Las operaciones de lectua no dejan registro en orders */
public async selectOne(args: {
iccid: string
}) {
const res = await this.nosRepository.getLineInfo(args.iccid)
return res
}
public async selectPage(args: {
offset?: number,
limit?: number,
filter?: string,
orderBy?: string
}) {
const res = await this.nosRepository.getLinePage(args)
return res
}
/**
public selectMany(args: {
iccid: string[]
}) {
return {}
}
*/
}

View File

@@ -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

@@ -13,6 +13,7 @@ try {
} }
export const env = { export const env = {
ENVIRONMENT: process.env.ENVIORMENT,
POSTGRES_USER: process.env.POSTGRES_USER, POSTGRES_USER: process.env.POSTGRES_USER,
POSTGRES_PASSWORD: process.env.POSTGRES_PASSWORD, POSTGRES_PASSWORD: process.env.POSTGRES_PASSWORD,
POSTGRES_PORT: process.env.POSTGRES_PORT, POSTGRES_PORT: process.env.POSTGRES_PORT,
@@ -29,11 +30,7 @@ export const env = {
RABBITMQ_RETRY_INTERVAL: process.env.RABBITMQ_INTERVAL, RABBITMQ_RETRY_INTERVAL: process.env.RABBITMQ_INTERVAL,
RABBITMQ_VHOST: String(process.env.RABBITMQ_VHOST), 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 // ESPECIFICO NOS
NOS_BASE_URL: String(process.env.NOS_BASE_URL), 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 { RabbitMQEventBus, RMQConnectionParams } from "sim-shared/infrastructure/RabbitMQEventBus.js" import { RabbitMQEventBus, RMQConnectionParams } from "sim-shared/infrastructure/RabbitMQEventBus.js"
import { Channel } from "amqp-connection-manager" import { Channel } from "amqp-connection-manager"
import { env } from "./env/env.js" import { env } from "./env/index.js"
const rmqUser = env.RABBITMQ_USER const rmqUser = env.RABBITMQ_USER
const rmqPass = env.RABBITMQ_PASSWORD const rmqPass = env.RABBITMQ_PASSWORD
@@ -18,28 +18,24 @@ export const rmqConnOptions = <RMQConnectionParams>{
secure: rmqSecure, secure: rmqSecure,
} }
const QUEUES = {
NOS: "sim.nos",
NOSDLX: "sim.nos.dlx",
NOSDEL: "sim.nos.delayed",
}
const EXCHANGES = {
MAIN: "sim.exchange",
DLX: "sim.ex.nos.dlx",
DEL: "sim.ex.nos.delayed"
}
export const rabbitmqEventBus = new RabbitMQEventBus({ export const rabbitmqEventBus = new RabbitMQEventBus({
connectionParams: rmqConnOptions, connectionParams: rmqConnOptions,
buildStructure: buildQueues, buildStructure: buildQueues,
maxRetry: 2, maxRetry: 5
delayedExchange: EXCHANGES.DEL,
dlxExchange: EXCHANGES.DLX
}) })
async function buildQueues(channel: Channel) { async function buildQueues(channel: Channel) {
const QUEUES = {
NOS: "sim.nos",
NOSDLX: "sim.nos.dlx",
NOSDEL: "sim.nos.delayed",
}
const EXCHANGES = {
MAIN: "sim.exchange",
DLX: "sim.ex.nos.dlx",
DEL: "sim.ex.nos.delayed"
}
const DELAY = 10 * 1000 const DELAY = 10 * 1000
const BASE_NOS_KEY = "sim.nos.#" const BASE_NOS_KEY = "sim.nos.#"
@@ -64,6 +60,7 @@ async function buildQueues(channel: Channel) {
await channel.bindQueue(QUEUES.NOSDEL, EXCHANGES.DEL, BASE_NOS_KEY) await channel.bindQueue(QUEUES.NOSDEL, EXCHANGES.DEL, BASE_NOS_KEY)
// Cola nos -> main exchange // Cola nos -> main exchange
await channel.bindQueue(QUEUES.NOS, EXCHANGES.MAIN, BASE_NOS_KEY) await channel.bindQueue(QUEUES.NOS, EXCHANGES.MAIN, BASE_NOS_KEY)
} }
export async function startRMQClient() { export async function startRMQClient() {

View File

@@ -1,18 +0,0 @@
import { Pool, QueryResult } from 'pg';
import { PgClient } from 'sim-shared/infrastructure/PgClient.js'
import { env } from './env/env.js';
// Configuracion de la conexion a la BDD, deberia ser la
// Misma para todos los servicios pero hasta que se unifique todo
// se hace una por servicio.
export const pgPool = new Pool({
user: env.POSTGRES_USER,
host: env.POSTGRES_HOST,
database: env.POSTGRES_DATABASE,
password: env.POSTGRES_PASSWORD,
port: Number(env.POSTGRES_PORT) || 5433,
});
export const pgClient = new PgClient({
pool: pgPool
})

View File

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

View File

@@ -1,39 +0,0 @@
import { CommonSim } from "sim-shared/domain/CommonSim.js";
import { NosApi } from "./NosAPI.js";
const billingStates = new Map<
NosApi.LineData["billingState"]["currentState"],
CommonSim<any>["billing_status"]>([
["active", "ACTIVE"],
["terminated", "TERMINATED"]
])
const networkStates = new Map<
NosApi.LineData["networkState"]["currentState"],
CommonSim<any>["network_status"]
>([
["active", "ACTIVE"],
["terminated", "TERMINATED"],
["barred", "SUSPENDED"]
])
export function nosSimToCommonSim(nosSim: NosApi.LineData): CommonSim<NosApi.LineData> {
const billingState = billingStates.get(nosSim.billingState.currentState) ?? "UNKNOWN"
const networkState = networkStates.get(nosSim.networkState.currentState) ?? "UNKNOWN"
const commonSim: CommonSim<NosApi.LineData> = {
company: "NOS",
tariff: nosSim.tariffName,
iccid: nosSim.physicalId,
msisdn: nosSim.subscriberId,
billing_status: billingState!,
network_status: networkState!,
raw: nosSim,
imei: nosSim.imei,
activation_date: new Date(nosSim.connectionDate),
termination_date: new Date(nosSim.terminateDate),
suspension_date: null // NOS no especifica la fecha de de 'barred' que equivale a la suspension
}
return commonSim
}

View File

@@ -1,74 +1,14 @@
import express from "express"
import cors from 'cors';
import { SimNosRouter } from "./aplication/SimNOS.router.js"
import { SimNosController } from "./aplication/SimNOS.controller.js"
import { SimNosUsecases } from "./aplication/SimNOS.usecases.js"
import { NosHttpClient } from "./infrastructure/NosHttpClient.js"
import { env } from "#config/env/env.js"
import { NosRepository } from "./infrastructure/NosRepository.js"
import { OrderRepository } from "sim-shared/infrastructure/OrderRepository.js";
import { pgClient } from "#config/postgreConfig.js";
import { startRMQClient } from "#config/eventBus.config.js";
const RMQ_QUEUE = "sim.nos" import { startRMQClient } from "#config/eventBus.config.js"
const NOS_BASE_URL = env.NOS_BASE_URL import { SimNosController } from "./aplication/SimNOS.controller.js"
const PORT = env.APP_PORT
const HOSTNAME = env.APP_HOST
async function startWorker() { async function startWorker() {
// Instancia de dependencias
const rmqClient = await startRMQClient() const rmqClient = await startRMQClient()
const nosHttpClient = new NosHttpClient(
NOS_BASE_URL
)
const nosRepository = new NosRepository(
nosHttpClient
)
const orderRepository = new OrderRepository(
pgClient
)
const simUsecases = new SimNosUsecases(
nosHttpClient,
nosRepository,
orderRepository
)
const simController = new SimNosController( const simController = new SimNosController(
simUsecases,
rmqClient rmqClient
) )
const simRouter = new SimNosRouter( rmqClient.consume("sim.nos", simController.recibeMsg)
simController,
rmqClient
)
// RMQ
rmqClient.consume(RMQ_QUEUE, simRouter.route)
.then(() => console.log("Cliente rmq creado con exito"))
.catch(e => console.error("Error conectando con RABBITMQ", e))
// Express
const app = express()
app.use(cors());
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
app.get("/select", simController.selectREST())
app.get("/selectPage", simController.selectPageREST())
app.listen(PORT, HOSTNAME, (e) => {
if (e == undefined) {
console.log("[o] Servidor (NOS) iniciado en el puerto %d", PORT)
} else {
console.error("Error express ", e)
}
})
} }
startWorker() startWorker()

View File

@@ -1,41 +0,0 @@
import axios, { AxiosInstance } from "axios";
import { env } from "#config/env/env.js"
export class NosHttpClient {
public client: AxiosInstance;
constructor(
private baseURL: string,
//private jwtManager: JWTProvider<any>
) {
this.client = axios.create({
baseURL: baseURL
})
// Interceptor para los headers fijos
this.client.interceptors.request.use(
async (config) => {
// Configuracion especifica de NOS (El token simepre es el mismo?)
const token = env.NOS_ACCESS_TOKEN;
config.headers.Authorization = `Bearer ${token}`
config.headers.set("content-type", "application/json")
return config
},
(error) => Promise.reject(error)
)
}
get post() {
return this.client.post
}
get patch() {
return this.client.patch
}
get get() {
return this.client.get
}
}

View File

@@ -1,177 +0,0 @@
import { Result } from "sim-shared/domain/Result.js";
import { NosHttpClient } from "./NosHttpClient.js";
import { NosApi } from "#domain/NosAPI.js";
import axios, { AxiosError, AxiosResponse } from "axios";
export class NosRepository {
constructor(
private httpClient: NosHttpClient
) {
}
/**
* E => Tipo de error
* T => Tipo de dato para cod 200
*
* TODO:
* - Mejor gestion de los errores
* - E no se aplica todavia por no hacer la transformacion del error
*/
private async manageNosRequest<E, T>(promise: Promise<AxiosResponse<T>>): Promise<Result<string, T>> {
try {
const res = await promise
return {
data: res.data
}
} catch (e) {
if (axios.isAxiosError(e)) {
const error = e as AxiosError
return {
error: error.code + " : " + String(error.response?.statusText)
}
} else {
return {
error: String(e)
}
}
}
}
public async getLineInfo(iccid: string): Promise<Result<string, NosApi.LineData>> {
const PATH = "/subscribers/" + iccid
console.log("PAth", PATH)
const lineRequest = this.httpClient.get<NosApi.LineDataResponseOK>(PATH)
const lineResponse = await this.manageNosRequest<string, NosApi.LineDataResponseOK>(lineRequest)
if (lineResponse.error != undefined) {
return lineResponse
} else {
return {
data: lineResponse.data.content
}
}
}
/**
* El metodo de NOS de paginar las lineas
* maximo por pagina 100, default 25
* no devuelve el offset ni el numero de elementos restantes
* hay que llevar la cuenta
*/
public async getLinePage(args: {
limit?: number,
offset?: number,
filter?: string,
orderBy?: string
}): Promise<Result<string, any>> {
const PATH = "/subscribers"
const LIMIT = 100
const options = {
limit: args.limit ?? LIMIT,
offset: args.offset ?? 0,
filter: args.filter,
orderBy: args.orderBy
}
const pageRequest = this.httpClient.get(PATH, {
params: options
})
const pageResponse = await this.manageNosRequest<string, any>(pageRequest)
if (pageResponse.error != undefined) {
return pageResponse
} else {
return {
data: pageResponse.data.content
}
}
}
public async getLinesInfo(iccid: string[]) /*Promise<Result<string, NosApi.LineData>>*/ {
throw new Error("NOS no permite buscar iccid en bulk, se puede hacer un apaño pero está en proceso")
const PATH = "/subscribers"
const LIMIT = 100
const steps = Math.ceil(iccid.length / LIMIT)
const options = {
limit: LIMIT,
offset: 0,
}
const req = this.httpClient.post<NosApi.LineDataResponseOK>(PATH)
const resp = await this.manageNosRequest<string, NosApi.LineDataResponseOK>(req)
if (resp.error != undefined) {
return resp
} else {
return {
//@ts-expect-error
data: resp.data.content
}
}
}
public async activateSim(iccid: string): Promise<Result<string, NosApi.ActivationData>> {
const PATH = '/provisioning'
const PRODUCT_ID = 1330 // No se que es, preguntar a Ivan
const data = {
productSetId: PRODUCT_ID
}
const req = this.httpClient.post<NosApi.ActivateResponseOK>(PATH, data)
const resp = await this.manageNosRequest<string, NosApi.ActivateResponseOK>(req)
if (resp.error != undefined) {
return resp
} else {
return {
data: resp.data.content
}
}
}
/**
* "A bar is a service provisioning action that results in a subscriber being blocked from accessing an operator's network. The bar remains in place until the operator is sent an unbar request."
* Se entiende que un "bar" es una suspension temporal
*/
public async bar(iccid: string) {
const PATH = `/subscribers/${iccid}/products`
const data = {
product: "BAR DN TOTAL",
action: "enable"
}
const req = this.httpClient.patch<NosApi.BarResponseOk>(PATH, data)
const resp = await this.manageNosRequest<string, NosApi.BarResponseOk>(req)
if (resp.error != undefined) {
return resp
} else {
return {
data: resp.data.content
}
}
}
public async unbar(iccid: string) {
const PATH = `/subscribers/${iccid}/products`
const data = {
product: "BAR DN TOTAL",
action: "disable"
}
const req = this.httpClient.patch<NosApi.BarResponseOk>(PATH, data)
const resp = await this.manageNosRequest<string, NosApi.BarResponseOk>(req)
if (resp.error != undefined) {
return resp
} else {
return {
data: resp.data.content
}
}
}
}

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