Перевод на доменную архитектуру
This commit is contained in:
72
app/Application/Reports/CompareLegacyAndNewReportUseCase.php
Normal file
72
app/Application/Reports/CompareLegacyAndNewReportUseCase.php
Normal file
@@ -0,0 +1,72 @@
|
||||
<?php
|
||||
|
||||
namespace App\Application\Reports;
|
||||
|
||||
use App\Application\Reports\DTO\GenerateReportInput;
|
||||
use App\Application\Reports\DTO\ReportComparisonResult;
|
||||
use App\Domain\Reports\Contracts\ReportRepository;
|
||||
use App\Infrastructure\Reports\Adapters\LegacyReportServiceAdapter;
|
||||
use RuntimeException;
|
||||
|
||||
final readonly class CompareLegacyAndNewReportUseCase
|
||||
{
|
||||
public function __construct(
|
||||
private ReportRepository $reportRepository,
|
||||
private LegacyReportServiceAdapter $legacyAdapter,
|
||||
) {}
|
||||
|
||||
public function handle(GenerateReportInput $input): ReportComparisonResult
|
||||
{
|
||||
$startedAt = microtime(true);
|
||||
$expected = $this->legacyAdapter->buildSnapshotFromInput($input);
|
||||
|
||||
if ($input->persistedReportId === null) {
|
||||
throw new RuntimeException('persistedReportId is required for report comparison.');
|
||||
}
|
||||
|
||||
$actual = $this->reportRepository->findSnapshot($input->persistedReportId);
|
||||
|
||||
if ($actual === null) {
|
||||
throw new RuntimeException('Saved report snapshot was not found for comparison.');
|
||||
}
|
||||
|
||||
$diff = $this->buildDiff($expected->toComparableArray(), $actual->toComparableArray());
|
||||
$status = empty($diff) ? 'matched' : 'diff';
|
||||
|
||||
return new ReportComparisonResult(
|
||||
reportType: $input->reportType,
|
||||
path: 'new',
|
||||
status: $status,
|
||||
diff: $diff,
|
||||
departmentId: $input->departmentId,
|
||||
userId: $input->userId,
|
||||
periodStart: $input->periodStart->format('Y-m-d H:i:s'),
|
||||
periodEnd: $input->periodEnd->format('Y-m-d H:i:s'),
|
||||
reportId: $input->persistedReportId,
|
||||
durationMs: round((microtime(true) - $startedAt) * 1000, 2),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $expected
|
||||
* @param array<string, mixed> $actual
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
private function buildDiff(array $expected, array $actual): array
|
||||
{
|
||||
$diff = [];
|
||||
|
||||
foreach ($expected as $key => $value) {
|
||||
$actualValue = $actual[$key] ?? null;
|
||||
|
||||
if ($actualValue !== $value) {
|
||||
$diff[$key] = [
|
||||
'expected' => $value,
|
||||
'actual' => $actualValue,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
return $diff;
|
||||
}
|
||||
}
|
||||
117
app/Application/Reports/DTO/GenerateReportInput.php
Normal file
117
app/Application/Reports/DTO/GenerateReportInput.php
Normal file
@@ -0,0 +1,117 @@
|
||||
<?php
|
||||
|
||||
namespace App\Application\Reports\DTO;
|
||||
|
||||
use App\Domain\Reports\Models\ReportContext;
|
||||
use App\Domain\Reports\Models\ReportSnapshot;
|
||||
use DateTimeImmutable;
|
||||
|
||||
final readonly class GenerateReportInput
|
||||
{
|
||||
/**
|
||||
* @param array<int|string, int|float|string|null> $metrics
|
||||
* @param array<int, array<string, mixed>> $observationPatients
|
||||
* @param array<int, array<string, mixed>> $unwantedEvents
|
||||
* @param array<string, mixed>|null $rawPayload
|
||||
*/
|
||||
public function __construct(
|
||||
public int $departmentId,
|
||||
public int $userId,
|
||||
public ?int $actorUserId,
|
||||
public DateTimeImmutable $periodStart,
|
||||
public DateTimeImmutable $periodEnd,
|
||||
public array $metrics = [],
|
||||
public array $observationPatients = [],
|
||||
public array $unwantedEvents = [],
|
||||
public ?int $reportId = null,
|
||||
public string $status = 'draft',
|
||||
public string $reportType = 'daily',
|
||||
public bool $autoFill = false,
|
||||
public ?array $rawPayload = null,
|
||||
public ?int $persistedReportId = null,
|
||||
public ?DateTimeImmutable $createdAt = null,
|
||||
public ?DateTimeImmutable $sentAt = null,
|
||||
) {}
|
||||
|
||||
public function toContext(): ReportContext
|
||||
{
|
||||
return new ReportContext(
|
||||
departmentId: $this->departmentId,
|
||||
userId: $this->userId,
|
||||
actorUserId: $this->actorUserId,
|
||||
periodStart: $this->periodStart,
|
||||
periodEnd: $this->periodEnd,
|
||||
reportType: $this->reportType,
|
||||
metadata: [
|
||||
'auto_fill' => $this->autoFill,
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
public function toSnapshot(): ReportSnapshot
|
||||
{
|
||||
return new ReportSnapshot(
|
||||
departmentId: $this->departmentId,
|
||||
userId: $this->userId,
|
||||
actorUserId: $this->actorUserId,
|
||||
periodStart: $this->periodStart,
|
||||
periodEnd: $this->periodEnd,
|
||||
status: $this->status,
|
||||
autoFill: $this->autoFill,
|
||||
metrics: $this->metrics,
|
||||
observationPatients: $this->observationPatients,
|
||||
unwantedEvents: $this->unwantedEvents,
|
||||
reportId: $this->reportId,
|
||||
createdAt: $this->createdAt,
|
||||
sentAt: $this->sentAt,
|
||||
reportType: $this->reportType,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $payload
|
||||
*/
|
||||
public function withRawPayload(array $payload): self
|
||||
{
|
||||
return new self(
|
||||
departmentId: $this->departmentId,
|
||||
userId: $this->userId,
|
||||
actorUserId: $this->actorUserId,
|
||||
periodStart: $this->periodStart,
|
||||
periodEnd: $this->periodEnd,
|
||||
metrics: $this->metrics,
|
||||
observationPatients: $this->observationPatients,
|
||||
unwantedEvents: $this->unwantedEvents,
|
||||
reportId: $this->reportId,
|
||||
status: $this->status,
|
||||
reportType: $this->reportType,
|
||||
autoFill: $this->autoFill,
|
||||
rawPayload: $payload,
|
||||
persistedReportId: $this->persistedReportId,
|
||||
createdAt: $this->createdAt,
|
||||
sentAt: $this->sentAt,
|
||||
);
|
||||
}
|
||||
|
||||
public function withPersistedReportId(int $reportId): self
|
||||
{
|
||||
return new self(
|
||||
departmentId: $this->departmentId,
|
||||
userId: $this->userId,
|
||||
actorUserId: $this->actorUserId,
|
||||
periodStart: $this->periodStart,
|
||||
periodEnd: $this->periodEnd,
|
||||
metrics: $this->metrics,
|
||||
observationPatients: $this->observationPatients,
|
||||
unwantedEvents: $this->unwantedEvents,
|
||||
reportId: $this->reportId,
|
||||
status: $this->status,
|
||||
reportType: $this->reportType,
|
||||
autoFill: $this->autoFill,
|
||||
rawPayload: $this->rawPayload,
|
||||
persistedReportId: $reportId,
|
||||
createdAt: $this->createdAt,
|
||||
sentAt: $this->sentAt,
|
||||
);
|
||||
}
|
||||
}
|
||||
13
app/Application/Reports/DTO/GenerateReportResult.php
Normal file
13
app/Application/Reports/DTO/GenerateReportResult.php
Normal file
@@ -0,0 +1,13 @@
|
||||
<?php
|
||||
|
||||
namespace App\Application\Reports\DTO;
|
||||
|
||||
final readonly class GenerateReportResult
|
||||
{
|
||||
public function __construct(
|
||||
public int $reportId,
|
||||
public string $path,
|
||||
public bool $usedNewArchitecture,
|
||||
public ?ReportComparisonResult $comparison = null,
|
||||
) {}
|
||||
}
|
||||
22
app/Application/Reports/DTO/ReportComparisonResult.php
Normal file
22
app/Application/Reports/DTO/ReportComparisonResult.php
Normal file
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
namespace App\Application\Reports\DTO;
|
||||
|
||||
final readonly class ReportComparisonResult
|
||||
{
|
||||
/**
|
||||
* @param array<string, mixed> $diff
|
||||
*/
|
||||
public function __construct(
|
||||
public string $reportType,
|
||||
public string $path,
|
||||
public string $status,
|
||||
public array $diff,
|
||||
public int $departmentId,
|
||||
public int $userId,
|
||||
public string $periodStart,
|
||||
public string $periodEnd,
|
||||
public ?int $reportId,
|
||||
public float $durationMs,
|
||||
) {}
|
||||
}
|
||||
143
app/Application/Reports/GenerateReportUseCase.php
Normal file
143
app/Application/Reports/GenerateReportUseCase.php
Normal file
@@ -0,0 +1,143 @@
|
||||
<?php
|
||||
|
||||
namespace App\Application\Reports;
|
||||
|
||||
use App\Application\Reports\DTO\GenerateReportInput;
|
||||
use App\Application\Reports\DTO\GenerateReportResult;
|
||||
use App\Application\Reports\DTO\ReportComparisonResult;
|
||||
use App\Domain\Reports\Contracts\AuditLogger;
|
||||
use App\Domain\Reports\Contracts\MetricCalculator;
|
||||
use App\Domain\Reports\Contracts\PatientSource;
|
||||
use App\Domain\Reports\Contracts\ReportRepository;
|
||||
use App\Domain\Reports\Models\PatientCollection;
|
||||
use App\Domain\Reports\ValueObjects\MetrikaConfig;
|
||||
use DateTimeImmutable;
|
||||
|
||||
final readonly class GenerateReportUseCase
|
||||
{
|
||||
/**
|
||||
* @param iterable<MetricCalculator> $calculators
|
||||
*/
|
||||
public function __construct(
|
||||
private ReportRepository $reportRepository,
|
||||
private AuditLogger $auditLogger,
|
||||
private CompareLegacyAndNewReportUseCase $comparator,
|
||||
private ?PatientSource $patientSource = null,
|
||||
private iterable $calculators = [],
|
||||
private bool $compareBeforeCutover = true,
|
||||
) {}
|
||||
|
||||
public function handle(GenerateReportInput $input): GenerateReportResult
|
||||
{
|
||||
$resolvedInput = $this->resolveInput($input);
|
||||
$saved = $this->reportRepository->save($resolvedInput->toSnapshot());
|
||||
|
||||
$comparison = null;
|
||||
|
||||
if ($this->compareBeforeCutover) {
|
||||
try {
|
||||
$comparison = $this->comparator->handle(
|
||||
$resolvedInput->withPersistedReportId($saved->reportId)
|
||||
);
|
||||
} catch (\Throwable $exception) {
|
||||
$comparison = new ReportComparisonResult(
|
||||
reportType: $resolvedInput->reportType,
|
||||
path: 'new',
|
||||
status: 'failed',
|
||||
diff: ['error' => $exception->getMessage()],
|
||||
departmentId: $resolvedInput->departmentId,
|
||||
userId: $resolvedInput->userId,
|
||||
periodStart: $resolvedInput->periodStart->format('Y-m-d H:i:s'),
|
||||
periodEnd: $resolvedInput->periodEnd->format('Y-m-d H:i:s'),
|
||||
reportId: $saved->reportId,
|
||||
durationMs: 0.0,
|
||||
);
|
||||
}
|
||||
|
||||
$this->auditLogger->logComparison($comparison);
|
||||
}
|
||||
|
||||
return new GenerateReportResult(
|
||||
reportId: $saved->reportId,
|
||||
path: 'new',
|
||||
usedNewArchitecture: true,
|
||||
comparison: $comparison,
|
||||
);
|
||||
}
|
||||
|
||||
private function resolveInput(GenerateReportInput $input): GenerateReportInput
|
||||
{
|
||||
if ($input->rawPayload !== null) {
|
||||
return $input;
|
||||
}
|
||||
|
||||
if ($this->patientSource === null) {
|
||||
return $input;
|
||||
}
|
||||
|
||||
$patients = $this->patientSource->load($input->toContext());
|
||||
$payload = $patients->metadata('payload', []);
|
||||
|
||||
if (! is_array($payload) || $payload === []) {
|
||||
$payload = $this->buildPayloadFromCalculators($input, $patients);
|
||||
}
|
||||
|
||||
return new GenerateReportInput(
|
||||
departmentId: $input->departmentId,
|
||||
userId: $input->userId,
|
||||
actorUserId: $input->actorUserId,
|
||||
periodStart: $input->periodStart,
|
||||
periodEnd: $input->periodEnd,
|
||||
metrics: (array) ($payload['metrics'] ?? $input->metrics),
|
||||
observationPatients: (array) ($payload['observationPatients'] ?? $input->observationPatients),
|
||||
unwantedEvents: (array) ($payload['unwantedEvents'] ?? $input->unwantedEvents),
|
||||
reportId: isset($payload['reportId']) ? (int) $payload['reportId'] : $input->reportId,
|
||||
status: (string) ($payload['status'] ?? $input->status),
|
||||
reportType: $input->reportType,
|
||||
autoFill: $input->autoFill,
|
||||
rawPayload: $payload,
|
||||
persistedReportId: $input->persistedReportId,
|
||||
createdAt: isset($payload['created_at'])
|
||||
? new DateTimeImmutable((string) $payload['created_at'])
|
||||
: $input->createdAt,
|
||||
sentAt: isset($payload['sent_at'])
|
||||
? new DateTimeImmutable((string) $payload['sent_at'])
|
||||
: $input->sentAt,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Переходный fallback для будущих сценариев с calculator-based flow.
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
private function buildPayloadFromCalculators(GenerateReportInput $input, PatientCollection $patients): array
|
||||
{
|
||||
$metrics = $input->metrics;
|
||||
|
||||
foreach ($this->calculators as $calculator) {
|
||||
$metrics = [
|
||||
...$metrics,
|
||||
...$calculator->calculate($input->toContext(), $patients)->normalized(),
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
'departmentId' => $input->departmentId,
|
||||
'userId' => $input->userId,
|
||||
'actorUserId' => $input->actorUserId,
|
||||
'autoFill' => $input->autoFill,
|
||||
'dates' => [
|
||||
$input->periodStart->getTimestamp(),
|
||||
$input->periodEnd->getTimestamp(),
|
||||
],
|
||||
'status' => $input->status,
|
||||
'created_at' => $input->createdAt?->format('Y-m-d H:i:s') ?? $input->periodEnd->format('Y-m-d H:i:s'),
|
||||
'sent_at' => $input->sentAt?->format('Y-m-d H:i:s') ?? $input->periodEnd->format('Y-m-d H:i:s'),
|
||||
'metrics' => MetrikaConfig::toPayloadMetrics($metrics),
|
||||
'observationPatients' => $input->observationPatients,
|
||||
'unwantedEvents' => $input->unwantedEvents,
|
||||
'reportId' => $input->reportId,
|
||||
];
|
||||
}
|
||||
}
|
||||
13
app/Application/Reports/ReportFlowDecider.php
Normal file
13
app/Application/Reports/ReportFlowDecider.php
Normal file
@@ -0,0 +1,13 @@
|
||||
<?php
|
||||
|
||||
namespace App\Application\Reports;
|
||||
|
||||
final class ReportFlowDecider
|
||||
{
|
||||
public function shouldUseNewArchitecture(string $reportType = 'daily'): bool
|
||||
{
|
||||
$enabledTypes = config('reports.use_new_arch.report_types', []);
|
||||
|
||||
return in_array($reportType, $enabledTypes, true);
|
||||
}
|
||||
}
|
||||
76
app/Application/Reports/ReportInputFactory.php
Normal file
76
app/Application/Reports/ReportInputFactory.php
Normal file
@@ -0,0 +1,76 @@
|
||||
<?php
|
||||
|
||||
namespace App\Application\Reports;
|
||||
|
||||
use App\Application\Reports\DTO\GenerateReportInput;
|
||||
use App\Models\Department;
|
||||
use App\Models\User;
|
||||
use App\Services\DateRange;
|
||||
use App\Services\DateRangeService;
|
||||
use DateTimeImmutable;
|
||||
|
||||
final readonly class ReportInputFactory
|
||||
{
|
||||
public function __construct(
|
||||
private DateRangeService $dateRangeService,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $validated
|
||||
*/
|
||||
public function forManualSave(User $actor, array $validated, string $reportType = 'daily'): GenerateReportInput
|
||||
{
|
||||
$dateRange = $this->dateRangeService->getNormalizedDateRange(
|
||||
$actor,
|
||||
(string) ($validated['dates'][0] ?? null),
|
||||
(string) ($validated['dates'][1] ?? null),
|
||||
);
|
||||
|
||||
$endedAt = new DateTimeImmutable($dateRange->endSql());
|
||||
|
||||
return new GenerateReportInput(
|
||||
departmentId: (int) $validated['departmentId'],
|
||||
userId: (int) $validated['userId'],
|
||||
actorUserId: (int) $actor->id,
|
||||
periodStart: new DateTimeImmutable($dateRange->startSql()),
|
||||
periodEnd: $endedAt,
|
||||
metrics: (array) ($validated['metrics'] ?? []),
|
||||
observationPatients: (array) ($validated['observationPatients'] ?? []),
|
||||
unwantedEvents: (array) ($validated['unwantedEvents'] ?? []),
|
||||
reportId: isset($validated['reportId']) ? (int) $validated['reportId'] : null,
|
||||
status: (string) ($validated['status'] ?? 'draft'),
|
||||
reportType: $reportType,
|
||||
autoFill: false,
|
||||
rawPayload: [
|
||||
...$validated,
|
||||
'actorUserId' => $actor->id,
|
||||
'created_at' => $dateRange->endSql(),
|
||||
'sent_at' => $dateRange->endSql(),
|
||||
],
|
||||
createdAt: $endedAt,
|
||||
sentAt: $endedAt,
|
||||
);
|
||||
}
|
||||
|
||||
public function forAutoFill(
|
||||
User $scopedUser,
|
||||
Department $department,
|
||||
DateRange $dateRange,
|
||||
string $reportType = 'daily',
|
||||
): GenerateReportInput {
|
||||
$endedAt = new DateTimeImmutable($dateRange->endSql());
|
||||
|
||||
return new GenerateReportInput(
|
||||
departmentId: (int) $department->department_id,
|
||||
userId: (int) ($scopedUser->rf_lpudoctor_id ?? $scopedUser->id),
|
||||
actorUserId: (int) $scopedUser->id,
|
||||
periodStart: new DateTimeImmutable($dateRange->startSql()),
|
||||
periodEnd: $endedAt,
|
||||
status: 'submitted',
|
||||
reportType: $reportType,
|
||||
autoFill: true,
|
||||
createdAt: $endedAt,
|
||||
sentAt: $endedAt,
|
||||
);
|
||||
}
|
||||
}
|
||||
56
app/Application/Reports/ReportSavePathService.php
Normal file
56
app/Application/Reports/ReportSavePathService.php
Normal file
@@ -0,0 +1,56 @@
|
||||
<?php
|
||||
|
||||
namespace App\Application\Reports;
|
||||
|
||||
use App\Application\Reports\DTO\GenerateReportResult;
|
||||
use App\Models\Department;
|
||||
use App\Models\Report;
|
||||
use App\Models\User;
|
||||
use App\Services\DateRange;
|
||||
use App\Services\ReportService;
|
||||
|
||||
final readonly class ReportSavePathService
|
||||
{
|
||||
public function __construct(
|
||||
private ReportFlowDecider $reportFlowDecider,
|
||||
private ReportInputFactory $reportInputFactory,
|
||||
private GenerateReportUseCase $generateReportUseCase,
|
||||
private ReportService $reportService,
|
||||
) {}
|
||||
|
||||
public function usesNewArchitecture(string $reportType = 'daily'): bool
|
||||
{
|
||||
return $this->reportFlowDecider->shouldUseNewArchitecture($reportType);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $validated
|
||||
*/
|
||||
public function saveManual(User $actor, array $validated, string $reportType = 'daily'): GenerateReportResult|Report
|
||||
{
|
||||
if (! $this->usesNewArchitecture($reportType)) {
|
||||
return $this->reportService->storeReport($validated, $actor, false);
|
||||
}
|
||||
|
||||
return $this->generateReportUseCase->handle(
|
||||
$this->reportInputFactory->forManualSave($actor, $validated, $reportType)
|
||||
);
|
||||
}
|
||||
|
||||
public function saveAutoFill(
|
||||
User $scopedUser,
|
||||
Department $department,
|
||||
DateRange $dateRange,
|
||||
string $reportType = 'daily',
|
||||
): GenerateReportResult|Report {
|
||||
if (! $this->usesNewArchitecture($reportType)) {
|
||||
$payload = $this->reportService->buildAutoFillReportPayload($scopedUser, $department, $dateRange);
|
||||
|
||||
return $this->reportService->storeReport($payload, $scopedUser, true);
|
||||
}
|
||||
|
||||
return $this->generateReportUseCase->handle(
|
||||
$this->reportInputFactory->forAutoFill($scopedUser, $department, $dateRange, $reportType)
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user