Перейти к основному содержимому

API contracts LMS

Зачем нужно

Документ задаёт контракты данных для API из api-map.md. Он нужен, чтобы frontend, backend, QA и AI-агенты одинаково понимали форму запросов, ответов и ошибок.

Endpoint coverage

api-map.md является endpoint registry. Этот файл не дублирует полный DTO для каждой CRUD-строки; все endpoints из api-map.md покрываются platform envelope, pagination, error и idempotency rules плюс DTO/feature docs из таблицы.

API map sectionEndpoint rows coveredContract source
Course catalog inside LMS/courses*Course DTO; features/courses.md
Course versions and content tree/course-versions*, /nodes*, /content-blocks*Course DTO, Content tree DTO, Rules DTO; features/courses.md
Enrollment and access/enrollments*Enrollment DTO; features/courses.md
Student API/me/enrollments*Enrollment DTO, Progress DTO, Attempts; features/learning-workspace.md, features/progress.md
Parent API/family/student-profiles*Enrollment DTO, Progress DTO, Workbook DTO, Chat DTO; features/learning-workspace.md
Attempts, submissions and feedback/attempts*, /submissions*, /feedback*Attempts, Submissions and feedback; features/lessons.md, features/progress.md
Teacher API/teacher/*Teacher DTO, Submissions and feedback; features/internal-teacher-tools.md
Learning Workspace/learning-workspace/*Learning Workspace DTO; features/learning-workspace.md
Workbooks/workbooks*Workbook DTO, Attachment ref; features/workbooks.md
Projects/projects*Project DTO, Attachment ref; features/course-projects.md
Roadmap/roadmap/*, /me/roadmap/*, /family/*/roadmap/*, /teacher/*/roadmap/*Roadmap DTO; features/roadmap.md
Booking and calendar/groups, /sessions*, /booking/*, /bookings*Booking DTO; features/calendar.md, features/class-booking.md
Chat/chats*Chat DTO; features/teacher-chat.md
Public storefront read/public/*Public roadmap/program/fact DTO; ../integrations/lms--storefront.md
Admin and operations/admin/*common envelope/error/idempotency rules; features/admin.md
Webhooks and service API/webhooks/*Webhook contracts; integrations.md

Общие типы

type UUID = string;
type ISODateTime = string;

type LmsActorContext = {
userId: UUID;
roles: string[];
familyUserIds: UUID[];
organizationIds: UUID[];
activeOrganizationId?: UUID;
teacherScopes: TeacherScopeDto[];
learningWorkspaceScopes?: LearningWorkspaceScopeDto[];
};

type PageRequest = {
cursor?: string;
limit?: number;
};

type PageResponse<T> = {
items: T[];
nextCursor?: string;
};

type ApiError = {
code: string;
message: string;
details?: Record<string, unknown>;
};

type ApiEnvelope<TData, TMeta = Record<string, unknown>> = {
data: TData | null;
meta?: TMeta;
error?: ApiError;
};

Course DTO

type CourseStatus = 'draft' | 'review' | 'published' | 'archived';
type CourseVersionStatus = 'draft' | 'review' | 'published' | 'retired';

type LmsCourseDto = {
id: UUID;
slug: string;
title: string;
description?: string;
subjectKey: string;
status: CourseStatus;
visibility: 'private' | 'internal' | 'public_preview';
defaultLocale: string;
activePublishedVersionId?: UUID;
createdAt: ISODateTime;
updatedAt: ISODateTime;
};

type CreateCourseRequest = {
slug: string;
title: string;
description?: string;
subjectKey: string;
visibility?: 'private' | 'internal' | 'public_preview';
defaultLocale?: string;
};

type UpdateCourseRequest = {
title?: string;
description?: string | null;
subjectKey?: string;
visibility?: 'private' | 'internal' | 'public_preview';
};

type CourseVersionDto = {
id: UUID;
courseId: UUID;
version: number;
status: CourseVersionStatus;
sourceVersionId?: UUID;
contentHash?: string;
publishedAt?: ISODateTime;
publishedByUserId?: UUID;
retiredAt?: ISODateTime;
};

Content tree DTO

type LmsNodeType =
| 'module'
| 'section'
| 'lesson'
| 'intensive_day'
| 'checkpoint'
| 'project_stage'
| 'supplement';

type ContentBlockType =
| 'text'
| 'video'
| 'file'
| 'image'
| 'embed'
| 'quiz'
| 'task_bank_ref'
| 'assignment'
| 'workbook_prompt'
| 'project_milestone'
| 'interactive';

type LmsNodeDto = {
id: UUID;
courseVersionId: UUID;
parentId?: UUID;
type: LmsNodeType;
title: string;
description?: string;
position: number;
unlockRule: UnlockRuleDto;
completionRule: CompletionRuleDto;
roadmapTopicRefs: RoadmapTopicRefDto[];
estimatedMinutes?: number;
blocks?: ContentBlockDto[];
progress?: ProgressSummaryDto;
};

type ContentBlockDto = {
id: UUID;
nodeId: UUID;
type: ContentBlockType;
title?: string;
body: Record<string, unknown>;
position: number;
required: boolean;
activityKind?: 'view' | 'quiz' | 'task' | 'submission' | 'project' | 'workbook';
taskBankProblemRef?: TaskBankProblemRefDto;
maxScore?: number;
estimatedMinutes?: number;
};

type TaskBankProblemRefDto = {
problemId: UUID;
variantId?: UUID;
revisionId?: UUID;
displayMode: 'inline' | 'link' | 'embedded_checker';
};

type RoadmapTopicRefDto = {
topicId: UUID;
relation: 'covers' | 'practices' | 'reviews' | 'extends';
weight?: number;
};

Rules DTO

type UnlockRuleDto = {
kind: 'always' | 'after_nodes_completed' | 'after_date' | 'manual' | 'custom';
requiredNodeIds?: UUID[];
opensAt?: ISODateTime;
expression?: Record<string, unknown>;
};

type CompletionRuleDto = {
kind: 'manual' | 'required_blocks' | 'required_activities' | 'score_threshold' | 'custom';
requiredBlockIds?: UUID[];
requiredActivityBlockIds?: UUID[];
minScore?: number;
expression?: Record<string, unknown>;
};

Правила хранятся как JSON, но backend должен валидировать discriminated union по kind.

Roadmap DTO

type RoadmapLayer =
| 'core'
| 'supplemental'
| 'free_entry'
| 'gap_closing'
| 'event'
| 'competition'
| 'enrichment'
| 'diagnostic'
| 'individual';

type RoadmapAudience = 'public' | 'student' | 'parent' | 'teacher' | 'management';
type TopicProgressState =
| 'not_started'
| 'available'
| 'in_progress'
| 'ready_for_completion'
| 'gap'
| 'completed'
| 'enrichment';

type RoadmapProjection = {
program: RoadmapProgramView;
modules: RoadmapModuleView[];
audience: RoadmapAudience;
filtersApplied: RoadmapProjectionFilter;
generatedAt: ISODateTime;
sourceVersion: string;
};

type RoadmapProjectionFilter = {
subjectKey?: string;
levelKey?: string;
formatKey?: string;
layer?: RoadmapLayer;
trackRef?: string;
availability?: string;
scheduledState?: 'not_started' | 'open' | 'running' | 'finished';
sort?: 'position' | 'recommended' | 'availability' | 'scheduled_start';
};

type RoadmapProgramView = {
id: UUID;
slug: string;
title: string;
subjectKey: string;
levelKey?: string;
supportsTracks: boolean;
};

type RoadmapModuleView = {
id: UUID;
title: string;
position: number;
topics: RoadmapTopicView[];
entryRequirements?: EntryRequirementView[];
catchup?: CatchupView;
};

type RoadmapTopicView = {
id: UUID;
code: string;
title: string;
difficulty?: string;
layer: RoadmapLayer;
prerequisiteTopicRefs: UUID[];
progress?: {
state: TopicProgressState;
masteryScore?: number;
completedAt?: ISODateTime;
};
pathways: TopicPathwayView[];
};

type TopicPathwayView = {
id: UUID;
formatKey: string;
targetLevelKey?: string;
roadmapLayer: RoadmapLayer;
recommendedLmsRefs: RoadmapLmsRef[];
recommendedTaskRefs: RoadmapTaskRef[];
productRefs: ProductRef[];
productRunRefs: ProductRunRef[];
groupRefs: GroupRef[];
sessionRefs: SessionRef[];
trackRef?: string;
availabilityState: 'available' | 'not_available' | 'enrolled' | 'purchased' | 'free' | 'waitlist' | 'closed';
completionContribution: 'none' | 'evidence_only' | 'partial' | 'full_if_rule_allows';
isRecommended: boolean;
scheduledState?: ScheduledStateView;
};

type EntryRequirementView = {
text: string;
importance: 'required' | 'recommended' | 'optional';
whereToCover?: ProductRef[];
diagnosticRef?: string;
scopeType: 'product' | 'product_run' | 'course' | 'roadmap_module' | 'roadmap_program';
scopeRef: string;
displayOrder: number;
};

type CatchupView = {
text: string;
severity: 'none' | 'soft' | 'recommended' | 'strong';
appliesToTrackRef?: string;
appliesToProductRunRef?: string;
};

type RoadmapLmsRef = {
type: 'course' | 'course_version' | 'node' | 'group' | 'session' | 'track';
id: UUID | string;
};

type RoadmapTaskRef = {
type: 'problem' | 'problem_set' | 'activity_template';
id: UUID | string;
};

type ProductRef = {
productId: UUID | string;
slug?: string;
};

type ProductRunRef = {
productRunId: UUID | string;
startsAt?: ISODateTime;
};

type GroupRef = {
groupId: UUID | string;
title?: string;
};

type SessionRef = {
sessionId: UUID | string;
startsAt?: ISODateTime;
};

type ScheduledStateView = {
moduleTitle?: string;
topicTitle?: string;
lessonNo?: number;
lessonsInModule?: number;
trackRef?: string;
canJoinNow: boolean;
catchupRequired: boolean;
};

type ProductTrajectoryBlockView = {
mode: 'static' | 'scheduled' | 'one_to_one';
productRef: ProductRef;
roadmapProgramRef: string;
moduleRefs: string[];
topicRefs: string[];
minimap: RoadmapModuleView[];
entryRequirements: EntryRequirementView[];
catchup?: CatchupView;
ctaPolicy: {
primary: 'buy' | 'enroll' | 'diagnostic' | 'join_waitlist' | 'contact';
secondary?: string[];
};
};

ProductTrajectoryBlockView является projection DTO для сборки storefront-блока. Каноническими источниками остаются LMS roadmap и CRM product/product run.

Enrollment DTO

type EnrollmentStatus = 'pending' | 'active' | 'paused' | 'completed' | 'revoked';

type EnrollmentDto = {
id: UUID;
studentProfileId: UUID;
courseId: UUID;
courseVersionId: UUID;
source: 'manual' | 'crm_entitlement' | 'competition' | 'migration';
sourceRef: Record<string, unknown>;
status: EnrollmentStatus;
startedAt?: ISODateTime;
completedAt?: ISODateTime;
pausedAt?: ISODateTime;
revokedAt?: ISODateTime;
progress: ProgressSummaryDto;
};

type CreateEnrollmentRequest = {
studentProfileId: UUID;
courseId: UUID;
courseVersionId?: UUID;
source: 'manual' | 'crm_entitlement' | 'competition' | 'migration';
sourceRef?: Record<string, unknown>;
activateImmediately?: boolean;
};

type EnrollmentTransitionRequest = {
reason: string;
sourceRef?: Record<string, unknown>;
};

Progress DTO

type ProgressStatus = 'not_started' | 'in_progress' | 'completed' | 'needs_review';

type ProgressSummaryDto = {
status: ProgressStatus;
completionPercent: number;
scoreSummary: {
score?: number;
maxScore?: number;
passed?: boolean;
};
evidenceSummary: {
requiredBlocksCompleted: number;
requiredBlocksTotal: number;
requiredActivitiesCompleted: number;
requiredActivitiesTotal: number;
lastEvidenceType?: string;
};
lastActivityAt?: ISODateTime;
completedAt?: ISODateTime;
calculatedAt: ISODateTime;
};

type ProgressSnapshotDto = ProgressSummaryDto & {
id: UUID;
enrollmentId: UUID;
courseVersionId: UUID;
nodeId?: UUID;
};

type LearningEvidenceDto = {
id: UUID;
enrollmentId: UUID;
nodeId?: UUID;
contentBlockId?: UUID;
evidenceType: string;
sourceType: string;
sourceId?: UUID;
payload: Record<string, unknown>;
occurredAt: ISODateTime;
};

completionPercent должен быть в диапазоне 0..100.

Attempts

type AttemptStatus =
| 'started'
| 'submitted'
| 'checking'
| 'checked'
| 'accepted'
| 'returned'
| 'cancelled';

type ActivityAttemptDto = {
id: UUID;
enrollmentId: UUID;
nodeId: UUID;
contentBlockId?: UUID;
attemptNo: number;
status: AttemptStatus;
answer?: Record<string, unknown>;
score?: number;
maxScore?: number;
checkerSource?: 'task-bank' | 'teacher' | 'manual' | 'external';
checkerRef?: Record<string, unknown>;
startedAt: ISODateTime;
submittedAt?: ISODateTime;
checkedAt?: ISODateTime;
};

type StartAttemptRequest = {
enrollmentId: UUID;
nodeId: UUID;
contentBlockId: UUID;
};

type SubmitAttemptRequest = {
answer: Record<string, unknown>;
attachments?: AttachmentRefDto[];
};

Sensitive answer payload может быть скрыт из response для родителя или преподавателя, если политика предмета этого требует.

Для олимпиадного тренажёра или разбора ActivityAttemptDto остаётся учебной попыткой LMS. Связь с опубликованным competitions material хранится в checkerRef/targetRef, но response не должен выглядеть как competition_submission или competition_result.

Submissions and feedback

type SubmissionStatus = 'draft' | 'submitted' | 'in_review' | 'returned' | 'accepted' | 'reopened';

type SubmissionDto = {
id: UUID;
enrollmentId: UUID;
attemptId?: UUID;
sourceType: 'activity' | 'workbook' | 'project' | 'homework';
sourceId: UUID;
status: SubmissionStatus;
payload: Record<string, unknown>;
submittedAt?: ISODateTime;
feedback: FeedbackDto[];
};

type CreateSubmissionRequest = {
enrollmentId: UUID;
sourceType: 'activity' | 'workbook' | 'project' | 'homework';
sourceId: UUID;
payload: Record<string, unknown>;
};

type FeedbackDto = {
id: UUID;
submissionId: UUID;
authorUserId?: UUID;
authorType: 'teacher' | 'auto_checker' | 'support' | 'system';
statusDecision?: 'accepted' | 'returned' | 'needs_review';
score?: number;
rubric: Record<string, unknown>;
comment?: string;
visibleToStudent: boolean;
createdAt: ISODateTime;
};

type CreateFeedbackRequest = {
statusDecision: 'accepted' | 'returned' | 'needs_review';
score?: number;
rubric?: Record<string, unknown>;
comment?: string;
visibleToStudent?: boolean;
};

Teacher DTO

type TeacherScopeDto = {
assignmentId: UUID;
teacherUserId: UUID;
scopeType: 'course' | 'course_version' | 'cohort' | 'group' | 'learning_group' | 'enrollment' | 'project' | 'workbook' | 'booking_slot';
scopeId: UUID;
role: 'teacher' | 'checker' | 'mentor' | 'substitute';
status: 'active' | 'paused' | 'ended';
startsAt?: ISODateTime;
endsAt?: ISODateTime;
};

type ReviewQueueItemDto = {
submissionId: UUID;
enrollmentId: UUID;
studentProfileId: UUID;
courseId: UUID;
nodeId?: UUID;
sourceType: string;
submittedAt: ISODateTime;
priority: 'normal' | 'high' | 'overdue';
};

scopeType = 'group' относится к live delivery group. scopeType = 'learning_group' относится к Learning Workspace и не даёт автоматический доступ к LMS progress без отдельного учебного scope.

Learning Workspace DTO

type LearningGroupStatus = 'draft' | 'active' | 'archived';
type LearningGroupParticipantStatus = 'invited' | 'active' | 'paused' | 'removed';
type LearningGroupInviteStatus = 'created' | 'sent' | 'accepted' | 'expired' | 'cancelled' | 'revoked';
type LearningGroupAssignmentStatus = 'draft' | 'assigned' | 'in_progress' | 'completed' | 'cancelled' | 'expired';

type LearningWorkspaceScopeDto = {
organizationId: UUID;
permissions: Array<
| 'lms.learning-groups.read'
| 'lms.learning-groups.manage'
| 'lms.learning-group-participants.manage'
| 'lms.learning-group-invites.manage'
| 'lms.learning-group-assignments.assign'
>;
};

type LearningGroupDto = {
id: UUID;
organizationId: UUID;
ownerTeacherUserId: UUID;
title: string;
description?: string;
status: LearningGroupStatus;
participantsCount: number;
activeAssignmentsCount: number;
createdAt: ISODateTime;
updatedAt: ISODateTime;
archivedAt?: ISODateTime;
};

type CreateLearningGroupRequest = {
organizationId: UUID;
title: string;
description?: string;
};

type UpdateLearningGroupRequest = {
title?: string;
description?: string | null;
};

type LearningGroupParticipantDto = {
id: UUID;
groupId: UUID;
studentProfileId: UUID;
status: LearningGroupParticipantStatus;
source: 'manual' | 'invite' | 'organization_import' | 'competition_import';
addedByUserId: UUID;
joinedAt?: ISODateTime;
removedAt?: ISODateTime;
};

type AddLearningGroupParticipantRequest = {
studentProfileId: UUID;
source?: 'manual' | 'organization_import' | 'competition_import';
};

type LearningGroupInviteDto = {
id: UUID;
groupId: UUID;
targetType: 'student' | 'parent' | 'family_adult';
studentProfileId?: UUID;
identityInviteRef: Record<string, unknown>;
status: LearningGroupInviteStatus;
expiresAt?: ISODateTime;
acceptedAt?: ISODateTime;
};

type CreateLearningGroupInviteRequest = {
targetType: 'student' | 'parent' | 'family_adult';
studentProfileId?: UUID;
recipientRef: Record<string, unknown>;
expiresAt?: ISODateTime;
};

type LearningGroupAssignmentDto = {
id: UUID;
groupId: UUID;
targetDomain: 'lms' | 'task-bank' | 'competitions';
targetType: 'lms_course' | 'lms_lesson' | 'trainer' | 'competition_training' | 'competition_debrief' | 'task_bank_set';
targetRef: Record<string, unknown>;
participantFilter: { kind: 'all' } | { kind: 'participants'; participantIds: UUID[] };
status: LearningGroupAssignmentStatus;
dueAt?: ISODateTime;
createdByUserId: UUID;
createdAt: ISODateTime;
};

type CreateLearningGroupAssignmentRequest = {
targetDomain: 'lms' | 'task-bank' | 'competitions';
targetType: 'lms_course' | 'lms_lesson' | 'trainer' | 'competition_training' | 'competition_debrief' | 'task_bank_set';
targetRef: Record<string, unknown>;
participantFilter?: { kind: 'all' } | { kind: 'participants'; participantIds: UUID[] };
dueAt?: ISODateTime;
};

type LearningGroupProgressRowDto = {
participantId: UUID;
studentProfileId: UUID;
access: 'allowed' | 'denied';
denialReason?: 'no_teacher_assignment' | 'no_enrollment' | 'inactive_enrollment';
progress?: ProgressSummaryDto;
};

Learning Group DTOs never include canonical student profile fields as source of truth. UI enriches display names from identity read models after the same organization/family access checks.

Workbook DTO

type WorkbookDto = {
id: UUID;
enrollmentId: UUID;
nodeId?: UUID;
title: string;
status: 'active' | 'submitted' | 'reviewed' | 'archived';
latestRevision?: WorkbookRevisionDto;
};

type WorkbookRevisionDto = {
id: UUID;
workbookId: UUID;
revisionNo: number;
body: Record<string, unknown>;
authorUserId: UUID;
submitted: boolean;
createdAt: ISODateTime;
};

type SaveWorkbookRevisionRequest = {
body: Record<string, unknown>;
baseRevisionNo?: number;
};

Project DTO

type ProjectDto = {
id: UUID;
enrollmentId: UUID;
courseVersionId: UUID;
title: string;
status: 'not_started' | 'active' | 'submitted' | 'in_review' | 'completed' | 'returned' | 'paused' | 'cancelled';
milestones: ProjectMilestoneDto[];
};

type ProjectMilestoneDto = {
id: UUID;
projectId: UUID;
title: string;
position: number;
status: 'not_started' | 'active' | 'submitted' | 'in_review' | 'completed' | 'returned';
dueAt?: ISODateTime;
completedAt?: ISODateTime;
};

Booking DTO

type BookingSlotDto = {
id: UUID;
courseId?: UUID;
courseVersionId?: UUID;
nodeId?: UUID;
teacherUserId?: UUID;
startsAt: ISODateTime;
endsAt: ISODateTime;
capacity: number;
availableSeats: number;
status: 'available' | 'held' | 'booked' | 'cancelled' | 'completed' | 'missed';
};

type BookingDto = {
id: UUID;
slotId: UUID;
enrollmentId: UUID;
status: 'held' | 'booked' | 'cancelled' | 'completed' | 'missed';
bookedByUserId: UUID;
cancelledAt?: ISODateTime;
cancellationReason?: string;
};

Chat DTO

type ChatThreadDto = {
id: UUID;
contextType: 'course' | 'lesson' | 'enrollment' | 'project' | 'booking';
contextId: UUID;
status: 'open' | 'readonly' | 'closed' | 'archived';
participants: ChatParticipantDto[];
lastMessage?: ChatMessageDto;
unreadCount: number;
};

type ChatParticipantDto = {
studentProfileId: UUID;
role: 'student' | 'parent' | 'teacher' | 'coordinator' | 'moderator';
status: 'active' | 'left' | 'blocked';
};

type ChatMessageDto = {
id: UUID;
threadId: UUID;
senderUserId: UUID;
body?: string;
attachments: AttachmentRefDto[];
status: 'visible' | 'hidden' | 'deleted';
createdAt: ISODateTime;
editedAt?: ISODateTime;
};

type SendChatMessageRequest = {
body?: string;
attachments?: AttachmentRefDto[];
};

Attachment ref

type AttachmentRefDto = {
storageObjectId: UUID;
fileName: string;
mimeType: string;
sizeBytes: number;
checksum?: string;
};

Webhook contracts

CRM entitlement

type CrmEntitlementWebhook = {
messageId: UUID;
messageKind: 'event';
messageType: 'crm.entitlement.activated' | 'crm.entitlement.suspended' | 'crm.entitlement.resumed' | 'crm.entitlement.expired' | 'crm.entitlement.revoked';
occurredAt: ISODateTime;
entitlementId: UUID;
studentProfileId: UUID;
courseId: UUID;
courseVersionId?: UUID;
reason?: string;
};

Task-bank check result

type TaskBankCheckResultWebhook = {
messageId: UUID;
messageKind: 'event';
messageType: 'task-bank.answer_check.completed';
attemptId: UUID;
problemId: UUID;
variantId?: UUID;
status: 'accepted' | 'returned' | 'needs_review';
score?: number;
maxScore?: number;
checkerRef: Record<string, unknown>;
occurredAt: ISODateTime;
};

Ошибки валидации

API должен возвращать field-level details:

type ValidationErrorDetails = {
fields: Array<{
path: string;
code: string;
message: string;
}>;
};

Примеры:

FieldCodeСмысл
courseVersionIdimmutable_versionpublished version нельзя менять
contentBlock.bodyinvalid_block_schemabody не соответствует типу блока
enrollmentIdinactive_enrollmentнельзя выполнить действие
slotIdcapacity_exceededмест нет
threadIdnot_participantactor не участник чата