v0.2.0:51 — Zaprite recurring polish from sandbox testing (:46-:51)
Six routine bumps land together, all driven by end-to-end sandbox testing
of the Zaprite recurring auto-charge path that shipped in :45:
:46 Provider create-invoice failures now surface the underlying cause.
Switched user-facing format from `{e}` to `{e:#}` so the full anyhow
chain reaches the buy page; added `tracing::error!` for symmetric
daemon-log visibility. Without this, failed checkouts showed only
"ZapriteProvider.create_invoice" with no clue what actually went wrong.
:47 Zaprite recurring purchases now create the contact upfront. Sandbox
surfaced that `allowSavePaymentProfile: true` requires an explicit
`contactId` on the order — passing only `customerData: { email }`
returns 400. Added `client.create_contact(email, name)` + threaded
the returned id as `contactId`. Graceful degradation: recurring +
no buyer_email → one-shot mode with a warn log; renewals fall back
to manual-pay.
:48 Thank-you page copy is now provider-aware. The wait-page lede
hardcoded "Your Bitcoin payment was received" + Lightning/on-chain
timing — wrong for Zaprite card payments. Reads SETTING_ACTIVE_PROVIDER
and branches the copy + the JS polling-status text accordingly.
:49 Zaprite saved-profile capture: full diagnostic logging + reconciler
path. Discovered five recurring subscriptions settled successfully
but with NULL `zaprite_payment_profile_id`. Root cause: capture
hook had six silent early-return paths, AND the reconciler (which
catches missed webhooks) never called `on_invoice_settled` so subs
created via that path never got their profile captured. Added warn
logs on every early-return + wired capture into `reconcile.rs`'s
post-license-issuance flow.
:50 Webhook event-type extraction probes multiple field names. Confirmed
deliveries were arriving but all logged as "non-actionable event_type=
" — Zaprite doesn't use the convention-suggested `event` field. Now
probes `event` / `eventType` / `type` / `name`, first non-empty wins.
Also widened the order-id probe to include `data.object.id`. On a
miss, warn-logs the raw payload truncated to 2KB so the actual field
name can be added to the probe list.
:51 Zaprite `order.change` event is now actionable. The :50 probe-fix
surfaced that Zaprite's primary delivery shape is a generic
`order.change` event that just says "something about this order
changed" — the receiver has to look at `/data/status` to figure out
what actually changed. They do NOT send the convention-suggested
`order.paid` / `order.complete` events. Added an `order.change`
match arm that branches on status (PAID/COMPLETE/OVERPAID →
InvoiceSettled, EXPIRED → InvoiceExpired, INVALID/CANCELLED →
InvoiceInvalid, in-flight states → Other). End result: webhook-
driven settles now flip subscriptions within seconds of Zaprite's
callback instead of waiting ~45s for the reconciler.
Net effect of the batch: the recurring auto-charge flow is now validated
end-to-end against the Zaprite sandbox. Buyers paying with a card via
Stripe-backed Zaprite trigger contact + saved-profile creation, the
webhook fires `order.change` with status PAID, Keysat captures the
saved-profile id within seconds, and the renewal worker is wired to
auto-charge subsequent cycles. Manual-pay fallback intact for buyers
who decline save-card or pay via Bitcoin/Lightning.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -573,6 +573,41 @@ async fn thank_you(
|
||||
.or(state.config.operator_name.as_deref())
|
||||
.unwrap_or("Keysat");
|
||||
let operator = html_escape(operator_str);
|
||||
|
||||
// Provider-aware confirmation copy. BTCPay is Bitcoin-only (Lightning
|
||||
// + on-chain); Zaprite brokers card payments too (Stripe / etc.) plus
|
||||
// Bitcoin. The lede and the polling-status copy should reflect which
|
||||
// payment rails are actually in play so a buyer who paid by card
|
||||
// doesn't see "your Bitcoin payment was received" while their Stripe
|
||||
// transaction shows up in the operator's dashboard.
|
||||
//
|
||||
// Today this reads `SETTING_ACTIVE_PROVIDER` (the singleton model).
|
||||
// When the multi-provider work lands, swap this for a lookup of the
|
||||
// invoice's own `payment_provider_id` so the copy matches the rail
|
||||
// that actually settled THIS purchase, not whatever's currently
|
||||
// active on the daemon.
|
||||
let provider_kind = crate::payment::read_active_provider_preference(&state.db).await;
|
||||
let (lede_text, provider_kind_str) = match provider_kind {
|
||||
Some(crate::payment::ProviderKind::Zaprite) => (
|
||||
"Your payment was received. We\u{2019}re waiting for it to settle and \
|
||||
for the license to be signed. Card payments confirm in seconds; \
|
||||
Bitcoin Lightning also settles in seconds; on-chain Bitcoin typically \
|
||||
settles in 10\u{2013}20 minutes (one block confirmation).",
|
||||
"zaprite",
|
||||
),
|
||||
// BTCPay or unconfigured → original Bitcoin-only copy. Unconfigured
|
||||
// is rare on this page (operator hit /thank-you without a provider
|
||||
// connected) so we keep it Bitcoin-flavored rather than introducing
|
||||
// a third "unknown" branch.
|
||||
_ => (
|
||||
"Your Bitcoin payment was received. We\u{2019}re waiting for it to settle \
|
||||
and for the license to be signed. Lightning settles in seconds; on-chain \
|
||||
typically settles in 10\u{2013}20 minutes (one block confirmation).",
|
||||
"btcpay",
|
||||
),
|
||||
};
|
||||
let provider_kind_json = serde_json::to_string(provider_kind_str)
|
||||
.unwrap_or_else(|_| "\"btcpay\"".into());
|
||||
let body = format!(
|
||||
r#"<!doctype html>
|
||||
<html lang="en">
|
||||
@@ -748,7 +783,7 @@ footer.kfooter a:hover {{ color:var(--navy-900); }}
|
||||
<div class="wrap">
|
||||
<div class="eyebrow">Payment received</div>
|
||||
<h1 id="page-title">Issuing your license…</h1>
|
||||
<p class="lede" id="page-lede">Your Bitcoin payment was received. We’re waiting for it to settle and for the license to be signed. Lightning settles in seconds; on-chain typically settles in 10–20 minutes (one block confirmation).</p>
|
||||
<p class="lede" id="page-lede">{lede_text}</p>
|
||||
|
||||
<!-- pending state (default): polling for the license -->
|
||||
<div class="pending-card" id="pending-card">
|
||||
@@ -788,6 +823,10 @@ footer.kfooter a:hover {{ color:var(--navy-900); }}
|
||||
<script>
|
||||
(function() {{
|
||||
const INVOICE_ID = {invoice_id_json};
|
||||
// 'zaprite' | 'btcpay' — selects which payment-rail copy the
|
||||
// polling status uses (Zaprite: card + Lightning + on-chain; BTCPay:
|
||||
// Lightning + on-chain only).
|
||||
const PROVIDER_KIND = {provider_kind_json};
|
||||
if (!INVOICE_ID) {{
|
||||
document.getElementById('pending-card').classList.add('hide');
|
||||
document.getElementById('error-card').classList.remove('hide');
|
||||
@@ -857,10 +896,21 @@ footer.kfooter a:hover {{ color:var(--navy-900); }}
|
||||
|
||||
function waitingCopy(status) {{
|
||||
const min = Math.floor(elapsedMs / 60000);
|
||||
const isZaprite = PROVIDER_KIND === 'zaprite';
|
||||
if (status === 'pending' || status === 'processing') {{
|
||||
if (min < 2) return 'invoice ' + status + ' — Lightning settles in seconds; on-chain takes a block (~10 min).';
|
||||
if (min < 10) return 'invoice ' + status + ' — looks like an on-chain payment, waiting for block confirmation. Safe to leave this tab open or bookmark this URL.';
|
||||
return 'invoice ' + status + ' — slow block. Still polling. Bookmark this URL and refresh later if you close the tab.';
|
||||
if (min < 2) {{
|
||||
return isZaprite
|
||||
? 'invoice ' + status + ' — card payments confirm in seconds; Bitcoin Lightning in seconds; on-chain takes a block (~10 min).'
|
||||
: 'invoice ' + status + ' — Lightning settles in seconds; on-chain takes a block (~10 min).';
|
||||
}}
|
||||
if (min < 10) {{
|
||||
return isZaprite
|
||||
? 'invoice ' + status + ' — waiting for confirmation. Card auth or on-chain Bitcoin can take a few minutes. Safe to leave this tab open or bookmark this URL.'
|
||||
: 'invoice ' + status + ' — looks like an on-chain payment, waiting for block confirmation. Safe to leave this tab open or bookmark this URL.';
|
||||
}}
|
||||
return isZaprite
|
||||
? 'invoice ' + status + ' — slow confirmation. Still polling. Bookmark this URL and refresh later if you close the tab.'
|
||||
: 'invoice ' + status + ' — slow block. Still polling. Bookmark this URL and refresh later if you close the tab.';
|
||||
}}
|
||||
return 'invoice status: ' + (status || 'pending');
|
||||
}}
|
||||
|
||||
Reference in New Issue
Block a user