=== MX Contract Withdrawal for WooCommerce ===
Contributors: mediax
Tags: woocommerce, withdrawal, right-of-withdrawal, consumer-rights, ecommerce
Requires at least: 6.5
Tested up to: 7.0
Requires PHP: 8.0
Stable tag: 2.3.0
WC requires at least: 8.0
WC tested up to: 10.8
License: GPLv2 or later
License URI: https://www.gnu.org/licenses/gpl-2.0.html

Provides a privacy-first technical workflow for receiving, recording, and reviewing contract withdrawal requests. This plugin is not a legal compliance tool and does not provide legal advice.

== Description ==

MX Contract Withdrawal for WooCommerce is built to help WooCommerce merchants support the electronic withdrawal function introduced by Directive (EU) 2023/2673, applicable from 19 June 2026. Merchants should verify final legal requirements for their jurisdiction.

The plugin provides a technical workflow for receiving, recording, and reviewing contract withdrawal requests. It is not a legal compliance tool and does not provide legal advice.

The product direction focuses on a two-step withdrawal flow, guest secure-link verification, customer receipt email with durable-medium style confirmation, admin review, and audit records. Croatian-first merchant details include OIB / Tax ID, registered seat, contact details, and return address handling. Possible exceptions and uncertain cases are handled conservatively through fail-open/manual review wording and status handling.

== Installation ==

1. Upload or keep the plugin folder in `wp-content/plugins/mx-contract-withdrawal`.
2. Activate the plugin in WordPress.
3. Make sure WooCommerce is active.
4. Go to WooCommerce > Withdrawal Settings and select an existing public page, or create a draft withdrawal page from the plugin settings.
5. Publish the selected page when you are ready for customers to use the form.

== Basic Setup ==

On activation the plugin creates or updates its custom tables and default settings only. It does not automatically publish or create a public withdrawal page.

Merchants must select an existing page or manually create a withdrawal page from WooCommerce > Withdrawal Settings. The manual create action creates a draft page containing:

`[mx_contract_withdrawal_form]`

The created page title and slug are generated from a translatable source string, so localized stores can use localized page names.

== Shortcode ==

Use this shortcode where customers should submit requests:

`[mx_contract_withdrawal_form]`

== Cache Compatibility ==

The withdrawal page can show customer-specific order and declaration details after verification, especially for guest requests. Exclude the configured withdrawal page URL from page cache before going live.

Always exclude the configured withdrawal page path from full-page cache. If your cache supports combined URL and cookie conditions, limit any `mx_cw_access_token` cookie bypass to the withdrawal page or other MX Contract Withdrawal routes. Do not bypass unrelated pages solely because the cookie is present. Ensure plugin-specific sensitive query routes, public actions, and customer print/status routes also bypass cache where supported. Review rules in page-cache tools such as WP Rocket, LiteSpeed Cache, Cloudflare APO, Varnish/server cache, and managed host caches.

If your store is behind Cloudflare, nginx reverse proxy, load balancer, or managed host proxy, configure real visitor IP handling at the infrastructure level or via the `mx_contract_withdrawal/client_ip` filter. The plugin defaults to `REMOTE_ADDR` and does not blindly trust forwarded headers.

== Update Checks and Privacy ==

The plugin can contact the Media X update server to check whether a plugin update is available. Update checks may include the site URL, plugin version, and basic build information. This is used only to provide available update information.

== Data Retention and Uninstall ==

Withdrawal request, item, and audit records are retained according to the plugin's retention/anonymization behavior. Uninstalling the plugin does not automatically remove those records by default, because stores may need operational request history. Review the retention settings and your database cleanup process before removing stored data.

Retention maintenance uses WooCommerce Action Scheduler when available and keeps a bounded WP-Cron fallback. Action Scheduler improves batching and visibility, but production stores should still use a reliable server cron or WP-CLI queue runner; low-traffic sites that rely only on traffic-triggered WP-Cron may see delayed cleanup.

== Supporting Attachment Storage ==

When optional supporting attachments are enabled, uploaded files are stored in the standard WordPress uploads directory under `mx-contract-withdrawal/YYYY/MM/`. Stored filenames receive a random prefix to reduce collisions and guessability, but file URLs should still be treated as sensitive operational data.

The plugin UI serves attachments through an authenticated admin proxy that requires WooCommerce management capability and request-specific verification. The request detail screen no longer outputs raw attachment upload URLs for previews, opening, or downloads.

The plugin writes Apache `.htaccess` rules and directory index files to reduce direct exposure in its upload folder where supported, and upgrades known legacy plugin rules to deny direct web access to attachment files. This is a conservative layer: on Nginx, LiteSpeed, CDN/proxy deployments, or any server where `.htaccess` is ignored, add server-level rules to deny direct web access to `/wp-content/uploads/mx-contract-withdrawal/` and exclude that directory from public CDN or edge caching. This may still be required to block pre-existing or independently discovered raw upload URLs.

== Custom Order Numbers ==

The public lookup supports standard WooCommerce order numbers and accepts the number with or without the `#` symbol, for example `1234` and `#1234`.

Stores that use custom or sequential order number plugins should test the public withdrawal lookup with the customer-facing order number before going live. Compatibility depends on how the order number plugin stores the display number.

The plugin includes built-in lookup support for a few common custom order number meta keys, including `_order_number`, `_alg_wc_custom_order_number`, and `_ywson_custom_number_order_complete`.

Developers can extend lookup support with these filters:

* `mx_contract_withdrawal/order_lookup_meta_keys`
* `mx_contract_withdrawal/order_number_meta_keys`
* `mx_contract_withdrawal/lookup_order_by_number`

Example: add a custom meta key used by your order number plugin:

```php
add_filter( 'mx_contract_withdrawal/order_lookup_meta_keys', function ( $keys ) {
	$keys[] = '_my_custom_order_number';
	return $keys;
} );
```

`mx_contract_withdrawal/order_number_meta_keys` is a backward-compatible alias applied after `mx_contract_withdrawal/order_lookup_meta_keys`.

Example: resolve the display number with fully custom logic:

```php
add_filter( 'mx_contract_withdrawal/lookup_order_by_number', function ( $order, $order_number, $raw_order_number, $billing_email, $context ) {
	if ( $order instanceof WC_Order ) {
		return $order;
	}

	// Resolve the custom display number to an order ID using your plugin's storage.
	$order_id = 0;

	return $order_id ? wc_get_order( $order_id ) : null;
}, 10, 5 );
```

Resolver results must exist and their customer-facing order number must match the normalized lookup value. Billing email and guest ownership verification continue through the plugin's secure lookup flow.

Improved support for custom WooCommerce order numbers, including popular Sequential/YITH/WPFactory numbering setups and a filter for custom resolvers.

== Current Features ==

* Guest lookup with order number and billing email verification.
* Secure magic-link guest flow with a short-lived access token.
* Logged-in customer My Account withdrawal requests endpoint and eligible-order action.
* Polished frontend lookup, declaration, item selection, review, and success flow.
* Consumer details confirmation before final submission.
* Optional selected-item and quantity withdrawal requests, including decimal quantity support.
* Merchant details including OIB / Tax ID in declaration context.
* Runtime-localized request output for customer emails, magic links, printable records, and request-bound content.
* Multilingual withdrawal page URL resolution for WPML and Polylang, with safe fallback to the configured default page.
* Croatian and Slovenian translation files.
* Readiness widget for page, merchant identity, cache, and email checks.
* Durable request, item, and audit tables.
* Conservative manual-review eligibility classification.
* Product and category informational exclusion flags.
* Customer receipt, admin notification, status update, and secure-link emails.
* WooCommerce-native email classes and templates.
* Optional contract withdrawal links in selected WooCommerce customer order emails.
* Admin request list/detail screens with status changes.
* CSV export for withdrawal requests with current filters.
* WooCommerce order notes and admin notices for operational visibility.
* Refund-resolution prompt and resolved-state tracking for accepted withdrawal requests.
* Optional supporting attachments with file limits, type validation, protected storage, admin gallery, and cleanup support.
* Optional return-conditions notice with link/PDF support and stored acknowledgement snapshot.
* Optional browser-printable request records for administrators and customers.
* Optional print logo for printable request records.
* Light, Dark, and Custom frontend color schemes for the public withdrawal interface.
* Settings page under WooCommerce with grouped configuration, diagnostics, and cache guidance.
* Privacy exporter/eraser hooks and retention cleanup via Action Scheduler with WP-Cron fallback.
* Dynamic block registration for the withdrawal form.
* Privacy-preserving hashed IP and user agent storage.
* Transient-based lookup rate limiting.
* Built-in honeypot protection for public lookup and submission.
* Custom WooCommerce order-number lookup support with developer filters.
* Cache compatibility guidance for customer-specific withdrawal pages.

== Limitations ==

This plugin focuses specifically on recording electronic contract withdrawal statements, providing durable customer receipts, and supporting a clear administrative review workflow. It does not automate refunds, return shipping labels, return logistics, complaints or warranty claims, marketplace workflows, or accounting integrations. Review and test the configured workflow on a staging store before production use.

== Legal Notice ==

This plugin helps implement a technical withdrawal request workflow. It is not legal advice. Please verify the final legal requirements for your store and jurisdiction.

== Changelog ==

= 2.3.0 =
* Added a new optional floating withdrawal button with configurable label, icon/text display, device targeting, desktop and mobile positions, edge offsets, z-index, colors, hover states, and border styling.
* Added flexible floating button visibility rules for entire-site, WooCommerce, product, My Account/Orders, and custom contexts, with searchable page exclusions and automatic exclusion of the configured withdrawal page.
* Polished the floating button settings UI with dependent-field visibility, visual icon choices, modern position selectors, and updated Croatian translation catalogs (`hr` and `hr_HR`).
* Improved accessibility across the public withdrawal flow, including clearer focus handling, validation feedback, field descriptions, keyboard controls, and visible focus states.
* Improved floating button accessibility, including icon-only labels and better grouped controls in the admin settings UI.
* Added a defensive multilingual withdrawal page URL resolver for WPML and Polylang, including translated page lookup, validation, and safe fallback to the configured default page.
* Updated customer-facing withdrawal page links in the footer, My Account order actions, and WooCommerce customer order emails to use the multilingual resolver while keeping existing order query arguments and prefill tokens unchanged.
* Improved multilingual language-code matching for WPML and Polylang by checking registered language metadata before falling back to conservative locale mapping.
* Added the `mx_contract_withdrawal/withdrawal_page_language_code` filter so advanced multilingual/order-language setups can provide a safe language hint.
* Aligned the request `language` database column with stored locale normalization by widening it to `VARCHAR(35)` for new installs and adding a guarded upgrade for existing tables.
* Added bundled German (`de_DE`), Spanish (`es_ES`), French (`fr_FR`), Italian (`it_IT`), and Slovenian (`sl_SI`) translation catalogs with compiled MO files.
* Refined the Slovenian translation for release-quality terminology, including audit-log wording and removal of Croatian/Serbian remnants.
* Hardened withdrawal reason validation so only supported reason codes can be submitted and stored.
* Preserved the acknowledged return-conditions content with each request so historical records, receipt emails, and printable copies continue to show the terms accepted at submission time.
* Added scoped locale handling for request-bound output, with reliable locale restoration after customer emails and printable records are generated.
* Refreshed WooCommerce email runtime settings before sending so current recipients, subjects, headings, and enabled states are respected.
* Centralized settings defaults and sanitization in one authoritative schema.
* Corrected missing-setting fallback drift between new and upgraded installations while preserving intentionally empty and customized values.
* Migrated previously stored translated default text back to runtime-translated defaults without overwriting merchant customizations.
* Added safer handling of malformed or partial settings input and checkbox groups.
* Added an optional logo for standalone printable withdrawal request records and browser-generated PDF files, using either the site logo or a separately selected JPEG, PNG, or WebP image.
* Kept print-logo branding isolated from frontend withdrawal pages, WooCommerce emails, My Account, admin request details, CSV exports, and stored request snapshots.
* Added an optional Light or Dark color scheme for the public withdrawal interface, including forms, validation states, attachments, confirmation screens, success screens, and related My Account components.
* Added a Custom frontend color scheme with controls for core surfaces, text, borders, links, focus indicators, and primary and secondary buttons, including a one-click restore-defaults action and a responsive two-column layout.
* Kept the frontend color scheme isolated from wp-admin, WooCommerce emails, printable records, and other site elements outside the plugin interface.

= 2.2.0 =
* Hardened runtime cache protection so no-cache headers are applied before theme output on withdrawal pages and verified guest sessions.
* Improved refund-resolution reliability after WooCommerce AJAX refreshes and tied resolution records to the exact validated refund.
* Improved updater resilience with bulk-update package refresh support, short failure backoff, and standard plugin dependency metadata.
* Clarified request overview statistics and reduced duplicate screen-reader announcements for attachment changes.
* Improved public form accessibility with managed step focus, linked field errors and descriptions, clearer quantity controls, upload announcements, and stronger keyboard focus states.
* Added a cache-exclusion guidance card with copyable page URL and path, common cache-provider examples, and details about the plugin's built-in WordPress runtime cache protection.
* Added built-in honeypot protection to guest order lookup and final withdrawal request submission.
* Added the `mx_contract_withdrawal/honeypot_enabled` developer filter to override honeypot protection for specific public form contexts.
* Prevented stale same-version or package-less plugin update notices from remaining available after an update.
* Approved withdrawal requests can be marked as resolved after an order refund.
* Added a refund prompt in the WooCommerce order-items panel.
* Stored resolution separately from accepted and partially accepted decisions.
* Logged successful resolutions in the audit timeline and private order notes.
* Displayed resolved state in admin and My Account without sending an additional customer email.

= 2.1.1 =
* Fixed self-hosted plugin updates by correctly providing the downloadable update package.
* Added WordPress compatibility information to update notices.
* Prevented unavailable updates from being offered when no valid download package exists.

= 2.1.0 =
* Added withdrawal requests for selected order items and quantities, enabled by default and configurable in plugin settings while respecting existing saved settings.
* When selected-item and quantity requests are disabled, the public form now records entire-order withdrawal requests only.
* Entire-order requests now show the full product list in a read-only view on the items step before confirmation.
* Updated printable records, admin request details, and CSV exports to clearly show selected quantities versus ordered quantities.
* Improved the selected quantity control with a compact minus/plus stepper.
* Added safe support for decimal order and withdrawal quantities with precision up to six decimal places, including consistent validation, dynamic input steps, and a database upgrade for higher-precision request item quantities.
* Removed monetary previews from selected-item withdrawal requests.
* Enforced a minimum retention period of one year and protected open `received` and `processing` requests from normal scheduled anonymization.
* Added throttling to prevent repeated magic-link emails for the same order and billing email.
* Added safer attachment upload directory protection using `index.html` files alongside existing server protection.
* Fixed WooCommerce email previews showing undefined array key notices when debug mode is enabled.
* Fixed unchanged request status updates being reported as failures.
* Improved translation catalogs and added missing Croatian translations.
* Added minified frontend/admin CSS and JavaScript assets with SCRIPT_DEBUG fallback for easier development.
* Optimized admin asset loading by using a smaller stylesheet on WooCommerce order list screens.
* Improved admin performance with cached request overview stats and optimized WooCommerce order-list request lookups.
* Hid empty attachment sections from request detail and printable records.
* Clarified attachment visibility by treating "Prefer not to say" as no selected reason.

= 2.0.1 =
* Added a global opt-in setting for product/category withdrawal advisory flags; disabled flags remain stored but do not affect forms, snapshots, or manual-review classification.
* Fixed submitted declaration line breaks collapsing into a single paragraph in Outlook HTML receipt emails.
* Improved Croatian translation loading on frontend, including hr/hr_HR locale fallback.

= 2.0.0 =
* Added optional supporting attachments with configurable visibility, reason rules, file-count and size limits, and a restricted JPG, JPEG, PNG, WebP, and PDF allowlist.
* Added secure attachment storage handling, randomized stored filenames, upload-directory protections, validation for real browser uploads, and cleanup during request anonymization.
* Added multi-step frontend attachment selection, selected-file previews, removal controls, upload result notices, and a 2 MB default maximum file size.
* Added an admin attachment gallery, safe attachment deletion, active attachment counts, request-list attachment filters, and attachment count/detail links in admin notification emails.
* Extended the WordPress privacy exporter and eraser to handle supporting attachment metadata without exposing private file paths.
* Added optional browser-native printable request records for administrators and short-lived customer print links after submission, with browser Save as PDF support.
* Added a configurable Return conditions notice with custom title, text, link, Media Library PDF selection, display locations, and optional customer acknowledgement recorded with the request.
* Added Return conditions content to supported frontend, receipt-email, and printable request contexts, including plain-text email URL handling.
* Added settings importance badges for mandatory, recommended, and optional configuration areas, and hid the readiness Mandatory badge once the plugin is ready to accept requests.
* Added an option to show the optional storefront footer link only on WooCommerce shop, product, category, and tag pages.
* Expanded the admin CSV export with human-readable request status and classifications, selected item and quantity summaries, active attachment counts, reason, and customer note.
* Improved CSV export performance by bulk-loading item rows and attachment counts, while preserving current filters and spreadsheet formula-injection protection.
* Improved the Classic and HPOS WooCommerce Orders withdrawal column with the latest request status badge, total request count, and active attachment count.
* Bulk-loaded WooCommerce Orders column summaries to avoid per-order request and attachment queries.
* Hardened custom order-number lookup with broader input normalization, contextual resolver arguments, public-number verification, duplicate-match rejection, and the backward-compatible `mx_contract_withdrawal/order_number_meta_keys` filter alias.
* Hardened the self-hosted updater so updates are offered only when the remote version is strictly newer, same-version notices are removed, and updater caches are cleared after successful updates.
* Rechecked remote version availability before returning self-hosted update package URLs.
* Migrated retention maintenance to WooCommerce Action Scheduler when available, with chained batches, chain-scoped locking, failure reporting, stale submit-lock cleanup, and a bounded WP-Cron fallback.
* Improved retention teardown on deactivation and uninstall by clearing plugin-owned Action Scheduler actions, WP-Cron events, and retention locks.
* Improved retention failure recovery so failed Action Scheduler batches release their matching chain lock before being marked failed.
* Replaced timestamp-only WordPress-local time calls with UTC timestamps where required.
* Prevented customers from continuing to request review when no order item is selected, with both frontend and server-side validation.
* Improved the final confirmation button styling so it inherits the active theme button color instead of using the plugin danger style.
* Improved Croatian translations and regenerated translation files for the new settings, notices, attachments, print views, and admin export fields.
* Updated WordPress and WooCommerce compatibility metadata for the 2.0.0 release.

= 1.0.1 =
* Hardened self-hosted update package URL handling to require HTTPS downloads.
* Improved public form nonce expiry handling with customer-facing recovery notices.
* Improved privacy erasure by removing customer actor identifiers from retained audit log events.
* Aligned release metadata for the 1.0.1 public build.
* Small fixes

= 1.0 =
* First stable release.
* Redesigned the plugin admin settings screen with a cleaner card-based layout, tabbed sections, inline icons, responsive navigation, switch-style toggles, preview actions, and shortcode copy support.
* Reorganized settings into clearer sections for page/display options, merchant details, eligibility and review rules, guest access and privacy, email notifications, and diagnostics.
* Added a dedicated Email notifications section with quick links to the related WooCommerce email settings used by the withdrawal workflow.
* Added privacy-safe email diagnostics for plugin emails, including last attempted send, last confirmed success, email type, request ID, error code, test email support, and a clear diagnostics action.
* Improved email delivery logging so unconfirmed sends and WordPress/PHPMailer errors are recorded more accurately per email type.
* Added CSV export for withdrawal requests using the current status, eligibility, date, and search filters.
* Added admin request overview stats for open requests, requests needing review, unknown deadlines, resolved requests this month, and total requests this month.
* Improved request list and detail badges so manual-review states are easier to understand.
* Changed the default request retention period from 6 years to 10 years.
* Improved retention cleanup so expired records are anonymized through the request repository and stored consumer details and billing snapshots are cleared.
* Added cleanup for stale submit-lock options.
* Improved public order lookup so customers can enter order numbers with or without the # symbol.
* Hardened order number normalization by trimming hidden characters and common punctuation, verifying the public order number, and supporting custom order number variants stored with or without #.
* Added frontend help text explaining where customers can find their order number.
* Reduced order email prefill token lifetime from 7 days to 72 hours.
* Improved expired or already-used secure guest link handling with a clearer message and a Start again action.
* Improved secure guest link handling when a valid guest access session already exists.
* Adjusted guest secure-link email sending so the public lookup flow can redirect before the email is sent.
* Fixed frontend price and currency display so order totals do not split awkwardly across lines.
* Polished the final request received screen by moving the success checkmark next to the title and normalizing its size.
* Improved personal data export by showing submitted consumer details as readable fields instead of raw JSON.
* Updated privacy policy text and readme documentation for update checks, retention/uninstall behavior, and custom order number compatibility.
* Updated Croatian translations and regenerated translation files.

= 0.3.3 =
* Added an optional contract withdrawal link block for WooCommerce customer order emails.
* Added a new settings section for the order email withdrawal link.
* Added the ability to enable or disable the withdrawal link in order emails.
* Added the ability to choose which WooCommerce customer order emails should include the link.
* The link is shown by default in processing, on-hold, and completed customer order emails.
* Added optional support for showing the link in customer invoice emails.
* Added the ability to display the link as either a button or a plain text link.
* Added a setting for customizing the link/button text.
* The email link now points to the configured contract withdrawal page.
* Added a safe prefill token for order email links.
* The email link no longer exposes the customer e-mail address directly in the URL.
* The prefill token only pre-fills the order number and billing e-mail address in the lookup form.
* The prefill token does not open the second step of the form, does not verify order ownership, and does not bypass the guest secure-link flow.
* Added a 72-hour lifetime for prefill tokens.
* Added the `mx_contract_withdrawal/prefill_token_ttl` filter for changing the prefill token lifetime.
* The email link is not shown when the withdrawal page is missing or not published.
* The email link is not shown when the order already has a withdrawal request.
* The email link is not shown for orders that are not in allowed lookup statuses.
* The email link is not shown for older orders outside the configured action window, unless manual review for older orders is enabled.
* Added protection against rendering the contract withdrawal email block more than once.
* The email block now hooks into multiple WooCommerce e-mail positions for better compatibility with different e-mail templates.
* Improved the e-mail button markup so it works better across e-mail clients.
* The e-mail button now uses inline styling and WooCommerce e-mail colors instead of relying on CSS classes.
* Updated the Croatian default button text to “Pokreni zahtjev za raskid ugovora”.
* Added translation loading before default options are saved on activation, so new Croatian installations can receive Croatian default text.
* Added a new deadline estimation option based on the order received date.
* Added the new “Order received date + delivery buffer” estimation mode.
* Clarified that the existing completed-date estimation option uses the WooCommerce Completed order date.
* Added helper text explaining that WooCommerce Completed date means the date when the order was marked as Completed in WooCommerce.
* Deadline estimation no longer falls back to the order received date when the WooCommerce Completed date is missing; those cases are sent to manual review instead.
* Updated deadline estimation helper text to make it clearer that these options are only review-window estimates and not automatic legal decisions.
* Updated admin deadline notes to clearly show whether the estimate is based on the order received date or the WooCommerce Completed date.
* Updated Croatian translations for the new order email link settings, prefill flow, and deadline estimation text.

= 0.3.2 =
* Maintenance release with packaging/version alignment for the current 0.3.x build.

= 0.3.1 =
* Added a configurable customer-facing order action visibility window for account and public request entry points, without treating it as a legal deadline decision.
* Added a clear My Account withdrawal requests empty state with a link back to customer orders.
* Expanded product and category advisory flags for manual review while keeping flagged items selectable.
* Kept public guest lookup privacy-neutral for old orders and preserved manual-review handling where enabled.
* Secure magic-link guest flow is enabled by default for new installs.
* Polished the frontend withdrawal flow from lookup through success, including consumer detail confirmation before declaration submission.
* Added merchant details including OIB / Tax ID to declaration context and receipt snapshots.
* Added a readiness widget for withdrawal page, merchant identity, shortcode, and email setup.
* Integrated customer/admin/status/secure-link notifications with WooCommerce emails.
* Added WooCommerce order notes and admin notices for request and email events.
* Added cache guidance for customer-specific withdrawal pages.
* Updated Croatian terminology for contract withdrawal contexts.
