=== Cartnorm B2B – EU VAT Number Validation (VIES) & Wholesale Prices for WooCommerce ===
Contributors: cartnorm
Tags: eu vat, vat number, vies, wholesale, b2b
Requires at least: 6.4
Tested up to: 7.1
Requires PHP: 7.4
Stable tag: 0.3.2
License: GPLv2 or later
License URI: https://www.gnu.org/licenses/gpl-2.0.html

Validate EU VAT numbers in VIES, apply 0% VAT (reverse charge) correctly, and add wholesale prices, customer groups, order minimums and quotes.

== Description ==

Cartnorm B2B lets one WooCommerce shop sell to consumers and to businesses. Consumers keep seeing your normal prices. Business customers in a group see their own prices, net or gross, can be held to minimum order values and pack sizes, can request quotes, and can apply for a wholesale account themselves. EU businesses from other member states with a valid VAT ID buy without VAT, with the right legal note on the order.

= Customer groups =

* **Retail** is built in: guests and every customer who is not in a group see exactly your normal WooCommerce prices.
* Create **as many B2B groups as you need** (for example "Wholesale", "Dealer" and "Distributor"), each with a name, prices shown including or excluding VAT, and an optional discount on all products.
* Only shop staff can put a customer into a group (Users › Edit user). Customers can never choose their own group, not at registration, not at checkout, not in My Account.

= Wholesale prices with a clear order of precedence =

For each group, the price of a product is found in this order:

1. A **fixed price** for the group on the product (simple products and every single variation).
2. A **category discount** of the group (subcategories included; the largest discount wins if a product is in several categories; 0% excludes a category).
3. The group's **discount on all products**.
4. The normal price.

A group price is never higher than the current normal or sale price.

= Quantity prices =

Add quantity prices per product and group, for example from 10 pieces €7, from 50 pieces €6. They are applied in the cart and shown as a small table on the product page.

Works with simple and variable products, in the shop, on the product page, in the mini cart, in the classic cart and checkout and in the **cart and checkout blocks**. Your stored product prices are never changed.

= Order rules: minimum order value, minimum quantity, pack sizes =

* **Minimum order value** per group, for example "Wholesale customers order at least €100". It is compared with the cart subtotal before coupons, excluding or including VAT the way the group sees prices.
* **Minimum number of items** per group.
* **Per product and variation**: minimum and maximum quantity and **pack size** ("sold in cases of 24"), separately for each group. Variations use the product's rules unless they have their own.
* Quantity fields on the product page and in the cart only offer valid quantities, in the classic cart and in the **cart block** (via WooCommerce's Store API quantity limits). Shop buttons add a full pack.
* Friendly, translatable messages such as "Minimum order for Wholesale customers is €100.00 excl. VAT — add €23.50 more" or "“Mug” is sold in packs of 24" in the classic cart, the mini cart and the cart block. Checkout is blocked on the server until the rules are met, in the classic checkout and in the **checkout block**.
* Retail customers and guests are not affected unless you set rules for Retail.

= Request a quote =

* Per group, and for Retail customers and guests: an **"Add to quote" button** next to "Add to cart", or **instead of** it.
* Optionally **hide prices** from a group or from guests ("Log in to see prices", "Price on request"). Hidden prices are also removed from the Store API product data used by product blocks (`/wc/store/v1/products`, including the price filter range) and from the product's structured data (JSON-LD), so they cannot be read from the page source. Customers who cannot see prices cannot buy, also not through the Store API.
* A quote request page (created automatically, works in block themes; shortcode `[cartnorm_b2b_quote]`) where customers change quantities and send their details: name, company, email, country, VAT ID (prefilled) and a message. Spam protection with a honeypot field and a rate limit.
* Quotes are listed in **WooCommerce › Quotes** with the statuses New, Answered, Accepted, Expired and Converted. You enter a price per line (the customer's current shop price is shown as reference), a validity date and a note, and send the answer with one click.
* The customer accepts from a **signed link that expires** with the quote. The plugin then creates a WooCommerce order with your prices, "pending payment", with the same VIES and 0% VAT rules as the checkout, and sends the customer to the "pay for order" page.
* Emails (WooCommerce › Settings › Emails, translatable, using your WooCommerce email template): new quote request (to you), quote answered and quote expired (to the customer). Answered quotes expire automatically after the validity date.

= Wholesale registration =

* A registration page (created automatically; shortcode `[cartnorm_b2b_registration]`) with company name, contact name, email, password (or a "set your password" email link), country, VAT ID, phone and message, plus a privacy consent checkbox.
* The VAT ID is checked in VIES. If VIES is unavailable, the application waits for your review.
* **Manual approval** (default) or **automatic approval** when VIES confirms the VAT ID. Applicants stay Retail customers until they are approved; a group is never taken from the form.
* Users › **Pending wholesale** lists the applications, with approve and reject links. You and the customer are informed by email.
* Existing customers can apply from My Account.
* Spam protection: nonce, honeypot field and a rate limit per IP address.

= Store modes: Hybrid or B2B only =

* **Hybrid** (default): consumers and guests see your normal WooCommerce prices; B2B groups see their own prices, net if the group is set to "excluding VAT".
* **B2B only**: the shop sells only to approved business customers (customers in a B2B group). Guests and customers without a group see no prices (also not in product blocks, the Store API and the structured data), cannot buy and see a notice with links to log in or apply for a business account.
* Optionally the **whole catalog** is restricted to approved customers: other visitors are sent to the login page or the wholesale registration page.

Showing only net prices is allowed only if consumers are effectively excluded, for example because every account is checked (VAT ID, company details) before it is approved. A checkbox "I am a business customer" is not enough. The settings screen explains this.

= Safe with page caches =

A consumer must never see a cached page with net prices that was made for a wholesale customer. Pages for logged-in B2B customers (and every page in "B2B only" mode) are sent with `Cache-Control: private, no-store` and `DONOTCACHEPAGE`. Group members get a signed cookie (`cartnorm_b2b_group`) so caches that cache logged-in users can keep the groups apart; prices are never taken from that cookie.

Built-in support for **WP Rocket**, **LiteSpeed Cache**, **W3 Total Cache**, **WP Super Cache** and **FlyingPress**. The settings show which cache plugin was found and what Cartnorm B2B did, and a **self-test** loads your shop like an anonymous visitor and checks that no B2B prices leak.

= Unit prices =

* With **Germanized**, the unit price (price per kg, l …) of group members is recalculated from their group price, and Germanized's "incl. / excl. VAT" notice follows the group's net or gross display.
* Without Germanized or German Market you can turn on a **simple unit price**: add unit, reference amount and content to a product, and a line such as "€7.00 / 1 kg" is shown under the price, calculated from the price each customer sees.

= Orders created in wp-admin =

When you create or edit an order in WooCommerce › Orders for a customer in a group, added items get the customer's group price (with quantity prices). "Recalculate" re-prices lines that still have the normal price, for example when you added the items before choosing the customer. Prices you changed by hand are kept.

= Net or gross prices per group =

Groups set to "excluding VAT" see shop and cart prices without VAT and with a clear "excl. VAT" suffix. Only the display changes; VAT is calculated as usual. Retail always uses your store settings.

= EU VAT ID check with correct 0% VAT =

* VAT ID field in the classic checkout, the block checkout, My Account › Account details and the user profile.
* Format check for all 27 EU member states and Northern Ireland (XI), then a live check in **VIES**, the official EU database.
* Your own VAT ID is sent as the requester, so VIES returns a **consultation number** – your proof that you checked.
* **0% VAT only when it is correct**: the VAT ID is valid in VIES and the customer is in an EU country other than your shop's country. Customers from your own country always pay VAT.
* **VIES down? Checkout keeps working.** VAT is charged, the order is accepted and marked "pending verification", and the number is checked again automatically in the background.
* Invalid numbers get a clear message (or, if you prefer, the order is accepted with VAT).
* Results are cached (7 days for valid numbers) so checkout stays fast.

= The right legal wording on every order =

* Goods: "Tax-exempt intra-Community supply (Art. 138 Directive 2006/112/EC)"
* Services and virtual products: "Reverse charge: VAT to be paid by the recipient (Art. 196 Directive 2006/112/EC)"
* Mixed carts show both.

The wording is stored on the order **in the language of the order** and shown on the thank-you page, in My Account, in the order emails and in the admin order screen. Both phrases are built in for all **24 official EU languages** (for example "Steuerfreie innergemeinschaftliche Lieferung (Art. 138 MwStSystRL)" and "Steuerschuldnerschaft des Leistungsempfängers (Reverse Charge, Art. 196 MwStSystRL)" in German, "Autoliquidation" in French, "Odwrotne obciążenie" in Polish), so they do not depend on installed translations. The language comes from WPML or Polylang, or from the language of the shop at checkout. The phrases and the language can be changed with filters.

= Works with =

* WooCommerce cart and checkout blocks, High-Performance Order Storage
* Cartnorm Omnibus Lowest Price: B2B group members do not see the consumer "lowest price in the last 30 days" notice, and Omnibus' strikethrough logic does not touch group prices. Retail customers are not affected.
* Germanized: unit prices and VAT notices follow the group price and display
* German Market: best effort, not yet tested (see FAQ)
* WPML and WooCommerce Multilingual: group prices, quantity prices, quantity rules and unit price fields are copied to translations (wpml-config.xml); labels and group names can be translated; fixed group prices and quantity prices follow WCML's exchange rates in other currencies
* WP Rocket, LiteSpeed Cache, W3 Total Cache, WP Super Cache, FlyingPress

= Translations =

Translations come from translate.wordpress.org as language packs. So that shops do not have to wait for a language pack, the texts your customers see (price suffix, VAT ID field and its messages, order rule messages, quote and registration pages, customer emails) have built-in wording for German (Germany and Austria, formal "Sie"), Polish, French (France and Belgium), Italian, Spanish and Dutch (Netherlands and Belgium). The built-in wording is used only for strings that no installed language pack translates. The settings screens are in English until a language pack is available.

= Evidence for your tax office =

Every order with a VAT ID stores the VAT ID, its country, the VIES result, the consultation number, the time of the check and the decision. You see it in a box on the order screen, and invoice plugins can read it from the order meta (see "For developers").

= Fast =

No database queries per cart line, also for the order rules. In our test a cart with 300 lines in a B2B group is calculated in about 40 ms.

= Not tax advice =

Cartnorm B2B applies the general EU rules for business-to-business sales (Articles 138 and 196 of Directive 2006/112/EC). It is not tax advice. Special cases – for example triangular trade, goods that are not dispatched to another member state, special schemes, or national rules – are not covered. Please confirm with your tax adviser how these rules apply to your business.

== External services ==

This plugin connects to **VIES (VAT Information Exchange System)**, a service of the European Commission, to check whether a VAT identification number is valid.

* **When:** when a customer enters a VAT ID at checkout (classic or block checkout) and the cart is calculated, when someone applies for a wholesale account with a VAT ID, when a customer accepts a quote (the order is checked like a checkout), and when an order whose VAT ID could not be checked is checked again in the background. Results are cached, so the same number is not sent again for up to 7 days.
* **What is sent:** the country code and number of the VAT ID the customer entered and, if you entered it in the settings, your shop's own VAT ID (as "requester", to receive a consultation number). No other customer data is sent.
* **Where:** `https://ec.europa.eu/taxation_customs/vies/rest-api/check-vat-number`
* Service: https://ec.europa.eu/taxation_customs/vies/
* Privacy statement: https://commission.europa.eu/privacy-policy-websites-managed-european-commission_en

No data is sent anywhere else. The plugin does not contact cartnorm.com.

== Installation ==

1. Install and activate WooCommerce.
2. Install and activate Cartnorm B2B.
3. Go to WooCommerce › Settings › B2B and enter your own VAT ID.
4. Under "Customer groups", create a group (for example "Wholesale").
5. Assign customers to the group in Users › Edit user.
6. Add group prices and quantity prices in the product editor (General tab, or each variation), and order quantities and pack sizes in the Inventory tab.
7. Optional: set minimum orders, quote buttons and hidden prices per group under "Customer groups & rules", and the approval mode under "Wholesale registration". The quote and registration pages are created on activation; an existing page with the shortcode is used instead.

== Frequently Asked Questions ==

= Do my normal customers see any difference? =

No. Guests and customers without a group are "Retail" and see exactly the prices WooCommerce shows without this plugin.

= Can a customer put themselves into a wholesale group? =

No. Only users who can manage WooCommerce or edit users can change a customer's group. The plugin never reads a group from a registration, checkout or account form.

= How do I answer a quote? =

Open WooCommerce › Quotes, enter a unit price (excluding VAT) for each line, check the validity date and click "Save and send quote". The customer receives an email with a link to accept. If you send the quote again, earlier links stop working.

= What happens when the customer accepts a quote? =

An order with your prices is created with the status "Pending payment". VAT is added with the same rules as at checkout (0% for valid EU VAT IDs from other member states). The customer goes straight to the "pay for order" page. Shipping is not added automatically; if you want to charge it, add it to your quote prices or edit the order before the customer pays.

= Is the minimum order value checked with or without VAT? =

The way the group sees prices: groups that see prices excluding VAT need the net subtotal, groups that see prices including VAT the gross subtotal. Coupons are not deducted.

= Can a spammer get a wholesale account? =

Applications get no group until you approve them (or VIES confirms the VAT ID, if you chose automatic approval). The form has a nonce, a honeypot field and a rate limit per IP address.

= What happens if VIES is not available? =

The customer can still order. VAT is charged, the order is marked "pending verification" and the VAT ID is checked again automatically (up to six times over about four days). If VIES then confirms the number, you get an order note so you can issue a corrected invoice. Order totals are never changed automatically.

= Why is VAT charged although the VAT ID is valid? =

Because the customer is in the same country as your shop (domestic sales always include VAT), outside the EU, or the VAT ID is from a different country than the billing (or shipping) address. The reason is stored on the order.

= Which address decides about VAT? =

The billing address by default. You can switch to the shipping address in the settings.

= Does it work with the checkout block? =

Yes. The VAT ID field is added with the official WooCommerce Additional Checkout Fields API (WooCommerce 8.9 or newer), and 0% VAT is shown as soon as the address is updated.

= What happens when I deactivate the plugin? =

Your products keep their normal prices and everyone simply sees them; nothing is changed in your product data. VAT evidence on existing orders stays. When you delete the plugin, its settings, groups, group prices, quantity rules, quotes, wholesale applications and cached VIES results are removed, and the quote and registration pages it created are moved to the trash. Orders, including orders created from quotes and their VAT evidence, are kept, because they belong to your order records.

= Can a consumer see a cached page with net prices? =

Not from WordPress page caches: pages for B2B customers are sent with "Cache-Control: private, no-store", `DONOTCACHEPAGE` is set and WP Rocket, LiteSpeed Cache, W3 Total Cache, WP Super Cache and FlyingPress are told not to cache them. Caches outside WordPress (your host's Varnish or nginx cache, Cloudflare "Cache Everything") cannot be configured by a plugin; most respect "no-store", and you should exclude logged-in users there. Run the self-test in WooCommerce › Settings › B2B › Store mode & caching after you change your caching setup.

= Can customers change their group with the cartnorm_b2b_group cookie? =

No. The cookie only helps caches keep separate copies. Prices always come from the group stored for the logged-in user on the server.

= When may I show only net prices ("B2B only")? =

Only when consumers are effectively excluded from buying: every customer has to register as a business and you check the account (VAT ID, company details) before it is approved. A notice or a checkbox "I am a business customer" is not enough, and consumers who can still order must see prices including VAT. Use "B2B only" mode with manual approval or automatic approval by VIES. This is not legal advice; please ask your lawyer or tax adviser.

= Does it work with German Market? =

Not yet tested, because German Market's source code was not available to us. Group prices work as usual. If German Market calculates unit prices automatically from the product price, they follow the group price; unit prices you entered by hand in German Market are not recalculated. Cartnorm B2B's own simple unit price is switched off when German Market (or Germanized) is active.

= How many customer groups can I create? =

As many as you need. There is no limit on the number of B2B groups, and every group can have its own prices, quantity prices, order rules and quote settings.

= Do I need the Pro version? =

No. Everything described here is free, without time limits and without a limit on customer groups. A separate add-on, Cartnorm B2B Pro, exists for shops that need more.

== Screenshots ==

1. The same product for a guest or retail customer (normal price including VAT) and for a customer in the Wholesale group (group price excluding VAT).
2. Product page for a wholesale customer: net group price and quantity prices.
3. Block checkout: a business customer from another EU country enters a VAT ID, VIES confirms it, and the order is calculated with 0% VAT.
4. VAT evidence on the order: VAT ID, VIES result, consultation number, time of the check, decision and the legal note.
5. Cart block: minimum order hint for a customer group ("add €80.00 more").
6. Answering a quote in WooCommerce › Quotes: price per line, validity date and a note to the customer.
7. Wholesale registration form with VAT ID check in VIES.
8. Settings: rules for Retail and for each customer group (prices with or without VAT, discounts, category discounts, minimum order, quotes).

== For developers ==

Order meta (read with `$order->get_meta()`):

* `_cartnorm_b2b_vat_id` – VAT ID with country prefix, e.g. DE123456789
* `_cartnorm_b2b_vat_country` – country of the VAT ID
* `_cartnorm_b2b_vat_destination` – country that decided about VAT
* `_cartnorm_b2b_vies_status` – valid, invalid, unavailable or format_invalid
* `_cartnorm_b2b_vies_consultation` – VIES consultation number
* `_cartnorm_b2b_vies_checked_at` – time of the VIES check (UTC, ISO 8601)
* `_cartnorm_b2b_vies_name`, `_cartnorm_b2b_vies_address` – trader data returned by VIES
* `_cartnorm_b2b_vat_treatment` – standard, intra_community_supply, reverse_charge, mixed or pending_verification
* `_cartnorm_b2b_vat_reason` – cross_border, same_country, outside_eu, country_mismatch, invalid, unavailable, verified_after_order …
* `_cartnorm_b2b_vat_notes` – array of legal notes to print on the invoice
* `_cartnorm_b2b_customer_group` – customer group at the time of the order
* `_cartnorm_b2b_locale` – language of the legal notes
* `_cartnorm_b2b_quote_id` – quote the order was created from

Filters (selection): `cartnorm_b2b_group_price`, `cartnorm_b2b_never_above_retail`, `cartnorm_b2b_tier_quantity`, `cartnorm_b2b_user_group`, `cartnorm_b2b_can_assign_group`, `cartnorm_b2b_vat_decision`, `cartnorm_b2b_require_matching_country`, `cartnorm_b2b_is_service`, `cartnorm_b2b_legal_note_goods`, `cartnorm_b2b_legal_note_services`, `cartnorm_b2b_legal_notes`, `cartnorm_b2b_vies_timeout`, `cartnorm_b2b_vies_cache_ttl`, `cartnorm_b2b_keep_data_on_uninstall`, `cartnorm_b2b_group_order_rules`, `cartnorm_b2b_product_quantity_rule`, `cartnorm_b2b_min_order_cart_amount`, `cartnorm_b2b_cart_rule_messages`, `cartnorm_b2b_quote_mode`, `cartnorm_b2b_hide_prices`, `cartnorm_b2b_add_to_quote_label`, `cartnorm_b2b_registration_errors`, `cartnorm_b2b_auto_approve`, `cartnorm_b2b_rate_limit`, `cartnorm_b2b_store_locked`, `cartnorm_b2b_store_notice`, `cartnorm_b2b_is_catalog_request`, `cartnorm_b2b_private_page`, `cartnorm_b2b_cache_plugins`, `cartnorm_b2b_legal_phrases`, `cartnorm_b2b_order_language`, `cartnorm_b2b_convert_price`, `cartnorm_b2b_show_excl_suffix`, `cartnorm_b2b_units`.

Order item meta `_cartnorm_b2b_group_price` marks lines that were priced for a group in the order editor.

Actions (selection): `cartnorm_b2b_quote_created`, `cartnorm_b2b_quote_after_answer`, `cartnorm_b2b_quote_converted`, `cartnorm_b2b_quote_status_changed`, `cartnorm_b2b_application_submitted`, `cartnorm_b2b_application_approved`, `cartnorm_b2b_application_rejected`.

Quote statuses are post statuses of the private post type `cartnorm_b2b_quote`: `cnb2b-new`, `cnb2b-answered`, `cnb2b-accepted`, `cnb2b-expired`, `cnb2b-converted`.

Assign a group from your own code: `\Cartnorm\B2B\Groups::assign( $user_id, 'wholesale' );`

== Changelog ==

= 0.3.2 =
* New plugin name and description that say what it does: EU VAT number validation (VIES) and wholesale prices.
* A "Pro version" link in the plugin's row on the Plugins screen, and one dismissible note about the Pro add-on at the top of the B2B settings screens (only for users who can manage WooCommerce; hidden when Pro is active; nowhere else in wp-admin).
* The order-editor AJAX handlers verify the request nonce themselves.

= 0.3.1 =
* Unlimited customer groups: create as many B2B groups as you need. The limit of 2 groups and the `cartnorm_b2b_max_groups` filter were removed; groups that were saved above the old limit are active again.
* Admin screens: no more Pro hints or Pro information page; the plugin shows no promotional content in wp-admin. The "WooCommerce is required" message appears only on the Plugins screen, for users who can activate plugins.
* Translations now come from translate.wordpress.org. The bundled .po/.mo files and their loader were removed. Customer-facing texts keep built-in wording for German (DE, AT), Polish, French (FR, BE), Italian, Spanish and Dutch (NL, BE), used only where no language pack translates a string.
* Fix: the VAT ID appeared twice in the billing column of the order screen in wp-admin. It is now shown once (orders from the classic checkout still show it).
* Fix: in the block checkout, logged-in customers had to type their saved VAT ID again. It is now filled in for them, like in the classic checkout, and still checked with VIES.
* Fix: on product pages in block themes such as Twenty Twenty-Five, the "Add to quote" button stretched across the full width under "Add to cart". It now sits under "Add to cart" with the theme's normal button style.

= 0.3.0 =
* Store modes: "Hybrid" (default) and "B2B only" with optional catalog restriction, notice with login and registration links, legal note in the settings.
* Cache safety: private, no-store pages for B2B customers (all pages in "B2B only" mode), DONOTCACHEPAGE, signed group cookie, integrations for WP Rocket, LiteSpeed Cache, W3 Total Cache, WP Super Cache and FlyingPress, status panel with self-test.
* Compatibility: Cartnorm Omnibus Lowest Price (no consumer notice for B2B groups), Germanized unit prices and VAT notices, WPML / WooCommerce Multilingual (wpml-config.xml, currency conversion of fixed group prices), German Market (best effort).
* Simple unit price for shops without Germanized or German Market (off by default).
* Orders created in wp-admin get the customer's group prices.
* Legal invoice phrases in all 24 EU languages, chosen by the order's language.
* Translations: German (DE, AT), Polish, French (FR, BE), Italian, Spanish, Dutch (NL, BE).
* Fix: a quote list item could get a numeric key in rare cases.

= 0.2.0 =
* Order rules: minimum order value and minimum number of items per group, minimum / maximum quantity and pack size per product and variation, enforced in the classic and block cart and checkout.
* Request a quote: "Add to quote" button next to or instead of "Add to cart", hidden prices, quote page, quotes in WooCommerce › Quotes, signed accept link, conversion into a "pending payment" order with VAT rules, emails, automatic expiry.
* Wholesale registration with VIES check, manual or automatic approval, Users › Pending wholesale, emails, application from My Account.

= 0.1.0 =
* First release: customer groups, group prices, category discounts, quantity prices, net/gross display per group, EU VAT ID check with VIES, 0% VAT for intra-Community B2B sales, legal notes and VAT evidence on orders.
