# Porting webhook WhatsApp/Wablas ke CI4

Pembaruan: 16 Oktober 2026.

Dua endpoint sudah tersedia melalui controller `WablasWebhook` dan layanan `WablasEvents`:

| Path di bawah `api/` | Fungsi |
| --- | --- |
| `wablas_inbox` dan `wablas_inbox.php` | Pesan masuk, kontak customer, metadata, dan auto-reply |
| `wablas_tracking` dan `wablas_tracking.php` | Callback status pesan keluar |

Keduanya menerima POST JSON, mengembalikan JSON, memakai Cache-Control: no-store, dan menolak method lain dengan 405. Endpoint tidak memakai sesi login browser. Semua akses database melalui CI4; tidak ada runtime include PHP dari `pos`.

## Konfigurasi

Tambahkan secret pada `.env` aplikasi CI4:

```dotenv
WABLAS_WEBHOOK_TOKEN = 'isi-secret-webhook-yang-kuat'
```

Provider harus mengirim header `X-Webhook-Token` yang cocok. Parameter query `token` tetap diterima untuk kompatibilitas, dengan prioritas header. **Konfigurasi kosong menolak callback 401**, memperketat sumber yang membuka webhook saat token tidak dikonfigurasi. Token ini bersifat bersama untuk kedua callback dan seluruh perangkat pada aplikasi; pemetaan toko tetap berdasarkan perangkat/pesan.

Atur Device ID pada Pengaturan WhatsApp toko. Halaman tersebut sekarang menampilkan kedua URL callback CI4. Tidak ada perubahan secret pada `.env` pengguna atau konfigurasi provider yang dilakukan otomatis.

## Pesan masuk

Contoh body:

```json
{
  "id": "provider-message-id",
  "deviceId": "device-id-toko",
  "phone": "081234567890",
  "message": "Halo",
  "pushName": "Budi",
  "isGroup": false,
  "isFromMe": false,
  "messageType": "text",
  "timestamp": 1700000000000
}
```

ID pesan, Device ID, dan phone wajib terisi. Timestamp menerima Unix detik/milidetik atau string waktu yang dapat diparse; waktu tidak valid ditolak. Field file/url/mimeType/sender/profileImage serta metadata grup juga disimpan. Body store_id tidak menentukan toko.

Device ID wajib terpetakan ke tepat satu toko aktif: tidak ditemukan menghasilkan 404, ambigu 409. Tidak ada penyimpanan pesan/customer ke toko null atau fallback ke sesi login. Identitas perangkat diperiksa kembali di bawah lock toko.

Penyimpanan customer dan pesan berada dalam transaksi yang sama. Customer dicari berdasarkan toko dan variasi nomor yang dinormalisasi (62/0), tanpa pencarian substring lintas toko. Lebih dari satu customer cocok menghasilkan 409 dan membatalkan transaksi. Customer baru memakai nama pushName (fallback nomor); update foto profil dibatasi ID dan toko. Grup serta echo isFromMe tidak membuat/memperbarui customer.

Callback berulang dikenali melalui toko, perangkat, external_id, dan direction di bawah lock toko, lalu menghasilkan:

```json
{"status":"ok","external_id":"provider-message-id","duplicate":true}
```

Callback pertama mempertahankan respons `status: ok` dan external_id. Echo pesan keluar tidak menambah baris lagi bila external_id yang sama sudah dicatat untuk toko/perangkat, termasuk baris keluar lama dengan device_id null.

## Auto-reply

Aturan aktif tetap memakai tabel `whatsapp_autoreplies`, dengan mode contains/exact/starts/ends/regex. Regex tidak valid dianggap tidak cocok. Rule disaring berdasarkan toko dan perangkat, dengan prioritas perangkat spesifik, toko spesifik, lalu rule global. Rule global yang memang tidak memiliki toko/perangkat tetap berlaku; rule milik toko lain tidak ikut dipakai.

Hanya pesan text masuk non-grup dan bukan isFromMe yang dievaluasi. Rule pertama yang cocok dipakai. Placeholder [salam] dan [nama] tetap didukung; nama berasal dari customer toko tersebut atau pushName.

Respons dan penanda is_autoreply disimpan **sebelum commit**, lalu pengiriman dilakukan sesudah commit melalui helper pengiriman WhatsApp yang sudah ada. Callback duplikat tidak mengirim respons lagi. Pengujian menggunakan callback pengirim tiruan; tidak ada pesan nyata yang dikirim.

Semantiknya adalah **maksimal satu percobaan pengiriman per callback yang diterima**, bukan jaminan exactly-once delivery. Jika proses berhenti setelah commit sebelum mengirim, atau provider gagal, callback ulang tetap tidak mengirim lagi. Tidak ada retry/outbox pemulihan auto-reply pada tahap ini. Ini mempertahankan sifat best effort sumber sambil mencegah loop dan balasan ganda; is_autoreply menandai rule yang diklaim, bukan bukti pesan sudah diterima tujuan.

Halaman admin pengelolaan rule dan worker antrean WhatsApp belum dinyatakan selesai oleh tahap webhook ini. Rule yang sudah ada di database tetap dapat digunakan.

## Tracking pesan keluar

Contoh:

```json
{"id":"provider-message-id","deviceId":"device-id-toko","status":"delivered","note":"Delivered"}
```

ID dan status wajib terisi. Device ID direkomendasikan untuk membedakan ID yang sama pada perangkat berbeda. Bila tidak dikirim, callback hanya diterima jika external_id menunjuk tepat satu pesan keluar pada toko aktif. Tidak ditemukan menghasilkan 404; lebih dari satu kandidat menghasilkan 409. Tracking tidak memperbarui pesan masuk.

Baris pesan dikunci dan diperiksa ulang berdasarkan ID internal, toko, external_id, direction, serta perangkat bila dikirim. Pembaruan memakai ID internal dan toko, bukan UPDATE global berdasarkan external_id dengan LIMIT 1.

Callback status/note yang sama menghasilkan HTTP 200 dengan updated: 0, sehingga callback ulang tidak dianggap gagal seperti rowCount nol pada sumber. Urutan status umum pending/queued/processing → sent → delivered → read tidak diturunkan oleh event lama. failed/error/cancelled tidak menimpa delivered/read. Status lain tetap dapat disimpan; tidak ada aturan urutan lengkap untuk semua status provider. Note yang tidak dikirim mempertahankan nilai sebelumnya.

## Schema dan pencatatan

`WablasSchema` menyiapkan tabel/kolom kompatibilitas sebelum transaksi, termasuk metadata pesan/grup serta tabel rule jika belum ada. Tabel stores/customers aplikasi tetap menjadi prasyarat; ini bukan installer database penuh. Jika direction baru ditambahkan pada schema lama, defaultnya out untuk riwayat lama; arah pesan yang tidak pernah direkam tidak dapat direkonstruksi.

Pencatatan menggunakan API Logs CI4 dengan store_id hasil resolusi callback, bukan sesi browser. Token dan field kredensial dalam payload disamarkan secara rekursif; header/query token tidak disalin. File raw log publik `pos/logs/wablas_*.log` tidak dipakai lagi. Isi pesan/metadata tetap ada pada database dan API log untuk operasional.

## Verifikasi

- PHPUnit seluruh aplikasi: **230 tes, 2.002 assertion**, lulus.
- Termasuk **23 tes Wablas** untuk route/method, secret header/query, payload rusak, pemetaan toko/perangkat, customer, callback duplikat, grup/echo, rollback, lima mode rule, pengiriman sesudah commit dengan callback tiruan, kegagalan auto-reply, tracking berulang/terlambat/ambigu, serta penyamaran kredensial.
- Lint **313 file PHP**: tanpa kesalahan sintaks.
- Tes memakai koneksi database tiruan dan pengirim tiruan; tidak mengubah data produksi atau menghubungi Wablas/WhatsApp. Uji HTTP provider, konkurensi MySQL nyata, dan pengiriman pesan staging masih diperlukan.

## Berikutnya

Sembilan dari dua belas file API sumber mempunyai route CI4. Yang tersisa: `whatsapp_chat_api`, `crm_chat`, dan `qris`. Berikutnya adalah API percakapan WhatsApp/CRM, kemudian QRIS dan audit worker/admin yang belum dipindahkan.
