mivita/tests/Unit/Services/InvoiceServiceTest.php
2026-02-20 17:55:06 +01:00

290 lines
7.1 KiB
PHP

<?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);
}
}