productReservationUtil = $this->get(ProductReservationUtil::class); $this->productList = $this->get(ProductList::class); } public function testCreateProductReservation(): void { $this->assertNotEmpty( $this->productReservationUtil->createProductReservation(3, null, 5, ProductReservationUtil::TYPE_RETAIL_RESERVE) ); $this->assertEquals(5, $this->productReservationUtil->getProductReservedQuantity(3), 'Assert that reservation quantity is same as we set'); } public function testCreateProductReservationWithInvalidType(): void { $this->expectException(\InvalidArgumentException::class); $this->productReservationUtil->createProductReservation(3, null, 5, 'nesmysl'); } /** @dataProvider data_testProductsReservationMultiFetch */ public function testProductsReservationMultiFetch(int $productId, ?int $quantity): void { if ($quantity !== null) { $this->productReservationUtil->createProductReservation($productId, null, $quantity, ProductReservationUtil::TYPE_RETAIL_RESERVE); } $products = $this->productList->andSpec(Operator::equals(['p.id' => $productId])) ->getProducts(); $products->fetchReservations(); $product = $products->current(); if ($quantity !== null) { $this->assertNotEmpty($product->reservations); $this->assertEquals($quantity, array_sum(array_map(fn ($x) => $x['quantity'], $product->reservations))); return; } $this->assertEmpty($product->reservations); } public function data_testProductsReservationMultiFetch(): iterable { yield 'Multi fetch for product with existing reservation' => [3, 5]; yield 'Multi fetch for product with zero quantity reservation' => [3, 0]; yield 'Multi fetch for product without existing reservation' => [3, null]; } /** * @dataProvider data_testProductReservationIsAppliedOnProduct */ public function testProductReservationIsAppliedOnProduct(int $productId, ?int $variationId, int $quantity, string $reservationType, bool $withIsTypeB2B, int $expectedInStore): void { if ($withIsTypeB2B) { Contexts::clear(); $this->mockUserContextWithIsTypeCallable(fn () => true); } $this->productReservationUtil->createProductReservation($productId, $variationId, $quantity, $reservationType); $product = $variationId ? new \Variation($productId, $variationId) : new \Product($productId); $product->createFromDB(); $this->assertEquals($expectedInStore, $product->inStore); } /** * @dataProvider data_testProductReservationIsAppliedOnProduct */ public function testProductReservationIsAppliedOnProductList(int $productId, ?int $variationId, int $quantity, string $reservationType, bool $withIsTypeB2B, int $expectedInStore): void { if ($withIsTypeB2B) { Contexts::clear(); $this->mockUserContextWithIsTypeCallable(fn () => true); } $this->productReservationUtil->createProductReservation($productId, $variationId, $quantity, $reservationType); $productList = $this->productList; // reset `resultModifiers` to remove call of `fetchDeliveryText` that changes inStore value to max(inStore, 0), so negative pieces are removed $reflectionClass = new \ReflectionClass($productList); $property = $reflectionClass->getProperty('resultModifiers'); $property->setAccessible(true); $property->setValue($productList, []); $product = $productList ->andSpec(Operator::equals(['p.id' => $productId])) ->fetchVariations(true, false) ->getProducts() ->current(); if ($variationId) { $this->assertEquals($expectedInStore, $product->variations[$variationId]['in_store']); return; } $this->assertEquals($expectedInStore, $product->inStore); } public function data_testProductReservationIsAppliedOnProduct(): iterable { yield 'Product store is 19 pcs, we are creating reservation for 10 pcs so expected store should be 9 pcs' => [9, null, 10, ProductReservationUtil::TYPE_RESERVATION, false, 9]; yield 'Product store is 0 pcs, we are creating reservation for 5 pcs so expected store should be -5 pcs' => [8, null, 5, ProductReservationUtil::TYPE_RESERVATION, false, -5]; yield 'Variation store is 2 pcs, we are creating reservation for 2 pcs so expected store should be 0 pcs' => [2, 9, 2, ProductReservationUtil::TYPE_RESERVATION, false, 0]; yield 'Variation store is 2 pcs, we are creating reservation for 2 pcs, but type TYPE_RETAIL_RESERVE is not active so expected store is still 2 pcs' => [2, 9, 2, ProductReservationUtil::TYPE_RETAIL_RESERVE, false, 2]; yield 'Variation store is 2 pcs, we are creating reservation for 2 pcs and type TYPE_RETAIL_RESERVE is active so expected store is 0 pcs' => [2, 9, 2, ProductReservationUtil::TYPE_RETAIL_RESERVE, true, 0]; } private function mockUserContextWithIsTypeCallable(callable $fn): void { $mock = $this->autowire(UserContextMock::class); $mock->_setIsTypeCallable($fn); $this->set(UserContext::class, $mock); } } class UserContextMock extends UserContext { private ?\Closure $_isTypeCallable = null; public function _setIsTypeCallable(?callable $isTypeCallable): void { $this->_isTypeCallable = $isTypeCallable; } public function isType(string $type): bool { if ($this->_isTypeCallable) { return ($this->_isTypeCallable)($type); } return parent::isType($type); } }