Basis Data
Di produksi, satu database PostgreSQL (laravel_db) dibagi menjadi tiga schema.
Tiga schema, tiga koneksi
| Koneksi | search_path | Dipakai model dengan |
|---|---|---|
pgsql (default) | public | Model tanpa trait (mis. User) |
pgsql_crm | crm,public | trait BelongsToCrmSchema |
pgsql_ai | ai,public | trait BelongsToAiSchema |
simrs | — (MySQL) | Hanya dibaca SyncManager |
search_path, bukan nama tabel bertitikKalau nama tabel ditulis crm.polyclinics, Grammar::wrapTable() milik Laravel salah menangani titik ketika ada prefix/schema. Karena itu koneksinya yang diarahkan lewat search_path, sehingga nama tabel tetap satu segmen (polyclinics).
Pada driver non-PostgreSQL (SQLite/MySQL untuk lokal & staging), kedua trait mengembalikan null — semua tabel hidup di satu database tanpa schema.
Peta relasi utama — klinis
Peta relasi utama — percakapan & AI
Tiga relasi ini sengaja tanpa constraint FK — cukup kolom terindeks, karena constraint lintas-schema tidak diinginkan:
| Dari | Ke | Kolom |
|---|---|---|
contacts (ai) | users (public) | assigned_user_id |
whatsapp_messages (crm) | users (public) | sent_by_user_id |
booking_requests (crm) | whatsapp_business_accounts (ai) | whatsapp_account_id |
Pivot chat_label_contact juga tanpa FK, meski kedua tabelnya sama-sama di schema ai — mengikuti pola yang sama.
Relasi Contact ↔ WhatsappMessage dan Contact ↔ ChatbotSession juga bukan FK: keduanya dijodohkan lewat nomor telepon (contacts.number ↔ phone_number), karena itulah identitas yang dipakai gerbang WhatsApp.
Tabel audit sinkronisasi
Kolom terenkripsi
Disimpan terenkripsi otomatis lewat cast Eloquent:
| Model | Kolom |
|---|---|
AiAgent | llm_api_key |
WhatsappBusinessAccount | gateway_api_key, gateway_api_secret, gateway_client_secret, gateway_password |
APP_KEY adalah kunci semuanyaKehilangan atau mengganti APP_KEY membuat seluruh kolom terenkripsi tidak bisa dibaca lagi. Kredensial gerbang dan API key LLM harus diisi ulang manual.
Kolom llm_api_key juga masuk daftar EXCLUDED_ATTRIBUTES di AiAgentKnowledgeDataService, bersama password dan remember_token — supaya tidak pernah bocor ke prompt AI.
CHECK constraint (hanya PostgreSQL)
Migrasi add_crm_postgres_check_constraints menambahkan constraint enum di level database. Ini yang membuat data SIMRS harus dibersihkan sebelum ditulis.
| Tabel | Constraint |
|---|---|
patients | gender IN ('M','F') atau null · loyalty_tier IN ('Regular','Silver','Gold','Platinum') |
patient_insurance | type IN ('BPJS','Swasta','Mandiri') |
doctor_schedules | day_of_week BETWEEN 0 AND 6 |
appointments | status IN ('Scheduled','Confirmed','Completed','Cancelled','Rescheduled') |
queues | status IN ('Waiting','Called','In-Service','Done','Skipped') |
billings | status IN ('Unpaid','Partial','Paid','Refunded','Disputed') |
payments | payment_method IN ('VA','QRIS','E-Wallet','Credit-Card','BPJS','Cash') atau null · status IN ('Pending','Success','Failed','Expired','Refunded') |
loyalty_points_log | change_type IN ('Earn','Redeem','Expire','Adjust') |
whatsapp_messages | direction IN ('IN','OUT') · message_type IN ('Text','Template','Media','Interactive','System') atau null · status IN ('Queued','Sent','Delivered','Read','Failed') |
chatbot_sessions | status IN ('Active','Closed','Escalated') |
chatbot_messages | role IN ('user','assistant','system') |
complaints | priority IN ('Low','Medium','High','Critical') · status IN ('Open','In-Progress','Resolved','Closed') |
surveys | csat_score BETWEEN 1 AND 5 · nps_score BETWEEN 0 AND 10 |
insurance_claims | status IN ('Submitted','Processing','Approved','Rejected','Paid') |
booking_requests | status IN ('Pending','Paid','Expired','Cancelled','Failed') |
Migrasi ini idempoten: ia berhenti lebih awal kalau patients_gender_check sudah ada.
Indeks penting
| Tabel | Indeks | Kegunaan |
|---|---|---|
booking_requests | (status, expires_at) | Sapuan expireDue() |
booking_requests | patient_id, payment_id | Lookup rekonsiliasi |
whatsapp_messages | patient_id | Timeline pasien |
whatsapp_messages | sent_by_user_id | Atribusi agen |
contacts | assigned_user_id | Filter percakapan per agen |
chat_label_contact | unik (chat_label_id, contact_id) | Satu label hanya sekali per kontak |
Primary key non-standar
Banyak tabel klinis memakai PK bernama khusus, bukan id. Ini penting saat menulis query mentah atau menambah syncer baru.
| Tabel | Primary key |
|---|---|
patients | patient_id |
doctors | doctor_id |
polyclinics | poly_id |
appointments | appointment_id |
billings | billing_id |
payments | payment_id |
queues | queue_id |
contacts | contact_id |
booking_requests | booking_request_id |
queue_counters | counter_id |
insertGetId di PostgreSQLPostgreSQL butuh nama kolom PK eksplisit untuk klausa RETURNING. Karena itu SyncManager dan RollbackService masing-masing menyimpan pemetaan tabel → kolom PK. Kalau menambah target tabel baru, dua-duanya harus diperbarui.
Migrasi
62 berkas migrasi, dijalankan berurutan. Kelompok besarnya:
Beberapa migrasi bersifat idempoten (mengecek hasTable/hasColumn lebih dulu) karena harus aman dijalankan di database yang skemanya sudah sebagian ada.