diff --git a/Gemfile b/Gemfile index 798f85deb..ddb0fb338 100644 --- a/Gemfile +++ b/Gemfile @@ -32,6 +32,8 @@ gem 'omniauth-facebook' gem 'omniauth-linkedin' gem 'omniauth-google-oauth2' +gem 'zendesk_api' + gem 'twitter' gem 'koala' gem 'mail', '~> 2.5.0' diff --git a/Gemfile.lock b/Gemfile.lock index f2c446bfa..b9cf4bbd4 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -130,6 +130,7 @@ GEM http_parser.rb (0.6.0) i18n (0.7.0) icalendar (2.3.0) + inflection (1.0.0) json (1.8.3) jwt (1.5.2) koala (2.2.0) @@ -259,6 +260,7 @@ GEM sprockets (>= 2.8, < 4.0) sprockets-rails (>= 2.0, < 4.0) tilt (>= 1.1, < 3) + scrub_rb (1.0.1) selenium-webdriver (2.48.1) childprocess (~> 0.5) multi_json (~> 1.0) @@ -317,6 +319,13 @@ GEM unf_ext (0.0.7.1) websocket (1.2.2) writeexcel (1.0.5) + zendesk_api (1.13.1) + faraday (~> 0.9) + hashie (>= 1.2, < 4.0, != 3.3.0) + inflection + mime-types + multipart-post (~> 2.0) + scrub_rb (~> 1.0.1) PLATFORMS ruby @@ -371,6 +380,7 @@ DEPENDENCIES twitter uglifier writeexcel + zendesk_api BUNDLED WITH - 1.10.6 + 1.11.0 diff --git a/db/migrate/20151217191239_zendesk_import_settings.rb b/db/migrate/20151217191239_zendesk_import_settings.rb new file mode 100644 index 000000000..c22a87e09 --- /dev/null +++ b/db/migrate/20151217191239_zendesk_import_settings.rb @@ -0,0 +1,61 @@ +class ZendeskImportSettings < ActiveRecord::Migration + def change + + Setting.create_if_not_exists( + title: 'Import Endpoint', + name: 'import_zendesk_endpoint', + area: 'Import::Zendesk', + description: 'Defines Zendesk endpoint to import users, ticket, states and articles.', + options: { + form: [ + { + display: '', + null: false, + name: 'import_zendesk_endpoint', + tag: 'input', + }, + ], + }, + state: 'https://yours.zendesk.com/api/v2', + frontend: false + ) + + Setting.create_if_not_exists( + title: 'Import Key for requesting the Zendesk API', + name: 'import_zendesk_endpoint_key', + area: 'Import::Zendesk', + description: 'Defines Zendesk endpoint auth key.', + options: { + form: [ + { + display: '', + null: false, + name: 'import_zendesk_endpoint_key', + tag: 'input', + }, + ], + }, + state: '', + frontend: false + ) + + Setting.create_if_not_exists( + title: 'Import User for requesting the Zendesk API', + name: 'import_zendesk_endpoint_username', + area: 'Import::Zendesk', + description: 'Defines Zendesk endpoint auth key.', + options: { + form: [ + { + display: '', + null: true, + name: 'import_zendesk_endpoint_username', + tag: 'input', + }, + ], + }, + state: '', + frontend: false + ) + end +end diff --git a/db/seeds.rb b/db/seeds.rb index 13eace9bf..7806788f0 100644 --- a/db/seeds.rb +++ b/db/seeds.rb @@ -1381,10 +1381,10 @@ Setting.create_if_not_exists( ) Setting.create_if_not_exists( - title: 'Import User for http basic authentiation', + title: 'Import User for http basic authentication', name: 'import_otrs_user', area: 'Import::OTRS', - description: 'Defines http basic authentiation user (only if OTRS is protected via http basic auth).', + description: 'Defines http basic authentication user (only if OTRS is protected via http basic auth).', options: { form: [ { @@ -1400,10 +1400,10 @@ Setting.create_if_not_exists( ) Setting.create_if_not_exists( - title: 'Import Password for http basic authentiation', + title: 'Import Password for http basic authentication', name: 'import_otrs_password', area: 'Import::OTRS', - description: 'Defines http basic authentiation password (only if OTRS is protected via http basic auth).', + description: 'Defines http basic authentication password (only if OTRS is protected via http basic auth).', options: { form: [ { @@ -1418,6 +1418,62 @@ Setting.create_if_not_exists( frontend: false ) +Setting.create_if_not_exists( + title: 'Import Endpoint', + name: 'import_zendesk_endpoint', + area: 'Import::Zendesk', + description: 'Defines Zendesk endpoint to import users, ticket, states and articles.', + options: { + form: [ + { + display: '', + null: false, + name: 'import_zendesk_endpoint', + tag: 'input', + }, + ], + }, + state: 'https://yours.zendesk.com/api/v2', + frontend: false +) +Setting.create_if_not_exists( + title: 'Import Key for requesting the Zendesk API', + name: 'import_zendesk_endpoint_key', + area: 'Import::Zendesk', + description: 'Defines Zendesk endpoint auth key.', + options: { + form: [ + { + display: '', + null: false, + name: 'import_zendesk_endpoint_key', + tag: 'input', + }, + ], + }, + state: '', + frontend: false +) + +Setting.create_if_not_exists( + title: 'Import User for requesting the Zendesk API', + name: 'import_zendesk_endpoint_username', + area: 'Import::Zendesk', + description: 'Defines Zendesk endpoint auth key.', + options: { + form: [ + { + display: '', + null: true, + name: 'import_zendesk_endpoint_username', + tag: 'input', + }, + ], + }, + state: '', + frontend: false +) + Setting.create_if_not_exists( title: 'Default calendar Tickets subscriptions', name: 'defaults_calendar_subscriptions_tickets', diff --git a/lib/import/zendesk.rb b/lib/import/zendesk.rb new file mode 100644 index 000000000..48c99ce7c --- /dev/null +++ b/lib/import/zendesk.rb @@ -0,0 +1,907 @@ +require 'base64' +require 'zendesk_api' + +module Import +end +module Import::Zendesk + + module_function + + def start + Rails.logger.info 'Start import...' + + # check if system is in import mode + if !Setting.get('import_mode') + fail 'System is not in import mode!' + end + + initialize_client + + import_fields + + # TODO + # import_oauth + # import_twitter_channel + + import_groups + + import_organizations + + import_users + + import_tickets + + # TODO + # import_sla_policies + + # import_macros + + # import_schedules + + # import_views + + # import_automations + + Setting.set( 'system_init_done', true ) + Setting.set( 'import_mode', false ) + + true + end + + def statistic + + # check cache + cache = Cache.get('import_zendesk_stats') + if cache + return cache + end + + initialize_client + + # retrive statistic + statistic = { + 'Tickets' => 0, + 'TicketFields' => 0, + 'UserFields' => 0, + 'OrganizationFields' => 0, + 'Groups' => 0, + 'Organizations' => 0, + 'Users' => 0, + 'GroupMemberships' => 0, + 'Macros' => 0, + 'Views' => 0, + 'Automations' => 0, + } + + statistic.each { |object, _score| + + counter = 0 + @client.send( object.underscore.to_sym ).all do |_resource| + counter += 1 + end + + statistic[ object ] = counter + } + + if statistic + Cache.write('import_zendesk_stats', statistic) + end + statistic + end + + private + + def initialize_client + return nil if @client + + @client = ZendeskAPI::Client.new do |config| + config.url = Setting.get('import_zendesk_endpoint') + + # Basic / Token Authentication + config.username = Setting.get('import_zendesk_endpoint_username') + config.token = Setting.get('import_zendesk_endpoint_key') + + # when hitting the rate limit, sleep automatically, + # then retry the request. + config.retry = true + end + end + + def mapping_state(zendesk_state) + + mapping = { + 'pending' => 'pending reminder', + 'solved' => 'closed', + } + return zendesk_state if !mapping[zendesk_state] + mapping[zendesk_state] + end + + def mapping_priority(zendesk_priority) + + mapping = { + 'low' => '1 low', + nil => '2 normal', + 'normal' => '2 normal', + 'high' => '3 high', + 'urgent' => '3 high', + } + mapping[zendesk_priority] + end + + # NOT IMPLEMENTED YET + def mapping_type(zendesk_type) + + mapping = { + nil => '', + 'question' => '', + 'incident' => '', + 'problem' => '', + 'task' => '', + } + return zendesk_type if !mapping[zendesk_type] + mapping[zendesk_type] + end + + def mapping_ticket_field(zendesk_field) + + mapping = { + 'subject' => 'title', + 'description' => 'note', + 'status' => 'state_id', + 'tickettype' => 'type', + 'priority' => 'priority_id', + 'group' => 'group_id', + 'assignee' => 'owner_id', + } + return zendesk_field if !mapping[zendesk_field] + mapping[zendesk_field] + end + + # FILTER: + # TODO: + # https://developer.zendesk.com/rest_api/docs/core/views#conditions-reference + def mapping_filter(zendesk_filter) + + end + + # Ticket Fields + # User Fields + # Organization Fields + # TODO: + # https://developer.zendesk.com/rest_api/docs/core/ticket_fields + # https://developer.zendesk.com/rest_api/docs/core/user_fields + # https://developer.zendesk.com/rest_api/docs/core/organization_fields + def import_fields + + %w(Ticket User Organization).each { |local_object| + + local_fields = local_object.constantize.column_names + + @client.send("#{local_object.downcase}_fields").all { |object_field| + + if local_object == 'Ticket' + mapped_object_field = method("mapping_#{local_object.downcase}_field").call( object_field.type ) + + next if local_fields.include?( mapped_object_field ) + end + + import_field(local_object, object_field) + } + } + end + + def import_field(local_object, zendesk_field) + + name = '' + if local_object == 'Ticket' + name = zendesk_field.title + else + name = zendesk_field['key'] # TODO: y?! + end + + @zendesk_ticket_field_mapping ||= {} + @zendesk_ticket_field_mapping[ zendesk_field.id ] = name + + data_type = zendesk_field.type + data_option = { + null: !zendesk_field.required, + note: zendesk_field.description, + } + + if zendesk_field.type == 'date' + + data_option = { + future: true, + past: true, + }.merge( data_option ) + + elsif zendesk_field.type == 'regexp' + + data_type = 'input' + data_option = { + type: 'text', + regex: zendesk_field.regexp_for_validation, + }.merge( data_option ) + + elsif zendesk_field.type == 'text' + + data_type = 'input' + data_option = { + type: zendesk_field.type, + }.merge( data_option ) + + elsif zendesk_field.type == 'textarea' + + data_type = 'input' + data_option = { + type: zendesk_field.type, + }.merge( data_option ) + + elsif zendesk_field.type == 'tagger' + + # \"custom_field_options\"=>[{\"id\"=>28353445 + # \"name\"=>\"Another Value\" + # \"raw_name\"=>\"Another Value\" + # \"value\"=>\"anotherkey\"} + # {\"id\"=>28353425 + # \"name\"=>\"Value 1\" + # \"raw_name\"=>\"Value 1\" + # \"value\"=>\"key1\"} + # {\"id\"=>28353435 + # \"name\"=>\"Value 2\" + # \"raw_name\"=>\"Value 2\" + # \"value\"=>\"key2\"}]}> + + # " + + options = {} + @zendesk_ticket_field_value_mapping ||= {} + zendesk_field.custom_field_options.each { |entry| + + if local_object == 'Ticket' + @zendesk_ticket_field_value_mapping[ name ] ||= {} + @zendesk_ticket_field_value_mapping[ name ][ entry['id'] ] = entry['value'] + end + + options[ entry['value'] ] = entry['name'] + } + + data_type = 'select' + data_option = { + options: options, + }.merge( data_option ) + end + + screens = { + view: { + '-all-' => { + shown: true, + }, + } + } + + if zendesk_field.visible_in_portal || !zendesk_field.required_in_portal + screens = { + edit: { + Customer: { + shown: zendesk_field.visible_in_portal, + null: !zendesk_field.required_in_portal, + }, + }.merge(screens) + } + end + + ObjectManager::Attribute.add( + object: local_object, + name: name, + display: zendesk_field.title, + data_type: data_type, + data_option: data_option, + editable: !zendesk_field.removable, + active: zendesk_field.active, + screens: screens, + pending_migration: false, + position: zendesk_field.position, + created_by_id: 1, + updated_by_id: 1, + ) + end + + # OAuth + # TODO: + # https://developer.zendesk.com/rest_api/docs/core/oauth_tokens + # https://developer.zendesk.com/rest_api/docs/core/oauth_clients + def import_oauth + + end + + # Twitter + # TODO: + # https://developer.zendesk.com/rest_api/docs/core/twitter_channel + def import_twitter + + end + + # Groups + # TODO: + # https://developer.zendesk.com/rest_api/docs/core/groups + def import_groups + + @zendesk_group_mapping = {} + @client.groups.all { |zendesk_group| + + local_group = Group.create_if_not_exists( + name: zendesk_group.name, + active: !zendesk_group.deleted, + updated_by_id: 1, + created_by_id: 1 + ) + + @zendesk_group_mapping[ zendesk_group.id ] = local_group.id + } + end + + # Organizations + # TODO: + # https://developer.zendesk.com/rest_api/docs/core/organizations + def import_organizations + + @zendesk_organization_mapping = {} + + @client.organizations.each { |zendesk_organization| + + local_organization_fields = { + name: zendesk_organization.name, + note: zendesk_organization.note, + shared: zendesk_organization.shared_tickets, + # shared: zendesk_organization.shared_comments, # TODO, not yet implemented + # }.merge(zendesk_organization.organization_fields) # TODO + } + + local_organization = Organization.create_if_not_exists( local_organization_fields ) + + @zendesk_organization_mapping[ zendesk_organization.id ] = local_organization.id + } + end + + # Users + # TODO: + # https://developer.zendesk.com/rest_api/docs/core/users + def import_users + import_group_memberships + import_custom_roles + + @zendesk_user_mapping = {} + + role_admin = Role.find_by( name: 'Admin' ) + role_agent = Role.find_by( name: 'Agent' ) + role_customer = Role.find_by( name: 'Customer' ) + + @client.users.all { |zendesk_user| + + local_user_fields = { + login: zendesk_user.id.to_s, # Zendesk users may have no other identifier than the ID, e.g. twitter users + firstname: zendesk_user.name, + email: zendesk_user.email, + phone: zendesk_user.phone, + password: '', + active: !zendesk_user.suspended, + groups: [], + roles: [], + note: zendesk_user.notes, + verified: zendesk_user.verified, + organization_id: @zendesk_organization_mapping[ zendesk_user.organization_id ], + last_login: zendesk_user.last_login_at, + } + + if @zendesk_user_group_mapping[ zendesk_user.id ] + + @zendesk_user_group_mapping[ zendesk_user.id ].each { |zendesk_group_id| + + local_group_id = @zendesk_group_mapping[ zendesk_group_id ] + + next if !local_group_id + + group = Group.find( local_group_id ) + + local_user_fields[:groups].push group + } + end + + if zendesk_user.role.name == 'end-user' + local_user_fields[:roles].push role_customer + + elsif zendesk_user.role.name == 'agent' + + local_user_fields[:roles].push role_agent + + if !zendesk_user.restricted_agent + local_user_fields[:roles].push role_admin + end + + elsif zendesk_user.role.name == 'admin' + local_user_fields[:roles].push role_agent + local_user_fields[:roles].push role_admin + end + + if zendesk_user.photo && zendesk_user.photo.content_url + local_user_fields[:image_source] = zendesk_user.photo.content_url + end + + # TODO + # local_user_fields = local_user_fields.merge( user.user_fields ) + + # TODO + # user.custom_role_id (Enterprise only) + local_user = User.create_or_update( local_user_fields ) + + @zendesk_user_mapping[ zendesk_user.id ] = local_user.id + } + end + + # Group Memberships + # TODO: + # https://developer.zendesk.com/rest_api/docs/core/group_memberships + def import_group_memberships + + @zendesk_user_group_mapping = {} + + @client.group_memberships.all { |group_membership| + @zendesk_user_group_mapping[ group_membership.user_id ] ||= [] + @zendesk_user_group_mapping[ group_membership.user_id ].push group_membership.group_id + } + end + + # Custom Roles (Enterprise only) + # TODO: + # https://developer.zendesk.com/rest_api/docs/core/custom_roles + def import_custom_roles + + end + + # Tickets + # TODO: + # https://developer.zendesk.com/rest_api/docs/core/tickets + # https://developer.zendesk.com/rest_api/docs/core/ticket_comments#ticket-comments + # https://developer.zendesk.com/rest_api/docs/core/ticket_audits#the-via-object + # https://developer.zendesk.com/rest_api/docs/help_center/article_attachments + # https://developer.zendesk.com/rest_api/docs/core/ticket_audits # v2 + def import_tickets + + article_sender_customer = Ticket::Article::Sender.find_by(name: 'Customer') + article_sender_agent = Ticket::Article::Sender.find_by(name: 'Agent') + article_sender_system = Ticket::Article::Sender.find_by(name: 'System') + + # TODO + article_type_web = Ticket::Article::Type.find_by(name: 'web') + article_type_note = Ticket::Article::Type.find_by(name: 'note') + article_type_email = Ticket::Article::Type.find_by(name: 'email') + article_type_twitter_status = Ticket::Article::Type.find_by(name: 'twitter status') + article_type_twitter_dm = Ticket::Article::Type.find_by(name: 'twitter direct-message') + article_type_facebook_feed_post = Ticket::Article::Type.find_by(name: 'facebook feed post') + article_type_facebook_feed_comment = Ticket::Article::Type.find_by(name: 'facebook feed comment') + + @client.tickets.all { |zendesk_ticket| + + zendesk_ticket_fields = {} + zendesk_ticket.custom_fields.each { |zendesk_ticket_field| + + field_name = @zendesk_ticket_field_mapping[ zendesk_ticket_field['id'] ] + field_value = zendesk_ticket_field['value'] + if @zendesk_ticket_field_value_mapping[ field_name ] + field_value = @zendesk_ticket_field_value_mapping[ field_name ][ field_value ] + end + + zendesk_ticket_fields[ field_name ] = field_value + } + + local_ticket_fields = { + title: zendesk_ticket.subject, + note: zendesk_ticket.description, + group_id: @zendesk_group_mapping[ zendesk_ticket.group_id ] || 1, # TODO + customer_id: @zendesk_user_mapping[ zendesk_ticket.requester_id ], + organization_id: @zendesk_organization_mapping[ zendesk_ticket.organization_id ], + state: Ticket::State.lookup( name: mapping_state( zendesk_ticket.status ) ), + priority: Ticket::Priority.lookup( name: mapping_priority( zendesk_ticket.priority ) ), + pending_time: zendesk_ticket.due_at, + updated_at: zendesk_ticket.updated_at, + created_at: zendesk_ticket.created_at, + updated_by_id: @zendesk_user_mapping[ zendesk_ticket.requester_id ], + created_by_id: @zendesk_user_mapping[ zendesk_ticket.requester_id ], + # }.merge(zendesk_ticket_fields) TODO + } + + ticket_author = User.find( @zendesk_user_mapping[ zendesk_ticket.requester_id ] ) + + if ticket_author.role?('Customer') + local_ticket_fields[:create_article_sender_id] = article_sender_customer.id + elsif ticket_author.role?('Agent') + local_ticket_fields[:create_article_sender_id] = article_sender_agent.id + else + local_ticket_fields[:create_article_sender_id] = article_sender_system.id + end + + # TODO: zendesk_ticket.external_id ? + if zendesk_ticket.via.channel == 'web' + local_ticket_fields[:create_article_type_id] = article_type_note.id # TODO + elsif zendesk_ticket.via.channel == 'email' + local_ticket_fields[:create_article_type_id] = article_type_email.id + elsif zendesk_ticket.via.channel == 'sample_ticket' + local_ticket_fields[:create_article_type_id] = article_type_note.id # TODO + elsif zendesk_ticket.via.channel == 'twitter' + + # TODO + if zendesk_ticket.via.source.rel == 'mention' + local_ticket_fields[:create_article_type_id] = article_type_twitter_status.id + else + local_ticket_fields[:create_article_type_id] = article_type_twitter_dm.id + end + + elsif zendesk_ticket.via.channel == 'facebook' + + # TODO + if zendesk_ticket.via.source.rel == 'post' + local_ticket_fields[:create_article_type_id] = article_type_facebook_feed_post.id + else + local_ticket_fields[:create_article_type_id] = article_type_facebook_feed_comment.id + end + end + + local_ticket = Ticket.create( local_ticket_fields ) + + zendesk_ticket.tags.each { |tag| + Tag.tag_add( + object: 'Ticket', + o_id: local_ticket.id, + item: tag.id, + created_by_id: @zendesk_user_mapping[ zendesk_ticket.requester_id ], + ) + } + + zendesk_ticket.comments.each { |zendesk_article| + + # p zendesk_article.inspect + + # "#31964468391, + # \"type\"=>\"Comment\", + # \"author_id\"=>1150734731, + # \"body\"=>\"This is the first comment. Feel free to delete this sample ticket.\", + # \"html_body\"=>\"

This is the first comment. Feel free to delete this sample ticket.

\", + # \"public\"=>true, + # \"attachments\"=>[], + # \"audit_id\"=>31964468381, + # \"via\"=>{\"channel\"=>\"sample_ticket\", + # \"source\"=>{\"from\"=>{}, + # \"to\"=>{}, + # \"rel\"=>nil}}, + # \"metadata\"=>{\"system\"=>{}, + # \"custom\"=>{}}, + # \"created_at\"=>2015-07-19 22:41:43 UTC} + # " + local_article_fields = { + ticket_id: local_ticket.id, + body: zendesk_article.html_body, + internal: !zendesk_article.public, + updated_by_id: @zendesk_user_mapping[ zendesk_article.author_id ], + created_by_id: @zendesk_user_mapping[ zendesk_article.author_id ], + } + + article_author = User.find( @zendesk_user_mapping[ zendesk_article.author_id ] ) + + if article_author.role?('Customer') + local_article_fields[:sender_id] = article_sender_customer.id + elsif article_author.role?('Agent') + local_article_fields[:sender_id] = article_sender_agent.id + else + local_article_fields[:sender_id] = article_sender_system.id + end + + if zendesk_article.via.channel == 'web' + local_article_fields[:message_id] = zendesk_article.id + local_article_fields[:type_id] = article_type_note.id # TODO + elsif zendesk_article.via.channel == 'email' + local_article_fields[:from] = zendesk_article.via.source.from.address + local_article_fields[:to] = zendesk_article.via.source.to.address # TODO: or zendesk_article.via.from.original_recipients=[\"martin.edenhofer42@gmail.com\", \"support@znunyhelp.zendesk.com\"] + local_article_fields[:message_id] = zendesk_article.id + local_article_fields[:type_id] = article_type_email.id + elsif zendesk_article.via.channel == 'sample_ticket' + local_article_fields[:message_id] = zendesk_article.id + local_article_fields[:type_id] = article_type_note.id # TODO + elsif zendesk_article.via.channel == 'twitter' + local_article_fields[:message_id] = zendesk_article.id + + # TODO + if zendesk_article.via.source.rel == 'mention' + local_article_fields[:type_id] = article_type_twitter_status.id + else + local_article_fields[:type_id] = article_type_twitter_dm.id + end + + elsif zendesk_article.via.channel == 'facebook' + + local_article_fields[:from] = zendesk_article.via.source.from.facebook_id + local_article_fields[:to] = zendesk_article.via.source.to.facebook_id + local_article_fields[:message_id] = zendesk_article.id + + # TODO + if zendesk_article.via.source.rel == 'post' + local_article_fields[:type_id] = article_type_facebook_feed_post.id + else + local_article_fields[:type_id] = article_type_facebook_feed_comment.id + end + end + + # create article + local_article = Ticket::Article.create( local_article_fields ) + + zendesk_attachments = zendesk_article.attachments + + next if zendesk_attachments.size == 0 + + local_attachments = local_article.attachments + + zendesk_attachments.each { |zendesk_attachment| + + response = UserAgent.get( + zendesk_attachment.content_url, + {}, + { + open_timeout: 10, + read_timeout: 60, + }, + ) + + if !response.success? + Rails.logger.error response.error + next + end + + local_attachment = Store.add( + object: 'Ticket::Article', + o_id: local_article.id, + data: response.body, + filename: zendesk_attachment.file_name, + preferences: { + 'Content-Type' => zendesk_attachment.content_type + } + ) + } + } + } + end + + # SLA Policies + # TODO: + # https://github.com/zendesk/zendesk_api_client_rb/issues/271 + # https://developer.zendesk.com/rest_api/docs/core/sla_policies + def import_sla_policies + + end + + # Macros + # TODO: + # https://developer.zendesk.com/rest_api/docs/core/macros + def import_macros + + @client.macros.all { |macro| + + # TODO + next if !macro.active + + # "url"=>"https://znunyhelp.zendesk.com/api/v2/macros/59511191.json" + # "id"=>59511191 + # "title"=>"Herabstufen und informieren" + # "active"=>true + # "updated_at"=>2015-08-03 13:51:14 UTC + # "created_at"=>2015-07-19 22:41:42 UTC + # "restriction"=>nil + # "actions"=>[ + # { + # "field"=>"priority" + # "value"=>"low" + # } + # { + # "field"=>"comment_value" + # "value"=>"Das Verkehrsaufkommen ist g....." + # } + # ] + + perform = {} + macro.actions.each { |action| + + # TODO: ID fields + perform["ticket.#{action.field}"] = action.value + } + + Macro.create_if_not_exists( + name: macro.title, + perform: perform, + note: '', + active: macro.active, + ) + } + end + + # Schedulers + # TODO: + # https://github.com/zendesk/zendesk_api_client_rb/issues/281 + # https://developer.zendesk.com/rest_api/docs/core/schedules + def import_schedules + + end + + # Views + # TODO: + # https://developer.zendesk.com/rest_api/docs/core/views + def import_views + + @client.views.all { |view| + + # "url" => "https://znunyhelp.zendesk.com/api/v2/views/59511071.json" + # "id" => 59511071 + # "title" => "Ihre Tickets" + # "active" => true + # "updated_at" => 2015-08-03 13:51:14 UTC + # "created_at" => 2015-07-19 22:41:42 UTC + # "restriction" => nil + # "sla_id" => nil + # "execution" => { + # "group_by" => "status" + # "group_order" => "asc" + # "sort_by" => "score" + # "sort_order" => "desc" + # "group" => { + # "id" => "status" + # "title" => "Status" + # "order" => "asc" + # } + # "sort" => { + # "id" => "score" + # "title" => "Score" + # "order" => "desc" + # } + # "columns" => [ + # { + # "id" => "score" + # "title" => "Score" + # } + # { + # "id" => "subject" + # "title" => "Subject" + # } + # { + # "id" => "requester" + # "title" => "Requester" + # } + # { + # "id" => "created" + # "title" => "Requested" + # } + # { + # "id" => "type" + # "title" => "Type" + # } + # { + # "id" => "priority" + # "title" => "Priority" + # } + # ] + # "fields" => [ + # { + # "id" => "score" + # "title" => "Score" + # } + # { + # "id" => "subject" + # "title" => "Subject" + # } + # { + # "id" => "requester" + # "title" => "Requester" + # } + # { + # "id" => "created" + # "title" => "Requested" + # } + # { + # "id" => "type" + # "title" => "Type" + # } + # { + # "id" => "priority" + # "title" => "Priority" + # } + # ] + # "custom_fields" => [] + # } + # "conditions" => { + # "all" => [ + # { + # "field" => "status" + # "operator" => "less_than" + # "value" => "solved" + # } + # { + # "field" => "assignee_id" + # "operator" => "is" + # "value" => "current_user" + # } + # ] + # "any" => [] + # } + + Overview.create_if_not_exists( + name: view.title, + link: 'my_assigned', # TODO + prio: 1000, + role_id: overview_role.id, + condition: { + 'ticket.state_id' => { + operator: 'is', + value: [ 1, 2, 3, 7 ], + }, + 'ticket.owner_id' => { + operator: 'is', + pre_condition: 'current_user.id', + }, + }, + order: { + by: 'created_at', + direction: 'ASC', + }, + view: { + d: %w(title customer group created_at), + s: %w(title customer group created_at), + m: %w(number title customer group created_at), + view_mode_default: 's', + }, + ) + } + end + + # Automations + # TODO: + # https://developer.zendesk.com/rest_api/docs/core/automations + def import_automations + + @client.automations.all { |automation| + + # "url" => "https://znunyhelp.zendesk.com/api/v2/automations/60037892.json" + # "id" => 60037892 + # "title" => "Ticket aus Facebook-Nachricht 1 ..." + # "active" => true + # "updated_at" => 2015-08-03 13:51:15 UTC + # "created_at" => 2015-07-28 11:27:50 UTC + # "actions" => [ + # { + # "field" => "status" + # "value" => "closed" + # } + # ] + # "conditions" => { + # "all" => [ + # { + # "field" => "status" + # "operator" => "is" + # "value" => "solved" + # } + # { + # "field" => "SOLVED" + # "operator" => "is" + # "value" => "24" + # } + # { + # "field" => "via_type" + # "operator" => "is" + # "value" => "facebook" + # } + # ] + # "any" => [] + # } + # "position" => 10000 + + } + end + +end diff --git a/test/integration/zendesk_import_test.rb b/test/integration/zendesk_import_test.rb new file mode 100644 index 000000000..a5e8608fc --- /dev/null +++ b/test/integration/zendesk_import_test.rb @@ -0,0 +1,471 @@ +# encoding: utf-8 +require 'integration_test_helper' + +class ZendeskImportTest < ActiveSupport::TestCase + + if !ENV['IMPORT_ZENDESK_ENDPOINT'] + fail "ERROR: Need IMPORT_ZENDESK_ENDPOINT - hint IMPORT_ZENDESK_ENDPOINT='https://znuny.zendesk.com/api/v2'" + end + if !ENV['IMPORT_ZENDESK_ENDPOINT_KEY'] + fail "ERROR: Need IMPORT_ZENDESK_ENDPOINT_KEY - hint IMPORT_ZENDESK_ENDPOINT_KEY='01234567899876543210'" + end + if !ENV['IMPORT_ZENDESK_ENDPOINT_USERNAME'] + fail "ERROR: Need IMPORT_ZENDESK_ENDPOINT_USERNAME - hint IMPORT_ZENDESK_ENDPOINT_USERNAME='bob.ross@happylittletrees.com'" + end + + Setting.set('import_zendesk_endpoint', ENV['IMPORT_ZENDESK_ENDPOINT']) + Setting.set('import_zendesk_endpoint_key', ENV['IMPORT_ZENDESK_ENDPOINT_KEY']) + Setting.set('import_zendesk_endpoint_username', ENV['IMPORT_ZENDESK_ENDPOINT_USERNAME']) + Setting.set('import_mode', true) + Import::Zendesk.start + + # check statistic count + test 'check statistic' do + + remote_statistic = Import::Zendesk.statistic + + # retrive statistic + compare_statistic = { + 'Tickets' => 143, + 'TicketFields' => 13, + 'UserFields' => 1, + 'OrganizationFields' => 1, + 'Groups' => 2, + 'Organizations' => 1, + 'Users' => 141, + 'GroupMemberships' => 3, + 'Macros' => 5, + 'Views' => 11, + 'Automations' => 5 + } + + assert_equal( compare_statistic, remote_statistic, 'statistic' ) + end + + # check count of imported items + test 'check counts' do + assert_equal( 143, User.count, 'users' ) + assert_equal( 3, Group.count, 'groups' ) + assert_equal( 5, Role.count, 'roles' ) + assert_equal( 2, Organization.count, 'organizations' ) + assert_equal( 144, Ticket.count, 'tickets' ) + assert_equal( 151, Ticket::Article.count, 'ticket articles' ) + assert_equal( 2, Store.count, 'ticket article attachments' ) + + # TODO: Macros, Views, Automations... + end + + # check imported users and permission + test 'check users' do + + role_admin = Role.find_by( name: 'Admin' ) + role_agent = Role.find_by( name: 'Agent' ) + role_customer = Role.find_by( name: 'Customer' ) + + group_users = Group.find_by( name: 'Users' ) + group_support = Group.find_by( name: 'Support' ) + group_additional_group = Group.find_by( name: 'Additional Group' ) + + checks = [ + { + id: 4, + data: { + firstname: 'Bob', + lastname: 'Smith', + login: '1150734731', + email: 'bob.smith@znuny.com', + active: true, + phone: '00114124', + }, + roles: [role_agent, role_admin], + groups: [group_support], + }, + { + id: 5, + data: { + firstname: 'Hansimerkur', + lastname: '', + login: '1202726471', + email: 'hansimerkur@znuny.com', + active: true, + }, + roles: [role_agent, role_admin], + groups: [group_additional_group, group_support], + }, + { + id: 6, + data: { + firstname: 'Bernd', + lastname: 'Hofbecker', + login: '1202726611', + email: 'bernd.hofbecker@znuny.com', + active: true, + }, + roles: [role_customer], + groups: [], + }, + { + id: 7, + data: { + firstname: 'Zendesk', + lastname: '', + login: '1202737821', + email: 'noreply@zendesk.com', + active: true, + }, + roles: [role_customer], + groups: [], + }, + { + id: 89, + data: { + firstname: 'Hans', + lastname: 'Peter Wurst', + login: '1205512622', + email: 'hansimerkur+zd-c1@znuny.com', + active: true, + }, + roles: [role_customer], + groups: [], + }, + ] + + checks.each { |check| + user = User.find( check[:id] ) + + assert_equal( check[:data][:firstname], user.firstname, 'firstname' ) + assert_equal( check[:data][:lastname], user.lastname, 'lastname' ) + assert_equal( check[:data][:login], user.login, 'login' ) + assert_equal( check[:data][:email], user.email, 'email' ) + assert_equal( check[:data][:phone], user.phone, 'phone' ) + assert_equal( check[:data][:active], user.active, 'active' ) + + assert_equal( check[:roles], user.roles.to_a, "#{user.login} roles" ) + assert_equal( check[:groups], user.groups.to_a, "#{user.login} groups" ) + } + end + + # check user fields + test 'check user fields' do + + local_fields = User.column_names + + # TODO + copmare_fields = %w( + id + organization_id + login + firstname + lastname + email + image + image_source + web + password + phone + fax + mobile + department + street + zip + city + country + address + vip + verified + active + note + last_login + source + login_failed + preferences + updated_by_id + created_by_id + created_at + updated_at) + + assert_equal( copmare_fields, local_fields, 'user fields' ) + end + + # check groups/queues + test 'check groups' do + + checks = [ + { + id: 1, + data: { + name: 'Users', + active: true, + }, + }, + { + id: 2, + data: { + name: 'Additional Group', + active: true, + }, + }, + { + id: 3, + data: { + name: 'Support', + active: true, + }, + }, + ] + + checks.each { |check| + group = Group.find( check[:id] ) + + assert_equal( check[:data][:name], group.name, 'name' ) + assert_equal( check[:data][:active], group.active, 'active' ) + } + end + + # check imported organizations + test 'check organizations' do + + checks = [ + { + id: 1, + data: { + name: 'Zammad Foundation', + note: '', + }, + }, + { + id: 2, + data: { + name: 'Znuny', + note: nil, + }, + }, + ] + + checks.each { |check| + organization = Organization.find( check[:id] ) + + assert_equal( check[:data][:name], organization.name, 'name' ) + assert_equal( check[:data][:note], organization.note, 'note' ) + } + end + + # check organization fields + test 'check organization fields' do + + local_fields = Organization.column_names + + # TODO + copmare_fields = %w( + id + name + shared + active + note + updated_by_id + created_by_id + created_at + updated_at) + + assert_equal( copmare_fields, local_fields, 'organization fields' ) + end + + # check imported tickets + test 'check tickets' do + + checks = [ + { + id: 3, + data: { + title: 'test', + note: 'test email', + create_article_type_id: 1, + create_article_sender_id: 2, + article_count: 2, + state_id: 3, + group_id: 3, + priority_id: 3, + owner_id: 1, + customer_id: 6, + organization_id: 2, + }, + }, + { + id: 143, + data: { + title: 'Basti ist cool', + note: 'Basti ist cool', + create_article_type_id: 8, + create_article_sender_id: 2, + article_count: 1, + state_id: 1, + group_id: 1, + priority_id: 2, + owner_id: 1, + customer_id: 143, + organization_id: nil, + }, + }, + { + id: 5, + data: { + title: 'Twitter', + note: '@DesafioCaracol sh q acaso sto se vale ver el jueg...', + create_article_type_id: 6, + create_article_sender_id: 2, + article_count: 1, + state_id: 1, + group_id: 3, + priority_id: 2, + owner_id: 1, + customer_id: 90, + organization_id: nil, + }, + }, + { + id: 2, + data: { + title: 'This is a sample ticket requested and submitted by you', + note: 'This is the first comment. Feel free to delete this sample ticket.', + create_article_type_id: 10, + create_article_sender_id: 1, + article_count: 4, + state_id: 3, + group_id: 3, + priority_id: 3, + owner_id: 1, + customer_id: 4, + organization_id: 2, + }, + }, + # { + # id: , + # data: { + # title: , + # note: , + # create_article_type_id: , + # create_article_sender_id: , + # article_count: , + # state_id: , + # group_id: , + # priority_id: , + # owner_id: , + # customer_id: , + # organization_id: , + # }, + # }, + ] + + checks.each { |check| + ticket = Ticket.find( check[:id] ) + + assert_equal( check[:data][:title], ticket.title, 'title' ) + assert_equal( check[:data][:create_article_type_id], ticket.create_article_type_id, 'created_article_type_id' ) + assert_equal( check[:data][:create_article_sender_id], ticket.create_article_sender_id, 'created_article_sender_id' ) + assert_equal( check[:data][:article_count], ticket.article_count, 'article_count' ) + assert_equal( check[:data][:state_id], ticket.state.id, 'state_id' ) + assert_equal( check[:data][:group_id], ticket.group.id, 'group_id' ) + assert_equal( check[:data][:priority_id], ticket.priority.id, 'priority_id' ) + assert_equal( check[:data][:owner_id], ticket.owner.id, 'owner_id' ) + assert_equal( check[:data][:customer_id], ticket.customer.id, 'customer_id' ) + assert_equal( check[:data][:organization_id], ticket.organization.try(:id), 'organization_id' ) + } + end + + test 'check article attachments' do + + checks = [ + { + id: 5, + data: { + count: 1, + 1 => { + preferences: { + 'Content-Type' => 'image/jpeg' + }, + filename: '1a3496b9-53d9-494d-bbb0-e1d2e22074f8.jpeg', + }, + }, + }, + { + id: 7, + data: { + count: 1, + 1 => { + preferences: { + 'Content-Type' => 'image/jpeg' + }, + filename: 'paris.jpg', + }, + }, + }, + ] + + checks.each { |check| + article = Ticket::Article.find(check[:id]) + + assert_equal( check[:data][:count], article.attachments.count, 'attachemnt count' ) + + (1..check[:data][:count] ).each { |attachment_counter| + + attachment = article.attachments[ attachment_counter - 1 ] + compare_attachment = check[:data][ attachment_counter ] + + assert_equal( compare_attachment[:filename], attachment.filename, 'attachment file name' ) + + assert_equal( compare_attachment[:preferences], attachment[:preferences], 'attachment preferences') + + } + } + end + + # check ticket fields + test 'check ticket fields' do + + local_fields = Ticket.column_names + + # TODO + copmare_fields = %w( + id + group_id + priority_id + state_id + organization_id + number + title + owner_id + customer_id + note + first_response + first_response_escal_date + first_response_sla_time + first_response_in_min + first_response_diff_in_min + close_time + close_time_escal_date + close_time_sla_time + close_time_in_min + close_time_diff_in_min + update_time_escal_date + updtate_time_sla_time + update_time_in_min + update_time_diff_in_min + last_contact + last_contact_agent + last_contact_customer + create_article_type_id + create_article_sender_id + article_count + escalation_time + pending_time + type + updated_by_id + created_by_id + created_at + updated_at + preferences) + + assert_equal( copmare_fields, local_fields, 'ticket fields' ) + end + +end