using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using Domain.Entities.Members;
namespace Domain.Entities.Paper;
///
/// 모의투자 계좌 — 회원당 1개(상시 단일 계좌). 토큰을 입금해 매매, 출금해 회수한다.
/// 좌수제(펀드 NAV): 입출금 왜곡 없이 순수 운용 성과를 측정 (d4 §③ 좌수제).
/// Equity/nav 등 평가값은 핸들러가 시세로 계산 — 엔티티에 저장하지 않는다.
///
public class PaperAccount
{
[ForeignKey(nameof(MemberID))]
public virtual Member Member { get; private set; } = null!;
[Key]
public int ID { get; private set; }
public int MemberID { get; private set; }
/// 자유 현금 토큰 (매수 예약 제외)
public decimal Token { get; private set; }
/// 매수 주문으로 예약된 토큰
public decimal ReservedToken { get; private set; }
/// 좌수 (18,8) — 발행/소각으로만 변동, 매매는 좌수 불변
public decimal Units { get; private set; }
/// 누적 입금 토큰
public decimal TotalDeposited { get; private set; }
/// 누적 출금 토큰
public decimal TotalWithdrawn { get; private set; }
public DateTime CreatedAt { get; private set; } = DateTime.UtcNow;
[Timestamp]
public byte[] RowVersion { get; private set; } = default!;
private PaperAccount() { }
public static PaperAccount Create(int memberID)
{
if (memberID <= 0)
{
throw new ArgumentOutOfRangeException(nameof(memberID));
}
return new PaperAccount
{
MemberID = memberID,
Token = 0,
ReservedToken = 0,
Units = 0,
TotalDeposited = 0,
TotalWithdrawn = 0
};
}
/// 입금 — 토큰/좌수 증가. 좌수 발행 계산은 호출자(nav 필요).
public void Deposit(decimal tokenAmount, decimal unitsIssued)
{
if (tokenAmount <= 0)
{
throw new ArgumentOutOfRangeException(nameof(tokenAmount));
}
if (unitsIssued <= 0)
{
throw new ArgumentOutOfRangeException(nameof(unitsIssued));
}
Token += tokenAmount;
Units += unitsIssued;
TotalDeposited += tokenAmount;
}
/// 출금 — 자유 현금 토큰만 인출 가능. 좌수 소각.
public void Withdraw(decimal tokenAmount, decimal unitsBurned)
{
if (tokenAmount <= 0)
{
throw new ArgumentOutOfRangeException(nameof(tokenAmount));
}
if (Token < tokenAmount)
{
throw new InvalidOperationException("출금 가능한 자유 토큰이 부족합니다.");
}
if (unitsBurned <= 0 || unitsBurned > Units)
{
throw new ArgumentOutOfRangeException(nameof(unitsBurned));
}
Token -= tokenAmount;
Units -= unitsBurned;
TotalWithdrawn += tokenAmount;
}
/// 매수 예약 — 자유 토큰을 예약금으로 이동.
public void ReserveForBuy(decimal amount)
{
if (amount <= 0)
{
throw new ArgumentOutOfRangeException(nameof(amount));
}
if (Token < amount)
{
throw new InvalidOperationException("매수 예약 가능한 토큰이 부족합니다.");
}
Token -= amount;
ReservedToken += amount;
}
/// 매수 예약 환원 (주문 취소·거부).
public void ReleaseBuyReserve(decimal amount)
{
if (amount <= 0)
{
throw new ArgumentOutOfRangeException(nameof(amount));
}
if (ReservedToken < amount)
{
throw new InvalidOperationException("환원할 예약금이 부족합니다.");
}
ReservedToken -= amount;
Token += amount;
}
///
/// 매수 체결 정산 (배치 M2) — 예약금 전액을 해제하고 실제 사용분(actualCost)을 제외한 잔여를 자유 토큰으로 환급한다.
/// ReservedToken -= reservedAmount; Token += (reservedAmount - actualCost). 좌수는 불변.
///
public void SettleBuy(decimal reservedAmount, decimal actualCost)
{
if (reservedAmount <= 0)
{
throw new ArgumentOutOfRangeException(nameof(reservedAmount));
}
if (actualCost < 0 || actualCost > reservedAmount)
{
throw new ArgumentOutOfRangeException(nameof(actualCost));
}
if (ReservedToken < reservedAmount)
{
throw new InvalidOperationException("정산할 예약금이 부족합니다.");
}
ReservedToken -= reservedAmount;
Token += reservedAmount - actualCost;
}
/// 매도 대금·청산 대금 입금 (배치 M2) — 자유 토큰 증가. 좌수는 불변.
public void AddCash(decimal amount)
{
if (amount < 0)
{
throw new ArgumentOutOfRangeException(nameof(amount));
}
Token += amount;
}
}