# Opinión de Cumplimiento — Integration Design **Date:** 2026-04-13 **Status:** Approved ## Problem Horux360 has no way to check or track a tenant's SAT compliance status (Opinión de Cumplimiento). This is a critical fiscal document that indicates whether a company is current on all tax obligations. Currently, users must manually download it from the SAT portal. ## Solution Integrate the existing standalone Playwright-based prototype into Horux360 as a weekly automated process. Display results in a new "Documentos" page accessible to all roles (business+ plans). Store last 6 months in DB, show last 5 in UI. Alert if status is not Positiva. ## Source Prototype Located at `C:\Users\chtr1\Downloads\sat-opinion-prototype`. Key files to adapt: - `src/sat-login.ts` — Playwright navigation: public page → FIEL login → report - `src/opinion-scraper.ts` — 4 strategies to extract PDF base64 from DOM - `src/pdf-parser.ts` — Regex extraction of RFC, razón social, estatus, folio, cadena original - `src/types.ts` — `OpinionCumplimiento`, `Obligacion` interfaces ## Architecture ### New Files | File | Purpose | |------|---------| | `src/services/opinion-cumplimiento.service.ts` | Orchestration: decrypt FIEL → temp files → Playwright → parse → save to DB → cleanup | | `src/services/sat/sat-opinion-login.ts` | Adapted sat-login.ts: works with temp file paths from decrypted FIEL Buffers | | `src/services/sat/sat-opinion-scraper.ts` | Adapted opinion-scraper.ts: extracts PDF from SAT Angular SPA | | `src/services/sat/sat-opinion-parser.ts` | Adapted pdf-parser.ts: regex extraction from PDF text | | `src/controllers/documentos.controller.ts` | Endpoints: list opinions, download PDF, manual trigger | | `src/routes/documentos.routes.ts` | Routes with tenantMiddleware + feature gate | | `src/migrations/tenant/002_create_opiniones_cumplimiento.sql` | Tenant DB migration | | `apps/web/app/(dashboard)/documentos/page.tsx` | Frontend: Documentos page with Opinión tab | | `apps/web/lib/api/documentos.ts` | API client functions | | `apps/web/lib/hooks/use-documentos.ts` | React Query hooks | ### Modified Files | File | Change | |------|--------| | `src/jobs/sat-sync.job.ts` | Add weekly cron for opinion download | | `src/services/alertas-auto.service.ts` | Add alert for non-Positiva status | | `apps/web/components/layouts/sidebar.tsx` | Add Documentos nav item | | `apps/api/package.json` | Add playwright, pdf-parse dependencies | | `packages/shared/src/types/` | Add OpinionCumplimiento types | ## Database ### Table: `opiniones_cumplimiento` (per-tenant DB) ```sql CREATE TABLE IF NOT EXISTS opiniones_cumplimiento ( id SERIAL PRIMARY KEY, rfc VARCHAR(14) NOT NULL, razon_social VARCHAR(255), estatus VARCHAR(50) NOT NULL, folio VARCHAR(50), cadena_original TEXT, fecha_consulta TIMESTAMP NOT NULL, pdf BYTEA NOT NULL, created_at TIMESTAMP DEFAULT NOW() ); CREATE INDEX IF NOT EXISTS idx_opiniones_fecha ON opiniones_cumplimiento(fecha_consulta DESC); ``` Migration file: `002_create_opiniones_cumplimiento.sql` **Retention:** Records older than 6 months are deleted during the weekly cron run. **UI display:** Only the last 5 records are shown via `ORDER BY fecha_consulta DESC LIMIT 5`. ## FIEL Security The FIEL is stored encrypted (AES-256-GCM) in the central DB. For Playwright, which requires file paths: 1. `getDecryptedFiel(tenantId)` returns Buffers in memory 2. Write .cer and .key to `os.tmpdir()` with permissions `0o600` 3. Pass paths to Playwright `page.setInputFiles()` 4. Delete temp files in `finally` block (guaranteed cleanup even on error) 5. Password is only passed via `page.fill()` — never written to disk Additional: - Playwright runs headless in production (no `slowMo`) - 3-minute timeout per tenant to prevent hanging processes - Temp file names use `crypto.randomUUID()` to avoid collisions ## Cron Schedule ``` '0 4 * * 0' — Sundays 4:00 AM (America/Mexico_City) ``` Runs after the daily SAT sync (3:00 AM) to avoid overlap. Processes tenants sequentially (Playwright is heavy — no parallelism). ### Cron Flow For each active tenant with FIEL configured: 1. Decrypt FIEL → write temp files 2. Launch Playwright headless → login → navigate to report 3. Extract PDF base64 from DOM → parse text 4. INSERT into `opiniones_cumplimiento` 5. DELETE records older than 6 months 6. Cleanup temp files 7. Close browser Error handling: if one tenant fails, log error and continue to next. Don't stop the batch. ## API Endpoints | Method | Route | Auth | Description | |--------|-------|------|-------------| | GET | `/api/documentos/opiniones` | All roles | Last 5 opinions (metadata only, no PDF binary) | | GET | `/api/documentos/opiniones/:id/pdf` | All roles | Download PDF as binary (Content-Type: application/pdf) | | POST | `/api/documentos/opiniones/consultar` | Admin only | Trigger manual download for current tenant | All routes use `tenantMiddleware` + `requireFeature('documentos')`. ### GET /api/documentos/opiniones response ```json [ { "id": 1, "rfc": "HTS240708LJA", "razonSocial": "HORUX 360 SA DE CV", "estatus": "Positiva", "folio": "26NC4144337", "cadenaOriginal": "||HTS240708LJA|26NC4144337|...", "fechaConsulta": "2026-04-13T20:59:00.000Z", "createdAt": "2026-04-13T22:00:00.000Z" } ] ``` ## Auto-Alert New alert in `alertas-auto.service.ts`: ```typescript async function alertaOpinionCumplimiento(pool: Pool): Promise ``` - Queries latest record from `opiniones_cumplimiento` - If `estatus !== 'Positiva'` → returns alert with priority 'alta' - Message: "Tu Opinión de Cumplimiento es {estatus}. Última consulta: {fecha}." - No drill-down (Documentos page shows details) - If no records exist → no alert ## Frontend ### Sidebar ```typescript { name: 'Documentos', href: '/documentos', icon: FileCheck, feature: 'documentos' } ``` Between Facturación and Usuarios in the navigation array. Visible to all roles. ### Page: `/documentos` - Tab structure (future-proof for other document types): first tab "Opinión de Cumplimiento" - Card/row per opinion showing: - Fecha de consulta (formatted) - Estatus badge (green=Positiva, red=Negativa, yellow=others) - Folio - Button to download PDF - "Consultar ahora" button (admin only) triggers POST - Empty state: "No hay opiniones registradas. La consulta automática se ejecuta cada semana." - Loading/error states with React Query ### Dependencies Add to `apps/api/package.json`: - `playwright` (for Chromium automation) - `pdf-parse` v2 (for PDF text extraction) Post-install: `npx playwright install chromium` (needed on deploy) ## Scope Exclusions - Parser for "Negativa" opinion obligations list (refine when sample PDF available) - Email notifications on status change (only auto-alert for now) - Multiple document types in the Documentos page (only Opinión de Cumplimiento in v1) - PDF viewer in browser (download only)