fixedVersion; } public function resolveVehicleSubscriptionVersionsAt(int $customer_number, string $timestamp): array { return $this->subscriptionVersions; } public function resolveDiscountOverrideAt(int $customer_number, bool $is_category, string|int $object_id, string $timestamp, ?int $department_id = null): ?array { return null; } public function runBestEffortBackfill(): array { $this->backfillCalls++; return [ 'fixed_pricing' => ['inserted' => 0, 'updated' => 0, 'closed' => 0, 'noop' => 0], 'vehicle_subscriptions' => ['inserted' => 0, 'updated' => 0, 'closed' => 0, 'noop' => 0], 'discount_overrides' => ['inserted' => 0, 'updated' => 0, 'closed' => 0, 'noop' => 0], 'inferred' => ['fixed_pricing' => 0, 'vehicle_subscriptions' => 0], 'warnings' => [], ]; } } } if (!class_exists('TestableEconomicV2DistributionService')) { class TestableEconomicV2DistributionService extends economic_v2_distribution_service { public array $stubOrders = []; public array $stubOrderItems = []; public array $stubVersionRows = []; public array $subscriptionPrices = []; public array $versionTableState = []; public int $fallbackDepartmentId = 8; public array $customerDefaultDepartments = []; public ?array $includedCustomers = null; public function __construct(?economic_v2_versioning_service $versioning = null) { parent::__construct($versioning); } protected function fetchOrdersInRange(string $from_ts, string $to_ts): array { return $this->stubOrders; } protected function fetchOrderItemsByOrderIds(array $order_ids): array { return $this->stubOrderItems; } protected function fetchVehicleSubscriptionVersionRows(string $from_ts, string $to_ts): array { return $this->stubVersionRows; } protected function calculateOrderOriginalPrice(array $order_items, int $customer_number, int $department_id, string $timestamp): float { $total = 0.0; foreach ($order_items as $item) { $total += (float)($item['price'] ?? 0) * (float)($item['quantity'] ?? 0); } return $total; } protected function getSubscriptionMonthlyPrice(int $vehicle_type): float { return (float)($this->subscriptionPrices[$vehicle_type] ?? 0.0); } protected function getProductDepartmentPrice(int $product_id, int $department_id): float { return 100.0; } protected function resolveDiscountForProduct(int $customer_number, int $product_id, string $timestamp, ?int $department_id = null): ?array { return null; } protected function buildCustomerEnvelope(int $customer_number, array $transaction_map): array { return [ 'id' => $customer_number, 'customer_number' => $customer_number, 'customer_name' => 'Customer ' . $customer_number, 'transactions' => array_values($transaction_map), 'requires_action' => false, 'meta' => [], ]; } protected function buildTransactionObject(int $order_id, string $created_at, int $department_id, ?float $amount = null, ?bool $included = null): array { return [ 'id' => $order_id, 'date' => $created_at, 'amount' => round((float)($amount ?? 0.0), 5), 'booked' => true, 'department_id' => $department_id, 'excluded' => !($included ?? $this->isDepartmentEligible($department_id)), ]; } protected function isDepartmentEligible(int $department_id): bool { return $department_id > 0 && $department_id !== 10; } protected function shouldIncludeCustomerNumber(int $customer_number): bool { if ($this->includedCustomers === null) { return true; } return in_array($customer_number, $this->includedCustomers, true); } protected function getCustomerDefaultDepartmentId(int $customer_number): ?int { return $this->customerDefaultDepartments[$customer_number] ?? null; } protected function getFallbackDistributionDepartmentId(): int { return $this->fallbackDepartmentId; } protected function parseDepartmentMap(array $department_map): array { $parsed = []; foreach ($department_map as $department_id => $amount) { $parsed['Department ' . $department_id] = round((float)$amount, 5); } return $parsed; } protected function versionTableHasRows(string $table): bool { if (array_key_exists($table, $this->versionTableState)) { return (bool)$this->versionTableState[$table]; } return true; } } } it('runs best-effort backfill when fixed pricing version history is empty', function (): void { $versioning = new FakeEconomicV2DistributionVersioningService(); $versioning->fixedVersion = [ 'id' => 91, 'price' => 499.95, 'description' => 'Fixed pricing system order', 'source' => 'backfill.inferred_fixed_pricing_order', 'confidence' => 0.55, 'inferred' => true, 'effective_from' => '2026-01-01 00:00:00', 'effective_to' => null, ]; $service = new TestableEconomicV2DistributionService($versioning); $service->versionTableState = [ 'customer_fixed_pricing_versions' => false, ]; $service->stubOrders = [[ 'id' => 501, 'customer_id' => 12345, 'department_id' => 10, 'created_at' => '2026-01-15 10:00:00', 'reg_1' => '', 'reference' => 'Fast pris aftale', ]]; $service->stubOrderItems = [ 501 => [[ 'product_id' => 61, 'price' => 499.95, 'quantity' => 1, 'reference' => '', ]], ]; $result = $service->getFixedPricingDistribution('2026-01-01', '2026-01-31'); expect($versioning->backfillCalls)->toBe(1); expect($result['customers'])->toHaveCount(1); }); it('falls back to system orders for fixed pricing using the configured fallback department', function (): void { $versioning = new FakeEconomicV2DistributionVersioningService(); $versioning->fixedVersion = [ 'id' => 91, 'price' => 499.95, 'description' => 'Fixed pricing system order', 'source' => 'backfill.inferred_fixed_pricing_order', 'confidence' => 0.55, 'inferred' => true, 'effective_from' => '2026-01-01 00:00:00', 'effective_to' => null, ]; $service = new TestableEconomicV2DistributionService($versioning); $service->stubOrders = [[ 'id' => 501, 'customer_id' => 12345, 'department_id' => 10, 'created_at' => '2026-01-15 10:00:00', 'reg_1' => '', 'reference' => 'Fast pris aftale', ]]; $service->stubOrderItems = [ 501 => [[ 'product_id' => 61, 'price' => 499.95, 'quantity' => 1, 'reference' => '', ]], ]; $result = $service->getFixedPricingDistribution('2026-01-01', '2026-01-31'); expect($result['customers'])->toHaveCount(1); expect($result['customers'][0]['customer_number'])->toBe(12345); expect($result['customers'][0]['meta']['fixed_pricing']['price'])->toBe(499.95); expect($result['customers'][0]['meta']['fixed_pricing']['version_groups'][0]['order_ids'])->toBe([501]); expect($result['customers'][0]['meta']['fixed_pricing']['version_groups'][0]['department_totals']['8'])->toBe(499.95); expect($result['customers'][0]['meta']['fixed_pricing']['version_groups'][0]['department_totals'])->not->toHaveKey('10'); expect($result['collective_results']['total_department_totals']['8'])->toBe(499.95); expect($result['collective_results']['total_fixed_price'])->toBe(499.95); expect($result['warnings'])->toContain('System order fallback used for fixed pricing (department 10).'); }); it('falls back to system orders for wash subscriptions using the configured fallback department', function (): void { $versioning = new FakeEconomicV2DistributionVersioningService(); $versioning->subscriptionVersions = [[ 'id' => 42, 'reg' => 'AB12345', 'vehicle_type' => 77, 'source' => 'backfill.inferred_subscription_order', 'confidence' => 0.5, 'inferred' => true, ]]; $service = new TestableEconomicV2DistributionService($versioning); $service->subscriptionPrices = [ 77 => 299.0, ]; $service->stubOrders = [[ 'id' => 601, 'customer_id' => 12345, 'department_id' => 10, 'created_at' => '2026-01-15 10:00:00', 'reg_1' => '', 'reference' => 'Vaskeabonnementer', ]]; $service->stubOrderItems = [ 601 => [[ 'product_id' => 77, 'price' => 299.0, 'quantity' => 1, 'reference' => 'AB12345', ]], ]; $result = $service->getWashSubscriptionsDistribution('2026-01-01', '2026-01-31'); expect($result['customers'])->toHaveCount(1); expect($result['customers'][0]['customer_number'])->toBe(12345); expect($result['customers'][0]['meta']['subscription']['subscription_total'])->toBe(299.0); expect($result['customers'][0]['meta']['subscription']['version_groups'][0]['reg'])->toBe('AB12345'); expect($result['customers'][0]['meta']['subscription']['version_groups'][0]['department_distribution']['8'])->toBe(299.0); expect($result['customers'][0]['meta']['subscription']['version_groups'][0]['department_distribution'])->not->toHaveKey('10'); expect($result['collective_results']['subscription_price_department_distribution']['8'])->toBe(299.0); expect($result['collective_results']['total_subscription_price'])->toBe(299.0); expect($result['warnings'])->toContain('System order fallback used for wash subscriptions (department 10).'); }); it('uses the configured fallback department when a subscription has no customer department basis', function (): void { $versioning = new FakeEconomicV2DistributionVersioningService(); $service = new TestableEconomicV2DistributionService($versioning); $service->subscriptionPrices = [ 77 => 299.0, ]; $service->stubVersionRows = [[ 'id' => 42, 'customer_number' => 12345, 'reg' => 'AB12345', 'vehicle_type' => 77, 'wash_subscription' => 1, 'source' => 'test.version', 'confidence' => 1.0, 'inferred' => false, 'effective_from' => '2026-01-01 00:00:00', 'effective_to' => null, ]]; $result = $service->getWashSubscriptionsDistribution('2026-01-01', '2026-01-31'); expect($result['customers'])->toHaveCount(1); expect($result['customers'][0]['meta']['subscription']['version_groups'][0]['department_distribution']['8'])->toBe(299.0); expect($result['customers'][0]['meta']['subscription']['version_groups'][0]['department_distribution'])->not->toHaveKey('1'); expect($result['collective_results']['subscription_price_department_distribution']['8'])->toBe(299.0); }); it('excludes orphaned customer traces from fixed pricing and customer price distributions', function (): void { $versioning = new FakeEconomicV2DistributionVersioningService(); $versioning->fixedVersion = [ 'id' => 91, 'price' => 499.95, 'description' => 'Fixed pricing agreement', 'source' => 'test.fixed', 'confidence' => 1.0, 'inferred' => false, 'effective_from' => '2026-01-01 00:00:00', 'effective_to' => null, ]; $service = new TestableEconomicV2DistributionService($versioning); $service->includedCustomers = [12345]; $service->stubOrders = [ [ 'id' => 501, 'customer_id' => 80000038, 'department_id' => 1, 'created_at' => '2026-01-15 10:00:00', 'include_in_invoice' => 1, 'reg_1' => '', 'reference' => '', ], [ 'id' => 502, 'customer_id' => 12345, 'department_id' => 1, 'created_at' => '2026-01-16 10:00:00', 'include_in_invoice' => 1, 'reg_1' => '', 'reference' => '', ], ]; $service->stubOrderItems = [ 501 => [[ 'product_id' => 61, 'price' => 250.0, 'quantity' => 1, 'reference' => '', ]], 502 => [[ 'product_id' => 61, 'price' => 300.0, 'quantity' => 1, 'reference' => '', ]], ]; $fixedPricing = $service->getFixedPricingDistribution('2026-01-01', '2026-01-31'); $customerPrices = $service->getCustomerPricesDistribution('2026-01-01', '2026-01-31'); expect(array_column($fixedPricing['customers'], 'customer_number'))->toBe([12345]); expect($fixedPricing['collective_results']['total_fixed_price'])->toBe(499.95); expect($fixedPricing['collective_results']['total_original_price'])->toBe(300.0); expect(array_column($customerPrices['customers'], 'customer_number'))->toBe([12345]); expect($customerPrices['collective_results']['total_discount_amount'])->toBe(0.0); }); it('excludes orphaned customers from subscription fallback versions', function (): void { $versioning = new FakeEconomicV2DistributionVersioningService(); $service = new TestableEconomicV2DistributionService($versioning); $service->includedCustomers = [12345]; $service->stubOrders = [[ 'id' => 701, 'customer_id' => 12345, 'department_id' => 4, 'created_at' => '2026-01-15 10:00:00', 'include_in_invoice' => 1, 'reg_1' => 'AB12345', 'reference' => '', ]]; $service->subscriptionPrices = [ 77 => 299.0, ]; $service->stubVersionRows = [ [ 'id' => 41, 'customer_number' => 80000038, 'reg' => 'ZZ99999', 'vehicle_type' => 77, 'wash_subscription' => 1, 'source' => 'test.version', 'confidence' => 1.0, 'inferred' => false, 'effective_from' => '2026-01-01 00:00:00', 'effective_to' => null, ], [ 'id' => 42, 'customer_number' => 12345, 'reg' => 'AB12345', 'vehicle_type' => 77, 'wash_subscription' => 1, 'source' => 'test.version', 'confidence' => 1.0, 'inferred' => false, 'effective_from' => '2026-01-01 00:00:00', 'effective_to' => null, ], ]; $result = $service->getWashSubscriptionsDistribution('2026-01-01', '2026-01-31'); expect(array_column($result['customers'], 'customer_number'))->toBe([12345]); expect($result['warnings'])->toBe([ 'Fallback allocation used for subscription AB12345 customer 12345 in 2026-01', ]); });