Payment.java

package com.newbit.payment.command.domain.aggregate;

import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.EnumType;
import jakarta.persistence.Enumerated;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.PrePersist;
import jakarta.persistence.PreUpdate;
import jakarta.persistence.Table;
import lombok.AccessLevel;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

import java.math.BigDecimal;
import java.time.LocalDateTime;

@Entity
@Table(name = "payment")
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Payment {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long paymentId;

    @Column(nullable = false)
    private BigDecimal amount;
    
    @Column(name = "total_amount", nullable = false)
    private BigDecimal totalAmount;
    
    @Column(name = "balance_amount")
    private BigDecimal balanceAmount;

    @Enumerated(EnumType.STRING)
    @Column(nullable = false)
    private PaymentMethod paymentMethod;

    @Column(nullable = false, unique = true)
    private String orderId;
    
    @Column(name = "order_name")
    private String orderName;

    @Column(unique = true)
    private String paymentKey;

    @Column(nullable = false)
    private Long userId;

    @Enumerated(EnumType.STRING)
    @Column(nullable = false)
    private PaymentStatus paymentStatus;

    @Column
    private LocalDateTime approvedAt;
    
    @Column(name = "requested_at")
    private LocalDateTime requestedAt;

    @Column(name = "is_refunded")
    private boolean isRefunded;
    
    @Column(name = "vat")
    private BigDecimal vat;
    
    @Column(name = "supplied_amount")
    private BigDecimal suppliedAmount;
    
    @Column(name = "tax_free_amount")
    private BigDecimal taxFreeAmount;
    
    @Column(name = "is_partial_cancelable")
    private boolean isPartialCancelable;
    
    @Column(name = "receipt_url")
    private String receiptUrl;
    
    @Column(length = 10)
    private String currency;

    private LocalDateTime createdAt;

    private LocalDateTime updatedAt;

    @PrePersist
    protected void onCreate() {
        this.createdAt = LocalDateTime.now();
        this.updatedAt = LocalDateTime.now();
        this.requestedAt = LocalDateTime.now();
        
        // 기본값 설정
        if (this.totalAmount == null) {
            this.totalAmount = this.amount;
        }
        if (this.balanceAmount == null) {
            this.balanceAmount = this.amount;
        }
        if (this.currency == null) {
            this.currency = "KRW";
        }
        if (this.taxFreeAmount == null) {
            this.taxFreeAmount = BigDecimal.ZERO;
        }
    }

    @PreUpdate
    protected void onUpdate() {
        this.updatedAt = LocalDateTime.now();
    }

    @Builder
    private Payment(BigDecimal amount, PaymentMethod paymentMethod, String paymentKey, 
                   Long userId, PaymentStatus paymentStatus, String orderId, String orderName, 
                   LocalDateTime approvedAt, BigDecimal vat, BigDecimal suppliedAmount, 
                   BigDecimal taxFreeAmount, String receiptUrl, String currency,
                   boolean isPartialCancelable) {
        this.amount = amount;
        this.totalAmount = amount;
        this.balanceAmount = amount;
        this.paymentMethod = paymentMethod;
        this.paymentKey = paymentKey;
        this.userId = userId;
        this.paymentStatus = paymentStatus != null ? paymentStatus : PaymentStatus.READY;
        this.orderId = orderId;
        this.orderName = orderName;
        this.approvedAt = approvedAt;
        this.isRefunded = false;
        this.vat = vat;
        this.suppliedAmount = suppliedAmount;
        this.taxFreeAmount = taxFreeAmount != null ? taxFreeAmount : BigDecimal.ZERO;
        this.receiptUrl = receiptUrl;
        this.currency = currency != null ? currency : "KRW";
        this.isPartialCancelable = isPartialCancelable;
    }

    /**
     * 결제 준비를 위한 Payment 객체 생성
     */
    public static Payment createPayment(BigDecimal amount, PaymentMethod paymentMethod, 
                                      Long userId, String orderId, String orderName) {
        return Payment.builder()
                .amount(amount)
                .paymentMethod(paymentMethod)
                .userId(userId)
                .orderId(orderId)
                .orderName(orderName)
                .paymentStatus(PaymentStatus.READY)
                .isPartialCancelable(true)
                .build();
    }
    
    /**
     * 간단한 결제 생성 (이름 없는 주문)
     */
    public static Payment createPayment(BigDecimal amount, PaymentMethod paymentMethod, 
                                      Long userId, String orderId) {
        return createPayment(amount, paymentMethod, userId, orderId, null);
    }

    /**
     * 결제 승인
     */
    public void approve(LocalDateTime approvedAt) {
        this.paymentStatus = PaymentStatus.DONE;
        this.approvedAt = approvedAt;
    }

    /**
     * 결제 상태 업데이트
     */
    public void updatePaymentStatus(PaymentStatus status) {
        this.paymentStatus = status;
    }

    /**
     * 결제 키 업데이트
     */
    public void updatePaymentKey(String paymentKey) {
        this.paymentKey = paymentKey;
    }
    
    /**
     * 영수증 URL 업데이트
     */
    public void updateReceiptUrl(String receiptUrl) {
        this.receiptUrl = receiptUrl;
    }
    
    /**
     * 전체 환불 처리
     */
    public void refund() {
        this.isRefunded = true;
        this.paymentStatus = PaymentStatus.CANCELED;
        this.balanceAmount = BigDecimal.ZERO;
    }
    
    /**
     * 부분 환불 처리
     */
    public void partialRefund(BigDecimal refundAmount) {
        if (!this.isPartialCancelable) {
            throw new IllegalStateException("해당 결제는 부분 환불이 불가능합니다");
        }
        
        if (refundAmount.compareTo(this.balanceAmount) > 0) {
            throw new IllegalArgumentException("환불 금액은 잔액보다 클 수 없습니다");
        }
        
        this.balanceAmount = this.balanceAmount.subtract(refundAmount);
        
        if (this.balanceAmount.compareTo(BigDecimal.ZERO) == 0) {
            this.isRefunded = true;
            this.paymentStatus = PaymentStatus.CANCELED;
        } else {
            this.paymentStatus = PaymentStatus.PARTIAL_CANCELED;
        }
    }
}