Files
api/services/nginx/app/objects/product_options_o.php
T
Jeppe Bundgaard d58fa69560 Refactor addon sales calculation logic and improve date handling in reports
- Standardize date formatting to cover full days in queries (`00:00:00` to `23:59:59`).
- Replace direct SQL queries with reusable department methods for calculating total addons and max addons.
- Update Slack notifications to include percentage breakdown with sold/total for each department.
- Remove redundant code and improve error handling for department data retrieval.
2026-01-22 10:19:06 +01:00

293 lines
9.8 KiB
PHP

<?php
namespace objects;
use classes\db;
use classes\object_property;
use Exception;
use traits\db_object_t;
class product_options_o extends db
{
use db_object_t;
public object_property $product_id;
public object_property $option_id;
public object_property $name;
public object_property $min;
public object_property $max;
public static function getPDO(): \PDO
{
return db::getPDO();
}
private static function fetchRow(string $query, array $params): array
{
$stmt = self::getPDO()->prepare($query);
$stmt->execute($params);
$result = $stmt->fetch(\PDO::FETCH_ASSOC);
return $result ?: [];
}
public function structure(): void
{
$this->setTable('products_options');
}
/**
* Add a product option, and set this object to the new object
* @param int $product_id
* @param int $option_id
* @return void
* @throws Exception If the object was not created successfully
*/
public function add(int $product_id, int $option_id): void
{
$tmp_id = self::add_object([
'product_id' => $product_id,
'option_id' => $option_id
]);
$this->id = $tmp_id;
self::getObjectProperties();
self::objectChanged();
}
public function getObjectProperties(): void
{
$this->product_id = new object_property($this->table, $this->id, 'product_id', 'int', false);
$this->option_id = new object_property($this->table, $this->id, 'option_id', 'int', false);
$this->name = new object_property($this->table, $this->id, 'name', 'string', false);
$this->min = new object_property($this->table, $this->id, 'min', 'int', false);
$this->max = new object_property($this->table, $this->id, 'max', 'int', false);
}
public function objectChanged(): void
{
//TODO: Add cache invalidation
}
/**
* There's no need to keep track of when a product option is deleted,
* so we just forcefully delete it.
* @throws Exception If the object was not selected
* @throws Exception If the object was not deleted successfully
*/
public function delete(): void
{
self::requireSelected();
self::deletePermanently();
}
/**
* Set the name of the product option
* @param string $name
* @throws Exception If the object was not selected
*/
public function set_name(string $name): void
{
self::requireSelected();
// If the string is empty, set it to null
if (empty($name)) {
$this->name->nullify();
self::objectChanged();
return;
}
$this->name->set($name);
self::objectChanged();
}
/**
* Set the min value of the product option
* @param int|null $min
* @notation If the int is less than 1 or empty, set it to null
* @throws Exception If the object was not selected
*/
public function set_min(int|null $min): void
{
self::requireSelected();
// If the int is empty, set it to null
if (empty($min) || $min < 1) {
$this->min->nullify();
self::objectChanged();
return;
}
$this->min->set($min);
self::objectChanged();
}
/**
* Set the max value of the product option
* @param int|null $max
* @notation If the int is less than 1, or empty, it will be set to null
* @throws Exception If the object was not selected
*/
public function set_max(int|null $max): void
{
self::requireSelected();
// If the int is empty, set it to null
if (empty($max) || $max < 1) {
$this->max->nullify();
self::objectChanged();
return;
}
$this->max->set($max);
self::objectChanged();
}
/**
* Get all options for a product
* @param int $id The id of the product
* @return array
* @throws Exception If the object was not selected
*/
public function getProductOptions(int $id): array
{
$options = self::getFieldsWhere(['product_id' => $id],
[
'id',
'product_id',
'option_id',
'name',
'min',
'max',
'created_at',
'updated_at'
]
);
$tmp = [];
foreach ( $options as $option ) {
$tmp_product = (new products_o())->select($option['option_id'])->asArray();
$tmp[] = [
'id' => (int)$option['id'],
'product_id' => (int)$option['product_id'],
'option_id' => (int)$option['option_id'],
'name' => (string)$option['name'] === '' ? $tmp_product['name'] : (string)$option['name'],
'min' => $option['min'] === null ? 0 : (int)$option['min'],
'max' => $option['max'] === null ? -1 : (int)$option['max'],
'created_at' => (string)$option['created_at'],
'updated_at' => (string)$option['updated_at'],
'price' => (int)$tmp_product['price'],
'product' => $tmp_product
];
}
return $tmp;
}
/**
* Get the product option as an array
* @return array
* @throws Exception If the object was not selected
*/
public function asArray(): array
{
self::requireSelected();
return [
'id' => (int)$this->id,
'product_id' => (int)$this->product_id->value(),
'option_id' => (int)$this->option_id->value(),
'name' => (string)$this->name->value(),
'min' => $this->min->value() === null ? null : (int)$this->min->value(),
'max' => $this->max->value() === null ? null : (int)$this->max->value()
];
}
/**
* Get all products with a specific option
* @param int $product_id The id of the addon product
* @return array
*/
public function getOptionProducts(int $product_id): array
{
return self::getFieldsWhere(['option_id' => $product_id], ['product_id']);
}
public function isOptionAllowedOnType(int $option_id, int $primary_type_id): bool
{
// Check if the option is allowed on the primary type
$allowed_options = self::getFieldsWhere(
['option_id' => $option_id, 'product_id' => $primary_type_id],
['id']
);
return !empty($allowed_options);
}
public function countSoldAddonsInDateRange(int $product_id, string $date_start, string $date_end, int $department_id): int
{
global /** @var db $db */
$db;
/**
* Count the number of sold addons (order items that's related to any another product)
* The order they were sold in must:
* - Be within the date range
* - Belong to the specified department
* - Have deleted_at IS NULL
* The item must:
* - Be an addon (related_item_id IS NOT NULL)
* - Not be deleted (deleted_at IS NULL)
*/
// Sanitize inputs
$product_id = (int)$product_id;
$date_start = date('Y-m-d 00:00:00', strtotime($date_start));
$date_end = date('Y-m-d 23:59:59', strtotime($date_end));
$department_id = (int)$department_id;
$query = "
SELECT SUM(oi.quantity) AS sold_addons_count
FROM order_items oi
INNER JOIN orders o ON oi.order_id = o.id
AND oi.product_id = {$product_id}
AND oi.deleted_at IS NULL
AND o.department_id = {$department_id}
AND o.created_at BETWEEN '{$date_start}' AND '{$date_end}'
AND o.deleted_at IS NULL
";
return (int)$db->query($query)->fetch_object()->sold_addons_count;
}
public function getMaxAddonsInDateRange(int $product_id, string $date_start, string $date_end, int $department_id): int
{
/**
* Get the number of addons that could have been sold (maximum)
* Every order item that has the product_id as an option should be counted (number of orders with that order item * the product_option max ?? 0)
* The order they were sold in must:
* - Be within the date range
* - Belong to the specified department
* - Have deleted_at IS NULL
*/
global /** @var db $db */
$db;
// Sanitize inputs
$product_id = (int)$product_id;
$date_start = date('Y-m-d 00:00:00', strtotime($date_start));
$date_end = date('Y-m-d 23:59:59', strtotime($date_end));
$department_id = (int)$department_id;
// Get all products that can have this addon
$applicable_products = $this->getOptionProducts($product_id);
if (empty($applicable_products)) {
return 0;
}
$applicable_product_ids = array_column($applicable_products, 'product_id');
$applicable_product_ids_str = implode(',', $applicable_product_ids);
// Build the query (oi.quantity * (po.max ? (po.max >= 1 ? po.max : 1) : 1) )
$query = "
SELECT SUM(oi.quantity * (CASE
WHEN po.max IS NULL THEN 1
WHEN po.max < 1 THEN 1
ELSE po.max
END)) AS max_addons_count
FROM order_items oi
INNER JOIN orders o ON oi.order_id = o.id
INNER JOIN products_options po ON oi.product_id = po.product_id
WHERE oi.product_id IN ({$applicable_product_ids_str})
AND po.option_id = {$product_id}
AND oi.deleted_at IS NULL
AND o.department_id = {$department_id}
AND o.created_at BETWEEN '{$date_start}' AND '{$date_end}'
AND o.deleted_at IS NULL
";
return (int)$db->query($query)->fetch_object()->max_addons_count;
}
}