# 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<AlertaAuto | null>
```

- 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)
