Solidshop Core 1.3.2 — tax that follows the discount

The core that Discount & Coupons is built on — and two tax fixes that matter whether or not you ever install it.

Core 1.3.2 is out. We cut it because our first commercial extension needed it: Discount & Coupons 1.0.1 pins 1.3.2 as its minimum core, and the plugin cannot store its action settings, discount a shipping rate, or tell a shopper about an unclaimed offer without the seams this release adds.

But building those seams meant taking a hard look at how discounts and tax interact, and that turned up two bugs that were never plugin-specific. They are the reason to update even if you never buy a promotions plugin.

Tax is now calculated on the discounted amount

Until 1.3.2, Solidshop computed tax on the pre-discount subtotal and subtracted the discount afterwards. On a 20 % VAT store, a €100 cart with a €20 coupon collected €20 of tax instead of €16 — the shopper paid tax on money they never spent, and the tax you recorded in #__sshop_order_taxes and reported on the invoice was overstated. On a free item the effect was starker still: a product given away carried full tax.

The discount engine now allocates every discount to the cart lines it actually belongs to, and the tax for each line is computed on line subtotal minus its share of the discount. A discount that knows its own targets — a free item, a buy-one-get-one, a fixed price per unit — reports them exactly; a plain percentage or fixed amount off the cart is prorated across the lines it applies to. Checkout, the checkout summary, the cart page and the mini-cart all agree, in both tax-exclusive and tax-inclusive modes.

Historical orders are never restated. New orders on discounted carts simply collect the right amount of tax.

Tax-inclusive stores were double-counting tax

This one is worth reading carefully if your store displays prices including tax.

In inclusive mode an order’s stored subtotal is gross — the tax is already inside it. Every formula that derived a grand total was nonetheless adding the extracted tax on top a second time. That inflated the admin order screen, the orders list, dashboard KPIs, lifetime customer spend, the account order list and the stored invoice total — and, worst of all, the amount handed to the payment gateway, which would charge the shopper more than the checkout page had promised them.

Every one of those formulas now branches on whether prices include tax: the reports helper, the admin order model (which also fixes the gateway charge amount and the outstanding balance), the account page, the invoice generator, the admin order-edit summary in the browser, subscription renewal charges, and the Square plugin’s itemised order. Checkout summaries were always correct and are unchanged, and exclusive-mode stores behave exactly as before.

If you run an inclusive-tax store, compare an order’s admin total with what the shopper saw at checkout after updating. They should now match.

What the Discount plugin needed

The rest of the promotion work lives in the core so that any extension — ours or yours — can build on it:

  • Per-action settings — the discount edit form now round-trips handler-specific settings through the action row, so a plugin can ship an action type with its own fields and have them saved without a save hook. Released 1.3.1 accepted those fields and silently dropped them.
  • Shipping discounts — a discount can reduce the shipping cost, and that now survives into the order and the invoice on its own Shipping discount line, visible on the admin order, the confirmation email, the account order page and every invoice layout. A coupon whose only effect is a shipping discount validates on the cart page and is marked “Confirmed at checkout”, because the shipping cost is not known until a rate is picked.
  • Promotion notices — a seam for telling the shopper about an offer that has not paid out yet. Qualification runs through the same code checkout uses, so a notice can never promise something the order will not honour, and the cart re-renders its notices on every quantity change. The Discount plugin’s first use of it: “Add X to your cart — it is free with this order.”
  • Line-level discount records — order lines now store the promotion amount allocated to them, so the Sales-by-product report attributes coupon discounts to the right products and invoices show the discount per line.
  • Unknown rules fail closed — a discount gated by a condition the site has no handler for no longer applies. Previously, disabling a promotions plugin turned every discount that depended on its rules into an unconditional one — a coupon-shaped hole in your pricing.

Payment methods read like labels again

Solidshop stores a machine id for the payment method (cod, banktransfer, free), and several screens printed it raw. Customers received emails saying “Payment method: Cod”; the Sales-by-payment-method report showed merchants cash and (none) rows. Every display surface now resolves a proper label, falling back to a readable form when a plugin has been removed, so an uninstalled gateway can no longer leak a raw language key to a shopper. The stored id is untouched — it is still what reports group on and what the CSV export carries.

The orders list’s payment filter was built from enabled payment plugins, which meant methods written by the admin create-order screen were never offered, and disabling a gateway made every order ever paid through it unreachable through the filter while leaving those orders in the list. The options are now read from the orders themselves: every option matches at least one order, and every order is reachable through some option — including Not specified for orders with no method at all.

Storefront and admin polish

  • A free product no longer breaks checkout. A product saved without a price stores NULL, and the checkout order summary died with a type error the moment such an item was in the cart — exactly what a free-item promotion produces.
  • No more phantom “Discount −$0.00” row. The cart summary emitted the row hidden, but a framework display rule beat the browser’s hidden styling. The fix is global, so it also cleans up the collapsed coupon form, the billing-address block and the mini-cart’s “0” badge under Foundra.
  • Filters module: the section headers work. They are Bootstrap collapse toggles and nothing was loading the collapse script, so on a clean install they did nothing — and with Collapsed by default switched on, every filter stayed hidden. The module now ships its own dependency. Its colours also resolve through the active template’s tokens, so it follows your Foundra preset and its dark values instead of hardcoded Bootstrap variables.
  • The applied-coupon chip is tidy, and its remove cross is finally visible in both light and dark mode under Foundra. The stylesheet those rules live in is now loaded by the Checkout, Account and Tracking views themselves, which also fixes styling on the plugin-owned account pages (bookings, subscriptions, downloads, reviews) that never loaded it.
  • The discounts list gained a Published filter, so an unpublished draft or a finished seasonal campaign is findable without scanning the whole list.

Updating

Core 1.3.2 is a one-click update from System › Update. It carries a schema update that adds the shipping-discount and line-discount columns and one index; existing orders and invoices keep a zero shipping discount and a zero line discount, and their totals do not change.

Two things to check afterwards. If you customised the Order completed or Invoice issued email body, the email editor will show a drift banner — reset to the default, or add the new totals row by hand, to pick up the shipping-discount line and the readable payment-method label. And if you are installing Discount & Coupons, update the core first: its installer checks for 1.3.2 and refuses to run against anything older.

For the complete, itemised list of every change, see the Solidshop changelog.