ProductionOrderControllerTest.php 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330
  1. <?php
  2. declare(strict_types=1);
  3. namespace Tests\Feature;
  4. use App\Enums\ProductionOrderExecutionType;
  5. use App\Enums\ProductionOrderStatus;
  6. use App\Models\CommonCatalogItem;
  7. use App\Models\ProductionCalendarDay;
  8. use App\Models\ProductionOrder;
  9. use App\Models\ProductionOrderItem;
  10. use App\Models\User;
  11. use Illuminate\Foundation\Testing\RefreshDatabase;
  12. use Tests\TestCase;
  13. class ProductionOrderControllerTest extends TestCase
  14. {
  15. use RefreshDatabase;
  16. protected bool $seed = true;
  17. private User $admin;
  18. private User $manager;
  19. private User $assistant;
  20. private CommonCatalogItem $catalogItem;
  21. protected function setUp(): void
  22. {
  23. parent::setUp();
  24. $this->admin = User::factory()->admin()->create();
  25. $this->manager = User::factory()->manager()->create();
  26. $this->assistant = User::factory()->assistantHead()->create();
  27. $this->catalogItem = CommonCatalogItem::factory()->create([
  28. 'article' => 'ГЗ-001',
  29. 'calculator_name' => 'Игровой комплекс',
  30. ]);
  31. }
  32. public function test_authorized_roles_can_view_year_independent_order_schedule(): void
  33. {
  34. $visible = ProductionOrder::factory()->create([
  35. 'order_number' => 'ГЗ-2026',
  36. 'order_year' => 2026,
  37. 'manager_id' => $this->manager->id,
  38. ]);
  39. ProductionOrder::factory()->create([
  40. 'order_number' => 'ГЗ-2024',
  41. 'order_year' => 2024,
  42. 'manager_id' => $this->manager->id,
  43. ]);
  44. $this->get(route('schedule.orders'))->assertRedirect(route('login'));
  45. $this->actingAs($this->manager)
  46. ->withSession(['year' => 2020])
  47. ->get(route('schedule.orders'))
  48. ->assertOk()
  49. ->assertViewIs('production_orders.index')
  50. ->assertSee($visible->order_number)
  51. ->assertSee('ГЗ-2024');
  52. $driver = User::factory()->driver()->create();
  53. $this->actingAs($driver)->get(route('schedule.orders'))->assertOk();
  54. }
  55. public function test_admin_can_create_order_with_items_and_calendar_shipment_date(): void
  56. {
  57. ProductionCalendarDay::factory()->create([
  58. 'date' => '2026-09-07',
  59. 'is_working_day' => false,
  60. 'name' => 'Перенесённый выходной',
  61. ]);
  62. $secondCatalogItem = CommonCatalogItem::factory()->create([
  63. 'article' => 'ГЗ-002',
  64. 'calculator_name' => 'Качели',
  65. ]);
  66. $response = $this->actingAs($this->admin)->post(
  67. route('schedule.orders.store'),
  68. $this->validData([
  69. 'order_number' => 'ЗАК-100',
  70. 'payment_date' => '2026-09-04',
  71. 'supply_working_days' => 1,
  72. 'items' => [
  73. [
  74. 'common_catalog_item_id' => $this->catalogItem->id,
  75. 'order_item_number' => 'МАФ-100/1',
  76. ],
  77. [
  78. 'common_catalog_item_id' => $secondCatalogItem->id,
  79. 'order_item_number' => 'МАФ-100/2',
  80. 'factory_number' => 'ЗН-002',
  81. 'manufacture_date' => '2026-09-03',
  82. ],
  83. ],
  84. ]),
  85. );
  86. $order = ProductionOrder::query()->where('order_number', 'ЗАК-100')->firstOrFail();
  87. $response->assertRedirect();
  88. $this->assertSame('2026-09-08', $order->contract_shipment_date->toDateString());
  89. $this->assertSame($this->admin->id, $order->created_by);
  90. $this->assertSame(2, $order->items()->count());
  91. $this->assertDatabaseHas('production_order_items', [
  92. 'production_order_id' => $order->id,
  93. 'factory_number' => 'ЗН-002',
  94. 'manufacture_date' => '2026-09-03',
  95. ]);
  96. }
  97. public function test_order_number_is_unique_within_year(): void
  98. {
  99. ProductionOrder::factory()->create([
  100. 'order_number' => 'ДУБЛЬ-1',
  101. 'order_year' => 2026,
  102. 'manager_id' => $this->manager->id,
  103. ]);
  104. $this->actingAs($this->admin)
  105. ->post(route('schedule.orders.store'), $this->validData([
  106. 'order_number' => 'ДУБЛЬ-1',
  107. 'order_year' => 2026,
  108. ]))
  109. ->assertSessionHasErrors('order_number');
  110. $this->actingAs($this->admin)
  111. ->post(route('schedule.orders.store'), $this->validData([
  112. 'order_number' => 'ДУБЛЬ-1',
  113. 'order_year' => 2027,
  114. ]))
  115. ->assertSessionHasNoErrors();
  116. $this->assertDatabaseHas('production_orders', [
  117. 'order_number' => 'ДУБЛЬ-1',
  118. 'order_year' => 2027,
  119. ]);
  120. }
  121. public function test_manager_cannot_create_or_update_production_order(): void
  122. {
  123. $order = ProductionOrder::factory()->create(['manager_id' => $this->manager->id]);
  124. $this->actingAs($this->manager)
  125. ->post(route('schedule.orders.store'), $this->validData())
  126. ->assertForbidden();
  127. $this->actingAs($this->manager)
  128. ->put(route('schedule.orders.update', $order), ['note' => 'Подмена'])
  129. ->assertForbidden();
  130. }
  131. public function test_assistant_can_update_only_allowed_order_and_item_fields(): void
  132. {
  133. $otherCatalogItem = CommonCatalogItem::factory()->create();
  134. $order = ProductionOrder::factory()->create([
  135. 'order_number' => 'ЗАК-OLD',
  136. 'customer_name' => 'Исходный заказчик',
  137. 'object_address' => 'Старый адрес',
  138. 'status' => ProductionOrderStatus::Placed,
  139. 'manager_id' => $this->manager->id,
  140. ]);
  141. $item = ProductionOrderItem::factory()->create([
  142. 'production_order_id' => $order->id,
  143. 'common_catalog_item_id' => $this->catalogItem->id,
  144. 'order_item_number' => 'МАФ-OLD',
  145. ]);
  146. $this->actingAs($this->assistant)
  147. ->put(route('schedule.orders.update', $order), [
  148. 'order_number' => 'ЗАК-FORGED',
  149. 'customer_name' => 'Подменённый заказчик',
  150. 'object_address' => 'Новый адрес',
  151. 'status' => ProductionOrderStatus::Closed->value,
  152. 'note' => 'Уточнённое примечание',
  153. 'items' => [[
  154. 'id' => $item->id,
  155. 'common_catalog_item_id' => $otherCatalogItem->id,
  156. 'order_item_number' => 'МАФ-FORGED',
  157. 'factory_number' => 'ЗН-NEW',
  158. 'manufacture_date' => '2026-09-02',
  159. ]],
  160. ])
  161. ->assertRedirect();
  162. $order->refresh();
  163. $item->refresh();
  164. $this->assertSame('ЗАК-OLD', $order->order_number);
  165. $this->assertSame('Исходный заказчик', $order->customer_name);
  166. $this->assertSame('Новый адрес', $order->object_address);
  167. $this->assertSame(ProductionOrderStatus::Placed, $order->status);
  168. $this->assertSame('Уточнённое примечание', $order->note);
  169. $this->assertSame($this->catalogItem->id, $item->common_catalog_item_id);
  170. $this->assertSame('МАФ-OLD', $item->order_item_number);
  171. $this->assertSame('ЗН-NEW', $item->factory_number);
  172. $this->assertSame('2026-09-02', $item->manufacture_date?->toDateString());
  173. }
  174. public function test_order_card_displays_separate_catalog_items(): void
  175. {
  176. $order = ProductionOrder::factory()->create(['manager_id' => $this->manager->id]);
  177. ProductionOrderItem::factory()->create([
  178. 'production_order_id' => $order->id,
  179. 'common_catalog_item_id' => $this->catalogItem->id,
  180. 'order_item_number' => 'МАФ-КАРТОЧКА-1',
  181. ]);
  182. $this->actingAs($this->manager)
  183. ->get(route('schedule.orders.show', $order))
  184. ->assertOk()
  185. ->assertSee('ГЗ-001')
  186. ->assertSee('Игровой комплекс')
  187. ->assertSee('МАФ-КАРТОЧКА-1');
  188. }
  189. public function test_status_filter_uses_labels_and_filters_by_enum_value(): void
  190. {
  191. ProductionOrder::factory()->create([
  192. 'order_number' => 'ГЗ-РАЗМЕЩЕН',
  193. 'status' => ProductionOrderStatus::Placed,
  194. 'manager_id' => $this->manager->id,
  195. ]);
  196. ProductionOrder::factory()->create([
  197. 'order_number' => 'ГЗ-СКЛАД',
  198. 'status' => ProductionOrderStatus::InStock,
  199. 'manager_id' => $this->manager->id,
  200. ]);
  201. $this->actingAs($this->manager)
  202. ->getJson(route('getFilters', [
  203. 'table' => 'schedule_orders',
  204. 'column' => 'status_name',
  205. ]))
  206. ->assertOk()
  207. ->assertJsonFragment(['Размещен'])
  208. ->assertJsonFragment(['На складе']);
  209. $this->actingAs($this->manager)
  210. ->get(route('schedule.orders', [
  211. 'filters' => ['status_name' => 'На складе'],
  212. ]))
  213. ->assertOk()
  214. ->assertSee('ГЗ-СКЛАД')
  215. ->assertDontSee('ГЗ-РАЗМЕЩЕН');
  216. }
  217. public function test_admin_can_search_common_catalog_when_adding_order_items(): void
  218. {
  219. $matchingByName = CommonCatalogItem::factory()->create([
  220. 'article' => 'SEARCH-002',
  221. 'calculator_name' => 'Особенная карусель',
  222. ]);
  223. CommonCatalogItem::factory()->create([
  224. 'article' => 'OTHER-003',
  225. 'calculator_name' => 'Другая позиция',
  226. ]);
  227. $deleted = CommonCatalogItem::factory()->create([
  228. 'article' => 'SEARCH-DELETED',
  229. 'calculator_name' => 'Удалённая карусель',
  230. ]);
  231. $deleted->delete();
  232. $this->actingAs($this->admin)
  233. ->getJson(route('schedule.orders.catalog-search', ['q' => 'карусель']))
  234. ->assertOk()
  235. ->assertJsonFragment([
  236. 'id' => $matchingByName->id,
  237. 'label' => 'SEARCH-002 — Особенная карусель',
  238. ])
  239. ->assertJsonMissing(['id' => $deleted->id]);
  240. $this->actingAs($this->admin)
  241. ->getJson(route('schedule.orders.catalog-search', ['q' => 'ГЗ-001']))
  242. ->assertOk()
  243. ->assertJsonFragment(['id' => $this->catalogItem->id]);
  244. $this->actingAs($this->manager)
  245. ->getJson(route('schedule.orders.catalog-search', ['q' => 'карусель']))
  246. ->assertForbidden();
  247. }
  248. public function test_create_form_loads_catalog_items_only_through_search(): void
  249. {
  250. $unrelated = CommonCatalogItem::factory()->create([
  251. 'article' => 'НЕ-ЗАГРУЖАТЬ',
  252. 'calculator_name' => 'Тяжёлая позиция каталога',
  253. ]);
  254. $this->actingAs($this->admin)
  255. ->get(route('schedule.orders.create'))
  256. ->assertOk()
  257. ->assertSee('Добавить МАФ из общего каталога')
  258. ->assertSee('production-order-catalog-search', false)
  259. ->assertDontSee($unrelated->article);
  260. }
  261. /** @return array<string, mixed> */
  262. private function validData(array $overrides = []): array
  263. {
  264. return array_replace_recursive([
  265. 'order_number' => 'ЗАК-001',
  266. 'order_year' => 2026,
  267. 'customer_name' => 'ООО Заказчик',
  268. 'object_address' => 'г. Москва, ул. Строителей, 1',
  269. 'invoice_number' => 'СЧ-001',
  270. 'invoice_date' => '2026-09-01',
  271. 'contract_number' => 'Д-001',
  272. 'contract_date' => '2026-08-31',
  273. 'payment_date' => '2026-09-01',
  274. 'supply_working_days' => 10,
  275. 'application_shipment_date' => '2026-09-20',
  276. 'status' => ProductionOrderStatus::Placed->value,
  277. 'execution_type' => ProductionOrderExecutionType::Delivery->value,
  278. 'manager_id' => $this->manager->id,
  279. 'note' => 'Тестовый заказ',
  280. 'items' => [[
  281. 'common_catalog_item_id' => $this->catalogItem->id,
  282. 'order_item_number' => 'МАФ-001',
  283. 'factory_number' => null,
  284. 'manufacture_date' => null,
  285. ]],
  286. ], $overrides);
  287. }
  288. }