financial page with bank data
All checks were successful
Deploy / build-deploy (push) Successful in 1m48s
All checks were successful
Deploy / build-deploy (push) Successful in 1m48s
This commit is contained in:
parent
e68d2c245c
commit
33c68c2f44
11 changed files with 394 additions and 87 deletions
|
|
@ -1426,24 +1426,23 @@ void RunMoneySelfTest() {
|
|||
const Financials fin = LoadFinancials(
|
||||
R"({"as_of":"2026-08-14",)"
|
||||
R"("donations":{"count":3,"total_minor":4500},)"
|
||||
R"("recurring":[{"label":"Hosting","total_minor":1200},)"
|
||||
R"({"label":"Insurance","total_minor":3600}],)"
|
||||
R"("single":[{"label":"Inventory","total_minor":230000}]})");
|
||||
R"("expenses":[{"label":"Hosting","total_minor":1200},)"
|
||||
R"({"label":"Insurance","total_minor":3600},)"
|
||||
R"({"label":"Inventory","total_minor":230000}]})");
|
||||
Check(fin.Loaded(), "financials: loads");
|
||||
Check(fin.donationCount == 3 && fin.donationsMinor == 4500,
|
||||
"financials: donations aggregate");
|
||||
Check(fin.recurring.size() == 2 && fin.recurring[0].label == "Hosting"
|
||||
&& fin.recurring[1].totalMinor == 3600,
|
||||
"financials: recurring categories in order");
|
||||
Check(fin.single.size() == 1 && fin.single[0].label == "Inventory",
|
||||
"financials: one-off categories");
|
||||
Check(fin.expenses.size() == 3 && fin.expenses[0].label == "Hosting"
|
||||
&& fin.expenses[1].totalMinor == 3600
|
||||
&& fin.expenses[2].label == "Inventory",
|
||||
"financials: expense categories in file order");
|
||||
Check(fin.ExpensesMinor() == 234800, "financials: expense total");
|
||||
Check(!LoadFinancials("garbage").Loaded(),
|
||||
"financials: malformed input yields none");
|
||||
Check(!LoadFinancials(R"({"donations":{"count":1,"total_minor":1}})").Loaded(),
|
||||
"financials: undated figures stay unpublished");
|
||||
Check(LoadFinancials(R"({"as_of":"2026-08-14","recurring":[{"total_minor":5}]})")
|
||||
.recurring.empty(),
|
||||
Check(LoadFinancials(R"({"as_of":"2026-08-14","expenses":[{"total_minor":5}]})")
|
||||
.expenses.empty(),
|
||||
"financials: a category without a label is dropped");
|
||||
|
||||
Check(ParseRoute("/financials").kind == RouteKind::Financials,
|
||||
|
|
@ -1458,8 +1457,19 @@ void RunMoneySelfTest() {
|
|||
Check(notes.slug == "financials" && !notes.lede.empty()
|
||||
&& notes.sections.size() >= 2,
|
||||
"content: financials notes present");
|
||||
Check(notes.lede.find("never published") != std::string::npos,
|
||||
"content: financials lede states the privacy promise");
|
||||
// The PROMISE, not the wording that happens to carry it. Pinning a
|
||||
// phrase in the lede made rewriting the page's opening sentence a
|
||||
// test failure, which is backwards: the lede is voice, the promise
|
||||
// below is the commitment that must survive every edit.
|
||||
bool statesPromise = false;
|
||||
for (const LegalSection& sec : notes.sections) {
|
||||
for (const std::string& para : sec.body) {
|
||||
if (para.find("No individual transactions") != std::string::npos) {
|
||||
statesPromise = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
Check(statesPromise, "content: financials page states what it never publishes");
|
||||
|
||||
// The rendered page: live sales plus the bank aggregates, with the
|
||||
// machine-readable copy the e2e suite reads.
|
||||
|
|
@ -1475,6 +1485,15 @@ void RunMoneySelfTest() {
|
|||
Check(fp.main.View().find("Hosting") != std::string_view::npos
|
||||
&& fp.main.View().find("€2348") != std::string_view::npos,
|
||||
"financials: expense categories and their total render");
|
||||
// Net = income - expenses = (4500 + 113745) - 234800 = -116555.
|
||||
// Deliberately a NEGATIVE case: a shop that has just bought stock is
|
||||
// the normal way for this figure to go below zero, and "€-1165.55" is
|
||||
// what must render rather than a mangled or unsigned number.
|
||||
Check(fp.main.View().find("data-fin-net-minor=\"-116555\"") != std::string_view::npos
|
||||
&& fp.main.View().find("€-1165.55") != std::string_view::npos,
|
||||
"financials: net renders, and renders negative honestly");
|
||||
Check(Money::FormatEuro(-26260) == "€-262.60" && Money::FormatEuro(-500) == "€-5.00",
|
||||
"financials: negative euro formatting");
|
||||
|
||||
// Before the bank figures exist the page says so instead of lying
|
||||
// with zeros — and publishes no donation figures at all.
|
||||
|
|
@ -1483,6 +1502,12 @@ void RunMoneySelfTest() {
|
|||
&& bare.main.View().find("not been published yet") != std::string_view::npos
|
||||
&& bare.main.View().find("data-fin-donations-count") == std::string_view::npos,
|
||||
"financials: unpublished bank figures say so and publish nothing");
|
||||
// And no net either: income minus an unknown expense side is not a
|
||||
// net of anything, and printing sales there would read as a company
|
||||
// with no costs.
|
||||
Check(bare.main.View().find("data-fin-net-minor") == std::string_view::npos
|
||||
&& bare.main.View().find(">Net<") == std::string_view::npos,
|
||||
"financials: no net figure while expenses are unpublished");
|
||||
|
||||
// Lifetime sales: ever-paid counts, awaiting doesn't, a refund after
|
||||
// payment stays counted, a hand-shipped legacy order counts too.
|
||||
|
|
@ -1554,11 +1579,11 @@ void RunMoneySelfTest() {
|
|||
R"({"donation_accounts":[9911],)"
|
||||
R"("rules":[)"
|
||||
R"({"iban":"NL01OWNSELF0000000","group":"ignore"},)"
|
||||
R"({"description_contains":"hetzner","group":"recurring","label":"Hosting"},)"
|
||||
R"({"iban":"DE02SUPPLIER000000","group":"single","label":"Inventory"},)"
|
||||
R"({"group":"single","label":"Claims everything"},)"
|
||||
R"({"description_contains":"hetzner","group":"expense","label":"Hosting"},)"
|
||||
R"({"iban":"DE02SUPPLIER000000","group":"expense","label":"Inventory"},)"
|
||||
R"({"group":"expense","label":"Claims everything"},)"
|
||||
R"({"iban":"NL03TYPO0000000000","group":"nonsense","label":"X"},)"
|
||||
R"({"iban":"NL04NOLABEL0000000","group":"recurring"}]})");
|
||||
R"({"iban":"NL04NOLABEL0000000","group":"expense"}]})");
|
||||
Check(rules.donationAccounts.size() == 1 && rules.donationAccounts[0] == "9911",
|
||||
"bunq: numeric donation account loads as text");
|
||||
// Three of the six survive: the criterion-less rule would claim every
|
||||
|
|
@ -1601,7 +1626,7 @@ void RunMoneySelfTest() {
|
|||
bill.description = "HETZNER ONLINE GMBH invoice";
|
||||
bill.created = "2026-08-15";
|
||||
const Server::MutationClass billClass = Server::ClassifyMutation(bill, rules);
|
||||
Check(billClass.group == "recurring" && billClass.label == "Hosting",
|
||||
Check(billClass.group == "expense" && billClass.label == "Hosting",
|
||||
"bunq: description matching, case-insensitively");
|
||||
|
||||
// Folding into the aggregates.
|
||||
|
|
@ -1611,8 +1636,8 @@ void RunMoneySelfTest() {
|
|||
"bunq: a donation moves the count and the total");
|
||||
Check(fin.asOf == "2026-08-14", "bunq: as-of follows the mutation date");
|
||||
Server::ApplyMutation(fin, billClass, bill);
|
||||
Check(fin.recurring.size() == 1 && fin.recurring[0].label == "Hosting"
|
||||
&& fin.recurring[0].totalMinor == 1200,
|
||||
Check(fin.expenses.size() == 1 && fin.expenses[0].label == "Hosting"
|
||||
&& fin.expenses[0].totalMinor == 1200,
|
||||
"bunq: an outgoing bill becomes a positive expense");
|
||||
Check(fin.asOf == "2026-08-15", "bunq: as-of advances");
|
||||
// A supplier refund reduces the category rather than appearing as
|
||||
|
|
@ -1621,7 +1646,7 @@ void RunMoneySelfTest() {
|
|||
refund.amountMinor = 500;
|
||||
refund.created = "2026-08-01";
|
||||
Server::ApplyMutation(fin, billClass, refund);
|
||||
Check(fin.recurring[0].totalMinor == 700, "bunq: a refund reduces its category");
|
||||
Check(fin.expenses[0].totalMinor == 700, "bunq: a refund reduces its category");
|
||||
Check(fin.asOf == "2026-08-15", "bunq: as-of never moves backwards");
|
||||
// An unclassified mutation touches nothing at all.
|
||||
const Financials before = fin;
|
||||
|
|
|
|||
Loading…
Reference in a new issue