command?->warn("No assessment packs directory at {$dir}"); return; } $files = collect(File::files($dir)) ->filter(fn ($file) => str_ends_with(strtolower($file->getFilename()), '.json')) ->sortBy(fn ($file) => $file->getFilename()) ->values(); if ($files->isEmpty()) { $this->command?->warn('No assessment JSON packs found.'); return; } foreach ($files as $file) { $this->seedPack($file->getPathname()); } } public function seedPack(string $path): AssessmentTemplate { $raw = File::get($path); $data = json_decode($raw, true); if (! is_array($data) || ! isset($data['template'], $data['questions'])) { throw new RuntimeException("Invalid assessment pack: {$path}"); } $t = $data['template']; foreach (['code', 'name', 'category', 'version'] as $required) { if (! array_key_exists($required, $t)) { throw new RuntimeException("Assessment pack missing template.{$required}: {$path}"); } } return DB::transaction(function () use ($t, $data, $path) { $template = AssessmentTemplate::withTrashed() ->whereNull('organization_id') ->where('code', $t['code']) ->where('version', (int) $t['version']) ->first(); $attributes = [ 'organization_id' => null, 'code' => $t['code'], 'name' => $t['name'], 'category' => $t['category'], 'description' => $t['description'] ?? null, 'version' => (int) $t['version'], 'is_current' => (bool) ($t['is_current'] ?? true), 'is_active' => (bool) ($t['is_active'] ?? true), 'scoring_strategy' => $t['scoring_strategy'] ?? null, 'meta' => $t['meta'] ?? null, 'deleted_at' => null, ]; if ($template) { if ($template->trashed()) { $template->restore(); } $template->fill(collect($attributes)->except('deleted_at')->all())->save(); } else { $template = AssessmentTemplate::create(collect($attributes)->except('deleted_at')->all()); } if ($template->is_current) { AssessmentTemplate::query() ->whereNull('organization_id') ->where('code', $template->code) ->where('id', '!=', $template->id) ->where('is_current', true) ->update(['is_current' => false]); } // 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++; $keptCodes[] = $q['code']; $attributes = [ 'section' => $q['section'] ?? null, 'label' => $q['label'], 'help_text' => $q['help_text'] ?? null, 'answer_type' => $q['answer_type'], 'options' => $q['options'] ?? null, 'is_required' => (bool) ($q['is_required'] ?? false), '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, $template->version, count($data['questions']), )); return $template->fresh('questions'); }); } }