Merge main -> migracion alai
This commit is contained in:
14
packages/sim-shared/domain/PaginationArgs.ts
Normal file
14
packages/sim-shared/domain/PaginationArgs.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
/**
|
||||
* Los parametros según entran por la query pueden ser de tipo string
|
||||
*/
|
||||
export type QueryPaginationArgs = {
|
||||
limit?: string,
|
||||
offset?: string,
|
||||
start?: string// start = offset; Por compatibilidad, normalmente solo limit y offset
|
||||
}
|
||||
|
||||
export type PaginationArgs = {
|
||||
limit?: number,
|
||||
offset?: number,
|
||||
start?: number // start = offset; Por compatibilidad, normalmente solo limit y offset
|
||||
}
|
||||
@@ -6,6 +6,7 @@ export interface IOperationsRepository {
|
||||
createOperation(data: ObjeniousOperation): Promise<Result<string, ObjeniousOperation>>
|
||||
updateOperation(data: ObjeniousOperationChange): Promise<Result<string, ObjeniousOperation>>
|
||||
getPendingOperations(): Promise<Result<string, ObjeniousOperation[]>>
|
||||
getSuspendedTime(iccid: string): Promise<Result<string, { total_milliseconds: number, total_days: number }>>
|
||||
}
|
||||
|
||||
export type ObjeniousOperation = {
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
import test, { after, before, describe } from "node:test";
|
||||
import { CreateObjeniousLineDTO } from "sim-shared/domain/objeniousLine.js";
|
||||
import { ObjeniousLinesRepository } from "sim-shared/infrastructure/ObjeniousLinesRepository.js";
|
||||
import assert from "node:assert";
|
||||
import { postgresClient } from "../config/config.test.js";
|
||||
|
||||
describe("Line insertion test", async () => {
|
||||
//const pgClient = postgreClientIntranet
|
||||
const pgClient = postgresClient// En prod hay que usar el de Intrantet para usar la otra base de datos
|
||||
const lineRepository = new ObjeniousLinesRepository(pgClient)
|
||||
const lineaTest: CreateObjeniousLineDTO = {
|
||||
simId: 1234,
|
||||
iccid: "9999999999999",
|
||||
msisdn: "34654674732",
|
||||
imei: "219789481293",
|
||||
imeiChangeDate: new Date(),
|
||||
offerCode: "SAVEFAMILY1",
|
||||
status: "ACTIVATED",
|
||||
preactivationDate: new Date(),
|
||||
activationDate: new Date(),
|
||||
commercialStatus: "test",
|
||||
commercialStatusDate: new Date(),
|
||||
billingStatus: "test",
|
||||
billingStatusChangeDate: new Date(),
|
||||
billingActivationDate: new Date(),
|
||||
createDate: new Date(),
|
||||
raw: { test: "test" } as any // Para este test no hace falta
|
||||
}
|
||||
|
||||
// Clean up before and after tests to ensure isolation
|
||||
const cleanup = async () => {
|
||||
await pgClient.query("DELETE FROM objenious_lines WHERE simId = 1234");
|
||||
};
|
||||
|
||||
before(async () => {
|
||||
await cleanup()
|
||||
})
|
||||
|
||||
after(async () => {
|
||||
await cleanup()
|
||||
})
|
||||
|
||||
test("Should insert new line", async () => {
|
||||
const res = await lineRepository.insertOrUpdate(lineaTest)
|
||||
assert.ok(res != undefined, "The line wasn't created")
|
||||
})
|
||||
|
||||
test("Should not update a line if the hash is the same", async () => {
|
||||
const res = await lineRepository.insertOrUpdate(lineaTest)
|
||||
assert.ok(res == undefined, "The line have been updated")
|
||||
})
|
||||
|
||||
test("Should update a line if the hash changes", async () => {
|
||||
const updated = structuredClone(lineaTest)
|
||||
lineaTest.billingActivationDate = new Date()
|
||||
const res = await lineRepository.insertOrUpdate(lineaTest)
|
||||
assert.ok(res != undefined, "The line have been updated")
|
||||
})
|
||||
})
|
||||
164
packages/sim-shared/infrastructure/ObjeniousLinesRepository.ts
Normal file
164
packages/sim-shared/infrastructure/ObjeniousLinesRepository.ts
Normal file
@@ -0,0 +1,164 @@
|
||||
/**
|
||||
* Repositorio para el volcado de lineas de objenious en intranet
|
||||
* solo para uso en el volcado.
|
||||
*/
|
||||
import { createHash } from "node:crypto";
|
||||
import { PoolClient } from "pg";
|
||||
import { CreateObjeniousLineDTO, ObjeniousLineDb } from "sim-shared/domain/objeniousLine.js";
|
||||
import { PgClient } from "sim-shared/infrastructure/PgClient.js";
|
||||
|
||||
export class ObjeniousLinesRepository {
|
||||
constructor(
|
||||
private pgClient: PgClient
|
||||
) {
|
||||
}
|
||||
|
||||
private generateLineHash(data: CreateObjeniousLineDTO) {
|
||||
try {
|
||||
const lineStr = JSON.stringify(data)
|
||||
const hash = createHash("sha256").update(lineStr).digest("base64url")
|
||||
return hash
|
||||
} catch (e) {
|
||||
console.error("[x] Error generando el hash de la linea", data)
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Hay que hacer la query un poco mas general
|
||||
*/
|
||||
public async getLinesByStatus(query: { status?: string | undefined }, pagination: { limit: number, offset: number }) {
|
||||
// $1 y $2 se reservan para paginación
|
||||
|
||||
const values: (string | number)[] = [
|
||||
pagination.limit,
|
||||
pagination.offset,
|
||||
]
|
||||
const paginationStr = `
|
||||
LIMIT $1
|
||||
OFFSET $2
|
||||
`
|
||||
|
||||
const conditionsStr = `
|
||||
WHERE
|
||||
raw -> 'status' ->> 'networkStatus' = $3
|
||||
`
|
||||
|
||||
let queryStr = `
|
||||
SELECT * FROM objenious_lines
|
||||
`
|
||||
if (query.status != undefined) {
|
||||
queryStr = queryStr + conditionsStr
|
||||
values.push(query.status)
|
||||
|
||||
}
|
||||
|
||||
queryStr = queryStr + `
|
||||
${paginationStr};
|
||||
`
|
||||
|
||||
let client: PoolClient | undefined = undefined;
|
||||
try {
|
||||
client = await this.pgClient.connect();
|
||||
const res = await client.query<ObjeniousLineDb>(queryStr, values);
|
||||
return {
|
||||
data: res.rows,
|
||||
offset: pagination.offset,
|
||||
rowCount: res.rowCount ?? 0,
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error en la query:', err);
|
||||
throw err;
|
||||
} finally {
|
||||
if (client != undefined) {
|
||||
client.release()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public async insertOrUpdate(data: CreateObjeniousLineDTO) {
|
||||
const query = `
|
||||
INSERT INTO objenious_lines (
|
||||
simId,
|
||||
iccid,
|
||||
msisdn,
|
||||
imei,
|
||||
imeiChangeDate,
|
||||
offerCode,
|
||||
status,
|
||||
preactivationDate,
|
||||
activationDate,
|
||||
commercialStatus,
|
||||
commercialStatusDate,
|
||||
billingStatus,
|
||||
billingStatusChangeDate,
|
||||
billingActivationDate,
|
||||
createDate,
|
||||
raw,
|
||||
hash
|
||||
) VALUES (
|
||||
$1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17
|
||||
)
|
||||
ON CONFLICT (simId)
|
||||
DO UPDATE SET
|
||||
iccid = EXCLUDED.iccid,
|
||||
msisdn = EXCLUDED.msisdn,
|
||||
imei = EXCLUDED.imei,
|
||||
imeiChangeDate = EXCLUDED.imeiChangeDate,
|
||||
offerCode = EXCLUDED.offerCode,
|
||||
status = EXCLUDED.status,
|
||||
preactivationDate = EXCLUDED.preactivationDate,
|
||||
activationDate = EXCLUDED.activationDate,
|
||||
commercialStatus = EXCLUDED.commercialStatus,
|
||||
commercialStatusDate = EXCLUDED.commercialStatusDate,
|
||||
billingStatus = EXCLUDED.billingStatus,
|
||||
billingStatusChangeDate = EXCLUDED.billingStatusChangeDate,
|
||||
billingActivationDate = EXCLUDED.billingActivationDate,
|
||||
raw = EXCLUDED.raw,
|
||||
hash = EXCLUDED.hash
|
||||
WHERE objenious_lines.hash IS DISTINCT FROM EXCLUDED.hash
|
||||
RETURNING id;
|
||||
`;
|
||||
|
||||
const lineHash = this.generateLineHash(data)
|
||||
|
||||
if (lineHash == undefined) {
|
||||
console.error("[x] Ignorando linea ", data)
|
||||
return;
|
||||
}
|
||||
|
||||
const values = [
|
||||
data.simId,
|
||||
data.iccid,
|
||||
data.msisdn,
|
||||
data.imei,
|
||||
data.imeiChangeDate,
|
||||
data.offerCode,
|
||||
data.status,
|
||||
data.preactivationDate,
|
||||
data.activationDate,
|
||||
data.commercialStatus,
|
||||
data.commercialStatusDate,
|
||||
data.billingStatus,
|
||||
data.billingStatusChangeDate,
|
||||
data.billingActivationDate,
|
||||
data.createDate || new Date(), // Default a ahora si no viene
|
||||
JSON.stringify(data.raw), // El driver de pg requiere string o el objeto directo para JSONB
|
||||
lineHash
|
||||
];
|
||||
|
||||
let client: PoolClient | undefined = undefined;
|
||||
try {
|
||||
client = await this.pgClient.connect();
|
||||
const res = await client.query<{ id: number }>(query, values);
|
||||
return res.rows[0];
|
||||
} catch (err) {
|
||||
console.error('Error en la inserción:', err);
|
||||
throw err;
|
||||
} finally {
|
||||
if (client != undefined) {
|
||||
client.release()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { before, describe, it } from "node:test";
|
||||
import { before, after, describe, it } from "node:test";
|
||||
import { ObjeniousOperationsRepository } from "./ObjeniousOperationRepository.js";
|
||||
import { httpObjClient, postgresClient } from "../config/config.test.js";
|
||||
import { ObjeniousOperation } from "../domain/operationsRepository.port.js";
|
||||
@@ -21,6 +21,7 @@ describe("[Integration] Test API requests", () => {
|
||||
httpObjClient,
|
||||
postgresClient
|
||||
)
|
||||
const suspend_iccid = "test_suspended_time_iccid";
|
||||
|
||||
before(async () => {
|
||||
await repository.createOperation(correctOperation)
|
||||
@@ -36,4 +37,40 @@ describe("[Integration] Test API requests", () => {
|
||||
* - Se ignoran las erroneas
|
||||
*/
|
||||
})
|
||||
|
||||
it("Calculates suspended time accurately", async () => {
|
||||
// Se crean registros con un iccid concocido
|
||||
await postgresClient.query(`
|
||||
INSERT INTO objenious_operation (operation, iccids, status, error, start_date, end_date) VALUES
|
||||
('suspend', $1, 'finished', NULL, NOW() - INTERVAL '3 days', NOW() - INTERVAL '3 days'),
|
||||
('reactivate', $1, 'finished', NULL, NOW() - INTERVAL '2 days', NOW() - INTERVAL '2 days'),
|
||||
('suspend', $1, 'finished', NULL, NOW() - INTERVAL '1 day', NOW() - INTERVAL '1 day');
|
||||
`, [suspend_iccid]);
|
||||
|
||||
const result = await repository.getSuspendedTime(suspend_iccid);
|
||||
|
||||
if (result.error) {
|
||||
throw new Error("Query returned an error: " + result.error);
|
||||
}
|
||||
|
||||
// Se esperan mas o menos 2 dias para cada periodo, total 4 (Puede cambiar un poco por zona horaria)
|
||||
// 2 dias en ms
|
||||
const expectedApproxMs = 2 * 24 * 60 * 60 * 1000;
|
||||
const msDiff = Math.abs(result.data!.total_milliseconds - expectedApproxMs);
|
||||
|
||||
// Margen de 5s
|
||||
if (msDiff > 5000) {
|
||||
throw new Error(`Expected approx ${expectedApproxMs} ms, got ${result.data!.total_milliseconds}`);
|
||||
}
|
||||
|
||||
// como se incluye el dia de sespension los dias van a variar de 2 a 3
|
||||
if (result.data!.total_days < 2) {
|
||||
throw new Error("total_days should be at least 2");
|
||||
}
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
// Eliminacion de los iccid de periodo de suspensiones
|
||||
await postgresClient.query(`DELETE FROM objenious_operation WHERE iccids = '${suspend_iccid}'`);
|
||||
});
|
||||
})
|
||||
|
||||
@@ -231,4 +231,66 @@ export class ObjeniousOperationsRepository implements IOperationsRepository {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtiene el tiempo en suspensión de una linea en miliseguntos y dias efetivos
|
||||
* Todo el calculo se hace en postgres. Puede que haga falta traer las transiciones
|
||||
* que normalmente son pocas, para hacer filtros personalizados.
|
||||
*/
|
||||
async getSuspendedTime(iccid: string): Promise<Result<string, { total_milliseconds: number, total_days: number }>> {
|
||||
const query = `
|
||||
WITH ordered_events AS (
|
||||
-- 1. Selecciona y normaliza los eventos relevantes del historial
|
||||
-- Se define el 'estado' final (suspendido vs activo) basado en la operación
|
||||
SELECT operation, end_date,
|
||||
CASE WHEN operation = 'suspend' THEN 'suspended' ELSE 'active' END as state
|
||||
FROM objenious_operation
|
||||
WHERE iccids = $1 AND status = 'finished' AND error IS NULL
|
||||
AND operation IN ('suspend', 'activate', 'reactivate', 'terminate')
|
||||
ORDER BY end_date
|
||||
),
|
||||
state_transitions AS (
|
||||
-- 2. Detecta cambios de estado comparando con la fila anterior (LAG)
|
||||
SELECT state, end_date,
|
||||
LAG(state) OVER (ORDER BY end_date) as prev_state
|
||||
FROM ordered_events
|
||||
),
|
||||
filtered_transitions AS (
|
||||
-- 3. Filtra solo las filas donde el estado realmente ha cambaido
|
||||
-- Se obtiene la fecha de inicio del siguiente intervalo (LEAD)
|
||||
SELECT state, end_date,
|
||||
LEAD(end_date) OVER (ORDER BY end_date) as next_end_date
|
||||
FROM state_transitions
|
||||
WHERE state IS DISTINCT FROM prev_state
|
||||
),
|
||||
intervals AS (
|
||||
-- 4. Calcula la duración de los periodos en los que el estado fue 'suspended'
|
||||
-- Se usa NOW() para el intervalo abierto (último estado hasta hoy)
|
||||
SELECT EXTRACT(EPOCH FROM (COALESCE(next_end_date, NOW() at time zone 'utc') - end_date)) * 1000 as ms_duration,
|
||||
(COALESCE(next_end_date, NOW() at time zone 'utc')::date - end_date::date) + 1 as days_duration
|
||||
FROM filtered_transitions
|
||||
WHERE state = 'suspended'
|
||||
)
|
||||
-- 5. Suma total de tiempo en estado de suspensión
|
||||
SELECT COALESCE(SUM(ms_duration)::bigint, 0) as total_milliseconds,
|
||||
COALESCE(SUM(days_duration), 0) as total_days
|
||||
FROM intervals;
|
||||
`;
|
||||
|
||||
try {
|
||||
const { rows } = await this.pgClient.query<{ total_milliseconds: string, total_days: string }>(query, [iccid]);
|
||||
return {
|
||||
data: {
|
||||
total_milliseconds: parseFloat(rows[0].total_milliseconds),
|
||||
total_days: parseInt(rows[0].total_days)
|
||||
}
|
||||
};
|
||||
} catch (e) {
|
||||
console.error("Error calculating suspended time", e);
|
||||
return {
|
||||
error: String(e),
|
||||
data: undefined
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,7 +20,6 @@ const order2 = <CreateOrderDTO>{
|
||||
payload: { iccid: "5678", action: "activate" }
|
||||
}
|
||||
|
||||
|
||||
describe("Test OrderRepository", {}, (ctx) => {
|
||||
const orderRepo = new OrderRepository(postgresClient)
|
||||
let testIds: number[] = []
|
||||
|
||||
Reference in New Issue
Block a user