# 4.5.0

- New: "Include orders from other Sales Channels" — the revocation form can now also find orders placed in a different Sales Channel of the same installation. Intended for shops whose Sales Channels share one customer base, for example a second channel with different prices. Applies both to manual order-number entry and to the order selection shown to logged-in customers.
- The option is disabled by default; existing installations behave exactly as before.
- Security: for logged-in customers the customer-id check still applies, so other people's orders remain out of reach. For guests the option only takes effect while email validation is enabled, so a bare order number cannot trigger a cross-channel search.
- Requirement: customers must not be bound to a Sales Channel (Settings → Login and registration), otherwise each channel keeps its own customer record.
- Note: the revocation period and all exclusions are still read from the Sales Channel the customer is currently browsing. If the same order number exists in two channels, the revocation is rejected as ambiguous instead of guessing.
- No database migration required.

# 4.4.14

- New: "Revocation form" card in the plugin settings with two switches.
- New: "Hide the 'reason for revocation' field" — the optional free-text field can now be removed from the form entirely. A reason posted regardless is discarded server-side and appears neither on the confirmation page nor in the internal notification.
- New: "Disable captcha on the revocation form" — the shop-wide captcha (Honeypot, basic captcha, reCAPTCHA v2/v3) can be skipped for the revocation page without disabling it shop-wide. Useful when the captcha causes accessibility or consent problems on this legally required page. Caution: without a captcha, bots can trigger mail dispatch through your shop — hence off by default.
- Both options are disabled by default; existing installations behave exactly as before.
- No database migration required.

# 4.4.13

- Fix: cancelled orders no longer show a "Revoke contract" link in the customer account. Such orders count as already revoked in the plugin and were rejected on submission anyway — the link was a dead end and was consequently also missing from the order selection on the revocation page. Account link, order selection and the submit check now behave consistently.
- No database migration, no configuration change required.

# 4.4.12

- Fix: the "Revoke contract" link in the account order history (and on the account overview page) is now shown/hidden based on the **real** revocation deadline — using the same logic the submit flow enforces. Previously the display approximated the period from the order date; with the period-start modes "Invoice date" and "From delivery status" (e.g. "Delivered") the link was therefore wrongly missing for orders older than the period that had only recently been delivered or invoiced. The revocation itself (via the revocation page) was never affected and always worked correctly.
- No database migration, no configuration change required.

# 4.4.11

- Feature: new **"From delivery status"** option for the "Revocation period starts from" setting (Revocation Settings card). In addition to the fixed dates (order, shipping, invoice date) you can now pick **any delivery status of your shop** as the start of the period — including custom statuses such as "Delivered" that a tracking service (Trackingmore etc.) sets automatically. The revocation period then runs from the moment the order's delivery reached that status. For partial deliveries the **latest** transition counts — matching the common legal wording "from receipt of the last goods" (Art. 9 Directive 2011/83/EU). As long as the chosen status has not been reached, the order stays revocable.
- The revocation-period-start dropdown is now grouped into **"Fixed dates"** and **"From delivery status"**, each with an explanatory help tooltip.
- Note: the start date is resolved from the delivery status history (state machine). If a status is exceptionally set without a real state transition (e.g. by a direct database update), the moment cannot be determined — the order then stays revocable as a safe default (no fallback to the order date).
- No database migration, no configuration change required. Existing shops are unchanged (default remains "Shipping date").

# 4.4.10

- Feature: the internal revocation notification to the merchant now carries the revoking customer's email address as the `Reply-To` mail header. The merchant can reply straight from the notification and reaches the customer who submitted the revocation. The `From` address stays the shop address, so SPF/DKIM and deliverability are unaffected. Applies only to the internal merchant mail — the customer confirmation gets no Reply-To. No configuration required.

# 4.4.9

- Feature: in addition to customer groups, revocation can now also be disabled via **Rule Builder rules** (new "Disable revocation for these rules" selection in the "Customer groups & rules excluded from revocation" card). Both criteria can be combined freely (OR-linked) — ideal for cases customer groups cannot express (e.g. "billing country outside the EU"). For logged-in customers the rule applies directly to the display and form; for the guest revocation (order number + email) it applies when order-number validation is enabled (the rule is then evaluated against the resolved order).
- Note: do not accidentally exclude EU consumers — the online withdrawal function is legally required for them (Art. 11a Directive 2011/83/EU).
- No database migration required. Existing shops are unchanged (no rules are pre-selected).

# 4.4.8

- Feature: new **"Invoice date"** option for "Revocation period starts from" (card "Revocation Settings"). The period then runs from the date of the order's invoice document — independent of the order or shipping date. If no invoice exists (yet), the order cannot be revoked.
- Clarification: the help text for "Revocation period starts from" now explains all three options more precisely. In particular, "Shipping date" is the moment the delivery is set to status "Shipped" in Shopware (this is NOT the delivery-note or invoice date); until an order is marked as shipped it stays revocable.
- No database migration or configuration change required. Existing shops are unchanged (default remains "Shipping date").

# 4.4.7

- Fix: compatibility with plugins that replace Shopware's `product.repository` with their own decorator (e.g. NetInventors "PurchaseBlocker"). Previously the revocation page failed in such shops with a `TypeError` ("… must be an instance of EntityRepository, instance of …ProductRepositoryDecorator given") and showed "Something went wrong". The internal service now accepts any repository, regardless of whether it extends `EntityRepository`. No database migration or configuration change required.

# 4.4.6

- Feature: new switch **"Enforce revocation period"** (card "Revocation Settings", default **on**). When disabled, revocations are accepted **regardless of the deadline** — for merchants who cannot verify the delivery date up front and want to judge validity manually afterwards (e.g. from the tracking data). With the check off, logged-in customers also see older orders in the selection list, and submitting is no longer rejected with "period expired". The "Revocation period (days)" and "Revocation period starts from" fields then have no effect.
- Existing shops are unchanged: the switch is on by default (even without a stored value), so the deadline check behaves as before. No database migration required.

# 4.4.5

- Change: the default "privacy notice" link on the revocation form now opens the privacy page as a **normal, full page in a new tab** instead of a modal. Rationale: a plain page link is more robust, behaves identically in the storefront and in e-mail, and keeps the half-filled form thanks to the new tab. Applies to **new installations** only — existing shops are unchanged.
- Feature: two magic-link schemes are available for consent labels. `page://privacy`, `page://tos`, `page://imprint` open the configured CMS page as a **full page** (recommended; add `target="_blank"` for a new tab). `cms://privacy` etc. still open it **in a modal**. Choose per entry.
- Fix: the **modal** did not open because the HTML sanitizer stripped the required `data-ajax-modal` attributes. `cms://` links now open correctly in a modal — like Shopware's own footer links (the label is sanitized server-side and the modal attributes are re-applied afterwards).
- Fix: the consent link in the **revocation confirmation e-mail** was relative (no domain) and pointed at the bare CMS fragment — in some cases it did not work at all. It is now an **absolute, full-page URL**.
- Existing shops: an existing `cms://privacy` is preserved and still opens the (now working) modal; the e-mail automatically gets the absolute full-page link. No database migration required.

# 4.4.4

- Feature: confirmation entries on the revocation form can now be shown as a **plain notice without a checkbox**. New per-entry switch "Show as plain notice only" (card "Confirmation checkboxes"). When enabled, the text is rendered as a plain hint with no tick box — nothing can be ticked and nothing is stored as consent (informational only). "Mandatory confirmation" is disabled for such entries. Existing entries remain normal checkboxes.

# 4.4.3

- Fix: installation failed on **MySQL 8.4 (Oracle)** with "General error: 6125 Failed to add the foreign key constraint. Missing unique key for constraint 'fk.jga_revocation.order_id' in the referenced table 'order'". Cause: the revocation table's foreign key referenced the single column `order`(id), but Shopware's `order` table has a composite primary key (id, version_id) — `order.id` alone is not unique. MySQL 8.4 enforces the SQL standard more strictly than MySQL 8.0 / MariaDB and rejects it. The database foreign key is no longer created (the `order_id` column and its index remain; the relation is handled by the Shopware DAL).
- Existing installations are not modified (the table migration only runs on first install). An additional, idempotent migration drops the old foreign key if present, so existing shops stay consistent and survive a later move to MySQL 8.4 (e.g. dump/import). No data is changed.

# 4.4.2

- Change: the "Validate order number" switch (card "Form validation", formerly "Contract number must resolve to an order") now also controls whether the order number is a required field. Enabled (default): the order number is mandatory and must match an existing order (previous behaviour). Disabled: the field becomes optional — customers may leave it blank, the form shows it as "(optional)", and the entry is no longer matched.
- Behaviour change: previously an (unchecked) order number still had to be entered even when matching was disabled. From now on the field is fully optional when the switch is off — affected shops may then receive revocations without an order number.
- Rename: the "Email must match the order" switch is now called "Validate email".
- Technical: the revocation entity's `orderNumber` field is no longer `Required` and now allows empty strings (`AllowEmptyString`), so revocations without an order number can be stored. No database migration required.

# 4.4.1

- Feature: the "Revoke order" footer button is now shown on every page by default — including the cart and checkout. It used to be hidden on those pages. Background: several merchants want the revocation permanently visible instead of hiding it during the ordering process.
- New setting "Hide footer button during checkout" (card "Button placement", off by default). Merchants who do not want the button during the ordering process can enable it — the button then disappears on the cart, confirm and finish pages. Only takes effect when "Show button in footer" is enabled.

# 4.4.0

- Feature: disable revocation for customer groups. The plugin settings gain a new card ("Customer groups excluded from revocation") where customer groups can be selected for which the online revocation is fully disabled: logged-in members see neither the footer button nor the account menu entry, the revocation page shows them a notice instead of the form, and orders placed by customers of these groups cannot be revoked through the guest access (order number + email) either. The revocation hint in the order confirmation email is also omitted for these orders. Intended for business customer (B2B) groups without a statutory right of withdrawal.
- Important: anonymous visitors always see the form — guests technically run under the sales channel's default customer group, and consumers are legally entitled to the withdrawal function as of 19 June 2026 (Art. 11a Directive 2011/83/EU). Logged-out B2B customers are therefore caught via the ordering customer's group on submission. The notice text is adjustable as a snippet (jga-revocation.form.unavailableForCustomerGroup).

# 4.3.4

- Fix: the revocation confirmation to the customer was never sent on shops whose message-queue worker is not running — silently, with no error and no log entry (while the internal notification to the shop owner still arrived). Background: the customer mail is sent through the Flow Builder flow created at installation. Shopware builds that flow's executable payload via the flow indexer, which used to be triggered through the message queue; if the queue is never consumed, the payload stays empty and Shopware silently skips the flow. Fixed: the plugin now runs the flow indexer synchronously on activation and after every plugin update — without any queue dependency. Existing affected installations are repaired automatically by this update; no manual steps required.
- Note for operators: a non-running message-queue worker affects far more than this plugin (thumbnails, indexing, scheduled tasks). This update decouples the revocation mails from it, but it does not replace setting up the worker.

# 4.3.3

- Feature: multiple internal notification email addresses. The "Internal notification emails" field now accepts several comma-separated addresses — each one receives the internal notification about a new revocation. Invalid entries are ignored (a single typo does not suppress delivery to the remaining addresses), and a single address keeps working as before.

# 4.3.2

- Fix: pages displaying the footer revocation button could be scrolled horizontally (~20px overflow). The button was needlessly wrapped in a Bootstrap `.row`/`.col-12` whose negative gutter margin was not offset by container padding (theme-dependent `padding: 0`). Removed the grid nesting and center the button directly on the container.

# 4.3.1

- Fix: an order could be revoked multiple times as soon as the automatic state-machine transition to "Revoked" was skipped — be it because `autoTransitionToRevoked` was disabled, the current order state forbade the transition (e.g. connector-driven setups like plentyONE/Lenz, JTL, Pickware), or because the order contained excluded items. Background: the duplicate-revocation safeguard was tied exclusively to the order state; the revocation tag was applied but never read. Fix: the new `isOrderAlreadyRevoked()` helper now checks both the state (`revoked`/`cancelled`) and the presence of the configured revocation tag on the order. Either marker is sufficient to reject a follow-up submission with "A revocation has already been submitted for this order." The solution honours manual merchant intervention: when the admin removes both tag and state, the order becomes eligible again.
- Help text for the "Add tag to order on revocation" setting clarifies that at least one of the two options (state change or tag) should be active so the plugin can recognise an already-revoked order.
- Important for connector setups: the plugin does not need its own status column workaround — the existing Shopware signals (state + tag) are sufficient once both are evaluated.

# 4.3.0

- New configuration card "Form validation" with two independently switchable checks: "Contract number must resolve to an order" (default on) and "Email must match the order" (default on). When the first option is disabled, revocation submissions without a matching order are still accepted; the plugin then skips the automatic transition to "Revoked" and the order tag because there is no order to act on. Both customer and merchant notification mails are still sent, the "Revoked items" section is omitted via conditional template rendering.
- New configuration list "Additional search fields for the contract number" in the same card. Merchants can register arbitrary order custom fields that are matched against the entered contract number in addition to the native Shopware order number. Use case: connector setups (e.g. plentyONE/Lenz, JTL, Pickware, Xentral) where the customer only knows the external ERP/WMS order id — typically stored as a JSON property in `order.custom_fields`. Each search field can be enabled or disabled individually; the UI selector exposes every custom field registered in the system. Duplicate selections are prevented in both frontend and backend.
- Storefront form field renamed from "Order number" to "Contract number (order number, subscription number, ...)" to reflect the plugin's broader scope (contracts, subscriptions, orders).
- Backend lookup `findOrderForRevocation()` iterates sequentially: first native `orderNumber`, then each active search field via JSON-path filter `customFields.<name>`. For numeric inputs an integer cast is also attempted (`EqualsAnyFilter` with both string and int variant) so connector ids match whether stored as string or as JSON integer.
- Ambiguous-match protection: when a search-field lookup hits more than one order (connector bug, migration duplicate), the plugin throws an `AmbiguousRevocationLookupException` and shows the customer a clear error message instead of silently revoking the newest order. This preserves the evidentiary value of the plugin even under data anomalies.
- Defensive: when a configured custom field has been deleted from the `custom_field` table (e.g. set reconfigured), the backend lookup silently skips the entry via try/catch — no 500. The admin UI marks the orphaned entry in red as "Custom field not found — please re-select or remove".
- Custom-field values stored as wrapped objects (e.g. `{"value": "X"}`) are intentionally not supported — the lookup expects scalar values directly in the JSON column. A non-scalar value gracefully falls back to "not found".

# 4.2.5

- Fix: logged-in customers could not revoke their own orders if they had changed the account email after the order was placed. Background: `order_customer` in Shopware is a snapshot taken at order time — the email stored there is *not* updated when the customer later changes their account email (intentionally, for evidentiary reasons). The plugin nevertheless filtered on that snapshot email in `findOrderByNumberAndEmail()`. As a result, the customer saw the order in the account-page revocation list (which filters on `customerId`), but the submit returned "order not found". Fix: for logged-in customers, the lookup now filters on `orderCustomer.customerId`; for guests, the existing email-based filter is preserved.

# 4.2.4

- Fix: `UnmappedFieldException: Field "id" in entity "state_machine_history" was not found` when a logged-in customer opened `/revocation` while having at least one revocable order with shipped deliveries (delivery state `shipped`). `getRevocationStartDate()` used two non-existent filter paths:
  - `entityId.id` → the field `entityId` does not exist on `StateMachineHistoryDefinition`. The correct field name is `referencedId` (DB column `referenced_id`).
  - `toStateMachineStateId` → does not exist either. Correct field name: `toStateId` (DB column `to_state_id`); `toStateMachineState` is only the ManyToOne association name.
  - Additionally, the query now filters on `referencedVersionId = LIVE_VERSION` so draft versions cannot be matched accidentally.
- Defensive: the history query is now wrapped in `try { … } catch (\Throwable)`. If it fails again for any reason in the future (e.g. Shopware renaming fields in a later 6.x version), the plugin silently falls back to `orderDate` instead of producing a 500 — worst case the customer gets a slightly tighter revocation window, but the page stays functional.
- Replaced the magic string `'order_delivery'` with `OrderDeliveryDefinition::ENTITY_NAME` — will break at compile time if Shopware ever renames the entity.
- Bug existed in the 4.x line since the initial SW 6.7 release; went unnoticed because it only triggers with default config `revocationPeriodStart=shippingDate` AND an order with at least one `shipped` delivery. Identical bug present in parallel in the 3.x line (SW 6.6), fixed there in 3.3.4.

# 4.2.3

- Fix: the footer revocation button is now hidden on all checkout pages, not just on the cart, confirm and finish pages. Previously the button was visible on `/checkout/register` (shipping information / guest checkout), so an accidental click in the middle of an in-progress order kicked the customer out of checkout even though no order had been placed yet. The route check in the footer template (`@Storefront/storefront/layout/footer/footer.html.twig`) was switched from a whitelist of individual checkout routes to `activeRoute starts with 'frontend.checkout.'` — this covers all current and future checkout sub-routes automatically. The CMS element and the account-menu link are not affected.

# 4.2.2

- Fix: the revocation form now uses Shopware's standard captcha-form pattern. The form element carries `data-form-handler="true"`, which makes Shopware's built-in `FormHandler` plugin disable native HTML5 validation (`<form novalidate>` at runtime) and take over validation itself. This eliminates browser console errors such as "An invalid form control with name='shopware_basic_captcha_check' is not focusable" or `_grecaptcha_v3`, which could block form submission when BasicCaptcha or reCAPTCHA v3 was active. The defensive JavaScript that stripped the `required` attribute from hidden captcha inputs, and the inline `onsubmit` script that disabled the submit button, are no longer needed — cleaner form architecture, no workaround.
- Fix: on BasicCaptcha failure the Shopware server returned a `MethodNotAllowedException` (HTTP 500). Background: Shopware's `ErrorController::onCaptchaFailure()` calls `forwardToRoute($request->get('_route'))` for non-"breaking" captchas (BasicCaptcha is the only built-in with `shouldBreak()=false`); the forward mechanism matches the target route with method=GET, but our submit route is POST-only. The plugin's `CaptchaExceptionSubscriber` now additionally catches `Symfony\Component\Routing\Exception\MethodNotAllowedException` and handles it exactly like a regular `CaptchaException::INVALID_CAPTCHA_ERROR`: redirect to the form with a flash error and pre-filled data as query params.
- New plugin-config card "Products excluded from revocation": merchants can exclude products from the revocation flow (e.g. custom-made, sealed hygiene articles, perishable goods).
  - **Product custom field**: plugin automatically creates an "Excluded from revocation" boolean field in a new "Revocation" custom-field set on the product entity. Settable directly on the product, filterable in the product list, supports bulk-edit.
  - **Dynamic product groups**: multi-select for product streams. Products that fall under at least one selected stream are excluded. Both mechanisms combine via OR.
- The revocation form now shows, for the selected order, which items are revocable and which are excluded. For orders that contain only excluded items, the form is replaced by a notice with support contact information (per-language editable in the plugin config, default text pre-seeded).
- For orders containing excluded items (partial revocation), the automatic order-state transition to "Revoked" is skipped — the order instead unconditionally receives the revocation tag "Widerruf-Prüfung" so the merchant can filter it in the order list and manually decide which positions to refund. The `autoTransitionToRevoked` setting only applies to orders without excluded items.
- The internal revocation-notification email now lists revoked and excluded items separately and, for partial revocations, explicitly mentions the "Widerruf-Prüfung" tag that was attached to the order. Customised mail templates are respected (only unmodified system-default templates are migrated). New mail context variables: `templateData.revocableLineItems`, `templateData.excludedLineItems`, `templateData.hasExcludedLineItems`.
- Data model extension: `jga_revocation.revocable_line_items` and `jga_revocation.excluded_line_items` (JSON) store a snapshot of both lists at the moment of revocation — evidentiary record kept even if plugin configuration changes later.
- Performance: `ExcludedLineItemDetector` issues only a single database query for N configured streams (OR-wrapped `MultiFilter` instead of a loop).
- Reminder: the exclusion from revocation must additionally be communicated in the withdrawal instructions BEFORE contract conclusion (§ 312g BGB) — this responsibility lies with the merchant. The plugin only provides the tooling.

# 4.2.1

- Fix: on Shopware 6.7.x the revocation page (`/revocation`) failed with a 500 error (`Attempted to call an undefined method named "setTwig"`). Root cause: the service definition called `setTwig` as a setter injection in `services.xml`, but that method was removed from `StorefrontController` in Shopware 6.7. Twig is now pulled from the service container via `getSubscribedServices` — no setter needed. The `setTwig` call was removed from the service definition; the revocation form works again on 6.7.
- Fix: `CaptchaExceptionSubscriber` referenced the no-longer-existing class `Shopware\Storefront\Framework\Captcha\Exception\CaptchaInvalidException` (static-analysis warning `class.notFound`). In Shopware 6.7 this was replaced by the generic `Shopware\Storefront\Framework\Captcha\CaptchaException` carrying an `INVALID_CAPTCHA_ERROR` error code. The subscriber now checks `instanceof CaptchaException && getErrorCode() === CaptchaException::INVALID_CAPTCHA_ERROR`. Behaviour unchanged: captcha failures on the revocation submit route still produce a flash error and redirect back to the form.
- Fix: when updating to a newer version, the plugin-config cards (confirmation-checkbox list, link-hint box) sometimes appeared empty. Root cause: the admin-bundle asset-version cache-buster was not reliably bumped on the update path, so the browser kept serving the previous bundle from cache. The plugin now defensively re-writes the bundle manifest in `postUpdate`, which bumps the cache-buster and makes the browser pull the fresh bundle.

# 4.2.0

- Confirmation-checkbox editor: when editing an existing entry, the language tab opens on the language that already has content (German preferred, then English, otherwise the first locale with text). When adding a new entry or editing an empty one, German is preselected if available in the shop.
- Configurable confirmation checkboxes in the revocation form (plugin settings → "Confirmation checkboxes in revocation form"): per-locale labels, WYSIWYG editor with magic CMS links (`cms://privacy`, `cms://tos`, `cms://imprint`), required/active flags per entry. Default privacy notice provided. Consents the customer ticked at submission time are persisted with a text snapshot in `jga_revocation.accepted_consents` (JSON) and listed in the confirmation email — evidence-grade.
- New plugin settings card "Action on revocation": toggle "Automatically set order status to 'Revoked'" (default on) and toggle "Add tag to order on revocation" (default off, with a pre-selected "Widerruf-Prüfung" tag — admin opts in). Both actions are independent — e.g. tag-only mode for manual review of custom-made products.
- Information box with copy-to-clipboard direct URL for the revocation page in the "Button Placement" settings card. Clearly separated from the footer/account-menu placement toggles, which are only additional surfaces.
- Captcha failures no longer surface as a 403 page: on captcha validation errors the user is redirected back to the form with a flash error message, entered form data preserved.
- Frontend fix: the captcha hidden input (`_grecaptcha_v3`) no longer blocks the submit due to a `required` attribute on a `display:none` element (HTML5 "not focusable" warning).

# 4.1.0

- Captcha validation on the revocation form switched to Shopware's central pipeline (`_captcha` annotation + `CaptchaRouteListener`). The manual captcha loop in the controller (`iterable<AbstractCaptcha>`) is no longer needed; captcha registration is now fully framework-managed. Behaviour unchanged: all active captcha types (Honeypot, Basic Captcha, reCAPTCHA v2/v3) are still checked.

# 4.0.2

- Fix: Header navigation and footer columns are now rendered correctly on the revocation pages (`/revocation`, confirmation, success). Custom themes (e.g. ATMOS, STRATUS, GRAVITY etc.) that rely on `page.header.*` and `page.footer.*` previously received empty values because the controller did not load a `Page` object.

# 4.0.1

- Improved contrast of the revoked order status badge in the order overview
- Aligned revocation button styling in the order list context menu with the native Shopware dropdown
- Updated product description

# 4.0.0

- Initial version for Shopware 6.7
- Support for all Shopware captcha types in the revocation form (Honeypot, BasicCaptcha, Google reCAPTCHA v2/v3)
