Add simple_withdrawalbutton PrestaShop module
This commit is contained in:
@@ -0,0 +1,59 @@
|
||||
# CLAUDE.md
|
||||
|
||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||
|
||||
## Project Overview
|
||||
|
||||
PrestaShop module (v0.1.0) implementing an electronic withdrawal declaration form for German B2C e-commerce. Fulfills EU consumer protection law requirements (Widerrufsrecht) by providing a documented receipt of withdrawal requests.
|
||||
|
||||
**Compatibility**: PrestaShop 1.7.8+ / 8 / 9, PHP 7.2+
|
||||
|
||||
## No Build System
|
||||
|
||||
This is a classical PrestaShop legacy module with no npm, Composer, Makefile, or test framework. Deployment is via ZIP upload in PrestaShop Admin → Module Manager.
|
||||
|
||||
## Architecture
|
||||
|
||||
### Core Components
|
||||
|
||||
| File | Role |
|
||||
|------|------|
|
||||
| `simple_withdrawalbutton.php` | Main module class: install/uninstall, config form, hook handlers, utility methods |
|
||||
| `controllers/front/withdraw.php` | Public front controller for the withdrawal form (no auth required) |
|
||||
| `controllers/admin/AdminSimpleWithdrawalController.php` | Backoffice list/detail view with status management |
|
||||
|
||||
### Request Flow
|
||||
|
||||
1. Customer clicks footer link (`displayFooter` hook) or account dashboard link (`displayCustomerAccount` hook)
|
||||
2. `controllers/front/withdraw.php` handles two POST steps:
|
||||
- **Step 1** (`prepare`): validate + render `confirm.tpl`
|
||||
- **Step 2** (`confirm`): write to DB, send emails, render `success.tpl`
|
||||
3. Two emails sent: customer confirmation + shop notification (configured admin email)
|
||||
|
||||
### Database
|
||||
|
||||
Single table `ps_cyp_withdrawal_request`. Schema is defined inline in `simple_withdrawalbutton.php::install()` — no separate SQL files. Records are intentionally **not deleted on uninstall** (legal compliance).
|
||||
|
||||
Key fields: `order_reference`, `withdrawal_scope` (ENUM: full|partial), `status` (ENUM: new|processing|closed), `customer_ip_hash` / `user_agent_hash` (SHA256, privacy).
|
||||
|
||||
### Security Model
|
||||
|
||||
- CSRF token stored in session, validated on confirm step
|
||||
- Honeypot field (`website` input, must be empty)
|
||||
- Rate limiting: configurable max requests per email+IP hash per hour (default 5)
|
||||
- All DB queries use PrestaShop's `pSQL()` parameterization
|
||||
- IP and user-agent are hashed (SHA256) before storage
|
||||
|
||||
### Templates
|
||||
|
||||
Smarty templates in `views/templates/front/` (form.tpl, confirm.tpl, success.tpl) and `views/templates/hook/`. Email templates in `mails/de/` and `mails/en/`.
|
||||
|
||||
The items field in `form.tpl` is conditionally shown via vanilla JS only when `withdrawal_scope === 'partial'`.
|
||||
|
||||
## PrestaShop Conventions
|
||||
|
||||
- Use `pSQL()` for all DB string interpolation; `(int)` cast for integers
|
||||
- Hook registration/deregistration in `install()`/`uninstall()`
|
||||
- Admin tab registered as `AdminSimpleWithdrawal` — the controller class name must match
|
||||
- Module config stored via `Configuration::get/updateValue()`
|
||||
- Email sending uses `Mail::Send()` with template files in `mails/{lang}/`
|
||||
@@ -0,0 +1,55 @@
|
||||
# simple_withdrawalbutton
|
||||
|
||||
Minimales PrestaShop-Modul für eine elektronische Widerrufsfunktion (Widerrufsrecht nach § 312k / § 356 BGB).
|
||||
|
||||
**[→ Aktuelle Version herunterladen](https://git.arne-weiss.de/arne/simple-withdrawalbutton/releases/latest)**
|
||||
|
||||
## Enthaltene Funktionen
|
||||
|
||||
- Footer-Link **„Vertrag widerrufen"** über `displayFooter`
|
||||
- zusätzlicher Link im Kundenkonto über `displayCustomerAccount`
|
||||
- öffentliches Formular ohne Login-Zwang
|
||||
- vollständiger Widerruf oder Teilwiderruf per Freitext
|
||||
- zweistufiger Ablauf: Angaben prüfen → **„Widerruf bestätigen"**
|
||||
- Speicherung in eigener Tabelle `ps_simple_withdrawal_request`
|
||||
- automatische Eingangsbestätigung per E-Mail an den Verbraucher
|
||||
- interne Benachrichtigung per E-Mail an den Shopbetreiber
|
||||
- einfache Backoffice-Liste unter **Bestellungen → Widerrufe**
|
||||
- Statusverwaltung: `new`, `processing`, `closed`
|
||||
- CSRF-Token, Honeypot und konfigurierbares Rate-Limit
|
||||
|
||||
## Installation
|
||||
|
||||
1. ZIP-Datei von der [Releases-Seite](https://git.arne-weiss.de/arne/simple-withdrawalbutton/releases/latest) herunterladen.
|
||||
2. In PrestaShop unter **Module → Module Manager → Modul hochladen** hochladen.
|
||||
3. Modul installieren und aktivieren.
|
||||
|
||||
## Konfiguration
|
||||
|
||||
Nach der Installation unter **Module → Module Manager → simple_withdrawalbutton → Konfigurieren**:
|
||||
|
||||
| Einstellung | Beschreibung |
|
||||
|-------------|--------------|
|
||||
| Shop-Benachrichtigungs-E-Mail | Adresse, an die neue Widerrufe gemeldet werden |
|
||||
| Rate-Limit pro Stunde | Max. Einreichungen pro E-Mail oder IP-Hash (Standard: 5) |
|
||||
| Datenschutzerklärung URL | Link zur Datenschutzerklärung im Formular (DSGVO Art. 13) |
|
||||
| Widerrufsbelehrung URL | Link zur Widerrufsbelehrung oberhalb des Formulars |
|
||||
| Aufbewahrungsfrist (Monate) | Ältere Einträge können manuell gelöscht werden (0 = unbegrenzt) |
|
||||
|
||||
## Test nach der Installation
|
||||
|
||||
1. Frontend-Link im Footer oder im Kundenkonto öffnen.
|
||||
2. Testwiderruf ausfüllen und bestätigen.
|
||||
3. Prüfen, ob Kunden-E-Mail und Shop-Benachrichtigung ankommen.
|
||||
4. Im Backoffice unter **Bestellungen → Widerrufe** prüfen, ob der Datensatz sichtbar ist.
|
||||
|
||||
## Wichtige Hinweise
|
||||
|
||||
- Das Modul bestätigt nur den **Eingang** des Widerrufs, nicht dessen rechtliche Wirksamkeit.
|
||||
- Die Widerrufsbelehrung, Datenschutzerklärung und AGB müssen separat gepflegt werden.
|
||||
- Beim Deinstallieren löscht das Modul die gespeicherten Widerrufe **nicht** — diese können rechtlich relevant sein.
|
||||
- Das Modul erzeugt keine Retourenlabels und führt keine automatische Erstattung aus.
|
||||
|
||||
## Kompatibilität
|
||||
|
||||
Klassisches PrestaShop-Legacy-Modul für PrestaShop 1.7.8+ / 8 / 9, PHP 7.2+. Vor Live-Nutzung in einer Staging-Umgebung testen.
|
||||
@@ -0,0 +1,153 @@
|
||||
<?php
|
||||
|
||||
if (!defined('_PS_VERSION_')) {
|
||||
exit;
|
||||
}
|
||||
|
||||
class AdminSimpleWithdrawalController extends ModuleAdminController
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
$this->bootstrap = true;
|
||||
$this->table = 'simple_withdrawal_request';
|
||||
$this->identifier = 'id_withdrawal_request';
|
||||
$this->lang = false;
|
||||
$this->explicitSelect = true;
|
||||
$this->_select = 'a.*';
|
||||
$this->_defaultOrderBy = 'created_at';
|
||||
$this->_defaultOrderWay = 'DESC';
|
||||
$this->allow_export = true;
|
||||
$this->list_no_link = false;
|
||||
$this->actions = ['view'];
|
||||
|
||||
$this->fields_list = [
|
||||
'id_withdrawal_request' => [
|
||||
'title' => $this->l('ID'),
|
||||
'align' => 'center',
|
||||
'class' => 'fixed-width-xs',
|
||||
],
|
||||
'created_at' => [
|
||||
'title' => $this->l('Received'),
|
||||
'type' => 'datetime',
|
||||
'filter_key' => 'a!created_at',
|
||||
],
|
||||
'order_reference' => [
|
||||
'title' => $this->l('Order reference'),
|
||||
'filter_key' => 'a!order_reference',
|
||||
],
|
||||
'customer_name' => [
|
||||
'title' => $this->l('Name'),
|
||||
'filter_key' => 'a!customer_name',
|
||||
],
|
||||
'customer_email' => [
|
||||
'title' => $this->l('Email'),
|
||||
'filter_key' => 'a!customer_email',
|
||||
],
|
||||
'withdrawal_scope' => [
|
||||
'title' => $this->l('Scope'),
|
||||
'callback' => 'renderScopeLabel',
|
||||
'callback_object' => $this,
|
||||
'filter_key' => 'a!withdrawal_scope',
|
||||
],
|
||||
'status' => [
|
||||
'title' => $this->l('Status'),
|
||||
'callback' => 'renderStatusLabel',
|
||||
'callback_object' => $this,
|
||||
'filter_key' => 'a!status',
|
||||
],
|
||||
];
|
||||
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
public function renderScopeLabel($value, $row)
|
||||
{
|
||||
if ($value === 'partial') {
|
||||
return '<span class="label label-info">' . $this->l('Partial') . '</span>';
|
||||
}
|
||||
|
||||
return '<span class="label label-default">' . $this->l('Full order') . '</span>';
|
||||
}
|
||||
|
||||
public function renderStatusLabel($value, $row)
|
||||
{
|
||||
$labels = [
|
||||
'new' => ['class' => 'label-warning', 'text' => $this->l('New')],
|
||||
'processing' => ['class' => 'label-info', 'text' => $this->l('Processing')],
|
||||
'closed' => ['class' => 'label-success', 'text' => $this->l('Closed')],
|
||||
];
|
||||
|
||||
$label = isset($labels[$value]) ? $labels[$value] : ['class' => 'label-default', 'text' => $value];
|
||||
return '<span class="label ' . $label['class'] . '">' . Tools::safeOutput($label['text']) . '</span>';
|
||||
}
|
||||
|
||||
public function postProcess()
|
||||
{
|
||||
if (Tools::isSubmit('submitSimpleWithdrawalStatus')) {
|
||||
$this->processStatusUpdate();
|
||||
return;
|
||||
}
|
||||
|
||||
parent::postProcess();
|
||||
}
|
||||
|
||||
private function processStatusUpdate()
|
||||
{
|
||||
$id = (int) Tools::getValue($this->identifier);
|
||||
$status = (string) Tools::getValue('status');
|
||||
$allowedStatuses = ['new', 'processing', 'closed'];
|
||||
|
||||
if ($id <= 0 || !in_array($status, $allowedStatuses, true)) {
|
||||
$this->errors[] = $this->l('Invalid status update.');
|
||||
return;
|
||||
}
|
||||
|
||||
$ok = Db::getInstance()->update(
|
||||
'simple_withdrawal_request',
|
||||
['status' => $status],
|
||||
'`id_withdrawal_request` = ' . (int) $id
|
||||
);
|
||||
|
||||
if ($ok) {
|
||||
Tools::redirectAdmin(self::$currentIndex . '&token=' . $this->token . '&conf=4&view' . $this->table . '=1&' . $this->identifier . '=' . (int) $id);
|
||||
}
|
||||
|
||||
$this->errors[] = $this->l('The status could not be updated.');
|
||||
}
|
||||
|
||||
public function renderView()
|
||||
{
|
||||
$id = (int) Tools::getValue($this->identifier);
|
||||
$row = Db::getInstance()->getRow(
|
||||
'SELECT * FROM `' . _DB_PREFIX_ . pSQL('simple_withdrawal_request') . '`
|
||||
WHERE `id_withdrawal_request` = ' . (int) $id
|
||||
);
|
||||
|
||||
if (!$row) {
|
||||
$this->errors[] = $this->l('Withdrawal request not found.');
|
||||
return parent::renderList();
|
||||
}
|
||||
|
||||
$orderLink = '';
|
||||
if (!empty($row['id_order'])) {
|
||||
$orderLink = $this->context->link->getAdminLink('AdminOrders', true, [], [
|
||||
'id_order' => (int) $row['id_order'],
|
||||
'vieworder' => 1,
|
||||
]);
|
||||
}
|
||||
|
||||
$this->context->smarty->assign([
|
||||
'request' => $row,
|
||||
'order_link' => $orderLink,
|
||||
'current_index' => self::$currentIndex,
|
||||
'token' => $this->token,
|
||||
'identifier' => $this->identifier,
|
||||
'back_link' => self::$currentIndex . '&token=' . $this->token,
|
||||
]);
|
||||
|
||||
return $this->module->display(
|
||||
_PS_MODULE_DIR_ . $this->module->name . '/' . $this->module->name . '.php',
|
||||
'views/templates/admin/view.tpl'
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
<?php
|
||||
/** Silence is golden. */
|
||||
@@ -0,0 +1,2 @@
|
||||
<?php
|
||||
/** Silence is golden. */
|
||||
@@ -0,0 +1,223 @@
|
||||
<?php
|
||||
|
||||
if (!defined('_PS_VERSION_')) {
|
||||
exit;
|
||||
}
|
||||
|
||||
class Simple_withdrawalbuttonWithdrawModuleFrontController extends ModuleFrontController
|
||||
{
|
||||
public $ssl = true;
|
||||
public $auth = false;
|
||||
public $guestAllowed = true;
|
||||
|
||||
/** @var string */
|
||||
private $currentView = 'form';
|
||||
|
||||
/** @var array */
|
||||
private $formData = [];
|
||||
|
||||
/** @var array */
|
||||
private $errorsList = [];
|
||||
|
||||
/** @var array */
|
||||
private $successData = [];
|
||||
|
||||
public function initContent()
|
||||
{
|
||||
parent::initContent();
|
||||
|
||||
$this->context->smarty->assign([
|
||||
'errors_list' => $this->errorsList,
|
||||
'form_data' => $this->formData,
|
||||
'csrf_token' => $this->module->getFrontToken(),
|
||||
'action_url' => $this->context->link->getModuleLink($this->module->name, 'withdraw', [], true),
|
||||
'success_data' => $this->successData,
|
||||
'privacy_url' => (string) Configuration::get(Simple_withdrawalbutton::CONF_PRIVACY_URL),
|
||||
'revocation_url' => (string) Configuration::get(Simple_withdrawalbutton::CONF_REVOCATION_URL),
|
||||
]);
|
||||
|
||||
if ($this->currentView === 'confirm') {
|
||||
$this->setTemplate('module:' . $this->module->name . '/views/templates/front/confirm.tpl');
|
||||
return;
|
||||
}
|
||||
|
||||
if ($this->currentView === 'success') {
|
||||
$this->setTemplate('module:' . $this->module->name . '/views/templates/front/success.tpl');
|
||||
return;
|
||||
}
|
||||
|
||||
$this->setTemplate('module:' . $this->module->name . '/views/templates/front/form.tpl');
|
||||
}
|
||||
|
||||
public function postProcess()
|
||||
{
|
||||
if (Tools::isSubmit('submit_withdrawal_prepare')) {
|
||||
$this->handlePrepare();
|
||||
return;
|
||||
}
|
||||
|
||||
if (Tools::isSubmit('submit_withdrawal_confirm')) {
|
||||
$this->handleConfirm();
|
||||
return;
|
||||
}
|
||||
|
||||
if (Tools::isSubmit('submit_withdrawal_back')) {
|
||||
$this->formData = $this->collectInput();
|
||||
$this->currentView = 'form';
|
||||
return;
|
||||
}
|
||||
|
||||
$this->formData = $this->getEmptyFormData();
|
||||
}
|
||||
|
||||
private function handlePrepare()
|
||||
{
|
||||
$data = $this->collectInput();
|
||||
$this->formData = $data;
|
||||
|
||||
if (!$this->module->isValidFrontToken(Tools::getValue('csrf_token'))) {
|
||||
$this->errorsList[] = $this->module->l('The form could not be validated. Please reload the page and try again.', 'withdraw');
|
||||
}
|
||||
|
||||
if ($this->isHoneypotFilled()) {
|
||||
$this->errorsList[] = $this->module->l('The form could not be validated. Please reload the page and try again.', 'withdraw');
|
||||
}
|
||||
|
||||
$this->errorsList = array_merge($this->errorsList, $this->validateData($data));
|
||||
|
||||
if (!empty($this->errorsList)) {
|
||||
$this->currentView = 'form';
|
||||
return;
|
||||
}
|
||||
|
||||
$this->currentView = 'confirm';
|
||||
}
|
||||
|
||||
private function handleConfirm()
|
||||
{
|
||||
$data = $this->collectInput();
|
||||
$this->formData = $data;
|
||||
|
||||
if (!$this->module->isValidFrontToken(Tools::getValue('csrf_token'))) {
|
||||
$this->errorsList[] = $this->module->l('The form could not be validated. Please reload the page and try again.', 'withdraw');
|
||||
}
|
||||
|
||||
if ($this->isHoneypotFilled()) {
|
||||
// Do not save obvious bot submissions. Show a generic success page to avoid feedback loops.
|
||||
$this->currentView = 'success';
|
||||
$this->successData = [
|
||||
'mail_ok' => true,
|
||||
'fake' => true,
|
||||
];
|
||||
return;
|
||||
}
|
||||
|
||||
$this->errorsList = array_merge($this->errorsList, $this->validateData($data));
|
||||
|
||||
$ipHash = $this->module->hashForPrivacy($this->module->getClientIp());
|
||||
if (empty($this->errorsList) && $this->module->isRateLimited($data['customer_email'], $ipHash)) {
|
||||
$this->errorsList[] = $this->module->l('Too many withdrawal declarations were submitted in a short period. Please try again later or contact us by email.', 'withdraw');
|
||||
}
|
||||
|
||||
if (!empty($this->errorsList)) {
|
||||
$this->currentView = 'form';
|
||||
return;
|
||||
}
|
||||
|
||||
$saved = $this->module->saveWithdrawal($data);
|
||||
if (!$saved) {
|
||||
$this->errorsList[] = $this->module->l('Your withdrawal declaration could not be saved. Please try again or contact us by email.', 'withdraw');
|
||||
$this->currentView = 'form';
|
||||
return;
|
||||
}
|
||||
|
||||
$customerMailOk = $this->module->sendCustomerConfirmation($data, $saved['created_at']);
|
||||
$shopMailOk = $this->module->sendShopNotification($data, $saved['created_at'], (int) $saved['id_withdrawal_request']);
|
||||
|
||||
if ($customerMailOk) {
|
||||
$this->module->markConfirmationSent((int) $saved['id_withdrawal_request']);
|
||||
}
|
||||
|
||||
$this->currentView = 'success';
|
||||
$this->successData = [
|
||||
'id_withdrawal_request' => (int) $saved['id_withdrawal_request'],
|
||||
'created_at' => $this->module->formatDateTimeForMail($saved['created_at']),
|
||||
'mail_ok' => (bool) $customerMailOk,
|
||||
'shop_mail_ok' => (bool) $shopMailOk,
|
||||
'customer_email' => $data['customer_email'],
|
||||
'order_reference' => $data['order_reference'],
|
||||
'withdrawal_scope' => $data['withdrawal_scope'],
|
||||
'withdrawal_items_text' => $data['withdrawal_items_text'],
|
||||
'message' => $data['message'],
|
||||
];
|
||||
}
|
||||
|
||||
private function collectInput()
|
||||
{
|
||||
$scope = (string) Tools::getValue('withdrawal_scope');
|
||||
if (!in_array($scope, ['full', 'partial'], true)) {
|
||||
$scope = 'full';
|
||||
}
|
||||
|
||||
return [
|
||||
'customer_name' => $this->module->cleanText(Tools::getValue('customer_name'), 255),
|
||||
'customer_email' => strtolower($this->module->cleanText(Tools::getValue('customer_email'), 255)),
|
||||
'order_reference' => $this->module->cleanText(Tools::getValue('order_reference'), 64),
|
||||
'withdrawal_scope' => $scope,
|
||||
'withdrawal_items_text' => $this->module->cleanText(Tools::getValue('withdrawal_items_text'), 5000),
|
||||
'message' => $this->module->cleanText(Tools::getValue('message'), 5000),
|
||||
];
|
||||
}
|
||||
|
||||
private function getEmptyFormData()
|
||||
{
|
||||
$customerName = '';
|
||||
$customerEmail = '';
|
||||
|
||||
if (Validate::isLoadedObject($this->context->customer)) {
|
||||
$customerName = trim((string) $this->context->customer->firstname . ' ' . (string) $this->context->customer->lastname);
|
||||
$customerEmail = (string) $this->context->customer->email;
|
||||
}
|
||||
|
||||
return [
|
||||
'customer_name' => $customerName,
|
||||
'customer_email' => $customerEmail,
|
||||
'order_reference' => '',
|
||||
'withdrawal_scope' => 'full',
|
||||
'withdrawal_items_text' => '',
|
||||
'message' => '',
|
||||
];
|
||||
}
|
||||
|
||||
private function validateData(array $data)
|
||||
{
|
||||
$errors = [];
|
||||
|
||||
if ($data['customer_name'] === '' || Tools::strlen($data['customer_name']) < 2) {
|
||||
$errors[] = $this->module->l('Please enter your name.', 'withdraw');
|
||||
}
|
||||
|
||||
if (!Validate::isEmail($data['customer_email'])) {
|
||||
$errors[] = $this->module->l('Please enter a valid email address for the confirmation.', 'withdraw');
|
||||
}
|
||||
|
||||
if ($data['order_reference'] === '') {
|
||||
$errors[] = $this->module->l('Please enter the order number or order reference.', 'withdraw');
|
||||
}
|
||||
|
||||
if (!in_array($data['withdrawal_scope'], ['full', 'partial'], true)) {
|
||||
$errors[] = $this->module->l('Please select whether you want to withdraw the full order or part of it.', 'withdraw');
|
||||
}
|
||||
|
||||
if ($data['withdrawal_scope'] === 'partial' && $data['withdrawal_items_text'] === '') {
|
||||
$errors[] = $this->module->l('For a partial withdrawal, please enter the affected items and quantities.', 'withdraw');
|
||||
}
|
||||
|
||||
return $errors;
|
||||
}
|
||||
|
||||
private function isHoneypotFilled()
|
||||
{
|
||||
return trim((string) Tools::getValue('cyp_hp_v1')) !== '';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
<?php
|
||||
/** Silence is golden. */
|
||||
@@ -0,0 +1,2 @@
|
||||
<?php
|
||||
/** Silence is golden. */
|
||||
@@ -0,0 +1,43 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Eingangsbestätigung Ihres Widerrufs</title>
|
||||
</head>
|
||||
<body>
|
||||
<p>Guten Tag {customer_name},</p>
|
||||
|
||||
<p>wir bestätigen den Eingang Ihrer Widerrufserklärung.</p>
|
||||
|
||||
<p>
|
||||
<strong>Eingang des Widerrufs:</strong><br>
|
||||
{submitted_at}
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<strong>Bestellnummer / Bestellreferenz:</strong><br>
|
||||
{order_reference}
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<strong>Widerruf betrifft:</strong><br>
|
||||
{scope_label}
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<strong>Betroffene Artikel / Mengen:</strong><br>
|
||||
{withdrawal_items_text}
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<strong>Ihre Nachricht / Bemerkung:</strong><br>
|
||||
{message}
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Diese Nachricht bestätigt nur den Eingang Ihrer Widerrufserklärung. Die weitere Bearbeitung und Prüfung erfolgt separat.
|
||||
</p>
|
||||
|
||||
<p>Mit freundlichen Grüßen<br>{shop_name}</p>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,23 @@
|
||||
Guten Tag {customer_name},
|
||||
|
||||
wir bestätigen den Eingang Ihrer Widerrufserklärung.
|
||||
|
||||
Eingang des Widerrufs:
|
||||
{submitted_at}
|
||||
|
||||
Bestellnummer / Bestellreferenz:
|
||||
{order_reference}
|
||||
|
||||
Widerruf betrifft:
|
||||
{scope_label}
|
||||
|
||||
Betroffene Artikel / Mengen:
|
||||
{withdrawal_items_text}
|
||||
|
||||
Ihre Nachricht / Bemerkung:
|
||||
{message}
|
||||
|
||||
Diese Nachricht bestätigt nur den Eingang Ihrer Widerrufserklärung. Die weitere Bearbeitung und Prüfung erfolgt separat.
|
||||
|
||||
Mit freundlichen Grüßen
|
||||
{shop_name}
|
||||
@@ -0,0 +1,33 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Neuer Widerruf</title>
|
||||
</head>
|
||||
<body>
|
||||
<p>Ein neuer Widerruf ist eingegangen.</p>
|
||||
|
||||
<p>
|
||||
<strong>ID:</strong> {id_withdrawal_request}<br>
|
||||
<strong>Eingang:</strong> {submitted_at}<br>
|
||||
<strong>Bestellnummer / Bestellreferenz:</strong> {order_reference}<br>
|
||||
<strong>Name:</strong> {customer_name}<br>
|
||||
<strong>E-Mail:</strong> {customer_email}<br>
|
||||
<strong>Widerruf betrifft:</strong> {scope_label}
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<strong>Betroffene Artikel / Mengen:</strong><br>
|
||||
{withdrawal_items_text}
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<strong>Nachricht / Bemerkung:</strong><br>
|
||||
{message}
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Backoffice: <a href="{admin_link}">{admin_link}</a>
|
||||
</p>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,17 @@
|
||||
Ein neuer Widerruf ist eingegangen.
|
||||
|
||||
ID: {id_withdrawal_request}
|
||||
Eingang: {submitted_at}
|
||||
Bestellnummer / Bestellreferenz: {order_reference}
|
||||
Name: {customer_name}
|
||||
E-Mail: {customer_email}
|
||||
Widerruf betrifft: {scope_label}
|
||||
|
||||
Betroffene Artikel / Mengen:
|
||||
{withdrawal_items_text}
|
||||
|
||||
Nachricht / Bemerkung:
|
||||
{message}
|
||||
|
||||
Backoffice:
|
||||
{admin_link}
|
||||
@@ -0,0 +1,2 @@
|
||||
<?php
|
||||
/** Silence is golden. */
|
||||
@@ -0,0 +1,22 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Confirmation of receipt of your withdrawal</title>
|
||||
</head>
|
||||
<body>
|
||||
<p>Hello {customer_name},</p>
|
||||
|
||||
<p>We confirm receipt of your withdrawal declaration.</p>
|
||||
|
||||
<p><strong>Received:</strong><br>{submitted_at}</p>
|
||||
<p><strong>Order number / order reference:</strong><br>{order_reference}</p>
|
||||
<p><strong>Withdrawal concerns:</strong><br>{scope_label}</p>
|
||||
<p><strong>Affected items / quantities:</strong><br>{withdrawal_items_text}</p>
|
||||
<p><strong>Your message / note:</strong><br>{message}</p>
|
||||
|
||||
<p>This message only confirms receipt of your withdrawal declaration. Further processing and review will follow separately.</p>
|
||||
|
||||
<p>Kind regards<br>{shop_name}</p>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,23 @@
|
||||
Hello {customer_name},
|
||||
|
||||
We confirm receipt of your withdrawal declaration.
|
||||
|
||||
Received:
|
||||
{submitted_at}
|
||||
|
||||
Order number / order reference:
|
||||
{order_reference}
|
||||
|
||||
Withdrawal concerns:
|
||||
{scope_label}
|
||||
|
||||
Affected items / quantities:
|
||||
{withdrawal_items_text}
|
||||
|
||||
Your message / note:
|
||||
{message}
|
||||
|
||||
This message only confirms receipt of your withdrawal declaration. Further processing and review will follow separately.
|
||||
|
||||
Kind regards
|
||||
{shop_name}
|
||||
@@ -0,0 +1,24 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>New withdrawal declaration</title>
|
||||
</head>
|
||||
<body>
|
||||
<p>A new withdrawal declaration has been received.</p>
|
||||
|
||||
<p>
|
||||
<strong>ID:</strong> {id_withdrawal_request}<br>
|
||||
<strong>Received:</strong> {submitted_at}<br>
|
||||
<strong>Order number / order reference:</strong> {order_reference}<br>
|
||||
<strong>Name:</strong> {customer_name}<br>
|
||||
<strong>Email:</strong> {customer_email}<br>
|
||||
<strong>Withdrawal concerns:</strong> {scope_label}
|
||||
</p>
|
||||
|
||||
<p><strong>Affected items / quantities:</strong><br>{withdrawal_items_text}</p>
|
||||
<p><strong>Message / note:</strong><br>{message}</p>
|
||||
|
||||
<p>Back office: <a href="{admin_link}">{admin_link}</a></p>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,17 @@
|
||||
A new withdrawal declaration has been received.
|
||||
|
||||
ID: {id_withdrawal_request}
|
||||
Received: {submitted_at}
|
||||
Order number / order reference: {order_reference}
|
||||
Name: {customer_name}
|
||||
Email: {customer_email}
|
||||
Withdrawal concerns: {scope_label}
|
||||
|
||||
Affected items / quantities:
|
||||
{withdrawal_items_text}
|
||||
|
||||
Message / note:
|
||||
{message}
|
||||
|
||||
Back office:
|
||||
{admin_link}
|
||||
@@ -0,0 +1,2 @@
|
||||
<?php
|
||||
/** Silence is golden. */
|
||||
@@ -0,0 +1,597 @@
|
||||
<?php
|
||||
/**
|
||||
* Minimal electronic withdrawal button for PrestaShop.
|
||||
*
|
||||
* This module implements a minimal two-step withdrawal declaration flow:
|
||||
* - public footer link "Vertrag widerrufen"
|
||||
* - public form for full or partial withdrawal
|
||||
* - confirmation step with button "Widerruf bestätigen"
|
||||
* - database record
|
||||
* - email confirmation to customer and notification to shop owner
|
||||
*/
|
||||
|
||||
if (!defined('_PS_VERSION_')) {
|
||||
exit;
|
||||
}
|
||||
|
||||
class Simple_withdrawalbutton extends Module
|
||||
{
|
||||
public const TABLE_REQUEST = 'simple_withdrawal_request';
|
||||
public const CONF_SHOP_EMAIL = 'SIMPLE_WITHDRAWAL_SHOP_EMAIL';
|
||||
public const CONF_RATE_LIMIT = 'SIMPLE_WITHDRAWAL_RATE_LIMIT';
|
||||
public const CONF_PRIVACY_URL = 'SIMPLE_WITHDRAWAL_PRIVACY_URL';
|
||||
public const CONF_REVOCATION_URL = 'SIMPLE_WITHDRAWAL_REVOCATION_URL';
|
||||
public const CONF_RETENTION_MONTHS = 'SIMPLE_WITHDRAWAL_RETENTION_MONTHS';
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->name = 'simple_withdrawalbutton';
|
||||
$this->tab = 'administration';
|
||||
$this->version = '0.1.0';
|
||||
$this->author = 'Arne Weiss';
|
||||
$this->need_instance = 0;
|
||||
$this->bootstrap = true;
|
||||
$this->ps_versions_compliancy = [
|
||||
'min' => '1.7.8.0',
|
||||
'max' => _PS_VERSION_,
|
||||
];
|
||||
|
||||
parent::__construct();
|
||||
|
||||
$this->displayName = $this->l('Withdrawal button');
|
||||
$this->description = $this->l('Adds a minimal two-step electronic withdrawal function for B2C orders.');
|
||||
$this->confirmUninstall = $this->l('Uninstall the module? Existing withdrawal records will be kept in the database.');
|
||||
}
|
||||
|
||||
public function install()
|
||||
{
|
||||
return parent::install()
|
||||
&& $this->installSql()
|
||||
&& $this->installTab()
|
||||
&& $this->registerHook('displayFooter')
|
||||
&& $this->registerHook('displayCustomerAccount')
|
||||
&& Configuration::updateValue(self::CONF_SHOP_EMAIL, (string) Configuration::get('PS_SHOP_EMAIL'))
|
||||
&& Configuration::updateValue(self::CONF_RATE_LIMIT, '5')
|
||||
&& Configuration::updateValue(self::CONF_PRIVACY_URL, '')
|
||||
&& Configuration::updateValue(self::CONF_REVOCATION_URL, '')
|
||||
&& Configuration::updateValue(self::CONF_RETENTION_MONTHS, '0');
|
||||
}
|
||||
|
||||
public function uninstall()
|
||||
{
|
||||
// Keep withdrawal records intentionally. These may be legally relevant.
|
||||
return $this->uninstallTab()
|
||||
&& Configuration::deleteByName(self::CONF_SHOP_EMAIL)
|
||||
&& Configuration::deleteByName(self::CONF_RATE_LIMIT)
|
||||
&& Configuration::deleteByName(self::CONF_PRIVACY_URL)
|
||||
&& Configuration::deleteByName(self::CONF_REVOCATION_URL)
|
||||
&& Configuration::deleteByName(self::CONF_RETENTION_MONTHS)
|
||||
&& parent::uninstall();
|
||||
}
|
||||
|
||||
private function installSql()
|
||||
{
|
||||
$sql = 'CREATE TABLE IF NOT EXISTS `' . _DB_PREFIX_ . pSQL(self::TABLE_REQUEST) . '` (
|
||||
`id_withdrawal_request` INT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
`id_order` INT UNSIGNED NULL,
|
||||
`order_reference` VARCHAR(64) NOT NULL,
|
||||
`customer_name` VARCHAR(255) NOT NULL,
|
||||
`customer_email` VARCHAR(255) NOT NULL,
|
||||
`withdrawal_scope` ENUM("full", "partial") NOT NULL,
|
||||
`withdrawal_items_text` TEXT NULL,
|
||||
`message` TEXT NULL,
|
||||
`created_at` DATETIME NOT NULL,
|
||||
`confirmation_sent_at` DATETIME NULL,
|
||||
`status` ENUM("new", "processing", "closed") NOT NULL DEFAULT "new",
|
||||
`id_shop` INT UNSIGNED NULL,
|
||||
`id_lang` INT UNSIGNED NULL,
|
||||
`customer_ip_hash` CHAR(64) NULL,
|
||||
`user_agent_hash` CHAR(64) NULL,
|
||||
PRIMARY KEY (`id_withdrawal_request`),
|
||||
KEY `idx_order_reference` (`order_reference`),
|
||||
KEY `idx_customer_email` (`customer_email`),
|
||||
KEY `idx_created_at` (`created_at`),
|
||||
KEY `idx_status` (`status`),
|
||||
KEY `idx_shop` (`id_shop`)
|
||||
) ENGINE=' . _MYSQL_ENGINE_ . ' DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;';
|
||||
|
||||
return Db::getInstance()->execute($sql);
|
||||
}
|
||||
|
||||
private function installTab()
|
||||
{
|
||||
$className = 'AdminSimpleWithdrawal';
|
||||
if ((int) Tab::getIdFromClassName($className) > 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$tab = new Tab();
|
||||
$tab->active = 1;
|
||||
$tab->class_name = $className;
|
||||
$tab->module = $this->name;
|
||||
$tab->id_parent = $this->getOrdersParentTabId();
|
||||
|
||||
foreach (Language::getLanguages(false) as $lang) {
|
||||
$tab->name[(int) $lang['id_lang']] = 'Widerrufe';
|
||||
}
|
||||
|
||||
return (bool) $tab->add();
|
||||
}
|
||||
|
||||
private function uninstallTab()
|
||||
{
|
||||
$idTab = (int) Tab::getIdFromClassName('AdminSimpleWithdrawal');
|
||||
if ($idTab <= 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$tab = new Tab($idTab);
|
||||
return (bool) $tab->delete();
|
||||
}
|
||||
|
||||
private function getOrdersParentTabId()
|
||||
{
|
||||
$candidates = [
|
||||
'AdminParentOrders',
|
||||
'AdminOrders',
|
||||
'AdminParentModulesSf',
|
||||
];
|
||||
|
||||
foreach ($candidates as $className) {
|
||||
$id = (int) Tab::getIdFromClassName($className);
|
||||
if ($id > 0) {
|
||||
return $id;
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
public function getContent()
|
||||
{
|
||||
$output = '';
|
||||
|
||||
if (Tools::isSubmit('submitCypWithdrawalSettings')) {
|
||||
$email = trim((string) Tools::getValue(self::CONF_SHOP_EMAIL));
|
||||
$rateLimit = (int) Tools::getValue(self::CONF_RATE_LIMIT);
|
||||
$privacyUrl = trim((string) Tools::getValue(self::CONF_PRIVACY_URL));
|
||||
$revocationUrl = trim((string) Tools::getValue(self::CONF_REVOCATION_URL));
|
||||
$retentionMonths = (int) Tools::getValue(self::CONF_RETENTION_MONTHS);
|
||||
|
||||
if (!Validate::isEmail($email)) {
|
||||
$output .= $this->displayError($this->l('Please enter a valid shop notification email.'));
|
||||
} else {
|
||||
Configuration::updateValue(self::CONF_SHOP_EMAIL, $email);
|
||||
Configuration::updateValue(self::CONF_RATE_LIMIT, (string) max(1, min(50, $rateLimit)));
|
||||
Configuration::updateValue(self::CONF_PRIVACY_URL, $privacyUrl);
|
||||
Configuration::updateValue(self::CONF_REVOCATION_URL, $revocationUrl);
|
||||
Configuration::updateValue(self::CONF_RETENTION_MONTHS, (string) max(0, $retentionMonths));
|
||||
$output .= $this->displayConfirmation($this->l('Settings updated.'));
|
||||
}
|
||||
}
|
||||
|
||||
if (Tools::isSubmit('submitCypWithdrawalPurge')) {
|
||||
$retentionMonths = (int) Configuration::get(self::CONF_RETENTION_MONTHS);
|
||||
if ($retentionMonths > 0) {
|
||||
$deleted = $this->purgeOldRecords($retentionMonths);
|
||||
$output .= $this->displayConfirmation(
|
||||
sprintf($this->l('%d withdrawal record(s) older than %d month(s) have been deleted.'), $deleted, $retentionMonths)
|
||||
);
|
||||
} else {
|
||||
$output .= $this->displayError($this->l('No retention period configured. Set a value greater than 0 first.'));
|
||||
}
|
||||
}
|
||||
|
||||
$adminLink = $this->context->link->getAdminLink('AdminSimpleWithdrawal');
|
||||
$withdrawalLink = $this->getWithdrawalLink();
|
||||
|
||||
$this->context->smarty->assign([
|
||||
'admin_link' => $adminLink,
|
||||
'withdrawal_link' => $withdrawalLink,
|
||||
]);
|
||||
|
||||
return $output . $this->renderForm() . $this->display(__FILE__, 'views/templates/admin/config_info.tpl');
|
||||
}
|
||||
|
||||
private function renderForm()
|
||||
{
|
||||
$helper = new HelperForm();
|
||||
$helper->show_toolbar = false;
|
||||
$helper->table = $this->table;
|
||||
$helper->module = $this;
|
||||
$helper->default_form_language = (int) Configuration::get('PS_LANG_DEFAULT');
|
||||
$helper->allow_employee_form_lang = (int) Configuration::get('PS_BO_ALLOW_EMPLOYEE_FORM_LANG');
|
||||
$helper->identifier = $this->name;
|
||||
$helper->submit_action = 'submitCypWithdrawalSettings';
|
||||
$helper->currentIndex = AdminController::$currentIndex . '&configure=' . $this->name;
|
||||
$helper->token = Tools::getAdminTokenLite('AdminModules');
|
||||
$helper->tpl_vars = [
|
||||
'fields_value' => [
|
||||
self::CONF_SHOP_EMAIL => (string) Configuration::get(self::CONF_SHOP_EMAIL),
|
||||
self::CONF_RATE_LIMIT => (string) Configuration::get(self::CONF_RATE_LIMIT),
|
||||
self::CONF_PRIVACY_URL => (string) Configuration::get(self::CONF_PRIVACY_URL),
|
||||
self::CONF_REVOCATION_URL => (string) Configuration::get(self::CONF_REVOCATION_URL),
|
||||
self::CONF_RETENTION_MONTHS => (string) Configuration::get(self::CONF_RETENTION_MONTHS),
|
||||
],
|
||||
'languages' => $this->context->controller->getLanguages(),
|
||||
'id_language' => (int) $this->context->language->id,
|
||||
];
|
||||
|
||||
$fieldsForm = [
|
||||
'form' => [
|
||||
'legend' => [
|
||||
'title' => $this->l('Withdrawal button settings'),
|
||||
'icon' => 'icon-undo',
|
||||
],
|
||||
'input' => [
|
||||
[
|
||||
'type' => 'text',
|
||||
'label' => $this->l('Shop notification email'),
|
||||
'name' => self::CONF_SHOP_EMAIL,
|
||||
'required' => true,
|
||||
'desc' => $this->l('New withdrawal declarations are sent to this address.'),
|
||||
],
|
||||
[
|
||||
'type' => 'text',
|
||||
'label' => $this->l('Rate limit per hour'),
|
||||
'name' => self::CONF_RATE_LIMIT,
|
||||
'required' => true,
|
||||
'desc' => $this->l('Maximum saved withdrawal declarations per email or IP hash per hour.'),
|
||||
],
|
||||
[
|
||||
'type' => 'text',
|
||||
'label' => $this->l('Privacy policy URL'),
|
||||
'name' => self::CONF_PRIVACY_URL,
|
||||
'required' => false,
|
||||
'desc' => $this->l('Link to your Datenschutzerklärung shown in the withdrawal form (GDPR Art. 13). Leave empty to show plain text without link.'),
|
||||
],
|
||||
[
|
||||
'type' => 'text',
|
||||
'label' => $this->l('Widerrufsbelehrung URL'),
|
||||
'name' => self::CONF_REVOCATION_URL,
|
||||
'required' => false,
|
||||
'desc' => $this->l('Link to your revocation policy page shown above the withdrawal form. Leave empty to hide the link.'),
|
||||
],
|
||||
[
|
||||
'type' => 'text',
|
||||
'label' => $this->l('Retention period (months)'),
|
||||
'name' => self::CONF_RETENTION_MONTHS,
|
||||
'required' => false,
|
||||
'desc' => $this->l('Delete withdrawal records older than this many months when purging. Set to 0 to keep records indefinitely (GDPR: define a retention period).'),
|
||||
],
|
||||
],
|
||||
'buttons' => [
|
||||
[
|
||||
'title' => $this->l('Purge old records'),
|
||||
'name' => 'submitCypWithdrawalPurge',
|
||||
'icon' => 'process-icon-delete',
|
||||
'class' => 'btn btn-default pull-right',
|
||||
],
|
||||
],
|
||||
'submit' => [
|
||||
'title' => $this->l('Save'),
|
||||
],
|
||||
],
|
||||
];
|
||||
|
||||
return $helper->generateForm([$fieldsForm]);
|
||||
}
|
||||
|
||||
public function hookDisplayFooter(array $params)
|
||||
{
|
||||
$this->context->smarty->assign([
|
||||
'withdrawal_link' => $this->getWithdrawalLink(),
|
||||
]);
|
||||
|
||||
return $this->display(__FILE__, 'views/templates/hook/footer.tpl');
|
||||
}
|
||||
|
||||
public function hookDisplayCustomerAccount(array $params)
|
||||
{
|
||||
$this->context->smarty->assign([
|
||||
'withdrawal_link' => $this->getWithdrawalLink(),
|
||||
]);
|
||||
|
||||
return $this->display(__FILE__, 'views/templates/hook/customer_account.tpl');
|
||||
}
|
||||
|
||||
public function getWithdrawalLink()
|
||||
{
|
||||
return $this->context->link->getModuleLink($this->name, 'withdraw', [], true);
|
||||
}
|
||||
|
||||
public function getFrontToken()
|
||||
{
|
||||
$guestId = isset($this->context->cookie->id_guest) ? (string) $this->context->cookie->id_guest : '';
|
||||
$customerId = isset($this->context->customer->id) ? (string) $this->context->customer->id : '';
|
||||
|
||||
return Tools::hash($this->name . '|withdraw|' . $guestId . '|' . $customerId . '|' . _COOKIE_KEY_);
|
||||
}
|
||||
|
||||
public function isValidFrontToken($token)
|
||||
{
|
||||
return hash_equals($this->getFrontToken(), (string) $token);
|
||||
}
|
||||
|
||||
public function cleanText($value, $maxLength = 255)
|
||||
{
|
||||
$value = trim((string) $value);
|
||||
$value = strip_tags($value);
|
||||
$value = preg_replace('/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/u', '', $value);
|
||||
if ($maxLength > 0 && Tools::strlen($value) > $maxLength) {
|
||||
$value = Tools::substr($value, 0, $maxLength);
|
||||
}
|
||||
|
||||
return $value;
|
||||
}
|
||||
|
||||
public function lookupOrderIdByReference($orderReference)
|
||||
{
|
||||
$orderReference = $this->cleanText($orderReference, 64);
|
||||
if ($orderReference === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
$sql = 'SELECT `id_order`
|
||||
FROM `' . _DB_PREFIX_ . 'orders`
|
||||
WHERE `reference` = "' . pSQL($orderReference) . '"
|
||||
AND `id_shop` = ' . (int) $this->context->shop->id . '
|
||||
ORDER BY `id_order` DESC';
|
||||
|
||||
$idOrder = (int) Db::getInstance()->getValue($sql);
|
||||
return $idOrder > 0 ? $idOrder : null;
|
||||
}
|
||||
|
||||
public function hashForPrivacy($value)
|
||||
{
|
||||
$value = trim((string) $value);
|
||||
if ($value === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
return hash('sha256', $value . '|' . _COOKIE_KEY_);
|
||||
}
|
||||
|
||||
public function getClientIp()
|
||||
{
|
||||
if (!empty($_SERVER['HTTP_CF_CONNECTING_IP'])) {
|
||||
return (string) $_SERVER['HTTP_CF_CONNECTING_IP'];
|
||||
}
|
||||
if (!empty($_SERVER['HTTP_X_FORWARDED_FOR'])) {
|
||||
$parts = explode(',', (string) $_SERVER['HTTP_X_FORWARDED_FOR']);
|
||||
return trim($parts[0]);
|
||||
}
|
||||
if (!empty($_SERVER['REMOTE_ADDR'])) {
|
||||
return (string) $_SERVER['REMOTE_ADDR'];
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
public function isRateLimited($customerEmail, $ipHash)
|
||||
{
|
||||
$limit = (int) Configuration::get(self::CONF_RATE_LIMIT);
|
||||
if ($limit <= 0) {
|
||||
$limit = 5;
|
||||
}
|
||||
|
||||
$conditions = ['`customer_email` = "' . pSQL($customerEmail) . '"'];
|
||||
if ($ipHash) {
|
||||
$conditions[] = '`customer_ip_hash` = "' . pSQL($ipHash) . '"';
|
||||
}
|
||||
|
||||
$sql = 'SELECT COUNT(*)
|
||||
FROM `' . _DB_PREFIX_ . pSQL(self::TABLE_REQUEST) . '`
|
||||
WHERE (`created_at` >= DATE_SUB(NOW(), INTERVAL 1 HOUR))
|
||||
AND (' . implode(' OR ', $conditions) . ')';
|
||||
|
||||
return (int) Db::getInstance()->getValue($sql) >= $limit;
|
||||
}
|
||||
|
||||
public function saveWithdrawal(array $data)
|
||||
{
|
||||
$createdAt = date('Y-m-d H:i:s');
|
||||
$ipHash = $this->hashForPrivacy($this->getClientIp());
|
||||
$userAgentHash = $this->hashForPrivacy(isset($_SERVER['HTTP_USER_AGENT']) ? (string) $_SERVER['HTTP_USER_AGENT'] : '');
|
||||
|
||||
$idOrder = $this->lookupOrderIdByReference($data['order_reference']);
|
||||
|
||||
$insert = [
|
||||
'id_order' => $idOrder ? (int) $idOrder : null,
|
||||
'order_reference' => $data['order_reference'],
|
||||
'customer_name' => $data['customer_name'],
|
||||
'customer_email' => $data['customer_email'],
|
||||
'withdrawal_scope' => $data['withdrawal_scope'],
|
||||
'withdrawal_items_text' => $data['withdrawal_items_text'] !== '' ? $data['withdrawal_items_text'] : null,
|
||||
'message' => $data['message'] !== '' ? $data['message'] : null,
|
||||
'created_at' => $createdAt,
|
||||
'status' => 'new',
|
||||
'id_shop' => (int) $this->context->shop->id,
|
||||
'id_lang' => (int) $this->context->language->id,
|
||||
'customer_ip_hash' => $ipHash ?: null,
|
||||
'user_agent_hash' => $userAgentHash ?: null,
|
||||
];
|
||||
|
||||
$ok = Db::getInstance()->insert(self::TABLE_REQUEST, $insert, false, true, Db::INSERT, false);
|
||||
if (!$ok) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$id = (int) Db::getInstance()->Insert_ID();
|
||||
return [
|
||||
'id_withdrawal_request' => $id,
|
||||
'created_at' => $createdAt,
|
||||
'id_order' => $idOrder,
|
||||
'customer_ip_hash' => $ipHash,
|
||||
];
|
||||
}
|
||||
|
||||
public function markConfirmationSent($idWithdrawalRequest)
|
||||
{
|
||||
return Db::getInstance()->update(
|
||||
self::TABLE_REQUEST,
|
||||
['confirmation_sent_at' => date('Y-m-d H:i:s')],
|
||||
'`id_withdrawal_request` = ' . (int) $idWithdrawalRequest
|
||||
);
|
||||
}
|
||||
|
||||
public function sendCustomerConfirmation(array $data, $createdAt)
|
||||
{
|
||||
$idLang = (int) $this->context->language->id;
|
||||
$idShop = (int) $this->context->shop->id;
|
||||
$isoCode = $this->getLangIsoCode($idLang);
|
||||
$shopName = (string) Configuration::get('PS_SHOP_NAME');
|
||||
$shopEmail = (string) Configuration::get('PS_SHOP_EMAIL');
|
||||
$scopeLabel = $this->getScopeLabel($data['withdrawal_scope'], $isoCode);
|
||||
|
||||
$templateVars = [
|
||||
'{customer_name}' => $data['customer_name'],
|
||||
'{order_reference}' => $data['order_reference'],
|
||||
'{scope_label}' => $scopeLabel,
|
||||
'{withdrawal_items_text}' => $data['withdrawal_items_text'] !== '' ? $data['withdrawal_items_text'] : '-',
|
||||
'{message}' => $data['message'] !== '' ? $data['message'] : '-',
|
||||
'{submitted_at}' => $this->formatDateTimeForMail($createdAt, $isoCode),
|
||||
'{shop_name}' => $shopName,
|
||||
];
|
||||
|
||||
try {
|
||||
return Mail::Send(
|
||||
$idLang,
|
||||
'withdrawal_confirmation',
|
||||
$this->getConfirmationSubject($isoCode),
|
||||
$templateVars,
|
||||
$data['customer_email'],
|
||||
$data['customer_name'],
|
||||
$shopEmail,
|
||||
$shopName,
|
||||
null,
|
||||
null,
|
||||
_PS_MODULE_DIR_ . $this->name . '/mails/',
|
||||
false,
|
||||
$idShop
|
||||
);
|
||||
} catch (\Exception $e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public function sendShopNotification(array $data, $createdAt, $idWithdrawalRequest)
|
||||
{
|
||||
$idLang = (int) $this->context->language->id;
|
||||
$idShop = (int) $this->context->shop->id;
|
||||
$isoCode = $this->getLangIsoCode($idLang);
|
||||
$shopName = (string) Configuration::get('PS_SHOP_NAME');
|
||||
$shopEmail = (string) Configuration::get('PS_SHOP_EMAIL');
|
||||
$toEmail = (string) Configuration::get(self::CONF_SHOP_EMAIL);
|
||||
if (!Validate::isEmail($toEmail)) {
|
||||
$toEmail = $shopEmail;
|
||||
}
|
||||
|
||||
$scopeLabel = $this->getScopeLabel($data['withdrawal_scope'], $isoCode);
|
||||
$adminLink = $this->context->link->getAdminLink('AdminSimpleWithdrawal');
|
||||
|
||||
$templateVars = [
|
||||
'{id_withdrawal_request}' => (string) $idWithdrawalRequest,
|
||||
'{customer_name}' => $data['customer_name'],
|
||||
'{customer_email}' => $data['customer_email'],
|
||||
'{order_reference}' => $data['order_reference'],
|
||||
'{scope_label}' => $scopeLabel,
|
||||
'{withdrawal_items_text}' => $data['withdrawal_items_text'] !== '' ? $data['withdrawal_items_text'] : '-',
|
||||
'{message}' => $data['message'] !== '' ? $data['message'] : '-',
|
||||
'{submitted_at}' => $this->formatDateTimeForMail($createdAt, $isoCode),
|
||||
'{admin_link}' => $adminLink,
|
||||
'{shop_name}' => $shopName,
|
||||
];
|
||||
|
||||
try {
|
||||
return Mail::Send(
|
||||
$idLang,
|
||||
'withdrawal_notification',
|
||||
$this->getNotificationSubject($data['order_reference'], $isoCode),
|
||||
$templateVars,
|
||||
$toEmail,
|
||||
null,
|
||||
$shopEmail,
|
||||
$shopName,
|
||||
null,
|
||||
null,
|
||||
_PS_MODULE_DIR_ . $this->name . '/mails/',
|
||||
false,
|
||||
$idShop
|
||||
);
|
||||
} catch (\Exception $e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public function purgeOldRecords($months)
|
||||
{
|
||||
$months = (int) $months;
|
||||
if ($months <= 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
$sql = 'SELECT COUNT(*) FROM `' . _DB_PREFIX_ . pSQL(self::TABLE_REQUEST) . '`
|
||||
WHERE `created_at` < DATE_SUB(NOW(), INTERVAL ' . $months . ' MONTH)';
|
||||
$count = (int) Db::getInstance()->getValue($sql);
|
||||
|
||||
if ($count > 0) {
|
||||
Db::getInstance()->execute(
|
||||
'DELETE FROM `' . _DB_PREFIX_ . pSQL(self::TABLE_REQUEST) . '`
|
||||
WHERE `created_at` < DATE_SUB(NOW(), INTERVAL ' . $months . ' MONTH)'
|
||||
);
|
||||
}
|
||||
|
||||
return $count;
|
||||
}
|
||||
|
||||
public function formatDateTimeForMail($dateTime, $isoCode = 'de')
|
||||
{
|
||||
$timestamp = strtotime((string) $dateTime);
|
||||
if (!$timestamp) {
|
||||
$timestamp = time();
|
||||
}
|
||||
|
||||
if ($isoCode === 'de') {
|
||||
return date('d.m.Y H:i:s', $timestamp) . ' Uhr';
|
||||
}
|
||||
|
||||
return date('Y-m-d H:i:s', $timestamp);
|
||||
}
|
||||
|
||||
private function getLangIsoCode($idLang)
|
||||
{
|
||||
$iso = Language::getIsoById((int) $idLang);
|
||||
return $iso ?: 'de';
|
||||
}
|
||||
|
||||
private function getScopeLabel($scope, $isoCode)
|
||||
{
|
||||
$labels = [
|
||||
'de' => ['partial' => 'Teil der Bestellung', 'full' => 'gesamte Bestellung'],
|
||||
'en' => ['partial' => 'part of the order', 'full' => 'full order'],
|
||||
];
|
||||
$map = isset($labels[$isoCode]) ? $labels[$isoCode] : $labels['de'];
|
||||
|
||||
return $scope === 'partial' ? $map['partial'] : $map['full'];
|
||||
}
|
||||
|
||||
private function getConfirmationSubject($isoCode)
|
||||
{
|
||||
$subjects = [
|
||||
'de' => 'Eingangsbestätigung Ihres Widerrufs',
|
||||
'en' => 'Confirmation of receipt of your withdrawal',
|
||||
];
|
||||
|
||||
return isset($subjects[$isoCode]) ? $subjects[$isoCode] : $subjects['de'];
|
||||
}
|
||||
|
||||
private function getNotificationSubject($orderReference, $isoCode)
|
||||
{
|
||||
if ($isoCode === 'en') {
|
||||
return 'New withdrawal: order ' . $orderReference;
|
||||
}
|
||||
|
||||
return 'Neuer Widerruf: Bestellung ' . $orderReference;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
<?php
|
||||
/** Silence is golden. */
|
||||
@@ -0,0 +1,2 @@
|
||||
<?php
|
||||
/** Silence is golden. */
|
||||
@@ -0,0 +1,14 @@
|
||||
<div class="panel">
|
||||
<h3><i class="icon-info-circle"></i> {l s='Installation notes' mod='simple_withdrawalbutton'}</h3>
|
||||
<p>
|
||||
{l s='Frontend withdrawal page:' mod='simple_withdrawalbutton'}
|
||||
<a href="{$withdrawal_link|escape:'html':'UTF-8'}" target="_blank" rel="noopener noreferrer">{$withdrawal_link|escape:'html':'UTF-8'}</a>
|
||||
</p>
|
||||
<p>
|
||||
{l s='Back office requests:' mod='simple_withdrawalbutton'}
|
||||
<a href="{$admin_link|escape:'html':'UTF-8'}">{l s='Open withdrawal requests' mod='simple_withdrawalbutton'}</a>
|
||||
</p>
|
||||
<p class="help-block">
|
||||
{l s='The module keeps existing withdrawal records when it is uninstalled. Delete the database table manually only after checking your retention obligations.' mod='simple_withdrawalbutton'}
|
||||
</p>
|
||||
</div>
|
||||
@@ -0,0 +1,2 @@
|
||||
<?php
|
||||
/** Silence is golden. */
|
||||
@@ -0,0 +1,81 @@
|
||||
<div class="panel">
|
||||
<div class="panel-heading">
|
||||
<i class="icon-undo"></i>
|
||||
{l s='Withdrawal request' mod='simple_withdrawalbutton'} #{$request.id_withdrawal_request|intval}
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-lg-8">
|
||||
<table class="table">
|
||||
<tbody>
|
||||
<tr>
|
||||
<th>{l s='Received' mod='simple_withdrawalbutton'}</th>
|
||||
<td>{$request.created_at|escape:'html':'UTF-8'}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>{l s='Confirmation sent' mod='simple_withdrawalbutton'}</th>
|
||||
<td>{if $request.confirmation_sent_at}{$request.confirmation_sent_at|escape:'html':'UTF-8'}{else}-{/if}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>{l s='Order reference' mod='simple_withdrawalbutton'}</th>
|
||||
<td>
|
||||
{$request.order_reference|escape:'html':'UTF-8'}
|
||||
{if $order_link != ''}
|
||||
<br><a href="{$order_link|escape:'html':'UTF-8'}">{l s='Open order' mod='simple_withdrawalbutton'}</a>
|
||||
{/if}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>{l s='Name' mod='simple_withdrawalbutton'}</th>
|
||||
<td>{$request.customer_name|escape:'html':'UTF-8'}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>{l s='Email' mod='simple_withdrawalbutton'}</th>
|
||||
<td><a href="mailto:{$request.customer_email|escape:'html':'UTF-8'}">{$request.customer_email|escape:'html':'UTF-8'}</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>{l s='Scope' mod='simple_withdrawalbutton'}</th>
|
||||
<td>
|
||||
{if $request.withdrawal_scope == 'partial'}
|
||||
{l s='Partial withdrawal' mod='simple_withdrawalbutton'}
|
||||
{else}
|
||||
{l s='Full order' mod='simple_withdrawalbutton'}
|
||||
{/if}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>{l s='Affected items / quantities' mod='simple_withdrawalbutton'}</th>
|
||||
<td>{if $request.withdrawal_items_text}{$request.withdrawal_items_text|escape:'html':'UTF-8'|nl2br nofilter}{else}-{/if}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>{l s='Message' mod='simple_withdrawalbutton'}</th>
|
||||
<td>{if $request.message}{$request.message|escape:'html':'UTF-8'|nl2br nofilter}{else}-{/if}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="col-lg-4">
|
||||
<form method="post" action="{$current_index|escape:'html':'UTF-8'}&token={$token|escape:'html':'UTF-8'}">
|
||||
<input type="hidden" name="{$identifier|escape:'html':'UTF-8'}" value="{$request.id_withdrawal_request|intval}">
|
||||
<div class="form-group">
|
||||
<label for="cyp_withdrawal_status">{l s='Status' mod='simple_withdrawalbutton'}</label>
|
||||
<select name="status" id="cyp_withdrawal_status" class="form-control">
|
||||
<option value="new" {if $request.status == 'new'}selected{/if}>{l s='New' mod='simple_withdrawalbutton'}</option>
|
||||
<option value="processing" {if $request.status == 'processing'}selected{/if}>{l s='Processing' mod='simple_withdrawalbutton'}</option>
|
||||
<option value="closed" {if $request.status == 'closed'}selected{/if}>{l s='Closed' mod='simple_withdrawalbutton'}</option>
|
||||
</select>
|
||||
</div>
|
||||
<button type="submit" name="submitSimpleWithdrawalStatus" value="1" class="btn btn-primary">
|
||||
{l s='Update status' mod='simple_withdrawalbutton'}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="panel-footer">
|
||||
<a href="{$back_link|escape:'html':'UTF-8'}" class="btn btn-default">
|
||||
<i class="process-icon-back"></i> {l s='Back to list' mod='simple_withdrawalbutton'}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,70 @@
|
||||
<section class="cyp-withdrawal-page">
|
||||
<header class="page-header">
|
||||
<h1>{l s='Widerruf bestätigen' mod='simple_withdrawalbutton'}</h1>
|
||||
</header>
|
||||
|
||||
<div class="card card-block">
|
||||
{if $errors_list|@count > 0}
|
||||
<div class="alert alert-danger">
|
||||
<ul>
|
||||
{foreach from=$errors_list item=error}
|
||||
<li>{$error|escape:'html':'UTF-8'}</li>
|
||||
{/foreach}
|
||||
</ul>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<p>{l s='Bitte prüfen Sie Ihre Angaben. Wenn die Angaben korrekt sind, können Sie Ihren Widerruf jetzt absenden.' mod='simple_withdrawalbutton'}</p>
|
||||
|
||||
<dl class="row">
|
||||
<dt class="col-sm-4">{l s='Name' mod='simple_withdrawalbutton'}</dt>
|
||||
<dd class="col-sm-8">{$form_data.customer_name|escape:'html':'UTF-8'}</dd>
|
||||
|
||||
<dt class="col-sm-4">{l s='E-Mail-Adresse' mod='simple_withdrawalbutton'}</dt>
|
||||
<dd class="col-sm-8">{$form_data.customer_email|escape:'html':'UTF-8'}</dd>
|
||||
|
||||
<dt class="col-sm-4">{l s='Bestellnummer / Bestellreferenz' mod='simple_withdrawalbutton'}</dt>
|
||||
<dd class="col-sm-8">{$form_data.order_reference|escape:'html':'UTF-8'}</dd>
|
||||
|
||||
<dt class="col-sm-4">{l s='Widerruf betrifft' mod='simple_withdrawalbutton'}</dt>
|
||||
<dd class="col-sm-8">
|
||||
{if $form_data.withdrawal_scope == 'partial'}
|
||||
{l s='einen Teil der Bestellung' mod='simple_withdrawalbutton'}
|
||||
{else}
|
||||
{l s='die gesamte Bestellung' mod='simple_withdrawalbutton'}
|
||||
{/if}
|
||||
</dd>
|
||||
|
||||
{if $form_data.withdrawal_scope == 'partial'}
|
||||
<dt class="col-sm-4">{l s='Betroffene Artikel / Mengen' mod='simple_withdrawalbutton'}</dt>
|
||||
<dd class="col-sm-8">{$form_data.withdrawal_items_text|escape:'html':'UTF-8'|nl2br nofilter}</dd>
|
||||
{/if}
|
||||
|
||||
{if $form_data.message != ''}
|
||||
<dt class="col-sm-4">{l s='Nachricht / Bemerkung' mod='simple_withdrawalbutton'}</dt>
|
||||
<dd class="col-sm-8">{$form_data.message|escape:'html':'UTF-8'|nl2br nofilter}</dd>
|
||||
{/if}
|
||||
</dl>
|
||||
|
||||
<form method="post" action="{$action_url|escape:'html':'UTF-8'}">
|
||||
<input type="hidden" name="csrf_token" value="{$csrf_token|escape:'html':'UTF-8'}">
|
||||
<input type="hidden" name="customer_name" value="{$form_data.customer_name|escape:'html':'UTF-8'}">
|
||||
<input type="hidden" name="customer_email" value="{$form_data.customer_email|escape:'html':'UTF-8'}">
|
||||
<input type="hidden" name="order_reference" value="{$form_data.order_reference|escape:'html':'UTF-8'}">
|
||||
<input type="hidden" name="withdrawal_scope" value="{$form_data.withdrawal_scope|escape:'html':'UTF-8'}">
|
||||
<input type="hidden" name="withdrawal_items_text" value="{$form_data.withdrawal_items_text|escape:'html':'UTF-8'}">
|
||||
<input type="hidden" name="message" value="{$form_data.message|escape:'html':'UTF-8'}">
|
||||
<div style="position:absolute; left:-10000px; top:auto; width:1px; height:1px; overflow:hidden;" aria-hidden="true">
|
||||
<label for="cyp_withdrawal_hp_confirm">Website</label>
|
||||
<input type="text" id="cyp_withdrawal_hp_confirm" name="cyp_hp_v1" value="" tabindex="-1" autocomplete="off">
|
||||
</div>
|
||||
|
||||
<button type="submit" name="submit_withdrawal_confirm" value="1" class="btn btn-primary">
|
||||
{l s='Widerruf bestätigen' mod='simple_withdrawalbutton'}
|
||||
</button>
|
||||
<button type="submit" name="submit_withdrawal_back" value="1" class="btn btn-secondary" formaction="{$action_url|escape:'html':'UTF-8'}">
|
||||
{l s='Angaben ändern' mod='simple_withdrawalbutton'}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</section>
|
||||
@@ -0,0 +1,127 @@
|
||||
<section class="cyp-withdrawal-page">
|
||||
<header class="page-header">
|
||||
<h1>{l s='Vertrag widerrufen' mod='simple_withdrawalbutton'}</h1>
|
||||
</header>
|
||||
|
||||
<div class="card card-block">
|
||||
<p>
|
||||
{l s='Sie können hier den Vertrag zu einer Bestellung vollständig oder teilweise widerrufen. Bitte geben Sie die Bestellnummer und bei einem Teilwiderruf die betroffenen Artikel und Mengen an.' mod='simple_withdrawalbutton'}
|
||||
</p>
|
||||
|
||||
{if isset($revocation_url) && $revocation_url != ''}
|
||||
<p>
|
||||
<a href="{$revocation_url|escape:'html':'UTF-8'}" target="_blank" rel="noopener">
|
||||
{l s='Unsere Widerrufsbelehrung (14-Tage-Frist)' mod='simple_withdrawalbutton'}
|
||||
</a>
|
||||
</p>
|
||||
{/if}
|
||||
|
||||
{if $errors_list|@count > 0}
|
||||
<div class="alert alert-danger">
|
||||
<ul>
|
||||
{foreach from=$errors_list item=error}
|
||||
<li>{$error|escape:'html':'UTF-8'}</li>
|
||||
{/foreach}
|
||||
</ul>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<form method="post" action="{$action_url|escape:'html':'UTF-8'}" class="cyp-withdrawal-form" novalidate>
|
||||
<input type="hidden" name="csrf_token" value="{$csrf_token|escape:'html':'UTF-8'}">
|
||||
<div style="position:absolute; left:-10000px; top:auto; width:1px; height:1px; overflow:hidden;" aria-hidden="true">
|
||||
<label for="cyp_withdrawal_hp">Website</label>
|
||||
<input type="text" id="cyp_withdrawal_hp" name="cyp_hp_v1" value="" tabindex="-1" autocomplete="off">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="cyp_withdrawal_customer_name" class="form-control-label">
|
||||
{l s='Name' mod='simple_withdrawalbutton'} <span aria-hidden="true">*</span>
|
||||
</label>
|
||||
<input class="form-control" type="text" id="cyp_withdrawal_customer_name" name="customer_name" maxlength="255" required value="{$form_data.customer_name|default:''|escape:'html':'UTF-8'}">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="cyp_withdrawal_customer_email" class="form-control-label">
|
||||
{l s='E-Mail-Adresse für die Eingangsbestätigung' mod='simple_withdrawalbutton'} <span aria-hidden="true">*</span>
|
||||
</label>
|
||||
<input class="form-control" type="email" id="cyp_withdrawal_customer_email" name="customer_email" maxlength="255" required value="{$form_data.customer_email|default:''|escape:'html':'UTF-8'}">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="cyp_withdrawal_order_reference" class="form-control-label">
|
||||
{l s='Bestellnummer / Bestellreferenz' mod='simple_withdrawalbutton'} <span aria-hidden="true">*</span>
|
||||
</label>
|
||||
<input class="form-control" type="text" id="cyp_withdrawal_order_reference" name="order_reference" maxlength="64" required value="{$form_data.order_reference|default:''|escape:'html':'UTF-8'}">
|
||||
</div>
|
||||
|
||||
<fieldset class="form-group">
|
||||
<legend class="form-control-label">
|
||||
{l s='Widerruf betrifft' mod='simple_withdrawalbutton'} <span aria-hidden="true">*</span>
|
||||
</legend>
|
||||
<label class="custom-radio" for="cyp_withdrawal_scope_full">
|
||||
<input type="radio" id="cyp_withdrawal_scope_full" name="withdrawal_scope" value="full" {if !isset($form_data.withdrawal_scope) || $form_data.withdrawal_scope != 'partial'}checked{/if}>
|
||||
<span></span>
|
||||
{l s='die gesamte Bestellung' mod='simple_withdrawalbutton'}
|
||||
</label>
|
||||
<br>
|
||||
<label class="custom-radio" for="cyp_withdrawal_scope_partial">
|
||||
<input type="radio" id="cyp_withdrawal_scope_partial" name="withdrawal_scope" value="partial" {if isset($form_data.withdrawal_scope) && $form_data.withdrawal_scope == 'partial'}checked{/if}>
|
||||
<span></span>
|
||||
{l s='einen Teil der Bestellung' mod='simple_withdrawalbutton'}
|
||||
</label>
|
||||
</fieldset>
|
||||
|
||||
<div class="form-group" id="cyp-withdrawal-items-group">
|
||||
<label for="cyp_withdrawal_items_text" class="form-control-label">
|
||||
{l s='Welche Artikel / Mengen möchten Sie widerrufen?' mod='simple_withdrawalbutton'}
|
||||
</label>
|
||||
<textarea class="form-control" id="cyp_withdrawal_items_text" name="withdrawal_items_text" rows="4" maxlength="5000" placeholder="{l s='Beispiel: 1x Tomizu 720 ml, 2x Magokoro 300 ml' mod='simple_withdrawalbutton'}">{$form_data.withdrawal_items_text|default:''|escape:'html':'UTF-8'}</textarea>
|
||||
<small class="form-text text-muted">
|
||||
{l s='Nur bei einem Teilwiderruf erforderlich.' mod='simple_withdrawalbutton'}
|
||||
</small>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="cyp_withdrawal_message" class="form-control-label">
|
||||
{l s='Nachricht / Bemerkung optional' mod='simple_withdrawalbutton'}
|
||||
</label>
|
||||
<textarea class="form-control" id="cyp_withdrawal_message" name="message" rows="4" maxlength="5000">{$form_data.message|default:''|escape:'html':'UTF-8'}</textarea>
|
||||
</div>
|
||||
|
||||
<p class="small text-muted">
|
||||
{l s='Pflichtfelder sind mit * gekennzeichnet. Ein Widerrufsgrund ist nicht erforderlich.' mod='simple_withdrawalbutton'}
|
||||
</p>
|
||||
|
||||
<p class="small text-muted">
|
||||
{l s='Ihre Angaben (Name, E-Mail-Adresse) werden zur Bearbeitung Ihrer Widerrufserklärung verarbeitet. Rechtsgrundlage: Art. 6 Abs. 1 lit. b und c DSGVO.' mod='simple_withdrawalbutton'}
|
||||
{if isset($privacy_url) && $privacy_url != ''}
|
||||
<a href="{$privacy_url|escape:'html':'UTF-8'}" target="_blank" rel="noopener">
|
||||
{l s='Datenschutzerklärung' mod='simple_withdrawalbutton'}
|
||||
</a>
|
||||
{/if}
|
||||
</p>
|
||||
|
||||
<button type="submit" name="submit_withdrawal_prepare" value="1" class="btn btn-primary">
|
||||
{l s='Angaben prüfen' mod='simple_withdrawalbutton'}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<script>
|
||||
(function () {
|
||||
var partial = document.getElementById('cyp_withdrawal_scope_partial');
|
||||
var full = document.getElementById('cyp_withdrawal_scope_full');
|
||||
var group = document.getElementById('cyp-withdrawal-items-group');
|
||||
var textarea = document.getElementById('cyp_withdrawal_items_text');
|
||||
function update() {
|
||||
if (!group || !partial || !textarea) return;
|
||||
var show = partial.checked;
|
||||
group.style.display = show ? '' : 'none';
|
||||
textarea.required = show;
|
||||
}
|
||||
if (partial) partial.addEventListener('change', update);
|
||||
if (full) full.addEventListener('change', update);
|
||||
update();
|
||||
})();
|
||||
</script>
|
||||
@@ -0,0 +1,2 @@
|
||||
<?php
|
||||
/** Silence is golden. */
|
||||
@@ -0,0 +1,59 @@
|
||||
<section class="cyp-withdrawal-page">
|
||||
<header class="page-header">
|
||||
<h1>{l s='Widerruf übermittelt' mod='simple_withdrawalbutton'}</h1>
|
||||
</header>
|
||||
|
||||
<div class="card card-block">
|
||||
{if isset($success_data.mail_ok) && $success_data.mail_ok}
|
||||
<div class="alert alert-success">
|
||||
{l s='Ihr Widerruf wurde übermittelt. Eine Eingangsbestätigung wurde an die angegebene E-Mail-Adresse gesendet.' mod='simple_withdrawalbutton'}
|
||||
</div>
|
||||
{else}
|
||||
<div class="alert alert-warning">
|
||||
{l s='Ihr Widerruf wurde gespeichert, aber die automatische Eingangsbestätigung konnte möglicherweise nicht versendet werden. Bitte kontaktieren Sie uns zusätzlich per E-Mail, falls Sie keine Bestätigung erhalten.' mod='simple_withdrawalbutton'}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<dl class="row">
|
||||
{if isset($success_data.created_at)}
|
||||
<dt class="col-sm-4">{l s='Eingang:' mod='simple_withdrawalbutton'}</dt>
|
||||
<dd class="col-sm-8">{$success_data.created_at|escape:'html':'UTF-8'}</dd>
|
||||
{/if}
|
||||
|
||||
{if isset($success_data.customer_email)}
|
||||
<dt class="col-sm-4">{l s='E-Mail-Adresse:' mod='simple_withdrawalbutton'}</dt>
|
||||
<dd class="col-sm-8">{$success_data.customer_email|escape:'html':'UTF-8'}</dd>
|
||||
{/if}
|
||||
|
||||
{if isset($success_data.order_reference) && $success_data.order_reference != ''}
|
||||
<dt class="col-sm-4">{l s='Bestellnummer / Bestellreferenz' mod='simple_withdrawalbutton'}</dt>
|
||||
<dd class="col-sm-8">{$success_data.order_reference|escape:'html':'UTF-8'}</dd>
|
||||
{/if}
|
||||
|
||||
{if isset($success_data.withdrawal_scope)}
|
||||
<dt class="col-sm-4">{l s='Widerruf betrifft' mod='simple_withdrawalbutton'}</dt>
|
||||
<dd class="col-sm-8">
|
||||
{if $success_data.withdrawal_scope == 'partial'}
|
||||
{l s='einen Teil der Bestellung' mod='simple_withdrawalbutton'}
|
||||
{else}
|
||||
{l s='die gesamte Bestellung' mod='simple_withdrawalbutton'}
|
||||
{/if}
|
||||
</dd>
|
||||
{/if}
|
||||
|
||||
{if isset($success_data.withdrawal_items_text) && $success_data.withdrawal_items_text != ''}
|
||||
<dt class="col-sm-4">{l s='Betroffene Artikel / Mengen' mod='simple_withdrawalbutton'}</dt>
|
||||
<dd class="col-sm-8">{$success_data.withdrawal_items_text|escape:'html':'UTF-8'|nl2br nofilter}</dd>
|
||||
{/if}
|
||||
|
||||
{if isset($success_data.message) && $success_data.message != ''}
|
||||
<dt class="col-sm-4">{l s='Nachricht / Bemerkung' mod='simple_withdrawalbutton'}</dt>
|
||||
<dd class="col-sm-8">{$success_data.message|escape:'html':'UTF-8'|nl2br nofilter}</dd>
|
||||
{/if}
|
||||
</dl>
|
||||
|
||||
<p class="small text-muted">
|
||||
{l s='Diese Bestätigung betrifft nur den Eingang Ihrer Widerrufserklärung. Die weitere Bearbeitung und Prüfung erfolgt separat.' mod='simple_withdrawalbutton'}
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
@@ -0,0 +1,6 @@
|
||||
<a class="col-lg-4 col-md-6 col-sm-6 col-xs-12" id="withdrawal-link" href="{$withdrawal_link|escape:'html':'UTF-8'}" rel="nofollow">
|
||||
<span class="link-item">
|
||||
<i class="material-icons">undo</i>
|
||||
{l s='Vertrag widerrufen' mod='simple_withdrawalbutton'}
|
||||
</span>
|
||||
</a>
|
||||
@@ -0,0 +1,5 @@
|
||||
<div class="cyp-withdrawal-footer" style="margin-top: 1rem;">
|
||||
<a href="{$withdrawal_link|escape:'html':'UTF-8'}" class="cyp-withdrawal-link" rel="nofollow">
|
||||
{l s='Vertrag widerrufen' mod='simple_withdrawalbutton'}
|
||||
</a>
|
||||
</div>
|
||||
@@ -0,0 +1,2 @@
|
||||
<?php
|
||||
/** Silence is golden. */
|
||||
@@ -0,0 +1,2 @@
|
||||
<?php
|
||||
/** Silence is golden. */
|
||||
Reference in New Issue
Block a user