## Summary
- lock the transaction row before reconciling `budget_transactions` so
concurrent assignment work serializes per transaction
- retry the assignment transaction on deadlocks using Laravel's built-in
transaction attempts
- add regression coverage that asserts the deadlock retry path remains
configured for `PHP-LARAVEL-D`
## Testing
- php artisan test --compact
tests/Feature/BudgetTransactionServiceTest.php
tests/Feature/Listeners/AssignTransactionToBudgetTest.php
## Summary
- replace wipe-and-reinsert budget assignment with idempotent
reconciliation in `BudgetTransactionService`
- keep the fix scoped to the service layer instead of relying on queued
listener uniqueness or event metadata
- add regression coverage for duplicate reruns, stale assignment
cleanup, historical reruns, and label-only listener updates
## Testing
- php artisan test --compact
tests/Feature/BudgetTransactionServiceTest.php
tests/Feature/Listeners/AssignTransactionToBudgetTest.php
tests/Feature/TransactionTest.php
tests/Feature/BulkUpdateTransactionsTest.php
## Why
### Problem
When a refund (positive transaction amount) is assigned to a budget, it
incorrectly **increases** the cumulative spending instead of
**reducing** it. This causes:
- The spending chart line to go **up** on refunds instead of down
- The "Spent" amount to be inflated
- The "Remaining" amount to be understated
- Period rollover calculations to carry over incorrect amounts
### Root Cause
`BudgetTransactionService` uses `abs($transaction->amount)` when
creating budget transactions, which forces all amounts to be positive —
including refunds. Since expenses are stored as negative in the
`transactions` table and refunds as positive, `abs()` treats both as
spending.
## What
### Changes
- Replace `abs($transaction->amount)` with `-$transaction->amount` in
both `assignTransaction()` and `assignHistoricalTransactionsToPeriod()`
— expenses (`-5000`) become positive spending (`5000`), refunds
(`+1000`) become negative spending (`-1000`)
- Remove redundant `abs()` in `BudgetPeriodService::closePeriod()`
rollover calculation
- Add data migration to fix existing `budget_transactions` rows where
the original transaction was a refund
- No frontend changes needed — the chart and budget card already sum
`t.amount` directly
## Verification
### Tests
- Updated `assignHistoricalTransactionsToPeriod stores negated
transaction amount for expenses` — verifies expense sign is preserved
- Added `assignHistoricalTransactionsToPeriod stores refund as negative
amount` — verifies refunds reduce spending
- Added `budget spending correctly reflects mix of expenses and refunds`
— verifies net spending (expense - refund)
- Added `assignTransaction stores refund as negative budget transaction
amount` — verifies real-time assignment handles refunds