Skip to content

feat!: make property tax a list of match/formula rules - #28

Merged
md5sha256 merged 1 commit into
mainfrom
feat/tax-rule-list
Aug 23, 2026
Merged

feat!: make property tax a list of match/formula rules#28
md5sha256 merged 1 commit into
mainfrom
feat/tax-rule-list

Conversation

@md5sha256

Copy link
Copy Markdown
Collaborator

The tax was a global default-formula plus exempt-plot-threshold, with rules as an optional per-property override. Scoping the tax to a subset of plots was not expressible: rule predicates have no negation, and a rule match removes a plot from the aggregate count, so the plots you want counted cannot be shielded from a catch-all.

The tax is now the rules list and nothing else. Each rule pairs a tag predicate with the formula charged to the plots it claims:

  • every plot falls to the FIRST rule matching its tags; a plot matching no rule is untaxed and counted nowhere
  • each rule's formula is evaluated ONCE per owner, with bound to that owner's plot count in THAT rule, not their total holdings
  • exempt-threshold moves onto the rule, so each bracket carries its own
  • charges are summed and floored to the cent

A server-wide tax is a rule with no match. Rules are aggregate rather than per-property, which loses nothing: a flat per-plot rate is 10 * <plots>. A rule whose formula fails to parse is dropped and its plots fall through to the next match, so a typo under-charges rather than charging something unintended.

BREAKING CHANGE: default-formula, exempt-plot-threshold and TaxSettings.DEFAULT_FORMULA are removed. An un-migrated taxes.yml has its rules key filled from the shipped defaults on startup, switching that server to the packaged rule. rules: [] does not disable collection — Configurate treats an empty list as absent, so the default returns on the next restart; enabled: false is the off-switch.

Tests: 18 policy tests cover scoping, per-rule counting and exemption, rule ordering and drop-on-parse-failure. A new TaxesConfigTest loads the packaged taxes.yml as the plugin does and pins that it deserializes, that its formulas compile, that it charges the figures its own comments quote, that every tag it names exists in region-tags.yml, and both startup-merge behaviours above.

The tax was a global `default-formula` plus `exempt-plot-threshold`, with
`rules` as an optional per-property override. Scoping the tax to a subset of
plots was not expressible: rule predicates have no negation, and a rule match
removes a plot from the aggregate count, so the plots you want counted cannot
be shielded from a catch-all.

The tax is now the `rules` list and nothing else. Each rule pairs a tag
predicate with the formula charged to the plots it claims:

- every plot falls to the FIRST rule matching its tags; a plot matching no
  rule is untaxed and counted nowhere
- each rule's formula is evaluated ONCE per owner, with <plots> bound to that
  owner's plot count in THAT rule, not their total holdings
- `exempt-threshold` moves onto the rule, so each bracket carries its own
- charges are summed and floored to the cent

A server-wide tax is a rule with no `match`. Rules are aggregate rather than
per-property, which loses nothing: a flat per-plot rate is `10 * <plots>`.
A rule whose formula fails to parse is dropped and its plots fall through to
the next match, so a typo under-charges rather than charging something
unintended.

BREAKING CHANGE: `default-formula`, `exempt-plot-threshold` and
TaxSettings.DEFAULT_FORMULA are removed. An un-migrated taxes.yml has its
`rules` key filled from the shipped defaults on startup, switching that server
to the packaged rule. `rules: []` does not disable collection — Configurate
treats an empty list as absent, so the default returns on the next restart;
`enabled: false` is the off-switch.

Tests: 18 policy tests cover scoping, per-rule counting and exemption, rule
ordering and drop-on-parse-failure. A new TaxesConfigTest loads the packaged
taxes.yml as the plugin does and pins that it deserializes, that its formulas
compile, that it charges the figures its own comments quote, that every tag it
names exists in region-tags.yml, and both startup-merge behaviours above.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@md5sha256
md5sha256 merged commit 71f4957 into main Aug 23, 2026
1 check failed
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.

2 participants