Compare commits

16 Commits

Author SHA1 Message Date
Daeng Deni Mardaeni
4616137e0c feat(webstatement): tambahkan fitur konfirmasi email dan optimasi proses download statement
- **Penambahan Fitur Konfirmasi Email:**
  - Menambahkan event listener untuk form submit:
    - Menampilkan SweetAlert jika field email telah diisi.
    - Mengonfirmasi pengiriman statement ke alamat email yang diisi pengguna.
    - Submit form hanya setelah user mengonfirmasi.

- **Optimalisasi Proses Download Statement:**
  - Menangani logic download statement dalam rentang periode (period range):
    - Mencatat log keberadaan file untuk setiap periode.
    - Membuat file ZIP yang berisi semua file statement yang tersedia dalam rentang tersebut.
    - Mengelola file sementara untuk proses kompresi dengan pembersihan otomatis.
    - Menambahkan log error dan warning untuk file yang hilang dalam rentang periode.
    - Mendukung mekanisme download file tunggal untuk periode tertentu.
  - Menyesuaikan log dengan detail proses, seperti:
    - Informasi periode yang tersedia dan tidak.
    - Notifikasi penyelesaian atau kegagalan proses download ZIP.
  - Menambahkan logging trace pada exception untuk debugging lebih rinci.

- **Perubahan Validasi Logic:**
  - Validasi baru pada `PrintStatementRequest`:
    - Menentukan `is_period_range` hanya jika `period_to` berbeda dengan `period_from`.

- **Perbaikan dan Penyesuaian Pengiriman Email:**
  - Menambahkan pengecekan field email sebelum menjalankan fungsi kirim email di `PrintStatementController`.
  - Mengintegrasikan fungsi `sendEmail` jika terdapat email pada statement.

- **Penambahan Dokumentasi Kode:**
  - Menambahkan komentar inline di beberapa bagian:
    - Logika konfirmasi email.
    - Proses pembuatan ZIP dan penanganan download.
  - Menjelaskan tiap langkah operasional untuk mempermudah pemahaman dan debugging.

Perubahan ini mengintegrasikan fitur konfirmasi email yang lebih interaktif, meningkatkan proses download statement berjenjang, serta memperbaiki validasi dan logging pada tiap langkah proses.

Signed-off-by: Daeng Deni Mardaeni <ddeni05@gmail.com>
2025-06-23 10:24:23 +07:00
Daeng Deni Mardaeni
19c962307e 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 <ddeni05@gmail.com>
2025-06-22 20:57:24 +07:00
Daeng Deni Mardaeni
a79b1bd99e 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 <ddeni05@gmail.com>
2025-06-22 16:50:37 +07:00
daengdeni
fd5b8e1dad 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.
2025-06-20 14:33:56 +07:00
daengdeni
8fb16028d9 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.
2025-06-20 13:59:58 +07:00
Daeng Deni Mardaeni
6035c61cc4 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 <ddeni05@gmail.com>
2025-06-17 09:54:26 +07:00
Daeng Deni Mardaeni
2c8f49af20 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 <ddeni05@gmail.com>
2025-06-17 09:45:41 +07:00
Daeng Deni Mardaeni
4bfd937490 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 <ddeni05@gmail.com>
2025-06-16 22:51:26 +07:00
Daeng Deni Mardaeni
7b32cb8d39 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 <ddeni05@gmail.com>
2025-06-13 15:11:25 +07:00
Daeng Deni Mardaeni
4b889da5a5 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 <ddeni05@gmail.com>
2025-06-13 15:06:37 +07:00
Daeng Deni Mardaeni
dbdeceb4c0 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 <ddeni05@gmail.com>
2025-06-13 14:33:47 +07:00
Daeng Deni Mardaeni
f7a92a5336 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 <ddeni05@gmail.com>
2025-06-12 12:15:56 +07:00
Daeng Deni Mardaeni
b717749450 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 `<wbr>` yang berlebihan.

- **Penghapusan Footer:**
  - Menghilangkan bagian footer yang terdapat elemen copyright dan informasi customer service.

Signed-off-by: Daeng Deni Mardaeni <ddeni05@gmail.com>
2025-06-12 09:19:07 +07:00
Daeng Deni Mardaeni
e5b8dfc7c4 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 <ddeni05@gmail.com>
2025-06-12 09:18:42 +07:00
Daeng Deni Mardaeni
d5482fb824 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 <ddeni05@gmail.com>
2025-06-12 09:18:16 +07:00
Daeng Deni Mardaeni
f6df453ddc 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 <ddeni05@gmail.com>
2025-06-12 09:15:38 +07:00
17 changed files with 2061 additions and 903 deletions

View File

@@ -1,22 +1,23 @@
<?php
namespace Modules\Webstatement\Console;
namespace Modules\Webstatement\Console;
use Exception;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Log;
use Modules\Webstatement\Jobs\SendStatementEmailJob;
use Modules\Webstatement\Models\Account;
use Modules\Webstatement\Models\PrintStatementLog;
use Modules\Basicdata\Models\Branch;
use Exception;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Log;
use Modules\Basicdata\Models\Branch;
use Modules\Webstatement\Jobs\SendStatementEmailJob;
use Modules\Webstatement\Models\Account;
use Modules\Webstatement\Models\PrintStatementLog;
use InvalidArgumentException;
/**
* Command untuk mengirim email statement PDF ke nasabah
* Mendukung pengiriman per rekening, per cabang, atau seluruh cabang
*/
class SendStatementEmailCommand extends Command
{
protected $signature = 'webstatement:send-email
/**
* Command untuk mengirim email statement PDF ke nasabah
* Mendukung pengiriman per rekening, per cabang, atau seluruh cabang
*/
class SendStatementEmailCommand extends Command
{
protected $signature = 'webstatement:send-email
{period : Format periode YYYYMM (contoh: 202401)}
{--type=single : Tipe pengiriman: single, branch, all}
{--account= : Nomor rekening (untuk type=single)}
@@ -25,224 +26,224 @@ class SendStatementEmailCommand extends Command
{--queue=emails : Nama queue untuk job (default: emails)}
{--delay=0 : Delay dalam menit sebelum job dijalankan}';
protected $description = 'Mengirim email statement PDF ke nasabah (per rekening, per cabang, atau seluruh cabang)';
protected $description = 'Mengirim email statement PDF ke nasabah (per rekening, per cabang, atau seluruh cabang)';
public function handle()
{
$this->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}");
}
}

View File

@@ -0,0 +1,110 @@
<?php
namespace Modules\Webstatement\Console;
use Exception;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Log;
use Modules\Webstatement\Jobs\UpdateAllAtmCardsBatchJob;
class UpdateAllAtmCardsCommand extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'atmcard:update-all
{--sync-log-id= : ID sync log yang akan digunakan}
{--batch-size=100 : Ukuran batch untuk processing}
{--queue=atmcard-update : Nama queue untuk job}
{--filters= : Filter JSON untuk kondisi kartu}
{--dry-run : Preview tanpa eksekusi aktual}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Jalankan job untuk update seluruh kartu ATM secara batch';
/**
* Execute the console command.
*
* @return int
*/
public function handle(): int
{
Log::info('Memulai command update seluruh kartu ATM');
try {
$syncLogId = $this->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;
}
}
}

View File

@@ -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,9 +25,15 @@
*/
public function index(Request $request)
{
$branches = Branch::orderBy('name')->get();
$branches = Branch::whereNotNull('customer_company')
->where('code', '!=', 'ID0019999')
->orderBy('name')
->get();
return view('webstatement::statements.index', compact('branches'));
$branch = Branch::find(Auth::user()->branch_id);
$multiBranch = session('MULTI_BRANCH') ?? false;
return view('webstatement::statements.index', compact('branches', 'branch', 'multiBranch'));
}
/**
@@ -35,52 +42,90 @@
*/
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
$this->checkStatementAvailability($statement);
$statement = PrintStatementLog::find($statement->id);
if($statement->email){
$this->sendEmail($statement->id);
}
DB::commit();
return redirect()->route('statements.index')
->with('success', 'Statement request has been created successfully.');
->with('success', 'Statement request has been created successfully.');
} catch (Exception $e) {
DB::rollBack();
Log::error('Failed to create statement request', [
'error' => $e->getMessage(),
'error' => $e->getMessage(),
'user_id' => Auth::id()
]);
return redirect()->back()
->withInput()
->with('error', 'Failed to create statement request: ' . $e->getMessage());
->withInput()
->with('error', 'Failed to create statement request: ' . $e->getMessage());
}
}
@@ -102,19 +147,26 @@
DB::beginTransaction();
try {
$disk = Storage::disk('sftpStatement');
$filePath = "{$statement->period_from}/Print/{$statement->branch_code}/{$statement->account_number}.pdf";
$disk = Storage::disk('sftpStatement');
$filePath = "{$statement->period_from}/{$statement->branch_code}/{$statement->account_number}_{$statement->period_from}.pdf";
// Log untuk debugging
Log::info('Checking SFTP file path', [
'file_path' => $filePath,
'sftp_root' => config('filesystems.disks.sftpStatement.root'),
'full_path' => config('filesystems.disks.sftpStatement.root') . '/' . $filePath
]);
if ($statement->is_period_range && $statement->period_to) {
$periodFrom = Carbon::createFromFormat('Ym', $statement->period_from);
$periodTo = Carbon::createFromFormat('Ym', $statement->period_to);
$periodTo = Carbon::createFromFormat('Ym', $statement->period_to);
$missingPeriods = [];
$missingPeriods = [];
$availablePeriods = [];
for ($period = clone $periodFrom; $period->lte($periodTo); $period->addMonth()) {
$periodFormatted = $period->format('Ym');
$periodPath = $periodFormatted . "/Print/{$statement->branch_code}/{$statement->account_number}.pdf";
$periodPath = $periodFormatted . "/{$statement->branch_code}/{$statement->account_number}_{$periodFormatted}.pdf";
if ($disk->exists($periodPath)) {
$availablePeriods[] = $periodFormatted;
@@ -127,55 +179,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 +237,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,33 +275,161 @@
$statement->update([
'is_downloaded' => true,
'downloaded_at' => now(),
'updated_by' => Auth::id()
'updated_by' => Auth::id()
]);
Log::info('Statement downloaded', [
'statement_id' => $statement->id,
'user_id' => Auth::id(),
'statement_id' => $statement->id,
'user_id' => Auth::id(),
'account_number' => $statement->account_number
]);
DB::commit();
// Generate or fetch the statement file
$disk = Storage::disk('sftpStatement');
$filePath = "{$statement->period_from}/Print/{$statement->branch_code}/{$statement->account_number}.pdf";
$disk = Storage::disk('sftpStatement');
$filePath = "{$statement->period_from}/{$statement->branch_code}/{$statement->account_number}_{$statement->period_from}.pdf";
if ($statement->is_period_range && $statement->period_to) {
// Handle period range download (existing logic)
// ... existing zip creation logic ...
// Log: Memulai proses download period range
Log::info('Starting period range download', [
'statement_id' => $statement->id,
'period_from' => $statement->period_from,
'period_to' => $statement->period_to
]);
/**
* Handle period range download dengan membuat zip file
* yang berisi semua statement dalam rentang periode
*/
$periodFrom = Carbon::createFromFormat('Ym', $statement->period_from);
$periodTo = Carbon::createFromFormat('Ym', $statement->period_to);
// Loop through each month in the range
$missingPeriods = [];
$availablePeriods = [];
for ($period = clone $periodFrom; $period->lte($periodTo); $period->addMonth()) {
$periodFormatted = $period->format('Ym');
$periodPath = $periodFormatted . "/{$statement->branch_code}/{$statement->account_number}_{$periodFormatted}.pdf";
if ($disk->exists($periodPath)) {
$availablePeriods[] = $periodFormatted;
Log::info('Period available for download', [
'period' => $periodFormatted,
'path' => $periodPath
]);
} else {
$missingPeriods[] = $periodFormatted;
Log::warning('Period not available for download', [
'period' => $periodFormatted,
'path' => $periodPath
]);
}
}
// If any period is available, create a zip and download it
if (count($availablePeriods) > 0) {
/**
* Membuat zip file temporary untuk download
* dengan semua statement yang tersedia dalam periode
*/
$zipFileName = "{$statement->account_number}_{$statement->period_from}_to_{$statement->period_to}.zip";
$zipFilePath = storage_path("app/temp/{$zipFileName}");
// Ensure the temp directory exists
if (!file_exists(storage_path('app/temp'))) {
mkdir(storage_path('app/temp'), 0755, true);
Log::info('Created temp directory for zip files');
}
// Create a new zip archive
$zip = new ZipArchive();
if ($zip->open($zipFilePath, ZipArchive::CREATE | ZipArchive::OVERWRITE) === true) {
Log::info('Zip archive created successfully', ['zip_path' => $zipFilePath]);
// Add each available statement to the zip
foreach ($availablePeriods as $period) {
$periodFilePath = "{$period}/{$statement->branch_code}/{$statement->account_number}_{$period}.pdf";
$localFilePath = storage_path("app/temp/{$statement->account_number}_{$period}.pdf");
try {
// Download the file from SFTP to local storage temporarily
file_put_contents($localFilePath, $disk->get($periodFilePath));
// Add the file to the zip
$zip->addFile($localFilePath, "{$statement->account_number}_{$period}.pdf");
Log::info('Added file to zip', [
'period' => $period,
'local_path' => $localFilePath
]);
} catch (Exception $e) {
Log::error('Failed to add file to zip', [
'period' => $period,
'error' => $e->getMessage()
]);
}
}
$zip->close();
Log::info('Zip archive closed successfully');
// Return the zip file for download
$response = response()->download($zipFilePath, $zipFileName)->deleteFileAfterSend(true);
// Clean up temporary PDF files
foreach ($availablePeriods as $period) {
$localFilePath = storage_path("app/temp/{$statement->account_number}_{$period}.pdf");
if (file_exists($localFilePath)) {
unlink($localFilePath);
Log::info('Cleaned up temporary file', ['file' => $localFilePath]);
}
}
Log::info('Period range download completed successfully', [
'statement_id' => $statement->id,
'available_periods' => count($availablePeriods),
'missing_periods' => count($missingPeriods)
]);
return $response;
} else {
Log::error('Failed to create zip archive', ['zip_path' => $zipFilePath]);
return back()->with('error', 'Failed to create zip archive for download.');
}
} else {
Log::warning('No statements available for download in period range', [
'statement_id' => $statement->id,
'missing_periods' => $missingPeriods
]);
return back()->with('error', 'No statements available for download in the specified period range.');
}
} else if ($disk->exists($filePath)) {
/**
* Handle single period download
* Download file PDF tunggal untuk periode tertentu
*/
Log::info('Single period download', [
'statement_id' => $statement->id,
'file_path' => $filePath
]);
return $disk->download($filePath, "{$statement->account_number}_{$statement->period_from}.pdf");
} else {
Log::warning('Statement file not found', [
'statement_id' => $statement->id,
'file_path' => $filePath
]);
return back()->with('error', 'Statement file not found.');
}
} catch (Exception $e) {
DB::rollBack();
Log::error('Failed to download statement', [
'statement_id' => $statement->id,
'error' => $e->getMessage()
'error' => $e->getMessage(),
'trace' => $e->getTraceAsString()
]);
return back()->with('error', 'Failed to download statement: ' . $e->getMessage());
@@ -295,6 +475,13 @@
// Retrieve data from the database
$query = PrintStatementLog::query();
if (!auth()->user()->hasRole('administrator')) {
$query->where(function($q) {
$q->where('user_id', Auth::id())
->orWhere('branch_code', Auth::user()->branch->code);
});
}
// Apply search filter if provided
if ($request->has('search') && !empty($request->get('search'))) {
$search = $request->get('search');
@@ -337,13 +524,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;
@@ -426,6 +613,7 @@
public function sendEmail($id)
{
$statement = PrintStatementLog::findOrFail($id);
// Check if statement has email
if (empty($statement->email)) {
return redirect()->back()->with('error', 'No email address provided for this statement.');
@@ -438,7 +626,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);
@@ -450,7 +638,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;
@@ -475,7 +663,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
@@ -541,8 +729,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();

View File

@@ -21,7 +21,6 @@ class PrintStatementRequest extends FormRequest
public function rules(): array
{
$rules = [
'branch_code' => ['required', 'string', 'exists:branches,code'],
'account_number' => ['required', 'string'],
'is_period_range' => ['sometimes', 'boolean'],
'email' => ['nullable', 'email'],
@@ -36,6 +35,7 @@ class PrintStatementRequest extends FormRequest
function ($attribute, $value, $fail) {
$query = Statement::where('account_number', $this->input('account_number'))
->where('authorization_status', '!=', 'rejected')
->where('is_available', true)
->where('period_from', $value);
// If this is an update request, exclude the current record
@@ -77,8 +77,6 @@ class PrintStatementRequest extends FormRequest
public function messages(): array
{
return [
'branch_code.required' => 'Branch code is required',
'branch_code.exists' => 'Selected branch does not exist',
'account_number.required' => 'Account number is required',
'period_from.required' => 'Period is required',
'period_from.regex' => 'Period must be in YYYYMM format',
@@ -106,13 +104,13 @@ class PrintStatementRequest extends FormRequest
$this->merge([
'period_to' => substr($this->period_to, 0, 4) . substr($this->period_to, 5, 2),
]);
}
// Convert is_period_range to boolean if it exists
if ($this->has('period_to')) {
$this->merge([
'is_period_range' => true,
]);
// Only set is_period_range to true if period_to is different from period_from
if ($this->period_to !== $this->period_from) {
$this->merge([
'is_period_range' => true,
]);
}
}
// Set default request_type if not provided

View File

@@ -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);
@@ -175,6 +177,8 @@
->whereNotNull('currency')
->where('currency', '!=', '')
->whereIn('ctdesc', $cardTypes)
->whereNotIn('product_code',['6002','6004','6042','6031'])
->where('branch','!=','ID0019999')
->get();
}
@@ -413,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;
}
}
}

View File

@@ -1,5 +1,4 @@
<?php
namespace Modules\Webstatement\Jobs;
use Exception;
@@ -11,6 +10,7 @@
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Storage;
use Modules\Webstatement\Models\StmtEntry;
use Illuminate\Support\Facades\DB;
class ProcessStmtEntryDataJob implements ShouldQueue
{
@@ -184,33 +184,57 @@
}
/**
* Save batched records to the database
* Simpan batch data ke database menggunakan updateOrCreate
* untuk menghindari error unique constraint
*
* @return void
*/
private function saveBatch()
: void
private function saveBatch(): void
{
Log::info('Memulai proses saveBatch dengan updateOrCreate');
DB::beginTransaction();
try {
if (!empty($this->entryBatch)) {
// Process in smaller chunks for better memory management
foreach ($this->entryBatch as $entry) {
// Extract all stmt_entry_ids from the current chunk
$entryIds = array_column($entry, 'stmt_entry_id');
$totalProcessed = 0;
// Delete existing records with these IDs to avoid conflicts
StmtEntry::whereIn('stmt_entry_id', $entryIds)->delete();
// Process each entry data directly (tidak ada nested array)
foreach ($this->entryBatch as $entryData) {
// Validasi bahwa entryData adalah array dan memiliki stmt_entry_id
if (is_array($entryData) && isset($entryData['stmt_entry_id'])) {
// Gunakan updateOrCreate untuk menghindari duplicate key error
StmtEntry::updateOrCreate(
[
'stmt_entry_id' => $entryData['stmt_entry_id']
],
$entryData
);
// Insert all records in the chunk at once
StmtEntry::insert($entry);
$totalProcessed++;
} else {
Log::warning('Invalid entry data structure', ['data' => $entryData]);
$this->errorCount++;
}
}
// Reset entry batch after processing
DB::commit();
Log::info("Berhasil memproses {$totalProcessed} record dengan updateOrCreate");
// Reset entry batch after successful processing
$this->entryBatch = [];
}
} catch (Exception $e) {
DB::rollback();
Log::error("Error in saveBatch: " . $e->getMessage() . "\n" . $e->getTraceAsString());
$this->errorCount += count($this->entryBatch);
// Reset batch even if there's an error to prevent reprocessing the same failed records
$this->entryBatch = [];
throw $e;
}
}

View File

@@ -1,418 +1,425 @@
<?php
namespace Modules\Webstatement\Jobs;
namespace Modules\Webstatement\Jobs;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Mail;
use Illuminate\Support\Facades\Storage;
use Modules\Webstatement\Models\Account;
use Modules\Webstatement\Models\PrintStatementLog;
use Modules\Webstatement\Mail\StatementEmail;
use Modules\Basicdata\Models\Branch;
/**
* Job untuk mengirim email PDF statement ke nasabah
* Mendukung pengiriman per rekening, per cabang, atau seluruh cabang
*/
class SendStatementEmailJob implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
protected $period;
protected $requestType;
protected $targetValue; // account_number, branch_code, atau null untuk all
protected $batchId;
protected $logId;
use Exception;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Mail;
use Illuminate\Support\Facades\Storage;
use InvalidArgumentException;
use Modules\Webstatement\Mail\StatementEmail;
use Modules\Webstatement\Models\Account;
use Modules\Webstatement\Models\PrintStatementLog;
use Throwable;
/**
* 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
* Job untuk mengirim email PDF statement ke nasabah
* Mendukung pengiriman per rekening, per cabang, atau seluruh cabang
*/
public function __construct($period, $requestType = 'single_account', $targetValue = null, $batchId = null, $logId = null)
class SendStatementEmailJob implements ShouldQueue
{
$this->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()
]);
}
}

View File

@@ -0,0 +1,379 @@
<?php
namespace Modules\Webstatement\Jobs;
use Exception;
use Illuminate\Bus\Queueable;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
use Illuminate\Queue\SerializesModels;
use Illuminate\Queue\InteractsWithQueue;
use Modules\Webstatement\Models\Atmcard;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Modules\Webstatement\Models\KartuSyncLog;
class UpdateAllAtmCardsBatchJob implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
/**
* Konstanta untuk konfigurasi batch processing
*/
private const BATCH_SIZE = 100;
private const MAX_EXECUTION_TIME = 7200; // 2 jam dalam detik
private const DELAY_BETWEEN_JOBS = 2; // 2 detik delay antar job
private const MAX_DELAY_SPREAD = 300; // Spread maksimal 5 menit
/**
* ID log sinkronisasi
*
* @var int
*/
protected $syncLogId;
/**
* Model log sinkronisasi
*
* @var KartuSyncLog
*/
protected $syncLog;
/**
* Batch size untuk processing
*
* @var int
*/
protected $batchSize;
/**
* Filter kondisi kartu yang akan diupdate
*
* @var array
*/
protected $filters;
/**
* Create a new job instance.
*
* @param int|null $syncLogId ID log sinkronisasi
* @param int $batchSize Ukuran batch untuk processing
* @param array $filters Filter kondisi kartu
*/
public function __construct(?int $syncLogId = null, int $batchSize = self::BATCH_SIZE, array $filters = [])
{
$this->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
]);
}
}
}

View File

@@ -4,13 +4,14 @@ namespace Modules\Webstatement\Jobs;
use Exception;
use Illuminate\Bus\Queueable;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Http;
use Illuminate\Queue\SerializesModels;
use Illuminate\Queue\InteractsWithQueue;
use Modules\Webstatement\Models\Account;
use Modules\Webstatement\Models\Atmcard;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Log;
use Modules\Webstatement\Models\Atmcard;
class UpdateAtmCardBranchCurrencyJob implements ShouldQueue
{
@@ -77,7 +78,7 @@ class UpdateAtmCardBranchCurrencyJob implements ShouldQueue
}
/**
* Get account information from the API
* Get account information from Account model or API
*
* @param string $accountNumber
* @return array|null
@@ -85,10 +86,26 @@ class UpdateAtmCardBranchCurrencyJob implements ShouldQueue
private function getAccountInfo(string $accountNumber): ?array
{
try {
// Coba dapatkan data dari model Account terlebih dahulu
$account = Account::where('account_number', $accountNumber)->first();
if ($account) {
// Jika account ditemukan, format data sesuai dengan format response dari API
return [
'responseCode' => '00',
'acctCompany' => $account->branch_code,
'acctCurrency' => $account->currency,
'acctType' => $account->open_category
// Tambahkan field lain yang mungkin diperlukan
];
}
// Jika tidak ditemukan di database, ambil dari Fiorano API
$url = env('FIORANO_URL') . self::API_BASE_PATH;
$path = self::API_INQUIRY_PATH;
$data = [
'accountNo' => $accountNumber
'accountNo' => $accountNumber,
];
$response = Http::post($url . $path, $data);
@@ -110,6 +127,7 @@ class UpdateAtmCardBranchCurrencyJob implements ShouldQueue
$cardData = [
'branch' => !empty($accountInfo['acctCompany']) ? $accountInfo['acctCompany'] : null,
'currency' => !empty($accountInfo['acctCurrency']) ? $accountInfo['acctCurrency'] : null,
'product_code' => !empty($accountInfo['acctType']) ? $accountInfo['acctType'] : null,
];
$this->card->update($cardData);

View File

@@ -1,79 +1,204 @@
<?php
namespace Modules\Webstatement\Mail;
namespace Modules\Webstatement\Mail;
use Illuminate\Bus\Queueable;
use Illuminate\Mail\Mailable;
use Illuminate\Queue\SerializesModels;
use Modules\Webstatement\Models\Account;
use Modules\Webstatement\Models\PrintStatementLog;
use Carbon\Carbon;
use Exception;
use Illuminate\Bus\Queueable;
use Illuminate\Mail\Mailable;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Config;
use Log;
use Modules\Webstatement\Models\Account;
use Modules\Webstatement\Models\PrintStatementLog;
use Symfony\Component\Mailer\Mailer;
use Symfony\Component\Mailer\Transport\Smtp\EsmtpTransport;
use Symfony\Component\Mime\Email;
class StatementEmail extends Mailable
{
use Queueable, SerializesModels;
protected $statement;
protected $filePath;
protected $isZip;
/**
* 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)
class StatementEmail extends Mailable
{
$this->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;
}
}
}

View File

@@ -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']
]);
}
}

View File

@@ -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
]);
}

View File

@@ -0,0 +1,63 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\DB;
return new class extends Migration
{
/**
* Menjalankan migration untuk menambahkan field product_code pada tabel atmcards
*
* @return void
*/
public function up(): void
{
Log::info('Memulai migration: menambahkan field product_code ke tabel atmcards');
DB::beginTransaction();
try {
Schema::table('atmcards', function (Blueprint $table) {
// Menambahkan field product_code setelah field ctdesc
$table->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;
}
}
};

View File

@@ -30,7 +30,8 @@
"attributes": [],
"permission": "",
"roles": [
"administrator"
"administrator",
"customer_service"
]
},
{

View File

@@ -16,8 +16,8 @@
}
.container {
max-width: 90%;
margin: 20px auto;
max-width: 100%;
margin: 0px auto;
background-color: #ffffff;
border-radius: 8px;
overflow: hidden;
@@ -37,7 +37,7 @@
}
.content {
padding: 30px;
padding: 5px;
font-size: 14px;
}
@@ -89,25 +89,21 @@
Silahkan gunakan password Electronic Statement Anda untuk membukanya.<br><br>
Password standar Elektronic Statement ini adalah <strong>ddMonyyyyxx</strong> (contoh: 01Aug1970xx)
dimana :
<ul class="dashed-list">
<li>dd : <strong>2 digit</strong> tanggal lahir anda, contoh: 01</li>
<li>Mon :
<ul style="list-style-type: none;">
<li>- dd : <strong>2 digit</strong> tanggal lahir anda, contoh: 01</li>
<li>- Mon :
<strong>3 huruf pertama</strong> bulan lahir anda dalam bahasa Ingris. Huruf pertama adalah
huruf besar dan selanjutnya huruf kecil, contoh : Aug
</li>
<li>yyyy : <strong>4 digit</strong> tahun kelahiran anda, contoh : 1970</li>
<li>xx : <strong>2 digit terakhir</strong> dari nomer rekening anda, contoh : 12</li>
<li>- yyyy : <strong>4 digit</strong> tahun kelahiran anda, contoh : 1970</li>
<li>- xx : <strong>2 digit terakhir</strong> dari nomer rekening anda, contoh : 12</li>
</ul>
<br>
Terima Kasih,<br><br>
<strong>Bank Artha Graha Internasional</strong><br>
------------------------------
<wbr>
------------------------------
<wbr>
--------<br>
------------------------------------------------------------<br>
Kami sangat menghargai masukan dan saran Anda untuk meningkatkan layanan dan produk kami.<br>
Untuk memberikan masukan, silakan hubungi <strong>GrahaCall 24 Jam</strong> kami di
<strong>0-800-191-8880</strong>.<br><br><br>
@@ -118,25 +114,21 @@
Please use your Electronic Statement password to open it.<br><br>
The Electronic Statement standard password is <strong>ddMonyyyyxx</strong> (example: 01Aug1970xx) where:
<ul class="dashed-list">
<li>dd : <strong>The first 2 digits</strong> of your birthdate, example: 01</li>
<li>Mon :
<ul style="list-style-type: none;">
<li>- dd : <strong>The first 2 digits</strong> of your birthdate, example: 01</li>
<li>- Mon :
<strong>The first 3 letters</strong> of your birth month in English. The first letter is
uppercase and the rest are lowercase, example: Aug
</li>
<li>yyyy : <strong>4 digit</strong> of your birth year, example: 1970</li>
<li>xx : <strong>The last 2 digits</strong> of your account number, example: 12.</li>
<li>- yyyy : <strong>4 digit</strong> of your birth year, example: 1970</li>
<li>- xx : <strong>The last 2 digits</strong> of your account number, example: 12.</li>
</ul>
<br>
Regards,<br><br>
<strong>Bank Artha Graha Internasional</strong><br>
------------------------------
<wbr>
------------------------------
<wbr>
--------<br>
------------------------------------------------------------<br>
We welcome any feedback or suggestions to improve our product and services.<br>
If you have any feedback, please contact our <strong>GrahaCall 24 Hours</strong> at
<strong>0-800-191-8880</strong>.
@@ -145,10 +137,6 @@
</div>
</div>
</div>
<div class="footer">
<p>© {{ date('Y') }} Bank Artha Graha Internasional. All rights reserved.</p>
<p>Jika Anda memiliki pertanyaan, silakan hubungi customer service kami.</p>
</div>
</div>
</body>

View File

@@ -11,41 +11,59 @@
<h3 class="card-title">Request Print Stetement</h3>
</div>
<div class="card-body">
<form action="{{ isset($statement) ? route('statements.update', $statement->id) : route('statements.store') }}" method="POST">
<form
action="{{ isset($statement) ? route('statements.update', $statement->id) : route('statements.store') }}"
method="POST">
@csrf
@if(isset($statement))
@if (isset($statement))
@method('PUT')
@endif
<div class="grid grid-cols-1 gap-5">
<div class="form-group">
<label class="form-label required" for="branch_code">Branch</label>
<select class="select tomselect @error('branch_code') is-invalid @enderror" id="branch_code" name="branch_code" required>
<option value="">Select Branch</option>
@foreach($branches as $branch)
<option value="{{ $branch->code }}" {{ (old('branch_code', $statement->branch_code ?? '') == $branch->code) ? 'selected' : '' }}>
{{ $branch->name }}
</option>
@endforeach
</select>
@error('branch_code')
<div class="invalid-feedback">{{ $message }}</div>
@enderror
</div>
@if ($multiBranch)
<div class="form-group">
<label class="form-label required" for="branch_id">Branch/Cabang</label>
<select class="input form-control tomselect @error('branch_id') is-invalid @enderror"
id="branch_id" name="branch_id" required>
<option value="">Pilih Branch/Cabang</option>
@foreach ($branches as $branchOption)
<option value="{{ $branchOption->code }}"
{{ old('branch_id', $statement->branch_id ?? ($branch->code ?? '')) == $branchOption->code ? 'selected' : '' }}>
{{ $branchOption->code }} - {{ $branchOption->name }}
</option>
@endforeach
</select>
@error('branch_id')
<div class="invalid-feedback">{{ $message }}</div>
@enderror
</div>
@else
<div class="form-group">
<label class="form-label" for="branch_display">Branch/Cabang</label>
<input type="text" class="input form-control" id="branch_display"
value="{{ $branch->code ?? '' }} - {{ $branch->name ?? '' }}" readonly>
<input type="hidden" name="branch_id" value="{{ $branch->code ?? '' }}">
</div>
@endif
<div class="form-group">
<label class="form-label required" for="account_number">Account Number</label>
<input type="text" class="input form-control @error('account_number') is-invalid @enderror" id="account_number" name="account_number" value="{{ old('account_number', $statement->account_number ?? '') }}" required>
<input type="text" class="input form-control @error('account_number') is-invalid @enderror"
id="account_number" name="account_number"
value="{{ old('account_number', $statement->account_number ?? '') }}" required>
@error('account_number')
<div class="invalid-feedback">{{ $message }}</div>
<div class="invalid-feedback">{{ $message }}</div>
@enderror
</div>
<div class="form-group">
<label class="form-label" for="email">Email</label>
<input type="email" class="input form-control @error('email') is-invalid @enderror" id="email" name="email" value="{{ old('email', $statement->email ?? '') }}" placeholder="Optional email for send statement">
<input type="email" class="input form-control @error('email') is-invalid @enderror"
id="email" name="email" value="{{ old('email', $statement->email ?? '') }}"
placeholder="Optional email for send statement">
@error('email')
<div class="invalid-feedback">{{ $message }}</div>
<div class="invalid-feedback">{{ $message }}</div>
@enderror
</div>
@@ -53,24 +71,21 @@
<label class="form-label required" for="start_date">Start Date</label>
<input class="input @error('period_from') border-danger bg-danger-light @enderror"
type="month"
name="period_from"
value="{{ $statement->period_from ?? old('period_from') }}"
max="{{ date('Y-m', strtotime('-1 month')) }}">
type="month" name="period_from"
value="{{ $statement->period_from ?? old('period_from') }}"
max="{{ date('Y-m', strtotime('-1 month')) }}">
@error('period_from')
<em class="alert text-danger text-sm">{{ $message }}</em>
<em class="text-sm alert text-danger">{{ $message }}</em>
@enderror
</div>
<div class="form-group">
<label class="form-label required" for="end_date">End Date</label>
<input class="input @error('period_to') border-danger bg-danger-light @enderror"
type="month"
name="period_to"
value="{{ $statement->period_to ?? old('period_to') }}"
max="{{ date('Y-m', strtotime('-1 month')) }}">
<input class="input @error('period_to') border-danger bg-danger-light @enderror" type="month"
name="period_to" value="{{ $statement->period_to ?? old('period_to') }}"
max="{{ date('Y-m', strtotime('-1 month')) }}">
@error('period_to')
<em class="alert text-danger text-sm">{{ $message }}</em>
<em class="text-sm alert text-danger">{{ $message }}</em>
@enderror
</div>
</div>
@@ -85,8 +100,9 @@
</div>
</div>
<div class="col-span-6">
<div class="card card-grid min-w-full" data-datatable="false" data-datatable-page-size="10" data-datatable-state-save="false" id="statement-table" data-api-url="{{ route('statements.datatables') }}">
<div class="card-header py-5 flex-wrap">
<div class="min-w-full card card-grid" data-datatable="false" data-datatable-page-size="10"
data-datatable-state-save="false" id="statement-table" data-api-url="{{ route('statements.datatables') }}">
<div class="flex-wrap py-5 card-header">
<h3 class="card-title">
Daftar Statement Request
</h3>
@@ -100,55 +116,54 @@
</div>
<div class="card-body">
<div class="scrollable-x-auto">
<table class="table table-auto table-border align-middle text-gray-700 font-medium text-sm" data-datatable-table="true">
<table class="table text-sm font-medium text-gray-700 align-middle table-auto table-border"
data-datatable-table="true">
<thead>
<tr>
<th class="w-14">
<input class="checkbox checkbox-sm" data-datatable-check="true" type="checkbox"/>
</th>
<th class="min-w-[100px]" data-datatable-column="id">
<span class="sort"> <span class="sort-label"> ID </span>
<span class="sort-icon"> </span> </span>
</th>
<th class="min-w-[150px]" data-datatable-column="branch_name">
<span class="sort"> <span class="sort-label"> Branch </span>
<span class="sort-icon"> </span> </span>
</th>
<th class="min-w-[150px]" data-datatable-column="account_number">
<span class="sort"> <span class="sort-label"> Account Number </span>
<span class="sort-icon"> </span> </span>
</th>
<th class="min-w-[150px]" data-datatable-column="period">
<span class="sort"> <span class="sort-label"> Period </span>
<span class="sort-icon"> </span> </span>
</th>
<th class="min-w-[150px]" data-datatable-column="authorization_status">
<span class="sort"> <span class="sort-label"> Status </span>
<span class="sort-icon"> </span> </span>
</th>
<th class="min-w-[150px]" data-datatable-column="is_available">
<span class="sort"> <span class="sort-label"> Available </span>
<span class="sort-icon"> </span> </span>
</th>
<th class="min-w-[150px]" data-datatable-column="remarks">
<span class="sort"> <span class="sort-label"> Notes </span>
<span class="sort-icon"> </span> </span>
</th>
<th class="min-w-[180px]" data-datatable-column="created_at">
<span class="sort"> <span class="sort-label"> Created At </span>
<span class="sort-icon"> </span> </span>
</th>
<th class="min-w-[50px] text-center" data-datatable-column="actions">Action</th>
</tr>
<tr>
<th class="w-14">
<input class="checkbox checkbox-sm" data-datatable-check="true" type="checkbox" />
</th>
<th class="min-w-[100px]" data-datatable-column="id">
<span class="sort"> <span class="sort-label"> ID </span>
<span class="sort-icon"> </span> </span>
</th>
<th class="min-w-[150px]" data-datatable-column="branch_name">
<span class="sort"> <span class="sort-label"> Branch </span>
<span class="sort-icon"> </span> </span>
</th>
<th class="min-w-[150px]" data-datatable-column="account_number">
<span class="sort"> <span class="sort-label"> Account Number </span>
<span class="sort-icon"> </span> </span>
</th>
<th class="min-w-[150px]" data-datatable-column="period">
<span class="sort"> <span class="sort-label"> Period </span>
<span class="sort-icon"> </span> </span>
</th>
<th class="min-w-[150px]" data-datatable-column="is_available">
<span class="sort"> <span class="sort-label"> Available </span>
<span class="sort-icon"> </span> </span>
</th>
<th class="min-w-[150px]" data-datatable-column="remarks">
<span class="sort"> <span class="sort-label"> Notes </span>
<span class="sort-icon"> </span> </span>
</th>
<th class="min-w-[180px]" data-datatable-column="created_at">
<span class="sort"> <span class="sort-label"> Created At </span>
<span class="sort-icon"> </span> </span>
</th>
<th class="min-w-[50px] text-center" data-datatable-column="actions">Action</th>
</tr>
</thead>
</table>
</div>
<div class="card-footer justify-center md:justify-between flex-col md:flex-row gap-3 text-gray-600 text-2sm font-medium">
<div class="flex items-center gap-2">
<div
class="flex-col gap-3 justify-center font-medium text-gray-600 card-footer md:justify-between md:flex-row text-2sm">
<div class="flex gap-2 items-center">
Show
<select class="select select-sm w-16" data-datatable-size="true" name="perpage"> </select> per page
<select class="w-16 select select-sm" data-datatable-size="true" name="perpage"> </select>
per page
</div>
<div class="flex items-center gap-4">
<div class="flex gap-4 items-center">
<span data-datatable-info="true"> </span>
<div class="pagination" data-datatable-pagination="true">
</div>
@@ -162,6 +177,10 @@
@push('scripts')
<script type="text/javascript">
/**
* Fungsi untuk menghapus data statement
* @param {number} data - ID statement yang akan dihapus
*/
function deleteData(data) {
Swal.fire({
title: 'Are you sure?',
@@ -175,7 +194,7 @@
if (result.isConfirmed) {
$.ajaxSetup({
headers: {
'X-CSRF-TOKEN': '{{ csrf_token() }}'
'X-CSRF-TOKEN': '{{ csrf_token() }}'
}
});
@@ -192,6 +211,56 @@
}
})
}
/**
* Konfirmasi email sebelum submit form
* Menampilkan SweetAlert jika email diisi untuk konfirmasi pengiriman
*/
document.addEventListener('DOMContentLoaded', function() {
const form = document.querySelector('form');
const emailInput = document.getElementById('email');
// Log: Inisialisasi event listener untuk konfirmasi email
console.log('Email confirmation listener initialized');
form.addEventListener('submit', function(e) {
const emailValue = emailInput.value.trim();
// Jika email diisi, tampilkan konfirmasi
if (emailValue) {
e.preventDefault(); // Hentikan submit form sementara
// Log: Email terdeteksi, menampilkan konfirmasi
console.log('Email detected:', emailValue);
Swal.fire({
title: 'Konfirmasi Pengiriman Email',
text: `Apakah Anda yakin ingin mengirimkan statement ke email: ${emailValue}?`,
icon: 'question',
showCancelButton: true,
confirmButtonColor: '#3085d6',
cancelButtonColor: '#d33',
confirmButtonText: 'Ya, Kirim Email',
cancelButtonText: 'Batal',
reverseButtons: true
}).then((result) => {
if (result.isConfirmed) {
// Log: User konfirmasi pengiriman email
console.log('User confirmed email sending');
// Submit form setelah konfirmasi
form.submit();
} else {
// Log: User membatalkan pengiriman email
console.log('User cancelled email sending');
}
});
} else {
// Log: Tidak ada email, submit form normal
console.log('No email provided, submitting form normally');
}
});
});
</script>
<script type="module">
@@ -243,31 +312,16 @@
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 `<span class="${statusClass}">${data.authorization_status}</span>`;
},
},
is_available: {
title: 'Available',
render: (item, data) => {
let statusClass = data.is_available ? 'badge badge-light-success' : 'badge badge-light-danger';
let statusClass = data.is_available ? 'badge badge-light-success' :
'badge badge-light-danger';
let statusText = data.is_available ? 'Yes' : 'No';
return `<span class="${statusClass}">${statusText}</span>`;
},
},
remarks : {
remarks: {
title: 'Notes',
},
created_at: {
@@ -315,7 +369,7 @@
let dataTable = new KTDataTable(element, dataTableOptions);
// Custom search functionality
searchInput.addEventListener('input', function () {
searchInput.addEventListener('input', function() {
const searchValue = this.value.trim();
dataTable.search(searchValue, true);
});

View File

@@ -73,19 +73,6 @@
</div>
</div>
<div class="d-flex flex-column">
<div class="text-gray-500 fw-semibold">Status</div>
<div>
@if($statement->authorization_status === 'pending')
<span class="badge badge-warning">Pending Authorization</span>
@elseif($statement->authorization_status === 'approved')
<span class="badge badge-success">Approved</span>
@elseif($statement->authorization_status === 'rejected')
<span class="badge badge-danger">Rejected</span>
@endif
</div>
</div>
<div class="d-flex flex-column">
<div class="text-gray-500 fw-semibold">Availability</div>
<div>