# Razorpay Route & Split Payments — Test Cases & Validation Specification

This document details all automated test cases, flows, preconditions, and assertions across the 8 test suites in `tests/Unit/`.
All tests run against MySQL test database `stackfood_test` (port 3306) and local Redis (`task-5033`).

---

## Suite Summary & Breakdown

| Test Suite File | Test Class | Total Tests | Focus Domain |
| :--- | :--- | :---: | :--- |
| `tests/Unit/RestaurantConfirmationModelTest.php` | `RestaurantConfirmationModelTest` | 29 | Restaurant Accept/Reject flows, Unanswered sweepers, Progressive release gates, Multi-status formatting, Cache lock deduplication, COD payment_confirmed |
| `tests/Unit/RefundOrderTest.php` | `RefundOrderTest` | 38 | Full refund (`reverse_all: 1`), Transfer reversals, Cash wallet fallback, 4xx description guards, Two-connection concurrency race proof, Terminal state preservation |
| `tests/Unit/ReleaseTransferTest.php` | `ReleaseTransferTest` | 10 | Progressive release (Restaurant at pickup, Delivery at delivered), Pre-fetch verification, Timeout recovery, Refusal guards, Inactive restaurant non-blocking release |
| `tests/Unit/RazorpayRouteSplitPaymentTest.php` | `RazorpayRouteSplitPaymentTest` | 17 | Section 9(5) GST tax remittance, Integer paise math, Preflight checks, Webhook signatures, Driver assignment fee transfers |
| `tests/Unit/QueueAndWebhookHardeningTest.php` | `QueueAndWebhookHardeningTest` | 9 | Dedicated queue routing, Retry backoff & timeout invariants, Health checks, Sweeper redispatch, Stuck refund alerts |
| `tests/Unit/RazorpayRouteTest.php` | `RazorpayRouteTest` | 5 | RBI IFSC format validation, Phone cleaning, Legal business types, Delivery app blocking rule |
| `tests/Unit/FeeCalculatorTest.php` | `FeeCalculatorTest` | 5 | Handling fee slab evaluation, BCMath half-up rounding, Boundary continuity, Zone overrides |
| `tests/Unit/ExampleTest.php` | `ExampleTest` | 1 | Smoke test |
| **Total** | | **114** | **100% Passing** |

---

## 1. Restaurant Confirmation Model (`RestaurantConfirmationModelTest.php`)

### Test 1: `test_accept_vs_timeout_ordering_a_restaurant_accepts_first`
- **Flow:** Restaurant accepts order before timeout sweep.
- **Preconditions:** Paid online order in `paid_held` with `order_status = 'pending'`.
- **Action:** Vendor calls `update_order_status` with `status = 'confirmed'`.
- **Assertions:**
  - Order moves to `confirmed`.
  - `OrderRestaurantConfirmation` created with `answer = 'yes'`, `admin_review = 0`.
  - Subsequent run of `razorpay:cancel-unanswered-orders` skips the order.

### Test 2: `test_accept_vs_timeout_ordering_b_timeout_triggers_first`
- **Flow:** Unanswered timeout sweep runs before restaurant responds.
- **Preconditions:** Paid online order in `paid_held` created 6 minutes ago (timeout = 5 min).
- **Action:** `CancelUnansweredOrdersCommand::handle()` executes.
- **Assertions:**
  - Order transitions to `canceled` with reason `Restaurant unanswered within timeout`.
  - Payment transitions to `refund_pending` and dispatches `RefundOrderJob`.
  - Late restaurant accept attempt is rejected with 403 / error.

### Test 3: `test_backfilled_in_flight_order_cutoff_exemption`
- **Flow:** Orders paid prior to deploy cutoff timestamp are exempt from restaurant acceptance gates.
- **Preconditions:** `feature_enabled_from` set to `2026-11-01 00:00:00`. Order `paid_at` is `2026-10-31 23:50:00`.
- **Assertions:**
  - `order->restaurant_confirmed` evaluates to `true`.
  - Driver can pick up order without requiring a confirmation row.

### Test 4: `test_scheduled_order_not_canceled_before_lead_time`
- **Flow:** Scheduled orders are not canceled by the sweeper before their activation lead time.
- **Preconditions:** Scheduled order with `schedule_at` in 2 hours, `restaurant_schedule_order_lead_time_minutes` = 30.
- **Assertions:**
  - Sweeper command leaves order untouched in `pending`.

### Test 5: `test_vendor_web_panel_and_admin_accept_and_cancel_paths`
- **Flow:** Tests Vendor Web Panel (`Vendor\OrderController`) and Admin Panel (`Admin\OrderController`) accept & cancel endpoints.
- **Assertions:**
  - Admin accept records confirmation with admin ID.
  - Admin/Vendor cancels invoke `RefundOrderService::cancelWithFullRefund()`.

### Test 6: `test_accept_on_canceled_order_returns_error`
- **Flow:** Restaurant attempts to accept an already canceled order.
- **Assertions:**
  - Rejection with error code and no state change.

### Test 7: `test_payment_state_attribute_display`
- **Flow:** Verifies user-facing `payment_state` translation across all internal states.
- **Assertions:**
  - `cash_on_delivery` $\to$ `'cod'`
  - `unpaid` $\to$ `'pending'`
  - `paid_held` $\to$ `'confirmed'`
  - `partially_released` $\to$ `'confirmed'`
  - `paid` / `released` $\to$ `'released'`
  - `refund_pending` $\to$ `'refunding'`
  - `refund_failed` $\to$ `'refunding'` (masked from internal name)
  - `refunded` / `refunded_to_wallet` $\to$ `'refunded'`

### Test 8: `test_cod_and_unpaid_through_cancel_with_full_refund_sends_nothing`
- **Flow:** Canceling COD or unpaid order through `cancelWithFullRefund`.
- **Assertions:**
  - `refund_needed = false`.
  - Zero calls made to Razorpay API.

### Test 9: `test_dual_stage_sweeper_redispatches_stuck_transfers_and_alerts_on_repeated_failure`
- **Flow:** Sweeper `razorpay:sweep-transfer-releases` finds stuck transfers.
- **Assertions:**
  - Action row in `failed` re-queued up to 3 times, then alerts operations.

### Test 10: `test_after_commit_jobs_actually_dispatch_when_transaction_commits`
- **Flow:** DB transaction rollback cancels job dispatch; commit actually enqueues job.
- **Assertions:**
  - `Queue::assertPushed` only after commit.

### Test 11: `test_restaurant_reject_after_accept_before_pickup_triggers_automated_refund`
- **Flow:** Restaurant accepts, but later rejects before pickup.
- **Assertions:**
  - Order canceled and full automated refund dispatched.

### Test 12: `test_end_to_end_capture_listed_accept_driver_notified`
- **Flow:** Complete capture $\to$ Vendor list $\to$ Vendor accept $\to$ Driver FCM notification.
- **Assertions:**
  - Push notification sent to driver.

### Test 13: `test_scheduler_contains_cancel_unanswered_and_sweep_releases`
- **Flow:** Console schedule inspection.
- **Assertions:**
  - `razorpay:cancel-unanswered-orders` scheduled every minute.
  - `razorpay:sweep-transfer-releases` scheduled every 2 minutes.

### Test 14: `test_api_responses_contain_formatting_attributes_with_no_n_plus_one`
- **Flow:** Batch formatting of collection of 5 orders.
- **Assertions:**
  - `loadMissing('restaurantConfirmation')` executes exactly 1 batch query ($O(1)$).
  - Accessing `payment_confirmed`, `payment_state`, and `restaurant_confirmed` executes 0 additional queries (no N+1).

### Test 15: `test_pickup_gate_for_all_payment_error_codes_and_restaurant_not_confirmed`
- **Flow:** Driver attempts pickup under all payment failure states.
- **Assertions:**
  - 403 `RESTAURANT_NOT_CONFIRMED` if restaurant hasn't confirmed.
  - 403 `PAYMENT_UNPAID` / `PAYMENT_REFUNDED` for illegal states.

### Test 16: `test_delivered_gate_allows_delivered_and_releases_delivery_transfer`
- **Flow:** Delivery completion releases driver fee transfer.
- **Assertions:**
  - Delivery transfer released upon transition to `delivered`.

### Test 17: `test_driver_cancel_blocked_for_paid_online_orders`
- **Flow:** Driver taps cancel on a paid online order.
- **Assertions:**
  - 403 `DRIVER_CANCEL_BLOCKED`.

### Test 18: `test_driver_payment_push_sent_once`
- **Flow:** Push notification deduplication for driver assignment.
- **Assertions:**
  - Notification sent exactly once per order.

### Test 19: `test_delivered_releases_still_held_restaurant_transfer`
- **Flow:** Catch-up release: if restaurant transfer was still held at delivery, `delivered` status releases both.
- **Assertions:**
  - Both transfers released, payment transitions to `paid`.

### Test 20: `test_unanswered_command_cancels_and_refunds_under_real_services`
- **Flow:** Unanswered command running against real database entities.
- **Assertions:**
  - Order canceled, refund job queued.

### Test 21: `test_three_minute_warning_sent_once`
- **Flow:** 3-minute urgency warning sent to restaurant.
- **Assertions:**
  - Warning sent once per order.

### Test 22: `test_accept_then_report_restaurant_closed_gives_full_refund`
- **Flow:** Driver arrives and finds restaurant closed after accept.
- **Assertions:**
  - Order canceled with full refund, transfers reversed.

### Test 23: `test_delivery_release_not_dropped_while_restaurant_release_is_queued`
- **Flow:** Stage-aware queue locks (`Cache::lock`) for `ShouldBeUnique`.
- **Assertions:**
  - Unique ID contains stage (`:restaurant` vs `:delivery`).
  - Real cache lock acquired for restaurant does not drop or block delivery stage lock.
  - Duplicate dispatch of same stage is deduplicated.

### Test 24: `test_customer_cancel_paid_held_order_uses_cancel_with_full_refund`
- **Flow:** Customer cancels pending paid order from customer mobile app.
- **Assertions:**
  - Uses `RefundOrderService::cancelWithFullRefund()`, transitions to `refund_pending`, dispatches `RefundOrderJob`.

### Test 25: `test_backfill_and_cutoff_exemption`
- **Flow:** Validates cutoff timestamp logic and backfill rows.
- **Assertions:**
  - Cutoff timestamp correctly exempts past orders.

### Test 26: `test_restaurant_a_never_sees_restaurant_b_pending_paid_orders`
- **Flow:** Multi-tenant vendor isolation in `get_current_orders`.
- **Assertions:**
  - Restaurant A never sees Restaurant B's pending paid orders.
  - Query SQL confirms nested `exists` vendor scope and grouped parentheses `AND (...)`.

### Test 27: `test_non_razorpay_and_wallet_order_cancel_runs_legacy_refund_before_delivered`
- **Flow:** Order paid via non-Route gateway or wallet canceled by admin.
- **Assertions:**
  - `OrderLogic::refund_before_delivered()` runs.
  - `AdminWallet` debited and customer wallet credited.
  - `PaymentLedger` records `order_canceled_legacy_refund_completed`.

### Test 28: `test_stuck_refund_pending_alert_respects_configurable_age_and_deduplicates_once_per_day`
- **Flow:** Stuck `refund_pending` alerting via `queue:health-check`.
- **Assertions:**
  - Alerts when order older than configurable threshold (default 6h).
  - Deduplicates to at most once per order per day.
  - State machine transition away from `refund_pending` clears the suppression cache key.

### Test 29: `test_multi_status_api_responses_and_cod_payment_confirmed`
- **Flow:** Matrix test across 6 statuses (COD, unpaid, paid_held, refund_pending, refund_failed, refunded) in Vendor and Delivery responses.
- **Assertions:**
  - `payment_confirmed` MUST be `true` for COD!
  - `payment_state` and `restaurant_confirmed` correct for all 6 states.

---

## 2. Refund & Reversal Layer (`RefundOrderTest.php` - 38 Tests)

### Gateway Refunds & Route Transfer Reversals
- **`test_full_refund_uses_reverse_all_and_dispatches_reversal_jobs`**: Verifies `reverse_all: 1` parameter sent to Razorpay `/payments/{id}/refund` and records `OrderRefund`.
- **`test_gateway_refund_timeout_recovers_idempotently_via_fetch_payment_refunds`**: Simulates timeout on POST refund; recovery worker fetches existing refund list and reconciles.
- **`test_out_of_order_refund_events_preserve_terminal_refunded_state`**: Late webhook cannot downgrade `refunded` state.
- **`test_two_connection_race_between_release_and_refund`**: Real MySQL concurrency test on two distinct connection IDs proving mutual exclusion and zero double-payout.

### Customer Cash Wallet Fallback Layer
- **`test_webhook_refund_failed_leads_to_wallet_credit_once`**: Gateway `refund.failed` webhook automatically credits customer cash wallet by default (no description list required).
- **`test_two_refund_failed_events_lead_to_one_wallet_credit`**: Idempotency guard prevents duplicate wallet credits.
- **`test_4xx_rejection_with_no_refund_found_credits_the_wallet`**: 4xx business rejection with configured substring and empty refund list falls back to cash wallet credit.
- **`test_wallet_fallback_default_empty_descriptions_ends_in_refund_failed`**: 4xx rejection with default empty list `[]` halts in `refund_failed` with admin alert.
- **`test_insufficient_merchant_balance_ends_in_refund_failed_with_admin_alert`**: Insufficient balance error halts in `refund_failed` and never falls back to wallet.
- **`test_refunded_to_wallet_is_restricted_to_refund_pending_and_refund_failed`**: State machine blocks transitions to `refunded_to_wallet` from `paid_held` or `paid`.
- **`test_wallet_fallback_with_wallet_status_disabled_ends_in_refund_failed_with_admin_alert`**: Aborts fallback and raises alert if wallet system is disabled.
- **`test_wallet_credit_failure_aborts_fallback_and_sets_refund_failed`**: Transaction failure when crediting wallet aborts cleanly to `refund_failed`.

---

## 3. Progressive Transfer Release Layer (`ReleaseTransferTest.php` - 10 Tests)

- **`test_same_payload_delivered_twice_produces_one_release_call`**: Release idempotency via monotonic action states.
- **`test_pre_fetch_on_hold_false_skips_patch_call`**: If Razorpay reports `on_hold: false`, skips redundant PATCH.
- **`test_timeout_then_retry_leads_to_fetch_before_second_call`**: Timeout recovery pattern for transfers.
- **`test_release_is_blocked_for_rejected_closed_and_refunded_orders`**: Guards refuse release for terminal orders.
- **`test_out_of_order_events_do_not_downgrade_status`**: Webhook event ordering guard.
- **`test_sequential_worker_invocation_is_idempotent_and_skips_razorpay_call`**: Concurrency protection under serial worker runs.
- **`test_webhook_duplicate_event_stuck_in_received_is_redispatched`**: Stuck event recovery.
- **`test_release_order_transfers_job_parameters_and_failure_hook`**: Job retry and failure auditing.
- **`test_progressive_release_stages_restaurant_then_delivery`**: Stage 1 releases restaurant transfer; Stage 2 releases delivery transfer only at `delivered`.
- **`test_closed_or_inactive_restaurant_does_not_block_release`**: Proves that restaurant status = 0 (closed) or active = 0 (inactive) does NOT block fund settlement.

---

## 4. Split Payments & Linked Accounts (`RazorpayRouteSplitPaymentTest.php` & `RazorpayRouteTest.php`)

- **PAISE-level integer rounding**: All fee splits, taxes, and net transfers calculated in integer paise without floating-point inaccuracies.
- **Section 9(5) GST Compliance**: Platform collects and remits GST directly; restaurant split excludes customer-paid tax.
- **IFSC Validation**: Strict regex `^[A-Z]{4}0[A-Z0-9]{6}$` preventing malformed bank transfers.
- **Delivery App Blocking Rule**: Drivers cannot go online without an activated Linked Account.

