Dentolize · Suppliers (Marketplace) Walkthrough
On this pageTermsData model (verified against packages/prisma/schema.prisma)Entitlement tiers used by this PR

Glossary & Data Model

Terms

Vendor — a Company in Dentolize that also has a VendorProfile row, marking it as a supplier and/or lab. A company only becomes a vendor once admin-onboarded (enableVendorModule).

Buyer — any clinic or pharmacy company placing an order; no special provisioning needed, just the right permission flags.

Listing — a single product or service a vendor has published for sale, with its own price, MOQ, pack size, availability, and lead time.

TradeOrder — the shared record both sides act on: a buyer's placed order and a seller's fulfilment target are the same row, viewed from two different tenancy angles (buyerCompanyId vs. vendorCompanyId).

RFQ (Request for Quote) — a buyer's ask to one or more vendors to bid on a list of items. Creation/awarding is out of scope for this PR; this PR only builds the vendor's inbox + bid submission.

Offer — a vendor's priced response to an RFQ; one offer per vendor per RFQ (resubmitting replaces the previous one).

Entitlement — a company-level (not user-level) flag saying whether a vendor's subscription plan includes a given capability (e.g. RFQ_RESPOND, ACCOUNTING). Distinct from a permission, which is a per-user role flag.

Quota — a company-level numeric cap from the vendor's plan (e.g. maxListings), independently overridable per vendor.

Tier — a number (0–3) carried by a vendor's subscription plan; each entitlement has a minimum tier required to unlock it.

Receive (bridge) — the buyer-side action that turns a fulfilled TradeOrder into a real InventoryOrder (stock-in) and Expense (supplier bill), using Dentolize's existing purchase/expense machinery.

Catalog product mapping — the shared catalogProductId linking a vendor's Listing to a buyer's InventorySubItem, so the same underlying product can be priced/compared across vendors and matched during receiving.

Data model (verified against packages/prisma/schema.prisma)

This PR does not add or modify any Prisma models — all of the following were added by the shared feat/marketplace foundation (migration 20260810141539_trade_platform_foundation) and are only used by this PR's resolvers.

Core trade models

ModelKey fieldsNotes
ListingvendorCompanyId, type (PRODUCT/SERVICE), catalogProductId/catalogServiceId, name, price, moq, packSize, availability, activeCross-tenant vendorCompanyId is a raw String, no Prisma FK — resolved by explicit query, not a relation
TradeOrderbuyerCompanyId, vendorCompanyId, status, source, subtotal/delivery/discount/tax/total, inventoryOrderIdBoth company ids are raw strings; inventoryOrderId is the receive-bridge's write-back link
TradeOrderLineorderId, listingId?, type, catalogProductId?, qty, price, total
Rfq / RfqLine / RfqInviteebuyerCompanyId, status, scope (SELECTED/CITY/REGION/ALL), revealOffersLiveSealed-bid by default — buyer sees offers only after close unless revealOffersLive
Offer / OfferLinerfqId, vendorCompanyId, status, totals@@unique([rfqId, vendorCompanyId]) — one offer per vendor per RFQ

Vendor/subscription models

ModelKey fieldsNotes
VendorProfilecompanyId (unique), kind[] (SUPPLIER/LAB), enabled, verificationStatusAbsent row or enabled: false ⇒ not a vendor
VendorPlancode, tier, priceMonthly/priceYearly, maxListings?, maxUsers?, maxImagesPerListing?Nullable quota fields = unlimited
VendorSubscriptionvendorCompanyId (unique), planId, status, trialEndsAt?, addonFeatures[], featureOverrides, quotaOverridesOne active subscription per vendor company
VendorFeaturekey (unique), minTier, isAddon, activeAdmin-editable registry backing the entitlement gate — tier boundaries are config, not code

Status enums (exactly as declared)

  • TradeOrderStatus: DRAFT, PLACED, ACCEPTED, PREPARING, SHIPPED, DELIVERED, RECEIVED, CLOSED, CANCELLED, REJECTED
  • TradeOrderSource: DIRECT_CART, RFQ_ACCEPT, AUTO_CHEAPEST — this PR only ever produces DIRECT_CART
  • RfqStatus: DRAFT, OPEN, CLOSED, AWARDED, CANCELLED, EXPIRED
  • OfferStatus: INVITED, DRAFT, SUBMITTED, WITHDRAWN, ACCEPTED, REJECTED, EXPIRED
  • ListingAvailability: IN_STOCK, OUT_OF_STOCK, MADE_TO_ORDER, BACKORDER

Existing models this PR reads/writes into (not owned by the trade platform)

ModelWhat this PR does with it
InventorySubItemReads preferredAmount/amount for reorder suggestions; reads catalogProductId to match order lines during receiving; reads value/amount to derive the vendor's own unit cost for COGS
InventoryOrderCreated by the receive bridge (type: PURCHASE, status: COMPLETED) via the existing createNewInventoryOrder resolver
ExpenseCreated by the receive bridge (mainType: COST_OF_REVENUE) via the existing addNewExpense resolver, linked to the InventoryOrder
JournalSource (enum)No new enum value added — supplier fulfilment reuses INVOICE as the source, with a distinct sourceType: 'SupplierOrder' string for dedupe/idempotency keying

Entitlement tiers used by this PR

Feature keyMin tierAdd-on?
LISTINGS, ORDERS, INVENTORY0 (free)No
ACCOUNTING1No
BULK_IMPORT1No
RFQ_RESPOND1No
ANALYTICS_BASIC1No
MULTI_USER1No
FEATURED_LISTINGS2Yes

(Full registry, including keys not used by this PR — RFQ_BROADCAST_CITY, RFQ_BROADCAST_REGION_ALL, PAGE_BUILDER, CUSTOM_BRANDING, ADS_HIGHLIGHTS, ANALYTICS_ADVANCED, VERIFIED_BADGE, CUSTOM_DOMAIN, WHATSAPP_BLASTS, API_ACCESS — lives in packages/server/src/services/trade/entitlements.js.)