diff --git a/app/controllers/api/v1/setup_survey_controller.rb b/app/controllers/api/v1/setup_survey_controller.rb new file mode 100644 index 0000000..045197d --- /dev/null +++ b/app/controllers/api/v1/setup_survey_controller.rb @@ -0,0 +1,32 @@ +# frozen_string_literal: true + +# Authenticated endpoint for saving/checking the onboarding survey. +# Used when the user logs in before completing the survey (e.g. survey_token expired). +class Api::V1::SetupSurveyController < Api::BaseController + # GET /api/v1/setup_survey + def show + success_response( + data: { completed: current_user.setup_survey_completed? }, + message: 'Survey status retrieved successfully' + ) + end + + # POST /api/v1/setup_survey + def create + survey = current_user.setup_survey_response || current_user.build_setup_survey_response + survey.assign_attributes(survey_params) + + if survey.save + success_response(data: { completed: true }, message: 'Survey saved successfully') + else + render_unprocessable_entity(survey.errors) + end + end + + private + + def survey_params + params.permit(:team_size, :daily_volume, :main_channel, :main_channel_other, + :uses_ai, :biggest_pain, :crm_experience, :main_goal) + end +end diff --git a/app/controllers/api/v1/user_tours_controller.rb b/app/controllers/api/v1/user_tours_controller.rb new file mode 100644 index 0000000..2955102 --- /dev/null +++ b/app/controllers/api/v1/user_tours_controller.rb @@ -0,0 +1,50 @@ +class Api::V1::UserToursController < Api::BaseController + def index + tours = current_user.user_tours.order(:completed_at) + success_response( + data: tours.map { |t| serialize_tour(t) }, + message: 'Tours retrieved successfully' + ) + end + + def create + tour = current_user.user_tours.find_or_initialize_by(tour_key: tour_params[:tour_key]) + tour.completed_at = tour_params[:completed_at].presence || Time.current + tour.status = tour_params[:status].presence.then { |s| UserTour::STATUSES.include?(s) ? s : 'completed' } + + if tour.save + success_response( + data: serialize_tour(tour), + message: 'Tour saved successfully' + ) + else + render_unprocessable_entity(tour.errors) + end + end + + def destroy + tour = current_user.user_tours.find_by(tour_key: params[:id]) + + if tour + tour.destroy + success_response(data: {}, message: 'Tour reset successfully') + else + render_not_found('Tour not found') + end + end + + private + + def tour_params + params.require(:tour).permit(:tour_key, :completed_at, :status) + end + + def serialize_tour(tour) + { + id: tour.id, + tour_key: tour.tour_key, + completed_at: tour.completed_at, + status: tour.status + } + end +end diff --git a/app/models/setup_survey_response.rb b/app/models/setup_survey_response.rb new file mode 100644 index 0000000..48b4298 --- /dev/null +++ b/app/models/setup_survey_response.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +# == Schema Information +# +# Table name: setup_survey_responses +# +# id :uuid not null, primary key +# user_id :uuid not null +# team_size :string +# daily_volume :string +# main_channel :string +# main_channel_other :string +# uses_ai :string +# biggest_pain :string +# crm_experience :string +# main_goal :string +# created_at :datetime not null +# updated_at :datetime not null +# +class SetupSurveyResponse < ApplicationRecord + belongs_to :user + + validates :user_id, uniqueness: true +end diff --git a/app/models/user_tour.rb b/app/models/user_tour.rb new file mode 100644 index 0000000..6a23b7f --- /dev/null +++ b/app/models/user_tour.rb @@ -0,0 +1,23 @@ +# == Schema Information +# +# Table name: user_tours +# +# id :uuid not null, primary key +# user_id :uuid not null +# tour_key :string not null +# completed_at :datetime not null +# status :string not null, default: 'completed' +# created_at :datetime not null +# updated_at :datetime not null +# + +class UserTour < ApplicationRecord + STATUSES = %w[completed skipped].freeze + + belongs_to :user + + validates :tour_key, presence: true + validates :tour_key, uniqueness: { scope: :user_id, message: 'already seen by this user' } + validates :completed_at, presence: true + validates :status, inclusion: { in: STATUSES } +end diff --git a/db/migrate/20260407000001_create_user_tours.rb b/db/migrate/20260407000001_create_user_tours.rb new file mode 100644 index 0000000..7f242cf --- /dev/null +++ b/db/migrate/20260407000001_create_user_tours.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +class CreateUserTours < ActiveRecord::Migration[7.1] + def change + create_table :user_tours, id: :uuid do |t| + t.references :user, null: false, foreign_key: true, type: :uuid + t.string :tour_key, null: false + t.datetime :completed_at, null: false + + t.timestamps + end + + add_index :user_tours, [:user_id, :tour_key], unique: true + end +end diff --git a/db/migrate/20260407000002_add_status_to_user_tours.rb b/db/migrate/20260407000002_add_status_to_user_tours.rb new file mode 100644 index 0000000..3c3e1a0 --- /dev/null +++ b/db/migrate/20260407000002_add_status_to_user_tours.rb @@ -0,0 +1,5 @@ +class AddStatusToUserTours < ActiveRecord::Migration[7.1] + def change + add_column :user_tours, :status, :string, null: false, default: 'completed' + end +end diff --git a/db/migrate/20260409000001_create_setup_survey_responses.rb b/db/migrate/20260409000001_create_setup_survey_responses.rb new file mode 100644 index 0000000..b37f4b5 --- /dev/null +++ b/db/migrate/20260409000001_create_setup_survey_responses.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +class CreateSetupSurveyResponses < ActiveRecord::Migration[7.1] + def change + create_table :setup_survey_responses, id: :uuid do |t| + t.references :user, null: false, foreign_key: true, type: :uuid, index: { unique: true } + t.string :team_size + t.string :daily_volume + t.string :main_channel + t.string :main_channel_other + t.string :uses_ai + t.string :biggest_pain + t.string :crm_experience + t.string :main_goal + t.timestamps + end + end +end