Skip to content

Domain Settlement

Guard duplicated from ARCHITECTURE §18.4 — ARCHITECTURE wins on conflict.

⚠️ SILENT DATA CORRUPTION: Wrong trigger set = phantom or missing settlements. Non-atomic impl = orphaned session status or orphaned settlement row on partial failure.

Trigger: ONLY these two terminal transitions

Section titled “Trigger: ONLY these two terminal transitions”
  • lesson_session → COMPLETED
  • lesson_session → NO_SHOW_STUDENT

Do NOT auto-create settlements for any other terminal state (REJECTED, CANCELLED, RESCHEDULED, NO_SHOW_TUTOR).

Both INSERTs happen in the same DB transaction as the session status update:

await db.transaction(async (tx) => {
await tx.update(lesson_sessions).set({ status: 'COMPLETED' }).where(...);
await tx.insert(student_settlements).values({
lesson_session_id: session.id,
student_id: session.student_id,
billing_period_id: /* OPEN billing_period covering session.scheduled_at */,
gross_amount_idr: session.amount_student_final_idr,
net_amount_idr: session.amount_student_final_idr, // gross = net at creation; adjustments come later
});
await tx.insert(tutor_settlements).values({
lesson_session_id: session.id,
tutor_id: session.tutor_id,
billing_period_id: /* same OPEN period */,
honor_amount_idr: session.amount_tutor_final_idr,
net_amount_idr: session.amount_tutor_final_idr, // recomputed after any adjustments
});
});

Find the OPEN billing_period whose starts_on ≤ session.scheduled_at ≤ ends_on. If none found, surface an error — do not create settlement without a period.

Full spec: plans/ARCHITECTURE.md §18.4.