diff --git a/.gitignore b/.gitignore index f69b410..af2ec1d 100644 --- a/.gitignore +++ b/.gitignore @@ -16,8 +16,10 @@ node_modules #!.yarn/cache .pnp.* +# Certificados *.pem - +*.p12 +*.key dist/* diff --git a/README.md b/README.md index 6328041..c3fea4f 100644 --- a/README.md +++ b/README.md @@ -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) diff --git a/deployment/database/migrations/1.0.0_orders.sql b/deployment/database/migrations/1.0.0_orders.sql index 912710a..3278e9f 100644 --- a/deployment/database/migrations/1.0.0_orders.sql +++ b/deployment/database/migrations/1.0.0_orders.sql @@ -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 diff --git a/deployment/develop/jenkinsfile.groovy b/deployment/develop/jenkinsfile.groovy index 3ca865d..4acd4a5 100644 --- a/deployment/develop/jenkinsfile.groovy +++ b/deployment/develop/jenkinsfile.groovy @@ -44,19 +44,27 @@ pipeline { ), sshTransfer( 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/packages/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" + execCommand: "ln -sf $BASE_REMOTE_PATH/vault/savefamily/sf-sims/sim-consumidor-nos.env $APP_REMOTE_PATH/packages/sim-consumidor-nos/.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" + execCommand: "ln -sf $BASE_REMOTE_PATH/vault/savefamily/sf-sims/sim-consumidor-alai.env $APP_REMOTE_PATH/packages/sim-consumidor-alai/.env" ), sshTransfer( 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/wsaccess_alaisecure_com_cert_client_new.p12 $APP_REMOTE_PATH/packages/wsaccess_alaisecure_com_cert_client_new.p12 + ), + sshTransfer( + cleanRemote: false, + execCommand: "ln -sf $BASE_REMOTE_PATH/vault/savefamily/sf-sims/sim-objenious-cron.env $APP_REMOTE_PATH/packages/sim-objenious-cron/.env" + ), + sshTransfer( + cleanRemote: false, + execCommand: "ln -sf $BASE_REMOTE_PATH/vault/savefamily/sf-sims/obj.pem $APP_REMOTE_PATH/packages/sim-consumidor-objenious/obj.pem" ), sshTransfer( cleanRemote: false, diff --git a/docs/sim-alai/Change External ID.yml b/docs/sim-alai/Change External ID.yml new file mode 100644 index 0000000..65c6c3d --- /dev/null +++ b/docs/sim-alai/Change External ID.yml @@ -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 diff --git a/docs/sim-alai/IMEI of subscription.yml b/docs/sim-alai/IMEI of subscription.yml new file mode 100644 index 0000000..8d946f2 --- /dev/null +++ b/docs/sim-alai/IMEI of subscription.yml @@ -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 diff --git a/docs/sim-alai/Login.yml b/docs/sim-alai/Login.yml new file mode 100644 index 0000000..02e84c0 --- /dev/null +++ b/docs/sim-alai/Login.yml @@ -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 diff --git a/docs/sim-alai/New Order.yml b/docs/sim-alai/New Order.yml new file mode 100644 index 0000000..f38a0c5 --- /dev/null +++ b/docs/sim-alai/New Order.yml @@ -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 diff --git a/docs/sim-alai/SIM.yml b/docs/sim-alai/SIM.yml new file mode 100644 index 0000000..34377ee --- /dev/null +++ b/docs/sim-alai/SIM.yml @@ -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 diff --git a/docs/sim-alai/Subscription.yml b/docs/sim-alai/Subscription.yml new file mode 100644 index 0000000..5ba5f45 --- /dev/null +++ b/docs/sim-alai/Subscription.yml @@ -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 diff --git a/docs/sim-alai/certificates/alai_cert.p12 b/docs/sim-alai/certificates/alai_cert.p12 new file mode 100644 index 0000000..86217c6 Binary files /dev/null and b/docs/sim-alai/certificates/alai_cert.p12 differ diff --git a/docs/sim-alai/environments/local.yml b/docs/sim-alai/environments/local.yml new file mode 100644 index 0000000..db39cbc --- /dev/null +++ b/docs/sim-alai/environments/local.yml @@ -0,0 +1,7 @@ +name: local +color: "#2E8A54" +variables: + - name: baseurl + value: http://localhost:3002 + - secret: true + name: token diff --git a/docs/sim-alai/environments/prod.yml b/docs/sim-alai/environments/prod.yml new file mode 100644 index 0000000..7f83d83 --- /dev/null +++ b/docs/sim-alai/environments/prod.yml @@ -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 diff --git a/docs/sim-alai/opencollection.yml b/docs/sim-alai/opencollection.yml new file mode 100644 index 0000000..6d6bccb --- /dev/null +++ b/docs/sim-alai/opencollection.yml @@ -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 diff --git a/docs/sim-api/Activate.bru b/docs/sim-api/Activate.bru index 9b392e4..46a58b0 100644 --- a/docs/sim-api/Activate.bru +++ b/docs/sim-api/Activate.bru @@ -6,16 +6,80 @@ meta { post { url: {{baseurl}}/sim/activate - body: formUrlEncoded + body: json auth: inherit } +body:json { + { + "iccid": "1234" + } +} + 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 +} diff --git a/docs/sim-api/Activation Email Health.bru b/docs/sim-api/Activation Email Health.bru index eebbd63..280096d 100644 --- a/docs/sim-api/Activation Email Health.bru +++ b/docs/sim-api/Activation Email Health.bru @@ -1,7 +1,7 @@ meta { name: Activation Email Health type: http - seq: 8 + seq: 9 } post { diff --git a/docs/sim-api/Activation Email.bru b/docs/sim-api/Activation Email.bru index 8f72b9e..7db3f33 100644 --- a/docs/sim-api/Activation Email.bru +++ b/docs/sim-api/Activation Email.bru @@ -1,7 +1,7 @@ meta { name: Activation Email type: http - seq: 6 + seq: 8 } post { diff --git a/docs/sim-api/Alai/Preactivate.bru b/docs/sim-api/Alai/Preactivate.bru new file mode 100644 index 0000000..fa0e454 --- /dev/null +++ b/docs/sim-api/Alai/Preactivate.bru @@ -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 +} diff --git a/docs/sim-api/Alai/Select SIM.bru b/docs/sim-api/Alai/Select SIM.bru new file mode 100644 index 0000000..7fe0f84 --- /dev/null +++ b/docs/sim-api/Alai/Select SIM.bru @@ -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 +} diff --git a/docs/sim-api/Alai/folder.bru b/docs/sim-api/Alai/folder.bru new file mode 100644 index 0000000..aef4508 --- /dev/null +++ b/docs/sim-api/Alai/folder.bru @@ -0,0 +1,8 @@ +meta { + name: Alai + seq: 14 +} + +auth { + mode: inherit +} diff --git a/docs/sim-api/Cancel.bru b/docs/sim-api/Cancel.bru index fbb24c4..7122531 100644 --- a/docs/sim-api/Cancel.bru +++ b/docs/sim-api/Cancel.bru @@ -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 { diff --git a/docs/sim-api/Docs.bru b/docs/sim-api/Docs.bru index 613bf7b..124dd07 100644 --- a/docs/sim-api/Docs.bru +++ b/docs/sim-api/Docs.bru @@ -1,7 +1,7 @@ meta { name: Docs type: http - seq: 12 + seq: 11 } get { diff --git a/docs/sim-api/Health.bru b/docs/sim-api/Health.bru index 5c7748b..2d5774c 100644 --- a/docs/sim-api/Health.bru +++ b/docs/sim-api/Health.bru @@ -1,7 +1,7 @@ meta { name: Health type: http - seq: 5 + seq: 7 } get { diff --git a/docs/sim-api/Nos/Select.bru b/docs/sim-api/Nos/Select.bru new file mode 100644 index 0000000..e9a2699 --- /dev/null +++ b/docs/sim-api/Nos/Select.bru @@ -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 +} diff --git a/docs/sim-api/Nos/folder.bru b/docs/sim-api/Nos/folder.bru new file mode 100644 index 0000000..fadeb09 --- /dev/null +++ b/docs/sim-api/Nos/folder.bru @@ -0,0 +1,8 @@ +meta { + name: Nos + seq: 15 +} + +auth { + mode: inherit +} diff --git a/docs/sim-api/France Suspended Lines.bru b/docs/sim-api/Objenious/France Suspended Lines.bru similarity index 97% rename from docs/sim-api/France Suspended Lines.bru rename to docs/sim-api/Objenious/France Suspended Lines.bru index fe38d87..029c7f9 100644 --- a/docs/sim-api/France Suspended Lines.bru +++ b/docs/sim-api/Objenious/France Suspended Lines.bru @@ -1,7 +1,7 @@ meta { name: France Suspended Lines type: http - seq: 17 + seq: 16 } get { diff --git a/docs/sim-api/France Suspended Time.bru b/docs/sim-api/Objenious/France Suspended Time.bru similarity index 100% rename from docs/sim-api/France Suspended Time.bru rename to docs/sim-api/Objenious/France Suspended Time.bru diff --git a/docs/sim-api/Objenious/folder.bru b/docs/sim-api/Objenious/folder.bru new file mode 100644 index 0000000..91b431a --- /dev/null +++ b/docs/sim-api/Objenious/folder.bru @@ -0,0 +1,8 @@ +meta { + name: Objenious + seq: 16 +} + +auth { + mode: inherit +} diff --git a/docs/sim-api/Get pending orders.bru b/docs/sim-api/Orders/Get pending orders.bru similarity index 94% rename from docs/sim-api/Get pending orders.bru rename to docs/sim-api/Orders/Get pending orders.bru index 70b9eed..3888958 100644 --- a/docs/sim-api/Get pending orders.bru +++ b/docs/sim-api/Orders/Get pending orders.bru @@ -1,7 +1,7 @@ meta { name: Get pending orders type: http - seq: 11 + seq: 10 } get { diff --git a/docs/sim-api/Order by id.bru b/docs/sim-api/Orders/Order by id.bru similarity index 68% rename from docs/sim-api/Order by id.bru rename to docs/sim-api/Orders/Order by id.bru index 80104c0..6d3d66a 100644 --- a/docs/sim-api/Order by id.bru +++ b/docs/sim-api/Orders/Order by id.bru @@ -5,7 +5,7 @@ meta { } get { - url: {{baseurl}}/orders/ + url: {{baseurl}}/orders/019dbeaf-8abb-7783-8b51-94fbd9f0b0df body: none auth: inherit } diff --git a/docs/sim-api/Orders by message_id.bru b/docs/sim-api/Orders/Orders by message_id.bru similarity index 50% rename from docs/sim-api/Orders by message_id.bru rename to docs/sim-api/Orders/Orders by message_id.bru index a37a6a6..787fd86 100644 --- a/docs/sim-api/Orders by message_id.bru +++ b/docs/sim-api/Orders/Orders by message_id.bru @@ -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 diff --git a/docs/sim-api/Orders/folder.bru b/docs/sim-api/Orders/folder.bru new file mode 100644 index 0000000..7fd4620 --- /dev/null +++ b/docs/sim-api/Orders/folder.bru @@ -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 + + + +} diff --git a/docs/sim-api/Pause.bru b/docs/sim-api/Pause.bru index 68c7375..2bb5401 100644 --- a/docs/sim-api/Pause.bru +++ b/docs/sim-api/Pause.bru @@ -1,7 +1,7 @@ meta { name: Pause type: http - seq: 1 + seq: 5 } post { diff --git a/docs/sim-api/Preactivate.bru b/docs/sim-api/Preactivate.bru index c9053de..1bc990d 100644 --- a/docs/sim-api/Preactivate.bru +++ b/docs/sim-api/Preactivate.bru @@ -1,7 +1,7 @@ meta { name: Preactivate type: http - seq: 1 + seq: 6 } post { diff --git a/docs/sim-api/ReActivate.bru b/docs/sim-api/ReActivate.bru index 6477b85..7093634 100644 --- a/docs/sim-api/ReActivate.bru +++ b/docs/sim-api/ReActivate.bru @@ -1,7 +1,7 @@ meta { name: ReActivate type: http - seq: 13 + seq: 12 } post { diff --git a/docs/sim-api/Select.bru b/docs/sim-api/Select.bru new file mode 100644 index 0000000..27796fa --- /dev/null +++ b/docs/sim-api/Select.bru @@ -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 +} diff --git a/docs/sim-api/Test Order.bru b/docs/sim-api/Test Order.bru index 27b0879..6995c17 100644 --- a/docs/sim-api/Test Order.bru +++ b/docs/sim-api/Test Order.bru @@ -1,7 +1,7 @@ meta { name: Test Order type: http - seq: 9 + seq: 10 } post { diff --git a/docs/sim-api/collection.bru b/docs/sim-api/collection.bru index 5fe677a..1ec9cfd 100644 --- a/docs/sim-api/collection.bru +++ b/docs/sim-api/collection.bru @@ -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, diff --git a/docs/sim-api/environments/local.bru b/docs/sim-api/environments/local.bru index fab4907..04056b4 100644 --- a/docs/sim-api/environments/local.bru +++ b/docs/sim-api/environments/local.bru @@ -1,4 +1,5 @@ vars { baseurl: http://localhost:3000 + baseAlai: http://localhost:3002 } color: #2E8A54 diff --git a/docs/sim-api/test proxy.bru b/docs/sim-api/test proxy.bru index d4e17b0..2e66502 100644 --- a/docs/sim-api/test proxy.bru +++ b/docs/sim-api/test proxy.bru @@ -1,7 +1,7 @@ meta { name: test proxy type: http - seq: 14 + seq: 13 } get { diff --git a/docs/sim-objenious/Alarms/Alarm by id.bru b/docs/sim-objenious/Alarms/Alarm by id.bru new file mode 100644 index 0000000..8c07ffc --- /dev/null +++ b/docs/sim-objenious/Alarms/Alarm by id.bru @@ -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 +} diff --git a/docs/sim-objenious/Alarms/Alerts.bru b/docs/sim-objenious/Alarms/Alerts.bru new file mode 100644 index 0000000..17d4873 --- /dev/null +++ b/docs/sim-objenious/Alarms/Alerts.bru @@ -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 +} diff --git a/docs/sim-objenious/Alarms/All Alarms.bru b/docs/sim-objenious/Alarms/All Alarms.bru new file mode 100644 index 0000000..d16789b --- /dev/null +++ b/docs/sim-objenious/Alarms/All Alarms.bru @@ -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 +} diff --git a/docs/sim-objenious/Alarms/folder.bru b/docs/sim-objenious/Alarms/folder.bru new file mode 100644 index 0000000..3acaf58 --- /dev/null +++ b/docs/sim-objenious/Alarms/folder.bru @@ -0,0 +1,8 @@ +meta { + name: Alarms + seq: 21 +} + +auth { + mode: inherit +} diff --git a/docs/sim-objenious/Appel lines.bru b/docs/sim-objenious/Appel lines.bru index afc428d..e2f6a3c 100644 --- a/docs/sim-objenious/Appel lines.bru +++ b/docs/sim-objenious/Appel lines.bru @@ -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 { diff --git a/docs/sim-objenious/Consumption details.bru b/docs/sim-objenious/Consumption details.bru new file mode 100644 index 0000000..95413e4 --- /dev/null +++ b/docs/sim-objenious/Consumption details.bru @@ -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 +} diff --git a/docs/sim-objenious/Line by iccid.bru b/docs/sim-objenious/Line by iccid.bru new file mode 100644 index 0000000..9639c6f --- /dev/null +++ b/docs/sim-objenious/Line by iccid.bru @@ -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 +} diff --git a/packages/sim-consumidor-alai/aplication/AlaiTokenManager.ts b/packages/sim-consumidor-alai/aplication/AlaiTokenManager.ts new file mode 100644 index 0000000..5a7b69d --- /dev/null +++ b/packages/sim-consumidor-alai/aplication/AlaiTokenManager.ts @@ -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> { + // En Alai no existe el concepto de refresh, se solicita otro token nuevo + return this.getAccessToken() + }; + + public async getAccessToken(): Promise> { + // 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 + }; +} diff --git a/packages/sim-consumidor-alai/aplication/DebugTokenManager.ts b/packages/sim-consumidor-alai/aplication/DebugTokenManager.ts new file mode 100644 index 0000000..4d94ff1 --- /dev/null +++ b/packages/sim-consumidor-alai/aplication/DebugTokenManager.ts @@ -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> { + // En Alai no existe el concepto de refresh, se solicita otro token nuevo + return this.getAccessToken() + }; + + public async getAccessToken(): Promise> { + // 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 + }; +} diff --git a/packages/sim-consumidor-alai/aplication/SimAlai.controller.ts b/packages/sim-consumidor-alai/aplication/SimAlai.controller.ts new file mode 100644 index 0000000..0177b4f --- /dev/null +++ b/packages/sim-consumidor-alai/aplication/SimAlai.controller.ts @@ -0,0 +1,195 @@ +import { ConsumeMessage } from "amqplib"; +import { Request, Response } from "express" +import { SimAlaiUsecases } from "./SimAlai.usecases.js"; +import { EventBus } from "sim-shared/domain/EventBus.port.js"; +import { Result } from "sim-shared/domain/Result.js"; +import { SimEvents } from "sim-shared/domain/SimEvents.js"; +import { iccidValidator } from "./httpValidators.js"; +import { alaiSimToCommonSim } from "#domain/transformers.js"; + +export class SimAlaiController { + + constructor( + private uscases: SimAlaiUsecases, + private eventBus: EventBus, + ) { + } + + private validateMsg(msg: ConsumeMessage | null) { + if (msg == undefined) return false; + const msgData = this.decodeMsg(msg) as SimEvents.general + if (msgData == undefined || msgData.payload == undefined) throw new Error("Mensaje invalido") + return msgData; + } + + private decodeMsg(msg: ConsumeMessage): object | undefined { + if (msg.content == undefined) { + console.warn('[Sim.controller] Mensaje vacío'); + return undefined; + } + + try { + // Convertir el Buffer a String (UTF-8) + const contentJson = JSON.parse(Buffer.from(msg.content).toString('utf8')) + return contentJson; + + } catch (error) { + console.error('Error al decodificar JSON:', error); + console.error(Buffer.from(msg.content).toString(("utf8"))) + // Aquí podrías decidir devolver el string crudo o null + return undefined; + } + } + + /** + * Metodo duplicado se puede generalizar la a una clase sharedController con las funciones basicas + * TODO: meter un check de 429 + */ + private async tryUseCase + (msg: ConsumeMessage, usecase: () => Promise>): Promise> { + try { + const result = await usecase() + if (result.error == undefined) { + await this.eventBus.ack(msg) + return result + } else { + console.error("Error procesando el caso de uso (Alai)", result.error) + this.eventBus.nack(msg) + return result + } + } catch (e) { + console.error("Error general procesando el caso de uso (Alai)") + this.eventBus.nack(msg) + return { + error: String(e) + } + } + } + + public activate() { + return async (msg: ConsumeMessage) => { + console.log("[i] Evento activate ", msg.fields) + const data = this.validateMsg(msg) as SimEvents.activation + const iccid = data.payload.iccid + const correlation_id = data.headers?.message_id + const res = await this.tryUseCase(msg, this.uscases.activate({ + iccid: iccid, + correlation_id: correlation_id + })) + return res; + } + } + + public suspend() { + return async (msg: ConsumeMessage) => { + console.log("Evento suspend ", msg.fields) + const data = this.validateMsg(msg) as SimEvents.suspend + const iccid = data.payload.iccid + const correlation_id = data.headers?.message_id + const res = await this.tryUseCase(msg, this.uscases.suspend({ + iccid: iccid, + correlation_id: correlation_id + })) + return res; + } + } + + + + public reActivate() { + return async (msg: ConsumeMessage) => { + console.log("Evento reActivate ", msg.fields) + const data = this.validateMsg(msg) as SimEvents.reActivation + const iccid = data.payload.iccid + const correlation_id = data.headers?.message_id + const res = await this.tryUseCase(msg, this.uscases.reactivate({ + iccid: iccid, + correlation_id: correlation_id + })) + + return res; + } + } + + + public terminate() { + return async (msg: ConsumeMessage) => { + console.log("Evento reActivate ", msg.fields) + const data = this.validateMsg(msg) as SimEvents.reActivation + const iccid = data.payload.iccid + const correlation_id = data.headers?.message_id + const res = await this.tryUseCase(msg, this.uscases.terminate({ + iccid: iccid, + correlation_id: correlation_id + })) + + return res; + } + } + + + /** + * Select especificamente por REST para evitar pasar por las colas. + * La respuesta es instantanea no se tiene que registrar como operación. + */ + public selectREST() { + return async (req: Request, res: Response) => { + const { query } = req + const body = { iccid: query.iccid as string } + console.log("Evento select", body) + const validateBody = iccidValidator.validate(body); + + if (validateBody.error != undefined) { + res.status(422).json(validateBody) + return; + } + + const iccid: string | string[] = body.iccid + + if (Array.isArray(iccid)) { + // TODO: Automatizar la paginacion + //const usecaseRes = this.uscases.selectMany({ iccid }) + } else { + //const usecaseRes = await this.uscases.selectOne(iccid) + const usecaseRes = await this.uscases.selectCompleteSim(iccid) + if (usecaseRes.error != undefined) { + res.status(500).json(usecaseRes) + return; + } else { + const { sim, subscription, imei } = usecaseRes.data + const simData = alaiSimToCommonSim(sim, subscription, imei) + res.send(simData) + return; + } + } + + res.status(200).json(validateBody) + } + } + + /** + public selectPageREST() { + return async (req: Request, res: Response) => { + const { offset, limit, filter, orderBy } = req.query + const params = { + offset: (offset != undefined) ? Number(offset) : undefined, + limit: (limit != undefined) ? Number(limit) : undefined, + filter: (filter != undefined) ? String(filter) : undefined, + orderBy: (orderBy != undefined) ? String(orderBy) : undefined + } + + const usecaseRes = await this.uscases.selectPage(params) + + if (usecaseRes.error != undefined) { + res.status(500).json(usecaseRes) + return; + } else { + res.status(200).send(usecaseRes.data) + return; + } + } + } + **/ +} + + diff --git a/packages/sim-consumidor-alai/aplication/SimAlai.router.ts b/packages/sim-consumidor-alai/aplication/SimAlai.router.ts new file mode 100644 index 0000000..d09a1b9 --- /dev/null +++ b/packages/sim-consumidor-alai/aplication/SimAlai.router.ts @@ -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>) + +export class SimAlaiRouter { + private readonly routes: Map; + + // WIP + constructor( + private readonly simController: SimAlaiController, + private readonly eventBus: EventBus + ) { + this.routes = new Map([ + ["activate", this.simController.activate()], + ["pause", this.simController.suspend()], + ["reactivate", this.simController.reActivate()], + ["cancel", this.simController.terminate()], + ["preActivate", this.simController.activate()] + ]); + } + + /** + * Enruta el mensaje a la acción correspondiente basándose en la routing key + * TODO: No estoy seguro que deba meter el nack aqui + * - De moemento el ack-nack se gestiona en los controller, por si acaso hay casos + * limite en + */ + public route = async (msg: ConsumeMessage | null): Promise => { + 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]; + } +} diff --git a/packages/sim-consumidor-alai/aplication/SimAlai.usecases.ts b/packages/sim-consumidor-alai/aplication/SimAlai.usecases.ts new file mode 100644 index 0000000..20fcb6a --- /dev/null +++ b/packages/sim-consumidor-alai/aplication/SimAlai.usecases.ts @@ -0,0 +1,262 @@ +/** + * Documentación de referencia: + * https://pelion-help.iot-x.com/nos/en-US/Content/API/APIReference/API%20Reference.htm?tocpath=_____7 + * + * En nos el correlation_id ya va a ser obligatorio en todos los mensajes + * + * TODO: + * - Control de errores más preciso + * + */ +import { AlaiAPI } from "#domain/AlaiAPI.js"; +import { AlaiRepository } from "#infrastructure/AlaiRepository.js"; +import { ConsumeMessage } from "amqplib"; +import { ErrorOrderDTO, FinishOrderDTO, UpdateOrderDTO } from "sim-shared/domain/Order.js"; +import { Result } from "sim-shared/domain/Result.js"; +import { HttpClient } from "sim-shared/infrastructure/HTTPClient.js"; +import { OrderRepository } from "sim-shared/infrastructure/OrderRepository.js"; +import { isOmittedExpression } from "typescript"; + +export class SimAlaiUsecases { + constructor( + private httpClient: HttpClient, + private alaiRepository: AlaiRepository, + private orderRepository: OrderRepository + ) { + } + + private async setRunning(correlation_id: string) { + const updateData: UpdateOrderDTO = { + new_status: "running", + correlation_id: correlation_id + } + const order = await this.orderRepository.updateOrder(updateData) + return order + } + + private async setFinished(correlation_id: string) { + // En NOS el updateOrder se hace con el correlation_id que viene en la cabecera del + // mensaje consumido + const updateData: FinishOrderDTO = { + correlation_id: correlation_id + } + const order = await this.orderRepository.finishOrder(updateData) + return order + } + + private async setFailed(correlation_id: string, reason: string, detail?: string) { + // En NOS el updateOrder se hace con el correlation_id que viene en la cabecera del + // mensaje consumido + const updateData: ErrorOrderDTO = { + status: "failed", + correlation_id: correlation_id, + reason: reason, + error: reason, + stackTrace: detail + } + + console.log("SET FAILED DATA:", updateData) + const order = await this.orderRepository.errorOrder(updateData) + console.log("SET FAILED RES:", order) + return order + } + + /** + * Gestiona el ciclo de vida de una petición. No aplica + * a peticiones de lectura (no pasan por la cola y no generan un order) + */ + public usecaseTemplate( + func: (_: T) => Promise>, + args: T, + correlation_id?: string | undefined + ) { + return async (): Promise> => { + // Operacion pending -> running + if (correlation_id != undefined) + this.setRunning(correlation_id) + .then() + .catch(e => console.error("Error actualizando el order", e)) + else + console.warn("[!] Se ha lanzado una caso de uso sin correlation_id") + + try { + const res = await func(args) + + if (res.error != undefined) { + console.log("Error peticion: ", res, correlation_id) + if (correlation_id != undefined) + this.setFailed(correlation_id, res.error) + .then(e => console.log("failed", e)) + .catch(e => console.error(e)) + return res; + } else { + if (correlation_id != undefined) + this.setFinished(correlation_id).then() + return res; + } + + } catch (e) { + if (correlation_id != undefined) + this.setFailed(correlation_id, "Error general de operacion de SIM (NOS) ", String(e)).then() + return { + error: "Error general de operacion de SIM (NOS) " + String(e) + } + } + + } + } + + public activate(args: { + iccid: string, + correlation_id: string | undefined + }) { + return this.usecaseTemplate(async (iccid /*iccid*/) => { + const order = await this.alaiRepository.createOrder() + if (order.error != undefined) { + // Falla el crearse un order (problema de servidor, token, etc) + console.error(order.error) + return order + } + const reserved = await this.alaiRepository.createReserve(order.data.id, iccid) + return reserved + }, args.iccid, args.correlation_id) + } + + public preactivate(args: { + iccid: string, + correlation_id: string | undefined, + externalId: string | undefined // Por compatibilidad + }) { + const inputargs = { + iccid: args.iccid, + externalId: args.externalId + } + return this.usecaseTemplate(async (args) => { + const order = await this.alaiRepository.createOrder() + if (order.error != undefined) { + return order + } + const orderId = order.data.id + const reserve = await this.alaiRepository.createReserve(orderId, args.iccid) + if (reserve.error != undefined) { + return reserve + } + + const applyOrder = await this.alaiRepository.applyOrder(orderId) + if (applyOrder.error != undefined) { + return applyOrder + } + + const preactivatedSim = await this.alaiRepository.getSimByICCID(args.iccid) + if (preactivatedSim.error != undefined) { + return preactivatedSim + } + + // TODO: Controlar sim no encotrada (No deberia pasar) + const subscriptionId = preactivatedSim.data!.subscription.id + if (args.externalId) { + const externalIdAdded = await this.alaiRepository.changeExternalId(subscriptionId, args.externalId) + if (externalIdAdded.error != undefined) { + return externalIdAdded + } + } + + // En connections acaba buscando el numero. + const subscription = await this.alaiRepository.getSubscriptionById(subscriptionId) + return subscription + }, inputargs, args.correlation_id) + } + + public suspend(args: { + iccid: string, + correlation_id: string | undefined + }) { + return this.usecaseTemplate(async (args) => { + const subscription = await this.alaiRepository.getSimByICCID(args.iccid) + if (subscription.error != undefined) { + return subscription + } + + // TODO: Controlar que no se encuentre la subscription + const suspension = this.alaiRepository.pauseSubscription(subscription.data!.id) + return suspension + }, args, args.correlation_id) + } + + + public reactivate(args: { + iccid: string, + correlation_id: string | undefined + }) { + return this.usecaseTemplate(async (args) => { + const subscription = await this.alaiRepository.getSimByICCID(args.iccid) + if (subscription.error != undefined) { + return subscription + } + + // TODO: Controlar que no se encuentre la subscription + const suspension = this.alaiRepository.unPauseSubscription(subscription.data!.id) + return suspension + }, args, args.correlation_id) + } + + + public terminate(args: { + iccid: string, + correlation_id: string | undefined + }) { + return this.usecaseTemplate(async (args) => { + const subscription = await this.alaiRepository.getSimByICCID(args.iccid) + if (subscription.error != undefined) { + return subscription + } + + // TODO: Controlar que no se encuentre la subscription + const suspension = this.alaiRepository.terminateSubscription(subscription.data!.id) + return suspension + }, args, args.correlation_id) + } + + public async selectOne(iccid: string) { + const sim = await this.alaiRepository.getSimByICCID(iccid) + return sim + } + + /** + * Para sacar los datos de una liena hay que sacar sim -> subscripcion -> imei + * son 3 llamadas distintas. + */ + public async selectCompleteSim(iccid: string): Promise> { + const sim = await this.alaiRepository.getSimByICCID(iccid) + + if (sim.error != undefined) { + return sim + } + + const subscriptionId = sim.data!.subscription.id + const subscription = await this.alaiRepository.getSubscriptionById(subscriptionId) + + if (subscription.error != undefined) { + return subscription + } + + const imei = await this.alaiRepository.getImeiFromSubscription(subscriptionId) + if (imei.error != undefined) { + return imei + } + + return { + data: { + sim: sim.data!, + subscription: subscription.data!, + imei: imei.data! + } + } + + + } +} diff --git a/packages/sim-consumidor-alai/aplication/SslService.ts b/packages/sim-consumidor-alai/aplication/SslService.ts new file mode 100644 index 0000000..0cda0b5 --- /dev/null +++ b/packages/sim-consumidor-alai/aplication/SslService.ts @@ -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 { + 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 { + 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) + } + } + } + +} + diff --git a/packages/sim-consumidor-alai/aplication/httpValidators.ts b/packages/sim-consumidor-alai/aplication/httpValidators.ts new file mode 100644 index 0000000..b452272 --- /dev/null +++ b/packages/sim-consumidor-alai/aplication/httpValidators.ts @@ -0,0 +1,39 @@ +import { BodyValidator, Validator } from "sim-shared/aplication/BodyValidator.js"; + +const iccidNotNull = >{ + field: "iccid", + errorMsg: "El iccid no está definido", + validationFunc: (a: { iccid: unknown }) => { + return (a.iccid != null && a.iccid != undefined) + } +} + +const iccidValueOrArray = >{ + 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 = >{ + 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, + ] +) + diff --git a/packages/sim-consumidor-alai/certificates/.debugToken b/packages/sim-consumidor-alai/certificates/.debugToken new file mode 100644 index 0000000..ce3d0ee --- /dev/null +++ b/packages/sim-consumidor-alai/certificates/.debugToken @@ -0,0 +1 @@ +eyJhbGciOiJIUzM4NCJ9.eyJiciI6InNhdmVmYW1pbHkiLCJpcCI6Ijg4LjE1LjE1Ny4xNjciLCJzdWIiOiJwYWxvbWFpYmFuZXoiLCJzIjoiRVdTMTY0NmFmNjNlZGUyMjgzIiwicG9zIjoic2F2ZWZhbWlseUNhYyIsImlkV3NVc2VyIjoiODYiLCJpc012bmEiOmZhbHNlLCJkb21haW4iOiJBbGFpfHNhdmVmYW1pbHkiLCJpYXQiOjE3Nzc4OTk3MzcsImV4cCI6MTc3NzkxMDUzN30.PvTTRhUpKlslGOerQsLY4RLBXdQ5FIVvUKb_1ZK4b2Zggt04KZhwX0d-XoLAcP93 diff --git a/packages/sim-consumidor-alai/certificates/wsaccess_alaisecure_com_cert_client_new.p12 b/packages/sim-consumidor-alai/certificates/wsaccess_alaisecure_com_cert_client_new.p12 new file mode 100644 index 0000000..86217c6 Binary files /dev/null and b/packages/sim-consumidor-alai/certificates/wsaccess_alaisecure_com_cert_client_new.p12 differ diff --git a/packages/sim-consumidor-alai/config/env/env.ts b/packages/sim-consumidor-alai/config/env/env.ts new file mode 100644 index 0000000..ef9ed13 --- /dev/null +++ b/packages/sim-consumidor-alai/config/env/env.ts @@ -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") diff --git a/packages/sim-consumidor-alai/config/eventBus.config.ts b/packages/sim-consumidor-alai/config/eventBus.config.ts new file mode 100644 index 0000000..c794b99 --- /dev/null +++ b/packages/sim-consumidor-alai/config/eventBus.config.ts @@ -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 = { + 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 +} diff --git a/packages/sim-consumidor-alai/config/httpClient.config.ts b/packages/sim-consumidor-alai/config/httpClient.config.ts new file mode 100644 index 0000000..e0182b1 --- /dev/null +++ b/packages/sim-consumidor-alai/config/httpClient.config.ts @@ -0,0 +1,17 @@ +import { HttpClient } from "sim-shared/infrastructure/HTTPClient.js" +import { AlaiTokenManager } from "#aplication/AlaiTokenManager.js" +import { env } from "#config/env/env.js"; +import { httpsAgent } from "./httpsAgent.js" +import { DebugTokenManager } from "#aplication/DebugTokenManager.js"; + +const tokenManager = new AlaiTokenManager() +//const debugTokenManagr = new DebugTokenManager() +//console.error("USANDO DebugTokenManager! Eliminar en prod") + + +export const alaiHttp = new HttpClient({ + baseURL: env.ALAI_API_URL as string, + headers: {}, + jwtManager: tokenManager, + httpsAgent: httpsAgent +}) diff --git a/packages/sim-consumidor-alai/config/httpsAgent.ts b/packages/sim-consumidor-alai/config/httpsAgent.ts new file mode 100644 index 0000000..be41583 --- /dev/null +++ b/packages/sim-consumidor-alai/config/httpsAgent.ts @@ -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 +}); + diff --git a/packages/sim-consumidor-alai/config/postgreConfig.ts b/packages/sim-consumidor-alai/config/postgreConfig.ts new file mode 100644 index 0000000..9c67604 --- /dev/null +++ b/packages/sim-consumidor-alai/config/postgreConfig.ts @@ -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 +}) diff --git a/packages/sim-consumidor-alai/domain/AlaiAPI.ts b/packages/sim-consumidor-alai/domain/AlaiAPI.ts new file mode 100644 index 0000000..70a0994 --- /dev/null +++ b/packages/sim-consumidor-alai/domain/AlaiAPI.ts @@ -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 + } + } +} + diff --git a/packages/sim-consumidor-alai/domain/transformers.ts b/packages/sim-consumidor-alai/domain/transformers.ts new file mode 100644 index 0000000..45e09d7 --- /dev/null +++ b/packages/sim-consumidor-alai/domain/transformers.ts @@ -0,0 +1,47 @@ +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["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["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> { + + const status = alaiStates.get(alaiSubscription.status) ?? "UNKNOWN" + const networkStatus = alaiNetworkStates.get(alaiSubscription.networkStatus) ?? "UNKNOWN" + + const commonSim: CommonSim = { + company: "ALAI", + tariff: alaiSubscription.name, + iccid: alaiSim.id, + msisdn: alaiSubscription.lastMsisdnID, + billing_status: status, + network_status: networkStatus, + raw: alaiSubscription, + imei: imeiSubscription.content[0].id ?? "0", + preactivation_date: new Date(alaiSubscription.creationDate), + activation_date: new Date(alaiSubscription.firstActivationDate) + } + + return { + data: commonSim + } +} diff --git a/packages/sim-consumidor-alai/index.ts b/packages/sim-consumidor-alai/index.ts new file mode 100644 index 0000000..11890d7 --- /dev/null +++ b/packages/sim-consumidor-alai/index.ts @@ -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 {} diff --git a/packages/sim-shared/domain/SimOrder.ts b/packages/sim-consumidor-alai/infrastructure/AlaiJwtService.ts similarity index 100% rename from packages/sim-shared/domain/SimOrder.ts rename to packages/sim-consumidor-alai/infrastructure/AlaiJwtService.ts diff --git a/packages/sim-consumidor-alai/infrastructure/AlaiRepository.test.ts b/packages/sim-consumidor-alai/infrastructure/AlaiRepository.test.ts new file mode 100644 index 0000000..e69de29 diff --git a/packages/sim-consumidor-alai/infrastructure/AlaiRepository.ts b/packages/sim-consumidor-alai/infrastructure/AlaiRepository.ts new file mode 100644 index 0000000..890b6b6 --- /dev/null +++ b/packages/sim-consumidor-alai/infrastructure/AlaiRepository.ts @@ -0,0 +1,190 @@ +import { AlaiAPI } from "#domain/AlaiAPI.js"; +import axios, { AxiosError, AxiosResponse } from "axios"; +import { Result } from "sim-shared/domain/Result.js"; +import { env } from "#config/env/env.js"; +import { HttpClient } from "sim-shared/infrastructure/HTTPClient.js"; +import https from "https" + +export class AlaiRepository { + constructor( + private httpClient: HttpClient + ) { + } + + private async manageRequest(promiseReq: Promise>): Promise> { + try { + const res = await promiseReq + return { + data: res.data + } + } catch (e) { + if (axios.isAxiosError(e)) { + const error = e as AxiosError + return { + error: error.code + " : " + String(error.response?.statusText) + } + } else { + return { + error: String(e) + } + } + } + } + + public static async login(httpsAgent: https.Agent): Promise> { + 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(fullUrl, data, { httpsAgent }) + return { + data: loginRes.data + } + } catch (e) { + if (axios.isAxiosError(e)) { + const error = e as AxiosError + return { + error: error.code + " : " + String(error.response?.statusText) + } + } else { + return { + error: String(e) + } + } + } + } + + /** + * Los orders son la unidad que envuelve las peticiones para garantizar ideponencia. + */ + public async createOrder() { + // POST + const endpoint = "/v1/order" + const data: AlaiAPI.CreateOrderDTO = { + type: "RETAIL", + salesChannel: "OWN_CALLCENTER", + status: "CONFIRMED", + packages: [{ id: env.ALAI_PACKAGE as string }], + subscriber: { id: env.ALAI_SUBSCRIBER_ID as string } + } + const promReq = this.httpClient.post(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(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(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> { + const endpoint = `/v1/sim/${iccid}/order/${orderId}` + // Crear la reserva no usa datos en el body + const promReq = this.httpClient.post(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(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(endpoint, data, { params: params }) + const res = await this.manageRequest(promReq) + return res + } + + public async unPauseSubscription(subscriptionId: string) { + const endpoint = `/v1/subscription/${subscriptionId}` + // En teoria ahora se usa ["action", "UNBLOCK"] pero no he probado + const params = new URLSearchParams([ + ["action", "CHANGE_STATUS"] + ]) + const data = { + status: "ACTIVE" + } + const promReq = this.httpClient.patch(endpoint, data, { params: params }) + const res = await this.manageRequest(promReq) + return res + } + + public async terminateSubscription(subscriptionId: string) { + const endpoint = `/v1/subscription/${subscriptionId}` + // Esta llamada si es de acuerdo a la docu + const params = new URLSearchParams([ + ["action", "TERMINATE"] + ]) + const promReq = this.httpClient.patch(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(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(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(endpoint) + const res = await this.manageRequest(promReq) + return res + } +} diff --git a/packages/sim-consumidor-alai/infrastructure/LegacyJWTTokensRepository.ts b/packages/sim-consumidor-alai/infrastructure/LegacyJWTTokensRepository.ts new file mode 100644 index 0000000..41bb140 --- /dev/null +++ b/packages/sim-consumidor-alai/infrastructure/LegacyJWTTokensRepository.ts @@ -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> { + const query = "SELECT * FROM alai_api_credentials;" + + try { + const res = await this.pgClient.query(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 { + 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) + } + } + } + +} diff --git a/packages/sim-consumidor-alai/package.json b/packages/sim-consumidor-alai/package.json new file mode 100644 index 0000000..02f89a7 --- /dev/null +++ b/packages/sim-consumidor-alai/package.json @@ -0,0 +1,71 @@ +{ + "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 package.json ../../dist/packages/sim-consumidor-nos/ && cp -r certificates/ ../../dist/packages/sim-consumidor-alai/", + "esbuild": "esbuild index.ts --platform=node", + "start": "node ../../dist/packages/sim-consumidor-nos/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": "*" + } +} diff --git a/packages/sim-consumidor-alai/readme.md b/packages/sim-consumidor-alai/readme.md new file mode 100644 index 0000000..750d04f --- /dev/null +++ b/packages/sim-consumidor-alai/readme.md @@ -0,0 +1,5 @@ +# Alai + +## Particularidades de las operaciones de Alai + +TODO: Copiar de obsidian diff --git a/packages/sim-consumidor-alai/tsconfig.json b/packages/sim-consumidor-alai/tsconfig.json new file mode 100644 index 0000000..119ff00 --- /dev/null +++ b/packages/sim-consumidor-alai/tsconfig.json @@ -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" + ] +} \ No newline at end of file diff --git a/packages/sim-consumidor-nos/aplication/SimNOS.controller.ts b/packages/sim-consumidor-nos/aplication/SimNOS.controller.ts index 18dc8b8..5912ad6 100644 --- a/packages/sim-consumidor-nos/aplication/SimNOS.controller.ts +++ b/packages/sim-consumidor-nos/aplication/SimNOS.controller.ts @@ -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; } } diff --git a/packages/sim-consumidor-nos/config/env/env.ts b/packages/sim-consumidor-nos/config/env/env.ts index 9b099c7..de7c7c9 100644 --- a/packages/sim-consumidor-nos/config/env/env.ts +++ b/packages/sim-consumidor-nos/config/env/env.ts @@ -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), + APP_HOST: String(process.env.NOS_HOST), // ESPECIFICO NOS NOS_BASE_URL: String(process.env.NOS_BASE_URL), diff --git a/packages/sim-consumidor-nos/domain/NosAPI.ts b/packages/sim-consumidor-nos/domain/NosAPI.ts index 2bcb728..93f5fc6 100644 --- a/packages/sim-consumidor-nos/domain/NosAPI.ts +++ b/packages/sim-consumidor-nos/domain/NosAPI.ts @@ -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 = { diff --git a/packages/sim-consumidor-nos/domain/transformers.ts b/packages/sim-consumidor-nos/domain/transformers.ts new file mode 100644 index 0000000..8e3b52a --- /dev/null +++ b/packages/sim-consumidor-nos/domain/transformers.ts @@ -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["billing_status"]>([ + ["active", "ACTIVE"], + ["terminated", "TERMINATED"] + ]) + +const networkStates = new Map< + NosApi.LineData["networkState"]["currentState"], + CommonSim["network_status"] +>([ + ["active", "ACTIVE"], + ["terminated", "TERMINATED"], + ["barred", "SUSPENDED"] +]) + +export function nosSimToCommonSim(nosSim: NosApi.LineData): CommonSim { + const billingState = billingStates.get(nosSim.billingState.currentState) ?? "UNKNOWN" + const networkState = networkStates.get(nosSim.networkState.currentState) ?? "UNKNOWN" + + const commonSim: CommonSim = { + 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 +} diff --git a/packages/sim-consumidor-nos/index.ts b/packages/sim-consumidor-nos/index.ts index 1afab93..b5fb5d1 100644 --- a/packages/sim-consumidor-nos/index.ts +++ b/packages/sim-consumidor-nos/index.ts @@ -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) } diff --git a/packages/sim-consumidor-objenious/.env b/packages/sim-consumidor-objenious/.env deleted file mode 100644 index 17b1246..0000000 --- a/packages/sim-consumidor-objenious/.env +++ /dev/null @@ -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 diff --git a/packages/sim-consumidor-objenious/aplication/Sim.controller.ts b/packages/sim-consumidor-objenious/aplication/Sim.controller.ts index 2905385..0fee3d4 100644 --- a/packages/sim-consumidor-objenious/aplication/Sim.controller.ts +++ b/packages/sim-consumidor-objenious/aplication/Sim.controller.ts @@ -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 diff --git a/packages/sim-consumidor-objenious/aplication/Sim.usecases.ts b/packages/sim-consumidor-objenious/aplication/Sim.usecases.ts index e6a73e8..ba30c09 100644 --- a/packages/sim-consumidor-objenious/aplication/Sim.usecases.ts +++ b/packages/sim-consumidor-objenious/aplication/Sim.usecases.ts @@ -488,4 +488,32 @@ export class SimUseCases { } } + + public async getLineByIccid(iccid: string): + Promise> { + 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] + } + + } } diff --git a/packages/sim-consumidor-objenious/config/jwtService.config.ts b/packages/sim-consumidor-objenious/config/jwtService.config.ts index 3d9bf78..b7319ab 100644 --- a/packages/sim-consumidor-objenious/config/jwtService.config.ts +++ b/packages/sim-consumidor-objenious/config/jwtService.config.ts @@ -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) { diff --git a/packages/sim-consumidor-objenious/domain/transformers.ts b/packages/sim-consumidor-objenious/domain/transformers.ts new file mode 100644 index 0000000..a30ca93 --- /dev/null +++ b/packages/sim-consumidor-objenious/domain/transformers.ts @@ -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["billing_status"]>([ + ["ACTIVATED", "ACTIVE"], + ["CANCELED", "TERMINATED"], + ["SUSPENDED", "SUSPENDED"], + ["TEST", "PREACTIVATED"] +]) + +const objeiousNetworkStates = new Map["network_status"]>([ + ["ACTIVATED", "ACTIVE"], + ["CANCELED", "TERMINATED"], + ["SUSPENDED", "SUSPENDED"], + ["BARRED", "SUSPENDED"] +]) + +export function objeniousSimToCommon(objSim: ObjeniousLine): CommonSim { + 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 = { + 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 +} diff --git a/packages/sim-consumidor-objenious/index.ts b/packages/sim-consumidor-objenious/index.ts index 38b6dda..6b8e3a5 100644 --- a/packages/sim-consumidor-objenious/index.ts +++ b/packages/sim-consumidor-objenious/index.ts @@ -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") diff --git a/packages/sim-entrada-eventos/README.md b/packages/sim-entrada-eventos/README.md new file mode 100644 index 0000000..e69de29 diff --git a/packages/sim-entrada-eventos/aplication/Order.controller.ts b/packages/sim-entrada-eventos/aplication/Order.controller.ts index 39c7909..06f02f5 100644 --- a/packages/sim-entrada-eventos/aplication/Order.controller.ts +++ b/packages/sim-entrada-eventos/aplication/Order.controller.ts @@ -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) diff --git a/packages/sim-entrada-eventos/aplication/Order.usecases.ts b/packages/sim-entrada-eventos/aplication/Order.usecases.ts index ec1a1a1..13a978a 100644 --- a/packages/sim-entrada-eventos/aplication/Order.usecases.ts +++ b/packages/sim-entrada-eventos/aplication/Order.usecases.ts @@ -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) + } + } } diff --git a/packages/sim-entrada-eventos/aplication/Sim.controller.ts b/packages/sim-entrada-eventos/aplication/Sim.controller.ts index 4b529cd..39a2e58 100644 --- a/packages/sim-entrada-eventos/aplication/Sim.controller.ts +++ b/packages/sim-entrada-eventos/aplication/Sim.controller.ts @@ -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 { diff --git a/packages/sim-entrada-eventos/aplication/Sim.usecases.ts b/packages/sim-entrada-eventos/aplication/Sim.usecases.ts index 6ba0727..91f3278 100644 --- a/packages/sim-entrada-eventos/aplication/Sim.usecases.ts +++ b/packages/sim-entrada-eventos/aplication/Sim.usecases.ts @@ -270,5 +270,6 @@ export class SimUsecases { return this.eventBus.publish([cancelationEvent]) } + } diff --git a/packages/sim-entrada-eventos/aplication/httpValidators.ts b/packages/sim-entrada-eventos/aplication/httpValidators.ts index ad566f8..ba2b066 100644 --- a/packages/sim-entrada-eventos/aplication/httpValidators.ts +++ b/packages/sim-entrada-eventos/aplication/httpValidators.ts @@ -32,10 +32,10 @@ const offerExists = >{ validationFunc: (a: { offer: string }) => offers.has(a.offer), } -const isUuidv7 = >{ - field: "correlation_id", +const isUuidv7 = >{ + 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 = >{ @@ -56,12 +56,27 @@ const validNumericId = >{ 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 = >{ + 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 ]) diff --git a/packages/sim-entrada-eventos/config/env/index.ts b/packages/sim-entrada-eventos/config/env/index.ts index 431fde7..8e2d4b4 100644 --- a/packages/sim-entrada-eventos/config/env/index.ts +++ b/packages/sim-entrada-eventos/config/env/index.ts @@ -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, }; diff --git a/packages/sim-entrada-eventos/config/servicesProxy.ts b/packages/sim-entrada-eventos/config/servicesProxy.ts new file mode 100644 index 0000000..aabaa56 --- /dev/null +++ b/packages/sim-entrada-eventos/config/servicesProxy.ts @@ -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] +]) + + diff --git a/packages/sim-entrada-eventos/index.ts b/packages/sim-entrada-eventos/index.ts index 78cfbb2..7a48307 100644 --- a/packages/sim-entrada-eventos/index.ts +++ b/packages/sim-entrada-eventos/index.ts @@ -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" }) }) diff --git a/packages/sim-entrada-eventos/infrastructure/franceRoutes.http.ts b/packages/sim-entrada-eventos/infrastructure/franceRoutes.http.ts index 3446ece..1412e7a 100644 --- a/packages/sim-entrada-eventos/infrastructure/franceRoutes.http.ts +++ b/packages/sim-entrada-eventos/infrastructure/franceRoutes.http.ts @@ -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}`); } } } diff --git a/packages/sim-entrada-eventos/infrastructure/orderRoutes.http.ts b/packages/sim-entrada-eventos/infrastructure/orderRoutes.http.ts index 2c1fbfb..9d4d574 100644 --- a/packages/sim-entrada-eventos/infrastructure/orderRoutes.http.ts +++ b/packages/sim-entrada-eventos/infrastructure/orderRoutes.http.ts @@ -28,13 +28,17 @@ const orderController = new OrderController({ * */ orderRoutes.get("/", (req, res) => { res.send("ok") }) -orderRoutes.get("/message_id/:correlation_id", orderController.getByQueueId()) +/* + * Ahora es el id de bdd + * */ +orderRoutes.get("/message_id/:id", orderController.getById()) /** Operaciones pendientes */ orderRoutes.get("/pending", orderController.getPending()) /** Order por id (uuid del mensaje) */ -orderRoutes.get("/:id", orderController.getById()) +// TODO: falla +orderRoutes.get("/:id", orderController.getByQueueId()) export { orderRoutes } diff --git a/packages/sim-entrada-eventos/infrastructure/portugalRoutes.http.ts b/packages/sim-entrada-eventos/infrastructure/portugalRoutes.http.ts new file mode 100644 index 0000000..48b151a --- /dev/null +++ b/packages/sim-entrada-eventos/infrastructure/portugalRoutes.http.ts @@ -0,0 +1,32 @@ +import { Router, Request } from "express" +import { ClientRequest, } from "http" +import { createProxyMiddleware } from "http-proxy-middleware" +import { env } from "#config/env/index.js" +import assert from "node:assert" + +const portugalRoutes = Router() + +const PORTUGAL_URL = env.NOS_CONSUMER_URL +assert.ok(PORTUGAL_URL, "No se ha definido una URL para el servicio consumidor de Francia") + +portugalRoutes.use("", createProxyMiddleware({ + target: PORTUGAL_URL, + changeOrigin: true, + pathRewrite: { + '^/spain/*': '/' + }, + + on: { + proxyReq: (proxyReq: ClientRequest, req: Request) => { + /* Debug de las peticiones */ + const protocol = req.protocol; + const host = req.get('host'); + const originalFullUrl = `${protocol}://${host}${req.originalUrl}`; + const destinationFullUrl = `${PORTUGAL_URL}${proxyReq.path}`; + console.log(`[Proxy PT]: ${req.method} ${req.url} -> ${proxyReq.path}`); + } + } +} +)) + +export { portugalRoutes } diff --git a/packages/sim-entrada-eventos/infrastructure/simRoutes.http.ts b/packages/sim-entrada-eventos/infrastructure/simRoutes.http.ts index c2e53dd..6f41891 100644 --- a/packages/sim-entrada-eventos/infrastructure/simRoutes.http.ts +++ b/packages/sim-entrada-eventos/infrastructure/simRoutes.http.ts @@ -1,9 +1,11 @@ import { rabbitmqEventBus } from '../config/eventBusConfig.js'; import { SimUsecases } from '../aplication/Sim.usecases.js'; import { SimController } from '../aplication/Sim.controller.js'; -import { Router } from 'express'; import { OrderRepository } from 'sim-shared/infrastructure/OrderRepository.js'; import { postgresClient } from '#config/postgreConfig.js'; +import { createProxyMiddleware } from 'http-proxy-middleware'; +import { ClientRequest, } from "http" +import { Router, Request } from "express" const simRoutes = Router() const orderRepository = new OrderRepository(postgresClient) @@ -37,4 +39,6 @@ simRoutes.post("/test", simController.test()) simRoutes.post("/free", simController.free()) +// WIP +simRoutes.get("/select", simController.select()) export { simRoutes } diff --git a/packages/sim-entrada-eventos/infrastructure/simconnectionsRoutes.ts b/packages/sim-entrada-eventos/infrastructure/simconnectionsRoutes.ts index 3f04022..b66e154 100644 --- a/packages/sim-entrada-eventos/infrastructure/simconnectionsRoutes.ts +++ b/packages/sim-entrada-eventos/infrastructure/simconnectionsRoutes.ts @@ -9,8 +9,7 @@ export const connectionsRoutes = Router() const CONNECTIONS_URL = env.CONNECTIONS_URL// TODO: Meter al ENV //const CONNECTIONS_URL = "http://sf-nfc-server.savefamilygps.net" -console.log("CONNURL: ", CONNECTIONS_URL) - +//console.log("CONNURL: ", CONNECTIONS_URL) connectionsRoutes.use("", createProxyMiddleware({ target: CONNECTIONS_URL, changeOrigin: true, diff --git a/packages/sim-entrada-eventos/infrastructure/spainRoutes.http.ts b/packages/sim-entrada-eventos/infrastructure/spainRoutes.http.ts new file mode 100644 index 0000000..cf2512c --- /dev/null +++ b/packages/sim-entrada-eventos/infrastructure/spainRoutes.http.ts @@ -0,0 +1,33 @@ +import { Router, Request } from "express" +import { ClientRequest, } from "http" +import { createProxyMiddleware } from "http-proxy-middleware" +import { env } from "#config/env/index.js" +import assert from "node:assert" + +const spainRoutes = Router() + +const SPAIN_URL = env.ALAI_CONSUMER_URL +assert.ok(SPAIN_URL, "No se ha definido una URL para el servicio consumidor de Francia") + +spainRoutes.use("", createProxyMiddleware({ + target: SPAIN_URL, + changeOrigin: true, + pathRewrite: { + '^/spain/*': '/' + }, + + on: { + proxyReq: (proxyReq: ClientRequest, req: Request) => { + /* Debug de las peticiones */ + const protocol = req.protocol; + const host = req.get('host'); + const originalFullUrl = `${protocol}://${host}${req.originalUrl}`; + const destinationFullUrl = `${SPAIN_URL}${proxyReq.path}`; + console.log(`[Proxy ES]: ${req.method} ${req.url} -> ${proxyReq.path}`); + } + } +} +)) + +//orderRoutes.get("/:iccid/suspended-time",) +export { spainRoutes } diff --git a/packages/sim-shared/aplication/JWT.service.test.ts b/packages/sim-shared/aplication/JWT.service.test.ts index 1d37d7e..28553d7 100644 --- a/packages/sim-shared/aplication/JWT.service.test.ts +++ b/packages/sim-shared/aplication/JWT.service.test.ts @@ -1,7 +1,7 @@ import { test, describe } from "vitest" import { jwtService } from "../config/jwtService.config.js" - -describe("Tokens Objenious", () => { +/* +describe("Tokens Objenious", (test) => { const jwt = jwtService test("Solicicitud normal de auth", async () => { @@ -14,4 +14,4 @@ describe("Tokens Objenious", () => { console.log("acceso refresh objenious", token) }) }) - +*/ diff --git a/packages/sim-shared/domain/CommonSim.ts b/packages/sim-shared/domain/CommonSim.ts new file mode 100644 index 0000000..6445f57 --- /dev/null +++ b/packages/sim-shared/domain/CommonSim.ts @@ -0,0 +1,26 @@ +export type CommonSim = { + company: "NOS" | "OBJ" | "ALAI", + iccid: string, + msisdn: string, + tariff: string, // Depende de la compañia + billing_status: "PREACTIVATED" | "ACTIVE" | "SUSPENDED" | "TERMINATED" | "UNKNOWN", + network_status: "AVAILABLE" | "PREACTIVATED" | "ACTIVE" | "SUSPENDED" | "TERMINATED" | "UNKNOWN", + preactivation_date?: Date | null, + activation_date?: Date | null, + suspension_date?: Date | null, + termination_date?: Date | null, + imei?: string, + raw: T +} + +/** + * Acorde a una peticion rest donde `raw` va a depender de `company` + */ +export type CommonSimDTO = CommonSim> & { + preactivation_date?: string | null, + activation_date?: string | null, + suspension_date?: string | null, + termination_date?: string | null, +} + + diff --git a/packages/sim-shared/domain/Order.ts b/packages/sim-shared/domain/Order.ts index df1c2f3..027228e 100644 --- a/packages/sim-shared/domain/Order.ts +++ b/packages/sim-shared/domain/Order.ts @@ -94,4 +94,19 @@ export type ErrorOrderDTO = stackTrace?: string } - +/* + * Se considera cada entrada de conditions como un filtro sobre un campo + * cada fila se podrá expresar como campo:filtro + * ```json + * { + * "value": "-gte 200" // Un valor >= 200 + * "text": "-eq 'busqueda' " // El campo tiene que ser exactamente 'busqueada' + * } + * ``` + * TODO: sacar opciones de paginación + * */ +export type OrderQuery = { + conditions: Record, + limit?: number | undefined, + offset?: number | undefined, +} diff --git a/packages/sim-shared/domain/SimCard.ts b/packages/sim-shared/domain/SimCard.ts deleted file mode 100644 index 08f86f6..0000000 --- a/packages/sim-shared/domain/SimCard.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { User } from "./User.js" - -export type SimCard = { - iccid: string, - imei: string, - - /* Pedido de shopify */ - orderdId?: string, // Pasar a tipo - - /* Subscripcion de shopify */ - subscriptionId?: string, // Pasar a tipo - - user?: User - - createdAt?: Date, - updatedAt?: Date, - - codigoOrigen?: string, -} - diff --git a/packages/sim-shared/domain/SimEvents.ts b/packages/sim-shared/domain/SimEvents.ts index e59a15e..9911f04 100644 --- a/packages/sim-shared/domain/SimEvents.ts +++ b/packages/sim-shared/domain/SimEvents.ts @@ -12,7 +12,8 @@ export namespace SimEvents { key: `sim.${string}.activate`, payload: { iccid: string, - offer?: string + offer?: string, + orderId?: string }, } diff --git a/packages/sim-shared/domain/User.ts b/packages/sim-shared/domain/User.ts deleted file mode 100644 index b3199ef..0000000 --- a/packages/sim-shared/domain/User.ts +++ /dev/null @@ -1,6 +0,0 @@ -export type User = { - userId: string, - userName?: string, - email?: string, - tlfn?: string, -} diff --git a/packages/sim-shared/domain/objeniousLine.ts b/packages/sim-shared/domain/objeniousLine.ts index cf48f3b..093f8ec 100644 --- a/packages/sim-shared/domain/objeniousLine.ts +++ b/packages/sim-shared/domain/objeniousLine.ts @@ -82,7 +82,7 @@ export type ObjeniousLine = { activationDate: string | null, //"2026-03-17T11:04:11.408+00:00", commercialStatus: string, //"test", commercialStatusDate: string, //"2026-03-17T11:41:01.493+00:00", - networkStatus: string, // "ACTIVATED", + networkStatus: "ACTIVATED" | "SUSPENDED" | "CANCELED" | "BARRED", // "ACTIVATED", billingStatus: "ACTIVATED" | "SUSPENDED" | "CANCELED" | "TEST", billingStatusChangeDate: string | null, // "2026-03-17T11:01:00.276+00:00", billingActivationDate: string | null //, diff --git a/packages/sim-shared/infrastructure/HTTPClient.ts b/packages/sim-shared/infrastructure/HTTPClient.ts index 00084d1..71607a7 100644 --- a/packages/sim-shared/infrastructure/HTTPClient.ts +++ b/packages/sim-shared/infrastructure/HTTPClient.ts @@ -1,5 +1,6 @@ import axios, { AxiosInstance } from "axios" import { IJWTService, JWTToken } from "../domain/JWT.js" +import https from "https" // Cambiar por IJWRGeneralService @@ -19,12 +20,15 @@ export class HttpClient { constructor(args: { baseURL: string, - headers: Object, - jwtManager: JWTProvider<{}> // todo: asociar el tipo de token, - jwtService?: IJWTService + headers: Record, + jwtManager: JWTProvider<{}>, // todo: asociar el tipo de token + jwtService?: IJWTService, + httpsAgent?: https.Agent }) { this.client = axios.create({ - ...args + baseURL: args.baseURL, + headers: args.headers, + httpsAgent: args.httpsAgent }) this.jwtManager = args.jwtManager @@ -37,7 +41,7 @@ export class HttpClient { // token valido de forma preventiva const token = await this.jwtManager.getAccessToken() - if (token == undefined) throw new Error("No se ha obtenido el token para la peticion") + if (token == undefined) throw new Error("No se ha obtenido el token para la petición") config.headers.Authorization = `Bearer ${this.jwtManager.authToken!.rawToken}` console.log("request completa", config.data) @@ -50,8 +54,7 @@ export class HttpClient { this.client.interceptors.response.use( (response) => { return response; - }, - async (error) => { + }, async (error) => { // TODO: Esta parte no tiene tipos, hay que asegurar el error const req = error.config console.error("[http] Error en la respuesta ", error, error.response) @@ -61,8 +64,17 @@ export class HttpClient { return Promise.reject(error) } ) - } + get get() { + return this.client.get + } + get post() { + return this.client.post + } + + get patch() { + return this.client.patch + } } diff --git a/packages/sim-shared/infrastructure/ObjeniousOperationRepository.ts b/packages/sim-shared/infrastructure/ObjeniousOperationRepository.ts index 9adc9ef..7f80747 100644 --- a/packages/sim-shared/infrastructure/ObjeniousOperationRepository.ts +++ b/packages/sim-shared/infrastructure/ObjeniousOperationRepository.ts @@ -14,6 +14,33 @@ export class ObjeniousOperationsRepository implements IOperationsRepository { ) { } + public async getLineByIccid(iccid: string): Promise> { + const path = "/lines" + const params = new URLSearchParams([ + ["identifier.identifierType", "ICCID"], + ["identifier.identifiers", iccid] + ]) + + const req = this.http.client.get(path, { + params: params + }) + + const res = await tryCatch(req) + + if (res.error != undefined) { + return { + error: res.error?.message + } + } + + const lines = res.data.data.content + + return { + data: lines + } + + } + /** * Consulta el estado de una o mas lineas directamente a la API de Objenious * TODO: No hay paginacion como en getLinesByStatusAPI diff --git a/packages/sim-shared/infrastructure/OrderRepository.test.ts b/packages/sim-shared/infrastructure/OrderRepository.test.ts index 68ba429..e4c3eb8 100644 --- a/packages/sim-shared/infrastructure/OrderRepository.test.ts +++ b/packages/sim-shared/infrastructure/OrderRepository.test.ts @@ -1,6 +1,6 @@ import { before, describe, it } from "node:test"; import { OrderRepository } from "./OrderRepository.js"; -import { CreateOrderDTO } from "../domain/Order.js"; +import { CreateOrderDTO, OrderQuery } from "../domain/Order.js"; import { postgresClient } from "../config/config.test.js"; import assert from "node:assert"; @@ -28,11 +28,6 @@ describe("Test OrderRepository", {}, (ctx) => { const result1 = await orderRepo.createOrder(order1) assert.ok(result1.data != undefined, result1.error as string) testIds.push(result1.data.id) - - // Order2 -> Para el test de crearOrder - // const result2 = await orderRepo.createOrder(order2) - // assert(result2.data != undefined) - // testIds.push(result2.data.id) }) it("Insert new Order", async () => { @@ -73,7 +68,6 @@ describe("Test OrderRepository", {}, (ctx) => { it("Get pending orders should return all pending orders in ASC order", async () => { // We already have 'testId' from before block - // Insert two more orders const orderA = { ...order1, correlation_id: "pending-A" } const orderB = { ...order1, correlation_id: "pending-B" } @@ -168,4 +162,20 @@ describe("Test OrderRepository", {}, (ctx) => { assert(result.data.status === "dlx") assert(result.data.finish_date != null) }) + + it("Query generates with parameters", async () => { + const params: OrderQuery = { + conditions: { + status: "-eq 'pending'", + retry_count: "-gte 2", + webhook_host: "-null NULL" + } + } + //@ts-expect-error + const res = orderRepo.generateTableQuery("test", params) + console.log("Query:", res) + assert.ok(res != undefined, "Query must be defined") + assert.ok(res.values.length == 3, "Query parameters must be the same as conditions") + + }) }) diff --git a/packages/sim-shared/infrastructure/OrderRepository.ts b/packages/sim-shared/infrastructure/OrderRepository.ts index 26d05f0..1faa675 100644 --- a/packages/sim-shared/infrastructure/OrderRepository.ts +++ b/packages/sim-shared/infrastructure/OrderRepository.ts @@ -2,7 +2,7 @@ * TODO: Usar */ import { PoolClient, QueryResult, QueryResultRow } from "pg"; -import { CreateOrderDTO, ErrorOrderDTO, FinishOrderDTO, OrderTracking, UpdateOrderDTO } from "../domain/Order.js"; +import { CreateOrderDTO, ErrorOrderDTO, FinishOrderDTO, OrderQuery, OrderTracking, UpdateOrderDTO } from "../domain/Order.js"; import { Result, tryCatch } from "../domain/Result.js"; import { PgClient } from "./PgClient.js"; import assert from "node:assert"; @@ -55,7 +55,77 @@ export class OrderRepository { } } + /** + * Mapeo de prefijos a operadores SQL + */ + private OPERATOR_MAP: Record = { + "-eq": "=", + "-neq": "!=", + "-gt": ">", + "-gte": ">=", + "-lt": "<", + "-lte": "<=", + "-like": "LIKE", + }; + /** + * Tabla general para sacar datos de la tabla en base a unas condiciones + * TODO: + * - Dar la opción de generar los campos a devolver en vez de * + * - Garantizar el numero de parametros de respuesta + */ + private generateTableQuery(table: string, query: OrderQuery) { + const { conditions, limit, offset } = query; + const whereClauses: string[] = []; + const queryValues: any[] = []; + + let paramIndex = 1; // Para los parametros de PostgreSQL ($1, $2) (que empiezan por 1) + + for (const [column, filter] of Object.entries(conditions)) { + const match = filter.match(/^(-\w+)\s+(.+)$/); + + if (match) { + const [_, prefix, value] = match; + const operator = this.OPERATOR_MAP[prefix]; + + if (operator) { + // Eliminación de comillas + const cleanValue = value.replace(/^'|'$/g, ""); + + whereClauses.push(`${column} ${operator} $${paramIndex}`); + queryValues.push(operator === "LIKE" ? `%${cleanValue}%` : cleanValue); + paramIndex++; + } + } + } + + // 2. Query completa + // TODO: Cambair el * por parametros + let sql = `SELECT * FROM ${table}`; + + if (whereClauses.length > 0) { + sql += ` WHERE ${whereClauses.join(" AND ")}`; + } + + // 3. Paginacion + if (limit !== undefined) { + sql += ` LIMIT ${Number(limit)}`; + } + if (offset !== undefined) { + sql += ` OFFSET ${Number(offset)}`; + } + + return { + sql, + values: queryValues, + }; + } + + public async getOrdersByQuery(args: OrderQuery) { + const query = this.generateTableQuery('order_tracking', args) + const queryPromise = this.pgClient.query>>(query.sql, query.values) + const result = await this.getAll(queryPromise) + } /** * El tipo representa el contenido del mensaje de los order diff --git a/test_api.ts b/test_api.ts new file mode 100644 index 0000000..ac3571d --- /dev/null +++ b/test_api.ts @@ -0,0 +1,37 @@ +import { NosHttpClient } from "./packages/sim-consumidor-nos/infrastructure/NosHttpClient.js"; +import { NosRepository } from "./packages/sim-consumidor-nos/infrastructure/NosRepository.js"; +import { env } from "./packages/sim-consumidor-nos/config/env/env.js"; + +async function main() { + console.log("NOS_BASE_URL", env.NOS_BASE_URL); + const client = new NosHttpClient(env.NOS_BASE_URL); + + // Try to get a subscriber + const res = await client.get("/subscribers", { params: { limit: 1 } }); + console.log("SUBSCRIBER:", res.data.content[0].physicalId); + + const iccid = res.data.content[0].physicalId; + + try { + const history = await client.get(`/subscribers/${iccid}/history`); + console.log("HISTORY:", history.data); + } catch(e) { + console.error("HISTORY ERROR"); + } + + try { + const audit = await client.get(`/subscribers/${iccid}/audit`); + console.log("AUDIT:", audit.data); + } catch(e) { + console.error("AUDIT ERROR"); + } + + try { + const actions = await client.get(`/subscribers/${iccid}/actions`); + console.log("ACTIONS:", actions.data); + } catch(e) { + console.error("ACTIONS ERROR"); + } +} + +main().catch(console.error); diff --git a/yarn.lock b/yarn.lock index 36d0a34..49d4cbf 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2943,6 +2943,30 @@ __metadata: languageName: node linkType: hard +"sim-consumidor-alai@workspace:packages/sim-consumidor-alai": + version: 0.0.0-use.local + resolution: "sim-consumidor-alai@workspace:packages/sim-consumidor-alai" + dependencies: + "@tsconfig/node22": "npm:*" + "@types/amqplib": "npm:^0.10.8" + "@types/cors": "npm:*" + "@types/express": "npm:*" + "@types/node": "npm:*" + "@types/supertest": "npm:*" + amqplib: "npm:^0.10.9" + cors: "npm:*" + dotenv: "npm:*" + express: "npm:*" + prettier: "npm:*" + sim-shared: "sim-shared:*" + supertest: "npm:*" + tsc-alias: "npm:^1.8.16" + tsx: "npm:*" + typescript: "npm:*" + vitest: "npm:*" + languageName: unknown + linkType: soft + "sim-consumidor-nos@workspace:packages/sim-consumidor-nos": version: 0.0.0-use.local resolution: "sim-consumidor-nos@workspace:packages/sim-consumidor-nos"