SparePartOrderObserverTest.php 8.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271
  1. <?php
  2. namespace Tests\Unit\Observers;
  3. use App\Models\Reclamation;
  4. use App\Models\Reservation;
  5. use App\Models\Shortage;
  6. use App\Models\SparePart;
  7. use App\Models\SparePartOrder;
  8. use Illuminate\Foundation\Testing\RefreshDatabase;
  9. use Tests\TestCase;
  10. class SparePartOrderObserverTest extends TestCase
  11. {
  12. use RefreshDatabase;
  13. protected $seed = true;
  14. public function test_creating_order_with_in_stock_status_covers_shortages(): void
  15. {
  16. // Arrange
  17. $sparePart = SparePart::factory()->create();
  18. $reclamation = Reclamation::factory()->create();
  19. // Create open shortage
  20. $shortage = Shortage::factory()
  21. ->open()
  22. ->withQuantities(5, 0) // required=5, reserved=0, missing=5
  23. ->forSparePart($sparePart)
  24. ->forReclamation($reclamation)
  25. ->withDocuments(false)
  26. ->create();
  27. // Act - create order with in_stock status (observer should trigger)
  28. $order = SparePartOrder::factory()
  29. ->inStock()
  30. ->withDocuments(false)
  31. ->withQuantity(10)
  32. ->forSparePart($sparePart)
  33. ->create();
  34. // Assert
  35. $shortage->refresh();
  36. // Shortage should be covered (status=closed when fully covered)
  37. $this->assertEquals(5, $shortage->reserved_qty);
  38. $this->assertEquals(0, $shortage->missing_qty);
  39. $this->assertEquals(Shortage::STATUS_CLOSED, $shortage->status);
  40. // Reservation should be created
  41. $this->assertDatabaseHas('reservations', [
  42. 'spare_part_id' => $sparePart->id,
  43. 'spare_part_order_id' => $order->id,
  44. 'reclamation_id' => $reclamation->id,
  45. 'reserved_qty' => 5,
  46. 'status' => Reservation::STATUS_ACTIVE,
  47. ]);
  48. }
  49. public function test_creating_order_with_ordered_status_does_not_trigger_shortage_coverage(): void
  50. {
  51. // Arrange
  52. $sparePart = SparePart::factory()->create();
  53. $reclamation = Reclamation::factory()->create();
  54. Shortage::factory()
  55. ->open()
  56. ->withQuantities(5, 0)
  57. ->forSparePart($sparePart)
  58. ->forReclamation($reclamation)
  59. ->withDocuments(false)
  60. ->create();
  61. // Act - create order with ordered status
  62. SparePartOrder::factory()
  63. ->ordered()
  64. ->withDocuments(false)
  65. ->withQuantity(10)
  66. ->forSparePart($sparePart)
  67. ->create();
  68. // Assert - shortage should remain open
  69. $this->assertDatabaseHas('shortages', [
  70. 'spare_part_id' => $sparePart->id,
  71. 'reclamation_id' => $reclamation->id,
  72. 'status' => Shortage::STATUS_OPEN,
  73. 'missing_qty' => 5,
  74. ]);
  75. }
  76. public function test_updating_order_status_to_in_stock_covers_shortages(): void
  77. {
  78. // Arrange
  79. $sparePart = SparePart::factory()->create();
  80. $reclamation = Reclamation::factory()->create();
  81. Shortage::factory()
  82. ->open()
  83. ->withQuantities(3, 0)
  84. ->forSparePart($sparePart)
  85. ->forReclamation($reclamation)
  86. ->withDocuments(true)
  87. ->create();
  88. // Create order with ordered status (not in_stock)
  89. $order = SparePartOrder::factory()
  90. ->ordered()
  91. ->withDocuments(true)
  92. ->withQuantity(10)
  93. ->forSparePart($sparePart)
  94. ->create();
  95. // Act - update status to in_stock
  96. $order->update(['status' => SparePartOrder::STATUS_IN_STOCK]);
  97. // Assert
  98. $this->assertDatabaseHas('shortages', [
  99. 'spare_part_id' => $sparePart->id,
  100. 'reclamation_id' => $reclamation->id,
  101. 'status' => Shortage::STATUS_CLOSED,
  102. 'reserved_qty' => 3,
  103. 'missing_qty' => 0,
  104. ]);
  105. }
  106. public function test_updating_other_fields_does_not_trigger_shortage_coverage(): void
  107. {
  108. // Arrange
  109. $sparePart = SparePart::factory()->create();
  110. $reclamation = Reclamation::factory()->create();
  111. Shortage::factory()
  112. ->open()
  113. ->withQuantities(5, 0)
  114. ->forSparePart($sparePart)
  115. ->forReclamation($reclamation)
  116. ->withDocuments(false)
  117. ->create();
  118. $order = SparePartOrder::factory()
  119. ->inStock()
  120. ->withDocuments(false)
  121. ->withQuantity(10)
  122. ->forSparePart($sparePart)
  123. ->create();
  124. // Clear the shortage that was covered on creation
  125. Shortage::query()->update([
  126. 'status' => Shortage::STATUS_OPEN,
  127. 'reserved_qty' => 0,
  128. 'missing_qty' => 5,
  129. ]);
  130. Reservation::query()->delete();
  131. // Act - update note (not status)
  132. $order->update(['note' => 'Updated note']);
  133. // Assert - shortage should remain open (status didn't change)
  134. $this->assertDatabaseHas('shortages', [
  135. 'spare_part_id' => $sparePart->id,
  136. 'status' => Shortage::STATUS_OPEN,
  137. 'missing_qty' => 5,
  138. ]);
  139. }
  140. public function test_observer_respects_with_documents_flag(): void
  141. {
  142. // Arrange
  143. $sparePart = SparePart::factory()->create();
  144. $reclamation = Reclamation::factory()->create();
  145. // Create shortage requiring documents
  146. Shortage::factory()
  147. ->open()
  148. ->withQuantities(5, 0)
  149. ->forSparePart($sparePart)
  150. ->forReclamation($reclamation)
  151. ->withDocuments(true)
  152. ->create();
  153. // Act - create order WITHOUT documents
  154. SparePartOrder::factory()
  155. ->inStock()
  156. ->withDocuments(false) // Different from shortage
  157. ->withQuantity(10)
  158. ->forSparePart($sparePart)
  159. ->create();
  160. // Assert - shortage should remain open (documents mismatch)
  161. $this->assertDatabaseHas('shortages', [
  162. 'spare_part_id' => $sparePart->id,
  163. 'with_documents' => true,
  164. 'status' => Shortage::STATUS_OPEN,
  165. 'missing_qty' => 5,
  166. ]);
  167. }
  168. public function test_observer_covers_multiple_shortages_fifo(): void
  169. {
  170. // Arrange
  171. $sparePart = SparePart::factory()->create();
  172. $reclamation1 = Reclamation::factory()->create();
  173. $reclamation2 = Reclamation::factory()->create();
  174. // Create older shortage
  175. $shortage1 = Shortage::factory()
  176. ->open()
  177. ->withQuantities(3, 0)
  178. ->forSparePart($sparePart)
  179. ->forReclamation($reclamation1)
  180. ->withDocuments(false)
  181. ->create(['created_at' => now()->subDays(2)]);
  182. // Create newer shortage
  183. $shortage2 = Shortage::factory()
  184. ->open()
  185. ->withQuantities(4, 0)
  186. ->forSparePart($sparePart)
  187. ->forReclamation($reclamation2)
  188. ->withDocuments(false)
  189. ->create(['created_at' => now()->subDay()]);
  190. // Act - create order with 5 items (should cover first fully, second partially)
  191. SparePartOrder::factory()
  192. ->inStock()
  193. ->withDocuments(false)
  194. ->withQuantity(5)
  195. ->forSparePart($sparePart)
  196. ->create();
  197. // Assert
  198. $shortage1->refresh();
  199. $shortage2->refresh();
  200. // First shortage fully covered (closed)
  201. $this->assertEquals(Shortage::STATUS_CLOSED, $shortage1->status);
  202. $this->assertEquals(3, $shortage1->reserved_qty);
  203. // Second shortage partially covered
  204. $this->assertEquals(Shortage::STATUS_OPEN, $shortage2->status);
  205. $this->assertEquals(2, $shortage2->reserved_qty);
  206. $this->assertEquals(2, $shortage2->missing_qty);
  207. }
  208. public function test_observer_does_not_cover_already_covered_shortages(): void
  209. {
  210. // Arrange
  211. $sparePart = SparePart::factory()->create();
  212. $reclamation = Reclamation::factory()->create();
  213. // Create already covered shortage
  214. Shortage::factory()
  215. ->covered()
  216. ->withQuantities(5, 5) // fully covered
  217. ->forSparePart($sparePart)
  218. ->forReclamation($reclamation)
  219. ->withDocuments(false)
  220. ->create();
  221. // Act
  222. $order = SparePartOrder::factory()
  223. ->inStock()
  224. ->withDocuments(false)
  225. ->withQuantity(10)
  226. ->forSparePart($sparePart)
  227. ->create();
  228. // Assert - no new reservations for already covered shortage
  229. $this->assertEquals(0, Reservation::where('spare_part_order_id', $order->id)->count());
  230. }
  231. }