# DOCUMENTO DE DISEÑO — EVOLUCIÓN DE `Company` CON DIRECCIÓN EMPRESARIAL ## 1. Objetivo Definir la evolución del agregado `Company` para incorporar una dirección empresarial utilizable por el ERP, especialmente por configuración, onboarding y reporting. Esta fase consolida exclusivamente: - dirección empresarial; - país mediante código ISO 3166-1 alpha-2. Quedan expresamente fuera de esta evolución: - idioma por defecto de empresa; - moneda por defecto de empresa; - logo de empresa; - múltiples direcciones; - sedes/sucursales; - snapshot del emisor en facturas emitidas; - internacionalización completa de códigos postales. El diseño debe mantenerse: ```txt explícito simple compatible con empresas existentes coherente con DDD pragmático sin sobreingeniería ``` --- ## 2. Contexto arquitectónico `Company` pertenece al módulo: ```txt modules/companies ``` La arquitectura general mantiene: ```txt Domain ↓ Application ↓ Infrastructure ``` con las reglas ya consolidadas del ERP: - `Domain` no conoce HTTP, Sequelize ni Infrastructure; - `Application` no conoce Express ni modelos ORM; - `Infrastructure` implementa persistencia, transporte y DI; - Zod v4 es la SSOT de los contratos HTTP; - otros módulos consumen `companies` mediante servicios públicos explícitos; - no se exponen repositorios ni infraestructura interna. La dirección empresarial no debe vivir en `onboarding`, `customer-invoices`, `reports` ni ningún contenedor genérico de settings. Su SSOT debe ser `Company`. --- ## 3. Estado actual de `Company` Campos actuales relevantes: ```txt id legalName tradeName tin slug email phone website defaultPaymentTermId status createdAt updatedAt ``` No existen actualmente en `Company`: ```txt address countryCode defaultLanguage defaultCurrency logo ``` Esto provoca que consumidores como el perfil de informes de proformas no puedan obtener una dirección empresarial real desde `companies`. --- ## 4. Decisión principal Se incorpora a `Company` una única propiedad: ```ts address: Maybe ``` La dirección será: - propiedad del agregado `Company`; - única en V1; - embebida en la tabla `companies`; - nullable para compatibilidad con empresas existentes; - completa cuando esté presente. No se crea: ```txt CompanyAddress FiscalAddress CompanyPostalAddress company_addresses CompanyProfile SettingsBag ``` La existencia de una sola dirección empresarial no justifica una entidad ni una tabla independiente. --- ## 5. Naming El nombre definitivo es: ```txt address ``` No: ```txt fiscalAddress registeredAddress legalAddress ``` Motivo: - actualmente solo existe una dirección empresarial; - el reporting ya utiliza conceptualmente `address`; - no existe hoy una necesidad real de diferenciar dirección fiscal, postal, sede o envío; - introducir esa distinción ahora anticiparía requisitos no existentes. Si en el futuro aparecen varios conceptos de dirección, deberán modelarse entonces de forma explícita. --- ## 6. Value Object `PostalAddress` Se reutilizará: ```txt @repo/rdx-ddd PostalAddress ``` No debe importarse una dirección propia de `customers` ni `suppliers`. `PostalAddress` pertenece a un paquete compartido de dominio y es reutilizable legítimamente desde `companies`. ### 6.1 Evolución necesaria Actualmente el país del VO usa un concepto textual equivalente a: ```ts country: Maybe ``` Debe evolucionar a: ```ts countryCode: Maybe ``` La representación definitiva será: ```ts interface PostalAddressProps { street: Maybe; street2: Maybe; city: Maybe; postalCode: Maybe; province: Maybe; countryCode: Maybe; } ``` Motivo: ```txt HTTP → CountryCode ISO 3166-1 alpha-2 Dominio actual → Country textual libre ``` La evolución evita perpetuar esa inconsistencia en `Company`. --- ## 7. País El país se representa mediante: ```txt CountryCode ``` y debe utilizar: ```txt ISO 3166-1 alpha-2 ``` Ejemplos: ```txt ES PT FR ``` ### Contratos Dominio: ```ts CountryCode ``` HTTP: ```ts CountryCodeSchema ``` Persistencia: ```txt CHAR(2) ``` No se crea un catálogo de países. Las listas frontend de países son auxiliares de presentación y no son SSOT del dominio. --- ## 8. Estructura de la dirección Campos definitivos: ```txt street street2 city postalCode province countryCode ``` Semántica: | Campo | Dirección presente | Onboarding | Observaciones | |---|---:|---:|---| | `street` | obligatorio | obligatorio | texto | | `street2` | opcional | opcional | texto adicional | | `city` | obligatorio | obligatorio | texto | | `postalCode` | obligatorio | obligatorio | VO existente | | `province` | obligatorio | obligatorio | texto | | `countryCode` | obligatorio | obligatorio | ISO 3166-1 alpha-2 | No se incorporan: ```txt regionCode state locality building county district ``` porque no existe un consumidor actual que los justifique. --- ## 9. Invariantes ### 9.1 Invariante de `Company` Una `Company` puede existir sin dirección: ```txt address = Maybe.none() ``` Esto es necesario para: - empresas ya existentes; - seeds; - fixtures; - transición progresiva; - compatibilidad con integraciones actuales. ### 9.2 Invariante cuando existe dirección Si `address` está presente, debe ser completa. Válido: ```txt address = none ``` Válido: ```txt street city postalCode province countryCode street2 opcional ``` Inválido: ```txt street presente city ausente countryCode ausente ``` No se permiten direcciones parcialmente válidas dentro de `Company`. --- ## 10. Diferencia entre `Company` y onboarding La dirección no es obligatoria como invariante universal de `Company`. El onboarding exige una dirección completa al crear inicialmente la empresa. Regla: ```txt Company existente → puede tener address = none Company creada mediante onboarding → debe recibir una dirección completa ``` La obligatoriedad del onboarding pertenece al caso de uso de creación inicial, no al agregado de forma universal. --- ## 11. Código postal En esta evolución se mantiene el `PostalCode` actual. Limitación conocida: ```txt PostalCode actual → cinco dígitos ``` Esto es suficiente para el alcance actual español, pero no constituye soporte internacional completo. No se amplía ahora el VO para formatos dependientes de `countryCode`. Ese trabajo se pospone a una evolución específica de internacionalización de direcciones. --- ## 12. Persistencia La dirección se persiste embebida en `companies`. Columnas conceptuales: ```txt address_street address_street_2 address_city address_postal_code address_province address_country_code ``` Tipos orientativos: ```txt address_street VARCHAR(255) NULL address_street_2 VARCHAR(255) NULL address_city VARCHAR(255) NULL address_postal_code VARCHAR(...) NULL address_province VARCHAR(255) NULL address_country_code CHAR(2) NULL ``` No se añade: ```txt company_addresses ``` No se crean índices nuevos. Actualmente no existen consultas que justifiquen índices por: ```txt street postalCode province countryCode ``` --- ## 13. Rehidratación El mapper de persistencia debe respetar estas reglas. ### Todas las columnas de dirección a `NULL` Resultado: ```txt Maybe.none() ``` ### Dirección completa Resultado: ```txt PostalAddress → Maybe.some(...) ``` ### Estado parcial en persistencia Ejemplo: ```txt address_street != NULL address_city = NULL ``` Debe considerarse estado persistido inconsistente. Regla: ```txt Fail Fast ``` No: ```txt rellenar con "" ignorar campos convertir silenciosamente a none rehidratar una dirección parcial ``` --- ## 14. `Company.create()` `Company.create()` debe admitir: ```ts address: Maybe ``` No debe exigir dirección universalmente. El flujo HTTP general de creación debe continuar siendo compatible con clientes actuales. --- ## 15. `Company.rehydrate()` Debe restaurar: ```ts address: Maybe ``` sin aplicar reglas propias de creación funcional. La infraestructura es responsable de detectar estados persistidos incoherentes antes de entregar las props internas al agregado. --- ## 16. `Company.update()` `CompanyPatchProps` incorporará conceptualmente: ```ts address?: Maybe ``` Semántica: ```txt omitido → no modificar Maybe.none() → eliminar dirección Maybe.some(address) → reemplazar dirección completa ``` No se implementará patch granular de campos internos de dirección. No: ```txt actualizar solo city actualizar solo province ``` La dirección funciona como una unidad coherente. --- ## 17. Contratos HTTP Se definirá un schema compartido de dirección dentro de `companies`. Ubicación conceptual: ```txt modules/companies/src/common/dto/shared/company-address.dto.ts ``` Shape: ```ts export const CompanyAddressSchema = z.object({ street: z.string().trim().min(1).max(255), street_2: z.string().trim().max(255).nullable(), city: z.string().trim().min(1).max(255), postal_code: PostalCodeSchema, province: z.string().trim().min(1).max(255), country_code: CountryCodeSchema, }); export type CompanyAddressDTO = z.infer; ``` Zod continúa siendo la SSOT. No se crearán interfaces manuales paralelas. --- ## 18. CREATE HTTP El request general de creación admitirá: ```ts address: CompanyAddressSchema.nullable().optional() ``` Semántica: ```txt omitido → sin dirección null → sin dirección objeto → dirección completa ``` Esto evita romper consumidores actuales de `POST /companies`. El onboarding utiliza un contrato más estricto donde la dirección es obligatoria. --- ## 19. PATCH HTTP Para la actualización de Company: ```ts address: CompanyAddressSchema.nullable().optional() ``` Semántica: ```txt omitido → no modificar null → eliminar dirección objeto → sustituir dirección completa ``` No se utilizarán defaults Zod. No se transformará una dirección parcial en completa. --- ## 20. Response DTO Las respuestas de `Company` deben incorporar: ```ts address: CompanyAddressSchema.nullable() ``` Una empresa sin dirección debe devolver: ```json { "address": null } ``` No omitir el campo en responses de detalle/configuración. --- ## 21. Application Se ampliarán conceptualmente: ```txt ICompanyCreateProps CompanyInternalProps CompanyPatchProps ``` con: ```ts address: Maybe ``` o: ```ts address?: Maybe ``` según corresponda. Los mappings HTTP/Application deben crear los Value Objects necesarios. No se pasará directamente el objeto DTO al agregado. --- ## 22. Public API de `companies` `CompanyPublicModel` debe exponer la dirección. No se crea otro finder. El finder público actual seguirá siendo la capacidad de lectura entre módulos. Otros módulos podrán consumir: ```txt company.address ``` sin importar: ```txt Company aggregate SequelizeCompanyRepository CompanyModel ``` --- ## 23. Settings de Company La pantalla actual debe incorporar una sección: ```txt Dirección fiscal ``` Distribución recomendada: ```txt Información general ------------------- Razón social Nombre comercial NIF Contacto -------- Email Teléfono Web Dirección fiscal ---------------- Dirección Dirección adicional Código postal Ciudad Provincia País Condiciones comerciales ----------------------- Plazo de pago predeterminado ``` Se reutilizarán: ```txt RdxPanel RdxFormGrid TextField SelectField ``` No se importará: ```txt CustomerAddressEditor ``` desde `customers`. Eso crearía una dependencia funcional indebida: ```txt companies → customers ``` --- ## 24. Opciones de país en frontend No se importará directamente: ```txt COUNTRY_OPTIONS ``` desde `customers`. Si la misma lista es necesaria en varios módulos, podrá extraerse a una ubicación frontend compartida apropiada. Antes debe revisarse que la lista actual es parcial y no representa todos los países ISO. La lista visual no debe confundirse con la validación del backend. --- ## 25. Reporting de proformas Existe un consumidor real inmediato: ```txt CompanyReportProfileFinder ``` Actualmente la dirección empresarial se resuelve como ausencia. Después de esta evolución: ```txt Company ↓ CompanyPublicModel.address ↓ CompanyReportProfileFinder ↓ CompanyReportProfile.address ↓ ProformaReportSnapshot ``` La proforma es un documento mutable, por lo que puede continuar utilizando información empresarial viva mientras permanezca en esa naturaleza. --- ## 26. Facturas emitidas `IssuedInvoice` no materializa actualmente un snapshot del emisor. Sí materializa otros datos históricos: ```txt recipient items taxes payment information fiscal information VeriFactu ``` pero no: ```txt issuer ``` Además, el reporting actual de facturas emitidas mantiene datos empresariales y slug hardcoded en algunos puntos. ### Decisión No conectar directamente: ```txt IssuedInvoice report → Company viva ``` como solución a esta evolución. Hacerlo permitiría que modificar la dirección de Company cambiara posteriormente la representación de una factura histórica regenerada. Esto sería contrario al principio: ```txt Snapshot models must not recalculate. ``` La evolución del emisor de `IssuedInvoice` queda como trabajo separado. --- ## 27. Snapshot futuro del emisor Debe quedar registrada como deuda de diseño: ```txt IssuedInvoice issuer snapshot ``` Probablemente deberá materializar en emisión, como mínimo: ```txt legalName tradeName tin address ``` y cualquier otro dato empresarial que deba permanecer históricamente congelado. No se diseña ni implementa en esta fase. --- ## 28. Idioma Decisión: ```txt POSPONER ``` No se añade: ```txt Company.defaultLanguage ``` SSOT actuales: ```txt Account → idioma UI Customer → idioma del cliente Proforma → idioma del documento IssuedInvoice → idioma materializado ``` No existe actualmente una cadena funcional clara: ```txt Company.defaultLanguage → Customer → Proforma ``` Añadir el campo ahora introduciría configuración sin consumidor estable. --- ## 29. Moneda Decisión: ```txt POSPONER ``` No se añade: ```txt Company.defaultCurrency ``` Actualmente: ```txt Customer → currencyCode Proforma → currencyCode IssuedInvoice → currencyCode ``` La creación de documentos maneja actualmente su propia moneda y existen defaults frontend. No existe una precedencia empresarial consolidada. Si se desea en el futuro: ```txt Company → Customer → Proforma ``` debe diseñarse explícitamente. --- ## 30. Logo Decisión: ```txt POSPONER ``` No se añade: ```txt Company.logo Company.logoUrl Company.logoAssetId ``` Todavía no existe infraestructura empresarial adecuada para: ```txt upload asset storage authorization replacement delete lifecycle ``` Los assets actuales de Handlebars pertenecen a las plantillas y no constituyen almacenamiento de medios de Company. --- ## 31. Slug y creación desde onboarding El onboarding no debe pedir: ```txt slug ``` El slug es un detalle técnico de `companies`. La futura capability pública de creación inicial debe poder recibir conceptualmente: ```ts { legalName, tradeName, tin, email, phone, website, address, transaction } ``` y `companies` debe resolver internamente: ```txt companyId slug status timestamps ``` El módulo `companies` será responsable de: ```txt generar slug normalizarlo comprobar unicidad desambiguar colisiones ``` La política exacta de desambiguación se cerrará al diseñar esa capability. --- ## 32. Campos del onboarding La evolución de `Company` permite fijar el bloque empresarial del onboarding. ### Empresa ```txt Razón social * Nombre comercial NIF Email Teléfono Web ``` ### Dirección fiscal ```txt Dirección * Dirección adicional Código postal * Ciudad * Provincia * País * ``` El onboarding no debe mostrar: ```txt companyId slug status timestamps idioma moneda logo IVA por defecto retención por defecto recargo por defecto ``` --- ## 33. Compatibilidad y transición Las nuevas columnas comenzarán nullable. Las empresas existentes podrán continuar temporalmente con: ```txt address = none ``` Settings permitirá completar la información posteriormente. El onboarding de nuevas empresas impondrá una regla más estricta: ```txt address obligatorio ``` No se diseñan migraciones productivas en esta fase. La estrategia de migraciones continuará tratándose de forma independiente. --- ## 34. Tests de una futura implementación ### Domain Debe verificarse: ```txt create sin dirección create con dirección update set update replace update clear rehydrate sin dirección ``` ### Shared domain Debe verificarse: ```txt PostalAddress con CountryCode normalización de CountryCode comparación semántica serialización ``` ### Application Debe verificarse: ```txt mapeo create mapeo patch omitido null objeto public model ``` ### Persistence Debe verificarse: ```txt none → columnas null address → columnas columnas null → none columnas completas → address estado parcial → fail fast ``` ### HTTP Debe verificarse: ```txt POST sin address POST con address PATCH omitido PATCH null PATCH objeto GET address null GET address completo ``` ### Frontend Debe verificarse: ```txt hydrate validación dirty state payload completo clear address country selector ``` ### Reports Debe verificarse: ```txt CompanyReportProfile recibe address proforma snapshot recibe address ``` --- ## 35. Riesgos conocidos ### RELEVANTE — `PostalAddress` y país Actualmente `PostalAddress` utiliza un concepto textual de país mientras los contratos modernos usan código ISO. Debe corregirse antes de consolidar Company.address. ### RELEVANTE — consumidores existentes de `PostalAddress` La evolución a `CountryCode` puede afectar: ```txt customers suppliers tests mappers DTO mappings ``` Debe implementarse de forma transversal y comprobada. ### RELEVANTE — reporting histórico de facturas `IssuedInvoice` no materializa emisor. No debe resolverse leyendo Company viva sin diseño específico. ### RELEVANTE — templates con información empresarial estática Existen plantillas que todavía contienen assets o datos empresariales hardcoded. `Company.address` no resuelve por sí sola esa deuda. ### MENOR — código postal internacional `PostalCode` actual no es compatible con todos los países representables por `CountryCode`. Queda como limitación conocida. --- ## 36. Decisiones consolidadas 1. `Company` tendrá una única propiedad `address`. 2. Su tipo será `Maybe`. 3. No se creará `CompanyAddress`. 4. No se creará una tabla `company_addresses`. 5. `PostalAddress` compartido evolucionará de `Country` a `CountryCode`. 6. País se representará mediante ISO 3166-1 alpha-2. 7. `Company.address` podrá estar ausente. 8. Cuando exista, la dirección deberá ser completa. 9. `street2` será el único campo opcional dentro de una dirección presente. 10. Onboarding exigirá una dirección completa. 11. Las empresas existentes podrán continuar sin dirección durante la transición. 12. La dirección se almacenará embebida en `companies`. 13. No se crearán índices nuevos para campos de dirección. 14. PATCH reemplazará la dirección completa. 15. PATCH con `null` eliminará la dirección. 16. `CompanyPublicModel` expondrá `address`. 17. El finder público existente seguirá siendo suficiente para lectura. 18. Settings incorporará una sección de Dirección fiscal. 19. `companies` no dependerá de componentes ni dominio de `customers`. 20. Reporting de proformas podrá utilizar la dirección viva de Company. 21. Reporting de facturas emitidas no utilizará Company viva como solución histórica. 22. El snapshot del emisor de `IssuedInvoice` se diseñará por separado. 23. Idioma se pospone. 24. Moneda se pospone. 25. Logo se pospone. 26. El slug no será un campo del onboarding. 27. La futura capability de creación inicial de Company resolverá internamente ID, slug, status y timestamps. 28. Se mantiene temporalmente la restricción actual de `PostalCode` como limitación conocida. --- ## 37. Fuera de alcance Fuera del alcance de esta evolución original: ```txt snapshot issuer de IssuedInvoice defaultLanguage defaultCurrency logo asset management company branches multiple company addresses international postal code engine country catalog migration runner template versioning ``` --- ## 38. Secuencia recomendada de implementación futura ```txt P1 Evolucionar PostalAddress → CountryCode P2 Añadir Company.address en dominio y persistencia P3 Actualizar DTOs / Application / PublicModel P4 Actualizar Settings P5 Conectar Company.address al reporting de proformas P6 Crear capability pública de creación inicial de Company para onboarding — completado P7 Diseñar por separado issuer snapshot de IssuedInvoice P8 Cerrar e implementar onboarding — completado ``` Esta secuencia mantiene cambios incrementales y evita mezclar `Company`, reporting histórico y onboarding en una única modificación. --- ## 39. Regla final La dirección empresarial debe tener una única fuente de verdad: ```txt Company.address ``` El onboarding la configura. Settings la mantiene. Los consumidores la leen mediante la API pública de `companies`. Las facturas emitidas no deben depender de su valor vivo para preservar histórico. La evolución debe priorizar: ```txt SSOT claridad semántica compatibilidad KISS ``` por encima de cualquier abstracción futura no necesaria.