-
1
class ContactBuilder
-
1
def initialize(user, params, search_if_exists = false)
-
7
@params = params
-
7
@user = user
-
7
@search_if_exists = search_if_exists
-
end
-
-
1
def perform
-
7
if @search_if_exists
-
5
@contact = Accounts::Contacts::GetByParams.call(Current.account, contact_params.slice(:phone, :email).to_h)[:ok]
-
end
-
7
@contact ||= Contact.new
-
7
@contact.assign_attributes(contact_params)
-
7
@contact
-
end
-
-
1
def contact_params
-
12
@params.permit(:full_name, :phone, :email, :label_list,
-
custom_attributes: {}, additional_attributes: {})
-
end
-
end
-
1
class DealBuilder
-
1
include DealConcern
-
-
1
def initialize(user, params)
-
10
@params = params
-
10
@user = user
-
end
-
-
1
def build
-
6
@deal = Deal.new(deal_params.merge(created_by_id: user.id))
-
6
attach_contact_if_needed
-
6
assign_user_to_deal
-
6
deal
-
end
-
-
1
def perform = build
-
-
1
private
-
-
1
attr_reader :user, :params, :deal
-
-
1
def attach_contact_if_needed
-
9
return if deal_params[:contact_id].present? || deal_params[:contact_attributes].blank?
-
-
3
contact = ContactBuilder.new(user, deal_params[:contact_attributes], true).perform
-
3
deal.contact = contact
-
end
-
-
1
def assign_user_to_deal
-
6
deal.deal_assignees.build(user:)
-
end
-
-
1
def deal_params
-
23
params.permit(*permitted_deal_params)
-
end
-
end
-
1
class DealProductBuilder
-
1
include DealProductConcern
-
-
1
def initialize(params)
-
7
@params = params
-
end
-
-
1
def build
-
7
@deal_product = DealProduct.new(deal_product_params)
-
7
set_unit_amount_in_cents
-
7
set_product_identifier
-
7
set_product_name
-
7
@deal_product
-
end
-
-
1
def perform
-
7
build
-
7
@deal_product
-
end
-
-
1
private
-
-
1
def set_unit_amount_in_cents
-
7
product_amount_in_cents = @deal_product.product&.amount_in_cents
-
7
@deal_product.unit_amount_in_cents = product_amount_in_cents
-
end
-
-
1
def set_product_identifier
-
7
product_identifier = @deal_product.product&.identifier
-
7
@deal_product.product_identifier = product_identifier
-
end
-
-
1
def set_product_name
-
7
product_name = @deal_product.product&.name
-
7
@deal_product.product_name = product_name
-
end
-
-
1
def deal_product_params
-
7
@params.permit(
-
*permitted_deal_product_params
-
)
-
end
-
end
-
1
class EventBuilder
-
1
def initialize(user, params)
-
23
@params = params
-
23
@user = user
-
23
@account = user.account
-
end
-
-
1
def build
-
23
@event = @user.account.events.new(@params)
-
23
set_contact
-
23
set_deal
-
23
@event.done = true if @event.kind == 'note'
-
# clean_html_codes()
-
23
build_files if @params.key?('files')
-
23
@event
-
end
-
-
1
def clean_html_codes
-
@event.content.body = '' if @event.content.present? && @event.kind != 'note'
-
end
-
-
1
def set_contact
-
23
if @params.key?(:contact_id)
-
23
@contact = @account.contacts.find(@params[:contact_id])
-
23
@event.contact = @contact
-
end
-
end
-
-
1
def set_deal
-
23
if @params.key?(:deal_id)
-
23
@deal = @account.deals.find(@params[:deal_id])
-
23
@event.deal = @deal
-
end
-
end
-
-
1
def build_files
-
2
result = @params['files'].map.with_index do |file, index|
-
8
if index.zero?
-
2
@event = set_attachment(@event, file)
-
2
next
-
else
-
6
file_event_params = @params.except(:content, :files)
-
6
file_event = EventBuilder.new(@user, file_event_params).build
-
6
file_event = set_attachment(file_event, file)
-
6
file_event
-
end
-
end
-
-
2
@event.files_events = result.compact
-
end
-
-
1
def set_attachment(event, file)
-
8
attachment = event.build_attachment
-
8
attachment.file = file
-
7
attachment.file_type = attachment.check_file_type
-
7
event
-
rescue StandardError
-
1
@event.invalid_files = true
-
-
1
event
-
end
-
end
-
1
class EvolutionApiBuilder
-
1
def initialize(user, params)
-
2
@params = params
-
2
@user = user
-
end
-
-
1
def build
-
2
@evolution_api = @user.account.apps_evolution_apis.new(@params)
-
2
@evolution_api.instance = @evolution_api.generate_token('instance')
-
2
@evolution_api.token = @evolution_api.generate_token('token')
-
2
@evolution_api.endpoint_url = ENV['EVOLUTION_API_ENDPOINT']
-
2
@evolution_api
-
end
-
end
-
1
class ProductBuilder
-
1
def initialize(user, params)
-
6
@params = params
-
6
@user = user
-
end
-
-
1
def build
-
6
@product = @user.account.products.new(@params)
-
6
@product
-
end
-
-
1
def perform
-
6
build
-
6
@product
-
end
-
end
-
1
class Reports::BaseTimeseriesBuilder
-
1
include TimezoneHelper
-
1
include DateRangeHelper
-
1
DEFAULT_GROUP_BY = 'month'.freeze
-
-
1
attr_reader :account, :params
-
-
1
def initialize(account, params)
-
45
raise ArgumentError, 'account is required' unless account
-
44
raise ArgumentError, 'params is required' unless params
-
-
43
@account = account
-
43
@params = params
-
end
-
-
1
def scope
-
18
case params[:type].to_sym
-
when :account
-
17
account
-
when :stage
-
1
stage
-
end
-
end
-
-
1
def stage
-
3
@stage ||= Stage.find(params[:id])
-
end
-
-
1
def group_by
-
16
@group_by ||= %w[day week month year hour].include?(params[:group_by]) ? params[:group_by] : DEFAULT_GROUP_BY
-
end
-
-
1
def timezone
-
80
@timezone ||= timezone_name_from_offset(params[:timezone_offset])
-
end
-
end
-
1
class Reports::Deals::BaseReportBuilder
-
1
def initialize(account, params)
-
29
raise ArgumentError, 'account is required' unless account
-
27
raise ArgumentError, 'params is required' unless params
-
-
25
@account = account
-
25
@params = params
-
end
-
-
1
private
-
-
1
attr_reader :account, :params
-
-
COUNT_METRICS = %w[
-
1
won_deals_count
-
lost_deals_count
-
open_deals_count
-
all_deals_count
-
].freeze
-
-
SUM_METRICS = %w[
-
1
won_deals_sum
-
lost_deals_sum
-
open_deals_sum
-
all_deals_sum
-
].freeze
-
-
1
def builder_class(metric)
-
23
case metric
-
when *COUNT_METRICS
-
12
Reports::Deals::Timeseries::CountReportBuilder
-
when *SUM_METRICS
-
8
Reports::Deals::Timeseries::SumReportBuilder
-
end
-
end
-
-
1
def log_invalid_metric
-
3
Rails.logger.error "ReportBuilder: Invalid metric - #{params[:metric]}"
-
-
3
{}
-
end
-
end
-
1
class Reports::Deals::MetricBuilder < Reports::Deals::BaseReportBuilder
-
1
def summary
-
{
-
5
title: fetch_summary_name,
-
amount_in_cents: count("#{params[:metric]}_sum"),
-
quantity: count("#{params[:metric]}_count")
-
}
-
end
-
-
1
private
-
-
1
def count(metric)
-
10
builder_class(metric).new(account, builder_params(metric)).aggregate_value
-
end
-
-
1
def builder_params(metric)
-
10
params.merge({ metric: })
-
end
-
-
1
def fetch_summary_name
-
9
case params[:metric].to_sym
-
when :open_deals
-
2
I18n.t('activerecord.models.deal.open_deals')
-
when :lost_deals
-
2
I18n.t('activerecord.models.deal.lost_deals')
-
when :won_deals
-
3
I18n.t('activerecord.models.deal.won_deals')
-
when :all_deals
-
2
I18n.t('activerecord.models.deal.created_deals')
-
end
-
end
-
end
-
1
class Reports::Deals::ReportBuilder < Reports::Deals::BaseReportBuilder
-
1
def timeseries
-
4
perform_action(:timeseries)
-
end
-
-
1
def aggregate_value
-
2
perform_action(:aggregate_value)
-
end
-
-
1
private
-
-
1
def perform_action(method_name)
-
6
return builder.new(account, params).public_send(method_name) if builder.present?
-
-
2
log_invalid_metric
-
end
-
-
1
def builder
-
6
builder_class(params[:metric])
-
end
-
end
-
1
class Reports::Deals::Timeseries::BaseReportBuilder < Reports::BaseTimeseriesBuilder
-
1
def timeseries
-
3
grouped_count.each_with_object([]) do |element, arr|
-
69
event_date, event_count = element
-
-
# The `event_date` is in Date format (without time), such as "Wed, 15 May 2024".
-
# We need a timestamp for the start of the day. However, we can't use `event_date.to_time.to_i`
-
# because it converts the date to 12:00 AM server timezone.
-
# The desired output should be 12:00 AM in the specified timezone.
-
69
arr << { value: event_count, timestamp: event_date.in_time_zone(timezone).to_i }
-
end
-
end
-
-
1
private
-
-
1
def grouped_count
-
# Override this method
-
end
-
-
1
def metric
-
31
filtered_metric = params[:metric].gsub(/_(sum|count)\z/, '')
-
31
@metric ||= filtered_metric
-
end
-
-
1
def object_scope
-
16
scope = send("scope_for_#{metric}")
-
-
16
Query::Filter.new(scope, params[:filter]).call
-
end
-
-
1
def scope_for_won_deals
-
6
scope.deals.won.where(won_at: range)
-
end
-
-
1
def scope_for_lost_deals
-
4
scope.deals.lost.where(lost_at: range)
-
end
-
-
1
def scope_for_open_deals
-
3
scope.deals.open.where(created_at: range)
-
end
-
-
1
def scope_for_all_deals
-
3
scope.deals.where(created_at: range)
-
end
-
-
1
def grouping_field
-
14
case metric.to_sym
-
when :won_deals
-
10
:won_at
-
when :lost_deals
-
2
:lost_at
-
else
-
2
:created_at
-
end
-
end
-
end
-
1
class Reports::Deals::Timeseries::CountReportBuilder < Reports::Deals::Timeseries::BaseReportBuilder
-
1
def aggregate_value
-
5
object_scope.count
-
end
-
-
1
private
-
-
1
def grouped_count
-
6
@grouped_values = object_scope.group_by_period(
-
group_by,
-
grouping_field,
-
default_value: 0,
-
range:,
-
permit: %w[day week month year hour],
-
time_zone: timezone
-
).count
-
end
-
end
-
1
class Reports::Deals::Timeseries::SumReportBuilder < Reports::Deals::Timeseries::BaseReportBuilder
-
1
def aggregate_value
-
5
object_scope.sum(:total_deal_products_amount_in_cents)
-
end
-
-
1
private
-
-
1
def grouped_count
-
4
@grouped_values = object_scope.group_by_period(
-
group_by,
-
grouping_field,
-
default_value: 0,
-
range:,
-
permit: %w[day week month year hour],
-
time_zone: timezone
-
).sum(:total_deal_products_amount_in_cents)
-
end
-
end
-
1
class Reports::Pipeline::StagesMetricBuilder
-
1
include DateRangeHelper
-
-
1
def initialize(account, params)
-
14
raise ArgumentError, 'account is required' unless account
-
13
raise ArgumentError, 'params is required' unless params
-
-
12
@account = account
-
12
@params = params
-
end
-
-
1
def metrics
-
4
return build_metrics if valid_deal_status?
-
-
1
raise ArgumentError, 'invalid metric'
-
end
-
-
1
private
-
-
1
attr_reader :account, :params
-
-
1
def pipeline
-
5
@pipeline ||= Pipeline.find(params[:id])
-
end
-
-
1
def valid_deal_status?
-
9
%i[won_deals lost_deals open_deals all_deals].include?(params[:metric]&.to_sym)
-
end
-
-
1
def build_metrics
-
3
pipeline.stages.order(:position).each_with_object({}) do |stage, hash|
-
4
params_stage = params.merge(metric: "#{params[:metric]}_count", id: stage.id, type: :stage)
-
4
hash[stage.name] = Reports::Deals::ReportBuilder.new(account, params_stage).aggregate_value
-
end
-
end
-
end
-
1
module ApplicationCable
-
1
class Channel < ActionCable::Channel::Base
-
end
-
end
-
1
module ApplicationCable
-
1
class Connection < ActionCable::Connection::Base
-
end
-
end
-
1
class Accounts::AdvancedSearchesController < InternalController
-
1
def index
-
end
-
-
1
def results
-
2
@results = Query::AdvancedSearch.new(current_user, current_user.account, search_params).call
-
end
-
-
1
private
-
-
1
def search_params
-
2
params.permit(:q, :search_type)
-
end
-
end
-
# frozen_string_literal: true
-
-
1
class Accounts::Apps::AiAssistentsController < InternalController
-
1
before_action :set_ai_assistent
-
-
1
def edit; end
-
-
1
def update
-
2
if @ai_assistent.update(ai_assistent_params)
-
1
redirect_to edit_account_apps_ai_assistent_path(current_user.account),
-
notice: t('flash_messages.updated', model: Apps::AiAssistent.model_name.human)
-
else
-
1
render :edit, status: :unprocessable_entity
-
end
-
end
-
-
1
private
-
-
1
def set_ai_assistent
-
3
@ai_assistent = Apps::AiAssistent.first.presence || Apps::AiAssistent.create
-
end
-
-
1
def ai_assistent_params
-
2
params.require(:apps_ai_assistent).permit(:auto_reply, :model, :api_key, :enabled)
-
end
-
end
-
1
class Accounts::Apps::ChatwootsController < InternalController
-
1
before_action :set_chatwoot, only: %i[edit update destroy]
-
-
1
def new
-
2
if current_user.account.apps_chatwoots.blank?
-
1
@chatwoot = current_user.account.apps_chatwoots.new
-
else
-
1
redirect_to edit_account_apps_chatwoot_path(current_user.account, current_user.account.apps_chatwoots.first)
-
end
-
end
-
-
1
def edit; end
-
-
1
def create
-
2
result = Accounts::Apps::Chatwoots::Create.call(current_user.account, chatwoot_params)
-
2
@chatwoot = result[result.keys.first]
-
2
if result.key?(:ok)
-
1
redirect_to edit_account_apps_chatwoot_path(current_user.account, @chatwoot),
-
notice: t('flash_messages.created', model: Apps::Chatwoot.model_name.human)
-
else
-
1
render :new
-
end
-
end
-
-
1
def destroy
-
1
result = Accounts::Apps::Chatwoots::Delete.call(current_user.account, @chatwoot)
-
1
if result.key?(:ok)
-
1
redirect_to account_settings_path(current_user.account),
-
notice: t('flash_messages.deleted', model: Apps::Chatwoot.model_name.human)
-
end
-
end
-
-
1
def update
-
1
@chatwoot.update(chatwoot_params)
-
1
redirect_to edit_account_apps_chatwoot_path(current_user.account, current_user.account.apps_chatwoots.first)
-
end
-
-
1
private
-
-
1
def set_chatwoot
-
3
@chatwoot = current_user.account.apps_chatwoots.first
-
end
-
-
1
def chatwoot_params
-
3
params.require(:apps_chatwoot).permit(:chatwoot_endpoint_url, :chatwoot_account_id, :chatwoot_user_token, :active)
-
end
-
end
-
1
class Accounts::Apps::EvolutionApisController < InternalController
-
1
before_action :set_evolution_api, only: %i[edit update refresh_qr_code pair_qr_code destroy]
-
-
1
def new
-
1
@evolution_api = Apps::EvolutionApi.new
-
end
-
-
1
def create
-
2
result = Accounts::Apps::EvolutionApis::Create.call(current_user, evolution_api_params)
-
2
@evolution_api = result[result.keys.first]
-
2
if result.key?(:ok)
-
1
redirect_to pair_qr_code_account_apps_evolution_api_path(current_user.account, @evolution_api.id)
-
else
-
1
@evolution_api = result[:error]
-
1
render :new, status: :unprocessable_entity
-
end
-
end
-
-
1
def index
-
3
@evolution_apis = current_user.account.apps_evolution_apis.order(updated_at: :desc)
-
3
@pagy, @evolution_apis = pagy(@evolution_apis)
-
end
-
-
1
def edit; end
-
-
1
def update
-
2
if @evolution_api.update(evolution_api_params)
-
1
flash[:notice] = t('flash_messages.updated', model: Apps::EvolutionApi.model_name.human)
-
1
redirect_to edit_account_apps_evolution_api_path(current_user.account, @evolution_api)
-
else
-
1
render :edit, status: :unprocessable_entity
-
end
-
end
-
-
1
def refresh_qr_code
-
1
@evolution_api.update({
-
instance: @evolution_api.generate_token('instance'),
-
token: @evolution_api.generate_token('token')
-
})
-
-
1
Accounts::Apps::EvolutionApis::Instance::Create.call(@evolution_api)
-
end
-
-
1
def destroy
-
1
if @evolution_api.destroy
-
1
respond_to do |format|
-
1
format.html do
-
1
redirect_to account_apps_evolution_apis_path(current_user.account),
-
notice: t('flash_messages.deleted', model: Apps::EvolutionApi.model_name.human)
-
end
-
1
format.turbo_stream
-
end
-
end
-
end
-
-
1
def pair_qr_code; end
-
-
1
private
-
-
1
def set_evolution_api
-
9
@evolution_api = current_user.account.apps_evolution_apis.find(params[:id])
-
end
-
-
1
def evolution_api_params
-
4
params.require(:apps_evolution_api).permit(:name)
-
end
-
end
-
1
class Accounts::AppsController < InternalController
-
1
before_action :set_contact, only: %i[show edit update destroy]
-
-
# GET /contacts or /contacts.json
-
1
def index
-
@apps = current_user.account.apps
-
@pagy, @apps = pagy(@apps)
-
end
-
-
# GET /contacts/new
-
1
def new
-
@contact = Contact.new
-
end
-
-
# GET /contacts/1/edit
-
1
def edit; end
-
-
# POST /contacts or /contacts.json
-
1
def create
-
@contact = current_user.account.contacts.new(contact_params)
-
-
if @contact.save
-
redirect_to account_contact_path(current_user.account, @contact),
-
notice: t('flash_messages.created', model: Contact.model_name.human)
-
else
-
render :new, status: :unprocessable_entity
-
end
-
end
-
-
# PATCH/PUT /contacts/1 or /contacts/1.json
-
1
def update
-
respond_to do |format|
-
if @contact.update(contact_params)
-
redirect_to account_contact_path(current_user.account, @contact),
-
notice: t('flash_messages.updated', model: Contact.model_name.human)
-
else
-
format.html { render :edit, status: :unprocessable_entity }
-
format.json { render json: @contact.errors, status: :unprocessable_entity }
-
end
-
end
-
end
-
-
# DELETE /contacts/1 or /contacts/1.json
-
1
def destroy
-
@contact.destroy
-
respond_to do |format|
-
format.html do
-
redirect_to contacts_url, notice: t('flash_messages.deleted', model: Contact.model_name.human)
-
end
-
format.json { head :no_content }
-
end
-
end
-
-
1
private
-
-
# Use callbacks to share common setup or constraints between actions.
-
1
def set_contact
-
@contact = Contact.find(params[:id])
-
end
-
-
# Only allow a list of trusted parameters through.
-
1
def contact_params
-
params.require(:contact).permit(:full_name, :phone, :email, custom_attributes: {})
-
end
-
end
-
1
class Accounts::AttachmentsController < InternalController
-
1
before_action :set_attachment, only: %i[destroy]
-
-
1
def destroy
-
1
@attachment.destroy
-
1
respond_to do |format|
-
1
format.turbo_stream
-
end
-
end
-
-
1
private
-
-
1
def set_attachment
-
1
@attachment = Attachment.find(params[:id])
-
end
-
end
-
1
class Accounts::Contacts::ChatwootEmbedController < InternalController
-
1
layout 'embed'
-
1
before_action :set_contact, only: %i[show]
-
-
1
def search
-
6
contact = contact_search
-
-
6
if contact.present?
-
3
redirect_to account_chatwoot_embed_path(current_user.account, contact)
-
else
-
3
chatwoot_contact = JSON.parse(params['chatwoot_contact'])
-
3
@contact = current_user.account.contacts.new({
-
full_name: chatwoot_contact['name'],
-
email: chatwoot_contact['email'],
-
phone: chatwoot_contact['phone_number'],
-
additional_attributes: { 'chatwoot_id': chatwoot_contact['id'] }
-
})
-
3
render :new
-
end
-
end
-
-
1
def show; end
-
-
1
def new
-
1
chatwoot_contact = JSON.parse(params['chatwoot_contact'])
-
1
@contact = current_user.account.contacts.new({
-
full_name: chatwoot_contact['name'],
-
email: chatwoot_contact['email'],
-
phone: chatwoot_contact['phone_number'],
-
additional_attributes: { 'chatwoot_id': chatwoot_contact['id'] }
-
})
-
end
-
-
1
def create
-
1
@contact = current_user.account.contacts.new(contact_params)
-
-
1
if @contact.save
-
1
redirect_to account_chatwoot_embed_path(current_user.account, @contact),
-
notice: t('flash_messages.created', model: Contact.model_name.human)
-
else
-
render :new, status: :unprocessable_entity
-
end
-
end
-
-
1
private
-
-
1
def set_contact
-
5
@contact = Contact.find(params[:id])
-
end
-
-
1
def contact_params
-
1
params.require(:contact).permit(:full_name, :phone, :email, additional_attributes: {})
-
end
-
-
1
def chatwoot_contact
-
18
@chatwoot_contact ||= JSON.parse(params['chatwoot_contact'])
-
end
-
-
1
def contact_search
-
6
result = current_user.account.contacts.by_chatwoot_id(chatwoot_contact['id']).first
-
6
return result if result.present?
-
-
6
Accounts::Contacts::GetByParams.call(current_user.account,
-
{ email: chatwoot_contact['email'],
-
phone: chatwoot_contact['phone_number'] })[:ok]
-
end
-
end
-
1
class Accounts::Contacts::EventsController < InternalController
-
1
before_action :set_event, only: %i[show edit update destroy show]
-
1
before_action :set_contact, only: %i[show edit update destroy new]
-
-
1
def new
-
# @event = current_user.account.events.new(event_params.merge({contact: @contact}))
-
2
@event = EventBuilder.new(current_user,
-
event_params.merge({ contact_id: @contact.id, kind: params[:kind],
-
deal_id: params[:deal_id] })).build
-
end
-
-
1
def edit; end
-
-
1
def create
-
14
@event = EventBuilder.new(current_user, event_params).build
-
-
14
if @event.save
-
13
respond_to do |format|
-
13
format.html do
-
13
redirect_to(new_account_contact_event_path(account_id: current_user.account, contact_id: @event.deal.contact.id,
-
deal_id: @event.deal.id))
-
end
-
13
format.turbo_stream
-
end
-
else
-
1
render :new, status: :unprocessable_entity
-
end
-
end
-
-
1
def destroy
-
1
@event.destroy
-
end
-
-
1
def update
-
8
@deal = current_user.account.deals.find(params[:deal_id])
-
8
@events = @deal.contact.events
-
8
render :edit, status: :unprocessable_entity unless @event.update(event_params)
-
end
-
-
1
def show; end
-
-
1
private
-
-
# Use callbacks to share common setup or constraints between actions.
-
1
def set_event
-
9
@event = current_user.account.events.find(params[:id])
-
end
-
-
1
def set_contact
-
11
@contact = current_user.account.contacts.find(params[:contact_id])
-
end
-
-
# Only allow a list of trusted parameters through.
-
1
def event_params
-
24
params.require(:event).permit(:content, :contact_id, :send_now, :done, :deal_id, :auto_done, :title, :scheduled_at, :from_me, :kind, :app_type,
-
:app_id, files: [], custom_attributes: {}, additional_attributes: {})
-
rescue StandardError
-
2
{}
-
end
-
end
-
1
class Accounts::ContactsController < InternalController
-
1
before_action :set_contact, only: %i[show edit update destroy chatwoot_conversation_link hovercard_preview]
-
-
1
def show
-
4
@pagy_deals, @deals = pagy(@contact.deals.order(created_at: :desc), items: 10, page_param: :deals_page)
-
4
respond_to do |format|
-
4
format.html
-
4
format.turbo_stream
-
end
-
end
-
-
# GET /contacts or /contacts.json
-
1
def index
-
8
@contacts = if params[:query].present?
-
7
Contact.where(
-
'full_name ILIKE :search OR email ILIKE :search OR phone ILIKE :search', search: "%#{params[:query]}%"
-
).order(updated_at: :desc)
-
else
-
1
Contact.all.order(created_at: :desc)
-
end
-
-
8
@pagy, @contacts = pagy(@contacts)
-
end
-
-
1
def select_contact_search
-
10
@contacts = if params[:query].present?
-
2
current_user.account.contacts.where(
-
'full_name ILIKE :search OR email ILIKE :search OR phone ILIKE :search', search: "%#{params[:query]}%"
-
).order(updated_at: :desc).limit(5)
-
else
-
8
current_user.account.contacts.order(updated_at: :desc).limit(5)
-
end
-
end
-
-
1
def search
-
@contacts = current_user.account.contacts.where(
-
'full_name ILIKE :search OR email ILIKE :search OR phone ILIKE :search', search: "%#{params[:q]}%"
-
).limit(5).map(&:attributes)
-
-
@results = @contacts.each do |c|
-
c[:text] = "#{c['full_name']} - #{c['email']} - #{c['phone']}"
-
c
-
end
-
-
@results.insert(0, { "id": 0, "text": 'New contact' })
-
-
json = {
-
"results": @results
-
}
-
render json:
-
end
-
-
# GET /contacts/new
-
1
def new
-
1
@contact = Contact.new
-
end
-
-
# GET /contacts/1/edit
-
1
def edit; end
-
-
1
def edit_custom_attributes
-
1
@contact = current_user.account.contacts.find(params[:contact_id])
-
1
@custom_attribute_definitions = current_user.account.custom_attribute_definitions.contact_attribute
-
end
-
-
# POST /contacts or /contacts.json
-
1
def create
-
2
@contact = current_user.account.contacts.new(contact_params)
-
2
if @contact.save
-
1
@pagy_deals, @deals = pagy(@contact.deals.order(created_at: :desc), items: 10, page_param: :deals_page)
-
1
respond_to do |format|
-
1
format.html do
-
1
redirect_to account_contact_path(current_user.account, @contact),
-
notice: t('flash_messages.created', model: Contact.model_name.human)
-
end
-
1
format.turbo_stream
-
end
-
-
else
-
1
render :new, status: :unprocessable_entity
-
end
-
end
-
-
# PATCH/PUT /contacts/1 or /contacts/1.json
-
1
def update
-
2
if params[:contact][:att_key].present?
-
@contact.custom_attributes[params[:contact][:att_key]] = params[:contact][:att_value]
-
end
-
-
2
if @contact.update(contact_params)
-
1
flash[:notice] = t('flash_messages.updated', model: Contact.model_name.human)
-
1
respond_to do |format|
-
2
format.html { redirect_to account_contact_path(current_user.account, @contact) }
-
1
format.turbo_stream
-
end
-
else
-
1
render :edit, status: :unprocessable_entity
-
end
-
end
-
-
# DELETE /contacts/1 or /contacts/1.json
-
1
def destroy
-
1
@contact.destroy
-
1
respond_to do |format|
-
1
format.html do
-
1
redirect_to account_contacts_path(current_user.account),
-
notice: t('flash_messages.deleted', model: Contact.model_name.human)
-
end
-
1
format.json { head :no_content }
-
end
-
end
-
-
1
def chatwoot_conversation_link
-
6
@display_format = params[:display_format].presence || 'icon'
-
6
@chatwoot_conversation_link = Contact::Integrations::Chatwoot::GenerateConversationLink.new(@contact).call[:ok]
-
rescue Faraday::TimeoutError, Faraday::ConnectionFailed, JSON::ParserError
-
2
@connection_error = true
-
end
-
-
1
def hovercard_preview
-
end
-
-
1
private
-
-
# Use callbacks to share common setup or constraints between actions.
-
1
def set_contact
-
14
@contact = Contact.find(params[:id])
-
end
-
-
# Only allow a list of trusted parameters through.
-
1
def contact_params
-
4
params.require(:contact).permit(:full_name, :phone, :email, :label_list,
-
custom_attributes: {})
-
end
-
end
-
1
class Accounts::DealAssigneesController < InternalController
-
1
before_action :set_deal_assignee, only: %i[destroy]
-
1
before_action :set_deal, only: %i[new]
-
-
1
def destroy
-
1
return unless @deal_assignee.destroy
-
-
1
respond_to do |format|
-
1
format.html do
-
1
redirect_to account_deal_path(current_user.account, @deal_assignee.deal),
-
notice: t('flash_messages.deleted', model: DealAssignee.model_name.human)
-
end
-
1
format.turbo_stream
-
end
-
end
-
-
1
def new
-
1
@deal_assignee = @deal.deal_assignees.new
-
end
-
-
1
def create
-
4
@deal_assignee = DealAssignee.new(deal_assignees_params)
-
4
if @deal_assignee.save
-
1
respond_to do |format|
-
2
format.html { redirect_to account_deal_path(@deal_assignee.account, @deal_assignee.deal) }
-
1
format.turbo_stream
-
end
-
else
-
3
render :new, status: :unprocessable_entity
-
end
-
end
-
-
1
private
-
-
1
def deal_assignees_params
-
4
params.require(:deal_assignee).permit(:user_id, :deal_id)
-
end
-
-
1
def set_deal
-
1
@deal = Deal.find(params[:deal_id])
-
end
-
-
1
def set_deal_assignee
-
1
@deal_assignee = DealAssignee.find(params[:id])
-
end
-
end
-
1
class Accounts::DealProductsController < InternalController
-
1
include DealProductConcern
-
-
1
before_action :set_deal_product, only: %i[destroy]
-
1
before_action :set_deal, only: %i[new]
-
-
1
def destroy
-
1
if DealProduct::Destroy.new(@deal_product).call
-
1
respond_to do |format|
-
1
format.html do
-
1
redirect_to account_deal_path(current_user.account, @deal_product.deal),
-
notice: t('flash_messages.deleted', model: Product.model_name.human)
-
end
-
1
format.turbo_stream
-
end
-
end
-
end
-
-
1
def new
-
1
@deal_product = @deal.deal_products.new
-
end
-
-
1
def create
-
2
@deal_product = DealProductBuilder.new(deal_product_params).perform
-
2
if DealProduct::CreateOrUpdate.new(@deal_product, {}).call
-
1
@deal_product.reload
-
1
respond_to do |format|
-
2
format.html { redirect_to account_deal_path(@deal_product.account, @deal_product.deal) }
-
1
format.turbo_stream
-
end
-
else
-
1
render :new, status: :unprocessable_entity
-
end
-
end
-
-
1
private
-
-
1
def deal_product_params
-
2
params.require(:deal_product).permit(*permitted_deal_product_params)
-
end
-
-
1
def set_deal
-
1
@deal = current_user.account.deals.find(params[:deal_id])
-
end
-
-
1
def set_deal_product
-
1
@deal_product = current_user.account.deal_products.find(params[:id])
-
end
-
end
-
1
class Accounts::DealsController < InternalController
-
1
include DealProductConcern
-
1
include DealConcern
-
-
1
before_action :set_deal,
-
only: %i[show edit update destroy events_to_do events_done deal_products deal_assignees mark_as_lost mark_as_won]
-
1
before_action :set_deal_product, only: %i[edit_deal_product
-
update_deal_product]
-
-
# GET /deals or /deals.json
-
1
def index
-
8
@first_pipeline = Pipeline.first
-
8
@deals = if params[:query].present?
-
7
Deal.left_joins(:contact)
-
.where(
-
'deals.name ILIKE :search OR ' +
-
'contacts.full_name ILIKE :search OR ' +
-
'deals.id = :id',
-
search: "%#{params[:query]}%",
-
id: params[:query].to_i
-
)
-
.order(updated_at: :desc)
-
else
-
1
Deal.all.order(created_at: :desc)
-
end
-
-
8
@pagy, @deals = pagy(@deals)
-
end
-
-
# GET /deals/1 or /deals/1.json
-
1
def show; end
-
-
# GET /deals/new
-
1
def new
-
2
@deal = Deal.new
-
2
@stages = Stage.ordered_by_pipeline_and_position
-
2
@deal.contact_id = params.dig(:deal, :contact_id)
-
-
2
if @deal.contact_id.blank?
-
1
@deal.errors.add(:contact, :blank)
-
1
render :new_select_contact, status: :unprocessable_entity
-
return
-
end
-
end
-
-
1
def new_select_contact
-
1
@deal = Deal.new
-
end
-
-
1
def add_contact
-
@deal = Deal.find(params[:deal_id])
-
end
-
-
1
def commit_add_contact
-
@deal = Deal.find(params[:deal_id])
-
@new_contact = Contact.find(params['deal']['contact_id'])
-
@deal.contacts.push(@new_contact)
-
-
if Deal::CreateOrUpdate.new(@deal, deal_params).call
-
redirect_to account_deal_path(current_user.account, @deal)
-
else
-
render :add_contact, status: :unprocessable_entity
-
end
-
rescue StandardError
-
render :add_contact, status: :unprocessable_entity
-
end
-
-
1
def remove_contact
-
@deal = Deal.find(params[:deal_id])
-
@contacts_deal = @deal.contacts_deals.find_by_contact_id(params['contact_id'])
-
-
if @contacts_deal.destroy
-
redirect_to account_deal_path(current_user.account, @deal)
-
else
-
render :show, status: :unprocessable_entity
-
end
-
rescue StandardError
-
render :show, status: :unprocessable_entity
-
end
-
-
# GET /deals/1/edit
-
1
def edit
-
5
@stages = Stage.ordered_by_pipeline_and_position
-
end
-
-
1
def edit_custom_attributes
-
@deal = current_user.account.deals.find(params[:deal_id])
-
@custom_attribute_definitions = current_user.account.custom_attribute_definitions.deal_attribute
-
end
-
-
# POST /deals or /deals.json
-
1
def create
-
1
@stages = Stage.ordered_by_pipeline_and_position
-
1
@deal = DealBuilder.new(current_user, deal_params).perform
-
-
1
if Deal::CreateOrUpdate.new(@deal, deal_params).call
-
1
redirect_to account_deal_path(current_user.account, @deal)
-
else
-
render :new, status: :unprocessable_entity
-
end
-
end
-
-
# PATCH/PUT /deals/1 or /deals/1.json
-
1
def update
-
4
@stages = Stage.ordered_by_pipeline_and_position
-
4
if params[:deal][:att_key].present?
-
@deal.custom_attributes[params[:deal][:att_key]] = params[:deal][:att_value]
-
end
-
-
4
if Deal::CreateOrUpdate.new(@deal, deal_params).call
-
4
respond_to do |format|
-
8
format.html { redirect_to account_deal_path(current_user.account, @deal) }
-
4
format.turbo_stream
-
end
-
else
-
render :edit, status: :unprocessable_entity
-
end
-
end
-
-
# DELETE /deals/1 or /deals/1.json
-
1
def destroy
-
1
@deal.destroy
-
1
respond_to do |format|
-
1
format.turbo_stream
-
2
format.html { redirect_to root_path, notice: t('flash_messages.deleted', model: Deal.model_name.human) }
-
1
format.json { head :no_content }
-
end
-
end
-
-
1
def events_to_do
-
2
@pagy, @events = pagy(@deal.contact.events.where(deal_id: [nil, @deal.id]).to_do, items: 5)
-
2
respond_to do |format|
-
2
format.turbo_stream
-
2
format.html
-
end
-
end
-
-
1
def events_done
-
2
@pagy, @events = pagy(@deal.contact.events.where(deal_id: [nil, @deal.id]).done, items: 5)
-
2
respond_to do |format|
-
2
format.turbo_stream
-
2
format.html
-
end
-
end
-
-
1
def deal_products
-
1
@deal_products = @deal.deal_products
-
end
-
-
1
def deal_assignees
-
1
@deal_assignees = @deal.deal_assignees
-
end
-
-
1
def edit_deal_product
-
end
-
-
1
def update_deal_product
-
1
if DealProduct::CreateOrUpdate.new(@deal_product, deal_product_params).call
-
1
respond_to do |format|
-
1
format.html do
-
1
redirect_to deal_products_account_deal_path(current_user.account, @deal_product.deal)
-
end
-
1
format.turbo_stream
-
end
-
else
-
render :edit_deal_product, status: :unprocessable_entity
-
end
-
end
-
-
1
def mark_as_lost
-
2
@stages = Stage.ordered_by_pipeline_and_position
-
2
@lost_reasons = DealLostReason.order(:name).pluck(:name).uniq
-
2
@exists_deal_lost_reasons = DealLostReason.exists?
-
2
@allow_edit_lost_at = Current.account.deal_allow_edit_lost_at_won_at
-
end
-
-
1
def mark_as_won
-
1
@stages = Stage.ordered_by_pipeline_and_position
-
1
@allow_edit_won_at = Current.account.deal_allow_edit_lost_at_won_at
-
end
-
-
1
private
-
-
1
def set_deal
-
22
@deal = current_user.account.deals.find(params[:id])
-
end
-
-
1
def set_deal_product
-
2
@deal_product = current_user.account.deal_products.find(params[:deal_product_id])
-
end
-
-
1
def deal_product_params
-
1
params.require(:deal_product).permit(*permitted_deal_product_params)
-
end
-
-
# Only allow a list of trusted parameters through.
-
1
def deal_params
-
6
params.require(:deal).permit(*permitted_deal_params)
-
end
-
end
-
1
class Accounts::EventsController < InternalController
-
1
def calendar
-
end
-
-
1
def calendar_events
-
1
start_date = Time.zone.parse(params[:start])
-
1
end_date = Time.zone.parse(params[:end])
-
-
1
events = Event.planned.where(scheduled_at: start_date..end_date)
-
-
1
render json: events.map { |event| {
-
3
id: event.id,
-
title: "#{event.title} - #{event.contact.full_name}",
-
start: event.scheduled_at.iso8601,
-
backgroundColor: events_kind_color(event.kind),
-
borderColor: events_kind_color(event.kind),
-
extendedProps: {
-
account_id: Current.account.id,
-
contact_id: event.contact_id,
-
deal_id: event.deal_id
-
},
-
url: account_deal_path(Current.account, event.deal)
-
}}
-
end
-
-
1
private
-
-
1
def events_kind_color(kind)
-
6
case kind
-
when 'chatwoot_message'
-
2
'#369EF2'
-
when 'evolution_api_message'
-
2
'#26D367'
-
else
-
2
'#6857D9'
-
end
-
end
-
end
-
1
require 'csv'
-
1
require 'json_csv'
-
-
1
class Accounts::PipelinesController < InternalController
-
1
before_action :set_pipeline, only: %i[show edit update destroy bulk_action new_bulk_action]
-
1
before_action :set_bulk_action_event, only: %i[bulk_action new_bulk_action]
-
1
before_action :set_stage, only: %i[bulk_action new_bulk_action]
-
-
# GET /pipelines or /pipelines.json
-
1
def index
-
2
pipeline = Pipeline.first
-
2
if pipeline
-
1
redirect_to(account_pipeline_path(Current.account, pipeline))
-
else
-
1
redirect_to account_welcome_index_path(Current.account)
-
end
-
end
-
-
# GET /pipelines/1 or /pipelines/1.json
-
1
def show
-
@pipelines = Pipeline.all
-
@filter_status_deal = if params[:filter_status_deal].present?
-
params[:filter_status_deal]
-
else
-
'open'
-
end
-
end
-
-
# GET /pipelines/new
-
1
def new
-
1
@pipeline = Pipeline.new
-
end
-
-
# GET /pipelines/1/edit
-
1
def edit
-
1
@stages = @pipeline.stages.order(:position)
-
end
-
-
# POST /pipelines/1/import_file
-
1
def import_file
-
@pipeline = Pipeline.find(params[:pipeline_id])
-
-
uploaded_io = params[:import_file]
-
-
csv_text = uploaded_io.read
-
csv = CSV.parse(csv_text, headers: true)
-
-
path_to_output_csv_file = "#{Rails.root}/tmp/deals-#{Time.current.to_i}.csv"
-
line = 0
-
CSV.open(path_to_output_csv_file, 'wb') do |csv_output|
-
csv.each do |row|
-
csv_output << row.to_h.keys + ['result'] if line == 0
-
-
row_json = JsonCsv.csv_row_hash_to_hierarchical_json_hash(row, {})
-
-
row_params = ActionController::Parameters.new(row_json).merge({ "stage_id": params[:stage_id] })
-
-
deal = DealBuilder.new(current_user, row_params).perform
-
-
csv_output << if deal.save
-
row.to_h.values + [I18n.t('activerecord.models.deal.import_file_success', deal_id: deal.id)]
-
else
-
row.to_h.values + [I18n.t('activerecord.models.deal.import_file_failed',
-
message_error: deal.errors.messages)]
-
end
-
line += 1
-
end
-
end
-
-
response.headers['Content-Type'] = 'text/csv'
-
response.headers['Content-Disposition'] = 'attachment; filename=deals.csv'
-
# flash[:notice] = 'Arquivo processado com sucesso.'
-
send_file path_to_output_csv_file
-
# redirect_to account_pipeline_path(current_user.id, @pipeline.id), notice: 'Arquivo processado com sucesso.'
-
end
-
-
# GET /pipelines/1/import
-
1
def import
-
@pipeline = Pipeline.find(params[:pipeline_id])
-
@stage = Stage.find(params[:stage_id])
-
-
respond_to do |format|
-
format.turbo_stream
-
format.html
-
format.csv do
-
path_to_output_csv_file = "#{Rails.root}/tmp/deals-#{Time.current.to_i}.csv"
-
# headers = Deal.csv_header(Current.account)
-
headers = ['name', 'contact_attributes.full_name', 'contact_attributes.phone']
-
CSV.open(path_to_output_csv_file, 'w') do |csv|
-
csv << headers
-
end
-
-
response.headers['Content-Type'] = 'text/csv'
-
response.headers['Content-Disposition'] = 'attachment; filename=deals.csv'
-
render file: path_to_output_csv_file
-
end
-
end
-
end
-
-
# GET /pipelines/1/export
-
1
def export
-
@deals = Deal.where(stage_id: params['stage_id'])
-
-
path_to_output_csv_file = "#{Rails.root}/tmp/deals-#{Time.current.to_i}.csv"
-
JsonCsv.create_csv_for_json_records(path_to_output_csv_file) do |csv_builder|
-
@deals.each do |deal|
-
json = JSON.parse(deal.to_json(include: :contacts))
-
csv_builder.add(json)
-
end
-
end
-
-
respond_to do |format|
-
format.html
-
format.csv do
-
response.headers['Content-Type'] = 'text/csv'
-
response.headers['Content-Disposition'] = 'attachment; filename=deals.csv'
-
render file: path_to_output_csv_file
-
end
-
end
-
end
-
-
1
def bulk_action; end
-
-
1
def new_bulk_action; end
-
-
1
def create_bulk_action
-
@deals = Deal.where(stage_id: params['event']['stage_id'], status: 'open')
-
@stage = Stage.find(params['event']['stage_id'])
-
if params['event']['send_now'] == 'true'
-
time_start = DateTime.current
-
elsif !params['event']['scheduled_at'].nil?
-
time_start = params['event']['scheduled_at'].in_time_zone
-
end
-
@result = @deals.each_with_index do |deal, index|
-
if params['event']['kind'] == 'chatwoot_message' || params['event']['kind'] == 'evolution_api_message'
-
if params['event']['send_now'] == 'true'
-
time_start += rand(10..15).seconds
-
params['event']['send_now'] = 'false'
-
elsif !time_start.nil?
-
time_start += rand(10..15).seconds
-
end
-
end
-
@event = EventBuilder.new(current_user,
-
event_params.merge({ contact: deal.contact, scheduled_at: time_start })).build
-
@event.deal = deal
-
-
if !@event.valid? && index == 0
-
render :new_bulk_action, status: :unprocessable_entity
-
return
-
end
-
@event.save
-
end
-
respond_to do |format|
-
format.turbo_stream
-
end
-
end
-
-
1
def bulk_action_2; end
-
-
# POST /pipelines or /pipelines.json
-
1
def create
-
1
@pipeline = Pipeline.new(pipeline_params)
-
-
1
respond_to do |format|
-
1
if @pipeline.save
-
1
format.html do
-
1
redirect_to account_pipeline_path(Current.account, @pipeline),
-
notice: t('flash_messages.created', model: Pipeline.model_name.human)
-
end
-
1
format.json { render :show, status: :created, location: @pipeline }
-
else
-
format.html { render :new, status: :unprocessable_entity }
-
format.json { render json: @pipeline.errors, status: :unprocessable_entity }
-
end
-
end
-
end
-
-
# PATCH/PUT /pipelines/1 or /pipelines/1.json
-
1
def update
-
1
if @pipeline.update(pipeline_params)
-
1
respond_to do |format|
-
1
format.html do
-
1
redirect_to account_pipeline_path(Current.account, @pipeline),
-
notice: t('flash_messages.updated', model: Pipeline.model_name.human)
-
end
-
1
format.turbo_stream
-
end
-
end
-
end
-
-
# DELETE /pipelines/1 or /pipelines/1.json
-
1
def destroy
-
@pipeline.destroy
-
respond_to do |format|
-
format.html { redirect_to pipelines_url, notice: t('flash_messages.deleted', model: Pipeline.model_name.human) }
-
format.json { head :no_content }
-
end
-
end
-
-
1
private
-
-
# Use callbacks to share common setup or constraints between actions.
-
1
def set_pipeline
-
2
@pipeline = Pipeline.find(params[:id])
-
end
-
-
# Only allow a list of trusted parameters through.
-
1
def pipeline_params
-
2
params.require(:pipeline).permit(:name, stages_attributes: %i[id name _destroy account_id position])
-
end
-
-
1
def set_bulk_action_event
-
@event = EventBuilder.new(current_user,
-
{ kind: params[:kind] }).build
-
end
-
-
1
def set_stage
-
@stage = Stage.find(params[:stage_id])
-
end
-
-
1
def deal_params(params)
-
params.permit(
-
:name, :status, :stage_id, :contact_id,
-
contact_attributes: %i[id full_name phone email],
-
custom_attributes: {}
-
)
-
end
-
-
1
def event_params
-
params.require(:event).permit(:content, :send_now, :done, :auto_done, :title, :kind, :app_type, :app_id, :from_me, files: [],
-
custom_attributes: {}, additional_attributes: {})
-
rescue StandardError
-
{}
-
end
-
end
-
1
class Accounts::ProductsController < InternalController
-
1
include ProductConcern
-
-
1
before_action :set_product, only: %i[edit destroy update show edit_custom_attributes update_custom_attributes]
-
-
1
def new
-
1
@product = current_user.account.products.new
-
1
@product.attachments.build
-
end
-
-
1
def create
-
6
@product = ProductBuilder.new(current_user, product_params).perform
-
6
if @product.save
-
2
respond_to do |format|
-
2
format.html do
-
2
redirect_to account_products_path(current_user.account),
-
notice: t('flash_messages.created', model: Product.model_name.human)
-
end
-
2
format.turbo_stream
-
end
-
else
-
4
render :new, status: :unprocessable_entity
-
end
-
end
-
-
1
def edit; end
-
-
1
def update
-
3
if @product.update(product_params)
-
1
redirect_to edit_account_product_path(current_user.account, @product),
-
notice: t('flash_messages.updated', model: Product.model_name.human)
-
else
-
2
render :edit, status: :unprocessable_entity
-
end
-
end
-
-
1
def edit_custom_attributes
-
1
@custom_attribute_definitions = current_user.account.custom_attribute_definitions.product_attribute
-
end
-
-
1
def update_custom_attributes
-
1
@product.custom_attributes[params[:product][:att_key]] = params[:product][:att_value]
-
1
render :edit_custom_attributes, status: :unprocessable_entity unless @product.save
-
end
-
-
1
def index
-
9
@products = if params[:query].present?
-
8
Product.where(
-
'name ILIKE :search OR identifier ILIKE :search', search: "%#{params[:query]}%"
-
).order(updated_at: :desc)
-
else
-
1
Product.all.order(created_at: :desc)
-
end
-
-
9
@pagy, @products = pagy(@products)
-
end
-
-
1
def destroy
-
2
@product.destroy
-
2
respond_to do |format|
-
2
format.html do
-
2
redirect_to account_products_path(current_user.account),
-
notice: t('flash_messages.deleted', model: Product.model_name.human)
-
end
-
2
format.json { head :no_content }
-
end
-
end
-
-
1
def select_product_search
-
10
@products = if params[:query].present?
-
2
current_user.account.products.where(
-
'name ILIKE :search', search: "%#{params[:query]}%"
-
).order(updated_at: :desc).limit(5)
-
else
-
8
current_user.account.products.order(updated_at: :desc).limit(5)
-
end
-
end
-
-
1
def show
-
end
-
-
1
private
-
-
1
def set_product
-
9
@product = current_user.account.products.find(params[:id])
-
end
-
end
-
1
class Accounts::ReportsController < InternalController
-
1
before_action :set_date_range
-
-
1
def index
-
1
@users = User.all
-
end
-
-
1
def summary
-
2
@deal_summary = build_deal_sumary
-
2
@deals_timeseries_count = build_chart_deals_timeseries_body
-
end
-
-
1
def pipeline_summary
-
3
pipeline_id = params[:pipeline_id].presence || Pipeline.first&.id
-
-
3
if pipeline_id
-
2
series_data = Reports::Pipeline::StagesMetricBuilder.new(Current.account, report_params.merge(id: pipeline_id)).metrics
-
1
@pipeline_summary = build_chart_pipeline_summary_body(series_data)
-
else
-
1
@pipeline_summary = {}
-
end
-
end
-
-
1
private
-
-
1
def build_deal_sumary
-
[
-
2
Reports::Deals::MetricBuilder.new(Current.account, report_params.merge(metric: 'open_deals')).summary,
-
Reports::Deals::MetricBuilder.new(Current.account, report_params.merge(metric: 'all_deals')).summary,
-
Reports::Deals::MetricBuilder.new(Current.account, report_params.merge(metric: 'won_deals')).summary,
-
Reports::Deals::MetricBuilder.new(Current.account, report_params.merge(metric: 'lost_deals')).summary
-
]
-
end
-
-
1
def report_params
-
14
common_params.merge({
-
metric: params[:metric],
-
since: params[:since].to_time.to_i.to_s,
-
until: params[:until].to_time.to_i.to_s,
-
timezone_offset: params[:timezone_offset]
-
})
-
end
-
-
1
def common_params
-
{
-
14
type: params[:type]&.to_sym,
-
id: params[:id],
-
group_by: params[:group_by],
-
filter: params[:filter]&.to_unsafe_h
-
}
-
end
-
-
1
def build_chart_deals_timeseries_body
-
{
-
2
chart_type: 'column',
-
data: [
-
{ name: I18n.t('activerecord.models.deal.won_deals'),
-
color: metric_color('won_deals'),
-
series_data: Reports::Deals::ReportBuilder.new(Current.account,
-
report_params.merge(metric: 'won_deals_count')).timeseries },
-
{ name: I18n.t('activerecord.models.deal.lost_deals'),
-
color: metric_color('lost_deals'),
-
series_data: Reports::Deals::ReportBuilder.new(Current.account,
-
report_params.merge(metric: 'lost_deals_count')).timeseries }
-
-
]
-
}.to_json
-
end
-
-
1
def build_chart_pipeline_summary_body(series_data)
-
{
-
1
chart_type: 'funnel',
-
data: [
-
{ name: Deal.model_name.human,
-
color: metric_color(params[:metric]),
-
series_data: series_data
-
}
-
]
-
}.to_json
-
end
-
-
1
def metric_color(metric)
-
5
case metric
-
when 'lost_deals'
-
2
'#CF4F27'
-
when 'won_deals'
-
3
'#259C50'
-
when 'open_deals'
-
'#5491F5'
-
else
-
'#6857D9'
-
end
-
end
-
-
1
def set_date_range
-
6
if params[:date_range].present?
-
4
starts_str, ends_str = params[:date_range].split(' - ')
-
4
params[:since] = starts_str
-
4
params[:until] = ends_str
-
else
-
2
params[:date_range] = "#{params[:since]} - #{params[:until]}"
-
end
-
end
-
end
-
1
class Accounts::Settings::AccountsController < InternalController
-
1
include AccountConcern
-
-
1
def edit; end
-
-
1
def update
-
2
if @account.update(account_params)
-
1
respond_to do |format|
-
1
format.html do
-
1
redirect_to edit_account_settings_account_path(@account),
-
notice: t('flash_messages.updated', model: Account.model_name.human)
-
end
-
1
format.turbo_stream
-
end
-
else
-
1
render :edit, status: :unprocessable_entity
-
end
-
end
-
-
1
private
-
-
1
def account_params
-
2
params.require(:account).permit(*permitted_account_params)
-
end
-
end
-
1
class Accounts::Settings::CustomAttributesDefinitionsController < InternalController
-
1
before_action :set_custom_attribute_deffinition, only: %i[edit update destroy]
-
-
1
def index
-
1
@custom_attributes_definitions = current_user.account.custom_attributes_definitions
-
end
-
-
1
def new
-
1
@custom_attribute_definition = current_user.account.custom_attributes_definitions.new
-
end
-
-
1
def create
-
4
@custom_attribute_definition = current_user.account.custom_attributes_definitions.new(custom_attribute_definition_params)
-
4
if @custom_attribute_definition.save
-
2
redirect_to account_custom_attributes_definitions_path(current_user.account),
-
notice: t('flash_messages.created', model: CustomAttributeDefinition.model_name.human)
-
else
-
2
render :new, status: :unprocessable_entity
-
end
-
end
-
-
1
def edit; end
-
-
1
def update
-
2
if @custom_attribute_definition.update(custom_attribute_definition_params)
-
1
redirect_to edit_account_custom_attributes_definition_path(current_user.account, @custom_attribute_definition),
-
notice: t('flash_messages.updated', model: CustomAttributeDefinition.model_name.human)
-
else
-
1
render :edit, status: :unprocessable_entity
-
end
-
end
-
-
1
def destroy
-
1
render :index, status: :unprocessable_entity unless @custom_attribute_definition.destroy
-
end
-
-
1
private
-
-
1
def set_custom_attribute_deffinition
-
4
@custom_attribute_definition = current_user.account.custom_attribute_definitions.find(params[:id])
-
end
-
-
1
def custom_attribute_definition_params
-
6
params.require(:custom_attribute_definition).permit(
-
:attribute_model,
-
:attribute_key,
-
:attribute_display_name,
-
:attribute_description
-
)
-
end
-
end
-
1
class Accounts::Settings::Deals::DealLostReasonsController < InternalController
-
1
before_action :set_deal_lost_reason, only: %i[edit update destroy]
-
-
1
def index
-
2
@deal_lost_reasons = DealLostReason.all
-
end
-
-
1
def edit; end
-
-
1
def update
-
2
if @deal_lost_reason.update(deal_lost_reason_params)
-
1
respond_to do |format|
-
1
format.html do
-
1
redirect_to edit_account_settings_deals_deal_lost_reason_path(Current.account, @deal_lost_reason),
-
notice: t('flash_messages.updated', model: DealLostReason.model_name.human)
-
end
-
1
format.turbo_stream
-
end
-
else
-
1
render :edit, status: :unprocessable_entity
-
end
-
end
-
-
1
def new
-
1
@deal_lost_reason = DealLostReason.new
-
end
-
-
1
def create
-
2
@deal_lost_reason = DealLostReason.new(deal_lost_reason_params)
-
2
if @deal_lost_reason.save
-
1
respond_to do |format|
-
1
format.html do
-
1
redirect_to account_settings_deals_deal_lost_reasons_path(Current.account),
-
notice: t('flash_messages.created', model: DealLostReason.model_name.human)
-
end
-
1
format.turbo_stream
-
end
-
else
-
1
render :new, status: :unprocessable_entity
-
end
-
end
-
-
1
def destroy
-
1
if @deal_lost_reason.destroy
-
1
respond_to do |format|
-
1
format.html do
-
1
redirect_to account_settings_deals_deal_lost_reasons_path(Current.account),
-
notice: t('flash_messages.deleted', model: DealLostReason.model_name.human)
-
end
-
end
-
else
-
respond_to do |format|
-
format.html do
-
redirect_to account_settings_deals_deal_lost_reasons_path(Current.account),
-
flash: { error: @deal_lost_reason.errors.full_messages.to_sentence }
-
end
-
end
-
end
-
end
-
-
1
private
-
-
1
def set_deal_lost_reason
-
4
@deal_lost_reason = DealLostReason.find(params[:id])
-
end
-
-
1
def deal_lost_reason_params
-
4
params.require(:deal_lost_reason).permit(:name)
-
end
-
end
-
1
class Accounts::Settings::DealsController < InternalController
-
1
def edit
-
end
-
end
-
1
class Accounts::Settings::WebhooksController < InternalController
-
1
before_action :set_webhook, only: %i[edit update destroy]
-
-
1
def index
-
1
@webhooks = current_user.account.webhooks
-
1
@pagy, @webhooks = pagy(@webhooks)
-
end
-
-
1
def new
-
1
@webhook = Webhook.new
-
end
-
-
1
def create
-
2
@webhook = current_user.account.webhooks.new(webhook_params)
-
2
if @webhook.save
-
1
redirect_to account_webhooks_path(current_user.account),
-
notice: t('flash_messages.created', model: Webhook.model_name.human)
-
else
-
1
render :new, status: :unprocessable_entity
-
end
-
end
-
-
1
def edit; end
-
-
1
def update
-
2
if @webhook.update(webhook_params)
-
1
redirect_to edit_account_webhook_path(current_user.account, @webhook),
-
notice: t('flash_messages.updated', model: Webhook.model_name.human)
-
else
-
1
render :edit, status: :unprocessable_entity
-
end
-
end
-
-
1
def destroy
-
1
if @webhook.destroy
-
1
flash[:notice] = t('flash_messages.deleted', model: Webhook.model_name.human)
-
else
-
render :index, status: :unprocessable_entity
-
end
-
end
-
-
1
private
-
-
1
def set_webhook
-
4
@webhook = current_user.account.webhooks.find(params[:id])
-
end
-
-
1
def webhook_params
-
4
params.require(:webhook).permit(:url, :status)
-
end
-
end
-
1
class Accounts::SettingsController < InternalController
-
1
def index
-
end
-
end
-
1
class Accounts::StagesController < InternalController
-
1
before_action :set_stage, only: %i[show]
-
-
1
def show
-
7
@filter_status_deal = if params[:filter_status_deal].present?
-
4
params[:filter_status_deal]
-
else
-
3
'open'
-
end
-
7
if @filter_status_deal == 'all'
-
1
@pagy, @deals = pagy(@stage.deals.order(position: :desc), items: 8)
-
else
-
6
@pagy, @deals = pagy(@stage.deals.where(status: @filter_status_deal).order(position: :desc), items: 8)
-
end
-
end
-
-
1
private
-
-
1
def set_stage
-
7
@stage = Stage.find(params[:id])
-
end
-
end
-
1
class Accounts::StoresController < InternalController
-
1
def show
-
2
@store_base_url = ENV.fetch('STORE_URL', 'https://store.woofedcrm.com')
-
2
@path = params[:path] || ''
-
2
@store_url = "#{@store_base_url}/#{@path}"
-
end
-
end
-
1
class Accounts::UsersController < InternalController
-
1
include UserConcern
-
-
1
before_action :set_user, only: %i[edit update destroy hovercard_preview]
-
-
1
def index
-
9
@users = if params[:query].present?
-
7
User.where(
-
'full_name ILIKE :search OR email ILIKE :search OR phone ILIKE :search', search: "%#{params[:query]}%"
-
).order(updated_at: :desc)
-
else
-
2
User.all.order(created_at: :desc)
-
end
-
-
9
@pagy, @users = pagy(@users)
-
end
-
-
1
def edit; end
-
-
1
def update
-
17
params_without_blank_password = user_params.reject { |key, value| value.blank? && key.include?('password') }
-
-
4
if @user.update(params_without_blank_password)
-
3
flash[:notice] = t('flash_messages.updated', model: User.model_name.human)
-
3
redirect_to edit_account_user_path(current_user.account, @user)
-
else
-
1
render :edit, status: :unprocessable_entity
-
end
-
end
-
-
1
def new
-
2
@user = User.new
-
end
-
-
1
def create
-
2
@user = current_user.account.users.new(user_params)
-
2
if @user.save
-
1
redirect_to account_users_path(current_user.account),
-
notice: t('flash_messages.created', model: User.model_name.human)
-
else
-
1
render :new, status: :unprocessable_entity
-
end
-
end
-
-
1
def destroy
-
2
if @user.destroy
-
2
redirect_to account_users_path(current_user.account),
-
notice: t('flash_messages.deleted', model: User.model_name.human)
-
end
-
end
-
-
1
def select_user_search
-
10
@users = if params[:query].present?
-
2
User.where(
-
'full_name ILIKE :search OR email ILIKE :search OR phone ILIKE :search', search: "%#{params[:query]}%"
-
).order(updated_at: :desc).limit(5)
-
else
-
8
User.order(updated_at: :desc).limit(5)
-
end
-
end
-
-
1
def hovercard_preview
-
end
-
-
1
private
-
-
1
def set_user
-
10
@user = current_user.account.users.find(params[:id])
-
end
-
-
1
def user_params
-
6
params.require(:user).permit(*permitted_user_params)
-
end
-
end
-
1
class Accounts::WebpushSubscriptionsController < InternalController
-
1
def create
-
3
webpush_subscription = WebpushSubscription.new(
-
user: current_user,
-
endpoint: params[:endpoint],
-
auth_key: params[:keys][:auth],
-
p256dh_key: params[:keys][:p256dh]
-
)
-
3
if webpush_subscription.save
-
1
render json: webpush_subscription
-
else
-
2
render json: webpush_subscription.errors.full_messages, status: :unprocessable_entity
-
end
-
end
-
end
-
1
class Accounts::WelcomeController < InternalController
-
1
def index
-
-
end
-
end
-
1
module Api::Concerns::RequestExceptionHandler
-
1
extend ActiveSupport::Concern
-
-
1
included do
-
1
rescue_from ActiveRecord::RecordInvalid, with: :render_record_invalid
-
end
-
-
1
private
-
-
1
def handle_with_exception
-
63
yield
-
rescue ActiveRecord::RecordNotFound => e
-
10
log_handled_error(e)
-
10
render_not_found_error('Resource could not be found')
-
rescue ActionController::ParameterMissing => e
-
log_handled_error(e)
-
render_could_not_create_error(e.message)
-
ensure
-
# to address the thread variable leak issues in Puma/Thin webserver
-
63
Current.reset
-
end
-
-
1
def render_unauthorized(message)
-
render json: { error: message }, status: :unauthorized
-
end
-
-
1
def render_not_found_error(message)
-
10
render json: { error: message }, status: :not_found
-
end
-
-
1
def render_could_not_create_error(message)
-
render json: { error: message }, status: :unprocessable_entity
-
end
-
-
1
def render_payment_required(message)
-
render json: { error: message }, status: :payment_required
-
end
-
-
1
def render_internal_server_error(message)
-
render json: { error: message }, status: :internal_server_error
-
end
-
-
1
def render_record_invalid(exception)
-
log_handled_error(exception)
-
render json: {
-
message: exception.record.errors.full_messages.join(', '),
-
attributes: exception.record.errors.attribute_names
-
}, status: :unprocessable_entity
-
end
-
-
1
def log_handled_error(exception)
-
10
logger.info("Handled error: #{exception.inspect}")
-
end
-
end
-
# frozen_string_literal: true
-
-
1
class Api::V1::Accounts::AccountsController < Api::V1::InternalController
-
1
before_action :set_account, only: %i[show update]
-
-
1
def show
-
1
if @account
-
1
render json: @account, status: :ok
-
else
-
render json: { errors: 'Not found' }, status: :not_found
-
end
-
end
-
-
1
def update
-
2
if @account.update(account_params)
-
1
render json: @account, status: :ok
-
else
-
1
render json: { errors: @account.errors.full_messages }, status: :unprocessable_entity
-
end
-
end
-
-
1
private
-
-
1
def set_account
-
3
@account = Account.find(params['id'])
-
end
-
-
1
def account_params
-
2
params.permit(:name, :number_of_employees, :segment, :site_url, :woofbot_auto_reply)
-
end
-
end
-
1
class Api::V1::Accounts::ContactsController < Api::V1::InternalController
-
1
before_action :set_contact, only: %i[show destroy]
-
-
1
def show
-
1
render json: @contact, include: %i[deals events], status: :ok
-
end
-
-
1
def create
-
2
@contact = Contact.new(contact_params)
-
-
2
if @contact.save
-
1
render json: @contact, status: :created
-
else
-
1
render json: { errors: @contact.errors.full_messages }, status: :unprocessable_entity
-
end
-
end
-
-
1
def upsert
-
3
existing_contact = Accounts::Contacts::GetByParams.call(Account.first, contact_params.to_h)[:ok]
-
-
3
if existing_contact.nil?
-
2
@contact = Contact.new(contact_params)
-
2
status = :created
-
else
-
1
@contact = existing_contact
-
1
@contact.assign_attributes(contact_params)
-
1
status = :ok
-
end
-
-
3
if @contact.save
-
2
render(json: @contact, status:)
-
else
-
1
render json: @contact.errors, status: :unprocessable_entity
-
end
-
end
-
-
1
def search
-
5
contacts = Contact.ransack(params[:query])
-
-
4
@pagy, @contacts = pagy(contacts.result, metadata: %i[page items count pages from last to prev next])
-
4
render json: { data: @contacts,
-
pagination: pagy_metadata(@pagy) }
-
rescue ArgumentError => e
-
1
render json: {
-
errors: 'Invalid search parameters',
-
details: e.message
-
}, status: :unprocessable_entity
-
end
-
-
1
def destroy
-
1
if @contact.destroy
-
1
head :no_content
-
else
-
render json: { errors: @contact.errors.full_messages }, status: :unprocessable_entity
-
end
-
end
-
-
1
private
-
-
1
def contact_params
-
8
params.permit(:full_name, :phone, :email, :label_list,
-
custom_attributes: {})
-
end
-
-
1
def set_contact
-
4
@contact = Contact.find(params[:id])
-
end
-
end
-
1
class Api::V1::Accounts::DealAssigneesController < Api::V1::InternalController
-
1
before_action :set_deal_assignee, only: %i[destroy]
-
-
1
def destroy
-
1
if @deal_assignee.destroy
-
1
head :no_content
-
else
-
render json: { errors: @deal_assignee.errors.full_messages }, status: :unprocessable_entity
-
end
-
end
-
-
1
def create
-
3
@deal_assignee = DealAssignee.new(deal_assignees_params)
-
-
3
if @deal_assignee.save
-
1
render json: @deal_assignee, status: :created
-
else
-
2
render json: { errors: @deal_assignee.errors.full_messages }, status: :unprocessable_entity
-
end
-
end
-
-
1
private
-
-
1
def deal_assignees_params
-
3
params.permit(:user_id, :deal_id)
-
end
-
-
1
def set_deal_assignee
-
2
@deal_assignee = DealAssignee.find(params[:id])
-
end
-
end
-
1
class Api::V1::Accounts::DealProductsController < Api::V1::InternalController
-
1
include DealProductConcern
-
1
def show
-
2
@deal_product = DealProduct.find(params['id'])
-
-
1
if @deal_product
-
1
render json: @deal_product, include: %i[product deal], status: :ok
-
else
-
render json: { errors: 'Not found' }, status: :not_found
-
end
-
end
-
-
1
def create
-
2
@deal_product = DealProductBuilder.new(deal_product_params).perform
-
-
2
if DealProduct::CreateOrUpdate.new(@deal_product, {}).call
-
1
render json: @deal_product, include: %i[product deal], status: :created
-
else
-
1
render json: { errors: @deal_product.errors.full_messages }, status: :unprocessable_entity
-
end
-
end
-
-
1
def update
-
3
@deal_product = DealProduct.find(params['id'])
-
-
2
if DealProduct::CreateOrUpdate.new(@deal_product, deal_product_params).call
-
1
render json: @deal_product, include: %i[product deal], status: :ok
-
else
-
1
render json: { errors: @deal_product.errors.full_messages }, status: :unprocessable_entity
-
end
-
end
-
-
1
def deal_product_params
-
4
params.permit(*permitted_deal_product_params)
-
end
-
end
-
1
class Api::V1::Accounts::Deals::EventsController < Api::V1::InternalController
-
1
def create
-
3
@deal = Deal.find(params['deal_id'])
-
2
event = @deal.events.new(event_params)
-
2
event.contact = @deal.contact
-
2
event.from_me = true
-
-
2
if event.save
-
1
render json: event, status: :created
-
else
-
1
render json: { errors: event.errors.full_messages }, status: :unprocessable_entity
-
end
-
end
-
-
1
def event_params
-
2
params.permit(:content, :send_now, :done, :auto_done, :done_at, :title, :scheduled_at, :kind, :app_type, :app_id,
-
custom_attributes: {}, additional_attributes: {})
-
end
-
end
-
1
class Api::V1::Accounts::DealsController < Api::V1::InternalController
-
1
include DealConcern
-
-
1
def show
-
2
@deal = Deal.find(params['id'])
-
-
1
if @deal
-
1
render json: @deal, include: %i[contact stage pipeline deal_assignees deal_products], status: :ok
-
else
-
render json: { errors: 'Not found' }, status: :not_found
-
end
-
end
-
-
1
def create
-
2
@deal = DealBuilder.new(current_user, deal_params).perform
-
2
if Deal::CreateOrUpdate.new(@deal, deal_params).call
-
1
render json: @deal, status: :created
-
else
-
1
render json: { errors: @deal.errors.full_messages }, status: :unprocessable_entity
-
end
-
end
-
-
1
def upsert
-
3
@deal = Deal.where(
-
contact_id: params['contact_id']
-
).first_or_initialize
-
-
3
if Deal::CreateOrUpdate.new(@deal, deal_params).call
-
2
render json: @deal, status: :ok
-
else
-
1
render json: { errors: @deal.errors.full_messages }, status: :unprocessable_entity
-
end
-
end
-
-
1
def update
-
6
@deal = Deal.find(params['id'])
-
-
5
if Deal::CreateOrUpdate.new(@deal, deal_params).call
-
4
render json: @deal, status: :ok
-
else
-
1
render json: { errors: @deal.errors.full_messages }, status: :unprocessable_entity
-
end
-
end
-
-
1
def deal_params
-
12
params.permit(*permitted_deal_params)
-
end
-
end
-
1
class Api::V1::Accounts::ProductsController < Api::V1::InternalController
-
1
def show
-
2
@product = Product.find(params['id'])
-
-
1
if @product
-
1
render json: @product, include: %i[deal_products], status: :ok
-
else
-
render json: { errors: 'Not found' }, status: :not_found
-
end
-
end
-
-
1
def create
-
3
@product = Product.new(product_params)
-
-
3
if @product.save
-
1
render json: @product, status: :created
-
else
-
2
render json: { errors: @product.errors.full_messages }, status: :unprocessable_entity
-
end
-
end
-
-
1
def search
-
4
products = Product.ransack(params[:query])
-
3
@pagy, @products = pagy(products.result, metadata: %i[page items count pages from last to prev next])
-
-
3
render json: { data: @products,
-
pagination: pagy_metadata(@pagy) }
-
rescue ArgumentError => e
-
1
render json: {
-
errors: 'Invalid search parameters',
-
details: e.message
-
}, status: :unprocessable_entity
-
end
-
-
1
def update
-
3
@product = Product.find(params['id'])
-
-
2
if @product.update(product_params)
-
1
render json: @product, status: :ok
-
else
-
1
render json: { errors: @product.errors.full_messages }, status: :unprocessable_entity
-
end
-
end
-
-
1
def product_params
-
5
params.permit(:identifier, :amount_in_cents, :quantity_available, :description, :name, attachments_attributes: %i[file _destroy id],
-
custom_attributes: {}, additional_attributes: {})
-
end
-
end
-
# frozen_string_literal: true
-
-
1
class Api::V1::Accounts::UsersController < Api::V1::InternalController
-
1
include UserConcern
-
-
1
def search
-
4
users = User.ransack(params[:query])
-
-
3
@pagy, @users = pagy(users.result, metadata: %i[page items count pages from last to prev next])
-
3
render json: { data: @users,
-
pagination: pagy_metadata(@pagy) }
-
rescue ArgumentError => e
-
1
render json: {
-
errors: 'Invalid search parameters',
-
details: e.message
-
}, status: :unprocessable_entity
-
end
-
-
1
def create
-
2
@user = User.new(user_params)
-
-
2
if @user.save
-
1
render json: @user, status: :created
-
else
-
1
render json: { errors: @user.errors.full_messages }, status: :unprocessable_entity
-
end
-
end
-
-
1
private
-
-
1
def user_params
-
2
params.permit(*permitted_user_params)
-
end
-
end
-
1
class Api::V1::ContactsController < Api::V1::InternalController
-
-
1
def create
-
@contact = Contact.new(contact_params)
-
-
if @contact.save
-
render json: @contact, status: :created
-
else
-
render json: { errors: @contact.errors.full_messages }, status: :unprocessable_entity
-
end
-
end
-
-
1
def contact_params
-
params.permit(:full_name, :phone, :email)
-
end
-
end
-
1
class Api::V1::InternalController < ActionController::API
-
1
include Pagy::Backend
-
1
include Api::Concerns::RequestExceptionHandler
-
1
before_action :authenticate_user
-
1
around_action :handle_with_exception, unless: :devise_controller?
-
-
1
def authenticate_user
-
86
header = request.headers['Authorization']
-
86
header = header.split(' ').last if header
-
-
begin
-
86
decoded = Users::JsonWebToken.decode_user(header)
-
86
@current_user = decoded[:ok]
-
86
@current_account = @current_user.account
-
rescue
-
23
render json: { errors: 'Unauthorized' }, status: :unauthorized
-
end
-
end
-
end
-
1
class Api::V1::PublicController < ActionController::API
-
end
-
1
class ApplicationController < ActionController::Base
-
1
include Localized
-
1
include Pagy::Backend
-
-
1
if ENV['HIGHLIGHT_PROJECT_ID'].present?
-
require 'highlight'
-
include Highlight::Integrations::Rails
-
around_action :with_highlight_context
-
end
-
1
before_action :set_account
-
1
before_action :setup_installation if Installation.installation_flow?
-
-
1
private
-
-
1
def setup_installation
-
if Installation.installation_flow? && !request.path.include?('/installation')
-
redirect_to installation_new_path and return
-
end
-
end
-
-
1
def set_account
-
@account = Current.account
-
end
-
end
-
1
class Apps::ChatwootsController < ActionController::Base
-
1
before_action :load_chatwoot
-
1
before_action :authenticate_by_token, if: :check_user_authentication
-
1
skip_before_action :verify_authenticity_token, except: :embedding
-
1
layout 'embed'
-
-
1
def webhooks
-
2
return render json: { error: 'Chatwoot is inactive' }, status: :unprocessable_entity if @chatwoot.inactive?
-
-
1
Accounts::Apps::Chatwoots::Webhooks::ProcessWebhookJob.perform_later(params.to_json, @chatwoot.account_id)
-
1
render json: { ok: true }, status: 200
-
end
-
-
1
def embedding
-
end
-
-
1
def embedding_init_authenticate
-
@token = params['token']
-
end
-
-
1
def embedding_authenticate
-
event = JSON.parse(params['event'])
-
@user_email = event['data']['currentAgent']['email']
-
user = User.find_by(email: @user_email)
-
return render 'user_not_found', status: 400 if user.blank?
-
-
sign_out_all_scopes
-
sign_in(user)
-
redirect_to embedding_apps_chatwoots_path(token: params['token'])
-
end
-
-
1
private
-
-
1
def check_user_authentication
-
3
User.find_by_id(current_user&.id).blank?
-
end
-
-
1
def authenticate_by_token
-
3
if @chatwoot.present? && action_name == 'embedding'
-
if action_name != 'embedding_authenticate'
-
redirect_to embedding_init_authenticate_apps_chatwoots_path(token: params['token'])
-
end
-
3
elsif @chatwoot.blank?
-
1
render plain: 'Unauthorized', status: 400
-
end
-
end
-
-
1
def load_chatwoot
-
3
@chatwoot = Apps::Chatwoot.find_by(embedding_token: params['token'])
-
end
-
end
-
1
class Apps::EvolutionApisController < Api::V1::PublicController
-
1
def webhooks
-
18
Accounts::Apps::EvolutionApis::Webhooks::ProcessWebhookWorker.perform_async(params.to_json)
-
18
render json: { ok: true }, status: 200
-
end
-
end
-
1
module AccountConcern
-
1
def permitted_account_params
-
6
%i[name number_of_employees segment site_url woofbot_auto_reply currency_code deal_free_form_lost_reasons deal_allow_edit_lost_at_won_at]
-
end
-
end
-
1
module DealConcern
-
1
def permitted_deal_params
-
[
-
41
:name,
-
:status,
-
:stage_id,
-
:pipeline_id,
-
:contact_id,
-
:position,
-
:lost_reason,
-
:lost_at,
-
:won_at,
-
{ contact_attributes: %i[id full_name phone email] },
-
{ custom_attributes: {} }
-
]
-
end
-
end
-
1
module DealProductConcern
-
1
def permitted_deal_product_params
-
14
%i[product_id deal_id quantity unit_amount_in_cents product_name product_identifier]
-
end
-
end
-
1
module Localized
-
1
extend ActiveSupport::Concern
-
-
1
included do
-
1
around_action :set_locale
-
1
before_action :set_time_zone
-
end
-
-
1
def set_locale(&block)
-
428
I18n.with_locale(requested_locale || I18n.default_locale, &block)
-
end
-
-
1
private
-
-
1
def requested_locale
-
428
if respond_to?(:user_signed_in?) && user_signed_in?
-
295
requested_locale_name ||= available_locale_or_nil(current_user.language)
-
end
-
428
requested_locale_name
-
end
-
-
1
def available_locale_or_nil(locale_name)
-
295
locale_name.to_sym if locale_name.present? && I18n.available_locales.map(&:to_s).include?(locale_name.to_s)
-
end
-
-
1
def set_time_zone
-
428
browser_timezone = cookies[:browser_timezone].presence || ENV.fetch('DEFAULT_TIMEZONE', 'Brasilia')
-
-
428
Time.zone = (browser_timezone if ActiveSupport::TimeZone[browser_timezone])
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module ProductConcern
-
1
def product_params
-
9
params.require(:product).permit(:identifier, :amount_in_cents, :quantity_available, :description, :name, attachments_attributes: %i[file _destroy id],
-
custom_attributes: {}, additional_attributes: {})
-
end
-
end
-
1
module UserConcern
-
1
def permitted_user_params
-
26
%i[email password password_confirmation full_name phone language avatar_url job_description
-
webpush_notify_on_event_expired]
-
end
-
end
-
1
class Embedded::Accounts::Apps::ChatwootsController < Embedded::InternalController
-
1
def index
-
#redirect_to account_contact_note_path(@current_account, @current_account.contacts.first)
-
redirect_to root_path
-
end
-
end
-
1
class Embedded::InternalController < ApplicationController
-
1
before_action :authenticate_app
-
-
1
def authenticate_app
-
token = params['token']
-
-
begin
-
@chatwoot = Apps::Chatwoot.find_by_embedding_token(token)
-
@current_account = @chatwoot.account
-
@current_user = @current_account.users.first
-
rescue ActiveRecord::RecordNotFound => e
-
render json: { errors: e.message }, status: :unauthorized
-
rescue JWT::DecodeError => e
-
render json: { errors: e.message }, status: :unauthorized
-
end
-
end
-
end
-
1
class InstallationController < ApplicationController
-
1
include AccountConcern
-
1
include UserConcern
-
-
1
before_action :authenticate_user!, except: %i[new create]
-
1
before_action :set_user, except: %i[new create]
-
1
before_action :set_account, only: %i[step_3 update_step_3]
-
-
1
layout 'devise'
-
-
1
def new
-
end
-
-
1
def step_1
-
end
-
-
1
def update_step_1
-
3
if @user.update(user_params)
-
2
redirect_to installation_step_2_path
-
else
-
1
render :step_1, status: :unprocessable_entity
-
end
-
end
-
-
1
def step_2
-
end
-
-
1
def update_step_2
-
2
if @user.update(user_params)
-
1
bypass_sign_in(@user)
-
1
redirect_to installation_step_3_path
-
else
-
1
render :step_2, status: :unprocessable_entity
-
end
-
end
-
-
1
def step_3
-
end
-
-
1
def update_step_3
-
4
if @account.update(account_params)
-
2
redirect_to installation_loading_path
-
else
-
2
render :step_3, status: :unprocessable_entity
-
end
-
end
-
-
1
def loading
-
1
Installation.first.complete_installation!
-
end
-
-
1
def create
-
7
installation = Installation.first_or_initialize
-
7
user = User.find_or_initialize_by(user_params.slice('email'))
-
7
installation.assign_attributes(installation_params)
-
6
user.assign_attributes(user_params)
-
6
user.password = SecureRandom.hex(8)
-
6
installation.user = user
-
6
ActiveRecord::Base.transaction do
-
6
installation.save!
-
6
user.save!
-
end
-
5
sign_in(user)
-
5
redirect_to installation_step_1_path
-
rescue ActiveRecord::RecordInvalid, ActionController::ParameterMissing
-
2
render :new, status: :unprocessable_entity
-
end
-
-
1
private
-
-
1
def set_user
-
15
@user = current_user
-
end
-
-
1
def set_account
-
25
@account = Account.first_or_initialize
-
end
-
-
1
def installation_params
-
7
params.require(:installation).permit(:id, :key1, :key2, :token)
-
end
-
-
1
def user_params
-
18
params.require(:user).permit(*permitted_user_params)
-
end
-
-
1
def account_params
-
4
params.require(:account).permit(*permitted_account_params)
-
end
-
end
-
1
class InternalController < ApplicationController
-
1
before_action :sign_in_preview_env
-
1
before_action :authenticate_user!
-
1
layout "internal"
-
-
1
def sign_in_preview_env
-
390
sign_in User.first if ENV['PREVIEW_APP'].present? && current_user.blank?
-
end
-
end
-
1
class PwaController < ApplicationController
-
1
skip_forgery_protection
-
-
# We need a stable URL at the root, so we can't use the regular asset path here.
-
1
def service_worker; end
-
-
# Need ERB interpolation for paths, so can't use asset path here either.
-
1
def manifest; end
-
end
-
1
class SettingsController < InternalController
-
-
1
def index
-
end
-
end
-
# frozen_string_literal: true
-
-
1
class Users::RegistrationsController < Devise::RegistrationsController
-
1
before_action :configure_permitted_parameters
-
-
1
protected
-
-
1
def configure_permitted_parameters
-
5
devise_parameter_sanitizer.permit(:sign_up,
-
keys: [:full_name, :email, :phone, :password, :password_confirmation,
-
{ account_attributes: %i[name site_url] }])
-
end
-
end
-
1
module AdvancedSearchHelper
-
1
def search_types
-
[
-
2
{ key: :contacts, type: :contact, value: 'contact', icon: 'contact',
-
label: t('activerecord.models.contact.other') },
-
{ key: :deals, type: :deal, value: 'deal', icon: 'clipboard-list', label: t('activerecord.models.deal.other') },
-
{ key: :products, type: :product, value: 'product', icon: 'box', label: t('activerecord.models.product.other') },
-
{ key: :pipelines, type: :pipeline, value: 'pipeline', icon: 'funnel',
-
label: t('activerecord.models.pipeline.other') },
-
{ key: :activities, type: :activity, value: 'activity', icon: 'calendar-check-2',
-
label: Event.human_enum_name(:kind, :activity).pluralize }
-
]
-
end
-
end
-
1
module ApplicationHelper
-
1
include Pagy::Frontend
-
-
1
def embedded_svg(filename, options = {})
-
74
assets = Rails.application.assets
-
74
asset = assets.find_asset(filename)
-
-
74
if asset
-
74
file = asset.source.force_encoding("UTF-8")
-
74
doc = Nokogiri::HTML::DocumentFragment.parse file
-
74
svg = doc.at_css "svg"
-
74
svg["class"] = options[:class] if options[:class].present?
-
else
-
doc = "<!-- SVG #{filename} not found -->"
-
end
-
-
74
raw doc
-
end
-
end
-
##############################################
-
# Helpers to implement date range filtering to APIs
-
# Include in your controller or service class where params is available
-
##############################################
-
-
1
module DateRangeHelper
-
1
def range
-
18
return if params[:since].blank? || params[:until].blank?
-
-
18
parse_date_time(params[:since])...parse_date_time(params[:until])
-
end
-
-
1
def parse_date_time(datetime)
-
36
return datetime if datetime.is_a?(DateTime)
-
36
return datetime.to_datetime if datetime.is_a?(Time) || datetime.is_a?(Date)
-
-
36
DateTime.strptime(datetime, '%s')
-
end
-
end
-
1
module TimezoneHelper
-
# ActiveSupport TimeZone is not aware of the current time, so ActiveSupport::Timezone[offset]
-
# would return the timezone without considering day light savings. To get the correct timezone,
-
# this method uses zone.now.utc_offset for comparison as referenced in the issues below
-
#
-
# https://github.com/rails/rails/pull/22243
-
# https://github.com/rails/rails/issues/21501
-
# https://github.com/rails/rails/issues/7297
-
1
def timezone_name_from_offset(offset)
-
11
return 'UTC' if offset.blank?
-
-
11
offset_in_seconds = offset.to_f * 3600
-
11
matching_zone = ActiveSupport::TimeZone.all.find do |zone|
-
362
zone.now.utc_offset == offset_in_seconds
-
end
-
-
11
matching_zone.name if matching_zone
-
end
-
end
-
1
class ApplicationJob < ActiveJob::Base
-
# Automatically retry jobs that encountered a deadlock
-
# retry_on ActiveRecord::Deadlocked
-
-
# Most jobs are safe to ignore if the underlying records are no longer available
-
# discard_on ActiveJob::DeserializationError
-
end
-
# frozen_string_literal: true
-
-
1
class Apps::Chatwoot::Connection::RefreshJob < ApplicationJob
-
1
self.queue_adapter = :good_job
-
-
1
def perform
-
Apps::Chatwoot.active.find_each do |chatwoot_app|
-
Apps::Chatwoot::Connection::Refresh.new(chatwoot_app).call
-
end
-
end
-
end
-
1
class Webhook::Status::RefreshJob < ApplicationJob
-
1
self.queue_adapter = :good_job
-
1
def perform
-
Webhook.active.find_each do |webhook|
-
next if webhook.valid_url?
-
-
webhook.inactive!
-
end
-
end
-
end
-
1
class WebhookListener
-
1
def extract_changed_attributes(event)
-
5
changed_attributes = event.previous_changes
-
5
return nil if changed_attributes.blank?
-
39
changed_attributes.map { |k, v| { k => { previous_value: v[0], current_value: v[1] } } }
-
end
-
-
## Contact
-
1
def contact_updated(contact)
-
25
Webhook.active.find_each do | wh |
-
WebhookWorker.perform_async(wh.url, build_contact_payload( 'contact_updated', contact))
-
end
-
end
-
-
1
def contact_created(contact)
-
808
if Webhook.active.present?
-
1
Webhook.active.find_each do | wh |
-
1
WebhookWorker.perform_async(wh.url, build_contact_payload( 'contact_created', contact))
-
end
-
end
-
end
-
-
## Deal
-
-
1
def deal_updated(deal)
-
35
if Webhook.active.present?
-
1
Webhook.active.find_each do | wh |
-
1
WebhookWorker.perform_async(wh.url, build_deal_payload( 'deal_updated', deal))
-
end
-
end
-
end
-
-
1
def deal_created(deal)
-
481
if Webhook.active.present?
-
1
Webhook.active.find_each do | wh |
-
1
WebhookWorker.perform_async(wh.url, build_deal_payload( 'deal_created', deal))
-
end
-
end
-
end
-
-
1
def build_deal_payload(event, deal)
-
2
changed_attributes = extract_changed_attributes(deal)
-
-
2
deal_json = deal.as_json(:include => :contact).merge({changed_attributes: changed_attributes})
-
2
{ event: event, data: deal_json }.to_json
-
end
-
-
1
def build_contact_payload(event, contact)
-
1
changed_attributes = extract_changed_attributes(contact)
-
-
1
contact_json = contact.as_json(:include => :deals).merge({changed_attributes: changed_attributes})
-
1
{ event: event, data: contact_json }.to_json
-
end
-
-
## Events
-
-
1
def event_created(event)
-
749
if Webhook.active.present?
-
2
Webhook.active.find_each do | wh |
-
2
WebhookWorker.perform_async(wh.url, build_event_payload( 'event_created', event))
-
end
-
end
-
end
-
-
1
def event_updated(event)
-
25
if Webhook.active.present?
-
Webhook.active.find_each do | wh |
-
WebhookWorker.perform_async(wh.url, build_event_payload( 'event_updated', event))
-
end
-
end
-
end
-
-
1
def build_event_payload(event, event_model)
-
2
changed_attributes = extract_changed_attributes(event_model)
-
-
2
event_json = event_model.as_json(include: %i[deal contact],
-
methods: :content).merge({ changed_attributes: changed_attributes })
-
2
{ event: event, data: event_json }.to_json
-
end
-
end
-
# frozen_string_literal: true
-
-
1
class WoofbotListener
-
1
def event_created(event)
-
749
Accounts::Contacts::Events::WoofbotWorker.perform_async(event.id) if Apps::AiAssistent.first&.auto_reply?
-
end
-
end
-
1
class ApplicationMailer < ActionMailer::Base
-
1
default from: 'from@example.com'
-
1
layout 'mailer'
-
end
-
# == Schema Information
-
#
-
# Table name: accounts
-
#
-
# id :bigint not null, primary key
-
# ai_usage :jsonb not null
-
# currency_code :string default("BRL"), not null
-
# name :string default(""), not null
-
# number_of_employees :string default("1-10"), not null
-
# segment :string default("other"), not null
-
# settings :jsonb not null
-
# site_url :string default(""), not null
-
# woofbot_auto_reply :boolean default(FALSE), not null
-
# created_at :datetime not null
-
# updated_at :datetime not null
-
#
-
1
class Account < ApplicationRecord
-
1
include Account::Settings
-
-
1
validates :name, presence: true
-
1
validates :name, length: { maximum: 255 }
-
1
validates :currency_code, presence: true, inclusion: { in: Money::Currency.table.keys.map(&:to_s).map(&:upcase) }
-
-
1
enum segment: {
-
technology: 'technology',
-
health: 'health',
-
finance: 'finance',
-
education: 'education',
-
retail: 'retail',
-
services: 'services',
-
manufacturing: 'manufacturing',
-
telecommunications: 'telecommunications',
-
transportation_logistics: 'transportation_logistics',
-
real_estate: 'real_estate',
-
energy: 'energy',
-
agriculture: 'agriculture',
-
tourism_hospitality: 'tourism_hospitality',
-
entertainment_media: 'entertainment_media',
-
construction: 'construction',
-
public_sector: 'public_sector',
-
consulting: 'consulting',
-
startup: 'startup',
-
ecommerce: 'ecommerce',
-
security: 'security',
-
automotive: 'automotive',
-
other: 'other'
-
}
-
1
enum number_of_employees: {
-
'1-10' => '1-10',
-
'11-50' => '11-50',
-
'51-200' => '51-200',
-
'201-500' => '201-500',
-
'501+' => '501+'
-
}
-
-
1
def events
-
39
Event.all
-
end
-
-
1
def apps
-
App.all
-
end
-
-
1
def users
-
13
User.all
-
end
-
-
1
def contacts
-
163
Contact.all
-
end
-
-
1
def deals
-
70
Deal.all
-
end
-
-
1
def custom_attribute_definitions
-
23
CustomAttributeDefinition.all
-
end
-
-
1
def custom_attributes_definitions
-
17
custom_attribute_definitions
-
end
-
-
1
def apps_wpp_connects
-
Apps::WppConnect.all
-
end
-
-
1
def apps_chatwoots
-
907
Apps::Chatwoot.all
-
end
-
-
1
def apps_evolution_apis
-
14
Apps::EvolutionApi.all
-
end
-
-
1
def webhooks
-
8
Webhook.all
-
end
-
-
1
def stages
-
Stage.all
-
end
-
-
1
def products
-
26
Product.all
-
end
-
-
1
def embedding_documments
-
3
EmbeddingDocumment.all
-
end
-
-
1
def deal_products
-
3
DealProduct.all
-
end
-
-
1
def apps_ai_assistents
-
Apps::AiAssistent.all
-
end
-
-
1
def site_url=(url)
-
931
super(normalize_url(url))
-
end
-
-
1
def normalize_url(url)
-
931
url = "https://#{url}" unless url.match?(%r{\Ahttp(s)?://})
-
-
931
url
-
end
-
end
-
# == Schema Information
-
#
-
# Table name: apps
-
#
-
# id :bigint not null, primary key
-
# active :boolean default(FALSE), not null
-
# kind :string
-
# name :string
-
# settings :jsonb not null
-
# created_at :datetime not null
-
# updated_at :datetime not null
-
#
-
1
class App < ApplicationRecord
-
1
enum kind: { 'wpp_connect': 'wpp_connect' }
-
end
-
1
class ApplicationRecord < ActiveRecord::Base
-
1
include Applicable
-
-
1
self.abstract_class = true
-
-
1
def self.human_enum_name(enum_name, enum_value)
-
462
I18n.t("activerecord.attributes.#{model_name
-
.i18n_key}.#{enum_name.to_s.pluralize}.#{enum_value}")
-
end
-
-
1
def sanitize_amount(amount)
-
167
amount.is_a?(String) ? amount.gsub(/[^\d-]/, '').to_i : amount
-
end
-
end
-
1
module Apps
-
1
def self.table_name_prefix
-
3
'apps_'
-
end
-
end
-
# frozen_string_literal: true
-
-
# == Schema Information
-
#
-
# Table name: apps_ai_assistents
-
#
-
# id :bigint not null, primary key
-
# api_key :string default(""), not null
-
# auto_reply :boolean default(FALSE), not null
-
# enabled :boolean default(FALSE), not null
-
# model :string default("gpt-4o"), not null
-
# usage :jsonb not null
-
# created_at :datetime not null
-
# updated_at :datetime not null
-
#
-
1
class Apps::AiAssistent < ApplicationRecord
-
1
validates :model, presence: true
-
1
validates :api_key, presence: true, if: :enabled?
-
-
10
after_update :embed_company_site, if: -> { saved_change_to_enabled? || saved_change_to_api_key? }
-
-
1
def embed_company_site
-
4
Accounts::Create::EmbedCompanySiteJob.perform_later(id) if Current.account.site_url.present? && enabled?
-
end
-
-
1
def exceeded_usage_limit?
-
9
return false if usage['limit'].blank?
-
-
4
usage['tokens'] >= usage['limit']
-
end
-
end
-
# == Schema Information
-
#
-
# Table name: apps_chatwoots
-
#
-
# id :bigint not null, primary key
-
# chatwoot_endpoint_url :string default(""), not null
-
# chatwoot_user_token :string default(""), not null
-
# embedding_token :string default(""), not null
-
# inboxes :jsonb not null
-
# name :string
-
# status :string default("active"), not null
-
# created_at :datetime not null
-
# updated_at :datetime not null
-
# chatwoot_account_id :integer not null
-
# chatwoot_dashboard_app_id :integer not null
-
# chatwoot_webhook_id :integer not null
-
#
-
1
class Apps::Chatwoot < ApplicationRecord
-
1
scope :actives, -> { where(active: true) }
-
126
normalizes :chatwoot_endpoint_url, with: ->(value) { value&.gsub(/\/+\z/, '') }
-
-
1
enum status: {
-
'inactive': 'inactive',
-
'active': 'active',
-
'sync': 'sync',
-
'pair': 'pair'
-
}
-
-
1
validate :validate_chatwoot, on: :create
-
1
before_destroy :chatwoot_delete_flow
-
-
1
def request_headers
-
204
{ 'api_access_token': chatwoot_user_token.to_s, 'Content-Type': 'application/json' }
-
end
-
-
1
def validate_chatwoot
-
2
if invalid_token?
-
1
errors.add(:chatwoot_user_token, I18n.t('activerecord.errors.messages.invalid_chatwoot_token'))
-
1
return
-
end
-
-
1
chatwoot_create_flow
-
1
if chatwoot_dashboard_app_id.blank? || chatwoot_webhook_id.blank?
-
errors.add(:chatwoot_endpoint_url, I18n.t('activerecord.errors.messages.invalid_chatwoot_configuration'))
-
errors.add(:chatwoot_user_token, I18n.t('activerecord.errors.messages.invalid_chatwoot_configuration'))
-
end
-
end
-
-
1
def invalid_token?
-
2
!valid_token?
-
end
-
-
1
def valid_token?
-
6
return false if chatwoot_account_is_suspended?
-
-
2
response = Apps::Chatwoot::ApiClient.new(self).user_profile
-
-
2
return false if response[:error].present?
-
-
2
account = response[:ok]['accounts'].select do |acc|
-
4
acc['id'] == chatwoot_account_id
-
end
-
-
2
return true if account&.dig(0, 'role') == 'administrator'
-
-
false
-
-
rescue
-
1
false
-
end
-
-
1
def chatwoot_create_flow
-
1
self.embedding_token = generate_token
-
1
dashboard_apps_response = Faraday.post(
-
"#{chatwoot_endpoint_url}/api/v1/accounts/#{chatwoot_account_id}/dashboard_apps",
-
{
-
"title": 'WoofedCRM',
-
"content": [{ "type": 'frame', "url": woofedcrm_embedding_url }]
-
}.to_json,
-
{ 'api_access_token': chatwoot_user_token.to_s, 'Content-Type': 'application/json' }
-
)
-
-
1
webhook_response = Faraday.post(
-
"#{chatwoot_endpoint_url}/api/v1/accounts/#{chatwoot_account_id}/webhooks",
-
{
-
"webhook": {
-
"url": woofedcrm_webhooks_url,
-
"subscriptions": %w[
-
contact_created
-
contact_updated
-
conversation_created
-
conversation_status_changed
-
conversation_updated
-
message_created
-
message_updated
-
webwidget_triggered
-
]
-
}
-
}.to_json,
-
{ 'api_access_token': chatwoot_user_token.to_s, 'Content-Type': 'application/json' }
-
)
-
-
1
self.inboxes = Accounts::Apps::Chatwoots::GetInboxes.call(self)[:ok]
-
-
1
if dashboard_apps_response.status == 200 && webhook_response.status == 200
-
1
dashboard_apps_body = JSON.parse(dashboard_apps_response.body)
-
1
webhook_body = JSON.parse(webhook_response.body)
-
1
self.chatwoot_dashboard_app_id = dashboard_apps_body['id']
-
1
self.chatwoot_webhook_id = webhook_body['payload']['webhook']['id']
-
1
true
-
else
-
false
-
end
-
rescue Exception => e
-
Rails.logger.error('Chatwoot connection error')
-
Rails.logger.error(e.inspect)
-
false
-
end
-
-
1
def chatwoot_delete_flow
-
1
dashboard_apps_response = Faraday.delete(
-
"#{chatwoot_endpoint_url}/api/v1/accounts/#{chatwoot_account_id}/dashboard_apps/#{chatwoot_dashboard_app_id}",
-
{},
-
{ 'api_access_token': chatwoot_user_token.to_s, 'Content-Type': 'application/json' }
-
)
-
-
1
webhook_response = Faraday.delete(
-
"#{chatwoot_endpoint_url}/api/v1/accounts/#{chatwoot_account_id}/webhooks/#{chatwoot_webhook_id}",
-
{},
-
{ 'api_access_token': chatwoot_user_token.to_s, 'Content-Type': 'application/json' }
-
)
-
-
1
true
-
rescue StandardError
-
true
-
end
-
-
1
def chatwoot_account_is_suspended?
-
6
response = Accounts::Apps::Chatwoots::GetInboxes.call(self)
-
-
4
response.key?(:error) && JSON.parse(response[:error]) == {"error"=>"Account is suspended"}
-
rescue Faraday::TimeoutError, Faraday::ConnectionFailed
-
1
false
-
end
-
-
1
private
-
-
1
def woofedcrm_webhooks_url
-
1
"#{ENV['FRONTEND_URL']}/apps/chatwoots/webhooks?token=#{embedding_token}"
-
end
-
-
1
def woofedcrm_embedding_url
-
1
"#{ENV['FRONTEND_URL']}/apps/chatwoots/embedding?token=#{embedding_token}"
-
end
-
-
1
def generate_token
-
1
loop do
-
1
token = SecureRandom.hex(10)
-
1
break token unless Apps::Chatwoot.where(embedding_token: token).exists?
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
class Apps::Chatwoot::ApiClient
-
1
include Apps::Chatwoot::ApiClient::UserProfile
-
-
1
def initialize(chatwoot)
-
7
@chatwoot = chatwoot
-
7
@request_headers = chatwoot.request_headers
-
7
@connection = create_connection
-
end
-
-
1
def create_connection
-
retry_options = {
-
7
max: 5,
-
interval: 0.05,
-
interval_randomness: 0.5,
-
backoff_factor: 2,
-
exceptions: [
-
Faraday::ConnectionFailed,
-
Faraday::TimeoutError,
-
'Timeout::Error'
-
]
-
}
-
-
7
Faraday.new(@chatwoot.chatwoot_endpoint_url) do |faraday|
-
7
faraday.options.open_timeout = 5
-
7
faraday.options.timeout = 10
-
7
faraday.headers = { 'api_access_token': @chatwoot.chatwoot_user_token.to_s, 'Content-Type': 'application/json' }
-
7
faraday.request :retry, retry_options
-
end
-
end
-
-
1
def get_request(path, params = {})
-
7
response = @connection.get(path, params)
-
-
7
if response.success?
-
4
{ ok: JSON.parse(response.body), request: response }
-
else
-
3
logger_error('Failed get_request', response)
-
3
{ error: response.body, request: response }
-
end
-
end
-
-
1
def logger_error(message, request)
-
3
Rails.logger.error "Chatwoot Api Client error #{message} - Chatwoot #{@chatwoot.id}"
-
3
Rails.logger.error "Chatwoot: #{@chatwoot.inspect}"
-
3
Rails.logger.error "Request: #{request.inspect}"
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Apps::Chatwoot::ApiClient::UserProfile
-
1
def user_profile
-
4
response = get_request('/api/v1/profile')
-
-
4
return { error: 'Failed to fetch user profile', request: response[:request] } if response[:request].status != 200
-
-
3
response
-
end
-
end
-
# frozen_string_literal: true
-
-
1
class Apps::Chatwoot::Connection::Refresh
-
1
def initialize(chatwoot)
-
3
@chatwoot = chatwoot
-
end
-
-
1
def call
-
3
return @chatwoot.inactive! if @chatwoot.invalid_token?
-
-
2
inboxes = Accounts::Apps::Chatwoots::GetInboxes.call(@chatwoot)
-
-
2
if inboxes.key?(:ok)
-
1
@chatwoot.inboxes = inboxes[:ok]
-
1
@chatwoot.save!
-
end
-
2
true
-
end
-
end
-
1
class Apps::Chatwoot::Migrations::RemoveTrailingSlashesFromChatwootEndpointUrlJob < ApplicationJob
-
1
self.queue_adapter = :good_job
-
-
1
def perform(chatwoot_id)
-
chatwoot = Apps::Chatwoot.find_by(id: chatwoot_id)
-
return unless chatwoot
-
return unless chatwoot.chatwoot_endpoint_url.present?
-
-
cleaned_url = chatwoot.chatwoot_endpoint_url.gsub(/\/+\z/, '')
-
if chatwoot.chatwoot_endpoint_url != cleaned_url
-
chatwoot.update_column(:chatwoot_endpoint_url, cleaned_url)
-
end
-
end
-
end
-
# == Schema Information
-
#
-
# Table name: apps_evolution_apis
-
#
-
# id :bigint not null, primary key
-
# active :boolean default(TRUE), not null
-
# additional_attributes :jsonb
-
# connection_status :string default("disconnected"), not null
-
# endpoint_url :string default(""), not null
-
# instance :string default(""), not null
-
# name :string default(""), not null
-
# phone :string default(""), not null
-
# qrcode :string default(""), not null
-
# token :string default(""), not null
-
# created_at :datetime not null
-
# updated_at :datetime not null
-
#
-
1
class Apps::EvolutionApi < ApplicationRecord
-
1
include Rails.application.routes.url_helpers
-
1
include EvolutionApi::Broadcastable
-
-
1
validates :endpoint_url, presence: true
-
1
validates :token, presence: true
-
1
validates :name, presence: true
-
1
validates :instance, presence: true
-
1
scope :actives, -> { where(active: true) }
-
-
1
enum connection_status: {
-
'disconnected': 'disconnected',
-
'connected': 'connected',
-
'sync': 'sync',
-
'connecting': 'connecting'
-
}
-
-
1
def request_instance_headers
-
20
{ 'apiKey': token.to_s, 'Content-Type': 'application/json' }
-
end
-
-
1
def woofedcrm_webhooks_url
-
4
"#{ENV['FRONTEND_URL']}/apps/evolution_apis/webhooks"
-
end
-
-
-
1
def generate_token(field)
-
6
loop do
-
6
security_token = SecureRandom.hex(10)
-
6
break security_token unless Apps::EvolutionApi.where(field => security_token).exists?
-
end
-
end
-
end
-
# == Schema Information
-
#
-
# Table name: attachments
-
#
-
# id :bigint not null, primary key
-
# attachable_type :string not null
-
# file_type :integer
-
# created_at :datetime not null
-
# updated_at :datetime not null
-
# attachable_id :bigint not null
-
#
-
# Indexes
-
#
-
# index_attachments_on_attachable (attachable_type,attachable_id)
-
#
-
1
class Attachment < ApplicationRecord
-
ACCEPTABLE_FILE_TYPES = %w[
-
1
text/csv text/plain text/rtf
-
application/json application/pdf
-
application/zip application/x-7z-compressed application/vnd.rar application/x-tar
-
application/msword application/vnd.ms-excel application/vnd.ms-powerpoint application/rtf
-
application/vnd.oasis.opendocument.text
-
application/vnd.openxmlformats-officedocument.presentationml.presentation
-
application/vnd.openxmlformats-officedocument.spreadsheetml.sheet
-
application/vnd.openxmlformats-officedocument.wordprocessingml.document application/x-rar-compressed;version=5
-
].freeze
-
-
1
belongs_to :attachable, polymorphic: true
-
1
has_one_attached :file
-
1
validates :file, presence: true
-
1
validate :acceptable_file
-
1
enum file_type: { image: 0, audio: 1, video: 2, file: 3, location: 4, fallback: 5, share: 6, story_mention: 7,
-
contact: 8 }
-
-
1
before_validation :fill_file_type
-
-
1
def media_file?(file_content_type)
-
17
file_content_type.start_with?('image/', 'video/', 'audio/')
-
end
-
-
39
scope :by_file_type, ->(file_type) { where(file_type: file_types[file_type]) }
-
-
1
def check_file_type
-
17
if media_file?(file.content_type)
-
11
file.content_type.split('/').first
-
6
elsif ACCEPTABLE_FILE_TYPES.include?(file.content_type)
-
6
'file'
-
end
-
end
-
-
1
def file_download
-
2
file_url = Rails.application.routes.url_helpers.rails_blob_url(file)
-
2
file_temp = Down.download(file_url)
-
2
FileUtils.mv(file_temp.path, "tmp/#{file_temp.original_filename}")
-
2
File.open("tmp/#{file_temp.original_filename}")
-
end
-
-
1
def download_url
-
3
file.attached? ? Rails.application.routes.url_helpers.rails_blob_url(file) : ''
-
end
-
-
1
def fill_file_type
-
24
self.file_type = check_file_type if file_type.blank?
-
end
-
-
1
def acceptable_file
-
24
errors.add(:file, I18n.t('activerecord.errors.messages.file_type_not_supported')) if file_type.blank?
-
24
errors.add(:file, I18n.t('activerecord.errors.messages.file_size_too_big')) if acceptable_file_size
-
end
-
-
1
def acceptable_file_size
-
23
file.byte_size > 40.megabytes
-
end
-
end
-
1
module Account::Settings
-
1
extend ActiveSupport::Concern
-
-
1
included do
-
1
store_accessor :settings, :free_form_lost_reasons, prefix: :deal
-
1
store_accessor :settings, :allow_edit_lost_at_won_at, prefix: :deal
-
-
1
def deal_free_form_lost_reasons
-
6
return false if DealLostReason.none?
-
-
4
super
-
end
-
-
1
def deal_free_form_lost_reasons=(value)
-
3
super(ActiveRecord::Type::Boolean.new.cast(value))
-
end
-
-
1
def deal_allow_edit_lost_at_won_at=(value)
-
1
super(ActiveRecord::Type::Boolean.new.cast(value))
-
end
-
end
-
end
-
1
module Applicable
-
1
extend ActiveSupport::Concern
-
1
included do
-
1
belongs_to :account, optional: true
-
1
attribute :account_id
-
-
1
def account
-
2771
Current.account
-
end
-
-
1
def account_id
-
310
Current.account&.id
-
end
-
end
-
end
-
1
module ChatwootLabels
-
1
extend ActiveSupport::Concern
-
-
1
included do
-
1
acts_as_taggable_on :chatwoot_conversations_labels
-
1
acts_as_taggable_tenant :account_id
-
end
-
-
1
def update_chatwoot_conversations_labels(labels = nil)
-
update!(chatwoot_conversations_label_list: labels)
-
end
-
-
1
def add_chatwoot_conversations_labels(new_labels = nil)
-
new_labels = Array(new_labels) # Make sure new_labels is an array
-
combined_labels = chatwoot_conversations_labels + new_labels
-
update!(chatwoot_conversations_label_list: combined_labels)
-
end
-
end
-
1
module Contact::Presenters
-
1
extend ActiveSupport::Concern
-
-
1
def full_name_at_format
-
full_name.blank? ? I18n.t('activerecord.models.contact.unknown', locale: I18n.locale) : full_name
-
end
-
end
-
1
module CustomAttributeDefinition::Broadcastable
-
1
extend ActiveSupport::Concern
-
1
included do
-
1
after_create_commit do
-
25
broadcast_append_later_to [account, :custom_attribute_definition],
-
target: 'custom_attributes_definitions', partial: 'accounts/settings/custom_attributes_definitions/custom_attribute_definition', locals: { custom_attributes_definition: self }
-
end
-
1
after_update_commit do
-
1
broadcast_replace_later_to [account, :custom_attribute_definition], target: self,
-
partial: 'accounts/settings/custom_attributes_definitions/custom_attribute_definition', locals: { custom_attributes_definition: self }
-
end
-
1
after_destroy_commit do
-
1
broadcast_remove_to [account, :custom_attribute_definition], target: self
-
end
-
end
-
end
-
1
module CustomAttributes
-
1
extend ActiveSupport::Concern
-
-
1
def custom_attribute_display_name(attribute_key)
-
account.custom_attribute_definitions.where(
-
attribute_model: "#{self.class.name.downcase}_attribute",
-
attribute_key: attribute_key
-
).first.attribute_display_name
-
rescue StandardError
-
attribute_key
-
end
-
end
-
1
module Deal::Broadcastable
-
1
extend ActiveSupport::Concern
-
1
included do
-
1
after_create_commit do
-
753
if done == false
-
141
broadcast_prepend_later_to [contact_id, 'events'],
-
partial: 'accounts/contacts/events/event',
-
target: "events_to_do_#{contact.id}"
-
else
-
612
broadcast_prepend_later_to [contact_id, 'events'],
-
partial: 'accounts/contacts/events/event',
-
target: "events_done_#{contact.id}"
-
end
-
end
-
-
1
def broadcast_events
-
17
events_to_do = deal.contact.events.to_do.limit(5).to_a
-
17
events_done = deal.contact.events.done.limit(5).to_a
-
17
broadcast_replace_later_to [contact_id, 'events'], target: "events_to_do_#{contact.id}",
-
partial: 'accounts/contacts/events/events_to_do', locals: { deal: deal, events: events_to_do, pagy: 1 }
-
17
broadcast_replace_later_to [contact_id, 'events'], target: "events_done_#{contact.id}",
-
partial: 'accounts/contacts/events/events_done', locals: { deal: deal, events: events_done, pagy: 1 }
-
end
-
-
1
after_update_commit do
-
31
if saved_change_to_done_at?
-
17
broadcast_events
-
else
-
14
broadcast_replace_later_to [contact_id, 'events'],
-
partial: 'accounts/contacts/events/event'
-
end
-
end
-
-
1
after_destroy_commit do
-
2
broadcast_remove_to [contact_id, 'events']
-
end
-
end
-
end
-
1
module Deal::EventCreator
-
1
extend ActiveSupport::Concern
-
1
included do
-
1
around_create :create_deal_and_event
-
1
around_update :update_deal_and_create_event
-
-
1
def create_deal_and_event
-
481
transaction do
-
481
yield
-
481
create_event_log('deal_opened')
-
end
-
end
-
-
1
def update_deal_and_create_event
-
35
transaction do
-
35
yield
-
35
create_event_based_on_changes
-
end
-
end
-
-
1
def create_event_based_on_changes
-
35
return create_event_log('deal_won') if status_previously_changed?(to: 'won')
-
29
return create_event_log('deal_lost') if status_previously_changed?(to: 'lost')
-
23
return create_event_log('deal_reopened') if status_previously_changed?(to: 'open')
-
-
20
create_event_log_stage_changes if stage_previously_changed?
-
end
-
end
-
-
1
private
-
-
1
def create_event_log_stage_changes
-
4
old_stage_id, new_stage_id = previous_changes['stage_id']
-
4
old_stage = Stage.find_by(id: old_stage_id) if old_stage_id
-
4
new_stage = Stage.find_by(id: new_stage_id) if new_stage_id
-
-
4
Event.create!(
-
deal: self,
-
kind: 'deal_stage_change',
-
done: true,
-
contact:,
-
from_me: true,
-
additional_attributes: {
-
old_stage_id: old_stage.id,
-
old_stage_name: old_stage.name,
-
old_stage_pipeline_id: old_stage.pipeline.id,
-
old_stage_pipeline_name: old_stage.pipeline.name,
-
new_stage_id: new_stage.id,
-
new_stage_name: new_stage.name,
-
new_stage_pipeline_id: new_stage.pipeline.id,
-
new_stage_pipeline_name: new_stage.pipeline.name,
-
deal_name: name
-
}
-
)
-
end
-
-
1
def create_event_log(log_kind)
-
496
Event.create!(
-
deal: self,
-
kind: log_kind,
-
done: true,
-
from_me: true,
-
contact:,
-
additional_attributes: {
-
stage_id: stage.id,
-
stage_name: stage.name,
-
pipeline_id: pipeline.id,
-
pipeline_name: pipeline.name,
-
deal_name: name
-
}
-
)
-
end
-
end
-
1
module Deal::HandleInCentsValues
-
1
extend ActiveSupport::Concern
-
1
included do
-
1
%i[
-
total_amount_in_cents
-
].each do |attribute|
-
1
define_method("#{attribute}=") do |amount|
-
amount = sanitize_amount(amount)
-
super(amount)
-
end
-
end
-
end
-
end
-
1
module DealProduct::Broadcastable
-
1
extend ActiveSupport::Concern
-
1
included do
-
1
after_destroy_commit do
-
4
broadcast_remove_to [account.id, :deal], target: self
-
end
-
1
after_create_commit do
-
39
broadcast_append_later_to [deal.id, :deal_product], target: 'deal_products',
-
partial: '/accounts/deals/details/deal_products/deal_product',
-
locals: { deal_product: self }
-
end
-
end
-
end
-
1
module DealProduct::EventCreator
-
1
extend ActiveSupport::Concern
-
1
included do
-
1
around_create :create_deal_product_and_event
-
1
around_destroy :destroy_deal_product_and_create_event
-
-
1
def destroy_deal_product_and_create_event
-
4
transaction do
-
4
create_event_log_destroy
-
4
yield
-
end
-
end
-
-
1
def create_deal_product_and_event
-
39
transaction do
-
39
yield
-
39
create_event_log
-
end
-
end
-
-
1
private
-
-
1
def create_event_log
-
39
Event.create!(
-
deal:,
-
kind: 'deal_product_added',
-
done: true,
-
from_me: true,
-
contact: deal.contact,
-
additional_attributes: {
-
product_id: product.id,
-
deal_id: deal.id,
-
product_name: product.name,
-
deal_name: deal.name
-
}
-
)
-
end
-
-
1
def create_event_log_destroy
-
4
product_name = product.name
-
4
deal_name = deal.name
-
4
product_id = product.id
-
4
deal_id = deal.id
-
-
4
Event.create!(
-
deal:,
-
kind: 'deal_product_removed',
-
done: true,
-
from_me: true,
-
contact: deal.contact,
-
additional_attributes: {
-
product_id:,
-
deal_id:,
-
product_name:,
-
deal_name:
-
}
-
)
-
end
-
end
-
end
-
1
module DealProduct::HandleInCentsValues
-
1
extend ActiveSupport::Concern
-
1
included do
-
1
def unit_amount_in_cents=(amount)
-
20
amount = sanitize_amount(amount)
-
20
super(amount)
-
end
-
end
-
end
-
1
module EvolutionApi::Broadcastable
-
1
extend ActiveSupport::Concern
-
1
included do
-
81
after_commit :broadcast_update_qrcode, if: -> { saved_change_to_qrcode? }
-
-
1
after_update_commit do
-
13
if saved_change_to_connection_status?(from: 'connecting', to: 'connected')
-
1
broadcast_replace_later_to "qrcode_#{self.id}_#{self.account.id}", target: self, partial: '/components/redirect_page',
-
locals: { path: Rails.application.routes.url_helpers.account_apps_evolution_apis_path(self.account) }
-
end
-
13
broadcast_replace_later_to "evolution_apis_#{account_id}", target: self, partial: '/accounts/apps/evolution_apis/evolution_api',
-
locals: { evolution_api: self }
-
end
-
-
1
after_create_commit do
-
66
broadcast_append_later_to "evolution_apis_#{account_id}", target: 'evolution_apis', partial: '/accounts/apps/evolution_apis/evolution_api',
-
locals: { evolution_api: self }
-
end
-
-
1
def broadcast_update_qrcode
-
11
broadcast_replace_later_to "qrcode_#{self.id}_#{self.account.id}", target: self, partial: 'accounts/apps/evolution_apis/qrcode',
-
locals: { evolution_api: self }
-
end
-
-
end
-
end
-
1
module Labelable
-
1
extend ActiveSupport::Concern
-
-
1
included do
-
1
acts_as_taggable_on :labels
-
1
acts_as_taggable_tenant :account_id
-
end
-
-
1
def update_labels(labels = nil)
-
update!(label_list: labels)
-
end
-
-
1
def add_labels(new_labels = nil)
-
new_labels = Array(new_labels) # Make sure new_labels is an array
-
combined_labels = labels + new_labels
-
update!(label_list: combined_labels)
-
end
-
end
-
1
module Product::Broadcastable
-
1
extend ActiveSupport::Concern
-
1
included do
-
4
after_update_commit { deal_products_broadcasts }
-
1
after_create_commit do
-
137
broadcast_append_later_to [account.id, :product], target: 'products', partial: '/accounts/products/product',
-
locals: { product: self }
-
end
-
-
1
after_update_commit do
-
3
broadcast_replace_later_to [account.id, :product], target: self, partial: '/accounts/products/product',
-
locals: { product: self }
-
end
-
1
after_destroy_commit do
-
2
broadcast_remove_to [account.id, :product], target: self
-
end
-
-
1
def deal_products_broadcasts
-
3
deal_products.each do |deal_product|
-
broadcast_replace_later_to [account.id, :deal], target: deal_product,
-
partial: '/accounts/deals/details/deal_products/deal_product', locals: { deal_product: deal_product }
-
end
-
end
-
end
-
end
-
1
module Stage::Decorators
-
1
include ActionView::Helpers::NumberHelper
-
-
1
def total_quantity_deals_resume(filter_status_deal)
-
7
number_to_human(total_quantity_deals(filter_status_deal), units: { thousand: 'K', million: 'M', billion: 'B' })
-
end
-
end
-
# == Schema Information
-
#
-
# Table name: contacts
-
#
-
# id :bigint not null, primary key
-
# additional_attributes :jsonb
-
# app_type :string
-
# custom_attributes :jsonb
-
# email :string default(""), not null
-
# full_name :string default(""), not null
-
# phone :string default(""), not null
-
# created_at :datetime not null
-
# updated_at :datetime not null
-
# app_id :bigint
-
#
-
# Indexes
-
#
-
# index_contacts_on_app (app_type,app_id)
-
# index_contacts_on_chatwoot_id (((additional_attributes ->> 'chatwoot_id'::text)), id)
-
# index_contacts_on_lower_email (lower(NULLIF((email)::text, ''::text))) UNIQUE
-
# index_contacts_on_phone (NULLIF((phone)::text, ''::text)) UNIQUE
-
#
-
1
class Contact < ApplicationRecord
-
1
include Labelable
-
1
include ChatwootLabels
-
1
include CustomAttributes
-
1
include Contact::Presenters
-
-
1
has_many :events
-
-
1
attr_accessor :skip_validation
-
-
1
validates :email, allow_blank: true, uniqueness: { case_sensitive: false },
-
format: { with: Devise.email_regexp,
-
message: I18n.t('activerecord.errors.contact.email.invalid',
-
locale: I18n.locale) }, unless: :skip_validation
-
-
1
validates :phone, allow_blank: true, uniqueness: true,
-
format: { with: /\+[1-9]\d{1,14}\z/,
-
message: I18n.t('activerecord.errors.contact.phone.invalid',
-
locale: I18n.locale) }, unless: :skip_validation
-
-
1
has_many :deals, dependent: :destroy
-
1
belongs_to :app, polymorphic: true, optional: true
-
1
scope :by_chatwoot_id, lambda { |chatwoot_id|
-
35
chatwoot_id.present? ? where("additional_attributes->>'chatwoot_id' = ?", chatwoot_id.to_s) : none
-
}
-
-
1
def self.ransackable_attributes(_auth_object = nil)
-
29
%w[additional_attributes app_id app_type created_at custom_attributes email full_name id
-
phone updated_at]
-
end
-
-
1
def connected_with_chatwoot?
-
additional_attributes['chatwoot_id'].present?
-
end
-
-
1
FORM_FIELDS = %i[full_name email phone label_list chatwoot_conversations_label_list]
-
-
1
SHOW_FIELDS = { details: %i[full_name email phone id label_list custom_attributes created_at
-
updated_at],
-
deal_page_overview_details: %i[full_name email phone label_list
-
chatwoot_conversations_label_list] }.freeze
-
-
1
after_commit :export_contact_to_chatwoot, on: %i[create update], unless: :skip_validation
-
-
1
def phone=(value)
-
866
value = "+#{value}" if value.present? && !value.start_with?('+')
-
866
super(value)
-
end
-
-
## Events
-
-
1
include Wisper::Publisher
-
1
after_commit :publish_created, on: :create, unless: :skip_validation
-
1
after_commit :publish_updated, on: :update, unless: :skip_validation
-
-
1
private
-
-
1
def export_contact_to_chatwoot
-
833
account.apps_chatwoots.present? && Accounts::Apps::Chatwoots::ExportContactWorker.perform_async(
-
account.apps_chatwoots.first.id, id
-
)
-
end
-
-
1
def publish_created
-
808
broadcast(:contact_created, self)
-
end
-
-
1
def publish_updated
-
25
broadcast(:contact_updated, self)
-
end
-
end
-
1
class Contact::Integrations::Chatwoot::GenerateConversationLink
-
1
def initialize(contact)
-
5
@contact = contact
-
end
-
-
1
def call
-
5
chatwoot = fetch_chatwoot_for_account
-
5
chatwoot_contact_id = @contact.additional_attributes['chatwoot_id']
-
-
5
return { error: 'no_chatwoot_or_id' } unless chatwoot && chatwoot_contact_id
-
-
3
conversation_id = fetch_conversation_id(chatwoot, chatwoot_contact_id)
-
2
return { error: 'no_conversation' } unless conversation_id
-
-
1
{ ok: build_conversation_url(chatwoot, conversation_id) }
-
end
-
-
1
private
-
-
1
def fetch_chatwoot_for_account
-
5
Apps::Chatwoot.first
-
end
-
-
1
def fetch_conversation_id(chatwoot, chatwoot_contact_id)
-
3
conversations = Accounts::Apps::Chatwoots::GetConversations.call(chatwoot, chatwoot_contact_id)
-
2
conversations.dig(:ok, 0, 'id')
-
end
-
-
1
def build_conversation_url(chatwoot, conversation_id)
-
1
conversation_path = "/app/accounts/#{chatwoot.chatwoot_account_id}/conversations/#{conversation_id}"
-
1
chatwoot.chatwoot_endpoint_url + conversation_path
-
end
-
end
-
1
class Contact::Merge
-
1
MERGEABLE_KEYS = %w[full_name email phone additional_attributes custom_attributes].freeze
-
-
1
def initialize(base_contact:, mergee_contact:)
-
17
raise ArgumentError, 'base_contact is required' unless base_contact
-
17
raise ArgumentError, 'mergee_contact is required' unless mergee_contact
-
-
17
@base_contact = base_contact
-
17
@mergee_contact = mergee_contact
-
end
-
-
1
def perform
-
5
ActiveRecord::Base.transaction do
-
5
validate_contacts
-
4
merge_deals
-
4
merge_events
-
4
merge_labels
-
4
merge_and_remove_mergee_contact
-
end
-
3
@base_contact
-
end
-
-
1
private
-
-
1
def validate_contacts
-
7
return if @base_contact != @mergee_contact
-
-
2
raise StandardError, 'contact does merge with same contact'
-
end
-
-
1
def merge_deals
-
5
@mergee_contact.deals.update_all(contact_id: @base_contact.id)
-
end
-
-
1
def merge_events
-
5
@mergee_contact.events.update_all(contact_id: @base_contact.id)
-
end
-
-
1
def merge_labels
-
9
merged_labels = (@base_contact.label_list + @mergee_contact.label_list)
-
9
merged_labels_chatwoot_conversations_labels = (@base_contact.chatwoot_conversations_label_list + @mergee_contact.chatwoot_conversations_label_list)
-
9
@base_contact.label_list.add(merged_labels) unless merged_labels.blank?
-
9
@base_contact.chatwoot_conversations_label_list.add(merged_labels_chatwoot_conversations_labels) unless merged_labels_chatwoot_conversations_labels.blank?
-
end
-
-
1
def merge_and_remove_mergee_contact
-
7
base_attrs = @base_contact.attributes.slice(*MERGEABLE_KEYS).compact_blank
-
7
mergee_attrs = @mergee_contact.attributes.slice(*MERGEABLE_KEYS).compact_blank
-
-
7
merged_attrs = mergee_attrs.deep_merge(base_attrs)
-
-
7
@mergee_contact.destroy!
-
7
@base_contact.update!(merged_attrs)
-
end
-
end
-
1
class Contact::Migrations::MergeDuplicateContactsJob < ApplicationJob
-
1
self.queue_adapter = :good_job
-
-
1
def perform
-
phone_groups = group_duplicate_contacts_by_phone
-
-
merge_process(phone_groups) if phone_groups.present?
-
-
email_groups = group_duplicate_contacts_by_email
-
-
merge_process(email_groups) if email_groups.present?
-
end
-
-
1
private
-
-
1
def group_duplicate_contacts_by_email
-
Contact.where.not(email: [nil, ''])
-
.group(:email)
-
.having('COUNT(*) > 1')
-
.pluck(:email)
-
.map { |email| Contact.where(email:).order(:id).pluck(:id) }
-
end
-
-
1
def group_duplicate_contacts_by_phone
-
Contact.where.not(phone: [nil, ''])
-
.group(:phone)
-
.having('COUNT(*) > 1')
-
.pluck(:phone)
-
.map { |phone| Contact.where(phone:).order(:id).pluck(:id) }
-
end
-
-
-
1
def merge_process(contact_groups)
-
contact_groups.each do |contact_ids|
-
next if contact_ids.size < 2
-
merge_group(contact_ids)
-
end
-
end
-
-
1
def merge_group(contact_ids)
-
contacts = Contact.where(id: contact_ids).order(:id).to_a
-
return if contacts.size < 2
-
-
base_contact = contacts.shift
-
base_contact.skip_validation = true
-
-
contacts.each do |mergee_contact|
-
Contact::Merge.new(base_contact:, mergee_contact:).perform
-
end
-
end
-
end
-
1
class Current < ActiveSupport::CurrentAttributes
-
1
def account
-
5362
Account.first
-
end
-
end
-
# == Schema Information
-
#
-
# Table name: custom_attribute_definitions
-
#
-
# id :bigint not null, primary key
-
# attribute_description :text
-
# attribute_display_name :string
-
# attribute_key :string
-
# attribute_model :integer default("contact_attribute")
-
# created_at :datetime not null
-
# updated_at :datetime not null
-
#
-
1
class CustomAttributeDefinition < ApplicationRecord
-
1
include CustomAttributeDefinition::Broadcastable
-
1
scope :with_attribute_model, lambda { |attribute_model|
-
attribute_model.presence && where(attribute_model: attribute_model)
-
}
-
-
1
validates :attribute_display_name, presence: true
-
1
validates :attribute_key,
-
presence: true,
-
uniqueness: { scope: %i[attribute_model] }
-
1
validates :attribute_model, presence: true
-
-
1
enum attribute_model: { contact_attribute: 0, deal_attribute: 1, product_attribute: 2 }
-
end
-
# == Schema Information
-
#
-
# Table name: deals
-
#
-
# id :bigint not null, primary key
-
# custom_attributes :jsonb
-
# lost_at :datetime
-
# lost_reason :string default(""), not null
-
# name :string default(""), not null
-
# position :integer
-
# status :string default("open"), not null
-
# total_deal_products_amount_in_cents :bigint default(0), not null
-
# won_at :datetime
-
# created_at :datetime not null
-
# updated_at :datetime not null
-
# contact_id :bigint not null
-
# created_by_id :integer
-
# pipeline_id :bigint
-
# stage_id :bigint not null
-
#
-
# Indexes
-
#
-
# index_deals_on_contact_id (contact_id)
-
# index_deals_on_created_by_id (created_by_id)
-
# index_deals_on_pipeline_id (pipeline_id)
-
# index_deals_on_stage_id (stage_id)
-
#
-
# Foreign Keys
-
#
-
# fk_rails_... (contact_id => contacts.id)
-
# fk_rails_... (created_by_id => users.id) ON DELETE => nullify
-
# fk_rails_... (stage_id => stages.id)
-
#
-
1
class Deal < ApplicationRecord
-
1
include CustomAttributes
-
1
include Deal::EventCreator
-
1
include Deal::HandleInCentsValues
-
-
1
belongs_to :contact
-
1
belongs_to :stage
-
1
belongs_to :pipeline, touch: true
-
1
belongs_to :creator, class_name: 'User', foreign_key: 'created_by_id', optional: true
-
1
acts_as_list scope: :stage
-
1
has_many :events, dependent: :destroy
-
1
has_many :activities
-
1
has_many :contact_events, through: :primary_contact, source: :events
-
1
has_many :deal_products, dependent: :destroy
-
1
has_many :deal_assignees, dependent: :destroy
-
1
has_many :users, through: :deal_assignees
-
-
1
accepts_nested_attributes_for :contact
-
-
1
enum status: { 'open': 'open', 'won': 'won', 'lost': 'lost' }
-
-
1
FORM_FIELDS = %i[name creator total_amount_in_cents]
-
-
1
SHOW_FIELDS = { deal_page_overview_details: [:name,
-
{ relations: { stage: :name, creator: :full_name } }, :total_amount_in_cents] }.freeze
-
1
before_validation do
-
554
self.account = @current_account if account.blank? && @current_account.present?
-
-
554
self.pipeline = stage.pipeline if pipeline.blank? && stage.present?
-
-
554
self.stage = pipeline.stages.first if stage.blank? && pipeline.present?
-
end
-
-
1
def self.ransackable_attributes(_auth_object = nil)
-
88
%w[]
-
end
-
-
1
def self.ransackable_associations(_auth_object = nil)
-
44
%w[users]
-
end
-
-
1
def total_amount_in_cents
-
59
total_deal_products_amount_in_cents
-
end
-
-
1
def next_event_planned?
-
24
next_event_planned
-
rescue StandardError
-
false
-
end
-
-
1
def next_event_planned
-
24
events.planned.first
-
rescue StandardError
-
nil
-
end
-
-
1
def self.csv_header(account_id)
-
custom_fields = CustomAttributeDefinition.where(attribute_model: 'deal_attribute').map do |i|
-
"custom_attributes.#{i.attribute_key}"
-
end
-
column_names.excluding('account_id', 'created_at', 'updated_at', 'id', 'custom_attributes') + custom_fields
-
end
-
-
## Events
-
-
1
include Wisper::Publisher
-
1
after_commit :publish_created, on: :create
-
1
after_commit :publish_updated, on: :update
-
-
1
private
-
-
1
def publish_created
-
481
broadcast(:deal_created, self)
-
end
-
-
1
def publish_updated
-
35
broadcast(:deal_updated, self)
-
end
-
end
-
1
class Deal::CreateOrUpdate
-
1
def initialize(deal, params)
-
41
@deal = deal
-
41
@params = params
-
end
-
-
1
def call
-
38
@deal.assign_attributes(@params)
-
38
return false if @deal.invalid?
-
-
35
set_lost_at_and_won_at if should_update_lost_at_or_won_at?
-
35
@deal.save!
-
35
@deal
-
end
-
-
1
private
-
-
1
def should_update_lost_at_or_won_at?
-
38
@deal.status_changed? || @deal.new_record?
-
end
-
-
1
def set_lost_at_and_won_at
-
25
allow_edit = Current.account.deal_allow_edit_lost_at_won_at
-
-
25
if @deal.won?
-
8
@deal.won_at = Time.current unless allow_edit && @params[:won_at].present?
-
8
@deal.lost_at = nil
-
8
@deal.lost_reason = ''
-
17
elsif @deal.lost?
-
8
@deal.lost_at = Time.current unless allow_edit && @params[:lost_at].present?
-
8
@deal.won_at = nil
-
else
-
9
@deal.lost_at = nil
-
9
@deal.won_at = nil
-
9
@deal.lost_reason = ''
-
end
-
end
-
end
-
1
class Deal::Migrations::PopulateDealLostAtAndWonAtJob < ApplicationJob
-
1
self.queue_adapter = :good_job
-
-
1
def perform(deal_id)
-
deal = Deal.find_by(id: deal_id)
-
return unless deal
-
-
ActiveRecord::Base.transaction do
-
if deal.won?
-
latest_won_event = deal.events
-
.where(kind: 'deal_won')
-
.order(created_at: :desc)
-
.select(:created_at)
-
.first
-
deal.update_column(:won_at, latest_won_event&.created_at) if latest_won_event
-
elsif deal.lost?
-
latest_lost_event = deal.events
-
.where(kind: 'deal_lost')
-
.order(created_at: :desc)
-
.select(:created_at)
-
.first
-
deal.update_column(:lost_at, latest_lost_event&.created_at) if latest_lost_event
-
end
-
end
-
Rails.logger.info "Processed deal #{deal.id} for lost_at and won_at"
-
end
-
end
-
1
class Deal::RecalculateAndSaveAllMonetaryValues
-
1
def initialize(deal)
-
9
@deal = deal
-
end
-
-
1
def call
-
9
ActiveRecord::Base.transaction do
-
9
recalculate_deal
-
end
-
end
-
-
1
private
-
-
1
def recalculate_deal
-
9
@deal.total_deal_products_amount_in_cents = @deal.deal_products.sum(:total_amount_in_cents)
-
9
@deal.save!
-
end
-
end
-
# == Schema Information
-
#
-
# Table name: deal_assignees
-
#
-
# id :bigint not null, primary key
-
# created_at :datetime not null
-
# updated_at :datetime not null
-
# deal_id :bigint not null
-
# user_id :bigint not null
-
#
-
# Indexes
-
#
-
# index_deal_assignees_on_deal_id (deal_id)
-
# index_deal_assignees_on_deal_id_and_user_id (deal_id,user_id) UNIQUE
-
# index_deal_assignees_on_user_id (user_id)
-
#
-
# Foreign Keys
-
#
-
# fk_rails_... (deal_id => deals.id)
-
# fk_rails_... (user_id => users.id)
-
#
-
1
class DealAssignee < ApplicationRecord
-
1
belongs_to :deal
-
1
belongs_to :user
-
-
1
validates :user_id, uniqueness: { scope: :deal_id, message: :taken }
-
end
-
# == Schema Information
-
#
-
# Table name: deal_lost_reasons
-
#
-
# id :bigint not null, primary key
-
# name :string default(""), not null
-
# created_at :datetime not null
-
# updated_at :datetime not null
-
#
-
1
class DealLostReason < ApplicationRecord
-
1
validates :name, presence: true
-
end
-
# == Schema Information
-
#
-
# Table name: deal_products
-
#
-
# id :bigint not null, primary key
-
# product_identifier :string default(""), not null
-
# product_name :string default(""), not null
-
# quantity :bigint default(1), not null
-
# total_amount_in_cents :bigint default(0), not null
-
# unit_amount_in_cents :bigint default(0), not null
-
# created_at :datetime not null
-
# updated_at :datetime not null
-
# deal_id :bigint not null
-
# product_id :bigint not null
-
#
-
# Indexes
-
#
-
# index_deal_products_on_deal_id (deal_id)
-
# index_deal_products_on_product_id (product_id)
-
#
-
# Foreign Keys
-
#
-
# fk_rails_... (deal_id => deals.id)
-
# fk_rails_... (product_id => products.id)
-
#
-
1
class DealProduct < ApplicationRecord
-
1
include DealProduct::Broadcastable
-
1
include DealProduct::EventCreator
-
1
include DealProduct::HandleInCentsValues
-
1
belongs_to :product
-
1
belongs_to :deal
-
1
validates :product_id, uniqueness: { scope: :deal_id, message: :taken }
-
-
1
FORM_FIELDS = %i[product_name unit_amount_in_cents product_identifier]
-
end
-
1
class DealProduct::CreateOrUpdate
-
1
def initialize(deal_product, params)
-
11
@deal_product = deal_product
-
11
@params = params
-
end
-
-
1
def call
-
11
@deal_product.assign_attributes(@params)
-
11
return false if @deal_product.invalid?
-
-
7
if needs_recalculation?
-
6
ActiveRecord::Base.transaction do
-
6
update_deal_product
-
6
@deal_product.save!
-
6
Deal::RecalculateAndSaveAllMonetaryValues.new(@deal_product.deal).call
-
end
-
else
-
1
@deal_product.save!
-
end
-
-
7
@deal_product
-
end
-
-
1
private
-
-
1
def needs_recalculation?
-
7
should_recalculate_base_values?
-
end
-
-
1
def update_deal_product
-
6
recalculate_from_base_values if should_recalculate_base_values?
-
end
-
-
1
def recalculate_from_base_values
-
6
@deal_product.total_amount_in_cents = @deal_product.quantity * @deal_product.unit_amount_in_cents
-
end
-
-
1
def should_recalculate_base_values?
-
13
@deal_product.quantity_changed? || @deal_product.unit_amount_in_cents_changed? || @deal_product.new_record?
-
end
-
end
-
1
class DealProduct::Destroy
-
1
def initialize(deal_product)
-
1
@deal_product = deal_product
-
end
-
-
1
def call
-
1
ActiveRecord::Base.transaction do
-
1
@deal_product.destroy!
-
1
Deal::RecalculateAndSaveAllMonetaryValues.new(@deal_product.deal).call
-
end
-
1
@deal_product
-
end
-
end
-
# == Schema Information
-
#
-
# Table name: embedding_documments
-
#
-
# id :bigint not null, primary key
-
# content :text
-
# embedding :vector(1536)
-
# source_reference :string
-
# source_type :string
-
# status :integer default(0)
-
# created_at :datetime not null
-
# updated_at :datetime not null
-
# source_id :bigint
-
#
-
# Indexes
-
#
-
# index_embedding_documments_on_source (source_type,source_id)
-
#
-
1
class EmbeddingDocumment < ApplicationRecord
-
1
belongs_to :source, polymorphic: true, optional: true
-
1
has_neighbors :embedding, normalize: true
-
end
-
# == Schema Information
-
#
-
# Table name: events
-
#
-
# id :bigint not null, primary key
-
# additional_attributes :jsonb
-
# app_type :string
-
# auto_done :boolean default(FALSE)
-
# custom_attributes :jsonb
-
# done_at :datetime
-
# from_me :boolean
-
# kind :string not null
-
# scheduled_at :datetime
-
# status :integer
-
# title :string default(""), not null
-
# created_at :datetime not null
-
# updated_at :datetime not null
-
# app_id :bigint
-
# contact_id :bigint
-
# deal_id :bigint
-
#
-
# Indexes
-
#
-
# index_events_on_app (app_type,app_id)
-
# index_events_on_contact_id (contact_id)
-
# index_events_on_deal_id (deal_id)
-
#
-
1
class Event < ApplicationRecord
-
1
include Deal::Broadcastable
-
# default_scope { order('created_at DESC') }
-
1
DEAL_UPDATE_KINDS = %w[deal_stage_change deal_opened deal_won deal_lost deal_reopened deal_product_added
-
deal_product_removed].freeze
-
1
belongs_to :deal, optional: true
-
1
belongs_to :contact
-
# belongs_to :event_kind, default: -> { EventKind }
-
# belongs_to :record, polymorphic: true
-
1
belongs_to :app, polymorphic: true, optional: true
-
1
has_rich_text :content
-
1
alias original_content content
-
-
1
attribute :done, :boolean
-
1
attribute :send_now, :boolean
-
1
validates :kind, presence: true
-
1
has_one :attachment, as: :attachable
-
-
1
after_commit do
-
# To refactory
-
786
if send_now == true
-
10
Accounts::Contacts::Events::SendNow.call(self)
-
776
elsif scheduled_delivery_event?
-
9
Accounts::Contacts::Events::EnqueueWorker.perform_async(id)
-
end
-
786
schedule_webpush_notifications
-
end
-
-
1
attribute :files, default: []
-
1
attribute :files_events, default: []
-
1
attribute :invalid_files
-
-
1
validate :validate_invalid_files
-
-
1
def validate_invalid_files
-
805
errors.add(:files, 'Invalid files') if invalid_files == true
-
end
-
-
1
def save
-
73
ActiveRecord::Base.transaction do
-
73
@result = super
-
73
return @result if @result == false
-
-
71
if files_events.present?
-
1
files_events.each do |file_event|
-
5
file_event.save!
-
end
-
end
-
end
-
71
@result
-
end
-
-
1
def schedule_webpush_notifications
-
786
return unless scheduled_at.present? && saved_change_to_scheduled_at? && !send_now
-
-
98
Pwa::SendNotificationsWorker.set(wait_until: scheduled_at).perform_later(id)
-
end
-
-
1
def content=(value)
-
213
original_content.body = value
-
end
-
-
1
def content
-
50
if text_content? && original_content.body.present?
-
21
original_content.body.to_plain_text
-
else
-
29
original_content
-
end
-
end
-
-
1
def text_content?
-
50
chatwoot_message? || evolution_api_message?
-
end
-
-
1
def generate_content_hash(key, value)
-
14
if content_is_blank?(value)
-
5
{ key.to_s => '' }
-
else
-
9
{ key.to_s => value }
-
end
-
end
-
-
1
def content_is_blank?(value)
-
14
value.respond_to?(:body)
-
end
-
-
1
def should_delivery_event_scheduled?
-
88
!done? && (Time.current.in_time_zone > scheduled_at)
-
end
-
-
1
def changed_scheduled_values?
-
776
saved_change_to_scheduled_at? || saved_change_to_auto_done?
-
end
-
-
1
def scheduled_delivery_event?
-
776
changed_scheduled_values? && (auto_done == true && scheduled_at.present? && done_at.blank?)
-
end
-
-
1
def done
-
1630
done_at.present?
-
end
-
-
1
def done?
-
114
done
-
end
-
-
1
def done=(value)
-
622
value_boolean = ActiveRecord::Type::Boolean.new.cast(value)
-
622
return if value_boolean == done
-
-
618
self.done_at = (Time.now if value_boolean == true)
-
end
-
-
1
def send_now=(value)
-
24
self[:send_now] = ActiveRecord::Type::Boolean.new.cast(value)
-
end
-
-
1
scope :to_do, lambda {
-
49
where('done_at IS NULL').order(:scheduled_at)
-
}
-
-
1
scope :planned, lambda {
-
27
to_do.where('auto_done = false AND scheduled_at IS NOT NULL').order(:scheduled_at)
-
}
-
-
1
scope :scheduled, lambda {
-
1
to_do.where('auto_done = true AND scheduled_at IS NOT NULL')
-
}
-
-
1
scope :planned_overdue, lambda {
-
1
planned.where('scheduled_at < ?', DateTime.current)
-
}
-
-
1
scope :planned_without_date, lambda {
-
1
to_do.where('auto_done = false AND scheduled_at IS NULL')
-
}
-
-
1
scope :done, lambda {
-
20
where('done_at IS NOT NULL').order(done_at: :desc)
-
}
-
-
1
scope :by_message_id, lambda { |message_id|
-
1
where("additional_attributes ->> 'message_id' = ?", message_id)
-
}
-
-
1
enum kind: {
-
'note': 'note',
-
'evolution_api_message': 'evolution_api_message',
-
'activity': 'activity',
-
'chatwoot_message': 'chatwoot_message',
-
'deal_stage_change': 'deal_stage_change',
-
'deal_opened': 'deal_opened',
-
'deal_won': 'deal_won',
-
'deal_lost': 'deal_lost',
-
'deal_reopened': 'deal_reopened',
-
'deal_product_added': 'deal_product_added',
-
'deal_product_removed': 'deal_product_removed'
-
}
-
-
1
enum status: { sent: 0, delivered: 1, read: 2, failed: 3 }
-
-
1
before_validation do
-
805
self.done = false if scheduled_at.present? && done.nil?
-
end
-
-
1
def icon_key
-
22
if note?
-
'menu-square'
-
22
elsif activity?
-
22
'calendar-check-2'
-
elsif chatwoot_message?
-
'message-circle'
-
end
-
end
-
-
1
def editable?
-
19
return true if %w[note activity].include?(kind)
-
6
return true if %w[chatwoot_message evolution_api_message].include?(kind) && !done?
-
-
5
false
-
end
-
-
1
def deal_updates?
-
13
DEAL_UPDATE_KINDS.include?(kind)
-
end
-
-
1
def kind_message?
-
chatwoot_message? || evolution_api_message?
-
end
-
-
1
def overdue?
-
return false if done == true || scheduled_at.blank?
-
-
DateTime.current > scheduled_at
-
end
-
-
1
def primary_date
-
11
if scheduled_at.present?
-
scheduled_at.iso8601
-
else
-
11
created_at.iso8601
-
end
-
end
-
-
1
def from
-
13
if from_me == true
-
2
'from-me'
-
else
-
11
'from-contacts'
-
end
-
end
-
-
1
def scheduled_kind
-
13
if done == true
-
7
'done'
-
else
-
6
'scheduled'
-
end
-
end
-
-
1
def has_media_attachment?
-
11
attachment.present? && (attachment.image? || attachment.file? || attachment.video?)
-
end
-
-
## Events
-
-
1
include Wisper::Publisher
-
1
after_commit :publish_created, on: :create
-
1
after_commit :publish_updated, on: :update
-
-
1
private
-
-
1
def publish_created
-
749
broadcast(:event_created, self)
-
end
-
-
1
def publish_updated
-
25
broadcast(:event_updated, self)
-
end
-
end
-
# == Schema Information
-
#
-
# Table name: installations
-
#
-
# id :string not null, primary key
-
# key1 :string default(""), not null
-
# key2 :string default(""), not null
-
# status :integer default("in_progress"), not null
-
# token :string default(""), not null
-
# created_at :datetime not null
-
# updated_at :datetime not null
-
# user_id :bigint
-
#
-
# Indexes
-
#
-
# index_installations_on_user_id (user_id)
-
#
-
1
class Installation < ApplicationRecord
-
1
include Installation::Complete
-
-
1
belongs_to :user, optional: true
-
-
1
validates_presence_of :key1
-
1
validates_presence_of :key2
-
1
validates_presence_of :token
-
-
1
enum status: {
-
in_progress: 0,
-
completed: 1
-
}
-
1
def self.installation_url
-
8
"#{ENV.fetch('STORE_URL', 'https://store.woofedcrm.com')}/installations/new?installation_params=#{{ url: ENV.fetch('FRONTEND_URL', 'http://localhost:3001'),
-
kind: :self_hosted }.to_json}"
-
end
-
-
1
def self.installation_flow?
-
660
Installation.first&.status != 'completed'
-
rescue StandardError
-
true
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Installation::Complete
-
1
def complete_installation!
-
3
return unless Installation.installation_flow?
-
2
return unless register_completed_install
-
-
1
completed!
-
1
app_reload
-
1
true
-
end
-
-
1
def register_completed_install
-
5
return false if Current.account.blank?
-
-
4
user = self.user
-
-
4
result_request = Faraday.post(
-
"#{ENV.fetch('STORE_URL', 'https://store.woofedcrm.com')}/installations/complete",
-
{
-
user_details: { name: user.full_name, email: user.email,
-
phone_number: user.phone, job_description: user.job_description },
-
company_details: { name: Current.account.name, site_url: Current.account.site_url,
-
segment: Current.account.segment, number_of_employees: Current.account.number_of_employees }
-
}.to_json,
-
{ 'Content-Type' => 'application/json', 'Authorization' => "Bearer #{token}" }
-
)
-
-
4
result_request.status == 200
-
end
-
-
1
def app_reload
-
6
load "#{Rails.root}/app/controllers/application_controller.rb"
-
6
Rails.application.reload_routes!
-
end
-
end
-
# == Schema Information
-
#
-
# Table name: pipelines
-
#
-
# id :bigint not null, primary key
-
# name :string default(""), not null
-
# created_at :datetime not null
-
# updated_at :datetime not null
-
#
-
1
class Pipeline < ApplicationRecord
-
1
broadcasts_refreshes
-
1
has_many :stages
-
1
has_many :deals
-
1
accepts_nested_attributes_for :stages, reject_if: :all_blank, allow_destroy: true
-
end
-
# == Schema Information
-
#
-
# Table name: products
-
#
-
# id :bigint not null, primary key
-
# additional_attributes :jsonb
-
# amount_in_cents :integer default(0), not null
-
# custom_attributes :jsonb
-
# description :text default(""), not null
-
# identifier :string default(""), not null
-
# name :string default(""), not null
-
# quantity_available :integer default(0), not null
-
# created_at :datetime not null
-
# updated_at :datetime not null
-
#
-
1
class Product < ApplicationRecord
-
1
include Product::Broadcastable
-
1
include CustomAttributes
-
-
1
has_many :attachments, as: :attachable
-
1
validates :quantity_available, :amount_in_cents,
-
numericality: { greater_than_or_equal_to: 0, message: 'Can not be negative' }
-
1
has_many :deal_products, dependent: :destroy
-
1
accepts_nested_attributes_for :attachments, reject_if: :all_blank, allow_destroy: true
-
-
1
FORM_FIELDS = %i[name amount_in_cents quantity_available identifier]
-
-
1
SHOW_FIELDS = { details: %i[name amount_in_cents quantity_available identifier description custom_attributes created_at
-
updated_at] }.freeze
-
-
1
%i[image file video].each do |file_type|
-
3
define_method "#{file_type}_attachments" do
-
38
attachments.by_file_type(file_type)
-
end
-
end
-
-
1
def self.ransackable_associations(auth_object = nil)
-
%w[account attachments deal_products]
-
end
-
-
1
def self.ransackable_attributes(_auth_object = nil)
-
20
%w[identifier amount_in_cents quantity_available description name created_at updated_at]
-
end
-
-
1
def amount_in_cents=(amount)
-
147
amount = sanitize_amount(amount)
-
147
super(amount)
-
end
-
end
-
1
class Query::AdvancedSearch
-
1
def initialize(current_user, current_account, params)
-
23
raise ArgumentError, 'current_user is required' if current_user.blank?
-
22
raise ArgumentError, 'current_account is required' if current_account.blank?
-
21
raise ArgumentError, 'params is required' if params.blank?
-
-
20
@current_user = current_user
-
20
@current_account = current_account
-
20
@params = params
-
20
@limit = 7
-
end
-
-
1
def call
-
6
case search_type
-
when 'contact'
-
2
{ contacts: filter_contacts }
-
when 'deal'
-
1
{ deals: filter_deals }
-
when 'product'
-
{ products: filter_products }
-
when 'pipeline'
-
{ pipelines: filter_pipelines }
-
when 'activity'
-
{ activities: filter_activities }
-
else
-
3
{ contacts: filter_contacts, deals: filter_deals, products: filter_products, pipelines: filter_pipelines,
-
activities: filter_activities }
-
end
-
end
-
-
1
private
-
-
1
attr_reader :current_user, :current_account, :params, :limit
-
-
1
def filter_contacts
-
8
scope = Contact
-
-
8
if search_query.present?
-
6
pattern = "%#{search_query}%"
-
6
scope = scope.where(
-
'full_name ILIKE :q OR email ILIKE :q OR phone ILIKE :q',
-
q: pattern
-
)
-
end
-
-
8
scope.reorder('updated_at DESC').limit(limit)
-
end
-
-
1
def filter_deals
-
6
scope = Deal
-
-
6
if search_query.present?
-
5
pattern = "%#{search_query}%"
-
5
scope = scope.where(
-
'name ILIKE :q',
-
q: pattern
-
)
-
end
-
-
6
scope.reorder('updated_at DESC').limit(limit)
-
end
-
-
1
def filter_products
-
6
scope = Product
-
-
6
if search_query.present?
-
5
pattern = "%#{search_query}%"
-
5
scope = scope.where(
-
'name ILIKE :q OR identifier ILIKE :q',
-
q: pattern
-
)
-
end
-
-
6
scope.reorder('updated_at DESC').limit(limit)
-
end
-
-
1
def filter_pipelines
-
5
scope = Pipeline
-
-
5
if search_query.present?
-
4
pattern = "%#{search_query}%"
-
4
scope = scope.where(
-
'name ILIKE :q',
-
q: pattern
-
)
-
end
-
-
5
scope.reorder('updated_at DESC').limit(limit)
-
end
-
-
1
def filter_activities
-
4
scope = Event.activity
-
-
4
if search_query.present?
-
4
pattern = "%#{search_query}%"
-
4
scope = scope.where(
-
'title ILIKE :q',
-
q: pattern
-
)
-
end
-
-
4
scope.reorder('updated_at DESC').limit(limit)
-
end
-
-
1
def search_type
-
7
@search_type ||= params[:search_type]&.downcase
-
end
-
-
1
def search_query
-
54
@search_query ||= params[:q].to_s.strip
-
end
-
end
-
1
class Query::Filter
-
1
def initialize(rel, params)
-
16
@rel = rel
-
16
@params = params
-
end
-
-
1
def call
-
16
apply_filters
-
end
-
-
1
private
-
-
1
attr_reader :rel, :params
-
-
1
def apply_filters
-
16
rel.ransack(params).result
-
end
-
end
-
# == Schema Information
-
#
-
# Table name: stages
-
#
-
# id :bigint not null, primary key
-
# name :string default(""), not null
-
# position :integer
-
# created_at :datetime not null
-
# updated_at :datetime not null
-
# pipeline_id :bigint not null
-
#
-
# Indexes
-
#
-
# index_stages_on_pipeline_id (pipeline_id)
-
#
-
# Foreign Keys
-
#
-
# fk_rails_... (pipeline_id => pipelines.id)
-
#
-
1
class Stage < ApplicationRecord
-
1
include Stage::Decorators
-
1
belongs_to :pipeline, touch: true
-
1
acts_as_list scope: :pipeline
-
1
has_many :deals, dependent: :destroy
-
-
1
scope :ordered_by_pipeline_and_position, lambda {
-
15
joins(:pipeline).order('pipelines.name ASC, stages.position ASC')
-
}
-
-
1
def total_amount_deals(filter_status_deal)
-
17
return deals.sum(&:total_amount_in_cents) if filter_status_deal == 'all'
-
-
14
deals.where(status: filter_status_deal).sum(&:total_amount_in_cents)
-
end
-
-
1
def total_quantity_deals(filter_status_deal)
-
17
return deals.count if filter_status_deal == 'all'
-
-
14
deals.where(status: filter_status_deal).count
-
end
-
end
-
# == Schema Information
-
#
-
# Table name: users
-
#
-
# id :bigint not null, primary key
-
# avatar_url :string default(""), not null
-
# email :string default(""), not null
-
# encrypted_password :string default(""), not null
-
# full_name :string default(""), not null
-
# job_description :string default("other"), not null
-
# language :string default("en"), not null
-
# notifications :jsonb not null
-
# phone :string
-
# remember_created_at :datetime
-
# reset_password_sent_at :datetime
-
# reset_password_token :string
-
# created_at :datetime not null
-
# updated_at :datetime not null
-
#
-
# Indexes
-
#
-
# index_users_on_email (email) UNIQUE
-
# index_users_on_reset_password_token (reset_password_token) UNIQUE
-
#
-
1
class User < ApplicationRecord
-
1
has_one :installation
-
1
has_many :webpush_subscriptions
-
1
has_many :deal_assignees, dependent: :destroy
-
1
has_many :deals, through: :deal_assignees
-
1
has_many :created_deals,
-
class_name: 'Deal',
-
foreign_key: 'created_by_id',
-
dependent: :nullify,
-
inverse_of: :creator
-
# Include default devise modules. Others available are:
-
# :confirmable, :lockable, :timeoutable, :trackable and :omniauthable
-
1
devise :database_authenticatable, :registerable,
-
:recoverable, :rememberable, :validatable
-
-
1
accepts_nested_attributes_for :account
-
-
1
attribute :language, :string, default: ENV.fetch('LANGUAGE', 'en')
-
-
1
validates :phone,
-
allow_blank: true,
-
format: { with: /\+[1-9]\d{1,14}\z/ }
-
1
store :notifications, accessors: [
-
:webpush_notify_on_event_expired
-
], coder: JSON
-
-
1
enum job_description: {
-
ceo: 'ceo',
-
cfo: 'cfo',
-
cto: 'cto',
-
project_manager: 'project_manager',
-
software_engineer: 'software_engineer',
-
marketing_manager: 'marketing_manager',
-
sales_representative: 'sales_representative',
-
hr_specialist: 'hr_specialist',
-
customer_support: 'customer_support',
-
product_manager: 'product_manager',
-
operations_manager: 'operations_manager',
-
business_development_manager: 'business_development_manager',
-
data_analyst: 'data_analyst',
-
account_manager: 'account_manager',
-
consultant: 'consultant',
-
financial_analyst: 'financial_analyst',
-
graphic_designer: 'graphic_designer',
-
ux_ui_designer: 'ux_ui_designer',
-
content_creator: 'content_creator',
-
legal_counsel: 'legal_counsel',
-
research_scientist: 'research_scientist',
-
it_administrator: 'it_administrator',
-
system_administrator: 'system_administrator',
-
project_coordinator: 'project_coordinator',
-
operations_coordinator: 'operations_coordinator',
-
executive_assistant: 'executive_assistant',
-
other: 'other'
-
}
-
-
1
def self.ransackable_attributes(_auth_object = nil)
-
119
%w[full_name email created_at updated_at phone language job_description id
-
avatar_url]
-
end
-
-
1
def get_jwt_token
-
70
Users::JsonWebToken.encode_user(self)
-
end
-
-
1
def webpush_notify_on_event_expired=(value)
-
2
self[:notifications][:webpush_notify_on_event_expired] = ActiveRecord::Type::Boolean.new.cast(value)
-
end
-
end
-
# == Schema Information
-
#
-
# Table name: webhooks
-
#
-
# id :bigint not null, primary key
-
# status :string default("active")
-
# url :string default(""), not null
-
# created_at :datetime not null
-
# updated_at :datetime not null
-
#
-
1
class Webhook < ApplicationRecord
-
1
validates :url, presence: true, format: URI::DEFAULT_PARSER.make_regexp(%w[http https])
-
1
validates :status, presence: true
-
1
validate :validate_webhook_url, if: :active?
-
1
enum status: {
-
inactive: 'inactive',
-
active: 'active'
-
}
-
-
1
after_update_commit do
-
1
broadcast_replace_later_to "webhooks_#{account_id}", target: self, partial: 'accounts/settings/webhooks/webhook',
-
locals: { webhook: self }
-
end
-
1
after_create_commit do
-
22
broadcast_append_later_to "webhooks_#{account_id}", target: 'webhooks',
-
partial: 'accounts/settings/webhooks/webhook', locals: { webhook: self }
-
end
-
1
after_destroy_commit do
-
1
broadcast_remove_to "webhooks_#{account_id}", target: self
-
end
-
-
1
def valid_url?
-
9
return false if url.blank?
-
-
6
response = Webhook::ApiClient.new(self).post_request
-
-
4
return false if response.key?(:error)
-
-
3
true
-
rescue Faraday::ConnectionFailed, Faraday::TimeoutError, Faraday::SSLError
-
2
false
-
end
-
-
1
private
-
-
1
def validate_webhook_url
-
9
return if valid_url?
-
-
3
errors.add(:url)
-
end
-
end
-
1
class Webhook::ApiClient
-
1
def initialize(webhook)
-
8
@webhook = webhook
-
8
@connection = create_connection
-
end
-
-
1
def create_connection
-
8
Faraday.new(@webhook.url) do |faraday|
-
8
faraday.options.timeout = 5
-
8
faraday.headers = { 'Content-Type': 'application/json' }
-
end
-
end
-
-
1
def post_request
-
8
response = @connection.post
-
-
8
if response.success?
-
4
{ ok: response.status, request: response }
-
else
-
4
logger_error('Failed to validate webhook URL', response)
-
4
{ error: "Invalid or unreachable URL (status: #{response.status})", request: response }
-
end
-
end
-
-
1
private
-
-
1
def logger_error(message, response)
-
4
Rails.logger.error "Webhook Api Client error: #{message} - Webhook #{@webhook.id || 'new'}"
-
4
Rails.logger.error "Webhook: #{@webhook.inspect}"
-
4
Rails.logger.error "Request: #{response.inspect}"
-
end
-
end
-
# == Schema Information
-
#
-
# Table name: webpush_subscriptions
-
#
-
# id :bigint not null, primary key
-
# auth_key :string default(""), not null
-
# endpoint :string default(""), not null
-
# p256dh_key :string default(""), not null
-
# created_at :datetime not null
-
# updated_at :datetime not null
-
# user_id :bigint not null
-
#
-
# Indexes
-
#
-
# index_webpush_subscriptions_on_user_id (user_id)
-
#
-
# Foreign Keys
-
#
-
# fk_rails_... (user_id => users.id)
-
#
-
1
class WebpushSubscription < ApplicationRecord
-
1
belongs_to :user
-
1
validates :endpoint, presence: true
-
1
validates :p256dh_key, presence: true
-
1
validates :auth_key, presence: true, uniqueness: true
-
-
1
def send_notification(message)
-
2
WebPush.payload_send(
-
message: JSON.generate(message),
-
endpoint: endpoint,
-
p256dh: p256dh_key,
-
auth: auth_key,
-
vapid: {
-
private_key: ENV['VAPID_PRIVATE_KEY'],
-
public_key: ENV['VAPID_PUBLIC_KEY']
-
}
-
)
-
rescue WebPush::ExpiredSubscription
-
1
destroy
-
end
-
end
-
1
class Accounts::Apps::Chatwoots::Create
-
1
def self.call(account, chatwoot_params)
-
2
chatwoot = account.apps_chatwoots.build(chatwoot_params)
-
2
if chatwoot.save
-
1
Accounts::Apps::Chatwoots::SyncChatwootWorker.perform_async(account.id, chatwoot.id)
-
1
{ ok: chatwoot }
-
else
-
1
{ error: chatwoot }
-
end
-
end
-
end
-
1
class Accounts::Apps::Chatwoots::CreateConversation
-
1
def self.call(chatwoot, contact_id, inbox_id)
-
4
request = Faraday.post(
-
"#{chatwoot.chatwoot_endpoint_url}/api/v1/accounts/#{chatwoot.chatwoot_account_id}/conversations",
-
build_body(contact_id, inbox_id).to_json,
-
chatwoot.request_headers
-
)
-
4
return { ok: JSON.parse(request.body) }
-
end
-
-
1
def self.build_body(contact_id, inbox_id)
-
{
-
4
"inbox_id": inbox_id,
-
"contact_id": contact_id,
-
}
-
end
-
end
-
1
class Accounts::Apps::Chatwoots::Delete
-
1
def self.call(account, chatwoot)
-
1
if chatwoot.destroy
-
1
Accounts::Apps::Chatwoots::RemoveChatwootIdFromContactsWorker.perform_async(account.id)
-
1
{ ok: 'Chatwoot was successfully destroyed.' }
-
else
-
{ error: chatwoot }
-
end
-
end
-
end
-
1
class Accounts::Apps::Chatwoots::ExportContact
-
1
def self.call(chatwoot, contact)
-
9
create_or_update_contact(chatwoot, contact)
-
end
-
-
1
def self.create_or_update_contact(chatwoot, contact)
-
9
contact_chatwoot_id = contact['additional_attributes']['chatwoot_id']
-
-
9
if contact_chatwoot_id.present?
-
2
update_contact(chatwoot, contact)
-
else
-
7
create_contact(chatwoot, contact)
-
end
-
end
-
-
1
def self.update_contact(chatwoot, contact)
-
2
request = Faraday.put(
-
"#{chatwoot.chatwoot_endpoint_url}/api/v1/accounts/#{chatwoot.chatwoot_account_id}/contacts/#{contact.additional_attributes['chatwoot_id']}",
-
build_body(contact),
-
chatwoot.request_headers
-
)
-
2
if request.status == 200
-
1
export_contact_tags(chatwoot, contact)
-
1
{ ok: contact }
-
else
-
1
{ error: request.body }
-
end
-
end
-
-
1
def self.export_contact_tags(chatwoot, contact)
-
4
request = Faraday.post(
-
"#{chatwoot.chatwoot_endpoint_url}/api/v1/accounts/#{chatwoot.chatwoot_account_id}/contacts/#{contact.additional_attributes['chatwoot_id']}/labels",
-
{ labels: contact.label_list }.to_json,
-
chatwoot.request_headers
-
)
-
4
JSON.parse(request.body)['payload']
-
end
-
-
1
def self.create_contact(chatwoot, contact)
-
7
request = Faraday.post(
-
"#{chatwoot.chatwoot_endpoint_url}/api/v1/accounts/#{chatwoot.chatwoot_account_id}/contacts",
-
build_body(contact),
-
chatwoot.request_headers
-
)
-
7
response_body = JSON.parse(request.body)
-
7
if response_body['message'] == 'Email has already been taken' && request.status == 422
-
1
search_chatwoot_contact = Accounts::Apps::Chatwoots::SearchContact.call(chatwoot, contact['email'])
-
1
update_contact_chatwoot_id_and_identifier(contact, search_chatwoot_contact['id'],
-
search_chatwoot_contact['identifier'])
-
1
{ ok: contact }
-
6
elsif response_body['message'] == 'Phone number has already been taken' && request.status == 422
-
1
search_chatwoot_contact = Accounts::Apps::Chatwoots::SearchContact.call(chatwoot, contact['phone'])
-
1
update_contact_chatwoot_id_and_identifier(contact, search_chatwoot_contact['id'],
-
search_chatwoot_contact['identifier'])
-
1
{ ok: contact }
-
5
elsif request.status == 200
-
3
update_contact_chatwoot_id_and_identifier(contact, response_body['payload']['contact']['id'],
-
response_body['payload']['contact']['identifier'])
-
3
export_contact_tags(chatwoot, contact)
-
3
{ ok: contact }
-
else
-
2
Rails.logger.error(
-
'Error when export contact to chatwoot,' +
-
"Chatwoot Apps: #{chatwoot.inspect}," +
-
"Chatwoot request: #{request.inspect}," +
-
"Chatwoot response: #{request.body}"
-
)
-
2
{ error: request.body }
-
end
-
end
-
-
1
def self.update_contact_chatwoot_id_and_identifier(contact, chatwoot_id, chatwoot_identifier)
-
5
contact.update(additional_attributes: contact['additional_attributes'].merge({ chatwoot_id: chatwoot_id,
-
chatwoot_identifier: chatwoot_identifier }))
-
end
-
-
1
def self.build_body(contact)
-
{
-
9
"name": contact['full_name'],
-
"email": contact['email'],
-
"phone_number": contact['phone'],
-
"custom_attributes": contact['custom_attributes']
-
}.to_json
-
end
-
end
-
1
class Accounts::Apps::Chatwoots::ExportContactWorker
-
1
include Sidekiq::Worker
-
1
sidekiq_options queue: :chatwoot_webhooks
-
-
1
def perform(chatwoot_id, contact_id)
-
1
contact = Contact.find_by_id(contact_id)
-
1
chatwoot = Apps::Chatwoot.find_by_id(chatwoot_id)
-
1
Accounts::Apps::Chatwoots::ExportContact.call(chatwoot, contact) if contact.present? && chatwoot.present?
-
end
-
end
-
1
class Accounts::Apps::Chatwoots::FindOrCreateConversation
-
1
def self.call(chatwoot, contact_id, inbox_id)
-
9
conversations = Accounts::Apps::Chatwoots::GetConversations.call(
-
chatwoot, contact_id, inbox_id
-
)
-
-
9
return { ok: conversations.dig(:ok, 0) } if conversations.dig(:ok, 0, 'id').present?
-
-
3
Accounts::Apps::Chatwoots::CreateConversation.call(
-
chatwoot, contact_id, inbox_id
-
)
-
end
-
end
-
1
class Accounts::Apps::Chatwoots::GetConversationAndSendMessage
-
1
def self.call(chatwoot, contact_id, inbox_id, event)
-
7
conversation = Accounts::Apps::Chatwoots::FindOrCreateConversation.call(
-
chatwoot, contact_id, inbox_id
-
)[:ok]
-
-
7
Accounts::Apps::Chatwoots::SendMessage.call(chatwoot, conversation['id'], event)
-
end
-
end
-
1
class Accounts::Apps::Chatwoots::GetConversations
-
1
def self.call(chatwoot, contact_id, inbox_id = nil)
-
11
request = Faraday.get(
-
"#{chatwoot.chatwoot_endpoint_url}/api/v1/accounts/#{chatwoot.chatwoot_account_id}/contacts/#{contact_id}/conversations",
-
{},
-
chatwoot.request_headers
-
)
-
-
11
conversation_list = JSON.parse(request.body)['payload']
-
-
11
return { ok: conversation_list } if inbox_id.nil?
-
-
5
{ ok: list_conversations_by_inbox(conversation_list, inbox_id) }
-
end
-
-
1
def self.list_conversations_by_inbox(conversation_list, inbox_id)
-
5
conversation_list.select do |conversation|
-
32
conversation['inbox_id'] == inbox_id.to_i
-
end
-
end
-
end
-
1
class Accounts::Apps::Chatwoots::GetInboxes
-
1
def self.call(chatwoot)
-
8
inboxes_request = Faraday.get(
-
"#{chatwoot.chatwoot_endpoint_url}/api/v1/accounts/#{chatwoot.chatwoot_account_id}/inboxes",
-
{},
-
chatwoot.request_headers
-
)
-
7
return { ok: JSON.parse(inboxes_request.body)['payload'] } if inboxes_request.status == 200
-
-
3
{ error: inboxes_request.body }
-
end
-
end
-
1
class Accounts::Apps::Chatwoots::Messages::DeliveryJob < ApplicationJob
-
1
self.queue_adapter = :good_job
-
1
def perform(event_id)
-
6
event = Event.find(event_id)
-
6
if event.should_delivery_event_scheduled?
-
6
result = Accounts::Apps::Chatwoots::GetConversationAndSendMessage.call(
-
event.app,
-
event.contact.additional_attributes['chatwoot_id'],
-
event.additional_attributes['chatwoot_inbox_id'],
-
event
-
)
-
6
if result.key?(:ok)
-
6
event.additional_attributes['chatwoot_id'] = result[:ok]['id']
-
6
event.additional_attributes['chatwoot_conversation_id'] = result[:ok]['conversation_id']
-
6
event.done = true
-
6
event.save!
-
6
{ ok: event }
-
else
-
{ error: result[:error] }
-
end
-
end
-
end
-
end
-
1
class Accounts::Apps::Chatwoots::RemoveChatwootIdFromContacts
-
1
def self.call(account)
-
1
account.contacts.where("additional_attributes -> 'chatwoot_id' IS NOT NULL").find_each do |contact|
-
1
contact.additional_attributes.delete('chatwoot_id')
-
1
contact.save
-
end
-
1
return { ok: 'Contacts chatwoot id was successfully removed.' }
-
end
-
end
-
-
1
class Accounts::Apps::Chatwoots::RemoveChatwootIdFromContactsWorker
-
1
include Sidekiq::Worker
-
1
sidekiq_options queue: :chatwoot_webhooks
-
1
def perform(account_id)
-
1
account = Account.find(account_id)
-
1
Accounts::Apps::Chatwoots::RemoveChatwootIdFromContacts.call(account)
-
end
-
end
-
-
1
class Accounts::Apps::Chatwoots::SearchContact
-
1
def self.call(chatwoot, params)
-
4
request = Faraday.get(
-
"#{chatwoot.chatwoot_endpoint_url}/api/v1/accounts/#{chatwoot.chatwoot_account_id}/contacts/search",
-
build_body(params),
-
chatwoot.request_headers
-
)
-
4
body = JSON.parse(request.body)
-
-
4
return body['payload'].first if body['payload'].present?
-
1
return { error: 'Contact not found' }
-
end
-
-
1
def self.build_body(content)
-
{
-
4
"q": content,
-
}
-
end
-
end
-
1
class Accounts::Apps::Chatwoots::SendMessage
-
1
def self.call(chatwoot, conversation_id, event)
-
10
if event.attachment.present?
-
2
send_message_with_attachment(chatwoot, conversation_id, event)
-
else
-
8
send_message_without_attachment(chatwoot, conversation_id, event)
-
end
-
end
-
-
1
def self.send_message_with_attachment(chatwoot, conversation_id, event)
-
2
require 'uri'
-
2
require 'net/http'
-
-
2
url = URI("#{chatwoot.chatwoot_endpoint_url}/api/v1/accounts/#{chatwoot.chatwoot_account_id}/conversations/#{conversation_id}/messages")
-
-
2
https = Net::HTTP.new(url.host, url.port)
-
2
https.use_ssl = true
-
-
2
request = Net::HTTP::Post.new(url)
-
2
request['api_access_token'] = chatwoot.chatwoot_user_token
-
2
form_data = [['attachments[]', event.attachment.file_download],
-
['content', event.generate_content_hash('content', event.content)['content'].to_s]]
-
2
request.set_form form_data, 'multipart/form-data'
-
2
response = https.request(request)
-
2
{ ok: JSON.parse(response.read_body) }
-
end
-
-
1
def self.send_message_without_attachment(chatwoot, conversation_id, event)
-
8
request = Faraday.post(
-
"#{chatwoot.chatwoot_endpoint_url}/api/v1/accounts/#{chatwoot.chatwoot_account_id}/conversations/#{conversation_id}/messages",
-
build_body(event).to_json,
-
request_headers(event, chatwoot)
-
)
-
8
{ ok: JSON.parse(request.body) }
-
end
-
-
1
def self.build_body(event)
-
8
event.generate_content_hash('content', event.content)
-
end
-
-
1
def self.request_headers(event, chatwoot)
-
8
if event.attachment.present?
-
{ 'api_access_token': chatwoot.chatwoot_user_token.to_s,
-
'Content-Type': 'multipart/form-data; boundary=----WebKitFormBoundary' }
-
else
-
8
chatwoot.request_headers
-
end
-
end
-
end
-
1
class Accounts::Apps::Chatwoots::SyncChatwootWorker
-
1
include Sidekiq::Worker
-
1
sidekiq_options queue: :chatwoot_webhooks
-
-
1
def perform(account_id, chatwoot_id)
-
chatwoot = Apps::Chatwoot.find(chatwoot_id)
-
account = Account.find(account_id)
-
-
Accounts::Apps::Chatwoots::SyncImportContacts.new(chatwoot).call
-
Accounts::Apps::Chatwoots::SyncExportContacts.call(account)
-
end
-
end
-
1
class Accounts::Apps::Chatwoots::SyncExportContacts
-
1
def self.call(account)
-
1
export_contacts(account)
-
end
-
-
1
def self.export_contacts(account)
-
1
account.contacts.where("additional_attributes -> 'chatwoot_id' IS NULL").find_in_batches(batch_size: 30) do |group|
-
1
group.each do |contact|
-
1
Accounts::Apps::Chatwoots::ExportContact.call(account.apps_chatwoots.first, contact)
-
end
-
1
sleep(15)
-
end
-
1
{ ok: 'Contacts exported successfully' }
-
end
-
end
-
1
class Accounts::Apps::Chatwoots::SyncImportContacts
-
1
def initialize(chatwoot)
-
6
@chatwoot = chatwoot
-
6
@account = @chatwoot.account
-
end
-
-
1
def call
-
6
response = update_or_create_contact
-
6
{ ok: response }
-
end
-
-
1
def update_or_create_contact
-
6
contacts_imported = 0
-
6
contacts_failed = 0
-
6
contacts_updated = 0
-
6
quantity_per_page = 1
-
6
page = 0
-
6
until quantity_per_page.zero?
-
18
page += 1
-
18
request = Faraday.get(
-
"#{@chatwoot.chatwoot_endpoint_url}/api/v1/accounts/#{@chatwoot.chatwoot_account_id}/contacts/",
-
{ page: page },
-
@chatwoot.request_headers
-
)
-
18
if request.status == 200
-
18
chatwoot_contacts = JSON.parse(request.body)['payload']
-
18
quantity_per_page = chatwoot_contacts.count
-
18
chatwoot_contacts.each do |chatwoot_contact|
-
12
contact = Accounts::Contacts::GetByParams.call(@account,
-
chatwoot_contact.slice('email',
-
'phone', 'identifier').transform_values(&:to_s))
-
12
if contact[:ok]
-
3
update_contact_chatwoot_id(contact[:ok], chatwoot_contact['id'])
-
3
import_labels(contact[:ok])
-
3
contact[:ok].save
-
3
contacts_updated += 1
-
else
-
9
contact = build_contact_att(chatwoot_contact)
-
9
import_labels(contact)
-
9
if contact.save
-
9
contacts_imported += 1
-
else
-
contacts_failed += 1
-
Rails.logger.error("Error import contact from chatwoot #{contact.errors.inspect}, chatwoot: #{@chatwoot.inspect}")
-
end
-
end
-
end
-
else
-
return { error: request.body }
-
end
-
end
-
6
"Contacts imported #{contacts_imported} / Contacts updated #{contacts_updated} / Contacts failed #{contacts_failed}"
-
end
-
-
1
def update_contact_chatwoot_id(contact, chatwoot_id)
-
3
contact.additional_attributes.merge!({ 'chatwoot_id' => chatwoot_id })
-
end
-
-
1
def import_labels(contact)
-
12
Accounts::Apps::Chatwoots::Webhooks::ImportContact.import_contact_tags(@chatwoot, contact)
-
12
Accounts::Apps::Chatwoots::Webhooks::ImportContact.import_contact_converstions_tags(@chatwoot, contact)
-
end
-
-
1
def build_contact_att(body)
-
9
contact = @account.contacts.new(
-
full_name: body['name'],
-
9
email: (body['email']).to_s,
-
9
phone: (body['phone_number']).to_s
-
)
-
9
contact.additional_attributes.merge!({ 'chatwoot_id' => body['id'], 'chatwoot_identifier' => body['identifier'] })
-
9
contact.custom_attributes.merge!(body['custom_attributes'])
-
9
contact
-
end
-
end
-
1
class Accounts::Apps::Chatwoots::SyncInboxes
-
-
1
def self.call(chatwoot)
-
inboxes_request = Faraday.get(
-
"#{chatwoot.chatwoot_endpoint_url}/api/v1/accounts/#{chatwoot.chatwoot_account_id}/inboxes",
-
{},
-
chatwoot.request_headers
-
)
-
-
chatwoot.update(inboxes: JSON.parse(inboxes_request.body)['payload'])
-
return true
-
end
-
end
-
1
class Accounts::Apps::Chatwoots::Webhooks::Events::Contact
-
1
def self.call(chatwoot, webhook)
-
3
return Accounts::Apps::Chatwoots::Webhooks::ImportContact.call(chatwoot, webhook['id'])
-
end
-
end
-
1
class Accounts::Apps::Chatwoots::Webhooks::Events::ConversationUpdated
-
1
def self.call(chatwoot, webhook)
-
1
contact = Accounts::Apps::Chatwoots::Webhooks::ImportContact.call(chatwoot, webhook['contact_inbox']['contact_id'])
-
1
return { ok: contact }
-
end
-
end
-
1
class Accounts::Apps::Chatwoots::Webhooks::Events::Message
-
-
1
def self.call(chatwoot, webhook)
-
25
contact = Accounts::Apps::Chatwoots::Webhooks::ImportContact.call(chatwoot, webhook['conversation']['contact_inbox']['contact_id'])[:ok]
-
25
message = Accounts::Apps::Chatwoots::Webhooks::ImportMessage.new(chatwoot, contact, webhook).call
-
25
return { ok: message }
-
end
-
end
-
1
class Accounts::Apps::Chatwoots::Webhooks::ImportContact
-
1
def self.call(chatwoot, contact_id)
-
29
contact = get_or_import_contact(chatwoot, contact_id)
-
29
{ ok: contact }
-
end
-
-
1
def self.get_or_import_contact(chatwoot, contact_id)
-
29
contact = Contact.by_chatwoot_id(contact_id).first
-
-
29
contact_att = get_contact(chatwoot, contact_id)
-
29
return 'Contact not found' if contact_att == false
-
-
28
contact = if contact.present?
-
1
update_contact(contact, contact_att)
-
else
-
27
import_contact(chatwoot, contact_att)
-
end
-
-
28
contact = import_contact_tags(chatwoot, contact)
-
28
contact = import_contact_converstions_tags(chatwoot, contact)
-
28
contact.save
-
28
contact
-
end
-
-
1
def self.get_contact(chatwoot, contact_id)
-
29
contact_response = Faraday.get(
-
"#{chatwoot.chatwoot_endpoint_url}/api/v1/accounts/#{chatwoot.chatwoot_account_id}/contacts/#{contact_id}",
-
{},
-
chatwoot.request_headers
-
)
-
29
if contact_response.status == 200
-
28
body = JSON.parse(contact_response.body)
-
28
body['payload']
-
1
elsif contact_response.status == 404
-
1
Rails.logger.info "Contact id #{contact_id} not found in Chatwoot App #{chatwoot.id}"
-
1
false
-
else
-
Rails.logger.info "contact_response: #{contact_response.inspect}"
-
Rails.logger.info "contact_response body: #{contact_response.body}"
-
raise 'ErrorChatwootGetContact'
-
end
-
end
-
-
1
def self.import_contact_converstions_tags(chatwoot, contact)
-
40
contact_response = Faraday.get(
-
"#{chatwoot.chatwoot_endpoint_url}/api/v1/accounts/#{chatwoot.chatwoot_account_id}/contacts/#{contact.additional_attributes['chatwoot_id']}/conversations",
-
{},
-
chatwoot.request_headers
-
)
-
-
40
body = JSON.parse(contact_response.body)
-
80
conversations_tags = body['payload'].map { |c| c['labels'] }.flatten.uniq
-
40
contact.assign_attributes({ chatwoot_conversations_label_list: conversations_tags })
-
40
contact
-
end
-
-
1
def self.import_contact_tags(chatwoot, contact)
-
40
contact_response = Faraday.get(
-
"#{chatwoot.chatwoot_endpoint_url}/api/v1/accounts/#{chatwoot.chatwoot_account_id}/contacts/#{contact.additional_attributes['chatwoot_id']}/labels",
-
{},
-
chatwoot.request_headers
-
)
-
40
body = JSON.parse(contact_response.body)
-
40
contact.assign_attributes({ label_list: body['payload'] })
-
40
contact
-
end
-
-
1
def self.import_contact(chatwoot, contact_att)
-
27
contact = chatwoot.account.contacts.new
-
27
build_contact_att(contact, contact_att)
-
end
-
-
1
def self.update_contact(contact, contact_att)
-
1
build_contact_att(contact, contact_att)
-
end
-
-
1
def self.build_contact_att(contact, body)
-
28
contact.assign_attributes({
-
full_name: body['name'],
-
28
email: (body['email']).to_s,
-
28
phone: (body['phone_number']).to_s
-
})
-
-
28
contact.additional_attributes.merge!({ 'chatwoot_id' => body['id'], 'chatwoot_identifier' => body['identifier'] })
-
28
contact.custom_attributes.merge!(body['custom_attributes'])
-
28
contact
-
end
-
end
-
1
class Accounts::Apps::Chatwoots::Webhooks::ImportMessage
-
1
require 'open-uri'
-
-
1
def initialize(chatwoot, contact, webhook)
-
25
@chatwoot = chatwoot
-
25
@contact = contact
-
25
@webhook = webhook
-
end
-
-
1
def call
-
25
message = get_or_import_message()
-
25
{ ok: message }
-
end
-
-
1
def get_or_import_message()
-
25
message = @contact.events.where(
-
'? <@ additional_attributes', { chatwoot_id: @webhook['id'] }.to_json
-
).first
-
25
if message.nil?
-
25
if attachments?
-
3
message = import_message_with_attachments()
-
else
-
22
message = import_message()
-
end
-
end
-
25
message
-
end
-
-
1
def import_message_with_attachments()
-
3
@webhook['attachments'].reverse.map.with_index do |attachment, index|
-
5
import_message(
-
{ attachment: attachment, order: index, last_element: last_element?(index)}
-
)
-
end
-
end
-
-
1
def last_element?(index)
-
5
index == @webhook['attachments'].count - 1
-
end
-
-
1
def import_message(attachment_params = {})
-
27
message = @contact.events.new(
-
account: @chatwoot.account,
-
kind: 'chatwoot_message',
-
from_me: is_from_me?(),
-
contact: @contact,
-
done: true,
-
done_at: build_done_at(attachment_params[:order]),
-
app: @chatwoot
-
)
-
27
message.additional_attributes.merge!({ 'chatwoot_id' => @webhook['conversation']['messages'].first['id'] })
-
-
27
if attachment_params.present?
-
5
create_attachment(message, attachment_params[:attachment])
-
5
message.content = @webhook['content'] if attachment_params[:last_element] == true
-
else
-
22
message.content = @webhook['content']
-
end
-
-
27
message.save
-
27
message
-
end
-
-
1
def create_attachment(event, attachment_params)
-
begin
-
5
downloaded_file = URI.open(attachment_params['data_url'])
-
4
attachment = event.build_attachment(
-
file_type: attachment_params['file_type']
-
)
-
4
attachment.file.attach(io: downloaded_file,
-
filename: File.basename(attachment_params['data_url']))
-
rescue OpenURI::HTTPError
-
1
event.status = 'failed'
-
end
-
end
-
-
1
def build_done_at(order = nil)
-
27
if order.present?
-
5
created_at = @webhook['created_at'].dup
-
5
created_at.to_time + miliseconds(order)
-
else
-
22
@webhook['created_at'].to_time
-
end
-
end
-
-
1
def miliseconds(miliseconds)
-
5
miliseconds/1000.0
-
end
-
-
1
def attachments?()
-
25
@webhook['attachments'].present?
-
end
-
-
1
def is_from_me?()
-
27
@webhook.dig('sender', 'type') == 'user' if @webhook.dig('sender', 'id').present?
-
end
-
end
-
1
class Accounts::Apps::Chatwoots::Webhooks::ProcessWebhook
-
1
def self.call(webhook)
-
24
chatwoot = Apps::Chatwoot.find_by(embedding_token: webhook['token'])
-
-
24
return { error: 'Chatwoot integration not found' } if chatwoot.blank?
-
23
return { error: 'Chatwoot integration inactive' } if chatwoot.inactive?
-
-
22
if webhook['event'].include?('contact_')
-
2
Accounts::Apps::Chatwoots::Webhooks::Events::Contact.call(
-
chatwoot, webhook
-
)
-
20
elsif webhook['event'] == 'conversation_updated'
-
Accounts::Apps::Chatwoots::Webhooks::Events::ConversationUpdated.call(
-
chatwoot, webhook
-
)
-
20
elsif webhook['event'].include?('message_')
-
20
Accounts::Apps::Chatwoots::Webhooks::Events::Message.call(
-
chatwoot, webhook
-
)
-
end
-
-
22
{ ok: chatwoot }
-
end
-
end
-
# frozen_string_literal: true
-
-
1
class Accounts::Apps::Chatwoots::Webhooks::ProcessWebhookJob < ApplicationJob
-
1
include GoodJob::ActiveJobExtensions::Concurrency
-
-
1
self.queue_adapter = :good_job
-
-
1
good_job_control_concurrency_with(
-
# Maximum number of jobs with the concurrency key to be
-
# concurrently performed (excludes enqueued jobs)
-
perform_limit: 1,
-
60
key: -> { "#{self.class.name}-#{arguments.last}" }
-
)
-
-
1
def perform(event, _token)
-
20
event_hash = JSON.parse(event)
-
20
Accounts::Apps::Chatwoots::Webhooks::ProcessWebhook.call(event_hash)
-
end
-
end
-
1
class Accounts::Apps::EvolutionApis::Create
-
1
def self.call(user, evolution_apis_params)
-
2
evolution_api = EvolutionApiBuilder.new(user, evolution_apis_params).build
-
2
if evolution_api.save
-
1
Accounts::Apps::EvolutionApis::Instance::Create.call(evolution_api)
-
1
return { ok: evolution_api }
-
else
-
1
return { error: evolution_api }
-
end
-
end
-
end
-
1
class Accounts::Apps::EvolutionApis::Instance::Create
-
-
1
def self.call(evolution_api)
-
4
evolution_api.update(connection_status: 'connecting')
-
4
request = Faraday.post(
-
"#{evolution_api.endpoint_url}/instance/create",
-
build_body(evolution_api).to_json,
-
{'apiKey': "#{ENV['EVOLUTION_API_ENDPOINT_TOKEN']}", 'Content-Type': 'application/json'}
-
)
-
4
if request.status == 201
-
# set_settings(evolution_api)
-
3
return { ok: JSON.parse(request.body) }
-
else
-
1
evolution_api.update(connection_status: 'disconnected')
-
1
return { error: JSON.parse(request.body) }
-
end
-
end
-
-
1
def self.set_settings(evolution_api)
-
Faraday.post(
-
"#{evolution_api.endpoint_url}/settings/set/#{evolution_api.instance}",
-
{
-
"reject_call": false,
-
"groups_ignore": false,
-
"always_online": false,
-
"read_messages": false,
-
"read_status": false
-
}.to_json,
-
evolution_api.request_instance_headers
-
)
-
end
-
-
1
def self.build_body(evolution_api)
-
{
-
4
"instanceName": evolution_api.instance,
-
"token": evolution_api.token,
-
"qrcode": true,
-
"webhook": evolution_api.woofedcrm_webhooks_url,
-
"events": [
-
"QRCODE_UPDATED",
-
"MESSAGES_SET",
-
"MESSAGES_UPSERT",
-
"MESSAGES_UPDATE",
-
"MESSAGES_DELETE",
-
"SEND_MESSAGE",
-
"CONTACTS_SET",
-
"CONTACTS_UPSERT",
-
"CONTACTS_UPDATE",
-
"PRESENCE_UPDATE",
-
"CHATS_SET",
-
"CHATS_UPSERT",
-
"CHATS_UPDATE",
-
"CHATS_DELETE",
-
"GROUPS_UPSERT",
-
"GROUP_UPDATE",
-
"GROUP_PARTICIPANTS_UPDATE",
-
"CONNECTION_UPDATE",
-
"CALL"
-
]
-
}
-
end
-
-
end
-
1
class Accounts::Apps::EvolutionApis::Instance::DeleteDisconnected
-
1
def initialize(evolution_api)
-
6
@evolution_api = evolution_api
-
end
-
-
1
def call
-
6
if disconnected_or_deleted?
-
4
send_delete_instance_request
-
4
@evolution_api.update(connection_status: 'disconnected', qrcode: '', phone: '')
-
else
-
2
{ error: 'Cannot delete, instance is already active on evolution API server' }
-
end
-
end
-
-
1
def send_delete_instance_request
-
4
return unless @evolution_api_instance_found
-
-
2
request = Faraday.delete(
-
"#{@evolution_api.endpoint_url}/instance/delete/#{@evolution_api.instance}",
-
{},
-
@evolution_api.request_instance_headers
-
)
-
2
{ ok: JSON.parse(request.body) }
-
end
-
-
1
def disconnected_or_deleted?
-
6
request = Faraday.get(
-
"#{@evolution_api.endpoint_url}/instance/connectionState/#{@evolution_api.instance}",
-
{},
-
@evolution_api.request_instance_headers
-
)
-
6
@evolution_api_instance_found = request.status == 200
-
6
request_body = JSON.parse(request.body)
-
6
return true if request_body.dig('instance', 'state') == 'close' || request.status != 200
-
-
2
false
-
end
-
end
-
1
class Accounts::Apps::EvolutionApis::Instance::DeleteDisconnectedWorker
-
1
include Sidekiq::Worker
-
-
1
def perform(evolution_api_id)
-
1
evolution_api = Apps::EvolutionApi.find(evolution_api_id)
-
1
Accounts::Apps::EvolutionApis::Instance::DeleteDisconnected.new(evolution_api).call
-
end
-
end
-
# frozen_string_literal: true
-
-
1
class Accounts::Apps::EvolutionApis::Instance::SessionsRefreshStatusJob < ApplicationJob
-
1
self.queue_adapter = :good_job
-
-
1
def perform
-
1
Apps::EvolutionApi.connected.find_each do |evolution_api|
-
2
Accounts::Apps::EvolutionApis::Instance::DeleteDisconnected.new(evolution_api).call
-
end
-
end
-
end
-
1
class Accounts::Apps::EvolutionApis::Message::DeliveryJob < ApplicationJob
-
1
self.queue_adapter = :good_job
-
1
def perform(event_id)
-
9
@event = Event.find(event_id)
-
9
if @event.should_delivery_event_scheduled?
-
9
result = Accounts::Apps::EvolutionApis::Message::Send.new(@event).call
-
9
if result.key?(:ok)
-
7
@event.done = true
-
7
@event.additional_attributes.merge!({ 'message_id' => result[:ok]['key']['id'] })
-
7
@event.save!
-
7
{ ok: @event }
-
else
-
2
{ error: result[:error] }
-
end
-
end
-
end
-
end
-
1
class Accounts::Apps::EvolutionApis::Message::Import
-
1
def initialize(evolution_api, webhook, content)
-
9
@evolution_api = evolution_api
-
9
@webhook = webhook
-
9
@content = content
-
end
-
-
1
def call
-
9
result = create_evolution_api_message_event
-
9
{ ok: result }
-
end
-
-
1
def create_evolution_api_message_event
-
9
contact = find_or_create_contact
-
9
import_message(contact)
-
end
-
-
1
def find_or_create_contact
-
9
if group_message?
-
1
find_or_create_group_contact
-
else
-
8
find_or_create_person_contact
-
end
-
end
-
-
1
def find_or_create_person_contact
-
8
phone_number = '+' + @webhook['data']['key']['remoteJid'].gsub(/\D/, '')
-
8
contact = Accounts::Contacts::GetByParams.call(@evolution_api.account, { phone: phone_number })[:ok]
-
8
contact = create_person_contact(phone_number) if contact.blank?
-
8
update_contact_name_if_missing(contact)
-
8
contact
-
end
-
-
1
def update_contact_name_if_missing(contact)
-
8
if @webhook['data']['key']['fromMe'].to_s == 'false' && contact.full_name.blank?
-
1
contact.update(full_name: @webhook['data']['pushName'])
-
end
-
end
-
-
1
def find_or_create_group_contact
-
1
group_id = @webhook['data']['key']['remoteJid']
-
1
group_details = group_details(group_id)
-
contact_params = {
-
1
full_name: "#{group_details[:group_name]} - Grupo",
-
additional_attributes: group_details
-
}
-
-
1
contact = @evolution_api.account.contacts.where('additional_attributes @> ?', { group_id: group_id }.to_json).first
-
1
if contact.present?
-
1
contact.full_name = contact_params[:full_name]
-
1
contact.additional_attributes = contact.additional_attributes.merge(contact_params[:additional_attributes])
-
else
-
contact = ContactBuilder.new(
-
@evolution_api.account.users.first,
-
ActionController::Parameters.new(contact_params)
-
).perform
-
end
-
-
1
contact.save!
-
1
contact
-
end
-
-
1
def create_person_contact(phone_number)
-
2
if @webhook['data']['key']['fromMe'].to_s == 'true'
-
1
Contact.create(phone: phone_number,
-
account: @evolution_api.account)
-
else
-
1
Contact.create(full_name: @webhook['data']['pushName'], phone: phone_number,
-
account: @evolution_api.account)
-
end
-
end
-
-
1
def group_details(group_id)
-
1
request = Faraday.get(
-
"#{@evolution_api.endpoint_url}/group/findGroupInfos/#{@evolution_api.instance}?groupJid=#{group_id}",
-
{},
-
@evolution_api.request_instance_headers
-
)
-
1
request_body = JSON.parse(request.body)
-
-
1
{ group_id: group_id,
-
group_name: request_body['subject'],
-
group_owner_id: request_body['subjectOwner'] }
-
end
-
-
1
def group_message?
-
9
@webhook['data']['key']['remoteJid'].gsub(/\D/, '').size > 15
-
end
-
-
1
def import_message(contact)
-
9
Event.create(
-
account: @evolution_api.account,
-
kind: 'evolution_api_message',
-
from_me: @webhook['data']['key']['fromMe'],
-
contact: contact,
-
content: @content,
-
done: true,
-
done_at: @webhook['date_time'],
-
app: @evolution_api,
-
additional_attributes: { message_id: @webhook['data']['key']['id'] }
-
)
-
end
-
end
-
1
class Accounts::Apps::EvolutionApis::Message::Send
-
1
def initialize(event)
-
9
@event = event
-
9
@evolution_api = event.app
-
9
@phone = sending_to_group? ? event.contact.additional_attributes['group_id'] : @event.contact.phone
-
end
-
-
1
def call
-
9
if @event.attachment.present?
-
3
send_message_with_attachment
-
else
-
6
send_request('sendText', build_message_text_body)
-
end
-
end
-
-
1
def send_message_with_attachment
-
3
if @event.attachment.audio?
-
1
send_request('sendWhatsAppAudio', build_message_audio_body)
-
else
-
2
send_request('sendMedia', build_message_file_body)
-
end
-
end
-
-
1
def send_request(type, body)
-
9
request = Faraday.post(
-
"#{@evolution_api.endpoint_url}/message/#{type}/#{@evolution_api.instance}",
-
body.to_json,
-
@evolution_api.request_instance_headers
-
)
-
9
if request.status == 201
-
7
{ ok: JSON.parse(request.body) }
-
-
else
-
2
{ error: JSON.parse(request.body) }
-
end
-
end
-
-
1
def build_message_file_body
-
2
file_media_type = if @event.attachment.image? || @event.attachment.video?
-
1
@event.attachment.file_type
-
else
-
1
'document'
-
end
-
{
-
2
"number": normalize_phone,
-
"options": {
-
"delay": 1200,
-
"presence": 'composing',
-
"linkPreview": false
-
},
-
"mediaMessage": {
-
"mediatype": file_media_type,
-
"caption": @event.generate_content_hash('content', @event.content)['content'],
-
"media": @event.attachment.download_url
-
}
-
}
-
end
-
-
1
def build_message_audio_body
-
{
-
1
"number": normalize_phone,
-
"options": {
-
"delay": 1200,
-
"presence": 'recording',
-
"linkPreview": false
-
},
-
"audioMessage": {
-
"audio": @event.attachment.download_url
-
}
-
}
-
end
-
-
1
def build_message_text_body
-
{
-
6
"number": normalize_phone,
-
"options": {
-
"delay": 1200,
-
"presence": 'composing',
-
"linkPreview": false
-
},
-
"textMessage": {
-
"text": @event.content
-
}
-
}
-
end
-
-
1
def normalize_phone
-
9
@phone.sub(/^\+/, '')
-
end
-
-
1
def sending_to_group?
-
9
@event.contact.additional_attributes['group_id'].present?
-
end
-
end
-
1
class Accounts::Apps::EvolutionApis::Webhooks::Events::ConnectionCreated
-
1
def self.call(evolution_api, phone)
-
1
response = update_evolution_api(evolution_api, phone)
-
1
{ ok: response }
-
end
-
-
1
def self.update_evolution_api(evolution_api, phone)
-
1
evolution_api.connection_status = 'connected'
-
1
evolution_api.phone = "+#{phone}"
-
1
evolution_api.qrcode = ''
-
1
evolution_api.save
-
1
evolution_api
-
end
-
end
-
1
class Accounts::Apps::EvolutionApis::Webhooks::Events::ConnectionDeleted
-
1
def self.call(evolution_api)
-
3
if evolution_api.connected?
-
1
Accounts::Apps::EvolutionApis::Instance::DeleteDisconnectedWorker.perform_in(1.seconds, evolution_api.id)
-
end
-
3
Accounts::Apps::EvolutionApis::Instance::DeleteDisconnected.new(evolution_api).call if evolution_api.connecting?
-
end
-
end
-
1
class Accounts::Apps::EvolutionApis::Webhooks::Events::ImportMessage
-
1
def self.call(evolution_api, webhook, content)
-
11
if evolution_api.connected?
-
9
response = Accounts::Apps::EvolutionApis::Message::Import.new(evolution_api, webhook, content).call
-
9
{ ok: response }
-
end
-
end
-
end
-
1
class Accounts::Apps::EvolutionApis::Webhooks::Events::QrcodeConnectRefresh
-
1
def self.call(evolution_api, qrcode)
-
3
if evolution_api.connecting?
-
1
evolution_api.update(qrcode: qrcode)
-
1
{ ok: evolution_api }
-
end
-
end
-
end
-
1
class Accounts::Apps::EvolutionApis::Webhooks::ProcessWebhook
-
1
def self.call(webhook)
-
18
evolution_api = Apps::EvolutionApi.find_by(instance: webhook['instance'])
-
18
if webhook['event'] == 'qrcode.updated'
-
3
Accounts::Apps::EvolutionApis::Webhooks::Events::QrcodeConnectRefresh.call(
-
evolution_api, webhook['data']['qrcode']['base64']
-
)
-
15
elsif webhook['event'] == 'connection.update'
-
4
if connection_created?(webhook)
-
1
Accounts::Apps::EvolutionApis::Webhooks::Events::ConnectionCreated.call(evolution_api,
-
webhook['sender'].gsub(/\D/, ''))
-
-
3
elsif connection_deleted?(webhook)
-
3
Accounts::Apps::EvolutionApis::Webhooks::Events::ConnectionDeleted.call(evolution_api)
-
end
-
11
elsif webhook['event'] == 'messages.upsert'
-
11
if webhook['data']['messageType'] == 'extendedTextMessage'
-
8
Accounts::Apps::EvolutionApis::Webhooks::Events::ImportMessage.call(evolution_api, webhook,
-
webhook['data']['message']['extendedTextMessage']['text'])
-
3
elsif webhook['data']['messageType'] == 'conversation'
-
3
Accounts::Apps::EvolutionApis::Webhooks::Events::ImportMessage.call(evolution_api, webhook,
-
webhook['data']['message']['conversation'])
-
end
-
end
-
18
{ ok: evolution_api }
-
end
-
-
1
def self.connection_created?(webhook)
-
4
webhook['data']['statusReason'].to_i == 200 && webhook['data']['state'] == 'open'
-
end
-
-
1
def self.connection_deleted?(webhook)
-
3
(webhook['data']['statusReason'].to_i == 401 || webhook['data']['statusReason'].to_i == 428) && webhook['data']['state'] == 'close'
-
end
-
end
-
1
class Accounts::Apps::EvolutionApis::Webhooks::ProcessWebhookWorker
-
1
include Sidekiq::Worker
-
-
1
sidekiq_options queue: :evolution_api_webhooks
-
-
1
def perform(event)
-
18
event_hash = JSON.parse(event)
-
18
Accounts::Apps::EvolutionApis::Webhooks::ProcessWebhook.call(event_hash)
-
end
-
end
-
1
class Accounts::Contacts::Events::Enqueue
-
-
1
def self.call(event)
-
2
if event.chatwoot_message?
-
1
Accounts::Apps::Chatwoots::Messages::DeliveryJob.set(wait_until: event.scheduled_at).perform_later(event.id)
-
1
elsif event.evolution_api_message?
-
1
Accounts::Apps::EvolutionApis::Message::DeliveryJob.set(wait_until: event.scheduled_at).perform_later(event.id)
-
end
-
end
-
end
-
1
class Accounts::Contacts::Events::EnqueueWorker
-
1
include Sidekiq::Worker
-
-
1
def perform(event_id)
-
2
event = Event.find(event_id)
-
2
Accounts::Contacts::Events::Enqueue.call(event)
-
end
-
end
-
1
class Accounts::Contacts::Events::GenerateAiResponse
-
1
def initialize(event)
-
4
@event = event
-
4
@account = event.account
-
4
@ai_assistent = Apps::AiAssistent.first
-
end
-
-
1
def call
-
4
return '' if @ai_assistent.exceeded_usage_limit?
-
-
3
question = @event.content.to_s
-
3
context = get_context(question)
-
3
data = prepare_data(context, question)
-
3
response = post_request(data)
-
3
response_body = JSON.parse(response.body)
-
3
update_ai_usage(response_body['usage']['total_tokens'])
-
3
content = response_body.dig('output', 0, 'content', 0, 'text')
-
3
JSON.parse(content)['response']
-
rescue StandardError
-
''
-
end
-
-
1
def update_ai_usage(tokens)
-
3
@ai_assistent.usage['tokens'] += tokens
-
3
@ai_assistent.save
-
end
-
-
1
def get_context(query)
-
3
embedding = OpenAi::Embeddings.new.get_embedding(@ai_assistent, query, 'text-embedding-3-small')
-
3
documents = EmbeddingDocumment.nearest_neighbors(:embedding, embedding, distance: 'cosine').first(6)
-
3
documents.pluck(:content, :source_reference)
-
end
-
-
1
def post_request(data)
-
3
Rails.logger.info "Requesting Chat GPT with body: #{data}"
-
3
response = Faraday.post(
-
'https://api.openai.com/v1/responses',
-
data.to_json,
-
headers
-
)
-
3
Rails.logger.info "Chat GPT response: #{response.body}"
-
3
response
-
end
-
-
1
def headers
-
{
-
3
'Content-Type' => 'application/json',
-
'Authorization' => "Bearer #{@ai_assistent.api_key}"
-
}
-
end
-
-
1
def prepare_data(context, question)
-
{
-
3
model: @ai_assistent.model,
-
input: build_prompt(context, question),
-
text: response_format,
-
max_output_tokens: 2048,
-
temperature: 0.3,
-
}
-
end
-
-
1
def response_format
-
{
-
3
format: {
-
type: 'json_schema',
-
name: 'suggestion',
-
schema: {
-
type: 'object',
-
properties: {
-
response: {
-
type: 'string'
-
},
-
confidence: {
-
type: 'integer'
-
}
-
},
-
required: %w[response confidence],
-
additionalProperties: false
-
},
-
strict: true
-
}
-
}
-
end
-
-
1
def build_prompt(context, question)
-
3
system_prompt_message = <<~SYSTEM_PROMPT_MESSAGE
-
You are an assistant that will help answer questions from potential customers.
-
Only respond if you are 100% certain; otherwise, your response should be left blank.
-
If it is relevant to the response, include the link to the page where the information was found so the user can obtain more details.
-
Respond in the language the customer used to ask the question.
-
Never make up information.
-
Respond in a short and objective manner, always in plain text, without Markdown formatting, without lists, without bold text, without formatted code, and without special symbols.
-
SYSTEM_PROMPT_MESSAGE
-
-
3
user_prompt_message = <<~USER_PROMPT_MESSAGE
-
Context sections:
-
#{context}
-
-
Question:
-
#{question}
-
USER_PROMPT_MESSAGE
-
-
[
-
{
-
3
role: 'system',
-
content: system_prompt_message
-
},
-
{
-
role: 'user',
-
content: user_prompt_message
-
}
-
]
-
end
-
end
-
1
class Accounts::Contacts::Events::SendNow
-
1
def self.call(event)
-
10
event.send_now = nil
-
10
if event.chatwoot_message? || event.evolution_api_message?
-
7
event.update(scheduled_at: DateTime.current, auto_done: false)
-
7
if event.chatwoot_message?
-
4
Accounts::Apps::Chatwoots::Messages::DeliveryJob.perform_later(event.id)
-
3
elsif event.evolution_api_message?
-
3
Accounts::Apps::EvolutionApis::Message::DeliveryJob.perform_later(event.id)
-
end
-
else
-
3
event.update(done: true)
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
class Accounts::Contacts::Events::Woofbot
-
1
def initialize(event)
-
1
@event = event
-
1
@account = event.account
-
1
@ai_assistent = Apps::AiAssistent.first
-
end
-
-
1
def call
-
1
return unless woofbot_should_be_run?
-
-
1
@woofbot_response = Accounts::Contacts::Events::GenerateAiResponse.new(@event).call
-
1
create_reply_event if @woofbot_response.present?
-
end
-
-
1
def create_reply_event
-
event_params = {
-
1
kind: @event.kind,
-
contact_id: @event.contact_id,
-
app_type: @event.app_type,
-
app_id: @event.app_id,
-
from_me: true,
-
send_now: true,
-
content: "#{@woofbot_response}\n\n🤖 Mensagem automática"
-
}
-
-
1
event_params.merge!({ deal_id: @event.deal_id }) if @event.deal_id.present?
-
-
1
@response_event = EventBuilder.new(@account.users.first,
-
event_params).build
-
-
1
@response_event.save
-
1
@response_event
-
end
-
-
1
def woofbot_should_be_run?
-
1
@event.from_me == false && woofbot_active? && event_is_question?
-
end
-
-
1
def event_is_question?
-
1
@event.content.to_s.include?('?')
-
end
-
-
1
def woofbot_active?
-
1
@account.site_url.present? && @ai_assistent.auto_reply
-
end
-
end
-
# frozen_string_literal: true
-
-
1
class Accounts::Contacts::Events::WoofbotWorker
-
1
include Sidekiq::Worker
-
-
1
def perform(event_id)
-
event = Event.find(event_id)
-
Accounts::Contacts::Events::Woofbot.new(event).call
-
end
-
end
-
1
class Accounts::Contacts::GetByParams
-
1
def self.call(account, params)
-
47
params.stringify_keys!
-
47
return { error: 'Not found' } if params.blank?
-
-
125
params.reject! { |_key, value| value.blank? }
-
-
46
params = params.slice('email', 'phone', 'identifier')
-
-
46
query_params = build_query_conditions(params)
-
46
if params.key?('phone')
-
22
query_params << "phone ILIKE '%#{sanitized_phone(params['phone'])}%'"
-
22
query_params << "phone ILIKE '%#{phone_with_9_digit(params['phone'])}%'"
-
22
query_params << "phone ILIKE '%#{phone_number_without_9_digit(params['phone'])}%'"
-
end
-
46
contact = account.contacts.where(query_params.join(' OR ')).first if query_params.present?
-
46
{ ok: contact }
-
end
-
-
1
def self.build_query_conditions(params)
-
46
params.map do |field, value|
-
54
case field
-
when 'identifier'
-
3
"additional_attributes ->> 'chatwoot_identifier' = '#{value}'"
-
else
-
51
"#{field} ILIKE '%#{value}%'"
-
end
-
end
-
end
-
-
1
def self.phone_number_without_9_digit(phone)
-
22
sanitized_phone = sanitized_phone(phone)
-
-
22
if sanitized_phone.size == 13
-
8
sanitized_phone
-
else
-
14
"#{sanitized_phone[0..4]}#{sanitized_phone[6..-1]}"
-
-
end
-
end
-
-
1
def self.phone_with_9_digit(phone)
-
22
sanitized_phone = sanitized_phone(phone)
-
22
if sanitized_phone.size >= 14
-
10
sanitized_phone
-
else
-
12
"#{sanitized_phone[0..4]}9#{sanitized_phone[5..-1]}"
-
-
end
-
end
-
-
1
def self.sanitized_phone(phone_number)
-
66
raise TypeError, 'phone_number must be a String' unless phone_number.is_a?(String)
-
-
66
cleaned_phone_number = phone_number.gsub(/\D/, '')
-
66
cleaned_phone_number.prepend('+')
-
66
cleaned_phone_number
-
end
-
end
-
1
class Accounts::Create::EmbedCompanySiteJob < ApplicationJob
-
1
self.queue_adapter = :good_job
-
-
1
def perform(account_id)
-
account = Account.find(account_id)
-
Accounts::Create::EmbededCompanySite.new(account).call
-
end
-
end
-
1
class Accounts::Create::EmbededCompanySite
-
1
def initialize(account)
-
1
@account = account
-
1
@ai_assistent = Apps::AiAssistent.first
-
1
@start_url = @account.site_url
-
1
@start_url_host = URI.parse(@start_url).host
-
end
-
-
1
def call(max_pages = 100)
-
1
crawl_website(@start_url, max_pages)
-
end
-
-
1
private
-
-
1
def clean_data
-
@account.embedding_documments.where(source: @account).destroy_all
-
end
-
-
1
def crawl_website(start_url, max_pages)
-
1
visited = []
-
1
queue = [start_url]
-
-
1
pages_visited = 0
-
-
1
while !queue.empty? && pages_visited < max_pages
-
3
current_url = queue.shift
-
3
next if visited.include?(current_url)
-
3
visited.push(current_url)
-
-
begin
-
3
page = Accounts::Create::PageCrawler.new(current_url)
-
3
next unless page.valid_page?
-
-
3
embed_page(page)
-
3
links = filter_site_subpages(page.page_links) - visited
-
-
3
links.each do |link|
-
12
queue << link
-
end
-
-
3
pages_visited += 1
-
rescue => e
-
puts "Failed to fetch #{current_url}: #{e.message}"
-
end
-
end
-
-
1
visited
-
end
-
-
1
def embed_page(page)
-
3
splitter = ::TextSplitters::RecursiveCharacterTextSplitter.new(chunk_size: 1000, chunk_overlap: 100)
-
3
page_filter_links = page.body_text_content.gsub(/\[(.*?)\]\(.*?\)/m, '')
-
3
output = splitter.split(page_filter_links)
-
-
3
output.each do |content_split|
-
3
@account.embedding_documments.create(
-
source_reference: page.page_link,
-
source: @account,
-
content: content_split,
-
embedding: OpenAi::Embeddings.new.get_embedding(@ai_assistent, content_split, 'text-embedding-3-small')
-
)
-
end
-
end
-
-
1
def filter_site_subpages(links)
-
3
links.filter do |link|
-
34
link.include?(@start_url_host)
-
end
-
end
-
end
-
1
require 'faraday/follow_redirects'
-
-
1
class Accounts::Create::PageCrawler
-
1
attr_reader :page_link
-
-
1
def initialize(page_link)
-
3
@page_link = page_link
-
-
3
conn = Faraday.new() do |faraday|
-
3
faraday.response :follow_redirects
-
3
faraday.adapter Faraday.default_adapter
-
end
-
-
3
@response = conn.get(page_link)
-
3
@doc = Nokogiri::HTML(@response.body)
-
end
-
-
1
def valid_page?
-
3
@response.status == 200 && @doc.at_xpath('//body').present?
-
end
-
-
1
def page_links
-
3
sitemap? ? extract_links_from_sitemap : extract_links_from_html
-
end
-
-
1
def page_title
-
title_element = @doc.at_xpath('//title')
-
title_element&.text&.strip
-
end
-
-
1
def body_text_content
-
3
ReverseMarkdown.convert @doc.at_xpath('//body'), unknown_tags: :bypass, github_flavored: true
-
end
-
-
1
private
-
-
1
def sitemap?
-
3
@page_link.end_with?('.xml')
-
end
-
-
1
def extract_links_from_sitemap
-
@doc.xpath('//loc').to_set(&:text)
-
end
-
-
1
def extract_links_from_html
-
3
@doc.xpath('//a/@href').to_set do |link|
-
55
absolute_url = URI.join(@page_link, URI::Parser.new.escape(link.value)).to_s
-
55
absolute_url
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
class OpenAi::Embeddings
-
1
def get_embedding(ai_assistent, content, model = 'text-embedding-ada-002')
-
6
fetch_embeddings(ai_assistent, content, model)
-
end
-
-
1
private
-
-
1
def fetch_embeddings(ai_assistent, input, model)
-
6
url = 'https://api.openai.com/v1/embeddings'
-
headers = {
-
6
'Authorization' => "Bearer #{ai_assistent.api_key}",
-
'Content-Type' => 'application/json'
-
}
-
data = {
-
6
input: input,
-
model: model
-
}
-
-
6
response = Net::HTTP.post(URI(url), data.to_json, headers)
-
6
JSON.parse(response.body)['data']&.pick('embedding')
-
end
-
end
-
1
class Pwa::SendNotificationsWorker < ApplicationJob
-
1
self.queue_adapter = :good_job
-
-
1
def perform(event_id)
-
73
event = Event.find(event_id)
-
73
if event.present? && event.should_delivery_event_scheduled?
-
50
WebpushSubscription.find_each do |subscription|
-
2
if subscription.user.webpush_notify_on_event_expired
-
2
subscription.send_notification(
-
{
-
title: "#{Event.human_enum_name(:kind, event.kind)} #{event.title}",
-
body: I18n.t('use_cases.pwa.send_notifications_worker.body',
-
event_kind: Event.human_enum_name(:kind, event.kind), event_title: event.title, deal_name: event.deal.name),
-
icon: ActionController::Base.helpers.image_url('logo-patinha.svg'),
-
url: Rails.application.routes.url_helpers.account_deal_url(Current.account, event.deal)
-
}
-
)
-
end
-
end
-
end
-
end
-
end
-
1
class Users::JsonWebToken
-
1
SECRET_KEY = Rails.application.secrets.secret_key_base.to_s
-
-
1
def self.encode_user(user)
-
70
hmac_secret = SECRET_KEY
-
70
JWT.encode({ sub: user.id }, hmac_secret)
-
end
-
-
1
def self.decode_user(token)
-
begin
-
86
decoded = JWT.decode(token, SECRET_KEY)[0]
-
63
user = User.find(decoded["sub"])
-
63
return { ok: user }
-
rescue => e
-
23
return { error: e }
-
end
-
end
-
end
-
1
class WebhookWorker
-
1
include Sidekiq::Worker
-
-
1
def perform(url, payload)
-
Faraday.post(
-
url,
-
payload,
-
{'Content-Type': 'application/json'}
-
)
-
end
-
end