# Porting cron dan worker ke CI4

Pembaruan: 17 Oktober 2026.

Kelima file di `pos/cron` sekarang tersedia sebagai service CI4, perintah spark, dan endpoint HTTP dengan alias `.php`. File PHP lama tidak dieksekusi atau di-include saat runtime.

| Cron sumber | Perintah dari folder aplikasi CI4 |
| --- | --- |
| process_whatsapp_queue | php spark wa:process-queue |
| send_scheduled_blasts | php spark wa:scheduled-blasts |
| charge_daily_saldo | php spark billing:charge-daily |
| cek_status | php spark wa:check-status |
| cek_status_realtime | php spark wa:check-status-realtime |

Masing-masing endpoint `cron/<nama>` menerima GET/POST. Akses HTTP memerlukan **CRON_KEY yang tidak kosong**, melalui header X-Cron-Key atau query key, dibandingkan dengan hash_equals. Sesi login tidak menggantikan key. CLI tidak membutuhkan key HTTP. Kesalahan worker dilaporkan lewat JSON; CLI mengembalikan exit code gagal jika success false.

`CronJobs` mengambil named lock MySQL per job dan selalu melepaskannya dalam finally. Proses job yang sama yang sedang berjalan menghasilkan busy:true; koneksi default CI4 harus dipertahankan selama run. Worker status harian dan realtime memakai lock berbeda, tetapi pembaruan pesan tetap memakai lock toko/pesan.

## Antrean WhatsApp

- Maksimal 1000 baris pending yang sudah waktunya, attempts kurang dari lima, dan toko aktif per run. Dikelompokkan menurut toko/source dengan maksimal 200 pesan per batch. Nomor invalid difilter sebelum pengiriman agar urutan hasil helper tidak bergeser.
- Status processing ditambahkan pada schema antrean bila diperlukan. ID dipilih lalu ditandai processing dan attempts dinaikkan sebelum request provider. Penyimpanan hasil dibatasi ID, toko, dan status processing.
- Hasil per item digunakan jika tersedia. Kegagalan eksplisit dicoba kembali hingga lima percobaan; setelah itu failed. Aggregate success tidak dianggap berhasil untuk item yang hilang dari results.
- Exception pengiriman, hasil item yang tidak diketahui, dan baris processing tersisa dari worker yang terputus ditandai failed dengan catatan hasil tidak pasti. **Periksa provider sebelum mengulang baris tersebut**, karena provider mungkin sudah menerima pesan. Tidak ada jaminan exactly-once delivery atau retry otomatis untuk hasil yang tidak pasti.
- Ketika results tidak disediakan sama sekali, hasil aggregate berlaku untuk seluruh batch, mengikuti kontrak helper. Retry kegagalan provider tetap bergantung pada ketepatan hasil provider. Helper batch diperketat agar status string error/failed dan respons non-array tidak dianggap sukses.
- Field media diteruskan sesuai helper yang sudah ada. Dukungan media tetap bergantung pada gateway; jalur Wablas instant lama mengirim teks, sedangkan bot batch mendukung URL media. Worker ini tidak menambahkan API upload media Wablas baru.
- Baris lama pending dengan attempts sudah lima tidak dipilih. Pemulihan manual dan perubahan status antrean tidak dilakukan otomatis selain penanganan processing di atas.

## Blast terjadwal

Pencocokan hari ISO dan jam-menit tetap mengikuti sumber. Jadwal yang terlewat satu menit tidak di-catch-up otomatis. Jadwal diperiksa ulang di bawah lock toko dan jadwal, termasuk last_sent_date, sehingga dua pemanggilan tidak menggandakan enqueue.

Pemilihan customer, enqueue seluruh penerima, last_sent_date, dan log jadwal memakai satu transaksi. Kegagalan enqueue/log menggulung balik seluruh jadwal, sehingga dapat diulang tanpa antrean parsial. Nomor dinormalisasi dan penerima duplikat dilewati. Tag dicocokkan sebagai tag penuh; fallback LIKE sumber yang dapat mencocokkan substring tag dihapus.

Placeholder {name}/{link} tetap tersedia. Media harus merupakan file dalam public/uploads/wa_blast; file hilang atau di luar folder membatalkan jadwal, bukan mengirim tanpa lampiran diam-diam. Log sent_count dan summary sent pada scheduler berarti **diantrikan**, bukan bukti terkirim. Label halaman jadwal diperjelas menjadi Diantrikan.

## Saldo harian dan affiliate

Saldo, paket, serta last_charged_date dibaca ulang di bawah SELECT FOR UPDATE. Tanggal null mulai ditagih hari ini; tanggal sebelumnya di-catch-up sampai hari ini. Paket tarif nol hanya memajukan tanggal. Saldo dan setiap ledger harian beserta penanda tanggal berada dalam satu transaksi per toko; kegagalan hari berikutnya membatalkan seluruh catch-up toko pada run tersebut.

Saldo tidak cukup menyebabkan downgrade hanya bila paket gratis aktif dengan tarif nol benar-benar tersedia. Ledger downgrade, penggantian paket, sinkronisasi subscription (jika tabel tersedia), saldo, dan tanggal diperlakukan atomik. Paket gratis tidak tersedia atau kegagalan subscription membatalkan perubahan, bukan memajukan tanggal secara diam-diam. Last charged date hari ini/masa depan tidak ditagih ulang.

Komisi affiliate tetap diproses melalui helper yang sudah ada, dalam transaksi terpisah dari tagihan paket dan bersifat best effort seperti sumber. Helper diperkuat dengan lock toko dan pengecekan ulang ledger sale sebelum komisi dipotong, agar pemanggilan dari laporan affiliate dan cron tidak menggandakan debit. Komisi yang sudah commit tidak dibatalkan jika tagihan paket kemudian gagal.

## Polling status Wablas

Kredensial diambil per toko aktif yang menggunakan Wablas; tidak ada pembaruan global berdasarkan external_id saja. URL report diturunkan dari origin HTTPS whatsapp_url toko, dengan verifikasi TLS dan timeout. Konfigurasi global lama tidak ditebak sebagai kredensial untuk semua toko.

Report biasa membaca data.messages, realtime juga mendukung message. Pesan keluar dicari menurut toko dan external_id; hasil ambigu dilewati. Pembaruan menggunakan WablasEvents::tracking sehingga mempertahankan pemeriksaan perangkat, callback idempoten, serta perlindungan status read/delivered dari regresi status umum. Summary dan API log tidak menyalin kredensial atau respons provider mentah ke hasil HTTP/CLI.

Default tetap satu halaman (page 1, limit/perPage 100), bukan seluruh histori. Endpoint HTTP menerima page, limit (1–1000), dan message_id untuk realtime; nilai tidak valid ditolak. Layanan mendukung opsi tersebut secara internal, sedangkan perintah CLI tahap ini memakai default. Jadwalkan polling sesuai kebutuhan jika webhook tracking tidak cukup; tidak ada auto-pagination.

## Menjalankan pada server

**Tidak ada scheduler OS yang dipasang atau worker nyata yang dijalankan pada tahap porting ini.** Jalankan dua job WA setiap menit dan saldo harian sekali sehari sesuai kebutuhan deployment. Gunakan executable PHP dan path spark CI4 yang benar pada server.

Zona waktu mengikuti konfigurasi aplikasi; default App saat ini UTC. Atur app.appTimezone agar cocok dengan jam jadwal dan tanggal bisnis, misalnya Asia/Jakarta, sebelum mengaktifkan scheduler. Timestamp antrean lama harus menggunakan zona waktu yang konsisten.

Saat berpindah, hentikan scheduler script lama yang memproses tabel sama sebelum mengaktifkan worker CI4. Named lock baru tidak melindungi dari script lama yang tidak mengambil lock tersebut. Jangan menjalankan versi lama dan CI4 secara bersamaan untuk job yang sama.

## Verifikasi

- PHPUnit seluruh aplikasi: **281 tes, 2.199 assertion**, lulus.
- Termasuk **21 tes cron**: alias/key HTTP, lock busy/release, claim sebelum kirim, batas 200 pesan, hasil parsial/retry/hasil tidak pasti, enqueue jadwal atomik, deduplikasi nomor, jadwal berulang, catch-up saldo, downgrade/subscription, rollback ledger, serta report harian/realtime dengan transport tiruan.
- Lint **333 file PHP**: tanpa kesalahan sintaks.
- spark list --simple mengenali kelima perintah baru. Perintah worker tidak dijalankan terhadap data nyata.
- Seluruh test menggunakan koneksi database dan transport tiruan. Tidak mengirim WhatsApp, mengambil report provider nyata, atau memotong saldo produksi. Pengujian MySQL/concurrency, scheduler OS, gateway, dan tagihan staging masih perlu dilakukan.

## Sisa porting

Daftar sebelumnya berisi 28 file belum dipindahkan. Setelah lima cron ini, tersisa **23 file halaman/endpoint** yang belum mempunyai route CI4: tiga di pos/admin dan dua puluh di folder utama (publik, checkout/booking, autentikasi tambahan, tracking, dan lainnya). Konfigurasi/helper gabungan tidak dihitung sebagai halaman terpisah. Batasan implementasi dan integrasi nyata pada tahap sebelumnya tetap berlaku.
