where('organization_id', $organizationId)->findOrFail($courseVersionId); if ($version->status === CourseVersionStatus::Published) { $this->invalid('courseVersionId', 'Published course versions cannot receive new mappings.'); } TaxonomyNode::query()->where('organization_id', $organizationId)->findOrFail($taxonomyNodeId); $this->resolveTarget($organizationId, $version, $mappableType, $mappableId); $confirmation = $source === MappingSource::AiSuggested ? MappingConfirmationStatus::Draft : MappingConfirmationStatus::Confirmed; return ContentTaxonomyMapping::query()->create([ 'organization_id' => $organizationId, 'course_version_id' => $version->getKey(), 'mappable_type' => $mappableType, 'mappable_id' => $mappableId, 'taxonomy_node_id' => $taxonomyNodeId, 'mapping_type' => $mappingType, 'mastery_level' => $masteryLevel, 'weight' => $weight, 'source' => $source, 'confidence' => $confidence, 'confirmation_status' => $confirmation, 'confirmed_by' => $confirmation === MappingConfirmationStatus::Confirmed ? $actor->getKey() : null, 'confirmed_at' => $confirmation === MappingConfirmationStatus::Confirmed ? now() : null, ]); } private function resolveTarget(string $organizationId, CourseVersion $version, MappableType $type, string $id): Model { $modelClass = $type->modelClass(); $query = $modelClass::query()->where('organization_id', $organizationId); if ($type !== MappableType::Course) { $query->where('course_version_id', $version->getKey()); } $target = $query->find($id); if (! $target || ($type === MappableType::Course && $target->getKey() !== $version->getKey())) { $this->invalid('mappableId', 'The selected content does not belong to this course version.'); } return $target; } private function invalid(string $field, string $message): never { throw ValidationException::withMessages([$field => [$message]]); } }