diff --git a/realty-paper/src/main/java/io/github/md5sha256/realty/economy/TreasuryEconomyProvider.java b/realty-paper/src/main/java/io/github/md5sha256/realty/economy/TreasuryEconomyProvider.java index 1294cd9..28fa680 100644 --- a/realty-paper/src/main/java/io/github/md5sha256/realty/economy/TreasuryEconomyProvider.java +++ b/realty-paper/src/main/java/io/github/md5sha256/realty/economy/TreasuryEconomyProvider.java @@ -7,6 +7,7 @@ import org.jetbrains.annotations.NotNull; import java.math.BigDecimal; +import java.math.RoundingMode; import java.util.List; import java.util.UUID; @@ -17,10 +18,11 @@ *

* Account resolution: the payer is always resolved as a personal account * (created with starting balance if missing). The recipient is resolved by - * preferring its PERSONAL account — rental/sale income belongs to the - * landlord personally, even if they happen to own a firm. Only when the - * recipient has no personal account (a synthetic authority UUID backing a - * government entity) do we fall back to a government/business account. + * preferring its GOVERNMENT account, then PERSONAL, then BUSINESS — so + * government landlords (legacy DCGovernment-style real UUIDs that own both a + * personal and a government account) route income to their government + * treasury, while ordinary landlords still get their personal balance rather + * than a firm BUSINESS account they happen to own. */ public final class TreasuryEconomyProvider implements EconomyProvider { @@ -47,10 +49,14 @@ public double getBalance(@NotNull UUID playerId) { try { Account payer = treasuryApi.resolveOrCreatePersonal(fromId); Account recipient = resolveRecipientAccount(toId); + // Treasury rejects amounts with more than 2 decimal places. Amounts + // derived from arithmetic (e.g. pro-rata refunds: price * remaining / + // total) can carry extra precision, so normalise to 2 decimals here. + BigDecimal normalisedAmount = BigDecimal.valueOf(amount).setScale(2, RoundingMode.HALF_UP); treasuryApi.transfer(new TransferRequest( payer.getAccountId(), recipient.getAccountId(), - BigDecimal.valueOf(amount), + normalisedAmount, ledgerMessage, fromId, null, @@ -74,27 +80,31 @@ public boolean hasLedgerSupport() { } /** - * Resolves the recipient's Treasury account. + * Resolves the recipient's Treasury account, preferring + * GOVERNMENT > PERSONAL > BUSINESS > first-available. *

- * A real player landlord always owns a PERSONAL account (Treasury enforces - * one per player), so we prefer it: their rental/sale income must land in - * their personal balance, never in a firm BUSINESS account they happen to - * own (firm accounts are owned by the proprietor's own UUID, which is how - * such funds previously leaked into business accounts). + * GOVERNMENT wins first because legacy government entities (e.g. + * DCGovernment) are real Minecraft accounts whose UUID owns both a + * personal and a government account; their leasehold income must land in + * the government treasury, not the player's personal balance. *

- * Only when the recipient has no personal account — i.e. a synthetic - * authority UUID that backs a government entity — do we fall back to the - * prior government > business > first-available ordering so authority - * payments still route to the configured government treasury account. + * Ordinary landlords have no government account, so PERSONAL is chosen next: + * rental/sale income belongs to them personally, never a firm BUSINESS + * account they happen to own (firm accounts are owned by the proprietor's + * own UUID, which is how such funds previously leaked into business + * accounts). + *

+ * When the recipient has no account at all, resolve-or-create their personal + * account. */ private @NotNull Account resolveRecipientAccount(@NotNull UUID ownerUuid) { List accounts = treasuryApi.getAccountsByOwner(ownerUuid); if (!accounts.isEmpty()) { return accounts.stream() - .filter(a -> a.getAccountType() == AccountType.PERSONAL) + .filter(a -> a.getAccountType() == AccountType.GOVERNMENT) .findFirst() .or(() -> accounts.stream() - .filter(a -> a.getAccountType() == AccountType.GOVERNMENT) + .filter(a -> a.getAccountType() == AccountType.PERSONAL) .findFirst()) .or(() -> accounts.stream() .filter(a -> a.getAccountType() == AccountType.BUSINESS) diff --git a/realty-paper/src/test/java/io/github/md5sha256/realty/economy/TreasuryEconomyProviderTest.java b/realty-paper/src/test/java/io/github/md5sha256/realty/economy/TreasuryEconomyProviderTest.java index 28412d7..aaf98c5 100644 --- a/realty-paper/src/test/java/io/github/md5sha256/realty/economy/TreasuryEconomyProviderTest.java +++ b/realty-paper/src/test/java/io/github/md5sha256/realty/economy/TreasuryEconomyProviderTest.java @@ -55,7 +55,8 @@ private int capturedDestination(UUID recipient) { ArgumentCaptor req = ArgumentCaptor.forClass(TransferRequest.class); verify(treasuryApi).transfer(req.capture()); assertEquals(payerPersonal.getAccountId(), req.getValue().fromAccountId()); - assertEquals(BigDecimal.valueOf(50.0), req.getValue().amount()); + // amount() is normalised to scale 2; compareTo is scale-insensitive. + assertEquals(0, new BigDecimal("50.00").compareTo(req.getValue().amount())); return req.getValue().toAccountId(); } @@ -72,6 +73,20 @@ void landlordWithFirm_routesToPersonalNotBusiness() { "rent must land in the landlord's personal account, not their firm"); } + @Test + void legacyGovernment_withPersonalAndGovernmentAccount_routesToGovernment() { + UUID government = UUID.randomUUID(); + // Legacy DCGovernment-style entity: a real Minecraft UUID that owns both + // a personal account (the original player) and the government account. + // Leasehold income must route to the government account, not personal. + when(treasuryApi.getAccountsByOwner(government)).thenReturn(List.of( + account(13, AccountType.PERSONAL, government), + account(9, AccountType.GOVERNMENT, government))); + + assertEquals(9, capturedDestination(government), + "government landlord income must route to the government account, not personal"); + } + @Test void authorityUuid_withOnlyGovernmentAccount_routesToGovernment() { UUID authority = UUID.randomUUID();