Application Logic
:::info Source
Sourced from services/delivery-service/APPLICATION_LOGIC.md in the documentation repo.
:::
Companion: DOMAIN_MODEL · API_CONTRACTS · EVENT_SCHEMAS
1. Use Cases
1.1 StartPlaySession
Trigger: POST /api/v1/play-sessions
Flow:
- Validate JWT claims and extract
tenantId,userId,deviceId. - Call
EnrollmentClient.validateEnrollment(enrollmentId)— reject if not active. - Check for existing active session for
(userId, courseVersionId, deviceId)— if found, force-pause it. - Resolve
attemptNumber— query max attempt for(userId, enrollmentId)and increment. - Fetch PlayPackage manifest via
ContentClient.getPlayPackageManifest(courseVersionId). - Construct initial
NavigationCursorpointing to first module/lesson. - Create
PlaySessionaggregate ininitstate, transition toactive. - Persist via
PlaySessionRepository.save()inside transaction with outbox event. - Emit
delivery.play_session.started.v1. - Return session state with cursor and manifest metadata.
Error Conditions:
404— Enrollment not found403— Enrollment revoked or expired409— Rate limit on session creation (max 5 sessions per user per minute)422— Invalid courseVersionId or missing PlayPackage
1.2 Navigate
Trigger: PATCH /api/v1/play-sessions/{id}/navigate
Flow:
- Load
PlaySessionby id; verify ownership (userId + tenantId match JWT). - Validate session is in
activestate. - Parse
NavigationTargetfrom request body. - Call
NavigationService.resolve(currentCursor, target, manifest). - If target is quiz-gated, check gate status via cached assessment result or event.
- Update cursor on PlaySession aggregate.
- Update
lastActivityAt. - Persist with outbox event.
- Emit
delivery.play_session.navigated.v1. - Return updated session state.
Error Conditions:
404— Session not found403— Not session owner409— Session not inactivestate422— Navigation target unreachable (prerequisite not met, invalid node)
1.3 PausePlaySession
Trigger: POST /api/v1/play-sessions/{id}/pause
Flow:
- Load session; verify ownership.
- Validate state is
active. - Transition to
paused. - Persist with outbox event.
- Emit
delivery.play_session.paused.v1.
1.4 CompletePlaySession
Trigger: POST /api/v1/play-sessions/{id}/complete
Flow:
- Load session; verify ownership.
- Validate state is
active. - Evaluate completion rules from PlayPackage manifest (all required lessons visited, all gates passed).
- If rules not satisfied, return
422with details of unmet requirements. - Transition to
completed; setendedAt. - Calculate
durationSeconds. - Persist with outbox event.
- Emit
delivery.play_session.completed.v1.
1.5 AbandonPlaySession
Trigger: POST /api/v1/play-sessions/{id}/abandon or automatic via inactivity timeout
Flow:
- Load session; verify ownership.
- Validate state is
activeorpaused. - Transition to
abandoned; setendedAtand reason. - Persist with outbox event.
- Emit
delivery.play_session.abandoned.v1.
1.6 GetSessionState
Trigger: GET /api/v1/play-sessions/{id}/state
Flow:
- Load session; verify ownership.
- Return
SessionSummaryprojection. - Cache result in Redis (TTL 30s) keyed by
(tenantId, sessionId).
1.7 StreamTutorResponse (AI Tutor)
Trigger: POST /api/v1/play-sessions/{id}/tutor/turn
Response: SSE stream at GET /api/v1/play-sessions/{id}/tutor/stream
Flow:
- Load session; verify ownership; validate
activestate. - Extract current lesson context from cursor.
- Call
TutorOrchestrationService: a. Gather RAG context: current lesson content blocks, previous 5 turns in conversation. b. Build prompt with system instructions (scope to lesson, avoid curriculum drift). c. Determine inference path: online (AIClient → cloud) or offline (AIClient → local model). d. Stream response chunks via SSE. - On stream completion, create
AssistantTurnentity. - Persist turn; emit
delivery.play_session.tutor_turn.completed.v1.
Error Conditions:
404— Session not found503— AI service unavailable (graceful degradation message sent to client)429— Tutor rate limit (max 30 turns per session per hour)
1.8 RateAssistantTurn
Trigger: POST /api/v1/play-sessions/{id}/tutor/turn/{turnId}/rate (from API spec: handled within tutor turn flow)
Flow:
- Load session and turn; verify ownership.
- Update
AssistantTurn.rating. - Persist.
1.9 MountOfflineBundle
Trigger: POST /api/v1/play-sessions/{id}/mount-offline
Flow:
- Validate JWT; extract
deviceId. - Verify device is registered and trusted (via identity-service device binding).
- Validate license envelope: verify JWS signature, check expiry, confirm device binding.
- Verify bundle integrity via
ContentClient.validateBundleIntegrity(). - Create
OfflineMountrecord. - Persist with outbox event.
- Emit
delivery.offline_mounted.v1. - Return mount metadata (expiresAt, bundleId).
Error Conditions:
403— Device not trusted or license invalid409— Bundle already mounted on this device422— Bundle integrity check failed
1.10 UnmountOfflineBundle
Trigger: POST /api/v1/play-sessions/{id}/unmount-offline
Flow:
- Load mount; verify ownership.
- Set
unmountedAtandunmountReason. - Invalidate any active sessions referencing this mount.
- Persist with outbox event.
- Emit
delivery.offline_unmounted.v1.
2. Event Consumers
2.1 OnPlayPackageBuilt
Event: content.play_package.built.v1
Handler:
- Extract
courseVersionIdand manifest metadata. - Cache manifest in Redis for fast runtime access.
- Invalidate any stale manifest cache for previous versions.
2.2 OnEnrollmentCreated
Event: enrollment.created.v1
Handler:
- Store enrollment reference in local projection table.
- Pre-warm PlayPackage manifest cache if not already present.
2.3 OnBundlePublished
Event: content.play_package.bundle.published.v1
Handler:
- Update bundle availability projection.
- Mark bundle as available for offline mount.
2.4 OnBundleTamperDetected
Event: content.bundle.tamper_detected.v1
Handler:
- Find all OfflineMounts referencing the tampered bundle.
- Force unmount all affected mounts (set
unmountReason: 'tamper_detected'). - Pause any active sessions using those mounts.
- Emit
delivery.offline_unmounted.v1for each affected mount. - Log security alert.
2.5 OnAssessmentScored
Event: assessment.attempt_result.scored.v1 (consumed for quiz-gated navigation)
Handler:
- Extract quiz gate result.
- Update local gate-status projection for the session.
- If the gate was blocking a navigation request, the next navigate call will now succeed.
3. Command/Query Separation
3.1 Commands (Write Side)
| Command | Handler | Aggregate |
|---|---|---|
StartPlaySessionCommand | StartPlaySessionHandler | PlaySession |
NavigateCommand | NavigateHandler | PlaySession |
PauseSessionCommand | PauseSessionHandler | PlaySession |
CompleteSessionCommand | CompleteSessionHandler | PlaySession |
AbandonSessionCommand | AbandonSessionHandler | PlaySession |
RequestTutorTurnCommand | TutorTurnHandler | PlaySession + AssistantTurn |
MountOfflineCommand | MountOfflineHandler | OfflineMount |
UnmountOfflineCommand | UnmountOfflineHandler | OfflineMount |
3.2 Queries (Read Side)
| Query | Handler | Source |
|---|---|---|
GetSessionStateQuery | SessionStateHandler | PlaySession (Redis cache + DB fallback) |
ListSessionsQuery | ListSessionsHandler | PlaySession (DB) |
GetTutorHistoryQuery | TutorHistoryHandler | AssistantTurn (DB) |
GetOfflineMountsQuery | OfflineMountsHandler | OfflineMount (DB) |
4. Saga: Offline-to-Online Resume
When a device comes back online after an offline session:
┌──────────────────────────────────────────────────────────────┐
│ 1. sync-service delivers offline session mutations │
│ 2. Delivery validates each mutation against server state │
│ 3. If session exists online with divergent cursor: │
│ a. Server cursor wins for completed lessons │
│ b. Offline cursor wins for lessons not yet seen online │
│ c. Merge navigation history │
│ 4. Replay offline assistant turns (store only, no re-infer) │
│ 5. Emit reconciled events to progress-service │
│ 6. Mark sync complete │
└──────────────────────────────────────────────────────────────┘
5. Idempotency
- All write endpoints require
Idempotency-Keyheader (ULID). - Keys stored in Redis with 24h TTL.
- Duplicate requests return the original response without re-executing side effects.
- Event consumers use inbox table with
eventIdunique constraint for exactly-once application.
6. Concurrency Control
- PlaySession uses optimistic concurrency via
versionfield. PATCH /navigateincludesIf-Matchheader with current version.- Concurrent navigation attempts from the same user on different tabs receive
409 Conflictwith retry guidance.