using XFEEDToolBox.Core.Execution;
using XFEExtension.NetCore.XUnit.Attributes;
namespace XFEEDToolBox.Test;
[TestFixture]
public class TradeProfitLedgerTests
{
private static TradeCashEntry Entry(TradeCashKind kind, decimal amount, string commodity = "", int quantity = 0)
=> new(Guid.NewGuid().ToString("N"), kind, amount, 1, DateTimeOffset.UtcNow, commodity, quantity);
[Test] public void BoughtCargoIsInvestmentUntilSoldAndAllIncurredServicesReduceProfit()
{
using var ledger = new TradeProfitLedger();
ledger.Record(Entry(TradeCashKind.Purchase, 600, "coffee", 3));
ledger.Record(Entry(TradeCashKind.Fuel, 7));
ledger.Record(Entry(TradeCashKind.Repair, 11));
ledger.Record(Entry(TradeCashKind.Rearm, 13));
Check.Equal(-31m, ledger.Snapshot.NetProfit);
Check.Equal(-631m, ledger.Snapshot.CashChange);
Check.Equal(600m, ledger.Snapshot.CargoInvestment);
ledger.Record(Entry(TradeCashKind.Sale, 500, "coffee", 2));
Check.Equal(69m, ledger.Snapshot.NetProfit);
Check.Equal(200m, ledger.Snapshot.CargoInvestment);
Check.Equal(-131m, ledger.Snapshot.CashChange);
}
[Test] public void MultipleCargoLotsUseTheirActualPurchaseCostsWithoutRoundingLoss()
{
using var ledger = new TradeProfitLedger();
ledger.Record(Entry(TradeCashKind.Purchase, 1001, "uranium", 3));
ledger.Record(Entry(TradeCashKind.Purchase, 400, "uranium", 2));
for (var i = 0; i < 5; i++) ledger.Record(Entry(TradeCashKind.Sale, 500, "uranium", 1));
Check.Equal(1401m, ledger.Snapshot.SoldCargoCost);
Check.Equal(1099m, ledger.Snapshot.NetProfit);
Check.Equal(0m, ledger.Snapshot.CargoInvestment);
}
[Test] public void ReopeningRestoresUnfinishedCargoAndDuplicateReceiptsAreNotCountedAgain()
{
var directory = Path.Combine(Path.GetTempPath(), "xfe-trade-profit-" + Guid.NewGuid().ToString("N"));
var buy = Entry(TradeCashKind.Purchase, 100, "polymers", 2);
var fuel = Entry(TradeCashKind.Fuel, 5);
using (var first = new TradeProfitLedger(directory)) { first.Record(buy); first.Record(fuel); }
// This represents a process ending before its next ledger record was committed.
File.WriteAllText(Path.Combine(directory, "incomplete.tmp"), "{\"Sequence\":");
using var restored = new TradeProfitLedger(directory);
Check.That(!restored.Record(buy)); Check.That(!restored.Record(fuel));
restored.Record(Entry(TradeCashKind.Sale, 250, "polymers", 2));
Check.Equal(145m, restored.Snapshot.NetProfit);
Check.Equal(3, Directory.GetFiles(directory, "*.entry.json").Length);
Check.Throws<InvalidDataException>(() => restored.Record(fuel with { Amount = 10 }));
Check.Equal(145m, restored.Snapshot.NetProfit);
}
[Test] public async Task ConcurrentTradeAndDockedCostsPublishOneConsistentTotal()
{
using var ledger = new TradeProfitLedger();
ledger.Record(Entry(TradeCashKind.Purchase, 100, "water", 1));
var fuel = Entry(TradeCashKind.Fuel, 7);
await Task.WhenAll(Enumerable.Range(0, 20).Select(_ => Task.Run(() => ledger.Record(fuel))).Append(
Task.Run(() => ledger.Record(Entry(TradeCashKind.Sale, 130, "water", 1)))));
Check.Equal(23m, ledger.Snapshot.NetProfit);
}
[Test] public void UnknownCostAndUnconfirmedNativeReceiptsCannotBecomeProfit()
{
using var ledger = new TradeProfitLedger();
Check.Throws<InvalidDataException>(() => ledger.Record(Entry(TradeCashKind.Sale, 500, "water", 1)));
var receipt = new ConfirmedTradeReceipt(Guid.NewGuid().ToString("N"), 1, "water", 1, 10,
1000, 990, 0, 1, "commander", 7, DateTimeOffset.UtcNow.AddMinutes(-1), DateTimeOffset.UtcNow, false, true);
Check.Throws<InvalidDataException>(() => ledger.RecordTrade(receipt));
Check.Equal(TradeProfitSnapshot.Empty, ledger.Snapshot);
}
[Test] public void OnlyCollectedRewardsAndConfirmedServiceDebitsAreCounted()
{
using var ledger = new TradeProfitLedger();
ledger.Record(new ConfirmedDockedServiceReceipt(Guid.NewGuid().ToString("N"), "integrity", 1,
1000, 990, DateTimeOffset.UtcNow).ToCashEntry());
Check.Equal(-10m, ledger.Snapshot.NetProfit);
ledger.Record(Entry(TradeCashKind.CollectedReward, 5000));
Check.Equal(4990m, ledger.Snapshot.NetProfit);
Check.Throws<InvalidDataException>(() => new ConfirmedDockedServiceReceipt(Guid.NewGuid().ToString("N"), "refuel", 1,
1000, 1001, DateTimeOffset.UtcNow).ToCashEntry());
}
}
using XFEEDToolBox.Core.Execution;
using XFEExtension.NetCore.XUnit.Attributes;
namespace XFEEDToolBox.Test;
[TestFixture]
public class TradeProfitLedgerTests
{
private static TradeCashEntry Entry(TradeCashKind kind, decimal amount, string commodity = "", int quantity = 0)
=> new(Guid.NewGuid().ToString("N"), kind, amount, 1, DateTimeOffset.UtcNow, commodity, quantity);
[Test] public void BoughtCargoIsInvestmentUntilSoldAndAllIncurredServicesReduceProfit()
{
using var ledger = new TradeProfitLedger();
ledger.Record(Entry(TradeCashKind.Purchase, 600, "coffee", 3));
ledger.Record(Entry(TradeCashKind.Fuel, 7));
ledger.Record(Entry(TradeCashKind.Repair, 11));
ledger.Record(Entry(TradeCashKind.Rearm, 13));
Check.Equal(-31m, ledger.Snapshot.NetProfit);
Check.Equal(-631m, ledger.Snapshot.CashChange);
Check.Equal(600m, ledger.Snapshot.CargoInvestment);
ledger.Record(Entry(TradeCashKind.Sale, 500, "coffee", 2));
Check.Equal(69m, ledger.Snapshot.NetProfit);
Check.Equal(200m, ledger.Snapshot.CargoInvestment);
Check.Equal(-131m, ledger.Snapshot.CashChange);
}
[Test] public void MultipleCargoLotsUseTheirActualPurchaseCostsWithoutRoundingLoss()
{
using var ledger = new TradeProfitLedger();
ledger.Record(Entry(TradeCashKind.Purchase, 1001, "uranium", 3));
ledger.Record(Entry(TradeCashKind.Purchase, 400, "uranium", 2));
for (var i = 0; i < 5; i++) ledger.Record(Entry(TradeCashKind.Sale, 500, "uranium", 1));
Check.Equal(1401m, ledger.Snapshot.SoldCargoCost);
Check.Equal(1099m, ledger.Snapshot.NetProfit);
Check.Equal(0m, ledger.Snapshot.CargoInvestment);
}
[Test] public void ReopeningRestoresUnfinishedCargoAndDuplicateReceiptsAreNotCountedAgain()
{
var directory = Path.Combine(Path.GetTempPath(), "xfe-trade-profit-" + Guid.NewGuid().ToString("N"));
var buy = Entry(TradeCashKind.Purchase, 100, "polymers", 2);
var fuel = Entry(TradeCashKind.Fuel, 5);
using (var first = new TradeProfitLedger(directory)) { first.Record(buy); first.Record(fuel); }
// This represents a process ending before its next ledger record was committed.
File.WriteAllText(Path.Combine(directory, "incomplete.tmp"), "{\"Sequence\":");
using var restored = new TradeProfitLedger(directory);
Check.That(!restored.Record(buy)); Check.That(!restored.Record(fuel));
restored.Record(Entry(TradeCashKind.Sale, 250, "polymers", 2));
Check.Equal(145m, restored.Snapshot.NetProfit);
Check.Equal(3, Directory.GetFiles(directory, "*.entry.json").Length);
Check.Throws<InvalidDataException>(() => restored.Record(fuel with { Amount = 10 }));
Check.Equal(145m, restored.Snapshot.NetProfit);
}
[Test] public async Task ConcurrentTradeAndDockedCostsPublishOneConsistentTotal()
{
using var ledger = new TradeProfitLedger();
ledger.Record(Entry(TradeCashKind.Purchase, 100, "water", 1));
var fuel = Entry(TradeCashKind.Fuel, 7);
await Task.WhenAll(Enumerable.Range(0, 20).Select(_ => Task.Run(() => ledger.Record(fuel))).Append(
Task.Run(() => ledger.Record(Entry(TradeCashKind.Sale, 130, "water", 1)))));
Check.Equal(23m, ledger.Snapshot.NetProfit);
}
[Test] public void UnknownCostAndUnconfirmedNativeReceiptsCannotBecomeProfit()
{
using var ledger = new TradeProfitLedger();
Check.Throws<InvalidDataException>(() => ledger.Record(Entry(TradeCashKind.Sale, 500, "water", 1)));
var receipt = new ConfirmedTradeReceipt(Guid.NewGuid().ToString("N"), 1, "water", 1, 10,
1000, 990, 0, 1, "commander", 7, DateTimeOffset.UtcNow.AddMinutes(-1), DateTimeOffset.UtcNow, false, true);
Check.Throws<InvalidDataException>(() => ledger.RecordTrade(receipt));
Check.Equal(TradeProfitSnapshot.Empty, ledger.Snapshot);
}
[Test] public void OnlyCollectedRewardsAndConfirmedServiceDebitsAreCounted()
{
using var ledger = new TradeProfitLedger();
ledger.Record(new ConfirmedDockedServiceReceipt(Guid.NewGuid().ToString("N"), "integrity", 1,
1000, 990, DateTimeOffset.UtcNow).ToCashEntry());
Check.Equal(-10m, ledger.Snapshot.NetProfit);
ledger.Record(Entry(TradeCashKind.CollectedReward, 5000));
Check.Equal(4990m, ledger.Snapshot.NetProfit);
Check.Throws<InvalidDataException>(() => new ConfirmedDockedServiceReceipt(Guid.NewGuid().ToString("N"), "refuel", 1,
1000, 1001, DateTimeOffset.UtcNow).ToCashEntry());
}
}