From 8d5fa3a68e743ad6c3c9f3de6223b1caab315e57 Mon Sep 17 00:00:00 2001 From: creme332 <65414576+creme332@users.noreply.github.com> Date: Wed, 22 May 2024 10:56:33 +0400 Subject: [PATCH 01/14] remove validation from addLineItem validation should only take place before persistence --- src/models/Order.php | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/models/Order.php b/src/models/Order.php index 00cef30..426c3bd 100644 --- a/src/models/Order.php +++ b/src/models/Order.php @@ -192,14 +192,9 @@ 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; } From 49ac371e203644b2960ed4b2517cf3711949f557 Mon Sep 17 00:00:00 2001 From: creme332 <65414576+creme332@users.noreply.github.com> Date: Wed, 22 May 2024 11:14:50 +0400 Subject: [PATCH 02/14] improve exception message when line item is invalid in save() --- src/models/Order.php | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/models/Order.php b/src/models/Order.php index 426c3bd..b5008e8 100644 --- a/src/models/Order.php +++ b/src/models/Order.php @@ -122,11 +122,19 @@ public function save(): bool $update_stock_stm = $conn->prepare($query); foreach ($this->line_items as $line_item) { - if (!empty($line_item->validate())) { + $line_item_errors = $line_item->validate(); + + if (!empty($line_item_errors)) { // line item contains invalid attributes $conn->rollBack(); $conn = null; - throw new Exception("Invalid line item:" . json_encode($line_item)); + + $error_message = "Invalid line item:" . json_encode($line_item->toArray()); + $error_message .= " Errors: " . json_encode($line_item_errors); + + throw new Exception( + $error_message + ); } // fetch product corresponding to line item From 248c40ed829517c955902b56c5a25a4adc73e01c Mon Sep 17 00:00:00 2001 From: creme332 <65414576+creme332@users.noreply.github.com> Date: Wed, 22 May 2024 11:15:15 +0400 Subject: [PATCH 03/14] improve error messages --- src/controllers/Cart.php | 34 +++++++++++++++++++++------------- 1 file changed, 21 insertions(+), 13 deletions(-) diff --git a/src/controllers/Cart.php b/src/controllers/Cart.php index 5f93ec9..aec8d85 100644 --- a/src/controllers/Cart.php +++ b/src/controllers/Cart.php @@ -7,7 +7,6 @@ use Exception; use Steamy\Core\Controller; use Steamy\Core\Utility; -use Steamy\Model\Mailer; use Steamy\Model\Order; use Steamy\Model\OrderProduct; use Steamy\Model\Product; @@ -71,8 +70,6 @@ private function handleInvalidURL(): void private function handleCheckout(): void { - // TODO: write appropriate errors to Cart view instead of sending response code - // check if user is logged in $signed_client = $this->getSignedInClient(); if (!$signed_client) { @@ -110,24 +107,35 @@ private function handleCheckout(): void $new_order->addLineItem($line_item); } - // save order - $success_order = false; + // attempt to save order. An exception will be generated in case of any errors. try { $success_order = $new_order->save(); - http_response_code($success_order ? 201 : 400); } catch (Exception $e) { error_log($e->getMessage()); http_response_code(500); echo json_encode(['error' => $e->getMessage()]); + return; } - // send confirmation email if order was successfully saved - if ($success_order) { - try { - $signed_client->sendOrderConfirmationEmail($new_order); - } catch (Exception $e) { - error_log($e->getMessage()); - } + // if order was unsuccessfully saved without any exceptions generated + if (!$success_order) { + http_response_code(500); + echo json_encode(['error' => "Order could not be saved for an unknown reason."]); + return; + } + + // send confirmation email + try { + $success_mail = $signed_client->sendOrderConfirmationEmail($new_order); + } catch (Exception $e) { + http_response_code(503); + echo json_encode(['error' => "Order was saved but email could not be sent: " . $e->getMessage()]); + return; + } + + if (!$success_mail) { + http_response_code(503); + echo json_encode(['error' => "Order was saved but email could not be sent for an unknown reason."]); } } From 69239d8b8b07c18d34be8f337879cf69ea06579a Mon Sep 17 00:00:00 2001 From: creme332 <65414576+creme332@users.noreply.github.com> Date: Wed, 22 May 2024 11:48:33 +0400 Subject: [PATCH 04/14] add doc to constructor --- src/models/OrderProduct.php | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/models/OrderProduct.php b/src/models/OrderProduct.php index 0048327..2c79e13 100644 --- a/src/models/OrderProduct.php +++ b/src/models/OrderProduct.php @@ -20,6 +20,15 @@ class OrderProduct private int $quantity; private float $unit_price; + /** + * Create a new OrderProduct object + * @param int $product_id + * @param string $cup_size + * @param string $milk_type + * @param int $quantity + * @param float|null $unit_price If not set, the default $unit_price is -1. + * @param int|null $order_id If not set, the default $order_id is -1. + */ public function __construct( int $product_id, string $cup_size, From 48b2d7c817119ad0e4f1e4607692bb64cf3f9388 Mon Sep 17 00:00:00 2001 From: creme332 <65414576+creme332@users.noreply.github.com> Date: Wed, 22 May 2024 11:50:01 +0400 Subject: [PATCH 05/14] change order of steps when saving order set order id and unit price of each line item before validating line item --- src/models/Order.php | 53 ++++++++++++++++++++++++++++---------------- 1 file changed, 34 insertions(+), 19 deletions(-) diff --git a/src/models/Order.php b/src/models/Order.php index b5008e8..1448a12 100644 --- a/src/models/Order.php +++ b/src/models/Order.php @@ -122,20 +122,8 @@ public function save(): bool $update_stock_stm = $conn->prepare($query); foreach ($this->line_items as $line_item) { - $line_item_errors = $line_item->validate(); - - if (!empty($line_item_errors)) { - // line item contains invalid attributes - $conn->rollBack(); - $conn = null; - - $error_message = "Invalid line item:" . json_encode($line_item->toArray()); - $error_message .= " Errors: " . json_encode($line_item_errors); - - throw new Exception( - $error_message - ); - } + // set order ID of line item + $line_item->setOrderID($new_order_id); // fetch product corresponding to line item $product = Product::getByID($line_item->getProductID()); @@ -147,6 +135,9 @@ public function save(): bool throw new Exception("Product with ID " . $line_item->getProductID() . " does not exist"); } + // set true unit price of line item + $line_item->setUnitPrice($product->getPrice()); + // get stock level for current product $stock_level = $store->getProductStock($product->getProductID()); @@ -154,15 +145,39 @@ public function save(): bool // store does not have enough stock $conn->rollBack(); $conn = null; + + $error_message = <<< EOL + Store with ID $this->store_id has insufficient stock ($stock_level) for the following line item: + Product ID = {$line_item->getProductID()} and quantity = {$line_item->getQuantity()}. + EOL; + + throw new Exception($error_message); + } + + // validate line item + $line_item_errors = $line_item->validate(); + if (!empty($line_item_errors)) { + // line item contains invalid attributes + $conn->rollBack(); + $conn = null; + + $line_item_info = json_encode($line_item->toArray()); + $line_item_errors = json_encode($line_item_errors); + + $error_message = <<< EOL + Invalid line item: + $line_item_info + + Errors: + $line_item_errors + EOL; + throw new Exception( - "Store with ID " . $this->store_id - . " has insufficient stock for product " . $line_item->getProductID() + $error_message ); } - // insert into order_product table - $line_item->setOrderID($new_order_id); - $line_item->setUnitPrice($product->getPrice()); + // insert line item into order_product table $success = $insert_line_item_stm->execute($line_item->toArray()); if (!$success) { From ca30ae9df049cfb3cde4d84d5cff79588f72be0d Mon Sep 17 00:00:00 2001 From: creme332 <65414576+creme332@users.noreply.github.com> Date: Wed, 22 May 2024 12:09:53 +0400 Subject: [PATCH 06/14] update features --- README.md | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 3e6ab01..d16da13 100644 --- a/README.md +++ b/README.md @@ -25,13 +25,16 @@ The code for the admin website is found in a separate repository. ## Main features - MVC pattern -- Dynamic routing +- Semantic URL routing - Email-based password recovery - Email notification on order - Testing with phpUnit - Mobile-responsive website -- Javascript code bundling with backwards compatibility +- Utilizes Webpack for efficient code bundling and compatibility with older browsers. - Product review system with nested comments +- Fuzzy searching on shop page +- Pagination +- SEO optimized - REST API ## Documentation From 60b0f1f9ef99c6326020eddffa63f1eb3a917739 Mon Sep 17 00:00:00 2001 From: creme332 <65414576+creme332@users.noreply.github.com> Date: Wed, 22 May 2024 13:31:45 +0400 Subject: [PATCH 07/14] add phpdoc getSignedInClient --- src/core/Controller.php | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/core/Controller.php b/src/core/Controller.php index a7cb393..f3e0a55 100644 --- a/src/core/Controller.php +++ b/src/core/Controller.php @@ -38,8 +38,13 @@ private function getLibrariesTags(array $required_libraries): string return $script_str; } + /** + * @return Client|null Client account of currently logged-in user. Null if no one is logged in. + */ private function getSignedInClient(): ?Client { + // $_SESSION['user'] was set to the client email on login + // if it is empty, no one is logged in if (empty($_SESSION['user'])) { return null; } From 8c1f85d7db39a527301a204403d0a5860392ddd3 Mon Sep 17 00:00:00 2001 From: creme332 <65414576+creme332@users.noreply.github.com> Date: Wed, 22 May 2024 13:32:14 +0400 Subject: [PATCH 08/14] disable comment submit button for unsigned users --- src/views/Product.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/views/Product.php b/src/views/Product.php index 89873ac..c1fd697 100644 --- a/src/views/Product.php +++ b/src/views/Product.php @@ -258,7 +258,7 @@ class="close" - + From d79c8c09a235bb6929cd8228797c17e0a04603dc Mon Sep 17 00:00:00 2001 From: creme332 <65414576+creme332@users.noreply.github.com> Date: Wed, 22 May 2024 13:32:54 +0400 Subject: [PATCH 09/14] use `getSignedInClient` for simplifcation --- src/controllers/Product.php | 11 ++++------- src/controllers/Profile.php | 2 +- 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/src/controllers/Product.php b/src/controllers/Product.php index d1bcb20..8a2d609 100644 --- a/src/controllers/Product.php +++ b/src/controllers/Product.php @@ -21,7 +21,7 @@ class Product private ?ProductModel $product = null; // product to be displayed private array $view_data; - private ?User $signed_user = null; // currently logged-in user + private ?User $signed_user; // currently logged-in user public function __construct() { @@ -38,13 +38,10 @@ public function __construct() // get product id from URL $product_id = filter_var(Utility::splitURL()[2], FILTER_VALIDATE_INT); - // check if user is logged in - $reviewer_email = $_SESSION['user'] ?? ""; - // get user details - $user_account = Client::getByEmail($reviewer_email); - if (!empty($user_account)) { - $this->signed_user = $user_account; + $this->signed_user = $this->getSignedInClient(); + + if (!empty($this->signed_user)) { $this->view_data["signed_in_user"] = true; } diff --git a/src/controllers/Profile.php b/src/controllers/Profile.php index d64fd06..6bd132b 100644 --- a/src/controllers/Profile.php +++ b/src/controllers/Profile.php @@ -74,7 +74,7 @@ private function handleAccountDeletion(): void */ private function handleUnsignedUsers(): void { - if (!array_key_exists('user', $_SESSION) || !isset($_SESSION['user'])) { + if (empty($this->getSignedInClient())) { Utility::redirect('login'); } } From 21841fd969d5f09203b743760f0d9fec4b1cdf4e Mon Sep 17 00:00:00 2001 From: creme332 <65414576+creme332@users.noreply.github.com> Date: Wed, 22 May 2024 13:44:44 +0400 Subject: [PATCH 10/14] show loading animation on checkout button after form submission --- public/js_original/cart-view.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/public/js_original/cart-view.js b/public/js_original/cart-view.js index dc5cca2..c530d71 100644 --- a/public/js_original/cart-view.js +++ b/public/js_original/cart-view.js @@ -50,6 +50,9 @@ function updateCart(e) { } async function checkout() { + // set loading animation on checkout button to prevent multiple form submissions + document.querySelector("#checkout-btn").setAttribute("aria-busy", "true"); + const myCart = Cart(); const items = myCart.getItems(); @@ -63,6 +66,9 @@ async function checkout() { body: JSON.stringify(data), }); + // stop loading animation + document.querySelector("#checkout-btn").setAttribute("aria-busy", "false"); + if (response.ok) { // Clear cart items from localStorage if checkout is successful myCart.clear(); From 72e17b3611c09556cbe9233dce1ca8a1736a88cd Mon Sep 17 00:00:00 2001 From: creme332 <65414576+creme332@users.noreply.github.com> Date: Wed, 22 May 2024 14:26:37 +0400 Subject: [PATCH 11/14] - add listener to checkout button only if present - remove unused import ModalManager --- public/js_original/cart-view.js | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/public/js_original/cart-view.js b/public/js_original/cart-view.js index c530d71..1b1eb18 100644 --- a/public/js_original/cart-view.js +++ b/public/js_original/cart-view.js @@ -4,7 +4,6 @@ import Cart from "./models/Cart"; import CartItem from "./models/CartItem"; -import ModalManager from "./modal"; function updateCart(e) { const sectionNode = e.target.parentNode.parentNode; @@ -67,12 +66,13 @@ async function checkout() { }); // stop loading animation - document.querySelector("#checkout-btn").setAttribute("aria-busy", "false"); + document.querySelector("#checkout-btn").removeAttribute("aria-busy"); if (response.ok) { // Clear cart items from localStorage if checkout is successful myCart.clear(); - ModalManager("my-modal").openModal(); + + document.querySelector("#my-modal").setAttribute("open", ""); return; } const x = await response.json(); @@ -91,9 +91,12 @@ function initCartPage() { ...document.querySelectorAll("section input[type='number']"), ]; - ModalManager("my-modal").init(); + const checkoutBtn = document.querySelector("#checkout-btn"); - document.querySelector("#checkout-btn").addEventListener("click", checkout); + // if checkout button is present on page (button is absent when cart is empty) + if (checkoutBtn !== null) { + checkoutBtn.addEventListener("click", checkout); + } quantityInputs.forEach((input) => { input.addEventListener("change", updateCart); From 7fc6e370476abf87a87d45ea0edb0b8247429c43 Mon Sep 17 00:00:00 2001 From: creme332 <65414576+creme332@users.noreply.github.com> Date: Wed, 22 May 2024 14:27:27 +0400 Subject: [PATCH 12/14] use a simpler modal when displaying order success message --- src/views/Cart.php | 15 +-------------- 1 file changed, 1 insertion(+), 14 deletions(-) diff --git a/src/views/Cart.php b/src/views/Cart.php index e29a73d..4948ddd 100644 --- a/src/views/Cart.php +++ b/src/views/Cart.php @@ -16,24 +16,11 @@ ?>
- -

Checkout successful! ✨

+

Your order has been successfully placed and an email has been sent to you.