Merge branch 'dev' into homo
This commit is contained in:
@@ -103,9 +103,14 @@ class Cart extends Model
|
|||||||
$cartQuantity = (int) $this->items()
|
$cartQuantity = (int) $this->items()
|
||||||
->where('catalog_item_id', $catalogItemId)
|
->where('catalog_item_id', $catalogItemId)
|
||||||
->sum('cantidad');
|
->sum('cantidad');
|
||||||
$this->assertUserPurchaseLimit($selectedItem, $cartQuantity + $quantity);
|
|
||||||
$inventoryService = app(CatalogInventoryService::class);
|
$inventoryService = app(CatalogInventoryService::class);
|
||||||
$availableQuantity = $inventoryService->availableQuantity($selectedItem);
|
$availableQuantity = $inventoryService->availableQuantity($selectedItem);
|
||||||
|
$this->assertUserPurchaseLimit(
|
||||||
|
$selectedItem,
|
||||||
|
$cartQuantity + $quantity,
|
||||||
|
heldQuantity: $cartQuantity,
|
||||||
|
maximumAddableCeiling: $availableQuantity,
|
||||||
|
);
|
||||||
|
|
||||||
if ($availableQuantity !== null && $availableQuantity < $quantity) {
|
if ($availableQuantity !== null && $availableQuantity < $quantity) {
|
||||||
throw ValidationException::withMessages([
|
throw ValidationException::withMessages([
|
||||||
@@ -181,10 +186,13 @@ class Cart extends Model
|
|||||||
->where('catalog_item_id', $item->catalog_item_id)
|
->where('catalog_item_id', $item->catalog_item_id)
|
||||||
->whereKeyNot($item->getKey())
|
->whereKeyNot($item->getKey())
|
||||||
->sum('cantidad');
|
->sum('cantidad');
|
||||||
|
$nextAvailableQuantity = $inventoryService->availableQuantity($nextSelection);
|
||||||
$this->assertUserPurchaseLimit(
|
$this->assertUserPurchaseLimit(
|
||||||
$nextSelection,
|
$nextSelection,
|
||||||
$otherVariantsQuantity + $quantity,
|
$otherVariantsQuantity + $quantity,
|
||||||
$excludedPurchaseId,
|
$excludedPurchaseId,
|
||||||
|
$otherVariantsQuantity + $item->cantidad,
|
||||||
|
$nextAvailableQuantity,
|
||||||
);
|
);
|
||||||
|
|
||||||
app(StockReservationService::class)->release($item, $currentSelection, $item->cantidad);
|
app(StockReservationService::class)->release($item, $currentSelection, $item->cantidad);
|
||||||
@@ -222,6 +230,7 @@ class Cart extends Model
|
|||||||
}
|
}
|
||||||
|
|
||||||
$delta = $quantity - $item->cantidad;
|
$delta = $quantity - $item->cantidad;
|
||||||
|
$availableQuantity = $inventoryService->availableQuantity($currentSelection);
|
||||||
|
|
||||||
if ($delta > 0) {
|
if ($delta > 0) {
|
||||||
$otherVariantsQuantity = (int) $this->items()
|
$otherVariantsQuantity = (int) $this->items()
|
||||||
@@ -232,11 +241,11 @@ class Cart extends Model
|
|||||||
$currentSelection,
|
$currentSelection,
|
||||||
$otherVariantsQuantity + $quantity,
|
$otherVariantsQuantity + $quantity,
|
||||||
$excludedPurchaseId,
|
$excludedPurchaseId,
|
||||||
|
$otherVariantsQuantity + $item->cantidad,
|
||||||
|
$availableQuantity,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
$availableQuantity = $inventoryService->availableQuantity($currentSelection);
|
|
||||||
|
|
||||||
if ($delta > 0 && $availableQuantity !== null && $availableQuantity < $delta) {
|
if ($delta > 0 && $availableQuantity !== null && $availableQuantity < $delta) {
|
||||||
$maxAvailable = $availableQuantity + $item->cantidad;
|
$maxAvailable = $availableQuantity + $item->cantidad;
|
||||||
throw ValidationException::withMessages([
|
throw ValidationException::withMessages([
|
||||||
@@ -355,6 +364,8 @@ class Cart extends Model
|
|||||||
CatalogItem|Variant $selectedItem,
|
CatalogItem|Variant $selectedItem,
|
||||||
int $cartQuantity,
|
int $cartQuantity,
|
||||||
?int $excludedPurchaseId = null,
|
?int $excludedPurchaseId = null,
|
||||||
|
int $heldQuantity = 0,
|
||||||
|
?int $maximumAddableCeiling = null,
|
||||||
): void {
|
): void {
|
||||||
if ($this->user_id === null) {
|
if ($this->user_id === null) {
|
||||||
return;
|
return;
|
||||||
@@ -369,6 +380,9 @@ class Cart extends Model
|
|||||||
$this->user_id,
|
$this->user_id,
|
||||||
$cartQuantity,
|
$cartQuantity,
|
||||||
$excludedPurchaseId,
|
$excludedPurchaseId,
|
||||||
|
$this->getKey(),
|
||||||
|
$heldQuantity,
|
||||||
|
$maximumAddableCeiling,
|
||||||
field: 'cantidad',
|
field: 'cantidad',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,17 +17,20 @@ use App\Domains\Catalog\Resources\CatalogItemDetailResource;
|
|||||||
use App\Domains\Catalog\Resources\CatalogItemResource;
|
use App\Domains\Catalog\Resources\CatalogItemResource;
|
||||||
use App\Domains\Catalog\Resources\CatalogSearchItemResource;
|
use App\Domains\Catalog\Resources\CatalogSearchItemResource;
|
||||||
use App\Domains\Catalog\Resources\CatalogVariantOptionsResource;
|
use App\Domains\Catalog\Resources\CatalogVariantOptionsResource;
|
||||||
|
use App\Domains\Catalog\Services\CatalogItemAllowanceService;
|
||||||
use App\Domains\Catalog\Services\CatalogService;
|
use App\Domains\Catalog\Services\CatalogService;
|
||||||
use App\Domains\Catalog\Services\FeaturedGroupService;
|
use App\Domains\Catalog\Services\FeaturedGroupService;
|
||||||
use App\Domains\Catalog\Services\VariantSelectionService;
|
use App\Domains\Catalog\Services\VariantSelectionService;
|
||||||
use App\Domains\Tenant\Models\Tenant;
|
use App\Domains\Tenant\Models\Tenant;
|
||||||
use App\Http\Controllers\Controller;
|
use App\Http\Controllers\Controller;
|
||||||
use Illuminate\Http\JsonResponse;
|
use Illuminate\Http\JsonResponse;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
use Illuminate\Http\Resources\Json\AnonymousResourceCollection;
|
use Illuminate\Http\Resources\Json\AnonymousResourceCollection;
|
||||||
|
use Illuminate\Support\Facades\Auth;
|
||||||
|
|
||||||
class CatalogController extends Controller
|
class CatalogController extends Controller
|
||||||
{
|
{
|
||||||
public function index(Tenant $tenant, FeaturedGroupService $featuredGroupService): JsonResponse
|
public function index(Request $request, Tenant $tenant, FeaturedGroupService $featuredGroupService): JsonResponse
|
||||||
{
|
{
|
||||||
$featuredGroups = FeaturedGroup::query()
|
$featuredGroups = FeaturedGroup::query()
|
||||||
->where('tenant_code', $tenant->codigo)
|
->where('tenant_code', $tenant->codigo)
|
||||||
@@ -37,7 +40,7 @@ class CatalogController extends Controller
|
|||||||
return response()->json($featuredGroups->map(
|
return response()->json($featuredGroups->map(
|
||||||
fn (FeaturedGroup $featuredGroup): array => (new CatalogFeaturedGroupResource(
|
fn (FeaturedGroup $featuredGroup): array => (new CatalogFeaturedGroupResource(
|
||||||
$featuredGroup,
|
$featuredGroup,
|
||||||
$featuredGroupService->itemsResponse($featuredGroup, 1),
|
$featuredGroupService->itemsResponse($featuredGroup, 1, $this->userId($request)),
|
||||||
))->resolve()
|
))->resolve()
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
@@ -46,15 +49,17 @@ class CatalogController extends Controller
|
|||||||
SearchCatalogItemsRequest $request,
|
SearchCatalogItemsRequest $request,
|
||||||
Tenant $tenant,
|
Tenant $tenant,
|
||||||
CatalogService $catalogService,
|
CatalogService $catalogService,
|
||||||
|
CatalogItemAllowanceService $allowances,
|
||||||
): AnonymousResourceCollection {
|
): AnonymousResourceCollection {
|
||||||
return CatalogSearchItemResource::collection(
|
$items = $catalogService->search(
|
||||||
$catalogService->search(
|
$tenant,
|
||||||
$tenant,
|
$request->validated('q'),
|
||||||
$request->validated('q'),
|
$tenant->search_items_per_page,
|
||||||
$tenant->search_items_per_page,
|
(int) $request->validated('page', 1),
|
||||||
(int) $request->validated('page', 1),
|
|
||||||
)
|
|
||||||
);
|
);
|
||||||
|
$allowances->attach($items->getCollection(), $this->userId($request));
|
||||||
|
|
||||||
|
return CatalogSearchItemResource::collection($items);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function category(
|
public function category(
|
||||||
@@ -62,17 +67,19 @@ class CatalogController extends Controller
|
|||||||
Tenant $tenant,
|
Tenant $tenant,
|
||||||
Category $category,
|
Category $category,
|
||||||
CatalogService $catalogService,
|
CatalogService $catalogService,
|
||||||
|
CatalogItemAllowanceService $allowances,
|
||||||
): AnonymousResourceCollection {
|
): AnonymousResourceCollection {
|
||||||
abort_unless($category->tenant_code === $tenant->codigo, 404);
|
abort_unless($category->tenant_code === $tenant->codigo, 404);
|
||||||
|
|
||||||
return CatalogSearchItemResource::collection(
|
$items = $catalogService->categoryItems(
|
||||||
$catalogService->categoryItems(
|
$tenant,
|
||||||
$tenant,
|
$category,
|
||||||
$category,
|
$tenant->search_items_per_page,
|
||||||
$tenant->search_items_per_page,
|
(int) $request->validated('page', 1),
|
||||||
(int) $request->validated('page', 1),
|
);
|
||||||
)
|
$allowances->attach($items->getCollection(), $this->userId($request));
|
||||||
)->additional([
|
|
||||||
|
return CatalogSearchItemResource::collection($items)->additional([
|
||||||
'category' => [
|
'category' => [
|
||||||
'id' => $category->id,
|
'id' => $category->id,
|
||||||
'nombre' => $category->nombre,
|
'nombre' => $category->nombre,
|
||||||
@@ -93,7 +100,11 @@ class CatalogController extends Controller
|
|||||||
|
|
||||||
$page = (int) $request->validated('page', 1);
|
$page = (int) $request->validated('page', 1);
|
||||||
|
|
||||||
return response()->json($featuredGroupService->itemsResponse($featuredGroup, $page));
|
return response()->json($featuredGroupService->itemsResponse(
|
||||||
|
$featuredGroup,
|
||||||
|
$page,
|
||||||
|
$this->userId($request),
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
public function show(
|
public function show(
|
||||||
@@ -101,17 +112,19 @@ class CatalogController extends Controller
|
|||||||
Tenant $tenant,
|
Tenant $tenant,
|
||||||
CatalogItem $catalogItem,
|
CatalogItem $catalogItem,
|
||||||
CatalogService $catalogService,
|
CatalogService $catalogService,
|
||||||
|
CatalogItemAllowanceService $allowances,
|
||||||
): CatalogItemDetailResource {
|
): CatalogItemDetailResource {
|
||||||
abort_unless($catalogItem->tenant_code === $tenant->codigo, 404);
|
abort_unless($catalogItem->tenant_code === $tenant->codigo, 404);
|
||||||
|
|
||||||
$variantId = $request->validated('variant_id');
|
$variantId = $request->validated('variant_id');
|
||||||
|
|
||||||
return CatalogItemDetailResource::make(
|
$item = $catalogService->getDetail(
|
||||||
$catalogService->getDetail(
|
$catalogItem,
|
||||||
$catalogItem,
|
$variantId === null ? null : (int) $variantId,
|
||||||
$variantId === null ? null : (int) $variantId,
|
|
||||||
)
|
|
||||||
);
|
);
|
||||||
|
$allowances->attach(collect([$item]), $this->userId($request));
|
||||||
|
|
||||||
|
return CatalogItemDetailResource::make($item);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function variantOptions(
|
public function variantOptions(
|
||||||
@@ -164,4 +177,11 @@ class CatalogController extends Controller
|
|||||||
->response()
|
->response()
|
||||||
->setStatusCode(201);
|
->setStatusCode(201);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function userId(Request $request): ?int
|
||||||
|
{
|
||||||
|
$userId = $request->user()?->getAuthIdentifier() ?? Auth::guard('sanctum')->id();
|
||||||
|
|
||||||
|
return $userId === null ? null : (int) $userId;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ use App\Domains\Catalog\Enums\ProductLayout;
|
|||||||
use App\Domains\Catalog\Models\CatalogItem;
|
use App\Domains\Catalog\Models\CatalogItem;
|
||||||
use App\Domains\Catalog\Models\FeaturedGroup;
|
use App\Domains\Catalog\Models\FeaturedGroup;
|
||||||
use App\Domains\Catalog\Models\Variant;
|
use App\Domains\Catalog\Models\Variant;
|
||||||
|
use App\Domains\Catalog\Services\CatalogItemAllowanceService;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Illuminate\Http\Resources\Json\JsonResource;
|
use Illuminate\Http\Resources\Json\JsonResource;
|
||||||
|
|
||||||
@@ -19,6 +20,7 @@ class CatalogFeaturedItemResource extends JsonResource
|
|||||||
$catalogItem = $this->resource;
|
$catalogItem = $this->resource;
|
||||||
/** @var FeaturedGroup $featuredGroup */
|
/** @var FeaturedGroup $featuredGroup */
|
||||||
$featuredGroup = $catalogItem->getRelation('featuredGroup');
|
$featuredGroup = $catalogItem->getRelation('featuredGroup');
|
||||||
|
$remainingUserQuota = $catalogItem->getAttribute('remaining_user_quota');
|
||||||
|
|
||||||
if ($featuredGroup->product_layout === ProductLayout::ColumnWithImage) {
|
if ($featuredGroup->product_layout === ProductLayout::ColumnWithImage) {
|
||||||
return $this->columnWithImageData($catalogItem);
|
return $this->columnWithImageData($catalogItem);
|
||||||
@@ -34,7 +36,10 @@ class CatalogFeaturedItemResource extends JsonResource
|
|||||||
'nombre' => $catalogItem->nombre,
|
'nombre' => $catalogItem->nombre,
|
||||||
'descripcion' => $catalogItem->descripcion,
|
'descripcion' => $catalogItem->descripcion,
|
||||||
'precio' => $catalogItem->precio,
|
'precio' => $catalogItem->precio,
|
||||||
'stock_tecnico' => $catalogItem->availableStock(),
|
'maximum_addable_quantity' => $this->maximumAddable(
|
||||||
|
$catalogItem->availableStock(),
|
||||||
|
$remainingUserQuota,
|
||||||
|
),
|
||||||
'variants' => $catalogItem->visibleVariants()
|
'variants' => $catalogItem->visibleVariants()
|
||||||
->map(fn (Variant $variant): array => [
|
->map(fn (Variant $variant): array => [
|
||||||
'id' => $variant->id,
|
'id' => $variant->id,
|
||||||
@@ -44,9 +49,12 @@ class CatalogFeaturedItemResource extends JsonResource
|
|||||||
'event_dates' => $variant->selectedEventDates()->map(fn ($eventDate): string => $eventDate->date->format('Y-m-d'))->values(),
|
'event_dates' => $variant->selectedEventDates()->map(fn ($eventDate): string => $eventDate->date->format('Y-m-d'))->values(),
|
||||||
'descripcion' => $variant->getDescription(),
|
'descripcion' => $variant->getDescription(),
|
||||||
'precio' => number_format($variant->getPrice(), 2, '.', ''),
|
'precio' => number_format($variant->getPrice(), 2, '.', ''),
|
||||||
'stock_tecnico' => $catalogItem->inventory_policy === InventoryPolicy::Unlimited
|
'maximum_addable_quantity' => $this->maximumAddable(
|
||||||
? null
|
$catalogItem->inventory_policy === InventoryPolicy::Unlimited
|
||||||
: $variant->inventory->availableStock(),
|
? null
|
||||||
|
: $variant->inventory->availableStock(),
|
||||||
|
$remainingUserQuota,
|
||||||
|
),
|
||||||
'values' => $variant->selectorOptions($catalogItem->itemAttributes),
|
'values' => $variant->selectorOptions($catalogItem->itemAttributes),
|
||||||
])
|
])
|
||||||
->values(),
|
->values(),
|
||||||
@@ -89,4 +97,10 @@ class CatalogFeaturedItemResource extends JsonResource
|
|||||||
|
|
||||||
return $attachment?->getTemporaryUrl(1440);
|
return $attachment?->getTemporaryUrl(1440);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function maximumAddable(?int $stock, ?int $remainingUserQuota): ?int
|
||||||
|
{
|
||||||
|
return app(CatalogItemAllowanceService::class)
|
||||||
|
->maximumAddableQuantity($stock, $remainingUserQuota);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ use App\Domains\Catalog\Enums\InventoryPolicy;
|
|||||||
use App\Domains\Catalog\Models\CatalogItem;
|
use App\Domains\Catalog\Models\CatalogItem;
|
||||||
use App\Domains\Catalog\Models\ItemAttribute;
|
use App\Domains\Catalog\Models\ItemAttribute;
|
||||||
use App\Domains\Catalog\Models\Variant;
|
use App\Domains\Catalog\Models\Variant;
|
||||||
|
use App\Domains\Catalog\Services\CatalogItemAllowanceService;
|
||||||
use App\Domains\Shared\Enums\FieldType;
|
use App\Domains\Shared\Enums\FieldType;
|
||||||
use App\Domains\Ticket\Resources\ValidityTimeResource;
|
use App\Domains\Ticket\Resources\ValidityTimeResource;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
@@ -37,9 +38,9 @@ class CatalogItemDetailResource extends JsonResource
|
|||||||
'max_units_per_user' => $this->max_units_per_user,
|
'max_units_per_user' => $this->max_units_per_user,
|
||||||
'has_tickets' => $this->has_tickets,
|
'has_tickets' => $this->has_tickets,
|
||||||
'attributes' => $this->attributesData(),
|
'attributes' => $this->attributesData(),
|
||||||
'stock_tecnico' => $this->when(
|
'maximum_addable_quantity' => $this->when(
|
||||||
$selectedVariant === null,
|
$selectedVariant === null,
|
||||||
fn () => $this->availableStock(),
|
fn () => $this->maximumAddable($this->availableStock()),
|
||||||
),
|
),
|
||||||
'images' => $this->when(
|
'images' => $this->when(
|
||||||
$selectedVariant === null,
|
$selectedVariant === null,
|
||||||
@@ -167,7 +168,7 @@ class CatalogItemDetailResource extends JsonResource
|
|||||||
'event_dates' => $eventDates->map(fn ($eventDate): string => $eventDate->date->format('Y-m-d'))->values(),
|
'event_dates' => $eventDates->map(fn ($eventDate): string => $eventDate->date->format('Y-m-d'))->values(),
|
||||||
'descripcion' => $variant->getDescription(),
|
'descripcion' => $variant->getDescription(),
|
||||||
'precio' => number_format($variant->getPrice(), 2, '.', ''),
|
'precio' => number_format($variant->getPrice(), 2, '.', ''),
|
||||||
'stock_tecnico' => $this->variantStock($variant),
|
'maximum_addable_quantity' => $this->maximumAddable($this->variantStock($variant)),
|
||||||
'values' => $values,
|
'values' => $values,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
@@ -186,4 +187,12 @@ class CatalogItemDetailResource extends JsonResource
|
|||||||
? null
|
? null
|
||||||
: $variant->inventory->availableStock();
|
: $variant->inventory->availableStock();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function maximumAddable(?int $stock): ?int
|
||||||
|
{
|
||||||
|
return app(CatalogItemAllowanceService::class)->maximumAddableQuantity(
|
||||||
|
$stock,
|
||||||
|
$this->getAttribute('remaining_user_quota'),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ namespace App\Domains\Catalog\Resources;
|
|||||||
use App\Domains\Catalog\Enums\InventoryPolicy;
|
use App\Domains\Catalog\Enums\InventoryPolicy;
|
||||||
use App\Domains\Catalog\Models\CatalogItem;
|
use App\Domains\Catalog\Models\CatalogItem;
|
||||||
use App\Domains\Catalog\Models\Variant;
|
use App\Domains\Catalog\Models\Variant;
|
||||||
|
use App\Domains\Catalog\Services\CatalogItemAllowanceService;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Illuminate\Http\Resources\Json\JsonResource;
|
use Illuminate\Http\Resources\Json\JsonResource;
|
||||||
|
|
||||||
@@ -26,7 +27,7 @@ class CatalogSearchItemResource extends JsonResource
|
|||||||
'descripcion' => $this->descripcion,
|
'descripcion' => $this->descripcion,
|
||||||
'precio' => $this->precio,
|
'precio' => $this->precio,
|
||||||
'image' => $attachment?->getTemporaryUrl(1440),
|
'image' => $attachment?->getTemporaryUrl(1440),
|
||||||
'stock_tecnico' => $this->availableStock(),
|
'maximum_addable_quantity' => $this->maximumAddable($this->availableStock()),
|
||||||
'variants' => $this->visibleVariants()
|
'variants' => $this->visibleVariants()
|
||||||
->map(fn (Variant $variant): array => [
|
->map(fn (Variant $variant): array => [
|
||||||
'id' => $variant->id,
|
'id' => $variant->id,
|
||||||
@@ -36,12 +37,22 @@ class CatalogSearchItemResource extends JsonResource
|
|||||||
'event_dates' => $variant->selectedEventDates()->map(fn ($eventDate): string => $eventDate->date->format('Y-m-d'))->values(),
|
'event_dates' => $variant->selectedEventDates()->map(fn ($eventDate): string => $eventDate->date->format('Y-m-d'))->values(),
|
||||||
'descripcion' => $variant->getDescription(),
|
'descripcion' => $variant->getDescription(),
|
||||||
'precio' => number_format($variant->getPrice(), 2, '.', ''),
|
'precio' => number_format($variant->getPrice(), 2, '.', ''),
|
||||||
'stock_tecnico' => $this->inventory_policy === InventoryPolicy::Unlimited
|
'maximum_addable_quantity' => $this->maximumAddable(
|
||||||
? null
|
$this->inventory_policy === InventoryPolicy::Unlimited
|
||||||
: $variant->inventory?->availableStock(),
|
? null
|
||||||
|
: $variant->inventory?->availableStock(),
|
||||||
|
),
|
||||||
'values' => $variant->selectorOptions($this->itemAttributes),
|
'values' => $variant->selectorOptions($this->itemAttributes),
|
||||||
])
|
])
|
||||||
->values(),
|
->values(),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function maximumAddable(?int $stock): ?int
|
||||||
|
{
|
||||||
|
return app(CatalogItemAllowanceService::class)->maximumAddableQuantity(
|
||||||
|
$stock,
|
||||||
|
$this->getAttribute('remaining_user_quota'),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
40
app/Domains/Catalog/Services/CatalogItemAllowanceService.php
Normal file
40
app/Domains/Catalog/Services/CatalogItemAllowanceService.php
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Domains\Catalog\Services;
|
||||||
|
|
||||||
|
use App\Domains\Catalog\Models\CatalogItem;
|
||||||
|
use App\Domains\Purchase\Services\UserPurchaseLimitService;
|
||||||
|
use Illuminate\Support\Collection;
|
||||||
|
|
||||||
|
class CatalogItemAllowanceService
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private readonly UserPurchaseLimitService $purchaseLimits,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
/** @param Collection<int, CatalogItem> $catalogItems */
|
||||||
|
public function attach(Collection $catalogItems, ?int $userId): void
|
||||||
|
{
|
||||||
|
$remaining = $this->purchaseLimits->remainingByCatalogItem($catalogItems, $userId);
|
||||||
|
|
||||||
|
foreach ($catalogItems as $catalogItem) {
|
||||||
|
$catalogItem->setAttribute(
|
||||||
|
'remaining_user_quota',
|
||||||
|
$remaining->get($catalogItem->getKey()),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function maximumAddableQuantity(?int $availableStock, ?int $remainingUserQuota): ?int
|
||||||
|
{
|
||||||
|
if ($availableStock === null) {
|
||||||
|
return $remainingUserQuota;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($remainingUserQuota === null) {
|
||||||
|
return $availableStock;
|
||||||
|
}
|
||||||
|
|
||||||
|
return min($availableStock, $remainingUserQuota);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -15,7 +15,11 @@ class FeaturedGroupService
|
|||||||
private const ITEMS_PER_PAGE = 12;
|
private const ITEMS_PER_PAGE = 12;
|
||||||
|
|
||||||
/** @return array<array-key, mixed> */
|
/** @return array<array-key, mixed> */
|
||||||
public function itemsResponse(FeaturedGroup $featuredGroup, int $page): array
|
public function __construct(
|
||||||
|
private readonly CatalogItemAllowanceService $allowances,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public function itemsResponse(FeaturedGroup $featuredGroup, int $page, ?int $userId = null): array
|
||||||
{
|
{
|
||||||
if ($featuredGroup->group_layout !== GroupLayout::Paginated) {
|
if ($featuredGroup->group_layout !== GroupLayout::Paginated) {
|
||||||
$query = $this->itemsQuery($featuredGroup);
|
$query = $this->itemsQuery($featuredGroup);
|
||||||
@@ -26,12 +30,14 @@ class FeaturedGroupService
|
|||||||
|
|
||||||
$items = $query->get();
|
$items = $query->get();
|
||||||
$this->attachGroup($items, $featuredGroup);
|
$this->attachGroup($items, $featuredGroup);
|
||||||
|
$this->allowances->attach($items, $userId);
|
||||||
|
|
||||||
return CatalogFeaturedItemResource::collection($items)->resolve();
|
return CatalogFeaturedItemResource::collection($items)->resolve();
|
||||||
}
|
}
|
||||||
|
|
||||||
$paginator = $this->paginateItems($featuredGroup, $page);
|
$paginator = $this->paginateItems($featuredGroup, $page);
|
||||||
$this->attachGroup($paginator->getCollection(), $featuredGroup);
|
$this->attachGroup($paginator->getCollection(), $featuredGroup);
|
||||||
|
$this->allowances->attach($paginator->getCollection(), $userId);
|
||||||
|
|
||||||
return CatalogFeaturedItemResource::collection($paginator)
|
return CatalogFeaturedItemResource::collection($paginator)
|
||||||
->response()
|
->response()
|
||||||
|
|||||||
@@ -0,0 +1,31 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Domains\Purchase\Exceptions;
|
||||||
|
|
||||||
|
use App\Domains\Catalog\Models\CatalogItem;
|
||||||
|
use Illuminate\Validation\ValidationException;
|
||||||
|
|
||||||
|
class PurchaseLimitExceededException extends ValidationException
|
||||||
|
{
|
||||||
|
public readonly int $catalogItemId;
|
||||||
|
|
||||||
|
public readonly string $catalogItemName;
|
||||||
|
|
||||||
|
public readonly int $maximumAddableQuantity;
|
||||||
|
|
||||||
|
public function __construct(CatalogItem $catalogItem, int $maximumAddableQuantity, string $field)
|
||||||
|
{
|
||||||
|
$this->catalogItemId = (int) $catalogItem->getKey();
|
||||||
|
$this->catalogItemName = $catalogItem->nombre;
|
||||||
|
$this->maximumAddableQuantity = $maximumAddableQuantity;
|
||||||
|
|
||||||
|
parent::__construct(validator([], []));
|
||||||
|
|
||||||
|
$message = trans_choice('api.purchase_limit.exceeded', $maximumAddableQuantity, [
|
||||||
|
'max' => $maximumAddableQuantity,
|
||||||
|
'product' => $this->catalogItemName,
|
||||||
|
]);
|
||||||
|
$this->message = $message;
|
||||||
|
$this->validator->errors()->add($field, $message);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -120,10 +120,16 @@ class StartCheckoutService
|
|||||||
->each(function (Collection $catalogLines) use ($userId): void {
|
->each(function (Collection $catalogLines) use ($userId): void {
|
||||||
/** @var CatalogItem $catalogItem */
|
/** @var CatalogItem $catalogItem */
|
||||||
$catalogItem = $catalogLines->first()['catalog_item'];
|
$catalogItem = $catalogLines->first()['catalog_item'];
|
||||||
|
$availableQuantities = $catalogLines
|
||||||
|
->map(fn (array $line): ?int => $this->inventory->availableQuantity($line['selection']));
|
||||||
|
$maximumAddableCeiling = $availableQuantities->contains(null)
|
||||||
|
? null
|
||||||
|
: (int) $availableQuantities->sum();
|
||||||
$this->purchaseLimits->assertCanPurchase(
|
$this->purchaseLimits->assertCanPurchase(
|
||||||
$catalogItem,
|
$catalogItem,
|
||||||
$userId,
|
$userId,
|
||||||
(int) $catalogLines->sum('quantity'),
|
(int) $catalogLines->sum('quantity'),
|
||||||
|
maximumAddableCeiling: $maximumAddableCeiling,
|
||||||
field: 'direct_items',
|
field: 'direct_items',
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@@ -243,7 +249,7 @@ class StartCheckoutService
|
|||||||
|
|
||||||
$this->loadCartItems($cartItems);
|
$this->loadCartItems($cartItems);
|
||||||
$this->verifyTenantItems($tenant, $cartItems);
|
$this->verifyTenantItems($tenant, $cartItems);
|
||||||
$this->assertCartPurchaseLimits($tenant, $userId, $cartItems);
|
$this->assertCartPurchaseLimits($tenant, $userId, $cartItems, $cart->getKey());
|
||||||
$cart->setRelation('items', $cartItems);
|
$cart->setRelation('items', $cartItems);
|
||||||
|
|
||||||
$purchase = $this->createPurchase(
|
$purchase = $this->createPurchase(
|
||||||
@@ -312,6 +318,7 @@ class StartCheckoutService
|
|||||||
Tenant $tenant,
|
Tenant $tenant,
|
||||||
int $userId,
|
int $userId,
|
||||||
Collection $cartItems,
|
Collection $cartItems,
|
||||||
|
int $cartId,
|
||||||
): void {
|
): void {
|
||||||
$quantities = $cartItems
|
$quantities = $cartItems
|
||||||
->groupBy('catalog_item_id')
|
->groupBy('catalog_item_id')
|
||||||
@@ -333,6 +340,8 @@ class StartCheckoutService
|
|||||||
$catalogItem,
|
$catalogItem,
|
||||||
$userId,
|
$userId,
|
||||||
$quantity,
|
$quantity,
|
||||||
|
excludedCartId: $cartId,
|
||||||
|
heldQuantity: $quantity,
|
||||||
field: 'cart_id',
|
field: 'cart_id',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,10 +4,11 @@ namespace App\Domains\Purchase\Services;
|
|||||||
|
|
||||||
use App\Domains\Cart\Models\CartItem;
|
use App\Domains\Cart\Models\CartItem;
|
||||||
use App\Domains\Catalog\Models\CatalogItem;
|
use App\Domains\Catalog\Models\CatalogItem;
|
||||||
|
use App\Domains\Purchase\Exceptions\PurchaseLimitExceededException;
|
||||||
use App\Domains\Purchase\Models\Purchase;
|
use App\Domains\Purchase\Models\Purchase;
|
||||||
use App\Domains\Purchase\Models\PurchaseItem;
|
use App\Domains\Purchase\Models\PurchaseItem;
|
||||||
|
use Illuminate\Support\Collection;
|
||||||
use Illuminate\Support\Facades\DB;
|
use Illuminate\Support\Facades\DB;
|
||||||
use Illuminate\Validation\ValidationException;
|
|
||||||
|
|
||||||
class UserPurchaseLimitService
|
class UserPurchaseLimitService
|
||||||
{
|
{
|
||||||
@@ -16,6 +17,9 @@ class UserPurchaseLimitService
|
|||||||
int $userId,
|
int $userId,
|
||||||
int $requestedQuantity,
|
int $requestedQuantity,
|
||||||
?int $excludedPurchaseId = null,
|
?int $excludedPurchaseId = null,
|
||||||
|
?int $excludedCartId = null,
|
||||||
|
int $heldQuantity = 0,
|
||||||
|
?int $maximumAddableCeiling = null,
|
||||||
string $field = 'quantity',
|
string $field = 'quantity',
|
||||||
): void {
|
): void {
|
||||||
DB::transaction(function () use (
|
DB::transaction(function () use (
|
||||||
@@ -23,6 +27,9 @@ class UserPurchaseLimitService
|
|||||||
$userId,
|
$userId,
|
||||||
$requestedQuantity,
|
$requestedQuantity,
|
||||||
$excludedPurchaseId,
|
$excludedPurchaseId,
|
||||||
|
$excludedCartId,
|
||||||
|
$heldQuantity,
|
||||||
|
$maximumAddableCeiling,
|
||||||
$field,
|
$field,
|
||||||
): void {
|
): void {
|
||||||
/** @var CatalogItem $catalogItem */
|
/** @var CatalogItem $catalogItem */
|
||||||
@@ -70,11 +77,95 @@ class UserPurchaseLimitService
|
|||||||
})
|
})
|
||||||
->sum('cantidad');
|
->sum('cantidad');
|
||||||
|
|
||||||
if ($purchasedQuantity + $checkoutQuantity + $requestedQuantity > $limit) {
|
$reservedCartQuantity = (int) CartItem::query()
|
||||||
throw ValidationException::withMessages([
|
->where('catalog_item_id', $catalogItem->getKey())
|
||||||
$field => __('api.purchase_limit.exceeded', ['max' => $limit]),
|
->whereHas('cart', fn ($query) => $query
|
||||||
]);
|
->where('user_id', $userId)
|
||||||
|
->where('status', 'active')
|
||||||
|
->when(
|
||||||
|
$excludedCartId !== null,
|
||||||
|
fn ($query) => $query->whereKeyNot($excludedCartId),
|
||||||
|
))
|
||||||
|
->whereHas('stockReservations', fn ($query) => $query->where('status', 'active'))
|
||||||
|
->sum('cantidad');
|
||||||
|
|
||||||
|
if ($purchasedQuantity + $checkoutQuantity + $reservedCartQuantity + $requestedQuantity > $limit) {
|
||||||
|
$remainingQuota = max(
|
||||||
|
0,
|
||||||
|
$limit - $purchasedQuantity - $checkoutQuantity - $reservedCartQuantity,
|
||||||
|
);
|
||||||
|
|
||||||
|
$maximumAddableQuantity = max(0, $remainingQuota - $heldQuantity);
|
||||||
|
|
||||||
|
throw new PurchaseLimitExceededException(
|
||||||
|
$catalogItem,
|
||||||
|
$maximumAddableCeiling === null
|
||||||
|
? $maximumAddableQuantity
|
||||||
|
: min($maximumAddableQuantity, $maximumAddableCeiling),
|
||||||
|
$field,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param Collection<int, CatalogItem> $catalogItems
|
||||||
|
* @return Collection<int, int|null>
|
||||||
|
*/
|
||||||
|
public function remainingByCatalogItem(Collection $catalogItems, ?int $userId): Collection
|
||||||
|
{
|
||||||
|
$limits = $catalogItems
|
||||||
|
->unique('id')
|
||||||
|
->mapWithKeys(fn (CatalogItem $item): array => [$item->getKey() => $item->max_units_per_user]);
|
||||||
|
|
||||||
|
if ($userId === null || $limits->filter(fn ($limit) => $limit !== null)->isEmpty()) {
|
||||||
|
return $limits->map(fn (): ?int => null);
|
||||||
|
}
|
||||||
|
|
||||||
|
$ids = $limits->keys();
|
||||||
|
$purchased = PurchaseItem::query()
|
||||||
|
->selectRaw('source_catalog_item_id, SUM(cantidad) AS quantity')
|
||||||
|
->whereIn('source_catalog_item_id', $ids)
|
||||||
|
->whereHas('purchase', fn ($query) => $query
|
||||||
|
->where('user_id', $userId)
|
||||||
|
->whereIn('status', [
|
||||||
|
Purchase::STATUS_CREATED,
|
||||||
|
Purchase::STATUS_PENDING_PAYMENT,
|
||||||
|
Purchase::STATUS_PAID,
|
||||||
|
]))
|
||||||
|
->groupBy('source_catalog_item_id')
|
||||||
|
->pluck('quantity', 'source_catalog_item_id');
|
||||||
|
|
||||||
|
$checkout = CartItem::query()
|
||||||
|
->selectRaw('catalog_item_id, SUM(cantidad) AS quantity')
|
||||||
|
->whereIn('catalog_item_id', $ids)
|
||||||
|
->whereHas('cart.purchases', fn ($query) => $query
|
||||||
|
->where('user_id', $userId)
|
||||||
|
->whereIn('status', [Purchase::STATUS_CREATED, Purchase::STATUS_PENDING_PAYMENT])
|
||||||
|
->whereDoesntHave('items'))
|
||||||
|
->groupBy('catalog_item_id')
|
||||||
|
->pluck('quantity', 'catalog_item_id');
|
||||||
|
|
||||||
|
$reserved = CartItem::query()
|
||||||
|
->selectRaw('catalog_item_id, SUM(cantidad) AS quantity')
|
||||||
|
->whereIn('catalog_item_id', $ids)
|
||||||
|
->whereHas('cart', fn ($query) => $query
|
||||||
|
->where('user_id', $userId)
|
||||||
|
->where('status', 'active'))
|
||||||
|
->whereHas('stockReservations', fn ($query) => $query->where('status', 'active'))
|
||||||
|
->groupBy('catalog_item_id')
|
||||||
|
->pluck('quantity', 'catalog_item_id');
|
||||||
|
|
||||||
|
return $limits->map(function (?int $limit, int $catalogItemId) use ($purchased, $checkout, $reserved): ?int {
|
||||||
|
if ($limit === null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$used = (int) ($purchased[$catalogItemId] ?? 0)
|
||||||
|
+ (int) ($checkout[$catalogItemId] ?? 0)
|
||||||
|
+ (int) ($reserved[$catalogItemId] ?? 0);
|
||||||
|
|
||||||
|
return max(0, $limit - $used);
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
use App\Domains\Auth\Exceptions\AccountLockedException;
|
use App\Domains\Auth\Exceptions\AccountLockedException;
|
||||||
use App\Domains\Purchase\Exceptions\InsufficientStockException;
|
use App\Domains\Purchase\Exceptions\InsufficientStockException;
|
||||||
|
use App\Domains\Purchase\Exceptions\PurchaseLimitExceededException;
|
||||||
use App\Domains\Ticket\Exceptions\TicketNotAvailableException;
|
use App\Domains\Ticket\Exceptions\TicketNotAvailableException;
|
||||||
use App\Http\Middleware\EnsureAdminAppTenant;
|
use App\Http\Middleware\EnsureAdminAppTenant;
|
||||||
use App\Http\Middleware\EnsureScannerTenant;
|
use App\Http\Middleware\EnsureScannerTenant;
|
||||||
@@ -87,6 +88,20 @@ return Application::configure(basePath: dirname(__DIR__))
|
|||||||
'unavailable_items' => $exception->unavailableItems,
|
'unavailable_items' => $exception->unavailableItems,
|
||||||
], 422);
|
], 422);
|
||||||
});
|
});
|
||||||
|
$exceptions->render(function (PurchaseLimitExceededException $exception, Request $request) {
|
||||||
|
if (! $request->is('api/*')) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return response()->json([
|
||||||
|
'code' => 'purchase.limit_exceeded',
|
||||||
|
'message' => $exception->getMessage(),
|
||||||
|
'errors' => $exception->errors(),
|
||||||
|
'catalog_item_id' => $exception->catalogItemId,
|
||||||
|
'catalog_item_name' => $exception->catalogItemName,
|
||||||
|
'maximum_addable_quantity' => $exception->maximumAddableQuantity,
|
||||||
|
], 422);
|
||||||
|
});
|
||||||
$exceptions->render(function (ModelNotFoundException $exception, Request $request) {
|
$exceptions->render(function (ModelNotFoundException $exception, Request $request) {
|
||||||
if (! $request->is('api/*')) {
|
if (! $request->is('api/*')) {
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ return [
|
|||||||
'not_available_for_review' => 'The purchase is no longer available for review.',
|
'not_available_for_review' => 'The purchase is no longer available for review.',
|
||||||
],
|
],
|
||||||
'purchase_limit' => [
|
'purchase_limit' => [
|
||||||
'exceeded' => 'You can purchase up to :max units of this product.',
|
'exceeded' => '{0} You cannot add more units of “:product”.|{1} You can add up to :max more unit of “:product”.|[2,*] You can add up to :max more units of “:product”.',
|
||||||
],
|
],
|
||||||
'ticket' => [
|
'ticket' => [
|
||||||
'not_available' => 'One or more tickets are not available.',
|
'not_available' => 'One or more tickets are not available.',
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ return [
|
|||||||
'not_available_for_review' => "La compra ya no est\u{00E1} disponible para revisi\u{00F3}n.",
|
'not_available_for_review' => "La compra ya no est\u{00E1} disponible para revisi\u{00F3}n.",
|
||||||
],
|
],
|
||||||
'purchase_limit' => [
|
'purchase_limit' => [
|
||||||
'exceeded' => 'Podés comprar hasta :max unidades de este producto.',
|
'exceeded' => '{0} No podés agregar más unidades de “:product”.|{1} Podés agregar hasta :max unidad más de “:product”.|[2,*] Podés agregar hasta :max unidades más de “:product”.',
|
||||||
],
|
],
|
||||||
'ticket' => [
|
'ticket' => [
|
||||||
'not_available' => 'Uno o más tickets no están disponibles.',
|
'not_available' => 'Uno o más tickets no están disponibles.',
|
||||||
|
|||||||
@@ -241,7 +241,15 @@ class CartControllerTest extends TestCase
|
|||||||
'cantidad' => 2,
|
'cantidad' => 2,
|
||||||
])
|
])
|
||||||
->assertUnprocessable()
|
->assertUnprocessable()
|
||||||
->assertJsonValidationErrors('cantidad');
|
->assertJsonValidationErrors('cantidad')
|
||||||
|
->assertJsonPath('code', 'purchase.limit_exceeded')
|
||||||
|
->assertJsonPath('catalog_item_id', $item->id)
|
||||||
|
->assertJsonPath('catalog_item_name', $item->nombre)
|
||||||
|
->assertJsonPath('maximum_addable_quantity', 1)
|
||||||
|
->assertJsonPath(
|
||||||
|
'message',
|
||||||
|
"Podés agregar hasta 1 unidad más de “{$item->nombre}”.",
|
||||||
|
);
|
||||||
|
|
||||||
$this->assertDatabaseHas('carrito_items', [
|
$this->assertDatabaseHas('carrito_items', [
|
||||||
'catalog_item_id' => $item->id,
|
'catalog_item_id' => $item->id,
|
||||||
|
|||||||
@@ -267,7 +267,8 @@ class BundleCatalogItemTest extends TestCase
|
|||||||
$this->getJson("/api/tenants/{$this->tenant->codigo}/catalog-items/{$bundleId}")
|
$this->getJson("/api/tenants/{$this->tenant->codigo}/catalog-items/{$bundleId}")
|
||||||
->assertOk()
|
->assertOk()
|
||||||
->assertJsonPath('data.type', CatalogItemType::Bundle->value)
|
->assertJsonPath('data.type', CatalogItemType::Bundle->value)
|
||||||
->assertJsonPath('data.stock_tecnico', 4)
|
->assertJsonPath('data.maximum_addable_quantity', 4)
|
||||||
|
->assertJsonMissingPath('data.stock_tecnico')
|
||||||
->assertJsonCount(1, 'data.components')
|
->assertJsonCount(1, 'data.components')
|
||||||
->assertJsonPath('data.components.0.catalog_item_id', $component->id)
|
->assertJsonPath('data.components.0.catalog_item_id', $component->id)
|
||||||
->assertJsonPath('data.components.0.variant_id', null)
|
->assertJsonPath('data.components.0.variant_id', null)
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ 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\Auth\Models\User;
|
||||||
|
use App\Domains\Cart\Models\Cart;
|
||||||
use App\Domains\Catalog\Enums\FeaturedGroupSource;
|
use App\Domains\Catalog\Enums\FeaturedGroupSource;
|
||||||
use App\Domains\Catalog\Enums\GroupLayout;
|
use App\Domains\Catalog\Enums\GroupLayout;
|
||||||
use App\Domains\Catalog\Enums\ProductLayout;
|
use App\Domains\Catalog\Enums\ProductLayout;
|
||||||
@@ -20,7 +22,7 @@ class CatalogControllerTest extends TestCase
|
|||||||
{
|
{
|
||||||
use RefreshDatabase;
|
use RefreshDatabase;
|
||||||
|
|
||||||
public function test_row_and_column_with_cart_return_item_details_variants_and_technical_stock(): void
|
public function test_row_and_column_with_cart_return_item_details_variants_and_maximum_quantity(): void
|
||||||
{
|
{
|
||||||
$tenant = $this->createTenant('catalog-index');
|
$tenant = $this->createTenant('catalog-index');
|
||||||
$row = $this->createGroup($tenant, ProductLayout::Row, 'Row', 2);
|
$row = $this->createGroup($tenant, ProductLayout::Row, 'Row', 2);
|
||||||
@@ -70,16 +72,54 @@ class CatalogControllerTest extends TestCase
|
|||||||
->assertJsonPath('0.items.0.nombre', 'Variants')
|
->assertJsonPath('0.items.0.nombre', 'Variants')
|
||||||
->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.stock_tecnico', 7)
|
->assertJsonPath('0.items.0.maximum_addable_quantity', 7)
|
||||||
->assertJsonCount(2, '0.items.0.variants')
|
->assertJsonCount(2, '0.items.0.variants')
|
||||||
->assertJsonPath('0.items.0.variants.0.stock_tecnico', 4)
|
->assertJsonPath('0.items.0.variants.0.maximum_addable_quantity', 4)
|
||||||
->assertJsonPath('0.items.0.variants.1.stock_tecnico', 3)
|
->assertJsonPath('0.items.0.variants.1.maximum_addable_quantity', 3)
|
||||||
->assertJsonMissing(['id' => $unavailableVariant->id, 'stock_tecnico' => 0])
|
->assertJsonMissing(['id' => $unavailableVariant->id])
|
||||||
->assertJsonPath('1.title', 'Row')
|
->assertJsonPath('1.title', 'Row')
|
||||||
->assertJsonPath('1.items.data.0.stock_tecnico', 8)
|
->assertJsonPath('1.items.data.0.maximum_addable_quantity', 8)
|
||||||
|
->assertJsonMissingPath('1.items.data.0.stock_tecnico')
|
||||||
->assertJsonCount(0, '1.items.data.0.variants');
|
->assertJsonCount(0, '1.items.data.0.variants');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function test_maximum_addable_quantity_shares_the_authenticated_user_quota_between_variants(): void
|
||||||
|
{
|
||||||
|
$tenant = $this->createTenant('catalog-allowance');
|
||||||
|
$group = $this->createGroup(
|
||||||
|
$tenant,
|
||||||
|
ProductLayout::ColumnWithCart,
|
||||||
|
'Allowances',
|
||||||
|
groupLayout: GroupLayout::Simple,
|
||||||
|
);
|
||||||
|
$user = User::factory()->create();
|
||||||
|
$item = $this->createItem($tenant, 'Limited variants');
|
||||||
|
$item->update(['max_units_per_user' => 5]);
|
||||||
|
$firstVariant = $item->variants()->create([
|
||||||
|
'inventory_id' => Inventory::query()->create(['real_stock' => 10])->id,
|
||||||
|
]);
|
||||||
|
$secondVariant = $item->variants()->create([
|
||||||
|
'inventory_id' => Inventory::query()->create(['real_stock' => 10])->id,
|
||||||
|
]);
|
||||||
|
$group->featuredItems()->create(['catalog_item_id' => $item->id]);
|
||||||
|
|
||||||
|
$cart = Cart::query()->create([
|
||||||
|
'tenant_codigo' => $tenant->codigo,
|
||||||
|
'user_id' => $user->id,
|
||||||
|
'status' => 'active',
|
||||||
|
]);
|
||||||
|
$cart->addItem($item->id, $firstVariant->id, 2);
|
||||||
|
$cart->addItem($item->id, $secondVariant->id, 1);
|
||||||
|
|
||||||
|
$this->actingAs($user, 'sanctum')
|
||||||
|
->getJson("/api/tenants/{$tenant->codigo}/catalog")
|
||||||
|
->assertOk()
|
||||||
|
->assertJsonPath('0.items.0.variants.0.maximum_addable_quantity', 2)
|
||||||
|
->assertJsonPath('0.items.0.variants.1.maximum_addable_quantity', 2)
|
||||||
|
->assertJsonMissingPath('0.items.0.variants.0.stock_tecnico')
|
||||||
|
->assertJsonMissingPath('0.items.0.variants.1.stock_tecnico');
|
||||||
|
}
|
||||||
|
|
||||||
public function test_it_excludes_items_when_all_of_their_variants_are_out_of_stock(): void
|
public function test_it_excludes_items_when_all_of_their_variants_are_out_of_stock(): void
|
||||||
{
|
{
|
||||||
$tenant = $this->createTenant('catalog-available-variants');
|
$tenant = $this->createTenant('catalog-available-variants');
|
||||||
|
|||||||
@@ -40,7 +40,8 @@ class CatalogItemDetailControllerTest extends TestCase
|
|||||||
|
|
||||||
$response
|
$response
|
||||||
->assertOk()
|
->assertOk()
|
||||||
->assertJsonPath('data.stock_tecnico', 7)
|
->assertJsonPath('data.maximum_addable_quantity', 7)
|
||||||
|
->assertJsonMissingPath('data.stock_tecnico')
|
||||||
->assertJsonCount(0, 'data.variants')
|
->assertJsonCount(0, 'data.variants')
|
||||||
->assertJsonCount(1, 'data.images');
|
->assertJsonCount(1, 'data.images');
|
||||||
$response->assertJsonMissingPath('data.selected_variant');
|
$response->assertJsonMissingPath('data.selected_variant');
|
||||||
@@ -71,7 +72,8 @@ class CatalogItemDetailControllerTest extends TestCase
|
|||||||
->assertJsonCount(1, 'data.variants')
|
->assertJsonCount(1, 'data.variants')
|
||||||
->assertJsonPath('data.variants.0.id', $secondVariant->id)
|
->assertJsonPath('data.variants.0.id', $secondVariant->id)
|
||||||
->assertJsonPath('data.selected_variant.id', $secondVariant->id)
|
->assertJsonPath('data.selected_variant.id', $secondVariant->id)
|
||||||
->assertJsonPath('data.selected_variant.stock_tecnico', 6)
|
->assertJsonPath('data.selected_variant.maximum_addable_quantity', 6)
|
||||||
|
->assertJsonMissingPath('data.selected_variant.stock_tecnico')
|
||||||
->assertJsonCount(1, 'data.selected_variant.images');
|
->assertJsonCount(1, 'data.selected_variant.images');
|
||||||
$response
|
$response
|
||||||
->assertJsonMissingPath('data.stock_tecnico')
|
->assertJsonMissingPath('data.stock_tecnico')
|
||||||
@@ -125,11 +127,11 @@ class CatalogItemDetailControllerTest extends TestCase
|
|||||||
$response
|
$response
|
||||||
->assertOk()
|
->assertOk()
|
||||||
->assertJsonPath('data.variants.0.id', $firstVariant->id)
|
->assertJsonPath('data.variants.0.id', $firstVariant->id)
|
||||||
->assertJsonPath('data.variants.0.stock_tecnico', 4)
|
->assertJsonPath('data.variants.0.maximum_addable_quantity', 4)
|
||||||
->assertJsonPath('data.variants.0.values.size.value', 'S')
|
->assertJsonPath('data.variants.0.values.size.value', 'S')
|
||||||
->assertJsonPath('data.variants.0.values.size.label', 'Small')
|
->assertJsonPath('data.variants.0.values.size.label', 'Small')
|
||||||
->assertJsonPath('data.variants.1.id', $secondVariant->id)
|
->assertJsonPath('data.variants.1.id', $secondVariant->id)
|
||||||
->assertJsonPath('data.variants.1.stock_tecnico', 7)
|
->assertJsonPath('data.variants.1.maximum_addable_quantity', 7)
|
||||||
->assertJsonPath('data.variants.1.values.size.value', 'M')
|
->assertJsonPath('data.variants.1.values.size.value', 'M')
|
||||||
->assertJsonPath('data.variants.1.values.size.label', 'Medium')
|
->assertJsonPath('data.variants.1.values.size.label', 'Medium')
|
||||||
->assertJsonPath('data.attributes.0.codigo', 'size')
|
->assertJsonPath('data.attributes.0.codigo', 'size')
|
||||||
@@ -137,7 +139,7 @@ class CatalogItemDetailControllerTest extends TestCase
|
|||||||
->assertJsonPath('data.attributes.0.options.1.value', 'M')
|
->assertJsonPath('data.attributes.0.options.1.value', 'M')
|
||||||
->assertJsonCount(2, 'data.attributes.0.options')
|
->assertJsonCount(2, 'data.attributes.0.options')
|
||||||
->assertJsonPath('data.selected_variant.id', $secondVariant->id)
|
->assertJsonPath('data.selected_variant.id', $secondVariant->id)
|
||||||
->assertJsonPath('data.selected_variant.stock_tecnico', 7)
|
->assertJsonPath('data.selected_variant.maximum_addable_quantity', 7)
|
||||||
->assertJsonPath('data.selected_variant.values.size.value', 'M')
|
->assertJsonPath('data.selected_variant.values.size.value', 'M')
|
||||||
->assertJsonPath('data.selected_variant.values.size.label', 'Medium')
|
->assertJsonPath('data.selected_variant.values.size.label', 'Medium')
|
||||||
->assertJsonCount(1, 'data.selected_variant.images');
|
->assertJsonCount(1, 'data.selected_variant.images');
|
||||||
@@ -177,9 +179,11 @@ class CatalogItemDetailControllerTest extends TestCase
|
|||||||
$this->getJson("/api/tenants/{$tenant->codigo}/catalog-items/{$item->id}")
|
$this->getJson("/api/tenants/{$tenant->codigo}/catalog-items/{$item->id}")
|
||||||
->assertOk()
|
->assertOk()
|
||||||
->assertJsonPath('data.selected_variant.id', $variant->id)
|
->assertJsonPath('data.selected_variant.id', $variant->id)
|
||||||
->assertJsonPath('data.selected_variant.stock_tecnico', null)
|
->assertJsonPath('data.selected_variant.maximum_addable_quantity', null)
|
||||||
->assertJsonMissingPath('data.stock_tecnico')
|
->assertJsonMissingPath('data.stock_tecnico')
|
||||||
->assertJsonPath('data.variants.0.stock_tecnico', null);
|
->assertJsonPath('data.variants.0.maximum_addable_quantity', null)
|
||||||
|
->assertJsonMissingPath('data.selected_variant.stock_tecnico')
|
||||||
|
->assertJsonMissingPath('data.variants.0.stock_tecnico');
|
||||||
}
|
}
|
||||||
|
|
||||||
public function test_it_exposes_event_dates_as_a_dynamic_variant_attribute(): void
|
public function test_it_exposes_event_dates_as_a_dynamic_variant_attribute(): void
|
||||||
|
|||||||
@@ -340,7 +340,11 @@ class StorePurchaseTest extends TestCase
|
|||||||
],
|
],
|
||||||
])
|
])
|
||||||
->assertUnprocessable()
|
->assertUnprocessable()
|
||||||
->assertJsonValidationErrors('direct_items');
|
->assertJsonValidationErrors('direct_items')
|
||||||
|
->assertJsonPath('code', 'purchase.limit_exceeded')
|
||||||
|
->assertJsonPath('catalog_item_id', $variant->catalog_item_id)
|
||||||
|
->assertJsonPath('catalog_item_name', $variant->catalogItem->nombre)
|
||||||
|
->assertJsonPath('maximum_addable_quantity', 1);
|
||||||
|
|
||||||
$this->actingAs($otherUser, 'sanctum')
|
$this->actingAs($otherUser, 'sanctum')
|
||||||
->postJson('/api/tenants/sonder/compras/start-checkout', [
|
->postJson('/api/tenants/sonder/compras/start-checkout', [
|
||||||
|
|||||||
Reference in New Issue
Block a user