Fixes #4052 - Freshdesk migration which doesn't support time entries runs in unwanted behaviour.

This commit is contained in:
Dominik Klein 2022-04-14 07:43:04 +02:00
parent 52834f0d85
commit 3cada0f15a
18 changed files with 242 additions and 45 deletions

View file

@ -11,6 +11,7 @@ class Sequencer
'Import::Common::ImportMode::Check', 'Import::Common::ImportMode::Check',
'Import::Common::SystemInitDone::Check', 'Import::Common::SystemInitDone::Check',
'Import::Common::ImportJob::DryRun', 'Import::Common::ImportJob::DryRun',
'Import::Freshdesk::TimeEntry::Available',
'Import::Freshdesk::IdMap', 'Import::Freshdesk::IdMap',
'Import::Freshdesk::Groups', 'Import::Freshdesk::Groups',
'Import::Freshdesk::FieldMap', 'Import::Freshdesk::FieldMap',

View file

@ -8,6 +8,7 @@ class Sequencer
def self.sequence def self.sequence
[ [
'Import::Freshdesk::TimeEntry::Skip',
'Import::Freshdesk::Request', 'Import::Freshdesk::Request',
'Import::Freshdesk::Resources', 'Import::Freshdesk::Resources',
'Import::Freshdesk::ModelClass', 'Import::Freshdesk::ModelClass',

View file

@ -5,6 +5,9 @@ class Sequencer
module Import module Import
module Freshdesk module Freshdesk
class ModelClass < Sequencer::Unit::Common::Provider::Named class ModelClass < Sequencer::Unit::Common::Provider::Named
prepend ::Sequencer::Unit::Import::Common::Model::Mixin::Skip::Action
skip_action :skipped, :failed
uses :object uses :object

View file

@ -9,17 +9,18 @@ class Sequencer
skip_action :skipped, :failed skip_action :skipped, :failed
uses :resources, :object, :import_job, :dry_run, :field_map, :id_map uses :resources, :object, :import_job, :dry_run, :field_map, :id_map, :time_entry_available
def process def process
resources.each do |resource| resources.each do |resource|
::Sequencer.process("Import::Freshdesk::#{object}", ::Sequencer.process("Import::Freshdesk::#{object}",
parameters: { parameters: {
import_job: import_job, import_job: import_job,
dry_run: dry_run, dry_run: dry_run,
resource: resource, resource: resource,
field_map: field_map, field_map: field_map,
id_map: id_map, id_map: id_map,
time_entry_available: time_entry_available,
}) })
end end
end end

View file

@ -6,6 +6,9 @@ class Sequencer
module Freshdesk module Freshdesk
class Request < Sequencer::Unit::Common::Provider::Attribute class Request < Sequencer::Unit::Common::Provider::Attribute
extend ::Sequencer::Unit::Import::Freshdesk::Requester extend ::Sequencer::Unit::Import::Freshdesk::Requester
prepend ::Sequencer::Unit::Import::Common::Model::Mixin::Skip::Action
skip_action :skipped, :failed
uses :object, :request_params uses :object, :request_params
provides :response provides :response
@ -13,7 +16,6 @@ class Sequencer
private private
def response def response
builder = backend.new( builder = backend.new(
object: object, object: object,
request_params: request_params request_params: request_params

View file

@ -31,12 +31,14 @@ class Sequencer
else else
logger.info "Unknown response: #{response.inspect}. Sleeping 10 seconds and retry (##{iteration + 1}/10)." logger.info "Unknown response: #{response.inspect}. Sleeping 10 seconds and retry (##{iteration + 1}/10)."
end end
sleep sleep_for sleep sleep_for
end end
def handle_exception(e, iteration) def handle_exception(e, iteration)
logger.error e logger.error e
logger.info "Sleeping 10 seconds after #{e.class.name} and retry (##{iteration + 1}/10)." logger.info "Sleeping 10 seconds after #{e.class.name} and retry (##{iteration + 1}/10)."
sleep 10 sleep 10
end end

View file

@ -6,6 +6,9 @@ class Sequencer
module Freshdesk module Freshdesk
class Resources < Sequencer::Unit::Common::Provider::Named class Resources < Sequencer::Unit::Common::Provider::Named
include ::Sequencer::Unit::Import::Common::Model::Mixin::HandleFailure include ::Sequencer::Unit::Import::Common::Model::Mixin::HandleFailure
prepend ::Sequencer::Unit::Import::Common::Model::Mixin::Skip::Action
skip_action :skipped, :failed
uses :response, :skipped_resource_id uses :response, :skipped_resource_id

View file

@ -7,7 +7,7 @@ class Sequencer
module SubSequence module SubSequence
class Generic < Sequencer::Unit::Base class Generic < Sequencer::Unit::Base
uses :dry_run, :import_job, :field_map, :id_map uses :dry_run, :import_job, :field_map, :id_map, :time_entry_available
attr_accessor :iteration, :result attr_accessor :iteration, :result
@ -18,13 +18,14 @@ class Sequencer
@iteration = iteration @iteration = iteration
@result = ::Sequencer.process(sequence_name, @result = ::Sequencer.process(sequence_name,
parameters: { parameters: {
request_params: request_params, request_params: request_params,
import_job: import_job, import_job: import_job,
dry_run: dry_run, dry_run: dry_run,
object: object, object: object,
field_map: field_map, field_map: field_map,
id_map: id_map, id_map: id_map,
skipped_resource_id: skipped_resource_id, skipped_resource_id: skipped_resource_id,
time_entry_available: time_entry_available,
}, },
expecting: self.class.const_get(:EXPECTING)) expecting: self.class.const_get(:EXPECTING))
break if iteration_should_stop? break if iteration_should_stop?
@ -56,7 +57,7 @@ class Sequencer
end end
def iteration_should_stop? def iteration_should_stop?
return true if result[:action] == :failed return true if result[:action] == :failed || result[:action] == :skipped
return true if result[:response].header['link'].blank? return true if result[:response].header['link'].blank?
false false

View file

@ -8,8 +8,6 @@ class Sequencer
class TimeEntries < Sequencer::Unit::Import::Freshdesk::SubSequence::Generic class TimeEntries < Sequencer::Unit::Import::Freshdesk::SubSequence::Generic
prepend ::Sequencer::Unit::Import::Common::Model::Mixin::Skip::Action prepend ::Sequencer::Unit::Import::Common::Model::Mixin::Skip::Action
optional :action
skip_action :skipped, :failed skip_action :skipped, :failed
uses :resource uses :resource

View file

@ -0,0 +1,34 @@
# Copyright (C) 2012-2022 Zammad Foundation, https://zammad-foundation.org/
class Sequencer
class Unit
module Import
module Freshdesk
module TimeEntry
class Available < Sequencer::Unit::Common::Provider::Attribute
extend ::Sequencer::Unit::Import::Freshdesk::Requester
provides :time_entry_available
def process
state.provide(:time_entry_available, time_entry_available)
end
private
def time_entry_available
response = self.class.perform_request(
api_path: 'time_entries',
)
response.is_a?(Net::HTTPOK)
rescue => e
logger.info e
nil
end
end
end
end
end
end
end

View file

@ -9,7 +9,6 @@ class Sequencer
include ::Sequencer::Unit::Import::Common::Mapping::Mixin::ProvideMapped include ::Sequencer::Unit::Import::Common::Mapping::Mixin::ProvideMapped
uses :resource, :id_map uses :resource, :id_map
provides :action
def process def process
provide_mapped do provide_mapped do
@ -21,14 +20,6 @@ class Sequencer
updated_at: resource['updated_at'], updated_at: resource['updated_at'],
} }
end end
rescue TypeError => e
# TimeTracking is not available in the plans: Sprout, Blossom
# In this case `resource`s value is `["code", "require_feature"]`
# See:
# - Ticket# 1077135
# - https://support.freshdesk.com/support/solutions/articles/37583-keeping-track-of-time-spent
logger.debug { e }
state.provide(:action, :skipped)
end end
private private

View file

@ -0,0 +1,22 @@
# Copyright (C) 2012-2022 Zammad Foundation, https://zammad-foundation.org/
class Sequencer
class Unit
module Import
module Freshdesk
module TimeEntry
class Skip < Sequencer::Unit::Base
uses :time_entry_available
provides :action
def process
return if time_entry_available
state.provide(:action, :skipped)
end
end
end
end
end
end
end

View file

@ -33,13 +33,14 @@ RSpec.describe ::Sequencer::Sequence::Import::Freshdesk::GenericObject, sequence
let(:process_payload) do let(:process_payload) do
{ {
import_job: build_stubbed(:import_job, name: 'Import::Freshdesk', payload: {}), import_job: build_stubbed(:import_job, name: 'Import::Freshdesk', payload: {}),
dry_run: false, dry_run: false,
object: 'Group', object: 'Group',
request_params: {}, request_params: {},
field_map: {}, field_map: {},
id_map: {}, id_map: {},
skipped_resource_id: nil, skipped_resource_id: nil,
time_entry_available: true,
} }
end end

View file

@ -69,11 +69,12 @@ RSpec.describe ::Sequencer::Sequence::Import::Freshdesk::Ticket, sequencer: :seq
end end
let(:process_payload) do let(:process_payload) do
{ {
import_job: build_stubbed(:import_job, name: 'Import::Freshdesk', payload: {}), import_job: build_stubbed(:import_job, name: 'Import::Freshdesk', payload: {}),
dry_run: false, dry_run: false,
resource: resource, resource: resource,
field_map: field_map, field_map: field_map,
id_map: id_map, id_map: id_map,
time_entry_available: true,
} }
end end
let(:owner) { create :agent, group_ids: [group.id] } let(:owner) { create :agent, group_ids: [group.id] }

View file

@ -0,0 +1,103 @@
# Copyright (C) 2012-2022 Zammad Foundation, https://zammad-foundation.org/
require 'rails_helper'
RSpec.describe ::Sequencer::Sequence::Import::Freshdesk::TimeEntries, sequencer: :sequence, db_strategy: 'reset' do
let(:time_entry_available) { true }
let(:ticket) { create :ticket }
let(:process_payload) do
{
import_job: build_stubbed(:import_job, name: 'Import::Freshdesk', payload: {}),
dry_run: false,
object: 'TimeEntry',
request_params: {
ticket: {
'id' => 1001,
},
},
field_map: {},
id_map: {
'Ticket' => {
1001 => ticket.id,
},
'User' => {
80_014_400_475 => 1,
}
},
skipped_resource_id: nil,
time_entry_available: time_entry_available,
}
end
context 'when time entry feature is available' do
let(:resources_payloud) do
[
{
'id' => 80_027_218_656,
'billable' => true,
'note' => 'Example Preparation',
'timer_running' => false,
'agent_id' => 80_014_400_475,
'ticket_id' => 1001,
'time_spent' => '01:20',
'created_at' => '2021-05-14T12:29:27Z',
'updated_at' => '2021-05-14T12:29:27Z',
'start_time' => '2021-05-14T12:29:27Z',
'executed_at' => '2021-05-14T12:29:27Z'
},
{
'id' => 80_027_218_657,
'billable' => true,
'note' => 'Example Preparation 2',
'timer_running' => false,
'agent_id' => 80_014_400_475,
'ticket_id' => 1001,
'time_spent' => '02:20',
'created_at' => '2021-05-15T12:29:27Z',
'updated_at' => '2021-05-15T12:29:27Z',
'start_time' => '2021-05-15T12:29:27Z',
'executed_at' => '2021-05-15T12:29:27Z'
}
]
end
let(:imported_time_entry) do
{
ticket_id: ticket.id,
created_by_id: 1,
time_unit: 140,
}
end
before do
# Mock the groups get request
stub_request(:get, 'https://yours.freshdesk.com/api/v2/tickets/1001/time_entries?per_page=100').to_return(status: 200, body: JSON.generate(resources_payloud), headers: {})
end
it 'add time entry for ticket' do
expect { process(process_payload) }.to change(Ticket::TimeAccounting, :count).by(2)
end
it 'check last time unit for ticket' do
process(process_payload)
expect(Ticket::TimeAccounting.last).to have_attributes(imported_time_entry)
end
context 'with empty time entries' do
let(:resources_payloud) { [] }
it 'do not change time entry for ticket' do
expect { process(process_payload) }.to change(Ticket::TimeAccounting, :count).by(0)
end
end
end
context 'when time entry feature is not available' do
let(:time_entry_available) { false }
it 'add time entry for ticket' do
expect { process(process_payload) }.to change(Ticket::TimeAccounting, :count).by(0)
end
end
end

View file

@ -10,7 +10,7 @@ RSpec.describe ::Sequencer::Sequence::Import::Freshdesk::TimeEntry, sequencer: :
{ {
'id' => 80_027_218_656, 'id' => 80_027_218_656,
'billable' => true, 'billable' => true,
'note' => 'Example Prepartion', 'note' => 'Example Preparation',
'timer_running' => false, 'timer_running' => false,
'agent_id' => 80_014_400_475, 'agent_id' => 80_014_400_475,
'ticket_id' => 1001, 'ticket_id' => 1001,
@ -57,6 +57,7 @@ RSpec.describe ::Sequencer::Sequence::Import::Freshdesk::TimeEntry, sequencer: :
it 'correct attributes for added time entry' do it 'correct attributes for added time entry' do
process(process_payload) process(process_payload)
Rails.logger.debug Ticket::TimeAccounting.last
expect(Ticket::TimeAccounting.last).to have_attributes(imported_time_entry) expect(Ticket::TimeAccounting.last).to have_attributes(imported_time_entry)
end end

View file

@ -64,11 +64,12 @@ RSpec.describe ::Sequencer::Unit::Import::Freshdesk::Tickets, sequencer: :unit,
let(:process_payload) do let(:process_payload) do
{ {
import_job: build_stubbed(:import_job, name: 'Import::Freshdesk', payload: {}), import_job: build_stubbed(:import_job, name: 'Import::Freshdesk', payload: {}),
dry_run: false, dry_run: false,
request_params: {}, request_params: {},
field_map: {}, field_map: {},
id_map: id_map, id_map: id_map,
time_entry_available: true,
} }
end end

View file

@ -0,0 +1,31 @@
# Copyright (C) 2012-2022 Zammad Foundation, https://zammad-foundation.org/
require 'rails_helper'
RSpec.describe Sequencer::Unit::Import::Freshdesk::TimeEntry::Available, sequencer: :unit do
context 'when checking the availability of the time entry feature' do
let(:params) do
{
dry_run: false,
import_job: instance_double(ImportJob),
field_map: {},
id_map: {},
}
end
let(:response_ok) { Net::HTTPOK.new(1.0, '200', 'OK') }
let(:response_forbidden) { Net::HTTPUnauthorized.new(1.0, '403', 'Forbidden') }
it 'check for avilable time entry feature' do
allow(described_class).to receive(:perform_request).with(any_args).and_return(response_ok)
expect(process(params)).to eq({ time_entry_available: true })
end
it 'check for not available time entry feature' do
allow(described_class).to receive(:perform_request).with(any_args).and_return(response_forbidden)
expect(process(params)).to eq({ time_entry_available: false })
end
end
end