Add invoice period review workflow (#336)
Improve the superuser invoice-period review API, stale-preview protection, queue visibility, review blockers, and e-conomic eligibility.
This commit is contained in:
@@ -43,6 +43,7 @@ class economic_transfer_queue
|
||||
|
||||
$active_job = $this->findActiveJobByTarget($transfer_type, $payload, $created_by);
|
||||
if ($active_job !== null) {
|
||||
$this->registerJobRequester((int)($active_job['id'] ?? 0), $created_by);
|
||||
$target_label = $this->buildTargetLabel($transfer_type, $payload);
|
||||
$this->logQueueEvent(
|
||||
1,
|
||||
@@ -75,6 +76,8 @@ class economic_transfer_queue
|
||||
$job_id = (int)$db->insert_id();
|
||||
$stmt->close();
|
||||
|
||||
$this->registerJobRequester($job_id, $created_by);
|
||||
|
||||
$this->logQueueEvent(
|
||||
1,
|
||||
$created_by,
|
||||
@@ -145,12 +148,19 @@ class economic_transfer_queue
|
||||
return null;
|
||||
}
|
||||
|
||||
$stmt = $db->prepare("SELECT * FROM economic_transfer_queue_jobs WHERE id = ? AND created_by = ? LIMIT 1");
|
||||
$stmt = $db->prepare(
|
||||
"SELECT q.*
|
||||
FROM economic_transfer_queue_jobs q
|
||||
LEFT JOIN economic_transfer_queue_job_requesters r
|
||||
ON r.queue_job_id = q.id AND r.user_id = ?
|
||||
WHERE q.id = ? AND (q.created_by = ? OR r.user_id = ?)
|
||||
LIMIT 1"
|
||||
);
|
||||
if (!$stmt) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$stmt->bind_param('ii', $job_id, $created_by);
|
||||
$stmt->bind_param('iiii', $created_by, $job_id, $created_by, $created_by);
|
||||
if (!$stmt->execute()) {
|
||||
$stmt->close();
|
||||
return null;
|
||||
@@ -179,7 +189,8 @@ class economic_transfer_queue
|
||||
$offset = max(0, $offset);
|
||||
|
||||
$where = $this->buildListJobsWhereClause($statuses, $transfer_type);
|
||||
$where .= $where === '' ? 'WHERE created_by = ' . $created_by : ' AND created_by = ' . $created_by;
|
||||
$visibility = $this->jobVisibilitySql('economic_transfer_queue_jobs', $created_by);
|
||||
$where .= $where === '' ? 'WHERE ' . $visibility : ' AND ' . $visibility;
|
||||
$sql = "SELECT * FROM economic_transfer_queue_jobs $where ORDER BY id DESC LIMIT $limit OFFSET $offset";
|
||||
$result = $db->query($sql);
|
||||
if (!$result instanceof mysqli_result) {
|
||||
@@ -203,7 +214,8 @@ class economic_transfer_queue
|
||||
}
|
||||
|
||||
$where = $this->buildListJobsWhereClause($statuses, $transfer_type);
|
||||
$where .= $where === '' ? 'WHERE created_by = ' . $created_by : ' AND created_by = ' . $created_by;
|
||||
$visibility = $this->jobVisibilitySql('economic_transfer_queue_jobs', $created_by);
|
||||
$where .= $where === '' ? 'WHERE ' . $visibility : ' AND ' . $visibility;
|
||||
$sql = "SELECT COUNT(*) AS total FROM economic_transfer_queue_jobs $where";
|
||||
$result = $db->query($sql);
|
||||
if (!$result instanceof mysqli_result) {
|
||||
@@ -242,6 +254,9 @@ class economic_transfer_queue
|
||||
global $db;
|
||||
|
||||
$user_id = max(0, $user_id);
|
||||
if ($user_id < 1) {
|
||||
return [];
|
||||
}
|
||||
$limit = max(1, min(100, $limit));
|
||||
try {
|
||||
$normalized_transfer_type = $transfer_type !== null && trim($transfer_type) !== ''
|
||||
@@ -262,7 +277,7 @@ class economic_transfer_queue
|
||||
ON d.queue_job_id = q.id
|
||||
AND d.user_id = $user_id
|
||||
AND d.dismissed_status = q.status
|
||||
WHERE q.created_by = $user_id
|
||||
WHERE " . $this->jobVisibilitySql('q', $user_id) . "
|
||||
$transfer_condition
|
||||
AND (
|
||||
q.status IN ('" . self::STATUS_QUEUED . "', '" . self::STATUS_PROCESSING . "')
|
||||
@@ -355,7 +370,7 @@ class economic_transfer_queue
|
||||
ON d.queue_job_id = q.id
|
||||
AND d.user_id = $user_id
|
||||
AND d.dismissed_status = q.status
|
||||
WHERE q.created_by = $user_id
|
||||
WHERE " . $this->jobVisibilitySql('q', $user_id) . "
|
||||
AND q.status IN ('" . self::STATUS_COMPLETED . "', '" . self::STATUS_FAILED . "')
|
||||
$transfer_condition
|
||||
AND d.queue_job_id IS NULL
|
||||
@@ -389,6 +404,9 @@ class economic_transfer_queue
|
||||
if ($existing_job === null) {
|
||||
throw new Exception('Queue job not found');
|
||||
}
|
||||
if ($created_by !== null && (int)($existing_job['created_by'] ?? 0) !== $created_by) {
|
||||
throw new Exception('Only the queue job creator can retry this job');
|
||||
}
|
||||
if ((string)($existing_job['status'] ?? '') !== self::STATUS_FAILED) {
|
||||
throw new Exception('Only failed jobs can be retried');
|
||||
}
|
||||
@@ -727,6 +745,38 @@ class economic_transfer_queue
|
||||
$db->query("DELETE FROM economic_transfer_queue_job_dismissals WHERE queue_job_id = $job_id");
|
||||
}
|
||||
|
||||
private function registerJobRequester(int $job_id, int $user_id): void
|
||||
{
|
||||
global $db;
|
||||
|
||||
if ($job_id < 1 || $user_id < 1) {
|
||||
return;
|
||||
}
|
||||
$stmt = $db->prepare(
|
||||
"INSERT INTO economic_transfer_queue_job_requesters (queue_job_id, user_id, requested_at)
|
||||
VALUES (?, ?, NOW())
|
||||
ON DUPLICATE KEY UPDATE requested_at = VALUES(requested_at)"
|
||||
);
|
||||
if (!$stmt) {
|
||||
throw new Exception('Failed to prepare queue requester registration');
|
||||
}
|
||||
$stmt->bind_param('ii', $job_id, $user_id);
|
||||
if (!$stmt->execute()) {
|
||||
$stmt->close();
|
||||
throw new Exception('Failed to register queue requester');
|
||||
}
|
||||
$stmt->close();
|
||||
}
|
||||
|
||||
private function jobVisibilitySql(string $alias, int $user_id): string
|
||||
{
|
||||
$user_id = max(0, $user_id);
|
||||
return "($alias.created_by = $user_id OR EXISTS (
|
||||
SELECT 1 FROM economic_transfer_queue_job_requesters requester
|
||||
WHERE requester.queue_job_id = $alias.id AND requester.user_id = $user_id
|
||||
))";
|
||||
}
|
||||
|
||||
/**
|
||||
* Release jobs stuck in PROCESSING due to crashes or killed workers.
|
||||
*/
|
||||
@@ -843,8 +893,8 @@ class economic_transfer_queue
|
||||
{
|
||||
global $db;
|
||||
|
||||
$created_by = max(0, $created_by);
|
||||
if ($target_value < 1 || $created_by < 1) {
|
||||
// Active work is unique by transfer type and business target across all requesting users.
|
||||
if ($target_value < 1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -854,7 +904,6 @@ class economic_transfer_queue
|
||||
WHERE transfer_type = ?
|
||||
AND status IN (?, ?)
|
||||
AND CAST(JSON_UNQUOTE(JSON_EXTRACT(payload_json, '$json_path')) AS UNSIGNED) = ?
|
||||
AND created_by = ?
|
||||
ORDER BY id DESC
|
||||
LIMIT 1"
|
||||
);
|
||||
@@ -864,7 +913,7 @@ class economic_transfer_queue
|
||||
|
||||
$queued = self::STATUS_QUEUED;
|
||||
$processing = self::STATUS_PROCESSING;
|
||||
$stmt->bind_param('sssii', $transfer_type, $queued, $processing, $target_value, $created_by);
|
||||
$stmt->bind_param('sssi', $transfer_type, $queued, $processing, $target_value);
|
||||
if (!$stmt->execute()) {
|
||||
$stmt->close();
|
||||
return null;
|
||||
|
||||
Reference in New Issue
Block a user