2018-08-02 18:38:54 +02:00
|
|
|
|
module Tasks
|
|
|
|
|
class DossierProcedureMigrator
|
|
|
|
|
# Migrates dossiers from an old source procedure to a revised destination procedure.
|
|
|
|
|
|
|
|
|
|
class ChampMapping
|
2018-10-09 21:23:27 +02:00
|
|
|
|
def initialize(source_procedure, destination_procedure, is_private)
|
2018-08-02 18:38:54 +02:00
|
|
|
|
@source_procedure = source_procedure
|
|
|
|
|
@destination_procedure = destination_procedure
|
2018-10-09 21:23:27 +02:00
|
|
|
|
@is_private = is_private
|
2018-08-02 18:38:54 +02:00
|
|
|
|
|
|
|
|
|
@expected_source_types_de_champ = {}
|
|
|
|
|
@expected_destination_types_de_champ = {}
|
|
|
|
|
@source_to_destination_mapping = {}
|
|
|
|
|
@source_champs_to_discard = Set[]
|
|
|
|
|
@destination_champ_computations = []
|
2018-10-09 20:09:13 +02:00
|
|
|
|
|
|
|
|
|
setup_mapping
|
2018-08-02 18:38:54 +02:00
|
|
|
|
end
|
|
|
|
|
|
2018-10-10 15:00:59 +02:00
|
|
|
|
def check_source_destination_consistency
|
2018-10-09 21:23:27 +02:00
|
|
|
|
check_champs_consistency("#{privacy_label}source", @expected_source_types_de_champ, types_de_champ(@source_procedure))
|
|
|
|
|
check_champs_consistency("#{privacy_label}destination", @expected_destination_types_de_champ, types_de_champ(@destination_procedure))
|
2018-10-09 18:58:26 +02:00
|
|
|
|
end
|
|
|
|
|
|
2018-10-10 13:07:12 +02:00
|
|
|
|
def can_migrate?(dossier)
|
|
|
|
|
true
|
|
|
|
|
end
|
|
|
|
|
|
2018-10-10 15:00:59 +02:00
|
|
|
|
def migrate(dossier)
|
2018-10-09 18:16:21 +02:00
|
|
|
|
# Since we’re going to iterate and change the champs at the same time,
|
|
|
|
|
# we use to_a to make the list static and avoid nasty surprises
|
2018-10-09 21:23:27 +02:00
|
|
|
|
original_champs = champs(dossier).to_a
|
2018-10-09 18:16:21 +02:00
|
|
|
|
|
|
|
|
|
compute_new_champs(dossier)
|
|
|
|
|
|
|
|
|
|
original_champs.each do |c|
|
|
|
|
|
tdc_to = destination_type_de_champ(c)
|
|
|
|
|
if tdc_to.present?
|
2018-10-10 17:13:14 +02:00
|
|
|
|
c.update_columns(type_de_champ_id: tdc_to.id)
|
2018-10-09 18:16:21 +02:00
|
|
|
|
elsif discard_champ?(c)
|
2018-10-09 21:23:27 +02:00
|
|
|
|
champs(dossier).destroy(c)
|
2018-10-09 18:16:21 +02:00
|
|
|
|
else
|
2018-10-09 21:23:27 +02:00
|
|
|
|
fail "Unhandled source #{privacy_label}type de champ #{c.type_de_champ.order_place}"
|
2018-10-09 18:16:21 +02:00
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
|
2018-10-10 13:56:35 +02:00
|
|
|
|
private
|
2018-08-02 18:38:54 +02:00
|
|
|
|
|
|
|
|
|
def compute_new_champs(dossier)
|
|
|
|
|
@destination_champ_computations.each do |tdc, block|
|
2018-10-09 21:23:27 +02:00
|
|
|
|
champs(dossier) << block.call(dossier, tdc)
|
2018-08-02 18:38:54 +02:00
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
|
2018-10-10 13:56:35 +02:00
|
|
|
|
def destination_type_de_champ(champ)
|
|
|
|
|
@source_to_destination_mapping[champ.type_de_champ.order_place]
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
def discard_champ?(champ)
|
|
|
|
|
@source_champs_to_discard.member?(champ.type_de_champ.order_place)
|
|
|
|
|
end
|
2018-08-02 18:38:54 +02:00
|
|
|
|
|
2018-10-09 21:23:27 +02:00
|
|
|
|
def setup_mapping
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
def champs(dossier)
|
|
|
|
|
@is_private ? dossier.champs_private : dossier.champs
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
def types_de_champ(procedure)
|
|
|
|
|
@is_private ? procedure.types_de_champ_private : procedure.types_de_champ
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
def privacy_label
|
|
|
|
|
@is_private ? 'private ' : ''
|
|
|
|
|
end
|
|
|
|
|
|
2018-10-09 18:58:26 +02:00
|
|
|
|
def check_champs_consistency(label, expected_tdcs, actual_tdcs)
|
|
|
|
|
if actual_tdcs.size != expected_tdcs.size
|
|
|
|
|
raise "Incorrect #{label} size #{actual_tdcs.size} (expected #{expected_tdcs.size})"
|
|
|
|
|
end
|
|
|
|
|
actual_tdcs.each { |tdc| check_champ_consistency(label, expected_tdcs[tdc.order_place], tdc) }
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
def check_champ_consistency(label, expected_tdc, actual_tdc)
|
|
|
|
|
errors = []
|
|
|
|
|
if actual_tdc.libelle != expected_tdc['libelle']
|
|
|
|
|
errors.append("incorrect libelle #{actual_tdc.libelle} (expected #{expected_tdc['libelle']})")
|
|
|
|
|
end
|
|
|
|
|
if actual_tdc.type_champ != expected_tdc['type_champ']
|
|
|
|
|
errors.append("incorrect type champ #{actual_tdc.type_champ} (expected #{expected_tdc['type_champ']})")
|
|
|
|
|
end
|
|
|
|
|
if (!actual_tdc.mandatory) && expected_tdc['mandatory']
|
|
|
|
|
errors.append("champ should be mandatory")
|
|
|
|
|
end
|
|
|
|
|
drop_down = actual_tdc.drop_down_list.presence&.options&.presence
|
|
|
|
|
if drop_down != expected_tdc['drop_down']
|
|
|
|
|
errors.append("incorrect drop down list #{drop_down} (expected #{expected_tdc['drop_down']})")
|
|
|
|
|
end
|
|
|
|
|
if errors.present?
|
|
|
|
|
fail "On #{label} type de champ #{actual_tdc.order_place} (#{actual_tdc.libelle}) " + errors.join(', ')
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
|
2018-08-02 18:38:54 +02:00
|
|
|
|
def map_source_to_destination_champ(source_order_place, destination_order_place, source_overrides: {}, destination_overrides: {})
|
2018-10-09 21:23:27 +02:00
|
|
|
|
destination_type_de_champ = types_de_champ(@destination_procedure).find_by(order_place: destination_order_place)
|
2018-08-02 18:38:54 +02:00
|
|
|
|
@expected_source_types_de_champ[source_order_place] =
|
|
|
|
|
type_de_champ_to_expectation(destination_type_de_champ)
|
|
|
|
|
.merge!(source_overrides)
|
|
|
|
|
@expected_destination_types_de_champ[destination_order_place] =
|
2018-10-09 21:23:27 +02:00
|
|
|
|
type_de_champ_to_expectation(types_de_champ(@source_procedure).find_by(order_place: source_order_place))
|
2018-08-02 18:38:54 +02:00
|
|
|
|
.merge!({ "mandatory" => false }) # Even if the source was mandatory, it’s ok for the destination to be optional
|
|
|
|
|
.merge!(destination_overrides)
|
|
|
|
|
@source_to_destination_mapping[source_order_place] = destination_type_de_champ
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
def discard_source_champ(source_type_de_champ)
|
|
|
|
|
@expected_source_types_de_champ[source_type_de_champ.order_place] = type_de_champ_to_expectation(source_type_de_champ)
|
|
|
|
|
@source_champs_to_discard << source_type_de_champ.order_place
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
def compute_destination_champ(destination_type_de_champ, &block)
|
|
|
|
|
@expected_destination_types_de_champ[destination_type_de_champ.order_place] = type_de_champ_to_expectation(destination_type_de_champ)
|
2018-10-09 21:23:27 +02:00
|
|
|
|
@destination_champ_computations << [types_de_champ(@destination_procedure).find_by(order_place: destination_type_de_champ.order_place), block]
|
2018-08-02 18:38:54 +02:00
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
def type_de_champ_to_expectation(tdc)
|
|
|
|
|
if tdc.present?
|
2018-09-11 18:57:51 +02:00
|
|
|
|
expectation = tdc.as_json(only: [:libelle, :type_champ, :mandatory])
|
2018-08-02 18:38:54 +02:00
|
|
|
|
expectation['drop_down'] = tdc.drop_down_list.presence&.options&.presence
|
|
|
|
|
expectation
|
|
|
|
|
else
|
|
|
|
|
{}
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
|
2018-10-10 15:00:59 +02:00
|
|
|
|
class PieceJustificativeMapping
|
|
|
|
|
def initialize(source_procedure, destination_procedure)
|
|
|
|
|
@source_procedure = source_procedure
|
|
|
|
|
@destination_procedure = destination_procedure
|
|
|
|
|
|
|
|
|
|
@expected_source_pj = {}
|
|
|
|
|
@expected_destination_pj = {}
|
|
|
|
|
@source_to_destination_mapping = {}
|
|
|
|
|
|
|
|
|
|
setup_mapping
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
def check_source_destination_consistency
|
|
|
|
|
check_pjs_consistency('source', @expected_source_pj, @source_procedure.types_de_piece_justificative)
|
|
|
|
|
check_pjs_consistency('destination', @expected_destination_pj, @destination_procedure.types_de_piece_justificative)
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
def can_migrate?(dossier)
|
|
|
|
|
true
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
def migrate(dossier)
|
|
|
|
|
# Since we’re going to iterate and change the pjs at the same time,
|
|
|
|
|
# we use to_a to make the list static and avoid nasty surprises
|
|
|
|
|
original_pjs = dossier.pieces_justificatives.to_a
|
|
|
|
|
|
|
|
|
|
original_pjs.each do |pj|
|
|
|
|
|
pj_to = destination_pj(pj)
|
|
|
|
|
if pj_to.present?
|
|
|
|
|
pj.update_columns(type_de_piece_justificative_id: pj_to.id)
|
|
|
|
|
elsif discard_pj?(pj)
|
|
|
|
|
dossier.pieces_justificatives.destroy(pj)
|
|
|
|
|
else
|
|
|
|
|
fail "Unhandled source pièce justificative #{c.type_de_piece_justificative.order_place}"
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
private
|
|
|
|
|
|
|
|
|
|
def destination_pj(pj)
|
|
|
|
|
@source_to_destination_mapping[pj.order_place]
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
def discard_pj?(champ)
|
|
|
|
|
@source_pjs_to_discard.member?(pj.order_place)
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
def setup_mapping
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
def map_source_to_destination_pj(source_order_place, destination_order_place, source_overrides: {}, destination_overrides: {})
|
|
|
|
|
destination_pj = @destination_procedure.types_de_piece_justificative.find_by(order_place: destination_order_place)
|
|
|
|
|
@expected_source_pj[source_order_place] =
|
|
|
|
|
pj_to_expectation(destination_pj)
|
|
|
|
|
.merge!(source_overrides)
|
|
|
|
|
@expected_destination_pj[destination_order_place] =
|
|
|
|
|
pj_to_expectation(@source_procedure.types_de_piece_justificative.find_by(order_place: source_order_place))
|
|
|
|
|
.merge!({ "mandatory" => false }) # Even if the source was mandatory, it’s ok for the destination to be optional
|
|
|
|
|
.merge!(destination_overrides)
|
|
|
|
|
@source_to_destination_mapping[source_order_place] = destination_pj
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
def discard_source_pj(source_pj)
|
|
|
|
|
@expected_source_pj[source_pj.order_place] = pj_to_expectation(source_pj)
|
|
|
|
|
@source_pjs_to_discard << source_pj.order_place
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
def leave_destination_pj_blank(destination_pj)
|
|
|
|
|
@expected_destination_pj[destination_pj.order_place] = pj_to_expectation(destination_pj)
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
def pj_to_expectation(pj)
|
|
|
|
|
pj&.as_json(only: [:libelle, :mandatory]) || {}
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
def check_pjs_consistency(label, expected_pjs, actual_pjs)
|
|
|
|
|
if actual_pjs.size != expected_pjs.size
|
|
|
|
|
raise "Incorrect #{label} pièce justificative count #{actual_pjs.size} (expected #{expected_pjs.size})"
|
|
|
|
|
end
|
|
|
|
|
actual_pjs.each { |pj| check_pj_consistency(label, expected_pjs[pj.order_place], pj) }
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
def check_pj_consistency(label, expected_pj, actual_pj)
|
|
|
|
|
errors = []
|
|
|
|
|
if actual_pj.libelle != expected_pj['libelle']
|
|
|
|
|
errors.append("incorrect libelle #{actual_pj.libelle} (expected #{expected_pj['libelle']})")
|
|
|
|
|
end
|
|
|
|
|
if (!actual_pj.mandatory) && expected_pj['mandatory']
|
|
|
|
|
errors.append("pj should be mandatory")
|
|
|
|
|
end
|
|
|
|
|
if errors.present?
|
|
|
|
|
fail "On #{label} type de pièce justificative #{actual_pj.order_place} (#{actual_pj.libelle}) " + errors.join(', ')
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
def initialize(source_procedure, destination_procedure, champ_mapping, private_champ_mapping = ChampMapping, piece_justificative_mapping = PieceJustificativeMapping)
|
2018-08-02 18:38:54 +02:00
|
|
|
|
@source_procedure = source_procedure
|
|
|
|
|
@destination_procedure = destination_procedure
|
2018-10-09 21:23:27 +02:00
|
|
|
|
@champ_mapping = champ_mapping.new(source_procedure, destination_procedure, false)
|
|
|
|
|
@private_champ_mapping = private_champ_mapping.new(source_procedure, destination_procedure, true)
|
2018-10-10 15:00:59 +02:00
|
|
|
|
@piece_justificative_mapping = piece_justificative_mapping.new(source_procedure, destination_procedure)
|
2018-08-02 18:38:54 +02:00
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
def migrate_procedure
|
|
|
|
|
check_consistency
|
|
|
|
|
migrate_dossiers
|
|
|
|
|
migrate_gestionnaires
|
|
|
|
|
publish_destination_procedure_in_place_of_source
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
def check_consistency
|
|
|
|
|
check_same_administrateur
|
2018-10-10 15:00:59 +02:00
|
|
|
|
@champ_mapping.check_source_destination_consistency
|
|
|
|
|
@private_champ_mapping.check_source_destination_consistency
|
|
|
|
|
@piece_justificative_mapping.check_source_destination_consistency
|
2018-08-02 18:38:54 +02:00
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
def check_same_administrateur
|
|
|
|
|
if @source_procedure.administrateur != @destination_procedure.administrateur
|
|
|
|
|
raise "Mismatching administrateurs #{@source_procedure.administrateur&.email} → #{@destination_procedure.administrateur&.email}"
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
def migrate_dossiers
|
|
|
|
|
@source_procedure.dossiers.find_each(batch_size: 100) do |d|
|
2018-10-10 15:00:59 +02:00
|
|
|
|
if @champ_mapping.can_migrate?(d) && @private_champ_mapping.can_migrate?(d) && @piece_justificative_mapping.can_migrate?(d)
|
|
|
|
|
@champ_mapping.migrate(d)
|
|
|
|
|
@private_champ_mapping.migrate(d)
|
|
|
|
|
@piece_justificative_mapping.migrate(d)
|
2018-08-02 18:38:54 +02:00
|
|
|
|
|
2018-10-10 13:07:12 +02:00
|
|
|
|
# Use update_columns to avoid triggering build_default_champs
|
|
|
|
|
d.update_columns(procedure_id: @destination_procedure.id)
|
|
|
|
|
end
|
2018-08-02 18:38:54 +02:00
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
def migrate_gestionnaires
|
|
|
|
|
@source_procedure.gestionnaires.find_each(batch_size: 100) { |g| g.assign_to_procedure(@destination_procedure) }
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
def publish_destination_procedure_in_place_of_source
|
|
|
|
|
@destination_procedure.publish!(@source_procedure.path)
|
|
|
|
|
@source_procedure.archive
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
end
|