@@ -111,3 +111,111 @@ public function up()
111111 ->and ($ secondRecord ->number )->toBe (2 )
112112 ->and (config ('running-number.model ' )::where ('type ' , 'SECTION ' )->count ())->toBe (1 );
113113});
114+
115+ // Race Condition & Rollback Tests
116+ it ('prevents duplicate numbers when generating concurrently ' , function () {
117+ // Generate multiple numbers in sequence
118+ $ numbers = [];
119+ for ($ i = 0 ; $ i < 5 ; $ i ++) {
120+ $ numbers [] = RunningNumberGenerator::make ()
121+ ->type (Organization::ORGANIZATION ->value )
122+ ->generate ();
123+ }
124+
125+ // All numbers should be unique
126+ expect (count ($ numbers ))->toBe (5 )
127+ ->and (count (array_unique ($ numbers )))->toBe (5 )
128+ ->and ($ numbers )->toMatchArray ([
129+ 'ORGANIZATION001 ' ,
130+ 'ORGANIZATION002 ' ,
131+ 'ORGANIZATION003 ' ,
132+ 'ORGANIZATION004 ' ,
133+ 'ORGANIZATION005 ' ,
134+ ]);
135+ });
136+
137+ it ('handles type creation race condition gracefully ' , function () {
138+ // First creation should succeed
139+ RunningNumberGenerator::make ()->type (Organization::DIVISION ->value )->generate ();
140+
141+ // Verify only one record was created
142+ $ count = config ('running-number.model ' )::where ('type ' , 'DIVISION ' )->count ();
143+
144+ expect ($ count )->toBe (1 );
145+ });
146+
147+ it ('maintains sequential integrity after multiple operations ' , function () {
148+ $ type = Organization::UNIT ->value ;
149+
150+ // Generate 10 numbers
151+ for ($ i = 1 ; $ i <= 10 ; $ i ++) {
152+ RunningNumberGenerator::make ()->type ($ type )->generate ();
153+ }
154+
155+ // Verify the final number is 10
156+ $ record = config ('running-number.model ' )::where ('type ' , 'UNIT ' )->first ();
157+
158+ expect ($ record ->number )->toBe (10 );
159+
160+ // Generate 5 more
161+ for ($ i = 1 ; $ i <= 5 ; $ i ++) {
162+ RunningNumberGenerator::make ()->type ($ type )->generate ();
163+ }
164+
165+ // Verify the final number is now 15
166+ $ record ->refresh ();
167+ expect ($ record ->number )->toBe (15 );
168+ });
169+
170+ it ('uses database transactions for generation ' , function () {
171+ // This test verifies that transactions are being used
172+ // by checking that the number is properly incremented
173+ $ type = Organization::SECTION ->value ;
174+
175+ // Generate first number
176+ $ first = RunningNumberGenerator::make ()->type ($ type )->generate ();
177+ expect ($ first )->toBe ('SECTION001 ' );
178+
179+ // Generate second number - should be sequential
180+ $ second = RunningNumberGenerator::make ()->type ($ type )->generate ();
181+ expect ($ second )->toBe ('SECTION002 ' );
182+
183+ // Verify database state
184+ $ record = config ('running-number.model ' )::where ('type ' , 'SECTION ' )->first ();
185+ expect ($ record ->number )->toBe (2 );
186+ });
187+
188+ it ('does not lose numbers on successful generation ' , function () {
189+ $ type = Organization::PROFILE ->value ;
190+ $ generated = [];
191+
192+ // Generate 20 numbers
193+ for ($ i = 1 ; $ i <= 20 ; $ i ++) {
194+ $ generated [] = RunningNumberGenerator::make ()->type ($ type )->generate ();
195+ }
196+
197+ // Extract the numeric part from each generated number
198+ $ numbers = array_map (function ($ num ) {
199+ return (int ) preg_replace ('/[^0-9]/ ' , '' , $ num );
200+ }, $ generated );
201+
202+ // Verify no numbers are skipped (sequential from 1 to 20)
203+ expect ($ numbers )->toEqual (range (1 , 20 ));
204+ });
205+
206+ it ('ensures atomic operations with firstOrCreate ' , function () {
207+ // Create a new type
208+ RunningNumberGenerator::make ()->type (Organization::DIVISION ->value )->generate ();
209+
210+ // Try to generate again - should not create duplicate type record
211+ RunningNumberGenerator::make ()->type (Organization::DIVISION ->value )->generate ();
212+ RunningNumberGenerator::make ()->type (Organization::DIVISION ->value )->generate ();
213+
214+ // Should only have ONE record for this type
215+ $ count = config ('running-number.model ' )::where ('type ' , 'DIVISION ' )->count ();
216+ expect ($ count )->toBe (1 );
217+
218+ // And the number should be 3
219+ $ record = config ('running-number.model ' )::where ('type ' , 'DIVISION ' )->first ();
220+ expect ($ record ->number )->toBe (3 );
221+ });
0 commit comments