Kundenabo-Seite: Vorschau taggenau, Liste mit den tatsaechlich gebuchten Punkten
Vorschau und Liste liessen sich nicht gegeneinander nachrechnen — zwei Ursachen,
beide hier behoben.
1. Die Vorschau nahm eine pauschale Monatssumme als Basis. Damit enthielt die
Kachel zum 5. auch schon die Lieferungen vom 10. Jetzt zaehlt je Termin, was
bis zu diesem Tag gebucht war, plus die bis dahin offenen Lieferungen.
Buchungen nach dem letzten Liefertag (Nachzuegler, manuelle Gutschriften)
haengen sich an die letzte Kachel, damit die den Monatsstand zeigt. Jede
Kachel weist ihre Aufschluesselung aus ("457 abgerechnet + 131 offen").
Beraterin 454 im September: 132 / 457 / 588 / 776 statt 0 / 0 / 131 / 319.
2. Die Liste zeigte nur den wiederkehrenden Abo-Inhalt. Einmal-Artikel und
sonstige Zubuchungen einer Lieferung werden nach der Ausfuehrung aus dem Abo
entfernt und tauchten deshalb nirgends mehr auf — bei Abo 266 waren das 43
Punkte, bei Abo 418 rund 39. Neue Spalte "Diesen Monat" mit den je Abo
tatsaechlich gebuchten Punkten, bei Abweichung mit dem Zusatz "+43 einmalig",
dazu eine Summenzeile. Ein nicht zuordenbarer Rest (geloeschtes Abo, Buchung
ohne Ausfuehrungsbezug) wird gesondert ausgewiesen, damit die Addition aufgeht.
Die Spalte erscheint nur ohne Monatsfilter; fuer einen vergangenen Monat waere
sie irrefuehrend.
Summe der Monatsspalte = Basis der letzten Kachel; Summe der Abo-Punkte der Abos
mit Termin 20./25. = deren offener Teil.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
cb5664a483
commit
685f7a90b9
11 changed files with 458 additions and 59 deletions
|
|
@ -91,6 +91,11 @@ class AboController extends Controller
|
|||
->orderBy('id', 'desc')
|
||||
->get();
|
||||
|
||||
// Vorschau und Monatsspalte gelten fuer den laufenden Monat; bei einem
|
||||
// Filter auf einen anderen Monat waeren sie irrefuehrend.
|
||||
$isCurrentMonthView = $selectedMonth === 0
|
||||
|| ($selectedMonth === (int) now()->month && $selectedYear === (int) now()->year);
|
||||
|
||||
return view('user.abo.index', [
|
||||
'user_abos' => $user_abos,
|
||||
'abo_payment_links' => $this->getAboPaymentLinks(),
|
||||
|
|
@ -98,9 +103,18 @@ class AboController extends Controller
|
|||
'isAdmin' => false,
|
||||
// Liefertag-Vorschau nur aus den eigenen Kundenabos
|
||||
// (Kundenwunsch 2026-09-07), und nur im laufenden Monat sinnvoll.
|
||||
'customerAboForecast' => $selectedMonth === 0 || ($selectedMonth === (int) now()->month && $selectedYear === (int) now()->year)
|
||||
'customerAboForecast' => $isCurrentMonthView
|
||||
? app(PaylineForecastService::class)->personalCustomerAboForecast(\Auth::user())
|
||||
: null,
|
||||
// Je Abo die im laufenden Monat abgerechneten Punkte — inklusive
|
||||
// Einmal-Artikel, die nach der Ausfuehrung aus dem Abo verschwinden.
|
||||
// Ohne diese Spalte laesst sich die Vorschau oben nicht nachrechnen.
|
||||
'customerAboBilled' => $isCurrentMonthView
|
||||
? app(PaylineForecastService::class)->billedCustomerAboPointsByAbo(\Auth::user(), (int) now()->month, (int) now()->year)
|
||||
: null,
|
||||
'customerAboBilledTotal' => $isCurrentMonthView
|
||||
? app(PaylineForecastService::class)->billedCustomerAboPoints(\Auth::user(), (int) now()->month, (int) now()->year)
|
||||
: null,
|
||||
'chartData' => AboHelper::getMonthlyAboCounts($baseQuery, $selectedYear, 'ot', \Auth::user()->id),
|
||||
'chartYear' => $selectedYear,
|
||||
'chartYears' => HTMLHelper::getYearRange(2026),
|
||||
|
|
|
|||
|
|
@ -164,14 +164,18 @@ 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 — 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.
|
||||
* Ohne das eigene Abo und ohne Payline, aber MIT dem, was im Monat aus den
|
||||
* Kundenabos schon abgerechnet ist — und zwar taggenau: jeder Termin zeigt
|
||||
* den Stand, den er an diesem Tag tatsaechlich hatte, plus die bis dahin noch
|
||||
* offenen Lieferungen. Damit laesst sich die Kachelreihe gegen die Liste
|
||||
* darunter nachrechnen, was mit einer pauschalen Monatssumme als Basis nicht
|
||||
* ging: ein vergangener Termin haette dann auch spaetere Lieferungen enthalten.
|
||||
*
|
||||
* @return array{base: float, days: array<int, array{day: int, date: string, upcoming: float, total: float, is_past: bool}>}
|
||||
* Anders als {@see forecastDays()} auf dem Dashboard, wo die Basis ein
|
||||
* einzelner Tagesstand (Kundenumsatz + Payline-Punkte) ist und fuer alle
|
||||
* Termine gleich bleibt.
|
||||
*
|
||||
* @return array{base: float, days: array<int, array{day: int, date: string, billed: float, upcoming: float, total: float, is_past: bool}>}
|
||||
*/
|
||||
public function personalCustomerAboForecast(User $user): array
|
||||
{
|
||||
|
|
@ -186,32 +190,90 @@ class PaylineForecastService
|
|||
->with('user_abo_items.product')
|
||||
->get();
|
||||
|
||||
$base = $this->billedCustomerAboPoints($user, (int) $today->month, (int) $today->year);
|
||||
$billedByDay = $this->billedCustomerAboPointsByDay($user, (int) $today->month, (int) $today->year);
|
||||
|
||||
return [
|
||||
'base' => $base,
|
||||
'days' => $this->cumulateByDeliveryDay($abos, $base, $today),
|
||||
'base' => (float) array_sum($billedByDay),
|
||||
'days' => $this->cumulateByDeliveryDay($abos, 0.0, $today, $billedByDay),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Im Monat bereits abgerechnete Punkte aus den EIGENEN Kundenabos des
|
||||
* Beraters — die Basis der Kundenabo-Terminvorschau.
|
||||
* Grundabfrage fuer die im Monat abgerechneten Punkte aus den EIGENEN
|
||||
* Kundenabos des Beraters.
|
||||
*
|
||||
* Nutzt dieselbe Abgrenzung wie die Backoffice-Statistik
|
||||
* ({@see SalesVolumePoints::customerAboPoints()}): Shop-/Kundenbuchungen aus
|
||||
* Abgrenzung wie in der Backoffice-Statistik
|
||||
* ({@see SalesVolumePoints::shopPredicate()}): Shop-/Kundenbuchungen aus
|
||||
* Abo-Bestellungen, Stornos vorzeichengetreu. Das eigene Abo des Beraters
|
||||
* bleibt draussen, es laeuft nicht ueber dieses Praedikat.
|
||||
* faellt heraus, es laeuft als Beraterbestellung (Status 1) nicht ueber
|
||||
* dieses Praedikat.
|
||||
*
|
||||
* Gefiltert statt als SUM(CASE ...), damit dieselbe Menge sowohl je Liefertag
|
||||
* als auch je Abo gruppiert werden kann — die Vorschau und die Liste darunter
|
||||
* muessen auf dieselbe Summe kommen.
|
||||
*/
|
||||
private function billedCustomerAboPoints(User $user, int $month, int $year): float
|
||||
private function billedCustomerAboQuery(User $user, int $month, int $year): Builder
|
||||
{
|
||||
return (float) UserSalesVolume::query()
|
||||
return 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');
|
||||
->where('shopping_orders.is_abo', 1)
|
||||
->whereRaw(SalesVolumePoints::shopPredicate());
|
||||
}
|
||||
|
||||
/**
|
||||
* Im Monat abgerechnete Kundenabo-Punkte, nach dem Kalendertag der Buchung
|
||||
* gruppiert. Gruppiert wird in PHP: es sind wenige Zeilen je Berater und
|
||||
* Monat, und eine Datumsfunktion in SQL waere zwischen MySQL und der
|
||||
* SQLite-Testdatenbank nicht dieselbe.
|
||||
*
|
||||
* @return array<int, float> Tag im Monat => Punkte
|
||||
*/
|
||||
private function billedCustomerAboPointsByDay(User $user, int $month, int $year): array
|
||||
{
|
||||
$byDay = [];
|
||||
|
||||
foreach ($this->billedCustomerAboQuery($user, $month, $year)
|
||||
->select('user_sales_volumes.points', 'user_sales_volumes.created_at')
|
||||
->get() as $row) {
|
||||
$day = $row->created_at ? (int) $row->created_at->day : 1;
|
||||
$byDay[$day] = ($byDay[$day] ?? 0.0) + (float) $row->points;
|
||||
}
|
||||
|
||||
return $byDay;
|
||||
}
|
||||
|
||||
/**
|
||||
* Im Monat abgerechnete Kundenabo-Punkte je Abo — fuer die Spalte „diesen
|
||||
* Monat abgerechnet" in der Kundenabo-Liste. Enthaelt auch Einmal-Artikel und
|
||||
* sonstige Zubuchungen der jeweiligen Lieferung; die werden nach der
|
||||
* Ausfuehrung aus dem Abo entfernt und stehen deshalb nicht mehr im
|
||||
* wiederkehrenden Aboinhalt.
|
||||
*
|
||||
* @return Collection<int, float> user_abo_id => Punkte
|
||||
*/
|
||||
public function billedCustomerAboPointsByAbo(User $user, int $month, int $year): Collection
|
||||
{
|
||||
return $this->billedCustomerAboQuery($user, $month, $year)
|
||||
->join('user_abo_orders', 'user_abo_orders.shopping_order_id', '=', 'user_sales_volumes.shopping_order_id')
|
||||
->groupBy('user_abo_orders.user_abo_id')
|
||||
->select('user_abo_orders.user_abo_id')
|
||||
->selectRaw('COALESCE(SUM(user_sales_volumes.points), 0) as points')
|
||||
->pluck('points', 'user_abo_id')
|
||||
->map(fn ($points) => (float) $points);
|
||||
}
|
||||
|
||||
/**
|
||||
* Monatssumme der abgerechneten Kundenabo-Punkte. Massgeblich fuer die
|
||||
* Vorschau; {@see billedCustomerAboPointsByAbo()} kann davon abweichen, wenn
|
||||
* eine Buchung keiner Abo-Ausfuehrung zugeordnet ist — die Liste weist die
|
||||
* Differenz dann gesondert aus, damit die Addition aufgeht.
|
||||
*/
|
||||
public function billedCustomerAboPoints(User $user, int $month, int $year): float
|
||||
{
|
||||
return (float) array_sum($this->billedCustomerAboPointsByDay($user, $month, $year));
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -223,7 +285,7 @@ class PaylineForecastService
|
|||
* @param Collection<int, UserAbo> $abos
|
||||
* @return array<int, array{day: int, date: string, upcoming: float, total: float, is_past: bool}>
|
||||
*/
|
||||
private function cumulateByDeliveryDay($abos, float $basePoints, Carbon $today): array
|
||||
private function cumulateByDeliveryDay($abos, float $basePoints, Carbon $today, ?array $billedByDay = null): array
|
||||
{
|
||||
$pointsByDay = [];
|
||||
foreach ($abos as $abo) {
|
||||
|
|
@ -231,8 +293,11 @@ class PaylineForecastService
|
|||
$pointsByDay[$day] = ($pointsByDay[$day] ?? 0.0) + (float) $abo->getTotalPoints();
|
||||
}
|
||||
|
||||
$deliveryDays = UserAbo::$aboDeliveryDays;
|
||||
$lastDeliveryDay = (int) max($deliveryDays);
|
||||
|
||||
$days = [];
|
||||
foreach (UserAbo::$aboDeliveryDays as $deliveryDay) {
|
||||
foreach ($deliveryDays as $deliveryDay) {
|
||||
$upcoming = 0.0;
|
||||
foreach ($pointsByDay as $day => $points) {
|
||||
if ($day <= $deliveryDay) {
|
||||
|
|
@ -240,11 +305,26 @@ class PaylineForecastService
|
|||
}
|
||||
}
|
||||
|
||||
if ($billedByDay === null) {
|
||||
$base = $basePoints;
|
||||
} else {
|
||||
$base = 0.0;
|
||||
foreach ($billedByDay as $day => $points) {
|
||||
// Buchungen nach dem letzten Liefertag (Nachzuegler, manuelle
|
||||
// Gutschriften) haengen sich an den letzten Termin, damit die
|
||||
// letzte Kachel immer den Monatsstand zeigt.
|
||||
if ($day <= $deliveryDay || ($deliveryDay === $lastDeliveryDay && $day > $lastDeliveryDay)) {
|
||||
$base += $points;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$days[] = [
|
||||
'day' => $deliveryDay,
|
||||
'date' => $today->copy()->startOfMonth()->addDays($deliveryDay - 1)->format(\Util::formatDateDB()),
|
||||
'billed' => $base,
|
||||
'upcoming' => $upcoming,
|
||||
'total' => $basePoints + $upcoming,
|
||||
'total' => $base + $upcoming,
|
||||
'is_past' => $deliveryDay < (int) $today->day,
|
||||
];
|
||||
}
|
||||
|
|
|
|||
|
|
@ -110,16 +110,36 @@ Kachel nicht sichtbar waren. Nur eine Anzeige — es wurde nichts falsch gebucht
|
|||
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.
|
||||
**Behebung, Teil 1 — die Vorschau rechnet taggenau.** Basis jeder Kachel sind die bis zu diesem
|
||||
Termin aus den eigenen Kundenabos abgerechneten Punkte, dazu die bis dahin noch offenen Lieferungen.
|
||||
Abgrenzung wie in der Backoffice-Statistik (`SalesVolumePoints::shopPredicate()`), Stornos
|
||||
vorzeichengetreu; das eigene Abo des Beraters bleibt draußen. Buchungen nach dem 25. (Nachzügler,
|
||||
manuelle Gutschriften) hängen sich an die letzte Kachel, damit die immer den Monatsstand zeigt.
|
||||
|
||||
**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.
|
||||
Für Beraterin 454 ergibt das **132 / 457 / 588 / 776** statt vorher 0 / 0 / 131 / 319. Jede Kachel
|
||||
zeigt jetzt zusätzlich ihre Aufschlüsselung („457 abgerechnet + 131 offen"), damit man sie gegen die
|
||||
Liste darunter nachrechnen kann.
|
||||
|
||||
**Behebung, Teil 2 — die Liste zeigt, was tatsächlich gebucht wurde.** Die Spalte „Punkte" enthielt
|
||||
nur den *wiederkehrenden* Abo-Inhalt. Was bei einer Lieferung zusätzlich dabei war — Einmal-Artikel
|
||||
und sonstige Zubuchungen — wird nach der Ausführung aus dem Abo entfernt und tauchte deshalb nirgends
|
||||
mehr auf. Bei Abo 266 waren das 43 Punkte (CBD Öl 27 + MSM PURE 16), bei Abo 418 rund 39.
|
||||
|
||||
Neu daneben: **„Diesen Monat"** mit den je Abo tatsächlich gebuchten Punkten, bei Abweichung mit
|
||||
einem Zusatz „+43 einmalig". Dazu eine Summenzeile unter der Tabelle. Kann eine Monatsbuchung keiner
|
||||
gelisteten Abo-Zeile zugeordnet werden — gelöschtes Abo, Buchung ohne Ausführungsbezug —, weist die
|
||||
Summenzeile den Rest gesondert aus, damit die Addition aufgeht.
|
||||
|
||||
Damit stimmt die Rechnung durchgängig:
|
||||
|
||||
```
|
||||
Summe Spalte "Diesen Monat" = 457 → Basis der letzten Kachel
|
||||
Summe "Abo-Punkte" der Abos mit Termin 20./25. = 319 → offener Teil der letzten Kachel
|
||||
---
|
||||
letzte Kachel 776
|
||||
```
|
||||
|
||||
Die Monatsspalte erscheint nur, solange kein anderer Monat gefiltert ist; für einen vergangenen Monat
|
||||
wäre sie irreführend.
|
||||
|
||||
**Deployment:** nur Dateien, keine Migration, kein Skript. Danach `php artisan view:clear`.
|
||||
|
|
|
|||
|
|
@ -53,7 +53,17 @@ 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 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_forecast_hint' => 'Taggenau: je Termin die bis dahin aus deinen Kundenabos abgerechneten Punkte plus die Lieferungen, die bis dahin noch anstehen. Ohne dein eigenes Abo und ohne Team.',
|
||||
'customer_abo_forecast_split' => ':billed abgerechnet + :upcoming offen',
|
||||
'customer_abo_points_recurring' => 'Abo-Punkte',
|
||||
'customer_abo_points_recurring_hint' => 'Punkte des wiederkehrenden Abo-Inhalts — das, was bei der nächsten Lieferung zählt.',
|
||||
'customer_abo_points_billed' => 'Diesen Monat',
|
||||
'customer_abo_points_billed_hint' => 'Punkte, die dieses Abo in diesem Monat tatsächlich gebucht hat — einschließlich Einmal-Artikel und sonstiger Zubuchungen der Lieferung.',
|
||||
'customer_abo_points_extra' => 'einmalig',
|
||||
'customer_abo_points_extra_hint' => 'Differenz zum wiederkehrenden Abo-Inhalt: Einmal-Artikel oder sonstige Zubuchungen, die nach der Lieferung wieder aus dem Abo entfernt wurden.',
|
||||
'customer_abo_list_sum' => 'Summe',
|
||||
'customer_abo_list_sum_rest' => 'Weitere Abo-Buchungen des Monats (gelöschte Abos, Buchungen ohne Ausführungsbezug)',
|
||||
'customer_abo_list_sum_month' => 'Monat gesamt',
|
||||
'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,17 @@ 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 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_forecast_hint' => 'Day by day: for each date, the points billed from your customer subscriptions up to that day, plus the deliveries still due by then. Without your own subscription and without your team.',
|
||||
'customer_abo_forecast_split' => ':billed billed + :upcoming due',
|
||||
'customer_abo_points_recurring' => 'Subscription points',
|
||||
'customer_abo_points_recurring_hint' => 'Points of the recurring subscription content — what counts on the next delivery.',
|
||||
'customer_abo_points_billed' => 'This month',
|
||||
'customer_abo_points_billed_hint' => 'Points this subscription actually booked this month — including one-off items and other additions to the delivery.',
|
||||
'customer_abo_points_extra' => 'one-off',
|
||||
'customer_abo_points_extra_hint' => 'Difference from the recurring subscription content: one-off items or other additions that were removed from the subscription after delivery.',
|
||||
'customer_abo_list_sum' => 'Total',
|
||||
'customer_abo_list_sum_rest' => 'Other subscription bookings this month (deleted subscriptions, bookings without a delivery reference)',
|
||||
'customer_abo_list_sum_month' => 'Month total',
|
||||
'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,17 @@ 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 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_forecast_hint' => 'Día a día: en cada fecha, los puntos facturados de tus suscripciones de clientes hasta ese día, más las entregas que siguen pendientes. Sin tu propia suscripción y sin el equipo.',
|
||||
'customer_abo_forecast_split' => ':billed facturados + :upcoming pendientes',
|
||||
'customer_abo_points_recurring' => 'Puntos de la suscripción',
|
||||
'customer_abo_points_recurring_hint' => 'Puntos del contenido recurrente de la suscripción: lo que cuenta en la próxima entrega.',
|
||||
'customer_abo_points_billed' => 'Este mes',
|
||||
'customer_abo_points_billed_hint' => 'Puntos que esta suscripción ha facturado realmente este mes, incluidos los artículos puntuales y otras adiciones a la entrega.',
|
||||
'customer_abo_points_extra' => 'puntual',
|
||||
'customer_abo_points_extra_hint' => 'Diferencia con el contenido recurrente de la suscripción: artículos puntuales u otras adiciones que se retiraron de la suscripción tras la entrega.',
|
||||
'customer_abo_list_sum' => 'Total',
|
||||
'customer_abo_list_sum_rest' => 'Otros movimientos de suscripción del mes (suscripciones eliminadas, movimientos sin referencia de entrega)',
|
||||
'customer_abo_list_sum_month' => 'Total del mes',
|
||||
'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,17 @@ 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 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_forecast_hint' => 'Jour par jour : à chaque date, les points facturés sur tes abonnements clients jusqu’à ce jour, plus les livraisons encore à venir. Sans ton propre abonnement et sans l’équipe.',
|
||||
'customer_abo_forecast_split' => ':billed facturés + :upcoming à venir',
|
||||
'customer_abo_points_recurring' => 'Points de l’abonnement',
|
||||
'customer_abo_points_recurring_hint' => 'Points du contenu récurrent de l’abonnement — ce qui compte à la prochaine livraison.',
|
||||
'customer_abo_points_billed' => 'Ce mois-ci',
|
||||
'customer_abo_points_billed_hint' => 'Points réellement facturés par cet abonnement ce mois-ci, y compris les articles ponctuels et autres ajouts à la livraison.',
|
||||
'customer_abo_points_extra' => 'ponctuel',
|
||||
'customer_abo_points_extra_hint' => 'Écart par rapport au contenu récurrent de l’abonnement : articles ponctuels ou autres ajouts retirés de l’abonnement après la livraison.',
|
||||
'customer_abo_list_sum' => 'Total',
|
||||
'customer_abo_list_sum_rest' => 'Autres écritures d’abonnement du mois (abonnements supprimés, écritures sans référence de livraison)',
|
||||
'customer_abo_list_sum_month' => 'Total du mois',
|
||||
'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).',
|
||||
|
|
|
|||
|
|
@ -3,7 +3,18 @@
|
|||
nach ihrer Vorgabe: Kunde, Liefertag, Abo-Punkte, nächste Ausführung,
|
||||
Startdatum, Ausführungen.
|
||||
|
||||
Erwartet $listAbos (Collection<UserAbo>) und $customerAboForecast|null. --}}
|
||||
Erwartet $listAbos (Collection<UserAbo>), $customerAboForecast|null sowie
|
||||
$customerAboBilled|null (user_abo_id => im Monat abgerechnete Punkte) und
|
||||
$customerAboBilledTotal|null. Die beiden letzten sind null, sobald auf einen
|
||||
anderen Monat gefiltert wird — dann waere eine Monatsspalte irrefuehrend. --}}
|
||||
@php
|
||||
// Defensiv: die Teilansicht wird auch aus Kontexten eingebunden, die keine
|
||||
// Monatswerte kennen (gefilterter Monat, Tests).
|
||||
$customerAboForecast = $customerAboForecast ?? null;
|
||||
$customerAboBilled = $customerAboBilled ?? null;
|
||||
$customerAboBilledTotal = $customerAboBilledTotal ?? null;
|
||||
@endphp
|
||||
|
||||
@if (! empty($customerAboForecast['days']))
|
||||
<div class="card mb-4">
|
||||
<div class="card-body">
|
||||
|
|
@ -25,6 +36,16 @@
|
|||
<div class="small {{ $forecastDay['upcoming'] > 0 ? 'text-success' : 'text-muted' }}">
|
||||
{{ $forecastDay['is_past'] && $forecastDay['upcoming'] <= 0 ? __('home.forecast_done') : __('navigation.points') }}
|
||||
</div>
|
||||
@if ($forecastDay['upcoming'] != 0.0)
|
||||
{{-- Aufschluesselung, damit die Kachel gegen die Liste
|
||||
darunter nachrechenbar ist. --}}
|
||||
<div class="small text-muted mt-1">
|
||||
{{ __('abo.customer_abo_forecast_split', [
|
||||
'billed' => \App\Services\Util::formatNumber($forecastDay['billed'] ?? 0.0),
|
||||
'upcoming' => \App\Services\Util::formatNumber($forecastDay['upcoming']),
|
||||
]) }}
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -57,7 +78,10 @@
|
|||
<tr>
|
||||
<th data-sortable="true"><span class="sortable-label">{{ __('tables.customer') }} <i class="ion ion-ios-swap sort-icon"></i></span></th>
|
||||
<th data-sortable="true" data-sort-type="number"><span class="sortable-label">{{ __('tables.abo_delivery_day') }} <i class="ion ion-ios-swap sort-icon"></i></span></th>
|
||||
<th class="text-right" data-sortable="true" data-sort-type="number"><span class="sortable-label justify-content-end">{{ __('navigation.points') }} <i class="ion ion-ios-swap sort-icon"></i></span></th>
|
||||
<th class="text-right" data-sortable="true" data-sort-type="number" title="{{ __('abo.customer_abo_points_recurring_hint') }}"><span class="sortable-label justify-content-end">{{ __('abo.customer_abo_points_recurring') }} <i class="ion ion-ios-swap sort-icon"></i></span></th>
|
||||
@if ($customerAboBilled !== null)
|
||||
<th class="text-right" data-sortable="true" data-sort-type="number" title="{{ __('abo.customer_abo_points_billed_hint') }}"><span class="sortable-label justify-content-end">{{ __('abo.customer_abo_points_billed') }} <i class="ion ion-ios-swap sort-icon"></i></span></th>
|
||||
@endif
|
||||
<th data-sortable="true" data-sort-type="date"><span class="sortable-label">{{ __('tables.next_date') }} <i class="ion ion-ios-swap sort-icon"></i></span></th>
|
||||
<th data-sortable="true" data-sort-type="date"><span class="sortable-label">{{ __('tables.start_date') }} <i class="ion ion-ios-swap sort-icon"></i></span></th>
|
||||
<th class="text-right" data-sortable="true" data-sort-type="number"><span class="sortable-label justify-content-end">{{ __('tables.abo_delivery') }} <i class="ion ion-ios-swap sort-icon"></i></span></th>
|
||||
|
|
@ -65,6 +89,17 @@
|
|||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
@php
|
||||
// Fuer die Summenzeile: was steht in der Liste, und bleibt vom
|
||||
// Monatswert etwas uebrig, das keiner gelisteten Abo-Zeile zugeordnet
|
||||
// werden kann (geloeschtes Abo, Buchung ohne Ausfuehrungsbezug)?
|
||||
$billedShown = $customerAboBilled === null
|
||||
? 0.0
|
||||
: (float) $listAbos->sum(fn ($abo) => (float) ($customerAboBilled[$abo->id] ?? 0.0));
|
||||
$billedRest = $customerAboBilled === null
|
||||
? 0.0
|
||||
: round((float) $customerAboBilledTotal - $billedShown, 2);
|
||||
@endphp
|
||||
<tbody>
|
||||
@foreach ($listAbos as $listAbo)
|
||||
<tr>
|
||||
|
|
@ -87,6 +122,26 @@
|
|||
</td>
|
||||
<td data-sort-value="{{ (int) $listAbo->abo_interval }}">{{ \App\Services\HTMLHelper::getAboStrLang($listAbo->abo_interval) }}</td>
|
||||
<td class="text-right font-weight-bold text-primary" data-sort-value="{{ $listAbo->getTotalPoints() }}">{{ $listAbo->getFormattedTotalPoints() }}</td>
|
||||
@if ($customerAboBilled !== null)
|
||||
@php
|
||||
$billedPoints = (float) ($customerAboBilled[$listAbo->id] ?? 0.0);
|
||||
// Differenz zum wiederkehrenden Aboinhalt: Einmal-Artikel und
|
||||
// sonstige Zubuchungen der Lieferung, die danach wieder aus
|
||||
// dem Abo entfernt werden. Nur ausweisen, wenn das Abo diesen
|
||||
// Monat ueberhaupt geliefert hat.
|
||||
$extraPoints = $billedPoints != 0.0
|
||||
? round($billedPoints - (float) $listAbo->getTotalPoints(), 2)
|
||||
: 0.0;
|
||||
@endphp
|
||||
<td class="text-right" data-sort-value="{{ $billedPoints }}">
|
||||
<span class="font-weight-bold">{{ \App\Services\Util::formatNumber($billedPoints) }}</span>
|
||||
@if ($extraPoints != 0.0)
|
||||
<div class="small text-muted" title="{{ __('abo.customer_abo_points_extra_hint') }}">
|
||||
{{ $extraPoints > 0 ? '+' : '' }}{{ \App\Services\Util::formatNumber($extraPoints) }} {{ __('abo.customer_abo_points_extra') }}
|
||||
</div>
|
||||
@endif
|
||||
</td>
|
||||
@endif
|
||||
<td data-sort-value="{{ $listAbo->getRawOriginal('next_date') }}">{{ $listAbo->next_date }}</td>
|
||||
<td data-sort-value="{{ $listAbo->getRawOriginal('start_date') }}">{{ $listAbo->start_date }}</td>
|
||||
<td class="text-right" data-sort-value="{{ $listAbo->getCountPaidOrders() }}">{{ $listAbo->getCountPaidOrders() }}</td>
|
||||
|
|
@ -99,6 +154,28 @@
|
|||
</tr>
|
||||
@endforeach
|
||||
</tbody>
|
||||
<tfoot>
|
||||
<tr class="font-weight-bold bg-light">
|
||||
<td colspan="2">{{ __('abo.customer_abo_list_sum') }}</td>
|
||||
<td class="text-right">{{ \App\Services\Util::formatNumber($listAbos->sum(fn ($abo) => (float) $abo->getTotalPoints())) }}</td>
|
||||
@if ($customerAboBilled !== null)
|
||||
<td class="text-right">{{ \App\Services\Util::formatNumber($billedShown) }}</td>
|
||||
@endif
|
||||
<td colspan="5"></td>
|
||||
</tr>
|
||||
@if ($customerAboBilled !== null && $billedRest != 0.0)
|
||||
<tr class="small text-muted bg-light">
|
||||
<td colspan="3">{{ __('abo.customer_abo_list_sum_rest') }}</td>
|
||||
<td class="text-right">{{ \App\Services\Util::formatNumber($billedRest) }}</td>
|
||||
<td colspan="5"></td>
|
||||
</tr>
|
||||
<tr class="font-weight-bold bg-light">
|
||||
<td colspan="3">{{ __('abo.customer_abo_list_sum_month') }}</td>
|
||||
<td class="text-right">{{ \App\Services\Util::formatNumber((float) $customerAboBilledTotal) }}</td>
|
||||
<td colspan="5"></td>
|
||||
</tr>
|
||||
@endif
|
||||
</tfoot>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -51,7 +51,9 @@
|
|||
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
@php($abo_payment_links = $abo_payment_links ?? collect())
|
||||
@php
|
||||
$abo_payment_links = $abo_payment_links ?? collect();
|
||||
@endphp
|
||||
|
||||
@if($view === 'ot' && $abo_payment_links->isNotEmpty())
|
||||
@foreach($abo_payment_links as $paymentLink)
|
||||
|
|
@ -140,7 +142,19 @@
|
|||
</div>
|
||||
@endif
|
||||
|
||||
@foreach($user_abos as $user_abo)
|
||||
@php
|
||||
// Kundenabos laufen ueber die sortierbare Liste; als Karte bleiben nur
|
||||
// die Ausnahmefaelle mit offener Erstzahlung, weil dort Aktionsbuttons
|
||||
// haengen (Kundenwunsch 2026-09-07).
|
||||
$cardAbos = $view === 'ot'
|
||||
? collect($user_abos)->filter(fn ($abo) => $abo->isPendingInitialPayment())
|
||||
: collect($user_abos);
|
||||
$listAbos = $view === 'ot'
|
||||
? collect($user_abos)->reject(fn ($abo) => $abo->isPendingInitialPayment())->values()
|
||||
: collect();
|
||||
@endphp
|
||||
|
||||
@foreach($cardAbos as $user_abo)
|
||||
<div class="card mb-4">
|
||||
@if($user_abo->isPendingInitialPayment())
|
||||
<div class="card-body pb-0">
|
||||
|
|
@ -262,6 +276,15 @@
|
|||
@endif
|
||||
</div>
|
||||
@endforeach
|
||||
|
||||
@if($view === 'ot' && $listAbos->isNotEmpty())
|
||||
@include('user.abo._customer_abo_list', [
|
||||
'listAbos' => $listAbos,
|
||||
'customerAboForecast' => $customerAboForecast ?? null,
|
||||
'customerAboBilled' => $customerAboBilled ?? null,
|
||||
'customerAboBilledTotal' => $customerAboBilledTotal ?? null,
|
||||
])
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
||||
|
|
|
|||
|
|
@ -53,10 +53,10 @@ it('rendert die Kundenabo-Liste mit allen gewuenschten Spalten', function (): vo
|
|||
$html = view('user.abo._customer_abo_list', [
|
||||
'listAbos' => collect([$abo]),
|
||||
'customerAboForecast' => ['base' => 0.0, 'days' => [
|
||||
['day' => 5, 'date' => '05.09.2026', 'upcoming' => 0.0, 'total' => 0.0, 'is_past' => true],
|
||||
['day' => 10, 'date' => '10.09.2026', 'upcoming' => 36.0, 'total' => 36.0, 'is_past' => false],
|
||||
['day' => 20, 'date' => '20.09.2026', 'upcoming' => 81.0, 'total' => 81.0, 'is_past' => false],
|
||||
['day' => 25, 'date' => '25.09.2026', 'upcoming' => 81.0, 'total' => 81.0, 'is_past' => false],
|
||||
['day' => 5, 'date' => '05.09.2026', 'billed' => 0.0, 'upcoming' => 0.0, 'total' => 0.0, 'is_past' => true],
|
||||
['day' => 10, 'date' => '10.09.2026', 'billed' => 0.0, 'upcoming' => 36.0, 'total' => 36.0, 'is_past' => false],
|
||||
['day' => 20, 'date' => '20.09.2026', 'billed' => 0.0, 'upcoming' => 81.0, 'total' => 81.0, 'is_past' => false],
|
||||
['day' => 25, 'date' => '25.09.2026', 'billed' => 0.0, 'upcoming' => 81.0, 'total' => 81.0, 'is_past' => false],
|
||||
]],
|
||||
])->render();
|
||||
|
||||
|
|
@ -135,3 +135,82 @@ it('gibt unformatierte Sortierwerte aus, unabhaengig von der Sprache (A14)', fun
|
|||
->toContain('data-sort-value="2026-04-05"') // Startdatum
|
||||
->toContain('data-sort-value="20"'); // Liefertag
|
||||
})->with(['de', 'en']);
|
||||
|
||||
it('zeigt die im Monat abgerechneten Punkte je Abo samt Summenzeile', function (): void {
|
||||
// Die Liste muss sich gegen die Vorschau darueber nachrechnen lassen: Summe
|
||||
// der Monatsspalte = Basis der letzten Kachel.
|
||||
$user = User::forceCreate([
|
||||
'email' => 'cal-billed@example.com',
|
||||
'password' => bcrypt('secret'),
|
||||
'lang' => 'de',
|
||||
'm_level' => 1,
|
||||
'payment_account' => now()->addYear(),
|
||||
]);
|
||||
|
||||
$geliefert = calMakeAbo($user->id, '2026-04-05', '2026-10-05');
|
||||
$offen = calMakeAbo($user->id, '2026-05-20', '2026-09-20', 20);
|
||||
|
||||
$html = view('user.abo._customer_abo_list', [
|
||||
'listAbos' => collect([$geliefert, $offen]),
|
||||
'customerAboForecast' => null,
|
||||
// 80 gebucht, aber nur 37 wiederkehrend → 43 einmalig.
|
||||
'customerAboBilled' => collect([$geliefert->id => 80.0]),
|
||||
'customerAboBilledTotal' => 80.0,
|
||||
])->render();
|
||||
|
||||
expect($html)
|
||||
->toContain(e(__('abo.customer_abo_points_billed')))
|
||||
->toContain('data-sort-value="80"')
|
||||
->toContain(e(__('abo.customer_abo_points_extra')))
|
||||
->toContain(e(__('abo.customer_abo_list_sum')))
|
||||
// Kein Rest → weder Rest- noch Monatszeile
|
||||
->not->toContain(e(__('abo.customer_abo_list_sum_rest')));
|
||||
});
|
||||
|
||||
it('weist einen nicht zuordenbaren Rest in der Summenzeile aus', function (): void {
|
||||
// Ein im Monat geliefertes, inzwischen geloeschtes Abo steht nicht mehr in der
|
||||
// Liste. Ohne die Restzeile ginge die Addition nicht auf.
|
||||
$user = User::forceCreate([
|
||||
'email' => 'cal-rest@example.com',
|
||||
'password' => bcrypt('secret'),
|
||||
'lang' => 'de',
|
||||
'm_level' => 1,
|
||||
'payment_account' => now()->addYear(),
|
||||
]);
|
||||
|
||||
$abo = calMakeAbo($user->id, '2026-04-05', '2026-10-05');
|
||||
|
||||
$html = view('user.abo._customer_abo_list', [
|
||||
'listAbos' => collect([$abo]),
|
||||
'customerAboForecast' => null,
|
||||
'customerAboBilled' => collect([$abo->id => 50.0]),
|
||||
'customerAboBilledTotal' => 78.0,
|
||||
])->render();
|
||||
|
||||
expect($html)
|
||||
->toContain(e(__('abo.customer_abo_list_sum_rest')))
|
||||
->toContain(e(__('abo.customer_abo_list_sum_month')))
|
||||
->toContain('28,00') // Rest
|
||||
->toContain('78,00'); // Monat gesamt
|
||||
});
|
||||
|
||||
it('blendet die Monatsspalte aus, wenn auf einen anderen Monat gefiltert wird', function (): void {
|
||||
$user = User::forceCreate([
|
||||
'email' => 'cal-nobilled@example.com',
|
||||
'password' => bcrypt('secret'),
|
||||
'lang' => 'de',
|
||||
'm_level' => 1,
|
||||
'payment_account' => now()->addYear(),
|
||||
]);
|
||||
|
||||
$html = view('user.abo._customer_abo_list', [
|
||||
'listAbos' => collect([calMakeAbo($user->id, '2026-04-05', '2026-10-05')]),
|
||||
'customerAboForecast' => null,
|
||||
'customerAboBilled' => null,
|
||||
'customerAboBilledTotal' => null,
|
||||
])->render();
|
||||
|
||||
expect($html)
|
||||
->not->toContain(e(__('abo.customer_abo_points_billed')))
|
||||
->toContain(e(__('abo.customer_abo_list_sum')));
|
||||
});
|
||||
|
|
|
|||
|
|
@ -89,6 +89,19 @@ function pflMakeAbo(int $userId, string $isFor, string $nextDate, int $points, i
|
|||
return $abo->fresh();
|
||||
}
|
||||
|
||||
function pflBookAboPoints(int $userId, int $orderId, float $points, string $createdAt, int $month = 7, int $year = 2026): UserSalesVolume
|
||||
{
|
||||
return UserSalesVolume::forceCreate([
|
||||
'user_id' => $userId,
|
||||
'shopping_order_id' => $orderId,
|
||||
'month' => $month,
|
||||
'year' => $year,
|
||||
'points' => $points,
|
||||
'status' => 2,
|
||||
'created_at' => $createdAt,
|
||||
]);
|
||||
}
|
||||
|
||||
function pflSeedContext(User $user, array $memberIds, int $paylines = 2, ?int $month = null, ?int $year = null): void
|
||||
{
|
||||
$month ??= (int) now()->month;
|
||||
|
|
@ -491,10 +504,10 @@ 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 {
|
||||
it('rechnet die bereits abgerechneten Kundenabo-Punkte taggenau 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.
|
||||
// Jeder Termin zeigt den Stand AN DIESEM TAG, sonst enthaelt die Kachel zum 5.
|
||||
// auch schon die Lieferungen vom 10. und laesst sich nicht nachrechnen.
|
||||
Carbon::setTestNow(Carbon::parse('2026-07-11'));
|
||||
|
||||
$berater = pflMakeUser();
|
||||
|
|
@ -508,13 +521,12 @@ it('rechnet die bereits abgerechneten Kundenabo-Punkte in die persoenliche Vorsc
|
|||
'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]);
|
||||
pflBookAboPoints($berater->id, $aboOrder->id, 40, '2026-07-05 04:10:00');
|
||||
pflBookAboPoints($berater->id, $aboOrder->id, 60, '2026-07-10 04:10:00');
|
||||
// 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]);
|
||||
pflBookAboPoints($berater->id, $shopOrder->id, 500, '2026-07-10 04:10:00');
|
||||
// Anderer Monat.
|
||||
UserSalesVolume::forceCreate(['user_id' => $berater->id, 'shopping_order_id' => $aboOrder->id, 'month' => 6, 'year' => 2026, 'points' => 900, 'status' => 2]);
|
||||
pflBookAboPoints($berater->id, $aboOrder->id, 900, '2026-06-10 04:10:00', 6, 2026);
|
||||
|
||||
// Noch offen zum 20.
|
||||
pflMakeAbo($berater->id, 'ot', '2026-07-20', 30);
|
||||
|
|
@ -522,9 +534,31 @@ it('rechnet die bereits abgerechneten Kundenabo-Punkte in die persoenliche Vorsc
|
|||
$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]);
|
||||
->and($forecast['days'][0])->toMatchArray(['day' => 5, 'billed' => 40.0, 'upcoming' => 0.0, 'total' => 40.0])
|
||||
->and($forecast['days'][1])->toMatchArray(['day' => 10, 'billed' => 100.0, 'upcoming' => 0.0, 'total' => 100.0])
|
||||
->and($forecast['days'][2])->toMatchArray(['day' => 20, 'billed' => 100.0, 'upcoming' => 30.0, 'total' => 130.0])
|
||||
->and($forecast['days'][3])->toMatchArray(['day' => 25, 'billed' => 100.0, 'upcoming' => 30.0, 'total' => 130.0]);
|
||||
});
|
||||
|
||||
it('haengt Buchungen nach dem letzten Liefertag an die letzte Kachel', function (): void {
|
||||
// Nachzuegler und manuelle Gutschriften duerfen nicht aus der Monatssumme
|
||||
// fallen — sonst zeigt die letzte Kachel weniger als den Monatsstand.
|
||||
Carbon::setTestNow(Carbon::parse('2026-07-29'));
|
||||
|
||||
$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,
|
||||
]);
|
||||
|
||||
pflBookAboPoints($berater->id, $aboOrder->id, 10, '2026-07-05 04:10:00');
|
||||
pflBookAboPoints($berater->id, $aboOrder->id, 25, '2026-07-28 11:00:00');
|
||||
|
||||
$forecast = app(PaylineForecastService::class)->personalCustomerAboForecast($berater);
|
||||
|
||||
expect($forecast['days'][0]['total'])->toBe(10.0)
|
||||
->and($forecast['days'][3]['total'])->toBe(35.0)
|
||||
->and($forecast['base'])->toBe(35.0);
|
||||
});
|
||||
|
||||
it('zieht ein Storno von der Basis der persoenlichen Vorschau ab', function (): void {
|
||||
|
|
@ -536,10 +570,42 @@ it('zieht ein Storno von der Basis der persoenlichen Vorschau ab', function ():
|
|||
'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]);
|
||||
pflBookAboPoints($berater->id, $aboOrder->id, 100, '2026-07-05 04:10:00');
|
||||
UserSalesVolume::forceCreate([
|
||||
'user_id' => $berater->id, 'shopping_order_id' => $aboOrder->id, 'month' => 7, 'year' => 2026,
|
||||
'points' => -40, 'status' => 6, 'storno_origin_status' => 2, 'created_at' => '2026-07-08 09:00:00',
|
||||
]);
|
||||
|
||||
expect(app(PaylineForecastService::class)->personalCustomerAboForecast($berater)['base'])->toBe(60.0);
|
||||
$forecast = app(PaylineForecastService::class)->personalCustomerAboForecast($berater);
|
||||
|
||||
expect($forecast['base'])->toBe(60.0)
|
||||
->and($forecast['days'][1]['billed'])->toBe(60.0);
|
||||
});
|
||||
|
||||
it('weist die abgerechneten Punkte je Abo aus, inklusive Einmal-Artikel', function (): void {
|
||||
// Die Liste unter der Vorschau muss auf dieselbe Summe kommen. Einmal-Artikel
|
||||
// werden nach der Ausfuehrung aus dem Abo entfernt, stehen also nicht mehr im
|
||||
// wiederkehrenden Aboinhalt — gebucht wurden sie trotzdem.
|
||||
Carbon::setTestNow(Carbon::parse('2026-07-11'));
|
||||
|
||||
$berater = pflMakeUser();
|
||||
$abo = pflMakeAbo($berater->id, 'ot', '2026-08-05', 37);
|
||||
|
||||
$aboOrder = ShoppingOrder::forceCreate([
|
||||
'shopping_user_id' => 0, 'auth_user_id' => $berater->id, 'country_id' => 0,
|
||||
'user_shop_id' => 0, 'is_abo' => true, 'paid' => true,
|
||||
]);
|
||||
$abo->user_abo_orders()->create(['shopping_order_id' => $aboOrder->id, 'status' => 1, 'paid' => true]);
|
||||
|
||||
// Geliefert wurden 80: 37 wiederkehrend + 43 einmalig.
|
||||
pflBookAboPoints($berater->id, $aboOrder->id, 80, '2026-07-05 04:10:00');
|
||||
|
||||
$svc = app(PaylineForecastService::class);
|
||||
$byAbo = $svc->billedCustomerAboPointsByAbo($berater, 7, 2026);
|
||||
|
||||
expect($byAbo[$abo->id])->toBe(80.0)
|
||||
->and($byAbo->sum())->toBe(80.0)
|
||||
->and($svc->billedCustomerAboPoints($berater, 7, 2026))->toBe(80.0);
|
||||
});
|
||||
|
||||
it('prognostiziert auch ohne bereits erreichte Qualifikationsstufe', function (): void {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue