-
Notifications
You must be signed in to change notification settings - Fork 7
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add refund page with advanced refund process form
The page allows to execute complex refund process extendable by widgets and data providers. With actual refund it: - Stops or changes the subscription linked to the payment. - Stops recurrent payment linked to the payment. - Displays information about payment, subscription, and their owner. Dropdown list of payment statuses (used to change the status) is extracted to widget and is now reused across the different places. Refund selection doesn't change the status immediately, but redirects user to the refund page. remp/crm#2960
- Loading branch information
Showing
20 changed files
with
931 additions
and
55 deletions.
There are no files selected for viewing
23 changes: 23 additions & 0 deletions
23
src/Components/PaymentStatusDropdownMenuWidget/PaymentStatusDropdownMenuWidget.php
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
<?php | ||
|
||
namespace Crm\PaymentsModule\Components\PaymentStatusDropdownMenuWidget; | ||
|
||
use Crm\ApplicationModule\Models\Widget\BaseLazyWidget; | ||
use Nette\Database\Table\ActiveRow; | ||
|
||
class PaymentStatusDropdownMenuWidget extends BaseLazyWidget | ||
{ | ||
private string $templateName = 'payment_status_dropdown_menu_widget.latte'; | ||
|
||
public function identifier(): string | ||
{ | ||
return 'paymentstatusdropdownmenuwidget'; | ||
} | ||
|
||
public function render(ActiveRow $payment): void | ||
{ | ||
$this->template->payment = $payment; | ||
$this->template->setFile(__DIR__ . DIRECTORY_SEPARATOR . $this->templateName); | ||
$this->template->render(); | ||
} | ||
} |
24 changes: 24 additions & 0 deletions
24
src/Components/PaymentStatusDropdownMenuWidget/payment_status_dropdown_menu_widget.latte
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
{var $btn_class = 'btn-default'} | ||
{if $payment->status == \Crm\PaymentsModule\Repositories\PaymentsRepository::STATUS_PAID} | ||
{var $btn_class = 'btn-success'} | ||
{elseif $payment->status == \Crm\PaymentsModule\Repositories\PaymentsRepository::STATUS_FORM} | ||
{var $btn_class = 'btn-info'} | ||
{elseif $payment->status == \Crm\PaymentsModule\Repositories\PaymentsRepository::STATUS_FAIL || $payment->status == \Crm\PaymentsModule\Repositories\PaymentsRepository::STATUS_TIMEOUT} | ||
{var $btn_class = 'btn-danger'} | ||
{/if} | ||
|
||
<div class="dropdown clearfix"> | ||
<button class="btn {$btn_class} btn-sm dropdown-toggle" type="button" id="dropdownMenu1" data-toggle="dropdown" aria-expanded="true"> | ||
{$payment->status|firstUpper} | ||
<span class="caret"></span> | ||
</button> | ||
<ul class="dropdown-menu" role="menu" aria-labelledby="dropdownMenu1"> | ||
<li role="presentation"><a role="menuitem" tabindex="-1" href="{plink :Payments:PaymentsAdmin:changeStatus status => \Crm\PaymentsModule\Repositories\PaymentsRepository::STATUS_FORM, payment => $payment->id}">Form</a></li> | ||
<li role="presentation"><a role="menuitem" tabindex="-1" href="#" data-toggle="modal" data-target="#change-status-modal-{$payment->id}">Paid</a></li> | ||
<li role="presentation"><a role="menuitem" tabindex="-1" href="{plink :Payments:PaymentsAdmin:changeStatus status => \Crm\PaymentsModule\Repositories\PaymentsRepository::STATUS_PREPAID, payment => $payment->id}">Prepaid</a></li> | ||
<li role="presentation"><a role="menuitem" tabindex="-1" href="{plink :Payments:PaymentsAdmin:changeStatus status => \Crm\PaymentsModule\Repositories\PaymentsRepository::STATUS_FAIL, payment => $payment->id}">Fail</a></li> | ||
<li role="presentation"><a role="menuitem" tabindex="-1" href="{plink :Payments:PaymentsAdmin:changeStatus status => \Crm\PaymentsModule\Repositories\PaymentsRepository::STATUS_TIMEOUT, payment => $payment->id}">Timeout</a></li> | ||
<li role="presentation"><a role="menuitem" tabindex="-1" href="{plink :Payments:PaymentsRefundAdmin:default $payment->id}">Refund</a></li> | ||
<li role="presentation"><a role="menuitem" tabindex="-1" href="#">Imported</a></li> | ||
</ul> | ||
</div> |
26 changes: 26 additions & 0 deletions
26
src/Components/RefundPaymentItemsListWidget/RefundPaymentItemsListWidget.php
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
<?php | ||
|
||
namespace Crm\PaymentsModule\Components\RefundPaymentItemsListWidget; | ||
|
||
use Crm\ApplicationModule\Models\Widget\BaseLazyWidget; | ||
|
||
class RefundPaymentItemsListWidget extends BaseLazyWidget | ||
{ | ||
private string $templateName = 'refund_payment_items_list_widget.latte'; | ||
|
||
public function identifier(): string | ||
{ | ||
return 'refundpaymentitemslistwidget'; | ||
} | ||
|
||
public function render(array $params): void | ||
{ | ||
/* @var Nette\Database\Table\ActiveRow $payment */ | ||
$payment = $params['payment']; | ||
|
||
$this->template->paymentItems = $payment->related('payment_items'); | ||
|
||
$this->template->setFile(__DIR__ . DIRECTORY_SEPARATOR . $this->templateName); | ||
$this->template->render(); | ||
} | ||
} |
67 changes: 67 additions & 0 deletions
67
src/Components/RefundPaymentItemsListWidget/refund_payment_items_list_widget.latte
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,67 @@ | ||
<div class="panel panel-default"> | ||
<div class="panel-heading"> | ||
{_payments.admin.payments.show.payment_items} | ||
</div> | ||
<div class="panel-body"> | ||
<table class="table table-responsive table-striped"> | ||
<thead> | ||
<tr> | ||
<th> | ||
{_payments.admin.payments.show.payment_item} | ||
</th> | ||
<th> | ||
{_payments.admin.payments.show.payment_item_type} | ||
</th> | ||
<th> | ||
{_payments.admin.payments.count} | ||
</th> | ||
<th> | ||
{_payments.admin.payments.short_unit_price} | ||
</th> | ||
<th class="text-right"> | ||
{_payments.admin.payments.amount} | ||
</th> | ||
</tr> | ||
</thead> | ||
<tbody> | ||
{var $totalAmount = 0} | ||
{var $totalAmountWithoutVat = 0} | ||
{foreach $paymentItems as $paymentItem} | ||
<tr> | ||
<td class="truncate-text">{$paymentItem->name}</td> | ||
<td class="truncate-text"> | ||
<span class="label label-default">{$paymentItem->type}</span> | ||
</td> | ||
<td>{$paymentItem->count}</td> | ||
<td>{$paymentItem->amount|price}</td> | ||
<td class="text-right">{($paymentItem->amount * $paymentItem->count)|price}</td> | ||
</tr> | ||
|
||
{php $totalAmount += $paymentItem->amount * $paymentItem->count} | ||
{php $totalAmountWithoutVat += $paymentItem->amount_without_vat * $paymentItem->count} | ||
{/foreach} | ||
</tbody> | ||
</table> | ||
|
||
<hr /> | ||
|
||
<div class="row"> | ||
<div class="col-sm-6 col-sm-offset-6 col-xs-12"> | ||
<table class="table table-clear table-left-label"> | ||
<tr class="text-right" style="font-size: 1.2em;"> | ||
<td>{_payments.admin.payments.amount}</td> | ||
<td><b>{$totalAmount|price}</b></td> | ||
</tr> | ||
<tr class="text-right"> | ||
<td>{_payments.admin.payments.amount_without_vat}</td> | ||
<td>{$totalAmountWithoutVat|price}</td> | ||
</tr> | ||
<tr class="text-right"> | ||
<td>{_payments.admin.payments.vat_rate}</td> | ||
<td>{($totalAmount-$totalAmountWithoutVat)|price}</td> | ||
</tr> | ||
</table> | ||
</div> | ||
</div> | ||
</div> | ||
</div> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
16 changes: 16 additions & 0 deletions
16
src/DataProviders/PaymentRefundFormDataProviderInterface.php
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
<?php | ||
|
||
namespace Crm\PaymentsModule\DataProviders; | ||
|
||
use Crm\ApplicationModule\Models\DataProvider\DataProviderInterface; | ||
use Nette\Application\UI\Form; | ||
use Nette\Utils\ArrayHash; | ||
|
||
interface PaymentRefundFormDataProviderInterface extends DataProviderInterface | ||
{ | ||
const PATH = 'admin.dataprovider.payment_refund'; | ||
|
||
public function provide(array $params): Form; | ||
|
||
public function formSucceeded(Form $form, ArrayHash $values): array; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,199 @@ | ||
<?php | ||
|
||
namespace Crm\PaymentsModule\Forms; | ||
|
||
use Crm\ApplicationModule\Models\DataProvider\DataProviderException; | ||
use Crm\ApplicationModule\Models\DataProvider\DataProviderManager; | ||
use Crm\PaymentsModule\DataProviders\PaymentRefundFormDataProviderInterface; | ||
use Crm\PaymentsModule\Repositories\PaymentsRepository; | ||
use Crm\PaymentsModule\Repositories\RecurrentPaymentsRepository; | ||
use Crm\SubscriptionsModule\Events\SubscriptionShortenedEvent; | ||
use Crm\SubscriptionsModule\Models\Subscription\StopSubscriptionHandler; | ||
use Crm\SubscriptionsModule\Repositories\SubscriptionsRepository; | ||
use DateTime; | ||
use League\Event\Emitter; | ||
use Nette\Application\UI\Form; | ||
use Nette\Database\Table\ActiveRow; | ||
use Nette\Localization\Translator; | ||
use Nette\Utils\ArrayHash; | ||
use Tomaj\Form\Renderer\BootstrapRenderer; | ||
|
||
class PaymentRefundFormFactory | ||
{ | ||
const PAYMENT_ID_KEY = 'payment_id'; | ||
const SUBSCRIPTION_ENDS_AT_KEY = 'subscription_ends_at'; | ||
const NEW_PAYMENT_STATUS = 'new_payment_status'; | ||
const STOP_RECURRENT_CHARGE_KEY = 'stop_recurrent_charge'; | ||
|
||
/** @var callable */ | ||
public $onSave; | ||
|
||
public function __construct( | ||
private DataProviderManager $dataProviderManager, | ||
private PaymentsRepository $paymentsRepository, | ||
private StopSubscriptionHandler $stopSubscriptionHandler, | ||
private SubscriptionsRepository $subscriptionsRepository, | ||
private RecurrentPaymentsRepository $recurrentPaymentsRepository, | ||
private Translator $translator, | ||
private Emitter $emitter, | ||
) { | ||
} | ||
|
||
/** | ||
* @throws DataProviderException | ||
*/ | ||
public function create(int $paymentId): Form | ||
{ | ||
$form = new Form(); | ||
$form->addProtection(); | ||
$form->setTranslator($this->translator); | ||
$form->setRenderer(new BootstrapRenderer()); | ||
|
||
$payment = $this->paymentsRepository->find($paymentId); | ||
|
||
$form->addHidden(self::PAYMENT_ID_KEY)->setRequired()->setDefaultValue($payment->id); | ||
|
||
// For case, if you want to change final payment status via dataProvider | ||
$form->addHidden(self::NEW_PAYMENT_STATUS) | ||
->setRequired() | ||
->setDefaultValue(PaymentsRepository::STATUS_REFUND); | ||
|
||
$now = new DateTime(); | ||
if ($payment->subscription && $payment->subscription->end_time > $now) { | ||
if ($payment->status != PaymentsRepository::STATUS_REFUND) { | ||
$form->addText( | ||
self::SUBSCRIPTION_ENDS_AT_KEY, | ||
'payments.admin.payment_refund.form.cancel_subscription_date' | ||
) | ||
->setRequired('payments.admin.payment_refund.form.required.subscription_ends_at') | ||
->setHtmlAttribute('class', 'flatpickr') | ||
->setHtmlAttribute('flatpickr_datetime', "1") | ||
->setHtmlAttribute('flatpickr_datetime_seconds', "1") | ||
->setHtmlAttribute('flatpickr_mindate', $now->format('d.m.Y H:i:s')) | ||
->setDefaultValue($now->format('Y-m-d H:i:s')); | ||
} | ||
|
||
$form->addHidden('subscription_default_ends_at') | ||
->setRequired() | ||
->setHtmlId('subscription_default_ends_at') | ||
->setDefaultValue($payment->subscription->end_time); | ||
|
||
$form->addHidden('subscription_starts_at') | ||
->setRequired() | ||
->setHtmlId('subscription_starts_at') | ||
->setDefaultValue($payment->subscription->start_time); | ||
} | ||
|
||
if ($this->recurrentPaymentCanBeStoppedInRefund($payment) && $payment->status != PaymentsRepository::STATUS_REFUND) { | ||
$form->addCheckbox( | ||
self::STOP_RECURRENT_CHARGE_KEY, | ||
'payments.admin.payment_refund.form.stop_recurrent_charge' | ||
) | ||
->setDisabled() | ||
->setDefaultValue(true); | ||
} | ||
|
||
if ($payment->status != PaymentsRepository::STATUS_REFUND) { | ||
$form->addSubmit('submit', 'payments.admin.payment_refund.confirm_refund') | ||
->getControlPrototype() | ||
->setName('button') | ||
->setAttribute('class', 'btn btn-danger'); | ||
} | ||
|
||
/** @var PaymentRefundFormDataProviderInterface[] $providers */ | ||
$providers = $this->dataProviderManager->getProviders( | ||
PaymentRefundFormDataProviderInterface::PATH, | ||
PaymentRefundFormDataProviderInterface::class | ||
); | ||
foreach ($providers as $provider) { | ||
$form = $provider->provide(['form' => $form]); | ||
} | ||
|
||
$form->onSuccess[] = [$this, 'formSucceeded']; | ||
|
||
return $form; | ||
} | ||
|
||
public function formSucceeded(Form $form, ArrayHash $values): void | ||
{ | ||
$payment = $this->paymentsRepository->find($values[self::PAYMENT_ID_KEY]); | ||
$newEndTime = $values[self::SUBSCRIPTION_ENDS_AT_KEY] ?? null; | ||
$newPaymentStatus = $values[self::NEW_PAYMENT_STATUS] ?? $payment->status; | ||
$stopRecurrentPayment = $values[self::STOP_RECURRENT_CHARGE_KEY] ?? true; | ||
|
||
if ($newEndTime && $payment->subscription_id) { | ||
$this->updateSubscriptionOnSuccess($payment->subscription, new DateTime($newEndTime)); | ||
} | ||
|
||
if ($stopRecurrentPayment && $this->recurrentPaymentCanBeStoppedInRefund($payment)) { | ||
$this->stopRecurrentChargeInRefundedPayment($payment); | ||
} | ||
|
||
$this->paymentsRepository->update($payment, ['status' => $newPaymentStatus]); | ||
|
||
/** @var PaymentRefundFormDataProviderInterface[] $providers */ | ||
$providers = $this->dataProviderManager->getProviders( | ||
PaymentRefundFormDataProviderInterface::PATH, | ||
PaymentRefundFormDataProviderInterface::class | ||
); | ||
foreach ($providers as $provider) { | ||
[$form, $values] = $provider->formSucceeded($form, $values); | ||
} | ||
|
||
if (isset($this->onSave)) { | ||
($this->onSave)($values[self::PAYMENT_ID_KEY]); | ||
} | ||
} | ||
|
||
protected function updateSubscriptionOnSuccess(ActiveRow $subscription, DateTime $newEndTime): void | ||
{ | ||
if ($newEndTime <= new DateTime()) { | ||
$this->stopSubscriptionHandler->stopSubscription($subscription, true); | ||
} else { | ||
$this->shortenSubscription($subscription, $newEndTime); | ||
} | ||
} | ||
|
||
protected function recurrentPaymentCanBeStoppedInRefund(ActiveRow $payment): bool | ||
{ | ||
$recurrentPayment = $this->recurrentPaymentsRepository->recurrent($payment); | ||
|
||
if (!$recurrentPayment) { | ||
return false; | ||
} | ||
|
||
$lastRecurrentPayment = $this->recurrentPaymentsRepository->getLastWithState( | ||
$recurrentPayment, | ||
RecurrentPaymentsRepository::STATE_ACTIVE, | ||
); | ||
|
||
return $lastRecurrentPayment | ||
&& $this->recurrentPaymentsRepository->canBeStopped($lastRecurrentPayment); | ||
} | ||
|
||
protected function stopRecurrentChargeInRefundedPayment(ActiveRow $payment): void | ||
{ | ||
$recurrentPayment = $this->recurrentPaymentsRepository->recurrent($payment); | ||
$lastRecurrentPayment = $this->recurrentPaymentsRepository->getLastWithState( | ||
$recurrentPayment, | ||
RecurrentPaymentsRepository::STATE_ACTIVE, | ||
); | ||
|
||
$this->recurrentPaymentsRepository->stoppedByAdmin($lastRecurrentPayment); | ||
} | ||
|
||
protected function shortenSubscription(ActiveRow $subscription, DateTime $newEndTime): void | ||
{ | ||
$note = '[Admin shortened] From ' . $subscription->end_time->format('Y-m-d H:i:s') . ' to ' . $newEndTime->format('Y-m-d H:i:s'); | ||
if (!empty($subscription->note)) { | ||
$note = $subscription->note . "\n" . $note; | ||
} | ||
|
||
$this->subscriptionsRepository->update($subscription, [ | ||
'end_time' => $newEndTime, | ||
'note' => $note, | ||
]); | ||
|
||
$this->emitter->emit(new SubscriptionShortenedEvent($subscription, $newEndTime)); | ||
} | ||
} |
Oops, something went wrong.