- 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.
21 KiB
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:
explícito
simple
compatible con empresas existentes
coherente con DDD pragmático
sin sobreingeniería
2. Contexto arquitectónico
Company pertenece al módulo:
modules/companies
La arquitectura general mantiene:
Domain
↓
Application
↓
Infrastructure
con las reglas ya consolidadas del ERP:
Domainno conoce HTTP, Sequelize ni Infrastructure;Applicationno conoce Express ni modelos ORM;Infrastructureimplementa persistencia, transporte y DI;- Zod v4 es la SSOT de los contratos HTTP;
- otros módulos consumen
companiesmediante 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:
id
legalName
tradeName
tin
slug
email
phone
website
defaultPaymentTermId
status
createdAt
updatedAt
No existen actualmente en Company:
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:
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:
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:
address
No:
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á:
@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:
country: Maybe<Country>
Debe evolucionar a:
countryCode: Maybe<CountryCode>
La representación definitiva será:
interface PostalAddressProps {
street: Maybe<Street>;
street2: Maybe<Street>;
city: Maybe<City>;
postalCode: Maybe<PostalCode>;
province: Maybe<Province>;
countryCode: Maybe<CountryCode>;
}
Motivo:
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:
CountryCode
y debe utilizar:
ISO 3166-1 alpha-2
Ejemplos:
ES
PT
FR
Contratos
Dominio:
CountryCode
HTTP:
CountryCodeSchema
Persistencia:
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:
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:
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:
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:
address = none
Válido:
street
city
postalCode
province
countryCode
street2 opcional
Inválido:
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:
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:
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:
address_street
address_street_2
address_city
address_postal_code
address_province
address_country_code
Tipos orientativos:
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:
company_addresses
No se crean índices nuevos.
Actualmente no existen consultas que justifiquen índices por:
street
postalCode
province
countryCode
13. Rehidratación
El mapper de persistencia debe respetar estas reglas.
Todas las columnas de dirección a NULL
Resultado:
Maybe.none<PostalAddress>()
Dirección completa
Resultado:
PostalAddress
→ Maybe.some(...)
Estado parcial en persistencia
Ejemplo:
address_street != NULL
address_city = NULL
Debe considerarse estado persistido inconsistente.
Regla:
Fail Fast
No:
rellenar con ""
ignorar campos
convertir silenciosamente a none
rehidratar una dirección parcial
14. Company.create()
Company.create() debe admitir:
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:
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:
address?: Maybe<PostalAddress>
Semántica:
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:
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:
modules/companies/src/common/dto/shared/company-address.dto.ts
Shape:
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á:
address: CompanyAddressSchema.nullable().optional()
Semántica:
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:
address: CompanyAddressSchema.nullable().optional()
Semántica:
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:
address: CompanyAddressSchema.nullable()
Una empresa sin dirección debe devolver:
{
"address": null
}
No omitir el campo en responses de detalle/configuración.
21. Application
Se ampliarán conceptualmente:
ICompanyCreateProps
CompanyInternalProps
CompanyPatchProps
con:
address: Maybe<PostalAddress>
o:
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:
company.address
sin importar:
Company aggregate
SequelizeCompanyRepository
CompanyModel
23. Settings de Company
La pantalla actual debe incorporar una sección:
Dirección fiscal
Distribución recomendada:
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:
RdxPanel
RdxFormGrid
TextField
SelectField
No se importará:
CustomerAddressEditor
desde customers.
Eso crearía una dependencia funcional indebida:
companies → customers
24. Opciones de país en frontend
No se importará directamente:
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:
CompanyReportProfileFinder
Actualmente la dirección empresarial se resuelve como ausencia.
Después de esta evolución:
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:
recipient
items
taxes
payment information
fiscal information
VeriFactu
pero no:
issuer
Además, el reporting actual de facturas emitidas mantiene datos empresariales y slug hardcoded en algunos puntos.
Decisión
No conectar directamente:
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:
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:
IssuedInvoice
issuer snapshot
Probablemente deberá materializar en emisión, como mínimo:
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:
POSPONER
No se añade:
Company.defaultLanguage
SSOT actuales:
Account
→ idioma UI
Customer
→ idioma del cliente
Proforma
→ idioma del documento
IssuedInvoice
→ idioma materializado
No existe actualmente una cadena funcional clara:
Company.defaultLanguage
→ Customer
→ Proforma
Añadir el campo ahora introduciría configuración sin consumidor estable.
29. Moneda
Decisión:
POSPONER
No se añade:
Company.defaultCurrency
Actualmente:
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:
Company → Customer → Proforma
debe diseñarse explícitamente.
30. Logo
Decisión:
POSPONER
No se añade:
Company.logo
Company.logoUrl
Company.logoAssetId
Todavía no existe infraestructura empresarial adecuada para:
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:
slug
El slug es un detalle técnico de companies.
La futura capability pública de creación inicial debe poder recibir conceptualmente:
{
legalName,
tradeName,
tin,
email,
phone,
website,
address,
transaction
}
y companies debe resolver internamente:
companyId
slug
status
timestamps
El módulo companies será responsable de:
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
Razón social *
Nombre comercial
NIF
Email
Teléfono
Web
Dirección fiscal
Dirección *
Dirección adicional
Código postal *
Ciudad *
Provincia *
País *
El onboarding no debe mostrar:
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:
address = none
Settings permitirá completar la información posteriormente.
El onboarding de nuevas empresas impondrá una regla más estricta:
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:
create sin dirección
create con dirección
update set
update replace
update clear
rehydrate sin dirección
Shared domain
Debe verificarse:
PostalAddress con CountryCode
normalización de CountryCode
comparación semántica
serialización
Application
Debe verificarse:
mapeo create
mapeo patch
omitido
null
objeto
public model
Persistence
Debe verificarse:
none → columnas null
address → columnas
columnas null → none
columnas completas → address
estado parcial → fail fast
HTTP
Debe verificarse:
POST sin address
POST con address
PATCH omitido
PATCH null
PATCH objeto
GET address null
GET address completo
Frontend
Debe verificarse:
hydrate
validación
dirty state
payload completo
clear address
country selector
Reports
Debe verificarse:
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:
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
Companytendrá una única propiedadaddress.- Su tipo será
Maybe<PostalAddress>. - No se creará
CompanyAddress. - No se creará una tabla
company_addresses. PostalAddresscompartido evolucionará deCountryaCountryCode.- País se representará mediante ISO 3166-1 alpha-2.
Company.addresspodrá estar ausente.- Cuando exista, la dirección deberá ser completa.
street2será el único campo opcional dentro de una dirección presente.- Onboarding exigirá una dirección completa.
- Las empresas existentes podrán continuar sin dirección durante la transición.
- La dirección se almacenará embebida en
companies. - No se crearán índices nuevos para campos de dirección.
- PATCH reemplazará la dirección completa.
- PATCH con
nulleliminará la dirección. CompanyPublicModelexpondráaddress.- El finder público existente seguirá siendo suficiente para lectura.
- Settings incorporará una sección de Dirección fiscal.
companiesno dependerá de componentes ni dominio decustomers.- Reporting de proformas podrá utilizar la dirección viva de Company.
- Reporting de facturas emitidas no utilizará Company viva como solución histórica.
- El snapshot del emisor de
IssuedInvoicese diseñará por separado. - Idioma se pospone.
- Moneda se pospone.
- Logo se pospone.
- El slug no será un campo del onboarding.
- La futura capability de creación inicial de Company resolverá internamente ID, slug, status y timestamps.
- Se mantiene temporalmente la restricción actual de
PostalCodecomo limitación conocida.
37. Fuera de alcance
Fuera del alcance de esta evolución original:
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
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:
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:
SSOT
claridad semántica
compatibilidad
KISS
por encima de cualquier abstracción futura no necesaria.