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) }