> For the complete documentation index, see [llms.txt](https://orbit-b2b.gitbook.io/orbit-b2b-docs/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://orbit-b2b.gitbook.io/orbit-b2b-docs/changelog/changelog.md).

# Changelog

All notable changes to Orbit Wholesale B2B are documented here.

***

## v1.0.11 — 2026-08-26

### New: a volume rule can require a minimum order

A volume pricing rule can now **block checkout** until the customer reaches its first tier — not merely withhold the discount, which the tiers already did. Until now only wholesale rules carried a minimum, so a merchant pricing in tiers had to keep a parallel wholesale rule alive just to borrow its minimum. *(Advanced plan and above.)*

* **The threshold is tier 1** — a switch, not a second number field. Edit the tier and the minimum moves with it, so the two can never drift apart and no tier can be left unreachable underneath the block
* **Works on both volume types** — minimum order value (MOV) on amount rules, minimum order quantity (MOQ) on quantity rules
* **Measured the way the tiers are measured**, so a cart that clears the minimum is exactly a cart that earns tier 1: per cart line for amount rules; following the rule's Discount method (same variant / mix variants / mix products) for quantity rules
* **Says so on the product page** — *"Save 10% — minimum €1,000.00 spend"* under the volume table, so shoppers learn the minimum before filling a cart instead of at checkout. Drops the saving clause when tier 1 is a fixed price. Editable per language in **Settings → Storefront Text → Volume Pricing**
* **Wholesale minimum quantity moves to Advanced too**, so the two minimums behave alike
* **A downgrade stops the enforcement, not the discount.** The setting stays saved and the rule keeps discounting — losing the ability to block checkout should not also cost you the pricing

[Full guide →](/orbit-b2b-docs/volume-pricing/minimum-order.md)

### Fixed

* **"Unlock wholesale price" went to the product page instead of login.** On themes that make the whole product card clickable, the card's own link was swallowing the button's click. The button now owns its click on every theme — and Ctrl/Cmd-click still opens login in a new tab
* **Every pricing screen printed "$"** regardless of your store's currency, so a SEK store setting a fixed price of 50 was told it was $50. The admin now reads your shop's real currency and money format everywhere, on existing installs as well as new ones
* **Rule values were compared against the buyer's currency without converting.** A "fixed price 50" rule sold at 50 EUR, 50 SEK and 50 JPY to whoever had selected that currency. All three layers that hold this arithmetic — storefront table, app embed, and the checkout function that decides what is actually charged — now convert from your shop's currency first, so they agree. Percent-off rules were never affected
* **A rule quota now caps how many rules RUN, not just how many exist.** After a downgrade, rules the app had switched off could be switched straight back on from the list, one at a time — three active volume rules on a plan that allows one. Activation is refused at the limit; editing a rule you already have is not
* **One free trial per store, not one per plan.** Trialling Essential, then Advanced, then Platinum was handing out three free weeks from a single install. Enforced on the subscription itself rather than only in the Plans page, so it holds no matter how the request arrives
* **A paying merchant kept being labelled "Trial"** after their trial quietly ran out and the first charge went through. The plan's status is now derived from the stored trial window rather than a flag that nothing updates at expiry
* **Settings → Storefront Text never reached the volume pricing table.** Editing "Qty range header" saved fine and changed nothing on the storefront — the table only ever read a rule's own Translations tab. The order is now: the rule's Translations → Settings → Storefront Text → the app's bundled English
* **The Plans comparison table claimed volume rules could only block on spend** — the MOQ row sat under Wholesale Pricing alone
* **The bell said nothing when marking a notification read failed** — it now says what went wrong

***

## v1.0.10 — 2026-08-19

### New: the whole admin in 7 languages

The app's admin now speaks **English, French, German, Spanish, Japanese, Chinese (Simplified) and Russian**. Every page is covered — Dashboard, Pricing, Customer Groups, Register Form, Quick Order, Quotes, Draft Orders, Orders, Settings, Tax, Plans and the Help Center.

* **Picks your language on first load** from your Shopify admin language, and remembers an explicit choice from then on
* **Switch any time** from the language button in the header
* Storefront copy stays where it always was: under each feature's Translations panel

### New: Analytics — what your B2B pricing actually earned

A new **Orders → Analytics** page reporting on B2B orders only, read from Shopify's own sales reports.

* **Date range and comparison** — today, last 7 / 30 / 90 days, last 12 months, month or year to date, or a custom range; compare against the previous period or the previous year
* **Per customer group** — orders, gross sales, discounts, returns and net sales, so you can see what each group costs and returns
* **Says how a customer was matched** — by customer group, by tag, or "no group", instead of a bare "No tag"
* **Export CSV** for anything you want to take further
* **Currency** — pick the currency to read the figures in; Shopify converts at each order's own rate
* **Available on every plan**
* **Needs one reconnect.** The page reads Shopify's reports, which requires a permission your store has not approved yet. Open the app from your Shopify admin and approve it — the page tells you when this is what is missing. Test orders never appear in these figures

### New: apply B2B pricing to a draft order, from the Shopify admin

An **Apply Wholesale Discounts** action on the draft order page (Advanced plan and above).

* **Preview before you commit** — pick the market, run a dry run, and see exactly which wholesale and volume rules would fire, or a plain "No eligible discount found"
* **Market-aware** — a rule scoped to one market no longer applies under another
* **Names the rule** that won on each discounted line, and tags the draft when B2B pricing has been applied

### New: in-app notifications

The what's-new banner is replaced by a **bell in the Dashboard header**. Announcements accumulate instead of overwriting each other, each one is marked read on its own, and nothing competes with the plan or setup banners for the page's one banner slot.

### Improved

* **Volume pricing — "Compact + Popup" table** (Advanced plan and above): a one-line teaser that opens a modal with quantity, unit price and saving per unit. The template picker now shows which plan each template needs
* **Volume table follows the variant you picked** — switching variant updates the table immediately instead of lagging one selection behind, including on themes with button or swatch pickers
* **7-day trial from install** — every storefront block is unlocked for the first week, no plan choice needed to try the app
* **Payment terms on quotes** — accepted quotes now carry Shopify's native payment terms onto the draft order
* **Date format is an app-wide setting** (Settings → General) and defaults to your store's own locale instead of DD/MM/YYYY. Emails, the PDF invoice and the admin all read the same setting
* **SMTP is validated as all-or-nothing**, so a half-filled mail config can no longer be saved into a silent failure
* **Built for Shopify pass** — a long round of admin fixes for Shopify's own quality standard: working back links and breadcrumbs on every sub-page, unsaved-changes protection on every form, validation errors shown on the field, no stacked banners, AA text contrast, and layouts that stop overflowing on mobile

### Fixed

* **Catalog Visibility masked too much** — only the gated product is hidden now, not the whole product page, and the form no longer paints before its data arrives
* **Hidden prices leaked into the volume table** under a narrow scope (products / collections / tags); the gate was comparing the wrong form of the product id
* **Storefront popups are keyboard- and screen-reader-usable** — the quote, variant drawer and volume modals move focus in, trap it while open, close on ESC, show a visible focus ring, and announce their totals and errors
* **Draft orders priced off the market price** in the presentment currency, instead of the shop's base price
* **B2B pricing survived duplicating a draft order**
* **Customer group tags re-sync on every save**, so a newly added group takes effect on the storefront immediately
* **Admin UI extensions could not call the app** — a CORS preflight redirect broke their requests
* **Quotes** — dates render in the merchant's chosen format, and a stale tag or group selection is cleared when the eligibility rule changes

***

## v1.0.9 — 2026-07-27

### New: Draft Orders — let B2B customers order on account

A whole-cart "order on account" flow. Eligible B2B buyers submit their cart for approval without paying; you confirm and send a Shopify draft order with a payment link (or Net-terms invoice).

* **Submit Order for Approval** — for eligible customers, the app replaces the cart's Checkout button with a submit-for-approval button (works on Dawn and most free themes; no Shopify Plus needed). The whole cart is captured, no payment is taken, and the cart is cleared on submit
* **Draft Orders inbox** — new admin page (Orders → Draft Orders) with status tracking (Pending / Invoiced / Paid / Cancelled); open a request to review items, customer, and payment term, then **Confirm & send payment link** or Cancel
* **Payment terms** — uses Shopify's built-in terms (Due on receipt, Net 15/30/45/60/90). Cascade: per-order override → Customer Group default → shop default → receipt. Set a group's term in Customer Groups → *B2B payment term*
* **Auto-create on submit** — optional setting to skip manual review: the draft order + payment link are created and emailed automatically the moment the buyer submits
* **Emails** — admin notified on a new request; customer emailed the payment link on confirm. Editable templates (English + Vietnamese defaults)
* **Multi-language storefront** — button label and confirmation strings are translatable
* **Plan** — runs on all Shopify plans

### Fixed

* **Quote currency** — quotes now display in the store's live currency instead of always showing USD, and amounts render with the plain currency symbol (e.g. `$240.00 NZD`, not `NZ$240.00`)
* **B2B group tags** — the storefront eligibility tag list now re-syncs on every Customer Group save (previously only on a tag rename), so newly added groups take effect immediately
* **Quotes** — the seller now gets an email when a customer replies or counter-offers on a quote (previously only an in-app badge)

***

## v1.0.8 — 2026-07-13

### Improved: One-form registration layout, translatable Company section, real store name in emails

* **One continuous form** — the Company information section no longer sits inside a bordered box; when a customer picks "A company", the extra fields now flow seamlessly with the rest of the form
* **Company section fully translatable** — the *I'm registering as* options, the "Company information" heading, all its field labels, and the country/state picker placeholders are now editable under **Form editor → Content Settings → Company section labels**, per language (defaults ship in 6 languages). Previously these texts always displayed in English
* **Emails show your store name** — customer-facing emails (application approved/rejected, email confirmation, company invites) now sign off with your actual store name instead of your `.myshopify.com` subdomain. No setup needed

***

## v1.0.7 — 2026-07-08

### New: Custom registration fields & file upload

Add your own fields to the B2B registration form, on top of the built-in set.

* **New field types** — Number, Radio, Checkbox (single consent), Checkbox group, and Date, alongside Text, Textarea, Dropdown, and File upload
* **+ Add element** — build custom fields in the form editor, configure per-type settings (list options, number min/max/step, date range, file rules), reorder, and delete
* **File upload → Shopify Files** — applicants attach documents (business license, tax certificate, …); files upload straight to Shopify admin → Content → Files, with server-enforced type and size limits, and appear as links in the submission detail
* **Auto-generated IDs** — each custom field gets a stable ID from its label (e.g. `reseller_license_ab12`), frozen after the first save so submissions keep their reference
* **Built-in fields untouched** — the standard fields still work on every plan and can't be deleted or retyped
* **Plan** — Custom fields require Advanced+; built-in fields remain available on all plans

***

## v1.0.6 — 2026-06-29

### New: Catalog Visibility

Hide your prices from shoppers who aren't your B2B customers. One global setting (Pricing → Catalog Visibility).

* **Two modes** — `Hide price only` (still buyable) or `Hide price and block Add-to-cart`
* **Audience** — show prices to any logged-in customer, or only to specific B2B customer groups; everyone else is gated
* **Product scope** — All products, Specific products, Specific collections, Product tags, or Vendors, plus an Exclude list that stays visible to everyone
* **Masked price** — gated shoppers see the currency symbol next to a blurred `XXX`; the real price is never sent to the page, so it can't be revealed by editing CSS. Price ranges (`$10 – $20`) are fully masked
* **"Unlock wholesale price" button** — appears under the price on product pages and collection cards; opens Shopify's (passwordless) sign-in and returns the shopper to the same page. The label is editable + translatable in Settings → Storefront Text → Catalog Visibility (defaults ship in 6 languages)
* **Takes priority over pricing** — a gated shopper never sees a Wholesale Discount badge, B2B price, or Volume Pricing table, even when a rule targets "All Customers"; normal pricing returns the moment they qualify
* **Hard checkout backstop** — in `Hide price and block Add-to-cart` mode, a Cart & Checkout Validation Function blocks checkout for an ineligible buyer who force-adds a gated product
* **Surfaces** — product pages, collection/listing cards, search results, the Volume Pricing table, and the cart
* **Plan** — Advanced+

***

## v1.0.5 — 2026-05-31

### New: B2B Customer Portal

The single-page Reorder Portal grows into a full **B2B Customer Portal** at `/apps/orbit-boost-wholesale/portal` — a sidebar shell wrapping four sections so wholesale buyers can self-serve orders, quotes, profile, and invoices without leaving the storefront. Server-rendered through the App Proxy; gated by login + plan + B2B eligibility (Customer Group membership). On mobile the sidebar collapses to horizontal tabs.

* **Sidebar shell** — every section (My Orders · My Quotes · Profile · Invoices) renders inside one consistent shell with the active item highlighted
* **My Orders** (`/portal`, `/portal/orders`) — recent orders with one-click reorder; the `/portal/reorder?order=<id>` preview shows live B2B + volume pricing (what the buyer pays today, not the historical price)
* **My Quotes** (`/portal/quotes`) — the buyer's quote list embedded in the shell. This is now the canonical "all quotes" entry. Standalone `/quotes` and `/quotes/:id` stay for email deep links and not-logged-in access
* **Profile** (`/portal/profile`) — view name / email / company / pricing tier; edit **phone** (flag country-code picker → E.164) and **default address** (Country select + dependent Province select). Returns the specific Shopify Admin error and preserves the buyer's input on failure; saves the rest of the address even when a province isn't a valid Shopify subdivision (best-effort, with a warning)
* **Invoices** (`/portal/invoices`) — accepted quotes with **Download PDF** (Net / Prepay terms) or **Pay / View** (Due-on-receipt receipt)

### Improved: Quotes

* **`QuoteSettings` now applies on the storefront** — the `formFields` toggles (target price, needed-by date, file attachment, PO number, tax ID) and the `copy` overrides (heading, description, button, success / reject messages) actually drive the storefront modal. The contact-info section is hidden when the customer is logged in
* **Specific, localized errors** (English + Vietnamese) with the actual threshold values — auto-reject minimum quantity / floor %, eligibility (must log in / must be approved B2B), and public-mode rate limit now say exactly what failed instead of a generic "Could not save"
* **Persisted request details** — PO number, needed-by date, tax / VAT ID, and submitter IP are stored on the quote and shown in the admin Quote detail. Guest submissions allow a null customer ID
* **Admin deep link fixed** — the "Open quote in admin" link in admin emails (`/quotes/:id`) now opens that quote pre-selected in the inbox

### Fixed

* **App Proxy JSON responses no longer wrapped in theme HTML** — the quotes, reorder-price, and portal JSON endpoints force `Content-Type: application/json`, so `fetch().json()` works (Shopify was rendering JSON inside the theme Liquid layout)
* **Reorder preview pricing** — the preview now mirrors the checkout discount Function: it evaluates the wholesale **and** volume pools, resolves the customer-group tier, and combines them per the shop's discount strategy (stack vs best). Changing a line quantity re-prices via the API so volume tiers show before checkout
* **Quote settings toggles were unlabeled** — the admin switches now render their label + helper text correctly
* **"Create group" / quote-settings modals** open reliably from the page title bar

***

## v1.0.4 — 2026-05-23

### New: Reorder Portal

Customer-facing portal at `/apps/orbit-boost-wholesale/portal/orders` where eligible B2B customers see their recent orders and one-click reorder selected items back into the cart. Pairs with a scheduled reminder email that nudges customers to come back N days after fulfillment.

* **Recent orders list** at `/apps/orbit-boost-wholesale/portal/orders` — last 20 orders with B2B badge, `B2B only` filter toggle, Reorder button per row
* **Reorder preview** at `/apps/orbit-boost-wholesale/portal/reorder?order=<id>` — preview each item, adjust qty, deselect out-of-stock items, Add selected to cart
* **Wholesale-aware pricing in the preview** — the price each item shows is what the customer would pay today (their current customer group + active rules + product eligibility), not the historical order price. Cart total matches the preview total
* **Scheduled reminder cron** — up to 3 reminders at configurable delays (e.g. 30 / 60 / 90 days after fulfillment). Daily at 06:00 UTC, multi-server safe via Mongo lock. 3-day query window absorbs missed ticks
* **Reorder reminder email template** under Settings → Email Templates → Reorder Reminder, with shortcodes `{{first_name}}`, `{{shop_name}}`, `{{days_ago}}`, `{{reorder_link}}`, `{{unsubscribe_link}}`. Per-locale translations (en, vi, fr, de, es, ja)
* **HMAC-signed unsubscribe link** in every email — customer clicks → recorded in `ReorderUnsubscribe` → all future cron runs skip them
* **CustomerGroup-derived eligibility** — a customer is B2B-eligible iff they belong to any Customer Group. No tag list to configure. Eligibility used everywhere: portal access, order tagging, reminder cron targeting
* **`orders/create` webhook** auto-tags B2B orders with `orb2b-b2b`. Idempotent. Reminder cron queries by this tag
* **Schedule + sender config** under Apps → Orders → Reorder Settings → Schedule (enable, day delays, marketing consent). Email content lives in Email Templates so it's edited alongside Registration + Quote emails

### New: B2B Prices app embed

Replaces the drag-into-section `Wholesale Price` + `Card Prices` blocks with a single app embed (target: head) that overrides retail prices everywhere on the storefront — PDP, sticky add-to-cart, and product cards on collection / search / homepage surfaces.

* **One toggle, applies everywhere** — Theme editor → App embeds → Orbit B2B Wholesale → ON. No per-section drag, no theme code edits, no separate blocks for PDP vs cards
* **Two display modes** — Strikethrough (default: `~$25.00~ $21.25 [Save 15%]`) or Replace (`$21.25`). Pick under Settings → General → B2B Price Display
* **Save badge computed from total savings** — works correctly when Wholesale + Volume stack. `(retail - final) / retail` formula so a 30% wholesale + 5% volume stack shows `Save 34%`, not the nominal value of any single rule
* **Optional currency code suffix** (`$21.25 USD`) toggleable under the same settings section
* **Theme adapter chain** — built-in adapters for Dawn, Horizon, Craft, Refresh + a generic fallback covering most other themes. Each adapter declares the right CSS selectors for PDP price, sticky bar, card selector, card link, card price container, card price text
* **Merchant selector overrides** — the embed's advanced section in Theme editor (or the `theme_override` shop metafield for operators) lets you point at custom selectors if your theme is exotic. Block settings take precedence; metafield is the operator escape hatch
* **Variant-change reactivity without flash** — uses the cart **Add to cart** button's `disabled` attribute as the theme render lifecycle signal. Pre-hide CSS gate re-engages on enabled → disabled, releases on disabled → enabled. Eliminates flash of retail price during Section Rendering API swaps
* **Quantity-change reactivity** — Volume Pricing tiers update live as the customer changes the qty input or presses +/- stepper buttons. The PDP shows exactly what the customer will pay at checkout for that qty
* **Pre-hide layer** — `orb2b-pending` class on `<html>` hides theme price selectors via `visibility: hidden` until the resolver decides what to do. 1.5s safety release prevents permanent freeze
* **Settings → General → B2B Price Display** is the merchant entry point: pick mode + toggle Save badge + toggle currency code

### Removed

* `wholesale-price.liquid` section block (drag-into-PDP) — replaced by the B2B Prices app embed
* `wholesale-card-prices.liquid` body embed — folded into the new app embed's payload + listing JS
* `extensions/wholesale-block/src/main.jsx`, `pricing.js`, `PriceDisplay.jsx`, `vite.config.js` — the wholesale-price build pipeline (volume-pricing-table build kept)
* Per-feature From name + Reply-to fields in Reorder Settings → Schedule — reorder reminders now use the shared SMTP From email (same as registration + quote emails)
* `eligibilityTags` text input in Reorder Settings — replaced by automatic eligibility derivation from CustomerGroup membership

### Improved: Email Templates

Reorder reminder is the third feature with editable Email Templates (alongside Registration and Quote). The `Reorder Reminder` template lives under **Settings → Email Templates → Email Templates · Reorder**, edited the same way as the others.

The cron job uses `EmailTemplateService.getForSending` and the shared `EmailService.send` (shop's SMTP config) instead of a feature-specific renderer. One sender path, one template editor, one set of shortcodes.

### Improved: Wholesale rule "winning" logic clarified in Help Center

Existing "Which rule wins when multiple match" docs updated to reflect the actual specificity → best-price → priority tiebreak chain in `pricing.js`. Old docs claimed priority is the primary factor (it's actually the final tiebreaker). Updated example calls out the gotcha that caused several support tickets: a `logged_in` rule with 0% discount silently outranks an `all` rule with 15% discount because logged\_in scores higher on specificity.

### Improved: Settings → General → B2B Price Display section

New subsection on the existing General settings page. Display mode (strikethrough / replace) + Save badge toggle + Currency code toggle. Synced to `shop.metafields.orbit_wholesale.price_display` via DiscountSyncService so the storefront embed reads it without a per-request DB hit.

### Improved: PLAN\_ENFORCEMENT=off (local dev) also writes platinum metafield

Setting `PLAN_ENFORCEMENT=off` previously unlocked admin UI + proxy gates but storefront Liquid blocks (which read `shop.metafields.orb2b.plan`) still saw the shop as Free. The boot-time hook now iterates active merchants and writes `platinum` to each shop's plan metafield when env=off, so storefront blocks render unlocked on local dev without per-shop unlock-store curls.

> **Superseded 2026-08-11** — the boot-time hook has been removed. It re-ran on every restart (nodemon fires on each save), and on a container left at `PLAN_ENFORCEMENT=off` it re-unlocked every shop on each deploy. It also only ever wrote `platinum`, so flipping back to `enforce` never relocked anything. Run `node backend/scripts/resync-plan-metafields.js` instead — it handles both directions.

### Fixed

* Pre-hide CSS gate properly re-engages on variant change so retail price doesn't flash during theme's async section render
* Wholesale rule resolver no longer returns the LAST rule applied as "winning" in stack mode — Save badge text now reflects total savings (`(retail - final) / retail`) not the last rule's discountValue. Eliminates "Save 0%" displaying $7 off
* Cart overlay markup wipes the theme's existing price text before injecting the B2B overlay — eliminates the triple-render of retail + struck-retail + B2B in some themes
* Collection card overlay same fix — was duplicating the theme's `$600.00 USD` text alongside the overlay
* `Customer.acceptsMarketing` deprecated GraphQL field swapped to `emailMarketingConsent.marketingState` (removed in API 2024-01+; would have silently broken reorder reminders on 2025-07 API)

### New: Quote requests (RFQ)

A full B2B price-negotiation workflow inspired by Alibaba's "Contact Supplier" flow. Buyers click **Request a Quote** on any product page, your team replies with custom prices, buyer accepts → Shopify draft order created. Works on every Shopify plan, no Plus required.

* **Storefront block** — Drag the **Request a Quote** app block onto product pages from theme editor. The modal opens inline (no redirect) with the product's variants pre-loaded. Logged-in customer info auto-fills name + email via Liquid.
* **Buyer flow** — Set quantities per variant, suggest a target price (optional), add a message, attach a PDF / JPG / PNG up to 2 MB.
* **Admin inbox** — Email-style inbox at **Apps → Orbit B2B → Quotes** with filter tabs (Pending / Replied / Negotiating / Accepted / Rejected / Expired), search, unread badges, and background polling every 30s.
* **Customer info card** — Each quote shows the buyer's company link, pricing tier link, Shopify tags, and full quote history (total / accepted / rejected / expired + accept rate %).
* **Items + adjustments editor** — Edit unit prices, add discount (fixed / percentage), shipping, validity (3 / 7 / 14 / 30 days or custom), payment terms.
* **Conversation thread** — Back-and-forth messaging between buyer and admin. Two reply buttons: **Send message only** (text reply, no price change) vs **Send price update** (saves a new revision + emails the buyer).
* **Customer storefront page** — Buyers see their quotes at `/apps/orbit-boost-wholesale/quotes` and individual detail pages with Accept / Decline / Send reply buttons (appear once admin has replied).
* **Eligibility rules** — Approved B2B only (default — requires `b2b` / `wholesale` / `tier-*` tag), any logged-in customer, or public. Backend re-validates at submit time so frontend tags can't be spoofed.
* **Auto-reject** — Floor percentage below catalog (reject low-ball offers) and minimum total quantity (filter retail-size requests). Rate limit per IP for public mode.

### New: Payment terms with PDF invoice (any Shopify plan)

Quotes support **Due on receipt**, **Net 15 / Net 30 / Net 60**, **Prepay 50%**, and **Custom** payment terms — without requiring Shopify Plus's B2B feature set.

* **Due on receipt** — buyer accepts → redirected to Shopify checkout invoice → pays immediately
* **Net X / Prepay / Custom** — app generates a PDF invoice from the quote, emails it to the buyer with payment instructions. Buyer pays offline (bank transfer / wire / check). Admin marks the order paid manually in Shopify Orders when the money lands.
* **Shopify draft order tags** — Every accepted quote creates a draft order tagged with `Quote #<number>` and a `Payment terms: Net 30` custom attribute so admin can trace it from Shopify Orders.
* **orders/paid webhook** — Once admin marks paid, the order links back to the quote via webhook for full traceability.

### New: Auto-expiry + reminders

A Mongo-locked hourly cron job:

* Expires any **Replied** / **Negotiating** quote past its `validity.validUntil` → status flips to **Expired** and buyer receives a polite expired email
* Sends an **Expiring soon** reminder email 24 hours before validity ends (one-shot, idempotent)
* Skips quotes with no validity set (e.g. still Pending)

### New: 6 quote email templates

Editable per language under **Email Templates → Quote**:

* **Quote Submitted** (to admin) when a buyer submits a new request
* **Quote Reply** (to buyer) when admin sends a price update
* **Quote Accepted** (to admin) when buyer accepts the offer
* **Quote Invoice (Net terms)** (to buyer, with PDF attached) for Net X / Prepay / Custom payment flow
* **Quote Expiring Soon** (to buyer) 24h before validity
* **Quote Expired** (to buyer) when validity lapses

All templates ship with default translations for English, Vietnamese, French, German, Spanish, Japanese.

### New: Multi-language storefront text

Every visible string on the Quote modal + customer detail page is editable per language under **Settings → Storefront Text** in 6 groups: Modal form, Detail page, Status badges, Errors & notices, Payment terms, plus the existing Volume Pricing / Register Form / Global groups.

* Pushed to `shop.metafields.orbit_wholesale.app_settings_i18n` on save
* Storefront block reads it inline via Liquid — no extra round trip on cold load
* Default translations for 6 languages, unlimited additional languages via the language tabs

### Improved: customers/delete webhook (GDPR)

When a Shopify customer is deleted, the app now also deletes their Quotes + QuoteMessages alongside the existing Company / CustomerGroup cleanup. Idempotent + safe to retry.

### Improved: Documentation

* New **Quotes** section in the in-app Help Center (Documentation menu) — overview, modal flow, admin inbox, payment terms, auto-expiry, email templates
* Workflow diagrams updated with 5 flows: Buyer requests → Admin replies → Accept (Due on receipt) → Accept (Net X PDF) → Auto-expiry
* New troubleshooting guides for common Quote issues

### Fixed

* Modal "Request a Quote" no longer redirects to App Proxy URL — opens inline on product page
* Liquid block now uses Shopify CDN via `asset_url` filter for `quote-storefront.{js,css}` (same pattern as variant-drawer) — faster + resilient to backend outages
* Quote thread message authors now show **Wholesale Team** / **Customer** / **System** correctly based on `author.type` instead of falling back to "Customer" for everyone
* "Send price update" admin button no longer triggers loading spinner on both reply buttons — each tracks its own action

***

## v1.0.3 — 2026-05-15

### New: Quick Order

Bulk-order form for B2B buyers, embeddable on any page or as a dedicated `/pages/quick-order`.

* **Multi-form** — Create different forms for different customer groups (Gold, Silver, Wholesale, etc.). Storefront automatically picks the most specific active form for the logged-in customer
* **4 product modes** — All products, Specific products, Specific collections, or Products with tags. Use the Shopify resource picker to select
* **Auto-populate** — In `Specific products` mode, the form pre-fills rows with the chosen products on page load. Multi-variant products show as parent group with chevron toggle to expand/collapse variants
* **MOQ rules** — Min, max, and step quantity per row. Industry presets (Beverage, Apparel, Cosmetics, Electronics, Food) one-click apply suggested values
* **Quick-pick chips** — Configurable shortcuts under each row (e.g. 12 / 24 / 48 / 96). Click to fill the focused row or bulk-fill all available rows
* **Pagination & infinite scroll** — `None` / `Pagination` (Prev/Next + page number) / `Infinite scroll` (Load more button). Items per page configurable. Background prefetch loads remaining chunks while the user is busy with the first batch — pagination shows the correct total page count from first paint
* **Skeleton loader** — Shimmer-animated placeholder while the form initializes; respects `prefers-reduced-motion`
* **Full design tokens** — Primary color, button text color, text/subdued/border/background/row-hover, font size, row padding, border radius, custom CSS
* **Per-locale translations** — Heading, description, button labels translatable per published locale. Generic strings (In stock / Out of stock / Quick pick / errors) come from bundled translations (en, vi, fr, de, es, ja)
* **Tier pricing in Unit column** — Wholesale Discount applied per variant for the logged-in customer (matches the price shown on product pages). Volume Pricing tiers apply at checkout via the Shopify Function

### New: Metafield-first storefront pattern

All storefront blocks (Wholesale Price, Variant Drawer, Quick Order) now read directly from Shopify metafields in Liquid — zero app proxy round trip on cold load. Eligibility, locale, and config resolve client-side using the customer tags Liquid hands down. App proxy is used only for customer-aware work (per-variant tier pricing).

### New: Tax hub (preview)

New top-level menu **Tax** with four coming-soon sub-pages: Tax Rules, Tax-exempt customers, Invoices, Settings. UI placeholder for the upcoming VAT / invoice feature.

### Improved: Hub URL consistency

All hub landing pages now use the `-hub` suffix: `/pricing-hub`, `/customers-hub`, `/orders-hub`, `/tax-hub`, `/settings-hub`. Feature pages keep their short URLs (`/wholesale-pricing`, `/companies`, `/orders`, `/settings`). Existing breadcrumb links updated.

### Improved: Quick Order admin polish

* Settings consolidated into the admin form (drop `show_header` / `show_image` / `max_visible_rows` from theme block schema — they live on the form now and apply across all block instances)
* Locale-aware Copy section with per-locale fallback to primary
* Translation index lists Quick Order forms with missing-keys count + restore default
* List page mirrors Wholesale Discount pattern: status tabs, search, sort popover, bulk delete, pagination, inline status switch
* Collection list uses local state + immediate picker result (no waiting for GraphQL refetch)

***

## v1.0.2 — 2026-05-13

### New: Companies (B2B organizations)

A dedicated record for each B2B business buyer. Each Company can have multiple contacts (the people who place orders for it) and an assigned Customer Group (pricing tier). When you set a tier on a Company, every contact in that company automatically gets the tier's tag and the wholesale pricing that goes with it.

* **5 tabs in the Company editor** — General, Customer group, Address, Registration (shareable invite link), Contacts (B2B Customer Picker UI for add/remove)
* **One-click invite links** — Each Company gets a unique registration URL. Share it with the company's team so they can self-register against that company in one click
* **Invite by email** — Send the invite link to one or many addresses with a single click; recipients get the templated invite email
* **Tier inheritance** — Assign a Customer Group to a Company → all contacts inherit the tier's tag automatically. Remove a contact → tag removed (unless they're a contact of another company with the same tier)
* **Active / Inactive status** — Inactive companies hold contacts dormant: their tier is intentionally NOT applied until you flip the status back to active

### New: B2B Customers — unified view

The Registration page now has two views you switch between with a button at the top:

* **Applications** — the inbox of sign-up requests waiting for review (the old default view)
* **All customers** — aggregated list of every active B2B customer regardless of how they were added (form approval, Customer Group, or Company contact)

Filters on the All customers view:

* **Buyer-type buttons**: All / Individual buyers (solo wholesale — not in any company) / Company contacts
* **Tier dropdown** next to search: pick All tiers (default), a specific Customer Group, or "No tier yet" to surface customers you still need to assign a discount tier to
* **Empty state** with quick links to Customer Groups and Form Settings when no customers exist yet

### New: Three registration modes on the storefront form

The storefront registration form now supports three modes via a toggle:

* **Individual buyer** — solo wholesale customer, fills personal info + tax ID + address inline in the main form
* **A company** — registers as a new company. A "Company information" section appears with company name + tax ID + HQ address. On approval, the app auto-creates a Company record + adds the submitter as its first contact
* **Join company** — when the form is opened via a company's invite link (`?company=<slug>`), the toggle is hidden and the customer registers as a contact of that company (only personal info asked; the linked company already has the rest)

Mode toggle labels (Individual buyer / A company / Company information heading) are translatable via Settings → Translations and ship with defaults for English, Vietnamese, French, German, Spanish, and Japanese.

### New: Company registration approved email

A dedicated template (`company_registration_approved`) sent when a "new-company" mode submission is approved. Welcomes the contact and includes the company's invite link so they can share it with their team. Ships with translations for the 6 supported locales out of the box.

### New: Help Center inside the app

The Dashboard's "Documents" button now opens an in-app Help Center (`/help-center`) with a sidebar of topics mirroring this docs site. No more leaving the admin to read the manual.

### Improved: Layout for editor pages

All `New` / `Edit` pages for Wholesale Pricing, Volume Pricing, and Companies now use a 3-column grid (300px left tab nav | content | 300px right info sidebar) with consistent widths and Polaris `inlineSize="large"`. Companies info card now renders properly as a card section.

### Improved: Rich text editor loading state

The email body editor shows a spinner placeholder while templates are loading from the server, instead of mounting an empty Quill instance that flashes blank.

### Operational: Scheduled jobs

Two safety-net jobs now run automatically in the backend:

* **Reconcile Company ↔ CustomerGroup membership** — daily at 03:00 UTC. Backfills `CustomerGroup.customerIds` if any drift slips past the webhook path
* **Cleanup orphan customer IDs** — weekly Sunday 04:00 UTC. Removes Shopify customer GIDs from Company contacts and Customer Group members when the customer no longer exists on Shopify (covers missed `customers/delete` webhooks)

Both jobs use a Mongo lock so multi-instance deploys don't double-run. Set `DISABLE_SCHEDULED_JOBS=1` in development to skip.

***

## v1.0.1 — 2026-05-09

### New: Variant Drawer

A brand-new feature that lets B2B customers order multiple variants at once through a slide-out drawer.

* **Multi-variant ordering** — Customers select quantities for every variant (size, color, style) in a single drawer and add them all to cart at once
* **Grouped variant display** — Products with 2 options (e.g. Color + Size) show the first option as clickable swatches that filter the variant list below
* **4 display modes** — Open on variant swatch click, secondary button next to Add to Cart, replace Add to Cart, or floating sticky button
* **Order constraints** — Set minimum order quantity per variant, minimum total across all variants, and maximum per variant. Real-time validation in the drawer
* **Action buttons** — Configurable Add to Cart (primary) and Start Order (secondary) buttons. Each can be enabled/disabled per rule
* **25+ design tokens** — Full control over every visual element: drawer background, header, swatches, variant rows, quantity stepper, footer, action buttons, MOQ notice, trigger button, and custom CSS
* **Zero-latency styling** — Design settings stored in Shopify metafields and read in Liquid at page render. Drawer opens with correct styling instantly, no style jump
* **Per-locale labels** — Drawer title, button labels, and action text can be translated for each published store language. Fallback to primary locale for untranslated fields
* **Live admin preview** — Full-page editor with 3-panel layout (section nav, live preview, settings). Preview uses real product data from your store
* **Real product picker** — Click the product link in the editor sidebar to pick any product from your catalog. Variants and options update dynamically in the preview
* **Customer/product/market targeting** — Same powerful eligibility system as Wholesale Pricing: target by customer tags, groups, login status, specific products, collections, product tags, or Shopify Markets
* **Exclude rules** — Exclude specific customers, products, or collections even if they match the eligibility criteria
* **Priority system** — When multiple rules match the same product, the highest priority rule wins

### Improved: Save bar across all pages

* All admin pages now use Shopify's native `<ui-save-bar>` component with proper loading state
* Save button shows a spinner while saving; Discard button is disabled during save
* Applied to: Wholesale Pricing (New/Edit), Volume Pricing (New/Edit), Customer Groups (Detail), Settings
* Error feedback via `shopify.toast.show()` with error details

### Improved: Settings — About section

* Updated app description and version number
* Added feature list reflecting all current capabilities

***

## v1.0.0 — 2026-04-01

### Initial release

* **Wholesale Pricing** — Create rules to set special prices for B2B customers by percentage off, fixed amount off, or fixed price. Target by customer tag, login status, specific products, collections, product tags, or Shopify Markets. Per-variant pricing support. Rule priority and scheduling with start/end dates
* **Volume Pricing** — Quantity-break and amount-break tier discounts. Three discount methods: same variant, mix variants, mix products. Storefront pricing table with multiple templates (Default, Alibaba Cards). Table design customization
* **Customer Groups** — Organize B2B buyers into tiers (Bronze, Silver, Gold, Diamond) with color-coded tags. Bulk assignment via registration approval
* **B2B Registration Form** — Drag-and-drop form builder with 9 field types. Custom design settings. Approval/rejection workflow with resubmission support. Auto-approve option
* **Email Notifications** — 4 email templates (new submission, approved, rejected, auto-approved). Rich text editor with shortcodes. Custom SMTP support. Built-in relay fallback
* **Storefront Translations** — Translate pricing labels, volume table headers, registration form fields, and email templates into any published store language
* **Card Prices** — Automatic wholesale price display on collection pages and product cards
* **Discount Strategy** — Choose between "best price for customer" (default) or "stack" mode for combining wholesale + volume discounts
* **Shopify Functions** — Checkout discounts applied via Shopify's native discount function — no coupon codes needed


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://orbit-b2b.gitbook.io/orbit-b2b-docs/changelog/changelog.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
