Dentolize · Suppliers (Marketplace) Walkthrough
On this pageWhat to testWhere the edges are (known code-level gaps to regression-test specifically, not just spot-check)

For Quality

What to test

Tenancy (do this first — it's the highest-severity class of bug here)

Every supplier query/mutation scopes by vendorCompanyId; every buyer query/mutation scopes by buyerCompanyId. Verify directly, not just through the UI:

  • Vendor A cannot read, edit, delete, or transition Vendor B's listings or orders, even with a guessed/known id (listingService.js, orderService.js all use findFirst({ id, vendorCompanyId }) — confirm every mutation path enforces this, not just the list queries).
  • A buyer cannot receive an order that isn't theirs, or receive using another company's tradeOrderId.
  • browseListings (buyer marketplace search) should only ever surface active: true listings — confirm inactive/soft-retired listings never appear.

The seller state machine — test the edges, not just the happy path

Reference: Order Lifecycle & Accounting for the full transition table. Specifically probe:

  • Every disallowed transition throws (e.g. PLACED → DELIVERED directly, DELIVERED → anything, SHIPPED → CANCELLED) — confirm the error message and that no state change/posting occurred.
  • ACCEPTED → SHIPPED (skipping Preparing) and PREPARING → DELIVERED (skipping Shipped) should both succeed — these are valid shortcuts, not bugs, per the coded table.
  • Confirm SHIPPED → CANCELLED is genuinely blocked end-to-end (this is a known, documented gap — see Order Lifecycle) rather than assuming it works because reversal code exists. If product wants this fixed, it needs a TRANSITIONS table change, not just a reversal-logic check.
  • REJECTED should only be offered/accepted from PLACED.

Accounting — balance and idempotency

  • Post a fulfilment (mark Shipped or Delivered) and confirm the journal entry balances, using the exact role amounts from Order Lifecycle & Accounting (AR = netRevenue + VAT; COGS pair only present when a cost is resolvable).
  • Re-trigger posting for the same order (e.g. Shipped → Delivered, which re-fires the posting call) and confirm no duplicate journal entry is created — idempotency is supposed to be guaranteed by postSourceCreated's existing-entry check.
  • Test COGS resolution specifically: an order line whose product has no matching InventorySubItem on the supplier's side, or whose matched stock has amount: 0, should silently contribute 0 cost — confirm the entry still balances with just the revenue/VAT pair (no COGS/Inventory lines at all) rather than erroring or unbalancing.
  • Test a GL misconfiguration (e.g. a missing default account mapping) — confirm the order status transition still succeeds (posting failure must not roll back the operational write), and that the posting is retryable rather than silently lost.

Quotas and entitlements

  • maxListings: create listings up to the plan's limit, confirm the next create is blocked with the exact upgrade message, confirm a per-vendor quotaOverrides.maxListings override takes precedence over the plan's field (both raising and lowering the effective limit).
  • maxImagesPerListing: confirm this one silently truncates the image list rather than blocking the whole listing save — deliberately different behavior from the listing-count quota; a test asserting an error here would be testing for the wrong behavior.
  • RFQ_RESPOND entitlement: test at all three enforcement points independently — (1) the supplierRfqs/supplierRfq queries return empty/null (not an error) when not entitled, (2) submitSupplierOffer throws an explicit upsell error, (3) a downgrade mid-session (entitled at page load, downgraded before submit) still blocks the submit — this proves the mutation-side check isn't relying on stale client state.
  • Broadcast RFQ visibility: confirm a vendor whose service-area geo matches an OPEN broadcast RFQ (scope: CITY/REGION/ALL) can see it regardless of the RFQ_BROADCAST_CITY/RFQ_BROADCAST_REGION_ALL entitlement tier — this is current, verified behavior, not a bug, but worth a product sign-off since those entitlement keys otherwise look unused.

The receive bridge — the trickiest data-integrity surface

  • Partial mapping: an order with 3 product lines where only 2 are mapped to buyer inventory (catalogProductId match) must be fully blocked, not partially received — confirm zero inventory/expense side effects occur (no partial InventoryOrder), and the error names all unmatched products.
  • Double receive: attempt to receive the same order twice — second attempt must fail cleanly with "already been received," and must not create a second InventoryOrder/Expense.
  • Status guard: attempt to receive an order still at PLACED/ACCEPTED/PREPARING — must be blocked, with the current status named in the error.
  • Service-only orders: an order with only SERVICE lines and no stockable product lines should be blocked ("no stockable product lines to receive") — confirm this doesn't crash rather than error cleanly.
  • Tax proportioning: confirm the posted expense's tax is the matched-lines' proportional share of the order's total tax, not the full order tax — verify with an order that mixes SERVICE and PRODUCT lines.
  • Branch/storage fallback: test a buyer company with no branches at all (should error cleanly, "No branch available"), and a stock line with no defaultStorageId (should fall back to the company's master storage, or any storage if no master is flagged).

Reorder math

suggestedReorderQty is a pure function — cheap to property-test directly: max(0, preferred − onHand), rounded to 6 decimal places, never negative, handles fractional stock quantities correctly (the module exists specifically to be tested without a database).

Cross-cutting / integration

  • Full path: place order (buyer) → accept → ship (seller) → confirm revenue posted (seller books) → receive (buyer) → confirm stock + expense posted (buyer books) → confirm the two sides' journal entries are entirely independent (no shared/reconciling entry, per the "no intercompany mirroring" design).
  • Currency/rounding: an order with delivery fee and discount together — confirm subtotal/tax/total rounding matches the accounting entry exactly (this is covered by a unit test for the derivation function, but worth an integration-level check that the resolver-computed order total and the posted journal amounts agree).

Where the edges are (known code-level gaps to regression-test specifically, not just spot-check)

  1. deleteSupplierListing returns true regardless of whether it hard-deleted or soft-retired — if any future UI depends on distinguishing these, it can't from this return value today.
  2. SHIPPED → CANCELLED is unreachable via transitionSupplierOrder even though the revenue-reversal code path exists and is unit-tested in isolation — don't let a future refactor "fix" this silently without a product decision, and don't assume integration coverage exists here just because the reversal math is tested.
  3. The resilient-posting retry path considers RECEIVED/CLOSED orders still "fulfilled enough to post," while the live transition trigger only fires on entering SHIPPED/DELIVERED — if a parked posting retries after the buyer has already received the order, confirm it still posts correctly rather than being skipped or erroring.