20-02-2026
This commit is contained in:
parent
a8b395e20d
commit
a00c42e770
252 changed files with 28785 additions and 8907 deletions
290
tests/Unit/Services/InvoiceServiceTest.php
Normal file
290
tests/Unit/Services/InvoiceServiceTest.php
Normal file
|
|
@ -0,0 +1,290 @@
|
|||
<?php
|
||||
|
||||
namespace Tests\Unit\Services;
|
||||
|
||||
use App\Models\Setting;
|
||||
use App\Services\Invoice;
|
||||
use Illuminate\Foundation\Testing\DatabaseTransactions;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Tests\TestCase;
|
||||
|
||||
/**
|
||||
* Unit tests for Invoice Service
|
||||
*/
|
||||
class InvoiceServiceTest extends TestCase
|
||||
{
|
||||
use DatabaseTransactions;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
// Ensure clean state
|
||||
Setting::where('slug', 'invoice-number')->delete();
|
||||
}
|
||||
|
||||
/**
|
||||
* Test invoice number retrieval
|
||||
*
|
||||
* @test
|
||||
*/
|
||||
public function it_gets_current_invoice_number()
|
||||
{
|
||||
Setting::create([
|
||||
'slug' => 'invoice-number',
|
||||
'type' => 'int',
|
||||
'int' => 12345,
|
||||
]);
|
||||
|
||||
$number = Invoice::getInvoiceNumber();
|
||||
|
||||
$this->assertEquals(12345, $number);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test invoice number increment
|
||||
*
|
||||
* @test
|
||||
*/
|
||||
public function it_increments_invoice_number()
|
||||
{
|
||||
Setting::create([
|
||||
'slug' => 'invoice-number',
|
||||
'type' => 'int',
|
||||
'int' => 100,
|
||||
]);
|
||||
|
||||
$newNumber = Invoice::makeNextInvoiceNumber();
|
||||
|
||||
$this->assertEquals(101, $newNumber);
|
||||
|
||||
// Verify it was persisted
|
||||
$storedNumber = Invoice::getInvoiceNumber();
|
||||
$this->assertEquals(101, $storedNumber);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test multiple sequential increments
|
||||
*
|
||||
* @test
|
||||
*/
|
||||
public function it_increments_sequentially()
|
||||
{
|
||||
Setting::create([
|
||||
'slug' => 'invoice-number',
|
||||
'type' => 'int',
|
||||
'int' => 1,
|
||||
]);
|
||||
|
||||
$numbers = [];
|
||||
for ($i = 0; $i < 5; $i++) {
|
||||
$numbers[] = Invoice::makeNextInvoiceNumber();
|
||||
}
|
||||
|
||||
$this->assertEquals([2, 3, 4, 5, 6], $numbers);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test invoice number format with year prefix
|
||||
*
|
||||
* @test
|
||||
*/
|
||||
public function it_formats_invoice_number_with_year_prefix()
|
||||
{
|
||||
$formatted = Invoice::createInvoiceNumber(123, '15.06.2024');
|
||||
$this->assertEquals('202400123', $formatted);
|
||||
|
||||
$formatted = Invoice::createInvoiceNumber(1, '01.01.2025');
|
||||
$this->assertEquals('202500001', $formatted);
|
||||
|
||||
$formatted = Invoice::createInvoiceNumber(99999, '31.12.2026');
|
||||
$this->assertEquals('202699999', $formatted);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test invoice storage directory path
|
||||
*
|
||||
* @test
|
||||
*/
|
||||
public function it_generates_correct_storage_paths()
|
||||
{
|
||||
$path = Invoice::getInvoiceStorageDir('15.06.2024');
|
||||
$this->assertEquals('invoice/2024/06/', $path);
|
||||
|
||||
$path = Invoice::getDeliveryStorageDir('01.01.2025');
|
||||
$this->assertEquals('delivery/2025/01/', $path);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test invoice filename generation
|
||||
*
|
||||
* @test
|
||||
*/
|
||||
public function it_generates_correct_filenames()
|
||||
{
|
||||
$filename = Invoice::makeInvoiceFilename('202400123');
|
||||
$this->assertEquals('202400123-MIVITA-Rechnung.pdf', $filename);
|
||||
|
||||
$filename = Invoice::makeDeliveryFilename('202400123');
|
||||
$this->assertEquals('202400123-MIVITA-Lieferschein.pdf', $filename);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test invoice number initialization when not exists
|
||||
*
|
||||
* @test
|
||||
*/
|
||||
public function it_initializes_invoice_number_when_not_exists()
|
||||
{
|
||||
// Make sure it doesn't exist
|
||||
Setting::where('slug', 'invoice-number')->delete();
|
||||
|
||||
$number = Invoice::makeNextInvoiceNumber();
|
||||
|
||||
$this->assertEquals(1, $number);
|
||||
|
||||
// Verify setting was created
|
||||
$setting = Setting::where('slug', 'invoice-number')->first();
|
||||
$this->assertNotNull($setting);
|
||||
$this->assertEquals('int', $setting->type);
|
||||
$this->assertEquals(1, $setting->int);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test invoice number atomicity with explicit transaction
|
||||
*
|
||||
* @test
|
||||
*/
|
||||
public function it_uses_transaction_for_invoice_number_increment()
|
||||
{
|
||||
Setting::create([
|
||||
'slug' => 'invoice-number',
|
||||
'type' => 'int',
|
||||
'int' => 500,
|
||||
]);
|
||||
|
||||
// makeNextInvoiceNumber includes its own transaction
|
||||
$number = Invoice::makeNextInvoiceNumber();
|
||||
|
||||
$this->assertEquals(501, $number);
|
||||
|
||||
// Can be called within another transaction
|
||||
DB::transaction(function () {
|
||||
$number = Invoice::makeNextInvoiceNumber();
|
||||
$this->assertEquals(502, $number);
|
||||
});
|
||||
|
||||
$finalNumber = Invoice::getInvoiceNumber();
|
||||
$this->assertEquals(502, $finalNumber);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test invoice number with database lock
|
||||
*
|
||||
* @test
|
||||
*/
|
||||
public function it_locks_setting_during_increment()
|
||||
{
|
||||
Setting::create([
|
||||
'slug' => 'invoice-number',
|
||||
'type' => 'int',
|
||||
'int' => 1000,
|
||||
]);
|
||||
|
||||
// Verify the lock mechanism works
|
||||
DB::transaction(function () {
|
||||
$setting = Setting::where('slug', 'invoice-number')
|
||||
->lockForUpdate()
|
||||
->first();
|
||||
|
||||
$this->assertNotNull($setting);
|
||||
$this->assertEquals(1000, $setting->int);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Test invoice number padding
|
||||
*
|
||||
* @test
|
||||
*/
|
||||
public function it_pads_invoice_numbers_correctly()
|
||||
{
|
||||
$tests = [
|
||||
[1, '202400001'],
|
||||
[12, '202400012'],
|
||||
[123, '202400123'],
|
||||
[1234, '202401234'],
|
||||
[12345, '202412345'],
|
||||
];
|
||||
|
||||
foreach ($tests as [$number, $expected]) {
|
||||
$formatted = Invoice::createInvoiceNumber($number, '01.01.2024');
|
||||
$this->assertEquals($expected, $formatted);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test getInvoiceNumber returns 0 when setting doesn't exist
|
||||
*
|
||||
* @test
|
||||
*/
|
||||
public function it_returns_zero_when_invoice_number_not_set()
|
||||
{
|
||||
Setting::where('slug', 'invoice-number')->delete();
|
||||
|
||||
$number = Invoice::getInvoiceNumber();
|
||||
|
||||
$this->assertEquals(0, $number);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test concurrent increment simulation
|
||||
*
|
||||
* @test
|
||||
*/
|
||||
public function it_handles_rapid_increments_without_gaps()
|
||||
{
|
||||
Setting::create([
|
||||
'slug' => 'invoice-number',
|
||||
'type' => 'int',
|
||||
'int' => 7000,
|
||||
]);
|
||||
|
||||
$numbers = [];
|
||||
$iterations = 15;
|
||||
|
||||
for ($i = 0; $i < $iterations; $i++) {
|
||||
$numbers[] = Invoice::makeNextInvoiceNumber();
|
||||
}
|
||||
|
||||
// Check for uniqueness
|
||||
$unique = array_unique($numbers);
|
||||
$this->assertCount($iterations, $unique, 'All numbers must be unique');
|
||||
|
||||
// Check for no gaps
|
||||
sort($numbers);
|
||||
for ($i = 0; $i < $iterations; $i++) {
|
||||
$expected = 7001 + $i;
|
||||
$this->assertEquals($expected, $numbers[$i], 'No gaps allowed in sequence');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test invoice number type casting
|
||||
*
|
||||
* @test
|
||||
*/
|
||||
public function it_returns_integer_invoice_number()
|
||||
{
|
||||
Setting::create([
|
||||
'slug' => 'invoice-number',
|
||||
'type' => 'int',
|
||||
'int' => 999,
|
||||
]);
|
||||
|
||||
$number = Invoice::getInvoiceNumber();
|
||||
|
||||
$this->assertIsInt($number);
|
||||
$this->assertEquals(999, $number);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue