# Audit perbandingan API dan webhook: pos → pos_kiosq_id_ci

Tanggal: 17 Oktober 2026. Pemeriksaan kode lokal, route CI, helper HTTP, worker, dan tes otomatis. Tidak mengirim request ke provider, menjalankan cron, membaca database hosting, atau mengubah konfigurasi integrasi.

**Kesimpulan: semua 12 endpoint dalam folder `pos/api` mempunyai padanan CI, tetapi belum identik kontrak/perilakunya.** Alias root `tracking.php` juga tersedia, dengan perubahan respons yang perlu diperhatikan. Status porting file selesai tidak berarti kompatibilitas seluruh caller sudah dibuktikan.

`BASE_LAMA` = base URL aplikasi `pos`; `BASE_CI` = base URL publik aplikasi CI. Semua path di bawah relatif terhadap masing-masing base URL. Pada CI, path dalam tabel tersedia dengan **dan tanpa `.php`**. Method yang disebut adalah method operasional handler, bukan sekadar daftar method yang terdaftar di router.

## 1. API/webhook masuk utama (13 path)

| Path lama → CI (path dipertahankan) | Method CI / akses | Fungsi dan perbandingan |
| --- | --- | --- |
| `api/api_create_sale.php` | POST JSON/form; `X-Api-Key` atau `api_key` body | Menyimpan penjualan. Struktur respons pokok dipertahankan, tetapi bundle ditolak 422, referensi asing/SKU ambigu ditolak, validasi nominal/tanggal diperketat, nominal memakai dua desimal. CI menambah enqueue sinkron stok Shopee setelah commit. |
| `api/api_check_stock.php` | GET/POST; `X-Api-Key`, `api_key` body/query | Satu SKU, array SKU, atau semua produk. Format pokok `success/data/not_found/total` dipertahankan. Lookup diperluas dari barcode menjadi SKU atau barcode; produk ambigu tidak dipilih acak. |
| `api/api_get_invoice.php` | GET/POST; API key toko | Lookup `invoice_number`/`order_number`, dibatasi toko dari key. Kontrak pokok dipertahankan; input/error lebih ketat dan exception SQL tidak dikirim mentah. |
| `api/webhook_shopee_order.php` | POST JSON; `secret_key` body + `shop_id` | Menerima envelope gateway Shopee (`order_sn`, `new_status`, `order`, `items`). **Secret kosong dahulu melewati autentikasi; CI menolak 401.** Shop ambigu ditolak 409; toko harus aktif; locking/idempotensi/pembatalan diperketat. Ini bukan penerima signature Shopee OpenAPI langsung. |
| `api/webhook_jaknot_sync.php` | POST JSON; `X-Jaknot-Token` | Snapshot `items[].sku_id/stock/price`, mengubah stok absolut dan harga modal. Token tetap menentukan outlet/toko. CI memeriksa toko aktif/ambiguitas; item tanpa stock/price kini dilewati, bukan menimpa menjadi nol. SKU ambigu dilaporkan. |
| `api/wablas_inbox.php` | POST JSON; `X-Webhook-Token` atau query `token` | Pesan masuk, customer, auto-reply. **WABLAS_WEBHOOK_TOKEN kosong kini ditolak 401**; device harus menunjuk satu toko aktif. Callback duplikat tidak menyimpan/membalas dua kali. |
| `api/wablas_tracking.php` | POST JSON; token webhook yang sama | Pelacakan status pesan keluar. ID pesan harus unik dalam scope toko/device; ID ambigu ditolak 409. Event sama menghasilkan 200/updated 0; event terlambat tidak menurunkan delivered/read. Lebih ketat dari UPDATE by external_id lama. |
| `tracking.php` | POST JSON; token webhook yang sama | Dahulu handler terpisah yang dapat memperbarui semua baris external_id dan opsional phone. CI diarahkan ke handler tracking Wablas yang scoped. **Respons lama `success/updated_rows`, sekarang `status/updated`; phone tidak lagi diperbarui dari callback ini.** Caller lama perlu penyesuaian. |
| `api/whatsapp_chat_api.php` | GET membaca; POST send/mark_read; sesi toko + CSRF pada POST | `contacts/messages/new_messages/send/mark_read`. Pengiriman lewat GET ditolak; origin harus cocok tepat jika dikirim. GET messages tidak otomatis menandai dibaca; gunakan POST mark_read. Riwayat dibatasi dan akses lebih ketat per toko/device. Hasil provider mentah tidak lagi diteruskan. Grup ditolak 422. |
| `api/crm_chat.php` | GET/POST seperti chat; sesi + CSRF | Kontrak kontak CRM/customer dipertahankan secara pokok; query dan respons send dibatasi seperti API WhatsApp. Caller lama tanpa CSRF atau yang membaca result provider mentah tidak identik. |
| `api/qris.php` | GET/POST, sesi toko; input `amount` | Menghasilkan payload QRIS lokal, **bukan panggilan ke bank/payment gateway**. Respons sukses pokok tetap; CI mendukung input POST, menolak nominal pecahan/rusak, memeriksa CRC/payload dan toko aktif, serta memakai HTTP error yang lebih tepat. |
| `api/notifications.php` | GET daftar; POST mark_read/mark_all_read; sesi login | Format `success/count/unread_count/notifications` secara pokok dipertahankan. Mutasi POST belum memakai CSRF khusus. Ada masalah query booking terhadap skema baru; lihat temuan di bawah. |
| `api/switch-outlet.php` | GET/POST, sesi login | Input `outlet_id`; hasil `ok/outlet_id` dipertahankan. Sama seperti sumber, GET masih dapat mengubah outlet; belum memakai CSRF. Ini API internal browser, bukan API key eksternal. |

Tiga Store API menyediakan OPTIONS 204/CORS seperti sumber. Key menentukan toko; bukan `store_id` dari body dan bukan sesi. Shopee/Jaknot/Wablas bukan API browser ber-CORS dan tidak memakai CSRF login; masing-masing memakai secret/token sendiri.

Contoh kontrak utama yang perlu dipertahankan oleh pengirim:

```text
POST BASE_CI/api/api_create_sale.php
X-Api-Key: <key-toko>
{order_number, customer_name, payment_method, items:[{sku, quantity, price?, discount?}]}

POST BASE_CI/api/webhook_shopee_order.php
{shop_id, secret_key, order_sn, new_status, order:{...}, items:[...]}

POST BASE_CI/api/webhook_jaknot_sync.php
X-Jaknot-Token: <token-outlet>
{items:[{sku_id, stock, price}], ...metadata}

POST BASE_CI/api/wablas_inbox.php
X-Webhook-Token: <WABLAS_WEBHOOK_TOKEN>
{id, deviceId, phone, message, ...metadata}

POST BASE_CI/api/wablas_tracking.php
X-Webhook-Token: <WABLAS_WEBHOOK_TOKEN>
{id, status, deviceId?, note?}
```

## 2. Endpoint JSON, sinkronisasi, dan worker pendukung

| Path CI (juga ada `.php`) | Method / akses | Dibanding sumber |
| --- | --- | --- |
| `cart.php` | GET get; POST add/update/remove/clear + CSRF | Mutasi GET ditolak; harga/stok di-refresh dari server, token baru disertakan dalam JSON. |
| `booking_api.php` | POST + CSRF | GET yang dahulu dapat membuat booking ditolak. Harga/deposit/capacity dari server; cek dua tabel reservasi, hold pending 15 menit, pengulangan terbatas booking milik sesi. |
| `order_meja.php` | GET menu; POST JSON-response + CSRF dan token meja | Identitas toko/outlet dari token. CI menambah request_key/idempotensi, transaksi atomik; stok menunggu konfirmasi staff. |
| `ajax/product_lookup.php` | GET q, sesi login | Lookup barcode/nama, format array pokok sama. Filter auth CI dapat mengalihkan caller tanpa sesi ke login (bukan selalu JSON). |
| `ajax/customer_lookup.php` | POST phone, sesi login | Variasi nomor 62/0 dan scope toko tetap. Tidak terbuka untuk checkout anonim. Filter auth dapat mengalihkan caller tanpa sesi. |
| `ajax/customer_search.php` | GET q, sesi login | Customer toko, hasil array; filter auth seperti di atas. |
| `ajax/supplier_search.php` | GET q, sesi login | Supplier toko, hasil array; filter auth seperti di atas. |
| `tools/sync_export.php` | GET/POST route; `X-Sync-Key`/query key atau sesi system super admin | Menghasilkan SQL, bukan JSON. Key wajib dikonfigurasi. Tujuan sinkronisasi dari ToolsSyncImport tetap path ini. |
| `tools/cron_shopee_stock_sync.php` | GET/POST, `X-Cron-Key`/query key | Proses antrean stok Shopee. Bukan file PHP yang dieksekusi langsung di CI; tersedia command `php spark tools:shopee-stock-sync`. |
| `cron/process_whatsapp_queue.php` | GET/POST, CRON_KEY | Worker antrean WA; klaim pekerjaan/locking dan penanganan hasil tak pasti berbeda dari sumber. |
| `cron/send_scheduled_blasts.php` | GET/POST, CRON_KEY | Enqueue blast terjadwal; locking/marker mencegah enqueue ganda. |
| `cron/charge_daily_saldo.php` | GET/POST, CRON_KEY | Worker saldo harian; bukan webhook eksternal masuk dan bukan otomatis API pembayaran bank. |
| `cron/cek_status.php` | GET/POST, CRON_KEY | Memicu GET report Wablas berdasarkan konfigurasi toko. |
| `cron/cek_status_realtime.php` | GET/POST, CRON_KEY | Memicu GET report realtime Wablas berdasarkan konfigurasi toko. |

CLI worker berada dalam `app/Commands`; daftar/nama dan opsi selengkapnya di `PORTING_CRON.md` dan `PORTING_TOOLS.md`. Jangan menjalankan `php /.../cron/nama.php` untuk deployment CI karena path tersebut adalah route, bukan script fisik.

Halaman `master/marketplaces.php`, `pengaturan/webhook-marketplace.php`, `pengaturan/coa-akuntansi.php`, dan `tools/sync_import.php` juga menerima aksi UI yang memicu request keluar. Itu endpoint admin/session, bukan API key publik. Halaman kasir memiliki route JSON native tambahan (`kasir/create-customer`, `kasir/send-transfer-wa`, `kasir/process-sale`, `kasir/open-session`, `kasir/close-session`); ini pemisahan controller alur kasir, bukan API baru untuk integrator eksternal.

## 3. API/webhook keluar (server → layanan lain)

URL dinamis di sini merupakan **nama kolom/config**, bukan URL aktual hosting yang sudah dibaca/dibuktikan. Tidak ada satu URL Shopee/stock webhook universal yang boleh diasumsikan dari kode.

| Tujuan | Method / otentikasi / payload | Sumber → CI |
| --- | --- | --- |
| `webhook_configs.url` (stock_update toko) | POST JSON; `X-Webhook-Event`, `X-Delivery-Time`, opsional `X-Webhook-Signature: sha256=<HMAC>`; `event/store_id/timestamp/products:[{sku,product_name,stock}]` | Format dasar dan signature sama. SKU tetap barcode, bukan otomatis kolom SKU. Trigger per alur harus dibandingkan; create-sale CI hanya mengirim produk yang stoknya bergerak. |
| `marketplaces.webhook_url` (stok Shopee tunggal) | POST JSON + X-API-KEY; `{shop_id,sku,stock}` | Format sama; stok negatif dikirim sebagai 0. |
| `marketplaces.webhook_url` (stok Shopee bulk/worker) | POST JSON + X-API-KEY; `{shop_id,items:[{sku,stock}]}` | Format sama. Worker CI mengklaim ID antrean tertentu dan mempertahankan enqueue baru saat request berjalan. |
| `marketplaces.price_webhook_url` (harga tunggal/bulk) | POST JSON + X-API-KEY; `{shop_id,sku,price}` atau `{shop_id,items:[{sku,price}]}` | Format dasar sama; target URL tetap konfigurasi marketplace. |
| `marketplaces.products_webhook_url` (import produk Shopee) | GET + X-API-KEY; query `shop_id/from/limit` | Path/parameter/format pemanggilan dasar sama. |
| `image_url` / `images[0]` hasil import marketplace | GET file gambar; tanpa header API key marketplace | Download gambar produk tetap ada; bukan webhook. |
| `WA_API_URL` / `stores.whatsapp_url` | POST form `phone/message/flag`; Authorization `<token>.<secret>` | Helper umum dipertahankan. Default URL global tetap `https://pati.wablas.com/api/send-message`; override/toko/global seperti sumber. |
| `WA_BOT_URL` / `stores.wa_bot_url` | POST form `token/to_number/message_text`; opsional multipart media_file | Format sama. URL berakhiran `/kirim.php` dinormalisasi menjadi `/kirim`, nomor umumnya lokal 08xxx; grup diteruskan oleh helper yang mendukungnya. Default `https://crm.kiosq.id/kirim`. |
| URL bot batch turunan `/kirim-batch` | POST JSON `{token,messages:[...]}` | Format dasar sama; respons per pesan/hasil tidak pasti ditangani lebih ketat pada worker CI. |
| `stores.botq_url` atau `BOTQ_URL` + `/webhook/send/{device}` | POST JSON + X-Api-Key; `{to,message,imageUrl?}` | Helper Bot Q dan pemilihan gateway toko tetap tersedia. |
| Host dari `stores.whatsapp_url` + `/api/report/message` | GET Authorization token.secret; `date/page/perPage/phone` | Lama memakai host pati.wablas.com pada script laporan; CI memakai host konfigurasi toko aktif dan HTTPS, serta memperbarui pesan dalam scope toko. |
| Host dari `stores.whatsapp_url` + `/api/report-realtime` | GET Authorization token.secret; `page/limit/message_id` | Perubahan host/config/scoping sama seperti report biasa. |
| `https://akuntansi.kiosq.id/api/webhook/coa` | GET + X-API-KEY; query `tenant` | Endpoint pull COA sama. Tidak ditemukan pengiriman jurnal penjualan otomatis ke akuntansi pada kedua versi. |
| `<hosting_url>/tools/sync_export.php` | GET; query `key`, opsional `tables` | Format request sinkron database sama; target berasal dari form admin. |

`CheckoutMessenger` merupakan jalur khusus OTP dan pemberitahuan checkout/akun: memakai konfigurasi global Wablas, satu percobaan, timeout, TLS aktif, tanpa log isi OTP/hasil provider. Sumber menggunakan helper dengan fallback dan logging; perilaku ini **berbeda**. `getActiveWaGateway(null)` selalu mengembalikan wablas, sehingga cabang bot/botq di CheckoutMessenger tidak dipilih saat ini. Pemilihan gateway toko tetap bekerja pada helper chat/blast biasa yang menerima store_id; jangan menganggap OTP mengikuti switcher toko.

`Jaknot` pada proyek ini adalah webhook **masuk** untuk snapshot; tidak ditemukan pemanggilan API Jaknot keluar. QRIS juga dibuat lokal, tanpa endpoint konfirmasi bank.

Request dari browser yang juga tetap ada: GET `https://api.qrserver.com/v1/create-qr-code/` untuk gambar QR meja (query size/data), tautan `https://wa.me/...` untuk membuka WhatsApp, dan aset CDN. Itu bukan request webhook server, bukan pula bukti pesan sudah terkirim.

## 4. Perbedaan/temuan yang memerlukan keputusan atau penyesuaian caller

1. **API bundle belum setara.** `StoreSale.php:116` menolak bundle 422, sedangkan sumber dapat menerimanya sebagai item produk biasa. Penolakan menghindari mutasi stok virtual yang salah, tetapi integrator yang mengirim SKU bundle akan gagal. Ekspansi komponen bundle via API belum tersedia.
2. **Root tracking berubah kontrak.** Sumber `pos/tracking.php:59` mengembalikan `success/updated_rows`; handler CI `WablasEvents.php:103/107` mengembalikan `status/updated`, tidak memperbarui phone, dan membatasi satu pesan keluar. Bila caller memeriksa field success, harus disesuaikan atau dibuat adapter kompatibilitas.
3. **Secret kosong sekarang gagal.** Shopee dan Wablas wajib mempunyai secret dan pengirim harus mengirimkannya. Route yang tersedia tidak cukup membuktikan webhook hosting akan diterima.
4. **Chat lama tidak dapat digunakan tanpa penyesuaian.** POST wajib CSRF; GET tidak mengirim/mark_read; group send ditolak; result provider dan cakupan histori berubah. Chat tidak mempunyai idempotency key send; retry jaringan tidak membuktikan pesan belum terkirim.
5. **Pemicu keluar create sale berbeda.** CI menambahkan enqueue Shopee (`StoreSale.php:157`); sumber `api_create_sale.php` tidak memanggil enqueue tersebut. Webhook stok CI memakai daftar produk yang benar-benar bergerak, sedangkan sumber memakai semua prepared items, termasuk produk pause. Payload event pokok sama, jumlah/pemicu event tidak identik.
6. **Notifikasi booking tidak cocok dengan skema booking CI.** `Api.php:240–242` masih membaca `br.customer_name` dan `br.store_id`, tetapi `RentalAvailability::schema()` tidak membuat kedua kolom tersebut; booking dibuat lewat rental_id. Pada skema ini query gagal dan ditelan catch, sehingga endpoint sukses tetapi notifikasi booking hilang. Query serupa ada pada sumber: ini temuan kompatibilitas skema/warisan, bukan bukti route belum dibuat. Perlu JOIN rental_products untuk scope toko dan sumber nama/fallback yang benar.
7. **Perilaku pengiriman/worker tidak identik.** OTP memakai satu percobaan global tanpa fallback; report memakai konfigurasi toko; worker WA menghindari retry otomatis pada hasil kirim yang tidak pasti. Tidak boleh menyamakan jumlah request/percobaan hanya dari kesamaan payload.
8. **CSRF internal belum seragam.** Chat/cart/booking/QR sudah memeriksa token, sedangkan switch-outlet masih menerima mutasi GET dan notifications POST tidak mempunyai CSRF khusus. Ini mempertahankan pola lama pada dua endpoint internal; bukan kesetaraan dengan pola keamanan endpoint CI yang baru.

## 5. Yang belum diverifikasi pada hosting

- URL tujuan aktual dan API key/secret di `.env` serta tabel stores/marketplaces/outlets/webhook_configs/accounting_settings.
- URL callback yang terdaftar pada Shopee gateway, Jaknot, Wablas, Bot Q, dan scheduler hosting.
- Header yang benar-benar dikirim provider, status HTTP, timeout, format respons nyata, serta dukungan bulk pada gateway marketplace.
- Konektivitas/TLS, hak GET_LOCK MySQL, serta konkurensi callback/worker pada database nyata.

Tidak ada perubahan handler aplikasi dalam audit ini. Laporan membedakan kompatibilitas format dasar, perubahan yang disengaja, dan temuan yang perlu diperbaiki; bukan sertifikasi bahwa semua integrasi hosting telah bekerja sama persis.

## 6. Verifikasi audit

Perintah yang dijalankan:

```sh
php vendor/bin/phpunit --no-coverage --filter 'StoreApiPortTest|ShopeeWebhookPortTest|JaknotWebhookPortTest|WablasWebhookPortTest|ChatApiPortTest|QrisApiPortTest|CronPortTest|FinalPublicPortTest'
```

Hasil: **198 tes / 1.097 assertion lulus**. Tes memakai database/pengirim tiruan; tidak mengeksekusi request provider. Pemeriksaan route seluruh entry point termasuk di FinalPublicPortTest. Hasil tes memvalidasi perilaku CI yang diuji, termasuk beberapa perubahan kontrak tersebut; bukan tes diferensial HTTP otomatis yang mengirim payload identik ke aplikasi lama dan baru.

Rujukan kode utama:

- Masuk: `app/Config/Routes.php`, `app/Controllers/StoreApi.php`, `ShopeeWebhook.php`, `JaknotWebhook.php`, `WablasWebhook.php`, `ChatApi.php`, `QrisApi.php`, `Api.php`.
- Efek transaksi: `app/Libraries/StoreSale.php`, `ShopeeOrderWebhook.php`, `JaknotSync.php`, `WablasEvents.php`, `ChatConversations.php`.
- Keluar: `app/Helpers/pos_helper.php`, `marketplace_helper.php`, `stock_helper.php`, `app/Libraries/CheckoutMessenger.php`, `WablasReportWorker.php`, `ShopeeStockSync.php`, `WhatsappQueueWorker.php`.
- Pemicu UI: `app/Controllers/Marketplaces.php`, `SettingsWebhookMarketplace.php`, `SettingsCoaAkuntansi.php`, `ToolsSyncImport.php`.
- Pembanding: `pos/api/*.php`, `pos/tracking.php`, `pos/config/database.php`, `pos/includes/webhook.php`, halaman marketplace/pengaturan/sync, dan `pos/cron/*.php`.
