@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 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
@@ -0,0 +1,5 @@
1
+ export declare function getIPCVariation(): Promise<{
2
+ value: number;
3
+ date: Date;
4
+ }>;
5
+ export declare function getRipteVariation(): Promise<number | null>;
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 {};
@@ -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
+ });
@@ -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
+ }