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): ?array { return null; } public function runBestEffortBackfill(): array { 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('TestableEconomicV2BookedDepartment75DistributionService')) { class TestableEconomicV2BookedDepartment75DistributionService extends economic_v2_distribution_service { public array $stubOrders = []; public array $stubOrderItems = []; public array $stubVersionRows = []; public array $subscriptionPrices = []; public array $stubBookedInvoices = []; public array $stubBookedInvoiceLines = []; 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.0) * (float)($item['quantity'] ?? 0.0); } return $total; } protected function getSubscriptionMonthlyPrice(int $vehicle_type): float { return (float)($this->subscriptionPrices[$vehicle_type] ?? 0.0); } protected function fetchBookedInvoicesInDateRange(string $date_from, string $date_to, array &$warnings): array { return $this->stubBookedInvoices; } protected function fetchBookedInvoiceLines(array $invoice_ids, array &$warnings): array { return $this->stubBookedInvoiceLines; } 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 { return true; } } } it('redistributes booked department 75 net amounts using fixed pricing and wash subscription weights', function (): void { $versioning = new FakeEconomicV2BookedDepartment75VersioningService(); $versioning->fixedVersion = [ 'id' => 91, 'price' => 500.0, 'description' => 'Fixed pricing agreement', 'source' => 'test.fixed', 'confidence' => 1.0, 'inferred' => false, 'effective_from' => '2026-01-01 00:00:00', 'effective_to' => null, ]; $versioning->subscriptionVersions = [[ 'id' => 42, 'reg' => 'AB12345', 'vehicle_type' => 77, 'source' => 'test.subscription', 'confidence' => 1.0, 'inferred' => false, ]]; $service = new TestableEconomicV2BookedDepartment75DistributionService($versioning); $service->subscriptionPrices = [ 77 => 120.0, ]; $service->stubOrders = [ [ 'id' => 101, 'customer_id' => 12345, 'department_id' => 1, 'created_at' => '2026-01-10 10:00:00', 'reg_1' => '', 'reference' => 'Internal fixed pricing basis', ], [ 'id' => 102, 'customer_id' => 12345, 'department_id' => 2, 'created_at' => '2026-01-11 10:00:00', 'reg_1' => 'AB12345', 'reference' => '', ], ]; $service->stubOrderItems = [ 101 => [[ 'product_id' => 61, 'price' => 400.0, 'quantity' => 1, 'reference' => '', ]], 102 => [[ 'product_id' => 77, 'price' => 60.0, 'quantity' => 2, 'reference' => 'AB12345', ]], ]; $service->stubBookedInvoices = [[ 'bookedInvoiceNumber' => 7001, 'date' => '2026-01-31', 'customer' => [ 'customerNumber' => 12345, ], ]]; $service->stubBookedInvoiceLines = [ 7001 => [ ['lineNumber' => 1, 'description' => '[ 01/01/2026 00:00 Auto #999 ]'], ['lineNumber' => 2, 'description' => 'Reference:'], ['lineNumber' => 3, 'description' => '# Fast pris aftale'], [ 'lineNumber' => 4, 'description' => 'Fixed price', 'quantity' => 1, 'unitNetPrice' => 200, 'totalNetAmount' => 200, 'product' => ['productNumber' => 61], 'departmentalDistribution' => [ 'distributions' => [ ['percentage' => 50, 'department' => ['departmentNumber' => 75]], ['percentage' => 50, 'department' => ['departmentNumber' => 1]], ], ], ], [ 'lineNumber' => 5, 'description' => 'Rabat', 'quantity' => 1, 'unitNetPrice' => -40, 'totalNetAmount' => -40, 'product' => ['productNumber' => 'TotDiscount'], 'departmentalDistribution' => [ 'distributions' => [ ['percentage' => 50, 'department' => ['departmentNumber' => 75]], ['percentage' => 50, 'department' => ['departmentNumber' => 1]], ], ], ], ['lineNumber' => 6, 'description' => '[ 01/01/2026 00:00 Auto #1000 ]'], ['lineNumber' => 7, 'description' => 'Reference:'], ['lineNumber' => 8, 'description' => '# Vaskeabonnementer'], [ 'lineNumber' => 9, 'description' => 'Subscription', 'quantity' => 1, 'unitNetPrice' => 120, 'totalNetAmount' => 120, 'product' => ['productNumber' => 77], 'departmentalDistribution' => [ 'distributions' => [ ['percentage' => 100, 'department' => ['departmentNumber' => 75]], ], ], ], ], ]; $result = $service->getBookedDepartment75Distribution('2026-01-01', '2026-01-31'); expect($result['warnings'])->toBe([]); expect($result['customers'])->toHaveCount(1); $meta = $result['customers'][0]['meta']['booked_department_75']; expect($meta['booked_net_amount'])->toBe(200.0); expect($meta['distributed_net_amount'])->toBe(200.0); expect($meta['undistributed_net_amount'])->toBe(0.0); expect($meta['department_distribution']['1'])->toBe(61.53846); expect($meta['department_distribution']['2'])->toBe(138.46154); $groups = []; foreach ($meta['booked_groups'] as $group) { $groups[$group['source_category']] = $group; } expect($groups['fixed_pricing']['booked_net_amount'])->toBe(80.0); expect($groups['fixed_pricing']['department_distribution']['1'])->toBe(61.53846); expect($groups['fixed_pricing']['department_distribution']['2'])->toBe(18.46154); expect($groups['fixed_pricing']['undistributed_net_amount'])->toBe(0.0); expect($groups['wash_subscriptions']['booked_net_amount'])->toBe(120.0); expect($groups['wash_subscriptions']['department_distribution']['2'])->toBe(120.0); expect($groups['wash_subscriptions']['undistributed_net_amount'])->toBe(0.0); expect($result['collective_results']['booked_net_amount'])->toBe(200.0); expect($result['collective_results']['distributed_net_amount'])->toBe(200.0); expect($result['collective_results']['undistributed_net_amount'])->toBe(0.0); expect($result['collective_results']['department_distribution']['1'])->toBe(61.53846); expect($result['collective_results']['department_distribution']['2'])->toBe(138.46154); }); it('assigns classified booked department 75 amounts to fallback when no monthly basis exists', function (): void { $service = new TestableEconomicV2BookedDepartment75DistributionService(new FakeEconomicV2BookedDepartment75VersioningService()); $service->stubBookedInvoices = [[ 'bookedInvoiceNumber' => 7002, 'date' => '2026-01-31', 'customer' => [ 'customerNumber' => 67890, ], ]]; $service->stubBookedInvoiceLines = [ 7002 => [ ['lineNumber' => 1, 'description' => '[ 01/01/2026 00:00 Auto #1001 ]'], ['lineNumber' => 2, 'description' => 'Reference:'], ['lineNumber' => 3, 'description' => '# Fast pris aftale'], [ 'lineNumber' => 4, 'description' => 'Fixed price', 'quantity' => 1, 'unitNetPrice' => 60, 'totalNetAmount' => 60, 'product' => ['productNumber' => 61], 'departmentalDistribution' => [ 'distributions' => [ ['percentage' => 100, 'department' => ['departmentNumber' => 75]], ], ], ], ], ]; $result = $service->getBookedDepartment75Distribution('2026-01-01', '2026-01-31'); expect($result['customers'])->toHaveCount(1); $meta = $result['customers'][0]['meta']['booked_department_75']; expect($meta['booked_groups'][0]['source_category'])->toBe('fixed_pricing'); expect($meta['distributed_net_amount'])->toBe(60.0); expect($meta['undistributed_net_amount'])->toBe(0.0); expect($meta['department_distribution']['8'])->toBe(60.0); expect($meta['booked_groups'][0]['department_distribution']['8'])->toBe(60.0); expect($meta['booked_groups'][0]['undistributed_net_amount'])->toBe(0.0); expect($result['collective_results']['distributed_net_amount'])->toBe(60.0); expect($result['collective_results']['undistributed_net_amount'])->toBe(0.0); expect($result['collective_results']['department_distribution']['8'])->toBe(60.0); expect(implode("\n", $result['warnings']))->toContain('has no redistribution basis'); expect(implode("\n", $result['warnings']))->toContain('assigned to fallback department 8'); }); it('keeps unclassified booked department 75 lines undistributed with warnings', function (): void { $service = new TestableEconomicV2BookedDepartment75DistributionService(new FakeEconomicV2BookedDepartment75VersioningService()); $service->stubBookedInvoices = [[ 'bookedInvoiceNumber' => 7003, 'date' => '2026-01-31', 'customer' => [ 'customerNumber' => 77777, ], ]]; $service->stubBookedInvoiceLines = [ 7003 => [ ['lineNumber' => 1, 'description' => '[ 01/01/2026 00:00 Auto #1002 ]'], [ 'lineNumber' => 2, 'description' => 'Unknown booked line', 'quantity' => 1, 'unitNetPrice' => 25, 'totalNetAmount' => 25, 'product' => ['productNumber' => 99], 'departmentalDistribution' => [ 'distributions' => [ ['percentage' => 100, 'department' => ['departmentNumber' => 75]], ], ], ], ], ]; $result = $service->getBookedDepartment75Distribution('2026-01-01', '2026-01-31'); expect($result['customers'])->toHaveCount(1); expect($result['customers'][0]['meta']['booked_department_75']['booked_groups'][0]['source_category'])->toBe('unclassified'); expect($result['customers'][0]['meta']['booked_department_75']['distributed_net_amount'])->toBe(0.0); expect($result['customers'][0]['meta']['booked_department_75']['undistributed_net_amount'])->toBe(25.0); expect($result['collective_results']['undistributed_net_amount'])->toBe(25.0); $warning_text = implode("\n", $result['warnings']); expect($warning_text)->toContain('Unable to classify booked department 75 line'); expect($warning_text)->toContain('could not be classified and remains undistributed'); }); it('supports bulk booked invoice line payloads with top-level department numbers', function (): void { $versioning = new FakeEconomicV2BookedDepartment75VersioningService(); $versioning->fixedVersion = [ 'id' => 92, 'price' => 100.0, 'description' => 'Fixed pricing agreement', 'source' => 'test.fixed.bulk', 'confidence' => 1.0, 'inferred' => false, 'effective_from' => '2026-01-01 00:00:00', 'effective_to' => null, ]; $service = new TestableEconomicV2BookedDepartment75DistributionService($versioning); $service->stubOrders = [[ 'id' => 201, 'customer_id' => 54321, 'department_id' => 3, 'created_at' => '2026-01-10 09:00:00', 'reg_1' => '', 'reference' => 'Internal fixed pricing basis', ]]; $service->stubOrderItems = [ 201 => [[ 'product_id' => 41, 'price' => 100.0, 'quantity' => 1, 'reference' => '', ]], ]; $service->stubBookedInvoices = [[ 'bookedInvoiceNumber' => 7004, 'date' => '2026-01-31', 'customer' => [ 'customerNumber' => 54321, ], ]]; $service->stubBookedInvoiceLines = [ 7004 => [ ['number' => 1, 'description' => '[ 01/01/2026 00:00 Auto #1002 ]'], ['number' => 2, 'description' => '# Fast pris aftale'], [ 'number' => 3, 'description' => 'Fixed price', 'productNumber' => '41', 'quantity' => 1, 'unitNetPrice' => 100, 'totalNetAmount' => 100, 'departmentNumber' => 75, ], ], ]; $result = $service->getBookedDepartment75Distribution('2026-01-01', '2026-01-31'); expect($result['warnings'])->toBe([]); expect($result['collective_results']['booked_net_amount'])->toBe(100.0); expect($result['collective_results']['distributed_net_amount'])->toBe(100.0); expect($result['collective_results']['department_distribution']['3'])->toBe(100.0); });