'string', 'no_akaun_lama' => 'string', 'nama_pemilik' => 'string', 'alamatpos_1' => 'string', 'alamatpos_2' => 'string', 'alamatpos_3' => 'string', 'ic_no' => 'string', 'old_ic_no' => 'string', 'bangsa' => 'string', 'warganegara' => 'string', 'nolot' => 'string', 'nogeran' => 'string', 'kegunaan' => 'string', 'mukim' => 'string', 'no_bangunan' => 'string', 'lokasiharta_1' => 'string', 'lokasiharta_2' => 'string', 'kawasan' => 'string', 'jenisbangunan' => 'string', 'new_taksiran' => 'decimal', 'kadar' => 'decimal', 'cukai_setengahtahun' => 'decimal', 'tarikh_kuatkuasa' => 'string', 'old_taksiran' => 'decimal', 'old_kadar' => 'decimal', 'tunggakan_cukai' => 'decimal', 'cukai_semasa_1' => 'decimal', 'cukai_semasa_2' => 'decimal', 'pelarasan_cukai' => 'decimal', 'bayaran_cukai_diterima' => 'decimal', 'baki_cukai' => 'decimal', 'baki_najis' => 'decimal', 'baki_notis' => 'decimal', 'baki_waran' => 'decimal', 'jumlah_baki' => 'decimal', ]; public function import(int $year, string $path, string $extension, bool $replaceExisting): array { $table = $year.'_ratemas'; if (! preg_match('/^\d{4}_ratemas$/', $table)) { $this->fail('Tahun tidak sah.'); } $tableExists = Schema::hasTable($table); if ($tableExists && ! $replaceExisting) { $this->fail("Table {$table} sudah wujud. Tandakan pilihan ganti data jika mahu overwrite."); } $rows = $extension === 'xml' ? $this->readXml($path) : $this->readCsv($path); if ($rows === []) { $this->fail('Fail tidak mengandungi rekod yang boleh diimport.'); } DB::transaction(function () use ($table, $tableExists, $replaceExisting, $rows): void { if (! $tableExists) { $this->createRateMasTable($table); } elseif ($replaceExisting) { DB::table($table)->truncate(); } foreach (array_chunk($rows, 500) as $chunk) { DB::table($table)->insert($chunk); } }); return [ 'table' => $table, 'rows' => count($rows), ]; } private function createRateMasTable(string $table): void { Schema::create($table, function (Blueprint $blueprint): void { $blueprint->string('no_akaun')->primary(); foreach (self::COLUMNS as $column => $type) { if ($column === 'no_akaun') { continue; } if ($type === 'decimal') { $blueprint->decimal($column, 14, 2)->default(0); } else { $blueprint->text($column)->nullable(); } } }); } private function readCsv(string $path): array { $handle = fopen($path, 'rb'); if ($handle === false) { $this->fail('Fail CSV tidak boleh dibaca.'); } $firstLine = fgets($handle) ?: ''; rewind($handle); $delimiter = $this->detectDelimiter($firstLine); $headers = fgetcsv($handle, 0, $delimiter); if (! is_array($headers)) { fclose($handle); $this->fail('Header CSV tidak sah.'); } $headers = array_map(fn ($header) => $this->normalizeHeader((string) $header), $headers); $this->assertRequiredHeaders($headers); $rows = []; while (($data = fgetcsv($handle, 0, $delimiter)) !== false) { if ($data === [null] || $this->isEmptyRow($data)) { continue; } $rows[] = $this->normalizeRow(array_combine($headers, array_pad($data, count($headers), null)) ?: []); } fclose($handle); return $rows; } private function readXml(string $path): array { $xml = simplexml_load_file($path); if (! $xml instanceof SimpleXMLElement) { $this->fail('Fail XML tidak sah.'); } $nodes = $this->recordNodes($xml); $rows = []; foreach ($nodes as $node) { $row = []; foreach ($node->children() as $key => $value) { $row[$this->normalizeHeader($key)] = trim((string) $value); } if ($row !== []) { $rows[] = $this->normalizeRow($row); } } if ($rows !== []) { $this->assertRequiredHeaders(array_keys($rows[0])); } return $rows; } /** * @return array */ private function recordNodes(SimpleXMLElement $xml): array { $children = iterator_to_array($xml->children()); if ($children === []) { return []; } $first = reset($children); if ($first instanceof SimpleXMLElement && $first->children()->count() > 0) { return array_values(array_filter($children, fn ($child) => $child instanceof SimpleXMLElement)); } return [$xml]; } private function normalizeRow(array $row): array { $normalized = []; foreach (self::COLUMNS as $column => $type) { $value = $row[$column] ?? null; $normalized[$column] = $type === 'decimal' ? $this->decimalValue($value) : $this->stringValue($value); } if ($normalized['no_akaun'] === null || $normalized['no_akaun'] === '') { $this->fail('Setiap rekod mesti mempunyai no_akaun.'); } return $normalized; } private function detectDelimiter(string $line): string { $delimiters = [',', ';', "\t", '|']; $counts = array_map(fn ($delimiter) => substr_count($line, $delimiter), $delimiters); arsort($counts); return $delimiters[(int) array_key_first($counts)]; } private function normalizeHeader(string $header): string { return Str::of($header) ->trim() ->lower() ->replace([' ', '-', '.', '/', '(', ')', '%'], '_') ->replaceMatches('/_+/', '_') ->trim('_') ->toString(); } private function assertRequiredHeaders(array $headers): void { if (! in_array('no_akaun', $headers, true)) { $this->fail('Fail mesti mempunyai kolum no_akaun.'); } } private function isEmptyRow(array $row): bool { return trim(implode('', array_map(fn ($value) => (string) $value, $row))) === ''; } private function stringValue(mixed $value): ?string { $value = trim((string) ($value ?? '')); return $value === '' ? null : $value; } private function decimalValue(mixed $value): float { $value = trim((string) ($value ?? '')); $value = str_replace([',', 'RM', 'rm', ' '], '', $value); return $value === '' || ! is_numeric($value) ? 0.0 : (float) $value; } private function fail(string $message): never { throw new HttpResponseException(back()->withErrors(['file' => $message])->withInput()); } }