item_offers
Ofertas feitas sobre listings (compra ou troca), com confirmação dupla.
Cada offer representa o interesse de um usuário em um item anunciado. Tem tipo (buy ou trade), itens/preços oferecidos, e passa por dupla confirmação antes da transferência efetivar.
A negociação real acontece via offer_proposals (rodadas pending → accepted | rejected | superseded). Os campos offered_price/offered_item_id em item_offers armazenam a proposta inicial; counter-propostas vivem em offer_proposals.
Colunas
| Coluna | Tipo | Nullable | Default |
|---|---|---|---|
| id ● | uuid | NOT NULL | gen_random_uuid() |
| type | enumcolumn | NOT NULL | — |
| item_id | uuid | NOT NULL | — |
| owner_id | uuid | NOT NULL | — |
| offerer_id | uuid | NOT NULL | — |
| offered_item_id | uuid | NULL ok | — |
| offered_price | numeric | NULL ok | — |
| message | text | NULL ok | — |
| status | enumcolumn | NOT NULL | pending |
| offerer_confirmed_at | timestamp | NULL ok | — |
| owner_confirmed_at | timestamp | NULL ok | — |
| listing_id | uuid | NULL ok | — |
| created_at | timestamp | NOT NULL | now() |
| updated_at | timestamp | NOT NULL | now() |
● primary key ◆ unique
Funcionalidade dos campos
id— UUID, PKtype—buy(oferta com dinheiro) outrade(oferta com outro item)item_id— item alvo (o anunciado). Cascade delete.owner_id— dono do item alvo. Denormalizado.offerer_id— quem está fazendo a oferta. Cascade delete.offered_item_id— item oferecido em troca. Apenas paratype=trade.set nullem delete pra preservar histórico.offered_price— valor proposto. Apenas paratype=buyouboth.numeric(12, 2).message— texto livre opcional do ofertante.status— enum:pending,accepted,rejected,cancelled,completed. Estado da offer como um todo (não da rodada).offerer_confirmed_at— timestamp em que o ofertante confirmou (após accept). Dual confirm.owner_confirmed_at— timestamp em que o dono confirmou. Quando ambos setados, transferência executa.listing_id— FK pro listing alvo.set nullem delete (preserva offer history quando listing é hard-deleted).created_at/updated_at— managed.
Foreign keys
| Coluna(s) | Referência | ON DELETE | ON UPDATE |
|---|---|---|---|
| item_id | items.id | cascade | no action |
| owner_id | users.id | cascade | no action |
| offerer_id | users.id | cascade | no action |
| offered_item_id | items.id | set null | no action |
| listing_id | listings.id | set null | no action |
Índices
| Nome | Único | Colunas | WHERE (parcial) |
|---|---|---|---|
| item_offers_pending_unique | sim | item_id, offerer_id | [object Object] = 'pending' |
| item_offers_owner_status_idx | não | owner_id, status, created_at | — |
| item_offers_offerer_status_idx | não | offerer_id, status, created_at | — |
| item_offers_listing_idx | não | listing_id | — |
Detalhes:
item_offers_pending_unique— partial unique em(item_id, offerer_id) WHERE status='pending'. Impede que o mesmo user faça 2 ofertas ativas pro mesmo item simultaneamente. O service faz pre-check pra retornarDUPLICATE_PENDING_OFFER(HTTP 409) em vez de erro 500.item_offers_owner_status_idx— query "minhas ofertas recebidas" do donoitem_offers_offerer_status_idx— query "minhas ofertas feitas" do ofertanteitem_offers_listing_idx— usado praautoRejectByListingquando listing é encerrado
Constraints
PRIMARY KEY (id)
Ciclo de vida (status global)
pending ──(accept)──→ accepted ──(both confirm)──→ completed
│ │
│ └──(cancel/expire)──→ cancelled / rejected
↓
rejected (final)A nuance: accepted significa que a última proposta foi aceita, mas a transferência depende de dois confirms. A 7 dias após accepted sem confirms, a offer expira (cron runOffersExpire roda a cada 1h).
Counter-propostas
A "negociação" acontece via offer_proposals. Quando o receptor da proposta atual a rejeita mas quer counter-propor:
- Cria novo proposal com
proposer_id = receptorestatus='pending' - A anterior vai pra
superseded - Turno passa pro lado oposto
A offer (item_offers) fica com status='pending' durante toda a negociação. Apenas quando uma proposta vira accepted é que a offer evolui pra accepted.
Dupla confirmação e transferência
proposal accepted
↓
[ambas as partes precisam confirmar]
│
│ ofererConfirm() → set offerer_confirmed_at
│
│ ownerConfirm() → set owner_confirmed_at
│
[ambos setados]
↓
executeTransfer()
- move item_id pra coleção destino do offerer
- se trade: move offered_item_id pra coleção destino do owner
- cria coleção destino se não houver (auto-create)
- status = 'completed'
- listing.status = 'closed'confirm() é idempotente: se ambos já confirmaram mas a transferência falhou (ex: faltava coleção, sem disco), re-tentar confirm() re-executa em vez de retornar ALREADY_CONFIRMED.
Padrões de uso
- Criar oferta —
POST /offerscom pre-check de duplicate - Aceitar última proposta —
POST /offers/:id/accept(cria proposal accepted, evolui offer) - Rejeitar última proposta —
POST /offers/:id/reject(apenas rejeita rodada, offer continuapending) - Counter-propor —
POST /offers/:id/propose(supersede + nova pending) - Confirmar transação —
POST /offers/:id/confirm - Cancelar —
POST /offers/:id/cancelantes de completed - Auto-rejeição — em listing close/delete, todas as
pendingviramrejected
Tabelas relacionadas
items— alvo + oferecidousers— owner + offererlistings— anúncio referenciadooffer_proposals— rodadas de negociação (cascade)listing_ratings— avaliação pós-transação (FK pra offer)notifications—offer_received,offer_accepted,offer_rejected,counter_proposal_received,proposal_rejected,offer_completed,offer_cancelled,offer_expired