fix: make government account resolution symmetric across transfers and balance checks - #27
Merged
Merged
Conversation
PR #17 made recipient resolution prefer a party's GOVERNMENT account over their PERSONAL one, so rent from a government-owned leasehold lands in the government treasury. The paying side was never changed: transfer() still resolved the payer with resolveOrCreatePersonal, so when the government is the payer -- a lease-termination refund, landlord -> tenant -- the money came out of the entity's personal balance instead of the treasury the rent went into. Resolve both sides through the same GOVERNMENT > PERSONAL > BUSINESS > first-available lookup. Ordinary players are unaffected: with no government account they still resolve to PERSONAL, and a firm proprietor still pays from their personal balance rather than a BUSINESS account they own. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SUskvuegwBzQMufaYxp5eM
getBalance still went through getBalanceByOwnerUuid, which does not apply the GOVERNMENT > PERSONAL > BUSINESS preference the two sides of transfer() now share. An affordability check on a government entity therefore inspected its personal balance while the payment itself would debit the treasury. Extract the preference into preferredAccount(List<Account>) and have both getBalance and resolveAccount use it, so a balance check and the transfer it gates can never disagree about which account is in play. getBalance keeps its no-account-means-zero contract and deliberately has no create-if-missing fallback -- a read must not open an account. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SUskvuegwBzQMufaYxp5eM
Government account resolution is now symmetric across transfers and balance checks; a bugfix release. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SUskvuegwBzQMufaYxp5eM
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Follow-up to #17. Branched from
main, independent of the notification-adapter work.Problem
#17 made the recipient side of
TreasuryEconomyProvider.transferprefer a party'sGOVERNMENTaccount over theirPERSONALone, so rent from a government-owned leasehold lands in the government treasury.The paying side was never changed.
transferstill resolved the payer withtreasuryApi.resolveOrCreatePersonal(fromId)unconditionally, so when the government is the payer the money came out of the entity's personal balance instead of the treasury the rent went into. That happens on every lease-termination refund, which islandlordId → tenantId:Realty.java:509— scheduled terminations refunding prepaid-but-unused timeRealtyPaperApiImpl.java:483— the defensive refund when the backend update failsNet effect: renting a government region credits the treasury, un-renting it debits the personal account. The treasury only ever grows.
getBalancehad the same asymmetry from the other direction — it usedgetBalanceByOwnerUuid, which does not apply the preference at all, so an affordability check on a government entity inspected a different account than the payment it gates would touch.Fix
One preference, applied everywhere:
GOVERNMENT > PERSONAL > BUSINESS > first-available.preferredAccount(List<Account>), returning empty when the party holds no accounts.transferresolves both payer and recipient throughresolveAccount, which falls back toresolveOrCreatePersonalwhen there are none.getBalanceuses the same preference and readsgetBalanceByAccountId. It keeps its no-account-means-zero contract and deliberately has no create-if-missing fallback — a balance read must not have the side effect of opening an account.Ordinary players are unaffected: with no government account they still resolve to
PERSONAL, and a firm proprietor still pays from their personal balance rather than aBUSINESSaccount owned by their own UUID (the leak #17 originally fixed).Tests
Written before the fix; all failed first, all pass now.
TreasuryEconomyProviderTest: 11 tests, 0 failures, full:realty-paper:testgreen.New payer-side coverage mirroring the existing recipient-side cases:
governmentPayer_refundIsDebitedFromGovernmentNotPersonalfirmProprietorPayer_paysFromPersonalNotBusinesspayerWithNoAccounts_resolvesOrCreatesPersonalNew balance coverage:
governmentBalance_readsTheGovernmentAccountNotPersonalfirmProprietorBalance_readsPersonalNotBusinessbalanceWithNoAccounts_isZeroAndCreatesNothing— assertsresolveOrCreatePersonalis never calledbalanceOfNull_isZeroThe existing recipient-side test helper was reworked to stub the payer through
getAccountsByOwnerrather thanresolveOrCreatePersonal, matching the new resolution path.🤖 Generated with Claude Code
https://claude.ai/code/session_01SUskvuegwBzQMufaYxp5eM