Compare commits

...

30 Commits

Author SHA1 Message Date
843659583e Merge branch 'homo' of https://gitea.quo.ar/tbianchini/shopit-back into homo 2026-08-25 10:49:07 -03:00
c2921166bd feat(catalog): exclude out-of-stock items and update variant visibility logic 2026-08-25 10:49:05 -03:00
0f67e66b6b feat(category-visibility): add success messages for category visibility updates 2026-08-25 10:47:53 -03:00
725abed06a feat(category-visibility): implement category visibility controller and service with update functionality 2026-08-25 10:47:53 -03:00
743939fc63 feat(category-visibility): add success messages for category visibility updates 2026-08-25 10:47:53 -03:00
eb50e65fef feat(purchase): add filtering for multiple statuses in purchase index and order by status then date 2026-08-25 10:47:53 -03:00
1cf13501d8 feat(category-visibility): implement category visibility controller and service with update functionality 2026-08-25 10:47:53 -03:00
2eeef8d392 feat(catalog): add is_enabled attribute to categories and filter items accordingly 2026-08-25 10:47:53 -03:00
07d2129410 feat(catalog): exclude out-of-stock items and update variant visibility logic 2026-08-25 09:32:56 -03:00
ceb1d1f1af Merge branch 'homo' of https://gitea.quo.ar/tbianchini/shopit-back into homo 2026-08-24 17:01:55 -03:00
885ab2a6e3 feat(category-visibility): add success messages for category visibility updates 2026-08-24 17:01:46 -03:00
47196a572a feat(category-visibility): implement category visibility controller and service with update functionality 2026-08-24 17:01:46 -03:00
45bba9516d feat(category-visibility): add success messages for category visibility updates 2026-08-24 17:00:48 -03:00
c6da71894e feat(purchase): add filtering for multiple statuses in purchase index and order by status then date 2026-08-24 17:00:48 -03:00
75d3c819d9 feat(category-visibility): implement category visibility controller and service with update functionality 2026-08-24 17:00:48 -03:00
bd6d672df2 feat(catalog): add is_enabled attribute to categories and filter items accordingly 2026-08-24 17:00:48 -03:00
4162e2c1cc Merge branch 'homo' of https://gitea.quo.ar/tbianchini/shopit-back into homo 2026-08-24 16:05:15 -03:00
8d01620fa8 feat(sale-modification): add changed_at field serialization to ISO 8601 format 2026-08-24 16:05:10 -03:00
f8b8bab474 feat(category-visibility): add success messages for category visibility updates 2026-08-24 16:05:10 -03:00
53d1415b18 feat(purchase): add filtering for multiple statuses in purchase index and order by status then date 2026-08-24 16:05:10 -03:00
6ff9119086 feat(category-visibility): implement category visibility controller and service with update functionality 2026-08-24 16:05:10 -03:00
24e000b423 feat(catalog): add is_enabled attribute to categories and filter items accordingly 2026-08-24 16:05:10 -03:00
c69ebe5f7c feat(sale-modification): add changed_at field serialization to ISO 8601 format 2026-08-24 15:59:00 -03:00
c3a2da607d Merge branch 'dev' into homo 2026-08-24 12:45:51 -03:00
d1c4acc7ae feat(category-visibility): add success messages for category visibility updates 2026-08-24 12:44:37 -03:00
f8c4cc7428 Merge branch 'dev' into homo 2026-08-24 12:36:57 -03:00
21cfbe2a46 feat(purchase): add filtering for multiple statuses in purchase index and order by status then date 2026-08-24 12:35:20 -03:00
abc060ab25 Merge branch 'homo' of https://gitea.quo.ar/tbianchini/shopit-back into homo 2026-08-24 11:32:09 -03:00
80adbd9ca9 feat(category-visibility): implement category visibility controller and service with update functionality 2026-08-24 11:31:26 -03:00
12c10bbe06 feat(catalog): add is_enabled attribute to categories and filter items accordingly 2026-08-24 10:49:14 -03:00
20 changed files with 438 additions and 37 deletions

View File

@@ -175,18 +175,30 @@ class CatalogItem extends Model
} }
/** @param Builder<CatalogItem> $query */ /** @param Builder<CatalogItem> $query */
public function scopeWhereVariantsAvailable(Builder $query): Builder public function scopeWhereAvailable(Builder $query): Builder
{ {
return $query->where(function (Builder $query): void { return $query->where(function (Builder $query): void {
$query $query
->whereDoesntHave('variants') ->where('catalog_items.inventory_policy', InventoryPolicy::Unlimited->value)
->orWhere('catalog_items.inventory_policy', InventoryPolicy::Unlimited->value)
->orWhereHas( ->orWhereHas(
'variants.inventory', 'variants.inventory',
fn (Builder $inventoryQuery): Builder => $inventoryQuery fn (Builder $inventoryQuery): Builder => $inventoryQuery
->whereColumn('inventories.real_stock', '>', 'inventories.reserved_stock') ->whereColumn('inventories.real_stock', '>', 'inventories.reserved_stock')
)
->orWhere(function (Builder $directItemQuery): void {
$directItemQuery
->whereDoesntHave('variants')
->where(function (Builder $inventoryQuery): void {
$inventoryQuery
->whereNull('catalog_items.inventory_id')
->orWhereHas(
'inventory',
fn (Builder $availableInventoryQuery): Builder => $availableInventoryQuery
->whereColumn('inventories.real_stock', '>', 'inventories.reserved_stock')
); );
}); });
});
});
} }
/** @return Collection<int, Variant> */ /** @return Collection<int, Variant> */

View File

@@ -15,6 +15,7 @@ use Illuminate\Database\Eloquent\Relations\HasMany;
'tenant_code', 'tenant_code',
'categoria_id', 'categoria_id',
'nombre', 'nombre',
'is_enabled',
])] ])]
class Category extends Model class Category extends Model
{ {
@@ -22,6 +23,10 @@ class Category extends Model
protected $table = 'categorias'; protected $table = 'categorias';
protected $attributes = [
'is_enabled' => true,
];
/** /**
* @return array<string, string> * @return array<string, string>
*/ */
@@ -29,6 +34,7 @@ class Category extends Model
{ {
return [ return [
'categoria_id' => 'integer', 'categoria_id' => 'integer',
'is_enabled' => 'boolean',
]; ];
} }

View File

@@ -46,7 +46,7 @@ class CatalogFeaturedItemResource extends JsonResource
$availableStock, $availableStock,
$remainingUserQuota, $remainingUserQuota,
), ),
'variants' => $catalogItem->variants 'variants' => $catalogItem->visibleVariants()
->map(function (Variant $variant) use ($catalogItem, $remainingUserQuota): array { ->map(function (Variant $variant) use ($catalogItem, $remainingUserQuota): array {
$variantStock = $catalogItem->inventory_policy === InventoryPolicy::Unlimited $variantStock = $catalogItem->inventory_policy === InventoryPolicy::Unlimited
? null ? null
@@ -115,7 +115,7 @@ class CatalogFeaturedItemResource extends JsonResource
private function firstImageUrl(CatalogItem $catalogItem): ?string private function firstImageUrl(CatalogItem $catalogItem): ?string
{ {
$attachment = $catalogItem->attachments->first() $attachment = $catalogItem->attachments->first()
?? $catalogItem->variants ?? $catalogItem->visibleVariants()
->flatMap(fn (Variant $variant) => $variant->attachments) ->flatMap(fn (Variant $variant) => $variant->attachments)
->first(); ->first();

View File

@@ -16,8 +16,9 @@ class CatalogSearchItemResource extends JsonResource
public function toArray(Request $request): array public function toArray(Request $request): array
{ {
$availableStock = $this->availableStock(); $availableStock = $this->availableStock();
$visibleVariants = $this->visibleVariants();
$attachment = $this->attachments->first() $attachment = $this->attachments->first()
?? $this->variants ?? $visibleVariants
->flatMap(fn (Variant $variant) => $variant->attachments) ->flatMap(fn (Variant $variant) => $variant->attachments)
->first(); ->first();
@@ -30,7 +31,7 @@ class CatalogSearchItemResource extends JsonResource
'image' => $attachment?->getTemporaryUrl(1440), 'image' => $attachment?->getTemporaryUrl(1440),
'maximum_addable_quantity' => $this->maximumAddable($availableStock), 'maximum_addable_quantity' => $this->maximumAddable($availableStock),
'unavailable_message' => $this->unavailableMessage($availableStock), 'unavailable_message' => $this->unavailableMessage($availableStock),
'variants' => $this->variants 'variants' => $visibleVariants
->map(function (Variant $variant): array { ->map(function (Variant $variant): array {
$variantStock = $this->inventory_policy === InventoryPolicy::Unlimited $variantStock = $this->inventory_policy === InventoryPolicy::Unlimited
? null ? null

View File

@@ -205,6 +205,13 @@ class CatalogService
]); ]);
$visibleVariants = $catalogItem->visibleVariants(); $visibleVariants = $catalogItem->visibleVariants();
if ($catalogItem->type === CatalogItemType::Standard
&& ($catalogItem->inventory_id !== null || $catalogItem->variants->isNotEmpty())
&& ! $catalogItem->isAvailable()) {
throw new NotFoundHttpException('Catalog item is out of stock.');
}
$catalogItem->setRelation('variants', $visibleVariants);
$selectedVariant = $variantId === null $selectedVariant = $variantId === null
? $visibleVariants->first() ? $visibleVariants->first()
: $visibleVariants->firstWhere('id', $variantId); : $visibleVariants->firstWhere('id', $variantId);
@@ -231,6 +238,7 @@ class CatalogService
$paginator = CatalogItem::query() $paginator = CatalogItem::query()
->where('tenant_code', $tenant->codigo) ->where('tenant_code', $tenant->codigo)
->whereAvailable()
->where(function (Builder $query) use ($containsPattern): void { ->where(function (Builder $query) use ($containsPattern): void {
$query $query
->whereRaw('LOWER(nombre) LIKE ?', [$containsPattern]) ->whereRaw('LOWER(nombre) LIKE ?', [$containsPattern])
@@ -280,6 +288,7 @@ class CatalogService
return CatalogItem::query() return CatalogItem::query()
->where('tenant_code', $tenant->codigo) ->where('tenant_code', $tenant->codigo)
->where('category_id', $category->id) ->where('category_id', $category->id)
->whereAvailable()
->with([ ->with([
'attachments', 'attachments',
'inventory', 'inventory',

View File

@@ -49,6 +49,16 @@ class FeaturedGroupService
{ {
$query = CatalogItem::query() $query = CatalogItem::query()
->where('catalog_items.tenant_code', $featuredGroup->tenant_code) ->where('catalog_items.tenant_code', $featuredGroup->tenant_code)
->whereAvailable()
->where(function (Builder $query): void {
$query
->whereDoesntHave('category')
->orWhereHas(
'category',
fn (Builder $categoryQuery): Builder => $categoryQuery
->where('is_enabled', true),
);
})
->with([ ->with([
'inventory', 'inventory',
'attachments', 'attachments',

View File

@@ -0,0 +1,52 @@
<?php
namespace App\Domains\FiestaFutbolInfantil\Controllers;
use App\Domains\FiestaFutbolInfantil\Enums\FiestaCategory;
use App\Domains\FiestaFutbolInfantil\Requests\UpdateCategoryVisibilityRequest;
use App\Domains\FiestaFutbolInfantil\Services\CategoryVisibilityService;
use App\Http\Controllers\Controller;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
class CategoryVisibilityController extends Controller
{
public function __construct(private readonly CategoryVisibilityService $visibilityService) {}
public function show(Request $request): JsonResponse
{
$category = $this->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,
],
'message' => $model->is_enabled
? 'Mostrar en sitio web se activo correctamente'
: 'Mostrar en sitio web se desactivo correctamente',
]);
}
private function category(Request $request): FiestaCategory
{
return FiestaCategory::from((string) $request->route('category'));
}
}

View File

@@ -0,0 +1,21 @@
<?php
namespace App\Domains\FiestaFutbolInfantil\Enums;
enum FiestaCategory: string
{
case Entries = 'entries';
case Foods = 'foods';
case Accommodations = 'accommodations';
case Merchandise = 'merchandise';
public function categoryName(): string
{
return match ($this) {
self::Entries => 'Entradas',
self::Foods => 'Comidas',
self::Accommodations => 'Alojamientos',
self::Merchandise => 'Merchandising',
};
}
}

View File

@@ -0,0 +1,16 @@
<?php
namespace App\Domains\FiestaFutbolInfantil\Requests;
use Illuminate\Foundation\Http\FormRequest;
class UpdateCategoryVisibilityRequest extends FormRequest
{
/** @return array<string, mixed> */
public function rules(): array
{
return [
'is_enabled' => ['required', 'boolean'],
];
}
}

View File

@@ -0,0 +1,29 @@
<?php
namespace App\Domains\FiestaFutbolInfantil\Services;
use App\Domains\Catalog\Models\Category;
use App\Domains\FiestaFutbolInfantil\Enums\FiestaCategory;
use App\Domains\Tenant\Models\Tenant;
class CategoryVisibilityService
{
public function isEnabled(Tenant $tenant, FiestaCategory $category): bool
{
return Category::query()
->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;
}
}

View File

@@ -1,6 +1,7 @@
<?php <?php
use App\Domains\FiestaFutbolInfantil\Controllers\AccommodationController; use App\Domains\FiestaFutbolInfantil\Controllers\AccommodationController;
use App\Domains\FiestaFutbolInfantil\Controllers\CategoryVisibilityController;
use App\Domains\FiestaFutbolInfantil\Controllers\EntryController; use App\Domains\FiestaFutbolInfantil\Controllers\EntryController;
use App\Domains\FiestaFutbolInfantil\Controllers\FoodController; use App\Domains\FiestaFutbolInfantil\Controllers\FoodController;
use App\Domains\FiestaFutbolInfantil\Controllers\MerchandiseController; use App\Domains\FiestaFutbolInfantil\Controllers\MerchandiseController;
@@ -9,6 +10,20 @@ use Illuminate\Support\Facades\Route;
Route::prefix('v1/adminapp/tenant') Route::prefix('v1/adminapp/tenant')
->middleware(['auth:sanctum', 'adminapp.tenant']) ->middleware(['auth:sanctum', 'adminapp.tenant'])
->group(function (): void { ->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']) Route::get('entries', [EntryController::class, 'index'])
->middleware('tenant.menu:adminapp.fiesta-futbol-infantil.entradas') ->middleware('tenant.menu:adminapp.fiesta-futbol-infantil.entradas')
->name('adminapp.fiesta-futbol-infantil.entries.index'); ->name('adminapp.fiesta-futbol-infantil.entries.index');

View File

@@ -24,13 +24,22 @@ class PurchaseController extends Controller
{ {
public function index(Request $request, Tenant $tenant): JsonResponse 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( return PurchaseResource::collection(
Purchase::query() Purchase::query()
->where('tenant_codigo', $tenant->codigo) ->where('tenant_codigo', $tenant->codigo)
->where('user_id', $request->user()->id) ->where('user_id', $request->user()->id)
->when($request->query('status'), function ($query, $status) { ->when($statuses !== [], fn ($query) => $query->whereIn('status', $statuses))
$query->where('status', $status); ->orderBy('status')
})
->latest() ->latest()
->paginateFromRequest() ->paginateFromRequest()
)->response(); )->response();

View File

@@ -0,0 +1,22 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::table('categorias', function (Blueprint $table): void {
$table->boolean('is_enabled')->default(true)->after('nombre');
});
}
public function down(): void
{
Schema::table('categorias', function (Blueprint $table): void {
$table->dropColumn('is_enabled');
});
}
};

View File

@@ -73,19 +73,18 @@ class CatalogControllerTest extends TestCase
->assertJsonPath('0.items.0.descripcion', 'Variants description') ->assertJsonPath('0.items.0.descripcion', 'Variants description')
->assertJsonPath('0.items.0.precio', '100.00') ->assertJsonPath('0.items.0.precio', '100.00')
->assertJsonPath('0.items.0.maximum_addable_quantity', 7) ->assertJsonPath('0.items.0.maximum_addable_quantity', 7)
->assertJsonCount(3, '0.items.0.variants') ->assertJsonCount(2, '0.items.0.variants')
->assertJsonPath('0.items.0.variants.0.maximum_addable_quantity', 4) ->assertJsonPath('0.items.0.variants.0.maximum_addable_quantity', 4)
->assertJsonPath('0.items.0.variants.1.maximum_addable_quantity', 3) ->assertJsonPath('0.items.0.variants.1.maximum_addable_quantity', 3)
->assertJsonPath('0.items.0.variants.2.id', $unavailableVariant->id)
->assertJsonPath('0.items.0.variants.2.maximum_addable_quantity', 0)
->assertJsonPath(
'0.items.0.variants.2.unavailable_message',
'Este producto no tiene stock disponible.',
)
->assertJsonPath('1.title', 'Row') ->assertJsonPath('1.title', 'Row')
->assertJsonPath('1.items.data.0.maximum_addable_quantity', 8) ->assertJsonPath('1.items.data.0.maximum_addable_quantity', 8)
->assertJsonMissingPath('1.items.data.0.stock_tecnico') ->assertJsonMissingPath('1.items.data.0.stock_tecnico')
->assertJsonCount(0, '1.items.data.0.variants'); ->assertJsonCount(0, '1.items.data.0.variants');
$this->assertNotContains(
$unavailableVariant->id,
collect($response->json('0.items.0.variants'))->pluck('id')->all(),
);
} }
public function test_maximum_addable_quantity_shares_the_authenticated_user_quota_between_variants(): void public function test_maximum_addable_quantity_shares_the_authenticated_user_quota_between_variants(): void
@@ -133,7 +132,7 @@ class CatalogControllerTest extends TestCase
->assertJsonMissingPath('0.items.0.variants.1.stock_tecnico'); ->assertJsonMissingPath('0.items.0.variants.1.stock_tecnico');
} }
public function test_it_includes_out_of_stock_items_with_an_unavailable_message(): void public function test_it_excludes_out_of_stock_items(): void
{ {
$tenant = $this->createTenant('catalog-available-variants'); $tenant = $this->createTenant('catalog-available-variants');
$group = $this->createGroup( $group = $this->createGroup(
@@ -161,15 +160,10 @@ class CatalogControllerTest extends TestCase
$this->getJson("/api/tenants/{$tenant->codigo}/catalog") $this->getJson("/api/tenants/{$tenant->codigo}/catalog")
->assertOk() ->assertOk()
->assertJsonCount(2, '0.items') ->assertJsonCount(1, '0.items')
->assertJsonPath('0.items.0.nombre', 'Unavailable') ->assertJsonPath('0.items.0.nombre', 'Available')
->assertJsonPath('0.items.0.maximum_addable_quantity', 0) ->assertJsonPath('0.items.0.unavailable_message', null)
->assertJsonPath( ->assertJsonMissing(['nombre' => 'Unavailable']);
'0.items.0.unavailable_message',
'Este producto no tiene stock disponible.',
)
->assertJsonPath('0.items.1.nombre', 'Available')
->assertJsonPath('0.items.1.unavailable_message', null);
} }
public function test_column_with_image_uses_item_image_then_variant_image_then_null(): void public function test_column_with_image_uses_item_image_then_variant_image_then_null(): void
@@ -372,6 +366,53 @@ class CatalogControllerTest extends TestCase
->assertJsonPath('1.items.meta.total', 2); ->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( private function createGroup(
Tenant $tenant, Tenant $tenant,
ProductLayout $layout, ProductLayout $layout,

View File

@@ -48,7 +48,7 @@ class CatalogItemDetailControllerTest extends TestCase
$this->assertStringContainsString($itemImage->path, $response->json('data.images.0')); $this->assertStringContainsString($itemImage->path, $response->json('data.images.0'));
} }
public function test_it_lists_unavailable_variants_and_selects_the_first_available_one(): void public function test_it_omits_unavailable_variants_and_selects_the_first_available_one(): void
{ {
Storage::fake('s3'); Storage::fake('s3');
$tenant = $this->createTenant('detail-default'); $tenant = $this->createTenant('detail-default');
@@ -69,14 +69,8 @@ class CatalogItemDetailControllerTest extends TestCase
$response $response
->assertOk() ->assertOk()
->assertJsonCount(2, 'data.variants') ->assertJsonCount(1, 'data.variants')
->assertJsonPath('data.variants.0.id', $firstVariant->id) ->assertJsonPath('data.variants.0.id', $secondVariant->id)
->assertJsonPath('data.variants.0.maximum_addable_quantity', 0)
->assertJsonPath(
'data.variants.0.unavailable_message',
'Este producto no tiene stock disponible.',
)
->assertJsonPath('data.variants.1.id', $secondVariant->id)
->assertJsonPath('data.selected_variant.id', $secondVariant->id) ->assertJsonPath('data.selected_variant.id', $secondVariant->id)
->assertJsonPath('data.selected_variant.maximum_addable_quantity', 6) ->assertJsonPath('data.selected_variant.maximum_addable_quantity', 6)
->assertJsonMissingPath('data.selected_variant.stock_tecnico') ->assertJsonMissingPath('data.selected_variant.stock_tecnico')
@@ -84,6 +78,10 @@ class CatalogItemDetailControllerTest extends TestCase
$response $response
->assertJsonMissingPath('data.stock_tecnico') ->assertJsonMissingPath('data.stock_tecnico')
->assertJsonMissingPath('data.images'); ->assertJsonMissingPath('data.images');
$this->assertNotContains(
$firstVariant->id,
collect($response->json('data.variants'))->pluck('id')->all(),
);
$this->assertStringContainsString($secondImage->path, $response->json('data.selected_variant.images.0')); $this->assertStringContainsString($secondImage->path, $response->json('data.selected_variant.images.0'));
$this->assertStringNotContainsString($firstImage->path, $response->json('data.selected_variant.images.0')); $this->assertStringNotContainsString($firstImage->path, $response->json('data.selected_variant.images.0'));
$this->assertStringNotContainsString($itemImage->path, $response->json('data.selected_variant.images.0')); $this->assertStringNotContainsString($itemImage->path, $response->json('data.selected_variant.images.0'));
@@ -93,6 +91,19 @@ class CatalogItemDetailControllerTest extends TestCase
)->assertNotFound(); )->assertNotFound();
} }
public function test_it_does_not_return_an_out_of_stock_item(): void
{
$tenant = $this->createTenant('detail-out-of-stock');
$inventory = Inventory::query()->create([
'real_stock' => 5,
'reserved_stock' => 5,
]);
$item = $this->createItem($tenant, 'Sold out item', $inventory);
$this->getJson("/api/tenants/{$tenant->codigo}/catalog-items/{$item->id}")
->assertNotFound();
}
public function test_it_selects_the_requested_variant_and_lists_variant_values_and_stock(): void public function test_it_selects_the_requested_variant_and_lists_variant_values_and_stock(): void
{ {
Storage::fake('s3'); Storage::fake('s3');

View File

@@ -5,6 +5,7 @@ namespace Tests\Feature\Catalog;
use App\Domains\Attachable\Enums\AttachmentType; use App\Domains\Attachable\Enums\AttachmentType;
use App\Domains\Attachable\Models\Attachment; use App\Domains\Attachable\Models\Attachment;
use App\Domains\Catalog\Models\CatalogItem; use App\Domains\Catalog\Models\CatalogItem;
use App\Domains\Catalog\Models\Category;
use App\Domains\Catalog\Models\Inventory; use App\Domains\Catalog\Models\Inventory;
use App\Domains\Tenant\Models\Tenant; use App\Domains\Tenant\Models\Tenant;
use Illuminate\Foundation\Testing\RefreshDatabase; use Illuminate\Foundation\Testing\RefreshDatabase;
@@ -15,6 +16,21 @@ class CatalogSchemaTest extends TestCase
{ {
use RefreshDatabase; 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 public function test_legacy_product_tables_are_replaced_by_catalog_tables(): void
{ {
$this->assertFalse(Schema::hasTable('productos')); $this->assertFalse(Schema::hasTable('productos'));

View File

@@ -62,6 +62,24 @@ class CatalogSearchTest extends TestCase
$this->createCatalogItem($tenant, "Running {$number}"); $this->createCatalogItem($tenant, "Running {$number}");
} }
$exactMatch = $this->createCatalogItem($tenant, 'Running'); $exactMatch = $this->createCatalogItem($tenant, 'Running');
$outOfStock = CatalogItem::query()->create([
'tenant_code' => $tenant->codigo,
'slug' => 'running-sold-out',
'nombre' => 'Running sold out',
'descripcion' => 'Running sold out description',
'precio' => 100,
]);
$outOfStock->variants()->create([
'inventory_id' => Inventory::query()->create(['real_stock' => 0])->id,
]);
CatalogItem::query()->create([
'tenant_code' => $tenant->codigo,
'inventory_id' => Inventory::query()->create(['real_stock' => 2, 'reserved_stock' => 2])->id,
'slug' => 'running-direct-sold-out',
'nombre' => 'Running direct sold out',
'descripcion' => 'Running direct sold out description',
'precio' => 100,
]);
$this->createCatalogItem($tenant, 'Unrelated'); $this->createCatalogItem($tenant, 'Unrelated');
$this->createCatalogItem($otherTenant, 'Running foreign'); $this->createCatalogItem($otherTenant, 'Running foreign');
@@ -76,6 +94,8 @@ class CatalogSearchTest extends TestCase
->assertJsonPath('meta.total', 6) ->assertJsonPath('meta.total', 6)
->assertJsonCount(4, 'data') ->assertJsonCount(4, 'data')
->assertJsonPath('data.0.id', $exactMatch->id) ->assertJsonPath('data.0.id', $exactMatch->id)
->assertJsonMissing(['nombre' => 'Running sold out'])
->assertJsonMissing(['nombre' => 'Running direct sold out'])
->assertJsonMissing(['nombre' => 'Running foreign']) ->assertJsonMissing(['nombre' => 'Running foreign'])
->assertJsonMissing(['nombre' => 'Unrelated']); ->assertJsonMissing(['nombre' => 'Unrelated']);
} }

View File

@@ -29,6 +29,17 @@ class CategoryDetailTest extends TestCase
$this->createCatalogItem($tenant, $category, 'Remera C'); $this->createCatalogItem($tenant, $category, 'Remera C');
$firstItem = $this->createCatalogItem($tenant, $category, 'Remera A'); $firstItem = $this->createCatalogItem($tenant, $category, 'Remera A');
$secondItem = $this->createCatalogItem($tenant, $category, 'Remera B'); $secondItem = $this->createCatalogItem($tenant, $category, 'Remera B');
$outOfStock = CatalogItem::query()->create([
'tenant_code' => $tenant->codigo,
'category_id' => $category->id,
'slug' => 'remera-agotada',
'nombre' => 'Remera agotada',
'descripcion' => 'Sin stock',
'precio' => 100,
]);
$outOfStock->variants()->create([
'inventory_id' => Inventory::query()->create(['real_stock' => 0])->id,
]);
$this->createCatalogItem($tenant, $otherCategory, 'Pantalón'); $this->createCatalogItem($tenant, $otherCategory, 'Pantalón');
$this->getJson("/api/tenants/{$tenant->codigo}/categories/{$category->id}") $this->getJson("/api/tenants/{$tenant->codigo}/categories/{$category->id}")
@@ -44,6 +55,7 @@ class CategoryDetailTest extends TestCase
->assertJsonCount(2, 'data') ->assertJsonCount(2, 'data')
->assertJsonPath('data.0.id', $firstItem->id) ->assertJsonPath('data.0.id', $firstItem->id)
->assertJsonPath('data.1.id', $secondItem->id) ->assertJsonPath('data.1.id', $secondItem->id)
->assertJsonMissing(['nombre' => 'Remera agotada'])
->assertJsonMissing(['nombre' => 'Pantalón']); ->assertJsonMissing(['nombre' => 'Pantalón']);
} }

View File

@@ -38,6 +38,59 @@ class EntryControllerTest extends TestCase
->assertUnauthorized(); ->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)
->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' => true,
]);
}
}
public function test_it_creates_multiple_entries_with_dates_and_tracked_inventory(): void public function test_it_creates_multiple_entries_with_dates_and_tracked_inventory(): void
{ {
$tenant = $this->createFiestaTenant(); $tenant = $this->createFiestaTenant();

View File

@@ -1230,6 +1230,52 @@ class StorePurchaseTest extends TestCase
->assertJsonPath('data.0.total', '100.00'); ->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 public function test_it_rejects_a_cart_from_another_user(): void
{ {
$this->createTenant('sonder', 'Sonder', 'sonder.com.ar'); $this->createTenant('sonder', 'Sonder', 'sonder.com.ar');