Skip to content

Configurable per-property tax by region tag + formula#16

Merged
ParadauxIO merged 2 commits into
mainfrom
feature/configurable-property-tax
May 30, 2026
Merged

Configurable per-property tax by region tag + formula#16
ParadauxIO merged 2 commits into
mainfrom
feature/configurable-property-tax

Conversation

@ParadauxIO
Copy link
Copy Markdown
Contributor

@ParadauxIO ParadauxIO commented May 30, 2026

What

Replaces the single hardcoded property-tax formula with a configurable formula that follows the Taxation Act, plus an optional tag-matched override mechanism for local governments.

Property tax is a single function of an owner's plot count, evaluated once per owner (not per property), rounded down to the nearest cent, with owners of 7 or fewer plots exempt β€” exactly as the Act specifies.

taxes.yml ships the Act's formula with overrides disabled:

exempt-plot-threshold: 7
rules: []
default-formula: "0.25 * 1.16^<plots> + 0.3 * <plots>^2 + 2.5 * <plots> - 25"

This reproduces the Act's published figures: ~$15.01/day at 8 plots, ~$149.86 at 20, ~$1,267 at 50.

Semantics

  • default-formula is the federal tax: the owner's total daily tax as a function of <plots> (their freehold plot count). It is evaluated once on the federally-taxed plot count and floored to the cent; owners at/below exempt-plot-threshold plots pay nothing. exempt-uuids are also honoured.
  • rules are optional local-government overrides (the Act's "unless otherwise provided by Local Governments where the plot is located" clause). A freehold region whose tags match a rule (first match, top-to-bottom) is taxed per-property by that rule and is excluded from the federal count. Rules ship empty, so by default the Act applies uniformly to every plot.
  • <plots> = the owner's total freehold plot count. match.all β€” region must carry every listed tag; match.any β€” at least one; empty/omitted = no constraint. Case-insensitive; tag ids are the tag-ids from region-tags.yml.

How

  • TaxFormula β€” recursive-descent evaluator over <plots>: + - * /, right-associative ^ (incl. a constant base raised to the variable, e.g. 1.16^<plots>), unary minus, parens, decimals. Compile-once / evaluate-many; throws on malformed input.
  • TagMatch + TaxRule config records; TaxSettings carries rules, default-formula, exempt-plot-threshold. DEFAULT_FORMULA is the Act's formula.
  • PropertyTaxPolicy.taxForOwner(plotTagSets, exemptThreshold) β€” partitions an owner's plots into federal (no matching rule) and overridden (matched); the federal formula is evaluated once on the federal count above the threshold, per-rule overrides are summed, the total is floored. Bad rule formulas are logged & dropped (that plot falls back to federal); an invalid default falls back to the built-in Act formula; non-finite/non-positive results clamp to zero.
  • Backend β€” TitleHeldRegionTag entity + FreeholdContractMapper.selectTitleHeldRegionTags() (FreeholdContract β†’ Contract β†’ RealtyRegion LEFT JOIN RegionTag). No schema migration needed.
  • PropertyTaxListener β€” enumerates regions, groups by owner, and emits one charge per owner from a single taxForOwner call.

Testing

  • PropertyTaxPolicyTest β€” asserts the Act's behaviour: evaluated once per owner (and explicitly not the old NΓ— value), the ≀7-plot exemption, floor rounding, the published figures ($15.01 @ 8, $149.86 @ 20), the local-override path, case-insensitive matching, bad-formula fallback, and invalid-default fallback.
  • TaxFormulaTest β€” precedence, right-assoc power, and the Act's 1.16^<plots> exponential term + full default formula.
  • Backend integration test for the new query (Testcontainers in CI).
  • :realty-paper:test green.

Incidental

Fixes a pre-existing compile break in AbstractDatabaseTest β€” its RealtyBackendImpl name-resolver arg had drifted to an async (CompletableFuture) signature, which was blocking the entire realty-backend test source set.

πŸ€– Generated with Claude Code

ParadauxIO and others added 2 commits May 30, 2026 10:53
Property tax was a single hardcoded formula per owner (2.5*plots^2 - 6*plots).
Replace it with a configurable, tag-matched ruleset assessed per property.

For each freehold region an owner holds, the first matching rule (top-to-
bottom) supplies a formula; if none match, default-formula applies. Each
property's tax is summed into one daily charge per owner. <plots> binds to
the owner's total freehold count, and the exemption threshold is configurable.

- TaxFormula: recursive-descent evaluator over <plots> (+ - * /, right-assoc ^,
  unary minus, parens, decimals). Compile-once / evaluate-many.
- TagMatch (all/any, case-insensitive) + TaxRule config records; TaxSettings
  gains rules, default-formula, exempt-plot-threshold. Default taxes.yml ships
  an example ruleset.
- PropertyTaxPolicy: compiles the ruleset (bad formulas logged + dropped,
  invalid default falls back to the built-in), first-match selection, clamps
  non-positive/non-finite to zero.
- Backend: TitleHeldRegionTag entity + FreeholdContractMapper.
  selectTitleHeldRegionTags() (FreeholdContract -> Contract -> RealtyRegion
  LEFT JOIN RegionTag) to enumerate each owner's regions with their tags.
  No schema migration needed.
- Rewrote PropertyTaxListener to enumerate per property, group by owner, apply
  the configurable threshold + exempt-uuids, emit one summed charge per owner.

Tests: TaxFormulaTest (12) + PropertyTaxPolicyTest (9) pass; backend
integration test for the new query (runs under Testcontainers in CI).

Also fixes a pre-existing compile break in AbstractDatabaseTest (its
RealtyBackendImpl name-resolver arg had drifted to an async signature),
which was blocking the realty-backend test source set.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The previous per-property summation charged N Γ— formula(N) (cubic) with a
quadratic default and a 3-plot exemption β€” overcharging by 60–235Γ— vs the
Taxation Act and taxing owners the Act exempts.

Align with the Act:
- Tax is a single function of plot count, evaluated ONCE per owner (not summed
  per property), rounded DOWN to the cent.
- Default formula = the Act's: 0.25*1.16^<plots> + 0.3*<plots>^2 + 2.5*<plots> - 25.
- Exemption threshold = 7 plots (was 3).
- Verified against the Act's published figures: $15.01 @ 8 plots, $149.86 @ 20.

Tagging is retained as optional local-government overrides (the Act's "unless
otherwise provided by Local Governments" clause): a tagged plot is taxed
per-property by its rule and excluded from the federal count. Rules ship empty,
so by default the Act applies uniformly.

PropertyTaxPolicy.taxForRegion β†’ taxForOwner; tests assert once-per-owner
evaluation, the exemption, floor rounding, the published figures, and that the
parser handles the 1.16^<plots> term.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@ParadauxIO ParadauxIO merged commit 87b2011 into main May 30, 2026
1 check passed
@ParadauxIO ParadauxIO deleted the feature/configurable-property-tax branch May 31, 2026 00:44
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant