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>
93 lines
3.5 KiB
PHP
93 lines
3.5 KiB
PHP
<?php
|
||
|
||
namespace App\Services\Backoffice;
|
||
|
||
use App\Services\BusinessPlan\SalesPointsVolume;
|
||
|
||
/**
|
||
* Liefert SQL-Aggregat-Ausdrücke für die monatliche Punkte-/Umsatzauswertung
|
||
* der Backoffice-Statistik.
|
||
*
|
||
* Wichtig: In `user_sales_volumes` existiert pro Bestellung eine Zeile, und die
|
||
* Spalten `month_KP_points` / `month_shop_points` / `month_*_total_net` sind der
|
||
* kumulative Monats-Laufstand zum Zeitpunkt der Bestellung – KEINE Einzelbeträge.
|
||
* Eine SUM() über diese Spalten zählt mehrfach. Korrekt wird je Zeile die
|
||
* `points`/`total_net`-Spalte summiert und nach `status` gefiltert – analog zu
|
||
* {@see SalesPointsVolume::reCalculateSalesPointsVolume()}.
|
||
*
|
||
* Status-Zuordnung:
|
||
* - Eigenpunkte (KP): status 1, 4, 5, 6
|
||
* - Shop-/Kundenpunkte: status 2 (sowie status 6 mit status_turnover = 2)
|
||
* - status 0 (nicht zugeordnet) und 3 (pending) zählen nicht
|
||
*
|
||
* Alle Spalten werden voll qualifiziert, da `shopping_orders` ebenfalls eine
|
||
* `points`-Spalte besitzt (Join-Mehrdeutigkeit).
|
||
*/
|
||
class SalesVolumePoints
|
||
{
|
||
private const OWN_PREDICATE = 'user_sales_volumes.status IN (1, 4, 5, 6)';
|
||
|
||
private const SHOP_PREDICATE = '(user_sales_volumes.status = 2 OR (user_sales_volumes.status = 6 AND user_sales_volumes.status_turnover = 2))';
|
||
|
||
private const TURNOVER_PREDICATE = 'user_sales_volumes.status IN (1, 2, 4, 5, 6)';
|
||
|
||
public static function ownPoints(): string
|
||
{
|
||
return self::sumWhere(self::OWN_PREDICATE, 'user_sales_volumes.points');
|
||
}
|
||
|
||
public static function externalPoints(): string
|
||
{
|
||
return self::sumWhere(self::SHOP_PREDICATE, 'user_sales_volumes.points');
|
||
}
|
||
|
||
public static function customerAboPoints(): string
|
||
{
|
||
return self::sumWhere(self::SHOP_PREDICATE.' AND shopping_orders.is_abo = 1', 'user_sales_volumes.points');
|
||
}
|
||
|
||
public static function customerSingleOrderPoints(): string
|
||
{
|
||
return self::sumWhere(self::SHOP_PREDICATE.' AND shopping_orders.id IS NOT NULL AND (shopping_orders.is_abo = 0 OR shopping_orders.is_abo IS NULL)', 'user_sales_volumes.points');
|
||
}
|
||
|
||
public static function customerOtherPoints(): string
|
||
{
|
||
return self::sumWhere(self::SHOP_PREDICATE.' AND shopping_orders.id IS NULL', 'user_sales_volumes.points');
|
||
}
|
||
|
||
public static function totalPoints(): string
|
||
{
|
||
return self::ownPoints().' + '.self::externalPoints();
|
||
}
|
||
|
||
/**
|
||
* Punkte aus Abo-Bestellungen (eigene wie Kundenabos) — alle zählbaren Status.
|
||
* Benötigt den Join auf shopping_orders.
|
||
*/
|
||
public static function aboPoints(): string
|
||
{
|
||
return self::sumWhere(self::TURNOVER_PREDICATE.' AND shopping_orders.is_abo = 1', 'user_sales_volumes.points');
|
||
}
|
||
|
||
/**
|
||
* Punkte, die nur als Karrierepunkte (KP) des Beraters zählen und NICHT als
|
||
* Teampunkte in die Payline fließen (status_points = 2, z. B.
|
||
* Neupartner-Einschreibungspunkte, manuelle Gutschriften) — vgl.
|
||
* {@see SalesPointsVolume::add_KP_TP_Points()}.
|
||
*/
|
||
public static function nonPaylinePoints(): string
|
||
{
|
||
return self::sumWhere(self::OWN_PREDICATE.' AND user_sales_volumes.status_points = 2', 'user_sales_volumes.points');
|
||
}
|
||
|
||
public static function turnoverNet(): string
|
||
{
|
||
return self::sumWhere(self::TURNOVER_PREDICATE, 'user_sales_volumes.total_net');
|
||
}
|
||
|
||
private static function sumWhere(string $predicate, string $column): string
|
||
{
|
||
return 'COALESCE(SUM(CASE WHEN '.$predicate.' THEN '.$column.' ELSE 0 END), 0)';
|
||
}
|
||
}
|