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

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:

  • 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:

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.


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

  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:

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.