From 15f66fbc4193cf91b646b53d79983bf0b657c1c8 Mon Sep 17 00:00:00 2001 From: Leonardo Scappatura <95079472+Leonard013@users.noreply.github.com> Date: Sun, 19 Jul 2026 18:47:06 +0200 Subject: [PATCH] =?UTF-8?q?fix:=20update=20GetProductsHandler=20to=20use?= =?UTF-8?q?=20filterProducts=20to=20avoid=20type=20mismatch=20=F0=9F=A4=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Handlers/Product/GetProductsHandler.php | 11 +- .../Domain/Product/ProductFilterService.php | 144 +++++++++--------- .../Product/GetProductsHandlerTest.php | 110 +++++++++++++ 3 files changed, 189 insertions(+), 76 deletions(-) create mode 100644 backend/tests/Unit/Services/Application/Handlers/Product/GetProductsHandlerTest.php diff --git a/backend/app/Services/Application/Handlers/Product/GetProductsHandler.php b/backend/app/Services/Application/Handlers/Product/GetProductsHandler.php index 09a01e0576..5d54441540 100644 --- a/backend/app/Services/Application/Handlers/Product/GetProductsHandler.php +++ b/backend/app/Services/Application/Handlers/Product/GetProductsHandler.php @@ -13,10 +13,8 @@ class GetProductsHandler { public function __construct( private readonly ProductRepositoryInterface $productRepository, - private readonly ProductFilterService $productFilterService, - ) - { - } + private readonly ProductFilterService $productFilterService, + ) {} public function handle(int $eventId, QueryParamsDTO $queryParamsDTO): LengthAwarePaginator { @@ -25,10 +23,9 @@ public function handle(int $eventId, QueryParamsDTO $queryParamsDTO): LengthAwar ->loadRelation(TaxAndFeesDomainObject::class) ->findByEventId($eventId, $queryParamsDTO); - $filteredProducts = $this->productFilterService->filter( - productsCategories: $productPaginator->getCollection(), + $filteredProducts = $this->productFilterService->filterProducts( + products: $productPaginator->getCollection(), hideSoldOutProducts: false, - hideHiddenCategories: false, ); $productPaginator->setCollection($filteredProducts); diff --git a/backend/app/Services/Domain/Product/ProductFilterService.php b/backend/app/Services/Domain/Product/ProductFilterService.php index 36af1acc19..4fc3a6cc55 100644 --- a/backend/app/Services/Domain/Product/ProductFilterService.php +++ b/backend/app/Services/Domain/Product/ProductFilterService.php @@ -23,71 +23,83 @@ class ProductFilterService { private ?AccountConfigurationDomainObject $accountConfiguration = null; + private ?EventSettingDomainObject $eventSettings = null; + private ?string $eventCurrency = null; public function __construct( - private readonly TaxAndFeeCalculationService $taxCalculationService, - private readonly ProductPriceService $productPriceService, + private readonly TaxAndFeeCalculationService $taxCalculationService, + private readonly ProductPriceService $productPriceService, private readonly AvailableProductQuantitiesFetchService $fetchAvailableProductQuantitiesService, - private readonly OrderPlatformFeePassThroughService $platformFeeService, - private readonly AccountRepositoryInterface $accountRepository, - private readonly EventRepositoryInterface $eventRepository, - ) - { - } + private readonly OrderPlatformFeePassThroughService $platformFeeService, + private readonly AccountRepositoryInterface $accountRepository, + private readonly EventRepositoryInterface $eventRepository, + ) {} /** - * @param Collection $productsCategories - * @param PromoCodeDomainObject|null $promoCode - * @param bool $hideSoldOutProducts - * @param bool $hideHiddenCategories + * @param Collection $productsCategories * @return Collection */ public function filter( - Collection $productsCategories, + Collection $productsCategories, ?PromoCodeDomainObject $promoCode = null, - bool $hideSoldOutProducts = true, - bool $hideHiddenCategories = true, - ): Collection - { + bool $hideSoldOutProducts = true, + bool $hideHiddenCategories = true, + ): Collection { if ($productsCategories->isEmpty()) { return $productsCategories; } $products = $productsCategories - ->flatMap(fn(ProductCategoryDomainObject $category) => $category->getProducts()); + ->flatMap(fn (ProductCategoryDomainObject $category) => $category->getProducts()); if ($products->isEmpty()) { return $hideHiddenCategories - ? $productsCategories->reject(fn(ProductCategoryDomainObject $category) => $category->getIsHidden()) + ? $productsCategories->reject(fn (ProductCategoryDomainObject $category) => $category->getIsHidden()) : $productsCategories; } - $eventId = $products->first()->getEventId(); - $this->loadAccountConfiguration($eventId); - - $productQuantities = $this - ->fetchAvailableProductQuantitiesService - ->getAvailableProductQuantities($eventId); - - $filteredProducts = $products - ->map(fn(ProductDomainObject $product) => $this->processProduct($product, $productQuantities->productQuantities, $promoCode)) - ->reject(fn(ProductDomainObject $product) => $this->filterProduct($product, $promoCode, $hideSoldOutProducts)) - ->each(fn(ProductDomainObject $product) => $this->processProductPrices($product, $hideSoldOutProducts)); + $filteredProducts = $this->filterProducts($products, $promoCode, $hideSoldOutProducts); $filteredCategories = $hideHiddenCategories - ? $productsCategories->reject(fn(ProductCategoryDomainObject $category) => $category->getIsHidden()) + ? $productsCategories->reject(fn (ProductCategoryDomainObject $category) => $category->getIsHidden()) : $productsCategories; return $filteredCategories - ->each(fn(ProductCategoryDomainObject $category) => $category->setProducts( + ->each(fn (ProductCategoryDomainObject $category) => $category->setProducts( $filteredProducts->where( - static fn(ProductDomainObject $product) => $product->getProductCategoryId() === $category->getId() + static fn (ProductDomainObject $product) => $product->getProductCategoryId() === $category->getId() ) )); } + /** + * @param Collection $products + * @return Collection + */ + public function filterProducts( + Collection $products, + ?PromoCodeDomainObject $promoCode = null, + bool $hideSoldOutProducts = true, + ): Collection { + if ($products->isEmpty()) { + return $products; + } + + $eventId = $products->first()->getEventId(); + $this->loadAccountConfiguration($eventId); + + $productQuantities = $this + ->fetchAvailableProductQuantitiesService + ->getAvailableProductQuantities($eventId); + + return $products + ->map(fn (ProductDomainObject $product) => $this->processProduct($product, $productQuantities->productQuantities, $promoCode)) + ->reject(fn (ProductDomainObject $product) => $this->filterProduct($product, $promoCode, $hideSoldOutProducts)) + ->each(fn (ProductDomainObject $product) => $this->processProductPrices($product, $hideSoldOutProducts)); + } + private function loadAccountConfiguration(int $eventId): void { $account = $this->accountRepository @@ -109,10 +121,10 @@ private function loadAccountConfiguration(int $eventId): void private function isHiddenByPromoCode(ProductDomainObject $product, ?PromoCodeDomainObject $promoCode): bool { - return $product->getIsHiddenWithoutPromoCode() && !( - $promoCode - && $promoCode->appliesToProduct($product) - ); + return $product->getIsHiddenWithoutPromoCode() && ! ( + $promoCode + && $promoCode->appliesToProduct($product) + ); } private function shouldProductBeDiscounted(?PromoCodeDomainObject $promoCode, ProductDomainObject $product): bool @@ -127,17 +139,13 @@ private function shouldProductBeDiscounted(?PromoCodeDomainObject $promoCode, Pr } /** - * @param PromoCodeDomainObject|null $promoCode - * @param ProductDomainObject $product - * @param Collection $productQuantities - * @return ProductDomainObject + * @param Collection $productQuantities */ private function processProduct( - ProductDomainObject $product, - Collection $productQuantities, + ProductDomainObject $product, + Collection $productQuantities, ?PromoCodeDomainObject $promoCode = null, - ): ProductDomainObject - { + ): ProductDomainObject { if ($this->shouldProductBeDiscounted($promoCode, $product)) { $product->getProductPrices()?->each(function (ProductPriceDomainObject $price) use ($product, $promoCode) { $price->setPriceBeforeDiscount($price->getPrice()); @@ -156,7 +164,7 @@ private function processProduct( $productQuantities->each(function (AvailableProductQuantitiesDTO $quantity) use ($product) { if ($quantity->capacities !== null && $quantity->capacities->isNotEmpty() && $quantity->product_id === $product->getId()) { $product->setQuantityAvailable( - $quantity->capacities->min(fn(CapacityAssignmentDomainObject $capacity) => $capacity->getAvailableCapacity()) + $quantity->capacities->min(fn (CapacityAssignmentDomainObject $capacity) => $capacity->getAvailableCapacity()) ); } }); @@ -165,11 +173,10 @@ private function processProduct( } private function filterProduct( - ProductDomainObject $product, + ProductDomainObject $product, ?PromoCodeDomainObject $promoCode = null, - bool $hideSoldOutProducts = true, - ): bool - { + bool $hideSoldOutProducts = true, + ): bool { $hidden = false; if ($this->isHiddenByPromoCode($product, $promoCode)) { @@ -202,7 +209,7 @@ private function filterProduct( private function processProductPrice(ProductDomainObject $product, ProductPriceDomainObject $price): void { - if (!$price->isFree()) { + if (! $price->isFree()) { $taxAndFees = $this->taxCalculationService ->calculateTaxAndFeesForProductPrice($product, $price); @@ -244,11 +251,11 @@ private function addPlatformFeeToProduct(ProductDomainObject $product): void $existingTaxesAndFees = $product->getTaxAndFees() ?? collect(); $hasPlatformFee = $existingTaxesAndFees->contains( - fn(TaxAndFeesDomainObject $fee) => $fee->getId() === OrderPlatformFeePassThroughService::PLATFORM_FEE_ID + fn (TaxAndFeesDomainObject $fee) => $fee->getId() === OrderPlatformFeePassThroughService::PLATFORM_FEE_ID ); - if (!$hasPlatformFee) { - $platformFeeDomainObject = (new TaxAndFeesDomainObject()) + if (! $hasPlatformFee) { + $platformFeeDomainObject = (new TaxAndFeesDomainObject) ->setId(OrderPlatformFeePassThroughService::PLATFORM_FEE_ID) ->setAccountId(0) ->setName(OrderPlatformFeePassThroughService::getPlatformFeeName()) @@ -261,14 +268,13 @@ private function addPlatformFeeToProduct(ProductDomainObject $product): void } private function filterProductPrice( - ProductDomainObject $product, + ProductDomainObject $product, ProductPriceDomainObject $price, - bool $hideSoldOutProducts = true - ): bool - { + bool $hideSoldOutProducts = true + ): bool { $hidden = false; - if (!$product->isTieredType()) { + if (! $product->isTieredType()) { return false; } @@ -299,23 +305,23 @@ private function processProductPrices(ProductDomainObject $product, bool $hideSo { $product->setProductPrices( $product->getProductPrices() - ?->each(fn(ProductPriceDomainObject $price) => $this->processProductPrice($product, $price)) - ->reject(fn(ProductPriceDomainObject $price) => $this->filterProductPrice($product, $price, $hideSoldOutProducts)) + ?->each(fn (ProductPriceDomainObject $price) => $this->processProductPrice($product, $price)) + ->reject(fn (ProductPriceDomainObject $price) => $this->filterProductPrice($product, $price, $hideSoldOutProducts)) ); } private function getPriceAvailability(ProductPriceDomainObject $price, ProductDomainObject $product): bool { if ($product->isTieredType()) { - return !$price->isSoldOut() - && !$price->isBeforeSaleStartDate() - && !$price->isAfterSaleEndDate() - && !$price->getIsHidden(); + return ! $price->isSoldOut() + && ! $price->isBeforeSaleStartDate() + && ! $price->isAfterSaleEndDate() + && ! $price->getIsHidden(); } - return !$product->isSoldOut() - && !$product->isBeforeSaleStartDate() - && !$product->isAfterSaleEndDate() - && !$product->getIsHidden(); + return ! $product->isSoldOut() + && ! $product->isBeforeSaleStartDate() + && ! $product->isAfterSaleEndDate() + && ! $product->getIsHidden(); } } diff --git a/backend/tests/Unit/Services/Application/Handlers/Product/GetProductsHandlerTest.php b/backend/tests/Unit/Services/Application/Handlers/Product/GetProductsHandlerTest.php new file mode 100644 index 0000000000..a2660d94aa --- /dev/null +++ b/backend/tests/Unit/Services/Application/Handlers/Product/GetProductsHandlerTest.php @@ -0,0 +1,110 @@ +productRepository = Mockery::mock(ProductRepositoryInterface::class); + $this->productFilterService = Mockery::mock(ProductFilterService::class); + + $this->handler = new GetProductsHandler( + $this->productRepository, + $this->productFilterService + ); + } + + public function test_handle_calls_filter_and_returns_paginator(): void + { + $eventId = 1; + $queryParams = QueryParamsDTO::fromArray([]); + + $product1 = new ProductDomainObject; + $product1->setId(10); + $product1->setEventId($eventId); + + $productsCollection = collect([$product1]); + + $paginator = new LengthAwarePaginator($productsCollection, 1, 15); + + $this->productRepository + ->shouldReceive('loadRelation') + ->with(ProductPriceDomainObject::class) + ->andReturnSelf(); + + $this->productRepository + ->shouldReceive('loadRelation') + ->with(TaxAndFeesDomainObject::class) + ->andReturnSelf(); + + $this->productRepository + ->shouldReceive('findByEventId') + ->with($eventId, $queryParams) + ->andReturn($paginator); + + // Here we mock the behavior of ProductFilterService::filterProducts. + $this->productFilterService + ->shouldReceive('filterProducts') + ->with( + Mockery::on(function ($collection) { + return $collection instanceof Collection && $collection->first() instanceof ProductDomainObject; + }), + null, + false + ) + ->andReturn($productsCollection); + + $result = $this->handler->handle($eventId, $queryParams); + + $this->assertSame($paginator, $result); + $this->assertSame($productsCollection, $result->getCollection()); + } + + public function test_real_filter_throws_on_product_collection(): void + { + $taxService = Mockery::mock(\HiEvents\Services\Domain\Tax\TaxAndFeeCalculationService::class); + $priceService = Mockery::mock(\HiEvents\Services\Domain\Product\ProductPriceService::class); + $fetchService = Mockery::mock(\HiEvents\Services\Domain\Product\AvailableProductQuantitiesFetchService::class); + $feeService = Mockery::mock(\HiEvents\Services\Domain\Order\OrderPlatformFeePassThroughService::class); + $accountRepo = Mockery::mock(\HiEvents\Repository\Interfaces\AccountRepositoryInterface::class); + $eventRepo = Mockery::mock(\HiEvents\Repository\Interfaces\EventRepositoryInterface::class); + + $filterService = new ProductFilterService( + $taxService, + $priceService, + $fetchService, + $feeService, + $accountRepo, + $eventRepo + ); + + $product = new ProductDomainObject; + + $this->expectException(\TypeError::class); + $this->expectExceptionMessageMatches('/must be of type.*ProductCategoryDomainObject.*ProductDomainObject given/'); + + $filterService->filter(collect([$product])); + } +}