F0 🔒 Fase 0 · Hardening (1–2 semanas)
F0-1 · Implementar autenticación en endpoints administrativos
Crítico · 2 díasReactivar session_start() + verificación de auth en:
caja/reset_db.php · sync.php · sync_bulk.phpcashflow/reset_db.php · sync.php · api/save.php · api/init.php · api/delete-sync.phpventa/sync_api.php · sync_api_process.php · reinit_import.phparticulos/sync.php · api/init.php · api/save.phpcaja/api/save.php · save_raw.php · init.php
Diseño propuesto
// /reportes/_shared/auth.php
function require_login(): array {
session_start();
if (!isset($_SESSION['usuario_id'])) {
if (isset($_SERVER['HTTP_X_REQUESTED_WITH'])) {
http_response_code(401);
exit(json_encode(['error' => 'No autenticado']));
}
header('Location: /login.php?next=' . urlencode($_SERVER['REQUEST_URI']));
exit;
}
return [
'id' => $_SESSION['usuario_id'],
'email' => $_SESSION['usuario_email'],
'roles' => $_SESSION['usuario_roles'] ?? [],
];
}
function require_role(array $rolesPermitidos): array {
$user = require_login();
if (empty(array_intersect($user['roles'], $rolesPermitidos))) {
http_response_code(403);
exit(json_encode(['error' => 'No autorizado']));
}
return $user;
}
// En cada endpoint:
require_once __DIR__ . '/../_shared/auth.php';
$user = require_role(['admin', 'finanzas']);
F0-2 · Mover credenciales a .env
Crítico · 1 día
# /home/u120688891/.env (FUERA del doc root)
DB_HOST=localhost
DB_NAME=u120688891_chess
DB_USER=u120688891_chess
DB_PASS=t2#h*wvQ2./wZaS
CHESS_API_URL=https://zonasaridas.chesserp.com/AR1185/web/api/chess/v1
CHESS_API_USER=api_zonas
CHESS_API_PASS=z0n4saridas
APP_ENV=production
APP_DEBUG=false
// /reportes/_shared/config.php
$envPath = '/home/u120688891/.env';
if (!file_exists($envPath)) die('Config not found');
foreach (parse_ini_file($envPath) as $k => $v) {
if (!defined($k)) define($k, $v);
}
// Aliases para retrocompatibilidad
if (!defined('REPORTES_DB_HOST')) define('REPORTES_DB_HOST', DB_HOST);
// ... etc
// SITE_VERSION local del módulo
require_once __DIR__ . '/../venta/config/site_version.php';
Los 5 config/database.php se reemplazan por:
<?php
require_once __DIR__ . '/../../_shared/config.php';
// Constantes propias del módulo (no credenciales)
define('UPLOAD_MAX_MB', 50);
F0-3 · Bloquear archivos sensibles
Crítico · 30 min# /reportes/.htaccess
<FilesMatch "\.(sql|md|bak|env|log|swp|orig|jsx|json5)$">
Order allow,deny
Deny from all
</FilesMatch>
# Bloquear lista de archivos específicos
<Files "u120688891_chess.sql">
Order allow,deny
Deny from all
</Files>
<Files "_debug_samarelli.php">
Order allow,deny
Deny from all
</Files>
# Bloquear directorios de config
<DirectoryMatch "/config/">
Order allow,deny
Deny from all
</DirectoryMatch>
# Disable directory listing
Options -Indexes
Adicionalmente, mover el dump fuera del doc root:
mv /home/u120688891/public_html/reportes/u120688891_chess.sql \
/home/u120688891/backups/u120688891_chess_2026-05-21.sql
F0-4 · Reemplazar interpolaciones SQL
Alto · 2 díasAuditoría completa con grep:
grep -rn 'query("[^"]*\$' /reportes --include="*.php"
# Lista todas las queries con interpolación de variables
Reemplazar todas por prepared statements. Para nombres de tabla (no parametrizables), usar whitelist explícita.
F0-5 · Eliminar código muerto
Alto · 1 hora- Borrar
caja/dashboard.jsx(22 KB legado React). - Borrar
venta/_debug_samarelli.php(debug ad-hoc). - Confirmar uso de
listas/api/sync_process.phpantes de borrarlo.
F0-6 · Crear robots.txt
Medio · 5 min
# /reportes/robots.txt
User-agent: *
Disallow: /
# Disallow: /reportes/ (si robots.txt está en raíz del sitio)
# Para evitar indexación accidental por buscadores
F0-7 · Audit log de accesos
Medio · 1 díaCREATE TABLE reportes_audit_log (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
usuario_id INT,
email VARCHAR(150),
ruta VARCHAR(255),
metodo VARCHAR(10),
ip VARCHAR(45),
user_agent VARCHAR(255),
payload JSON, -- input recibido (sin passwords!)
response_code INT,
duracion_ms INT,
creado_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
INDEX idx_usuario (usuario_id),
INDEX idx_fecha (creado_at),
INDEX idx_ruta (ruta)
);
-- Middleware en _shared/audit.php que loggea cada hit
-- Especialmente útil para endpoints destructivos
📊 Resumen de Fase 0
F1 🏗️ Fase 1 · Plataforma compartida (3–4 semanas)
Reducir duplicación, preparar la base para escalar.
F1-1 · Crear paquete _shared/
/reportes/ ├── _shared/ │ ├── config.php ← carga .env + constantes globales │ ├── auth.php ← require_login(), require_role() │ ├── db.php ← PDO singleton + bootstrap automático │ ├── logger.php ← log_error(), log_event(), log_audit() │ ├── rate_limit.php ← rate_limit($key, $max, $window) │ ├── formatters.php ← fmtMoney, fmtPct, fmtTons, fmtNum (PHP) │ ├── response.php ← jsonOk(), jsonErr(), api_response() │ ├── validators.php ← validateInt, validateEnum, validateDate │ ├── cache.php ← cache simple file-based para snapshots │ └── version.php ← REPORTES_PLATFORM_VERSION └── ...
F1-2 · Estandarizar respuestas de API
// /reportes/_shared/response.php
function api_response($data = null, ?string $error = null, int $code = 200): void {
http_response_code($code);
header('Content-Type: application/json; charset=utf-8');
header('X-Content-Type-Options: nosniff');
echo json_encode([
'ok' => $error === null,
'data' => $data,
'error' => $error,
'timestamp' => date('c'),
'version' => REPORTES_PLATFORM_VERSION,
], JSON_UNESCAPED_UNICODE);
exit;
}
// Uso:
api_response(['ventas' => $rows]);
api_response(null, 'Sync not found', 404);
F1-3 · Migrar CAJERO_PROV a tabla DB
CREATE TABLE caja_cajero_provincia (
cajero VARCHAR(100) PRIMARY KEY,
provincia VARCHAR(50) NOT NULL,
activo TINYINT(1) DEFAULT 1,
notas TEXT,
creado_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);
-- Seed inicial desde el JS hardcoded actual
INSERT INTO caja_cajero_provincia (cajero, provincia) VALUES
('lar-cajero1', 'LA RIOJA'),
('sgo-cajero2', 'SANTIAGO DEL ESTERO'),
('cat-cajero3', 'CATAMARCA'),
...;
-- UI: /reportes/caja/admin/cajeros.php (CRUD básico)
-- Pipeline JS hace fetch /caja/api/cajero_provincia.php al iniciar
F1-4 · Extraer fórmulas a clase reutilizable
// /reportes/venta/includes/VentaFormulas.php
final class VentaFormulas {
public const CBTE_KEY = "CONCAT(idEmpresa,'-',idDocumento,'-',letra,'-',IFNULL(serie,0),'-',IFNULL(nrodoc,0))";
public const SUM_VENTAS_REALES = "SUM(CASE WHEN dsArticulo IS NOT NULL AND dsArticulo != '' THEN subtotalNeto ELSE 0 END)";
public const SUM_COSTO_NETO = "SUM(preciocomprant * cantidadesTotal)";
public static function exprContribucion(): string {
return '(' . self::SUM_VENTAS_REALES . ' - ' . self::SUM_COSTO_NETO . ')';
}
public static function exprMargenPct(): string {
return "CASE WHEN " . self::SUM_VENTAS_REALES . " > 0 "
. "THEN " . self::exprContribucion() . " / " . self::SUM_VENTAS_REALES . " "
. "ELSE 0 END";
}
}
// Uso en data_api.php y filter.php:
use VentaFormulas as F;
$sql = "SELECT " . F::exprContribucion() . " AS contribucion, ...";
F1-5 · Composer autoload
Si crece la cantidad de helpers, vale la pena agregar Composer:
// composer.json
{
"require": {
"php": ">=8.0",
"vlucas/phpdotenv": "^5.5",
"monolog/monolog": "^3.0"
},
"autoload": {
"psr-4": {
"ZA\\Reportes\\": "_shared/"
}
}
}
F2 📊 Fase 2 · Capa BI estable (4–6 semanas)
Convierte el sistema en una BI propiamente dicha.
F2-1 · Vistas materializadas
CREATE TABLE ventas_mensual_supervisor_mv (
sync_id INT,
mes TINYINT,
anio SMALLINT,
dsSupervisor VARCHAR(100),
ventas_netas DECIMAL(15,2),
ventas_reales DECIMAL(15,2),
costo_total DECIMAL(15,2),
contribucion DECIMAL(15,2),
bultos_total DECIMAL(15,2),
peso_kg_total DECIMAL(15,2),
clientes_unicos INT,
total_comprobantes INT,
refreshed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (sync_id, dsSupervisor)
);
-- Refresh cron diario 04:00:
TRUNCATE ventas_mensual_supervisor_mv;
INSERT INTO ventas_mensual_supervisor_mv
SELECT sync_id, ... FROM ventas LEFT JOIN articulos_raw ... GROUP BY sync_id, dsSupervisor;
Reduce las queries del dashboard de 800-2000 ms a <50 ms.
F2-2 · Cron jobs
# Crontab en Hostinger
# Sync diario del mes en curso (ventas)
0 3 * * * php /home/u120688891/public_html/reportes/venta/cron/sync_mes_actual.php >> /var/log/za/venta_sync.log 2>&1
# Refresh de MVs
30 3 * * * php /home/u120688891/public_html/reportes/cron/refresh_mvs.php >> /var/log/za/mvs.log 2>&1
# Email diario de cashflow
0 7 * * 1-5 php /home/u120688891/public_html/reportes/cashflow/cron/email_diario.php
# Cleanup de cache cada hora
0 * * * * find /home/u120688891/public_html/reportes/_shared/cache -mmin +60 -delete
# Healthcheck cada 5 min
*/5 * * * * curl -s -o /dev/null -w "%{http_code}" https://zonasaridas.com.ar/reportes/health.php || \
echo "[$(date)] healthcheck failed" >> /var/log/za/healthcheck.log
F2-3 · Cache HTTP en endpoints JSON
// /reportes/_shared/cache.php
function http_cache(string $key, callable $generator, int $ttl = 300): void {
$etag = md5($key);
$ifNoneMatch = trim($_SERVER['HTTP_IF_NONE_MATCH'] ?? '', '"');
if ($ifNoneMatch === $etag) {
http_response_code(304);
exit;
}
header("ETag: \"$etag\"");
header("Cache-Control: private, max-age=$ttl, must-revalidate");
echo $generator();
}
// Uso en cashflow/api/data.php
http_cache(
"cashflow:$corte:$sucursal:" . filemtime(__FILE__),
fn() => json_encode($result)
);
F2-4 · Export Excel/CSV de cualquier dashboard
// /reportes/_shared/export.php
function export_csv(array $rows, string $filename): void {
header('Content-Type: text/csv; charset=utf-8');
header("Content-Disposition: attachment; filename=\"$filename\"");
echo "\xEF\xBB\xBF"; // BOM UTF-8 para Excel
$out = fopen('php://output', 'w');
if ($rows) fputcsv($out, array_keys($rows[0]));
foreach ($rows as $r) fputcsv($out, $r);
fclose($out);
}
// Botón "📥 Exportar" en cada Tabulator
// Tabulator soporta nativamente:
table.download("csv", "ventas_abr2026.csv");
table.download("xlsx", "ventas_abr2026.xlsx", {sheetName: "Ventas"});
F2-5 · Dashboard multi-período comparativo
Permitir seleccionar 2 períodos lado a lado para comparar (MoM, YoY):
┌────────────────────────────────────────────────────────────┐ │ 📊 Comparativo: [Mar 2026 ▼] vs [Mar 2025 ▼] │ ├────────────────────────────────────────────────────────────┤ │ │ MAR 2026 │ MAR 2025 │ YoY │ │ Vta │ $ 564.5 M │ $ 421.3 M │ +33.9% │ │ Margen │ 19.8 % │ 21.4 % │ -1.6 pp │ │ Comprobantes │ 21.864 │ 18.349 │ +19.2% │ │ Ticket promedio │ $ 25.821 │ $ 22.961 │ +12.5% │ └────────────────────────────────────────────────────────────┘
F2-6 · Drill-down universal
Hacer que CUALQUIER KPI o celda sea clicable y abra detalle:
- Click en "Vta total" → tabla de comprobantes del período.
- Click en "Bultos" → distribución por SKU.
- Click en una fila → mini-dashboard de esa entidad.
F3 🤖 Fase 3 · Inteligencia y proyecciones (6–8 semanas)
F3-1 · Forecast de cashflow con regresión
Sustituir la "estimación por cuartos" del calendario semanal por modelo basado en histórico:
// Pseudocódigo
function predict_cobranza_semanal($cliente_id, $semana_offset) {
// 1. Obtener cobranzas históricas del cliente (últimos 24 meses)
// 2. Para cada saldo en aging:
// ¿Qué % se cobró en semana N? (promedio histórico)
// 3. Ajustar por:
// - Estacionalidad (mes calendario)
// - Vendedor (algunos cobran más rápido)
// - Tendencia (mejora o empeora últimos 3 meses)
// 4. Devolver: { punto, intervalo_inferior, intervalo_superior }
}
F3-2 · Alertas activas
CREATE TABLE reportes_alertas (
id INT PRIMARY KEY AUTO_INCREMENT,
tipo VARCHAR(40),
nivel ENUM('info', 'warning', 'critical'),
titulo VARCHAR(255),
mensaje TEXT,
payload JSON,
canal SET('email', 'whatsapp', 'slack', 'dashboard'),
destinatarios JSON,
enviado_at TIMESTAMP NULL,
reconocido_at TIMESTAMP NULL,
creado_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- Cron diario evalúa reglas y crea alertas
-- Otro cron envía las pendientes
Reglas iniciales
- Saldo proyectado a 30d < $X → email a finanzas.
- Margen del mes < promedio histórico - 3% → email a gerencia.
- Cliente con NDCON nuevo → WhatsApp al vendedor.
- Cheque a presentar mañana → email a tesorería.
- Sync que no se ejecuta hace 3 días → email a admin.
F3-3 · Detección de anomalías
// Z-score sobre gasto mensual por rubro × provincia
SELECT
rubro, provincia, mes, anio,
SUM(monto) AS gasto_actual,
AVG(SUM(monto)) OVER (PARTITION BY rubro, provincia) AS media_historica,
STDDEV(SUM(monto)) OVER (PARTITION BY rubro, provincia) AS stddev,
(SUM(monto) - AVG(SUM(monto)) OVER (PARTITION BY rubro, provincia))
/ NULLIF(STDDEV(SUM(monto)) OVER (PARTITION BY rubro, provincia), 0) AS z_score
FROM caja_gastos
GROUP BY rubro, provincia, mes, anio
HAVING ABS(z_score) > 2;
-- |z| > 2 → anomalía estadística significativa
F3-4 · Recomendador de límite de crédito
Para cada cliente, sugerir un límite de crédito basado en:
- Venta promedio mensual últimos 12 meses.
- Plazo de pago dominante.
- Historial de NDCONs (cheques rechazados).
- Estacionalidad del cliente.
- Variabilidad mes a mes.
limite_sugerido = venta_mensual_promedio
× (1 + plazo_dias / 30)
× factor_riesgo(antig, ndcons, dias_sin_comprar)
// factor_riesgo:
// sano → 1.5
// normal → 1.0
// creciente → 0.7
// fantasma → 0.3
// incumplim. → 0.0 (cortar)
F3-5 · Reporte ejecutivo PDF mensual
Job mensual genera PDF de 8-12 páginas con:
- Carátula con logo + mes + KPIs principales.
- Resumen ejecutivo (1 página, 5-7 takeaways).
- Detalle por área: Venta · Caja · Cashflow.
- Comparativa MoM y YoY.
- Anexo con tablas detalladas.
Stack sugerido: dompdf o mPDF (PHP-native, sin browser headless).
F3-6 · API pública (read-only)
// /reportes/api/v1/
// GET /ventas/kpis?sync=28&sucursal=ZA
// GET /caja/gastos?anio=2026&mes=4
// GET /cashflow/snapshot?corte=2026-05-20
// Auth: API key (Bearer token)
// Rate limit: 1000 req/h por key
// Documentación: OpenAPI 3.0
// Permite integración con Power BI, Looker, Tableau
CREATE TABLE api_keys (
id INT PRIMARY KEY AUTO_INCREMENT,
key_hash VARCHAR(64) NOT NULL UNIQUE,
nombre VARCHAR(100),
scopes SET('venta:read','caja:read','cashflow:read','articulos:read'),
rate_limit INT DEFAULT 1000,
ultima_uso TIMESTAMP NULL,
creado_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
expira_at TIMESTAMP NULL,
activo TINYINT(1) DEFAULT 1
);
11.5 📋 Mejoras de informes existentes
Mejora #1 · Filtros guardables ("Mis vistas")
Cada usuario puede guardar combinaciones de filtros frecuentes:
- "Cierre mensual ZA" = sync más reciente + sucursal ZONAS ARIDAS + sin filtro extra.
- "Gastos LA RIOJA Q1" = provincia + rango mes/año.
- "Top deudores +60d" = corte actual + tramo +60.
Mejora #2 · Exportación masiva
- "Descargar TODO el dashboard como Excel" (cada sección = una hoja).
- "Generar reporte PDF de este período" (1 click → PDF estilizado).
- "Compartir link" → URL con todos los filtros embebidos.
Mejora #3 · Anotaciones por celda
Permitir agregar notas a cualquier KPI/fila:
CREATE TABLE reportes_anotaciones (
id INT PRIMARY KEY AUTO_INCREMENT,
contexto VARCHAR(100), -- 'venta:sync:28:supervisor:LAR_ANDREA'
usuario_id INT,
titulo VARCHAR(255),
texto TEXT,
visible_para SET('yo','equipo','todos'),
creado_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
INDEX idx_contexto (contexto)
);
Útil para "Esta caída de margen fue por la promo de Easter" o "Cheque rechazado, ya conversado con el cliente".
Mejora #4 · Comparativa contra presupuesto
CREATE TABLE presupuestos (
id INT PRIMARY KEY AUTO_INCREMENT,
anio SMALLINT,
mes TINYINT,
dimension VARCHAR(50), -- 'venta_total', 'gasto_combustible', 'margen_pct'
scope VARCHAR(100), -- 'ZA', 'LA_RIOJA', etc
valor DECIMAL(15,2),
creado_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
UNIQUE KEY uk_periodo_scope (anio, mes, dimension, scope)
);
-- En el dashboard: "Vta: $591M (vs $580M presup) +1.9% ✓"
11.6 🔭 Observabilidad
Métricas a trackear
- Performance: tiempo de respuesta por endpoint (p50, p95, p99).
- Errores: tasa por hora, tipo (PHP fatal, DB error, API timeout).
- Sync health: cuántos minutos pasaron desde último sync exitoso.
- BD: tamaño de cada tabla, slow queries (>500ms).
- Negocio: # users diarios, dashboards más visitados.
Stack sugerido (low cost)
- Logs: archivos JSONL en
/var/log/za/rotados por mes. - Métricas: tabla
reportes_metrics+ cron de agregación. - Dashboard interno:
/reportes/admin/observability.php. - Alerting: cron que evalúa thresholds y envía email/WhatsApp.
CREATE TABLE reportes_metrics (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
metric VARCHAR(80),
value DECIMAL(15,4),
tags JSON,
recorded_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
INDEX idx_metric_time (metric, recorded_at)
) ENGINE=InnoDB;
-- Particionar por mes para retención
ALTER TABLE reportes_metrics PARTITION BY RANGE (TO_DAYS(recorded_at)) (
PARTITION p202605 VALUES LESS THAN (TO_DAYS('2026-06-01')),
PARTITION p202606 VALUES LESS THAN (TO_DAYS('2026-07-01')),
...
);
11.7 🧪 Testing
Estado actual
El proyecto NO tiene tests automatizados. Verifica todo manualmente con "casos conocidos" (SAMARELLI, MOLINA, etc.).
Propuesta mínima viable
/reportes/tests/
├── PHPUnit.xml
├── bootstrap.php ← carga _shared/config + DB de test
│
├── Unit/
│ ├── FormulasTest.php ← VentaFormulas::exprMargenPct() es válido SQL
│ ├── FormattersTest.php ← formatMoney(1234567.89) === '$ 1.234.568'
│ ├── ParserTest.php ← parseMovDetalleMap regresión bug v2.0.2
│
├── Integration/
│ ├── ApiKpisTest.php ← _apiKpis devuelve estructura esperada
│ ├── PipelineTest.php ← pipeline 3-tier sobre dataset fixture
│ ├── ChessApiTest.php ← (mock) login + fetchVentas con sample
│
└── Regression/
├── SamarelliTest.php ← margen 25.20% en período Abr 2026
├── MolinaTest.php ← margen 21.09% con NDCONs
├── TotalesCoherentes.php ← 6 tabs Rentabilidad dan mismo TOTAL
Stack: PHPUnit 10+ con DB en SQLite o MariaDB de test.
Run en GitHub Actions o GitLab CI antes de cada deploy.
11.8 🚀 CI/CD
Pipeline propuesto
git push origin main
│
▼
┌─────────────────────────────────────────────────────────────┐
│ GitHub Actions / GitLab CI │
│ │
│ 1. PHP Lint │
│ php -l en cada .php tocado │
│ │
│ 2. PHPStan / Psalm │
│ Type checking nivel 5 │
│ │
│ 3. PHPUnit │
│ Unit + Integration + Regression │
│ │
│ 4. Security Audit │
│ composer audit + grep sensitive patterns │
│ │
│ 5. Build assets (si hay) │
│ │
│ 6. Deploy a staging (auto) │
│ rsync a /reportes-staging/ │
│ │
│ 7. Smoke tests en staging │
│ curl /health.php → status 200 │
│ │
│ 8. Deploy a producción (manual approval) │
│ rsync a /reportes/ │
│ + invalidación de cache │
└─────────────────────────────────────────────────────────────┘
Versionado del análisis
Este documento de análisis sigue su propio semver. Ver §14 Changelog.
11.9 👥 Equipo y procesos
Roles recomendados
| Rol | Responsabilidades | Dedicación |
|---|---|---|
| Tech Lead | Arquitectura, code reviews, decisiones de stack, mentoría | 50% del proyecto |
| Backend Dev (PHP) | Endpoints, syncs, BD, refactor | 100% |
| Frontend Dev (JS) | Dashboards, Tabulator, Chart.js, UX | 50-100% |
| Data Analyst | Validar fórmulas, nuevos reportes, training a usuarios | 25-50% |
| Product Owner (negocio) | Priorizar features, validar resultados, vínculo con stakeholders | 25% |
Procesos sugeridos
- Sprints de 2 semanas con demo al final.
- Code review obligatorio antes de merge a main.
- Standup async diario (mensaje en Slack/WhatsApp).
- Retro mensual + revisión del roadmap.
- Backup verificado semanal (no solo automatizar — VERIFICAR que restaure).
- Verificaciones del cliente al final de cada feature (caso real como SAMARELLI/MOLINA → no cambia el resultado).
Documentación viva
Mantener el sistema de docs markdown existente (~4.700 líneas) + este análisis HTML versionado. Bumpear las versiones con cada cambio mayor.