fix: make product department_id required to prevent picking ticket generation failures
When products don't have departments assigned, generatePickingTickets() skips them entirely, resulting in no picking tickets being created. This causes the "All Items Picked" banner to show immediately due to a vacuous truth bug (zero tickets = all tickets complete). Changes: - Add migration to assign default department to products without one - Make department_id NOT NULL in products table - Update DevSeeder to create default department before products - Update CannabrandsSeeder to assign department_id to all products - Add test coverage for department requirement (4 tests, all passing) The graceful handling in FulfillmentWorkOrderService (lines 46-48) is kept as defensive programming, though it won't be triggered with this constraint. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
10
boost.json
Normal file
10
boost.json
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"agents": [
|
||||
"claude_code"
|
||||
],
|
||||
"editors": [
|
||||
"claude_code"
|
||||
],
|
||||
"guidelines": [],
|
||||
"sail": true
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
// First, assign a default department to all products without one
|
||||
// Get the first department for each business and assign it to products
|
||||
DB::statement('
|
||||
UPDATE products p
|
||||
SET department_id = (
|
||||
SELECT d.id
|
||||
FROM departments d
|
||||
INNER JOIN brands b ON b.business_id = d.business_id
|
||||
WHERE b.id = p.brand_id
|
||||
ORDER BY d.id
|
||||
LIMIT 1
|
||||
)
|
||||
WHERE p.department_id IS NULL
|
||||
');
|
||||
|
||||
// Now make the column NOT NULL
|
||||
Schema::table('products', function (Blueprint $table) {
|
||||
$table->foreignId('department_id')->nullable(false)->change();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('products', function (Blueprint $table) {
|
||||
$table->foreignId('department_id')->nullable()->change();
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -15,6 +15,9 @@ class CannabrandsSeeder extends Seeder
|
||||
{
|
||||
$catalog = require __DIR__.'/data/cannabrands_catalog.php';
|
||||
|
||||
// Get the first department for this business
|
||||
$defaultDepartment = \App\Models\Department::where('business_id', $businessId)->first();
|
||||
|
||||
foreach ($catalog['brands'] as $brandIndex => $brandData) {
|
||||
$brand = Brand::updateOrCreate(
|
||||
['slug' => $brandData['slug']],
|
||||
@@ -44,6 +47,7 @@ class CannabrandsSeeder extends Seeder
|
||||
['sku' => $sku],
|
||||
[
|
||||
'brand_id' => $brand->id,
|
||||
'department_id' => $defaultDepartment?->id,
|
||||
'slug' => $slug,
|
||||
'name' => $productData['name'],
|
||||
'description' => sprintf('%s - %s %s', $brandData['name'], $productData['name'], ucfirst($category)),
|
||||
|
||||
@@ -554,6 +554,22 @@ class DevSeeder extends Seeder
|
||||
]
|
||||
);
|
||||
|
||||
// ================================================================
|
||||
// DEPARTMENTS
|
||||
// ================================================================
|
||||
|
||||
// Create default department for seller business
|
||||
$defaultDepartment = \App\Models\Department::updateOrCreate(
|
||||
[
|
||||
'business_id' => $sellerBusiness->id,
|
||||
'name' => 'General',
|
||||
],
|
||||
[
|
||||
'description' => 'Default department for products',
|
||||
'is_active' => true,
|
||||
]
|
||||
);
|
||||
|
||||
// ================================================================
|
||||
// BRANDS & PRODUCT CATALOG
|
||||
// ================================================================
|
||||
@@ -872,6 +888,7 @@ class DevSeeder extends Seeder
|
||||
['sku' => 'DBP-PR-BD-001'],
|
||||
[
|
||||
'brand_id' => $brand1->id,
|
||||
'department_id' => $defaultDepartment->id,
|
||||
'name' => 'Blue Dream Pre-Roll - 1g',
|
||||
'description' => 'Single 1g pre-roll of premium Blue Dream flower',
|
||||
'type' => 'pre_roll',
|
||||
|
||||
58
k8s/local/mailpit.yaml
Normal file
58
k8s/local/mailpit.yaml
Normal file
@@ -0,0 +1,58 @@
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: mailpit
|
||||
namespace: ${NS}
|
||||
spec:
|
||||
selector:
|
||||
app: mailpit
|
||||
ports:
|
||||
- name: smtp
|
||||
port: 1025
|
||||
targetPort: 1025
|
||||
- name: http
|
||||
port: 8025
|
||||
targetPort: 8025
|
||||
---
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: mailpit
|
||||
namespace: ${NS}
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app: mailpit
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: mailpit
|
||||
spec:
|
||||
containers:
|
||||
- name: mailpit
|
||||
image: axllent/mailpit:latest
|
||||
ports:
|
||||
- containerPort: 1025
|
||||
name: smtp
|
||||
- containerPort: 8025
|
||||
name: http
|
||||
resources:
|
||||
requests:
|
||||
memory: "32Mi"
|
||||
cpu: "50m"
|
||||
limits:
|
||||
memory: "128Mi"
|
||||
cpu: "200m"
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: /
|
||||
port: 8025
|
||||
initialDelaySeconds: 10
|
||||
periodSeconds: 10
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /
|
||||
port: 8025
|
||||
initialDelaySeconds: 5
|
||||
periodSeconds: 5
|
||||
57
tests/Feature/ProductDepartmentRequiredTest.php
Normal file
57
tests/Feature/ProductDepartmentRequiredTest.php
Normal file
@@ -0,0 +1,57 @@
|
||||
<?php
|
||||
|
||||
use App\Models\Brand;
|
||||
use App\Models\Business;
|
||||
use App\Models\Department;
|
||||
use App\Models\Product;
|
||||
use App\Models\User;
|
||||
|
||||
use function Pest\Laravel\actingAs;
|
||||
|
||||
beforeEach(function () {
|
||||
$this->business = Business::factory()->create(['business_type' => 'seller']);
|
||||
$this->user = User::factory()->create(['user_type' => 'seller']);
|
||||
$this->user->businesses()->attach($this->business->id);
|
||||
$this->brand = Brand::factory()->create(['business_id' => $this->business->id]);
|
||||
$this->department = Department::factory()->create(['business_id' => $this->business->id]);
|
||||
|
||||
actingAs($this->user);
|
||||
});
|
||||
|
||||
test('product cannot be created without department_id in database', function () {
|
||||
$product = Product::factory()->make([
|
||||
'brand_id' => $this->brand->id,
|
||||
'department_id' => null,
|
||||
]);
|
||||
|
||||
expect(fn () => $product->save())
|
||||
->toThrow(Exception::class);
|
||||
});
|
||||
|
||||
test('product requires department_id when creating', function () {
|
||||
$productData = Product::factory()->make([
|
||||
'brand_id' => $this->brand->id,
|
||||
'department_id' => null,
|
||||
])->toArray();
|
||||
|
||||
$product = new Product($productData);
|
||||
|
||||
expect($product->department_id)->toBeNull()
|
||||
->and(fn () => $product->save())->toThrow(Exception::class);
|
||||
});
|
||||
|
||||
test('product can be created with valid department_id', function () {
|
||||
$product = Product::factory()->create([
|
||||
'brand_id' => $this->brand->id,
|
||||
'department_id' => $this->department->id,
|
||||
]);
|
||||
|
||||
expect($product->department_id)->toBe($this->department->id)
|
||||
->and($product->exists)->toBeTrue();
|
||||
});
|
||||
|
||||
test('all existing products have departments assigned after migration', function () {
|
||||
$productsWithoutDepartments = Product::whereNull('department_id')->count();
|
||||
|
||||
expect($productsWithoutDepartments)->toBe(0);
|
||||
});
|
||||
Reference in New Issue
Block a user