Skip to content
Snippets Groups Projects
Commit 9656d5f8 authored by Dmytrii Kaiun's avatar Dmytrii Kaiun Committed by Jonathan Sacksick
Browse files

Issue #3112812 by tBKoT, alexpott, jsacksick, lisastreeter, Jing Qian: Add...

Issue #3112812 by tBKoT, alexpott, jsacksick, lisastreeter, Jing Qian: Add logging for payment failures during checkout.
parent 542b8be4
No related branches found
No related tags found
7 merge requests!379Issue #3491248 Validation is breaking the amount number format decimal point,!375Issue #3413020 by czigor: Using a translatable string as a category for field...,!357Issue #2914933: Add service tags to order-related interfaces,!344Resolve #3107602 "Product attributes do not update visually when switching variations",!343Resolve #3107602 "Product attributes do not update visually when switching variations",!342Issue #3476581 by josephr5000: add OrderItemLabelEvent,!151Issue #3112812: Add logging for payment failures during checkout
Pipeline #59958 passed with warnings
......@@ -88,3 +88,7 @@ payment_deleted:
category: commerce_payment
label: 'Payment deleted'
template: '<p>Payment deleted: {{ amount|commerce_price_format }}.{% if method %} [{{ method }}].{% endif %}{% if remote_id %}<br /> Transaction ID: {{ remote_id }}.{% endif %}</p>'
payment_failed:
category: commerce_payment
label: 'Payment failed'
template: '<p>Payment failed via <em>{{ gateway }}</em> for {{ amount|commerce_price_format }}{% if method %} using <em>{{ method }}</em>{% endif %}.<br />Message: {{ error_message }}.{% if remote_id %}<br /> Transaction ID: {{ remote_id }}.{% endif %}</p>'
......@@ -3,8 +3,10 @@
namespace Drupal\commerce_log\EventSubscriber;
use Drupal\commerce_log\LogStorageInterface;
use Drupal\commerce_payment\Event\FailedPaymentEvent;
use Drupal\commerce_payment\Event\PaymentEvent;
use Drupal\commerce_payment\Event\PaymentEvents;
use Drupal\commerce_payment\FailedPaymentDetailsInterface;
use Drupal\commerce_payment\Plugin\Commerce\PaymentGateway\ManualPaymentGatewayInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
......@@ -37,6 +39,7 @@ class PaymentEventSubscriber implements EventSubscriberInterface {
PaymentEvents::PAYMENT_INSERT => ['onPaymentInsert', -100],
PaymentEvents::PAYMENT_UPDATE => ['onPaymentUpdate', -100],
PaymentEvents::PAYMENT_DELETE => ['onPaymentDelete', -100],
PaymentEvents::PAYMENT_FAILURE => ['onPaymentFailure', -100],
];
}
......@@ -145,4 +148,33 @@ class PaymentEventSubscriber implements EventSubscriberInterface {
])->save();
}
/**
* Creates a log when payment failed.
*
* @param \Drupal\commerce_payment\Event\FailedPaymentEvent $event
* The failed payment event.
*/
public function onPaymentFailure(FailedPaymentEvent $event): void {
$payment = $event->getPayment();
$payment_method = $event->getPaymentMethod();
// Allow payment methods to add additional information to the commerce log.
// These value will not be used by the log template but can allow contrib
// and custom code to provide additional reports based on failed payment
// data.
if ($payment_method?->getType() instanceof FailedPaymentDetailsInterface) {
$params = $payment_method->getType()->failedPaymentDetails($payment_method);
}
else {
$params = [];
}
$this->logStorage->generate($event->getOrder(), 'payment_failed', [
'remote_id' => $payment?->getRemoteId(),
'method' => $payment_method?->label(),
'error_message' => $event->getGatewayException()->getMessage(),
'gateway' => $event->getPaymentGateway()->label(),
'amount' => $payment?->getBalance(),
] + $params)->save();
}
}
<?php
namespace Drupal\Tests\commerce_log\Functional;
use Drupal\commerce_event_recorder_test\CommerceEventRecorder;
use Drupal\commerce_log\LogStorageInterface;
use Drupal\commerce_log\LogViewBuilder;
use Drupal\commerce_order\Entity\OrderInterface;
use Drupal\profile\Entity\Profile;
use Drupal\Tests\commerce_order\Functional\OrderBrowserTestBase;
/**
* Test logging for failed payments.
*
* @group commerce
*/
class FailedPaymentTest extends OrderBrowserTestBase {
/**
* A sample order.
*/
protected OrderInterface $order;
/**
* The log storage.
*/
protected LogStorageInterface $logStorage;
/**
* The log view builder.
*/
protected LogViewBuilder $logViewBuilder;
/**
* The default profile's address.
*/
protected array $defaultAddress = [
'country_code' => 'US',
'administrative_area' => 'SC',
'locality' => 'Greenville',
'postal_code' => '53140',
'address_line1' => '9 Drupal Ave',
'given_name' => 'Bryan',
'family_name' => 'Centarro',
];
/**
* Modules to enable.
*
* @var array
*/
protected static $modules = [
'commerce_product',
'commerce_cart',
'commerce_checkout',
'commerce_payment',
'commerce_payment_example',
'commerce_log',
'commerce_event_recorder_test',
];
/**
* {@inheritdoc}
*/
protected function getAdministratorPermissions(): array {
return array_merge([
'administer profile',
], parent::getAdministratorPermissions());
}
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$entity_type_manager = $this->container->get('entity_type.manager');
$this->logStorage = $entity_type_manager->getStorage('commerce_log');
$this->logViewBuilder = $entity_type_manager->getViewBuilder('commerce_log');
/** @var \Drupal\commerce_payment\Entity\PaymentGatewayInterface $payment_gateway */
$payment_gateway = $this->createEntity('commerce_payment_gateway', [
'id' => 'example',
'label' => 'Example',
'plugin' => 'example_onsite',
]);
$payment_gateway->save();
$user = $this->adminUser;
$profile = Profile::create([
'type' => 'customer',
'uid' => $user->id(),
'address' => $this->defaultAddress,
]);
$profile->save();
$profile = $this->reloadEntity($profile);
$payment_method_active = $this->createEntity('commerce_payment_method', [
'uid' => $user->id(),
'type' => 'credit_card',
'payment_gateway' => 'example',
'card_type' => 'visa',
'card_number' => '1111',
'billing_profile' => $profile,
'reusable' => TRUE,
]);
$payment_method_active->save();
/** @var \Drupal\commerce_order\OrderItemStorageInterface $order_item_storage */
$order_item_storage = $entity_type_manager
->getStorage('commerce_order_item');
$order_item = $order_item_storage->createFromPurchasableEntity($this->variation);
$order_item->save();
$order = $this->createEntity('commerce_order', [
'type' => 'default',
'store_id' => $this->store->id(),
'state' => 'draft',
'mail' => $user->getEmail(),
'uid' => $user->id(),
'ip_address' => '127.0.0.1',
'order_number' => '6',
'billing_profile' => $profile,
'order_items' => [$order_item],
]);
$order->save();
$this->order = $this->reloadEntity($order);
}
/**
* Create logs for failed payment.
*/
public function testFailedPayment(): void {
$this->drupalGet("checkout/{$this->order->id()}");
$this->submitForm([], 'Continue to review');
$this->submitForm([], 'Pay and complete purchase');
$this->assertSession()->pageTextContains('We encountered an error processing your payment method. Please verify your details and try again.');
// Ensure the expected payment failure event has been recorded.
$expected = [
[
'order_id' => '1',
'payment_type' => 'Default',
'payment_gateway' => 'Example',
'payment_method' => 'Visa ending in 1111',
],
];
$this->assertSame($expected, \Drupal::state()->get(CommerceEventRecorder::STATE_KEY_PREFIX . 'onPaymentFailure'));
// Check the payment failed log.
$logs = $this->logStorage->loadMultipleByEntity($this->order);
$this->assertEquals(1, count($logs));
/** @var \Drupal\commerce_log\Entity\Log $log */
$log = reset($logs);
// Ensure that payment method types with
// \Drupal\commerce_payment\FailedPaymentDetailsInterface have the
// additional data.
$this->assertSame('visa', $log->getParams()['card_type']);
$this->drupalGet($this->order->toUrl()->toString());
$this->assertSession()->pageTextContains('Payment failed via Example for $999.00 using Visa ending in 1111.Message: The payment was declined.');
}
}
<?php
namespace Drupal\commerce_payment;
use Drupal\commerce_payment\Entity\PaymentMethodInterface;
use Drupal\commerce_payment\Plugin\Commerce\PaymentMethodType\PaymentMethodTypeInterface;
/**
* Defines an interface to report additional details for failed payments.
*
* Failed payments are not saved therefore the best we can do is to add data
* to the commerce log entry for a failed payment.
*/
interface FailedPaymentDetailsInterface extends PaymentMethodTypeInterface {
/**
* Gets additional data to log for failed payments.
*
* @param \Drupal\commerce_payment\Entity\PaymentMethodInterface $payment_method
* The payment method to get failed payment details for.
*
* @return array<string, mixed>
* Parameters to add to the commerce log entry for the failed payment. Keys
* should be strings and the values can be anything that is useful for
* reporting on. Note: the keys should not match the default keys set in
* PaymentEventSubscriber::onPaymentFailure() as the values assigned there
* will override values set in this array.
*
* @see \Drupal\commerce_log\EventSubscriber\PaymentEventSubscriber::onPaymentFailure
*/
public function failedPaymentDetails(PaymentMethodInterface $payment_method): array;
}
......@@ -4,6 +4,7 @@ namespace Drupal\commerce_payment\Plugin\Commerce\PaymentMethodType;
use Drupal\commerce_payment\CreditCard as CreditCardHelper;
use Drupal\commerce_payment\Entity\PaymentMethodInterface;
use Drupal\commerce_payment\FailedPaymentDetailsInterface;
use Drupal\entity\BundleFieldDefinition;
/**
......@@ -14,7 +15,7 @@ use Drupal\entity\BundleFieldDefinition;
* label = @Translation("Credit card"),
* )
*/
class CreditCard extends PaymentMethodTypeBase {
class CreditCard extends PaymentMethodTypeBase implements FailedPaymentDetailsInterface {
/**
* {@inheritdoc}
......@@ -29,6 +30,13 @@ class CreditCard extends PaymentMethodTypeBase {
return $card_number ? $this->t('@card_type ending in @card_number', $args) : $this->t('@card_type', $args);
}
/**
* {@inheritdoc}
*/
public function failedPaymentDetails(PaymentMethodInterface $payment_method): array {
return ['card_type' => $payment_method->card_type->value];
}
/**
* {@inheritdoc}
*/
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment