diff --git a/app/Services/Care/CareFeatures.php b/app/Services/Care/CareFeatures.php index 7bd959b..f3a032a 100644 --- a/app/Services/Care/CareFeatures.php +++ b/app/Services/Care/CareFeatures.php @@ -8,7 +8,9 @@ use App\Models\Organization; use Database\Seeders\AssessmentTemplateSeeder; use Database\Seeders\ClinicalPathwaySeeder; use Illuminate\Support\Facades\Artisan; +use Illuminate\Support\Facades\Log; use Illuminate\Support\Facades\Schema; +use Throwable; /** * Product rollout flags stored under organization settings.rollout.* @@ -66,8 +68,21 @@ class CareFeatures /** * Ensure platform assessment/pathway catalog exists (idempotent seed). * Call when the engine is enabled so consult/patient UI has templates. + * Never throws — web requests must not 500 if catalog seed races with live answers. */ public function ensureAssessmentCatalog(): void + { + try { + $this->seedAssessmentCatalogIfNeeded(); + } catch (Throwable $e) { + Log::warning('ensureAssessmentCatalog failed', [ + 'message' => $e->getMessage(), + 'exception' => $e::class, + ]); + } + } + + protected function seedAssessmentCatalogIfNeeded(): void { if (! Schema::hasTable('care_assessment_templates')) { return; diff --git a/database/seeders/AssessmentTemplateSeeder.php b/database/seeders/AssessmentTemplateSeeder.php index becd581..07b1395 100644 --- a/database/seeders/AssessmentTemplateSeeder.php +++ b/database/seeders/AssessmentTemplateSeeder.php @@ -94,18 +94,16 @@ class AssessmentTemplateSeeder extends Seeder ->update(['is_current' => false]); } - // Replace questions for this template version (stable codes within version). - AssessmentQuestion::query()->where('template_id', $template->id)->delete(); - + // Upsert questions by code so live answers keep their FK targets. + $keptCodes = []; $sort = 0; foreach ($data['questions'] as $q) { if (empty($q['code']) || empty($q['label']) || empty($q['answer_type'])) { throw new RuntimeException("Invalid question in pack {$path}"); } $sort++; - AssessmentQuestion::create([ - 'template_id' => $template->id, - 'code' => $q['code'], + $keptCodes[] = $q['code']; + $attributes = [ 'section' => $q['section'] ?? null, 'label' => $q['label'], 'help_text' => $q['help_text'] ?? null, @@ -115,9 +113,30 @@ class AssessmentTemplateSeeder extends Seeder 'sort_order' => (int) ($q['sort_order'] ?? $sort), 'score_key' => $q['score_key'] ?? null, 'validation' => $q['validation'] ?? null, - ]); + ]; + + $question = AssessmentQuestion::query() + ->where('template_id', $template->id) + ->where('code', $q['code']) + ->first(); + + if ($question) { + $question->fill($attributes)->save(); + } else { + AssessmentQuestion::create(array_merge($attributes, [ + 'template_id' => $template->id, + 'code' => $q['code'], + ])); + } } + // Drop obsolete pack questions only when nothing references them. + AssessmentQuestion::query() + ->where('template_id', $template->id) + ->whereNotIn('code', $keptCodes) + ->whereDoesntHave('answers') + ->delete(); + $this->command?->info(sprintf( 'Seeded assessment pack %s v%d (%d questions)', $template->code, diff --git a/tests/Feature/CareContentPacksTest.php b/tests/Feature/CareContentPacksTest.php index cc2fbfd..98a6bd5 100644 --- a/tests/Feature/CareContentPacksTest.php +++ b/tests/Feature/CareContentPacksTest.php @@ -91,4 +91,75 @@ class CareContentPacksTest extends TestCase $this->assertTrue($copd->questions()->where('code', 'mmrc')->exists()); $this->assertTrue($hf->questions()->where('code', 'nyha')->exists()); } + + public function test_reseed_preserves_questions_that_have_answers(): void + { + $this->seed(AssessmentTemplateSeeder::class); + + $user = \App\Models\User::create([ + 'public_id' => 'pack-reseed-user', + 'name' => 'Pack', + 'email' => 'pack-reseed@example.com', + ]); + $organization = \App\Models\Organization::create([ + 'owner_ref' => $user->public_id, + 'name' => 'Pack Clinic', + 'slug' => 'pack-clinic', + 'settings' => ['onboarded' => true], + ]); + $branch = \App\Models\Branch::create([ + 'owner_ref' => $user->public_id, + 'organization_id' => $organization->id, + 'name' => 'Main', + 'is_active' => true, + ]); + + $template = AssessmentTemplate::currentSystemByCode('universal_intake'); + $this->assertNotNull($template); + $question = $template->questions()->orderBy('id')->first(); + $this->assertNotNull($question); + $questionId = $question->id; + + $patient = \App\Models\Patient::create([ + 'owner_ref' => $user->public_id, + 'organization_id' => $organization->id, + 'branch_id' => $branch->id, + 'patient_number' => 'P-PACK-1', + 'first_name' => 'Pack', + 'last_name' => 'Patient', + ]); + + $assessment = \App\Models\Assessment::create([ + 'owner_ref' => $user->public_id, + 'organization_id' => $organization->id, + 'branch_id' => $branch->id, + 'patient_id' => $patient->id, + 'template_id' => $template->id, + 'status' => \App\Models\Assessment::STATUS_DRAFT, + 'assessed_at' => now(), + ]); + + \App\Models\AssessmentAnswer::create([ + 'owner_ref' => $user->public_id, + 'assessment_id' => $assessment->id, + 'question_id' => $questionId, + 'value_text' => 'kept', + ]); + + // Delete nursing packs so ensureAssessmentCatalog re-runs the seeder. + AssessmentTemplate::query() + ->whereNull('organization_id') + ->whereIn('code', ['news2', 'braden', 'morse', 'pain_nrs']) + ->delete(); + + app(\App\Services\Care\CareFeatures::class)->ensureAssessmentCatalog(); + + $this->assertDatabaseHas('care_assessment_questions', ['id' => $questionId]); + $this->assertDatabaseHas('care_assessment_answers', [ + 'assessment_id' => $assessment->id, + 'question_id' => $questionId, + 'value_text' => 'kept', + ]); + $this->assertNotNull(AssessmentTemplate::currentSystemByCode('news2')); + } }