# 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(); 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; getOrganizationStatus(orgId: string): Promise; emitInvoice(orgId: string, data: InvoiceData): Promise; cancelInvoice(orgId: string, invoiceId: string, motivo: string, folioSustitucion?: string): Promise; // ... 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(/* 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 ( ); } // 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 ( ); } ``` **Nota**: usa `` en vez de `` 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 `` nativo. - Eliminar imports de `next/link`, `next/navigation`, `next/image` — reemplazar por primitivas DOM (el wrapper en apps/web pondrá `` y `` 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 ; } ``` 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 `` → ``) - `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).