-- smb-crm — source-of-truth schema (Postgres). -- Google Sheets is a one-way downstream mirror fed by the DB->Sheets sync. -- Column sets mirror the Sheet tabs so the mirror stays 1:1. CREATE TABLE IF NOT EXISTS clients ( client_id text PRIMARY KEY, business_name text, owner_name text, email text, phone text, niche text, tier text, status text, domain text, stack_notes text, vault_ref text, services text, billing_cycle text, monthly_fee_eur numeric, start_date date, renewal_date date, created_at timestamptz, notes text, notify_channel text, updated_at timestamptz NOT NULL DEFAULT now() ); CREATE TABLE IF NOT EXISTS leads ( lead_id text PRIMARY KEY, received_at timestamptz, client_id text, source text, name text, contact text, service_interest text, message text, status text, notified boolean, updated_at timestamptz NOT NULL DEFAULT now() ); CREATE TABLE IF NOT EXISTS projects ( project_id text PRIMARY KEY, client_id text, deliverable text, tier text, checklist text, go_live_date date, status text, updated_at timestamptz NOT NULL DEFAULT now() ); CREATE TABLE IF NOT EXISTS activity_log ( id bigserial PRIMARY KEY, ts timestamptz, workflow text, client_id text, action text, detail text, result text ); CREATE TABLE IF NOT EXISTS bookings ( booking_id text PRIMARY KEY, created_at timestamptz, client_id text, customer_name text, customer_contact text, service text, start_time timestamptz, end_time timestamptz, source text, status text, updated_at timestamptz NOT NULL DEFAULT now() ); CREATE TABLE IF NOT EXISTS invoices ( invoice_id text PRIMARY KEY, client_id text, issued_date date, due_date date, amount_eur numeric, period text, status text, paid_date date, updated_at timestamptz NOT NULL DEFAULT now() ); -- Per-client login credentials (e.g. the auto-generated Easy!Appointments -- provider login). Deliberately NOT mirrored to Sheets — see db.py TABLES -- ("mirror": False) — so secrets never leave Postgres. CREATE TABLE IF NOT EXISTS credentials ( cred_id text PRIMARY KEY, client_id text, label text, username text, secret text, notes text, created_at timestamptz, updated_at timestamptz NOT NULL DEFAULT now() ); -- keep updated_at fresh on row changes CREATE OR REPLACE FUNCTION touch_updated_at() RETURNS trigger AS $$ BEGIN NEW.updated_at = now(); RETURN NEW; END; $$ LANGUAGE plpgsql; DO $$ DECLARE t text; BEGIN FOREACH t IN ARRAY ARRAY['clients','leads','projects','bookings','invoices','credentials'] LOOP EXECUTE format( 'CREATE TRIGGER %I_touch BEFORE UPDATE ON %I FOR EACH ROW EXECUTE FUNCTION touch_updated_at()', t, t); END LOOP; END $$; CREATE INDEX IF NOT EXISTS leads_received_idx ON leads (received_at DESC); CREATE INDEX IF NOT EXISTS clients_status_idx ON clients (status); CREATE INDEX IF NOT EXISTS activity_ts_idx ON activity_log (ts DESC); CREATE INDEX IF NOT EXISTS credentials_client_idx ON credentials (client_id);