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

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,398 @@
<?php
namespace App\Infrastructure\Reports\Services;
use App\Data\UnifiedPatientData;
use App\Models\Department;
use App\Models\MedicalHistorySnapshot;
use App\Models\User;
use App\Services\DateRange;
use App\Services\SnapshotService;
use App\Services\UnifiedPatientService;
use Illuminate\Support\Collection;
/**
* Сервис чтения пациентских выборок для отчётов.
*
* Он решает, откуда обслуживать запрос: из submitted-снапшотов или из live-реплики,
* и при этом сохраняет API близким к старым методам ReportService для
* постепенной strangler-миграции.
*/
class ReportPatientsReadService
{
public function __construct(
private readonly UnifiedPatientService $unifiedPatientService,
private readonly SnapshotService $snapshotService,
private readonly ReportReadContextResolver $contextResolver,
) {}
/**
* Получить пациентов отчёта по запрошенному статусу и области источника.
*/
public function getPatientsByStatus(
Department $department,
User $user,
string $status,
DateRange $dateRange,
bool $onlyIds = false,
bool $beforeCreate = false,
?bool $includeCurrentPatients = null
) {
[$baseStatus, $sourceScope] = $this->parseScopedStatus($status);
$branchId = $this->contextResolver->resolveBranchId($department);
if (! $branchId) {
return collect();
}
if ($sourceScope === 'special' || $baseStatus === 'reanimation') {
return $this->getPatientsFromReplica(
$department,
$user,
$status,
$dateRange,
$branchId,
$onlyIds,
$includeCurrentPatients
);
}
$useSnapshots = ! $this->contextResolver->shouldUseReplicaForLiveStatus($user, $baseStatus, $dateRange)
&& $this->contextResolver->shouldUseSnapshots($department, $dateRange, $beforeCreate);
if ($useSnapshots) {
return $this->getPatientsFromSnapshots($department, $status, $dateRange, $branchId, $onlyIds);
}
return $this->getPatientsFromReplica(
$department,
$user,
$status,
$dateRange,
$branchId,
$onlyIds,
$includeCurrentPatients
);
}
/**
* Посчитать пациентов отчёта по запрошенному статусу и области источника.
*/
public function getPatientsCountByStatus(
Department $department,
User $user,
string $status,
DateRange $dateRange
): int {
[$baseStatus, $sourceScope] = $this->parseScopedStatus($status);
$branchId = $this->contextResolver->resolveBranchId($department);
if (! $branchId) {
return 0;
}
if ($sourceScope === 'special' || $baseStatus === 'reanimation') {
return $this->getPatientsCountFromReplica($department, $user, $status, $dateRange, $branchId);
}
$useSnapshots = ! $this->contextResolver->shouldUseReplicaForLiveStatus($user, $baseStatus, $dateRange)
&& $this->contextResolver->shouldUseSnapshots($department, $dateRange);
if ($useSnapshots) {
return $this->getPatientsCountFromSnapshots($department, $status, $dateRange, $branchId);
}
return $this->getPatientsCountFromReplica($department, $user, $status, $dateRange, $branchId);
}
/**
* Построить карту счётчиков пациентов по scope для интерфейса.
*/
public function getPatientsCountsMap(Department $department, User $user, DateRange $dateRange): array
{
$baseStatuses = [
'plan',
'emergency',
'observation',
'reanimation',
'outcome-discharged',
'outcome-deceased',
'outcome-transferred',
];
$counts = [
'mis-plan' => 0,
'mis-emergency' => 0,
'mis-observation' => 0,
'mis-reanimation' => 0,
'mis-outcome' => 0,
'mis-outcome-discharged' => 0,
'mis-outcome-deceased' => 0,
'mis-outcome-transferred' => 0,
'special-plan' => 0,
'special-emergency' => 0,
'special-observation' => 0,
'special-reanimation' => 0,
'special-outcome' => 0,
'special-outcome-discharged' => 0,
'special-outcome-deceased' => 0,
'special-outcome-transferred' => 0,
];
foreach ($baseStatuses as $baseStatus) {
$counts["mis-{$baseStatus}"] = $this->getPatientsCountByStatus(
$department,
$user,
"mis-{$baseStatus}",
$dateRange
);
$counts["special-{$baseStatus}"] = $this->getPatientsCountByStatus(
$department,
$user,
"special-{$baseStatus}",
$dateRange
);
}
$counts['mis-outcome'] = ($counts['mis-outcome-discharged'] ?? 0) + ($counts['mis-outcome-deceased'] ?? 0);
$counts['special-outcome'] = ($counts['special-outcome-discharged'] ?? 0) + ($counts['special-outcome-deceased'] ?? 0);
return $counts;
}
/**
* Получить пациентскую выборку из submitted-снапшотов.
*/
public function getPatientsFromSnapshots(
Department $department,
string $status,
DateRange $dateRange,
int $branchId,
bool $onlyIds = false
) {
[$baseStatus, $sourceScope] = $this->parseScopedStatus($status);
$reportIds = $this->contextResolver
->getReportsForDateRange($department->department_id, $dateRange)
->pluck('report_id')
->all();
$recipientReportIds = $this->contextResolver->getRecipientReportIds($reportIds);
$patientTypeMap = [
'plan' => 'plan',
'emergency' => 'emergency',
'current' => 'current',
'recipient' => 'recipient',
'outcome-discharged' => 'discharged',
'outcome-transferred' => 'transferred',
'outcome-deceased' => 'deceased',
'observation' => 'observation',
];
$patientType = $patientTypeMap[$baseStatus] ?? null;
if ($patientType === 'observation') {
return $this->unifiedPatientService->getObservationPatients($department, $onlyIds, $sourceScope);
}
if ($baseStatus === 'outcome') {
$discharged = $this->snapshotService->getPatientsFromSnapshots(
'discharged',
$reportIds,
false,
false,
$recipientReportIds
);
$deceased = $this->snapshotService->getPatientsFromSnapshots(
'deceased',
$reportIds,
false,
false,
$recipientReportIds
);
$merged = UnifiedPatientData::unique($discharged->concat($deceased))
->sortByDesc(fn (UnifiedPatientData $patient) => $patient->admittedAt ?? '')
->values();
return $this->filterSnapshotPatientsByScope($merged, $sourceScope, $onlyIds);
}
if (! $patientType) {
return collect();
}
if ($dateRange->isOneDay && in_array($baseStatus, ['plan', 'emergency'], true)) {
$patients = $this->snapshotService->getPatientsFromOneDayCurrentSnapshots(
$patientType,
$reportIds,
false,
$recipientReportIds
);
return $this->filterSnapshotPatientsByScope($patients, $sourceScope, $onlyIds);
}
$patients = $this->snapshotService->getPatientsFromSnapshots(
$patientType,
$reportIds,
false,
in_array($baseStatus, ['plan', 'emergency'], true),
$recipientReportIds
);
return $this->filterSnapshotPatientsByScope($patients, $sourceScope, $onlyIds);
}
/**
* Получить пациентов напрямую из live-реплики и manual-источников.
*/
private function getPatientsFromReplica(
Department $department,
User $user,
string $status,
DateRange $dateRange,
int $branchId,
bool $onlyIds = false,
?bool $includeCurrent = null
) {
[$baseStatus] = $this->parseScopedStatus($status);
$includeCurrent ??= in_array($baseStatus, ['plan', 'emergency', 'reanimation'], true);
return $this->unifiedPatientService->getLivePatientsByStatus(
$department,
$user,
$status,
$dateRange,
$branchId,
$onlyIds,
$includeCurrent
);
}
/**
* Посчитать пациентов в снапшотах с той же семантикой scope, что и в legacy-сервисе.
*/
private function getPatientsCountFromSnapshots(
Department $department,
string $status,
DateRange $dateRange,
int $branchId
): int {
[$baseStatus, $sourceScope] = $this->parseScopedStatus($status);
$reportIds = $this->contextResolver
->getReportsForDateRange($department->department_id, $dateRange)
->pluck('report_id')
->all();
if ($baseStatus === 'outcome') {
if ($sourceScope !== 'all') {
return $this->getPatientsFromSnapshots($department, $status, $dateRange, $branchId)->count();
}
return MedicalHistorySnapshot::query()
->whereIn('rf_report_id', $reportIds)
->whereIn('patient_type', ['discharged', 'deceased'])
->distinct('rf_medicalhistory_id')
->count('rf_medicalhistory_id');
}
$patientTypeMap = [
'plan' => 'plan',
'emergency' => 'emergency',
'observation' => 'observation',
'outcome-discharged' => 'discharged',
'outcome-transferred' => 'transferred',
'outcome-deceased' => 'deceased',
];
$patientType = $patientTypeMap[$baseStatus] ?? null;
if (! $patientType) {
return 0;
}
if ($patientType === 'observation') {
return $this->unifiedPatientService->getObservationPatients($department, false, $sourceScope)->count();
}
if ($sourceScope !== 'all') {
return $this->getPatientsFromSnapshots($department, $status, $dateRange, $branchId)->count();
}
return MedicalHistorySnapshot::query()
->whereIn('rf_report_id', $reportIds)
->where('patient_type', $patientType)
->distinct('rf_medicalhistory_id')
->count('rf_medicalhistory_id');
}
/**
* Посчитать пациентов из реплики и manual-источников с legacy-правилами include-current.
*/
private function getPatientsCountFromReplica(
Department $department,
User $user,
string $status,
DateRange $dateRange,
int $branchId
): int {
[$baseStatus] = $this->parseScopedStatus($status);
return match ($status) {
'plan', 'emergency', 'observation', 'reanimation', 'outcome', 'outcome-discharged', 'outcome-transferred', 'outcome-deceased', 'current', 'recipient' => $this->unifiedPatientService->getLivePatientCountByStatus(
$department,
$user,
$status,
$dateRange,
$branchId,
in_array($status, ['plan', 'emergency'], true)
),
default => $this->unifiedPatientService->getLivePatientCountByStatus(
$department,
$user,
$status,
$dateRange,
$branchId,
in_array($baseStatus, ['plan', 'emergency'], true)
),
};
}
/**
* Применить фильтрацию по MIS/manual scope к коллекции DTO из снапшотов.
*/
private function filterSnapshotPatientsByScope(Collection $patients, string $sourceScope, bool $onlyIds = false)
{
if ($sourceScope === 'all') {
return $onlyIds ? $patients->pluck('id') : $patients;
}
$filtered = $patients->filter(function ($patient) use ($sourceScope) {
return match ($sourceScope) {
'mis' => $patient->sourceType === 'mis',
'special' => in_array($patient->sourceType, ['manual', 'special'], true),
default => true,
};
})->values();
return $onlyIds ? $filtered->pluck('id') : $filtered;
}
/**
* Разбить scoped-статус вроде "mis-plan" на базовый статус и scope источника.
*
* @return array{0: string, 1: string}
*/
private function parseScopedStatus(string $status): array
{
foreach (['mis', 'special'] as $scope) {
$prefix = "{$scope}-";
if (str_starts_with($status, $prefix)) {
return [substr($status, strlen($prefix)), $scope];
}
}
return [$status, 'all'];
}
}