feat(catalog): scope availability policies by subject
This commit is contained in:
9
app/Domains/Catalog/Enums/AvailabilitySubject.php
Normal file
9
app/Domains/Catalog/Enums/AvailabilitySubject.php
Normal file
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domains\Catalog\Enums;
|
||||
|
||||
enum AvailabilitySubject: string
|
||||
{
|
||||
case Product = 'product';
|
||||
case Variant = 'variant';
|
||||
}
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
namespace App\Domains\Catalog\Resources;
|
||||
|
||||
use App\Domains\Catalog\Enums\AvailabilitySubject;
|
||||
use App\Domains\Catalog\Enums\InventoryPolicy;
|
||||
use App\Domains\Catalog\Enums\ProductLayout;
|
||||
use App\Domains\Catalog\Models\CatalogItem;
|
||||
@@ -47,6 +48,11 @@ class CatalogFeaturedItemResource extends JsonResource
|
||||
$variantStock = $catalogItem->inventory_policy === InventoryPolicy::Unlimited
|
||||
? null
|
||||
: $variant->inventory->availableStock();
|
||||
$availability = $this->availability(
|
||||
$variantStock,
|
||||
$remainingUserQuota,
|
||||
AvailabilitySubject::Variant,
|
||||
);
|
||||
|
||||
return [
|
||||
'id' => $variant->id,
|
||||
@@ -56,14 +62,11 @@ class CatalogFeaturedItemResource extends JsonResource
|
||||
'event_dates' => $variant->selectedEventDates()->map(fn ($eventDate): string => $eventDate->date->format('Y-m-d'))->values(),
|
||||
'descripcion' => $variant->getDescription(),
|
||||
'precio' => number_format($variant->getPrice(), 2, '.', ''),
|
||||
'availability' => $this->availability(
|
||||
$variantStock,
|
||||
$remainingUserQuota,
|
||||
'variant',
|
||||
),
|
||||
'availability' => $availability,
|
||||
'values' => $variant->selectorOptions($catalogItem->itemAttributes),
|
||||
];
|
||||
})
|
||||
->filter(fn (array $variant): bool => $variant['availability']['capabilities']['display'])
|
||||
->values(),
|
||||
];
|
||||
|
||||
@@ -117,9 +120,9 @@ class CatalogFeaturedItemResource extends JsonResource
|
||||
private function availability(
|
||||
?int $stock,
|
||||
?int $remainingUserQuota,
|
||||
string $scope = 'product',
|
||||
AvailabilitySubject $subject = AvailabilitySubject::Product,
|
||||
): array {
|
||||
return app(CatalogItemAllowanceService::class)
|
||||
->availability($stock, $remainingUserQuota, $scope);
|
||||
->availability($stock, $remainingUserQuota, $subject);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
namespace App\Domains\Catalog\Resources;
|
||||
|
||||
use App\Domains\Catalog\Enums\AvailabilitySubject;
|
||||
use App\Domains\Catalog\Enums\InventoryPolicy;
|
||||
use App\Domains\Catalog\Models\CatalogItem;
|
||||
use App\Domains\Catalog\Models\ItemAttribute;
|
||||
@@ -45,6 +46,7 @@ class CatalogItemDetailResource extends JsonResource
|
||||
),
|
||||
'variants' => $this->variants
|
||||
->map(fn (Variant $variant): array => $this->variantData($variant))
|
||||
->filter(fn (array $variant): bool => $variant['availability']['capabilities']['display'])
|
||||
->values(),
|
||||
'selected_variant' => $this->when(
|
||||
$selectedVariant !== null,
|
||||
@@ -157,6 +159,10 @@ class CatalogItemDetailResource extends JsonResource
|
||||
$values = $variant->selectionOptions($this->itemAttributes);
|
||||
$eventDates = $variant->selectedEventDates();
|
||||
$variantStock = $this->variantStock($variant);
|
||||
$availability = $this->availability(
|
||||
$variantStock,
|
||||
AvailabilitySubject::Variant,
|
||||
);
|
||||
|
||||
return [
|
||||
'id' => $variant->id,
|
||||
@@ -166,7 +172,7 @@ class CatalogItemDetailResource extends JsonResource
|
||||
'event_dates' => $eventDates->map(fn ($eventDate): string => $eventDate->date->format('Y-m-d'))->values(),
|
||||
'descripcion' => $variant->getDescription(),
|
||||
'precio' => number_format($variant->getPrice(), 2, '.', ''),
|
||||
'availability' => $this->availability($variantStock, 'variant'),
|
||||
'availability' => $availability,
|
||||
'values' => $values,
|
||||
];
|
||||
}
|
||||
@@ -187,12 +193,14 @@ class CatalogItemDetailResource extends JsonResource
|
||||
}
|
||||
|
||||
/** @return array<string, mixed> */
|
||||
private function availability(?int $stock, string $scope = 'product'): array
|
||||
{
|
||||
private function availability(
|
||||
?int $stock,
|
||||
AvailabilitySubject $subject = AvailabilitySubject::Product,
|
||||
): array {
|
||||
return app(CatalogItemAllowanceService::class)->availability(
|
||||
$stock,
|
||||
$this->getAttribute('remaining_user_quota'),
|
||||
$scope,
|
||||
$subject,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
namespace App\Domains\Catalog\Resources;
|
||||
|
||||
use App\Domains\Catalog\Enums\AvailabilitySubject;
|
||||
use App\Domains\Catalog\Enums\InventoryPolicy;
|
||||
use App\Domains\Catalog\Models\CatalogItem;
|
||||
use App\Domains\Catalog\Models\Variant;
|
||||
@@ -34,6 +35,10 @@ class CatalogSearchItemResource extends JsonResource
|
||||
$variantStock = $this->inventory_policy === InventoryPolicy::Unlimited
|
||||
? null
|
||||
: $variant->inventory?->availableStock();
|
||||
$availability = $this->availability(
|
||||
$variantStock,
|
||||
AvailabilitySubject::Variant,
|
||||
);
|
||||
|
||||
return [
|
||||
'id' => $variant->id,
|
||||
@@ -43,21 +48,24 @@ class CatalogSearchItemResource extends JsonResource
|
||||
'event_dates' => $variant->selectedEventDates()->map(fn ($eventDate): string => $eventDate->date->format('Y-m-d'))->values(),
|
||||
'descripcion' => $variant->getDescription(),
|
||||
'precio' => number_format($variant->getPrice(), 2, '.', ''),
|
||||
'availability' => $this->availability($variantStock, 'variant'),
|
||||
'availability' => $availability,
|
||||
'values' => $variant->selectorOptions($this->itemAttributes),
|
||||
];
|
||||
})
|
||||
->filter(fn (array $variant): bool => $variant['availability']['capabilities']['display'])
|
||||
->values(),
|
||||
];
|
||||
}
|
||||
|
||||
/** @return array<string, mixed> */
|
||||
private function availability(?int $stock, string $scope = 'product'): array
|
||||
{
|
||||
private function availability(
|
||||
?int $stock,
|
||||
AvailabilitySubject $subject = AvailabilitySubject::Product,
|
||||
): array {
|
||||
return app(CatalogItemAllowanceService::class)->availability(
|
||||
$stock,
|
||||
$this->getAttribute('remaining_user_quota'),
|
||||
$scope,
|
||||
$subject,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,8 +2,10 @@
|
||||
|
||||
namespace App\Domains\Catalog\Services;
|
||||
|
||||
use App\Domains\Catalog\Enums\AvailabilitySubject;
|
||||
use App\Domains\Catalog\Models\CatalogItem;
|
||||
use App\Domains\Purchase\Services\UserPurchaseLimitService;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
class CatalogItemAllowanceService
|
||||
@@ -42,17 +44,42 @@ class CatalogItemAllowanceService
|
||||
return min($availableStock, $remainingUserQuota);
|
||||
}
|
||||
|
||||
/** @param Builder<CatalogItem> $query */
|
||||
public function applyProductVisibilityPolicy(Builder $query): Builder
|
||||
{
|
||||
return $this->isVisible(
|
||||
0,
|
||||
null,
|
||||
AvailabilitySubject::Product,
|
||||
)
|
||||
? $query
|
||||
: $query->whereAvailableInCatalog();
|
||||
}
|
||||
|
||||
public function isVisible(
|
||||
?int $availableStock,
|
||||
?int $remainingUserQuota,
|
||||
AvailabilitySubject $subject,
|
||||
): bool {
|
||||
return $this->availability(
|
||||
$availableStock,
|
||||
$remainingUserQuota,
|
||||
$subject,
|
||||
)['capabilities']['display'];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{
|
||||
* subject: string,
|
||||
* maximum_quantity: int|null,
|
||||
* restrictions: list<array{code: string, message: string, scope: string}>,
|
||||
* capabilities: array{select_variant: bool, change_quantity: bool, add_to_cart: bool, buy_now: bool}
|
||||
* restrictions: list<array{code: string, message: string}>,
|
||||
* capabilities: array{display: bool, select_variant: bool, change_quantity: bool, add_to_cart: bool, buy_now: bool}
|
||||
* }
|
||||
*/
|
||||
public function availability(
|
||||
?int $availableStock,
|
||||
?int $remainingUserQuota,
|
||||
string $scope = 'product',
|
||||
AvailabilitySubject $subject = AvailabilitySubject::Product,
|
||||
): array {
|
||||
$restrictions = [];
|
||||
|
||||
@@ -60,7 +87,6 @@ class CatalogItemAllowanceService
|
||||
$restrictions[] = [
|
||||
'code' => 'user_quota_reached',
|
||||
'message' => self::USER_QUOTA_REACHED_MESSAGE,
|
||||
'scope' => $scope,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -68,11 +94,11 @@ class CatalogItemAllowanceService
|
||||
$restrictions[] = [
|
||||
'code' => 'out_of_stock',
|
||||
'message' => self::OUT_OF_STOCK_MESSAGE,
|
||||
'scope' => $scope,
|
||||
];
|
||||
}
|
||||
|
||||
$capabilities = [
|
||||
'display' => true,
|
||||
'select_variant' => true,
|
||||
'change_quantity' => true,
|
||||
'add_to_cart' => true,
|
||||
@@ -81,7 +107,10 @@ class CatalogItemAllowanceService
|
||||
|
||||
foreach ($restrictions as $restriction) {
|
||||
/** @var array<string, bool> $policy */
|
||||
$policy = config("catalog.availability_policies.{$restriction['code']}", []);
|
||||
$policy = config(
|
||||
"catalog.availability_policies.{$restriction['code']}.{$subject->value}",
|
||||
[],
|
||||
);
|
||||
|
||||
foreach ($capabilities as $capability => $allowed) {
|
||||
$capabilities[$capability] = $allowed && ($policy[$capability] ?? true);
|
||||
@@ -89,6 +118,7 @@ class CatalogItemAllowanceService
|
||||
}
|
||||
|
||||
return [
|
||||
'subject' => $subject->value,
|
||||
'maximum_quantity' => $this->maximumAddableQuantity(
|
||||
$availableStock,
|
||||
$remainingUserQuota,
|
||||
@@ -107,7 +137,6 @@ class CatalogItemAllowanceService
|
||||
$availability['restrictions'][] = [
|
||||
'code' => 'requested_quantity_exceeds_user_quota',
|
||||
'message' => $message,
|
||||
'scope' => 'product',
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -4,16 +4,36 @@ return [
|
||||
'stock_reservation_expiration_minutes' => (int) env('STOCK_RESERVATION_EXPIRATION_MINUTES', 30),
|
||||
'availability_policies' => [
|
||||
'user_quota_reached' => [
|
||||
'select_variant' => false,
|
||||
'change_quantity' => false,
|
||||
'add_to_cart' => false,
|
||||
'buy_now' => false,
|
||||
'product' => [
|
||||
'display' => true,
|
||||
'select_variant' => false,
|
||||
'change_quantity' => false,
|
||||
'add_to_cart' => false,
|
||||
'buy_now' => false,
|
||||
],
|
||||
'variant' => [
|
||||
'display' => true,
|
||||
'select_variant' => false,
|
||||
'change_quantity' => false,
|
||||
'add_to_cart' => false,
|
||||
'buy_now' => false,
|
||||
],
|
||||
],
|
||||
'out_of_stock' => [
|
||||
'select_variant' => false,
|
||||
'change_quantity' => false,
|
||||
'add_to_cart' => false,
|
||||
'buy_now' => false,
|
||||
'product' => [
|
||||
'display' => false,
|
||||
'select_variant' => false,
|
||||
'change_quantity' => false,
|
||||
'add_to_cart' => false,
|
||||
'buy_now' => false,
|
||||
],
|
||||
'variant' => [
|
||||
'display' => false,
|
||||
'select_variant' => false,
|
||||
'change_quantity' => false,
|
||||
'add_to_cart' => false,
|
||||
'buy_now' => false,
|
||||
],
|
||||
],
|
||||
],
|
||||
];
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
namespace Tests\Unit\Catalog;
|
||||
|
||||
use App\Domains\Catalog\Enums\AvailabilitySubject;
|
||||
use App\Domains\Catalog\Services\CatalogItemAllowanceService;
|
||||
use App\Domains\Purchase\Services\UserPurchaseLimitService;
|
||||
use Mockery;
|
||||
@@ -15,15 +16,16 @@ class CatalogItemAllowanceServiceTest extends TestCase
|
||||
Mockery::mock(UserPurchaseLimitService::class),
|
||||
);
|
||||
|
||||
$availability = $service->availability(0, 0, 'variant');
|
||||
$availability = $service->availability(0, 0, AvailabilitySubject::Variant);
|
||||
|
||||
$this->assertSame('variant', $availability['subject']);
|
||||
$this->assertSame(0, $availability['maximum_quantity']);
|
||||
$this->assertSame(
|
||||
['user_quota_reached', 'out_of_stock'],
|
||||
array_column($availability['restrictions'], 'code'),
|
||||
);
|
||||
$this->assertSame(['variant', 'variant'], array_column($availability['restrictions'], 'scope'));
|
||||
$this->assertSame([
|
||||
'display' => false,
|
||||
'select_variant' => false,
|
||||
'change_quantity' => false,
|
||||
'add_to_cart' => false,
|
||||
@@ -39,11 +41,25 @@ class CatalogItemAllowanceServiceTest extends TestCase
|
||||
|
||||
$availability = $service->availability(5, 3);
|
||||
|
||||
$this->assertSame('product', $availability['subject']);
|
||||
$this->assertSame(3, $availability['maximum_quantity']);
|
||||
$this->assertSame([], $availability['restrictions']);
|
||||
$this->assertNotContains(false, $availability['capabilities'], true);
|
||||
}
|
||||
|
||||
public function test_reaching_the_user_quota_keeps_the_product_visible(): void
|
||||
{
|
||||
$service = new CatalogItemAllowanceService(
|
||||
Mockery::mock(UserPurchaseLimitService::class),
|
||||
);
|
||||
|
||||
$availability = $service->availability(5, 0);
|
||||
|
||||
$this->assertTrue($availability['capabilities']['display']);
|
||||
$this->assertFalse($availability['capabilities']['add_to_cart']);
|
||||
$this->assertSame('user_quota_reached', $availability['restrictions'][0]['code']);
|
||||
}
|
||||
|
||||
public function test_it_describes_a_requested_quantity_that_exceeds_the_remaining_quota(): void
|
||||
{
|
||||
$service = new CatalogItemAllowanceService(
|
||||
@@ -57,6 +73,7 @@ class CatalogItemAllowanceServiceTest extends TestCase
|
||||
'requested_quantity_exceeds_user_quota',
|
||||
$availability['restrictions'][0]['code'],
|
||||
);
|
||||
$this->assertTrue($availability['capabilities']['display']);
|
||||
$this->assertTrue($availability['capabilities']['change_quantity']);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user