From 44b287d56028bc4845906ff92800d0b51ffc3d1a Mon Sep 17 00:00:00 2001 From: Rob Sterner Date: Tue, 5 Mar 2024 18:44:48 -0500 Subject: [PATCH 1/7] unrelated, this is not needed --- app/sidekiq/sync_customer_subscription_job.rb | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/app/sidekiq/sync_customer_subscription_job.rb b/app/sidekiq/sync_customer_subscription_job.rb index a09ffbc8..44660339 100644 --- a/app/sidekiq/sync_customer_subscription_job.rb +++ b/app/sidekiq/sync_customer_subscription_job.rb @@ -11,8 +11,7 @@ def perform(id) company.subscription.stripe_id, { items: [ {id: company.subscription.item_id, quantity: subscription_count } - ]}, - proration_date: Date.today.iso8601 + ]} ) end end From 57bb6a16e2f774f782a49d7561eb9c4313d4c87c Mon Sep 17 00:00:00 2001 From: Rob Sterner Date: Tue, 5 Mar 2024 19:32:21 -0500 Subject: [PATCH 2/7] adds cost to the Project type definition --- app/graphql/schema.graphql | 1 + app/graphql/types/staff_plan/project_type.rb | 1 + 2 files changed, 2 insertions(+) diff --git a/app/graphql/schema.graphql b/app/graphql/schema.graphql index 03ec6dde..ae286593 100644 --- a/app/graphql/schema.graphql +++ b/app/graphql/schema.graphql @@ -140,6 +140,7 @@ type Mutation { type Project { assignments: [Assignment!]! client: Client! + cost: Float! createdAt: ISO8601DateTime! endsOn: ISO8601Date id: ID! diff --git a/app/graphql/types/staff_plan/project_type.rb b/app/graphql/types/staff_plan/project_type.rb index 0850b713..c8a10e0e 100644 --- a/app/graphql/types/staff_plan/project_type.rb +++ b/app/graphql/types/staff_plan/project_type.rb @@ -7,6 +7,7 @@ class ProjectType < Types::BaseObject field :client, Types::StaffPlan::ClientType, null: false field :name, String, null: false field :status, String, null: false + field :cost, Float, null: false field :payment_frequency, String, null: false field :starts_on, GraphQL::Types::ISO8601Date, null: true field :ends_on, GraphQL::Types::ISO8601Date, null: true From 24cd5b3e5250f1146bb39e0a7d7656ecae3ac93d Mon Sep 17 00:00:00 2001 From: Rob Sterner Date: Tue, 5 Mar 2024 19:32:41 -0500 Subject: [PATCH 3/7] relax some validations on Project fields (there are DB level defaults) --- app/models/project.rb | 6 +++--- spec/models/project_spec.rb | 3 --- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/app/models/project.rb b/app/models/project.rb index 227814e0..71f58c72 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -25,9 +25,9 @@ class Project < ApplicationRecord validates :client_id, presence: true validates :name, presence: true, uniqueness: { case_sensitive: false } - validates :status, presence: true, inclusion: { in: VALID_STATUSES } - validates :cost, presence: true, numericality: { greater_than_or_equal_to: 0.0 } - validates :payment_frequency, presence: true, inclusion: { in: VALID_PAYMENT_FREQUENCIES } + validates :status, inclusion: { in: VALID_STATUSES }, allow_blank: true + validates :cost, numericality: { greater_than_or_equal_to: 0.0 }, allow_blank: true + validates :payment_frequency, inclusion: { in: VALID_PAYMENT_FREQUENCIES }, allow_blank: true def active? status == ACTIVE diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index 6b91f98e..01b76137 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -6,11 +6,8 @@ it { should validate_presence_of(:client_id) } it { should validate_presence_of(:name) } - it { should validate_presence_of(:status) } it { should validate_inclusion_of(:status).in_array(%w(active archived)) } - it { should validate_presence_of(:cost) } it { should validate_numericality_of(:cost).is_greater_than_or_equal_to(0.0) } - it { should validate_presence_of(:payment_frequency) } it { should validate_inclusion_of(:payment_frequency).in_array(%w(weekly monthly fortnightly quarterly annually)) } end From a323b1d1bc871c27610bd08fcbe93c39e3e7ec75 Mon Sep 17 00:00:00 2001 From: Rob Sterner Date: Tue, 5 Mar 2024 19:32:58 -0500 Subject: [PATCH 4/7] companies need a user --- spec/factories.rb | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/spec/factories.rb b/spec/factories.rb index 819b4b0a..07d18c8e 100644 --- a/spec/factories.rb +++ b/spec/factories.rb @@ -27,6 +27,12 @@ factory :company do name { Faker::Company.name } + + after(:build) do |company, _options| + next if company.memberships.any? + + company.memberships << build(:membership, company: company) + end end factory :membership do From de36d3b81e497da6d9ca2d878229b0d5a08db61e Mon Sep 17 00:00:00 2001 From: Rob Sterner Date: Tue, 5 Mar 2024 19:33:33 -0500 Subject: [PATCH 5/7] adds upsertProject --- app/graphql/mutations/upsert_project.rb | 57 +++++ app/graphql/schema.graphql | 45 ++++ app/graphql/types/mutation_type.rb | 1 + spec/graphql/mutations/upsert_project_spec.rb | 198 ++++++++++++++++++ 4 files changed, 301 insertions(+) create mode 100644 app/graphql/mutations/upsert_project.rb create mode 100644 spec/graphql/mutations/upsert_project_spec.rb diff --git a/app/graphql/mutations/upsert_project.rb b/app/graphql/mutations/upsert_project.rb new file mode 100644 index 00000000..b424fc84 --- /dev/null +++ b/app/graphql/mutations/upsert_project.rb @@ -0,0 +1,57 @@ +module Mutations + class UpsertProject < BaseMutation + description "Create or update a project." + + # arguments passed to the `resolve` method + argument :id, ID, required: false, description: "The ID of the project to update." + argument :client_id, ID, required: false, description: "The ID of the client for this project." + argument :name, String, required: false, description: "The name of the project." + argument :status, String, required: false, description: "The status of the assignment." + argument :cost, Float, required: false, description: "The cost of the project." + argument :payment_frequency, String, required: false, description: "The frequency of payment for the project." + 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::ProjectType + + def resolve(id: nil, client_id: nil, name: nil, status: nil, cost: nil, payment_frequency: nil, starts_on: nil, ends_on: nil) + current_company = context[:current_company] + + # try and find the assignment + project = if id.present? + current_company.projects.find(id) + end + + if project.blank? + project = current_company.projects.new(client_id:, name:, status:) + end + + project.assign_attributes(name:) if name.present? + project.assign_attributes(status:) if status.present? + project.assign_attributes(cost:) if cost.present? + project.assign_attributes(payment_frequency:) if payment_frequency.present? + project.assign_attributes(starts_on:) if starts_on.present? + project.assign_attributes(ends_on:) if ends_on.present? + + if project.valid? + project.save! + else + project.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 + + project + end + end +end \ No newline at end of file diff --git a/app/graphql/schema.graphql b/app/graphql/schema.graphql index ae286593..0c7b3756 100644 --- a/app/graphql/schema.graphql +++ b/app/graphql/schema.graphql @@ -106,6 +106,51 @@ type Mutation { userId: ID! ): Assignment! + """ + Create or update a project. + """ + upsertProject( + """ + The ID of the client for this project. + """ + clientId: ID! + + """ + The cost of the project. + """ + cost: Float + + """ + The date this assignment ends. + """ + endsOn: ISO8601Date + + """ + The ID of the project to update. + """ + id: ID + + """ + The name of the project. + """ + name: String! + + """ + The frequency of payment for the project. + """ + paymentFrequency: String + + """ + The date this assignment starts. + """ + startsOn: ISO8601Date + + """ + The status of the assignment. + """ + status: String! + ): Project! + """ 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 fb49cd6e..9fb58cbc 100644 --- a/app/graphql/types/mutation_type.rb +++ b/app/graphql/types/mutation_type.rb @@ -5,5 +5,6 @@ class MutationType < Types::BaseObject field :set_current_company, mutation: Mutations::SetCurrentCompany field :upsert_work_week, mutation: Mutations::UpsertWorkWeek field :upsert_assignment, mutation: Mutations::UpsertAssignment + field :upsert_project, mutation: Mutations::UpsertProject end end diff --git a/spec/graphql/mutations/upsert_project_spec.rb b/spec/graphql/mutations/upsert_project_spec.rb new file mode 100644 index 00000000..276fcb17 --- /dev/null +++ b/spec/graphql/mutations/upsert_project_spec.rb @@ -0,0 +1,198 @@ +# 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($clientId: ID, $name: String, $status: String) { + upsertProject(clientId: $clientId, name: $name, status: $status) { + id + client { + id + } + name + status + startsOn + endsOn + } + } + GRAPHQL + + user = create(:user) + client = create(:client, company: user.current_company) + + result = StaffplanReduxSchema.execute( + query_string, + context: { + current_user: user, + current_company: user.current_company + }, + variables: { + clientId: client.id, + name: project_name = Faker::Company.buzzword, + status: Project::PROPOSED + } + ) + + post_result = result["data"]["upsertProject"] + expect(result["errors"]).to be_nil + expect(post_result["client"]["id"]).to eq(client.id.to_s) + expect(post_result["name"]).to eq(project_name) + expect(post_result["status"]).to eq(Project::PROPOSED) + expect(post_result["startsOn"]).to be_nil + expect(post_result["endsOn"]).to be_nil + end + + it "updates a project with valid params" do + query_string = <<-GRAPHQL + mutation($id: ID, $clientId: ID, $name: String, $status: String, $cost: Float, $paymentFrequency: String, $startsOn: ISO8601Date, $endsOn: ISO8601Date) { + upsertProject(id: $id, clientId: $clientId, name: $name, status: $status, cost: $cost, paymentFrequency: $paymentFrequency, startsOn: $startsOn, endsOn: $endsOn) { + id + client { + id + } + name + cost + paymentFrequency + status + startsOn + endsOn + } + } + GRAPHQL + + project = create(:project) + user = User.find_by(current_company_id: project.company.id) + + result = StaffplanReduxSchema.execute( + query_string, + context: { + current_user: user, + current_company: user.current_company + }, + variables: { + id: project.id, + clientId: project.client.id, + name: project.name + " updated", + status: Project::COMPLETED, + cost: 1000.00, + paymentFrequency: Project::ANNUALLY, + startsOn: starts_on = 2.weeks.from_now.to_date.iso8601, + endsOn: ends_on = 10.weeks.from_now.to_date.iso8601 + } + ) + + post_result = result["data"]["upsertProject"] + expect(result["errors"]).to be_nil + expect(post_result["client"]["id"]).to eq(project.client.id.to_s) + expect(post_result["name"]).to eq(project.name + " updated") + expect(post_result["status"]).to eq(Project::COMPLETED) + expect(post_result["cost"]).to eq(1000.00) + expect(post_result["paymentFrequency"]).to eq(Project::ANNUALLY) + expect(post_result["startsOn"]).to eq(starts_on.to_s) + expect(post_result["endsOn"]).to eq(ends_on.to_s) + end + + it "does not allow client_id to be overridden" do + query_string = <<-GRAPHQL + mutation($id: ID, $clientId: ID) { + upsertProject(id: $id, clientId: $clientId) { + id + client { + id + } + } + } + GRAPHQL + + project = create(:project) + user = project.company.users.first + other_client = create(:client) + + expect(project.client.company).to_not eq(other_client.company) + + result = StaffplanReduxSchema.execute( + query_string, + context: { + current_user: user, + current_company: user.current_company + }, + variables: { + id: project.id, + clientId: other_client.id, + } + ) + + post_result = result["data"]["upsertProject"] + expect(post_result["client"]["id"]).to eq(project.client.id.to_s) + end + + it "renders validation errors" do + query_string = <<-GRAPHQL + mutation($id: ID, $status: String) { + upsertProject(id: $id, status: $status) { + id + client { + id + } + status + } + } + GRAPHQL + + project = create(:project) + user = project.company.users.first + + result = StaffplanReduxSchema.execute( + query_string, + context: { + current_user: user, + current_company: user.current_company + }, + variables: { + id: project.id, + status: "invalid-status", + } + ) + + post_result = result["errors"] + expect(post_result.length).to eq(1) + expect(post_result.first["message"]).to eq("Status is not included in the list") + end + + it "raises a 404 if given an assignment id that doesn't exist on the company" do + query_string = <<-GRAPHQL + mutation($id: ID, $name: String) { + upsertProject(id: $id, name: $name) { + id + name + } + } + GRAPHQL + + project = create(:project) + user = project.company.users.first + second_project = create(:project) + + expect(project.company).to_not eq(second_project.company) + + result = StaffplanReduxSchema.execute( + query_string, + context: { + current_user: user, + current_company: user.current_company + }, + variables: { + id: second_project.id, + name: "new name" + } + ) + + post_result = result["errors"] + expect(post_result.first["message"]).to eq("Project not found") + end + end +end \ No newline at end of file From 610be55f24c5bc41f4969d37ea7aa2795a8cc86f Mon Sep 17 00:00:00 2001 From: Rob Sterner Date: Tue, 5 Mar 2024 20:28:22 -0500 Subject: [PATCH 6/7] enforce that the client must belong to the current company --- app/graphql/mutations/upsert_project.rb | 13 ++++++- spec/graphql/mutations/upsert_project_spec.rb | 39 +++++++++++++++++++ 2 files changed, 51 insertions(+), 1 deletion(-) diff --git a/app/graphql/mutations/upsert_project.rb b/app/graphql/mutations/upsert_project.rb index b424fc84..368e026f 100644 --- a/app/graphql/mutations/upsert_project.rb +++ b/app/graphql/mutations/upsert_project.rb @@ -24,7 +24,18 @@ def resolve(id: nil, client_id: nil, name: nil, status: nil, cost: nil, payment_ end if project.blank? - project = current_company.projects.new(client_id:, name:, status:) + client = current_company.clients.find_by(id: client_id) + + # client must belong to the current company + if client.nil? + context.add_error( + GraphQL::ExecutionError.new("Client not found", extensions: { attribute: "client_id" }) + ) + + return + end + + project = client.projects.new end project.assign_attributes(name:) if name.present? diff --git a/spec/graphql/mutations/upsert_project_spec.rb b/spec/graphql/mutations/upsert_project_spec.rb index 276fcb17..eeb342e9 100644 --- a/spec/graphql/mutations/upsert_project_spec.rb +++ b/spec/graphql/mutations/upsert_project_spec.rb @@ -46,6 +46,45 @@ expect(post_result["endsOn"]).to be_nil end + it "does not allow a client_id from another company to be specified" do + query_string = <<-GRAPHQL + mutation($clientId: ID, $name: String) { + upsertProject(clientId: $clientId, name: $name) { + id + client { + id + } + name + } + } + GRAPHQL + + user = create(:user) + client = create(:client, company: user.current_company) + other_client = create(:client) + + expect(client.company).to_not eq(other_client.company) + expect(other_client.company.users).to_not include(user) + result = nil + + expect do + result = StaffplanReduxSchema.execute( + query_string, + context: { + current_user: user, + current_company: user.current_company + }, + variables: { + clientId: other_client.id, + name: project_name = Faker::Company.buzzword + } + ) + end.to_not change(Project, :count) + + post_result = result["errors"] + expect(post_result.first["message"]).to eq("Client not found") + end + it "updates a project with valid params" do query_string = <<-GRAPHQL mutation($id: ID, $clientId: ID, $name: String, $status: String, $cost: Float, $paymentFrequency: String, $startsOn: ISO8601Date, $endsOn: ISO8601Date) { From 3fea19fdb1dfff4e2d50d39c7a6f456eef59a100 Mon Sep 17 00:00:00 2001 From: Rob Sterner Date: Tue, 5 Mar 2024 20:42:10 -0500 Subject: [PATCH 7/7] not sure how these were missed :thinking: --- app/graphql/mutations/upsert_project.rb | 4 ++-- app/graphql/schema.graphql | 8 ++++---- app/models/project.rb | 2 +- spec/sidekiq/sync_customer_subscription_job_spec.rb | 3 +-- 4 files changed, 8 insertions(+), 9 deletions(-) diff --git a/app/graphql/mutations/upsert_project.rb b/app/graphql/mutations/upsert_project.rb index 368e026f..44feb57e 100644 --- a/app/graphql/mutations/upsert_project.rb +++ b/app/graphql/mutations/upsert_project.rb @@ -13,7 +13,7 @@ class UpsertProject < BaseMutation argument :ends_on, GraphQL::Types::ISO8601Date, required: false, description: "The date this assignment ends." # return type from the mutation - type Types::StaffPlan::ProjectType + type Types::StaffPlan::ProjectType, null: true def resolve(id: nil, client_id: nil, name: nil, status: nil, cost: nil, payment_frequency: nil, starts_on: nil, ends_on: nil) current_company = context[:current_company] @@ -32,7 +32,7 @@ def resolve(id: nil, client_id: nil, name: nil, status: nil, cost: nil, payment_ GraphQL::ExecutionError.new("Client not found", extensions: { attribute: "client_id" }) ) - return + return {} end project = client.projects.new diff --git a/app/graphql/schema.graphql b/app/graphql/schema.graphql index 0c7b3756..a9aabba3 100644 --- a/app/graphql/schema.graphql +++ b/app/graphql/schema.graphql @@ -113,7 +113,7 @@ type Mutation { """ The ID of the client for this project. """ - clientId: ID! + clientId: ID """ The cost of the project. @@ -133,7 +133,7 @@ type Mutation { """ The name of the project. """ - name: String! + name: String """ The frequency of payment for the project. @@ -148,8 +148,8 @@ type Mutation { """ The status of the assignment. """ - status: String! - ): Project! + status: String + ): Project """ Create or update a work week record for a StaffPlan user. diff --git a/app/models/project.rb b/app/models/project.rb index 71f58c72..d1f8d8b7 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -24,7 +24,7 @@ class Project < ApplicationRecord VALID_PAYMENT_FREQUENCIES = [WEEKLY, MONTHLY, FORTNIGHTLY, QUARTERLY, ANNUALLY].freeze validates :client_id, presence: true - validates :name, presence: true, uniqueness: { case_sensitive: false } + validates :name, presence: true, uniqueness: { scope: :client_id, case_sensitive: false } validates :status, inclusion: { in: VALID_STATUSES }, allow_blank: true validates :cost, numericality: { greater_than_or_equal_to: 0.0 }, allow_blank: true validates :payment_frequency, inclusion: { in: VALID_PAYMENT_FREQUENCIES }, allow_blank: true diff --git a/spec/sidekiq/sync_customer_subscription_job_spec.rb b/spec/sidekiq/sync_customer_subscription_job_spec.rb index 441a658e..20cebfb5 100644 --- a/spec/sidekiq/sync_customer_subscription_job_spec.rb +++ b/spec/sidekiq/sync_customer_subscription_job_spec.rb @@ -26,8 +26,7 @@ company.subscription.stripe_id, { items: [ {id: company.subscription.item_id, quantity: 2 } - ]}, - {proration_date: Date.today.iso8601} + ]} ) SyncCustomerSubscriptionJob.perform_inline(company.id)