Replaced mollie
All checks were successful
Deploy / build-deploy (push) Successful in 3m47s

This commit is contained in:
Jorijn van der Graaf 2026-08-20 20:15:47 +02:00
commit df91762271
29 changed files with 3079 additions and 838 deletions

View file

@ -164,7 +164,8 @@ HTTPResponse RenderPage(std::string_view target) {
const ShippingTable ship = CurrentShippingTable();
const Views::RenderedPage page =
Views::RenderProduct(*product, gContent.rates, ship.perCountry,
{}, {}, CryptoPaymentAvailable());
{}, {}, CryptoPaymentAvailable(),
BankPaymentAvailable());
HTTPResponse res;
res.status = std::to_string(page.status);
ApplyPageHeaders(res, "text/html; charset=utf-8",
@ -266,7 +267,7 @@ HTTPResponse RenderPage(std::string_view target) {
// ArrivalPollAllowed. A reload past that renders from the ledger and
// lets the reconciler do its job, which is the whole point of having
// one. The interval is the ORDER'S rail's, so a crypto order is not
// paced by Mollie's cadence or the other way round.
// paced by the bank rail's cadence or the other way round.
if (order->status == "awaiting_payment") {
if (const PaymentRail* rail = gRails.For(order->payChoice);
rail && ArrivalPollAllowed(order->token, rail->PollInterval())) {
@ -314,6 +315,15 @@ HTTPResponse RenderPage(std::string_view target) {
OrderCryptoPay pay;
pay.address = instr->address;
pay.amount = instr->amount;
// Set only by bank-transfer rails, and what the renderer
// switches on. The structured reference is derived from the
// order token rather than carried by the rail, so the two
// forms the page prints cannot disagree with each other.
pay.beneficiary = instr->beneficiary;
pay.bic = instr->bic;
if (!instr->beneficiary.empty()) {
pay.structuredReference = CreditorReferenceFromToken(order->token);
}
const std::int64_t now =
std::chrono::duration_cast<std::chrono::seconds>(
std::chrono::system_clock::now().time_since_epoch()).count();
@ -650,7 +660,7 @@ HTTPResponse HandleCheckout(const HTTPRequest& req, const Route& route) {
ApplyPageHeaders(res, "text/html; charset=utf-8", false, true);
const Views::RenderedPage page = Views::RenderProduct(
*product, gContent.rates, shipTable.perCountry, errors, prev,
CryptoPaymentAvailable());
CryptoPaymentAvailable(), BankPaymentAvailable());
res.body = Views::RenderDocument(page, Views::RenderNav(RouteKind::Product),
Views::RenderFooter(), {}, gCssHref);
return res;
@ -861,7 +871,7 @@ HTTPResponse HandleCheckout(const HTTPRequest& req, const Route& route) {
// Straight to the payment page — the buyer clicked "buy", not "read an
// interim status page". The order page stays the receipt/status URL that
// Mollie redirects back to afterwards.
// a hosted provider would redirect back to afterwards.
res.status = "303";
res.headers["location"] = order.payUrl;
res.headers["cache-control"] = "no-store";
@ -874,7 +884,7 @@ HTTPResponse HandleCheckout(const HTTPRequest& req, const Route& route) {
// The gate on the order page's arrival poll.
//
// Rendering /order/<token> asks the provider whether the payment landed, so a
// buyer redirected back from Mollie sees "paid" immediately instead of an
// buyer returning to this page sees "paid" immediately instead of an
// alarming "awaiting payment" that flips ten seconds later. That is a good
// thing to do once. The problem was that it happened on EVERY render: an
// outbound HTTPS round trip, on the request thread, holding the rail's mutex,
@ -882,7 +892,7 @@ HTTPResponse HandleCheckout(const HTTPRequest& req, const Route& route) {
//
// The hole that closes: an attacker places one order — their own, so no token
// guessing is involved — and then reloads it in a loop. Every reload spent a
// live Mollie API call against the shop's key, and because CreateLink shares
// live call against the shop's account, and because CreateLink shares
// that same mutex, real buyers' checkouts queued behind the flood. The
// listener is thread-per-connection with no cap, so the blocked threads piled
// up as well.
@ -1056,6 +1066,8 @@ void ConfigurePayments(PaymentRails rails, std::string redirectBase) {
bool CryptoPaymentAvailable() { return gRails.crypto != nullptr; }
bool BankPaymentAvailable() { return gRails.bank != nullptr; }
namespace {
// The reconciler: the ONLY thing that moves an order to paid.