foundation(); Sanctum::actingAs($designer); $this->postJson('/api/v1/content-mappings', [ 'courseVersionId' => $version->getKey(), 'mappableType' => 'lesson', 'mappableId' => $lesson->getKey(), 'taxonomyNodeId' => $node->getKey(), 'mappingType' => 'develops', 'masteryLevel' => 'applied', 'weight' => 0.7, 'source' => 'manual', ])->assertCreated() ->assertJsonPath('data.weight', 0.7) ->assertJsonPath('data.masteryLevel', 'applied') ->assertJsonPath('data.confirmationStatus', 'confirmed') ->assertJsonPath('data.direct', true); } public function test_ai_mapping_stays_draft_until_designer_confirmation(): void { [$designer, $version, $lesson, $node] = $this->foundation(); Sanctum::actingAs($designer); $this->postJson('/api/v1/content-mappings', [ 'courseVersionId' => $version->getKey(), 'mappableType' => 'lesson', 'mappableId' => $lesson->getKey(), 'taxonomyNodeId' => $node->getKey(), 'mappingType' => 'related', 'weight' => 0.5, 'source' => 'ai_suggested', 'confidence' => 0.82, ])->assertCreated() ->assertJsonPath('data.confirmationStatus', 'draft') ->assertJsonPath('data.confidence', 0.82); } public function test_designer_can_remove_draft_mapping_but_not_published_mapping(): void { [$designer, $version, $lesson, $node] = $this->foundation(); Sanctum::actingAs($designer); $payload = ['courseVersionId' => $version->getKey(), 'mappableType' => 'lesson', 'mappableId' => $lesson->getKey(), 'taxonomyNodeId' => $node->getKey(), 'mappingType' => 'develops', 'weight' => 1, 'source' => 'manual']; $mapping = $this->postJson('/api/v1/content-mappings', $payload)->assertCreated()->json('data.id'); $this->deleteJson("/api/v1/content-mappings/{$mapping}")->assertNoContent(); $mapping = $this->postJson('/api/v1/content-mappings', $payload)->assertCreated()->json('data.id'); $version->update(['status' => CourseVersionStatus::Published, 'published_at' => now()]); $this->deleteJson("/api/v1/content-mappings/{$mapping}")->assertUnprocessable()->assertJsonValidationErrors('courseVersionId'); } public function test_mapping_target_must_belong_to_selected_course_version(): void { [$designer, $version, , $node] = $this->foundation(); [, , $otherLesson] = $this->courseStructure($designer, 2); Sanctum::actingAs($designer); $this->postJson('/api/v1/content-mappings', [ 'courseVersionId' => $version->getKey(), 'mappableType' => 'lesson', 'mappableId' => $otherLesson->getKey(), 'taxonomyNodeId' => $node->getKey(), 'mappingType' => 'develops', 'weight' => 1, 'source' => 'manual', ])->assertUnprocessable()->assertJsonValidationErrors('mappableId'); } public function test_published_version_and_its_existing_mappings_are_immutable(): void { [$designer, $version, $lesson, $node] = $this->foundation(); Sanctum::actingAs($designer); $mappingId = $this->postJson('/api/v1/content-mappings', [ 'courseVersionId' => $version->getKey(), 'mappableType' => 'lesson', 'mappableId' => $lesson->getKey(), 'taxonomyNodeId' => $node->getKey(), 'mappingType' => 'develops', 'weight' => 1, 'source' => 'manual', ])->assertCreated()->json('data.id'); $version->update(['status' => CourseVersionStatus::Published, 'published_at' => now()]); $this->postJson('/api/v1/content-mappings', [ 'courseVersionId' => $version->getKey(), 'mappableType' => 'lesson', 'mappableId' => $lesson->getKey(), 'taxonomyNodeId' => $node->getKey(), 'mappingType' => 'practices', 'weight' => 1, 'source' => 'manual', ])->assertUnprocessable()->assertJsonValidationErrors('courseVersionId'); $this->expectException(DomainException::class); ContentTaxonomyMapping::query()->findOrFail($mappingId)->update(['weight' => 0.5]); } private function foundation(): array { $designer = User::factory()->create(['role' => UserRole::CourseDesigner]); [$course, $version, $lesson] = $this->courseStructure($designer, 1); $type = TaxonomyType::query()->create([ 'organization_id' => $designer->organization_id, 'key' => 'skill', 'name' => 'Skill', 'status' => TaxonomyStatus::Active, ]); $node = TaxonomyNode::query()->create([ 'organization_id' => $designer->organization_id, 'taxonomy_type_id' => $type->getKey(), 'name' => 'Giving feedback', 'status' => TaxonomyStatus::Active, ]); return [$designer, $version, $lesson, $node, $course]; } private function courseStructure(User $designer, int $versionNumber): array { $course = Course::query()->create([ 'organization_id' => $designer->organization_id, 'title' => 'Leadership', 'slug' => 'leadership-'.$versionNumber, 'created_by' => $designer->getKey(), ]); $version = CourseVersion::query()->create([ 'organization_id' => $designer->organization_id, 'course_id' => $course->getKey(), 'version_number' => $versionNumber, 'status' => CourseVersionStatus::Draft, 'title' => 'Leadership', ]); $module = CourseModule::query()->create([ 'organization_id' => $designer->organization_id, 'course_version_id' => $version->getKey(), 'title' => 'Communication', 'position' => 1, ]); $lesson = Lesson::query()->create([ 'organization_id' => $designer->organization_id, 'course_version_id' => $version->getKey(), 'course_module_id' => $module->getKey(), 'title' => 'Feedback', 'position' => 1, ]); return [$course, $version, $lesson]; } }