Files
breadcrumb-the-shire/lib/Repository/Audit/ImportAuditRunRepository.php
2026-03-05 08:26:51 +01:00

321 lines
11 KiB
PHP

<?php
namespace MintyPHP\Repository\Audit;
use MintyPHP\Domain\Taxonomy\ImportAuditStatus;
use MintyPHP\DB;
use MintyPHP\Repository\Support\RepoQuery;
class ImportAuditRunRepository implements ImportAuditRunRepositoryInterface
{
private const FILTER_OPTIONS_LIMIT_MAX = 200;
public function createRunning(array $data): int|false
{
$id = DB::insert(
'insert into import_audit_runs (
run_uuid, profile_key, status, source_filename, mapped_targets_csv, started_at, user_id, current_tenant_id
) values (?,?,?,?,?,NOW(),?,?)',
(string) ($data['run_uuid'] ?? ''),
(string) ($data['profile_key'] ?? ''),
(string) ($data['status'] ?? ImportAuditStatus::Running->value),
$data['source_filename'] ?? null,
$data['mapped_targets_csv'] ?? null,
$data['user_id'] !== null ? (string) ((int) $data['user_id']) : null,
$data['current_tenant_id'] !== null ? (string) ((int) $data['current_tenant_id']) : null
);
return $id ? (int) $id : false;
}
public function finishById(int $id, array $data): bool
{
if ($id <= 0) {
return false;
}
$updated = DB::update(
'update import_audit_runs
set status = ?,
rows_total = ?,
created_count = ?,
skipped_count = ?,
failed_count = ?,
error_codes_json = ?,
duration_ms = ?,
finished_at = NOW()
where id = ?',
(string) ($data['status'] ?? ImportAuditStatus::Failed->value),
(string) ((int) ($data['rows_total'] ?? 0)),
(string) ((int) ($data['created_count'] ?? 0)),
(string) ((int) ($data['skipped_count'] ?? 0)),
(string) ((int) ($data['failed_count'] ?? 0)),
$data['error_codes_json'] ?? null,
$data['duration_ms'] !== null ? (string) ((int) $data['duration_ms']) : null,
(string) $id
);
return (int) $updated > 0;
}
public function listPaged(array $filters): array
{
$search = trim((string) ($filters['search'] ?? ''));
$profileKey = strtolower(trim((string) ($filters['profile_key'] ?? '')));
$status = strtolower(trim((string) ($filters['status'] ?? '')));
$createdFrom = trim((string) ($filters['created_from'] ?? ''));
$createdTo = trim((string) ($filters['created_to'] ?? ''));
$userIds = array_slice(
RepoQuery::normalizeIdList($filters['user_ids'] ?? ''),
0,
self::FILTER_OPTIONS_LIMIT_MAX
);
[$limit, $offset] = RepoQuery::sanitizeLimitOffset($filters, 20, 1, 200, 0);
[$order, $dir] = RepoQuery::sanitizeOrder(
$filters,
[
'id',
'started_at',
'finished_at',
'duration_ms',
'rows_total',
'created_count',
'skipped_count',
'failed_count',
'status',
'profile_key',
],
'started_at',
'desc'
);
$where = [];
$params = [];
RepoQuery::addLikeFilter(
$where,
$params,
[
'import_audit_runs.run_uuid',
'import_audit_runs.source_filename',
'import_audit_runs.error_codes_json',
],
$search
);
if (in_array($profileKey, ['users', 'departments'], true)) {
$where[] = 'import_audit_runs.profile_key = ?';
$params[] = $profileKey;
}
if (ImportAuditStatus::tryNormalize($status) !== null) {
$where[] = 'import_audit_runs.status = ?';
$params[] = $status;
}
if (preg_match('/^\d{4}-\d{2}-\d{2}$/', $createdFrom)) {
$where[] = 'import_audit_runs.started_at >= ?';
$params[] = $createdFrom . ' 00:00:00';
}
if (preg_match('/^\d{4}-\d{2}-\d{2}$/', $createdTo)) {
$where[] = 'import_audit_runs.started_at <= ?';
$params[] = $createdTo . ' 23:59:59';
}
if ($userIds !== []) {
$where[] = 'import_audit_runs.user_id in (???)';
$params[] = $userIds;
}
$whereSql = $where ? (' where ' . implode(' and ', $where)) : '';
$fromSql = ' from import_audit_runs ' .
'left join users on users.id = import_audit_runs.user_id ' .
'left join tenants on tenants.id = import_audit_runs.current_tenant_id ';
$total = (int) (DB::selectValue('select count(*)' . $fromSql . $whereSql, ...$params) ?? 0);
$rows = DB::select(
'select
import_audit_runs.id,
import_audit_runs.run_uuid,
import_audit_runs.profile_key,
import_audit_runs.status,
import_audit_runs.source_filename,
import_audit_runs.mapped_targets_csv,
import_audit_runs.rows_total,
import_audit_runs.created_count,
import_audit_runs.skipped_count,
import_audit_runs.failed_count,
import_audit_runs.error_codes_json,
import_audit_runs.started_at,
import_audit_runs.finished_at,
import_audit_runs.duration_ms,
import_audit_runs.user_id,
import_audit_runs.current_tenant_id,
users.id,
users.uuid,
users.display_name,
users.email,
tenants.id,
tenants.uuid,
tenants.description
' . $fromSql . $whereSql .
sprintf(' order by import_audit_runs.`%s` %s limit ? offset ?', $order, $dir),
...array_merge($params, [(string) $limit, (string) $offset])
);
$normalized = [];
if (is_array($rows)) {
foreach ($rows as $row) {
$item = $this->normalizeRow($row);
if ($item !== null) {
$normalized[] = $item;
}
}
}
return [
'total' => $total,
'rows' => $normalized,
];
}
public function listFilterOptions(int $limit = self::FILTER_OPTIONS_LIMIT_MAX): array
{
$limit = max(1, min(self::FILTER_OPTIONS_LIMIT_MAX, $limit));
$userRows = DB::select(
'select
import_audit_runs.user_id,
max(import_audit_runs.started_at) as last_used_at,
max(users.id) as user_exists_id,
max(users.display_name) as user_display_name,
max(users.email) as user_email
from import_audit_runs
left join users on users.id = import_audit_runs.user_id
where import_audit_runs.user_id is not null
and import_audit_runs.user_id > 0
group by import_audit_runs.user_id
order by last_used_at desc
limit ?',
(string) $limit
);
$users = [];
if (is_array($userRows)) {
foreach ($userRows as $row) {
$flat = self::flattenRow($row);
$id = (int) ($flat['user_id'] ?? 0);
if ($id <= 0) {
continue;
}
$users[] = [
'id' => $id,
'display_name' => trim((string) ($flat['user_display_name'] ?? '')),
'email' => trim((string) ($flat['user_email'] ?? '')),
'exists' => (int) ($flat['user_exists_id'] ?? 0) > 0,
];
}
}
return ['users' => $users];
}
public function find(int $id): ?array
{
if ($id <= 0) {
return null;
}
$row = DB::selectOne(
'select
import_audit_runs.id,
import_audit_runs.run_uuid,
import_audit_runs.profile_key,
import_audit_runs.status,
import_audit_runs.source_filename,
import_audit_runs.mapped_targets_csv,
import_audit_runs.rows_total,
import_audit_runs.created_count,
import_audit_runs.skipped_count,
import_audit_runs.failed_count,
import_audit_runs.error_codes_json,
import_audit_runs.started_at,
import_audit_runs.finished_at,
import_audit_runs.duration_ms,
import_audit_runs.user_id,
import_audit_runs.current_tenant_id,
users.id,
users.uuid,
users.display_name,
users.email,
tenants.id,
tenants.uuid,
tenants.description
from import_audit_runs
left join users on users.id = import_audit_runs.user_id
left join tenants on tenants.id = import_audit_runs.current_tenant_id
where import_audit_runs.id = ?
limit 1',
(string) $id
);
return $this->normalizeRow($row);
}
public function purgeOlderThanDays(int $days): int
{
if ($days <= 0) {
return 0;
}
$cutoff = (new \DateTimeImmutable('now', new \DateTimeZone('UTC')))
->modify('-' . $days . ' days')
->format('Y-m-d H:i:s');
$deleted = DB::delete('delete from import_audit_runs where started_at < ?', $cutoff);
return is_int($deleted) ? $deleted : 0;
}
private function normalizeRow(mixed $row): ?array
{
if (!is_array($row)) {
return null;
}
$item = $row['import_audit_runs'] ?? [];
if (!is_array($item) || !isset($item['id'])) {
return null;
}
$user = is_array($row['users'] ?? null) ? $row['users'] : [];
$tenant = is_array($row['tenants'] ?? null) ? $row['tenants'] : [];
$item['user_uuid'] = (string) ($user['uuid'] ?? '');
$item['user_display_name'] = (string) ($user['display_name'] ?? '');
$item['user_email'] = (string) ($user['email'] ?? '');
$item['current_tenant_uuid'] = (string) ($tenant['uuid'] ?? '');
$item['current_tenant_description'] = (string) ($tenant['description'] ?? '');
return $item;
}
/**
* @return array<string, mixed>
*/
private static function flattenRow(mixed $row): array
{
if (!is_array($row)) {
return [];
}
$flat = [];
foreach ($row as $key => $value) {
if (is_array($value)) {
$flat = array_merge($flat, $value);
} elseif (is_string($key)) {
$flat[$key] = $value;
}
}
return $flat;
}
}