Introduce goals module with methods for result, percentage, and remaining calculations

- Add `goals` class for managing goal metrics and calculations.
- Extend `goals_criteria` with timeframe initialization and validation.
- Implement `getListByCriteria` in `order_items_o.php` for product-based goal evaluation.
- Add unit tests for `goals` to validate
This commit is contained in:
Jeppe Bundgaard
2026-01-26 16:24:23 +01:00
parent f94efa7c59
commit 51b8198a67
6 changed files with 181 additions and 6 deletions
@@ -0,0 +1,62 @@
<?php
namespace goals\classes;
use goals\helpers\goals_criteria_type;
/**
* Class goals
* Main class for the goals module.
*/
class goals
{
/**
* The goals criteria
* @var goals_criteria $criteria
*/
public goals_criteria $criteria;
/**
* Constructor
* @param goals_criteria|null $criteria
*/
public function __construct(?goals_criteria $criteria = null)
{
$this->criteria = $criteria ?? new goals_criteria();
}
/**
* Get the current result for the goal based on the criteria.
* @return int|float
*/
public function getResult(): int|float
{
return match ($this->criteria->type) {
goals_criteria_type::PRODUCT => $this->criteria->products->count($this->criteria),
default => 0,
};
}
/**
* Get the percentage completion of the goal.
* @return float
*/
public function getPercentage(): float
{
if ($this->criteria->target <= 0) {
return 0.0;
}
return ($this->getResult() / $this->criteria->target) * 100;
}
/**
* Get the remaining value to reach the goal target.
* @return int|float
*/
public function getRemaining(): int|float
{
$remaining = $this->criteria->target - $this->getResult();
return max(0, $remaining);
}
}
@@ -41,6 +41,8 @@ class goals_criteria implements goals_criteria_i
$this->users = new goals_criteria_users();
$this->departments = new goals_criteria_departments();
$this->products = new goals_criteria_products();
$this->start = new \DateTime();
$this->end = new \DateTime();
}
}
@@ -18,17 +18,19 @@ class goals_criteria_products implements goals_criteria_products_i
public function count(goals_criteria $goal): int
{
$count = 0;
$datetime_start = $goal->start ?? new \DateTime();
$datetime_end = $goal->end ?? new \DateTime();
$items = order_items_o::getListByCriteria(
department_ids: $goal->departments->listIDs(),
product_ids: $this->listIDs(),
customer_numbers: $goal->users->listCustomerNumbers(),
datetime_start: $goal->start,
datetime_end: $goal->end,
datetime_start: $datetime_start,
datetime_end: $datetime_end,
);
foreach ($items as $item) {
$count += $item->quantity;
$count += (int)$item->quantity->value();
}
return $count;
}
@@ -278,4 +278,65 @@ class order_items_o extends db
$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;
}
}
+3 -2
View File
@@ -4,6 +4,8 @@ namespace routes;
use classes\entra;
use classes\redis;
use goals\classes\goals;
use goals\classes\goals_criteria;
use objects\departments_o;
use traits\route_t;
@@ -49,8 +51,7 @@ class exampleRoute
$this->get('/debug', function () {
global $response;
$entra = new entra();
$GraphClient = $entra->getGraphClient();
$goal = (new goals());
$response->success(['message' => 'Debugging route!']);
});
}
@@ -0,0 +1,47 @@
<?php
namespace tests\goalsModule;
use goals\classes\goals;
use goals\classes\goals_criteria;
use goals\helpers\goals_criteria_type;
use PHPUnit\Framework\TestCase;
class goalsTest extends TestCase
{
public function testGoalsInitialization()
{
$goal = new goals();
$this->assertInstanceOf(goals::class, $goal);
$this->assertInstanceOf(goals_criteria::class, $goal->criteria);
}
public function testGetResultDefault()
{
$goal = new goals();
$this->assertEquals(0, $goal->getResult());
}
public function testGetPercentage()
{
$goal = new goals();
$goal->criteria->setTarget(100);
// Mocking getResult is hard without dependency injection or better structure,
// but since we know it returns 0 by default:
$this->assertEquals(0.0, $goal->getPercentage());
// Manually setting a result isn't possible as getResult() calculates it.
// But we can check if it handles target 0 correctly.
$goal->criteria->setTarget(0);
$this->assertEquals(0.0, $goal->getPercentage());
}
public function testGetRemaining()
{
$goal = new goals();
$goal->criteria->setTarget(50);
// Result is 0, so remaining should be 50
$this->assertEquals(50, $goal->getRemaining());
}
}