Files
JobcardSystem/app/Domain/Notification/NotificationRecord.php
T

95 lines
3.7 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Domain\Notification;
/** Normalizes and validates notification metadata; delivery is deliberately out of scope. */
final class NotificationRecord
{
/** @var list<string> */
private const TYPES = [
'jobcard_created', 'jobcard_status_changed', 'assignment_created',
'time_entry_created', 'sla_threshold', 'attachment_uploaded',
];
/** @return array<string, mixed> */
public function normalize(array $record): array
{
$type = is_scalar($record['type'] ?? null) ? strtolower(trim((string) $record['type'])) : '';
$rawRecipients = $record['recipients'] ?? ($record['recipient'] ?? []);
if (!is_array($rawRecipients)) $rawRecipients = [$rawRecipients];
$recipients = [];
foreach ($rawRecipients as $recipient) {
if (is_scalar($recipient)) {
$value = strtolower(trim((string) $recipient));
if ($value !== '' && !in_array($value, $recipients, true)) $recipients[] = $value;
}
}
$key = $record['deduplication_key'] ?? $record['dedup_key'] ?? null;
$key = is_scalar($key) ? strtolower(trim((string) $key)) : '';
return [
'type' => $type,
'recipients' => $recipients,
'is_read' => $this->normalizeBoolean($record['is_read'] ?? $record['read'] ?? false),
'deduplication_key' => $key,
];
}
/** @return array<string, mixed> */
public function validate(array $record): array
{
$normalized = $this->normalize($record);
$errors = [];
if (!in_array($normalized['type'], self::TYPES, true)) {
$errors['type'] = 'Notification type is not supported.';
}
if ($normalized['recipients'] === []) {
$errors['recipients'] = 'At least one notification recipient is required.';
} else {
foreach ($normalized['recipients'] as $recipient) {
if (filter_var($recipient, FILTER_VALIDATE_EMAIL) === false) {
$errors['recipients'] = 'Notification recipients must be valid email addresses.';
break;
}
}
}
if (!is_bool($normalized['is_read'])) $errors['is_read'] = 'Read state must be boolean.';
if ($normalized['deduplication_key'] === '' || mb_strlen($normalized['deduplication_key']) > 190 || preg_match('/[\x00-\x1F\x7F]/', $normalized['deduplication_key']) === 1) {
$errors['deduplication_key'] = 'A safe deduplication key is required and must be 190 characters or fewer.';
}
return [...$normalized, 'valid' => $errors === [], 'errors' => $errors];
}
public function deduplicationKey(array $record): string
{
return $this->normalize($record)['deduplication_key'];
}
/** @return array<string, mixed> */
public function display(array $record): array
{
$normalized = $this->normalize($record);
return [
'type' => $normalized['type'],
'recipients' => $normalized['recipients'],
'is_read' => $normalized['is_read'],
'deduplication_key' => $normalized['deduplication_key'],
];
}
/** @return array<string, mixed> */
public function toDisplay(array $record): array { return $this->display($record); }
private function normalizeBoolean(mixed $value): mixed
{
if (is_bool($value)) return $value;
if (is_int($value) && ($value === 0 || $value === 1)) return $value === 1;
if (is_string($value)) return match (strtolower(trim($value))) {
'1', 'true', 'yes', 'on' => true,
'0', 'false', 'no', 'off' => false,
default => $value,
};
return $value;
}
}