# Porting webhook pesanan Shopee ke CI4

Pembaruan: 16 Oktober 2026.

Endpoint `POST api/webhook_shopee_order` dan alias `.php` sekarang memakai controller `ShopeeWebhook` serta layanan `ShopeeOrderWebhook`. Method lain mengembalikan JSON 405. Endpoint tidak memakai sesi login/CSRF browser; marketplace dan toko ditentukan melalui konfigurasi Shop ID dan secret payload.

## Konfigurasi dan payload

Di master marketplace, isi Shop ID, platform Shopee, status aktif, dan **Secret Key Webhook Pesanan (Masuk)**. Toko juga harus aktif. Petunjuk pada form marketplace sudah menampilkan URL CI4 yang sesuai. Shop ID yang ambigu pada beberapa marketplace aktif ditolak 409, bukan dipilih secara acak.

**Secret Key wajib terisi dan cocok**, diverifikasi dengan `hash_equals`. Ini memperketat aplikasi lama yang melewati verifikasi ketika secret konfigurasi kosong. Tidak ada mode webhook tanpa secret. Secret, API key, token, dan password dalam payload disamarkan pada API log.

Ini adalah kontrak JSON gateway aplikasi lama, bukan implementasi verifikasi signature Shopee OpenAPI secara langsung:

```json
{
  "shop_id": "SHOP-1",
  "secret_key": "secret-dari-konfigurasi-marketplace",
  "order_sn": "ORDER-1",
  "new_status": "READY_TO_SHIP",
  "order": {
    "recipient_name": "Pembeli",
    "recipient_phone": "081234567890",
    "recipient_address": "Alamat pengiriman",
    "tracking_number": "RESI-1",
    "payment_method": "Shopee"
  },
  "items": [
    {"model_sku": "SKU-1", "model_quantity": 2, "model_discounted_price": 10000}
  ]
}
```

Field lain dari sumber tetap dipetakan, termasuk item_sku sebagai fallback model_sku, harga original, variasi, berat, informasi pembeli, subtotal_pesanan, diskon, total_potongan, dan total_setelah_potongan. Maksimal 500 item; jenis data, panjang kolom, quantity positif, dan nominal nonnegatif diperiksa. Invalid payload menghasilkan 400, secret ditolak 401, Shop ID tidak ditemukan 404, referensi lintas toko/total tidak valid 422, kegagalan database 500 tanpa mengirim detail SQL kepada caller.

## Writeback dan transaksi POS

- Tabel `shopee_orders` dan `shopee_orders_audit` tetap sama dengan tools impor Shopee. Writeback menggunakan kunci store_id/order_sn/sku_ref dan mencatat perubahan nilai kolom, dengan changed_by null untuk webhook otomatis.
- Event tanpa item memperbarui setiap baris item yang sudah tersimpan beserta auditnya. Data item yang tidak dikirim tidak dikosongkan. Jika belum ada baris sama sekali, satu baris order-level dapat disimpan dengan SKU kosong.
- Toko dikunci sebelum pengecekan pesanan. Penjualan dicocokkan melalui order_number atau invoice_number dalam toko yang sama, lalu dikunci. Request ulang dari layanan ini tidak mengulang insert sale_details atau pemotongan stok.
- Writeback, audit perubahan, header/detail penjualan, dan mutasi stok berada dalam transaksi database yang sama. Kegagalan detail atau audit membatalkan semuanya. Schema kompatibilitas disiapkan sebelum transaksi; tidak ada ALTER TABLE di dalam transaksi bisnis.
- Outlet dan reseller yang dikonfigurasi harus milik toko. Marketplace inhouse memakai harga payload; marketplace reseller memakai harga khusus sesuai tier 1–3, fallback ke harga Shopee jika harga khusus belum diisi. Total reseller dihitung dari harga reseller, sedangkan breakdown marketplace disimpan terpisah.
- Semua item harus cocok secara unik melalui SKU/barcode sebelum penjualan dibuat. Jika satu saja tidak cocok, ambigu, kosong, atau bundle, writeback tetap disimpan tetapi respons sale.action adalah skipped dan unmatched_skus menjelaskan masalahnya. Ini memperketat sumber yang dapat membuat penjualan hanya untuk sebagian item.
- Tidak ada proses otomatis untuk melengkapi penjualan skipped. Perbaiki mapping produk, kemudian kirim ulang **payload lengkap dengan seluruh item**. Event status-only berikutnya tidak menciptakan item yang tidak pernah dikirim.
- Untuk sale yang sudah ada, event berikutnya hanya memperbarui status, resi, dan breakdown marketplace. Kuantitas/harga detail dan nominal transaksi yang sudah dibuat tidak direvisi otomatis, sesuai sumber.

## Pembatalan dan stok

Stok kurang tetap menerima pesanan dan boleh menjadi minus. Produk pause tetap dicatat sebagai detail, tanpa pengurangan stok.

Kolom nullable `sale_details.shopee_stock_quantity` mencatat jumlah stok yang benar-benar dipotong oleh webhook baru. Nilainya nol untuk produk pause dan pesanan yang sejak event pertama sudah dibatalkan. Pembatalan tidak mengkredit stok yang tidak pernah dikurangi; marker dikonsumsi menjadi nol setelah pembatalan diproses. Helper pembatalan manual CI4 juga membaca marker ini agar pembatalan lewat UI tidak menggandakan kredit stok webhook.

Pembatalan diperlakukan terminal: event nonbatal yang terlambat tidak membuka kembali pesanan atau mengulangi pemotongan/pengembalian stok. Ini juga berlaku bila pembatalan datang tanpa item sebelum sale lokal dibuat. Pendeteksian status batal mengikuti sumber: teks status yang mengandung `cancel`. Tidak ada urutan lengkap status nonbatal atau perbandingan timestamp event; status nonbatal masih mengikuti urutan kedatangan.

Untuk detail lama dengan marker null, pengembalian memakai quantity sebagai fallback, mengikuti perilaku lama. Histori apakah produk dahulu di-pause tidak dapat direkonstruksi. Produk yang sedang pause ketika pembatalan datang tetap membekukan stok; marker dikonsumsi dan tidak dijadwalkan untuk dikreditkan otomatis ketika pause dilepas.

Sinkronisasi antrean Shopee, webhook stok outbound, notifikasi stok rendah, dan audit ringkasan berjalan best effort setelah commit. Kegagalan pengiriman tidak mengubah pesanan yang sudah tersimpan menjadi respons gagal. Tidak ada scheduler baru yang dipasang.

## Respons berhasil

```json
{"status":"ok","sale":{"action":"created","sale_id":123,"invoice_number":"ORDER-1","grand_total":20000,"items_matched":1,"unmatched_skus":[]}}
```

Event untuk sale yang sudah ada menghasilkan action status_updated. Payload tanpa item atau mapping yang belum lengkap dapat menghasilkan action skipped dengan reason. Caller harus memeriksa hasil sale, bukan hanya HTTP 200.

## Verifikasi dan batasan

- PHPUnit seluruh aplikasi: **188 tes, 1.840 assertion**, lulus; termasuk **26 tes webhook Shopee** untuk routing/method, secret, Shop ID ambigu, rollback, retry, stok pause, pembatalan, event terlambat, audit status-only, harga reseller, outlet/reseller lintas toko, serta helper pembatalan manual.
- Lint **306 file PHP**: tanpa kesalahan sintaks.
- Tes memakai database tiruan; tidak membuat transaksi nyata atau menghubungi layanan Shopee/WhatsApp/webhook. Uji konkurensi dua koneksi MySQL, payload gateway nyata, migrasi schema staging, dan pengiriman HTTP eksternal masih perlu dilakukan.
- Bundle, alokasi batch FEFO, deposit reseller, dan penyuntingan otomatis detail sale existing tidak ditambahkan oleh webhook ini. Kunci idempotensi tetap per toko/order, sesuai sumber; tidak ada unique constraint global untuk semua writer aplikasi lama.
- Perubahan pada helper pembatalan membaca marker baru jika kolom tersedia, dan memakai alur lama ketika kolom belum tersedia. Transaksi milik pemanggil tetap dihormati.

## Berikutnya

Enam dari dua belas file API sumber sekarang mempunyai route CI4. Berikutnya: `webhook_jaknot_sync`, lalu `wablas_inbox`, `wablas_tracking`, `whatsapp_chat_api`, `crm_chat`, dan `qris`.
