Skip to content

Commit 91fde85

Browse files
feat: implement product statuses (CT-57)
* feat: implement product statuses * chore: product status improvements & fixes * chore: refactor into enum, fix n+1, and other minor fixes * chore: standardize enum class & fix product error --------- Co-authored-by: Omer Sabic <omer@datalinx.si>
1 parent 3cee667 commit 91fde85

27 files changed

Lines changed: 1430 additions & 6 deletions
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
<?php
2+
3+
use Illuminate\Database\Migrations\Migration;
4+
use Illuminate\Database\Schema\Blueprint;
5+
use Illuminate\Support\Facades\Schema;
6+
7+
return new class extends Migration
8+
{
9+
public function up(): void
10+
{
11+
Schema::create('pim_product_statuses', function (Blueprint $table) {
12+
$table->id();
13+
14+
$tenantModel = config('eclipse-catalogue.tenancy.model');
15+
$tenantFk = config('eclipse-catalogue.tenancy.foreign_key');
16+
if ($tenantModel && $tenantFk) {
17+
/** @var \Illuminate\Database\Eloquent\Model $tenant */
18+
$tenant = new $tenantModel;
19+
$table->foreignId($tenantFk)
20+
->constrained($tenant->getTable(), $tenant->getKeyName())
21+
->cascadeOnUpdate()
22+
->cascadeOnDelete();
23+
}
24+
25+
$table->string('code', 20)->nullable();
26+
$table->json('title');
27+
$table->json('description')->nullable();
28+
$table->string('label_type', 50)->default('gray');
29+
$table->boolean('shown_in_browse')->default(true);
30+
$table->boolean('allow_price_display')->default(true);
31+
$table->boolean('allow_sale')->default(true);
32+
$table->boolean('is_default')->default(false);
33+
$table->tinyInteger('priority');
34+
$table->string('sd_item_availability', 50);
35+
$table->boolean('skip_stock_qty_check')->default(false);
36+
$table->timestamps();
37+
38+
if ($tenantModel && $tenantFk) {
39+
$table->unique([$tenantFk, 'code']);
40+
} else {
41+
$table->unique('code');
42+
}
43+
});
44+
}
45+
46+
public function down(): void
47+
{
48+
Schema::dropIfExists('pim_product_statuses');
49+
}
50+
};
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
<?php
2+
3+
use Illuminate\Database\Migrations\Migration;
4+
use Illuminate\Database\Schema\Blueprint;
5+
use Illuminate\Support\Facades\Schema;
6+
7+
return new class extends Migration
8+
{
9+
public function up(): void
10+
{
11+
Schema::table('catalogue_product_data', function (Blueprint $table) {
12+
$table->foreignId('product_status_id')
13+
->nullable()
14+
->after('product_id')
15+
->constrained('pim_product_statuses')
16+
->nullOnDelete()
17+
->cascadeOnUpdate();
18+
});
19+
}
20+
21+
public function down(): void
22+
{
23+
Schema::table('catalogue_product_data', function (Blueprint $table) {
24+
$table->dropForeign(['product_status_id']);
25+
$table->dropColumn('product_status_id');
26+
});
27+
}
28+
};

database/seeders/CatalogueSeeder.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ public function run(): void
1515
$this->call(ProductTypeSeeder::class);
1616
$this->call(GroupSeeder::class);
1717
$this->call(PropertySeeder::class);
18+
$this->call(ProductStatusSeeder::class);
1819
$this->call(ProductSeeder::class);
1920
}
2021
}

database/seeders/ProductSeeder.php

Lines changed: 48 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
use Eclipse\Catalogue\Models\Group;
77
use Eclipse\Catalogue\Models\Product;
88
use Eclipse\Catalogue\Models\ProductData;
9+
use Eclipse\Catalogue\Models\ProductStatus;
910
use Eclipse\Catalogue\Models\ProductType;
1011
use Exception;
1112
use Illuminate\Database\Seeder;
@@ -19,6 +20,7 @@ public function run(): void
1920
$this->ensureSampleImagesExist();
2021
$this->ensureProductTypesExist();
2122
$this->ensureGroupsExist();
23+
$this->ensureProductStatusesExist();
2224

2325
$productTypes = ProductType::all();
2426

@@ -45,14 +47,17 @@ public function run(): void
4547
->inRandomOrder()
4648
->value('id');
4749

48-
ProductData::factory()->create([
50+
$productData = ProductData::factory()->create([
4951
'product_id' => $product->id,
5052
$tenantFK => $tenant->id,
5153
'is_active' => true,
5254
'has_free_delivery' => false,
5355
'category_id' => $categoryId,
5456
]);
5557

58+
// Assign random product status for this tenant
59+
$this->assignRandomProductStatus($productData, $tenant->id);
60+
5661
// Get groups for this specific tenant
5762
$tenantGroups = Group::where($tenantFK, $tenant->id)->get();
5863
$groupsToAdd = $this->determineGroupsForProduct($index, $tenantGroups);
@@ -64,13 +69,16 @@ public function run(): void
6469
} else {
6570
$categoryId = Category::query()->inRandomOrder()->value('id');
6671

67-
ProductData::factory()->create([
72+
$productData = ProductData::factory()->create([
6873
'product_id' => $product->id,
6974
'is_active' => true,
7075
'has_free_delivery' => false,
7176
'category_id' => $categoryId,
7277
]);
7378

79+
// Assign random product status for non-tenant scenario
80+
$this->assignRandomProductStatus($productData, null);
81+
7482
// For non-tenant scenarios, use all groups
7583
$groups = Group::all();
7684
$groupsToAdd = $this->determineGroupsForProduct($index, $groups);
@@ -152,4 +160,42 @@ private function ensureGroupsExist(): void
152160
$this->call(GroupSeeder::class);
153161
}
154162
}
163+
164+
private function ensureProductStatusesExist(): void
165+
{
166+
$productStatuses = ProductStatus::all();
167+
168+
if ($productStatuses->isEmpty()) {
169+
$this->call(ProductStatusSeeder::class);
170+
}
171+
}
172+
173+
/**
174+
* Assign a random product status to a product data record, respecting tenancy.
175+
*/
176+
private function assignRandomProductStatus(ProductData $productData, ?int $tenantId): void
177+
{
178+
$tenantFK = config('eclipse-catalogue.tenancy.foreign_key');
179+
180+
// Get available product statuses for the tenant
181+
$query = ProductStatus::query();
182+
183+
if ($tenantFK && $tenantId !== null) {
184+
// Multi-tenant scenario: get statuses for this specific tenant
185+
$query->where($tenantFK, $tenantId);
186+
} elseif (! $tenantFK) {
187+
// Single-tenant scenario: get all statuses (site_id will be null)
188+
$query->whereNull('site_id');
189+
}
190+
191+
$availableStatuses = $query->get();
192+
193+
if ($availableStatuses->isNotEmpty()) {
194+
// Randomly select a status (with 70% chance of getting a status, 30% chance of no status)
195+
if (rand(1, 100) <= 70) {
196+
$randomStatus = $availableStatuses->random();
197+
$productData->update(['product_status_id' => $randomStatus->id]);
198+
}
199+
}
200+
}
155201
}
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
<?php
2+
3+
namespace Eclipse\Catalogue\Seeders;
4+
5+
use Eclipse\Catalogue\Models\ProductStatus;
6+
use Illuminate\Database\Seeder;
7+
8+
class ProductStatusSeeder extends Seeder
9+
{
10+
public function run(): void
11+
{
12+
$tenantFK = config('eclipse-catalogue.tenancy.foreign_key');
13+
$tenants = [];
14+
if ($tenantFK && ($model = config('eclipse-catalogue.tenancy.model'))) {
15+
$tenants = (new $model)::query()->pluck('id')->all();
16+
} else {
17+
$tenants = [null];
18+
}
19+
20+
foreach ($tenants as $tenantId) {
21+
$defaults = [
22+
['code' => 'in_stock', 'title' => ['en' => 'In stock', 'sl' => 'Na zalogi'], 'label_type' => 'success', 'priority' => 1, 'is_default' => true],
23+
['code' => 'out_of_stock', 'title' => ['en' => 'Out of stock', 'sl' => 'Trenutno razprodan'], 'label_type' => 'danger', 'priority' => 5],
24+
['code' => 'coming', 'title' => ['en' => 'Coming soon', 'sl' => 'V prihodu'], 'label_type' => 'info', 'priority' => 3],
25+
];
26+
27+
foreach ($defaults as $row) {
28+
$availabilityMap = [
29+
'in_stock' => \Eclipse\Catalogue\Enums\StructuredData\ItemAvailability::IN_STOCK->value,
30+
'out_of_stock' => \Eclipse\Catalogue\Enums\StructuredData\ItemAvailability::OUT_OF_STOCK->value,
31+
'coming' => \Eclipse\Catalogue\Enums\StructuredData\ItemAvailability::PREORDER->value,
32+
];
33+
34+
$data = array_merge([
35+
'description' => null,
36+
'shown_in_browse' => true,
37+
'allow_price_display' => true,
38+
'allow_sale' => true,
39+
'sd_item_availability' => $availabilityMap[$row['code']] ?? \Eclipse\Catalogue\Enums\StructuredData\ItemAvailability::IN_STOCK->value,
40+
'skip_stock_qty_check' => false,
41+
], $row);
42+
43+
if ($tenantFK && $tenantId !== null) {
44+
$data[$tenantFK] = $tenantId;
45+
}
46+
47+
$unique = ['code' => $data['code']];
48+
if ($tenantFK && $tenantId !== null) {
49+
$unique[$tenantFK] = $tenantId;
50+
}
51+
ProductStatus::query()->firstOrCreate($unique, $data);
52+
}
53+
}
54+
}
55+
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
<?php
2+
3+
return [
4+
'singular' => 'Product Status',
5+
'plural' => 'Product Statuses',
6+
'navigation_label' => 'Product Statuses',
7+
8+
'fields' => [
9+
'code' => 'Code',
10+
'title' => 'Title',
11+
'description' => 'Description',
12+
'label_type' => 'Label Type',
13+
'shown_in_browse' => 'Shown in browse',
14+
'allow_price_display' => 'Allow price display',
15+
'allow_sale' => 'Allow sale',
16+
'is_default' => 'Default status',
17+
'priority' => 'Priority',
18+
'sd_item_availability' => 'Item availability',
19+
'skip_stock_qty_check' => 'Skip stock quantity check',
20+
'no_status' => 'No status',
21+
],
22+
23+
'help_text' => [
24+
'code' => 'Unique identifier for this status',
25+
'title' => 'Display name for this status',
26+
'description' => 'Optional description of this status',
27+
'label_type' => 'Color theme for displaying this status as a badge',
28+
'shown_in_browse' => 'Whether products with this status appear in catalog browsing',
29+
'allow_price_display' => 'Whether to show prices for products with this status',
30+
'allow_sale' => 'Whether products with this status can be purchased (automatically disabled if price display is off)',
31+
'is_default' => 'Set as the default status for new products',
32+
'priority' => 'Status priority — lower number is better. Used for automatically deciding between e.g. supplier offers or product variants.',
33+
'sd_item_availability' => 'Structured data item availability value <a href="https://schema.org/ItemAvailability" target="_blank" rel="noopener">(more info)</a>',
34+
'skip_stock_qty_check' => 'When ordering is possible, do not check if there is available stock',
35+
],
36+
37+
'sections' => [
38+
'visibility_rules' => 'Visibility & Rules',
39+
],
40+
41+
'actions' => [
42+
'create' => 'New product status',
43+
],
44+
45+
'validation' => [
46+
'code_unique' => 'This code is already in use.',
47+
],
48+
];
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
<?php
2+
3+
return [
4+
'singular' => 'Status izdelka',
5+
'plural' => 'Statusi izdelka',
6+
'navigation_label' => 'Statusi izdelka',
7+
8+
'fields' => [
9+
'code' => 'Koda',
10+
'title' => 'Naziv',
11+
'description' => 'Opis',
12+
'label_type' => 'Vrsta oznake',
13+
'shown_in_browse' => 'Prikaži v katalogu',
14+
'allow_price_display' => 'Prikaži ceno',
15+
'allow_sale' => 'Dovoli prodajo',
16+
'is_default' => 'Privzeti status',
17+
'priority' => 'Prioriteta',
18+
'sd_item_availability' => 'Razpoložljivost',
19+
'skip_stock_qty_check' => 'Ne preverjaj zaloge',
20+
'no_status' => 'Brez statusa',
21+
],
22+
23+
'help_text' => [
24+
'code' => 'Edinstveni identifikator za ta status',
25+
'title' => 'Prikazno ime za ta status',
26+
'description' => 'Opcijski opis tega statusa',
27+
'label_type' => 'Barvna tema za prikaz tega statusa kot oznake',
28+
'shown_in_browse' => 'Ali se izdelki s tem statusom prikažejo pri brskanju po katalogu',
29+
'allow_price_display' => 'Ali prikazati cene za izdelke s tem statusom',
30+
'allow_sale' => 'Ali se izdelki s tem statusom lahko kupijo (samodejno onemogočeno, če je prikaz cene izklopljen)',
31+
'is_default' => 'Nastavi kot privzeti status za nove izdelke',
32+
'priority' => 'Prioriteta statusa — manjše število je boljše. Uporabljeno pri samodejnem odločanju pri npr. primerjavi ponudbi dobaviteljev ali variant izdelka.',
33+
'sd_item_availability' => 'Strukturirana vrednost razpoložljivosti izdelka <a href="https://schema.org/ItemAvailability" target="_blank" rel="noopener">(več informacij)</a>',
34+
'skip_stock_qty_check' => 'Ko je naročanje omogočeno, ne preverjaj razpoložljivosti zaloge',
35+
],
36+
37+
'sections' => [
38+
'visibility_rules' => 'Vidnost in pravila',
39+
],
40+
41+
'actions' => [
42+
'create' => 'Nov status izdelka',
43+
],
44+
45+
'validation' => [
46+
'code_unique' => 'Ta koda je že v uporabi.',
47+
],
48+
];
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
<?php
2+
3+
namespace Eclipse\Catalogue\Enums\StructuredData;
4+
5+
use Filament\Support\Contracts\HasLabel;
6+
7+
enum ItemAvailability: string implements HasLabel
8+
{
9+
case DISCONTINUED = 'Discontinued';
10+
case IN_STOCK = 'InStock';
11+
case IN_STORE_ONLY = 'InStoreOnly';
12+
case LIMITED_AVAILABILITY = 'LimitedAvailability';
13+
case ONLINE_ONLY = 'OnlineOnly';
14+
case OUT_OF_STOCK = 'OutOfStock';
15+
case PREORDER = 'PreOrder';
16+
case PRESALE = 'PreSale';
17+
case SOLD_OUT = 'SoldOut';
18+
19+
public function getLabel(): ?string
20+
{
21+
return match ($this) {
22+
self::DISCONTINUED => 'Discontinued - Indicates that the item has been discontinued.',
23+
self::IN_STOCK => 'InStock - Indicates that the item is in stock.',
24+
self::IN_STORE_ONLY => 'InStoreOnly - Indicates that the item is available only at physical locations.',
25+
self::LIMITED_AVAILABILITY => 'LimitedAvailability - Indicates that the item has limited availability.',
26+
self::ONLINE_ONLY => 'OnlineOnly - Indicates that the item is available only online.',
27+
self::OUT_OF_STOCK => 'OutOfStock - Indicates that the item is out of stock.',
28+
self::PREORDER => 'PreOrder - Indicates that the item is available for pre-order, but will be delivered when generally available.',
29+
self::PRESALE => 'PreSale - Indicates that the item is available for ordering and delivery before general availability.',
30+
self::SOLD_OUT => 'SoldOut - Indicates that the item has sold out.',
31+
};
32+
}
33+
}

0 commit comments

Comments
 (0)