Core 1.3.2 Stable

Released on: Wednesday, 26 August 2026 08:35

The core the first commercial Solidshop extension needs. Discount & Coupons 1.0.1 pins 1.3.2 as its minimum: somewhere to store per-action settings, a discount that can reduce shipping and have that survive into the order and the invoice, a shopper-facing seam for offers that have not paid out yet, and per-line discount allocation so a free item carries no tax.

That work exposed three tax and totals bugs that were never plugin-specific — tax charged on the pre-discount subtotal, tax-inclusive stores double-counting tax in every derived grand total, and a discount engine that treated an unknown condition as no condition. Those are fixed for every store, discounts installed or not.

Added

  • Promotion notices — a seam for telling the shopper about an offer that has not paid out yet. PromotionNoticesEvent (onSolidshopPromotionNotices) fires when the promotion.notices layout renders on the cart or a product page; listeners contribute plain-text PromotionNotice objects and the layout owns the markup. The cart re-renders the fragment on every cart.update, so an offer can appear or lapse on a quantity change without a reload. DiscountEngine::resolveApplicable() is split out of evaluate() so listeners share checkout's exact definition of "qualifies". First consumer: the Discount plugin's free-item notice.
  • Shipping discounts. New total_shipping_discount column on #__sshop_orders and #__sshop_invoices; checkout writes what the discount engine resolved and every grand total subtracts it (order screens, orders list, customer spend, account order list, reports, invoices, subscription renewals, the Square plugin's order total). A "Shipping discount" line (COM_SOLIDSHOP_ORDER_SHIPPING_DISCOUNT, 23 locales) shows wherever totals are printed — admin, account, checkout finish, every invoice layout, and the order_completed / invoice_issued emails — only when greater than zero. The Sales-by-period report's Shipping column is net of it so the row still sums to its total.
  • Discount engine: a discount whose action only reduces shipping is now recorded in DiscountResult::$appliedDiscounts (new shipping_amount key), so it gets a usage row, a coupon badge and a name on the order instead of reading "invalid". On the cart page such a code is accepted and marked "Confirmed at checkout", like a location-restricted code — the shipping cost is unknown until a rate is chosen. Both clamps are unchanged.
  • Orders: new discount_total column on #__sshop_order_products — the promotion amount allocated to that line at checkout, kept separate from the manual admin line-discount rule. SUM(discount_total) equals orders.total_discount for checkout-created orders. Admin edits round-trip it (so editing a coupon order no longer erases the promotion from the totals), InvoiceGenerator folds it into invoice_items.discount_amount, and the Sales-by-product report attributes coupon discounts to the right products. Renewal pricing ignores it — a one-time coupon must not reprice every renewal.
  • Discounts list: a Published status filter. The model already honoured it; the field was missing from the filter form, so drafts and finished campaigns could only be found by scanning.

Fixed

  • Tax is now calculated on the discounted amount. The engine allocates every cart discount to the lines it belongs to (DiscountResult::$lineDiscounts, opt-in LineAllocatingActionInterface, proportional proration for scalar actions). Checkout, the checkout summary, the cart and the mini-cart feed each line's discounted subtotal to the tax computation, so a free item carries $0 tax and a 10 % discount reduces each affected rate by exactly 10 %, in both tax modes. Previously tax was computed pre-discount and subtracted after — the shopper paid tax on money they never spent, and #__sshop_order_taxes overstated collected tax.
  • Tax-inclusive stores no longer double-count tax in derived grand totals. With tax_price_input_type = inclusive the subtotal is gross and already contains total_tax, but every grand-total formula added it again — inflating the admin order screen, orders list, dashboard KPIs, customer spend, the account order list, stored invoice totals and (worst) the amount handed to payment gateways. All formulas now branch on the new TaxHelper::pricesIncludeTax(). Exclusive-mode stores are byte-identical to before; checkout summaries were already correct.
  • Per-line clamping also corrects over-stacking: two 100 % product-scoped discounts on one line no longer both pay out against unrelated lines' money — each sees only the headroom earlier discounts left. appliedDiscounts[n]['amount'] is now the post-clamp figure.
  • Discount engine: a condition whose handler is not registered now fails closed. Previously disabling the Discount & Coupons plugin turned every discount gated only by a rule-builder condition into an unconditional one.
  • Cart page and mini-cart: the discount row shows the display-basis (tax-adjusted) amount, so subtotal − discount = estimated total holds on screen when prices include tax. The cart.update JSON keeps its discount_total key; only the basis changed.
  • Payment method names read as labels, not slugs. payment_method_id stores a machine id (cod, banktransfer, free) and several surfaces printed it raw — confirmation, payment-received and order-cancelled emails said "Payment method: Cod", and the report and admin order screen showed cash / free / (none). New PaymentMethodHelper::label() resolves both key namespaces and de-slugs when the string is missing, so an uninstalled plugin can no longer leak a raw language key to a shopper. Every display surface funnels through it (emails via a new payment_method_label variable); the stored id is unchanged and stays what reports group on and CSV exports carry.
  • The orders-list payment filter now offers every method the orders actually used. Its options came from enabled payment plugins only, so admin-created orders (cash, bank_transfer, other), free orders and orders with no method were never offered — and disabling a plugin made every order paid through it unreachable. Options are now read from the orders themselves (PaymentMethodUsage, store-scoped); orders with no method are offered as Not specified. New composite index idx_#__orders_store_payment_method keeps that query index-only.
  • The cart's "Discount" row no longer renders when nothing was discounted. The row carries hidden, but that attribute's display: none comes from the UA stylesheet and lost to Bootstrap's .d-flex and Foundra's summary-row rule — so every cart with no qualifying discount showed a green "Discount −$0.00". Both stylesheets now carry a [hidden][hidden] { display: none !important } guard, which fixes every hidden toggle at once: under Foundra that also covers the collapsed coupon form, the billing address block, and the mini-cart badge's "0" bubble on an empty cart.
  • Checkout no longer fatals on a free product. A product saved with no price stores NULL, and the cart line carried it into DisplayCurrency::format() — a type error whenever a free item was in the cart, exactly what the Discount plugin's Free item action produces. CartController casts on add and quantity change, and both checkout summaries coerce on render, so existing sessions are safe too.
  • Filters module: the section headers were dead on a clean install — they are Bootstrap collapse toggles and nothing loaded bootstrap.collapse, so with Collapsed by default on, every filter stayed hidden. The module now declares the dependency itself.
  • The applied-coupon chip showed a wide gap before its remove control (.btn-close padding plus ms-1 plus a collapsed source newline). The chip is now flex, btn-sm ms-1 is gone from both coupon layouts, and under Foundra the cross is masked from currentColor so it works in both themes.
  • The component stylesheet now has an owner on every view that needs it. com_solidshop.core was reaching checkout second-hand from the mini-cart module, which is not rendered under tmpl=component. Checkout, Account and Tracking load it themselves, covering the Foundra override and the plugin-owned account templates too; the redundant layout-level call is removed.

Changed

  • Discount edit form: the action row's config JSON now carries handler-specific settings submitted through an action_config group, and loadItem() exposes the decoded JSON so fields bind back on edit. Only keys whose showon matches the chosen action type are stored, so hidden fields' defaults don't leak between types. This is the seam plugin action types (BOGO, Fixed price, Tiered, Free item) save through — no plugin-side save hook needed. Released 1.3.1 accepted these fields and dropped them on save.
  • Discount edit form: saving a discount whose stored action type the form cannot offer (a plugin-registered type while that plugin is disabled) leaves the action row untouched instead of rewriting it as a plain percentage discount.
  • Filters module: colours resolve through the active template's tokens — --solidshop-filters-* falling back Foundra → Bootstrap → literals — so the module follows the active Foundra preset and its dark values and still looks right under Cassiopeia.
  • Admin list filters: the empty option on the discount and invoice status filters reads - All - (JALL), matching the other Solidshop lists and Joomla's own.

Upgrade notes

  • Schema update 1.3.2.sql adds total_shipping_discount to #__sshop_orders and #__sshop_invoices, discount_total to #__sshop_order_products, and the composite index on #__sshop_orders. Existing rows keep NULL / 0 and their totals are unchanged — historical orders are never restated; new orders on discounted carts simply collect less tax (the fix).
  • Third-party discount actions registered via DiscountRegisterEvent keep working unchanged: calculate() is untouched and scalar amounts get prorated allocation automatically; LineAllocatingActionInterface is opt-in. Callers evaluating the engine without cart_items keep the previous cart-level behaviour.
  • Discount & Coupons 1.0.1 pins 1.3.2 as its minimum core and refuses to install against anything older; update core first.
  • Stores that customised the order_completed (seed 1.0.11), invoice_issued (1.0.1), order_cancelled (1.0.1) or payment_received (1.0.1) bodies will see a drift banner: those defaults gained the shipping-discount row and now print payment_method_label instead of payment_method_id. A customised body keeps the old lines until it is reset to the default; the raw payment_method_id stays available to templates that branch on it, such as the bank-transfer instructions partial.

Solidshop Core v1.3.2

Joomla! 6.0 Joomla! 6.1

File size 9.10 Mb
SHA-256 Signature 28937cc7e4389d2a239468c0e2af4b2e0c434a21e05e7ba70b44c1ff8788e587
Compatibility Joomla! 6.0 Joomla! 6.1