|
@@ -0,0 +1,326 @@
|
|
|
|
|
+<?php
|
|
|
|
|
+
|
|
|
|
|
+declare(strict_types=1);
|
|
|
|
|
+
|
|
|
|
|
+namespace Tests\Feature;
|
|
|
|
|
+
|
|
|
|
|
+use App\Enums\ProductionOrderActivityType;
|
|
|
|
|
+use App\Enums\ProductionOrderStatus;
|
|
|
|
|
+use App\Models\ProductionOrder;
|
|
|
|
|
+use App\Models\ProductionOrderDelivery;
|
|
|
|
|
+use App\Models\ProductionOrderInstallation;
|
|
|
|
|
+use App\Models\Role;
|
|
|
|
|
+use App\Models\User;
|
|
|
|
|
+use Illuminate\Foundation\Testing\RefreshDatabase;
|
|
|
|
|
+use Tests\TestCase;
|
|
|
|
|
+
|
|
|
|
|
+class ProductionOrderPlanningControllerTest extends TestCase
|
|
|
|
|
+{
|
|
|
|
|
+ use RefreshDatabase;
|
|
|
|
|
+
|
|
|
|
|
+ protected bool $seed = true;
|
|
|
|
|
+
|
|
|
|
|
+ private User $admin;
|
|
|
|
|
+
|
|
|
|
|
+ private User $assistant;
|
|
|
|
|
+
|
|
|
|
|
+ private User $manager;
|
|
|
|
|
+
|
|
|
|
|
+ private User $driver;
|
|
|
|
|
+
|
|
|
|
|
+ private User $brigadier;
|
|
|
|
|
+
|
|
|
|
|
+ private ProductionOrder $order;
|
|
|
|
|
+
|
|
|
|
|
+ protected function setUp(): void
|
|
|
|
|
+ {
|
|
|
|
|
+ parent::setUp();
|
|
|
|
|
+
|
|
|
|
|
+ $this->admin = User::factory()->admin()->create();
|
|
|
|
|
+ $this->assistant = User::factory()->assistantHead()->create();
|
|
|
|
|
+ $this->manager = User::factory()->manager()->create();
|
|
|
|
|
+ $this->driver = User::factory()->driver()->create();
|
|
|
|
|
+ $this->brigadier = User::factory()->brigadier()->create();
|
|
|
|
|
+ $this->order = ProductionOrder::factory()->create(['manager_id' => $this->manager->id]);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ public function test_admin_can_add_multiple_deliveries_and_installations_with_audit(): void
|
|
|
|
|
+ {
|
|
|
|
|
+ $this->actingAs($this->admin)
|
|
|
|
|
+ ->post(route('schedule.orders.deliveries.store', $this->order), $this->deliveryData([
|
|
|
|
|
+ 'delivery_date' => '2026-09-10',
|
|
|
|
|
+ ]))
|
|
|
|
|
+ ->assertRedirect();
|
|
|
|
|
+
|
|
|
|
|
+ $this->actingAs($this->admin)
|
|
|
|
|
+ ->post(route('schedule.orders.deliveries.store', $this->order), $this->deliveryData([
|
|
|
|
|
+ 'delivery_date' => '2026-09-12',
|
|
|
|
|
+ 'driver_id' => $this->brigadier->id,
|
|
|
|
|
+ ]))
|
|
|
|
|
+ ->assertRedirect();
|
|
|
|
|
+
|
|
|
|
|
+ $this->actingAs($this->admin)
|
|
|
|
|
+ ->post(route('schedule.orders.installations.store', $this->order), $this->installationData())
|
|
|
|
|
+ ->assertRedirect();
|
|
|
|
|
+
|
|
|
|
|
+ $this->assertSame(2, $this->order->deliveries()->count());
|
|
|
|
|
+ $this->assertSame(1, $this->order->installations()->count());
|
|
|
|
|
+ $installation = $this->order->installations()->firstOrFail();
|
|
|
|
|
+ $this->assertSame(3, $installation->schedules()->count());
|
|
|
|
|
+ $this->assertSame(
|
|
|
|
|
+ ['2026-09-20', '2026-09-21', '2026-09-22'],
|
|
|
|
|
+ $installation->schedules()->orderBy('installation_date')->pluck('installation_date')->all(),
|
|
|
|
|
+ );
|
|
|
|
|
+ $this->assertDatabaseHas('production_order_activities', [
|
|
|
|
|
+ 'production_order_id' => $this->order->id,
|
|
|
|
|
+ 'type' => ProductionOrderActivityType::DeliveryScheduled->value,
|
|
|
|
|
+ 'user_id' => $this->admin->id,
|
|
|
|
|
+ 'message' => 'Заказ добавлен в график доставок.',
|
|
|
|
|
+ ]);
|
|
|
|
|
+ $this->assertDatabaseHas('production_order_activities', [
|
|
|
|
|
+ 'production_order_id' => $this->order->id,
|
|
|
|
|
+ 'type' => ProductionOrderActivityType::InstallationScheduled->value,
|
|
|
|
|
+ 'user_id' => $this->admin->id,
|
|
|
|
|
+ 'message' => 'Заказ добавлен в график монтажей.',
|
|
|
|
|
+ ]);
|
|
|
|
|
+ $this->assertSame(3, $this->order->activities()->count());
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ public function test_assistant_can_update_and_delete_planning_records_without_extra_audit(): void
|
|
|
|
|
+ {
|
|
|
|
|
+ $delivery = ProductionOrderDelivery::factory()->create([
|
|
|
|
|
+ 'production_order_id' => $this->order->id,
|
|
|
|
|
+ 'driver_id' => $this->driver->id,
|
|
|
|
|
+ 'delivery_date' => '2026-09-10',
|
|
|
|
|
+ ]);
|
|
|
|
|
+ $installation = ProductionOrderInstallation::factory()->create([
|
|
|
|
|
+ 'production_order_id' => $this->order->id,
|
|
|
|
|
+ 'brigadier_id' => $this->brigadier->id,
|
|
|
|
|
+ ]);
|
|
|
|
|
+
|
|
|
|
|
+ $this->actingAs($this->assistant)
|
|
|
|
|
+ ->put(
|
|
|
|
|
+ route('schedule.orders.deliveries.update', [$this->order, $delivery]),
|
|
|
|
|
+ $this->deliveryData(['delivery_date' => '2026-09-15']),
|
|
|
|
|
+ )
|
|
|
|
|
+ ->assertRedirect();
|
|
|
|
|
+
|
|
|
|
|
+ $this->actingAs($this->assistant)
|
|
|
|
|
+ ->put(
|
|
|
|
|
+ route('schedule.orders.installations.update', [$this->order, $installation]),
|
|
|
|
|
+ $this->installationData(['installation_days' => 5]),
|
|
|
|
|
+ )
|
|
|
|
|
+ ->assertRedirect();
|
|
|
|
|
+
|
|
|
|
|
+ $this->assertDatabaseHas('production_order_deliveries', [
|
|
|
|
|
+ 'id' => $delivery->id,
|
|
|
|
|
+ 'delivery_date' => '2026-09-15',
|
|
|
|
|
+ 'updated_by' => $this->assistant->id,
|
|
|
|
|
+ ]);
|
|
|
|
|
+ $this->assertDatabaseHas('production_order_installations', [
|
|
|
|
|
+ 'id' => $installation->id,
|
|
|
|
|
+ 'installation_days' => 5,
|
|
|
|
|
+ 'updated_by' => $this->assistant->id,
|
|
|
|
|
+ ]);
|
|
|
|
|
+ $this->assertSame(5, $installation->schedules()->count());
|
|
|
|
|
+ $this->assertDatabaseHas('schedules', [
|
|
|
|
|
+ 'production_order_installation_id' => $installation->id,
|
|
|
|
|
+ 'installation_date' => '2026-09-24',
|
|
|
|
|
+ 'source' => \App\Models\Schedule::SOURCE_PRODUCTION_ORDER,
|
|
|
|
|
+ 'brigadier_id' => $this->brigadier->id,
|
|
|
|
|
+ ]);
|
|
|
|
|
+ $this->assertSame(0, $this->order->activities()->count());
|
|
|
|
|
+
|
|
|
|
|
+ $this->actingAs($this->assistant)
|
|
|
|
|
+ ->delete(route('schedule.orders.deliveries.destroy', [$this->order, $delivery]))
|
|
|
|
|
+ ->assertRedirect();
|
|
|
|
|
+ $this->actingAs($this->assistant)
|
|
|
|
|
+ ->delete(route('schedule.orders.installations.destroy', [$this->order, $installation]))
|
|
|
|
|
+ ->assertRedirect();
|
|
|
|
|
+
|
|
|
|
|
+ $this->assertSoftDeleted('production_order_deliveries', ['id' => $delivery->id]);
|
|
|
|
|
+ $this->assertSoftDeleted('production_order_installations', ['id' => $installation->id]);
|
|
|
|
|
+ $this->assertDatabaseMissing('schedules', ['production_order_installation_id' => $installation->id]);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ public function test_manager_cannot_manage_delivery_or_installation(): void
|
|
|
|
|
+ {
|
|
|
|
|
+ $this->actingAs($this->manager)
|
|
|
|
|
+ ->post(route('schedule.orders.deliveries.store', $this->order), $this->deliveryData())
|
|
|
|
|
+ ->assertForbidden();
|
|
|
|
|
+
|
|
|
|
|
+ $this->actingAs($this->manager)
|
|
|
|
|
+ ->post(route('schedule.orders.installations.store', $this->order), $this->installationData())
|
|
|
|
|
+ ->assertForbidden();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ public function test_delivery_and_installation_require_users_with_allowed_roles(): void
|
|
|
|
|
+ {
|
|
|
|
|
+ $wrongRole = User::factory()->create(['role' => Role::MANAGER]);
|
|
|
|
|
+
|
|
|
|
|
+ $this->actingAs($this->admin)
|
|
|
|
|
+ ->post(route('schedule.orders.deliveries.store', $this->order), $this->deliveryData([
|
|
|
|
|
+ 'driver_id' => $wrongRole->id,
|
|
|
|
|
+ ]))
|
|
|
|
|
+ ->assertSessionHasErrors('driver_id');
|
|
|
|
|
+
|
|
|
|
|
+ $this->actingAs($this->admin)
|
|
|
|
|
+ ->post(route('schedule.orders.installations.store', $this->order), $this->installationData([
|
|
|
|
|
+ 'brigadier_id' => $this->driver->id,
|
|
|
|
|
+ ]))
|
|
|
|
|
+ ->assertSessionHasErrors('brigadier_id');
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ public function test_planning_record_from_another_order_returns_not_found(): void
|
|
|
|
|
+ {
|
|
|
|
|
+ $otherOrder = ProductionOrder::factory()->create(['manager_id' => $this->manager->id]);
|
|
|
|
|
+ $delivery = ProductionOrderDelivery::factory()->create([
|
|
|
|
|
+ 'production_order_id' => $otherOrder->id,
|
|
|
|
|
+ 'driver_id' => $this->driver->id,
|
|
|
|
|
+ ]);
|
|
|
|
|
+
|
|
|
|
|
+ $this->actingAs($this->admin)
|
|
|
|
|
+ ->put(
|
|
|
|
|
+ route('schedule.orders.deliveries.update', [$this->order, $delivery]),
|
|
|
|
|
+ $this->deliveryData(),
|
|
|
|
|
+ )
|
|
|
|
|
+ ->assertNotFound();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ public function test_order_card_shows_planning_and_audit_to_read_only_role(): void
|
|
|
|
|
+ {
|
|
|
|
|
+ $delivery = ProductionOrderDelivery::factory()->create([
|
|
|
|
|
+ 'production_order_id' => $this->order->id,
|
|
|
|
|
+ 'driver_id' => $this->driver->id,
|
|
|
|
|
+ 'delivery_date' => '2026-09-18',
|
|
|
|
|
+ ]);
|
|
|
|
|
+ $this->order->activities()->create([
|
|
|
|
|
+ 'type' => ProductionOrderActivityType::DeliveryScheduled,
|
|
|
|
|
+ 'user_id' => $this->admin->id,
|
|
|
|
|
+ 'delivery_id' => $delivery->id,
|
|
|
|
|
+ 'message' => 'Заказ добавлен в график доставок.',
|
|
|
|
|
+ ]);
|
|
|
|
|
+
|
|
|
|
|
+ $this->actingAs($this->manager)
|
|
|
|
|
+ ->get(route('schedule.orders.show', $this->order))
|
|
|
|
|
+ ->assertOk()
|
|
|
|
|
+ ->assertSee('18.09.2026')
|
|
|
|
|
+ ->assertSee($this->driver->name)
|
|
|
|
|
+ ->assertSee('Заказ добавлен в график доставок.')
|
|
|
|
|
+ ->assertDontSee('Добавить доставку');
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ public function test_status_change_is_the_only_order_field_update_written_to_audit(): void
|
|
|
|
|
+ {
|
|
|
|
|
+ $this->order->update(['note' => 'Обычное изменение']);
|
|
|
|
|
+ $this->assertSame(0, $this->order->activities()->count());
|
|
|
|
|
+
|
|
|
|
|
+ $service = app(\App\Services\ProductionOrderService::class);
|
|
|
|
|
+ $service->update(
|
|
|
|
|
+ $this->order,
|
|
|
|
|
+ ['status' => ProductionOrderStatus::InTransit->value],
|
|
|
|
|
+ $this->admin,
|
|
|
|
|
+ false,
|
|
|
|
|
+ );
|
|
|
|
|
+
|
|
|
|
|
+ $this->assertDatabaseHas('production_order_activities', [
|
|
|
|
|
+ 'production_order_id' => $this->order->id,
|
|
|
|
|
+ 'type' => ProductionOrderActivityType::StatusChanged->value,
|
|
|
|
|
+ 'message' => 'Изменён статус заказа.',
|
|
|
|
|
+ ]);
|
|
|
|
|
+ $this->assertSame(1, $this->order->activities()->count());
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ public function test_weekly_delivery_schedule_contains_only_selected_week(): void
|
|
|
|
|
+ {
|
|
|
|
|
+ ProductionOrderDelivery::factory()->create([
|
|
|
|
|
+ 'production_order_id' => $this->order->id,
|
|
|
|
|
+ 'driver_id' => $this->driver->id,
|
|
|
|
|
+ 'delivery_date' => '2026-09-10',
|
|
|
|
|
+ 'delivery_time' => '10:30',
|
|
|
|
|
+ 'carrier' => 'Нужный перевозчик',
|
|
|
|
|
+ ]);
|
|
|
|
|
+ ProductionOrderDelivery::factory()->create([
|
|
|
|
|
+ 'production_order_id' => $this->order->id,
|
|
|
|
|
+ 'driver_id' => $this->driver->id,
|
|
|
|
|
+ 'delivery_date' => '2026-09-20',
|
|
|
|
|
+ 'carrier' => 'Перевозчик другой недели',
|
|
|
|
|
+ ]);
|
|
|
|
|
+
|
|
|
|
|
+ $this->actingAs($this->manager)
|
|
|
|
|
+ ->withSession(['year' => 2020])
|
|
|
|
|
+ ->get(route('schedule.deliveries', ['week' => '2026-09-07']))
|
|
|
|
|
+ ->assertOk()
|
|
|
|
|
+ ->assertViewIs('production_order_deliveries.index')
|
|
|
|
|
+ ->assertSee('Нужный перевозчик')
|
|
|
|
|
+ ->assertSee($this->order->order_number)
|
|
|
|
|
+ ->assertSee($this->driver->name)
|
|
|
|
|
+ ->assertDontSee('Перевозчик другой недели');
|
|
|
|
|
+
|
|
|
|
|
+ $this->actingAs($this->driver)
|
|
|
|
|
+ ->get(route('schedule.deliveries', ['week' => '2026-09-07']))
|
|
|
|
|
+ ->assertOk();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ public function test_installation_is_visible_in_existing_schedule_and_managed_from_order_card(): void
|
|
|
|
|
+ {
|
|
|
|
|
+ $this->actingAs($this->admin)
|
|
|
|
|
+ ->post(route('schedule.orders.installations.store', $this->order), $this->installationData([
|
|
|
|
|
+ 'installation_date' => '2026-09-15',
|
|
|
|
|
+ 'installation_days' => 2,
|
|
|
|
|
+ ]))
|
|
|
|
|
+ ->assertRedirect();
|
|
|
|
|
+
|
|
|
|
|
+ $installation = $this->order->installations()->firstOrFail();
|
|
|
|
|
+ $schedule = $installation->schedules()->firstOrFail();
|
|
|
|
|
+
|
|
|
|
|
+ $this->actingAs($this->admin)
|
|
|
|
|
+ ->get(route('schedule.index', ['tab' => 'week', 'year' => 2026, 'week' => 38]))
|
|
|
|
|
+ ->assertOk()
|
|
|
|
|
+ ->assertSee($this->order->order_number)
|
|
|
|
|
+ ->assertSee($this->order->object_address)
|
|
|
|
|
+ ->assertSee('Размещен')
|
|
|
|
|
+ ->assertSee(route('schedule.orders.show', $this->order), false);
|
|
|
|
|
+
|
|
|
|
|
+ $this->actingAs($this->admin)
|
|
|
|
|
+ ->delete(route('schedule.delete', $schedule))
|
|
|
|
|
+ ->assertForbidden();
|
|
|
|
|
+
|
|
|
|
|
+ $this->assertDatabaseHas('schedules', ['id' => $schedule->id]);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /** @return array<string, mixed> */
|
|
|
|
|
+ private function deliveryData(array $overrides = []): array
|
|
|
|
|
+ {
|
|
|
|
|
+ return array_replace([
|
|
|
|
|
+ 'delivery_date' => '2026-09-10',
|
|
|
|
|
+ 'delivery_time' => '10:30',
|
|
|
|
|
+ 'driver_id' => $this->driver->id,
|
|
|
|
|
+ 'carrier' => 'ООО Перевозчик',
|
|
|
|
|
+ 'request_number' => 'ЗД-100',
|
|
|
|
|
+ 'request_date' => '2026-09-08',
|
|
|
|
|
+ 'carrier_note' => 'Позвонить заранее',
|
|
|
|
|
+ 'contact_position' => 'Прораб',
|
|
|
|
|
+ 'contact_name' => 'Иван Иванов',
|
|
|
|
|
+ 'contact_phone' => '+7 900 000-00-00',
|
|
|
|
|
+ 'desired_time' => '11:00',
|
|
|
|
|
+ 'access_system' => 'По пропуску',
|
|
|
|
|
+ 'unloading_place' => 'Вторые ворота',
|
|
|
|
|
+ 'advance_notice' => 'За один час',
|
|
|
|
|
+ 'document_signing_method' => 'Оригинал на объекте',
|
|
|
|
|
+ 'has_passports_and_certificates' => true,
|
|
|
|
|
+ 'note' => 'Без опозданий',
|
|
|
|
|
+ ], $overrides);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /** @return array<string, mixed> */
|
|
|
|
|
+ private function installationData(array $overrides = []): array
|
|
|
|
|
+ {
|
|
|
|
|
+ return array_replace([
|
|
|
|
|
+ 'installation_date' => '2026-09-20',
|
|
|
|
|
+ 'installation_days' => 3,
|
|
|
|
|
+ 'brigadier_id' => $this->brigadier->id,
|
|
|
|
|
+ 'note' => 'Первый этап',
|
|
|
|
|
+ ], $overrides);
|
|
|
|
|
+ }
|
|
|
|
|
+}
|