fix(api): do not raise historical_primary_product_mismatch for single-tractor orders (#363)
Partition primary rows by reg_2 presence in getPrimaryProductHistory() so a single-tractor order (reg_2 empty) is compared only against other single-tractor orders, not against historical tractor-trailer orders. 2 new tests + 1 updated signature contract test in InvoicePeriodFlagServiceTest.
This commit is contained in:
@@ -1297,7 +1297,34 @@ class invoice_period_flag_service
|
||||
}
|
||||
}
|
||||
|
||||
$history = $this->getPrimaryProductHistory($dateFrom, array_column($primaryRows, 'reg_1'));
|
||||
// Partition primary rows by whether reg_2 is empty. A single-tractor order (reg_2 = '')
|
||||
// should only be compared against historical orders that ALSO had reg_2 empty, so we don't
|
||||
// raise a "historical_primary_product_mismatch" flag that names the tractor-trailer
|
||||
// (Forvogn med hænger) product as the expected one when the current order has no trailer.
|
||||
$hasReg2 = [];
|
||||
$emptyReg2 = [];
|
||||
foreach ($primaryRows as $row) {
|
||||
if (trim((string)($row['reg_2'] ?? '')) === '') {
|
||||
$emptyReg2[] = $row;
|
||||
} else {
|
||||
$hasReg2[] = $row;
|
||||
}
|
||||
}
|
||||
$history = [];
|
||||
if (!empty($emptyReg2)) {
|
||||
$history = $history + $this->getPrimaryProductHistory(
|
||||
$dateFrom,
|
||||
array_column($emptyReg2, 'reg_1'),
|
||||
true
|
||||
);
|
||||
}
|
||||
if (!empty($hasReg2)) {
|
||||
$history = $history + $this->getPrimaryProductHistory(
|
||||
$dateFrom,
|
||||
array_column($hasReg2, 'reg_1'),
|
||||
false
|
||||
);
|
||||
}
|
||||
foreach ($primaryRows as $row) {
|
||||
$reg = strtoupper(trim((string)($row['reg_1'] ?? '')));
|
||||
if ($reg === '' || !isset($history[$reg])) {
|
||||
@@ -1774,7 +1801,7 @@ class invoice_period_flag_service
|
||||
return $map;
|
||||
}
|
||||
|
||||
private function getPrimaryProductHistory(string $dateFrom, array $registrationNumbers): array
|
||||
private function getPrimaryProductHistory(string $dateFrom, array $registrationNumbers, ?bool $requireReg2Empty = null): array
|
||||
{
|
||||
global $db;
|
||||
|
||||
@@ -1795,6 +1822,16 @@ class invoice_period_flag_service
|
||||
$registrationFilter = implode(',', array_map(static function (string $registrationNumber) use ($db): string {
|
||||
return "'" . $db->escape_string($registrationNumber) . "'";
|
||||
}, array_keys($registrations)));
|
||||
// Restrict historical orders to those whose reg_2 status matches the current rows:
|
||||
// - null → no filter (default behaviour, backwards compatible)
|
||||
// - true → reg_2 empty (single-tractor orders only)
|
||||
// - false → reg_2 non-empty (tractor-trailer combo orders only)
|
||||
$reg2Filter = '';
|
||||
if ($requireReg2Empty === true) {
|
||||
$reg2Filter = "AND (COALESCE(o.reg_2, '') = '')";
|
||||
} elseif ($requireReg2Empty === false) {
|
||||
$reg2Filter = "AND (COALESCE(o.reg_2, '') <> '')";
|
||||
}
|
||||
$result = $db->query(
|
||||
"SELECT UPPER(TRIM(o.reg_1)) AS reg, oi.product_id, p.name AS product_name, COUNT(*) AS usage_count
|
||||
FROM orders o
|
||||
@@ -1808,6 +1845,7 @@ class invoice_period_flag_service
|
||||
AND COALESCE(oi.related_item_id, 0) = 0
|
||||
AND COALESCE(o.reg_1, '') <> ''
|
||||
AND o.reg_1 IN ({$registrationFilter})
|
||||
{$reg2Filter}
|
||||
GROUP BY UPPER(TRIM(o.reg_1)), oi.product_id, p.name
|
||||
ORDER BY reg, usage_count DESC, oi.product_id ASC"
|
||||
);
|
||||
|
||||
@@ -1166,8 +1166,151 @@ it('limits historical primary product lookup to current period registrations', f
|
||||
expect($content)->not->toBeFalse();
|
||||
$content = (string)$content;
|
||||
|
||||
expect($content)->toContain('getPrimaryProductHistory($dateFrom, array_column($primaryRows, \'reg_1\'))');
|
||||
expect($content)->toContain('private function getPrimaryProductHistory(string $dateFrom, array $registrationNumbers): array');
|
||||
expect($content)->toContain('private function getPrimaryProductHistory(string $dateFrom, array $registrationNumbers, ?bool $requireReg2Empty = null): array');
|
||||
expect($content)->toContain('AND o.reg_1 IN ({$registrationFilter})');
|
||||
expect($content)->not->toContain('$byReg = []');
|
||||
});
|
||||
|
||||
it('does not flag single-tractor orders against historical tractor-trailer products', function (): void {
|
||||
global $db;
|
||||
|
||||
$hadDb = array_key_exists('db', $GLOBALS);
|
||||
$previousDb = $GLOBALS['db'] ?? null;
|
||||
$db = new class {
|
||||
public function escape_string(string $value): string
|
||||
{
|
||||
return addslashes($value);
|
||||
}
|
||||
|
||||
public function query(string $sql): object|false
|
||||
{
|
||||
if (str_contains($sql, 'SHOW COLUMNS FROM `customer_vehicles`')) {
|
||||
return $this->result([]);
|
||||
}
|
||||
|
||||
if (str_contains($sql, 'FROM customer_vehicles')) {
|
||||
return $this->result([]);
|
||||
}
|
||||
|
||||
// History lookup that matches the reg_2-empty filter (single-tractor history)
|
||||
if (str_contains($sql, 'FROM orders o')
|
||||
&& str_contains($sql, "COALESCE(o.reg_2, '') = ''")) {
|
||||
return $this->result([
|
||||
[
|
||||
'reg' => 'EC21233',
|
||||
'product_id' => 3,
|
||||
'product_name' => 'Forvogn',
|
||||
'usage_count' => 8,
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
// History lookup that matches the reg_2-non-empty filter (tractor-trailer history)
|
||||
if (str_contains($sql, 'FROM orders o')
|
||||
&& str_contains($sql, "COALESCE(o.reg_2, '') <> ''")) {
|
||||
return $this->result([
|
||||
[
|
||||
'reg' => 'EC21233',
|
||||
'product_id' => 7,
|
||||
'product_name' => 'Forvogn med hænger',
|
||||
'usage_count' => 12,
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private function result(array $rows): object
|
||||
{
|
||||
return new class($rows) {
|
||||
public int $num_rows;
|
||||
public array $rows;
|
||||
|
||||
public function __construct(array $rows)
|
||||
{
|
||||
$this->rows = $rows;
|
||||
$this->num_rows = count($rows);
|
||||
}
|
||||
|
||||
public function fetch_assoc(): ?array
|
||||
{
|
||||
return array_shift($this->rows);
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
try {
|
||||
$baseRow = [
|
||||
'customer_number' => 35131752,
|
||||
'customer_name' => 'Single Tractor Customer',
|
||||
'order_id' => 7001,
|
||||
'order_item_id' => 8001,
|
||||
'invoice_collection_id' => 901,
|
||||
'department_id' => 7,
|
||||
'reg_1' => 'EC21233',
|
||||
'reg_2' => '',
|
||||
'is_wash' => 1,
|
||||
'related_item_id' => 0,
|
||||
'order_created_at' => '2026-08-01 08:05:21',
|
||||
];
|
||||
|
||||
// Single-tractor order (reg_2 = '') whose primary product is just "Forvogn" should NOT be
|
||||
// flagged, even though historical tractor-trailer orders (reg_2 non-empty) have used the
|
||||
// "Forvogn med hænger" product for the same registration.
|
||||
$singleTractorFlags = invoice_period_flag_service_invoke('detectVehicleTypeMismatches', [
|
||||
[
|
||||
$baseRow + [
|
||||
'product_id' => 3,
|
||||
'product_name' => 'Forvogn',
|
||||
],
|
||||
],
|
||||
'2026-08-01 00:00:00',
|
||||
]);
|
||||
|
||||
expect(array_column($singleTractorFlags, 'definition_key'))->not->toContain('historical_primary_product_mismatch');
|
||||
|
||||
// Tractor-trailer order (reg_2 non-empty) using a non-matching product SHOULD still be flagged.
|
||||
$trailerFlags = invoice_period_flag_service_invoke('detectVehicleTypeMismatches', [
|
||||
[
|
||||
[
|
||||
'product_id' => 5,
|
||||
'product_name' => 'Kassevogn',
|
||||
'reg_2' => 'AB12345',
|
||||
] + $baseRow,
|
||||
],
|
||||
'2026-08-01 00:00:00',
|
||||
]);
|
||||
|
||||
$mismatchFlags = array_values(array_filter(
|
||||
$trailerFlags,
|
||||
static fn(array $flag): bool => ($flag['definition_key'] ?? null) === 'historical_primary_product_mismatch'
|
||||
));
|
||||
expect(count($mismatchFlags))->toBe(1);
|
||||
expect($mismatchFlags[0]['message_params']['expected_product'] ?? null)->toBe('Forvogn med hænger');
|
||||
} finally {
|
||||
if ($hadDb) {
|
||||
$db = $previousDb;
|
||||
} else {
|
||||
unset($GLOBALS['db']);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it('partitions historical primary product lookup by current rows reg_2 presence', function (): void {
|
||||
$content = file_get_contents(app_path('classes/invoice_period_flag_service.php'));
|
||||
|
||||
expect($content)->not->toBeFalse();
|
||||
$content = (string)$content;
|
||||
|
||||
// The detect() function must call getPrimaryProductHistory twice: once with requireReg2Empty=true
|
||||
// for rows whose reg_2 is empty, and once with requireReg2Empty=false for rows that do have a
|
||||
// trailer. This prevents the historical_primary_product_mismatch flag from naming the
|
||||
// tractor-trailer (Forvogn med hænger) product as the expected product when the current order
|
||||
// has no trailer.
|
||||
expect($content)->toContain("getPrimaryProductHistory(\n \$dateFrom,\n array_column(\$emptyReg2, 'reg_1'),\n true\n )");
|
||||
expect($content)->toContain("getPrimaryProductHistory(\n \$dateFrom,\n array_column(\$hasReg2, 'reg_1'),\n false\n )");
|
||||
expect($content)->toContain("\$reg2Filter = \"AND (COALESCE(o.reg_2, '') = '')\"");
|
||||
expect($content)->toContain("\$reg2Filter = \"AND (COALESCE(o.reg_2, '') <> '')\"");
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user