Перевод на доменную архитектуру

This commit is contained in:
brusnitsyn
2026-04-26 23:37:50 +09:00
parent 75ca01ffd8
commit f107ebd167
70 changed files with 4656 additions and 2070 deletions

View File

@@ -0,0 +1,33 @@
<?php
namespace App\Domain\Reports\Calculators;
use App\Domain\Reports\Models\MetricAggregate;
use App\Domain\Reports\Models\StayInterval;
final class BedDaysCalculator
{
/**
* @param iterable<StayInterval> $intervals
*/
public function calculate(iterable $intervals): MetricAggregate
{
$totalDays = 0;
$patientCount = 0;
foreach ($intervals as $interval) {
if ($interval->endAt < $interval->startAt) {
continue;
}
$totalDays += $interval->startAt->setTime(0, 0)->diff($interval->endAt->setTime(0, 0))->days;
$patientCount++;
}
return new MetricAggregate(
total: $totalDays,
count: $patientCount,
average: $patientCount > 0 ? round($totalDays / $patientCount, 2) : 0.0,
);
}
}

View File

@@ -0,0 +1,15 @@
<?php
namespace App\Domain\Reports\Calculators;
final class DepartmentLoadCalculator
{
public function calculate(int|float $currentCount, int|float $bedsCount): int
{
if ($bedsCount <= 0) {
return 0;
}
return (int) round($currentCount * 100 / $bedsCount);
}
}

View File

@@ -0,0 +1,33 @@
<?php
namespace App\Domain\Reports\Calculators;
use App\Domain\Reports\Models\MetricAggregate;
use App\Domain\Reports\Models\OperationInterval;
final class PreoperativeDaysCalculator
{
/**
* @param iterable<OperationInterval> $intervals
*/
public function calculate(iterable $intervals): MetricAggregate
{
$totalDays = 0;
$patientCount = 0;
foreach ($intervals as $interval) {
if ($interval->operationAt < $interval->admittedAt) {
continue;
}
$totalDays += $interval->admittedAt->setTime(0, 0)->diff($interval->operationAt->setTime(0, 0))->days;
$patientCount++;
}
return new MetricAggregate(
total: $totalDays,
count: $patientCount,
average: $patientCount > 0 ? round($totalDays / $patientCount, 1) : 0.0,
);
}
}

View File

@@ -0,0 +1,10 @@
<?php
namespace App\Domain\Reports\Contracts;
use App\Application\Reports\DTO\ReportComparisonResult;
interface AuditLogger
{
public function logComparison(ReportComparisonResult $result): void;
}

View File

@@ -0,0 +1,12 @@
<?php
namespace App\Domain\Reports\Contracts;
use App\Domain\Reports\Models\MetricResultCollection;
use App\Domain\Reports\Models\PatientCollection;
use App\Domain\Reports\Models\ReportContext;
interface MetricCalculator
{
public function calculate(ReportContext $context, PatientCollection $patients): MetricResultCollection;
}

View File

@@ -0,0 +1,11 @@
<?php
namespace App\Domain\Reports\Contracts;
use App\Domain\Reports\Models\PatientCollection;
use App\Domain\Reports\Models\ReportContext;
interface PatientSource
{
public function load(ReportContext $context): PatientCollection;
}

View File

@@ -0,0 +1,13 @@
<?php
namespace App\Domain\Reports\Contracts;
use App\Domain\Reports\Models\ReportSnapshot;
use App\Domain\Reports\Models\SavedReportResult;
interface ReportRepository
{
public function save(ReportSnapshot $snapshot): SavedReportResult;
public function findSnapshot(int $reportId): ?ReportSnapshot;
}

View File

@@ -0,0 +1,12 @@
<?php
namespace App\Domain\Reports\Models;
final readonly class MetricAggregate
{
public function __construct(
public int $total,
public int $count,
public float $average,
) {}
}

View File

@@ -0,0 +1,23 @@
<?php
namespace App\Domain\Reports\Models;
use App\Domain\Reports\ValueObjects\MetrikaConfig;
final readonly class MetricResultCollection
{
/**
* @param array<int|string, int|float|string|null> $metrics
*/
public function __construct(
public array $metrics = [],
) {}
/**
* @return array<int, int|float|string|null>
*/
public function normalized(): array
{
return MetrikaConfig::normalizeMetrics($this->metrics);
}
}

View File

@@ -0,0 +1,13 @@
<?php
namespace App\Domain\Reports\Models;
use DateTimeImmutable;
final readonly class OperationInterval
{
public function __construct(
public DateTimeImmutable $admittedAt,
public DateTimeImmutable $operationAt,
) {}
}

View File

@@ -0,0 +1,20 @@
<?php
namespace App\Domain\Reports\Models;
final readonly class PatientCollection
{
/**
* @param array<int, array<string, mixed>> $items
* @param array<string, mixed> $metadata
*/
public function __construct(
public array $items = [],
public array $metadata = [],
) {}
public function metadata(string $key, mixed $default = null): mixed
{
return $this->metadata[$key] ?? $default;
}
}

View File

@@ -0,0 +1,21 @@
<?php
namespace App\Domain\Reports\Models;
use DateTimeImmutable;
final readonly class ReportContext
{
/**
* @param array<string, mixed> $metadata
*/
public function __construct(
public int $departmentId,
public int $userId,
public ?int $actorUserId,
public DateTimeImmutable $periodStart,
public DateTimeImmutable $periodEnd,
public string $reportType = 'daily',
public array $metadata = [],
) {}
}

View File

@@ -0,0 +1,85 @@
<?php
namespace App\Domain\Reports\Models;
use App\Domain\Reports\ValueObjects\MetrikaConfig;
use DateTimeImmutable;
use InvalidArgumentException;
final readonly class ReportSnapshot
{
/**
* @param array<int|string, int|float|string|null> $metrics
* @param array<int, array<string, mixed>> $observationPatients
* @param array<int, array<string, mixed>> $unwantedEvents
*/
public function __construct(
public int $departmentId,
public int $userId,
public ?int $actorUserId,
public DateTimeImmutable $periodStart,
public DateTimeImmutable $periodEnd,
public string $status = 'draft',
public bool $autoFill = false,
public array $metrics = [],
public array $observationPatients = [],
public array $unwantedEvents = [],
public ?int $reportId = null,
public ?DateTimeImmutable $createdAt = null,
public ?DateTimeImmutable $sentAt = null,
public string $reportType = 'daily',
) {
if ($this->departmentId <= 0) {
throw new InvalidArgumentException('departmentId must be positive.');
}
if ($this->userId <= 0) {
throw new InvalidArgumentException('userId must be positive.');
}
if ($this->periodEnd < $this->periodStart) {
throw new InvalidArgumentException('periodEnd must not be earlier than periodStart.');
}
}
/**
* @return array<int, int|float|string|null>
*/
public function normalizedMetrics(): array
{
return MetrikaConfig::normalizeMetrics($this->metrics);
}
/**
* @return array<string, int|float|string|null>
*/
public function payloadMetrics(): array
{
return MetrikaConfig::toPayloadMetrics($this->normalizedMetrics());
}
/**
* @return array<string, mixed>
*/
public function toComparableArray(): array
{
$observationPatients = $this->observationPatients;
$unwantedEvents = $this->unwantedEvents;
sort($observationPatients);
sort($unwantedEvents);
return [
'department_id' => $this->departmentId,
'user_id' => $this->userId,
'actor_user_id' => $this->actorUserId,
'period_start' => $this->periodStart->format('Y-m-d H:i:s'),
'period_end' => $this->periodEnd->format('Y-m-d H:i:s'),
'status' => $this->status,
'auto_fill' => $this->autoFill,
'metrics' => $this->normalizedMetrics(),
'observation_patients' => $observationPatients,
'unwanted_events' => $unwantedEvents,
];
}
}

View File

@@ -0,0 +1,11 @@
<?php
namespace App\Domain\Reports\Models;
final readonly class SavedReportResult
{
public function __construct(
public int $reportId,
public ReportSnapshot $snapshot,
) {}
}

View File

@@ -0,0 +1,13 @@
<?php
namespace App\Domain\Reports\Models;
use DateTimeImmutable;
final readonly class StayInterval
{
public function __construct(
public DateTimeImmutable $startAt,
public DateTimeImmutable $endAt,
) {}
}

View File

@@ -0,0 +1,100 @@
<?php
namespace App\Domain\Reports\ValueObjects;
final class MetrikaConfig
{
public const BEDS = 1;
public const RECIPIENT = 3;
public const PLAN = 4;
public const OUTCOME = 7;
public const CURRENT = 8;
public const DECEASED = 9;
public const EMERGENCY_SURGERY = 10;
public const PLAN_SURGERY = 11;
public const EMERGENCY = 12;
public const TRANSFERRED = 13;
public const OBSERVATION = 14;
public const DISCHARGED = 15;
public const UNWANTED_EVENTS = 16;
public const AVERAGE_BED_DAYS = 18;
public const PREOPERATIVE_AVERAGE_DAYS = 21;
public const DEPARTMENT_LOADED = 22;
public const TOTAL_BED_DAYS = 25;
public const TOTAL_PREOPERATIVE_DAYS = 26;
public const PREOPERATIVE_PATIENT_COUNT = 27;
public static function payloadKey(int $metricId): string
{
return 'metrika_item_'.$metricId;
}
/**
* @param array<int|string, int|float|string|null> $metrics
* @return array<int, int|float|string|null>
*/
public static function normalizeMetrics(array $metrics): array
{
$normalized = [];
foreach ($metrics as $key => $value) {
$metricId = self::extractMetricId($key);
if ($metricId === null) {
continue;
}
$normalized[$metricId] = $value;
}
ksort($normalized);
return $normalized;
}
/**
* @param array<int, int|float|string|null> $metrics
* @return array<string, int|float|string|null>
*/
public static function toPayloadMetrics(array $metrics): array
{
$payload = [];
foreach (self::normalizeMetrics($metrics) as $metricId => $value) {
$payload[self::payloadKey($metricId)] = $value;
}
return $payload;
}
public static function extractMetricId(int|string $key): ?int
{
if (is_int($key) || ctype_digit((string) $key)) {
return (int) $key;
}
if (preg_match('/^metrika_item_(\d+)$/', (string) $key, $matches) !== 1) {
return null;
}
return (int) $matches[1];
}
}