From 394d3465e31b769ca67c2781acf23362fbdbe002 Mon Sep 17 00:00:00 2001 From: David Date: Sat, 5 Sep 2026 20:30:37 +0200 Subject: [PATCH] =?UTF-8?q?feat:=20a=C3=B1adir=20documento=20de=20dise?= =?UTF-8?q?=C3=B1o=20para=20la=20evoluci=C3=B3n=20de=20`Company`=20con=20d?= =?UTF-8?q?irecci=C3=B3n=20empresarial?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../documento-diseno-company-address.md | 1348 +++++++++++++++++ 1 file changed, 1348 insertions(+) create mode 100644 docs/companies/documento-diseno-company-address.md diff --git a/docs/companies/documento-diseno-company-address.md b/docs/companies/documento-diseno-company-address.md new file mode 100644 index 00000000..73f1f583 --- /dev/null +++ b/docs/companies/documento-diseno-company-address.md @@ -0,0 +1,1348 @@ +# 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`. + +Sin embargo, el futuro onboarding sí debe exigirla. + +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 futuro onboarding utilizará un contrato más estricto donde la dirección sí será 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 futura 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 futuro 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 + +No implementar en esta evolución: + +```txt +onboarding +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 + +P7 +Diseñar por separado issuer snapshot de IssuedInvoice + +P8 +Cerrar e implementar onboarding +``` + +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.