150, 'lines' => [ [ 'lineNumber' => 1, 'description' => 'Subscription', 'quantity' => 2, 'unitNetPrice' => 75, 'totalNetAmount' => 150, 'product' => [ 'productNumber' => 1, ], 'departmentalDistribution' => [ 'distributions' => [ [ 'percentage' => 60, 'department' => ['departmentNumber' => 75], ], [ 'percentage' => 40, 'department' => ['departmentNumber' => 10], ], ], ], ], ], ]; $normalized = economic_v2_line_normalizer::normalizeDraftInvoice($draft); expect($normalized['source'])->toBe('draft'); expect($normalized['totals']['net_total'])->toBe(150.0); expect($normalized['totals']['line_count'])->toBe(1); expect($normalized['lines'][0]['product_number'])->toBe('1'); expect($normalized['lines'][0]['department_distribution']['75'])->toBe(60.0); expect($normalized['lines'][0]['department_distribution']['10'])->toBe(40.0); }); it('marks text-only zero-value lines as non-billable and keeps deterministic key', function (): void { $draft = [ 'lines' => [ [ 'lineNumber' => 1, 'description' => '# Header line', 'quantity' => 0, 'unitNetPrice' => 0, 'totalNetAmount' => 0, ], ], ]; $normalized = economic_v2_line_normalizer::normalizeDraftInvoice($draft); $line = $normalized['lines'][0]; expect($line['billable'])->toBeFalse(); expect($line['line_type'])->toBe('text'); expect($line['match_key'])->toStartWith('text:'); expect($line['department_distribution']['unassigned'])->toBe(100.0); }); it('normalizes booked invoices and computes net total delta from lines', function (): void { $booked = [ 'net_amount' => 100, 'lines' => [ [ 'line_number' => 1, 'description' => 'Wash', 'quantity' => 1, 'unit_net_price' => 90, 'total_net_amount' => 90, 'product' => ['product_number' => 33], ], ], ]; $normalized = economic_v2_line_normalizer::normalizeBookedInvoice($booked); expect($normalized['source'])->toBe('booked'); expect($normalized['totals']['net_total'])->toBe(100.0); expect($normalized['totals']['line_net_total'])->toBe(90.0); expect($normalized['totals']['difference_from_line_sum'])->toBe(10.0); }); it('contains internal normalization path with departmental metadata support', function (): void { $classFile = app_path('classes/economic_v2_line_normalizer.php'); $content = file_get_contents($classFile); expect($content)->not->toBeFalse(); expect($content)->toContain('normalizeInternalCollectedInvoice('); expect($content)->toContain("'department_distribution'"); expect($content)->toContain('buildMatchKey('); });