finacial page
All checks were successful
Deploy / build-deploy (push) Successful in 2m20s

This commit is contained in:
Jorijn van der Graaf 2026-08-14 02:50:58 +02:00
commit e68d2c245c
17 changed files with 1801 additions and 15 deletions

View file

@ -281,6 +281,67 @@ export Rates LoadRates(std::string_view json) {
return out;
}
// The bank-derived aggregates for the public /financials page, read from a
// state file the owner's tooling writes (<orders>.financials.json on the
// server). Aggregates by construction: a category is a label and a running
// total, donations are a count and a running total, and nothing finer ever
// exists in this structure — that is the page's privacy design, not an
// implementation shortcut. Sales are not in here: they fold live out of the
// order ledger on the server and arrive at the renderer as two integers.
export struct FinCategory {
std::string label; // "Hosting" — shown verbatim
std::int64_t totalMinor = 0; // running total, EUR cents
};
export struct Financials {
std::string asOf; // ISO date the figures are current to;
// empty = nothing published yet
std::int64_t donationCount = 0;
std::int64_t donationsMinor = 0;
std::vector<FinCategory> recurring; // insurance, hosting, …
std::vector<FinCategory> single; // inventory, fees, tax, …
bool Loaded() const { return !asOf.empty(); }
std::int64_t ExpensesMinor() const {
std::int64_t sum = 0;
for (const FinCategory& c : recurring) sum += c.totalMinor;
for (const FinCategory& c : single) sum += c.totalMinor;
return sum;
}
};
export Financials LoadFinancials(std::string_view json) {
Financials out;
auto doc = Json::Parse(json);
if (!doc || !doc->IsObject()) return out;
out.asOf = std::string(doc->Str("as_of"));
// Undated figures stay unpublished: the page promises an honest
// freshness line, and numbers that cannot carry one are not shown.
if (out.asOf.empty()) return out;
if (const Json::Value* d = doc->Find("donations"); d && d->IsObject()) {
out.donationCount = d->Int("count");
out.donationsMinor = d->Int("total_minor");
}
auto categories = [](const Json::Value* arr) {
std::vector<FinCategory> cats;
if (!arr || !arr->IsArray()) return cats;
for (const Json::Value& v : arr->array) {
if (!v.IsObject()) continue;
FinCategory c;
c.label = std::string(v.Str("label"));
c.totalMinor = v.Int("total_minor");
// A category with no label has nothing to render as; dropping it
// beats an anonymous row that looks like a redaction.
if (c.label.empty()) continue;
cats.push_back(std::move(c));
}
return cats;
};
out.recurring = categories(doc->Find("recurring"));
out.single = categories(doc->Find("single"));
return out;
}
// Everything the order status page needs to render — a projection of the
// server's order record, not the record itself. The renderer stays a pure
// function in Shared; the server owns storage and fills this in.