From 12c10bbe066de4c5595f106df0ae9db737786e68 Mon Sep 17 00:00:00 2001 From: ncoronel Date: Mon, 24 Aug 2026 10:49:14 -0300 Subject: [PATCH 01/10] feat(catalog): add is_enabled attribute to categories and filter items accordingly --- app/Domains/Catalog/Models/Category.php | 6 +++ .../Catalog/Services/FeaturedGroupService.php | 9 ++++ ...000_add_is_enabled_to_categorias_table.php | 22 +++++++++ .../Feature/Catalog/CatalogControllerTest.php | 47 +++++++++++++++++++ tests/Feature/Catalog/CatalogSchemaTest.php | 16 +++++++ 5 files changed, 100 insertions(+) create mode 100644 database/migrations/2026_08_24_000000_add_is_enabled_to_categorias_table.php diff --git a/app/Domains/Catalog/Models/Category.php b/app/Domains/Catalog/Models/Category.php index ee683ba..c0ed114 100644 --- a/app/Domains/Catalog/Models/Category.php +++ b/app/Domains/Catalog/Models/Category.php @@ -15,6 +15,7 @@ use Illuminate\Database\Eloquent\Relations\HasMany; 'tenant_code', 'categoria_id', 'nombre', + 'is_enabled', ])] class Category extends Model { @@ -22,6 +23,10 @@ class Category extends Model protected $table = 'categorias'; + protected $attributes = [ + 'is_enabled' => true, + ]; + /** * @return array */ @@ -29,6 +34,7 @@ class Category extends Model { return [ 'categoria_id' => 'integer', + 'is_enabled' => 'boolean', ]; } diff --git a/app/Domains/Catalog/Services/FeaturedGroupService.php b/app/Domains/Catalog/Services/FeaturedGroupService.php index f01b679..0302a1f 100644 --- a/app/Domains/Catalog/Services/FeaturedGroupService.php +++ b/app/Domains/Catalog/Services/FeaturedGroupService.php @@ -49,6 +49,15 @@ class FeaturedGroupService { $query = CatalogItem::query() ->where('catalog_items.tenant_code', $featuredGroup->tenant_code) + ->where(function (Builder $query): void { + $query + ->whereDoesntHave('category') + ->orWhereHas( + 'category', + fn (Builder $categoryQuery): Builder => $categoryQuery + ->where('is_enabled', true), + ); + }) ->with([ 'inventory', 'attachments', diff --git a/database/migrations/2026_08_24_000000_add_is_enabled_to_categorias_table.php b/database/migrations/2026_08_24_000000_add_is_enabled_to_categorias_table.php new file mode 100644 index 0000000..e61c5b0 --- /dev/null +++ b/database/migrations/2026_08_24_000000_add_is_enabled_to_categorias_table.php @@ -0,0 +1,22 @@ +boolean('is_enabled')->default(true)->after('nombre'); + }); + } + + public function down(): void + { + Schema::table('categorias', function (Blueprint $table): void { + $table->dropColumn('is_enabled'); + }); + } +}; diff --git a/tests/Feature/Catalog/CatalogControllerTest.php b/tests/Feature/Catalog/CatalogControllerTest.php index 63f1051..cdc98d2 100644 --- a/tests/Feature/Catalog/CatalogControllerTest.php +++ b/tests/Feature/Catalog/CatalogControllerTest.php @@ -367,6 +367,53 @@ class CatalogControllerTest extends TestCase ->assertJsonPath('1.items.meta.total', 2); } + public function test_index_excludes_items_from_disabled_categories(): void + { + $tenant = $this->createTenant('catalog-disabled-category'); + $group = $this->createGroup( + $tenant, + ProductLayout::Row, + 'All products', + groupLayout: GroupLayout::Simple, + ); + $enabledCategory = Category::query()->create([ + 'tenant_code' => $tenant->codigo, + 'nombre' => 'Enabled', + ]); + $disabledCategory = Category::query()->create([ + 'tenant_code' => $tenant->codigo, + 'nombre' => 'Disabled', + 'is_enabled' => false, + ]); + + $enabledItem = $this->createItem($tenant, 'Enabled item'); + $enabledItem->category()->associate($enabledCategory)->save(); + $disabledItem = $this->createItem($tenant, 'Disabled item'); + $disabledItem->category()->associate($disabledCategory)->save(); + $uncategorizedItem = $this->createItem($tenant, 'Uncategorized item'); + + foreach ([$enabledItem, $disabledItem, $uncategorizedItem] as $order => $item) { + $group->featuredItems()->create([ + 'catalog_item_id' => $item->id, + 'order' => $order, + ]); + } + + $this->getJson("/api/tenants/{$tenant->codigo}/catalog") + ->assertOk() + ->assertJsonCount(2, '0.items') + ->assertJsonPath('0.items.0.nombre', 'Enabled item') + ->assertJsonPath('0.items.1.nombre', 'Uncategorized item') + ->assertJsonMissing(['nombre' => 'Disabled item']); + + $this->getJson( + "/api/tenants/{$tenant->codigo}/catalog/featured-groups/{$group->id}/items" + ) + ->assertOk() + ->assertJsonCount(2) + ->assertJsonMissing(['nombre' => 'Disabled item']); + } + private function createGroup( Tenant $tenant, ProductLayout $layout, diff --git a/tests/Feature/Catalog/CatalogSchemaTest.php b/tests/Feature/Catalog/CatalogSchemaTest.php index 705bfc7..43147b4 100644 --- a/tests/Feature/Catalog/CatalogSchemaTest.php +++ b/tests/Feature/Catalog/CatalogSchemaTest.php @@ -5,6 +5,7 @@ namespace Tests\Feature\Catalog; use App\Domains\Attachable\Enums\AttachmentType; use App\Domains\Attachable\Models\Attachment; use App\Domains\Catalog\Models\CatalogItem; +use App\Domains\Catalog\Models\Category; use App\Domains\Catalog\Models\Inventory; use App\Domains\Tenant\Models\Tenant; use Illuminate\Foundation\Testing\RefreshDatabase; @@ -15,6 +16,21 @@ class CatalogSchemaTest extends TestCase { use RefreshDatabase; + public function test_categories_are_enabled_by_default(): void + { + $this->assertTrue(Schema::hasColumn('categorias', 'is_enabled')); + + $category = Category::query()->create([ + 'nombre' => 'Enabled category', + ]); + + $this->assertTrue($category->is_enabled); + $this->assertDatabaseHas('categorias', [ + 'id' => $category->id, + 'is_enabled' => true, + ]); + } + public function test_legacy_product_tables_are_replaced_by_catalog_tables(): void { $this->assertFalse(Schema::hasTable('productos')); From 80adbd9ca944e33ceb8a461aa14e18c46a8fedca Mon Sep 17 00:00:00 2001 From: ncoronel Date: Mon, 24 Aug 2026 11:31:26 -0300 Subject: [PATCH 02/10] feat(category-visibility): implement category visibility controller and service with update functionality --- .../CategoryVisibilityController.php | 49 +++++++++++++++++++ .../Enums/FiestaCategory.php | 21 ++++++++ .../UpdateCategoryVisibilityRequest.php | 16 ++++++ .../Services/CategoryVisibilityService.php | 29 +++++++++++ .../FiestaFutbolInfantil/routes/api.php | 15 ++++++ .../EntryControllerTest.php | 39 +++++++++++++++ 6 files changed, 169 insertions(+) create mode 100644 app/Domains/FiestaFutbolInfantil/Controllers/CategoryVisibilityController.php create mode 100644 app/Domains/FiestaFutbolInfantil/Enums/FiestaCategory.php create mode 100644 app/Domains/FiestaFutbolInfantil/Requests/UpdateCategoryVisibilityRequest.php create mode 100644 app/Domains/FiestaFutbolInfantil/Services/CategoryVisibilityService.php diff --git a/app/Domains/FiestaFutbolInfantil/Controllers/CategoryVisibilityController.php b/app/Domains/FiestaFutbolInfantil/Controllers/CategoryVisibilityController.php new file mode 100644 index 0000000..26d6f7f --- /dev/null +++ b/app/Domains/FiestaFutbolInfantil/Controllers/CategoryVisibilityController.php @@ -0,0 +1,49 @@ +category($request); + $tenant = $request->user()->tenant()->firstOrFail(); + + return response()->json([ + 'data' => [ + 'is_enabled' => $this->visibilityService->isEnabled($tenant, $category), + ], + ]); + } + + public function update(UpdateCategoryVisibilityRequest $request): JsonResponse + { + $category = $this->category($request); + $tenant = $request->user()->tenant()->firstOrFail(); + $model = $this->visibilityService->update( + $tenant, + $category, + $request->boolean('is_enabled'), + ); + + return response()->json([ + 'data' => [ + 'is_enabled' => $model->is_enabled, + ], + ]); + } + + private function category(Request $request): FiestaCategory + { + return FiestaCategory::from((string) $request->route('category')); + } +} diff --git a/app/Domains/FiestaFutbolInfantil/Enums/FiestaCategory.php b/app/Domains/FiestaFutbolInfantil/Enums/FiestaCategory.php new file mode 100644 index 0000000..652e4a8 --- /dev/null +++ b/app/Domains/FiestaFutbolInfantil/Enums/FiestaCategory.php @@ -0,0 +1,21 @@ + 'Entradas', + self::Foods => 'Comidas', + self::Accommodations => 'Alojamientos', + self::Merchandise => 'Merchandising', + }; + } +} diff --git a/app/Domains/FiestaFutbolInfantil/Requests/UpdateCategoryVisibilityRequest.php b/app/Domains/FiestaFutbolInfantil/Requests/UpdateCategoryVisibilityRequest.php new file mode 100644 index 0000000..49def22 --- /dev/null +++ b/app/Domains/FiestaFutbolInfantil/Requests/UpdateCategoryVisibilityRequest.php @@ -0,0 +1,16 @@ + */ + public function rules(): array + { + return [ + 'is_enabled' => ['required', 'boolean'], + ]; + } +} diff --git a/app/Domains/FiestaFutbolInfantil/Services/CategoryVisibilityService.php b/app/Domains/FiestaFutbolInfantil/Services/CategoryVisibilityService.php new file mode 100644 index 0000000..d1fb4d5 --- /dev/null +++ b/app/Domains/FiestaFutbolInfantil/Services/CategoryVisibilityService.php @@ -0,0 +1,29 @@ +where('tenant_code', $tenant->codigo) + ->where('nombre', $category->categoryName()) + ->value('is_enabled') ?? true; + } + + public function update(Tenant $tenant, FiestaCategory $category, bool $isEnabled): Category + { + $model = Category::query()->firstOrCreate([ + 'tenant_code' => $tenant->codigo, + 'nombre' => $category->categoryName(), + ]); + $model->update(['is_enabled' => $isEnabled]); + + return $model; + } +} diff --git a/app/Domains/FiestaFutbolInfantil/routes/api.php b/app/Domains/FiestaFutbolInfantil/routes/api.php index 86065e8..39d4a47 100644 --- a/app/Domains/FiestaFutbolInfantil/routes/api.php +++ b/app/Domains/FiestaFutbolInfantil/routes/api.php @@ -1,6 +1,7 @@ middleware(['auth:sanctum', 'adminapp.tenant']) ->group(function (): void { + $visibilityRoutes = static function (string $endpoint, string $category, string $menuCode): void { + Route::get("{$endpoint}/visibility", [CategoryVisibilityController::class, 'show']) + ->defaults('category', $category) + ->middleware("tenant.menu:{$menuCode}"); + Route::patch("{$endpoint}/visibility", [CategoryVisibilityController::class, 'update']) + ->defaults('category', $category) + ->middleware("tenant.menu:{$menuCode}"); + }; + + $visibilityRoutes('entries', 'entries', 'adminapp.fiesta-futbol-infantil.entradas'); + $visibilityRoutes('foods', 'foods', 'adminapp.fiesta-futbol-infantil.comida'); + $visibilityRoutes('accommodations', 'accommodations', 'adminapp.fiesta-futbol-infantil.alojamientos'); + $visibilityRoutes('merchandise', 'merchandise', 'adminapp.fiesta-futbol-infantil.merchandising'); + Route::get('entries', [EntryController::class, 'index']) ->middleware('tenant.menu:adminapp.fiesta-futbol-infantil.entradas') ->name('adminapp.fiesta-futbol-infantil.entries.index'); diff --git a/tests/Feature/FiestaFutbolInfantil/EntryControllerTest.php b/tests/Feature/FiestaFutbolInfantil/EntryControllerTest.php index 3a66f36..4cc9a7b 100644 --- a/tests/Feature/FiestaFutbolInfantil/EntryControllerTest.php +++ b/tests/Feature/FiestaFutbolInfantil/EntryControllerTest.php @@ -38,6 +38,45 @@ class EntryControllerTest extends TestCase ->assertUnauthorized(); } + public function test_category_visibility_can_be_read_and_updated_for_every_fiesta_menu(): void + { + $tenant = $this->createFiestaTenant(); + $categories = [ + 'entries' => ['Entradas', 'adminapp.fiesta-futbol-infantil.entradas'], + 'foods' => ['Comidas', 'adminapp.fiesta-futbol-infantil.comida'], + 'accommodations' => ['Alojamientos', 'adminapp.fiesta-futbol-infantil.alojamientos'], + 'merchandise' => ['Merchandising', 'adminapp.fiesta-futbol-infantil.merchandising'], + ]; + + foreach ($categories as [$label, $menuCode]) { + $menu = Menu::query()->firstOrCreate( + ['code' => $menuCode], + ['label' => $label, 'route' => '/admin'], + ); + $tenant->menues()->syncWithoutDetaching([$menu->code]); + } + + Sanctum::actingAs($this->createAdminAppUser($tenant)); + + foreach ($categories as $endpoint => [$categoryName]) { + $this->getJson("/api/v1/adminapp/tenant/{$endpoint}/visibility") + ->assertOk() + ->assertJsonPath('data.is_enabled', true); + + $this->patchJson("/api/v1/adminapp/tenant/{$endpoint}/visibility", [ + 'is_enabled' => false, + ]) + ->assertOk() + ->assertJsonPath('data.is_enabled', false); + + $this->assertDatabaseHas('categorias', [ + 'tenant_code' => $tenant->codigo, + 'nombre' => $categoryName, + 'is_enabled' => false, + ]); + } + } + public function test_it_creates_multiple_entries_with_dates_and_tracked_inventory(): void { $tenant = $this->createFiestaTenant(); From 21cfbe2a4630626601e74b6ca22ae357f8e24ec4 Mon Sep 17 00:00:00 2001 From: ncoronel Date: Mon, 24 Aug 2026 12:35:20 -0300 Subject: [PATCH 03/10] feat(purchase): add filtering for multiple statuses in purchase index and order by status then date --- .../Controllers/PurchaseController.php | 15 ++++-- tests/Feature/Purchase/StorePurchaseTest.php | 46 +++++++++++++++++++ 2 files changed, 58 insertions(+), 3 deletions(-) diff --git a/app/Domains/Purchase/Controllers/PurchaseController.php b/app/Domains/Purchase/Controllers/PurchaseController.php index 483b73f..5db012f 100644 --- a/app/Domains/Purchase/Controllers/PurchaseController.php +++ b/app/Domains/Purchase/Controllers/PurchaseController.php @@ -24,13 +24,22 @@ class PurchaseController extends Controller { public function index(Request $request, Tenant $tenant): JsonResponse { + $statusParam = $request->query('status'); + $statuses = is_string($statusParam) + ? collect(explode(',', $statusParam)) + ->map(fn (string $status): string => trim($status)) + ->filter() + ->unique() + ->values() + ->all() + : []; + return PurchaseResource::collection( Purchase::query() ->where('tenant_codigo', $tenant->codigo) ->where('user_id', $request->user()->id) - ->when($request->query('status'), function ($query, $status) { - $query->where('status', $status); - }) + ->when($statuses !== [], fn ($query) => $query->whereIn('status', $statuses)) + ->orderBy('status') ->latest() ->paginateFromRequest() )->response(); diff --git a/tests/Feature/Purchase/StorePurchaseTest.php b/tests/Feature/Purchase/StorePurchaseTest.php index 9fda55a..91f521a 100644 --- a/tests/Feature/Purchase/StorePurchaseTest.php +++ b/tests/Feature/Purchase/StorePurchaseTest.php @@ -1230,6 +1230,52 @@ class StorePurchaseTest extends TestCase ->assertJsonPath('data.0.total', '100.00'); } + public function test_purchase_index_filters_multiple_comma_separated_statuses_and_orders_by_status_ascending_then_date(): void + { + $this->createTenant('sonder', 'Sonder', 'sonder.com.ar'); + $user = User::factory()->create(); + + $olderPaid = Purchase::query()->create([ + 'tenant_codigo' => 'sonder', + 'user_id' => $user->id, + 'status' => Purchase::STATUS_PAID, + 'total' => '10.00', + ]); + $newerInReview = Purchase::query()->create([ + 'tenant_codigo' => 'sonder', + 'user_id' => $user->id, + 'status' => Purchase::STATUS_IN_REVIEW, + 'total' => '20.00', + ]); + $newerPaid = Purchase::query()->create([ + 'tenant_codigo' => 'sonder', + 'user_id' => $user->id, + 'status' => Purchase::STATUS_PAID, + 'total' => '30.00', + ]); + Purchase::query()->create([ + 'tenant_codigo' => 'sonder', + 'user_id' => $user->id, + 'status' => Purchase::STATUS_CANCELLED, + 'total' => '40.00', + ]); + + $olderPaid->forceFill(['created_at' => now()->subDays(3)])->saveQuietly(); + $newerInReview->forceFill(['created_at' => now()])->saveQuietly(); + $newerPaid->forceFill(['created_at' => now()->subDay()])->saveQuietly(); + + $this->actingAs($user, 'sanctum') + ->getJson('/api/tenants/sonder/compras?status=paid,%20in_review,paid') + ->assertOk() + ->assertJsonCount(3, 'data') + ->assertJsonPath('data.0.id', $newerInReview->id) + ->assertJsonPath('data.0.status', Purchase::STATUS_IN_REVIEW) + ->assertJsonPath('data.1.id', $newerPaid->id) + ->assertJsonPath('data.1.status', Purchase::STATUS_PAID) + ->assertJsonPath('data.2.id', $olderPaid->id) + ->assertJsonPath('data.2.status', Purchase::STATUS_PAID); + } + public function test_it_rejects_a_cart_from_another_user(): void { $this->createTenant('sonder', 'Sonder', 'sonder.com.ar'); From d1c4acc7ae84e5e5aafe7b1f4c6a15e723a44b47 Mon Sep 17 00:00:00 2001 From: ncoronel Date: Mon, 24 Aug 2026 12:44:37 -0300 Subject: [PATCH 04/10] feat(category-visibility): add success messages for category visibility updates --- .../CategoryVisibilityController.php | 3 +++ .../EntryControllerTest.php | 18 ++++++++++++++++-- 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/app/Domains/FiestaFutbolInfantil/Controllers/CategoryVisibilityController.php b/app/Domains/FiestaFutbolInfantil/Controllers/CategoryVisibilityController.php index 26d6f7f..c67aef4 100644 --- a/app/Domains/FiestaFutbolInfantil/Controllers/CategoryVisibilityController.php +++ b/app/Domains/FiestaFutbolInfantil/Controllers/CategoryVisibilityController.php @@ -39,6 +39,9 @@ class CategoryVisibilityController extends Controller 'data' => [ 'is_enabled' => $model->is_enabled, ], + 'message' => $model->is_enabled + ? 'Mostrar en sitio web se activo correctamente' + : 'Mostrar en sitio web se desactivo correctamente', ]); } diff --git a/tests/Feature/FiestaFutbolInfantil/EntryControllerTest.php b/tests/Feature/FiestaFutbolInfantil/EntryControllerTest.php index 4cc9a7b..9b084f4 100644 --- a/tests/Feature/FiestaFutbolInfantil/EntryControllerTest.php +++ b/tests/Feature/FiestaFutbolInfantil/EntryControllerTest.php @@ -67,12 +67,26 @@ class EntryControllerTest extends TestCase 'is_enabled' => false, ]) ->assertOk() - ->assertJsonPath('data.is_enabled', false); + ->assertJsonPath('data.is_enabled', false) + ->assertJsonPath( + 'message', + 'Mostrar en sitio web se desactivo correctamente', + ); + + $this->patchJson("/api/v1/adminapp/tenant/{$endpoint}/visibility", [ + 'is_enabled' => true, + ]) + ->assertOk() + ->assertJsonPath('data.is_enabled', true) + ->assertJsonPath( + 'message', + 'Mostrar en sitio web se activo correctamente', + ); $this->assertDatabaseHas('categorias', [ 'tenant_code' => $tenant->codigo, 'nombre' => $categoryName, - 'is_enabled' => false, + 'is_enabled' => true, ]); } } From c69ebe5f7c6f939c2b35d147f31d4845bcb93929 Mon Sep 17 00:00:00 2001 From: ncoronel Date: Mon, 24 Aug 2026 15:59:00 -0300 Subject: [PATCH 05/10] feat(sale-modification): add changed_at field serialization to ISO 8601 format --- .../AdminApp/SaleModificationResource.php | 1 + .../Sale/SaleModificationResourceTest.php | 33 +++++++++++++++++++ 2 files changed, 34 insertions(+) create mode 100644 tests/Unit/Sale/SaleModificationResourceTest.php diff --git a/app/Domains/Sale/Resources/AdminApp/SaleModificationResource.php b/app/Domains/Sale/Resources/AdminApp/SaleModificationResource.php index c842c0a..8a9a651 100644 --- a/app/Domains/Sale/Resources/AdminApp/SaleModificationResource.php +++ b/app/Domains/Sale/Resources/AdminApp/SaleModificationResource.php @@ -23,6 +23,7 @@ class SaleModificationResource extends JsonResource 'attribute' => $this->attribute, 'old_value' => $this->old_value, 'new_value' => $this->new_value, + 'changed_at' => $this->changed_at->utc()->toIso8601String(), 'date' => $this->changed_at->format('Y-m-d'), 'time' => $this->changed_at->format('H:i:s'), 'actor_type' => $this->actor_type->value, diff --git a/tests/Unit/Sale/SaleModificationResourceTest.php b/tests/Unit/Sale/SaleModificationResourceTest.php new file mode 100644 index 0000000..1fe1386 --- /dev/null +++ b/tests/Unit/Sale/SaleModificationResourceTest.php @@ -0,0 +1,33 @@ +forceFill([ + 'id' => 1, + 'trackable_id' => 33, + 'attribute' => 'status', + 'old_value' => 'pending_payment', + 'new_value' => 'cancelled', + 'changed_at' => CarbonImmutable::parse('2026-08-24 17:53:00', 'UTC'), + 'actor_type' => 'system', + ]); + $modification->setRelation('trackable', null); + $modification->setRelation('user', null); + + $data = (new SaleModificationResource($modification))->resolve(Request::create('/')); + + $this->assertSame('2026-08-24T17:53:00+00:00', $data['changed_at']); + $this->assertSame('2026-08-24', $data['date']); + $this->assertSame('17:53:00', $data['time']); + } +} From 24e000b423958644d4c532d8d8c2d707f6db01fc Mon Sep 17 00:00:00 2001 From: ncoronel Date: Mon, 24 Aug 2026 10:49:14 -0300 Subject: [PATCH 06/10] feat(catalog): add is_enabled attribute to categories and filter items accordingly --- app/Domains/Catalog/Models/Category.php | 6 +++ .../Catalog/Services/FeaturedGroupService.php | 9 ++++ ...000_add_is_enabled_to_categorias_table.php | 22 +++++++++ .../Feature/Catalog/CatalogControllerTest.php | 47 +++++++++++++++++++ tests/Feature/Catalog/CatalogSchemaTest.php | 16 +++++++ 5 files changed, 100 insertions(+) create mode 100644 database/migrations/2026_08_24_000000_add_is_enabled_to_categorias_table.php diff --git a/app/Domains/Catalog/Models/Category.php b/app/Domains/Catalog/Models/Category.php index ee683ba..c0ed114 100644 --- a/app/Domains/Catalog/Models/Category.php +++ b/app/Domains/Catalog/Models/Category.php @@ -15,6 +15,7 @@ use Illuminate\Database\Eloquent\Relations\HasMany; 'tenant_code', 'categoria_id', 'nombre', + 'is_enabled', ])] class Category extends Model { @@ -22,6 +23,10 @@ class Category extends Model protected $table = 'categorias'; + protected $attributes = [ + 'is_enabled' => true, + ]; + /** * @return array */ @@ -29,6 +34,7 @@ class Category extends Model { return [ 'categoria_id' => 'integer', + 'is_enabled' => 'boolean', ]; } diff --git a/app/Domains/Catalog/Services/FeaturedGroupService.php b/app/Domains/Catalog/Services/FeaturedGroupService.php index f01b679..0302a1f 100644 --- a/app/Domains/Catalog/Services/FeaturedGroupService.php +++ b/app/Domains/Catalog/Services/FeaturedGroupService.php @@ -49,6 +49,15 @@ class FeaturedGroupService { $query = CatalogItem::query() ->where('catalog_items.tenant_code', $featuredGroup->tenant_code) + ->where(function (Builder $query): void { + $query + ->whereDoesntHave('category') + ->orWhereHas( + 'category', + fn (Builder $categoryQuery): Builder => $categoryQuery + ->where('is_enabled', true), + ); + }) ->with([ 'inventory', 'attachments', diff --git a/database/migrations/2026_08_24_000000_add_is_enabled_to_categorias_table.php b/database/migrations/2026_08_24_000000_add_is_enabled_to_categorias_table.php new file mode 100644 index 0000000..e61c5b0 --- /dev/null +++ b/database/migrations/2026_08_24_000000_add_is_enabled_to_categorias_table.php @@ -0,0 +1,22 @@ +boolean('is_enabled')->default(true)->after('nombre'); + }); + } + + public function down(): void + { + Schema::table('categorias', function (Blueprint $table): void { + $table->dropColumn('is_enabled'); + }); + } +}; diff --git a/tests/Feature/Catalog/CatalogControllerTest.php b/tests/Feature/Catalog/CatalogControllerTest.php index 63f1051..cdc98d2 100644 --- a/tests/Feature/Catalog/CatalogControllerTest.php +++ b/tests/Feature/Catalog/CatalogControllerTest.php @@ -367,6 +367,53 @@ class CatalogControllerTest extends TestCase ->assertJsonPath('1.items.meta.total', 2); } + public function test_index_excludes_items_from_disabled_categories(): void + { + $tenant = $this->createTenant('catalog-disabled-category'); + $group = $this->createGroup( + $tenant, + ProductLayout::Row, + 'All products', + groupLayout: GroupLayout::Simple, + ); + $enabledCategory = Category::query()->create([ + 'tenant_code' => $tenant->codigo, + 'nombre' => 'Enabled', + ]); + $disabledCategory = Category::query()->create([ + 'tenant_code' => $tenant->codigo, + 'nombre' => 'Disabled', + 'is_enabled' => false, + ]); + + $enabledItem = $this->createItem($tenant, 'Enabled item'); + $enabledItem->category()->associate($enabledCategory)->save(); + $disabledItem = $this->createItem($tenant, 'Disabled item'); + $disabledItem->category()->associate($disabledCategory)->save(); + $uncategorizedItem = $this->createItem($tenant, 'Uncategorized item'); + + foreach ([$enabledItem, $disabledItem, $uncategorizedItem] as $order => $item) { + $group->featuredItems()->create([ + 'catalog_item_id' => $item->id, + 'order' => $order, + ]); + } + + $this->getJson("/api/tenants/{$tenant->codigo}/catalog") + ->assertOk() + ->assertJsonCount(2, '0.items') + ->assertJsonPath('0.items.0.nombre', 'Enabled item') + ->assertJsonPath('0.items.1.nombre', 'Uncategorized item') + ->assertJsonMissing(['nombre' => 'Disabled item']); + + $this->getJson( + "/api/tenants/{$tenant->codigo}/catalog/featured-groups/{$group->id}/items" + ) + ->assertOk() + ->assertJsonCount(2) + ->assertJsonMissing(['nombre' => 'Disabled item']); + } + private function createGroup( Tenant $tenant, ProductLayout $layout, diff --git a/tests/Feature/Catalog/CatalogSchemaTest.php b/tests/Feature/Catalog/CatalogSchemaTest.php index 705bfc7..43147b4 100644 --- a/tests/Feature/Catalog/CatalogSchemaTest.php +++ b/tests/Feature/Catalog/CatalogSchemaTest.php @@ -5,6 +5,7 @@ namespace Tests\Feature\Catalog; use App\Domains\Attachable\Enums\AttachmentType; use App\Domains\Attachable\Models\Attachment; use App\Domains\Catalog\Models\CatalogItem; +use App\Domains\Catalog\Models\Category; use App\Domains\Catalog\Models\Inventory; use App\Domains\Tenant\Models\Tenant; use Illuminate\Foundation\Testing\RefreshDatabase; @@ -15,6 +16,21 @@ class CatalogSchemaTest extends TestCase { use RefreshDatabase; + public function test_categories_are_enabled_by_default(): void + { + $this->assertTrue(Schema::hasColumn('categorias', 'is_enabled')); + + $category = Category::query()->create([ + 'nombre' => 'Enabled category', + ]); + + $this->assertTrue($category->is_enabled); + $this->assertDatabaseHas('categorias', [ + 'id' => $category->id, + 'is_enabled' => true, + ]); + } + public function test_legacy_product_tables_are_replaced_by_catalog_tables(): void { $this->assertFalse(Schema::hasTable('productos')); From 6ff91190862e01908a51fa3d78b49fdc277bd3a1 Mon Sep 17 00:00:00 2001 From: ncoronel Date: Mon, 24 Aug 2026 11:31:26 -0300 Subject: [PATCH 07/10] feat(category-visibility): implement category visibility controller and service with update functionality --- .../CategoryVisibilityController.php | 49 +++++++++++++++++++ .../Enums/FiestaCategory.php | 21 ++++++++ .../UpdateCategoryVisibilityRequest.php | 16 ++++++ .../Services/CategoryVisibilityService.php | 29 +++++++++++ .../FiestaFutbolInfantil/routes/api.php | 15 ++++++ .../EntryControllerTest.php | 39 +++++++++++++++ 6 files changed, 169 insertions(+) create mode 100644 app/Domains/FiestaFutbolInfantil/Controllers/CategoryVisibilityController.php create mode 100644 app/Domains/FiestaFutbolInfantil/Enums/FiestaCategory.php create mode 100644 app/Domains/FiestaFutbolInfantil/Requests/UpdateCategoryVisibilityRequest.php create mode 100644 app/Domains/FiestaFutbolInfantil/Services/CategoryVisibilityService.php diff --git a/app/Domains/FiestaFutbolInfantil/Controllers/CategoryVisibilityController.php b/app/Domains/FiestaFutbolInfantil/Controllers/CategoryVisibilityController.php new file mode 100644 index 0000000..26d6f7f --- /dev/null +++ b/app/Domains/FiestaFutbolInfantil/Controllers/CategoryVisibilityController.php @@ -0,0 +1,49 @@ +category($request); + $tenant = $request->user()->tenant()->firstOrFail(); + + return response()->json([ + 'data' => [ + 'is_enabled' => $this->visibilityService->isEnabled($tenant, $category), + ], + ]); + } + + public function update(UpdateCategoryVisibilityRequest $request): JsonResponse + { + $category = $this->category($request); + $tenant = $request->user()->tenant()->firstOrFail(); + $model = $this->visibilityService->update( + $tenant, + $category, + $request->boolean('is_enabled'), + ); + + return response()->json([ + 'data' => [ + 'is_enabled' => $model->is_enabled, + ], + ]); + } + + private function category(Request $request): FiestaCategory + { + return FiestaCategory::from((string) $request->route('category')); + } +} diff --git a/app/Domains/FiestaFutbolInfantil/Enums/FiestaCategory.php b/app/Domains/FiestaFutbolInfantil/Enums/FiestaCategory.php new file mode 100644 index 0000000..652e4a8 --- /dev/null +++ b/app/Domains/FiestaFutbolInfantil/Enums/FiestaCategory.php @@ -0,0 +1,21 @@ + 'Entradas', + self::Foods => 'Comidas', + self::Accommodations => 'Alojamientos', + self::Merchandise => 'Merchandising', + }; + } +} diff --git a/app/Domains/FiestaFutbolInfantil/Requests/UpdateCategoryVisibilityRequest.php b/app/Domains/FiestaFutbolInfantil/Requests/UpdateCategoryVisibilityRequest.php new file mode 100644 index 0000000..49def22 --- /dev/null +++ b/app/Domains/FiestaFutbolInfantil/Requests/UpdateCategoryVisibilityRequest.php @@ -0,0 +1,16 @@ + */ + public function rules(): array + { + return [ + 'is_enabled' => ['required', 'boolean'], + ]; + } +} diff --git a/app/Domains/FiestaFutbolInfantil/Services/CategoryVisibilityService.php b/app/Domains/FiestaFutbolInfantil/Services/CategoryVisibilityService.php new file mode 100644 index 0000000..d1fb4d5 --- /dev/null +++ b/app/Domains/FiestaFutbolInfantil/Services/CategoryVisibilityService.php @@ -0,0 +1,29 @@ +where('tenant_code', $tenant->codigo) + ->where('nombre', $category->categoryName()) + ->value('is_enabled') ?? true; + } + + public function update(Tenant $tenant, FiestaCategory $category, bool $isEnabled): Category + { + $model = Category::query()->firstOrCreate([ + 'tenant_code' => $tenant->codigo, + 'nombre' => $category->categoryName(), + ]); + $model->update(['is_enabled' => $isEnabled]); + + return $model; + } +} diff --git a/app/Domains/FiestaFutbolInfantil/routes/api.php b/app/Domains/FiestaFutbolInfantil/routes/api.php index 86065e8..39d4a47 100644 --- a/app/Domains/FiestaFutbolInfantil/routes/api.php +++ b/app/Domains/FiestaFutbolInfantil/routes/api.php @@ -1,6 +1,7 @@ middleware(['auth:sanctum', 'adminapp.tenant']) ->group(function (): void { + $visibilityRoutes = static function (string $endpoint, string $category, string $menuCode): void { + Route::get("{$endpoint}/visibility", [CategoryVisibilityController::class, 'show']) + ->defaults('category', $category) + ->middleware("tenant.menu:{$menuCode}"); + Route::patch("{$endpoint}/visibility", [CategoryVisibilityController::class, 'update']) + ->defaults('category', $category) + ->middleware("tenant.menu:{$menuCode}"); + }; + + $visibilityRoutes('entries', 'entries', 'adminapp.fiesta-futbol-infantil.entradas'); + $visibilityRoutes('foods', 'foods', 'adminapp.fiesta-futbol-infantil.comida'); + $visibilityRoutes('accommodations', 'accommodations', 'adminapp.fiesta-futbol-infantil.alojamientos'); + $visibilityRoutes('merchandise', 'merchandise', 'adminapp.fiesta-futbol-infantil.merchandising'); + Route::get('entries', [EntryController::class, 'index']) ->middleware('tenant.menu:adminapp.fiesta-futbol-infantil.entradas') ->name('adminapp.fiesta-futbol-infantil.entries.index'); diff --git a/tests/Feature/FiestaFutbolInfantil/EntryControllerTest.php b/tests/Feature/FiestaFutbolInfantil/EntryControllerTest.php index 3a66f36..4cc9a7b 100644 --- a/tests/Feature/FiestaFutbolInfantil/EntryControllerTest.php +++ b/tests/Feature/FiestaFutbolInfantil/EntryControllerTest.php @@ -38,6 +38,45 @@ class EntryControllerTest extends TestCase ->assertUnauthorized(); } + public function test_category_visibility_can_be_read_and_updated_for_every_fiesta_menu(): void + { + $tenant = $this->createFiestaTenant(); + $categories = [ + 'entries' => ['Entradas', 'adminapp.fiesta-futbol-infantil.entradas'], + 'foods' => ['Comidas', 'adminapp.fiesta-futbol-infantil.comida'], + 'accommodations' => ['Alojamientos', 'adminapp.fiesta-futbol-infantil.alojamientos'], + 'merchandise' => ['Merchandising', 'adminapp.fiesta-futbol-infantil.merchandising'], + ]; + + foreach ($categories as [$label, $menuCode]) { + $menu = Menu::query()->firstOrCreate( + ['code' => $menuCode], + ['label' => $label, 'route' => '/admin'], + ); + $tenant->menues()->syncWithoutDetaching([$menu->code]); + } + + Sanctum::actingAs($this->createAdminAppUser($tenant)); + + foreach ($categories as $endpoint => [$categoryName]) { + $this->getJson("/api/v1/adminapp/tenant/{$endpoint}/visibility") + ->assertOk() + ->assertJsonPath('data.is_enabled', true); + + $this->patchJson("/api/v1/adminapp/tenant/{$endpoint}/visibility", [ + 'is_enabled' => false, + ]) + ->assertOk() + ->assertJsonPath('data.is_enabled', false); + + $this->assertDatabaseHas('categorias', [ + 'tenant_code' => $tenant->codigo, + 'nombre' => $categoryName, + 'is_enabled' => false, + ]); + } + } + public function test_it_creates_multiple_entries_with_dates_and_tracked_inventory(): void { $tenant = $this->createFiestaTenant(); From 53d1415b1895f693faedbf4ae19e1341df8ecdae Mon Sep 17 00:00:00 2001 From: ncoronel Date: Mon, 24 Aug 2026 12:35:20 -0300 Subject: [PATCH 08/10] feat(purchase): add filtering for multiple statuses in purchase index and order by status then date --- .../Controllers/PurchaseController.php | 15 ++++-- tests/Feature/Purchase/StorePurchaseTest.php | 46 +++++++++++++++++++ 2 files changed, 58 insertions(+), 3 deletions(-) diff --git a/app/Domains/Purchase/Controllers/PurchaseController.php b/app/Domains/Purchase/Controllers/PurchaseController.php index 483b73f..5db012f 100644 --- a/app/Domains/Purchase/Controllers/PurchaseController.php +++ b/app/Domains/Purchase/Controllers/PurchaseController.php @@ -24,13 +24,22 @@ class PurchaseController extends Controller { public function index(Request $request, Tenant $tenant): JsonResponse { + $statusParam = $request->query('status'); + $statuses = is_string($statusParam) + ? collect(explode(',', $statusParam)) + ->map(fn (string $status): string => trim($status)) + ->filter() + ->unique() + ->values() + ->all() + : []; + return PurchaseResource::collection( Purchase::query() ->where('tenant_codigo', $tenant->codigo) ->where('user_id', $request->user()->id) - ->when($request->query('status'), function ($query, $status) { - $query->where('status', $status); - }) + ->when($statuses !== [], fn ($query) => $query->whereIn('status', $statuses)) + ->orderBy('status') ->latest() ->paginateFromRequest() )->response(); diff --git a/tests/Feature/Purchase/StorePurchaseTest.php b/tests/Feature/Purchase/StorePurchaseTest.php index 9fda55a..91f521a 100644 --- a/tests/Feature/Purchase/StorePurchaseTest.php +++ b/tests/Feature/Purchase/StorePurchaseTest.php @@ -1230,6 +1230,52 @@ class StorePurchaseTest extends TestCase ->assertJsonPath('data.0.total', '100.00'); } + public function test_purchase_index_filters_multiple_comma_separated_statuses_and_orders_by_status_ascending_then_date(): void + { + $this->createTenant('sonder', 'Sonder', 'sonder.com.ar'); + $user = User::factory()->create(); + + $olderPaid = Purchase::query()->create([ + 'tenant_codigo' => 'sonder', + 'user_id' => $user->id, + 'status' => Purchase::STATUS_PAID, + 'total' => '10.00', + ]); + $newerInReview = Purchase::query()->create([ + 'tenant_codigo' => 'sonder', + 'user_id' => $user->id, + 'status' => Purchase::STATUS_IN_REVIEW, + 'total' => '20.00', + ]); + $newerPaid = Purchase::query()->create([ + 'tenant_codigo' => 'sonder', + 'user_id' => $user->id, + 'status' => Purchase::STATUS_PAID, + 'total' => '30.00', + ]); + Purchase::query()->create([ + 'tenant_codigo' => 'sonder', + 'user_id' => $user->id, + 'status' => Purchase::STATUS_CANCELLED, + 'total' => '40.00', + ]); + + $olderPaid->forceFill(['created_at' => now()->subDays(3)])->saveQuietly(); + $newerInReview->forceFill(['created_at' => now()])->saveQuietly(); + $newerPaid->forceFill(['created_at' => now()->subDay()])->saveQuietly(); + + $this->actingAs($user, 'sanctum') + ->getJson('/api/tenants/sonder/compras?status=paid,%20in_review,paid') + ->assertOk() + ->assertJsonCount(3, 'data') + ->assertJsonPath('data.0.id', $newerInReview->id) + ->assertJsonPath('data.0.status', Purchase::STATUS_IN_REVIEW) + ->assertJsonPath('data.1.id', $newerPaid->id) + ->assertJsonPath('data.1.status', Purchase::STATUS_PAID) + ->assertJsonPath('data.2.id', $olderPaid->id) + ->assertJsonPath('data.2.status', Purchase::STATUS_PAID); + } + public function test_it_rejects_a_cart_from_another_user(): void { $this->createTenant('sonder', 'Sonder', 'sonder.com.ar'); From f8b8bab4747d668d10773d0016dd335881addb49 Mon Sep 17 00:00:00 2001 From: ncoronel Date: Mon, 24 Aug 2026 12:44:37 -0300 Subject: [PATCH 09/10] feat(category-visibility): add success messages for category visibility updates --- .../CategoryVisibilityController.php | 3 +++ .../EntryControllerTest.php | 18 ++++++++++++++++-- 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/app/Domains/FiestaFutbolInfantil/Controllers/CategoryVisibilityController.php b/app/Domains/FiestaFutbolInfantil/Controllers/CategoryVisibilityController.php index 26d6f7f..c67aef4 100644 --- a/app/Domains/FiestaFutbolInfantil/Controllers/CategoryVisibilityController.php +++ b/app/Domains/FiestaFutbolInfantil/Controllers/CategoryVisibilityController.php @@ -39,6 +39,9 @@ class CategoryVisibilityController extends Controller 'data' => [ 'is_enabled' => $model->is_enabled, ], + 'message' => $model->is_enabled + ? 'Mostrar en sitio web se activo correctamente' + : 'Mostrar en sitio web se desactivo correctamente', ]); } diff --git a/tests/Feature/FiestaFutbolInfantil/EntryControllerTest.php b/tests/Feature/FiestaFutbolInfantil/EntryControllerTest.php index 4cc9a7b..9b084f4 100644 --- a/tests/Feature/FiestaFutbolInfantil/EntryControllerTest.php +++ b/tests/Feature/FiestaFutbolInfantil/EntryControllerTest.php @@ -67,12 +67,26 @@ class EntryControllerTest extends TestCase 'is_enabled' => false, ]) ->assertOk() - ->assertJsonPath('data.is_enabled', false); + ->assertJsonPath('data.is_enabled', false) + ->assertJsonPath( + 'message', + 'Mostrar en sitio web se desactivo correctamente', + ); + + $this->patchJson("/api/v1/adminapp/tenant/{$endpoint}/visibility", [ + 'is_enabled' => true, + ]) + ->assertOk() + ->assertJsonPath('data.is_enabled', true) + ->assertJsonPath( + 'message', + 'Mostrar en sitio web se activo correctamente', + ); $this->assertDatabaseHas('categorias', [ 'tenant_code' => $tenant->codigo, 'nombre' => $categoryName, - 'is_enabled' => false, + 'is_enabled' => true, ]); } } From 8d01620fa8e8d268a5d0f715ee6562616b1a8928 Mon Sep 17 00:00:00 2001 From: ncoronel Date: Mon, 24 Aug 2026 15:59:00 -0300 Subject: [PATCH 10/10] feat(sale-modification): add changed_at field serialization to ISO 8601 format --- .../AdminApp/SaleModificationResource.php | 1 + .../Sale/SaleModificationResourceTest.php | 33 +++++++++++++++++++ 2 files changed, 34 insertions(+) create mode 100644 tests/Unit/Sale/SaleModificationResourceTest.php diff --git a/app/Domains/Sale/Resources/AdminApp/SaleModificationResource.php b/app/Domains/Sale/Resources/AdminApp/SaleModificationResource.php index c842c0a..8a9a651 100644 --- a/app/Domains/Sale/Resources/AdminApp/SaleModificationResource.php +++ b/app/Domains/Sale/Resources/AdminApp/SaleModificationResource.php @@ -23,6 +23,7 @@ class SaleModificationResource extends JsonResource 'attribute' => $this->attribute, 'old_value' => $this->old_value, 'new_value' => $this->new_value, + 'changed_at' => $this->changed_at->utc()->toIso8601String(), 'date' => $this->changed_at->format('Y-m-d'), 'time' => $this->changed_at->format('H:i:s'), 'actor_type' => $this->actor_type->value, diff --git a/tests/Unit/Sale/SaleModificationResourceTest.php b/tests/Unit/Sale/SaleModificationResourceTest.php new file mode 100644 index 0000000..1fe1386 --- /dev/null +++ b/tests/Unit/Sale/SaleModificationResourceTest.php @@ -0,0 +1,33 @@ +forceFill([ + 'id' => 1, + 'trackable_id' => 33, + 'attribute' => 'status', + 'old_value' => 'pending_payment', + 'new_value' => 'cancelled', + 'changed_at' => CarbonImmutable::parse('2026-08-24 17:53:00', 'UTC'), + 'actor_type' => 'system', + ]); + $modification->setRelation('trackable', null); + $modification->setRelation('user', null); + + $data = (new SaleModificationResource($modification))->resolve(Request::create('/')); + + $this->assertSame('2026-08-24T17:53:00+00:00', $data['changed_at']); + $this->assertSame('2026-08-24', $data['date']); + $this->assertSame('17:53:00', $data['time']); + } +}