# Server-Side Filtering Implementation Plan

> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.

**Goal:** Add server-side filtering to owner promo-codes, admin geo/config lists, owner revenue, admin revenue, and owner calendar endpoints, with all filter logic centralized in `FilterService`.

**Architecture:** Extend `FilterService` with one public method per domain (promo codes, geo entities, owner revenue, admin revenue, owner calendar). Each controller builds a base query scoped to the authenticated role/owner, passes it through the matching `FilterService` method, and returns a paginated resource or summary response.

**Tech Stack:** PHP 8.x, Laravel 11, Eloquent, Spatie roles/permissions, Pest/PHPUnit.

---

## File map

| File | Action | Responsibility |
|------|--------|----------------|
| `app/Services/Filter/FilterService.php` | Modify | Add filter methods for all 5 domains |
| `app/Http/Controllers/PromoCodeController.php` | Modify | Filter owner promo-codes list |
| `app/Http/Controllers/CountryController.php` | Modify | Filter countries list |
| `app/Http/Controllers/CityController.php` | Modify | Filter cities list |
| `app/Http/Controllers/RegionController.php` | Modify | Filter regions list |
| `app/Http/Controllers/CurrencyController.php` | Modify | Filter currencies list |
| `app/Http/Controllers/Api/Owner/RevenueController.php` | Create | Owner revenue index/summary/chart |
| `app/Http/Controllers/Api/Owner/CalendarController.php` | Create | Owner calendar with status/region filters |
| `app/Http/Controllers/Admin/RevenueController.php` | Create | Global admin revenue + summary |
| `app/Http/Requests/Owner/IndexPromoCodeRequest.php` | Create | Validation for promo-code filters |
| `app/Http/Requests/Owner/IndexRevenueRequest.php` | Create | Validation for owner revenue filters |
| `app/Http/Requests/Owner/IndexCalendarRequest.php` | Create | Validation for calendar filters |
| `app/Http/Requests/Admin/IndexRevenueRequest.php` | Create | Validation for admin revenue filters |
| `app/Http/Requests/ListCountriesRequest.php` | Modify | Add `currency_id` filter rule |
| `app/Http/Requests/ListCitiesRequest.php` | Modify | Add `is_active` filter rule |
| `app/Http/Requests/ListRegionsRequest.php` | Modify | Add `is_active` filter rule |
| `app/Http/Requests/ListCurrenciesRequest.php` | Create | Validation for currency filters |
| `routes/api.php` | Modify | Register new revenue/calendar routes |

---

## Task 1: Create FormRequests

**Files:**
- Create: `app/Http/Requests/Owner/IndexPromoCodeRequest.php`
- Create: `app/Http/Requests/Owner/IndexRevenueRequest.php`
- Create: `app/Http/Requests/Owner/IndexCalendarRequest.php`
- Create: `app/Http/Requests/Admin/IndexRevenueRequest.php`
- Create: `app/Http/Requests/ListCurrenciesRequest.php`
- Modify: `app/Http/Requests/ListCountriesRequest.php`
- Modify: `app/Http/Requests/ListCitiesRequest.php`
- Modify: `app/Http/Requests/ListRegionsRequest.php`

- [ ] **Step 1: Create `Owner/IndexPromoCodeRequest.php`**

```php
<?php

namespace App\Http\Requests\Owner;

use Illuminate\Foundation\Http\FormRequest;

class IndexPromoCodeRequest extends FormRequest
{
    public function authorize(): bool
    {
        return true;
    }

    public function rules(): array
    {
        return [
            'status' => ['nullable', 'in:active,expired,upcoming'],
            'discount_type' => ['nullable', 'in:percentage,fixed'],
            'starts_from' => ['nullable', 'date'],
            'starts_until' => ['nullable', 'date', 'after_or_equal:starts_from'],
            'expires_from' => ['nullable', 'date'],
            'expires_until' => ['nullable', 'date', 'after_or_equal:expires_from'],
            'search' => ['nullable', 'string', 'max:255'],
            'per_page' => ['nullable', 'integer', 'min:1', 'max:100'],
        ];
    }
}
```

- [ ] **Step 2: Create `Owner/IndexRevenueRequest.php`**

```php
<?php

namespace App\Http\Requests\Owner;

use Illuminate\Foundation\Http\FormRequest;

class IndexRevenueRequest extends FormRequest
{
    public function authorize(): bool
    {
        return true;
    }

    public function rules(): array
    {
        return [
            'period' => ['nullable', 'in:today,week,month,year,last_30_days,last_90_days,last_12_months'],
            'from' => ['nullable', 'date', 'required_with:to'],
            'to' => ['nullable', 'date', 'after_or_equal:from', 'required_with:from'],
            'building_id' => ['nullable', 'integer', 'exists:buildings,id'],
            'transaction_type' => ['nullable', 'in:payment,payout'],
            'type' => ['nullable', 'in:payment,refund'],
            'payment_method' => ['nullable', 'in:cash,card,wallet'],
            'collected_status' => ['nullable', 'in:collected,outstanding'],
            'per_page' => ['nullable', 'integer', 'min:1', 'max:100'],
        ];
    }
}
```

- [ ] **Step 3: Create `Owner/IndexCalendarRequest.php`**

```php
<?php

namespace App\Http\Requests\Owner;

use Illuminate\Foundation\Http\FormRequest;

class IndexCalendarRequest extends FormRequest
{
    public function authorize(): bool
    {
        return true;
    }

    public function rules(): array
    {
        return [
            'month' => ['nullable', 'integer', 'min:1', 'max:12'],
            'year' => ['nullable', 'integer', 'min:2000', 'max:2100'],
            'building_id' => ['nullable', 'integer', 'exists:buildings,id'],
            'unit_id' => ['nullable', 'integer', 'exists:units,id'],
            'status' => ['nullable', 'in:available,blocked,booked'],
            'region_id' => ['nullable', 'integer', 'exists:regions,id'],
        ];
    }
}
```

- [ ] **Step 4: Create `Admin/IndexRevenueRequest.php`**

```php
<?php

namespace App\Http\Requests\Admin;

use Illuminate\Foundation\Http\FormRequest;

class IndexRevenueRequest extends FormRequest
{
    public function authorize(): bool
    {
        return true;
    }

    public function rules(): array
    {
        return [
            'period' => ['nullable', 'in:today,week,month,year,last_30_days,last_90_days,last_12_months'],
            'from' => ['nullable', 'date', 'required_with:to'],
            'to' => ['nullable', 'date', 'after_or_equal:from', 'required_with:from'],
            'building_id' => ['nullable', 'integer', 'exists:buildings,id'],
            'transaction_type' => ['nullable', 'in:payment,payout'],
            'type' => ['nullable', 'in:payment,refund'],
            'payment_method' => ['nullable', 'in:cash,card,wallet'],
            'search' => ['nullable', 'string', 'max:255'],
            'per_page' => ['nullable', 'integer', 'min:1', 'max:100'],
        ];
    }
}
```

- [ ] **Step 5: Create `ListCurrenciesRequest.php`**

```php
<?php

namespace App\Http\Requests;

use Illuminate\Foundation\Http\FormRequest;

class ListCurrenciesRequest extends FormRequest
{
    public function authorize(): bool
    {
        return true;
    }

    public function rules(): array
    {
        return [
            'search' => ['nullable', 'string', 'max:255'],
            'per_page' => ['nullable', 'integer', 'min:1', 'max:100'],
        ];
    }
}
```

- [ ] **Step 6: Extend geo FormRequests**

Add `currency_id` to `ListCountriesRequest.php`:
```php
'currency_id' => ['nullable', 'integer', 'exists:currencies,id'],
```

Add `is_active` to `ListCitiesRequest.php`:
```php
'is_active' => ['nullable', 'boolean'],
```

Add `is_active` to `ListRegionsRequest.php`:
```php
'is_active' => ['nullable', 'boolean'],
```

- [ ] **Step 7: Verify syntax**

Run: `~/.config/herd/bin/php85/php.exe -l` on each created/modified request file.

---

## Task 2: Extend `FilterService` with promo-code filters

**Files:**
- Modify: `app/Services/Filter/FilterService.php`

- [ ] **Step 1: Add `applyToPromoCodeQuery` method**

Append to `FilterService`:

```php
public function applyToPromoCodeQuery(Builder $query, array $filters): Builder
{
    if ($status = $filters['status'] ?? null) {
        $now = now();
        match ($status) {
            'active' => $query->where(function (Builder $q) use ($now) {
                $q->whereNull('starts_at')->orWhere('starts_at', '<=', $now);
            })->where(function (Builder $q) use ($now) {
                $q->whereNull('expires_at')->orWhere('expires_at', '>=', $now);
            })->where(function (Builder $q) {
                $q->whereNull('usage_limit')->orWhereColumn('uses_count', '<', 'usage_limit');
            }),
            'expired' => $query->where(function (Builder $q) use ($now) {
                $q->whereNotNull('expires_at')->where('expires_at', '<', $now);
            })->orWhere(function (Builder $q) {
                $q->whereNotNull('usage_limit')->whereColumn('uses_count', '>=', 'usage_limit');
            }),
            'upcoming' => $query->whereNotNull('starts_at')->where('starts_at', '>', $now),
            default => null,
        };
    }

    if ($discountType = $filters['discount_type'] ?? null) {
        $query->where('discount_type', $discountType);
    }

    if ($from = $filters['starts_from'] ?? null) {
        $query->where(function (Builder $q) use ($from) {
            $q->whereNull('starts_at')->orWhere('starts_at', '>=', $from);
        });
    }

    if ($until = $filters['starts_until'] ?? null) {
        $query->where(function (Builder $q) use ($until) {
            $q->whereNull('starts_at')->orWhere('starts_at', '<=', $until);
        });
    }

    if ($from = $filters['expires_from'] ?? null) {
        $query->where(function (Builder $q) use ($from) {
            $q->whereNull('expires_at')->orWhere('expires_at', '>=', $from);
        });
    }

    if ($until = $filters['expires_until'] ?? null) {
        $query->where(function (Builder $q) use ($until) {
            $q->whereNull('expires_at')->orWhere('expires_at', '<=', $until);
        });
    }

    if ($search = $filters['search'] ?? null) {
        $query->where('code', 'like', "%{$search}%");
    }

    return $query;
}
```

- [ ] **Step 2: Verify syntax**

Run: `~/.config/herd/bin/php85/php.exe -l app/Services/Filter/FilterService.php`

---

## Task 3: Extend `FilterService` with geo/config filters

**Files:**
- Modify: `app/Services/Filter/FilterService.php`

- [ ] **Step 1: Add geo/config filter methods**

Append to `FilterService`:

```php
public function applyToCountryQuery(Builder $query, array $filters): Builder
{
    if ($currencyId = $filters['currency_id'] ?? null) {
        $query->where('currency_id', $currencyId);
    }

    if ($search = $filters['search'] ?? null) {
        $query->where('name', 'like', "%{$search}%");
    }

    return $query;
}

public function applyToCityQuery(Builder $query, array $filters): Builder
{
    if ($countryId = $filters['country_id'] ?? null) {
        $query->where('country_id', $countryId);
    }

    if (array_key_exists('is_active', $filters) && $filters['is_active'] !== null) {
        $query->where('is_active', $filters['is_active']);
    }

    if ($search = $filters['search'] ?? null) {
        $query->where('name', 'like', "%{$search}%");
    }

    return $query;
}

public function applyToRegionQuery(Builder $query, array $filters): Builder
{
    if ($cityId = $filters['city_id'] ?? null) {
        $query->where('city_id', $cityId);
    }

    if (array_key_exists('is_active', $filters) && $filters['is_active'] !== null) {
        $query->where('is_active', $filters['is_active']);
    }

    if ($search = $filters['search'] ?? null) {
        $query->where('name', 'like', "%{$search}%");
    }

    return $query;
}

public function applyToCurrencyQuery(Builder $query, array $filters): Builder
{
    if ($search = $filters['search'] ?? null) {
        $query->where(function (Builder $q) use ($search) {
            $q->where('name', 'like', "%{$search}%")
              ->orWhere('code', 'like', "%{$search}%");
        });
    }

    return $query;
}
```

- [ ] **Step 2: Verify syntax**

Run: `~/.config/herd/bin/php85/php.exe -l app/Services/Filter/FilterService.php`

---

## Task 4: Extend `FilterService` with revenue filters

**Files:**
- Modify: `app/Services/Filter/FilterService.php`

- [ ] **Step 1: Add `applyToOwnerRevenueQuery` and `applyToAdminRevenueQuery`**

Append to `FilterService`:

```php
public function applyToOwnerRevenueQuery(Builder $query, array $filters): Builder
{
    return $this->applyRevenueFilters($query, $filters);
}

public function applyToAdminRevenueQuery(Builder $query, array $filters): Builder
{
    return $this->applyRevenueFilters($query, $filters);
}

private function applyRevenueFilters(Builder $query, array $filters): Builder
{
    $this->applyDateRangeFilters($query, $filters, 'transactions.created_at');

    if ($transactionType = $filters['transaction_type'] ?? null) {
        $query->where('transactions.transaction_type', $transactionType);
    }

    if ($type = $filters['type'] ?? null) {
        $query->where('transactions.type', $type);
    }

    if ($paymentMethod = $filters['payment_method'] ?? null) {
        $query->where('transactions.payment_method', $paymentMethod);
    }

    if ($buildingId = $filters['building_id'] ?? null) {
        $query->whereHas('invoice.reservation.unit.building', fn (Builder $q) =>
            $q->where('id', $buildingId));
    }

    if ($search = $filters['search'] ?? null) {
        $like = "%{$search}%";
        $query->where(function (Builder $q) use ($like) {
            $q->where('transactions.id', 'like', $like)
              ->orWhereHas('invoice', fn (Builder $inv) => $inv->where('document_number', 'like', $like))
              ->orWhereHas('invoice.reservation.customer', fn (Builder $c) =>
                  $c->where('name', 'like', $like));
        });
    }

    return $query;
}

private function applyDateRangeFilters(Builder $query, array $filters, string $column): void
{
    $period = $filters['period'] ?? null;

    if ($period) {
        match ($period) {
            'today' => $query->whereDate($column, today()),
            'week' => $query->whereBetween($column, [now()->startOfWeek(), now()->endOfWeek()]),
            'month' => $query->whereBetween($column, [now()->startOfMonth(), now()->endOfMonth()]),
            'year' => $query->whereBetween($column, [now()->startOfYear(), now()->endOfYear()]),
            'last_30_days' => $query->where($column, '>=', now()->subDays(30)),
            'last_90_days' => $query->where($column, '>=', now()->subDays(90)),
            'last_12_months' => $query->where($column, '>=', now()->subMonths(12)),
            default => null,
        };
    } elseif (($from = $filters['from'] ?? null) && ($to = $filters['to'] ?? null)) {
        $query->whereBetween($column, [$from, $to]);
    }
}
```

- [ ] **Step 2: Verify syntax**

Run: `~/.config/herd/bin/php85/php.exe -l app/Services/Filter/FilterService.php`

---

## Task 5: Extend `FilterService` with calendar filters

**Files:**
- Modify: `app/Services/Filter/FilterService.php`

- [ ] **Step 1: Add `applyToOwnerCalendarQuery`**

Append to `FilterService`:

```php
public function applyToOwnerCalendarQuery(Builder $query, array $filters): Builder
{
    $month = $filters['month'] ?? now()->month;
    $year = $filters['year'] ?? now()->year;

    $query->whereMonth('date', $month)->whereYear('date', $year);

    if ($buildingId = $filters['building_id'] ?? null) {
        $query->whereHas('unit', fn (Builder $q) => $q->where('building_id', $buildingId));
    }

    if ($unitId = $filters['unit_id'] ?? null) {
        $query->where('unit_id', $unitId);
    }

    if ($status = $filters['status'] ?? null) {
        $query->where('status', $status);
    }

    if ($regionId = $filters['region_id'] ?? null) {
        $query->whereHas('unit.building', fn (Builder $q) => $q->where('region_id', $regionId));
    }

    return $query;
}
```

- [ ] **Step 2: Verify syntax**

Run: `~/.config/herd/bin/php85/php.exe -l app/Services/Filter/FilterService.php`

---

## Task 6: Wire promo-code filtering into `PromoCodeController`

**Files:**
- Modify: `app/Http/Controllers/PromoCodeController.php`

- [ ] **Step 1: Update `index` method**

Replace the current `index` method with:

```php
public function index(\App\Http\Requests\Owner\IndexPromoCodeRequest $request, \App\Services\Filter\FilterService $filterService)
{
    $this->authorize('viewAny', PromoCode::class);

    $user = auth()->user();
    $query = PromoCode::query();

    if ($user->isOwner()) {
        $query->where('owner_id', $user->owner?->id);
    } elseif ($user->isEmployee()) {
        $query->where('owner_id', $user->employee?->owner_id);
    }

    $query = $filterService->applyToPromoCodeQuery($query, $request->validated());

    $perPage = $request->validated('per_page', 20);

    return PromoCodeResource::collection($query->latest()->paginate($perPage)->withQueryString());
}
```

- [ ] **Step 2: Verify syntax**

Run: `~/.config/herd/bin/php85/php.exe -l app/Http/Controllers/PromoCodeController.php`

---

## Task 7: Wire geo/config filtering into controllers

**Files:**
- Modify: `app/Http/Controllers/CountryController.php`
- Modify: `app/Http/Controllers/CityController.php`
- Modify: `app/Http/Controllers/RegionController.php`
- Modify: `app/Http/Controllers/CurrencyController.php`

- [ ] **Step 1: Inject `FilterService` and update `CountryController::index`**

```php
public function index(ListCountriesRequest $request, \App\Services\Filter\FilterService $filterService)
{
    $query = $filterService->applyToCountryQuery(Country::query(), $request->validated());

    $countries = $query->with('currency')
        ->paginate($request->validated('per_page', 5))
        ->withQueryString();

    return CountryResource::collection($countries);
}
```

- [ ] **Step 2: Update `CityController::index`**

```php
public function index(ListCitiesRequest $request, \App\Services\Filter\FilterService $filterService)
{
    $query = $filterService->applyToCityQuery(City::query(), $request->validated());

    $cities = $query->with('country')
        ->paginate($request->validated('per_page', 10))
        ->withQueryString();

    return CityResource::collection($cities);
}
```

- [ ] **Step 3: Update `RegionController::index`**

```php
public function index(ListRegionsRequest $request, \App\Services\Filter\FilterService $filterService)
{
    $query = $filterService->applyToRegionQuery(Region::query(), $request->validated());

    $regions = $query->withCount(['units' => fn ($q) => $q->where('units.status', 'available')])
        ->with('city')
        ->paginate($request->validated('per_page', 50))
        ->withQueryString();

    return RegionResource::collection($regions);
}
```

- [ ] **Step 4: Update `CurrencyController::index`**

```php
public function index(\App\Http\Requests\ListCurrenciesRequest $request, \App\Services\Filter\FilterService $filterService)
{
    $query = $filterService->applyToCurrencyQuery(Currency::query(), $request->validated());

    return CurrencyResource::collection(
        $query->paginate($request->validated('per_page', 5))->withQueryString()
    );
}
```

- [ ] **Step 5: Verify syntax**

Run: `~/.config/herd/bin/php85/php.exe -l` on all four files.

---

## Task 8: Create `Api/Owner/RevenueController`

**Files:**
- Create: `app/Http/Controllers/Api/Owner/RevenueController.php`

- [ ] **Step 1: Create controller**

```php
<?php

namespace App\Http\Controllers\Api\Owner;

use App\Http\Controllers\Controller;
use App\Http\Requests\Owner\IndexRevenueRequest;
use App\Http\Resources\TransactionResource;
use App\Models\Transaction;
use App\Services\DashboardMetrics;
use App\Services\Filter\FilterService;
use Illuminate\Http\JsonResponse;
use Illuminate\Support\Facades\DB;

class RevenueController extends Controller
{
    public function index(IndexRevenueRequest $request, FilterService $filterService)
    {
        $ownerId = auth()->user()->owner?->id;

        $query = Transaction::query()
            ->with('invoice.reservation.unit.building')
            ->whereHas('invoice.reservation.unit.building', fn ($q) => $q->where('owner_id', $ownerId));

        $query = $filterService->applyToOwnerRevenueQuery($query, $request->validated());

        $perPage = $request->validated('per_page', 15);

        return TransactionResource::collection($query->latest()->paginate($perPage)->withQueryString());
    }

    public function summary(IndexRevenueRequest $request, FilterService $filterService): JsonResponse
    {
        $ownerId = auth()->user()->owner?->id;

        $query = Transaction::query()
            ->whereHas('invoice.reservation.unit.building', fn ($q) => $q->where('owner_id', $ownerId));

        $query = $filterService->applyToOwnerRevenueQuery($query, $request->validated());

        $collected = (clone $query)->where('transaction_type', 'payment')->sum('amount');
        $payouts = (clone $query)->where('transaction_type', 'payout')->sum('amount');
        $refunds = (clone $query)->where('type', 'refund')->sum('amount');

        $outstanding = DB::table('invoices')
            ->join('reservations', 'invoices.reservation_id', '=', 'reservations.id')
            ->join('units', 'reservations.unit_id', '=', 'units.id')
            ->join('buildings', 'units.building_id', '=', 'buildings.id')
            ->where('buildings.owner_id', $ownerId)
            ->where('invoices.remaining_amount', '>', 0)
            ->sum('invoices.remaining_amount');

        return response()->json([
            'data' => [
                'collected' => (float) $collected,
                'payouts' => (float) $payouts,
                'refunds' => (float) $refunds,
                'outstanding' => (float) $outstanding,
            ],
        ]);
    }

    public function chart(IndexRevenueRequest $request, FilterService $filterService)
    {
        $ownerId = auth()->user()->owner?->id;

        $query = Transaction::query()
            ->whereHas('invoice.reservation.unit.building', fn ($q) => $q->where('owner_id', $ownerId))
            ->where('transaction_type', 'payment');

        $query = $filterService->applyToOwnerRevenueQuery($query, $request->validated());

        $period = $request->validated('period', 'last_12_months');
        [$groupFormat, $start] = $this->chartGrouping($period);

        $rows = (clone $query)
            ->selectRaw("DATE_FORMAT(transactions.created_at, '{$groupFormat}') as label")
            ->selectRaw('SUM(amount) as total')
            ->groupBy('label')
            ->orderBy('label')
            ->get();

        return response()->json(['data' => $rows]);
    }

    private function chartGrouping(string $period): array
    {
        return match ($period) {
            'today', 'week' => ['%Y-%m-%d', now()->subWeek()],
            'month', 'last_30_days', 'last_90_days' => ['%Y-%m-%d', now()->subMonths(3)],
            default => ['%Y-%m', now()->subYear()],
        };
    }
}
```

- [ ] **Step 2: Verify syntax**

Run: `~/.config/herd/bin/php85/php.exe -l app/Http/Controllers/Api/Owner/RevenueController.php`

---

## Task 9: Create `Api/Owner/CalendarController`

**Files:**
- Create: `app/Http/Controllers/Api/Owner/CalendarController.php`

- [ ] **Step 1: Create controller**

```php
<?php

namespace App\Http\Controllers\Api\Owner;

use App\Http\Controllers\Controller;
use App\Http\Requests\Owner\IndexCalendarRequest;
use App\Http\Resources\UnitAvailabilityResource;
use App\Models\UnitAvailability;
use App\Services\Filter\FilterService;

class CalendarController extends Controller
{
    public function index(IndexCalendarRequest $request, FilterService $filterService)
    {
        $ownerId = auth()->user()->owner?->id;

        $query = UnitAvailability::query()
            ->select('id', 'unit_id', 'date', 'status', 'reservation_id', 'created_at', 'updated_at')
            ->with(['unit.media', 'reservation.customer'])
            ->whereHas('unit.building', fn ($q) => $q->where('owner_id', $ownerId));

        $query = $filterService->applyToOwnerCalendarQuery($query, $request->validated());

        return UnitAvailabilityResource::collection($query->orderBy('date')->get());
    }
}
```

- [ ] **Step 2: Verify syntax**

Run: `~/.config/herd/bin/php85/php.exe -l app/Http/Controllers/Api/Owner/CalendarController.php`

---

## Task 10: Create `Admin/RevenueController`

**Files:**
- Create: `app/Http/Controllers/Admin/RevenueController.php`

- [ ] **Step 1: Create controller**

```php
<?php

namespace App\Http\Controllers\Admin;

use App\Http\Controllers\Controller;
use App\Http\Requests\Admin\IndexRevenueRequest;
use App\Http\Resources\TransactionResource;
use App\Models\Transaction;
use App\Services\Filter\FilterService;
use Illuminate\Http\JsonResponse;
use Illuminate\Support\Facades\DB;

class RevenueController extends Controller
{
    public function index(IndexRevenueRequest $request, FilterService $filterService)
    {
        $query = Transaction::query()->with('invoice.reservation.unit.building');

        $query = $filterService->applyToAdminRevenueQuery($query, $request->validated());

        $perPage = $request->validated('per_page', 15);

        $transactions = $query->latest()->paginate($perPage)->withQueryString();

        $summary = [
            'collected' => (float) Transaction::query()
                ->where('transaction_type', 'payment')
                ->when($request->validated('from'), fn ($q, $from) => $q->where('created_at', '>=', $from))
                ->when($request->validated('to'), fn ($q, $to) => $q->where('created_at', '<=', $to))
                ->sum('amount'),
            'refunds' => (float) Transaction::query()
                ->where('type', 'refund')
                ->when($request->validated('from'), fn ($q, $from) => $q->where('created_at', '>=', $from))
                ->when($request->validated('to'), fn ($q, $to) => $q->where('created_at', '<=', $to))
                ->sum('amount'),
            'outstanding' => (float) DB::table('invoices')->where('remaining_amount', '>', 0)->sum('remaining_amount'),
            'pending_approvals' => DB::table('owners')->where('status', 'pending')->count(),
        ];

        return response()->json([
            'data' => $transactions->items(),
            'meta' => [
                'summary' => $summary,
                'current_page' => $transactions->currentPage(),
                'last_page' => $transactions->lastPage(),
                'per_page' => $transactions->perPage(),
                'total' => $transactions->total(),
            ],
        ]);
    }
}
```

- [ ] **Step 2: Verify syntax**

Run: `~/.config/herd/bin/php85/php.exe -l app/Http/Controllers/Admin/RevenueController.php`

---

## Task 11: Register routes

**Files:**
- Modify: `routes/api.php`

- [ ] **Step 1: Add owner revenue and calendar routes**

Inside the `role:owner` group, add:

```php
Route::get('owner/revenue', [\App\Http\Controllers\Api\Owner\RevenueController::class, 'index']);
Route::get('owner/revenue/summary', [\App\Http\Controllers\Api\Owner\RevenueController::class, 'summary']);
Route::get('owner/revenue/chart', [\App\Http\Controllers\Api\Owner\RevenueController::class, 'chart']);
Route::get('owner/calendar', [\App\Http\Controllers\Api\Owner\CalendarController::class, 'index']);
```

- [ ] **Step 2: Add admin revenue route**

Inside the `role:super_admin|admin` group, add:

```php
Route::get('admin/revenue', [\App\Http\Controllers\Api\Admin\RevenueController::class, 'index']);
```

- [ ] **Step 3: Verify syntax**

Run: `~/.config/herd/bin/php85/php.exe -l routes/api.php && ~/.config/herd/bin/php85/php.exe artisan route:list --path=owner/revenue`

---

## Task 12: Run tests and Pint

**Files:**
- All modified/created files

- [ ] **Step 1: Syntax check all modified/created files**

```bash
for f in \
  app/Services/Filter/FilterService.php \
  app/Http/Controllers/PromoCodeController.php \
  app/Http/Controllers/CountryController.php \
  app/Http/Controllers/CityController.php \
  app/Http/Controllers/RegionController.php \
  app/Http/Controllers/CurrencyController.php \
  app/Http/Controllers/Api/Owner/RevenueController.php \
  app/Http/Controllers/Api/Owner/CalendarController.php \
  app/Http/Controllers/Admin/RevenueController.php \
  app/Http/Requests/Owner/IndexPromoCodeRequest.php \
  app/Http/Requests/Owner/IndexRevenueRequest.php \
  app/Http/Requests/Owner/IndexCalendarRequest.php \
  app/Http/Requests/Admin/IndexRevenueRequest.php \
  app/Http/Requests/ListCurrenciesRequest.php \
  app/Http/Requests/ListCountriesRequest.php \
  app/Http/Requests/ListCitiesRequest.php \
  app/Http/Requests/ListRegionsRequest.php \
  routes/api.php; do
  ~/.config/herd/bin/php85/php.exe -l "$f"
done
```

- [ ] **Step 2: Run Pint**

Run: `~/.config/herd/bin/php85/php.exe vendor/bin/pint --test`
Expected: PASS

- [ ] **Step 3: Run tests**

Run: `~/.config/herd/bin/php85/php.exe artisan test`
Expected: All tests pass

- [ ] **Step 4: Fix regressions**

If any tests fail, read the failures and fix the related code. Re-run tests until green.

---

## Self-review checklist

- [ ] Every filter from the design spec maps to a `FilterService` method.
- [ ] Every controller delegates filter application to `FilterService`.
- [ ] Owner/admin scoping is applied before filtering in every controller.
- [ ] No placeholders remain in code snippets.
- [ ] Method signatures are consistent across the plan.

---

## Execution handoff

Plan complete and saved to `docs/superpowers/plans/2026-06-24-server-side-filtering-plan.md`.

**Execution approach:** Subagent-Driven (recommended) — dispatch a fresh coder subagent per task, review between tasks, iterate fast.
