normalize($row); $calculated = $this->calculateDerivedFields($normalized); return [ 'normalized' => $normalized, 'calculated' => $calculated, 'merged' => array_merge($normalized, $calculated), ]; } private function normalize(array $row): array { $normalized = []; foreach ($row as $header => $value) { $normalized[$header] = $this->normalizeValue($value); } return $normalized; } private function normalizeValue(mixed $value): ?string { if ($value === null) { return null; } $value = trim((string) $value); return $value === '' ? null : $value; } private function calculateDerivedFields(array $row): array { $contractingParty = $this->normalizeCode($row['Contracting Party'] ?? null); $localSellingCompany = $this->normalizeCode($row['Local Selling Company (S/T)'] ?? null); $rightsHolderLiableFlag = $this->normalizeFlag($row['Rights Holder Liable Flag'] ?? null); $globalTransferPricingFlag = $this->normalizeFlag($row['Global Transfer Pricing Flag'] ?? null); $domesticForeignFlag = $this->normalizeFlag($row['Domestic/ Foreign Flag'] ?? null); $ownershipType = $this->normalizeValue($row['Ownership Type'] ?? null) ?? ''; // Business rule: Actual vs accrual is driven by the raw Accrual Category value. $actualAccrual = (($row['Accrual Category'] ?? null) === '##') ? 'Actual' : 'Accrual'; // Business rule: US/ex-US depends on the local selling company range. $usExUs = $this->inRange($localSellingCompany, 1700, 1809) ? 'US' : 'ex-US'; // Business rule: Contracting Party is narrowed to the special codes; all others become LOCAL. $contractingPartyFiltered = in_array($contractingParty, ['1756', '1100', '1902'], true) ? $contractingParty : 'LOCAL'; // Business rule: String is a direct concatenation of the filtered contracting party plus the flags. $string = $contractingPartyFiltered . $rightsHolderLiableFlag . $globalTransferPricingFlag . $domesticForeignFlag . $ownershipType; // Business rule: 2110 applicability is a fixed whitelist lookup against the derived String. $applicableX201 = in_array($string, self::X201_ELIGIBLE_STRINGS, true) ? 'X' : ''; // Business rule: Class uses ordered precedence. The first matching rule wins. $class = $this->calculateClass($contractingParty, $localSellingCompany, $rightsHolderLiableFlag, $globalTransferPricingFlag, $applicableX201, $contractingPartyFiltered); return [ 'Actual/Accrual' => $actualAccrual, 'US/ex-US sale' => $usExUs, 'Contracting Party Filtered' => $contractingPartyFiltered, 'String' => $string, '2110 Applicable X201' => $applicableX201, 'Class' => $class, ]; } private function calculateClass( ?string $contractingParty, ?string $localSellingCompany, string $rightsHolderLiableFlag, string $globalTransferPricingFlag, string $applicableX201, string $contractingPartyFiltered ): string { if ($contractingParty === '1902') { return 'A'; } if ( $contractingParty === '1756' && $rightsHolderLiableFlag === 'Y' && !$this->inRange($localSellingCompany, 1700, 1808) ) { return 'B'; } if ($contractingParty === '1756' && $this->inRange($localSellingCompany, 1700, 1808) && $applicableX201 === 'X') { return 'C'; } if ($contractingParty === '1756' && $this->inRange($localSellingCompany, 1700, 1808) && $applicableX201 !== 'X') { return 'D'; } if ($contractingParty === '1100' && !$this->inRange($localSellingCompany, 1700, 1808)) { return 'E'; } if ($contractingParty === '1100' && $this->inRange($localSellingCompany, 1700, 1808)) { return 'F'; } if ($contractingPartyFiltered === 'LOCAL') { return 'G'; } if ( $contractingParty === '1756' && $rightsHolderLiableFlag === 'N' && $globalTransferPricingFlag === 'Y' && !$this->inRange($localSellingCompany, 1700, 1808) ) { return 'H'; } return ''; } private function normalizeCode(mixed $value): ?string { $value = $this->normalizeValue($value); if ($value === null) { return null; } if (is_numeric($value) && (float) $value == (int) $value) { return (string) (int) $value; } return $value; } private function normalizeFlag(mixed $value): string { $normalized = $this->normalizeValue($value); return $normalized ?? ''; } private function inRange(?string $value, int $min, int $max): bool { if ($value === null || $value === '') { return false; } if (!is_numeric($value)) { return false; } $number = (int) $value; return $number >= $min && $number <= $max; } }