retire the bunq integration
All checks were successful
Deploy / build-deploy (push) Successful in 3m32s
All checks were successful
Deploy / build-deploy (push) Successful in 3m32s
The webhook is deregistered at bunq and the callback endpoint, its parser, default-deny classifier, dedup ledger and signature check are removed; the code is in git history if a bank feed ever comes back. /financials keeps reading the hand-maintained aggregates file, and sales + shop donations stay live from the order ledger. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
4c49000d46
commit
c31797bd9a
13 changed files with 70 additions and 1694 deletions
|
|
@ -139,36 +139,17 @@ export namespace Catcrafts::Server {
|
|||
|
||||
// ── the bank side of /financials ──────────────────────────────────
|
||||
//
|
||||
// Donations and expenses come from a bank mutation callback rather than
|
||||
// from an API key on this box: a bunq key can INITIATE PAYMENTS and has
|
||||
// no read-only scope, so the key stays on the owner's machine (IP-bound)
|
||||
// and is used there once to register a notification filter. From then on
|
||||
// bunq pushes mutations here, and the server can learn that money moved
|
||||
// without being able to move any.
|
||||
//
|
||||
// What crosses this boundary is deliberately small. A mutation is
|
||||
// classified, its amount lands in a category total, its opaque id goes in
|
||||
// a dedup ledger, and the name, IBAN and description are dropped before
|
||||
// anything is written. Classification is default-deny: money no rule
|
||||
// claims is withheld from the page and logged, never published as a guess.
|
||||
// The owner's weekly pull recomputes every total from the full bunq
|
||||
// history and overwrites the aggregates file, which is what makes this
|
||||
// path safe to be lossy.
|
||||
// Bank-side donations and expenses come from an aggregates file written
|
||||
// by the owner's own tooling, off this box — no bank credential and no
|
||||
// bank callback exists here (the bunq mutation callback was retired
|
||||
// 2026-08-17; its code is in git history). Aggregates by construction:
|
||||
// category totals and an as-of date are all the file can carry, which is
|
||||
// the page's privacy design.
|
||||
|
||||
struct FinancialsConfig {
|
||||
std::filesystem::path publicPath; // <orders>.financials.json — what
|
||||
// the page reads; also written by
|
||||
// the owner's reconciliation
|
||||
std::filesystem::path seenPath; // <orders>.financials-seen.json —
|
||||
// ingested mutation ids, so a
|
||||
// redelivery cannot double-count
|
||||
std::filesystem::path rulesPath; // <orders>.financial-rules.json —
|
||||
// the classifier, authored by hand
|
||||
std::string callbackSecret; // BUNQ_CALLBACK_SECRET; the last
|
||||
// path segment of the callback
|
||||
// URL. Empty = endpoint disabled
|
||||
std::filesystem::path publicKeyPem; // BUNQ_CALLBACK_PUBKEY; empty =
|
||||
// signature checking off
|
||||
// the page reads; written by the
|
||||
// owner's reconciliation
|
||||
};
|
||||
void ConfigureFinancials(FinancialsConfig config);
|
||||
|
||||
|
|
@ -177,72 +158,6 @@ export namespace Catcrafts::Server {
|
|||
// published yet, which the page states rather than showing zeros.
|
||||
Financials CurrentFinancials();
|
||||
|
||||
// One bank mutation, reduced to what a total needs. Everything
|
||||
// identifying is dropped by the classifier and never persisted.
|
||||
struct BankMutation {
|
||||
std::string id; // bunq's opaque id — the dedup key
|
||||
std::int64_t amountMinor = 0; // SIGNED: negative is money leaving
|
||||
std::string currency; // only EUR is ever counted
|
||||
std::string counterpartyIban; // classifier input; never stored
|
||||
std::string description; // classifier input; never stored
|
||||
std::string account; // monetary account id
|
||||
std::string created; // ISO date only — the time is
|
||||
// dropped at the parser
|
||||
};
|
||||
|
||||
// One classification rule. A rule matches when every criterion it states
|
||||
// matches; the first matching rule wins. `group` is "donations",
|
||||
// "expense" or "ignore" — anything else is a typo and the rule is dropped
|
||||
// at load rather than inventing a category. (Expenses were once split
|
||||
// into recurring/one-off; see Financials::expenses for why that went.)
|
||||
struct FinancialRule {
|
||||
std::string iban; // exact, case-insensitive
|
||||
std::string descriptionContains; // substring, case-insensitive
|
||||
std::string account; // monetary account id
|
||||
std::string group;
|
||||
std::string label; // the page's category name
|
||||
};
|
||||
|
||||
struct FinancialRules {
|
||||
// Accounts whose INCOMING money is a donation by default. Keyed on
|
||||
// the account because donors are strangers: no IBAN list can know
|
||||
// them in advance.
|
||||
std::vector<std::string> donationAccounts;
|
||||
std::vector<FinancialRule> rules;
|
||||
};
|
||||
|
||||
// Empty group = no rule claimed it. That is the default-deny answer, and
|
||||
// the caller must withhold rather than guess.
|
||||
struct MutationClass {
|
||||
std::string group;
|
||||
std::string label;
|
||||
};
|
||||
|
||||
// Pure and exported for the self-test.
|
||||
std::optional<std::int64_t> ParseSignedAmountToMinor(std::string_view s);
|
||||
std::optional<BankMutation> ParseBunqMutation(std::string_view json);
|
||||
FinancialRules LoadFinancialRules(std::string_view json);
|
||||
MutationClass ClassifyMutation(const BankMutation& m, const FinancialRules& rules);
|
||||
void ApplyMutation(Financials& fin, const MutationClass& cls, const BankMutation& m);
|
||||
|
||||
// Whether the callback endpoint exists at all. Unconfigured, the path is
|
||||
// a plain 404 — an endpoint that is off should not announce itself.
|
||||
bool BunqCallbackConfigured();
|
||||
|
||||
// Constant-time secret check, plus the RSA-SHA256 body signature when a
|
||||
// public key is configured. False means 404, for the same reason an
|
||||
// unknown order token is: a probe learns nothing from the shape.
|
||||
bool BunqCallbackAuthorised(std::string_view pathSecret, std::string_view body,
|
||||
std::string_view signatureB64);
|
||||
|
||||
enum class BunqIngestResult { Applied, Duplicate, Withheld, Ignored, Failed };
|
||||
|
||||
// Parse, classify, and fold one notification into the aggregates. Never
|
||||
// reports a transport-level error for an unparseable body: bunq retries
|
||||
// non-2xx, so an unknown payload shape would redeliver forever. It is
|
||||
// logged and left to the weekly reconciliation instead.
|
||||
BunqIngestResult IngestBunqNotification(std::string_view body);
|
||||
|
||||
// ── invoices ──────────────────────────────────────────────────────
|
||||
//
|
||||
// A paid order's invoice: plain markdown, clearsigned with the shop's
|
||||
|
|
|
|||
Loading…
Reference in a new issue