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:
Jon Leopard
2025-11-16 16:53:18 -07:00
parent efc61680c9
commit 9833cc592d
6 changed files with 191 additions and 0 deletions

10
boost.json Normal file
View File

@@ -0,0 +1,10 @@
{
"agents": [
"claude_code"
],
"editors": [
"claude_code"
],
"guidelines": [],
"sail": true
}

View File

@@ -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();
});
}
};

View File

@@ -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)),

View File

@@ -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
View 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

View 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);
});