TackQuote for WooCommerce: full changelog for release 1.10.0 and earlier, newest first.
The latest releases are listed in readme.txt (1.10.0 there in summary).

= 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.
