20-02-2026

This commit is contained in:
Kevin Adametz 2026-02-20 17:55:06 +01:00
parent a8b395e20d
commit a00c42e770
252 changed files with 28785 additions and 8907 deletions

View file

@ -0,0 +1,84 @@
<?php
namespace Tests\Unit;
use App\Models\DashboardNews;
use Carbon\Carbon;
use Tests\TestCase;
class DashboardNewsTest extends TestCase
{
/** @test */
public function it_returns_german_title_when_locale_is_de(): void
{
$news = new DashboardNews();
$news->title = 'Deutsche Überschrift';
$news->trans_title = ['en' => 'English Title'];
app()->setLocale('de');
$this->assertEquals('Deutsche Überschrift', $news->getLang('title'));
}
/** @test */
public function it_returns_translated_title_when_locale_is_set(): void
{
$news = new DashboardNews();
$news->title = 'Deutsche Überschrift';
$news->trans_title = ['en' => 'English Title'];
app()->setLocale('en');
$this->assertEquals('English Title', $news->getLang('title'));
}
/** @test */
public function it_falls_back_to_german_when_translation_is_missing(): void
{
$news = new DashboardNews();
$news->title = 'Deutsche Überschrift';
$news->trans_title = ['en' => ''];
app()->setLocale('en');
$this->assertEquals('Deutsche Überschrift', $news->getLang('title'));
}
/** @test */
public function it_formats_display_date_correctly(): void
{
$news = new DashboardNews();
$news->display_date = Carbon::create(2026, 2, 19);
$this->assertEquals('19.02.2026', $news->getDisplayDateFormatted());
}
/** @test */
public function it_falls_back_to_created_at_when_display_date_is_null(): void
{
$news = new DashboardNews();
$news->display_date = null;
$news->created_at = Carbon::create(2025, 12, 1);
$this->assertEquals('01.12.2025', $news->getDisplayDateFormatted());
}
/** @test */
public function it_returns_empty_string_when_no_dates_are_set(): void
{
$news = new DashboardNews();
$news->display_date = null;
$news->created_at = null;
$this->assertEquals('', $news->getDisplayDateFormatted());
}
/** @test */
public function it_detects_no_file_links_when_empty(): void
{
$news = new DashboardNews();
$news->file_links = null;
$this->assertFalse($news->hasFileLinks());
}
}

View file

@ -0,0 +1,670 @@
<?php
namespace Tests\Unit\Services;
use App\Models\DatevExport;
use App\Models\DatevExportLine;
use App\Services\DatevExportService;
use Tests\TestCase;
class DatevExportServiceTest extends TestCase
{
private DatevExportService $service;
protected function setUp(): void
{
parent::setUp();
$this->service = new DatevExportService;
}
/*
|--------------------------------------------------------------------------
| Revenue Account Mapping Tests
|--------------------------------------------------------------------------
*/
/** @test */
public function it_maps_domestic_19_percent_to_account_8400_bu_9()
{
$method = new \ReflectionMethod(DatevExportService::class, 'determineRevenueAccount');
$method->setAccessible(true);
$result = $method->invoke($this->service, 19, true, false, false);
$this->assertEquals(8400, $result['konto']);
$this->assertEquals(9, $result['bu']);
}
/** @test */
public function it_maps_domestic_7_percent_to_account_8300_bu_8()
{
$method = new \ReflectionMethod(DatevExportService::class, 'determineRevenueAccount');
$method->setAccessible(true);
$result = $method->invoke($this->service, 7, true, false, false);
$this->assertEquals(8300, $result['konto']);
$this->assertEquals(8, $result['bu']);
}
/** @test */
public function it_maps_domestic_5_percent_to_account_8300_bu_8()
{
$method = new \ReflectionMethod(DatevExportService::class, 'determineRevenueAccount');
$method->setAccessible(true);
$result = $method->invoke($this->service, 5, true, false, false);
$this->assertEquals(8300, $result['konto']);
$this->assertEquals(8, $result['bu']);
}
/** @test */
public function it_maps_eu_with_ustid_to_account_8125_bu_1()
{
$method = new \ReflectionMethod(DatevExportService::class, 'determineRevenueAccount');
$method->setAccessible(true);
$result = $method->invoke($this->service, 19, false, true, true);
$this->assertEquals(8125, $result['konto']);
$this->assertEquals(1, $result['bu']);
}
/** @test */
public function it_maps_eu_without_ustid_to_domestic_19_percent()
{
$method = new \ReflectionMethod(DatevExportService::class, 'determineRevenueAccount');
$method->setAccessible(true);
$result = $method->invoke($this->service, 19, false, true, false);
$this->assertEquals(8400, $result['konto']);
$this->assertEquals(9, $result['bu']);
}
/** @test */
public function it_maps_eu_without_ustid_7_percent_to_domestic_7()
{
$method = new \ReflectionMethod(DatevExportService::class, 'determineRevenueAccount');
$method->setAccessible(true);
$result = $method->invoke($this->service, 7, false, true, false);
$this->assertEquals(8300, $result['konto']);
$this->assertEquals(8, $result['bu']);
}
/** @test */
public function it_maps_third_country_exempt_to_account_8120_bu_11()
{
$method = new \ReflectionMethod(DatevExportService::class, 'determineRevenueAccount');
$method->setAccessible(true);
$result = $method->invoke($this->service, 19, false, false, false);
$this->assertEquals(8120, $result['konto']);
$this->assertEquals(11, $result['bu']);
}
/*
|--------------------------------------------------------------------------
| Commission Tax Status Tests
|--------------------------------------------------------------------------
*/
/** @test */
public function it_determines_normal_tax_status_for_vat_registered()
{
$method = new \ReflectionMethod(DatevExportService::class, 'determineCommissionTaxStatus');
$method->setAccessible(true);
$account = new \stdClass;
$account->reverse_charge = false;
$account->taxable_sales = 1;
$result = $method->invoke($this->service, $account);
$this->assertEquals('normal', $result);
}
/** @test */
public function it_determines_kleinunternehmer_for_taxable_sales_2()
{
$method = new \ReflectionMethod(DatevExportService::class, 'determineCommissionTaxStatus');
$method->setAccessible(true);
$account = new \stdClass;
$account->reverse_charge = false;
$account->taxable_sales = 2;
$result = $method->invoke($this->service, $account);
$this->assertEquals('kleinunternehmer', $result);
}
/** @test */
public function it_determines_reverse_charge_when_flag_is_set()
{
$method = new \ReflectionMethod(DatevExportService::class, 'determineCommissionTaxStatus');
$method->setAccessible(true);
$account = new \stdClass;
$account->reverse_charge = true;
$account->taxable_sales = 1;
$result = $method->invoke($this->service, $account);
$this->assertEquals('reverse_charge', $result);
}
/** @test */
public function reverse_charge_takes_priority_over_other_statuses()
{
$method = new \ReflectionMethod(DatevExportService::class, 'determineCommissionTaxStatus');
$method->setAccessible(true);
$account = new \stdClass;
$account->reverse_charge = true;
$account->taxable_sales = 2; // Even if Kleinunternehmer, RC takes priority
$result = $method->invoke($this->service, $account);
$this->assertEquals('reverse_charge', $result);
}
/** @test */
public function null_account_defaults_to_normal()
{
$method = new \ReflectionMethod(DatevExportService::class, 'determineCommissionTaxStatus');
$method->setAccessible(true);
$result = $method->invoke($this->service, null);
$this->assertEquals('normal', $result);
}
/*
|--------------------------------------------------------------------------
| CSV Rendering Tests
|--------------------------------------------------------------------------
*/
/** @test */
public function it_renders_csv_row_with_correct_column_count()
{
$method = new \ReflectionMethod(DatevExportService::class, 'renderCsvRow');
$method->setAccessible(true);
$data = [
'amount_gross' => 119.00,
'soll_haben' => 'H',
'konto' => 8400,
'gegenkonto' => 10000,
'bu_schluessel' => 9,
'belegdatum' => '2025-07-01',
'belegfeld1' => '202500123',
'buchungstext' => 'Mustermann Max',
'eu_ustid' => null,
];
$row = $method->invoke($this->service, $data);
$columns = explode(';', $row);
$this->assertCount(116, $columns, 'DATEV CSV row must have exactly 116 columns');
}
/** @test */
public function it_formats_amount_with_comma_as_decimal_separator()
{
$method = new \ReflectionMethod(DatevExportService::class, 'renderCsvRow');
$method->setAccessible(true);
$data = [
'amount_gross' => 1234.56,
'soll_haben' => 'H',
'konto' => 8400,
'gegenkonto' => 10000,
'bu_schluessel' => 9,
'belegdatum' => '2025-07-01',
'belegfeld1' => 'TEST-001',
'buchungstext' => 'Test',
];
$row = $method->invoke($this->service, $data);
$columns = explode(';', $row);
$this->assertEquals('1234,56', $columns[0], 'Amount must use comma as decimal separator');
}
/** @test */
public function it_formats_belegdatum_as_ddmm()
{
$method = new \ReflectionMethod(DatevExportService::class, 'renderCsvRow');
$method->setAccessible(true);
$data = [
'amount_gross' => 100.00,
'soll_haben' => 'H',
'konto' => 8400,
'gegenkonto' => 10000,
'bu_schluessel' => 9,
'belegdatum' => '2025-07-15',
'belegfeld1' => 'TEST',
'buchungstext' => 'Test',
];
$row = $method->invoke($this->service, $data);
$columns = explode(';', $row);
$this->assertEquals('1507', $columns[9], 'Belegdatum must be in TTMM format');
}
/** @test */
public function it_places_fields_in_correct_datev_columns()
{
$method = new \ReflectionMethod(DatevExportService::class, 'renderCsvRow');
$method->setAccessible(true);
$data = [
'amount_gross' => 119.00,
'soll_haben' => 'H',
'konto' => 8400,
'gegenkonto' => 10000,
'bu_schluessel' => 9,
'belegdatum' => '2025-01-07',
'belegfeld1' => '202500001',
'buchungstext' => 'Nachname Vorname',
'eu_ustid' => 'DE123456789',
];
$row = $method->invoke($this->service, $data);
$columns = explode(';', $row);
// Spalte A (Index 0): Umsatz
$this->assertEquals('119,00', $columns[0]);
// Spalte B (Index 1): Soll/Haben
$this->assertEquals('H', $columns[1]);
// Spalte G (Index 6): Konto
$this->assertEquals('8400', $columns[6]);
// Spalte H (Index 7): Gegenkonto
$this->assertEquals('10000', $columns[7]);
// Spalte I (Index 8): BU-Schluessel
$this->assertEquals('9', $columns[8]);
// Spalte J (Index 9): Belegdatum (TTMM)
$this->assertEquals('0701', $columns[9]);
// Spalte K (Index 10): Belegfeld 1
$this->assertEquals('202500001', $columns[10]);
// Spalte N (Index 13): Buchungstext
$this->assertEquals('Nachname Vorname', $columns[13]);
// Spalte AN (Index 39): EU-Land u. UStID
$this->assertEquals('DE123456789', $columns[39]);
}
/*
|--------------------------------------------------------------------------
| Validation Tests
|--------------------------------------------------------------------------
*/
/** @test */
public function validation_detects_missing_belegdatum()
{
$lines = collect([
[
'source_type' => 'invoice',
'source_id' => 1,
'amount_gross' => 100,
'belegdatum' => null,
'belegfeld1' => 'TEST-001',
'buchungstext' => 'Test',
'bu_schluessel' => 9,
],
]);
$result = $this->service->validate($lines);
$this->assertFalse($result['valid']);
$this->assertNotEmpty($result['errors']);
$this->assertStringContainsString('Belegdatum', $result['errors'][0]['message']);
}
/** @test */
public function validation_detects_missing_belegnummer()
{
$lines = collect([
[
'source_type' => 'invoice',
'source_id' => 1,
'amount_gross' => 100,
'belegdatum' => '2025-01-01',
'belegfeld1' => '',
'buchungstext' => 'Test',
'bu_schluessel' => 9,
],
]);
$result = $this->service->validate($lines);
$this->assertFalse($result['valid']);
$this->assertStringContainsString('Belegnummer', $result['errors'][0]['message']);
}
/** @test */
public function validation_warns_on_eu_delivery_without_ustid()
{
$lines = collect([
[
'source_type' => 'invoice',
'source_id' => 1,
'amount_gross' => 100,
'belegdatum' => '2025-01-01',
'belegfeld1' => 'TEST-001',
'buchungstext' => 'Test',
'bu_schluessel' => 1,
'eu_ustid' => null,
],
]);
$result = $this->service->validate($lines);
$this->assertTrue($result['valid']);
$this->assertNotEmpty($result['warnings']);
$this->assertStringContainsString('EU-Lieferung', $result['warnings'][0]['message']);
}
/** @test */
public function validation_warns_on_reverse_charge_without_ustid()
{
$lines = collect([
[
'source_type' => 'credit',
'source_id' => 1,
'amount_gross' => 50,
'belegdatum' => '2025-01-01',
'belegfeld1' => 'GS-001',
'buchungstext' => 'Test',
'bu_schluessel' => 94,
'eu_ustid' => null,
],
]);
$result = $this->service->validate($lines);
$this->assertTrue($result['valid']);
$this->assertNotEmpty($result['warnings']);
$this->assertStringContainsString('Reverse Charge', $result['warnings'][0]['message']);
}
/** @test */
public function validation_passes_for_complete_data()
{
$lines = collect([
[
'source_type' => 'invoice',
'source_id' => 1,
'amount_gross' => 119.00,
'belegdatum' => '2025-07-01',
'belegfeld1' => '202500001',
'buchungstext' => 'Mustermann Max',
'bu_schluessel' => 9,
'eu_ustid' => null,
],
]);
$result = $this->service->validate($lines);
$this->assertTrue($result['valid']);
$this->assertEmpty($result['errors']);
}
/*
|--------------------------------------------------------------------------
| Number Parsing Tests
|--------------------------------------------------------------------------
*/
/** @test */
public function it_parses_formatted_numbers_correctly()
{
$method = new \ReflectionMethod(DatevExportService::class, 'parseNumber');
$method->setAccessible(true);
// Standard format from number_format
$this->assertEquals(5.00, $method->invoke($this->service, '5.00'));
$this->assertEquals(1234.56, $method->invoke($this->service, '1234.56'));
// Comma format
$this->assertEquals(5.00, $method->invoke($this->service, '5,00'));
}
/** @test */
public function it_parses_homeparty_tax_split_format()
{
$method = new \ReflectionMethod(DatevExportService::class, 'parseNumber');
$method->setAccessible(true);
// Homeparty format: ['vk_tax' => '5.00', 'ek_tax' => '2.00']
$result = $method->invoke($this->service, ['vk_tax' => '15.50', 'ek_tax' => '7.50']);
$this->assertEquals(15.50, $result);
}
/*
|--------------------------------------------------------------------------
| Model Tests
|--------------------------------------------------------------------------
*/
/** @test */
public function datev_export_status_labels_are_defined()
{
$this->assertEquals('Entwurf', DatevExport::STATUS_LABELS[DatevExport::STATUS_DRAFT]);
$this->assertEquals('Generiert', DatevExport::STATUS_LABELS[DatevExport::STATUS_GENERATED]);
$this->assertEquals('Heruntergeladen', DatevExport::STATUS_LABELS[DatevExport::STATUS_DOWNLOADED]);
$this->assertEquals('Gesperrt', DatevExport::STATUS_LABELS[DatevExport::STATUS_LOCKED]);
}
/** @test */
public function datev_export_line_source_types_are_defined()
{
$this->assertEquals('invoice', DatevExportLine::SOURCE_INVOICE);
$this->assertEquals('credit', DatevExportLine::SOURCE_CREDIT);
$this->assertEquals('cancellation', DatevExportLine::SOURCE_CANCELLATION);
}
/*
|--------------------------------------------------------------------------
| EU USt-ID Resolution Tests
|--------------------------------------------------------------------------
*/
/** @test */
public function it_resolves_eu_ustid_from_order_auth_user_account()
{
$method = new \ReflectionMethod(DatevExportService::class, 'resolveEuUstid');
$method->setAccessible(true);
$account = new \stdClass;
$account->tax_identification_number = 'ATU12345678';
$user = new \stdClass;
$user->account = $account;
$order = new \stdClass;
$order->auth_user = $user;
$result = $method->invoke($this->service, $order, false);
$this->assertEquals('ATU12345678', $result);
}
/** @test */
public function it_returns_null_ustid_for_domestic_orders()
{
$method = new \ReflectionMethod(DatevExportService::class, 'resolveEuUstid');
$method->setAccessible(true);
$account = new \stdClass;
$account->tax_identification_number = 'DE123456789';
$user = new \stdClass;
$user->account = $account;
$order = new \stdClass;
$order->auth_user = $user;
$result = $method->invoke($this->service, $order, true);
$this->assertNull($result);
}
/** @test */
public function it_returns_null_ustid_for_guest_orders()
{
$method = new \ReflectionMethod(DatevExportService::class, 'resolveEuUstid');
$method->setAccessible(true);
$order = new \stdClass;
$order->auth_user = null;
$result = $method->invoke($this->service, $order, false);
$this->assertNull($result);
}
/** @test */
public function it_returns_null_ustid_when_account_has_no_tax_id()
{
$method = new \ReflectionMethod(DatevExportService::class, 'resolveEuUstid');
$method->setAccessible(true);
$account = new \stdClass;
$account->tax_identification_number = null;
$user = new \stdClass;
$user->account = $account;
$order = new \stdClass;
$order->auth_user = $user;
$result = $method->invoke($this->service, $order, false);
$this->assertNull($result);
}
/** @test */
public function validation_no_warning_for_eu_delivery_with_ustid()
{
$lines = collect([
[
'source_type' => 'invoice',
'source_id' => 1,
'amount_gross' => 100,
'belegdatum' => '2025-01-01',
'belegfeld1' => 'TEST-001',
'buchungstext' => 'Test',
'bu_schluessel' => 1,
'eu_ustid' => 'ATU12345678',
],
]);
$result = $this->service->validate($lines);
$this->assertTrue($result['valid']);
$euWarnings = array_filter($result['warnings'], fn ($w) => str_contains($w['message'], 'EU-Lieferung'));
$this->assertEmpty($euWarnings);
}
/** @test */
public function validation_returns_structured_entries_with_metadata()
{
$lines = collect([
[
'source_type' => 'invoice',
'source_id' => 42,
'order_id' => 100,
'user_id' => 5,
'amount_gross' => 100,
'belegdatum' => '2025-01-01',
'belegfeld1' => 'TEST-001',
'buchungstext' => 'Test',
'bu_schluessel' => 1,
'eu_ustid' => null,
],
]);
$result = $this->service->validate($lines);
$warning = $result['warnings'][0];
$this->assertIsArray($warning);
$this->assertArrayHasKey('message', $warning);
$this->assertArrayHasKey('source_id', $warning);
$this->assertArrayHasKey('order_id', $warning);
$this->assertArrayHasKey('user_id', $warning);
$this->assertEquals(42, $warning['source_id']);
$this->assertEquals(100, $warning['order_id']);
$this->assertEquals(5, $warning['user_id']);
}
/*
|--------------------------------------------------------------------------
| Config Tests
|--------------------------------------------------------------------------
*/
/** @test */
public function datev_config_has_required_keys()
{
$this->assertNotNull(config('datev.revenue_accounts'));
$this->assertNotNull(config('datev.commission_accounts'));
$this->assertNotNull(config('datev.commission_tax_keys'));
$this->assertNotNull(config('datev.counteraccount_map'));
$this->assertNotNull(config('datev.sammeldebitor'));
$this->assertNotNull(config('datev.sammelkreditor'));
}
/** @test */
public function datev_config_revenue_accounts_are_complete()
{
$accounts = config('datev.revenue_accounts');
$this->assertArrayHasKey('domestic_19', $accounts);
$this->assertArrayHasKey('domestic_7', $accounts);
$this->assertArrayHasKey('eu_exempt', $accounts);
$this->assertArrayHasKey('third_country_exempt', $accounts);
// Verify correct account numbers
$this->assertEquals(8400, $accounts['domestic_19']['konto']);
$this->assertEquals(8300, $accounts['domestic_7']['konto']);
$this->assertEquals(8125, $accounts['eu_exempt']['konto']);
$this->assertEquals(8120, $accounts['third_country_exempt']['konto']);
}
/** @test */
public function datev_config_commission_accounts_map_correctly()
{
$accounts = config('datev.commission_accounts');
$this->assertEquals(4760, $accounts['shop']);
$this->assertEquals(4760, $accounts['growth_bonus']);
$this->assertEquals(4764, $accounts['payline']);
}
/** @test */
public function datev_column_headers_have_correct_count()
{
$reflection = new \ReflectionClass(DatevExportService::class);
$prop = $reflection->getConstant('COLUMN_COUNT');
$this->assertEquals(116, $prop);
}
}

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