Geek Social — Documentação
Banco de dadosTables

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

ColunaTipoNullableDefault
id uuidNOT NULLgen_random_uuid()
typeenumcolumnNOT NULL
item_iduuidNOT NULL
owner_iduuidNOT NULL
offerer_iduuidNOT NULL
offered_item_iduuidNULL ok
offered_pricenumericNULL ok
messagetextNULL ok
statusenumcolumnNOT NULLpending
offerer_confirmed_attimestampNULL ok
owner_confirmed_attimestampNULL ok
listing_iduuidNULL ok
created_attimestampNOT NULLnow()
updated_attimestampNOT NULLnow()

primary key   unique

Funcionalidade dos campos

  • id — UUID, PK
  • typebuy (oferta com dinheiro) ou trade (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 para type=trade. set null em delete pra preservar histórico.
  • offered_price — valor proposto. Apenas para type=buy ou both. 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 null em delete (preserva offer history quando listing é hard-deleted).
  • created_at / updated_at — managed.

Foreign keys

Coluna(s)ReferênciaON DELETEON UPDATE
item_iditems.idcascadeno action
owner_idusers.idcascadeno action
offerer_idusers.idcascadeno action
offered_item_iditems.idset nullno action
listing_idlistings.idset nullno action

Índices

NomeÚnicoColunasWHERE (parcial)
item_offers_pending_uniquesimitem_id, offerer_id[object Object] = 'pending'
item_offers_owner_status_idxnãoowner_id, status, created_at
item_offers_offerer_status_idxnãoofferer_id, status, created_at
item_offers_listing_idxnãolisting_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 retornar DUPLICATE_PENDING_OFFER (HTTP 409) em vez de erro 500.
  • item_offers_owner_status_idx — query "minhas ofertas recebidas" do dono
  • item_offers_offerer_status_idx — query "minhas ofertas feitas" do ofertante
  • item_offers_listing_idx — usado pra autoRejectByListing quando 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:

  1. Cria novo proposal com proposer_id = receptor e status='pending'
  2. A anterior vai pra superseded
  3. 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 ofertaPOST /offers com pre-check de duplicate
  • Aceitar última propostaPOST /offers/:id/accept (cria proposal accepted, evolui offer)
  • Rejeitar última propostaPOST /offers/:id/reject (apenas rejeita rodada, offer continua pending)
  • Counter-proporPOST /offers/:id/propose (supersede + nova pending)
  • Confirmar transaçãoPOST /offers/:id/confirm
  • CancelarPOST /offers/:id/cancel antes de completed
  • Auto-rejeição — em listing close/delete, todas as pending viram rejected

Tabelas relacionadas

  • items — alvo + oferecido
  • users — owner + offerer
  • listings — anúncio referenciado
  • offer_proposals — rodadas de negociação (cascade)
  • listing_ratings — avaliação pós-transação (FK pra offer)
  • notificationsoffer_received, offer_accepted, offer_rejected, counter_proposal_received, proposal_rejected, offer_completed, offer_cancelled, offer_expired

On this page