diff --git a/app/Domains/Catalog/Services/CatalogService.php b/app/Domains/Catalog/Services/CatalogService.php index 6d9aaa7..d6919cf 100644 --- a/app/Domains/Catalog/Services/CatalogService.php +++ b/app/Domains/Catalog/Services/CatalogService.php @@ -305,6 +305,39 @@ class CatalogService }); } + public function deleteVariant(Variant $variant): void + { + DB::transaction(function () use ($variant): void { + $variant = Variant::query() + ->with('attachments') + ->lockForUpdate() + ->findOrFail($variant->getKey()); + $catalogItem = CatalogItem::query() + ->lockForUpdate() + ->findOrFail($variant->catalog_item_id); + $attachments = $variant->attachments; + $inventoryId = $variant->inventory_id; + + $variant->attachments()->detach(); + $variant->delete(); + Inventory::query()->whereKey($inventoryId)->delete(); + + $minimumPrice = $catalogItem->variants()->min('precio'); + + if ($minimumPrice === null) { + $this->delete($catalogItem); + } else { + $catalogItem->update(['precio' => $minimumPrice]); + } + + foreach ($attachments as $attachment) { + if (! DB::table('catalog_items_attachments')->where('attachment_id', $attachment->id)->exists()) { + $this->attachmentService->delete($attachment); + } + } + }); + } + private function createInventory(int $realStock): Inventory { return Inventory::query()->create([ diff --git a/app/Domains/FiestaFutbolInfantil/Controllers/AccommodationController.php b/app/Domains/FiestaFutbolInfantil/Controllers/AccommodationController.php index d99b14d..804c320 100644 --- a/app/Domains/FiestaFutbolInfantil/Controllers/AccommodationController.php +++ b/app/Domains/FiestaFutbolInfantil/Controllers/AccommodationController.php @@ -7,6 +7,7 @@ use App\Domains\FiestaFutbolInfantil\Resources\AccommodationResource; use App\Domains\FiestaFutbolInfantil\Services\AccommodationService; use App\Http\Controllers\Controller; use Illuminate\Http\Request; +use Illuminate\Http\Response; class AccommodationController extends Controller { @@ -30,4 +31,14 @@ class AccommodationController extends Controller ) ); } + + public function destroy(Request $request, int $accommodation): Response + { + $this->accommodationService->delete( + $request->user()->tenant()->firstOrFail(), + $accommodation, + ); + + return response()->noContent(); + } } diff --git a/app/Domains/FiestaFutbolInfantil/Controllers/EntryController.php b/app/Domains/FiestaFutbolInfantil/Controllers/EntryController.php index 5fde9ac..9203341 100644 --- a/app/Domains/FiestaFutbolInfantil/Controllers/EntryController.php +++ b/app/Domains/FiestaFutbolInfantil/Controllers/EntryController.php @@ -9,6 +9,7 @@ use App\Http\Controllers\Controller; use Illuminate\Http\JsonResponse; use Illuminate\Http\Request; use Illuminate\Http\Resources\Json\AnonymousResourceCollection; +use Illuminate\Http\Response; class EntryController extends Controller { @@ -34,4 +35,14 @@ class EntryController extends Controller ->response() ->setStatusCode(200); } + + public function destroy(Request $request, int $entry): Response + { + $this->entryService->delete( + $request->user()->tenant()->firstOrFail(), + $entry, + ); + + return response()->noContent(); + } } diff --git a/app/Domains/FiestaFutbolInfantil/Controllers/FoodController.php b/app/Domains/FiestaFutbolInfantil/Controllers/FoodController.php index 396656a..109a836 100644 --- a/app/Domains/FiestaFutbolInfantil/Controllers/FoodController.php +++ b/app/Domains/FiestaFutbolInfantil/Controllers/FoodController.php @@ -7,6 +7,7 @@ use App\Domains\FiestaFutbolInfantil\Resources\FoodResource; use App\Domains\FiestaFutbolInfantil\Services\FoodService; use App\Http\Controllers\Controller; use Illuminate\Http\Request; +use Illuminate\Http\Response; class FoodController extends Controller { @@ -30,4 +31,14 @@ class FoodController extends Controller ) ); } + + public function destroy(Request $request, int $food): Response + { + $this->foodService->delete( + $request->user()->tenant()->firstOrFail(), + $food, + ); + + return response()->noContent(); + } } diff --git a/app/Domains/FiestaFutbolInfantil/Controllers/MerchandiseController.php b/app/Domains/FiestaFutbolInfantil/Controllers/MerchandiseController.php index a55d454..cbce5a5 100644 --- a/app/Domains/FiestaFutbolInfantil/Controllers/MerchandiseController.php +++ b/app/Domains/FiestaFutbolInfantil/Controllers/MerchandiseController.php @@ -9,6 +9,7 @@ use App\Http\Controllers\Controller; use Illuminate\Http\JsonResponse; use Illuminate\Http\Request; use Illuminate\Http\Resources\Json\AnonymousResourceCollection; +use Illuminate\Http\Response; class MerchandiseController extends Controller { @@ -33,4 +34,14 @@ class MerchandiseController extends Controller ->response() ->setStatusCode(200); } + + public function destroy(Request $request, int $merchandise): Response + { + $this->merchandiseService->delete( + $request->user()->tenant()->firstOrFail(), + $merchandise, + ); + + return response()->noContent(); + } } diff --git a/app/Domains/FiestaFutbolInfantil/Services/AccommodationService.php b/app/Domains/FiestaFutbolInfantil/Services/AccommodationService.php index 6aa6c30..accde3e 100644 --- a/app/Domains/FiestaFutbolInfantil/Services/AccommodationService.php +++ b/app/Domains/FiestaFutbolInfantil/Services/AccommodationService.php @@ -11,6 +11,7 @@ use App\Domains\Catalog\Models\Category; use App\Domains\Catalog\Models\Inventory; use App\Domains\Catalog\Models\ItemAttribute; use App\Domains\Catalog\Models\Variant; +use App\Domains\Catalog\Services\CatalogService; use App\Domains\Tenant\Models\Tenant; use Illuminate\Support\Collection; use Illuminate\Support\Facades\DB; @@ -20,6 +21,8 @@ class AccommodationService { private const ATTRIBUTE_CODE = 'tipo_alojamiento'; + public function __construct(private readonly CatalogService $catalogService) {} + public function current(Tenant $tenant): ?CatalogItem { return CatalogItem::query() @@ -86,6 +89,18 @@ class AccommodationService }); } + public function delete(Tenant $tenant, int $accommodationId): void + { + $variant = Variant::query() + ->whereKey($accommodationId) + ->whereHas('catalogItem', fn ($query) => $query + ->where('tenant_code', $tenant->codigo) + ->where('slug', 'alojamiento')) + ->firstOrFail(); + + $this->catalogService->deleteVariant($variant); + } + private function attribute(Tenant $tenant): Attribute { $attribute = Attribute::query() diff --git a/app/Domains/FiestaFutbolInfantil/Services/EntryService.php b/app/Domains/FiestaFutbolInfantil/Services/EntryService.php index f3dae96..a0b4ae2 100644 --- a/app/Domains/FiestaFutbolInfantil/Services/EntryService.php +++ b/app/Domains/FiestaFutbolInfantil/Services/EntryService.php @@ -76,6 +76,18 @@ class EntryService }); } + public function delete(Tenant $tenant, int $entryId): void + { + $entry = CatalogItem::query() + ->whereKey($entryId) + ->where('tenant_code', $tenant->codigo) + ->where('event_product_type', EventProductType::Entry->value) + ->whereHas('category', fn ($query) => $query->where('nombre', 'Entradas')) + ->firstOrFail(); + + $this->catalogService->delete($entry); + } + /** @param array $entry */ private function update(Tenant $tenant, Category $category, array $entry, int $index): CatalogItem { diff --git a/app/Domains/FiestaFutbolInfantil/Services/FoodService.php b/app/Domains/FiestaFutbolInfantil/Services/FoodService.php index ea76d4d..94aac15 100644 --- a/app/Domains/FiestaFutbolInfantil/Services/FoodService.php +++ b/app/Domains/FiestaFutbolInfantil/Services/FoodService.php @@ -11,6 +11,7 @@ use App\Domains\Catalog\Models\Category; use App\Domains\Catalog\Models\Inventory; use App\Domains\Catalog\Models\ItemAttribute; use App\Domains\Catalog\Models\Variant; +use App\Domains\Catalog\Services\CatalogService; use App\Domains\Tenant\Models\Tenant; use Illuminate\Support\Collection; use Illuminate\Support\Facades\DB; @@ -20,6 +21,8 @@ class FoodService { private const ATTRIBUTE_CODES = ['event_date', 'horario', 'servicio']; + public function __construct(private readonly CatalogService $catalogService) {} + public function current(Tenant $tenant): ?CatalogItem { return CatalogItem::query() @@ -87,6 +90,18 @@ class FoodService }); } + public function delete(Tenant $tenant, int $foodId): void + { + $variant = Variant::query() + ->whereKey($foodId) + ->whereHas('catalogItem', fn ($query) => $query + ->where('tenant_code', $tenant->codigo) + ->where('slug', 'comida')) + ->firstOrFail(); + + $this->catalogService->deleteVariant($variant); + } + /** @return Collection */ private function attributes(Tenant $tenant): Collection { diff --git a/app/Domains/FiestaFutbolInfantil/Services/MerchandiseService.php b/app/Domains/FiestaFutbolInfantil/Services/MerchandiseService.php index e2ff079..463b8c7 100644 --- a/app/Domains/FiestaFutbolInfantil/Services/MerchandiseService.php +++ b/app/Domains/FiestaFutbolInfantil/Services/MerchandiseService.php @@ -11,6 +11,7 @@ use App\Domains\Catalog\Models\Category; use App\Domains\Catalog\Models\Inventory; use App\Domains\Catalog\Models\ItemAttribute; use App\Domains\Catalog\Models\Variant; +use App\Domains\Catalog\Services\CatalogService; use App\Domains\Tenant\Models\Tenant; use Illuminate\Support\Collection; use Illuminate\Support\Facades\DB; @@ -21,6 +22,8 @@ class MerchandiseService { private const ATTRIBUTE_CODES = ['color', 'talle']; + public function __construct(private readonly CatalogService $catalogService) {} + /** @return Collection */ public function all(Tenant $tenant): Collection { @@ -120,6 +123,20 @@ class MerchandiseService }); } + public function delete(Tenant $tenant, int $merchandiseId): void + { + $variant = Variant::query() + ->whereKey($merchandiseId) + ->whereHas('catalogItem', fn ($query) => $query + ->where('tenant_code', $tenant->codigo) + ->where('event_product_type', EventProductType::Product->value) + ->whereHas('category', fn ($categoryQuery) => $categoryQuery + ->where('nombre', 'Merchandising'))) + ->firstOrFail(); + + $this->catalogService->deleteVariant($variant); + } + /** @return Collection */ private function attributes(Tenant $tenant): Collection { diff --git a/app/Domains/FiestaFutbolInfantil/routes/api.php b/app/Domains/FiestaFutbolInfantil/routes/api.php index 33138dd..86065e8 100644 --- a/app/Domains/FiestaFutbolInfantil/routes/api.php +++ b/app/Domains/FiestaFutbolInfantil/routes/api.php @@ -15,22 +15,34 @@ Route::prefix('v1/adminapp/tenant') Route::post('entries', [EntryController::class, 'store']) ->middleware('tenant.menu:adminapp.fiesta-futbol-infantil.entradas') ->name('adminapp.fiesta-futbol-infantil.entries.store'); + Route::delete('entries/{entry}', [EntryController::class, 'destroy']) + ->middleware('tenant.menu:adminapp.fiesta-futbol-infantil.entradas') + ->name('adminapp.fiesta-futbol-infantil.entries.destroy'); Route::get('foods', [FoodController::class, 'index']) ->middleware('tenant.menu:adminapp.fiesta-futbol-infantil.comida') ->name('adminapp.fiesta-futbol-infantil.foods.index'); Route::post('foods', [FoodController::class, 'store']) ->middleware('tenant.menu:adminapp.fiesta-futbol-infantil.comida') ->name('adminapp.fiesta-futbol-infantil.foods.store'); + Route::delete('foods/{food}', [FoodController::class, 'destroy']) + ->middleware('tenant.menu:adminapp.fiesta-futbol-infantil.comida') + ->name('adminapp.fiesta-futbol-infantil.foods.destroy'); Route::get('accommodations', [AccommodationController::class, 'index']) ->middleware('tenant.menu:adminapp.fiesta-futbol-infantil.alojamientos') ->name('adminapp.fiesta-futbol-infantil.accommodations.index'); Route::post('accommodations', [AccommodationController::class, 'store']) ->middleware('tenant.menu:adminapp.fiesta-futbol-infantil.alojamientos') ->name('adminapp.fiesta-futbol-infantil.accommodations.store'); + Route::delete('accommodations/{accommodation}', [AccommodationController::class, 'destroy']) + ->middleware('tenant.menu:adminapp.fiesta-futbol-infantil.alojamientos') + ->name('adminapp.fiesta-futbol-infantil.accommodations.destroy'); Route::get('merchandise', [MerchandiseController::class, 'index']) ->middleware('tenant.menu:adminapp.fiesta-futbol-infantil.merchandising') ->name('adminapp.fiesta-futbol-infantil.merchandise.index'); Route::post('merchandise', [MerchandiseController::class, 'store']) ->middleware('tenant.menu:adminapp.fiesta-futbol-infantil.merchandising') ->name('adminapp.fiesta-futbol-infantil.merchandise.store'); + Route::delete('merchandise/{merchandise}', [MerchandiseController::class, 'destroy']) + ->middleware('tenant.menu:adminapp.fiesta-futbol-infantil.merchandising') + ->name('adminapp.fiesta-futbol-infantil.merchandise.destroy'); }); diff --git a/tests/Feature/FiestaFutbolInfantil/AccommodationControllerTest.php b/tests/Feature/FiestaFutbolInfantil/AccommodationControllerTest.php index 0b2ad98..eb86981 100644 --- a/tests/Feature/FiestaFutbolInfantil/AccommodationControllerTest.php +++ b/tests/Feature/FiestaFutbolInfantil/AccommodationControllerTest.php @@ -125,6 +125,25 @@ class AccommodationControllerTest extends TestCase $this->assertDatabaseCount('variantes', 2); } + public function test_it_deletes_an_accommodation_and_its_empty_product(): void + { + [$tenant] = $this->configuredTenant(); + Sanctum::actingAs($this->createAdminAppUser($tenant)); + + $accommodationId = $this->postJson('/api/v1/adminapp/tenant/accommodations', [ + 'variants' => [ + $this->variantPayload('Carpa', null, 100, 35000), + ], + ])->assertOk()->json('data.variants.0.id'); + + $this->deleteJson("/api/v1/adminapp/tenant/accommodations/{$accommodationId}") + ->assertNoContent(); + + $this->assertDatabaseCount('catalog_items', 0); + $this->assertDatabaseCount('variantes', 0); + $this->assertDatabaseCount('inventories', 0); + } + public function test_it_rejects_duplicate_normalized_titles(): void { [$tenant] = $this->configuredTenant(); diff --git a/tests/Feature/FiestaFutbolInfantil/EntryControllerTest.php b/tests/Feature/FiestaFutbolInfantil/EntryControllerTest.php index 4fcb908..3a0322f 100644 --- a/tests/Feature/FiestaFutbolInfantil/EntryControllerTest.php +++ b/tests/Feature/FiestaFutbolInfantil/EntryControllerTest.php @@ -168,6 +168,35 @@ class EntryControllerTest extends TestCase ]); } + public function test_it_deletes_an_entry_and_its_inventory(): void + { + $tenant = $this->createFiestaTenant(); + $this->createEventDateAttribute($tenant); + $eventDate = $tenant->eventDates()->create([ + 'date' => '2026-10-09', + 'time_start' => '00:00', + 'time_end' => '23:59', + ]); + Sanctum::actingAs($this->createAdminAppUser($tenant)); + + $entryId = $this->postJson('/api/v1/adminapp/tenant/entries', [ + 'entries' => [[ + 'title' => 'Entrada diaria', + 'description' => null, + 'event_date_ids' => [$eventDate->id], + 'stock' => 100, + 'price' => 5000, + ]], + ])->assertOk()->json('data.0.id'); + + $this->deleteJson("/api/v1/adminapp/tenant/entries/{$entryId}") + ->assertNoContent(); + + $this->assertDatabaseCount('catalog_items', 0); + $this->assertDatabaseCount('variantes', 0); + $this->assertDatabaseCount('inventories', 0); + } + public function test_dates_must_belong_to_the_authenticated_tenant(): void { $tenant = $this->createFiestaTenant(); diff --git a/tests/Feature/FiestaFutbolInfantil/FoodControllerTest.php b/tests/Feature/FiestaFutbolInfantil/FoodControllerTest.php index 63ae5ed..af5ea4e 100644 --- a/tests/Feature/FiestaFutbolInfantil/FoodControllerTest.php +++ b/tests/Feature/FiestaFutbolInfantil/FoodControllerTest.php @@ -123,6 +123,39 @@ class FoodControllerTest extends TestCase ]); } + public function test_it_deletes_food_records_and_removes_the_empty_product(): void + { + [$tenant, $firstDate, $secondDate] = $this->configuredTenant(); + Sanctum::actingAs($this->createAdminAppUser($tenant)); + + $created = $this->postJson('/api/v1/adminapp/tenant/foods', [ + 'variants' => [ + $this->variantPayload($firstDate->id, 'Almuerzo', 'Comedor', 100, 10000), + $this->variantPayload($secondDate->id, 'Cena', 'Vianda', 50, 8000), + ], + ])->assertOk(); + $firstId = $created->json('data.variants.0.id'); + $secondId = $created->json('data.variants.1.id'); + + $this->deleteJson("/api/v1/adminapp/tenant/foods/{$secondId}") + ->assertNoContent(); + + $this->assertDatabaseHas('catalog_items', [ + 'tenant_code' => $tenant->codigo, + 'slug' => 'comida', + 'precio' => 10000, + ]); + $this->assertDatabaseCount('variantes', 1); + $this->assertDatabaseCount('inventories', 1); + + $this->deleteJson("/api/v1/adminapp/tenant/foods/{$firstId}") + ->assertNoContent(); + + $this->assertDatabaseCount('catalog_items', 0); + $this->assertDatabaseCount('variantes', 0); + $this->assertDatabaseCount('inventories', 0); + } + public function test_it_rejects_duplicate_combinations(): void { [$tenant, $firstDate] = $this->configuredTenant(); diff --git a/tests/Feature/FiestaFutbolInfantil/MerchandiseControllerTest.php b/tests/Feature/FiestaFutbolInfantil/MerchandiseControllerTest.php index d8bd320..c7b7e5b 100644 --- a/tests/Feature/FiestaFutbolInfantil/MerchandiseControllerTest.php +++ b/tests/Feature/FiestaFutbolInfantil/MerchandiseControllerTest.php @@ -139,6 +139,40 @@ class MerchandiseControllerTest extends TestCase ]); } + public function test_it_deletes_merchandise_records_and_removes_the_empty_item(): void + { + [$tenant] = $this->configuredTenant(); + Sanctum::actingAs($this->createAdminAppUser($tenant)); + + $created = $this->postJson('/api/v1/adminapp/tenant/merchandise', [ + 'items' => [ + $this->itemPayload('Camiseta', 3, [ + $this->variantPayload('Verde', 'S', 100, 10000), + $this->variantPayload('Blanco', 'M', 50, 15000), + ]), + ], + ])->assertOk(); + $itemId = $created->json('data.0.id'); + $firstId = $created->json('data.0.variants.0.id'); + $secondId = $created->json('data.0.variants.1.id'); + + $this->deleteJson("/api/v1/adminapp/tenant/merchandise/{$firstId}") + ->assertNoContent(); + + $this->assertDatabaseHas('catalog_items', [ + 'id' => $itemId, + 'precio' => 15000, + ]); + $this->assertDatabaseCount('variantes', 1); + + $this->deleteJson("/api/v1/adminapp/tenant/merchandise/{$secondId}") + ->assertNoContent(); + + $this->assertDatabaseMissing('catalog_items', ['id' => $itemId]); + $this->assertDatabaseCount('variantes', 0); + $this->assertDatabaseCount('inventories', 0); + } + public function test_it_rejects_duplicate_color_and_size_combinations(): void { [$tenant] = $this->configuredTenant();