parent
3d64ba49fc
commit
65ed4c78a4
23 changed files with 329 additions and 7 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -59,4 +59,4 @@ package-lock.json
|
|||
|
||||
|
||||
# cypress
|
||||
test/cypress/videos/*
|
||||
test/cypress/videos/*
|
||||
|
|
|
@ -80,6 +80,7 @@ class Api::V1::Accounts::InboxesController < Api::V1::Accounts::BaseController
|
|||
|
||||
def inbox_update_params
|
||||
params.permit(:enable_auto_assignment, :name, :avatar, :greeting_message, :greeting_enabled,
|
||||
:working_hours_enabled, :out_of_office_message,
|
||||
channel: [
|
||||
:website_url,
|
||||
:widget_color,
|
||||
|
|
18
app/controllers/api/v1/accounts/working_hours_controller.rb
Normal file
18
app/controllers/api/v1/accounts/working_hours_controller.rb
Normal file
|
@ -0,0 +1,18 @@
|
|||
class Api::V1::Accounts::WorkingHoursController < Api::V1::Accounts::BaseController
|
||||
before_action :check_authorization
|
||||
before_action :fetch_webhook, only: [:update]
|
||||
|
||||
def update
|
||||
@working_hour.update!(working_hour_params)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def working_hour_params
|
||||
params.require(:working_hour).permit(:inbox_id, :open_hour, :open_minutes, :close_hour, :close_minutes, :closed_all_day)
|
||||
end
|
||||
|
||||
def fetch_working_hour
|
||||
@working_hour = Current.account.working_hours.find(params[:id])
|
||||
end
|
||||
end
|
|
@ -9,6 +9,7 @@
|
|||
# name :string not null
|
||||
# settings_flags :integer default(0), not null
|
||||
# support_email :string(100)
|
||||
# timezone :string default("UTC")
|
||||
# created_at :datetime not null
|
||||
# updated_at :datetime not null
|
||||
#
|
||||
|
@ -48,6 +49,7 @@ class Account < ApplicationRecord
|
|||
has_many :labels, dependent: :destroy
|
||||
has_many :notification_settings, dependent: :destroy
|
||||
has_many :hooks, dependent: :destroy, class_name: 'Integrations::Hook'
|
||||
has_many :working_hours, dependent: :destroy
|
||||
has_many :kbase_portals, dependent: :destroy, class_name: '::Kbase::Portal'
|
||||
has_many :kbase_categories, dependent: :destroy, class_name: '::Kbase::Category'
|
||||
has_many :kbase_articles, dependent: :destroy, class_name: '::Kbase::Article'
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
#
|
||||
# id :integer not null, primary key
|
||||
# feature_flags :integer default(3), not null
|
||||
# reply_time :integer default(0)
|
||||
# reply_time :integer default("in_a_few_minutes")
|
||||
# website_token :string
|
||||
# website_url :string
|
||||
# welcome_tagline :string
|
||||
|
|
30
app/models/concerns/out_of_offisable.rb
Normal file
30
app/models/concerns/out_of_offisable.rb
Normal file
|
@ -0,0 +1,30 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module OutOfOffisable
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
included do
|
||||
has_many :working_hours, dependent: :destroy
|
||||
after_create :create_default_working_hours
|
||||
end
|
||||
|
||||
def out_of_office?
|
||||
working_hours_enabled? && working_hours.today.closed_now?
|
||||
end
|
||||
|
||||
def working_now?
|
||||
!out_of_office?
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def create_default_working_hours
|
||||
working_hours.create!(day_of_week: 1, open_hour: 9, open_minutes: 0, close_hour: 17, close_minutes: 0)
|
||||
working_hours.create!(day_of_week: 2, open_hour: 9, open_minutes: 0, close_hour: 17, close_minutes: 0)
|
||||
working_hours.create!(day_of_week: 3, open_hour: 9, open_minutes: 0, close_hour: 17, close_minutes: 0)
|
||||
working_hours.create!(day_of_week: 4, open_hour: 9, open_minutes: 0, close_hour: 17, close_minutes: 0)
|
||||
working_hours.create!(day_of_week: 5, open_hour: 9, open_minutes: 0, close_hour: 17, close_minutes: 0)
|
||||
working_hours.create!(day_of_week: 6, closed_all_day: true)
|
||||
working_hours.create!(day_of_week: 7, closed_all_day: true)
|
||||
end
|
||||
end
|
|
@ -11,6 +11,8 @@
|
|||
# greeting_enabled :boolean default(FALSE)
|
||||
# greeting_message :string
|
||||
# name :string not null
|
||||
# out_of_office_message :string
|
||||
# working_hours_enabled :boolean default(FALSE)
|
||||
# created_at :datetime not null
|
||||
# updated_at :datetime not null
|
||||
# account_id :integer not null
|
||||
|
@ -24,6 +26,7 @@
|
|||
class Inbox < ApplicationRecord
|
||||
include Reportable
|
||||
include Avatarable
|
||||
include OutOfOffisable
|
||||
|
||||
validates :account_id, presence: true
|
||||
|
||||
|
|
|
@ -62,6 +62,7 @@ class Message < ApplicationRecord
|
|||
# .succ is a hack to avoid https://makandracards.com/makandra/1057-why-two-ruby-time-objects-are-not-equal-although-they-appear-to-be
|
||||
scope :unread_since, ->(datetime) { where('EXTRACT(EPOCH FROM created_at) > (?)', datetime.to_i.succ) }
|
||||
scope :chat, -> { where.not(message_type: :activity).where(private: false) }
|
||||
scope :today, -> { where("date_trunc('day', created_at) = ?", Date.current) }
|
||||
default_scope { order(created_at: :asc) }
|
||||
|
||||
belongs_to :account
|
||||
|
|
71
app/models/working_hour.rb
Normal file
71
app/models/working_hour.rb
Normal file
|
@ -0,0 +1,71 @@
|
|||
# == Schema Information
|
||||
#
|
||||
# Table name: working_hours
|
||||
#
|
||||
# id :bigint not null, primary key
|
||||
# close_hour :integer
|
||||
# close_minutes :integer
|
||||
# closed_all_day :boolean default(FALSE)
|
||||
# day_of_week :integer not null
|
||||
# open_hour :integer
|
||||
# open_minutes :integer
|
||||
# created_at :datetime not null
|
||||
# updated_at :datetime not null
|
||||
# account_id :bigint
|
||||
# inbox_id :bigint
|
||||
#
|
||||
# Indexes
|
||||
#
|
||||
# index_working_hours_on_account_id (account_id)
|
||||
# index_working_hours_on_inbox_id (inbox_id)
|
||||
#
|
||||
class WorkingHour < ApplicationRecord
|
||||
belongs_to :inbox
|
||||
|
||||
before_save :assign_account
|
||||
|
||||
validates :open_hour, presence: true, unless: :closed_all_day?
|
||||
validates :open_minutes, presence: true, unless: :closed_all_day?
|
||||
validates :close_hour, presence: true, unless: :closed_all_day?
|
||||
validates :close_minutes, presence: true, unless: :closed_all_day?
|
||||
|
||||
validates :open_hour, inclusion: 0..23, unless: :closed_all_day?
|
||||
validates :close_hour, inclusion: 0..23, unless: :closed_all_day?
|
||||
validates :open_minutes, inclusion: 0..59, unless: :closed_all_day?
|
||||
validates :close_minutes, inclusion: 0..59, unless: :closed_all_day?
|
||||
|
||||
validate :close_after_open, unless: :closed_all_day?
|
||||
|
||||
def self.today
|
||||
find_by(day_of_week: Date.current.cwday)
|
||||
end
|
||||
|
||||
def open_at?(time)
|
||||
return false if closed_all_day?
|
||||
|
||||
time.hour >= open_hour &&
|
||||
time.min >= open_minutes &&
|
||||
time.hour <= close_hour &&
|
||||
time.min <= close_minutes
|
||||
end
|
||||
|
||||
def open_now?
|
||||
open_at?(Time.zone.now)
|
||||
end
|
||||
|
||||
def closed_now?
|
||||
!open_now?
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def assign_account
|
||||
self.account_id = inbox.account_id
|
||||
end
|
||||
|
||||
def close_after_open
|
||||
return unless open_hour.hours + open_minutes.minutes >= close_hour.hours + close_minutes.minutes
|
||||
|
||||
errors.add(:close_hour, 'Closing time cannot be before opening time')
|
||||
end
|
||||
end
|
|
@ -4,8 +4,8 @@ class MessageTemplates::HookExecutionService
|
|||
def perform
|
||||
return if inbox.agent_bot_inbox&.active?
|
||||
|
||||
::MessageTemplates::Template::OutOfOffice.new(conversation: conversation).perform if should_send_out_of_office_message?
|
||||
::MessageTemplates::Template::Greeting.new(conversation: conversation).perform if should_send_greeting?
|
||||
|
||||
::MessageTemplates::Template::EmailCollect.new(conversation: conversation).perform if should_send_email_collect?
|
||||
end
|
||||
|
||||
|
@ -14,12 +14,16 @@ class MessageTemplates::HookExecutionService
|
|||
delegate :inbox, :conversation, to: :message
|
||||
delegate :contact, to: :conversation
|
||||
|
||||
def should_send_out_of_office_message?
|
||||
inbox.out_of_office? && conversation.messages.today.template.empty? && inbox.out_of_office_message.present?
|
||||
end
|
||||
|
||||
def first_message_from_contact?
|
||||
conversation.messages.outgoing.count.zero? && conversation.messages.template.count.zero?
|
||||
end
|
||||
|
||||
def should_send_greeting?
|
||||
first_message_from_contact? && conversation.inbox.greeting_enabled? && conversation.inbox.greeting_message.present?
|
||||
first_message_from_contact? && inbox.greeting_enabled? && inbox.greeting_message.present?
|
||||
end
|
||||
|
||||
def email_collect_was_sent?
|
||||
|
@ -27,7 +31,7 @@ class MessageTemplates::HookExecutionService
|
|||
end
|
||||
|
||||
def should_send_email_collect?
|
||||
!contact_has_email? && conversation.inbox.web_widget? && !email_collect_was_sent?
|
||||
!contact_has_email? && inbox.web_widget? && !email_collect_was_sent?
|
||||
end
|
||||
|
||||
def contact_has_email?
|
||||
|
|
28
app/services/message_templates/template/out_of_office.rb
Normal file
28
app/services/message_templates/template/out_of_office.rb
Normal file
|
@ -0,0 +1,28 @@
|
|||
class MessageTemplates::Template::OutOfOffice
|
||||
pattr_initialize [:conversation!]
|
||||
|
||||
def perform
|
||||
ActiveRecord::Base.transaction do
|
||||
conversation.messages.create!(out_of_office_message_params)
|
||||
end
|
||||
rescue StandardError => e
|
||||
Raven.capture_exception(e)
|
||||
true
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
delegate :contact, :account, to: :conversation
|
||||
delegate :inbox, to: :message
|
||||
|
||||
def out_of_office_message_params
|
||||
content = @conversation.inbox&.out_of_office_message
|
||||
|
||||
{
|
||||
account_id: @conversation.account_id,
|
||||
inbox_id: @conversation.inbox_id,
|
||||
message_type: :template,
|
||||
content: content
|
||||
}
|
||||
end
|
||||
end
|
|
@ -100,6 +100,7 @@ Rails.application.routes.draw do
|
|||
resources :apps, only: [:index, :show]
|
||||
resource :slack, only: [:create, :update, :destroy], controller: 'slack'
|
||||
end
|
||||
resources :working_hours, only: [:update]
|
||||
|
||||
namespace :kbase do
|
||||
resources :portals do
|
||||
|
|
40
db/migrate/20201027135006_create_working_hours.rb
Normal file
40
db/migrate/20201027135006_create_working_hours.rb
Normal file
|
@ -0,0 +1,40 @@
|
|||
class CreateWorkingHours < ActiveRecord::Migration[6.0]
|
||||
def change
|
||||
create_table :working_hours do |t|
|
||||
t.belongs_to :inbox
|
||||
t.belongs_to :account
|
||||
|
||||
t.integer :day_of_week, null: false
|
||||
t.boolean :closed_all_day, default: false
|
||||
t.integer :open_hour
|
||||
t.integer :open_minutes
|
||||
t.integer :close_hour
|
||||
t.integer :close_minutes
|
||||
|
||||
t.timestamps
|
||||
end
|
||||
|
||||
add_column :accounts, :timezone, :string, default: 'UTC'
|
||||
|
||||
change_table :inboxes, bulk: true do |t|
|
||||
t.boolean :working_hours_enabled, default: false
|
||||
t.string :out_of_office_message
|
||||
end
|
||||
|
||||
Inbox.where.not(id: WorkingHour.select(:inbox_id)).each do |inbox|
|
||||
create_working_hours_for_inbox(inbox)
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def create_working_hours_for_inbox(inbox)
|
||||
inbox.working_hours.create!(day_of_week: 1, open_hour: 9, open_minutes: 0, close_hour: 17, close_minutes: 0)
|
||||
inbox.working_hours.create!(day_of_week: 2, open_hour: 9, open_minutes: 0, close_hour: 17, close_minutes: 0)
|
||||
inbox.working_hours.create!(day_of_week: 3, open_hour: 9, open_minutes: 0, close_hour: 17, close_minutes: 0)
|
||||
inbox.working_hours.create!(day_of_week: 4, open_hour: 9, open_minutes: 0, close_hour: 17, close_minutes: 0)
|
||||
inbox.working_hours.create!(day_of_week: 5, open_hour: 9, open_minutes: 0, close_hour: 17, close_minutes: 0)
|
||||
inbox.working_hours.create!(day_of_week: 6, closed_all_day: true)
|
||||
inbox.working_hours.create!(day_of_week: 7, closed_all_day: true)
|
||||
end
|
||||
end
|
20
db/schema.rb
20
db/schema.rb
|
@ -10,7 +10,7 @@
|
|||
#
|
||||
# It's strongly recommended that you check this file into your version control system.
|
||||
|
||||
ActiveRecord::Schema.define(version: 2020_10_19_173944) do
|
||||
ActiveRecord::Schema.define(version: 2020_10_27_135006) do
|
||||
|
||||
# These are extensions that must be enabled in order to support this database
|
||||
enable_extension "pg_stat_statements"
|
||||
|
@ -49,6 +49,7 @@ ActiveRecord::Schema.define(version: 2020_10_19_173944) do
|
|||
t.string "support_email", limit: 100
|
||||
t.integer "settings_flags", default: 0, null: false
|
||||
t.integer "feature_flags", default: 0, null: false
|
||||
t.string "timezone", default: "UTC"
|
||||
end
|
||||
|
||||
create_table "action_mailbox_inbound_emails", force: :cascade do |t|
|
||||
|
@ -280,6 +281,8 @@ ActiveRecord::Schema.define(version: 2020_10_19_173944) do
|
|||
t.boolean "greeting_enabled", default: false
|
||||
t.string "greeting_message"
|
||||
t.string "email_address"
|
||||
t.boolean "working_hours_enabled", default: false
|
||||
t.string "out_of_office_message"
|
||||
t.index ["account_id"], name: "index_inboxes_on_account_id"
|
||||
end
|
||||
|
||||
|
@ -513,6 +516,21 @@ ActiveRecord::Schema.define(version: 2020_10_19_173944) do
|
|||
t.index ["account_id", "url"], name: "index_webhooks_on_account_id_and_url", unique: true
|
||||
end
|
||||
|
||||
create_table "working_hours", force: :cascade do |t|
|
||||
t.bigint "inbox_id"
|
||||
t.bigint "account_id"
|
||||
t.integer "day_of_week", null: false
|
||||
t.boolean "closed_all_day", default: false
|
||||
t.integer "open_hour"
|
||||
t.integer "open_minutes"
|
||||
t.integer "close_hour"
|
||||
t.integer "close_minutes"
|
||||
t.datetime "created_at", precision: 6, null: false
|
||||
t.datetime "updated_at", precision: 6, null: false
|
||||
t.index ["account_id"], name: "index_working_hours_on_account_id"
|
||||
t.index ["inbox_id"], name: "index_working_hours_on_inbox_id"
|
||||
end
|
||||
|
||||
add_foreign_key "account_users", "accounts"
|
||||
add_foreign_key "account_users", "users"
|
||||
add_foreign_key "active_storage_attachments", "active_storage_blobs", column: "blob_id"
|
||||
|
|
|
@ -2,4 +2,9 @@ module Current
|
|||
thread_mattr_accessor :user
|
||||
thread_mattr_accessor :account
|
||||
thread_mattr_accessor :account_user
|
||||
|
||||
def account=(account)
|
||||
super
|
||||
Time.zone = account.timezone
|
||||
end
|
||||
end
|
||||
|
|
|
@ -10,7 +10,7 @@ FactoryBot.define do
|
|||
|
||||
after(:build) do |message|
|
||||
message.sender ||= message.outgoing? ? create(:user, account: message.account) : create(:contact, account: message.account)
|
||||
message.inbox ||= create(:inbox, account: message.account)
|
||||
message.inbox ||= message.conversation&.inbox || create(:inbox, account: message.account)
|
||||
message.conversation ||= create(:conversation, account: message.account, inbox: message.inbox)
|
||||
end
|
||||
end
|
||||
|
|
12
spec/factories/working_hours.rb
Normal file
12
spec/factories/working_hours.rb
Normal file
|
@ -0,0 +1,12 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
FactoryBot.define do
|
||||
factory :working_hour do
|
||||
inbox
|
||||
day_of_week { 1 }
|
||||
open_hour { 9 }
|
||||
open_minutes { 0 }
|
||||
close_hour { 17 }
|
||||
close_minutes { 0 }
|
||||
end
|
||||
end
|
19
spec/models/concerns/out_of_offisable_spec.rb
Normal file
19
spec/models/concerns/out_of_offisable_spec.rb
Normal file
|
@ -0,0 +1,19 @@
|
|||
require 'rails_helper'
|
||||
|
||||
shared_examples_for 'out_of_offisable' do
|
||||
let(:obj) { create(described_class.to_s.underscore, working_hours_enabled: true, out_of_office_message: 'Message') }
|
||||
|
||||
it 'has after create callback' do
|
||||
expect(obj.working_hours.count).to eq(7)
|
||||
end
|
||||
|
||||
it 'is working on monday 10am' do
|
||||
travel_to '26.10.2020 10:00'.to_datetime
|
||||
expect(obj.working_now?).to be true
|
||||
end
|
||||
|
||||
it 'is out of office on sunday 1pm' do
|
||||
travel_to '01.11.2020 13:00'.to_datetime
|
||||
expect(obj.out_of_office?).to be true
|
||||
end
|
||||
end
|
|
@ -1,6 +1,7 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'rails_helper'
|
||||
require Rails.root.join 'spec/models/concerns/out_of_offisable_spec.rb'
|
||||
|
||||
RSpec.describe Inbox do
|
||||
describe 'validations' do
|
||||
|
@ -33,6 +34,10 @@ RSpec.describe Inbox do
|
|||
it { is_expected.to have_many(:hooks) }
|
||||
end
|
||||
|
||||
describe 'concerns' do
|
||||
it_behaves_like 'out_of_offisable'
|
||||
end
|
||||
|
||||
describe '#add_member' do
|
||||
let(:inbox) { FactoryBot.create(:inbox) }
|
||||
let(:user) { FactoryBot.create(:user) }
|
||||
|
|
29
spec/models/working_hour_spec.rb
Normal file
29
spec/models/working_hour_spec.rb
Normal file
|
@ -0,0 +1,29 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe WorkingHour do
|
||||
context 'when on monday 10am' do
|
||||
before do
|
||||
Time.zone = 'UTC'
|
||||
create(:working_hour)
|
||||
travel_to '26.10.2020 10:00'.to_datetime
|
||||
end
|
||||
|
||||
it 'is considered working hour' do
|
||||
expect(described_class.today.open_now?).to be true
|
||||
end
|
||||
end
|
||||
|
||||
context 'when on sunday 1pm' do
|
||||
before do
|
||||
Time.zone = 'UTC'
|
||||
create(:working_hour, day_of_week: 7, closed_all_day: true)
|
||||
travel_to '01.11.2020 13:00'.to_datetime
|
||||
end
|
||||
|
||||
it 'is considered out of office' do
|
||||
expect(described_class.today.closed_now?).to be true
|
||||
end
|
||||
end
|
||||
end
|
|
@ -65,6 +65,7 @@ RSpec.configure do |config|
|
|||
# config.filter_gems_from_backtrace("gem name")
|
||||
config.include SlackStubs
|
||||
config.include Devise::Test::IntegrationHelpers, type: :request
|
||||
config.include ActiveSupport::Testing::TimeHelpers
|
||||
end
|
||||
|
||||
Shoulda::Matchers.configure do |config|
|
||||
|
|
|
@ -45,4 +45,25 @@ describe ::MessageTemplates::HookExecutionService do
|
|||
expect(email_collect_service).to have_received(:perform)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when it is after working hours' do
|
||||
it 'calls ::MessageTemplates::Template::OutOfOffice' do
|
||||
contact = create :contact
|
||||
conversation = create :conversation, contact: contact
|
||||
|
||||
conversation.inbox.update(working_hours_enabled: true, out_of_office_message: 'We are out of office')
|
||||
conversation.inbox.working_hours.today.update!(closed_all_day: true)
|
||||
|
||||
out_of_office_service = double
|
||||
|
||||
allow(::MessageTemplates::Template::OutOfOffice).to receive(:new).and_return(out_of_office_service)
|
||||
allow(out_of_office_service).to receive(:perform).and_return(true)
|
||||
|
||||
# described class gets called in message after commit
|
||||
message = create(:message, conversation: conversation)
|
||||
|
||||
expect(::MessageTemplates::Template::OutOfOffice).to have_received(:new).with(conversation: message.conversation)
|
||||
expect(out_of_office_service).to have_received(:perform)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -0,0 +1,13 @@
|
|||
require 'rails_helper'
|
||||
|
||||
describe ::MessageTemplates::Template::OutOfOffice do
|
||||
context 'when this hook is called' do
|
||||
let(:conversation) { create(:conversation) }
|
||||
|
||||
it 'creates the out of office messages' do
|
||||
described_class.new(conversation: conversation).perform
|
||||
expect(conversation.messages.template.count).to eq(1)
|
||||
expect(conversation.messages.template.first.content).to eq(conversation.inbox.out_of_office_message)
|
||||
end
|
||||
end
|
||||
end
|
Loading…
Reference in a new issue