diff --git a/apps/api/src/controllers/tenants.controller.ts b/apps/api/src/controllers/tenants.controller.ts index 645cf9a..2f74e4f 100644 --- a/apps/api/src/controllers/tenants.controller.ts +++ b/apps/api/src/controllers/tenants.controller.ts @@ -52,7 +52,7 @@ export async function createTenant(req: Request, res: Response, next: NextFuncti try { await requireGlobalAdmin(req); - const { nombre, rfc, plan, adminEmail, adminNombre, amount, firstPaymentDueAt } = req.body; + const { nombre, rfc, plan, adminEmail, adminNombre, amount, firstPaymentDueAt, verticalProfile, codigoPostal } = req.body; if (!nombre || !rfc || !adminEmail || !adminNombre) { throw new AppError(400, 'Nombre, RFC, adminEmail y adminNombre son requeridos'); @@ -66,6 +66,8 @@ export async function createTenant(req: Request, res: Response, next: NextFuncti adminNombre, amount: amount || 0, firstPaymentDueAt: firstPaymentDueAt || null, + verticalProfile: verticalProfile || 'CONTABLE', + codigoPostal, }); res.status(201).json(result); diff --git a/apps/api/src/services/tenants.service.ts b/apps/api/src/services/tenants.service.ts index a456138..25ed8d7 100644 --- a/apps/api/src/services/tenants.service.ts +++ b/apps/api/src/services/tenants.service.ts @@ -15,6 +15,8 @@ export async function getAllTenants() { plan: true, databaseName: true, createdAt: true, + verticalProfile: true, + codigoPostal: true, _count: { select: { memberships: { where: { active: true } } as any } }, @@ -43,6 +45,8 @@ export async function getTenantById(id: string) { plan: true, databaseName: true, createdAt: true, + verticalProfile: true, + codigoPostal: true, } }); } @@ -57,6 +61,8 @@ export async function createTenant(data: { /** Solo plan custom: primera fecha de pago (deadline para que el cliente * complete su primer cobro). Se persiste como Subscription.currentPeriodEnd. */ firstPaymentDueAt?: string | null; + verticalProfile?: 'CONTABLE' | 'JURIDICO' | 'ARQUITECTURA'; + codigoPostal?: string; }) { const plan = data.plan || 'trial'; @@ -70,12 +76,18 @@ export async function createTenant(data: { }).catch(err => console.error('[METABASE] Register failed:', err)); // 2. Create tenant record + const isTrial = plan === 'trial'; + const tenant = await prisma.tenant.create({ data: { nombre: data.nombre, rfc: data.rfc.toUpperCase(), plan, databaseName, + dbMode: 'MANAGED', + verticalProfile: data.verticalProfile || 'CONTABLE', + codigoPostal: data.codigoPostal || undefined, + trialEndsAt: isTrial ? new Date(Date.now() + 30 * 24 * 60 * 60 * 1000) : undefined, } }); diff --git a/apps/web/app/(dashboard)/clientes/page.tsx b/apps/web/app/(dashboard)/clientes/page.tsx index 62baa09..4f652ab 100644 --- a/apps/web/app/(dashboard)/clientes/page.tsx +++ b/apps/web/app/(dashboard)/clientes/page.tsx @@ -84,6 +84,7 @@ export default function ClientesPage() { adminNombre: string; amount: number; firstPaymentDueAt: string; + verticalProfile: 'CONTABLE' | 'JURIDICO' | 'ARQUITECTURA'; }>({ nombre: '', rfc: '', @@ -92,6 +93,7 @@ export default function ClientesPage() { adminNombre: '', amount: 0, firstPaymentDueAt: '', + verticalProfile: 'CONTABLE', }); // Only global admin can access this page @@ -122,7 +124,7 @@ export default function ClientesPage() { } else { await createTenant.mutateAsync(formData); } - setFormData({ nombre: '', rfc: '', plan: 'trial', adminEmail: '', adminNombre: '', amount: 0, firstPaymentDueAt: '' }); + setFormData({ nombre: '', rfc: '', plan: 'trial', adminEmail: '', adminNombre: '', amount: 0, firstPaymentDueAt: '', verticalProfile: 'CONTABLE' }); setShowForm(false); } catch (error) { console.error('Error:', error); @@ -142,6 +144,7 @@ export default function ClientesPage() { firstPaymentDueAt: sub?.currentPeriodEnd ? new Date(sub.currentPeriodEnd).toISOString().slice(0, 10) : '', + verticalProfile: tenant.verticalProfile || 'CONTABLE', }); setShowForm(true); }; @@ -159,7 +162,7 @@ export default function ClientesPage() { const handleCancelForm = () => { setShowForm(false); setEditingTenant(null); - setFormData({ nombre: '', rfc: '', plan: 'trial', adminEmail: '', adminNombre: '', amount: 0, firstPaymentDueAt: '' }); + setFormData({ nombre: '', rfc: '', plan: 'trial', adminEmail: '', adminNombre: '', amount: 0, firstPaymentDueAt: '', verticalProfile: 'CONTABLE' }); }; const handleViewClient = (tenantId: string, tenantName: string) => { @@ -477,6 +480,25 @@ export default function ClientesPage() { +
+ + +
+ {/* Campos de admin — solo al crear */} {!editingTenant && (
diff --git a/apps/web/lib/api/tenants.ts b/apps/web/lib/api/tenants.ts index 14648b3..702d662 100644 --- a/apps/web/lib/api/tenants.ts +++ b/apps/web/lib/api/tenants.ts @@ -14,6 +14,8 @@ export interface Tenant { plan: string; databaseName: string; createdAt: string; + verticalProfile?: 'CONTABLE' | 'JURIDICO' | 'ARQUITECTURA' | null; + codigoPostal?: string | null; _count?: { /** Memberships activos (matches el `_count.memberships` que retorna `getAllTenants`). */ memberships: number; @@ -32,6 +34,10 @@ export interface CreateTenantData { amount?: number; /** Solo plan custom: deadline para primer pago (formato ISO YYYY-MM-DD). */ firstPaymentDueAt?: string | null; + /** Tipo de despacho (CONTABLE, JURIDICO, ARQUITECTURA). Default: CONTABLE */ + verticalProfile?: 'CONTABLE' | 'JURIDICO' | 'ARQUITECTURA'; + /** Código postal del domicilio fiscal (5 dígitos) */ + codigoPostal?: string; } export async function getTenants(): Promise { diff --git a/docs/sessions/2026-05-04-fix-clientes-crea-despacho.md b/docs/sessions/2026-05-04-fix-clientes-crea-despacho.md new file mode 100644 index 0000000..aeef8d5 --- /dev/null +++ b/docs/sessions/2026-05-04-fix-clientes-crea-despacho.md @@ -0,0 +1,130 @@ +# Sesión: Fix — Crear cliente desde `/clientes` registra como despacho + +**Fecha:** 2026-05-04 +**Bug:** El admin global creaba tenants "legacy" (sin `dbMode`, `verticalProfile`, `trialEndsAt`) desde `/clientes`. Ahora se registran como despachos completos. + +--- + +## 1. Problema + +### Síntoma +Cuando el administrador global creaba un nuevo cliente desde `/clientes`, el tenant se creaba sin los campos de despacho: +- `dbMode` = `null` (debería ser `MANAGED`) +- `verticalProfile` = `null` (debería ser `CONTABLE`/`JURIDICO`/`ARQUITECTURA`) +- `trialEndsAt` = `null` (debería ser +30 días para plan trial) +- `codigoPostal` no se seteaba + +Esto causaba que el nuevo cliente no funcionara correctamente como despacho (no aparecía en el selector de despachos, no tenía trial, etc.). + +### Causa raíz +El flujo de **registro público** (`/register-despacho` → `signupDespacho()`) sí seteaba estos campos correctamente. +Pero el flujo de **creación por admin** (`/clientes` → `createTenant()`) no lo hacía — solo creaba el tenant con `nombre`, `rfc`, `plan`, `databaseName`. + +--- + +## 2. Cambios realizados + +### 2.1 Backend — `tenants.service.ts` + +**Firma ampliada:** +```ts +export async function createTenant(data: { + // ... campos existentes ... + verticalProfile?: 'CONTABLE' | 'JURIDICO' | 'ARQUITECTURA'; + codigoPostal?: string; +}) +``` + +**Creación del tenant ahora setea despacho fields:** +```ts +const tenant = await prisma.tenant.create({ + data: { + nombre: data.nombre, + rfc: data.rfc.toUpperCase(), + plan, + databaseName, + dbMode: 'MANAGED', + verticalProfile: data.verticalProfile || 'CONTABLE', + codigoPostal: data.codigoPostal || undefined, + trialEndsAt: isTrial ? new Date(Date.now() + 30 * 24 * 60 * 60 * 1000) : undefined, + } +}); +``` + +**Queries `getAllTenants` y `getTenantById` ahora retornan:** +- `verticalProfile` +- `codigoPostal` + +### 2.2 Backend — `tenants.controller.ts` + +El endpoint `POST /tenants` ahora acepta y pasa: +- `verticalProfile` (default: `'CONTABLE'`) +- `codigoPostal` (opcional) + +### 2.3 Frontend — `clientes/page.tsx` + +**Nuevo campo en el formulario:** `Tipo de Despacho` +- Select con opciones: Contable, Jurídico, Arquitectura +- Default: Contable +- Se persiste al crear y se carga al editar + +**C.P. deliberadamente omitido:** +El campo `codigoPostal` **no** se incluyó en el formulario. Se obtiene automáticamente de la CSF cuando el dueño del despacho sube su FIEL por primera vez (`fiel.service.ts` → `consultarConstancia()` → `sincronizarDatosFiscales()`). + +### 2.4 Frontend — `lib/api/tenants.ts` + +Tipos actualizados: +```ts +export interface Tenant { + // ... campos existentes ... + verticalProfile?: 'CONTABLE' | 'JURIDICO' | 'ARQUITECTURA' | null; + codigoPostal?: string | null; +} + +export interface CreateTenantData { + // ... campos existentes ... + verticalProfile?: 'CONTABLE' | 'JURIDICO' | 'ARQUITECTURA'; + codigoPostal?: string; +} +``` + +--- + +## 3. Flujo resultante + +``` +Admin global crea cliente en /clientes + ↓ +Tenant creado con: + dbMode = 'MANAGED' + verticalProfile = (seleccionado por admin, default CONTABLE) + trialEndsAt = +30 días (si plan = trial) + codigoPostal = undefined (se llenará después) + ↓ +Dueño del despacho entra a la plataforma + ↓ +Sube FIEL del despacho (primer contribuyente) + ↓ +Sistema extrae CSF automáticamente + ↓ +Tenant actualizado con codigoPostal, calle, estado, regímenes... +``` + +--- + +## 4. Archivos modificados + +| Archivo | Cambio | +|---------|--------| +| `apps/api/src/services/tenants.service.ts` | `createTenant` setea `dbMode`, `verticalProfile`, `trialEndsAt`, `codigoPostal`; queries retornan nuevos campos | +| `apps/api/src/controllers/tenants.controller.ts` | Acepta `verticalProfile` y `codigoPostal` del body | +| `apps/web/app/(dashboard)/clientes/page.tsx` | Formulario con selector de `verticalProfile`; C.P. omitido (viene de CSF) | +| `apps/web/lib/api/tenants.ts` | Tipos `Tenant` y `CreateTenantData` ampliados | + +--- + +## 5. Notas + +- El campo `codigoPostal` del backend es **opcional** y se dejó en la firma por si en el futuro se quiere pasar manualmente (ej. onboarding asistido). +- No se requirió migración de base de datos — `codigoPostal` ya existía como columna `String?` en `Tenant`. +- `trialEndsAt` se calcula como `Date.now() + 30 días` para plan `trial`, igual que en `signupDespacho()`.