feat: Add bulk imports API for contacts (#1724)
This commit is contained in:
@@ -1,21 +1,14 @@
|
||||
class Api::V1::Accounts::Contacts::ContactInboxesController < Api::V1::Accounts::BaseController
|
||||
before_action :ensure_contact
|
||||
before_action :ensure_inbox, only: [:create]
|
||||
before_action :validate_channel_type
|
||||
|
||||
def create
|
||||
source_id = params[:source_id] || SecureRandom.uuid
|
||||
@contact_inbox = ContactInbox.create(contact: @contact, inbox: @inbox, source_id: source_id)
|
||||
@contact_inbox = ContactInbox.create!(contact: @contact, inbox: @inbox, source_id: source_id)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def validate_channel_type
|
||||
return if @inbox.channel_type == 'Channel::Api'
|
||||
|
||||
render json: { error: 'Contact Inbox creation is only allowed in API inboxes' }, status: :unprocessable_entity
|
||||
end
|
||||
|
||||
def ensure_inbox
|
||||
@inbox = Current.account.inboxes.find(params[:inbox_id])
|
||||
end
|
||||
|
||||
@@ -22,6 +22,14 @@ class Api::V1::Accounts::ContactsController < Api::V1::Accounts::BaseController
|
||||
@contacts = fetch_contact_last_seen_at(contacts)
|
||||
end
|
||||
|
||||
def import
|
||||
ActiveRecord::Base.transaction do
|
||||
import = Current.account.data_imports.create!(data_type: 'contacts')
|
||||
import.import_file.attach(params[:import_file])
|
||||
end
|
||||
head :ok
|
||||
end
|
||||
|
||||
# returns online contacts
|
||||
def active
|
||||
contacts = Current.account.contacts.where(id: ::OnlineStatusTracker
|
||||
|
||||
46
app/jobs/data_import_job.rb
Normal file
46
app/jobs/data_import_job.rb
Normal file
@@ -0,0 +1,46 @@
|
||||
# TODO: logic is written tailored to contact import since its the only import available
|
||||
# let's break this logic and clean this up in future
|
||||
|
||||
class DataImportJob < ApplicationJob
|
||||
queue_as :low
|
||||
|
||||
def perform(data_import)
|
||||
contacts = []
|
||||
data_import.update!(status: :processing)
|
||||
csv = CSV.parse(data_import.import_file.download, headers: true)
|
||||
csv.each { |row| contacts << build_contact(row.to_h.with_indifferent_access, data_import.account) }
|
||||
result = Contact.import contacts, on_duplicate_key_update: :all, batch_size: 1000
|
||||
data_import.update!(status: :completed, processed_records: csv.length - result.failed_instances.length, total_records: csv.length)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def build_contact(params, account)
|
||||
# TODO: rather than doing the find or initialize individually lets fetch objects in bulk and update them in memory
|
||||
contact = init_contact(params, account)
|
||||
|
||||
contact.name = params[:name] if params[:name].present?
|
||||
contact.assign_attributes(custom_attributes: contact.custom_attributes.merge(params.except(:identifier, :email, :name)))
|
||||
|
||||
# since callbacks aren't triggered lets ensure a pubsub token
|
||||
contact.pubsub_token ||= SecureRandom.base58(24)
|
||||
contact
|
||||
end
|
||||
|
||||
def get_identified_contacts(params, account)
|
||||
identifier_contact = account.contacts.find_by(identifier: params[:identifier]) if params[:identifier]
|
||||
email_contact = account.contacts.find_by(email: params[:email]) if params[:email]
|
||||
[identifier_contact, email_contact]
|
||||
end
|
||||
|
||||
def init_contact(params, account)
|
||||
identifier_contact, email_contact = get_identified_contacts(params, account)
|
||||
|
||||
# intiating the new contact / contact attributes only by ensuring the identifier or email duplication errors won't occur
|
||||
contact = identifier_contact
|
||||
contact&.email = params[:email] if params[:email].present? && email_contact.blank?
|
||||
contact ||= email_contact
|
||||
contact ||= account.contacts.new(params.slice(:email, :identifier))
|
||||
contact
|
||||
end
|
||||
end
|
||||
@@ -34,6 +34,7 @@ class Account < ApplicationRecord
|
||||
|
||||
has_many :account_users, dependent: :destroy
|
||||
has_many :agent_bot_inboxes, dependent: :destroy
|
||||
has_many :data_imports, dependent: :destroy
|
||||
has_many :users, through: :account_users
|
||||
has_many :inboxes, dependent: :destroy
|
||||
has_many :conversations, dependent: :destroy
|
||||
|
||||
@@ -27,6 +27,7 @@ class ContactInbox < ApplicationRecord
|
||||
validates :inbox_id, presence: true
|
||||
validates :contact_id, presence: true
|
||||
validates :source_id, presence: true
|
||||
validate :valid_source_id_format?
|
||||
|
||||
belongs_to :contact
|
||||
belongs_to :inbox
|
||||
@@ -47,4 +48,24 @@ class ContactInbox < ApplicationRecord
|
||||
def current_conversation
|
||||
conversations.last
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def validate_twilio_source_id
|
||||
# https://www.twilio.com/docs/glossary/what-e164#regex-matching-for-e164
|
||||
if inbox.channel.medium == 'sms' && !/^\+[1-9]\d{1,14}$/.match?(source_id)
|
||||
errors.add(:source_id, 'invalid source id for twilio sms inbox. valid Regex /^\+[1-9]\d{1,14}$/')
|
||||
elsif inbox.channel.medium == 'whatsapp' && !/^whatsapp:\+[1-9]\d{1,14}$/.match?(source_id)
|
||||
errors.add(:source_id, 'invalid source id for twilio whatsapp inbox. valid Regex /^whatsapp:\+[1-9]\d{1,14}$/')
|
||||
end
|
||||
end
|
||||
|
||||
def validate_email_source_id
|
||||
errors.add(:source_id, "invalid source id for Email inbox. valid Regex #{Device.email_regexp}") unless Devise.email_regexp.match?(source_id)
|
||||
end
|
||||
|
||||
def valid_source_id_format?
|
||||
validate_twilio_source_id if inbox.channel_type == 'Channel::TwilioSms'
|
||||
validate_email_source_id if inbox.channel_type == 'Channel::Email'
|
||||
end
|
||||
end
|
||||
|
||||
37
app/models/data_import.rb
Normal file
37
app/models/data_import.rb
Normal file
@@ -0,0 +1,37 @@
|
||||
# == Schema Information
|
||||
#
|
||||
# Table name: data_imports
|
||||
#
|
||||
# id :bigint not null, primary key
|
||||
# data_type :string not null
|
||||
# processed_records :integer
|
||||
# processing_errors :text
|
||||
# status :integer default("pending"), not null
|
||||
# total_records :integer
|
||||
# created_at :datetime not null
|
||||
# updated_at :datetime not null
|
||||
# account_id :bigint not null
|
||||
#
|
||||
# Indexes
|
||||
#
|
||||
# index_data_imports_on_account_id (account_id)
|
||||
#
|
||||
# Foreign Keys
|
||||
#
|
||||
# fk_rails_... (account_id => accounts.id)
|
||||
#
|
||||
class DataImport < ApplicationRecord
|
||||
belongs_to :account
|
||||
validates :data_type, inclusion: { in: ['contacts'], message: '%<value>s is an invalid data type' }
|
||||
enum status: { pending: 0, processing: 1, completed: 2, failed: 3 }
|
||||
|
||||
has_one_attached :import_file
|
||||
|
||||
after_create_commit :process_data_import
|
||||
|
||||
private
|
||||
|
||||
def process_data_import
|
||||
DataImportJob.perform_later(self)
|
||||
end
|
||||
end
|
||||
@@ -7,6 +7,10 @@ class ContactPolicy < ApplicationPolicy
|
||||
true
|
||||
end
|
||||
|
||||
def import?
|
||||
@account_user.administrator?
|
||||
end
|
||||
|
||||
def search?
|
||||
true
|
||||
end
|
||||
|
||||
Reference in New Issue
Block a user