donation item, shop soft open
All checks were successful
Deploy / build-deploy (push) Successful in 4m11s

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Jorijn van der Graaf 2026-08-17 11:04:03 +02:00
commit abbd616b40
23 changed files with 2898 additions and 209 deletions

View file

@ -120,6 +120,9 @@ std::vector<OrderRecord> FoldLocked() {
r.shippingMinor = doc->Int("shipping_minor");
r.totalMinor = doc->Int("total_minor");
r.vatIncluded = doc->Bool("vat_included");
// Additive key: absent (every pre-donations line) reads false,
// so an old ledger's orders all stay sales.
r.donation = doc->Bool("donation");
r.status = std::string(doc->Str("status", "awaiting_payment"));
// Read as written, with no default applied here: the ledger
// should keep saying exactly what it recorded, and resolving an
@ -175,11 +178,14 @@ void SetOrdersPath(const std::filesystem::path& p) {
bool CreateOrder(const OrderRecord& o) {
std::lock_guard lock(gOrdersMutex);
// "donation" is written only when true, matching the status writer's
// omit-rather-than-empty rule: absent-means-false is what lets a ledger
// from before donations existed keep reading correctly.
return AppendLine(std::format(
R"({{"type":"order","at":"{}","id":"{}","ref":"{}","product":"{}",)"
R"("color":"{}","quantity":{},"unit_minor":{},)"
R"("email":"{}","name":"{}","street":"{}","postal":"{}","city":"{}","country":"{}",)"
R"("goods_minor":{},"shipping_minor":{},"total_minor":{},"vat_included":{},)"
R"("goods_minor":{},"shipping_minor":{},"total_minor":{},"vat_included":{},{})"
R"("status":"{}","pay_choice":"{}","pay_url":"{}","pay_id":"{}"}})",
JsonEscape(o.createdAt), JsonEscape(o.token), JsonEscape(o.reference),
JsonEscape(o.product),
@ -187,6 +193,7 @@ bool CreateOrder(const OrderRecord& o) {
JsonEscape(o.buyer.email), JsonEscape(o.buyer.name), JsonEscape(o.buyer.street),
JsonEscape(o.buyer.postal), JsonEscape(o.buyer.city), JsonEscape(o.buyer.country),
o.goodsMinor, o.shippingMinor, o.totalMinor, o.vatIncluded,
o.donation ? R"("donation":true,)" : "",
JsonEscape(o.status), JsonEscape(o.payChoice),
JsonEscape(o.payUrl), JsonEscape(o.payId)));
}
@ -311,6 +318,13 @@ SalesSummary SummarizeSales(std::span<const OrderRecord> orders) {
// being written — the same paid-or-shipped idiom the invoice
// download uses.
if (r.paidAt.empty() && r.status != "paid" && r.status != "shipped") continue;
// A paid donation is income but not a sale: /financials shows it in
// the donations row, and counting it here too would double-book it.
if (r.donation) {
++out.donationCount;
out.donationsMinor += r.totalMinor;
continue;
}
++out.count;
out.totalMinor += r.totalMinor;
}