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>
284 lines
10 KiB
PHP
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');
|