Skip to content

Commit b1df4fe

Browse files
authored
allow restricting products to personal pickup only (#4671)
2 parents 2cfa334 + dfc7a46 commit b1df4fe

7 files changed

Lines changed: 155 additions & 5 deletions

File tree

src/Model/Resolver/Products/DataMapper/ProductArrayFieldMapper.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,11 @@ public function isSellingDenied(array $data): bool
110110
return $data['selling_denied'] === true;
111111
}
112112

113+
public function isPersonalPickupOnly(array $data): bool
114+
{
115+
return $data['personal_pickup_only'] === true;
116+
}
117+
113118
public function isCurrentlyOutOfStock(array $data): bool
114119
{
115120
if ($this->isAllowedNegativeStock($data)) {

src/Model/Resolver/Transport/TransportsQuery.php

Lines changed: 113 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,17 +4,31 @@
44

55
namespace Shopsys\FrontendApiBundle\Model\Resolver\Transport;
66

7+
use ArrayObject;
8+
use Shopsys\FrameworkBundle\Component\Cache\InMemoryCache;
9+
use Shopsys\FrameworkBundle\Model\Cart\Cart;
710
use Shopsys\FrameworkBundle\Model\Customer\User\CurrentCustomerUser;
11+
use Shopsys\FrameworkBundle\Model\Transport\Transport;
812
use Shopsys\FrameworkBundle\Model\Transport\TransportFacade;
13+
use Shopsys\FrameworkBundle\Model\Transport\TransportUnavailabilityReasonInCartEnum;
14+
use Shopsys\FrameworkBundle\Model\Transport\TransportVisibilityCalculation;
15+
use Shopsys\FrontendApiBundle\Component\GqlContext\GqlContextHelper;
916
use Shopsys\FrontendApiBundle\Model\Cart\CartApiFacade;
1017
use Shopsys\FrontendApiBundle\Model\Resolver\AbstractQuery;
1118

1219
class TransportsQuery extends AbstractQuery
1320
{
21+
protected const string CART_CACHE_NAMESPACE = 'transportsQueryCart';
22+
protected const string EXCLUDING_PRODUCTS_CACHE_NAMESPACE = 'transportsQueryExcludingProductsByTransportId';
23+
protected const string CURRENT_CUSTOMER_CART_CACHE_KEY = 'currentCustomerCart';
24+
1425
public function __construct(
1526
protected readonly TransportFacade $transportFacade,
1627
protected readonly CartApiFacade $cartApiFacade,
1728
protected readonly CurrentCustomerUser $currentCustomerUser,
29+
protected readonly GqlContextHelper $gqlContextHelper,
30+
protected readonly InMemoryCache $inMemoryCache,
31+
protected readonly TransportVisibilityCalculation $transportVisibilityCalculation,
1832
) {
1933
}
2034

@@ -23,18 +37,112 @@ public function __construct(
2337
*/
2438
public function transportsQuery(?string $cartUuid = null): array
2539
{
26-
$customerUser = $this->currentCustomerUser->findCurrentCustomerUser();
40+
$cart = $this->findCart($cartUuid);
2741

28-
if ($customerUser === null && $cartUuid === null) {
42+
if ($cart === null) {
2943
return $this->transportFacade->getVisibleOnCurrentDomainWithEagerLoadedDomainsAndTranslations();
3044
}
3145

32-
$cart = $this->cartApiFacade->findCart($customerUser, $cartUuid);
46+
$transports = $this->transportFacade->getVisibleOnCurrentDomainWithEagerLoadedDomainsAndTranslations($cart);
47+
48+
return $this->sortAvailableTransportsFirst($transports, $cart, $cartUuid);
49+
}
50+
51+
/**
52+
* @return array<int, array{reason: string, products: \Shopsys\FrameworkBundle\Model\Product\Product[]}>
53+
*/
54+
public function transportProductsBlockingSelectionInCartQuery(
55+
Transport $transport,
56+
?string $cartUuid = null,
57+
?ArrayObject $context = null,
58+
): array {
59+
$resolvedCartUuid = $cartUuid ?? $this->gqlContextHelper->getCartUuid($context);
60+
$cart = $this->findCart($resolvedCartUuid);
3361

3462
if ($cart === null) {
35-
return $this->transportFacade->getVisibleOnCurrentDomainWithEagerLoadedDomainsAndTranslations();
63+
return [];
64+
}
65+
66+
return $this->getProductsBlockingSelectionGroupedByReason($transport, $cart, $resolvedCartUuid);
67+
}
68+
69+
/**
70+
* @return array<int, array{reason: string, products: \Shopsys\FrameworkBundle\Model\Product\Product[]}>
71+
*/
72+
protected function getProductsBlockingSelectionGroupedByReason(
73+
Transport $transport,
74+
Cart $cart,
75+
?string $cartUuid,
76+
): array {
77+
$productsGroupedByReason = [];
78+
79+
$excludingProducts = $this->getExcludingProductsByTransportId($cart, $cartUuid)[$transport->getId()] ?? [];
80+
81+
if ($excludingProducts !== []) {
82+
$productsGroupedByReason[] = [
83+
'reason' => TransportUnavailabilityReasonInCartEnum::EXCLUDED_FOR_PRODUCT,
84+
'products' => $excludingProducts,
85+
];
86+
}
87+
88+
$personalPickupOnlyProducts = array_values($cart->getPersonalPickupOnlyProducts());
89+
90+
if (!$transport->isPersonalPickup() && $personalPickupOnlyProducts !== []) {
91+
$productsGroupedByReason[] = [
92+
'reason' => TransportUnavailabilityReasonInCartEnum::PERSONAL_PICKUP_REQUIRED,
93+
'products' => $personalPickupOnlyProducts,
94+
];
95+
}
96+
97+
return $productsGroupedByReason;
98+
}
99+
100+
protected function findCart(?string $cartUuid): ?Cart
101+
{
102+
return $this->inMemoryCache->getOrSaveValue(
103+
self::CART_CACHE_NAMESPACE,
104+
function () use ($cartUuid): ?Cart {
105+
$customerUser = $this->currentCustomerUser->findCurrentCustomerUser();
106+
107+
if ($customerUser === null && $cartUuid === null) {
108+
return null;
109+
}
110+
111+
return $this->cartApiFacade->findCart($customerUser, $cartUuid);
112+
},
113+
$cartUuid ?? self::CURRENT_CUSTOMER_CART_CACHE_KEY,
114+
);
115+
}
116+
117+
/**
118+
* @return array<int, \Shopsys\FrameworkBundle\Model\Product\Product[]>
119+
*/
120+
protected function getExcludingProductsByTransportId(Cart $cart, ?string $cartUuid): array
121+
{
122+
return $this->inMemoryCache->getOrSaveValue(
123+
self::EXCLUDING_PRODUCTS_CACHE_NAMESPACE,
124+
fn (): array => $this->transportVisibilityCalculation->getExcludingProductsByTransportIdForCart($cart),
125+
$cartUuid ?? self::CURRENT_CUSTOMER_CART_CACHE_KEY,
126+
);
127+
}
128+
129+
/**
130+
* @param \Shopsys\FrameworkBundle\Model\Transport\Transport[] $transports
131+
* @return \Shopsys\FrameworkBundle\Model\Transport\Transport[]
132+
*/
133+
protected function sortAvailableTransportsFirst(array $transports, Cart $cart, ?string $cartUuid): array
134+
{
135+
$availableTransports = [];
136+
$unavailableTransports = [];
137+
138+
foreach ($transports as $transport) {
139+
if ($this->getProductsBlockingSelectionGroupedByReason($transport, $cart, $cartUuid) === []) {
140+
$availableTransports[] = $transport;
141+
} else {
142+
$unavailableTransports[] = $transport;
143+
}
36144
}
37145

38-
return $this->transportFacade->getVisibleOnCurrentDomainWithEagerLoadedDomainsAndTranslations($cart);
146+
return [...$availableTransports, ...$unavailableTransports];
39147
}
40148
}

src/Model/Transport/TransportValidationFacade.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,10 @@ public function checkTransportAvailabilityForProductsInCart(Transport $transport
6363
if ($this->transportVisibilityCalculation->filterTransportsByProductsInCart([$transport], $cart) === []) {
6464
throw new TransportUnavailableForProductsInCartException();
6565
}
66+
67+
if ($cart->isPersonalPickupRequired() && !$transport->isPersonalPickup()) {
68+
throw new TransportUnavailableForProductsInCartException();
69+
}
6670
}
6771

6872
public function checkTransportPriceAndWeightLimit(Transport $transport, Cart $cart): void
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
TransportUnavailabilityReasonInCartEnumDecorator:
2+
type: enum
3+
decorator: true
4+
config:
5+
description: "Reason why a transport cannot be selected for the given cart"
6+
values:
7+
personal_pickup_required:
8+
value: '@=constant("Shopsys\\FrameworkBundle\\Model\\Transport\\TransportUnavailabilityReasonInCartEnum::PERSONAL_PICKUP_REQUIRED")'
9+
excluded_for_product:
10+
value: '@=constant("Shopsys\\FrameworkBundle\\Model\\Transport\\TransportUnavailabilityReasonInCartEnum::EXCLUDED_FOR_PRODUCT")'

src/Resources/config/graphql-types/ModelType/Product/ProductDecorator.types.yaml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,9 @@ ProductDecorator:
6565
description: "Brand of product"
6666
isSellingDenied:
6767
type: "Boolean!"
68+
isPersonalPickupOnly:
69+
type: "Boolean!"
70+
description: "Whether a cart containing this product is limited to personal pickup transports"
6871
isCurrentlyOutOfStock:
6972
type: "Boolean!"
7073
description: "Whether the product is currently out of stock (but may be back in stock later), but selling is not permanently denied"
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
ProductsByTransportUnavailabilityReasonDecorator:
2+
type: object
3+
decorator: true
4+
config:
5+
description: "Cart products grouped by the reason why they cannot be delivered using the transport"
6+
fields:
7+
reason:
8+
type: "TransportUnavailabilityReasonInCartEnum!"
9+
description: "Reason why the products cannot be delivered using the transport"
10+
products:
11+
type: "[Product!]!"
12+
description: "Cart products that cannot be delivered using the transport for this reason"

src/Resources/config/graphql-types/ModelType/Transport/TransportDecorator.types.yaml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,5 +41,13 @@ TransportDecorator:
4141
transportTypeCode:
4242
type: "TransportTypeEnum!"
4343
description: "Code of transport type"
44+
productsBlockingSelectionInCart:
45+
type: "[ProductsByTransportUnavailabilityReason!]!"
46+
description: "Cart products that cannot be delivered using this transport, grouped by the reason (empty when the transport can be selected)"
47+
resolve: '@=query("transportProductsBlockingSelectionInCartQuery", value, args["cartUuid"], context)'
48+
args:
49+
cartUuid:
50+
type: "Uuid"
51+
defaultValue: null
4452
vatPercent:
4553
type: "String!"

0 commit comments

Comments
 (0)