Skip to content

feat: enhance invoice capture with improved Stripe webhook handling#346

Merged
jfrench9 merged 2 commits into
mainfrom
feature/improve-invoice-capture-stripe
Feb 13, 2026
Merged

feat: enhance invoice capture with improved Stripe webhook handling#346
jfrench9 merged 2 commits into
mainfrom
feature/improve-invoice-capture-stripe

Conversation

@jfrench9
Copy link
Copy Markdown
Member

Summary

This PR significantly improves the billing system's invoice capture and synchronization capabilities by enhancing Stripe webhook handling and streamlining invoice processing workflows.

Key Accomplishments

  • Enhanced Stripe Integration: Expanded webhook handlers to better capture and process invoice-related events from Stripe
  • Improved Data Synchronization: Added new database fields to support better invoice sync tracking and state management
  • Streamlined Invoice Processing: Refactored billing job workflows to be more efficient and reliable
  • Simplified API Layer: Reduced complexity in invoice routing logic while maintaining functionality
  • Enhanced Payment Provider Support: Extended payment provider operations to support improved invoice workflows

Technical Changes

  • Added database migration for new invoice synchronization fields
  • Significantly expanded billing job capabilities with 200+ lines of new processing logic
  • Updated invoice model with additional tracking fields
  • Enhanced webhook routing for better event handling
  • Refactored invoice API endpoints for improved maintainability
  • Updated test suite to reflect new invoice processing behaviors

Breaking Changes

None - all changes are backwards compatible and enhance existing functionality.

Testing Notes

  • All existing invoice API tests have been updated and are passing
  • New webhook handling has been tested with Stripe's event simulation
  • Database migration has been tested in development environment
  • Invoice synchronization flows have been validated end-to-end

Infrastructure Considerations

  • Database migration required for new invoice sync fields
  • Enhanced webhook processing may increase system throughput
  • Improved job processing should reduce billing-related background task duration
  • No changes to external API contracts or authentication requirements

🤖 Generated with Claude Code

Branch Info:

  • Source: feature/improve-invoice-capture-stripe
  • Target: main
  • Type: feature

Co-Authored-By: Claude noreply@anthropic.com

…pdates

- Added handlers for `invoice.updated`, `invoice.voided`, and `charge.refunded` events to process changes in invoice status and details.
- Updated the `BillingInvoice` model to include new fields for `invoice_pdf`, `hosted_invoice_url`, and `currency`.
- Improved the `list_invoices` function to return additional invoice details from the database.
- Refactored invoice creation and update logic to ensure accurate syncing with Stripe data, including handling invoice line items.
- Enhanced tests for invoice listing and processing to cover new functionality and ensure reliability.
@jfrench9
Copy link
Copy Markdown
Member Author

@claude please review this PR

@claude
Copy link
Copy Markdown

claude Bot commented Feb 13, 2026

Claude finished @jfrench9's task in 1m 28s —— View job


PR Review: Enhanced Invoice Capture with Stripe Webhook Handling

Tasks

  • Read and analyze database migration
  • Review billing job expansion
  • Examine invoice model changes
  • Review webhook routing enhancements
  • Analyze invoice API endpoint refactoring
  • Review test suite updates
  • Provide comprehensive feedback

Review Summary

This PR implements a solid enhancement to invoice capture and Stripe webhook handling. The changes are well-structured and follow the codebase patterns. I've identified several important issues that should be addressed before merging.

Critical Issues

1. Missing _handle_setup_intent_succeeded Implementation

Location: robosystems/routers/admin/webhooks.py:76

The webhook handler imports and calls _handle_setup_intent_succeeded on line 77:

elif event_type == "setup_intent.succeeded":
    await _handle_setup_intent_succeeded(event_data, db, ctx)

But this function is defined in robosystems/dagster/jobs/billing.py:725-754 and is not exported by the billing job module. This will cause a runtime error when Stripe sends a setup_intent.succeeded webhook.

Fix: Add _handle_setup_intent_succeeded to the imports in webhooks.py:38-49.

2. Database Migration: Missing server_default Constraint

Location: alembic/versions/f599c98cf98c_add_invoice_sync_fields.py:30

The currency column is added with server_default="usd" which is correct, but for existing rows, Alembic doesn't backfill values. If there are any existing invoices in the database, they will have NULL in the currency column until updated.

Recommendation: Consider whether you need to backfill existing rows. If yes, add a data migration:

# After adding the column
op.execute("UPDATE billing_invoices SET currency = 'usd' WHERE currency IS NULL")

High Priority Issues

3. Inconsistent Invoice Number Handling

Location: robosystems/dagster/jobs/billing.py:298-300

The invoice number logic uses Stripe's invoice number when available, but falls back to a generated ID:

stripe_number = invoice_data.get("number")
invoice_number = stripe_number or f"STRIPE-{stripe_invoice_id[-8:]}"

However, line 479 updates the invoice number when transitioning from draft → open:

if stripe_number and invoice.invoice_number.startswith("STRIPE-"):
    invoice.invoice_number = stripe_number

Potential Issue: This creates two invoice numbers for the same invoice (temporary + final). This could confuse users if they see the temporary number in their first notification.

Recommendation: Consider whether you want to:

  • Generate invoice numbers locally (more control)
  • Always use Stripe's numbers (simpler, but drafts have no number)
  • Document this behavior clearly

4. Missing Subscription ID in list_invoices Response

Location: robosystems/routers/billing/invoices.py:104

The endpoint always sets subscription_id=None:

Invoice(
    # ... other fields
    subscription_id=None,  # Always None
)

But line items have subscription_id (line 329 in billing job). This inconsistency means API consumers can't easily link invoices to subscriptions.

Fix: Add subscription_id to the response:

subscription_id=inv.line_items[0].subscription_id if inv.line_items else None,

5. Race Condition in Webhook Processing

Location: robosystems/routers/admin/webhooks.py:200-206

The idempotency check happens before adding the background task:

if BillingAuditLog.is_webhook_processed(event_id, "stripe", db):
    # ...
    return {"status": "success", "message": "Event already processed"}

# Process directly as a background task
background_tasks.add_task(_process_webhook_event, ...)

Problem: Two concurrent webhook deliveries could both pass the idempotency check (both see "not processed"), then both add background tasks. The second check inside _process_webhook_event (line 98) prevents duplicate processing, but it's inefficient.

Recommendation: Consider marking the webhook as "processing" immediately (with a different status) to prevent duplicate work.

Medium Priority Issues

6. Inconsistent Error Handling in _handle_invoice_created

Location: robosystems/dagster/jobs/billing.py:294-296

When an invoice already exists, the function logs and returns early:

if existing_invoice:
    context.log.info(f"Invoice already synced from Stripe: {stripe_invoice_id}")
    return

But when the subscription is not found (line 284), it also returns silently. This makes debugging harder when invoices are missing.

Recommendation: Consider logging a warning when subscription is not found to distinguish between "already processed" and "can't process yet".

7. Potential Integer Overflow in Currency Conversion

Location: robosystems/dagster/jobs/billing.py:309-310

Tax and total amounts use invoice_data.get("tax", 0) or 0 pattern:

tax_cents=invoice_data.get("tax", 0) or 0,
total_cents=invoice_data.get("total", 0),

Issue: Stripe returns amounts in cents, but these are stored as Python integers. For very large invoices (>$21M), this could theoretically overflow a 32-bit integer in PostgreSQL.

Recommendation: While unlikely in practice, consider using Numeric/Decimal for currency fields instead of Integer if you expect large enterprise customers.

Minor Issues / Style

8. Redundant Database Commits

Location: robosystems/dagster/jobs/billing.py:683

Multiple handlers commit twice in the same function:

subscription.status = new_status
subscription.updated_at = datetime.now(UTC)
db_session.commit()  # First commit

# ... then later ...
db_session.commit()  # Second commit (line 683)

Recommendation: Consolidate commits where possible to reduce transaction overhead.

9. Missing Type Hints in Helper Functions

Location: robosystems/dagster/jobs/billing.py:183-253

The async helper functions lack return type annotations:

async def _handle_checkout_completed(
  session_data: dict, db_session: Any, context: OpExecutionContext
) -> None:  # Good!

But function parameters use Any for db_session:

db_session: Any  # Could be Session

Recommendation: Use from sqlalchemy.orm import Session for better type safety.

10. Unnecessary String Interpolation

Location: robosystems/dagster/jobs/billing.py:233

Line 233 uses a complex metadata update pattern:

subscription.subscription_metadata = {
    **(subscription.subscription_metadata or {}),
    "checkout_session_id": subscription.provider_subscription_id,
}

The comment above (line 232) explains this is necessary for SQLAlchemy to detect JSONB mutations, which is correct. However, ensure this pattern is documented in your codebase guidelines.

Positive Observations

  1. Good idempotency handling: Proper use of BillingAuditLog.is_webhook_processed and mark_webhook_processed
  2. Comprehensive webhook coverage: Handles 11 different Stripe event types including edge cases like refunds and voided invoices
  3. Test coverage: Tests have been updated to reflect the new invoice sync behavior
  4. Security: Proper webhook signature verification with security audit logging
  5. Database design: New fields (invoice_pdf, hosted_invoice_url, currency) are sensible additions
  6. Documentation: Good inline comments explaining complex logic (e.g., portal cancellation vs deletion)

Recommendations

Before Merge

  1. Fix critical issue Inital open source release of robosystems #1 - Add missing import for _handle_setup_intent_succeeded
  2. Address issue chore: improve documentation and suppress build output #4 - Fix missing subscription_id in list_invoices response
  3. Consider issue Fix MCP tools query results to include units and improve dimensions #2 - Decide on backfill strategy for existing invoices

Post-Merge Follow-up

  1. Add integration tests for the full webhook → database → API flow
  2. Monitor for duplicate webhook processing in production logs
  3. Document the invoice number transition behavior for support team

Test Coverage Analysis

The test file updates look good:

  • Tests properly mock database queries and Stripe API calls
  • Edge cases are covered (no customer, no upcoming invoice, multiple line items)
  • Authorization tests verify RBAC (only owners/admins can view invoices)

Missing test coverage:

  • No tests for the new webhook event handlers
  • No tests for invoice number transitions (draft → open)
  • No tests for the new invoice sync fields (invoice_pdf, hosted_invoice_url)

Overall, this is a well-implemented enhancement that significantly improves invoice capture capabilities. The critical issues should be addressed before merging, but the overall architecture and approach are sound.

…pty line items gracefully. Update test to include subscription_id for line items.
@jfrench9 jfrench9 merged commit ca1b4a6 into main Feb 13, 2026
7 checks passed
@jfrench9 jfrench9 deleted the feature/improve-invoice-capture-stripe branch February 14, 2026 04:27
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