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
disabledstate now only applies on 2.4.7+ (ButtonLockManager re-enables it). Adds aMagentoVersionview model and removes the obsoleteenable-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_formsetting (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/frameworkto 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 onMagento_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
selectCSS class on the withdrawal form. The item-selection column in the legacy (Luma)withdrawal/view.phtmlusedclass="col select", which Luma styles as a<select>dropdown and made the checkbox cell render incorrectly. Renamed tocol-withdrawal-select. (Hyvä templates were unaffected.)
Changed¶
- Inline styles moved to
view/frontend/web/css/withdrawal.cssacross the Luma templateswithdrawal/view.phtml,withdrawal/success.phtml,request/success.phtml, andrequest/form.phtml. Removes allstyle="…"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 thehyva_defaultlayout handle that Hyvä themes apply on every frontend request. No separate Composer package needed. - Partial (item-level) withdrawals. New
copex_orderwithdrawal_itemstable with FK cascade onwithdrawal_id. Per-store config flagcopex_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/cancellationFormstreams Annex I (B) of Directive 2011/83/EU as a one-page PDF. Withoutorder_idit serves a blank form; withorder_idand a logged-in, matching customer, the form is pre-filled with order number, order date, and consumer name. Rendered viampdf/mpdf(new Composer dependency) on a small HTML template — picked over the deprecatedZend_Pdfso the module survives the upcoming Magento 2.4.9 cleanup that drops the Zend Framework 1 stack. - Auto-refund.
Model\RefundCreatorbuilds a Magento credit memo for a confirmed withdrawal viaCreditmemoFactory::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.graphqlsdefines: - Query
copexCustomerWithdrawals— list withdrawals for the logged-in customer. - Mutation
submitCopexWithdrawal(input: SubmitCopexWithdrawalInput!)— mirror of the registered-customer submit flow. - Type
CopexWithdrawalwith aitemsfield that resolves on demand. - REST API extension. New endpoint
GET /V1/orderwithdrawal/withdrawals/:id/items, ACL-gated underCopeX_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 inpending(orneeds_validationfor 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/massConfirmandwithdrawal/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 (
pendingorneeds_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 viaButtonProviderInterfaceblocks underBlock\Adminhtml\Withdrawal\Edit\*Button.php. - Customer notification email on status transition. When an admin
moves a withdrawal into
confirmedorrejected, 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'scopex_orderwithdrawal_save_afterevent 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 newStores > Configuration > Sales > Withdrawal Settings > Email Settingsfields; 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_GraphQladded toetc/module.xmlsequence — required for the new resolvers. - WithdrawalInterface grew
IS_PARTIAL+ITEMSconstants and the matchingisPartial()/setIsPartial()/getItems()/setItems()methods. Existing get/save flows continue to work; the new fields are optional on input and default tofalse/[]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
Nooptreqsource). 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 withingrace_period_daysare accepted and flaggedneeds_validationfor admin review of the actual delivery date. - Cron-driven cleanup of abandoned
awaiting_confirmationrows:copex_orderwithdrawal_cleanup_expired_confirmationsruns daily at 02:00 and deletes rows whose token expired more thanexpired_token_grace_hoursago. - Status lifecycle:
awaiting_confirmation→pending(orneeds_validationfor 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\WithdrawalSubmitterorchestrates the registered flow;Model\ReasonResolvervalidates per-mode reason input;Model\ReasonsListParserdeserialises admin reasons config;Model\CustomerNameResolveris shared between flows;ViewModel\Withdrawal\ReasonFieldhandles form rendering. Controllers stay thin — HTTP gatekeeping plus exception → redirect mapping. - Full CRUD REST API at
/V1/orderwithdrawal/withdrawals, gated by ACLCopeX_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 atTest/Playwright/driving both customer flows end-to-end through the local mailcatcher. - DE/EN translations and DE/EN operator manual under
docs/.