Parent
Parte da PRD #250 — Módulo Profile Fase 1.
Contexto
Hoje o endereço de um User vive na tabela user_address dentro do módulo Identity, com FK direta pra users. Essa modelagem é rígida — se amanhã um Event ou Tenant precisar de endereço, seria necessário criar outra tabela.
A decisão (ver ADR-0001) é transformar Address em um model polimórfico compartilhado. Qualquer entidade (User, Event, Tenant, Sponsor) pode ter um endereço via morphOne. O model vive fora de módulo (shared/app) porque é infraestrutura transversal.
O que muda
- Antes:
user_address com user_id FK → acoplado ao User
- Depois:
addresses com addressable_type + addressable_id → desacoplado, reutilizável
A tabela user_address antiga permanece no banco como safety net, mas o model He4rt\Identity\User\Models\Address será removido na issue de remoção de legado (#TBD).
O que construir
1. Migration create_addresses_table
Campos:
id — UUID, PK
addressable_type — string (morph type)
addressable_id — UUID (morph id)
country — varchar(4), nullable (ISO code)
state — varchar(4), nullable (ISO code)
city — varchar, nullable
zip_code — varchar, nullable
timestamps
- Index composto em
(addressable_type, addressable_id)
2. Model Address
Local: app/Models/Address.php (fora de módulo, é shared).
final class, HasUuids, HasFactory
- Relacionamento:
morphTo('addressable')
- Sem lógica de domínio — é um value object persistido
3. Factory AddressFactory
database/factories/AddressFactory.php
- Defaults: country
'BR', state 'SP', city via faker, zip_code via faker
- State
forUser(User $user) e genérico for(Model $model)
4. Trait HasAddress (opcional)
Se fizer sentido, criar um trait App\Concerns\HasAddress que adiciona morphOne(Address::class, 'addressable') pra qualquer model. Usar no User model substituindo a relação address() existente.
5. Registrar morph map
No AppServiceProvider ou em local adequado: 'address' => Address::class. Também registrar os morphable types se necessário.
Cenários BDD
Cenário: User tem um endereço polimórfico
Dado que existe um User "danielhe4rt"
Quando atribuo um Address com country "BR", state "SP", city "São Paulo"
Então o Address é salvo com addressable_type "user" e addressable_id do User
E $user->address retorna o Address criado
Cenário: Múltiplas entidades com address
Dado que existe um User e um Event
Quando ambos têm um Address associado
Então cada Address tem seu próprio registro
E os addressable_type são diferentes ("user" vs "event" ou morph alias)
Cenário: User sem endereço retorna null
Dado que existe um User sem Address associado
Quando acesso $user->address
Então retorna null
Cenário: Deletar User deleta Address via cascade
Dado que um User tem um Address associado
Quando o User é deletado
Então o Address associado também é deletado
Cenário: Factory cria Address válido para User
Quando crio um Address via factory com forUser($user)
Então o Address pertence ao User
E os campos country e state são válidos
Acceptance Criteria
Blocked by
None — pode começar imediatamente (paralelo ao #251).
Parent
Parte da PRD #250 — Módulo Profile Fase 1.
Contexto
Hoje o endereço de um User vive na tabela
user_addressdentro do módulo Identity, com FK direta prausers. Essa modelagem é rígida — se amanhã um Event ou Tenant precisar de endereço, seria necessário criar outra tabela.A decisão (ver ADR-0001) é transformar Address em um model polimórfico compartilhado. Qualquer entidade (User, Event, Tenant, Sponsor) pode ter um endereço via
morphOne. O model vive fora de módulo (shared/app) porque é infraestrutura transversal.O que muda
user_addresscomuser_idFK → acoplado ao Useraddressescomaddressable_type+addressable_id→ desacoplado, reutilizávelA tabela
user_addressantiga permanece no banco como safety net, mas o modelHe4rt\Identity\User\Models\Addressserá removido na issue de remoção de legado (#TBD).O que construir
1. Migration
create_addresses_tableCampos:
id— UUID, PKaddressable_type— string (morph type)addressable_id— UUID (morph id)country— varchar(4), nullable (ISO code)state— varchar(4), nullable (ISO code)city— varchar, nullablezip_code— varchar, nullabletimestamps(addressable_type, addressable_id)2. Model
AddressLocal:
app/Models/Address.php(fora de módulo, é shared).final class,HasUuids,HasFactorymorphTo('addressable')3. Factory
AddressFactorydatabase/factories/AddressFactory.php'BR', state'SP', city via faker, zip_code via fakerforUser(User $user)e genéricofor(Model $model)4. Trait
HasAddress(opcional)Se fizer sentido, criar um trait
App\Concerns\HasAddressque adicionamorphOne(Address::class, 'addressable')pra qualquer model. Usar no User model substituindo a relaçãoaddress()existente.5. Registrar morph map
No
AppServiceProviderou em local adequado:'address' => Address::class. Também registrar os morphable types se necessário.Cenários BDD
Acceptance Criteria
addressescom campos polimórficos e indexAddressemapp/Models/Address.phpcommorphToAddressFactorycom states pra diferentes entidadesUsermodel tem relaçãoaddress()viamorphOne(Address::class, 'addressable')user_addressnão é tocada (fica no banco)vendor/bin/pint --dirty --format agentpassa sem errosBlocked by
None — pode começar imediatamente (paralelo ao #251).