Skip to content

Payment status transitions lack atomic validation in models/Task.js #1427

@KlementMultiverse

Description

@KlementMultiverse

Problem

The payment status update flow in models/Task.js allows invalid state transitions without validation. For example, a payment marked as paid can be reset to open or pending, and a failed payment can jump directly to paid without passing through intermediate states. This creates orphaned database records and breaks refund logic.

Why this matters

Payment status is the source of truth for bounty lifecycle. When invalid transitions corrupt this state, the system cannot:

  • Generate accurate payment reports
  • Prevent double-payments on retry
  • Calculate developer earnings correctly
  • Enforce audit trails for financial records

Related issue #1200 identified this as a refactor priority but didn't specify the technical root cause.

What needs to happen

Add a state machine validator to models/Task.js that enforces these transitions:

open → pending → paid ✓
pending → failed → open ✓
paid → refunded ✓

open → paid (skip pending) ✗
refunded → paid ✗
failed → paid (direct) ✗

The validator should:

  1. Be called in Task.updatePaymentStatus() before save()
  2. Reject invalid transitions with a specific error message (e.g., "Cannot transition from paid to open")
  3. Log the attempted transition (old state, new state, timestamp) for audit

This prevents invalid updates at the application layer rather than database constraints, so the error surfaces immediately in logs and tests.

Example of the bug

In a payment retry scenario:

  1. Payment marked paid by Stripe webhook
  2. Developer reports issue, and updatePaymentStatus('open') is called
  3. Status changes without error (current behavior)
  4. Database now has conflicting records: payment_status = open but transaction already executed
  5. Retry logic cannot determine if payment is actually pending or already sent

Success criteria

  • All state transitions in test suite pass through the validator
  • Invalid transitions throw PaymentStatusError with the invalid transition as context
  • Existing payment webhooks (Stripe, PayPal, etc.) still work because they always follow the valid path: pending → paid or pending → failed
  • New tests cover the 3-5 edge cases (refund after paid, retry after failed, etc.)

Contributed by Klement Gunndu

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions