64 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
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
122 changed files with 3219 additions and 234 deletions

4
.gitignore vendored
View File

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

View File

@@ -2,10 +2,12 @@ compressionLevel: mixed
enableGlobalCache: false
enableScripts: true
nodeLinker: node-modules
npmRegistryServer: "https://registry.npmjs.org/"
npmScopes:
sf-alvar:
npmRegistryServer: "https://git.savefamilygps.net/api/packages/SaveFamily/npm/"
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)
- **3001**: Consumidor NOS (sim-consumidor-nos)
- **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
-- Campos de reintentos?
status order_status NOT NULL DEFAULT 'pending',
retry_count INT DEFAULT 0,
error_message TEXT, -- Razón del fallo

View File

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

View File

@@ -23,7 +23,7 @@ pipeline {
stage("🧱 Building") {
steps {
sh 'rm -rf dist/'
sh 'yarn run build'
sh 'yarn run build:prod'
}
}
stage("🏗 Deploying") {
@@ -38,10 +38,28 @@ pipeline {
cleanRemote: false,
execCommand: "mkdir -p $APP_REMOTE_PATH"
),
sshTransfer(
cleanRemote: false,
execCommand: "rm -rf $APP_REMOTE_PATH/dist"
),
sshTransfer(
cleanRemote: false,
execCommand: "ls -la $BASE_REMOTE_PATH/vault/savefamily/sf-sims/"
),
sshTransfer(
cleanRemote: false,
remoteDirectory: "$APP_REMOTE_PATH",
sourceFiles: "dist/**/*",
excludes: "dist/**/node_modules/**"
),
sshTransfer(
cleanRemote: false,
execCommand: "ln -sf $BASE_REMOTE_PATH/vault/savefamily/sf-sims/.env $APP_REMOTE_PATH/.env"
),
sshTransfer(
cleanRemote: false,
execCommand: "ln -sf $BASE_REMOTE_PATH/vault/savefamily/sf-sims/wsaccess_alaisecure_com_cert_client_new.p12 $APP_REMOTE_PATH/wsaccess_alaisecure_com_cert_client_new.p12"
),
sshTransfer(
cleanRemote: false,
execCommand: "ln -sf $BASE_REMOTE_PATH/vault/savefamily/sf-sims/sim-consumidor-objenious.env $APP_REMOTE_PATH/sim-consumidor-objenious.env"
@@ -50,6 +68,10 @@ pipeline {
cleanRemote: false,
execCommand: "ln -sf $BASE_REMOTE_PATH/vault/savefamily/sf-sims/sim-consumidor-nos.env $APP_REMOTE_PATH/sim-consumidor-nos.env"
),
sshTransfer(
cleanRemote: false,
execCommand: "ln -sf $BASE_REMOTE_PATH/vault/savefamily/sf-sims/sim-consumidor-alai.env $APP_REMOTE_PATH/sim-consumidor-alai.env"
),
sshTransfer(
cleanRemote: false,
execCommand: "ln -sf $BASE_REMOTE_PATH/vault/savefamily/sf-sims/sim-objenious-cron.env $APP_REMOTE_PATH/sim-objenious-cron.env"
@@ -58,12 +80,6 @@ pipeline {
cleanRemote: false,
execCommand: "ln -sf $BASE_REMOTE_PATH/vault/savefamily/sf-sims/obj.pem $APP_REMOTE_PATH/obj.pem"
),
sshTransfer(
cleanRemote: false,
remoteDirectory: "$APP_REMOTE_PATH",
sourceFiles: "dist/**/*",
excludes: "dist/**/node_modules/**"
),
sshTransfer(
cleanRemote: false,
remoteDirectory: "$APP_REMOTE_PATH",

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,7 +1,7 @@
meta {
name: ReActivate
type: http
seq: 13
seq: 12
}
post {
@@ -11,7 +11,7 @@ post {
}
body:form-urlencoded {
iccid: 8935103196306448300
iccid: 8934909001500561503
~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 {
name: Test Order
type: http
seq: 9
seq: 10
}
post {

View File

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

View File

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

View File

@@ -1,7 +1,7 @@
meta {
name: test proxy
type: http
seq: 14
seq: 13
}
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 {
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
auth: bearer
}
params:query {
pageSize: 1000
simStatus: ACTIVATED
~identifier.identifierType: ICCID
~identifier.identifiers: 8933201125065160455
identifier.identifierType: ICCID
identifier.identifiers: 8933201125065160455
~pageSize: 1000
~simStatus: ACTIVATED
}
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,13 +1,14 @@
{
"name": "sim-eventos",
"version": "1.0.0",
"packageManager": "yarn@4.12.0",
"packageManager": "yarn@4.14.1",
"workspaces": [
"packages/*"
],
"scripts": {
"test": "vitest watch",
"build": "rm -rf ./dist && yarn workspaces foreach -Api run build && yarn setup:runtime",
"build:prod": "rm -rf ./dist && yarn workspaces foreach -Api run build:prod && yarn setup:runtime",
"setup:runtime": "mkdir -p dist/packages/node_modules && ln -sf ../sim-shared dist/packages/node_modules/sim-shared && ln -sf ../sf-consumidor-objenious dist/packages/node_modules/sim-consumidor-objenious",
"start": "yarn workspaces foreach -Apiv run start",
"typecheck": "npx tsc --noEmit",

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 { SimEvents } from "sim-shared/domain/SimEvents.js";
import { iccidValidator } from "./httpValidators.js";
import { error } from "node:console";
import { nosSimToCommonSim } from "#domain/transformers.js";
export class SimNosController {
@@ -127,7 +129,7 @@ export class SimNosController {
const validateBody = iccidValidator.validate(body);
if (validateBody.error != undefined) {
res.status(402).json(validateBody)
res.status(422).json(validateBody)
return;
}
@@ -142,12 +144,17 @@ export class SimNosController {
res.status(500).json(usecaseRes)
return;
} else {
res.send(usecaseRes.data)
const simComun = nosSimToCommonSim(usecaseRes.data)
res.status(200).send({ data: simComun })
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)
return;
} else {
res.status(200).send(usecaseRes.data)
res.status(200).send(usecaseRes)
return;
}
}

View File

@@ -29,8 +29,8 @@ export const env = {
RABBITMQ_RETRY_INTERVAL: process.env.RABBITMQ_INTERVAL,
RABBITMQ_VHOST: String(process.env.RABBITMQ_VHOST),
APP_PORT: Number(process.env.APP_PORT),
APP_HOST: String(process.env.APP_HOST),
APP_PORT: Number(process.env.NOS_PORT ?? 3001),
APP_HOST: String(process.env.NOS_HOST ?? "0.0.0.0"),
// ESPECIFICO NOS
NOS_BASE_URL: String(process.env.NOS_BASE_URL),

View File

@@ -103,7 +103,7 @@ export namespace NosApi {
export type NetworkState = {
currentStateId: number
currentState: string
currentState: "active" | "barred" | "terminated"
isTransferring: boolean
lastTransferred: number
isOnline: boolean
@@ -112,7 +112,7 @@ export namespace NosApi {
export type BillingState = {
currentStateId: number
currentState: string
currentState: "active" | "terminated"
}
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) => {
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 {
console.error("Error express ", e)
}

View File

@@ -39,7 +39,8 @@
},
"scripts": {
"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",
"start": "node ../../dist/packages/sim-consumidor-nos/index.js",
"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 { PaginationArgs, QueryPaginationArgs } from "sim-shared/domain/PaginationArgs.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
@@ -236,7 +238,7 @@ export class SimController {
const validationRes = paginationValidator.validate(paginationArgs)
if (validationRes.error != undefined) {
res.status(402).json(validationRes)
res.status(422).json(validationRes)
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:
* - Loguear motivos de la no validacion

View File

@@ -20,7 +20,7 @@ export class SimRouter {
["pause", this.simController.suspend()],
["cancel", this.simController.terminate()],
["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)
const operation: ObjeniousOperation = {
correlation_id: preActivateData.correlation_id,
operation: "preActivate",
operation: "preactivate",
iccids: String(preActivateData.identifier.identifiers),
status: "noMassID",
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.MAIN, "topic")
await channel.assertQueue(QUEUES.OBJ)
await channel.assertQueue(QUEUES.OBJDLX)
await channel.assertQueue(QUEUES.OBJ, { durable: true })
await channel.assertQueue(QUEUES.OBJDLX, { durable: true })
await channel.assertQueue(QUEUES.OBJDEL, {
durable: true,
arguments: {

View File

@@ -31,7 +31,6 @@ const DEFAULT_DATA_JWT = {
iss: env.OBJ_CLIENT_ID,
aud: "https://idp.docapost.io/auth/realms/GETWAY",
jti: Date.now().toString(),
}
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("/select", simController.queryLine())
assert.ok(port, "Puerto del servicio no definido")

View File

@@ -55,7 +55,8 @@
"scripts": {
"test": "node --import tsx --test ./**/*.test.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",
"type:test": "tsc --noEmit"
},

View File

View File

@@ -32,14 +32,23 @@ export class OrderController {
}
public getByQueueId() {
return this.controllerGenerator<{ correlation_id: string }, { correlation_id: string }>({
return this.controllerGenerator<{ uuid: string }, { correlation_id: string }>({
validator: uuidValidator,
mapBody: (e) => ({ correlation_id: e.uuid }),
useCase: this.orderUseCases.getByQueueId(),
onError: (data, error) => { console.error(error) },
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:
@@ -77,7 +86,7 @@ export class OrderController {
})
}
// 2. Transformacion del body
// 2. Transformacion del body O => P
let data: P = body;
try {
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 { 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 { BodyValidator } from "sim-shared/aplication/BodyValidator.js"
import { tryCatch } from "sim-shared/domain/Result.js"
import { mapCompanyService } from "#config/servicesProxy.js"
import axios, { AxiosError, isAxiosError } from "axios"
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() {
return async (req: Request, res: Response) => {
try {

View File

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

View File

@@ -32,10 +32,10 @@ const offerExists = <Validator<{ offer: string }>>{
validationFunc: (a: { offer: string }) => offers.has(a.offer),
}
const isUuidv7 = <Validator<{ correlation_id?: string }>>{
field: "correlation_id",
const isUuidv7 = <Validator<{ uuid?: string }>>{
field: "uuid",
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 }>>{
@@ -56,12 +56,27 @@ const validNumericId = <Validator<{ id?: number }>>{
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 }>(
[
iccidRequired,
iccidLongitudValidator,
iccidWithValidCompany,
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
])

View File

@@ -24,6 +24,8 @@ export const env = {
RABBITMQ_RETRY_INTERVAL: process.env.RABBITMQ_INTERVAL,
RABBITMQ_VHOST: String(process.env.RABBITMQ_VHOST),
CONNECTIONS_URL: String(process.env.CONNECTIONS_URL),
OBJENIOUS_CONSUMER_URL: process.env.OBJENIOUS_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]
])

View File

@@ -7,6 +7,7 @@ import { env } from "#config/env/index.js"
import { orderRoutes } from "#adapters/orderRoutes.http.js";
import { connectionsRoutes } from "#adapters/simconnectionsRoutes.js";
import { franceRoutes } from "#adapters/franceRoutes.http.js";
import { spainRoutes } from "#adapters/spainRoutes.http.js";
const PORT = env.API_PORT
const HOSTNAME = "0.0.0.0"
@@ -35,6 +36,11 @@ app.use("/docs", express.static(path.join(process.cwd(), '../../docs')))
// Rutas especificas para casos especiales como el tiempo de suspension de francia
app.use("/france", franceRoutes)
// Rutas especificas de España (Alai)
app.use("/spain", spainRoutes)
//TODO: app.use("/portugal", portugalRoutes)
app.get("/health", (req, res) => {
res.status(200).json({ status: "ok" })
})

View File

@@ -23,7 +23,7 @@ franceRoutes.use("", createProxyMiddleware({
const host = req.get('host');
const originalFullUrl = `${protocol}://${host}${req.originalUrl}`;
const destinationFullUrl = `${FRANCE_URL}${proxyReq.path}`;
//console.log(`[Proxy Req]: ${req.method} ${req.url} -> ${proxyReq.path}`);
console.log(`[Proxy FR]: ${req.method} ${req.url} -> ${proxyReq.path}`);
}
}
}

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