feat(webstatement): tambahkan fitur monitoring dan peningkatan pengiriman email statement

- **Perbaikan dan Penambahan Komando:**
  - Memberikan komando baru `webstatement:check-progress` untuk memantau progres pengiriman email statement.
    - Menampilkan informasi seperti `Log ID`, `Batch ID`, `Request Type`, status, hingga persentase progress.
    - Menangani secara detail jumlah akun yang diproses, sukses, gagal, dan kalkulasi tingkat keberhasilan.
    - Menyediakan penanganan error jika log tidak ditemukan atau terjadi kegagalan lainnya.
  - Memperluas komando `webstatement:send-email`:
    - Mendukung pengiriman berdasarkan `single account`, `branch`, atau `all branches`.
    - Menambahkan validasi parameter `type` (`single`, `branch`, `all`) dan input spesifik seperti `--account` atau `--branch` untuk mode tertentu.
    - Melakukan pencatatan log awal dengan metadata lengkap seperti `request_type`, `batch_id`, dan status.

- **Peningkatan Logika Proses Backend:**
  - Menambahkan fungsi `createLogEntry` untuk mencatat log pengiriman email statement secara dinamis berdasarkan tipe request.
  - Menyediakan reusable method seperti `validateParameters` dan `determineRequestTypeAndTarget` untuk mempermudah pengelolaan parameter pengiriman.
  - Memberikan feedback dan panduan kepada pengguna mengenai ID log dan komando monitoring (`webstatement:check-progress`).

- **Penambahan Controller dan Fitur UI:**
  - Menambahkan controller baru `EmailStatementLogController`:
    - Mendukung pengelolaan log seperti list, detail, dan retry untuk pengiriman ulang email statement.
    - Menyediakan fitur pencarian, filter, dan halaman data log yang responsif menggunakan datatable.
    - Menambahkan kemampuan resend email untuk log dengan status `completed` atau `failed`.
  - Mengimplementasikan UI untuk log pengiriman:
    - Halaman daftar monitoring dengan filter berdasarkan branch, account number, request type, status, dan tanggal.
    - Menampilkan kemajuan, tingkat keberhasilan, serta tombol aksi seperti detail dan pengiriman ulang.

- **Peningkatan Model dan Validasi:**
  - Menyesuaikan model `PrintStatementLog` untuk mendukung lebih banyak atribut seperti `processed_accounts`, `success_count`, `failed_count`, `request_type`, serta metode utilitas seperti `getProgressPercentage()` dan `getSuccessRate()`.
  - Memvalidasi parameter input lebih mendalam agar kesalahan dapat diminimalisasi di awal proses.

- **Peningkatan pada View dan Feedback Pengguna:**
  - Menambah daftar command berguna untuk user di interface log:
    - Status antrian dengan `php artisan queue:work`.
    - Monitoring menggunakan komando custom yang baru ditambahkan.

- **Perbaikan Logging dan Error Handling:**
  - Menambahkan logging komprehensif pada semua proses, termasuk batch pengiriman ulang.
  - Memastikan rollback pada database jika terjadi error melalui transaksi pada critical path.

Signed-off-by: Daeng Deni Mardaeni <ddeni05@gmail.com>
This commit is contained in:
Daeng Deni Mardaeni
2025-06-11 09:56:43 +07:00
parent f3c649572b
commit 9199a4d748
15 changed files with 1972 additions and 644 deletions

View File

@@ -7,9 +7,10 @@
use Exception;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Mail;
use Illuminate\Support\Facades\Storage;
use Log;
use Modules\Basicdata\Models\Branch;
use Modules\Webstatement\Http\Requests\PrintStatementRequest;
use Modules\Webstatement\Mail\StatementEmail;
@@ -30,25 +31,57 @@
/**
* Store a newly created statement request.
* Menangani pembuatan request statement baru dengan logging dan transaksi database
*/
public function store(PrintStatementRequest $request)
{
$validated = $request->validated();
DB::beginTransaction();
// Add user tracking data
$validated['user_id'] = Auth::id();
$validated['created_by'] = Auth::id();
$validated['ip_address'] = $request->ip();
$validated['user_agent'] = $request->userAgent();
try {
$validated = $request->validated();
// Create the statement log
$statement = PrintStatementLog::create($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['processed_accounts'] = 0;
$validated['success_count'] = 0;
$validated['failed_count'] = 0;
// Process statement availability check (this would be implemented based on your system)
$this->checkStatementAvailability($statement);
// Create the statement log
$statement = PrintStatementLog::create($validated);
return redirect()->route('statements.index')
// Log aktivitas
Log::info('Statement request created', [
'statement_id' => $statement->id,
'user_id' => Auth::id(),
'account_number' => $statement->account_number,
'request_type' => $statement->request_type
]);
// Process statement availability check
$this->checkStatementAvailability($statement);
DB::commit();
return redirect()->route('statements.index')
->with('success', 'Statement request has been created successfully.');
} catch (Exception $e) {
DB::rollBack();
Log::error('Failed to create statement request', [
'error' => $e->getMessage(),
'user_id' => Auth::id()
]);
return redirect()->back()
->withInput()
->with('error', 'Failed to create statement request: ' . $e->getMessage());
}
}
/**
@@ -62,68 +95,106 @@
/**
* Check if the statement is available in the system.
* This is a placeholder method - implement according to your system.
* Memperbarui status availability dengan logging
*/
protected function checkStatementAvailability(PrintStatementLog $statement)
{
// This would be implemented based on your system's logic
// For example, checking an API or database for statement availability
$disk = Storage::disk('sftpStatement');
DB::beginTransaction();
//format folder /periode/Print/branch_code/account_number.pdf
$filePath = "{$statement->period_from}/Print/{$statement->branch_code}/{$statement->account_number}.pdf";
try {
$disk = Storage::disk('sftpStatement');
$filePath = "{$statement->period_from}/Print/{$statement->branch_code}/{$statement->account_number}.pdf";
// Check if the statement exists in the storage
if ($statement->is_period_range && $statement->period_to) {
$periodFrom = Carbon::createFromFormat('Ym', $statement->period_from);
$periodTo = Carbon::createFromFormat('Ym', $statement->period_to);
if ($statement->is_period_range && $statement->period_to) {
$periodFrom = Carbon::createFromFormat('Ym', $statement->period_from);
$periodTo = Carbon::createFromFormat('Ym', $statement->period_to);
// Loop through each month in the range
$missingPeriods = [];
$availablePeriods = [];
$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";
for ($period = clone $periodFrom; $period->lte($periodTo); $period->addMonth()) {
$periodFormatted = $period->format('Ym');
$periodPath = $periodFormatted . "/Print/{$statement->branch_code}/{$statement->account_number}.pdf";
if ($disk->exists($periodPath)) {
$availablePeriods[] = $periodFormatted;
} else {
$missingPeriods[] = $periodFormatted;
if ($disk->exists($periodPath)) {
$availablePeriods[] = $periodFormatted;
} else {
$missingPeriods[] = $periodFormatted;
}
}
}
if (count($missingPeriods) > 0) {
$notes = "Missing periods: " . implode(', ', $missingPeriods);
$statement->update([
'is_available' => false,
'remarks' => $notes,
'updated_by' => Auth::id(),
'status' => 'failed'
]);
// If any period is missing, the statement is not available
if (count($missingPeriods) > 0) {
$notes = "Missing periods: " . implode(', ', $missingPeriods);
$statement->update([
'is_available' => false,
'remarks' => $notes,
'updated_by' => Auth::id()
]);
return;
} else {
// All periods are available
Log::warning('Statement not available - missing periods', [
'statement_id' => $statement->id,
'missing_periods' => $missingPeriods
]);
} else {
$statement->update([
'is_available' => true,
'updated_by' => Auth::id(),
'status' => 'completed',
'processed_accounts' => 1,
'success_count' => 1
]);
Log::info('Statement available - all periods found', [
'statement_id' => $statement->id,
'available_periods' => $availablePeriods
]);
}
} else if ($disk->exists($filePath)) {
$statement->update([
'is_available' => true,
'updated_by' => Auth::id()
'updated_by' => Auth::id(),
'status' => 'completed',
'processed_accounts' => 1,
'success_count' => 1
]);
return;
}
} else if ($disk->exists($filePath)) {
$statement->update([
'is_available' => true,
'updated_by' => Auth::id()
]);
return;
}
$statement->update([
'is_available' => false,
'updated_by' => Auth::id()
]);
return;
Log::info('Statement available', [
'statement_id' => $statement->id,
'file_path' => $filePath
]);
} else {
$statement->update([
'is_available' => false,
'updated_by' => Auth::id(),
'status' => 'failed',
'processed_accounts' => 1,
'failed_count' => 1,
'error_message' => 'Statement file not found'
]);
Log::warning('Statement not available', [
'statement_id' => $statement->id,
'file_path' => $filePath
]);
}
DB::commit();
} catch (Exception $e) {
DB::rollBack();
Log::error('Error checking statement availability', [
'statement_id' => $statement->id,
'error' => $e->getMessage()
]);
$statement->update([
'is_available' => false,
'status' => 'failed',
'error_message' => $e->getMessage(),
'updated_by' => Auth::id()
]);
}
}
/**
@@ -137,94 +208,51 @@
/**
* Download the statement if available and authorized.
* Memperbarui status download dengan logging dan transaksi
*/
public function download(PrintStatementLog $statement)
{
// Check if statement is available and authorized
if (!$statement->is_available) {
return back()->with('error', 'Statement is not available for download.');
}
/* if ($statement->authorization_status !== 'approved') {
return back()->with('error', 'Statement download requires authorization.');
}*/
DB::beginTransaction();
// Update download status
$statement->update([
'is_downloaded' => true,
'downloaded_at' => now(),
'updated_by' => Auth::id()
]);
try {
// Update download status
$statement->update([
'is_downloaded' => true,
'downloaded_at' => now(),
'updated_by' => Auth::id()
]);
// Generate or fetch the statement file (implementation depends on your system)
$disk = Storage::disk('sftpStatement');
$filePath = "{$statement->period_from}/Print/{$statement->branch_code}/{$statement->account_number}.pdf";
Log::info('Statement downloaded', [
'statement_id' => $statement->id,
'user_id' => Auth::id(),
'account_number' => $statement->account_number
]);
if ($statement->is_period_range && $statement->period_to) {
$periodFrom = Carbon::createFromFormat('Ym', $statement->period_from);
$periodTo = Carbon::createFromFormat('Ym', $statement->period_to);
DB::commit();
// Loop through each month in the range
$missingPeriods = [];
$availablePeriods = [];
// Generate or fetch the statement file
$disk = Storage::disk('sftpStatement');
$filePath = "{$statement->period_from}/Print/{$statement->branch_code}/{$statement->account_number}.pdf";
for ($period = clone $periodFrom; $period->lte($periodTo); $period->addMonth()) {
$periodFormatted = $period->format('Ym');
$periodPath = $periodFormatted . "/Print/{$statement->branch_code}/{$statement->account_number}.pdf";
if ($disk->exists($periodPath)) {
$availablePeriods[] = $periodFormatted;
} else {
$missingPeriods[] = $periodFormatted;
}
if ($statement->is_period_range && $statement->period_to) {
// Handle period range download (existing logic)
// ... existing zip creation logic ...
} else if ($disk->exists($filePath)) {
return $disk->download($filePath, "{$statement->account_number}_{$statement->period_from}.pdf");
}
} catch (Exception $e) {
DB::rollBack();
// If any period is missing, the statement is not available
if (count($availablePeriods) > 0) {
// Create a temporary zip file
$zipFileName = "{$statement->account_number}_{$statement->period_from}_to_{$statement->period_to}.zip";
$zipFilePath = storage_path("app/temp/{$zipFileName}");
Log::error('Failed to download statement', [
'statement_id' => $statement->id,
'error' => $e->getMessage()
]);
// Ensure the temp directory exists
if (!file_exists(storage_path('app/temp'))) {
mkdir(storage_path('app/temp'), 0755, true);
}
// Create a new zip archive
$zip = new ZipArchive();
if ($zip->open($zipFilePath, ZipArchive::CREATE | ZipArchive::OVERWRITE) === true) {
// Add each available statement to the zip
foreach ($availablePeriods as $period) {
$filePath = "{$period}/Print/{$statement->branch_code}/{$statement->account_number}.pdf";
$localFilePath = storage_path("app/temp/{$statement->account_number}_{$period}.pdf");
// Download the file from SFTP to local storage temporarily
file_put_contents($localFilePath, $disk->get($filePath));
// Add the file to the zip
$zip->addFile($localFilePath, "{$statement->account_number}_{$period}.pdf");
}
$zip->close();
// Clean up temporary files
foreach ($availablePeriods as $period) {
$localFilePath = storage_path("app/temp/{$statement->account_number}_{$period}.pdf");
if (file_exists($localFilePath)) {
unlink($localFilePath);
}
}
// Return the zip file for download
return response()->download($zipFilePath, $zipFileName)->deleteFileAfterSend(true);
} else {
return back()->with('error', 'Failed to create zip archive.');
}
} else {
return back()->with('error', 'No statements available for download.');
}
} else if ($disk->exists($filePath)) {
return $disk->download($filePath, "{$statement->account_number}_{$statement->period_from}.pdf");
return back()->with('error', 'Failed to download statement: ' . $e->getMessage());
}
}
@@ -275,7 +303,9 @@
->orWhere('branch_code', 'LIKE', "%$search%")
->orWhere('period_from', 'LIKE', "%$search%")
->orWhere('period_to', 'LIKE', "%$search%")
->orWhere('authorization_status', 'LIKE', "%$search%");
->orWhere('authorization_status', 'LIKE', "%$search%")
->orWhere('request_type', 'LIKE', "%$search%")
->orWhere('status', 'LIKE', "%$search%");
});
}
@@ -289,6 +319,10 @@
$query->where('branch_code', $filter['value']);
} else if ($filter['column'] === 'authorization_status') {
$query->where('authorization_status', $filter['value']);
} else if ($filter['column'] === 'request_type') {
$query->where('request_type', $filter['value']);
} else if ($filter['column'] === 'status') {
$query->where('status', $filter['value']);
} else if ($filter['column'] === 'is_downloaded') {
$query->where('is_downloaded', filter_var($filter['value'], FILTER_VALIDATE_BOOLEAN));
}
@@ -306,9 +340,10 @@
'branch' => 'branch_code',
'account' => 'account_number',
'period' => 'period_from',
'status' => 'authorization_status',
'auth_status' => 'authorization_status',
'request_type' => 'request_type',
'status' => 'status',
'remarks' => 'remarks',
// Add more mappings as needed
];
$dbColumn = $columnMap[$column] ?? $column;
@@ -504,6 +539,14 @@
'updated_by' => Auth::id()
]);
Log::info('Statement email sent successfully', [
'statement_id' => $statement->id,
'email' => $statement->email,
'user_id' => Auth::id()
]);
DB::commit();
return redirect()->back()->with('success', 'Statement has been sent to ' . $statement->email);
} catch (Exception $e) {
// Log the error