magendoo / magendoowithdrawal
Article 11a CRD withdrawal function for PrestaShop: statutory two-step withdrawal flow with a guaranteed acknowledgment of receipt and a full audit trail.
Package info
github.com/magendooro/magendoowithdrawal
Type:prestashop-module
pkg:composer/magendoo/magendoowithdrawal
Fund package maintenance!
Requires
- php: >=8.1
Requires (Dev)
- phpunit/phpunit: ^13.2
README
EU Article 11a (Directive (EU) 2023/2673) withdrawal function for PrestaShop.
Article 11a, inserted into the Consumer Rights Directive 2011/83/EU by Directive (EU) 2023/2673, requires every B2C shop that concludes distance contracts online to offer a "withdrawal function": a statutory two-step withdraw/confirm flow with a guaranteed acknowledgment of receipt on a durable medium, and a record of the request. The obligation applies from 19 June 2026.
This module implements that function end to end: a storefront statement flow, a receipt-only acknowledgment email, a back-office grid and workflow, and a full audit trail — with no foreign key from the compliance record to the order, so the record survives even if the order is later deleted.
- Compatibility: PrestaShop 9.0.0 and above (
ps_versions_compliancyinmagendoowithdrawal.phppinsmin: 9.0.0,max= the installed core version). PHP 8.1+. - License: OSL-3.0 (Open Software License 3.0).
- Version: 2.0.1.
- Package:
magendoo/magendoowithdrawalon Packagist.
Screenshots
Features
- Storefront flow — guest (order-reference + e-mail lookup) and logged-in customer paths
through
start→form→review→submit→successfront controllers, each a plainModuleFrontControllerrendering Smarty templates. - Statutory verbatim labels — the function link and the confirmation-control label are the
wording from the EUR-Lex Official Journal text, provided in English, German and Romanian
(
Magendoo\Withdrawal\Service\Labels). A merchant may override either label from the configuration screen; leaving them empty keeps the statutory wording. - Receipt-only acknowledgment — the customer e-mail sent after a submission is worded as an
acknowledgment of receipt only; it never says "accepted" or "approved" (
AcknowledgmentSender- the
withdrawal_ackmail templates inmails/en|de|ro/).
- the
- Late submissions accepted and flagged — because the withdrawal period can be extended up
to 12 months under Article 10, a submission made after the statutory period is, by default,
accepted and flagged as late (
is_late) rather than rejected outright; a "block after the period ends" mode is also available. - Per-line withdrawal quantities — a customer can withdraw part of an order; each item line tracks its own withdrawn quantity to 4 decimal places, and remaining withdrawable quantity is recomputed against everything already withdrawn for that order.
- Back-office grid and workflow — Sales → Withdrawal Requests lists every request (shop
scoped) and a detail view drives approve / decline / complete / cancel transitions plus a
manual "resend acknowledgment" action (
AdminMagendooWithdrawalController). - Retention anonymiser — closed requests past a configurable retention window have their
name, e-mail and IP redacted while the compliance skeleton (reference, status, dates, item
lines) is kept (
Magendoo\Withdrawal\Service\Anonymizer). There is no hard-delete path. - Token-guarded cron endpoint — a single URL retries pending acknowledgment e-mails and runs
the retention anonymiser; guarded by a random token generated at install
(
controllers/front/cron.php). - Shop-scoped / multistore-aware — every table and query carries
id_shop; the admin grid and the eligibility/repository services all filter by the current shop context. - UTC evidence timestamps — submission, acknowledgment and anonymisation timestamps are stored as UTC so the audit trail's evidentiary timestamps are unambiguous regardless of the shop's display timezone.
Installation
Requires PHP 8.1+ and PrestaShop 9.0.0 or above. The module is distributed through
Packagist as
magendoo/magendoowithdrawal (type: prestashop-module).
1. Get the module into modules/magendoowithdrawal
Composer-managed PrestaShop project (recommended). PrestaShop core does not place
prestashop-module packages on its own, so use PrestaShop's official
composer-script-handler in your
shop's root composer.json:
{
"require-dev": {
"prestashop/composer-script-handler": "^0.6"
},
"scripts": {
"post-install-cmd": [ "PrestaShop\\Composer\\ScriptHandler::install" ]
},
"extra": {
"prestashop": {
"modules": {
"magendoo/magendoowithdrawal": "^2.0"
}
}
}
}
Then run composer install from the shop root; the handler downloads the package and unpacks
it into modules/magendoowithdrawal/. The shipped vendor/ (the module's own PSR-4
autoloader) is included in the release archive, so no extra Composer step is needed inside the
module.
Manual, without Composer. Download the release archive from Packagist/GitHub and extract it
so the tree lives at modules/magendoowithdrawal/ (the folder name must be exactly
magendoowithdrawal), or upload that ZIP through Modules -> Module Manager -> Upload a
module in the back office.
2. Enable it
php bin/console prestashop:module install magendoowithdrawal
Or from the back office: Modules -> Module Manager, search "Magendoo Withdrawal Function", click Install.
Installation creates two tables (ps_withdrawal_request, ps_withdrawal_request_item),
registers the displayFooter, displayCustomerAccount, displayOrderDetail and
actionFrontControllerSetMedia hooks, seeds the configuration defaults below, and adds the
Withdrawal Requests tab under Sales (formerly Orders) in the back-office menu.
Configuration
Open Modules → Module Manager, find the module, and click Configure. The screen is
organised into four sections, backed by these Configuration keys:
General
MAGENDOOWITHDRAWAL_ENABLED— master on/off switch (default: on).MAGENDOOWITHDRAWAL_PERIOD_DAYS— statutory withdrawal period in days (default: 14).MAGENDOOWITHDRAWAL_TRANSIT_DAYS— extra days added to the delivery anchor before the period starts (default: 0).MAGENDOOWITHDRAWAL_ANCHOR_STATES— comma-separated order-state IDs whose entry starts the period; seeded at install fromPS_OS_SHIPPING/PS_OS_DELIVERED(default shop:4,5). If empty, the period is treated as not started and the order stays eligible.MAGENDOOWITHDRAWAL_EXCLUDED_STATES— comma-separated order-state IDs that are never withdrawable; seeded at install fromPS_OS_CANCELED/PS_OS_REFUND(default shop:6,7).MAGENDOOWITHDRAWAL_LATE_MODE—accept(accept and flag as late, default) orblock(reject once the period has ended).
Presentation
MAGENDOOWITHDRAWAL_SHOW_FOOTER— show the statutory link in the storefront footer (default: on).MAGENDOOWITHDRAWAL_SHOW_REASON— offer the customer an optional reason field on the statement form (default: on; the reason is never mandatory).MAGENDOOWITHDRAWAL_FUNCTION_LABEL/MAGENDOOWITHDRAWAL_CONFIRM_LABEL— overrides for the statutory link text / confirmation-button text (default: empty, i.e. use the built-in verbatim wording).
Notifications
MAGENDOOWITHDRAWAL_MERCHANT_EMAIL— recipient for new-request notifications (default: empty, falls back to the shop's own e-mail address).MAGENDOOWITHDRAWAL_NOTIFY_MERCHANT/MAGENDOOWITHDRAWAL_NOTIFY_CUSTOMER— toggle merchant new-request e-mails and customer status-change e-mails (both default: on).MAGENDOOWITHDRAWAL_ACK_MAX_ATTEMPTS— retry ceiling for the acknowledgment e-mail before it is treated as failed (default: 5).MAGENDOOWITHDRAWAL_ALERT_EMAIL— address alerted once an acknowledgment exhausts its retries (default: empty, falls back to the merchant recipient).
Privacy & anti-abuse
MAGENDOOWITHDRAWAL_RETENTION_DAYS— days after which closed requests are anonymised;0keeps records indefinitely (default: 0).MAGENDOOWITHDRAWAL_RATE_ATTEMPTS/MAGENDOOWITHDRAWAL_RATE_WINDOW— guest lookup rate limit: max attempts (default: 10) per window in seconds (default: 60), per IP.
Two more keys exist but are not on the form: MAGENDOOWITHDRAWAL_CRON_TOKEN (a random token
generated at install, used to authorise the cron endpoint) and MAGENDOOWITHDRAWAL_BO_LINK (a
cached, token-less back-office deep link, captured the first time an administrator opens the
grid, used to build the link inside merchant-notification e-mails).
The Withdrawal Requests grid and workflow live at Sales → Withdrawal Requests in the back office.
Cron
The configuration screen displays the exact URL to schedule, built from the module's own token:
index.php?fc=module&module=magendoowithdrawal&controller=cron&token=<MAGENDOOWITHDRAWAL_CRON_TOKEN>
Hitting it (e.g. every 5 minutes from an external scheduler) retries any pending acknowledgment
e-mails up to MAGENDOOWITHDRAWAL_ACK_MAX_ATTEMPTS attempts and runs the retention anonymiser
against MAGENDOOWITHDRAWAL_RETENTION_DAYS. The request must present the correct token
(hash_equals) or it is rejected with 403 Forbidden.
Architecture
- Domain — framework-free plain PHP under
src/Domain/(Eligibility,EligibilityResult,StatusMachine,SubmissionData,WithdrawalStatus), PSR-4 autoloaded asMagendoo\Withdrawal\via the module's ownvendor/autoload.php(Composer, no PrestaShop autoload magic involved). - Persistence — Doctrine ORM entities (
src/Entity/WithdrawalRequest.php,WithdrawalRequestItem.php) mapped by annotations ontops_withdrawal_requestandps_withdrawal_request_item, read/written through a shop-scopedMagendoo\Withdrawal\Repository\WithdrawalRequestRepository. There is no legacy ObjectModel for these tables. - Services —
src/Service/(RequestManager,Notifier,AcknowledgmentSender,Anonymizer,OrderLocator,OrderStateAnchor,RateLimiter,CsrfToken,Labels), wired as public Symfony services inconfig/common.ymland imported into both the front and admin containers (config/front/services.yml,config/admin/services.yml). Front and admin controllers resolve them from the current controller's own container (Controller::getContainer()), notSymfonyContainer::getInstance(), since the latter isnullin the default (non-PS_FF_FRONT_CONTAINER_V2) legacy front dispatch. - Presentation — legacy
ModuleFrontControllerclasses undercontrollers/front/(start,form,review,submit,success,cron) rendering Smarty templates underviews/templates/front/andviews/templates/hook/; a legacyModuleAdminController(AdminMagendooWithdrawalController) with a hand-builtHelperListgrid (there being no ObjectModel to drive the default one) and a detail view driving the status workflow. - Datetimes are stored and compared as UTC strings throughout, so audit-trail evidence timestamps do not depend on the shop's configured display timezone.
- Install/uninstall SQL lives in
sql/install.php/sql/uninstall.php; both tables are created with no foreign key tops_ordersby design, so a compliance record outlives the order it references.
Testing
cd modules/magendoowithdrawal
composer install
php vendor/bin/phpunit
This runs the 77-test unit suite under tests/Unit/ (pure domain logic — Eligibility,
StatusMachine, WithdrawalStatus, Labels, OrderStateAnchor, RateLimiter,
SubmissionData — no database or PrestaShop bootstrap required).
tests/Integration/ contains additional scripts (repository, entity write-path,
acknowledgment sender, anonymiser, front-office smoke tests) that exercise the module against a
running PrestaShop install with a live database and a mail catcher; they are not part of the
default phpunit.xml.dist test suite and require that environment to be booted first.
License
Released under the Open Software License 3.0 (OSL-3.0), matching the license
declared in composer.json.






