diff --git a/.github/workflows/db.yml b/.github/workflows/db.yml new file mode 100644 index 0000000..06bb375 --- /dev/null +++ b/.github/workflows/db.yml @@ -0,0 +1,56 @@ +name: Generate schema + +on: + push: + paths: + - resources/database/** + workflow_dispatch: + +jobs: + build: + runs-on: ubuntu-latest + + env: + DB_USER: root # do not change + DB_PASSWORD: root # do not change + + steps: + - uses: actions/checkout@v4 + + - name: Setup database + run: | + mysql -V + sudo /etc/init.d/mysql start + mysql -u$DB_USER -p$DB_PASSWORD -hlocalhost -P3306 < "resources/database/cafe_schema.sql" + mysql -e "USE cafe; SHOW TABLES;" -u$DB_USER -p$DB_PASSWORD + + - name: Install spyschema dependencies + run: | + wget https://github.com/schemaspy/schemaspy/releases/download/v6.2.4/schemaspy-6.2.4.jar + wget https://dev.mysql.com/get/Downloads/Connector-J/mysql-connector-j-8.4.0.tar.gz + tar -xvzf mysql-connector-j-8.4.0.tar.gz + + - name: Output dependencies information + run: | + mysql --version + java --version + + - name: Generate schema spy website + run: | + java -jar schemaspy-6.2.4.jar \ + -t mysql \ + -dp mysql-connector-j-8.4.0/mysql-connector-j-8.4.0.jar \ + -db cafe \ + -host localhost \ + -u $DB_USER \ + -p $DB_PASSWORD \ + -o dist \ + -s cafe \ + -vizjs + + - name: Deploy + uses: peaceiris/actions-gh-pages@v3 + if: github.ref == 'refs/heads/main' + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + publish_dir: ./dist \ No newline at end of file diff --git a/resources/database/cafe_data.sql b/resources/database/cafe_data.sql index 0d066e4..f6dc3b2 100644 --- a/resources/database/cafe_data.sql +++ b/resources/database/cafe_data.sql @@ -29,7 +29,7 @@ UNLOCK TABLES; LOCK TABLES `client` WRITE; /*!40000 ALTER TABLE `client` DISABLE KEYS */; -INSERT INTO `client` VALUES (1,'Royal Road','Rochester',2),(2,'Main Road','Curepipe',9),(10,'Main Road','Curepipe',9); +INSERT INTO `client` VALUES (1,'Royal Road','Rochester',2),(2,'Main Road','Curepipe',9); /*!40000 ALTER TABLE `client` ENABLE KEYS */; UNLOCK TABLES; diff --git a/resources/database/cafe_schema.sql b/resources/database/cafe_schema.sql index e23c94e..dc97a7d 100644 --- a/resources/database/cafe_schema.sql +++ b/resources/database/cafe_schema.sql @@ -33,7 +33,7 @@ DROP TABLE IF EXISTS `administrator`; CREATE TABLE `administrator` ( `user_id` int(11) unsigned NOT NULL, `job_title` varchar(255) NOT NULL, - `is_super_admin` tinyint(1) DEFAULT 0, + `is_super_admin` tinyint(1) NOT NULL DEFAULT 0, PRIMARY KEY (`user_id`), CONSTRAINT `admin_fk` FOREIGN KEY (`user_id`) REFERENCES `user` (`user_id`) ON DELETE CASCADE ON UPDATE CASCADE, CONSTRAINT `job_title_length` CHECK (char_length(`job_title`) > 3) @@ -51,7 +51,7 @@ CREATE TABLE `client` ( `user_id` int(11) unsigned NOT NULL, `street` varchar(255) NOT NULL, `city` varchar(255) NOT NULL, - `district_id` int(11) unsigned DEFAULT NULL, + `district_id` int(11) unsigned NOT NULL, PRIMARY KEY (`user_id`), KEY `client_district_district_id_fk` (`district_id`), CONSTRAINT `client_district_district_id_fk` FOREIGN KEY (`district_id`) REFERENCES `district` (`district_id`) ON UPDATE CASCADE, @@ -74,7 +74,7 @@ CREATE TABLE `comment` ( `created_date` datetime NOT NULL DEFAULT current_timestamp(), `parent_comment_id` int(10) unsigned DEFAULT NULL, `user_id` int(10) unsigned NOT NULL, - `review_id` int(10) unsigned DEFAULT NULL, + `review_id` int(10) unsigned NOT NULL COMMENT 'ID of review under which comment is found ', PRIMARY KEY (`comment_id`), KEY `comment_comment_comment_id_fk` (`parent_comment_id`), KEY `comment_user_user_id_fk` (`user_id`), @@ -94,10 +94,9 @@ DROP TABLE IF EXISTS `district`; /*!40101 SET character_set_client = utf8 */; CREATE TABLE `district` ( `district_id` int(11) unsigned NOT NULL AUTO_INCREMENT, - `name` varchar(30) NOT NULL, + `name` enum('Moka','Port Louis','Flacq','Curepipe','Black River','Savanne','Grand Port','Riviere du Rempart','Pamplemousses','Mahebourg','Plaines Wilhems') NOT NULL, PRIMARY KEY (`district_id`), - UNIQUE KEY `name` (`name`), - CONSTRAINT `name_values` CHECK (`name` in ('Moka','Port Louis','Flacq','Curepipe','Black River','Savanne','Grand Port','Riviere du Rempart','Pamplemousses','Mahebourg','Plaines Wilhems')) + UNIQUE KEY `name` (`name`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci; /*!40101 SET character_set_client = @saved_cs_client */; @@ -110,11 +109,11 @@ DROP TABLE IF EXISTS `order`; /*!40101 SET character_set_client = utf8 */; CREATE TABLE `order` ( `order_id` int(11) unsigned NOT NULL AUTO_INCREMENT, - `status` varchar(20) DEFAULT 'pending', - `created_date` datetime DEFAULT current_timestamp(), - `pickup_date` datetime DEFAULT NULL, + `status` enum('pending','cancelled','completed') NOT NULL DEFAULT 'pending', + `created_date` datetime NOT NULL DEFAULT current_timestamp(), + `pickup_date` datetime DEFAULT NULL COMMENT 'Date when client picks up his order at the store', `client_id` int(11) unsigned DEFAULT NULL, - `store_id` int(10) unsigned DEFAULT NULL, + `store_id` int(10) unsigned NOT NULL, PRIMARY KEY (`order_id`), KEY `order_fk` (`client_id`), KEY `order_store_store_id_fk` (`store_id`), @@ -134,16 +133,14 @@ DROP TABLE IF EXISTS `order_product`; CREATE TABLE `order_product` ( `order_id` int(11) unsigned NOT NULL, `product_id` int(11) unsigned NOT NULL, - `cup_size` varchar(20) NOT NULL, - `milk_type` varchar(20) NOT NULL, - `quantity` int(11) unsigned DEFAULT NULL, - `unit_price` decimal(10,2) DEFAULT NULL, + `cup_size` enum('small','medium','large') NOT NULL, + `milk_type` enum('almond','coconut','oat','soy') NOT NULL, + `quantity` int(11) unsigned NOT NULL, + `unit_price` decimal(10,2) NOT NULL COMMENT 'Unit price of product', PRIMARY KEY (`order_id`,`product_id`,`cup_size`,`milk_type`), KEY `order_product_product_product_id_fk` (`product_id`), CONSTRAINT `order_product_order_order_id_fk` FOREIGN KEY (`order_id`) REFERENCES `order` (`order_id`), CONSTRAINT `order_product_product_product_id_fk` FOREIGN KEY (`product_id`) REFERENCES `product` (`product_id`), - CONSTRAINT `cup_size` CHECK (`cup_size` in ('small','medium','large')), - CONSTRAINT `milk_type` CHECK (`milk_type` in ('almond','coconut','oat','soy')), CONSTRAINT `quantity_range` CHECK (`quantity` > 0), CONSTRAINT `unit_price_range` CHECK (`unit_price` > 0) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci; @@ -161,7 +158,7 @@ CREATE TABLE `password_change_request` ( `user_id` int(11) unsigned NOT NULL, `token_hash` varchar(255) NOT NULL, `expiry_date` datetime NOT NULL, - `used` tinyint(1) NOT NULL DEFAULT 0, + `used` tinyint(1) NOT NULL DEFAULT 0 COMMENT 'Whether token has been used once ', PRIMARY KEY (`request_id`), KEY `request_fk` (`user_id`), CONSTRAINT `request_fk` FOREIGN KEY (`user_id`) REFERENCES `user` (`user_id`) ON DELETE CASCADE ON UPDATE CASCADE @@ -178,12 +175,12 @@ DROP TABLE IF EXISTS `product`; CREATE TABLE `product` ( `product_id` int(11) unsigned NOT NULL AUTO_INCREMENT, `name` varchar(255) NOT NULL, - `calories` int(11) unsigned DEFAULT NULL CHECK (`calories` >= 0), + `calories` int(11) unsigned NOT NULL, `img_url` varchar(255) NOT NULL, `img_alt_text` varchar(150) NOT NULL, `category` varchar(50) NOT NULL, `price` decimal(10,2) NOT NULL, - `description` text DEFAULT NULL CHECK (char_length(`description`) > 0), + `description` text NOT NULL CHECK (char_length(`description`) > 0), `created_date` datetime NOT NULL DEFAULT current_timestamp(), PRIMARY KEY (`product_id`), CONSTRAINT `name_length` CHECK (char_length(`name`) > 2), @@ -205,8 +202,8 @@ CREATE TABLE `review` ( `rating` int(11) unsigned NOT NULL, `created_date` datetime NOT NULL DEFAULT current_timestamp(), `text` varchar(2000) NOT NULL, - `client_id` int(11) unsigned DEFAULT NULL, - `product_id` int(11) unsigned DEFAULT NULL, + `client_id` int(11) unsigned NOT NULL, + `product_id` int(11) unsigned NOT NULL, PRIMARY KEY (`review_id`), KEY `review_1fk` (`client_id`), KEY `review_2fk` (`product_id`), @@ -266,10 +263,10 @@ DROP TABLE IF EXISTS `user`; CREATE TABLE `user` ( `user_id` int(11) unsigned NOT NULL AUTO_INCREMENT, `email` varchar(320) NOT NULL, - `first_name` varchar(255) DEFAULT NULL, - `password` varchar(255) DEFAULT NULL, + `first_name` varchar(255) NOT NULL, + `password` varchar(255) NOT NULL, `phone_no` varchar(255) NOT NULL, - `last_name` varchar(255) DEFAULT NULL, + `last_name` varchar(255) NOT NULL, PRIMARY KEY (`user_id`), UNIQUE KEY `unique_email` (`email`), CONSTRAINT `email_format` CHECK (`email` like '%@%.%'), @@ -289,4 +286,4 @@ CREATE TABLE `user` ( /*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */; /*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */; --- Dump completed on 2024-05-15 20:45:06 +-- Dump completed on 2024-05-21 8:07:34 diff --git a/resources/database/cafe_test_schema.sql b/resources/database/cafe_test_schema.sql index 106b02d..c3e4a87 100644 --- a/resources/database/cafe_test_schema.sql +++ b/resources/database/cafe_test_schema.sql @@ -33,7 +33,7 @@ DROP TABLE IF EXISTS `administrator`; CREATE TABLE `administrator` ( `user_id` int(11) unsigned NOT NULL, `job_title` varchar(255) NOT NULL, - `is_super_admin` tinyint(1) DEFAULT 0, + `is_super_admin` tinyint(1) NOT NULL DEFAULT 0, PRIMARY KEY (`user_id`), CONSTRAINT `admin_fk` FOREIGN KEY (`user_id`) REFERENCES `user` (`user_id`) ON DELETE CASCADE ON UPDATE CASCADE, CONSTRAINT `job_title_length` CHECK (char_length(`job_title`) > 3) @@ -51,7 +51,7 @@ CREATE TABLE `client` ( `user_id` int(11) unsigned NOT NULL, `street` varchar(255) NOT NULL, `city` varchar(255) NOT NULL, - `district_id` int(11) unsigned DEFAULT NULL, + `district_id` int(11) unsigned NOT NULL, PRIMARY KEY (`user_id`), KEY `client_district_district_id_fk` (`district_id`), CONSTRAINT `client_district_district_id_fk` FOREIGN KEY (`district_id`) REFERENCES `district` (`district_id`) ON UPDATE CASCADE, @@ -74,7 +74,7 @@ CREATE TABLE `comment` ( `created_date` datetime NOT NULL DEFAULT current_timestamp(), `parent_comment_id` int(10) unsigned DEFAULT NULL, `user_id` int(10) unsigned NOT NULL, - `review_id` int(10) unsigned DEFAULT NULL, + `review_id` int(10) unsigned NOT NULL COMMENT 'ID of review under which comment is found ', PRIMARY KEY (`comment_id`), KEY `comment_comment_comment_id_fk` (`parent_comment_id`), KEY `comment_user_user_id_fk` (`user_id`), @@ -94,10 +94,9 @@ DROP TABLE IF EXISTS `district`; /*!40101 SET character_set_client = utf8 */; CREATE TABLE `district` ( `district_id` int(11) unsigned NOT NULL AUTO_INCREMENT, - `name` varchar(30) NOT NULL, + `name` enum('Moka','Port Louis','Flacq','Curepipe','Black River','Savanne','Grand Port','Riviere du Rempart','Pamplemousses','Mahebourg','Plaines Wilhems') NOT NULL, PRIMARY KEY (`district_id`), - UNIQUE KEY `name` (`name`), - CONSTRAINT `name_values` CHECK (`name` in ('Moka','Port Louis','Flacq','Curepipe','Black River','Savanne','Grand Port','Riviere du Rempart','Pamplemousses','Mahebourg','Plaines Wilhems')) + UNIQUE KEY `name` (`name`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci; /*!40101 SET character_set_client = @saved_cs_client */; @@ -110,11 +109,11 @@ DROP TABLE IF EXISTS `order`; /*!40101 SET character_set_client = utf8 */; CREATE TABLE `order` ( `order_id` int(11) unsigned NOT NULL AUTO_INCREMENT, - `status` varchar(20) DEFAULT 'pending', - `created_date` datetime DEFAULT current_timestamp(), - `pickup_date` datetime DEFAULT NULL, + `status` enum('pending','cancelled','completed') NOT NULL DEFAULT 'pending', + `created_date` datetime NOT NULL DEFAULT current_timestamp(), + `pickup_date` datetime DEFAULT NULL COMMENT 'Date when client picks up his order at the store', `client_id` int(11) unsigned DEFAULT NULL, - `store_id` int(10) unsigned DEFAULT NULL, + `store_id` int(10) unsigned NOT NULL, PRIMARY KEY (`order_id`), KEY `order_fk` (`client_id`), KEY `order_store_store_id_fk` (`store_id`), @@ -134,16 +133,14 @@ DROP TABLE IF EXISTS `order_product`; CREATE TABLE `order_product` ( `order_id` int(11) unsigned NOT NULL, `product_id` int(11) unsigned NOT NULL, - `cup_size` varchar(20) NOT NULL, - `milk_type` varchar(20) NOT NULL, - `quantity` int(11) unsigned DEFAULT NULL, - `unit_price` decimal(10,2) DEFAULT NULL, + `cup_size` enum('small','medium','large') NOT NULL, + `milk_type` enum('almond','coconut','oat','soy') NOT NULL, + `quantity` int(11) unsigned NOT NULL, + `unit_price` decimal(10,2) NOT NULL COMMENT 'Unit price of product', PRIMARY KEY (`order_id`,`product_id`,`cup_size`,`milk_type`), KEY `order_product_product_product_id_fk` (`product_id`), CONSTRAINT `order_product_order_order_id_fk` FOREIGN KEY (`order_id`) REFERENCES `order` (`order_id`), CONSTRAINT `order_product_product_product_id_fk` FOREIGN KEY (`product_id`) REFERENCES `product` (`product_id`), - CONSTRAINT `cup_size` CHECK (`cup_size` in ('small','medium','large')), - CONSTRAINT `milk_type` CHECK (`milk_type` in ('almond','coconut','oat','soy')), CONSTRAINT `quantity_range` CHECK (`quantity` > 0), CONSTRAINT `unit_price_range` CHECK (`unit_price` > 0) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci; @@ -161,7 +158,7 @@ CREATE TABLE `password_change_request` ( `user_id` int(11) unsigned NOT NULL, `token_hash` varchar(255) NOT NULL, `expiry_date` datetime NOT NULL, - `used` tinyint(1) NOT NULL DEFAULT 0, + `used` tinyint(1) NOT NULL DEFAULT 0 COMMENT 'Whether token has been used once ', PRIMARY KEY (`request_id`), KEY `request_fk` (`user_id`), CONSTRAINT `request_fk` FOREIGN KEY (`user_id`) REFERENCES `user` (`user_id`) ON DELETE CASCADE ON UPDATE CASCADE @@ -178,12 +175,12 @@ DROP TABLE IF EXISTS `product`; CREATE TABLE `product` ( `product_id` int(11) unsigned NOT NULL AUTO_INCREMENT, `name` varchar(255) NOT NULL, - `calories` int(11) unsigned DEFAULT NULL CHECK (`calories` >= 0), + `calories` int(11) unsigned NOT NULL, `img_url` varchar(255) NOT NULL, `img_alt_text` varchar(150) NOT NULL, `category` varchar(50) NOT NULL, `price` decimal(10,2) NOT NULL, - `description` text DEFAULT NULL CHECK (char_length(`description`) > 0), + `description` text NOT NULL CHECK (char_length(`description`) > 0), `created_date` datetime NOT NULL DEFAULT current_timestamp(), PRIMARY KEY (`product_id`), CONSTRAINT `name_length` CHECK (char_length(`name`) > 2), @@ -205,8 +202,8 @@ CREATE TABLE `review` ( `rating` int(11) unsigned NOT NULL, `created_date` datetime NOT NULL DEFAULT current_timestamp(), `text` varchar(2000) NOT NULL, - `client_id` int(11) unsigned DEFAULT NULL, - `product_id` int(11) unsigned DEFAULT NULL, + `client_id` int(11) unsigned NOT NULL, + `product_id` int(11) unsigned NOT NULL, PRIMARY KEY (`review_id`), KEY `review_1fk` (`client_id`), KEY `review_2fk` (`product_id`), @@ -266,10 +263,10 @@ DROP TABLE IF EXISTS `user`; CREATE TABLE `user` ( `user_id` int(11) unsigned NOT NULL AUTO_INCREMENT, `email` varchar(320) NOT NULL, - `first_name` varchar(255) DEFAULT NULL, - `password` varchar(255) DEFAULT NULL, + `first_name` varchar(255) NOT NULL, + `password` varchar(255) NOT NULL, `phone_no` varchar(255) NOT NULL, - `last_name` varchar(255) DEFAULT NULL, + `last_name` varchar(255) NOT NULL, PRIMARY KEY (`user_id`), UNIQUE KEY `unique_email` (`email`), CONSTRAINT `email_format` CHECK (`email` like '%@%.%'), @@ -289,4 +286,4 @@ CREATE TABLE `user` ( /*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */; /*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */; --- Dump completed on 2024-05-15 20:45:06 +-- Dump completed on 2024-05-21 8:07:34 diff --git a/src/controllers/Profile.php b/src/controllers/Profile.php index be9e8e0..d64fd06 100644 --- a/src/controllers/Profile.php +++ b/src/controllers/Profile.php @@ -9,6 +9,7 @@ use Steamy\Model\Client; use Steamy\Model\District; use Steamy\Model\Location; +use Steamy\Model\Order; class Profile { @@ -218,17 +219,10 @@ public function index(): void return; } - // TODO: fetch 5 latest orders - $this->view_data["orders"] = array_fill( - 0, - 5, - (object)[ - 'date' => '16/01/2024', - 'id' => 4343, - 'cost' => 100.00, - 'status' => 'Completed' - ] - ); + // Fetch orders for the signed-in client + $orders = Order::getOrdersByClientId($this->signed_client->getUserID()); + + $this->view_data["orders"] = $orders; // initialize user details for template $this->view_data["name"] = $this->signed_client->getFirstName() . " " . $this->signed_client->getLastName(); diff --git a/src/models/Order.php b/src/models/Order.php index 5a88b5d..00cef30 100644 --- a/src/models/Order.php +++ b/src/models/Order.php @@ -4,6 +4,7 @@ namespace Steamy\Model; +use PDO; use DateTime; use Exception; use PDOException; @@ -121,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; @@ -191,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; } @@ -264,6 +270,52 @@ private static function getOrderProducts(int $order_id): array return $order_products_arr; } + /** + * Retrieves a list of orders for a specific client. + * + * @param int $client_id The ID of the client whose orders are to be retrieved. + * @param int $limit The maximum number of orders to retrieve. Defaults to 5. + * @return Order[] An array of Order objects ordered in descending order of created_date + * @throws PDOException If there is an error executing the database query. + */ + public static function getOrdersByClientId(int $client_id, int $limit = 5): array + { + $db = self::connect(); + $stmt = $db->prepare( + ' + SELECT o.order_id, o.created_date, o.status, o.store_id, o.pickup_date, o.client_id + FROM `order` o + WHERE o.client_id = :client_id + ORDER BY o.created_date DESC + LIMIT :limit; + ' + ); + $stmt->bindParam(':client_id', $client_id, PDO::PARAM_INT); + $stmt->bindParam(':limit', $limit, PDO::PARAM_INT); + $stmt->execute(); + + $orderDataArray = $stmt->fetchAll(PDO::FETCH_OBJ); + $orders = []; + + foreach ($orderDataArray as $orderData) { + // Get the line items for this order + $lineItems = self::getOrderProducts((int)$orderData->order_id); + + // Create an Order object with the retrieved data + $orders[] = new Order( + store_id: (int)$orderData->store_id, + client_id: (int)$orderData->client_id, + line_items: $lineItems, + order_id: (int)$orderData->order_id, + pickup_date: $orderData->pickup_date ? Utility::stringToDate($orderData->pickup_date) : null, + status: OrderStatus::from($orderData->status), + created_date: Utility::stringToDate($orderData->created_date), + ); + } + $db = null; + return $orders; + } + public function getOrderID(): int { 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/src/views/Profile.php b/src/views/Profile.php index 54d9425..14f600b 100644 --- a/src/views/Profile.php +++ b/src/views/Profile.php @@ -3,14 +3,15 @@ declare(strict_types=1); /** - * The following attributes are defined in controllers/Profile.php + * The following attributes are defined in controllers/Profile.php: * - * @var $client Client signed in client - * @var $show_account_deletion_confirmation bool Whether to display a confirmation dialog for account deletion - * @var $orders array array of orders + * @var Client $client signed in client + * @var Order[] $orders array of orders + * @var bool $show_account_deletion_confirmation Whether to display a confirmation dialog for account deletion */ use Steamy\Model\Client; +use Steamy\Model\Order; ?> @@ -85,30 +86,33 @@
- - + + + date); - $id = filter_var($order->id, FILTER_SANITIZE_NUMBER_INT); - $cost = filter_var($order->cost, FILTER_SANITIZE_NUMBER_FLOAT, FILTER_FLAG_ALLOW_FRACTION); - $status = htmlspecialchars($order->status); + $date = htmlspecialchars($order->getCreatedDate()->format('Y-m-d H:i:s')); + $id = filter_var($order->getOrderID(), FILTER_SANITIZE_NUMBER_INT); + $storeid = filter_var($order->getStoreID(), FILTER_SANITIZE_NUMBER_INT); + $status = htmlspecialchars(ucfirst($order->getStatus()->value)); + $totalPrice = htmlspecialchars(number_format($order->calculateTotalPrice(), 2)); echo <<< EOL - - + + + - EOL; + EOL; } ?> @@ -201,9 +205,8 @@ function openTab(evt, tabName) { }, ); }); - - + @@ -219,4 +222,4 @@ function openTab(evt, tabName) { +endif; ?> \ No newline at end of file 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); + } +}
Date Order IDTotal costStore IDDate StatusTotal Price Actions
$date $id$cost$storeid$date $status\$$totalPrice