mirror of
https://github.com/Significant-Gravitas/AutoGPT.git
synced 2026-04-30 03:00:41 -04:00
fix(backend): add invoice_id idempotency key for subscription payment failure handler
Pass the Stripe invoice ID as transaction_key to _add_transaction in handle_subscription_payment_failure. This prevents double-charging user credits when Stripe retries the invoice.payment_failed webhook after a transient failure (e.g. if stripe.Invoice.pay raises a network error). _add_transaction silently skips insertion when the key already exists, so subsequent retries deduct nothing while still attempting to mark the invoice as paid on the Stripe side. Adds test to verify the idempotency key is set correctly.
This commit is contained in:
@@ -1716,6 +1716,9 @@ async def handle_subscription_payment_failure(invoice: dict) -> None:
|
||||
amount=-amount_due,
|
||||
transaction_type=CreditTransactionType.SUBSCRIPTION,
|
||||
fail_insufficient_credits=True,
|
||||
# Use invoice_id as the idempotency key so that Stripe webhook retries
|
||||
# (e.g. on a transient stripe.Invoice.pay failure) do not double-charge.
|
||||
transaction_key=invoice_id or None,
|
||||
metadata=SafeJson(
|
||||
{
|
||||
"stripe_customer_id": customer_id,
|
||||
|
||||
@@ -988,3 +988,31 @@ async def test_handle_subscription_payment_failure_invoice_pay_error_does_not_ra
|
||||
):
|
||||
# Must not raise — the pay failure is only logged as a warning
|
||||
await handle_subscription_payment_failure(invoice)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_handle_subscription_payment_failure_passes_invoice_id_as_transaction_key():
|
||||
"""invoice_id is used as the idempotency key to prevent double-charging on webhook retries."""
|
||||
mock_user = _make_user(user_id="user-1", tier=SubscriptionTier.PRO)
|
||||
invoice = {
|
||||
"id": "in_idempotency_test",
|
||||
"customer": "cus_123",
|
||||
"subscription": "sub_abc123",
|
||||
"amount_due": 2000,
|
||||
}
|
||||
|
||||
with (
|
||||
patch(
|
||||
"backend.data.credit.User.prisma",
|
||||
return_value=MagicMock(find_first=AsyncMock(return_value=mock_user)),
|
||||
),
|
||||
patch(
|
||||
"backend.data.credit.UserCredit._add_transaction",
|
||||
new_callable=AsyncMock,
|
||||
) as mock_add_tx,
|
||||
patch("backend.data.credit.stripe.Invoice.pay"),
|
||||
):
|
||||
await handle_subscription_payment_failure(invoice)
|
||||
mock_add_tx.assert_called_once()
|
||||
_, kwargs = mock_add_tx.call_args
|
||||
assert kwargs.get("transaction_key") == "in_idempotency_test"
|
||||
|
||||
Reference in New Issue
Block a user