diff --git a/app/controllers/internal_api/v1/invoices/view_controller.rb b/app/controllers/internal_api/v1/invoices/view_controller.rb index ecc5946d34..137d966c8e 100644 --- a/app/controllers/internal_api/v1/invoices/view_controller.rb +++ b/app/controllers/internal_api/v1/invoices/view_controller.rb @@ -9,7 +9,7 @@ class InternalApi::V1::Invoices::ViewController < InternalApi::V1::ApplicationCo def show invoice.viewed! if invoice.sent? Invoices::EventTrackerService.new("view", invoice, params).process - render :show, locals: { invoice:, stripe_connected_account: } + render :show, locals: { invoice:, stripe_connected_account:, bank_account: invoice.company.bank_account } end private diff --git a/app/controllers/internal_api/v1/payment_settings_controller.rb b/app/controllers/internal_api/v1/payment_settings_controller.rb index 0d79d2ecd7..1a18b20858 100644 --- a/app/controllers/internal_api/v1/payment_settings_controller.rb +++ b/app/controllers/internal_api/v1/payment_settings_controller.rb @@ -4,13 +4,13 @@ class InternalApi::V1::PaymentSettingsController < InternalApi::V1::ApplicationC after_action :save_stripe_settings, only: :index def index - authorize :index, policy_class: PaymentSettingsPolicy + authorize current_company, policy_class: PaymentSettingsPolicy - render :index, locals: { stripe_connected_account: } + render :index, locals: { stripe_connected_account:, bank_account: current_company.bank_account } end def connect_stripe - authorize :connect_stripe, policy_class: PaymentSettingsPolicy + authorize current_company, policy_class: PaymentSettingsPolicy StripeConnectedAccount.create!({ company: current_company }) if stripe_connected_account.nil? @@ -18,7 +18,7 @@ def connect_stripe end def destroy - authorize :destroy, policy_class: PaymentSettingsPolicy + authorize current_company, policy_class: PaymentSettingsPolicy if stripe_connected_account.destroy render json: { notice: "Stripe connection disconnected" }, status: :ok @@ -27,6 +27,17 @@ def destroy end end + def update_bank_account + authorize current_company, policy_class: PaymentSettingsPolicy + + @bank_account = current_company.bank_account || current_company.build_bank_account + if @bank_account.update(bank_account_params) + render json: { notice: "Bank account details updated successfully" }, status: :ok + else + render json: { error: @bank_account.errors.full_messages }, status: :unprocessable_entity + end + end + private def stripe_connected_account @@ -36,4 +47,8 @@ def stripe_connected_account def save_stripe_settings PaymentProviders::CreateStripeProviderService.process(current_company) end + + def bank_account_params + params.require(:bank_account).permit(:routing_number, :account_number, :account_type, :bank_name) + end end diff --git a/app/controllers/payment_settings_controller.rb b/app/controllers/payment_settings_controller.rb index 41a28005cf..27b9f43592 100644 --- a/app/controllers/payment_settings_controller.rb +++ b/app/controllers/payment_settings_controller.rb @@ -2,11 +2,11 @@ class PaymentSettingsController < ApplicationController def index - authorize :index, policy_class: PaymentSettingsPolicy + authorize current_company, policy_class: PaymentSettingsPolicy end def refresh_stripe_connect - authorize :refresh_stripe_connect, policy_class: PaymentSettingsPolicy + authorize current_company, policy_class: PaymentSettingsPolicy redirect_to stripe_connected_account.url, allow_other_host: true end diff --git a/app/models/bank_account.rb b/app/models/bank_account.rb new file mode 100644 index 0000000000..11b092377f --- /dev/null +++ b/app/models/bank_account.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +class BankAccount < ApplicationRecord + belongs_to :company + + validates :routing_number, presence: true + validates :account_number, presence: true + validates :account_type, presence: true, inclusion: { in: %w[checking savings] } + validates :bank_name, presence: true +end diff --git a/app/models/company.rb b/app/models/company.rb index 07eb91e3f6..4706a4abe7 100644 --- a/app/models/company.rb +++ b/app/models/company.rb @@ -31,6 +31,7 @@ class Company < ApplicationRecord has_many :invoices has_many :payments, through: :invoices has_one :stripe_connected_account, dependent: :destroy + has_one :bank_account, dependent: :destroy has_many :payments_providers, dependent: :destroy has_many :addresses, as: :addressable, dependent: :destroy has_many :devices, dependent: :destroy diff --git a/app/policies/payment_settings_policy.rb b/app/policies/payment_settings_policy.rb index 508aaf33a8..3cb35c78e1 100644 --- a/app/policies/payment_settings_policy.rb +++ b/app/policies/payment_settings_policy.rb @@ -2,18 +2,31 @@ class PaymentSettingsPolicy < ApplicationPolicy def index? - user_owner_role? || user_admin_role? + has_owner_or_admin_role?(record) end def connect_stripe? - user_owner_role? || user_admin_role? + has_owner_or_admin_role?(record) end def destroy? - user_owner_role? || user_admin_role? + has_owner_or_admin_role?(record) end def refresh_stripe_connect? - user_owner_role? || user_admin_role? + has_owner_or_admin_role?(record) end + + def update_bank_account? + has_owner_or_admin_role?(record) + end + + private + + def has_owner_or_admin_role?(company) + user.has_any_role?( + { name: :admin, resource: company }, + { name: :owner, resource: company } + ) + end end diff --git a/app/services/invoice_payment/pdf_generation.rb b/app/services/invoice_payment/pdf_generation.rb index 3bae8dd76d..9bbe106333 100644 --- a/app/services/invoice_payment/pdf_generation.rb +++ b/app/services/invoice_payment/pdf_generation.rb @@ -9,6 +9,7 @@ def initialize(invoice, company_logo, root_url, filepath = nil) @base_currency = invoice.company.base_currency @root_url = root_url @filepath = filepath + @bank_account = invoice.company.bank_account end def process @@ -24,7 +25,8 @@ def process client: @invoice.client, invoice_line_items: invoice_data[:invoice_line_items], sub_total: format_currency(invoice_data[:sub_total]), - total: format_currency(invoice_data[:total]) + total: format_currency(invoice_data[:total]), + bank_account: @bank_account } Pdf::HtmlGenerator.new( diff --git a/app/views/internal_api/v1/invoices/_bank_account.json.jbuilder b/app/views/internal_api/v1/invoices/_bank_account.json.jbuilder new file mode 100644 index 0000000000..733072e463 --- /dev/null +++ b/app/views/internal_api/v1/invoices/_bank_account.json.jbuilder @@ -0,0 +1,6 @@ +# frozen_string_literal: true + +json.bank_name bank_account&.bank_name +json.account_type bank_account&.account_type&.capitalize +json.routing_number bank_account&.routing_number +json.account_number bank_account&.account_number diff --git a/app/views/internal_api/v1/invoices/view/show.json.jbuilder b/app/views/internal_api/v1/invoices/view/show.json.jbuilder index 5d2106b80e..70312d4dfa 100644 --- a/app/views/internal_api/v1/invoices/view/show.json.jbuilder +++ b/app/views/internal_api/v1/invoices/view/show.json.jbuilder @@ -11,3 +11,6 @@ end json.client do json.partial! "internal_api/v1/partial/client", locals: { client: invoice.client } end +json.bank_account do + json.partial! "internal_api/v1/invoices/bank_account", bank_account: +end diff --git a/app/views/internal_api/v1/payment_settings/index.json.jbuilder b/app/views/internal_api/v1/payment_settings/index.json.jbuilder index 605ec49271..4d58245eaa 100644 --- a/app/views/internal_api/v1/payment_settings/index.json.jbuilder +++ b/app/views/internal_api/v1/payment_settings/index.json.jbuilder @@ -10,4 +10,7 @@ json.providers do json.paypal do json.connected false end + json.bank_account do + json.partial! "internal_api/v1/invoices/bank_account", bank_account: + end end diff --git a/app/views/pdfs/invoices.html.erb b/app/views/pdfs/invoices.html.erb index 0a4a7266ae..b7a300d3a7 100644 --- a/app/views/pdfs/invoices.html.erb +++ b/app/views/pdfs/invoices.html.erb @@ -137,4 +137,13 @@ html { + <% if bank_account %> +
+

Bank Account Details

+

Bank Name: <%= bank_account[:bank_name] %>

+

Account Type: <%= bank_account[:account_type].capitalize %>

+

Routing Number: <%= bank_account[:routing_number] %>

+

Account Number: <%= bank_account[:account_number] %>

+
+ <% end %> diff --git a/config/routes/internal_api.rb b/config/routes/internal_api.rb index 5a5b575687..384e0670c5 100644 --- a/config/routes/internal_api.rb +++ b/config/routes/internal_api.rb @@ -129,6 +129,7 @@ get "payments/settings", to: "payment_settings#index" post "payments/settings/stripe/connect", to: "payment_settings#connect_stripe" delete "payments/settings/stripe/disconnect", to: "payment_settings#destroy" + put "payments/settings/bank_account", to: "payment_settings#update_bank_account" get "calendars/redirect", to: "calendars#redirect", as: "redirect" get "calendars/callback", to: "calendars#callback", as: "callback" get "calendars/calendars", to: "calendars#calendars", as: "calendars" diff --git a/db/migrate/20240903141939_create_bank_accounts.rb b/db/migrate/20240903141939_create_bank_accounts.rb new file mode 100644 index 0000000000..31ff13df9e --- /dev/null +++ b/db/migrate/20240903141939_create_bank_accounts.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +class CreateBankAccounts < ActiveRecord::Migration[7.1] + def change + create_table :bank_accounts do |t| + t.references :company, null: false, foreign_key: true + t.string :routing_number, null: false + t.string :account_number, null: false + t.string :account_type, null: false + t.string :bank_name, null: false + t.timestamps + end + end +end diff --git a/db/schema.rb b/db/schema.rb index 2992576374..e1f97a9133 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[7.1].define(version: 2024_08_05_040547) do +ActiveRecord::Schema[7.1].define(version: 2024_09_03_141939) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -100,6 +100,17 @@ t.index ["visit_token"], name: "index_ahoy_visits_on_visit_token", unique: true end + create_table "bank_accounts", force: :cascade do |t| + t.bigint "company_id", null: false + t.string "routing_number", null: false + t.string "account_number", null: false + t.string "account_type", null: false + t.string "bank_name", null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["company_id"], name: "index_bank_accounts_on_company_id" + end + create_table "bulk_invoice_download_statuses", force: :cascade do |t| t.string "download_id" t.string "status" @@ -691,6 +702,7 @@ add_foreign_key "active_storage_attachments", "active_storage_blobs", column: "blob_id" add_foreign_key "active_storage_variant_records", "active_storage_blobs", column: "blob_id" + add_foreign_key "bank_accounts", "companies" add_foreign_key "carryovers", "companies" add_foreign_key "carryovers", "leave_types" add_foreign_key "carryovers", "users" diff --git a/spec/policies/payment_settings_policy_spec.rb b/spec/policies/payment_settings_policy_spec.rb index ad5e92d82c..94cff8c374 100644 --- a/spec/policies/payment_settings_policy_spec.rb +++ b/spec/policies/payment_settings_policy_spec.rb @@ -4,46 +4,72 @@ RSpec.describe PaymentSettingsPolicy, type: :policy do let(:company) { create(:company) } - let(:user) { create(:user, current_workspace_id: company.id) } + let(:admin) { create(:user, current_workspace_id: company.id) } + let(:owner) { create(:user, current_workspace_id: company.id) } + let(:employee) { create(:user, current_workspace_id: company.id) } + let(:book_keeper) { create(:user, current_workspace_id: company.id) } + + let(:another_company) { create(:company) } + let(:another_admin) { create(:user, current_workspace_id: another_company.id) } + let(:another_owner) { create(:user, current_workspace_id: another_company.id) } + let(:another_employee) { create(:user, current_workspace_id: another_company.id) } subject { described_class } - context "when user is an admin" do - before do - create(:employment, company:, user:) - user.add_role :admin, company - end + before do + admin.add_role :admin, company + owner.add_role :owner, company + employee.add_role :employee, company + book_keeper.add_role :book_keeper, company - permissions :index? do - it "is permitted to access index" do - expect(subject).to permit(user, :payment_settings) - end - end + another_admin.add_role :admin, another_company + another_owner.add_role :owner, another_company + another_employee.add_role :employee, another_company end - context "when user is an employee" do - before do - create(:employment, company:, user:) - user.add_role :employee, company + shared_examples "grants access to admin and owner" do |action| + it "allows admin and owner to perform #{action}" do + expect(subject).to permit(admin, company) + expect(subject).to permit(owner, company) end + end - permissions :index? do - it "is not permitted to access index" do - expect(subject).not_to permit(user, :payment_settings) - end + shared_examples "denies access to employee and book keeper" do |action| + it "denies #{action} to employee and book keeper" do + expect(subject).not_to permit(employee, company) + expect(subject).not_to permit(book_keeper, company) end end - context "when user is a book keeper" do - before do - create(:employment, company:, user:) - user.add_role :book_keeper, company + shared_examples "denies access to another company's users" do |action| + it "denies #{action} to users from another company" do + expect(subject).not_to permit(another_admin, company) + expect(subject).not_to permit(another_owner, company) + expect(subject).not_to permit(another_employee, company) end + end - permissions :index? do - it "is not permitted to access index" do - expect(subject).not_to permit(user, :payment_settings) - end - end + permissions :index? do + include_examples "grants access to admin and owner", "index" + include_examples "denies access to employee and book keeper", "index" + include_examples "denies access to another company's users", "index" + end + + permissions :update_bank_account? do + include_examples "grants access to admin and owner", "update_bank_account" + include_examples "denies access to employee and book keeper", "update_bank_account" + include_examples "denies access to another company's users", "update_bank_account" + end + + permissions :destroy? do + include_examples "grants access to admin and owner", "destroy" + include_examples "denies access to employee and book keeper", "destroy" + include_examples "denies access to another company's users", "destroy" + end + + permissions :connect_stripe? do + include_examples "grants access to admin and owner", "connect_stripe" + include_examples "denies access to employee and book keeper", "connect_stripe" + include_examples "denies access to another company's users", "connect_stripe" end end