Tuition Service
This is the reference page for lumie-backend/modules/tuition, the tenant-scoped billing module for guardians, invoices, collections, refunds, and cash-receipt records.
Source Paths
| Path | Role |
|---|---|
lumie-backend/modules/tuition/src/main/java/com/lumie/tuition/adapter/in/web/{GuardianController,TuitionInvoiceController,TuitionPaymentController}.java | Public HTTP surface |
lumie-backend/modules/tuition/src/main/java/com/lumie/tuition/application/service/{GuardianCommandService,TuitionInvoiceCommandService,TuitionPaymentCommandService}.java | Main write-side application services |
lumie-backend/modules/tuition/src/main/java/com/lumie/tuition/adapter/out/external/{NotConfiguredTuitionBillingGateway,PopbillCashReceiptClient}.java | External payment-gateway and cash-receipt adapters |
lumie-backend/modules/tuition/src/main/java/com/lumie/tuition/adapter/in/internal/TuitionServiceAdapter.java | Current internal adapter, still a placeholder rather than a published contract implementation |
lumie-backend/modules/tuition/src/main/java/com/lumie/tuition/domain/entity/{Guardian,StudentGuardian,TuitionInvoice,TuitionPayment,CashReceipt}.java | Tuition aggregates and persistence-backed entities |
lumie-backend/app/src/main/resources/db/migration/public/V29__tuition_tenant_tables.sql | Source-of-truth schema for guardians, links, invoices, payments, and cash receipts |
lumie-backend/app/src/main/resources/db/migration/public/V30__student_guardians_add_version.sql | Optimistic-locking follow-up for the guardian link table |
Public Surface
| Endpoint | Purpose |
|---|---|
POST /v1/guardians, GET /v1/guardians, GET /v1/guardians/{id}, PATCH /v1/guardians/{id}, DELETE /v1/guardians/{id} | Guardian CRUD |
POST /v1/guardians/{guardianId}/link, DELETE /v1/guardians/{guardianId}/link | Link or unlink a guardian to a student |
POST /v1/tuition-invoices, GET /v1/tuition-invoices, GET /v1/tuition-invoices/{id}, POST /v1/tuition-invoices/{id}/cancel | Invoice issue, listing, read, and cancellation |
POST /v1/tuition-payments, GET /v1/tuition-payments, GET /v1/tuition-payments/{id}, POST /v1/tuition-payments/{id}/refund | Payment request, listing, read, and refund recording |
Idempotency-Key is optional on the invoice and payment create endpoints. When supplied, the module uses it to make repeat submits deterministic, but it does not use the shared IdempotencyService interceptor pattern that billing uses.
Internal Surface And Boundaries
| Surface | Reality in code |
|---|---|
TuitionServiceAdapter | Placeholder component only; it does not implement a published libs/internal-api interface yet |
TuitionBillingGatewayPort | Abstraction for external invoice collection and collection-status lookup |
CashReceiptPort | Abstraction for cash-receipt issuance |
| Cross-module references | guardian_id, student_id, class_enrollment_id, and refunded_by_staff_id are soft references by convention; the migration comments explicitly avoid DB foreign keys across module boundaries |
Aggregates And Tables
| Aggregate | Notes |
|---|---|
Guardian | Contact information for parents or other responsible adults |
StudentGuardian | Join table linking students to guardians and marking the primary contact |
TuitionInvoice | DRAFT, ISSUED, PAID, OVERDUE, and CANCELLED invoice lifecycle |
TuitionPayment | PENDING, CAPTURED, FAILED, and CANCELLED collection record |
CashReceipt | Receipt issuance record for personal or business tax evidence |
Runtime Flow
TuitionPaymentCommandService.requestPayment(...) intentionally commits the pending payment before calling the external gateway so the backend does not hold a database transaction open across the network round-trip.
Contract Notes
The external collection path is not fully wired yet.
// lumie-backend/modules/tuition/src/main/java/com/lumie/tuition/adapter/out/external/NotConfiguredTuitionBillingGateway.java
@Override
public SendInvoiceResult sendInvoice(...) {
throw new TuitionException(TuitionErrorCode.GATEWAY_NOT_CONFIGURED,
"Tuition billing gateway is not configured. Wire the PaySsam adapter.");
}
TuitionServiceAdapter is also still a placeholder, so there is no stable in-process tuition API for other modules to consume yet.
Example Contracts
These examples come directly from TuitionPaymentController, RequestPaymentRequest, TuitionPaymentResponse, TuitionPaymentCommandService.recordCollection(...), and TuitionBillingGatewayPort.
Request A Tuition Payment
POST /v1/tuition-payments
Idempotency-Key: tuition-15-june
Content-Type: application/json
{
"invoiceId": 15,
"method": "CARD"
}
HTTP/1.1 201 Created
{
"id": 201,
"tuitionInvoiceId": 15,
"orderId": "TUITION-15-tuition15jun",
"pgTransactionId": null,
"amount": 150000,
"method": "CARD",
"status": "PENDING",
"capturedAt": null,
"failedAt": null,
"failureReason": null,
"refundedAmount": null,
"refundReason": null,
"refundedAt": null,
"refundedByStaffId": null,
"createdAt": "<timestamp>",
"updatedAt": "<timestamp>"
}
Collection Callback Payload Shape
There is no public webhook controller in this module today. The future gateway adapter still has to map an external callback onto TuitionPaymentCommandService.recordCollection(orderId, externalRef, amount, collectedAt).
{
"orderId": "TUITION-15-tuition15jun",
"externalRef": "payssam-collection-001",
"amount": 150000,
"collectedAt": "2026-06-14T12:34:56Z"
}
// recordCollection(...) returns TuitionPaymentResponse
{
"id": 201,
"tuitionInvoiceId": 15,
"orderId": "TUITION-15-tuition15jun",
"pgTransactionId": "payssam-collection-001",
"amount": 150000,
"method": "CARD",
"status": "CAPTURED",
"capturedAt": "2026-06-14T12:34:56Z"
}
Failure, Retry, And Observability
TuitionInvoiceCommandService.issueInvoice(...)rejects a reused invoice idempotency key withIDEMPOTENCY_CONFLICT.TuitionPaymentCommandService.requestPayment(...)derives a deterministicorderIdfrom the idempotency key when one is supplied, so duplicate submits can target the same downstream payment identity.recordCollection(...)is idempotent for already captured payments and rejects amount mismatches before marking the invoice paid.NotConfiguredTuitionBillingGatewaythrows immediately for payment dispatch, so payment requests are not operable until a real gateway adapter is wired.PopbillCashReceiptClientis a stub that returns a failed result rather than issuing a real receipt.- There is no public webhook controller in this module yet for collection callbacks;
recordCollection(...)exists, but another adapter still has to call it.
Verification
cd lumie-backend
./gradlew :modules:tuition:test
./gradlew :modules:tuition:test --tests '*TuitionInvoice*'
./gradlew :modules:tuition:test --tests '*TuitionPayment*'
Expected success signals:
- Gradle exits with
BUILD SUCCESSFUL, and the tuition tests still cover invoice and payment command flows. TuitionPaymentCommandService.requestPayment(...)still builds deterministicorderIdvalues from the idempotency key, andrecordCollection(...)still returns the already captured payment unchanged on duplicate callbacks.