Events
:::info Source
Sourced from services/progress-service/EVENT_SCHEMAS.md in the documentation repo.
:::
1. Envelope
All events use the platform envelope (04-event-driven-architecture.md §4). retentionClass = regulated (180 days hot, 7 years cold — LRS audit).
2. Events Published
2.1 progress.statement.stored.v1
interface StatementStoredV1 {
statementId: StatementId;
attemptId: AttemptId;
enrollmentId: EnrollmentId;
userId: UserId;
courseVersionId: CourseVersionId;
verbId: string;
timestamp: ISODate;
stored: ISODate;
}
2.2 progress.attempt.started.v1
interface AttemptStartedV1 {
attemptId: AttemptId;
enrollmentId: EnrollmentId;
userId: UserId;
courseVersionId: CourseVersionId;
attemptNumber: number;
startedAt: ISODate;
}
2.3 progress.attempt.closed.v1
interface AttemptClosedV1 {
attemptId: AttemptId;
enrollmentId: EnrollmentId;
userId: UserId;
outcome: 'passed' | 'failed' | 'incomplete' | 'abandoned';
score?: number;
durationSeconds: number;
endedAt: ISODate;
}
2.4 progress.completion.recorded.v1 (integration event)
interface CompletionRecordedV1 {
completionRecordId: ULID;
attemptId: AttemptId;
enrollmentId: EnrollmentId;
userId: UserId;
courseId: CourseId;
courseVersionId: CourseVersionId;
completedAt: ISODate;
score: number;
passed: boolean;
evidenceStatementIds: StatementId[];
}
Consumers: certification-service (issue cert), assignment-service (close compliance window), analytics-service (cohort progress), notification-service (learner congrats).
2.5 progress.score.recorded.v1
interface ScoreRecordedV1 {
attemptId: AttemptId;
userId: UserId;
enrollmentId: EnrollmentId;
score: number;
gradingRule: string;
recordedAt: ISODate;
}
3. Events Consumed
3.1 delivery.play_session.started.v1
Creates Attempt if not already exists for (enrollmentId, attemptNumber).
3.2 delivery.play_session.navigated.v1
Emits internal http://adlnet.gov/expapi/verbs/experienced statement for the block.
3.3 delivery.play_session.completed.v1
Closes Attempt; if outcome passed → progress.completion.recorded.v1.
3.4 assessment.attempt_result.scored.v1
Emits statement with verb = passed | failed, result.score.
3.5 gdpr.subject_request.received.v1
Triggers GDPR erasure of statements, attempts, completions for user.
3.6 Sync replayed offline statements
POST /xapi/statements via sync delta; idempotent on statementId.
4. Event Versioning
- v1 schemas frozen (additive).
- Add optional field → same v1.
- Add required field → v2 with dual-publish ≥ 1 milestone.
- Remove/rename → v2 + deprecation.
5. Idempotency
statementIdis PK in DB; duplicate inserts are no-ops.- Consumer inbox (generic pattern §6 of event-driven doc) dedupes events.
6. Outbox / Inbox
-- outbox
CREATE TABLE progress.outbox (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
occurred_at TIMESTAMPTZ NOT NULL DEFAULT now(),
tenant_id UUID NOT NULL,
topic TEXT NOT NULL,
envelope JSONB NOT NULL,
published_at TIMESTAMPTZ,
attempts INT DEFAULT 0,
last_error TEXT
);
-- inbox
CREATE TABLE progress.inbox (
event_id ULID PRIMARY KEY,
consumer TEXT NOT NULL,
processed_at TIMESTAMPTZ DEFAULT now(),
result TEXT
);
- Outbox relay publishes events in
occurred_atorder. - Inbox dedupes by
event_id; all processing is transactional with inbox insert.
7. Partition Key
partitionKey = enrollmentIdto preserve ordering per learner.- NATS subject:
progress.statement.stored.v1.{enrollmentIdHash[0-9a-f]{2}}for partitioning by hash bucket.
8. DLQ Handling
progress.dlq: malformed schemas, processing exceptions.- Alert on non-empty.
- Poison-message recovery: fix schema, replay from
seqortimestamp.
9. AsyncAPI hub (CI)
Machine-readable published subjects: Ghasi-edTech/packages/contracts/asyncapi/progress-service.asyncapi.yaml (AsyncAPI 2.6, validated in PR via pnpm --filter @ghasi/contracts validate). Add a channel and payload stub there whenever you add a new outbox topic; keep field-level truth in this document and in JSON Schema if split later.