25 KiB
Sesión 2026-04-30 — Cleanup Horux 360 + Catálogo despacho a tabla + Facturapi Live
Sesión enfocada en eliminar el legacy de Horux 360 (planes, helpers, columnas
huérfanas), mover el catálogo despacho a tabla despacho_plan_prices con
edición desde UI admin, agregar flag de SAT incremental gobernado por BD, y
preparar Facturapi para emitir en modo Live multi-RFC con cache cifrada de
las Live Secret Keys.
12 tareas completadas. Typecheck limpio en 5 paquetes (@horux/shared,
@horux/core, @horux/shared-ui, @horux/vertical-contable, @horux/api).
Smoke test del ciclo completo Facturapi queda para mañana.
Índice
- Cleanup planes Horux 360 legacy
- Catálogo despacho a tabla
despacho_plan_prices - Deuda técnica seed.ts
- Precios
monthly/firstYear/renewaldesde BD - Flag
permite_sat_incrementalen BD - Facturapi Live multi-RFC end-to-end
- Migraciones aplicadas
- Pendientes derivados
- Smoke test plan para mañana
1. Cleanup planes Horux 360 legacy
Problema
Coexistían dos catálogos: PLANS (Horux 360: starter/business/business_ia/
custom/enterprise) y DESPACHO_PLANS (despacho: trial/mi_empresa/
mi_empresa_plus/business_control/business_cloud/custom). El enum Prisma
Plan tenía los 9 mezclados, default starter. Tenants legacy en local;
producción ya no los tiene. feature-gate.middleware.ts bifurcaba entre
ambos catálogos con un Set incompleto que silenciosamente daba 403 a
mi_empresa/mi_empresa_plus/custom para features que sí incluyen.
Decisiones
- No hay tenants legacy en producción → cleanup puro código (con migración de datos local del tenant Horux 360 admin).
customcambia su semántica: monto mensual variable definido al provisionar; si $0 sin Subscription/cobro, si >$0 Subscription con preapproval MP mensual.starter→trialcomo default del enum y signup.Tenant.cfdiLimityTenant.usersLimitse eliminan (limits viven enDESPACHO_PLANS).
Cambios
Datos local:
- 1 tenant
enterprise→custom(Horux 360 admin RFCHTS240708LJA) - 1 subscription
enterprise→custom - 8 filas legacy de
plan_pricesborradas
Schema Prisma:
- Enum
Planestrechado de 9 → 6 valores:trial,custom,business_control,business_cloud,mi_empresa,mi_empresa_plus Tenant.plan @default(trial)Tenant.cfdiLimityTenant.usersLimiteliminadas
Backend refactor:
auth.service.ts:register→ plan defaulttrialtenants.service.ts→createTenant/addTenantToOwner/updateTenanttipos deplanmigrados aDespachoPlandespacho.service.ts:registerDespacho→plan: 'trial'tenants.controller.ts→ schema Zod actualizado, paramscfdiLimit/usersLimitremovidosusuarios.service.ts:inviteUsuario→ maxUsers desde catálogoplan-limits.middleware.ts:checkCfdiLimit→ no-op (límite real es por contribuyente, no por tenant)feature-gate.middleware.ts→ unificado ahasDespachoFeature(cierra bug latente demi_empresa/customcayendo al hasFeature legacy)subscription.controller.ts:VALID_PLANS→ solo planes despachosubscription.service.ts→ type alias localPlanreducidosat-sync.job.ts→ tenant filterenterprise→business_cloud(después se vuelve a refactorizar en sección 5)
Frontend cleanup:
- 4 sidebars/topnav →
hasFeaturelegacy →hasDespachoFeature mis-empresas/page.tsx,clientes/page.tsx,configuracion/suscripcion/ page.tsx,lib/api/tenants.ts→ tipos y selectores migrados a planes despacho
Borrados:
packages/shared/src/constants/plans.ts(legacyPLANS,getPlanLimits,hasFeature, typePlan)- Bloque
planCatalogoDatadel seed - 8 filas legacy de catálogo Horux 360 en
plan_prices(modeloPlanPricequeda huérfano — ver pendientes)
Migración Prisma
20260430184123_cleanup_legacy_plans/migration.sql:
CREATE TYPE "Plan_new" AS ENUM ('trial','custom','business_control',
'business_cloud','mi_empresa','mi_empresa_plus');
ALTER TABLE tenants/subscriptions/plan_prices ALTER COLUMN ... USING ... ;
ALTER TYPE "Plan" RENAME TO "Plan_old"; DROP TYPE;
ALTER TABLE tenants DROP COLUMN cfdi_limit, DROP COLUMN users_limit,
ALTER COLUMN plan SET DEFAULT 'trial';
2. Catálogo despacho a tabla despacho_plan_prices
Problema
El catálogo despacho estaba 100% hardcoded en
packages/shared/src/constants/despacho-plans.ts. Existía una tabla
PlanCatalogo huérfana con datos viejos sin uso. Existía
despacho_plan_prices solo para precios. Admin no podía ajustar
precios/limits sin redeploy.
Decisión: Opción B
- Limits + precios en BD (
despacho_plan_pricesextendida) - Features siguen en TS (acopladas a UI/middleware, contrato de código)
PlanCatalogohuérfano se elimina del schema- Conservar el nombre
despacho_plan_prices(no renombrar)
Cambios
Schema:
DespachoPlanPriceextendido con:nombre VARCHAR(50),maxRfcs INT,maxUsers INT,timbresIncluidosMes INT default 0,dbMode DbMode,permiteServidorBackup BOOLEAN default falsefirstYearyrenewalcambiados aDecimal?(nullable para trial/custom)PlanCatalogomodel eliminado (drop table)
Migración aditiva no destructiva:
- ADD COLUMN como nullable
- UPDATE backfill desde catálogo TS para las 4 filas existentes
- ALTER COLUMN ... SET NOT NULL
- ALTER first_year/renewal DROP NOT NULL
Seed:
- 6 filas (incluye
trialycustomcon precios null) UPSERT idempotente - Bloque legacy
planCatalogoDataeliminado
Service nuevo plan-catalogo.service.ts:
interface DespachoPlanLimits(precio + limits)getDespachoPlanLimits(plan)async con cache 5min en memoriagetAllDespachoPlanLimits()igualinvalidateDespachoPlanCache()para post-edit adminlistPlans/listAddons/getPlanByCodenamereescritos para leer dedespacho_plan_prices(anteriormente leían dePlanCatalogohuérfano)
Backend callers migrados:
usuarios.service.ts:inviteUsuario→maxUsersdesdegetDespachoPlanLimits(BD) en vez de objeto TS
Backend endpoints nuevos:
GET /api/planes/despacho→ listDespachoCatalogo (auth required)PATCH /api/planes/despacho/:plan→ updateDespachoCatalogo (canEditPrices)- Schema Zod:
nombre,monthly,firstYear,renewal,permiteMonthly,maxRfcs,maxUsers,timbresIncluidosMes,dbMode,permiteServidorBackup,permiteSatIncremental - Llama
invalidateDespachoPlanCache()post-update
- Schema Zod:
UI admin reescrita:
apps/web/app/(dashboard)/configuracion/precios-suscripcion/page.tsx:- Lee de
/api/planes/despacho(BD) en vez de catálogo TS - 9 columnas: Plan, Mensual, Anual 1°, Renovación, RFCs, Usuarios, Timbres, DB, Backup + (en sección 5: SAT Inc)
- Edición inline: lápiz → inputs → ✓ guarda / ✗ cancela
- useMutation invalida
['despacho-catalogo']post-edit - Botón Save deshabilitado durante mutación
- Lee de
Documentación split TS/BD:
- Comentario al inicio de
DESPACHO_PLANS(TS) explica:features→ autoritativo en TS (acoplado a UI/middleware)name,maxRfcs,maxUsers,timbresIncluidosMes,dbMode,permiteServidorBackup→ defaults en TS, autoritativo en BDmaxCfdisPorContribuyente→ solo en TS (no se gating en runtime)
Estado en BD post-migración
plan | nombre | monthly | first_year | renewal | maxRfcs | maxUsers | timbres | dbMode | backup
trial | Prueba | - | - | - | 3 | 1 | 20 | MANAGED | -
mi_empresa | Mi Empresa | 580 | 5800 | 5800 | 1 | 3 | 50 | MANAGED | -
mi_empresa_plus | Mi Empresa + | 900 | 9000 | 9000 | 1 | 3 | 50 | MANAGED | -
business_control | Business Control | - | 25850 | 25850 | 100 | -1 | 0 | BYO | ✓
business_cloud | Enterprise | - | 43000 | 43000 | 100 | -1 | 0 | BYO | ✓
custom | Custom | - | - | - | 1 | 3 | 50 | MANAGED | -
3. Deuda técnica seed.ts
Bugs encontrados al re-correr seed (ya no funcionaba después de F6 + cleanup planes):
| Bug | Fix |
|---|---|
trial_usages.rfc varchar(13) no admite slugs DESPACHO_xxx (~22 chars) del backfill |
Filtrar WHERE LENGTH(rfc) <= 13 (slugs no participan del padrón anti-abuso de trial) |
Backfill user_platform_roles referenciaba users.tenant_id/rol_id (eliminados en F6) |
Migrado a tenant_memberships |
Demo users creados con tenantId/rolId (eliminados en F6) |
Crear User sin tenant + crear membership por separado |
CFDIs demo ON CONFLICT (uuid) DO NOTHING falla — el UNIQUE en cfdis.uuid se reemplazó por índice funcional LOWER(uuid) (migración 027) que no soporta ON CONFLICT por columna plana |
Removido — los UUIDs son crypto.randomUUID() y las tablas se dropean cada seed; sin colisión posible |
Seed Tenant demo con plan: 'business' + cfdiLimit/usersLimit |
Cambiado a plan: 'mi_empresa_plus', columnas eliminadas |
Bloque planCatalogoData (4 filas que insertaban en PlanCatalogo huérfano) |
Eliminado |
pnpm db:seed ahora corre end-to-end exitoso.
4. Precios monthly/firstYear/renewal desde BD
Problema
Después de #2, el admin podía editar precios desde UI pero
subscription.service.ts:getPlanPrice seguía leyendo del catálogo TS
hardcoded vía getPrecioDespacho. Editar precio en UI no afectaba al
cobro real.
Cambios
Service plan-catalogo.service.ts extendido con 3 helpers async:
permiteFrecuenciaMensualDb(plan)— lee BD via cachedespachoPlanTieneDualidadDb(plan)—firstYear !== renewaldesde BDgetPrecioDespachoDb(plan, frequency, phase)— precio desde BD; throws si plan no existe o no permite frecuencia
subscription.service.ts:getPlanPrice simplificado:
- Removida rama legacy
prisma.planPrice.findUnique(planes Horux 360 no existen) - Reemplazado
getPrecioDespacho+permiteFrecuenciaMensualcon un sologetPrecioDespachoDb(plan, frequency, phase) customsigue throwando (monto variable se fija al provisionar)
subscription.service.ts:436:
despachoPlanTieneDualidad(plan)→await despachoPlanTieneDualidadDb(plan)
webhook.controller.ts:174:
- Mismo cambio en el handler que ajusta preapproval tras primer pago
Imports limpiados:
- Removidos del shared:
DESPACHO_PLAN_PRICES,getPrecioDespacho,permiteFrecuenciaMensual,despachoPlanTieneDualidad(siguen exportados por el shared package, pero el backend ya no los usa).
Resultado
Si admin baja el precio de Mi Empresa de $580 a $400 desde la UI, el
próximo subscribe cobra $400. Cache se invalida automáticamente vía
invalidateDespachoPlanCache() en el PATCH.
5. Flag permite_sat_incremental en BD
Problema
La extracción SAT incremental (3 syncs/día adicionales al daily) estaba
hardcoded para plan business_cloud solo. El usuario quiere que aplique
también a mi_empresa_plus y business_control.
Decisión: Opción B
Flag editable en BD (siguiendo el patrón de permite_servidor_backup)
en vez de hardcoded IN (...). Admin gobierna desde UI sin redeploy.
Cambios
Schema:
DespachoPlanPrice.permiteSatIncremental Boolean @default(false)- Migración aditiva con backfill:
mi_empresa_plus,business_control,business_cloud→true
Helper service:
interface DespachoPlanLimitsextendido conpermiteSatIncremental
Cron sat-sync.job.ts:
getEnterpriseTenantsWithFiel()renombrado agetTenantsConSatIncremental()- Query refactorizada:
findManyendespachoPlanPricefiltrandopermiteSatIncremental: true→ IN sobretenants.plan - Lee directo de Prisma (no usa el cache de 5min) para decisiones in-instant en cada disparo
Endpoint admin:
- Schema Zod del PATCH acepta
permiteSatIncremental - Response incluye el campo
UI admin:
- Nueva columna "SAT Inc" con check verde / dash
- Checkbox editable inline
- Nota explicativa al pie: "3 syncs SAT extra al día (11:00, 15:00, 19:00) además del daily de las 03:00. Ventana de 8h por sync, deduplicado por UUID. Latencia ~1-2h en horario laboral vs ~24h con solo el daily."
Seed:
- Catálogo despacho actualizado con
permiteSatIncrementalpara los 6 planes
Estado actual
| Plan | permite_sat_incremental |
|---|---|
| trial | false |
| mi_empresa | false |
| mi_empresa_plus | true ✓ |
| business_control | true ✓ |
| business_cloud | true ✓ |
| custom | false |
Latencia esperada
| Plan | Daily 03:00 | Inc 11/15/19 | Worst case |
|---|---|---|---|
| trial, mi_empresa, custom | ✓ | ✗ | ~24h |
| mi_empresa_plus, business_control, business_cloud | ✓ | ✓ | ~5h (emit ~22:00); ~1-2h (horario laboral) |
6. Facturapi Live multi-RFC end-to-end
Problema
El working dir usaba modo Test/Sandbox (GET /apikeys/test). Para
producción Live multi-RFC se necesita:
- Cada organización Facturapi (1:1 con contribuyente) tiene su propia
sk_live_xxx - Endpoint oficial:
PUT /v2/organizations/{id}/apikeys/live(idempotente) - Cada Live Secret Key se debe almacenar (cifrada, decisión del usuario)
- El primer emit no debe pagar el costo del PUT — eager generation tras crear la org
Adicional: en Live, SAT rechaza CFDIs con relatedDocuments mal-formados
(ej. UUID inexistente, cancelado, RFC receptor que no coincide). Hay que
validar antes de consumir timbre.
Decisiones del usuario
- Encriptar la api_key (AES-256-GCM con derivación
FIEL_ENCRYPTION_KEY, mismo patrón que credenciales FIEL) - Eager generation: tras crear org, PUT
/apikeys/live+ cifrar + guardar inmediatamente - Sí al paquete completo: validaciones controller + estructura
relatedDocumentsSAT 4.0
Cambios
Migraciones:
- Tenant:
041_facturapi_orgs_api_key_enc.sqlagregaapi_key_enc/iv/tag BYTEAafacturapi_orgs - Central:
20260430230000_add_facturapi_org_key_enc/migration.sqlagregaTenant.facturapiOrgKeyEnc/Iv/Tag BYTEA
Service contribuyente-facturapi.service.ts (multi-RFC):
generateLiveKey(orgId)— helper PUT idempotente; valida que la respuesta empiece consk_live_; throws con error legible si Facturapi rechazapersistEncryptedKey(pool, orgId, plaintextKey)—encryptStringAES-256-GCM + UPDATEgetOrgApiKey(pool, orgId)reescrito:- SELECT
api_key_enc/iv/tagdesdefacturapi_orgs - Si existe →
decryptToStringy retornar - Si no (org legacy sin key cacheada) →
generateLiveKey→persistEncryptedKey→ retornar
- SELECT
createOrgContribuyenteahora eager:client.organizations.create({name})con User Key- INSERT en
facturapi_orgs ensureLiveKeyCached(pool, org.id)— PUT live + cifrar + guardar
ensureLiveKeyCached— idempotente: si ya hay key cifrada, no-op; si no, genera y persiste
Service facturapi.service.ts (tenant central — Horux 360 emite a
clientes):
- Mismo patrón:
generateLiveKey,getOrgClient(tenantId)lee BD cifrada → fallback PUT live + persist createOrganization(tenantId)eager: tras crear org, PUT live + cifrar- UPDATE
Tenant.facturapiOrgKeyEnc/Iv/Tag
- UPDATE
Estructura relatedDocuments SAT 4.0:
- Working dir antes:
Array<{ uuid: string; relationship: string }>— una entrada por UUID (Facturapi Live rechaza por validación SAT estricta) - Ahora:
Array<{ relationship: string; uuids: string[] }>— una entrada por tipo de relación, agrupando N UUIDs - Backend:
documents: r.uuidsen lugar dedocuments: [r.uuid] - Service contribuyente acepta ambos formatos para compat (legacy
r.uuidfallback a[r.uuid])
Validaciones controller facturacion.controller.ts:emitir:
Antes de consumir timbre, valida cada UUID en relatedDocuments:
- Existe en
cfdisdel tenant (case-insensitiveLOWER(uuid)) - No cancelado (
status NOT IN ('Cancelado', '0')) - Match RFC receptor:
cfdis.rfc_receptorvscustomer.taxIddel CFDI nuevo (uppercase + trim) - Si alguno falla →
AppError(400, ...)con mensaje legible (sin consumir timbre)
Frontend facturacion/page.tsx:639:
- Manda
{ relationship: relatedRelationship, uuids: [relatedUuid] }(formato SAT 4.0 nuevo)
Flow nuevo de provisioning
createOrgContribuyente(pool, contribId, nombre)
↓ User Key
client.organizations.create({name})
↓
INSERT INTO facturapi_orgs (contribuyente_id, facturapi_org_id)
↓
ensureLiveKeyCached(pool, orgId)
↓ User Key
PUT /v2/organizations/{orgId}/apikeys/live ← idempotente, devuelve sk_live_xxx
↓
encryptString(sk_live_xxx) ← AES-256-GCM con FIEL_ENCRYPTION_KEY
↓
UPDATE facturapi_orgs SET api_key_enc, api_key_iv, api_key_tag
Flow nuevo de emisión
getOrgClientContribuyente(pool, contribId)
↓
SELECT facturapi_org_id FROM facturapi_orgs
↓
getOrgApiKey(pool, orgId)
↓
SELECT api_key_enc, api_key_iv, api_key_tag FROM facturapi_orgs
↓ si existe
decryptToString(...) → sk_live_xxx
↓ si NO existe (org legacy)
generateLiveKey(orgId) → persistEncryptedKey → return
↓
new Facturapi(sk_live_xxx)
Validación pre-emisión
emitir() controller
↓
relatedDocs = req.body.relatedDocuments || []
customerRfc = req.body.customer?.taxId?.toUpperCase().trim()
↓ por cada uuid en relatedDocs.flatMap(r => r.uuids)
SELECT rfc_receptor, status FROM cfdis WHERE LOWER(uuid) = LOWER($1)
↓ si no existe / cancelado / RFC no match
throw AppError(400, mensaje específico)
↓ todo OK
consumeTimbre() → createInvoiceContribuyente() → ...
Diferencias con producción
Producción tiene un approach plain text (sin cifrar) y lazy (solo genera la live key al primer emit). El working dir ahora es cifrado y eager (más seguro y la org queda lista para emitir desde el momento 0 sin un PUT extra).
7. Migraciones aplicadas
| Fecha | ID | Descripción |
|---|---|---|
| 2026-04-30 | 20260430184123_cleanup_legacy_plans |
Estrechar enum Plan + drop cfdi_limit/users_limit |
| 2026-04-30 | 20260430195000_extend_despacho_plan_prices_with_limits |
Agregar nombre/maxRfcs/maxUsers/timbres/dbMode/backup a despacho_plan_prices con backfill aditivo |
| 2026-04-30 | 20260430200000_drop_plan_catalogo_orphan |
Drop tabla plan_catalogo huérfana |
| 2026-04-30 | 20260430215000_add_permite_sat_incremental |
Agregar permite_sat_incremental a despacho_plan_prices con backfill |
| 2026-04-30 | 20260430230000_add_facturapi_org_key_enc |
Agregar Tenant.facturapiOrgKeyEnc/Iv/Tag BYTEA |
| 2026-04-30 | tenant/041_facturapi_orgs_api_key_enc.sql |
Agregar facturapi_orgs.api_key_enc/iv/tag BYTEA |
8. Pendientes derivados
Bloqueantes para producción
- Aplicar las 5 migraciones Prisma + 1 tenant en producción (en
orden cronológico). La tenant se aplica vía
pnpm db:migrate-tenants(lazy engetPooltambién). - Variable de entorno: confirmar
FIEL_ENCRYPTION_KEYconfigurada en prod (mismo valor o key rotation strategy si se rota).
Eliminar deuda restante
- Modelo
PlanPricequeda huérfano (sin filas, sin callers en código activo). Drop del schema en próxima release. DESPACHO_PLAN_PRICES(TS) ya no se usa en backend pero sigue exportado del shared. Algún caller frontend podría seguir consumiéndolo → grepDESPACHO_PLAN_PRICESenapps/web.
Mejoras opcionales
- Smoke test del ciclo Facturapi Live completo — pendiente para mañana (ver sección 9).
Frequencytype ensubscription.service.tsy el'monthly'| 'annual'que recibegetPrecioDespachoDbson tipos paralelos. Si se agrega una frecuencia (e.g.'quarterly'), sincronizar ambos lados.- Errores typecheck pre-existentes en web (cfdi/page.tsx, sidebar*, usuarios/page.tsx, etc.) — no relacionados a esta sesión, persisten.
bootstrap-horux360-admin.tssigue con nombre legacy. Renombrar abootstrap-platform-admin.tscuando convenga (no urgente).
9. Smoke test plan para mañana
Setup
pnpm devcon dev server arriba (API :4000, web :3000)- Login como admin@demo.com / demo123 (tenant demo
EDE123456AB1) - Verificar
FACTURAPI_USER_KEYyFIEL_ENCRYPTION_KEYen.env
Test 1 — Catálogo BD funciona
- Ir a
/configuracion/precios-suscripcion(login como admin global) - Esperado: 9 columnas + 6 planes con limits desde BD
- Editar
mi_empresa.maxUsersde 3 → 5 → guardar - Verificar SQL:
SELECT max_users FROM despacho_plan_prices WHERE plan='mi_empresa'→ 5 - Cancelar edit en otro plan → no se persiste
Test 2 — Toggle SAT Inc desde UI
- Editar
mi_empresa→ marcar checkbox SAT Inc → guardar - Verificar SQL:
permite_sat_incremental = t - Revertir (UI o SQL)
Test 3 — Precio editado aplica al cobro
- Editar
mi_empresa.firstYearde 5800 → 5000 - Cualquier nuevo
subscribecon planmi_empresa annualdebe cobrar 5000 - Revertir a 5800
Test 4 — Crear contribuyente con auto-org Facturapi (Live)
- Ir a
/contribuyentes(o equivalente) - Crear contribuyente con RFC válido + nombre
- Verificar SQL en BD tenant:
PGPASSWORD='Hesoy@m11' psql -h localhost -U postgres -d horux_ede123456ab1 \ -c "SELECT contribuyente_id, facturapi_org_id, csd_uploaded, (api_key_enc IS NOT NULL) AS has_live_key, octet_length(api_key_enc) AS enc_size, octet_length(api_key_iv) AS iv_size, octet_length(api_key_tag) AS tag_size FROM facturapi_orgs;" - Esperado:
has_live_key = t,iv_size = 12,tag_size = 16,enc_size ~50-60bytes
Test 5 — Subir CSD del contribuyente
- Subir
.cer + .key + passworddel CSD - Esperado:
csd_uploaded = truesin errores (validación CSD vs RFC del contribuyente)
Test 6 — Emitir factura tipo I
- Ir a
/facturacion, crear factura tipo I (Ingreso) con receptor PÚBLICO en general (XAXX010101000) o un RFC con datos válidos - Esperado: factura emitida sin errores; logs muestran
decryptToString(api_key_enc, ...)(sin PUT live extra) - Verificar timbre consumido:
SELECT * FROM timbre_suscripciones WHERE tenant_id = ...
Test 7 — Emitir tipo E (Egreso/NC) con CFDI relacionado
- Crear factura tipo E que referencie un UUID de una factura I previa
- Validar que la validación pre-emit pasa (UUID existe + vigente + match RFC receptor)
- Probar caso negativo: pasar UUID inventado → esperado AppError(400) con mensaje "El CFDI relacionado con UUID X no existe"
- Probar caso negativo: pasar UUID con RFC receptor diferente al
customer.taxId→ esperado mensaje específico
Test 8 — Emitir tipo P (Complemento de Pago)
- Crear factura tipo P referenciando una factura PPD previa
- Validar emisión + persistencia con
cfdi_tipo_relacion='04'ycfdis_relacionados=UUID|UUID|...
Test 9 — Idempotencia eager
- Eliminar contribuyente y recrearlo con mismo RFC
- Esperado:
client.organizations.createse llama;ensureLiveKeyCachedgenera nueva live key; no errores - Verificar que
api_key_encse actualizó (timestamp updated_at vs anterior)
Comandos útiles para debugging
# Ver logs del backend en tiempo real
tail -f /tmp/claude/.../tasks/<id>.output
# Inspeccionar facturapi_orgs cifrado
PGPASSWORD='Hesoy@m11' psql -h localhost -U postgres -d horux_ede123456ab1 \
-c "SELECT contribuyente_id, facturapi_org_id, csd_uploaded,
encode(api_key_enc, 'hex') as enc_hex,
octet_length(api_key_enc) as enc_size FROM facturapi_orgs;"
# Inspeccionar Tenant central
PGPASSWORD='Hesoy@m11' psql -h localhost -U postgres -d horux_despachos \
-c "SELECT nombre, rfc, plan, facturapi_org_id,
(facturapi_org_key_enc IS NOT NULL) AS has_live_key
FROM tenants WHERE rfc = 'HTS240708LJA';"
Si algo falla
- Facturapi PUT 4xx: verificar
FACTURAPI_USER_KEYválida, org_id existe en dashboard de Facturapi, no se hizoclient.organizations.deletedesde otro lado - decryptToString throws: el cifrado está corrupto (api_key_enc/iv/tag
inconsistentes). Borrar la fila y dejar que
getOrgApiKeyregenere - AppError "no existe en el sistema": el UUID relacionado no se sincronizó vía SAT sync — el daily de 03:00 lo pondría disponible. O el CFDI nunca existió.
- "RFC no corresponde": el
customer.taxIdque se manda en el body no coincide con elrfc_receptordel UUID relacionado. Es validación SAT correcta, no bug.