Lewati ke konten utama

Basis Data

Di produksi, satu database PostgreSQL (laravel_db) dibagi menjadi tiga schema.


Tiga schema, tiga koneksi

Koneksisearch_pathDipakai model dengan
pgsql (default)publicModel tanpa trait (mis. User)
pgsql_crmcrm,publictrait BelongsToCrmSchema
pgsql_aiai,publictrait BelongsToAiSchema
simrs— (MySQL)Hanya dibaca SyncManager
Kenapa search_path, bukan nama tabel bertitik

Kalau 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

Relasi lintas-schema tanpa foreign key

Tiga relasi ini sengaja tanpa constraint FK — cukup kolom terindeks, karena constraint lintas-schema tidak diinginkan:

DariKeKolom
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 ContactWhatsappMessage dan ContactChatbotSession juga bukan FK: keduanya dijodohkan lewat nomor telepon (contacts.numberphone_number), karena itulah identitas yang dipakai gerbang WhatsApp.


Tabel audit sinkronisasi


Kolom terenkripsi

Disimpan terenkripsi otomatis lewat cast Eloquent:

ModelKolom
AiAgentllm_api_key
WhatsappBusinessAccountgateway_api_key, gateway_api_secret, gateway_client_secret, gateway_password
APP_KEY adalah kunci semuanya

Kehilangan 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.

TabelConstraint
patientsgender IN ('M','F') atau null · loyalty_tier IN ('Regular','Silver','Gold','Platinum')
patient_insurancetype IN ('BPJS','Swasta','Mandiri')
doctor_schedulesday_of_week BETWEEN 0 AND 6
appointmentsstatus IN ('Scheduled','Confirmed','Completed','Cancelled','Rescheduled')
queuesstatus IN ('Waiting','Called','In-Service','Done','Skipped')
billingsstatus IN ('Unpaid','Partial','Paid','Refunded','Disputed')
paymentspayment_method IN ('VA','QRIS','E-Wallet','Credit-Card','BPJS','Cash') atau null · status IN ('Pending','Success','Failed','Expired','Refunded')
loyalty_points_logchange_type IN ('Earn','Redeem','Expire','Adjust')
whatsapp_messagesdirection IN ('IN','OUT') · message_type IN ('Text','Template','Media','Interactive','System') atau null · status IN ('Queued','Sent','Delivered','Read','Failed')
chatbot_sessionsstatus IN ('Active','Closed','Escalated')
chatbot_messagesrole IN ('user','assistant','system')
complaintspriority IN ('Low','Medium','High','Critical') · status IN ('Open','In-Progress','Resolved','Closed')
surveyscsat_score BETWEEN 1 AND 5 · nps_score BETWEEN 0 AND 10
insurance_claimsstatus IN ('Submitted','Processing','Approved','Rejected','Paid')
booking_requestsstatus IN ('Pending','Paid','Expired','Cancelled','Failed')

Migrasi ini idempoten: ia berhenti lebih awal kalau patients_gender_check sudah ada.


Indeks penting

TabelIndeksKegunaan
booking_requests(status, expires_at)Sapuan expireDue()
booking_requestspatient_id, payment_idLookup rekonsiliasi
whatsapp_messagespatient_idTimeline pasien
whatsapp_messagessent_by_user_idAtribusi agen
contactsassigned_user_idFilter percakapan per agen
chat_label_contactunik (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.

TabelPrimary key
patientspatient_id
doctorsdoctor_id
polyclinicspoly_id
appointmentsappointment_id
billingsbilling_id
paymentspayment_id
queuesqueue_id
contactscontact_id
booking_requestsbooking_request_id
queue_counterscounter_id
insertGetId di PostgreSQL

PostgreSQL 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.