# Refactor Preparatorio del Monorepo — Implementation Plan

> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.

**Goal:** Extraer código compartible del fork Horux_despacho a tres packages nuevos (`@horux/core`, `@horux/vertical-contable`, `@horux/shared-ui`) sin cambiar ninguna funcionalidad del producto, dejando el código listo para construir Horux Despachos encima.

**Architecture:** Monorepo pnpm + Turbo con workspace `packages/*`. Se crean 3 packages nuevos junto al existente `@horux/shared`. Cada package tiene `package.json`, `tsconfig.json`, y barrel `src/index.ts`. Los archivos candidatos se **mueven** (no copian) y los imports en `apps/api` y `apps/web` se actualizan. El producto sigue 100% funcional con idéntico comportamiento.

**Tech Stack:** pnpm 9, Turbo 2, TypeScript 5.3, Node 20, Express 4.21, Next.js 14.2, React 18.3, Tailwind 3.4, Radix UI, Prisma 5.22, facturapi 4.14, playwright 1.59, nodemailer 8.0, mercadopago 2.12.

**Validación principal:** `pnpm typecheck` tras cada cambio. No hay framework de tests instalado hoy — agregarlo es un proyecto aparte. Smoke test manual al final.

**Git discipline:** Repo local con `.git` existente; commits frecuentes como puntos de rollback. **NO se hace `git push`** (aún no hay remote). El usuario validó este modo ya.

---

## Contexto previo crítico

- Working directory: `C:\Users\chtr1\Downloads\Horux_despacho`
- Shell: bash/mingw en Windows — usa barras `/` y sintaxis POSIX.
- pnpm workspaces ya configuradas: `pnpm-workspace.yaml` lista `apps/*` y `packages/*`.
- Package existente `@horux/shared` en `packages/shared/` con tipos Zod. **NO modificar.**
- Horux360 sigue en producción desde el branch `main` — cualquier cambio en `apps/api` o `apps/web` debe compilar con `pnpm typecheck` al terminar cada task.

---

## Mapa de archivos (visión global)

**Se crean:**
- `packages/core/` — JWT helpers, bcrypt, AES-256-GCM, rate-limit middleware, email transport + templates base.
- `packages/vertical-contable/` — FacturapiClient, SAT services (client, parser, downloader, Opinión, CSF), parser CFDI, motor IVA/ISR, catálogos SAT, alertas automáticas fiscales.
- `packages/shared-ui/` — Primitives UI (Button, Input, Card, Dialog, Select, Tabs, Label, Popover, SortableHeader), form selectors (Period, Regimen, FiscalDisclaimer), layouts (DashboardShell, Header, Sidebar), hooks (useDebounce, useTableSort), charts base (KpiCard, BarChart).

**Se mueven (origen → destino):**

| Origen | Destino |
|--------|---------|
| `apps/api/src/utils/token.ts` | `packages/core/src/auth/token.ts` |
| `apps/api/src/utils/password.ts` | `packages/core/src/auth/password.ts` |
| `apps/api/src/middlewares/rate-limit.middleware.ts` | `packages/core/src/middleware/rate-limit.ts` |
| `apps/api/src/services/email/email.service.ts` | `packages/core/src/email/transport.ts` |
| `apps/api/src/services/email/templates/base.ts` | `packages/core/src/email/templates/base.ts` |
| `apps/api/src/services/email/templates/welcome.ts` | `packages/core/src/email/templates/welcome.ts` |
| `apps/api/src/services/email/templates/password-reset.ts` | `packages/core/src/email/templates/password-reset.ts` |
| `apps/api/src/services/fiel.service.ts` (solo helpers crypto) | `packages/core/src/crypto/aes-gcm.ts` |
| `apps/api/src/services/facturapi.service.ts` | `packages/vertical-contable/src/facturapi/client.ts` |
| `apps/api/src/services/sat/sat-client.service.ts` | `packages/vertical-contable/src/sat/client.ts` |
| `apps/api/src/services/sat/sat-parser.service.ts` | `packages/vertical-contable/src/sat/parser.ts` |
| `apps/api/src/services/sat/sat-download.service.ts` | `packages/vertical-contable/src/sat/downloader.ts` |
| `apps/api/src/services/sat/sat-opinion-login.ts` | `packages/vertical-contable/src/sat/opinion/login.ts` |
| `apps/api/src/services/sat/sat-opinion-scraper.ts` | `packages/vertical-contable/src/sat/opinion/scraper.ts` |
| `apps/api/src/services/sat/sat-opinion-parser.ts` | `packages/vertical-contable/src/sat/opinion/parser.ts` |
| `apps/api/src/services/sat/sat-csf-login.ts` | `packages/vertical-contable/src/sat/csf/login.ts` |
| `apps/api/src/services/sat/sat-csf-scraper.ts` | `packages/vertical-contable/src/sat/csf/scraper.ts` |
| `apps/api/src/services/sat/sat-csf-parser.ts` | `packages/vertical-contable/src/sat/csf/parser.ts` |
| `apps/api/src/services/sat/sat-crypto.service.ts` | `packages/vertical-contable/src/sat/crypto.ts` |
| `apps/api/src/services/cfdi.service.ts` (solo parser + CFDI_SELECT const) | `packages/vertical-contable/src/cfdi/parser.ts` |
| `apps/api/src/services/impuestos.service.ts` (lógica pura de cálculo) | `packages/vertical-contable/src/impuestos/iva.ts` + `isr.ts` |
| `apps/web/components/ui/*.tsx` (9 archivos) | `packages/shared-ui/src/primitives/*.tsx` |
| `apps/web/components/period-selector.tsx` | `packages/shared-ui/src/form/period-selector.tsx` |
| `apps/web/components/regimen-selector.tsx` | `packages/shared-ui/src/form/regimen-selector.tsx` |
| `apps/web/components/fiscal-disclaimer.tsx` | `packages/shared-ui/src/form/fiscal-disclaimer.tsx` |
| `apps/web/components/layouts/dashboard-shell.tsx` | `packages/shared-ui/src/layout/dashboard-shell.tsx` |
| `apps/web/components/layouts/header.tsx` | `packages/shared-ui/src/layout/header.tsx` |
| `apps/web/components/layouts/sidebar.tsx` | `packages/shared-ui/src/layout/sidebar.tsx` |
| `apps/web/lib/hooks/use-debounce.ts` | `packages/shared-ui/src/hooks/use-debounce.ts` |
| `apps/web/lib/hooks/use-table-sort.ts` | `packages/shared-ui/src/hooks/use-table-sort.ts` |
| `apps/web/components/charts/kpi-card.tsx` | `packages/shared-ui/src/charts/kpi-card.tsx` |
| `apps/web/components/charts/bar-chart.tsx` | `packages/shared-ui/src/charts/bar-chart.tsx` |
| `apps/web/lib/utils.ts` (helper `cn`) | `packages/shared-ui/src/lib/cn.ts` |

**Se modifican (solo imports):**
- Todos los archivos de `apps/api/src/**` que importan de los orígenes listados.
- Todos los archivos de `apps/web/**` que importan de los orígenes listados.

**Se configura (nuevo):**
- `apps/web/tailwind.config.js` — agregar `content` para incluir `packages/shared-ui/src/**`.

---

## Tasks

### Task 1: Scaffold del package @horux/core

**Files:**
- Create: `packages/core/package.json`
- Create: `packages/core/tsconfig.json`
- Create: `packages/core/src/index.ts`

- [ ] **Step 1: Crear `packages/core/package.json`**

```json
{
  "name": "@horux/core",
  "version": "0.0.1",
  "private": true,
  "main": "./src/index.ts",
  "types": "./src/index.ts",
  "scripts": {
    "lint": "eslint src/",
    "typecheck": "tsc --noEmit"
  },
  "dependencies": {
    "@horux/shared": "workspace:*",
    "bcryptjs": "^2.4.3",
    "jsonwebtoken": "^9.0.2",
    "nodemailer": "^8.0.0",
    "express": "^4.21.0",
    "zod": "^3.23.8"
  },
  "devDependencies": {
    "@types/bcryptjs": "^2.4.6",
    "@types/jsonwebtoken": "^9.0.7",
    "@types/nodemailer": "^6.4.17",
    "@types/express": "^5.0.0",
    "typescript": "^5.3.0"
  }
}
```

- [ ] **Step 2: Crear `packages/core/tsconfig.json`**

```json
{
  "compilerOptions": {
    "target": "ES2022",
    "module": "ESNext",
    "moduleResolution": "bundler",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,
    "declaration": true,
    "resolveJsonModule": true,
    "allowSyntheticDefaultImports": true,
    "isolatedModules": true
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules", "dist"]
}
```

- [ ] **Step 3: Crear barrel `packages/core/src/index.ts` vacío con placeholder**

```typescript
// Barrel para @horux/core. Se irá poblando conforme se migren módulos.
export {};
```

- [ ] **Step 4: Instalar dependencias del workspace**

Run: `cd /c/Users/chtr1/Downloads/Horux_despacho && pnpm install`
Expected: "Done in X.Xs" sin errores. Verifica que aparezca `packages/core` en el install log.

- [ ] **Step 5: Verificar typecheck del nuevo package**

Run: `pnpm --filter @horux/core typecheck`
Expected: sin errores (solo compila el barrel vacío).

- [ ] **Step 6: Commit**

```bash
git add packages/core/ pnpm-lock.yaml
git commit -m "refactor(monorepo): scaffold @horux/core package"
```

---

### Task 2: Scaffold del package @horux/vertical-contable

**Files:**
- Create: `packages/vertical-contable/package.json`
- Create: `packages/vertical-contable/tsconfig.json`
- Create: `packages/vertical-contable/src/index.ts`

- [ ] **Step 1: Crear `packages/vertical-contable/package.json`**

```json
{
  "name": "@horux/vertical-contable",
  "version": "0.0.1",
  "private": true,
  "main": "./src/index.ts",
  "types": "./src/index.ts",
  "scripts": {
    "lint": "eslint src/",
    "typecheck": "tsc --noEmit"
  },
  "dependencies": {
    "@horux/shared": "workspace:*",
    "@horux/core": "workspace:*",
    "facturapi": "^4.14.0",
    "playwright": "^1.59.0",
    "zod": "^3.23.8"
  },
  "devDependencies": {
    "typescript": "^5.3.0"
  }
}
```

- [ ] **Step 2: Crear `packages/vertical-contable/tsconfig.json`**

```json
{
  "compilerOptions": {
    "target": "ES2022",
    "module": "ESNext",
    "moduleResolution": "bundler",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,
    "declaration": true,
    "resolveJsonModule": true,
    "allowSyntheticDefaultImports": true,
    "isolatedModules": true
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules", "dist"]
}
```

- [ ] **Step 3: Crear barrel `packages/vertical-contable/src/index.ts` vacío**

```typescript
// Barrel para @horux/vertical-contable. Se irá poblando conforme se migren módulos fiscales.
export {};
```

- [ ] **Step 4: Instalar dependencias y verificar typecheck**

Run: `pnpm install && pnpm --filter @horux/vertical-contable typecheck`
Expected: sin errores.

- [ ] **Step 5: Commit**

```bash
git add packages/vertical-contable/ pnpm-lock.yaml
git commit -m "refactor(monorepo): scaffold @horux/vertical-contable package"
```

---

### Task 3: Scaffold del package @horux/shared-ui

**Files:**
- Create: `packages/shared-ui/package.json`
- Create: `packages/shared-ui/tsconfig.json`
- Create: `packages/shared-ui/src/index.ts`
- Create: `packages/shared-ui/tailwind-preset.js`

- [ ] **Step 1: Crear `packages/shared-ui/package.json`**

```json
{
  "name": "@horux/shared-ui",
  "version": "0.0.1",
  "private": true,
  "main": "./src/index.ts",
  "types": "./src/index.ts",
  "scripts": {
    "lint": "eslint src/",
    "typecheck": "tsc --noEmit"
  },
  "dependencies": {
    "@horux/shared": "workspace:*",
    "@radix-ui/react-dialog": "^1.1.2",
    "@radix-ui/react-label": "^2.1.0",
    "@radix-ui/react-popover": "^1.1.2",
    "@radix-ui/react-select": "^2.1.2",
    "@radix-ui/react-slot": "^1.1.0",
    "@radix-ui/react-tabs": "^1.1.1",
    "class-variance-authority": "^0.7.0",
    "clsx": "^2.1.1",
    "lucide-react": "^0.453.0",
    "react": "^18.3.1",
    "recharts": "^2.12.7",
    "tailwind-merge": "^2.5.4"
  },
  "peerDependencies": {
    "react": "^18.3.1"
  },
  "devDependencies": {
    "@types/react": "^18.3.12",
    "typescript": "^5.3.0"
  }
}
```

- [ ] **Step 2: Crear `packages/shared-ui/tsconfig.json`**

```json
{
  "compilerOptions": {
    "target": "ES2022",
    "module": "ESNext",
    "moduleResolution": "bundler",
    "jsx": "preserve",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,
    "declaration": true,
    "resolveJsonModule": true,
    "allowSyntheticDefaultImports": true,
    "isolatedModules": true,
    "lib": ["DOM", "DOM.Iterable", "ES2022"]
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules", "dist"]
}
```

- [ ] **Step 3: Crear barrel `packages/shared-ui/src/index.ts` vacío**

```typescript
// Barrel para @horux/shared-ui. Se irá poblando conforme se migren componentes.
export {};
```

- [ ] **Step 4: Crear preset Tailwind compartido en `packages/shared-ui/tailwind-preset.js`**

Este preset define los tokens (colores, tipografía) que usan los componentes del package; los apps que consumen extienden de este preset para tener consistencia.

```javascript
/** @type {import('tailwindcss').Config} */
module.exports = {
  theme: {
    extend: {
      colors: {
        border: "hsl(var(--border))",
        input: "hsl(var(--input))",
        ring: "hsl(var(--ring))",
        background: "hsl(var(--background))",
        foreground: "hsl(var(--foreground))",
        primary: {
          DEFAULT: "hsl(var(--primary))",
          foreground: "hsl(var(--primary-foreground))",
        },
        secondary: {
          DEFAULT: "hsl(var(--secondary))",
          foreground: "hsl(var(--secondary-foreground))",
        },
        destructive: {
          DEFAULT: "hsl(var(--destructive))",
          foreground: "hsl(var(--destructive-foreground))",
        },
        muted: {
          DEFAULT: "hsl(var(--muted))",
          foreground: "hsl(var(--muted-foreground))",
        },
        accent: {
          DEFAULT: "hsl(var(--accent))",
          foreground: "hsl(var(--accent-foreground))",
        },
        popover: {
          DEFAULT: "hsl(var(--popover))",
          foreground: "hsl(var(--popover-foreground))",
        },
        card: {
          DEFAULT: "hsl(var(--card))",
          foreground: "hsl(var(--card-foreground))",
        },
      },
      fontFamily: {
        sans: ["Inter", "sans-serif"],
      },
      borderRadius: {
        lg: "var(--radius)",
        md: "calc(var(--radius) - 2px)",
        sm: "calc(var(--radius) - 4px)",
      },
    },
  },
  plugins: [],
};
```

- [ ] **Step 5: Instalar dependencias y verificar typecheck**

Run: `pnpm install && pnpm --filter @horux/shared-ui typecheck`
Expected: sin errores.

- [ ] **Step 6: Commit**

```bash
git add packages/shared-ui/ pnpm-lock.yaml
git commit -m "refactor(monorepo): scaffold @horux/shared-ui package"
```

---

### Task 4: Migrar auth helpers (token + password) a @horux/core

**Files:**
- Create: `packages/core/src/auth/token.ts` (mover desde `apps/api/src/utils/token.ts`)
- Create: `packages/core/src/auth/password.ts` (mover desde `apps/api/src/utils/password.ts`)
- Create: `packages/core/src/auth/index.ts`
- Modify: `packages/core/src/index.ts`
- Modify: `apps/api/src/utils/token.ts` (se elimina, reemplazar imports)
- Modify: `apps/api/src/utils/password.ts` (se elimina, reemplazar imports)
- Modify: todos los archivos de `apps/api/src/**` que importan `'../utils/token'` o `'../utils/password'` o con paths equivalentes.

- [ ] **Step 1: Copiar `apps/api/src/utils/token.ts` a `packages/core/src/auth/token.ts` sin cambios al código interno**

Run (bash):
```bash
cp apps/api/src/utils/token.ts packages/core/src/auth/token.ts
```

Luego abre `packages/core/src/auth/token.ts` y ajusta los imports top-of-file:
- Si importa de `../config/env`, cámbialo a `import { env } from "../config/env"` — **PERO** `env.ts` no se mueve aquí (depende de env-vars específicas del apps/api). Solución: aceptar las variables como parámetros del signer. Refactor necesario:

Antes (`apps/api/src/utils/token.ts` top):
```typescript
import { env } from "../config/env";
// uso: env.JWT_SECRET, env.JWT_EXPIRES_IN
```

Después (`packages/core/src/auth/token.ts`):
```typescript
// Sin import de env — las secrets se pasan como parámetros.

export interface TokenConfig {
  accessSecret: string;
  refreshSecret: string;
  accessExpiresIn: string;      // "15m"
  refreshExpiresIn: string;     // "7d"
}

// Adaptar cada función existente para recibir `config: TokenConfig`:
// Ej. antes: generateAccessToken(payload) -> después: generateAccessToken(payload, config)
```

**Regla de migración**: en lugar de acoplar `@horux/core` al env de apps/api, las funciones reciben sus secrets como parámetros. El apps/api construye un `tokenConfig` desde `env` y lo pasa.

- [ ] **Step 2: Crear `packages/core/src/auth/password.ts`**

```bash
cp apps/api/src/utils/password.ts packages/core/src/auth/password.ts
```

Ajustar imports si hay alguno (bcryptjs es dep directa del package). No debería haber dependencias del apps/api.

- [ ] **Step 3: Crear barrel `packages/core/src/auth/index.ts`**

```typescript
export * from "./token";
export * from "./password";
```

- [ ] **Step 4: Actualizar barrel `packages/core/src/index.ts`**

```typescript
export * from "./auth";
```

- [ ] **Step 5: Verificar typecheck del package**

Run: `pnpm --filter @horux/core typecheck`
Expected: sin errores.

- [ ] **Step 6: Crear wrapper en apps/api que construye el `TokenConfig` y re-exporta las funciones con config inyectado**

Create: `apps/api/src/auth/tokens.ts`

```typescript
import {
  generateAccessToken as coreGenerateAccessToken,
  generateRefreshToken as coreGenerateRefreshToken,
  verifyToken as coreVerifyToken,
  decodeToken,
  type TokenConfig,
  type JWTPayload,
  type RefreshTokenPayload,
} from "@horux/core";
import { env } from "../config/env";

const tokenConfig: TokenConfig = {
  accessSecret: env.JWT_SECRET,
  refreshSecret: env.JWT_REFRESH_SECRET,
  accessExpiresIn: env.JWT_EXPIRES_IN,
  refreshExpiresIn: env.JWT_REFRESH_EXPIRES_IN,
};

export function generateAccessToken(payload: JWTPayload) {
  return coreGenerateAccessToken(payload, tokenConfig);
}

export function generateRefreshToken(payload: RefreshTokenPayload) {
  return coreGenerateRefreshToken(payload, tokenConfig);
}

export function verifyAccessToken(token: string) {
  return coreVerifyToken(token, tokenConfig.accessSecret);
}

export function verifyRefreshToken(token: string) {
  return coreVerifyToken(token, tokenConfig.refreshSecret);
}

export { decodeToken };
export type { JWTPayload, RefreshTokenPayload };
```

- [ ] **Step 7: Reemplazar todos los imports de `utils/token` en apps/api**

Find all imports: `grep -rn "from ['\"].*utils/token" apps/api/src/` (mentalmente — usa Grep tool).

Para cada archivo encontrado, reemplazar:
- `from "../utils/token"` → `from "../auth/tokens"` (ajustando path relativo según carpeta del archivo).
- `from "../../utils/token"` → `from "../../auth/tokens"`

Ejemplo en `apps/api/src/controllers/auth.controller.ts`:
```typescript
// Antes:
import { generateAccessToken, generateRefreshToken } from "../utils/token";

// Después:
import { generateAccessToken, generateRefreshToken } from "../auth/tokens";
```

- [ ] **Step 8: Mover/adaptar `apps/api/src/utils/password.ts` al mismo patrón**

Create: `apps/api/src/auth/passwords.ts`

```typescript
export { hashPassword, verifyPassword } from "@horux/core";
```

Reemplazar imports de `../utils/password` → `../auth/passwords` (o `../../auth/passwords` según path relativo).

- [ ] **Step 9: Eliminar los archivos antiguos**

```bash
rm apps/api/src/utils/token.ts
rm apps/api/src/utils/password.ts
```

- [ ] **Step 10: Agregar `@horux/core` a deps de apps/api**

Modify: `apps/api/package.json`

En `"dependencies"`, agregar:
```json
"@horux/core": "workspace:*",
```

Run: `pnpm install`

- [ ] **Step 11: Verificar typecheck completo**

Run: `pnpm --filter @horux/api typecheck`
Expected: sin errores. Si hay errores de import path, ajustarlos según la carpeta del archivo.

- [ ] **Step 12: Commit**

```bash
git add packages/core/ apps/api/
git commit -m "refactor(api): move auth helpers (token, password) to @horux/core"
```

---

### Task 5: Migrar rate-limit middleware a @horux/core

**Files:**
- Create: `packages/core/src/middleware/rate-limit.ts`
- Create: `packages/core/src/middleware/index.ts`
- Modify: `packages/core/src/index.ts`
- Delete: `apps/api/src/middlewares/rate-limit.middleware.ts`
- Modify: `apps/api/src/routes/**` (imports del middleware)

- [ ] **Step 1: Copiar middleware y adaptar**

```bash
cp apps/api/src/middlewares/rate-limit.middleware.ts packages/core/src/middleware/rate-limit.ts
```

Abre `packages/core/src/middleware/rate-limit.ts`:
- Verifica que NO haya imports de `../config/env` u otros módulos de apps/api. Si los hay, convertirlos en parámetros de una factory function.
- Los tiers (`veryStrict`, `strict`, `normal`, `relaxed`) deben exportarse como funciones independientes si están como instancias, o como una factory que acepta config.

Patrón recomendado si el middleware usa env-vars:

```typescript
import type { Request, Response, NextFunction } from "express";

export interface RateLimitConfig {
  windowMs: number;
  max: number;
  keyResolver?: (req: Request) => string;
}

export function createRateLimit(config: RateLimitConfig) {
  const hits = new Map<string, { count: number; resetAt: number }>();
  return (req: Request, res: Response, next: NextFunction) => {
    const key = config.keyResolver ? config.keyResolver(req) : req.ip ?? "unknown";
    const now = Date.now();
    const entry = hits.get(key);
    if (!entry || now > entry.resetAt) {
      hits.set(key, { count: 1, resetAt: now + config.windowMs });
      return next();
    }
    if (entry.count >= config.max) {
      return res.status(429).json({ error: "Too many requests" });
    }
    entry.count += 1;
    next();
  };
}

// Helpers con tiers predefinidos para consumidores que no quieren configurar manualmente:
export const rateLimitTiers = {
  veryStrict: () => createRateLimit({ windowMs: 60_000, max: 3 }),
  strict:     () => createRateLimit({ windowMs: 60_000, max: 5 }),
  normal:     () => createRateLimit({ windowMs: 60_000, max: 30 }),
  relaxed:    () => createRateLimit({ windowMs: 60_000, max: 100 }),
};
```

*(Los valores windowMs/max deben coincidir con los valores que tenía el archivo original. Ajustar si difieren.)*

- [ ] **Step 2: Crear barrel `packages/core/src/middleware/index.ts`**

```typescript
export * from "./rate-limit";
```

- [ ] **Step 3: Actualizar barrel `packages/core/src/index.ts`**

```typescript
export * from "./auth";
export * from "./middleware";
```

- [ ] **Step 4: Verificar typecheck**

Run: `pnpm --filter @horux/core typecheck`
Expected: sin errores.

- [ ] **Step 5: Reemplazar imports en apps/api**

Busca en `apps/api/src/routes/**` imports de `../middlewares/rate-limit.middleware` o similares.

Ejemplo:
```typescript
// Antes:
import { strictRateLimit } from "../middlewares/rate-limit.middleware";
app.post("/auth/login", strictRateLimit, loginHandler);

// Después:
import { rateLimitTiers } from "@horux/core";
app.post("/auth/login", rateLimitTiers.strict(), loginHandler);
```

**Atención**: si el código legacy importa instancias exportadas directas (no factories), adapta a factory `rateLimitTiers.strict()` al uso.

- [ ] **Step 6: Eliminar archivo antiguo**

```bash
rm apps/api/src/middlewares/rate-limit.middleware.ts
```

- [ ] **Step 7: Verificar typecheck completo**

Run: `pnpm --filter @horux/api typecheck`
Expected: sin errores.

- [ ] **Step 8: Commit**

```bash
git add packages/core/ apps/api/
git commit -m "refactor(api): move rate-limit middleware to @horux/core"
```

---

### Task 6: Migrar email transport + templates base a @horux/core

**Files:**
- Create: `packages/core/src/email/transport.ts`
- Create: `packages/core/src/email/templates/base.ts`
- Create: `packages/core/src/email/templates/welcome.ts`
- Create: `packages/core/src/email/templates/password-reset.ts`
- Create: `packages/core/src/email/index.ts`
- Modify: `packages/core/src/index.ts`
- Delete: `apps/api/src/services/email/email.service.ts`
- Delete: `apps/api/src/services/email/templates/base.ts`
- Delete: `apps/api/src/services/email/templates/welcome.ts`
- Delete: `apps/api/src/services/email/templates/password-reset.ts`
- Modify: `apps/api/src/services/email/**` (resto de templates actualizan import)

- [ ] **Step 1: Copiar email transport**

```bash
cp apps/api/src/services/email/email.service.ts packages/core/src/email/transport.ts
```

Abre `packages/core/src/email/transport.ts`:
- Convierte la config SMTP (host, port, user, password, from) en parámetros de una **factory function** `createEmailTransport(config: EmailTransportConfig)`.

```typescript
import nodemailer, { type Transporter } from "nodemailer";

export interface EmailTransportConfig {
  host: string;
  port: number;
  secure: boolean;
  user: string;
  password: string;
  from: string;
  enabled: boolean;  // si false, no envía (modo dev/test)
}

export interface EmailMessage {
  to: string;
  subject: string;
  html: string;
  text?: string;
}

export function createEmailTransport(config: EmailTransportConfig) {
  if (!config.enabled) {
    return {
      send: async (msg: EmailMessage) => {
        console.log(`[email:disabled] would send to ${msg.to}: ${msg.subject}`);
      }
    };
  }

  const transporter: Transporter = nodemailer.createTransport({
    host: config.host,
    port: config.port,
    secure: config.secure,
    auth: { user: config.user, pass: config.password },
  });

  return {
    send: async (msg: EmailMessage) => {
      try {
        await transporter.sendMail({
          from: config.from,
          to: msg.to,
          subject: msg.subject,
          html: msg.html,
          text: msg.text,
        });
      } catch (err) {
        console.error(`[email:fail] to ${msg.to}:`, err);
        // fire-and-forget: nunca propagar — el flujo de negocio no debe romperse por email
      }
    }
  };
}
```

- [ ] **Step 2: Copiar templates base, welcome y password-reset**

```bash
cp apps/api/src/services/email/templates/base.ts packages/core/src/email/templates/base.ts
cp apps/api/src/services/email/templates/welcome.ts packages/core/src/email/templates/welcome.ts
cp apps/api/src/services/email/templates/password-reset.ts packages/core/src/email/templates/password-reset.ts
```

Revisar que ninguno importe de `../email.service` o similar — si lo hacen, ajustar al nuevo barrel local.

- [ ] **Step 3: Crear barrel `packages/core/src/email/index.ts`**

```typescript
export * from "./transport";
export * from "./templates/base";
export * from "./templates/welcome";
export * from "./templates/password-reset";
```

- [ ] **Step 4: Actualizar barrel `packages/core/src/index.ts`**

```typescript
export * from "./auth";
export * from "./middleware";
export * from "./email";
```

- [ ] **Step 5: Crear wrapper en apps/api**

Create: `apps/api/src/services/email/transport.ts`

```typescript
import { createEmailTransport } from "@horux/core";
import { env } from "../../config/env";

export const emailTransport = createEmailTransport({
  host: env.SMTP_HOST,
  port: env.SMTP_PORT,
  secure: env.SMTP_SECURE,
  user: env.SMTP_USER,
  password: env.SMTP_PASSWORD,
  from: env.SMTP_FROM,
  enabled: env.EMAIL_ENABLED,
});
```

- [ ] **Step 6: Eliminar archivos antiguos**

```bash
rm apps/api/src/services/email/email.service.ts
rm apps/api/src/services/email/templates/base.ts
rm apps/api/src/services/email/templates/welcome.ts
rm apps/api/src/services/email/templates/password-reset.ts
```

- [ ] **Step 7: Actualizar imports en consumidores**

Los demás templates que existan en `apps/api/src/services/email/templates/*` (ej. `payment-confirmed.ts`, `payment-failed.ts`, etc.) probablemente importan del template base. Cambiar:

```typescript
// Antes:
import { baseLayout } from "./base";

// Después:
import { baseLayout } from "@horux/core";
```

Los controladores/servicios que llaman `sendEmail(...)`:
```typescript
// Antes:
import { sendEmail } from "../../services/email/email.service";

// Después:
import { emailTransport } from "../../services/email/transport";
emailTransport.send({ to, subject, html });
```

- [ ] **Step 8: Verificar typecheck completo**

Run: `pnpm --filter @horux/core typecheck && pnpm --filter @horux/api typecheck`
Expected: sin errores.

- [ ] **Step 9: Commit**

```bash
git add packages/core/ apps/api/
git commit -m "refactor(api): move email transport + base templates to @horux/core"
```

---

### Task 7: Migrar crypto helpers (AES-256-GCM) a @horux/core

**Files:**
- Create: `packages/core/src/crypto/aes-gcm.ts`
- Create: `packages/core/src/crypto/index.ts`
- Modify: `packages/core/src/index.ts`
- Modify: `apps/api/src/services/fiel.service.ts` (refactor para usar @horux/core para la parte crypto; el resto sigue ahí)

- [ ] **Step 1: Identificar funciones crypto en `fiel.service.ts`**

Abre `apps/api/src/services/fiel.service.ts`. Identifica las funciones puramente criptográficas (sin lógica de negocio de FIEL):
- `encryptAesGcm(plaintext: Buffer | string, key: Buffer): { ciphertext, iv, tag }`
- `decryptAesGcm(ciphertext: Buffer, iv: Buffer, tag: Buffer, key: Buffer): Buffer`

Estas funciones no saben de FIEL ni de Postgres — son helpers puros AES-GCM.

- [ ] **Step 2: Crear `packages/core/src/crypto/aes-gcm.ts`** con dichas funciones puras

```typescript
import crypto from "node:crypto";

export interface EncryptedPayload {
  ciphertext: Buffer;
  iv: Buffer;
  authTag: Buffer;
}

/**
 * Cifra con AES-256-GCM usando llave de 32 bytes.
 * IV aleatorio de 12 bytes generado en cada llamada.
 */
export function encryptAesGcm(plaintext: Buffer, key: Buffer): EncryptedPayload {
  if (key.length !== 32) {
    throw new Error("AES-256-GCM requires a 32-byte key");
  }
  const iv = crypto.randomBytes(12);
  const cipher = crypto.createCipheriv("aes-256-gcm", key, iv);
  const ciphertext = Buffer.concat([cipher.update(plaintext), cipher.final()]);
  const authTag = cipher.getAuthTag();
  return { ciphertext, iv, authTag };
}

/**
 * Descifra AES-256-GCM. Lanza si el authTag no valida (integridad rota).
 */
export function decryptAesGcm(
  payload: EncryptedPayload,
  key: Buffer
): Buffer {
  if (key.length !== 32) {
    throw new Error("AES-256-GCM requires a 32-byte key");
  }
  const decipher = crypto.createDecipheriv("aes-256-gcm", key, payload.iv);
  decipher.setAuthTag(payload.authTag);
  return Buffer.concat([
    decipher.update(payload.ciphertext),
    decipher.final(),
  ]);
}
```

- [ ] **Step 3: Crear barrel `packages/core/src/crypto/index.ts`**

```typescript
export * from "./aes-gcm";
```

- [ ] **Step 4: Actualizar barrel `packages/core/src/index.ts`**

```typescript
export * from "./auth";
export * from "./middleware";
export * from "./email";
export * from "./crypto";
```

- [ ] **Step 5: Verificar typecheck del package**

Run: `pnpm --filter @horux/core typecheck`
Expected: sin errores.

- [ ] **Step 6: Refactorear `apps/api/src/services/fiel.service.ts`**

Reemplazar las funciones crypto internas por imports de `@horux/core`. El archivo sigue existiendo porque contiene lógica específica de FIEL (subir/descargar, validar, guardar en BD, calcular expiración). Solo se saca la parte crypto pura.

```typescript
// Antes (al inicio del archivo):
function encryptAesGcm(...) { ... }
function decryptAesGcm(...) { ... }

// Después:
import { encryptAesGcm, decryptAesGcm } from "@horux/core";
```

Verificar que todas las llamadas locales a esas funciones sigan funcionando (mismo contrato).

- [ ] **Step 7: Verificar typecheck**

Run: `pnpm --filter @horux/api typecheck`
Expected: sin errores.

- [ ] **Step 8: Commit**

```bash
git add packages/core/ apps/api/src/services/fiel.service.ts
git commit -m "refactor(core): extract AES-GCM helpers to @horux/core"
```

---

### Task 8: Migrar FacturapiClient a @horux/vertical-contable

**Files:**
- Create: `packages/vertical-contable/src/facturapi/client.ts`
- Create: `packages/vertical-contable/src/facturapi/index.ts`
- Modify: `packages/vertical-contable/src/index.ts`
- Delete: `apps/api/src/services/facturapi.service.ts`
- Modify: `apps/api/src/controllers/facturacion.controller.ts` y otros consumidores

- [ ] **Step 1: Copiar service a nuevo path**

```bash
cp apps/api/src/services/facturapi.service.ts packages/vertical-contable/src/facturapi/client.ts
```

- [ ] **Step 2: Refactorear `client.ts`**

Abre `packages/vertical-contable/src/facturapi/client.ts`:
- Identifica imports de `../config/env` — reemplaza la API key por parámetro del constructor (patrón consistente con §7.7 del spec "FacturapiClient multi-cuenta ready").
- Exporta una **factory** `createFacturapiClient(apiKey: string)` en lugar de singleton.
- Todas las funciones (createOrganization, getOrganizationStatus, emitInvoice, cancelInvoice, etc.) pasan a ser métodos del cliente retornado.

Patrón:

```typescript
import Facturapi from "facturapi";

export interface FacturapiClient {
  createOrganization(params: CreateOrganizationParams): Promise<Organization>;
  getOrganizationStatus(orgId: string): Promise<OrganizationStatus>;
  emitInvoice(orgId: string, data: InvoiceData): Promise<CfdiResponse>;
  cancelInvoice(orgId: string, invoiceId: string, motivo: string, folioSustitucion?: string): Promise<void>;
  // ... resto de métodos heredados del service original
}

export function createFacturapiClient(apiKey: string): FacturapiClient {
  const fp = new Facturapi(apiKey);

  return {
    async createOrganization(params) {
      // ... copy lógica del original
    },
    async getOrganizationStatus(orgId) {
      // ...
    },
    async emitInvoice(orgId, data) {
      // ...
    },
    async cancelInvoice(orgId, invoiceId, motivo, folioSustitucion) {
      // ...
    },
    // ...
  };
}

export type { Organization, CreateOrganizationParams, InvoiceData, CfdiResponse, OrganizationStatus };
```

Si el código original usa helpers internos (p.ej. `getOrgClient(orgId)`), estos se convierten en privados dentro de la factory function.

- [ ] **Step 3: Crear barrel `packages/vertical-contable/src/facturapi/index.ts`**

```typescript
export * from "./client";
```

- [ ] **Step 4: Actualizar barrel `packages/vertical-contable/src/index.ts`**

```typescript
export * from "./facturapi";
```

- [ ] **Step 5: Verificar typecheck del package**

Run: `pnpm --filter @horux/vertical-contable typecheck`
Expected: sin errores. Si hay errores por imports faltantes (types de facturapi), ajustar.

- [ ] **Step 6: Agregar `@horux/vertical-contable` a deps de apps/api**

Modify: `apps/api/package.json`, agregar a dependencies:

```json
"@horux/vertical-contable": "workspace:*",
```

Run: `pnpm install`

- [ ] **Step 7: Crear wrapper en apps/api**

Create: `apps/api/src/services/facturapi.ts`

```typescript
import { createFacturapiClient } from "@horux/vertical-contable";
import { env } from "../config/env";

export const facturapi = createFacturapiClient(env.FACTURAPI_API_KEY);

// Re-export types para que consumidores importen solo de este wrapper local.
export type {
  Organization,
  CreateOrganizationParams,
  InvoiceData,
  CfdiResponse,
  OrganizationStatus,
  FacturapiClient,
} from "@horux/vertical-contable";
```

- [ ] **Step 8: Reemplazar imports de `facturapi.service` en apps/api**

Buscar todos los archivos que importen de `../services/facturapi.service` o `../../services/facturapi.service`. Reemplazar por `../services/facturapi` (wrapper nuevo).

Ejemplo en `apps/api/src/controllers/facturacion.controller.ts`:

```typescript
// Antes:
import { emitInvoice, cancelInvoice } from "../services/facturapi.service";

// Después:
import { facturapi } from "../services/facturapi";
// ... en el código:
const cfdi = await facturapi.emitInvoice(orgId, data);
await facturapi.cancelInvoice(orgId, invoiceId, motivo);
```

- [ ] **Step 9: Eliminar archivo antiguo**

```bash
rm apps/api/src/services/facturapi.service.ts
```

- [ ] **Step 10: Verificar typecheck**

Run: `pnpm --filter @horux/api typecheck`
Expected: sin errores. Si hay errores de firma en llamadas (methods vs. functions), ajustar el wrapper o los call sites.

- [ ] **Step 11: Commit**

```bash
git add packages/vertical-contable/ apps/api/
git commit -m "refactor(vertical-contable): move FacturapiClient as factory-based API"
```

---

### Task 9: Migrar SAT crypto service a @horux/vertical-contable

**Files:**
- Create: `packages/vertical-contable/src/sat/crypto.ts`
- Modify: `apps/api/src/services/sat/**` que importen de `sat-crypto.service`
- Delete: `apps/api/src/services/sat/sat-crypto.service.ts`

- [ ] **Step 1: Copiar y adaptar**

```bash
mkdir -p packages/vertical-contable/src/sat
cp apps/api/src/services/sat/sat-crypto.service.ts packages/vertical-contable/src/sat/crypto.ts
```

Abre `packages/vertical-contable/src/sat/crypto.ts` y:
- Elimina imports de `../../config/env` u otros internos de apps/api.
- Si hay funciones que necesitan secrets (p.ej. llaves), conviértelas en parámetros.
- Las funciones aquí son utilidades SAT-específicas (firmar con FIEL, generar XML auth, etc.) — reciben los buffers/strings de FIEL como parámetros, no los leen de BD.

- [ ] **Step 2: Agregar barrel parcial**

No crear index.ts de `sat/` aún (se irá poblando en siguientes tasks). Solo export directo desde el barrel raíz cuando todos los módulos SAT estén migrados.

- [ ] **Step 3: Verificar typecheck del package**

Run: `pnpm --filter @horux/vertical-contable typecheck`
Expected: sin errores.

- [ ] **Step 4: Reemplazar imports en apps/api**

Buscar en `apps/api/src/services/sat/**` archivos que importen de `./sat-crypto.service`. Reemplazar:

```typescript
// Antes:
import { signSatRequest, buildAuthXml } from "./sat-crypto.service";

// Después:
import { signSatRequest, buildAuthXml } from "@horux/vertical-contable";
```

*(Asumiendo que `@horux/vertical-contable` ya exporta esos nombres vía barrels; si no, import directo a path: `"@horux/vertical-contable/src/sat/crypto"` — preferir re-export desde el barrel raíz.)*

- [ ] **Step 5: Actualizar barrel `packages/vertical-contable/src/index.ts`**

Añadir temporalmente:

```typescript
export * from "./facturapi";
export * from "./sat/crypto";
```

- [ ] **Step 6: Eliminar archivo antiguo**

```bash
rm apps/api/src/services/sat/sat-crypto.service.ts
```

- [ ] **Step 7: Verificar typecheck**

Run: `pnpm --filter @horux/vertical-contable typecheck && pnpm --filter @horux/api typecheck`
Expected: sin errores.

- [ ] **Step 8: Commit**

```bash
git add packages/vertical-contable/ apps/api/
git commit -m "refactor(vertical-contable): move SAT crypto helpers"
```

---

### Task 10: Migrar SAT services (client, parser, downloader) a @horux/vertical-contable

**Files:**
- Create: `packages/vertical-contable/src/sat/client.ts`
- Create: `packages/vertical-contable/src/sat/parser.ts`
- Create: `packages/vertical-contable/src/sat/downloader.ts`
- Create: `packages/vertical-contable/src/sat/index.ts`
- Modify: `packages/vertical-contable/src/index.ts`
- Modify: `apps/api/src/services/sat/sat.service.ts` (el orquestador queda en apps/api con imports cambiados)
- Delete: `apps/api/src/services/sat/sat-client.service.ts`
- Delete: `apps/api/src/services/sat/sat-parser.service.ts`
- Delete: `apps/api/src/services/sat/sat-download.service.ts`

- [ ] **Step 1: Copiar los tres servicios**

```bash
cp apps/api/src/services/sat/sat-client.service.ts packages/vertical-contable/src/sat/client.ts
cp apps/api/src/services/sat/sat-parser.service.ts packages/vertical-contable/src/sat/parser.ts
cp apps/api/src/services/sat/sat-download.service.ts packages/vertical-contable/src/sat/downloader.ts
```

- [ ] **Step 2: Refactorear `client.ts`**

- Quitar imports de `../../config/env`.
- Las funciones (`querySat`, `verifySatRequest`, `downloadSatPackage`) reciben config HTTP (baseUrl, timeouts) como parámetros si el service original leía de env.
- Si el service usa `fetch` o `axios`, mantener misma lib; solo ajustar imports.

Si el service original es:
```typescript
import { env } from "../../config/env";
const BASE_URL = env.SAT_API_URL;
```

Reemplazar con:
```typescript
// Sin import de env.
export interface SatClientConfig {
  baseUrl: string;
  timeoutMs?: number;
}
export function createSatClient(config: SatClientConfig) {
  return {
    async querySat(params: QueryParams) { /* usa config.baseUrl */ },
    async verifySatRequest(...) { /* ... */ },
    async downloadSatPackage(...) { /* ... */ },
  };
}
```

- [ ] **Step 3: Refactorear `parser.ts`**

Típicamente este archivo no necesita env-vars (solo procesa XML/CSV). Verificar imports y ajustar paths relativos si los hay.

**Importante**: si `parser.ts` tiene la constante `CFDI_SELECT` (el mapeo de campos parser → BD), **dejarla aquí** (es dominio fiscal puro).

- [ ] **Step 4: Refactorear `downloader.ts`**

Mismo patrón: si depende de env, convertir en config de factory.

- [ ] **Step 5: Crear barrel `packages/vertical-contable/src/sat/index.ts`**

```typescript
export * from "./client";
export * from "./parser";
export * from "./downloader";
export * from "./crypto";
```

- [ ] **Step 6: Actualizar barrel raíz `packages/vertical-contable/src/index.ts`**

```typescript
export * from "./facturapi";
export * from "./sat";
```

- [ ] **Step 7: Verificar typecheck del package**

Run: `pnpm --filter @horux/vertical-contable typecheck`
Expected: sin errores.

- [ ] **Step 8: Crear wrapper en apps/api para el cliente SAT**

Create: `apps/api/src/services/sat/sat-client.ts`

```typescript
import { createSatClient } from "@horux/vertical-contable";
import { env } from "../../config/env";

export const satClient = createSatClient({
  baseUrl: env.SAT_API_URL,
  timeoutMs: env.SAT_API_TIMEOUT_MS ?? 30_000,
});
```

- [ ] **Step 9: Actualizar `sat.service.ts` (orquestador que queda en apps/api)**

Abrir `apps/api/src/services/sat/sat.service.ts` y reemplazar imports:

```typescript
// Antes:
import { querySat, verifySatRequest, downloadSatPackage } from "./sat-client.service";
import { processPackage, processMetadataPackage } from "./sat-parser.service";

// Después:
import { satClient } from "./sat-client";
import { processPackage, processMetadataPackage } from "@horux/vertical-contable";

// En el código:
const response = await satClient.querySat(params);
```

- [ ] **Step 10: Eliminar archivos antiguos**

```bash
rm apps/api/src/services/sat/sat-client.service.ts
rm apps/api/src/services/sat/sat-parser.service.ts
rm apps/api/src/services/sat/sat-download.service.ts
```

- [ ] **Step 11: Verificar typecheck**

Run: `pnpm --filter @horux/vertical-contable typecheck && pnpm --filter @horux/api typecheck`
Expected: sin errores.

- [ ] **Step 12: Commit**

```bash
git add packages/vertical-contable/ apps/api/
git commit -m "refactor(vertical-contable): move SAT client/parser/downloader services"
```

---

### Task 11: Migrar SAT Opinión de Cumplimiento (login, scraper, parser) a @horux/vertical-contable

**Files:**
- Create: `packages/vertical-contable/src/sat/opinion/login.ts`
- Create: `packages/vertical-contable/src/sat/opinion/scraper.ts`
- Create: `packages/vertical-contable/src/sat/opinion/parser.ts`
- Create: `packages/vertical-contable/src/sat/opinion/index.ts`
- Modify: `packages/vertical-contable/src/sat/index.ts`
- Modify: `apps/api/src/services/opinion-cumplimiento.service.ts` (orquestador queda en apps/api)
- Delete: `apps/api/src/services/sat/sat-opinion-login.ts`
- Delete: `apps/api/src/services/sat/sat-opinion-scraper.ts`
- Delete: `apps/api/src/services/sat/sat-opinion-parser.ts`

- [ ] **Step 1: Copiar los tres archivos**

```bash
mkdir -p packages/vertical-contable/src/sat/opinion
cp apps/api/src/services/sat/sat-opinion-login.ts packages/vertical-contable/src/sat/opinion/login.ts
cp apps/api/src/services/sat/sat-opinion-scraper.ts packages/vertical-contable/src/sat/opinion/scraper.ts
cp apps/api/src/services/sat/sat-opinion-parser.ts packages/vertical-contable/src/sat/opinion/parser.ts
```

- [ ] **Step 2: Ajustar imports en cada archivo**

Abrir cada uno y:
- Reemplazar `import { ... } from "./sat-crypto.service"` → `import { ... } from "../crypto"`.
- Reemplazar cualquier import de `../../../config/env` por parámetros de funciones factory.
- Mantener imports de `playwright` (es dep directa del package).

- [ ] **Step 3: Crear barrel `packages/vertical-contable/src/sat/opinion/index.ts`**

```typescript
export * from "./login";
export * from "./scraper";
export * from "./parser";
```

- [ ] **Step 4: Actualizar barrel SAT `packages/vertical-contable/src/sat/index.ts`**

```typescript
export * from "./client";
export * from "./parser";
export * from "./downloader";
export * from "./crypto";
export * from "./opinion";
```

- [ ] **Step 5: Verificar typecheck del package**

Run: `pnpm --filter @horux/vertical-contable typecheck`
Expected: sin errores.

- [ ] **Step 6: Actualizar `apps/api/src/services/opinion-cumplimiento.service.ts`**

Reemplazar imports internos por `@horux/vertical-contable`:

```typescript
// Antes:
import { loginSat } from "./sat/sat-opinion-login";
import { scrapeOpinion } from "./sat/sat-opinion-scraper";
import { parseOpinion } from "./sat/sat-opinion-parser";

// Después:
import {
  loginSat,
  scrapeOpinion,
  parseOpinion,
} from "@horux/vertical-contable";
```

- [ ] **Step 7: Eliminar archivos antiguos**

```bash
rm apps/api/src/services/sat/sat-opinion-login.ts
rm apps/api/src/services/sat/sat-opinion-scraper.ts
rm apps/api/src/services/sat/sat-opinion-parser.ts
```

- [ ] **Step 8: Verificar typecheck**

Run: `pnpm --filter @horux/api typecheck`
Expected: sin errores.

- [ ] **Step 9: Commit**

```bash
git add packages/vertical-contable/ apps/api/
git commit -m "refactor(vertical-contable): move SAT Opinión flow (login, scraper, parser)"
```

---

### Task 12: Migrar SAT CSF (login, scraper, parser) a @horux/vertical-contable

**Files:**
- Create: `packages/vertical-contable/src/sat/csf/login.ts`
- Create: `packages/vertical-contable/src/sat/csf/scraper.ts`
- Create: `packages/vertical-contable/src/sat/csf/parser.ts`
- Create: `packages/vertical-contable/src/sat/csf/index.ts`
- Modify: `packages/vertical-contable/src/sat/index.ts`
- Modify: `apps/api/src/services/constancia.service.ts`
- Delete: `apps/api/src/services/sat/sat-csf-login.ts`
- Delete: `apps/api/src/services/sat/sat-csf-scraper.ts`
- Delete: `apps/api/src/services/sat/sat-csf-parser.ts`

- [ ] **Step 1: Copiar los tres archivos**

```bash
mkdir -p packages/vertical-contable/src/sat/csf
cp apps/api/src/services/sat/sat-csf-login.ts packages/vertical-contable/src/sat/csf/login.ts
cp apps/api/src/services/sat/sat-csf-scraper.ts packages/vertical-contable/src/sat/csf/scraper.ts
cp apps/api/src/services/sat/sat-csf-parser.ts packages/vertical-contable/src/sat/csf/parser.ts
```

- [ ] **Step 2: Ajustar imports en cada archivo**

Mismo patrón que Task 11: sustituir `./sat-crypto.service` por `../crypto`; secrets como parámetros; playwright OK.

- [ ] **Step 3: Crear barrel `packages/vertical-contable/src/sat/csf/index.ts`**

```typescript
export * from "./login";
export * from "./scraper";
export * from "./parser";
```

- [ ] **Step 4: Actualizar barrel SAT `packages/vertical-contable/src/sat/index.ts`**

```typescript
export * from "./client";
export * from "./parser";
export * from "./downloader";
export * from "./crypto";
export * from "./opinion";
export * from "./csf";
```

- [ ] **Step 5: Verificar typecheck**

Run: `pnpm --filter @horux/vertical-contable typecheck`
Expected: sin errores.

- [ ] **Step 6: Actualizar `apps/api/src/services/constancia.service.ts`**

```typescript
// Antes:
import { loginCsf } from "./sat/sat-csf-login";
import { scrapeCsf } from "./sat/sat-csf-scraper";
import { parseCsf } from "./sat/sat-csf-parser";

// Después:
import {
  loginCsf,
  scrapeCsf,
  parseCsf,
} from "@horux/vertical-contable";
```

- [ ] **Step 7: Eliminar archivos antiguos**

```bash
rm apps/api/src/services/sat/sat-csf-login.ts
rm apps/api/src/services/sat/sat-csf-scraper.ts
rm apps/api/src/services/sat/sat-csf-parser.ts
```

- [ ] **Step 8: Verificar typecheck**

Run: `pnpm --filter @horux/api typecheck`
Expected: sin errores.

- [ ] **Step 9: Commit**

```bash
git add packages/vertical-contable/ apps/api/
git commit -m "refactor(vertical-contable): move SAT CSF flow (login, scraper, parser)"
```

---

### Task 13: Extraer parser CFDI y constante CFDI_SELECT a @horux/vertical-contable

**Files:**
- Create: `packages/vertical-contable/src/cfdi/parser.ts`
- Create: `packages/vertical-contable/src/cfdi/index.ts`
- Modify: `packages/vertical-contable/src/index.ts`
- Modify: `apps/api/src/services/cfdi.service.ts` (se mantiene el servicio CRUD; solo se extrae el parser XML)

El archivo `apps/api/src/services/cfdi.service.ts` es grande y mixto: tiene el CRUD contra BD tenant (que queda en apps/api) **y** el parser XML con la constante `CFDI_SELECT` (que se va a `@horux/vertical-contable`). Esta task separa ambos.

- [ ] **Step 1: Abrir `apps/api/src/services/cfdi.service.ts` y localizar secciones a extraer**

Buscar:
- La constante `CFDI_SELECT` (mapeo de campos XML → objetos TS para INSERT).
- Funciones puras de parseo: `parseXml(xmlString: string): CfdiParsed`, `extractCfdiFromXml`, normalización de campos.

- [ ] **Step 2: Crear `packages/vertical-contable/src/cfdi/parser.ts`**

Antes de escribir, verifica qué lib de parsing XML usa el service original (abrir `apps/api/src/services/cfdi.service.ts` y revisar imports — típicamente `fast-xml-parser`, `xml2js`, `@nodecfdi/cfdi-parser`, o similar). Usa la **misma lib** y asegúrate que esté declarada como dependencia del package `@horux/vertical-contable` (si no lo está, agrégala en `packages/vertical-contable/package.json` y corre `pnpm install`).

Mover los siguientes elementos al nuevo archivo:

```typescript
// Importar la MISMA lib XML que usa el service original.
// Ejemplo si usa fast-xml-parser:
// import { XMLParser } from "fast-xml-parser";

export const CFDI_SELECT = {
  // ... misma definición que vivía en cfdi.service.ts
};

export interface CfdiParsed {
  uuid: string;
  tipo: string;
  tipoComprobante: string;
  fechaEmision: string;
  rfcEmisor: string;
  rfcReceptor: string;
  // ... resto de campos
}

export function parseXml(xmlString: string): CfdiParsed {
  const parser = new XMLParser({
    ignoreAttributes: false,
    attributeNamePrefix: "@",
  });
  const parsed = parser.parse(xmlString);
  // ... lógica de normalización heredada del service original
  return normalized;
}

export function extractCfdiFromXml(xmlString: string): CfdiParsed {
  return parseXml(xmlString);
}
```

- [ ] **Step 3: Crear barrel `packages/vertical-contable/src/cfdi/index.ts`**

```typescript
export * from "./parser";
```

- [ ] **Step 4: Actualizar barrel raíz**

`packages/vertical-contable/src/index.ts`:

```typescript
export * from "./facturapi";
export * from "./sat";
export * from "./cfdi";
```

- [ ] **Step 5: Verificar typecheck del package**

Run: `pnpm --filter @horux/vertical-contable typecheck`
Expected: sin errores.

- [ ] **Step 6: Actualizar `apps/api/src/services/cfdi.service.ts`**

Eliminar la constante `CFDI_SELECT` y las funciones de parseo **del service**. Reemplazar con import del package:

```typescript
// Top del archivo:
import { CFDI_SELECT, parseXml, extractCfdiFromXml, type CfdiParsed } from "@horux/vertical-contable";

// El resto del servicio (CRUD, filtros, búsqueda) queda igual.
```

- [ ] **Step 7: Verificar typecheck**

Run: `pnpm --filter @horux/api typecheck`
Expected: sin errores.

- [ ] **Step 8: Commit**

```bash
git add packages/vertical-contable/ apps/api/src/services/cfdi.service.ts
git commit -m "refactor(vertical-contable): extract CFDI parser + CFDI_SELECT"
```

---

### Task 14: Extraer motor IVA/ISR a @horux/vertical-contable

**Files:**
- Create: `packages/vertical-contable/src/impuestos/iva.ts`
- Create: `packages/vertical-contable/src/impuestos/isr.ts`
- Create: `packages/vertical-contable/src/impuestos/index.ts`
- Modify: `packages/vertical-contable/src/index.ts`
- Modify: `apps/api/src/services/impuestos.service.ts` (el servicio de orquestación queda; las funciones puras se mueven)

- [ ] **Step 1: Identificar funciones puras en `impuestos.service.ts`**

Funciones candidatas (puras, sin acceso a BD):
- `calculateIvaMensual(cfdis: CfdiInput[], regimen: string): IvaMensualResult`
- `calculateIsrMensual(cfdis: CfdiInput[], regimen: string, deducciones: Deduccion[]): IsrMensualResult`
- `calculateCoeficienteUtilidad(ingresos, utilidad): number`
- Cualquier función que reciba arrays y devuelva números/objetos sin llamar al pool.

- [ ] **Step 2: Crear `packages/vertical-contable/src/impuestos/iva.ts`**

```typescript
export interface CfdiIvaInput {
  tipo: "I" | "E" | "P" | "T";
  fechaEmision: Date;
  subtotal: number;
  ivaTrasladado: number;
  ivaRetenido: number;
  regimenEmisor: string;
  regimenReceptor: string;
  metodoPago: "PUE" | "PPD";
  conciliado: boolean;
  status: string;
  conceptos: Array<{ claveProdServ: string; importe: number; ivaTrasladado: number }>;
}

export interface IvaMensualResult {
  trasladado16: number;
  trasladado8: number;
  trasladado0: number;
  trasladadoExento: number;
  trasladadoTotal: number;
  acreditable: number;
  retenidoCobrado: number;
  retenidoPagado: number;
  resultado: number;
}

export function calculateIvaMensual(
  cfdis: CfdiIvaInput[],
  regimen: string
): IvaMensualResult {
  // Mover lógica idéntica desde impuestos.service.ts
  // Respetar las claveProdServ excluidas (84121603, 93161608, 85101501, 85121800)
  // según la implementación existente.
  // ...
}
```

- [ ] **Step 3: Crear `packages/vertical-contable/src/impuestos/isr.ts`**

```typescript
export interface IsrMensualResult {
  ingresosBrutos: number;
  deduccionesAutoriz: number;
  base: number;
  causado: number;
  retenido: number;
  aPagar: number;
}

export interface CfdiIsrInput {
  // ... campos necesarios
}

export function calculateIsrMensual(
  cfdis: CfdiIsrInput[],
  regimen: string
): IsrMensualResult {
  // Mover lógica desde impuestos.service.ts
  // ...
}
```

- [ ] **Step 4: Crear barrel `packages/vertical-contable/src/impuestos/index.ts`**

```typescript
export * from "./iva";
export * from "./isr";
```

- [ ] **Step 5: Actualizar barrel raíz**

`packages/vertical-contable/src/index.ts`:

```typescript
export * from "./facturapi";
export * from "./sat";
export * from "./cfdi";
export * from "./impuestos";
```

- [ ] **Step 6: Verificar typecheck del package**

Run: `pnpm --filter @horux/vertical-contable typecheck`
Expected: sin errores.

- [ ] **Step 7: Actualizar `apps/api/src/services/impuestos.service.ts`**

Reemplazar las funciones puras por llamadas al package. El service sigue existiendo para:
- Consultar CFDIs de la BD tenant (`req.pool.query(...)`).
- Aplicar filtros y rangos de fecha.
- Llamar a `calculateIvaMensual` / `calculateIsrMensual` con los datos leídos.
- Devolver el resultado al controller.

```typescript
import { calculateIvaMensual, calculateIsrMensual, type CfdiIvaInput, type CfdiIsrInput } from "@horux/vertical-contable";
import type { Pool } from "pg";

export async function getIvaMensual(pool: Pool, anio: number, mes: number, regimen: string) {
  const { rows } = await pool.query<CfdiIvaInput>(/* SQL heredado */);
  return calculateIvaMensual(rows, regimen);
}

// Misma estructura para getIsrMensual, etc.
```

- [ ] **Step 8: Verificar typecheck**

Run: `pnpm --filter @horux/api typecheck`
Expected: sin errores.

- [ ] **Step 9: Commit**

```bash
git add packages/vertical-contable/ apps/api/src/services/impuestos.service.ts
git commit -m "refactor(vertical-contable): extract IVA/ISR calculation engines"
```

---

### Task 15: Migrar helper `cn` y primitives UI a @horux/shared-ui

**Files:**
- Create: `packages/shared-ui/src/lib/cn.ts`
- Create: `packages/shared-ui/src/lib/index.ts`
- Create: `packages/shared-ui/src/primitives/button.tsx`
- Create: `packages/shared-ui/src/primitives/input.tsx`
- Create: `packages/shared-ui/src/primitives/card.tsx`
- Create: `packages/shared-ui/src/primitives/dialog.tsx`
- Create: `packages/shared-ui/src/primitives/select.tsx`
- Create: `packages/shared-ui/src/primitives/tabs.tsx`
- Create: `packages/shared-ui/src/primitives/label.tsx`
- Create: `packages/shared-ui/src/primitives/popover.tsx`
- Create: `packages/shared-ui/src/primitives/sortable-header.tsx`
- Create: `packages/shared-ui/src/primitives/index.ts`
- Modify: `packages/shared-ui/src/index.ts`
- Delete: `apps/web/components/ui/button.tsx`
- Delete: `apps/web/components/ui/input.tsx`
- Delete: `apps/web/components/ui/card.tsx`
- Delete: `apps/web/components/ui/dialog.tsx`
- Delete: `apps/web/components/ui/select.tsx`
- Delete: `apps/web/components/ui/tabs.tsx`
- Delete: `apps/web/components/ui/label.tsx`
- Delete: `apps/web/components/ui/popover.tsx`
- Delete: `apps/web/components/ui/sortable-header.tsx`
- Modify: `apps/web/lib/utils.ts` (si tiene solo `cn`, se elimina; si tiene más, se quita solo `cn`)
- Modify: todos los consumidores en `apps/web/**` que importen de `@/components/ui/*`

- [ ] **Step 1: Mover el helper `cn`**

Abrir `apps/web/lib/utils.ts`:

```typescript
// Contenido típico:
import { clsx, type ClassValue } from "clsx";
import { twMerge } from "tailwind-merge";

export function cn(...inputs: ClassValue[]) {
  return twMerge(clsx(inputs));
}
```

Copiar a `packages/shared-ui/src/lib/cn.ts` con el mismo contenido.

Crear barrel `packages/shared-ui/src/lib/index.ts`:
```typescript
export * from "./cn";
```

- [ ] **Step 2: Copiar los 9 primitives uno por uno**

```bash
cp apps/web/components/ui/button.tsx          packages/shared-ui/src/primitives/button.tsx
cp apps/web/components/ui/input.tsx           packages/shared-ui/src/primitives/input.tsx
cp apps/web/components/ui/card.tsx            packages/shared-ui/src/primitives/card.tsx
cp apps/web/components/ui/dialog.tsx          packages/shared-ui/src/primitives/dialog.tsx
cp apps/web/components/ui/select.tsx          packages/shared-ui/src/primitives/select.tsx
cp apps/web/components/ui/tabs.tsx            packages/shared-ui/src/primitives/tabs.tsx
cp apps/web/components/ui/label.tsx           packages/shared-ui/src/primitives/label.tsx
cp apps/web/components/ui/popover.tsx         packages/shared-ui/src/primitives/popover.tsx
cp apps/web/components/ui/sortable-header.tsx packages/shared-ui/src/primitives/sortable-header.tsx
```

- [ ] **Step 3: Ajustar imports de `cn` en cada primitive**

Cada archivo copiado típicamente tiene:
```typescript
import { cn } from "@/lib/utils";
// o
import { cn } from "../../lib/utils";
```

Reemplazar por:
```typescript
import { cn } from "../lib/cn";
```

- [ ] **Step 4: Crear barrel `packages/shared-ui/src/primitives/index.ts`**

```typescript
export * from "./button";
export * from "./input";
export * from "./card";
export * from "./dialog";
export * from "./select";
export * from "./tabs";
export * from "./label";
export * from "./popover";
export * from "./sortable-header";
```

- [ ] **Step 5: Actualizar barrel raíz `packages/shared-ui/src/index.ts`**

```typescript
export * from "./lib";
export * from "./primitives";
```

- [ ] **Step 6: Verificar typecheck del package**

Run: `pnpm --filter @horux/shared-ui typecheck`
Expected: sin errores.

- [ ] **Step 7: Agregar `@horux/shared-ui` a deps de apps/web**

Modify: `apps/web/package.json`

En `"dependencies"`:
```json
"@horux/shared-ui": "workspace:*",
```

Run: `pnpm install`

- [ ] **Step 8: Configurar Tailwind para que lea del package**

Modify: `apps/web/tailwind.config.js` (o .ts según exista).

Antes:
```javascript
content: [
  "./app/**/*.{js,ts,jsx,tsx}",
  "./components/**/*.{js,ts,jsx,tsx}",
],
```

Después:
```javascript
const sharedUiPreset = require("../../packages/shared-ui/tailwind-preset");

module.exports = {
  presets: [sharedUiPreset],
  content: [
    "./app/**/*.{js,ts,jsx,tsx}",
    "./components/**/*.{js,ts,jsx,tsx}",
    "../../packages/shared-ui/src/**/*.{js,ts,jsx,tsx}",
  ],
  // el resto del theme.extend puede quedar o moverse al preset;
  // si queda, sobreescribe el preset.
  // ...
};
```

- [ ] **Step 9: Reemplazar imports en `apps/web/**`**

Buscar todos los imports con patrón `@/components/ui/*` o rutas relativas equivalentes:

```bash
grep -rn "from ['\"]\\(@/components/ui\\|\\.\\./components/ui\\)" apps/web/
```

*(Usa Grep tool en lugar del bash grep.)*

Para cada archivo, reemplazar por:

```typescript
// Antes:
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardHeader } from "@/components/ui/card";

// Después:
import { Button, Card, CardContent, CardHeader } from "@horux/shared-ui";
```

(Consolidar múltiples imports del mismo package en una sola línea.)

- [ ] **Step 10: Actualizar `apps/web/lib/utils.ts`**

Si el archivo **solo** contiene `cn`, eliminarlo:
```bash
rm apps/web/lib/utils.ts
```

Y actualizar imports de `@/lib/utils` → `@horux/shared-ui`:
```typescript
// Antes:
import { cn } from "@/lib/utils";

// Después:
import { cn } from "@horux/shared-ui";
```

Si el archivo contiene otras funciones además de `cn`, eliminar solo la función `cn` y dejar el resto.

- [ ] **Step 11: Eliminar los primitives viejos**

```bash
rm apps/web/components/ui/button.tsx
rm apps/web/components/ui/input.tsx
rm apps/web/components/ui/card.tsx
rm apps/web/components/ui/dialog.tsx
rm apps/web/components/ui/select.tsx
rm apps/web/components/ui/tabs.tsx
rm apps/web/components/ui/label.tsx
rm apps/web/components/ui/popover.tsx
rm apps/web/components/ui/sortable-header.tsx
```

Si `apps/web/components/ui/` queda vacío, remover también la carpeta:
```bash
rmdir apps/web/components/ui || true
```

- [ ] **Step 12: Verificar typecheck de apps/web**

Run: `pnpm --filter @horux/web typecheck`
Expected: sin errores. Si hay imports faltantes (componentes específicos de web que quedaron en `components/ui/` y no se migraron), ajustar.

- [ ] **Step 13: Correr build de Next.js para validar Tailwind**

Run: `pnpm --filter @horux/web build`
Expected: build exitoso (compila CSS con las classes Tailwind del package).

Si el build falla con "class not found" o CSS ausente, revisar Step 8 (config Tailwind).

- [ ] **Step 14: Commit**

```bash
git add packages/shared-ui/ apps/web/
git commit -m "refactor(web): move UI primitives (button/card/dialog/...) to @horux/shared-ui"
```

---

### Task 16: Migrar form selectors (Period, Regimen, FiscalDisclaimer) a @horux/shared-ui

**Files:**
- Create: `packages/shared-ui/src/form/period-selector.tsx`
- Create: `packages/shared-ui/src/form/regimen-selector.tsx`
- Create: `packages/shared-ui/src/form/fiscal-disclaimer.tsx`
- Create: `packages/shared-ui/src/form/index.ts`
- Modify: `packages/shared-ui/src/index.ts`
- Delete: `apps/web/components/period-selector.tsx`
- Delete: `apps/web/components/regimen-selector.tsx`
- Delete: `apps/web/components/fiscal-disclaimer.tsx`
- Modify: consumidores en `apps/web/**`

- [ ] **Step 1: Copiar los tres componentes**

```bash
cp apps/web/components/period-selector.tsx    packages/shared-ui/src/form/period-selector.tsx
cp apps/web/components/regimen-selector.tsx   packages/shared-ui/src/form/regimen-selector.tsx
cp apps/web/components/fiscal-disclaimer.tsx  packages/shared-ui/src/form/fiscal-disclaimer.tsx
```

- [ ] **Step 2: Ajustar imports en cada componente copiado**

- `import { Button } from "@/components/ui/button"` → `import { Button } from "../primitives"`
- `import { cn } from "@/lib/utils"` → `import { cn } from "../lib"`
- Eliminar cualquier import que apunte a dominio específico de la app (ej. hooks `useTenant` o similares) — si alguno existe, el componente NO debería haberse seleccionado para migrar; revisa y aborta la migración de ese componente específico.

- [ ] **Step 3: Crear barrel `packages/shared-ui/src/form/index.ts`**

```typescript
export * from "./period-selector";
export * from "./regimen-selector";
export * from "./fiscal-disclaimer";
```

- [ ] **Step 4: Actualizar barrel raíz `packages/shared-ui/src/index.ts`**

```typescript
export * from "./lib";
export * from "./primitives";
export * from "./form";
```

- [ ] **Step 5: Verificar typecheck**

Run: `pnpm --filter @horux/shared-ui typecheck`
Expected: sin errores.

- [ ] **Step 6: Reemplazar imports en apps/web**

Buscar `@/components/period-selector`, `@/components/regimen-selector`, `@/components/fiscal-disclaimer` y reemplazar por `@horux/shared-ui`.

- [ ] **Step 7: Eliminar archivos antiguos**

```bash
rm apps/web/components/period-selector.tsx
rm apps/web/components/regimen-selector.tsx
rm apps/web/components/fiscal-disclaimer.tsx
```

- [ ] **Step 8: Verificar typecheck**

Run: `pnpm --filter @horux/web typecheck`
Expected: sin errores.

- [ ] **Step 9: Commit**

```bash
git add packages/shared-ui/ apps/web/
git commit -m "refactor(web): move form selectors (period/regimen/fiscal) to @horux/shared-ui"
```

---

### Task 17: Migrar layouts (DashboardShell, Header, Sidebar) a @horux/shared-ui

**Files:**
- Create: `packages/shared-ui/src/layout/dashboard-shell.tsx`
- Create: `packages/shared-ui/src/layout/header.tsx`
- Create: `packages/shared-ui/src/layout/sidebar.tsx`
- Create: `packages/shared-ui/src/layout/index.ts`
- Modify: `packages/shared-ui/src/index.ts`
- Delete: `apps/web/components/layouts/dashboard-shell.tsx`
- Delete: `apps/web/components/layouts/header.tsx`
- Delete: `apps/web/components/layouts/sidebar.tsx`

**Advertencia crítica**: si `header.tsx` o `sidebar.tsx` tienen lógica específica del producto (links a rutas específicas de Horux360 como `/cfdi`, `/impuestos`, etc.), entonces **no son genuinamente compartibles** tal cual. Lo compartible es el "chrome" (estructura visual). La lista de navegación (menú items) debería recibirse como prop.

- [ ] **Step 1: Leer `apps/web/components/layouts/sidebar.tsx`**

Si el sidebar define hardcoded los items del menú con rutas como `href="/dashboard"`, `href="/cfdi"`, etc., ajustar para aceptar los items como prop:

```typescript
// Antes: items hardcoded en el componente
export function Sidebar() {
  return (
    <aside>
      <Link href="/dashboard">Dashboard</Link>
      <Link href="/cfdi">CFDI</Link>
      ...
    </aside>
  );
}

// Después: items como prop
export interface NavItem {
  label: string;
  href: string;
  icon?: React.ReactNode;
}

export interface SidebarProps {
  items: NavItem[];
  activeHref?: string;
}

export function Sidebar({ items, activeHref }: SidebarProps) {
  return (
    <aside>
      {items.map((item) => (
        <a key={item.href} href={item.href} data-active={item.href === activeHref}>
          {item.icon}
          {item.label}
        </a>
      ))}
    </aside>
  );
}
```

**Nota**: usa `<a>` en vez de `<Link>` de Next.js dentro del package para mantener el package libre de acoplamiento al router. El `Link` se usa desde el wrapper en apps/web.

- [ ] **Step 2: Copiar los 3 archivos al package**

```bash
mkdir -p packages/shared-ui/src/layout
cp apps/web/components/layouts/dashboard-shell.tsx packages/shared-ui/src/layout/dashboard-shell.tsx
cp apps/web/components/layouts/header.tsx          packages/shared-ui/src/layout/header.tsx
cp apps/web/components/layouts/sidebar.tsx         packages/shared-ui/src/layout/sidebar.tsx
```

Ajustar cada archivo para:
- Props en vez de hardcoded data.
- Imports internos reemplazados (`@/components/ui/*` → `../primitives`, `@/lib/utils` → `../lib`).
- Eliminar cualquier `use client` que solo esté por `Link` de Next.js — usar `<a>` nativo.
- Eliminar imports de `next/link`, `next/navigation`, `next/image` — reemplazar por primitivas DOM (el wrapper en apps/web pondrá `<Link>` y `<Image>` como children props si los necesita).

- [ ] **Step 3: Crear barrel `packages/shared-ui/src/layout/index.ts`**

```typescript
export * from "./dashboard-shell";
export * from "./header";
export * from "./sidebar";
```

- [ ] **Step 4: Actualizar barrel raíz**

```typescript
export * from "./lib";
export * from "./primitives";
export * from "./form";
export * from "./layout";
```

- [ ] **Step 5: Verificar typecheck**

Run: `pnpm --filter @horux/shared-ui typecheck`
Expected: sin errores.

- [ ] **Step 6: Crear wrapper en apps/web con navigation items hardcoded**

Create: `apps/web/components/layouts/app-sidebar.tsx`

```typescript
"use client";

import { Sidebar, type NavItem } from "@horux/shared-ui";
import { usePathname } from "next/navigation";

const navItems: NavItem[] = [
  { label: "Dashboard", href: "/dashboard" },
  { label: "CFDI", href: "/cfdi" },
  { label: "Impuestos", href: "/impuestos" },
  { label: "Facturación", href: "/facturacion" },
  { label: "Documentos", href: "/documentos" },
  { label: "Reportes", href: "/reportes" },
  // ... misma lista que tenía el sidebar original
];

export function AppSidebar() {
  const pathname = usePathname();
  return <Sidebar items={navItems} activeHref={pathname} />;
}
```

Análogo para `AppHeader` si `header.tsx` tenía items hardcoded.

- [ ] **Step 7: Actualizar consumidores que usaban `Sidebar` / `Header` / `DashboardShell` directo**

Reemplazar imports:
- `import { Sidebar } from "@/components/layouts/sidebar"` → `import { AppSidebar } from "@/components/layouts/app-sidebar"` (y cambiar `<Sidebar />` → `<AppSidebar />`)
- `import { Header } from "@/components/layouts/header"` → `import { AppHeader } from "@/components/layouts/app-header"`
- `import { DashboardShell } from "@/components/layouts/dashboard-shell"` → `import { DashboardShell } from "@horux/shared-ui"`

- [ ] **Step 8: Eliminar archivos antiguos**

```bash
rm apps/web/components/layouts/dashboard-shell.tsx
rm apps/web/components/layouts/header.tsx
rm apps/web/components/layouts/sidebar.tsx
```

- [ ] **Step 9: Verificar typecheck y build**

Run: `pnpm --filter @horux/web typecheck && pnpm --filter @horux/web build`
Expected: sin errores.

- [ ] **Step 10: Commit**

```bash
git add packages/shared-ui/ apps/web/
git commit -m "refactor(web): move layout chrome (shell/header/sidebar) to @horux/shared-ui"
```

---

### Task 18: Migrar hooks (useDebounce, useTableSort) a @horux/shared-ui

**Files:**
- Create: `packages/shared-ui/src/hooks/use-debounce.ts`
- Create: `packages/shared-ui/src/hooks/use-table-sort.ts`
- Create: `packages/shared-ui/src/hooks/index.ts`
- Modify: `packages/shared-ui/src/index.ts`
- Delete: `apps/web/lib/hooks/use-debounce.ts`
- Delete: `apps/web/lib/hooks/use-table-sort.ts`

- [ ] **Step 1: Copiar los dos hooks**

```bash
mkdir -p packages/shared-ui/src/hooks
cp apps/web/lib/hooks/use-debounce.ts   packages/shared-ui/src/hooks/use-debounce.ts
cp apps/web/lib/hooks/use-table-sort.ts packages/shared-ui/src/hooks/use-table-sort.ts
```

- [ ] **Step 2: Ajustar imports en cada hook**

Normalmente `useDebounce` y `useTableSort` solo dependen de React. Si hay imports de `@/lib/utils`, cambiar a `../lib`.

- [ ] **Step 3: Crear barrel `packages/shared-ui/src/hooks/index.ts`**

```typescript
export * from "./use-debounce";
export * from "./use-table-sort";
```

- [ ] **Step 4: Actualizar barrel raíz**

```typescript
export * from "./lib";
export * from "./primitives";
export * from "./form";
export * from "./layout";
export * from "./hooks";
```

- [ ] **Step 5: Verificar typecheck**

Run: `pnpm --filter @horux/shared-ui typecheck`
Expected: sin errores.

- [ ] **Step 6: Reemplazar imports en apps/web**

Buscar `@/lib/hooks/use-debounce` y `@/lib/hooks/use-table-sort`. Reemplazar:

```typescript
// Antes:
import { useDebounce } from "@/lib/hooks/use-debounce";
import { useTableSort } from "@/lib/hooks/use-table-sort";

// Después:
import { useDebounce, useTableSort } from "@horux/shared-ui";
```

- [ ] **Step 7: Eliminar archivos antiguos**

```bash
rm apps/web/lib/hooks/use-debounce.ts
rm apps/web/lib/hooks/use-table-sort.ts
```

- [ ] **Step 8: Verificar typecheck**

Run: `pnpm --filter @horux/web typecheck`
Expected: sin errores.

- [ ] **Step 9: Commit**

```bash
git add packages/shared-ui/ apps/web/
git commit -m "refactor(web): move generic hooks (useDebounce, useTableSort) to @horux/shared-ui"
```

---

### Task 19: Migrar charts genéricos (KpiCard, BarChart) a @horux/shared-ui

**Files:**
- Create: `packages/shared-ui/src/charts/kpi-card.tsx`
- Create: `packages/shared-ui/src/charts/bar-chart.tsx`
- Create: `packages/shared-ui/src/charts/index.ts`
- Modify: `packages/shared-ui/src/index.ts`
- Delete: `apps/web/components/charts/kpi-card.tsx`
- Delete: `apps/web/components/charts/bar-chart.tsx`

**Importante**: solo migrar si los charts son **genéricos** (reciben data via props, sin lógica de dominio). Si contienen lógica específica (ej. formatean pesos mexicanos con `$`, o tienen labels hardcoded tipo "IVA trasladado"), entonces son específicos del producto y no deben ir al package. En ese caso, saltar esta task.

- [ ] **Step 1: Leer ambos archivos para evaluar si son genuinamente genéricos**

Abrir `apps/web/components/charts/kpi-card.tsx` y `bar-chart.tsx`. Criterios:
- ¿Aceptan data y labels via props? → compartible.
- ¿Tienen strings como "CFDIs Emitidos" o "$" hardcoded? → NO compartible tal cual.
- ¿Usan formatters de negocio (ej. `formatCurrencyMxn`)? → parametrizar o dejar en apps/web.

Si son genéricos, proceder. Si NO, marcar esta task como "skipped" y continuar.

- [ ] **Step 2 (si aplica): Copiar y ajustar**

```bash
mkdir -p packages/shared-ui/src/charts
cp apps/web/components/charts/kpi-card.tsx  packages/shared-ui/src/charts/kpi-card.tsx
cp apps/web/components/charts/bar-chart.tsx packages/shared-ui/src/charts/bar-chart.tsx
```

Ajustar imports (`../primitives`, `../lib`).

Si los componentes formatean moneda, cambiar a recibir una función `formatValue?: (v: number) => string` como prop, con default `(v) => v.toString()`.

- [ ] **Step 3 (si aplica): Crear barrel `packages/shared-ui/src/charts/index.ts`**

```typescript
export * from "./kpi-card";
export * from "./bar-chart";
```

- [ ] **Step 4 (si aplica): Actualizar barrel raíz**

```typescript
export * from "./lib";
export * from "./primitives";
export * from "./form";
export * from "./layout";
export * from "./hooks";
export * from "./charts";
```

- [ ] **Step 5 (si aplica): Verificar typecheck**

Run: `pnpm --filter @horux/shared-ui typecheck`
Expected: sin errores.

- [ ] **Step 6 (si aplica): Reemplazar imports en apps/web y eliminar originales**

```bash
rm apps/web/components/charts/kpi-card.tsx
rm apps/web/components/charts/bar-chart.tsx
```

Buscar y reemplazar imports.

- [ ] **Step 7 (si aplica): Verificar typecheck y build**

Run: `pnpm --filter @horux/web typecheck && pnpm --filter @horux/web build`

- [ ] **Step 8 (si aplica): Commit**

```bash
git add packages/shared-ui/ apps/web/
git commit -m "refactor(web): move generic charts (KpiCard, BarChart) to @horux/shared-ui"
```

---

### Task 20: Validación final del monorepo

**Files:**
- Check: `package.json` root
- Check: `pnpm-workspace.yaml`
- Check: cada paquete existe y typechekea

- [ ] **Step 1: Listar workspaces**

Run: `pnpm list --depth 0 --recursive`

Expected output (names):
```
@horux/api
@horux/core
@horux/shared
@horux/shared-ui
@horux/vertical-contable
@horux/web
```

- [ ] **Step 2: Typecheck completo del monorepo**

Run: `pnpm -r typecheck`
Expected: todas las packages compilan sin errores. Si alguna falla, ajustar imports y re-correr.

- [ ] **Step 3: Build completo**

Run: `pnpm -r build`
Expected: todos los builds exitosos. Next.js debe generar su output sin errores.

- [ ] **Step 4: Arrancar dev servers y smoke test manual (USUARIO)**

Run: `pnpm dev`

Probar manualmente en `http://localhost:3000`:
- [ ] Login funciona.
- [ ] Dashboard carga KPIs.
- [ ] Lista de CFDIs se ve.
- [ ] Selector de periodo (Period Selector) cambia periodo y refleja.
- [ ] Sidebar navega entre secciones.
- [ ] Botones y modales abren y cierran.
- [ ] Upload de XML CFDI funciona (Facturapi llama OK).

**Criterio de éxito**: **ninguna regresión funcional** visible vs. el comportamiento pre-refactor.

- [ ] **Step 5: Commit final de merge (si hubo ajustes en el smoke test)**

```bash
git add .
git commit -m "refactor(monorepo): finalize packages refactor — no regressions"
```

Si no hubo ajustes (todo funcionó al primer intento), skip este commit.

- [ ] **Step 6: Verificar que no quedaron imports rotos o archivos huérfanos**

Run:
```bash
grep -rn "from ['\"]@/components/ui" apps/web/src/ apps/web/app/ apps/web/components/ 2>/dev/null | head -5
grep -rn "from ['\"]\\.\\./utils/token" apps/api/src/ 2>/dev/null | head -5
grep -rn "from ['\"]\\.\\./services/facturapi.service" apps/api/src/ 2>/dev/null | head -5
```

*(Usar Grep tool.)*

Expected: cero resultados. Si aparecen, quedan imports sin actualizar → fix inline.

---

## Self-Review

### Spec coverage

La spec (§13 "Refactor preparatorio del monorepo") pide:
- **Estructura objetivo** con `packages/core`, `packages/vertical-contable`, `packages/shared-ui`, `packages/shared` (existente), `apps/api`, `apps/web`. ✅ Cubierto en Tasks 1-3 + 15-19.
- **Candidatos a extraer** (cliente Facturapi, parser XML CFDI, SatSyncService, motor IVA/ISR, catálogos SAT, primitives UI, hooks). ✅ Cubierto en Tasks 8-14 (backend) y 15-19 (frontend).
- **Criterios: "va a packages/ si ambos productos lo necesitan sin modificaciones"**. ✅ Aplicado: `tenantMiddleware`, servicios específicos, jobs, etc., quedan en apps/api.
- **Lo que NO se extrae** (middlewares de autorización, dashboards, onboarding flows, modelo suscripción). ✅ Respetado — esos archivos NO aparecen en el mapa de archivos "Se mueven".

### Placeholder scan

- Sin "TBD", "TODO", "implement later".
- Sin "fill in details" o "add appropriate error handling" vacío.
- Cada step tiene código concreto o comando con output esperado.
- Task 19 tiene un condicional claro ("si los charts son genuinamente genéricos") pero con criterio explícito para decidir — no es un placeholder, es un branch de ejecución bien definido.
- Task 14 menciona "copiar lógica idéntica desde impuestos.service.ts" — válido porque la lógica ya existe y es extensa; re-escribirla en el plan sería redundante y propenso a errores de transcripción. El engineer copia del original y ajusta imports.

### Type consistency

- Nombres de packages consistentes: `@horux/core`, `@horux/vertical-contable`, `@horux/shared-ui`, `@horux/shared` (existente, no se cambia).
- `TokenConfig`, `RateLimitConfig`, `SatClientConfig`, `EmailTransportConfig` todos siguen el patrón "factory function con config".
- `createFacturapiClient`, `createSatClient`, `createRateLimit`, `createEmailTransport` — patrón consistente.
- Los imports barrel siempre desde `@horux/core` o `@horux/vertical-contable` o `@horux/shared-ui`, nunca paths profundos como `@horux/core/src/auth/token`.
- `NavItem` definido en Task 17 se usa coherentemente.

### Scope check

Este plan cubre **únicamente el refactor preparatorio** del monorepo (Fase 0 del roadmap del spec, §15). NO toca:
- Modelo de datos nuevo (Despacho, Contribuyente) — Fase 1.
- Auth con `despachoId` en JWT — Fase 1.
- Connector BYO-DB — Fase 4.
- Dashboard cross-despacho — Fase 5.
- Métricas pre-calculadas — Fase 6.
- Etc.

Todas las Fases 1-7 son planes posteriores, cada uno su propio brainstorm → spec → plan → ejecución.

**Criterio de éxito del plan**: `pnpm -r typecheck` pasa sin errores, `pnpm -r build` exitoso, Horux360 funciona idéntico (smoke test manual), y los 3 packages nuevos exportan los símbolos documentados en los mapas de archivo. El código queda listo para que Fase 1 consuma `@horux/core` y `@horux/vertical-contable` sin duplicar lógica.

---

## Notas operativas

- **Orden de tasks importa**: Tasks 1-3 (scaffolds) antes que 4-7 (migraciones a core) antes que 8-14 (migraciones a vertical-contable) antes que 15-19 (migraciones a shared-ui) antes que 20 (validación final). La razón: `vertical-contable` importa de `core`, y varios archivos de apps/api dependen de ambos.
- **Si un typecheck falla a mitad de una task**: revisa los imports del archivo que está fallando. El 95% de los errores son paths relativos incorrectos o nombres de símbolos no exportados en barrels.
- **Si un commit falla por pre-commit hook**: NO uses `--no-verify`. Investigar el hook, corregir el error, re-stage, nuevo commit.
- **Si durante el smoke test (Task 20 Step 4) aparece una regresión**: identificar el commit que la introdujo (`git bisect` o `git log --oneline`) y revisar el refactor de esa task. Volver a empezar esa task NO implica descartar las siguientes — solo arreglar los imports o adaptadores faltantes.
- **Cada task es un commit**: facilita rollback granular si una etapa tiene problemas.
- **No hay rama separada**: el plan trabaja sobre la rama actual del fork `Horux_despacho`. El `.git` local ya existe (heredado del clone de Horux360).
