TackQuote: Request a Quote & B2B Wholesale for WooCommerce. Full changelog, newest first.
The latest releases are listed in readme.txt.

= 1.13.0 =
Released 2026-10-26.

* **One source for each setting, and every 1.12.0 setting kept in the plugin.** On a TackQuote server that offers it (the `store_settings` and `store_settings_write` capabilities), order sync, TackQuote prices, order limits, the buyer group badge, tax exemption and the store's wholesale form are kept in TackQuote. They stay editable on the plugin's own tabs, exactly where 1.12.0 had them: saving sends every switch the tab changed to TackQuote first, in one request (`PUT /integrations/woocommerce/store-settings/from-plugin`, with the acting WordPress user's ID, a number only, on a server that asks for it), and only when TackQuote takes it does the page show it saved; otherwise the previous value stays and the reason is shown. Turning order sync on asks you to tick a statement about the personal data it sends, which TackQuote records. The plugin reads the settings on its ten-minute background check of the store's connection (one ping plus one read; never during a shopper's request) and when an administrator opens stale settings, so a change made in TackQuote shows up here too. Each switch also has a "Turn off … on this store" box that only affects this store. When TackQuote cannot be reached, the last answer it gave stays in force; with none yet, the plugin's own setting decides. The first time, the plugin tells TackQuote how the store is set up today, including the wholesale form slug typed in the plugin, so nothing changes on the update. On any other TackQuote server everything works exactly as in 1.12.0.
* **Buyer groups and the wholesale form from TackQuote's lists.** On such a server the Buyer groups tab lists TackQuote's groups (name and code) to pick from for the restriction grids (none picked: all), and the Forms tab picks the wholesale form from TackQuote's forms: no code or slug is typed by hand any more. Rules saved before keep their groups.
* **TackQuote menu.** The top-level TackQuote menu (as in 1.12.0) now holds both pages: **Dashboard**, the TackQuote workspace that 1.12.0 showed under WooCommerce → TackQuote, and **Settings**, the plugin's tabs. Both share one tab bar. Old links (`admin.php?page=tackquote&tab=…`, `admin.php?page=tackquote-dashboard`) open the same pages. **Who can change what:** the Storefront, B2B pricing, Buyer groups and Forms tabs now need the WooCommerce shop-manager capability (`manage_woocommerce`) instead of `manage_options`, so shop managers can change them; on a TackQuote server that keeps these settings, their changes are written to TackQuote. The Connection tab (API key) and the Order sync tab still need an administrator (`manage_options`) and are read-only for everyone else.
* **Connection tab.** Connect with TackQuote comes first, with "Allow catalogue and order sync" right after it as part of the same flow; pasting an API key and changing the API URL moved under "Advanced".
* Fixed: the quote form now asks the seller's own registration questions (Settings in TackQuote: text, long text, number, choice, checkbox or date; for companies, individuals or both) and sends the answers. A seller with a required question used to get every quote request from the store refused. Answers are checked in the browser and on the store before they are sent, and only questions the seller's policy lists are sent.
* Fixed: on the Order sync tab the box showed ticked before the setting was ever saved, while no order was being sent; it now shows off, as it is.
* Fixed: at phone widths (768px and below, WooCommerce's own small-screen breakpoint) the product-card "Add to Quote" takes 1em side padding, so on a theme with wide button padding (Twenty Twenty-Five) the longer label fits beside Add to cart instead of wrapping; the theme's button style is otherwise unchanged.
* New storefront text translated in the seven bundled languages (machine-assisted, pending native review).
* **Link your TackQuote account in one place.** On TackQuote → Dashboard, "Link my TackQuote account" now opens TackQuote in a new tab: sign in as yourself, confirm "Link WordPress login … on … to …", and WordPress opens the dashboard. No code to copy between the two. It needs a TackQuote server that offers it; otherwise, and as a fallback ("Have a link code?"), the one-time code from Integrations → WooCommerce → Link a WordPress login works as before. The link is bound to the WordPress login that pressed the button (a one-time PKCE verifier and state kept in that user's meta for ten minutes), so an approval made from a forwarded link by someone else links nothing.

= 1.12.0 =
Released 2026-10-26.

* **Store details for TackQuote.** On a TackQuote server that asks for them, the connection check also sends the site name and address, the WordPress time zone, the WooCommerce currency and the store base address (WooCommerce → Settings → General). TackQuote keeps them on this store's connection and sets your workspace's time zone from the store's only when the workspace has none. A site on a manual UTC offset rather than a city is not used for the time zone.
* **Variations named the way catalogue sync names them.** On a TackQuote server that supports it, a quoted variation is sent as `<product ID>_<variation ID>`, the key TackQuote's catalogue sync gives that variation, so a store without catalogue sync no longer leaves a second product record in TackQuote at the bare variation ID.
* Fixed (live QA): a quote form you designed in TackQuote now marks its company field required (and drops "(optional)") when the request is a company one; the "Company details" heading no longer appears with no fields under it; an optional billing address on the net-terms or wholesale form no longer counts as started because the country and region were prefilled; linking a WordPress login with a refused store key shows only the reason and Reconnect; the link page names TackQuote's Integrations page; the seller-calendar date hint reads "Dates follow the seller's calendar (Kolkata), where today is …"; on an http site "Allow catalogue and order sync" is disabled with the https reason shown first; "Add to Quote" on block product grids takes the size and padding of the card's Add to cart and wraps a long label like it, never cut short; choosing "A company" on your designed form no longer leaves an empty divider when there are no company fields to show.
* Fixed: on a grouped product, "Add to Quote" and "Request a Quote" quote each product you give a quantity as its own line (name, SKU, sale or wholesale price), as WooCommerce adds them to the cart; before, the whole group became one line at the first quantity. With no quantity chosen the shopper is asked to choose one. A grouped product's products that are on quote or out of stock get a quote quantity where WooCommerce shows "Read more", on classic themes and in the Add to Cart with Options block. A grouped product left in a quote list from an earlier version is removed from the list when it is re-priced, with a note to choose the quantities.
* Fixed: a variation with an "Any" attribute keeps the shopper's choice on its quote line ("Polo - Blue, Regular"), and two choices of it are two lines. Variation lines are named the same way everywhere ("Parent - Large, Blue"), also for variations with three or more attributes.
* Fixed: a quote-only, unpriced, out-of-stock or external product gets a quantity beside the quote buttons (it was always quoted as 1); an external product's buttons sit after its "Buy" button, outside the link to the other store. A product sold individually is quoted as one. The quote buttons respect a product's minimum quantity, not its stock (a quote may ask for more than is in stock).
* Fixed: "Request a quote for your cart" in the Cart block now adds a variation as that variation of its product (with its options in the name, including a value chosen for an "Any" attribute), so it merges with the same variation added from the product page. TackQuote still receives one line per variation.
* Fixed: when TackQuote does not take a quote request because of the store's connection (for example a revoked API key), shoppers see "We could not send your quote request right now" instead of the server's message; administrators see the reason in a notice and on the Connection tab.
* Fixed: a key TackQuote refuses no longer schedules a background check on every page view (the check now waits ten minutes whatever its outcome), and a refused key hides the storefront even if TackQuote never accepted it before, after the same confirming second check, never during a shopper's visit.
* Fixed: the Connection tab no longer shows "Connected" for a key that was never tested or that TackQuote refused; the TackQuote page says when the key was refused and links to app.tackquote.com.
* Quick order: a product option's name reads the same as in the quote list ("Large, Blue"), "Available on quote" appears once, an empty price cell no longer overlaps on phones, and the fields take the theme's input styling on block themes.
* Product grids built with blocks: "Add to Quote" follows the alignment and size of the card's Add to cart button.
* Storefront theme on phones: the quote list button sits above the theme's bottom bar.
* Fixed: amounts on the Net terms My Account tab keep WooCommerce's own price markup (the `<bdi>` that isolates the amount, and the currency symbol's direction), which the tab's filter used to strip.
* **Coding standards.** The plugin now passes the WordPress and WooCommerce coding standards throughout (PHPCS with WordPress-Extra, WordPress-Docs and WooCommerce-Core, PHPStan level 5, and WordPress's JavaScript and CSS standards). Markup is escaped where it is printed, the "Link my TackQuote account" button checks its own permission and nonce, and the storefront scripts use WordPress's own formatting. Nothing changes in what shoppers or administrators see. The readme now lists the WooCommerce → TackQuote calls under External services, and its description reflects catalogue and order sync.
* **Your own quote request form.** When you design the "Request a quote" form in TackQuote (Settings, Quote request form) and your TackQuote server supports it, the quote modal shows that form: your fields, your order and labels in the shopper's language, fields shown only when an earlier answer matches, file fields (when attachments are on) and "No dates in the past" dates on your calendar. The answers travel with the quote request and TackQuote checks them against the form version the shopper saw; if you change the form while a page is open, the shopper is asked to reload. Without a designed form, on an older TackQuote server, or when a required file field cannot be answered because attachments are off, the quote modal is the one it always was.
* **Quick order.** New shortcode `[tackquote_quick_order]` and block "TackQuote quick order": a buyer types SKUs and quantities (or pastes a list, one per line), checks them against your catalogue, then adds them to the cart or to the quote list. Variable products ask for options on their own page; products this visitor cannot see are "Not found". Adding to the cart goes through WooCommerce's own add-to-cart rules, so quote-only products, stock, B2B prices and order limits apply exactly as on the product page. New template `tackquote/quick-order.php`.
* **"Add to Quote" on product grids built with blocks.** With "Product cards" on, the Product Collection block on your home page, a landing page, related products or any other grid now gets the quote button after each card's own button, not only the shop and category pages. A card never gets two.
* **"Request a quote for your cart" inside the Cart block**, below the totals and above Proceed to checkout, instead of a fixed button at the bottom of the page. On a WooCommerce without that slot the fixed button is still used.
* **Net terms at a block checkout follow the cart.** The Checkout block now asks again whether net terms can be offered whenever the cart changes (a coupon, a quantity), from the store itself, so the method appears or disappears with the total. Fixed: the method showed an empty label in the Checkout block when its settings held only "Enable"; it now reads "Pay on net terms" (translated) until you set your own title.
* New storefront text translated in the seven bundled languages (machine-assisted, pending native review).
* **Catalogue and order sync with one approval.** Stores connected through the plugin (Connect with TackQuote or a pasted key) can now let TackQuote sync the catalogue, stock and categories, receive WooCommerce webhooks for Flows and write orders back, with nothing to copy. Press **Allow catalogue and order sync** on the Connection tab (or approve it right after Connect with TackQuote): WooCommerce's own approval screen opens, and when you approve, WooCommerce creates a Read/Write REST API key and sends it to TackQuote directly. TackQuote checks the key with a live read before using it. Declining changes nothing: quotes, prices, forms and orders keep working through the plugin. Needs https and a TackQuote server that offers it.
* **Disconnect stand-down.** When TackQuote revokes this store's key (for example when the store is disconnected in TackQuote), the storefront quote buttons, quote list, quote page, application forms and account tabs are hidden, and every admin screen says so, until a working key is saved. Only TackQuote's explicit refusal of the key, answered to the plugin's own connection check and confirmed by a second check, does this; a timeout, an outage, a proxy error or a shopper's sign-in prompt never hides anything. Store policies you set (quote-only mode, prices, restrictions) are left as they are.
* **Deactivating or deleting the plugin tells TackQuote.** A connection that runs through the plugin only is paused, and activating the plugin again resumes the same connection, so customer links are kept; a connection with catalogue and order sync keeps syncing. Deleting the plugin also deletes the WooCommerce REST API key it approved for TackQuote, but only after TackQuote confirms it no longer uses that key; otherwise the key stays under WooCommerce → Settings → Advanced → REST API for you to revoke.
* **Update available.** The settings page says when a newer version exists and links to Dashboard → Updates. The zip from the TackQuote website now receives WordPress updates from the TackQuote GitHub release (its header carries `Update URI`); the WordPress.org build keeps updating from WordPress.org.
* **TackQuote inside WooCommerce admin.** A new WooCommerce → TackQuote page (for users who can manage WooCommerce) shows your TackQuote workspace right in WordPress. Each person links their own WordPress login once: in TackQuote, Integrations → WooCommerce → "Link a WordPress login" shows a one-time code (10 minutes, works once), which they enter on this page while signed in to WordPress as themselves. TackQuote emails the account about every new link. After that the page signs them in as themselves, with their own TackQuote role (two-step sign-in still applies); nobody is ever signed in as someone else, and a WordPress user who has not linked gets no session at all. The page asks TackQuote server to server with the store's API key and a random per-user secret kept in that user's meta (neither reaches the browser); the one-time sign-in code works once, for two minutes, and travels in the frame address's fragment. "Open in a new tab" is always there for browsers that block sign-in inside a page, and "Unlink my login" removes the link (the secret is kept if TackQuote could not confirm the removal). Use one TackQuote API key per WordPress site. Shown only when your TackQuote server offers it; uninstall removes the per-user secrets.

= 1.11.1 =
Released 2026-10-18.

* **"No dates in the past" follows the seller's calendar.** A wholesale application date field with that rule now starts on the seller's today (the day TackQuote checks against, sent with the form as `calendar`), not this store's UTC day, so a shopper west of the seller can no longer pick a day TackQuote then refuses. A hint under the field says so when the shopper's own date differs ("Dates follow the seller's calendar (Kolkata). Today there is Oct 17, 2026."), translated in the seven bundled languages (machine-assisted, pending native review). An older TackQuote server without `calendar` gets exactly the old field. The seller's today is worked out again when the page is built, so a form definition cached shortly before the seller's midnight does not keep yesterday as the earliest day, and in the browser a wrong device clock can move it no earlier than the day TackQuote sent and no later than the day after. The hint stays hidden under themes that style `.description` as a block.
* Readme: the short description now says the store connects with one button and no key to copy, as it does since 1.11.0.
* Readme: the 1.10.0 notes no longer list accepted quote to store checkout as off by default; it is active whenever the store is connected.

= 1.10.1 =
* No functional change: source comments and packaging cleaned for release; release audit added to the build.

= 1.10.0 =
* **Security and standards audit.** A customer who changes their own email by any route (including the REST API, not only My Account) is no longer trusted with another buyer's prices, net terms or tax exemption until confirmed. Rate limits can no longer be dodged with a forged `X-Real-IP`, and now also cover the application forms and quote checkout links. Draft, private, password-protected and group-hidden products can no longer be quoted. The API key is never re-sent after an HTTP redirect. Uninstall removes every setting, the plugin's user and product meta and queued jobs. Personal-data export and erasure cover the plugin's user meta.
* **Attachments.** Quote requests can carry up to 3 files (PDF, JPEG or PNG, 5 MB each) through an optional "Attach files (optional)" control in the quote form, when you switch on "Allow attachments on quote requests" (Storefront tab, off by default) and your TackQuote server advertises attachments. Wholesale application file fields now accept a file from signed-in customers (guests see "Sign in to your account to attach files"). Files are checked on your store (count, size, extension and content), streamed server to server to TackQuote and never stored in WordPress; the seller sees them on the quote or application.
* **Fixed: "Test connection" no longer calls a rejected key connected.** A 401 or 403 from the ping now reads "Key rejected" (Overview and notice) and clears the cached server features, instead of falling back to the public `GET /health` and saying "Connected". `/health` is asked only when the ping route is missing (404), and then reads "Reachable, key not verified". Timeouts, 429 and 5xx read "Connection failed" with a try-again hint; a failed check is never cached as a capability list.
* **Settings page redesigned into tabs**: Overview, Connection, Storefront, B2B pricing, Buyer groups, Forms and Order sync. The Overview shows the connection (API host and the key's last four characters; "Connected" only after a passing test of the key saved now), the storefront mode, order sync (including a refusal TackQuote answered and the queue length), every B2B switch and a first-run checklist. Each tab is its own form with its own option group, so saving one tab never changes another. Short help under every field, with the full explanation kept under "Learn more"; switches, conditional fields, buyer-group grids with one column per group code, a sticky save bar, and a confirmation before removing the API key. Option names, defaults and storefront behaviour are unchanged. "Tax-exempt buyers" moved to the B2B pricing tab. The TackQuote mark replaces the generic dollar icon in the admin menu (a single-colour SVG that follows your admin colour scheme) and heads the settings page; nothing is added to your storefront.
* **Hide product categories per buyer group** (off by default; TackQuote → Buyer groups). A grid of your product categories against your group codes plus "Guests and customers in no group"; sub-categories follow their parent. Hidden products leave the shop, category, tag and search loops and product blocks (`pre_get_posts`, `tax_query` NOT IN, front end and Store API only), related products, up-sells and cross-sells (`woocommerce_product_is_visible`, `woocommerce_related_products`); their own URL answers 404 (`template_redirect`); they cannot be bought (`woocommerce_is_purchasable`, `woocommerce_variation_is_purchasable`); a line already in a cart is removed with a notice. Store managers, wp-admin and the `/wc/v3` REST API are not affected. If TackQuote cannot be reached the catalogue is shown, unless you tick "Also hide them when the buyer group is unknown".
* **Free or discounted shipping per buyer group** (off by default): free on chosen methods, a percentage off chosen methods, or only the methods that already cost nothing. Applied in the same `woocommerce_package_rates` callback as the group restrictions, after them; shipping tax is scaled with the cost. Only buyers TackQuote places in the group get it. These are plugin settings; shipping rules sent by TackQuote come later.
* The buyer group is now part of each shipping package, so WooCommerce recalculates cached rates when a buyer's group changes (this also applies to the 1.8 shipping restrictions).
* **Optional WordPress role per buyer group** (off by default): a signed-in buyer in group `TIER2` gets the extra role `tackquote_tier2` (capability `read` only), removed when the group changes. Roles the plugin did not add are never removed, an outage changes nothing, and roles never affect prices. Uninstall removes the roles the plugin created.

* **Wholesale application form on your store.** New shortcode `[tackquote_wholesale_application slug="…"]` (slug defaults to the one under TackQuote → Storefront forms) renders the form you design in TackQuote under Settings → Wholesale forms, with every field kind (text, email, phone, number, select, multi-select, checkbox, textarea, date, address, tax ID; conditional fields shown and hidden as the server decides). File fields show a notice: attachments arrive in a later release. Submissions are nonce-protected and sent server to server; a signed-in customer's details are prefilled and their WordPress user ID travels as `wooCustomerId` so approval links the account. Shoppers see a friendly success, pending or error message, never a raw server answer.
* **My Account tabs "Wholesale account" and "Net terms"** (both off by default; TackQuote → Storefront forms). Registered with `add_rewrite_endpoint` through WooCommerce's `woocommerce_get_query_vars` filter and `woocommerce_account_menu_items`; rewrite rules are flushed on activation, deactivation and once after an update. The net-terms form (also `[tackquote_net_terms_application]`) is for signed-in customers only, uses the account's own email, and accepts up to three trade references.
* **Tax-exempt buyers** (off by default). When TackQuote marks a signed-in customer's buyer group tax exempt, `WC()->customer->set_is_vat_exempt( true )` is applied on `woocommerce_before_calculate_totals`, once per request and never saved to the customer record. Missing, false or unreachable means tax is charged.
* **Product-page prices, quantity breaks, order limits and the buyer group now read TackQuote's shared storefront API** (`/storefront/v1/*`), showing the currency each price is in and marking a price that is the customer's own ("Your account price"). Cart prices still use `/storefront-pricing/resolve`. On a TackQuote server without these routes the plugin falls back to the previous ones.
* **Fixed: order limits were never applied.** The plugin read `minQuantity`/`maxQuantity`, which TackQuote has never sent (it sends `min`/`max`), so no minimum or maximum was shown or enforced. Only quantity rules are now read as quantities; an order-total rule's money amount is no longer mistaken for one.
* **Fixed: stores that enter prices including tax were undercharged tax on TackQuote prices.** TackQuote prices are net, but B2B prices were handed to WooCommerce as if they already included tax, so 10.00 net at 20 % was charged 10.00 in total instead of 12.00. B2B cart prices, product-page and listing prices, volume tables, the quote list and quote checkout now add tax at the rates WooCommerce takes back out (base rates, or the customer's when `woocommerce_adjust_non_base_location_prices` is off), so the cart's price excluding tax is exactly the TackQuote price; a VAT-exempt buyer pays exactly that, and non-taxable products are unchanged. Displayed TackQuote prices now follow your "Display prices in the shop" setting.
* Every request now carries an `X-TackQuote-Plugin-Version` header, and buyer lookups send the WordPress user ID beside the email (`buyerExternalId`) so TackQuote can link the account.
* **Fixed:** a customer whose email was self-changed and not re-confirmed is now priced as a guest in listings and the cart (B2B pricing read the email without that check), and cannot apply for net terms or receive a tax exemption in another buyer's name.
* **Add to Quote on product cards** (`woocommerce_after_shop_loop_item`, priority 11), off by default. Simple products go straight to the quote list; variable, grouped and external products link to their page.
* **Request a quote for your cart** on the cart page, off by default: under Proceed to checkout on the classic cart (`woocommerce_proceed_to_checkout`), and as a fixed button on the Cart block (printed on `wp_footer` on the cart page when the cart has lines). It re-reads the live cart from WooCommerce's Store API before copying it into the quote list.
* **Floating launcher settings**, mirroring the Shopify launcher: position, side and bottom offsets, show on (all pages, product pages, cart page, nowhere), label, icon only, item count, size, hide on mobile. Compact on phones. Defaults reproduce the 1.8 launcher.
* **Quote page** shortcode `[tackquote_quote_page]` sharing the drawer's list: edit quantities, an optional target price per line (sent as the line's target price, shown to the seller as "Buyer asked for"; in the request note on older TackQuote servers) and a message. New setting "Quote button opens: drawer or page", drawer by default.
* **Signed-in buyers see their price at the line quantity** in the quote list and on the quote page, re-priced (debounced) through TackQuote pricing when the merchant uses it.
* **Per-product Quote only** checkbox (`woocommerce_product_options_general_product_data`, saved on `woocommerce_admin_process_product_object`, meta `_tackquote_quote_only`; variations inherit), enforced in `woocommerce_is_purchasable` and in the Store API through `woocommerce_store_api_validate_add_to_cart`. Off by default.
* **Quote-only scope "approved wholesale accounts"** through `GET /storefront/v1/price-access` (sends the customer's email and WordPress user ID; fails closed). Off by default.
* **Order limits now also stop the Cart and Checkout blocks** through `woocommerce_store_api_cart_errors`, beside `woocommerce_check_cart_items`, with the same message and the error code `tackquote_order_limit`.
* **Net terms at checkout** (off by default). A new offline payment method "Net terms (TackQuote)", gateway id `tackquote_net_terms`, registered on `woocommerce_payment_gateways` and, for the Checkout block, as an `AbstractPaymentMethodType` on `woocommerce_blocks_payment_method_type_registration`. It is offered only to a signed-in customer with a confirmed email whose TackQuote credit line (`GET /storefront/v1/net-terms`) is active, in the checkout currency, whose remaining available credit covers the order (the limit when TackQuote omits it). It FAILS CLOSED: any error, timeout, 404 or unknown answer hides it. Placing the order reads the standing again and refuses if it no longer qualifies; otherwise the order goes on hold with the note "Awaiting payment on net terms (N days)" and order meta `_tackquote_net_terms`. It is never marked paid. The Checkout block never receives the limit. Order sync sends `payment.method = tackquote_net_terms` so TackQuote can invoice it.
* **Optional purchase-order number at checkout** (off by default; a setting of the net-terms payment method). Checkout block: `woocommerce_register_additional_checkout_field` (`tackquote/po-number`, order section, at most 64 characters). Classic checkout: a field under the order notes. Saved as order meta `_tackquote_po_number` and sent as `poNumber` through the existing `tack_quotes_order_po_number` filter (a value your own filter supplies still wins).
* **Translations bundled** for de_DE, es_ES, fr_FR, it_IT, ja, nl_NL and pt_BR (`languages/`, generated from TackQuote's shared storefront catalogue by `bin/build-translations.php`; Italian pending native review). Header `Domain Path: /languages`; the textdomain is registered on `init`. The quote form and quote list script now reads its text through `wp.i18n` (`wp-i18n` dependency, `wp_set_script_translations`) instead of a localised array. A translate.wordpress.org language pack still takes precedence. The "Add to Quote", "Request a Quote" and "Checkout as Quote" labels left at their default now follow the visitor's language: activation no longer stores them in English, and a stored English default or a blank field means the translated default.
* **Accepted quote to store checkout.** A buyer who accepts a quote in TackQuote's portal and chooses to check out in your store arrives at `?tackquote_checkout=<token>`. The plugin exchanges the single-use token server to server (`GET /integrations/woocommerce/quote-checkout/<token>`, once, never retried), refuses a quote in another currency, empties the cart and adds every quoted line at its quantity and quoted unit price (all or nothing: one unavailable product refuses the whole quote, by name), then redirects to checkout without the token. Quote lines keep the quoted price over B2B pricing, their quantities are locked (classic cart and Store API), other products cannot be added beside them, and products sold on quote only can be bought through their accepted quote. The order stores `_tackquote_quote_ref` and the quote's PO (unless the buyer typed one); with order sync on, the order is sent with `tackQuoteRef` so TackQuote links it to the quote. Active only while an API key is saved.
* The plugin's buttons use WooCommerce's own button classes (`button`, plus `wp-element-button` on a block theme). **Fixed on block themes:** the product-page buttons and order-limit notice render with the Add to Cart block (`render_block_{name}`), once per product, also for quote-only products. Quote-only variations no longer disable them. They follow the Add to Cart with Options form, quoting its chosen variation and quantity.
* **Blends with your theme, and you can restyle it without editing the plugin.** Storefront CSS no longer carries a palette of its own: the blue quote buttons and the dark launcher are gone, and every control (product, card and cart buttons, the launcher, drawer checkout, quote page and request form) is the theme's button; the final action is WooCommerce's `alt` button. The drawer, request form and fixed cart bar take the theme's colours (theme.json `base`/`contrast` presets, neutral fallback on classic themes) and fonts; the form fields wear WooCommerce's `form-row`/`input-text`, the quote page is wrapped in `woocommerce` like WooCommerce's own shortcodes and uses `shop_table shop_table_responsive`, and the volume table is a `shop_table`. Two stylesheets: `tackquote-layout` (always) and `tackquote` (appearance). New **Styling** settings on the Storefront tab: "Use the theme's styles only" and an optional accent colour (WordPress colour picker; `sanitize_hex_color`, inlined only when set). Documented `--tackquote-*` CSS variables, overridable templates in `templates/tackquote/` located through `wc_get_template()` (theme copies in `yourtheme/woocommerce/tackquote/`), and new filters `tackquote_button_label`, `tackquote_button_classes`, `tackquote_theme_styles_only`, `tackquote_quote_page_args`, `tackquote_quote_list_drawer_args`, `tackquote_quantity_breaks_args` and `tackquote_storefront_form_args`. Accessibility: the launcher toggles the drawer and reports `aria-expanded`, Escape closes the drawer, plugin-only controls show a focus ring, the close and remove icons use the text colour (the old light grey failed contrast), the opening animation respects `prefers-reduced-motion`, and the layout uses logical CSS properties so right-to-left stores are mirrored.
* The quote form's email is prefilled for signed-in customers only. A guest's checkout email is no longer printed into the page, where a full-page cache could have served it to other visitors.
* **Fixed: variable products on quote showed no size or colour choice on classic themes** in store-wide quote-only mode. The variation form now renders on quote for classic and block themes, with the quantity and quote buttons in place of the cart button; a quote-only variation still cannot be added to the cart.
* **Fixed on block themes:** the volume-pricing table renders after the Add to Cart block, once per product, instead of above the excerpt.
* The readme Description is now a short overview (wordpress.org trims a Description over 2,500 words, External services and Privacy included); the detailed field lists moved, unchanged, into the FAQ, and changelog entries for 1.5.1 and earlier into `changelog.txt`.
* Net terms, tax exemption and credit standing now go only to customers the seller has linked in TackQuote (a TackQuote server rule; prices still follow the account email). See the FAQ "Which features need a linked account?".
* Tools → Export Personal Data now includes, per order, the purchase-order number, the TackQuote quote reference and number, and the net terms (through WooCommerce's order exporter). Erase Personal Data deletes the purchase-order number when WooCommerce's "Remove personal data from orders on request" is on.
* When a wholesale or net-terms application is refused, the answers put back in the form are kept for 2 minutes (was 5) and no longer include phone numbers or tax, VAT or registration numbers; the customer types those again.
* **Requires WordPress 6.4 and WooCommerce 8.0 or later** (was 6.0 and 6.0). Older releases are untested and no longer receive security fixes. The plugin still checks that a newer WooCommerce feature exists before it uses it.
* **Fixed: the quote form's success message was invisible.** It sat inside the form that is hidden on success, so a guest saw an empty dialog and could send the request twice. It now shows below the hidden form, takes focus and is announced to screen readers.
* **Fixed: a variation in the quote list showed the parent's SKU and lowest price.** "Add to Quote" on size M listed the parent SKU at the cheapest variation's price; the drawer and quote page now show the chosen variation's own SKU and unit price (excluding tax, like every other line).
* **Fixed: target prices now reach the seller as "Buyer asked for"** on each quote line (`lineItems[].targetPrice`) instead of only inside the request note. Sent only to TackQuote servers that take the field (those that advertise attachments); older servers still get them in the note.
* The quote form now also prefills a signed-in customer's first and last name (billing name, else profile name), as it already did the email. Guests get nothing prefilled.
* More of the quote form, drawer and launcher is translated: Email address, Note, Company name, Send request, Sending…, Legal name, Address, State / Province, the drawer title and the launcher label. Strings with no identical entry in TackQuote's shared catalogue (for example First name, Last name, Cancel and the order-limit messages) stay English until translate.wordpress.org has them.
* **When the wholesale form slug matches no form**, shoppers now read "This form isn't available right now" instead of "try again later", administrators see which slug failed with a link to the Forms tab, and the log gets one line a day per slug. A cached failure keeps its HTTP status, so this holds for the minute it is cached.
* Every request to TackQuote now also carries `X-TackQuote-Site-Url` with your store's address (`home_url()`), beside `X-TackQuote-Plugin-Version`, so TackQuote can tell which store a key is used from.

= 1.8.2 =
* **Repeated "slow down" answers back off further each time.** The first HTTP 429 from TackQuote holds order sync for the time TackQuote names (or one minute); if it happens again before any order got through, the wait doubles each time, with a random spread so held orders do not all reappear in the same second, up to one hour. The wait and the attempt count are stored as a site option, so every PHP worker and every scheduled run honours the same pause. A successful push resets it.
* **An order held by a 429 is sent again on its own.** It is put back into Action Scheduler for the moment the pause ends, instead of waiting for another order to change. Previously the last order of a quiet day could sit unsent until something else happened in the store.
* Together with the refusal handling below, a refused or throttled key now costs at most a handful of requests per hour.
* **Order sync stops when TackQuote refuses the API key, and tells you why.** If the key saved in TackQuote settings lacks the `orders:write` scope, has been revoked, or your TackQuote subscription is inactive, TackQuote refuses every order. The plugin used to try again on every order change, which on a busy store meant a refused request every few seconds, and the reason only appeared in WooCommerce > Status > Logs. It now stops sending, shows an error notice in wp-admin that names the missing scope or the billing step, and checks again at most once an hour. Saving a different key resumes at once.
* When order sync resumes, orders changed while it was refused are sent automatically (up to 200 of the oldest at a time; any beyond that are sent when they next change).
* When TackQuote asks the plugin to slow down (HTTP 429), the plugin waits for the time TackQuote names, up to one hour. A firewall or challenge page in front of TackQuote is treated as temporary and does not stop sync.

= 1.8.1 =
* **The "awaiting approval" message no longer says more than TackQuote knows.** TackQuote now answers "awaiting approval" for every quote request made on behalf of a company, so that a shopper typing a company name can no longer learn whether that company is already a customer of the store. The message used to read "Your company registration is awaiting approval by the seller", which is now false for a company that needs no approval. It reads "Request received. If your company account needs approval, we'll email you when it is ready."
* After a company request the buyer portal is offered as a link instead of not at all. The automatic redirect still happens only for an individual request; a company shopper may not be able to sign in yet, so they are never dropped onto a login they cannot pass.

= 1.8.0 =
* **The settings screen is grouped into a setup sequence.** Everything used to sit under a handful of headings in no particular order, with seven unrelated controls filed under "B2B pricing" — four of them about prices and three about hiding checkout methods. The page now reads top to bottom as the order you actually set the plugin up in: connect, choose how customers buy, put the buttons on the storefront, turn on order sync, price your trade customers, then restrict checkout methods. A section that cannot work yet says so where you are reading it, rather than saving happily and doing nothing.
* **Payment and shipping restrictions are no longer typed by hand.** Setting these up meant writing a small language into two text boxes — `cod: TIER2, TIER3`, one rule per line — which required knowing the WooCommerce GATEWAY ID (the Payments screen shows "Cash on delivery", not `cod`) and the TackQuote group code, from a different system. A wrong id did nothing; a wrong group code silently hid that payment method from every customer, permanently, with no error anywhere. You now get a row per real gateway and per real shipping method, showing the name you know it by with the id underneath, and you tick the buyer groups allowed to use it.
* **You type a group code once, not once per rule.** The new "Your buyer group codes" field is the single place codes are entered, and every rule then becomes a checkbox. Codes already used by rules you saved earlier are picked up automatically, so nothing needs retyping after this update — and no saved code can go missing from the list, which is what would have made it possible to lose one.
* **Nothing you already configured changes.** Rules are still stored in exactly the same format and read by exactly the same parser; the grid converts back to it on save. Rules for a gateway or shipping method your store does not currently offer — a payment plugin you deactivated, a full rate id you wrote by hand — are kept untouched and listed under the grid, instead of being quietly dropped because the screen could not draw a row for them. Your own `#` comments survive too.
* If the store reports no gateways or shipping methods at all, the field falls back to the text box it used to be, pre-filled with your rules. An empty grid would have posted an empty rule set and deleted the lot on the next save, so it deliberately never renders one.
* Fixed: uninstalling the plugin left thirteen options behind in the database — everything added in 1.6.0 and later, including the B2B pricing switches and the restriction rules. The readme said they were removed. They now are.

= 1.7.1 =
* **Security.** A customer could inherit another buyer's pricing group — and through it their payment terms — by changing their own email address. WooCommerce lets a customer change it on My Account with no verification: the current password is required only when the PASSWORD changes. Its one protection, `email_exists()`, refuses only an address already held by another WordPress user — so a TackQuote buyer approved for Net-30 who never registered on the store was takeable. A self-changed address is now untrusted until re-confirmed; the account still works normally, it is simply treated as anonymous by TackQuote. Stores that verify email another way can opt back in with `tackquote_trust_unverified_email`.
* **Security.** "TackQuote could not be reached" and "TackQuote says this buyer is in no group" were the same answer internally, and both fell through to the permissive default — so a buyer definitively placed in NO group was handed every group-restricted payment method. They are now distinguished: a real answer refuses a restricted method, and only a genuine outage leaves it visible.
* Group codes are matched case-insensitively.
* The payment/shipping rule fields now list the store's real gateway ids, since WooCommerce → Settings → Payments shows titles rather than the ids the field needs.

= 1.7.0 =
* New: **order limits**. TackQuote already held minimum/maximum order quantities and enforced them when a quote was converted, but a WooCommerce shopper never saw them — they filled a cart, reached checkout, and the order was refused. The limit is now shown on the product page AND enforced on the cart and at checkout.
* New: **buyer group badge**. Shows a signed-in customer which pricing group they are on. Without it a discounted price appears with no explanation, which reads as a pricing error rather than the negotiated rate it is.
* A TackQuote outage never blocks checkout. Nothing is refused unless TackQuote actually answered with a limit — a checkout that stops working because a supplier's API is slow costs the day's revenue, while an unenforced minimum costs a phone call.
* Quantities are summed across cart lines of the same product before checking, so 10 + 20 satisfies a minimum of 25.
* Both switches are off by default, under **WooCommerce → TackQuote → B2B pricing**.

= 1.6.0 =
* New: **B2B pricing**. A signed-in trade customer can now be priced from their TackQuote price book, buyer group and quantity breaks — the same pricing authority that prices a quote. Until now the plugin could send a quote request and receive Tack-resolved prices back on the quote, but a customer browsing the shop still saw the retail price, because nothing let the storefront ASK what Tack would charge before a quote existed.
* New: an optional **Volume pricing** table on product pages, showing only the tiers that actually change the price for that customer. It renders nothing when there is one tier, rather than showing an empty table under a heading.
* Quantity breaks apply to the CART, at the quantity actually ordered — not just to the table. The price is set on `woocommerce_before_calculate_totals`, which is the only hook that knows how many of each line the buyer wants; a filter on `woocommerce_product_get_price` has no quantity at all, so it could only ever resolve at one unit and would have advertised a volume discount the checkout did not honour.
* Both are **off by default** and must be switched on under **WooCommerce → TackQuote → B2B pricing**. B2B pricing changes the price used at CHECKOUT, not just the price displayed, so it is opted into rather than inherited from a plugin update.
* Correct on tax-inclusive stores. TackQuote returns a NET unit price, while `set_price()` means "the price in the basis this store is configured for". On a store set to enter prices inclusive of tax, the net figure is grossed up using the product's own tax class first — without that, WooCommerce would extract the tax back out of it and the seller would absorb the VAT on every wholesale line (a £100 net line at 20% would charge £100 instead of £120).
* Safety: if TackQuote cannot be reached, returns no price for a SKU, or your plan does not include B2B pricing, your store keeps its own prices. No product is ever left unpriced or silently zeroed — a resolved price of `0` is honoured as a real price, while "no answer" is not.
* Anonymous shoppers are never priced and no request is made for them, so a page cache holding a logged-out render can never contain one customer's negotiated price.
* One batched request per page rather than one per product: a category page resolves up to 50 SKUs in a single call.

Older releases (1.5.1 and earlier) are listed in `changelog.txt` inside the plugin and in the GitHub repository.

= 1.5.1 =
* Shortened the description and added links to the WooCommerce integration page and to account signup, so it is clear where to get an API key.
* No code changes.

= 1.5.0 =
* Order sync now sends the whole order, not eleven fields of it. Previously the payload carried no address of any kind. It now carries both addresses in full, phone numbers, the WooCommerce customer ID and order note, the real item subtotal alongside discount/shipping/tax/total, coupon codes, payment method and gateway transaction reference, the created/modified/paid/completed timestamps, shipping and fee lines, and per-line product/variation IDs, line subtotal, tax and item meta (so "Large / Blue" survives the sync).
* Fixed: `subtotal` was never sent, so the receiving end recorded `subtotal = total` — meaning every order from a store that charges tax or shipping claimed its goods cost what the customer paid.
* New: `tack_quotes_order_po_number` filter. WooCommerce core has no purchase-order field, so nothing is sent unless your store wires one up; see the FAQ. Previously a purchase-order number could never be reported at all.
* Fixed: `modifiedAt` is sent but deliberately excluded from the idempotency hash. Under HPOS, recording a successful push writes order meta, and that stamps a new modified date — so hashing it would have invalidated the key the push just recorded and de-duplication would never have converged.
* The privacy disclosure on the settings screen, the wording offered to **Settings → Privacy**, and the **External services** and **Privacy** sections of this readme now describe the payload that is actually sent. An under-disclosure is worse than none: a merchant reads it and concludes the transfer is narrower than it is.
* Fixed: the settings screen named the WooCommerce log source as `tack-quotes`; it has been `tackquote` since the 1.3.3 slug rename.
* Added `tests/test-order-payload.php`, a WP-CLI contract test that names every required payload field against a real WooCommerce order, and offline payload coverage in `tests/run.php`.
* Tests: the quote-only mode suite now asserts that `woocommerce_is_purchasable` is actually hooked, not just that the callback decides correctly. It previously proved only the latter, so a wiring mistake would have hidden the buttons while the Store API kept taking orders. No behaviour change — the wiring was already correct.

= 1.4.0 =
* New: **Store mode**. A single setting turns the whole storefront into a B2B catalogue — "Add to cart" is withdrawn and customers request a quote instead. Choose whether it applies to every customer, to signed-out visitors only (so approved trade customers keep a normal cart), or to specific roles. Optionally replace prices with "Price on request".
* The switch is enforced server-side via `woocommerce_is_purchasable`, which WooCommerce checks before accepting any cart line — so a hand-crafted `?add-to-cart=` link, the Store API and cached pages are all refused, not just the button hidden.
* Carts filled *before* the store was switched to quote-only are emptied on the cart and checkout pages with an explanatory notice. WooCommerce's own cart validation only checks that a product still exists, not that it is purchasable, so without this a pre-existing cart could still be checked out and the store would not really be quote-only.
* Anyone who can manage WooCommerce keeps a working cart, so you can test your own store while it is closed to customers.
* Quote buttons now also mount outside the add-to-cart form, so they survive when the cart button is withdrawn.

= 1.3.4 =
* Fixed: the **Settings** link on the Plugins screen led to "Sorry, you are not allowed to access this page." even for an administrator. The link carried a hardcoded `page=tack-quotes`, which was the admin page slug up to 1.3.1; the 1.3.2 and 1.3.3 renames moved the slug to `tackquote` and left the link behind. Pointing at an unregistered page makes WordPress emit its permission-denied message, so the failure looked like a capability problem and was not one. The link is now derived from the same constant the menu is registered with, so the two cannot drift again.
* Added a regression test (`tests/run.php`, no PHPUnit or WordPress install required) asserting the Settings link resolves to a registered admin page that requires `manage_options`.

= 1.3.3 =
* The plugin slug, text domain, plugin folder and distributed ZIP are now all `tackquote`, matching the slug assigned on WordPress.org. WordPress requires the text domain to equal the slug, and a plugin folder that disagrees with either is its own defect. The admin page, the enqueued script/style handles, the Action Scheduler group and the WooCommerce log source move with it, so the log source is now `tackquote`.
* readme.txt now carries an **External services** section disclosing the TackQuote API: that the plugin cannot function without it, that nothing is sent until an API key is entered, and, endpoint by endpoint, what is sent and when — with links to the Terms of Service and Privacy Policy.
* Fixed the download links, which pointed at a repository that does not exist. The source now lives at https://github.com/tackquote/woocommerce.
* Note for anyone updating a manually installed 1.3.2: the folder changed from `tackquote-for-woocommerce/` to `tackquote/`, so WordPress treats the new ZIP as a separate plugin. Deactivate and delete the old copy after installing this one. Your API key and toggles are stored as WordPress options and survive both.

= 1.3.2 =
* Packaging: the distributed ZIP now unpacks to `tackquote-for-woocommerce/`, matching the plugin slug, and is rebuilt from the current source. The previously published download still contained pre-1.2.0 code, so stores installing it got the old buttons and none of the 1.3.1 security fixes.
* No functional changes to the plugin itself beyond the version bump.

= 1.3.1 =
* Security: company field names supplied by the API are now escaped and allowlisted before being rendered into the quote form, closing a cross-site scripting hole.
* Security: the TackQuote API key is no longer rendered into the settings page HTML. Leave the field blank to keep the saved key; a new "Remove saved API key" button clears it.
* Security: the settings page now requires the administrator capability, which is also the capability WordPress requires to save it — a shop manager previously saw the page but could not save it.
* Quote requests from the storefront are rate limited per visitor, the note field is length-capped, and the request timeout is 5s instead of 20s.
* Order sync is now queued and sent on a background request through Action Scheduler, so it no longer runs inside checkout, and each push carries an idempotency key so the same order state is never sent twice.
* Order sync now defaults to OFF on new installs, and readme.txt documents exactly which fields are sent where. Existing stores keep their current setting.
* An expired or cache-stale security token now says so and offers a reload, instead of showing a generic error that could never be resolved.
* Quantity and variation are read from the clicked product's own form, fixing wrong values on grouped products, product archives, related-product rows and sticky add-to-cart bars.
* Declares compatibility with the Cart and Checkout blocks, and adds the `WC tested up to` header that WooCommerce needs before it will surface any compatibility declaration.
* Uninstall now removes every option and transient the plugin creates.

= 1.3.0 =
* "Add to Quote" no longer adds the product to the WooCommerce cart. It now adds to a separate, browser-side "quote list" that never touches stock, cart totals, or checkout. A floating "Quote list" button (bottom-right, site-wide) appears once at least one product is added, showing what's in it and a "Checkout as Quote" button that submits the whole list as one TackQuote request. The WooCommerce cart page no longer has a quote button — quoting and purchasing are now fully separate paths.

= 1.2.0 =
* Restored "Request a Quote" as an independent, configurable product-page button alongside "Add to Quote" — Settings → TackQuote now has checkboxes to show "Add to Quote", "Request a Quote", both, or neither on product pages (the cart page's "Checkout as Quote" is unaffected). Both default to on for existing and new installs.

= 1.1.0 =
* Split the single "Request a Quote" button into two: "Add to Quote" on product pages (adds the product to the cart, same as Add to Cart) and "Checkout as Quote" on the cart page (submits everything in the cart as one quote request). Previously the product-page button submitted a quote for that one product immediately, with no way to accumulate multiple products into a single request without using the whole-cart button on every add.

= 1.0.1 =
* Replaced the browser `prompt()`/`alert()` quote-request flow with a real modal dialog (email + optional note fields, inline validation, loading/success/error states). Email is pre-filled for logged-in customers.

= 1.0.0 =
* Initial release: Request a Quote button, one-way order sync, TackQuote settings, HPOS declaration.
