feat(billing): B4-B5 — tests, lint fixes, CHANGELOG + PROJECT.md
- Fix unused imports in stripe.py (hashlib, hmac, time) - Update test_billing_routes.py: insert into payment_products table, fix mock paths for extracted paddle.py, add Stripe webhook 404 test - Update CHANGELOG.md with Stripe provider feature - Update PROJECT.md Done section Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -12,11 +12,8 @@ Exports the same interface as paddle.py so billing/routes.py can dispatch:
|
||||
Stripe Tax add-on handles EU VAT collection (must be enabled in Stripe Dashboard).
|
||||
"""
|
||||
|
||||
import hashlib
|
||||
import hmac
|
||||
import json
|
||||
import logging
|
||||
import time
|
||||
|
||||
import stripe as stripe_sdk
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"""
|
||||
Route integration tests for Paddle billing endpoints.
|
||||
Checkout uses Paddle.js overlay (returns JSON), manage/cancel use Paddle SDK.
|
||||
Route integration tests for billing endpoints.
|
||||
Tests work with the default provider (Paddle) via dispatch layer.
|
||||
Checkout returns JSON (Paddle overlay or Stripe redirect URL), manage/cancel use provider SDK.
|
||||
"""
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
@@ -39,7 +40,24 @@ class TestSuccessPage:
|
||||
|
||||
|
||||
# ════════════════════════════════════════════════════════════
|
||||
# Checkout (Paddle.js overlay — returns JSON)
|
||||
# Helper: insert a product into both payment_products and paddle_products
|
||||
# ════════════════════════════════════════════════════════════
|
||||
|
||||
async def _insert_test_product(db, key="starter", price_id="pri_starter_123"):
|
||||
"""Insert a test product into payment_products (used by get_price_id) and paddle_products (legacy fallback)."""
|
||||
await db.execute(
|
||||
"INSERT INTO payment_products (provider, key, provider_product_id, provider_price_id, name, price_cents, currency, billing_type) VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
|
||||
("paddle", key, "pro_test", price_id, "Starter", 1900, "EUR", "subscription"),
|
||||
)
|
||||
await db.execute(
|
||||
"INSERT INTO paddle_products (key, paddle_product_id, paddle_price_id, name, price_cents, currency, billing_type) VALUES (?, ?, ?, ?, ?, ?, ?)",
|
||||
(key, "pro_test", price_id, "Starter", 1900, "EUR", "subscription"),
|
||||
)
|
||||
await db.commit()
|
||||
|
||||
|
||||
# ════════════════════════════════════════════════════════════
|
||||
# Checkout (returns JSON — Paddle overlay or Stripe redirect)
|
||||
# ════════════════════════════════════════════════════════════
|
||||
|
||||
class TestCheckoutRoute:
|
||||
@@ -48,12 +66,7 @@ class TestCheckoutRoute:
|
||||
assert response.status_code in (302, 303, 307)
|
||||
|
||||
async def test_returns_checkout_json(self, auth_client, db, test_user):
|
||||
# Insert a paddle_products row so get_paddle_price() finds it
|
||||
await db.execute(
|
||||
"INSERT INTO paddle_products (key, paddle_product_id, paddle_price_id, name, price_cents, currency, billing_type) VALUES (?, ?, ?, ?, ?, ?, ?)",
|
||||
("starter", "pro_test", "pri_starter_123", "Starter", 1900, "EUR", "subscription"),
|
||||
)
|
||||
await db.commit()
|
||||
await _insert_test_product(db)
|
||||
|
||||
response = await auth_client.post(f"/billing/checkout/{CHECKOUT_PLAN}")
|
||||
assert response.status_code == 200
|
||||
@@ -89,7 +102,7 @@ class TestManageRoute:
|
||||
mock_client = MagicMock()
|
||||
mock_client.subscriptions.get.return_value = mock_sub
|
||||
|
||||
with patch("padelnomics.billing.routes._paddle_client", return_value=mock_client):
|
||||
with patch("padelnomics.billing.paddle._paddle_client", return_value=mock_client):
|
||||
response = await auth_client.post("/billing/manage", follow_redirects=False)
|
||||
assert response.status_code in (302, 303, 307)
|
||||
|
||||
@@ -111,12 +124,27 @@ class TestCancelRoute:
|
||||
await create_subscription(test_user["id"], provider_subscription_id="sub_test")
|
||||
|
||||
mock_client = MagicMock()
|
||||
with patch("padelnomics.billing.routes._paddle_client", return_value=mock_client):
|
||||
with patch("padelnomics.billing.paddle._paddle_client", return_value=mock_client):
|
||||
response = await auth_client.post("/billing/cancel", follow_redirects=False)
|
||||
assert response.status_code in (302, 303, 307)
|
||||
mock_client.subscriptions.cancel.assert_called_once()
|
||||
|
||||
|
||||
# ════════════════════════════════════════════════════════════
|
||||
# Stripe webhook returns 404 when not configured
|
||||
# ════════════════════════════════════════════════════════════
|
||||
|
||||
class TestStripeWebhookEndpoint:
|
||||
async def test_returns_404_when_not_configured(self, client, db):
|
||||
"""Stripe webhook returns 404 when STRIPE_WEBHOOK_SECRET is empty."""
|
||||
response = await client.post(
|
||||
"/billing/webhook/stripe",
|
||||
data=b'{}',
|
||||
headers={"Content-Type": "application/json"},
|
||||
)
|
||||
assert response.status_code == 404
|
||||
|
||||
|
||||
# ════════════════════════════════════════════════════════════
|
||||
# subscription_required decorator
|
||||
# ════════════════════════════════════════════════════════════
|
||||
|
||||
Reference in New Issue
Block a user