demarches-normaliennes/app/models/dossier.rb

1487 lines
51 KiB
Ruby
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

class Dossier < ApplicationRecord
self.ignored_columns += [:re_instructed_at]
include DossierCloneConcern
include DossierCorrectableConcern
include DossierFilteringConcern
include DossierPrefillableConcern
include DossierRebaseConcern
include DossierSearchableConcern
include DossierSectionsConcern
enum state: {
brouillon: 'brouillon',
en_construction: 'en_construction',
en_instruction: 'en_instruction',
accepte: 'accepte',
refuse: 'refuse',
sans_suite: 'sans_suite'
}
EN_CONSTRUCTION_OU_INSTRUCTION = [states.fetch(:en_construction), states.fetch(:en_instruction)]
TERMINE = [states.fetch(:accepte), states.fetch(:refuse), states.fetch(:sans_suite)]
INSTRUCTION_COMMENCEE = TERMINE + [states.fetch(:en_instruction)]
SOUMIS = EN_CONSTRUCTION_OU_INSTRUCTION + TERMINE
REMAINING_DAYS_BEFORE_CLOSING = 2
INTERVAL_BEFORE_CLOSING = "#{REMAINING_DAYS_BEFORE_CLOSING} days"
REMAINING_WEEKS_BEFORE_EXPIRATION = 2
INTERVAL_BEFORE_EXPIRATION = "#{REMAINING_WEEKS_BEFORE_EXPIRATION} weeks"
MONTHS_AFTER_EXPIRATION = 1
DAYS_AFTER_EXPIRATION = 5
INTERVAL_EXPIRATION = "#{MONTHS_AFTER_EXPIRATION} month #{DAYS_AFTER_EXPIRATION} days"
has_secure_token :prefill_token
has_one :etablissement, dependent: :destroy
has_one :individual, validate: false, dependent: :destroy
has_one :attestation, dependent: :destroy
# FIXME: some dossiers have more than one attestation
has_many :attestations, dependent: :destroy
has_one_attached :justificatif_motivation
has_many :champs
# We have to remove champs in a particular order - champs with a reference to a parent have to be
# removed first, otherwise we get a foreign key constraint error.
has_many :champs_to_destroy, -> { order(:parent_id) }, class_name: 'Champ', inverse_of: false, dependent: :destroy
has_many :champs_public, -> { root.public_ordered }, class_name: 'Champ', inverse_of: false
has_many :champs_private, -> { root.private_ordered }, class_name: 'Champ', inverse_of: false
has_many :champs_public_all, -> { public_only }, class_name: 'Champ', inverse_of: false
has_many :champs_private_all, -> { private_only }, class_name: 'Champ', inverse_of: false
has_many :prefilled_champs_public, -> { root.public_only.prefilled }, class_name: 'Champ', inverse_of: false
has_many :commentaires, inverse_of: :dossier, dependent: :destroy
has_many :preloaded_commentaires, -> { includes(:dossier_correction, piece_jointe_attachment: :blob) }, class_name: 'Commentaire', inverse_of: :dossier
has_many :invites, dependent: :destroy
has_many :follows, -> { active }, inverse_of: :dossier
has_many :previous_follows, -> { inactive }, class_name: 'Follow', inverse_of: :dossier
has_many :followers_instructeurs, through: :follows, source: :instructeur
has_many :previous_followers_instructeurs, -> { distinct }, through: :previous_follows, source: :instructeur
has_many :avis, inverse_of: :dossier, dependent: :destroy
has_many :experts, through: :avis
has_many :traitements, -> { order(:processed_at) }, inverse_of: :dossier, dependent: :destroy do
def passer_en_construction(instructeur: nil, processed_at: Time.zone.now)
build(state: Dossier.states.fetch(:en_construction),
instructeur_email: instructeur&.email,
processed_at:,
browser: Current.browser)
end
def submit_en_construction(processed_at: Time.zone.now)
build(state: Dossier.states.fetch(:en_construction),
processed_at:,
browser: Current.browser)
end
def passer_en_instruction(instructeur: nil, processed_at: Time.zone.now)
build(state: Dossier.states.fetch(:en_instruction),
instructeur_email: instructeur&.email,
processed_at:,
browser: Current.browser)
end
def accepter_automatiquement(processed_at: Time.zone.now)
build(state: Dossier.states.fetch(:accepte),
processed_at:)
end
def accepter(motivation: nil, instructeur: nil, processed_at: Time.zone.now)
build(state: Dossier.states.fetch(:accepte),
instructeur_email: instructeur&.email,
motivation:,
processed_at:,
browser: Current.browser)
end
def refuser(motivation: nil, instructeur: nil, processed_at: Time.zone.now)
build(state: Dossier.states.fetch(:refuse),
instructeur_email: instructeur&.email,
motivation:,
processed_at:,
browser: Current.browser)
end
def refuser_automatiquement(processed_at: Time.zone.now, motivation:)
build(state: Dossier.states.fetch(:refuse),
motivation:,
processed_at:)
end
def classer_sans_suite(motivation: nil, instructeur: nil, processed_at: Time.zone.now)
build(state: Dossier.states.fetch(:sans_suite),
instructeur_email: instructeur&.email,
motivation:,
processed_at:,
browser: Current.browser)
end
end
has_one :traitement, -> { order(processed_at: :desc) }, inverse_of: false
has_many :dossier_operation_logs, -> { order(:created_at) }, inverse_of: :dossier
has_many :dossier_assignments, -> { order(:assigned_at) }, inverse_of: :dossier, dependent: :destroy
has_one :dossier_assignment, -> { order(assigned_at: :desc) }, inverse_of: false
belongs_to :groupe_instructeur, optional: true
belongs_to :revision, class_name: 'ProcedureRevision', optional: false
belongs_to :user, optional: true
belongs_to :batch_operation, optional: true
has_many :dossier_batch_operations, dependent: :destroy
has_many :batch_operations, through: :dossier_batch_operations
has_one :france_connect_information, through: :user
has_one :procedure, through: :revision
has_one :attestation_template, through: :procedure
has_many :types_de_champ, through: :revision, source: :types_de_champ_public
has_many :types_de_champ_private, through: :revision
belongs_to :transfer, class_name: 'DossierTransfer', foreign_key: 'dossier_transfer_id', optional: true, inverse_of: :dossiers
has_many :transfer_logs, class_name: 'DossierTransferLog', dependent: :destroy
after_destroy_commit :log_destroy
accepts_nested_attributes_for :champs
accepts_nested_attributes_for :champs_public
accepts_nested_attributes_for :champs_private
accepts_nested_attributes_for :champs_public_all
accepts_nested_attributes_for :champs_private_all
accepts_nested_attributes_for :individual
include AASM
aasm whiny_persistence: true, column: :state, enum: true do
state :brouillon, initial: true
state :en_construction
state :en_instruction
state :accepte
state :refuse
state :sans_suite
event :passer_en_construction, after: :after_passer_en_construction do
transitions from: :brouillon, to: :en_construction
end
event :passer_en_instruction, after: :after_passer_en_instruction do
transitions from: :en_construction, to: :en_instruction
end
event :passer_automatiquement_en_instruction, after: :after_passer_automatiquement_en_instruction do
transitions from: :en_construction, to: :en_instruction, guard: :can_passer_automatiquement_en_instruction?
end
event :repasser_en_construction, after: :after_repasser_en_construction do
transitions from: :en_instruction, to: :en_construction, guard: :can_repasser_en_construction?
end
event :repasser_en_construction_with_pending_correction, after: :after_repasser_en_construction do
transitions from: :en_instruction, to: :en_construction
end
event :accepter, after: :after_accepter do
transitions from: :en_instruction, to: :accepte, guard: :can_terminer?
end
event :accepter_automatiquement, after: :after_accepter_automatiquement do
transitions from: :en_construction, to: :accepte, guard: :can_accepter_automatiquement?
transitions from: :en_instruction, to: :accepte, guard: :can_accepter_automatiquement?
end
event :refuser, after: :after_refuser do
transitions from: :en_instruction, to: :refuse, guard: :can_terminer?
end
event :refuser_automatiquement, after: :after_refuser_automatiquement do
transitions from: :en_instruction, to: :refuse, guard: :can_refuser_automatiquement?
end
event :classer_sans_suite, after: :after_classer_sans_suite do
transitions from: :en_instruction, to: :sans_suite, guard: :can_terminer?
end
event :repasser_en_instruction, after: :after_repasser_en_instruction do
transitions from: :refuse, to: :en_instruction, guard: :can_repasser_en_instruction?
transitions from: :sans_suite, to: :en_instruction, guard: :can_repasser_en_instruction?
transitions from: :accepte, to: :en_instruction, guard: :can_repasser_en_instruction?
end
end
scope :state_brouillon, -> { where(state: states.fetch(:brouillon)) }
scope :state_not_brouillon, -> { where.not(state: states.fetch(:brouillon)) }
scope :state_en_construction, -> { where(state: states.fetch(:en_construction)) }
scope :state_not_en_construction, -> { where.not(state: states.fetch(:en_construction)) }
scope :state_en_instruction, -> { where(state: states.fetch(:en_instruction)) }
scope :state_en_construction_ou_instruction, -> { where(state: EN_CONSTRUCTION_OU_INSTRUCTION) }
scope :state_instruction_commencee, -> { where(state: INSTRUCTION_COMMENCEE) }
scope :state_termine, -> { where(state: TERMINE) }
scope :state_not_termine, -> { where.not(state: TERMINE) }
scope :state_accepte, -> { where(state: states.fetch(:accepte)) }
scope :state_refuse, -> { where(state: states.fetch(:refuse)) }
scope :state_sans_suite, -> { where(state: states.fetch(:sans_suite)) }
scope :archived, -> { where(archived: true) }
scope :not_archived, -> { where(archived: false) }
scope :prefilled, -> { where(prefilled: true) }
scope :hidden_by_user, -> { where.not(hidden_by_user_at: nil) }
scope :hidden_by_administration, -> { where.not(hidden_by_administration_at: nil) }
scope :visible_by_user, -> { where(for_procedure_preview: false).where(hidden_by_user_at: nil, editing_fork_origin_id: nil) }
scope :visible_by_administration, -> {
state_not_brouillon
.where(hidden_by_administration_at: nil)
.merge(visible_by_user.or(state_not_en_construction))
}
scope :visible_by_user_or_administration, -> { visible_by_user.or(visible_by_administration) }
scope :hidden_for_administration, -> {
state_not_brouillon.hidden_by_administration.or(state_en_construction.hidden_by_user)
}
scope :for_procedure_preview, -> { where(for_procedure_preview: true) }
scope :for_editing_fork, -> { where.not(editing_fork_origin_id: nil) }
scope :for_groupe_instructeur, -> (groupe_instructeurs) { where(groupe_instructeur: groupe_instructeurs) }
scope :order_by_updated_at, -> (order = :desc) { order(updated_at: order, id: order) }
scope :order_by_created_at, -> (order = :asc) { order(depose_at: order, id: order) }
scope :updated_since, -> (since) { where('dossiers.updated_at >= ?', since) }
scope :created_since, -> (since) { where('dossiers.depose_at >= ?', since) }
scope :hidden_by_user_since, -> (since) { where('dossiers.hidden_by_user_at IS NOT NULL AND dossiers.hidden_by_user_at >= ?', since) }
scope :hidden_by_administration_since, -> (since) { where('dossiers.hidden_by_administration_at IS NOT NULL AND dossiers.hidden_by_administration_at >= ?', since) }
scope :hidden_since, -> (since) { hidden_by_user_since(since).or(hidden_by_administration_since(since)) }
scope :with_type_de_champ, -> (stable_id) {
joins('INNER JOIN champs ON champs.dossier_id = dossiers.id INNER JOIN types_de_champ ON types_de_champ.id = champs.type_de_champ_id')
.where('types_de_champ.private = FALSE AND types_de_champ.stable_id = ?', stable_id)
}
scope :with_type_de_champ_private, -> (stable_id) {
joins('INNER JOIN champs ON champs.dossier_id = dossiers.id INNER JOIN types_de_champ ON types_de_champ.id = champs.type_de_champ_id')
.where('types_de_champ.private = TRUE AND types_de_champ.stable_id = ?', stable_id)
}
scope :all_state, -> { not_archived.state_not_brouillon }
scope :en_construction, -> { not_archived.state_en_construction }
scope :en_instruction, -> { not_archived.state_en_instruction }
scope :termine, -> { not_archived.state_termine }
scope :processed_by_month, -> (all_groupe_instructeurs) {
state_termine
.where(groupe_instructeurs: all_groupe_instructeurs)
.group_by_period(:month, :processed_at, reverse: true)
}
scope :processed_in_month, -> (date) do
date = date.to_datetime
state_termine
.where(processed_at: date.all_month)
end
scope :ordered_for_export, -> {
order(depose_at: 'asc')
}
scope :en_cours, -> { not_archived.state_en_construction_ou_instruction }
scope :without_followers, -> { where.missing(:follows) }
scope :with_followers, -> { left_outer_joins(:follows).where.not(follows: { id: nil }) }
scope :with_champs, -> {
includes(champs_public: [
:type_de_champ,
:geo_areas,
piece_justificative_file_attachments: :blob,
champs: [:type_de_champ, piece_justificative_file_attachments: :blob]
])
}
scope :brouillons_recently_updated, -> { updated_since(2.days.ago).state_brouillon.order_by_updated_at }
scope :with_annotations, -> {
includes(champs_private: [
:type_de_champ,
:geo_areas,
piece_justificative_file_attachments: :blob,
champs: [:type_de_champ, piece_justificative_file_attachments: :blob]
])
}
scope :for_api, -> {
with_champs
.with_annotations
.includes(commentaires: { piece_jointe_attachment: :blob },
justificatif_motivation_attachment: :blob,
attestation: [],
avis: { piece_justificative_file_attachment: :blob },
traitement: [],
etablissement: [],
individual: [],
user: [])
}
scope :with_notifiable_procedure, -> (opts = { notify_on_closed: false }) do
states = opts[:notify_on_closed] ? [:publiee, :close, :depubliee] : [:publiee, :depubliee]
joins(:procedure)
.where(procedures: { aasm_state: states })
.where.not(user_id: nil)
end
scope :interval_brouillon_close_to_expiration, -> do
state_brouillon
.visible_by_user
.where("dossiers.created_at + dossiers.conservation_extension + (procedures.duree_conservation_dossiers_dans_ds * INTERVAL '1 month') - INTERVAL :expires_in < :now", { now: Time.zone.now, expires_in: INTERVAL_BEFORE_EXPIRATION })
end
scope :interval_en_construction_close_to_expiration, -> do
state_en_construction
.visible_by_user_or_administration
.where("dossiers.en_construction_at + dossiers.conservation_extension + (procedures.duree_conservation_dossiers_dans_ds * INTERVAL '1 month') - INTERVAL :expires_in < :now", { now: Time.zone.now, expires_in: INTERVAL_BEFORE_EXPIRATION })
end
scope :interval_termine_close_to_expiration, -> do
state_termine
.visible_by_user_or_administration
.where(procedures: { procedure_expires_when_termine_enabled: true })
.where("dossiers.processed_at + dossiers.conservation_extension + (procedures.duree_conservation_dossiers_dans_ds * INTERVAL '1 month') - INTERVAL :expires_in < :now", { now: Time.zone.now, expires_in: INTERVAL_BEFORE_EXPIRATION })
end
scope :brouillon_close_to_expiration, -> do
joins(:procedure).interval_brouillon_close_to_expiration
end
scope :en_construction_close_to_expiration, -> do
joins(:procedure).interval_en_construction_close_to_expiration
end
scope :termine_close_to_expiration, -> do
joins(:procedure).interval_termine_close_to_expiration
end
scope :close_to_expiration, -> do
joins(:procedure).scoping do
interval_brouillon_close_to_expiration
.or(interval_en_construction_close_to_expiration)
.or(interval_termine_close_to_expiration)
end
end
scope :termine_or_en_construction_close_to_expiration, -> do
joins(:procedure).scoping do
interval_en_construction_close_to_expiration
.or(interval_termine_close_to_expiration)
end
end
scope :brouillon_expired, -> do
state_brouillon
.visible_by_user
.where("brouillon_close_to_expiration_notice_sent_at + INTERVAL :expires_in < :now", { now: Time.zone.now, expires_in: INTERVAL_EXPIRATION })
end
scope :en_construction_expired, -> do
state_en_construction
.visible_by_user_or_administration
.where("en_construction_close_to_expiration_notice_sent_at + INTERVAL :expires_in < :now", { now: Time.zone.now, expires_in: INTERVAL_EXPIRATION })
end
scope :termine_expired, -> do
state_termine
.visible_by_user_or_administration
.where("termine_close_to_expiration_notice_sent_at + INTERVAL :expires_in < :now", { now: Time.zone.now, expires_in: INTERVAL_EXPIRATION })
end
scope :without_brouillon_expiration_notice_sent, -> { where(brouillon_close_to_expiration_notice_sent_at: nil) }
scope :without_en_construction_expiration_notice_sent, -> { where(en_construction_close_to_expiration_notice_sent_at: nil) }
scope :without_termine_expiration_notice_sent, -> { where(termine_close_to_expiration_notice_sent_at: nil) }
scope :deleted_by_user_expired, -> { where('dossiers.hidden_by_user_at < ?', 1.week.ago) }
scope :deleted_by_administration_expired, -> { where('dossiers.hidden_by_administration_at < ?', 1.week.ago) }
scope :en_brouillon_expired_to_delete, -> { state_brouillon.deleted_by_user_expired }
scope :en_construction_expired_to_delete, -> { state_en_construction.deleted_by_user_expired }
scope :termine_expired_to_delete, -> { state_termine.deleted_by_user_expired.deleted_by_administration_expired }
scope :brouillon_near_procedure_closing_date, -> do
# select users who have submitted dossier for the given 'procedures.id'
users_who_submitted =
state_not_brouillon
.visible_by_user
.joins(:revision)
.where("procedure_revisions.procedure_id = procedures.id")
.select(:user_id)
# select dossier in brouillon where procedure closes in two days and for which the user has not submitted a Dossier
state_brouillon
.visible_by_user
.with_notifiable_procedure
.where("procedures.auto_archive_on - INTERVAL :before_closing = :now", { now: Time.zone.today, before_closing: INTERVAL_BEFORE_CLOSING })
.where.not(user: users_who_submitted)
end
scope :for_api_v2, -> { includes(:attestation_template, revision: [procedure: [:administrateurs]], etablissement: [], individual: [], traitement: []) }
scope :with_notifications, -> do
joins(:follows)
.where('last_champ_updated_at > follows.demande_seen_at' \
' OR identity_updated_at > follows.demande_seen_at' \
' OR groupe_instructeur_updated_at > follows.demande_seen_at' \
' OR last_champ_private_updated_at > follows.annotations_privees_seen_at' \
' OR last_avis_updated_at > follows.avis_seen_at' \
' OR last_commentaire_updated_at > follows.messagerie_seen_at')
.distinct
end
scope :by_statut, -> (statut, instructeur = nil) do
case statut
when 'a-suivre'
visible_by_administration
.without_followers
.en_cours
when 'suivis'
instructeur
.followed_dossiers
.merge(visible_by_administration)
.en_cours
when 'traites'
visible_by_administration.termine
when 'tous'
visible_by_administration.all_state
when 'supprimes_recemment'
hidden_by_administration.termine
when 'archives'
visible_by_administration.archived
when 'expirant'
visible_by_administration.termine_or_en_construction_close_to_expiration
end
end
scope :not_having_batch_operation, -> { where(batch_operation_id: nil) }
delegate :siret, :siren, to: :etablissement, allow_nil: true
delegate :france_connect_information, to: :user, allow_nil: true
before_save :build_default_champs_for_new_dossier, if: Proc.new { revision_id_was.nil? && parent_dossier_id.nil? && editing_fork_origin_id.nil? }
after_save :send_web_hook
validates :user, presence: true, if: -> { deleted_user_email_never_send.nil? }, unless: -> { prefilled }
validates :individual, presence: true, if: -> { revision.procedure.for_individual? }
validates_associated :prefilled_champs_public, on: :champs_public_value
def types_de_champ_public
types_de_champ
end
def self.downloadable_sorted_batch
DossierPreloader.new(includes(
:user,
:individual,
:followers_instructeurs,
:traitement,
:groupe_instructeur,
:etablissement,
:pending_corrections,
procedure: [:groupe_instructeurs],
avis: [:claimant, :expert]
).ordered_for_export).in_batches
end
def user_deleted?
persisted? && user_id.nil?
end
def user_email_for(use)
if user_deleted?
if use == :display
deleted_user_email_never_send
else
raise "Can not send email to discarded user"
end
else
user.email
end
end
def motivation
if termine?
traitement&.motivation || read_attribute(:motivation)
end
end
def build_default_champs_for_new_dossier
revision.build_champs_public.each do |champ|
champs_public << champ
end
revision.build_champs_private.each do |champ|
champs_private << champ
end
champs_public.filter { _1.repetition? && _1.mandatory? }.each do |champ|
champ.add_row(revision)
end
champs_private.filter(&:repetition?).each do |champ|
champ.add_row(revision)
end
end
def build_default_individual
if procedure.for_individual? && individual.blank?
self.individual = if france_connect_information.present?
Individual.from_france_connect(france_connect_information)
else
Individual.new
end
end
end
def en_construction_ou_instruction?
EN_CONSTRUCTION_OU_INSTRUCTION.include?(state)
end
def termine?
TERMINE.include?(state)
end
def instruction_commencee?
INSTRUCTION_COMMENCEE.include?(state)
end
def read_only?
en_instruction? || accepte? || refuse? || sans_suite? || procedure.discarded? || procedure.close? && brouillon?
end
def can_transition_to_en_construction?
brouillon? && procedure.dossier_can_transition_to_en_construction? && !for_procedure_preview? && !editing_fork?
end
def can_terminer?
return false if any_etablissement_as_degraded_mode?
true
end
def can_accepter_automatiquement?
return false unless can_terminer?
return true if declarative_triggered_at.nil? && procedure.declarative_accepte? && en_construction?
return true if procedure.sva? && can_terminer_automatiquement_by_sva_svr?
false
end
def can_refuser_automatiquement?
return false unless can_terminer?
return true if procedure.svr? && can_terminer_automatiquement_by_sva_svr?
false
end
def can_passer_automatiquement_en_instruction?
return true if declarative_triggered_at.nil? && procedure.declarative_en_instruction?
return true if procedure.auto_archive_on? && !procedure.auto_archive_on.future?
return true if procedure.sva_svr_enabled? && sva_svr_decision_triggered_at.nil? && !pending_correction?
false
end
def can_repasser_en_construction?
!procedure.sva_svr_enabled?
end
def can_repasser_en_instruction?
termine? && !user_deleted?
end
def can_be_updated_by_user?
brouillon? || en_construction?
end
def can_be_deleted_by_user?
brouillon? || en_construction? || termine?
end
def can_be_deleted_by_administration?(reason)
termine? || reason == :procedure_removed
end
def can_terminer_automatiquement_by_sva_svr?
sva_svr_decision_triggered_at.nil? && !pending_correction? && (sva_svr_decision_on.today? || sva_svr_decision_on.past?)
end
def any_etablissement_as_degraded_mode?
return true if etablissement&.as_degraded_mode?
return true if champs_public_all.any? { _1.etablissement&.as_degraded_mode? }
false
end
def messagerie_available?
visible_by_administration? && !hidden_by_user? && !user_deleted? && !archived
end
def expirable?
[
brouillon?,
en_construction?,
termine? && procedure.procedure_expires_when_termine_enabled
].any?
end
def expiration_date_reference
if brouillon?
created_at
elsif en_construction?
en_construction_at
elsif termine?
processed_at
else
fail "expiration_date_reference should not be called in state #{self.state}"
end
end
def expiration_date_with_extension
expiration_date_reference + conservation_extension + procedure.duree_conservation_dossiers_dans_ds.months
end
def expiration_notification_date
expiration_date_with_extension - REMAINING_WEEKS_BEFORE_EXPIRATION.weeks
end
def close_to_expiration?
return false if en_instruction?
expiration_notification_date < Time.zone.now
end
def after_notification_expiration_date
if brouillon? && brouillon_close_to_expiration_notice_sent_at.present?
brouillon_close_to_expiration_notice_sent_at + duration_after_notice
elsif en_construction? && en_construction_close_to_expiration_notice_sent_at.present?
en_construction_close_to_expiration_notice_sent_at + duration_after_notice
elsif termine? && termine_close_to_expiration_notice_sent_at.present?
termine_close_to_expiration_notice_sent_at + duration_after_notice
end
end
def expiration_date
after_notification_expiration_date.presence || expiration_date_with_extension
end
def duration_after_notice
MONTHS_AFTER_EXPIRATION.month + DAYS_AFTER_EXPIRATION.days
end
def expiration_can_be_extended?
brouillon? || en_construction?
end
def extend_conservation(conservation_extension)
update(conservation_extension: self.conservation_extension + conservation_extension,
brouillon_close_to_expiration_notice_sent_at: nil,
en_construction_close_to_expiration_notice_sent_at: nil,
termine_close_to_expiration_notice_sent_at: nil)
end
def show_procedure_state_warning?
procedure.discarded? || (brouillon? && !procedure.dossier_can_transition_to_en_construction?)
end
def assign_to_groupe_instructeur(groupe_instructeur, mode, author = nil)
return if groupe_instructeur.present? && groupe_instructeur.procedure != procedure
return if self.groupe_instructeur == groupe_instructeur
previous_groupe_instructeur = self.groupe_instructeur
track_assigned_dossier_without_groupe_instructeur if groupe_instructeur.nil?
update!(groupe_instructeur:, groupe_instructeur_updated_at: Time.zone.now)
update!(forced_groupe_instructeur: true) if mode == DossierAssignment.modes.fetch(:manual)
create_assignment(mode, previous_groupe_instructeur, groupe_instructeur, author&.email)
if !brouillon?
unfollow_stale_instructeurs
if author.present?
log_dossier_operation(author, :changer_groupe_instructeur, self)
end
end
end
def archiver!(instructeur)
update!(archived: true, archived_at: Time.zone.now, archived_by: instructeur.email)
end
def desarchiver!
update!(archived: false, archived_at: nil, archived_by: nil)
end
def text_summary
if brouillon?
parts = [
"Dossier en brouillon répondant à la démarche ",
procedure.libelle,
" gérée par l'organisme ",
procedure.organisation_name
]
else
parts = [
"Dossier déposé le ",
depose_at.strftime("%d/%m/%Y"),
" sur la démarche ",
procedure.libelle,
" gérée par l'organisme ",
procedure.organisation_name
]
end
parts.join
end
def duree_totale_conservation_in_months
procedure.duree_conservation_dossiers_dans_ds + (conservation_extension / 1.month.to_i)
end
def avis_for_instructeur(instructeur)
if instructeur.dossiers.include?(self)
avis.order(created_at: :asc)
else
avis
.where(confidentiel: false)
.or(avis.where(claimant: instructeur))
.order(created_at: :asc)
end
end
def avis_for_expert(expert)
Avis
.where(dossier_id: id, confidentiel: false)
.or(Avis.where(id: expert.avis, dossier_id: id))
.order(created_at: :asc)
end
def owner_name
if etablissement.present?
etablissement.entreprise_raison_sociale
elsif individual.present?
"#{individual.nom} #{individual.prenom}"
end
end
def orphan?
prefilled? && user.nil?
end
def owned_by?(a_user)
return false if a_user.nil?
return false if orphan?
user == a_user
end
def log_operations?
!procedure.brouillon? && !brouillon?
end
def hidden_by_user?
hidden_by_user_at.present?
end
def hidden_by_administration?
hidden_by_administration_at.present?
end
def hidden_for_administration?
hidden_by_administration? || (hidden_by_user? && en_construction?) || brouillon?
end
def visible_by_administration?
!hidden_for_administration?
end
def hidden_for_administration_and_user?
hidden_for_administration? && hidden_by_user?
end
def expose_legacy_carto_api?
procedure.expose_legacy_carto_api?
end
def geo_position
if etablissement.present?
point = Geocoder.search(etablissement.geo_adresse).first
end
lon = Champs::CarteChamp::DEFAULT_LON.to_s
lat = Champs::CarteChamp::DEFAULT_LAT.to_s
zoom = "13"
if point.present?
lat, lon = point.coordinates.map(&:to_s)
end
{ lon: lon, lat: lat, zoom: zoom }
end
def unspecified_attestation_champs
if attestation_template&.activated?
attestation_template.unspecified_champs_for_dossier(self)
else
[]
end
end
def build_attestation
if attestation_template&.activated?
attestation_template.attestation_for(self)
end
end
def expired_keep_track_and_destroy!
transaction do
DeletedDossier.create_from_dossier(self, :expired)
log_automatic_dossier_operation(:supprimer, self)
dossier_operation_logs.purge_discarded
destroy!
end
true
rescue
false
end
def author_is_user(author)
author.is_a?(User)
end
def author_is_administration(author)
author.is_a?(Instructeur) || author.is_a?(Administrateur) || author.is_a?(SuperAdmin)
end
def hide_and_keep_track!(author, reason)
transaction do
if author_is_administration(author) && can_be_deleted_by_administration?(reason)
update(hidden_by_administration_at: Time.zone.now, hidden_by_reason: reason)
elsif author_is_user(author) && can_be_deleted_by_user?
update(hidden_by_user_at: Time.zone.now, dossier_transfer_id: nil, hidden_by_reason: reason)
else
raise "Unauthorized dossier hide attempt Dossier##{id} by #{author} for reason #{reason}"
end
log_dossier_operation(author, :supprimer, self)
end
if en_construction? && !hidden_by_administration?
administration_emails = followers_instructeurs.present? ? followers_instructeurs.map(&:email) : procedure.administrateurs.map(&:email)
administration_emails.each do |email|
DossierMailer.notify_en_construction_deletion_to_administration(self, email).deliver_later
end
end
end
def restore(author)
transaction do
if author_is_administration(author)
update(hidden_by_administration_at: nil)
elsif author_is_user(author)
update(hidden_by_user_at: nil)
end
if !hidden_by_user? && !hidden_by_administration?
update(hidden_by_reason: nil)
end
log_dossier_operation(author, :restaurer, self)
end
end
def mail_template_for_state
procedure.mail_template_for(self)
end
def after_passer_en_construction
self.conservation_extension = 0.days
self.depose_at = self.en_construction_at = self.traitements
.passer_en_construction
.processed_at
save!
MailTemplatePresenterService.create_commentaire_for_state(self)
NotificationMailer.send_en_construction_notification(self).deliver_later
procedure.compute_dossiers_count
RoutingEngine.compute(self)
end
def submit_en_construction!(pending_correction_confirm: false)
self.traitements.submit_en_construction
save!
RoutingEngine.compute(self)
if pending_correction_confirm
resolve_pending_correction!
process_sva_svr!
end
end
def after_passer_en_instruction(h)
instructeur = h[:instructeur]
disable_notification = h.fetch(:disable_notification, false)
instructeur.follow(self)
self.en_construction_close_to_expiration_notice_sent_at = nil
self.conservation_extension = 0.days
self.en_instruction_at = self.traitements
.passer_en_instruction(instructeur: instructeur)
.processed_at
save!
resolve_pending_correction!
MailTemplatePresenterService.create_commentaire_for_state(self)
if !disable_notification
NotificationMailer.send_en_instruction_notification(self).deliver_later
end
log_dossier_operation(instructeur, :passer_en_instruction)
end
def after_passer_automatiquement_en_instruction
self.en_construction_close_to_expiration_notice_sent_at = nil
self.conservation_extension = 0.days
self.en_instruction_at = traitements.passer_en_instruction.processed_at
if procedure.declarative_en_instruction?
self.declarative_triggered_at = en_instruction_at
end
save!
MailTemplatePresenterService.create_commentaire_for_state(self)
NotificationMailer.send_en_instruction_notification(self).deliver_later
if procedure.sva_svr_enabled?
# TODO: handle serialization errors when SIRET demandeur was not completed
log_automatic_dossier_operation(:passer_en_instruction, self)
else
log_automatic_dossier_operation(:passer_en_instruction)
end
end
def after_repasser_en_construction(h)
instructeur = h[:instructeur]
create_missing_traitemets
self.en_construction_close_to_expiration_notice_sent_at = nil
self.conservation_extension = 0.days
self.en_construction_at = self.traitements
.passer_en_construction(instructeur: instructeur)
.processed_at
save!
log_dossier_operation(instructeur, :repasser_en_construction)
end
def after_repasser_en_instruction(h)
instructeur = h[:instructeur]
disable_notification = h.fetch(:disable_notification, false)
create_missing_traitemets
self.hidden_by_user_at = nil
self.archived = false
self.termine_close_to_expiration_notice_sent_at = nil
self.conservation_extension = 0.days
self.en_instruction_at = self.traitements
.passer_en_instruction(instructeur: instructeur)
.processed_at
attestation&.destroy
self.sva_svr_decision_on = nil
self.motivation = nil
self.justificatif_motivation.purge_later
save!
rebase_later
MailTemplatePresenterService.create_commentaire_for_state(self)
if !disable_notification
NotificationMailer.send_repasser_en_instruction_notification(self).deliver_later
end
log_dossier_operation(instructeur, :repasser_en_instruction)
end
def after_accepter(h)
instructeur = h[:instructeur]
motivation = h[:motivation]
justificatif = h[:justificatif]
disable_notification = h.fetch(:disable_notification, false)
self.processed_at = self.traitements
.accepter(motivation: motivation, instructeur: instructeur)
.processed_at
save!
if justificatif
self.justificatif_motivation.attach(justificatif)
end
if attestation.nil?
self.attestation = build_attestation
end
save!
remove_titres_identite!
MailTemplatePresenterService.create_commentaire_for_state(self)
if !disable_notification
NotificationMailer.send_accepte_notification(self).deliver_later
end
send_dossier_decision_to_experts(self)
log_dossier_operation(instructeur, :accepter, self)
end
def after_accepter_automatiquement
self.processed_at = traitements.accepter_automatiquement.processed_at
if procedure.declarative_accepte?
self.en_instruction_at = self.processed_at
self.declarative_triggered_at = self.processed_at
elsif procedure.sva?
self.sva_svr_decision_triggered_at = self.processed_at
end
save!
if attestation.nil?
self.attestation = build_attestation
end
save!
remove_titres_identite!
MailTemplatePresenterService.create_commentaire_for_state(self)
NotificationMailer.send_accepte_notification(self).deliver_later
log_automatic_dossier_operation(:accepter, self)
end
def after_refuser(h)
instructeur = h[:instructeur]
motivation = h[:motivation]
justificatif = h[:justificatif]
disable_notification = h.fetch(:disable_notification, false)
self.processed_at = self.traitements
.refuser(motivation: motivation, instructeur: instructeur)
.processed_at
save!
if justificatif
self.justificatif_motivation.attach(justificatif)
end
save!
remove_titres_identite!
MailTemplatePresenterService.create_commentaire_for_state(self)
if !disable_notification
NotificationMailer.send_refuse_notification(self).deliver_later
end
send_dossier_decision_to_experts(self)
log_dossier_operation(instructeur, :refuser, self)
end
def after_refuser_automatiquement
# Only SVR can refuse automatically
I18n.with_locale(user.locale || I18n.default_locale) do
self.motivation = I18n.t("shared.dossiers.motivation.refused_by_svr")
end
self.processed_at = traitements.refuser_automatiquement(motivation:).processed_at
self.sva_svr_decision_triggered_at = self.processed_at
save!
remove_titres_identite!
MailTemplatePresenterService.create_commentaire_for_state(self)
NotificationMailer.send_refuse_notification(self).deliver_later
log_automatic_dossier_operation(:refuser, self)
end
def after_classer_sans_suite(h)
instructeur = h[:instructeur]
motivation = h[:motivation]
justificatif = h[:justificatif]
disable_notification = h.fetch(:disable_notification, false)
self.processed_at = self.traitements
.classer_sans_suite(motivation: motivation, instructeur: instructeur)
.processed_at
save!
if justificatif
self.justificatif_motivation.attach(justificatif)
end
save!
remove_titres_identite!
MailTemplatePresenterService.create_commentaire_for_state(self)
if !disable_notification
NotificationMailer.send_sans_suite_notification(self).deliver_later
end
send_dossier_decision_to_experts(self)
log_dossier_operation(instructeur, :classer_sans_suite, self)
end
def process_declarative!
if procedure.declarative_accepte? && may_accepter_automatiquement?
accepter_automatiquement!
elsif procedure.declarative_en_instruction? && may_passer_automatiquement_en_instruction?
passer_automatiquement_en_instruction!
end
end
def process_sva_svr!
return unless procedure.sva_svr_enabled?
return if sva_svr_decision_triggered_at.present?
# set or recompute sva date, except for dossiers submitted before sva was enabled
if depose_at.today? || sva_svr_decision_on.present?
self.sva_svr_decision_on = SVASVRDecisionDateCalculatorService.new(self, procedure).decision_date
end
return if sva_svr_decision_on.nil?
if en_construction? && may_passer_automatiquement_en_instruction?
passer_automatiquement_en_instruction!
elsif en_instruction? && procedure.sva? && may_accepter_automatiquement?
accepter_automatiquement!
elsif en_instruction? && procedure.svr? && may_refuser_automatiquement?
refuser_automatiquement!
elsif will_save_change_to_sva_svr_decision_on?
save! # we always want the most up to date decision when there is a pending correction
end
end
def previously_termine?
traitements.termine.exists?
end
def remove_titres_identite!
champs_public.filter(&:titre_identite?).map(&:piece_justificative_file).each(&:purge_later)
end
def check_mandatory_and_visible_champs
(champs_public + champs_public.filter(&:block?).filter(&:visible?).flat_map(&:champs))
.filter(&:visible?)
.filter(&:mandatory_blank?)
.map do |champ|
champ.errors.add(:value, :missing)
end
end
def demander_un_avis!(avis)
log_dossier_operation(avis.claimant, :demander_un_avis, avis)
end
def spreadsheet_columns_csv(types_de_champ:)
spreadsheet_columns(with_etablissement: true, types_de_champ: types_de_champ)
end
def spreadsheet_columns_xlsx(types_de_champ:)
spreadsheet_columns(types_de_champ: types_de_champ)
end
def spreadsheet_columns_ods(types_de_champ:)
spreadsheet_columns(types_de_champ: types_de_champ)
end
def spreadsheet_columns(with_etablissement: false, types_de_champ:)
columns = [
['ID', id.to_s],
['Email', user_email_for(:display)]
]
if procedure.for_individual?
columns += [
['Civilité', individual&.gender],
['Nom', individual&.nom],
['Prénom', individual&.prenom]
]
if procedure.ask_birthday
columns += [['Date de naissance', individual&.birthdate]]
end
elsif with_etablissement
columns += [
['Établissement SIRET', etablissement&.siret],
['Établissement siège social', etablissement&.siege_social],
['Établissement NAF', etablissement&.naf],
['Établissement libellé NAF', etablissement&.libelle_naf],
['Établissement Adresse', etablissement&.adresse],
['Établissement numero voie', etablissement&.numero_voie],
['Établissement type voie', etablissement&.type_voie],
['Établissement nom voie', etablissement&.nom_voie],
['Établissement complément adresse', etablissement&.complement_adresse],
['Établissement code postal', etablissement&.code_postal],
['Établissement localité', etablissement&.localite],
['Établissement code INSEE localité', etablissement&.code_insee_localite],
['Entreprise SIREN', etablissement&.entreprise_siren],
['Entreprise capital social', etablissement&.entreprise_capital_social],
['Entreprise numero TVA intracommunautaire', etablissement&.entreprise_numero_tva_intracommunautaire],
['Entreprise forme juridique', etablissement&.entreprise_forme_juridique],
['Entreprise forme juridique code', etablissement&.entreprise_forme_juridique_code],
['Entreprise nom commercial', etablissement&.entreprise_nom_commercial],
['Entreprise raison sociale', etablissement&.entreprise_raison_sociale],
['Entreprise SIRET siège social', etablissement&.entreprise_siret_siege_social],
['Entreprise code effectif entreprise', etablissement&.entreprise_code_effectif_entreprise],
['Entreprise date de création', etablissement&.entreprise_date_creation],
['Entreprise état administratif', etablissement&.entreprise_etat_administratif],
['Entreprise nom', etablissement&.entreprise_nom],
['Entreprise prénom', etablissement&.entreprise_prenom],
['Association RNA', etablissement&.association_rna],
['Association titre', etablissement&.association_titre],
['Association objet', etablissement&.association_objet],
['Association date de création', etablissement&.association_date_creation],
['Association date de déclaration', etablissement&.association_date_declaration],
['Association date de publication', etablissement&.association_date_publication]
]
else
columns << ['Entreprise raison sociale', etablissement&.entreprise_raison_sociale]
end
if procedure.chorusable? && procedure.chorus.complete?
columns += [
['Domaine Fonctionnel', procedure.chorus_configuration.domaine_fonctionnel.code],
['Référentiel De Programmation', procedure.chorus_configuration.referentiel_de_programmation.code],
['Centre De Coup', procedure.chorus_configuration.centre_de_cout.code]
]
end
columns += [
['Archivé', :archived],
['État du dossier', Dossier.human_attribute_name("state.#{state}")],
['Dernière mise à jour le', :updated_at],
['Déposé le', :depose_at],
['Passé en instruction le', :en_instruction_at],
procedure.sva_svr_enabled? ? ["Date décision #{procedure.sva_svr_configuration.human_decision}", :sva_svr_decision_on] : nil,
['Traité le', :processed_at],
['Motivation de la décision', :motivation],
['Instructeurs', followers_instructeurs.map(&:email).join(' ')]
].compact
if procedure.routing_enabled?
columns << ['Groupe instructeur', groupe_instructeur.label]
end
columns + self.class.champs_for_export(champs_public + champs_private, types_de_champ)
end
# Get all the champs values for the types de champ in the final list.
# Dossier might not have corresponding champ display nil.
# To do so, we build a virtual champ when there is no value so we can call for_export with all indexes
def self.champs_for_export(champs, types_de_champ)
types_de_champ.flat_map do |type_de_champ|
champ = champs.find { |champ| champ.stable_id == type_de_champ.stable_id }
exported_values = if champ.nil? || !champ.visible?
# some champs export multiple columns
# ex: commune.for_export => [commune, insee, departement]
# so we build a fake champ to have the right export
type_de_champ.champ.build.for_export
else
champ.for_export
end
# nil => [nil]
# text => [text]
# [commune, insee, departement] => [commune, insee, departement]
wrapped_exported_values = [exported_values].flatten
wrapped_exported_values.map.with_index do |champ_value, index|
[type_de_champ.libelle_for_export(index), champ_value]
end
end
end
def linked_dossiers_for(instructeur_or_expert)
dossier_ids = champs_public.filter(&:dossier_link?).filter_map(&:value)
instructeur_or_expert.dossiers.where(id: dossier_ids)
end
def hash_for_deletion_mail
{ id: self.id, procedure_libelle: self.procedure.libelle }
end
def geo_data?
GeoArea.exists?(champ_id: champs_public.ids + champs_private.ids)
end
def to_feature_collection
{
type: 'FeatureCollection',
id: id,
bbox: bounding_box,
features: geo_areas.map(&:to_feature)
}
end
def self.to_feature_collection
{
type: 'FeatureCollection',
features: GeoArea.joins(:champ).where(champ: { dossier: ids }).map(&:to_feature)
}
end
def log_api_entreprise_job_exception(exception)
exceptions = self.api_entreprise_job_exceptions ||= []
exceptions << exception.inspect
update_column(:api_entreprise_job_exceptions, exceptions)
end
def user_locale
user&.locale || I18n.default_locale
end
def purge_discarded
transaction do
DeletedDossier.create_from_dossier(self, hidden_by_reason)
dossier_operation_logs.purge_discarded
destroy
end
end
def self.purge_discarded
en_brouillon_expired_to_delete.find_each(&:purge_discarded)
en_construction_expired_to_delete.find_each(&:purge_discarded)
termine_expired_to_delete.find_each(&:purge_discarded)
end
def skip_user_notification_email?
return true if brouillon? && procedure.declarative?
return true if for_procedure_preview?
return true if user_deleted?
false
end
def sva_svr_decision_in_days
(sva_svr_decision_on - Date.current).to_i
end
def create_assignment(mode, previous_groupe_instructeur, groupe_instructeur, instructeur_email = nil)
DossierAssignment.create!(
dossier_id: self.id,
mode: mode,
previous_groupe_instructeur_id: previous_groupe_instructeur&.id,
groupe_instructeur_id: groupe_instructeur.id,
previous_groupe_instructeur_label: previous_groupe_instructeur&.label,
groupe_instructeur_label: groupe_instructeur.label,
assigned_at: Time.zone.now,
assigned_by: instructeur_email
)
end
def service
groupe_instructeur&.contact_information || procedure.service
end
private
def create_missing_traitemets
if en_construction_at.present? && traitements.en_construction.empty?
self.traitements.passer_en_construction(processed_at: en_construction_at)
self.depose_at ||= en_construction_at
end
if en_instruction_at.present? && traitements.en_instruction.empty?
self.traitements.passer_en_instruction(processed_at: en_instruction_at)
end
end
def deleted_dossier
@deleted_dossier ||= DeletedDossier.find_by(dossier_id: id)
end
def defaut_groupe_instructeur?
groupe_instructeur == procedure.defaut_groupe_instructeur
end
def geo_areas
champs_public.flat_map(&:geo_areas) + champs_private.flat_map(&:geo_areas)
end
def bounding_box
GeojsonService.bbox(type: 'FeatureCollection', features: geo_areas.map(&:to_feature))
end
def log_dossier_operation(author, operation, subject = nil)
if log_operations?
DossierOperationLog.create_and_serialize(
dossier: self,
operation: DossierOperationLog.operations.fetch(operation),
author: author,
subject: subject
)
end
end
def log_automatic_dossier_operation(operation, subject = nil)
if log_operations?
DossierOperationLog.create_and_serialize(
dossier: self,
operation: DossierOperationLog.operations.fetch(operation),
automatic_operation: true,
subject: subject
)
end
end
def send_web_hook
if saved_change_to_state? && !brouillon? && procedure.web_hook_url.present?
WebHookJob.perform_later(
procedure.id,
self.id,
self.state,
self.updated_at
)
end
end
def unfollow_stale_instructeurs
followers_instructeurs.each do |instructeur|
if instructeur.groupe_instructeurs.exclude?(groupe_instructeur)
instructeur.unfollow(self)
if visible_by_administration?
DossierMailer.notify_groupe_instructeur_changed(instructeur, self).deliver_later
end
end
end
end
def self.notify_draft_not_submitted
brouillon_near_procedure_closing_date
.find_each do |dossier|
DossierMailer.notify_brouillon_not_submitted(dossier).deliver_later
end
end
def send_dossier_decision_to_experts(dossier)
avis_experts_procedures_ids = Avis
.joins(:experts_procedure)
.where(dossier: dossier, experts_procedures: { allow_decision_access: true })
.with_answer
.distinct
.pluck('avis.id, experts_procedures.id')
# rubocop:disable Lint/UnusedBlockArgument
avis = avis_experts_procedures_ids
.uniq { |(avis_id, experts_procedures_id)| experts_procedures_id }
.map { |(avis_id, _)| avis_id }
.then { |avis_ids| Avis.find(avis_ids) }
# rubocop:enable Lint/UnusedBlockArgument
avis.each { |a| ExpertMailer.send_dossier_decision_v2(a).deliver_later }
end
def log_destroy
app_traces = caller.reject { _1.match?(%r{/ruby/.+/gems/}) }.map { _1.sub(Rails.root.to_s, "") }
payload = {
message: "Dossier destroyed",
dossier_id: id,
procedure_id: procedure.id,
request_id: Current.request_id,
user_id: Current.user&.id,
controller: app_traces.find { _1.match?(%r{/controllers/|/jobs/}) },
caller: app_traces.first
}
logger = Lograge.logger || Rails.logger
logger.info payload.to_json
end
def track_assigned_dossier_without_groupe_instructeur
Sentry.capture_message(
"Assigned dossier without groupe_instructeur",
extra: {
dossier_id: self.id
}
)
end
end