From 0ed280f79f20a4f84bd17d23326d69065d5ff28f Mon Sep 17 00:00:00 2001 From: Ryan Barlow <7389646+ryanbarlow97@users.noreply.github.com> Date: Sat, 17 Oct 2026 14:55:19 +0000 Subject: [PATCH 1/2] fix: show character contributors in daily citizen tax ledgers --- pom.xml | 2 +- .../simplefactions/economy/FrozenReads.java | 17 +++++++++-- .../simplefactions/guild/income/Ledger.java | 9 ++++-- .../economy/FrozenReadsTest.java | 30 ++++++++++++++----- .../RealmSettlementCalculationTest.java | 19 ++++++++++++ .../LedgerCitizenTaxPersistenceTest.java | 16 ++++++++++ .../inventory/GuildMenusCoverageTest.java | 30 +++++++++++++++++++ 7 files changed, 109 insertions(+), 14 deletions(-) diff --git a/pom.xml b/pom.xml index 02814dd9..7052c262 100644 --- a/pom.xml +++ b/pom.xml @@ -13,7 +13,7 @@ 21 UTF-8 full - 0.6.0 + 0.7.0 2.16.0 2.6.0 2.13.0 diff --git a/src/main/java/net/tfminecraft/simplefactions/economy/FrozenReads.java b/src/main/java/net/tfminecraft/simplefactions/economy/FrozenReads.java index 90537d3d..c96133d7 100644 --- a/src/main/java/net/tfminecraft/simplefactions/economy/FrozenReads.java +++ b/src/main/java/net/tfminecraft/simplefactions/economy/FrozenReads.java @@ -24,12 +24,15 @@ public final class FrozenReads { private static final ThreadLocal CURRENT = new ThreadLocal<>(); private final Map cents; + private final Map> taxContributors; private final Map nodeUpkeep; private final Map> espionage; private FrozenReads( - Map cents, Map nodeUpkeep, Map> espionage) { + Map cents, Map> taxContributors, + Map nodeUpkeep, Map> espionage) { this.cents = Map.copyOf(cents); + this.taxContributors = Map.copyOf(taxContributors); this.nodeUpkeep = Map.copyOf(nodeUpkeep); this.espionage = Map.copyOf(espionage); } @@ -49,6 +52,7 @@ public static FrozenReads capture() { } } Map cents = new HashMap<>(); + Map> taxContributors = new HashMap<>(); Map nodeUpkeep = new HashMap<>(); for (Guild guild : FactionManager.getAllGuilds()) { Bank bank = guild == null ? null : guild.getBank(); @@ -60,10 +64,11 @@ public static FrozenReads capture() { if (incarnation != null) { String taxAccount = Denars.guildTax(incarnation); cents.put(taxAccount, Denars.balance(taxAccount)); + taxContributors.put(incarnation, Map.copyOf(Denars.taxContributors(incarnation))); nodeUpkeep.put(incarnation, Ledger.liveNodeUpkeep(guild)); } } - return new FrozenReads(cents, nodeUpkeep, espionage); + return new FrozenReads(cents, taxContributors, nodeUpkeep, espionage); } /** Runs the work with these reads standing in for the live state on this thread. */ @@ -90,6 +95,14 @@ public static long balance(String account) { return Denars.balance(account); } + /** Held tax by earning character, frozen with the balance for async previews and settlement. */ + public static Map taxContributors(UUID incarnation) { + if (incarnation == null) return Map.of(); + FrozenReads current = CURRENT.get(); + return current == null ? Denars.taxContributors(incarnation) + : current.taxContributors.getOrDefault(incarnation, Map.of()); + } + /** The frozen node upkeep inside {@link #read}, otherwise the live upkeep. */ public static double nodeUpkeep(Guild guild) { FrozenReads current = CURRENT.get(); diff --git a/src/main/java/net/tfminecraft/simplefactions/guild/income/Ledger.java b/src/main/java/net/tfminecraft/simplefactions/guild/income/Ledger.java index d521bfb2..6879d607 100644 --- a/src/main/java/net/tfminecraft/simplefactions/guild/income/Ledger.java +++ b/src/main/java/net/tfminecraft/simplefactions/guild/income/Ledger.java @@ -144,15 +144,18 @@ public Map getCitizenTaxesCopy() { return new HashMap<>(citizenTaxes); } - /** Held tax is authoritative; legacy contributor rows describe only part of it. */ + /** Held tax is authoritative; combine transaction attribution with legacy contributor rows. */ public Map getCitizenTaxBreakdown() { double collected = getCollectedCitizenTax(); if (collected <= 0) return Map.of(); - if (!Double.isFinite(LedgerHistory.total(citizenTaxes))) { + Map taxes = new HashMap<>(citizenTaxes); + FrozenReads.taxContributors(guild.getIncarnationId()).forEach((character, cents) -> + taxes.merge(character.toString(), cents / 100.0, Double::sum)); + if (!Double.isFinite(LedgerHistory.total(taxes))) { return Map.of("Other citizen taxes", collected); } Map amounts = new HashMap<>(); - citizenTaxes.forEach((name, value) -> { + taxes.forEach((name, value) -> { double amount = Formatter.formatDouble(value); if (amount > 0) amounts.put(name, amount); }); diff --git a/src/test/java/net/tfminecraft/simplefactions/economy/FrozenReadsTest.java b/src/test/java/net/tfminecraft/simplefactions/economy/FrozenReadsTest.java index 5d2902d1..6bdea968 100644 --- a/src/test/java/net/tfminecraft/simplefactions/economy/FrozenReadsTest.java +++ b/src/test/java/net/tfminecraft/simplefactions/economy/FrozenReadsTest.java @@ -4,6 +4,7 @@ import static org.junit.jupiter.api.Assertions.assertInstanceOf; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.CALLS_REAL_METHODS; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.mockStatic; @@ -50,6 +51,12 @@ class FrozenReadsTest { private final List savedFactions = new ArrayList<>(); private Guild guild; + private org.mockito.MockedStatic mockDenars() { + var denars = mockStatic(Denars.class, CALLS_REAL_METHODS); + denars.when(() -> Denars.taxContributors(any())).thenReturn(Map.of()); + return denars; + } + @BeforeEach void setUp() { savedFactions.addAll(FactionManager.factions); @@ -74,7 +81,7 @@ void tearDown() { @Test void asyncReadsUseBalancesCapturedOnTheMainThread() throws Exception { FrozenReads frozen; - try (var denars = mockStatic(Denars.class, CALLS_REAL_METHODS)) { + try (var denars = mockDenars()) { denars.when(() -> Denars.balance(anyString())).thenReturn(1234L); frozen = FrozenReads.capture(); } @@ -82,6 +89,8 @@ void asyncReadsUseBalancesCapturedOnTheMainThread() throws Exception { // Static mocks stay on the test thread, so only the frozen balance can give the worker 12.34. assertEquals(12.34, CompletableFuture.supplyAsync(() -> frozen.read(() -> guild.getBank().getWealth())).get()); assertEquals(0L, frozen.read(() -> FrozenReads.balance("unknown"))); + assertEquals(Map.of(), frozen.read(() -> FrozenReads.taxContributors(UUID.randomUUID()))); + assertEquals(Map.of(), FrozenReads.taxContributors(null)); } @Test @@ -91,22 +100,27 @@ void asyncCitizenTaxUsesTheHoldingBalanceCapturedOnTheMainThread() throws Except Ledger ledger = new Ledger(guild); String holding = Denars.guildTax(guild.getIncarnationId()); FrozenReads frozen; - try (var denars = mockStatic(Denars.class, CALLS_REAL_METHODS)) { + UUID contributor = UUID.randomUUID(); + var contributors = new java.util.HashMap<>(Map.of(contributor, 1000L)); + try (var denars = mockDenars()) { denars.when(() -> Denars.balance(anyString())).thenReturn(0L); denars.when(() -> Denars.balance(holding)).thenReturn(1234L); + denars.when(() -> Denars.taxContributors(guild.getIncarnationId())).thenReturn(contributors); frozen = FrozenReads.capture(); denars.when(() -> Denars.balance(holding)).thenReturn(5000L); + contributors.put(contributor, 2000L); assertEquals(50.0, ledger.getIncome(Cashflow.CITIZENS)); + assertEquals(Map.of(contributor.toString(), 20.0, "Other citizen taxes", 30.0), ledger.getCitizenTaxBreakdown()); } assertEquals(12.34, CompletableFuture.supplyAsync(() -> frozen.read(() -> ledger.getIncome(Cashflow.CITIZENS))).get()); - assertEquals(Map.of("Other citizen taxes", 12.34), CompletableFuture + assertEquals(Map.of(contributor.toString(), 10.0, "Other citizen taxes", 2.34), CompletableFuture .supplyAsync(() -> frozen.read(ledger::getCitizenTaxBreakdown)).get()); } @Test void readRestoresTheLiveLedgerAfterwards() { - try (var denars = mockStatic(Denars.class, CALLS_REAL_METHODS)) { + try (var denars = mockDenars()) { denars.when(() -> Denars.balance(anyString())).thenReturn(500L); FrozenReads frozen = FrozenReads.capture(); denars.when(() -> Denars.balance(anyString())).thenReturn(700L); @@ -118,7 +132,7 @@ void readRestoresTheLiveLedgerAfterwards() { @Test void nestedReadsRestoreTheOuterBalances() { - try (var denars = mockStatic(Denars.class, CALLS_REAL_METHODS)) { + try (var denars = mockDenars()) { denars.when(() -> Denars.balance(anyString())).thenReturn(100L); FrozenReads outer = FrozenReads.capture(); denars.when(() -> Denars.balance(anyString())).thenReturn(200L); @@ -135,7 +149,7 @@ void nestedReadsRestoreTheOuterBalances() { void previewCopiesCarryTheBalancesFromWhenTheyWereMade() { ProvinceManager live = new ProvinceManager(); live.start(Map.of()); - try (var denars = mockStatic(Denars.class, CALLS_REAL_METHODS)) { + try (var denars = mockDenars()) { denars.when(() -> Denars.balance(anyString())).thenReturn(-300L); EconomicPreview.Prepared economic = EconomicPreview.prepare(live); BranchIncomePreview.Prepared branch = BranchIncomePreview.prepare(live); @@ -168,7 +182,7 @@ void asyncStabilityUsesSpymasterDeathsCapturedOnTheMainThread() throws Exception assertInstanceOf(IllegalStateException.class, live.getCause()); FrozenReads frozen; - try (var denars = mockStatic(Denars.class, CALLS_REAL_METHODS)) { + try (var denars = mockDenars()) { denars.when(() -> Denars.balance(anyString())).thenReturn(0L); frozen = FrozenReads.capture(); } @@ -200,7 +214,7 @@ void asyncLedgerUsesNodeUpkeepCapturedOnTheMainThread() throws Exception { assertInstanceOf(IllegalStateException.class, live.getCause()); FrozenReads frozen; - try (var denars = mockStatic(Denars.class, CALLS_REAL_METHODS)) { + try (var denars = mockDenars()) { denars.when(() -> Denars.balance(anyString())).thenReturn(0L); frozen = FrozenReads.capture(); } diff --git a/src/test/java/net/tfminecraft/simplefactions/economy/settlement/RealmSettlementCalculationTest.java b/src/test/java/net/tfminecraft/simplefactions/economy/settlement/RealmSettlementCalculationTest.java index caaa2472..449b32ab 100644 --- a/src/test/java/net/tfminecraft/simplefactions/economy/settlement/RealmSettlementCalculationTest.java +++ b/src/test/java/net/tfminecraft/simplefactions/economy/settlement/RealmSettlementCalculationTest.java @@ -3,8 +3,11 @@ import java.util.List; import java.util.Map; import java.util.UUID; +import net.tfminecraft.denareconomy.DenarEconomy; import net.tfminecraft.denareconomy.api.Denars; import net.tfminecraft.denareconomy.api.Leg; +import net.tfminecraft.denareconomy.api.Result; +import net.tfminecraft.denareconomy.api.TaxLeg; import net.tfminecraft.simplefactions.Cache; import net.tfminecraft.simplefactions.database.JsonUtil; import net.tfminecraft.simplefactions.database.LoanData; @@ -95,6 +98,22 @@ RealmSettlementChanges changes(SettlementJournal.Plan plan) { assertTrue(capital.getLedger().getCitizenTaxesCopy().isEmpty()); } + @Test void postedTaxContributorsBecomeLastDayAndLifetimeWithoutChangingSettlementMoney() { + String holding = Denars.guildTax(capital.getIncarnationId()); + assertEquals(Result.APPLIED, DenarEconomy.getLedger().postEarning("tax", "earn", + List.of(new Leg(Denars.MINT, -2000), new Leg("char:" + character + ":bank", 766), + new Leg(holding, 1234)), character, new TaxLeg(holding, 1234))); + assertEquals(Map.of(character.toString(), 12.34), capital.getLedger().getCitizenTaxBreakdown()); + var plan = plan(List.of()); + assertEquals(-1234, net(plan, holding)); + assertEquals(1234, net(plan, capital.getBank().accountId())); + var saved = changes(plan).guilds.stream() + .filter(c -> c.incarnation.equals(capital.getIncarnationId())).findFirst().orElseThrow(); + assertEquals(Map.of(character.toString(), 12.34), saved.ledgerLastDay.get("CITIZENS")); + assertEquals(Map.of(character.toString(), 12.34), saved.ledgerLifetime.get("CITIZENS")); + assertTrue(capital.getLedger().getCitizenTaxesCopy().isEmpty()); + } + @Test void debtIsPreservedAndDailyShortfallsLiquidateWithoutTouchingLiveBranches() { guild.getBranch(0).levelUp(); guild.getBranch(0).levelUp(); diff --git a/src/test/java/net/tfminecraft/simplefactions/guild/income/LedgerCitizenTaxPersistenceTest.java b/src/test/java/net/tfminecraft/simplefactions/guild/income/LedgerCitizenTaxPersistenceTest.java index f79903d6..8335a07f 100644 --- a/src/test/java/net/tfminecraft/simplefactions/guild/income/LedgerCitizenTaxPersistenceTest.java +++ b/src/test/java/net/tfminecraft/simplefactions/guild/income/LedgerCitizenTaxPersistenceTest.java @@ -24,6 +24,22 @@ class LedgerCitizenTaxPersistenceTest { + @Test + void transactionContributorsCombineWithLegacyRowsWithoutChangingSavedAmounts() { + Guild guild = mock(Guild.class); + UUID incarnation = UUID.randomUUID(); + UUID character = UUID.randomUUID(); + when(guild.getIncarnationId()).thenReturn(incarnation); + Ledger ledger = new Ledger(guild); + ledger.setCitizenTaxes(Map.of(character.toString(), 0.50)); + try (var reads = mockStatic(FrozenReads.class)) { + reads.when(() -> FrozenReads.balance("guildtax:" + incarnation)).thenReturn(1234L); + reads.when(() -> FrozenReads.taxContributors(incarnation)).thenReturn(Map.of(character, 1000L)); + assertEquals(Map.of(character.toString(), 10.50, "Other citizen taxes", 1.84), ledger.getCitizenTaxBreakdown()); + assertEquals(Map.of(character.toString(), 0.50), ledger.getCitizenTaxesCopy()); + } + } + @Test void heldTaxIsVisibleWithoutLegacyContributorEntries() { Guild guild = mock(Guild.class); diff --git a/src/test/java/net/tfminecraft/simplefactions/managers/inventory/GuildMenusCoverageTest.java b/src/test/java/net/tfminecraft/simplefactions/managers/inventory/GuildMenusCoverageTest.java index bc9c060b..49253dd0 100644 --- a/src/test/java/net/tfminecraft/simplefactions/managers/inventory/GuildMenusCoverageTest.java +++ b/src/test/java/net/tfminecraft/simplefactions/managers/inventory/GuildMenusCoverageTest.java @@ -10,7 +10,13 @@ import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import java.util.UUID; import java.util.function.Consumer; +import net.tfminecraft.denareconomy.DenarEconomy; +import net.tfminecraft.denareconomy.api.Denars; +import net.tfminecraft.denareconomy.api.Leg; +import net.tfminecraft.denareconomy.api.Result; +import net.tfminecraft.denareconomy.api.TaxLeg; import net.tfminecraft.simplefactions.Cache; import net.tfminecraft.simplefactions.database.GuildData; import net.tfminecraft.simplefactions.diplomacy.RelationType; @@ -39,6 +45,7 @@ import net.tfminecraft.simplefactions.guild.upgrade.Upgrade; import net.tfminecraft.simplefactions.guild.upgrade.UpgradeExpansion; import net.tfminecraft.simplefactions.inactivity.InactivityService; +import net.tfminecraft.simplefactions.identity.Members; import net.tfminecraft.simplefactions.keys.Keys; import net.tfminecraft.simplefactions.loaders.BranchLoader; import net.tfminecraft.simplefactions.loaders.RelationLoader; @@ -641,6 +648,29 @@ void citizenTooltipShowsHeldTaxWithoutLegacyContributorRows() { } } + @Test + void citizenTooltipNamesTodaysContributorAndRetainsItAfterRollover() { + var character = UUID.randomUUID(); + var holding = Denars.guildTax(guild.getIncarnationId()); + assertEquals(Result.APPLIED, DenarEconomy.getLedger().postEarning("daily-tax", "earn", + List.of(new Leg(Denars.MINT, -1234), new Leg(holding, 1234)), character, new TaxLeg(holding, 1234))); + try (var members = mockStatic(Members.class, CALLS_REAL_METHODS)) { + members.when(() -> Members.display(character.toString())).thenReturn("Daily Citizen"); + ItemStack today = creator.createLedgerCitizensItem(guild); + assertLore(today, "Today: +12.34d", true); + assertLore(today, "Daily Citizen: +12.34d", true); + assertLore(today, "Other citizen taxes", false); + guild.getLedger().getHistory().closeDay(Map.of(LedgerHistory.Source.CITIZENS, guild.getLedger().getCitizenTaxBreakdown())); + assertEquals(Result.APPLIED, Denars.transfer("daily-settle", "settle", holding, guild.getBank().accountId(), 1234)); + ItemStack yesterday = creator.createLedgerCitizensItem(guild); + assertLore(yesterday, "Today: +0.00d", true); + assertLore(yesterday, "Last day: +12.34d", true); + assertLore(yesterday, "Lifetime: +12.34d", true); + assertEquals(2, yesterday.getItemMeta().getLore().stream() + .filter(line -> ChatColor.stripColor(line).equals("Daily Citizen: +12.34d")).count()); + } + } + @Test void ledgerSourcesSortCapAndSeparateTodayFromHistoricalTotals() { Guild display = displayGuild(guild); From 6f409a747ff98a2018db38e5fa65d39bb991693e Mon Sep 17 00:00:00 2001 From: Ryan Barlow <7389646+ryanbarlow97@users.noreply.github.com> Date: Sat, 17 Oct 2026 15:03:42 +0000 Subject: [PATCH 2/2] build: retain snapshot version for tagged releases --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 7052c262..d9641926 100644 --- a/pom.xml +++ b/pom.xml @@ -6,7 +6,7 @@ net.tfminecraft simplefactions - 4.2.0 + main-SNAPSHOT