Project baru, di luar unit bisnis biasa. Client: perusahaan jam tangan yang ingin melihat perilaku konsumen di toko/booth via kamera. Fokus saat ini bukan gamifikasi, tapi: mengukur performance kasir/toko dengan meng-korelasikan durasi/jenis interaksi (dari video) dengan hasil transaksi aktual (dari POS) — closing rate dan nominal transaksi per konsumen. Skema gamifikasi (poin/leaderboard) ditunda, lihat §11.
Perusahaan ingin tahu, per sesi kunjungan (bukan per identitas orang — lihat catatan di bawah):
| Metrik | Definisi | Sumber |
|---|---|---|
| Dwell time | Berapa lama dia berada di area toko/booth | Person tracking (masuk → keluar frame) |
| Interaction time | Berapa lama dia berinteraksi langsung — pegang jam, coba, ngobrol dengan SPG/SPB | Deteksi interaksi objek/tangan + tracking |
| Viewing/attention time | Berapa lama dia melihat ke arah display/produk (bukan sekadar lewat) | Head/gaze orientation estimation |
Semua ini tidak butuh realtime — cukup direkap per hari/per shift, lalu diagregasi. Ini penting karena membuka opsi arsitektur yang jauh lebih murah.
Catatan penting (update arah project): identifikasi siapa konsumennya (dan frekuensi kunjungan lintas hari) tidak diperlukan lewat biometrik. Fokusnya adalah kenapa transaksi terjadi (atau tidak) — jadi metrik di atas cukup hidup untuk satu sesi kunjungan (
track_idsekali pakai, tidak perlu di-link ke kunjungan sebelumnya lewat Re-ID), lalu dikorelasikan ke hasil transaksi di §5.Kenapa desain ini dipilih — bukan cuma soal UU PDP: alasan utamanya justru teknis & biaya, privasi cuma bonus. Person tracking dalam 1 sesi (orang A/B/C/D ditandai dari masuk sampai keluar frame, lalu di-relabel jadi identitas transaksi/member begitu dia bayar — alurnya persis §5.3) itu CV klasik yang murah: deteksi + tracking sederhana (YOLO+ByteTrack, §14), tidak butuh model wajah sama sekali. Re-ID biometrik persisten (simpan face/body embedding permanen supaya bisa kenali "orang yang sama" lintas hari) itu kelas masalah yang jauh lebih berat: butuh resolusi kamera tinggi buat wajah jelas, model embedding yang lebih besar & lebih mahal secara compute, storage untuk database embedding yang terus tumbuh, dan proses matching yang makin lambat/rawan salah seiring database membesar. Skema "tag sementara → relabel saat transaksi" di §5.3 menghindari semua beban itu — plus kebetulan juga lebih aman secara UU PDP (win-win, bukan trade-off).
⚠️ Versi optimal (rawan UU PDP + mahal secara compute): kalau kedua batasan itu diabaikan, opsi paling optimal adalah Re-ID biometrik persisten — ini memberi frekuensi kunjungan untuk semua pengunjung, bukan cuma yang transaksi + scan loyalty seperti alur backtrack di §5.3. Lebih kaya data, tapi mahal secara infrastruktur DAN melanggar UU PDP — dua alasan independen untuk skip, bukan cuma satu (lihat §20 untuk daftar lengkap opsi optimal yang di-skip).
[Kamera/CCTV]
│ (rekam terus, buffer lokal 5–15 menit/klip)
▼
[Layer 1 — Edge/Cheap CV, jalan terus-menerus, MURAH]
• Person detector + tracker (YOLOv8/v11 + ByteTrack/DeepSORT)
• Re-ID sederhana → deteksi "orang yang sama datang lagi"
• Hitung dwell time otomatis dari track masuk–keluar
• Estimasi head-pose/gaze kasar (opsional, model ringan) → proxy viewing time
• Output: tabel event mentah (track_id, timestamp masuk/keluar, koordinat, snapshot thumbnail)
│ → simpan ke DB lokal / object storage (bukan LLM, murni CV klasik)
▼
[Layer 2 — Batch VLM Pass, jalan PERIODIK (mis. tiap 15–30 menit atau end-of-day)]
• Ambil sampel frame/klip pendek dari track yang "menarik" saja:
- dwell time di atas threshold
- ada interaksi tangan-objek terdeteksi Layer 1
• Kirim ke VLM murah untuk enrichment semantik:
- "orang ini memegang jam tangan model apa?"
- "sedang mencoba di pergelangan tangan / hanya lihat-lihat?"
- "berinteraksi dengan staff?"
• Output: label semantik ditempel ke track_id yang sudah ada
│
▼
[Aggregation & Scoring Engine]
• Gabungkan Layer 1 (angka: durasi, frekuensi) + Layer 2 (label: jenis interaksi)
• Hitung skor gamifikasi per sesi/per konsumen/per toko
│
▼
[Dashboard / Leaderboard]
• Insight harian, badge, ranking toko/SPG, dsb.
Kenapa dipisah begini: Layer 1 (CV klasik) itu murah banget secara compute dan bisa jalan terus 24 jam di edge device (mini PC/NVR) tanpa biaya API. Layer 1 sendirian sudah cukup untuk 3 dari 4 metrik (dwell time, frekuensi, viewing proxy). VLM/LLM (Layer 2) baru dipanggil untuk hal yang butuh "pemahaman" — dan itu di-batch, bukan realtime, jadi biaya API jauh lebih kecil (mis. dari ribuan frame/hari jadi puluhan klip/hari yang benar-benar relevan).
Tidak perlu model paling pintar — cukup yang murah & cukup akurat untuk klasifikasi visual sederhana:
Ide inti: video analytics sendirian cuma tahu "berapa lama orang berinteraksi". Itu baru berguna kalau di-join dengan data transaksi POS — baru bisa jawab: interaksi yang lebih lama/jenis tertentu, apakah benar berkorelasi dengan closing rate lebih tinggi atau nominal transaksi lebih besar. Ini memungkinkan, dan justru lebih actionable daripada gamifikasi murni, karena ada ground truth (uang masuk) untuk validasi.
Video track dan transaksi POS adalah dua sumber data terpisah. Supaya bisa disatukan per orang/per momen, butuh kunci penghubung:
| Kunci | Dari mana | Catatan |
|---|---|---|
store_id |
Sama-sama ada di video & POS | Wajib |
counter_id / till_id |
Kamera diarahkan/di-zoning ke meja kasir spesifik, bukan cuma "toko" secara umum | Kalau 1 toko ada beberapa kasir, ini penting supaya interaksi ke-link ke kasir yang benar |
cashier_id |
POS sudah pasti catat siapa yang login/proses transaksi | Ini yang bikin bisa breakdown performance per orang kasir |
Waktu (entered_at–exited_at vs transaksi.created_at) |
Video kasih rentang waktu interaksi, POS kasih timestamp transaksi selesai | Match kalau transaksi.created_at jatuh di dalam [entered_at, exited_at + buffer beberapa menit] pada counter_id yang sama |
Kalau kamera masih general (bukan per-counter), korelasi masih bisa dibuat di level toko/shift (bukan per kasir individu) — tetap berguna, cuma resolusinya lebih kasar.
Track di zona kasir yang diikuti transaksi dalam window waktu tertentu → dianggap closing. Track yang tidak diikuti transaksi dalam window itu → non-closing (calon pembeli yang batal/hanya tanya-tanya). Ini penting: tanpa kelas non-closing, kamu cuma punya data orang yang beli, bukan funnel lengkap "berapa dari total interaksi yang jadi closing".
Insight tambahan dari ide "tandai A/B/C/D dulu, identitas baru muncul saat transaksi": kalau transaksi pakai program loyalty (member scan kartu/nomor HP saat checkout), identitas riil konsumen muncul sendiri di titik transaksi — tidak perlu dipaksa lewat face recognition/Re-ID selama dia keliling toko.
Alurnya:
1. Selama di toko, konsumen cuma "orang A/B/C/D" — track_id anonim biasa dari Layer 1, persis seperti desain di §1 (sekali pakai, tanpa identitas).
2. Begitu dia transaksi dan POS mencatat loyalty_id (karena dia scan member), track_transaction_match (§10) sudah otomatis tahu track_id mana yang match ke transaksi itu (via window waktu + counter_id, lihat §5.1).
3. Karena transaksi_id sekarang bisa punya loyalty_id, tinggal 1 join lagi: loyalty_id → transaksi_id → track_id → seluruh histori sesi kunjungan itu (dwell time, viewing time, produk yang dilihat, urutan interaksi) bisa di-backtrack dan diatribusikan ke member tersebut.
Kenapa ini lebih baik dari Re-ID biometrik:
- Identitas cuma nempel kalau konsumennya sendiri yang memilih identifikasi diri (scan member) — bukan sistem yang diam-diam mengenali wajah/tubuh dia dari video. Ini consent-based, bukan surveillance-based.
- Tidak butuh model Re-ID/embedding sama sekali untuk dapat manfaat ini — cukup 1 kolom loyalty_id di tabel transaksi yang sudah ada, dan join sederhana.
- Frekuensi kunjungan pun bisa didapat kembali tanpa biometrik apa pun: kalau loyalty_id yang sama muncul di beberapa transaksi lintas hari, itu otomatis histori kunjungan dia. Bedanya dengan Re-ID murni: ini cuma menangkap kunjungan yang berujung transaksi + scan member — kunjungan yang cuma lihat-lihat tanpa beli tetap anonim total, dan itu memang sudah sesuai arah §1 (gak perlu tahu siapa yang cuma lihat-lihat).
Syarat: POS/program loyalty perlu catat loyalty_id di tabel transaksi (kolom tambahan, lihat §10), dan matching track_transaction_match harus cukup presisi (counter_id + window waktu ketat) — kalau kamera masih general per-toko, ada risiko salah atribusi sesi ke orang lain yang kebetulan ada di dekat kasir saat transaksi berlangsung.
nominal_transaksi tiap bucket.interaction_seconds dan nominal_transaksi, atau antara interaction_type (dari VLM: ramah/informatif/dsb kalau mau digali lebih jauh) dan closing rate.interaction_type dan product_hint: apakah sesi yang closing didominasi pola tertentu (mis. "sempat coba di pergelangan tangan" jauh lebih sering closing dibanding "cuma lihat display")? Ini yang jadi insight ke perusahaan jam tangan — bukan soal siapa konsumennya, tapi pola interaksi apa yang mendorong closing/nominal lebih besar, supaya bisa dijadikan SOP/training buat kasir.Interaksi yang lama bisa berarti dua hal berlawanan: kasir jago closing (interaksi persuasif) atau transaksi itu memang rumit (banyak pertanyaan karena barang mahal/kompleks) sehingga makan waktu meski akhirnya closing. Supaya kesimpulan tidak salah arah:
- Kontrol dengan jumlah item per transaksi (item_count) — transaksi besar wajar makan waktu lebih lama.
- Bandingkan kasir yang pegang traffic/jam sibuk yang mirip, jangan bandingkan kasir shift sepi vs shift ramai langsung.
- Perlakukan hasil awal sebagai hipotesis untuk didiskusikan dengan store manager, bukan vonis final performance.
Ide: tandai staff pakai penanda fisik di seragam (mis. patch/armband warna di pundak) alih-alih pakai face recognition untuk tahu "ini kasir siapa". Ini lebih baik, bukan cuma alternatif — karena target adalah staff (bukan konsumen), staff bisa diminta pakai marker; konsumen tidak bisa.
2 opsi implementasi:
| Opsi | Cara kerja | Kapasitas ID | Kapan dipakai |
|---|---|---|---|
| Color patch/armband | Warna solid cerah (mis. kuning, merah, hijau) di bahu/kerah. Deteksi via HSV color segmentation — klasik CV, sangat ringan, robust di resolusi rendah karena blob warna besar tetap kedeteksi walau gambar buram | ~6–8 warna yang gampang dibedakan kamera CCTV di berbagai kondisi cahaya; kalau staff lebih banyak, kombinasikan 2 warna atau posisi kiri/kanan | Toko kecil, jumlah kasir sedikit (≤6–8), mau setup paling murah & cepat |
| Fiducial marker (ArUco/AprilTag) | Badge kecil pola hitam-putih unik per orang, decodable via cv2.aruco (OpenCV, built-in). Didesain khusus untuk robust di resolusi rendah & sudut pandang, tidak bergantung warna/cahaya seperti color patch |
Ratusan ID unik | Multi-toko/banyak staff, butuh scalable & lebih tahan kondisi cahaya toko yang bervariasi |
Implikasi ke arsitektur:
- cashier_id sekarang bisa langsung didapat dari deteksi marker di Layer 1 (CV klasik yang sudah jalan untuk person tracking) — tidak perlu embedding biometrik apa pun untuk staff. Ini menyederhanakan pipeline sekaligus memperkuat privasi (nol data biometrik staff yang disimpan).
- Karena marker menempel di badan/seragam (bukan posisi kamera tetap), staff yang berpindah counter/booth tetap ter-track dengan benar — gak perlu asumsi kaku "kasir X selalu di counter Y" seperti di §5.1.
- Mapping warna/kode marker → nama kasir cukup disimpan di tabel kecil terpisah (cashier_marker_map), diupdate manual oleh store manager tiap kali ada rotasi staff atau ganti seragam — bukan bagian dari pipeline CV.
- Catatan penting: ini solusi khusus untuk staff (marker fisik). Untuk konsumen, marker jelas tidak relevan — tapi itu juga tidak masalah, karena frekuensi kunjungan konsumen (kalau memang dibutuhkan) sudah bisa didapat lewat jalur lain yang lebih baik: backtrack via loyalty program saat transaksi, lihat §5.3. Jadi konsumen tidak butuh Re-ID/embedding tubuh sama sekali di skema ini.
Berbeda dari analytics konsumen anonim — ini secara langsung menilai kinerja individu karyawan (kasir). Perlu: - Transparansi ke karyawan bahwa video dipakai juga untuk evaluasi performance (bukan cuma "keamanan toko"), idealnya tertulis di kebijakan internal/kontrak kerja. - Dipakai sebagai coaching tool (identifikasi siapa butuh training) lebih aman secara relasi kerja daripada langsung jadi dasar punitive (potong bonus, dsb) tanpa proses lain.
track_id per konsumen bersifat sekali pakai (habis sesi, boleh langsung di-purge datanya, cukup sisakan angka agregat: durasi, jenis interaksi, hasil transaksi).Update dari riset eksternal (§18): kalau nanti sistem gamifikasi konsumen (§16-B) benar-benar dibangun dan mengumpulkan data pribadi (nama, no. HP, tanggal lahir lewat pendaftaran member/loyalty), berlaku kepatuhan tambahan di luar soal CCTV:
Lihat detail lengkap di riset-eksternal/gemini-2.md §I dan riset-eksternal/chatgpt-1.md.
⚠️ Versi optimal (rawan UU PDP): tanpa batasan consent/minimization, opsi paling optimal adalah auto-enroll semua pengunjung ke sistem (tanpa opt-in), retensi video mentah tanpa batas waktu (bukan purge 7–30 hari — supaya bisa diproses ulang pakai model masa depan yang lebih akurat), dan enrichment profil lewat inferensi VLM (estimasi umur, gender, kelas ekonomi dari pakaian/aksesoris) untuk personalisasi lebih tajam. Semua ini teknis mungkin dan akan menambah akurasi/insight, tapi masing-masing melanggar prinsip lawful basis, data minimization, dan storage limitation UU PDP — lihat ringkasan lengkap di §20.
track_transaction_match.cashier_id per transaksi (siapa yang login/proses)? Kalau belum, itu prasyarat yang perlu dibenahi dulu di sisi POS.| Komponen | Pilihan | Alasan |
|---|---|---|
| Kamera | IP camera 1080p existing CCTV, atau webcam USB kalau booth kecil | Tidak perlu kamera khusus untuk MVP |
| Edge compute | Mini PC / NUC dengan GPU kecil (mis. Jetson Orin Nano) atau cukup CPU kalau model ringan | Layer 1 (YOLO+tracker) ringan, bisa jalan di CPU utk 1 kamera |
| Person detection + tracking | YOLOv8n/v11n + ByteTrack (Ultralytics, open-source) | Model kecil, real-time di edge, gratis |
| Re-ID konsumen (deteksi "orang yang sama datang lagi") | OSNet embedding sederhana, cosine similarity antar track | Tidak butuh wajah utuh, cukup embedding vektor |
| Identifikasi kasir (bukan face) | HSV color segmentation untuk color patch/armband, atau cv2.aruco untuk badge fiducial marker |
Nol biometrik staff yang disimpan, robust di resolusi rendah, jauh lebih murah dari face recognition — lihat §5.6 |
| Storage klip & buffer | Disk lokal, rolling buffer 7–30 hari (auto-purge) | Kontrol retensi untuk privasi |
| Batch job scheduler | Cron / simple worker (tiap 15–30 menit) yang scan track baru → panggil VLM | Tidak perlu infra streaming yang berat |
| VLM Layer 2 | Gemini Flash / GPT-5-mini / Claude Haiku (API), atau SmolVLM self-host kalau volume tinggi | Model-agnostic, tinggal ganti endpoint |
| Database | Postgres/SQLite — 1 tabel track mentah, 1 tabel enrichment, 1 tabel skor agregat | Simpel, sesuai skala 1 toko dulu |
| Dashboard | Web sederhana (bisa reuse pattern Aldis ERP: Supabase + static admin page) kalau mau cepat | Konsisten dengan tooling yang sudah biasa dipakai |
-- Layer 1: hasil tracking mentah, ditulis terus-menerus oleh edge
CREATE TABLE track_event (
track_id TEXT PRIMARY KEY, -- id unik per kemunculan orang di frame
store_id TEXT NOT NULL,
counter_id TEXT, -- kasir/meja mana (null kalau kamera masih general per-toko)
detected_marker TEXT, -- kode warna/ArUco id kalau track ini staff (lihat cashier_marker_map) → null kalau konsumen
entered_at TIMESTAMP NOT NULL,
exited_at TIMESTAMP,
dwell_seconds INT, -- exited_at - entered_at
gaze_seconds INT, -- estimasi durasi menghadap display (proxy dari head-pose)
thumbnail_path TEXT, -- 1-2 snapshot untuk keperluan Layer 2, bukan video mentah
has_hand_object_interaction BOOLEAN DEFAULT FALSE -- trigger buat masuk antrian Layer 2
);
-- Layer 2: enrichment semantik, ditulis oleh batch VLM job
CREATE TABLE track_enrichment (
track_id TEXT REFERENCES track_event(track_id),
interaction_type TEXT, -- 'lihat_saja' | 'pegang_coba' | 'ngobrol_staff' | dst
product_hint TEXT, -- deskripsi model jam yang kelihatan, kalau VLM bisa infer
interaction_seconds INT,
processed_at TIMESTAMP,
model_used TEXT -- catat model VLM yang dipakai, buat audit biaya/akurasi
);
-- Definisi zona toko (Tier 2, §14 Fase 2) — dipetakan manual sekali, dipakai ulang oleh semua sesi
CREATE TABLE zone_definition (
zone_id TEXT PRIMARY KEY, -- mis. 'A', 'B', 'C', 'D'
store_id TEXT NOT NULL,
label TEXT, -- mis. "Rak Chronograph Series", "Display Dress Watch"
product_category TEXT -- kategori/koleksi produk di zona ini — dipakai untuk cocokkan ke item yang dibeli
);
-- Breakdown dwell time PER ZONA dalam satu sesi kunjungan — ini yang sebelumnya belum ada
-- track_event.dwell_seconds cuma total sesi; tabel ini pecah jadi per-zona (A 3 menit, B 4 menit, dst)
CREATE TABLE zone_dwell_segment (
id SERIAL PRIMARY KEY,
track_id TEXT REFERENCES track_event(track_id),
zone_id TEXT REFERENCES zone_definition(zone_id),
entered_at TIMESTAMP NOT NULL,
exited_at TIMESTAMP NOT NULL,
dwell_seconds INT NOT NULL -- SUM(dwell_seconds) per track_id boleh > track_event.dwell_seconds kalau orang bolak-balik ke zona yang sama
);
-- Mapping marker → staff, diupdate manual oleh store manager (bukan bagian pipeline CV)
CREATE TABLE cashier_marker_map (
marker_code TEXT PRIMARY KEY, -- mis. 'yellow_shoulder' atau ArUco id '#14'
cashier_id TEXT NOT NULL,
active_from DATE,
active_to DATE -- null kalau masih dipakai; diisi kalau ganti marker/staff resign
);
-- Data POS existing (asumsi sudah ada, tinggal dibaca via replikasi/export, bukan ditulis oleh pipeline video)
-- transaksi(transaksi_id, store_id, counter_id, cashier_id, created_at, nominal_total, item_count, loyalty_id)
-- loyalty_id: null kalau konsumen tidak scan member — itu wajar & tidak masalah, cukup dianalisis sebagai sesi anonim (§5.4/§5.5).
-- Kalau ada, ini kunci backtrack identitas per §5.3 — tanpa perlu Re-ID/embedding sama sekali.
-- Join layer: hasil matching video x POS, ini yang jadi basis analisis performance kasir
CREATE TABLE track_transaction_match (
track_id TEXT REFERENCES track_event(track_id),
transaksi_id TEXT, -- null kalau track ini non-closing (interaksi tanpa transaksi mengikuti)
cashier_id TEXT,
is_closing BOOLEAN,
match_confidence TEXT, -- 'exact_counter' | 'store_level_fallback' — transparansi soal seberapa presisi matching-nya
matched_at TIMESTAMP
);
-- Hand-off multi-staf per sesi (§17.3) — sebelumnya disebut di prosa tapi belum pernah didefinisikan di sini
-- Log SEMUA staf yang terdeteksi berinteraksi dengan 1 track_id, bukan cuma cashier_id di titik transaksi
CREATE TABLE track_staff_interaction (
id SERIAL PRIMARY KEY,
track_id TEXT REFERENCES track_event(track_id),
cashier_id TEXT NOT NULL, -- dari deteksi marker (§5.6), bisa lebih dari 1 baris per track_id
interaction_seconds INT,
role_hint TEXT -- 'approach' | 'closing' | dst — opsional, buat dasar split kredit di §17.3
);
-- Presence staf per zona (Tier 2-B, §13-B) — sebelumnya cuma disebut di prosa (§14 Fase 2 poin 4), belum ada tabelnya
-- Diisi dari deteksi marker staf (§5.6) yang di-track terus-menerus, bukan cuma di counter kasir
CREATE TABLE staff_zone_presence (
id SERIAL PRIMARY KEY,
cashier_id TEXT NOT NULL,
zone_id TEXT REFERENCES zone_definition(zone_id),
entered_at TIMESTAMP NOT NULL,
exited_at TIMESTAMP,
-- dari sini bisa dihitung: idle_time (durasi presence tanpa ada track_event konsumen tumpang tindih di zona sama),
-- response_time (selisih customer entered_at zona vs staff entered_at zona berikutnya),
-- coverage_gap (rentang waktu 1 zona tanpa baris staff_zone_presence sama sekali)
);
Tidak ada kolom identitas/embedding konsumen di skema ini — track_id untuk konsumen sengaja hanya berlaku dalam satu sesi kunjungan (tidak di-link ke kunjungan lain), karena tujuan analisisnya adalah "kenapa transaksi terjadi", bukan "siapa dia". Ini juga berarti nol data biometrik konsumen yang perlu disimpan atau di-retensi.
track_transaction_match diisi oleh batch job terpisah (jalan setelah Layer 1 & POS sync harian): untuk tiap track_event di suatu counter_id, cari transaksi dengan created_at di dalam [exited_at, exited_at + N menit] pada counter_id/cashier_id yang sama → kalau ketemu, is_closing = true dan simpan transaksi_id; kalau tidak, is_closing = false.
Ditunda per arahan client. Disimpan di sini karena arsitektur data (§10) sudah mendukung tanpa perubahan besar kalau nanti mau dilanjutkan — tinggal tambah scoring layer di atas
track_event+track_enrichment(dan sekarang juga bisa ambil sinyal daritrack_transaction_match, mis. poin ekstra untuk kasir yang closing rate-nya tinggi).
Ada 2 opsi (belum perlu diputuskan sekarang):
Opsi A — Gamifikasi konsumen (butuh link ke member lewat loyalty_id — bukan Re-ID, tapi backtrack saat scan member/QR check-in, persis alur §5.3 & tabel member di §19.1):
| Aksi | Poin |
|---|---|
| Kunjungan baru ke toko | +10 |
| Dwell time > 3 menit | +15 |
| Viewing produk tertentu > 30 detik | +10 |
Coba/pegang jam (interaction_type = pegang_coba) |
+25 |
| Kunjungan ke-N dalam sebulan (loyalty streak) | +5 × N (cap) |
Opsi B — Gamifikasi staff/toko (funnel tanpa identitas konsumen):
Skor toko/shift = Σ (interaction_seconds / gaze_seconds) per track_id yang punya gaze_seconds > 0
→ makin tinggi rasio "lihat → pegang/interaksi", makin tinggi skor konversi staff. Bisa dipecah per SPG kalau ada kamera/zona yang bisa diasosiasikan ke staff yang sedang jaga (butuh jadwal shift sebagai input tambahan).
Riset Haidilao (§18) kasih 2 contoh skema tier nyata yang bisa jadi starting point kalau Opsi A dilanjutkan — jangan pakai nama-nama Haidilao asli (Black Sea, dsb — risiko trademark, lihat catatan legal di §6), tapi pola tier & threshold-nya bisa diadaptasi:
| Sumber | Nama Tier (contoh, jangan dipakai persis) | Pola Threshold |
|---|---|---|
| gemini-2.md | "Rasa Coins": Bronze (Warga) → Silver (Tetangga) → Gold (Keluarga) | Berbasis spending tahunan (mis. Rp2jt → Rp5jt) |
| chatgpt-1.md | "Ramah Circle": Teman → Sahabat → Istimewa → Prioritas | Rolling 12 bulan, dari kunjungan + spend (bukan cuma nominal) |
Prinsip desain tier yang divalidasi kedua riset (bukan cuma tebakan): pakai window rolling 12 bulan (bukan lifetime), threshold berbasis kombinasi spend + frekuensi kunjungan (bukan salah satu saja), dan manfaatkan efek "near-miss" — tampilkan ke konsumen berapa lagi jaraknya ke tier berikutnya supaya jadi dorongan balik.
Angka ekonomi sebagai acuan awal (dari chatgpt-1.md, skenario ilustratif — bukan angka pasti, tapi lebih baik dari menebak): biaya reward efektif disarankan tetap 0,8–1,5% dari sales member, dengan break-even uplift kunjungan sekitar 5–14% tergantung margin kontribusi toko. Kalau nanti Opsi A dieksekusi, angka ini jadi baseline untuk simulasi biaya sebelum diluncurkan — bukan dikira-kira dari nol.
Guardrail legal penting (lihat §6 update): kalau skema poin nanti melibatkan elemen keberuntungan apa pun (spin wheel, blind box), itu perlu izin UGB Kemensos. Rekomendasi: desain Opsi A selalu deterministik (aksi pasti → reward pasti), bukan random — supaya bisa diluncurkan tanpa proses perizinan tambahan.
⚠️ Versi optimal (rawan Permensos UGB, bukan UU PDP): literatur behavioral economics (§18, chatgpt-1.md §D.1) menunjukkan variable reward (hadiah acak, tidak pasti besarnya) menghasilkan engagement lebih tinggi daripada reward deterministik — inilah kenapa Haidilao pakai lucky-wheel & secret code dengan hadiah tak terduga. Tanpa batasan UGB, mekanik random ini akan lebih optimal secara psikologis. Tapi karena butuh izin Kemensos + hibah 10% + pajak 25% dari nilai hadiah, opsi ini sengaja di-skip di desain final — lihat §20.
Catatan: ini cuma biaya infrastruktur video (Layer 1+2). Kalau nanti gamifikasi konsumen (§11, §16-B) juga dieksekusi, ada biaya terpisah untuk program reward — lihat angka ekonomi (biaya reward % sales, break-even uplift) di §11 "Update dari Riset Eksternal" dan detail skenario lengkap di riset-eksternal/chatgpt-1.md §Analisis Risiko Finansial. Dua pos biaya ini independen — biaya video tidak naik walau program gamifikasi konsumen belum/tidak jalan.
Ini kumpulan sinyal performance level toko yang bisa ditarik dari video, di luar korelasi kasir↔transaksi di §5. Dikelompokkan per tier kesulitan implementasi — mulai dari yang paling gampang dulu.
Murni Layer 1 (CV tracking dasar: deteksi + hitung orang di suatu area/waktu). Tidak butuh marker staff, tidak butuh zone mapping rumit, tidak butuh VLM. Akurasinya juga paling tinggi karena cuma "hitung orang di area X pada waktu Y" — kasus penggunaan paling matang untuk CV klasik.
A. Traffic & Footfall
- Jumlah pengunjung per jam/hari (hitung track_id unik yang masuk).
- Pola jam ramai vs sepi → dasar atur jadwal staff.
- Conversion rate toko secara umum = total transaksi ÷ total pengunjung (beda dari conversion per kasir di §5 — ini level toko).
- Window shopping vs benar-benar masuk (kalau kamera juga cover area depan toko/etalase).
E. Antrian & Waktu Tunggu - Panjang antrian dari waktu ke waktu (hitung orang mengelompok di zona kasir). - Rata-rata waktu tunggu sebelum dilayani. - Jam-jam antrian overflow → indikasi butuh tambahan kasir di jam itu.
Masih Layer 1, tapi butuh 2 hal tambahan yang menaikkan kompleksitas setup (bukan akurasi model-nya): (1) marker staff (§5.6) harus aktif & ter-track terus-menerus di seluruh toko, bukan cuma pas di counter kasir — perlu kalibrasi ulang cakupan kamera; (2) definisi zona toko (zone_id per area/rak/display) perlu dipetakan manual ke koordinat kamera, dan berubah tiap kali layout toko diubah. D juga butuh Layer 2 (VLM) untuk label produk, jadi mewarisi biaya & keterbatasan akurasi VLM dari §4.
B. Coverage & Responsiveness Staff (skema tabelnya: staff_zone_presence, §10)
- Response time: berapa lama dari konsumen masuk suatu zona sampai staff mendekat/menyapa.
- Staff idle time: staff diam tanpa interaksi konsumen berapa lama.
- Rasio staff : konsumen per jam → deteksi jam understaffed.
- Coverage gap: zona toko yang "kosong" dari staff dalam durasi tertentu.
D. Product & Zone Engagement (heatmap) - Heatmap zona toko: area mana paling banyak dilalui/dilihat vs paling sepi → evaluasi layout & penempatan produk. - Produk yang sering dipegang/dicoba vs yang sering dilihat tapi tidak dipegang — sinyal mismatch (mungkin harga tidak sesuai ekspektasi, atau butuh penjelasan staff lebih aktif). - Dwell time per zona → validasi apakah display tertentu benar-benar menarik perhatian atau cuma dilewati.
Ini yang secara teknis paling riskan — bukan berarti tidak bisa, tapi akurasinya lebih rendah dan/atau butuh effort setup yang gak sebanding dengan sinyal yang didapat. Ditaruh di sini supaya ekspektasinya realistis dari awal, bukan baru ketahuan pas sudah dibangun.
C. Kualitas Interaksi (bagian ekspresi/body language) - Rasio konsumen didekati vs diabaikan, dan durasi interaksi staff–konsumen — ini sebenarnya masih masuk akal (mewarisi akurasi Tier 2, cuma perluasan cakupan waktu dari §5). - Yang sulit: sinyal ekspresi/body language/nada (ramah vs jutek, dst). Kesulitannya: - Subjektif & tidak ada ground truth yang gampang — beda dari "closing/tidak" (ada di POS) atau "orang ada di zona X" (obyektif), "ramah" itu penilaian manusia yang bisa beda-beda, sulit divalidasi model-nya benar atau tidak. - Resolusi CCTV retail biasanya tidak cukup untuk baca ekspresi wajah dari jarak & sudut kamera keamanan standar — beda dari kasus video interview/webcam yang jarak dekat. - Aktivitas staff yang ambigu: staff yang "diam" di kamera bisa lagi idle, bisa juga lagi merapikan display atau ngecek stok — sulit dibedakan CV tanpa false positive tinggi. - Rekomendasi: skip dulu, tidak worth investasinya dibanding metrik durasi/frekuensi yang sudah robust.
F. Operasional & Kepatuhan
- Kehadiran staff sesuai shift, jam buka/tutup aktual, zona wajib-ada-staff.
- Kesulitannya bukan di CV-nya (marker detection sendiri robust, §5.6), tapi di operasional marker itu sendiri: marker bisa ketutup barang yang lagi dipegang staff, staff lupa/malas pakai marker, marker kotor/pudar seiring waktu, dan tiap rotasi staff/ganti seragam butuh update manual cashier_marker_map (§10) — kalau tidak rajin di-maintain, datanya jadi tidak reliable pelan-pelan tanpa ketahuan.
- Juga butuh integrasi ke sistem jadwal/absensi yang terpisah (bukan cuma video) — ada dependency ke sistem lain yang mungkin belum ada.
G. Sinyal Anomali - Dwell time tinggi tanpa closing, atau lonjakan "masuk lalu keluar cepat" — secara data gampang dihitung (turunan dari Tier 1/2), tapi interpretasinya susah: - Butuh baseline historis yang cukup panjang dulu untuk tahu "normal"-nya seperti apa, sebelum bisa bilang sesuatu itu "anomali". - False positive tinggi: banyak sebab non-toko untuk "masuk-keluar cepat" (lupa dompet, salah toko, telepon masuk, dsb) yang video sendiri tidak bisa bedakan. - Anomali cuma kasih sinyal "ada sesuatu yang aneh di sini", bukan jawaban "kenapa" — tetap butuh follow-up manual/kualitatif dari manager toko untuk jadi actionable, bukan langsung insight otomatis.
Rekomendasi prioritas kalau mau mulai: A (traffic) + E (antrian) + §5 (kasir↔transaksi) dulu — murni Layer 1, paling murah, ROI paling cepat divalidasi. B dan D butuh perluasan sistem marker staff & Layer 2 VLM yang sudah dirancang, jadi bisa nyusul begitu MVP jalan. C bagian ekspresi/body language sebaiknya di-skip dulu.
model.track(source=..., tracker="bytetrack.yaml"), pip install ultralytics). Model kecil ini cukup jalan di CPU mini PC untuk 1–2 kamera, gak perlu GPU mahal di fase ini.solutions.ObjectCounter) supaya gak perlu nulis logic counting dari nol.→ Realistis: 1–2 minggu untuk 1 toko kalau kamera sudah terpasang dengan baik — sebagian besar waktu habis di kalibrasi zona & testing di kondisi pencahayaan toko yang sebenarnya, bukan di coding model-nya.
cashier_id tertentu, bukan konsumen.has_hand_object_interaction = true di suatu zona, ambil snapshot → batch job VLM harian untuk label produk/interaksi, sesuai desain §4 & §10 (tabel track_enrichment).→ Realistis: 3–4 minggu tambahan di atas Fase 1 — waktunya lebih banyak habis di kalibrasi marker & zone mapping daripada coding, karena ini yang paling sensitif terhadap kondisi fisik toko yang sebenarnya (pencahayaan, sudut kamera, layout).
Rangkuman semua data amatan yang benar-benar tercatat di seluruh skema ini, dikelompokkan per sumber. Ini konsolidasi dari §10, §13, §14 — dikumpulkan di satu tempat biar gampang dicek.
track_id — id sementara, hidup selama satu sesi kunjungan saja (lihat §1).store_id, counter_id — lokasi toko & counter/kasir mana.entered_at, exited_at, dwell_seconds — kapan masuk/keluar frame & berapa lama.gaze_seconds — estimasi durasi menghadap ke display (proxy viewing time, dari head-pose kasar, bukan eye-tracking presisi).has_hand_object_interaction — flag ya/tidak ada interaksi tangan-objek (trigger buat masuk antrian Layer 2).thumbnail_path — 1–2 snapshot gambar (bukan video mentah), retensi pendek (§6), cuma dipakai kalau perlu diproses Layer 2 atau audit manual.detected_marker — kode warna/ArUco kalau track ini staff (§5.6); null kalau konsumen.interaction_type — label semantik: lihat saja / pegang-coba / ngobrol staff, dst.product_hint — deskripsi model/kategori jam yang kelihatan (kalau bisa diinfer VLM).interaction_seconds — durasi interaksi yang lebih spesifik dari dwell time umum.model_used, processed_at — audit trail model & waktu proses (untuk cek biaya/akurasi belakangan).zone_id — area/rak/display mana (hasil pemetaan manual denah toko ke koordinat kamera).cashier_marker_map: marker_code (kode warna/ArUco), cashier_id, active_from/active_to — mapping ini diinput manual oleh store manager, bukan hasil deteksi otomatis.transaksi_id, store_id, counter_id, cashier_id, created_at, nominal_total, item_count, loyalty_id (kalau member scan, lihat §5.3).track_transaction_match: track_id, transaksi_id (null kalau non-closing), cashier_id, is_closing, match_confidence.track_id sengaja sekali pakai per sesi (§1).Dikelompokkan jadi 3: gamifikasi staff (bisa langsung dieksekusi karena pipeline data-nya sudah ada dari §5), gamifikasi konsumen (butuh layer baru: app/loyalty), dan perbaikan proses bisnis yang lebih luas (pakai data yang sama, tapi bukan gamifikasi).
track_transaction_match (§5). Lebih objektif dan staff bisa lihat progress-nya sendiri.product_hint model X yang closing meningkat dibanding baseline — pakai data yang sudah ada di track_enrichment tanpa infrastruktur baru.loyalty_id yang sama seperti alur backtrack di §5.3 — ini jalan pintas untuk gamifikasi konsumen tanpa perlu bangun Re-ID sama sekali.product_hint yang di-link ke sesi konsumen yang sudah check-in, bukan dari tracking anonim.loyalty_id yang sama muncul di beberapa transaksi lintas waktu otomatis jadi basis "member setia", tanpa butuh sistem tambahan di luar apa yang sudah dirancang.Update dari riset eksternal (§18) — mekanik tambahan yang lebih konkret dari sekadar ide:
product_hint yang sudah dirancang di §10, tanpa infrastruktur tambahan.loyalty_id) — salah satu mekanik dengan bukti dampak paling kuat di ketiga riset, dan murah diimplementasikan (cuma broadcast terjadwal).Yang paling masuk akal dieksekusi duluan: bagian A (gamifikasi staff) dan sebagian C (perbaikan skema komisi + manajemen antrian) — karena keduanya langsung pakai data yang sudah dirancang di §5 dan §13, tidak butuh infrastruktur tambahan (app, QR, loyalty system) seperti bagian B.
Bukan cuma satu leaderboard datar — kombinasi mekanik ini biasanya lebih sehat secara motivasi:
| Mekanik | Bentuk konkret | Kenapa dipakai |
|---|---|---|
| Poin | Akumulasi dari composite score (§17.2) | Basis kuantitatif untuk semua mekanik lain |
| Level/Tier | Bronze/Silver/Gold/Platinum berdasarkan rolling 30-hari | Progres personal — staff bersaing dengan versi dirinya sendiri, bukan cuma dibanding orang lain |
| Badge/Achievement | "Approach Master" (response time tercepat), "Consistency Award" (variance rendah), "Product Expert" (closing merata di banyak model, bukan cuma 1 SKU favorit) | Mengapresiasi jenis kekuatan yang beda-beda, bukan cuma nominal penjualan terbesar |
| Quest/Challenge mingguan | "Minggu ini: tingkatkan approach rate ke >80%" — target achievable, bukan kompetitif | Cocok untuk staff yang levelnya belum tinggi, tetap punya cara menang tanpa harus juara umum |
| Leaderboard tim (toko) | "Toko health score" (§16-A) | Menyeimbangkan kompetisi individu dengan kerja sama — penting karena closing sering butuh lebih dari 1 staff (lihat §17.3) |
Kalau cuma pakai 1 metrik (mis. closing rate saja), gampang di-game dan gampang bias. Gabungkan beberapa sinyal dari §5 & §13 dengan bobot, contoh kerangka (bobot tinggal disesuaikan ke prioritas bisnis):
Skor = w1 × conversion_rate_ternormalisasi
+ w2 × avg_basket_value_ternormalisasi
+ w3 × approach_rate (§13-B)
+ w4 × product_diversity (jumlah model berbeda yang berhasil closing, bukan cuma 1 SKU termudah)
- penalti × non_response_rate (konsumen di zona tapi tidak pernah didekati)
"Ternormalisasi" penting: bandingkan staff hanya dengan staff lain di kondisi traffic/jam yang sebanding (lihat §5.5) — supaya staff shift sepi tidak otomatis kalah dari staff shift ramai.
Realita di toko: staf A menyapa & menjelaskan produk (approach), tapi transaksi diproses staf B di kasir (cashier_id dari POS beda dengan staf yang muncul di interaksi awal). Kalau atribusi cuma ambil cashier_id dari POS (default di §5.1), staf A yang kerja "closing"-nya justru tidak dapat kredit.
Solusi: log semua marker staff (§5.6) yang terdeteksi berinteraksi dalam satu track_id yang sama, bukan cuma cashier di titik transaksi — tabel track_staff_interaction (§10) menyimpan ini. Skema poin lalu bisa split kredit (mis. 70% ke yang proses transaksi, 30% ke yang approach/menjelaskan, pakai role_hint) — atau, lebih simpel di fase awal, biarkan dulu semua staf yang tercatat di sesi itu dapat kredit penuh (double count sengaja) sampai pola hand-off-nya cukup jelas untuk ditentukan bobot split yang wajar.
Begitu staff tahu videonya dinilai, mereka akan menyesuaikan perilaku ke arah metrik — beberapa lubang yang perlu diantisipasi dari awal desain, bukan ditambal belakangan:
track_enrichment), bukan buat menghukum, tapi buat validasi bahwa skor tinggi memang mencerminkan kerja bagus — sekaligus early-warning kalau ada pola gaming yang mulai muncul.Update dari riset eksternal (§18): riset chatgpt-1.md (§ Analisis Risiko) mengonfirmasi pola gaming yang sama terjadi di industri lain lewat istilah "staff collusion" — karyawan memanipulasi sistem reward pelanggan untuk kepentingan sendiri (di F&B: staf scan struk pelanggan yang ditinggal ke akun sendiri/teman). Analog di toko jam: staf berpotensi memanipulasi data supaya closing yang sebenarnya bukan hasil kerjanya sendiri tercatat sebagai miliknya (relevan ke masalah hand-off di §17.3). Mitigasi konkret dari riset yang bisa diadopsi: velocity check (batasi berapa kali suatu pola "closing" bisa diklaim per staf per hari sebelum di-flag), dan pastikan atribusi closing selalu tervalidasi silang ke cashier_id asli di POS (§10) — bukan cuma laporan staf sendiri.
Karena ini employee monitoring (§5.7), lompat langsung ke "skor menentukan bonus" berisiko menimbulkan resistensi kalau staff belum percaya sistemnya akurat. Rollout bertahap:
Urutan ini juga jadi mitigasi risiko hukum ketenagakerjaan (§5.7) — ada jejak bahwa evaluasi diperkenalkan bertahap dan transparan, bukan tiba-tiba dipakai buat memotong bonus tanpa staff pernah tahu kriterianya.
⚠️ Versi optimal (rawan UU PDP + Ketenagakerjaan): tanpa batasan, monitoring paling optimal adalah biometrik staf real-time — deteksi ekspresi/mood (§13-C yang sebelumnya di-skip karena akurasi & privasi) dipakai untuk skor instan, langsung dipotong dari insentif tanpa fase trust-building 3 tahap di atas, dan tanpa jalur banding (
staff_appeal_log, §19.2) sama sekali. Ini secara data lebih "real-time actionable", tapi biometrik karyawan adalah kategori data yang diproteksi ketat di UU PDP, dan penilaian sepihak tanpa transparansi berisiko pelanggaran UU Ketenagakerjaan soal keadilan evaluasi kerja — makanya desain final tetap pakai rollout bertahap di atas. Ringkasan lengkap di §20.
Catatan hasil riset dari luar (Gemini/ChatGPT deep research) soal framework/referensi gamifikasi yang relevan — ditempel di file terpisah biar §16–17 di atas tidak bercampur dengan kutipan mentah. Update §16/§17 kalau ada temuan yang perlu direvisi berdasarkan riset ini.
Temuan-temuan kunci dari ketiga riset ini sudah diintegrasikan ke §6 (kepatuhan UU PDP/UGB), §11 (contoh tier & angka ekonomi), §16-B (mekanik konsumen: WhatsApp-first, Queue/Table Mission), dan §17.4 (pola staff collusion & mitigasi).
Ini pelengkap §10/§15 yang sebelumnya cuma mencakup data video+POS. Skema di bawah menurunkan semua ide di §11, §16, §17 jadi tabel konkret — siap dipakai sebagai basis implementasi, bukan cuma daftar ide.
-- Data member — diisi saat registrasi WhatsApp-first (§16-B), bukan dari deteksi video
CREATE TABLE member (
loyalty_id TEXT PRIMARY KEY, -- sama dengan loyalty_id yang dipakai backtrack di §5.3
nama TEXT,
no_hp TEXT NOT NULL, -- data minimal sesuai prinsip data minimization UU PDP (§6)
bulan_lahir INT, -- cukup bulan, bukan tanggal lahir lengkap — kurangi data sensitif
store_registrasi TEXT,
current_tier TEXT NOT NULL DEFAULT 'Bronze',
registered_at TIMESTAMP NOT NULL,
deleted_at TIMESTAMP -- soft delete untuk penuhi hak "right to erasure" UU PDP
);
-- Log consent — wajib untuk pembuktian kepatuhan UU PDP (§6), terpisah dari data member itu sendiri
CREATE TABLE consent_log (
id SERIAL PRIMARY KEY,
loyalty_id TEXT REFERENCES member(loyalty_id),
consent_type TEXT NOT NULL, -- 'data_processing' | 'marketing_whatsapp'
channel TEXT, -- 'whatsapp_flow' | 'qr_form', dst
granted_at TIMESTAMP,
revoked_at TIMESTAMP -- null kalau masih berlaku
);
-- Ledger poin — SATU-SATUNYA sumber kebenaran untuk saldo poin (dihitung dari SUM(points_delta), bukan disimpan sebagai kolom terpisah yang gampang drift)
CREATE TABLE points_ledger (
id SERIAL PRIMARY KEY,
loyalty_id TEXT REFERENCES member(loyalty_id),
event_type TEXT NOT NULL, -- 'earn_transaksi' | 'earn_mission' | 'redeem' | 'expire' | 'referral_bonus' | 'birthday_bonus'
points_delta INT NOT NULL, -- positif untuk earn, negatif untuk redeem/expire
source_ref TEXT, -- transaksi_id, mission_id, atau referral_id terkait — untuk audit trail
created_at TIMESTAMP NOT NULL
);
-- Histori tier — perlu disimpan terpisah dari member.current_tier supaya bisa dianalisis kapan orang naik/turun tier
CREATE TABLE tier_history (
id SERIAL PRIMARY KEY,
loyalty_id TEXT REFERENCES member(loyalty_id),
tier TEXT NOT NULL,
effective_from TIMESTAMP NOT NULL,
effective_to TIMESTAMP, -- null = masih berlaku
reason TEXT -- 'rolling_recompute' | 'manual_adjustment'
);
-- Definisi misi (Queue Quest / Table Mission / Social Challenge, §16-B) — dikelola manual oleh tim marketing, bukan hasil sistem
CREATE TABLE mission_definition (
mission_id TEXT PRIMARY KEY,
mission_type TEXT NOT NULL, -- 'queue_quest' | 'table_mission' | 'social_challenge' | 'referral'
description TEXT,
reward_points INT NOT NULL,
is_deterministic BOOLEAN NOT NULL DEFAULT TRUE, -- WAJIB true kecuali sudah lolos review legal UGB (§6) — flag ini sengaja eksplisit supaya tidak kelewat saat desain misi baru
active_from DATE,
active_to DATE
);
-- Penyelesaian misi oleh member
CREATE TABLE mission_completion (
id SERIAL PRIMARY KEY,
loyalty_id TEXT REFERENCES member(loyalty_id),
mission_id TEXT REFERENCES mission_definition(mission_id),
track_id TEXT, -- opsional: link ke track_event (§10) kalau misi terkait sesi kunjungan tertentu, mis. Table Mission
completed_at TIMESTAMP NOT NULL,
reward_points_ledger_id INT REFERENCES points_ledger(id) -- link ke baris ledger yang dibuat akibat penyelesaian ini
);
-- Redemption reward (barang/voucher ditukar poin)
CREATE TABLE redemption_log (
id SERIAL PRIMARY KEY,
loyalty_id TEXT REFERENCES member(loyalty_id),
reward_item TEXT NOT NULL,
points_spent INT NOT NULL,
store_id TEXT,
approved_by TEXT, -- cashier_id yang validasi di kasir (§10)
redeemed_at TIMESTAMP NOT NULL
);
-- Referral (§16-B) — 1 baris per pasangan referrer-referee, status berubah seiring waktu
CREATE TABLE referral_log (
id SERIAL PRIMARY KEY,
referrer_loyalty_id TEXT REFERENCES member(loyalty_id),
referee_loyalty_id TEXT REFERENCES member(loyalty_id),
referee_first_transaksi_id TEXT, -- diisi begitu referee transaksi pertama kali (syarat reward cair, §18 chatgpt-1.md)
status TEXT NOT NULL DEFAULT 'pending', -- 'pending' | 'completed' | 'rejected'
reward_granted_at TIMESTAMP
);
-- Broadcast ulang tahun (§16-B) — log terpisah dari consent supaya bisa diaudit terkirim/diklaim atau tidak
CREATE TABLE birthday_broadcast_log (
id SERIAL PRIMARY KEY,
loyalty_id TEXT REFERENCES member(loyalty_id),
broadcast_month DATE NOT NULL,
sent_at TIMESTAMP,
claimed_at TIMESTAMP -- null = belum diklaim
);
-- Sinyal "minat sekunder" — zona yang di-dwell lama tapi BUKAN zona produk yang akhirnya dibeli (contoh Budi, §19.5)
-- Diisi oleh batch job harian: untuk tiap track_id yang closing (is_closing=true di track_transaction_match),
-- ambil zone_dwell_segment dengan dwell_seconds di atas threshold, KECUALI zona yang product_category-nya cocok dengan item yang dibeli
CREATE TABLE secondary_interest_signal (
id SERIAL PRIMARY KEY,
loyalty_id TEXT REFERENCES member(loyalty_id), -- diisi lewat backtrack §5.3 — makanya sinyal ini cuma ada untuk sesi yang closing + scan member
track_id TEXT REFERENCES track_event(track_id),
zone_id TEXT REFERENCES zone_definition(zone_id), -- zona minat sekunder, mis. 'D'
purchased_zone_id TEXT, -- zona yang produknya benar-benar dibeli, mis. 'C' — buat konteks/audit
dwell_seconds INT,
detected_at TIMESTAMP NOT NULL
);
-- Broadcast penawaran personal hasil dari secondary_interest_signal (atau trigger lain di masa depan)
-- Terpisah dari birthday_broadcast_log karena trigger & timing-nya beda, tapi pola tabelnya sama
CREATE TABLE personalized_offer_log (
id SERIAL PRIMARY KEY,
loyalty_id TEXT REFERENCES member(loyalty_id),
trigger_type TEXT NOT NULL, -- 'secondary_interest' | 'birthday' | 'reactivation_90d', dst — bisa diperluas
trigger_ref INT, -- id ke secondary_interest_signal.id (atau tabel trigger lain sesuai trigger_type)
offer_detail TEXT, -- mis. "diskon 10% zona D"
sent_at TIMESTAMP,
claimed_at TIMESTAMP -- null = belum diklaim
);
-- Snapshot skor komposit harian per kasir — historikal, bukan cuma angka "saat ini" (§17.2)
CREATE TABLE staff_score_daily (
id SERIAL PRIMARY KEY,
cashier_id TEXT NOT NULL,
score_date DATE NOT NULL,
conversion_rate NUMERIC, -- ternormalisasi terhadap traffic sebanding (§17.2)
avg_basket_value NUMERIC,
approach_rate NUMERIC, -- dari §13-B
product_diversity INT, -- jumlah model berbeda yang closing (§17.2)
composite_score NUMERIC NOT NULL,
computed_at TIMESTAMP NOT NULL,
UNIQUE (cashier_id, score_date)
);
-- Badge yang didapat staf (§17.1) — log historis, bukan cuma status boolean
CREATE TABLE staff_badge_earned (
id SERIAL PRIMARY KEY,
cashier_id TEXT NOT NULL,
badge_code TEXT NOT NULL, -- 'approach_master' | 'consistency_award' | 'product_expert', dst
earned_at TIMESTAMP NOT NULL,
evidence_ref TEXT -- referensi ke staff_score_daily.id atau periode yang jadi dasar badge ini, untuk audit
);
-- Histori level/tier staf (Bronze/Silver/Gold/Platinum, §17.1)
CREATE TABLE staff_level_history (
id SERIAL PRIMARY KEY,
cashier_id TEXT NOT NULL,
level TEXT NOT NULL,
effective_from DATE NOT NULL,
effective_to DATE
);
-- Kasus coaching otomatis (§16-A: sesi non-closing durasi tinggi yang di-flag buat dibahas)
CREATE TABLE staff_coaching_case (
id SERIAL PRIMARY KEY,
track_id TEXT, -- link ke track_event (§10) yang men-trigger kasus ini
cashier_id TEXT NOT NULL,
flagged_reason TEXT, -- 'high_duration_non_closing' | 'low_approach_rate', dst
status TEXT NOT NULL DEFAULT 'open', -- 'open' | 'reviewed' | 'resolved'
reviewed_by TEXT,
reviewed_at TIMESTAMP,
notes TEXT
);
-- Jalur banding/appeal (§17.5) — staf bisa keberatan atas skor tertentu, ini tempat pencatatannya
CREATE TABLE staff_appeal_log (
id SERIAL PRIMARY KEY,
cashier_id TEXT NOT NULL,
related_score_date DATE, -- merujuk ke staff_score_daily
reason TEXT,
submitted_at TIMESTAMP NOT NULL,
decision TEXT, -- 'upheld' | 'adjusted' | 'rejected'
decided_by TEXT,
decided_at TIMESTAMP
);
-- Flag anomali — dipakai untuk member (redemption/referral abuse) MAUPUN staf (gaming metrik, §17.4)
CREATE TABLE anomaly_flag (
id SERIAL PRIMARY KEY,
entity_type TEXT NOT NULL, -- 'member' | 'staff'
entity_id TEXT NOT NULL, -- loyalty_id atau cashier_id
flag_type TEXT NOT NULL, -- 'velocity_check' | 'duplicate_claim' | 'referral_self_dealing' | 'staff_collusion_suspect'
detected_at TIMESTAMP NOT NULL,
details TEXT,
status TEXT NOT NULL DEFAULT 'open' -- 'open' | 'dismissed' | 'confirmed'
);
Selaras dengan pentahapan MVP 30-hari vs advanced 6-12 bulan di riset (§18) dan rollout bertahap gamifikasi staf (§17.5) — tidak semua tabel di atas perlu dibangun sekaligus:
| Fase | Tabel yang dibutuhkan | Catatan |
|---|---|---|
| MVP konsumen (30 hari) | member, consent_log, points_ledger, redemption_log |
Cukup untuk loop inti: daftar → dapat poin → tukar reward. Bisa jalan di Google Sheet dulu sebelum DB sungguhan (§18 chatgpt-1.md) |
| Advanced konsumen | tier_history, mission_definition, mission_completion, referral_log, birthday_broadcast_log |
Ditambah setelah loop inti tervalidasi — sesuai prinsip "jangan bangun kompleksitas sebelum validasi" (§18) |
| Fase observasi staf (§17.5 fase 1) | staff_score_daily saja |
Manager lihat dulu, staf belum tahu — belum butuh badge/level/appeal |
| Fase pengakuan & insentif staf (§17.5 fase 2-3) | staff_badge_earned, staff_level_history, staff_coaching_case, staff_appeal_log |
Baru dibangun begitu staf mulai lihat skornya sendiri |
| Kapan pun ada reward/insentif nyata | anomaly_flag |
Jangan ditunda — begitu ada nilai (poin/uang) yang bisa disalahgunakan, anti-fraud harus sudah ada, bukan ditambah belakangan setelah insiden |
| Butuh Tier 2 penuh (§13/§14 Fase 2) lebih dulu | zone_definition, zone_dwell_segment, staff_zone_presence, secondary_interest_signal, personalized_offer_log |
Semua butuh pemetaan zona toko dulu (§14 Fase 2) — jangan dibangun kalau Tier 1 (traffic/antrean) saja belum stabil |
| Begitu ada >1 staf per sesi (hand-off, §17.3) | track_staff_interaction |
Bisa ditunda sampai pola hand-off cukup sering terjadi untuk worth di-track — di awal, atribusi 1 staf per sesi (cashier_id dari POS) sudah cukup |
Skenario konkret buat validasi seluruh alur — tiap angka di skenario ini datang dari tabel spesifik:
Konsumen A datang, berkeliling 30 menit total (20 menit lihat-lihat produk, 10 menit ngobrol dengan staf). Dia menyusuri zona A (3 menit), B (4 menit), C (5 menit), D (10 menit) — tapi akhirnya beli barang dari zona C. Karena D dapat perhatian paling lama tapi tidak dibeli, sistem kirim penawaran diskon khusus zona D lewat WhatsApp beberapa hari kemudian. Saat transaksi, dia scan member — namanya jadi "Budi".
Begini pemetaannya ke skema yang sudah dirancang:
track_event (track_id sementara, dwell_seconds=1800 detik/30 menit, gaze_seconds≈1200 detik/20 menit "lihat-lihat"). track_enrichment dari Layer 2 catat interaction_seconds≈600 detik (10 menit ngobrol staf) dan interaction_type.zone_dwell_segment dapat 4 baris untuk track_id yang sama: (A, 180 detik), (B, 240 detik), (C, 300 detik), (D, 600 detik) — total 1320 detik ≈ 22 menit (selisih wajar dari 30 menit total karena sebagian waktu di jalur antar-zona, bukan di dalam zona manapun).transaksi dengan counter_id & waktu tertentu, plus loyalty_id karena dia scan member. track_transaction_match (§10) otomatis link track_id ↔ transaksi_id lewat window waktu+counter (§5.1) → is_closing = true.loyalty_id di transaksi di-join ke member.loyalty_id → track_id anonim tadi sekarang "dipetakan" ke Budi. Seluruh histori sesi (poin 1-2 di atas) otomatis terhubung ke member Budi lewat 1 join, tanpa pernah butuh face recognition.zone_dwell_segment untuk track yang closing: zona C (yang dibeli) dikecualikan (via zone_definition.product_category dicocokkan ke item di transaksi), sisanya diurutkan by dwell_seconds — zona D (600 detik) di atas threshold → masuk secondary_interest_signal (loyalty_id=Budi, zone_id='D', purchased_zone_id='C').secondary_interest_signal yang belum ada offer, generate entry di personalized_offer_log (trigger_type='secondary_interest', offer_detail="diskon zona D"), kirim via WhatsApp broadcast (§16-B), catat sent_at. Kalau Budi datang lagi & redeem, claimed_at terisi.Kesimpulan validasi: skenario ini match penuh dengan arsitektur yang sudah dirancang — cuma butuh 2 tabel tambahan (zone_dwell_segment, secondary_interest_signal + personalized_offer_log) yang sekarang sudah masuk ke §19.1/§10. Tidak ada satu pun langkah yang butuh Re-ID biometrik atau data di luar yang sudah dirancang di §5.3 dan §14 Fase 2.
Ini kompilasi semua tag ⚠️ yang tersebar di dokumen — versi paling optimal secara data/akurasi/engagement kalau seandainya batasan hukum tidak ada, dan kenapa masing-masing sengaja tidak dipilih di desain final. Disimpan di satu tempat supaya gampang direview ulang kalau suatu saat kondisinya berubah (mis. sudah dapat consent eksplisit yang membuat sebagian opsi ini jadi legal).
| Area | Versi Compliant (dipakai) | Versi Optimal (di-skip) | Kenapa Lebih Optimal | Regulasi yang Dilanggar | Hambatan Non-Legal (tetap jadi alasan skip meski regulasi diabaikan) |
|---|---|---|---|---|---|
| Identifikasi konsumen (§1) | track_id sekali pakai per sesi (tag A/B/C/D dari CV klasik, di-relabel ke identitas transaksi/member saat bayar, §5.3) |
Re-ID biometrik persisten (face/body embedding permanen) | Frekuensi kunjungan untuk semua pengunjung, bukan cuma yang transaksi+scan loyalty | UU PDP — pemrosesan data biometrik tanpa consent | Ini alasan utama, bukan cuma UU PDP: butuh kamera resolusi tinggi buat wajah jelas, model embedding jauh lebih berat dari CV tracking biasa, storage database embedding yang terus tumbuh, dan matching yang makin lambat/error-prone seiring database membesar. Skema compliant justru lebih murah, bukan cuma "lebih aman" — win-win, bukan trade-off |
| Retensi data (§6) | Snapshot retensi pendek (7–30 hari), video di-purge | Video mentah disimpan tanpa batas waktu | Bisa diproses ulang pakai model masa depan yang lebih akurat; retrospective analysis | UU PDP — prinsip storage limitation | Storage jangka panjang untuk video mentah (bukan cuma snapshot) mahal secara linear dengan jumlah kamera & waktu — retensi pendek juga menghemat biaya storage, bukan cuma soal hukum |
| Enrollment member (§16-B) | Opt-in eksplisit (scan QR/consent form) | Auto-enroll semua pengunjung tanpa consent | Basis data lebih besar dari hari pertama, tidak bergantung willingness konsumen | UU PDP — lawful basis & consent | Butuh sumber identitas (nama/no HP) dari suatu tempat — tanpa consent, satu-satunya cara "auto-enroll" adalah lewat Re-ID biometrik di atas, jadi mewarisi masalah cost yang sama |
| Profil konsumen (§6, §19.1) | Data minimal: nama, no HP, bulan lahir | Enrichment penuh via VLM: estimasi umur, gender, kelas ekonomi dari pakaian/aksesoris | Personalisasi marketing jauh lebih tajam (ala AI personalization Haidilao, uplift 12–18%, §18) | UU PDP — prinsip data minimization | Tiap sesi butuh panggilan VLM tambahan (Layer 2, §4) khusus untuk inferensi demografis — menambah biaya API per kunjungan tanpa manfaat langsung ke tujuan utama (closing rate), bukan cuma soal privasi |
| Skema reward (§11, §16-B) | Selalu deterministik (aksi pasti → reward pasti) | Variable reward acak (lucky wheel, blind box, secret code ala Haidilao) | Terbukti secara behavioral economics lebih tinggi engagement-nya daripada reward pasti (§18) | Permensos No. 3/2024 (UGB) — butuh izin Kemensos + hibah 10% + pajak 25% dari nilai hadiah | Minim — ini murni soal legal, bukan soal cost/teknis (beda dari baris-baris lain di tabel ini) |
| Monitoring staf (§17.5) | Rollout 3 fase (observasi → non-finansial → insentif), skor staff_score_daily batch harian, ada staff_appeal_log |
Biometrik staf real-time (deteksi ekspresi/mood) → potong insentif instan tanpa banding | Feedback "actionable" seketika, tanpa jeda trust-building | UU PDP (data biometrik karyawan) + UU Ketenagakerjaan (keadilan evaluasi kerja) | Deteksi ekspresi/mood real-time butuh inferensi VLM terus-menerus (bukan batch), bertentangan dengan prinsip non-realtime hemat biaya di §2 — juga akurasinya sendiri masih diragukan (§13-C) |
Poin paling penting dari tabel ini: untuk 4 dari 6 baris, alasan skip bukan cuma kepatuhan hukum — arsitektur compliant kebetulan juga yang paling murah/efisien secara teknis. Cuma baris "skema reward" yang murni trade-off legal (variable reward secara teknis sama mudahnya dibangun, cuma butuh izin). Jadi kalau suatu saat UU PDP berubah atau sudah dapat consent penuh, sebagian besar opsi "optimal" di kolom ketiga tetap tidak worth dikejar karena cost-nya, bukan cuma karena legal — kecuali kasus reward random yang murni soal izin administratif.
Catatan jujur soal risiko yang tetap ada di luar UU PDP: menghapus batasan UU PDP secara hipotetis tidak menghapus semua risiko. Beberapa yang tetap relevan: - Platform ToS — WhatsApp Business API (dipakai di §16-B) melarang penggunaan data untuk profiling/tracking di luar konteks yang diizinkan; pelanggaran bisa berarti akun bisnis diblokir Meta, terlepas dari status hukum lokal. - Regulasi lain yang tidak hilang — Permensos UGB dan UU Ketenagakerjaan berdiri sendiri dari UU PDP; menghilangkan UU PDP tidak menghilangkan kewajiban di dua area itu. - Reputational risk — konsumen/staf yang sadar dipantau biometrik tanpa consent bisa menyebabkan backlash publik (viral di media sosial) yang dampaknya sering lebih mahal daripada denda administratif. - Risiko regulasi masa depan — UU PDP masih relatif baru (2022) dan penegakannya terus berkembang; desain yang "aman" hari ini karena celah penegakan belum berarti aman permanen.
Karena itu, tabel di atas dimaksudkan sebagai referensi trade-off, bukan rekomendasi implementasi — desain final di seluruh dokumen ini tetap versi compliant di kolom kedua.
Begitu zone_dwell_segment (§10) ada, banyak sinyal baru terbuka di luar "minat sekunder" (§19.5). Ini kumpulannya, dikelompokkan by kebutuhan tambahan skema.
zone_dwell_segment untuk 1 track_id menunjukkan bolak-balik ke 2 zona yang sama berkali-kali (mis. A→C→A→C), itu pola "membandingkan 2 produk", bukan sekadar jalan-jalan. Sinyal ini lebih kuat dari sekadar dwell time tinggi di 1 zona — staf bisa di-notify real-time (kalau infrastrukturnya sampai situ) untuk bantu putuskan, atau follow-up-nya berisi perbandingan 2 model itu, bukan cuma 1.zone_dwell_segment diurutkan entered_at per track_id yang closing) menunjukkan kombinasi zona yang sering dilalui bersama sebelum beli. Ini dasar cross-sell bundling dari pola jalan fisik, bukan cuma dari struk seperti market basket analysis biasa — bisa kasih insight merchandising yang analisis struk saja tidak bisa (mis. "orang yang akhirnya beli di C selalu mampir ke A dulu — naikkan mungkin taruh produk pelengkap A di dekat C").interaction_type = 'pegang_coba' (§10 track_enrichment) dan dwell tinggi tapi is_closing = false di track_transaction_match — ini prospek yang hampir beli. Kalau kebetulan dia sempat opt-in check-in (§16-B, lihat §21.2), follow-up di sini punya urgensi lebih tinggi dari sekadar "minat sekunder" biasa karena hampir closing, bukan cuma lihat-lihat.zone_dwell_segment.entered_at per jam/hari untuk tiap zone_id menunjukkan kapan orang paling tertarik ke kategori produk tertentu — dipakai untuk jadwalkan personalized_offer_log (§19.1) di jam yang secara historis paling relevan, bukan waktu random.personalized_offer_log yang di-generate dari interaksi seorang staf tertentu (staf yang menangani sesi awal) akhirnya di-claimed_at, itu bisa jadi komponen skor staf tambahan — bukan cuma closing di kunjungan yang sama, tapi juga "menabur" minat yang membuahkan hasil belakangan.loyalty_id yang sama di secondary_interest_signal), itu sinyal jauh lebih kuat daripada 1x kebetulan — prioritaskan follow-up untuk pola berulang ini. Tidak butuh tabel baru, cukup GROUP BY loyalty_id, zone_id di secondary_interest_signal yang sudah ada.loyalty_id bisa ter-link ke sesi meski tidak closing — ini besar karena §5.3 saat ini cuma capture kunjungan yang berakhir transaksi+scan. Konsekuensinya: prospek yang hampir beli (poin di atas) bisa di-follow-up meski dia tidak jadi beli hari itu. Perlu tambah kolom opsional di track_event: checkin_loyalty_id (diisi kalau consumer scan QR check-in di awal, terpisah dari loyalty_id yang datang dari track_transaction_match).price_tier (budget/mid/premium) di zone_definition (§10). Pola "banyak waktu di zona premium tapi beli di zona budget" adalah sinyal price-sensitivity — cocok untuk penawaran cicilan/promo khusus tier premium, bukan diskon generik.track_id konsisten masuk & keluar bersamaan dengan jarak posisi berdekatan, tandai sebagai 1 grup kunjungan. Berguna untuk cross-sell "hadiah pasangan" (mis. yang satu lama di zona jam pria, temannya lama di zona jam wanita → tawarkan set couple). Butuh tabel kecil baru: visit_companion_group (id, track_id_1, track_id_2, store_id, session_date, confidence).entered_at/exited_at per zona seperti zone_dwell_segment sekarang. Perlu tabel/log posisi tambahan (atau proses ini dilakukan langsung di Layer 1 sebelum ditulis ke DB — bandingkan jarak antar bounding box tiap frame, baru tulis hasil groupingnya saja ke visit_companion_group) — jangan simpan koordinat mentah tiap frame ke DB (mahal & tidak perlu), cukup hasil deteksi grup-nya.Prioritas kalau mau mulai: §21.1 dulu (query ulang dari data yang sudah ada, tanpa perubahan skema) — terutama path-to-purchase dan "almost bought", karena langsung actionable untuk campaign follow-up tanpa investasi tambahan. §21.2 nyusul begitu ada kapasitas untuk perluasan kecil skema, dengan voluntary check-in sebagai yang paling bernilai karena memperluas cakupan dari "cuma yang closing" ke "semua yang mau opt-in", bukan cuma nice-to-have.
Seluruh dokumen ini menghasilkan banyak tabel & sinyal — pertanyaan penutupnya: siapa yang benar-benar memakai apa, untuk keputusan apa? Dikelompokkan per audiens supaya jelas siapa dapat output apa, bukan cuma "data ada" tanpa pemilik.
staff_coaching_case (§19.2) jadi daftar kasus konkret untuk dibahas 1-on-1 dengan staf, bukan feedback abstrak.personalized_offer_log (§19.1, §19.5) jalan sendiri dari sinyal minat sekunder & "almost bought" (§21.1) — tidak perlu tim manual pilih siapa dikirim apa.product_hint) per model, dibandingkan dengan penjualan aktual, menunjukkan model mana yang "diminati tapi tidak dibeli" (masalah harga/availability) vs "jarang dilirik" (kandidat discontinue).Penting supaya ekspektasi realistis — tidak semua output dari §22.1–22.5 bisa berjalan sendiri:
| Otomatis penuh (sistem jalan sendiri) | Butuh keputusan manusia |
|---|---|
Trigger personalized_offer_log dari sinyal minat sekunder |
Desain isi/nominal penawaran, kapan campaign dihentikan |
Flag staff_coaching_case dari pola non-closing |
Isi pembicaraan coaching, keputusan promosi/teguran |
Skor komposit harian staf (staff_score_daily) |
Keputusan insentif/bonus final, terutama kalau ada banding (§19.2 staff_appeal_log) |
| Alert antrean overflow & traffic pattern | Keputusan jadwal staf final (tetap mempertimbangkan cuti, ketersediaan, dll di luar data) |
| Heatmap & path-to-purchase report | Keputusan reposisi produk aktual (mempertimbangkan biaya pindah display, estetika toko, dll) |
| Toko health score & KPI dashboard | Keputusan strategis (ekspansi, tutup cabang, ganti principal) — data jadi salah satu input, bukan satu-satunya |
Prinsip umum: sistem ini dirancang untuk mempersempit & memprioritaskan pilihan manusia (mana yang perlu diperhatikan duluan), bukan menggantikan keputusan manusia sepenuhnya — konsisten dengan catatan korelasi≠sebab-akibat di §5.5 dan kebutuhan jalur banding di §17.5.
Dua ide bisnis konkret (di luar video analytics/gamifikasi digital) yang justru menyambung rapi ke sistem yang sudah dirancang — dijadikan section terpisah karena ini perluasan model bisnis, bukan cuma fitur teknis.
Mekanik: konsumen yang beli jam dapat servis ganti baterai gratis selamanya, dengan notifikasi proaktif tiap 6 bulan — bukan menunggu baterai benar-benar habis (baterai quartz secara teknis bisa tahan 1–2 tahun), tapi dijadwalkan sebagai "cek rutin gratis" tiap 6 bulan. Biaya ke perusahaan nyaris nol (baterai belum tentu diganti tiap kunjungan kalau memang belum habis — cek rutin bisa jadi cuma servis kecil/kalibrasi), tapi nilai yang dirasakan konsumen tinggi dan frekuensi kontaknya jauh lebih terjadwal.
Kenapa siklus 6 bulan (bukan nunggu baterai habis) adalah keputusan tepat:
- 2x kontak wajib per tahun — dua kali lebih sering dari asumsi siklus baterai alami (1–2 tahun). Ini yang menjadikan servis baterai jantung utama seluruh sistem loyalitas (§25), bukan cuma perk pelengkap.
- Titik kontak untuk info rilisan baru — staf dapat 2 kesempatan per tahun per member untuk mengangkat topik koleksi terbaru, tanpa terasa "jualan paksa" karena bungkusnya "cek rutin gratis".
- Sumber data tambahan untuk sistem tracking — tiap kunjungan adalah momen loyalty_id yang pasti diketahui (§1) — 2x lebih sering dari asumsi awal, memperkaya sinyal reinforcement minat (§21.2) jauh lebih cepat.
- Basis mekanik streak — karena jadwalnya tetap (tiap 6 bulan), ini bisa digamifikasi jadi "streak" konsistensi (§25.2) — member yang rutin datang tepat waktu dapat bonus, mirip pola check-in harian di app lain tapi disesuaikan ke kadensi 6 bulanan yang realistis untuk jam tangan.
Mekanik: konsumen tukar jam lama dengan jam baru + bayar selisih (xxx). Jam lama dinilai internal (Class A/B/C atau reject), direfurbish (memungkinkan karena perusahaan adalah produsennya sendiri — akses ke spare part asli & standar QC), lalu dijual kembali sebagai unit refurbished tidak bisa ditukar lagi — sekaligus positioning ramah lingkungan (circular economy).
Ini pola yang sudah terbukti di industri lain (trade-in program Apple, Certified Pre-Owned Rolex) — masuk akal justru karena perusahaan adalah produsen sendiri, bukan reseller yang harus menebak kualitas barang bekas.
Update setelah dicek dengan margin riil (~3x, artinya COGS ≈ 33% dari harga jual — margin tinggi khas retail jam tangan): diskon 50% flat tidak worth it. Bukan cuma soal margin persentase tergerus, tapi masalah yang lebih dalam: setelah dikurangi biaya refurbish, ruang yang tersisa untuk trade-in credit ke konsumen jadi terlalu kecil — program jadi tidak menarik dari sisi konsumen yang mau nukar, bukan cuma dari sisi margin perusahaan.
Contoh konkret (harga jam baru Rp10.000.000, margin 3x → COGS ≈ Rp3.330.000):
| Skenario diskon | Harga jual refurbish | Target COGS refurbish (margin% dijaga) | Biaya refurbish (servis+baterai+QC, ~Rp700rb) | Sisa ruang untuk trade-in credit | Kesimpulan |
|---|---|---|---|---|---|
| 50% off | Rp5.000.000 | Rp1.670.000 | Rp700.000 | ~Rp970.000 | Terlalu kecil untuk jam bernilai jutaan — konsumen merasa tidak dihargai, program sepi peminat |
| 20% off | Rp8.000.000 | Rp2.670.000 | Rp700.000 | ~Rp1.970.000 | Hampir 2x lipat ruang trade-in credit dibanding 50% off — margin tetap terlindungi dan trade-in credit lebih realistis |
Insight kunci: besar diskon ke konsumen akhir secara langsung menentukan seberapa besar trade-in credit yang bisa ditawarkan ke konsumen yang nukar — diskon besar bukan cuma "biaya buat perusahaan", tapi juga "mencekik" budget trade-in credit, jadi merugikan di dua sisi sekaligus.
Rekomendasi: diskon bertingkat sesuai grade (§23.2), bukan flat 50%:
| Grade | Diskon dari harga baru | Alasan |
|---|---|---|
| A (nyaris mulus, minim refurbish) | 15–20% | Biaya refurbish rendah, kondisi mendekati baru — diskon kecil tetap terasa layak buat konsumen |
| B (ada tanda pakai, perlu servis sedang) | 25–30% | Biaya refurbish sedang, diskon lebih besar untuk kompensasi kondisi |
| C (banyak tanda pakai, butuh penggantian part) | 35–40%, atau jangan dijual sebagai refurbish sale | Kalau target COGS di grade ini sudah tidak realistis, lebih baik dikanibal jadi spare part daripada dipaksa dijual rugi |
Formula operasional per grade: trade_in_credit_diberikan + refurb_cost_estimate ≤ target_COGS_refurbish(grade) — pagar ini yang dipakai staf saat menilai grade & menentukan nilai tukar tambah di trade_in_appraisal (§23.4). refurb_inventory.listed_price (§23.4) jadi bervariasi per grade, bukan flat 50%.
-- Perluasan transaksi (POS existing, §10) — tambah kolom tipe transaksi
-- transaksi.transaction_type: 'new_sale' | 'battery_service' | 'trade_in' | 'refurb_sale'
-- Penilaian trade-in — 1 baris tiap kali konsumen bawa jam lama untuk ditukar
CREATE TABLE trade_in_appraisal (
id SERIAL PRIMARY KEY,
loyalty_id TEXT REFERENCES member(loyalty_id),
transaksi_id TEXT, -- transaksi trade-in terkait (transaction_type='trade_in')
watch_model_traded TEXT,
grade TEXT, -- 'A' | 'B' | 'C' | 'reject'
trade_in_credit NUMERIC, -- nilai kredit yang diberikan ke konsumen
refurb_cost_estimate NUMERIC,
appraised_by TEXT, -- staff yang menilai (cashier_id, §10)
appraised_at TIMESTAMP NOT NULL,
photos_ref TEXT -- foto before/after — dasar audit konsistensi grading antar staf/toko
);
-- Inventori unit refurbish — hasil dari trade_in_appraisal yang lolos grade (bukan reject)
CREATE TABLE refurb_inventory (
refurb_unit_id TEXT PRIMARY KEY,
source_appraisal_id INT REFERENCES trade_in_appraisal(id),
grade TEXT,
refurb_completed_at TIMESTAMP,
listed_price NUMERIC, -- bervariasi per grade (§23.3): ~80-85% (A) / ~70-75% (B) / ~60-65% (C) dari harga baru model setara
target_cogs NUMERIC, -- dari formula §23.3 (Skenario B)
sold_transaksi_id TEXT, -- null kalau belum terjual (transaction_type='refurb_sale')
non_exchangeable BOOLEAN NOT NULL DEFAULT TRUE -- eksplisit: unit ini tidak bisa di-trade-in lagi
);
-- Log servis baterai — touchpoint loyalitas, BUKAN transaksi penjualan (§23.1)
CREATE TABLE battery_service_log (
id SERIAL PRIMARY KEY,
loyalty_id TEXT REFERENCES member(loyalty_id),
store_id TEXT,
watch_model TEXT,
staff_id TEXT,
serviced_at TIMESTAMP NOT NULL,
next_due_at TIMESTAMP NOT NULL, -- serviced_at + 6 bulan, dihitung saat insert — basis reminder §24.6 & streak §25.2
was_on_time BOOLEAN, -- true kalau serviced_at <= next_due_at kunjungan SEBELUMNYA + grace period (mis. 2 minggu) — basis streak
notified_new_release BOOLEAN DEFAULT FALSE -- flag: apakah staf sempat sampaikan info koleksi baru di kunjungan ini
);
-- Reminder 6-bulanan — 1 baris dijadwalkan tiap kali battery_service_log baru diinsert (next_due_at dari baris itu)
CREATE TABLE battery_reminder_log (
id SERIAL PRIMARY KEY,
loyalty_id TEXT REFERENCES member(loyalty_id),
due_at TIMESTAMP NOT NULL, -- = battery_service_log.next_due_at yang memicunya
sent_at TIMESTAMP, -- kapan WhatsApp reminder benar-benar terkirim (biasanya H-7 dari due_at)
responded_at TIMESTAMP -- diisi begitu member benar-benar datang servis lagi (link ke battery_service_log baru)
);
Catatan integrasi: zone_definition (§10) sudah cukup untuk area "refurbished corner" di toko — tinggal set product_category = 'refurbished', tanpa perlu kolom baru. Dwell time & minat di zona ini otomatis ter-track lewat zone_dwell_segment yang sudah ada, sehingga bisa dibandingkan minat ke refurbished vs ke produk baru — insight langsung tanpa infrastruktur tambahan.
| Risiko | Penjelasan | Mitigasi |
|---|---|---|
| Kanibalisasi penjualan baru | Kalau diskon refurbish terlalu besar dibanding kondisinya, sebagian calon pembeli baru bisa geser ke refurbish | Sudah dimitigasi via diskon bertingkat by grade (§23.3, 15–40%, bukan flat 50%) — ditambah batasi refurbish ke model lama/discontinued, bukan rilisan terbaru |
| Konsistensi grading | Class A/B/C bisa subjektif antar staf/toko kalau tidak ada standar jelas | SOP grading tertulis + photos_ref wajib di trade_in_appraisal untuk audit |
| Siklus trade-in tak berujung | Tanpa aturan jelas, unit refurbish bisa di-trade-in lagi, bikin akuntansi & kualitas makin sulit dikontrol | non_exchangeable = true eksplisit di skema (§23.4), dikomunikasikan jelas ke konsumen saat beli refurbish |
| Klaim ramah lingkungan tanpa bukti | "Ramah lingkungan" sebagai marketing tanpa angka konkret berisiko dianggap greenwashing | Laporkan angka nyata (mis. "X unit diselamatkan dari limbah tahun ini") dari data refurb_inventory, bukan klaim umum |
| Margin meleset kalau salah skenario (§23.3) | Kalau tim finance/marketing asumsi margin nominal padahal secara matematis tidak feasible, program bisa rugi tanpa disadari sampai laporan keuangan keluar | Validasi skenario A vs B (§23.3) di depan, sebelum program diluncurkan — bukan setelah berjalan |
loyalty_id yang pasti diketahui per kunjungan — transaksi beli (§5.3) dan servis baterai (§23.1) — bukan cuma menunggu pembelian berikutnya.track_transaction_match — kunjungan trade-in bisa di-treat sama seperti closing biasa (§5.2), tinggal tambah transaction_type di join logic.staff_score_daily (§17.2, §19.2) kalau nanti mau dievaluasi juga sebagai bagian performa staf, bukan cuma closing rate jam baru.Sejauh ini gamifikasi konsumen (§16-B) baru berupa daftar ide yang tersebar. Section ini menyatukannya jadi satu strategi loyalitas yang koheren, dipetakan eksplisit dari tiap mekanik Haidilao (§18, 3 riset eksternal) ke konteks toko jam tangan — bukan cuma video tracking, tapi keseluruhan hubungan dengan konsumen.
Playbook Haidilao dibangun di atas satu fakta: orang makan tiap minggu, jadi ada banyak kesempatan alami untuk engagement berulang. Jam tangan sebaliknya — orang beli jam sekali per beberapa tahun. Kalau strategi loyalitas cuma dibangun di sekitar momen pembelian, brand akan "hilang" dari radar konsumen selama bertahun-tahun di antara pembelian.
Tapi ada 2 touchpoint yang justru dimiliki toko jam dan TIDAK dimiliki restoran: - Servis baterai berkala (§23.1) — kunjungan wajib tiap 6 bulan (notifikasi proaktif, bukan menunggu baterai habis), dengan alasan legitimate (bukan dipaksakan) untuk konsumen datang ke toko — 2x setahun, jauh lebih sering dari siklus baterai alami. - Trade-in/upgrade (§23.2) — momen "naik kelas" yang justru tidak ada padanannya di F&B sama sekali (orang tidak "tukar tambah" makanan).
Insight strategis kunci: strategi loyalitas toko jam harus dibangun di sekitar momen servis, bukan cuma momen pembelian — ini pengganti "makan lagi minggu depan" ala restoran, dan sudah match dengan §23 yang baru dirancang.
| Mekanik Haidilao | Fungsi Psikologis (§18) | Adaptasi untuk Toko Jam | Status |
|---|---|---|---|
| Waiting area entertainment (origami, manikur, arcade) | Resiprositas, sunk cost fallacy, alihkan persepsi waktu tunggu | Waktu tunggu servis baterai (15–30 menit) — mini-quiz produk via WhatsApp sambil menunggu (§24.3) | Baru — sebelumnya §16-B "Queue Quest" generik tanpa momen nyata untuk hidup di dalamnya |
| Secret codes / social hacks | Social proof, status insider, viral UGC | Frasa khusus terkait servis/produk untuk unlock bonus kecil (deterministik, bukan random — §6) | Adaptasi, prioritas rendah |
| Tiered membership + efek "near-miss" | Status seeking, loss aversion, progresi | Tier berbasis spend + kunjungan servis + trade-in (bukan cuma spend) — §24.5 | Diperkuat di section ini |
| Birthday benefits | Emotional attachment, katalis group booking | Sangat relevan — jam tangan produk gifting alami (ulang tahun, anniversary, wisuda) | Sudah ada di §16-B, diperkuat konteksnya |
| Co-creation (racik saus sendiri) → IKEA effect | Nilai dirasakan naik karena ikut "membuat" | Personalisasi/gravir case-back — momen paling natural untuk jam tangan, versi asli dari mekanik ini | Baru — insight paling kuat dari analisis ini |
| Pertunjukan tableside (Noodle Dance) | Halo effect, teatrikal, momen shareable | Reveal movement lewat exhibition caseback untuk jam mekanik — momen visual yang cuma dimiliki jam, bukan quartz biasa | Baru — spesifik ke kategori produk |
| Surprise & delight (staf beri hadiah spontan) | Endowment effect, reward variabel | Budget kecil staf untuk gestur spontan (kartu ucapan, cleaning gratis saat servis) | Sudah ada di §16-A, diperluas |
| Referral rewards | Akuisisi via jaringan sosial | Sudah dirancang skemanya (§19.1 referral_log) |
Sudah ada |
| Reaktivasi member lapse | Bawa kembali user dorman | Reminder servis baterai jadi trigger reaktivasi paling natural — lihat §24.6 | Diperkuat — sebelumnya cuma "90 hari tidak aktif" generik |
| Gamifikasi HR internal (micro-promotion, badge) | Motivasi staf Gen Z | Sudah dirancang mendalam (§17) | Sudah ada |
Haidilao mengeksploitasi waktu tunggu meja sebagai engagement surface utama. Toko jam biasanya tidak punya antrean besar — tapi servis baterai/reparasi kecil (15–30 menit) adalah momen tunggu yang setara, dan sebelumnya belum dimanfaatkan di desain manapun di dokumen ini.
battery_service_log (§23.4) jadi trigger untuk mission_definition type queue_quest — staf tinggal kirim link saat mencatat servis, tanpa infrastruktur tambahan.Dua mekanik Haidilao yang paling ikonik (Noodle Dance, racik saus sendiri) punya padanan yang justru lebih natural di jam tangan dibanding kebanyakan kategori retail lain:
Kenapa ini penting dimasukkan: dua momen ini adalah yang paling "Instagram-able" dari seluruh pengalaman beli jam — replikasi langsung dari insight §9 (Dampak Kuantitatif) gemini-1.md bahwa service innovation adalah faktor paling kuat ke loyalitas (skor 0,79), lebih kuat dari service quality standar.
Sebelumnya (§11, §16-B) skema tier masih berupa beberapa opsi tanpa keputusan. Berdasarkan analisis ini, rekomendasi final:
Threshold tier berbasis 3 dimensi (bukan cuma spend seperti kebanyakan program loyalitas retail biasa):
Skor_tier = spend_12_bulan (60%) + jumlah_kunjungan_termasuk_servis (25%) + aktivitas_trade_in (15%)
Alasan bobot ini: - Spend tetap dominan (60%) — tetap harus jadi basis utama karena mencerminkan nilai ekonomi riil member ke bisnis. - Kunjungan termasuk servis baterai (25%) — ini yang membedakan dari program loyalitas retail generik: member yang rajin servis (jadi sering ke toko, sering dengar info produk baru) dihargai, bukan cuma yang belanja besar sekali lalu hilang. - Trade-in (15%) — sinyal member yang aktif "upgrade siklus", biasanya jadi pelanggan jangka panjang paling bernilai.
| Tier (nama contoh — ganti sebelum dipakai, hindari nama Haidilao asli seperti §6) | Threshold Skor_tier | Privilese |
|---|---|---|
| Dasar | Baru terdaftar | Poin dasar per pembelian & servis |
| Menengah | Skor setara ~2 kunjungan servis + 1 pembelian/tahun | Prioritas booking servis, voucher ulang tahun |
| Utama | Skor setara pembelian rutin + servis konsisten + minimal 1 trade-in | Personalisasi gravir gratis, priority access rilisan baru |
| Kolektor | Skor tertinggi, riwayat multi-trade-in/multi-pembelian | Undangan private preview koleksi baru, dedicated service line |
Implikasi ke data model (§19.1): points_ledger.event_type perlu ditambah 'earn_battery_service' dan 'earn_trade_in' (sebelumnya cuma earn_transaksi), supaya kunjungan servis & trade-in ikut menyumbang ke perhitungan tier, bukan cuma transaksi beli.
Karena siklus beli jam sangat panjang, loop retensi/reaktivasi ala Haidilao (yang berasumsi orang makan lagi minggu depan) perlu disesuaikan:
battery_service_log.next_due_at (§23.4) jadi jadwal pasti — reminder terkirim otomatis H-7 sebelum jatuh tempo lewat battery_reminder_log, jauh lebih personal & terprediksi daripada reminder generik "90 hari tidak aktif".Konsolidasi §11, §16, §19.1, §23, §24 jadi satu sistem konkret siap-eksekusi — bukan lagi daftar opsi terbuka. Angka di bawah ini starting point yang perlu dikalibrasi ke data harga/COGS riil sebelum diluncurkan (lihat §25.6), tapi strukturnya sudah final.
Konsumen daftar (WhatsApp-first, §16-B)
│
▼
Poin didapat dari: pembelian, servis baterai, trade-in, referral,
personalisasi, misi tunggu (Queue Quest) — semua deterministik (§6)
│
▼
Total poin 12-bulan-rolling → tentukan Tier (§25.3)
│
▼
Poin ditukar reward (§25.4) ATAU tier buka privilese otomatis
│
▼
Loop retensi: reminder servis baterai (§24.6) + occasion trigger
menjaga member tetap aktif walau siklus beli jam panjang
Basis: 1 poin = Rp10.000 spend (mengikuti pola "Rasa Coins" di §11, gampang dijelaskan ke konsumen). Aktivitas non-spend dapat poin flat supaya semua touchpoint di §24.2 punya insentif jelas, bukan cuma pembelian.
| Aktivitas | Poin | Tipe (points_ledger.event_type, §19.1/§24.5) |
Catatan |
|---|---|---|---|
| Pembelian jam baru | 1 poin / Rp10.000 | earn_transaksi |
Basis utama, proporsional nilai riil ke bisnis |
| Servis baterai 6-bulanan (gratis) | +50 poin flat | earn_battery_service (baru, §24.5) |
Menghargai kunjungan meski servisnya sendiri gratis — ini yang paling sering terjadi (2x/tahun), jadi kontributor poin paling konsisten |
| Streak servis tepat waktu | +25 poin bonus / kunjungan ke-2 berturut-turut yang was_on_time = true (§23.4), naik +25 tiap streak berlanjut (cap wajar, mis. maks +100) |
earn_mission type on_time_streak |
Baru — hadiahi konsistensi datang sesuai jadwal 6-bulanan, bukan cuma datang sekali-sekali |
| Trade-in diproses | +200 poin flat | earn_trade_in (baru, §24.5) |
Terlepas dari nilai trade-in — mendorong partisipasi ke program §23.2 |
| Personalisasi/gravir saat beli | +30 poin | earn_mission |
Dorong opt-in ke momen paling shareable (§24.4) |
| Queue Quest (kuis saat tunggu servis) | +10–20 poin | earn_mission |
Via mission_definition type queue_quest (§24.3) — otomatis terpicu tiap kunjungan servis 6-bulanan |
| Table/Counter Mission (coba ≥2 model) | +25 poin | earn_mission |
Via mission_definition type table_mission |
| Referral berhasil (referee transaksi pertama) | +100 poin (referrer) / +50 poin (referee, welcome) | referral_bonus |
Sesuai referral_log (§19.1) |
| Bonus ulang tahun | +100 poin otomatis | birthday_bonus |
Trigger dari birthday_broadcast_log (§19.1) |
Streak sebagai mekanik utama (baru): karena jadwal servis sekarang tetap (tiap 6 bulan, §23.1), ini jadi kandidat kuat buat mekanik "streak" ala aplikasi kebiasaan (Duolingo, dst) tapi disesuaikan ke kadensi yang realistis untuk jam tangan — bukan harian, tapi tiap 6 bulan. battery_service_log.was_on_time (§23.4) jadi basis hitungnya: 2 kunjungan tepat waktu berturut-turut = 1 tahun penuh "on track", cukup jadi pencapaian yang terasa berarti tanpa perlu member mikir tiap hari.
Semua deterministik — tidak ada elemen acak (guardrail UGB, §6).
Penyederhanaan dari formula 3-dimensi di §24.5: karena aturan poin di §25.2 sudah otomatis mencerminkan spend + kunjungan servis + trade-in, tier cukup dihitung dari total poin earned 12-bulan-rolling (bukan saldo — poin yang sudah ditukar tetap dihitung untuk status tier, supaya menukar reward tidak menghukum status member).
| Tier | Threshold (poin earned/12bln) | Kira-kira setara | Privilese |
|---|---|---|---|
| Dasar | 0+ | Member baru terdaftar | Poin dasar semua aktivitas §25.2 |
| Menengah | 500+ | ~2 servis 6-bulanan (1 tahun penuh on-time + streak bonus) + 1 pembelian kelas menengah | Voucher ulang tahun lebih besar, prioritas booking servis |
| Utama | 1.500+ | Pembelian rutin + servis konsisten + minimal 1 trade-in | Personalisasi gravir gratis, priority access rilisan baru |
| Kolektor | 4.000+ | Riwayat multi-pembelian/multi-trade-in | Undangan private preview, dedicated service line |
tier_history (§19.1) dihitung ulang tiap ada points_ledger baru — pakai efek "near-miss" (§18, gemini-1.md): tampilkan ke member berapa poin lagi menuju tier berikutnya di ringkasan WhatsApp (§18, chatgpt-1.md pola "kamu dapat X poin, tinggal Y lagi").
| Poin | Reward | Kenapa Dipilih |
|---|---|---|
| 100 | Servis pembersihan/poles gratis | Biaya marginal rendah, dorong kunjungan lagi |
| 300 | Penggantian tali/strap | Item fisik bernilai rendah-menengah, COGS terkontrol |
| 800 | Voucher diskon 10% pembelian berikutnya | Dorong upgrade/pembelian kedua |
| 1.500 | Personalisasi gravir premium + priority service | Selaras §24.4, biaya marginal rendah (cuma jasa) |
| 3.000 | Akses private preview koleksi baru + voucher besar | Reward tier atas, prioritaskan retensi Kolektor |
Prinsip dari riset (§18, chatgpt-1.md): utamakan reward berupa item/jasa (biaya marginal rendah bagi perusahaan, nilai dirasakan tinggi bagi konsumen) dibanding voucher setara-cash — supaya biaya reward tetap di kisaran 0,8–1,5% dari sales member (§11) alih-alih tergerus diskon tunai.
| Tahap | Apa yang terjadi | Mekanik gamifikasi yang aktif |
|---|---|---|
| Discovery | Lihat konten/koleksi di toko atau medsos | — (belum jadi member) |
| Kunjungan pertama | Browsing di toko, video analytics jalan (§1) tapi anonim | — |
| Pembelian pertama | Closing di kasir, tawarkan registrasi WhatsApp-first (§16-B) | Poin pembelian (§25.2), opsi personalisasi (+30 poin), battery_service_log.next_due_at diset otomatis +6 bulan dari tanggal beli |
| Reminder H-7 (bulan ke-5,75) | WhatsApp otomatis dari battery_reminder_log (§23.4) |
Ajak datang cek rutin + sneak-peek rilisan baru kalau ada |
| Servis 6-bulanan #1 | Kunjungan wajib, gratis | +50 poin, Queue Quest saat tunggu, info rilisan baru, next_due_at baru diset lagi +6 bulan |
| Ulang tahun | Trigger otomatis (bisa jatuh di antara 2 siklus servis) | +100 poin, voucher sesuai tier |
| Servis 6-bulanan #2 (tepat waktu) | Loop berulang, jantung retensi (§24.1) | +50 poin + streak bonus +25 poin (2 kunjungan on-time berturut-turut = 1 tahun penuh), naikkan tier bertahap |
| Servis #3, #4, dst | Streak berlanjut kalau tetap tepat waktu | Bonus streak naik (cap wajar), makin kuat alasan untuk tidak skip jadwal |
| Trade-in / upgrade | Konsumen tukar jam lama (§23.2) | +200 poin, kemungkinan naik tier langsung |
| Referral | Konsumen ajak teman | +100/+50 poin, referral_log |
| Reaktivasi (lewat jatuh tempo servis 6-bulanan tanpa respons) | WhatsApp reminder susulan otomatis (§24.6) | Trigger personalized_offer_log, streak ter-reset kalau kelewat jauh dari grace period |
Angka di §25.2–§25.4 adalah starting point terstruktur, bukan angka final — sebelum eksekusi, kalibrasi ke: - COGS riil tiap item reward (§25.4) — supaya biaya reward tetap dalam target 0,8–1,5% sales member (§11). - Rata-rata harga jual & margin riil (§23.3) — supaya nilai poin (Rp10.000 = 1 poin) proporsional dengan margin, bukan cuma ditiru dari referensi Haidilao. - Threshold tier (§25.3) — sesuaikan dengan distribusi spend & frekuensi servis aktual konsumen, bukan angka ilustratif ini. - Cek ulang guardrail UGB (§6) tiap kali ada mekanik baru ditambahkan — pastikan tetap deterministik.
Ide: pasang kamera/sensor kecil di tiap etalase yang mendeteksi slot mana yang diangkat, lalu dicocokkan ke track_id/loyalty_id lewat timestamp + zona — bukan lewat koneksi langsung antar kamera. Ini lebih murah dan lebih presisi dari pendekatan Layer 2 VLM yang sudah ada, bukan tambahan kompleksitas — karena reuse pola join yang sama persis dengan track_transaction_match (§5.1) dan zone_dwell_segment (§10).
Layer 2 VLM (§4) saat ini menebak product_hint dari gambar — butuh model yang cukup pintar untuk "membaca" jam model apa yang kelihatan, dan bisa salah antara 2 model yang mirip. Pendekatan etalase ini membalik soalnya: karena slot → SKU sudah diketahui pasti dari data inventori, kamera per etalase cuma perlu jawab pertanyaan biner "slot ini kosong atau tidak?" — deteksi perubahan sederhana (frame differencing / background subtraction), bukan klasifikasi visual. Ini:
- Tidak butuh resolusi tinggi (beda dari kebutuhan baca detail wajah/model produk).
- Tidak butuh VLM sama sekali untuk identifikasi produk — product_hint jadi ground truth SKU-level, bukan tebakan.
- Mengurangi panggilan VLM yang sebelumnya dipakai untuk produk (Layer 2 masih relevan untuk interaction_type — mencoba vs sekadar pegang — tapi bukan lagi untuk "produk apa").
Kalau konsumen cuma lihat-lihat tanpa mengangkat barang, sinyalnya otomatis turun ke level yang sudah ada: zone_dwell_segment (dwell time di etalase itu, level zona) — bukan presisi SKU, tapi tetap tercatat sebagai minat. Sistem punya 2 tingkat presisi yang saling melengkapi, bukan 2 sistem terpisah:
| Kondisi | Presisi | Sumber |
|---|---|---|
| Item diangkat dari slot | SKU-level pasti | slot_event (baru, §26.3) |
| Cuma dilihat/berdiri di depan etalase | Level zona/etalase | zone_dwell_segment (§10, sudah ada) |
-- Mapping slot fisik di etalase → SKU, diupdate manual tiap rotasi barang (mirip zone_definition.product_category, lebih granular)
CREATE TABLE etalase_slot_definition (
slot_id TEXT PRIMARY KEY,
zone_id TEXT REFERENCES zone_definition(zone_id),
store_id TEXT NOT NULL,
sku TEXT NOT NULL, -- kode model jam persis
position_label TEXT, -- mis. "baris 2, kolom 3" — buat memudahkan staf update manual
active_from DATE,
active_to DATE -- null = masih di slot itu
);
-- Event mentah angkat/taruh kembali, dari kamera/sensor per etalase — deteksi perubahan sederhana, BUKAN VLM
-- 1 baris per kejadian ('lifted' saat diangkat, 'returned' saat ditaruh kembali) — mentah, belum dipasangkan
CREATE TABLE slot_event (
id SERIAL PRIMARY KEY,
slot_id TEXT REFERENCES etalase_slot_definition(slot_id),
event_type TEXT NOT NULL, -- 'lifted' | 'returned'
detected_at TIMESTAMP NOT NULL
);
-- Sesi pegang — pasangan (lifted, returned) yang sudah digabung jadi 1 baris dengan durasi terhitung,
-- konsisten dengan pola dwell_seconds/interaction_seconds di tabel lain (§10) — bukan dibiarkan mentah
CREATE TABLE slot_hold_session (
id SERIAL PRIMARY KEY,
slot_id TEXT REFERENCES etalase_slot_definition(slot_id),
lifted_event_id INT REFERENCES slot_event(id),
returned_event_id INT REFERENCES slot_event(id), -- null kalau belum ditaruh kembali (masih dipegang / sesi belum selesai)
lifted_at TIMESTAMP NOT NULL,
returned_at TIMESTAMP,
hold_seconds INT -- = returned_at - lifted_at, dihitung sekali saat returned_event masuk
);
-- Join layer: cocokkan 1 sesi pegang ke track_id yang paling mungkin (posisi + waktu terdekat di zona yang sama)
CREATE TABLE slot_track_match (
id SERIAL PRIMARY KEY,
slot_hold_session_id INT REFERENCES slot_hold_session(id),
track_id TEXT REFERENCES track_event(track_id),
match_confidence TEXT, -- 'single_person_in_zone' | 'closest_by_time' | 'ambiguous_multi_person' — transparansi soal keandalan matching
matched_at TIMESTAMP NOT NULL
);
Logika matching (batch job, sama pola dengan §5.1): tiap slot_event bertipe returned dipasangkan dengan lifted terdekat sebelumnya di slot_id yang sama → jadi 1 baris slot_hold_session dengan hold_seconds terhitung otomatis. Lalu untuk tiap sesi, cari track_event/zone_dwell_segment yang aktif di zone_id yang sama pada rentang lifted_at–returned_at — kalau cuma ada 1 orang di zona itu saat itu, match_confidence = 'single_person_in_zone' (paling andal, ini yang terjadi di contoh §26.2 barusan). Kalau ada lebih dari 1 orang, pakai jarak waktu terdekat sebagai fallback (closest_by_time), atau tandai ambiguous_multi_person kalau terlalu tidak pasti untuk dipakai sebagai basis keputusan bisnis (mis. jangan dipakai untuk atribusi skor staf individual, tapi tetap valid untuk agregat "SKU ini sering diangkat").
etalase_slot_definition tiap rotasi barang, mirip kebutuhan update cashier_marker_map (§10) tiap rotasi staf.match_confidence, tapi tetap perlu kebijakan jelas: data ambiguous_multi_person sebaiknya dipakai untuk insight agregat (§13-D, §21) saja, bukan untuk keputusan yang butuh presisi individual (mis. atribusi closing ke staf tertentu, §17).interaction_type, bukan lagi menebak SKU. Estimasi biaya di §12 kemungkinan turun lebih jauh.product_hint yang sebelumnya "tebakan VLM" sekarang jadi ground truth — memperkuat semua insight yang bergantung padanya (path-to-purchase, minat sekunder, comparison-shopping).secondary_interest_signal): bisa di-upgrade akurasinya begitu slot_track_match tersedia — zone_id yang sebelumnya jadi proxy kasar untuk "minat ke kategori" sekarang bisa dipersempit ke SKU spesifik.Dokumen ini di-generate otomatis dari README.md — untuk versi paling mutakhir, rujuk file sumber di project. Dua ringkasan pendamping tersedia terpisah: presentasi eksekutif (non-teknis) dan referensi teknis (arsitektur & skema data).