package name.abuchen.portfolio.model;
import java.io.Serializable;
import java.math.BigDecimal;
import java.text.MessageFormat;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.stream.Stream;
import name.abuchen.portfolio.Messages;
import name.abuchen.portfolio.money.CurrencyConverter;
import name.abuchen.portfolio.money.Money;
import name.abuchen.portfolio.money.MoneyCollectors;
import name.abuchen.portfolio.money.Values;
public abstract class Transaction implements Annotated
{
public static class Unit
{
public enum Type
{
GROSS_VALUE, TAX, FEE
}
/**
* Type of transaction unit
*/
private final Type type;
/**
* Amount in transaction currency
*/
private final Money amount;
/**
* Original amount in foreign currency; can be null if unit is recorded
* in currency of transaction
*/
private final Money forex;
/**
* Exchange rate used to convert forex amount to amount
*/
private final BigDecimal exchangeRate;
public Unit(Type type, Money amount)
{
this.type = Objects.requireNonNull(type);
this.amount = Objects.requireNonNull(amount);
this.forex = null;
this.exchangeRate = null;
}
public Unit(Type type, Money amount, Money forex, BigDecimal exchangeRate)
{
this.type = Objects.requireNonNull(type);
this.amount = Objects.requireNonNull(amount);
this.forex = Objects.requireNonNull(forex);
this.exchangeRate = Objects.requireNonNull(exchangeRate);
// check whether given amount is in range of converted amount
long upper = Math.round(exchangeRate.add(BigDecimal.valueOf(0.001))
.multiply(BigDecimal.valueOf(forex.getAmount())).doubleValue());
long lower = Math.round(exchangeRate.add(BigDecimal.valueOf(-0.001))
.multiply(BigDecimal.valueOf(forex.getAmount())).doubleValue());
if (amount.getAmount() < lower || amount.getAmount() > upper)
throw new IllegalArgumentException(
MessageFormat.format(Messages.MsgErrorIllegalForexUnit, type.toString(),
Values.Money.format(forex), exchangeRate, Values.Money.format(amount)));
}
public Type getType()
{
return type;
}
public Money getAmount()
{
return amount;
}
public Money getForex()
{
return forex;
}
public BigDecimal getExchangeRate()
{
return exchangeRate;
}
}
public static final class ByDate implements Comparator<Transaction>, Serializable
{
private static final long serialVersionUID = 1L;
@Override
public int compare(Transaction t1, Transaction t2)
{
return t1.getDate().compareTo(t2.getDate());
}
}
private LocalDate date;
private String currencyCode;
private long amount;
private Security security;
private CrossEntry crossEntry;
private long shares;
private String note;
private List<Unit> units;
public Transaction()
{}
public Transaction(LocalDate date, String currencyCode, long amount)
{
this(date, currencyCode, amount, null, 0, null);
}
public Transaction(LocalDate date, String currencyCode, long amount, Security security, long shares, String note)
{
this.date = date;
this.currencyCode = currencyCode;
this.amount = amount;
this.security = security;
this.shares = shares;
this.note = note;
}
public LocalDate getDate()
{
return date;
}
public void setDate(LocalDate date)
{
this.date = date;
}
public String getCurrencyCode()
{
return currencyCode;
}
public void setCurrencyCode(String currencyCode)
{
this.currencyCode = currencyCode;
}
public long getAmount()
{
return amount;
}
public void setAmount(long amount)
{
this.amount = amount;
}
public Money getMonetaryAmount()
{
return Money.of(currencyCode, amount);
}
public void setMonetaryAmount(Money value)
{
this.currencyCode = value.getCurrencyCode();
this.amount = value.getAmount();
}
public Security getSecurity()
{
return security;
}
public void setSecurity(Security security)
{
this.security = security;
}
public CrossEntry getCrossEntry()
{
return crossEntry;
}
/* package */void setCrossEntry(CrossEntry crossEntry)
{
this.crossEntry = crossEntry;
}
public long getShares()
{
return shares;
}
public void setShares(long shares)
{
this.shares = shares;
}
@Override
public String getNote()
{
return note;
}
@Override
public void setNote(String note)
{
this.note = note;
}
public Stream<Unit> getUnits()
{
return units != null ? units.stream() : Stream.empty();
}
/**
* Returns any unit of the given type
*/
public Optional<Unit> getUnit(Unit.Type type)
{
return getUnits().filter(u -> u.getType() == type).findAny();
}
/**
* Clears all currently set units
*/
public void clearUnits()
{
units = null;
}
public void addUnit(Unit unit)
{
Objects.requireNonNull(unit.getAmount());
if (!unit.getAmount().getCurrencyCode().equals(currencyCode))
throw new IllegalArgumentException(MessageFormat.format(Messages.MsgErrorUnitCurrencyMismatch,
unit.getType().toString(), unit.getAmount().getCurrencyCode(), currencyCode));
if (units == null)
units = new ArrayList<>();
units.add(unit);
}
public void addUnits(Stream<Unit> items)
{
if (units == null)
units = new ArrayList<>();
items.forEach(units::add);
}
public void removeUnit(Unit unit)
{
if (units == null)
units = new ArrayList<>();
units.remove(unit);
}
/**
* Returns the sum of units in transaction currency
*/
public Money getUnitSum(Unit.Type type)
{
return getUnits().filter(u -> u.getType() == type) //
.collect(MoneyCollectors.sum(getCurrencyCode(), Unit::getAmount));
}
/**
* Returns the sum of units in the term currency of the currency converter
*/
public Money getUnitSum(Unit.Type type, CurrencyConverter converter)
{
return getUnits().filter(u -> u.getType() == type)
.collect(MoneyCollectors.sum(converter.getTermCurrency(), unit -> {
if (converter.getTermCurrency().equals(unit.getAmount().getCurrencyCode()))
return unit.getAmount();
else if (unit.getForex() != null
&& converter.getTermCurrency().equals(unit.getForex().getCurrencyCode()))
return unit.getForex();
else
return unit.getAmount().with(converter.at(date));
}));
}
public static final <E extends Transaction> List<E> sortByDate(List<E> transactions)
{
Collections.sort(transactions, new ByDate());
return transactions;
}
}