From cc1cf6ea7f9d2e8fa219fcf722bc703dd8c14cc8 Mon Sep 17 00:00:00 2001 From: Konstantinos Arvanitakis Date: Tue, 22 Sep 2026 13:46:26 +0300 Subject: [PATCH] Feat: Displaying Draft Products, only when AppDebug = true --- src/Catalog/Services/ProductService.php | 7 ++-- src/Catalog/Support/ProductFilterBuilder.php | 34 ++++++++++++++++++-- 2 files changed, 36 insertions(+), 5 deletions(-) diff --git a/src/Catalog/Services/ProductService.php b/src/Catalog/Services/ProductService.php index c3ba915..a86c62a 100644 --- a/src/Catalog/Services/ProductService.php +++ b/src/Catalog/Services/ProductService.php @@ -254,7 +254,10 @@ class ProductService public function random(int $limit): array { $raw = Product::search('') - ->options(['attributesToRetrieve' => ['id']]) + ->options([ + 'attributesToRetrieve' => ['id'], + 'filter' => $this->filterBuilder->withVisibility(), + ]) ->raw(); $ids = collect($raw['hits'] ?? [])->pluck('id')->shuffle()->take($limit)->values(); @@ -287,7 +290,7 @@ class ProductService private function findAllWhere(string $filter, int $limit = 1000): array { $paginator = Product::search('') - ->options(['filter' => $filter]) + ->options(['filter' => $this->filterBuilder->withVisibility($filter)]) ->paginateRaw(perPage: $limit, page: 1); return collect($this->localizer->hitsFrom($paginator)) diff --git a/src/Catalog/Support/ProductFilterBuilder.php b/src/Catalog/Support/ProductFilterBuilder.php index dddf401..61fa177 100644 --- a/src/Catalog/Support/ProductFilterBuilder.php +++ b/src/Catalog/Support/ProductFilterBuilder.php @@ -10,6 +10,13 @@ use Modules\Core\Catalog\DTOs\ProductFilters; * out of ProductService (where it originated, scoped to browsing/filtering * without a search term) so ProductSearchService can apply the exact same * filter semantics to a text query too, rather than reimplementing it. + * + * Also the single place that composes the draft-visibility clause (see + * withVisibility()) — every Meilisearch `filter` string ProductService + * constructs, including the handful of ad-hoc ones that don't call build() + * at all (getById()/getBySlug()'s id lookup, random()'s id-only fetch), + * goes through this class so none of them can silently omit it the way a + * status filter was missing everywhere until now. */ class ProductFilterBuilder { @@ -19,10 +26,10 @@ class ProductFilterBuilder * ProductService::priceRange() excludes 'price' so a price slider's own * bounds don't shrink to whatever range is already selected on it. */ - public function build(?ProductFilters $filters, array $exclude = []): ?string + public function build(?ProductFilters $filters, array $exclude = []): string { if ($filters === null) { - return null; + return $this->withVisibility(); } $clauses = Collection::make([ @@ -36,6 +43,27 @@ class ProductFilterBuilder 'inStockOnly' => $filters->inStockOnly ? 'in_stock = true' : null, ])->except($exclude)->filter(); - return $clauses->isEmpty() ? null : $clauses->join(' AND '); + return $this->withVisibility($clauses->isEmpty() ? null : $clauses->join(' AND ')); + } + + /** + * A draft product (status = 'draft', see Lunar\Filament\Resources\ + * ProductResource's own status Select) is only ever visible while + * APP_DEBUG is true — a merchant/developer previewing an unfinished + * product locally or on a staging box, never a real storefront + * visitor. Every ProductService method that builds a Meilisearch + * `filter` string, build() included, calls this rather than passing + * $rawClause straight to Product::search() — the one seam that + * guarantees none of them can omit the visibility rule. + * + * Always returns a non-empty string (never null) — a bare + * 'status = "published"' is itself a complete, valid Meilisearch + * filter on its own when $rawClause is null. + */ + public function withVisibility(?string $rawClause = null): string + { + $visibility = config('app.debug') ? null : 'status = "published"'; + + return Collection::make([$visibility, $rawClause])->filter()->join(' AND '); } }