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');