diff --git a/app/graphql/mutations/upsert_assignment.rb b/app/graphql/mutations/upsert_assignment.rb new file mode 100644 index 00000000..4a946782 --- /dev/null +++ b/app/graphql/mutations/upsert_assignment.rb @@ -0,0 +1,54 @@ +module Mutations + class UpsertAssignment < BaseMutation + description "Create or update an assignment." + + # arguments passed to the `resolve` method + argument :id, ID, required: false, description: "The ID of the assignment to update." + argument :project_id, ID, required: true, description: "The ID of the project this assignment is being created for." + argument :user_id, ID, required: true, description: "The ID of the user being assigned to the project." + argument :status, String, required: true, description: "The status of the assignment." + argument :starts_on, GraphQL::Types::ISO8601Date, required: false, description: "The date this assignment starts." + argument :ends_on, GraphQL::Types::ISO8601Date, required: false, description: "The date this assignment ends." + + # return type from the mutation + type Types::StaffPlan::AssignmentType + + def resolve(id: nil, project_id:, user_id:, status:, starts_on: nil, ends_on: nil) + current_company = context[:current_company] + + # try and find the assignment + assignment = if id.present? + current_company.assignments.find(id) + end + + if assignment + assignment.assign_attributes(project_id:, user_id:, status:) + else + project = current_company.projects.find(project_id) + assignment = project.assignments.new(user_id:, status:) + end + + assignment.assign_attributes(starts_on: starts_on) if starts_on + assignment.assign_attributes(ends_on: ends_on) if ends_on + + if assignment.valid? + assignment.save! + else + assignment.errors.group_by_attribute.each do |attribute, errors| + errors.each do |error| + context.add_error( + GraphQL::ExecutionError.new( + error.full_message, + extensions: { + attribute: attribute.to_s, + } + ) + ) + end + end + end + + assignment + end + end +end \ No newline at end of file diff --git a/app/graphql/schema.graphql b/app/graphql/schema.graphql index 6767bb08..7af40a39 100644 --- a/app/graphql/schema.graphql +++ b/app/graphql/schema.graphql @@ -48,6 +48,41 @@ type Mutation { companyId: ID! ): Company! + """ + Create or update an assignment. + """ + upsertAssignment( + """ + The date this assignment ends. + """ + endsOn: ISO8601Date + + """ + The ID of the assignment to update. + """ + id: ID + + """ + The ID of the project this assignment is being created for. + """ + projectId: ID! + + """ + The date this assignment starts. + """ + startsOn: ISO8601Date + + """ + The status of the assignment. + """ + status: String! + + """ + The ID of the user being assigned to the project. + """ + userId: ID! + ): Assignment! + """ Create or update a work week record for a StaffPlan user. """ diff --git a/app/graphql/types/mutation_type.rb b/app/graphql/types/mutation_type.rb index 6087ce9f..fb49cd6e 100644 --- a/app/graphql/types/mutation_type.rb +++ b/app/graphql/types/mutation_type.rb @@ -4,5 +4,6 @@ module Types class MutationType < Types::BaseObject field :set_current_company, mutation: Mutations::SetCurrentCompany field :upsert_work_week, mutation: Mutations::UpsertWorkWeek + field :upsert_assignment, mutation: Mutations::UpsertAssignment end end diff --git a/app/models/assignment.rb b/app/models/assignment.rb index 7194ab64..914fdd80 100644 --- a/app/models/assignment.rb +++ b/app/models/assignment.rb @@ -16,11 +16,19 @@ class Assignment < ApplicationRecord validates :project_id, presence: true, uniqueness: { scope: :user_id } validates :status, presence: true, inclusion: { in: VALID_STATUSES } validate :starts_and_ends_on_rules + validate :project_and_user_belong_to_same_company scope :for_user, ->(user) { where(user: user) } private + def project_and_user_belong_to_same_company + project_company_users = project.company.active_users + return if project_company_users.include?(user) + + errors.add(:project, "and user must belong to the same company") + end + def starts_and_ends_on_rules if starts_on.blank? && ends_on.present? errors.add(:starts_on, "is required if an end date is set") diff --git a/app/models/company.rb b/app/models/company.rb index c54f1655..6f4d7982 100644 --- a/app/models/company.rb +++ b/app/models/company.rb @@ -8,6 +8,10 @@ class Company < ApplicationRecord validates :name, presence: true, uniqueness: { case_sensitive: false } + def active_users + users.joins(:memberships).where(memberships: { status: Membership::ACTIVE }) + end + def subscription @_subscription if defined?(@_subscription) diff --git a/spec/graphql/mutations/upsert_assignment_spec.rb b/spec/graphql/mutations/upsert_assignment_spec.rb new file mode 100644 index 00000000..07ca8cf9 --- /dev/null +++ b/spec/graphql/mutations/upsert_assignment_spec.rb @@ -0,0 +1,177 @@ +# frozen_string_literal: true + +require "rails_helper" + +RSpec.describe Mutations::UpsertAssignment do + + context "resolve" do + it "creates a new assignment with valid params" do + query_string = <<-GRAPHQL + mutation($projectId: ID!, $userId: ID!, $status: String!) { + upsertAssignment(projectId: $projectId, userId: $userId, status: $status) { + id + project { + id + } + user { + id + } + status + startsOn + endsOn + } + } + GRAPHQL + + user = create(:user) + project = create(:project, company: user.current_company) + + result = StaffplanReduxSchema.execute( + query_string, + context: { + current_user: user, + current_company: user.current_company + }, + variables: { + projectId: project.id, + userId: user.id, + status: Assignment::PROPOSED + } + ) + + post_result = result["data"]["upsertAssignment"] + expect(result["errors"]).to be_nil + expect(post_result["project"]["id"]).to eq(project.id.to_s) + expect(post_result["user"]["id"]).to eq(user.id.to_s) + expect(post_result["status"]).to eq(Assignment::PROPOSED) + expect(post_result["startsOn"]).to be_nil + expect(post_result["endsOn"]).to be_nil + end + + it "updates the assignment with valid params" do + query_string = <<-GRAPHQL + mutation($id: ID, $projectId: ID!, $userId: ID!, $status: String!, $startsOn: ISO8601Date, $endsOn: ISO8601Date) { + upsertAssignment(id: $id, projectId: $projectId, userId: $userId, status: $status, startsOn: $startsOn, endsOn: $endsOn) { + id + project { + id + } + user { + id + } + status + startsOn + endsOn + } + } + GRAPHQL + + user = create(:user) + assignment = assignment_for_user(user:, status: Assignment::PROPOSED) + + result = StaffplanReduxSchema.execute( + query_string, + context: { + current_user: user, + current_company: user.current_company + }, + variables: { + id: assignment.id, + projectId: assignment.project_id, + userId: assignment.user_id, + status: Assignment::ACTIVE, + startsOn: starts_on = 2.weeks.from_now.to_date.iso8601, + endsOn: ends_on = 10.weeks.from_now.to_date.iso8601 + } + ) + + post_result = result["data"]["upsertAssignment"] + expect(result["errors"]).to be_nil + expect(post_result["status"]).to eq(Assignment::ACTIVE) + expect(post_result["startsOn"]).to eq(starts_on.to_s) + expect(post_result["endsOn"]).to eq(ends_on.to_s) + end + + it "renders validation errors" do + query_string = <<-GRAPHQL + mutation($id: ID, $projectId: ID!, $userId: ID!, $status: String!) { + upsertAssignment(id: $id, projectId: $projectId, userId: $userId, status: $status) { + id + project { + id + } + user { + id + } + status + startsOn + endsOn + } + } + GRAPHQL + + user = create(:user) + assignment = assignment_for_user(user:) + + result = StaffplanReduxSchema.execute( + query_string, + context: { + current_user: user, + current_company: user.current_company + }, + variables: { + id: assignment.id, + projectId: create(:project).id, + userId: assignment.user_id, + status: "invalid-status", + } + ) + + post_result = result["errors"] + expect(post_result.length).to eq(2) + expect(post_result.first["message"]).to eq("Status is not included in the list") + expect(post_result.first["extensions"]["attribute"]).to eq("status") + expect(post_result.second["message"]).to eq("Project and user must belong to the same company") + expect(post_result.second["extensions"]["attribute"]).to eq("project") + end + + it "raises a 404 if given an assignment id that doesn't exist on the company" do + query_string = <<-GRAPHQL + mutation($id: ID, $projectId: ID!, $userId: ID!, $status: String!) { + upsertAssignment(id: $id, projectId: $projectId, userId: $userId, status: $status) { + id + project { + id + } + user { + id + } + status + startsOn + endsOn + } + } + GRAPHQL + + user = create(:user) + assignment = assignment_for_user(user:) + + result = StaffplanReduxSchema.execute( + query_string, + context: { + current_user: user, + current_company: user.current_company + }, + variables: { + id: assignment_for_user(user: create(:user)).id, + projectId: assignment.project_id, + userId: assignment.user_id, + status: Assignment::ACTIVE, + } + ) + + post_result = result["errors"] + expect(post_result.first["message"]).to eq("Assignment not found") + end + end +end \ No newline at end of file diff --git a/spec/graphql/mutations/upsert_work_week_spec.rb b/spec/graphql/mutations/upsert_work_week_spec.rb index 04295543..789313a1 100644 --- a/spec/graphql/mutations/upsert_work_week_spec.rb +++ b/spec/graphql/mutations/upsert_work_week_spec.rb @@ -4,14 +4,6 @@ RSpec.describe Mutations::UpsertWorkWeek do - # when the :work_weeks factory is used the user's current_company - # is not the same as the one the project/assignment belongs to. - def assignment_for_user(user:) - client = create(:client, company: user.current_company) - project = create(:project, client:) - create(:assignment, user:, project:) - end - context "when updating a work week" do it "updates the work week with valid params" do query_string = <<-GRAPHQL diff --git a/spec/models/assignment_spec.rb b/spec/models/assignment_spec.rb index 5726db94..eea9c7c0 100644 --- a/spec/models/assignment_spec.rb +++ b/spec/models/assignment_spec.rb @@ -43,4 +43,16 @@ expect(assignment.errors[:ends_on]).to include("is required if a start date is set") end end + + describe "#project_and_user_belong_to_same_company" do + it "validates that the user and project belong to the same company" do + user = create(:user) + project = create(:project) + assignment = build(:assignment, user: user, project: project) + expect(project.company.active_users).to_not include(user) + + expect(assignment).to_not be_valid + expect(assignment.errors[:project]).to include("user must belong to the same company as the project") + end + end end diff --git a/spec/support/helper_methods.rb b/spec/support/helper_methods.rb new file mode 100644 index 00000000..f44c756c --- /dev/null +++ b/spec/support/helper_methods.rb @@ -0,0 +1,7 @@ +# when the :work_weeks factory is used the user's current_company +# is not the same as the one the project/assignment belongs to. +def assignment_for_user(user:, status: Assignment::ACTIVE) + client = create(:client, company: user.current_company) + project = create(:project, client:) + create(:assignment, user:, project:, status:) +end \ No newline at end of file