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.

Maintainers

Package info

github.com/magendooro/magendoowithdrawal

Type:prestashop-module

pkg:composer/magendoo/magendoowithdrawal

Transparency log

Fund package maintenance!

magendoo.ro

Statistics

Installs: 0

Dependents: 0

Suggesters: 0

Stars: 1

Open Issues: 0

v2.0.1 2026-07-23 14:00 UTC

This package is auto-updated.

Last update: 2026-07-23 14:06:09 UTC


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_compliancy in magendoowithdrawal.php pins min: 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/magendoowithdrawal on Packagist.

Screenshots

Storefront – start Storefront: start / lookup Storefront – form Storefront: withdrawal statement form
Storefront – review Storefront: review before confirming Storefront – success Storefront: success page with reference
Acknowledgment email Acknowledgment-of-receipt e-mail Admin grid Back office: Withdrawal Requests grid
Admin detail Back office: request detail and workflow

Features

  • Storefront flow — guest (order-reference + e-mail lookup) and logged-in customer paths through startformreviewsubmitsuccess front controllers, each a plain ModuleFrontController rendering 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_ack mail templates in mails/en|de|ro/).
  • 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 from PS_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 from PS_OS_CANCELED / PS_OS_REFUND (default shop: 6,7).
  • MAGENDOOWITHDRAWAL_LATE_MODEaccept (accept and flag as late, default) or block (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; 0 keeps 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 as Magendoo\Withdrawal\ via the module's own vendor/autoload.php (Composer, no PrestaShop autoload magic involved).
  • Persistence — Doctrine ORM entities (src/Entity/WithdrawalRequest.php, WithdrawalRequestItem.php) mapped by annotations onto ps_withdrawal_request and ps_withdrawal_request_item, read/written through a shop-scoped Magendoo\Withdrawal\Repository\WithdrawalRequestRepository. There is no legacy ObjectModel for these tables.
  • Servicessrc/Service/ (RequestManager, Notifier, AcknowledgmentSender, Anonymizer, OrderLocator, OrderStateAnchor, RateLimiter, CsrfToken, Labels), wired as public Symfony services in config/common.yml and 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()), not SymfonyContainer::getInstance(), since the latter is null in the default (non-PS_FF_FRONT_CONTAINER_V2) legacy front dispatch.
  • Presentation — legacy ModuleFrontController classes under controllers/front/ (start, form, review, submit, success, cron) rendering Smarty templates under views/templates/front/ and views/templates/hook/; a legacy ModuleAdminController (AdminMagendooWithdrawalController) with a hand-built HelperList grid (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 to ps_orders by 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.