Skip to content

Implement recurring (wallet) payments to Stripe provider#467

Draft
PetrDlouhy wants to merge 22 commits intojazzband:mainfrom
PetrDlouhy:pr/stripe-recurring-clean
Draft

Implement recurring (wallet) payments to Stripe provider#467
PetrDlouhy wants to merge 22 commits intojazzband:mainfrom
PetrDlouhy:pr/stripe-recurring-clean

Conversation

@PetrDlouhy
Copy link
Contributor

This PR implements recurring payments for Stripe provider. It serves as (second after django-plans-payu) reference implementation of #217 which this is based on. The PR #217 is supposed to be reviewed based on this code and merged first.

For now this is still in working state, although the basic workflow works.

@PetrDlouhy PetrDlouhy force-pushed the pr/stripe-recurring-clean branch 4 times, most recently from b0d8ba0 to 5b76568 Compare December 8, 2025 08:47
PetrDlouhy added a commit to PetrDlouhy/django-payments that referenced this pull request Dec 8, 2025
Add comprehensive documentation for wallet interface:
- docs/wallet.rst: 500-line guide with architecture diagrams,
  implementation guide, provider examples, best practices
- docs/index.rst: Add wallet.rst to documentation index
- docs/usage.rst: Add cross-reference to wallet documentation
- docs/backends.rst: Remove premature Stripe recurring example
- CHANGELOG.rst: Add v4.1.0 entry with complete feature description

Documentation covers:
- Overview and use cases
- Architecture and flow diagrams
- Step-by-step implementation guide
- Multiple storage patterns (wallet FK, custom models)
- Provider implementation guide
- Best practices and troubleshooting
- Reference to Stripe PR jazzband#467 for in-development support

Examples use PayU (currently available) and reference DummyProvider.
@PetrDlouhy PetrDlouhy force-pushed the pr/stripe-recurring-clean branch from 5b76568 to 7e99207 Compare December 8, 2025 10:46
Add core interface for server-initiated payments with stored payment methods.

Components:
- BaseWallet model with lifecycle (PENDING → ACTIVE → ERASED)
- BasePayment methods: get_renew_token, set_renew_token, get_renew_data,
  autocomplete_with_wallet
- BasicProvider methods: autocomplete_with_wallet, _finalize_wallet_payment,
  erase_wallet
- WalletStatus constants (PENDING, ACTIVE, ERASED)

This enables variable-amount recurring payments where the application
controls when to charge and how much, unlike subscription-based systems
where the provider manages both schedule and amount.
Implement autocomplete_with_wallet in DummyProvider as reference
implementation showing:
- Token retrieval and validation
- Simulated server-initiated charge
- Status updates and captured_amount
- Wallet activation via _finalize_wallet_payment

Serves as template for other provider implementations.
Add 14 mock-based tests verifying wallet interface following the
established pattern from test_core.py.

Tests cover:
- WalletStatus constants and lifecycle
- BaseWallet payment_completed, activate, erase
- BasePayment token management (get/set_renew_token, get_renew_data)
- DummyProvider autocomplete_with_wallet implementation
- Provider helper methods (_finalize_wallet_payment)
- Error handling (missing token)
Add comprehensive documentation (569 lines) covering:
- Overview emphasizing variable amount capability
- Architecture with flow diagrams
- Amount flexibility section with examples
- Step-by-step implementation guide
- Multiple storage patterns
- Provider implementation guide
- Security best practices
- Troubleshooting
- Complete API reference

Integrated into docs/index.rst and cross-referenced from docs/usage.rst.
Add working example showing:
- Wallet model extending BaseWallet
- Payment model with wallet FK
- Token management implementation (get/set_renew_token)
- Migration for wallet support

Demonstrates the simple FK-based pattern for wallet integration.
Document v4.1.0 additions:
- Wallet-based recurring payments interface
- Components and use cases
- Provider support status
- Backward compatibility
- Add testmain to INSTALLED_APPS in test_settings.py
- Update PYTHONPATH to testapp/testapp for testmain module discovery
- Configure pytest testpaths to include testapp/testapp for integration tests
- Fix PYTHONPATH order in tox.ini (root first to avoid symlink issues)
- Add --ignore-glob for testapp/payments symlink

This infrastructure supports both mock-based unit tests (in payments/)
and integration tests with real DB (in testapp/testapp/testmain/).
@PetrDlouhy PetrDlouhy force-pushed the pr/stripe-recurring-clean branch 8 times, most recently from 23d3bed to 96decbc Compare December 12, 2025 16:52
@PetrDlouhy PetrDlouhy force-pushed the pr/stripe-recurring-clean branch 3 times, most recently from f65b442 to b727ff8 Compare December 15, 2025 15:47
@PetrDlouhy PetrDlouhy force-pushed the pr/stripe-recurring-clean branch from db73a47 to ad39f23 Compare December 17, 2025 13:55
Split wallet tests into mock-based unit tests and integration tests:

Mock tests (payments/test_wallet.py - 17 tests):
- Interface contracts and simple logic without database
- Test wallet status choices, default values, token retrieval logic
- Test provider delegation and error handling with mocks
- Document usage patterns (FK pattern, custom storage)

Integration tests (testapp/testapp/testmain/test_wallet.py - 12 tests):
- Real database operations and model lifecycle
- Wallet state transitions (activate, erase, payment_completed)
- DummyProvider full workflow with real Payment/Wallet instances
- End-to-end scenarios (first payment, recurring, variable amounts)

DummyProvider enhancements (payments/dummy/__init__.py):
- Add WalletStatus import
- Add wallet status check before charging (security best practice)
- Fix captured_amount and transaction_id persistence

Total: 29 tests, all passing independently.
Add support for provider-managed recurring subscriptions where the payment
provider controls the billing schedule and automatically charges on fixed
intervals. This complements the existing wallet interface (app-controlled,
variable amounts) with subscription support (provider-controlled, fixed
schedule).

Core components:
- BaseSubscription model with lifecycle (PENDING → ACTIVE → CANCELLED/EXPIRED)
- BasePayment methods: get_subscription(), cancel_subscription(),
  autocomplete_with_subscription()
- BasicProvider methods: autocomplete_with_subscription(),
  cancel_subscription(), _finalize_subscription_payment()
- SubscriptionStatus constants (PENDING, ACTIVE, CANCELLED, EXPIRED)

Provider implementation:
- DummyProvider subscription support as reference implementation
- Subscription creation, payment processing, and cancellation flows
- Proper error handling for missing subscriptions

Tests (48 tests total):
- 29 mock-based unit tests (payments/test_subscription.py)
- 19 integration tests with real models (testapp/testmain/test_subscription.py)
- Tests document interface patterns and usage examples
- Complete coverage of subscription methods (lines 490, 507-512, 535-536)
- All tests use pytest best practices (pytest.raises, proper assertions)

Test coverage improvements:
- payments/models.py: 50-58% → 64% (+6-14%)
- All subscription interface methods fully covered
- 77 total tests passing (48 subscription + 29 wallet)

Example implementation:
- Subscription model in testapp with plan and payment_provider fields
- Payment model with subscription FK and get_subscription() implementation
- Migration for subscription support (0004_subscription_payment_subscription)

This enables fixed-schedule recurring payments (monthly/yearly subscriptions)
managed by the provider, complementing the existing wallet-based variable
amount recurring payment support.
Implement wallet interface for Stripe recurring payments:
- autocomplete_with_wallet() for server-initiated charges
- Store PaymentMethod and Customer ID during first payment
- Use PaymentIntent API with off_session flag for recurring charges
- Support 3D Secure authentication when required

Changes:
- Add recurring_payments and store_payment_method parameters to init
- Implement autocomplete_with_wallet() using stored payment method
- Add customer_creation='always' to ensure customer is created
- Extract and store PaymentMethod + Customer ID from Checkout Session
- Handle payment_intent status responses (succeeded, requires_action, failed)

Tests:
- Webhook token storage test (test_webhook_stores_token.py)
- Recurring payment flow tests (test_recurring.py):
  * Success case with stored payment method
  * Missing customer_id error handling
  * 3D Secure redirect handling
  * Card declined handling
  * Stripe API error handling
  * Missing token error handling

Documentation:
- Comprehensive webhook setup guide (docs/webhooks.rst)
- Provider configuration examples (docs/backends.rst)
- Note on use_token parameter (optional, defaults to True)
The three failing tests were attempting to store Mock objects in
payment.attrs.payment_intent, which gets serialized to JSON. Mock
objects aren't JSON-serializable, causing TypeError.

Solution: Created MockStripeIntent class that extends dict (JSON-
serializable) while providing attribute access like real Stripe objects.

- Adds MockStripeIntent helper class for reusable test mocks
- Fixes test_autocomplete_with_wallet_success
- Fixes test_autocomplete_with_wallet_requires_3ds
- Fixes test_autocomplete_with_wallet_card_declined

All 141 tests now pass (137 passed, 4 skipped).
PetrDlouhy and others added 7 commits December 18, 2025 10:21
Enhances Stripe integration with better payment metadata:

**Payment Description:**
- Added payment.description to line items (visible in Stripe Dashboard)
- Added payment.description to PaymentIntent for recurring payments
- Improves payment tracking and customer support

**Billing Address Metadata:**
- Store all billing fields in session metadata for audit trail
- Includes: name, address, city, postcode, country, state, phone
- Available for accounting and support purposes
- Relies on Stripe's default billing_address_collection='auto'

**Tests:**
- Line items with/without description
- Metadata storage (full, partial, none)
- Recurring payments with description
Implements explicit configuration pattern matching PayU's approach, where
zero-dollar variants are configured separately rather than auto-detected.

Pattern:
- PayU: Separate variant with shop_name='AUTH_0' parameter
- Stripe: Separate variant with use_setup_mode=True parameter

Changes:
- Added use_setup_mode parameter to StripeProviderV3.__init__()
- When use_setup_mode=True:
  - Uses mode='setup' (SetupIntent) instead of mode='payment' (PaymentIntent)
  - Collects payment method WITHOUT charging
  - Auto-enables store_payment_method
- Webhook processing already handles both modes via _is_session_complete()
- _store_payment_method_from_session() already handles both PaymentIntent and SetupIntent

Benefits over auto-detection:
- Explicit: Variant name tells you what it does
- Testable: Each variant tested independently
- Consistent: Same pattern as PayU provider
- Predictable: No magic behavior based on payment amount

Usage in BlenderKit settings:
Stripe Checkout requires currency parameter even in setup mode (zero-dollar auth).
Without it, returns error: 'Missing required param: currency'

Setup mode doesn't charge anything, but Stripe still needs to know the currency
for the SetupIntent that will be used for future charges.
Enables static webhook endpoints to handle setup_intent.succeeded events,
which are emitted when using Stripe Checkout in setup mode (zero-dollar
authorization for card changes).

Changes:
- Store payment_token in Checkout Session metadata (always, for all modes)
- Handle setup_intent events in get_token_from_request()
- Look up Checkout Session by customer_id and time window
- Extract payment_token from session metadata

Why this approach:
- Stripe doesn't include client_reference_id in setup_intent events
- SetupIntent metadata may not be reliably copied by Stripe
- Session metadata is reliable and always available
- Narrow time window (5 min) keeps API calls efficient

This allows BlenderKit-style card changes where users update their
payment method without charging.
Added integration tests for setup_intent.succeeded webhooks:
- Tests token extraction from session metadata
- Tests full webhook flow for card changes
- Verifies payment status updates correctly

These tests cover the simplified setup_intent implementation.
@PetrDlouhy PetrDlouhy force-pushed the pr/stripe-recurring-clean branch from 47379d8 to d7229f2 Compare December 18, 2025 09:22
pre-commit-ci bot and others added 4 commits December 18, 2025 09:22
Retrieve Stripe transaction fees during webhook processing and store
in payment.attrs.stripe_fee for applications to extract.

Implementation:
- Add _retrieve_transaction_fee() method
- Fetch: PaymentIntent → Charge → BalanceTransaction → fee
- Store fee in cents (Stripe native format)
- Integrated into process_data() atomic transaction
- Graceful error handling (don't fail webhooks)

Tests:
- Successful fee retrieval
- Missing data handling (no charges, no balance_transaction)
- Error handling (Stripe API errors, exceptions)
- Integration with process_data webhook handler
- Verify fee retrieval errors don't fail webhooks

Applications can extract fee from extra_data in Payment.save().
setup_intent.succeeded events contain SetupIntent objects, not Checkout
Sessions. The process_data method was treating all events as sessions,
causing setup_intent webhooks to fail.

Changes:
- Add separate handling path for setup_intent.succeeded events
- Add _store_payment_method_from_setup_intent() helper method
- Extract event type and handle different event structures appropriately

Fixes 2 failing tests:
- test_process_data_setup_intent_succeeded
- test_webhook_flow_setup_intent_with_session_lookup

Remaining 3 failures are in get_token_from_request() - a different
method with pre-existing issues.
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.

1 participant