From 014b5bb012ede53aa15fea7a01c4608883335c18 Mon Sep 17 00:00:00 2001 From: ncoronel Date: Wed, 19 Aug 2026 10:15:37 -0300 Subject: [PATCH] feat(cart): implement cart editing policies and update related functionality --- ShopIt_API_Postman_Collection.json | 6 +- .../Cart/Resources/CartItemResource.php | 32 ++++++-- app/Domains/Cart/Services/CartService.php | 47 +++++++++-- .../Tenant/Enums/CartEditingPolicy.php | 32 ++++++++ app/Domains/Tenant/Models/Tenant.php | 7 +- .../Tenant/Requests/StoreTenantRequest.php | 3 +- .../Tenant/Requests/UpdateTenantRequest.php | 3 +- .../Resources/CartEditingPolicyResource.php | 23 ++++++ .../Tenant/Resources/TenantResource.php | 2 +- ...place_cart_editing_enabled_with_policy.php | 56 +++++++++++++ .../seeders/DesfilePuraTendenciaSeeder.php | 2 +- database/seeders/TenantSeeder.php | 2 + lang/en/api.php | 2 + lang/es/api.php | 2 + postman/generate-shopit-collection.php | 7 +- tests/Feature/Cart/CartControllerTest.php | 78 +++++++++++++++++++ .../AddCartEditingEnabledToTenantsTest.php | 18 ++++- .../DesfilePuraTendenciaSeederTest.php | 2 +- tests/Feature/Seeders/TenantSeederTest.php | 2 + .../Tenant/BootstrapTenantControllerTest.php | 18 +++-- 20 files changed, 305 insertions(+), 39 deletions(-) create mode 100644 app/Domains/Tenant/Enums/CartEditingPolicy.php create mode 100644 app/Domains/Tenant/Resources/CartEditingPolicyResource.php create mode 100644 database/migrations/2026_08_19_000000_replace_cart_editing_enabled_with_policy.php diff --git a/ShopIt_API_Postman_Collection.json b/ShopIt_API_Postman_Collection.json index 7bb4ba5..50301a5 100644 --- a/ShopIt_API_Postman_Collection.json +++ b/ShopIt_API_Postman_Collection.json @@ -2160,7 +2160,7 @@ }, "body": { "mode": "raw", - "raw": "{\n \"client_id\": {{client_id}},\n \"codigo\": \"{{tenant_code}}\",\n \"nombre\": \"Tenant Demo\",\n \"dominio\": \"{{tenant_domain}}\",\n \"site_title\": \"ShopIt Demo\",\n \"primary_color\": \"#111827\",\n \"secondary_color\": \"#2563EB\",\n \"danger_color\": \"#DC2626\",\n \"success_color\": \"#16A34A\",\n \"header_bg_color\": \"#FFFFFF\",\n \"footer_bg_color\": \"#111827\",\n \"header_logo\": \"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==\",\n \"footer_logo\": \"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==\",\n \"search_product_layout\": \"column_with_image\",\n \"search_group_layout\": \"paginated\",\n \"search_items_per_page\": 12,\n \"display_categories\": true,\n \"display_seach_bar\": true,\n \"display_cart\": true,\n \"cart_editing_enabled\": true,\n \"website_type_code\": \"shopit\"\n}", + "raw": "{\n \"client_id\": {{client_id}},\n \"codigo\": \"{{tenant_code}}\",\n \"nombre\": \"Tenant Demo\",\n \"dominio\": \"{{tenant_domain}}\",\n \"site_title\": \"ShopIt Demo\",\n \"primary_color\": \"#111827\",\n \"secondary_color\": \"#2563EB\",\n \"danger_color\": \"#DC2626\",\n \"success_color\": \"#16A34A\",\n \"header_bg_color\": \"#FFFFFF\",\n \"footer_bg_color\": \"#111827\",\n \"header_logo\": \"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==\",\n \"footer_logo\": \"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==\",\n \"search_product_layout\": \"column_with_image\",\n \"search_group_layout\": \"paginated\",\n \"search_items_per_page\": 12,\n \"display_categories\": true,\n \"display_seach_bar\": true,\n \"display_cart\": true,\n \"cart_editing_policy\": \"full\",\n \"website_type_code\": \"shopit\"\n}", "options": { "raw": { "language": "json" @@ -2226,7 +2226,7 @@ }, "body": { "mode": "raw", - "raw": "{\n \"nombre\": \"Tenant Demo Actualizado\",\n \"site_title\": \"ShopIt Demo\",\n \"primary_color\": \"#111827\",\n \"cart_editing_enabled\": true\n}", + "raw": "{\n \"nombre\": \"Tenant Demo Actualizado\",\n \"site_title\": \"ShopIt Demo\",\n \"primary_color\": \"#111827\",\n \"cart_editing_policy\": \"full\"\n}", "options": { "raw": { "language": "json" @@ -2266,7 +2266,7 @@ }, "body": { "mode": "raw", - "raw": "{\n \"nombre\": \"Tenant Demo Actualizado\",\n \"site_title\": \"ShopIt Demo\",\n \"primary_color\": \"#111827\",\n \"cart_editing_enabled\": true\n}", + "raw": "{\n \"nombre\": \"Tenant Demo Actualizado\",\n \"site_title\": \"ShopIt Demo\",\n \"primary_color\": \"#111827\",\n \"cart_editing_policy\": \"full\"\n}", "options": { "raw": { "language": "json" diff --git a/app/Domains/Cart/Resources/CartItemResource.php b/app/Domains/Cart/Resources/CartItemResource.php index 46a6719..4d960d6 100644 --- a/app/Domains/Cart/Resources/CartItemResource.php +++ b/app/Domains/Cart/Resources/CartItemResource.php @@ -4,6 +4,7 @@ namespace App\Domains\Cart\Resources; use App\Domains\Cart\Models\CartItem; use App\Domains\Catalog\Enums\InventoryPolicy; +use App\Domains\Catalog\Models\Variant; use App\Domains\Tenant\Models\Tenant; use Illuminate\Http\Request; use Illuminate\Http\Resources\Json\JsonResource; @@ -22,6 +23,8 @@ class CartItemResource extends JsonResource $imageUrl = null; $tenant = $request->route('tenant'); $displayImage = ! $tenant instanceof Tenant || $tenant->display_cart_item_images; + $includeVariants = $tenant instanceof Tenant + && $tenant->cart_editing_policy->allowsVariantChanges(); if ($displayImage && $selectedItem?->relationLoaded('attachments')) { $imageUrl = $selectedItem->attachments->first()?->getTemporaryUrl(1440); @@ -39,14 +42,27 @@ class CartItemResource extends JsonResource 'variant_id' => $this->variant_id, 'nombre' => $selectedItem?->getName(), 'imagen' => $imageUrl, - 'variant' => $this->variant === null ? null : [ - 'id' => $this->variant->id, - 'precio' => $this->formatMoney($this->variant->getPrice()), - 'stock_tecnico' => $this->catalogItem->inventory_policy === InventoryPolicy::Unlimited - ? null - : $this->variant->inventory->availableStock(), - 'values' => $this->variant->selectorOptions($this->catalogItem->itemAttributes), - ], + 'variant' => $this->variant === null ? null : $this->variantData($this->variant), + 'variants' => $this->when( + $includeVariants, + fn () => $this->catalogItem + ->visibleVariants($this->variant_id) + ->map(fn (Variant $variant): array => $this->variantData($variant)) + ->values(), + ), + ]; + } + + /** @return array */ + protected function variantData(Variant $variant): array + { + return [ + 'id' => $variant->id, + 'precio' => $this->formatMoney($variant->precio ?? $this->catalogItem->precio), + 'stock_tecnico' => $this->catalogItem->inventory_policy === InventoryPolicy::Unlimited + ? null + : $variant->inventory->availableStock(), + 'values' => $variant->selectorOptions($this->catalogItem->itemAttributes), ]; } diff --git a/app/Domains/Cart/Services/CartService.php b/app/Domains/Cart/Services/CartService.php index 954f784..8bb6268 100644 --- a/app/Domains/Cart/Services/CartService.php +++ b/app/Domains/Cart/Services/CartService.php @@ -8,6 +8,7 @@ use App\Domains\Tenant\Models\Tenant; use Illuminate\Http\Request; use Illuminate\Support\Facades\Auth; use Illuminate\Support\Str; +use Illuminate\Validation\ValidationException; use Symfony\Component\HttpFoundation\Cookie; use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; @@ -27,7 +28,7 @@ class CartService return $this->makeEmptyCart($tenant); } - return $this->loadCart($cart); + return $this->loadCart($cart, $tenant); } /** @@ -46,7 +47,7 @@ class CartService $cart->addItem($catalogItemId, $variantId, $quantity); return [ - 'cart' => $this->loadCart($cart), + 'cart' => $this->loadCart($cart, $tenant), 'guest_token' => $resolvedIdentity['generated_guest_token'], ]; } @@ -59,20 +60,38 @@ class CartService ?int $variantId, bool $updateVariant, ): Cart { + if ($updateVariant && ! $tenant->cart_editing_policy->allowsVariantChanges()) { + throw ValidationException::withMessages([ + 'variant_id' => __('api.cart.variant_change_disabled'), + ]); + } + + if (! $updateVariant && ! $tenant->cart_editing_policy->allowsQuantityChanges()) { + throw ValidationException::withMessages([ + 'cantidad' => __('api.cart.editing_disabled'), + ]); + } + $identity = $this->requireIdentity($request); $cart = $this->findCartOrFail($tenant, $identity); $cart->updateItem($cartItemId, $quantity, $variantId, $updateVariant); - return $this->loadCart($cart); + return $this->loadCart($cart, $tenant); } public function removeItem(Tenant $tenant, Request $request, int $cartItemId): Cart { + if (! $tenant->cart_editing_policy->allowsRemoval()) { + throw ValidationException::withMessages([ + 'cart_item' => __('api.cart.editing_disabled'), + ]); + } + $identity = $this->requireIdentity($request); $cart = $this->findCartOrFail($tenant, $identity); $cart->removeItem($cartItemId); - return $this->loadCart($cart); + return $this->loadCart($cart, $tenant); } public function makeGuestTokenCookie(string $guestToken): Cookie @@ -102,9 +121,9 @@ class CartService return $cart; } - protected function loadCart(Cart $cart): Cart + protected function loadCart(Cart $cart, Tenant $tenant): Cart { - return $cart->fresh()->load([ + $relations = [ 'items.catalogItem.attachments', 'items.catalogItem.inventory', 'items.catalogItem.itemAttributes.attribute', @@ -113,7 +132,21 @@ class CartService 'items.variant.definitions.itemAttribute.attribute.options', 'items.variant.eventDates', 'items.variant.eventDate', - ]); + ]; + + if ($tenant->cart_editing_policy->allowsVariantChanges()) { + $relations = [ + ...$relations, + 'items.catalogItem.variants' => fn ($query) => $query->orderBy('id'), + 'items.catalogItem.variants.inventory', + 'items.catalogItem.variants.definitions' => fn ($query) => $query->orderBy('id'), + 'items.catalogItem.variants.definitions.itemAttribute.attribute.options', + 'items.catalogItem.variants.eventDates', + 'items.catalogItem.variants.eventDate', + ]; + } + + return $cart->fresh()->load($relations); } /** diff --git a/app/Domains/Tenant/Enums/CartEditingPolicy.php b/app/Domains/Tenant/Enums/CartEditingPolicy.php new file mode 100644 index 0000000..c90fc05 --- /dev/null +++ b/app/Domains/Tenant/Enums/CartEditingPolicy.php @@ -0,0 +1,32 @@ +allowsRemoval() + || $this->allowsQuantityChanges() + || $this->allowsVariantChanges(); + } +} diff --git a/app/Domains/Tenant/Models/Tenant.php b/app/Domains/Tenant/Models/Tenant.php index 7016029..876ec6f 100644 --- a/app/Domains/Tenant/Models/Tenant.php +++ b/app/Domains/Tenant/Models/Tenant.php @@ -11,6 +11,7 @@ use App\Domains\Client\Models\Client; use App\Domains\Event\Models\EventDate; use App\Domains\Menu\Models\Menu; use App\Domains\Menu\Models\TenantMenu; +use App\Domains\Tenant\Enums\CartEditingPolicy; use Illuminate\Database\Eloquent\Attributes\Fillable; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; @@ -44,7 +45,7 @@ use Illuminate\Support\Facades\Schema; 'display_categories', 'display_seach_bar', 'display_cart', - 'cart_editing_enabled', + 'cart_editing_policy', 'display_cart_item_images', 'scanner_category_validation_enabled', 'event_title', @@ -63,7 +64,7 @@ class Tenant extends Model 'display_categories' => true, 'display_seach_bar' => true, 'display_cart' => true, - 'cart_editing_enabled' => true, + 'cart_editing_policy' => CartEditingPolicy::Full->value, 'display_cart_item_images' => true, 'scanner_category_validation_enabled' => true, ]; @@ -114,7 +115,7 @@ class Tenant extends Model 'display_categories' => 'boolean', 'display_seach_bar' => 'boolean', 'display_cart' => 'boolean', - 'cart_editing_enabled' => 'boolean', + 'cart_editing_policy' => CartEditingPolicy::class, 'display_cart_item_images' => 'boolean', 'scanner_category_validation_enabled' => 'boolean', ]; diff --git a/app/Domains/Tenant/Requests/StoreTenantRequest.php b/app/Domains/Tenant/Requests/StoreTenantRequest.php index 4743178..ad25958 100644 --- a/app/Domains/Tenant/Requests/StoreTenantRequest.php +++ b/app/Domains/Tenant/Requests/StoreTenantRequest.php @@ -5,6 +5,7 @@ namespace App\Domains\Tenant\Requests; use App\Domains\Catalog\Enums\GroupLayout; use App\Domains\Catalog\Enums\ProductLayout; use App\Domains\Shared\Rules\ImageOrBase64Rule; +use App\Domains\Tenant\Enums\CartEditingPolicy; use App\Domains\Tenant\Services\WebsiteExtraService; use App\Domains\Tenant\Support\TenantDomainNormalizer; use Closure; @@ -107,7 +108,7 @@ class StoreTenantRequest extends FormRequest 'display_categories' => ['sometimes', 'boolean'], 'display_seach_bar' => ['sometimes', 'boolean'], 'display_cart' => ['sometimes', 'boolean'], - 'cart_editing_enabled' => ['sometimes', 'boolean'], + 'cart_editing_policy' => ['sometimes', Rule::enum(CartEditingPolicy::class)], 'display_cart_item_images' => ['sometimes', 'boolean'], 'scanner_category_validation_enabled' => ['sometimes', 'boolean'], 'website_type_code' => [ diff --git a/app/Domains/Tenant/Requests/UpdateTenantRequest.php b/app/Domains/Tenant/Requests/UpdateTenantRequest.php index 6328743..cd45b58 100644 --- a/app/Domains/Tenant/Requests/UpdateTenantRequest.php +++ b/app/Domains/Tenant/Requests/UpdateTenantRequest.php @@ -5,6 +5,7 @@ namespace App\Domains\Tenant\Requests; use App\Domains\Catalog\Enums\GroupLayout; use App\Domains\Catalog\Enums\ProductLayout; use App\Domains\Shared\Rules\ImageOrBase64Rule; +use App\Domains\Tenant\Enums\CartEditingPolicy; use App\Domains\Tenant\Models\Tenant; use App\Domains\Tenant\Support\TenantDomainNormalizer; use Closure; @@ -128,7 +129,7 @@ class UpdateTenantRequest extends FormRequest 'display_categories' => ['sometimes', 'boolean'], 'display_seach_bar' => ['sometimes', 'boolean'], 'display_cart' => ['sometimes', 'boolean'], - 'cart_editing_enabled' => ['sometimes', 'boolean'], + 'cart_editing_policy' => ['sometimes', Rule::enum(CartEditingPolicy::class)], 'display_cart_item_images' => ['sometimes', 'boolean'], 'scanner_category_validation_enabled' => ['sometimes', 'boolean'], ]; diff --git a/app/Domains/Tenant/Resources/CartEditingPolicyResource.php b/app/Domains/Tenant/Resources/CartEditingPolicyResource.php new file mode 100644 index 0000000..aba3b20 --- /dev/null +++ b/app/Domains/Tenant/Resources/CartEditingPolicyResource.php @@ -0,0 +1,23 @@ + */ + public function toArray(Request $request): array + { + return [ + 'code' => $this->resource->value, + 'allow_modify' => $this->resource->allowsModification(), + 'allow_delete' => $this->resource->allowsRemoval(), + 'allow_update_quantity' => $this->resource->allowsQuantityChanges(), + 'allow_update_variant' => $this->resource->allowsVariantChanges(), + ]; + } +} diff --git a/app/Domains/Tenant/Resources/TenantResource.php b/app/Domains/Tenant/Resources/TenantResource.php index 5eab5cc..fd1b898 100644 --- a/app/Domains/Tenant/Resources/TenantResource.php +++ b/app/Domains/Tenant/Resources/TenantResource.php @@ -75,7 +75,7 @@ class TenantResource extends JsonResource 'display_categories' => $this->display_categories, 'display_seach_bar' => $this->display_seach_bar, 'display_cart' => $this->display_cart, - 'cart_editing_enabled' => $this->cart_editing_enabled, + 'cart_editing_policy' => CartEditingPolicyResource::make($this->cart_editing_policy), 'display_cart_item_images' => $this->display_cart_item_images, 'scanner_category_validation_enabled' => $this->scanner_category_validation_enabled, 'social_media' => $this->whenLoaded( diff --git a/database/migrations/2026_08_19_000000_replace_cart_editing_enabled_with_policy.php b/database/migrations/2026_08_19_000000_replace_cart_editing_enabled_with_policy.php new file mode 100644 index 0000000..163c5aa --- /dev/null +++ b/database/migrations/2026_08_19_000000_replace_cart_editing_enabled_with_policy.php @@ -0,0 +1,56 @@ + */ + private const TENANT_POLICIES = [ + 'sonder' => 'quantity_and_remove', + 'fiesta_futbol_infantil' => 'full', + 'desfile_pura_tendencia' => 'disabled', + ]; + + public function up(): void + { + Schema::table('tenants', function (Blueprint $table): void { + $table->string('cart_editing_policy') + ->default('full') + ->after('display_cart'); + }); + + DB::table('tenants') + ->where('cart_editing_enabled', false) + ->update(['cart_editing_policy' => 'disabled']); + + foreach (self::TENANT_POLICIES as $tenantCode => $policy) { + DB::table('tenants') + ->where('codigo', $tenantCode) + ->update(['cart_editing_policy' => $policy]); + } + + Schema::table('tenants', function (Blueprint $table): void { + $table->dropColumn('cart_editing_enabled'); + }); + } + + public function down(): void + { + Schema::table('tenants', function (Blueprint $table): void { + $table->boolean('cart_editing_enabled') + ->default(true) + ->after('display_cart'); + }); + + DB::table('tenants') + ->where('cart_editing_policy', 'disabled') + ->update(['cart_editing_enabled' => false]); + + Schema::table('tenants', function (Blueprint $table): void { + $table->dropColumn('cart_editing_policy'); + }); + } +}; diff --git a/database/seeders/DesfilePuraTendenciaSeeder.php b/database/seeders/DesfilePuraTendenciaSeeder.php index cd1dac1..7999ffb 100644 --- a/database/seeders/DesfilePuraTendenciaSeeder.php +++ b/database/seeders/DesfilePuraTendenciaSeeder.php @@ -72,7 +72,7 @@ class DesfilePuraTendenciaSeeder extends Seeder 'display_categories' => false, 'display_seach_bar' => false, 'display_cart' => true, - 'cart_editing_enabled' => false, + 'cart_editing_policy' => 'disabled', 'display_cart_item_images' => false, 'scanner_category_validation_enabled' => false, 'website_type_code' => 'onticket', diff --git a/database/seeders/TenantSeeder.php b/database/seeders/TenantSeeder.php index 110a709..4225989 100644 --- a/database/seeders/TenantSeeder.php +++ b/database/seeders/TenantSeeder.php @@ -75,6 +75,7 @@ class TenantSeeder extends Seeder 'display_categories' => true, 'display_seach_bar' => true, 'display_cart' => true, + 'cart_editing_policy' => 'quantity_and_remove', 'header_logo' => $this->uploadedImage('images/tennants/sonder/sonder_header.png', 'sonder_header.png'), 'footer_logo' => $this->uploadedImage('images/tennants/sonder/sonder_footer.png', 'sonder_footer.png'), 'social_media' => self::SOCIAL_MEDIA, @@ -131,6 +132,7 @@ class TenantSeeder extends Seeder 'display_categories' => false, 'display_seach_bar' => false, 'display_cart' => true, + 'cart_editing_policy' => 'full', 'header_logo' => $this->uploadedImage( 'images/tennants/fiesta_futbol_infantil/futbol_infantil_header.png', 'futbol_infantil_header.png', diff --git a/lang/en/api.php b/lang/en/api.php index e632e90..89a12c0 100644 --- a/lang/en/api.php +++ b/lang/en/api.php @@ -22,6 +22,8 @@ return [ 'google_email_unverified' => 'The Google account must have a verified email.', ], 'cart' => [ + 'editing_disabled' => 'Cart editing is disabled.', + 'variant_change_disabled' => 'Variant changes are disabled for this cart.', 'item_added' => 'Product added to cart.', 'quantity_updated' => 'Product quantity updated.', 'item_updated' => 'Product updated.', diff --git a/lang/es/api.php b/lang/es/api.php index c968c0d..53b2911 100644 --- a/lang/es/api.php +++ b/lang/es/api.php @@ -22,6 +22,8 @@ return [ 'google_email_unverified' => 'La cuenta de Google debe tener el email verificado.', ], 'cart' => [ + 'editing_disabled' => 'La edición del carrito está deshabilitada.', + 'variant_change_disabled' => 'El cambio de variante está deshabilitado para este carrito.', 'item_added' => 'Producto agregado al carrito.', 'quantity_updated' => 'Cantidad de producto actualizada.', 'item_updated' => 'Producto actualizado.', diff --git a/postman/generate-shopit-collection.php b/postman/generate-shopit-collection.php index bee6476..1fc99f9 100644 --- a/postman/generate-shopit-collection.php +++ b/postman/generate-shopit-collection.php @@ -8,7 +8,6 @@ declare(strict_types=1); * Run from the backend root with: * php postman/generate-shopit-collection.php */ - $root = dirname(__DIR__); chdir($root); @@ -38,7 +37,7 @@ function jsonBody(array $payload): array } /** @param array $fields - * @return array + * @return array */ function formDataBody(array $fields, array $fileFields = []): array { @@ -137,13 +136,13 @@ function bodyFor(string $method, string $uri): ?array 'display_categories' => true, 'display_seach_bar' => true, 'display_cart' => true, - 'cart_editing_enabled' => true, + 'cart_editing_policy' => 'full', 'website_type_code' => 'shopit', ]); } if (in_array($key, ['PUT api/tenants/{tenant}', 'PATCH api/tenants/{tenant}'], true)) { - return jsonBody(['nombre' => 'Tenant Demo Actualizado', 'site_title' => 'ShopIt Demo', 'primary_color' => '#111827', 'cart_editing_enabled' => true]); + 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') { diff --git a/tests/Feature/Cart/CartControllerTest.php b/tests/Feature/Cart/CartControllerTest.php index 54398d2..22286dc 100644 --- a/tests/Feature/Cart/CartControllerTest.php +++ b/tests/Feature/Cart/CartControllerTest.php @@ -121,6 +121,7 @@ class CartControllerTest extends TestCase ->assertOk() ->assertJsonPath('data.items.0.catalog_item_id', $item->id) ->assertJsonPath('data.items.0.variant_id', $variant->id) + ->assertJsonPath('data.items.0.variants.0.id', $variant->id) ->assertJsonPath('data.items.0.cantidad', 5) ->assertJsonPath('data.subtotal', '125.00'); @@ -383,6 +384,83 @@ class CartControllerTest extends TestCase ]); } + public function test_quantity_and_remove_policy_hides_variants_and_rejects_variant_changes(): void + { + $tenant = $this->createTenant('acme'); + $tenant->update(['cart_editing_policy' => 'quantity_and_remove']); + [$item, $firstVariant] = $this->createVariantItem($tenant, 10, '15.00'); + $secondInventory = Inventory::query()->create(['real_stock' => 10]); + $secondVariant = $item->variants()->create(['inventory_id' => $secondInventory->id]); + $createResponse = $this->postJson('/api/tenants/acme/cart/items', [ + 'catalog_item_id' => $item->id, + 'variant_id' => $firstVariant->id, + 'cantidad' => 1, + ])->assertJsonMissingPath('data.items.0.variants'); + $guestToken = $createResponse->getCookie('guest_token', false)?->getValue(); + $cartItemId = $createResponse->json('data.items.0.id'); + + $this->call( + 'PATCH', + "/api/tenants/acme/cart/items/{$cartItemId}", + [], + ['guest_token' => $guestToken], + [], + ['HTTP_Accept' => 'application/json', 'CONTENT_TYPE' => 'application/json'], + json_encode(['cantidad' => 2]), + )->assertOk()->assertJsonPath('data.items.0.cantidad', 2); + + $this->call( + 'PATCH', + "/api/tenants/acme/cart/items/{$cartItemId}", + [], + ['guest_token' => $guestToken], + [], + ['HTTP_Accept' => 'application/json', 'CONTENT_TYPE' => 'application/json'], + json_encode(['cantidad' => 2, 'variant_id' => $secondVariant->id]), + )->assertUnprocessable()->assertJsonValidationErrors('variant_id'); + + $this->call( + 'DELETE', + "/api/tenants/acme/cart/items/{$cartItemId}", + [], + ['guest_token' => $guestToken], + [], + ['HTTP_Accept' => 'application/json'], + )->assertOk()->assertJsonPath('data.items', []); + } + + public function test_disabled_policy_rejects_quantity_changes_and_removal(): void + { + $tenant = $this->createTenant('acme'); + $tenant->update(['cart_editing_policy' => 'disabled']); + $item = $this->createDirectItem($tenant, 10, '15.00'); + $createResponse = $this->postJson('/api/tenants/acme/cart/items', [ + 'catalog_item_id' => $item->id, + 'cantidad' => 1, + ])->assertOk(); + $guestToken = $createResponse->getCookie('guest_token', false)?->getValue(); + $cartItemId = $createResponse->json('data.items.0.id'); + + $this->call( + 'PATCH', + "/api/tenants/acme/cart/items/{$cartItemId}", + [], + ['guest_token' => $guestToken], + [], + ['HTTP_Accept' => 'application/json', 'CONTENT_TYPE' => 'application/json'], + json_encode(['cantidad' => 2]), + )->assertUnprocessable()->assertJsonValidationErrors('cantidad'); + + $this->call( + 'DELETE', + "/api/tenants/acme/cart/items/{$cartItemId}", + [], + ['guest_token' => $guestToken], + [], + ['HTTP_Accept' => 'application/json'], + )->assertUnprocessable()->assertJsonValidationErrors('cart_item'); + } + public function test_it_requires_a_variant_when_the_item_has_variant_inventory(): void { $tenant = $this->createTenant('acme'); diff --git a/tests/Feature/Migrations/AddCartEditingEnabledToTenantsTest.php b/tests/Feature/Migrations/AddCartEditingEnabledToTenantsTest.php index eb10e23..896959c 100644 --- a/tests/Feature/Migrations/AddCartEditingEnabledToTenantsTest.php +++ b/tests/Feature/Migrations/AddCartEditingEnabledToTenantsTest.php @@ -10,10 +10,10 @@ class AddCartEditingEnabledToTenantsTest extends TestCase { use RefreshDatabase; - public function test_it_disables_cart_editing_only_for_desfile(): void + public function test_it_migrates_the_boolean_to_the_equivalent_policy(): void { $migration = require database_path( - 'migrations/2026_08_18_060000_add_cart_editing_enabled_to_tenants_table.php' + 'migrations/2026_08_19_000000_replace_cart_editing_enabled_with_policy.php' ); $migration->down(); @@ -21,7 +21,17 @@ class AddCartEditingEnabledToTenantsTest extends TestCase $this->assertDatabaseHas('tenants', [ 'codigo' => 'desfile_pura_tendencia', - 'cart_editing_enabled' => false, + 'cart_editing_policy' => 'disabled', + ]); + + $this->assertDatabaseHas('tenants', [ + 'codigo' => 'sonder', + 'cart_editing_policy' => 'quantity_and_remove', + ]); + + $this->assertDatabaseHas('tenants', [ + 'codigo' => 'fiesta_futbol_infantil', + 'cart_editing_policy' => 'full', ]); DB::table('tenants')->insert([ @@ -32,7 +42,7 @@ class AddCartEditingEnabledToTenantsTest extends TestCase $this->assertDatabaseHas('tenants', [ 'codigo' => 'cart-editing-default', - 'cart_editing_enabled' => true, + 'cart_editing_policy' => 'full', ]); } } diff --git a/tests/Feature/Seeders/DesfilePuraTendenciaSeederTest.php b/tests/Feature/Seeders/DesfilePuraTendenciaSeederTest.php index e92b85c..cd1a80b 100644 --- a/tests/Feature/Seeders/DesfilePuraTendenciaSeederTest.php +++ b/tests/Feature/Seeders/DesfilePuraTendenciaSeederTest.php @@ -44,7 +44,7 @@ class DesfilePuraTendenciaSeederTest extends TestCase 'footer_bg_color' => '#D4441C', 'display_categories' => false, 'display_seach_bar' => false, - 'cart_editing_enabled' => false, + 'cart_editing_policy' => 'disabled', 'display_cart_item_images' => false, 'site_title' => 'Desfile Pura Tendencia', ]); diff --git a/tests/Feature/Seeders/TenantSeederTest.php b/tests/Feature/Seeders/TenantSeederTest.php index f0495aa..54ed1a3 100644 --- a/tests/Feature/Seeders/TenantSeederTest.php +++ b/tests/Feature/Seeders/TenantSeederTest.php @@ -61,6 +61,7 @@ class TenantSeederTest extends TestCase ->sole(); $this->assertSame('shopit', $sonder->website_type_code); + $this->assertSame('quantity_and_remove', $sonder->cart_editing_policy->value); $carousel = $sonder->websiteExtras ->firstWhere('websiteTypeExtra.codigo', 'carousel'); @@ -81,6 +82,7 @@ class TenantSeederTest extends TestCase ->sole(); $this->assertSame('onticket', $fiesta->website_type_code); + $this->assertSame('full', $fiesta->cart_editing_policy->value); $extras = $fiesta->websiteExtras->keyBy('websiteTypeExtra.codigo'); $this->assertEqualsCanonicalizing( diff --git a/tests/Feature/Tenant/BootstrapTenantControllerTest.php b/tests/Feature/Tenant/BootstrapTenantControllerTest.php index 9526d32..9ceafd0 100644 --- a/tests/Feature/Tenant/BootstrapTenantControllerTest.php +++ b/tests/Feature/Tenant/BootstrapTenantControllerTest.php @@ -73,7 +73,7 @@ class BootstrapTenantControllerTest extends TestCase 'display_categories' => false, 'display_seach_bar' => false, 'display_cart' => false, - 'cart_editing_enabled' => false, + 'cart_editing_policy' => 'disabled', 'display_cart_item_images' => false, ]); @@ -91,7 +91,11 @@ class BootstrapTenantControllerTest extends TestCase ->assertJsonPath('data.display_categories', false) ->assertJsonPath('data.display_seach_bar', false) ->assertJsonPath('data.display_cart', false) - ->assertJsonPath('data.cart_editing_enabled', false) + ->assertJsonPath('data.cart_editing_policy.code', 'disabled') + ->assertJsonPath('data.cart_editing_policy.allow_modify', false) + ->assertJsonPath('data.cart_editing_policy.allow_delete', false) + ->assertJsonPath('data.cart_editing_policy.allow_update_quantity', false) + ->assertJsonPath('data.cart_editing_policy.allow_update_variant', false) ->assertJsonPath('data.display_cart_item_images', false) ->assertJsonPath('data.header_bg_color', '#ffffff')->assertJsonPath('data.footer_bg_color', '#ffffff'); @@ -758,7 +762,7 @@ class BootstrapTenantControllerTest extends TestCase $response = $this->putJson("/api/tenants/{$tenant->codigo}", [ 'primary_color' => '#000000', - 'cart_editing_enabled' => false, + 'cart_editing_policy' => 'quantity_and_remove', 'display_cart_item_images' => false, ]); @@ -767,7 +771,11 @@ class BootstrapTenantControllerTest extends TestCase ->assertJsonPath('data.codigo', 'acme') ->assertJsonPath('data.nombre', 'Acme') ->assertJsonPath('data.primary_color', '#000000') - ->assertJsonPath('data.cart_editing_enabled', false) + ->assertJsonPath('data.cart_editing_policy.code', 'quantity_and_remove') + ->assertJsonPath('data.cart_editing_policy.allow_modify', true) + ->assertJsonPath('data.cart_editing_policy.allow_delete', true) + ->assertJsonPath('data.cart_editing_policy.allow_update_quantity', true) + ->assertJsonPath('data.cart_editing_policy.allow_update_variant', false) ->assertJsonPath('data.display_cart_item_images', false); $this->assertDatabaseHas('tenants', [ @@ -775,7 +783,7 @@ class BootstrapTenantControllerTest extends TestCase 'codigo' => 'acme', 'nombre' => 'Acme', 'primary_color' => '#000000', - 'cart_editing_enabled' => false, + 'cart_editing_policy' => 'quantity_and_remove', 'display_cart_item_images' => false, ]); }