Skip to content

Bug: Payment Duplicate Confirmation Returns Success #1344

@ArnavBallinCode

Description

@ArnavBallinCode

Payment Duplicate Confirmation Returns Success

Summary

Duplicate payment confirmation requests return HTTP 200/302 (success) instead of HTTP 409 (Conflict) or 422 (Unprocessable Entity). This breaks idempotency and prevents proper error handling in automated systems.

Location

File: /app/eventyay/base/models/orders.py
Method: OrderPayment.confirm()
Lines: 1668-1677

Current Behavior

def confirm(self, ...):
    with transaction.atomic():
        locked_instance = OrderPayment.objects.select_for_update().get(pk=self.pk)
        if locked_instance.state == self.PAYMENT_STATE_CONFIRMED:
            logger.info('Confirmed payment {} but ignored due to likely race condition.'.format(self.full_id))
            return  # Returns None - appears as success to caller

When a payment is already confirmed:

  • Method returns None silently
  • HTTP response: 200 OK or 302 Found (success)
  • Log message: "race condition" written but no exception raised
  • Client cannot distinguish success from duplicate

Expected Behavior

def confirm(self, ...):
    with transaction.atomic():
        locked_instance = OrderPayment.objects.select_for_update().get(pk=self.pk)
        if locked_instance.state == self.PAYMENT_STATE_CONFIRMED:
            raise PaymentAlreadyConfirmedException(
                f'Payment {self.full_id} has already been confirmed.'
            )

Should return:

  • HTTP 409 Conflict or 422 Unprocessable Entity
  • Error response body with clear message
  • Exception that can be caught and handled

Reproduction Steps

  1. Navigate to order with pending payment: http://localhost:8000/control/event/aer/xgzvvq/orders/90DEK/
  2. Click "Mark as paid" - payment confirmed successfully
  3. Open browser console and run:
fetch('/control/event/aer/xgzvvq/orders/90DEK/payments/3/confirm', {
    method: 'POST',
    headers: {'X-CSRFToken': document.querySelector('[name=csrfmiddlewaretoken]').value},
    credentials: 'include'
}).then(r => console.log('Status:', r.status))
  1. Observe: Returns 200 OK instead of 409 Conflict

Evidence

Console output:

Status: 200 - Expected: 409, Actual: BUG

Server logs:

INFO 2025-11-24 17:05:20,410 runserver: HTTP POST /control/event/aer/xgzvvq/orders/90DEK/payments/3/confirm 302
INFO 2025-11-24 17:05:49,745 runserver: HTTP POST /control/event/aer/xgzvvq/orders/90DEK/payments/3/confirm 302

Both requests return 302 (success redirect). No error returned to client.

Impact: Critical Use Cases

1. Payment Gateway Webhooks

Scenario: Stripe/PayPal sends duplicate webhook due to network retry or timeout.

@webhook_handler('/stripe/webhook')
def process_stripe_webhook(payload):
    payment_id = payload['payment_intent']
    payment = OrderPayment.objects.get(provider_id=payment_id)
    
    result = payment.confirm()  # Returns None for both first and duplicate
    
    if result is None:  # TRUE for both requests!
        send_ticket_email(payment.order)  # Sent twice
        credit_user_account(payment.order.user, payment.amount)  # Credited twice
        trigger_fulfillment(payment.order)  # Processed twice

Impact: Duplicate tickets sent, double account credits, duplicate order processing.

2. API Client Retry Logic

Scenario: Mobile app or external integration implements retry on network failure.

async function confirmPayment(paymentId) {
    try {
        const response = await api.post(`/payments/${paymentId}/confirm`);
        if (response.ok) {
            return {success: true};
        }
    } catch (error) {
        // Network timeout - retry
        const retry = await api.post(`/payments/${paymentId}/confirm`);
        if (retry.ok) {  // Also returns 200!
            return {success: true};  // Can't detect duplicate
        }
    }
}

Impact: Cannot implement safe retry logic. Cannot distinguish network failure from duplicate.

3. Background Job Idempotency

Scenario: Scheduled task processes pending payments, runs twice due to deployment or system error.

# Cron job: Process bank transfers
def process_bank_transfers():
    for payment in get_received_bank_transfers():
        result = payment.confirm()
        # result is None for both first run and duplicate run
        metrics.increment('payments.confirmed')  # Counted twice
        send_notification(payment.order.user)  # Sent twice

Impact: Incorrect metrics, duplicate notifications, impossible to detect job ran twice.

4. Concurrent Admin Actions

Scenario: Two administrators or admin + automated system confirm same payment simultaneously.

Time 10:00:00.500 - Admin A clicks "Confirm payment"
Time 10:00:00.502 - Admin B clicks "Confirm payment"
Both receive 200 OK response
Neither knows the other just confirmed it
Audit logs show both succeeded

Impact: Confusion in audit logs, no clear indication of duplicate action, compliance issues.

5. Load Balancer Request Duplication

Scenario: Load balancer retries request on backend timeout.

Client -> Load Balancer -> Backend Server A (processing, slow response)
Load Balancer timeout (30s) -> Retry to Backend Server B
Both servers process the request
Both return 200 OK

Impact: Silent duplicate processing at infrastructure level, impossible to detect or prevent.

Why This Matters

Broken Idempotency Contract

REST APIs should be idempotent: same request multiple times = same result with clear indication.

Current broken contract:

  • First request: 200 OK, payment confirmed
  • Second request: 200 OK, payment already confirmed (but looks identical)
  • Client cannot distinguish the two

Correct idempotent contract:

  • First request: 200 OK, payment confirmed
  • Second request: 409 Conflict, already confirmed (clearly different)
  • Client can handle each appropriately

No Error Signaling

Systems depend on HTTP status codes for automated decision-making:

  • 2xx = Success, proceed
  • 4xx = Client error, don't retry
  • 5xx = Server error, retry later

Current behavior breaks this by returning 2xx for a client error (duplicate confirmation).

Monitoring Blindness

Monitoring systems cannot detect issues:

  • No errors logged at ERROR level
  • No metrics for duplicate confirmations
  • No alerts triggered
  • Silent failures in production

Data Integrity Risk

Without proper error handling:

  • Financial reconciliation issues (duplicate credits)
  • Inventory management issues (quota counting errors)
  • Customer support issues (duplicate tickets/emails)
  • Compliance issues (audit log ambiguity)

Image

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    Status

    In review

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions