-- Enums DO $$ BEGIN IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'userrole') THEN CREATE TYPE userrole AS ENUM ('ADMIN', 'USER'); END IF; IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'campaignstatus') THEN CREATE TYPE campaignstatus AS ENUM ('DRAFT', 'SCHEDULED', 'SENDING', 'DONE', 'FAILED'); END IF; IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'recipientstatus') THEN CREATE TYPE recipientstatus AS ENUM ('PENDING', 'SENT', 'DELIVERED', 'READ', 'FAILED'); END IF; END$$; -- users CREATE TABLE IF NOT EXISTS users ( id SERIAL PRIMARY KEY, email VARCHAR NOT NULL, password_hash VARCHAR NOT NULL, role userrole NOT NULL, created_at TIMESTAMPTZ DEFAULT now() ); CREATE UNIQUE INDEX IF NOT EXISTS ix_users_email ON users (email); CREATE INDEX IF NOT EXISTS ix_users_id ON users (id); -- contacts CREATE TABLE IF NOT EXISTS contacts ( id SERIAL PRIMARY KEY, user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, phone_e164 VARCHAR NOT NULL, first_name VARCHAR, last_name VARCHAR, email VARCHAR, opted_in BOOLEAN NOT NULL, conversation_window_open BOOLEAN NOT NULL, source VARCHAR, created_at TIMESTAMPTZ DEFAULT now(), updated_at TIMESTAMPTZ DEFAULT now(), CONSTRAINT uq_user_phone UNIQUE (user_id, phone_e164) ); CREATE INDEX IF NOT EXISTS ix_contacts_user_phone ON contacts (user_id, phone_e164); CREATE INDEX IF NOT EXISTS ix_contacts_id ON contacts (id); CREATE INDEX IF NOT EXISTS ix_contacts_user_id ON contacts (user_id); -- contact_tags CREATE TABLE IF NOT EXISTS contact_tags ( id SERIAL PRIMARY KEY, user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, name VARCHAR NOT NULL, created_at TIMESTAMPTZ DEFAULT now() ); CREATE INDEX IF NOT EXISTS ix_contact_tags_id ON contact_tags (id); CREATE INDEX IF NOT EXISTS ix_contact_tags_user_id ON contact_tags (user_id); -- contact_tag_map CREATE TABLE IF NOT EXISTS contact_tag_map ( id SERIAL PRIMARY KEY, contact_id INTEGER NOT NULL REFERENCES contacts(id) ON DELETE CASCADE, tag_id INTEGER NOT NULL REFERENCES contact_tags(id) ON DELETE CASCADE, CONSTRAINT uq_contact_tag UNIQUE (contact_id, tag_id) ); CREATE INDEX IF NOT EXISTS ix_contact_tag_map_id ON contact_tag_map (id); -- dnd_list CREATE TABLE IF NOT EXISTS dnd_list ( id SERIAL PRIMARY KEY, user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, phone_e164 VARCHAR NOT NULL, reason TEXT, created_at TIMESTAMPTZ DEFAULT now(), CONSTRAINT uq_dnd_user_phone UNIQUE (user_id, phone_e164) ); CREATE INDEX IF NOT EXISTS ix_dnd_list_id ON dnd_list (id); CREATE INDEX IF NOT EXISTS ix_dnd_list_phone_e164 ON dnd_list (phone_e164); CREATE INDEX IF NOT EXISTS ix_dnd_list_user_id ON dnd_list (user_id); -- lists CREATE TABLE IF NOT EXISTS lists ( id SERIAL PRIMARY KEY, user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, name VARCHAR NOT NULL, created_at TIMESTAMPTZ DEFAULT now() ); CREATE INDEX IF NOT EXISTS ix_lists_id ON lists (id); CREATE INDEX IF NOT EXISTS ix_lists_user_id ON lists (user_id); -- list_members CREATE TABLE IF NOT EXISTS list_members ( id SERIAL PRIMARY KEY, list_id INTEGER NOT NULL REFERENCES lists(id) ON DELETE CASCADE, contact_id INTEGER NOT NULL REFERENCES contacts(id) ON DELETE CASCADE, CONSTRAINT uq_list_contact UNIQUE (list_id, contact_id) ); CREATE INDEX IF NOT EXISTS ix_list_members_contact_id ON list_members (contact_id); CREATE INDEX IF NOT EXISTS ix_list_members_id ON list_members (id); CREATE INDEX IF NOT EXISTS ix_list_members_list_id ON list_members (list_id); -- templates CREATE TABLE IF NOT EXISTS templates ( id SERIAL PRIMARY KEY, user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, name VARCHAR NOT NULL, language VARCHAR NOT NULL, body_text TEXT NOT NULL, is_whatsapp_template BOOLEAN NOT NULL, provider_template_name VARCHAR, created_at TIMESTAMPTZ DEFAULT now() ); CREATE INDEX IF NOT EXISTS ix_templates_id ON templates (id); CREATE INDEX IF NOT EXISTS ix_templates_user_id ON templates (user_id); -- campaigns CREATE TABLE IF NOT EXISTS campaigns ( id SERIAL PRIMARY KEY, user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, name VARCHAR NOT NULL, template_id INTEGER REFERENCES templates(id) ON DELETE SET NULL, list_id INTEGER REFERENCES lists(id) ON DELETE SET NULL, status campaignstatus NOT NULL, scheduled_at TIMESTAMPTZ, created_at TIMESTAMPTZ DEFAULT now() ); CREATE INDEX IF NOT EXISTS ix_campaigns_id ON campaigns (id); CREATE INDEX IF NOT EXISTS ix_campaigns_status ON campaigns (status); CREATE INDEX IF NOT EXISTS ix_campaigns_user_id ON campaigns (user_id); -- campaign_recipients CREATE TABLE IF NOT EXISTS campaign_recipients ( id SERIAL PRIMARY KEY, campaign_id INTEGER NOT NULL REFERENCES campaigns(id) ON DELETE CASCADE, contact_id INTEGER NOT NULL REFERENCES contacts(id) ON DELETE CASCADE, status recipientstatus NOT NULL, provider_message_id VARCHAR, last_error TEXT, updated_at TIMESTAMPTZ DEFAULT now() ); CREATE INDEX IF NOT EXISTS ix_campaign_recipients_campaign_status ON campaign_recipients (campaign_id, status); CREATE INDEX IF NOT EXISTS ix_campaign_recipients_campaign_id ON campaign_recipients (campaign_id); CREATE INDEX IF NOT EXISTS ix_campaign_recipients_contact_id ON campaign_recipients (contact_id); CREATE INDEX IF NOT EXISTS ix_campaign_recipients_id ON campaign_recipients (id); -- send_logs CREATE TABLE IF NOT EXISTS send_logs ( id SERIAL PRIMARY KEY, user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, campaign_id INTEGER REFERENCES campaigns(id) ON DELETE CASCADE, contact_id INTEGER REFERENCES contacts(id) ON DELETE CASCADE, provider VARCHAR NOT NULL, request_payload_json JSON, response_payload_json JSON, status VARCHAR NOT NULL, created_at TIMESTAMPTZ DEFAULT now() ); CREATE INDEX IF NOT EXISTS ix_send_logs_campaign_id ON send_logs (campaign_id); CREATE INDEX IF NOT EXISTS ix_send_logs_created_at ON send_logs (created_at); CREATE INDEX IF NOT EXISTS ix_send_logs_id ON send_logs (id); CREATE INDEX IF NOT EXISTS ix_send_logs_user_id ON send_logs (user_id); -- jobs CREATE TABLE IF NOT EXISTS jobs ( id SERIAL PRIMARY KEY, user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, type VARCHAR NOT NULL, payload_json JSON NOT NULL, status VARCHAR NOT NULL, attempts INTEGER NOT NULL, run_after TIMESTAMPTZ NOT NULL DEFAULT now(), last_error TEXT, created_at TIMESTAMPTZ DEFAULT now(), updated_at TIMESTAMPTZ DEFAULT now() ); CREATE INDEX IF NOT EXISTS ix_jobs_status_run_after ON jobs (status, run_after); CREATE INDEX IF NOT EXISTS ix_jobs_id ON jobs (id); CREATE INDEX IF NOT EXISTS ix_jobs_user_id ON jobs (user_id); -- google_tokens CREATE TABLE IF NOT EXISTS google_tokens ( id SERIAL PRIMARY KEY, user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, encrypted_token TEXT NOT NULL, created_at TIMESTAMPTZ DEFAULT now(), updated_at TIMESTAMPTZ DEFAULT now() ); CREATE UNIQUE INDEX IF NOT EXISTS ix_google_tokens_user_id ON google_tokens (user_id); CREATE INDEX IF NOT EXISTS ix_google_tokens_id ON google_tokens (id);