4cde540b6027c43584ac881745304e72d409d67f
3 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
4cde540b60 |
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>
|
||
|
|
fea6995192 |
v0.2.0:45 — Zaprite recurring auto-charge + mobile-friendly admin UI
Two routine bumps land together in this release: :44 — Admin UI mobile pass. Adds a phone breakpoint (≤640px) and hamburger-driven off-canvas drawer (≤720px) to the embedded web/index.html so triage flows (status check, license lookup, revoke) work from a phone. Tables now scroll horizontally inside their card, tap targets bump to ~40px, stats grid collapses to 1-up, toolbar inputs go full-width. Desktop layout unchanged. CSS + small JS toggle. :45 — Zaprite recurring auto-charge wired end-to-end. Closes the gap the subscriptions.rs module comment promised but never delivered: first-cycle invoices on recurring policies set allow_save_payment_profile, the on-settle hook captures the resulting Zaprite paymentProfileId into four new nullable columns on the subscriptions table (migration 0019, additive only), and the renewal worker calls POST /v1/orders/charge against the saved profile instead of waiting for manual pay. On charge failure (declined card, expired profile, network) the worker logs + audits + falls through to the existing subscription.renewal_pending event so the buyer still has a recovery path. Two new operator webhook events: subscription.auto_charge_initiated and subscription.auto_charge_failed. BTCPay subs and Zaprite subs whose buyer paid with Bitcoin/Lightning or declined the save-card prompt are untouched. NOT yet end-to-end tested against the Zaprite sandbox — control flow follows api.zaprite.com/llms.txt but exact failure-body shapes for declined cards aren't documented; sandbox validation pass recommended before relying in production. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
9eba309a8f |
v0.2.0:2 — Zaprite payment provider + recurring subscriptions schema foundation
This release adds Zaprite as an alternative to BTCPay. Operators can now choose between two payment rails: - BTCPay: Bitcoin-only, you run the BTCPay Server yourself - Zaprite: Bitcoin + fiat cards (USD/EUR via Stripe/Square), brokered by Zaprite, settles to your connected wallets Only one is active at a time per Keysat instance. Switching requires Disconnect → Connect; existing license keys are unaffected. Future v0.3 work routes per-policy choice (e.g., "free tier via Zaprite, paid tier via BTCPay") if operators want both, but for v0.2.0:2 it's either-or. What's in this release: **Migration 0011 — recurring subscriptions schema (dormant).** Adds `subscriptions` and `subscription_invoices` tables, plus `is_recurring`/`renewal_period_days`/`grace_period_days` (default 7)/ `trial_days` (default 0) on policies. No daemon code uses these yet — phases 2-6 of RECURRING_SUBSCRIPTIONS_DESIGN.md land in follow-up commits. Migration regression test covers the additive contract against populated data. **Migration 0012 — zaprite_config.** Singleton-row table for the operator's Zaprite API key + base URL + recorded webhook id. Mirrors btcpay_config from migration 0002. **ZapriteProvider implementation.** New module at src/payment/zaprite/ with client.rs (HTTP, Bearer auth), config.rs (DB persistence), provider.rs (PaymentProvider trait impl). Maps Zaprite's currency enum (BTC/USD/EUR) to/from the Money type; maps Zaprite's order status enum (PENDING/PROCESSING/PAID/COMPLETE/ OVERPAID/UNDERPAID) to ProviderInvoiceStatus. **Webhook security via externalUniqId round-trip.** Zaprite does NOT publish a webhook signature scheme (verified May 2026 against public OpenAPI + dashboard). Their docs explicitly designate receiver-side idempotency as the security model. Keysat's defense: attach our local invoice UUID as externalUniqId at order creation, then trust the webhook only insofar as the order id resolves to a local invoice in an expected state. Documented in detail in the payment::zaprite module-level comment + the validate_webhook docstring. **Admin endpoints.** - POST /v1/admin/zaprite/connect: validates the API key by pinging GET /v1/orders before persisting; swaps active provider atomically - POST /v1/admin/zaprite/disconnect: clears stored creds + provider - GET /v1/admin/zaprite/status: read-only connection snapshot - POST /v1/zaprite/webhook: webhook landing route (alias of the existing /v1/btcpay/webhook handler since validate_webhook is trait-level) **StartOS Actions** under a new "Zaprite" group: Connect Zaprite, Check Zaprite connection, Disconnect Zaprite. Operator pastes the API key into a masked input; daemon validates + saves. **Tests.** Two new in tests/api.rs (zaprite_webhook_event_parsing covers the full event-type mapping + missing-id rejection + malformed-JSON rejection; zaprite_provider_kind pins the identification). Migration regression test for 0011. Test count grows 39 → 41. Operators on BTCPay see no change. Operators wanting Zaprite go through the StartOS Actions tab → Connect Zaprite, paste their API key, register a webhook in Zaprite's dashboard pointing at their public Keysat URL + /v1/zaprite/webhook. Recurring subscriptions are NOT yet operator-visible — schema only in this release. Daemon-code that uses the subscriptions tables (renewal worker, validate-hot-path subscription branch, admin UI) lands in subsequent commits per the design doc's phased plan. |