Skip to content

Commit 5d03652

Browse files
vasikebojanz
authored andcommitted
Issue #2832493 by mglaman, vasike, arosboro, bojanz: Checkout shows payment methods which cannot be reused
1 parent 317652d commit 5d03652

File tree

6 files changed

+285
-6
lines changed

6 files changed

+285
-6
lines changed

modules/payment/src/Form/PaymentAddForm.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -149,7 +149,8 @@ protected function buildPaymentMethodForm(array $form, FormStateInterface $form_
149149

150150
/** @var \Drupal\commerce_payment\PaymentMethodStorageInterface $payment_method_storage */
151151
$payment_method_storage = $this->entityTypeManager->getStorage('commerce_payment_method');
152-
$payment_methods = $payment_method_storage->loadReusable($this->order->getCustomer(), $selected_payment_gateway);
152+
$billing_countries = $this->order->getStore()->getBillingCountries();
153+
$payment_methods = $payment_method_storage->loadReusable($this->order->getCustomer(), $selected_payment_gateway, $billing_countries);
153154

154155
if (!empty($payment_methods)) {
155156
$selected_payment_method = reset($payment_methods);

modules/payment/src/PaymentMethodStorage.php

Lines changed: 79 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,20 +3,78 @@
33
namespace Drupal\commerce_payment;
44

55
use Drupal\commerce\CommerceContentEntityStorage;
6+
use Drupal\commerce\TimeInterface;
67
use Drupal\commerce_payment\Entity\PaymentGatewayInterface;
78
use Drupal\commerce_payment\Plugin\Commerce\PaymentGateway\SupportsStoredPaymentMethodsInterface;
9+
use Drupal\Core\Cache\CacheBackendInterface;
10+
use Drupal\Core\Database\Connection;
11+
use Drupal\Core\Entity\EntityManagerInterface;
812
use Drupal\Core\Entity\EntityStorageException;
13+
use Drupal\Core\Entity\EntityTypeInterface;
14+
use Drupal\Core\Language\LanguageManagerInterface;
915
use Drupal\user\UserInterface;
16+
use Symfony\Component\DependencyInjection\ContainerInterface;
17+
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
1018

1119
/**
1220
* Defines the payment method storage.
1321
*/
1422
class PaymentMethodStorage extends CommerceContentEntityStorage implements PaymentMethodStorageInterface {
1523

24+
/**
25+
* The time.
26+
*
27+
* @var \Drupal\commerce\TimeInterface
28+
*/
29+
protected $time;
30+
31+
/**
32+
* Constructs a new PaymentMethodStorage object.
33+
*
34+
* @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
35+
* The entity type definition.
36+
* @param \Drupal\Core\Database\Connection $database
37+
* The database connection to be used.
38+
* @param \Drupal\Core\Entity\EntityManagerInterface $entity_manager
39+
* The entity manager.
40+
* @param \Drupal\Core\Cache\CacheBackendInterface $cache
41+
* The cache backend to be used.
42+
* @param \Drupal\Core\Language\LanguageManagerInterface $language_manager
43+
* The language manager.
44+
* @param \Symfony\Component\EventDispatcher\EventDispatcherInterface $event_dispatcher
45+
* The event dispatcher.
46+
* @param \Drupal\commerce\TimeInterface $time
47+
* The time.
48+
*/
49+
public function __construct(EntityTypeInterface $entity_type, Connection $database, EntityManagerInterface $entity_manager, CacheBackendInterface $cache, LanguageManagerInterface $language_manager, EventDispatcherInterface $event_dispatcher, TimeInterface $time) {
50+
parent::__construct($entity_type, $database, $entity_manager, $cache, $language_manager, $event_dispatcher);
51+
52+
$this->time = $time;
53+
}
54+
55+
/**
56+
* {@inheritdoc}
57+
*/
58+
public static function createInstance(ContainerInterface $container, EntityTypeInterface $entity_type) {
59+
return new static(
60+
$entity_type,
61+
$container->get('database'),
62+
$container->get('entity.manager'),
63+
$container->get('cache.entity'),
64+
$container->get('language_manager'),
65+
$container->get('event_dispatcher'),
66+
$container->get('commerce.time')
67+
);
68+
}
69+
1670
/**
1771
* {@inheritdoc}
1872
*/
19-
public function loadReusable(UserInterface $account, PaymentGatewayInterface $payment_gateway) {
73+
public function loadReusable(UserInterface $account, PaymentGatewayInterface $payment_gateway, array $billing_countries = []) {
74+
// Anonymous users cannot have reusable payment methods.
75+
if ($account->isAnonymous()) {
76+
return [];
77+
}
2078
if (!($payment_gateway->getPlugin() instanceof SupportsStoredPaymentMethodsInterface)) {
2179
return [];
2280
}
@@ -25,13 +83,32 @@ public function loadReusable(UserInterface $account, PaymentGatewayInterface $pa
2583
->condition('uid', $account->id())
2684
->condition('payment_gateway', $payment_gateway->id())
2785
->condition('reusable', TRUE)
86+
->condition('expires', $this->time->getRequestTime(), '>')
2887
->sort('created', 'DESC');
2988
$result = $query->execute();
3089
if (empty($result)) {
3190
return [];
3291
}
3392

34-
return $this->loadMultiple($result);
93+
/** @var \Drupal\commerce_payment\Entity\PaymentMethodInterface[] $payment_methods */
94+
$payment_methods = $this->loadMultiple($result);
95+
if (!empty($billing_countries)) {
96+
// Filter out payment methods that don't match the billing countries.
97+
// Payment methods without a billing profile should also be filtered out.
98+
// @todo Use a query condition once #2822359 is fixed.
99+
foreach ($payment_methods as $id => $payment_method) {
100+
$country_code = 'ZZ';
101+
if ($billing_profile = $payment_method->getBillingProfile()) {
102+
$country_code = $billing_profile->address->first()->getCountryCode();
103+
}
104+
105+
if (!in_array($country_code, $billing_countries)) {
106+
unset($payment_methods[$id]);
107+
}
108+
}
109+
}
110+
111+
return $payment_methods;
35112
}
36113

37114
/**

modules/payment/src/PaymentMethodStorageInterface.php

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,14 @@ interface PaymentMethodStorageInterface extends ContentEntityStorageInterface {
1818
* The user account.
1919
* @param \Drupal\commerce_payment\Entity\PaymentGatewayInterface $payment_gateway
2020
* The payment gateway.
21+
* @param array $billing_countries
22+
* (Optional) A list of billing countries to filter by.
23+
* For example, if ['US', 'FR'] is given, only payment methods
24+
* with billing profiles from those countries will be returned.
2125
*
2226
* @return \Drupal\commerce_payment\Entity\PaymentMethodInterface[]
2327
* The reusable payment methods.
2428
*/
25-
public function loadReusable(UserInterface $account, PaymentGatewayInterface $payment_gateway);
29+
public function loadReusable(UserInterface $account, PaymentGatewayInterface $payment_gateway, array $billing_countries = []);
2630

2731
}

modules/payment/src/Plugin/Commerce/CheckoutPane/PaymentInformation.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -171,7 +171,8 @@ protected function attachPaymentMethodForm(EntityPaymentGatewayInterface $paymen
171171
$order_payment_method = $this->order->payment_method->entity;
172172
$customer = $this->order->getCustomer();
173173
if ($customer) {
174-
$payment_methods = $payment_method_storage->loadReusable($customer, $payment_gateway);
174+
$billing_countries = $this->order->getStore()->getBillingCountries();
175+
$payment_methods = $payment_method_storage->loadReusable($customer, $payment_gateway, $billing_countries);
175176
foreach ($payment_methods as $payment_method) {
176177
$options[$payment_method->id()] = $payment_method->label();
177178
}

modules/payment/tests/src/Functional/PaymentAdminTest.php

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,16 +68,30 @@ protected function getAdministratorPermissions() {
6868
protected function setUp() {
6969
parent::setUp();
7070

71+
$profile = $this->createEntity('profile', [
72+
'type' => 'customer',
73+
'address' => [
74+
'country_code' => 'US',
75+
'postal_code' => '53177',
76+
'locality' => 'Milwaukee',
77+
'address_line1' => 'Pabst Blue Ribbon Dr',
78+
'administrative_area' => 'WI',
79+
'given_name' => 'Frederick',
80+
'family_name' => 'Pabst',
81+
],
82+
'uid' => $this->adminUser->id(),
83+
]);
84+
7185
$this->paymentGateway = $this->createEntity('commerce_payment_gateway', [
7286
'id' => 'example',
7387
'label' => 'Example',
7488
'plugin' => 'example_onsite',
7589
]);
76-
7790
$this->paymentMethod = $this->createEntity('commerce_payment_method', [
7891
'uid' => $this->loggedInUser->id(),
7992
'type' => 'credit_card',
8093
'payment_gateway' => 'example',
94+
'billing_profile' => $profile,
8195
]);
8296

8397
$details = [
Lines changed: 182 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,182 @@
1+
<?php
2+
3+
namespace Drupal\Tests\commerce_payment\Kernel;
4+
5+
use Drupal\commerce_order\Entity\OrderItemType;
6+
use Drupal\commerce_payment\Entity\PaymentGateway;
7+
use Drupal\commerce_payment\Entity\PaymentMethod;
8+
use Drupal\profile\Entity\Profile;
9+
use Drupal\user\Entity\User;
10+
use Drupal\Tests\commerce\Kernel\CommerceKernelTestBase;
11+
12+
/**
13+
* Tests the payment method storage.
14+
*
15+
* @group commerce
16+
*/
17+
class PaymentMethodStorageTest extends CommerceKernelTestBase {
18+
19+
/**
20+
* A sample user.
21+
*
22+
* @var \Drupal\user\UserInterface
23+
*/
24+
protected $user;
25+
26+
/**
27+
* A payment gateway.
28+
*
29+
* @var \Drupal\commerce_payment\Entity\PaymentGatewayInterface
30+
*/
31+
protected $paymentGateway;
32+
33+
/**
34+
* The payment method storage.
35+
*
36+
* @var \Drupal\commerce_payment\PaymentMethodStorageInterface
37+
*/
38+
protected $storage;
39+
40+
/**
41+
* Modules to enable.
42+
*
43+
* @var array
44+
*/
45+
public static $modules = [
46+
'address',
47+
'entity_reference_revisions',
48+
'profile',
49+
'state_machine',
50+
'commerce_product',
51+
'commerce_order',
52+
'commerce_payment',
53+
'commerce_payment_example',
54+
];
55+
56+
/**
57+
* {@inheritdoc}
58+
*/
59+
protected function setUp() {
60+
parent::setUp();
61+
62+
$this->installEntitySchema('profile');
63+
$this->installEntitySchema('commerce_order');
64+
$this->installEntitySchema('commerce_order_item');
65+
$this->installEntitySchema('commerce_payment');
66+
$this->installEntitySchema('commerce_payment_method');
67+
$this->installConfig('commerce_order');
68+
$this->installConfig('commerce_payment');
69+
70+
// An order item type that doesn't need a purchasable entity, for simplicity.
71+
OrderItemType::create([
72+
'id' => 'test',
73+
'label' => 'Test',
74+
'orderType' => 'default',
75+
])->save();
76+
77+
$payment_gateway = PaymentGateway::create([
78+
'id' => 'example',
79+
'label' => 'Example',
80+
'plugin' => 'example_onsite',
81+
]);
82+
$payment_gateway->save();
83+
$this->paymentGateway = $this->reloadEntity($payment_gateway);
84+
85+
$user = $this->createUser();
86+
$this->user = $this->reloadEntity($user);
87+
88+
$this->storage = $this->container->get('entity_type.manager')->getStorage('commerce_payment_method');
89+
}
90+
91+
/**
92+
* Tests loading reusable payment methods.
93+
*/
94+
public function testLoadReusable() {
95+
/** @var \Drupal\commerce_payment\Entity\PaymentMethodInterface $payment_method_expired */
96+
$payment_method_expired = PaymentMethod::create([
97+
'type' => 'credit_card',
98+
'payment_gateway' => 'example',
99+
// Sat, 16 Jan 2016.
100+
'expires' => '1452902400',
101+
'uid' => $this->user->id(),
102+
]);
103+
$payment_method_expired->save();
104+
/** @var \Drupal\commerce_payment\Entity\PaymentMethodInterface $payment_method */
105+
$payment_method_active = PaymentMethod::create([
106+
'type' => 'credit_card',
107+
'payment_gateway' => 'example',
108+
// Thu, 16 Jan 2020.
109+
'expires' => '1579132800',
110+
'uid' => $this->user->id(),
111+
]);
112+
$payment_method_active->save();
113+
// Confirm that only the active payment method was loaded.
114+
$reusable_payment_methods = $this->storage->loadReusable($this->user, $this->paymentGateway);
115+
$this->assertEquals([$payment_method_active->id()], array_keys($reusable_payment_methods));
116+
117+
// Confirm that anonymous users cannot have reusable payment methods.
118+
$payment_method_active->setOwnerId(0);
119+
$payment_method_active->save();
120+
$this->assertEmpty($this->storage->loadReusable(User::getAnonymousUser(), $this->paymentGateway));
121+
$this->assertEmpty($this->storage->loadReusable($this->user, $this->paymentGateway));
122+
}
123+
124+
/**
125+
* Tests filtering reusable payment methods by billing country.
126+
*/
127+
public function testBillingCountryFiltering() {
128+
/** @var \Drupal\profile\Entity\Profile $profile_fr */
129+
$profile_fr = Profile::create([
130+
'type' => 'customer',
131+
'address' => [
132+
'organization' => '',
133+
'country_code' => 'FR',
134+
'postal_code' => '75002',
135+
'locality' => 'Paris',
136+
'address_line1' => 'A french street',
137+
'given_name' => 'John',
138+
'family_name' => 'LeSmith',
139+
],
140+
'uid' => $this->user->id(),
141+
]);
142+
$profile_fr->save();
143+
/** @var \Drupal\commerce_payment\Entity\PaymentMethodInterface $payment_method_fr */
144+
$payment_method_fr = PaymentMethod::create([
145+
'type' => 'credit_card',
146+
'payment_gateway' => 'example',
147+
'expires' => '1579132800',
148+
'uid' => $this->user->id(),
149+
'billing_profile' => $profile_fr,
150+
]);
151+
$payment_method_fr->save();
152+
153+
$this->assertEmpty($this->storage->loadReusable($this->user, $this->paymentGateway, ['US']));
154+
155+
$profile_us = Profile::create([
156+
'type' => 'customer',
157+
'address' => [
158+
'country_code' => 'US',
159+
'postal_code' => '53177',
160+
'locality' => 'Milwaukee',
161+
'address_line1' => 'Pabst Blue Ribbon Dr',
162+
'administrative_area' => 'WI',
163+
'given_name' => 'Frederick',
164+
'family_name' => 'Pabst',
165+
],
166+
'uid' => $this->user->id(),
167+
]);
168+
$profile_us->save();
169+
/** @var \Drupal\commerce_payment\Entity\PaymentMethodInterface $payment_method_fr */
170+
$payment_method_us = PaymentMethod::create([
171+
'type' => 'credit_card',
172+
'payment_gateway' => 'example',
173+
'expires' => '1579132800',
174+
'uid' => $this->user->id(),
175+
'billing_profile' => $profile_us,
176+
]);
177+
$payment_method_us->save();
178+
179+
$this->assertTrue($this->storage->loadReusable($this->user, $this->paymentGateway, ['US']));
180+
}
181+
182+
}

0 commit comments

Comments
 (0)