FactuGES_ERP/docs/companies/documento-diseno-company-address.md
David dac22372b9 feat(docs): archive legacy DTO design and naming guidelines, introduce HTTP request flow, module boundaries, and testing guidelines
- Archived historical documents for DTO design and naming conventions.
- Added normative documentation for HTTP request flow and module boundaries.
- Introduced testing guidelines for backend layers.
- Updated onboarding module documentation and clarified onboarding requirements.
- Deprecated backend prompt base and prompt auth documents.
- Adjusted paths in testing documentation for consistency.
2026-09-08 23:40:49 +02:00

1348 lines
21 KiB
Markdown

# 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<PostalAddress>
```
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<Country>
```
Debe evolucionar a:
```ts
countryCode: Maybe<CountryCode>
```
La representación definitiva será:
```ts
interface PostalAddressProps {
street: Maybe<Street>;
street2: Maybe<Street>;
city: Maybe<City>;
postalCode: Maybe<PostalCode>;
province: Maybe<Province>;
countryCode: Maybe<CountryCode>;
}
```
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<PostalAddress>()
```
### 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<PostalAddress>
```
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<PostalAddress>
```
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<PostalAddress>
```
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<typeof CompanyAddressSchema>;
```
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<PostalAddress>
```
o:
```ts
address?: Maybe<PostalAddress>
```
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<PostalAddress>`.
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.