Add unit and integration tests for economic_transfer_queue and related endpoints, replacing synchronous fallback methods with queue-based processing.

This commit is contained in:
Jeppe Bundgaard
2026-04-08 12:29:57 +02:00
parent ba23ad6e8f
commit c5cd42be7f
17 changed files with 869 additions and 96 deletions
@@ -36,9 +36,22 @@ class economic_transfer_queue
{
global $db;
$transfer_type = $this->validateTransferType($transfer_type);
$max_attempts = max(1, min(10, $max_attempts));
$created_by = max(0, $created_by);
$max_attempts = max(1, min(10, $max_attempts));
$transfer_type = $this->validateTransferType($transfer_type);
$payload = $this->normalizePayloadForTransferType($transfer_type, $payload, $created_by);
$active_job = $this->findActiveJobByTarget($transfer_type, $payload);
if ($active_job !== null) {
$target_label = $this->buildTargetLabel($transfer_type, $payload);
$this->logQueueEvent(
1,
$created_by,
'ECONOMIC_TRANSFER_JOB_DEDUPED',
'Transfer job deduped for active target (' . $target_label . '), returning existing job #' . (int)($active_job['id'] ?? 0)
);
return $active_job;
}
$payload_json = json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
if ($payload_json === false) {
@@ -62,9 +75,7 @@ class economic_transfer_queue
$job_id = (int)$db->insert_id();
$stmt->close();
(new logs_o())->add(
'economic_transfer_queue',
'global',
$this->logQueueEvent(
1,
$created_by,
'ECONOMIC_TRANSFER_JOB_ENQUEUED',
@@ -372,9 +383,7 @@ class economic_transfer_queue
WHERE id = $job_id";
$db->query($sql);
(new logs_o())->add(
'economic_transfer_queue',
'global',
$this->logQueueEvent(
1,
0,
'ECONOMIC_TRANSFER_JOB_COMPLETED',
@@ -402,9 +411,7 @@ class economic_transfer_queue
WHERE id = $job_id";
$db->query($sql);
(new logs_o())->add(
'economic_transfer_queue',
'global',
$this->logQueueEvent(
3,
0,
'ECONOMIC_TRANSFER_JOB_FAILED',
@@ -462,6 +469,172 @@ class economic_transfer_queue
$db->query($sql);
}
/**
* @throws Exception
*/
private function normalizePayloadForTransferType(string $transfer_type, array $payload, int $created_by): array
{
$normalized_payload = $payload;
if (isset($normalized_payload['requested_by'])) {
$normalized_payload['requested_by'] = max(0, (int)$normalized_payload['requested_by']);
}
return match ($transfer_type) {
self::TYPE_ORDER_DRAFT_EXPORT, self::TYPE_ORDER_INVOICE_EXPORT => $this->normalizeOrderPayload($normalized_payload, $created_by),
self::TYPE_COLLECTED_INVOICE_EXPORT => $this->normalizeCollectedInvoicePayload($normalized_payload, $created_by),
default => $this->rejectPayload($created_by, 'Unsupported transfer type payload: ' . $transfer_type),
};
}
/**
* @throws Exception
*/
private function normalizeOrderPayload(array $payload, int $created_by): array
{
$order_id = $payload['order_id'] ?? null;
if ($order_id === null || !is_numeric($order_id) || (int)$order_id < 1) {
return $this->rejectPayload($created_by, 'order_id is required and must be a positive number');
}
$payload['order_id'] = (int)$order_id;
return $payload;
}
/**
* @throws Exception
*/
private function normalizeCollectedInvoicePayload(array $payload, int $created_by): array
{
$collected_invoice_id = $payload['collected_invoice_id'] ?? null;
if ($collected_invoice_id === null || !is_numeric($collected_invoice_id) || (int)$collected_invoice_id < 1) {
return $this->rejectPayload($created_by, 'collected_invoice_id is required and must be a positive number');
}
$payload['collected_invoice_id'] = (int)$collected_invoice_id;
$payload['send_as_is'] = $this->normalizeBooleanPayloadValue($payload['send_as_is'] ?? false, 'send_as_is', $created_by);
return $payload;
}
/**
* @throws Exception
*/
private function normalizeBooleanPayloadValue(mixed $value, string $field_name, int $created_by): bool
{
if (is_bool($value)) {
return $value;
}
if (is_int($value) || is_float($value) || (is_string($value) && is_numeric($value))) {
$numeric = (int)$value;
if ($numeric === 0 || $numeric === 1) {
return $numeric === 1;
}
return $this->rejectPayload($created_by, $field_name . ' must be a boolean');
}
if (is_string($value)) {
$normalized = strtolower(trim($value));
if (in_array($normalized, ['true', 'false', '1', '0'], true)) {
return in_array($normalized, ['true', '1'], true);
}
return $this->rejectPayload($created_by, $field_name . ' must be a boolean');
}
return $this->rejectPayload($created_by, $field_name . ' must be a boolean');
}
private function findActiveJobByTarget(string $transfer_type, array $payload): ?array
{
return match ($transfer_type) {
self::TYPE_ORDER_DRAFT_EXPORT, self::TYPE_ORDER_INVOICE_EXPORT => $this->findActiveJobByJsonNumericTarget(
$transfer_type,
'$.order_id',
(int)($payload['order_id'] ?? 0)
),
self::TYPE_COLLECTED_INVOICE_EXPORT => $this->findActiveJobByJsonNumericTarget(
$transfer_type,
'$.collected_invoice_id',
(int)($payload['collected_invoice_id'] ?? 0)
),
default => null,
};
}
private function findActiveJobByJsonNumericTarget(string $transfer_type, string $json_path, int $target_value): ?array
{
global $db;
if ($target_value < 1) {
return null;
}
$stmt = $db->prepare(
"SELECT id
FROM economic_transfer_queue_jobs
WHERE transfer_type = ?
AND status IN (?, ?)
AND CAST(JSON_UNQUOTE(JSON_EXTRACT(payload_json, '$json_path')) AS UNSIGNED) = ?
ORDER BY id DESC
LIMIT 1"
);
if (!$stmt) {
return null;
}
$queued = self::STATUS_QUEUED;
$processing = self::STATUS_PROCESSING;
$stmt->bind_param('sssi', $transfer_type, $queued, $processing, $target_value);
if (!$stmt->execute()) {
$stmt->close();
return null;
}
$result = $stmt->get_result();
$row = $result instanceof mysqli_result ? $result->fetch_assoc() : null;
$stmt->close();
if (!$row || !isset($row['id'])) {
return null;
}
return $this->getJobById((int)$row['id']);
}
private function buildTargetLabel(string $transfer_type, array $payload): string
{
return match ($transfer_type) {
self::TYPE_ORDER_DRAFT_EXPORT, self::TYPE_ORDER_INVOICE_EXPORT => 'order_id=' . (int)($payload['order_id'] ?? 0),
self::TYPE_COLLECTED_INVOICE_EXPORT => 'collected_invoice_id=' . (int)($payload['collected_invoice_id'] ?? 0),
default => 'unknown',
};
}
/**
* @throws Exception
*/
private function rejectPayload(int $created_by, string $message): never
{
$this->logQueueEvent(
3,
max(0, $created_by),
'ECONOMIC_TRANSFER_JOB_VALIDATION_REJECTED',
'Transfer job enqueue rejected: ' . $message
);
throw new Exception($message);
}
private function logQueueEvent(int $status_code, int $user_id, string $event, string $message): void
{
try {
(new logs_o())->add(
'economic_transfer_queue',
'global',
$status_code,
$user_id,
$event,
$message
);
} catch (\Throwable) {
// Logging is best effort for queue operations.
}
}
/**
* @throws Exception
*/