Add unit tests for InvoicingPeriodDraftOverlay and reference suggestion logic, including fake DB integration and aggregation methods
- Implemented `InvoicingPeriodDraftOverlayTest` with coverage for blocking and permitting invoicing actions based on draft states, transactions, and metadata. - Created `ReferenceSuggestionsApiTest` to validate ranked and filtered suggestions across bookings, orders, and vehicles with varied match relevance, context, and frequency. - Added `order_reference_suggestions_service` class, including query methods, normalization utilities, and aggregation logic for reference suggestions. - Enhanced query handling in `InvoicingPeriodDraftOverlayFakeDb` to validate SQL constraints and column cache resets in overlapping invoicing contexts.
This commit is contained in:
@@ -154,6 +154,132 @@ class economic_transfer_queue
|
||||
return max(0, (int)$row['total']);
|
||||
}
|
||||
|
||||
public function listMonitorJobsForUser(int $user_id, int $limit = 50, ?string $transfer_type = null): array
|
||||
{
|
||||
global $db;
|
||||
|
||||
$user_id = max(0, $user_id);
|
||||
$limit = max(1, min(100, $limit));
|
||||
try {
|
||||
$normalized_transfer_type = $transfer_type !== null && trim($transfer_type) !== ''
|
||||
? $this->validateTransferType($transfer_type)
|
||||
: null;
|
||||
} catch (Exception) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$transfer_condition = '';
|
||||
if ($normalized_transfer_type !== null) {
|
||||
$transfer_condition = "AND q.transfer_type = '" . $db->escape_string($normalized_transfer_type) . "'";
|
||||
}
|
||||
|
||||
$sql = "SELECT q.*
|
||||
FROM economic_transfer_queue_jobs q
|
||||
LEFT JOIN economic_transfer_queue_job_dismissals d
|
||||
ON d.queue_job_id = q.id
|
||||
AND d.user_id = $user_id
|
||||
AND d.dismissed_status = q.status
|
||||
WHERE 1 = 1
|
||||
$transfer_condition
|
||||
AND (
|
||||
q.status IN ('" . self::STATUS_QUEUED . "', '" . self::STATUS_PROCESSING . "')
|
||||
OR d.queue_job_id IS NULL
|
||||
)
|
||||
ORDER BY
|
||||
CASE WHEN q.status IN ('" . self::STATUS_QUEUED . "', '" . self::STATUS_PROCESSING . "') THEN 0 ELSE 1 END,
|
||||
q.id DESC
|
||||
LIMIT $limit";
|
||||
$result = $db->query($sql);
|
||||
if (!$result instanceof mysqli_result) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$jobs = [];
|
||||
while ($row = $result->fetch_assoc()) {
|
||||
$jobs[] = $this->normalizeJobRow($row);
|
||||
}
|
||||
return $jobs;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws Exception
|
||||
*/
|
||||
public function dismissTerminalJobForUser(int $job_id, int $user_id): array
|
||||
{
|
||||
global $db;
|
||||
|
||||
$job_id = max(0, $job_id);
|
||||
$user_id = max(0, $user_id);
|
||||
if ($job_id < 1 || $user_id < 1) {
|
||||
throw new Exception('Queue job and user are required');
|
||||
}
|
||||
|
||||
$job = $this->getJobById($job_id);
|
||||
if ($job === null) {
|
||||
throw new Exception('Queue job not found');
|
||||
}
|
||||
|
||||
$status = strtoupper((string)($job['status'] ?? ''));
|
||||
if (!in_array($status, [self::STATUS_COMPLETED, self::STATUS_FAILED], true)) {
|
||||
throw new Exception('Only completed or failed queue jobs can be dismissed');
|
||||
}
|
||||
|
||||
$stmt = $db->prepare(
|
||||
"INSERT INTO economic_transfer_queue_job_dismissals (queue_job_id, user_id, dismissed_status, dismissed_at)
|
||||
VALUES (?, ?, ?, NOW())
|
||||
ON DUPLICATE KEY UPDATE dismissed_status = VALUES(dismissed_status), dismissed_at = NOW()"
|
||||
);
|
||||
if (!$stmt) {
|
||||
throw new Exception('Failed to prepare queue dismissal statement');
|
||||
}
|
||||
|
||||
$stmt->bind_param('iis', $job_id, $user_id, $status);
|
||||
if (!$stmt->execute()) {
|
||||
$stmt->close();
|
||||
throw new Exception('Failed to dismiss queue job');
|
||||
}
|
||||
$stmt->close();
|
||||
|
||||
return $job;
|
||||
}
|
||||
|
||||
public function dismissTerminalJobsForUser(int $user_id, ?string $transfer_type = null): int
|
||||
{
|
||||
global $db;
|
||||
|
||||
$user_id = max(0, $user_id);
|
||||
if ($user_id < 1) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
try {
|
||||
$normalized_transfer_type = $transfer_type !== null && trim($transfer_type) !== ''
|
||||
? $this->validateTransferType($transfer_type)
|
||||
: null;
|
||||
} catch (Exception) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
$transfer_condition = '';
|
||||
if ($normalized_transfer_type !== null) {
|
||||
$transfer_condition = "AND q.transfer_type = '" . $db->escape_string($normalized_transfer_type) . "'";
|
||||
}
|
||||
|
||||
$sql = "INSERT INTO economic_transfer_queue_job_dismissals (queue_job_id, user_id, dismissed_status, dismissed_at)
|
||||
SELECT q.id, $user_id, q.status, NOW()
|
||||
FROM economic_transfer_queue_jobs q
|
||||
LEFT JOIN economic_transfer_queue_job_dismissals d
|
||||
ON d.queue_job_id = q.id
|
||||
AND d.user_id = $user_id
|
||||
AND d.dismissed_status = q.status
|
||||
WHERE q.status IN ('" . self::STATUS_COMPLETED . "', '" . self::STATUS_FAILED . "')
|
||||
$transfer_condition
|
||||
AND d.queue_job_id IS NULL
|
||||
ON DUPLICATE KEY UPDATE dismissed_status = VALUES(dismissed_status), dismissed_at = NOW()";
|
||||
$db->query($sql);
|
||||
return max(0, (int)($db->affected_rows ?? 0));
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws Exception
|
||||
*/
|
||||
@@ -193,6 +319,8 @@ class economic_transfer_queue
|
||||
throw new Exception('Failed to retry queue job');
|
||||
}
|
||||
|
||||
$this->clearDismissalsForJob($job_id);
|
||||
|
||||
$job = $this->getJobById($job_id);
|
||||
if ($job === null) {
|
||||
throw new Exception('Retry updated job could not be loaded');
|
||||
@@ -480,6 +608,18 @@ class economic_transfer_queue
|
||||
];
|
||||
}
|
||||
|
||||
private function clearDismissalsForJob(int $job_id): void
|
||||
{
|
||||
global $db;
|
||||
|
||||
$job_id = max(0, $job_id);
|
||||
if ($job_id < 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
$db->query("DELETE FROM economic_transfer_queue_job_dismissals WHERE queue_job_id = $job_id");
|
||||
}
|
||||
|
||||
/**
|
||||
* Release jobs stuck in PROCESSING due to crashes or killed workers.
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user