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>
122 lines
4.1 KiB
PHP
122 lines
4.1 KiB
PHP
<?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');
|
|
});
|