From 9d6598cd45da266757b4e8c259d9d70f7af724d2 Mon Sep 17 00:00:00 2001 From: Anna Velentsevich Date: Fri, 13 Dec 2024 14:32:25 +0100 Subject: [PATCH 01/19] WIP update customer with invoice_custom sections service --- .../select_invoice_custom_sections_service.rb | 39 +++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 app/services/customers/select_invoice_custom_sections_service.rb diff --git a/app/services/customers/select_invoice_custom_sections_service.rb b/app/services/customers/select_invoice_custom_sections_service.rb new file mode 100644 index 00000000000..260bf9ef709 --- /dev/null +++ b/app/services/customers/select_invoice_custom_sections_service.rb @@ -0,0 +1,39 @@ +# frozen_string_literal: true + +module Customers + class SelectInvoiceCustomSectionsService < BaseService + def initialize(customer:, section_ids: []) + @customer = customer + @section_ids = section_ids + + super + end + + def call + return result.not_found_failure!(resource: "customer") unless customer + return result if customer.applicable_invoice_custom_sections.ids == section_ids + + if customer.organization.selected_invoice_custom_sections.ids == section_ids + assign_organization_sections + else + assign_customer_sections + end + result + rescue ActiveRecord::RecordInvalid => e + result.record_validation_failure!(record: e.record) + end + + private + + attr_reader :customer, :section_ids + + def assign_organization_sections + # Note: when inheriting organization's selections, customer shouldn't have their selected sections + customer.selected_invoice_custom_sections = [] + end + + def assign_customer_sections + customer.selected_invoice_custom_sections = customer.applicable_invoice_custom_sections.where(id: section_ids) + end + end +end From f40bce5ca20b657294f4ab4ff2215abcb10b395a Mon Sep 17 00:00:00 2001 From: Anna Velentsevich Date: Fri, 13 Dec 2024 17:57:57 +0100 Subject: [PATCH 02/19] refactoring of the select and deselect services --- ...ons_service.rb => update_invoice_custom_sections_service.rb} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename app/services/customers/{select_invoice_custom_sections_service.rb => update_invoice_custom_sections_service.rb} (94%) diff --git a/app/services/customers/select_invoice_custom_sections_service.rb b/app/services/customers/update_invoice_custom_sections_service.rb similarity index 94% rename from app/services/customers/select_invoice_custom_sections_service.rb rename to app/services/customers/update_invoice_custom_sections_service.rb index 260bf9ef709..61595dd6225 100644 --- a/app/services/customers/select_invoice_custom_sections_service.rb +++ b/app/services/customers/update_invoice_custom_sections_service.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true module Customers - class SelectInvoiceCustomSectionsService < BaseService + class UpdateInvoiceCustomSectionsService < BaseService def initialize(customer:, section_ids: []) @customer = customer @section_ids = section_ids From 14a7c9fda1ee67b62a6c98348823904dc8de1200 Mon Sep 17 00:00:00 2001 From: Anna Velentsevich Date: Mon, 23 Dec 2024 15:51:20 +0100 Subject: [PATCH 03/19] fix and add tests --- .../update_invoice_custom_sections_service.rb | 8 ++- ...te_invoice_custom_sections_service_spec.rb | 69 +++++++++++++++++++ 2 files changed, 75 insertions(+), 2 deletions(-) create mode 100644 spec/services/customers/update_invoice_custom_sections_service_spec.rb diff --git a/app/services/customers/update_invoice_custom_sections_service.rb b/app/services/customers/update_invoice_custom_sections_service.rb index 61595dd6225..3ea36c50aae 100644 --- a/app/services/customers/update_invoice_custom_sections_service.rb +++ b/app/services/customers/update_invoice_custom_sections_service.rb @@ -13,7 +13,7 @@ def call return result.not_found_failure!(resource: "customer") unless customer return result if customer.applicable_invoice_custom_sections.ids == section_ids - if customer.organization.selected_invoice_custom_sections.ids == section_ids + if organization.selected_invoice_custom_sections.ids == section_ids assign_organization_sections else assign_customer_sections @@ -27,13 +27,17 @@ def call attr_reader :customer, :section_ids + def organization + @organization ||= customer.organization + end + def assign_organization_sections # Note: when inheriting organization's selections, customer shouldn't have their selected sections customer.selected_invoice_custom_sections = [] end def assign_customer_sections - customer.selected_invoice_custom_sections = customer.applicable_invoice_custom_sections.where(id: section_ids) + customer.selected_invoice_custom_sections = organization.invoice_custom_sections.where(id: section_ids) end end end diff --git a/spec/services/customers/update_invoice_custom_sections_service_spec.rb b/spec/services/customers/update_invoice_custom_sections_service_spec.rb new file mode 100644 index 00000000000..5bf02b78e0a --- /dev/null +++ b/spec/services/customers/update_invoice_custom_sections_service_spec.rb @@ -0,0 +1,69 @@ +# spec/services/customers/update_invoice_custom_sections_service_spec.rb + +require 'rails_helper' + +RSpec.describe Customers::UpdateInvoiceCustomSectionsService do + let(:customer) { create(:customer) } + let(:invoice_custom_sections) { create_list(:invoice_custom_section, 4, organization: customer.organization) } + let(:service) { described_class.new(customer: customer, section_ids:) } + let(:section_ids) { [] } + + before do + customer.selected_invoice_custom_sections << invoice_custom_sections[0] if customer + customer.organization.selected_invoice_custom_sections = invoice_custom_sections[2..3] if customer + end + + describe '#call' do + context 'when customer is not found' do + let(:customer) { nil } + + it 'returns not found failure' do + result = service.call + expect(result).not_to be_success + expect(result.error).to be_a(BaseService::NotFoundFailure) + expect(result.error.message).to eq('customer_not_found') + end + end + + context 'when section_ids match customer\'s applicable sections' do + let(:section_ids) { [invoice_custom_sections.first.id] } + + it 'returns the result without changes' do + result = service.call + expect(result).to be_success + expect(customer.applicable_invoice_custom_sections.ids).to match_array(section_ids) + end + end + + context 'when section_ids match organization\'s selected sections' do + let(:section_ids) { invoice_custom_sections[2..3].map(&:id) } + + it 'assigns organization sections to customer' do + service.call + expect(customer.reload.selected_invoice_custom_sections.ids).to match_array([]) + expect(customer.applicable_invoice_custom_sections.ids).to match_array(section_ids) + end + end + + context 'when section_ids need to be assigned to customer' do + let(:section_ids) { invoice_custom_sections[1..2].map(&:id) } + + it 'assigns customer sections' do + service.call + expect(customer.reload.selected_invoice_custom_sections.ids).to match_array(section_ids) + end + end + + context 'when an ActiveRecord::RecordInvalid error is raised' do + before do + allow(customer).to receive(:selected_invoice_custom_sections=).and_raise(ActiveRecord::RecordInvalid.new(customer)) + end + + it 'returns record validation failure' do + result = service.call + expect(result).not_to be_success + expect(result.error).to be_a(BaseService::ValidationFailure) + end + end + end +end From c62ed538bc551fd816a8721ffbd4f27f3a77ff01 Mon Sep 17 00:00:00 2001 From: Anna Velentsevich Date: Mon, 23 Dec 2024 15:59:21 +0100 Subject: [PATCH 04/19] fix linter --- .../customers/update_invoice_custom_sections_service_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/services/customers/update_invoice_custom_sections_service_spec.rb b/spec/services/customers/update_invoice_custom_sections_service_spec.rb index 5bf02b78e0a..ccc226e08c6 100644 --- a/spec/services/customers/update_invoice_custom_sections_service_spec.rb +++ b/spec/services/customers/update_invoice_custom_sections_service_spec.rb @@ -1,4 +1,4 @@ -# spec/services/customers/update_invoice_custom_sections_service_spec.rb +# frozen_string_literal: true require 'rails_helper' From 3989d8e1535a541228b0de5ef4146475dc05c8f5 Mon Sep 17 00:00:00 2001 From: Anna Velentsevich Date: Mon, 30 Dec 2024 11:13:34 +0100 Subject: [PATCH 05/19] refactor managing invoice_custom_sections --- .../update_invoice_custom_sections_service.rb | 43 ------------ ...te_invoice_custom_sections_service_spec.rb | 69 ------------------- 2 files changed, 112 deletions(-) delete mode 100644 app/services/customers/update_invoice_custom_sections_service.rb delete mode 100644 spec/services/customers/update_invoice_custom_sections_service_spec.rb diff --git a/app/services/customers/update_invoice_custom_sections_service.rb b/app/services/customers/update_invoice_custom_sections_service.rb deleted file mode 100644 index 3ea36c50aae..00000000000 --- a/app/services/customers/update_invoice_custom_sections_service.rb +++ /dev/null @@ -1,43 +0,0 @@ -# frozen_string_literal: true - -module Customers - class UpdateInvoiceCustomSectionsService < BaseService - def initialize(customer:, section_ids: []) - @customer = customer - @section_ids = section_ids - - super - end - - def call - return result.not_found_failure!(resource: "customer") unless customer - return result if customer.applicable_invoice_custom_sections.ids == section_ids - - if organization.selected_invoice_custom_sections.ids == section_ids - assign_organization_sections - else - assign_customer_sections - end - result - rescue ActiveRecord::RecordInvalid => e - result.record_validation_failure!(record: e.record) - end - - private - - attr_reader :customer, :section_ids - - def organization - @organization ||= customer.organization - end - - def assign_organization_sections - # Note: when inheriting organization's selections, customer shouldn't have their selected sections - customer.selected_invoice_custom_sections = [] - end - - def assign_customer_sections - customer.selected_invoice_custom_sections = organization.invoice_custom_sections.where(id: section_ids) - end - end -end diff --git a/spec/services/customers/update_invoice_custom_sections_service_spec.rb b/spec/services/customers/update_invoice_custom_sections_service_spec.rb deleted file mode 100644 index ccc226e08c6..00000000000 --- a/spec/services/customers/update_invoice_custom_sections_service_spec.rb +++ /dev/null @@ -1,69 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -RSpec.describe Customers::UpdateInvoiceCustomSectionsService do - let(:customer) { create(:customer) } - let(:invoice_custom_sections) { create_list(:invoice_custom_section, 4, organization: customer.organization) } - let(:service) { described_class.new(customer: customer, section_ids:) } - let(:section_ids) { [] } - - before do - customer.selected_invoice_custom_sections << invoice_custom_sections[0] if customer - customer.organization.selected_invoice_custom_sections = invoice_custom_sections[2..3] if customer - end - - describe '#call' do - context 'when customer is not found' do - let(:customer) { nil } - - it 'returns not found failure' do - result = service.call - expect(result).not_to be_success - expect(result.error).to be_a(BaseService::NotFoundFailure) - expect(result.error.message).to eq('customer_not_found') - end - end - - context 'when section_ids match customer\'s applicable sections' do - let(:section_ids) { [invoice_custom_sections.first.id] } - - it 'returns the result without changes' do - result = service.call - expect(result).to be_success - expect(customer.applicable_invoice_custom_sections.ids).to match_array(section_ids) - end - end - - context 'when section_ids match organization\'s selected sections' do - let(:section_ids) { invoice_custom_sections[2..3].map(&:id) } - - it 'assigns organization sections to customer' do - service.call - expect(customer.reload.selected_invoice_custom_sections.ids).to match_array([]) - expect(customer.applicable_invoice_custom_sections.ids).to match_array(section_ids) - end - end - - context 'when section_ids need to be assigned to customer' do - let(:section_ids) { invoice_custom_sections[1..2].map(&:id) } - - it 'assigns customer sections' do - service.call - expect(customer.reload.selected_invoice_custom_sections.ids).to match_array(section_ids) - end - end - - context 'when an ActiveRecord::RecordInvalid error is raised' do - before do - allow(customer).to receive(:selected_invoice_custom_sections=).and_raise(ActiveRecord::RecordInvalid.new(customer)) - end - - it 'returns record validation failure' do - result = service.call - expect(result).not_to be_success - expect(result.error).to be_a(BaseService::ValidationFailure) - end - end - end -end From 0a97f3752934db6dd3a5c7867355786314551dce Mon Sep 17 00:00:00 2001 From: Anna Velentsevich Date: Fri, 27 Dec 2024 16:52:34 +0100 Subject: [PATCH 06/19] fix index on code uniqueness to apply only on not deleted ics --- ...uniqueness_constraint_on_invoice_custom_sections.rb | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 db/migrate/20241227154337_add_code_uniqueness_constraint_on_invoice_custom_sections.rb diff --git a/db/migrate/20241227154337_add_code_uniqueness_constraint_on_invoice_custom_sections.rb b/db/migrate/20241227154337_add_code_uniqueness_constraint_on_invoice_custom_sections.rb new file mode 100644 index 00000000000..200c98bba74 --- /dev/null +++ b/db/migrate/20241227154337_add_code_uniqueness_constraint_on_invoice_custom_sections.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +class AddCodeUniquenessConstraintOnInvoiceCustomSections < ActiveRecord::Migration[7.1] + disable_ddl_transaction! + + def change + remove_index :invoice_custom_sections, %i[organization_id code], unique: true, algorithm: :concurrently + add_index :invoice_custom_sections, %i[organization_id code], unique: true, where: 'deleted_at IS NULL', algorithm: :concurrently + end +end From 153be68c5c5b147b1125be66d54439ed514d0ea5 Mon Sep 17 00:00:00 2001 From: Anna Velentsevich Date: Fri, 27 Dec 2024 17:52:20 +0100 Subject: [PATCH 07/19] split index removing and addition --- ..._add_code_uniqueness_constraint_on_invoice_custom_sections.rb | 1 - 1 file changed, 1 deletion(-) diff --git a/db/migrate/20241227154337_add_code_uniqueness_constraint_on_invoice_custom_sections.rb b/db/migrate/20241227154337_add_code_uniqueness_constraint_on_invoice_custom_sections.rb index 200c98bba74..e444ad43fc7 100644 --- a/db/migrate/20241227154337_add_code_uniqueness_constraint_on_invoice_custom_sections.rb +++ b/db/migrate/20241227154337_add_code_uniqueness_constraint_on_invoice_custom_sections.rb @@ -5,6 +5,5 @@ class AddCodeUniquenessConstraintOnInvoiceCustomSections < ActiveRecord::Migrati def change remove_index :invoice_custom_sections, %i[organization_id code], unique: true, algorithm: :concurrently - add_index :invoice_custom_sections, %i[organization_id code], unique: true, where: 'deleted_at IS NULL', algorithm: :concurrently end end From 9022998deaf24c2ff5ce6853e3a7b1abe3dc391a Mon Sep 17 00:00:00 2001 From: Anna Velentsevich Date: Tue, 7 Jan 2025 12:39:19 +0100 Subject: [PATCH 08/19] working on tests enhansing --- .../customers/manage_invoice_custom_sections_service.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/services/customers/manage_invoice_custom_sections_service.rb b/app/services/customers/manage_invoice_custom_sections_service.rb index 19432cbb6d4..ba6585dd3da 100644 --- a/app/services/customers/manage_invoice_custom_sections_service.rb +++ b/app/services/customers/manage_invoice_custom_sections_service.rb @@ -24,8 +24,8 @@ def call if !section_ids.nil? || !section_codes.nil? customer.skip_invoice_custom_sections = false - return result if customer.applicable_invoice_custom_sections.ids == section_ids || - customer.applicable_invoice_custom_sections.map(&:code) == section_codes + return result if customer.selected_invoice_custom_sections.ids == section_ids || + customer.selected_invoice_custom_sections.map(&:code) == section_codes assign_selected_sections end From a8fc104049740875fe92900ae89f36eca9d27e11 Mon Sep 17 00:00:00 2001 From: Anna Velentsevich Date: Thu, 26 Dec 2024 10:42:21 +0100 Subject: [PATCH 09/19] add create_invoice_applied_custom_sections service --- .../apply_invoice_custom_sections_service.rb | 34 +++++++++++ ...ly_invoice_custom_sections_service_spec.rb | 57 +++++++++++++++++++ 2 files changed, 91 insertions(+) create mode 100644 app/services/invoices/apply_invoice_custom_sections_service.rb create mode 100644 spec/services/invoices/apply_invoice_custom_sections_service_spec.rb diff --git a/app/services/invoices/apply_invoice_custom_sections_service.rb b/app/services/invoices/apply_invoice_custom_sections_service.rb new file mode 100644 index 00000000000..f9349474a98 --- /dev/null +++ b/app/services/invoices/apply_invoice_custom_sections_service.rb @@ -0,0 +1,34 @@ +# frozen_string_literal: true + +module Invoices + class ApplyInvoiceCustomSectionsService < BaseService + def initialize(invoice:) + @invoice = invoice + @customer = invoice.customer + + super() + end + + def call + result.applied_sections = [] + return result if customer.skip_invoice_custom_sections + + customer.applicable_invoice_custom_sections.each do |custom_section| + invoice.applied_invoice_custom_sections.create!( + code: custom_section.code, + details: custom_section.details, + display_name: custom_section.display_name, + name: custom_section.name + ) + end + result.applied_sections = invoice.applied_invoice_custom_sections + result + rescue ActiveRecord::RecordInvalid => e + result.record_validation_failure!(record: e.record) + end + + private + + attr_reader :invoice, :customer + end +end diff --git a/spec/services/invoices/apply_invoice_custom_sections_service_spec.rb b/spec/services/invoices/apply_invoice_custom_sections_service_spec.rb new file mode 100644 index 00000000000..e74f6111e80 --- /dev/null +++ b/spec/services/invoices/apply_invoice_custom_sections_service_spec.rb @@ -0,0 +1,57 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe Invoices::ApplyInvoiceCustomSectionsService, type: :service do + subject(:invoice_service) { described_class.new(invoice:) } + + let(:organization) { create(:organization) } + let(:customer) { create(:customer, organization:) } + let(:invoice) { create(:invoice, customer:) } + let(:custom_sections) { create_list(:invoice_custom_section, 3, organization:) } + + before do + organization.selected_invoice_custom_sections << custom_sections[1..2] + end + + describe '#call' do + context 'when the customer has skip_invoice_custom_sections flag' do + let(:customer) { create(:customer, organization:, skip_invoice_custom_sections: true) } + + it 'does not apply any custom sections' do + result = invoice_service.call + expect(result).to be_success + expect(result.applied_sections).to be_empty + expect(invoice.reload.applied_invoice_custom_sections).to be_empty + end + end + + context 'when the customer has custom sections' do + before do + customer.selected_invoice_custom_sections << custom_sections[0..1] + end + + it 'applies the custom sections to the invoice' do + result = invoice_service.call + expect(result).to be_success + sections = invoice.reload.applied_invoice_custom_sections + expect(sections.map(&:code)).to match_array(custom_sections[0..1].map(&:code)) + expect(sections.map(&:details)).to match_array(custom_sections[0..1].map(&:details)) + expect(sections.map(&:display_name)).to match_array(custom_sections[0..1].map(&:display_name)) + expect(sections.map(&:name)).to match_array(custom_sections[0..1].map(&:name)) + end + end + + context 'when the customer inherits custom sections from the organization' do + it 'applies the organization\'s sections to the invoice' do + result = invoice_service.call + expect(result).to be_success + sections = invoice.reload.applied_invoice_custom_sections + expect(sections.map(&:code)).to match_array(custom_sections[1..2].map(&:code)) + expect(sections.map(&:details)).to match_array(custom_sections[1..2].map(&:details)) + expect(sections.map(&:display_name)).to match_array(custom_sections[1..2].map(&:display_name)) + expect(sections.map(&:name)).to match_array(custom_sections[1..2].map(&:name)) + end + end + end +end From 90594ece09422cbd690fc3add2f517d85bc7ef63 Mon Sep 17 00:00:00 2001 From: Anna Velentsevich Date: Thu, 26 Dec 2024 15:19:15 +0100 Subject: [PATCH 10/19] call apply_invoice_custom_section service when creating invoices --- app/services/invoices/advance_charges_service.rb | 1 + app/services/invoices/refresh_draft_service.rb | 2 ++ app/services/invoices/subscription_service.rb | 1 + 3 files changed, 4 insertions(+) diff --git a/app/services/invoices/advance_charges_service.rb b/app/services/invoices/advance_charges_service.rb index 159159ba036..7b40bf0fea0 100644 --- a/app/services/invoices/advance_charges_service.rb +++ b/app/services/invoices/advance_charges_service.rb @@ -69,6 +69,7 @@ def create_group_invoice end Invoices::ComputeAmountsFromFees.call(invoice:) + Invoices::ApplyInvoiceCustomSectionsService.call(invoice).raise_if_error! invoice.payment_status = :succeeded Invoices::TransitionToFinalStatusService.call(invoice:) diff --git a/app/services/invoices/refresh_draft_service.rb b/app/services/invoices/refresh_draft_service.rb index 5a815a387f3..620a46e9347 100644 --- a/app/services/invoices/refresh_draft_service.rb +++ b/app/services/invoices/refresh_draft_service.rb @@ -121,6 +121,7 @@ def reset_invoice_values invoice_subscriptions.destroy_all invoice.applied_taxes.destroy_all invoice.error_details.discard_all + invoice.applied_invoice_custom_sections.destroy_all invoice.taxes_amount_cents = 0 invoice.total_amount_cents = 0 @@ -129,6 +130,7 @@ def reset_invoice_values invoice.sub_total_excluding_taxes_amount_cents = 0 invoice.sub_total_including_taxes_amount_cents = 0 invoice.progressive_billing_credit_amount_cents = 0 + invoice.save! end end diff --git a/app/services/invoices/subscription_service.rb b/app/services/invoices/subscription_service.rb index 2a5ce41ee3e..5d03bce4434 100644 --- a/app/services/invoices/subscription_service.rb +++ b/app/services/invoices/subscription_service.rb @@ -33,6 +33,7 @@ def call recurring:, context: ) + Invoices::ApplyInvoiceCustomSectionsService.call(invoice).raise_if_error! set_invoice_generated_status unless invoice.pending? invoice.save! From 6809e3a6ced525612b65d0e1f7e23907f8fd3496 Mon Sep 17 00:00:00 2001 From: Anna Velentsevich Date: Thu, 26 Dec 2024 17:30:33 +0100 Subject: [PATCH 11/19] fix service call --- app/services/invoices/advance_charges_service.rb | 2 +- app/services/invoices/subscription_service.rb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/services/invoices/advance_charges_service.rb b/app/services/invoices/advance_charges_service.rb index 7b40bf0fea0..2adc88f8156 100644 --- a/app/services/invoices/advance_charges_service.rb +++ b/app/services/invoices/advance_charges_service.rb @@ -69,7 +69,7 @@ def create_group_invoice end Invoices::ComputeAmountsFromFees.call(invoice:) - Invoices::ApplyInvoiceCustomSectionsService.call(invoice).raise_if_error! + Invoices::ApplyInvoiceCustomSectionsService.call(invoice:).raise_if_error! invoice.payment_status = :succeeded Invoices::TransitionToFinalStatusService.call(invoice:) diff --git a/app/services/invoices/subscription_service.rb b/app/services/invoices/subscription_service.rb index 5d03bce4434..433edc013ed 100644 --- a/app/services/invoices/subscription_service.rb +++ b/app/services/invoices/subscription_service.rb @@ -33,7 +33,7 @@ def call recurring:, context: ) - Invoices::ApplyInvoiceCustomSectionsService.call(invoice).raise_if_error! + Invoices::ApplyInvoiceCustomSectionsService.call(invoice:).raise_if_error! set_invoice_generated_status unless invoice.pending? invoice.save! From 02fb8c572ee49bf98e3d483530816e34870b962c Mon Sep 17 00:00:00 2001 From: Anna Velentsevich Date: Fri, 27 Dec 2024 10:42:48 +0100 Subject: [PATCH 12/19] add call to create applied_invoice_custom_sections from all invoice creating services --- app/services/invoices/advance_charges_service.rb | 2 +- app/services/invoices/create_one_off_service.rb | 1 + app/services/invoices/create_pay_in_advance_charge_service.rb | 1 + app/services/invoices/customer_usage_service.rb | 1 + app/services/invoices/paid_credit_service.rb | 1 + app/services/invoices/progressive_billing_service.rb | 1 + app/services/invoices/refresh_draft_service.rb | 1 + app/services/invoices/subscription_service.rb | 2 +- 8 files changed, 8 insertions(+), 2 deletions(-) diff --git a/app/services/invoices/advance_charges_service.rb b/app/services/invoices/advance_charges_service.rb index 2adc88f8156..a5ade782fde 100644 --- a/app/services/invoices/advance_charges_service.rb +++ b/app/services/invoices/advance_charges_service.rb @@ -69,7 +69,7 @@ def create_group_invoice end Invoices::ComputeAmountsFromFees.call(invoice:) - Invoices::ApplyInvoiceCustomSectionsService.call(invoice:).raise_if_error! + Invoices::ApplyInvoiceCustomSectionsService.call(invoice:) invoice.payment_status = :succeeded Invoices::TransitionToFinalStatusService.call(invoice:) diff --git a/app/services/invoices/create_one_off_service.rb b/app/services/invoices/create_one_off_service.rb index 44953c03332..b5719e92bdc 100644 --- a/app/services/invoices/create_one_off_service.rb +++ b/app/services/invoices/create_one_off_service.rb @@ -35,6 +35,7 @@ def call end Invoices::ComputeAmountsFromFees.call(invoice:, provider_taxes: result.fees_taxes) + Invoices::ApplyInvoiceCustomSectionsService.call(invoice:) invoice.payment_status = invoice.total_amount_cents.positive? ? :pending : :succeeded Invoices::TransitionToFinalStatusService.call(invoice:) invoice.save! diff --git a/app/services/invoices/create_pay_in_advance_charge_service.rb b/app/services/invoices/create_pay_in_advance_charge_service.rb index d78a6bdc0be..f002e8d7b5a 100644 --- a/app/services/invoices/create_pay_in_advance_charge_service.rb +++ b/app/services/invoices/create_pay_in_advance_charge_service.rb @@ -41,6 +41,7 @@ def call Invoices::ComputeAmountsFromFees.call(invoice:, provider_taxes: result.fees_taxes) create_credit_note_credit create_applied_prepaid_credit if should_create_applied_prepaid_credit? + Invoices::ApplyInvoiceCustomSectionsService.call(invoice:) invoice.payment_status = invoice.total_amount_cents.positive? ? :pending : :succeeded Invoices::TransitionToFinalStatusService.call(invoice:) diff --git a/app/services/invoices/customer_usage_service.rb b/app/services/invoices/customer_usage_service.rb index 2b8f13145af..58baeacd3b6 100644 --- a/app/services/invoices/customer_usage_service.rb +++ b/app/services/invoices/customer_usage_service.rb @@ -70,6 +70,7 @@ def compute_usage end format_usage + Invoices::ApplyInvoiceCustomSectionsService.call(invoice:) end def add_charge_fees diff --git a/app/services/invoices/paid_credit_service.rb b/app/services/invoices/paid_credit_service.rb index 401d04ef29d..3bf445b9735 100644 --- a/app/services/invoices/paid_credit_service.rb +++ b/app/services/invoices/paid_credit_service.rb @@ -22,6 +22,7 @@ def call ActiveRecord::Base.transaction do create_credit_fee(invoice) compute_amounts(invoice) + Invoices::ApplyInvoiceCustomSectionsService.call(invoice:) if License.premium? && wallet_transaction.invoice_requires_successful_payment? invoice.open! diff --git a/app/services/invoices/progressive_billing_service.rb b/app/services/invoices/progressive_billing_service.rb index 514b11a8f6c..e07ac6a039f 100644 --- a/app/services/invoices/progressive_billing_service.rb +++ b/app/services/invoices/progressive_billing_service.rb @@ -21,6 +21,7 @@ def call Credits::ProgressiveBillingService.call(invoice:) Credits::AppliedCouponsService.call(invoice:) + Invoices::ApplyInvoiceCustomSectionsService.call(invoice:) totals_result = Invoices::ComputeTaxesAndTotalsService.call(invoice:) return totals_result if !totals_result.success? && totals_result.error.is_a?(BaseService::UnknownTaxFailure) diff --git a/app/services/invoices/refresh_draft_service.rb b/app/services/invoices/refresh_draft_service.rb index 620a46e9347..4f61285dea6 100644 --- a/app/services/invoices/refresh_draft_service.rb +++ b/app/services/invoices/refresh_draft_service.rb @@ -56,6 +56,7 @@ def call recurring:, context: ) + Invoices::ApplyInvoiceCustomSectionsService.call(invoice:) invoice.credit_notes.each do |credit_note| subscription_id = cn_subscription_ids.find { |h| h[:credit_note_id] == credit_note.id }[:subscription_id] diff --git a/app/services/invoices/subscription_service.rb b/app/services/invoices/subscription_service.rb index 433edc013ed..f4d20fd6abc 100644 --- a/app/services/invoices/subscription_service.rb +++ b/app/services/invoices/subscription_service.rb @@ -33,7 +33,7 @@ def call recurring:, context: ) - Invoices::ApplyInvoiceCustomSectionsService.call(invoice:).raise_if_error! + Invoices::ApplyInvoiceCustomSectionsService.call(invoice:) set_invoice_generated_status unless invoice.pending? invoice.save! From 924d1a89ff6b1aee41cc22ead078c5f1797975c4 Mon Sep 17 00:00:00 2001 From: Anna Velentsevich Date: Fri, 27 Dec 2024 17:08:46 +0100 Subject: [PATCH 13/19] wip adding tests for applying invoice custom sections --- .../invoices/subscription_service_spec.rb | 4 +++ .../applied_invoice_custom_sections.rb | 25 +++++++++++++++++++ 2 files changed, 29 insertions(+) diff --git a/spec/services/invoices/subscription_service_spec.rb b/spec/services/invoices/subscription_service_spec.rb index 9a53d022525..3c03494b7d1 100644 --- a/spec/services/invoices/subscription_service_spec.rb +++ b/spec/services/invoices/subscription_service_spec.rb @@ -106,6 +106,10 @@ let(:service_call) { invoice_service.call } end + it_behaves_like "applies invoice_custom_sections" do + let(:service_call) { invoice_service.call } + end + it "enqueues a SendWebhookJob" do expect do invoice_service.call diff --git a/spec/support/shared_examples/applied_invoice_custom_sections.rb b/spec/support/shared_examples/applied_invoice_custom_sections.rb index e69de29bb2d..dcd08b4aa39 100644 --- a/spec/support/shared_examples/applied_invoice_custom_sections.rb +++ b/spec/support/shared_examples/applied_invoice_custom_sections.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'applies invoice_custom_sections' do + let(:invoice_custom_sections) { create_list(:invoice_custom_section, 4, organization:)} + + before do + organization.selected_invoice_custom_sections = invoice_custom_sections[2..3] + end + + context 'there customer has :skip_invoice_custom_sections flag' do + before { customer.update(skip_invoice_custom_sections: true) } + + it 'doesn\'t create applied_invoice_custom_section' do + expect { service_call }.not_to change { AppliedInvoiceCustomSection.count } + end + end + + context 'when customer follows organizations invoice_custom_sections' do + it 'creates applied_invoice_custom_sections' do + result = service_call + invoice = result.invoice + expect(invoice.applied_invoice_custom_sections.pluck(:code)).to match_array(organization.selected_invoice_custom_sections.pluck(:code)) + end + end +end From 877df57b7d35a904f824b1b65f85f529d5a11a08 Mon Sep 17 00:00:00 2001 From: Anna Velentsevich Date: Mon, 30 Dec 2024 14:41:11 +0100 Subject: [PATCH 14/19] add tests on creating applied_invoice_custom_section for different invoices --- app/services/invoices/add_on_service.rb | 1 + .../invoices/customer_usage_service.rb | 2 +- spec/services/fees/one_off_service_spec.rb | 4 ++++ spec/services/invoices/add_on_service_spec.rb | 4 ++++ .../invoices/create_one_off_service_spec.rb | 4 ++++ ...eate_pay_in_advance_charge_service_spec.rb | 4 ++++ .../invoices/paid_credit_service_spec.rb | 4 ++++ .../progressive_billing_service_spec.rb | 4 ++++ ...refresh_draft_and_finalize_service_spec.rb | 4 ++++ .../invoices/refresh_draft_service_spec.rb | 19 +++++++++++++++++++ 10 files changed, 49 insertions(+), 1 deletion(-) diff --git a/app/services/invoices/add_on_service.rb b/app/services/invoices/add_on_service.rb index 791665b2070..bc98bdd1110 100644 --- a/app/services/invoices/add_on_service.rb +++ b/app/services/invoices/add_on_service.rb @@ -26,6 +26,7 @@ def create create_add_on_fee(invoice) compute_amounts(invoice) + Invoices::ApplyInvoiceCustomSectionsService.call(invoice:) invoice.save! diff --git a/app/services/invoices/customer_usage_service.rb b/app/services/invoices/customer_usage_service.rb index 58baeacd3b6..f3b910e740f 100644 --- a/app/services/invoices/customer_usage_service.rb +++ b/app/services/invoices/customer_usage_service.rb @@ -70,7 +70,7 @@ def compute_usage end format_usage - Invoices::ApplyInvoiceCustomSectionsService.call(invoice:) + # Invoices::ApplyInvoiceCustomSectionsService.call(invoice:) end def add_charge_fees diff --git a/spec/services/fees/one_off_service_spec.rb b/spec/services/fees/one_off_service_spec.rb index 7131607206a..d0e7fca9417 100644 --- a/spec/services/fees/one_off_service_spec.rb +++ b/spec/services/fees/one_off_service_spec.rb @@ -342,5 +342,9 @@ end end end + + it_behaves_like "applies invoice_custom_sections" do + let(:service_call) { one_off_service.call } + end end end diff --git a/spec/services/invoices/add_on_service_spec.rb b/spec/services/invoices/add_on_service_spec.rb index 7ca1edc1873..4f1361b7034 100644 --- a/spec/services/invoices/add_on_service_spec.rb +++ b/spec/services/invoices/add_on_service_spec.rb @@ -107,6 +107,10 @@ let(:service_call) { invoice_service.create } end + it_behaves_like "applies invoice_custom_sections" do + let(:service_call) { invoice_service.call } + end + context 'with customer timezone' do before { applied_add_on.customer.update!(timezone: 'America/Los_Angeles') } diff --git a/spec/services/invoices/create_one_off_service_spec.rb b/spec/services/invoices/create_one_off_service_spec.rb index 501b0fdf269..21b9cb5b861 100644 --- a/spec/services/invoices/create_one_off_service_spec.rb +++ b/spec/services/invoices/create_one_off_service_spec.rb @@ -67,6 +67,10 @@ let(:service_call) { create_service.call } end + it_behaves_like "applies invoice_custom_sections" do + let(:service_call) { invoice_service.call } + end + it 'calls SegmentTrackJob' do invoice = create_service.call.invoice diff --git a/spec/services/invoices/create_pay_in_advance_charge_service_spec.rb b/spec/services/invoices/create_pay_in_advance_charge_service_spec.rb index d7147173e9e..4667671be93 100644 --- a/spec/services/invoices/create_pay_in_advance_charge_service_spec.rb +++ b/spec/services/invoices/create_pay_in_advance_charge_service_spec.rb @@ -318,5 +318,9 @@ expect(result.invoice).to be_finalized end end + + it_behaves_like "applies invoice_custom_sections" do + let(:service_call) { invoice_service.call } + end end end diff --git a/spec/services/invoices/paid_credit_service_spec.rb b/spec/services/invoices/paid_credit_service_spec.rb index 388171cb682..10dd929980c 100644 --- a/spec/services/invoices/paid_credit_service_spec.rb +++ b/spec/services/invoices/paid_credit_service_spec.rb @@ -62,6 +62,10 @@ let(:service_call) { invoice_service.call } end + it_behaves_like "applies invoice_custom_sections" do + let(:service_call) { invoice_service.call } + end + it 'does not enqueue an SendEmailJob' do expect do invoice_service.call diff --git a/spec/services/invoices/progressive_billing_service_spec.rb b/spec/services/invoices/progressive_billing_service_spec.rb index 3977b46faf4..7d9b2e75468 100644 --- a/spec/services/invoices/progressive_billing_service_spec.rb +++ b/spec/services/invoices/progressive_billing_service_spec.rb @@ -232,5 +232,9 @@ it_behaves_like 'syncs invoice' do let(:service_call) { create_service.call } end + + it_behaves_like "applies invoice_custom_sections" do + let(:service_call) { create_service.call } + end end end diff --git a/spec/services/invoices/refresh_draft_and_finalize_service_spec.rb b/spec/services/invoices/refresh_draft_and_finalize_service_spec.rb index c16e9f10714..7c93708790e 100644 --- a/spec/services/invoices/refresh_draft_and_finalize_service_spec.rb +++ b/spec/services/invoices/refresh_draft_and_finalize_service_spec.rb @@ -79,6 +79,10 @@ let(:service_call) { finalize_service.call } end + it_behaves_like "applies invoice_custom_sections" do + let(:service_call) { invoice_service.call } + end + it 'enqueues a SendWebhookJob' do expect do finalize_service.call diff --git a/spec/services/invoices/refresh_draft_service_spec.rb b/spec/services/invoices/refresh_draft_service_spec.rb index 590e0d4860d..7cca769a720 100644 --- a/spec/services/invoices/refresh_draft_service_spec.rb +++ b/spec/services/invoices/refresh_draft_service_spec.rb @@ -171,6 +171,10 @@ .to change { invoice.reload.progressive_billing_credit_amount_cents }.from(1239000).to(0) end + it_behaves_like "applies invoice_custom_sections" do + let(:service_call) { refresh_service.call } + end + context 'when there is a tax_integration set up' do let(:integration) { create(:anrok_integration, organization:) } let(:integration_customer) { create(:anrok_customer, integration:, customer:) } @@ -203,6 +207,21 @@ end end + context 'when invoice has other applied invoice_custom_sections' do + let(:invoice_custom_sections) { create_list(:invoice_custom_section, 4, organization: organization) } + let(:applied_invoice_custom_sections) { create_list(:applied_invoice_custom_section, 2, invoice: invoice) } + + before do + applied_invoice_custom_sections + customer.selected_invoice_custom_sections = invoice_custom_sections.take(3) + end + + it 'creates new applied_invoice_custom_sections' do + expect { refresh_service.call }.to change { invoice.reload.applied_invoice_custom_sections.count }.from(2).to(3) + expect(invoice.applied_invoice_custom_sections.map(&:code)).to match(customer.selected_invoice_custom_sections.map(&:code)) + end + end + it 'flags lifetime usage for refresh' do create(:usage_threshold, plan: subscription.plan) From 0ada1ba253ba0c6946ee0e25ca835599ab0fb4b9 Mon Sep 17 00:00:00 2001 From: Anna Velentsevich Date: Mon, 30 Dec 2024 14:48:01 +0100 Subject: [PATCH 15/19] fix linter --- .../shared_examples/applied_invoice_custom_sections.rb | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/spec/support/shared_examples/applied_invoice_custom_sections.rb b/spec/support/shared_examples/applied_invoice_custom_sections.rb index dcd08b4aa39..3b46340a9a4 100644 --- a/spec/support/shared_examples/applied_invoice_custom_sections.rb +++ b/spec/support/shared_examples/applied_invoice_custom_sections.rb @@ -1,17 +1,17 @@ # frozen_string_literal: true RSpec.shared_examples 'applies invoice_custom_sections' do - let(:invoice_custom_sections) { create_list(:invoice_custom_section, 4, organization:)} + let(:invoice_custom_sections) { create_list(:invoice_custom_section, 4, organization:) } before do organization.selected_invoice_custom_sections = invoice_custom_sections[2..3] end - context 'there customer has :skip_invoice_custom_sections flag' do + context 'when the customer has :skip_invoice_custom_sections flag' do before { customer.update(skip_invoice_custom_sections: true) } it 'doesn\'t create applied_invoice_custom_section' do - expect { service_call }.not_to change { AppliedInvoiceCustomSection.count } + expect { service_call }.not_to change(AppliedInvoiceCustomSection, :count) end end From 4ea553a1668829c3696c8f1189a3eb20d9943fbd Mon Sep 17 00:00:00 2001 From: Anna Velentsevich Date: Mon, 30 Dec 2024 15:11:23 +0100 Subject: [PATCH 16/19] fix tests --- spec/services/fees/one_off_service_spec.rb | 4 ---- spec/services/invoices/add_on_service_spec.rb | 2 +- spec/services/invoices/create_one_off_service_spec.rb | 2 +- .../invoices/refresh_draft_and_finalize_service_spec.rb | 2 +- 4 files changed, 3 insertions(+), 7 deletions(-) diff --git a/spec/services/fees/one_off_service_spec.rb b/spec/services/fees/one_off_service_spec.rb index d0e7fca9417..7131607206a 100644 --- a/spec/services/fees/one_off_service_spec.rb +++ b/spec/services/fees/one_off_service_spec.rb @@ -342,9 +342,5 @@ end end end - - it_behaves_like "applies invoice_custom_sections" do - let(:service_call) { one_off_service.call } - end end end diff --git a/spec/services/invoices/add_on_service_spec.rb b/spec/services/invoices/add_on_service_spec.rb index 4f1361b7034..4430340863d 100644 --- a/spec/services/invoices/add_on_service_spec.rb +++ b/spec/services/invoices/add_on_service_spec.rb @@ -108,7 +108,7 @@ end it_behaves_like "applies invoice_custom_sections" do - let(:service_call) { invoice_service.call } + let(:service_call) { invoice_service.create } end context 'with customer timezone' do diff --git a/spec/services/invoices/create_one_off_service_spec.rb b/spec/services/invoices/create_one_off_service_spec.rb index 21b9cb5b861..af26b5f3f9c 100644 --- a/spec/services/invoices/create_one_off_service_spec.rb +++ b/spec/services/invoices/create_one_off_service_spec.rb @@ -68,7 +68,7 @@ end it_behaves_like "applies invoice_custom_sections" do - let(:service_call) { invoice_service.call } + let(:service_call) { create_service.call } end it 'calls SegmentTrackJob' do diff --git a/spec/services/invoices/refresh_draft_and_finalize_service_spec.rb b/spec/services/invoices/refresh_draft_and_finalize_service_spec.rb index 7c93708790e..dfc977abbfd 100644 --- a/spec/services/invoices/refresh_draft_and_finalize_service_spec.rb +++ b/spec/services/invoices/refresh_draft_and_finalize_service_spec.rb @@ -80,7 +80,7 @@ end it_behaves_like "applies invoice_custom_sections" do - let(:service_call) { invoice_service.call } + let(:service_call) { finalize_service.call } end it 'enqueues a SendWebhookJob' do From 91b907935c95822af841bf13e1689adce09bd1a7 Mon Sep 17 00:00:00 2001 From: Anna Velentsevich Date: Thu, 9 Jan 2025 18:11:47 +0100 Subject: [PATCH 17/19] remove dyplicated migration --- ...e_uniqueness_constraint_on_invoice_custom_sections.rb | 9 --------- 1 file changed, 9 deletions(-) delete mode 100644 db/migrate/20241227154337_add_code_uniqueness_constraint_on_invoice_custom_sections.rb diff --git a/db/migrate/20241227154337_add_code_uniqueness_constraint_on_invoice_custom_sections.rb b/db/migrate/20241227154337_add_code_uniqueness_constraint_on_invoice_custom_sections.rb deleted file mode 100644 index e444ad43fc7..00000000000 --- a/db/migrate/20241227154337_add_code_uniqueness_constraint_on_invoice_custom_sections.rb +++ /dev/null @@ -1,9 +0,0 @@ -# frozen_string_literal: true - -class AddCodeUniquenessConstraintOnInvoiceCustomSections < ActiveRecord::Migration[7.1] - disable_ddl_transaction! - - def change - remove_index :invoice_custom_sections, %i[organization_id code], unique: true, algorithm: :concurrently - end -end From 901fa19506427782dfc4788257ec48b130dd0150 Mon Sep 17 00:00:00 2001 From: Anna Velentsevich Date: Fri, 10 Jan 2025 09:38:41 +0100 Subject: [PATCH 18/19] remove not needed call --- app/services/invoices/customer_usage_service.rb | 1 - 1 file changed, 1 deletion(-) diff --git a/app/services/invoices/customer_usage_service.rb b/app/services/invoices/customer_usage_service.rb index f3b910e740f..2b8f13145af 100644 --- a/app/services/invoices/customer_usage_service.rb +++ b/app/services/invoices/customer_usage_service.rb @@ -70,7 +70,6 @@ def compute_usage end format_usage - # Invoices::ApplyInvoiceCustomSectionsService.call(invoice:) end def add_charge_fees From 2c5755f931002e635956e74c375dbbf3cf051101 Mon Sep 17 00:00:00 2001 From: Anna Velentsevich Date: Tue, 14 Jan 2025 09:39:58 +0100 Subject: [PATCH 19/19] Feat(invoice_custom_sections): use custom sections in pdf (#3013) Use applied invoice custom sections in PDFs and include them in invoice payload Also includes fixes found after QA --- app/controllers/api/v1/invoices_controller.rb | 2 +- .../invoice_custom_sections_resolver.rb | 11 ++++--- app/models/customer.rb | 2 +- app/models/invoice_custom_section.rb | 4 ++- .../v1/invoice_custom_section_serializer.rb | 6 ++-- app/serializers/v1/invoice_serializer.rb | 9 ++++++ ...plied_invoice_custom_section_serializer.rb | 18 +++++++++++ .../invoices/add_on_created_service.rb | 2 +- .../webhooks/invoices/created_service.rb | 2 +- .../invoices/one_off_created_service.rb | 2 +- .../invoices/paid_credit_added_service.rb | 2 +- app/views/templates/invoices/v3.slim | 3 ++ .../invoices/v3/_custom_sections.slim | 15 +++++++++ app/views/templates/invoices/v3/charge.slim | 3 ++ app/views/templates/invoices/v3/one_off.slim | 3 ++ app/views/templates/invoices/v4.slim | 18 +++++++---- .../invoices/v4/_custom_sections.slim | 15 +++++++++ app/views/templates/invoices/v4/charge.slim | 3 ++ app/views/templates/invoices/v4/one_off.slim | 3 ++ .../invoice_custom_sections_resolver_spec.rb | 3 +- spec/models/customer_spec.rb | 4 +-- .../api/v1/customers_controller_spec.rb | 2 +- .../api/v1/invoices_controller_spec.rb | 3 +- .../invoice_custom_section_serializer_spec.rb | 6 ++-- ..._invoice_custom_section_serializer_spec.rb | 32 +++++++++++++++++++ .../invoices/refresh_draft_service_spec.rb | 2 +- 26 files changed, 144 insertions(+), 31 deletions(-) create mode 100644 app/serializers/v1/invoices/applied_invoice_custom_section_serializer.rb create mode 100644 app/views/templates/invoices/v3/_custom_sections.slim create mode 100644 app/views/templates/invoices/v4/_custom_sections.slim create mode 100644 spec/serializers/v1/invoices/applied_invoice_custom_section_serializer_spec.rb diff --git a/app/controllers/api/v1/invoices_controller.rb b/app/controllers/api/v1/invoices_controller.rb index a203051cdd3..5be51b7b6c7 100644 --- a/app/controllers/api/v1/invoices_controller.rb +++ b/app/controllers/api/v1/invoices_controller.rb @@ -241,7 +241,7 @@ def render_invoice(invoice) json: ::V1::InvoiceSerializer.new( invoice, root_name: 'invoice', - includes: %i[customer subscriptions fees credits metadata applied_taxes error_details] + includes: %i[customer subscriptions fees credits metadata applied_taxes error_details applied_invoice_custom_sections] ) ) end diff --git a/app/graphql/resolvers/invoice_custom_sections_resolver.rb b/app/graphql/resolvers/invoice_custom_sections_resolver.rb index f34d3fc1c43..f759479ab7f 100644 --- a/app/graphql/resolvers/invoice_custom_sections_resolver.rb +++ b/app/graphql/resolvers/invoice_custom_sections_resolver.rb @@ -15,10 +15,13 @@ class InvoiceCustomSectionsResolver < Resolvers::BaseResolver type Types::InvoiceCustomSections::Object.collection_type, null: true def resolve(page: nil, limit: nil) - current_organization.invoice_custom_sections.left_outer_joins(:invoice_custom_section_selections).order( - Arel.sql('CASE WHEN invoice_custom_section_selections.id IS NOT NULL THEN 0 ELSE 1 END'), - :name - ).page(page).per(limit) + current_organization.invoice_custom_sections + .joins('LEFT JOIN invoice_custom_section_selections ON invoice_custom_sections.id = invoice_custom_section_selections.invoice_custom_section_id + AND invoice_custom_section_selections.customer_id is NULL') + .order( + Arel.sql('CASE WHEN invoice_custom_section_selections.id IS NOT NULL THEN 0 ELSE 1 END'), + :name + ).page(page).per(limit) end end end diff --git a/app/models/customer.rb b/app/models/customer.rb index 25aea92262a..91015b26df4 100644 --- a/app/models/customer.rb +++ b/app/models/customer.rb @@ -134,7 +134,7 @@ def applicable_net_payment_term def applicable_invoice_custom_sections return [] if skip_invoice_custom_sections? - selected_invoice_custom_sections.presence || organization.selected_invoice_custom_sections + selected_invoice_custom_sections.order(:name).presence || organization.selected_invoice_custom_sections.order(:name) end def editable? diff --git a/app/models/invoice_custom_section.rb b/app/models/invoice_custom_section.rb index 4842aaed333..bd049ead9cf 100644 --- a/app/models/invoice_custom_section.rb +++ b/app/models/invoice_custom_section.rb @@ -8,7 +8,9 @@ class InvoiceCustomSection < ApplicationRecord has_many :invoice_custom_section_selections, dependent: :destroy validates :name, presence: true - validates :code, presence: true, uniqueness: {scope: :organization_id} + validates :code, + presence: true, + uniqueness: {conditions: -> { where(deleted_at: nil) }, scope: :organization_id} default_scope -> { kept } diff --git a/app/serializers/v1/invoice_custom_section_serializer.rb b/app/serializers/v1/invoice_custom_section_serializer.rb index 87dd9b6e914..f430be28a5c 100644 --- a/app/serializers/v1/invoice_custom_section_serializer.rb +++ b/app/serializers/v1/invoice_custom_section_serializer.rb @@ -10,10 +10,8 @@ def serialize description: model.description, details: model.details, display_name: model.display_name, - selected_for_organization: model.selected_for_organization?, - organization: { - lago_id: model.organization_id - } + applied_to_organization: model.selected_for_organization?, + organization_id: model.organization_id } end end diff --git a/app/serializers/v1/invoice_serializer.rb b/app/serializers/v1/invoice_serializer.rb index 522f94ebd81..ef1169162e3 100644 --- a/app/serializers/v1/invoice_serializer.rb +++ b/app/serializers/v1/invoice_serializer.rb @@ -39,6 +39,7 @@ def serialize payload.merge!(applied_taxes) if include?(:applied_taxes) payload.merge!(error_details) if include?(:error_details) payload.merge!(applied_usage_thresholds) if model.progressive_billing? + payload.merge!(applied_invoice_custom_sections) if include?(:applied_invoice_custom_sections) payload end @@ -112,5 +113,13 @@ def applied_usage_thresholds collection_name: 'applied_usage_thresholds' ).serialize end + + def applied_invoice_custom_sections + ::CollectionSerializer.new( + model.applied_invoice_custom_sections, + ::V1::Invoices::AppliedInvoiceCustomSectionSerializer, + collection_name: 'applied_invoice_custom_sections' + ).serialize + end end end diff --git a/app/serializers/v1/invoices/applied_invoice_custom_section_serializer.rb b/app/serializers/v1/invoices/applied_invoice_custom_section_serializer.rb new file mode 100644 index 00000000000..7894892b62b --- /dev/null +++ b/app/serializers/v1/invoices/applied_invoice_custom_section_serializer.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +module V1 + module Invoices + class AppliedInvoiceCustomSectionSerializer < ModelSerializer + def serialize + { + lago_id: model.id, + lago_invoice_id: model.invoice_id, + code: model.code, + details: model.details, + display_name: model.display_name, + created_at: model.created_at.iso8601 + } + end + end + end +end diff --git a/app/services/webhooks/invoices/add_on_created_service.rb b/app/services/webhooks/invoices/add_on_created_service.rb index 7ddffd3376c..92ddf958f13 100644 --- a/app/services/webhooks/invoices/add_on_created_service.rb +++ b/app/services/webhooks/invoices/add_on_created_service.rb @@ -13,7 +13,7 @@ def object_serializer ::V1::InvoiceSerializer.new( object, root_name: 'invoice', - includes: %i[customer subscriptions fees] + includes: %i[customer subscriptions fees applied_invoice_custom_sections] ) end diff --git a/app/services/webhooks/invoices/created_service.rb b/app/services/webhooks/invoices/created_service.rb index 8358922784b..48e395326a6 100644 --- a/app/services/webhooks/invoices/created_service.rb +++ b/app/services/webhooks/invoices/created_service.rb @@ -13,7 +13,7 @@ def object_serializer ::V1::InvoiceSerializer.new( object, root_name: 'invoice', - includes: %i[customer subscriptions fees credits applied_taxes] + includes: %i[customer subscriptions fees credits applied_taxes applied_invoice_custom_sections] ) end diff --git a/app/services/webhooks/invoices/one_off_created_service.rb b/app/services/webhooks/invoices/one_off_created_service.rb index 057fdddcd74..6090e5fb6c4 100644 --- a/app/services/webhooks/invoices/one_off_created_service.rb +++ b/app/services/webhooks/invoices/one_off_created_service.rb @@ -13,7 +13,7 @@ def object_serializer ::V1::InvoiceSerializer.new( object, root_name: 'invoice', - includes: %i[customer fees applied_taxes] + includes: %i[customer fees applied_taxes applied_invoice_custom_sections] ) end diff --git a/app/services/webhooks/invoices/paid_credit_added_service.rb b/app/services/webhooks/invoices/paid_credit_added_service.rb index 91e1ef83058..6956fc4476b 100644 --- a/app/services/webhooks/invoices/paid_credit_added_service.rb +++ b/app/services/webhooks/invoices/paid_credit_added_service.rb @@ -13,7 +13,7 @@ def object_serializer ::V1::InvoiceSerializer.new( object, root_name: 'invoice', - includes: %i[customer fees applied_taxes] + includes: %i[customer fees applied_taxes applied_invoice_custom_sections] ) end diff --git a/app/views/templates/invoices/v3.slim b/app/views/templates/invoices/v3.slim index 72a93e9eed1..44ef15c833c 100644 --- a/app/views/templates/invoices/v3.slim +++ b/app/views/templates/invoices/v3.slim @@ -425,6 +425,9 @@ html - else == SlimHelper.render('templates/invoices/v3/_subscriptions_summary', self) + + - if applied_invoice_custom_sections.present? + == SlimHelper.render('templates/invoices/v3/_custom_sections', self) p.body-3.mb-24 = LineBreakHelper.break_lines(organization.invoice_footer) .powered-by diff --git a/app/views/templates/invoices/v3/_custom_sections.slim b/app/views/templates/invoices/v3/_custom_sections.slim new file mode 100644 index 00000000000..d6e60631f3c --- /dev/null +++ b/app/views/templates/invoices/v3/_custom_sections.slim @@ -0,0 +1,15 @@ +css: + .invoice-custom-section { + margin-top: 24px; + border-bottom: 1px solid #D9DEE7; + } + + .invoice-custom-section p.section-name { + margin-bottom: 8px; + } + +.invoice-custom-sections.body-3.mb-24 + - applied_invoice_custom_sections.each do |section| + .invoice-custom-section + p.body-1.section-name = section.display_name + p.body-3.mb-24 = LineBreakHelper.break_lines(section.details) diff --git a/app/views/templates/invoices/v3/charge.slim b/app/views/templates/invoices/v3/charge.slim index a6f729c5f2e..d1d25b817bd 100644 --- a/app/views/templates/invoices/v3/charge.slim +++ b/app/views/templates/invoices/v3/charge.slim @@ -473,6 +473,9 @@ html td.body-1 = advance_charges? ? I18n.t('invoice.already_paid') : I18n.t('invoice.total_due') td.body-1 = MoneyHelper.format(total_amount) + + - if applied_invoice_custom_sections.present? + == SlimHelper.render('templates/invoices/v3/_custom_sections', self) p.body-3.mb-24 = LineBreakHelper.break_lines(organization.invoice_footer) .powered-by diff --git a/app/views/templates/invoices/v3/one_off.slim b/app/views/templates/invoices/v3/one_off.slim index c6ee18861ee..ea687005b4f 100644 --- a/app/views/templates/invoices/v3/one_off.slim +++ b/app/views/templates/invoices/v3/one_off.slim @@ -457,6 +457,9 @@ html td.body-1 = MoneyHelper.format(total_amount) + + - if applied_invoice_custom_sections.present? + == SlimHelper.render('templates/invoices/v3/_custom_sections', self) p.body-3.mb-24 = LineBreakHelper.break_lines(organization.invoice_footer) .powered-by diff --git a/app/views/templates/invoices/v4.slim b/app/views/templates/invoices/v4.slim index 4af9c015237..e71c74ae156 100644 --- a/app/views/templates/invoices/v4.slim +++ b/app/views/templates/invoices/v4.slim @@ -144,6 +144,9 @@ html /* ----------------------- variable ----------------------- */ + :root { + --border-color: #D9DEE7; + } @font-face { font-family: 'Inter var'; @@ -245,7 +248,7 @@ html color: #66758F; } .invoice-resume-table tr { - border-bottom: 1px solid #D9DEE7; + border-bottom: 1px solid var(--border-color); } .invoice-resume-table tr td { padding-top: 8px; @@ -284,12 +287,12 @@ html width: 50%; } .invoice-resume .total-table tr:not(:last-child) td:nth-child(2) { - border-bottom: 1px solid #D9DEE7; + border-bottom: 1px solid var(--border-color); text-align: left; width: 35%; } .invoice-resume .total-table tr:not(:last-child) td:nth-child(3) { - border-bottom: 1px solid #D9DEE7; + border-bottom: 1px solid var(--border-color); text-align: right; width: 15%; } @@ -320,10 +323,10 @@ html text-align: right; } .breakdown-details-table tr td { - border-bottom: 1px solid #d9dee7; + border-bottom: 1px solid var(--border-color); } .breakdown-details-table tr:first-child td { - border-top: 1px solid #d9dee7; + border-top: 1px solid var(--border-color); } .powered-by { @@ -365,7 +368,7 @@ html } .invoice-resume-table tr.details.subtotal td { padding-bottom: 8px; - border-bottom: 1px solid #d9dee7; + border-bottom: 1px solid var(--border-color); color: #19212e; } @@ -463,6 +466,9 @@ html - applied_usage_threshold = applied_usage_thresholds.order(created_at: :asc).last = I18n.t('invoice.reached_usage_threshold', usage_amount: MoneyHelper.format(applied_usage_threshold.lifetime_usage_amount), threshold_amount: MoneyHelper.format(applied_usage_threshold.passed_threshold_amount)) + - if applied_invoice_custom_sections.present? + == SlimHelper.render('templates/invoices/v4/_custom_sections', self) + p.body-3.mb-24 = LineBreakHelper.break_lines(organization.invoice_footer) .powered-by diff --git a/app/views/templates/invoices/v4/_custom_sections.slim b/app/views/templates/invoices/v4/_custom_sections.slim new file mode 100644 index 00000000000..19381502b72 --- /dev/null +++ b/app/views/templates/invoices/v4/_custom_sections.slim @@ -0,0 +1,15 @@ +css: + .invoice-custom-section { + margin-top: 24px; + border-bottom: 1px solid #D9DEE7; + } + + .invoice-custom-section p.section-name { + margin-bottom: 8px; + } + +.invoice-custom-sections.body-3.mb-24 + - applied_invoice_custom_sections.each do |section| + .invoice-custom-section + p.body-1.section-name = section.display_name + p.body-3.mb-24 = LineBreakHelper.break_lines(section.details) diff --git a/app/views/templates/invoices/v4/charge.slim b/app/views/templates/invoices/v4/charge.slim index a3f5aeed39d..1ba78e6f734 100644 --- a/app/views/templates/invoices/v4/charge.slim +++ b/app/views/templates/invoices/v4/charge.slim @@ -560,6 +560,9 @@ html == SlimHelper.render('templates/invoices/v4/_eu_tax_management', self) + - if applied_invoice_custom_sections.present? + == SlimHelper.render('templates/invoices/v4/_custom_sections', self) + p.body-3.mb-24 = LineBreakHelper.break_lines(organization.invoice_footer) .powered-by diff --git a/app/views/templates/invoices/v4/one_off.slim b/app/views/templates/invoices/v4/one_off.slim index 287c66f89db..dd63a93aae8 100644 --- a/app/views/templates/invoices/v4/one_off.slim +++ b/app/views/templates/invoices/v4/one_off.slim @@ -465,6 +465,9 @@ html == SlimHelper.render('templates/invoices/v4/_eu_tax_management', self) + - if applied_invoice_custom_sections.present? + == SlimHelper.render('templates/invoices/v4/_custom_sections', self) + p.body-3.mb-24 = LineBreakHelper.break_lines(organization.invoice_footer) .powered-by span.body-2 diff --git a/spec/graphql/resolvers/invoice_custom_sections_resolver_spec.rb b/spec/graphql/resolvers/invoice_custom_sections_resolver_spec.rb index ee30a65aabd..4191f0a7525 100644 --- a/spec/graphql/resolvers/invoice_custom_sections_resolver_spec.rb +++ b/spec/graphql/resolvers/invoice_custom_sections_resolver_spec.rb @@ -31,13 +31,14 @@ before do organization.selected_invoice_custom_sections.concat(invoice_custom_sections[2..4]) + customer.selected_invoice_custom_sections.concat(invoice_custom_sections[0..1]) end it_behaves_like 'requires current user' it_behaves_like 'requires current organization' it_behaves_like 'requires permission', 'invoice_custom_sections:view' - it 'returns a list of sorted invoice_custom_sections: alphabetical, selected first' do + it 'returns a list of sorted invoice_custom_sections: alphabetical, selected first without duplicates' do result = execute_graphql( current_user: membership.user, current_organization: organization, diff --git a/spec/models/customer_spec.rb b/spec/models/customer_spec.rb index b22169c920f..aea65daeb0d 100644 --- a/spec/models/customer_spec.rb +++ b/spec/models/customer_spec.rb @@ -433,8 +433,8 @@ context 'when customer does not have any selected_invoice_custom_sections but organization has' do before { organization.selected_invoice_custom_sections << organization_section } - it 'returns the organization\'s invoice_custom_sections' do - expect(customer.applicable_invoice_custom_sections).to eq([organization_section]) + it "returns the organization's invoice_custom_sections" do + expect(customer.applicable_invoice_custom_sections).to match_array([organization_section]) end end diff --git a/spec/requests/api/v1/customers_controller_spec.rb b/spec/requests/api/v1/customers_controller_spec.rb index e186c2752ae..1536156af22 100644 --- a/spec/requests/api/v1/customers_controller_spec.rb +++ b/spec/requests/api/v1/customers_controller_spec.rb @@ -266,7 +266,7 @@ sections = json[:customer][:applicable_invoice_custom_sections] expect(sections).to be_present expect(sections.length).to eq(2) - expect(sections.map { |sec| sec[:code] }).to eq(invoice_custom_sections.map(&:code)) + expect(sections.map { |sec| sec[:code] }).to match_array(invoice_custom_section_codes) end end diff --git a/spec/requests/api/v1/invoices_controller_spec.rb b/spec/requests/api/v1/invoices_controller_spec.rb index e781277fc37..a67dbce749c 100644 --- a/spec/requests/api/v1/invoices_controller_spec.rb +++ b/spec/requests/api/v1/invoices_controller_spec.rb @@ -177,7 +177,8 @@ customer: Hash, subscriptions: [], credits: [], - applied_taxes: [] + applied_taxes: [], + applied_invoice_custom_sections: [] ) expect(json[:invoice][:fees].first).to include(lago_charge_filter_id: charge_filter.id) end diff --git a/spec/serializers/v1/invoice_custom_section_serializer_spec.rb b/spec/serializers/v1/invoice_custom_section_serializer_spec.rb index 8e7456110a4..14b7a298c03 100644 --- a/spec/serializers/v1/invoice_custom_section_serializer_spec.rb +++ b/spec/serializers/v1/invoice_custom_section_serializer_spec.rb @@ -18,10 +18,8 @@ 'description' => invoice_custom_section.description, 'details' => invoice_custom_section.details, 'display_name' => invoice_custom_section.display_name, - 'selected_for_organization' => invoice_custom_section.selected_for_organization?, - 'organization' => { - 'lago_id' => invoice_custom_section.organization_id - } + 'applied_to_organization' => invoice_custom_section.selected_for_organization?, + 'organization_id' => invoice_custom_section.organization_id ) end end diff --git a/spec/serializers/v1/invoices/applied_invoice_custom_section_serializer_spec.rb b/spec/serializers/v1/invoices/applied_invoice_custom_section_serializer_spec.rb new file mode 100644 index 00000000000..f09e914b9f1 --- /dev/null +++ b/spec/serializers/v1/invoices/applied_invoice_custom_section_serializer_spec.rb @@ -0,0 +1,32 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe V1::Invoices::AppliedInvoiceCustomSectionSerializer, type: :serializer do + subject(:serializer) { described_class.new(applied_invoice_custom_section) } + + let(:invoice) { create(:invoice) } + let(:applied_invoice_custom_section) do + create(:applied_invoice_custom_section, + invoice:, + code: 'custom_code', + details: 'custom_details', + display_name: 'Custom Display Name', + created_at: Time.current) + end + + describe '#serialize' do + it 'serializes the applied invoice custom section correctly' do + serialized_data = serializer.serialize + + expect(serialized_data).to include( + lago_id: applied_invoice_custom_section.id, + lago_invoice_id: applied_invoice_custom_section.invoice_id, + code: 'custom_code', + details: 'custom_details', + display_name: 'Custom Display Name', + created_at: applied_invoice_custom_section.created_at.iso8601 + ) + end + end +end diff --git a/spec/services/invoices/refresh_draft_service_spec.rb b/spec/services/invoices/refresh_draft_service_spec.rb index 7cca769a720..b41e2837f72 100644 --- a/spec/services/invoices/refresh_draft_service_spec.rb +++ b/spec/services/invoices/refresh_draft_service_spec.rb @@ -218,7 +218,7 @@ it 'creates new applied_invoice_custom_sections' do expect { refresh_service.call }.to change { invoice.reload.applied_invoice_custom_sections.count }.from(2).to(3) - expect(invoice.applied_invoice_custom_sections.map(&:code)).to match(customer.selected_invoice_custom_sections.map(&:code)) + expect(invoice.applied_invoice_custom_sections.map(&:code)).to match_array(customer.selected_invoice_custom_sections.map(&:code)) end end