adminUser = User::factory()->create(['role' => Role::ADMIN]); $this->managerUser = User::factory()->create(['role' => Role::MANAGER]); } private function assertRedirectsToShowWithGeneratedNav($response, SparePartOrder $sparePartOrder): void { $location = $response->headers->get('Location'); $this->assertNotNull($location); $this->assertStringStartsWith(route('spare_part_orders.show', $sparePartOrder), $location); $this->assertStringContainsString('nav=', $location); } // ==================== Authentication ==================== public function test_guest_cannot_access_spare_part_orders_index(): void { $response = $this->get(route('spare_part_orders.index')); $response->assertRedirect(route('login')); } public function test_guest_cannot_access_spare_part_order_create(): void { $response = $this->get(route('spare_part_orders.create')); $response->assertRedirect(route('login')); } public function test_guest_cannot_store_spare_part_order(): void { $response = $this->post(route('spare_part_orders.store'), []); $response->assertRedirect(route('login')); } // ==================== Index ==================== public function test_admin_can_access_spare_part_orders_index(): void { $response = $this->actingAs($this->adminUser) ->get(route('spare_part_orders.index')); $response->assertStatus(200); $response->assertViewIs('spare_parts.index'); } public function test_manager_can_access_spare_part_orders_index(): void { $response = $this->actingAs($this->managerUser) ->get(route('spare_part_orders.index')); $response->assertStatus(200); $response->assertViewIs('spare_parts.index'); } public function test_index_passes_only_ordered_order_numbers_to_view(): void { SparePartOrder::factory()->ordered()->create([ 'order_number' => 'SP-BULK-001', ]); SparePartOrder::factory()->ordered()->create([ 'order_number' => 'SP-BULK-001', ]); SparePartOrder::factory()->ordered()->create([ 'order_number' => 'SP-BULK-002', ]); SparePartOrder::factory()->inStock()->create([ 'order_number' => 'SP-IN-STOCK-001', ]); SparePartOrder::factory()->ordered()->create([ 'order_number' => '', ]); SparePartOrder::factory()->ordered()->create([ 'order_number' => null, ]); $response = $this->actingAs($this->adminUser) ->get(route('spare_part_orders.index')); $response->assertStatus(200); $response->assertViewHas('order_numbers', function ($orderNumbers) { return $orderNumbers->values()->all() === ['SP-BULK-001', 'SP-BULK-002']; }); } // ==================== Show ==================== public function test_admin_can_view_spare_part_order(): void { $sparePartOrder = SparePartOrder::factory()->create(); $response = $this->actingAs($this->adminUser) ->get(route('spare_part_orders.show', $sparePartOrder)); $response->assertStatus(200); $response->assertViewIs('spare_part_orders.edit'); } public function test_manager_can_view_spare_part_order(): void { $sparePartOrder = SparePartOrder::factory()->create(); $response = $this->actingAs($this->managerUser) ->get(route('spare_part_orders.show', $sparePartOrder)); $response->assertStatus(200); $response->assertViewIs('spare_part_orders.edit'); } public function test_spare_part_order_show_uses_nav_context_back_url(): void { $sparePartOrder = SparePartOrder::factory()->create(); $indexResponse = $this->actingAs($this->adminUser) ->get(route('spare_part_orders.index', [ 'spare_part_id' => $sparePartOrder->spare_part_id, ])); $nav = $indexResponse->viewData('nav'); $response = $this->actingAs($this->adminUser) ->get(route('spare_part_orders.show', [ 'sparePartOrder' => $sparePartOrder, 'nav' => $nav, ])); $response->assertOk(); $response->assertViewHas('nav', $nav); $response->assertViewHas('back_url', function (string $backUrl) use ($sparePartOrder): bool { if (!str_starts_with($backUrl, route('spare_part_orders.index'))) { return false; } $query = parse_url($backUrl, PHP_URL_QUERY); parse_str((string) $query, $params); return (int) ($params['spare_part_id'] ?? 0) === (int) $sparePartOrder->spare_part_id; }); } public function test_ordered_spare_part_order_page_renders_set_in_stock_button_with_separate_form(): void { $sparePartOrder = SparePartOrder::factory()->ordered()->create(); $response = $this->actingAs($this->managerUser) ->get(route('spare_part_orders.show', $sparePartOrder)); $response->assertStatus(200); $response->assertSee('form="set-in-stock-form"', false); $response->assertSee('id="set-in-stock-form"', false); $response->assertSee(route('spare_part_orders.set_in_stock', $sparePartOrder), false); } public function test_guest_cannot_view_spare_part_order(): void { $sparePartOrder = SparePartOrder::factory()->create(); $response = $this->get(route('spare_part_orders.show', $sparePartOrder)); $response->assertRedirect(route('login')); } // ==================== Create ==================== public function test_admin_can_access_create_form(): void { $response = $this->actingAs($this->adminUser) ->get(route('spare_part_orders.create')); $response->assertStatus(200); $response->assertViewIs('spare_part_orders.edit'); } public function test_manager_can_access_create_form(): void { $response = $this->actingAs($this->managerUser) ->get(route('spare_part_orders.create')); $response->assertStatus(200); $response->assertViewIs('spare_part_orders.edit'); } // ==================== Store ==================== public function test_admin_can_create_spare_part_order(): void { $sparePart = SparePart::factory()->create(); $response = $this->actingAs($this->adminUser) ->post(route('spare_part_orders.store'), [ 'spare_part_id' => $sparePart->id, 'status' => SparePartOrder::STATUS_ORDERED, 'ordered_quantity' => 10, 'source_text' => 'Test Supplier', ]); $response->assertRedirect(); $response->assertSessionHas('success'); $this->assertDatabaseHas('spare_part_orders', [ 'spare_part_id' => $sparePart->id, 'status' => SparePartOrder::STATUS_ORDERED, 'ordered_quantity' => 10, ]); } public function test_store_spare_part_order_redirects_to_parent_url_from_nav_context(): void { $sparePart = SparePart::factory()->create(); $response = $this->actingAs($this->adminUser) ->withSession([ 'navigation' => [ 'spare-part-order-nav-token' => [ 'updated_at' => time(), 'stack' => [ route('spare_part_orders.index'), route('spare_part_orders.create'), ], ], ], ]) ->post(route('spare_part_orders.store'), [ 'nav' => 'spare-part-order-nav-token', 'spare_part_id' => $sparePart->id, 'status' => SparePartOrder::STATUS_ORDERED, 'ordered_quantity' => 10, 'source_text' => 'Test Supplier', ]); $response->assertRedirect(route('spare_part_orders.index')); } public function test_manager_can_create_spare_part_order(): void { $sparePart = SparePart::factory()->create(); $response = $this->actingAs($this->managerUser) ->post(route('spare_part_orders.store'), [ 'spare_part_id' => $sparePart->id, 'status' => SparePartOrder::STATUS_ORDERED, 'ordered_quantity' => 5, ]); $response->assertRedirect(); $response->assertSessionHas('success'); } public function test_store_validates_required_spare_part_id(): void { $response = $this->actingAs($this->adminUser) ->post(route('spare_part_orders.store'), [ 'status' => SparePartOrder::STATUS_ORDERED, 'ordered_quantity' => 10, ]); $response->assertSessionHasErrors('spare_part_id'); } public function test_store_validates_required_ordered_quantity(): void { $sparePart = SparePart::factory()->create(); $response = $this->actingAs($this->adminUser) ->post(route('spare_part_orders.store'), [ 'spare_part_id' => $sparePart->id, 'status' => SparePartOrder::STATUS_ORDERED, ]); $response->assertSessionHasErrors('ordered_quantity'); } // ==================== Update ==================== public function test_admin_can_update_spare_part_order(): void { $sparePartOrder = SparePartOrder::factory()->ordered()->create(); $response = $this->actingAs($this->adminUser) ->put(route('spare_part_orders.update', $sparePartOrder), [ 'spare_part_id' => $sparePartOrder->spare_part_id, 'status' => SparePartOrder::STATUS_ORDERED, 'ordered_quantity' => 20, 'order_number' => 'ORD-2024-001', ]); $response->assertRedirect(); $response->assertSessionHas('success'); $this->assertDatabaseHas('spare_part_orders', [ 'id' => $sparePartOrder->id, 'ordered_quantity' => 20, 'order_number' => 'ORD-2024-001', ]); } public function test_update_spare_part_order_redirects_to_parent_url_from_nav_context(): void { $sparePartOrder = SparePartOrder::factory()->ordered()->create(); $response = $this->actingAs($this->adminUser) ->withSession([ 'navigation' => [ 'spare-part-order-update-token' => [ 'updated_at' => time(), 'stack' => [ route('spare_part_orders.index'), route('spare_part_orders.show', $sparePartOrder), ], ], ], ]) ->put(route('spare_part_orders.update', $sparePartOrder), [ 'nav' => 'spare-part-order-update-token', 'spare_part_id' => $sparePartOrder->spare_part_id, 'status' => SparePartOrder::STATUS_ORDERED, 'ordered_quantity' => 21, ]); $response->assertRedirect(route('spare_part_orders.index')); } public function test_manager_can_update_spare_part_order(): void { $sparePartOrder = SparePartOrder::factory()->ordered()->create(); $response = $this->actingAs($this->managerUser) ->put(route('spare_part_orders.update', $sparePartOrder), [ 'spare_part_id' => $sparePartOrder->spare_part_id, 'status' => SparePartOrder::STATUS_ORDERED, 'ordered_quantity' => 15, ]); $response->assertRedirect(); $response->assertSessionHas('success'); } public function test_guest_cannot_update_spare_part_order(): void { $sparePartOrder = SparePartOrder::factory()->create(); $response = $this->put(route('spare_part_orders.update', $sparePartOrder), []); $response->assertRedirect(route('login')); } // ==================== Destroy ==================== public function test_admin_can_delete_spare_part_order_without_active_reservations(): void { $sparePartOrder = SparePartOrder::factory()->create(); $response = $this->actingAs($this->adminUser) ->delete(route('spare_part_orders.destroy', $sparePartOrder)); $response->assertRedirect(route('spare_part_orders.index')); $response->assertSessionHas('success'); $this->assertSoftDeleted('spare_part_orders', ['id' => $sparePartOrder->id]); } public function test_admin_cannot_delete_spare_part_order_with_active_reservations(): void { $sparePartOrder = SparePartOrder::factory()->inStock()->create(); Reservation::factory()->create([ 'spare_part_order_id' => $sparePartOrder->id, 'spare_part_id' => $sparePartOrder->spare_part_id, 'status' => Reservation::STATUS_ACTIVE, ]); $response = $this->actingAs($this->adminUser) ->delete(route('spare_part_orders.destroy', $sparePartOrder)); $this->assertRedirectsToShowWithGeneratedNav($response, $sparePartOrder); $response->assertSessionHas('error'); $this->assertDatabaseHas('spare_part_orders', ['id' => $sparePartOrder->id, 'deleted_at' => null]); } public function test_manager_cannot_delete_spare_part_order(): void { $sparePartOrder = SparePartOrder::factory()->create(); $response = $this->actingAs($this->managerUser) ->delete(route('spare_part_orders.destroy', $sparePartOrder)); $response->assertStatus(403); } public function test_guest_cannot_delete_spare_part_order(): void { $sparePartOrder = SparePartOrder::factory()->create(); $response = $this->delete(route('spare_part_orders.destroy', $sparePartOrder)); $response->assertRedirect(route('login')); } // ==================== SetInStock ==================== public function test_admin_can_set_order_in_stock(): void { $sparePartOrder = SparePartOrder::factory()->ordered()->create(); $response = $this->actingAs($this->adminUser) ->withSession([ 'navigation' => [ 'set-in-stock-nav-token' => [ 'updated_at' => time(), 'stack' => [ route('spare_part_orders.index'), route('spare_part_orders.show', $sparePartOrder), ], ], ], ]) ->post(route('spare_part_orders.set_in_stock', [ 'sparePartOrder' => $sparePartOrder, 'nav' => 'set-in-stock-nav-token', ])); $response->assertRedirect(route('spare_part_orders.show', [ 'sparePartOrder' => $sparePartOrder, 'nav' => 'set-in-stock-nav-token', ])); $response->assertSessionHas('success'); $this->assertDatabaseHas('spare_part_orders', [ 'id' => $sparePartOrder->id, 'status' => SparePartOrder::STATUS_IN_STOCK, ]); } public function test_manager_can_set_order_in_stock(): void { $sparePartOrder = SparePartOrder::factory()->ordered()->create(); $response = $this->actingAs($this->managerUser) ->withSession([ 'navigation' => [ 'set-in-stock-manager-token' => [ 'updated_at' => time(), 'stack' => [ route('spare_part_orders.index'), route('spare_part_orders.show', $sparePartOrder), ], ], ], ]) ->post(route('spare_part_orders.set_in_stock', [ 'sparePartOrder' => $sparePartOrder, 'nav' => 'set-in-stock-manager-token', ])); $response->assertRedirect(route('spare_part_orders.show', [ 'sparePartOrder' => $sparePartOrder, 'nav' => 'set-in-stock-manager-token', ])); $response->assertSessionHas('success'); } public function test_guest_cannot_set_order_in_stock(): void { $sparePartOrder = SparePartOrder::factory()->create(); $response = $this->post(route('spare_part_orders.set_in_stock', $sparePartOrder)); $response->assertRedirect(route('login')); } public function test_admin_can_set_whole_order_in_stock_by_order_number(): void { $first = SparePartOrder::factory()->ordered()->create([ 'order_number' => 'SP-BULK-100', ]); $second = SparePartOrder::factory()->ordered()->create([ 'order_number' => 'SP-BULK-100', ]); $sameNumberInStock = SparePartOrder::factory()->inStock()->create([ 'order_number' => 'SP-BULK-100', ]); $other = SparePartOrder::factory()->ordered()->create([ 'order_number' => 'SP-BULK-200', ]); $response = $this->actingAs($this->adminUser) ->post(route('spare_part_orders.set_order_in_stock'), [ 'bulk_order_number' => 'SP-BULK-100', ]); $response->assertRedirect(route('spare_part_orders.index')); $response->assertSessionHas('success'); $this->assertDatabaseHas('spare_part_orders', [ 'id' => $first->id, 'status' => SparePartOrder::STATUS_IN_STOCK, ]); $this->assertDatabaseHas('spare_part_orders', [ 'id' => $second->id, 'status' => SparePartOrder::STATUS_IN_STOCK, ]); $this->assertDatabaseHas('spare_part_orders', [ 'id' => $sameNumberInStock->id, 'status' => SparePartOrder::STATUS_IN_STOCK, ]); $this->assertDatabaseHas('spare_part_orders', [ 'id' => $other->id, 'status' => SparePartOrder::STATUS_ORDERED, ]); } public function test_bulk_set_order_in_stock_validates_selected_number_has_ordered_rows(): void { SparePartOrder::factory()->inStock()->create([ 'order_number' => 'SP-NOT-ORDERED', ]); $response = $this->from(route('spare_part_orders.index')) ->actingAs($this->adminUser) ->post(route('spare_part_orders.set_order_in_stock'), [ 'bulk_order_number' => 'SP-NOT-ORDERED', ]); $response->assertRedirect(route('spare_part_orders.index')); $response->assertSessionHasErrors('bulk_order_number'); } // ==================== Ship ==================== public function test_admin_can_ship_spare_part_order(): void { $sparePartOrder = SparePartOrder::factory()->inStock()->withQuantity(20)->create(); $response = $this->actingAs($this->adminUser) ->post(route('spare_part_orders.ship', $sparePartOrder), [ 'quantity' => 5, 'note' => 'Тестовая отгрузка', ]); $this->assertRedirectsToShowWithGeneratedNav($response, $sparePartOrder); $response->assertSessionHas('success'); $this->assertDatabaseHas('spare_part_orders', [ 'id' => $sparePartOrder->id, 'available_qty' => 15, ]); } public function test_ship_fails_if_quantity_exceeds_available(): void { $sparePartOrder = SparePartOrder::factory()->inStock()->withQuantity(3)->create(); $response = $this->actingAs($this->adminUser) ->post(route('spare_part_orders.ship', $sparePartOrder), [ 'quantity' => 10, 'note' => 'Попытка отгрузить больше чем есть', ]); $this->assertRedirectsToShowWithGeneratedNav($response, $sparePartOrder); $response->assertSessionHas('error'); $this->assertDatabaseHas('spare_part_orders', [ 'id' => $sparePartOrder->id, 'available_qty' => 3, ]); } public function test_ship_validates_required_quantity(): void { $sparePartOrder = SparePartOrder::factory()->inStock()->create(); $response = $this->actingAs($this->adminUser) ->post(route('spare_part_orders.ship', $sparePartOrder), [ 'note' => 'Без количества', ]); $response->assertSessionHasErrors('quantity'); } public function test_manager_can_ship_spare_part_order(): void { $sparePartOrder = SparePartOrder::factory()->inStock()->withQuantity(10)->create(); $response = $this->actingAs($this->managerUser) ->post(route('spare_part_orders.ship', $sparePartOrder), [ 'quantity' => 2, 'note' => 'Отгрузка менеджером', ]); $this->assertRedirectsToShowWithGeneratedNav($response, $sparePartOrder); $response->assertSessionHas('success'); } public function test_guest_cannot_ship_spare_part_order(): void { $sparePartOrder = SparePartOrder::factory()->create(); $response = $this->post(route('spare_part_orders.ship', $sparePartOrder), [ 'quantity' => 1, 'note' => 'Test', ]); $response->assertRedirect(route('login')); } // ==================== Correct (Inventory) ==================== public function test_admin_can_correct_inventory(): void { $sparePartOrder = SparePartOrder::factory()->inStock()->withQuantity(10)->create(); $response = $this->actingAs($this->adminUser) ->post(route('spare_part_orders.correct', $sparePartOrder), [ 'new_quantity' => 8, 'reason' => 'Инвентаризация — обнаружена недостача', ]); $this->assertRedirectsToShowWithGeneratedNav($response, $sparePartOrder); $response->assertSessionHas('success'); $this->assertDatabaseHas('spare_part_orders', [ 'id' => $sparePartOrder->id, 'available_qty' => 8, ]); } public function test_correct_fails_if_quantity_unchanged(): void { $sparePartOrder = SparePartOrder::factory()->inStock()->withQuantity(10)->create(); $response = $this->actingAs($this->adminUser) ->post(route('spare_part_orders.correct', $sparePartOrder), [ 'new_quantity' => 10, 'reason' => 'Без изменений', ]); $this->assertRedirectsToShowWithGeneratedNav($response, $sparePartOrder); $response->assertSessionHas('error'); } public function test_manager_cannot_correct_inventory(): void { $sparePartOrder = SparePartOrder::factory()->inStock()->create(); $response = $this->actingAs($this->managerUser) ->post(route('spare_part_orders.correct', $sparePartOrder), [ 'new_quantity' => 5, 'reason' => 'Test', ]); $response->assertStatus(403); } public function test_guest_cannot_correct_inventory(): void { $sparePartOrder = SparePartOrder::factory()->create(); $response = $this->post(route('spare_part_orders.correct', $sparePartOrder), [ 'new_quantity' => 5, 'reason' => 'Test', ]); $response->assertRedirect(route('login')); } }