offer_proposals
Rodadas de negociação dentro de uma offer (proposta inicial + counters).
Cada rodada de negociação dentro de uma offer é um row aqui. A primeira é criada junto com a offer (item_offers); counter-propostas adicionam novos rows. Apenas uma está pending por offer ao mesmo tempo (unique partial index).
A offer "como um todo" (item_offers.status) só evolui pra accepted quando uma rodada vira accepted. Rejeição de rodada NÃO mata a offer — pode-se contra-propor.
Colunas
| Coluna | Tipo | Nullable | Default |
|---|---|---|---|
| id ● | uuid | NOT NULL | gen_random_uuid() |
| offer_id | uuid | NOT NULL | — |
| proposer_id | uuid | NOT NULL | — |
| offered_price | numeric | NULL ok | — |
| offered_item_id | uuid | NULL ok | — |
| message | text | NULL ok | — |
| status | enumcolumn | NOT NULL | pending |
| created_at | timestamp | NOT NULL | now() |
| updated_at | timestamp | NOT NULL | now() |
● primary key ◆ unique
Funcionalidade dos campos
id— UUID, PKoffer_id— FK praitem_offers. Cascade delete.proposer_id— quem fez essa rodada. Identifica de quem é o turno: o LADO OPOSTO precisa responder. Cascade delete.offered_price— preço proposto nessa rodada (parabuy)offered_item_id— item oferecido nessa rodada (paratrade). Set null em delete.message— texto livre opcional explicando a contra-propostastatus— enum:pending(esperando ação do oposto),accepted(aceita — offer evolui),rejected(rejeitada — fim da rodada, mas offer pode continuar),superseded(substituída por counter-proposta nova)created_at/updated_at— managed.
Foreign keys
| Coluna(s) | Referência | ON DELETE | ON UPDATE |
|---|---|---|---|
| offer_id | item_offers.id | cascade | no action |
| proposer_id | users.id | cascade | no action |
| offered_item_id | items.id | set null | no action |
Índices
| Nome | Único | Colunas | WHERE (parcial) |
|---|---|---|---|
| offer_proposals_offer_idx | não | offer_id, created_at | — |
| offer_proposals_one_pending_uniq | sim | offer_id | [object Object] = 'pending' |
Detalhes:
offer_proposals_offer_idx—(offer_id, created_at). Usado pra listar todas as rodadas de uma offer em ordem cronológicaoffer_proposals_one_pending_uniq— partial unique emoffer_id WHERE status='pending'. Garante semântica de "1 rodada ativa por vez"
Constraints
PRIMARY KEY (id)
Estados e transições
pending ──(accept)──→ accepted (offer.status → accepted; transação a confirmar)
│
├──(reject)──→ rejected (final desta rodada)
│
└──(propose contraposta)──→ superseded (e nova proposal pending criada)Helpers de turno (frontend)
A DTO OfferWithDetails carrega latestProposal: { id, proposerId, status }. UI usa pra decidir:
isMyTurn—proposer_id !== currentUser.idANDstatus === 'pending'canAccept—isMyTurncanReject—isMyTurn(rejeita rodada, não offer)canPropose—isMyTurn(counter-propor)
Se proposer_id === currentUser.id, o usuário está esperando — só pode cancelar a oferta inteira.
Backfill em migration
A migration que adicionou essa tabela criou rows iniciais retroativos pra ofertas existentes — cada offer ganhou um proposal com os mesmos offered_price/offered_item_id e proposer_id = offerer_id, mantendo compatibilidade com o histórico.
Padrões de uso
- Insert inicial — quando offer é criada
- Accept — UPDATE
status='accepted'— offer evolui - Reject — UPDATE
status='rejected'— offer continua - Counter-proposta — UPDATE atual
status='superseded'+ INSERT novostatus='pending'(transação)
Tabelas relacionadas
item_offers— pai (cascade)users— proposer (cascade)items—offered_item_id(set null)notifications—counter_proposal_received,proposal_rejected