Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions app/controllers/api/v1/setup_survey_controller.rb
Original file line number Diff line number Diff line change
@@ -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
50 changes: 50 additions & 0 deletions app/controllers/api/v1/user_tours_controller.rb
Original file line number Diff line number Diff line change
@@ -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
24 changes: 24 additions & 0 deletions app/models/setup_survey_response.rb
Original file line number Diff line number Diff line change
@@ -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
23 changes: 23 additions & 0 deletions app/models/user_tour.rb
Original file line number Diff line number Diff line change
@@ -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
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nitpick: Align this model file with the frozen string literal convention used elsewhere.

Other new Ruby files in this change include # frozen_string_literal: true at the top, but this model doesn’t. Please add the magic comment here for consistency and to avoid accidental string mutation.

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
15 changes: 15 additions & 0 deletions db/migrate/20260407000001_create_user_tours.rb
Original file line number Diff line number Diff line change
@@ -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
5 changes: 5 additions & 0 deletions db/migrate/20260407000002_add_status_to_user_tours.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
class AddStatusToUserTours < ActiveRecord::Migration[7.1]
def change
add_column :user_tours, :status, :string, null: false, default: 'completed'
end
end
18 changes: 18 additions & 0 deletions db/migrate/20260409000001_create_setup_survey_responses.rb
Original file line number Diff line number Diff line change
@@ -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