← Tasks
[DM-05] Surveys table (configurable questionnaires)
completedcode_genP1
Description
## DM-05: Surveys table (configurable questionnaires)
Section: Data Model | Milestone: M2 | Owner: None
### Failing Tests
- **surveys_table_exists** (grep): Pattern `CREATE TABLE surveys` in `supabase/migrations/*.sql` (need 1+ matches)
- **surveys_fields** (grep): Pattern `campaign_id|questions_json|is_active` in `supabase/migrations/*.sql` (need 2+ matches)
### Acceptance Criteria
- File matching `supabase/migrations/*.sql` must contain pattern `CREATE TABLE surveys`
- File matching `supabase/migrations/*.sql` must contain pattern `campaign_id|questions_json|is_active`
### Codebase Conventions
- Trilingual comments: // EN: ... // VI: ... // ES: ...
- SPEC section refs in comments
- Zod at module level with .safeParse()
- (supabase as any) with eslint-disable
- NextResponse.json() responses
- Error format: { error: 'message' }
### Dependencies
- DM-01: FAIL
Assignee
-
Claimed By
Cloud Lu → Claude Sonnet
Created
23d ago
Completed
2d ago
Quality
Overall
79%
Pass
PASS
Graded By
-
Graded At
-
Cost
Input Tokens
-
Output Tokens
-
Est. USD
-
Result
{
"cost": {
"input_tokens": 23,
"estimated_usd": 0.056994,
"output_tokens": 3795
},
"model": "claude-sonnet-4-20250514",
"status": "success",
"transport": "anthropic_api",
"latency_ms": 47143,
"response_text": "I'll design a flexible surveys table that supports configurable questionnaires. Here's a comprehensive schema:\n\n```sql\n-- Main surveys table\nCREATE TABLE surveys (\n survey_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),\n title VARCHAR(255) NOT NULL,\n description TEXT,\n survey_type VARCHAR(50) DEFAULT 'questionnaire', -- questionnaire, poll, assessment, feedback\n status VARCHAR(20) DEFAULT 'draft', -- draft, active, paused, completed, archived\n \n -- Configuration\n settings JSONB DEFAULT '{}', -- Survey-level settings (randomize_questions, show_progress, etc.)\n max_responses INTEGER,\n allow_anonymous BOOLEAN DEFAULT true,\n allow_multiple_responses BOOLEAN DEFAULT false,\n \n -- Scheduling\n start_date TIMESTAMP WITH TIME ZONE,\n end_date TIMESTAMP WITH TIME ZONE,\n \n -- Access control\n is_public BOOLEAN DEFAULT false,\n access_code VARCHAR(50), -- Optional access code for private surveys\n target_audience JSONB, -- Criteria for who can take the survey\n \n -- Metadata\n created_by UUID NOT NULL,\n organization_id UUID,\n created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,\n updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,\n \n -- Constraints\n CONSTRAINT valid_status CHECK (status IN ('draft', 'active', 'paused', 'completed', 'archived')),\n CONSTRAINT valid_dates CHECK (start_date IS NULL OR end_date IS NULL OR start_date < end_date)\n);\n\n-- Survey sections for grouping questions\nCREATE TABLE survey_sections (\n section_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),\n survey_id UUID NOT NULL REFERENCES surveys(survey_id) ON DELETE CASCADE,\n title VARCHAR(255),\n description TEXT,\n display_order INTEGER NOT NULL,\n \n -- Section behavior\n is_required BOOLEAN DEFAULT true,\n show_condition JSONB, -- Conditional logic for showing this section\n \n created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP\n);\n\n-- Question types and templates\nCREATE TABLE question_types (\n type_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),\n name VARCHAR(100) NOT NULL UNIQUE,\n display_name VARCHAR(100) NOT NULL,\n description TEXT,\n input_type VARCHAR(50) NOT NULL, -- text, number, email, select, multiselect, radio, checkbox, scale, date, file\n default_settings JSONB DEFAULT '{}',\n validation_rules JSONB DEFAULT '{}',\n is_active BOOLEAN DEFAULT true,\n \n created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP\n);\n\n-- Survey questions\nCREATE TABLE survey_questions (\n question_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),\n survey_id UUID NOT NULL REFERENCES surveys(survey_id) ON DELETE CASCADE,\n section_id UUID REFERENCES survey_sections(section_id) ON DELETE SET NULL,\n question_type_id UUID NOT NULL REFERENCES question_types(type_id),\n \n -- Question content\n question_text TEXT NOT NULL,\n help_text TEXT,\n placeholder_text VARCHAR(255),\n \n -- Question settings\n is_required BOOLEAN DEFAULT false,\n display_order INTEGER NOT NULL,\n settings JSONB DEFAULT '{}', -- Question-specific settings (min_length, max_length, scale_min, scale_max, etc.)\n \n -- Conditional logic\n show_condition JSONB, -- When to show this question based on previous answers\n validation_rules JSONB DEFAULT '{}',\n \n created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP\n);\n\n-- Question options (for select, radio, checkbox questions)\nCREATE TABLE question_options (\n option_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),\n question_id UUID NOT NULL REFERENCES survey_questions(question_id) ON DELETE CASCADE,\n option_text VARCHAR(500) NOT NULL,\n option_value VARCHAR(255),\n display_order INTEGER NOT NULL,\n \n -- Option behavior\n is_other BOOLEAN DEFAULT false, -- \"Other\" option that allows text input\n jump_to_question_id UUID REFERENCES survey_questions(question_id), -- Skip logic\n \n created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP\n);\n\n-- Survey responses (one per participant)\nCREATE TABLE survey_responses (\n response_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),\n survey_id UUID NOT NULL REFERENCES surveys(survey_id) ON DELETE CASCADE,\n \n -- Respondent info\n respondent_id UUID, -- NULL for anonymous responses\n session_id VARCHAR(255), -- For tracking anonymous sessions\n ip_address INET,\n user_agent TEXT,\n \n -- Response metadata\n status VARCHAR(20) DEFAULT 'in_progress', -- in_progress, completed, abandoned\n start_time TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,\n completion_time TIMESTAMP WITH TIME ZONE,\n time_spent_seconds INTEGER,\n \n -- Response data\n responses JSONB NOT NULL DEFAULT '{}', -- Structured response data\n metadata JSONB DEFAULT '{}', -- Additional metadata (device info, referrer, etc.)\n \n created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,\n updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,\n \n CONSTRAINT valid_response_status CHECK (status IN ('in_progress', 'completed', 'abandoned'))\n);\n\n-- Individual question answers\nCREATE TABLE question_answers (\n answer_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),\n response_id UUID NOT NULL REFERENCES survey_responses(response_id) ON DELETE CASCADE,\n question_id UUID NOT NULL REFERENCES survey_questions(question_id) ON DELETE CASCADE,\n \n -- Answer data\n answer_text TEXT,\n answer_number DECIMAL,\n answer_date DATE,\n answer_options JSONB, -- For multi-select answers\n file_attachments JSONB, -- For file upload questions\n \n -- Answer metadata\n answered_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,\n time_spent_seconds INTEGER,\n \n UNIQUE(response_id, question_id)\n);\n\n-- Survey analytics and reporting\nCREATE TABLE survey_analytics (\n analytics_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),\n survey_id UUID NOT NULL REFERENCES surveys(survey_id) ON DELETE CASCADE,\n \n -- Participation metrics\n total_views INTEGER DEFAULT 0,\n total_starts INTEGER DEFAULT 0,\n total_completions INTEGER DEFAULT 0,\n completion_rate DECIMAL(5,2),\n average_time_minutes DECIMAL(8,2),\n \n -- Response metrics\n last_response_at TIMESTAMP WITH TIME ZONE,\n responses_today INTEGER DEFAULT 0,\n responses_this_week INTEGER DEFAULT 0,\n responses_this_month INTEGER DEFAULT 0,\n \n -- Question-level analytics\n question_analytics JSONB DEFAULT '{}',\n \n calculated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP\n);\n\n-- Survey sharing and distribution\nCREATE TABLE survey_distributions (\n distribution_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),\n survey_id UUID NOT NULL REFERENCES surveys(survey_id) ON DELETE CASCADE,\n \n -- Distribution method\n method VARCHAR(50) NOT NULL, -- email, link, embed, qr_code, social\n channel VARCHAR(100), -- specific channel (email_campaign, website, facebook, etc.)\n \n -- Distribution settings\n custom_url VARCHAR(255),\n embed_code TEXT,\n settings JSONB DEFAULT '{}',\n \n -- Tracking\n views INTEGER DEFAULT 0,\n responses INTEGER DEFAULT 0,\n created_by UUID NOT NULL,\n created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP\n);\n\n-- Indexes for performance\nCREATE INDEX idx_surveys_status ON surveys(status);\nCREATE INDEX idx_surveys_created_by ON surveys(created_by);\nCREATE INDEX idx_surveys_organization ON surveys(organization_id);\nCREATE INDEX idx_surveys_dates ON surveys(start_date, end_date) WHERE start_date IS NOT NULL OR end_date IS NOT NULL;\n\nCREATE INDEX idx_survey_sections_survey ON survey_sections(survey_id, display_order);\nCREATE INDEX idx_survey_questions_survey ON survey_questions(survey_id, display_order);\nCREATE INDEX idx_survey_questions_section ON survey_questions(section_id, display_order);\nCREATE INDEX idx_question_options_question ON question_options(question_id, display_order);\n\nCREATE INDEX idx_survey_responses_survey ON survey_responses(survey_id);\nCREATE INDEX idx_survey_responses_status ON survey_responses(status);\nCREATE INDEX idx_survey_responses_completion ON survey_responses(completion_time) WHERE completion_time IS NOT NULL;\nCREATE INDEX idx_question_answers_response ON question_answers(response_id);\nCREATE INDEX idx_question_answers_question ON question_answers(question_id);\n\n-- Create GIN indexes for JSONB columns\nCREATE INDEX idx_surveys_settings_gin ON surveys USING GIN(settings);\nCREATE INDEX idx_survey_questions_settings_gin ON survey_questions USING GIN(settings);\nCREATE INDEX idx_survey_responses_responses_gin ON survey_responses USING GIN(responses);\nCREATE INDEX idx_survey_responses_metadata_gin ON survey_responses USING GIN(metadata);\n\n-- Insert default question types\nINSERT INTO question_types (name, display_name, description, input_type, default_settings) VALUES\n('short_text', 'Short Text', 'Single line text input', 'text', '{\"max_length\": 255}'),\n('long_text', 'Long Text', 'Multi-line text area', 'textarea', '{\"max_length\": 2000}'),\n('email', 'Email', 'Email address input', 'email', '{}'),\n('number', 'Number', 'Numeric input', 'number', '{\"allow_decimal\": true}'),\n('single_choice', 'Single Choice', 'Radio buttons or dropdown', 'radio', '{\"display_as\": \"radio\"}'),\n('multiple_choice', 'Multiple Choice', 'Checkboxes for multiple selections', 'checkbox', '{}'),\n('dropdown', 'Dropdown', 'Dropdown selection', 'select', '{}'),\n('rating_scale', 'Rating Scale', 'Numeric rating scale', 'scale', '{\"min\": 1, \"max\": 5, \"step\": 1}'),\n('likert_scale', 'Likert Scale', 'Agreement scale', 'likert', '{\"options\": [\"Strongly Disagree\", \"Disagree\", \"Neutral\", \"Agree\", \"Strongly Agree\"]}'),\n('yes_no', 'Yes/No', 'Simple yes/no question', 'radio', '{\"options\": [{\"text\": \"Yes\", \"value\": \"yes\"}, {\"text\": \"No\", \"value\": \"no\"}]}'),\n('date', 'Date', 'Date picker', 'date', '{}'),\n('file_upload', 'File Upload', 'File attachment', 'file', '{\"max_files\": 1, \"max_size_mb\": 10}');\n\n-- Views for common queries\nCREATE VIEW survey_summary AS\nSELECT \n s.survey_id,\n s.title,\n s.status,\n s.created_at,\n COUNT(DISTINCT sr.response_id) as total_responses,\n COUNT(DISTINCT CASE WHEN sr.status = 'completed' THEN sr.response_id END) as completed_responses,\n ROUND(\n COUNT(DISTINCT CASE WHEN sr.status = 'completed' THEN sr.response_id END)::numeric * 100.0 / \n NULLIF(COUNT(DISTINCT sr.response_id), 0), 2\n ) as completion_rate,\n AVG(CASE WHEN sr.status = 'completed' THEN sr.time_spent_seconds END) as avg_completion_time_seconds\nFROM surveys s\nLEFT JOIN survey_responses sr ON s.survey_id = sr.survey_id\nGROUP BY s.survey_id, s.title, s.status, s.created_at;\n\n-- Function to update analytics\nCREATE OR REPLACE FUNCTION update_survey_analytics(p_survey_id UUID)\nRETURNS VOID AS $$\nBEGIN\n INSERT INTO survey_analytics (\n survey_id,\n total_views,\n total_starts,\n total_completions,\n completion_rate,\n average_time_minutes,\n last_response_at,\n responses_today,\n responses_this_week,\n responses_this_month\n )\n SELECT \n p_survey_id,\n COUNT(DISTINCT sr.response_id) as total_views,\n COUNT(DISTINCT sr.response_id) as total_starts,\n COUNT(DISTINCT CASE WHEN sr.status = 'completed' THEN sr.response_id END) as total_completions,\n ROUND(\n COUNT(DISTINCT CASE WHEN sr.status = 'completed' THEN sr.response_id END)::numeric * 100.0 / \n NULLIF(COUNT(DISTINCT sr.response_id), 0), 2\n ) as completion_rate,\n ROUND(AVG(CASE WHEN sr.status = 'completed' THEN sr.time_spent_seconds END) / 60.0, 2) as average_time_minutes,\n MAX(sr.created_at) as last_response_at,\n COUNT(DISTINCT CASE WHEN sr.created_at >= CURRENT_DATE THEN sr.response_id END) as responses_today,\n COUNT(DISTINCT CASE WHEN sr.created_at >= CURRENT_DATE - INTERVAL '7 days' THEN sr.response_id END) as responses_this_week,\n COUNT(DISTINCT CASE WHEN sr.created_at >= CURRENT_DATE - INTERVAL '30 days' THEN sr.response_id END) as responses_this_month\n FROM survey_responses sr\n WHERE sr.survey_id = p_survey_id\n ON CONFLICT (survey_id) DO UPDATE SET\n total_views = EXCLUDED.total_views,\n total_starts = EXCLUDED.total_starts,\n total_completions = EXCLUDED.total_completions,\n completion_rate = EXCLUDED.completion_rate,\n average_time_minutes = EXCLUDED.average_time_minutes,\n last_response_at = EXCLUDED.last_response_at,\n responses_today = EXCLUDED.responses_today,\n responses_this_week = EXCLUDED.responses_this_week,\n responses_this_month = EXCLUDED.responses_this_month,\n calculated_at = CURRENT_TIMESTAMP;\nEND;\n$$ LANGUAGE plpgsql;\n```\n\nThis survey system supports:\n\n**Key Features:**\n- Flexible question types with configurable settings\n- Survey sections for organization\n- Conditional logic and skip patterns\n- Anonymous and authenticated responses\n- Real-time analytics tracking\n- Multiple distribution methods\n- File upload capabilities\n- Progress tracking and session management\n\n**Question Types:**\n- Text inputs (short/long)\n- Single/multiple choice\n- Rating scales and Likert scales\n- Date selection\n- File uploads\n- Email validation\n- Numeric inputs\n\n**Advanced Capabilities:**\n- JSONB storage for flexible configuration\n- Conditional question display\n- Response validation\n- Time tracking\n- Comprehensive analytics\n- Survey sharing and embedding\n\nThe schema is designed to be highly configurable while maintaining performance through proper indexing and efficient query patterns."
}Audit Trail (1)
2d agodependents_unblockedfoundry_pipeline
Task ID: bf44dc67-1f60-4fcd-85c7-0c53eb22a021