Skip to content

fix(checkout, settings): storefront hardening, delivery currency, settings layout - #34

Merged
Seiger merged 5 commits into
Seiger:1.xfrom
elcreator:fix-settings-visual-bugs-and-tests
Oct 28, 2026
Merged

Seiger merged 5 commits into
Seiger:1.xfrom
elcreator:fix-settings-visual-bugs-and-tests

Conversation

@elcreator

Copy link
Copy Markdown
Contributor

Summary

Fixes checkout and storefront gaps that could charge customers the wrong amount or expose internal data. Fixes delivery cost currency. Tidies the settings and product list screens in the manager. Adds standalone tests for all
of it.

Checkout and storefront

  • Prices are checked again before the order is saved. processOrder() reprices the lines it stored at initCheckout() using current prices, publication status, stock and currency. If anything changed, it saves the
    current lines and returns error_code: cart_changed with the new products and cost. The customer then confirms the order again.
  • Stock is checked everywhere. The new sCart::shortages() is used by checkout and by quick orders (single product and cart). Lines above stock fail with out_of_stock.
  • Unpublished products can't be bought. The new StorefrontGuard::purchasable() is checked in addProduct(), getMiniCart() and quick orders.
  • Delivery cost:
    • It's recalculated on every setOrderData() and again in processOrder(). The cost stored in the session is no longer trusted.
    • It's converted from the method's currency into the order currency. Before this, 150 UAH was charged as 150 EUR. A new method, BaseDeliveryMethod::getCostCurrency(), returns the currency of the method's settings row, or
      the main shop currency if that row has none.
    • Negative and non-finite costs count as 0.
    • CourierDelivery now matches city names case-insensitively in any script, including Cyrillic.
  • Failed attempts roll back. A failed processOrder() restores the checkout data and session, so a retry no longer adds the delivery cost twice.
  • Input checks:
    • In quick orders, a missing productId orders the cart. Any value that isn't a whole number counts as an unknown product instead of causing a TypeError.
    • removeProduct() rejects malformed ids.
    • Piece quantities are capped at MeasuredQuantity::PIECE_MAX (1,000,000).
    • A negative price returned by a listener never lowers a total.
  • Responses don't leak internal data:
    • The process-order and quick-order routes return sOrder::toCustomerArray(), which leaves out manager notes, history and meta.
    • Failed quick orders return only the product's id, title and link, not the full product row.
  • Order rate limit: new setting orders.rate_limit (default 10 orders per IP address per 10 minutes; 0 = no limit). Requests above the limit get 429 with rate_limited. If no cache store is available, orders are still
    allowed.
  • Container exchange needs its own opt-in: new setting product.container_exchange_on, off by default. A customer's declared exchange waives the deposit before anyone has seen the containers. While the setting is off, the
    route returns 404 and declared exchanges are ignored.

Manager UI

  • Measured quantity defaults and step rounding moved into their own Measured quantities block at the end of the second column of Product settings (new partial: settingsMeasuredQuantitiesConfiguration).
  • New Without VAT column in the product list, showing the price without VAT and without the included retail excise (sProduct::priceNetTo()). It appears only while prices without VAT are on, and can be turned off with
    products.show_field_price_net.
  • Spacing fixes in the settings blocks: the last row no longer adds an extra gap, and the currency cards fill the block width without Bootstrap gutters.
  • The setting label "Prices shown" is now "Storefront prices by default", with clearer help text.

Docs and translations

  • Documented the checkout checks, rate limit, container exchange setting and delivery cost currency in the events, deliveries and payments developer docs (en/ru/uk).
  • Added the new strings to all lang/*/global.php files.

Tests

  • New tests/standalone/scommerce-storefront-hardening.php: unpublished products, malformed ids, quantity caps, negative prices, stock, repricing (price, publication, currency), delivery currency and address recalculation,
    retry rollback, the customer order payload and the rate limit.
  • New tests/standalone/scommerce-ukraine-beer.php: an end-to-end example selling beer in Ukraine. Prices include 20% VAT and 5% retail excise, and the bottle deposit sits outside both.
  • Updated the checkout harness (switchable currency, exchange rates, in-memory cache, published flag) and the existing measured-quantity, container-deposit, payment-schedule and VAT-display tests.

Breaking / behaviour changes

  • processOrder() can now fail with cart_changed. Storefront JS should show the returned products and cost and ask the customer to confirm again.
  • The order routes now return toCustomerArray() instead of the full order model.
  • Customer-declared container exchanges stay ignored until Customers declare exchanged containers is turned on.
  • A delivery method's calculateCost() is now read as being in getCostCurrency(). Custom methods whose cost is not in the currency of their settings row should override that method.

@Seiger
Seiger merged commit 9724098 into Seiger:1.x Oct 28, 2026
0 of 2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants