getPendingEmailStatements(); Log::info($statements); if ($statements->isEmpty()) { Log::info('AutoSendStatementEmailJob: Tidak ada statement yang perlu dikirim email'); return; } Log::info('AutoSendStatementEmailJob: Ditemukan statement untuk dikirim', [ 'count' => $statements->count(), 'statement_ids' => $statements->pluck('id')->toArray() ]); // Proses setiap statement foreach ($statements as $statement) { $this->processSingleStatement($statement); } Log::info('AutoSendStatementEmailJob: Selesai memproses semua statement'); } catch (Exception $e) { Log::error('AutoSendStatementEmailJob: Error dalam proses auto send email', [ 'error' => $e->getMessage(), 'trace' => $e->getTraceAsString() ]); throw $e; } } /** * Mengambil statement yang siap untuk dikirim email * * @return \Illuminate\Database\Eloquent\Collection */ private function getPendingEmailStatements() { return PrintStatementLog::where(function ($query) { // Statement yang sudah available atau generated $query->where('is_available', true) ->orWhere('is_generated', true); }) ->whereNotNull('email') // Harus ada email ->where('email', '!=', '') // Email tidak kosong ->whereNull('email_sent_at') // Belum pernah dikirim ->whereNull('deleted_at') // Tidak soft deleted ->orderBy('created_at', 'desc') // Prioritas yang lama dulu ->limit(1) // Batasi maksimal 50 per run untuk performa ->get(); } /** * Memproses pengiriman email untuk satu statement * * @param PrintStatementLog $statement */ private function processSingleStatement(PrintStatementLog $statement): void { DB::beginTransaction(); try { Log::info('AutoSendStatementEmailJob: Memproses statement', [ 'statement_id' => $statement->id, 'account_number' => $statement->account_number, 'email' => $statement->email ]); // Inisialisasi disk local dan SFTP $localDisk = Storage::disk('local'); $sftpDisk = Storage::disk('sftpStatement'); $filePath = "{$statement->period_from}/{$statement->branch_code}/{$statement->account_number}_{$statement->period_from}.pdf"; /** * Fungsi helper untuk mendapatkan file dari disk dengan prioritas local * @param string $path - Path file yang dicari * @return array - [disk, exists, content] */ $getFileFromDisk = function($path) use ($localDisk, $sftpDisk) { // Cek di local disk terlebih dahulu if ($localDisk->exists("statements/{$path}")) { Log::info('AutoSendStatementEmailJob: File found in local disk', ['path' => "statements/{$path}"]); return [ 'disk' => $localDisk, 'exists' => true, 'path' => "statements/{$path}", 'source' => 'local' ]; } // Jika tidak ada di local, cek di SFTP if ($sftpDisk->exists($path)) { Log::info('AutoSendStatementEmailJob: File found in SFTP disk', ['path' => $path]); return [ 'disk' => $sftpDisk, 'exists' => true, 'path' => $path, 'source' => 'sftp' ]; } Log::warning('AutoSendStatementEmailJob: File not found in any disk', ['path' => $path]); return [ 'disk' => null, 'exists' => false, 'path' => $path, 'source' => 'none' ]; }; if ($statement->is_period_range && $statement->period_to) { $this->processMultiPeriodStatement($statement, $getFileFromDisk); } else { $this->processSinglePeriodStatement($statement, $getFileFromDisk); } // Update statement record to mark as emailed $statement->update([ 'email_sent_at' => now(), 'updated_by' => 1 // System user ID, bisa disesuaikan ]); Log::info('AutoSendStatementEmailJob: Email berhasil dikirim', [ 'statement_id' => $statement->id, 'email' => $statement->email ]); DB::commit(); } catch (Exception $e) { DB::rollBack(); Log::error('AutoSendStatementEmailJob: Gagal mengirim email untuk statement', [ 'statement_id' => $statement->id, 'error' => $e->getMessage(), 'trace' => $e->getTraceAsString() ]); // Jangan throw exception untuk statement individual agar tidak menghentikan proses lainnya // Hanya log error saja } } /** * Memproses statement dengan multiple period (range) * * @param PrintStatementLog $statement * @param callable $getFileFromDisk */ private function processMultiPeriodStatement(PrintStatementLog $statement, callable $getFileFromDisk): void { $periodFrom = Carbon::createFromFormat('Ym', $statement->period_from); $periodTo = Carbon::createFromFormat('Ym', $statement->period_to); // Loop through each month in the range $missingPeriods = []; $availablePeriods = []; $periodFiles = []; // Menyimpan info file untuk setiap periode for ($period = clone $periodFrom; $period->lte($periodTo); $period->addMonth()) { $periodFormatted = $period->format('Ym'); $periodPath = "{$periodFormatted}/{$statement->branch_code}/{$statement->account_number}_{$periodFormatted}.pdf"; $fileInfo = $getFileFromDisk($periodPath); if ($fileInfo['exists']) { $availablePeriods[] = $periodFormatted; $periodFiles[$periodFormatted] = $fileInfo; } else { $missingPeriods[] = $periodFormatted; } } // If any period is available, create a zip and send it if (count($availablePeriods) > 0) { // Create a temporary zip file $zipFileName = "{$statement->account_number}_{$statement->period_from}_to_{$statement->period_to}.zip"; $zipFilePath = storage_path("app/temp/{$zipFileName}"); // Ensure the temp directory exists if (!file_exists(storage_path('app/temp'))) { mkdir(storage_path('app/temp'), 0755, true); } // Create a new zip archive $zip = new ZipArchive(); if ($zip->open($zipFilePath, ZipArchive::CREATE | ZipArchive::OVERWRITE) === true) { // Add each available statement to the zip foreach ($availablePeriods as $period) { $fileInfo = $periodFiles[$period]; $localFilePath = storage_path("app/temp/{$statement->account_number}_{$period}.pdf"); // Download/copy the file to local temp storage file_put_contents($localFilePath, $fileInfo['disk']->get($fileInfo['path'])); Log::info('AutoSendStatementEmailJob: File retrieved for zip', [ 'period' => $period, 'source' => $fileInfo['source'], 'path' => $fileInfo['path'] ]); // Add the file to the zip $zip->addFile($localFilePath, "{$statement->account_number}_{$period}.pdf"); } $zip->close(); // Send email with zip attachment Mail::to($statement->email) ->send(new StatementEmail($statement, $zipFilePath, true)); // Clean up temporary files foreach ($availablePeriods as $period) { $localFilePath = storage_path("app/temp/{$statement->account_number}_{$period}.pdf"); if (file_exists($localFilePath)) { unlink($localFilePath); } } // Delete the zip file after sending if (file_exists($zipFilePath)) { unlink($zipFilePath); } Log::info('AutoSendStatementEmailJob: Multi-period statement email sent successfully', [ 'statement_id' => $statement->id, 'periods' => $availablePeriods, 'sources' => array_map(fn($p) => $periodFiles[$p]['source'], $availablePeriods) ]); } else { throw new Exception('Failed to create zip archive for email.'); } } else { throw new Exception('No statements available for sending.'); } } /** * Memproses statement dengan single period * * @param PrintStatementLog $statement * @param callable $getFileFromDisk */ private function processSinglePeriodStatement(PrintStatementLog $statement, callable $getFileFromDisk): void { $account = Account::where('account_number',$statement->account_number)->first(); $filePath = "{$statement->period_from}/{$account->branch_code}/{$statement->account_number}_{$statement->period_from}.pdf"; $fileInfo = $getFileFromDisk($filePath); if ($fileInfo['exists']) { $localFilePath = storage_path("app/temp/{$statement->account_number}_{$statement->period_from}.pdf"); // Ensure the temp directory exists if (!file_exists(storage_path('app/temp'))) { mkdir(storage_path('app/temp'), 0755, true); } // Download/copy the file to local temp storage file_put_contents($localFilePath, $fileInfo['disk']->get($fileInfo['path'])); Log::info('AutoSendStatementEmailJob: Single period file retrieved', [ 'source' => $fileInfo['source'], 'path' => $fileInfo['path'] ]); // Send email with PDF attachment Mail::to($statement->email) ->send(new StatementEmail($statement, $localFilePath, false)); // Delete the temporary file if (file_exists($localFilePath)) { unlink($localFilePath); } } else { throw new Exception('Statement file not found.'); } } /** * Handle job failure * * @param Exception $exception */ public function failed(Exception $exception): void { Log::error('AutoSendStatementEmailJob: Job failed completely', [ 'error' => $exception->getMessage(), 'trace' => $exception->getTraceAsString() ]); } }