Módulos
Offers
Sistema de ofertas com counter-propostas (proposal rounds) e dual confirmation.
Visão geral
Offers modelam negociação entre ofertante e dono de um listing. Suporta:
- Tipo
buy(com dinheiro) outrade(com outro item) - Counter-propostas via tabela
offer_proposals— apenas 1 proposalpendingpor offer ao mesmo tempo - Dual confirmation antes da transferência efetivar
- Auto-create de coleção destino se receptor não tem
- Auto-rejeição quando listing é encerrado
A offer "como um todo" tem status pending → accepted → completed (ou rejected/cancelled/expired). A negociação real (counters) acontece em proposals; quando uma proposal é aceita, a offer evolui pra accepted.
Entidades principais
| Tabela | Papel |
|---|---|
item_offers | Offer guarda-chuva (status global, dual confirm timestamps) |
offer_proposals | Rodadas de negociação (pending/accepted/rejected/superseded) |
listings | Anúncio alvo |
items | Item alvo + opcional offered_item_id (em trade) |
Endpoints
10 endpoints em /offers:
| Endpoint | Resumo |
|---|---|
POST /offers | Criar oferta |
GET /offers/received | Recebidas (filtrar por status) |
GET /offers/sent | Enviadas |
GET /offers/:id | Detalhes (inclui latestProposal) |
POST /offers/:id/accept | Aceitar a proposal corrente |
POST /offers/:id/reject | Rejeitar a proposal corrente |
POST /offers/:id/cancel | Ofertante cancela |
POST /offers/:id/confirm | Dual confirm + execute transfer |
POST /offers/:id/propose | Counter-proposta |
GET /offers/:id/history | Timeline de proposals |
Fluxos
Criar oferta
Aceitar proposal corrente
Counter-proposta
Dual confirm + transferência
Cancelar (offerer)
Auto-expiração (cron)
runOffersExpire roda a cada 1h:
UPDATE item_offers
SET status = 'expired'
WHERE status = 'accepted'
AND (offerer_confirmed_at IS NULL OR owner_confirmed_at IS NULL)
AND updated_at < now() - INTERVAL '7 days'Notifica ambos os lados com offer_expired.
Eventos de socket
Eventos via NotificationsService:
offer_received,offer_accepted,offer_rejected,offer_cancelled,offer_completed,offer_expiredcounter_proposal_received,proposal_rejected
Edge cases e regras especiais
- Pre-check anti-500: oferta duplicada retorna 409 com
DUPLICATE_PENDING_OFFER, não erro de unique constraint isMyTurnno frontend — calculado delatestProposal.proposerId !== currentUserId- Confirm idempotente — re-chamar não falha; se transfer já rodou, retorna estado atual
- Auto-create de coleção destino — receptor não precisa ter coleção compatível previamente
- Hard delete de listing seta
listing_id=NULLem offers (preserva offer history) - Janela de avaliação 30 dias após
offer_completed— após isso, rate retorna RATING_WINDOW_CLOSED offered_item_idset null se item original deletado — preserva offer record- Reject só rejeita rodada, não offer inteira. Pra cancelar, ofertante usa
/cancel. - Trade requires both items present — se algum foi transferido em outra offer entre a criação e o confirm, falha com TRADE_ITEM_UNAVAILABLE.
Dependências entre módulos
- Listings — auto-reject em close, hard delete preserva via set null
- Items — alvo + opcional offered + transferência muda collection_id
- Collections — auto-create destino em transfer
- Listing Ratings — habilitado após completed
- Notifications — 8 tipos diferentes
- Friends — block impede oferta entre user bloqueado/bloqueador