Skip to content

Commit b78918d

Browse files
committed
Add reset functionality to running numbers with configurable periods
- Introduced 'reset_period' and 'last_reset_at' fields in the RunningNumber model. - Implemented logic to reset running numbers based on specified periods (never, daily, monthly, yearly). - Updated Generator to handle reset conditions during number generation. - Added tests to validate reset functionality and behavior under different reset periods. - Created migration for adding reset functionality to the running_numbers table.
1 parent 987d05f commit b78918d

File tree

7 files changed

+331
-2
lines changed

7 files changed

+331
-2
lines changed

config/running-number.php

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
<?php
22

33
use CleaniqueCoders\RunningNumber\Enums\Organization;
4+
use CleaniqueCoders\RunningNumber\Enums\ResetPeriod;
45

56
return [
67
/*
@@ -56,4 +57,32 @@
5657
*/
5758

5859
'padding' => 3,
60+
61+
/*
62+
|--------------------------------------------------------------------------
63+
| Reset Period Configuration
64+
|--------------------------------------------------------------------------
65+
|
66+
| Configure when running numbers should automatically reset. You can set
67+
| a global default reset period, or configure specific periods per type.
68+
|
69+
| Available periods: 'never', 'daily', 'monthly', 'yearly'
70+
| - never: Running numbers never reset (default)
71+
| - daily: Reset at midnight every day
72+
| - monthly: Reset on the 1st of each month
73+
| - yearly: Reset on January 1st each year
74+
|
75+
*/
76+
77+
'reset_period' => [
78+
'default' => ResetPeriod::NEVER->value,
79+
80+
// Per-type reset periods (optional)
81+
// Uncomment and configure specific types as needed
82+
// 'types' => [
83+
// 'invoice' => ResetPeriod::YEARLY->value,
84+
// 'receipt' => ResetPeriod::MONTHLY->value,
85+
// 'ticket' => ResetPeriod::DAILY->value,
86+
// ],
87+
],
5988
];
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
<?php
2+
3+
use Illuminate\Database\Migrations\Migration;
4+
use Illuminate\Database\Schema\Blueprint;
5+
use Illuminate\Support\Facades\Schema;
6+
7+
return new class extends Migration
8+
{
9+
public function up()
10+
{
11+
Schema::table('running_numbers', function (Blueprint $table) {
12+
$table->string('reset_period')->default('never')->after('type');
13+
$table->timestamp('last_reset_at')->nullable()->after('reset_period');
14+
});
15+
}
16+
17+
public function down()
18+
{
19+
Schema::table('running_numbers', function (Blueprint $table) {
20+
$table->dropColumn(['reset_period', 'last_reset_at']);
21+
});
22+
}
23+
};

src/Enums/ResetPeriod.php

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
<?php
2+
3+
namespace CleaniqueCoders\RunningNumber\Enums;
4+
5+
use CleaniqueCoders\Traitify\Concerns\InteractsWithEnum;
6+
7+
enum ResetPeriod: string
8+
{
9+
use InteractsWithEnum;
10+
11+
case NEVER = 'never';
12+
case DAILY = 'daily';
13+
case MONTHLY = 'monthly';
14+
case YEARLY = 'yearly';
15+
16+
public function label(): string
17+
{
18+
return match ($this) {
19+
self::NEVER => 'Never Reset',
20+
self::DAILY => 'Daily Reset',
21+
self::MONTHLY => 'Monthly Reset',
22+
self::YEARLY => 'Yearly Reset',
23+
};
24+
}
25+
26+
public function description(): string
27+
{
28+
return match ($this) {
29+
self::NEVER => 'Running number never resets',
30+
self::DAILY => 'Running number resets every day at midnight',
31+
self::MONTHLY => 'Running number resets on the first day of each month',
32+
self::YEARLY => 'Running number resets on January 1st each year',
33+
};
34+
}
35+
36+
/**
37+
* Check if reset is needed based on the period and last reset date
38+
*/
39+
public function needsReset(?\DateTime $lastResetAt): bool
40+
{
41+
if ($lastResetAt === null) {
42+
return false;
43+
}
44+
45+
$now = new \DateTime();
46+
47+
return match ($this) {
48+
self::NEVER => false,
49+
self::DAILY => $lastResetAt->format('Y-m-d') !== $now->format('Y-m-d'),
50+
self::MONTHLY => $lastResetAt->format('Y-m') !== $now->format('Y-m'),
51+
self::YEARLY => $lastResetAt->format('Y') !== $now->format('Y'),
52+
};
53+
}
54+
}

src/Generator.php

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,11 @@ public function generate(): string
6363
->lockForUpdate()
6464
->first();
6565

66+
// Check if reset is needed based on reset period
67+
if ($running_number->needsReset()) {
68+
$running_number->reset();
69+
}
70+
6671
// Increment and save atomically within the transaction
6772
$running_number->increment('number');
6873
$running_number->refresh();
@@ -80,9 +85,29 @@ private function createRunningNumberTypeIfNotExists()
8085
{
8186
// Use firstOrCreate() which is atomic and prevents race conditions
8287
// Multiple concurrent requests will not create duplicate types
88+
$resetPeriod = $this->getResetPeriod();
89+
8390
config('running-number.model')::firstOrCreate(
8491
['type' => $this->getType()],
85-
['number' => 0]
92+
[
93+
'number' => 0,
94+
'reset_period' => $resetPeriod,
95+
'last_reset_at' => now(),
96+
]
8697
);
8798
}
99+
100+
private function getResetPeriod(): string
101+
{
102+
// Check if there's a specific reset period for this type
103+
$typeResetPeriods = config('running-number.reset_period.types', []);
104+
$type = strtolower($this->type);
105+
106+
if (isset($typeResetPeriods[$type])) {
107+
return $typeResetPeriods[$type];
108+
}
109+
110+
// Fall back to default reset period
111+
return config('running-number.reset_period.default', 'never');
112+
}
88113
}

src/Models/RunningNumber.php

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,44 @@
22

33
namespace CleaniqueCoders\RunningNumber\Models;
44

5+
use CleaniqueCoders\RunningNumber\Enums\ResetPeriod;
56
use CleaniqueCoders\Traitify\Concerns\InteractsWithUuid;
67
use Illuminate\Database\Eloquent\Model;
78

9+
/**
10+
* @property int $number
11+
* @property string $type
12+
* @property ResetPeriod $reset_period
13+
* @property \Illuminate\Support\Carbon|null $last_reset_at
14+
* @property \Illuminate\Support\Carbon $created_at
15+
* @property \Illuminate\Support\Carbon $updated_at
16+
*/
817
class RunningNumber extends Model
918
{
1019
use InteractsWithUuid;
1120

1221
protected $guarded = [];
22+
23+
protected $casts = [
24+
'reset_period' => ResetPeriod::class,
25+
'last_reset_at' => 'datetime',
26+
];
27+
28+
/**
29+
* Check if this running number needs to be reset
30+
*/
31+
public function needsReset(): bool
32+
{
33+
return $this->reset_period->needsReset($this->last_reset_at);
34+
}
35+
36+
/**
37+
* Reset the running number to zero
38+
*/
39+
public function reset(): void
40+
{
41+
$this->number = 0;
42+
$this->last_reset_at = now();
43+
$this->save();
44+
}
1345
}

src/RunningNumberServiceProvider.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ public function configurePackage(Package $package): void
1818
->name('running-number')
1919
->hasConfigFile('running-number')
2020
->hasMigration('create_running_number_table')
21-
->hasMigration('add_uuid_to_running_numbers_table');
21+
->hasMigration('add_uuid_to_running_numbers_table')
22+
->hasMigration('add_reset_functionality_to_running_numbers_table');
2223
}
2324
}

tests/RunningNumberTest.php

Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
beforeEach(function () {
1111
include_once __DIR__.'/../database/migrations/create_running_number_table.php.stub';
1212
include_once __DIR__.'/../database/migrations/add_uuid_to_running_numbers_table.php.stub';
13+
include_once __DIR__.'/../database/migrations/add_reset_functionality_to_running_numbers_table.php.stub';
1314

1415
(new \CreateRunningNumberTable)->up();
1516

@@ -24,6 +25,25 @@ public function up()
2425
}
2526
};
2627
$uuidMigration->up();
28+
29+
// Run the reset functionality migration
30+
$resetMigration = new class extends \Illuminate\Database\Migrations\Migration
31+
{
32+
public function up()
33+
{
34+
Schema::table('running_numbers', function (\Illuminate\Database\Schema\Blueprint $table) {
35+
$table->string('reset_period')->default('never')->after('type');
36+
$table->timestamp('last_reset_at')->nullable()->after('reset_period');
37+
});
38+
}
39+
};
40+
$resetMigration->up();
41+
42+
// Add test-specific types to config
43+
config(['running-number.types' => array_merge(
44+
config('running-number.types'),
45+
['invoice', 'receipt', 'ticket', 'order', 'monthly']
46+
)]);
2747
});
2848

2949
it('can has running number helper', function () {
@@ -219,3 +239,148 @@ public function up()
219239
$record = config('running-number.model')::where('type', 'DIVISION')->first();
220240
expect($record->number)->toBe(3);
221241
});
242+
243+
// Reset Functionality Tests
244+
it('creates running number with default reset period', function () {
245+
RunningNumberGenerator::make()->type(Organization::UNIT->value)->generate();
246+
247+
$record = config('running-number.model')::where('type', 'UNIT')->first();
248+
249+
expect($record->reset_period->value)->toBe('never')
250+
->and($record->last_reset_at)->not->toBeNull();
251+
});
252+
253+
it('can manually reset a running number', function () {
254+
// Generate some numbers
255+
for ($i = 0; $i < 5; $i++) {
256+
RunningNumberGenerator::make()->type(Organization::SECTION->value)->generate();
257+
}
258+
259+
$record = config('running-number.model')::where('type', 'SECTION')->first();
260+
expect($record->number)->toBe(5);
261+
262+
// Manually reset
263+
$record->reset();
264+
265+
expect($record->number)->toBe(0)
266+
->and($record->last_reset_at)->not->toBeNull();
267+
268+
// Next generation should be 1
269+
$next = RunningNumberGenerator::make()->type(Organization::SECTION->value)->generate();
270+
expect($next)->toBe('SECTION001');
271+
});
272+
273+
it('does not reset when reset_period is never', function () {
274+
// Generate numbers over time
275+
RunningNumberGenerator::make()->type(Organization::ORGANIZATION->value)->generate();
276+
RunningNumberGenerator::make()->type(Organization::ORGANIZATION->value)->generate();
277+
278+
$record = config('running-number.model')::where('type', 'ORGANIZATION')->first();
279+
280+
// Simulate time passing
281+
$record->update(['last_reset_at' => now()->subYear()]);
282+
283+
// Should not reset since period is 'never'
284+
RunningNumberGenerator::make()->type(Organization::ORGANIZATION->value)->generate();
285+
286+
$record->refresh();
287+
expect($record->number)->toBe(3); // Should continue from 2 to 3
288+
});
289+
290+
it('resets correctly with yearly period', function () {
291+
$model = config('running-number.model');
292+
293+
// Create a record with yearly reset period
294+
$record = $model::create([
295+
'type' => 'INVOICE',
296+
'number' => 100,
297+
'reset_period' => 'yearly',
298+
'last_reset_at' => now()->subYear(), // Last year
299+
]);
300+
301+
expect($record->needsReset())->toBeTrue();
302+
303+
// Generate - should reset to 1
304+
$number = RunningNumberGenerator::make()->type('invoice')->generate();
305+
306+
expect($number)->toBe('INVOICE001');
307+
});
308+
309+
it('resets correctly with monthly period', function () {
310+
$model = config('running-number.model');
311+
312+
// Create a record with monthly reset period
313+
$record = $model::create([
314+
'type' => 'RECEIPT',
315+
'number' => 50,
316+
'reset_period' => 'monthly',
317+
'last_reset_at' => now()->subMonth(), // Last month
318+
]);
319+
320+
expect($record->needsReset())->toBeTrue();
321+
322+
// Generate - should reset to 1
323+
$number = RunningNumberGenerator::make()->type('receipt')->generate();
324+
325+
expect($number)->toBe('RECEIPT001');
326+
});
327+
328+
it('resets correctly with daily period', function () {
329+
$model = config('running-number.model');
330+
331+
// Create a record with daily reset period
332+
$record = $model::create([
333+
'type' => 'TICKET',
334+
'number' => 25,
335+
'reset_period' => 'daily',
336+
'last_reset_at' => now()->subDay(), // Yesterday
337+
]);
338+
339+
expect($record->needsReset())->toBeTrue();
340+
341+
// Generate - should reset to 1
342+
$number = RunningNumberGenerator::make()->type('ticket')->generate();
343+
344+
expect($number)->toBe('TICKET001');
345+
});
346+
347+
it('does not reset if same period has not passed', function () {
348+
$model = config('running-number.model');
349+
350+
// Create a record with daily reset, but last reset was today
351+
$record = $model::create([
352+
'type' => 'ORDER',
353+
'number' => 10,
354+
'reset_period' => 'daily',
355+
'last_reset_at' => now(), // Today
356+
]);
357+
358+
expect($record->needsReset())->toBeFalse();
359+
360+
// Generate - should continue from 10
361+
$number = RunningNumberGenerator::make()->type('order')->generate();
362+
363+
expect($number)->toBe('ORDER011');
364+
});
365+
366+
it('updates last_reset_at when reset occurs', function () {
367+
$model = config('running-number.model');
368+
369+
// Create record with monthly reset from last month
370+
$record = $model::create([
371+
'type' => 'MONTHLY',
372+
'number' => 99,
373+
'reset_period' => 'monthly',
374+
'last_reset_at' => now()->subMonth(),
375+
]);
376+
377+
$originalResetDate = $record->last_reset_at;
378+
379+
// Generate - should trigger reset
380+
RunningNumberGenerator::make()->type('monthly')->generate();
381+
382+
$record->refresh();
383+
384+
expect($record->number)->toBe(1)
385+
->and($record->last_reset_at->isAfter($originalResetDate))->toBeTrue();
386+
});

0 commit comments

Comments
 (0)