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(() => 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(() => 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(() => 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(() => new ConfirmedDockedServiceReceipt(Guid.NewGuid().ToString("N"), "refuel", 1, 1000, 1001, DateTimeOffset.UtcNow).ToCashEntry()); } }