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