Skip to content

Unified saved payment methods (NWC + Revolut) with automatic renewal#160

Merged
v0l merged 3 commits into
masterfrom
feat/revolut-auto-renewal
Jul 14, 2026
Merged

Unified saved payment methods (NWC + Revolut) with automatic renewal#160
v0l merged 3 commits into
masterfrom
feat/revolut-auto-renewal

Conversation

@v0l

@v0l v0l commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

Fixes #159

Summary

Reworks automatic renewal around a single, user-selectable saved payment methods model, and adds Revolut off-session (merchant-initiated) card renewals.

NWC and Revolut are now both modelled as payment methods in one provider-agnostic table, so a user can keep several methods (e.g. a Lightning wallet and a card) and choose which is the default. Auto-renewal charges the default method, dispatched by provider: NWC pays a Lightning invoice; Revolut charges the saved card off-session.

Changes

  • DB: new user_payment_method table (provider-agnostic; is_default/enabled; card_brand/card_last_four/exp_month/exp_year; nullable external_customer_id). Migration backfills existing users.nwc_connection_string into the table (provider nwc) and drops the column. Admin has_nwc is now a computed subquery.
  • Capture (payments/revolut.rs): on webhook completion, fetches the merchant-capable saved method + card details from the Revolut customer endpoint, dedupes, first method becomes default.
  • Auto-renew (subscription/mod.rs): auto_renew(sub_id) picks the user's default enabled, non-expired method and dispatches to auto_renew_via_nwc / auto_renew_via_revolut. Worker's separate branches collapsed into one.
  • New API:
    • GET /api/v1/payment-methods — list (masked: no tokens / NWC strings)
    • POST /api/v1/payment-methods — add an NWC connection (validated for pay_invoice)
    • PATCH /api/v1/payment-methods/{id} — set default / enable-disable
    • DELETE /api/v1/payment-methods/{id} — remove
  • Breaking: nwc_connection_string (and the interim revolut_payment_method_saved) removed from the account object; NWC is managed via the payment-methods API now.
  • payments-rs bumped to f4456be (nested customer.id, get_customer_payment_methods, saved card details, off-session charge).

Compliance

Only opaque provider token references are stored (encrypted at rest) plus PCI-safe card metadata (brand/last4/expiry). No PAN/CVV. GDPR token scrub on account deletion is a tracked follow-up (#159).

Testing

  • Offline unit tests: default-method selection (default flag, disabled/expired filtering), provider dispatch, off-session charge via a mock FiatPaymentService, NWC/card CRUD, is_expired.
  • End-to-end (#[ignore]) against the Revolut sandbox: widget save → get_customer_payment_methods → off-session merchant-initiated charge → completed, captured on the saved card.
  • Full workspace builds (default + revolut) and unit tests pass; clippy clean.

Frontend

Companion: LNVPS/web#16 adds savePaymentMethodFor: 'merchant' to the card widget. Follow-up needed in web: move the NWC settings UI from the account form to the new payment-methods API (the account nwc_connection_string field is gone).

Adds Revolut merchant-initiated (off-session) automatic renewals alongside the
existing Lightning/NWC path. When a customer completes a Revolut checkout while
auto-renewal is enabled, the card is saved (savePaymentMethodFor=merchant) and
its opaque token references + non-sensitive card metadata (brand/last4/expiry)
are captured on webhook completion into a new provider-agnostic
user_payment_method table. The worker then charges the default saved method
off-session when a subscription is expiring.

- DB: user_payment_method table (provider-agnostic, default/enabled/expiry) +
  model, mysql + mock impls, migration
- Capture: RevolutPaymentHandler fetches the merchant-capable saved method +
  card details from the customer endpoint, dedupes, first method becomes default
- Charge: SubscriptionHandler::auto_renew_via_revolut selects the default
  enabled, non-expired method and charges via charge_subscription
- Worker: Revolut fallback branch after NWC in handle_subscription_state
  (behind the revolut feature)
- API: read-only revolut_payment_method_saved on GET /api/v1/account
- Bumps payments-rs to include the Revolut saved-method fixes
- examples/revolut_sandbox.rs + revolut_widget.html: sandbox test harness
- ignored integration test validating the off-session charge end-to-end

Only opaque provider token references are stored (encrypted at rest); no card
PAN/CVV ever touches the DB.

Fixes #159
…selection

Reworks saved payment methods into a single user-selectable model:

- NWC (Nostr Wallet Connect) is now a payment method too. Existing
  users.nwc_connection_string values are migrated into user_payment_method
  (provider='nwc') and the column is dropped.
- external_customer_id is nullable (NWC has none).
- Auto-renewal now charges the user's DEFAULT enabled method, dispatched by
  provider (NWC Lightning wallet or Revolut card off-session). The worker's
  separate NWC/Revolut branches are replaced by a single auto_renew() call.
- New user-facing payment-methods API:
  - GET    /api/v1/payment-methods         list (masked; no tokens/NWC strings)
  - POST   /api/v1/payment-methods         add an NWC connection (validated)
  - PATCH  /api/v1/payment-methods/{id}    set default / enable-disable
  - DELETE /api/v1/payment-methods/{id}    remove
- Removes nwc_connection_string and revolut_payment_method_saved from the
  account object; NWC is managed via the payment-methods API. Admin has_nwc is
  computed via a subquery.

Adds offline unit tests (default selection, provider dispatch, mock
FiatPaymentService charge, NWC/card CRUD) plus UserPaymentMethod::is_expired.
@v0l v0l changed the title Automatic renewal via saved Revolut card (off-session) Unified saved payment methods (NWC + Revolut) with automatic renewal Jul 14, 2026
Adds a nullable name column to user_payment_method so users can label multiple
methods (e.g. 'Personal Visa', 'Backup wallet'). Exposed on GET
/api/v1/payment-methods, settable via POST (name?) and PATCH (name).

Refs #85
@v0l

v0l commented Jul 14, 2026

Copy link
Copy Markdown
Contributor Author

Also added an optional user-defined name on saved payment methods so users can distinguish multiple methods (e.g. "Personal Visa", "Backup wallet"). Refs #85 (payment method config).

@v0l
v0l merged commit c070f5e into master Jul 14, 2026
8 checks passed
@v0l
v0l deleted the feat/revolut-auto-renewal branch July 14, 2026 19:04
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Revolut automatic renewal (saved payment method + off-session charge)

1 participant