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';
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';
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()
{
$queueName = $this->option('queue_name');
// Log start of process
Log::info('Starting daily statement export process', [
'queue_name' => $queueName ?? 'default',
'command' => 'webstatement:export-statements'
]);
$this->info('Starting daily statement export process...');
$this->info('Queue Name: ' . ($queueName ?? 'default'));
try {
$controller = app(WebstatementController::class);
$response = $controller->index();
// Pass queue name to controller if needed
// Jika controller membutuhkan queue name, bisa ditambahkan sebagai parameter
$response = $controller->index($queueName);
$responseData = json_decode($response->getContent(), true);
$this->info($responseData['message']);
$message = $responseData['message'] ?? 'Export process completed';
$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) {
$this->error('Error exporting statements: ' . $e->getMessage());
$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,21 +1,33 @@
<?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()
public function index(Request $request)
{
$queueName = $request->get('queue_name', 'default');
Log::info('Starting statement export process', [
'queue_name' => $queueName
]);
$jobIds = [];
$data = [];
@@ -28,24 +40,34 @@
$this->getAccountBalance($accountNumber, $period),
$clientName // Pass the client name to the job
);
$jobIds[] = app(Dispatcher::class)->dispatch($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
'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)
@@ -142,8 +164,18 @@
}
function printStatementRekening($accountNumber, $period = null) {
/**
* 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')
@@ -159,21 +191,28 @@
$clientName = 'client1';
try {
\Log::info("Starting statement export for account: {$accountNumber}, period: {$period}, client: {$clientName}");
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
$job = ExportStatementPeriodJob::dispatch($accountNumber, $period, $balance, $clientName);
// Dispatch the job dengan queue name yang spesifik
$job = ExportStatementPeriodJob::dispatch($accountNumber, $period, $balance, $clientName)
->onQueue($queueName);
\Log::info("Statement export job dispatched successfully", [
Log::info("Statement export job dispatched successfully", [
'job_id' => $job->job_id ?? null,
'account' => $accountNumber,
'period' => $period,
'client' => $clientName
'client' => $clientName,
'queue_name' => $queueName
]);
return response()->json([
@@ -183,22 +222,25 @@
'job_id' => $job->job_id ?? null,
'account_number' => $accountNumber,
'period' => $period,
'client_name' => $clientName
'client_name' => $clientName,
'queue_name' => $queueName
]
]);
} catch (\Exception $e) {
\Log::error("Failed to export statement", [
Log::error("Failed to export statement", [
'error' => $e->getMessage(),
'account' => $accountNumber,
'period' => $period
'period' => $period,
'queue_name' => $queueName
]);
return response()->json([
'success' => false,
'message' => 'Failed to queue statement export job',
'error' => $e->getMessage()
'error' => $e->getMessage(),
'queue_name' => $queueName
]);
}
}
}
}

View File

@@ -1,30 +1,26 @@
<?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\Storage;
use Carbon\Carbon;
use Exception;
use Modules\Webstatement\Models\AccountBalance;
use Modules\Webstatement\Models\ClosingBalanceReportLog;
use Modules\Webstatement\Models\StmtEntry;
use Modules\Webstatement\Models\StmtEntryDetail;
use Modules\Webstatement\Models\TempFundsTransfer;
use Modules\Webstatement\Models\DataCapture;
use Carbon\Carbon;
use Exception;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\{InteractsWithQueue, SerializesModels};
use Illuminate\Support\Facades\{DB, Log, Storage};
use Modules\Webstatement\Models\{AccountBalance,
ClosingBalanceReportLog,
ProcessedClosingBalance,
StmtEntry,
StmtEntryDetail};
/**
* Job untuk generate laporan closing balance
* Mengambil data transaksi dan menghitung closing balance
/**
* Job untuk generate laporan closing balance dengan optimasi performa
* Menggunakan database staging sebelum export CSV
*/
class GenerateClosingBalanceReportJob implements ShouldQueue
{
class GenerateClosingBalanceReportJob implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
protected $accountNumber;
@@ -36,12 +32,8 @@ class GenerateClosingBalanceReportJob implements ShouldQueue
/**
* Create a new job instance.
*
* @param string $accountNumber
* @param string $period
* @param int $reportLogId
*/
public function __construct(string $accountNumber, string $period, int $reportLogId, string $groupName='DEFAULT')
public function __construct(string $accountNumber, string $period, int $reportLogId, string $groupName = 'DEFAULT')
{
$this->accountNumber = $accountNumber;
$this->period = $period;
@@ -50,8 +42,7 @@ class GenerateClosingBalanceReportJob implements ShouldQueue
}
/**
* Execute the job.
* Memproses data transaksi dan generate laporan closing balance
* Execute the job dengan optimasi performa
*/
public function handle(): void
{
@@ -63,51 +54,49 @@ class GenerateClosingBalanceReportJob implements ShouldQueue
}
try {
Log::info('Starting closing balance report generation', [
Log::info('Starting optimized closing balance report generation', [
'account_number' => $this->accountNumber,
'period' => $this->period,
'group_name' => $this->groupName,
'report_log_id' => $this->reportLogId
]);
DB::beginTransaction();
// Update status to processing
$reportLog->update([
'status' => 'processing',
'updated_at' => now()
]);
// Get opening balance
$openingBalance = $this->getOpeningBalance();
// Gunakan satu transaksi untuk seluruh proses
DB::transaction(function () use ($reportLog) {
// Step 1: Process and save to database (fast)
$this->processAndSaveClosingBalanceData();
// Generate report data
$reportData = $this->generateReportData($openingBalance);
// Step 2: Export from database to CSV (fast)
$filePath = $this->exportFromDatabaseToCsv();
// Export to CSV
$filePath = $this->exportToCsv($reportData);
// Get record count from database
$recordCount = $this->getProcessedRecordCount();
// Update report log with success
$reportLog->update([
'status' => 'completed',
'file_path' => $filePath,
'file_size' => Storage::disk($this->disk)->size($filePath),
'record_count' => count($reportData),
'record_count' => $recordCount,
'updated_at' => now()
]);
DB::commit();
Log::info('Closing balance report generation completed successfully', [
Log::info('Optimized closing balance report generation completed successfully', [
'account_number' => $this->accountNumber,
'period' => $this->period,
'file_path' => $filePath,
'record_count' => count($reportData)
'record_count' => $recordCount
]);
});
} catch (Exception $e) {
DB::rollback();
Log::error('Error generating closing balance report', [
Log::error('Error generating optimized closing balance report', [
'account_number' => $this->accountNumber,
'period' => $this->period,
'error' => $e->getMessage(),
@@ -124,11 +113,61 @@ class GenerateClosingBalanceReportJob implements ShouldQueue
}
}
/**
* Process and save closing balance data to database dengan proteksi duplikasi
* Memproses dan menyimpan data closing balance dengan perlindungan terhadap duplikasi
*/
private function processAndSaveClosingBalanceData(): void
{
$criteria = [
'account_number' => $this->accountNumber,
'period' => $this->period,
'group_name' => $this->groupName
];
// HAPUS DB::beginTransaction() - sudah ada di handle()
// Sederhana: hapus data existing terlebih dahulu seperti ExportStatementJob
$this->deleteExistingProcessedData($criteria);
// Get opening balance
$runningBalance = $this->getOpeningBalance();
$sequenceNo = 0;
Log::info('Starting to process closing balance data', [
'opening_balance' => $runningBalance,
'criteria' => $criteria
]);
// Build query yang sederhana tanpa eliminasi duplicate rumit
$query = $this->buildTransactionQuery();
// Proses dan insert data dengan batch updateOrCreate untuk efisiensi
$query->chunk($this->chunkSize, function ($transactions) use (&$runningBalance, &$sequenceNo) {
$processedData = $this->prepareProcessedClosingBalanceData($transactions, $runningBalance, $sequenceNo);
if (!empty($processedData)) {
// Gunakan batch processing untuk updateOrCreate
$this->batchUpdateOrCreate($processedData);
}
});
// HAPUS DB::commit() - akan di-handle di handle()
$recordCount = $this->getProcessedRecordCount();
Log::info('Closing balance data processing completed successfully', [
'final_sequence' => $sequenceNo,
'final_balance' => $runningBalance,
'record_count' => $recordCount
]);
}
/**
* Get opening balance from account balance table
* Mengambil saldo awal dari tabel account balance
*/
private function getOpeningBalance(): float
private function getOpeningBalance()
: float
{
Log::info('Getting opening balance', [
'account_number' => $this->accountNumber,
@@ -163,71 +202,42 @@ class GenerateClosingBalanceReportJob implements ShouldQueue
}
/**
* Build transaction query using pure Eloquent relationships
* Membangun query transaksi menggunakan relasi Eloquent murni
* Build transaction query dengan pendekatan sederhana tanpa eliminasi duplicate rumit
*/
private function buildTransactionQuery()
{
Log::info('Building transaction query using pure Eloquent relationships', [
Log::info('Building transaction query', [
'group_name' => $this->groupName,
'account_number' => $this->accountNumber,
'period' => $this->period
]);
// Tentukan model berdasarkan group name
$modelClass = $this->getModelByGroup();
// Build query menggunakan pure Eloquent dengan eager loading
$query = $modelClass::with([
'ft' => function($query) {
$query->select([
'_id',
'ref_no',
'debit_acct_no',
'debit_value_date',
'credit_acct_no',
'bif_rcv_acct',
'bif_rcv_name',
'credit_value_date',
'at_unique_id',
'bif_ref_no',
'atm_order_id',
'recipt_no',
'api_iss_acct',
'api_benff_acct',
'authoriser',
'remarks',
'payment_details',
'merchant_id',
'term_id',
'date_time'
]);
},
'dc' => function($query) {
$query->select([
'id',
'date_time'
]);
}
])
->select([
$query = $modelClass::select([
'id',
'trans_reference',
'booking_date',
'amount_lcy',
'date_time'
])
->with([
'ft' => function ($query) {
$query->select('ref_no', 'date_time', 'debit_acct_no', 'debit_value_date',
'credit_acct_no', 'bif_rcv_acct', 'bif_rcv_name', 'credit_value_date',
'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');
},
'dc' => function ($query) {
$query->select('id', 'date_time');
}
])
->where('account_number', $this->accountNumber)
->where('booking_date', $this->period)
->orderBy('booking_date')
->orderBy('date_time');
Log::info('Transaction query built successfully using pure Eloquent', [
'model_class' => $modelClass,
'account_number' => $this->accountNumber,
'period' => $this->period
]);
return $query;
}
@@ -251,11 +261,76 @@ class GenerateClosingBalanceReportJob implements ShouldQueue
return $model;
}
/**
* Prepare processed closing balance data tanpa validasi duplikasi
*/
private function prepareProcessedClosingBalanceData($transactions, &$runningBalance, &$sequenceNo)
: array
{
$processedData = [];
foreach ($transactions as $transaction) {
$sequenceNo++;
// Process transaction data
$processedTransactionData = $this->processTransactionData($transaction);
// Update running balance
$amount = (float) $transaction->amount_lcy;
$runningBalance += $amount;
// Format transaction date
$transactionDate = $this->formatDateTime($processedTransactionData['date_time']);
// Prepare data untuk database insert tanpa unique_hash
$processedData[] = [
'account_number' => $this->accountNumber,
'period' => $this->period,
'group_name' => $this->groupName,
'sequence_no' => $sequenceNo,
'trans_reference' => $processedTransactionData['trans_reference'],
'booking_date' => $processedTransactionData['booking_date'],
'transaction_date' => $transactionDate,
'amount_lcy' => $processedTransactionData['amount_lcy'],
'debit_acct_no' => $processedTransactionData['debit_acct_no'],
'debit_value_date' => $processedTransactionData['debit_value_date'],
'debit_amount' => $processedTransactionData['debit_amount'],
'credit_acct_no' => $processedTransactionData['credit_acct_no'],
'bif_rcv_acct' => $processedTransactionData['bif_rcv_acct'],
'bif_rcv_name' => $processedTransactionData['bif_rcv_name'],
'credit_value_date' => $processedTransactionData['credit_value_date'],
'credit_amount' => $processedTransactionData['credit_amount'],
'at_unique_id' => $processedTransactionData['at_unique_id'],
'bif_ref_no' => $processedTransactionData['bif_ref_no'],
'atm_order_id' => $processedTransactionData['atm_order_id'],
'recipt_no' => $processedTransactionData['recipt_no'],
'api_iss_acct' => $processedTransactionData['api_iss_acct'],
'api_benff_acct' => $processedTransactionData['api_benff_acct'],
'authoriser' => $processedTransactionData['authoriser'],
'remarks' => $processedTransactionData['remarks'],
'payment_details' => $processedTransactionData['payment_details'],
'ref_no' => $processedTransactionData['ref_no'],
'merchant_id' => $processedTransactionData['merchant_id'],
'term_id' => $processedTransactionData['term_id'],
'closing_balance' => $runningBalance,
'created_at' => now(),
'updated_at' => now(),
];
}
Log::info('Processed closing balance data prepared', [
'total_records' => count($processedData)
]);
return $processedData;
}
/**
* Process transaction data from ORM result
* Memproses data transaksi dari hasil ORM
*/
private function processTransactionData($transaction): array
private function processTransactionData($transaction)
: array
{
Log::info('Processing transaction data', [
'trans_reference' => $transaction->trans_reference,
@@ -310,174 +385,12 @@ class GenerateClosingBalanceReportJob implements ShouldQueue
return $processedData;
}
/**
* Updated generateReportData method using pure ORM
* Method generateReportData yang diperbarui menggunakan ORM murni
*/
private function generateReportData(): array
{
Log::info('Starting report data generation using pure ORM', [
'account_number' => $this->accountNumber,
'period' => $this->period,
'group_name' => $this->groupName,
'chunk_size' => $this->chunkSize
]);
$reportData = [];
$runningBalance = $this->getOpeningBalance();
$sequenceNo = 1;
try {
DB::beginTransaction();
// Build query menggunakan pure ORM
$query = $this->buildTransactionQuery();
// Process data dalam chunks untuk efisiensi memory
$query->chunk($this->chunkSize, function ($transactions) use (&$reportData, &$runningBalance, &$sequenceNo) {
Log::info('Processing transaction chunk', [
'chunk_size' => $transactions->count(),
'current_sequence' => $sequenceNo,
'current_balance' => $runningBalance
]);
foreach ($transactions as $transaction) {
// Process transaction data
$processedData = $this->processTransactionData($transaction);
// Update running balance
$amount = (float) $transaction->amount_lcy;
$runningBalance += $amount;
// Format transaction date
$transactionDate = $this->formatDateTime($processedData['date_time']);
// Build report data row
$reportData[] = $this->buildReportDataRow(
(object) $processedData,
$sequenceNo,
$transactionDate,
$runningBalance
);
$sequenceNo++;
}
Log::info('Chunk processed successfully', [
'processed_count' => $transactions->count(),
'total_records_so_far' => count($reportData),
'current_balance' => $runningBalance
]);
});
DB::commit();
Log::info('Report data generation completed using pure ORM', [
'total_records' => count($reportData),
'final_balance' => $runningBalance,
'final_sequence' => $sequenceNo - 1
]);
} catch (Exception $e) {
DB::rollback();
Log::error('Error generating report data using pure ORM', [
'error' => $e->getMessage(),
'trace' => $e->getTraceAsString(),
'account_number' => $this->accountNumber,
'period' => $this->period
]);
throw $e;
}
return $reportData;
}
/**
* Get table name based on group name
* Mendapatkan nama tabel berdasarkan group name
*/
private function getTableNameByGroup(): string
{
return $this->groupName === 'QRIS' ? 'stmt_entry' : 'stmt_entry_details';
}
/**
* Get select fields for the query
* Mendapatkan field select untuk query
*/
private function getSelectFields(): array
{
return [
's.trans_reference',
's.booking_date',
's.amount_lcy',
'ft.debit_acct_no',
'ft.debit_value_date',
DB::raw('CASE WHEN s.amount_lcy::numeric < 0 THEN s.amount_lcy::numeric ELSE NULL END AS debit_amount'),
'ft.credit_acct_no',
'ft.bif_rcv_acct',
'ft.bif_rcv_name',
'ft.credit_value_date',
DB::raw('CASE WHEN s.amount_lcy::numeric > 0 THEN s.amount_lcy::numeric ELSE NULL END AS credit_amount'),
'ft.at_unique_id',
'ft.bif_ref_no',
'ft.atm_order_id',
'ft.recipt_no',
'ft.api_iss_acct',
'ft.api_benff_acct',
DB::raw('COALESCE(ft.date_time, dc.date_time, s.date_time) AS date_time'),
'ft.authoriser',
'ft.remarks',
'ft.payment_details',
'ft.ref_no',
'ft.merchant_id',
'ft.term_id'
];
}
/**
* Build report data row from transaction
* Membangun baris data laporan dari transaksi
*/
private function buildReportDataRow($transaction, int $sequenceNo, string $transactionDate, float $runningBalance): array
{
return [
'sequence_no' => $sequenceNo,
'trans_reference' => $transaction->trans_reference,
'booking_date' => $transaction->booking_date,
'transaction_date' => $transactionDate,
'amount_lcy' => $transaction->amount_lcy,
'debit_acct_no' => $transaction->debit_acct_no,
'debit_value_date' => $transaction->debit_value_date,
'debit_amount' => $transaction->debit_amount,
'credit_acct_no' => $transaction->credit_acct_no,
'bif_rcv_acct' => $transaction->bif_rcv_acct,
'bif_rcv_name' => $transaction->bif_rcv_name,
'credit_value_date' => $transaction->credit_value_date,
'credit_amount' => $transaction->credit_amount,
'at_unique_id' => $transaction->at_unique_id,
'bif_ref_no' => $transaction->bif_ref_no,
'atm_order_id' => $transaction->atm_order_id,
'recipt_no' => $transaction->recipt_no,
'api_iss_acct' => $transaction->api_iss_acct,
'api_benff_acct' => $transaction->api_benff_acct,
'authoriser' => $transaction->authoriser,
'remarks' => $transaction->remarks,
'payment_details' => $transaction->payment_details,
'ref_no' => $transaction->ref_no,
'merchant_id' => $transaction->merchant_id,
'term_id' => $transaction->term_id,
'closing_balance' => $runningBalance
];
}
/**
* Format datetime string
* Memformat string datetime
*/
private function formatDateTime(?string $datetime): string
private function formatDateTime(?string $datetime)
: string
{
if (!$datetime) {
return '';
@@ -493,17 +406,16 @@ class GenerateClosingBalanceReportJob implements ShouldQueue
return $datetime;
}
}
/**
* Export report data to CSV file
* Export data laporan ke file CSV
* Export from database to CSV (very fast)
*/
private function exportToCsv(array $reportData): string
private function exportFromDatabaseToCsv()
: string
{
Log::info('Starting CSV export for closing balance report', [
Log::info('Starting CSV export from database for closing balance report', [
'account_number' => $this->accountNumber,
'period' => $this->period,
'record_count' => count($reportData)
'group_name' => $this->groupName
]);
// Create directory structure
@@ -514,7 +426,7 @@ class GenerateClosingBalanceReportJob implements ShouldQueue
Storage::disk($this->disk)->makeDirectory($accountPath);
// Generate filename
$fileName = "closing_balance_{$this->accountNumber}_{$this->period}.csv";
$fileName = "closing_balance_{$this->accountNumber}_{$this->period}_{$this->groupName}.csv";
$filePath = "{$accountPath}/{$fileName}";
// Delete existing file if exists
@@ -553,54 +465,177 @@ class GenerateClosingBalanceReportJob implements ShouldQueue
];
$csvContent = implode('|', $csvHeader) . "\n";
Storage::disk($this->disk)->put($filePath, $csvContent);
// Inisialisasi counter untuk sequence number
$sequenceCounter = 1;
$processedHashes = [];
ProcessedClosingBalance::where('account_number', $this->accountNumber)
->where('period', $this->period)
->where('group_name', $this->groupName)
->orderBy('sequence_no')
->chunk($this->chunkSize, function ($records) use ($filePath, &$sequenceCounter, &$processedHashes) {
$csvContent = [];
foreach ($records as $record) {
// Pengecekan unique_hash: skip jika sudah diproses
if (in_array($record->unique_hash, $processedHashes)) {
Log::debug('Skipping duplicate unique_hash in CSV export', [
'unique_hash' => $record->unique_hash,
'trans_reference' => $record->trans_reference
]);
continue;
}
// Tandai unique_hash sebagai sudah diproses
$processedHashes[] = $record->unique_hash;
// Add data rows
foreach ($reportData as $row) {
$csvRow = [
$row['sequence_no'],
$row['trans_reference'] ?? '',
$row['booking_date'] ?? '',
$row['transaction_date'] ?? '',
$row['amount_lcy'] ?? '',
$row['debit_acct_no'] ?? '',
$row['debit_value_date'] ?? '',
$row['debit_amount'] ?? '',
$row['credit_acct_no'] ?? '',
$row['bif_rcv_acct'] ?? '',
$row['bif_rcv_name'] ?? '',
$row['credit_value_date'] ?? '',
$row['credit_amount'] ?? '',
$row['at_unique_id'] ?? '',
$row['bif_ref_no'] ?? '',
$row['atm_order_id'] ?? '',
$row['recipt_no'] ?? '',
$row['api_iss_acct'] ?? '',
$row['api_benff_acct'] ?? '',
$row['authoriser'] ?? '',
$row['remarks'] ?? '',
$row['payment_details'] ?? '',
$row['ref_no'] ?? '',
$row['merchant_id'] ?? '',
$row['term_id'] ?? '',
$row['closing_balance'] ?? ''
$sequenceCounter++,
// Gunakan counter yang bertambah, bukan sequence_no dari database
$record->trans_reference ?? '',
$record->booking_date ?? '',
$record->transaction_date ?? '',
$record->amount_lcy ?? '',
$record->debit_acct_no ?? '',
$record->debit_value_date ?? '',
$record->debit_amount ?? '',
$record->credit_acct_no ?? '',
$record->bif_rcv_acct ?? '',
$record->bif_rcv_name ?? '',
$record->credit_value_date ?? '',
$record->credit_amount ?? '',
$record->at_unique_id ?? '',
$record->bif_ref_no ?? '',
$record->atm_order_id ?? '',
$record->recipt_no ?? '',
$record->api_iss_acct ?? '',
$record->api_benff_acct ?? '',
$record->authoriser ?? '',
$record->remarks ?? '',
$record->payment_details ?? '',
$record->ref_no ?? '',
$record->merchant_id ?? '',
$record->term_id ?? '',
$record->closing_balance ?? ''
];
$csvContent .= implode('|', $csvRow) . "\n";
}
// Save file
Storage::disk($this->disk)->put($filePath, $csvContent);
if (!empty($csvContent)) {
Storage::disk($this->disk)->append($filePath, $csvContent);
Log::debug('CSV content appended', [
'records_processed' => substr_count($csvContent, "\n"),
'current_sequence' => $sequenceCounter - 1
]);
}
});
// Verify file creation
if (!Storage::disk($this->disk)->exists($filePath)) {
throw new Exception("Failed to create CSV file: {$filePath}");
}
Log::info('CSV export completed successfully', [
Log::info('CSV export from database completed successfully', [
'file_path' => $filePath,
'file_size' => Storage::disk($this->disk)->size($filePath)
]);
return $filePath;
}
}
/**
* Get processed record count
*/
private function getProcessedRecordCount()
: int
{
return ProcessedClosingBalance::where('account_number', $this->accountNumber)
->where('period', $this->period)
->where('group_name', $this->groupName)
->count();
}
/**
* Delete existing processed data dengan pendekatan sederhana seperti ExportStatementJob
*/
private function deleteExistingProcessedData(array $criteria)
: void
{
Log::info('Deleting existing processed data', $criteria);
$deletedCount = ProcessedClosingBalance::where('account_number', $criteria['account_number'])
->where('period', $criteria['period'])
->delete();
Log::info('Existing processed data deleted', [
'deleted_count' => $deletedCount,
'criteria' => $criteria
]);
}
/**
* Batch update or create untuk mengurangi jumlah query dan lock
* Menggunakan pendekatan yang lebih efisien untuk menghindari max_lock_per_transaction
*/
private function batchUpdateOrCreate(array $processedData): void
{
Log::info('Starting batch updateOrCreate', [
'batch_size' => count($processedData)
]);
// Kumpulkan semua trans_reference yang akan diproses
$transReferences = collect($processedData)->pluck('trans_reference')->toArray();
// Ambil data yang sudah ada dalam satu query
$existingRecords = ProcessedClosingBalance::where('account_number', $this->accountNumber)
->where('period', $this->period)
->where('group_name', $this->groupName)
->whereIn('trans_reference', $transReferences)
->get()
->keyBy(function ($item) {
return $item->trans_reference . '_' . $item->amount_lcy;
});
$toInsert = [];
$toUpdate = [];
foreach ($processedData as $data) {
$key = $data['trans_reference'] . '_' . $data['amount_lcy'];
if ($existingRecords->has($key)) {
// Record sudah ada, siapkan untuk update
$existingRecord = $existingRecords->get($key);
$toUpdate[] = [
'id' => $existingRecord->id,
'data' => $data
];
} else {
// Record baru, siapkan untuk insert
$toInsert[] = $data;
}
}
// Batch insert untuk record baru
if (!empty($toInsert)) {
DB::table('processed_closing_balances')->insert($toInsert);
Log::info('Batch insert completed', ['count' => count($toInsert)]);
}
// Batch update untuk record yang sudah ada
if (!empty($toUpdate)) {
foreach ($toUpdate as $updateItem) {
ProcessedClosingBalance::where('id', $updateItem['id'])
->update($updateItem['data']);
}
Log::info('Batch update completed', ['count' => count($toUpdate)]);
}
Log::info('Batch updateOrCreate completed successfully', [
'inserted' => count($toInsert),
'updated' => count($toUpdate)
]);
}
}

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
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;

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