Skip to content

Credit System

GRANT → credits added when enrollment activated (payment confirmed)
RESERVE → credit blocked when session booked
CONSUME → credit used when session completed or penalty applied
RELEASE → credit returned when session cancelled within policy
EXPIRE → credits expired (enrollment validity period)

The credit_ledger table is immutable and append-only. Every credit operation creates a new entry.

idempotencyKey with partial UNIQUE index (WHERE NOT NULL). Format:

RESERVE:{enrollmentId}:{sessionId}:{studentId}
CONSUME:{enrollmentId}:{sessionId}:{studentId}
RELEASE:{enrollmentId}:{sessionId}:{studentId}
GRANT:{enrollmentId}:{reference}
EXPIRE:{enrollmentId}:{minuteKey}

Duplicate inserts are caught via PG error 23505 and silently skipped.

Never update enrollment counters directly. Always use SessionCreditHandler:

// On session booked → reserve credit
await SessionCreditHandler.onReserve(enrollmentId, sessionId, studentId);
// On session completed → consume credit
await SessionCreditHandler.onCompleted(enrollmentId, sessionId, studentId);
// On session cancelled → release credit
await SessionCreditHandler.onCanceled(enrollmentId, sessionId, studentId);
// On no-show → consume credit (penalty)
await SessionCreditHandler.onNoShow(enrollmentId, sessionId, studentId);

The handler orchestrates:

  1. CreditLedgerService — inserts ledger entry (with idempotency)
  2. EnrollmentService — updates session counters

Belt-and-suspenders: pre-checks for existing entries before calling the ledger.

sessionsTotal = total sessions purchased
sessionsScheduled = booked/reserved (not yet completed)
sessionsCompleted = completed
sessionsCancelled = cancelled (credit returned)
sessionsForfeited = forfeited (no-show, late cancel — credit consumed)
remaining = sessionsTotal - sessionsScheduled - sessionsCompleted
- sessionsCancelled - sessionsForfeited

Per-service rules stored in cancellation_policies table (JSONB rules column).

Resolution cascade:

  1. Service-specific policy
  2. Teacher default policy (isDefault=true)
  3. Null (allow all)

evaluateCancellation() builds context and evaluates rules in order (first match wins):

Context:

  • hoursBefore — hours until session starts
  • sessionsCompleted — total completed sessions
  • sessionsRemaining — credits left
  • rescheduleCount — times rescheduled

Actions:

ActionEffect
full_refundRELEASE credit
partial_penaltyCONSUME partial credit
forfeitCONSUME full credit
blockCancellation not allowed
allow_freeRELEASE credit
TemplateDescription
individual_relaxedFlexible for 1:1 sessions
group_strictStrict for group classes
course_no_refundNo refund for courses
subscription_flexibleFlexible for subscriptions

Dry-run endpoints show the student what will happen before confirming:

GET /student/sessions/:id/cancellation-preview
GET /student/sessions/:id/reschedule-preview