Skip to content

Instantly share code, notes, and snippets.

@misteral
Last active October 16, 2023 09:06
Show Gist options
  • Save misteral/31ecc06c11baf9d68b60bf40c51266ba to your computer and use it in GitHub Desktop.
Save misteral/31ecc06c11baf9d68b60bf40c51266ba to your computer and use it in GitHub Desktop.
Meeting Model in Consulting System
# frozen_string_literal: true
# Broadcasts functions
module Meeting::Broadcasts # rubocop:disable Style/ClassAndModuleChildren
def broadcast_to_replace_customer_text
broadcast_replace_to self, :customer_text,
target: 'customer_text',
partial: 'meetings/customer_text',
locals: { customer_text: customer_text }
end
private
def broadcast_to_replace_badge
broadcast_replace_to 'queued_count',
target: nil,
targets: '.queued-count',
locals: { queued_count: Meeting.badge },
partial: 'shared/badge'
end
def broadcast_to_replace_active_and_queued_count
broadcast_replace_to 'active_queued_count',
target: 'active-queued-count',
locals: {
active_count: Meeting.with_state(:active).count,
queued_count: Meeting.badge
},
partial: 'shared/active_queued_count'
end
def broadcast_to_replace_customer_meeting_page
broadcast_replace_to self, :customer_meeting,
target: 'customer_meeting',
partial: 'meetings/active'
end
def broadcast_to_replace_customer_when_agent_finish
broadcast_replace_to self, :customer_meeting,
target: 'customer_meeting',
partial: 'meetings/thanks'
end
def broadcast_to_prepend_in_active_meetings
broadcast_prepend_to 'active_meetings',
locals: { meeting: self },
partial: 'admin/meetings/active_meeting_line',
target: 'active-meetings-target'
end
def broadcast_to_remove_in_active_meetings
broadcast_remove_to 'active_meetings',
target: "meeting_#{id}"
end
def broadcast_to_replace_in_active_meetings
broadcast_replace_to 'active_meetings',
locals: { meeting: self },
partial: 'admin/meetings/active_meeting_line',
target: "meeting_#{id}"
end
end
# frozen_string_literal: true
require 'application_system_test_case'
class CustomerMeetingTest < ApplicationSystemTestCase
include VirtualConsultation::TestHelper
include ActiveJob::TestHelper
setup do
@agent = users(:agent)
@meeting = stub_chime
@customer = customers(:one)
@agent.set_online
end
test 'Customer initiate call' do
@agent.set_offline
manual_create_meeitng
wait_to_broadcasts
match_text(t('customer.mobile.wait_page.sorry'),
t('customer.wait.all_offline'))
assert_equal 'queued', current_meeting.state
assert_predicate all('div.loading-box').length, :zero?
end
test 'should create customer' do
manual_create_meeitng
assert_text t('customer.mobile.wait_page.connecting')
assert_text t('customer.wait.please_wait')
assert_predicate all('div.loading-box').length, :positive?
end
test 'Should update text via turbo' do
manual_create_meeitng
assert_enqueued_with(job: UpdateCustomerTextJob)
assert_broadcasts_text(text_before: t('customer.wait.please_wait'),
text_after: t('customer.wait.please_wait'),
to: :customer_text) do
@agent.set_offline
perform_enqueued_jobs
end
end
test 'Update meeting page if agent is take call' do
manual_create_meeitng
agent_serve(@customer.meeting)
video_buttons = all('.mobile-call-buttons')
assert_predicate video_buttons, :present?
end
test 'customer leave call' do
manual_create_meeitng
agent_serve(current_meeting)
accept_confirm do
find('button[data-action="chime#leave"]').click
end
wait_to_broadcasts
assert_text t('customer.mobile.leave_page.button_name')
end
test 'Auto finish call when agent finish call' do
manual_create_meeitng
agent_serve(current_meeting)
agent_finish(current_meeting)
assert_text t('customer.mobile.leave_page.button_name')
end
test "it contains 'Privacy Policy' and 'Terms and Conditions'" do
manual_create_meeitng
assert_text t('customer.privacy_policy')
assert_text t('customer.terms_and_conditions')
end
private
def current_meeting
Meeting.find_by(aws_id: @meeting[:meeting_id])
end
def match_text(first_text, second_text)
assert_text first_text
assert_text second_text
end
end
# frozen_string_literal: true
# == Schema Information
#
# Table name: meetings
#
# id :bigint not null, primary key
# agent_finish :datetime
# agent_start :datetime
# client_finish :datetime
# client_start :datetime
# coupon_code :string
# interested_service :integer
# notes :text
# state :string
# system_finish :datetime
# created_at :datetime not null
# updated_at :datetime not null
# aws_id :string
# customer_id :bigint not null
# user_id :bigint
#
# Indexes
#
# index_meetings_on_customer_id (customer_id)
# index_meetings_on_user_id (user_id)
#
# Foreign Keys
#
# fk_rails_... (customer_id => customers.id)
# fk_rails_... (user_id => users.id)
#
class Meeting < ApplicationRecord
NOT_PICKED_TIMEOUT = 5.minutes
include Meeting::Broadcasts
include Meeting::States
include Paginatable
include Notifiable
belongs_to :user, optional: true, inverse_of: :meetings
belongs_to :customer, inverse_of: :meetings
scope :not_finished, -> { without_state(:archived) }
scope :finished, -> { with_state(:archived) }
scope :queued, -> { with_state(:queued).order(client_start: :desc) }
enum interested_service: {
need_haircut_recommendations: 0,
need_hair_color_recommendations: 1,
need_product_recommendations: 2,
need_advice_on_how_to_self_color_my_hair: 3,
need_advice_on_hair_styling: 4
}
ransacker :client_start do
Arel.sql('DATE(meetings.client_start)')
end
def customer_text
return { title: title, message: message } if User.all_agents_offline?
return { title: title, message: message } if User.all_agents_on_call?
return unless client_start
{ title: title, message: message }
end
def wait_duration
return nil if agent_start.nil? && client_start.nil?
((agent_start - client_start) / 1.minute).round
end
def finish_time
return nil if agent_finish.nil? && client_finish.nil?
return client_finish if agent_finish.nil?
return agent_finish if client_finish.nil?
[client_finish, agent_finish].min_by(&:to_i)
end
def duration
return nil if agent_start.nil? || finish_time.nil?
((finish_time - agent_start) / 1.minute).round
end
def initial_time_and_served
"#{client_start&.strftime('%H:%M %p') || '-'} / #{
agent_start&.strftime('%H:%M %p') || '-'
}"
end
def self.badge
queued.count
end
private
def title
if User.all_agents_offline? || User.all_agents_on_call? ||
(User.at_least_one_online? &&
DateTime.current - NOT_PICKED_TIMEOUT >= client_start)
return I18n.t('customer.mobile.wait_page.sorry')
end
I18n.t('customer.mobile.wait_page.connecting')
end
def message
if User.all_agents_offline?
I18n.t('customer.wait.all_offline')
elsif User.all_agents_on_call?
I18n.t('customer.wait.all_on_call')
elsif User.at_least_one_online? &&
DateTime.current - NOT_PICKED_TIMEOUT >= client_start
I18n.t('customer.wait.not_picked')
else
I18n.t('customer.wait.please_wait')
end
end
end
# frozen_string_literal: true
# == Schema Information
#
# Table name: meetings
#
# id :bigint not null, primary key
# agent_finish :datetime
# agent_start :datetime
# client_finish :datetime
# client_start :datetime
# coupon_code :string
# interested_service :integer
# notes :text
# state :string
# system_finish :datetime
# created_at :datetime not null
# updated_at :datetime not null
# aws_id :string
# customer_id :bigint not null
# user_id :bigint
#
# Indexes
#
# index_meetings_on_customer_id (customer_id)
# index_meetings_on_user_id (user_id)
#
# Foreign Keys
#
# fk_rails_... (customer_id => customers.id)
# fk_rails_... (user_id => users.id)
#
require 'test_helper'
class MeetingTest < ActiveSupport::TestCase
include ActionCable::TestHelper
def setup
@customer = customers(:one)
@meeting = meetings(:one)
@meeting.update!(customer: @customer, state: 'initialized')
@archived_meeting = meetings(:archived)
@agent = users(:agent)
end
test '#customer_text - agent online and super not' do
super_admin = users(:super_admin)
super_admin.update!(state: 'online')
@agent.update!(state: 'offline')
@meeting.update!(state: 'queued', client_start: DateTime.current)
assert_equal t('customer.mobile.wait_page.connecting'),
@meeting.customer_text[:title]
assert_equal t('customer.wait.please_wait'),
@meeting.customer_text[:message]
end
test '#customer_text - all agents offline' do
User.agents.update!(state: 'offline')
@meeting.update!(state: 'queued')
assert_equal t('customer.mobile.wait_page.sorry'),
@meeting.customer_text[:title]
assert_equal t('customer.wait.all_offline'),
@meeting.customer_text[:message]
end
test '#customer_text - all agents on call' do
User.agents.update!(state: 'on_call')
assert_equal t('customer.mobile.wait_page.sorry'),
@meeting.customer_text[:title]
assert_equal t('customer.wait.all_on_call'),
@meeting.customer_text[:message]
end
test '#customer_text - call not picked' do
User.agents.update(state: 'offline')
@agent.update!(state: 'online')
start_time = DateTime.current - Meeting::NOT_PICKED_TIMEOUT - 1.minute
@meeting.update!(state: 'queued', client_start: start_time)
assert_equal t('customer.mobile.wait_page.sorry'),
@meeting.customer_text[:title]
assert_equal t('customer.wait.not_picked'),
@meeting.customer_text[:message]
end
test '#customer_text - Please wait while we connect' do
User.agents.update(state: 'offline')
@agent.update!(state: 'online')
@meeting.update!(state: 'queued', client_start: DateTime.current)
assert_equal t('customer.mobile.wait_page.connecting'),
@meeting.customer_text[:title]
assert_equal t('customer.wait.please_wait'),
@meeting.customer_text[:message]
end
test '#finish_time both' do
@meeting.client_finish = DateTime.current + 6.minutes
@meeting.agent_finish = DateTime.current + 1.minute
assert_equal @meeting.agent_finish, @meeting.finish_time
end
test '#wait_duration' do
@meeting.client_start = DateTime.current
@meeting.agent_start = DateTime.current + 3.minutes
assert_equal 3, @meeting.wait_duration
end
test '#duration with agent_finish' do
@meeting.client_start = DateTime.current
@meeting.agent_start = DateTime.current
@meeting.agent_finish = DateTime.current + 3.minutes
assert_equal 3, @meeting.duration
end
test '#duration with client_finish' do
@meeting.client_start = DateTime.current
@meeting.agent_start = DateTime.current
@meeting.client_finish = DateTime.current + 3.minutes
assert_equal 3, @meeting.duration
end
test '#duration with both finish' do
@meeting.client_start = DateTime.current
@meeting.agent_start = DateTime.current
@meeting.client_finish = DateTime.current + 3.minutes
@meeting.agent_finish = DateTime.current + 5.minutes
assert_equal 3, @meeting.duration
end
test '#duration w/o agent_start' do
@meeting.client_start = DateTime.current
@meeting.agent_start = nil
@meeting.agent_finish = nil
assert_nil @meeting.duration
end
test '#duration w/o agent_finish && client_finish' do
@meeting.client_start = DateTime.current
@meeting.agent_start = DateTime.current
@meeting.agent_finish = nil
@meeting.client_finish = nil
assert_nil @meeting.duration
end
end
# frozen_string_literal: true
# Describing states for meeting
module Meeting::States # rubocop:disable Style/ClassAndModuleChildren
extend ActiveSupport::Concern
included do
# Current states
# 'initialized' - model is created but meeting not created at Chime
# 'queued' - customer initiate meeting
# 'active' - customer and agent in call
# 'client_finished' - customer finish call
# 'agent_finished' - agent finish call
# 'system_finished' - system finish call
# 'archived' - archived call
state_machine :state, initial: :initialized do
event :client_start_call do
transition initialized: :queued
end
event :agent_start_call do
transition queued: :active
end
event :client_finish_call do
transition %i[queued active agent_finished] => :client_finished
end
event :agent_finish_call do
transition %i[active client_finished] => :agent_finished
end
event :system_finish_call do
transition to: :system_finished
end
event :archived do
transition %i[client_finished
agent_finished
system_finished] => :archived
end
after_transition to: :queued, do: :queued_callback
after_transition to: :active, do: :active_callback
before_transition from: :active, do: :before_from_active_callback
after_transition from: :active,
do: %i[from_active_callback call_state_changed_callback]
after_transition to: :client_finished, do: :client_finish_callback
after_transition to: :agent_finished, do: :agent_finish_callback
after_transition to: :system_finished, do: :system_finish_callback
after_transition to: :archived, do: :archived_callback
after_transition to: %i[queued active], do: :call_state_changed_callback
end
end
private
def before_from_active_callback
user.set_online! if user&.on_call? && user.meeting == self
end
# meeting is finished
def from_active_callback
broadcast_to_remove_in_active_meetings
end
# when client init call
def queued_callback
update!(client_start: DateTime.current)
broadcast_to_prepend_in_active_meetings
create_notifications
UpdateCustomerTextJob.set(wait_until: DateTime.current +
Meeting::NOT_PICKED_TIMEOUT)
.perform_later(self)
end
# when agent serve call
def active_callback
update(agent_start: DateTime.current)
disable_notifications
broadcast_to_replace_customer_meeting_page
broadcast_to_replace_in_active_meetings
end
def system_finish_callback
update(system_finish: DateTime.current)
archived!
end
def client_finish_callback
update(client_finish: DateTime.current)
archived!
end
def agent_finish_callback
update(agent_finish: DateTime.current)
archived!
end
def archived_callback
disable_notifications
broadcast_to_remove_in_active_meetings
broadcast_to_replace_customer_when_agent_finish
end
def call_state_changed_callback
broadcast_to_replace_badge
broadcast_to_replace_active_and_queued_count
end
end
# frozen_string_literal: true
require 'test_helper'
class Meeting::StatesTest < ActiveSupport::TestCase # rubocop:disable Style/ClassAndModuleChildren
include ActiveJob::TestHelper
def setup
@customer = customers(:one)
@meeting = meetings(:one)
@meeting.update!(customer: @customer, state: 'initialized')
@archived_meeting = meetings(:archived)
@agent = users(:agent)
end
test '#client_start_call! - fill client_start' do
@meeting.client_start_call!
assert_equal 'queued', @meeting.state
assert_predicate @meeting.client_start, :present?
end
test '#agent_start_call!' do
@meeting.state = 'queued'
@meeting.agent_start_call!
assert_equal 'active', @meeting.state
assert_predicate @meeting.agent_start, :present?
end
test '#client_finish_call!' do
@meeting.state = 'active'
@meeting.client_finish_call!
assert_equal 'archived', @meeting.state
assert_predicate @meeting.client_finish, :present?
end
test '#agent_finish_call!' do
@meeting.state = 'active'
@meeting.agent_finish_call!
assert_equal 'archived', @meeting.state
assert_predicate @meeting.agent_finish, :present?
end
test '#system_finish_call!' do
agent = users(:agent)
agent.update(state: 'on_call')
@meeting.state = 'active'
@meeting.update(user: agent)
@meeting.system_finish_call!
assert_equal 'archived', @meeting.state
assert_predicate @meeting.system_finish, :present?
end
test '#system_finish_call! - disable notifications' do
meeting = meetings(:queued)
assert_equal 1, meeting.notifications.enabled.count
meeting.system_finish_call!
assert_equal 0, meeting.reload.notifications.enabled.count
end
test 'online update online_time' do
@agent.set_online!
assert_not_nil @agent.last_seen
end
test 'switch user to online if meeting not active' do
meeting = meetings(:queued)
customer = meeting.customer
@agent.set_online!
@agent.start_callback_for(meeting)
assert_equal 'on_call', @agent.reload.state
customer.finish_callback_for(meeting)
assert_equal 'online', @agent.reload.state
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment