Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
89.47% covered (warning)
89.47%
34 / 38
50.00% covered (danger)
50.00%
1 / 2
CRAP
0.00% covered (danger)
0.00%
0 / 1
ProductController
89.47% covered (warning)
89.47%
34 / 38
50.00% covered (danger)
50.00%
1 / 2
8.07
0.00% covered (danger)
0.00%
0 / 1
 index
87.88% covered (warning)
87.88%
29 / 33
0.00% covered (danger)
0.00%
0 / 1
7.09
 show
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2
3namespace App\Http\Controllers\Api\V1;
4
5use App\Http\Controllers\Api\ApiController;
6use App\Http\Resources\V1\ProductDetailResource;
7use App\Http\Resources\V1\ProductResource;
8use App\Models\admin\Product;
9use Illuminate\Http\JsonResponse;
10use Illuminate\Http\Request;
11
12/**
13 * GET /api/v1/products, GET /api/v1/products/{id}
14 *
15 * Read-only storefront catalog — same filters as the Blade search page
16 * (WebsiteController::applyShopFilters/searchQuery; kept in sync by hand
17 * rather than shared, since the two controllers render very different
18 * things around the same query). Always excludes raw-material products
19 * (Manufacturing, Phase 9) and soft-deleted rows — Product has no
20 * SoftDeletes trait, so `deleted_at` has to be filtered explicitly.
21 */
22class ProductController extends ApiController
23{
24    public function index(Request $request): JsonResponse
25    {
26        $query = Product::query()
27            ->whereNull('deleted_at')
28            ->where(fn ($q) => $q->whereNull('type')->orWhere('type', '!=', 'raw_material'))
29            ->with('category');
30
31        if ($request->filled('q')) {
32            $term = '%' . trim((string) $request->input('q')) . '%';
33            $query->where(function ($q) use ($term) {
34                $q->where('name', 'like', $term)
35                    ->orWhere('sku_no', 'like', $term)
36                    ->orWhere('product_code', 'like', $term)
37                    ->orWhere('keywords', 'like', $term);
38            });
39        }
40
41        $query
42            ->when($request->filled('category'), fn ($q) => $q->where('category_id', $request->input('category')))
43            ->when($request->filled('brand'), fn ($q) => $q->whereIn('brand_id', array_filter((array) $request->input('brand'))))
44            ->when($request->filled('price_min'), fn ($q) => $q->where('selling_price', '>=', (float) $request->input('price_min')))
45            ->when($request->filled('price_max'), fn ($q) => $q->where('selling_price', '<=', (float) $request->input('price_max')))
46            ->when($request->boolean('in_stock'), fn ($q) => $q->where('stock_status', '!=', 'out_of_stock'));
47
48        match ($request->input('sort')) {
49            'price_low' => $query->orderBy('selling_price'),
50            'price_high' => $query->orderByDesc('selling_price'),
51            'name' => $query->orderBy('name'),
52            'oldest' => $query->orderBy('id'),
53            default => $query->orderByDesc('id'),
54        };
55
56        $perPage = min(50, max(1, (int) $request->input('per_page', 20)));
57        $products = $query->paginate($perPage)->withQueryString();
58
59        // ApiController::respond() only auto-derives pagination meta from a
60        // raw paginator — once it's wrapped in a ResourceCollection (needed
61        // to apply ProductResource) that detection no longer fires, so build
62        // the same shape explicitly here.
63        return $this->respond(ProductResource::collection($products), [
64            'current_page' => $products->currentPage(),
65            'per_page' => $products->perPage(),
66            'total' => $products->total(),
67            'last_page' => $products->lastPage(),
68        ]);
69    }
70
71    public function show(int $id): JsonResponse
72    {
73        $product = Product::query()
74            ->whereNull('deleted_at')
75            ->with(['category', 'images', 'specs', 'variations.attributeValues.attribute'])
76            ->findOrFail($id);
77
78        return $this->respond(new ProductDetailResource($product));
79    }
80}