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:
- Be called in
Task.updatePaymentStatus() before save()
- Reject invalid transitions with a specific error message (e.g.,
"Cannot transition from paid to open")
- 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:
- Payment marked
paid by Stripe webhook
- Developer reports issue, and
updatePaymentStatus('open') is called
- Status changes without error (current behavior)
- Database now has conflicting records: payment_status =
open but transaction already executed
- 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
Problem
The payment status update flow in
models/Task.jsallows invalid state transitions without validation. For example, a payment marked aspaidcan be reset toopenorpending, and afailedpayment can jump directly topaidwithout 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:
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.jsthat enforces these transitions:The validator should:
Task.updatePaymentStatus()beforesave()"Cannot transition from paid to open")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:
paidby Stripe webhookupdatePaymentStatus('open')is calledopenbut transaction already executedSuccess criteria
PaymentStatusErrorwith the invalid transition as contextpending → paidorpending → failedContributed by Klement Gunndu