Skip to main content

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

  • statementId is 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_at order.
  • Inbox dedupes by event_id; all processing is transactional with inbox insert.

7. Partition Key

  • partitionKey = enrollmentId to 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 seq or timestamp.

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.