Skip to content

Changelog

All notable changes to copex/module-order-withdrawal are documented in this file. Format follows Keep a Changelog 1.1.0; versioning follows Semantic Versioning 2.0.0.

[1.2.8] – 2026-06-18

Fixed

  • Admin approval/rejection emails now use the customer’s store-view language. The withdrawal’s store_id is saved on creation and used for emails; legacy withdrawals fall back to the default store view.

[1.2.7] – 2026-06-17

Fixed

  • reCAPTCHA submit button no longer stays permanently disabled on Magento ≤ 2.4.6; the server-side disabled state now only applies on 2.4.7+ (ButtonLockManager re-enables it). Adds a MagentoVersion view model and removes the obsolete enable-submit-on-recaptcha.js.

[1.2.6] – 2026-06-17

Changed

  • Hardened the public-form email confirmation flow; confirmation is now idempotent.
  • Confirmation success page now uses first-person wording.

[1.2.5] – 2026-06-15

Changed

  • Fix typed constant for compatibility with php 8.2

[1.2.4] – 2026-06-15

Added

  • Withdrawal request form for logged-in customers, configurable via the registered_form setting (default off). Account email shown read-only; order number validated by customer id.

Changed

  • Logged-in submits skip double opt-in (filed immediately); failed submits return to the form.
  • Compatibility with Magento 2.4.6: replace ButtonLockManager with isCaptchaEnabled in frontend forms and viewModel

[1.2.3] – 2026-06-11

Changed

  • Lowered minimum PHP requirement from 8.3 to 8.2.
  • Raised minimum magento/framework to 103.0.6 (Magento 2.4.6).

[1.2.2] – 2026-06-11

Changed

  • Show prices including tax in the withdrawal request.

[1.2.1] – 2026-05-30

Added

  • Form intro text. New store-scoped Form Intro Text setting (copex_orderwithdrawal/messages/form_intro_text) shown above the public withdrawal form (Luma + Hyvä). Rendered through the CMS block filter, so Magento directives such as {{store url=""}}, {{config path=""}} or {{widget ...}} resolve. Adds a dependency on Magento_Cms.

Documentation

  • Operator manual (EN/DE): documented the new intro text and added an info block on creating a custom internal URL rewrite (Redirect Type: No) so the form can be served under your own path (e.g. /widerruf).

[1.2.0] – 2026-05-30

A feature release. Additive and backward-compatible: the new option defaults to the previous behaviour (period anchored on the shipment date), so existing installs are unaffected until an admin opts in.

Added

  • Configurable withdrawal-period start date. New store-scoped settings under Withdrawal Settings → General: Withdrawal Period Starts From (copex_orderwithdrawal/general/period_start_basis, default Shipment date) and Period Start Status (copex_orderwithdrawal/general/period_start_status). When set to Order status change date, the withdrawal period is counted from the date the order entered the chosen status (read from the order status history). If that status has not been set, the calculation falls back to the latest shipment date, and an order that has not shipped stays withdrawable at any time — unchanged from the shipment-date default.

[1.1.1] – 2026-05-29

A frontend-only maintenance release. No schema, API, or PHP behaviour changes — storefront markup and styles only. Run setup:static-content:deploy (or clear the frontend cache in developer mode) after deploying so the updated CSS is published.

Fixed

  • Reserved select CSS class on the withdrawal form. The item-selection column in the legacy (Luma) withdrawal/view.phtml used class="col select", which Luma styles as a <select> dropdown and made the checkbox cell render incorrectly. Renamed to col-withdrawal-select. (Hyvä templates were unaffected.)

Changed

  • Inline styles moved to view/frontend/web/css/withdrawal.css across the Luma templates withdrawal/view.phtml, withdrawal/success.phtml, request/success.phtml, and request/form.phtml. Removes all style="…" attributes from these templates so they no longer trip a Content Security Policy in restrict mode. Visual output is unchanged.

[1.1.0] – 2026-05-21

A feature-add release. Schema changes are additive (one new column on the existing table, one new sibling table) — existing 1.0.0 rows continue to work unchanged. New API methods on WithdrawalInterface are additive: third-party implementations need to add isPartial(), setIsPartial(), getItems(), and setItems(), but built-in callers fall back gracefully on empty values.

Added

  • Hyvä theme support built into the same module. Tailwind/Alpine templates under view/frontend/templates/hyva/*, activated via the hyva_default layout handle that Hyvä themes apply on every frontend request. No separate Composer package needed.
  • Partial (item-level) withdrawals. New copex_orderwithdrawal_items table with FK cascade on withdrawal_id. Per-store config flag copex_orderwithdrawal/general/allow_partial_withdrawal (default off). Item-availability calculator (Model\WithdrawalItemAvailability) subtracts prior partial submissions so the storefront and validator agree on what is still withdrawable. Selecting every available unit of every item collapses back to a full withdrawal (is_partial = 0).
  • 24 EU language translations: bg, cs, da, el, es, et, fi, fr, ga, hr, hu, it, lt, lv, mt, nl, pl, pt, ro, sk, sl, sv (de + en updated). Translations use the legal-transposition terminology of EU Directive 2011/83/EU as adopted by each member state (e.g. PT "livre resolução", ES "derecho de desistimiento", IT "diritto di recesso").
  • PDF model withdrawal form download. GET /withdrawal/document/cancellationForm streams Annex I (B) of Directive 2011/83/EU as a one-page PDF. Without order_id it serves a blank form; with order_id and a logged-in, matching customer, the form is pre-filled with order number, order date, and consumer name. Rendered via mpdf/mpdf (new Composer dependency) on a small HTML template — picked over the deprecated Zend_Pdf so the module survives the upcoming Magento 2.4.9 cleanup that drops the Zend Framework 1 stack.
  • Auto-refund. Model\RefundCreator builds a Magento credit memo for a confirmed withdrawal via CreditmemoFactory::createByOrder(). For partial withdrawals it maps the withdrawal-item rows onto the qtys array, for full withdrawals it credits every invoiced item. New admin action "Create Creditmemo" surfaces only on confirmed rows linked to an order.
  • GraphQL API. etc/schema.graphqls defines:
  • Query copexCustomerWithdrawals — list withdrawals for the logged-in customer.
  • Mutation submitCopexWithdrawal(input: SubmitCopexWithdrawalInput!) — mirror of the registered-customer submit flow.
  • Type CopexWithdrawal with a items field that resolves on demand.
  • REST API extension. New endpoint GET /V1/orderwithdrawal/withdrawals/:id/items, ACL-gated under CopeX_OrderWithdrawal::withdrawals.
  • Optional double opt-in toggle. New config copex_orderwithdrawal/general/require_email_confirmation (default Yes). When set to No, the public form skips the tokenised confirmation email and creates the withdrawal directly in pending (or needs_validation for unresolved orders). The customer-receipt + admin notification emails still go out — Art. 11 EU 2011/83/EU durable-medium acknowledgement stays mandatory.
  • Admin grid Type column (Full / Partial), filterable.
  • Admin grid Order-Status column with native Magento order-status filter.
  • Mass actions "Approve" and "Decline" on the admin grid (withdrawal/index/massConfirm and withdrawal/index/massReject). Skipped rows whose current status doesn't permit the transition are reported as a notice.
  • Per-row "Approve" / "Decline" actions in the admin grid actions column. Surface only when the row's status (pending or needs_validation) permits the transition; the controller revalidates server-side.
  • Admin detail page at withdrawal/index/edit/id/N. Editable fields: status, customer name, reason label, comment. Reserved fields (token, order_id, created_at) are read-only and ignored on POST even if a tampered payload contains them. The form's Approve/Decline buttons render conditionally via ButtonProviderInterface blocks under Block\Adminhtml\Withdrawal\Edit\*Button.php.
  • Customer notification email on status transition. When an admin moves a withdrawal into confirmed or rejected, the customer automatically receives a tailored email — approval template tells them the refund is on its way, rejection template explains typical reasons and offers a reply path. Triggered by an observer on the model's copex_orderwithdrawal_save_after event so every transition path is covered (single-row buttons, mass actions, inline-edit, detail-page Save, REST, GraphQL, or any custom script). Templates are configurable via two new Stores > Configuration > Sales > Withdrawal Settings > Email Settings fields; failures are logged but do not roll back the status change.
  • Unit tests (PHPUnit 9.6): WithdrawalItem entity, WithdrawalItemAvailability, WithdrawalType source model, RefundCreator (full + partial + stale-item defence), and extended ConfigTest for the new getters. 57 tests / 130 assertions, all green.
  • Playwright E2E specs for the partial-withdrawal customer flow (item-checkbox deselection, qty input, success + receipt email) and for the PDF endpoint (anonymous blank form + authenticated pre-filled, verified via %PDF- magic-number check).

Changed

  • Public withdrawal form is now ON by default (copex_orderwithdrawal/general/public_form = 1). Previously 0. The 19 June 2026 EU directive deadline makes the public form a baseline expectation, not an opt-in extra. Existing installs that explicitly set the flag in admin retain their value; installs that never touched the field get the new default.
  • Double opt-in is now ON by default via the new copex_orderwithdrawal/general/require_email_confirmation = 1. Pairs with the public-form default above so a fresh install ships in the safest configuration.
  • Module dependency on Magento_GraphQl added to etc/module.xml sequence — required for the new resolvers.
  • WithdrawalInterface grew IS_PARTIAL + ITEMS constants and the matching isPartial() / setIsPartial() / getItems() / setItems() methods. Existing get/save flows continue to work; the new fields are optional on input and default to false / [] on read.
  • PHP-CS / phpinsights pass: same-line opening braces on multi-line constructor signatures, ordered imports. Scores after the auto-fix: Code 89.8%, Complexity 74.5%, Architecture 82.4%, Style 98.8%.

Database

Schema changes are picked up by bin/magento setup:upgrade automatically. No data migration required.

copex_orderwithdrawal:
  + is_partial smallint unsigned NOT NULL DEFAULT 0

copex_orderwithdrawal_items (new):
  entity_id        int unsigned PK, identity
  withdrawal_id    int unsigned NOT NULL  -- FK CASCADE on copex_orderwithdrawal.entity_id
  order_item_id    int unsigned NOT NULL
  order_item_name  varchar(255) NULL
  order_item_sku   varchar(255) NULL
  qty_withdrawn    decimal(12,4) unsigned NOT NULL DEFAULT 1
  created_at       timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP

Upgrade notes

If you have third-party code that implements WithdrawalInterface directly (rare — most callers use the Withdrawal model), add the four new methods: isPartial(): bool, setIsPartial(bool $isPartial): static, getItems(): array, setItems(array $items): static.

If you have third-party code that calls WithdrawalSubmitter::submitForRegisteredCustomer(): the new fourth parameter $postedItems defaults to [], so existing call sites continue to work without changes.

If you rely on the public-form behaviour being strictly double-opt-in, your current behaviour is preserved: the new require_email_confirmation config defaults to Yes.

If your 1.0.0 install had public_form explicitly disabled in the admin, your saved value is retained on upgrade — only installs that never touched the field receive the new default of 1. To preserve the 1.0.0 default behaviour explicitly, save Enable Public Withdrawal Form to "No" in Stores > Configuration > Sales > Withdrawal Settings before upgrading.

[1.0.0] – 2026-04

Initial release as CopeX_OrderWithdrawal.

Added

  • Logged-in customer flow: order-history column, order-view button, withdrawal detail page with confirm-then-submit, immediate email dispatch, success page.
  • Public form flow with double opt-in: order # + email + reCAPTCHA, tokenised email confirmation, configurable token expiry, resubmit-resends-email behaviour, resilience to invalid order numbers.
  • "Reason for Withdrawal" field on both flows. Configurable per store view: No / Optional / Required (Magento Nooptreq source). Reasons stored as stable codes plus a label snapshot — historical rows survive admin edits and deletions of reasons. Reason column visible in the admin grid; reason label exposed to all three email templates.
  • Eligibility check with a grace window: standard deadline = shipment + expected_shipping_days + withdrawal_period_days; submissions past that but within grace_period_days are accepted and flagged needs_validation for admin review of the actual delivery date.
  • Cron-driven cleanup of abandoned awaiting_confirmation rows: copex_orderwithdrawal_cleanup_expired_confirmations runs daily at 02:00 and deletes rows whose token expired more than expired_token_grace_hours ago.
  • Status lifecycle: awaiting_confirmationpending (or needs_validation for grace-zone submissions and unresolved-order rows) → confirmed / rejected.
  • Admin grid under Sales > Withdrawals with inline status edit, conditional view-order action, an autocomplete editor for assigning orders to unresolved rows, and a text-filterable reason column.
  • Customer + admin email notifications; admin only notified after customer confirmation in the public flow.
  • Order-history comment added on confirmation.
  • Configurable: period, expected shipping days, grace period, allowed order statuses, sender, templates, form messages, confirmation expiry, expired-token cleanup grace, reasons mode, reasons list per store view.
  • Service-oriented architecture: Model\WithdrawalSubmitter orchestrates the registered flow; Model\ReasonResolver validates per-mode reason input; Model\ReasonsListParser deserialises admin reasons config; Model\CustomerNameResolver is shared between flows; ViewModel\Withdrawal\ReasonField handles form rendering. Controllers stay thin — HTTP gatekeeping plus exception → redirect mapping.
  • Full CRUD REST API at /V1/orderwithdrawal/withdrawals, gated by ACL CopeX_OrderWithdrawal::withdrawals. Sensitive token fields are excluded from the data interface.
  • reCAPTCHA enforced server-side on the public form via predispatch observer.
  • Withdrawal period counted from the latest shipment date (per EU Directive 2011/83/EU); always allowed for orders not yet shipped.
  • Tests: 40 unit tests at Test/Unit/. Playwright E2E suite at Test/Playwright/ driving both customer flows end-to-end through the local mailcatcher.
  • DE/EN translations and DE/EN operator manual under docs/.