Files
nick-doc/02 - Data Models/SellerOffer.md
Siavash Sameni d072238fe8 docs: update PG migration status, data models, architecture + add Telegram Mini App flow (v2.8.59)
- Postgres Runtime Cutover Status: 17 migrations (0000–0017), dual-write repo matrix
- Backend Architecture: dual-DB architecture, repo factory, MONGO_CONNECT_MODE modes
- Data Model Overview: 23-model index with PG table names and migration status
- User, PurchaseRequest, SellerOffer, Chat, Dispute: Drizzle schema + cutover status added
- 04 - Flows/Telegram Mini App.md: new doc covering Mini App architecture and flows
- mongo-to-pg-migration-prd.md: status block prepended with 2026-06-03 milestone tracking

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-03 10:30:51 +04:00

8.4 KiB

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

SellerOffer

Last updated: 2026-06-03 — added Postgres/Drizzle table definition and migration status.

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/models/SellerOffer.ts:24 — Mongoose schema definition backend/src/models/SellerOffer.ts:100 — Mongoose model export backend/src/db/schema/sellerOffer.ts — Drizzle/Postgres table definition

Migration Status

DUAL-WRITE — part of DualWriteMarketplaceRepo. Writes go to both MongoDB and Postgres; reads still come from MongoDB.

Schema

Mongoose (MongoDB)

Field Type Required Default Validation Index Description
sellerId ObjectId → User yes yes Seller submitting the bid.
purchaseRequestId ObjectId → PurchaseRequest yes yes Parent request.
title String yes trim, maxlength 200 Offer headline.
description String yes trim, maxlength 1000 Pitch and details.
price.amount Number yes min 0 Quoted amount.
price.currency String yes USDT enum: USD / EUR / IRR / TRY / USDT / USDC Quote currency. TRY is supported by the oracle/depeg path through the off-chain FX provider.
deliveryTime.amount Number yes min 1 Numeric ETA.
deliveryTime.unit String yes enum: hours / days / weeks ETA unit.
status String no pending enum: pending / accepted / rejected / withdrawn / active yes Offer status.
attachments[] String[] no URLs of supporting files.
notes String no trim Internal/private notes.
validUntil Date no Expiration.
requireAmlCheck Boolean no If true, AML screening must pass before the offer is presented to the buyer.
amlBlockOnFailure Boolean no If true and AML screening fails, the offer is blocked. Otherwise it is flagged for manual review.
createdAt Date auto yes (desc) Mongoose timestamp.
updatedAt Date auto Mongoose timestamp.

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.

Postgres (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() PG primary key
legacy_object_id text yes Mongo ObjectId bridge; partial-unique WHERE NOT NULL
seller_id uuid FK → users CASCADE no Maps from sellerId
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.

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

Virtuals

None defined.

Mongoose Indexes

Defined at backend/src/models/SellerOffer.ts:95-98:

  • { sellerId: 1 }
  • { purchaseRequestId: 1 }
  • { status: 1 }
  • { createdAt: -1 }

Pre/Post Hooks

None declared.

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), 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

MongoDB

// Offers for a request
SellerOffer.find({ purchaseRequestId }).sort({ createdAt: -1 });

// Seller's active offers
SellerOffer.find({ sellerId, status: 'pending' });

// Reject siblings on accept
SellerOffer.updateMany(
  { purchaseRequestId, _id: { $ne: acceptedId }, status: 'pending' },
  { status: 'rejected' }
);

// Cleanup expired offers
SellerOffer.find({ validUntil: { $lt: new Date() }, status: 'pending' });

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.