mivita/tests/Feature/DecimalPointsDiffReportTest.php
Kevin a21bebc932 Punkte: Dezimalpunkte in Einmalprodukten, Homeparty und Shop-API nicht mehr abschneiden
Produkte mit 7,5 Punkten wurden an drei Stellen zu ganzen Zahlen:
user_abo_one_time_items (PHP-Cast int, 7,5 -> 7), homeparty_user_order_items
(MySQL rundete auf 8) und shopping_collect_orders (Summe der Shop-API).
Die Migration zieht die drei Spalten auf DECIMAL(10,2) nach (Nachtrag zu
2026_01_22), Cast und Abo-Snapshot rechnen mit float.

points:report-decimal-diff wertet die Differenzen je Berater und Monat aus
(nur lesend), points:repair-decimal korrigiert den laufenden Monat und
zieht die Monatssummen nach.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-23 15:42:27 +00:00

284 lines
10 KiB
PHP

<?php
/**
* Auswertung der Punkte-Differenzen aus dem Dezimal-Punkte-Fehler.
*
* Die Zahlen dienen als Grundlage fuer Gutschriften, deshalb sind hier vor allem
* die Abgrenzungen wichtig: was NICHT als Schaden gezaehlt werden darf.
*/
use App\Services\DecimalPointsDiffReportService;
use App\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\DB;
use Tests\TestCase;
uses(TestCase::class, RefreshDatabase::class);
function makeDiffUser(string $first = 'Erika', string $last = 'Muster'): User
{
$accountId = DB::table('user_accounts')->insertGetId([
'first_name' => $first,
'last_name' => $last,
'created_at' => now(),
'updated_at' => now(),
]);
return User::forceCreate([
'email' => 'u-'.uniqid('', true).'@example.com',
'password' => bcrypt('secret'),
'lang' => 'de',
'account_id' => $accountId,
]);
}
function makeDiffProduct(float $points): int
{
return (int) DB::table('products')->insertGetId([
'name' => 'P-'.uniqid('', true),
'title' => 'Produkt',
'active' => true,
'price' => 119,
'tax' => 19,
'weight' => 500,
'points' => $points,
'show_on' => json_encode(['2']),
'created_at' => now(),
'updated_at' => now(),
]);
}
function makeDiffOrder(int $userId, float $points): int
{
return (int) DB::table('shopping_orders')->insertGetId([
'shopping_user_id' => 1,
'country_id' => 1,
'user_shop_id' => 1,
'auth_user_id' => $userId,
'points' => $points,
'created_at' => now(),
'updated_at' => now(),
]);
}
function makeDiffItem(int $orderId, int $productId, float $points, int $qty = 1, bool $addon = true, ?string $createdAt = null): int
{
return (int) DB::table('shopping_order_items')->insertGetId([
'shopping_order_id' => $orderId,
'product_id' => $productId,
'qty' => $qty,
'points' => $points,
'is_abo_addon' => $addon,
'created_at' => $createdAt ?? now(),
'updated_at' => now(),
]);
}
function makeDiffVolume(int $userId, int $orderId, int $month, int $year, int $status = 1): int
{
return (int) DB::table('user_sales_volumes')->insertGetId([
'user_id' => $userId,
'shopping_order_id' => $orderId,
'month' => $month,
'year' => $year,
'date' => sprintf('01.%02d.%d', $month, $year),
'points' => 0,
'total_net' => 100,
'status_points' => 1,
'status' => $status,
'created_at' => now(),
'updated_at' => now(),
]);
}
/**
* Setzt den Produkt-Stichtag, indem eine regulaere Position mit dem
* Dezimalwert angelegt wird. Ohne so eine Position gilt das Produkt als
* "Stichtag unbekannt" und wird nicht gewertet.
*/
function setDiffCutoff(int $userId, int $productId, float $points, string $at): void
{
$order = makeDiffOrder($userId, $points);
makeDiffItem($order, $productId, $points, 1, false, $at);
}
describe('DecimalPointsDiffReportService', function () {
beforeEach(function () {
$this->service = new DecimalPointsDiffReportService;
});
it('weist abgeschnittene Abo-Einmalartikel als Gutschrift aus', function () {
$user = makeDiffUser('Anna', 'Berger');
$product = makeDiffProduct(7.5);
setDiffCutoff($user->id, $product, 7.5, '2026-01-27 09:00:00');
$order = makeDiffOrder($user->id, 7.0);
makeDiffItem($order, $product, 7.0, 2, true, '2026-07-10 08:00:00');
makeDiffVolume($user->id, $order, 7, 2026);
$report = $this->service->report();
expect($report['credits_owed'])->toBe(1.0)
->and($report['totals']['addon'])->toBe(1.0)
->and($report['rows'][0]['consultant'])->toBe('Anna Berger (#'.$user->id.')')
->and($report['rows'][0]['month'])->toBe(7);
});
it('ignoriert Positionen von vor dem Produkt-Stichtag', function () {
$user = makeDiffUser();
$product = makeDiffProduct(7.5);
setDiffCutoff($user->id, $product, 7.5, '2026-07-14 10:00:00');
$order = makeDiffOrder($user->id, 7.0);
makeDiffItem($order, $product, 7.0, 1, true, '2026-03-01 08:00:00');
makeDiffVolume($user->id, $order, 3, 2026);
expect($this->service->report()['rows'])->toBe([]);
});
it('ignoriert Produkte ohne bekannten Stichtag', function () {
$user = makeDiffUser();
$product = makeDiffProduct(7.5);
$order = makeDiffOrder($user->id, 7.0);
makeDiffItem($order, $product, 7.0, 1, true, '2026-07-10 08:00:00');
makeDiffVolume($user->id, $order, 7, 2026);
expect($this->service->report()['rows'])->toBe([]);
});
it('weist gerundete Sammelbestellungen als Überbuchung aus', function () {
$user = makeDiffUser();
$order = makeDiffOrder($user->id, 240.0);
makeDiffVolume($user->id, $order, 8, 2026);
DB::table('shopping_collect_orders')->insert([
'user_id' => $user->id,
'shopping_order_id' => $order,
'points' => 240,
'qty_total' => 2,
'shop_items' => json_encode([['points_total' => 120.0], ['points_total' => 119.5]]),
'created_at' => now(),
'updated_at' => now(),
]);
$report = $this->service->report();
expect($report['overbooked'])->toBe(-0.5)
->and($report['totals']['collect'])->toBe(-0.5)
->and($report['credits_owed'])->toBe(0.0);
});
it('summiert je Berater und Monat', function () {
$user = makeDiffUser();
$product = makeDiffProduct(7.5);
setDiffCutoff($user->id, $product, 7.5, '2026-01-27 09:00:00');
foreach ([7, 7, 8] as $month) {
$order = makeDiffOrder($user->id, 7.0);
makeDiffItem($order, $product, 7.0, 1, true, '2026-07-10 08:00:00');
makeDiffVolume($user->id, $order, $month, 2026);
}
$report = $this->service->report();
expect($report['rows'])->toHaveCount(2)
->and($report['by_month'])->toHaveCount(2)
->and(collect($report['by_month'])->firstWhere('month', 7)['delta'])->toBe(1.0)
->and(collect($report['by_month'])->firstWhere('month', 8)['delta'])->toBe(0.5)
->and($report['totals']['delta'])->toBe(1.5)
->and($report['totals']['users'])->toBe(1);
});
it('trennt Berater voneinander', function () {
$product = makeDiffProduct(7.5);
$first = makeDiffUser('Anna', 'Berger');
setDiffCutoff($first->id, $product, 7.5, '2026-01-27 09:00:00');
$second = makeDiffUser('Bea', 'Clausen');
foreach ([$first, $second] as $user) {
$order = makeDiffOrder($user->id, 7.0);
makeDiffItem($order, $product, 7.0, 1, true, '2026-07-10 08:00:00');
makeDiffVolume($user->id, $order, 7, 2026);
}
$report = $this->service->report();
expect($report['rows'])->toHaveCount(2)
->and($report['totals']['users'])->toBe(2)
->and($report['credits_owed'])->toBe(1.0);
});
it('grenzt auf den Zeitraum ein', function () {
$user = makeDiffUser();
$product = makeDiffProduct(7.5);
setDiffCutoff($user->id, $product, 7.5, '2026-01-27 09:00:00');
foreach ([7, 8] as $month) {
$order = makeDiffOrder($user->id, 7.0);
makeDiffItem($order, $product, 7.0, 1, true, '2026-07-10 08:00:00');
makeDiffVolume($user->id, $order, $month, 2026);
}
expect($this->service->report(null, '2026-07')['totals']['delta'])->toBe(0.5)
->and($this->service->report('2026-08', null)['totals']['delta'])->toBe(0.5)
->and($this->service->report('2026-07', '2026-08')['totals']['delta'])->toBe(1.0);
});
it('spiegelt das Delta bei Storno-Zeilen', function () {
$user = makeDiffUser();
$product = makeDiffProduct(7.5);
setDiffCutoff($user->id, $product, 7.5, '2026-01-27 09:00:00');
$order = makeDiffOrder($user->id, 7.0);
makeDiffItem($order, $product, 7.0, 1, true, '2026-07-10 08:00:00');
makeDiffVolume($user->id, $order, 7, 2026);
makeDiffVolume($user->id, $order, 7, 2026, 6);
expect($this->service->report()['totals']['delta'])->toBe(0.0);
});
it('ignoriert reguläre Positionen und Gratis-Artikel', function () {
$user = makeDiffUser();
$product = makeDiffProduct(7.5);
setDiffCutoff($user->id, $product, 7.5, '2026-01-27 09:00:00');
$regular = makeDiffOrder($user->id, 7.0);
makeDiffItem($regular, $product, 7.0, 1, false, '2026-07-10 08:00:00');
makeDiffVolume($user->id, $regular, 7, 2026);
$free = makeDiffOrder($user->id, 0.0);
makeDiffItem($free, $product, 0.0, 1, true, '2026-07-10 08:00:00');
makeDiffVolume($user->id, $free, 7, 2026);
expect($this->service->report()['rows'])->toBe([]);
});
it('läuft auch ohne die Homeparty-Tabelle', function () {
expect(Schema::hasTable('homeparty_user_order_items'))->toBeFalse()
->and($this->service->report()['totals']['homeparty'])->toBe(0.0);
});
})->group('points');
describe('points:report-decimal-diff Command', function () {
it('läuft und schreibt nichts', function () {
$this->artisan('points:report-decimal-diff')->assertExitCode(0);
});
it('exportiert CSV', function () {
$user = makeDiffUser('Anna', 'Berger');
$product = makeDiffProduct(7.5);
setDiffCutoff($user->id, $product, 7.5, '2026-01-27 09:00:00');
$order = makeDiffOrder($user->id, 7.0);
makeDiffItem($order, $product, 7.0, 1, true, '2026-07-10 08:00:00');
makeDiffVolume($user->id, $order, 7, 2026);
$path = sys_get_temp_dir().'/diff-'.uniqid('', true).'.csv';
$this->artisan('points:report-decimal-diff', ['--csv' => $path])->assertExitCode(0);
$csv = file_get_contents($path);
unlink($path);
expect($csv)->toContain('Berater')
->and($csv)->toContain('Anna Berger');
});
})->group('points');