본문으로 건너뛰기

수강료 서비스

이 페이지는 보호자, invoice, 수납, 환불, 현금영수증 기록을 담당하는 tenant 범위 billing 모듈 lumie-backend/modules/tuition의 레퍼런스입니다.

소스 경로

Path역할
lumie-backend/modules/tuition/src/main/java/com/lumie/tuition/adapter/in/web/{GuardianController,TuitionInvoiceController,TuitionPaymentController}.java공개 HTTP surface
lumie-backend/modules/tuition/src/main/java/com/lumie/tuition/application/service/{GuardianCommandService,TuitionInvoiceCommandService,TuitionPaymentCommandService}.java주요 write-side application service
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}.javatuition aggregate와 persistence-backed entity
lumie-backend/app/src/main/resources/db/migration/public/V29__tuition_tenant_tables.sqlguardian, link, invoice, payment, cash receipt에 대한 source-of-truth schema
lumie-backend/app/src/main/resources/db/migration/public/V30__student_guardians_add_version.sqlguardian link table의 optimistic-lock 후속 변경

공개 인터페이스

Endpoint목적
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}/linkguardian을 student에 연결하거나 해제
POST /v1/tuition-invoices, GET /v1/tuition-invoices, GET /v1/tuition-invoices/{id}, POST /v1/tuition-invoices/{id}/cancelinvoice 발행, 목록, 조회, 취소
POST /v1/tuition-payments, GET /v1/tuition-payments, GET /v1/tuition-payments/{id}, POST /v1/tuition-payments/{id}/refund결제 요청, 목록, 조회, 환불 기록

invoice 및 payment 생성 endpoint에서 Idempotency-Key는 선택 사항입니다. 값이 있으면 반복 제출을 결정적으로 만들지만, billing 모듈이 쓰는 공용 IdempotencyService interceptor 패턴은 사용하지 않습니다.

내부 인터페이스와 경계

Surface코드 기준 실제 상태
TuitionServiceAdapterplaceholder component일 뿐이며, 아직 공개된 libs/internal-api interface를 구현하지 않음
TuitionBillingGatewayPort외부 invoice 수납과 수납 상태 조회를 위한 추상화
CashReceiptPort현금영수증 발행을 위한 추상화
Cross-module referencesguardian_id, student_id, class_enrollment_id, refunded_by_staff_id는 관례상 soft reference이며, migration comment는 모듈 경계를 가로지르는 DB foreign key를 명시적으로 피함

집계와 테이블

Aggregate참고
Guardian부모 또는 기타 보호자의 연락처 정보
StudentGuardianstudent와 guardian을 연결하고 대표 연락처 여부를 표시하는 join table
TuitionInvoiceDRAFT, ISSUED, PAID, OVERDUE, CANCELLED lifecycle
TuitionPaymentPENDING, CAPTURED, FAILED, CANCELLED 수납 기록
CashReceipt개인 또는 사업자 증빙용 영수증 발행 기록

런타임 흐름

TuitionPaymentCommandService.requestPayment(...)는 외부 gateway를 호출하기 전에 pending payment를 의도적으로 commit합니다. 백엔드가 네트워크 왕복 동안 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(...)은 이미 capture된 payment에 대해 idempotent하며, 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(...)이 중복 callback에서 이미 capture된 payment를 변경 없이 반환합니다.

관련 페이지