From f9db35e07b0ef37d8b49cd06281f4ed7fc68e8b4 Mon Sep 17 00:00:00 2001 From: Apoorv Mishra Date: Thu, 14 Apr 2022 00:25:54 +0530 Subject: [PATCH 1/5] API for invoice creation --- .../internal_api/v1/invoices_controller.rb | 16 +++++ app/policies/invoice_policy.rb | 13 ++++ .../v1/invoices/create.json.jbuilder | 19 +++++ config/routes/internal_api.rb | 2 +- .../internal_api/v1/invoices/create_spec.rb | 71 +++++++++++++++++++ 5 files changed, 120 insertions(+), 1 deletion(-) create mode 100644 app/views/internal_api/v1/invoices/create.json.jbuilder create mode 100644 spec/requests/internal_api/v1/invoices/create_spec.rb diff --git a/app/controllers/internal_api/v1/invoices_controller.rb b/app/controllers/internal_api/v1/invoices_controller.rb index 95d259cca0..5a09956af3 100644 --- a/app/controllers/internal_api/v1/invoices_controller.rb +++ b/app/controllers/internal_api/v1/invoices_controller.rb @@ -21,4 +21,20 @@ def index } } end + + def create + authorize Invoice + render :create, locals: { + invoice: Invoice.create!(invoice_params), + client: Client.find(invoice_params[:client_id]) + } + end + + private + + def invoice_params + params.require(:invoice).permit( + policy(Invoice).permitted_attributes + ) + end end diff --git a/app/policies/invoice_policy.rb b/app/policies/invoice_policy.rb index 31b91cdb06..5f47c1fb76 100644 --- a/app/policies/invoice_policy.rb +++ b/app/policies/invoice_policy.rb @@ -5,7 +5,20 @@ def index? user_owner_or_admin? end + def create? + user_owner_or_admin? + end + def show? user_owner_or_admin? end + + def permitted_attributes + [ + :issue_date, :due_date, + :invoice_number, :reference, :amount, + :outstanding_amount, :tax, :amount_paid, + :amount_due, :discount, :client_id + ] + end end diff --git a/app/views/internal_api/v1/invoices/create.json.jbuilder b/app/views/internal_api/v1/invoices/create.json.jbuilder new file mode 100644 index 0000000000..d3814efb4f --- /dev/null +++ b/app/views/internal_api/v1/invoices/create.json.jbuilder @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +json.key_format! camelize: :lower +json.deep_format_keys! +json.id invoice.id +json.invoice_number invoice.invoice_number +json.issue_date invoice.issue_date +json.due_date invoice.due_date +json.reference invoice.reference +json.amount invoice.amount +json.outstanding_amount invoice.outstanding_amount +json.amount_paid invoice.amount_paid +json.amount_due invoice.amount_due +json.discount invoice.discount +json.tax invoice.tax +json.status invoice.status +json.client do + json.name client.name +end diff --git a/config/routes/internal_api.rb b/config/routes/internal_api.rb index 500d192eb2..2e9bd3c26e 100644 --- a/config/routes/internal_api.rb +++ b/config/routes/internal_api.rb @@ -13,7 +13,7 @@ resources :timesheet_entry, only: [:index, :create, :update, :destroy] resources :reports, only: [:index] resources :workspaces, only: [:update] - resources :invoices, only: [:index] + resources :invoices, only: [:index, :create] resources :generate_invoice, only: [:index] end end diff --git a/spec/requests/internal_api/v1/invoices/create_spec.rb b/spec/requests/internal_api/v1/invoices/create_spec.rb new file mode 100644 index 0000000000..615d73ff5e --- /dev/null +++ b/spec/requests/internal_api/v1/invoices/create_spec.rb @@ -0,0 +1,71 @@ +# frozen_string_literal: true + +require "rails_helper" + +RSpec.describe "InternalApi::V1::Invoices#create", type: :request do + let(:company) do + create(:company, clients: create_list(:client_with_invoices, 5)) + end + + let(:user) { create(:user, current_workspace_id: company.id) } + + context "when user is admin" do + before do + create(:company_user, company:, user:) + user.add_role :admin, company + sign_in user + end + + describe "invoice creation" do + it "creates invoice successfully" do + send_request :post, internal_api_v1_invoices_path( + invoice: { + client_id: 1, + invoice_number: "INV0001", + reference: "bar", + issue_date: "2022-01-01", + due_date: "2022-01-31" + }) + expect(response).to have_http_status(:ok) + expect(json_response["invoiceNumber"]).to eq("INV0001") + expect(json_response["issueDate"]).to eq("2022-01-01") + expect(json_response["dueDate"]).to eq("2022-01-31") + expect(json_response["status"]).to eq("draft") + end + + it "throws 422 if client doesn't exist" do + send_request :post, internal_api_v1_invoices_path( + invoice: { + client_id: 100000, + invoice_number: "INV0001", + reference: "bar", + issue_date: "2022-01-01", + due_date: "2022-01-31" + }) + expect(response).to have_http_status(:unprocessable_entity) + expect(json_response["errors"]["client"].first).to eq("must exist") + end + end + end + + context "when user is employee" do + before do + create(:company_user, company:, user:) + user.add_role :employee, company + sign_in user + send_request :post, internal_api_v1_invoices_path + end + + it "is not be permitted to generate an invoice" do + expect(response).to have_http_status(:forbidden) + end + end + + context "when unauthenticated" do + it "is not be permitted to generate an invoice" do + send_request :post, internal_api_v1_invoices_path + expect(response).to have_http_status(:unauthorized) + expect(json_response["error"]).to eq("You need to sign in or sign up before continuing.") + end + end +end From da890cb34528b0d7d4f5b18fc17d150d056ac9bd Mon Sep 17 00:00:00 2001 From: Apoorv Mishra Date: Thu, 14 Apr 2022 01:43:52 +0530 Subject: [PATCH 2/5] API to update an invoice --- .../internal_api/v1/invoices_controller.rb | 14 ++++ app/policies/invoice_policy.rb | 4 ++ .../v1/invoices/update.json.jbuilder | 19 ++++++ config/routes/internal_api.rb | 2 +- .../internal_api/v1/invoices/create_spec.rb | 2 +- .../internal_api/v1/invoices/update_spec.rb | 65 +++++++++++++++++++ 6 files changed, 104 insertions(+), 2 deletions(-) create mode 100644 app/views/internal_api/v1/invoices/update.json.jbuilder create mode 100644 spec/requests/internal_api/v1/invoices/update_spec.rb diff --git a/app/controllers/internal_api/v1/invoices_controller.rb b/app/controllers/internal_api/v1/invoices_controller.rb index 5a09956af3..97bf070813 100644 --- a/app/controllers/internal_api/v1/invoices_controller.rb +++ b/app/controllers/internal_api/v1/invoices_controller.rb @@ -30,8 +30,22 @@ def create } end + def update + authorize invoice + if invoice.update!(invoice_params) + render :update, locals: { + invoice:, + client: Client.find(invoice[:client_id]) + } + end + end + private + def invoice + @_invoice ||= Invoice.find(params[:id]) + end + def invoice_params params.require(:invoice).permit( policy(Invoice).permitted_attributes diff --git a/app/policies/invoice_policy.rb b/app/policies/invoice_policy.rb index 5f47c1fb76..0bbeea43b7 100644 --- a/app/policies/invoice_policy.rb +++ b/app/policies/invoice_policy.rb @@ -9,6 +9,10 @@ def create? user_owner_or_admin? end + def update? + user_owner_or_admin? + end + def show? user_owner_or_admin? end diff --git a/app/views/internal_api/v1/invoices/update.json.jbuilder b/app/views/internal_api/v1/invoices/update.json.jbuilder new file mode 100644 index 0000000000..d3814efb4f --- /dev/null +++ b/app/views/internal_api/v1/invoices/update.json.jbuilder @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +json.key_format! camelize: :lower +json.deep_format_keys! +json.id invoice.id +json.invoice_number invoice.invoice_number +json.issue_date invoice.issue_date +json.due_date invoice.due_date +json.reference invoice.reference +json.amount invoice.amount +json.outstanding_amount invoice.outstanding_amount +json.amount_paid invoice.amount_paid +json.amount_due invoice.amount_due +json.discount invoice.discount +json.tax invoice.tax +json.status invoice.status +json.client do + json.name client.name +end diff --git a/config/routes/internal_api.rb b/config/routes/internal_api.rb index 2e9bd3c26e..fd17857611 100644 --- a/config/routes/internal_api.rb +++ b/config/routes/internal_api.rb @@ -13,7 +13,7 @@ resources :timesheet_entry, only: [:index, :create, :update, :destroy] resources :reports, only: [:index] resources :workspaces, only: [:update] - resources :invoices, only: [:index, :create] + resources :invoices, only: [:index, :create, :update] resources :generate_invoice, only: [:index] end end diff --git a/spec/requests/internal_api/v1/invoices/create_spec.rb b/spec/requests/internal_api/v1/invoices/create_spec.rb index 615d73ff5e..393076d0d8 100644 --- a/spec/requests/internal_api/v1/invoices/create_spec.rb +++ b/spec/requests/internal_api/v1/invoices/create_spec.rb @@ -20,7 +20,7 @@ it "creates invoice successfully" do send_request :post, internal_api_v1_invoices_path( invoice: { - client_id: 1, + client_id: company.clients.first.id, invoice_number: "INV0001", reference: "bar", issue_date: "2022-01-01", diff --git a/spec/requests/internal_api/v1/invoices/update_spec.rb b/spec/requests/internal_api/v1/invoices/update_spec.rb new file mode 100644 index 0000000000..8e7adb10a6 --- /dev/null +++ b/spec/requests/internal_api/v1/invoices/update_spec.rb @@ -0,0 +1,65 @@ +# frozen_string_literal: true + +require "rails_helper" + +RSpec.describe "InternalApi::V1::Invoices#update", type: :request do + let(:company) do + create(:company, clients: create_list(:client_with_invoices, 5)) + end + + let(:user) { create(:user, current_workspace_id: company.id) } + + context "when user is admin" do + before do + create(:company_user, company:, user:) + user.add_role :admin, company + sign_in user + end + + describe "invoice updation" do + it "updates invoice successfully" do + send_request :patch, internal_api_v1_invoice_path( + id: company.clients.first.invoices.first.id, params: { + invoice: { + reference: "foo" + } + }) + expect(response).to have_http_status(:ok) + expect(json_response["reference"]).to eq("foo") + end + + it "throws 422 if client doesn't exist" do + send_request :patch, internal_api_v1_invoice_path( + id: company.clients.first.invoices.first.id, params: { + invoice: { + client_id: 100000, + reference: "foo" + } + }) + expect(response).to have_http_status(:unprocessable_entity) + expect(json_response["errors"]["client"].first).to eq("must exist") + end + end + end + + context "when user is employee" do + before do + create(:company_user, company:, user:) + user.add_role :employee, company + sign_in user + send_request :patch, internal_api_v1_invoice_path(id: company.clients.first.invoices.first.id) + end + + it "is not be permitted to update an invoice" do + expect(response).to have_http_status(:forbidden) + end + end + + context "when unauthenticated" do + it "is not be permitted to update an invoice" do + send_request :patch, internal_api_v1_invoice_path(id: 1) + expect(response).to have_http_status(:unauthorized) + expect(json_response["error"]).to eq("You need to sign in or sign up before continuing.") + end + end +end From 6959402c5e9af940b303fcffe2d57cd31718ea25 Mon Sep 17 00:00:00 2001 From: Apoorv Mishra Date: Thu, 14 Apr 2022 10:28:02 +0530 Subject: [PATCH 3/5] Remove unnecessary if clause --- .../internal_api/v1/invoices_controller.rb | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/app/controllers/internal_api/v1/invoices_controller.rb b/app/controllers/internal_api/v1/invoices_controller.rb index 97bf070813..10a2e2fe8f 100644 --- a/app/controllers/internal_api/v1/invoices_controller.rb +++ b/app/controllers/internal_api/v1/invoices_controller.rb @@ -32,12 +32,11 @@ def create def update authorize invoice - if invoice.update!(invoice_params) - render :update, locals: { - invoice:, - client: Client.find(invoice[:client_id]) - } - end + invoice.update!(invoice_params) + render :update, locals: { + invoice:, + client: Client.find(invoice[:client_id]) + } end private From 563b69348453954af001d884356cf861fb598397 Mon Sep 17 00:00:00 2001 From: Apoorv Mishra Date: Thu, 14 Apr 2022 12:15:36 +0530 Subject: [PATCH 4/5] use partials to dry up --- .../v1/invoices/_invoice.json.jbuilder | 20 +++++++++++++++++++ .../v1/invoices/create.json.jbuilder | 18 +---------------- .../v1/invoices/update.json.jbuilder | 18 +---------------- 3 files changed, 22 insertions(+), 34 deletions(-) create mode 100644 app/views/internal_api/v1/invoices/_invoice.json.jbuilder diff --git a/app/views/internal_api/v1/invoices/_invoice.json.jbuilder b/app/views/internal_api/v1/invoices/_invoice.json.jbuilder new file mode 100644 index 0000000000..1abbc9133e --- /dev/null +++ b/app/views/internal_api/v1/invoices/_invoice.json.jbuilder @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +json.key_format! camelize: :lower +json.deep_format_keys! + +json.id invoice.id +json.invoice_number invoice.invoice_number +json.issue_date invoice.issue_date +json.due_date invoice.due_date +json.reference invoice.reference +json.amount invoice.amount +json.outstanding_amount invoice.outstanding_amount +json.amount_paid invoice.amount_paid +json.amount_due invoice.amount_due +json.discount invoice.discount +json.tax invoice.tax +json.status invoice.status +json.client do + json.name client.name +end diff --git a/app/views/internal_api/v1/invoices/create.json.jbuilder b/app/views/internal_api/v1/invoices/create.json.jbuilder index d3814efb4f..69d2d9bcae 100644 --- a/app/views/internal_api/v1/invoices/create.json.jbuilder +++ b/app/views/internal_api/v1/invoices/create.json.jbuilder @@ -1,19 +1,3 @@ # frozen_string_literal: true -json.key_format! camelize: :lower -json.deep_format_keys! -json.id invoice.id -json.invoice_number invoice.invoice_number -json.issue_date invoice.issue_date -json.due_date invoice.due_date -json.reference invoice.reference -json.amount invoice.amount -json.outstanding_amount invoice.outstanding_amount -json.amount_paid invoice.amount_paid -json.amount_due invoice.amount_due -json.discount invoice.discount -json.tax invoice.tax -json.status invoice.status -json.client do - json.name client.name -end +json.partial! "invoice", locals: { invoice:, client: } diff --git a/app/views/internal_api/v1/invoices/update.json.jbuilder b/app/views/internal_api/v1/invoices/update.json.jbuilder index d3814efb4f..69d2d9bcae 100644 --- a/app/views/internal_api/v1/invoices/update.json.jbuilder +++ b/app/views/internal_api/v1/invoices/update.json.jbuilder @@ -1,19 +1,3 @@ # frozen_string_literal: true -json.key_format! camelize: :lower -json.deep_format_keys! -json.id invoice.id -json.invoice_number invoice.invoice_number -json.issue_date invoice.issue_date -json.due_date invoice.due_date -json.reference invoice.reference -json.amount invoice.amount -json.outstanding_amount invoice.outstanding_amount -json.amount_paid invoice.amount_paid -json.amount_due invoice.amount_due -json.discount invoice.discount -json.tax invoice.tax -json.status invoice.status -json.client do - json.name client.name -end +json.partial! "invoice", locals: { invoice:, client: } From 35caba62781932b22dc9fa88d35054726a6b0872 Mon Sep 17 00:00:00 2001 From: Apoorv Mishra Date: Thu, 14 Apr 2022 14:00:37 +0530 Subject: [PATCH 5/5] abstract client.find and fix specs --- .../internal_api/v1/invoices_controller.rb | 8 +++++-- .../internal_api/v1/invoices/create_spec.rb | 23 +++++++++---------- 2 files changed, 17 insertions(+), 14 deletions(-) diff --git a/app/controllers/internal_api/v1/invoices_controller.rb b/app/controllers/internal_api/v1/invoices_controller.rb index 10a2e2fe8f..58b9e6e821 100644 --- a/app/controllers/internal_api/v1/invoices_controller.rb +++ b/app/controllers/internal_api/v1/invoices_controller.rb @@ -26,7 +26,7 @@ def create authorize Invoice render :create, locals: { invoice: Invoice.create!(invoice_params), - client: Client.find(invoice_params[:client_id]) + client: load_client(invoice_params[:client_id]) } end @@ -35,12 +35,16 @@ def update invoice.update!(invoice_params) render :update, locals: { invoice:, - client: Client.find(invoice[:client_id]) + client: load_client(invoice[:client_id]) } end private + def load_client(client_id) + Client.find(client_id) + end + def invoice @_invoice ||= Invoice.find(params[:id]) end diff --git a/spec/requests/internal_api/v1/invoices/create_spec.rb b/spec/requests/internal_api/v1/invoices/create_spec.rb index 393076d0d8..37826a93a4 100644 --- a/spec/requests/internal_api/v1/invoices/create_spec.rb +++ b/spec/requests/internal_api/v1/invoices/create_spec.rb @@ -18,19 +18,18 @@ describe "invoice creation" do it "creates invoice successfully" do - send_request :post, internal_api_v1_invoices_path( - invoice: { - client_id: company.clients.first.id, - invoice_number: "INV0001", - reference: "bar", - issue_date: "2022-01-01", - due_date: "2022-01-31" - }) + invoice = attributes_for( + :invoice, + client: company.clients.first, + client_id: company.clients.first.id, + status: :draft) + send_request :post, internal_api_v1_invoices_path(invoice:) expect(response).to have_http_status(:ok) - expect(json_response["invoiceNumber"]).to eq("INV0001") - expect(json_response["issueDate"]).to eq("2022-01-01") - expect(json_response["dueDate"]).to eq("2022-01-31") - expect(json_response["status"]).to eq("draft") + expected_attrs = ["amount", "amountDue", "amountPaid", + "client", "discount", "dueDate", "id", + "invoiceNumber", "issueDate", "outstandingAmount", + "reference", "status", "tax"] + expect(json_response.keys.sort).to match(expected_attrs) end it "throws 422 if client doesn't exist" do