# magendoo/module-withdrawal

> EU withdrawal function (Article 11a, Directive 2011/83/EU as amended by Directive (EU) 2023/2673) for Magento 2: statutory two-step withdrawal flow, acknowledgment of receipt on a durable medium, and a full audit trail

`composer require magendoo/module-withdrawal`

Canonical URL: https://packagento.com/magendoo/module-withdrawal

## At a glance

- **Vendor**: magendoo (https://packagento.com/magendoo.md)
- **Latest version**: v1.1.0 — released 2026-07-22
- **Pricing**: Free
- **Package type**: Magento 2 module
- **Status**: active, accepting new buyers

## Installation

Packagento is licence-gated, so even free packages need a licence on a project before Composer can resolve them.

1. **Sign in or create an account** at https://packagento.com/customer/account/.

2. **Add the package to your account.** Open https://packagento.com/magendoo/module-withdrawal and complete the free checkout. A licence is minted automatically.

3. **Create or pick a project, then activate the licence on it.**
   - Projects represent the Magento installs you deploy to. Manage them at https://packagento.com/projects/.
   - Activate the new licence on the project you'll deploy this package to. Activation is what generates the Composer credentials scoped to that project.

4. **Add the project credentials to your Magento codebase.**

   Grab the project's public + private key from https://packagento.com/projects/ (open the project, then its Credentials tab), and add them to `auth.json`:

   ```json
   {
     "http-basic": {
       "packagento.com": {
         "username": "ppk_live_...",
         "password": "psk_live_..."
       }
     }
   }
   ```

   Add the Packagento Composer repository to `composer.json`:

   ```json
   {
     "repositories": [
       { "type": "composer", "url": "https://packagento.com" }
     ]
   }
   ```

5. **Install and apply.**

   ```bash
   composer require magendoo/module-withdrawal:*
   bin/magento setup:upgrade
   bin/magento setup:di:compile
   bin/magento cache:flush
   ```

## What it does

EU withdrawal function (Article 11a, Directive 2011/83/EU as amended by Directive (EU) 2023/2673) for Magento 2: statutory two-step withdrawal flow, acknowledgment of receipt on a durable medium, and a full audit trail

## README

**EU withdrawal function ("withdrawal button") for Magento 2** — implements Article 11a of the
Consumer Rights Directive 2011/83/EU, as inserted by Directive (EU) 2023/2673.

From **19 June 2026**, every B2C shop that concludes distance contracts through an online interface
must offer a *withdrawal function* wherever a right of withdrawal exists: a continuously available,
prominently displayed entry point labeled **"withdraw from contract here"** (or an unambiguous
corresponding formulation), a statement form asking only for the consumer's name, contract details,
and an email address, a confirmation step labeled **"confirm withdrawal"**, and an acknowledgment of
receipt on a durable medium containing the statement's content and the date and time of its
submission. This module adds exactly that to a Magento 2 storefront — the two-step statutory flow
for guests and logged-in customers, an undroppable acknowledgment email with automatic retries, a
full audit trail, an admin processing workflow, and privacy-aware data retention. It supplements —
it does not replace — the existing Article 11 withdrawal channels (the model withdrawal form of
Annex I(B) and free-form statements).

> **Legal notice**
>
> This module helps implement the Article 11a withdrawal function. It is **not legal advice**, and
> installing it does not by itself make a shop compliant. Wording, placement, scope (which of your
> products carry a withdrawal right), your withdrawal policy, and your terms and conditions remain
> your responsibility — have your implementation reviewed by legal counsel before 19 June 2026.

See also: [Merchant guide](docs/merchant-guide.md) (non-technical, how to process requests) and
[Legal background](docs/legal-background.md) (the statutory requirements and how the module maps to
them).

### Screenshots

| Storefront | Admin |
|---|---|
| ![Start page](docs/img/storefront-start.jpg) | ![Request grid](docs/img/admin-grid.jpg) |
| ![Statement form](docs/img/storefront-form.jpg) | ![Request view](docs/img/admin-view.jpg) |
| ![Review and confirm](docs/img/storefront-review.jpg) | ![Configuration](docs/img/admin-config.jpg) |
| ![Footer link](docs/img/storefront-footer-link.jpg) | ![Acknowledgment email](docs/img/email-acknowledgment.jpg) |

### Features

- **Statutory two-step flow** for both guests and logged-in customers: statement form → review page
  with a single confirmation button → success page with reference and submission timestamp. All
  flow pages are marked `cacheable="false"`, so the module is safe to run with full-page cache.
- **Continuous entry points**: a visually distinct footer link on every page (configurable), a
  My Account navigation link, plus a state-aware Withdrawal column in My Account → My Orders and
  an action link on the order view page that deep-link logged-in customers straight into the
  prefilled statement form. Guests reach the flow through an order number + email lookup — no
  login required, email never appears in a URL, lookups are rate-limited per IP with a uniform
  "not found" message to prevent order enumeration.
- **Statutory labels** sourced from the Official Journal language versions of Directive (EU)
  2023/2673 for English, German, and Romanian (see [i18n coverage](#i18n-coverage)). Per-store-view
  override fields exist for merchants whose counsel approves an "unambiguous corresponding
  formulation".
- **Acknowledgment of receipt** on a durable medium (email), sent immediately on submission,
  containing the full statement content and the date **and time** of submission (store timezone and
  UTC). Receipt-only wording — the email never implies the withdrawal was accepted. Delivery
  failures are persisted and retried by cron every 5 minutes; when the retry limit is exhausted, an
  alert email is sent to the merchant.
- **Persist-first design**: the request and its items are saved in one database transaction before
  any email is attempted. A mail failure can never lose a statement. Compliance records have no
  foreign keys into the sales tables, so they survive order deletion.
- **Partial withdrawal**: per-item quantity selection, with already-withdrawn quantities locked and
  multiple partial requests per order supported.
- **Late submissions accepted and flagged by default**: under Article 10, the withdrawal period is
  extended by up to 12 months if the consumer was not properly informed, so hard-blocking after the
  computed deadline risks rejecting statements that are still within the statutory period. A
  `block` mode is available for merchants who prefer it.
- **Admin workflow**: grid under Sales → Withdrawal Requests, request view with the statement
  snapshot exactly as submitted, and guarded status transitions — Approve, Decline (a written note
  is required), Complete, Cancel, Resend Acknowledgment. A "Create Credit Memo" link opens
  Magento's own credit memo form; the module does no refund math of its own.
- **Audit trail**: every request stores a JSON snapshot of the statement as the consumer saw it
  (including the labels displayed and the locale), the submission timestamp (UTC), IP and user
  agent; every submission and status change is recorded as an order status-history comment.
- **Privacy / GDPR**: an optional retention period after which name, email, IP, user agent, and the
  consumer note of closed requests are anonymized (daily cron and CLI, with dry run). The
  compliance record itself is kept.
- **Integrations**: three CLI commands, an admin-token REST endpoint, and service contracts
  (`Api/`) for programmatic access.
- Optional reason field (never required — Article 11a allows no preconditions beyond
  identification), optional consumer note, merchant notification email, and customer status-update
  emails.
- 185 unit tests; verified end-to-end (guest and logged-in flows, acknowledgment delivery, retry
  cron, admin transitions, REST, CLI).

### Requirements

_(README truncated for .md surface. Full README on https://packagento.com/magendoo/module-withdrawal.)_

## Recent Versions

| Version | Released |
|---|---|
| v1.1.0 | 2026-07-22 |
| v1.0.0 | 2026-07-21 |

## Dependencies

### Require

| Package | Constraint |
|---|---|
| magento/framework | >=103.0.6 |
| magento/module-email | >=101.1.6 |
| magento/module-sales | >=103.0.6 |
| magento/module-store | >=101.1.6 |
| php | ~8.2.0\|\|~8.3.0\|\|~8.4.0 |

## Quality

Latest release (v1.1.0) fails the Packagento QA pipeline. Verdicts below are per-cell (Magento line × PHP version) for the matrixed tools, and run-once for the static / security tiers.


### Compatibility

Each Magento line is installed on its supported PHP versions, then the module is built (DI compile + static-content deploy). Cells show passed / failed / untested; staircase gaps render as `–`.

| Magento | PHP 8.2 | PHP 8.3 | PHP 8.4 | PHP 8.5 |
|---|---|---|---|---|
| 2.4.7 | Pass | Pass | – | – |
| 2.4.8 | – | Pass | Pass | – |
| 2.4.9 | – | – | Pass | not tested |


### Code Quality

Advisory checks against the module's source. Never affect the Compatibility verdict — a phpcs finding can't make a module incompatible.

#### Static Analysis

Coding standards (phpcs), mess detection (phpmd), copy-pasted code (cpd), PHP cross-version compatibility, composer.json validity. Each runs once for the whole module.

| Tool | Status | Findings | Summary |
|---|---|---|---|
| PHPCS | Warning | 36 | 36 warnings (ruleset: Magento2) |
| PHPMD | Warning | 105 | 105 rule violations (UnusedPrivateField:105) |
| Cpd | Warning | 1 | 1 duplicated chunk spanning 26 total lines (min-lines=5, min-tokens=70) |
| Composer validate | Info | 4 | valid; 4 advisory notes (composer validate --strict) |

#### PHPStan

Type-checks the module against a real Magento install. Re-runs per Magento + PHP version because resolvable symbols differ between releases.

| Magento | PHP 8.2 | PHP 8.3 | PHP 8.4 | PHP 8.5 |
|---|---|---|---|---|
| 2.4.7 | 3 | 3 | – | – |
| 2.4.8 | – | 3 | 3 | – |
| 2.4.9 | – | – | 3 | N/A |


### Tests

Unit and integration suites run per Magento + PHP cell. Test failures speak to the module's behaviour, not its compatibility with a line, so they're reported here separately.

#### Unit Tests

| Magento | PHP 8.2 | PHP 8.3 | PHP 8.4 | PHP 8.5 |
|---|---|---|---|---|
| 2.4.7 | N/A | N/A | – | – |
| 2.4.8 | – | N/A | N/A | – |
| 2.4.9 | – | – | N/A | N/A |

#### Integration Tests

| Magento | PHP 8.2 | PHP 8.3 | PHP 8.4 | PHP 8.5 |
|---|---|---|---|---|
| 2.4.7 | N/A | N/A | – | – |
| 2.4.8 | – | N/A | N/A | – |
| 2.4.9 | – | – | N/A | N/A |


### Security

Dependency-advisory audit (composer audit) plus a source malware scan. A malware detection fails the version outright.

| Tool | Status | Findings | Summary |
|---|---|---|---|
| Composer audit | Pass | 0 |  |
| Malware scan | Pass | 0 |  |

## Licence and pricing

Free. A licence is still minted on checkout and bound to your project for Composer access — no payment step.

Refundable within 14 days of first purchase via https://packagento.com/account/refunds/.

## Install via Claude Code or any MCP client

The Packagento MCP server can run the licence + project + Composer steps above in one tool call:

```
purchase_and_install_packages(
  composer_names=["magendoo/module-withdrawal"],
  project_id="proj_xxx"
)
```

This handles cart, checkout, licence minting, project activation, and writes auth.json credentials. Connect a client with `claude mcp add packagento https://mcp.packagento.com`. Full setup at https://packagento.com/docs/mcp-setup.

## Vendor

magendoo is a Magento 2 vendor on Packagento. See https://packagento.com/magendoo.md for their full catalogue.

