Files
nick-doc/02 - Data Models/SellerOffer.md

7.7 KiB

title, tags, aliases
title tags aliases
SellerOffer
data-model
postgres
Seller Offer
Bid
ISellerOffer

SellerOffer

Last updated: 2026-06-06 — MongoDB/Mongoose fully removed; PostgreSQL + Drizzle is now the only database layer.

A seller's bid against a PurchaseRequest. Stores the proposed price, the delivery time commitment, optional notes/attachments, and a small status machine (pending / accepted / rejected / withdrawn). The parent PurchaseRequest keeps the array of offer ids in offers[] and the chosen one in selectedOfferId.

[!note] Source backend/src/db/schema/sellerOffer.ts — PostgreSQL schema (Drizzle) definition

Schema

PostgreSQL schema (Drizzle) — seller_offers

Table: seller_offers | Schema file: backend/src/db/schema/sellerOffer.ts

PG Column Drizzle Type Nullable Default Notes
id uuid PK no gen_random_uuid() Primary key (UUID string)
legacy_object_id text yes Former Mongo ObjectId; partial-unique WHERE NOT NULL
seller_id uuid FK → users CASCADE no Maps from sellerId (uses user.pgId)
purchase_request_id uuid FK → purchase_requests CASCADE no Maps from purchaseRequestId
title varchar(200) no
description varchar(1000) no
price_amount numeric(18,8) no CHECK price_amount >= 0
price_currency offer_currency enum no USD | EUR | IRR | USDT | USDC | TRY
delivery_time_amount int no CHECK delivery_time_amount >= 1
delivery_time_unit delivery_unit enum no hours | days | weeks
status offer_status enum no pending pending | accepted | rejected | withdrawn | active
attachments text[] yes
notes text yes
valid_until timestamp with time zone yes Maps from validUntil
require_aml_check boolean yes
aml_block_on_failure boolean yes CHECK: block requires check (AML coherence)
created_at timestamp with time zone no now()
updated_at timestamp with time zone no now()

Enums used:

Enum name Values
offer_status pending, accepted, rejected, withdrawn, active
offer_currency USD, EUR, IRR, USDT, USDC, TRY
delivery_unit hours, days, weeks

Constraints:

  • CHECK (price_amount >= 0)
  • CHECK (delivery_time_amount >= 1)
  • AML coherence check: aml_block_on_failure = true requires require_aml_check = true

Money precision note: price_amount uses numeric(18,8) — differs from the numeric(38,18) used by payments and funds_ledger_entries. This matches the Migration Guide specification for offer amounts.

ID note: The primary key is id (UUID string), not _id. legacy_object_id retains the former MongoDB ObjectId for backfill/bridging purposes only and is not used by any runtime query.

Postgres Indexes

Index Type Notes
seller_id btree
purchase_request_id btree
status btree
created_at DESC btree
(purchase_request_id, seller_id) btree composite
legacy_object_id partial-unique WHERE NOT NULL; idempotent backfill upserts

Domain Fields (TypeScript)

Field Type Required Default Notes
id string (UUID) yes auto PG primary key; replaces former _id ObjectId
sellerId string (UUID) yes user.pgId of the submitting seller
purchaseRequestId string (UUID) yes Parent request
title string yes Offer headline (max 200)
description string yes Pitch and details (max 1000)
price.amount number yes Quoted amount (min 0)
price.currency string yes USDT USD / EUR / IRR / TRY / USDT / USDC
deliveryTime.amount number yes Numeric ETA (min 1)
deliveryTime.unit string yes hours / days / weeks
status string no pending pending / accepted / rejected / withdrawn / active
attachments[] string[] no URLs of supporting files
notes string no Internal/private notes
validUntil Date no Expiration
requireAmlCheck boolean no AML screening required before presenting to buyer
amlBlockOnFailure boolean no Block offer on AML failure (vs. flag for review)
createdAt Date auto
updatedAt Date auto

Status enum note: active is accepted by the current backend schema for marketplace/listing flows, in addition to the negotiation statuses pending | accepted | rejected | withdrawn.

Currency note: TRY is supported by the oracle/depeg path through the off-chain FX provider.

UpdateSellerOfferInput

UpdateSellerOfferInput does not include an updatedAt field — the column is managed automatically by the database (now() default; updated by the repo layer on write).

Virtuals

None defined.

Pre/Post Hooks

None declared (Drizzle ORM does not use Mongoose-style lifecycle hooks).

Instance Methods

None defined.

Static Methods

None defined.

Service notes

createOffer — eligible parent request statuses

createOffer in SellerOfferService permits offers against a PurchaseRequest whose status is pending, received_offers, or active. Attempts against any other status are rejected.

withdrawOffer() — frontend action available

SellerOfferService.withdrawOffer() is not a dedicated HTTP route. The correct API path is PUT /api/marketplace/offers/:id/status with { status: 'withdrawn' }.

The frontend exposes this via the withdrawOffer(offerId) action in src/actions/marketplace.ts (added commit 240a668). It is called from:

  • step-2-waiting-for-payment.tsx (edit/cancel controls while requestDetails.status === 'received_offers')
  • frontend/src/app/dashboard/seller/marketplace/offers/page.tsx (Offer Management page, bulk view)

Relationships

  • References: User (sellerId = user.pgId), PurchaseRequest (purchaseRequestId).
  • Referenced by: PurchaseRequest (offers[], selectedOfferId), Payment (sellerOfferId), Chat (relatedTo.id when relatedTo.type === 'SellerOffer').
  • PG FKs: seller_offers.seller_id → users.id CASCADE, seller_offers.purchase_request_id → purchase_requests.id CASCADE.
  • Referenced by (PG): payments.seller_offer_id (polymorphic triple), payment_quotes (via payment join).

State Transitions

stateDiagram-v2
    [*] --> pending
    pending --> accepted : buyer accepts
    pending --> rejected : buyer rejects
    pending --> withdrawn : seller cancels
    accepted --> [*]
    rejected --> [*]
    withdrawn --> [*]

Common Queries

Postgres (Drizzle)

// Offers for a request
db.select().from(sellerOffers)
  .where(eq(sellerOffers.purchaseRequestId, requestId))
  .orderBy(desc(sellerOffers.createdAt));

// Seller's pending offers
db.select().from(sellerOffers)
  .where(and(
    eq(sellerOffers.sellerId, sellerId),
    eq(sellerOffers.status, 'pending')
  ));

// Reject siblings on accept
db.update(sellerOffers)
  .set({ status: 'rejected' })
  .where(and(
    eq(sellerOffers.purchaseRequestId, purchaseRequestId),
    ne(sellerOffers.id, acceptedId),
    eq(sellerOffers.status, 'pending')
  ));

// Cleanup expired offers
db.select().from(sellerOffers)
  .where(and(
    lt(sellerOffers.validUntil, new Date()),
    eq(sellerOffers.status, 'pending')
  ));

Related: PurchaseRequest, Payment, User.