Lewati ke isi

Sentra Healthcare — Database Design

Database dirancang dengan PostgreSQL di cloud dan SQLite terenkripsi (SQLCipher) di server lokal klinik. Skema didesain dengan relasi ketat, pengindeksan berkinerja tinggi, dan audit trail yang tidak dapat diubah (immutable/append-only).

Entity-Relationship Diagram (ERD)

erDiagram
    PATIENTS {
        uuid id PK
        varchar rm_number UK
        varchar nik UK
        varchar ihs_id UK
        varchar full_name
        date birth_date
        varchar phone_number
        jsonb allergies
    }
    ENCOUNTERS {
        uuid id PK
        uuid patient_id FK
        uuid doctor_id FK
        uuid branch_id FK
        timestamp check_in_time
        varchar status
        varchar billing_type
        varchar satusehat_encounter_id
    }
    VITAL_SIGNS {
        uuid id PK
        uuid encounter_id FK
        numeric blood_pressure_systolic
        numeric blood_pressure_diastolic
        numeric body_temperature
        numeric heart_rate
        numeric respiratory_rate
        numeric oxygen_saturation
    }
    SOAP_NOTES {
        uuid id PK
        uuid encounter_id FK
        text subjective
        text objective
        text assessment_icd10
        text plan
        timestamp signed_at
        varchar signed_by_doctor_id
        text digital_signature_hash
    }
    PRESCRIPTIONS {
        uuid id PK
        uuid encounter_id FK
        varchar prescription_number UK
        varchar status
    }
    PRESCRIPTION_ITEMS {
        uuid id PK
        uuid prescription_id FK
        uuid medicine_id FK
        varchar dosage_instruction
        integer quantity
        uuid allocated_batch_id FK
    }
    MEDICINE_BATCHES {
        uuid id PK
        uuid medicine_id FK
        varchar batch_number
        date expiry_date
        integer stock_qty
    }
    BILLING_INVOICES {
        uuid id PK
        uuid encounter_id FK
        varchar invoice_number UK
        numeric total_amount
        numeric bpjs_coverage
        numeric patient_copayment
        varchar payment_status
    }
    AUDIT_LOGS {
        uuid id PK
        timestamp action_time
        uuid user_id
        varchar action_type
        varchar table_name
        uuid record_id
        jsonb old_values
        jsonb new_values
        varchar ip_address
    }

    PATIENTS ||--o{ ENCOUNTERS : has
    ENCOUNTERS ||--|| VITAL_SIGNS : measures
    ENCOUNTERS ||--|| SOAP_NOTES : records
    ENCOUNTERS ||--|| PRESCRIPTIONS : generates
    ENCOUNTERS ||--|| BILLING_INVOICES : bills
    PRESCRIPTIONS ||--o{ PRESCRIPTION_ITEMS : contains
    MEDICINE_BATCHES ||--o{ PRESCRIPTION_ITEMS : dispenses
    ENCOUNTERS ||--o{ AUDIT_LOGS : audits

Production PostgreSQL DDL Schema

-- Enable UUID extension
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";

-- 1. Table Patients
CREATE TABLE patients (
    id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
    rm_number VARCHAR(20) UNIQUE NOT NULL,
    nik CHAR(16) UNIQUE NOT NULL,
    ihs_id VARCHAR(50) UNIQUE,
    full_name VARCHAR(100) NOT NULL,
    gender VARCHAR(10) CHECK (gender IN ('Laki-laki', 'Perempuan')) NOT NULL,
    birth_date DATE NOT NULL,
    phone_number VARCHAR(20) NOT NULL,
    allergies JSONB DEFAULT '[]'::jsonb NOT NULL,
    created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
);

CREATE INDEX idx_patients_rm ON patients(rm_number);
CREATE INDEX idx_patients_nik ON patients(nik);
CREATE INDEX idx_patients_ihs ON patients(ihs_id);

-- 2. Table Encounters
CREATE TABLE encounters (
    id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
    patient_id UUID NOT NULL REFERENCES patients(id) ON DELETE RESTRICT,
    doctor_id UUID NOT NULL,
    branch_id UUID NOT NULL,
    check_in_time TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP NOT NULL,
    check_out_time TIMESTAMP WITH TIME ZONE,
    status VARCHAR(20) CHECK (status IN (
        'REGISTERED', 'TRIAGE', 'CONSULTATION', 'PHARMACY', 'BILLING', 'COMPLETED', 'CANCELLED'
    )) DEFAULT 'REGISTERED' NOT NULL,
    billing_type VARCHAR(20) CHECK (billing_type IN (
        'CASH', 'BPJS', 'INSURANCE', 'COMPLIMENTARY'
    )) NOT NULL,
    satusehat_encounter_id VARCHAR(50) UNIQUE,
    created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
);

CREATE INDEX idx_encounters_patient ON encounters(patient_id);
CREATE INDEX idx_encounters_status ON encounters(status);

-- 3. Table Vital Signs
CREATE TABLE vital_signs (
    id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
    encounter_id UUID UNIQUE NOT NULL REFERENCES encounters(id) ON DELETE CASCADE,
    blood_pressure_systolic NUMERIC(5,2) NOT NULL,
    blood_pressure_diastolic NUMERIC(5,2) NOT NULL,
    body_temperature NUMERIC(4,2) NOT NULL
        CHECK (body_temperature > 30.00 AND body_temperature < 45.00),
    heart_rate NUMERIC(5,2) NOT NULL,
    respiratory_rate NUMERIC(5,2) NOT NULL,
    oxygen_saturation NUMERIC(5,2) NOT NULL
        CHECK (oxygen_saturation >= 0.00 AND oxygen_saturation <= 100.00),
    recorded_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
);

-- 4. Table SOAP Notes
-- Kolom digital_signature_hash menyimpan hash BSrE dari Balai Sertifikasi Elektronik
-- Setelah signed_at terisi, record ini bersifat LOCKED (immutable sesuai Permenkes 24/2022)
-- Modifikasi setelah lock hanya diizinkan melalui tabel soap_notes_history (Addendum)
CREATE TABLE soap_notes (
    id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
    encounter_id UUID UNIQUE NOT NULL REFERENCES encounters(id) ON DELETE RESTRICT,
    subjective TEXT NOT NULL,
    objective TEXT NOT NULL,
    assessment_icd10 JSONB DEFAULT '[]'::jsonb NOT NULL,
    -- Format: [{"code": "I10", "name": "Essential Hypertension"}]
    plan TEXT NOT NULL,
    signed_at TIMESTAMP WITH TIME ZONE,
    signed_by_doctor_id UUID,
    digital_signature_hash TEXT, -- BSrE certificate hash (SHA-256)
    created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
);

-- Addendum table: append-only revisi RME setelah lock
CREATE TABLE soap_notes_history (
    id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
    soap_note_id UUID NOT NULL REFERENCES soap_notes(id) ON DELETE RESTRICT,
    version INTEGER NOT NULL,
    subjective TEXT NOT NULL,
    objective TEXT NOT NULL,
    assessment_icd10 JSONB DEFAULT '[]'::jsonb NOT NULL,
    plan TEXT NOT NULL,
    addendum_reason TEXT NOT NULL,
    amended_by_doctor_id UUID NOT NULL,
    amended_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP NOT NULL,
    digital_signature_hash TEXT NOT NULL -- BSrE hash on amendment
);

-- 5. Table Medicines Master & Batches
CREATE TABLE medicines (
    id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
    code VARCHAR(50) UNIQUE NOT NULL,
    name VARCHAR(100) NOT NULL,
    unit VARCHAR(20) NOT NULL,
    min_stock_alert INTEGER DEFAULT 100 NOT NULL,
    created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
);

-- FEFO: ordered by expiry_date ASC when allocating stock
CREATE TABLE medicine_batches (
    id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
    medicine_id UUID NOT NULL REFERENCES medicines(id) ON DELETE RESTRICT,
    batch_number VARCHAR(50) NOT NULL,
    expiry_date DATE NOT NULL,
    stock_qty INTEGER NOT NULL CHECK (stock_qty >= 0),
    purchase_price NUMERIC(12,2) NOT NULL,
    selling_price NUMERIC(12,2) NOT NULL,
    UNIQUE (medicine_id, batch_number)
);

CREATE INDEX idx_batches_expiry ON medicine_batches(expiry_date ASC);

-- 6. Table Prescriptions & Items
CREATE TABLE prescriptions (
    id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
    encounter_id UUID UNIQUE NOT NULL REFERENCES encounters(id) ON DELETE RESTRICT,
    prescription_number VARCHAR(50) UNIQUE NOT NULL,
    status VARCHAR(20) CHECK (status IN (
        'PENDING', 'PREPARING', 'DISPENSED', 'CANCELLED'
    )) DEFAULT 'PENDING' NOT NULL,
    created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
);

CREATE TABLE prescription_items (
    id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
    prescription_id UUID NOT NULL REFERENCES prescriptions(id) ON DELETE CASCADE,
    medicine_id UUID NOT NULL REFERENCES medicines(id) ON DELETE RESTRICT,
    allocated_batch_id UUID REFERENCES medicine_batches(id) ON DELETE RESTRICT,
    dosage_instruction VARCHAR(150) NOT NULL,
    quantity INTEGER NOT NULL CHECK (quantity > 0),
    is_racikan BOOLEAN DEFAULT FALSE NOT NULL,
    racikan_group_id VARCHAR(50)
);

-- 7. Table Billing Invoices
CREATE TABLE billing_invoices (
    id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
    encounter_id UUID UNIQUE NOT NULL REFERENCES encounters(id) ON DELETE RESTRICT,
    invoice_number VARCHAR(50) UNIQUE NOT NULL,
    total_amount NUMERIC(12,2) NOT NULL CHECK (total_amount >= 0.00),
    bpjs_coverage NUMERIC(12,2) DEFAULT 0.00 NOT NULL CHECK (bpjs_coverage >= 0.00),
    insurance_coverage NUMERIC(12,2) DEFAULT 0.00 NOT NULL CHECK (insurance_coverage >= 0.00),
    patient_copayment NUMERIC(12,2) DEFAULT 0.00 NOT NULL CHECK (patient_copayment >= 0.00),
    payment_status VARCHAR(20) CHECK (payment_status IN (
        'UNPAID', 'PAID', 'REFUNDED'
    )) DEFAULT 'UNPAID' NOT NULL,
    created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
);

-- 8. Table Audit Logs (Immutable Logging for UU PDP Privacy Compliance)
-- Hak akses PostgreSQL: READ + INSERT only. UPDATE & DELETE dinonaktifkan untuk semua app users.
CREATE TABLE audit_logs (
    id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
    action_time TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP NOT NULL,
    user_id UUID NOT NULL,
    action_type VARCHAR(20) CHECK (action_type IN (
        'INSERT', 'UPDATE', 'DELETE', 'READ_EMR'
    )) NOT NULL,
    table_name VARCHAR(50) NOT NULL,
    record_id UUID NOT NULL,
    old_values JSONB,
    new_values JSONB,
    ip_address VARCHAR(45) NOT NULL,
    user_agent TEXT NOT NULL
);

CREATE INDEX idx_audit_record ON audit_logs(record_id);
CREATE INDEX idx_audit_time ON audit_logs(action_time DESC);

Design Notes

  • RME Immutability: Kolom signed_at dan digital_signature_hash pada soap_notes adalah sinyal kunci. Setelah signed_at terisi, aplikasi layer wajib menolak setiap UPDATE. Revisi hanya masuk ke soap_notes_history sebagai Addendum.
  • FEFO Allocation: Index idx_batches_expiry ASC pada medicine_batches memastikan query allocator selalu mengambil batch dengan expiry_date terpendek terlebih dahulu.
  • Audit Trail Append-Only: Di level PostgreSQL, hak UPDATE dan DELETE pada tabel audit_logs direvoke dari semua application roles. Hanya SELECT dan INSERT yang diizinkan — mencegah manipulasi jejak audit oleh insider.
  • Column-Level Encryption: Kolom sensitif (allergies, assessment_icd10, subjective, objective, plan) dienkripsi AES-256-GCM di application layer sebelum INSERT. Kunci enkripsi dikelola HashiCorp Vault — tidak pernah disimpan di database yang sama.
  • Multi-Tenant: branch_id di tabel encounters memungkinkan satu database cluster melayani banyak cabang klinik dengan isolasi data berbasis Row-Level Security (RLS) PostgreSQL.