diff --git a/src/models/Order.php b/src/models/Order.php index e89a57f..00cef30 100644 --- a/src/models/Order.php +++ b/src/models/Order.php @@ -122,7 +122,7 @@ public function save(): bool $update_stock_stm = $conn->prepare($query); foreach ($this->line_items as $line_item) { - if (!$line_item->validate()) { + if (!empty($line_item->validate())) { // line item contains invalid attributes $conn->rollBack(); $conn = null; @@ -192,9 +192,14 @@ public function save(): bool * * @param OrderProduct $orderProduct * @return void + * @throws Exception */ public function addLineItem(OrderProduct $orderProduct): void { + $errors = $orderProduct->validate(); + if (!empty($errors)) { + throw new Exception("Invalid line item: " . json_encode($errors)); + } $this->line_items[] = $orderProduct; } diff --git a/src/models/Store.php b/src/models/Store.php index e230aa8..7bb82be 100644 --- a/src/models/Store.php +++ b/src/models/Store.php @@ -146,6 +146,30 @@ public function validate(): array return $errors; } + /** + * Increments stock of a product + * @param int $product_id ID of product whose stock will increase + * @param int $quantity Amount by which stock increases + * @return bool Success or not + */ + public function addProductStock(int $product_id, int $quantity): bool + { + $conn = self::connect(); + $query = "INSERT INTO store_product (store_id, product_id, stock_level) VALUES (:store_id, :product_id, :quantity) + ON DUPLICATE KEY UPDATE stock_level = stock_level + :quantity"; + $params = ['store_id' => $this->store_id, 'product_id' => $product_id, 'quantity' => $quantity]; + $stm = $conn->prepare($query); + $stm->execute($params); + + $rows_affected = $stm->rowCount(); + $conn = null; + return $rows_affected === 1; + } + + /** + * @param int $product_id + * @return int Stock level of product. Defaults to 0. + */ public function getProductStock(int $product_id): int { $query = "SELECT stock_level FROM store_product WHERE store_id = :store_id AND product_id = :product_id;"; @@ -159,6 +183,9 @@ public function getProductStock(int $product_id): int } } + /** + * @return Product[] Array of products which store sells. + */ public function getProducts(): array { $query = "SELECT p.* FROM product p JOIN store_product sp ON p.product_id = sp.product_id WHERE sp.store_id = :store_id;"; diff --git a/tests/OrderProductTest.php b/tests/OrderProductTest.php new file mode 100644 index 0000000..2850896 --- /dev/null +++ b/tests/OrderProductTest.php @@ -0,0 +1,160 @@ +dummy_store = new Store( + phone_no: "987654321", // Phone number + address: new Location( + street: "Augus", + city: "Flacq", + district_id: 2, + latitude: 60, + longitude: 60 + ) + ); + + $success = $this->dummy_store->save(); + if (!$success) { + $errors = $this->dummy_store->validate(); + $error_msg = "Unable to save store to database. "; + if (!empty($errors)) { + $error_msg .= "Errors: " . implode(',', $errors); + } else { + $error_msg .= "Attributes seem to be ok as per validate()."; + } + + throw new Exception($error_msg); + } + + // Create a dummy client + $this->client = new Client( + "john@example.com", + "John", + "Doe", + "john_doe", + "password", + new Location("Royal", "Curepipe", 1, 50, 50) + ); + $success = $this->client->save(); + if (!$success) { + throw new Exception('Unable to save client'); + } + + // Create a dummy product + $this->dummy_product = new Product( + "Latte", + 50, + "latte.jpeg", + "A delicious latte", + "Beverage", + 5.0, + "A cup of latte", + new DateTime() + ); + $success = $this->dummy_product->save(); + if (!$success) { + throw new Exception('Unable to save product'); + } + + // Update stock level for the product + $this->dummy_store->addProductStock($this->dummy_product->getProductID(), 10); + + // Create dummy order line items + $this->line_items = [ + new OrderProduct($this->dummy_product->getProductID(), "medium", "oat", 2, 5.0) + ]; + + // Create a dummy order + $this->dummy_order = new Order( + $this->dummy_store->getStoreID(), + $this->client->getUserID() + ); + + // Add line items to the order + foreach ($this->line_items as $line_item) { + $this->dummy_order->addLineItem($line_item); + } + + $success = $this->dummy_order->save(); + if (!$success) { + throw new Exception('Unable to save order'); + } + } + + public function tearDown(): void + { + $this->dummy_order = null; + $this->client = null; + $this->dummy_store = null; + $this->dummy_product = null; + $this->line_items = []; + + // Clear all data from relevant tables + self::query( + 'DELETE FROM order_product; DELETE FROM `order`; DELETE FROM client; DELETE FROM user; DELETE FROM store_product; DELETE FROM product; DELETE FROM store;' + ); + } + + public function testValidate(): void + { + $invalidOrderProduct = new OrderProduct( + product_id: $this->dummy_product->getProductID(), + cup_size: "extra large", // Invalid cup size + milk_type: "invalid milk", // Invalid milk type + quantity: -1, // Invalid quantity + unit_price: -2.99, // Invalid unit price + order_id: $this->dummy_order->getOrderID() + ); + + $errors = $invalidOrderProduct->validate(); + + $this->assertArrayHasKey('quantity', $errors); + $this->assertArrayHasKey('cup_size', $errors); + $this->assertArrayHasKey('milk_type', $errors); + $this->assertArrayHasKey('unit_price', $errors); + } + + public function testGetByID(): void + { + // Assuming getByID is a method that retrieves an OrderProduct by order ID and product ID + $retrievedOrderProduct = OrderProduct::getByID( + $this->dummy_order->getOrderID(), + $this->dummy_product->getProductID() + ); + + $this->assertNotNull($retrievedOrderProduct); + $this->assertEquals($this->dummy_order->getOrderID(), $retrievedOrderProduct->getOrderID()); + $this->assertEquals($this->dummy_product->getProductID(), $retrievedOrderProduct->getProductID()); + $this->assertEquals("medium", $retrievedOrderProduct->getCupSize()); + $this->assertEquals("oat", $retrievedOrderProduct->getMilkType()); + $this->assertEquals(2, $retrievedOrderProduct->getQuantity()); + $this->assertEquals(5.0, $retrievedOrderProduct->getUnitPrice()); + } +} diff --git a/tests/OrderTest.php b/tests/OrderTest.php new file mode 100644 index 0000000..0bbda2a --- /dev/null +++ b/tests/OrderTest.php @@ -0,0 +1,241 @@ +dummy_store = new Store( + phone_no: "987654321", // Phone number + address: new Location( + street: "Augus", + city: "Flacq", + district_id: 2, + latitude: 60, + longitude: 60 + ) + ); + + $success = $this->dummy_store->save(); + if (!$success) { + $errors = $this->dummy_store->validate(); + $error_msg = "Unable to save store to database. "; + if (!empty($errors)) { + $error_msg .= "Errors: " . implode(',', $errors); + } else { + $error_msg .= "Attributes seem to be ok as per validate()."; + } + + throw new Exception($error_msg); + } + + // Create a dummy client + $this->client = new Client( + "john@example.com", + "John", + "Doe", + "john_doe", + "password", + new Location("Royal", "Curepipe", 1, 50, 50) + ); + $success = $this->client->save(); + if (!$success) { + throw new Exception('Unable to save client'); + } + + // Create dummy products + $product1 = new Product( + "Latte", + 50, + "latte.jpeg", + "A delicious latte", + "Beverage", + 5.0, + "A cup of latte", + new DateTime() + ); + $success = $product1->save(); + if (!$success) { + throw new Exception('Unable to save product 1'); + } + + $product2 = new Product( + "Espresso", + 30, + "espresso.jpeg", + "A strong espresso", + "Beverage", + 3.0, + "A cup of espresso", + new DateTime() + ); + $success = $product2->save(); + if (!$success) { + throw new Exception('Unable to save product 2'); + } + + // Add stock to the store for the products + $this->dummy_store->addProductStock($product1->getProductID(), 10); + $this->dummy_store->addProductStock($product2->getProductID(), 10); + + // Create dummy order line items + $this->line_items = [ + new OrderProduct($product1->getProductID(), "medium", "oat", 2, 5.0), + new OrderProduct($product2->getProductID(), "small", "almond", 1, 3.0) + ]; + + // Create a dummy order + $this->dummy_order = new Order( + $this->dummy_store->getStoreID(), + $this->client->getUserID(), + $this->line_items + ); + } + + public function tearDown(): void + { + $this->dummy_order = null; + $this->client = null; + $this->dummy_store = null; + $this->line_items = []; + + // Clear all data from relevant tables + self::query( + 'DELETE FROM order_product; DELETE FROM `order`; DELETE FROM client; DELETE FROM user; DELETE FROM store_product; DELETE FROM product; DELETE FROM store;' + ); + } + + public function testConstructor(): void + { + $new_order = new Order( + $this->dummy_store->getStoreID(), + $this->client->getUserID(), + $this->line_items + ); + + self::assertEquals($this->dummy_store->getStoreID(), $new_order->getStoreID()); + self::assertEquals($this->client->getUserID(), $new_order->getClientID()); + self::assertEquals(OrderStatus::PENDING, $new_order->getStatus()); + self::assertEquals($this->line_items, $new_order->getLineItems()); + } + + public function testToArray(): void + { + $result = $this->dummy_order->toArray(); + + self::assertArrayHasKey('order_id', $result); + self::assertArrayHasKey('status', $result); + self::assertArrayHasKey('created_date', $result); + self::assertArrayHasKey('pickup_date', $result); + self::assertArrayHasKey('client_id', $result); + self::assertArrayHasKey('store_id', $result); + + self::assertEquals($this->dummy_order->getOrderID(), $result['order_id']); + self::assertEquals($this->dummy_order->getStatus()->value, $result['status']); + self::assertEquals($this->dummy_order->getCreatedDate()->format('Y-m-d H:i:s'), $result['created_date']); + self::assertEquals($this->dummy_order->getPickupDate()?->format('Y-m-d H:i:s'), $result['pickup_date']); + self::assertEquals($this->dummy_order->getClientID(), $result['client_id']); + self::assertEquals($this->dummy_order->getStoreID(), $result['store_id']); + } + + /** + * @throws Exception + */ + public function testSave(): void + { + $success = $this->dummy_order->save(); + self::assertTrue($success); + + $order_id = $this->dummy_order->getOrderID(); + self::assertGreaterThan(0, $order_id); + + // Verify order in database + $saved_order = Order::getByID($order_id); + self::assertNotNull($saved_order); + self::assertEquals($this->dummy_order->getStoreID(), $saved_order->getStoreID()); + self::assertEquals($this->dummy_order->getClientID(), $saved_order->getClientID()); + } + + public function testSaveWithEmptyLineItems(): void + { + $this->expectException(Exception::class); + $this->expectExceptionMessage('Cart cannot be empty'); + + $order = new Order($this->dummy_store->getStoreID(), $this->client->getUserID(), []); + $order->save(); + } + + /** + * @throws Exception + */ + public function testAddLineItem(): void + { + $order = new Order($this->dummy_store->getStoreID(), $this->client->getUserID()); + $order->addLineItem(new OrderProduct(1, "medium", "oat", 1, 5.0)); + self::assertCount(1, $order->getLineItems()); + } + + /** + * @throws Exception + */ + public function testGetByID(): void + { + $this->dummy_order->save(); + $order_id = $this->dummy_order->getOrderID(); + + $fetched_order = Order::getByID($order_id); + self::assertNotNull($fetched_order); + + self::assertEquals($this->dummy_order->getStoreID(), $fetched_order->getStoreID()); + self::assertEquals($this->dummy_order->getClientID(), $fetched_order->getClientID()); + self::assertEquals($this->dummy_order->getStatus(), $fetched_order->getStatus()); + + // Test getByID with invalid ID + self::assertNull(Order::getByID(-1)); + } + + /** + * @throws Exception + */ + public function testCalculateTotalPrice(): void + { + $this->dummy_order->save(); + $total_price = $this->dummy_order->calculateTotalPrice(); + + $expected_price = array_reduce($this->line_items, function ($carry, $item) { + return $carry + $item->getQuantity() * $item->getUnitPrice(); + }, 0); + + self::assertEquals($expected_price, $total_price); + } + + public function testValidate(): void + { + $errors = $this->dummy_order->validate(); + self::assertEmpty($errors); + } +}