Backend Revocaton

This commit is contained in:
Thomas Peterson 2026-06-24 13:38:41 +02:00
parent 51061f1699
commit 982642543d
22 changed files with 833 additions and 4 deletions

View File

@ -476,7 +476,7 @@ use Symfony\Component\Config\Loader\ParamConfigurator as Param;
* datetime?: array{
* default_format?: scalar|Param|null, // Default: "Y-m-d\\TH:i:sP"
* default_deserialization_formats?: list<scalar|Param|null>,
* default_timezone?: scalar|Param|null, // Default: "Europe/Berlin"
* default_timezone?: scalar|Param|null, // Default: "UTC"
* cdata?: scalar|Param|null, // Default: true
* },
* array_collection?: array{
@ -576,7 +576,7 @@ use Symfony\Component\Config\Loader\ParamConfigurator as Param;
* datetime?: array{
* default_format?: scalar|Param|null, // Default: "Y-m-d\\TH:i:sP"
* default_deserialization_formats?: list<scalar|Param|null>,
* default_timezone?: scalar|Param|null, // Default: "Europe/Berlin"
* default_timezone?: scalar|Param|null, // Default: "UTC"
* cdata?: scalar|Param|null, // Default: true
* },
* array_collection?: array{

View File

@ -0,0 +1,77 @@
<?php
namespace PSC\Shop\QueueBundle\Event\Order\Revocation;
use PSC\Shop\QueueBundle\Event\Event;
use Symfony\Component\DependencyInjection\Attribute\AutoconfigureTag;
/**
* Wird aus dem Alt-System (Zend) ausgelöst, sobald ein Kunde auf der
* auftragsbezogenen Widerrufsseite eine konkrete Position widerrufen hat.
* Das Widerruf-Flag und der Zeitpunkt sind dann bereits auf der Position
* gesetzt. Hierauf kann z.B. der Queue-Typ "Mail" reagieren.
*/
#[AutoconfigureTag('events')]
class PositionRequest extends Event
{
/** @var string */
protected $order;
/** @var string */
protected $position;
public function getType()
{
return 'order_revocation_position_request';
}
public function getDescription()
{
return 'Widerruf für eine Position durchgeführt';
}
public function getData()
{
return array(
'order' => $this->order,
'position' => $this->position,
);
}
public function setData($data)
{
$this->order = $data['order'] ?? null;
$this->position = $data['position'] ?? null;
}
/**
* @return string
*/
public function getOrder()
{
return $this->order;
}
/**
* @param string $order
*/
public function setOrder($order)
{
$this->order = $order;
}
/**
* @return string
*/
public function getPosition()
{
return $this->position;
}
/**
* @param string $position
*/
public function setPosition($position)
{
$this->position = $position;
}
}

View File

@ -0,0 +1,97 @@
<?php
namespace PSC\Shop\QueueBundle\Event\Order\Revocation;
use PSC\Shop\QueueBundle\Event\Event;
use Symfony\Component\DependencyInjection\Attribute\AutoconfigureTag;
/**
* Wird aus dem Alt-System (Zend) ausgelöst, sobald ein Kunde über das
* Widerrufsformular (/revocationform) für einen Auftrag einen Widerruf
* anfordert. Hierauf kann z.B. der Queue-Typ "Mail" reagieren und dem
* Kunden den Deep-Link zur auftragsbezogenen Widerrufsseite zusenden.
*/
#[AutoconfigureTag('events')]
class Request extends Event
{
/** @var string */
protected $order;
/** @var string */
protected $alias;
/** @var string */
protected $email;
public function getType()
{
return 'order_revocation_request';
}
public function getDescription()
{
return 'Widerruf für einen Auftrag angefordert';
}
public function getData()
{
return array(
'order' => $this->order,
'alias' => $this->alias,
'email' => $this->email,
);
}
public function setData($data)
{
$this->order = $data['order'] ?? null;
$this->alias = $data['alias'] ?? null;
$this->email = $data['email'] ?? null;
}
/**
* @return string
*/
public function getOrder()
{
return $this->order;
}
/**
* @param string $order
*/
public function setOrder($order)
{
$this->order = $order;
}
/**
* @return string
*/
public function getAlias()
{
return $this->alias;
}
/**
* @param string $alias
*/
public function setAlias($alias)
{
$this->alias = $alias;
}
/**
* @return string
*/
public function getEmail()
{
return $this->email;
}
/**
* @param string $email
*/
public function setEmail($email)
{
$this->email = $email;
}
}

View File

@ -25,6 +25,8 @@ use PSC\Shop\QueueBundle\Event\Contact\Password\Reset\Finish;
use PSC\Shop\QueueBundle\Event\Contact\Password\Reset\Start;
use PSC\Shop\QueueBundle\Event\EventInterface;
use PSC\Shop\QueueBundle\Event\Order\Create;
use PSC\Shop\QueueBundle\Event\Order\Revocation\PositionRequest as RevocationPositionRequest;
use PSC\Shop\QueueBundle\Event\Order\Revocation\Request as RevocationRequest;
use PSC\Shop\QueueBundle\Event\Position\ApprovalExternalAccept;
use PSC\Shop\QueueBundle\Event\Position\ApprovalExternalDeclined;
use PSC\Shop\QueueBundle\Event\Position\ApprovalExternalRequest;
@ -1709,6 +1711,155 @@ class Mail implements QueueInterface, ConfigurableElementInterface
return false;
}
return true;
}
if ($event instanceof RevocationRequest) {
$templateVars->loadOrder($event->getOrder());
$vars = $templateVars->getTwigVars();
// Zusätzliche Variablen für die Mail an den Kunden inkl. Deep-Link
// auf die auftragsbezogene Widerrufsseite im Alt-System.
$vars['revocationEmail'] = $event->getEmail();
$vars['revocationAlias'] = $event->getAlias();
$vars['revocationLink'] = '/revocationform/order/uuid/' . $event->getOrder();
try {
$message = new TemplatedEmail()
->subject($subject->render($vars))
->from(new Address($from->render($vars), $fromName->render($vars)))
->to(new Address($to->render($vars), $toName->render($vars)));
if ($text) {
$message->text($text->render($vars));
}
if ($bcc) {
$bccArray = explode(',', $bcc->render($vars));
foreach ($bccArray as $bc) {
if (trim($bc) != '') {
$message->addBcc(trim($bc));
}
}
}
if ($html) {
$message->html($html->render(array_merge($vars, [
'email' => new WrappedTemplatedEmail($this->_template, $message),
])));
}
foreach ($mailDoc->getFiles() as $file) {
if ($file['name'] != '' && $file['content'] != '') {
$fileContent = $this->_template->createTemplate($file['content']);
$fileName = $this->_template->createTemplate($file['name']);
$message->attach($fileContent->render($vars), $fileName->render($vars));
}
}
$this->_logService->createLogEntry(
$templateVars->getOrder()->getShop(),
new Contact(),
LogEntry::INFO,
PSCShopQueueBundle::class,
$queue->getName(),
'Revocation Request Mail send',
[
'message' => $message->getTextBody(),
'from' => $from->render($vars),
'to' => $to->render($vars),
'subject' => $subject->render($vars),
],
);
$this->sendMail($message);
} catch (\Exception $e) {
$this->_logService->createLogEntry(
$templateVars->getOrder()->getShop(),
new Contact(),
LogEntry::ERROR,
PSCShopQueueBundle::class,
$queue->getName(),
'Revocation Request Mail error',
[
'error' => $e->getMessage(),
],
);
$this->_error = $e->getMessage();
return false;
}
return true;
}
if ($event instanceof RevocationPositionRequest) {
$templateVars->loadOrder($event->getOrder());
$vars = $templateVars->getPosTwigVars($event->getPosition());
try {
$message = new TemplatedEmail()
->subject($subject->render($vars))
->from(
new Address(
$from->render($templateVars->getTwigVars()),
$fromName->render($templateVars->getTwigVars()),
),
)
->to(
new Address(
$to->render($templateVars->getTwigVars()),
$toName->render($templateVars->getTwigVars()),
),
);
if ($text) {
$message->text($text->render($vars));
}
if ($bcc) {
$bccArray = explode(',', $bcc->render($vars));
foreach ($bccArray as $bc) {
if (trim($bc) != '') {
$message->addBcc(trim($bc));
}
}
}
if ($html) {
$message->html($html->render(array_merge($vars, [
'email' => new WrappedTemplatedEmail($this->_template, $message),
])));
}
foreach ($mailDoc->getFiles() as $file) {
if ($file['name'] != '' && $file['content'] != '') {
$fileContent = $this->_template->createTemplate($file['content']);
$fileName = $this->_template->createTemplate($file['name']);
$message->attach($fileContent->render($vars), $fileName->render($vars));
}
}
$this->_logService->createLogEntry(
$templateVars->getOrder()->getShop(),
new Contact(),
LogEntry::INFO,
PSCShopQueueBundle::class,
$queue->getName(),
'Revocation Position Request Mail send',
[
'message' => $message->getTextBody(),
'from' => $from->render($templateVars->getTwigVars()),
'to' => $to->render($templateVars->getTwigVars()),
'subject' => $subject->render($vars),
],
);
$this->sendMail($message);
} catch (\Exception $e) {
$this->_logService->createLogEntry(
$templateVars->getOrder()->getShop(),
new Contact(),
LogEntry::ERROR,
PSCShopQueueBundle::class,
$queue->getName(),
'Revocation Position Request Mail error',
[
'error' => $e->getMessage(),
],
);
$this->_error = $e->getMessage();
return false;
}
return true;
}
}

View File

@ -1,8 +1,20 @@
info:
datum: 10.06.2026
release: 2.3.6
datum: 23.06.2026
release: 2.3.7
changelog:
- version: 2.3.7
datum: 23.06.2026
changes:
- "Dashboard: Speicherverbrauch von System- und Daten-Volume mit Icons"
- "Produkt: Option \"Widerrufsbutton aktivieren\" (pro Produkt)"
- "Auftragsdetails: Positionen-Ansicht überarbeitet (Spalten Widerruf/Status, Preise, Aktionen; Vorschaubilder wie in der Auftragsliste)"
- "Auftragsposition: Widerruf-Status mit Datum (ob/wann widerrufen) inkl. Anzeige im Backend"
- "Widerrufsformular im Shop unter /revocationform (Bestellnummer + E-Mail + Captcha)"
- "Deep-Link /revocationform/order/uuid/<uuid>: Kunde kann pro Position widerrufen (Flag + Zeitpunkt werden gesetzt)"
- "Neue Events für die Mail-Queue: order_revocation_request (Widerruf angefordert) und order_revocation_position_request (Position widerrufen)"
- "Mail-Variablen order_revocation_request: order, shop, contact, revocationEmail, revocationAlias, revocationLink (Deep-Link-Pfad, z.B. https://{Shop-Domain}{{ revocationLink }})"
- "Mail-Variablen order_revocation_position_request: order, position, shop, contact"
- version: 2.3.6
datum: 10.06.2026
changes:

View File

@ -124,6 +124,8 @@ abstract class BaseArticle extends Doctrine_Record
$this->hasColumn('not_buy', 'boolean');
$this->hasColumn('revoke_button_enable', 'boolean', 1);
$this->hasColumn('file', 'string', 255, array('type' => 'string', 'length' => '255'));
$this->hasColumn('file1', 'string', 255, array('type' => 'string', 'length' => '255'));
$this->hasColumn('file2', 'string', 255, array('type' => 'string', 'length' => '255'));

View File

@ -103,6 +103,9 @@ abstract class BaseOrderspos extends Doctrine_Record
$this->hasColumn('ref', 'string', 255);
$this->hasColumn('kst', 'string', 255);
$this->hasColumn('revoked', 'boolean', 1);
$this->hasColumn('revoked_at', 'timestamp');
}
public function setUp()

View File

@ -0,0 +1,28 @@
[revocation]
; general form metainformation
global.class ="form-horizontal"
user.revocation.action = "/revocationform"
user.revocation.method = "post"
user.revocation.legend = "Widerruf"
; Bestellnummer
user.revocation.elements.ordernumber.type = "text"
user.revocation.elements.ordernumber.options.label = "Bestellnummer"
user.revocation.elements.ordernumber.options.required = true
; Email
user.revocation.elements.self_email.type = "text"
user.revocation.elements.self_email.options.label = "Email"
user.revocation.elements.self_email.options.required = true
user.revocation.elements.self_email.options.validators.email.validator = "EmailAddress"
; Captcha
user.revocation.elements.cp.type = "captcha"
user.revocation.elements.cp.options.label = ""
user.revocation.elements.cp.options.captcha.captcha = "Image"
user.revocation.elements.submit.type = "submit"
user.revocation.elements.submit.options.class = "submit btn btn-success btn-large"
user.revocation.elements.submit.options.label = "Widerruf absenden"

View File

@ -0,0 +1,17 @@
<div class="row">
<div class="col-xs-12">
<br>
<h1><?php echo $this->translate('Widerruf erhalten') ?></h1>
<p>
<?php echo $this->translate('Wir haben Ihren Widerruf erhalten und werden ihn bearbeiten.')?>
</p>
<br>
</div>
</div>

View File

@ -0,0 +1,52 @@
<div class="row">
<div class="col-xs-12">
<h1><?php echo $this->translate('Widerruf') ?></h1>
<p><strong><?php echo $this->translate('Sie möchten eine Bestellung widerrufen?')?></strong></p>
<br>
</div>
<div class="col-sm-6 col-xs-12">
<p>
<?php echo $this->translate('Geben Sie bitte Ihre Bestellnummer und die E-Mail-Adresse Ihrer Bestellung an und fahren Sie mit "Widerruf absenden" fort.')?>
</p>
</div>
<div class="col-sm-6 col-xs-12">
<div class="well" style="padding: 3em">
<?php
// Manuelles Rendern (wie bei den resale-Formularen), damit das Image-Captcha
// in bootstrap4_api zuverlässig erscheint. Der EasyBib-BOOTSTRAP-Dekorator
// rendert für Captcha-Elemente kein Bild/Eingabefeld.
?>
<form method="post" action="/revocationform" class="niceform form-horizontal" id="revocationform" enctype="application/x-www-form-urlencoded">
<div class="control-group form-group">
<label class="control-label form-label" for="ordernumber"><?php echo $this->translate('Bestellnummer') ?></label>
<div class="controls form-controls">
<input type="text" name="ordernumber" id="ordernumber" class="form-control required" value="<?php echo $this->escape($this->form->ordernumber->getValue()) ?>" />
</div>
</div>
<div class="control-group form-group">
<label class="control-label form-label" for="self_email"><?php echo $this->translate('Email') ?></label>
<div class="controls form-controls">
<input type="text" name="self_email" id="self_email" class="form-control required" value="<?php echo $this->escape($this->form->self_email->getValue()) ?>" />
</div>
</div>
<div id="captcha" class="control-group form-group">
<?php echo $this->form->cp ?>
</div>
<input type="submit" class="btn btn-success btn-large" value="<?php echo $this->translate('Widerruf absenden') ?>" />
</form>
</div>
</div>
</div>

View File

@ -0,0 +1,41 @@
<div class="row">
<div class="col-xs-12">
<h1><?php echo $this->translate('Widerruf') ?> &ndash; <?php echo $this->escape($this->order->alias) ?></h1>
<p><?php echo $this->translate('Wählen Sie die Positionen aus, die Sie widerrufen möchten.')?></p>
<br>
</div>
<div class="col-xs-12">
<table class="table table-striped">
<thead>
<tr>
<th><?php echo $this->translate('Pos') ?></th>
<th><?php echo $this->translate('Produkt') ?></th>
<th><?php echo $this->translate('Auflage') ?></th>
<th><?php echo $this->translate('Widerruf') ?></th>
</tr>
</thead>
<tbody>
<?php foreach ($this->positions as $pos): ?>
<tr>
<td><?php echo $this->escape($pos->pos) ?></td>
<td><?php echo $this->escape($pos->Article->title) ?></td>
<td><?php echo $this->escape($pos->count) ?></td>
<td>
<?php if ($pos->revoked): ?>
<span class="label label-info"><?php echo $this->translate('Widerrufen am') ?> <?php echo $this->escape($pos->revoked_at) ?></span>
<?php elseif ($pos->Article->revoke_button_enable): ?>
<form method="post" action="/revocationform/order/uuid/<?php echo $this->escape($this->order->uuid) ?>" style="margin:0">
<input type="hidden" name="pos" value="<?php echo $this->escape($pos->uuid) ?>" />
<button type="submit" class="btn btn-warning btn-sm"><?php echo $this->translate('Widerrufen') ?></button>
</form>
<?php else: ?>
<span class="text-muted"><?php echo $this->translate('Widerruf nicht möglich') ?></span>
<?php endif; ?>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
</div>

View File

@ -0,0 +1,28 @@
[revocation]
; general form metainformation
global.class ="form-horizontal"
user.revocation.action = "/revocationform"
user.revocation.method = "post"
user.revocation.legend = "Widerruf"
; Bestellnummer
user.revocation.elements.ordernumber.type = "text"
user.revocation.elements.ordernumber.options.label = "Bestellnummer"
user.revocation.elements.ordernumber.options.required = true
; Email
user.revocation.elements.self_email.type = "text"
user.revocation.elements.self_email.options.label = "Email"
user.revocation.elements.self_email.options.required = true
user.revocation.elements.self_email.options.validators.email.validator = "EmailAddress"
; Captcha
user.revocation.elements.cp.type = "captcha"
user.revocation.elements.cp.options.label = ""
user.revocation.elements.cp.options.captcha.captcha = "Image"
user.revocation.elements.submit.type = "submit"
user.revocation.elements.submit.options.class = "submit btn btn-success btn-large"
user.revocation.elements.submit.options.label = "Widerruf absenden"

View File

@ -0,0 +1,7 @@
<div class="md:w-2/4 w-3/4 m-auto mt-5">
<h1 class="text-lg mb-2"><?php echo $this->translate('Widerruf erhalten') ?></h1>
<p class="mb-2"><?php echo $this->translate('Wir haben Ihren Widerruf erhalten und werden ihn bearbeiten.')?></p>
</div>

View File

@ -0,0 +1,13 @@
<div class="md:w-2/4 w-11/12 m-auto mt-8 mb-10">
<h1 class="text-2xl font-semibold mb-2"><?php echo $this->translate('Widerruf') ?></h1>
<p class="mb-1"><strong><?php echo $this->translate('Sie möchten eine Bestellung widerrufen?')?></strong></p>
<p class="mb-6 opacity-80"><?php echo $this->translate('Geben Sie bitte Ihre Bestellnummer und die E-Mail-Adresse Ihrer Bestellung an und fahren Sie mit "Widerruf absenden" fort.')?></p>
<div class="bg-middleBase text-altMiddleBase rounded-lg shadow-md p-6">
<?= $this->partial('completeform.phtml', array('buttonName' => 'Widerruf absenden', 'buttonClass' => '', 'form' => $this->form)) ?>
</div>
</div>

View File

@ -0,0 +1,46 @@
<div class="md:w-3/4 w-11/12 m-auto mt-8 mb-10">
<h1 class="text-2xl font-semibold mb-1"><?php echo $this->translate('Widerruf') ?></h1>
<p class="opacity-80 mb-6"><?php echo $this->translate('Auftrag') ?>: <strong><?php echo $this->escape($this->order->alias) ?></strong> &middot; <?php echo $this->translate('Wählen Sie die Positionen aus, die Sie widerrufen möchten.')?></p>
<div class="bg-middleBase text-altMiddleBase rounded-lg shadow-md overflow-hidden">
<table class="w-full text-sm">
<thead>
<tr class="bg-headerFooter text-altHeaderFooter">
<th class="text-left font-medium py-3 px-4 w-12"><?php echo $this->translate('Pos') ?></th>
<th class="text-left font-medium py-3 px-4"><?php echo $this->translate('Produkt') ?></th>
<th class="text-left font-medium py-3 px-4 w-24"><?php echo $this->translate('Auflage') ?></th>
<th class="text-left font-medium py-3 px-4 w-64"><?php echo $this->translate('Widerruf') ?></th>
</tr>
</thead>
<tbody>
<?php foreach ($this->positions as $pos): ?>
<tr class="border-t border-base/40">
<td class="py-3 px-4 align-top font-medium"><?php echo $this->escape($pos->pos) ?></td>
<td class="py-3 px-4 align-top"><?php echo $this->escape($pos->Article->title) ?></td>
<td class="py-3 px-4 align-top"><?php echo $this->escape($pos->count) ?></td>
<td class="py-3 px-4 align-top">
<?php if ($pos->revoked): ?>
<span class="inline-flex items-center gap-1 rounded-md bg-sky-50 text-sky-800 border border-sky-200 px-2 py-1 text-xs">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-4 h-4"><path stroke-linecap="round" stroke-linejoin="round" d="M11.25 11.25l.041-.02a.75.75 0 011.063.852l-.708 2.836a.75.75 0 001.063.853l.041-.021M21 12a9 9 0 11-18 0 9 9 0 0118 0zm-9-3.75h.008v.008H12V8.25z" /></svg>
<?php echo $this->translate('Widerrufen am') ?> <?php echo $this->escape($pos->revoked_at) ?>
</span>
<?php elseif ($pos->Article->revoke_button_enable): ?>
<form method="post" action="/revocationform/order/uuid/<?php echo $this->escape($this->order->uuid) ?>">
<input type="hidden" name="pos" value="<?php echo $this->escape($pos->uuid) ?>" />
<button type="submit" class="border bg-highlight text-altHighlight py-1.5 px-4 rounded-md hover:bg-white hover:text-black transition"><?php echo $this->translate('Widerrufen') ?></button>
</form>
<?php else: ?>
<span class="inline-flex items-center gap-1 text-xs opacity-60">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-4 h-4"><path stroke-linecap="round" stroke-linejoin="round" d="M9.75 9.75l4.5 4.5m0-4.5l-4.5 4.5M21 12a9 9 0 11-18 0 9 9 0 0118 0z" /></svg>
<?php echo $this->translate('Widerruf nicht möglich') ?>
</span>
<?php endif; ?>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
</div>

View File

@ -0,0 +1,28 @@
[revocation]
; general form metainformation
global.class ="form-horizontal"
user.revocation.action = "/revocationform"
user.revocation.method = "post"
user.revocation.legend = "Widerruf"
; Bestellnummer
user.revocation.elements.ordernumber.type = "text"
user.revocation.elements.ordernumber.options.label = "Bestellnummer"
user.revocation.elements.ordernumber.options.required = true
; Email
user.revocation.elements.self_email.type = "text"
user.revocation.elements.self_email.options.label = "Email"
user.revocation.elements.self_email.options.required = true
user.revocation.elements.self_email.options.validators.email.validator = "EmailAddress"
; Captcha
user.revocation.elements.cp.type = "captcha"
user.revocation.elements.cp.options.label = ""
user.revocation.elements.cp.options.captcha.captcha = "Image"
user.revocation.elements.submit.type = "submit"
user.revocation.elements.submit.options.class = "submit btn btn-success btn-large"
user.revocation.elements.submit.options.label = "Widerruf absenden"

View File

@ -0,0 +1,7 @@
<div class="md:w-2/4 w-3/4 m-auto mt-5">
<h1 class="text-lg mb-2"><?php echo $this->translate('Widerruf erhalten') ?></h1>
<p class="mb-2"><?php echo $this->translate('Wir haben Ihren Widerruf erhalten und werden ihn bearbeiten.')?></p>
</div>

View File

@ -0,0 +1,13 @@
<div class="md:w-2/4 w-11/12 m-auto mt-8 mb-10">
<h1 class="text-2xl font-semibold mb-2"><?php echo $this->translate('Widerruf') ?></h1>
<p class="mb-1"><strong><?php echo $this->translate('Sie möchten eine Bestellung widerrufen?')?></strong></p>
<p class="mb-6 opacity-80"><?php echo $this->translate('Geben Sie bitte Ihre Bestellnummer und die E-Mail-Adresse Ihrer Bestellung an und fahren Sie mit "Widerruf absenden" fort.')?></p>
<div class="bg-middleBase text-altMiddleBase rounded-lg shadow-md p-6">
<?= $this->partial('completeform.phtml', array('buttonName' => 'Widerruf absenden', 'buttonClass' => '', 'form' => $this->form)) ?>
</div>
</div>

View File

@ -0,0 +1,46 @@
<div class="md:w-3/4 w-11/12 m-auto mt-8 mb-10">
<h1 class="text-2xl font-semibold mb-1"><?php echo $this->translate('Widerruf') ?></h1>
<p class="opacity-80 mb-6"><?php echo $this->translate('Auftrag') ?>: <strong><?php echo $this->escape($this->order->alias) ?></strong> &middot; <?php echo $this->translate('Wählen Sie die Positionen aus, die Sie widerrufen möchten.')?></p>
<div class="bg-middleBase text-altMiddleBase rounded-lg shadow-md overflow-hidden">
<table class="w-full text-sm">
<thead>
<tr class="bg-headerFooter text-altHeaderFooter">
<th class="text-left font-medium py-3 px-4 w-12"><?php echo $this->translate('Pos') ?></th>
<th class="text-left font-medium py-3 px-4"><?php echo $this->translate('Produkt') ?></th>
<th class="text-left font-medium py-3 px-4 w-24"><?php echo $this->translate('Auflage') ?></th>
<th class="text-left font-medium py-3 px-4 w-64"><?php echo $this->translate('Widerruf') ?></th>
</tr>
</thead>
<tbody>
<?php foreach ($this->positions as $pos): ?>
<tr class="border-t border-base/40">
<td class="py-3 px-4 align-top font-medium"><?php echo $this->escape($pos->pos) ?></td>
<td class="py-3 px-4 align-top"><?php echo $this->escape($pos->Article->title) ?></td>
<td class="py-3 px-4 align-top"><?php echo $this->escape($pos->count) ?></td>
<td class="py-3 px-4 align-top">
<?php if ($pos->revoked): ?>
<span class="inline-flex items-center gap-1 rounded-md bg-sky-50 text-sky-800 border border-sky-200 px-2 py-1 text-xs">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-4 h-4"><path stroke-linecap="round" stroke-linejoin="round" d="M11.25 11.25l.041-.02a.75.75 0 011.063.852l-.708 2.836a.75.75 0 001.063.853l.041-.021M21 12a9 9 0 11-18 0 9 9 0 0118 0zm-9-3.75h.008v.008H12V8.25z" /></svg>
<?php echo $this->translate('Widerrufen am') ?> <?php echo $this->escape($pos->revoked_at) ?>
</span>
<?php elseif ($pos->Article->revoke_button_enable): ?>
<form method="post" action="/revocationform/order/uuid/<?php echo $this->escape($this->order->uuid) ?>">
<input type="hidden" name="pos" value="<?php echo $this->escape($pos->uuid) ?>" />
<button type="submit" class="border bg-highlight text-altHighlight py-1.5 px-4 rounded-md hover:bg-white hover:text-black transition"><?php echo $this->translate('Widerrufen') ?></button>
</form>
<?php else: ?>
<span class="inline-flex items-center gap-1 text-xs opacity-60">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-4 h-4"><path stroke-linecap="round" stroke-linejoin="round" d="M9.75 9.75l4.5 4.5m0-4.5l-4.5 4.5M21 12a9 9 0 11-18 0 9 9 0 0118 0z" /></svg>
<?php echo $this->translate('Widerruf nicht möglich') ?>
</span>
<?php endif; ?>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
</div>

View File

@ -0,0 +1,153 @@
<?php
/**
* Widerrufsformular für Kunden.
*
* Öffentlich erreichbar unter /revocationform. Der Kunde gibt Bestellnummer und
* E-Mail-Adresse ein (mit Captcha). Existiert ein passender Auftrag, wird analog
* zum "Passwort vergessen"-Flow ein Event in die Mongo-Job-Collection geschrieben,
* das die weitere Verarbeitung (z.B. Benachrichtigung) anstößt.
*/
class RevocationformController extends TP_Controller_Action
{
public function indexAction()
{
$config = new Zend_Config_Ini($this->_configPath . '/revocationform.ini', 'revocation');
if ($config->global) {
$form = new EasyBib_Form($config->user->revocation);
$form->addAttribs(array('class' => $config->global->class, 'id' => 'revocationform'));
} else {
$form = new Zend_Form($config->user->revocation);
$form->addAttribs(array('class' => 'niceform', 'id' => 'revocationform'));
}
if (isset($form->cp)) {
$form->cp->setOptions(array(
'captcha' => array(
'captcha' => 'Image',
'font' => APPLICATION_PATH . '/fonts/verdana.ttf',
'imgDir' => PUBLIC_PATH . '/temp/thumb/',
'imgUrl' => '/temp/thumb/',
'wordLen' => '4',
),
));
}
if ($this->getRequest()->isPost() && $form->isValid($this->getRequest()->getPost())) {
$order = Doctrine_Query::create()
->from('Orders o')
->leftJoin('o.Contact c')
->where('o.shop_id = ? AND o.alias = ? AND c.self_email = ? AND o.enable = 1', array(
$this->shop->id,
$this->_getParam('ordernumber'),
$this->_getParam('self_email'),
))
->fetchOne();
if ($order) {
$dbMongo = TP_Mongo::getInstance();
$dbMongo->Job->insertOne(array(
'shop' => $this->shop->id,
'event' => 'order_revocation_request',
'data' => array(
'order' => $order->uuid,
'alias' => $order->alias,
'email' => $this->_getParam('self_email'),
),
'created' => new MongoDB\BSON\UTCDateTime(),
'updated' => new MongoDB\BSON\UTCDateTime(),
));
$this->view->priorityMessenger('Ihr Widerruf wurde erfolgreich übermittelt', 'success');
$this->redirectSpeak('/revocationform/done');
} else {
$this->view->priorityMessenger('Es wurde kein passender Auftrag gefunden', 'error');
$this->redirectSpeak('/revocationform');
}
}
$this->view->form = $form;
if ($this->shop->private && !Zend_Auth::getInstance()->hasIdentity()) {
$this->_helper->layout->setLayout('private');
} else {
$this->_helper->layout->setLayout('default');
}
}
public function doneAction()
{
if ($this->shop->private && !Zend_Auth::getInstance()->hasIdentity()) {
$this->_helper->layout->setLayout('private');
} else {
$this->_helper->layout->setLayout('default');
}
}
/**
* Deep-Link auf einen konkreten Auftrag (per UUID). Hier kann der Kunde
* final pro Position widerrufen. Beim Widerruf werden das Flag und der
* Zeitpunkt auf der Position gesetzt und das Event
* order_revocation_position_request ausgelöst.
*/
public function orderAction()
{
$uuid = $this->_getParam('uuid');
$order = Doctrine_Query::create()
->from('Orders o')
->where('o.shop_id = ? AND o.uuid = ? AND o.enable = 1', array($this->shop->id, $uuid))
->fetchOne();
if (!$order) {
$this->view->priorityMessenger('Es wurde kein passender Auftrag gefunden', 'error');
$this->redirectSpeak('/revocationform');
return;
}
// Eine Position widerrufen
if ($this->getRequest()->isPost() && $this->_getParam('pos')) {
/** @var Orderspos $pos */
$pos = Doctrine_Query::create()
->from('Orderspos p')
->leftJoin('p.Article a')
->where('p.uuid = ? AND p.orders_id = ?', array($this->_getParam('pos'), $order->id))
->fetchOne();
if ($pos && $pos->Article && $pos->Article->revoke_button_enable && !$pos->revoked) {
$pos->revoked = 1;
$pos->revoked_at = date('Y-m-d H:i:s');
$pos->save();
$dbMongo = TP_Mongo::getInstance();
$dbMongo->Job->insertOne(array(
'shop' => $this->shop->id,
'event' => 'order_revocation_position_request',
'data' => array(
'order' => $order->uuid,
'position' => $pos->uuid,
),
'created' => new MongoDB\BSON\UTCDateTime(),
'updated' => new MongoDB\BSON\UTCDateTime(),
));
$this->view->priorityMessenger('Die Position wurde erfolgreich widerrufen', 'success');
} else {
$this->view->priorityMessenger('Für diese Position ist kein Widerruf möglich', 'error');
}
$this->redirectSpeak('/revocationform/order/uuid/' . $order->uuid);
return;
}
$this->view->order = $order;
$this->view->positions = $order->Orderspos;
if ($this->shop->private && !Zend_Auth::getInstance()->hasIdentity()) {
$this->_helper->layout->setLayout('private');
} else {
$this->_helper->layout->setLayout('default');
}
}
}

View File

@ -523,6 +523,7 @@ class TP_Controller_Action extends Zend_Controller_Action
!Zend_Auth::getInstance()->hasIdentity() &&
$this->getRequest()->getParam('controller') != 'user' &&
$this->getRequest()->getParam('controller') != 'cms' &&
$this->getRequest()->getParam('controller') != 'revocationform' &&
$this->getRequest()->getParam('action') != 'login'
) {
$this->_redirect('/user/login');

View File

@ -59,6 +59,13 @@ class TP_Plugin_Auth extends Zend_Controller_Plugin_Abstract
$resource = $controller;
// Öffentlich zugängliche Controller, die (noch) keinen ACL-Eintrag in der
// Datenbank besitzen. Das Widerrufsformular ist für nicht eingeloggte
// Kunden gedacht und wird daher hier freigegeben.
if ($module == 'default' && $controller == 'revocationform') {
return;
}
if (!$this->_acl->has($module.'_'.$resource)) {
$this->_request->setModuleName($this->_nosite['module']);
$this->_request->setControllerName($this->_nosite['controller']);