tests and eurc
All checks were successful
Deploy / build-deploy (push) Successful in 4m19s

This commit is contained in:
Jorijn van der Graaf 2026-08-15 00:54:05 +02:00
commit 749f525f83
44 changed files with 5380 additions and 3532 deletions

View file

@ -804,11 +804,11 @@ SafeHtml RenderCheckoutForm(const Product& product,
R"(bank transfer. Handled by Mollie.</span></label>)"
R"(<label class="pay-option">)"
R"(<input type="radio" name="pay"{}{}>)"
R"(<span><strong>Cryptocurrency</strong> &mdash; Bitcoin and Lightning, )"
R"(stablecoins and the other coins CoinGate lists. You pay the euro )"
R"(total at the exchange rate CoinGate locks when you open the )"
R"(invoice; crypto invoices expire quickly, so pay soon after )"
R"(ordering or simply order again.</span></label>)"
R"(<span><strong>Cryptocurrency</strong> &mdash; EURC, a euro )"
R"(stablecoin, paid from your own wallet. The amount to send is the )"
R"(euro total exactly, no exchange rate; the receiving address and )"
R"(the networks it takes appear on the order page, and stay reserved )"
R"(for about a day.</span></label>)"
R"({})"
R"(</fieldset>)",
Attr("value", std::string(Form::kPayBank)),
@ -1242,18 +1242,86 @@ export RenderedPage RenderOrderStatus(const OrderView& o, std::string_view indic
// the payment on arrival. Reaching it means they abandoned the payment,
// so it reads as "resume", not as an alarming limbo.
SafeHtml payBlock;
if (awaiting && !o.payUrl.empty()) {
// Self-hosted crypto first: its payUrl is this very page, so the button
// branch would render a link to where the buyer already stands.
if (awaiting && o.cryptoPay) {
const OrderCryptoPay& pay = *o.cryptoPay;
// One list item per network, in the given order — the first entry is
// the recommendation, which is how "cheapest chain first" reaches the
// buyer as layout rather than as a lecture.
std::vector<SafeHtml> items;
for (const OrderCryptoPay::Chain& c : pay.chains) {
std::string label = c.name;
if (!label.empty() && label[0] >= 'a' && label[0] <= 'z') {
label[0] = static_cast<char>(label[0] - 'a' + 'A');
}
items.push_back(Format(
R"(<li><strong>{}</strong>{}{}<br>)"
R"(<small>EURC contract <code>{}</code></small></li>)",
Escape(label),
c.note.empty() ? SafeHtml{} : Format(" &mdash; {}", Escape(c.note)),
c.link.empty() ? SafeHtml{}
: Format(R"( &middot; <a{}>open in your wallet</a>)",
Url("href", c.link)),
Escape(c.contract)));
}
// The window line tells the truth for both signs of minutesLeft. A
// closed window does NOT mean sent money is gone — the address stays
// ours — so the copy says where it went instead of leaving a buyer
// staring at money that "vanished".
const SafeHtml windowLine = pay.minutesLeft > 0
? Format(
R"(<p class="order__note">This address is reserved for this order )"
R"(for about {} more {}. This page checks automatically and )"
R"(confirms once the full amount has arrived.</p>)",
Num(pay.minutesLeft >= 120 ? pay.minutesLeft / 60 : pay.minutesLeft),
pay.minutesLeft >= 120 ? Raw("hours") : Raw("minutes"))
: Raw(R"(<p class="order__note">The payment window for this order has )"
R"(closed and the order will lapse. If you already sent EURC it )"
R"(is not lost &mdash; it arrived at the address above; contact )"
R"(<a href="mailto:info@catcrafts.net">info@catcrafts.net</a> )"
R"(and it will be settled by hand.</p>)");
SafeHtml indicativeLine = indicative.empty() ? SafeHtml{} : Format(
R"(<p class="order__indicative">{}, indicative only. The charge is )"
R"(the euro amount above.</p>)",
Escape(indicative));
payBlock = Format(
R"(<section class="section">)"
R"(<h2 class="section__title">Pay with EURC</h2>)"
R"(<p>Send <strong>{} EURC</strong> to this address &mdash; one )"
R"(network, one payment:</p>)"
R"(<p><code class="order__address">{}</code></p>)"
R"(<ul class="order__chains">{}</ul>)"
R"({})"
R"(<p class="order__note">EURC is pegged to the euro, so the amount )"
R"(is exactly the euro total &mdash; no exchange rate. Send EURC only, )"
R"(and only on a network listed above. If your exchange deducts a )"
R"(withdrawal fee, send the missing rest to the same address; the )"
R"(order confirms once the full amount sits on one network. Your )"
R"(order reference is <strong>{}</strong>.</p>)"
R"({})"
R"(</section>)",
Escape(pay.amount),
Escape(pay.address),
Join(items),
indicativeLine,
Escape(o.reference),
windowLine);
} else if (awaiting && !o.payUrl.empty()) {
const bool crypto = o.payChoice == Form::kPayCrypto;
SafeHtml indicativeLine = indicative.empty() ? SafeHtml{} : Format(
R"(<p class="order__indicative">{}, indicative only. The charge is )"
R"(the euro amount above; your bank or card sets the actual conversion )"
R"(rate.</p>)",
Escape(indicative));
// What is waiting behind the button differs by rail, and so does what
// "left uncompleted" costs the buyer: a Mollie payment can be resumed
// for a good while, a crypto invoice expires in hours or minutes. A
// page that promised the crypto buyer their link would keep would be
// lying to exactly the person most likely to come back to it late.
// What is waiting behind the button differs by rail. The real crypto
// rail never reaches this branch (its instructions render above); a
// crypto choice here means a hosted stand-in — the fake rail in tests
// — so its copy stays generic rather than naming coins.
payBlock = Format(
R"(<section class="section">)"
R"(<h2 class="section__title">Complete your payment</h2>)"
@ -1265,13 +1333,10 @@ export RenderedPage RenderOrderStatus(const OrderView& o, std::string_view indic
R"(</section>)",
indicativeLine,
Url("href", o.payUrl), Escape(Money::FormatEuro(o.totalMinor)),
crypto ? Raw("The invoice takes Bitcoin, Lightning, stablecoins and the "
"other coins CoinGate lists;")
crypto ? Raw("The payment page completes your crypto payment;")
: Raw("The payment page offers iDEAL, cards and a bank transfer;"),
Escape(o.reference),
crypto ? Raw("Crypto invoices expire quickly &mdash; if this one has, "
"the order simply lapses and you can order again.")
: Raw("A payment left uncompleted simply lapses the order."));
Raw("A payment left uncompleted simply lapses the order."));
} else if (o.status == "paid") {
payBlock = Format(
R"(<section class="section"><h2 class="section__title">What happens now</h2>)"