290 lines
7.1 KiB
PHP
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);
|
|
}
|
|
}
|