← back to Lawyer Directory Builder

migrations/014_ratings.sql

46 lines

-- Star ratings + dimension scores (service / price / quality) for firms and attorneys.
-- Sources: user-submitted, Avvo, Yelp, Google, Reddit (future), manual.

CREATE TABLE IF NOT EXISTS ratings (
  id BIGSERIAL PRIMARY KEY,
  professional_id BIGINT REFERENCES professionals(id) ON DELETE CASCADE,
  organization_id BIGINT REFERENCES organizations(id) ON DELETE CASCADE,
  reviewer_user_id BIGINT REFERENCES app_users(id) ON DELETE SET NULL,
  source TEXT NOT NULL CHECK (source IN ('user','avvo','google','yelp','reddit','manual')),
  source_url TEXT,
  external_review_id TEXT,                       -- e.g. Yelp review id, for dedup
  stars NUMERIC(2,1) NOT NULL CHECK (stars BETWEEN 0 AND 5),
  service_score NUMERIC(2,1),
  price_score NUMERIC(2,1),
  quality_score NUMERIC(2,1),
  comment TEXT,
  reviewer_name TEXT,                            -- for external sources where we only have a display name
  posted_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
  ingested_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
  hidden_at TIMESTAMPTZ,                         -- moderation soft-delete
  hidden_reason TEXT,
  CONSTRAINT rating_target_check
    CHECK (professional_id IS NOT NULL OR organization_id IS NOT NULL)
);
CREATE INDEX IF NOT EXISTS idx_ratings_pro ON ratings (professional_id) WHERE professional_id IS NOT NULL;
CREATE INDEX IF NOT EXISTS idx_ratings_org ON ratings (organization_id) WHERE organization_id IS NOT NULL;
CREATE INDEX IF NOT EXISTS idx_ratings_source ON ratings (source);
CREATE UNIQUE INDEX IF NOT EXISTS uq_ratings_external
  ON ratings (source, external_review_id) WHERE external_review_id IS NOT NULL;

-- Aggregate cache (rebuilt from triggers / cron — keeps firm/attorney pages fast)
CREATE TABLE IF NOT EXISTS rating_aggregates (
  target_kind TEXT NOT NULL CHECK (target_kind IN ('professional','organization')),
  target_id BIGINT NOT NULL,
  total_reviews INT NOT NULL DEFAULT 0,
  user_review_count INT NOT NULL DEFAULT 0,
  avg_stars NUMERIC(3,2),
  avg_service NUMERIC(3,2),
  avg_price NUMERIC(3,2),
  avg_quality NUMERIC(3,2),
  overall_score NUMERIC(4,2),                   -- weighted formula from spec
  by_source JSONB,                              -- {avvo:{count,avg}, yelp:{...}, google:{...}, user:{...}}
  updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
  PRIMARY KEY (target_kind, target_id)
);