Skip to main content

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

PathRole
lumie-backend/modules/tuition/src/main/java/com/lumie/tuition/adapter/in/web/{GuardianController,TuitionInvoiceController,TuitionPaymentController}.javaPublic HTTP surface
lumie-backend/modules/tuition/src/main/java/com/lumie/tuition/application/service/{GuardianCommandService,TuitionInvoiceCommandService,TuitionPaymentCommandService}.javaMain write-side application services
lumie-backend/modules/tuition/src/main/java/com/lumie/tuition/adapter/out/external/{NotConfiguredTuitionBillingGateway,PopbillCashReceiptClient}.javaExternal payment-gateway and cash-receipt adapters
lumie-backend/modules/tuition/src/main/java/com/lumie/tuition/adapter/in/internal/TuitionServiceAdapter.javaCurrent 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}.javaTuition aggregates and persistence-backed entities
lumie-backend/app/src/main/resources/db/migration/public/V29__tuition_tenant_tables.sqlSource-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.sqlOptimistic-locking follow-up for the guardian link table

Public Surface

EndpointPurpose
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}/linkLink 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}/cancelInvoice issue, listing, read, and cancellation
POST /v1/tuition-payments, GET /v1/tuition-payments, GET /v1/tuition-payments/{id}, POST /v1/tuition-payments/{id}/refundPayment 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

SurfaceReality in code
TuitionServiceAdapterPlaceholder component only; it does not implement a published libs/internal-api interface yet
TuitionBillingGatewayPortAbstraction for external invoice collection and collection-status lookup
CashReceiptPortAbstraction for cash-receipt issuance
Cross-module referencesguardian_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

AggregateNotes
GuardianContact information for parents or other responsible adults
StudentGuardianJoin table linking students to guardians and marking the primary contact
TuitionInvoiceDRAFT, ISSUED, PAID, OVERDUE, and CANCELLED invoice lifecycle
TuitionPaymentPENDING, CAPTURED, FAILED, and CANCELLED collection record
CashReceiptReceipt 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 with IDEMPOTENCY_CONFLICT.
  • TuitionPaymentCommandService.requestPayment(...) derives a deterministic orderId from 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.
  • NotConfiguredTuitionBillingGateway throws immediately for payment dispatch, so payment requests are not operable until a real gateway adapter is wired.
  • PopbillCashReceiptClient is 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 deterministic orderId values from the idempotency key, and recordCollection(...) still returns the already captured payment unchanged on duplicate callbacks.