Compare commits

...

21 Commits

Author SHA1 Message Date
Daeng Deni Mardaeni
7af5bf2fe5 feat(customer): tambah field alamat lengkap & pemrosesan CSV dinamis
- Tambah 13 field alamat KTP & domisili di tabel customers (nullable, aman untuk rollback)
- Update model Customer: fillable & casting tanggal
- ProcessCustomerDataJob: header CSV dinamis, mapping otomatis, trim value
- Batch save pakai DB::transaction(), logging detail, error handling lengkap
- Fleksibel untuk CSV dengan header bervariasi & backward-compatible
2025-08-07 08:45:50 +07:00
Daeng Deni Mardaeni
8a6469ecc9 🔧 fix(job): perbaiki transaksi bersarang & optimasi batch processing di GenerateClosingBalanceReportJob
- Hapus transaksi bersarang:
  - Pindahkan DB::beginTransaction() & DB::commit() dari processAndSaveClosingBalanceData() ke handle() menggunakan DB::transaction()
  - Cegah error max_lock_per_transaction pada PostgreSQL

- Implementasi batch processing:
  - Tambah metode batchUpdateOrCreate()
  - Ambil data existing dalam satu query via whereIn()
  - Pisahkan data menjadi toInsert & toUpdate
  - Gunakan DB::table()->insert() untuk batch insert
  - Lakukan update individual untuk data yang sudah ada

- Penyederhanaan error handling:
  - Hapus try-catch di processAndSaveClosingBalanceData() karena sudah ditangani di handle()

- Penambahan logging:
  - Tambah log informasi untuk monitoring batch insert/update

- Optimasi performa:
  - Kurangi beban database & mencegah duplikasi data
  - Gunakan pendekatan "delete-first, then insert" seperti ExportStatementJob
2025-08-05 13:49:58 +07:00
Daeng Deni Mardaeni
aae0c4ab15 ♻️ refactor(GenerateClosingBalanceReportJob): Sederhanakan penanganan duplikasi sesuai pendekatan ExportStatementJob
- Menambahkan method `updateOrCreate` untuk penanganan duplikasi yang lebih efisien
- Menghapus method `deleteExistingProcessedDataWithVerification` dengan verifikasi kompleks
- Menghapus method `verifyNoDuplicatesAfterInsert` yang tidak diperlukan lagi
- Menghapus method `generateReportData` yang sudah tidak digunakan
- Menghapus method `buildReportDataRow` yang menjadi bagian dari method di atas
- Menghapus method `exportToCsv` yang sudah tidak relevan
- Menyederhanakan query penghapusan data dengan menghapus kondisi `group_name`
- Mengubah pendekatan dari insertOrIgnore menjadi updateOrCreate untuk penanganan duplikasi
- Menghapus kode yang tidak digunakan untuk mengurangi kompleksitas
2025-08-04 11:34:00 +07:00
Daeng Deni Mardaeni
150d52f8da refactor(GenerateClosingBalanceReportJob): Sederhanakan pendekatan eliminasi duplikasi seperti ExportStatementJob
- Menghapus logika duplikasi yang kompleks dengan unique_hash dan pengecekan duplikasi
- Menyederhanakan proses delete data existing seperti pada ExportStatementJob
- Menghapus lock table dan verifikasi duplikasi yang rumit
- Menggunakan pendekatan langsung: hapus data lama -> proses ulang -> insert baru
- Menghapus field unique_hash yang tidak diperlukan lagi
- Menyederhanakan query builder tanpa eliminasi duplicate yang kompleks

Perubahan ini mengikuti pola yang sudah terbukti berhasil pada ExportStatementJob,
yang tidak memiliki masalah duplikasi dengan pendekatan yang lebih sederhana.
2025-08-04 09:25:48 +07:00
Daeng Deni Mardaeni
8736ccf5f8 feat(closing-balance): Implementasi Pengecekan Unique Hash dan Sequence Counter pada Ekspor CSV
Memperbaiki logika ekspor CSV laporan penutupan saldo untuk memastikan konsistensi data dan urutan sequence yang benar.

Perubahan yang dilakukan:
• Menambahkan pengecekan unique_hash untuk mencegah duplikasi data dalam file CSV
• Mengimplementasikan array $processedHashes untuk melacak unique_hash yang sudah diproses
• Mengganti sequence_no dari database dengan counter yang bertambah ($sequenceCounter++)
• Menghapus groupBy('unique_hash') dari query untuk kontrol duplikasi yang lebih baik
• Menambahkan logging untuk record yang di-skip karena duplikasi unique_hash
• Menambahkan logging untuk tracking jumlah record yang diproses per chunk
• Menggunakan reference (&$sequenceCounter, &$processedHashes) untuk konsistensi data antar chunk
• Memastikan sequence number berurutan tanpa gap dimulai dari 1
2025-07-31 11:55:55 +07:00
Daeng Deni Mardaeni
710cbb5232 feat(closing-balance): implementasi unique_hash dan insertOrIgnore untuk eliminasi duplikasi
Perbaikan masalah duplikasi pada laporan penutupan saldo dengan pendekatan hash unik dan query insert yang toleran terhadap duplikasi.

Perubahan:
- Tambah kolom `unique_hash` pada tabel `processed_closing_balances` (via migrasi `2025_07_31_035159_add_unique_hash_field_to_processed_closing_balances_table.php`)
- Tambah field `unique_hash` ke `$fillable` pada model `ProcessedClosingBalance`
- Update logika generate unique key di `prepareProcessedClosingBalanceData()` menggunakan `md5(trans_reference + '_' + amount_lcy)`
- Query pencarian duplikasi berdasarkan `unique_hash`, bukan `trans_reference` saja
- Ganti `insert()` dengan `insertOrIgnore()` untuk mencegah error saat insert duplikat data

Dampak:
- Duplikasi data dihindari secara efektif lewat hash unik
- Tidak ada error meski data duplicate ditemukan, karena query otomatis mengabaikannya
- `trans_reference` yang sama tetap valid selama nilai `amount_lcy` berbeda
- Data laporan lebih konsisten dan terhindar dari konflik constraint
2025-07-31 11:06:11 +07:00
Daeng Deni Mardaeni
13e077073b fix(closing-balance): Perbaikan logika pengecekan duplikasi untuk memperbolehkan trans_reference duplikat dengan amount_lcy berbeda
- Mengubah kriteria pengecekan duplikasi dari hanya trans_reference menjadi kombinasi trans_reference + amount_lcy
- Memperbarui query existingReferences untuk memeriksa kombinasi trans_reference dan amount_lcy
- Memperbolehkan trans_reference yang sama selama amount_lcy berbeda value
- Menambahkan validasi yang lebih presisi untuk mencegah duplikasi data yang sebenarnya
- Mengurangi false positive pada pengecekan duplikasi
2025-07-31 10:35:05 +07:00
Daeng Deni Mardaeni
eff951c600 feat(webstatement): implementasi perbaikan duplikasi data dan optimasi GenerateClosingBalanceReportJob
Perubahan komprehensif pada GenerateClosingBalanceReportJob.php untuk mengatasi masalah duplikasi data dan meningkatkan performa:

**Perbaikan Duplikasi Data:**
- Menambahkan metode deleteExistingProcessedDataWithVerification() untuk penghapusan data yang lebih aman dengan verifikasi lengkap
- Implementasi verifyNoDuplicatesAfterInsert() untuk memastikan tidak ada duplikasi setelah insert data
- Penyederhanaan logika pengecekan duplikasi berdasarkan trans_reference dan amount_lcy saja, mengingat trans_reference bersifat unik secara global
- Perbaikan buildTransactionQuery() dengan eliminasi duplikasi yang lebih efektif menggunakan subquery

**Fitur Export CSV:**
- Menambahkan metode exportFromDatabaseToCsv() untuk export data langsung dari database ke CSV dengan performa tinggi
- Implementasi chunking untuk menangani dataset besar secara efisien
- Pengurutan data berdasarkan booking_date dan transaction_date untuk konsistensi output
- Struktur header CSV yang lengkap dengan 26 kolom sesuai kebutuhan laporan

**Utilitas Tambahan:**
- Menambahkan getProcessedRecordCount() untuk monitoring jumlah record yang diproses
- Implementasi getOpeningBalance() dengan logika penanganan periode sebelumnya
- Perbaikan handling untuk periode khusus (20250512) dengan pengurangan 2 hari
- Fallback ke saldo 0 jika account balance tidak ditemukan

**Optimasi Query:**
- Perbaikan eager loading pada relasi 'ft' dan 'dc' dengan select kolom spesifik
- Implementasi subquery untuk mendapatkan ID unik berdasarkan kombinasi trans_reference dan amount_lcy
- Penggunaan MIN(id) dan MIN(date_time) untuk konsistensi data

**Logging dan Monitoring:**
- Penambahan logging komprehensif di setiap tahap proses
- Monitoring ukuran file dan verifikasi keberhasilan export
- Warning log untuk kasus account balance tidak ditemukan
- Info log untuk tracking opening balance dan processed record count

**Perbaikan Teknis:**
- Fix syntax error pada import ShouldQueue (menambahkan semicolon yang hilang)
- Perbaikan indentasi dan formatting kode untuk konsistensi
- Penambahan spacing yang tepat antar metode

**Dampak:**
- Eliminasi duplikasi data pada tabel processed_closing_balances
- Peningkatan performa export dengan chunking dan direct database access
- Konsistensi data yang lebih baik dengan verifikasi berlapis
- Monitoring dan debugging yang lebih mudah dengan logging yang komprehensif
- Kemudahan maintenance dengan struktur kode yang lebih terorganisir

Perubahan ini memastikan integritas data, meningkatkan performa, dan memberikan monitoring yang lebih baik untuk proses generate closing balance report.
2025-07-31 10:19:50 +07:00
Daeng Deni Mardaeni
6ad5aff358 refactor(webstatement): Sederhanakan pengecekan duplicate pada GenerateClosingBalanceReportJobrefactor(webstatement): Sederhanakan pengecekan duplicate pada GenerateClosingBalanceReportJob
Menyederhanakan logika pengecekan duplicate berdasarkan karakteristik trans_reference yang sudah unique secara global, sehingga cukup menggunakan kombinasi trans_reference + amount_lcy tanpa perlu filter berdasarkan account_number dan period.

Perubahan:
- Simplifikasi verifyNoDuplicatesAfterInsert dengan pengecekan trans_reference + amount_lcy saja
- Perbaikan buildTransactionQuery dengan groupBy yang disederhanakan
- Menghapus groupBy booking_date karena trans_reference sudah unique
- Optimasi performa query dengan grouping yang lebih efisien
- Penyesuaian logging untuk mencerminkan logika yang disederhanakan
- Mempertahankan pengecekan sequence_no duplicate dalam scope yang tepat

Logika Bisnis:
- Trans_reference adalah unique identifier global
- Trans_reference hanya bisa duplicate jika amount_lcy berbeda
- Tidak perlu filter berdasarkan account_number/period untuk pengecekan duplicate
- Fokus pada kombinasi trans_reference + amount_lcy sebagai key uniqueness

Dampak:
- Query yang lebih efisien dan cepat
- Logika yang lebih sesuai dengan karakteristik data
- Maintenance code yang lebih mudah
- Performa duplicate detection yang lebih baik

Rekomendasi:
- Tambahkan unique constraint (trans_reference, amount_lcy) di database
- Monitor performa setelah simplifikasi
- Validasi hasil dengan data production

Menyederhanakan logika pengecekan duplicate berdasarkan karakteristik trans_reference yang sudah unique secara global, sehingga cukup menggunakan kombinasi trans_reference + amount_lcy tanpa perlu filter berdasarkan account_number dan period.

Perubahan:
- Simplifikasi verifyNoDuplicatesAfterInsert dengan pengecekan trans_reference + amount_lcy saja
- Perbaikan buildTransactionQuery dengan groupBy yang disederhanakan
- Menghapus groupBy booking_date karena trans_reference sudah unique
- Optimasi performa query dengan grouping yang lebih efisien
- Penyesuaian logging untuk mencerminkan logika yang disederhanakan
- Mempertahankan pengecekan sequence_no duplicate dalam scope yang tepat

Logika Bisnis:
- Trans_reference adalah unique identifier global
- Trans_reference hanya bisa duplicate jika amount_lcy berbeda
- Tidak perlu filter berdasarkan account_number/period untuk pengecekan duplicate
- Fokus pada kombinasi trans_reference + amount_lcy sebagai key uniqueness

Dampak:
- Query yang lebih efisien dan cepat
- Logika yang lebih sesuai dengan karakteristik data
- Maintenance code yang lebih mudah
- Performa duplicate detection yang lebih baik

Rekomendasi:
- Tambahkan unique constraint (trans_reference, amount_lcy) di database
- Monitor performa setelah simplifikasi
- Validasi hasil dengan data production
2025-07-31 10:08:23 +07:00
Daeng Deni Mardaeni
bd72eb7dfa fix(webstatement): perbaiki eliminasi duplicate pada GenerateClosingBalanceReportJob
Mengganti pendekatan eliminasi duplicate dari validasi di level aplikasi menjadi di level database untuk menangani kasus duplikat dengan sequence yang tidak berdekatan.

Perubahan yang dilakukan:
- Mengimplementasikan subquery untuk mendapatkan ID unik berdasarkan kombinasi `trans_reference`, `amount_lcy`, dan `booking_date`
- Menghapus validasi duplicate berbasis array `$seenTransactions` di level aplikasi
- Menggunakan `whereIn` untuk filter transaksi berdasarkan hasil subquery ID unik
- Menyederhanakan method `prepareProcessedClosingBalanceData` karena data sudah bersih dari duplicate
- Menambahkan logging jumlah transaksi unik untuk monitoring
- Mengurangi beban proses sejak awal untuk meningkatkan performa
2025-07-31 09:32:14 +07:00
Daeng Deni Mardaeni
8eb7e69b21 fix(jobs): eliminasi duplicate trans_reference dengan amount_lcy yang sama pada GenerateClosingBalanceReportJob
Memperbaiki bug yang menyebabkan data duplikat pada hasil closing balance report karena penggunaan `distinct` yang tidak efektif.

Perubahan utama:

• Query Optimization untuk Duplicate Elimination:
- Mengganti subquery `whereRaw` dengan pendekatan `groupBy` untuk performa dan akurasi yang lebih baik
- Menggunakan `MIN(id)` dan `MIN(date_time)` untuk menjaga konsistensi data terpilih
- Menambahkan `groupBy` pada `trans_reference`, `amount_lcy`, dan `booking_date` untuk filter duplikat yang akurat

• Double Layer Validation:
- Validasi di level database: eliminasi duplikat menggunakan `groupBy`
- Validasi di level aplikasi: menggunakan array `$seenTransactions` untuk deteksi duplikat saat iterasi
- Transaksi yang terdeteksi sebagai duplikat akan dilewati (skip processing)

• Enhanced Logging:
- Menambahkan log saat duplicate transaction terdeteksi dan dilewati
- Log jumlah total duplikat yang ditemukan untuk audit dan monitoring
- Menambahkan log jumlah record yang dihapus saat `deleteExistingProcessedData()`

• Database Index Recommendations:
- Menambahkan saran index untuk mendukung operasi `groupBy`:
  - `(account_number, booking_date, trans_reference, amount_lcy)`
- Index ini akan membantu efisiensi dalam filtering dan deduplikasi data

• Compatibility:
- Eager loading tetap dipertahankan untuk efisiensi query
- Skema output CSV tidak berubah (tidak ada breaking changes)
- Proses chunk tetap digunakan untuk efisiensi memory

Testing:
- Validasi output CSV tidak mengandung duplikat `trans_reference` dengan `amount_lcy` sama
- Performa tetap cepat (±5–10 menit)
- Validasi hasil closing balance tetap akurat dan konsisten

Breaking Changes: Tidak ada
Performa: Terjaga dengan eliminasi duplikat yang efektif
2025-07-31 09:07:28 +07:00
Daeng Deni Mardaeni
4ee5c2e419 perf(jobs): optimasi performa GenerateClosingBalanceReportJob untuk mengurangi waktu eksekusi
Melakukan refactor besar-besaran untuk meningkatkan efisiensi proses GenerateClosingBalanceReportJob yang sebelumnya memakan waktu hingga 1 jam per hari mutasi.

Perubahan utama:

• Eager Loading Implementation:
- Menambahkan eager loading pada relasi `ft` dan `dc` di `buildTransactionQuery()`
- Menggunakan `select` spesifik untuk mengambil hanya kolom yang diperlukan
- Menghindari N+1 query problem yang memperlambat proses signifikan

• Query Optimization:
- Mengganti `distinct()` dengan raw SQL berbasis subquery untuk kinerja lebih baik
- Menyederhanakan `orderBy` hanya pada `booking_date` dan `date_time`
- Memperkuat WHERE clause dengan subquery untuk filtering `trans_reference` dan `amount_lcy`

• Logging Optimization:
- Menghapus logging per transaksi pada `processTransactionData()`
- Menyisakan logging hanya per chunk untuk efisiensi monitoring
- Mengurangi overhead I/O log hingga 50%

• Chunk Size & Memory:
- Meningkatkan `chunk size` dari 1.000 → 5.000 record per iterasi
- Mengurangi overhead koneksi database dan iterasi array
- Menambahkan timeout 3600 detik dan memory limit yang lebih longgar
- Mengaktifkan mekanisme retry untuk job berat

• Indexing Recommendations:
- Menambahkan rekomendasi index:
  - `(account_number, booking_date)` untuk filtering awal
  - `(trans_reference, amount_lcy)` untuk deduplikasi
  - Index FK `ref_no`, `id` untuk relasi antar tabel

• Code Structure & Error Handling:
- Dokumentasi fungsi diperjelas
- Menambahkan penggunaan DB transaction pada critical section
- Flow processing data dirapikan dan lebih modular
2025-07-31 08:42:23 +07:00
Daeng Deni Mardaeni
ca92f32ccb feat(webstatement): tambah dukungan queue name pada StagingController dan WebstatementController
Menambahkan parameter queue_name untuk mengatur queue spesifik pada job processing:

- Menambahkan parameter queueName pada StagingController.processData() dan index()
- Menggunakan onQueue() method saat dispatch job di StagingController
- Memperbarui ProcessDailyStaging untuk mengirim queue_name ke controller
- Menambahkan parameter Request pada WebstatementController untuk menerima queue_name
- Menggunakan onQueue() method saat dispatch job di WebstatementController
- Menambahkan logging untuk queue_name di semua proses
- Memperbarui response JSON untuk menyertakan informasi queue_name
- Menambahkan komentar fungsi yang menjelaskan parameter queue_name
- Mempertahankan backward compatibility dengan default queue 'default'
- Meningkatkan fleksibilitas dalam manajemen queue untuk berbagai environment
- Memungkinkan pemisahan job berdasarkan prioritas atau resource yang tersedia
2025-07-31 08:13:02 +07:00
Daeng Deni Mardaeni
e1740c0850 refactor(webstatement): ubah ordering pada GenerateClosingBalanceReportJob
Mengubah ordering pada proses generate closing balance report agar hanya menggunakan booking_date dan date_time:

- Menghapus ordering berdasarkan trans_reference dan amount_lcy pada buildTransactionQuery()
- Menggunakan ordering booking_date dan date_time pada query transaksi
- Mengubah ordering pada exportFromDatabaseToCsv() dari sequence_no menjadi booking_date dan transaction_date
- Menambahkan komentar untuk menjelaskan perubahan ordering
- Mempertahankan distinct pada kombinasi trans_reference dan amount_lcy untuk menghindari duplikasi
- Memastikan konsistensi ordering antara proses penyimpanan dan export CSV
- Meningkatkan performa dengan ordering yang lebih sederhana dan relevan
2025-07-30 17:35:34 +07:00
Daeng Deni Mardaeni
d88f4a242e perf(webstatement): optimasi performa GenerateClosingBalanceReportJob dengan database staging
Perubahan yang dilakukan:
- Menambahkan model `ProcessedClosingBalance` untuk menyimpan data sementara laporan closing balance
- Membuat migration `processed_closing_balances` dengan 26 kolom dan index komposit untuk query optimal
- Mengganti proses langsung ekspor ke CSV menjadi dua tahap:
  * Tahap 1: Proses dan simpan data ke DB secara bertahap melalui `processAndSaveClosingBalanceData()`
  * Tahap 2: Ekspor data dari DB ke CSV via `exportFromDatabaseToCsv()`
- Menambahkan method:
  * `deleteExistingProcessedData()` untuk membersihkan data lama
  * `prepareProcessedClosingBalanceData()` untuk batch insert ke DB
  * `getProcessedRecordCount()` untuk monitoring progres
- Mengoptimalkan memori dengan menghindari akumulasi data dalam array
- Menambahkan DB transaction untuk menjamin konsistensi data selama proses
- Logging diperluas agar progres lebih mudah dipantau
- Menambahkan error handling untuk menangani kegagalan proses dengan aman

Keuntungan:
- Waktu proses menurun drastis dari 1+ jam menjadi beberapa menit
- Skalabilitas meningkat — mampu menangani jutaan record tanpa memory overload
- Data hasil olahan dapat diekspor ulang tanpa harus re-process
- Pola kerja selaras dengan `ExportStatementJob` untuk konsistensi antar modul
- Monitoring dan debugging lebih mudah melalui database dan log

Catatan tambahan:
- Tabel `processed_closing_balances` mendukung kolom `group_name` untuk segmentasi (QRIS/NON_QRIS)
- Menggunakan tipe data numerik dengan presisi untuk nilai keuangan (`amount_lcy`, `balance`, dsb)
2025-07-30 17:25:32 +07:00
Daeng Deni Mardaeni
c0e5ddd37a perf(webstatement): optimasi performa GenerateClosingBalanceReportJob dengan database staging
Perubahan yang dilakukan:
- Menambahkan model `ProcessedClosingBalance` untuk menyimpan data sementara laporan closing balance
- Membuat migration `processed_closing_balances` dengan 26 kolom dan index komposit untuk query optimal
- Mengganti proses langsung ekspor ke CSV menjadi dua tahap:
  * Tahap 1: Proses dan simpan data ke DB secara bertahap melalui `processAndSaveClosingBalanceData()`
  * Tahap 2: Ekspor data dari DB ke CSV via `exportFromDatabaseToCsv()`
- Menambahkan method:
  * `deleteExistingProcessedData()` untuk membersihkan data lama
  * `prepareProcessedClosingBalanceData()` untuk batch insert ke DB
  * `getProcessedRecordCount()` untuk monitoring progres
- Mengoptimalkan memori dengan menghindari akumulasi data dalam array
- Menambahkan DB transaction untuk menjamin konsistensi data selama proses
- Logging diperluas agar progres lebih mudah dipantau
- Menambahkan error handling untuk menangani kegagalan proses dengan aman

Keuntungan:
- Waktu proses menurun drastis dari 1+ jam menjadi beberapa menit
- Skalabilitas meningkat — mampu menangani jutaan record tanpa memory overload
- Data hasil olahan dapat diekspor ulang tanpa harus re-process
- Pola kerja selaras dengan `ExportStatementJob` untuk konsistensi antar modul
- Monitoring dan debugging lebih mudah melalui database dan log

Catatan tambahan:
- Tabel `processed_closing_balances` mendukung kolom `group_name` untuk segmentasi (QRIS/NON_QRIS)
- Menggunakan tipe data numerik dengan presisi untuk nilai keuangan (`amount_lcy`, `balance`, dsb)
2025-07-30 17:15:14 +07:00
Daeng Deni Mardaeni
5f9a82ec20 feat(console): tambah parameter queue_name pada ExportDailyStatements command
Menambahkan parameter queue_name untuk memberikan fleksibilitas dalam pemilihan queue saat menjalankan export daily statements:

- Menambahkan parameter queue_name dengan default value 'default' pada signature command
- Memperbarui description command untuk mencantumkan informasi queue name
- Menambahkan function-level comment pada class dan method handle sesuai standar
- Menambahkan import Log facade untuk logging yang konsisten
- Menambahkan logging di awal proses dengan informasi queue name dan command
- Memperbarui controller call untuk mengirim queue_name sebagai parameter
- Menambahkan output queue name pada info message untuk feedback user
- Menambahkan informasi queue pada job summary untuk transparansi
- Menambahkan logging sukses dengan detail job count dan queue name
- Memperbarui error logging untuk mencakup queue information
- Mempertahankan backward compatibility dengan parameter opsional
- Meningkatkan fleksibilitas dalam manajemen queue untuk proses export
- Memungkinkan penggunaan queue khusus untuk prioritas atau isolasi proses
- Meningkatkan observability dengan logging yang lebih komprehensif
2025-07-30 08:16:53 +07:00
Daeng Deni Mardaeni
33b1255dfb feat(console): tambah parameter queue_name pada ProcessDailyStaging command
Menambahkan parameter queue_name untuk memberikan fleksibilitas dalam pemilihan queue saat menjalankan proses staging:

- Menambahkan parameter queue_name dengan default value 'default' pada signature command
- Memperbarui description command untuk mencantumkan informasi queue name
- Menambahkan function-level comment pada class dan method handle sesuai standar
- Menambahkan queue_name ke semua log entries untuk tracking dan debugging yang lebih baik
- Menambahkan output queue name pada info message untuk feedback user
- Memperbarui controller call untuk mengirim queue_name sebagai parameter
- Menambahkan queue_name ke error logging untuk debugging yang lebih efektif
- Mempertahankan backward compatibility dengan parameter opsional
- Meningkatkan fleksibilitas dalam manajemen queue untuk proses staging
- Memungkinkan penggunaan queue khusus untuk prioritas atau isolasi proses
2025-07-30 08:01:49 +07:00
Daeng Deni Mardaeni
aff6039b33 feat(console): tambah parameter group pada GenerateClosingBalanceReportCommand
Perubahan yang dilakukan:
- Menambahkan parameter `group` sebagai argument wajib pada signature command
- Memperbarui deskripsi command untuk mencantumkan informasi tentang parameter `group`
- Menambahkan validasi `group` agar hanya menerima nilai yang diizinkan: `QRIS` dan `DEFAULT`
- Memperbarui method `validateParameters()` untuk mendukung validasi nilai `group`
- Memperbarui method `createReportLog()` agar menyimpan `group_name` ke database
- Menambahkan `group` ke semua entri log untuk keperluan tracking dan debugging
- Menyesuaikan pemanggilan `GenerateClosingBalanceReportJob` dengan menyertakan parameter `group`
- Menambahkan informasi `group` pada pesan output console untuk feedback pengguna
- Menjamin konsistensi dengan implementasi `GenerateClosingBalanceReportJob` yang telah mendukung parameter `group`
- Meningkatkan fleksibilitas command untuk mendukung multiple jenis transaksi

Manfaat:
- Memungkinkan generate laporan closing balance berdasarkan tipe transaksi
- Logging lebih informatif dan terstruktur berdasarkan kelompok transaksi
- Command lebih fleksibel dan extensible untuk kebutuhan selanjutnya
- Validasi ketat memastikan data yang diproses sesuai spesifikasi sistem

Refs: #closing-balance-refactor
2025-07-29 16:59:29 +07:00
Daeng Deni Mardaeni
51e432c74f refactor(jobs): optimasi query GenerateClosingBalanceReportJob dengan distinct trans_reference dan amount_lcy
Melakukan refactoring pada GenerateClosingBalanceReportJob untuk meningkatkan performa dan akurasi:

- Menghapus eager loading yang kompleks pada relasi 'ft' dan 'dc' untuk menyederhanakan query
- Menambahkan distinct(['trans_reference', 'amount_lcy']) pada level query untuk mencegah duplikasi data langsung dari database
- Menambahkan orderBy untuk trans_reference dan amount_lcy sebelum orderBy existing untuk konsistensi hasil
- Memperbaiki kondisi pada getTableNameByGroup() dari '===' menjadi '!==' untuk logika yang benar
- Menghapus method getSelectFields() yang tidak lagi digunakan setelah simplifikasi query
- Memperbarui log message untuk mencerminkan penggunaan distinct pada kombinasi trans_reference dan amount_lcy
- Meningkatkan efisiensi dengan mengurangi kompleksitas query dan menghindari pemrosesan data duplikat di level aplikasi
- Memastikan akurasi perhitungan running balance dengan mengeliminasi entri duplikat sejak awal

Perubahan ini menggantikan pendekatan sebelumnya yang menggunakan array $processedTransactions untuk skip duplikat di level aplikasi dengan solusi yang lebih efisien di level database.
2025-07-29 16:29:27 +07:00
Daeng Deni Mardaeni
9cdc7f9487 refactor(webstatement): Migrasi ke StagingController dan ubah storage disk ke local 'staging'
Perubahan yang dilakukan:
- Hapus file `MigrasiController.php` yang tidak lagi digunakan
- Ganti referensi controller dari `MigrasiController` menjadi `StagingController` di `ProcessDailyMigration.php`
- Update semua Job class untuk menggunakan disk `staging` menggantikan `sftpStatement`
- Ganti konstanta `DISK_NAME` di class berikut:
  * `ProcessAccountDataJob`
  * `ProcessArrangementDataJob`
  * `ProcessAtmTransactionJob`
  * `ProcessBillDetailDataJob`
  * `ProcessCategoryDataJob`
  * `ProcessCompanyDataJob`
  * `ProcessCustomerDataJob`
  * `ProcessDataCaptureDataJob`
  * `ProcessFtTxnTypeConditionJob`
  * `ProcessFundsTransferDataJob`
  * `ProcessProvinceDataJob`
- Komentari sementara `array_pop()` di `ProcessDataCaptureDataJob` untuk debugging
- Rapikan whitespace dan formatting di `GenerateClosingBalanceReportCommand`
- Sesuaikan konfigurasi storage agar menggunakan local filesystem (`disk: staging`)
- Konsolidasikan penamaan dan penggunaan disk untuk environment `staging`
- Hilangkan ketergantungan terhadap koneksi SFTP dalam proses development/staging

Manfaat:
- Mempercepat proses development dan debugging dengan akses file lokal
- Menyederhanakan konfigurasi untuk staging environment
- Meningkatkan konsistensi dan maintainability kode
- Mengurangi potensi error akibat koneksi eksternal (SFTP)
2025-07-28 16:00:45 +07:00
32 changed files with 1595 additions and 1111 deletions

View File

@@ -1,51 +1,88 @@
<?php
namespace Modules\Webstatement\Console;
namespace Modules\Webstatement\Console;
use Exception;
use Illuminate\Console\Command;
use Modules\Webstatement\Http\Controllers\WebstatementController;
use Exception;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Log;
use Modules\Webstatement\Http\Controllers\WebstatementController;
class ExportDailyStatements extends Command
/**
* Console command untuk export daily statements
* Command ini dapat dijalankan secara manual atau dijadwalkan
*/
class ExportDailyStatements extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'webstatement:export-statements
{--queue_name=default : Queue name untuk menjalankan export jobs (default: default)}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Export daily statements for all configured client accounts dengan queue name yang dapat dikustomisasi';
/**
* Execute the console command.
* Menjalankan proses export daily statements
*
* @return int
*/
public function handle()
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'webstatement:export-statements';
$queueName = $this->option('queue_name');
/**
* The console command description.
*
* @var string
*/
protected $description = 'Export daily statements for all configured client accounts';
// Log start of process
Log::info('Starting daily statement export process', [
'queue_name' => $queueName ?? 'default',
'command' => 'webstatement:export-statements'
]);
/**
* Execute the console command.
*
* @return int
*/
public function handle()
{
$this->info('Starting daily statement export process...');
$this->info('Starting daily statement export process...');
$this->info('Queue Name: ' . ($queueName ?? 'default'));
try {
$controller = app(WebstatementController::class);
$response = $controller->index();
try {
$controller = app(WebstatementController::class);
$responseData = json_decode($response->getContent(), true);
$this->info($responseData['message']);
// Pass queue name to controller if needed
// Jika controller membutuhkan queue name, bisa ditambahkan sebagai parameter
$response = $controller->index($queueName);
// Display summary of jobs queued
$jobCount = count($responseData['jobs'] ?? []);
$this->info("Successfully queued {$jobCount} statement export jobs");
$responseData = json_decode($response->getContent(), true);
$message = $responseData['message'] ?? 'Export process completed';
return Command::SUCCESS;
} catch (Exception $e) {
$this->error('Error exporting statements: ' . $e->getMessage());
return Command::FAILURE;
}
$this->info($message);
// Display summary of jobs queued
$jobCount = count($responseData['jobs'] ?? []);
$this->info("Successfully queued {$jobCount} statement export jobs");
$this->info("Jobs dispatched to queue: {$queueName}");
// Log successful completion
Log::info('Daily statement export process completed successfully', [
'message' => $message,
'job_count' => $jobCount,
'queue_name' => $queueName ?? 'default'
]);
return Command::SUCCESS;
} catch (Exception $e) {
$errorMessage = 'Error exporting statements: ' . $e->getMessage();
$this->error($errorMessage);
// Log error with queue information
Log::error($errorMessage, [
'exception' => $e->getTraceAsString(),
'queue_name' => $queueName ?? 'default'
]);
return Command::FAILURE;
}
}
}

View File

@@ -25,6 +25,7 @@ class GenerateClosingBalanceReportCommand extends Command
protected $signature = 'webstatement:generate-closing-balance-report
{account_number : Nomor rekening untuk generate laporan}
{period : Period laporan format YYYYMMDD, contoh: 20250515}
{group=DEFAULT : Group transaksi QRIS atau DEFAULT}
{--user_id=1 : ID user yang menjalankan command (default: 1)}';
/**
@@ -32,7 +33,7 @@ class GenerateClosingBalanceReportCommand extends Command
*
* @var string
*/
protected $description = 'Generate Closing Balance report untuk nomor rekening dan periode tertentu';
protected $description = 'Generate Closing Balance report untuk nomor rekening, periode, dan group tertentu';
/**
* Execute the console command.
@@ -47,10 +48,11 @@ class GenerateClosingBalanceReportCommand extends Command
// Get parameters
$accountNumber = $this->argument('account_number');
$period = $this->argument('period');
$group = $this->argument('group');
$userId = $this->option('user_id');
// Validate parameters
if (!$this->validateParameters($accountNumber, $period, $userId)) {
if (!$this->validateParameters($accountNumber, $period, $group, $userId)) {
return Command::FAILURE;
}
@@ -61,12 +63,13 @@ class GenerateClosingBalanceReportCommand extends Command
Log::info('Console command: Starting closing balance report generation', [
'account_number' => $accountNumber,
'period' => $period,
'group' => $group,
'user_id' => $userId,
'command' => 'webstatement:generate-closing-balance-report'
]);
// Create report log entry
$reportLog = $this->createReportLog($accountNumber, $period, $userId);
$reportLog = $this->createReportLog($accountNumber, $period, $group, $userId);
if (!$reportLog) {
$this->error('Failed to create report log entry');
@@ -74,14 +77,15 @@ class GenerateClosingBalanceReportCommand extends Command
return Command::FAILURE;
}
// Dispatch the job
GenerateClosingBalanceReportJob::dispatch($accountNumber, $period, $reportLog->id);
// Dispatch the job with group parameter
GenerateClosingBalanceReportJob::dispatch($accountNumber, $period, $reportLog->id, $group);
DB::commit();
$this->info("Closing Balance report generation job queued successfully!");
$this->info("Account Number: {$accountNumber}");
$this->info("Period: {$period}");
$this->info("Group: {$group}");
$this->info("Report Log ID: {$reportLog->id}");
$this->info('The report will be generated in the background.');
$this->info('Check the closing_balance_report_logs table for progress.');
@@ -90,6 +94,7 @@ class GenerateClosingBalanceReportCommand extends Command
Log::info('Console command: Closing balance report job dispatched successfully', [
'account_number' => $accountNumber,
'period' => $period,
'group' => $group,
'report_log_id' => $reportLog->id,
'user_id' => $userId
]);
@@ -105,6 +110,7 @@ class GenerateClosingBalanceReportCommand extends Command
Log::error('Console command: Error generating closing balance report', [
'account_number' => $accountNumber,
'period' => $period,
'group' => $group,
'user_id' => $userId,
'error' => $e->getMessage(),
'trace' => $e->getTraceAsString()
@@ -120,10 +126,11 @@ class GenerateClosingBalanceReportCommand extends Command
*
* @param string $accountNumber
* @param string $period
* @param string $group
* @param int $userId
* @return bool
*/
private function validateParameters(string $accountNumber, string $period, int $userId): bool
private function validateParameters(string $accountNumber, string $period, string $group, int $userId): bool
{
// Validate account number
if (empty($accountNumber)) {
@@ -147,6 +154,13 @@ class GenerateClosingBalanceReportCommand extends Command
return false;
}
// Validate group parameter
$allowedGroups = ['QRIS', 'DEFAULT'];
if (!in_array(strtoupper($group), $allowedGroups)) {
$this->error('Invalid group parameter. Allowed values: ' . implode(', ', $allowedGroups));
return false;
}
// Validate user exists
$user = User::find($userId);
if (!$user) {
@@ -163,10 +177,11 @@ class GenerateClosingBalanceReportCommand extends Command
*
* @param string $accountNumber
* @param string $period
* @param string $group
* @param int $userId
* @return ClosingBalanceReportLog|null
*/
private function createReportLog(string $accountNumber, string $period, int $userId): ?ClosingBalanceReportLog
private function createReportLog(string $accountNumber, string $period, string $group, int $userId): ?ClosingBalanceReportLog
{
try {
// Convert period string to Carbon date
@@ -176,6 +191,7 @@ class GenerateClosingBalanceReportCommand extends Command
'account_number' => $accountNumber,
'period' => $period,
'report_date' => $reportDate, // Required field yang sebelumnya missing
'group_name' => strtoupper($group), // Tambahkan group_name ke log
'status' => 'pending',
'user_id' => $userId,
'created_by' => $userId, // Required field yang sebelumnya missing
@@ -190,6 +206,7 @@ class GenerateClosingBalanceReportCommand extends Command
'report_log_id' => $reportLog->id,
'account_number' => $accountNumber,
'period' => $period,
'group' => $group,
'report_date' => $reportDate->format('Y-m-d'),
'user_id' => $userId
]);
@@ -200,6 +217,7 @@ class GenerateClosingBalanceReportCommand extends Command
Log::error('Console command: Error creating report log', [
'account_number' => $accountNumber,
'period' => $period,
'group' => $group,
'user_id' => $userId,
'error' => $e->getMessage(),
'trace' => $e->getTraceAsString()

View File

@@ -1,67 +0,0 @@
<?php
namespace Modules\Webstatement\Console;
use Exception;
use Illuminate\Console\Command;
use Modules\Webstatement\Http\Controllers\MigrasiController;
use Illuminate\Support\Facades\Log;
class ProcessDailyMigration extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'webstatement:process-daily-migration
{--process_parameter= : To process migration parameter true/false}
{--period= : Period to process (default: -1 day, format: Ymd or relative date)}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Process data migration for the specified period (default: previous day)';
/**
* Execute the console command.
*
* @return int
*/
public function handle()
{
$processParameter = $this->option('process_parameter');
$period = $this->option('period');
// Log start of process
Log::info('Starting daily data migration process', [
'process_parameter' => $processParameter ?? 'false',
'period' => $period ?? '-1 day'
]);
$this->info('Starting daily data migration process...');
$this->info('Process Parameter: ' . ($processParameter ?? 'False'));
$this->info('Period: ' . ($period ?? '-1 day (default)'));
try {
$controller = app(MigrasiController::class);
$response = $controller->index($processParameter, $period);
$responseData = json_decode($response->getContent(), true);
$message = $responseData['message'] ?? 'Process completed';
$this->info($message);
Log::info('Daily migration process completed successfully', ['message' => $message]);
return Command::SUCCESS;
} catch (Exception $e) {
$errorMessage = 'Error processing daily migration: ' . $e->getMessage();
$this->error($errorMessage);
Log::error($errorMessage, ['exception' => $e->getTraceAsString()]);
return Command::FAILURE;
}
}
}

View File

@@ -0,0 +1,85 @@
<?php
namespace Modules\Webstatement\Console;
use Exception;
use Illuminate\Console\Command;
use Modules\Webstatement\Http\Controllers\StagingController;
use Illuminate\Support\Facades\Log;
/**
* Console command untuk memproses data staging harian
* Command ini dapat dijalankan secara manual atau dijadwalkan
*/
class ProcessDailyStaging extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'webstatement:process-daily-staging
{--process_parameter= : To process staging parameter true/false}
{--period= : Period to process (default: -1 day, format: Ymd or relative date)}
{--queue_name=default : Queue name untuk menjalankan job (default: default)}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Process data staging for the specified period (default: previous day) dengan queue name yang dapat dikustomisasi';
/**
* Execute the console command.
* Menjalankan proses staging data harian
*
* @return int
*/
public function handle()
{
$processParameter = $this->option('process_parameter');
$period = $this->option('period');
$queueName = $this->option('queue_name');
// Log start of process
Log::info('Starting daily data staging process', [
'process_parameter' => $processParameter ?? 'false',
'period' => $period ?? '-1 day',
'queue_name' => $queueName ?? 'default'
]);
$this->info('Starting daily data staging process...');
$this->info('Process Parameter: ' . ($processParameter ?? 'False'));
$this->info('Period: ' . ($period ?? '-1 day (default)'));
$this->info('Queue Name: ' . ($queueName ?? 'default'));
try {
$controller = app(StagingController::class);
// Pass queue name to controller if needed
// Jika controller membutuhkan queue name, bisa ditambahkan sebagai parameter
$response = $controller->index($processParameter, $period, $queueName);
$responseData = json_decode($response->getContent(), true);
$message = $responseData['message'] ?? 'Process completed';
$this->info($message);
Log::info('Daily staging process completed successfully', [
'message' => $message,
'queue_name' => $queueName ?? 'default'
]);
return Command::SUCCESS;
} catch (Exception $e) {
$errorMessage = 'Error processing daily staging: ' . $e->getMessage();
$this->error($errorMessage);
Log::error($errorMessage, [
'exception' => $e->getTraceAsString(),
'queue_name' => $queueName ?? 'default'
]);
return Command::FAILURE;
}
}
}

View File

@@ -1,199 +0,0 @@
<?php
namespace Modules\Webstatement\Http\Controllers;
use App\Http\Controllers\Controller;
use BadMethodCallException;
use Exception;
use Illuminate\Http\JsonResponse;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Facades\Log;
use Modules\Webstatement\Jobs\{ProcessAccountDataJob,
ProcessArrangementDataJob,
ProcessAtmTransactionJob,
ProcessBillDetailDataJob,
ProcessCategoryDataJob,
ProcessCompanyDataJob,
ProcessCustomerDataJob,
ProcessDataCaptureDataJob,
ProcessFtTxnTypeConditionJob,
ProcessFundsTransferDataJob,
ProcessStmtEntryDataJob,
ProcessStmtNarrFormatDataJob,
ProcessStmtNarrParamDataJob,
ProcessTellerDataJob,
ProcessTransactionDataJob,
ProcessSectorDataJob,
ProcessProvinceDataJob,
ProcessStmtEntryDetailDataJob};
class MigrasiController extends Controller
{
private const PROCESS_TYPES = [
'transaction' => ProcessTransactionDataJob::class,
'stmtNarrParam' => ProcessStmtNarrParamDataJob::class,
'stmtNarrFormat' => ProcessStmtNarrFormatDataJob::class,
'ftTxnTypeCondition' => ProcessFtTxnTypeConditionJob::class,
'category' => ProcessCategoryDataJob::class,
'company' => ProcessCompanyDataJob::class,
'customer' => ProcessCustomerDataJob::class,
'account' => ProcessAccountDataJob::class,
'stmtEntry' => ProcessStmtEntryDataJob::class,
'stmtEntryDetail' => ProcessStmtEntryDetailDataJob::class, // Tambahan baru
'dataCapture' => ProcessDataCaptureDataJob::class,
'fundsTransfer' => ProcessFundsTransferDataJob::class,
'teller' => ProcessTellerDataJob::class,
'atmTransaction' => ProcessAtmTransactionJob::class,
'arrangement' => ProcessArrangementDataJob::class,
'billDetail' => ProcessBillDetailDataJob::class,
'sector' => ProcessSectorDataJob::class,
'province' => ProcessProvinceDataJob::class
];
private const PARAMETER_PROCESSES = [
'transaction',
'stmtNarrParam',
'stmtNarrFormat',
'ftTxnTypeCondition',
'sector',
'province'
];
private const DATA_PROCESSES = [
'category',
'company',
'customer',
'account',
'stmtEntry',
'stmtEntryDetail', // Tambahan baru
'dataCapture',
'fundsTransfer',
'teller',
'atmTransaction',
'arrangement',
'billDetail'
];
public function __call($method, $parameters)
{
if (strpos($method, 'process') === 0) {
$type = lcfirst(substr($method, 7));
if (isset(self::PROCESS_TYPES[$type])) {
return $this->processData($type, $parameters[0] ?? '');
}
}
throw new BadMethodCallException("Method {$method} does not exist.");
}
private function processData(string $type, string $period)
: JsonResponse
{
try {
$jobClass = self::PROCESS_TYPES[$type];
$jobClass::dispatch($period);
$message = sprintf('%s data processing job has been queued successfully', ucfirst($type));
Log::info($message);
return response()->json(['message' => $message]);
} catch (Exception $e) {
Log::error(sprintf('Error in %s processing: %s', $type, $e->getMessage()));
return response()->json(['error' => $e->getMessage()], 500);
}
}
/**
* Proses migrasi data dengan parameter dan periode yang dapat dikustomisasi
*
* @param bool|string $processParameter Flag untuk memproses parameter
* @param string|null $period Periode yang akan diproses (default: -1 day)
* @return JsonResponse
*/
public function index($processParameter = false, $period = null)
{
try {
Log::info('Starting migration process', [
'process_parameter' => $processParameter,
'period' => $period
]);
$disk = Storage::disk('sftpStatement');
if ($processParameter) {
Log::info('Processing parameter data');
foreach (self::PARAMETER_PROCESSES as $process) {
$this->processData($process, '_parameter');
}
Log::info('Parameter processes completed successfully');
return response()->json(['message' => 'Parameter processes completed successfully']);
}
// Tentukan periode yang akan diproses
$targetPeriod = $this->determinePeriod($period);
Log::info('Processing data for period', ['period' => $targetPeriod]);
if (!$disk->exists($targetPeriod)) {
$errorMessage = "Period {$targetPeriod} folder not found in SFTP storage";
Log::warning($errorMessage);
return response()->json([
"message" => $errorMessage
], 404);
}
foreach (self::DATA_PROCESSES as $process) {
$this->processData($process, $targetPeriod);
}
$successMessage = "Data processing for period {$targetPeriod} has been queued successfully";
Log::info($successMessage);
return response()->json([
'message' => $successMessage
]);
} catch (Exception $e) {
Log::error('Error in migration index method: ' . $e->getMessage());
return response()->json(['error' => $e->getMessage()], 500);
}
}
/**
* Tentukan periode berdasarkan input atau gunakan default
*
* @param string|null $period Input periode
* @return string Periode dalam format Ymd
*/
private function determinePeriod($period = null): string
{
if ($period === null) {
// Default: -1 day
$calculatedPeriod = date('Ymd', strtotime('-1 day'));
Log::info('Using default period', ['period' => $calculatedPeriod]);
return $calculatedPeriod;
}
// Jika periode sudah dalam format Ymd (8 digit)
if (preg_match('/^\d{8}$/', $period)) {
Log::info('Using provided period in Ymd format', ['period' => $period]);
return $period;
}
// Jika periode dalam format relative date (contoh: -2 days, -1 week, etc.)
try {
$calculatedPeriod = date('Ymd', strtotime($period));
Log::info('Calculated period from relative date', [
'input' => $period,
'calculated' => $calculatedPeriod
]);
return $calculatedPeriod;
} catch (Exception $e) {
Log::warning('Invalid period format, using default', [
'input' => $period,
'error' => $e->getMessage()
]);
return date('Ymd', strtotime('-1 day'));
}
}
}

View File

@@ -0,0 +1,240 @@
<?php
namespace Modules\Webstatement\Http\Controllers;
use App\Http\Controllers\Controller;
use BadMethodCallException;
use Exception;
use Illuminate\Http\JsonResponse;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Facades\Log;
use Modules\Webstatement\Jobs\{ProcessAccountDataJob,
ProcessArrangementDataJob,
ProcessAtmTransactionJob,
ProcessBillDetailDataJob,
ProcessCategoryDataJob,
ProcessCompanyDataJob,
ProcessCustomerDataJob,
ProcessDataCaptureDataJob,
ProcessFtTxnTypeConditionJob,
ProcessFundsTransferDataJob,
ProcessStmtEntryDataJob,
ProcessStmtNarrFormatDataJob,
ProcessStmtNarrParamDataJob,
ProcessTellerDataJob,
ProcessTransactionDataJob,
ProcessSectorDataJob,
ProcessProvinceDataJob,
ProcessStmtEntryDetailDataJob};
class StagingController extends Controller
{
private const PROCESS_TYPES = [
'transaction' => ProcessTransactionDataJob::class,
'stmtNarrParam' => ProcessStmtNarrParamDataJob::class,
'stmtNarrFormat' => ProcessStmtNarrFormatDataJob::class,
'ftTxnTypeCondition' => ProcessFtTxnTypeConditionJob::class,
'category' => ProcessCategoryDataJob::class,
'company' => ProcessCompanyDataJob::class,
'customer' => ProcessCustomerDataJob::class,
'account' => ProcessAccountDataJob::class,
'stmtEntry' => ProcessStmtEntryDataJob::class,
'stmtEntryDetail' => ProcessStmtEntryDetailDataJob::class,
'dataCapture' => ProcessDataCaptureDataJob::class,
'fundsTransfer' => ProcessFundsTransferDataJob::class,
'teller' => ProcessTellerDataJob::class,
'atmTransaction' => ProcessAtmTransactionJob::class,
'arrangement' => ProcessArrangementDataJob::class,
'billDetail' => ProcessBillDetailDataJob::class,
'sector' => ProcessSectorDataJob::class,
'province' => ProcessProvinceDataJob::class
];
private const PARAMETER_PROCESSES = [
'transaction',
'stmtNarrParam',
'stmtNarrFormat',
'ftTxnTypeCondition',
'sector',
'province'
];
private const DATA_PROCESSES = [
'category',
'company',
'customer',
'account',
'stmtEntry',
'stmtEntryDetail',
'dataCapture',
'fundsTransfer',
'teller',
'atmTransaction',
'arrangement',
'billDetail'
];
public function __call($method, $parameters)
{
if (strpos($method, 'process') === 0) {
$type = lcfirst(substr($method, 7));
if (isset(self::PROCESS_TYPES[$type])) {
return $this->processData($type, $parameters[0] ?? '', $parameters[1] ?? 'default');
}
}
throw new BadMethodCallException("Method {$method} does not exist.");
}
/**
* Memproses data dengan queue name yang dapat dikustomisasi
*
* @param string $type Tipe proses yang akan dijalankan
* @param string $period Periode data yang akan diproses
* @param string $queueName Nama queue untuk menjalankan job
* @return JsonResponse
*/
private function processData(string $type, string $period, string $queueName = 'default'): JsonResponse
{
try {
$jobClass = self::PROCESS_TYPES[$type];
// Dispatch job dengan queue name yang spesifik
$jobClass::dispatch($period)->onQueue($queueName);
$message = sprintf('%s data processing job has been queued successfully on queue: %s', ucfirst($type), $queueName);
Log::info($message, [
'type' => $type,
'period' => $period,
'queue_name' => $queueName
]);
return response()->json([
'message' => $message,
'queue_name' => $queueName
]);
} catch (Exception $e) {
Log::error(sprintf('Error in %s processing: %s', $type, $e->getMessage()), [
'type' => $type,
'period' => $period,
'queue_name' => $queueName,
'error' => $e->getMessage()
]);
return response()->json(['error' => $e->getMessage()], 500);
}
}
/**
* Proses migrasi data dengan parameter, periode, dan queue name yang dapat dikustomisasi
*
* @param bool|string $processParameter Flag untuk memproses parameter
* @param string|null $period Periode yang akan diproses (default: -1 day)
* @param string $queueName Nama queue untuk menjalankan job (default: default)
* @return JsonResponse
*/
public function index($processParameter = false, $period = null, $queueName = 'default')
{
try {
Log::info('Starting migration process', [
'process_parameter' => $processParameter,
'period' => $period,
'queue_name' => $queueName
]);
$disk = Storage::disk('staging');
if ($processParameter) {
Log::info('Processing parameter data', ['queue_name' => $queueName]);
foreach (self::PARAMETER_PROCESSES as $process) {
$this->processData($process, '_parameter', $queueName);
}
Log::info('Parameter processes completed successfully', ['queue_name' => $queueName]);
return response()->json([
'message' => 'Parameter processes completed successfully',
'queue_name' => $queueName
]);
}
// Tentukan periode yang akan diproses
$targetPeriod = $this->determinePeriod($period);
Log::info('Processing data for period', [
'period' => $targetPeriod,
'queue_name' => $queueName
]);
if (!$disk->exists($targetPeriod)) {
$errorMessage = "Period {$targetPeriod} folder not found in SFTP storage";
Log::warning($errorMessage, ['queue_name' => $queueName]);
return response()->json([
"message" => $errorMessage,
'queue_name' => $queueName
], 404);
}
foreach (self::DATA_PROCESSES as $process) {
$this->processData($process, $targetPeriod, $queueName);
}
$successMessage = "Data processing for period {$targetPeriod} has been queued successfully on queue: {$queueName}";
Log::info($successMessage, [
'period' => $targetPeriod,
'queue_name' => $queueName
]);
return response()->json([
'message' => $successMessage,
'queue_name' => $queueName
]);
} catch (Exception $e) {
Log::error('Error in migration index method: ' . $e->getMessage(), [
'queue_name' => $queueName,
'error' => $e->getMessage()
]);
return response()->json([
'error' => $e->getMessage(),
'queue_name' => $queueName
], 500);
}
}
/**
* Tentukan periode berdasarkan input atau gunakan default
*
* @param string|null $period Input periode
* @return string Periode dalam format Ymd
*/
private function determinePeriod($period = null): string
{
if ($period === null) {
// Default: -1 day
$calculatedPeriod = date('Ymd', strtotime('-1 day'));
Log::info('Using default period', ['period' => $calculatedPeriod]);
return $calculatedPeriod;
}
// Jika periode sudah dalam format Ymd (8 digit)
if (preg_match('/^\d{8}$/', $period)) {
Log::info('Using provided period in Ymd format', ['period' => $period]);
return $period;
}
// Jika periode dalam format relative date (contoh: -2 days, -1 week, etc.)
try {
$calculatedPeriod = date('Ymd', strtotime($period));
Log::info('Calculated period from relative date', [
'input' => $period,
'calculated' => $calculatedPeriod
]);
return $calculatedPeriod;
} catch (Exception $e) {
Log::warning('Invalid period format, using default', [
'input' => $period,
'error' => $e->getMessage()
]);
return date('Ymd', strtotime('-1 day'));
}
}
}

View File

@@ -1,204 +1,246 @@
<?php
namespace Modules\Webstatement\Http\Controllers;
namespace Modules\Webstatement\Http\Controllers;
use App\Http\Controllers\Controller;
use Carbon\Carbon;
use Illuminate\Contracts\Bus\Dispatcher;
use Modules\Webstatement\Jobs\ExportStatementJob;
use Modules\Webstatement\Models\AccountBalance;
use Modules\Webstatement\Jobs\ExportStatementPeriodJob;
use App\Http\Controllers\Controller;
use Carbon\Carbon;
use Illuminate\Contracts\Bus\Dispatcher;
use Illuminate\Http\Request;
use Modules\Webstatement\Jobs\ExportStatementJob;
use Modules\Webstatement\Models\AccountBalance;
use Modules\Webstatement\Jobs\ExportStatementPeriodJob;
use Illuminate\Support\Facades\Log;
class WebstatementController extends Controller
class WebstatementController extends Controller
{
/**
* Display a listing of the resource.
* Menjalankan export statement untuk semua akun dengan queue name yang dapat dikustomisasi
*
* @param Request $request
* @return \Illuminate\Http\JsonResponse
*/
public function index(Request $request)
{
/**
* Display a listing of the resource.
*/
public function index()
{
$jobIds = [];
$data = [];
$queueName = $request->get('queue_name', 'default');
foreach ($this->listAccount() as $clientName => $accounts) {
foreach ($accounts as $accountNumber) {
foreach ($this->listPeriod() as $period) {
$job = new ExportStatementJob(
$accountNumber,
$period,
$this->getAccountBalance($accountNumber, $period),
$clientName // Pass the client name to the job
);
$jobIds[] = app(Dispatcher::class)->dispatch($job);
$data[] = [
'client_name' => $clientName,
'account_number' => $accountNumber,
'period' => $period
];
}
Log::info('Starting statement export process', [
'queue_name' => $queueName
]);
$jobIds = [];
$data = [];
foreach ($this->listAccount() as $clientName => $accounts) {
foreach ($accounts as $accountNumber) {
foreach ($this->listPeriod() as $period) {
$job = new ExportStatementJob(
$accountNumber,
$period,
$this->getAccountBalance($accountNumber, $period),
$clientName // Pass the client name to the job
);
// Dispatch job dengan queue name yang spesifik
$jobIds[] = app(Dispatcher::class)->dispatch($job->onQueue($queueName));
$data[] = [
'client_name' => $clientName,
'account_number' => $accountNumber,
'period' => $period,
'queue_name' => $queueName
];
}
}
}
Log::info('Statement export jobs queued successfully', [
'total_jobs' => count($jobIds),
'queue_name' => $queueName
]);
return response()->json([
'message' => 'Statement export jobs have been queued',
'queue_name' => $queueName,
'jobs' => array_map(function ($index, $jobId) use ($data) {
return [
'job_id' => $jobId,
'client_name' => $data[$index]['client_name'],
'account_number' => $data[$index]['account_number'],
'period' => $data[$index]['period'],
'queue_name' => $data[$index]['queue_name'],
'file_name' => "{$data[$index]['client_name']}_{$data[$index]['account_number']}_{$data[$index]['period']}.csv"
];
}, array_keys($jobIds), $jobIds)
]);
}
function listAccount(){
return [
'PLUANG' => [
'1080426085',
'1080425781',
],
'OY' => [
'1081647484',
'1081647485',
],
'INDORAYA' => [
'1083123710',
'1083123711',
'1083123712',
'1083123713',
'1083123714',
'1083123715',
'1083123716',
'1083123718',
'1083123719',
'1083123721',
'1083123722',
'1083123723',
'1083123724',
'1083123726',
'1083123727',
'1083123728',
'1083123730',
'1083123731',
'1083123732',
'1083123734',
'1083123735',
],
'TDC' => [
'1086677889',
'1086677890',
'1086677891',
'1086677892',
'1086677893',
'1086677894',
'1086677895',
'1086677896',
'1086677897',
],
'ASIA_PARKING' => [
'1080119298',
'1080119361',
'1080119425',
'1080119387',
'1082208069',
],
'DAU' => [
'1085151668',
],
'EGR' => [
'1085368601',
],
'SARANA_PACTINDO' => [
'1078333878',
],
'SWADAYA_PANDU' => [
'0081272689',
],
"AWAN_LINTANG_SOLUSI"=> [
"1084269430"
],
"MONETA"=> [
"1085667890"
]
];
}
function listPeriod(){
return [
date('Ymd', strtotime('-1 day'))
];
}
function getAccountBalance($accountNumber, $period)
{
$accountBalance = AccountBalance::where('account_number', $accountNumber)
->where('period', '<', $period)
->orderBy('period', 'desc')
->first();
return $accountBalance->actual_balance ?? 0;
}
/**
* Print statement rekening dengan queue name yang dapat dikustomisasi
*
* @param string $accountNumber
* @param string|null $period
* @param Request $request
* @return \Illuminate\Http\JsonResponse
*/
function printStatementRekening($accountNumber, $period = null, Request $request = null) {
$queueName = $request ? $request->get('queue_name', 'default') : 'default';
$period = $period ?? date('Ym');
$balance = AccountBalance::where('account_number', $accountNumber)
->when($period === '202505', function($query) {
return $query->where('period', '>=', '20250512')
->orderBy('period', 'asc');
}, function($query) use ($period) {
// Get balance from last day of previous month
$firstDayOfMonth = Carbon::createFromFormat('Ym', $period)->startOfMonth();
$lastDayPrevMonth = $firstDayOfMonth->copy()->subDay()->format('Ymd');
return $query->where('period', $lastDayPrevMonth);
})
->first()
->actual_balance ?? '0.00';
$clientName = 'client1';
try {
Log::info("Starting statement export for account: {$accountNumber}, period: {$period}, client: {$clientName}", [
'account_number' => $accountNumber,
'period' => $period,
'client_name' => $clientName,
'queue_name' => $queueName
]);
// Validate inputs
if (empty($accountNumber) || empty($period) || empty($clientName)) {
throw new \Exception('Required parameters missing');
}
// Dispatch the job dengan queue name yang spesifik
$job = ExportStatementPeriodJob::dispatch($accountNumber, $period, $balance, $clientName)
->onQueue($queueName);
Log::info("Statement export job dispatched successfully", [
'job_id' => $job->job_id ?? null,
'account' => $accountNumber,
'period' => $period,
'client' => $clientName,
'queue_name' => $queueName
]);
return response()->json([
'message' => 'Statement export jobs have been queued',
'jobs' => array_map(function ($index, $jobId) use ($data) {
return [
'job_id' => $jobId,
'client_name' => $data[$index]['client_name'],
'account_number' => $data[$index]['account_number'],
'period' => $data[$index]['period'],
'file_name' => "{$data[$index]['client_name']}_{$data[$index]['account_number']}_{$data[$index]['period']}.csv"
];
}, array_keys($jobIds), $jobIds)
'success' => true,
'message' => 'Statement export job queued successfully',
'data' => [
'job_id' => $job->job_id ?? null,
'account_number' => $accountNumber,
'period' => $period,
'client_name' => $clientName,
'queue_name' => $queueName
]
]);
} catch (\Exception $e) {
Log::error("Failed to export statement", [
'error' => $e->getMessage(),
'account' => $accountNumber,
'period' => $period,
'queue_name' => $queueName
]);
return response()->json([
'success' => false,
'message' => 'Failed to queue statement export job',
'error' => $e->getMessage(),
'queue_name' => $queueName
]);
}
function listAccount(){
return [
'PLUANG' => [
'1080426085',
'1080425781',
],
'OY' => [
'1081647484',
'1081647485',
],
'INDORAYA' => [
'1083123710',
'1083123711',
'1083123712',
'1083123713',
'1083123714',
'1083123715',
'1083123716',
'1083123718',
'1083123719',
'1083123721',
'1083123722',
'1083123723',
'1083123724',
'1083123726',
'1083123727',
'1083123728',
'1083123730',
'1083123731',
'1083123732',
'1083123734',
'1083123735',
],
'TDC' => [
'1086677889',
'1086677890',
'1086677891',
'1086677892',
'1086677893',
'1086677894',
'1086677895',
'1086677896',
'1086677897',
],
'ASIA_PARKING' => [
'1080119298',
'1080119361',
'1080119425',
'1080119387',
'1082208069',
],
'DAU' => [
'1085151668',
],
'EGR' => [
'1085368601',
],
'SARANA_PACTINDO' => [
'1078333878',
],
'SWADAYA_PANDU' => [
'0081272689',
],
"AWAN_LINTANG_SOLUSI"=> [
"1084269430"
],
"MONETA"=> [
"1085667890"
]
];
}
function listPeriod(){
return [
date('Ymd', strtotime('-1 day'))
];
}
function getAccountBalance($accountNumber, $period)
{
$accountBalance = AccountBalance::where('account_number', $accountNumber)
->where('period', '<', $period)
->orderBy('period', 'desc')
->first();
return $accountBalance->actual_balance ?? 0;
}
function printStatementRekening($accountNumber, $period = null) {
$period = $period ?? date('Ym');
$balance = AccountBalance::where('account_number', $accountNumber)
->when($period === '202505', function($query) {
return $query->where('period', '>=', '20250512')
->orderBy('period', 'asc');
}, function($query) use ($period) {
// Get balance from last day of previous month
$firstDayOfMonth = Carbon::createFromFormat('Ym', $period)->startOfMonth();
$lastDayPrevMonth = $firstDayOfMonth->copy()->subDay()->format('Ymd');
return $query->where('period', $lastDayPrevMonth);
})
->first()
->actual_balance ?? '0.00';
$clientName = 'client1';
try {
\Log::info("Starting statement export for account: {$accountNumber}, period: {$period}, client: {$clientName}");
// Validate inputs
if (empty($accountNumber) || empty($period) || empty($clientName)) {
throw new \Exception('Required parameters missing');
}
// Dispatch the job
$job = ExportStatementPeriodJob::dispatch($accountNumber, $period, $balance, $clientName);
\Log::info("Statement export job dispatched successfully", [
'job_id' => $job->job_id ?? null,
'account' => $accountNumber,
'period' => $period,
'client' => $clientName
]);
return response()->json([
'success' => true,
'message' => 'Statement export job queued successfully',
'data' => [
'job_id' => $job->job_id ?? null,
'account_number' => $accountNumber,
'period' => $period,
'client_name' => $clientName
]
]);
} catch (\Exception $e) {
\Log::error("Failed to export statement", [
'error' => $e->getMessage(),
'account' => $accountNumber,
'period' => $period
]);
return response()->json([
'success' => false,
'message' => 'Failed to queue statement export job',
'error' => $e->getMessage()
]);
}
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -20,7 +20,7 @@ class ProcessAccountDataJob implements ShouldQueue
private const CSV_DELIMITER = '~';
private const MAX_EXECUTION_TIME = 86400; // 24 hours in seconds
private const FILENAME = 'ST.ACCOUNT.csv';
private const DISK_NAME = 'sftpStatement';
private const DISK_NAME = 'staging';
private const CHUNK_SIZE = 1000; // Process data in chunks to reduce memory usage
private string $period = '';

View File

@@ -19,7 +19,7 @@
private const CSV_DELIMITER = '~';
private const MAX_EXECUTION_TIME = 86400; // 24 hours in seconds
private const FILENAME = 'ST.AA.ARRANGEMENT.csv';
private const DISK_NAME = 'sftpStatement';
private const DISK_NAME = 'staging';
private const CHUNK_SIZE = 1000; // Process data in chunks to reduce memory usage
private string $period = '';

View File

@@ -19,7 +19,7 @@
private const CSV_DELIMITER = '~';
private const MAX_EXECUTION_TIME = 86400; // 24 hours in seconds
private const FILENAME = 'ST.ATM.TRANSACTION.csv';
private const DISK_NAME = 'sftpStatement';
private const DISK_NAME = 'staging';
private const CHUNK_SIZE = 1000; // Process data in chunks to reduce memory usage
private const HEADER_MAP = [
'id' => 'transaction_id',

View File

@@ -19,7 +19,7 @@
private const CSV_DELIMITER = '~';
private const MAX_EXECUTION_TIME = 86400; // 24 hours in seconds
private const FILENAME = 'ST.AA.BILL.DETAILS.csv';
private const DISK_NAME = 'sftpStatement';
private const DISK_NAME = 'staging';
private const CHUNK_SIZE = 1000; // Process data in chunks to reduce memory usage
private string $period = '';

View File

@@ -19,7 +19,7 @@
private const CSV_DELIMITER = '~';
private const MAX_EXECUTION_TIME = 86400; // 24 hours in seconds
private const FILENAME = 'ST.CATEGORY.csv';
private const DISK_NAME = 'sftpStatement';
private const DISK_NAME = 'staging';
private const HEADER_MAP = [
'id' => 'id_category',
'date_time' => 'date_time',

View File

@@ -19,7 +19,7 @@
private const CSV_DELIMITER = '~';
private const MAX_EXECUTION_TIME = 86400; // 24 hours in seconds
private const FILENAME = 'ST.COMPANY.csv';
private const DISK_NAME = 'sftpStatement';
private const DISK_NAME = 'staging';
private const FIELD_MAP = [
'id' => null, // Not mapped to model
'date_time' => null, // Not mapped to model

View File

@@ -8,6 +8,7 @@
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\Storage;
use Modules\Webstatement\Models\Customer;
@@ -19,7 +20,7 @@
private const CSV_DELIMITER = '~';
private const MAX_EXECUTION_TIME = 86400; // 24 hours in seconds
private const FILENAME = 'ST.CUSTOMER.csv';
private const DISK_NAME = 'sftpStatement';
private const DISK_NAME = 'staging';
private const CHUNK_SIZE = 1000; // Process data in chunks to reduce memory usage
private string $period = '';
@@ -112,13 +113,28 @@
return;
}
$headers = (new Customer())->getFillable();
// Read header from CSV file
$csvHeaders = fgetcsv($handle, 0, self::CSV_DELIMITER);
if ($csvHeaders === false) {
Log::error("Unable to read headers from file: $filePath");
fclose($handle);
return;
}
// Map CSV headers to database fields
$headerMapping = $this->getHeaderMapping($csvHeaders);
Log::info("CSV Headers found", [
'csv_headers' => $csvHeaders,
'mapped_fields' => array_values($headerMapping)
]);
$rowCount = 0;
$chunkCount = 0;
while (($row = fgetcsv($handle, 0, self::CSV_DELIMITER)) !== false) {
$rowCount++;
$this->processRow($row, $headers, $rowCount, $filePath);
$this->processRow($row, $csvHeaders, $headerMapping, $rowCount, $filePath);
// Process in chunks to avoid memory issues
if (count($this->customerBatch) >= self::CHUNK_SIZE) {
@@ -137,16 +153,61 @@
Log::info("Completed processing $filePath. Processed {$this->processedCount} records with {$this->errorCount} errors.");
}
private function processRow(array $row, array $headers, int $rowCount, string $filePath)
/**
* Map CSV headers to database field names
* Memetakan header CSV ke nama field database
*/
private function getHeaderMapping(array $csvHeaders): array
{
$mapping = [];
$fillableFields = (new Customer())->getFillable();
foreach ($csvHeaders as $index => $csvHeader) {
$csvHeader = trim($csvHeader);
// Direct mapping untuk field yang sama
if (in_array($csvHeader, $fillableFields)) {
$mapping[$index] = $csvHeader;
continue;
}
// Custom mapping untuk field yang berbeda nama
$customMapping = [
'co_code' => 'branch_code', // co_code di CSV menjadi branch_code di database
];
if (isset($customMapping[$csvHeader])) {
$mapping[$index] = $customMapping[$csvHeader];
} else {
// Jika field ada di fillable, gunakan langsung
if (in_array($csvHeader, $fillableFields)) {
$mapping[$index] = $csvHeader;
}
// Jika tidak ada mapping, skip field ini
}
}
return $mapping;
}
private function processRow(array $row, array $csvHeaders, array $headerMapping, int $rowCount, string $filePath)
: void
{
if (count($headers) !== count($row)) {
if (count($csvHeaders) !== count($row)) {
Log::warning("Row $rowCount in $filePath has incorrect column count. Expected: " .
count($headers) . ", Got: " . count($row));
count($csvHeaders) . ", Got: " . count($row));
return;
}
$data = array_combine($headers, $row);
// Map CSV data to database fields
$data = [];
foreach ($row as $index => $value) {
if (isset($headerMapping[$index])) {
$fieldName = $headerMapping[$index];
$data[$fieldName] = trim($value);
}
}
$this->addToBatch($data, $rowCount, $filePath);
}
@@ -175,12 +236,20 @@
/**
* Save batched records to the database
* Menyimpan data customer dalam batch ke database dengan transaksi
*/
private function saveBatch()
: void
{
if (empty($this->customerBatch)) {
return;
}
$batchSize = count($this->customerBatch);
Log::info("Starting batch save", ['batch_size' => $batchSize]);
try {
if (!empty($this->customerBatch)) {
DB::transaction(function () use ($batchSize) {
// Bulk insert/update customers
Customer::upsert(
$this->customerBatch,
@@ -188,14 +257,26 @@
array_diff((new Customer())->getFillable(), ['customer_code']) // Update columns
);
// Reset customer batch after processing
$this->customerBatch = [];
}
Log::info("Batch save completed successfully", ['batch_size' => $batchSize]);
});
// Reset customer batch after successful processing
$this->customerBatch = [];
} catch (Exception $e) {
Log::error("Error in saveBatch: " . $e->getMessage());
$this->errorCount += count($this->customerBatch);
Log::error("Error in saveBatch", [
'error' => $e->getMessage(),
'batch_size' => $batchSize,
'trace' => $e->getTraceAsString()
]);
$this->errorCount += $batchSize;
// Reset batch even if there's an error to prevent reprocessing the same failed records
$this->customerBatch = [];
// Re-throw exception untuk handling di level atas
throw $e;
}
}

View File

@@ -19,7 +19,7 @@
private const CSV_DELIMITER = '~';
private const MAX_EXECUTION_TIME = 86400; // 24 hours in seconds
private const FILENAME = 'ST.DATA.CAPTURE.csv';
private const DISK_NAME = 'sftpStatement';
private const DISK_NAME = 'staging';
private const CHUNK_SIZE = 1000; // Process data in chunks to reduce memory usage
private const CSV_HEADERS = [
'id',
@@ -187,7 +187,7 @@
{
// Exclude the last field from CSV
if (count($row) > 0) {
array_pop($row);
//array_pop($row);
Log::info("Excluded last field from row $rowCount. New column count: " . count($row));
}

View File

@@ -27,7 +27,7 @@
];
private const MAX_EXECUTION_TIME = 86400; // 24 hours in seconds
private const FILENAME = 'ST.FT.TXN.TYPE.CONDITION.csv';
private const DISK_NAME = 'sftpStatement';
private const DISK_NAME = 'staging';
private string $period = '';
private int $processedCount = 0;

View File

@@ -19,7 +19,7 @@
private const CSV_DELIMITER = '~';
private const MAX_EXECUTION_TIME = 86400; // 24 hours in seconds
private const FILENAME = 'ST.FUNDS.TRANSFER.csv';
private const DISK_NAME = 'sftpStatement';
private const DISK_NAME = 'staging';
private string $period = '';
private int $processedCount = 0;

View File

@@ -20,7 +20,7 @@ class ProcessProvinceDataJob implements ShouldQueue
private const CSV_DELIMITER = '~';
private const MAX_EXECUTION_TIME = 86400; // 24 hours in seconds
private const FILENAME = 'ST.PROVINCE.csv';
private const DISK_NAME = 'sftpStatement';
private const DISK_NAME = 'staging';
private string $period = '';
private int $processedCount = 0;
@@ -29,7 +29,7 @@ class ProcessProvinceDataJob implements ShouldQueue
/**
* Membuat instance job baru untuk memproses data provinsi
*
*
* @param string $period Periode data yang akan diproses
*/
public function __construct(string $period = '')
@@ -41,17 +41,17 @@ class ProcessProvinceDataJob implements ShouldQueue
/**
* Menjalankan job untuk memproses file ST.PROVINCE.csv
* Menggunakan transaction untuk memastikan konsistensi data
*
*
* @return void
* @throws Exception
*/
public function handle(): void
{
DB::beginTransaction();
try {
Log::info('ProcessProvinceDataJob: Memulai pemrosesan data provinsi');
$this->initializeJob();
if ($this->period === '') {
@@ -62,10 +62,10 @@ class ProcessProvinceDataJob implements ShouldQueue
$this->processPeriod();
$this->logJobCompletion();
DB::commit();
Log::info('ProcessProvinceDataJob: Transaction berhasil di-commit');
} catch (Exception $e) {
DB::rollback();
Log::error('ProcessProvinceDataJob: Error dalam pemrosesan, transaction di-rollback: ' . $e->getMessage());
@@ -76,7 +76,7 @@ class ProcessProvinceDataJob implements ShouldQueue
/**
* Inisialisasi pengaturan job
* Mengatur timeout dan reset counter
*
*
* @return void
*/
private function initializeJob(): void
@@ -85,14 +85,14 @@ class ProcessProvinceDataJob implements ShouldQueue
$this->processedCount = 0;
$this->errorCount = 0;
$this->skippedCount = 0;
Log::info('ProcessProvinceDataJob: Job diinisialisasi dengan timeout ' . self::MAX_EXECUTION_TIME . ' detik');
}
/**
* Memproses file untuk periode tertentu
* Mengambil file dari SFTP dan memproses data
*
*
* @return void
*/
private function processPeriod(): void
@@ -101,7 +101,7 @@ class ProcessProvinceDataJob implements ShouldQueue
$filePath = "$this->period/" . self::FILENAME;
Log::info('ProcessProvinceDataJob: Memproses periode ' . $this->period);
if (!$this->validateFile($disk, $filePath)) {
return;
}
@@ -113,7 +113,7 @@ class ProcessProvinceDataJob implements ShouldQueue
/**
* Validasi keberadaan file di storage
*
*
* @param mixed $disk Storage disk instance
* @param string $filePath Path file yang akan divalidasi
* @return bool
@@ -133,7 +133,7 @@ class ProcessProvinceDataJob implements ShouldQueue
/**
* Membuat file temporary untuk pemrosesan
*
*
* @param mixed $disk Storage disk instance
* @param string $filePath Path file sumber
* @return string Path file temporary
@@ -142,7 +142,7 @@ class ProcessProvinceDataJob implements ShouldQueue
{
$tempFilePath = storage_path("app/temp_" . self::FILENAME);
file_put_contents($tempFilePath, $disk->get($filePath));
Log::info("ProcessProvinceDataJob: File temporary dibuat: $tempFilePath");
return $tempFilePath;
}
@@ -150,7 +150,7 @@ class ProcessProvinceDataJob implements ShouldQueue
/**
* Memproses file CSV dan mengimpor data ke database
* Format CSV: id~date_time~province~province_name
*
*
* @param string $tempFilePath Path file temporary
* @param string $filePath Path file asli untuk logging
* @return void
@@ -164,20 +164,20 @@ class ProcessProvinceDataJob implements ShouldQueue
}
Log::info("ProcessProvinceDataJob: Memulai pemrosesan file: $filePath");
$rowCount = 0;
$isFirstRow = true;
while (($row = fgetcsv($handle, 0, self::CSV_DELIMITER)) !== false) {
$rowCount++;
// Skip header row
if ($isFirstRow) {
$isFirstRow = false;
Log::info("ProcessProvinceDataJob: Melewati header row: " . implode(self::CSV_DELIMITER, $row));
continue;
}
$this->processRow($row, $rowCount, $filePath);
}
@@ -187,7 +187,7 @@ class ProcessProvinceDataJob implements ShouldQueue
/**
* Memproses satu baris data CSV
*
*
* @param array $row Data baris CSV
* @param int $rowCount Nomor baris untuk logging
* @param string $filePath Path file untuk logging
@@ -207,16 +207,16 @@ class ProcessProvinceDataJob implements ShouldQueue
'code' => trim($row[2]), // province code
'name' => trim($row[3]) // province_name
];
Log::debug("ProcessProvinceDataJob: Memproses baris $rowCount dengan data: " . json_encode($data));
$this->saveRecord($data, $rowCount, $filePath);
}
/**
* Menyimpan record provinsi ke database
* Menggunakan updateOrCreate untuk menghindari duplikasi
*
*
* @param array $data Data provinsi yang akan disimpan
* @param int $rowCount Nomor baris untuk logging
* @param string $filePath Path file untuk logging
@@ -237,10 +237,10 @@ class ProcessProvinceDataJob implements ShouldQueue
['code' => $data['code']], // Kondisi pencarian
['name' => $data['name']] // Data yang akan diupdate/insert
);
$this->processedCount++;
Log::debug("ProcessProvinceDataJob: Berhasil menyimpan provinsi ID: {$province->id}, Code: {$data['code']}, Name: {$data['name']}");
} catch (Exception $e) {
$this->errorCount++;
Log::error("ProcessProvinceDataJob: Error menyimpan data provinsi pada baris $rowCount di $filePath: " . $e->getMessage());
@@ -250,7 +250,7 @@ class ProcessProvinceDataJob implements ShouldQueue
/**
* Membersihkan file temporary
*
*
* @param string $tempFilePath Path file temporary yang akan dihapus
* @return void
*/
@@ -264,7 +264,7 @@ class ProcessProvinceDataJob implements ShouldQueue
/**
* Logging hasil akhir pemrosesan job
*
*
* @return void
*/
private function logJobCompletion(): void
@@ -273,14 +273,14 @@ class ProcessProvinceDataJob implements ShouldQueue
"Total diproses: {$this->processedCount}, " .
"Total error: {$this->errorCount}, " .
"Total dilewati: {$this->skippedCount}";
Log::info($message);
// Log summary untuk monitoring
if ($this->errorCount > 0) {
Log::warning("ProcessProvinceDataJob: Terdapat {$this->errorCount} error dalam pemrosesan");
}
if ($this->skippedCount > 0) {
Log::info("ProcessProvinceDataJob: Terdapat {$this->skippedCount} baris yang dilewati");
}
@@ -288,7 +288,7 @@ class ProcessProvinceDataJob implements ShouldQueue
/**
* Handle job failure
*
*
* @param Exception $exception
* @return void
*/
@@ -297,4 +297,4 @@ class ProcessProvinceDataJob implements ShouldQueue
Log::error('ProcessProvinceDataJob: Job gagal dijalankan: ' . $exception->getMessage());
Log::error('ProcessProvinceDataJob: Stack trace: ' . $exception->getTraceAsString());
}
}
}

View File

@@ -19,7 +19,7 @@ class ProcessSectorDataJob implements ShouldQueue
private const CSV_DELIMITER = '~';
private const MAX_EXECUTION_TIME = 86400; // 24 hours in seconds
private const FILENAME = 'ST.SECTOR.csv';
private const DISK_NAME = 'sftpStatement';
private const DISK_NAME = 'staging';
private string $period = '';
private int $processedCount = 0;

View File

@@ -19,7 +19,7 @@
private const CSV_DELIMITER = '~';
private const MAX_EXECUTION_TIME = 86400; // 24 hours in seconds
private const FILENAME = 'ST.STMT.ENTRY.csv';
private const DISK_NAME = 'sftpStatement';
private const DISK_NAME = 'staging';
private const CHUNK_SIZE = 1000; // Process data in chunks to reduce memory usage
private string $period = '';

View File

@@ -20,7 +20,7 @@ class ProcessStmtEntryDetailDataJob implements ShouldQueue
private const CSV_DELIMITER = '~';
private const MAX_EXECUTION_TIME = 86400; // 24 hours in seconds
private const FILENAME = 'ST.STMT.ENTRY.DETAIL.csv';
private const DISK_NAME = 'sftpStatement';
private const DISK_NAME = 'staging';
private const CHUNK_SIZE = 1000; // Process data in chunks to reduce memory usage
private string $period = '';

View File

@@ -19,7 +19,7 @@
private const CSV_DELIMITER = '~';
private const MAX_EXECUTION_TIME = 86400; // 24 hours in seconds
private const FILENAME = 'ST.STMT.NARR.FORMAT.csv';
private const DISK_NAME = 'sftpStatement';
private const DISK_NAME = 'staging';
private string $period = '';
private int $processedCount = 0;

View File

@@ -19,7 +19,7 @@
private const CSV_DELIMITER = '~';
private const MAX_EXECUTION_TIME = 86400; // 24 hours in seconds
private const FILENAME = 'ST.STMT.NARR.PARAM.csv';
private const DISK_NAME = 'sftpStatement';
private const DISK_NAME = 'staging';
private string $period = '';
private int $processedCount = 0;

View File

@@ -19,7 +19,7 @@
private const CSV_DELIMITER = '~';
private const MAX_EXECUTION_TIME = 86400; // 24 hours in seconds
private const FILENAME = 'ST.TELLER.csv';
private const DISK_NAME = 'sftpStatement';
private const DISK_NAME = 'staging';
private const CHUNK_SIZE = 1000; // Process data in chunks to reduce memory usage
private const HEADER_MAP = [
'id' => 'id_teller',

View File

@@ -19,7 +19,7 @@
private const CSV_DELIMITER = '~';
private const MAX_EXECUTION_TIME = 86400; // 24 hours in seconds
private const FILENAME = 'ST.TRANSACTION.csv';
private const DISK_NAME = 'sftpStatement';
private const DISK_NAME = 'staging';
private string $period = '';
private int $processedCount = 0;

View File

@@ -29,8 +29,33 @@ class Customer extends Model
'home_rw',
'ktp_rt',
'ktp_rw',
'local_ref'
'local_ref',
'ktp_kelurahan',
'ktp_kecamatan',
'town_country',
'ktp_provinsi',
'post_code',
'l_dom_street',
'l_dom_rt',
'l_dom_kelurahan',
'l_dom_rw',
'l_dom_kecamatan',
'l_dom_provinsi',
'l_dom_t_country',
'l_dom_post_code'
];
/**
* Get the attributes that should be cast.
* Mendefinisikan casting untuk field-field tertentu
*/
protected function casts(): array
{
return [
'date_of_birth' => 'date',
'birth_incorp_date' => 'date',
];
}
public function accounts(){
return $this->hasMany(Account::class, 'customer_code', 'customer_code');
}

View File

@@ -0,0 +1,48 @@
<?php
namespace Modules\Webstatement\Models;
use Illuminate\Database\Eloquent\Model;
class ProcessedClosingBalance extends Model
{
protected $fillable = [
'account_number',
'period',
'group_name',
'sequence_no',
'trans_reference',
'booking_date',
'transaction_date',
'amount_lcy',
'debit_acct_no',
'debit_value_date',
'debit_amount',
'credit_acct_no',
'bif_rcv_acct',
'bif_rcv_name',
'credit_value_date',
'credit_amount',
'at_unique_id',
'bif_ref_no',
'atm_order_id',
'recipt_no',
'api_iss_acct',
'api_benff_acct',
'authoriser',
'remarks',
'payment_details',
'ref_no',
'merchant_id',
'term_id',
'closing_balance',
'unique_hash',
];
protected $casts = [
'amount_lcy' => 'decimal:2',
'debit_amount' => 'decimal:2',
'credit_amount' => 'decimal:2',
'closing_balance' => 'decimal:2'
];
}

View File

@@ -11,7 +11,7 @@ use Modules\Webstatement\Console\{
CombinePdf,
ConvertHtmlToPdf,
ExportDailyStatements,
ProcessDailyMigration,
ProcessDailyStaging,
ExportPeriodStatements,
UpdateAllAtmCardsCommand,
CheckEmailProgressCommand,
@@ -68,7 +68,7 @@ class WebstatementServiceProvider extends ServiceProvider
$this->commands([
GenerateBiayakartuCommand::class,
GenerateBiayaKartuCsvCommand::class,
ProcessDailyMigration::class,
ProcessDailyStaging::class,
ExportDailyStatements::class,
CombinePdf::class,
ConvertHtmlToPdf::class,

View File

@@ -0,0 +1,54 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up()
{
Schema::create('processed_closing_balances', function (Blueprint $table) {
$table->id();
$table->string('account_number', 20)->index();
$table->string('period', 8)->index();
$table->string('group_name', 20)->default('DEFAULT')->index();
$table->integer('sequence_no');
$table->string('trans_reference', 50)->nullable();
$table->string('booking_date', 8)->nullable();
$table->string('transaction_date', 20)->nullable();
$table->decimal('amount_lcy', 15, 2)->nullable();
$table->string('debit_acct_no', 20)->nullable();
$table->string('debit_value_date', 8)->nullable();
$table->decimal('debit_amount', 15, 2)->nullable();
$table->string('credit_acct_no', 20)->nullable();
$table->string('bif_rcv_acct', 20)->nullable();
$table->string('bif_rcv_name', 100)->nullable();
$table->string('credit_value_date', 8)->nullable();
$table->decimal('credit_amount', 15, 2)->nullable();
$table->string('at_unique_id', 50)->nullable();
$table->string('bif_ref_no', 50)->nullable();
$table->string('atm_order_id', 50)->nullable();
$table->string('recipt_no', 50)->nullable();
$table->string('api_iss_acct', 20)->nullable();
$table->string('api_benff_acct', 20)->nullable();
$table->string('authoriser', 50)->nullable();
$table->text('remarks')->nullable();
$table->text('payment_details')->nullable();
$table->string('ref_no', 50)->nullable();
$table->string('merchant_id', 50)->nullable();
$table->string('term_id', 50)->nullable();
$table->decimal('closing_balance', 15, 2)->nullable();
$table->timestamps();
// Composite index untuk performa query
$table->index(['account_number', 'period', 'group_name']);
$table->index(['account_number', 'period', 'sequence_no']);
});
}
public function down()
{
Schema::dropIfExists('processed_closing_balances');
}
};

View File

@@ -0,0 +1,28 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('processed_closing_balances', function (Blueprint $table) {
$table->string('unique_hash')->after('id')->unique();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('processed_closing_balances', function (Blueprint $table) {
$table->dropColumn('unique_hash');
});
}
};

View File

@@ -0,0 +1,57 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
* Menambahkan field-field yang belum ada pada tabel customers
*/
public function up(): void
{
Schema::table('customers', function (Blueprint $table) {
// Field yang belum ada berdasarkan CSV header
$table->string('ktp_kelurahan')->nullable()->after('local_ref')->comment('Kelurahan sesuai KTP');
$table->string('ktp_kecamatan')->nullable()->after('ktp_kelurahan')->comment('Kecamatan sesuai KTP');
$table->string('town_country')->nullable()->after('ktp_kecamatan')->comment('Kota/Negara');
$table->string('ktp_provinsi')->nullable()->after('town_country')->comment('Provinsi sesuai KTP');
$table->string('post_code')->nullable()->after('ktp_provinsi')->comment('Kode pos alternatif');
$table->string('l_dom_street')->nullable()->after('post_code')->comment('Alamat domisili - jalan');
$table->string('l_dom_rt')->nullable()->after('l_dom_street')->comment('Alamat domisili - RT');
$table->string('l_dom_kelurahan')->nullable()->after('l_dom_rt')->comment('Alamat domisili - kelurahan');
$table->string('l_dom_rw')->nullable()->after('l_dom_kelurahan')->comment('Alamat domisili - RW');
$table->string('l_dom_kecamatan')->nullable()->after('l_dom_rw')->comment('Alamat domisili - kecamatan');
$table->string('l_dom_provinsi')->nullable()->after('l_dom_kecamatan')->comment('Alamat domisili - provinsi');
$table->string('l_dom_t_country')->nullable()->after('l_dom_provinsi')->comment('Alamat domisili - kota/negara');
$table->string('l_dom_post_code')->nullable()->after('l_dom_t_country')->comment('Alamat domisili - kode pos');
});
}
/**
* Reverse the migrations.
* Menghapus field-field yang ditambahkan
*/
public function down(): void
{
Schema::table('customers', function (Blueprint $table) {
$table->dropColumn([
'ktp_kelurahan',
'ktp_kecamatan',
'town_country',
'ktp_provinsi',
'post_code',
'l_dom_street',
'l_dom_rt',
'l_dom_kelurahan',
'l_dom_rw',
'l_dom_kecamatan',
'l_dom_provinsi',
'l_dom_t_country',
'l_dom_post_code'
]);
});
}
};