['Import Rate', 'ImportRate', 'Import'], 'export' => ['Export Rate', 'ExportRate', 'Export'], ]; public const SECTION_MARKERS = [ 'document' => ['DOCUMENT', 'DOC', 'DOC NORMAL', 'DOC_NORMAL', 'DOCNORMAL'], 'parcel' => ['NON - DOCUMENT', 'NON-DOCUMENT', 'NON DOCUMENT', 'NONDOC', 'NON DOC', 'PARCEL', 'NON-DOC', 'NON - DOC'], ]; public array $summary = [ 'imported' => 0, 'updated' => 0, 'skipped' => 0, 'errors' => [], 'zones_updated' => 0, ]; protected string $sourceCurrency = 'AED'; protected ExchangeRateService $exchangeRate; public function import(string $filePath, string $sourceCurrency = 'AED'): array { $this->summary = [ 'imported' => 0, 'updated' => 0, 'skipped' => 0, 'errors' => [], 'zones_updated' => 0, ]; $this->sourceCurrency = strtoupper($sourceCurrency); $this->exchangeRate = app(ExchangeRateService::class); if (! file_exists($filePath)) { throw new Exception("File not found: {$filePath}"); } $spreadsheet = IOFactory::load($filePath); foreach (self::SHEET_NAMES['import'] as $name) { if ($spreadsheet->sheetNameExists($name)) { $this->importRateSheet($spreadsheet->getSheetByName($name), 'import'); break; } } foreach (self::SHEET_NAMES['export'] as $name) { if ($spreadsheet->sheetNameExists($name)) { $this->importRateSheet($spreadsheet->getSheetByName($name), 'export'); break; } } if ($spreadsheet->sheetNameExists('Zone')) { $this->importZoneSheet($spreadsheet->getSheetByName('Zone')); } elseif ($spreadsheet->sheetNameExists('Zones')) { $this->importZoneSheet($spreadsheet->getSheetByName('Zones')); } return $this->summary; } protected function importRateSheet(Worksheet $ws, string $direction): void { $maxRow = $ws->getHighestDataRow(); $maxCol = $ws->getHighestDataColumn(); $maxColIdx = \PhpOffice\PhpSpreadsheet\Cell\Coordinate::columnIndexFromString($maxCol); $currentSection = null; $headerRowIdx = null; for ($r = 1; $r <= $maxRow; $r++) { $rowValues = []; for ($c = 1; $c <= min($maxColIdx, 11); $c++) { $rowValues[$c] = $ws->getCellByColumnAndRow($c, $r)->getCalculatedValue(); } $matchedSection = $this->detectSection($rowValues); if ($matchedSection !== null) { $currentSection = $matchedSection; $headerRowIdx = null; continue; } if ($currentSection !== null && $headerRowIdx === null) { if ($this->isHeaderRow($rowValues)) { $headerRowIdx = $r; continue; } } if ($currentSection !== null && $headerRowIdx !== null) { $parsed = $this->parseRateRow($rowValues); if ($parsed === null) { if ($this->isEmptyRow($rowValues)) { $currentSection = null; $headerRowIdx = null; } continue; } ['weight' => $weight, 'rates' => $rates] = $parsed; foreach ($rates as $zone => $rate) { if ($rate === null) { $this->summary['skipped']++; continue; } $this->upsertRate($direction, $currentSection, $weight, $zone, (float) $rate); } } } } protected function detectSection(array $rowValues): ?string { foreach ($rowValues as $val) { if (! is_string($val) || trim($val) === '') { continue; } $normalized = strtoupper(trim($val)); foreach (self::SECTION_MARKERS['parcel'] as $marker) { if ($normalized === strtoupper($marker)) { return 'parcel'; } } foreach (self::SECTION_MARKERS['document'] as $marker) { if ($normalized === strtoupper($marker)) { return 'document'; } } } return null; } protected function isHeaderRow(array $rowValues): bool { if (! isset($rowValues[1]) || ! is_string($rowValues[1])) { return false; } $first = strtolower(trim($rowValues[1])); if (! in_array($first, ['weight (kg)', 'weight', 'weight(kg)', 'wt', 'wt (kg)'])) { return false; } $zoneCount = 0; $expectedInt = 1; for ($c = 2; $c <= 11; $c++) { $val = $rowValues[$c] ?? null; if ($val === null || $val === '') continue; if (is_string($val) && preg_match('/^zone\s*(\d+)$/i', trim($val), $m)) { $zoneCount++; continue; } if (is_int($val) || (is_string($val) && preg_match('/^\d+$/', trim($val)))) { $intVal = (int) $val; if ($intVal === $expectedInt) { $zoneCount++; $expectedInt++; continue; } } } return $zoneCount >= 3; } protected function parseRateRow(array $rowValues): ?array { if (! isset($rowValues[1]) || $rowValues[1] === null || $rowValues[1] === '') { return null; } $weightVal = $rowValues[1]; if (is_string($weightVal)) { $weightVal = trim($weightVal); if ($weightVal === '' || ! is_numeric($weightVal)) { return null; } $weightVal = (float) $weightVal; } elseif (! is_numeric($weightVal)) { return null; } else { $weightVal = (float) $weightVal; } $weightVal = round($weightVal, 3); $rates = []; for ($c = 2; $c <= 11; $c++) { $zone = $c - 1; $val = $rowValues[$c] ?? null; if ($val === null || $val === '' || (is_string($val) && trim($val) === '')) { $rates[$zone] = null; continue; } if (is_numeric($val)) { $rates[$zone] = (float) $val; } else { $this->summary['errors'][] = "Non-numeric rate at weight={$weightVal} zone={$zone}: " . substr((string)$val, 0, 40); $rates[$zone] = null; } } return ['weight' => $weightVal, 'rates' => $rates]; } protected function isEmptyRow(array $rowValues): bool { foreach ($rowValues as $v) { if ($v !== null && $v !== '' && !(is_string($v) && trim($v) === '')) { return false; } } return true; } /** * Upsert a rate row — همیشه به ریال ذخیره می‌شود. */ protected function upsertRate(string $direction, string $serviceType, float $weight, int $zone, float $rate): void { $rateInIrr = $this->exchangeRate->convertToIrr($rate, $this->sourceCurrency); try { $existing = RateEntry::where([ 'direction' => $direction, 'service_type' => $serviceType, 'weight' => $weight, 'zone' => $zone, 'currency' => 'IRR', ])->where('effective_at', '>=', now()->startOfDay()) ->first(); if ($existing) { $existing->update(['rate' => $rateInIrr]); $this->summary['updated']++; } else { RateEntry::create([ 'direction' => $direction, 'service_type' => $serviceType, 'weight' => $weight, 'zone' => $zone, 'rate' => $rateInIrr, 'currency' => 'IRR', 'effective_at' => now(), ]); $this->summary['imported']++; } } catch (Exception $e) { $this->summary['errors'][] = "DB error: {$direction}×{$serviceType} w={$weight} z={$zone}: " . $e->getMessage(); Log::warning('RateImport: ' . $e->getMessage()); } } protected function importZoneSheet(Worksheet $ws): void { $maxRow = $ws->getHighestDataRow(); $headerRow = null; for ($r = 1; $r <= min(20, $maxRow); $r++) { $b = $ws->getCellByColumnAndRow(2, $r)->getCalculatedValue(); $f = $ws->getCellByColumnAndRow(6, $r)->getCalculatedValue(); if ((is_string($b) && stripos($b, 'country') !== false) || (is_string($f) && stripos($f, 'country') !== false)) { $headerRow = $r; break; } } if ($headerRow === null) { $headerRow = 3; } for ($r = $headerRow + 1; $r <= $maxRow; $r++) { $parcelCountry = $ws->getCellByColumnAndRow(2, $r)->getCalculatedValue(); $parcelZone = $ws->getCellByColumnAndRow(3, $r)->getCalculatedValue(); $documentCountry = $ws->getCellByColumnAndRow(6, $r)->getCalculatedValue(); $documentZone = $ws->getCellByColumnAndRow(7, $r)->getCalculatedValue(); if (is_string($parcelCountry) && trim($parcelCountry) !== '' && $parcelZone !== null && $parcelZone !== '' && (int)$parcelZone > 0) { $this->upsertCountryZone(trim($parcelCountry), parcelZone: (int)$parcelZone); } if (is_string($documentCountry) && trim($documentCountry) !== '' && $documentZone !== null && $documentZone !== '' && (int)$documentZone > 0) { $this->upsertCountryZone(trim($documentCountry), documentZone: (int)$documentZone); } } } protected function upsertCountryZone(string $country, ?int $parcelZone = null, ?int $documentZone = null): void { try { $row = CountryZone::where('country_name', $country)->first(); if ($row) { $update = []; if ($parcelZone !== null) $update['parcel_zone'] = $parcelZone; if ($documentZone !== null) $update['document_zone'] = $documentZone; if (! empty($update)) { $row->update($update); $this->summary['zones_updated']++; } } else { CountryZone::create([ 'country_name' => $country, 'parcel_zone' => $parcelZone, 'document_zone' => $documentZone, ]); $this->summary['zones_updated']++; } } catch (Exception $e) { $this->summary['errors'][] = "Country zone error ({$country}): " . $e->getMessage(); } } }