'DE'], [ 'phone' => '49', 'en' => 'Germany', 'de' => 'Deutschland', 'es' => 'Alemania', 'fr' => 'Allemagne', 'it' => 'Germania', 'ru' => 'Германия', ] ); } function makeChecklistOrder(string $lastName, float $gross): ShoppingOrder { $country = makeChecklistCountry(); $shoppingUserId = DB::table('shopping_users')->insertGetId([ 'auth_user_id' => 0, 'member_id' => 0, 'billing_firstname' => 'Test', 'billing_lastname' => $lastName, 'billing_email' => strtolower($lastName).'-'.uniqid('', true).'@example.com', 'billing_country_id' => $country->id, 'shipping_country_id' => $country->id, 'wp_order_number' => 0, 'created_at' => now(), 'updated_at' => now(), ]); $net = round($gross / 1.19, 2); return ShoppingOrder::forceCreate([ 'shopping_user_id' => $shoppingUserId, 'auth_user_id' => 0, 'member_id' => 0, 'country_id' => $country->id, 'user_shop_id' => 1, 'total' => $gross, 'total_shipping' => $gross, 'tax_split' => ['19' => (string) round($gross - $net, 2)], 'net_split' => ['19' => (string) $net], ]); } function makeChecklistInvoice(ShoppingOrder $order, string $number, string $date, array $overrides = []): UserInvoice { return UserInvoice::forceCreate(array_merge([ 'shopping_order_id' => $order->id, 'date' => $date, 'month' => (int) date('n', strtotime($date)), 'year' => (int) date('Y', strtotime($date)), 'full_number' => $number, 'number' => (int) substr($number, -5), 'status' => 2, ], $overrides)); } it('lists both invoices behind a duplicated invoice number with the counterpart named', function () { makeChecklistInvoice(makeChecklistOrder('Hoppe', 24.80), '202699100', '2026-08-11'); makeChecklistInvoice(makeChecklistOrder('Kuhn', 144.90), '202699100', '2026-08-11'); $rows = app(DatevReconciliationService::class)->build(8, 2026) ->where('kategorie', DatevReconciliationService::CATEGORY_DUPLICATE_NUMBER); expect($rows)->toHaveCount(2); $hoppe = $rows->firstWhere('name', 'Hoppe Test'); expect($hoppe['betrag_brutto'])->toBe(24.80); expect($hoppe['hinweis'])->toContain('2× vergeben'); expect($hoppe['hinweis'])->toContain('169,70'); expect($hoppe['hinweis'])->toContain('Kuhn Test (144,90 €)'); }); it('flags an invoice refunded through the payment provider without a cancellation document', function () { $order = makeChecklistOrder('Matyjaszek', 113.40); makeChecklistInvoice($order, '202699200', '2026-08-05'); DB::table('shopping_payments')->insert([ 'shopping_order_id' => $order->id, 'clearingtype' => 'wlt', 'reference' => 'ref-'.uniqid('', true), 'identifier' => 'id-'.uniqid('', true), 'txaction' => 'refund', 'status' => 'success', 'amount' => 11340, 'currency' => 'EUR', 'created_at' => '2026-08-05 10:00:00', 'updated_at' => '2026-08-28 10:00:00', ]); $rows = app(DatevReconciliationService::class)->build(8, 2026) ->where('kategorie', DatevReconciliationService::CATEGORY_EXTERNAL_CANCELLATION); expect($rows)->toHaveCount(1); expect($rows->first()['belegnummer'])->toBe('202699200'); expect($rows->first()['hinweis'])->toContain('28.08.2026'); expect($rows->first()['hinweis'])->toContain('kein Stornobeleg'); }); it('points a cross-month cancellation at its original invoice and back', function () { $order = makeChecklistOrder('Ebner', 123.10); $original = makeChecklistInvoice($order, '202699300', '2026-08-24'); $storno = makeChecklistInvoice($order, '202699301', '2026-09-25', [ 'status' => 12, 'cancellation' => true, ]); $original->cancellation = true; $original->cancellation_id = $storno->id; $original->save(); $service = app(DatevReconciliationService::class); $august = $service->build(8, 2026) ->where('kategorie', DatevReconciliationService::CATEGORY_CANCELLED_LATER); expect($august)->toHaveCount(1); expect($august->first()['hinweis'])->toContain('202699301'); expect($august->first()['empfehlung'])->toContain('Stornomonat'); $september = $service->build(9, 2026) ->where('kategorie', DatevReconciliationService::CATEGORY_CROSS_MONTH_CANCELLATION); expect($september)->toHaveCount(1); expect($september->first()['hinweis'])->toContain('202699300'); expect($september->first()['hinweis'])->toContain('08/2026'); }); it('stays empty for a clean period', function () { makeChecklistInvoice(makeChecklistOrder('Sauber', 119.00), '202699400', '2026-08-15'); expect(app(DatevReconciliationService::class)->build(8, 2026))->toBeEmpty(); }); it('renders the checklist as a quoted csv with a header row', function () { makeChecklistInvoice(makeChecklistOrder('Hoppe', 24.80), '202699100', '2026-08-11'); makeChecklistInvoice(makeChecklistOrder('Kuhn', 144.90), '202699100', '2026-08-11'); $csv = app(DatevReconciliationService::class)->buildCsv(8, 2026); expect($csv)->toStartWith("\xEF\xBB\xBF\"Kategorie\";\"Belegnummer\""); expect($csv)->toContain('"Doppelt vergebene Rechnungsnummer"'); expect($csv)->toContain('"24,80"'); expect(substr_count($csv, "\r\n"))->toBe(3); });