Kundenabo-Vorschau: bereits abgerechnete Punkte fehlten in der Basis
Die Kachelreihe "Punkte aus meinen Kundenabos zum 5./10./20./25." startete bei einer Basis von 0 und addierte nur die noch offenen Ausfuehrungen. Damit standen die bereits vergangenen Termine auf null, obwohl im Monat laengst Lieferungen gebucht waren, und die spaeteren Termine zeigten nur den Rest statt des Monatsstands. Berater 454 im September: vorher 0 / 0 / 131 / 319, jetzt 457 / 457 / 588 / 776. Betrifft im September 86 Berater mit zusammen 15.936 Punkten — reine Anzeige, gebucht und abgerechnet wurde korrekt. Die Basis ist jetzt die im Monat aus den eigenen Kundenabos abgerechnete Punktzahl, ueber dieselbe Abgrenzung wie die Backoffice-Statistik (SalesVolumePoints::customerAboPoints(), Stornos vorzeichengetreu). Damit liest sich die Kachel wie die Prognose-Kachel auf dem Dashboard: Stand heute plus das, was bis zum Termin noch hereinkommt. Hinweistext in allen vier Sprachen nachgezogen. Nicht aus dem Audit-Paket: Die Null-Basis stammt aus der K9-Umsetzung vom 07.09., nicht aus der A13-Korrektur. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
ff0860fb81
commit
cb5664a483
7 changed files with 132 additions and 10 deletions
|
|
@ -164,10 +164,12 @@ class PaylineForecastService
|
|||
* des Beraters (Kundenwunsch 2026-09-07: „wieviel Volumenpunkte habe ich am
|
||||
* 5. / 10. / 20. / 25. — nur mit meinen Kundenabos").
|
||||
*
|
||||
* Bewusst ohne das eigene Abo und ohne Payline: Basis ist 0, die Kacheln
|
||||
* zeigen also die Punkte, die aus den eigenen Kundenabos bis zum jeweiligen
|
||||
* Termin noch hereinkommen. Das ist ein anderer Zuschnitt als
|
||||
* {@see forecastDays()}, wo eigene Abos in einer Team-Zahl mitlaufen.
|
||||
* Bewusst ohne das eigene Abo und ohne Payline — aber MIT dem, was im Monat
|
||||
* aus den Kundenabos schon abgerechnet ist. Die Basis war zuvor 0; damit
|
||||
* standen die bereits gelieferten Termine auf null, obwohl die Punkte
|
||||
* langst gebucht waren, und die spateren Termine zeigten nur den Rest statt
|
||||
* des Monatsstands. Gleiche Lesart wie {@see forecastDays()} auf dem
|
||||
* Dashboard: Stand heute plus das, was bis zum Termin noch hereinkommt.
|
||||
*
|
||||
* @return array{base: float, days: array<int, array{day: int, date: string, upcoming: float, total: float, is_past: bool}>}
|
||||
*/
|
||||
|
|
@ -184,12 +186,34 @@ class PaylineForecastService
|
|||
->with('user_abo_items.product')
|
||||
->get();
|
||||
|
||||
$base = $this->billedCustomerAboPoints($user, (int) $today->month, (int) $today->year);
|
||||
|
||||
return [
|
||||
'base' => 0.0,
|
||||
'days' => $this->cumulateByDeliveryDay($abos, 0.0, $today),
|
||||
'base' => $base,
|
||||
'days' => $this->cumulateByDeliveryDay($abos, $base, $today),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Im Monat bereits abgerechnete Punkte aus den EIGENEN Kundenabos des
|
||||
* Beraters — die Basis der Kundenabo-Terminvorschau.
|
||||
*
|
||||
* Nutzt dieselbe Abgrenzung wie die Backoffice-Statistik
|
||||
* ({@see SalesVolumePoints::customerAboPoints()}): Shop-/Kundenbuchungen aus
|
||||
* Abo-Bestellungen, Stornos vorzeichengetreu. Das eigene Abo des Beraters
|
||||
* bleibt draussen, es laeuft nicht ueber dieses Praedikat.
|
||||
*/
|
||||
private function billedCustomerAboPoints(User $user, int $month, int $year): float
|
||||
{
|
||||
return (float) UserSalesVolume::query()
|
||||
->leftJoin('shopping_orders', 'shopping_orders.id', '=', 'user_sales_volumes.shopping_order_id')
|
||||
->where('user_sales_volumes.user_id', $user->id)
|
||||
->where('user_sales_volumes.month', $month)
|
||||
->where('user_sales_volumes.year', $year)
|
||||
->selectRaw(SalesVolumePoints::customerAboPoints().' as points')
|
||||
->value('points');
|
||||
}
|
||||
|
||||
/**
|
||||
* Offene Ausführungen auf die vier Liefertermine kumulieren: je Termin die
|
||||
* Punkte aller Abos, deren nächste Ausführung bis einschließlich zu diesem
|
||||
|
|
|
|||
|
|
@ -76,3 +76,50 @@ Arbeitsbaum (DHL, Stripe/Payone, DATEV, `shipped_at`).
|
|||
Ebenfalls unverändert: die vorbestehenden Testfehler auf dem Branch (rund 42 SQLite-Schemafehler in
|
||||
Unit-Tests, 25 fehlende Übersetzungsschlüssel in `payment.php` für es und fr, verwendet in einer
|
||||
Admin-Ansicht). Gehören in ein eigenes Aufräumpaket.
|
||||
|
||||
---
|
||||
|
||||
## Nachtrag 11.09.2026 — Kundenabo-Vorschau zeigte nur den Rest des Monats
|
||||
|
||||
Beim Nachsehen auf Produktion aufgefallen (Berater 454): Auf `/user/abos/ot` stand die Kachelreihe
|
||||
„Punkte aus meinen Kundenabos zum 5. / 10. / 20. / 25." für die bereits vergangenen Termine auf
|
||||
**null**, obwohl im Monat längst Lieferungen gebucht waren.
|
||||
|
||||
**Ursache.** Die Vorschau startete bei einer Basis von 0 und addierte nur die noch **offenen**
|
||||
Ausführungen. Die im Monat bereits abgerechneten Kundenabo-Punkte fehlten vollständig. Die Kachel
|
||||
beantwortete damit „was kommt noch" statt der Frage aus der Anforderung vom 07.09. („wieviel
|
||||
Volumenpunkte habe ich am 5. / 10. / 20. / 25.").
|
||||
|
||||
Berater 454, September 2026:
|
||||
|
||||
| | vorher | jetzt |
|
||||
| --- | --- | --- |
|
||||
| 05.09. | 0 | 457 |
|
||||
| 10.09. | 0 | 457 |
|
||||
| 20.09. | 131 | 588 |
|
||||
| 25.09. | 319 | 776 |
|
||||
|
||||
Die 457 Punkte waren bereits abgerechnet (Lieferungen am 1., 5. und 10.), die 319 stehen noch aus.
|
||||
Der Monatsstand aus Kundenabos beträgt 776.
|
||||
|
||||
**Umfang.** Im September betrifft das **86 Berater** mit zusammen **15.936 Punkten**, die in dieser
|
||||
Kachel nicht sichtbar waren. Nur eine Anzeige — es wurde nichts falsch gebucht oder abgerechnet.
|
||||
|
||||
**Nicht aus dem Audit-Paket.** Die Null-Basis stammt aus der Umsetzung des Kundenwunschs vom 07.09.
|
||||
(K9), nicht aus der A13-Korrektur; dort wurde in dieser Methode nur der Datumsfilter ersetzt. In die
|
||||
Git-Historie ist sie erst mit `94e2b13` gekommen, weil der ganze Arbeitsstand vorher nicht committet
|
||||
war.
|
||||
|
||||
**Behebung.** Die Basis ist jetzt die im Monat aus den eigenen Kundenabos abgerechnete Punktzahl —
|
||||
dieselbe Abgrenzung wie in der Backoffice-Statistik (`SalesVolumePoints::customerAboPoints()`),
|
||||
Stornos vorzeichengetreu. Damit liest sich die Kachel wie die Prognose-Kachel auf dem Dashboard:
|
||||
Stand heute plus das, was bis zum Termin noch hereinkommt. Der Hinweistext unter der Überschrift ist
|
||||
in allen vier Sprachen nachgezogen.
|
||||
|
||||
**Bewusste Grenze.** Ein vergangener Termin zeigt jetzt den **Monatsstand**, nicht den Stand genau an
|
||||
jenem Tag — die Kachel am 05.09. enthält also auch die Lieferungen vom 10. Das ist dieselbe Lesart
|
||||
wie auf dem Dashboard, und das Unterlabel sagt „Termin liegt bereits im Ist-Umsatz". Wer stattdessen
|
||||
den echten Tagesverlauf sehen will, braucht eine Gruppierung der Buchungen nach Liefertag — das wäre
|
||||
ein eigener Umbau.
|
||||
|
||||
**Deployment:** nur Dateien, keine Migration, kein Skript. Danach `php artisan view:clear`.
|
||||
|
|
|
|||
|
|
@ -53,7 +53,7 @@ return [
|
|||
'abo_order_short' => '(Abo)',
|
||||
'points_order_legend' => 'Bestellungen: blau = eigene Bestellung · grau = Kundenbestellung · türkis mit „(Abo)" = Abo-Lieferung.',
|
||||
'customer_abo_forecast_title' => 'Punkte aus meinen Kundenabos zum 5. / 10. / 20. / 25.',
|
||||
'customer_abo_forecast_hint' => 'Kumuliert: die Punkte aller eigenen Kundenabos, deren Lieferung bis zum jeweiligen Termin dieses Monats noch aussteht. Ohne dein eigenes Abo und ohne Team.',
|
||||
'customer_abo_forecast_hint' => 'Kumuliert: die in diesem Monat aus deinen Kundenabos bereits abgerechneten Punkte plus die Lieferungen, die bis zum jeweiligen Termin noch anstehen. Ohne dein eigenes Abo und ohne Team.',
|
||||
'customer_abo_list_title' => 'Meine Kundenabos (:count)',
|
||||
'customer_abo_list_search' => 'Kundenabos durchsuchen...',
|
||||
'payline_none' => 'Noch keine Payline vorhanden (kein Provisionslevel erreicht).',
|
||||
|
|
|
|||
|
|
@ -179,7 +179,7 @@ return [
|
|||
'abo_order_short' => '(subscr.)',
|
||||
'points_order_legend' => 'Orders: blue = your own order · grey = customer order · turquoise with "(subscr.)" = subscription delivery.',
|
||||
'customer_abo_forecast_title' => 'Points from my customer subscriptions for the 5th / 10th / 20th / 25th',
|
||||
'customer_abo_forecast_hint' => 'Cumulative: the points of all your own customer subscriptions whose delivery is still due by the respective date this month. Without your own subscription and without your team.',
|
||||
'customer_abo_forecast_hint' => 'Cumulative: the points already billed from your customer subscriptions this month, plus the deliveries still due by the respective date. Without your own subscription and without your team.',
|
||||
'customer_abo_list_title' => 'My customer subscriptions (:count)',
|
||||
'customer_abo_list_search' => 'Search customer subscriptions...',
|
||||
'payline_none' => 'No payline yet (no commission level reached).',
|
||||
|
|
|
|||
|
|
@ -162,7 +162,7 @@ return [
|
|||
'abo_order_short' => '(susc.)',
|
||||
'points_order_legend' => 'Pedidos: azul = pedido propio · gris = pedido de cliente · turquesa con «(susc.)» = entrega de suscripción.',
|
||||
'customer_abo_forecast_title' => 'Puntos de mis suscripciones de clientes para el 5 / 10 / 20 / 25',
|
||||
'customer_abo_forecast_hint' => 'Acumulado: los puntos de todas tus suscripciones de clientes cuya entrega aún está pendiente hasta la fecha correspondiente de este mes. Sin tu propia suscripción y sin el equipo.',
|
||||
'customer_abo_forecast_hint' => 'Acumulado: los puntos ya facturados este mes de tus suscripciones de clientes, más las entregas que siguen pendientes hasta la fecha correspondiente. Sin tu propia suscripción y sin el equipo.',
|
||||
'customer_abo_list_title' => 'Mis suscripciones de clientes (:count)',
|
||||
'customer_abo_list_search' => 'Buscar suscripciones de clientes...',
|
||||
'payline_none' => 'Aún no hay payline (no se ha alcanzado ningún nivel de comisión).',
|
||||
|
|
|
|||
|
|
@ -53,7 +53,7 @@ return [
|
|||
'abo_order_short' => '(abo)',
|
||||
'points_order_legend' => 'Commandes : bleu = commande personnelle · gris = commande client · turquoise avec « (abo) » = livraison d’abonnement.',
|
||||
'customer_abo_forecast_title' => 'Points de mes abonnements clients pour le 5 / 10 / 20 / 25',
|
||||
'customer_abo_forecast_hint' => 'Cumulé : les points de tous tes abonnements clients dont la livraison est encore à venir jusqu’à la date concernée ce mois-ci. Sans ton propre abonnement et sans l’équipe.',
|
||||
'customer_abo_forecast_hint' => 'Cumulé : les points déjà facturés ce mois-ci sur tes abonnements clients, plus les livraisons encore à venir jusqu’à la date concernée. Sans ton propre abonnement et sans l’équipe.',
|
||||
'customer_abo_list_title' => 'Mes abonnements clients (:count)',
|
||||
'customer_abo_list_search' => 'Rechercher des abonnements clients...',
|
||||
'payline_none' => 'Pas encore de payline (aucun niveau de commission atteint).',
|
||||
|
|
|
|||
|
|
@ -491,6 +491,57 @@ it('prognostiziert die Liefertage ausschliesslich aus den eigenen Kundenabos', f
|
|||
->and($forecast['days'][3])->toMatchArray(['day' => 25, 'upcoming' => 75.0, 'total' => 75.0]);
|
||||
});
|
||||
|
||||
it('rechnet die bereits abgerechneten Kundenabo-Punkte in die persoenliche Vorschau ein', function (): void {
|
||||
// Die Kacheln beantworten "wieviel Volumenpunkte habe ich am 5./10./20./25.".
|
||||
// Ohne die bereits gebuchten Lieferungen stehen die vergangenen Termine auf
|
||||
// null, obwohl die Punkte laengst im Ist stecken.
|
||||
Carbon::setTestNow(Carbon::parse('2026-07-11'));
|
||||
|
||||
$berater = pflMakeUser();
|
||||
|
||||
$aboOrder = ShoppingOrder::forceCreate([
|
||||
'shopping_user_id' => 0, 'auth_user_id' => $berater->id, 'country_id' => 0,
|
||||
'user_shop_id' => 0, 'is_abo' => true, 'paid' => true,
|
||||
]);
|
||||
$shopOrder = ShoppingOrder::forceCreate([
|
||||
'shopping_user_id' => 0, 'auth_user_id' => $berater->id, 'country_id' => 0,
|
||||
'user_shop_id' => 0, 'is_abo' => false, 'paid' => true,
|
||||
]);
|
||||
|
||||
// Am 5. und 10. geliefert und gebucht.
|
||||
UserSalesVolume::forceCreate(['user_id' => $berater->id, 'shopping_order_id' => $aboOrder->id, 'month' => 7, 'year' => 2026, 'points' => 40, 'status' => 2]);
|
||||
UserSalesVolume::forceCreate(['user_id' => $berater->id, 'shopping_order_id' => $aboOrder->id, 'month' => 7, 'year' => 2026, 'points' => 60, 'status' => 2]);
|
||||
// Einzelbestellung eines Kunden: kein Abo, gehoert nicht in diese Kachel.
|
||||
UserSalesVolume::forceCreate(['user_id' => $berater->id, 'shopping_order_id' => $shopOrder->id, 'month' => 7, 'year' => 2026, 'points' => 500, 'status' => 2]);
|
||||
// Anderer Monat.
|
||||
UserSalesVolume::forceCreate(['user_id' => $berater->id, 'shopping_order_id' => $aboOrder->id, 'month' => 6, 'year' => 2026, 'points' => 900, 'status' => 2]);
|
||||
|
||||
// Noch offen zum 20.
|
||||
pflMakeAbo($berater->id, 'ot', '2026-07-20', 30);
|
||||
|
||||
$forecast = app(PaylineForecastService::class)->personalCustomerAboForecast($berater);
|
||||
|
||||
expect($forecast['base'])->toBe(100.0)
|
||||
->and($forecast['days'][0])->toMatchArray(['day' => 5, 'upcoming' => 0.0, 'total' => 100.0])
|
||||
->and($forecast['days'][2])->toMatchArray(['day' => 20, 'upcoming' => 30.0, 'total' => 130.0])
|
||||
->and($forecast['days'][3])->toMatchArray(['day' => 25, 'upcoming' => 30.0, 'total' => 130.0]);
|
||||
});
|
||||
|
||||
it('zieht ein Storno von der Basis der persoenlichen Vorschau ab', function (): void {
|
||||
Carbon::setTestNow(Carbon::parse('2026-07-11'));
|
||||
|
||||
$berater = pflMakeUser();
|
||||
$aboOrder = ShoppingOrder::forceCreate([
|
||||
'shopping_user_id' => 0, 'auth_user_id' => $berater->id, 'country_id' => 0,
|
||||
'user_shop_id' => 0, 'is_abo' => true, 'paid' => true,
|
||||
]);
|
||||
|
||||
UserSalesVolume::forceCreate(['user_id' => $berater->id, 'shopping_order_id' => $aboOrder->id, 'month' => 7, 'year' => 2026, 'points' => 100, 'status' => 2]);
|
||||
UserSalesVolume::forceCreate(['user_id' => $berater->id, 'shopping_order_id' => $aboOrder->id, 'month' => 7, 'year' => 2026, 'points' => -40, 'status' => 6, 'storno_origin_status' => 2]);
|
||||
|
||||
expect(app(PaylineForecastService::class)->personalCustomerAboForecast($berater)['base'])->toBe(60.0);
|
||||
});
|
||||
|
||||
it('prognostiziert auch ohne bereits erreichte Qualifikationsstufe', function (): void {
|
||||
// A08: Wer im Monat noch keine Stufe qualifiziert hat, kann die erste durch
|
||||
// kommende eigene Kundenabo-Punkte trotzdem erreichen. Vorher lieferte die
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue