feat(domain): allocation rules become a dated series
Post.standingRule becomes Post.rules: RuleVersion[], resolved by ruleAt() as the last version effective at or before a month. Per-month overrides still win outright; a post with no applicable version allocates zero rather than applying its earliest rule backwards. allocationFor keeps its signature, so the fold, the views and the ODS export are untouched. Settings gets a read-only rule summary for now; the history editor follows.
This commit is contained in:
1 parent
c74a66aea9
commit
68712e72a3
11 files changed
+252
-151
No files matched your search
@@ -1,5 +1,6 @@
|
||||
import { toBase } from "./fx.ts";
|
||||
import { roundMoney } from "./money.ts";
|
||||
import { compareMonths } from "./months.ts";
|
||||
import type {
|
||||
Currency,
|
||||
Dataset,
|
||||
@@ -9,6 +10,7 @@ import type {
|
||||
Post,
|
||||
PostId,
|
||||
Rule,
|
||||
RuleVersion,
|
||||
} from "./types.ts";
|
||||
|
||||
/** Resolves a rule to a base-currency amount. Percentages may exceed 100. */
|
||||
@@ -24,8 +26,36 @@ export function resolveRule(
|
||||
return roundMoney((income * rule.percent) / 100, baseCurrency);
|
||||
}
|
||||
|
||||
export function ruleForPost(month: Month | undefined, post: Post): Rule {
|
||||
return month?.ruleOverrides[post.id] ?? post.standingRule;
|
||||
/**
|
||||
* The version in effect for `monthId`: the last one whose `from` is at or
|
||||
* before it, or null when the post was not yet budgeted.
|
||||
*
|
||||
* Scans rather than assuming sorted order. The array is kept sorted by
|
||||
* `setRuleFrom`, but a hand-edited import must still resolve correctly.
|
||||
*/
|
||||
export function ruleAt(post: Post, monthId: MonthId): RuleVersion | null {
|
||||
let effective: RuleVersion | null = null;
|
||||
for (const version of post.rules) {
|
||||
if (compareMonths(version.from, monthId) > 0) continue;
|
||||
if (effective === null || compareMonths(version.from, effective.from) > 0) {
|
||||
effective = version;
|
||||
}
|
||||
}
|
||||
return effective;
|
||||
}
|
||||
|
||||
/**
|
||||
* Precedence: a one-month override wins outright; otherwise the version in
|
||||
* effect; otherwise nothing, which the caller renders as an allocation of zero.
|
||||
*/
|
||||
export function ruleForPost(
|
||||
month: Month | undefined,
|
||||
post: Post,
|
||||
monthId: MonthId,
|
||||
): Rule | null {
|
||||
const override = month?.ruleOverrides[post.id];
|
||||
if (override) return override;
|
||||
return ruleAt(post, monthId)?.rule ?? null;
|
||||
}
|
||||
|
||||
export function isOverridden(month: Month | undefined, postId: PostId): boolean {
|
||||
@@ -46,8 +76,10 @@ export function allocationFor(
|
||||
const post = dataset.posts.find((p) => p.id === postId);
|
||||
if (!post) throw new Error(`Unknown post: ${postId}`);
|
||||
const month = dataset.months.find((m) => m.id === monthId);
|
||||
const rule = ruleForPost(month, post, monthId);
|
||||
if (rule === null) return 0;
|
||||
return resolveRule(
|
||||
ruleForPost(month, post),
|
||||
rule,
|
||||
incomeFor(dataset, monthId),
|
||||
dataset.settings.baseCurrency,
|
||||
dataset.fxRates,
|
||||
|
||||
Reference in new issue
Block a user