Statistiken/Abo: Kundenfeedback 2026-07-24 komplett umgesetzt + Steuern: DATEV-Fixes 08+09/2025 (Sammel-Commit Branch-WIP)

Feedback-Umsetzung (dev/2026-07-22/feedback-analyse-2026-07-24.md, alle 10 Punkte):
- VIP-Statistik: eigener Berater ('ich', Linie 0) in Neue-Kundenabos- und beiden Top-10-Listen; Top-10-Umsatz sortiert nach abgerechneten Ist-Punkten + Abo-Anzahl-Spalte; Fußnote nicht-payline-wirksame Punkte (SalesVolumePoints::nonPaylinePoints)
- Führungskräfte-Liste: beide Sichten 'direkt' (an nächster FK abgeschnitten, additiv) und 'gesamt' (komplette Downline), Gesamt-Spalte nach Kundenzählweise
- Team-Abos: je Ebene 'abgerechnet (Monat)' und 'bis zum 25. erwartet'; Payline-Karte mit Teamabo-/Kundenabo-Zählern
- Incentive-Live-Ranking: Qualifizierte zuerst nummeriert (fixt auch isWinner rank<=max_winners)
- Porto: Netto-/Brutto-Kennzeichnung (Berater netto, Endkunden inkl. MwSt., portal_view-Flag; auch CRM-/Shop-Warenkorb + Checkout), Yard::shipping_tax-Fix
- Struktur-Ansicht: Abo-Badges 'A' (eigenes Abo) und '(n)' (Kundenabos) je Teampartner, gebatcht
- Abo-Löschung: Softdelete setzt active=0 (UserAbo deleting-Hook) + Bereinigungs-Migration für gelöschte-aber-aktive Abos
- Doku: technische Analyse + Kundinnen-Feedback-Dokument unter dev/2026-07-22/
- Tests: IncentiveRankingTest, TeamStructureAboBadgesTest, YardShippingTaxTest (neu); BackofficeStatisticsListsTest, TeamAboOverviewTest, PaylineForecastServiceTest, AboAdminDeleteTest, AboOneTimeViewTest erweitert

Außerdem enthalten (Branch-WIP vor dem Feedback): DATEV-Abgleich-Fixes 08+09/2025, USt-ID-Live-Validierung sowie restliche unkommittete Stände (u. a. Abo-Pause-Sprachkeys).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Kevin 2026-07-24 13:51:16 +00:00
parent 657d4bd5b7
commit ac3bd6e8ea
136 changed files with 7610 additions and 2688 deletions

View file

@ -60,7 +60,7 @@ class DatevExportServiceTest extends TestCase
}
/** @test */
public function it_maps_eu_with_ustid_to_account_8125_bu_1()
public function it_maps_eu_sold_tax_free_to_account_8125_bu_1()
{
$method = new \ReflectionMethod(DatevExportService::class, 'determineRevenueAccount');
$method->setAccessible(true);
@ -72,7 +72,7 @@ class DatevExportServiceTest extends TestCase
}
/** @test */
public function it_maps_eu_without_ustid_to_domestic_19_percent()
public function it_maps_eu_sold_with_tax_to_domestic_19_percent()
{
$method = new \ReflectionMethod(DatevExportService::class, 'determineRevenueAccount');
$method->setAccessible(true);
@ -84,7 +84,7 @@ class DatevExportServiceTest extends TestCase
}
/** @test */
public function it_maps_eu_without_ustid_7_percent_to_domestic_7()
public function it_maps_eu_sold_with_tax_7_percent_to_domestic_7()
{
$method = new \ReflectionMethod(DatevExportService::class, 'determineRevenueAccount');
$method->setAccessible(true);
@ -184,6 +184,71 @@ class DatevExportServiceTest extends TestCase
$this->assertEquals('normal', $result);
}
/** @test */
public function credit_taxable_snapshot_takes_priority_over_current_account()
{
$method = new \ReflectionMethod(DatevExportService::class, 'determineCommissionTaxStatus');
$method->setAccessible(true);
// Account ist heute USt-pflichtig, aber die Gutschrift wurde als
// Kleinunternehmer ausgestellt -> Beleg zählt
$account = new \stdClass;
$account->reverse_charge = false;
$account->taxable_sales = 1;
$credit = new \stdClass;
$credit->taxable = 2;
$this->assertEquals('kleinunternehmer', $method->invoke($this->service, $account, $credit));
// Umgekehrt: Account heute Kleinunternehmer, Beleg mit USt ausgestellt
$account->taxable_sales = 2;
$credit->taxable = 1;
$this->assertEquals('normal', $method->invoke($this->service, $account, $credit));
}
/** @test */
public function credit_taxable_3_maps_to_reverse_charge_for_eu_account()
{
$method = new \ReflectionMethod(DatevExportService::class, 'determineCommissionTaxStatus');
$method->setAccessible(true);
$country = new \stdClass;
$country->eu_country = true;
$account = new \stdClass;
$account->reverse_charge = false;
$account->taxable_sales = 1;
$account->country = $country;
$credit = new \stdClass;
$credit->taxable = 3;
$this->assertEquals('reverse_charge', $method->invoke($this->service, $account, $credit));
// Drittland -> Kleinunternehmer-Fallback (kein BU-Schlüssel nötig)
$country->eu_country = false;
$this->assertEquals('kleinunternehmer', $method->invoke($this->service, $account, $credit));
}
/** @test */
public function credit_without_taxable_snapshot_falls_back_to_account()
{
$method = new \ReflectionMethod(DatevExportService::class, 'determineCommissionTaxStatus');
$method->setAccessible(true);
$account = new \stdClass;
$account->reverse_charge = false;
$account->taxable_sales = 2;
$credit = new \stdClass;
$credit->taxable = 0;
$this->assertEquals('kleinunternehmer', $method->invoke($this->service, $account, $credit));
}
/*
|--------------------------------------------------------------------------
| CSV Rendering Tests
@ -446,6 +511,31 @@ class DatevExportServiceTest extends TestCase
$this->assertEquals(5.00, $method->invoke($this->service, '5,00'));
}
/** @test */
public function it_parses_numbers_with_thousands_separators()
{
$method = new \ReflectionMethod(DatevExportService::class, 'parseNumber');
$method->setAccessible(true);
// US-Format mit Tausendertrenner (so speichert net_split Beträge >= 1000)
$this->assertEquals(1434.70, $method->invoke($this->service, '1,434.70'));
$this->assertEquals(1044.69, $method->invoke($this->service, '1,044.69'));
$this->assertEquals(1234567.89, $method->invoke($this->service, '1,234,567.89'));
// Deutsches Format mit Tausendertrenner
$this->assertEquals(1434.70, $method->invoke($this->service, '1.434,70'));
// Nur Tausendertrenner ohne Dezimalstellen
$this->assertEquals(1434.0, $method->invoke($this->service, '1,434'));
// Negative Werte
$this->assertEquals(-1234.56, $method->invoke($this->service, '-1,234.56'));
// Numerische Eingaben und Leerstring
$this->assertEquals(12.5, $method->invoke($this->service, 12.5));
$this->assertEquals(0.0, $method->invoke($this->service, ''));
}
/** @test */
public function it_parses_homeparty_tax_split_format()
{
@ -599,6 +689,46 @@ class DatevExportServiceTest extends TestCase
$this->assertNull($result);
}
/** @test */
public function it_rejects_german_ustid_for_eu_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, false);
$this->assertNull($result);
}
/** @test */
public function it_rejects_implausible_ustid_for_eu_orders()
{
$method = new \ReflectionMethod(DatevExportService::class, 'resolveEuUstid');
$method->setAccessible(true);
$account = new \stdClass;
$account->tax_identification_number = '53618009';
$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 it_returns_null_ustid_when_account_has_no_tax_id()
{