72 Commits

Author SHA1 Message Date
3f7c052572 config antigua 2026-05-13 17:30:51 +02:00
3d9e2a6e9b Nuevo lock 2026-05-13 17:27:28 +02:00
f94ee799d0 Problema de tipado 2026-05-13 17:20:31 +02:00
d1e5892a0d Mejorado el control de errores para ALAI 2026-05-13 17:08:19 +02:00
b14464da39 Bug preactivacion si id de operacion 2026-05-13 13:45:28 +02:00
cf6204e231 Problema de select tarjetas no preactivadas 2026-05-13 13:12:28 +02:00
9d8788db39 Preactivacion y activacion alai 2026-05-13 12:46:30 +02:00
79ceb74604 Activación con externalId 2026-05-12 17:36:22 +02:00
d02b1d48bf Correccion id de subscripcion de alai 2026-05-12 17:25:26 +02:00
1fb50323b0 Fix error imei undefined 2026-05-12 16:19:24 +02:00
985c85da59 Colas persistentes 2026-05-12 13:37:48 +02:00
23c61097a8 Env por defecto para NOS 2026-05-12 13:18:24 +02:00
5372045bd7 Error path 2026-05-12 12:51:06 +02:00
72d61b8376 Error path 2026-05-12 12:04:32 +02:00
526c094494 Crear dir de certificados 2026-05-12 11:13:14 +02:00
474f7b7c68 nombre 2026-05-12 08:56:37 +02:00
f8692e3e2e cert 2026-05-12 08:46:08 +02:00
62715fae34 Fix jenkisfile 2026-05-12 08:34:23 +02:00
f8678a68bb compose 2026-05-11 17:37:35 +02:00
8f8ae22f23 tado p12 2026-05-11 17:35:47 +02:00
2611147eb3 debug 2026-05-11 17:25:48 +02:00
b0b3badd5c test path 2026-05-11 16:59:16 +02:00
2b812098fb Ruta 2026-05-11 16:56:29 +02:00
3146efec64 Faltaba alai 2026-05-11 16:49:38 +02:00
44e4b98e35 errata 2026-05-11 15:54:13 +02:00
6adb4d5c95 Codigo no usable 2026-05-11 15:52:40 +02:00
bcb1a28164 build limpia 2026-05-11 15:30:51 +02:00
d5798602e2 Error nombre paquete 2026-05-11 15:16:09 +02:00
1f94a89520 Ordern correcto de operaciones 2026-05-11 13:08:35 +02:00
44bbc8f17c Scripts distintos para prod 2026-05-11 12:51:13 +02:00
9e6877c329 Cambiado a cp -P para mantener symlink 2026-05-11 12:42:47 +02:00
d5883ba75e Links simbolicos para .env 2026-05-11 12:36:29 +02:00
421d0aa705 Probando con links simbolicos 2026-05-11 12:24:29 +02:00
69b5958296 Merge branch 'main' of git.savefamilygps.net:SaveFamily/sf-sim 2026-05-11 12:16:36 +02:00
86478b1073 Comillas 2026-05-11 12:15:37 +02:00
b9e3e1784f Merge pull request 'WEBINT-335_migracion_alai' (#4) from WEBINT-335_migracion_alai into main
Reviewed-on: #4
2026-05-11 08:12:54 +00:00
976cf1c3d2 delete 2026-05-11 10:10:17 +02:00
e4ba1576e5 delete env 2026-05-11 10:09:16 +02:00
4baa9f708f Certificado p12 2026-05-11 10:05:26 +02:00
6fb25e6055 Ignore p12 certificado 2026-05-11 09:23:49 +02:00
63698ee1aa Misma info de sim para el /select 2026-05-08 14:49:43 +02:00
410f659db0 Sim comun para nos 2026-05-08 12:06:24 +02:00
08c972e720 Todos los datos de Alai para agrupar 2026-05-07 16:19:18 +02:00
c4e4d87303 Imei from subscription, SIM común 2026-05-07 13:53:02 +02:00
9c74fb9a7b Funcionan todos los select, pendiente el general 2026-05-07 11:37:35 +02:00
1d7c2b2946 puertos 2026-05-07 10:36:16 +02:00
d2c86396b1 Organizacion docs 2026-05-07 09:20:26 +02:00
3cf5c3695e Pruebas a los endpoints internos 2026-05-06 16:59:09 +02:00
7dda25fbfb Router para el select de cualquier sim 2026-05-06 16:47:52 +02:00
eefb7c5a79 pruebas 2026-05-05 17:31:08 +02:00
13944a64d2 ontroladores y rutas 2026-05-05 17:01:24 +02:00
07e60690ab Ajuste de preactivacion con externalID 2026-05-05 14:24:24 +02:00
036ae20ac3 Subscripciones de ALAI 2026-05-05 13:12:31 +02:00
189de6c0fb La conexión con alai funciona 2026-05-04 15:12:53 +02:00
113d9f3786 Debug para tokens de Alai 2026-05-04 13:39:54 +02:00
331d920379 Operaciones basicas ALAI 2026-05-04 09:37:06 +02:00
a615fc2b81 Agente https para certificados 2026-04-30 17:41:49 +02:00
f98097d11d Estructura para el token de alai
cabecera automatica de bearer para todas las requests a alai
2026-04-30 15:49:59 +02:00
3e76c3c931 Merge branch 'main' into WEBINT-335_migracion_alai 2026-04-29 17:12:55 +02:00
bb31efb271 Merge main -> migracion alai 2026-04-29 17:08:30 +02:00
c9733113cf Persistencia de rabbit 2026-04-29 13:25:46 +02:00
4c1d6ac2c4 No se crean los enlaces simbolicos en start 2026-04-29 13:19:47 +02:00
07e7a0d457 .env de NOS en el compose 2026-04-29 12:47:59 +02:00
48361ab33f El .env no se deberia mover en el paso de build 2026-04-29 12:24:27 +02:00
a3c7c224b1 Copia del env de nos 2026-04-29 11:26:00 +02:00
324aec3001 Merge branch 'WEBINT-338_tiempo_suspension' 2026-04-28 17:25:09 +02:00
f78a333e1e Merge pull request 'WEBINT-338_tiempo_suspension' (#2) from WEBINT-338_tiempo_suspension into main
Reviewed-on: #2
2026-04-28 13:40:03 +00:00
c91965567d Actualizacion docs 2026-04-27 15:17:20 +02:00
858932f260 Test 2026-04-27 11:11:47 +02:00
a84f600fa2 Test orders 2026-04-27 11:05:09 +02:00
4e02ea021d Docs orders 2026-04-27 09:33:55 +02:00
9ec127433d Template alai 2026-04-23 13:18:50 +02:00
124 changed files with 3229 additions and 237 deletions

4
.gitignore vendored
View File

@@ -16,8 +16,10 @@ node_modules
#!.yarn/cache #!.yarn/cache
.pnp.* .pnp.*
# Certificados
*.pem *.pem
*.p12
*.key
dist/* dist/*

View File

@@ -2,10 +2,12 @@ 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

@@ -43,3 +43,4 @@ La decisión del numero de reintentos y la cola de dlx se hace en los servicios,
- **3000**: Gateway (sim-entrada-eventos) - **3000**: Gateway (sim-entrada-eventos)
- **3001**: Consumidor NOS (sim-consumidor-nos) - **3001**: Consumidor NOS (sim-consumidor-nos)
- **3002**: Consumidor Objenious (sim-consumidor-objenious) - **3002**: Consumidor Objenious (sim-consumidor-objenious)
- **3003**: Consumidor Alai (sim-consumidor-alai)

View File

@@ -24,7 +24,6 @@ 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,6 +6,8 @@ networks:
external: true external: true
internal: internal:
driver: bridge driver: bridge
volumes:
rabbitmq_data:
services: services:
rabbitmq-sim-broker: rabbitmq-sim-broker:
@@ -28,6 +30,7 @@ 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
@@ -72,7 +75,10 @@ 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' sh 'yarn run build:prod'
} }
} }
stage("🏗 Deploying") { stage("🏗 Deploying") {
@@ -38,14 +38,40 @@ 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"
@@ -54,12 +80,6 @@ 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,90 +1,92 @@
{ {
"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": ["administrator"] "tags": [
} "administrator"
], ]
"vhosts": [ }
{ ],
"name": "sim-vhost" "vhosts": [
} {
], "name": "sim-vhost"
"permissions": [ }
{ ],
"user": "RABBITMQ_USER_PLACEHOLDER", "permissions": [
"vhost": "sim-vhost", {
"configure": ".*", "user": "RABBITMQ_USER_PLACEHOLDER",
"write": ".*", "vhost": "sim-vhost",
"read": ".*" "configure": ".*",
} "write": ".*",
], "read": ".*"
"topic_permissions": [], }
"parameters": [], ],
"global_parameters": [ "topic_permissions": [],
{ "parameters": [],
"name": "cluster_name", "global_parameters": [
"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": [ }
{ ],
"vhost": "sim-vhost", "policies": [
"name": "pol.sim.dlx", {
"pattern": "sim.*", "vhost": "sim-vhost",
"apply-to": "queues", "name": "pol.sim.dlx",
"definition": { "pattern": "sim.*",
"dead-letter-exchange": "sim.dlx" "apply-to": "queues",
}, "definition": {
"priority": 7 "dead-letter-exchange": "sim.dlx"
} },
], "priority": 7
"exchanges": [ }
{ ],
"name": "sim.exchange", "exchanges": [
"vhost": "sim-vhost", {
"type": "topic", "name": "sim.exchange",
"durable": true, "vhost": "sim-vhost",
"auto_delete": false, "type": "topic",
"internal": false, "durable": true,
"argurments": {} "auto_delete": false,
}, "internal": false,
{ "argurments": {}
"name": "sim.dlx", },
"vhost": "sim-vhost", {
"type": "topic", "name": "sim.dlx",
"durable": true, "vhost": "sim-vhost",
"auto_delete": false, "type": "topic",
"internal": false, "durable": true,
"argurments": {} "auto_delete": false,
} "internal": false,
], "argurments": {}
"queues": [ }
{ ],
"name": "sim.logs", "queues": [
"vhost": "sim-vhost", {
"durable": true, "name": "sim.logs",
"auto_delete": false, "vhost": "sim-vhost",
"arguments": {} "durable": true,
} "auto_delete": false,
], "arguments": {}
"bindings": [ }
{ ],
"source": "sim.exchange", "bindings": [
"vhost": "sim-vhost", {
"destination": "sim.logs", "source": "sim.exchange",
"destination_type": "queue", "vhost": "sim-vhost",
"routing_key": "sim.#", "destination": "sim.logs",
"arguments": {} "destination_type": "queue",
} "routing_key": "sim.#",
] "arguments": {}
} }
]
}

View File

@@ -7,6 +7,7 @@ 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"
@@ -23,6 +24,7 @@ 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

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

View File

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

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

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

View File

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

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

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

View File

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

Binary file not shown.

View File

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

View File

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

View File

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

File diff suppressed because one or more lines are too long

View File

@@ -6,16 +6,80 @@ meta {
post { post {
url: {{baseurl}}/sim/activate url: {{baseurl}}/sim/activate
body: formUrlEncoded body: json
auth: inherit auth: inherit
} }
body:json {
{
"iccid": "8934909001500561503"
}
}
body:form-urlencoded { body:form-urlencoded {
iccid: 8935103196306448300 iccid: 123
offer: SAVEFAMILY1 offer: mensual
} }
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: 8 seq: 9
} }
post { post {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,7 +1,7 @@
meta { meta {
name: France Suspended Lines name: France Suspended Lines
type: http type: http
seq: 17 seq: 16
} }
get { get {

View File

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

View File

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

View File

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

View File

@@ -5,15 +5,11 @@ meta {
} }
get { get {
url: {{baseurl}}/orders/message_id/019c93d3-014a-711d-b958-03dd629be78d url: {{baseurl}}/orders/message_id/019dbeaf-8abb-7783-8b51-94fbd9f0b0df
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

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

View File

@@ -1,7 +1,7 @@
meta { meta {
name: Pause name: Pause
type: http type: http
seq: 1 seq: 5
} }
post { post {

View File

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

View File

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

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

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

View File

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

View File

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

View File

@@ -1,4 +1,5 @@
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: 14 seq: 13
} }
get { get {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,15 +1,16 @@
{ {
"name": "sim-eventos", "name": "sim-eventos",
"version": "1.0.0", "version": "1.0.0",
"packageManager": "yarn@4.12.0", "packageManager": "yarn@4.14.1",
"workspaces": [ "workspaces": [
"packages/*" "packages/*"
], ],
"scripts": { "scripts": {
"test": "vitest watch", "test": "vitest watch",
"build": "rm -rf ./dist && yarn workspaces foreach -Api run build && cp .env dist/ && yarn setup:runtime", "build": "rm -rf ./dist && yarn workspaces foreach -Api run build && yarn setup:runtime",
"build:prod": "rm -rf ./dist && yarn workspaces foreach -Api run build:prod && yarn setup:runtime",
"setup:runtime": "mkdir -p dist/packages/node_modules && ln -sf ../sim-shared dist/packages/node_modules/sim-shared && ln -sf ../sf-consumidor-objenious dist/packages/node_modules/sim-consumidor-objenious", "setup:runtime": "mkdir -p dist/packages/node_modules && ln -sf ../sim-shared dist/packages/node_modules/sim-shared && ln -sf ../sf-consumidor-objenious dist/packages/node_modules/sim-consumidor-objenious",
"start": "yarn setup:runtime && yarn workspaces foreach -Apiv run start", "start": "yarn workspaces foreach -Apiv run start",
"typecheck": "npx tsc --noEmit", "typecheck": "npx tsc --noEmit",
"dev": "yarn workspaces foreach -Apiv run dev", "dev": "yarn workspaces foreach -Apiv run dev",
"lint": "eslint .", "lint": "eslint .",

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

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

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

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

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

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

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

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

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

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

View File

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

View File

@@ -0,0 +1,41 @@
{
"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

@@ -5,6 +5,8 @@ import { EventBus } from "sim-shared/domain/EventBus.port.js";
import { Result } from "sim-shared/domain/Result.js"; import { Result } from "sim-shared/domain/Result.js";
import { SimEvents } from "sim-shared/domain/SimEvents.js"; import { SimEvents } from "sim-shared/domain/SimEvents.js";
import { iccidValidator } from "./httpValidators.js"; import { iccidValidator } from "./httpValidators.js";
import { error } from "node:console";
import { nosSimToCommonSim } from "#domain/transformers.js";
export class SimNosController { export class SimNosController {
@@ -127,7 +129,7 @@ export class SimNosController {
const validateBody = iccidValidator.validate(body); const validateBody = iccidValidator.validate(body);
if (validateBody.error != undefined) { if (validateBody.error != undefined) {
res.status(402).json(validateBody) res.status(422).json(validateBody)
return; return;
} }
@@ -142,12 +144,17 @@ export class SimNosController {
res.status(500).json(usecaseRes) res.status(500).json(usecaseRes)
return; return;
} else { } else {
res.send(usecaseRes.data) const simComun = nosSimToCommonSim(usecaseRes.data)
res.status(200).send({ data: simComun })
return; return;
} }
} }
res.status(200).json(validateBody) res.status(501).json({
errors: {
msg: "No está implementada la busqueda por lista de iccid"
}
})
} }
} }
@@ -168,7 +175,7 @@ export class SimNosController {
res.status(500).json(usecaseRes) res.status(500).json(usecaseRes)
return; return;
} else { } else {
res.status(200).send(usecaseRes.data) res.status(200).send(usecaseRes)
return; return;
} }
} }

View File

@@ -29,8 +29,8 @@ 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.APP_PORT), APP_PORT: Number(process.env.NOS_PORT ?? 3001),
APP_HOST: String(process.env.APP_HOST), 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),

View File

@@ -103,7 +103,7 @@ export namespace NosApi {
export type NetworkState = { export type NetworkState = {
currentStateId: number currentStateId: number
currentState: string currentState: "active" | "barred" | "terminated"
isTransferring: boolean isTransferring: boolean
lastTransferred: number lastTransferred: number
isOnline: boolean isOnline: boolean
@@ -112,7 +112,7 @@ export namespace NosApi {
export type BillingState = { export type BillingState = {
currentStateId: number currentStateId: number
currentState: string currentState: "active" | "terminated"
} }
export type BarData = { export type BarData = {

View File

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

@@ -63,7 +63,7 @@ async function startWorker() {
app.listen(PORT, HOSTNAME, (e) => { app.listen(PORT, HOSTNAME, (e) => {
if (e == undefined) { if (e == undefined) {
console.log("[o] Servidor iniciado en el puerto %d", PORT) console.log("[o] Servidor (NOS) iniciado en el puerto %d", PORT)
} else { } else {
console.error("Error express ", e) console.error("Error express ", e)
} }

View File

@@ -39,7 +39,8 @@
}, },
"scripts": { "scripts": {
"test": "echo \"Error: no test specified\" && exit 1", "test": "echo \"Error: no test specified\" && exit 1",
"build": "yarn tsc --project tsconfig.json && yarn tsc-alias && cp package.json ../../dist/packages/sim-consumidor-nos/", "build": "yarn tsc --project tsconfig.json && yarn tsc-alias && cp -P .env package.json ../../dist/packages/sim-consumidor-nos/",
"build:prod": "yarn tsc --project tsconfig.json && yarn tsc-alias && cp -P package.json ../../dist/packages/sim-consumidor-nos/",
"esbuild": "esbuild index.ts --platform=node", "esbuild": "esbuild index.ts --platform=node",
"start": "node ../../dist/packages/sim-consumidor-nos/index.js", "start": "node ../../dist/packages/sim-consumidor-nos/index.js",
"dev": "tsx watch index.ts" "dev": "tsx watch index.ts"

View File

@@ -1,12 +0,0 @@
# claves de Objenious
HOST=0.0.0.0
OBJ_PEM_PATH=./obj.pem
OBJ_AUTHORIZATION=XOc7FtwXD8hUX2SFVX94XSty8wkOmChkwDNF09O_aIxPubMDdFUdCDCB4zpzSIxi8nOcTg7r_LM_nmd5qm7uLbksf_XArjI8iAyhjKz_2BAXPhmvKs4Fc9f3vv5LDfCVrPB9lP8P7rJ66_qnWs4jvhLQxSfn29m96hgXeCf8oySdIDUjN2q9Js3KAS5LL52Ri6ryvUeO1PvMhaPQMWRqoHIqTV1wPfPtiqQwcjUPmu5GeW164Kq1JLgV3KaGzfCZ9Qv9lbv30EJrukXxWuLCAhBS0kzrBXZoWvf2pb9uh3Am_93_dDxiIGQfIap9ZU_m8ZD1HPgvZOMCY6ZkxQconQ
OBJ_CLI_ASSERTION=XOc7FtwXD8hUX2SFVX94XSty8wkOmChkwDNF09O_aIxPubMDdFUdCDCB4zpzSIxi8nOcTg7r_LM_nmd5qm7uLbksf_XArjI8iAyhjKz_2BAXPhmvKs4Fc9f3vv5LDfCVrPB9lP8P7rJ66_qnWs4jvhLQxSfn29m96hgXeCf8oySdIDUjN2q9Js3KAS5LL52Ri6ryvUeO1PvMhaPQMWRqoHIqTV1wPfPtiqQwcjUPmu5GeW164Kq1JLgV3KaGzfCZ9Qv9lbv30EJrukXxWuLCAhBS0kzrBXZoWvf2pb9uh3Am_93_dDxiIGQfIap9ZU_m8ZD1HPgvZOMCY6ZkxQconQ
OBJ_CLIENT_ID=savefamily_rest_ws
OBJ_KID=xNfbMiyL1ORXGP8lElhcv8nVaG3EJKye4Lc1YoN3I1E
OBJ_BASE_URL=https://api-getway.objenious.com/ws
OBJ_CUSTOMER_CODE=9.49411.10
//OBJ_BASE_URL=https://api-getway.objenious.com/ws/test

View File

@@ -7,6 +7,8 @@ import { ActionData } from "#domain/DTOs/objeniousapi.js";
import { Request, Response } from "express" import { Request, Response } from "express"
import { PaginationArgs, QueryPaginationArgs } from "sim-shared/domain/PaginationArgs.js"; import { PaginationArgs, QueryPaginationArgs } from "sim-shared/domain/PaginationArgs.js";
import { paginationValidator } from "./httpValidators.js"; import { paginationValidator } from "./httpValidators.js";
import { error } from "node:console";
import { objeniousSimToCommon } from "#domain/transformers.js";
/** /**
* La clase usa generadores de funciones para mantener el contexto * La clase usa generadores de funciones para mantener el contexto
@@ -236,7 +238,7 @@ export class SimController {
const validationRes = paginationValidator.validate(paginationArgs) const validationRes = paginationValidator.validate(paginationArgs)
if (validationRes.error != undefined) { if (validationRes.error != undefined) {
res.status(402).json(validationRes) res.status(422).json(validationRes)
return; return;
} }
@@ -252,6 +254,28 @@ export class SimController {
} }
} }
/**
* Una única linea para /select
*/
public queryLine() {
return async (req: Request, res: Response) => {
const queryParams = req.query
const queryArgs = {
iccid: queryParams.iccid as string // La validacion de iccid se ha tenido que hacer en el gateway
}
const line = await this.useCases.getLineByIccid(queryArgs.iccid)
if (line.error != undefined) {
res.status(line.error.code).json(line)
return;
}
const commonLine = objeniousSimToCommon(line.data)
res.status(200).json({ data: commonLine })
}
}
/** /**
* TODO: * TODO:
* - Loguear motivos de la no validacion * - Loguear motivos de la no validacion

View File

@@ -20,7 +20,7 @@ export class SimRouter {
["pause", this.simController.suspend()], ["pause", this.simController.suspend()],
["cancel", this.simController.terminate()], ["cancel", this.simController.terminate()],
["reactivate", this.simController.reActivate()], ["reactivate", this.simController.reActivate()],
["preActivate", this.simController.preActivate()] ["preactivate", this.simController.preActivate()]
]); ]);
} }

View File

@@ -185,7 +185,7 @@ export class SimUseCases {
console.log("Sim preactivada con exito", resp.data) console.log("Sim preactivada con exito", resp.data)
const operation: ObjeniousOperation = { const operation: ObjeniousOperation = {
correlation_id: preActivateData.correlation_id, correlation_id: preActivateData.correlation_id,
operation: "preActivate", operation: "preactivate",
iccids: String(preActivateData.identifier.identifiers), iccids: String(preActivateData.identifier.identifiers),
status: "noMassID", status: "noMassID",
request_id: resp.data.requestId request_id: resp.data.requestId
@@ -488,4 +488,32 @@ export class SimUseCases {
} }
} }
public async getLineByIccid(iccid: string):
Promise<Result<{ msg: string, code: number }, ObjeniousLine>> {
const line = await this.objeniousRepository.getLineByIccid(iccid)
if (line.error != undefined) {
return {
error: {
msg: "Error general buscando la sim",
code: 500
}
}
}
if (line.data.length == 0) {
return {
error: {
msg: "Sim no encontrada",
code: 204
}
}
}
return {
data: line.data[0]
}
}
} }

View File

@@ -46,8 +46,8 @@ async function buildQueues(channel: Channel) {
await channel.assertExchange(EXCHANGES.DLX, "topic") await channel.assertExchange(EXCHANGES.DLX, "topic")
await channel.assertExchange(EXCHANGES.MAIN, "topic") await channel.assertExchange(EXCHANGES.MAIN, "topic")
await channel.assertQueue(QUEUES.OBJ) await channel.assertQueue(QUEUES.OBJ, { durable: true })
await channel.assertQueue(QUEUES.OBJDLX) await channel.assertQueue(QUEUES.OBJDLX, { durable: true })
await channel.assertQueue(QUEUES.OBJDEL, { await channel.assertQueue(QUEUES.OBJDEL, {
durable: true, durable: true,
arguments: { arguments: {

View File

@@ -31,7 +31,6 @@ const DEFAULT_DATA_JWT = {
iss: env.OBJ_CLIENT_ID, iss: env.OBJ_CLIENT_ID,
aud: "https://idp.docapost.io/auth/realms/GETWAY", aud: "https://idp.docapost.io/auth/realms/GETWAY",
jti: Date.now().toString(), jti: Date.now().toString(),
} }
function addIATHeaders(authHeaders: Object) { function addIATHeaders(authHeaders: Object) {

View File

@@ -0,0 +1,46 @@
import { CommonSim } from "sim-shared/domain/CommonSim.js";
import { ObjeniousLine } from "sim-shared/domain/objeniousLine.js";
type ObjeniousBillingStates = ObjeniousLine["status"]["billingStatus"]
type ObjeniousNetworkStates = ObjeniousLine["status"]["networkStatus"]
//"PREACTIVATED" | "ACTIVE" | "SUSPENDED" | "TERMINATED" | "UNKNOWN",
const objeiousStates = new Map<ObjeniousBillingStates, CommonSim<any>["billing_status"]>([
["ACTIVATED", "ACTIVE"],
["CANCELED", "TERMINATED"],
["SUSPENDED", "SUSPENDED"],
["TEST", "PREACTIVATED"]
])
const objeiousNetworkStates = new Map<ObjeniousNetworkStates, CommonSim<any>["network_status"]>([
["ACTIVATED", "ACTIVE"],
["CANCELED", "TERMINATED"],
["SUSPENDED", "SUSPENDED"],
["BARRED", "SUSPENDED"]
])
export function objeniousSimToCommon(objSim: ObjeniousLine): CommonSim<ObjeniousLine> {
const status = objeiousStates.get(objSim.status.billingStatus) ?? "UNKNOWN"
const networkStatus = objeiousNetworkStates.get(objSim.status.networkStatus) ?? "UNKNOWN"
const preDate = objSim.status.preactivationDate
const actDate = objSim.status.activationDate
const preactivate = (preDate) ? new Date(preDate) : null
const activate = (actDate) ? new Date(actDate) : null
const common: CommonSim<ObjeniousLine> = {
company: "OBJ",
tariff: objSim.offer.code,
iccid: objSim.identifier.iccid,
msisdn: objSim.identifier.msisdn,
billing_status: status,
network_status: networkStatus,
raw: objSim,
imei: objSim.identifier.imei,
preactivation_date: preactivate,
activation_date: activate
}
return common
}

View File

@@ -97,6 +97,7 @@ async function startWorker() {
* } * }
*/ */
app.get("/lines", simController.queryLines()) app.get("/lines", simController.queryLines())
app.get("/select", simController.queryLine())
assert.ok(port, "Puerto del servicio no definido") assert.ok(port, "Puerto del servicio no definido")

View File

@@ -55,7 +55,8 @@
"scripts": { "scripts": {
"test": "node --import tsx --test ./**/*.test.ts", "test": "node --import tsx --test ./**/*.test.ts",
"dev": "tsx watch index.ts", "dev": "tsx watch index.ts",
"build": "tsc --build && yarn tsc-alias -p tsconfig.json && cp .env package.json ../../dist/packages/sim-consumidor-objenious/", "build": "tsc --build && yarn tsc-alias -p tsconfig.json && cp -P ./.env ./package.json ../../dist/packages/sim-consumidor-objenious/",
"build:prod": "tsc --build && yarn tsc-alias -p tsconfig.json && cp -P ./package.json ../../dist/packages/sim-consumidor-objenious/",
"start": "node ../../dist/packages/sim-consumidor-objenious/index.js", "start": "node ../../dist/packages/sim-consumidor-objenious/index.js",
"type:test": "tsc --noEmit" "type:test": "tsc --noEmit"
}, },

View File

View File

@@ -32,14 +32,23 @@ export class OrderController {
} }
public getByQueueId() { public getByQueueId() {
return this.controllerGenerator<{ correlation_id: string }, { correlation_id: string }>({ return this.controllerGenerator<{ uuid: string }, { correlation_id: string }>({
validator: uuidValidator, validator: uuidValidator,
mapBody: (e) => ({ correlation_id: e.uuid }),
useCase: this.orderUseCases.getByQueueId(), useCase: this.orderUseCases.getByQueueId(),
onError: (data, error) => { console.error(error) }, onError: (data, error) => { console.error(error) },
onSuccess: (data) => console.log(data) onSuccess: (data) => console.log(data)
}) })
} }
public getByQuery() {
return this.controllerGenerator({
validator: undefined,
useCase: this.orderUseCases.getByQuery(),
onError: (data, error) => { console.error(error) },
onSuccess: (data) => console.log(data)
})
}
/** /**
* TODO: * TODO:
@@ -77,7 +86,7 @@ export class OrderController {
}) })
} }
// 2. Transformacion del body // 2. Transformacion del body O => P
let data: P = body; let data: P = body;
try { try {
if (args.mapBody != undefined) if (args.mapBody != undefined)

View File

@@ -1,3 +1,4 @@
import { OrderQuery } from "sim-shared/domain/Order.js";
import { PaginationArgs } from "sim-shared/domain/PaginationArgs.js"; import { PaginationArgs } from "sim-shared/domain/PaginationArgs.js";
import { OrderRepository } from "sim-shared/infrastructure/OrderRepository.js"; import { OrderRepository } from "sim-shared/infrastructure/OrderRepository.js";
@@ -36,4 +37,10 @@ export class OrderUsecases {
} }
} }
// WIP
public getByQuery() {
return async (args: OrderQuery) => {
return await this.orderRepository.getOrdersByQuery(args)
}
}
} }

View File

@@ -4,6 +4,8 @@ import { activationValidator, iccidValidator } from "./httpValidators.js"
import { companyFromIccid } from "#domain/companies.js" import { companyFromIccid } from "#domain/companies.js"
import { BodyValidator } from "sim-shared/aplication/BodyValidator.js" import { BodyValidator } from "sim-shared/aplication/BodyValidator.js"
import { tryCatch } from "sim-shared/domain/Result.js" import { tryCatch } from "sim-shared/domain/Result.js"
import { mapCompanyService } from "#config/servicesProxy.js"
import axios, { AxiosError, isAxiosError } from "axios"
export class SimController { export class SimController {
@@ -208,6 +210,59 @@ export class SimController {
} }
} }
/**
* Select no pasa por la cola de eventos al ser de solo lectura.
* Cada uno de los servicios de los proveedores tiene que aderirse al
* modelo común de datos de SIM + campo "raw"
*
* De momento se va a buscar por iccid, mas adlante por movil u otro criterio
*/
public select() {
return async (req: Request, res: Response) => {
console.log("SELECT: ", req.query)
const iccid = req.query.iccid as string
const validationRes = iccidValidator.validate({ iccid: iccid })
if (validationRes.error != undefined) {
res.status(422).json({
errors: {
...validationRes.error
}
})
return;
}
const company = companyFromIccid(iccid)
const url = mapCompanyService.get(company)
const endpoint = "/select"
if (url == undefined) {
console.error("[x] Error buscando el servicio para el select del iccid ", iccid)
}
try {
const respSelect = await axios.get(url + endpoint, { params: req.query })
res.json(respSelect.data)
} catch (err) {
if (isAxiosError(err)) {
const axiosErr = err as AxiosError
res.status(axiosErr.status ?? 500).json(err)
} else {
res.status(500).json({
errors: {
msg: "Error general buscando la sim"
}
})
}
}
}
}
/**
* -- WIP
* Esta funcion se plantea para guardar tarjetas que no han llegado desde
* un operador conocido
*/
public save() { public save() {
return async (req: Request, res: Response) => { return async (req: Request, res: Response) => {
try { try {

View File

@@ -164,15 +164,16 @@ export class SimUsecases {
Promise<Result<string, { iccid: string, message_id: string, operation: "preactivation" }>> { Promise<Result<string, { iccid: string, message_id: string, operation: "preactivation" }>> {
const preActivationEvent = <SimEvents.preActivation>{ const preActivationEvent = <SimEvents.preActivation>{
key: `sim.${args.compañia}.preActivate`, key: `sim.${args.compañia}.preactivate`,
payload: { payload: {
iccid: args.iccid iccid: args.iccid
} }
} }
console.log("[d] Pre - activation ", preActivationEvent)
await this.eventBus.publish([preActivationEvent]) const preActivationWithId = this.addMessage_id(preActivationEvent)
const preactivationWithId = this.addMessage_id(preActivationEvent) console.log("[d] Pre - activation ", preActivationWithId)
const createdOrder = await this.saveOrder<SimEvents.preActivation>(preactivationWithId) await this.eventBus.publish([preActivationWithId])
const createdOrder = await this.saveOrder<SimEvents.preActivation>(preActivationWithId)
if (createdOrder.error != undefined) { if (createdOrder.error != undefined) {
console.error(createdOrder.error) console.error(createdOrder.error)
return { return {
@@ -270,5 +271,6 @@ export class SimUsecases {
return this.eventBus.publish([cancelationEvent]) return this.eventBus.publish([cancelationEvent])
} }
} }

View File

@@ -32,10 +32,10 @@ const offerExists = <Validator<{ offer: string }>>{
validationFunc: (a: { offer: string }) => offers.has(a.offer), validationFunc: (a: { offer: string }) => offers.has(a.offer),
} }
const isUuidv7 = <Validator<{ correlation_id?: string }>>{ const isUuidv7 = <Validator<{ uuid?: string }>>{
field: "correlation_id", field: "uuid",
errorMsg: "El uuid no es un uuidv7 valido", errorMsg: "El uuid no es un uuidv7 valido",
validationFunc: (a) => a.correlation_id != undefined && a.correlation_id.length < 36 validationFunc: (a) => a.uuid != undefined && a.uuid.length < 36
} }
const definedId = <Validator<{ id?: number }>>{ const definedId = <Validator<{ id?: number }>>{
@@ -56,12 +56,27 @@ const validNumericId = <Validator<{ id?: number }>>{
validationFunc: (e) => e.id! >= 0 validationFunc: (e) => e.id! >= 0
} }
/**
* Por un problema arrastrado de alai, se tiene que guardar el orderId del pedido
* de la sim en un campo de Alai.
*/
const ifAlaiOrderId = <Validator<{ iccid: string, orderId?: string }>>{
field: "orderId",
errorMsg: "Es necesario incluir un id de pedido (orderId) en las activaciones de Alai",
validationFunc: (e) => {
const company = companyFromIccid(e.iccid)
if (company == "alai" && e.orderId == undefined) return false
return true
}
}
export const activationValidator = new BodyValidator<{ iccid: string, offer: string }>( export const activationValidator = new BodyValidator<{ iccid: string, offer: string }>(
[ [
iccidRequired, iccidRequired,
iccidLongitudValidator, iccidLongitudValidator,
iccidWithValidCompany, iccidWithValidCompany,
offerExists, offerExists,
ifAlaiOrderId
] ]
) )
@@ -73,7 +88,7 @@ export const iccidValidator = new BodyValidator<{ iccid: string }>(
] ]
) )
export const uuidValidator = new BodyValidator<{ correlation_id?: string }>([ export const uuidValidator = new BodyValidator<{ uuid?: string }>([
isUuidv7 isUuidv7
]) ])

View File

@@ -24,6 +24,8 @@ 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),
CONNECTIONS_URL: String(process.env.CONNECTIONS_URL), CONNECTIONS_URL: String(process.env.CONNECTIONS_URL),
OBJENIOUS_CONSUMER_URL: process.env.OBJENIOUS_CONSUMER_URL, OBJENIOUS_CONSUMER_URL: process.env.OBJENIOUS_CONSUMER_URL,
NOS_CONSUMER_URL: process.env.NOS_CONSUMER_URL, NOS_CONSUMER_URL: process.env.NOS_CONSUMER_URL,
ALAI_CONSUMER_URL: process.env.ALAI_CONSUMER_URL,
}; };

View File

@@ -0,0 +1,10 @@
import { env } from "./env/index.js";
export const mapCompanyService = new Map([
["alai", env.ALAI_CONSUMER_URL],
["nos", env.NOS_CONSUMER_URL],
["objenious", env.OBJENIOUS_CONSUMER_URL]
])

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