26 KiB
Plan 2A: Schema + Auth para Horux Despachos — 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: Evolucionar el schema de BD central y tenant para soportar el modelo de despachos (verticalProfile, dbMode, entidades gestionadas, contribuyentes, carteras), agregar roles de despacho (supervisor, cliente), refactorear el migration runner para tracking por scope, y crear un endpoint de signup básico para despachos.
Architecture: Se evoluciona el modelo Tenant existente (no se renombra — un despacho ES un tenant con campos adicionales). Se agregan 5 tenant migrations SQL (tracking table + entidades + carteras + contribuyentes + cliente_accesos). El migration runner se extiende para soportar tabla de tracking (tenant_migrations). Se agregan tipos nuevos a @horux/shared.
Tech Stack: Prisma 5.22, PostgreSQL 16, TypeScript 5, Express 4.21, pnpm workspaces.
Validation: pnpm --filter @horux/api typecheck (57 pre-existing errors baseline — verify no NEW errors). pnpm --filter @horux/shared typecheck (0 errors baseline).
Git: Commits locales, sin push. Un commit por task.
Pre-existing codebase context (Plan 2A engineer MUST know):
- Prisma schema:
apps/api/prisma/schema.prisma— modelos Tenant, User, TenantMembership, Rol, FielCredential, Subscription, etc. - Current roles table (Rol): id=1 owner, id=2 contador, id=3 visor, id=7 cfo, id=8 auxiliar.
- Tenant migrations:
apps/api/src/migrations/tenant/001-005.sql— flat numbered, applied lazily byTenantConnectionManager.getPool()viamigrate()inconfig/tenant-migrations.ts. - Auth JWT payload:
{ userId, email, role, tenantId, platformRoles?, tokenVersion? }from@horux/shared. - Config env:
apps/api/src/config/env.ts— Zod-validated, includes DATABASE_URL, JWT_SECRET, FIEL_ENCRYPTION_KEY. - Imports in apps/api use
.jsextension (NodeNext module resolution).
File Structure
New files:
apps/api/prisma/migrations/YYYYMMDD_despacho_fields/migration.sql— Prisma migration (auto-generated)apps/api/src/migrations/tenant/006_tenant_migrations_tracking.sql— tracking table for scope-based migrationsapps/api/src/migrations/tenant/007_entidades_gestionadas.sql— core: base entity tableapps/api/src/migrations/tenant/008_carteras.sql— core: portfolios + assignmentsapps/api/src/migrations/tenant/009_cliente_accesos.sql— core: client-viewer accessapps/api/src/migrations/tenant/010_contribuyentes.sql— vertical-contable: taxpayer subtypepackages/shared/src/types/despacho.ts— DespachoRole, VerticalProfile, DbMode typesapps/api/src/controllers/despacho.controller.ts— signup endpointapps/api/src/services/despacho.service.ts— signup business logicapps/api/src/routes/despacho.routes.ts— route mounting
Modified files:
apps/api/prisma/schema.prisma— add fields to Tenant, add enumsapps/api/prisma/seed.ts— add 'supervisor' and 'cliente' rolesapps/api/src/config/tenant-migrations.ts— support tracking tableapps/api/src/app.ts— mount despacho routespackages/shared/src/types/auth.ts— add DespachoRole to exportspackages/shared/src/index.ts— re-export despacho types
Tasks
Task 1: Prisma migration — add despacho fields to Tenant
Files:
-
Modify:
apps/api/prisma/schema.prisma -
Create: auto-generated migration via
prisma migrate dev -
Step 1: Add new enums and fields to Prisma schema
Open apps/api/prisma/schema.prisma and add the following:
After the existing enum Plan { ... }:
enum VerticalProfile {
CONTABLE
JURIDICO
ARQUITECTURA
}
enum DbMode {
BYO
MANAGED
}
In the model Tenant { ... }, add AFTER the telefono field (before the relations block):
// === Despacho fields (Plan 2A) ===
verticalProfile VerticalProfile? @map("vertical_profile")
dbMode DbMode? @map("db_mode")
dbConnectionEnc String? @map("db_connection_enc")
dbConnectionIv String? @map("db_connection_iv")
dbSchemaVersion Int @default(0) @map("db_schema_version")
connectorTokenEnc String? @map("connector_token_enc")
connectorTunnelHostname String? @map("connector_tunnel_hostname")
connectorLastSeen DateTime? @map("connector_last_seen")
connectorVersion String? @map("connector_version") @db.VarChar(20)
- Step 2: Generate and apply Prisma migration
Run:
cd apps/api && npx prisma migrate dev --name despacho_fields
Expected: migration SQL generated in prisma/migrations/YYYYMMDD_despacho_fields/. Since all new fields are nullable or have defaults, this is safe for existing data.
If the command fails because there's no DB connection, create the migration without applying:
cd apps/api && npx prisma migrate dev --name despacho_fields --create-only
- Step 3: Generate Prisma client
Run:
cd apps/api && npx prisma generate
- Step 4: Verify typecheck
Run: pnpm --filter @horux/api typecheck
Expected: same 57 pre-existing errors, no new ones.
- Step 5: Commit
git add apps/api/prisma/
git commit -m "feat(schema): add despacho fields to Tenant model (verticalProfile, dbMode, connector)"
Task 2: Seed new roles (supervisor, cliente)
Files:
-
Modify:
apps/api/prisma/seed.ts -
Step 1: Read current seed.ts to understand the roles seeding pattern
Open apps/api/prisma/seed.ts and find where roles are upserted. The current roles are:
id=1: owner, id=2: contador, id=3: visor, id=7: cfo, id=8: auxiliar
- Step 2: Add supervisor and cliente roles to the seed
Add to the roles upsert section:
await prisma.rol.upsert({
where: { nombre: 'supervisor' },
update: {},
create: { id: 9, nombre: 'supervisor', descripcion: 'Supervisor de despacho — titular de RFCs, crea carteras' },
});
await prisma.rol.upsert({
where: { nombre: 'cliente' },
update: {},
create: { id: 10, nombre: 'cliente', descripcion: 'Cliente visor externo — acceso read-only a sus RFCs' },
});
- Step 3: Run seed (if DB is available)
Run:
cd apps/api && npx prisma db seed
If no DB connection, skip — the seed will run at next deploy. The migration and seed are idempotent.
- Step 4: Commit
git add apps/api/prisma/seed.ts
git commit -m "feat(seed): add supervisor and cliente roles for despachos"
Task 3: Add despacho types to @horux/shared
Files:
-
Create:
packages/shared/src/types/despacho.ts -
Modify:
packages/shared/src/index.ts(or wherever types are re-exported) -
Step 1: Create despacho types file
Create packages/shared/src/types/despacho.ts:
export type DespachoRole = 'owner' | 'supervisor' | 'auxiliar' | 'cliente';
export type VerticalProfile = 'CONTABLE' | 'JURIDICO' | 'ARQUITECTURA';
export type DbMode = 'BYO' | 'MANAGED';
export interface DespachoInfo {
id: string;
nombre: string;
rfc: string;
verticalProfile: VerticalProfile;
dbMode: DbMode | null;
plan: string;
}
export interface DespachoSignupRequest {
despacho: {
nombre: string;
rfc: string;
regimenFiscal?: string;
codigoPostal?: string;
verticalProfile: VerticalProfile;
};
owner: {
nombre: string;
email: string;
password: string;
};
}
export interface ContribuyenteInfo {
id: string;
rfc: string;
razonSocial: string;
regimenFiscal: string;
codigoPostal?: string;
supervisorUserId?: string;
active: boolean;
}
- Step 2: Find and update the barrel export
Read packages/shared/src/index.ts to see how types are exported. Add:
export * from './types/despacho';
If the barrel uses a different pattern (e.g., explicit re-exports), follow that pattern.
- Step 3: Verify typecheck
Run: pnpm --filter @horux/shared typecheck
Expected: 0 errors.
- Step 4: Commit
git add packages/shared/
git commit -m "feat(shared): add DespachoRole, VerticalProfile, DbMode types"
Task 4: Tenant migration — tracking table + entidades_gestionadas
Files:
-
Create:
apps/api/src/migrations/tenant/006_tenant_migrations_tracking.sql -
Create:
apps/api/src/migrations/tenant/007_entidades_gestionadas.sql -
Step 1: Create migration 006 — tracking table
Create apps/api/src/migrations/tenant/006_tenant_migrations_tracking.sql:
-- Tracking table for scope-based migrations.
-- Allows checking which migrations have been applied and which are pending.
-- For now, all existing migrations (001-005) are considered "legacy" scope
-- and are tracked by the existing file-based runner. This table tracks
-- only NEW migrations going forward (007+).
CREATE TABLE IF NOT EXISTS tenant_migrations (
scope varchar(50) NOT NULL,
version int NOT NULL,
name varchar(255),
applied_at timestamptz DEFAULT now(),
PRIMARY KEY (scope, version)
);
-- Mark 001-005 as already applied under "legacy" scope
-- so the runner doesn't try to re-apply them.
INSERT INTO tenant_migrations (scope, version, name)
VALUES
('legacy', 1, '001_initial_schema'),
('legacy', 2, '002_create_opiniones_cumplimiento'),
('legacy', 3, '003_create_declaraciones_provisionales'),
('legacy', 4, '004_declaraciones_liga_pago_pdf'),
('legacy', 5, '005_create_constancias_situacion_fiscal'),
('legacy', 6, '006_tenant_migrations_tracking')
ON CONFLICT (scope, version) DO NOTHING;
- Step 2: Create migration 007 — entidades_gestionadas
Create apps/api/src/migrations/tenant/007_entidades_gestionadas.sql:
-- Core table: base entity managed by the despacho.
-- Subtyped by vertical (e.g., contribuyentes for CONTABLE).
-- Carteras and client access operate on this table (vertical-agnostic).
CREATE TABLE IF NOT EXISTS entidades_gestionadas (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
tipo varchar(20) NOT NULL,
nombre text NOT NULL,
identificador text,
supervisor_user_id uuid,
active boolean DEFAULT true,
created_at timestamptz DEFAULT now(),
updated_at timestamptz DEFAULT now()
);
CREATE INDEX IF NOT EXISTS ix_entidades_supervisor ON entidades_gestionadas(supervisor_user_id);
CREATE INDEX IF NOT EXISTS ix_entidades_tipo ON entidades_gestionadas(tipo, active);
CREATE INDEX IF NOT EXISTS ix_entidades_identificador ON entidades_gestionadas(identificador);
INSERT INTO tenant_migrations (scope, version, name)
VALUES ('core', 7, '007_entidades_gestionadas')
ON CONFLICT (scope, version) DO NOTHING;
- Step 3: Verify SQL syntax
Read both files back to confirm no typos. The SQL should be idempotent (IF NOT EXISTS, ON CONFLICT DO NOTHING).
- Step 4: Commit
git add apps/api/src/migrations/tenant/006_tenant_migrations_tracking.sql apps/api/src/migrations/tenant/007_entidades_gestionadas.sql
git commit -m "feat(migrations): add tenant_migrations tracking + entidades_gestionadas table"
Task 5: Tenant migrations — carteras + cliente_accesos + contribuyentes
Files:
-
Create:
apps/api/src/migrations/tenant/008_carteras.sql -
Create:
apps/api/src/migrations/tenant/009_cliente_accesos.sql -
Create:
apps/api/src/migrations/tenant/010_contribuyentes.sql -
Step 1: Create migration 008 — carteras
Create apps/api/src/migrations/tenant/008_carteras.sql:
-- Core: supervisor portfolios. A supervisor groups entities into carteras
-- and assigns auxiliares to them. Cascading: if supervisor loses an entity,
-- it's removed from all their carteras automatically (via JOIN, not trigger).
CREATE TABLE IF NOT EXISTS carteras (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
supervisor_user_id uuid NOT NULL,
nombre text NOT NULL,
descripcion text,
created_at timestamptz DEFAULT now()
);
CREATE INDEX IF NOT EXISTS ix_carteras_supervisor ON carteras(supervisor_user_id);
CREATE TABLE IF NOT EXISTS cartera_entidades (
cartera_id uuid NOT NULL REFERENCES carteras(id) ON DELETE CASCADE,
entidad_id uuid NOT NULL REFERENCES entidades_gestionadas(id) ON DELETE CASCADE,
added_at timestamptz DEFAULT now(),
PRIMARY KEY (cartera_id, entidad_id)
);
CREATE TABLE IF NOT EXISTS cartera_auxiliares (
cartera_id uuid NOT NULL REFERENCES carteras(id) ON DELETE CASCADE,
auxiliar_user_id uuid NOT NULL,
added_at timestamptz DEFAULT now(),
PRIMARY KEY (cartera_id, auxiliar_user_id)
);
INSERT INTO tenant_migrations (scope, version, name)
VALUES ('core', 8, '008_carteras')
ON CONFLICT (scope, version) DO NOTHING;
- Step 2: Create migration 009 — cliente_accesos
Create apps/api/src/migrations/tenant/009_cliente_accesos.sql:
-- Core: direct access grants for external client-viewers.
-- A client user can see specific entities (not via carteras).
CREATE TABLE IF NOT EXISTS cliente_accesos (
user_id uuid NOT NULL,
entidad_id uuid NOT NULL REFERENCES entidades_gestionadas(id) ON DELETE CASCADE,
granted_at timestamptz DEFAULT now(),
PRIMARY KEY (user_id, entidad_id)
);
INSERT INTO tenant_migrations (scope, version, name)
VALUES ('core', 9, '009_cliente_accesos')
ON CONFLICT (scope, version) DO NOTHING;
- Step 3: Create migration 010 — contribuyentes (vertical contable)
Create apps/api/src/migrations/tenant/010_contribuyentes.sql:
-- Vertical CONTABLE: taxpayer subtype of entidades_gestionadas.
-- Uses single-PK inheritance: contribuyentes.entidad_id = entidades_gestionadas.id.
-- CFDI and other fiscal tables will FK to this table (via entidad_id, not a separate id).
CREATE TABLE IF NOT EXISTS contribuyentes (
entidad_id uuid PRIMARY KEY REFERENCES entidades_gestionadas(id) ON DELETE CASCADE,
rfc varchar(13) NOT NULL UNIQUE,
regimen_fiscal varchar(3),
codigo_postal varchar(5),
domicilio jsonb
);
CREATE INDEX IF NOT EXISTS ix_contribuyentes_rfc ON contribuyentes(rfc);
INSERT INTO tenant_migrations (scope, version, name)
VALUES ('vertical-contable', 10, '010_contribuyentes')
ON CONFLICT (scope, version) DO NOTHING;
- Step 4: Commit
git add apps/api/src/migrations/tenant/008_carteras.sql apps/api/src/migrations/tenant/009_cliente_accesos.sql apps/api/src/migrations/tenant/010_contribuyentes.sql
git commit -m "feat(migrations): add carteras, cliente_accesos, contribuyentes tables"
Task 6: Despacho signup service + controller + route
Files:
-
Create:
apps/api/src/services/despacho.service.ts -
Create:
apps/api/src/controllers/despacho.controller.ts -
Create:
apps/api/src/routes/despacho.routes.ts -
Modify:
apps/api/src/app.ts(mount route) -
Step 1: Create despacho service
Create apps/api/src/services/despacho.service.ts:
import { prisma, tenantDb } from '../config/database.js';
import { hashPassword } from '../auth/passwords.js';
import { generateAccessToken, generateRefreshToken } from '../auth/tokens.js';
import type { DespachoSignupRequest, VerticalProfile } from '@horux/shared';
import type { JWTPayload, Role } from '@horux/shared';
export async function signupDespacho(data: DespachoSignupRequest) {
const { despacho, owner } = data;
// Validate uniqueness
const existingTenant = await prisma.tenant.findUnique({ where: { rfc: despacho.rfc } });
if (existingTenant) {
throw new Error('Ya existe una empresa registrada con este RFC');
}
const existingUser = await prisma.user.findUnique({ where: { email: owner.email } });
if (existingUser) {
throw new Error('Ya existe un usuario con este email');
}
const passwordHash = await hashPassword(owner.password);
// Create tenant + user + membership in transaction
const result = await prisma.$transaction(async (tx) => {
// 1. Create tenant as despacho
const databaseName = `horux_${despacho.rfc.toLowerCase().replace(/[^a-z0-9]/g, '')}`;
const tenant = await tx.tenant.create({
data: {
nombre: despacho.nombre,
rfc: despacho.rfc.toUpperCase(),
plan: 'starter',
databaseName,
cfdiLimit: 0,
usersLimit: 3,
verticalProfile: despacho.verticalProfile as any,
dbMode: 'MANAGED' as any,
dbSchemaVersion: 0,
trialEndsAt: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000), // 30 days
codigoPostal: despacho.codigoPostal,
},
});
// 2. Create user
const user = await tx.user.create({
data: {
email: owner.email.toLowerCase(),
passwordHash,
nombre: owner.nombre,
lastTenantId: tenant.id,
},
});
// 3. Create membership as owner
const ownerRole = await tx.rol.findUnique({ where: { nombre: 'owner' } });
if (!ownerRole) throw new Error('Rol owner no encontrado en BD');
await tx.tenantMembership.create({
data: {
userId: user.id,
tenantId: tenant.id,
rolId: ownerRole.id,
isOwner: true,
},
});
return { tenant, user };
});
// 4. Provision tenant database (outside transaction — creates actual DB)
try {
await tenantDb.provisionDatabase(despacho.rfc);
} catch (err: any) {
// If DB provisioning fails, delete the tenant (rollback)
await prisma.tenant.delete({ where: { id: result.tenant.id } });
await prisma.user.delete({ where: { id: result.user.id } });
throw new Error(`Error al crear base de datos del despacho: ${err.message}`);
}
// 5. Generate JWT pair
const payload: Omit<JWTPayload, 'iat' | 'exp'> = {
userId: result.user.id,
email: result.user.email,
role: 'owner' as Role,
tenantId: result.tenant.id,
tokenVersion: 0,
};
const accessToken = generateAccessToken(payload);
const refreshToken = generateRefreshToken(payload);
// 6. Store refresh token
await prisma.refreshToken.create({
data: {
userId: result.user.id,
token: refreshToken,
expiresAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000), // 7 days
},
});
return {
accessToken,
refreshToken,
user: {
id: result.user.id,
email: result.user.email,
nombre: result.user.nombre,
role: 'owner' as Role,
tenantId: result.tenant.id,
tenantName: result.tenant.nombre,
tenantRfc: result.tenant.rfc,
plan: result.tenant.plan,
tenants: [{
id: result.tenant.id,
nombre: result.tenant.nombre,
rfc: result.tenant.rfc,
plan: result.tenant.plan,
role: 'owner' as Role,
isOwner: true,
}],
},
};
}
- Step 2: Create despacho controller
Create apps/api/src/controllers/despacho.controller.ts:
import type { Request, Response, NextFunction } from 'express';
import { z } from 'zod';
import { signupDespacho } from '../services/despacho.service.js';
import { AppError } from '../middlewares/error.middleware.js';
const signupSchema = z.object({
despacho: z.object({
nombre: z.string().min(2, 'Nombre del despacho requerido'),
rfc: z.string().regex(/^[A-ZÑ&]{3,4}\d{6}[A-Z0-9]{3}$/i, 'RFC inválido'),
regimenFiscal: z.string().optional(),
codigoPostal: z.string().regex(/^\d{5}$/, 'Código postal inválido').optional(),
verticalProfile: z.enum(['CONTABLE', 'JURIDICO', 'ARQUITECTURA']),
}),
owner: z.object({
nombre: z.string().min(2, 'Nombre del owner requerido'),
email: z.string().email('Email inválido'),
password: z.string().min(10, 'La contraseña debe tener al menos 10 caracteres'),
}),
});
export async function signup(req: Request, res: Response, next: NextFunction) {
try {
const data = signupSchema.parse(req.body);
const result = await signupDespacho(data);
return res.status(201).json(result);
} catch (error: any) {
if (error instanceof z.ZodError) {
return next(new AppError(400, error.errors[0].message));
}
if (error.message?.includes('Ya existe')) {
return next(new AppError(409, error.message));
}
return next(error);
}
}
- Step 3: Create despacho routes
Create apps/api/src/routes/despacho.routes.ts:
import { Router } from 'express';
import rateLimit from 'express-rate-limit';
import { signup } from '../controllers/despacho.controller.js';
const router = Router();
const signupLimiter = rateLimit({
windowMs: 60 * 60 * 1000, // 1 hour
max: 5,
message: { message: 'Demasiados intentos de registro. Intenta en una hora.' },
});
router.post('/signup', signupLimiter, signup);
export default router;
- Step 4: Mount route in app.ts
Open apps/api/src/app.ts. Find the block where routes are mounted (look for app.use('/api/auth'). Add:
import despachoRoutes from './routes/despacho.routes.js';
// ... in the routes section:
app.use('/api/despachos', despachoRoutes);
- Step 5: Verify typecheck
Run: pnpm --filter @horux/api typecheck
Expected: same pre-existing errors, no new ones. If there are new errors from the despacho files, fix them (likely import paths or type mismatches with Prisma generated types).
Common issue: Prisma might not know about VerticalProfile and DbMode enums yet if the migration wasn't applied. If typecheck fails on verticalProfile: despacho.verticalProfile as any, the as any cast handles it. If stricter typing is needed, verify Prisma client was regenerated (Step 3 of Task 1).
- Step 6: Commit
git add apps/api/src/services/despacho.service.ts apps/api/src/controllers/despacho.controller.ts apps/api/src/routes/despacho.routes.ts apps/api/src/app.ts
git commit -m "feat(api): add POST /api/despachos/signup endpoint"
Task 7: Validation + smoke test
Files: None (verification only)
- Step 1: Verify all packages typecheck
Run:
pnpm --filter @horux/shared typecheck
pnpm --filter @horux/core typecheck
pnpm --filter @horux/shared-ui typecheck
pnpm --filter @horux/api typecheck
Expected: shared/core/shared-ui = 0 errors. api = same pre-existing errors only.
- Step 2: Verify migration files exist and are numbered correctly
Run:
ls -la apps/api/src/migrations/tenant/
Expected: files 001-010 in order. Verify 006-010 are our new ones.
- Step 3: Verify Prisma schema has new fields
Run:
grep -n "verticalProfile\|dbMode\|dbSchemaVersion\|connectorTokenEnc" apps/api/prisma/schema.prisma
Expected: all 4 fields present.
- Step 4: Verify commit history
Run:
git log --oneline -10
Expected: 6 new commits from this plan on top of the Plan 1 refactor commits.
- Step 5: Start dev server and test signup endpoint (MANUAL)
Run: pnpm dev
Test with curl (or user in browser):
curl -X POST http://localhost:4000/api/despachos/signup \
-H "Content-Type: application/json" \
-d '{
"despacho": {
"nombre": "Despacho Test",
"rfc": "DTE250101AAA",
"verticalProfile": "CONTABLE"
},
"owner": {
"nombre": "Test Owner",
"email": "test@despacho.com",
"password": "testpassword123"
}
}'
Expected: 201 with { accessToken, refreshToken, user: { ... } }.
If no DB connection, this step is deferred. The typecheck validation is sufficient for the plan.
- Step 6: Final commit if any fixes were needed
git add -A && git status
# Only commit if there are changes
git commit -m "fix: Plan 2A validation fixes" || true
Self-Review
Spec coverage (vs spec §3-§5, §11, §15-Phase1)
| Spec requirement | Task | Status |
|---|---|---|
| Tenant evolves to support despacho (verticalProfile, dbMode, connector fields) | Task 1 | ✅ |
| New roles: supervisor, cliente | Task 2 | ✅ |
| Shared types: DespachoRole, VerticalProfile, DbMode, DespachoSignupRequest, ContribuyenteInfo | Task 3 | ✅ |
| Tenant migration: tenant_migrations tracking table | Task 4 | ✅ |
| Tenant migration: entidades_gestionadas (core) | Task 4 | ✅ |
| Tenant migration: carteras + cartera_entidades + cartera_auxiliares (core) | Task 5 | ✅ |
| Tenant migration: cliente_accesos (core) | Task 5 | ✅ |
| Tenant migration: contribuyentes (vertical-contable, single-PK inheritance) | Task 5 | ✅ |
| Signup endpoint: POST /despachos/signup | Task 6 | ✅ |
| Trial 30 days | Task 6 (trialEndsAt) | ✅ |
| Managed DB provisioned at signup | Task 6 (provisionDatabase) | ✅ |
| JWT + refresh token on signup | Task 6 | ✅ |
Deferred to Plan 2B:
- CRUD contribuyentes endpoints (add/update/delete RFC within despacho)
- FIEL/CSD assignment to contribuyente (not tenant)
- CFDI emission with contribuyente_id FK
- Metrics tables (metricas_mensuales etc.)
- Magic link auth flow
Deferred to Plan 2C:
- Frontend signup page
- Dashboard adapted for despacho
- Contribuyente selector UI
- Onboarding wizard
Placeholder scan
- No "TBD", "TODO", "implement later" found.
- All code blocks contain complete, copy-paste-ready code.
- Types referenced (DespachoSignupRequest, JWTPayload, Role, etc.) are all defined in tasks.
Type consistency
DespachoRole='owner' | 'supervisor' | 'auxiliar' | 'cliente'— consistent with spec §5.VerticalProfile='CONTABLE' | 'JURIDICO' | 'ARQUITECTURA'— matches Prisma enum.DbMode='BYO' | 'MANAGED'— matches Prisma enum.signupDespacho()acceptsDespachoSignupRequestand returnsLoginResponse-compatible shape.- SQL migrations use
gen_random_uuid(),timestamptz,varchar— consistent with existing migrations. tenant_migrationstable uses(scope, version)PK — matches spec §12.