setTable('order_items'); } public function getOrderItemById(int $id): order_items_o { global $db; // Get the record from the database $sql = "SELECT * FROM $this->table WHERE id = $id"; $result = $db->query($sql); if ($result->num_rows > 0) { $this->id = $id; $this->getObjectProperties(); } return $this; } public function getObjectProperties(): void { $this->order_id = new object_property($this->table, $this->id, 'order_id', 'int', true); $this->product_id = new object_property($this->table, $this->id, 'product_id', 'int', true); $this->reference = new object_property($this->table, $this->id, 'reference', 'string', true); $this->notes = new object_property($this->table, $this->id, 'notes', 'string', false); $this->cashier_id = new object_property($this->table, $this->id, 'cashier_id', 'int', true); $this->price = new object_property($this->table, $this->id, 'price', 'int', true); $this->quantity = new object_property($this->table, $this->id, 'quantity', 'int', true); $this->related_item_id = new object_property($this->table, $this->id, 'related_item_id', 'int', false); $this->include_in_invoice = new object_property($this->table, $this->id, 'include_in_invoice', 'bool', false); } private static function validatedRelatedItemId(int $orderId, mixed $relatedItemId): ?int { if ($relatedItemId === null || $relatedItemId === '' || (int)$relatedItemId === 0) { return null; } $normalizedRelatedItemId = (int)$relatedItemId; if ($normalizedRelatedItemId < 1) { throw new RuntimeException('Related item ID must be a positive integer'); } global $db; $result = $db->query( "SELECT order_id FROM order_items WHERE id = {$normalizedRelatedItemId} AND deleted_at IS NULL LIMIT 1" ); if (!$result || $result->num_rows < 1) { throw new RuntimeException('Related order item not found'); } $row = $result->fetch_assoc(); if ((int)($row['order_id'] ?? 0) !== $orderId) { throw new RuntimeException('Related order item must belong to the same order'); } return $normalizedRelatedItemId; } public function add(int $order_id, int $product_id, string $reference, string $notes, int $cashier_id, int $price, int $quantity, $related_item_id = null): void { global $db, $response; try { $related_item_id = self::validatedRelatedItemId($order_id, $related_item_id); customer_order_product_policy::assertOrderAllowsProduct($order_id, $product_id); // Avoid SQL injection $reference = $db->escape_string($reference); $notes = $db->escape_string($notes); // Create a new record in the database $sql = "INSERT INTO $this->table (order_id, product_id, reference, notes, cashier_id, price, quantity) VALUES ($order_id, $product_id, '$reference', '$notes', $cashier_id, $price, $quantity)"; $db->query($sql); // Get the id of the new record $this->id = $db->insert_id(); // Set the values of the object properties $this->getObjectProperties(); // Set the related item id, if it is set if ($related_item_id) { $this->related_item_id->set($related_item_id); } // Inform the order object that a new item has been added $this->getOrder()->objectChanged(); } catch (Exception $e) { $response->error($e->getMessage()); } } /** * @throws Exception */ public function objectChanged(): void { // This method is called when the object is changed, to inform the order object // that an item has been added, edited or removed. // This is used to update the order total and other related properties. $order = $this->getOrder(); $order->objectChanged(); } /** * Get the order object associated with this order item * @return orders_o The order object associated with this order item * @throws Exception If the order item is not selected * @throws Exception If no order is selected */ public function getOrder(): orders_o { self::requireSelected(); return (new orders_o())->select((int)$this->order_id->value()); } public function edit(int $id, int $order_id, int $product_id, string $reference, string $notes, int $cashier_id, int $price, int $quantity): void { global $db, $response; $this->id = $id; try { // Avoid SQL injection $reference = $db->escape_string($reference); $notes = $db->escape_string($notes); // Update the record in the database $sql = "UPDATE $this->table SET order_id = $order_id, product_id = $product_id, reference = '$reference', notes = '$notes', cashier_id = $cashier_id, price = $price, quantity = $quantity WHERE id = $this->id"; $db->query($sql); // Set the values of the object properties $this->getObjectProperties(); // Inform the order object that an item has been edited $this->objectChanged(); } catch (Exception $e) { $response->error($e->getMessage()); } } public function addItemToOrder(int $order_id, int $product_id, int $cashier_id, int $quantity, $related_item_id = null, $notes = null, $forcePrice = null): order_items_o { global $db, $response; try { // Get the order $order = (new orders_o())->getOrderById($order_id); $related_item_id = self::validatedRelatedItemId($order_id, $related_item_id); customer_order_product_policy::assertOrderAllowsProduct($order_id, $product_id); // Get the product price $product = (new products_o())->getProductById($product_id); $priceResolution = $product->getDepartmentPriceResolution((int)$order->department_id->value()); $price = $priceResolution['price']; // Check if the user has a discount on the product, or category $customer = (new orders_o())->getOrderCustomer($order_id); if (!products_o::priceResolutionIsCustomMissing($priceResolution)) { $price = $customer->applyProductCustomerPricing($product_id, (int)$price, false, (int)$order->department_id->value()); } // If the price is forced, set the price to the forced price if ($forcePrice !== null) { $price = (int)$forcePrice; } // Create a new record in the database $sql = "INSERT INTO $this->table (order_id, product_id, price, cashier_id, quantity) VALUES ($order_id, $product_id, $price, $cashier_id, $quantity)"; $db->query($sql); // Get the id of the new record $this->id = $db->insert_id(); // Set the values of the object properties $this->getObjectProperties(); // Set the related item id, if it is set if ($related_item_id) { $this->related_item_id->set($related_item_id); } // Set the notes, if it is set if ($notes) { $this->notes->set($notes); } // Invalidate the order cache $order->objectChanged(); return (new order_items_o())->select($this->id); } catch (Exception $e) { $response->error($e->getMessage()); } } /** * @throws Exception */ public function removeOrderItem(int $id): void { // TODO: Implement delete() method instead global $db; $this->id = $id; $this->select($this->id); // Inform the order object that an item has been removed $this->getOrder()->objectChanged(); $sql = "DELETE FROM $this->table WHERE id = $this->id or related_item_id = $this->id"; $db->query($sql); } public function getItemAsArray(): array { return [ 'id' => (int)$this->id, 'order_id' => (int)$this->order_id->value(), 'product_id' => (int)$this->product_id->value(), 'reference' => (string)$this->reference->value(), 'notes' => (string)$this->notes->value(), 'cashier_id' => (int)$this->cashier_id->value(), 'price' => (int)$this->price->value(), 'quantity' => (int)$this->quantity->value(), 'related_item_id' => (int)$this->related_item_id->value(), 'product' => (array)(new products_o())->getProductById($this->product_id->value())->asArray(), 'cashier' => (array)(new users_o())->getUserById($this->cashier_id->value())->asArray(), 'include_in_invoice' => (bool)$this->include_in_invoice->value(), ]; } public function getAllItemsAsArray(int $orderId, ?array $onlySpecificColumns = null): array { global $db; // Get all items for the order if ($onlySpecificColumns) { $columns = implode(', ', $onlySpecificColumns); } else { $columns = '*'; } $sql = "SELECT $columns FROM $this->table WHERE order_id = $orderId AND deleted_at IS NULL"; $result = $db->query($sql); // Circumvent the repeated instantiation of the object, by just selecting the fields $items = []; if ($result->num_rows > 0) { // Fetch all rows return $result->fetch_all(MYSQLI_ASSOC); } return $items; } /** * @throws Exception */ public function updateOrderItem(int $id, int $price, string $notes, string $reference, int $quantity): void { global $db, $response; $this->select((int)$id); $this->requireSelected(); try { // Avoid SQL injection $price = $db->escape_string($price); $notes = $db->escape_string($notes); $reference = $db->escape_string($reference); // Update the record in the database $this->price->set((int)$price); $this->notes->set($notes); $this->reference->set($reference); $this->quantity->set($quantity); // Set the values of the object properties $this->getObjectProperties(); $this->objectChanged(); } catch (Exception $e) { $response->error($e->getMessage()); } } /** * Get the list of order items based on the criteria. * @param int[] $department_ids * @param int[] $product_ids * @param int[] $customer_numbers * @param \DateTime|null $datetime_start * @param \DateTime|null $datetime_end * @return order_items_o[] */ public static function getListByCriteria( array $department_ids = [], array $product_ids = [], array $customer_numbers = [], \DateTime $datetime_start = null, \DateTime $datetime_end = null ): array { global $db; $sql = "SELECT oi.* FROM order_items oi JOIN orders o ON oi.order_id = o.id WHERE oi.deleted_at IS NULL AND o.deleted_at IS NULL"; if (!empty($department_ids)) { $sql .= " AND o.department_id IN (" . implode(',', array_map('intval', $department_ids)) . ")"; } if (!empty($product_ids)) { $sql .= " AND oi.product_id IN (" . implode(',', array_map('intval', $product_ids)) . ")"; } if (!empty($customer_numbers)) { // customer_id in orders table matches customer_number in users table (at least it is used this way in many places) $sql .= " AND o.customer_id IN (" . implode(',', array_map('intval', $customer_numbers)) . ")"; } if ($datetime_start) { $sql .= " AND o.created_at >= '" . $datetime_start->format('Y-m-d H:i:s') . "'"; } if ($datetime_end) { $sql .= " AND o.created_at <= '" . $datetime_end->format('Y-m-d H:i:s') . "'"; } $result = $db->query($sql); $items = []; if ($result && $result->num_rows > 0) { while ($row = $result->fetch_assoc()) { $item = new order_items_o(); $item->id = (int)$row['id']; // We don't call getObjectProperties() here to avoid many SQL queries, // but the count() method in goals_criteria_products only uses $item->quantity // Let's check if we can populate quantity directly. // Actually, goals_criteria_products uses $item->quantity. // In this codebase, properties are often object_property objects. $item->getObjectProperties(); $items[] = $item; } } return $items; } /** * @throws Exception */ public function getProduct(): products_o { return (new products_o())->select((int)$this->product_id->value()); } }