refactor: move stripe webhook handler to app/handlers/

- Create app/handlers/ directory for event handlers
- Move stripe_webhook_handler.py to app/handlers/stripe_webhook.py
- Update imports in webhooks.py, tests, and docs
- Handlers are distinct from services (event-driven vs request-driven)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2025-12-25 21:58:48 +01:00
parent 8a0a5c594a
commit 3b67515bc2
5 changed files with 23 additions and 9 deletions

View File

@@ -14,7 +14,7 @@ from sqlalchemy.orm import Session
from app.core.database import get_db from app.core.database import get_db
from app.exceptions import InvalidWebhookSignatureException, WebhookMissingSignatureException from app.exceptions import InvalidWebhookSignatureException, WebhookMissingSignatureException
from app.services.stripe_service import stripe_service from app.services.stripe_service import stripe_service
from app.services.stripe_webhook_handler import stripe_webhook_handler from app.handlers.stripe_webhook import stripe_webhook_handler
router = APIRouter(prefix="/webhooks") router = APIRouter(prefix="/webhooks")
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)

10
app/handlers/__init__.py Normal file
View File

@@ -0,0 +1,10 @@
# app/handlers/__init__.py
"""
Handlers module.
Contains event handlers for webhooks and other external events.
"""
from app.handlers.stripe_webhook import stripe_webhook_handler
__all__ = ["stripe_webhook_handler"]

View File

@@ -1,5 +1,4 @@
# app/services/stripe_webhook_handler.py # app/handlers/stripe_webhook.py
# noqa: NAM-002 - This is a webhook handler, not a service
""" """
Stripe webhook event handler. Stripe webhook event handler.
@@ -84,14 +83,14 @@ class StripeWebhookHandler:
logger.debug(f"No handler for event type {event_type}") logger.debug(f"No handler for event type {event_type}")
existing.status = "processed" existing.status = "processed"
existing.processed_at = datetime.now(timezone.utc) existing.processed_at = datetime.now(timezone.utc)
db.commit() # noqa: SVC-006 - Webhook handler controls its own transaction db.commit()
return {"status": "ignored", "reason": f"no handler for {event_type}"} return {"status": "ignored", "reason": f"no handler for {event_type}"}
try: try:
result = handler(db, event) result = handler(db, event)
existing.status = "processed" existing.status = "processed"
existing.processed_at = datetime.now(timezone.utc) existing.processed_at = datetime.now(timezone.utc)
db.commit() # noqa: SVC-006 - Webhook handler controls its own transaction db.commit()
logger.info(f"Successfully processed event {event_id} ({event_type})") logger.info(f"Successfully processed event {event_id} ({event_type})")
return {"status": "processed", "result": result} return {"status": "processed", "result": result}
@@ -99,7 +98,7 @@ class StripeWebhookHandler:
logger.error(f"Error processing event {event_id}: {e}") logger.error(f"Error processing event {event_id}: {e}")
existing.status = "failed" existing.status = "failed"
existing.error_message = str(e) existing.error_message = str(e)
db.commit() # noqa: SVC-006 - Webhook handler controls its own transaction db.commit()
raise raise
# ========================================================================= # =========================================================================

View File

@@ -34,7 +34,12 @@ All subscription models are defined in `models/database/subscription.py`:
| `BillingService` | `app/services/billing_service.py` | Subscription operations, checkout, portal | | `BillingService` | `app/services/billing_service.py` | Subscription operations, checkout, portal |
| `SubscriptionService` | `app/services/subscription_service.py` | Limit checks, usage tracking | | `SubscriptionService` | `app/services/subscription_service.py` | Limit checks, usage tracking |
| `StripeService` | `app/services/stripe_service.py` | Core Stripe API operations | | `StripeService` | `app/services/stripe_service.py` | Core Stripe API operations |
| `StripeWebhookHandler` | `app/services/stripe_webhook_handler.py` | Webhook event processing |
### Handlers
| Handler | Location | Purpose |
|---------|----------|---------|
| `StripeWebhookHandler` | `app/handlers/stripe_webhook.py` | Webhook event processing |
### API Endpoints ### API Endpoints

View File

@@ -6,7 +6,7 @@ from unittest.mock import MagicMock, patch
import pytest import pytest
from app.services.stripe_webhook_handler import StripeWebhookHandler from app.handlers.stripe_webhook import StripeWebhookHandler
from models.database.subscription import ( from models.database.subscription import (
BillingHistory, BillingHistory,
StripeWebhookEvent, StripeWebhookEvent,
@@ -59,7 +59,7 @@ class TestStripeWebhookHandlerCheckout:
"""Initialize handler instance before each test.""" """Initialize handler instance before each test."""
self.handler = StripeWebhookHandler() self.handler = StripeWebhookHandler()
@patch("app.services.stripe_webhook_handler.stripe.Subscription.retrieve") @patch("app.handlers.stripe_webhook.stripe.Subscription.retrieve")
def test_handle_checkout_completed_success( def test_handle_checkout_completed_success(
self, mock_stripe_retrieve, db, test_vendor, test_subscription, mock_checkout_event self, mock_stripe_retrieve, db, test_vendor, test_subscription, mock_checkout_event
): ):