Merge pull request #4612 from betagouv/dev

2019-12-04-01
This commit is contained in:
Paul Chavard 2019-12-04 14:52:44 +01:00 committed by GitHub
commit 75df51a783
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
38 changed files with 704 additions and 67 deletions

View file

@ -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'

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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:

View file

@ -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 denseignement
"""
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
"""

View file

@ -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

View file

@ -0,0 +1,7 @@
module Types::Champs
class CiviliteChampType < Types::BaseObject
implements Types::ChampType
field :value, Types::Civilite, null: true
end
end

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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"

View file

@ -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"

View file

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

View file

@ -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

View file

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

View file

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

View file

@ -0,0 +1,2 @@
Phonelib.default_country = "FR"
Phonelib.parse_special = true

View file

@ -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é

View file

@ -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 :"

View file

@ -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 <b>un mois</b>"
other: "Si vous souhaitez toujours déposer ces dossiers, vous pouvez retrouver vos brouillons pendant encore <b>un mois</b>"

View file

@ -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

View file

@ -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"

View file

@ -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

View file

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

View file

@ -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(" #{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(" #{dossier.id} (#{dossier.procedure.libelle})") }
end
end

View file

@ -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

View file

@ -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

View file

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