# Porting webhook sinkronisasi Jaknot ke CI4

Pembaruan: 16 Oktober 2026.

Endpoint `POST api/webhook_jaknot_sync` beserta alias `.php` sudah dipindahkan ke controller `JaknotWebhook` dan layanan `JaknotSync`. Implementasi menggunakan request/response JSON, koneksi, transaksi, dan logger CI4 tanpa menjalankan PHP dari folder `pos`. Method selain POST menghasilkan 405 dengan header Allow: POST. Response memakai Cache-Control: no-store.

## Konfigurasi dan kontrak

Generate token outlet melalui **Pengaturan > Pengaturan Jaknot**. Halaman pengaturan yang sudah diporting menampilkan URL CI4 dan mendukung pembuatan ulang/pencabutan token. Kolom outlets.jaknot_token disiapkan melalui pengaturan tersebut; endpoint penerima tidak menjalankan migrasi schema produk/outlet.

Kirim token **hanya melalui header**:

```http
POST /api/webhook_jaknot_sync.php
Content-Type: application/json
X-Jaknot-Token: token-outlet-dari-pengaturan
```

Path di atas relatif terhadap base URL aplikasi CI4.

```json
{
  "marketplace_id": 1,
  "marketplace_name": "Tokopedia",
  "branch_id": "jz23mo",
  "branch_name": "Jaknot Benhil",
  "synced_at": "2026-10-16 12:00:00",
  "items": [
    {"sku_id": "HWWA0LBK", "price": 8400.25, "stock": 5}
  ]
}
```

- Token menentukan outlet dan toko; store_id/outlet_id/branch_id/marketplace_id dari payload **tidak digunakan untuk memilih target**. Sesi browser serta token dalam body/query tidak menggantikan header.
- Outlet dan toko harus aktif. Token dibandingkan persis, termasuk huruf besar/kecil, dengan hash_equals setelah lookup database. Token yang terpetakan ke lebih dari satu outlet ditolak.
- Identitas toko/outlet dan token diperiksa ulang di dalam transaksi setelah penguncian. Token yang sudah dicabut/diganti tidak dapat digunakan untuk melanjutkan pembaruan setelah pemeriksaan ulang.
- Payload harus object JSON dengan items berupa array berisi 1 sampai 1000 item. Metadata lain tetap boleh dikirim dan dicatat sebagai informasi.

## Pembaruan data

Untuk setiap item, sku_id dicocokkan dengan products.sku atau products.barcode dalam toko dari token. Schema lama tanpa kolom sku memakai barcode saja. Produk inactive tetap dapat dicocokkan, sesuai sumber.

Produk yang cocok menerima **stok absolut** pada products.stock dan **harga modal** pada products.purchase_price. Field price_original, price_discount, dan harga jual produk tidak digunakan sebagai harga modal. Stok dibulatkan ke integer terdekat seperti sumber; stok negatif dan nilai nol diperbolehkan. Harga modal harus nonnegatif dan dibulatkan ke dua desimal. Nilai dibatasi kapasitas INT signed dan DECIMAL(15,2).

Ini mempertahankan model aplikasi lama: token memang per outlet, tetapi stok yang diubah berada pada record produk **per toko**, bukan inventori terpisah per outlet. Outlet berbeda dalam toko yang sama dapat mengubah produk yang sama. Ini merupakan penetapan snapshot, sehingga aturan pause pergerakan stok transaksi tidak membekukan pembaruan Jaknot ini. Produk bundle juga tidak diekspansi; stok tersimpan dan harga modal record tetap diperbarui sesuai sumber, bukan stok efektif komponennya.

Perubahan dari sumber untuk mencegah kehilangan data:

- SKU yang tidak ditemukan dilewati dan dilaporkan pada unmatched_skus, tanpa membatalkan item lain yang cocok.
- SKU/barcode yang cocok dengan lebih dari satu produk dilewati dan dilaporkan pada ambiguous_skus. API tidak memakai LIMIT 1 untuk memilih produk secara acak.
- Setiap item wajib menyediakan stock dan price yang valid. Field hilang/null, tipe salah, nominal negatif, dan nilai di luar batas tidak menimpa data menjadi nol; item dilewati dengan penjelasan pada invalid_items. Pengiriman eksplisit angka nol tetap valid.
- Satu batch memakai transaksi database. Jika pembaruan produk kedua atau berikutnya gagal, seluruh pembaruan batch dibatalkan. Item invalid/tidak ditemukan merupakan skip yang disengaja; kegagalan database menghasilkan 500.

Pengiriman ulang snapshot menetapkan nilai yang sama, bukan menambah stok. Jika satu produk muncul beberapa kali melalui SKU/barcode dalam satu batch, item terakhir menentukan nilai akhirnya, sesuai urutan payload. updated menghitung item cocok yang diproses, termasuk nilai yang sebenarnya sama dengan sebelumnya.

synced_at tetap metadata. Tidak ada pengurutan versi/timestamp: snapshot lama yang datang belakangan dapat menimpa snapshot baru. Endpoint ini tidak membuat histori mutasi stok, batch FEFO, atau transaksi pembelian. Sesuai sumber, penerima ini tidak memicu webhook stok outbound atau antrean sinkronisasi Shopee.

## Respons dan pencatatan

```json
{"status":"ok","updated":1,"skipped":0,"unmatched_skus":[]}
```

Bila diperlukan, respons juga memuat ambiguous_skus serta invalid_items (index item mulai nol, SKU bila tersedia, dan reason). HTTP 200 dapat berisi item yang dilewati; caller harus memeriksa skipped dan rinciannya. Payload tidak valid menghasilkan 400, token ditolak 401, kesalahan database 500 tanpa rincian SQL kepada caller.

Audit ringkasan menggunakan konteks toko dari token, bukan toko sesi browser. API log menyamarkan token dan field kredensial pada payload secara rekursif, tidak menyalin header token, dan tidak mencatat teks exception database yang mungkin berisi kredensial. Audit/log bersifat best effort setelah transaksi; kegagalan pencatatan tidak menggagalkan pembaruan yang sudah commit.

## Verifikasi

- PHPUnit seluruh aplikasi: **207 tes, 1.907 assertion**, lulus.
- Termasuk **19 tes Jaknot**: alias/method, token header dan perbandingan persis, token ambigu/dicabut, isolasi toko/outlet, snapshot absolut, SKU tidak ditemukan/ambigu, nilai nol/desimal/negatif, input tidak lengkap, rollback kegagalan pembaruan kedua, pengiriman ulang, dan penyamaran kredensial.
- Lint **309 file PHP**: tanpa kesalahan sintaks.
- Pengujian memakai koneksi database tiruan. Tidak mengubah data produksi atau menghubungi Jaknot, marketplace, maupun WhatsApp. Integrasi HTTP, konkurensi MySQL nyata, dan payload Jaknot staging masih perlu diverifikasi.

## Berikutnya

Tujuh dari dua belas file API sumber sekarang mempunyai route CI4. Yang tersisa: wablas_inbox, wablas_tracking, whatsapp_chat_api, crm_chat, dan qris. Prioritas berikutnya adalah webhook WhatsApp masuk dan pelacakan status Wablas.
