diff --git a/src/Plugin.php b/src/Plugin.php index 163c160711..f80bde43aa 100644 --- a/src/Plugin.php +++ b/src/Plugin.php @@ -121,7 +121,7 @@ public static function t($message, $params = [], $language = null) * @inheritDoc */ - public $schemaVersion = '3.1.13'; + public $schemaVersion = '3.1.22'; /** * @inheritdoc @@ -362,6 +362,9 @@ private function _registerPermissions() 'commerce-capturePayment' => [ 'label' => self::t('Capture payment') ], + 'commerce-voidPayment' => [ + 'label' => self::t('Void payment') + ], 'commerce-refundPayment' => [ 'label' => self::t('Refund payment') ], diff --git a/src/base/Gateway.php b/src/base/Gateway.php index a880b2288e..a2e3d35419 100644 --- a/src/base/Gateway.php +++ b/src/base/Gateway.php @@ -10,10 +10,15 @@ use Craft; use craft\base\SavableComponent; use craft\commerce\elements\Order; +use craft\commerce\errors\NotImplementedException; use craft\commerce\models\payments\BasePaymentForm; +use craft\commerce\models\PaymentSource; +use craft\commerce\models\Transaction; use craft\commerce\Plugin; use craft\helpers\StringHelper; use craft\helpers\UrlHelper; +use craft\web\Response as WebResponse; +use Throwable; /** * Class Gateway @@ -99,6 +104,14 @@ public function rules() ]; } + /** + * returns the void window in seconds + */ + public function getVoidWindow() + { + + } + /** * Returns the html to use when paying with a stored payment source. * @@ -119,18 +132,183 @@ public function availableForUseWithOrder(Order $order): bool } /** - * Returns payment Form HTML - * - * @param array $params - * @return string|null + * @inheritDoc */ - abstract public function getPaymentFormHtml(array $params); + public function getPaymentFormHtml(array $params) + { + return ''; + } /** - * Returns the transaction hash based on a webhook request - * - * @return string|null - * @since 3.1.9 + * @inheritDoc + */ + public function authorize(Transaction $transaction, BasePaymentForm $form): RequestResponseInterface + { + throw new NotImplementedException('Not implemented by the payment gateway'); + } + + /** + * @inheritDoc + */ + public function capture(Transaction $transaction, string $reference): RequestResponseInterface + { + throw new NotImplementedException('Not implemented by the payment gateway'); + } + + /** + * @inheritDoc + */ + public function completeAuthorize(Transaction $transaction): RequestResponseInterface + { + throw new NotImplementedException('Not implemented by the payment gateway'); + } + + /** + * @inheritDoc + */ + public function completePurchase(Transaction $transaction): RequestResponseInterface + { + throw new NotImplementedException('Not implemented by the payment gateway'); + } + + /** + * @inheritDoc + */ + public function createPaymentSource(BasePaymentForm $sourceData, int $userId): PaymentSource + { + throw new NotImplementedException('Not implemented by the payment gateway'); + } + + /** + * @inheritDoc + */ + public function deletePaymentSource($token): bool + { + throw new NotImplementedException('Not implemented by the payment gateway'); + } + + /** + * @inheritDoc + */ + public function getPaymentFormModel(): BasePaymentForm + { + throw new NotImplementedException('Not implemented by the payment gateway'); + } + + /** + * @inheritDoc + */ + public function purchase(Transaction $transaction, BasePaymentForm $form): RequestResponseInterface + { + throw new NotImplementedException('Not implemented by the payment gateway'); + } + + /** + * @inheritDoc + */ + public function refund(Transaction $transaction): RequestResponseInterface + { + throw new NotImplementedException('Not implemented by the payment gateway'); + } + + /** + * @inheritDoc + */ + public function void(Transaction $transaction): RequestResponseInterface + { + throw new NotImplementedException('Not implemented by the payment gateway'); + } + + /** + * @inheritDoc + */ + public function processWebHook(): WebResponse + { + throw new NotImplementedException('Not implemented by the payment gateway'); + } + + /** + * @inheritDoc + */ + public function supportsAuthorize(): bool + { + return false; + } + + /** + * @inheritDoc + */ + public function supportsCapture(): bool + { + return false; + } + + /** + * @inheritDoc + */ + public function supportsCompleteAuthorize(): bool + { + return false; + } + + /** + * @inheritDoc + */ + public function supportsCompletePurchase(): bool + { + return false; + } + + /** + * @inheritDoc + */ + public function supportsPaymentSources(): bool + { + return false; + } + + /** + * @inheritDoc + */ + public function supportsPurchase(): bool + { + return false; + } + + /** + * @inheritDoc + */ + public function supportsRefund(): bool + { + return false; + } + + /** + * @inheritDoc + */ + public function supportsPartialRefund(): bool + { + return false; + } + + /** + * @inheritDoc + */ + public function supportsVoid(): bool + { + return false; + } + + /** + * @inheritDoc + */ + public function supportsWebhooks(): bool + { + return false; + } + + /** + * @inheritDoc */ public function getTransactionHashFromWebhook() { diff --git a/src/base/GatewayInterface.php b/src/base/GatewayInterface.php index d6845af5f4..64135c9240 100644 --- a/src/base/GatewayInterface.php +++ b/src/base/GatewayInterface.php @@ -101,6 +101,14 @@ public function purchase(Transaction $transaction, BasePaymentForm $form): Reque */ public function refund(Transaction $transaction): RequestResponseInterface; + /** + * Makes an void request. + * + * @param Transaction $transaction The voidable transaction + * @return RequestResponseInterface + */ + public function void(Transaction $transaction): RequestResponseInterface; + /** * Processes a webhook and return a response * @@ -165,6 +173,13 @@ public function supportsRefund(): bool; */ public function supportsPartialRefund(): bool; + /** + * Returns true if gateway supports void requests. + * + * @return bool + */ + public function supportsVoid(): bool; + /** * Returns true if gateway supports webhooks. * diff --git a/src/controllers/OrdersController.php b/src/controllers/OrdersController.php index d07abeafa2..4162119850 100644 --- a/src/controllers/OrdersController.php +++ b/src/controllers/OrdersController.php @@ -773,6 +773,44 @@ public function actionTransactionCapture(): Response return $this->redirectToPostedUrl(); } + /** + * Voids Transaction + * + * @return Response + * @throws TransactionException + * @throws MissingComponentException + * @throws BadRequestHttpException + */ + public function actionTransactionVoid(): Response + { + $this->requirePermission('commerce-VoidPayment'); + $this->requirePostRequest(); + $id = Craft::$app->getRequest()->getRequiredBodyParam('id'); + $transaction = Plugin::getInstance()->getTransactions()->getTransactionById($id); + + if ($transaction->canVoid()) { + // void transaction and display result + $child = Plugin::getInstance()->getPayments()->voidTransaction($transaction); + + $message = $child->message ? ' (' . $child->message . ')' : ''; + + if ($child->status == TransactionRecord::STATUS_SUCCESS) { + $child->order->updateOrderPaidInformation(); + Craft::$app->getSession()->setNotice(Plugin::t('Transaction voided successfully: {message}', [ + 'message' => $message + ])); + } else { + Craft::$app->getSession()->setError(Plugin::t('Couldn’t void transaction: {message}', [ + 'message' => $message + ])); + } + } else { + Craft::$app->getSession()->setError(Plugin::t('Couldn’t void transaction.', ['id' => $id])); + } + + return $this->redirectToPostedUrl(); + } + /** * Refunds transaction. * @@ -1293,19 +1331,26 @@ private function _getTransactionsWIthLevelsTableArray($transactions, $level = 0) $user = Craft::$app->getUser()->getIdentity(); foreach ($transactions as $transaction) { if (!ArrayHelper::firstWhere($return, 'id', $transaction->id)) { - $refundCapture = ''; - if ($user->can('commerce-capturePayment') && $transaction->canCapture()) { - $refundCapture = Craft::$app->getView()->renderTemplate( - 'commerce/orders/includes/_capture', - [ - 'currentUser' => $user, - 'transaction' => $transaction, - ] - ); - } else if ($user->can('commerce-refundPayment') && $transaction->canRefund()) { - $refundCapture = Craft::$app->getView()->renderTemplate( - 'commerce/orders/includes/_refund', + $refundCaptureVoid = ''; + $actions = []; + + if ($user->can('commerce-capturePayment') && $canCapture = $transaction->canCapture()) { + $actions[] = 'capture'; + } + + if ($user->can('commerce-voidPayment') && $canVoid = $transaction->canVoid()) { + $actions[] = 'void'; + } + + if (!$canCapture && !$canVoid && $user->can('commerce-refundPayment') && $transaction->canRefund()) { + $actions[] = 'refund'; + } + + if (!empty($actions)) { + $refundCaptureVoid = Craft::$app->getView()->renderTemplate( + 'commerce/orders/includes/_refundCaptureVoid', [ + 'actions' => $actions, 'currentUser' => $user, 'transaction' => $transaction, ] @@ -1345,7 +1390,7 @@ private function _getTransactionsWIthLevelsTableArray($transactions, $level = 0) ['label' => Html::encode(Plugin::t('Converted Price')), 'type' => 'text', 'value' => Plugin::getInstance()->getPaymentCurrencies()->convert($transaction->paymentAmount, $transaction->paymentCurrency) . ' (' . $transaction->currency . ')' . ' (1 ' . $transaction->currency . ' = ' . number_format($transaction->paymentRate) . ' ' . $transaction->paymentCurrency . ')'], ['label' => Html::encode(Plugin::t('Gateway Response')), 'type' => 'response', 'value' => $transactionResponse], ], - 'actions' => $refundCapture, + 'actions' => $refundCaptureVoid, ]; if (!empty($transaction->childTransactions)) { diff --git a/src/elements/Order.php b/src/elements/Order.php index c578bd7e63..d6e72a8343 100644 --- a/src/elements/Order.php +++ b/src/elements/Order.php @@ -2244,10 +2244,15 @@ public function getTotalPaid(): float return $transaction->status == TransactionRecord::STATUS_SUCCESS && $transaction->type == TransactionRecord::TYPE_REFUND; }); + $voidedTransactions = ArrayHelper::where($this->_transactions, static function(Transaction $transaction) { + return $transaction->status == TransactionRecord::STATUS_SUCCESS && $transaction->type == TransactionRecord::TYPE_VOID; + }); + $paid = array_sum(ArrayHelper::getColumn($paidTransactions, 'amount', false)); $refunded = array_sum(ArrayHelper::getColumn($refundedTransactions, 'amount', false)); + $voided = array_sum(ArrayHelper::getColumn($voidedTransactions, 'amount', false)); - return $paid - $refunded; + return $paid - ($refunded + $voided); } /** diff --git a/src/gateways/Dummy.php b/src/gateways/Dummy.php index c804897531..19055b0622 100644 --- a/src/gateways/Dummy.php +++ b/src/gateways/Dummy.php @@ -89,7 +89,7 @@ public function authorize(Transaction $transaction, BasePaymentForm $form): Requ */ public function capture(Transaction $transaction, string $reference): RequestResponseInterface { - return new DummyRequestResponse(); + return new DummyRequestResponse(null, true); } /** @@ -153,7 +153,7 @@ public function processWebHook(): WebResponse */ public function refund(Transaction $transaction): RequestResponseInterface { - return new DummyRequestResponse(); + return new DummyRequestResponse(null, true); } /** @@ -164,6 +164,22 @@ public function supportsAuthorize(): bool return true; } + /** + * @inheritdoc + */ + public function supportsVoid(): bool + { + return true; + } + + /** + * @inheritdoc + */ + public function void(Transaction $transaction): RequestResponseInterface + { + return new DummyRequestResponse(null, true); + } + /** * @inheritdoc */ diff --git a/src/migrations/m200528_201030_add_void.php b/src/migrations/m200528_201030_add_void.php new file mode 100644 index 0000000000..59665318c6 --- /dev/null +++ b/src/migrations/m200528_201030_add_void.php @@ -0,0 +1,43 @@ +db->getIsPgsql()) { + // Manually construct the SQL for Postgres + $check = '[[type]] in ('; + foreach ($values as $i => $value) { + if ($i != 0) { + $check .= ','; + } + $check .= $this->db->quoteValue($value); + } + $check .= ')'; + $this->execute("alter table {{%commerce_transactions}} drop constraint {{%commerce_transactions_type_check}}, add check ({$check})"); + } else { + $this->alterColumn('{{%commerce_transactions}}', 'type', $this->enum('type', $values)); + } + } + + /** + * @inheritdoc + */ + public function safeDown() + { + echo "m200528_201030_add_void cannot be reverted.\n"; + return false; + } +} diff --git a/src/models/Transaction.php b/src/models/Transaction.php index c7c19cea16..e2c8239971 100644 --- a/src/models/Transaction.php +++ b/src/models/Transaction.php @@ -228,6 +228,14 @@ public function canCapture(): bool return Plugin::getInstance()->getTransactions()->canCaptureTransaction($this); } + /** + * @return bool + */ + public function canVoid(): bool + { + return Plugin::getInstance()->getTransactions()->canVoidTransaction($this); + } + /** * @return bool */ diff --git a/src/models/responses/Dummy.php b/src/models/responses/Dummy.php index 16f40c2025..27b89fa84c 100644 --- a/src/models/responses/Dummy.php +++ b/src/models/responses/Dummy.php @@ -22,8 +22,19 @@ class Dummy implements RequestResponseInterface private $_success = true; - public function __construct(CreditCardPaymentForm $form = null) + /** + * Dummy constructor. + * + * @param CreditCardPaymentForm|null $form + * @param bool|null $successOverride + */ + public function __construct(CreditCardPaymentForm $form = null, $successOverride = null) { + if ($successOverride !== null) { + $this->_success = $successOverride; + return; + } + if ($form === null) { $this->_success = false; return; @@ -40,6 +51,10 @@ public function __construct(CreditCardPaymentForm $form = null) if (!$isValid) { $this->_success = false; } + + if ($successOverride !== null) { + $this->_success = $successOverride; + } } /** diff --git a/src/records/Transaction.php b/src/records/Transaction.php index ef3441cb49..3a43e959d8 100644 --- a/src/records/Transaction.php +++ b/src/records/Transaction.php @@ -39,6 +39,7 @@ class Transaction extends ActiveRecord { const TYPE_AUTHORIZE = 'authorize'; const TYPE_CAPTURE = 'capture'; + const TYPE_VOID = 'void'; const TYPE_PURCHASE = 'purchase'; const TYPE_REFUND = 'refund'; const STATUS_PENDING = 'pending'; diff --git a/src/services/Payments.php b/src/services/Payments.php index 25705a9a53..e6d8d4dbee 100644 --- a/src/services/Payments.php +++ b/src/services/Payments.php @@ -111,6 +111,54 @@ class Payments extends Component */ const EVENT_AFTER_CAPTURE_TRANSACTION = 'afterCaptureTransaction'; + /** + * @event TransactionEvent The event that is triggered before a payment transaction is voided. + * + * ```php + * use craft\commerce\events\TransactionEvent; + * use craft\commerce\services\Payments; + * use craft\commerce\models\Transaction; + * use yii\base\Event; + * + * Event::on( + * Payments::class, + * Payments::EVENT_BEFORE_VOID_TRANSACTION, + * function(TransactionEvent $event) { + * // @var Transaction $transaction + * $transaction = $event->transaction; + * + * // Check that order isn't already processing + * // ... + * } + * ); + * ``` + */ + const EVENT_BEFORE_VOID_TRANSACTION = 'beforeVoidTransaction'; + + /** + * @event TransactionEvent The event that is triggered after a payment transaction is voided. + * + * ```php + * use craft\commerce\events\TransactionEvent; + * use craft\commerce\services\Payments; + * use craft\commerce\models\Transaction; + * use yii\base\Event; + * + * Event::on( + * Payments::class, + * Payments::EVENT_AFTER_VOID_TRANSACTION, + * function(TransactionEvent $event) { + * // @var Transaction $transaction + * $transaction = $event->transaction; + * + * // Notify customer their transaction has been voided + * // ... + * } + * ); + * ``` + */ + const EVENT_AFTER_VOID_TRANSACTION = 'afterVoidTransaction'; + /** * @event TransactionEvent The event that is triggered before a transaction is refunded. * @@ -348,6 +396,34 @@ public function captureTransaction(Transaction $transaction): Transaction return $transaction; } + /** + * Void a transaction. + * + * @param Transaction $transaction the transaction to void. + * @return Transaction + * @throws TransactionException if something went wrong when saving the transaction + */ + public function voidTransaction(Transaction $transaction): Transaction + { + // Raise 'beforeVoidTransaction' event + if ($this->hasEventHandlers(self::EVENT_BEFORE_VOID_TRANSACTION)) { + $this->trigger(self::EVENT_BEFORE_VOID_TRANSACTION, new TransactionEvent([ + 'transaction' => $transaction + ])); + } + + $transaction = $this->_void($transaction); + + // Raise 'afterVoidTransaction' event + if ($this->hasEventHandlers(self::EVENT_AFTER_VOID_TRANSACTION)) { + $this->trigger(self::EVENT_AFTER_VOID_TRANSACTION, new TransactionEvent([ + 'transaction' => $transaction + ])); + } + + return $transaction; + } + /** * Refund a transaction. * @@ -573,7 +649,7 @@ private function _handleRedirect(RequestResponseInterface $response, &$redirect) } /** - * Process a capture or refund exception. + * Process a capture transaction. * * @param Transaction $parent * @return Transaction @@ -600,7 +676,34 @@ private function _capture(Transaction $parent): Transaction } /** - * Process a capture or refund exception. + * Process a void transaction. + * + * @param Transaction $parent + * @return Transaction + * @throws TransactionException if unable to save transaction + */ + private function _void(Transaction $parent): Transaction + { + $child = Plugin::getInstance()->getTransactions()->createTransaction(null, $parent, TransactionRecord::TYPE_VOID); + + $gateway = $parent->getGateway(); + + try { + $response = $gateway->void($child, (string)$parent->reference); + $this->_updateTransaction($child, $response); + } catch (Exception $e) { + $child->status = TransactionRecord::STATUS_FAILED; + $child->message = $e->getMessage(); + $this->_saveTransaction($child); + + Craft::error($e->getMessage()); + } + + return $child; + } + + /** + * Process a refund transaction. * * @param Transaction $parent * @param float|null $amount diff --git a/src/services/Transactions.php b/src/services/Transactions.php index 1fd4ef610a..0e0b6316d7 100644 --- a/src/services/Transactions.php +++ b/src/services/Transactions.php @@ -18,7 +18,10 @@ use craft\commerce\Plugin; use craft\commerce\records\Transaction as TransactionRecord; use craft\db\Query; +use craft\helpers\DateTimeHelper; use yii\base\Component; +use DateTime; +use DateInterval; /** * Transaction service. @@ -43,7 +46,7 @@ class Transactions extends Component * function(TransactionEvent $event) { * // @var Transaction $transaction * $transaction = $event->transaction; - * + * * // Run custom logic for failed transactions * // ... * } @@ -60,14 +63,14 @@ class Transactions extends Component * use craft\commerce\services\Transactions; * use craft\commerce\models\Transaction; * use yii\base\Event; - * + * * Event::on( * Transactions::class, * Transactions::EVENT_AFTER_CREATE_TRANSACTION, * function(TransactionEvent $event) { * // @var Transaction $transaction * $transaction = $event->transaction; - * + * * // Run custom logic depending on the transaction type * // ... * } @@ -78,7 +81,7 @@ class Transactions extends Component /** - * Returns true if a specific transaction can be refunded. + * Returns true if a specific transaction can be captured. * * @param Transaction $transaction the transaction * @return bool @@ -100,10 +103,10 @@ public function canCaptureTransaction(Transaction $transaction): bool return false; } - // And only if we don't have a successful refund transaction for this order already + // And only if we don't have a successful capture or void transaction for this transaction already return !$this->_createTransactionQuery() ->where([ - 'type' => TransactionRecord::TYPE_CAPTURE, + 'type' => [TransactionRecord::TYPE_CAPTURE, TransactionRecord::TYPE_VOID], 'status' => TransactionRecord::STATUS_SUCCESS, 'orderId' => $transaction->orderId, 'parentId' => $transaction->id @@ -111,6 +114,82 @@ public function canCaptureTransaction(Transaction $transaction): bool ->exists(); } + /** + * Returns true if a specific transaction can be voided. + * + * @param Transaction $transaction the transaction + * @return bool + */ + public function canVoidTransaction(Transaction $transaction): bool + { + // Can only void successful authorize, capture, or purchase transactions + $types = [TransactionRecord::TYPE_AUTHORIZE, TransactionRecord::TYPE_CAPTURE, TransactionRecord::TYPE_PURCHASE]; + if (!in_array($transaction->type, $types) || $transaction->status !== TransactionRecord::STATUS_SUCCESS) { + return false; + } + + $gateway = $transaction->getGateway(); + + if (!$gateway) { + return false; + } + + if (!$gateway->supportsVoid()) { + return false; + } + + $voidedAlready = $this->_createTransactionQuery() + ->where([ + 'type' => TransactionRecord::TYPE_VOID, + 'status' => TransactionRecord::STATUS_SUCCESS, + 'parentId' => $transaction->id + ]) + ->exists(); + + if ($voidedAlready) { + return false; + } + + // If authorized, then check it has not been captured already. The capture will have a refund or void itself + if ($transaction->type === TransactionRecord::TYPE_AUTHORIZE) { + + $alreadyCaptured = $this->_createTransactionQuery() + ->where([ + 'type' => TransactionRecord::TYPE_CAPTURE, + 'status' => TransactionRecord::STATUS_SUCCESS, + 'parentId' => $transaction->id + ]) + ->exists(); + + if ($alreadyCaptured) { + return false; + } + } + + // If authorized, then check it has not been captured already. The capture will have a refund or void itself + if (in_array($transaction->type, [TransactionRecord::TYPE_PURCHASE, TransactionRecord::TYPE_CAPTURE])) { + + $alreadyRefundedOrVoided = $this->_createTransactionQuery() + ->where([ + 'type' => [TransactionRecord::TYPE_REFUND, TransactionRecord::TYPE_VOID], + 'status' => TransactionRecord::STATUS_SUCCESS, + 'parentId' => $transaction->id + ]) + ->exists(); + + if ($alreadyRefundedOrVoided) { + return false; + } + } + + // Only if current time is within gateway void window + $now = new DateTime(); + $interval = DateTimeHelper::secondsToInterval('86400'); + $edge = $transaction->dateCreated->add($interval); + + return ($now < $edge); + } + /** * Returns true if a specific transaction can be refunded. * @@ -138,6 +217,30 @@ public function canRefundTransaction(Transaction $transaction): bool return false; } + if ($gateway->supportsVoid()) { + + // And only if we don't have a successful refund + $alreadyVoided = $this->_createTransactionQuery() + ->where([ + 'type' => TransactionRecord::TYPE_VOID, + 'status' => TransactionRecord::STATUS_SUCCESS, + 'parentId' => $transaction->id + ]) + ->exists(); + + if ($alreadyVoided) { + return false; + } + + // Only if current time is outside gateway void window + $now = new DateTime(); + $interval = DateTimeHelper::secondsToInterval('86400'); + $edge = $transaction->dateCreated->add($interval); + if ($now >= $edge) { + return false; + } + } + return ($this->refundableAmountForTransaction($transaction) > 0); } diff --git a/src/templates/orders/includes/_capture.html b/src/templates/orders/includes/_capture.html deleted file mode 100644 index 1b718a450b..0000000000 --- a/src/templates/orders/includes/_capture.html +++ /dev/null @@ -1,11 +0,0 @@ -{% if currentUser.can('commerce-capturePayment') and transaction.canCapture() %} -
- {{ csrfInput() }} - {{ 'Capture'|t('commerce') }} -
-{% endif %} \ No newline at end of file diff --git a/src/templates/orders/includes/_refund.html b/src/templates/orders/includes/_refund.html deleted file mode 100644 index b136adb507..0000000000 --- a/src/templates/orders/includes/_refund.html +++ /dev/null @@ -1,25 +0,0 @@ -{% if currentUser.can('commerce-refundPayment') and transaction.canRefund() %} -
- {{ csrfInput() }} - {% import "_includes/forms" as forms %} - {{ forms.text({ - id: 'amount', - size: 10, - name: 'amount', - placeholder: transaction.paymentCurrency~' '~transaction.refundableAmount - }) }} - {{ forms.text({ - id: 'note', - size: 20, - name: 'note', - value: transaction.note, - placeholder: 'Refund Note' - }) }} - - {{ 'Refund'|t('commerce') }} -
-{% endif %} diff --git a/src/templates/orders/includes/_refundCaptureVoid.html b/src/templates/orders/includes/_refundCaptureVoid.html new file mode 100644 index 0000000000..66d71acfd4 --- /dev/null +++ b/src/templates/orders/includes/_refundCaptureVoid.html @@ -0,0 +1,43 @@ +{% if actions %} +
+ {{ csrfInput() }} + {% if 'capture' in actions and currentUser.can('commerce-capturePayment') and transaction.canCapture() %} + {{ 'Capture'|t('commerce') }} + {% endif %} + {% if 'void' in actions and currentUser.can('commerce-voidPayment') and transaction.canVoid() %} + {{ 'Void'|t('commerce') }} + {% endif %} + {% if 'refund' in actions and currentUser.can('commerce-refundPayment') and transaction.canRefund() %} + {% import "_includes/forms" as forms %} + {{ forms.text({ + id: 'amount', + size: 10, + name: 'amount', + placeholder: transaction.paymentCurrency~' '~transaction.refundableAmount + }) }} + {{ forms.text({ + id: 'note', + size: 20, + name: 'note', + value: transaction.note, + placeholder: 'Refund Note' + }) }} + {{ 'Refund'|t('commerce') }} + {% endif %} +
+{% endif %}