diff --git a/ShopIt_API_Postman_Collection.json b/ShopIt_API_Postman_Collection.json index 11cd2f6..162634f 100644 --- a/ShopIt_API_Postman_Collection.json +++ b/ShopIt_API_Postman_Collection.json @@ -2,7 +2,7 @@ "info": { "_postman_id": "76fd6fd2-53b9-4d92-8e02-1ddcc6207fa2", "name": "ShopIt API — Complete", - "description": "Colección canónica generada desde las rutas reales de Laravel. Incluye 137 operaciones HTTP, presets de configuración para cada integración, ejemplos de payload, filtros, archivos y tokens separados para Storefront, Admin App y Scanner.\n\nUso rápido:\n1. Ajustá `base_url` y las credenciales.\n2. Ejecutá el Login de la aplicación correspondiente; el token se guarda automáticamente.\n3. Ajustá los IDs y códigos de las variables de colección.\n\nRegeneración: `php postman/generate-shopit-collection.php`.", + "description": "Colección canónica generada desde las rutas reales de Laravel. Incluye 136 operaciones HTTP, presets de configuración para cada integración, ejemplos de payload, filtros, archivos y tokens separados para Storefront, Admin App y Scanner.\n\nUso rápido:\n1. Ajustá `base_url` y las credenciales.\n2. Ejecutá el Login de la aplicación correspondiente; el token se guarda automáticamente.\n3. Ajustá los IDs y códigos de las variables de colección.\n\nRegeneración: `php postman/generate-shopit-collection.php`.", "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json" }, "item": [ @@ -678,57 +678,6 @@ }, "response": [] }, - { - "name": "Create Catalog", - "request": { - "method": "POST", - "header": [ - { - "key": "Accept", - "value": "application/json", - "type": "text" - }, - { - "key": "Content-Type", - "value": "application/json", - "type": "text" - } - ], - "description": "Ruta Laravel: `POST /api/tenants/{tenant:codigo}/catalog-items`\n\nControlador: `App\\Domains\\Catalog\\Controllers\\CatalogController@store`", - "url": { - "raw": "{{base_url}}/api/tenants/{{tenant_code}}/catalog-items?q=demo&page=1", - "host": [ - "{{base_url}}" - ], - "path": [ - "api", - "tenants", - "{{tenant_code}}", - "catalog-items" - ], - "query": [ - { - "key": "q", - "value": "demo" - }, - { - "key": "page", - "value": "1" - } - ] - }, - "body": { - "mode": "raw", - "raw": "{\n \"type\": \"standard\",\n \"category_id\": {{category_id}},\n \"slug\": \"producto-demo\",\n \"nombre\": \"Producto Demo\",\n \"descripcion\": \"Producto creado desde Postman\",\n \"precio\": 10000,\n \"inventory_policy\": \"tracked\",\n \"inventory_subject\": \"product\",\n \"max_units_per_user\": 5,\n \"has_tickets\": false,\n \"real_stock\": 100,\n \"images\": [\n \"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==\"\n ]\n}", - "options": { - "raw": { - "language": "json" - } - } - } - }, - "response": [] - }, { "name": "Get Catalog", "request": { diff --git a/app/Domains/Commerce/Cart/Models/Cart.php b/app/Domains/Commerce/Cart/Models/Cart.php index 1e9790f..282e57e 100644 --- a/app/Domains/Commerce/Cart/Models/Cart.php +++ b/app/Domains/Commerce/Cart/Models/Cart.php @@ -375,7 +375,7 @@ class Cart extends Model ): CatalogItem|Variant { $catalogItemQuery = CatalogItem::query() ->whereKey($catalogItemId) - ->where('tenant_code', $this->tenant_codigo); + ->forTenantCatalog($this->tenant()->firstOrFail()); if ($lockForUpdate) { $catalogItemQuery->lockForUpdate(); diff --git a/app/Domains/Commerce/Catalog/Controllers/CatalogController.php b/app/Domains/Commerce/Catalog/Controllers/CatalogController.php index 211c214..0dce199 100644 --- a/app/Domains/Commerce/Catalog/Controllers/CatalogController.php +++ b/app/Domains/Commerce/Catalog/Controllers/CatalogController.php @@ -11,10 +11,8 @@ use App\Domains\Commerce\Catalog\Requests\CatalogVariantOptionsRequest; use App\Domains\Commerce\Catalog\Requests\CategoryPageRequest; use App\Domains\Commerce\Catalog\Requests\FeaturedGroupPageRequest; use App\Domains\Commerce\Catalog\Requests\SearchCatalogItemsRequest; -use App\Domains\Commerce\Catalog\Requests\StoreCatalogItemRequest; use App\Domains\Commerce\Catalog\Resources\CatalogFeaturedGroupResource; use App\Domains\Commerce\Catalog\Resources\CatalogItemDetailResource; -use App\Domains\Commerce\Catalog\Resources\CatalogItemResource; use App\Domains\Commerce\Catalog\Resources\CatalogSearchItemResource; use App\Domains\Commerce\Catalog\Resources\CatalogVariantOptionsResource; use App\Domains\Commerce\Catalog\Services\CatalogItemAllowanceService; @@ -40,7 +38,7 @@ class CatalogController extends Controller return response()->json($featuredGroups->map( fn (FeaturedGroup $featuredGroup): array => (new CatalogFeaturedGroupResource( $featuredGroup, - $featuredGroupService->itemsResponse($featuredGroup, 1, $this->userId($request)), + $featuredGroupService->itemsResponse($featuredGroup, $tenant, 1, $this->userId($request)), ))->resolve() )); } @@ -102,6 +100,7 @@ class CatalogController extends Controller return response()->json($featuredGroupService->itemsResponse( $featuredGroup, + $tenant, $page, $this->userId($request), )); @@ -114,7 +113,8 @@ class CatalogController extends Controller CatalogService $catalogService, CatalogItemAllowanceService $allowances, ): CatalogItemDetailResource { - abort_unless($catalogItem->tenant_code === $tenant->codigo, 404); + abort_unless($catalogItem->tenant_code === $tenant->codigo + && ($tenant->active_event_id === null || $catalogItem->event_id === $tenant->active_event_id), 404); $variantId = $request->validated('variant_id'); @@ -134,7 +134,8 @@ class CatalogController extends Controller VariantSelectionService $variantSelectionService, CartService $cartService, ): CatalogVariantOptionsResource { - abort_unless($catalogItem->tenant_code === $tenant->codigo, 404); + abort_unless($catalogItem->tenant_code === $tenant->codigo + && ($tenant->active_event_id === null || $catalogItem->event_id === $tenant->active_event_id), 404); $includedVariantId = null; $cartItemId = $request->validated('cart_item_id'); @@ -163,21 +164,6 @@ class CatalogController extends Controller ); } - public function store( - StoreCatalogItemRequest $request, - Tenant $tenant, - CatalogService $catalogService, - ): JsonResponse { - $catalogItem = $catalogService->create([ - ...$request->validated(), - 'tenant_code' => $tenant->codigo, - ]); - - return CatalogItemResource::make($catalogItem) - ->response() - ->setStatusCode(201); - } - private function userId(Request $request): ?int { $userId = $request->user()?->getAuthIdentifier() ?? Auth::guard('sanctum')->id(); diff --git a/app/Domains/Commerce/Catalog/Models/CatalogItem.php b/app/Domains/Commerce/Catalog/Models/CatalogItem.php index 0e38d99..bdf29b8 100644 --- a/app/Domains/Commerce/Catalog/Models/CatalogItem.php +++ b/app/Domains/Commerce/Catalog/Models/CatalogItem.php @@ -52,6 +52,19 @@ class CatalogItem extends Model 'group_order' => 0, ]; + protected static function booted(): void + { + static::creating(function (CatalogItem $item): void { + if ($item->event_id !== null || $item->tenant_code === null) { + return; + } + + $item->event_id = Tenant::query() + ->where('codigo', $item->tenant_code) + ->value('active_event_id'); + }); + } + protected function casts(): array { return [ @@ -81,6 +94,15 @@ class CatalogItem extends Model return $this->belongsTo(Event::class); } + /** @param Builder $query */ + public function scopeForTenantCatalog(Builder $query, Tenant $tenant): Builder + { + return $query + ->where('catalog_items.tenant_code', $tenant->codigo) + ->when($tenant->active_event_id !== null, fn (Builder $query): Builder => $query + ->where('catalog_items.event_id', $tenant->active_event_id)); + } + /** @return BelongsTo */ public function category(): BelongsTo { diff --git a/app/Domains/Commerce/Catalog/Requests/StoreCatalogItemRequest.php b/app/Domains/Commerce/Catalog/Requests/StoreCatalogItemRequest.php deleted file mode 100644 index 186d393..0000000 --- a/app/Domains/Commerce/Catalog/Requests/StoreCatalogItemRequest.php +++ /dev/null @@ -1,146 +0,0 @@ - */ - public function rules(): array - { - $tenantCode = $this->route('tenant')?->codigo; - $type = $this->input('type', CatalogItemType::Standard->value); - $isBundle = $type === CatalogItemType::Bundle->value; - - return [ - 'tenant_code' => ['prohibited'], - 'event_id' => ['prohibited'], - 'type' => ['sometimes', Rule::enum(CatalogItemType::class)], - 'category_id' => [ - 'sometimes', - 'nullable', - 'integer', - Rule::exists('categorias', 'id')->where( - fn ($query) => $query->where('tenant_code', $tenantCode) - ), - ], - 'brand_id' => [ - 'sometimes', - 'nullable', - 'integer', - Rule::exists('brands', 'id')->where( - fn ($query) => $query->where('tenant_codigo', $tenantCode) - ), - ], - 'slug' => [ - 'required', - 'string', - 'max:255', - Rule::unique('catalog_items', 'slug')->where( - fn ($query) => $query->where('tenant_code', $tenantCode) - ), - ], - 'nombre' => ['required', 'string', 'max:255'], - 'group_order' => ['sometimes', 'integer', 'min:0'], - 'descripcion' => ['sometimes', 'nullable', 'string'], - 'precio' => ['required', 'numeric', 'min:0'], - 'inventory_policy' => [Rule::prohibitedIf($isBundle), 'sometimes', Rule::enum(InventoryPolicy::class)], - 'inventory_subject' => ['sometimes', Rule::enum(InventorySubject::class)], - 'max_units_per_user' => ['sometimes', 'nullable', 'integer', 'min:1'], - 'has_tickets' => [Rule::prohibitedIf($isBundle), 'sometimes', 'boolean'], - 'real_stock' => [Rule::prohibitedIf($isBundle), 'sometimes', 'integer', 'min:0'], - 'inventory_id' => ['prohibited'], - 'reserved_stock' => ['prohibited'], - 'sold_units' => ['prohibited'], - 'attribute_codes' => [Rule::prohibitedIf($isBundle), 'sometimes', 'array'], - 'attribute_codes.*' => [ - 'required', - 'string', - 'distinct', - Rule::exists('attribute', 'codigo')->where( - fn ($query) => $query->where('tenant_codigo', $tenantCode) - ), - ], - 'multi_select_attribute_codes' => [Rule::prohibitedIf($isBundle), 'sometimes', 'array'], - 'multi_select_attribute_codes.*' => [ - 'required', - 'string', - 'distinct', - Rule::exists('attribute', 'codigo')->where( - fn ($query) => $query->where('tenant_codigo', $tenantCode) - ), - ], - 'hidden_attribute_codes' => [Rule::prohibitedIf($isBundle), 'sometimes', 'array'], - 'hidden_attribute_codes.*' => [ - 'required', - 'string', - 'distinct', - Rule::exists('attribute', 'codigo')->where( - fn ($query) => $query->where('tenant_codigo', $tenantCode) - ), - ], - 'images' => ['sometimes', 'array'], - 'images.*' => ['required', new ImageOrBase64Rule], - 'variants' => [Rule::prohibitedIf($isBundle), 'sometimes', 'array'], - 'variants.*.real_stock' => ['sometimes', 'integer', 'min:0'], - 'variants.*.descripcion' => ['sometimes', 'nullable', 'string'], - 'variants.*.precio' => ['sometimes', 'nullable', 'numeric', 'min:0', 'max:99999999.99'], - 'variants.*.event_date_id' => [ - 'sometimes', - 'nullable', - 'integer', - Rule::exists('event_dates', 'id')->where( - fn ($query) => $query->where('tenant_code', $tenantCode) - ), - ], - 'variants.*.event_date_ids' => ['sometimes', 'array', 'min:1'], - 'variants.*.event_date_ids.*' => [ - 'required', - 'integer', - 'distinct', - Rule::exists('event_dates', 'id')->where( - fn ($query) => $query->where('tenant_code', $tenantCode) - ), - ], - 'variants.*.inventory_id' => ['prohibited'], - 'variants.*.reserved_stock' => ['prohibited'], - 'variants.*.sold_units' => ['prohibited'], - 'variants.*.values' => ['sometimes', 'array'], - 'variants.*.values.*' => ['nullable'], - 'variants.*.values.*.*' => ['required', 'string'], - 'variants.*.images' => ['sometimes', 'array'], - 'variants.*.images.*' => ['required', new ImageOrBase64Rule], - 'components' => [ - Rule::requiredIf($isBundle), - Rule::prohibitedIf(! $isBundle), - 'array', - 'min:1', - ], - 'components.*.catalog_item_id' => [ - 'required', - 'integer', - Rule::exists('catalog_items', 'id')->where( - fn ($query) => $query->where('tenant_code', $tenantCode) - ), - ], - 'components.*.variant_id' => [ - 'sometimes', - 'nullable', - 'integer', - Rule::exists('variantes', 'id'), - ], - 'components.*.quantity' => ['required', 'integer', 'min:1'], - ]; - } -} diff --git a/app/Domains/Commerce/Catalog/Resources/CatalogItemResource.php b/app/Domains/Commerce/Catalog/Resources/CatalogItemResource.php deleted file mode 100644 index b14b256..0000000 --- a/app/Domains/Commerce/Catalog/Resources/CatalogItemResource.php +++ /dev/null @@ -1,50 +0,0 @@ - */ - public function toArray(Request $request): array - { - return [ - 'id' => $this->id, - 'type' => $this->type->value, - 'category_id' => $this->category_id, - 'brand_id' => $this->brand_id, - 'slug' => $this->slug, - 'nombre' => $this->nombre, - 'descripcion' => $this->descripcion, - 'precio' => $this->precio, - 'inventory_policy' => $this->inventory_policy?->value, - 'inventory_subject' => $this->inventory_subject->value, - 'max_units_per_user' => $this->max_units_per_user, - 'has_tickets' => $this->has_tickets, - 'real_stock' => $this->whenLoaded('inventory', fn () => $this->inventory?->real_stock), - 'images' => $this->whenLoaded('attachments', fn () => $this->attachments - ->map(fn ($attachment) => $attachment->getTemporaryUrl(1440)) - ->values()), - 'variants' => $this->whenLoaded('variants', fn () => $this->variants - ->map(fn ($variant) => [ - 'id' => $variant->id, - 'event_date_id' => $variant->event_date_id, - 'event_date' => $variant->eventDate?->date?->format('Y-m-d'), - 'event_date_ids' => $variant->selectedEventDates()->pluck('id')->values(), - 'event_dates' => $variant->selectedEventDates()->map(fn ($eventDate): string => $eventDate->date->format('Y-m-d'))->values(), - 'descripcion' => $variant->getDescription(), - 'precio' => number_format($variant->getPrice(), 2, '.', ''), - 'real_stock' => $variant->inventory?->real_stock, - 'values' => $variant->selectorOptions($this->itemAttributes), - 'images' => $variant->attachments - ->map(fn ($attachment) => $attachment->getTemporaryUrl(1440)) - ->values(), - ]) - ->values()), - ]; - } -} diff --git a/app/Domains/Commerce/Catalog/Services/CatalogService.php b/app/Domains/Commerce/Catalog/Services/CatalogService.php index 687f106..61cef22 100644 --- a/app/Domains/Commerce/Catalog/Services/CatalogService.php +++ b/app/Domains/Commerce/Catalog/Services/CatalogService.php @@ -238,7 +238,7 @@ class CatalogService $startsWithPattern = "{$normalizedTerm}%"; $paginator = CatalogItem::query() - ->where('tenant_code', $tenant->codigo) + ->forTenantCatalog($tenant) ->whereAvailable() ->where(function (Builder $query) use ($containsPattern): void { $query @@ -287,7 +287,7 @@ class CatalogService int $page, ): LengthAwarePaginator { return CatalogItem::query() - ->where('tenant_code', $tenant->codigo) + ->forTenantCatalog($tenant) ->where('category_id', $category->id) ->whereAvailable() ->with([ @@ -369,6 +369,8 @@ class CatalogService $componentItem = CatalogItem::query() ->whereKey($catalogItemId) ->where('tenant_code', $bundle->tenant_code) + ->when($bundle->event_id !== null, fn (Builder $query): Builder => $query + ->where('event_id', $bundle->event_id)) ->first(); if ($componentItem === null) { diff --git a/app/Domains/Commerce/Catalog/Services/FeaturedGroupService.php b/app/Domains/Commerce/Catalog/Services/FeaturedGroupService.php index 05182c9..561aad7 100644 --- a/app/Domains/Commerce/Catalog/Services/FeaturedGroupService.php +++ b/app/Domains/Commerce/Catalog/Services/FeaturedGroupService.php @@ -7,6 +7,7 @@ use App\Domains\Commerce\Catalog\Enums\GroupLayout; use App\Domains\Commerce\Catalog\Models\CatalogItem; use App\Domains\Commerce\Catalog\Models\FeaturedGroup; use App\Domains\Commerce\Catalog\Resources\CatalogFeaturedItemResource; +use App\Domains\Core\Tenant\Models\Tenant; use Illuminate\Database\Eloquent\Builder; use Illuminate\Pagination\LengthAwarePaginator; @@ -19,10 +20,10 @@ class FeaturedGroupService private readonly CatalogItemAllowanceService $allowances, ) {} - public function itemsResponse(FeaturedGroup $featuredGroup, int $page, ?int $userId = null): array + public function itemsResponse(FeaturedGroup $featuredGroup, Tenant $tenant, int $page, ?int $userId = null): array { if ($featuredGroup->group_layout !== GroupLayout::Paginated) { - $query = $this->itemsQuery($featuredGroup); + $query = $this->itemsQuery($featuredGroup, $tenant); if ($featuredGroup->group_layout === GroupLayout::Single) { $query->limit(1); @@ -35,7 +36,7 @@ class FeaturedGroupService return CatalogFeaturedItemResource::collection($items)->resolve(); } - $paginator = $this->paginateItems($featuredGroup, $page); + $paginator = $this->paginateItems($featuredGroup, $tenant, $page); $this->attachGroup($paginator->getCollection(), $featuredGroup); $this->allowances->attach($paginator->getCollection(), $userId); @@ -45,10 +46,10 @@ class FeaturedGroupService } /** @return Builder */ - private function itemsQuery(FeaturedGroup $featuredGroup): Builder + private function itemsQuery(FeaturedGroup $featuredGroup, Tenant $tenant): Builder { $query = CatalogItem::query() - ->where('catalog_items.tenant_code', $featuredGroup->tenant_code) + ->forTenantCatalog($tenant) ->whereAvailable() ->where(function (Builder $query): void { $query @@ -90,9 +91,10 @@ class FeaturedGroupService private function paginateItems( FeaturedGroup $featuredGroup, + Tenant $tenant, int $page, ): LengthAwarePaginator { - $paginator = $this->itemsQuery($featuredGroup)->paginate( + $paginator = $this->itemsQuery($featuredGroup, $tenant)->paginate( perPage: self::ITEMS_PER_PAGE, pageName: 'page', page: $page, diff --git a/app/Domains/Commerce/Catalog/routes/api.php b/app/Domains/Commerce/Catalog/routes/api.php index 8d8134a..9e2eb76 100644 --- a/app/Domains/Commerce/Catalog/routes/api.php +++ b/app/Domains/Commerce/Catalog/routes/api.php @@ -13,7 +13,6 @@ Route::prefix('tenants/{tenant:codigo}')->group(function (): void { ->name('categories.show'); Route::get('catalog-items/{catalogItem}', [CatalogController::class, 'show']); Route::post('catalog-items/{catalogItem}/variant-options', [CatalogController::class, 'variantOptions']); - Route::post('catalog-items', [CatalogController::class, 'store']); }); require __DIR__.'/adminapp.php'; diff --git a/app/Domains/Commerce/Purchase/Services/Checkout/CatalogSelectionResolver.php b/app/Domains/Commerce/Purchase/Services/Checkout/CatalogSelectionResolver.php index d531913..6c8888b 100644 --- a/app/Domains/Commerce/Purchase/Services/Checkout/CatalogSelectionResolver.php +++ b/app/Domains/Commerce/Purchase/Services/Checkout/CatalogSelectionResolver.php @@ -20,7 +20,7 @@ class CatalogSelectionResolver /** @var CatalogItem|null $catalogItem */ $catalogItem = CatalogItem::query() ->whereKey($catalogItemId) - ->where('tenant_code', $tenant->codigo) + ->forTenantCatalog($tenant) ->lockForUpdate() ->first(); diff --git a/app/Domains/Commerce/Purchase/Services/Checkout/StartCheckoutService.php b/app/Domains/Commerce/Purchase/Services/Checkout/StartCheckoutService.php index de12aa5..e2a1bf1 100644 --- a/app/Domains/Commerce/Purchase/Services/Checkout/StartCheckoutService.php +++ b/app/Domains/Commerce/Purchase/Services/Checkout/StartCheckoutService.php @@ -359,6 +359,13 @@ class StartCheckoutService 'cart_id' => __('api.purchase.catalog_item_wrong_tenant'), ]); } + + if ($tenant->active_event_id !== null + && $item->catalogItem->event_id !== $tenant->active_event_id) { + throw ValidationException::withMessages([ + 'cart_id' => __('api.purchase.catalog_item_missing'), + ]); + } } } diff --git a/app/Domains/Core/Tenant/Models/Tenant.php b/app/Domains/Core/Tenant/Models/Tenant.php index f23a9f3..3d573cc 100644 --- a/app/Domains/Core/Tenant/Models/Tenant.php +++ b/app/Domains/Core/Tenant/Models/Tenant.php @@ -152,6 +152,7 @@ class Tenant extends Model protected function casts(): array { return [ + 'active_event_id' => 'integer', 'search_product_layout' => ProductLayout::class, 'search_group_layout' => GroupLayout::class, 'search_items_per_page' => 'integer', diff --git a/app/Domains/Ticketing/Desfile/Services/EntryService.php b/app/Domains/Ticketing/Desfile/Services/EntryService.php index c97954d..1266177 100644 --- a/app/Domains/Ticketing/Desfile/Services/EntryService.php +++ b/app/Domains/Ticketing/Desfile/Services/EntryService.php @@ -278,7 +278,7 @@ class EntryService private function entryQuery(Tenant $tenant): Builder { return CatalogItem::query() - ->where('tenant_code', $tenant->codigo) + ->forTenantCatalog($tenant) ->where('slug', 'entrada'); } diff --git a/app/Domains/Ticketing/FiestaFutbolInfantil/Services/AccommodationService.php b/app/Domains/Ticketing/FiestaFutbolInfantil/Services/AccommodationService.php index a299d7c..6e5baae 100644 --- a/app/Domains/Ticketing/FiestaFutbolInfantil/Services/AccommodationService.php +++ b/app/Domains/Ticketing/FiestaFutbolInfantil/Services/AccommodationService.php @@ -25,7 +25,7 @@ class AccommodationService public function current(Tenant $tenant): ?CatalogItem { return CatalogItem::query() - ->where('tenant_code', $tenant->codigo) + ->forTenantCatalog($tenant) ->where('slug', 'alojamiento') ->with([ 'itemAttributes.attribute.options', @@ -126,7 +126,7 @@ class AccommodationService 'nombre' => 'Alojamientos', ]); $accommodation = CatalogItem::withTrashed() - ->where('tenant_code', $tenant->codigo) + ->forTenantCatalog($tenant) ->where('slug', 'alojamiento') ->lockForUpdate() ->first(); diff --git a/app/Domains/Ticketing/FiestaFutbolInfantil/Services/EntryService.php b/app/Domains/Ticketing/FiestaFutbolInfantil/Services/EntryService.php index 95a2b33..5d028f5 100644 --- a/app/Domains/Ticketing/FiestaFutbolInfantil/Services/EntryService.php +++ b/app/Domains/Ticketing/FiestaFutbolInfantil/Services/EntryService.php @@ -22,7 +22,7 @@ class EntryService public function all(Tenant $tenant): Collection { return CatalogItem::query() - ->where('tenant_code', $tenant->codigo) + ->forTenantCatalog($tenant) ->whereHas('category', fn ($query) => $query->where('nombre', 'Entradas')) ->whereHas('variants', fn ($query) => $query ->whereNull('replaced_by_variant_id') @@ -84,7 +84,7 @@ class EntryService { $entry = CatalogItem::query() ->whereKey($entryId) - ->where('tenant_code', $tenant->codigo) + ->forTenantCatalog($tenant) ->whereHas('category', fn ($query) => $query->where('nombre', 'Entradas')) ->firstOrFail(); @@ -96,7 +96,7 @@ class EntryService { $catalogItem = CatalogItem::query() ->whereKey($entry['id']) - ->where('tenant_code', $tenant->codigo) + ->forTenantCatalog($tenant) ->whereHas('category', fn ($query) => $query->where('nombre', 'Entradas')) ->lockForUpdate() ->firstOrFail(); diff --git a/app/Domains/Ticketing/FiestaFutbolInfantil/Services/FoodService.php b/app/Domains/Ticketing/FiestaFutbolInfantil/Services/FoodService.php index 1a86cf0..8848854 100644 --- a/app/Domains/Ticketing/FiestaFutbolInfantil/Services/FoodService.php +++ b/app/Domains/Ticketing/FiestaFutbolInfantil/Services/FoodService.php @@ -33,7 +33,7 @@ class FoodService public function current(Tenant $tenant): ?CatalogItem { return CatalogItem::query() - ->where('tenant_code', $tenant->codigo) + ->forTenantCatalog($tenant) ->where('slug', 'comida') ->with([ 'variants.catalogItem', @@ -119,7 +119,7 @@ class FoodService { return DB::transaction(function () use ($tenant, $variants): CatalogItem { $food = CatalogItem::query() - ->where('tenant_code', $tenant->codigo) + ->forTenantCatalog($tenant) ->where('slug', 'comida') ->lockForUpdate() ->firstOrFail(); @@ -233,7 +233,7 @@ class FoodService 'nombre' => 'Comidas', ]); $food = CatalogItem::withTrashed() - ->where('tenant_code', $tenant->codigo) + ->forTenantCatalog($tenant) ->where('slug', 'comida') ->lockForUpdate() ->first(); diff --git a/app/Domains/Ticketing/FiestaFutbolInfantil/Services/MerchandiseService.php b/app/Domains/Ticketing/FiestaFutbolInfantil/Services/MerchandiseService.php index b2a4cb8..3d3183c 100644 --- a/app/Domains/Ticketing/FiestaFutbolInfantil/Services/MerchandiseService.php +++ b/app/Domains/Ticketing/FiestaFutbolInfantil/Services/MerchandiseService.php @@ -27,7 +27,7 @@ class MerchandiseService public function all(Tenant $tenant): Collection { return CatalogItem::query() - ->where('tenant_code', $tenant->codigo) + ->forTenantCatalog($tenant) ->whereHas('category', fn ($query) => $query->where('nombre', 'Merchandising')) ->with([ 'itemAttributes.attribute.options', @@ -164,7 +164,7 @@ class MerchandiseService ): CatalogItem { $item = CatalogItem::query() ->whereKey($itemId) - ->where('tenant_code', $tenant->codigo) + ->forTenantCatalog($tenant) ->where('category_id', $category->id) ->lockForUpdate() ->first(); diff --git a/postman/generate-shopit-collection.php b/postman/generate-shopit-collection.php index 7393dee..85a6a31 100644 --- a/postman/generate-shopit-collection.php +++ b/postman/generate-shopit-collection.php @@ -150,23 +150,6 @@ function bodyFor(string $method, string $uri): ?array return jsonBody(['nombre' => 'Tenant Demo Actualizado', 'site_title' => 'ShopIt Demo', 'primary_color' => '#111827', 'cart_editing_policy' => 'full']); } - if ($key === 'POST api/tenants/{tenant:codigo}/catalog-items') { - return jsonBody([ - 'type' => 'standard', - 'category_id' => '{{category_id}}', - 'slug' => 'producto-demo', - 'nombre' => 'Producto Demo', - 'descripcion' => 'Producto creado desde Postman', - 'precio' => 10000, - 'inventory_policy' => 'tracked', - 'inventory_subject' => 'product', - 'max_units_per_user' => 5, - 'has_tickets' => false, - 'real_stock' => 100, - 'images' => [TINY_PNG], - ]); - } - if ($key === 'POST api/storage-test/s3/upload') { return formDataBody(['path' => 'postman/test-file.png', 'expires_in_minutes' => '60'], ['file']); } diff --git a/tests/Feature/Catalog/BundleCatalogItemTest.php b/tests/Feature/Catalog/BundleCatalogItemTest.php index 4ff8532..e0a0283 100644 --- a/tests/Feature/Catalog/BundleCatalogItemTest.php +++ b/tests/Feature/Catalog/BundleCatalogItemTest.php @@ -247,24 +247,14 @@ class BundleCatalogItemTest extends TestCase ]); } - public function test_catalog_api_creates_and_returns_bundle_details(): void + public function test_catalog_api_returns_bundle_details_created_by_the_service(): void { $component = $this->createStandardItem('api-component', 8); + $bundle = $this->createBundle('api-bundle', [ + ['catalog_item_id' => $component->id, 'quantity' => 2], + ], '75.00'); - $bundleId = $this->postJson("/api/tenants/{$this->tenant->codigo}/catalog-items", [ - 'type' => CatalogItemType::Bundle->value, - 'slug' => 'api-bundle', - 'nombre' => 'API Bundle', - 'precio' => 75, - 'components' => [ - ['catalog_item_id' => $component->id, 'quantity' => 2], - ], - ]) - ->assertCreated() - ->assertJsonPath('data.type', CatalogItemType::Bundle->value) - ->json('data.id'); - - $this->getJson("/api/tenants/{$this->tenant->codigo}/catalog-items/{$bundleId}") + $this->getJson("/api/tenants/{$this->tenant->codigo}/catalog-items/{$bundle->id}") ->assertOk() ->assertJsonPath('data.type', CatalogItemType::Bundle->value) ->assertJsonPath('data.maximum_addable_quantity', 4) @@ -274,7 +264,9 @@ class BundleCatalogItemTest extends TestCase ->assertJsonPath('data.components.0.variant_id', null) ->assertJsonPath('data.components.0.quantity', 2); - $this->postJson("/api/tenants/{$this->tenant->codigo}/catalog-items", [ + $this->expectException(ValidationException::class); + $this->catalogService->create([ + 'tenant_code' => $this->tenant->codigo, 'type' => CatalogItemType::Bundle->value, 'slug' => 'bundle-with-stock', 'nombre' => 'Invalid Bundle', @@ -283,9 +275,7 @@ class BundleCatalogItemTest extends TestCase 'components' => [ ['catalog_item_id' => $component->id, 'quantity' => 1], ], - ]) - ->assertUnprocessable() - ->assertJsonValidationErrors(['real_stock']); + ]); } public function test_bundle_components_are_preserved_when_the_bundle_is_soft_deleted(): void diff --git a/tests/Feature/Catalog/CatalogControllerTest.php b/tests/Feature/Catalog/CatalogControllerTest.php index 3663b91..56e5530 100644 --- a/tests/Feature/Catalog/CatalogControllerTest.php +++ b/tests/Feature/Catalog/CatalogControllerTest.php @@ -14,6 +14,7 @@ use App\Domains\Commerce\Catalog\Models\Category; use App\Domains\Commerce\Catalog\Models\FeaturedGroup; use App\Domains\Commerce\Catalog\Models\Inventory; use App\Domains\Core\Tenant\Models\Tenant; +use App\Domains\Ticketing\Event\Models\Event; use Illuminate\Foundation\Testing\RefreshDatabase; use Illuminate\Support\Facades\Storage; use Tests\TestCase; @@ -22,6 +23,41 @@ class CatalogControllerTest extends TestCase { use RefreshDatabase; + public function test_active_event_limits_catalog_reads_to_its_items(): void + { + $tenant = $this->createTenant('active-event-catalog'); + $group = $this->createGroup($tenant, ProductLayout::Row, 'Events', groupLayout: GroupLayout::Simple); + $group->update(['source_type' => FeaturedGroupSource::All]); + $activeEvent = Event::query()->create(['tenant_code' => $tenant->codigo, 'title' => 'Active']); + $otherEvent = Event::query()->create(['tenant_code' => $tenant->codigo, 'title' => 'Other']); + + $activeItem = $this->createItem($tenant, 'Active item', Inventory::query()->create(['real_stock' => 5])); + $activeItem->event_id = $activeEvent->id; + $activeItem->save(); + + $otherItem = $this->createItem($tenant, 'Other item', Inventory::query()->create(['real_stock' => 5])); + $otherItem->event_id = $otherEvent->id; + $otherItem->save(); + + $tenant->update(['active_event_id' => $activeEvent->id]); + + $this->getJson("/api/tenants/{$tenant->codigo}/catalog") + ->assertOk() + ->assertJsonCount(1, '0.items') + ->assertJsonPath('0.items.0.id', $activeItem->id) + ->assertJsonMissing(['nombre' => 'Other item']); + + $this->getJson("/api/tenants/{$tenant->codigo}/catalog-items?q=item") + ->assertOk() + ->assertJsonCount(1, 'data') + ->assertJsonPath('data.0.id', $activeItem->id); + + $this->getJson("/api/tenants/{$tenant->codigo}/catalog-items/{$otherItem->id}") + ->assertNotFound(); + $this->getJson("/api/tenants/{$tenant->codigo}/catalog-items/{$activeItem->id}") + ->assertOk(); + } + public function test_row_and_column_with_cart_return_item_details_variants_and_maximum_quantity(): void { $tenant = $this->createTenant('catalog-index'); diff --git a/tests/Feature/Catalog/CatalogItemControllerTest.php b/tests/Feature/Catalog/CatalogItemControllerTest.php deleted file mode 100644 index 9566a6f..0000000 --- a/tests/Feature/Catalog/CatalogItemControllerTest.php +++ /dev/null @@ -1,156 +0,0 @@ -createTenant(); - $attribute = Attribute::query()->create([ - 'tenant_codigo' => $tenant->codigo, - 'codigo' => 'size', - 'nombre' => 'Size', - 'type' => FieldType::String, - ]); - $image = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg=='; - - $response = $this->postJson("/api/tenants/{$tenant->codigo}/catalog-items", [ - 'slug' => 'shirt', - 'nombre' => 'Shirt', - 'group_order' => 7, - 'precio' => 100, - 'max_units_per_user' => 4, - 'attribute_codes' => [$attribute->codigo], - 'images' => [$image, $image], - 'variants' => [ - [ - 'real_stock' => 5, - 'values' => ['size' => 'M'], - 'images' => [$image], - ], - ], - ]); - - $response - ->assertCreated() - ->assertJsonPath('data.nombre', 'Shirt') - ->assertJsonMissingPath('data.event_id') - ->assertJsonMissingPath('data.group_order') - ->assertJsonPath('data.max_units_per_user', 4) - ->assertJsonCount(2, 'data.images') - ->assertJsonCount(1, 'data.variants') - ->assertJsonCount(1, 'data.variants.0.images'); - - $item = CatalogItem::query()->where('slug', 'shirt')->firstOrFail(); - $variant = $item->variants()->firstOrFail(); - - $this->assertSame([0, 1], $item->attachments()->get()->pluck('pivot.orden')->all()); - $this->assertSame(4, $item->max_units_per_user); - $this->assertSame(7, $item->group_order); - $this->assertNull($item->event_id); - $this->assertSame([0], $variant->attachments()->get()->pluck('pivot.orden')->all()); - $this->assertDatabaseHas('catalog_items_attachments', [ - 'catalog_item_id' => $item->id, - 'variant_id' => $variant->id, - 'orden' => 0, - 'is_enabled' => true, - ]); - } - - public function test_event_id_is_an_internal_nullable_catalog_item_link(): void - { - $this->assertTrue(Schema::hasColumn('catalog_items', 'event_id')); - - $tenant = $this->createTenant('catalog-event-link'); - $event = Event::query()->create([ - 'tenant_code' => $tenant->codigo, - 'title' => 'Internal event', - ]); - - $this->postJson("/api/tenants/{$tenant->codigo}/catalog-items", [ - 'slug' => 'client-event-id', - 'nombre' => 'Client event ID', - 'precio' => 100, - 'real_stock' => 1, - 'event_id' => $event->id, - ])->assertUnprocessable()->assertJsonValidationErrors('event_id'); - - $this->assertDatabaseMissing('catalog_items', ['slug' => 'client-event-id']); - } - - public function test_it_validates_images_before_creating_the_catalog_item(): void - { - $tenant = $this->createTenant('validation'); - - $this->postJson("/api/tenants/{$tenant->codigo}/catalog-items", [ - 'slug' => 'invalid-image', - 'nombre' => 'Invalid image', - 'precio' => 100, - 'real_stock' => 1, - 'images' => ['not-an-image'], - ])->assertUnprocessable()->assertJsonValidationErrors('images.0'); - - $this->assertDatabaseMissing('catalog_items', ['slug' => 'invalid-image']); - } - - public function test_it_rejects_a_non_positive_user_purchase_limit(): void - { - $tenant = $this->createTenant('purchase-limit-validation'); - - $this->postJson("/api/tenants/{$tenant->codigo}/catalog-items", [ - 'slug' => 'invalid-purchase-limit', - 'nombre' => 'Invalid purchase limit', - 'precio' => 100, - 'real_stock' => 10, - 'max_units_per_user' => 0, - ])->assertUnprocessable()->assertJsonValidationErrors('max_units_per_user'); - - $this->assertDatabaseMissing('catalog_items', ['slug' => 'invalid-purchase-limit']); - } - - private function createTenant(string $code = 'catalog-controller'): Tenant - { - $headerLogo = $this->createAttachment("{$code}-header"); - $footerLogo = $this->createAttachment("{$code}-footer"); - - return Tenant::query()->create([ - 'codigo' => $code, - 'nombre' => ucfirst($code), - 'dominio' => "{$code}.local", - 'primary_color' => '#000000', - 'secondary_color' => '#000000', - 'danger_color' => '#000000', - 'success_color' => '#000000', - 'header_bg_color' => '#000000', - 'footer_bg_color' => '#000000', - 'header_logo_id' => $headerLogo->id, - 'footer_logo_id' => $footerLogo->id, - ]); - } - - private function createAttachment(string $name): Attachment - { - return Attachment::query()->create([ - 'path' => "test/{$name}.png", - 'filename' => "{$name}.png", - 'type' => AttachmentType::Image, - 'mime_type' => 'image/png', - ]); - } -}