diff --git a/app/models/attestation_template.rb b/app/models/attestation_template.rb index a391e9272..446b2096e 100644 --- a/app/models/attestation_template.rb +++ b/app/models/attestation_template.rb @@ -11,6 +11,7 @@ class AttestationTemplate < ApplicationRecord validates :footer, length: { maximum: 190 } FILE_MAX_SIZE_IN_MB = 0.5 + DOSSIER_STATE = 'accepte' def attestation_for(dossier) Attestation.new(title: replace_tags(title, dossier), pdf: build_pdf(dossier)) diff --git a/app/models/concerns/mail_template_concern.rb b/app/models/concerns/mail_template_concern.rb index 17fdd5246..387b515e2 100644 --- a/app/models/concerns/mail_template_concern.rb +++ b/app/models/concerns/mail_template_concern.rb @@ -1,8 +1,6 @@ module MailTemplateConcern extend ActiveSupport::Concern - include Rails.application.routes.url_helpers - include ActionView::Helpers::UrlHelper include TagsSubstitutionConcern def subject_for_dossier(dossier) @@ -13,10 +11,6 @@ module MailTemplateConcern replace_tags(body, dossier) end - def tags(is_dossier_termine: self.class.const_get(:IS_DOSSIER_TERMINE)) - super - end - module ClassMethods def default_for_procedure(procedure) body = ActionController::Base.new.render_to_string(template: self.const_get(:TEMPLATE_NAME)) @@ -24,14 +18,7 @@ module MailTemplateConcern end end - private - def dossier_tags - super + [{ libelle: 'lien dossier', description: '', lambda: -> (d) { users_dossier_recapitulatif_link(d) } }] - end - - def users_dossier_recapitulatif_link(dossier) - url = users_dossier_recapitulatif_url(dossier) - link_to(url, url, target: '_blank') + TagsSubstitutionConcern::DOSSIER_TAGS + TagsSubstitutionConcern::DOSSIER_TAGS_FOR_MAIL end end diff --git a/app/models/concerns/tags_substitution_concern.rb b/app/models/concerns/tags_substitution_concern.rb index 6dcfc25ce..385fe951c 100644 --- a/app/models/concerns/tags_substitution_concern.rb +++ b/app/models/concerns/tags_substitution_concern.rb @@ -1,61 +1,126 @@ module TagsSubstitutionConcern extend ActiveSupport::Concern - def tags(is_dossier_termine: true) + include Rails.application.routes.url_helpers + include ActionView::Helpers::UrlHelper + + DOSSIER_TAGS = [ + { + libelle: 'motivation', + description: 'Motivation facultative associée à la décision finale d’acceptation, refus ou classement sans suite', + target: :motivation, + available_for_states: Dossier::TERMINE + }, + { + libelle: 'date de dépôt', + description: 'Date du passage en construction du dossier par l’usager', + lambda: -> (d) { format_date(d.en_construction_at) }, + available_for_states: Dossier::SOUMIS + }, + { + libelle: 'date de passage en instruction', + description: '', + lambda: -> (d) { format_date(d.en_instruction_at) }, + available_for_states: Dossier::INSTRUCTION_COMMENCEE + }, + { + libelle: 'date de décision', + description: 'Date de la décision d’acceptation, refus, ou classement sans suite', + lambda: -> (d) { format_date(d.processed_at) }, + available_for_states: Dossier::TERMINE + }, + { + libelle: 'libellé procédure', + description: '', + lambda: -> (d) { d.procedure.libelle }, + available_for_states: Dossier::SOUMIS + }, + { + libelle: 'numéro du dossier', + description: '', + target: :id, + available_for_states: Dossier::SOUMIS + } + ] + + DOSSIER_TAGS_FOR_MAIL = [ + { + libelle: 'lien dossier', + description: '', + lambda: -> (d) { users_dossier_recapitulatif_link(d) }, + available_for_states: Dossier::SOUMIS + } + ] + + INDIVIDUAL_TAGS = [ + { + libelle: 'civilité', + description: 'M., Mme', + target: :gender, + available_for_states: Dossier::SOUMIS + }, + { + libelle: 'nom', + description: "nom de l'usager", + target: :nom, + available_for_states: Dossier::SOUMIS + }, + { + libelle: 'prénom', + description: "prénom de l'usager", + target: :prenom, + available_for_states: Dossier::SOUMIS + } + ] + + ENTREPRISE_TAGS = [ + { + libelle: 'SIREN', + description: '', + target: :siren, + available_for_states: Dossier::SOUMIS + }, + { + libelle: 'numéro de TVA intracommunautaire', + description: '', + target: :numero_tva_intracommunautaire, + available_for_states: Dossier::SOUMIS + }, + { + libelle: 'SIRET du siège social', + description: '', + target: :siret_siege_social, + available_for_states: Dossier::SOUMIS + }, + { + libelle: 'raison sociale', + description: '', + target: :raison_sociale, + available_for_states: Dossier::SOUMIS + } + ] + + ETABLISSEMENT_TAGS = [ + { + libelle: 'adresse', + description: '', + target: :inline_adresse, + available_for_states: Dossier::SOUMIS + } + ] + + def tags if procedure.for_individual? - identity_tags = individual_tags + identity_tags = INDIVIDUAL_TAGS else - identity_tags = entreprise_tags + etablissement_tags + identity_tags = ENTREPRISE_TAGS + ETABLISSEMENT_TAGS end - tags = identity_tags + dossier_tags + procedure_type_de_champ_public_private_tags - filter_tags(tags, is_dossier_termine) + filter_tags(identity_tags + dossier_tags + champ_public_tags + champ_private_tags) end private - def filter_tags(tags, is_dossier_termine) - if !is_dossier_termine - tags.reject { |tag| tag[:dossier_termine_only] } - else - tags - end - end - - def procedure_type_de_champ_public_private_tags - (procedure.types_de_champ + procedure.types_de_champ_private) - .map { |tdc| { libelle: tdc.libelle, description: tdc.description } } - end - - def dossier_tags - [ - { - libelle: 'motivation', - description: 'Motivation facultative associée à la décision finale d’acceptation, refus ou classement sans suite', - target: :motivation, - dossier_termine_only: true - }, - { - libelle: 'date de dépôt', - description: 'Date du passage en construction du dossier par l’usager', - lambda: -> (d) { format_date(d.en_construction_at) } - }, - { - libelle: 'date de passage en instruction', - description: '', - lambda: -> (d) { format_date(d.en_instruction_at) } - }, - { - libelle: 'date de décision', - description: 'Date de la décision d’acceptation, refus, ou classement sans suite', - lambda: -> (d) { format_date(d.processed_at) }, - dossier_termine_only: true - }, - { libelle: 'libellé procédure', description: '', lambda: -> (d) { d.procedure.libelle } }, - { libelle: 'numéro du dossier', description: '', target: :id } - ] - end - def format_date(date) if date.present? date.localtime.strftime('%d/%m/%Y') @@ -64,25 +129,49 @@ module TagsSubstitutionConcern end end - def individual_tags - [ - { libelle: 'civilité', description: 'M., Mme', target: :gender }, - { libelle: 'nom', description: "nom de l'usager", target: :nom }, - { libelle: 'prénom', description: "prénom de l'usager", target: :prenom } - ] + def users_dossier_recapitulatif_link(dossier) + url = users_dossier_recapitulatif_url(dossier) + link_to(url, url, target: '_blank') end - def entreprise_tags - [ - { libelle: 'SIREN', description: '', target: :siren }, - { libelle: 'numéro de TVA intracommunautaire', description: '', target: :numero_tva_intracommunautaire }, - { libelle: 'SIRET du siège social', description: '', target: :siret_siege_social }, - { libelle: 'raison sociale', description: '', target: :raison_sociale } - ] + def dossier_tags + # Overridden by MailTemplateConcern + DOSSIER_TAGS end - def etablissement_tags - [{ libelle: 'adresse', description: '', target: :inline_adresse }] + def filter_tags(tags) + # Implementation note: emails and attestation generations are generally + # triggerred by changes to the dossier’s state. The email or attestation + # is generated right after the dossier has reached its new state. + # + # DOSSIER_STATE should be equal to this new state. + # + # For instance, for an email that gets generated for the brouillon->en_construction + # transition, DOSSIER_STATE should equal 'en_construction'. + + if !defined?(self.class::DOSSIER_STATE) + raise NameError.new("The class #{self.class.name} includes TagsSubstitutionConcern, it should define the DOSSIER_STATE constant but it does not", :DOSSIER_STATE) + end + + tags.select { |tag| tag[:available_for_states].include?(self.class::DOSSIER_STATE) } + end + + def champ_public_tags + types_de_champ_tags(procedure.types_de_champ, Dossier::SOUMIS) + end + + def champ_private_tags + types_de_champ_tags(procedure.types_de_champ_private, Dossier::INSTRUCTION_COMMENCEE) + end + + def types_de_champ_tags(types_de_champ, available_for_states) + types_de_champ.map { |tdc| + { + libelle: tdc.libelle, + description: tdc.description, + available_for_states: available_for_states + } + } end def replace_tags(text, dossier) @@ -90,18 +179,18 @@ module TagsSubstitutionConcern return '' end - text = replace_type_de_champ_tags(text, procedure.types_de_champ, dossier.champs) - text = replace_type_de_champ_tags(text, procedure.types_de_champ_private, dossier.champs_private) + text = replace_type_de_champ_tags(text, filter_tags(champ_public_tags), dossier.champs) + text = replace_type_de_champ_tags(text, filter_tags(champ_private_tags), dossier.champs_private) tags_and_datas = [ [dossier_tags, dossier], - [individual_tags, dossier.individual], - [entreprise_tags, dossier.entreprise], - [etablissement_tags, dossier.entreprise&.etablissement] + [INDIVIDUAL_TAGS, dossier.individual], + [ENTREPRISE_TAGS, dossier.entreprise], + [ETABLISSEMENT_TAGS, dossier.entreprise&.etablissement] ] tags_and_datas - .map { |(tags, data)| [filter_tags(tags, dossier.termine?), data] } + .map { |(tags, data)| [filter_tags(tags), data] } .inject(text) { |acc, (tags, data)| replace_tags_with_values_from_data(acc, tags, data) } end @@ -121,7 +210,7 @@ module TagsSubstitutionConcern if tag.key?(:target) value = data.send(tag[:target]) else - value = tag[:lambda].(data) + value = instance_exec(data, &tag[:lambda]) end replace_tag(acc, tag, value) end diff --git a/app/models/dossier.rb b/app/models/dossier.rb index 25b022407..5843337d2 100644 --- a/app/models/dossier.rb +++ b/app/models/dossier.rb @@ -10,6 +10,8 @@ class Dossier < ActiveRecord::Base EN_CONSTRUCTION_OU_INSTRUCTION = %w(en_construction en_instruction) TERMINE = %w(accepte refuse sans_suite) + INSTRUCTION_COMMENCEE = TERMINE + %w(en_instruction) + SOUMIS = EN_CONSTRUCTION_OU_INSTRUCTION + TERMINE has_one :etablissement, dependent: :destroy has_one :entreprise, dependent: :destroy diff --git a/app/models/mails/closed_mail.rb b/app/models/mails/closed_mail.rb index cf7231836..97f0f1985 100644 --- a/app/models/mails/closed_mail.rb +++ b/app/models/mails/closed_mail.rb @@ -8,6 +8,6 @@ module Mails TEMPLATE_NAME = "mails/closed_mail" DISPLAYED_NAME = "Accusé d'acceptation" DEFAULT_SUBJECT = 'Votre dossier TPS nº --numéro du dossier-- a été accepté' - IS_DOSSIER_TERMINE = true + DOSSIER_STATE = 'accepte' end end diff --git a/app/models/mails/initiated_mail.rb b/app/models/mails/initiated_mail.rb index c9ca384ac..d4a2cbe0f 100644 --- a/app/models/mails/initiated_mail.rb +++ b/app/models/mails/initiated_mail.rb @@ -8,6 +8,6 @@ module Mails TEMPLATE_NAME = "mails/initiated_mail" DISPLAYED_NAME = 'Accusé de réception' DEFAULT_SUBJECT = 'Votre dossier TPS nº --numéro du dossier-- a bien été reçu' - IS_DOSSIER_TERMINE = false + DOSSIER_STATE = 'en_construction' end end diff --git a/app/models/mails/received_mail.rb b/app/models/mails/received_mail.rb index 30f58de9a..ef903b388 100644 --- a/app/models/mails/received_mail.rb +++ b/app/models/mails/received_mail.rb @@ -8,6 +8,6 @@ module Mails TEMPLATE_NAME = "mails/received_mail" DISPLAYED_NAME = 'Accusé de passage en instruction' DEFAULT_SUBJECT = 'Votre dossier TPS nº --numéro du dossier-- va être instruit' - IS_DOSSIER_TERMINE = false + DOSSIER_STATE = 'en_instruction' end end diff --git a/app/models/mails/refused_mail.rb b/app/models/mails/refused_mail.rb index 4e0d818c6..a133e6534 100644 --- a/app/models/mails/refused_mail.rb +++ b/app/models/mails/refused_mail.rb @@ -8,6 +8,6 @@ module Mails TEMPLATE_NAME = "mails/refused_mail" DISPLAYED_NAME = 'Accusé de rejet du dossier' DEFAULT_SUBJECT = 'Votre dossier TPS nº --numéro du dossier-- a été refusé' - IS_DOSSIER_TERMINE = true + DOSSIER_STATE = 'refuse' end end diff --git a/app/models/mails/without_continuation_mail.rb b/app/models/mails/without_continuation_mail.rb index 2c1e2f8b2..142db0982 100644 --- a/app/models/mails/without_continuation_mail.rb +++ b/app/models/mails/without_continuation_mail.rb @@ -8,6 +8,6 @@ module Mails TEMPLATE_NAME = "mails/without_continuation_mail" DISPLAYED_NAME = 'Accusé de classement sans suite' DEFAULT_SUBJECT = 'Votre dossier TPS nº --numéro du dossier-- a été classé sans suite' - IS_DOSSIER_TERMINE = true + DOSSIER_STATE = 'sans_suite' end end diff --git a/spec/models/concern/mail_template_concern_spec.rb b/spec/models/concern/mail_template_concern_spec.rb index 13a2cbf34..136f58b8c 100644 --- a/spec/models/concern/mail_template_concern_spec.rb +++ b/spec/models/concern/mail_template_concern_spec.rb @@ -4,7 +4,7 @@ describe MailTemplateConcern do let(:procedure) { create(:procedure) } let(:dossier) { create(:dossier, procedure: procedure) } let(:dossier2) { create(:dossier, procedure: procedure) } - let(:initiated_mail) { Mails::InitiatedMail.default_for_procedure(procedure) } + let(:initiated_mail) { create(:initiated_mail, procedure: procedure) } shared_examples "can replace tokens in template" do describe 'with no token to replace' do @@ -47,6 +47,22 @@ describe MailTemplateConcern do it_behaves_like "can replace tokens in template" end + describe 'tags' do + describe 'in initiated mail' do + it "does not treat date de passage en instruction as a tag" do + expect(initiated_mail.tags).not_to include(include({ libelle: 'date de passage en instruction' })) + end + end + + describe 'in received mail' do + let(:received_mail) { create(:received_mail, procedure: procedure) } + + it "treats date de passage en instruction as a tag" do + expect(received_mail.tags).to include(include({ libelle: 'date de passage en instruction' })) + end + end + end + describe '.replace_tags' do before { initiated_mail.body = "n --numéro du dossier--" } it "avoids side effects" do diff --git a/spec/models/concern/tags_substitution_concern_spec.rb b/spec/models/concern/tags_substitution_concern_spec.rb index 911fa29de..22188d0d6 100644 --- a/spec/models/concern/tags_substitution_concern_spec.rb +++ b/spec/models/concern/tags_substitution_concern_spec.rb @@ -2,6 +2,7 @@ describe TagsSubstitutionConcern, type: :model do let(:types_de_champ) { [] } let(:types_de_champ_private) { [] } let(:for_individual) { false } + let(:state) { 'accepte' } let(:procedure) do create(:procedure, @@ -13,17 +14,18 @@ describe TagsSubstitutionConcern, type: :model do let(:template_concern) do (Class.new do - include TagsSubstitutionConcern - public :replace_tags + include TagsSubstitutionConcern + public :replace_tags - def initialize(p) - @procedure = p - end + def initialize(p, s) + @procedure = p + self.class.const_set(:DOSSIER_STATE, s) + end - def procedure - @procedure - end - end).new(procedure) + def procedure + @procedure + end + end).new(procedure, state) end describe 'replace_tags' do @@ -119,8 +121,6 @@ describe TagsSubstitutionConcern, type: :model do context 'when the dossier has a motivation' do let(:dossier) { create(:dossier, motivation: 'motivation') } - before { dossier.accepte! } - context 'and the template has some dossier tags' do let(:template) { '--motivation-- --numéro du dossier--' } @@ -131,10 +131,10 @@ describe TagsSubstitutionConcern, type: :model do context 'when the procedure has a type de champ prive named libelleA' do let(:types_de_champ_private) { [create(:type_de_champ_private, libelle: 'libelleA')] } - context 'and the are used in the template' do + context 'and it is used in the template' do let(:template) { '--libelleA--' } - context 'and its value in the dossier are not nil' do + context 'and its value in the dossier is not nil' do before { dossier.champs_private.first.update_attributes(value: 'libelle1') } it { is_expected.to eq('libelle1') } @@ -142,6 +142,28 @@ describe TagsSubstitutionConcern, type: :model do end end + context 'when the dossier is en construction' do + let(:state) { 'en_construction' } + let(:template) { '--libelleA--' } + + context 'champs privés are not valid tags' do + # The dossier just transitionned from brouillon to en construction, + # so champs private are not valid tags yet + + let(:types_de_champ_private) { [create(:type_de_champ_private, libelle: 'libelleA')] } + + it { is_expected.to eq('--libelleA--') } + end + + context 'champs publics are valid tags' do + let(:types_de_champ) { [create(:type_de_champ_public, libelle: 'libelleA')] } + + before { dossier.champs.first.update_attributes(value: 'libelle1') } + + it { is_expected.to eq('libelle1') } + end + end + context 'when the procedure has 2 types de champ date and datetime' do let(:types_de_champ) do [ @@ -173,7 +195,6 @@ describe TagsSubstitutionConcern, type: :model do context "when using a date tag" do before do - dossier.accepte! dossier.en_construction_at = DateTime.new(2001, 2, 3) dossier.en_instruction_at = DateTime.new(2004, 5, 6) dossier.processed_at = DateTime.new(2007, 8, 9) @@ -237,6 +258,7 @@ describe TagsSubstitutionConcern, type: :model do context 'when generating a document for a dossier that is not termine' do let(:dossier) { create(:dossier) } let(:template) { '--motivation-- --date de décision--' } + let(:state) { 'en_instruction' } subject { template_concern.replace_tags(template, dossier) } @@ -247,18 +269,36 @@ describe TagsSubstitutionConcern, type: :model do end describe 'tags' do - context 'when generating a document for a dossier terminé' do - subject { template_concern.tags } + subject { template_concern.tags } + let(:types_de_champ) { [create(:type_de_champ_public, libelle: 'public')] } + let(:types_de_champ_private) { [create(:type_de_champ_private, libelle: 'privé')] } + + context 'when generating a document for a dossier terminé' do it { is_expected.to include(include({ libelle: 'motivation' })) } it { is_expected.to include(include({ libelle: 'date de décision' })) } + it { is_expected.to include(include({ libelle: 'public' })) } + it { is_expected.to include(include({ libelle: 'privé' })) } end - context 'when generating a document for a dossier that is not terminé' do - subject { template_concern.tags(is_dossier_termine: false) } + context 'when generating a document for a dossier en instruction' do + let(:state) { 'en_instruction' } it { is_expected.not_to include(include({ libelle: 'motivation' })) } it { is_expected.not_to include(include({ libelle: 'date de décision' })) } + + it { is_expected.to include(include({ libelle: 'public' })) } + it { is_expected.to include(include({ libelle: 'privé' })) } + end + + context 'when generating a document for a dossier en construction' do + let(:state) { 'en_construction' } + + it { is_expected.not_to include(include({ libelle: 'motivation' })) } + it { is_expected.not_to include(include({ libelle: 'date de décision' })) } + it { is_expected.not_to include(include({ libelle: 'privé' })) } + + it { is_expected.to include(include({ libelle: 'public' })) } end end end