ProductionOrderControllerTest.php 14 KB

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