diff --git a/app/Console/SendStatementEmailCommand.php b/app/Console/SendStatementEmailCommand.php index 2ccc05c..f7648b4 100644 --- a/app/Console/SendStatementEmailCommand.php +++ b/app/Console/SendStatementEmailCommand.php @@ -1,22 +1,23 @@ info('🚀 Memulai proses pengiriman email statement...'); + public function handle() + { + $this->info('🚀 Memulai proses pengiriman email statement...'); - try { - $period = $this->argument('period'); - $type = $this->option('type'); - $accountNumber = $this->option('account'); - $branchCode = $this->option('branch'); - $batchId = $this->option('batch-id'); - $queueName = $this->option('queue'); - $delay = (int) $this->option('delay'); + try { + $period = $this->argument('period'); + $type = $this->option('type'); + $accountNumber = $this->option('account'); + $branchCode = $this->option('branch'); + $batchId = $this->option('batch-id'); + $queueName = $this->option('queue'); + $delay = (int) $this->option('delay'); - // Validasi parameter - if (!$this->validateParameters($period, $type, $accountNumber, $branchCode)) { + // Validasi parameter + if (!$this->validateParameters($period, $type, $accountNumber, $branchCode)) { + return Command::FAILURE; + } + + // Tentukan request type dan target value + [$requestType, $targetValue] = $this->determineRequestTypeAndTarget($type, $accountNumber, $branchCode); + + // Buat log entry + $log = $this->createLogEntry($period, $requestType, $targetValue, $batchId); + + // Dispatch job + $job = SendStatementEmailJob::dispatch($period, $requestType, $targetValue, $batchId, $log->id) + ->onQueue($queueName); + + if ($delay > 0) { + $job->delay(now()->addMinutes($delay)); + $this->info("⏰ Job dijadwalkan untuk dijalankan dalam {$delay} menit"); + } + + $this->displayJobInfo($period, $requestType, $targetValue, $queueName, $log); + $this->info('✅ Job pengiriman email statement berhasil didispatch!'); + $this->info('📊 Gunakan command berikut untuk monitoring:'); + $this->line(" php artisan queue:work {$queueName}"); + $this->line(' php artisan webstatement:check-progress ' . $log->id); + + return Command::SUCCESS; + + } catch (Exception $e) { + $this->error('❌ Error saat mendispatch job: ' . $e->getMessage()); + Log::error('SendStatementEmailCommand failed', [ + 'error' => $e->getMessage(), + 'trace' => $e->getTraceAsString() + ]); return Command::FAILURE; } + } - // Tentukan request type dan target value - [$requestType, $targetValue] = $this->determineRequestTypeAndTarget($type, $accountNumber, $branchCode); - - // Buat log entry - $log = $this->createLogEntry($period, $requestType, $targetValue, $batchId); - - // Dispatch job - $job = SendStatementEmailJob::dispatch($period, $requestType, $targetValue, $batchId, $log->id) - ->onQueue($queueName); - - if ($delay > 0) { - $job->delay(now()->addMinutes($delay)); - $this->info("⏰ Job dijadwalkan untuk dijalankan dalam {$delay} menit"); + private function validateParameters($period, $type, $accountNumber, $branchCode) + { + // Validasi format periode + if (!preg_match('/^\d{6}$/', $period)) { + $this->error('❌ Format periode tidak valid. Gunakan format YYYYMM (contoh: 202401)'); + return false; } - $this->displayJobInfo($period, $requestType, $targetValue, $queueName, $log); - $this->info('✅ Job pengiriman email statement berhasil didispatch!'); - $this->info('📊 Gunakan command berikut untuk monitoring:'); - $this->line(" php artisan queue:work {$queueName}"); - $this->line(' php artisan webstatement:check-progress ' . $log->id); + // Validasi type + if (!in_array($type, ['single', 'branch', 'all'])) { + $this->error('❌ Type tidak valid. Gunakan: single, branch, atau all'); + return false; + } - return Command::SUCCESS; + // Validasi parameter berdasarkan type + switch ($type) { + case 'single': + if (!$accountNumber) { + $this->error('❌ Parameter --account diperlukan untuk type=single'); + return false; + } - } catch (Exception $e) { - $this->error('❌ Error saat mendispatch job: ' . $e->getMessage()); - Log::error('SendStatementEmailCommand failed', [ - 'error' => $e->getMessage(), - 'trace' => $e->getTraceAsString() - ]); - return Command::FAILURE; + $account = Account::with('customer') + ->where('account_number', $accountNumber) + ->first(); + + if (!$account) { + $this->error("❌ Account {$accountNumber} tidak ditemukan"); + return false; + } + + $hasEmail = !empty($account->stmt_email) || + ($account->customer && !empty($account->customer->email)); + + if (!$hasEmail) { + $this->error("❌ Account {$accountNumber} tidak memiliki email"); + return false; + } + + $this->info("✅ Account {$accountNumber} ditemukan dengan email"); + break; + + case 'branch': + if (!$branchCode) { + $this->error('❌ Parameter --branch diperlukan untuk type=branch'); + return false; + } + + $branch = Branch::where('code', $branchCode)->first(); + if (!$branch) { + $this->error("❌ Branch {$branchCode} tidak ditemukan"); + return false; + } + + $accountCount = Account::with('customer') + ->where('branch_code', $branchCode) + ->where('stmt_sent_type', 'BY.EMAIL') + ->get() + ->filter(function ($account) { + return !empty($account->stmt_email) || + ($account->customer && !empty($account->customer->email)); + }) + ->count(); + + if ($accountCount === 0) { + $this->error("❌ Tidak ada account dengan email di branch {$branchCode}"); + return false; + } + + $this->info("✅ Ditemukan {$accountCount} account dengan email di branch {$branch->name}"); + break; + + case 'all': + $accountCount = Account::with('customer') + ->where('stmt_sent_type', 'BY.EMAIL') + ->get() + ->filter(function ($account) { + return !empty($account->stmt_email) || + ($account->customer && !empty($account->customer->email)); + }) + ->count(); + + if ($accountCount === 0) { + $this->error('❌ Tidak ada account dengan email ditemukan'); + return false; + } + + $this->info("✅ Ditemukan {$accountCount} account dengan email di seluruh cabang"); + break; + } + + return true; + } + + private function determineRequestTypeAndTarget($type, $accountNumber, $branchCode) + { + switch ($type) { + case 'single': + return ['single_account', $accountNumber]; + case 'branch': + return ['branch', $branchCode]; + case 'all': + return ['all_branches', null]; + default: + throw new InvalidArgumentException("Invalid type: {$type}"); + } + } + + private function createLogEntry($period, $requestType, $targetValue, $batchId) + { + $logData = [ + 'user_id' => null, // Command line execution + 'period_from' => $period, + 'period_to' => $period, + 'is_period_range' => false, + 'request_type' => $requestType, + 'batch_id' => $batchId ?? uniqid('cmd_'), + 'status' => 'pending', + 'authorization_status' => 'approved', // Auto-approved untuk command line + 'created_by' => null, + 'ip_address' => '127.0.0.1', + 'user_agent' => 'Command Line' + ]; + + // Set branch_code dan account_number berdasarkan request type + switch ($requestType) { + case 'single_account': + $account = Account::where('account_number', $targetValue)->first(); + $logData['branch_code'] = $account->branch_code; + $logData['account_number'] = $targetValue; + break; + case 'branch': + $logData['branch_code'] = $targetValue; + $logData['account_number'] = null; + break; + case 'all_branches': + $logData['branch_code'] = 'ALL'; + $logData['account_number'] = null; + break; + } + + return PrintStatementLog::create($logData); + } + + private function displayJobInfo($period, $requestType, $targetValue, $queueName, $log) + { + $this->info('📋 Detail Job:'); + $this->line(" Log ID: {$log->id}"); + $this->line(" Periode: {$period}"); + $this->line(" Request Type: {$requestType}"); + + switch ($requestType) { + case 'single_account': + $this->line(" Account: {$targetValue}"); + break; + case 'branch': + $branch = Branch::where('code', $targetValue)->first(); + $this->line(" Branch: {$targetValue} ({$branch->name})"); + break; + case 'all_branches': + $this->line(" Target: Seluruh cabang"); + break; + } + + $this->line(" Batch ID: {$log->batch_id}"); + $this->line(" Queue: {$queueName}"); } } - - private function validateParameters($period, $type, $accountNumber, $branchCode) - { - // Validasi format periode - if (!preg_match('/^\d{6}$/', $period)) { - $this->error('❌ Format periode tidak valid. Gunakan format YYYYMM (contoh: 202401)'); - return false; - } - - // Validasi type - if (!in_array($type, ['single', 'branch', 'all'])) { - $this->error('❌ Type tidak valid. Gunakan: single, branch, atau all'); - return false; - } - - // Validasi parameter berdasarkan type - switch ($type) { - case 'single': - if (!$accountNumber) { - $this->error('❌ Parameter --account diperlukan untuk type=single'); - return false; - } - - $account = Account::with('customer') - ->where('account_number', $accountNumber) - ->first(); - - if (!$account) { - $this->error("❌ Account {$accountNumber} tidak ditemukan"); - return false; - } - - $hasEmail = !empty($account->stmt_email) || - ($account->customer && !empty($account->customer->email)); - - if (!$hasEmail) { - $this->error("❌ Account {$accountNumber} tidak memiliki email"); - return false; - } - - $this->info("✅ Account {$accountNumber} ditemukan dengan email"); - break; - - case 'branch': - if (!$branchCode) { - $this->error('❌ Parameter --branch diperlukan untuk type=branch'); - return false; - } - - $branch = Branch::where('code', $branchCode)->first(); - if (!$branch) { - $this->error("❌ Branch {$branchCode} tidak ditemukan"); - return false; - } - - $accountCount = Account::with('customer') - ->where('branch_code', $branchCode) - ->where('stmt_sent_type', 'BY.EMAIL') - ->get() - ->filter(function ($account) { - return !empty($account->stmt_email) || - ($account->customer && !empty($account->customer->email)); - }) - ->count(); - - if ($accountCount === 0) { - $this->error("❌ Tidak ada account dengan email di branch {$branchCode}"); - return false; - } - - $this->info("✅ Ditemukan {$accountCount} account dengan email di branch {$branch->name}"); - break; - - case 'all': - $accountCount = Account::with('customer') - ->where('stmt_sent_type', 'BY.EMAIL') - ->get() - ->filter(function ($account) { - return !empty($account->stmt_email) || - ($account->customer && !empty($account->customer->email)); - }) - ->count(); - - if ($accountCount === 0) { - $this->error('❌ Tidak ada account dengan email ditemukan'); - return false; - } - - $this->info("✅ Ditemukan {$accountCount} account dengan email di seluruh cabang"); - break; - } - - return true; - } - - private function determineRequestTypeAndTarget($type, $accountNumber, $branchCode) - { - switch ($type) { - case 'single': - return ['single_account', $accountNumber]; - case 'branch': - return ['branch', $branchCode]; - case 'all': - return ['all_branches', null]; - default: - throw new \InvalidArgumentException("Invalid type: {$type}"); - } - } - - private function createLogEntry($period, $requestType, $targetValue, $batchId) - { - $logData = [ - 'user_id' => null, // Command line execution - 'period_from' => $period, - 'period_to' => $period, - 'is_period_range' => false, - 'request_type' => $requestType, - 'batch_id' => $batchId ?? uniqid('cmd_'), - 'status' => 'pending', - 'authorization_status' => 'approved', // Auto-approved untuk command line - 'created_by' => null, - 'ip_address' => '127.0.0.1', - 'user_agent' => 'Command Line' - ]; - - // Set branch_code dan account_number berdasarkan request type - switch ($requestType) { - case 'single_account': - $account = Account::where('account_number', $targetValue)->first(); - $logData['branch_code'] = $account->branch_code; - $logData['account_number'] = $targetValue; - break; - case 'branch': - $logData['branch_code'] = $targetValue; - $logData['account_number'] = null; - break; - case 'all_branches': - $logData['branch_code'] = 'ALL'; - $logData['account_number'] = null; - break; - } - - return PrintStatementLog::create($logData); - } - - private function displayJobInfo($period, $requestType, $targetValue, $queueName, $log) - { - $this->info('📋 Detail Job:'); - $this->line(" Log ID: {$log->id}"); - $this->line(" Periode: {$period}"); - $this->line(" Request Type: {$requestType}"); - - switch ($requestType) { - case 'single_account': - $this->line(" Account: {$targetValue}"); - break; - case 'branch': - $branch = Branch::where('code', $targetValue)->first(); - $this->line(" Branch: {$targetValue} ({$branch->name})"); - break; - case 'all_branches': - $this->line(" Target: Seluruh cabang"); - break; - } - - $this->line(" Batch ID: {$log->batch_id}"); - $this->line(" Queue: {$queueName}"); - } -} diff --git a/app/Console/UpdateAllAtmCardsCommand.php b/app/Console/UpdateAllAtmCardsCommand.php new file mode 100644 index 0000000..2d8fa45 --- /dev/null +++ b/app/Console/UpdateAllAtmCardsCommand.php @@ -0,0 +1,110 @@ +option('sync-log-id'); + $batchSize = (int) $this->option('batch-size'); + $queueName = $this->option('queue'); + $filtersJson = $this->option('filters'); + $isDryRun = $this->option('dry-run'); + + // Parse filters jika ada + $filters = []; + if ($filtersJson) { + $filters = json_decode($filtersJson, true); + if (json_last_error() !== JSON_ERROR_NONE) { + $this->error('Format JSON filters tidak valid'); + return Command::FAILURE; + } + } + + // Validasi input + if ($batchSize <= 0) { + $this->error('Batch size harus lebih besar dari 0'); + return Command::FAILURE; + } + + $this->info('Konfigurasi job:'); + $this->info("- Sync Log ID: " . ($syncLogId ?: 'Akan dibuat baru')); + $this->info("- Batch Size: {$batchSize}"); + $this->info("- Queue: {$queueName}"); + $this->info("- Filters: " . ($filtersJson ?: 'Tidak ada')); + $this->info("- Dry Run: " . ($isDryRun ? 'Ya' : 'Tidak')); + + if ($isDryRun) { + $this->warn('Mode DRY RUN - Job tidak akan dijalankan'); + return Command::SUCCESS; + } + + // Konfirmasi sebelum menjalankan + if (!$this->confirm('Apakah Anda yakin ingin menjalankan job update seluruh kartu ATM?')) { + $this->info('Operasi dibatalkan'); + return Command::SUCCESS; + } + + // Dispatch job + $job = new UpdateAllAtmCardsBatchJob($syncLogId, $batchSize, $filters); + $job->onQueue($queueName); + dispatch($job); + + $this->info('Job berhasil dijadwalkan!'); + $this->info("Queue: {$queueName}"); + $this->info('Gunakan command berikut untuk memonitor:'); + $this->info('php artisan queue:work --queue=' . $queueName); + + Log::info('Command update seluruh kartu ATM selesai', [ + 'sync_log_id' => $syncLogId, + 'batch_size' => $batchSize, + 'queue' => $queueName, + 'filters' => $filters + ]); + + return Command::SUCCESS; + + } catch (Exception $e) { + $this->error('Terjadi error: ' . $e->getMessage()); + Log::error('Error dalam command update seluruh kartu ATM: ' . $e->getMessage(), [ + 'file' => $e->getFile(), + 'line' => $e->getLine() + ]); + + return Command::FAILURE; + } + } +} diff --git a/app/Http/Controllers/PrintStatementController.php b/app/Http/Controllers/PrintStatementController.php index c91ed83..46ff571 100644 --- a/app/Http/Controllers/PrintStatementController.php +++ b/app/Http/Controllers/PrintStatementController.php @@ -12,6 +12,7 @@ use Modules\Webstatement\{ Http\Requests\PrintStatementRequest, Mail\StatementEmail, Models\PrintStatementLog, + Models\Account, Models\AccountBalance, Jobs\ExportStatementPeriodJob }; @@ -24,9 +25,15 @@ use ZipArchive; */ public function index(Request $request) { - $branches = Branch::orderBy('name')->get(); + $branches = Branch::whereNotNull('customer_company') + ->where('code', '!=', 'ID0019999') + ->orderBy('name') + ->get(); - return view('webstatement::statements.index', compact('branches')); + $branch = Branch::find(Auth::user()->branch_id); + $multiBranch = session('MULTI_BRANCH') ?? false; + + return view('webstatement::statements.index', compact('branches', 'branch', 'multiBranch')); } /** @@ -35,33 +42,67 @@ use ZipArchive; */ public function store(PrintStatementRequest $request) { + // Add account verification before storing + $accountNumber = $request->input('account_number'); // Assuming this is the field name for account number + // First, check if the account exists and get branch information + $account = Account::where('account_number', $accountNumber)->first(); + if ($account) { + $branch_code = $account->branch_code; + $userBranchId = session('branch_id'); // Assuming branch ID is stored in session + $multiBranch = session('MULTI_BRANCH'); + + if (!$multiBranch) { + // Check if account branch matches user's branch + if ($account->branch_id !== $userBranchId) { + return redirect()->route('statements.index') + ->with('error', 'Nomor rekening tidak sesuai dengan cabang Anda. Transaksi tidak dapat dilanjutkan.'); + } + } + + // Check if account belongs to restricted branch ID0019999 + if ($account->branch_id === 'ID0019999') { + return redirect()->route('statements.index') + ->with('error', 'Nomor rekening terdaftar pada cabang khusus. Silakan hubungi bagian HC untuk informasi lebih lanjut.'); + } + + // If all checks pass, proceed with storing data + // Your existing store logic here + + } else { + // Account not found + return redirect()->route('statements.index') + ->with('error', 'Nomor rekening tidak ditemukan dalam sistem.'); + } + DB::beginTransaction(); try { $validated = $request->validated(); // Add user tracking data dan field baru untuk single account request - $validated['user_id'] = Auth::id(); - $validated['created_by'] = Auth::id(); - $validated['ip_address'] = $request->ip(); - $validated['user_agent'] = $request->userAgent(); - $validated['request_type'] = 'single_account'; // Default untuk request manual - $validated['status'] = 'pending'; // Status awal - $validated['total_accounts'] = 1; // Untuk single account + $validated['user_id'] = Auth::id(); + $validated['created_by'] = Auth::id(); + $validated['ip_address'] = $request->ip(); + $validated['user_agent'] = $request->userAgent(); + $validated['request_type'] = 'single_account'; // Default untuk request manual + $validated['status'] = 'pending'; // Status awal + $validated['authorization_status'] = 'approved'; // Status otorisasi awal + $validated['total_accounts'] = 1; // Untuk single account $validated['processed_accounts'] = 0; $validated['success_count'] = 0; $validated['failed_count'] = 0; $validated['stmt_sent_type'] = implode(',', $request->input('stmt_sent_type')); + $validated['branch_code'] = $branch_code; // Awal tidak tersedia // Create the statement log $statement = PrintStatementLog::create($validated); // Log aktivitas Log::info('Statement request created', [ - 'statement_id' => $statement->id, - 'user_id' => Auth::id(), + 'statement_id' => $statement->id, + 'user_id' => Auth::id(), 'account_number' => $statement->account_number, - 'request_type' => $statement->request_type + 'request_type' => $statement->request_type ]); // Process statement availability check @@ -69,21 +110,26 @@ use ZipArchive; if(!$statement->is_available){ $this->printStatementRekening($statement->account_number,$statement->period_from,$statement->period_to,$statement->stmt_sent_type); } + + $statement = PrintStatementLog::find($statement->id); + if($statement->email){ + $this->sendEmail($statement->id); + } DB::commit(); return redirect()->route('statements.index') - ->with('success', 'Statement request has been created successfully.'); + ->with('success', 'Statement request has been created successfully.'); } catch (Exception $e) { DB::rollBack(); Log::error('Failed to create statement request', [ - 'error' => $e->getMessage(), + 'error' => $e->getMessage(), 'user_id' => Auth::id() ]); return redirect()->back() - ->withInput() - ->with('error', 'Failed to create statement request: ' . $e->getMessage()); + ->withInput() + ->with('error', 'Failed to create statement request: ' . $e->getMessage()); } } @@ -105,19 +151,26 @@ use ZipArchive; DB::beginTransaction(); try { - $disk = Storage::disk('sftpStatement'); - $filePath = "{$statement->period_from}/Print/{$statement->branch_code}/{$statement->account_number}.pdf"; + $disk = Storage::disk('sftpStatement'); + $filePath = "{$statement->period_from}/{$statement->branch_code}/{$statement->account_number}_{$statement->period_from}.pdf"; + + // Log untuk debugging + Log::info('Checking SFTP file path', [ + 'file_path' => $filePath, + 'sftp_root' => config('filesystems.disks.sftpStatement.root'), + 'full_path' => config('filesystems.disks.sftpStatement.root') . '/' . $filePath + ]); if ($statement->is_period_range && $statement->period_to) { $periodFrom = Carbon::createFromFormat('Ym', $statement->period_from); - $periodTo = Carbon::createFromFormat('Ym', $statement->period_to); + $periodTo = Carbon::createFromFormat('Ym', $statement->period_to); - $missingPeriods = []; + $missingPeriods = []; $availablePeriods = []; for ($period = clone $periodFrom; $period->lte($periodTo); $period->addMonth()) { $periodFormatted = $period->format('Ym'); - $periodPath = $periodFormatted . "/Print/{$statement->branch_code}/{$statement->account_number}.pdf"; + $periodPath = $periodFormatted . "/{$statement->branch_code}/{$statement->account_number}_{$periodFormatted}.pdf"; if ($disk->exists($periodPath)) { $availablePeriods[] = $periodFormatted; @@ -130,55 +183,55 @@ use ZipArchive; $notes = "Missing periods: " . implode(', ', $missingPeriods); $statement->update([ 'is_available' => false, - 'remarks' => $notes, - 'updated_by' => Auth::id(), - 'status' => 'failed' + 'remarks' => $notes, + 'updated_by' => Auth::id(), + 'status' => 'failed' ]); Log::warning('Statement not available - missing periods', [ - 'statement_id' => $statement->id, + 'statement_id' => $statement->id, 'missing_periods' => $missingPeriods ]); } else { $statement->update([ - 'is_available' => true, - 'updated_by' => Auth::id(), - 'status' => 'completed', + 'is_available' => true, + 'updated_by' => Auth::id(), + 'status' => 'completed', 'processed_accounts' => 1, - 'success_count' => 1 + 'success_count' => 1 ]); Log::info('Statement available - all periods found', [ - 'statement_id' => $statement->id, + 'statement_id' => $statement->id, 'available_periods' => $availablePeriods ]); } } else if ($disk->exists($filePath)) { $statement->update([ - 'is_available' => true, - 'updated_by' => Auth::id(), - 'status' => 'completed', + 'is_available' => true, + 'updated_by' => Auth::id(), + 'status' => 'completed', 'processed_accounts' => 1, - 'success_count' => 1 + 'success_count' => 1 ]); Log::info('Statement available', [ 'statement_id' => $statement->id, - 'file_path' => $filePath + 'file_path' => $filePath ]); } else { $statement->update([ - 'is_available' => false, - 'updated_by' => Auth::id(), - 'status' => 'failed', + 'is_available' => false, + 'updated_by' => Auth::id(), + 'status' => 'failed', 'processed_accounts' => 1, - 'failed_count' => 1, - 'error_message' => 'Statement file not found' + 'failed_count' => 1, + 'error_message' => 'Statement file not found' ]); Log::warning('Statement not available', [ 'statement_id' => $statement->id, - 'file_path' => $filePath + 'file_path' => $filePath ]); } @@ -188,14 +241,14 @@ use ZipArchive; Log::error('Error checking statement availability', [ 'statement_id' => $statement->id, - 'error' => $e->getMessage() + 'error' => $e->getMessage() ]); $statement->update([ - 'is_available' => false, - 'status' => 'failed', + 'is_available' => false, + 'status' => 'failed', 'error_message' => $e->getMessage(), - 'updated_by' => Auth::id() + 'updated_by' => Auth::id() ]); } } @@ -226,33 +279,161 @@ use ZipArchive; $statement->update([ 'is_downloaded' => true, 'downloaded_at' => now(), - 'updated_by' => Auth::id() + 'updated_by' => Auth::id() ]); Log::info('Statement downloaded', [ - 'statement_id' => $statement->id, - 'user_id' => Auth::id(), + 'statement_id' => $statement->id, + 'user_id' => Auth::id(), 'account_number' => $statement->account_number ]); DB::commit(); // Generate or fetch the statement file - $disk = Storage::disk('sftpStatement'); - $filePath = "{$statement->period_from}/Print/{$statement->branch_code}/{$statement->account_number}.pdf"; + $disk = Storage::disk('sftpStatement'); + $filePath = "{$statement->period_from}/{$statement->branch_code}/{$statement->account_number}_{$statement->period_from}.pdf"; if ($statement->is_period_range && $statement->period_to) { - // Handle period range download (existing logic) - // ... existing zip creation logic ... + // Log: Memulai proses download period range + Log::info('Starting period range download', [ + 'statement_id' => $statement->id, + 'period_from' => $statement->period_from, + 'period_to' => $statement->period_to + ]); + + /** + * Handle period range download dengan membuat zip file + * yang berisi semua statement dalam rentang periode + */ + $periodFrom = Carbon::createFromFormat('Ym', $statement->period_from); + $periodTo = Carbon::createFromFormat('Ym', $statement->period_to); + + // Loop through each month in the range + $missingPeriods = []; + $availablePeriods = []; + + for ($period = clone $periodFrom; $period->lte($periodTo); $period->addMonth()) { + $periodFormatted = $period->format('Ym'); + $periodPath = $periodFormatted . "/{$statement->branch_code}/{$statement->account_number}_{$periodFormatted}.pdf"; + + if ($disk->exists($periodPath)) { + $availablePeriods[] = $periodFormatted; + Log::info('Period available for download', [ + 'period' => $periodFormatted, + 'path' => $periodPath + ]); + } else { + $missingPeriods[] = $periodFormatted; + Log::warning('Period not available for download', [ + 'period' => $periodFormatted, + 'path' => $periodPath + ]); + } + } + + // If any period is available, create a zip and download it + if (count($availablePeriods) > 0) { + /** + * Membuat zip file temporary untuk download + * dengan semua statement yang tersedia dalam periode + */ + $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); + Log::info('Created temp directory for zip files'); + } + + // Create a new zip archive + $zip = new ZipArchive(); + if ($zip->open($zipFilePath, ZipArchive::CREATE | ZipArchive::OVERWRITE) === true) { + Log::info('Zip archive created successfully', ['zip_path' => $zipFilePath]); + + // Add each available statement to the zip + foreach ($availablePeriods as $period) { + $periodFilePath = "{$period}/{$statement->branch_code}/{$statement->account_number}_{$period}.pdf"; + $localFilePath = storage_path("app/temp/{$statement->account_number}_{$period}.pdf"); + + try { + // Download the file from SFTP to local storage temporarily + file_put_contents($localFilePath, $disk->get($periodFilePath)); + + // Add the file to the zip + $zip->addFile($localFilePath, "{$statement->account_number}_{$period}.pdf"); + + Log::info('Added file to zip', [ + 'period' => $period, + 'local_path' => $localFilePath + ]); + } catch (Exception $e) { + Log::error('Failed to add file to zip', [ + 'period' => $period, + 'error' => $e->getMessage() + ]); + } + } + + $zip->close(); + Log::info('Zip archive closed successfully'); + + // Return the zip file for download + $response = response()->download($zipFilePath, $zipFileName)->deleteFileAfterSend(true); + + // Clean up temporary PDF files + foreach ($availablePeriods as $period) { + $localFilePath = storage_path("app/temp/{$statement->account_number}_{$period}.pdf"); + if (file_exists($localFilePath)) { + unlink($localFilePath); + Log::info('Cleaned up temporary file', ['file' => $localFilePath]); + } + } + + Log::info('Period range download completed successfully', [ + 'statement_id' => $statement->id, + 'available_periods' => count($availablePeriods), + 'missing_periods' => count($missingPeriods) + ]); + + return $response; + } else { + Log::error('Failed to create zip archive', ['zip_path' => $zipFilePath]); + return back()->with('error', 'Failed to create zip archive for download.'); + } + } else { + Log::warning('No statements available for download in period range', [ + 'statement_id' => $statement->id, + 'missing_periods' => $missingPeriods + ]); + return back()->with('error', 'No statements available for download in the specified period range.'); + } } else if ($disk->exists($filePath)) { + /** + * Handle single period download + * Download file PDF tunggal untuk periode tertentu + */ + Log::info('Single period download', [ + 'statement_id' => $statement->id, + 'file_path' => $filePath + ]); + return $disk->download($filePath, "{$statement->account_number}_{$statement->period_from}.pdf"); + } else { + Log::warning('Statement file not found', [ + 'statement_id' => $statement->id, + 'file_path' => $filePath + ]); + return back()->with('error', 'Statement file not found.'); } } catch (Exception $e) { DB::rollBack(); Log::error('Failed to download statement', [ 'statement_id' => $statement->id, - 'error' => $e->getMessage() + 'error' => $e->getMessage(), + 'trace' => $e->getTraceAsString() ]); return back()->with('error', 'Failed to download statement: ' . $e->getMessage()); @@ -298,6 +479,13 @@ use ZipArchive; // Retrieve data from the database $query = PrintStatementLog::query(); + if (!auth()->user()->hasRole('administrator')) { + $query->where(function($q) { + $q->where('user_id', Auth::id()) + ->orWhere('branch_code', Auth::user()->branch->code); + }); + } + // Apply search filter if provided if ($request->has('search') && !empty($request->get('search'))) { $search = $request->get('search'); @@ -340,13 +528,13 @@ use ZipArchive; // Map frontend column names to database column names if needed $columnMap = [ - 'branch' => 'branch_code', - 'account' => 'account_number', - 'period' => 'period_from', - 'auth_status' => 'authorization_status', + 'branch' => 'branch_code', + 'account' => 'account_number', + 'period' => 'period_from', + 'auth_status' => 'authorization_status', 'request_type' => 'request_type', - 'status' => 'status', - 'remarks' => 'remarks', + 'status' => 'status', + 'remarks' => 'remarks', ]; $dbColumn = $columnMap[$column] ?? $column; @@ -430,6 +618,7 @@ use ZipArchive; public function sendEmail($id) { $statement = PrintStatementLog::findOrFail($id); + // Check if statement has email if (empty($statement->email)) { return redirect()->back()->with('error', 'No email address provided for this statement.'); @@ -442,7 +631,7 @@ use ZipArchive; try { $disk = Storage::disk('sftpStatement'); - $filePath = "{$statement->period_from}/Print/{$statement->branch_code}/{$statement->account_number}.pdf"; + $filePath = "{$statement->period_from}/{$statement->branch_code}/{$statement->account_number}_{$statement->period_from}.pdf"; if ($statement->is_period_range && $statement->period_to) { $periodFrom = Carbon::createFromFormat('Ym', $statement->period_from); @@ -454,7 +643,7 @@ use ZipArchive; for ($period = clone $periodFrom; $period->lte($periodTo); $period->addMonth()) { $periodFormatted = $period->format('Ym'); - $periodPath = $periodFormatted . "/Print/{$statement->branch_code}/{$statement->account_number}.pdf"; + $periodPath = $periodFormatted . "/{$statement->branch_code}/{$statement->account_number}_{$periodFormatted}.pdf"; if ($disk->exists($periodPath)) { $availablePeriods[] = $periodFormatted; @@ -479,7 +668,7 @@ use ZipArchive; if ($zip->open($zipFilePath, ZipArchive::CREATE | ZipArchive::OVERWRITE) === true) { // Add each available statement to the zip foreach ($availablePeriods as $period) { - $filePath = "{$period}/Print/{$statement->branch_code}/{$statement->account_number}.pdf"; + $filePath = "{$period}/{$statement->branch_code}/{$statement->account_number}_{$statement->period_from}.pdf"; $localFilePath = storage_path("app/temp/{$statement->account_number}_{$period}.pdf"); // Download the file from SFTP to local storage temporarily @@ -545,8 +734,8 @@ use ZipArchive; Log::info('Statement email sent successfully', [ 'statement_id' => $statement->id, - 'email' => $statement->email, - 'user_id' => Auth::id() + 'email' => $statement->email, + 'user_id' => Auth::id() ]); DB::commit(); diff --git a/app/Http/Requests/PrintStatementRequest.php b/app/Http/Requests/PrintStatementRequest.php index cd6835b..afdfe4d 100644 --- a/app/Http/Requests/PrintStatementRequest.php +++ b/app/Http/Requests/PrintStatementRequest.php @@ -21,7 +21,6 @@ class PrintStatementRequest extends FormRequest public function rules(): array { $rules = [ - 'branch_code' => ['required', 'string', 'exists:branches,code'], 'account_number' => ['required', 'string'], 'is_period_range' => ['sometimes', 'boolean'], 'email' => ['nullable', 'email'], @@ -36,6 +35,7 @@ class PrintStatementRequest extends FormRequest function ($attribute, $value, $fail) { $query = Statement::where('account_number', $this->input('account_number')) ->where('authorization_status', '!=', 'rejected') + ->where('is_available', true) ->where('period_from', $value); // If this is an update request, exclude the current record @@ -77,8 +77,6 @@ class PrintStatementRequest extends FormRequest public function messages(): array { return [ - 'branch_code.required' => 'Branch code is required', - 'branch_code.exists' => 'Selected branch does not exist', 'account_number.required' => 'Account number is required', 'period_from.required' => 'Period is required', 'period_from.regex' => 'Period must be in YYYYMM format', @@ -106,13 +104,13 @@ class PrintStatementRequest extends FormRequest $this->merge([ 'period_to' => substr($this->period_to, 0, 4) . substr($this->period_to, 5, 2), ]); - } - // Convert is_period_range to boolean if it exists - if ($this->has('period_to')) { - $this->merge([ - 'is_period_range' => true, - ]); + // Only set is_period_range to true if period_to is different from period_from + if ($this->period_to !== $this->period_from) { + $this->merge([ + 'is_period_range' => true, + ]); + } } // Set default request_type if not provided diff --git a/app/Jobs/GenerateBiayaKartuCsvJob.php b/app/Jobs/GenerateBiayaKartuCsvJob.php index 0924108..03cc175 100644 --- a/app/Jobs/GenerateBiayaKartuCsvJob.php +++ b/app/Jobs/GenerateBiayaKartuCsvJob.php @@ -63,7 +63,9 @@ $this->updateCsvLogStart(); // Generate CSV file - $result = $this->generateAtmCardCsv(); +// $result = $this->generateAtmCardCsv(); + + $result = $this->generateSingleAtmCardCsv(); // Update status CSV generation berhasil $this->updateCsvLogSuccess($result); @@ -443,4 +445,155 @@ Log::error('Pembuatan file CSV gagal: ' . $errorMessage); } + + /** + * Generate single CSV file with all ATM card data without branch separation + * + * @return array Information about the generated file and upload status + * @throws RuntimeException + */ + private function generateSingleAtmCardCsv(): array + { + Log::info('Memulai pembuatan file CSV tunggal untuk semua kartu ATM'); + + try { + // Ambil semua kartu yang memenuhi syarat + $cards = $this->getEligibleAtmCards(); + + if ($cards->isEmpty()) { + Log::warning('Tidak ada kartu ATM yang memenuhi syarat untuk periode ini'); + throw new RuntimeException('Tidak ada kartu ATM yang memenuhi syarat untuk diproses'); + } + + // Buat nama file dengan timestamp + $dateTime = now()->format('Ymd_Hi'); + $singleFilename = pathinfo($this->csvFilename, PATHINFO_FILENAME) + . '_ALL_BRANCHES_' + . $dateTime . '.' + . pathinfo($this->csvFilename, PATHINFO_EXTENSION); + + $filename = storage_path('app/' . $singleFilename); + + Log::info('Membuat file CSV: ' . $filename); + + // Buka file untuk menulis + $handle = fopen($filename, 'w+'); + if (!$handle) { + throw new RuntimeException("Tidak dapat membuat file CSV: $filename"); + } + + $recordCount = 0; + + try { + // Tulis semua kartu ke dalam satu file + foreach ($cards as $card) { + $fee = $this->determineCardFee($card); + $csvRow = $this->createCsvRow($card, $fee); + + if (fputcsv($handle, $csvRow, '|') === false) { + throw new RuntimeException("Gagal menulis data kartu ke file CSV: {$card->crdno}"); + } + + $recordCount++; + + // Log progress setiap 1000 record + if ($recordCount % 1000 === 0) { + Log::info("Progress: {$recordCount} kartu telah diproses"); + } + } + } finally { + fclose($handle); + } + + Log::info("Selesai menulis {$recordCount} kartu ke file CSV"); + + // Bersihkan file CSV (hapus double quotes) + $this->cleanupCsvFile($filename); + + Log::info('File CSV berhasil dibersihkan dari double quotes'); + + // Upload file ke SFTP (tanpa branch specific directory) + $uploadSuccess = true; // $this->uploadSingleFileToSftp($filename); + + $result = [ + 'localFilePath' => $filename, + 'recordCount' => $recordCount, + 'uploadToSftp' => $uploadSuccess, + 'timestamp' => now()->format('Y-m-d H:i:s'), + 'fileName' => $singleFilename + ]; + + Log::info('Pembuatan file CSV tunggal selesai', $result); + + return $result; + + } catch (Exception $e) { + Log::error('Error dalam generateSingleAtmCardCsv: ' . $e->getMessage(), [ + 'file' => $e->getFile(), + 'line' => $e->getLine(), + 'trace' => $e->getTraceAsString() + ]); + throw $e; + } + } + + /** + * Upload single CSV file to SFTP server without branch directory + * + * @param string $localFilePath Path to the local CSV file + * @return bool True if upload successful, false otherwise + */ + private function uploadSingleFileToSftp(string $localFilePath): bool + { + try { + Log::info('Memulai upload file tunggal ke SFTP: ' . $localFilePath); + + // Update status SFTP upload dimulai + $this->updateSftpLogStart(); + + // Ambil nama file dari path + $filename = basename($localFilePath); + + // Ambil konten file + $fileContent = file_get_contents($localFilePath); + if ($fileContent === false) { + Log::error("Tidak dapat membaca file untuk upload: {$localFilePath}"); + return false; + } + + // Dapatkan disk SFTP + $disk = Storage::disk('sftpKartu'); + + // Tentukan path tujuan di server SFTP (root directory) + $remotePath = env('BIAYA_KARTU_REMOTE_PATH', '/'); + $remoteFilePath = rtrim($remotePath, '/') . '/' . $filename; + + Log::info('Mengunggah ke path remote: ' . $remoteFilePath); + + // Upload file ke server SFTP + $result = $disk->put($remoteFilePath, $fileContent); + + if ($result) { + $this->updateSftpLogSuccess(); + Log::info("File CSV tunggal berhasil diunggah ke SFTP: {$remoteFilePath}"); + return true; + } else { + $errorMsg = "Gagal mengunggah file CSV tunggal ke SFTP: {$remoteFilePath}"; + $this->updateSftpLogFailed($errorMsg); + Log::error($errorMsg); + return false; + } + + } catch (Exception $e) { + $errorMsg = "Error saat mengunggah file tunggal ke SFTP: " . $e->getMessage(); + $this->updateSftpLogFailed($errorMsg); + + Log::error($errorMsg, [ + 'file' => $e->getFile(), + 'line' => $e->getLine(), + 'periode' => $this->periode + ]); + return false; + } + } } diff --git a/app/Jobs/ProcessStmtEntryDataJob.php b/app/Jobs/ProcessStmtEntryDataJob.php index 11e129b..f4f04c1 100644 --- a/app/Jobs/ProcessStmtEntryDataJob.php +++ b/app/Jobs/ProcessStmtEntryDataJob.php @@ -1,5 +1,4 @@ entryBatch)) { - // Process in smaller chunks for better memory management - foreach ($this->entryBatch as $entry) { - // Extract all stmt_entry_ids from the current chunk - $entryIds = array_column($entry, 'stmt_entry_id'); + $totalProcessed = 0; - // Delete existing records with these IDs to avoid conflicts - StmtEntry::whereIn('stmt_entry_id', $entryIds)->delete(); + // Process each entry data directly (tidak ada nested array) + foreach ($this->entryBatch as $entryData) { + // Validasi bahwa entryData adalah array dan memiliki stmt_entry_id + if (is_array($entryData) && isset($entryData['stmt_entry_id'])) { + // Gunakan updateOrCreate untuk menghindari duplicate key error + StmtEntry::updateOrCreate( + [ + 'stmt_entry_id' => $entryData['stmt_entry_id'] + ], + $entryData + ); - // Insert all records in the chunk at once - StmtEntry::insert($entry); + $totalProcessed++; + } else { + Log::warning('Invalid entry data structure', ['data' => $entryData]); + $this->errorCount++; + } } - // Reset entry batch after processing + DB::commit(); + + Log::info("Berhasil memproses {$totalProcessed} record dengan updateOrCreate"); + + // Reset entry batch after successful processing $this->entryBatch = []; } } catch (Exception $e) { + DB::rollback(); + Log::error("Error in saveBatch: " . $e->getMessage() . "\n" . $e->getTraceAsString()); $this->errorCount += count($this->entryBatch); + // Reset batch even if there's an error to prevent reprocessing the same failed records $this->entryBatch = []; + + throw $e; } } diff --git a/app/Jobs/SendStatementEmailJob.php b/app/Jobs/SendStatementEmailJob.php index 98a970d..1c2c83f 100644 --- a/app/Jobs/SendStatementEmailJob.php +++ b/app/Jobs/SendStatementEmailJob.php @@ -1,424 +1,425 @@ period = $period; - $this->requestType = $requestType; - $this->targetValue = $targetValue; - $this->batchId = $batchId ?? uniqid('batch_'); - $this->logId = $logId; + use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; - Log::info('SendStatementEmailJob created with PHPMailer', [ - 'period' => $this->period, - 'request_type' => $this->requestType, - 'target_value' => $this->targetValue, - 'batch_id' => $this->batchId, - 'log_id' => $this->logId - ]); - } + protected $period; + protected $requestType; + protected $targetValue; // account_number, branch_code, atau null untuk all + protected $batchId; + protected $logId; - /** - * Menjalankan job pengiriman email statement - */ - public function handle(): void - { - Log::info('Starting SendStatementEmailJob execution', [ - 'batch_id' => $this->batchId, - 'period' => $this->period, - 'request_type' => $this->requestType, - 'target_value' => $this->targetValue - ]); + /** + * Membuat instance job baru + * + * @param string $period Format: YYYYMM + * @param string $requestType 'single_account', 'branch', 'all_branches' + * @param string|null $targetValue account_number untuk single, branch_code untuk branch, null untuk all + * @param string|null $batchId ID batch untuk tracking + * @param int|null $logId ID log untuk update progress + */ + public function __construct($period, $requestType = 'single_account', $targetValue = null, $batchId = null, $logId = null) + { + $this->period = $period; + $this->requestType = $requestType; + $this->targetValue = $targetValue; + $this->batchId = $batchId ?? uniqid('batch_'); + $this->logId = $logId; - DB::beginTransaction(); + Log::info('SendStatementEmailJob created', [ + 'period' => $this->period, + 'request_type' => $this->requestType, + 'target_value' => $this->targetValue, + 'batch_id' => $this->batchId, + 'log_id' => $this->logId + ]); + } - try { - // Update log status menjadi processing - $this->updateLogStatus('processing', ['started_at' => now()]); + /** + * Menjalankan job pengiriman email statement + */ + public function handle() + : void + { + Log::info('Starting SendStatementEmailJob execution', [ + 'batch_id' => $this->batchId, + 'period' => $this->period, + 'request_type' => $this->requestType, + 'target_value' => $this->targetValue + ]); - // Ambil accounts berdasarkan request type - $accounts = $this->getAccountsByRequestType(); + DB::beginTransaction(); - if ($accounts->isEmpty()) { - Log::warning('No accounts with email found', [ - 'period' => $this->period, - 'request_type' => $this->requestType, - 'target_value' => $this->targetValue, - 'batch_id' => $this->batchId + try { + // Update log status menjadi processing + $this->updateLogStatus('processing', ['started_at' => now()]); + + // Ambil accounts berdasarkan request type + $accounts = $this->getAccountsByRequestType(); + + if ($accounts->isEmpty()) { + Log::warning('No accounts with email found', [ + 'period' => $this->period, + 'request_type' => $this->requestType, + 'target_value' => $this->targetValue, + 'batch_id' => $this->batchId + ]); + + $this->updateLogStatus('completed', [ + 'completed_at' => now(), + 'total_accounts' => 0, + 'processed_accounts' => 0, + 'success_count' => 0, + 'failed_count' => 0 + ]); + + DB::commit(); + return; + } + + // Update total accounts + $this->updateLogStatus('processing', [ + 'total_accounts' => $accounts->count(), + 'target_accounts' => $accounts->pluck('account_number')->toArray() ]); - $this->updateLogStatus('completed', [ - 'completed_at' => now(), - 'total_accounts' => 0, - 'processed_accounts' => 0, - 'success_count' => 0, - 'failed_count' => 0 + $successCount = 0; + $failedCount = 0; + $processedCount = 0; + + foreach ($accounts as $account) { + try { + $this->sendStatementEmail($account); + $successCount++; + + Log::info('Statement email sent successfully', [ + 'account_number' => $account->account_number, + 'branch_code' => $account->branch_code, + 'email' => $this->getEmailForAccount($account), + 'batch_id' => $this->batchId + ]); + } catch (Exception $e) { + $failedCount++; + + Log::error('Failed to send statement email', [ + 'account_number' => $account->account_number, + 'branch_code' => $account->branch_code, + 'email' => $this->getEmailForAccount($account), + 'error' => $e->getMessage(), + 'batch_id' => $this->batchId + ]); + } + + $processedCount++; + + // Update progress setiap 10 account atau di akhir + if ($processedCount % 10 === 0 || $processedCount === $accounts->count()) { + $this->updateLogStatus('processing', [ + 'processed_accounts' => $processedCount, + 'success_count' => $successCount, + 'failed_count' => $failedCount + ]); + } + } + + // Update status final + $finalStatus = $failedCount === 0 ? 'completed' : ($successCount === 0 ? 'failed' : 'completed'); + $this->updateLogStatus($finalStatus, [ + 'completed_at' => now(), + 'processed_accounts' => $processedCount, + 'success_count' => $successCount, + 'failed_count' => $failedCount ]); DB::commit(); + + Log::info('SendStatementEmailJob completed', [ + 'batch_id' => $this->batchId, + 'total_accounts' => $accounts->count(), + 'success_count' => $successCount, + 'failed_count' => $failedCount, + 'final_status' => $finalStatus + ]); + + } catch (Exception $e) { + DB::rollBack(); + + $this->updateLogStatus('failed', [ + 'completed_at' => now(), + 'error_message' => $e->getMessage() + ]); + + Log::error('SendStatementEmailJob failed', [ + 'batch_id' => $this->batchId, + 'error' => $e->getMessage(), + 'trace' => $e->getTraceAsString() + ]); + + throw $e; + } + } + + /** + * Update status log + */ + private function updateLogStatus($status, $additionalData = []) + { + if (!$this->logId) { return; } - // Update total accounts - $this->updateLogStatus('processing', [ - 'total_accounts' => $accounts->count(), - 'target_accounts' => $accounts->pluck('account_number')->toArray() + try { + $updateData = array_merge(['status' => $status], $additionalData); + PrintStatementLog::where('id', $this->logId)->update($updateData); + } catch (Exception $e) { + Log::error('Failed to update log status', [ + 'log_id' => $this->logId, + 'status' => $status, + 'error' => $e->getMessage() + ]); + } + } + + /** + * Mengambil accounts berdasarkan request type + */ + private function getAccountsByRequestType() + { + Log::info('Fetching accounts by request type', [ + 'period' => $this->period, + 'request_type' => $this->requestType, + 'target_value' => $this->targetValue ]); - $successCount = 0; - $failedCount = 0; - $processedCount = 0; + $query = Account::with('customer') + ->where('stmt_sent_type', 'BY.EMAIL'); - foreach ($accounts as $account) { - try { - $this->sendStatementEmail($account); - $successCount++; + switch ($this->requestType) { + case 'single_account': + if ($this->targetValue) { + $query->where('account_number', $this->targetValue); + } + break; - Log::info('Statement email sent successfully', [ - 'account_number' => $account->account_number, - 'branch_code' => $account->branch_code, - 'email' => $this->getEmailForAccount($account), - 'batch_id' => $this->batchId - ]); - } catch (\Exception $e) { - $failedCount++; + case 'branch': + if ($this->targetValue) { + $query->where('branch_code', $this->targetValue); + } + break; - Log::error('Failed to send statement email', [ - 'account_number' => $account->account_number, - 'branch_code' => $account->branch_code, - 'email' => $this->getEmailForAccount($account), - 'error' => $e->getMessage(), - 'batch_id' => $this->batchId - ]); - } + case 'all_branches': + // Tidak ada filter tambahan, ambil semua + break; - $processedCount++; - - // Update progress setiap 10 account atau di akhir - if ($processedCount % 10 === 0 || $processedCount === $accounts->count()) { - $this->updateLogStatus('processing', [ - 'processed_accounts' => $processedCount, - 'success_count' => $successCount, - 'failed_count' => $failedCount - ]); - } + default: + throw new InvalidArgumentException("Invalid request type: {$this->requestType}"); } - // Update status final - $finalStatus = $failedCount === 0 ? 'completed' : ($successCount === 0 ? 'failed' : 'completed'); - $this->updateLogStatus($finalStatus, [ - 'completed_at' => now(), - 'processed_accounts' => $processedCount, - 'success_count' => $successCount, - 'failed_count' => $failedCount + $accounts = $query->get(); + + // Filter accounts yang memiliki email + $accountsWithEmail = $accounts->filter(function ($account) { + return !empty($account->stmt_email) || + ($account->customer && !empty($account->customer->email)); + }); + + Log::info('Accounts with email retrieved', [ + 'total_accounts' => $accounts->count(), + 'accounts_with_email' => $accountsWithEmail->count(), + 'request_type' => $this->requestType, + 'batch_id' => $this->batchId ]); - DB::commit(); + return $accountsWithEmail; + } - Log::info('SendStatementEmailJob completed', [ - 'batch_id' => $this->batchId, - 'total_accounts' => $accounts->count(), - 'success_count' => $successCount, - 'failed_count' => $failedCount, - 'final_status' => $finalStatus + /** + * Mengirim email statement untuk account tertentu + * + * @param Account $account + * + * @return void + * @throws \Exception + */ + private function sendStatementEmail(Account $account) + { + // Dapatkan email untuk pengiriman + $emailAddress = $this->getEmailForAccount($account); + + if (!$emailAddress) { + throw new Exception("No email address found for account {$account->account_number}"); + } + + // Cek apakah file PDF ada + $pdfPath = $this->getPdfPath($account->account_number, $account->branch_code); + + if (!Storage::exists($pdfPath)) { + throw new Exception("PDF file not found: {$pdfPath}"); + } + + // Buat atau update log statement + $statementLog = $this->createOrUpdateStatementLog($account); + + // Dapatkan path absolut file + $absolutePdfPath = Storage::path($pdfPath); + + // Kirim email + // Add delay between email sends to prevent rate limiting + sleep(1); // 2 second delay + Mail::to($emailAddress)->send( + new StatementEmail($statementLog, $absolutePdfPath, false) + ); + + // Update status log dengan email yang digunakan + $statementLog->update([ + 'email_sent_at' => now(), + 'email_status' => 'sent', + 'email_address' => $emailAddress // Simpan email yang digunakan untuk tracking ]); - } catch (\Exception $e) { - DB::rollBack(); + Log::info('Email sent for account', [ + 'account_number' => $account->account_number, + 'branch_code' => $account->branch_code, + 'email' => $emailAddress, + 'email_source' => !empty($account->stmt_email) ? 'account.stmt_email' : 'customer.email', + 'pdf_path' => $pdfPath, + 'batch_id' => $this->batchId + ]); + } + /** + * Mendapatkan email untuk pengiriman statement + * + * @param Account $account + * + * @return string|null + */ + private function getEmailForAccount(Account $account) + { + // Prioritas pertama: stmt_email dari account + if (!empty($account->stmt_email)) { + Log::info('Using stmt_email from account', [ + 'account_number' => $account->account_number, + 'email' => $account->stmt_email, + 'batch_id' => $this->batchId + ]); + return $account->stmt_email; + } + + // Prioritas kedua: email dari customer + if ($account->customer && !empty($account->customer->email)) { + Log::info('Using email from customer', [ + 'account_number' => $account->account_number, + 'customer_code' => $account->customer_code, + 'email' => $account->customer->email, + 'batch_id' => $this->batchId + ]); + return $account->customer->email; + } + + Log::warning('No email found for account', [ + 'account_number' => $account->account_number, + 'customer_code' => $account->customer_code, + 'batch_id' => $this->batchId + ]); + + return null; + } + + /** + * Mendapatkan path file PDF statement + * + * @param string $accountNumber + * @param string $branchCode + * + * @return string + */ + private function getPdfPath($accountNumber, $branchCode) + { + return "combine/{$this->period}/{$branchCode}/{$accountNumber}_{$this->period}.pdf"; + } + + /** + * Membuat atau update log statement + * + * @param Account $account + * + * @return PrintStatementLog + */ + private function createOrUpdateStatementLog(Account $account) + { + $emailAddress = $this->getEmailForAccount($account); + + $logData = [ + 'account_number' => $account->account_number, + 'customer_code' => $account->customer_code, + 'branch_code' => $account->branch_code, + 'period' => $this->period, + 'print_date' => now(), + 'batch_id' => $this->batchId, + 'email_address' => $emailAddress, + 'email_source' => !empty($account->stmt_email) ? 'account' : 'customer' + ]; + + $statementLog = PrintStatementLog::updateOrCreate( + [ + 'account_number' => $account->account_number, + 'period_from' => $this->period, + 'period_to' => $this->period + ], + $logData + ); + + Log::info('Statement log created/updated', [ + 'log_id' => $statementLog->id, + 'account_number' => $account->account_number, + 'email_address' => $emailAddress, + 'batch_id' => $this->batchId + ]); + + return $statementLog; + } + + /** + * Handle job failure + */ + public function failed(Throwable $exception) + { $this->updateLogStatus('failed', [ - 'completed_at' => now(), - 'error_message' => $e->getMessage() + 'completed_at' => now(), + 'error_message' => $exception->getMessage() ]); - Log::error('SendStatementEmailJob failed', [ - 'batch_id' => $this->batchId, - 'error' => $e->getMessage(), - 'trace' => $e->getTraceAsString() - ]); - - throw $e; - } - } - - /** - * Mengambil accounts berdasarkan request type - */ - private function getAccountsByRequestType() - { - Log::info('Fetching accounts by request type', [ - 'period' => $this->period, - 'request_type' => $this->requestType, - 'target_value' => $this->targetValue - ]); - - $query = Account::with('customer') - ->where('stmt_sent_type', 'BY.EMAIL'); - - switch ($this->requestType) { - case 'single_account': - if ($this->targetValue) { - $query->where('account_number', $this->targetValue); - } - break; - - case 'branch': - if ($this->targetValue) { - $query->where('branch_code', $this->targetValue); - } - break; - - case 'all_branches': - // Tidak ada filter tambahan, ambil semua - break; - - default: - throw new \InvalidArgumentException("Invalid request type: {$this->requestType}"); - } - - $accounts = $query->get(); - - // Filter accounts yang memiliki email - $accountsWithEmail = $accounts->filter(function ($account) { - return !empty($account->stmt_email) || - ($account->customer && !empty($account->customer->email)); - }); - - Log::info('Accounts with email retrieved', [ - 'total_accounts' => $accounts->count(), - 'accounts_with_email' => $accountsWithEmail->count(), - 'request_type' => $this->requestType, - 'batch_id' => $this->batchId - ]); - - return $accountsWithEmail; - } - - /** - * Update status log - */ - private function updateLogStatus($status, $additionalData = []) - { - if (!$this->logId) { - return; - } - - try { - $updateData = array_merge(['status' => $status], $additionalData); - PrintStatementLog::where('id', $this->logId)->update($updateData); - } catch (\Exception $e) { - Log::error('Failed to update log status', [ - 'log_id' => $this->logId, - 'status' => $status, - 'error' => $e->getMessage() + Log::error('SendStatementEmailJob failed permanently', [ + 'batch_id' => $this->batchId, + 'period' => $this->period, + 'request_type' => $this->requestType, + 'target_value' => $this->targetValue, + 'error' => $exception->getMessage(), + 'trace' => $exception->getTraceAsString() ]); } } - - /** - * Mendapatkan email untuk pengiriman statement - * - * @param Account $account - * @return string|null - */ - private function getEmailForAccount(Account $account) - { - // Prioritas pertama: stmt_email dari account - if (!empty($account->stmt_email)) { - Log::info('Using stmt_email from account', [ - 'account_number' => $account->account_number, - 'email' => $account->stmt_email, - 'batch_id' => $this->batchId - ]); - return $account->stmt_email; - } - - // Prioritas kedua: email dari customer - if ($account->customer && !empty($account->customer->email)) { - Log::info('Using email from customer', [ - 'account_number' => $account->account_number, - 'customer_code' => $account->customer_code, - 'email' => $account->customer->email, - 'batch_id' => $this->batchId - ]); - return $account->customer->email; - } - - Log::warning('No email found for account', [ - 'account_number' => $account->account_number, - 'customer_code' => $account->customer_code, - 'batch_id' => $this->batchId - ]); - - return null; - } - - /** - * Mengirim email statement untuk account tertentu - * - * @param Account $account - * @return void - * @throws \Exception - */ - private function sendStatementEmail(Account $account) - { - // Dapatkan email untuk pengiriman - $emailAddress = $this->getEmailForAccount($account); - - if (!$emailAddress) { - throw new \Exception("No email address found for account {$account->account_number}"); - } - - // Cek apakah file PDF ada - $pdfPath = $this->getPdfPath($account->account_number, $account->branch_code); - - if (!Storage::exists($pdfPath)) { - throw new \Exception("PDF file not found: {$pdfPath}"); - } - - // Buat atau update log statement - $statementLog = $this->createOrUpdateStatementLog($account); - - // Dapatkan path absolut file - $absolutePdfPath = Storage::path($pdfPath); - - // Buat instance StatementEmail dengan PHPMailer - $statementEmail = new StatementEmail($statementLog, $absolutePdfPath, false); - - // Kirim email menggunakan PHPMailer - $emailSent = $statementEmail->send($emailAddress); - - if (!$emailSent) { - throw new \Exception("Failed to send email to {$emailAddress} for account {$account->account_number}"); - } - - // Update status log dengan email yang digunakan - $statementLog->update([ - 'email_sent_at' => now(), - 'email_status' => 'sent', - 'email_address' => $emailAddress // Simpan email yang digunakan untuk tracking - ]); - - Log::info('Email sent via PHPMailer for account', [ - 'account_number' => $account->account_number, - 'branch_code' => $account->branch_code, - 'email' => $emailAddress, - 'email_source' => !empty($account->stmt_email) ? 'account.stmt_email' : 'customer.email', - 'pdf_path' => $pdfPath, - 'batch_id' => $this->batchId - ]); - - // Add delay between email sends to prevent rate limiting - sleep(2); // 2 second delay for NTLM/GSSAPI connections - } - - /** - * Mendapatkan path file PDF statement - * - * @param string $accountNumber - * @param string $branchCode - * @return string - */ - private function getPdfPath($accountNumber, $branchCode) - { - return "combine/{$this->period}/{$branchCode}/{$accountNumber}_{$this->period}.pdf"; - } - - /** - * Membuat atau update log statement - * - * @param Account $account - * @return PrintStatementLog - */ - private function createOrUpdateStatementLog(Account $account) - { - $emailAddress = $this->getEmailForAccount($account); - - $logData = [ - 'account_number' => $account->account_number, - 'customer_code' => $account->customer_code, - 'branch_code' => $account->branch_code, - 'period' => $this->period, - 'print_date' => now(), - 'batch_id' => $this->batchId, - 'email_address' => $emailAddress, - 'email_source' => !empty($account->stmt_email) ? 'account' : 'customer' - ]; - - $statementLog = PrintStatementLog::updateOrCreate( - [ - 'account_number' => $account->account_number, - 'period_from' => $this->period, - 'period_to' => $this->period - ], - $logData - ); - - Log::info('Statement log created/updated', [ - 'log_id' => $statementLog->id, - 'account_number' => $account->account_number, - 'email_address' => $emailAddress, - 'batch_id' => $this->batchId - ]); - - return $statementLog; - } - - /** - * Handle job failure - */ - public function failed(\Throwable $exception) - { - $this->updateLogStatus('failed', [ - 'completed_at' => now(), - 'error_message' => $exception->getMessage() - ]); - - Log::error('SendStatementEmailJob failed permanently', [ - 'batch_id' => $this->batchId, - 'period' => $this->period, - 'request_type' => $this->requestType, - 'target_value' => $this->targetValue, - 'error' => $exception->getMessage(), - 'trace' => $exception->getTraceAsString() - ]); - } -} diff --git a/app/Jobs/UpdateAllAtmCardsBatchJob.php b/app/Jobs/UpdateAllAtmCardsBatchJob.php new file mode 100644 index 0000000..e2c420c --- /dev/null +++ b/app/Jobs/UpdateAllAtmCardsBatchJob.php @@ -0,0 +1,379 @@ +syncLogId = $syncLogId; + $this->batchSize = $batchSize > 0 ? $batchSize : self::BATCH_SIZE; + $this->filters = $filters; + } + + /** + * Execute the job untuk update seluruh kartu ATM + * + * @return void + * @throws Exception + */ + public function handle(): void + { + set_time_limit(self::MAX_EXECUTION_TIME); + + Log::info('Memulai job update seluruh kartu ATM', [ + 'sync_log_id' => $this->syncLogId, + 'batch_size' => $this->batchSize, + 'filters' => $this->filters + ]); + + try { + DB::beginTransaction(); + + // Load atau buat log sinkronisasi + $this->loadOrCreateSyncLog(); + + // Update status job dimulai + $this->updateJobStartStatus(); + + // Ambil total kartu yang akan diproses + $totalCards = $this->getTotalCardsCount(); + + if ($totalCards === 0) { + Log::info('Tidak ada kartu ATM yang perlu diupdate'); + $this->updateJobCompletedStatus(0, 0); + DB::commit(); + return; + } + + Log::info("Ditemukan {$totalCards} kartu ATM yang akan diproses"); + + // Proses kartu dalam batch + $processedCount = $this->processCardsInBatches($totalCards); + + // Update status job selesai + $this->updateJobCompletedStatus($totalCards, $processedCount); + + Log::info('Job update seluruh kartu ATM selesai', [ + 'total_cards' => $totalCards, + 'processed_count' => $processedCount, + 'sync_log_id' => $this->syncLog->id + ]); + + DB::commit(); + + } catch (Exception $e) { + DB::rollBack(); + + $this->updateJobFailedStatus($e->getMessage()); + + Log::error('Gagal menjalankan job update seluruh kartu ATM: ' . $e->getMessage(), [ + 'file' => $e->getFile(), + 'line' => $e->getLine(), + 'sync_log_id' => $this->syncLogId, + 'trace' => $e->getTraceAsString() + ]); + + throw $e; + } + } + + /** + * Load atau buat log sinkronisasi baru + * + * @return void + * @throws Exception + */ + private function loadOrCreateSyncLog(): void + { + Log::info('Loading atau membuat sync log', ['sync_log_id' => $this->syncLogId]); + + if ($this->syncLogId) { + $this->syncLog = KartuSyncLog::find($this->syncLogId); + if (!$this->syncLog) { + throw new Exception("Sync log dengan ID {$this->syncLogId} tidak ditemukan"); + } + } else { + // Buat log sinkronisasi baru + $this->syncLog = KartuSyncLog::create([ + 'periode' => now()->format('Y-m'), + 'sync_notes' => 'Batch update seluruh kartu ATM dimulai', + 'is_sync' => false, + 'sync_at' => null, + 'is_csv' => false, + 'csv_at' => null, + 'is_ftp' => false, + 'ftp_at' => null + ]); + } + + Log::info('Sync log berhasil dimuat/dibuat', ['sync_log_id' => $this->syncLog->id]); + } + + /** + * Update status saat job dimulai + * + * @return void + */ + private function updateJobStartStatus(): void + { + Log::info('Memperbarui status job dimulai'); + + $this->syncLog->update([ + 'sync_notes' => $this->syncLog->sync_notes . "\nBatch update seluruh kartu ATM dimulai pada " . now()->format('Y-m-d H:i:s'), + 'is_sync' => false, + 'sync_at' => null + ]); + } + + /** + * Ambil total jumlah kartu yang akan diproses + * + * @return int + */ + private function getTotalCardsCount(): int + { + Log::info('Menghitung total kartu yang akan diproses', ['filters' => $this->filters]); + + $query = $this->buildCardQuery(); + $count = $query->count(); + + Log::info("Total kartu ditemukan: {$count}"); + return $count; + } + + /** + * Build query untuk mengambil kartu berdasarkan filter + * + * @return \Illuminate\Database\Eloquent\Builder + */ + private function buildCardQuery() + { + $query = Atmcard::where('crsts', 1) // Kartu aktif + ->whereNotNull('accflag') + ->where('accflag', '!=', ''); + + // Terapkan filter default untuk kartu yang perlu update branch/currency + if (empty($this->filters) || !isset($this->filters['skip_branch_currency_filter'])) { + $query->where(function ($q) { + $q->whereNull('branch') + ->orWhere('branch', '') + ->orWhereNull('currency') + ->orWhere('currency', ''); + }); + } + + // Terapkan filter tambahan jika ada + if (!empty($this->filters)) { + foreach ($this->filters as $field => $value) { + if ($field === 'skip_branch_currency_filter') { + continue; + } + + if (is_array($value)) { + $query->whereIn($field, $value); + } else { + $query->where($field, $value); + } + } + } + + return $query; + } + + /** + * Proses kartu dalam batch + * + * @param int $totalCards + * @return int Jumlah kartu yang berhasil diproses + */ + private function processCardsInBatches(int $totalCards): int + { + Log::info('Memulai pemrosesan kartu dalam batch', [ + 'total_cards' => $totalCards, + 'batch_size' => $this->batchSize + ]); + + $processedCount = 0; + $batchNumber = 1; + $totalBatches = ceil($totalCards / $this->batchSize); + + // Proses kartu dalam chunk/batch + $this->buildCardQuery()->chunk($this->batchSize, function ($cards) use (&$processedCount, &$batchNumber, $totalBatches, $totalCards) { + Log::info("Memproses batch {$batchNumber}/{$totalBatches}", [ + 'cards_in_batch' => $cards->count(), + 'processed_so_far' => $processedCount + ]); + + try { + // Dispatch job untuk setiap kartu dalam batch dengan delay + foreach ($cards as $index => $card) { + // Hitung delay berdasarkan nomor batch dan index untuk menyebar eksekusi job + $delay = (($batchNumber - 1) * $this->batchSize + $index) % self::MAX_DELAY_SPREAD; + $delay += self::DELAY_BETWEEN_JOBS; // Tambah delay minimum + + // Dispatch job UpdateAtmCardBranchCurrencyJob + UpdateAtmCardBranchCurrencyJob::dispatch($card, $this->syncLog->id) + ->delay(now()->addSeconds($delay)) + ->onQueue('default'); + + $processedCount++; + } + + // Update progress di log setiap 10 batch + if ($batchNumber % 10 === 0) { + $this->updateProgressStatus($processedCount, $totalCards, $batchNumber, $totalBatches); + } + + Log::info("Batch {$batchNumber} berhasil dijadwalkan", [ + 'cards_scheduled' => $cards->count(), + 'total_processed' => $processedCount + ]); + + } catch (Exception $e) { + Log::error("Error saat memproses batch {$batchNumber}: " . $e->getMessage(), [ + 'batch_number' => $batchNumber, + 'cards_count' => $cards->count(), + 'error' => $e->getMessage() + ]); + throw $e; + } + + $batchNumber++; + }); + + Log::info('Selesai memproses semua batch', [ + 'total_processed' => $processedCount, + 'total_batches' => $batchNumber - 1 + ]); + + return $processedCount; + } + + /** + * Update status progress pemrosesan + * + * @param int $processedCount + * @param int $totalCards + * @param int $batchNumber + * @param int $totalBatches + * @return void + */ + private function updateProgressStatus(int $processedCount, int $totalCards, int $batchNumber, int $totalBatches): void + { + Log::info('Memperbarui status progress', [ + 'processed' => $processedCount, + 'total' => $totalCards, + 'batch' => $batchNumber, + 'total_batches' => $totalBatches + ]); + + $percentage = round(($processedCount / $totalCards) * 100, 2); + $progressNote = "\nProgress: {$processedCount}/{$totalCards} kartu dijadwalkan ({$percentage}%) - Batch {$batchNumber}/{$totalBatches}"; + + $this->syncLog->update([ + 'sync_notes' => $this->syncLog->sync_notes . $progressNote + ]); + } + + /** + * Update status saat job selesai + * + * @param int $totalCards + * @param int $processedCount + * @return void + */ + private function updateJobCompletedStatus(int $totalCards, int $processedCount): void + { + Log::info('Memperbarui status job selesai', [ + 'total_cards' => $totalCards, + 'processed_count' => $processedCount + ]); + + $completionNote = "\nBatch update selesai pada " . now()->format('Y-m-d H:i:s') . + " - Total {$processedCount} kartu dari {$totalCards} berhasil dijadwalkan untuk update"; + + $this->syncLog->update([ + 'is_sync' => true, + 'sync_at' => now(), + 'sync_notes' => $this->syncLog->sync_notes . $completionNote + ]); + } + + /** + * Update status saat job gagal + * + * @param string $errorMessage + * @return void + */ + private function updateJobFailedStatus(string $errorMessage): void + { + Log::error('Memperbarui status job gagal', ['error' => $errorMessage]); + + if ($this->syncLog) { + $failureNote = "\nBatch update gagal pada " . now()->format('Y-m-d H:i:s') . + " - Error: {$errorMessage}"; + + $this->syncLog->update([ + 'is_sync' => false, + 'sync_notes' => $this->syncLog->sync_notes . $failureNote + ]); + } + } +} diff --git a/app/Jobs/UpdateAtmCardBranchCurrencyJob.php b/app/Jobs/UpdateAtmCardBranchCurrencyJob.php index 90829d8..acedd9b 100644 --- a/app/Jobs/UpdateAtmCardBranchCurrencyJob.php +++ b/app/Jobs/UpdateAtmCardBranchCurrencyJob.php @@ -4,13 +4,14 @@ namespace Modules\Webstatement\Jobs; use Exception; use Illuminate\Bus\Queueable; +use Illuminate\Support\Facades\Log; +use Illuminate\Support\Facades\Http; +use Illuminate\Queue\SerializesModels; +use Illuminate\Queue\InteractsWithQueue; +use Modules\Webstatement\Models\Account; +use Modules\Webstatement\Models\Atmcard; use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Foundation\Bus\Dispatchable; -use Illuminate\Queue\InteractsWithQueue; -use Illuminate\Queue\SerializesModels; -use Illuminate\Support\Facades\Http; -use Illuminate\Support\Facades\Log; -use Modules\Webstatement\Models\Atmcard; class UpdateAtmCardBranchCurrencyJob implements ShouldQueue { @@ -77,7 +78,7 @@ class UpdateAtmCardBranchCurrencyJob implements ShouldQueue } /** - * Get account information from the API + * Get account information from Account model or API * * @param string $accountNumber * @return array|null @@ -85,10 +86,26 @@ class UpdateAtmCardBranchCurrencyJob implements ShouldQueue private function getAccountInfo(string $accountNumber): ?array { try { + // Coba dapatkan data dari model Account terlebih dahulu + $account = Account::where('account_number', $accountNumber)->first(); + + if ($account) { + // Jika account ditemukan, format data sesuai dengan format response dari API + return [ + 'responseCode' => '00', + 'acctCompany' => $account->branch_code, + 'acctCurrency' => $account->currency, + 'acctType' => $account->open_category + // Tambahkan field lain yang mungkin diperlukan + ]; + } + + // Jika tidak ditemukan di database, ambil dari Fiorano API $url = env('FIORANO_URL') . self::API_BASE_PATH; $path = self::API_INQUIRY_PATH; $data = [ - 'accountNo' => $accountNumber + 'accountNo' => $accountNumber, + ]; $response = Http::post($url . $path, $data); @@ -110,6 +127,7 @@ class UpdateAtmCardBranchCurrencyJob implements ShouldQueue $cardData = [ 'branch' => !empty($accountInfo['acctCompany']) ? $accountInfo['acctCompany'] : null, 'currency' => !empty($accountInfo['acctCurrency']) ? $accountInfo['acctCurrency'] : null, + 'product_code' => !empty($accountInfo['acctType']) ? $accountInfo['acctType'] : null, ]; $this->card->update($cardData); diff --git a/app/Mail/StatementEmail.php b/app/Mail/StatementEmail.php index 51cb2af..215f42c 100644 --- a/app/Mail/StatementEmail.php +++ b/app/Mail/StatementEmail.php @@ -1,195 +1,208 @@ statement = $statement; - $this->filePath = $filePath; - $this->isZip = $isZip; - $this->phpMailerService = new PHPMailerService(); - } - - /** - * Kirim email statement - * - * @param string $emailAddress - * @return bool - */ - public function send(string $emailAddress): bool - { - try { - // Generate subject - $subject = $this->generateSubject(); - - // Generate email body - $body = $this->generateEmailBody(); - - // Generate attachment name - $attachmentName = $this->generateAttachmentName(); - - // Determine MIME type - $mimeType = $this->isZip ? 'application/zip' : 'application/pdf'; - - // Send email using PHPMailer - $result = $this->phpMailerService->sendEmail( - $emailAddress, - $subject, - $body, - $this->filePath, - $attachmentName, - $mimeType - ); - - Log::info('Statement email sent via PHPMailer', [ - 'to' => $emailAddress, - 'subject' => $subject, - 'attachment' => $attachmentName, - 'account_number' => $this->statement->account_number, - 'period' => $this->statement->period_from, - 'success' => $result - ]); - - return $result; - - } catch (\Exception $e) { - Log::error('Failed to send statement email via PHPMailer', [ - 'to' => $emailAddress, - 'account_number' => $this->statement->account_number, - 'error' => $e->getMessage(), - 'trace' => $e->getTraceAsString() - ]); - - return false; - } - } - - /** - * Generate email subject - * - * @return string - */ - protected function generateSubject(): string - { - $subject = 'Statement Rekening Bank Artha Graha Internasional'; + use Carbon\Carbon; + use Exception; + use Illuminate\Bus\Queueable; + use Illuminate\Mail\Mailable; + use Illuminate\Queue\SerializesModels; + use Illuminate\Support\Facades\Config; + use Log; + use Modules\Webstatement\Models\Account; + use Modules\Webstatement\Models\PrintStatementLog; + use Symfony\Component\Mailer\Mailer; + use Symfony\Component\Mailer\Transport\Smtp\EsmtpTransport; + use Symfony\Component\Mime\Email; if ($this->statement->is_period_range) { $subject .= " - {$this->statement->period_from} to {$this->statement->period_to}"; } else { $subject .= " - " . \Carbon\Carbon::createFromFormat('Ym', $this->statement->period_from)->locale('id')->isoFormat('MMMM Y'); - } - - // Add batch info for batch requests - if ($this->statement->request_type && $this->statement->request_type !== 'single_account') { - $subject .= " [{$this->statement->request_type}]"; - } - - if ($this->statement->batch_id) { - $subject .= " [Batch: {$this->statement->batch_id}]"; - } - - return $subject; - } - - /** - * Generate email body HTML - * - * @return string - */ - protected function generateEmailBody(): string + class StatementEmail extends Mailable { - try { - // Get account data - $account = Account::where('account_number', $this->statement->account_number)->first(); + use Queueable, SerializesModels; - // Prepare data for view - $data = [ - 'statement' => $this->statement, + protected $statement; + protected $filePath; + protected $isZip; + protected $message; + + /** + * Create a new message instance. + * Membuat instance email baru untuk pengiriman statement + * + * @param PrintStatementLog $statement + * @param string $filePath + * @param bool $isZip + */ + public function __construct(PrintStatementLog $statement, $filePath, $isZip = false) + { + $this->statement = $statement; + $this->filePath = $filePath; + $this->isZip = $isZip; + } + + /** + * Override the send method to use EsmtpTransport directly + * Using the working configuration from Python script with multiple fallback methods + */ + public function send($mailer) + { + // Get mail configuration + $host = Config::get('mail.mailers.smtp.host'); + $port = Config::get('mail.mailers.smtp.port'); + $username = Config::get('mail.mailers.smtp.username'); + $password = Config::get('mail.mailers.smtp.password'); + + Log::info('StatementEmail: Attempting to send email with multiple fallback methods'); + + // Define connection methods like in Python script + $method = + // Method 3: STARTTLS with original port + [ + 'port' => $port, + 'ssl' => false, + 'name' => 'STARTTLS (Port $port)' + ]; + + $lastException = null; + + // Try each connection method until one succeeds + try { + Log::info('StatementEmail: Trying ' . $method['name']); + + // Create EsmtpTransport with current method + $transport = new EsmtpTransport($host, $method['port'], $method['ssl']); + + // Set username and password + if ($username) { + $transport->setUsername($username); + } + if ($password) { + $transport->setPassword($password); + } + + // Disable SSL verification for development + $streamOptions = [ + 'ssl' => [ + 'verify_peer' => false, + 'verify_peer_name' => false, + 'allow_self_signed' => true + ] + ]; + $transport->getStream()->setStreamOptions($streamOptions); + + // Build the email content + $this->build(); + + // Start transport connection + $transport->start(); + + // Create Symfony mailer + $symfonyMailer = new Mailer($transport); + + // Convert Laravel message to Symfony Email + $email = $this->toSymfonyEmail(); + + // Send the email + $symfonyMailer->send($email); + + // Close connection + $transport->stop(); + + Log::info('StatementEmail: Successfully sent email using ' . $method['name']); + return $this; + + } catch (Exception $e) { + $lastException = $e; + Log::warning('StatementEmail: Failed to send with ' . $method['name'] . ': ' . $e->getMessage()); + // Continue to next method + } + + try { + return parent::send($mailer); + } catch (Exception $e) { + Log::error('StatementEmail: Laravel mailer also failed: ' . $e->getMessage()); + // If we got here, throw the last exception from our custom methods + throw $lastException; + } + } + + /** + * Build the message. + * Membangun struktur email dengan attachment statement + * + * @return $this + */ + public function build() + { + $subject = 'Statement Rekening Bank Artha Graha Internasional'; + + if ($this->statement->is_period_range) { + $subject .= " - {$this->statement->period_from} to {$this->statement->period_to}"; + } else { + $subject .= " - " . Carbon::createFromFormat('Ym', $this->statement->period_from) + ->locale('id') + ->isoFormat('MMMM Y'); + } + + $email = $this->subject($subject); + + // Store the email in the message property for later use in toSymfonyEmail() + $this->message = $email; + + return $email; + } + + /** + * Convert Laravel message to Symfony Email + */ + protected function toSymfonyEmail() + { + // Build the message if it hasn't been built yet + $this->build(); + // Create a new Symfony Email + $email = new Email(); + + // Set from address using config values instead of trying to call getFrom() + $fromAddress = Config::get('mail.from.address'); + $fromName = Config::get('mail.from.name'); + $email->from($fromName ? "$fromName <$fromAddress>" : $fromAddress); + + // Set to addresses - use the to addresses from the mailer instead of trying to call getTo() + // We'll get the to addresses from the Mail facade when the email is sent + // For now, we'll just add a placeholder recipient that will be overridden by the Mail facade + $email->to($this->message->to[0]['address']); + + $email->subject($this->message->subject); + + // Set body - use a simple HTML content instead of trying to call getHtmlBody() + // In a real implementation, we would need to find a way to access the rendered HTML content + $email->html(view('webstatement::statements.email', [ + 'statement' => $this->statement, 'accountNumber' => $this->statement->account_number, - 'periodFrom' => $this->statement->period_from, - 'periodTo' => $this->statement->period_to, - 'isRange' => $this->statement->is_period_range, - 'requestType' => $this->statement->request_type, - 'batchId' => $this->statement->batch_id, - 'accounts' => $account - ]; + 'periodFrom' => $this->statement->period_from, + 'periodTo' => $this->statement->period_to, + 'isRange' => $this->statement->is_period_range, + 'requestType' => $this->statement->request_type, + 'batchId' => $this->statement->batch_id, + 'accounts' => Account::where('account_number', $this->statement->account_number)->first() + ])->render()); + //$email->text($this->message->getTextBody()); - // Render view to HTML - return View::make('webstatement::statements.email', $data)->render(); + // Add attachments - use the file path directly instead of trying to call getAttachments() + if ($this->filePath && file_exists($this->filePath)) { + if ($this->isZip) { + $fileName = "{$this->statement->account_number}_{$this->statement->period_from}_to_{$this->statement->period_to}.zip"; + $contentType = 'application/zip'; + } else { + $fileName = "{$this->statement->account_number}_{$this->statement->period_from}.pdf"; + $contentType = 'application/pdf'; + } + $email->attachFromPath($this->filePath, $fileName, $contentType); + } - } catch (\Exception $e) { - Log::error('Failed to generate email body', [ - 'account_number' => $this->statement->account_number, - 'error' => $e->getMessage() - ]); - - // Fallback to simple HTML - return $this->generateFallbackEmailBody(); + return $email; } } - - /** - * Generate fallback email body - * - * @return string - */ - protected function generateFallbackEmailBody(): string - { - $periodText = $this->statement->is_period_range - ? "periode {$this->statement->period_from} sampai {$this->statement->period_to}" - : "periode " . \Carbon\Carbon::createFromFormat('Ym', $this->statement->period_from)->locale('id')->isoFormat('MMMM Y'); - - return " - - -

Statement Rekening Bank Artha Graha Internasional

-

Yth. Nasabah,

-

Terlampir adalah statement rekening Anda untuk {$periodText}.

-

Nomor Rekening: {$this->statement->account_number}

-

Terima kasih atas kepercayaan Anda.

-
-

Salam,
Bank Artha Graha Internasional

- - - "; - } - - /** - * Generate attachment filename - * - * @return string - */ - protected function generateAttachmentName(): string - { - if ($this->isZip) { - return "{$this->statement->account_number}_{$this->statement->period_from}_to_{$this->statement->period_to}.zip"; - } else { - return "{$this->statement->account_number}_{$this->statement->period_from}.pdf"; - } - } -} diff --git a/app/Models/Atmcard.php b/app/Models/Atmcard.php index 7bcdc04..d81f41a 100644 --- a/app/Models/Atmcard.php +++ b/app/Models/Atmcard.php @@ -4,6 +4,7 @@ namespace Modules\Webstatement\Models; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Factories\HasFactory; +use Illuminate\Support\Facades\Log; // use Modules\Webstatement\Database\Factories\AtmcardFactory; class Atmcard extends Model @@ -15,7 +16,64 @@ class Atmcard extends Model */ protected $guarded = ['id']; + /** + * Relasi ke tabel JenisKartu untuk mendapatkan informasi biaya kartu + * + * @return \Illuminate\Database\Eloquent\Relations\BelongsTo + */ public function biaya(){ + Log::info('Mengakses relasi biaya untuk ATM card', ['card_id' => $this->id]); return $this->belongsTo(JenisKartu::class,'ctdesc','code'); } + + /** + * Scope untuk mendapatkan kartu ATM yang aktif + * + * @param \Illuminate\Database\Eloquent\Builder $query + * @return \Illuminate\Database\Eloquent\Builder + */ + public function scopeActive($query) + { + Log::info('Menggunakan scope active untuk filter kartu ATM aktif'); + return $query->where('crsts', 1); + } + + /** + * Scope untuk mendapatkan kartu berdasarkan product_code + * + * @param \Illuminate\Database\Eloquent\Builder $query + * @param string $productCode + * @return \Illuminate\Database\Eloquent\Builder + */ + public function scopeByProductCode($query, $productCode) + { + Log::info('Menggunakan scope byProductCode', ['product_code' => $productCode]); + return $query->where('product_code', $productCode); + } + + /** + * Accessor untuk mendapatkan product_code dengan format yang konsisten + * + * @param string $value + * @return string|null + */ + public function getProductCodeAttribute($value) + { + return $value ? strtoupper(trim($value)) : null; + } + + /** + * Mutator untuk menyimpan product_code dengan format yang konsisten + * + * @param string $value + * @return void + */ + public function setProductCodeAttribute($value) + { + $this->attributes['product_code'] = $value ? strtoupper(trim($value)) : null; + Log::info('Product code diset untuk ATM card', [ + 'card_id' => $this->id ?? 'new', + 'product_code' => $this->attributes['product_code'] + ]); + } } diff --git a/app/Providers/WebstatementServiceProvider.php b/app/Providers/WebstatementServiceProvider.php index fa82202..61283a2 100644 --- a/app/Providers/WebstatementServiceProvider.php +++ b/app/Providers/WebstatementServiceProvider.php @@ -6,18 +6,19 @@ use Illuminate\Support\Facades\Blade; use Illuminate\Support\ServiceProvider; use Nwidart\Modules\Traits\PathNamespace; use Illuminate\Console\Scheduling\Schedule; -use Modules\Webstatement\Console\CheckEmailProgressCommand; use Modules\Webstatement\Console\UnlockPdf; use Modules\Webstatement\Console\CombinePdf; use Modules\Webstatement\Console\ConvertHtmlToPdf; use Modules\Webstatement\Console\ExportDailyStatements; use Modules\Webstatement\Console\ProcessDailyMigration; use Modules\Webstatement\Console\ExportPeriodStatements; +use Modules\Webstatement\Console\UpdateAllAtmCardsCommand; +use Modules\Webstatement\Console\CheckEmailProgressCommand; use Modules\Webstatement\Console\GenerateBiayakartuCommand; +use Modules\Webstatement\Console\SendStatementEmailCommand; use Modules\Webstatement\Jobs\UpdateAtmCardBranchCurrencyJob; use Modules\Webstatement\Console\GenerateAtmTransactionReport; use Modules\Webstatement\Console\GenerateBiayaKartuCsvCommand; -use Modules\Webstatement\Console\SendStatementEmailCommand; class WebstatementServiceProvider extends ServiceProvider { @@ -70,7 +71,8 @@ class WebstatementServiceProvider extends ServiceProvider ExportPeriodStatements::class, GenerateAtmTransactionReport::class, SendStatementEmailCommand::class, - CheckEmailProgressCommand::class + CheckEmailProgressCommand::class, + UpdateAllAtmCardsCommand::class ]); } diff --git a/database/migrations/2025_06_13_075827_add_product_code_to_atmcards_table.php b/database/migrations/2025_06_13_075827_add_product_code_to_atmcards_table.php new file mode 100644 index 0000000..91dd4ff --- /dev/null +++ b/database/migrations/2025_06_13_075827_add_product_code_to_atmcards_table.php @@ -0,0 +1,63 @@ +string('product_code')->nullable()->after('ctdesc')->comment('Kode produk kartu ATM'); + }); + + DB::commit(); + Log::info('Migration berhasil: field product_code telah ditambahkan ke tabel atmcards'); + + } catch (Exception $e) { + DB::rollback(); + Log::error('Migration gagal: ' . $e->getMessage()); + throw $e; + } + } + + /** + * Membalikkan migration dengan menghapus field product_code dari tabel atmcards + * + * @return void + */ + public function down(): void + { + Log::info('Memulai rollback migration: menghapus field product_code dari tabel atmcards'); + + DB::beginTransaction(); + + try { + Schema::table('atmcards', function (Blueprint $table) { + $table->dropColumn('product_code'); + }); + + DB::commit(); + Log::info('Rollback migration berhasil: field product_code telah dihapus dari tabel atmcards'); + + } catch (Exception $e) { + DB::rollback(); + Log::error('Rollback migration gagal: ' . $e->getMessage()); + throw $e; + } + } +}; diff --git a/module.json b/module.json index b3e0eae..5f83d10 100644 --- a/module.json +++ b/module.json @@ -30,7 +30,8 @@ "attributes": [], "permission": "", "roles": [ - "administrator" + "administrator", + "customer_service" ] }, { diff --git a/resources/views/statements/email.blade.php b/resources/views/statements/email.blade.php index 5589b70..bf872f9 100644 --- a/resources/views/statements/email.blade.php +++ b/resources/views/statements/email.blade.php @@ -16,8 +16,8 @@ } .container { - max-width: 90%; - margin: 20px auto; + max-width: 100%; + margin: 0px auto; background-color: #ffffff; border-radius: 8px; overflow: hidden; @@ -37,7 +37,7 @@ } .content { - padding: 30px; + padding: 5px; font-size: 14px; } @@ -89,7 +89,7 @@ Silahkan gunakan password Electronic Statement Anda untuk membukanya.

Password standar Elektronic Statement ini adalah ddMonyyyyxx (contoh: 01Aug1970xx) dimana : -