diff --git a/app/graphql/api/v2/stored_query.rb b/app/graphql/api/v2/stored_query.rb index e2b5c78c0..1c48a5bd1 100644 --- a/app/graphql/api/v2/stored_query.rb +++ b/app/graphql/api/v2/stored_query.rb @@ -819,6 +819,19 @@ class API::V2::StoredQuery } } + mutation dossierSupprimerMessage($input: DossierSupprimerMessageInput!) { + dossierSupprimerMessage(input: $input) { + message { + id + createdAt + discardedAt + } + errors { + message + } + } + } + mutation dossierModifierAnnotationText( $input: DossierModifierAnnotationTextInput! ) { diff --git a/app/graphql/mutations/dossier_passer_en_instruction.rb b/app/graphql/mutations/dossier_passer_en_instruction.rb index 1ece00ede..d9886b724 100644 --- a/app/graphql/mutations/dossier_passer_en_instruction.rb +++ b/app/graphql/mutations/dossier_passer_en_instruction.rb @@ -21,6 +21,9 @@ module Mutations if !dossier.en_construction? return false, { errors: ["Le dossier est déjà #{dossier_display_state(dossier, lower: true)}"] } end + if dossier.blocked_with_pending_correction? + return false, { errors: ["Le dossier est en attente de correction"] } + end dossier_authorized_for?(dossier, instructeur) end end diff --git a/app/graphql/mutations/dossier_supprimer_message.rb b/app/graphql/mutations/dossier_supprimer_message.rb new file mode 100644 index 000000000..bde6de51f --- /dev/null +++ b/app/graphql/mutations/dossier_supprimer_message.rb @@ -0,0 +1,24 @@ +module Mutations + class DossierSupprimerMessage < Mutations::BaseMutation + description "Supprimer un message." + + argument :message_id, ID, required: true, loads: Types::MessageType + argument :instructeur_id, ID, required: true, loads: Types::ProfileType + + field :message, Types::MessageType, null: true + field :errors, [Types::ValidationErrorType], null: true + + def resolve(message:, **args) + message.soft_delete! + + { message: } + end + + def authorized?(message:, instructeur:, **args) + if !message.soft_deletable?(instructeur) + return false, { errors: ["Le message ne peut pas être supprimé"] } + end + dossier_authorized_for?(message.dossier, instructeur) + end + end +end diff --git a/app/graphql/schema.graphql b/app/graphql/schema.graphql index 52d58d1ad..c422408fe 100644 --- a/app/graphql/schema.graphql +++ b/app/graphql/schema.graphql @@ -2192,6 +2192,30 @@ enum DossierState { sans_suite } +""" +Autogenerated input type of DossierSupprimerMessage +""" +input DossierSupprimerMessageInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + instructeurId: ID! + messageId: ID! +} + +""" +Autogenerated return type of DossierSupprimerMessage. +""" +type DossierSupprimerMessagePayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + errors: [ValidationError!] + message: Message +} + type DropDownListChampDescriptor implements ChampDescriptor { """ Description des champs d’un bloc répétable. @@ -3084,6 +3108,7 @@ type Message { body: String! correction: Correction createdAt: ISO8601DateTime! + discardedAt: ISO8601DateTime email: String! id: ID! } @@ -3313,6 +3338,16 @@ type Mutation { input: DossierRepasserEnInstructionInput! ): DossierRepasserEnInstructionPayload + """ + Supprimer un message. + """ + dossierSupprimerMessage( + """ + Parameters for DossierSupprimerMessage + """ + input: DossierSupprimerMessageInput! + ): DossierSupprimerMessagePayload + """ Ajouter des instructeurs à un groupe instructeur. """ diff --git a/app/graphql/types/message_type.rb b/app/graphql/types/message_type.rb index 70622647b..788075bb2 100644 --- a/app/graphql/types/message_type.rb +++ b/app/graphql/types/message_type.rb @@ -4,6 +4,7 @@ module Types field :email, String, null: false field :body, String, null: false field :created_at, GraphQL::Types::ISO8601DateTime, null: false + field :discarded_at, GraphQL::Types::ISO8601DateTime, null: true field :attachment, Types::File, null: true, deprecation_reason: "Utilisez le champ `attachments` à la place.", extensions: [ { Extensions::Attachment => { attachments: :piece_jointe, as: :single } } ] @@ -19,5 +20,9 @@ module Types def correction Loaders::Association.for(object.class, :dossier_correction).load(object) end + + def self.authorized?(object, context) + context.authorized_demarche?(object.dossier.revision.procedure) + end end end diff --git a/app/graphql/types/mutation_type.rb b/app/graphql/types/mutation_type.rb index ce2f8ac56..b9ad5a92c 100644 --- a/app/graphql/types/mutation_type.rb +++ b/app/graphql/types/mutation_type.rb @@ -3,6 +3,7 @@ module Types field :create_direct_upload, mutation: Mutations::CreateDirectUpload field :dossier_envoyer_message, mutation: Mutations::DossierEnvoyerMessage + field :dossier_supprimer_message, mutation: Mutations::DossierSupprimerMessage field :dossier_passer_en_instruction, mutation: Mutations::DossierPasserEnInstruction field :dossier_classer_sans_suite, mutation: Mutations::DossierClasserSansSuite field :dossier_refuser, mutation: Mutations::DossierRefuser diff --git a/app/models/dossier.rb b/app/models/dossier.rb index 123c7768c..54c187169 100644 --- a/app/models/dossier.rb +++ b/app/models/dossier.rb @@ -558,8 +558,12 @@ class Dossier < ApplicationRecord false end + def blocked_with_pending_correction? + procedure.feature_enabled?(:blocking_pending_correction) && pending_correction? + end + def can_passer_en_instruction? - return false if procedure.feature_enabled?(:blocking_pending_correction) && pending_correction? + return false if blocked_with_pending_correction? true end diff --git a/spec/controllers/api/v2/graphql_controller_stored_queries_spec.rb b/spec/controllers/api/v2/graphql_controller_stored_queries_spec.rb index 6fa77c40b..a42d90210 100644 --- a/spec/controllers/api/v2/graphql_controller_stored_queries_spec.rb +++ b/spec/controllers/api/v2/graphql_controller_stored_queries_spec.rb @@ -912,6 +912,17 @@ describe API::V2::GraphqlController do expect(ActionMailer::Base.deliveries.size).to eq(0) } end + + context 'with pending corrections' do + before { Flipper.enable(:blocking_pending_correction, dossier.procedure) } + let!(:dossier_correction) { create(:dossier_correction, dossier:) } + + it { + expect(dossier.pending_correction?).to be_truthy + expect(gql_errors).to be_nil + expect(gql_data[:dossierPasserEnInstruction][:errors]).to eq([{ message: "Le dossier est en attente de correction" }]) + } + end end context 'dossierRepasserEnConstruction' do @@ -1332,5 +1343,77 @@ describe API::V2::GraphqlController do } end end + + context 'dossierEnvoyerMessage' do + let(:dossier) { create(:dossier, :en_construction, :with_individual, procedure:) } + let(:variables) { { input: { dossierId: dossier.to_typed_id, instructeurId: instructeur.to_typed_id, body: 'Hello World!' } } } + let(:operation_name) { 'dossierEnvoyerMessage' } + + it { + expect(gql_errors).to be_nil + expect(gql_data[:dossierEnvoyerMessage][:errors]).to be_nil + expect(gql_data[:dossierEnvoyerMessage][:message][:id]).to eq(dossier.commentaires.first.to_typed_id) + perform_enqueued_jobs + expect(ActionMailer::Base.deliveries.size).to eq(1) + } + end + + context 'dossierSupprimerMessage' do + let(:dossier) { create(:dossier, :en_construction, :with_individual, procedure:) } + let(:message) { create(:commentaire, dossier:, instructeur:) } + let(:dossier_correction) { create(:dossier_correction, dossier:, commentaire: message) } + let(:variables) { { input: { messageId: message.to_typed_id, instructeurId: instructeur.to_typed_id } } } + let(:operation_name) { 'dossierSupprimerMessage' } + + it { + expect(message.discarded?).to be_falsey + expect(gql_errors).to be_nil + expect(gql_data[:dossierSupprimerMessage][:errors]).to be_nil + expect(gql_data[:dossierSupprimerMessage][:message][:id]).to eq(message.to_typed_id) + expect(gql_data[:dossierSupprimerMessage][:message][:discardedAt]).not_to be_nil + expect(message.reload.discarded?).to be_truthy + } + + it { + expect(dossier_correction.commentaire.discarded?).to be_falsey + expect(dossier.pending_correction?).to be_truthy + expect(gql_errors).to be_nil + expect(gql_data[:dossierSupprimerMessage][:errors]).to be_nil + expect(gql_data[:dossierSupprimerMessage][:message][:id]).to eq(message.to_typed_id) + expect(gql_data[:dossierSupprimerMessage][:message][:discardedAt]).not_to be_nil + expect(message.reload.discarded?).to be_truthy + expect(dossier.pending_correction?).to be_falsey + } + + context 'when unauthorized' do + let(:dossier) { create(:dossier, :en_construction, :with_individual) } + + it { + expect(message.discarded?).to be_falsey + expect(gql_errors.first[:message]).to eq("An object of type Message was hidden due to permissions") + } + end + + context 'when from not the same instructeur' do + let(:other_instructeur) { create(:instructeur, followed_dossiers: dossiers) } + let(:variables) { { input: { messageId: message.to_typed_id, instructeurId: other_instructeur.to_typed_id } } } + + it { + expect(message.discarded?).to be_falsey + expect(gql_errors).to be_nil + expect(gql_data[:dossierSupprimerMessage][:errors]).to eq([{ message: "Le message ne peut pas être supprimé" }]) + } + end + + context 'when from usager' do + let(:message) { create(:commentaire, dossier:) } + + it { + expect(message.discarded?).to be_falsey + expect(gql_errors).to be_nil + expect(gql_data[:dossierSupprimerMessage][:errors]).to eq([{ message: "Le message ne peut pas être supprimé" }]) + } + end + end end end