Files
HoruxDespachos/docs/superpowers/plans/2026-04-16-refactor-monorepo-packages.md
2026-04-27 22:09:36 -06:00

79 KiB

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

{
  "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
{
  "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
// 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
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

{
  "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
{
  "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
// 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
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

{
  "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
{
  "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
// 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.

/** @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
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):

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

import { env } from "../config/env";
// uso: env.JWT_SECRET, env.JWT_EXPIRES_IN

Después (packages/core/src/auth/token.ts):

// 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
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
export * from "./token";
export * from "./password";
  • Step 4: Actualizar barrel packages/core/src/index.ts
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

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:

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

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

"@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
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

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:

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
export * from "./rate-limit";
  • Step 3: Actualizar barrel packages/core/src/index.ts
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:

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

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).
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
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
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
export * from "./auth";
export * from "./middleware";
export * from "./email";
  • Step 5: Crear wrapper en apps/api

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

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

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

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

Los controladores/servicios que llaman sendEmail(...):

// 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
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
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
export * from "./aes-gcm";
  • Step 4: Actualizar barrel packages/core/src/index.ts
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.

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

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:

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
export * from "./client";
  • Step 4: Actualizar barrel packages/vertical-contable/src/index.ts
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:

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

Run: pnpm install

  • Step 7: Crear wrapper en apps/api

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

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:

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

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:

// 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:

export * from "./facturapi";
export * from "./sat/crypto";
  • Step 6: Eliminar archivo antiguo
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
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

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:

import { env } from "../../config/env";
const BASE_URL = env.SAT_API_URL;

Reemplazar con:

// 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
export * from "./client";
export * from "./parser";
export * from "./downloader";
export * from "./crypto";
  • Step 6: Actualizar barrel raíz packages/vertical-contable/src/index.ts
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

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:

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

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

export * from "./login";
export * from "./scraper";
export * from "./parser";
  • Step 4: Actualizar barrel SAT packages/vertical-contable/src/sat/index.ts
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:

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

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
export * from "./login";
export * from "./scraper";
export * from "./parser";
  • Step 4: Actualizar barrel SAT packages/vertical-contable/src/sat/index.ts
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
// 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
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
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:

// 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
export * from "./parser";
  • Step 4: Actualizar barrel raíz

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

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:

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

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
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
export * from "./iva";
export * from "./isr";
  • Step 5: Actualizar barrel raíz

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

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.
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
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:

// 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:

export * from "./cn";
  • Step 2: Copiar los 9 primitives uno por uno
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:

import { cn } from "@/lib/utils";
// o
import { cn } from "../../lib/utils";

Reemplazar por:

import { cn } from "../lib/cn";
  • Step 4: Crear barrel packages/shared-ui/src/primitives/index.ts
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
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":

"@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:

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

Después:

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:

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

(Usa Grep tool en lugar del bash grep.)

Para cada archivo, reemplazar por:

// 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:

rm apps/web/lib/utils.ts

Y actualizar imports de @/lib/utils@horux/shared-ui:

// 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
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:

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

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

export * from "./period-selector";
export * from "./regimen-selector";
export * from "./fiscal-disclaimer";
  • Step 4: Actualizar barrel raíz packages/shared-ui/src/index.ts
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
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
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:

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

export * from "./dashboard-shell";
export * from "./header";
export * from "./sidebar";
  • Step 4: Actualizar barrel raíz
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

"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

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

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
export * from "./use-debounce";
export * from "./use-table-sort";
  • Step 4: Actualizar barrel raíz
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:

// 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
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
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
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
export * from "./kpi-card";
export * from "./bar-chart";
  • Step 4 (si aplica): Actualizar barrel raíz
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
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
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)
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:

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