From 30b94e89d773f90a2241a07ecbdc2f44dd247cd5 Mon Sep 17 00:00:00 2001 From: Jeppe Bundgaard Date: Wed, 22 Oct 2025 11:38:35 +0200 Subject: [PATCH] Add PO number support and enhance permissions for order management - Introduced `po` property in `orders_o` for handling Purchase Order (PO) numbers, including API integration for retrieval and validation. - Enhanced customer permissions to allow limited order editing (`po` updates) and attachment downloads for their own orders. - Added new helper methods to `users_o` for attributes like `showPricesOnBookingPage` and `usePONumbers`. - Improved order item listing logic with distinct permissions for customers' own orders and price visibility. - Implemented numeric value casting in filters within `db_object_t`. --- .../helpers/economic_invoice_draft.php | 4 ++ services/nginx/app/objects/orders_o.php | 12 ++++ services/nginx/app/objects/users_o.php | 27 +++++++++ services/nginx/app/routes/orderItemsRoute.php | 17 +++++- services/nginx/app/routes/ordersRoute.php | 57 ++++++++++++++++++- services/nginx/app/traits/db_object_t.php | 5 ++ 6 files changed, 117 insertions(+), 5 deletions(-) diff --git a/services/nginx/app/modules/economic/helpers/economic_invoice_draft.php b/services/nginx/app/modules/economic/helpers/economic_invoice_draft.php index adb517ef..c1156559 100644 --- a/services/nginx/app/modules/economic/helpers/economic_invoice_draft.php +++ b/services/nginx/app/modules/economic/helpers/economic_invoice_draft.php @@ -143,6 +143,10 @@ class economic_invoice_draft $parsed_date = date('d/m/Y H:i', strtotime($order->created_at->value())); // Add the text line to the draft invoice self::addTextLine("[ " . $parsed_date . ' ' . $department_name . ' #' . $order->id . " ]"); + // If there's a PO number, add it to the invoice + if ($order->po->value() !== '') { + self::addTextLine('PO: ' . $order->po->value()); + } // If there's a reference, add it to the invoice if ($order->reference->value() !== '') { self::addTextLine('Reference:'); diff --git a/services/nginx/app/objects/orders_o.php b/services/nginx/app/objects/orders_o.php index 09820c00..5f59f512 100644 --- a/services/nginx/app/objects/orders_o.php +++ b/services/nginx/app/objects/orders_o.php @@ -34,6 +34,7 @@ class orders_o extends db public object_property $booking_id; public object_property $wash_id; // The XL Vask Wash ID, if any public object_property $lane; // The lane used for the order, if any + public object_property $po; // The (optional) PO number, filled by the customer. public object_property $using_hand_held; // Whether the order is being processed using a handheld device /** @@ -88,6 +89,7 @@ class orders_o extends db $this->booking_id = new object_property($this->table, $this->id, 'booking_id', 'int', false); $this->wash_id = new object_property($this->table, $this->id, 'wash_id', 'string', false); $this->lane = new object_property($this->table, $this->id, 'lane', 'string', false); + $this->po = new object_property($this->table, $this->id, 'po', 'string', false); $this->using_hand_held = new object_property($this->table, $this->id, 'using_hand_held', 'bool', false); } @@ -563,6 +565,7 @@ class orders_o extends db 'booking_id' => (int)$this->booking_id->value(), 'wash_id' => $this->wash_id->value(), 'lane' => $this->lane->value(), + 'po' => $this->po->value(), 'closed_at' => (int)$this->invoice_collection_id->value() ? (new collected_order_invoices_o())->select((int)$this->invoice_collection_id->value())->closed_at->value() : null, 'pending_handheld' => $this->isPendingHandheld(), ]; @@ -1334,4 +1337,13 @@ class orders_o extends db return $post_discount; }, $order_items)); } + + /** + * @throws Exception + */ + public function isOwnOrder(int $customer_id): bool + { + self::requireSelected(); + return (int)$this->customer_id->value() === $customer_id; + } } \ No newline at end of file diff --git a/services/nginx/app/objects/users_o.php b/services/nginx/app/objects/users_o.php index 273e2287..1df610c1 100644 --- a/services/nginx/app/objects/users_o.php +++ b/services/nginx/app/objects/users_o.php @@ -460,6 +460,24 @@ class users_o extends db return $this->doesUserHaveAttribute('restrictTankCleaning'); } + /** + * If the customer is shown prices on the booking page + * @return bool + */ + public function showPricesOnBookingPage(): bool + { + return $this->doesUserHaveAttribute('showPricesOnBookingPage'); + } + + /** + * If the customer uses PO numbers + * @return bool + */ + public function usePONumbers(): bool + { + return $this->doesUserHaveAttribute('usePONumbers'); + } + public function includeIncludes(array $includes = []): users_o { global /** @var response $response */ @@ -519,6 +537,15 @@ class users_o extends db while ($row = $result->fetch_assoc()) { $perms[] = $row['permission']; } + // Add the attributes as `has_attribute_key_{attribute_key}` + $attributes = array_map(function ($attribute_arr) { + return $attribute_arr['attribute']; + }, (new users_o())->getUserAttributes($this->id)); + // Remove all duplicates + $attributes = array_unique($attributes, SORT_REGULAR); + foreach ($attributes as $attribute) { + $perms[] = 'has_attribute_' . $attribute; + } $this->permissions = $perms; } diff --git a/services/nginx/app/routes/orderItemsRoute.php b/services/nginx/app/routes/orderItemsRoute.php index ea6041b4..87074bb6 100644 --- a/services/nginx/app/routes/orderItemsRoute.php +++ b/services/nginx/app/routes/orderItemsRoute.php @@ -93,9 +93,18 @@ class orderItemsRoute $this->get('/order/items', function () { // Require the user to be logged in global $response; - $this->requirePermission('list_order_items'); - // Get the user object + // Check if the user is requesting their own order items + $isCustomerAccess = ($this->hasPermission('user') && !($this->hasPermission('list_order_items'))); $user = (new authentication())->get_user(); + if ($isCustomerAccess) { + $hasPermission = $this->hasPermission('list_own_order_items'); + $hasAttribute = $user->showPricesOnBookingPage(); // Check if the user has the attribute to show prices on the booking page + if (!$hasPermission && !$hasAttribute) { + $response->error('You do not have permission to list order items, neither your own nor all orders', 403); + } + } else { + $this->requirePermission('list_order_items'); + } // Check if the request was successful if ($user) { // Get the post data @@ -113,6 +122,10 @@ class orderItemsRoute $response->error('Order not found', 404); } $order = (new orders_o())->getOrderById((int)$data['order_id']); + // If the user is requesting their own order items, check if the order belongs to them + if ($isCustomerAccess && !$order->isOwnOrder((int)$user->customer_number->value())) { + $response->error('Order does not belong to the user', 400); + }; // Apply the departments unique pricing $orderItems = $order->getOrderItems($order->id); $orderItems = $order->applyDepartmentPrices($orderItems, $order->department_id->value()); diff --git a/services/nginx/app/routes/ordersRoute.php b/services/nginx/app/routes/ordersRoute.php index 2e8365bc..7222ba8a 100644 --- a/services/nginx/app/routes/ordersRoute.php +++ b/services/nginx/app/routes/ordersRoute.php @@ -93,6 +93,8 @@ class ordersRoute $order['user_id'] = (int)$tmp_customer->id; $order['pending_handheld'] = $order_obj->isPendingHandheld(); $order['attachments'] = $order_obj->listAttachments(); + $order['po'] = $order['po'] ?? null; + $order['lane'] = $order['lane'] ?? null; /** @var array $order */ return $order; }, @@ -208,7 +210,13 @@ class ordersRoute $this->put('/orders', function () { // Require the user to be logged in global $response; - $this->requirePermission('edit_order'); + // Check if the user has permission to partially edit the order + $isCustomerAccess = ((new authentication())->get_user()->hasPermission('user') && !((new authentication())->get_user()->hasPermission('edit_order'))); + if (!$isCustomerAccess) { + $this->requirePermission('edit_order'); + } else { + $this->requirePermission('user'); // This is used to allow the user to edit their own order + } // Get the user object $user = (new authentication())->get_user(); // Check if the request was successful @@ -225,6 +233,33 @@ class ordersRoute if (!$order->exists()) { $response->error('Order not found', 400); } + // Check if the user has customer edit access + if ($isCustomerAccess) { + if ($order->customer_id->value() !== $user->customer_number->value()) { + $response->error('You do not have permission to edit this order', 400); + } + // Allowed to edit list + $allowed_to_edit = [ + // Include the order ID (Even though it is not editable) + 'id', + 'po', + ]; + // Check if the $data contains any non-allowed keys + foreach ($data as $key => $value) { + if (!in_array($key, $allowed_to_edit)) { + $response->error('You do not have permission to edit this order field (key: ' . $key . ')', 400); + break; + } + }; + // PO + if (isset($data['po'])) { + $order->po->set((string)$data['po']); + } + $order->objectChanged(); + // Return a success message + $response->success($order->asArray()); + } + /** Departmental access */ // If the customer ID is set, validate it if (isset($data['customer_id'])) { if (!(new users_o())->getCustomerByIdOrCustomerNumber((int)$data['customer_id'])->exists() || empty($data['customer_id'])) { @@ -252,6 +287,10 @@ class ordersRoute if (isset($data['reg_3'])) { $order->reg_3->set($data['reg_3']); } + // If the PO is set, validate it + if (isset($data['po'])) { + $order->po->set((string)$data['po']); + } // If the lane is set, validate it if (isset($data['lane'])) { $order->lane->set((int)$data['lane']); @@ -338,7 +377,12 @@ class ordersRoute $this->get('/orders/attachments/download', function () { // Require the user to be logged in global $response; - $this->requirePermission('download_order_attachments'); + $isCustomerAccess = ($this->hasPermission('user') && !($this->hasPermission('download_order_attachments'))); + if (!$isCustomerAccess) { + $this->requirePermission('download_order_attachments'); + } else { + $this->requirePermission('download_order_attachments_own'); + } // Get the user object $user = (new authentication())->get_user(); // Check if the request was successful @@ -362,6 +406,12 @@ class ordersRoute if (!$order->exists()) { $response->error('Order not found', 400); } + // Check if the user has customer edit access + if ($isCustomerAccess) { + if ($order->isOwnOrder($user->customer_number->value()) === false) { + $response->error('You do not have permission to download this order', 400); + } + } // Get the attachment $attachment = $order->getAttachment((int)$attachment_id); if (!$attachment->exists()) { @@ -387,7 +437,8 @@ class ordersRoute } }, [ - 'download_order_attachments' => 'Download attachments for an order' + 'download_order_attachments' => 'Download attachments for an order', + 'download_order_attachments_own' => 'Download attachments for an order (Only for own orders)' ] ); diff --git a/services/nginx/app/traits/db_object_t.php b/services/nginx/app/traits/db_object_t.php index 7b513707..856da673 100644 --- a/services/nginx/app/traits/db_object_t.php +++ b/services/nginx/app/traits/db_object_t.php @@ -728,6 +728,11 @@ trait db_object_t } continue; } + // If the values can be changed to integers, convert them to integers + if (is_numeric($value) && is_numeric($filters[$field])) { + $value = (int)$value; + $filters[$field] = (int)$filters[$field]; + } // Check if the user tries to search for a different value than the one provided if ($filters[$field] !== $value) { throw new Exception('Invalid filter: ' . $field . ' - ' . $filters[$field] . '. Permitted value: ' . $value);