10 Commits

31 changed files with 1186 additions and 187 deletions

View File

@@ -0,0 +1,32 @@
/**
* Para la tarea WEBINT-328-Pausas-cacelaciones.
* Almacena las pausas/cancelaciones que no se han podido hacer porque la linea esta en
* "Test"
*/
DO $$ BEGIN
CREATE TYPE SUSPENDTERMINATE AS ENUM ('suspend','terminate');
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
CREATE TABLE IF NOT EXISTS pause_cancel_tasks (
id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
iccid TEXT NOT NULL,
operation_type SUSPENDTERMINATE,
last_checked TIMESTAMPTZ, -- Última vez que se ha comprobado que no esté en test
activation_date TIMESTAMPTZ, -- Fecha de activacion para comprobar si ha pasdo un mes
next_check TIMESTAMPTZ, -- Si se ha comprobado se asignará la siguiente fecha de revision
completed_date TIMESTAMPTZ, -- Cuando se ha completado, para bien o mal.
error TEXT,
action_data JSONB -- datos de la operacion original.
);
-- Indice de las tareas que no han terminado
CREATE INDEX idx_pause_cancel_tasks_pending
ON pause_cancel_tasks (next_check)
WHERE completed_date IS NULL;

View File

@@ -15,7 +15,7 @@ params:query {
}
body:form-urlencoded {
iccid: 8933201125065160414
iccid: 8933201125068886692
}
settings {

View File

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

View File

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

View File

@@ -0,0 +1,118 @@
import { describe, it, beforeEach, mock, after } from "node:test";
import assert from "node:assert";
import { SimController } from "./Sim.controller.js";
import { EventBus } from "sim-shared/domain/EventBus.port.js";
import { SimUseCases } from "./Sim.usecases.js";
import { ConsumeMessage } from "amqplib";
import { postgrClient, pgPool } from "#config/postgreConfig.js";
import { httpInstance } from "#config/httpClient.config.js";
import { OrderRepository } from "sim-shared/infrastructure/OrderRepository.js";
import { PauseCancelTaskRepository } from "#adapters/PauseCancelTaskRepository.js";
import { ObjeniousOperationsRepository } from "sim-shared/infrastructure/ObjeniousOperationRepository.js";
import { ActionData } from "#domain/DTOs/objeniousapi.js";
describe("SimController Integration Tests (Real UseCases)", () => {
let eventBusMock: any;
let controller: SimController;
let useCases: SimUseCases;
beforeEach(() => {
// Mock ONLY the event bus as requested
eventBusMock = {
publish: mock.fn(),
addSubscribers: mock.fn(),
consume: mock.fn(),
ack: mock.fn(async () => { }),
nack: mock.fn(async () => { }),
};
const operationRepository = new ObjeniousOperationsRepository(
httpInstance,
postgrClient,
);
const orderRepository = new OrderRepository(postgrClient);
const pauseRepository = new PauseCancelTaskRepository(postgrClient);
useCases = new SimUseCases({
httpClient: httpInstance,
operationRepository: operationRepository,
orderRepository: orderRepository,
pauseRepository: pauseRepository
});
// @ts-expect-error
useCases.findActivationDate = async (data: ActionData) => new Date()
controller = new SimController(eventBusMock as unknown as EventBus, useCases);
});
const createMockMsg = (payload: any): ConsumeMessage => {
return {
content: Buffer.from(JSON.stringify(payload)),
fields: {},
properties: {
headers: {
message_id: "test-correlation-id"
}
},
} as unknown as ConsumeMessage;
};
after(async () => {
await pgPool.end();
});
describe("suspend", () => {
it("should call stage_suspend and interact with DB and EventBus", async () => {
const iccid = "test-iccid-suspend-" + Date.now();
const msg = createMockMsg({
key: "sim.test.pause",
payload: {
iccid: iccid
},
headers: {
message_id: "correlation-suspend-" + iccid
}
});
const handler = controller.suspend();
await handler(msg);
// Verify that it reached the stage_suspend logic (which adds to pauseRepository)
// We can query the DB or check if ACK was called
assert.strictEqual(eventBusMock.ack.mock.callCount(), 1, "Message should be ACKed on success");
assert.strictEqual(eventBusMock.nack.mock.callCount(), 0, "Message should not be NACKed");
});
});
describe("terminate", () => {
it("should call stage_terminate and interact with DB and EventBus", async () => {
const iccid = "test-iccid-terminate-" + Date.now();
const msg = createMockMsg({
key: "sim.test.pause",
payload: {
iccid: iccid
},
headers: {
message_id: "correlation-terminate-" + iccid
}
});
const handler = controller.terminate();
await handler(msg);
assert.strictEqual(eventBusMock.ack.mock.callCount(), 1, "Message should be ACKed on success");
assert.strictEqual(eventBusMock.nack.mock.callCount(), 0, "Message should not be NACKed");
});
});
describe("Error Handling", () => {
it("should nack if message is invalid", async () => {
const msg = {
content: Buffer.from("invalid json"),
fields: {},
properties: {},
} as unknown as ConsumeMessage;
const handler = controller.suspend();
await assert.rejects(handler(msg), "Error de suspension consumiendo el mensaje no es valido");
});
});
});

View File

@@ -3,7 +3,7 @@ import { ConsumeMessage } from "amqplib";
import { SimUseCases } from "./Sim.usecases.js";
import { SimEvents } from "sim-shared/domain/SimEvents.js";
import { Result } from "sim-shared/domain/Result.js";
import { env } from "#config/env/index.js";
import { ActionData } from "#domain/DTOs/objeniousapi.js";
/**
* La clase usa generadores de funciones para mantener el contexto
@@ -37,6 +37,7 @@ export class SimController {
} 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;
}
@@ -157,6 +158,9 @@ export class SimController {
}
}
/**
* Lo mismo que pause
*/
public suspend() {
return async (msg: ConsumeMessage) => {
let msgData;
@@ -171,14 +175,18 @@ export class SimController {
}
const iccid = msgData.payload.iccid
const res = await this.tryUseCase(msg, this.useCases.suspend({
const suspendData: ActionData = {
correlation_id: msgData.headers?.message_id,
dueDate: this.genDueDate(2 * 60).toISOString(),
identifier: {
identifierType: "ICCID",
identifiers: [iccid]
identifiers: [iccid] // Por algún motivo solo he puesto un iccd por identifier
}
}))
}
const useCaseRes = await this.tryUseCase(msg, this.useCases.stage_suspend(suspendData))
/*
const res = await this.tryUseCase(msg, this.useCases.suspend(actionData))
*/
}
}
@@ -195,16 +203,20 @@ export class SimController {
if (msgData == undefined) {
return Promise.reject("Mensaje invalido")
}
const iccid = msgData.payload.iccid
console.log("Mensaje procesado", msgData)
const res = await this.tryUseCase(msg, this.useCases.terminate({
const terminateActionData: ActionData = {
correlation_id: msgData.headers?.message_id,
dueDate: this.genDueDate(2 * 60).toISOString(),
identifier: {
identifierType: "ICCID",
identifiers: [iccid]
}
}))
}
//const res = await this.tryUseCase(msg, this.useCases.terminate(terminateActionData))
const res = await this.tryUseCase(msg, this.useCases.stage_terminate(terminateActionData))
}
}

View File

@@ -5,6 +5,8 @@ import { Result } from "sim-shared/domain/Result.js"
import { ObjeniousOperation, IOperationsRepository as OperationsRepositoryPort } from "sim-shared/domain/operationsRepository.port.js"
import assert from "node:assert"
import { OrderRepository } from "sim-shared/infrastructure/OrderRepository.js"
import { CreatePauseCancelTaskDTO, PauseCancelTaskRepository } from "#adapters/PauseCancelTaskRepository.js"
import { ObjeniousOperationsRepository } from "sim-shared/infrastructure/ObjeniousOperationRepository.js"
// TODO:
// - Pasar a un archivo de DTOs
@@ -12,21 +14,24 @@ import { OrderRepository } from "sim-shared/infrastructure/OrderRepository.js"
export class SimUseCases {
private readonly httpClient: HttpClient
private readonly operationRepository: OperationsRepositoryPort
private readonly objeniousRepository: ObjeniousOperationsRepository
private readonly orderRepository: OrderRepository
private readonly pauseRepository: PauseCancelTaskRepository
constructor(args: {
httpClient: HttpClient,
operationRepository: OperationsRepositoryPort,
orderRepository: OrderRepository
operationRepository: ObjeniousOperationsRepository,
orderRepository: OrderRepository,
pauseRepository: PauseCancelTaskRepository
}) {
this.httpClient = args.httpClient
this.operationRepository = args.operationRepository
this.objeniousRepository = args.operationRepository
this.orderRepository = args.orderRepository
this.pauseRepository = args.pauseRepository
}
private async logOperation(data: ObjeniousOperation) {
await this.operationRepository.createOperation({
await this.objeniousRepository.createOperation({
...data
})
}
@@ -73,6 +78,8 @@ export class SimUseCases {
request_id: response.data.requestId
}
// TODO: Esto tiene poco sentido si la operacion ya se
// tenia que haber creado en el generador
this.logOperation(operation)
.then().catch(e => console.error(e))
@@ -89,7 +96,6 @@ export class SimUseCases {
error: undefined,
data: true
}
} else {
return {
error: String(response.status),
@@ -238,6 +244,133 @@ export class SimUseCases {
})
}
/**
* Metodo muy especifico para obtener la fecha e activacion o en su defecto
* la actual para aber cuando se va a completar el periodo de test de una linea
*/
private async findActivationDate(actionData: ActionData) {
const iccid = actionData.identifier.identifiers
const lineData = await this.objeniousRepository.getLinesAPI("ICCID", iccid)
let activationDate = new Date()
// Si no se pueden sacar datos de la linea guardo momentaneamente el error
// pero no se cancela la operacion, el error puede ser de objenious y no nos
// puede afectar
console.log("LineData", lineData.data)
if (lineData.error != undefined) {
console.error(lineData.error)
} else {
const activationDateStr = lineData.data[0].status.activationDate
if (activationDateStr != undefined && activationDateStr != "") {
activationDate = new Date(activationDateStr)
}
}
return activationDate
}
/**
* Paso previo a la suspension para evitar errores cuando el billing es test
*/
public stage_suspend(suspendData: ActionData): () => Promise<Result<string, boolean>> {
return async (): Promise<Result<string, boolean>> => {
const correlation_id = suspendData.correlation_id
const iccid = suspendData.identifier.identifiers
const fail = (error: string) => {
console.error("[Sim.usecases]", error)
if (correlation_id != undefined) {
this.orderRepository.updateOrder({
correlation_id: correlation_id,
new_status: "failed"
})
}
}
let activationDate;
try {
activationDate = await this.findActivationDate(suspendData)
} catch (e) {
return {
error: String(e)
}
}
const newTask: CreatePauseCancelTaskDTO = {
iccid: iccid[0],
activation_date: activationDate,
next_check: undefined, // Que se haga instantaneamente al ser la primera
operation_type: "suspend",
action_data: suspendData
}
const taskCreated = await this.pauseRepository.addTask(newTask)
// Caso que la task no se pueda crear en la BDD
if (taskCreated.error != undefined) {
fail(taskCreated.error)
return {
error: taskCreated.error
}
}
// Caso que se haya creado en la BDD
if (correlation_id != undefined) {
this.orderRepository.updateOrder({
correlation_id: correlation_id,
new_status: "running"
})
}
return {
data: true
}
}
}
/**
* Paso previo a la suspension para evitar errores cuando el billing es test
*/
public stage_terminate(terminateData: ActionData): () => Promise<Result<string, boolean>> {
return async (): Promise<Result<string, boolean>> => {
const correlation_id = terminateData.correlation_id
const activationDate = await this.findActivationDate(terminateData)
const newTask: CreatePauseCancelTaskDTO = {
iccid: terminateData.identifier.identifiers[0],
activation_date: activationDate,
next_check: undefined, // Que se haga instantaneamente al ser la primera
operation_type: "terminate",
action_data: terminateData
}
const taskCreated = await this.pauseRepository.addTask(newTask)
// Caso que la task no se pueda crear en la BDD
if (taskCreated.error != undefined) {
console.error("[Sim.usecases]", taskCreated.error)
if (correlation_id != undefined) {
this.orderRepository.updateOrder({
correlation_id: correlation_id,
new_status: "failed"
})
}
return {
error: taskCreated.error
}
}
// Caso que se haya creado en la BDD
if (correlation_id != undefined) {
this.orderRepository.updateOrder({
correlation_id: correlation_id,
new_status: "running"
})
}
return {
data: true
}
}
}
public terminate(terminationData: ActionData): () => Promise<Result<string, boolean>> {
const OPERATION_URL = "/actions/terminateLine"
return this.generateUseCase({

View File

@@ -1,6 +1,6 @@
import { HttpClient } from "sim-shared/infrastructure/HTTPClient.js"
import { JWTService } from "../aplication/JWT.service.js"
import { env } from "./env/index.js"
import { jwtService } from "./jwtService.config.js"
const OBJ_BASE_URL = env.OBJ_BASE_URL
@@ -9,5 +9,5 @@ export const httpInstance = new HttpClient({
headers: {
"content-type": " application/json; charset=utf-8"
},
jwtManager: new JWTService()
jwtManager: jwtService
})

View File

@@ -0,0 +1,59 @@
import { GrantAccessRequestBody, JWTService } from "sim-shared/aplication/JWT.service.js"
import { env } from "./env/index.js"
import { JWTHeader } from "sim-shared/domain/JWT.js"
const PRIVATE_KEY_PATH = env.OBJ_PEM_PATH
const GET_TOKEN_URL = "https://idp.docapost.io/auth/realms/GETWAY/protocol/openid-connect/token"
const REFRESH_TOKEN_URL = GET_TOKEN_URL
const DEFAULT_BODY: GrantAccessRequestBody = {
grant_type: "client_credentials",
client_id: env.OBJ_CLIENT_ID,
client_assertion_type: "urn:ietf:params:oauth:client-assertion-type:jwt-bearer",
client_assertion: env.OBJ_CLI_ASSERTION
}
const DEFAULT_HEADERS = {
"content-type": "application/x-www-form-urlencoded"
}
const DEFAULT_HEADERS_JWT = {
alg: "RS256",
typ: "JWT",
kid: env.OBJ_KID,
}
const DEFAULT_DATA_JWT = {
sub: env.OBJ_CLIENT_ID,
iss: env.OBJ_CLIENT_ID,
aud: "https://idp.docapost.io/auth/realms/GETWAY",
jti: Date.now().toString(),
}
function addIATHeaders(authHeaders: Object) {
const headers = <JWTHeader>{
...authHeaders,
sub: env.OBJ_CLIENT_ID,
iss: env.OBJ_CLIENT_ID,
aud: GET_TOKEN_URL,
jti: Date.now().toString(),
iat: Math.floor(Date.now() / 1000),
exp: Math.floor(Date.now() / 1000) + 5 * 60,
}
return headers
}
export const jwtService = new JWTService({
transformJWTHeaders: addIATHeaders,
defaultHeaders: DEFAULT_HEADERS,
defaultBody: DEFAULT_BODY,
defaultJWTHeaders: DEFAULT_HEADERS_JWT,
defaultJWTPayload: DEFAULT_DATA_JWT,
privateKeyPath: PRIVATE_KEY_PATH,
tokenUrl: GET_TOKEN_URL,
refreshTokenUrl: REFRESH_TOKEN_URL
})

View File

@@ -8,6 +8,7 @@ import { SimUseCases } from "./aplication/Sim.usecases.js"
import { SimController } from "./aplication/Sim.controller.js"
import { SimRouter } from "./aplication/Sim.router.js"
import { OrderRepository } from "sim-shared/infrastructure/OrderRepository.js"
import { PauseCancelTaskRepository } from "#adapters/PauseCancelTaskRepository.js"
async function startWorker() {
const rmqClient = await startRMQClient()
@@ -18,15 +19,21 @@ async function startWorker() {
await pgClient.checkDatabaseConnection()
const operationRepository = new ObjeniousOperationsRepository(pgClient)
const operationRepository = new ObjeniousOperationsRepository(
httpClient,
pgClient,
)
const orderRepository = new OrderRepository(pgClient)
const pauseRepository = new PauseCancelTaskRepository(pgClient)
const simActivationController = new SimController(
rmqClient,
new SimUseCases({
httpClient: httpClient,
operationRepository: operationRepository,
orderRepository: orderRepository
orderRepository: orderRepository,
pauseRepository: pauseRepository
})
)
const simRouter = new SimRouter(simActivationController, rmqClient)

View File

@@ -0,0 +1,72 @@
import { after, before, describe, it } from "node:test";
import { CreatePauseCancelTaskDTO, PauseCancelTaskRepository } from "./PauseCancelTaskRepository.js";
import { postgrClient } from "#config/postgreConfig.js";
import assert from "node:assert";
const testTask: CreatePauseCancelTaskDTO = {
iccid: "1234",
operation_type: "suspend",
activation_date: new Date(),
next_check: new Date(),
action_data: {
dueDate: new Date().toString(),
correlation_id: "12223",
identifier: {
identifiers: ["1234"],
identifierType: "ICCID"
}
}
}
describe("Test PauseCancelTaskRepository - DB", () => {
const createdIds: number[] = [];
const pauseRepo = new PauseCancelTaskRepository(postgrClient)
before(() => {
})
after(() => {
})
it("Should create a task", async () => {
const created = await pauseRepo.addTask(testTask)
assert.ok(created != undefined, "A value must be returned always")
assert.ok(created.error == undefined, "Should not return a error")
assert.ok(created.data != undefined, "Data must be returned")
createdIds.push(created.data.id)
})
it("Should update a existing task", async () => {
const updated = await pauseRepo.updateTask({
id: createdIds[0],
next_check: new Date()
})
assert.ok(updated != undefined, "A value must be returned always")
assert.ok(updated.error == undefined, "Should not return a error")
assert.ok(updated.data != undefined, "Data must be returned")
})
it("Should finish a existing task", async () => {
const finish = await pauseRepo.finishTask({
id: createdIds[0],
error: "ok"
})
assert.ok(finish != undefined, "A value must be returned always")
assert.ok(finish.error == undefined, "Should not return a error")
assert.ok(finish.data != undefined, "Data must be returned")
})
it("Should get at least 1 pending task", async () => {
const created = await pauseRepo.addTask(testTask)
const pending = await pauseRepo.getPending()
assert.ok(pending != undefined, "A value must be returned always")
assert.ok(pending.error == undefined, "Should not return a error")
assert.ok(pending.data != undefined, "Data must be returned")
console.log("--> ", pending.data[0])
})
})

View File

@@ -0,0 +1,126 @@
import { Result } from "sim-shared/domain/Result.js";
import { QueryResult } from "pg";
import { PgClient } from "sim-shared/infrastructure/PgClient.js";
import { AxiosError } from "axios";
import { ActionData } from "#domain/DTOs/objeniousapi.js";
export type PauseCancelTask = {
id: number;
iccid: string;
operation_type: "suspend" | "terminate",
last_checked?: Date | null;
activation_date?: Date | null;
next_check?: Date | null;
completed_date?: Date | null;
error?: string | null;
action_data: ActionData
}
export type CreatePauseCancelTaskDTO = Pick<PauseCancelTask, "iccid" | "activation_date" | "next_check" | "operation_type" | "action_data">
export type UpdatePauseCancelTaskDTO = Pick<PauseCancelTask, "id" | "next_check">
export type FinishPauseCancelTaskDTO = Pick<PauseCancelTask, "id" | "error">
/**
* Repositorio para compensar los problemas de cacelcaiones/pausas de objenious a
* la hora aplicarlo sobre una linea con el billing a test.
*/
export class PauseCancelTaskRepository {
constructor(
private readonly pgClient: PgClient
) {
}
/**
* Obtiene las siguientes que se pueden lanzar, puede haber más pero
* estan pendientes
*/
public async getPending(): Promise<Result<string, PauseCancelTask[]>> {
const sql = `
SELECT * FROM pause_cancel_tasks
WHERE completed_date IS NULL
AND (next_check <= NOW() OR next_check IS NULL)
ORDER BY id ASC;
`;
try {
const res: QueryResult<PauseCancelTask> = await this.pgClient.query(sql);
return {
data: res.rows
}
} catch (e) {
return {
error: (e as AxiosError).message
}
}
}
public async addTask(task: CreatePauseCancelTaskDTO): Promise<Result<string, PauseCancelTask>> {
const sql = `
INSERT INTO pause_cancel_tasks (iccid, activation_date, next_check, last_checked, operation_type, actionData)
VALUES ($1, $2, $3, now(), $4, $5)
RETURNING *;
`;
try {
const values = [task.iccid, task.activation_date, task.next_check, task.operation_type, JSON.stringify(task.action_data)];
const res: QueryResult<PauseCancelTask> = await this.pgClient.query(sql, values);
return {
data: res.rows[0]
}
} catch (e) {
return {
error: (e as AxiosError).message
}
}
}
/**
* Se ha vuelto a comprobar la tarea pero sigue en test
*/
public async updateTask(updateData: UpdatePauseCancelTaskDTO): Promise<Result<string, PauseCancelTask>> {
const sql = `
UPDATE pause_cancel_tasks
SET last_checked = now(), next_check = $1
WHERE id = $2
RETURNING *;
`;
try {
const res = await this.pgClient.query<PauseCancelTask>(sql, [updateData.next_check, updateData.id]);
return {
data: res.rows[0]
}
} catch (e) {
return {
error: (e as AxiosError).message
}
}
}
/**
* La tarea ha termiando bien o mal
*/
public async finishTask(finishData: FinishPauseCancelTaskDTO) {
const sql = `
UPDATE pause_cancel_tasks
SET completed_date = NOW(), error = $1
WHERE id = $2
RETURNING *;
`;
try {
const res = await this.pgClient.query(sql, [finishData.error, finishData.id]);
return {
data: res.rows[0]
}
} catch (e) {
return {
error: (e as AxiosError).message
}
}
}
}

View File

@@ -53,7 +53,7 @@
}
},
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"test": "node --import tsx --test ./**/*.test.ts",
"dev": "tsx watch index.ts",
"build": "tsc --build && yarn tsc-alias -p tsconfig.json && cp .env package.json ../../dist/packages/sim-consumidor-objenious/",
"start": "node ../../dist/packages/sim-consumidor-objenious/index.js",

View File

@@ -36,7 +36,6 @@ export class SimController {
}) {
return async (req: Request, res: Response) => {
const body = req.body
// 1. Validacion del body
if (args.validator != undefined) {
const validationResult = args.validator.validate(body)

View File

@@ -1,6 +1,7 @@
import { HttpClient } from "sim-shared/infrastructure/HTTPClient.js"
import { env } from "./env/index.js"
import { JWTService } from "packages/sim-consumidor-objenious/aplication/JWT.service.js"
import { jwtService } from "./jwtService.config.js"
const OBJ_BASE_URL = env.OBJ_BASE_URL
@@ -9,5 +10,5 @@ export const httpInstance = new HttpClient({
headers: {
"content-type": " application/json; charset=utf-8"
},
jwtManager: new JWTService()
jwtManager: jwtService
})

View File

@@ -0,0 +1,59 @@
import { GrantAccessRequestBody, JWTService } from "sim-shared/aplication/JWT.service.js"
import { env } from "./env/index.js"
import { JWTHeader } from "sim-shared/domain/JWT.js"
const PRIVATE_KEY_PATH = env.OBJ_PEM_PATH
const GET_TOKEN_URL = "https://idp.docapost.io/auth/realms/GETWAY/protocol/openid-connect/token"
const REFRESH_TOKEN_URL = GET_TOKEN_URL
const DEFAULT_BODY: GrantAccessRequestBody = {
grant_type: "client_credentials",
client_id: env.OBJ_CLIENT_ID,
client_assertion_type: "urn:ietf:params:oauth:client-assertion-type:jwt-bearer",
client_assertion: env.OBJ_CLI_ASSERTION
}
const DEFAULT_HEADERS = {
"content-type": "application/x-www-form-urlencoded"
}
const DEFAULT_HEADERS_JWT = {
alg: "RS256",
typ: "JWT",
kid: env.OBJ_KID,
}
const DEFAULT_DATA_JWT = {
sub: env.OBJ_CLIENT_ID,
iss: env.OBJ_CLIENT_ID,
aud: "https://idp.docapost.io/auth/realms/GETWAY",
jti: Date.now().toString(),
}
function addIATHeaders(authHeaders: Object) {
const headers = <JWTHeader>{
...authHeaders,
sub: env.OBJ_CLIENT_ID,
iss: env.OBJ_CLIENT_ID,
aud: GET_TOKEN_URL,
jti: Date.now().toString(),
iat: Math.floor(Date.now() / 1000),
exp: Math.floor(Date.now() / 1000) + 5 * 60,
}
return headers
}
export const jwtService = new JWTService({
transformJWTHeaders: addIATHeaders,
defaultHeaders: DEFAULT_HEADERS,
defaultBody: DEFAULT_BODY,
defaultJWTHeaders: DEFAULT_HEADERS_JWT,
defaultJWTPayload: DEFAULT_DATA_JWT,
privateKeyPath: PRIVATE_KEY_PATH,
tokenUrl: GET_TOKEN_URL,
refreshTokenUrl: REFRESH_TOKEN_URL
})

View File

@@ -8,6 +8,9 @@ import { OrderRepository } from "sim-shared/infrastructure/OrderRepository.js"
import { TaskVolcadoLineas } from "./tasks/volcado_lineas.js"
import { ObjeniousLinesRepository } from "./infranstructure/ObjeniousLinesRepository.js"
import { postgresClientIntranet } from "./config/intranetPostgresConfig.js"
import { PauseCancelTaskRepository } from "packages/sim-consumidor-objenious/infrastructure/PauseCancelTaskRepository.js"
import { PauseTerminateTask } from "./tasks/check_pause_terminate.js"
import { SimUseCases } from "packages/sim-consumidor-objenious/aplication/Sim.usecases.js"
async function startCron() {
const commonSettings = {
@@ -21,7 +24,10 @@ async function startCron() {
console.log("[i] Comprobando conexion con la BDD ")
await pgClient.checkDatabaseConnection()
const operationRepository = new ObjeniousOperationsRepository(pgClient)
const operationRepository = new ObjeniousOperationsRepository(
httpClient,
pgClient,
)
const orderRepository = new OrderRepository(pgClient)
const objeniousLineRepository = new ObjeniousLinesRepository(postgresClientIntranet)
@@ -31,7 +37,30 @@ async function startCron() {
httpClient,
)
const volcadoLineasTask = new TaskVolcadoLineas(httpClient, objeniousLineRepository)
const objeniosRepo = new ObjeniousOperationsRepository(
httpClient,
pgClient
)
const volcadoLineasTask = new TaskVolcadoLineas(
objeniousLineRepository,
objeniosRepo
)
const pauseRepo = new PauseCancelTaskRepository(pgClient)
const simUsecases = new SimUseCases({
httpClient: httpClient,
operationRepository: operationRepository,
orderRepository: orderRepository,
pauseRepository: pauseRepo
})
const pauseTask = new PauseTerminateTask(
objeniosRepo,
pauseRepo,
simUsecases,
orderRepository
)
const PERIODO_PETICIONES = 10 * 60 * 1000
const interval = setInterval(async () => {
@@ -51,7 +80,12 @@ async function startCron() {
}
}, PERIODO_VOLCADO)
await volcadoLineasTask.loadLines()
await pauseTask.run()
const PERIODO_CANCELACIONES = 60 * 60 * 1000;
const clacelacionesInterval = setInterval(async () => {
await pauseTask.run()
}, PERIODO_CANCELACIONES)
}

View File

@@ -3,10 +3,11 @@ import { OrderRepository } from "sim-shared/infrastructure/OrderRepository.js";
import axios from "axios";
import { IOperationsRepository, Objenious, ObjeniousOperation, ObjeniousOperationChange, StatusEnum } from "sim-shared/domain/operationsRepository.port.js";
import { HttpClient } from "sim-shared/infrastructure/HTTPClient.js";
import { ObjeniousOperationsRepository } from "packages/sim-shared/infrastructure/ObjeniousOperationRepository.js";
export class CheckObjeniousRequests {
constructor(
private readonly operationsRepository: IOperationsRepository,
private readonly operationsRepository: ObjeniousOperationsRepository,
private readonly orderRepository: OrderRepository,
private readonly httpClient: HttpClient
) {

View File

@@ -0,0 +1,189 @@
import { ObjeniousLine } from "sim-shared/domain/objeniousLine.js";
import { PauseCancelTaskRepository } from "sim-consumidor-objenious/infrastructure/PauseCancelTaskRepository.js";
import { ObjeniousOperationsRepository } from "sim-shared/infrastructure/ObjeniousOperationRepository.js";
import { SimUseCases } from "sim-consumidor-objenious/aplication/Sim.usecases.js";
import { OrderRepository } from "packages/sim-shared/infrastructure/OrderRepository.js";
const logger =
{
log: (...data: any[]) => console.log("[i] [TaskPauseTerminate]", ...data),
error: (...data: any[]) => console.error("[x] [TaskPauseTerminate] ", ...data),
}
export class PauseTerminateTask {
constructor(
private readonly objeniousRepo: ObjeniousOperationsRepository,
private readonly pauseRepo: PauseCancelTaskRepository,
private readonly simUsecases: SimUseCases,
private readonly orderRepo: OrderRepository
) {
}
public async run() {
const finError = (err: any) => {
logger.error("Finalizado con errores proceso de comprobacion de lineas en pausa o canceladas")
logger.error(err)
}
const finExito = () => {
logger.log("Finalizado con exito proceso de comprobacion de lineas en pausa o canceladas")
}
try {
logger.log("Iniciando proceso de comprobacion de lineas en pausa o canceladas")
// 1. Se comprueba cuantas peticiones hay qye revisar
const peticionesRevisar = await this.pauseRepo.getPending()
if (peticionesRevisar.error != undefined) {
finError(peticionesRevisar.error)
return 1;
}
logger.log(`Se van a revisar ${peticionesRevisar.data?.length} peticiones`)
if (peticionesRevisar.data == undefined || peticionesRevisar.data.length == 0) {
finExito()
return 0;
}
// 2. Se comprueba que alguna de las lineas haya dejado de estar en estado de test
const iccids = peticionesRevisar.data.map(e => e.iccid)
const lineasActualizadas: ObjeniousLine[] = []
const lineGenerator = this.objeniousRepo.getLinesByStatusAPI({
iccids: iccids
})
let lines = await lineGenerator.next()
if (lines.value.error != undefined || lines.value.data == undefined) {
logger.error("Error cargando las lineas", lines.value.error)
finError(lines.value.error)
return 1;
} else {
lineasActualizadas.push(...lines.value.data)
}
while (!lines.done) {
if (lines.value.error != undefined || lines.value.data == undefined) {
logger.error("Error cargando las lineas", lines.value.error)
finError(lines.value.error)
return 1;
} else {
lineasActualizadas.push(...lines.value.data)
}
lines = await lineGenerator.next()
}
console.log("Cargado: ", lineasActualizadas)
// 3. Se separan las lineas que se tienen que actualizar al no ser test
// y las que se tienen que reencolar al ser test
const lineasNoTest = lineasActualizadas.filter(e => e.status.billingStatus != "TEST")
const lineasTest = lineasActualizadas.filter(e => e.status.billingStatus == "TEST")
// 4. Las lineas de test se reencolan
// El proximo reintento es en 1 dia
const proximoReintento = new Date()
proximoReintento.setDate(new Date().getDate() + 1)
// 5. Reintentos en 1 dia
for (const linea of lineasTest) {
const lineaId = peticionesRevisar.data
.find(e => e.iccid == linea.identifier.iccid)?.id
if (lineaId == undefined) continue; // Esto puede ser un problema si se generaliza
this.pauseRepo.updateTask({
id: lineaId,
next_check: proximoReintento
})
}
// 6. Operaciones de pausa/cancelacion definitiva
for (const linea of lineasNoTest) {
const operacion = peticionesRevisar.data
.find(e => e.iccid == linea.identifier.iccid)
if (operacion == undefined) continue;
const dueDate = new Date()
dueDate.setMinutes(new Date().getMinutes() + 15)
const operacionTipo = operacion.operation_type
const actionData = operacion.action_data
const correlation_id = operacion.action_data.correlation_id
actionData.dueDate = dueDate.toISOString()
switch (linea.status.billingStatus) {
case "ACTIVATED":
let exito = false;
let result = null;
// IMPORTANTE COMRPOBAR EL DUE DATE
switch (operacionTipo) {
case "suspend":
result = await this.simUsecases.suspend(actionData)()
break;
case "terminate":
result = await this.simUsecases.terminate(actionData)()
break;
default:
break;
}
if (result == undefined) {
logger.error("Operacion desconocida", operacion)
} else if (result?.error != undefined) {
// error usecase
logger.error(result.error)
await this.pauseRepo.finishTask({
id: operacion.id,
error: result.error
})
if (correlation_id != undefined)
await this.orderRepo.errorOrder({
correlation_id: correlation_id,
status: "dlx",
reason: result.error
})
} else {
// ok
await this.pauseRepo.finishTask({ id: operacion.id })
if (correlation_id != undefined)
await this.orderRepo.finishOrder({ correlation_id })
}
break;
case "CANCELED":
await this.pauseRepo.finishTask({
id: operacion.id,
error: "billingStatus is CANCELED"
})
if (correlation_id != undefined)
await this.orderRepo.finishOrder({ correlation_id })
break;
case "SUSPENDED":
await this.pauseRepo.finishTask({
id: operacion.id,
error: "billingStatus is SUSPENDED"
})
if (correlation_id != undefined)
await this.orderRepo.finishOrder({ correlation_id })
break;
case "TEST":
// No puede ser
default:
logger.error("billingStatus desconocido", linea.status.billingStatus)
}
}
finExito()
} catch (e) {
finError(e)
}
return 0
}
}

View File

@@ -1,94 +1,14 @@
import assert from "node:assert";
import { lineToCreateLineDto, ObjeniousLine, ObjeniousLineResponse } from "sim-shared/domain/objeniousLine.js";
import { tryCatch, Result } from "sim-shared/domain/Result.js";
import { HttpClient } from "sim-shared/infrastructure/HTTPClient.js";
import { lineToCreateLineDto, ObjeniousLine } from "sim-shared/domain/objeniousLine.js";
import { ObjeniousLinesRepository } from "../infranstructure/ObjeniousLinesRepository.js";
import { AxiosResponse } from "axios";
import { constants } from "node:buffer";
const MAX_PAGE_SIZE = 100
import { ObjeniousOperationsRepository } from "sim-shared/infrastructure/ObjeniousOperationRepository.js";
export class TaskVolcadoLineas {
constructor(
private readonly httpClient: HttpClient,
private readonly linesRepository: ObjeniousLinesRepository,
private readonly objeniousRepository: ObjeniousOperationsRepository
) {
}
/**
* Mover al repo
*/
private async * getLinesByStatus(args?: {
pageSize?: number,
pageNumber?: number,
status?: string
}): AsyncGenerator<Result<string, ObjeniousLine[]>, Result<string, ObjeniousLine[]>, any> {
const path = "/lines"
const pageSize = args?.pageSize ?? MAX_PAGE_SIZE;
let currentPage = args?.pageNumber ?? 0;
let totalPages: number | undefined = undefined; // Como limite de paginas, igual es pasarse pero hasta que se lea
const params: Record<string, string | number> = {}
const loadNextLine = async (page: number): Promise<Result<string, ObjeniousLine[]>> => {
if (args?.status != undefined) params["simStatus"] = args.status
params["pageSize"] = pageSize
params["pageNumber"] = page
console.log("Params", params)
console.log(`[i] Cargando pagina ${currentPage} de ${totalPages ?? "(desc)"}`)
const nextPage = await tryCatch<AxiosResponse<ObjeniousLineResponse>>(this.httpClient.client.get(path, {
params: params
}))
if (nextPage.error != undefined) {
console.error(nextPage.error.msg)
return {
error: nextPage.error.msg.message
}
}
// Se aumenta para la siguiente ejecucion
console.log(`[i] Página ${currentPage} completa, total: ${nextPage.data.data.totalPages}`)
totalPages = nextPage.data.data.totalPages
return {
data: nextPage.data.data.content
}
}
// El inicio se ejecuta siempre
const lines = await loadNextLine(currentPage)
if (lines.error != undefined) {
console.error("[x] Error obteniendo las lineas, cancelando operación");
return {
error: "Error cargando lineas"
}
}
currentPage++;
yield {
data: lines.data
}
// Copia para evitar bucles infinitos por error de la api
const maxPages = totalPages
assert.ok(maxPages != undefined, "No se ha defindo el numero de paginas") // Nunca deberia pasar pero así se evitan bucles infnitos
console.log("maxPages", maxPages)
for (let i = currentPage; i < maxPages!; i++) {
console.log("Bucle i:", i, "page: ", currentPage)
yield await loadNextLine(currentPage);
currentPage++;
}
return {
data: []
}
}
private async saveLines(lines: ObjeniousLine[]) {
const linesToCreate = lines.map(lineToCreateLineDto)
@@ -107,7 +27,9 @@ export class TaskVolcadoLineas {
console.log("[i] Iniciando task de volcado de lineas de Objenious")
// Carga todas las lineas en memoria, hay que comprobar que no se gaste demasiada
const linesIterator = this.getLinesByStatus()
const linesIterator = this.objeniousRepository.getLinesByStatusAPI({
pageSize: 100
})
let lines = await linesIterator.next()
if (lines.value.error != undefined || lines.value.data == undefined) {
@@ -118,7 +40,6 @@ export class TaskVolcadoLineas {
await this.saveLines(lines.value.data)
while (!lines.done) {
console.log()
lines = await linesIterator.next()
if (lines.value.error != undefined || lines.value.data == undefined) {
console.error("[x] Error cargando las lineas a volcar", lines.value.error)

View File

@@ -1,16 +1,16 @@
import { test, describe } from "vitest"
import { JWTService } from "./JWT.service.js"
import { jwtService } from "../config/jwtService.config.js"
describe("Tokens Objenious", () => {
const jwtService = new JWTService()
const jwt = jwtService
test("Solicicitud normal de auth", async () => {
const token = await jwtService.getAccessToken()
const token = await jwt.getAccessToken()
console.log("acceso objenious", token)
}),
test("Solicicitud de refresh de auth", async () => {
const token = await jwtService.tryRefreshToken()
const token = await jwt.tryRefreshToken()
console.log("acceso refresh objenious", token)
})
})

View File

@@ -4,24 +4,24 @@
* el cliente HTTP
*/
import { env } from "#config/env/index.js";
import fs from "fs"
import {
JWTToken,
JWTHeader,
IJWTService
IJWTService,
JWTPayload
} from "sim-shared/domain/JWT.js"
import axios, { AxiosError } from "axios";
type GrantAccessRequestBody = {
export type GrantAccessRequestBody = {
grant_type: string,
client_id: string,
client_assertion_type: string,
client_assertion: string
}
type TokensRequestResponse = {
export type TokensRequestResponse = {
"access_token": string,
"expires_in": number,
"refresh_token": string
@@ -32,41 +32,6 @@ type TokensRequestResponse = {
"scope": string
}
const PRIVATE_KEY_PATH = env.OBJ_PEM_PATH
const GET_TOKEN_URL = "https://idp.docapost.io/auth/realms/GETWAY/protocol/openid-connect/token"
const REFRESH_TOKEN_URL = GET_TOKEN_URL
const DEFAULT_BODY: GrantAccessRequestBody = {
grant_type: "client_credentials",
client_id: env.OBJ_CLIENT_ID,
client_assertion_type: "urn:ietf:params:oauth:client-assertion-type:jwt-bearer",
client_assertion: env.OBJ_CLI_ASSERTION
}
const REFRESH_BODY = {
...DEFAULT_BODY,
grant_type: "refresh_token",
}
const DEFAULT_HEADERS = {
"content-type": "application/x-www-form-urlencoded"
}
function addIATHeaders(authHeaders: Object) {
const headers = <JWTHeader>{
...authHeaders,
sub: env.OBJ_CLIENT_ID,
iss: env.OBJ_CLIENT_ID,
aud: GET_TOKEN_URL,
jti: Date.now().toString(),
iat: Math.floor(Date.now() / 1000),
exp: Math.floor(Date.now() / 1000) + 5 * 60,
}
return headers
}
export type ObjeniousTokenBody = any
/**
@@ -82,27 +47,54 @@ export class JWTService implements IJWTService<ObjeniousTokenBody> {
public authToken: JWTToken<ObjeniousTokenBody> | undefined;
private refreshToken?: JWTToken<ObjeniousTokenBody> | undefined;
constructor(args?: {
// http
private transformHeaders?: (_: Object) => JWTHeader;
private defaultHttpHeaders: Record<string, string>;
private defaultBody: Record<string, string>;
// jwt
private defaultJWTHeaders: JWTHeader;
private defaultJWTPayload: JWTPayload<any>;
private privateKeyPath: string;
private tokenUrl: string;
private refreshTokenUrl: string;
constructor(args: {
token?: string // si se partiese de un token existente,
refreshToken?: string
refreshToken?: string,
transformJWTHeaders?: (_: Object) => JWTHeader,
defaultHeaders: Record<string, string>,
defaultBody: Record<string, string>,
defaultJWTHeaders: JWTHeader,
defaultJWTPayload: JWTPayload<any>,
privateKeyPath: string,
tokenUrl: string,
refreshTokenUrl: string
}) {
if (args?.token != undefined) this.authToken = new JWTToken(args.token)
if (args?.refreshToken != undefined) this.refreshToken = new JWTToken(args.refreshToken)
if (args?.transformJWTHeaders != undefined) this.transformHeaders = args.transformJWTHeaders
this.defaultHttpHeaders = args.defaultHeaders;
this.defaultBody = args.defaultBody;
this.defaultJWTHeaders = args.defaultJWTHeaders;
this.defaultJWTPayload = args.defaultJWTPayload;
this.privateKeyPath = args.privateKeyPath;
this.tokenUrl = args.tokenUrl;
this.refreshTokenUrl = args.refreshTokenUrl;
}
private buildJwtBody() {
const jwtHeaders = {
alg: "RS256",
typ: "JWT",
kid: env.OBJ_KID
}
const jwtData = addIATHeaders({
sub: env.OBJ_CLIENT_ID,
iss: env.OBJ_CLIENT_ID,
aud: "https://idp.docapost.io/auth/realms/GETWAY",
jti: Date.now().toString(),
})
const key = fs.readFileSync(PRIVATE_KEY_PATH, "utf8")
const jwtHeaders = this.defaultJWTHeaders
const jwtData = (this.transformHeaders) ?
this.transformHeaders(this.defaultJWTPayload) :
this.defaultJWTPayload;
const key = fs.readFileSync(this.privateKeyPath, "utf8")
const token = JWTToken.fromParts({
header: jwtHeaders,
payload: jwtData,
@@ -116,14 +108,16 @@ export class JWTService implements IJWTService<ObjeniousTokenBody> {
public async getNewAuthToken() {
const bodyWithtoken = {
...DEFAULT_BODY,
...this.defaultBody,
client_assertion: this.buildJwtBody()
}
const req = axios.post(GET_TOKEN_URL,
const headers = (this.transformHeaders) ? this.transformHeaders(this.defaultHttpHeaders) : this.defaultHttpHeaders;
const req = axios.post(this.tokenUrl,
bodyWithtoken,
{
headers: addIATHeaders(DEFAULT_HEADERS)
headers: headers
}
)
@@ -166,16 +160,21 @@ export class JWTService implements IJWTService<ObjeniousTokenBody> {
if (this.refreshToken == undefined) throw new Error("El refreshToken no está definido")
if (this.refreshToken.isExpired()) throw new Error("El refreshToken ha expirado")
const refreshBody = {
...this.defaultBody,
grant_type: "refresh_token",
}
const body = {
...REFRESH_BODY,
...refreshBody,
client_assertion: this.buildJwtBody(),
refresh_token: this.refreshToken.rawToken
}
const req = axios.post(REFRESH_TOKEN_URL,
const req = axios.post(this.refreshTokenUrl,
body,
{
headers: DEFAULT_HEADERS
headers: this.defaultHttpHeaders
}
)

View File

@@ -7,9 +7,12 @@
import { env, loadEnvFile } from "node:process";
import { Pool } from "pg";
import { PgClient } from "../infrastructure/PgClient.js";
import { HttpClient } from "../infrastructure/HTTPClient.js";
import { jwtService } from "./jwtService.config.js";
console.warn("[i!] Se está corriendo codigo de test")
loadEnvFile("../../.env") // Global
loadEnvFile("./test.env") // Local
// se hace una por servicio.
export const pgPool = new Pool({
@@ -24,4 +27,14 @@ export const postgresClient = new PgClient({
pool: pgPool
})
const OBJ_BASE_URL = "https://api-getway.objenious.com/ws"
export const httpObjClient = new HttpClient({
baseURL: OBJ_BASE_URL,
headers: {
"content-type": " application/json; charset=utf-8"
},
jwtManager: jwtService
})
console.warn(`[T] TEST DB : ${env.POSTGRES_DATABASE}@${env.POSTGRES_HOST}`)

View File

@@ -0,0 +1,67 @@
import assert from "assert"
import { env, loadEnvFile } from "process"
import { GrantAccessRequestBody, JWTService } from "sim-shared/aplication/JWT.service.js"
import { JWTHeader } from "sim-shared/domain/JWT.js"
loadEnvFile("../../.env") // Global
loadEnvFile("./test.env") // Local
assert(env.OBJ_CLIENT_ID != undefined)
assert(env.OBJ_CLI_ASSERTION != undefined)
assert(env.OBJ_PEM_PATH != undefined)
const PRIVATE_KEY_PATH = env.OBJ_PEM_PATH
const GET_TOKEN_URL = "https://idp.docapost.io/auth/realms/GETWAY/protocol/openid-connect/token"
const REFRESH_TOKEN_URL = GET_TOKEN_URL
const DEFAULT_BODY: GrantAccessRequestBody = {
grant_type: "client_credentials",
client_id: env.OBJ_CLIENT_ID,
client_assertion_type: "urn:ietf:params:oauth:client-assertion-type:jwt-bearer",
client_assertion: env.OBJ_CLI_ASSERTION
}
const DEFAULT_HEADERS = {
"content-type": "application/x-www-form-urlencoded"
}
const DEFAULT_HEADERS_JWT = {
alg: "RS256",
typ: "JWT",
kid: env.OBJ_KID,
}
const DEFAULT_DATA_JWT = {
sub: env.OBJ_CLIENT_ID,
iss: env.OBJ_CLIENT_ID,
aud: "https://idp.docapost.io/auth/realms/GETWAY",
jti: Date.now().toString(),
}
function addIATHeaders(authHeaders: Object) {
const headers = <JWTHeader>{
...authHeaders,
sub: env.OBJ_CLIENT_ID,
iss: env.OBJ_CLIENT_ID,
aud: GET_TOKEN_URL,
jti: Date.now().toString(),
iat: Math.floor(Date.now() / 1000),
exp: Math.floor(Date.now() / 1000) + 5 * 60,
}
return headers
}
export const jwtService = new JWTService({
transformJWTHeaders: addIATHeaders,
defaultHeaders: DEFAULT_HEADERS,
defaultBody: DEFAULT_BODY,
defaultJWTHeaders: DEFAULT_HEADERS_JWT,
defaultJWTPayload: DEFAULT_DATA_JWT,
privateKeyPath: PRIVATE_KEY_PATH,
tokenUrl: GET_TOKEN_URL,
refreshTokenUrl: REFRESH_TOKEN_URL
})

View File

@@ -14,7 +14,7 @@ export type Failure<E = Error> = {
*/
export type Result<E, D> = Failure<E> | Success<D>
export async function tryCatch<T>(func: Promise<T>): Promise<Result<{ msg: Error }, T>> {
export async function tryCatch<T>(func: Promise<T>): Promise<Result<Error, T>> {
try {
const res = await func;
return {
@@ -22,9 +22,8 @@ export async function tryCatch<T>(func: Promise<T>): Promise<Result<{ msg: Error
}
} catch (e: unknown) {
return {
error: {
msg: e as Error
}
error: e as Error
}
}
}

View File

@@ -83,7 +83,7 @@ export type ObjeniousLine = {
commercialStatus: string, //"test",
commercialStatusDate: string, //"2026-03-17T11:41:01.493+00:00",
networkStatus: string, // "ACTIVATED",
billingStatus: string, //"TEST",
billingStatus: "ACTIVATED" | "SUSPENDED" | "CANCELED" | "TEST",
billingStatusChangeDate: string | null, // "2026-03-17T11:01:00.276+00:00",
billingActivationDate: string | null //,
createdDate: string | null,//"2026-01-30T01:50:02.060+00:00"

View File

@@ -0,0 +1,14 @@
import { describe, it } from "node:test";
import { ObjeniousOperationsRepository } from "./ObjeniousOperationRepository.js";
import { httpObjClient, postgresClient } from "../config/config.test.js";
describe("[Integration] Test API requests", () => {
const repository = new ObjeniousOperationsRepository(
httpObjClient,
postgresClient
)
it("Read /lines with multiple iccids", () => {
})
})

View File

@@ -1,14 +1,139 @@
import { IOperationsRepository, ObjeniousOperation, ObjeniousOperationChange } from "sim-shared/domain/operationsRepository.port.js";
import { Result } from "sim-shared/domain/Result.js";
import { Result, tryCatch } from "sim-shared/domain/Result.js";
import { PgClient } from "sim-shared/infrastructure/PgClient.js";
import { ObjeniousLine, ObjeniousLineResponse } from "../domain/objeniousLine.js";
import { HttpClient } from "./HTTPClient.js";
import assert from "node:assert";
import { AxiosResponse } from "axios";
export class ObjeniousOperationsRepository implements IOperationsRepository {
constructor(
private http: HttpClient,
private readonly pgClient: PgClient
) {
}
/**
* Consulta el estado de una o mas lineas directamente a la API de Objenious
* TODO: No hay paginacion como en getLinesByStatusAPI
*/
public async getLinesAPI(
identifierType: "ICCID" | "IMSI" | "IMEI" | "MSISDN" | "REFERENCE",
identifiers: string[]
): Promise<Result<string, ObjeniousLine[]>> {
if (identifiers.length == 0) {
return {
data: []
}
}
// Comprobar < MAX_PAGE_SIZE (Poco probable)
const path = "/lines"
const params = {
"identifier.identifierType": identifierType,
"identifier.identifiers": identifiers.toString()
}
const req = this.http.client.get<ObjeniousLineResponse>(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
}
}
private MAX_PAGE_SIZE = 1000
public async * getLinesByStatusAPI(args?: {
pageSize?: number,
pageNumber?: number,
status?: string,
iccids?: string[]
}): AsyncGenerator<Result<string, ObjeniousLine[]>, Result<string, ObjeniousLine[]>, any> {
const path = "/lines"
const pageSize = args?.pageSize ?? this.MAX_PAGE_SIZE;
let currentPage = args?.pageNumber ?? 0;
let totalPages: number | undefined = undefined; // Como limite de paginas, igual es pasarse pero hasta que se lea
const params: Record<string, string | number> = {}
// Si se va a filtrar por iccids especificamente, en un futuro habra que ampliar el tipo de filtros
if (args?.iccids != undefined) {
params["identifier.identifierType"] = "ICCID"
params["identifier.identifiers"] = args.iccids.toString()
}
const loadNextLine = async (page: number): Promise<Result<string, ObjeniousLine[]>> => {
if (args?.status != undefined) params["simStatus"] = args.status
params["pageSize"] = pageSize
params["pageNumber"] = page
console.log(`[i] Cargando pagina ${currentPage} de ${totalPages ?? "(desc)"}`)
const nextPage = await tryCatch<AxiosResponse<ObjeniousLineResponse>>(this.http.client.get(path, {
params: params
}))
if (nextPage.error != undefined) {
console.error(nextPage.error)
return {
error: nextPage.error.message
}
}
// Se aumenta para la siguiente ejecucion
console.log(`[i] Página ${currentPage} completa, total: ${nextPage.data.data.totalPages}`)
totalPages = nextPage.data.data.totalPages
return {
data: nextPage.data.data.content
}
}
// El inicio se ejecuta siempre
const lines = await loadNextLine(currentPage)
if (lines.error != undefined) {
console.error("[x] Error obteniendo las lineas, cancelando operación");
return {
error: "Error cargando lineas"
}
}
currentPage++;
yield {
data: lines.data
}
// Copia para evitar bucles infinitos por error de la api
const maxPages = totalPages
assert.ok(maxPages != undefined, "No se ha defindo el numero de paginas") // Nunca deberia pasar pero así se evitan bucles infnitos
console.log("maxPages", maxPages)
for (let i = currentPage; i < maxPages!; i++) {
console.log("Bucle i:", i, "page: ", currentPage)
yield await loadNextLine(currentPage);
currentPage++;
}
return {
data: []
}
}
async createOperation(data: ObjeniousOperation): Promise<Result<string, ObjeniousOperation>> {
const query = `
INSERT INTO objenious_operation (operation, iccids, status, max_retry, request_id)
@@ -46,7 +171,7 @@ export class ObjeniousOperationsRepository implements IOperationsRepository {
request_id = COALESCE($4, request_id),
mass_action_id = COALESCE($5, mass_action_id),
last_change_date = now() at time zone 'utc',
end_date = CASE WHEN $2 IN ('finished') THEN now() at time zone 'utc' ELSE end_date END,
end_date = CASE WHEN $2 IN ('finished','error') THEN now() at time zone 'utc' ELSE end_date END,
objenious_status = $6
WHERE id = $1`;

View File

@@ -27,7 +27,7 @@ describe("Test OrderRepository", {}, (ctx) => {
before(async () => {
// Order1
const result1 = await orderRepo.createOrder(order1)
assert(result1.data != undefined)
assert.ok(result1.data != undefined, result1.error as string)
testIds.push(result1.data.id)
// Order2 -> Para el test de crearOrder

View File

@@ -3,10 +3,9 @@
*/
import { PoolClient, QueryResult, QueryResultRow } from "pg";
import { CreateOrderDTO, ErrorOrderDTO, FinishOrderDTO, OrderTracking, UpdateOrderDTO } from "../domain/Order.js";
import { Result } from "../domain/Result.js";
import { Result, tryCatch } from "../domain/Result.js";
import { PgClient } from "./PgClient.js";
import assert from "node:assert";
import { error } from "node:console";
/**
* Agrupa todas las operaciones de *Order*.
@@ -19,9 +18,8 @@ import { error } from "node:console";
*/
export class OrderRepository {
constructor(
private readonly pgClient: PgClient
private readonly pgClient: PgClient,
) {
}
/**
@@ -57,6 +55,8 @@ export class OrderRepository {
}
}
/**
* El tipo <T> representa el contenido del mensaje de los order
*/
@@ -191,6 +191,8 @@ export class OrderRepository {
const orderId = currentOrderResult.data?.id
if (orderId == undefined) {
await client.query("ROLLBACK")
client.release()
return {
error: "El order a actualizar no existe " + idType + ": " + idValue
}
@@ -261,7 +263,6 @@ export class OrderRepository {
return updatedOrder
}
public async finishOrder(args: FinishOrderDTO) {
const client = await this.pgClient.connect();
assert((args.id != undefined) != (args.correlation_id != undefined))
@@ -281,6 +282,8 @@ export class OrderRepository {
const orderId = currentOrderResult.data?.id
if (orderId == undefined) {
await client.query("ROLLBACK")
client.release()
return {
error: "El order a actualizar no existe " + idType + ": " + idValue
}

View File

@@ -0,0 +1,14 @@
## ENV PARA DATOS DE TEST - shared nunca se lanza en produccion
# claves de Objenious
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_BASE_URL=https://api-getway.objenious.com/ws/test
NOTIFICATION_URL="https://sf-sim-activation.savefamilygps.net/send-activation-mail"
# NOTIFICATION_URL="localhost"
SIM_ACTIVATION_API_KEY=9e48c4ac-1ab0-4397-b3f3-6c239200dfe6