Statistik: Stornos mindern die richtige Punktekategorie
Ein Storno einer Shop-Bestellung wurde als Eigenpunkt gebucht und liess damit die Shop-Provisionsbasis ungemindert. Betroffen sind 7 Faelle aus April bis August 2026 (178,82 EUR Differenz in der Provisionsbasis, gegen user_businesses geprueft). Ursache: `status_turnover` beschreibt nur, wohin der UMSATZ gehoert. Die Punkte folgen einer anderen Regel — eine Gutschrift (Status 4) bucht ihre Punkte immer auf KP/TP, auch wenn ihr Umsatz dem Shop zugeordnet ist. Die Gegenbuchung kann diese Aufteilung ohne die Ursprungsart nicht spiegeln, deshalb haelt `storno_origin_status` sie jetzt auf der Stornozeile fest. NULL heisst "Altzeile ohne Angabe" und faellt auf das bisherige Verhalten zurueck — abgeschlossene Monate aendern sich dadurch nicht. Stornoregel (Kundenentscheidung): Ein Storno mindert nur den Monat der Ursprungsbestellung. Ist der Monat abgeschlossen, passiert nichts mehr, denn Abrechnung und Gutschriften stehen. Ein Minus in den neuen Monat mitzunehmen waere nicht nachvollziehbar. Fuer den Ausnahmefall gibt es den Settings-Schalter `storno-points-current-month`, standardmaessig aus. Ausserdem: Die persoenliche Monatsstatistik summierte Status 0 (nicht zugeordnet) und 3 (pending) mit, die gemeinsame Punkteauswertung nicht. Beide nutzen jetzt dieselbe Statusliste, und ein Storno mindert das Segment seiner Ursprungsbuchung statt pauschal in "Sonstige" zu laufen. Die Migration fuer `status_turnover` zieht nur das Schema auf den tatsaechlichen Stand nach (nullable statt NOT NULL) — auf Produktion faktisch wirkungslos, aber Neuinstallationen und die Testdatenbank haben damit dieselbe Semantik. Audit-Befunde A02, A06, A15, R01-R03. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
75c2790bd2
commit
1900860c74
10 changed files with 733 additions and 24 deletions
|
|
@ -34,7 +34,7 @@ use Illuminate\Database\Eloquent\Model;
|
|||
* @property int|null $user_invoice_id
|
||||
* @property int|null $month_shop_points
|
||||
* @property float|null $month_shop_total_net
|
||||
* @property-read \App\Models\UserInvoice|null $user_invoice
|
||||
* @property-read UserInvoice|null $user_invoice
|
||||
*
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|UserSalesVolume newModelQuery()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|UserSalesVolume newQuery()
|
||||
|
|
@ -70,8 +70,10 @@ use Illuminate\Database\Eloquent\Model;
|
|||
* @method static \Illuminate\Database\Eloquent\Builder|UserSalesVolume whereMonthTPPoints($value)
|
||||
*
|
||||
* @property int|null $status_turnover
|
||||
* @property int|null $storno_origin_status
|
||||
*
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|UserSalesVolume whereStatusTurnover($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|UserSalesVolume whereStornoOriginStatus($value)
|
||||
*
|
||||
* @mixin \Eloquent
|
||||
*/
|
||||
|
|
@ -91,6 +93,7 @@ class UserSalesVolume extends Model
|
|||
'month_shop_points' => 'float',
|
||||
'status_points' => 'int',
|
||||
'status_turnover' => 'int',
|
||||
'storno_origin_status' => 'int',
|
||||
'total_net' => 'float',
|
||||
'month_total_net' => 'float',
|
||||
'month_shop_total_net' => 'float',
|
||||
|
|
@ -115,6 +118,7 @@ class UserSalesVolume extends Model
|
|||
'month_shop_points',
|
||||
'status_points',
|
||||
'status_turnover',
|
||||
'storno_origin_status',
|
||||
'total_net',
|
||||
'month_total_net',
|
||||
'month_shop_total_net',
|
||||
|
|
@ -247,6 +251,32 @@ class UserSalesVolume extends Model
|
|||
return isset(self::$statusTypes[$this->status]) ? __('payment.'.self::$statusTypes[$this->status]) : '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Gehoert die Buchung zu einer Abo-Lieferung?
|
||||
*
|
||||
* Setzt eine geladene `shopping_order`-Relation voraus — jede Liste, die das
|
||||
* auswertet, laedt sie bereits eager mit.
|
||||
*/
|
||||
public function isAboOrder(): bool
|
||||
{
|
||||
return (bool) $this->shopping_order?->is_abo;
|
||||
}
|
||||
|
||||
/**
|
||||
* Art der Buchung, bei Abo-Lieferungen mit dem Abo-Zusatz dahinter
|
||||
* ("Shopbestellung (Abo)") — Kundenwunsch 2026-09-08.
|
||||
*/
|
||||
public function getStatusTypeWithAbo(): string
|
||||
{
|
||||
$type = (string) $this->getStatusType();
|
||||
|
||||
if (! $this->isAboOrder()) {
|
||||
return $type;
|
||||
}
|
||||
|
||||
return trim($type.' '.__('abo.abo_order_short'));
|
||||
}
|
||||
|
||||
public static function getTransStatusType()
|
||||
{
|
||||
$ret = [];
|
||||
|
|
|
|||
|
|
@ -31,6 +31,10 @@ class PersonalStatisticsService
|
|||
->where('user_id', $user->id)
|
||||
->where('month', $month)
|
||||
->where('year', $year)
|
||||
// Dieselbe Abgrenzung wie die Team-Auswertung: nicht zugeordnete (0) und
|
||||
// pending (3) Buchungen zaehlen nirgends mit, sonst weicht die persoenliche
|
||||
// Summe von der gemeinsamen Punktebasis ab.
|
||||
->whereIn('status', SalesVolumePoints::countableStatuses())
|
||||
->orderBy('id')
|
||||
->get();
|
||||
|
||||
|
|
@ -78,6 +82,37 @@ class PersonalStatisticsService
|
|||
return $order->is_abo ? 'abo' : 'shop';
|
||||
}
|
||||
|
||||
if ($status === 6) {
|
||||
return $this->stornoGroupKey($volume, $order);
|
||||
}
|
||||
|
||||
return 'other';
|
||||
}
|
||||
|
||||
/**
|
||||
* Ein Storno gehoert in das Segment seiner Ursprungsbuchung — sonst steht das
|
||||
* Minus unter "Sonstige", waehrend der stornierte Umsatz weiter im Shop- oder
|
||||
* Abo-Segment ausgewiesen wird.
|
||||
*
|
||||
* Altzeilen ohne festgehaltene Ursprungsart bleiben unter "Sonstige": ihre
|
||||
* Herkunft ist nicht rekonstruierbar.
|
||||
*/
|
||||
private function stornoGroupKey(UserSalesVolume $volume, mixed $order): string
|
||||
{
|
||||
$origin = $volume->storno_origin_status;
|
||||
|
||||
if ($origin === null) {
|
||||
return 'other';
|
||||
}
|
||||
|
||||
if ((int) $origin === 1) {
|
||||
return 'own';
|
||||
}
|
||||
|
||||
if ((int) $origin === 2 && $order) {
|
||||
return $order->is_abo ? 'abo' : 'shop';
|
||||
}
|
||||
|
||||
return 'other';
|
||||
}
|
||||
|
||||
|
|
@ -108,10 +143,11 @@ class PersonalStatisticsService
|
|||
'points' => (float) $volume->points,
|
||||
'total_net' => (float) $volume->total_net,
|
||||
'status' => (int) $volume->status,
|
||||
'status_label' => $volume->getStatusType(),
|
||||
'status_label' => $volume->getStatusTypeWithAbo(),
|
||||
'status_color' => $volume->getStatusColor(),
|
||||
'order_id' => $order?->id,
|
||||
'order_type' => $order ? ((int) $volume->status === 1 ? 'own' : 'shop') : null,
|
||||
'is_abo' => $volume->isAboOrder(),
|
||||
'customer_name' => $customer ? trim(($customer->billing_firstname ?? '').' '.($customer->billing_lastname ?? '')) : null,
|
||||
'customer_email' => $customer?->billing_email,
|
||||
'info' => $volume->info,
|
||||
|
|
|
|||
|
|
@ -17,27 +17,64 @@ use Illuminate\Support\Collection;
|
|||
* `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-Zuordnung (überschneidungsfrei — jede Zeile zählt genau einmal):
|
||||
* - Eigenpunkte (KP): status 1, 4, 5 sowie Stornos (6), die KEINE Shop-Bestellung
|
||||
* betreffen
|
||||
* - Shop-/Kundenpunkte: status 2 sowie Stornos (6) einer Shop-Bestellung
|
||||
* - status 0 (nicht zugeordnet) und 3 (pending) zählen nicht
|
||||
*
|
||||
* Massgeblich ist bei Stornos die Ursprungsart in `storno_origin_status` — NICHT
|
||||
* `status_turnover`, denn eine stornierte Gutschrift bucht ihre Punkte auf KP/TP
|
||||
* und nur ihren Umsatz auf den Shop. Altzeilen ohne Ursprungsart bleiben bei den
|
||||
* Eigenpunkten; abgeschlossene Monate ändern sich dadurch 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)';
|
||||
/**
|
||||
* Welche Buchungsart wurde storniert? Entspricht der Auswertung in
|
||||
* {@see SalesPointsVolume::reCalculateSalesPointsVolume()}, Case 6: bevorzugt
|
||||
* die festgehaltene Ursprungsart, fuer Altzeilen ersatzweise die
|
||||
* Umsatzzuordnung. COALESCE/CASE statt NOT(...), damit NULL-Spalten nicht zu
|
||||
* einem unbestimmten Vergleich fuehren und die Zeile aus beiden Kategorien fliegt.
|
||||
*/
|
||||
private const STORNO_ORIGIN = 'COALESCE(user_sales_volumes.storno_origin_status, CASE WHEN user_sales_volumes.status_turnover = 2 THEN 2 ELSE 1 END)';
|
||||
|
||||
private const SHOP_PREDICATE = '(user_sales_volumes.status = 2 OR (user_sales_volumes.status = 6 AND user_sales_volumes.status_turnover = 2))';
|
||||
private const OWN_PREDICATE = '(user_sales_volumes.status IN (1, 4, 5) OR (user_sales_volumes.status = 6 AND '.self::STORNO_ORIGIN.' <> 2))';
|
||||
|
||||
private const SHOP_PREDICATE = '(user_sales_volumes.status = 2 OR (user_sales_volumes.status = 6 AND '.self::STORNO_ORIGIN.' = 2))';
|
||||
|
||||
private const TURNOVER_PREDICATE = 'user_sales_volumes.status IN (1, 2, 4, 5, 6)';
|
||||
|
||||
/**
|
||||
* Die zaehlbaren Buchungsstatus als Liste — fuer Auswertungen, die nicht ueber
|
||||
* die SQL-Ausdruecke hier gehen, aber dieselbe Abgrenzung brauchen. Status 0
|
||||
* (nicht zugeordnet) und 3 (pending) zaehlen nicht.
|
||||
*
|
||||
* @return int[]
|
||||
*/
|
||||
public static function countableStatuses(): array
|
||||
{
|
||||
return [1, 2, 4, 5, 6];
|
||||
}
|
||||
|
||||
public static function ownPoints(): string
|
||||
{
|
||||
return self::sumWhere(self::OWN_PREDICATE, 'user_sales_volumes.points');
|
||||
}
|
||||
|
||||
/**
|
||||
* Das Kundenpunkte-Prädikat ohne SUM-Hülle — für Auswertungen, die dieselben
|
||||
* Zeilen brauchen, aber auf Positionsebene weiterrechnen (Aufteilung der
|
||||
* Abo-Punkte in wiederkehrende und einmalig hinzugebuchte).
|
||||
*/
|
||||
public static function shopPredicate(): string
|
||||
{
|
||||
return self::SHOP_PREDICATE;
|
||||
}
|
||||
|
||||
public static function externalPoints(): string
|
||||
{
|
||||
return self::sumWhere(self::SHOP_PREDICATE, 'user_sales_volumes.points');
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
namespace App\Services\BusinessPlan;
|
||||
|
||||
use App\Models\Setting;
|
||||
use App\Models\ShoppingOrder;
|
||||
use App\Models\UserSalesVolume;
|
||||
use App\Services\Incentive\IncentiveTracker;
|
||||
|
|
@ -11,6 +12,14 @@ use stdClass;
|
|||
|
||||
class SalesPointsVolume
|
||||
{
|
||||
/**
|
||||
* Schalter fuer Stornos aus bereits abgeschlossenen Monaten (Kundenentscheidung
|
||||
* 2026-09-09): standardmaessig `false` — ein abgeschlossener Monat ist fixiert,
|
||||
* die Gutschriften sind erstellt, die Punkte bleiben unveraendert. Auf `true`
|
||||
* gesetzt wird der Stornobetrag stattdessen vom laufenden Monat abgezogen.
|
||||
*/
|
||||
public const SETTING_STORNO_CURRENT_MONTH = 'storno-points-current-month';
|
||||
|
||||
public static function changeSalesPointsVolumeUser(ShoppingOrder $shoppingOrder, $to_user_id)
|
||||
{
|
||||
|
||||
|
|
@ -103,10 +112,28 @@ class SalesPointsVolume
|
|||
$month_total_net += $userSalesVolume->total_net;
|
||||
break;
|
||||
case 6: // Storno - negative Punkte und Beträge
|
||||
$month_points = self::add_KP_TP_Points($userSalesVolume, $month_points);
|
||||
// Exaktes Spiegelbild der Ursprungsbuchung. Punkte und Umsatz folgen
|
||||
// dabei UNTERSCHIEDLICHEN Regeln:
|
||||
// - Punkte: nur eine stornierte Shop-Bestellung (Status 2) mindert die
|
||||
// Shop-Punkte. Gutschrift (4), Eigenbestellung (1) und Registrierung (5)
|
||||
// mindern KP/TP — genau wie ihre Ursprungszweige oben.
|
||||
// - Umsatz: folgt `status_turnover`, wie im Gutschrift-Zweig (Case 4).
|
||||
// Die Ursprungsart steht in `storno_origin_status`
|
||||
// ({@see self::cancelSalesPointsVolume()}). Altzeilen ohne diese Angabe
|
||||
// fallen auf `status_turnover` zurueck: abgeschlossene Monate bleiben so,
|
||||
// wie sie abgerechnet wurden.
|
||||
$originStatus = $userSalesVolume->storno_origin_status;
|
||||
$pointsToShop = $originStatus !== null
|
||||
? (int) $originStatus === 2
|
||||
: $userSalesVolume->status_turnover === 2;
|
||||
|
||||
if ($pointsToShop) {
|
||||
$month_shop_points += $userSalesVolume->points; // bereits negativ
|
||||
} else {
|
||||
$month_points = self::add_KP_TP_Points($userSalesVolume, $month_points);
|
||||
}
|
||||
|
||||
if ($userSalesVolume->status_turnover === 2) {
|
||||
$month_shop_points += $userSalesVolume->points; // bereits negativ
|
||||
$month_shop_total_net += $userSalesVolume->total_net; // bereits negativ
|
||||
} else {
|
||||
$month_total_net += $userSalesVolume->total_net; // bereits negativ
|
||||
|
|
@ -269,21 +296,92 @@ class SalesPointsVolume
|
|||
}
|
||||
|
||||
/**
|
||||
* Erstellt einen Storno-Eintrag für eine stornierte Rechnung
|
||||
* Negative Punkte werden dem aktuellen Monat zugeordnet
|
||||
* Umsatzzuordnung der Stornobuchung: 2 = Shop-Umsatz, 1 = Eigenumsatz.
|
||||
*
|
||||
* Frueher wurde `status_turnover` unveraendert aus der Ursprungsbuchung
|
||||
* uebernommen. Das Feld wird auf dem regulaeren Bestellweg aber gar nicht
|
||||
* gesetzt (NULL), weshalb der Storno einer Kundenbestellung faelschlich als
|
||||
* Eigenumsatz galt. Eine Gutschrift bringt ihre Zuordnung dagegen selbst mit
|
||||
* und behaelt sie.
|
||||
*
|
||||
* Achtung: Dies steuert ausschliesslich den UMSATZ. Wohin die Punkte gehen,
|
||||
* entscheidet die Ursprungsart in `storno_origin_status`.
|
||||
*/
|
||||
public static function stornoTurnoverFor(UserSalesVolume $original_sales_volume): int
|
||||
{
|
||||
if ((int) $original_sales_volume->status === 2) { // Shop-/Kundenbestellung
|
||||
return 2;
|
||||
}
|
||||
|
||||
if ((int) $original_sales_volume->status === 4) { // Gutschrift: eigene Zuordnung
|
||||
return (int) $original_sales_volume->status_turnover === 2 ? 2 : 1;
|
||||
}
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Faellt die Ursprungsbuchung in den laufenden Monat, ist dieser noch nicht
|
||||
* ausgewertet — der Storno darf ihn mindern. Ein abgeschlossener Monat ist
|
||||
* dagegen fixiert (monatliche Auswertung gelaufen, Gutschriften erstellt).
|
||||
*/
|
||||
public static function isStornoMonthOpen(UserSalesVolume $original_sales_volume): bool
|
||||
{
|
||||
// now() statt date(): Pruefung und Buchung muessen derselben Uhr folgen,
|
||||
// sonst haengt das Ergebnis in Tests an der echten Systemuhr.
|
||||
return (int) $original_sales_volume->month === (int) now()->month
|
||||
&& (int) $original_sales_volume->year === (int) now()->year;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sollen Stornos aus abgeschlossenen Monaten ersatzweise den laufenden Monat
|
||||
* mindern? {@see self::SETTING_STORNO_CURRENT_MONTH}
|
||||
*/
|
||||
public static function stornoDeductsInCurrentMonth(): bool
|
||||
{
|
||||
return Setting::getContentBySlug(self::SETTING_STORNO_CURRENT_MONTH) === true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Erstellt einen Storno-Eintrag für eine stornierte Rechnung.
|
||||
*
|
||||
* Stornoregel (Kundenentscheidung 2026-09-09): Nur solange der Monat der
|
||||
* Ursprungsbuchung noch laeuft, wird die Punktekorrektur gebucht. Ist der
|
||||
* Monat abgeschlossen, passiert nichts mehr — ein Minus im Folgemonat waere
|
||||
* fuer den Berater nicht nachvollziehbar. Ausnahme ist der Schalter
|
||||
* {@see self::SETTING_STORNO_CURRENT_MONTH}: steht er auf `true`, wird der
|
||||
* Betrag doch vom laufenden Monat abgezogen (bisheriges Verhalten).
|
||||
*
|
||||
* Die Stornorechnung selbst wird davon nicht beruehrt; hier geht es
|
||||
* ausschliesslich um die Punkte-/Umsatzbuchung.
|
||||
*
|
||||
* @param UserSalesVolume $original_sales_volume Der ursprüngliche Sales Volume Eintrag
|
||||
* @param int $cancellation_invoice_id Die ID der Stornorechnung
|
||||
* @return UserSalesVolume
|
||||
* @return UserSalesVolume|null null, wenn der Monat fixiert ist und nicht nachgebucht wird
|
||||
*/
|
||||
public static function cancelSalesPointsVolume(UserSalesVolume $original_sales_volume, $cancellation_invoice_id)
|
||||
public static function cancelSalesPointsVolume(UserSalesVolume $original_sales_volume, $cancellation_invoice_id): ?UserSalesVolume
|
||||
{
|
||||
$month = date('m');
|
||||
$year = date('Y');
|
||||
$date = date('d.m.Y');
|
||||
if (! self::isStornoMonthOpen($original_sales_volume) && ! self::stornoDeductsInCurrentMonth()) {
|
||||
\Log::info('Punktekorrektur für Stornorechnung übersprungen: Monat bereits abgeschlossen', [
|
||||
'original_invoice_id' => $original_sales_volume->user_invoice_id,
|
||||
'cancellation_invoice_id' => $cancellation_invoice_id,
|
||||
'original_points' => $original_sales_volume->points,
|
||||
'user_id' => $original_sales_volume->user_id,
|
||||
'original_month' => $original_sales_volume->month,
|
||||
'original_year' => $original_sales_volume->year,
|
||||
'setting' => self::SETTING_STORNO_CURRENT_MONTH,
|
||||
]);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
$bookedAt = now();
|
||||
$month = $bookedAt->month;
|
||||
$year = $bookedAt->year;
|
||||
$date = $bookedAt->format('d.m.Y');
|
||||
|
||||
$syslog = [
|
||||
date('d.m.Y H:i:s') => 'Stornorechnung erstellt - Punkte korrigiert (Original Invoice: #'.$original_sales_volume->user_invoice_id.')',
|
||||
$bookedAt->format('d.m.Y H:i:s') => 'Stornorechnung erstellt - Punkte korrigiert (Original Invoice: #'.$original_sales_volume->user_invoice_id.')',
|
||||
];
|
||||
|
||||
// Negativen Eintrag erstellen (alle Werte negativ)
|
||||
|
|
@ -303,8 +401,9 @@ class SalesPointsVolume
|
|||
'month_total_net' => 0,
|
||||
'status' => 6, // Status 6 = cancelled/storniert
|
||||
'status_points' => $original_sales_volume->status_points,
|
||||
'status_turnover' => $original_sales_volume->status_turnover,
|
||||
'message' => 'Storniert am '.date('d.m.Y'),
|
||||
'status_turnover' => self::stornoTurnoverFor($original_sales_volume),
|
||||
'storno_origin_status' => (int) $original_sales_volume->status,
|
||||
'message' => 'Storniert am '.$bookedAt->format('d.m.Y'),
|
||||
'info' => 'Storno für Invoice #'.$original_sales_volume->user_invoice_id,
|
||||
'syslog' => $syslog,
|
||||
]);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,35 @@
|
|||
<?php
|
||||
|
||||
use App\Services\Backoffice\SalesVolumePoints;
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
/**
|
||||
* Schema und Daten waren auseinandergelaufen: die Ursprungsmigration definiert
|
||||
* `user_sales_volumes.status_turnover` als NOT NULL mit Default 1, in der
|
||||
* Produktionsdatenbank ist die Spalte dagegen nullable ohne Default — rund
|
||||
* 44.000 Zeilen stehen dort auf NULL, weil der regulaere Bestellweg das Feld
|
||||
* nie setzt.
|
||||
*
|
||||
* Diese Migration zieht die Definition auf den tatsaechlichen Stand nach. Auf
|
||||
* Produktion ist sie damit faktisch wirkungslos; sie sorgt aber dafuer, dass
|
||||
* frische Installationen und die Testdatenbank dieselbe Semantik haben —
|
||||
* NULL heisst "Herkunft nicht gesetzt" und wird von der Punkteauswertung
|
||||
* ({@see SalesVolumePoints}) ausdruecklich so behandelt.
|
||||
*/
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('user_sales_volumes', function (Blueprint $table) {
|
||||
$table->unsignedTinyInteger('status_turnover')->nullable()->default(null)->change();
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
// Bewusst ohne Rueckbau: NOT NULL liesse sich nicht wiederherstellen,
|
||||
// solange die vorhandenen NULL-Zeilen existieren.
|
||||
}
|
||||
};
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
/**
|
||||
* Haelt auf einer Stornobuchung fest, welche Buchungsart storniert wurde.
|
||||
*
|
||||
* `status_turnover` allein reicht dafuer nicht: Es beschreibt nur, wohin der
|
||||
* UMSATZ gehoert. Die Punkte folgen einer anderen Regel — eine Gutschrift
|
||||
* (Status 4) bucht ihre Punkte immer auf KP/TP, auch wenn ihr Umsatz dem Shop
|
||||
* zugeordnet ist. Ohne die Ursprungsart kann die Gegenbuchung diese Aufteilung
|
||||
* nicht spiegeln.
|
||||
*
|
||||
* NULL bedeutet "Altzeile ohne Angabe"; die Auswertung faellt dort auf das
|
||||
* bisherige Verhalten zurueck, damit abgeschlossene Monate unveraendert bleiben.
|
||||
*/
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('user_sales_volumes', function (Blueprint $table) {
|
||||
if (! Schema::hasColumn('user_sales_volumes', 'storno_origin_status')) {
|
||||
$table->unsignedTinyInteger('storno_origin_status')->nullable()->after('status_turnover');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('user_sales_volumes', function (Blueprint $table) {
|
||||
if (Schema::hasColumn('user_sales_volumes', 'storno_origin_status')) {
|
||||
$table->dropColumn('storno_origin_status');
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
|
@ -90,6 +90,16 @@
|
|||
{{ Form::hidden('settings[day-exectute-business-structur][type]', 'int') }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="form-group col-sm-12">
|
||||
<label class="custom-control custom-checkbox mt-2">
|
||||
{!! Form::checkbox('settings[storno-points-current-month][val]', 1, \App\Models\Setting::getContentBySlug(\App\Services\BusinessPlan\SalesPointsVolume::SETTING_STORNO_CURRENT_MONTH), ['class'=>'custom-control-input']) !!}
|
||||
<span class="custom-control-label"><strong>Stornos aus abgeschlossenen Monaten im laufenden Monat abziehen</strong></span>
|
||||
</label>
|
||||
{{ Form::hidden('settings[storno-points-current-month][type]', 'bool') }}
|
||||
<small class="form-text text-muted">Standard: aus. Storniert wird nur, solange der Monat der Ursprungsbestellung noch läuft — ist der Monat ausgewertet und die Gutschrift erstellt, bleiben die Punkte unverändert. Eingeschaltet wandert der Stornobetrag stattdessen als Minus in den laufenden Monat. Die Stornorechnung selbst wird in beiden Fällen erstellt.</small>
|
||||
</div>
|
||||
</div>
|
||||
<button type="submit" name="action" value="save_prepayment" class="btn btn-primary btn-sm mb-2"><i class="ion ion-ios-save"></i> speichern</button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -234,11 +244,12 @@
|
|||
<label class="form-label">{{ __('Standard Produktcode') }}*</label>
|
||||
@php
|
||||
$selectedDhlProduct = \App\Models\Setting::getContentBySlug('dhl_product') ?: 'V01PAK';
|
||||
$selectedDhlProduct = \App\Services\DhlProductResolver::LEGACY_PRODUCT_CODE_ALIASES[$selectedDhlProduct] ?? $selectedDhlProduct;
|
||||
$selectedDhlProduct = $selectedDhlProduct === 'V62WP' ? 'V62KP' : $selectedDhlProduct;
|
||||
@endphp
|
||||
{{ Form::select('settings[dhl_product][val]', [
|
||||
'V01PAK' => 'V01PAK - DHL Paket National',
|
||||
'V53PAK' => 'V53PAK - DHL Paket International',
|
||||
'V53WPAK' => 'V53WPAK - DHL Paket International',
|
||||
'V62KP' => 'V62KP - DHL Kleinpaket'
|
||||
], $selectedDhlProduct, array('class'=>'form-control custom-select')) }}
|
||||
{{ Form::hidden('settings[dhl_product][type]', 'text') }}
|
||||
|
|
@ -303,7 +314,9 @@
|
|||
</div>
|
||||
{{ Form::hidden('settings[dhl_international_countries][type]', 'object') }}
|
||||
<small class="form-text text-muted">
|
||||
Aktivierte Länder verwenden im DHL-Modul automatisch V53PAK. Deutschland bleibt separat über V01PAK/V62KP geregelt.
|
||||
Aktivierte Länder verwenden im DHL-Modul automatisch V53WPAK. Deutschland bleibt separat über V01PAK/V62KP geregelt.
|
||||
Länder außerhalb der EU-Zollunion ({{ implode(', ', \App\Services\DhlProductResolver::CUSTOMS_REQUIRED_COUNTRIES) }}) bleiben gesperrt,
|
||||
solange das Modul keine Zolldaten (ExportDocument/CN23) erzeugt – Sendungen dorthin bitte manuell anlegen.
|
||||
</small>
|
||||
</div>
|
||||
<div class="form-group col-sm-4">
|
||||
|
|
@ -332,9 +345,9 @@
|
|||
{{ Form::hidden('settings[dhl_account_v07pak][type]', 'text') }}
|
||||
</div>
|
||||
<div class="form-group col-sm-6">
|
||||
<label class="form-label">{{ __('V53PAK - DHL Paket International') }}</label>
|
||||
{{ Form::text('settings[dhl_account_v53pak][val]', \App\Models\Setting::getContentBySlug('dhl_account_v53pak'), array('class'=>'form-control')) }}
|
||||
{{ Form::hidden('settings[dhl_account_v53pak][type]', 'text') }}
|
||||
<label class="form-label">{{ __('V53WPAK - DHL Paket International') }}</label>
|
||||
{{ Form::text('settings[dhl_account_v53wpak][val]', \App\Models\Setting::getContentBySlug('dhl_account_v53wpak') ?: \App\Models\Setting::getContentBySlug('dhl_account_v53pak'), array('class'=>'form-control')) }}
|
||||
{{ Form::hidden('settings[dhl_account_v53wpak][type]', 'text') }}
|
||||
</div>
|
||||
<div class="form-group col-sm-6">
|
||||
<label class="form-label">{{ __('V62KP - DHL Kleinpaket') }}</label>
|
||||
|
|
|
|||
124
tests/Feature/PointsAboHighlightTest.php
Normal file
124
tests/Feature/PointsAboHighlightTest.php
Normal file
|
|
@ -0,0 +1,124 @@
|
|||
<?php
|
||||
|
||||
use App\Models\ShoppingOrder;
|
||||
use App\Models\UserSalesVolume;
|
||||
use App\Services\Backoffice\PersonalStatisticsService;
|
||||
use App\User;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\URL;
|
||||
use Tests\TestCase;
|
||||
|
||||
uses(TestCase::class, RefreshDatabase::class);
|
||||
|
||||
beforeEach(function () {
|
||||
Carbon::setTestNow(Carbon::parse('2026-09-08'));
|
||||
URL::resolveMissingNamedRoutesUsing(fn (string $name, $parameters = []) => '/'.$name);
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
Carbon::setTestNow();
|
||||
});
|
||||
|
||||
function pahUser(string $email): User
|
||||
{
|
||||
return User::forceCreate([
|
||||
'email' => $email,
|
||||
'password' => bcrypt('secret'),
|
||||
'lang' => 'de',
|
||||
'm_level' => 1,
|
||||
'admin' => 1,
|
||||
'active_date' => '2026-01-10',
|
||||
'payment_account' => now()->addYear(),
|
||||
]);
|
||||
}
|
||||
|
||||
function pahVolume(User $user, bool $isAbo, int $status = 1): UserSalesVolume
|
||||
{
|
||||
$order = ShoppingOrder::create([
|
||||
'shopping_user_id' => 0,
|
||||
'auth_user_id' => $user->id,
|
||||
'member_id' => $user->id,
|
||||
'country_id' => 1,
|
||||
'language' => 'de',
|
||||
'user_shop_id' => 1,
|
||||
'payment_for' => 6,
|
||||
'total' => 50.00,
|
||||
'subtotal' => 50.00,
|
||||
'points' => 45,
|
||||
'paid' => true,
|
||||
'shipped' => 0,
|
||||
'is_abo' => $isAbo,
|
||||
'mode' => 'live',
|
||||
]);
|
||||
|
||||
return UserSalesVolume::forceCreate([
|
||||
'user_id' => $user->id,
|
||||
'shopping_order_id' => $order->id,
|
||||
'month' => 9,
|
||||
'year' => 2026,
|
||||
'date' => '08.09.2026',
|
||||
'status' => $status,
|
||||
'points' => 45,
|
||||
'total_net' => 100,
|
||||
]);
|
||||
}
|
||||
|
||||
it('hebt Abo-Bestellungen in der Dashboard-Karte "Aktuelle Punkte" ab', function (): void {
|
||||
// Kundenrueckmeldung 08.09.2026: In "Aktuelle Punkte" waren die Abos nicht
|
||||
// hervorgehoben — die Kennzeichnung gab es bisher nur unter Mein Team > Punkte.
|
||||
$user = pahUser('pah-dashboard@example.com');
|
||||
pahVolume($user, isAbo: true);
|
||||
pahVolume($user, isAbo: false);
|
||||
|
||||
$html = view('dashboard._points', ['user' => $user->fresh()])->render();
|
||||
|
||||
expect($html)
|
||||
->toContain('btn-xs btn-info')
|
||||
->toContain('btn-xs btn-primary')
|
||||
->toContain(e(__('abo.abo_order_short')))
|
||||
->toContain(e(__('abo.points_order_legend')))
|
||||
// Kundenwunsch 09.09.2026: auch in der Spalte "Art" haengt der Abo-Zusatz dahinter
|
||||
->toContain(e(__('payment.advisor_order').' '.__('abo.abo_order_short')));
|
||||
});
|
||||
|
||||
it('liefert das Abo-Kennzeichen fuer die persoenliche Statistik mit', function (): void {
|
||||
$user = pahUser('pah-personal@example.com');
|
||||
pahVolume($user, isAbo: true);
|
||||
pahVolume($user, isAbo: false);
|
||||
|
||||
$groups = app(PersonalStatisticsService::class)->monthly($user, 9, 2026)['groups'];
|
||||
|
||||
$rows = collect($groups)->flatMap(fn (array $group) => $group['rows']);
|
||||
|
||||
expect($rows->pluck('is_abo')->all())->toContain(true)->toContain(false);
|
||||
|
||||
expect($rows->firstWhere('is_abo', true)['status_label'])
|
||||
->toContain(__('abo.abo_order_short'));
|
||||
expect($rows->firstWhere('is_abo', false)['status_label'])
|
||||
->not->toContain(__('abo.abo_order_short'));
|
||||
});
|
||||
|
||||
it('haengt den Abo-Zusatz nur bei Abo-Lieferungen an die Art', function (): void {
|
||||
$user = pahUser('pah-model@example.com');
|
||||
$abo = pahVolume($user, isAbo: true)->fresh(['shopping_order']);
|
||||
$shop = pahVolume($user, isAbo: false)->fresh(['shopping_order']);
|
||||
|
||||
expect($abo->isAboOrder())->toBeTrue();
|
||||
expect($shop->isAboOrder())->toBeFalse();
|
||||
|
||||
expect($abo->getStatusTypeWithAbo())->toBe($abo->getStatusType().' '.__('abo.abo_order_short'));
|
||||
expect($shop->getStatusTypeWithAbo())->toBe($shop->getStatusType());
|
||||
});
|
||||
|
||||
it('faerbt Abo-Bestellungen auch in "Meine Statistik" ein', function (): void {
|
||||
// Die Seite haengt an layouts.layout-2 und laesst sich nicht isoliert rendern —
|
||||
// geprueft wird deshalb die Verdrahtung von is_abo in der Blade-Quelle.
|
||||
$view = file_get_contents(resource_path('views/user/backoffice/statistics/my.blade.php'));
|
||||
|
||||
expect($view)
|
||||
->toContain("\$row['is_abo']")
|
||||
->toContain("'info' : 'primary'")
|
||||
->toContain("'info' : 'secondary'")
|
||||
->toContain("__('abo.points_order_legend')");
|
||||
});
|
||||
122
tests/Feature/StornoClosedMonthTest.php
Normal file
122
tests/Feature/StornoClosedMonthTest.php
Normal file
|
|
@ -0,0 +1,122 @@
|
|||
<?php
|
||||
|
||||
use App\Models\Setting;
|
||||
use App\Models\UserSalesVolume;
|
||||
use App\Services\BusinessPlan\SalesPointsVolume;
|
||||
use App\User;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Tests\TestCase;
|
||||
|
||||
uses(TestCase::class, RefreshDatabase::class);
|
||||
|
||||
beforeEach(function () {
|
||||
Carbon::setTestNow('2026-09-09 12:00:00');
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
Carbon::setTestNow();
|
||||
});
|
||||
|
||||
function stornoUser(): User
|
||||
{
|
||||
return User::forceCreate([
|
||||
'email' => 'storno-'.uniqid().'@example.test',
|
||||
'password' => bcrypt('secret'),
|
||||
'lang' => 'de',
|
||||
'admin' => 0,
|
||||
'm_level' => 1,
|
||||
]);
|
||||
}
|
||||
|
||||
function stornoVolume(User $user, int $month, int $year, float $points = 100.0): UserSalesVolume
|
||||
{
|
||||
return UserSalesVolume::forceCreate([
|
||||
'user_id' => $user->id,
|
||||
'user_invoice_id' => 4711,
|
||||
'month' => $month,
|
||||
'year' => $year,
|
||||
'date' => sprintf('%d-%02d-05', $year, $month),
|
||||
'points' => $points,
|
||||
'total_net' => $points,
|
||||
'status' => 2,
|
||||
'status_points' => 1,
|
||||
'status_turnover' => 1,
|
||||
]);
|
||||
}
|
||||
|
||||
it('bucht den Storno, solange der Monat der Ursprungsbuchung noch laeuft', function () {
|
||||
$user = stornoUser();
|
||||
$original = stornoVolume($user, 9, 2026);
|
||||
|
||||
$cancellation = SalesPointsVolume::cancelSalesPointsVolume($original, 9911);
|
||||
|
||||
expect($cancellation)->not->toBeNull()
|
||||
->and((float) $cancellation->points)->toBe(-100.0)
|
||||
->and((int) $cancellation->status)->toBe(6)
|
||||
->and((int) $cancellation->month)->toBe(9)
|
||||
->and((int) $cancellation->year)->toBe(2026);
|
||||
|
||||
expect(UserSalesVolume::where('user_id', $user->id)->where('status', 6)->count())->toBe(1);
|
||||
});
|
||||
|
||||
it('laesst einen abgeschlossenen Monat unveraendert', function () {
|
||||
$user = stornoUser();
|
||||
$original = stornoVolume($user, 8, 2026);
|
||||
|
||||
$cancellation = SalesPointsVolume::cancelSalesPointsVolume($original, 9911);
|
||||
|
||||
expect($cancellation)->toBeNull()
|
||||
->and(UserSalesVolume::where('user_id', $user->id)->where('status', 6)->count())->toBe(0);
|
||||
});
|
||||
|
||||
it('behandelt denselben Monat im Vorjahr als abgeschlossen', function () {
|
||||
$user = stornoUser();
|
||||
$original = stornoVolume($user, 9, 2025);
|
||||
|
||||
expect(SalesPointsVolume::isStornoMonthOpen($original))->toBeFalse()
|
||||
->and(SalesPointsVolume::cancelSalesPointsVolume($original, 9911))->toBeNull();
|
||||
});
|
||||
|
||||
it('zieht abgeschlossene Monate im laufenden Monat ab, wenn der Schalter gesetzt ist', function () {
|
||||
Setting::setContentBySlug(SalesPointsVolume::SETTING_STORNO_CURRENT_MONTH, 1, 'bool');
|
||||
|
||||
$user = stornoUser();
|
||||
$original = stornoVolume($user, 8, 2026);
|
||||
|
||||
$cancellation = SalesPointsVolume::cancelSalesPointsVolume($original, 9911);
|
||||
|
||||
expect($cancellation)->not->toBeNull()
|
||||
->and((float) $cancellation->points)->toBe(-100.0)
|
||||
->and((int) $cancellation->month)->toBe(9)
|
||||
->and((int) $cancellation->year)->toBe(2026);
|
||||
});
|
||||
|
||||
it('ist ohne gesetzten Schalter ausgeschaltet', function () {
|
||||
expect(SalesPointsVolume::stornoDeductsInCurrentMonth())->toBeFalse();
|
||||
|
||||
Setting::setContentBySlug(SalesPointsVolume::SETTING_STORNO_CURRENT_MONTH, 0, 'bool');
|
||||
expect(SalesPointsVolume::stornoDeductsInCurrentMonth())->toBeFalse();
|
||||
|
||||
Setting::setContentBySlug(SalesPointsVolume::SETTING_STORNO_CURRENT_MONTH, 1, 'bool');
|
||||
expect(SalesPointsVolume::stornoDeductsInCurrentMonth())->toBeTrue();
|
||||
});
|
||||
|
||||
it('folgt der Anwendungsuhr, nicht der Systemuhr', function () {
|
||||
// Bewusst ein anderer Kalendermonat als der des Testlaufs: die Regel darf
|
||||
// nicht davon abhaengen, welchen Monat der Server gerade zeigt.
|
||||
Carbon::setTestNow('2027-03-15 09:00:00');
|
||||
|
||||
$user = stornoUser();
|
||||
$imLaufendenMonat = stornoVolume($user, 3, 2027);
|
||||
$imVormonat = stornoVolume($user, 2, 2027);
|
||||
|
||||
expect(SalesPointsVolume::isStornoMonthOpen($imLaufendenMonat))->toBeTrue()
|
||||
->and(SalesPointsVolume::isStornoMonthOpen($imVormonat))->toBeFalse();
|
||||
|
||||
$cancellation = SalesPointsVolume::cancelSalesPointsVolume($imLaufendenMonat, 9911);
|
||||
|
||||
expect((int) $cancellation->month)->toBe(3)
|
||||
->and((int) $cancellation->year)->toBe(2027)
|
||||
->and($cancellation->message)->toBe('Storniert am 15.03.2027');
|
||||
});
|
||||
175
tests/Feature/StornoPointsCategoryTest.php
Normal file
175
tests/Feature/StornoPointsCategoryTest.php
Normal file
|
|
@ -0,0 +1,175 @@
|
|||
<?php
|
||||
|
||||
use App\Models\UserSalesVolume;
|
||||
use App\Services\Backoffice\SalesVolumePoints;
|
||||
use App\Services\BusinessPlan\SalesPointsVolume;
|
||||
use App\User;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Tests\TestCase;
|
||||
|
||||
uses(TestCase::class, RefreshDatabase::class);
|
||||
|
||||
beforeEach(function () {
|
||||
Carbon::setTestNow('2026-09-09 12:00:00');
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
Carbon::setTestNow();
|
||||
});
|
||||
|
||||
function stornoCatUser(): User
|
||||
{
|
||||
return User::forceCreate([
|
||||
'email' => 'storno-cat-'.uniqid().'@example.test',
|
||||
'password' => bcrypt('secret'),
|
||||
'lang' => 'de',
|
||||
'admin' => 0,
|
||||
'm_level' => 1,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Buchung wie auf dem regulaeren Bestellweg: `status_turnover` bleibt ungesetzt.
|
||||
*/
|
||||
function stornoCatVolume(User $user, int $status, float $points, ?int $statusTurnover = null): UserSalesVolume
|
||||
{
|
||||
return UserSalesVolume::forceCreate([
|
||||
'user_id' => $user->id,
|
||||
'user_invoice_id' => 4711,
|
||||
'month' => 9,
|
||||
'year' => 2026,
|
||||
'date' => '2026-09-05',
|
||||
'points' => $points,
|
||||
'total_net' => $points,
|
||||
'status' => $status,
|
||||
'status_points' => 1,
|
||||
'status_turnover' => $statusTurnover,
|
||||
]);
|
||||
}
|
||||
|
||||
it('kennzeichnet den Storno einer Shopbestellung als Shopgeschaeft', function () {
|
||||
$user = stornoCatUser();
|
||||
$original = stornoCatVolume($user, 2, 100.0);
|
||||
|
||||
$cancellation = SalesPointsVolume::cancelSalesPointsVolume($original, 9911);
|
||||
|
||||
expect((int) $cancellation->status_turnover)->toBe(2);
|
||||
});
|
||||
|
||||
it('kennzeichnet den Storno einer Beraterbestellung als Eigengeschaeft', function () {
|
||||
$user = stornoCatUser();
|
||||
$original = stornoCatVolume($user, 1, 100.0);
|
||||
|
||||
$cancellation = SalesPointsVolume::cancelSalesPointsVolume($original, 9911);
|
||||
|
||||
expect((int) $cancellation->status_turnover)->toBe(1);
|
||||
});
|
||||
|
||||
it('mindert beim Shop-Storno die Shop-Provisionsbasis statt der Eigenwerte', function () {
|
||||
$user = stornoCatUser();
|
||||
$original = stornoCatVolume($user, 2, 100.0);
|
||||
|
||||
SalesPointsVolume::cancelSalesPointsVolume($original, 9911);
|
||||
|
||||
$last = UserSalesVolume::where('user_id', $user->id)->orderByDesc('id')->first();
|
||||
|
||||
expect((float) $last->month_shop_total_net)->toBe(0.0)
|
||||
->and((float) $last->month_shop_points)->toBe(0.0)
|
||||
->and((float) $last->month_total_net)->toBe(0.0)
|
||||
->and((float) $last->month_KP_points)->toBe(0.0);
|
||||
});
|
||||
|
||||
it('mindert beim Eigen-Storno die Eigenwerte und laesst die Shop-Basis unberuehrt', function () {
|
||||
$user = stornoCatUser();
|
||||
stornoCatVolume($user, 2, 50.0);
|
||||
$original = stornoCatVolume($user, 1, 100.0);
|
||||
|
||||
SalesPointsVolume::cancelSalesPointsVolume($original, 9911);
|
||||
|
||||
$last = UserSalesVolume::where('user_id', $user->id)->orderByDesc('id')->first();
|
||||
|
||||
expect((float) $last->month_KP_points)->toBe(0.0)
|
||||
->and((float) $last->month_total_net)->toBe(0.0)
|
||||
->and((float) $last->month_shop_total_net)->toBe(50.0)
|
||||
->and((float) $last->month_shop_points)->toBe(50.0);
|
||||
});
|
||||
|
||||
it('zaehlt jede Zeile genau einmal — Shop-Storno mindert die Kundenpunkte', function () {
|
||||
$user = stornoCatUser();
|
||||
stornoCatVolume($user, 2, 100.0);
|
||||
stornoCatVolume($user, 6, -100.0, 2);
|
||||
|
||||
$rows = UserSalesVolume::query()
|
||||
->where('user_id', $user->id)
|
||||
->selectRaw(SalesVolumePoints::ownPoints().' as own')
|
||||
->selectRaw(SalesVolumePoints::externalPoints().' as external')
|
||||
->selectRaw(SalesVolumePoints::totalPoints().' as total')
|
||||
->first();
|
||||
|
||||
expect((float) $rows->own)->toBe(0.0)
|
||||
->and((float) $rows->external)->toBe(0.0)
|
||||
->and((float) $rows->total)->toBe(0.0);
|
||||
});
|
||||
|
||||
it('laesst Altzeilen ohne Herkunft bei den Eigenpunkten', function () {
|
||||
$user = stornoCatUser();
|
||||
stornoCatVolume($user, 2, 100.0);
|
||||
stornoCatVolume($user, 6, -100.0);
|
||||
|
||||
$rows = UserSalesVolume::query()
|
||||
->where('user_id', $user->id)
|
||||
->selectRaw(SalesVolumePoints::ownPoints().' as own')
|
||||
->selectRaw(SalesVolumePoints::externalPoints().' as external')
|
||||
->selectRaw(SalesVolumePoints::totalPoints().' as total')
|
||||
->first();
|
||||
|
||||
expect((float) $rows->own)->toBe(-100.0)
|
||||
->and((float) $rows->external)->toBe(100.0)
|
||||
->and((float) $rows->total)->toBe(0.0);
|
||||
});
|
||||
|
||||
it('spiegelt den Storno einer Gutschrift mit Shop-Umsatz vollstaendig', function () {
|
||||
$user = stornoCatUser();
|
||||
$credit = stornoCatVolume($user, 4, 100.0, 2);
|
||||
SalesPointsVolume::reCalculateSalesPointsVolume($user->id, 9, 2026);
|
||||
|
||||
$before = $credit->fresh();
|
||||
expect((float) $before->month_KP_points)->toBe(100.0)
|
||||
->and((float) $before->month_shop_total_net)->toBe(100.0)
|
||||
->and((float) $before->month_shop_points)->toBe(0.0);
|
||||
|
||||
$cancellation = SalesPointsVolume::cancelSalesPointsVolume($credit, 9911)->fresh();
|
||||
|
||||
// Punkte gehen wie im Gutschrift-Zweig auf KP, der Umsatz auf den Shop.
|
||||
expect((int) $cancellation->storno_origin_status)->toBe(4)
|
||||
->and((float) $cancellation->month_KP_points)->toBe(0.0)
|
||||
->and((float) $cancellation->month_shop_points)->toBe(0.0)
|
||||
->and((float) $cancellation->month_shop_total_net)->toBe(0.0)
|
||||
->and((float) $cancellation->month_total_net)->toBe(0.0);
|
||||
});
|
||||
|
||||
it('haelt die Ursprungsart der Stornobuchung fest', function () {
|
||||
$user = stornoCatUser();
|
||||
|
||||
expect((int) SalesPointsVolume::cancelSalesPointsVolume(stornoCatVolume($user, 2, 10.0), 1)->storno_origin_status)->toBe(2)
|
||||
->and((int) SalesPointsVolume::cancelSalesPointsVolume(stornoCatVolume($user, 1, 10.0), 2)->storno_origin_status)->toBe(1)
|
||||
->and((int) SalesPointsVolume::cancelSalesPointsVolume(stornoCatVolume($user, 5, 10.0), 3)->storno_origin_status)->toBe(5);
|
||||
});
|
||||
|
||||
it('zaehlt den Storno einer Gutschrift bei den Eigenpunkten', function () {
|
||||
$user = stornoCatUser();
|
||||
$credit = stornoCatVolume($user, 4, 100.0, 2);
|
||||
SalesPointsVolume::cancelSalesPointsVolume($credit, 9911);
|
||||
|
||||
$rows = UserSalesVolume::query()
|
||||
->where('user_id', $user->id)
|
||||
->selectRaw(SalesVolumePoints::ownPoints().' as own')
|
||||
->selectRaw(SalesVolumePoints::externalPoints().' as external')
|
||||
->selectRaw(SalesVolumePoints::totalPoints().' as total')
|
||||
->first();
|
||||
|
||||
expect((float) $rows->own)->toBe(0.0)
|
||||
->and((float) $rows->external)->toBe(0.0)
|
||||
->and((float) $rows->total)->toBe(0.0);
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue