Tuition 모듈
학원 수강료(학원비) 수납 전 과정을 담당하는 모듈입니다. 보호자 정보 관리, 청구서(Invoice) 발행, 결제(Payment) 추적, 현금영수증 처리, 연체 자동 감지를 포함합니다.
모듈 개요
| 항목 | 내용 |
|---|---|
| Gradle 서브프로젝트 | modules/tuition |
| 베이스 패키지 | com.lumie.tuition |
| 데이터베이스 | PostgreSQL (멀티테넌트 RLS) |
| 외부 결제 포트 | TuitionBillingGatewayPort (PaySsam / 결제선생 seam) |
| 스케줄러 | OverdueNotificationScheduler (매일 09:00 KST) |
주요 책임
- 보호자(Guardian) 등록·수정·삭제 및 학생-보호자 연결
- 수강료 청구서(TuitionInvoice) 발행·취소
- 결제(TuitionPayment) 등록·캡처·환불
- 현금영수증(CashReceipt) 발행 (Popbill 연동)
- 매일 09:00에 연체 청구서(
ISSUED→OVERDUE) 자동 감지
결제 게이트웨이 seam
직접 Toss 수강료 결제 연동은 제거되었습니다(ff9db43). 현재 외부 결제는 TuitionBillingGatewayPort를 통해 추상화되어 있으며, 기본 구현체는 NotConfiguredTuitionBillingGateway(오류 발생)입니다. PaySsam(결제선생) 어댑터로 단일 파일 교체 시 즉시 라이브 전환이 가능합니다.
public interface TuitionBillingGatewayPort {
// 보호자 핸드폰으로 청구서 발송 (orderId: "TUITION-{invoiceId}-{suffix}")
SendInvoiceResult sendInvoice(String orderId, String guardianPhone, String guardianName,
String title, BigDecimal amount, Instant dueDate);
// 외부 청구서 수납 상태 조회
CollectionStatusResult queryStatus(String externalInvoiceId);
}
이 포트의 구현체는 반드시
@Transactional경계 밖에서 호출해야 합니다 (HARD_RULES_CROSS).
도메인 모델
Guardian 엔티티
@Entity @Table(name = "guardians")
public class Guardian extends TenantScopedEntity {
Long id; @Version Long version;
String name;
String phone; // 010-XXXX-XXXX
String email;
String relationship; // 부, 모, 조부모 등
}
StudentGuardian 연결 테이블
student_guardians 테이블이 학생과 보호자를 N:M으로 연결합니다. isPrimary 플래그로 주 보호자를 지정합니다.
TuitionInvoice 엔티티
@Entity @Table(name = "tuition_invoices")
public class TuitionInvoice extends TenantScopedEntity {
Long id; @Version Long version;
Long studentId;
Long guardianId;
Long classEnrollmentId;
String title;
List<LineItem> items; // 항목별 금액 (JSONB)
Money totalAmount;
Money vatAmount;
TuitionInvoiceStatus status; // DRAFT | ISSUED | PAID | OVERDUE | CANCELLED
Instant issuedAt;
Instant dueDate;
Instant paidAt;
Instant cancelledAt;
String cancellationReason;
String idempotencyKey; // 고유 인덱스 — 중복 발행 방지
}
TuitionInvoiceStatus 상태 전이
DRAFT → ISSUED → PAID
↘ OVERDUE → PAID
DRAFT / ISSUED / OVERDUE → CANCELLED
TuitionPayment 엔티티
@Entity @Table(name = "tuition_payments")
public class TuitionPayment extends TenantScopedEntity {
Long id; @Version Long version;
Long tuitionInvoiceId;
String orderId; // 고유 인덱스 (TUITION-{invoiceId}-...)
String pgTransactionId;
Money amount;
TuitionPaymentMethod method; // CARD | CASH | TRANSFER | VIRTUAL_ACCOUNT 등
TuitionPaymentStatus status; // PENDING | CAPTURED | FAILED | CANCELLED
Instant capturedAt;
Money refundedAmount;
String refundReason;
Instant refundedAt;
Long refundedByStaffId;
}
CashReceipt 엔티티
cash_receipts 테이블 — Popbill 현금영수증 발행 이력을 저장합니다.
REST API
보호자 API — 기본 경로: /v1/guardians
| 메서드 | 경로 | 설명 |
|---|---|---|
POST | /v1/guardians | 보호자 등록 (201 Created) |
GET | /v1/guardians/\{id\} | 보호자 상세 조회 |
GET | /v1/guardians | 보호자 목록 (페이지) |
PATCH | /v1/guardians/\{id\} | 보호자 정보 수정 |
DELETE | /v1/guardians/\{id\} | 보호자 삭제 |
POST | /v1/guardians/\{guardianId\}/link | 학생-보호자 연결 (?studentId=&isPrimary=) |
DELETE | /v1/guardians/\{guardianId\}/link | 학생-보호자 연결 해제 (?studentId=) |
청구서 API — 기본 경로: /v1/tuition-invoices
| 메서드 | 경로 | 설명 |
|---|---|---|
POST | /v1/tuition-invoices | 청구서 발행 (201, Idempotency-Key 지원) |
GET | /v1/tuition-invoices/\{id\} | 청구서 상세 조회 |
GET | /v1/tuition-invoices | 청구서 목록 (?studentId= 필터, 페이지) |
POST | /v1/tuition-invoices/\{id\}/cancel | 청구서 취소 |
정렬 허용 필드
createdAt, dueDate, totalAmount, status
결제 API — 기본 경로: /v1/tuition-payments
| 메서드 | 경로 | 설명 |
|---|---|---|
POST | /v1/tuition-payments | 결제 등록 (201, Idempotency-Key 지원) |
GET | /v1/tuition-payments/\{id\} | 결제 상세 조회 |
GET | /v1/tuition-payments | 결제 목록 (페이지) |
POST | /v1/tuition-payments/\{id\}/refund | 환불 처리 |
스케줄러
OverdueNotificationScheduler는 ShedLock(tuition-overdue-notify)으로 보호되어 멀티파드 환경에서 중복 실행을 방지합니다.
cron = "0 0 9 * * *" (매일 09:00 KST)
lockAtMostFor = PT20M
lockAtLeastFor = PT1M
모든 활성 테넌트를 순회하며 ISSUED 상태에서 dueDate가 경과한 청구서를 OVERDUE로 전환합니다. 원장(OWNER) 알림 디스패치는 Task 5에서 NotificationService 연동 예정입니다.
멀티테넌시
guardians, student_guardians, tuition_invoices, tuition_payments, cash_receipts 테이블 모두 tenant_id BIGINT NOT NULL + RLS 정책이 적용됩니다.
관련 문서
- Student 모듈 — 청구서 대상 학생 관리
- Class 모듈 — classEnrollmentId 연계
- Billing 모듈 — 구독 플랜·할당량 관리
- Notification 모듈 — 연체 알림 발송 예정