187 lines
6.8 KiB
Markdown
187 lines
6.8 KiB
Markdown
# 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)
|