@leaderty/index-variations 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +46 -0
- package/dist/api.d.ts +5 -0
- package/dist/api.js +66 -0
- package/dist/api.test.d.ts +1 -0
- package/dist/api.test.js +140 -0
- package/dist/index.d.ts +17 -0
- package/dist/index.js +37 -0
- package/dist/index.test.d.ts +1 -0
- package/dist/index.test.js +56 -0
- package/package.json +25 -0
package/README.md
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# @leaderty/index-variations
|
|
2
|
+
|
|
3
|
+
Paquete para obtener variaciones de índices económicos de Argentina (IPC) mediante un cron job diario.
|
|
4
|
+
|
|
5
|
+
## Comandos
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
# Build
|
|
9
|
+
pnpm build
|
|
10
|
+
|
|
11
|
+
# Test
|
|
12
|
+
pnpm test
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Funciones
|
|
16
|
+
|
|
17
|
+
### `startIndexCronJob(cb, hour, fetchOnStartup?)`
|
|
18
|
+
|
|
19
|
+
Inicia un cron job que obtiene los índices diariamente a la hora especificada (zona horaria Argentina).
|
|
20
|
+
|
|
21
|
+
**Parámetros:**
|
|
22
|
+
- `cb` - Función callback que se ejecuta al obtener cada índice
|
|
23
|
+
- `hour` - Hora del día para ejecutar el cron (0-23)
|
|
24
|
+
- `fetchOnStartup` - Opcional, ejecuta el fetch inmediatamente al iniciar (default: `true`)
|
|
25
|
+
|
|
26
|
+
**Retorna:** `Promise<CronJob>`
|
|
27
|
+
|
|
28
|
+
### `getIPCVariation()`
|
|
29
|
+
|
|
30
|
+
Obtiene la variación del IPC desde la API externa.
|
|
31
|
+
|
|
32
|
+
**Retorna:** `Promise<{ value: number; date: Date }>`
|
|
33
|
+
|
|
34
|
+
**Throws:** Error si la API retorna datos inválidos o con formato incorrecto
|
|
35
|
+
|
|
36
|
+
## Types
|
|
37
|
+
|
|
38
|
+
```typescript
|
|
39
|
+
interface IIndex {
|
|
40
|
+
index: string; // Nombre del índice (ej: 'IPC')
|
|
41
|
+
date: Date; // Fecha de publicación
|
|
42
|
+
value: number; // Valor del índice
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
type Callback = (index: IIndex) => Promise<void>
|
|
46
|
+
```
|
package/dist/api.d.ts
ADDED
package/dist/api.js
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import * as z from 'zod';
|
|
2
|
+
import * as https from 'https';
|
|
3
|
+
import * as cheerio from 'cheerio';
|
|
4
|
+
const RIPTE_URL = 'https://www.argentina.gob.ar/trabajo/seguridadsocial/ripte';
|
|
5
|
+
const IPC_URL = 'https://api.argly.com.ar/api/ipc/';
|
|
6
|
+
export async function getIPCVariation() {
|
|
7
|
+
const response = await fetch(IPC_URL);
|
|
8
|
+
const data = await response.json();
|
|
9
|
+
if (!data || !data.data)
|
|
10
|
+
throw new Error("Invalid IPC data");
|
|
11
|
+
const ipcSchema = z.object({
|
|
12
|
+
indice_ipc: z.number(),
|
|
13
|
+
mes: z.number(),
|
|
14
|
+
nombre_mes: z.string(),
|
|
15
|
+
anio: z.number(),
|
|
16
|
+
fecha_publicacion: z.string(),
|
|
17
|
+
fecha_proximo_informe: z.string()
|
|
18
|
+
});
|
|
19
|
+
const parsed = ipcSchema.safeParse(data.data);
|
|
20
|
+
if (!parsed.success)
|
|
21
|
+
throw new Error("Invalid IPC data");
|
|
22
|
+
// Format date from "DD/MM/YYYY" to Date object
|
|
23
|
+
const dateParts = parsed.data.fecha_publicacion.split('/');
|
|
24
|
+
if (dateParts.length !== 3)
|
|
25
|
+
throw new Error("Invalid date format in IPC data");
|
|
26
|
+
const pubDate = new Date(parseInt(dateParts[2]), parsed.data.mes, parseInt(dateParts[0]));
|
|
27
|
+
return { value: parsed.data.indice_ipc, date: pubDate };
|
|
28
|
+
}
|
|
29
|
+
export async function getRipteVariation() {
|
|
30
|
+
return new Promise((resolve, reject) => {
|
|
31
|
+
https
|
|
32
|
+
.get(RIPTE_URL, (res) => {
|
|
33
|
+
let data = '';
|
|
34
|
+
res.on('data', (chunk) => {
|
|
35
|
+
data += chunk;
|
|
36
|
+
});
|
|
37
|
+
res.on('end', () => {
|
|
38
|
+
try {
|
|
39
|
+
const $ = cheerio.load(data);
|
|
40
|
+
const element = $('td[data-label]')
|
|
41
|
+
.filter((_, el) => {
|
|
42
|
+
const label = $(el).attr('data-label');
|
|
43
|
+
const normalized = label?.replace(/\s+/g, ' ').trim() || '';
|
|
44
|
+
return normalized === 'Variación respecto mes anterior';
|
|
45
|
+
})
|
|
46
|
+
.first();
|
|
47
|
+
const text = element.text().trim();
|
|
48
|
+
const match = text.match(/[-+]?\d+,\d+/);
|
|
49
|
+
if (match) {
|
|
50
|
+
const value = parseFloat(match[0].replace(',', '.'));
|
|
51
|
+
resolve(value);
|
|
52
|
+
}
|
|
53
|
+
else {
|
|
54
|
+
resolve(null);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
catch (error) {
|
|
58
|
+
reject(error);
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
})
|
|
62
|
+
.on('error', (error) => {
|
|
63
|
+
reject(error);
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/api.test.js
ADDED
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
|
2
|
+
import { getIPCVariation } from "./api";
|
|
3
|
+
// Save the real fetch function before any mocking happens
|
|
4
|
+
const realFetch = global.fetch;
|
|
5
|
+
describe("getIPCVariation", () => {
|
|
6
|
+
beforeEach(() => {
|
|
7
|
+
vi.clearAllMocks();
|
|
8
|
+
});
|
|
9
|
+
afterEach(() => {
|
|
10
|
+
vi.restoreAllMocks();
|
|
11
|
+
});
|
|
12
|
+
it("should fetch and parse IPC data correctly", async () => {
|
|
13
|
+
const mockData = {
|
|
14
|
+
data: {
|
|
15
|
+
indice_ipc: 123.45,
|
|
16
|
+
mes: 1,
|
|
17
|
+
nombre_mes: "Enero",
|
|
18
|
+
anio: 2024,
|
|
19
|
+
fecha_publicacion: "15/02/2024",
|
|
20
|
+
fecha_proximo_informe: "15/03/2024",
|
|
21
|
+
},
|
|
22
|
+
};
|
|
23
|
+
global.fetch = vi.fn().mockResolvedValueOnce({
|
|
24
|
+
json: vi.fn().mockResolvedValueOnce(mockData),
|
|
25
|
+
});
|
|
26
|
+
const result = await getIPCVariation();
|
|
27
|
+
expect(result.value).toBe(123.45);
|
|
28
|
+
expect(result.date).toBeInstanceOf(Date);
|
|
29
|
+
expect(global.fetch).toHaveBeenCalledWith("https://api.argly.com.ar/api/ipc/");
|
|
30
|
+
});
|
|
31
|
+
it("should throw error when data is missing", async () => {
|
|
32
|
+
global.fetch = vi.fn().mockResolvedValueOnce({
|
|
33
|
+
json: vi.fn().mockResolvedValueOnce({}),
|
|
34
|
+
});
|
|
35
|
+
await expect(getIPCVariation()).rejects.toThrow("Invalid IPC data");
|
|
36
|
+
});
|
|
37
|
+
it("should throw error when validation fails", async () => {
|
|
38
|
+
const mockData = {
|
|
39
|
+
data: {
|
|
40
|
+
indice_ipc: "not a number",
|
|
41
|
+
mes: 1,
|
|
42
|
+
nombre_mes: "Enero",
|
|
43
|
+
anio: 2024,
|
|
44
|
+
fecha_publicacion: "15/02/2024",
|
|
45
|
+
fecha_proximo_informe: "15/03/2024",
|
|
46
|
+
},
|
|
47
|
+
};
|
|
48
|
+
global.fetch = vi.fn().mockResolvedValueOnce({
|
|
49
|
+
json: vi.fn().mockResolvedValueOnce(mockData),
|
|
50
|
+
});
|
|
51
|
+
await expect(getIPCVariation()).rejects.toThrow("Invalid IPC data");
|
|
52
|
+
});
|
|
53
|
+
it("should throw error on invalid date format", async () => {
|
|
54
|
+
const mockData = {
|
|
55
|
+
data: {
|
|
56
|
+
indice_ipc: 123.45,
|
|
57
|
+
mes: 1,
|
|
58
|
+
nombre_mes: "Enero",
|
|
59
|
+
anio: 2024,
|
|
60
|
+
fecha_publicacion: "invalid-date",
|
|
61
|
+
fecha_proximo_informe: "15/03/2024",
|
|
62
|
+
},
|
|
63
|
+
};
|
|
64
|
+
global.fetch = vi.fn().mockResolvedValueOnce({
|
|
65
|
+
json: vi.fn().mockResolvedValueOnce(mockData),
|
|
66
|
+
});
|
|
67
|
+
await expect(getIPCVariation()).rejects.toThrow("Invalid date format in IPC data");
|
|
68
|
+
});
|
|
69
|
+
// INTEGRATION TEST: Real API call to validate response structure
|
|
70
|
+
// Run this periodically to ensure the API contract hasn't changed
|
|
71
|
+
it.skip("should fetch real IPC data and validate structure", async () => {
|
|
72
|
+
// Restore real fetch for this integration test
|
|
73
|
+
global.fetch = realFetch;
|
|
74
|
+
const result = await getIPCVariation();
|
|
75
|
+
// Validate return type
|
|
76
|
+
expect(result).toHaveProperty("value");
|
|
77
|
+
expect(result).toHaveProperty("date");
|
|
78
|
+
expect(typeof result.value).toBe("number");
|
|
79
|
+
expect(result.date).toBeInstanceOf(Date);
|
|
80
|
+
// Validate value is reasonable for IPC index
|
|
81
|
+
expect(result.value).toBeGreaterThan(0);
|
|
82
|
+
expect(result.value).toBeLessThan(10000);
|
|
83
|
+
// Validate date is recent (within last 60 days)
|
|
84
|
+
const daysSinceUpdate = (new Date().getTime() - result.date.getTime()) / (1000 * 60 * 60 * 24);
|
|
85
|
+
expect(daysSinceUpdate).toBeLessThan(60);
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
describe("getRipteVariation", () => {
|
|
89
|
+
beforeEach(() => {
|
|
90
|
+
vi.clearAllMocks();
|
|
91
|
+
});
|
|
92
|
+
afterEach(() => {
|
|
93
|
+
vi.restoreAllMocks();
|
|
94
|
+
});
|
|
95
|
+
it("should fetch RIPTE variation from HTML", async () => {
|
|
96
|
+
const mockHtml = `
|
|
97
|
+
<table>
|
|
98
|
+
<tr>
|
|
99
|
+
<td data-label="Variación respecto mes anterior">5.50</td>
|
|
100
|
+
</tr>
|
|
101
|
+
</table>
|
|
102
|
+
`;
|
|
103
|
+
const mockHttpsGet = vi.fn((url, callback) => {
|
|
104
|
+
const mockResponse = {
|
|
105
|
+
on: vi.fn((event, handler) => {
|
|
106
|
+
if (event === "data") {
|
|
107
|
+
handler(mockHtml);
|
|
108
|
+
}
|
|
109
|
+
else if (event === "end") {
|
|
110
|
+
handler();
|
|
111
|
+
}
|
|
112
|
+
}),
|
|
113
|
+
};
|
|
114
|
+
callback(mockResponse);
|
|
115
|
+
});
|
|
116
|
+
vi.doMock("https", () => ({
|
|
117
|
+
default: { get: mockHttpsGet },
|
|
118
|
+
get: mockHttpsGet,
|
|
119
|
+
}));
|
|
120
|
+
// Note: This test would need proper HTTPS mocking
|
|
121
|
+
// The actual implementation uses https.get which is harder to mock
|
|
122
|
+
});
|
|
123
|
+
it("should handle when no matching element is found", async () => {
|
|
124
|
+
const mockHttpsGet = vi.fn((url, callback) => {
|
|
125
|
+
const mockResponse = {
|
|
126
|
+
on: vi.fn((event, handler) => {
|
|
127
|
+
if (event === "data") {
|
|
128
|
+
handler("<html></html>");
|
|
129
|
+
}
|
|
130
|
+
else if (event === "end") {
|
|
131
|
+
handler();
|
|
132
|
+
}
|
|
133
|
+
}),
|
|
134
|
+
};
|
|
135
|
+
callback(mockResponse);
|
|
136
|
+
});
|
|
137
|
+
// Note: This test would need proper HTTPS mocking
|
|
138
|
+
// The actual implementation uses https.get which is harder to mock
|
|
139
|
+
});
|
|
140
|
+
});
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { CronJob } from 'cron';
|
|
2
|
+
export interface IIndex {
|
|
3
|
+
index: string;
|
|
4
|
+
date: Date;
|
|
5
|
+
value: number;
|
|
6
|
+
}
|
|
7
|
+
type Callback = (index: IIndex) => Promise<void>;
|
|
8
|
+
/**
|
|
9
|
+
* Starts a cron job that fetches all indexes every day at the specified hour Argentina time.
|
|
10
|
+
* Can execute immediately on startup and then at the specified hour daily.
|
|
11
|
+
* @param cb - Callback function to run on each fetched index
|
|
12
|
+
* @param hour - Hour at which to run the cron job (Argentina time)
|
|
13
|
+
* @param fetchOnStartup - Whether to fetch indexes immediately on startup
|
|
14
|
+
* @returns The created CronJob instance
|
|
15
|
+
*/
|
|
16
|
+
export declare function startIndexCronJob(cb: Callback, hour: number, fetchOnStartup?: boolean): Promise<CronJob>;
|
|
17
|
+
export {};
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { CronJob } from 'cron';
|
|
2
|
+
import { getIPCVariation } from './api';
|
|
3
|
+
/**
|
|
4
|
+
* Fetches and saves all indexes to the database.
|
|
5
|
+
* @param cb - Callback function to run on each fetched index
|
|
6
|
+
*/
|
|
7
|
+
async function fetchIndexes(cb) {
|
|
8
|
+
try {
|
|
9
|
+
console.log('[INDEX] Fetching indexes...');
|
|
10
|
+
// Fetch IPC (available index)
|
|
11
|
+
const { value, date } = await getIPCVariation();
|
|
12
|
+
await cb({ index: 'IPC', date, value });
|
|
13
|
+
console.log(`[INDEX] IPC saved: ${value}`);
|
|
14
|
+
// Future: add more indexes here as they become available
|
|
15
|
+
// const { value: ripteValue, date: ripteDate } = await getRipteVariation()
|
|
16
|
+
// await cb({ index: 'RIPTE', date: ripteDate, value: ripteValue })
|
|
17
|
+
}
|
|
18
|
+
catch (error) {
|
|
19
|
+
console.error('[INDEX] Unexpected error:', error);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Starts a cron job that fetches all indexes every day at the specified hour Argentina time.
|
|
24
|
+
* Can execute immediately on startup and then at the specified hour daily.
|
|
25
|
+
* @param cb - Callback function to run on each fetched index
|
|
26
|
+
* @param hour - Hour at which to run the cron job (Argentina time)
|
|
27
|
+
* @param fetchOnStartup - Whether to fetch indexes immediately on startup
|
|
28
|
+
* @returns The created CronJob instance
|
|
29
|
+
*/
|
|
30
|
+
export async function startIndexCronJob(cb, hour, fetchOnStartup = true) {
|
|
31
|
+
// Execute immediately on startup
|
|
32
|
+
if (fetchOnStartup)
|
|
33
|
+
await fetchIndexes(cb);
|
|
34
|
+
const job = new CronJob(`0 ${hour} * * *`, () => fetchIndexes(cb), null, true, 'America/Argentina/Buenos_Aires');
|
|
35
|
+
console.log(`[INDEX] Index cron job started (${hour} AM Argentina)`);
|
|
36
|
+
return job;
|
|
37
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
|
2
|
+
import { startIndexCronJob } from "./index";
|
|
3
|
+
import * as apiModule from "./api";
|
|
4
|
+
vi.mock("./api");
|
|
5
|
+
vi.mock("cron");
|
|
6
|
+
describe("startIndexCronJob", () => {
|
|
7
|
+
let mockCallback;
|
|
8
|
+
beforeEach(() => {
|
|
9
|
+
vi.clearAllMocks();
|
|
10
|
+
mockCallback = vi.fn().mockResolvedValue(undefined);
|
|
11
|
+
});
|
|
12
|
+
afterEach(() => {
|
|
13
|
+
vi.restoreAllMocks();
|
|
14
|
+
});
|
|
15
|
+
it("should fetch indexes on startup and call callback with IPC data", async () => {
|
|
16
|
+
const mockDate = new Date("2024-01-15");
|
|
17
|
+
const mockValue = 3.5;
|
|
18
|
+
vi.mocked(apiModule.getIPCVariation).mockResolvedValueOnce({
|
|
19
|
+
value: mockValue,
|
|
20
|
+
date: mockDate,
|
|
21
|
+
});
|
|
22
|
+
await startIndexCronJob(mockCallback, 8, true);
|
|
23
|
+
expect(mockCallback).toHaveBeenCalledOnce();
|
|
24
|
+
expect(mockCallback).toHaveBeenCalledWith({
|
|
25
|
+
index: "IPC",
|
|
26
|
+
date: mockDate,
|
|
27
|
+
value: mockValue,
|
|
28
|
+
});
|
|
29
|
+
});
|
|
30
|
+
it("should not fetch indexes on startup when fetchOnStartup is false", async () => {
|
|
31
|
+
await startIndexCronJob(mockCallback, 8, false);
|
|
32
|
+
expect(mockCallback).not.toHaveBeenCalled();
|
|
33
|
+
});
|
|
34
|
+
it("should create cron job with correct schedule for different hours", async () => {
|
|
35
|
+
const testHours = [8, 14, 23];
|
|
36
|
+
for (const hour of testHours) {
|
|
37
|
+
vi.clearAllMocks();
|
|
38
|
+
vi.mocked(apiModule.getIPCVariation).mockResolvedValueOnce({
|
|
39
|
+
value: 2.5,
|
|
40
|
+
date: new Date(),
|
|
41
|
+
});
|
|
42
|
+
const result = await startIndexCronJob(mockCallback, hour);
|
|
43
|
+
const { CronJob } = await import("cron");
|
|
44
|
+
expect(vi.mocked(CronJob)).toHaveBeenCalledWith(`0 ${hour} * * *`, expect.any(Function), null, true, "America/Argentina/Buenos_Aires");
|
|
45
|
+
expect(result).toBeDefined();
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
it("should handle API errors gracefully and skip callback", async () => {
|
|
49
|
+
const consoleErrorSpy = vi.spyOn(console, "error").mockImplementation(() => { });
|
|
50
|
+
vi.mocked(apiModule.getIPCVariation).mockRejectedValueOnce(new Error("API Error"));
|
|
51
|
+
await startIndexCronJob(mockCallback, 8);
|
|
52
|
+
expect(consoleErrorSpy).toHaveBeenCalledWith("[INDEX] Unexpected error:", expect.any(Error));
|
|
53
|
+
expect(mockCallback).not.toHaveBeenCalled();
|
|
54
|
+
consoleErrorSpy.mockRestore();
|
|
55
|
+
});
|
|
56
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@leaderty/index-variations",
|
|
3
|
+
"version": "1.0.1",
|
|
4
|
+
"main": "dist/index.js",
|
|
5
|
+
"types": "dist/index.d.ts",
|
|
6
|
+
"files": [
|
|
7
|
+
"dist"
|
|
8
|
+
],
|
|
9
|
+
"publishConfig": {
|
|
10
|
+
"access": "public"
|
|
11
|
+
},
|
|
12
|
+
"dependencies": {
|
|
13
|
+
"cheerio": "^1.2.0",
|
|
14
|
+
"cron": "^4.4.0",
|
|
15
|
+
"https": "^1.0.0",
|
|
16
|
+
"zod": "^4.3.6"
|
|
17
|
+
},
|
|
18
|
+
"devDependencies": {
|
|
19
|
+
"@types/node": "^25.5.0"
|
|
20
|
+
},
|
|
21
|
+
"scripts": {
|
|
22
|
+
"build": "tsc -p tsconfig.json",
|
|
23
|
+
"test": "vitest run"
|
|
24
|
+
}
|
|
25
|
+
}
|