'DE', 'phone' => '49', 'en' => 'Germany', 'de' => 'Deutschland', 'es' => 'Alemania', 'fr' => 'Allemagne', 'it' => 'Germania', 'ru' => 'Германия', 'eu_country' => true, ]); $account = UserAccount::create([ 'first_name' => 'Klaus', 'last_name' => 'Kunde', 'address' => 'Hauptstrasse 1', 'zipcode' => '10115', 'city' => 'Berlin', 'country_id' => $country->id, ]); $user = User::forceCreate([ 'account_id' => $account->id, 'email' => 'klaus@example.com', 'password' => bcrypt('secret'), 'lang' => 'de', ]); $shoppingUser = ShoppingUser::create([ 'auth_user_id' => $user->id, 'billing_firstname' => 'Klaus', 'billing_lastname' => 'Kunde', 'billing_address' => 'Hauptstrasse 1', 'billing_zipcode' => '10115', 'billing_city' => 'Berlin', 'billing_country_id' => $country->id, 'billing_email' => 'klaus@example.com', 'shipping_country_id' => $country->id, 'same_as_billing' => true, ]); $shop = UserShop::create(['user_id' => $user->id, 'name' => 'Testshop', 'active' => 1]); return [$country, $shoppingUser, $shop, $user]; } function salesVolumeProduct(string $name, string $number, float $price = 16.9): Product { return Product::create([ 'name' => $name, 'title' => $name, 'number' => $number, 'price' => $price, 'tax' => 19, 'active' => 1, ]); } function salesVolumeOrder(array $seed, array $attributes = []): ShoppingOrder { [$country, $shoppingUser, $shop, $user] = $seed; return ShoppingOrder::create(array_merge([ 'shopping_user_id' => $shoppingUser->id, 'auth_user_id' => $user->id, 'country_id' => $country->id, 'user_shop_id' => $shop->id, 'payment_for' => 3, 'mode' => 'live', 'txaction' => 'paid', 'shipped' => 0, ], $attributes)); } it('setzt das Versanddatum beim Wechsel auf versendet', function () { $order = salesVolumeOrder(salesVolumeSeed()); expect($order->shipped_at)->toBeNull(); $order->shipped = 2; $order->save(); expect($order->fresh()->shipped_at)->not->toBeNull() ->and($order->fresh()->shipped_at_source)->toBe(ShoppingOrder::SHIPPED_AT_SOURCE_LIVE); }); it('ueberschreibt ein vorhandenes Versanddatum nicht', function () { $order = salesVolumeOrder(salesVolumeSeed(), [ 'shipped' => 2, 'shipped_at' => '2026-05-04 10:00:00', 'shipped_at_source' => ShoppingOrder::SHIPPED_AT_SOURCE_DHL, ]); $order->tracking = '123'; $order->save(); expect($order->fresh()->shipped_at->format('Y-m-d'))->toBe('2026-05-04') ->and($order->fresh()->shipped_at_source)->toBe(ShoppingOrder::SHIPPED_AT_SOURCE_DHL); }); it('zaehlt auf Versandbasis den Monat des Warenausgangs, nicht den der Rechnung', function () { $seed = salesVolumeSeed(); $product = salesVolumeProduct('Aloe Gel', '8020100', 16.9); $order = salesVolumeOrder($seed, ['shipped' => 2, 'shipped_at' => '2026-07-03 09:00:00']); ShoppingOrderItem::create(['shopping_order_id' => $order->id, 'product_id' => $product->id, 'qty' => 4]); // Rechnung liegt im Vormonat (Zahlung Ende Juni), Versand erfolgte im Juli UserInvoice::create([ 'shopping_order_id' => $order->id, 'month' => 6, 'year' => 2026, 'date' => '2026-06-30', 'full_number' => 'RE-1', 'number' => 1, ]); $shippingJuly = (new SalesVolumeService(7, 2026, SalesVolumeService::BASIS_SHIPPING))->getProductVolumes(); $invoiceJuly = (new SalesVolumeService(7, 2026, SalesVolumeService::BASIS_INVOICE))->getProductVolumes(); $invoiceJune = (new SalesVolumeService(6, 2026, SalesVolumeService::BASIS_INVOICE))->getProductVolumes(); expect($shippingJuly[$product->id]['value'])->toBe(4) ->and($invoiceJuly)->toBeEmpty() ->and($invoiceJune[$product->id]['value'])->toBe(4); }); it('zaehlt externe API-Bestellungen auf Versandbasis mit', function () { $seed = salesVolumeSeed(); $product = salesVolumeProduct('Direktsaft', '6030100', 26.9); $order = salesVolumeOrder($seed, [ 'payment_for' => 7, 'txaction' => 'extern_paid', 'shipped' => 2, 'shipped_at' => '2026-07-10 09:00:00', ]); ShoppingOrderItem::create(['shopping_order_id' => $order->id, 'product_id' => $product->id, 'qty' => 3]); $volumes = (new SalesVolumeService(7, 2026, SalesVolumeService::BASIS_SHIPPING))->getProductVolumes(); expect($volumes[$product->id]['value'])->toBe(3); }); it('zieht Stornorechnungen ab statt sie zu addieren', function () { $seed = salesVolumeSeed(); $product = salesVolumeProduct('Aloe Gel', '8020100', 16.9); $order = salesVolumeOrder($seed, ['shipped' => 2, 'shipped_at' => '2026-07-05 09:00:00']); ShoppingOrderItem::create(['shopping_order_id' => $order->id, 'product_id' => $product->id, 'qty' => 2]); $invoice = UserInvoice::create([ 'shopping_order_id' => $order->id, 'month' => 7, 'year' => 2026, 'date' => '2026-07-05', 'full_number' => 'RE-1', 'number' => 1, ]); $cancellation = UserInvoice::create([ 'shopping_order_id' => $order->id, 'month' => 7, 'year' => 2026, 'date' => '2026-07-20', 'full_number' => 'RE-2', 'number' => 2, 'cancellation' => true, ]); $invoice->update(['cancellation' => true, 'cancellation_id' => $cancellation->id]); $invoiceBasis = (new SalesVolumeService(7, 2026, SalesVolumeService::BASIS_INVOICE))->getProductVolumes(); $shippingBasis = (new SalesVolumeService(7, 2026, SalesVolumeService::BASIS_SHIPPING))->getProductVolumes(); // Rechnung +2, Storno -2; auf Versandbasis Warenausgang +2 und Retoure -2 expect($invoiceBasis[$product->id]['value'])->toBe(0) ->and($shippingBasis[$product->id]['value'])->toBe(0); }); it('zieht Stornos ohne vorherigen Versand auf Versandbasis nicht ab', function () { $seed = salesVolumeSeed(); $product = salesVolumeProduct('Aloe Gel', '8020100', 16.9); $order = salesVolumeOrder($seed, ['shipped' => 10]); ShoppingOrderItem::create(['shopping_order_id' => $order->id, 'product_id' => $product->id, 'qty' => 2]); UserInvoice::create([ 'shopping_order_id' => $order->id, 'month' => 7, 'year' => 2026, 'date' => '2026-07-20', 'full_number' => 'RE-2', 'number' => 2, 'cancellation' => true, ]); $volumes = (new SalesVolumeService(7, 2026, SalesVolumeService::BASIS_SHIPPING))->getProductVolumes(); expect($volumes)->toBeEmpty(); }); it('loest Sets in ihre Anlagen auf', function () { $seed = salesVolumeSeed(); $single = salesVolumeProduct('Direktsaft', '6030100', 26.9); $set = salesVolumeProduct('6 x Direktsaft', '5010050', 129.9); ProductBundle::create([ 'product_id' => $set->id, 'bundle_product_id' => $single->id, 'quantity' => 6, 'pos' => 1, ]); $order = salesVolumeOrder($seed, ['shipped' => 2, 'shipped_at' => '2026-07-08 09:00:00']); ShoppingOrderItem::create(['shopping_order_id' => $order->id, 'product_id' => $set->id, 'qty' => 2]); ShoppingOrderItem::create(['shopping_order_id' => $order->id, 'product_id' => $single->id, 'qty' => 1]); $service = new SalesVolumeService(7, 2026, SalesVolumeService::BASIS_SHIPPING); $products = $service->getProductVolumes(); $articles = $service->getArticleVolumes(); // Produktliste: Set bleibt eine eigene Zeile expect($products[$set->id]['value'])->toBe(2) ->and($products[$single->id]['value'])->toBe(1); // Einzelartikel: 2 Sets a 6 Flaschen + 1 Einzelflasche, Set-Zeile entfaellt expect($articles[$single->id]['value'])->toBe(13) ->and($articles)->not->toHaveKey($set->id); }); it('weist den Anteil geschaetzter Versanddaten aus', function () { $seed = salesVolumeSeed(); $product = salesVolumeProduct('Aloe Gel', '8020100', 16.9); foreach ([ShoppingOrder::SHIPPED_AT_SOURCE_DHL, ShoppingOrder::SHIPPED_AT_SOURCE_ESTIMATED] as $source) { $order = salesVolumeOrder($seed, [ 'shipped' => 2, 'shipped_at' => '2026-07-08 09:00:00', 'shipped_at_source' => $source, ]); ShoppingOrderItem::create(['shopping_order_id' => $order->id, 'product_id' => $product->id, 'qty' => 1]); } $stats = (new SalesVolumeService(7, 2026, SalesVolumeService::BASIS_SHIPPING))->getQualityStats(); expect($stats['orders'])->toBe(2) ->and($stats['estimated'])->toBe(1) ->and($stats['estimated_percent'])->toBe(50.0); });