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;
}