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.yamllistaapps/*ypackages/*. - Package existente
@horux/sharedenpackages/shared/con tipos Zod. NO modificar. - Horux360 sigue en producción desde el branch
main— cualquier cambio enapps/apioapps/webdebe compilar conpnpm typecheckal 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— agregarcontentpara incluirpackages/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.tsvací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.tsvací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.tsvací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 desdeapps/api/src/utils/token.ts) -
Create:
packages/core/src/auth/password.ts(mover desdeapps/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.tsapackages/core/src/auth/token.tssin 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 aimport { env } from "../config/env"— PEROenv.tsno 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
TokenConfigy 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/tokenen 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.tsal 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/corea 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/envu 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.tscon 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.tsy 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-contablea 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.serviceen 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 desat-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/envu 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
fetchoaxios, 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/envpor 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.tsy 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/calculateIsrMensualcon 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 solocn, se elimina; si tiene más, se quita solocn) -
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
cnen 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-uia 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
useTenanto 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 clientque solo esté porLinkde 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/DashboardShelldirecto
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.jsonroot -
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,EmailTransportConfigtodos siguen el patrón "factory function con config".createFacturapiClient,createSatClient,createRateLimit,createEmailTransport— patrón consistente.- Los imports barrel siempre desde
@horux/coreo@horux/vertical-contableo@horux/shared-ui, nunca paths profundos como@horux/core/src/auth/token. NavItemdefinido 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
despachoIden 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-contableimporta decore, 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 bisectogit 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.gitlocal ya existe (heredado del clone de Horux360).