수강료 서비스
이 페이지는 보호자, invoice, 수납, 환불, 현금영수증 기록을 담당하는
테넌트 범위 billing 모듈 lumie-backend/modules/tuition의 레퍼런스입니다.
소스 경로
| 경로 | 역할 |
|---|---|
lumie-backend/modules/tuition/src/main/java/com/lumie/tuition/adapter/in/web/{GuardianController,TuitionInvoiceController,TuitionPaymentController}.java | 공개 HTTP 인터페이스 |
lumie-backend/modules/tuition/src/main/java/com/lumie/tuition/application/service/{GuardianCommandService,TuitionInvoiceCommandService,TuitionPaymentCommandService}.java | 주요 write-side 애플리케이션 서비스 |
lumie-backend/modules/tuition/src/main/java/com/lumie/tuition/adapter/out/external/{NotConfiguredTuitionBillingGateway,PopbillCashReceiptClient}.java | 외부 payment gateway 및 cash receipt adapter |
lumie-backend/modules/tuition/src/main/java/com/lumie/tuition/adapter/in/internal/TuitionServiceAdapter.java | 공개 계약 구현이 아니라 아직 placeholder인 현재 internal adapter |
lumie-backend/modules/tuition/src/main/java/com/lumie/tuition/domain/entity/{Guardian,StudentGuardian,TuitionInvoice,TuitionPayment,CashReceipt}.java | tuition aggregate와 persistence-backed entity |
lumie-backend/app/src/main/resources/db/migration/public/V29__tuition_tenant_tables.sql | 보호자, link, invoice, payment, cash receipt에 대한 source-of-truth schema |
lumie-backend/app/src/main/resources/db/migration/public/V30__student_guardians_add_version.sql | 보호자 link table의 optimistic-lock 후속 변경 |
공개 인터페이스
| 엔드포인트 | 목적 |
|---|---|
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 | guardian을 student에 연결하거나 해제 |
POST /v1/tuition-invoices, GET /v1/tuition-invoices, GET /v1/tuition-invoices/{id}, POST /v1/tuition-invoices/{id}/cancel | invoice 발행, 목록, 조회, 취소 |
POST /v1/tuition-payments, GET /v1/tuition-payments, GET /v1/tuition-payments/{id}, POST /v1/tuition-payments/{id}/refund | 결제 요청, 목록, 조회, 환불 기록 |
invoice 및 payment 생성 엔드포인트에서 Idempotency-Key는 선택 사항입니다.
값이 있으면 반복 제출을 결정적으로 만들지만, billing 모듈이 쓰는 공용
IdempotencyService interceptor 패턴은 사용하지 않습니다.
내부 인터페이스와 경계
| 인터페이스 | 코드 기준 실제 상태 |
|---|---|
TuitionServiceAdapter | 자리표시자 컴포넌트일 뿐이며, 아직 공개된 libs/internal-api interface를 구현하지 않음 |
TuitionBillingGatewayPort | 외부 invoice 수납과 수납 상태 조회를 위한 추상화 |
CashReceiptPort | 현금영수증 발행을 위한 추상화 |
| Cross-module references | guardian_id, student_id, class_enrollment_id, refunded_by_staff_id는 관례상 soft reference이며, migration comment는 모듈 경계를 가로지르는 DB foreign key를 명시적으로 피함 |
집계와 테이블
| 집계 | 참고 |
|---|---|
Guardian | 부모 또는 기타 보호자의 연락처 정보 |
StudentGuardian | student와 guardian을 연결하고 대표 연락처 여부를 표시하는 join table |
TuitionInvoice | DRAFT, ISSUED, PAID, OVERDUE, CANCELLED 수명주기 |
TuitionPayment | PENDING, CAPTURED, FAILED, CANCELLED 수납 기록 |
CashReceipt | 개인 또는 사업자 증빙용 영수증 발행 기록 |
런타임 흐름
TuitionPaymentCommandService.requestPayment(...)는 외부 gateway를 호출하기
전에 대기 중인 결제를 의도적으로 커밋합니다. 백엔드가 네트워크 왕복 동안
DB 트랜잭션을 붙잡고 있지 않도록 하기 위해서입니다.
계약 참고 사항
외부 수납 경로는 아직 완전히 연결되지 않았습니다.
// 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도 아직 placeholder이기 때문에, 다른 모듈이 소비할 수
있는 안정적인 프로세스 내부 tuition API는 아직 없습니다.
예시 계약
이 예시는 TuitionPaymentController, RequestPaymentRequest,
TuitionPaymentResponse,
TuitionPaymentCommandService.recordCollection(...),
TuitionBillingGatewayPort에서 직접 가져왔습니다.
수강료 결제 요청
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>"
}
수금 콜백 페이로드 형태
현재 이 모듈에는 공개 webhook controller가 없습니다. 향후 gateway adapter가
외부 callback을
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"
}
실패, 재시도, 관측성
TuitionInvoiceCommandService.issueInvoice(...)는 재사용된 invoice idempotency key를IDEMPOTENCY_CONFLICT로 거부합니다.TuitionPaymentCommandService.requestPayment(...)는 idempotency key가 있을 때 결정적인orderId를 만들므로, 중복 제출이 같은 downstream 결제 identity를 가리킬 수 있습니다.recordCollection(...)은 이미 캡처된 결제에 대해 멱등이며, amount mismatch를 확인한 뒤에만 invoice를 paid로 표시합니다.NotConfiguredTuitionBillingGateway는 결제 전송 시 즉시 예외를 던지므로, 실제 gateway adapter가 연결되기 전까지 결제 요청은 운영할 수 없습니다.PopbillCashReceiptClient는 실제 영수증을 발행하지 않고 실패 결과를 반환하는 stub입니다.- 현재 이 모듈에는 수납 callback용 공개 webhook controller가 없습니다.
recordCollection(...)은 존재하지만, 다른 adapter가 이를 호출해야 합니다.
검증
cd lumie-backend
./gradlew :modules:tuition:test
./gradlew :modules:tuition:test --tests '*TuitionInvoice*'
./gradlew :modules:tuition:test --tests '*TuitionPayment*'
예상 성공 신호:
- Gradle이
BUILD SUCCESSFUL로 종료되고, tuition 테스트가 여전히 invoice와 payment command 흐름을 다룹니다. TuitionPaymentCommandService.requestPayment(...)가 여전히 idempotency key에서 결정적인orderId를 만들고,recordCollection(...)이 중복 콜백에서 이미 캡처된 결제를 변경 없이 반환합니다.