Browse Source

First draft of Zendesk import and integration tests.

Thorsten Eckel 9 years ago
parent
commit
e5245c612f

+ 2 - 0
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'

+ 11 - 1
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

+ 61 - 0
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

+ 60 - 4
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',

+ 907 - 0
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
+
+        # "#<ZendeskAPI::Ticket::Comment {\"id\"=>31964468391,
+        # \"type\"=>\"Comment\",
+        # \"author_id\"=>1150734731,
+        # \"body\"=>\"This is the first comment. Feel free to delete this sample ticket.\",
+        # \"html_body\"=>\"<div class=\\\"zd-comment\\\"><p>This is the first comment. Feel free to delete this sample ticket.</p></div>\",
+        # \"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

+ 471 - 0
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