mivita/tests/Feature/DatevExportCancellationTest.php
Kevin 1a3af17f9a DATEV: stornierte Rechnungen im Rechnungsmonat, Pruefliste fuer den Steuerberater
- Eine spaeter stornierte Rechnung fehlte im Export ihres Rechnungsmonats,
  weil cancellation = true auch an der Originalrechnung steht. Sie bleibt
  jetzt als Erloes im Rechnungsmonat; die Stornierung wird im Monat des
  Stornobelegs gegengebucht (nur Belege ohne cancellation_id).
- Pruefliste je Buchungsstapel als CSV (Button im Steuerberater-Bereich):
  Belege, die sich ohne Zusatzinformation nicht abstimmen lassen (doppelte
  Rechnungsnummern, Stornos ausserhalb des Systems).
- Korrespondenz und offene Differenzen mit dem Steuerberater unter
  dev/2026-07-17-steuern/.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-23 15:42:54 +00:00

130 lines
4.4 KiB
PHP

<?php
use App\Models\Country;
use App\Models\ShoppingOrder;
use App\Models\UserInvoice;
use App\Services\DatevExportService;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\DB;
use Tests\TestCase;
uses(TestCase::class, RefreshDatabase::class);
/**
* Legt eine Bestellung mit Original- und Stornorechnung an.
*
* `cancellation = true` steht an beiden Belegen; nur der Stornobeleg hat kein
* `cancellation_id` (siehe InvoiceRepository::cancel).
*
* @return array{original: UserInvoice, storno: UserInvoice}
*/
function makeCancelledInvoicePair(string $invoiceDate, string $cancellationDate): array
{
$country = Country::firstOrCreate(
['code' => 'DE'],
[
'phone' => '49', 'en' => 'Germany', 'de' => 'Deutschland', 'es' => 'Alemania',
'fr' => 'Allemagne', 'it' => 'Germania', 'ru' => 'Германия',
]
);
$shoppingUserId = DB::table('shopping_users')->insertGetId([
'auth_user_id' => 0,
'member_id' => 0,
'billing_firstname' => 'Erika',
'billing_lastname' => 'Mustermann',
'billing_email' => 'erika-'.uniqid('', true).'@example.com',
'billing_country_id' => $country->id,
'shipping_country_id' => $country->id,
'wp_order_number' => 0,
'created_at' => now(), 'updated_at' => now(),
]);
$order = ShoppingOrder::forceCreate([
'shopping_user_id' => $shoppingUserId,
'auth_user_id' => 0,
'member_id' => 0,
'country_id' => $country->id,
'user_shop_id' => 1,
'total' => 119.00,
'total_shipping' => 119.00,
'tax_split' => ['19' => '19.00'],
'net_split' => ['19' => '100.00'],
]);
$original = UserInvoice::forceCreate([
'shopping_order_id' => $order->id,
'date' => $invoiceDate,
'month' => (int) date('n', strtotime($invoiceDate)),
'year' => (int) date('Y', strtotime($invoiceDate)),
'full_number' => '202699001',
'number' => 99001,
'status' => 2,
]);
$storno = UserInvoice::forceCreate([
'shopping_order_id' => $order->id,
'date' => $cancellationDate,
'month' => (int) date('n', strtotime($cancellationDate)),
'year' => (int) date('Y', strtotime($cancellationDate)),
'full_number' => '202699002',
'number' => 99002,
'status' => 12,
'cancellation' => true,
]);
$original->cancellation = true;
$original->cancellation_id = $storno->id;
$original->cancellation_date = $cancellationDate;
$original->save();
return ['original' => $original, 'storno' => $storno];
}
it('buys the revenue in the invoice month and reverses it in the cancellation month', function () {
makeCancelledInvoicePair('2026-08-15', '2026-09-03');
$service = new DatevExportService;
$august = $service->collectInvoiceLines(8, 2026)
->concat($service->collectCancellationLines(8, 2026));
expect($august)->toHaveCount(1);
expect($august->first()['soll_haben'])->toBe('H');
expect($august->first()['belegfeld1'])->toBe('202699001');
expect(round($august->first()['amount_gross'], 2))->toBe(119.00);
$september = $service->collectInvoiceLines(9, 2026)
->concat($service->collectCancellationLines(9, 2026));
expect($september)->toHaveCount(1);
expect($september->first()['soll_haben'])->toBe('S');
expect($september->first()['belegfeld1'])->toBe('202699002');
expect(round($september->first()['amount_gross'], 2))->toBe(119.00);
});
it('nets a same-month cancellation to zero instead of booking it twice', function () {
makeCancelledInvoicePair('2026-08-15', '2026-08-20');
$service = new DatevExportService;
$lines = $service->collectInvoiceLines(8, 2026)
->concat($service->collectCancellationLines(8, 2026));
expect($lines)->toHaveCount(2);
$signed = $lines->sum(fn (array $line) => $line['soll_haben'] === 'S'
? -$line['amount_gross']
: $line['amount_gross']);
expect(round($signed, 2))->toBe(0.00);
});
it('does not treat the cancelled original invoice as a cancellation document', function () {
makeCancelledInvoicePair('2026-08-15', '2026-08-20');
$cancellationLines = (new DatevExportService)->collectCancellationLines(8, 2026);
expect($cancellationLines)->toHaveCount(1);
expect($cancellationLines->first()['belegfeld1'])->toBe('202699002');
});