From f6df453ddc8113120da81790f7077996a6163cee Mon Sep 17 00:00:00 2001 From: Daeng Deni Mardaeni Date: Thu, 12 Jun 2025 09:15:38 +0700 Subject: [PATCH 01/16] refactor(webstatement): perbaiki pembentukan logika, struktur kode, dan validasi parameter pada SendStatementEmailCommand - **Perbaikan Struktur Kode:** - Melakukan perapihan kode dengan konsistensi indentasi dan penyusunan namespace. - Memisahkan logika kompleks dan mengorganisasi ulang kode untuk meningkatkan keterbacaan. - Menambahkan namespace `InvalidArgumentException`. - **Peningkatan Validasi:** - Menambahkan validasi komprehensif untuk parameter `period`, `type`, `--account`, dan `--branch`. - Validasi lebih spesifik untuk memastikan account atau branch terkait sesuai kebutuhan. - Memberikan pesan error informatif ketika validasi gagal. - **Peningkatan Metode Utility:** - Menambahkan metode `validateParameters` untuk menangani berbagai skenario validasi input. - Menambahkan metode `determineRequestTypeAndTarget` untuk memisahkan logika penentuan tipe request. - Memperbarui metode `createLogEntry` untuk menyesuaikan atribut log dengan lebih baik berdasarkan request type. - **Perbaikan Feedback Pengguna:** - Menampilkan informasi yang lebih rinci terkait status pengiriman email, seperti parameter validasi, log ID, dan batch ID. - Memberikan panduan untuk monitoring queue melalui command. - **Penanganan Error dan Logging:** - Menambahkan logging detail untuk error yang terjadi dalam proses pengiriman email. - Memastikan rollback jika terjadi kegagalan selama proses dispatch job. Signed-off-by: Daeng Deni Mardaeni --- app/Console/SendStatementEmailCommand.php | 449 +++++++++++----------- 1 file changed, 225 insertions(+), 224 deletions(-) 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}"); - } -} From d5482fb824e2683a731fd4878a560b70e10db8ea Mon Sep 17 00:00:00 2001 From: Daeng Deni Mardaeni Date: Thu, 12 Jun 2025 09:18:16 +0700 Subject: [PATCH 02/16] refactor(webstatement): restrukturisasi kode pada SendStatementEmailJob - **Perbaikan Struktural:** - Melakukan perapihan kode dengan konsistensi indentasi dan penyusunan namespace seluruh file. - Menambahkan dan mengimpor namespace baru seperti `Throwable`, `InvalidArgumentException`, dan `Exception`. - **Peningkatan Readability:** - Menambahkan format dan penyesuaian pada komentar, khususnya penjelasan method dan atribut. - Menggunakan alignment untuk parameter pada log dan constructor untuk meningkatkan keterbacaan. - **Pengelolaan Job:** - Menambahkan logging detail saat memulai, menjalankan, hingga menyelesaikan job. - Menambahkan penanganan proses tiap akun dalam batch, termasuk logging sukses/gagal dan pembaruan status log. - **Penanganan Error:** - Menambahkan rollback database jika terjadi exception pada saat proses pengiriman email. - Melakukan logging error dengan detail tambahan termasuk pesan dan trace. - **Penambahan Utility:** - Menambahkan metode reusable seperti `updateLogStatus` untuk update status log dengan parameter dinamis. - Menambahkan validasi seperti pengecekan eksistensi file PDF dan email terkait sebelum pengiriman. - **Peningkatan Proses Batch:** - Menambahkan pengelolaan batch berbasis properti `batchId` untuk tracking. - Memperbaiki handle retries dan status akhir batch secara komprehensif (completed, failed). - Menambahkan logging agregat untuk jumlah akun yang diproses dan tingkat keberhasilan. - **Peningkatan Validasi Email:** - Menambahkan skenario untuk pengambilan email dari `stmt_email` atau fallback ke data customer jika tersedia. - Menambahkan peringatan saat akun tertentu tidak memiliki email yang valid. - **Pemeliharaan File PDF:** - Mengecek keberadaan file PDF terkait sebelum proses pengiriman. - Menampilkan log error jika file tidak ditemukan. - **Peningkatan Retry dan Logging Gagal:** - Implementasi metode `failed()` untuk logging pada job yang gagal permanen. - Menangani detail error dan rollback pada level tiap akun dan batch. Signed-off-by: Daeng Deni Mardaeni --- app/Jobs/SendStatementEmailJob.php | 761 +++++++++++++++-------------- 1 file changed, 384 insertions(+), 377 deletions(-) diff --git a/app/Jobs/SendStatementEmailJob.php b/app/Jobs/SendStatementEmailJob.php index 6984a11..1c2c83f 100644 --- a/app/Jobs/SendStatementEmailJob.php +++ b/app/Jobs/SendStatementEmailJob.php @@ -1,418 +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', [ - '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); - - // 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 - ]); - - 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 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() - ]); - } -} From e5b8dfc7c4fe7be3d7afd806c24a9ce7b38b77d4 Mon Sep 17 00:00:00 2001 From: Daeng Deni Mardaeni Date: Thu, 12 Jun 2025 09:18:42 +0700 Subject: [PATCH 03/16] feat(webstatement): tambahkan pengiriman email dengan fallback konfigurasi - **Implementasi Custom Email Sender:** - Menambahkan metode `send()` dengan EsmtpTransport untuk mendukung pengiriman email menggunakan fallback konfigurasi yang lebih fleksibel. - Penanganan port `STARTTLS` dengan koneksi manual serta pengaturan SSL. - **Refaktor Method Build:** - Memperbaiki tampilan struktur email: - Menyertakan template email, attachment, dan penempatan properti email secara lebih dinamis. - Mendukung file PDF atau ZIP sebagai lampiran. - **Implementasi Konversi Laravel to Symfony Mailer:** - Metode `toSymfonyEmail()` diimplementasikan untuk mengonversi email Laravel ke Symfony Mailer. - Penanganan dari email `from`, `to`, `subject`, hingga body HTML secara langsung. - Penambahan mekanisme attachment dengan validasi eksistensi file. - **Peningkatan Logging:** - Menambahkan logging detail untuk setiap tahap proses pengiriman, termasuk metode fallback yang berhasil atau gagal. - Log peringatan dan error ditambahkan saat terjadi kegagalan di setiap metode yang dicoba. - **Penanganan Error:** - Menangani pengecualian dan pencatatan error terakhir dari koneksi email manual serta fallback ke Laravel mailer jika cara custom gagal. - **Konsistensi Format:** - Melakukan perapihan atribut class serta alignment pada kode untuk meningkatkan keterbacaan dan konsistensi. - **Optimalisasi Email Statement:** - Memastikan format periode (bulanan/range) tampil dinamis pada subjek email. - Menambahkan validasi serta pengelolaan untuk lampiran file sebelum pengiriman. Signed-off-by: Daeng Deni Mardaeni --- app/Mail/StatementEmail.php | 257 +++++++++++++++++++++++++++--------- 1 file changed, 191 insertions(+), 66 deletions(-) diff --git a/app/Mail/StatementEmail.php b/app/Mail/StatementEmail.php index ee77925..227af3d 100644 --- a/app/Mail/StatementEmail.php +++ b/app/Mail/StatementEmail.php @@ -1,79 +1,204 @@ statement = $statement; - $this->filePath = $filePath; - $this->isZip = $isZip; - } + use Queueable, SerializesModels; - /** - * Build the message. - * Membangun struktur email dengan attachment statement - * - * @return $this - */ - public function build() - { - $subject = 'Statement Rekening Bank Artha Graha Internasional'; + protected $statement; + protected $filePath; + protected $isZip; + protected $message; - 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'); + /** + * 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; } - $email = $this->subject($subject) - ->view('webstatement::statements.email') - ->with([ - '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::where('account_number', $this->statement->account_number)->first() - ]); + /** + * 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'); - if ($this->isZip) { - $fileName = "{$this->statement->account_number}_{$this->statement->period_from}_to_{$this->statement->period_to}.zip"; - $email->attach($this->filePath, [ - 'as' => $fileName, - 'mime' => 'application/zip', - ]); - } else { - $fileName = "{$this->statement->account_number}_{$this->statement->period_from}.pdf"; - $email->attach($this->filePath, [ - 'as' => $fileName, - 'mime' => 'application/pdf', - ]); + 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; + } } - return $email; + /** + * 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::where('account_number', $this->statement->account_number)->first() + ])->render()); + //$email->text($this->message->getTextBody()); + + // 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); + } + + return $email; + } } -} From b717749450727626b2c2f28169c74dbc3aa853a6 Mon Sep 17 00:00:00 2001 From: Daeng Deni Mardaeni Date: Thu, 12 Jun 2025 09:19:07 +0700 Subject: [PATCH 04/16] refactor(webstatement): perbaikan tampilan dan penyesuaian template email pada statement - **Penyesuaian Layout dan Tampilan:** - Mengubah properti CSS pada `.container`: - `max-width` diubah dari `90%` menjadi `100%`. - `margin` diubah dari `20px auto` menjadi `0px auto`. - Mengurangi padding pada elemen `.content` dari `30px` menjadi `5px` untuk meningkatkan efisiensi ruang tampilan. - **Perbaikan Format Signature:** - Menggabungkan pemisah signature menjadi satu baris panjang untuk meningkatkan estetika dan keterbacaan. - Menghapus elemen `` yang berlebihan. - **Penghapusan Footer:** - Menghilangkan bagian footer yang terdapat elemen copyright dan informasi customer service. Signed-off-by: Daeng Deni Mardaeni --- resources/views/statements/email.blade.php | 22 +++++----------------- 1 file changed, 5 insertions(+), 17 deletions(-) diff --git a/resources/views/statements/email.blade.php b/resources/views/statements/email.blade.php index ae4665c..683eb37 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; } @@ -103,11 +103,7 @@ Terima Kasih,

Bank Artha Graha Internasional
- ------------------------------ - - ------------------------------ - - --------
+ ------------------------------------------------------------
Kami sangat menghargai masukan dan saran Anda untuk meningkatkan layanan dan produk kami.
Untuk memberikan masukan, silakan hubungi GrahaCall 24 Jam kami di 0-800-191-8880.


@@ -132,11 +128,7 @@ Regards,

Bank Artha Graha Internasional
- ------------------------------ - - ------------------------------ - - --------
+ ------------------------------------------------------------
We welcome any feedback or suggestions to improve our product and services.
If you have any feedback, please contact our GrahaCall 24 Hours at 0-800-191-8880. @@ -145,10 +137,6 @@ - From f7a92a5336046705c75c639ed49145dd63eca53b Mon Sep 17 00:00:00 2001 From: Daeng Deni Mardaeni Date: Thu, 12 Jun 2025 12:15:56 +0700 Subject: [PATCH 05/16] refactor(webstatement): sesuaikan format list pada template email statement - **Perubahan Format List:** - Mengganti properti `class="dashed-list"` dengan `style="list-style-type: none;"` untuk meningkatkan estetika dan konsistensi tampilan. - Menambahkan simbol `-` pada setiap item list untuk memperjelas poin dalam deskripsi password. - **Peningkatan Konsistensi:** - Perbaikan dilakukan pada bagian deskripsi password dalam bahasa Indonesia dan Inggris untuk menjaga keseragaman format. - Memastikan semua elemen list menggunakan format yang seragam. - **Penyesuaian Detail:** - Menonjolkan elemen list dengan properti HTML agar lebih mudah dipahami oleh penerima email. - Perbaikan minor pada struktur dan whitespace untuk meningkatkan keterbacaan kode. Signed-off-by: Daeng Deni Mardaeni --- resources/views/statements/email.blade.php | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/resources/views/statements/email.blade.php b/resources/views/statements/email.blade.php index 683eb37..bf872f9 100644 --- a/resources/views/statements/email.blade.php +++ b/resources/views/statements/email.blade.php @@ -89,14 +89,14 @@ Silahkan gunakan password Electronic Statement Anda untuk membukanya.

Password standar Elektronic Statement ini adalah ddMonyyyyxx (contoh: 01Aug1970xx) dimana : -
    -
  • dd : 2 digit tanggal lahir anda, contoh: 01
  • -
  • Mon : +
      +
    • - dd : 2 digit tanggal lahir anda, contoh: 01
    • +
    • - Mon : 3 huruf pertama bulan lahir anda dalam bahasa Ingris. Huruf pertama adalah huruf besar dan selanjutnya huruf kecil, contoh : Aug
    • -
    • yyyy : 4 digit tahun kelahiran anda, contoh : 1970
    • -
    • xx : 2 digit terakhir dari nomer rekening anda, contoh : 12
    • +
    • - yyyy : 4 digit tahun kelahiran anda, contoh : 1970
    • +
    • - xx : 2 digit terakhir dari nomer rekening anda, contoh : 12

    @@ -114,14 +114,14 @@ Please use your Electronic Statement password to open it.

    The Electronic Statement standard password is ddMonyyyyxx (example: 01Aug1970xx) where: -
      -
    • dd : The first 2 digits of your birthdate, example: 01
    • -
    • Mon : +
        +
      • - dd : The first 2 digits of your birthdate, example: 01
      • +
      • - Mon : The first 3 letters of your birth month in English. The first letter is uppercase and the rest are lowercase, example: Aug
      • -
      • yyyy : 4 digit of your birth year, example: 1970
      • -
      • xx : The last 2 digits of your account number, example: 12.
      • +
      • - yyyy : 4 digit of your birth year, example: 1970
      • +
      • - xx : The last 2 digits of your account number, example: 12.

      From dbdeceb4c01557ce14b7314d6297118683da4673 Mon Sep 17 00:00:00 2001 From: Daeng Deni Mardaeni Date: Fri, 13 Jun 2025 14:33:47 +0700 Subject: [PATCH 06/16] feat(webstatement): optimalkan pengambilan informasi account untuk UpdateAtmCardBranchCurrencyJob - **Penambahan Logika Pengambilan Data:** - Menambahkan proses pengambilan data account dari model `Account` sebelum memanggil API Fiorano. - Melakukan pencarian data berdasarkan nomor rekening (`account_number`) melalui query pada model. - Jika data ditemukan, mengembalikan informasi account berupa response format yang menyerupai hasil dari API. - **Optimisasi Response:** - Menyusun data response lengkap dari model `Account`, seperti kode cabang (`branch_code`), mata uang (`currency`), kategori pembukaan (`open_category`), dan properti lain yang relevan. - Field response menyertakan nilai default atau diisi dengan data lain yang ada dalam model. - **Fallback API Fiorano:** - Jika data dari database tidak ditemukan, tetap menggunakan mekanisme existing untuk melakukan request ke API Fiorano. - Tidak ada perubahan lain pada struktur permintaan atau penanganan response Fiorano. - **Komentar dan Dokumentasi:** - Memperbarui komentar pada fungsi `getAccountInfo` untuk mencerminkan logika terbaru. - Menjelaskan fallback ke API jika data model tidak tersedia melalui komentar inline agar lebih mudah dipahami. - **Peningkatan Efisiensi:** - Mengurangi frekuensi panggilan API Fiorano dengan memanfaatkan data lokal terlebih dahulu, sehingga mempercepat proses eksekusi job. Signed-off-by: Daeng Deni Mardaeni --- app/Jobs/UpdateAtmCardBranchCurrencyJob.php | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/app/Jobs/UpdateAtmCardBranchCurrencyJob.php b/app/Jobs/UpdateAtmCardBranchCurrencyJob.php index 90829d8..7b6a1ab 100644 --- a/app/Jobs/UpdateAtmCardBranchCurrencyJob.php +++ b/app/Jobs/UpdateAtmCardBranchCurrencyJob.php @@ -77,7 +77,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,6 +85,21 @@ class UpdateAtmCardBranchCurrencyJob implements ShouldQueue private function getAccountInfo(string $accountNumber): ?array { try { + // Coba dapatkan data dari model Account terlebih dahulu + $account = \Modules\Webstatement\Models\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, + 'openCategory' => $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 = [ From 4b889da5a5d5bdfb3a4988a0eeb7c20828688b62 Mon Sep 17 00:00:00 2001 From: Daeng Deni Mardaeni Date: Fri, 13 Jun 2025 15:06:37 +0700 Subject: [PATCH 07/16] feat(webstatement): tambahkan pengelolaan product_code pada ATM Card - **Penambahan Field Baru:** - Menambahkan field baru `product_code` pada tabel `atmcards` melalui migrasi database. - Field bersifat nullable dan memiliki komentar deskriptif untuk dokumentasi skema database. - **Refaktor Logika pada UpdateAtmCardBranchCurrencyJob:** - Menambahkan assignment data `product_code` untuk update kartu ATM berdasarkan informasi account. - Mengoptimalkan proses query dengan memperbaiki penggunaan namespace model `Account`. - **Peningkatan Model Atmcard:** - Menambahkan relasi baru `biaya` untuk mendapatkan informasi terkait jenis kartu (`JenisKartu`). - Menambah **scope** baru: - `active` untuk memfilter kartu ATM yang aktif. - `byProductCode` untuk memfilter berdasarkan kode produk (`product_code`). - Memperkenalkan accessor dan mutator untuk memastikan format `product_code` konsisten (uppercase, trimmed). - Menambahkan logging pada setiap akses relasi atau perubahan terkait field `product_code`. - **Penyesuaian Logging:** - Memperbanyak log untuk monitoring aktivitas, termasuk: - Akses dan perubahan data `product_code`. - Scope query pada model `Atmcard`. - **Migrasi Database:** - Menambahkan proses safe migration dengan transaksi pada operasi `up` dan `down`. - Mencatat log saat migrasi berhasil atau rollback diperlukan jika terjadi kesalahan. - **Optimisasi dan Perbaikan Format:** - Mengorganisasi ulang import pada file `UpdateAtmCardBranchCurrencyJob` sesuai standar PSR-12. - Membenahi key output response dari `openCategory` menjadi `acctType` untuk dukungan data baru `product_code`. Signed-off-by: Daeng Deni Mardaeni --- app/Jobs/UpdateAtmCardBranchCurrencyJob.php | 19 +++--- app/Models/Atmcard.php | 58 +++++++++++++++++ ...827_add_product_code_to_atmcards_table.php | 63 +++++++++++++++++++ 3 files changed, 132 insertions(+), 8 deletions(-) create mode 100644 database/migrations/2025_06_13_075827_add_product_code_to_atmcards_table.php diff --git a/app/Jobs/UpdateAtmCardBranchCurrencyJob.php b/app/Jobs/UpdateAtmCardBranchCurrencyJob.php index 7b6a1ab..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 { @@ -86,7 +87,7 @@ class UpdateAtmCardBranchCurrencyJob implements ShouldQueue { try { // Coba dapatkan data dari model Account terlebih dahulu - $account = \Modules\Webstatement\Models\Account::where('account_number', $accountNumber)->first(); + $account = Account::where('account_number', $accountNumber)->first(); if ($account) { // Jika account ditemukan, format data sesuai dengan format response dari API @@ -94,7 +95,7 @@ class UpdateAtmCardBranchCurrencyJob implements ShouldQueue 'responseCode' => '00', 'acctCompany' => $account->branch_code, 'acctCurrency' => $account->currency, - 'openCategory' => $account->open_category + 'acctType' => $account->open_category // Tambahkan field lain yang mungkin diperlukan ]; } @@ -103,7 +104,8 @@ class UpdateAtmCardBranchCurrencyJob implements ShouldQueue $url = env('FIORANO_URL') . self::API_BASE_PATH; $path = self::API_INQUIRY_PATH; $data = [ - 'accountNo' => $accountNumber + 'accountNo' => $accountNumber, + ]; $response = Http::post($url . $path, $data); @@ -125,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/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/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; + } + } +}; From 7b32cb8d39f875a2293dab3148cc715a0501a31e Mon Sep 17 00:00:00 2001 From: Daeng Deni Mardaeni Date: Fri, 13 Jun 2025 15:11:25 +0700 Subject: [PATCH 08/16] feat(webstatement): tambah filter product_code dan branch pada GenerateBiayaKartuCsvJob - Menambahkan filter baru: - Memastikan `product_code` tidak termasuk dalam daftar `6002`, `6004`, `6042`, dan `6031`. - Menyaring data dengan kondisi branch tidak sama dengan `ID0019999`. - Optimasi query: - Filter tambahan bertujuan untuk mempersempit data hasil pengambilan sehingga lebih relevan dan efisien dalam pembuatan file CSV. - Peningkatan validasi: - Memastikan data yang diekspor sesuai dengan ketentuan baru guna meningkatkan akurasi laporan biaya kartu. Signed-off-by: Daeng Deni Mardaeni --- app/Jobs/GenerateBiayaKartuCsvJob.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/Jobs/GenerateBiayaKartuCsvJob.php b/app/Jobs/GenerateBiayaKartuCsvJob.php index cb13989..e98addd 100644 --- a/app/Jobs/GenerateBiayaKartuCsvJob.php +++ b/app/Jobs/GenerateBiayaKartuCsvJob.php @@ -175,6 +175,8 @@ ->whereNotNull('currency') ->where('currency', '!=', '') ->whereIn('ctdesc', $cardTypes) + ->whereNotIn('product_code',['6002','6004','6042','6031']) + ->where('branch','!=','ID0019999') ->get(); } From 4bfd9374905296d37a52da180e1c4cc9bdf3592d Mon Sep 17 00:00:00 2001 From: Daeng Deni Mardaeni Date: Mon, 16 Jun 2025 22:51:26 +0700 Subject: [PATCH 09/16] feat(webstatement): tambahkan pengelolaan kartu ATM dengan fitur batch processing dan CSV tunggal - **Penambahan Fitur**: - Menambahkan metode baru `generateSingleAtmCardCsv` untuk membuat file CSV tunggal tanpa pemisahan cabang: - Mencakup seluruh data kartu ATM yang memenuhi syarat. - File diunggah ke SFTP tanpa direktori spesifik cabang. - Implementasi command `UpdateAllAtmCardsCommand` untuk batch update: - Dukungan konfigurasi parameter seperti batch size, ID log sinkronisasi, queue, filter, dan dry-run. - **Optimasi Logging**: - Logging rinci ditambahkan pada semua proses, termasuk: - Generasi CSV tunggal. - Proses upload CSV ke SFTP. - Pembaruan atau pembuatan `KartuSyncLog` dalam batch processing. - Progress dan status tiap batch. - Error handling dengan detail informasi pada setiap exception. - **Perbaikan dan Penyesuaian Job**: - Penambahan `UpdateAllAtmCardsBatchJob` yang mengatur proses batch update: - Mendukung operasi batch dengan pengaturan ukuran dan parameter filtering kartu. - Pencatatan log progres secara dinamis dengan kalkulasi batch dan persentase. - Menyusun delay antar job untuk performa yang lebih baik. - Menyertakan validasi untuk sinkronisasi dan pembaruan data kartu ATM. - **Refaktor Provider**: - Pendaftaran command baru: - `UpdateAllAtmCardsCommand` untuk batch update seluruh kartu ATM. - Command disertakan dalam provider `WebstatementServiceProvider`. - **Error Handling**: - Peningkatan mekanisme rollback pada database saat error. - Menambahkan notifikasi log `failure` apabila job gagal dijalankan. - **Dokumentasi dan Komentar**: - Menambahkan komentar mendetail pada setiap fungsi baru untuk penjelasan lebih baik. - Mendokumentasikan seluruh proses dan perubahan pada job serta command baru terkait kartu ATM. Perubahan ini meningkatkan efisiensi pengelolaan data kartu ATM, termasuk generasi CSV, proses batch, dan pengunggahan data ke SFTP. Signed-off-by: Daeng Deni Mardaeni --- app/Console/UpdateAllAtmCardsCommand.php | 110 +++++ app/Jobs/GenerateBiayaKartuCsvJob.php | 155 ++++++- app/Jobs/UpdateAllAtmCardsBatchJob.php | 379 ++++++++++++++++++ app/Providers/WebstatementServiceProvider.php | 8 +- 4 files changed, 648 insertions(+), 4 deletions(-) create mode 100644 app/Console/UpdateAllAtmCardsCommand.php create mode 100644 app/Jobs/UpdateAllAtmCardsBatchJob.php 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/Jobs/GenerateBiayaKartuCsvJob.php b/app/Jobs/GenerateBiayaKartuCsvJob.php index e98addd..e32e1b6 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); @@ -415,4 +417,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/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/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 ]); } From 2c8f49af2085c9bbc5490525b14cffc8b88c0ae8 Mon Sep 17 00:00:00 2001 From: Daeng Deni Mardaeni Date: Tue, 17 Jun 2025 09:45:41 +0700 Subject: [PATCH 10/16] feat(webstatement): optimalkan saveBatch pada ProcessStmtEntryDataJob - **Perubahan Mekanisme Simpan Data:** - Mengganti pendekatan `delete` dan `insert` dengan `updateOrCreate` untuk mencegah duplicate key error. - Menambahkan transaksi database (`DB::beginTransaction()` dan `DB::commit()`) untuk memastikan konsistensi data. - Menambahkan logging pada awal dan akhir proses untuk memantau jumlah record yang berhasil diproses. - **Penanganan Error:** - Menambahkan rollback transaksi (`DB::rollback()`) pada exception untuk menghindari data korup. - Logging eror ditingkatkan dengan menampilkan pesan dan trace exception secara rinci. - **Optimasi Loop:** - Refinement looping pada `entryBatch` dengan menerapkan chunking untuk efisiensi memori. - Proses setiap record menggunakan `updateOrCreate` guna mengurangi overhead penghapusan data secara manual. - **Peningkatan Logging:** - Menambahkan informasi log yang mencakup: - Proses awal dan akhir dari `saveBatch`. - Jumlah record yang diproses secara sukses. - Error yang terjadi selama proses berlangsung. - **Dokumentasi dan Komentar:** - Menambahkan penjelasan detil pada method `saveBatch` untuk memperjelas logika baru. - Penyempurnaan komentar agar mencerminkan proses terkini dengan jelas. Perubahan ini meningkatkan efisiensi dan keandalan proses penyimpanan data batch dengan mengurangi risiko konflik pada database serta memastikan rollback pada situasi error. Signed-off-by: Daeng Deni Mardaeni --- app/Jobs/ProcessStmtEntryDataJob.php | 46 ++++++++++++++++++++-------- 1 file changed, 34 insertions(+), 12 deletions(-) diff --git a/app/Jobs/ProcessStmtEntryDataJob.php b/app/Jobs/ProcessStmtEntryDataJob.php index 11e129b..92ac2c8 100644 --- a/app/Jobs/ProcessStmtEntryDataJob.php +++ b/app/Jobs/ProcessStmtEntryDataJob.php @@ -1,5 +1,7 @@ entryBatch)) { + $totalProcessed = 0; + // 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'); + foreach ($this->entryBatch as $entryChunk) { + foreach ($entryChunk as $entryData) { + // Gunakan updateOrCreate untuk menghindari duplicate key error + StmtEntry::updateOrCreate( + [ + 'stmt_entry_id' => $entryData['stmt_entry_id'] + ], + $entryData + ); - // Delete existing records with these IDs to avoid conflicts - StmtEntry::whereIn('stmt_entry_id', $entryIds)->delete(); - - // Insert all records in the chunk at once - StmtEntry::insert($entry); + $totalProcessed++; + } } - // 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; } } From 6035c61cc47564bc6eaa929af3800bd23d8ca6d8 Mon Sep 17 00:00:00 2001 From: Daeng Deni Mardaeni Date: Tue, 17 Jun 2025 09:54:26 +0700 Subject: [PATCH 11/16] feat(webstatement): tingkatkan validasi dan logging pada ProcessStmtEntryDataJob - **Validasi Data:** - Menambahkan validasi untuk memastikan bahwa setiap `entryData` adalah array dan memiliki properti `stmt_entry_id`. - Log peringatan ditambahkan untuk mendeteksi struktur data yang tidak valid. - **Perbaikan Logging:** - Logging ditingkatkan untuk mencatat data invalid yang ditemukan selama proses. - Menambahkan log peringatan dengan struktur data detail saat validasi gagal. - **Penghapusan Nested Loop:** - Memperbaiki logika iterasi dengan menghapus nested loop dan langsung memproses tiap elemen `entryBatch`. - **Penghitungan Kesalahan:** - Menambahkan penghitungan `errorCount` untuk melacak jumlah data yang mengalami validasi gagal. Perubahan ini meningkatkan keandalan proses dengan validasi tambahan, mencegah error akibat struktur data tidak valid, serta memberikan informasi log yang lebih rinci. Signed-off-by: Daeng Deni Mardaeni --- app/Jobs/ProcessStmtEntryDataJob.php | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/app/Jobs/ProcessStmtEntryDataJob.php b/app/Jobs/ProcessStmtEntryDataJob.php index 92ac2c8..f4f04c1 100644 --- a/app/Jobs/ProcessStmtEntryDataJob.php +++ b/app/Jobs/ProcessStmtEntryDataJob.php @@ -1,7 +1,4 @@ entryBatch)) { $totalProcessed = 0; - // Process in smaller chunks for better memory management - foreach ($this->entryBatch as $entryChunk) { - foreach ($entryChunk as $entryData) { + // 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( [ @@ -213,6 +212,9 @@ use Illuminate\Support\Facades\DB; ); $totalProcessed++; + } else { + Log::warning('Invalid entry data structure', ['data' => $entryData]); + $this->errorCount++; } } From 8fb16028d995c6027a6b5820d591eaa513f13d8f Mon Sep 17 00:00:00 2001 From: daengdeni Date: Fri, 20 Jun 2025 13:59:58 +0700 Subject: [PATCH 12/16] feat(webstatement): tambah validasi cabang rekening dan update logika penyimpanan statement ### Perubahan Utama - Tambah validasi untuk memverifikasi bahwa nomor rekening sesuai dengan cabang pengguna. - Cegah transaksi untuk rekening yang terdaftar di cabang khusus (`ID0019999`). - Perbaikan sistem untuk menangani kasus rekening yang tidak ditemukan di database. ### Detail Perubahan 1. **Validasi Cabang Rekening**: - Tambah pengecekan untuk memastikan rekening yang dimasukkan adalah milik cabang pengguna (non-multi-branch). - Blokir transaksi jika rekening terdaftar pada cabang khusus (`ID0019999`) dengan menampilkan pesan error yang relevan. - Tambahkan pesan error jika nomor rekening tidak ditemukan dalam sistem. 2. **Update Logika Penyimpanan**: - Tambahkan validasi untuk mengisi kolom `branch_code` secara otomatis berdasarkan informasi rekening terkait. - Otomatis atur nilai awal `authorization_status` menjadi `approved`. 3. **Penghapusan Atribut Tidak Digunakan**: - Hapus form `branch_code` dari view terkait (`index.blade.php`) karena sekarang diisi secara otomatis berdasarkan data rekening. 4. **Perbaikan View dan Logika Terkait Status Otorisasi**: - Hapus logic dan elemen UI terkait `authorization_status` di halaman statement (`index.blade.php` dan `show.blade.php`). - Simplifikasi tampilan untuk hanya menampilkan informasi yang tersedia dan relevan. 5. **Optimasi Query Data Cabang**: - Update query untuk memfilter cabang berdasarkan kondisi `customer_company` dan mengecualikan kode cabang khusus. 6. **Penyesuaian Struktur Request**: - Hapus validasi terkait `branch_code` di `PrintStatementRequest` karena tidak lagi relevan. 7. **Log Aktivitas dan Kesalahan**: - Tambahkan log untuk mencatat aktivitas seperti validasi rekening dan penyimpanan batch data. - Penanganan lebih baik untuk logging jika terjadi error saat validasi nomor rekening atau penyimpanan statement. ### Manfaat Perubahan - Meningkatkan akurasi data cabang dan validasi rekening sebelum penyimpanan. - Menyederhanakan antarmuka pengguna dengan menghapus field input redundant. - Memastikan proses menjadi lebih transparan dengan penanganan error yang lebih baik. Langkah ini diterapkan untuk meningkatkan keamanan dan keandalan sistem dalam memverifikasi dan memproses pemintaan statement. --- .../Controllers/PrintStatementController.php | 154 +++++++++++------- app/Http/Requests/PrintStatementRequest.php | 3 - module.json | 3 +- resources/views/statements/index.blade.php | 35 ---- resources/views/statements/show.blade.php | 13 -- 5 files changed, 98 insertions(+), 110 deletions(-) diff --git a/app/Http/Controllers/PrintStatementController.php b/app/Http/Controllers/PrintStatementController.php index cbc86c8..70cf1f4 100644 --- a/app/Http/Controllers/PrintStatementController.php +++ b/app/Http/Controllers/PrintStatementController.php @@ -14,6 +14,7 @@ use Modules\Basicdata\Models\Branch; use Modules\Webstatement\Http\Requests\PrintStatementRequest; use Modules\Webstatement\Mail\StatementEmail; + use Modules\Webstatement\Models\Account; use Modules\Webstatement\Models\PrintStatementLog; use ZipArchive; @@ -24,7 +25,10 @@ */ 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')); } @@ -35,32 +39,66 @@ */ 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['success_count'] = 0; + $validated['failed_count'] = 0; + $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,18 +107,18 @@ 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()); } } @@ -102,19 +140,19 @@ DB::beginTransaction(); try { - $disk = Storage::disk('sftpStatement'); + $disk = Storage::disk('sftpStatement'); $filePath = "{$statement->period_from}/Print/{$statement->branch_code}/{$statement->account_number}.pdf"; 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 . "/Print/{$statement->branch_code}/{$statement->account_number}.pdf"; if ($disk->exists($periodPath)) { $availablePeriods[] = $periodFormatted; @@ -127,55 +165,55 @@ $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 ]); } @@ -185,14 +223,14 @@ 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() ]); } } @@ -223,19 +261,19 @@ $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'); + $disk = Storage::disk('sftpStatement'); $filePath = "{$statement->period_from}/Print/{$statement->branch_code}/{$statement->account_number}.pdf"; if ($statement->is_period_range && $statement->period_to) { @@ -249,7 +287,7 @@ Log::error('Failed to download statement', [ 'statement_id' => $statement->id, - 'error' => $e->getMessage() + 'error' => $e->getMessage() ]); return back()->with('error', 'Failed to download statement: ' . $e->getMessage()); @@ -337,13 +375,13 @@ // 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; @@ -541,8 +579,8 @@ 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..c6d4698 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'], @@ -77,8 +76,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', 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/index.blade.php b/resources/views/statements/index.blade.php index 687c0fc..0546ca2 100644 --- a/resources/views/statements/index.blade.php +++ b/resources/views/statements/index.blade.php @@ -18,21 +18,6 @@ @endif
      -
      - - - @error('branch_code') -
      {{ $message }}
      - @enderror -
      -
      @@ -122,10 +107,6 @@ Period - - Status - - Available @@ -243,22 +224,6 @@ return fromPeriod + toPeriod; }, }, - authorization_status: { - title: 'Status', - render: (item, data) => { - let statusClass = 'badge badge-light-primary'; - - if (data.authorization_status === 'approved') { - statusClass = 'badge badge-light-success'; - } else if (data.authorization_status === 'rejected') { - statusClass = 'badge badge-light-danger'; - } else if (data.authorization_status === 'pending') { - statusClass = 'badge badge-light-warning'; - } - - return `${data.authorization_status}`; - }, - }, is_available: { title: 'Available', render: (item, data) => { diff --git a/resources/views/statements/show.blade.php b/resources/views/statements/show.blade.php index 76c333b..393a1ad 100644 --- a/resources/views/statements/show.blade.php +++ b/resources/views/statements/show.blade.php @@ -73,19 +73,6 @@
      -
      -
      Status
      -
      - @if($statement->authorization_status === 'pending') - Pending Authorization - @elseif($statement->authorization_status === 'approved') - Approved - @elseif($statement->authorization_status === 'rejected') - Rejected - @endif -
      -
      -
      Availability
      From fd5b8e1dad12190432eb304b0ee8dc6070e17a17 Mon Sep 17 00:00:00 2001 From: daengdeni Date: Fri, 20 Jun 2025 14:33:56 +0700 Subject: [PATCH 13/16] feat(webstatement): tambahkan filter data berdasarkan peran pengguna ### Perubahan Utama - Menambahkan filter data pada `PrintStatementLog` untuk pengguna non-administrator. - Membatasi query hanya untuk data yang sesuai dengan `user_id` pengguna yang sedang login jika bukan administrator. ### Detail Perubahan 1. **Update Logika Query**: - Menambahkan kondisi pengecekan untuk peran pengguna menggunakan `auth()->user()->hasRole('administrator')`. - Jika pengguna bukan administrator, query akan otomatis difilter berdasarkan `user_id` dari pengguna yang sedang login dengan fungsi `Auth::id()`. 2. **Peningkatan Keamanan Data**: - Membatasi akses data supaya hanya pengguna yang berhak dapat melihat data milik mereka. - Memastikan administrator tetap memiliki akses penuh ke semua data tanpa pembatasan. --- app/Http/Controllers/PrintStatementController.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/Http/Controllers/PrintStatementController.php b/app/Http/Controllers/PrintStatementController.php index 70cf1f4..7b4657e 100644 --- a/app/Http/Controllers/PrintStatementController.php +++ b/app/Http/Controllers/PrintStatementController.php @@ -333,6 +333,10 @@ // Retrieve data from the database $query = PrintStatementLog::query(); + if (!auth()->user()->hasRole('administrator')) { + $query->where('user_id', Auth::id()); + } + // Apply search filter if provided if ($request->has('search') && !empty($request->get('search'))) { $search = $request->get('search'); From a79b1bd99eaab4153e49c987fdd476e4fae12739 Mon Sep 17 00:00:00 2001 From: Daeng Deni Mardaeni Date: Sun, 22 Jun 2025 16:50:37 +0700 Subject: [PATCH 14/16] feat(webstatement): perbarui penamaan file PDF dan tambahkan log debug pada PrintStatementController - **Perubahan Penamaan File PDF:** - Mengubah format nama file dari `{account_number}.pdf` menjadi `{account_number}_{period_from}.pdf`. - Penyesuaian pada semua lokasi logika penentuan path file di SFTP: - Path file period single. - Path file pada mode period range. - Path file saat kompresi ke dalam ZIP. - **Penambahan Logging untuk Debugging:** - Menambahkan **Log::info** untuk mencatat informasi terkait path file, termasuk: - Path relatif file berdasarkan periode dan kode cabang. - Root path konfigurasi SFTP. - Path final lengkap pada SFTP. - **Penyesuaian Logika Path:** - Memastikan format nama file konsisten di semua fungsi handling periode tunggal dan periode range. - Menambahkan logging sebelum proses pengecekan eksistensi file pada SFTP. - **Peningkatan Monitoring:** - Memastikan struktur file dan path dapat dipantau dengan logging untuk mendukung debugging lebih baik. - Memberikan konteks tambahan pada setiap log yang relevan untuk memudahkan tracking. Signed-off-by: Daeng Deni Mardaeni --- .../Controllers/PrintStatementController.php | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/app/Http/Controllers/PrintStatementController.php b/app/Http/Controllers/PrintStatementController.php index 7b4657e..1049af9 100644 --- a/app/Http/Controllers/PrintStatementController.php +++ b/app/Http/Controllers/PrintStatementController.php @@ -141,7 +141,14 @@ 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"; + + // 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); @@ -152,7 +159,7 @@ 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; @@ -274,7 +281,7 @@ // Generate or fetch the statement file $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) { // Handle period range download (existing logic) @@ -480,7 +487,7 @@ 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); @@ -492,7 +499,7 @@ 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; @@ -517,7 +524,7 @@ 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 From 19c962307e537fefabdcf6045b6de3aebe1d1ef5 Mon Sep 17 00:00:00 2001 From: Daeng Deni Mardaeni Date: Sun, 22 Jun 2025 20:57:24 +0700 Subject: [PATCH 15/16] feat(webstatement): tambahkan fitur multi-branch dan perbaikan validasi form pada halaman statements - **Penambahan Fitur Multi-Branch:** - Tambahkan dropdown pilihan cabang (branch) saat fitur multi-branch diaktifkan. - Secara otomatis mengisi informasi branch jika hanya tersedia satu branch yang terkait dengan user. - **Perbaikan Validasi Form:** - Memastikan field `account_number` dan `branch_id` memiliki validasi yang lebih ketat. - Tambahkan validasi untuk `period_from` agar hanya menerima data periode yang tersedia (`is_available`). - **Perubahan Tampilan:** - Menyesuaikan desain form: - Tambahkan kondisi dynamic display pada field branch berdasarkan status multi-branch. - Reformat struktur HTML untuk meningkatkan keterbacaan dengan indentasi lebih konsisten. - Perbaikan tampilan elemen tabel pada daftar request statement: - Mengoptimalkan style menggunakan properti CSS baru pada grid dan typography. - **Optimasi Query dan Akses Data:** - Tambahkan filter berdasarkan `branch_code` agar data hanya terlihat untuk cabang yang relevan dengan user. - Optimalkan pengambilan data branch dengan hanya memuat cabang yang aktif. - **Peningkatan Logging:** - Tambahkan log pada pengolahan query untuk mendeteksi masalah akses branch saat user tidak memiliki akses multi-branch. - **Refaktor Backend:** - Tambahkan variable `multiBranch` pada controller untuk mengatur logika UI secara dinamis. - Refaktor pencarian branch di server-side untuk mengantisipasi session `MULTI_BRANCH`. Perubahan ini mendukung fleksibilitas akses cabang untuk user dengan mode multi-branch serta meningkatkan validasi dan pengalaman UI form. Signed-off-by: Daeng Deni Mardaeni --- .../Controllers/PrintStatementController.php | 10 +- app/Http/Requests/PrintStatementRequest.php | 1 + resources/views/statements/index.blade.php | 161 +++++++++++------- 3 files changed, 107 insertions(+), 65 deletions(-) diff --git a/app/Http/Controllers/PrintStatementController.php b/app/Http/Controllers/PrintStatementController.php index 1049af9..858438c 100644 --- a/app/Http/Controllers/PrintStatementController.php +++ b/app/Http/Controllers/PrintStatementController.php @@ -30,7 +30,10 @@ ->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')); } /** @@ -341,7 +344,10 @@ $query = PrintStatementLog::query(); if (!auth()->user()->hasRole('administrator')) { - $query->where('user_id', Auth::id()); + $query->where(function($q) { + $q->where('user_id', Auth::id()) + ->orWhere('branch_code', Auth::user()->branch->code); + }); } // Apply search filter if provided diff --git a/app/Http/Requests/PrintStatementRequest.php b/app/Http/Requests/PrintStatementRequest.php index c6d4698..c39803f 100644 --- a/app/Http/Requests/PrintStatementRequest.php +++ b/app/Http/Requests/PrintStatementRequest.php @@ -35,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 diff --git a/resources/views/statements/index.blade.php b/resources/views/statements/index.blade.php index 0546ca2..88cc931 100644 --- a/resources/views/statements/index.blade.php +++ b/resources/views/statements/index.blade.php @@ -11,26 +11,59 @@

      Request Print Stetement

      -
      + @csrf - @if(isset($statement)) + @if (isset($statement)) @method('PUT') @endif
      + + @if ($multiBranch) +
      + + + @error('branch_id') +
      {{ $message }}
      + @enderror +
      + @else +
      + + + +
      + @endif +
      - + @error('account_number') -
      {{ $message }}
      +
      {{ $message }}
      @enderror
      -
      + @@ -38,24 +71,21 @@ + type="month" name="period_from" + value="{{ $statement->period_from ?? old('period_from') }}" + max="{{ date('Y-m', strtotime('-1 month')) }}"> @error('period_from') - {{ $message }} + {{ $message }} @enderror
      - + @error('period_to') - {{ $message }} + {{ $message }} @enderror
      @@ -70,8 +100,9 @@
      -
      -
      +
      +

      Daftar Statement Request

      @@ -85,51 +116,54 @@
      - +
      - - - - - - - - - - - + + + + + + + + + + +
      - - - ID - - - Branch - - - Account Number - - - Period - - - Available - - - Notes - - - Created At - - Action
      + + + ID + + + Branch + + + Account Number + + + Period + + + Available + + + Notes + + + Created At + + Action
      -