On this page
TermsData model (verified againstpackages/prisma/schema.prisma)Entitlement tiers used by this PRGlossary & 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
| Model | Key fields | Notes |
|---|---|---|
Listing | vendorCompanyId, type (PRODUCT/SERVICE), catalogProductId/catalogServiceId, name, price, moq, packSize, availability, active | Cross-tenant vendorCompanyId is a raw String, no Prisma FK — resolved by explicit query, not a relation |
TradeOrder | buyerCompanyId, vendorCompanyId, status, source, subtotal/delivery/discount/tax/total, inventoryOrderId | Both company ids are raw strings; inventoryOrderId is the receive-bridge's write-back link |
TradeOrderLine | orderId, listingId?, type, catalogProductId?, qty, price, total | |
Rfq / RfqLine / RfqInvitee | buyerCompanyId, status, scope (SELECTED/CITY/REGION/ALL), revealOffersLive | Sealed-bid by default — buyer sees offers only after close unless revealOffersLive |
Offer / OfferLine | rfqId, vendorCompanyId, status, totals | @@unique([rfqId, vendorCompanyId]) — one offer per vendor per RFQ |
Vendor/subscription models
| Model | Key fields | Notes |
|---|---|---|
VendorProfile | companyId (unique), kind[] (SUPPLIER/LAB), enabled, verificationStatus | Absent row or enabled: false ⇒ not a vendor |
VendorPlan | code, tier, priceMonthly/priceYearly, maxListings?, maxUsers?, maxImagesPerListing? | Nullable quota fields = unlimited |
VendorSubscription | vendorCompanyId (unique), planId, status, trialEndsAt?, addonFeatures[], featureOverrides, quotaOverrides | One active subscription per vendor company |
VendorFeature | key (unique), minTier, isAddon, active | Admin-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, REJECTEDTradeOrderSource:DIRECT_CART, RFQ_ACCEPT, AUTO_CHEAPEST— this PR only ever producesDIRECT_CARTRfqStatus:DRAFT, OPEN, CLOSED, AWARDED, CANCELLED, EXPIREDOfferStatus:INVITED, DRAFT, SUBMITTED, WITHDRAWN, ACCEPTED, REJECTED, EXPIREDListingAvailability:IN_STOCK, OUT_OF_STOCK, MADE_TO_ORDER, BACKORDER
Existing models this PR reads/writes into (not owned by the trade platform)
| Model | What this PR does with it |
|---|---|
InventorySubItem | Reads 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 |
InventoryOrder | Created by the receive bridge (type: PURCHASE, status: COMPLETED) via the existing createNewInventoryOrder resolver |
Expense | Created 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 key | Min tier | Add-on? |
|---|---|---|
LISTINGS, ORDERS, INVENTORY | 0 (free) | No |
ACCOUNTING | 1 | No |
BULK_IMPORT | 1 | No |
RFQ_RESPOND | 1 | No |
ANALYTICS_BASIC | 1 | No |
MULTI_USER | 1 | No |
FEATURED_LISTINGS | 2 | Yes |
(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.)