makeContext(); ['purchase' => $po, 'item' => $item, 'part' => $part] = $ctx; $po->receiveItem($item, 3, $ctx['warehouse']); $item->refresh(); $po->refresh(); $part->refresh(); $this->assertEquals(3.0, (float) $item->qty_received); $this->assertEquals('partial', $po->status); $this->assertFalse($item->isFullyReceived()); $this->assertEquals(3.0, (float) $part->qty); $this->assertEquals(1, PartBatch::where('part_id', $part->id)->count()); $this->assertEquals(1, SupplierPartPrice::where('part_id', $part->id)->count()); $this->assertEquals(1, WarehouseEvent::where('part_id', $part->id)->where('type', 'receipt')->count()); } public function test_full_receipt_via_helper_sets_status_received(): void { $ctx = $this->makeContext(); $ctx['purchase']->receiveAllRemaining(); $ctx['purchase']->refresh(); $ctx['part']->refresh(); $this->assertEquals('received', $ctx['purchase']->status); $this->assertNotNull($ctx['purchase']->received_at); $this->assertTrue($ctx['item']->fresh()->isFullyReceived()); $this->assertEquals(10.0, (float) $ctx['part']->qty); } public function test_receipt_over_outstanding_throws(): void { $ctx = $this->makeContext(); $this->expectException(\InvalidArgumentException::class); $ctx['purchase']->receiveItem($ctx['item'], 11); } public function test_two_partial_receipts_complete_purchase(): void { $ctx = $this->makeContext(); $ctx['purchase']->receiveItem($ctx['item'], 4); $ctx['purchase']->refresh(); $this->assertEquals('partial', $ctx['purchase']->status); $ctx['purchase']->receiveItem($ctx['item']->fresh(), 6); $ctx['purchase']->refresh(); $this->assertEquals('received', $ctx['purchase']->status); // Two separate batches → FIFO will consume the first one first. $this->assertEquals(2, PartBatch::where('part_id', $ctx['part']->id)->count()); } public function test_supplier_analytics_on_time_rate(): void { $ctx = $this->makeContext(); $analytics = app(SupplierAnalytics::class); // No purchases received yet → null $this->assertNull($analytics->onTimeRate($ctx['supplier'])); // One on-time delivery $ctx['purchase']->update(['expected_at' => now()->addDays(5), 'received_at' => now()->addDays(3)]); $ctx['purchase']->refresh(); $ctx['purchase']->update(['status' => 'received']); // Second one late $po2 = Purchase::create([ 'number' => Purchase::generateNumber($ctx['supplier']->company_id), 'supplier_id' => $ctx['supplier']->id, 'warehouse_id' => $ctx['warehouse']->id, 'order_date' => now()->subDays(10), 'expected_at' => now()->subDays(2), 'received_at' => now(), 'status' => 'received', ]); $po2->refresh(); $rate = $analytics->onTimeRate($ctx['supplier']); $this->assertEquals(50.0, $rate, '1 on-time + 1 late = 50%'); } public function test_computed_rating_returns_null_with_insufficient_data(): void { $ctx = $this->makeContext(); $analytics = app(SupplierAnalytics::class); $this->assertNull($analytics->computedRating($ctx['supplier'])); } private function makeContext(): array { $plan = Plan::firstOrCreate(['slug' => 'test'], [ 'name' => 'Test', 'price' => 0, 'features' => [], ]); $company = Company::create([ 'plan_id' => $plan->id, 'slug' => 'pr-' . uniqid(), 'name' => 'PR Service', 'status' => 'active', ]); app(TenantManager::class)->setCurrent($company); $warehouse = Warehouse::create([ 'code' => 'MAIN', 'name' => 'Depozit', 'is_default' => true, 'is_active' => true, ]); $company->forceFill(['default_warehouse_id' => $warehouse->id])->saveQuietly(); $supplier = Supplier::create([ 'name' => 'Acme Parts', 'is_active' => true, 'rating' => 3, 'delivery_days' => 0, 'discount_pct' => 0, ]); $part = Part::create([ 'name' => 'Filtru aer', 'article' => 'FA-001', 'unit' => 'buc', 'qty' => 0, 'buy_price' => 0, 'sell_price' => 0, 'is_active' => true, ]); $purchase = Purchase::create([ 'number' => Purchase::generateNumber($company->id), 'supplier_id' => $supplier->id, 'warehouse_id' => $warehouse->id, 'order_date' => now(), 'expected_at' => now()->addDays(3), 'status' => 'ordered', ]); $item = PurchaseItem::create([ 'purchase_id' => $purchase->id, 'part_id' => $part->id, 'name' => $part->name, 'article' => $part->article, 'qty' => 10, 'unit' => 'buc', 'buy_price' => 50, ]); return compact('company', 'warehouse', 'supplier', 'part', 'purchase', 'item'); } }