diff --git a/Gemfile b/Gemfile index 20ddf1137..81ac050d8 100644 --- a/Gemfile +++ b/Gemfile @@ -47,6 +47,7 @@ gem 'omniauth-rails_csrf_protection', '~> 0.1' gem 'openid_connect' gem 'openstack' gem 'pg' +gem 'phonelib' gem 'prawn' # PDF Generation gem 'prawn_rails' gem 'premailer-rails' diff --git a/Gemfile.lock b/Gemfile.lock index 9a923b78b..423259e6f 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -241,7 +241,7 @@ GEM graphiql-rails (1.7.0) railties sprockets-rails - graphql (1.9.10) + graphql (1.9.15) graphql-batch (0.4.1) graphql (>= 1.3, < 2) promise.rb (~> 0.7.2) @@ -420,6 +420,7 @@ GEM ast (~> 2.4.0) pdf-core (0.7.0) pg (1.1.3) + phonelib (0.6.39) powerpack (0.1.2) prawn (2.2.2) pdf-core (~> 0.7.0) @@ -767,6 +768,7 @@ DEPENDENCIES openid_connect openstack pg + phonelib prawn prawn_rails premailer-rails diff --git a/README.md b/README.md index 94a643ecf..d64b44dbd 100644 --- a/README.md +++ b/README.md @@ -77,6 +77,7 @@ En local, un utilisateur de test est créé automatiquement, avec les identifian InstructeurEmailNotificationJob.set(cron: "0 10 * * 1,2,3,4,5,6").perform_later PurgeUnattachedBlobsJob.set(cron: "0 0 * * *").perform_later OperationsSignatureJob.set(cron: "0 6 * * *").perform_later + SeekAndDestroyExpiredDossiersJob.set(cron: "0 7 * * *").perform_later ### Voir les emails envoyés en local diff --git a/app/controllers/new_administrateur/procedures_controller.rb b/app/controllers/new_administrateur/procedures_controller.rb index b9236fdb6..fbcdce08e 100644 --- a/app/controllers/new_administrateur/procedures_controller.rb +++ b/app/controllers/new_administrateur/procedures_controller.rb @@ -68,7 +68,7 @@ module NewAdministrateur end def procedure_params - editable_params = [:libelle, :description, :organisation, :direction, :lien_site_web, :cadre_juridique, :deliberation, :notice, :web_hook_url, :euro_flag, :logo, :auto_archive_on, :monavis_embed] + editable_params = [:libelle, :description, :organisation, :direction, :lien_site_web, :cadre_juridique, :deliberation, :notice, :web_hook_url, :declarative_with_state, :euro_flag, :logo, :auto_archive_on, :monavis_embed] permited_params = if @procedure&.locked? params.require(:procedure).permit(*editable_params) else diff --git a/app/graphql/api/v2/schema.rb b/app/graphql/api/v2/schema.rb index 390d60c44..26089ef11 100644 --- a/app/graphql/api/v2/schema.rb +++ b/app/graphql/api/v2/schema.rb @@ -26,6 +26,10 @@ class Api::V2::Schema < GraphQL::Schema Types::MessageType when Instructeur, User Types::ProfileType + when Individual + Types::PersonnePhysiqueType + when Etablissement + Types::PersonneMoraleType else raise GraphQL::ExecutionError.new("Unexpected object: #{obj}") end @@ -33,6 +37,7 @@ class Api::V2::Schema < GraphQL::Schema orphan_types Types::Champs::CarteChampType, Types::Champs::CheckboxChampType, + Types::Champs::CiviliteChampType, Types::Champs::DateChampType, Types::Champs::DecimalNumberChampType, Types::Champs::DossierLinkChampType, @@ -45,7 +50,9 @@ class Api::V2::Schema < GraphQL::Schema Types::Champs::TextChampType, Types::GeoAreas::ParcelleCadastraleType, Types::GeoAreas::QuartierPrioritaireType, - Types::GeoAreas::SelectionUtilisateurType + Types::GeoAreas::SelectionUtilisateurType, + Types::PersonneMoraleType, + Types::PersonnePhysiqueType def self.unauthorized_object(error) # Add a top-level error to the response instead of returning nil: diff --git a/app/graphql/schema.graphql b/app/graphql/schema.graphql index 100e53bd8..e9ad183b8 100644 --- a/app/graphql/schema.graphql +++ b/app/graphql/schema.graphql @@ -1,3 +1,12 @@ +type Association { + dateCreation: ISO8601Date! + dateDeclaration: ISO8601Date! + datePublication: ISO8601Date! + objet: String! + rna: String! + titre: String! +} + type Avis { attachmentUrl: URL dateQuestion: ISO8601DateTime! @@ -76,6 +85,33 @@ type CheckboxChamp implements Champ { value: Boolean! } +enum Civilite { + """ + Monsieur + """ + M + + """ + Madame + """ + Mme +} + +type CiviliteChamp implements Champ { + id: ID! + + """ + Libellé du champ. + """ + label: String! + + """ + La valeur du champ sous forme texte. + """ + stringValue: String + value: Civilite +} + """ GeoJSON coordinates """ @@ -157,6 +193,10 @@ type DecimalNumberChamp implements Champ { value: Float } +interface Demandeur { + id: ID! +} + """ Une demarche """ @@ -184,6 +224,11 @@ type Demarche { """ datePublication: ISO8601DateTime! + """ + L'état de dossier pour une démarche déclarative + """ + declarative: DossierDeclarativeState + """ Description de la démarche. """ @@ -240,6 +285,7 @@ type Demarche { Le numero de la démarche. """ number: Int! + service: Service! """ L'état de la démarche. @@ -322,6 +368,7 @@ type Dossier { Date de traitement. """ dateTraitement: ISO8601DateTime + demandeur: Demandeur! id: ID! instructeurs: [Profile!]! messages: [Message!]! @@ -428,6 +475,18 @@ type DossierConnection { pageInfo: PageInfo! } +enum DossierDeclarativeState { + """ + Accepté + """ + accepte + + """ + En instruction + """ + en_instruction +} + """ An edge in a connection. """ @@ -578,6 +637,22 @@ enum DossierState { sans_suite } +type Entreprise { + capitalSocial: Int! + codeEffectifEntreprise: String! + dateCreation: ISO8601Date! + formeJuridique: String! + formeJuridiqueCode: String! + inlineAdresse: String! + nom: String! + nomCommercial: String! + numeroTvaIntracommunautaire: String! + prenom: String! + raisonSociale: String! + siren: String! + siretSiegeSocial: String! +} + interface GeoArea { geometry: GeoJSON! id: ID! @@ -615,6 +690,11 @@ type GroupeInstructeur { label: String! } +""" +An ISO 8601-encoded date +""" +scalar ISO8601Date + """ An ISO 8601-encoded datetime """ @@ -758,21 +838,32 @@ type ParcelleCadastrale implements GeoArea { surfaceParcelle: Float! } -type PersonneMorale { +type PersonneMorale implements Demandeur { adresse: String! + association: Association codeInseeLocalite: String! codePostal: String! complementAdresse: String! + entreprise: Entreprise + id: ID! libelleNaf: String! localite: String! naf: String! nomVoie: String! numeroVoie: String! - siegeSocial: String! + siegeSocial: Boolean! siret: String! typeVoie: String! } +type PersonnePhysique implements Demandeur { + civilite: Civilite + dateDeNaissance: ISO8601Date + id: ID! + nom: String! + prenom: String! +} + type PieceJustificativeChamp implements Champ { id: ID! @@ -845,6 +936,13 @@ type SelectionUtilisateur implements GeoArea { source: GeoAreaSource! } +type Service { + id: ID! + nom: String! + organisme: String! + typeOrganisme: TypeOrganisme! +} + type SiretChamp implements Champ { etablissement: PersonneMorale id: ID! @@ -1012,6 +1110,43 @@ enum TypeDeChamp { yes_no } +enum TypeOrganisme { + """ + Administration centrale + """ + administration_centrale + + """ + Association + """ + association + + """ + Autre + """ + autre + + """ + Collectivité territoriale + """ + collectivite_territoriale + + """ + Établissement d’enseignement + """ + etablissement_enseignement + + """ + Opérateur d'État + """ + operateur_d_etat + + """ + Service déconcentré de l'État + """ + service_deconcentre_de_l_etat +} + """ A valid URL, transported as a string """ diff --git a/app/graphql/types/champ_type.rb b/app/graphql/types/champ_type.rb index 8383e409f..928e5f582 100644 --- a/app/graphql/types/champ_type.rb +++ b/app/graphql/types/champ_type.rb @@ -31,6 +31,8 @@ module Types Types::Champs::MultipleDropDownListChampType when ::Champs::LinkedDropDownListChamp Types::Champs::LinkedDropDownListChampType + when ::Champs::CiviliteChamp + Types::Champs::CiviliteChampType else Types::Champs::TextChampType end diff --git a/app/graphql/types/champs/civilite_champ_type.rb b/app/graphql/types/champs/civilite_champ_type.rb new file mode 100644 index 000000000..412daeeac --- /dev/null +++ b/app/graphql/types/champs/civilite_champ_type.rb @@ -0,0 +1,7 @@ +module Types::Champs + class CiviliteChampType < Types::BaseObject + implements Types::ChampType + + field :value, Types::Civilite, null: true + end +end diff --git a/app/graphql/types/civilite.rb b/app/graphql/types/civilite.rb new file mode 100644 index 000000000..562670651 --- /dev/null +++ b/app/graphql/types/civilite.rb @@ -0,0 +1,6 @@ +module Types + class Civilite < Types::BaseEnum + value("M", "Monsieur", value: Individual::GENDER_MALE) + value("Mme", "Madame", value: Individual::GENDER_FEMALE) + end +end diff --git a/app/graphql/types/demandeur_type.rb b/app/graphql/types/demandeur_type.rb new file mode 100644 index 000000000..9f5d01b3d --- /dev/null +++ b/app/graphql/types/demandeur_type.rb @@ -0,0 +1,18 @@ +module Types + module DemandeurType + include Types::BaseInterface + + global_id_field :id + + definition_methods do + def resolve_type(object, context) + case object + when Individual + Types::PersonnePhysiqueType + when Etablissement + Types::PersonneMoraleType + end + end + end + end +end diff --git a/app/graphql/types/demarche_type.rb b/app/graphql/types/demarche_type.rb index 6b464370c..b1643d01a 100644 --- a/app/graphql/types/demarche_type.rb +++ b/app/graphql/types/demarche_type.rb @@ -6,6 +6,14 @@ module Types end end + class DossierDeclarativeState < Types::BaseEnum + Procedure.declarative_with_states.each do |symbol_name, string_name| + value(string_name, + I18n.t("declarative_with_state/#{string_name}", scope: [:activerecord, :attributes, :procedure]), + value: symbol_name) + end + end + description "Une demarche" global_id_field :id @@ -13,6 +21,7 @@ module Types field :title, String, "Le titre de la démarche.", null: false, method: :libelle field :description, String, "Description de la démarche.", null: false field :state, DemarcheState, "L'état de la démarche.", null: false + field :declarative, DossierDeclarativeState, "L'état de dossier pour une démarche déclarative", null: true, method: :declarative_with_state field :date_creation, GraphQL::Types::ISO8601DateTime, "Date de la création.", null: false, method: :created_at field :date_publication, GraphQL::Types::ISO8601DateTime, "Date de la publication.", null: false, method: :published_at @@ -20,6 +29,7 @@ module Types field :date_fermeture, GraphQL::Types::ISO8601DateTime, "Date de la fermeture.", null: true, method: :closed_at field :groupe_instructeurs, [Types::GroupeInstructeurType], null: false + field :service, Types::ServiceType, null: false field :dossiers, Types::DossierType.connection_type, "Liste de tous les dossiers d'une démarche.", null: false do argument :order, Types::Order, default_value: :asc, required: false, description: "L'ordre des dossiers." @@ -39,6 +49,10 @@ module Types Loaders::Association.for(object.class, groupe_instructeurs: { procedure: [:administrateurs] }).load(object) end + def service + Loaders::Record.for(Service).load(object.service_id) + end + def dossiers(updated_since: nil, created_since: nil, state: nil, order:) dossiers = object.dossiers.state_not_brouillon.for_api_v2 diff --git a/app/graphql/types/dossier_type.rb b/app/graphql/types/dossier_type.rb index fa571275b..ac8961241 100644 --- a/app/graphql/types/dossier_type.rb +++ b/app/graphql/types/dossier_type.rb @@ -24,6 +24,8 @@ module Types { Extensions::Attachment => { attachment: :justificatif_motivation } } ] + field :demandeur, Types::DemandeurType, null: false + field :usager, Types::ProfileType, null: false field :instructeurs, [Types::ProfileType], null: false @@ -61,6 +63,14 @@ module Types Loaders::Association.for(object.class, :champs_private).load(object) end + def demandeur + if object.procedure.for_individual + Loaders::Association.for(object.class, :individual).load(object) + else + Loaders::Association.for(object.class, :etablissement).load(object) + end + end + def self.authorized?(object, context) authorized_demarche?(object.procedure, context) end diff --git a/app/graphql/types/personne_morale_type.rb b/app/graphql/types/personne_morale_type.rb index 999fafee3..6d15cf84e 100644 --- a/app/graphql/types/personne_morale_type.rb +++ b/app/graphql/types/personne_morale_type.rb @@ -1,7 +1,34 @@ module Types class PersonneMoraleType < Types::BaseObject + class EntrepriseType < Types::BaseObject + field :siren, String, null: false + field :capital_social, Int, null: false + field :numero_tva_intracommunautaire, String, null: false + field :forme_juridique, String, null: false + field :forme_juridique_code, String, null: false + field :nom_commercial, String, null: false + field :raison_sociale, String, null: false + field :siret_siege_social, String, null: false + field :code_effectif_entreprise, String, null: false + field :date_creation, GraphQL::Types::ISO8601Date, null: false + field :nom, String, null: false + field :prenom, String, null: false + field :inline_adresse, String, null: false + end + + class AssociationType < Types::BaseObject + field :rna, String, null: false + field :titre, String, null: false + field :objet, String, null: false + field :date_creation, GraphQL::Types::ISO8601Date, null: false + field :date_declaration, GraphQL::Types::ISO8601Date, null: false + field :date_publication, GraphQL::Types::ISO8601Date, null: false + end + + implements Types::DemandeurType + field :siret, String, null: false - field :siege_social, String, null: false + field :siege_social, Boolean, null: false field :naf, String, null: false field :libelle_naf, String, null: false field :adresse, String, null: false @@ -12,5 +39,26 @@ module Types field :code_postal, String, null: false field :localite, String, null: false field :code_insee_localite, String, null: false + field :entreprise, EntrepriseType, null: true + field :association, AssociationType, null: true + + def entreprise + if object.entreprise_siren.present? + object.entreprise + end + end + + def association + if object.association? + { + rna: object.association_rna, + titre: object.association_titre, + objet: object.association_objet, + date_creation: object.association_date_creation, + date_declaration: object.association_date_declaration, + date_publication: object.association_date_publication + } + end + end end end diff --git a/app/graphql/types/personne_physique_type.rb b/app/graphql/types/personne_physique_type.rb new file mode 100644 index 000000000..5074a04ca --- /dev/null +++ b/app/graphql/types/personne_physique_type.rb @@ -0,0 +1,10 @@ +module Types + class PersonnePhysiqueType < Types::BaseObject + implements Types::DemandeurType + + field :nom, String, null: false + field :prenom, String, null: false + field :civilite, Types::Civilite, null: true, method: :gender + field :date_de_naissance, GraphQL::Types::ISO8601Date, null: true, method: :birthdate + end +end diff --git a/app/graphql/types/service_type.rb b/app/graphql/types/service_type.rb new file mode 100644 index 000000000..71b8c7b0d --- /dev/null +++ b/app/graphql/types/service_type.rb @@ -0,0 +1,15 @@ +module Types + class ServiceType < Types::BaseObject + class TypeOrganisme < Types::BaseEnum + Service.type_organismes.each do |symbol_name, string_name| + value(string_name, I18n.t(symbol_name, scope: [:type_organisme]), value: symbol_name) + end + end + + global_id_field :id + + field :nom, String, null: false + field :type_organisme, TypeOrganisme, null: false + field :organisme, String, null: false + end +end diff --git a/app/jobs/seek_and_destroy_expired_dossiers_job.rb b/app/jobs/seek_and_destroy_expired_dossiers_job.rb new file mode 100644 index 000000000..d720873bf --- /dev/null +++ b/app/jobs/seek_and_destroy_expired_dossiers_job.rb @@ -0,0 +1,8 @@ +class SeekAndDestroyExpiredDossiersJob < ApplicationJob + queue_as :cron + + def perform(*args) + Dossier.send_brouillon_expiration_notices + Dossier.destroy_brouillons_and_notify + end +end diff --git a/app/mailers/dossier_mailer.rb b/app/mailers/dossier_mailer.rb index 4754e526a..8813730fe 100644 --- a/app/mailers/dossier_mailer.rb +++ b/app/mailers/dossier_mailer.rb @@ -69,4 +69,18 @@ class DossierMailer < ApplicationMailer format.html { render layout: 'mailers/notifications_layout' } end end + + def notify_brouillon_near_deletion(user, dossiers) + @subject = default_i18n_subject(count: dossiers.count) + @dossiers = dossiers + + mail(to: user.email, subject: @subject) + end + + def notify_brouillon_deletion(user, dossier_hashes) + @subject = default_i18n_subject(count: dossier_hashes.count) + @dossier_hashes = dossier_hashes + + mail(to: user.email, subject: @subject) + end end diff --git a/app/models/dossier.rb b/app/models/dossier.rb index ab339ebd3..3075a1f32 100644 --- a/app/models/dossier.rb +++ b/app/models/dossier.rb @@ -18,6 +18,8 @@ class Dossier < ApplicationRecord TAILLE_MAX_ZIP = 50.megabytes + DRAFT_EXPIRATION = 1.month + 5.days + has_one :etablissement, dependent: :destroy has_one :individual, dependent: :destroy has_one :attestation, dependent: :destroy @@ -162,6 +164,14 @@ class Dossier < ApplicationRecord user: []) } + scope :brouillon_close_to_expiration, -> do + brouillon + .joins(:procedure) + .where("dossiers.created_at + (duree_conservation_dossiers_dans_ds * interval '1 month') - (1 * interval '1 month') <= now()") + end + scope :expired_brouillon, -> { brouillon.where("brouillon_close_to_expiration_notice_sent_at < ?", (Time.zone.now - (DRAFT_EXPIRATION))) } + scope :without_notice_sent, -> { where(brouillon_close_to_expiration_notice_sent_at: nil) } + scope :for_procedure, -> (procedure) { includes(:user, :groupe_instructeur).where(groupe_instructeurs: { procedure: procedure }) } scope :for_api_v2, -> { includes(procedure: [:administrateurs], etablissement: [], individual: []) } @@ -577,6 +587,10 @@ class Dossier < ApplicationRecord Dossier.where(id: champs.filter(&:dossier_link?).map(&:value).compact) end + def hash_for_deletion_mail + { id: self.id, procedure_libelle: self.procedure.libelle } + end + private def log_dossier_operation(author, operation, subject = nil) @@ -627,4 +641,37 @@ class Dossier < ApplicationRecord ) end end + + def self.send_brouillon_expiration_notices + brouillons = Dossier + .brouillon_close_to_expiration + .without_notice_sent + + brouillons + .includes(:user) + .group_by(&:user) + .each do |(user, dossiers)| + DossierMailer.notify_brouillon_near_deletion(user, dossiers).deliver_later + end + + brouillons.update_all(brouillon_close_to_expiration_notice_sent_at: Time.zone.now) + end + + def self.destroy_brouillons_and_notify + expired_brouillons = Dossier.expired_brouillon + + expired_brouillons + .includes(:procedure, :user) + .group_by(&:user) + .each do |(user, dossiers)| + + dossier_hashes = dossiers.map(&:hash_for_deletion_mail) + DossierMailer.notify_brouillon_deletion(user, dossier_hashes).deliver_later + + dossiers.each do |dossier| + DeletedDossier.create_from_dossier(dossier) + dossier.destroy + end + end + end end diff --git a/app/models/procedure.rb b/app/models/procedure.rb index 257490810..82120fe6b 100644 --- a/app/models/procedure.rb +++ b/app/models/procedure.rb @@ -305,6 +305,12 @@ class Procedure < ApplicationRecord declarative_with_state == Procedure.declarative_with_states.fetch(:accepte) end + def self.declarative_attributes_for_select + declarative_with_states.map do |state, _| + [I18n.t("activerecord.attributes.#{model_name.i18n_key}.declarative_with_state/#{state}"), state] + end + end + # Warning: dossier after_save build_default_champs must be removed # to save a dossier created from this method def new_dossier diff --git a/app/models/service.rb b/app/models/service.rb index 8dac51ba7..ad9bf651f 100644 --- a/app/models/service.rb +++ b/app/models/service.rb @@ -20,7 +20,7 @@ class Service < ApplicationRecord validates :organisme, presence: { message: 'doit être renseigné' }, allow_nil: false validates :type_organisme, presence: { message: 'doit être renseigné' }, allow_nil: false validates :email, presence: { message: 'doit être renseigné' }, allow_nil: false - validates :telephone, presence: { message: 'doit être renseigné' }, allow_nil: false + validates :telephone, phone: { possible: true, allow_blank: true } validates :horaires, presence: { message: 'doivent être renseignés' }, allow_nil: false validates :adresse, presence: { message: 'doit être renseignée' }, allow_nil: false validates :administrateur, presence: { message: 'doit être renseigné' }, allow_nil: false diff --git a/app/views/dossier_mailer/notify_brouillon_deletion.html.haml b/app/views/dossier_mailer/notify_brouillon_deletion.html.haml new file mode 100644 index 000000000..7a28ec3f7 --- /dev/null +++ b/app/views/dossier_mailer/notify_brouillon_deletion.html.haml @@ -0,0 +1,12 @@ +- content_for(:title, "#{@subject}") + +%p + Bonjour, + +%p= t('.automatic_dossier_deletion', count: @dossier_hashes.count) + +%ul + - @dossier_hashes.each do |d| + %li n° #{d[:id]} (#{d[:procedure_libelle]}) + += render partial: "layouts/mailers/signature" diff --git a/app/views/dossier_mailer/notify_brouillon_near_deletion.html.haml b/app/views/dossier_mailer/notify_brouillon_near_deletion.html.haml new file mode 100644 index 000000000..fc972ddd0 --- /dev/null +++ b/app/views/dossier_mailer/notify_brouillon_near_deletion.html.haml @@ -0,0 +1,16 @@ +- content_for(:title, "#{@subject}") + +%p + Bonjour, + +%p + Afin de limiter la conservation de vos données personnelles, + = t('.automatic_dossier_deletion', count: @dossiers.count) + %ul + - @dossiers.each do |d| + %li= link_to("n° #{d.id} (#{d.procedure.libelle})", dossier_url(d)) + +%p + #{sanitize(t('.send_your_draft', count: @dossiers.count))}. Et sinon, vous n'avez rien à faire. + += render partial: "layouts/mailers/signature" diff --git a/app/views/new_administrateur/procedures/_informations.html.haml b/app/views/new_administrateur/procedures/_informations.html.haml index a793479a4..2906bf359 100644 --- a/app/views/new_administrateur/procedures/_informations.html.haml +++ b/app/views/new_administrateur/procedures/_informations.html.haml @@ -113,3 +113,13 @@ %p.explication La clôture automatique suspend la publication de la démarche et entraîne le passage de tous les dossiers "en construction" (c'est à dire ceux qui ont été déposés), au statut "en instruction", ce qui ne permet plus aux usagers de les modifier. + + = f.label :declarative_with_state do + Démarche déclarative + = f.select :declarative_with_state, Procedure.declarative_attributes_for_select, { include_blank: true }, class: 'form-control' + + %p.explication + Par défaut, une démarche n'est pas déclarative; à son dépot, un dossier est «en construction». Vous pouvez choisir de la rendre déclarative, afin que tous les dossiers déposés soient immédiatement au statut "en instruction" en "accepté". + %br + %br + Dans le cadre d'une démarche déclarative, au dépot, seul l'email associé à l'état choisi est envoyé. (ex: démarche déclarative «accepté»: envoi uniquement de l'email d'acceptation) diff --git a/app/views/new_administrateur/services/_form.html.haml b/app/views/new_administrateur/services/_form.html.haml index b5d2010ae..4b547cc5c 100644 --- a/app/views/new_administrateur/services/_form.html.haml +++ b/app/views/new_administrateur/services/_form.html.haml @@ -18,16 +18,23 @@ %h2.header-section Informations de contact - %p.explication Ces informations seront visibles par les utilisateurs du formulaire. + %p.explication + Votre démarche sera hébergée par demarche-simplifiees.fr – mais nous ne pouvons pas assurer le support des démarches. Et malgré la dématérialisation, les usagers se poseront parfois des questions légitimes sur le processus administratif. + %br + Il est donc important que les usagers puissent vous contacter s'ils ont des questions sur votre démarche. + %br + Ces informations seront visibles par les utilisateurs de la démarche, affichées dans le menu "Aide". + %p.explication Indiquez une adresse mail en capacité de répondre à l'usager. = f.label :email do Adresse email %span.mandatory * = f.email_field :email, placeholder: 'contact@mon-service.fr', required: true + %p.explication Indiquez le numéro de téléphone du service le plus à même de fournir des réponses pertinentes à vos usagers. = f.label :telephone do - Numéro de téléphone %span.mandatory * + Numéro de téléphone = f.telephone_field :telephone, placeholder: '04 12 24 42 37', required: true = f.label :horaires do diff --git a/app/views/shared/dossiers/editable_champs/_editable_champ.html.haml b/app/views/shared/dossiers/editable_champs/_editable_champ.html.haml index 3af6e390c..156fd5699 100644 --- a/app/views/shared/dossiers/editable_champs/_editable_champ.html.haml +++ b/app/views/shared/dossiers/editable_champs/_editable_champ.html.haml @@ -1,6 +1,9 @@ .editable-champ{ class: "editable-champ-#{champ.type_champ}" } - if champ.repetition? = render partial: 'shared/dossiers/editable_champs/header_section', locals: { champ: champ } + + - if champ.description.present? + %p.notice= string_to_html(champ.description, false) - elsif has_label?(champ) = render partial: 'shared/dossiers/editable_champs/champ_label', locals: { form: form, champ: champ, seen_at: defined?(seen_at) ? seen_at : nil } diff --git a/app/views/users/_procedure_footer.html.haml b/app/views/users/_procedure_footer.html.haml index ca96a0316..b225927c6 100644 --- a/app/views/users/_procedure_footer.html.haml +++ b/app/views/users/_procedure_footer.html.haml @@ -24,13 +24,14 @@ Par email : = link_to service.email, "mailto:#{service.email}" - %li - Par téléphone : - = link_to service.telephone, service.telephone_url + - if service.telephone.present? + %li + Par téléphone : + = link_to service.telephone, service.telephone_url - %li - - horaires = "Horaires : #{formatted_horaires(service.horaires)}" - = simple_format(horaires, {}, wrapper_tag: 'span') + %li + - horaires = "Horaires : #{formatted_horaires(service.horaires)}" + = simple_format(horaires, {}, wrapper_tag: 'span') - politiques = politiques_conservation_de_donnees(procedure) diff --git a/config/initializers/phonelib.rb b/config/initializers/phonelib.rb new file mode 100644 index 000000000..f76a2d441 --- /dev/null +++ b/config/initializers/phonelib.rb @@ -0,0 +1,2 @@ +Phonelib.default_country = "FR" +Phonelib.parse_special = true diff --git a/config/locales/models/procedure/fr.yml b/config/locales/models/procedure/fr.yml index b441c4524..4f58b71ba 100644 --- a/config/locales/models/procedure/fr.yml +++ b/config/locales/models/procedure/fr.yml @@ -14,3 +14,5 @@ fr: aasm_state/publiee: Publiée aasm_state/close: Close aasm_state/hidden: Suprimée + declarative_with_state/en_instruction: En instruction + declarative_with_state/accepte: Accepté diff --git a/config/locales/views/dossier_mailer/notify_brouillon_deletion/fr.yml b/config/locales/views/dossier_mailer/notify_brouillon_deletion/fr.yml new file mode 100644 index 000000000..291dffc23 --- /dev/null +++ b/config/locales/views/dossier_mailer/notify_brouillon_deletion/fr.yml @@ -0,0 +1,9 @@ +fr: + dossier_mailer: + notify_brouillon_deletion: + subject: + one: "Un dossier en brouillon a été supprimé automatiquement" + other: "Des dossiers en brouillon ont été supprimés automatiquement" + automatic_dossier_deletion: + one: "Le délai maximum de conservation du dossier en brouillon suivant a été atteint, il a donc été supprimé :" + other: "Le délai maximum de conservation des dossiers en brouillon suivants a été atteint, ils ont donc été supprimés :" diff --git a/config/locales/views/dossier_mailer/notify_brouillon_near_deletion/fr.yml b/config/locales/views/dossier_mailer/notify_brouillon_near_deletion/fr.yml new file mode 100644 index 000000000..a3cbf01dc --- /dev/null +++ b/config/locales/views/dossier_mailer/notify_brouillon_near_deletion/fr.yml @@ -0,0 +1,12 @@ +fr: + dossier_mailer: + notify_brouillon_near_deletion: + subject: + one: Un dossier en brouillon va bientôt être supprimé + other: Des dossiers en brouillon vont bientôt être supprimés + automatic_dossier_deletion: + one: "le dossier en brouillon suivant sera bientôt automatiquement supprimé :" + other: "les dossiers en brouillon suivant seront bientôt automatiquement supprimés :" + send_your_draft: + one: "Si vous souhaitez toujours déposer ce dossier, vous pouvez retrouver votre brouillon pendant encore un mois" + other: "Si vous souhaitez toujours déposer ces dossiers, vous pouvez retrouver vos brouillons pendant encore un mois" diff --git a/db/migrate/20191128081324_add_near_deletion_notice_send_to_dossier.rb b/db/migrate/20191128081324_add_near_deletion_notice_send_to_dossier.rb new file mode 100644 index 000000000..45c159631 --- /dev/null +++ b/db/migrate/20191128081324_add_near_deletion_notice_send_to_dossier.rb @@ -0,0 +1,5 @@ +class AddNearDeletionNoticeSendToDossier < ActiveRecord::Migration[5.2] + def change + add_column :dossiers, :brouillon_close_to_expiration_notice_sent_at, :datetime + end +end diff --git a/db/schema.rb b/db/schema.rb index 009fa088b..cb42fecf4 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 2019_11_14_113700) do +ActiveRecord::Schema.define(version: 2019_11_28_081324) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -252,6 +252,7 @@ ActiveRecord::Schema.define(version: 2019_11_14_113700) do t.text "search_terms" t.text "private_search_terms" t.bigint "groupe_instructeur_id" + t.datetime "brouillon_close_to_expiration_notice_sent_at" t.index "to_tsvector('french'::regconfig, (search_terms || private_search_terms))", name: "index_dossiers_on_search_terms_private_search_terms", using: :gin t.index "to_tsvector('french'::regconfig, search_terms)", name: "index_dossiers_on_search_terms", using: :gin t.index ["archived"], name: "index_dossiers_on_archived" diff --git a/lib/tasks/deployment/20191203142402_enable_seek_and_destroy_job.rake b/lib/tasks/deployment/20191203142402_enable_seek_and_destroy_job.rake new file mode 100644 index 000000000..5cb1f9c65 --- /dev/null +++ b/lib/tasks/deployment/20191203142402_enable_seek_and_destroy_job.rake @@ -0,0 +1,7 @@ +namespace :after_party do + desc 'Deployment task: enable_seek_and_destroy_job' + task enable_seek_and_destroy_job: :environment do + SeekAndDestroyExpiredDossiersJob.set(cron: "0 7 * * *").perform_later + AfterParty::TaskRecord.create version: '20191203142402' + end +end diff --git a/spec/controllers/api/v2/graphql_controller_spec.rb b/spec/controllers/api/v2/graphql_controller_spec.rb index d87601827..0d7b6436e 100644 --- a/spec/controllers/api/v2/graphql_controller_spec.rb +++ b/spec/controllers/api/v2/graphql_controller_spec.rb @@ -3,18 +3,19 @@ require 'spec_helper' describe API::V2::GraphqlController do let(:admin) { create(:administrateur) } let(:token) { admin.renew_api_token } - let(:procedure) { create(:procedure, :with_all_champs, administrateurs: [admin]) } + let(:procedure) { create(:procedure, :published, :for_individual, :with_service, :with_all_champs, administrateurs: [admin]) } let(:dossier) do dossier = create(:dossier, :en_construction, :with_all_champs, + :for_individual, procedure: procedure) create(:commentaire, dossier: dossier, email: 'test@test.com') dossier end - let(:dossier1) { create(:dossier, :en_construction, procedure: procedure, en_construction_at: 1.day.ago) } - let(:dossier2) { create(:dossier, :en_construction, procedure: procedure, en_construction_at: 3.days.ago) } - let!(:dossier_brouillon) { create(:dossier, procedure: procedure) } + let(:dossier1) { create(:dossier, :en_construction, :for_individual, procedure: procedure, en_construction_at: 1.day.ago) } + let(:dossier2) { create(:dossier, :en_construction, :for_individual, procedure: procedure, en_construction_at: 3.days.ago) } + let!(:dossier_brouillon) { create(:dossier, :for_individual, procedure: procedure) } let(:dossiers) { [dossier2, dossier1, dossier] } let(:instructeur) { create(:instructeur, followed_dossiers: dossiers) } @@ -39,6 +40,11 @@ describe API::V2::GraphqlController do email } } + service { + nom + typeOrganisme + organisme + } champDescriptors { id type @@ -79,7 +85,7 @@ describe API::V2::GraphqlController do number: procedure.id, title: procedure.libelle, description: procedure.description, - state: 'brouillon', + state: 'publiee', dateFermeture: nil, dateCreation: procedure.created_at.iso8601, dateDerniereModification: procedure.updated_at.iso8601, @@ -89,6 +95,11 @@ describe API::V2::GraphqlController do label: "défaut" } ], + service: { + nom: procedure.service.nom, + typeOrganisme: procedure.service.type_organisme, + organisme: procedure.service.organisme + }, champDescriptors: procedure.types_de_champ.map do |tdc| { id: tdc.to_typed_id, @@ -149,6 +160,15 @@ describe API::V2::GraphqlController do id email } + demandeur { + id + ... on PersonnePhysique { + nom + prenom + civilite + dateDeNaissance + } + } instructeurs { id email @@ -177,45 +197,104 @@ describe API::V2::GraphqlController do }" end - it "should be returned" do - expect(gql_errors).to eq(nil) - expect(gql_data).to eq(dossier: { - id: dossier.to_typed_id, - number: dossier.id, - state: 'en_construction', - dateDerniereModification: dossier.updated_at.iso8601, - datePassageEnConstruction: dossier.en_construction_at.iso8601, - datePassageEnInstruction: nil, - dateTraitement: nil, - motivation: nil, - motivationAttachmentUrl: nil, - usager: { - id: dossier.user.to_typed_id, - email: dossier.user.email - }, - instructeurs: [ - { - id: instructeur.to_typed_id, - email: instructeur.email + context "with individual" do + it "should be returned" do + expect(gql_errors).to eq(nil) + expect(gql_data).to eq(dossier: { + id: dossier.to_typed_id, + number: dossier.id, + state: 'en_construction', + dateDerniereModification: dossier.updated_at.iso8601, + datePassageEnConstruction: dossier.en_construction_at.iso8601, + datePassageEnInstruction: nil, + dateTraitement: nil, + motivation: nil, + motivationAttachmentUrl: nil, + usager: { + id: dossier.user.to_typed_id, + email: dossier.user.email + }, + instructeurs: [ + { + id: instructeur.to_typed_id, + email: instructeur.email + } + ], + demandeur: { + id: dossier.individual.to_typed_id, + nom: dossier.individual.nom, + prenom: dossier.individual.prenom, + civilite: 'M', + dateDeNaissance: '1991-11-01' + }, + messages: dossier.commentaires.map do |commentaire| + { + body: commentaire.body, + attachmentUrl: nil, + email: commentaire.email + } + end, + avis: [], + champs: dossier.champs.map do |champ| + { + id: champ.to_typed_id, + label: champ.libelle, + stringValue: champ.for_api_v2 + } + end + }) + expect(gql_data[:dossier][:champs][0][:id]).to eq(dossier.champs[0].type_de_champ.to_typed_id) + end + end + + context "with entreprise" do + let(:procedure) { create(:procedure, :published, administrateurs: [admin]) } + let(:dossier) { create(:dossier, :en_construction, :with_entreprise, procedure: procedure) } + + let(:query) do + "{ + dossier(number: #{dossier.id}) { + id + number + usager { + id + email + } + demandeur { + id + ... on PersonneMorale { + siret + siegeSocial + entreprise { + siren + dateCreation + } + } + } } - ], - messages: dossier.commentaires.map do |commentaire| - { - body: commentaire.body, - attachmentUrl: nil, - email: commentaire.email + }" + end + + it "should be returned" do + expect(gql_errors).to eq(nil) + expect(gql_data).to eq(dossier: { + id: dossier.to_typed_id, + number: dossier.id, + usager: { + id: dossier.user.to_typed_id, + email: dossier.user.email + }, + demandeur: { + id: dossier.etablissement.to_typed_id, + siret: dossier.etablissement.siret, + siegeSocial: dossier.etablissement.siege_social, + entreprise: { + siren: dossier.etablissement.entreprise_siren, + dateCreation: dossier.etablissement.entreprise_date_creation.iso8601 + } } - end, - avis: [], - champs: dossier.champs.map do |champ| - { - id: champ.to_typed_id, - label: champ.libelle, - stringValue: champ.for_api_v2 - } - end - }) - expect(gql_data[:dossier][:champs][0][:id]).to eq(dossier.champs[0].type_de_champ.to_typed_id) + }) + end end end @@ -296,7 +375,7 @@ describe API::V2::GraphqlController do end describe 'dossierPasserEnInstruction' do - let(:dossier) { create(:dossier, :en_construction, procedure: procedure) } + let(:dossier) { create(:dossier, :en_construction, :for_individual, procedure: procedure) } let(:query) do "mutation { dossierPasserEnInstruction(input: { @@ -331,7 +410,7 @@ describe API::V2::GraphqlController do end context 'validation error' do - let(:dossier) { create(:dossier, :en_instruction, procedure: procedure) } + let(:dossier) { create(:dossier, :en_instruction, :for_individual, procedure: procedure) } it "should fail" do expect(gql_errors).to eq(nil) @@ -344,7 +423,7 @@ describe API::V2::GraphqlController do end describe 'dossierClasserSansSuite' do - let(:dossier) { create(:dossier, :en_instruction, procedure: procedure) } + let(:dossier) { create(:dossier, :en_instruction, :for_individual, procedure: procedure) } let(:query) do "mutation { dossierClasserSansSuite(input: { @@ -380,7 +459,7 @@ describe API::V2::GraphqlController do end context 'validation error' do - let(:dossier) { create(:dossier, :accepte, procedure: procedure) } + let(:dossier) { create(:dossier, :accepte, :for_individual, procedure: procedure) } it "should fail" do expect(gql_errors).to eq(nil) @@ -393,7 +472,7 @@ describe API::V2::GraphqlController do end describe 'dossierRefuser' do - let(:dossier) { create(:dossier, :en_instruction, procedure: procedure) } + let(:dossier) { create(:dossier, :en_instruction, :for_individual, procedure: procedure) } let(:query) do "mutation { dossierRefuser(input: { @@ -429,7 +508,7 @@ describe API::V2::GraphqlController do end context 'validation error' do - let(:dossier) { create(:dossier, :sans_suite, procedure: procedure) } + let(:dossier) { create(:dossier, :sans_suite, :for_individual, procedure: procedure) } it "should fail" do expect(gql_errors).to eq(nil) @@ -442,7 +521,7 @@ describe API::V2::GraphqlController do end describe 'dossierAccepter' do - let(:dossier) { create(:dossier, :en_instruction, procedure: procedure) } + let(:dossier) { create(:dossier, :en_instruction, :for_individual, procedure: procedure) } let(:query) do "mutation { dossierAccepter(input: { @@ -511,7 +590,7 @@ describe API::V2::GraphqlController do end context 'validation error' do - let(:dossier) { create(:dossier, :refuse, procedure: procedure) } + let(:dossier) { create(:dossier, :refuse, :for_individual, procedure: procedure) } it "should fail" do expect(gql_errors).to eq(nil) diff --git a/spec/mailers/dossier_mailer_spec.rb b/spec/mailers/dossier_mailer_spec.rb index d8c1b07f4..38c121a51 100644 --- a/spec/mailers/dossier_mailer_spec.rb +++ b/spec/mailers/dossier_mailer_spec.rb @@ -83,4 +83,27 @@ RSpec.describe DossierMailer, type: :mailer do it_behaves_like 'a dossier notification' end + + describe '.notify_brouillon_near_deletion' do + let(:dossier) { create(:dossier) } + + before do + duree = dossier.procedure.duree_conservation_dossiers_dans_ds + @date_suppression = dossier.created_at + duree.months + end + + subject { described_class.notify_brouillon_near_deletion(dossier.user, [dossier]) } + + it { expect(subject.body).to include("n° #{dossier.id} ") } + it { expect(subject.body).to include(dossier.procedure.libelle) } + end + + describe '.notify_brouillon_deletion' do + let(:dossier) { create(:dossier) } + + subject { described_class.notify_brouillon_deletion(dossier.user, [dossier.hash_for_deletion_mail]) } + + it { expect(subject.subject).to eq("Un dossier en brouillon a été supprimé automatiquement") } + it { expect(subject.body).to include("n° #{dossier.id} (#{dossier.procedure.libelle})") } + end end diff --git a/spec/mailers/previews/dossier_mailer_preview.rb b/spec/mailers/previews/dossier_mailer_preview.rb index 8ac4eab87..f1fc2dab7 100644 --- a/spec/mailers/previews/dossier_mailer_preview.rb +++ b/spec/mailers/previews/dossier_mailer_preview.rb @@ -20,6 +20,23 @@ class DossierMailerPreview < ActionMailer::Preview DossierMailer.notify_revert_to_instruction(dossier) end + def notify_brouillon_near_deletion + DossierMailer.notify_brouillon_near_deletion(User.new(email: "usager@example.com"), [dossier]) + end + + def notify_brouillons_near_deletion + DossierMailer.notify_brouillon_near_deletion(User.new(email: "usager@example.com"), [dossier, dossier]) + end + + def notify_brouillon_deletion + DossierMailer.notify_brouillon_deletion(User.new(email: "usager@example.com"), [dossier.hash_for_deletion_mail]) + end + + def notify_brouillons_deletion + dossier_hashes = [dossier, dossier].map(&:hash_for_deletion_mail) + DossierMailer.notify_brouillon_deletion(User.new(email: "usager@example.com"), dossier_hashes) + end + private def deleted_dossier diff --git a/spec/models/dossier_spec.rb b/spec/models/dossier_spec.rb index 70e141121..e0e31b9b4 100644 --- a/spec/models/dossier_spec.rb +++ b/spec/models/dossier_spec.rb @@ -1047,4 +1047,57 @@ describe Dossier do it { expect(Dossier.for_procedure(procedure_1)).to contain_exactly(dossier_1_1, dossier_1_2) } it { expect(Dossier.for_procedure(procedure_2)).to contain_exactly(dossier_2_1) } end + + describe '#send_brouillon_expiration_notices' do + let!(:procedure) { create(:procedure, duree_conservation_dossiers_dans_ds: 6) } + let!(:date_close_to_expiration) { Date.today - procedure.duree_conservation_dossiers_dans_ds.months + 1.month } + let!(:date_expired) { Date.today - procedure.duree_conservation_dossiers_dans_ds.months - 6.days } + let!(:date_not_expired) { Date.today - procedure.duree_conservation_dossiers_dans_ds.months + 2.months } + + before { Timecop.freeze(Time.zone.parse('12/12/2012').beginning_of_day) } + after { Timecop.return } + + context "Envoi de message pour les dossiers expirant dans - d'un mois" do + let!(:expired_brouillon) { create(:dossier, procedure: procedure, created_at: date_expired) } + let!(:brouillon_close_to_expiration) { create(:dossier, procedure: procedure, created_at: date_close_to_expiration) } + let!(:brouillon_close_but_with_notice_sent) { create(:dossier, procedure: procedure, created_at: date_close_to_expiration, brouillon_close_to_expiration_notice_sent_at: Time.zone.now) } + let!(:valid_brouillon) { create(:dossier, procedure: procedure, created_at: date_not_expired) } + + before do + allow(DossierMailer).to receive(:notify_brouillon_near_deletion).and_return(double(deliver_later: nil)) + Dossier.send_brouillon_expiration_notices + end + + it 'verification de la creation de mail' do + expect(DossierMailer).to have_received(:notify_brouillon_near_deletion).with(brouillon_close_to_expiration.user, [brouillon_close_to_expiration]) + expect(DossierMailer).to have_received(:notify_brouillon_near_deletion).with(expired_brouillon.user, [expired_brouillon]) + end + + it 'Verification du changement d etat du champ' do + expect(brouillon_close_to_expiration.reload.brouillon_close_to_expiration_notice_sent_at).not_to be_nil + end + end + end + + describe '#destroy_brouillons_and_notify' do + let!(:today) { Time.zone.now.at_midnight } + + let!(:expired_brouillon) { create(:dossier, brouillon_close_to_expiration_notice_sent_at: today - (Dossier::DRAFT_EXPIRATION + 1.day)) } + let!(:other_brouillon) { create(:dossier, brouillon_close_to_expiration_notice_sent_at: today - (Dossier::DRAFT_EXPIRATION - 1.day)) } + + before do + allow(DossierMailer).to receive(:notify_brouillon_deletion).and_return(double(deliver_later: nil)) + Dossier.destroy_brouillons_and_notify + end + + it 'notifies deletion' do + expect(DossierMailer).to have_received(:notify_brouillon_deletion).once + expect(DossierMailer).to have_received(:notify_brouillon_deletion).with(expired_brouillon.user, [expired_brouillon.hash_for_deletion_mail]) + end + + it 'deletes the expired brouillon' do + expect(DeletedDossier.find_by(dossier_id: expired_brouillon.id)).to be_present + expect { expired_brouillon.reload }.to raise_error(ActiveRecord::RecordNotFound) + end + end end diff --git a/spec/models/service_spec.rb b/spec/models/service_spec.rb index e34f56dac..b8a4c479a 100644 --- a/spec/models/service_spec.rb +++ b/spec/models/service_spec.rb @@ -7,7 +7,7 @@ describe Service, type: :model do organisme: 'mairie des iles', type_organisme: Service.type_organismes.fetch(:association), email: 'super@email.com', - telephone: '1212202', + telephone: '012345678', horaires: 'du lundi au vendredi', adresse: '12 rue des schtroumpfs', administrateur_id: administrateur.id @@ -16,6 +16,33 @@ describe Service, type: :model do it { expect(Service.new(params).valid?).to be_truthy } + it 'should forbid invalid phone numbers' do + service = Service.create(params) + invalid_phone_numbers = ["1", "Néant", "01 60 50 40 30 20"] + + invalid_phone_numbers.each do |tel| + service.telephone = tel + expect(service.valid?).to be_falsey + end + end + + it 'should accept no phone numbers' do + service = Service.create(params) + service.telephone = nil + + expect(service.valid?).to be_truthy + end + + it 'should accept valid phone numbers' do + service = Service.create(params) + valid_phone_numbers = ["3646", "273115", "0160376983", "01 60 50 40 30 ", "+33160504030"] + + valid_phone_numbers.each do |tel| + service.telephone = tel + expect(service.valid?).to be_truthy + end + end + context 'when a first service exists' do before { Service.create(params) }