diff --git a/app/controllers/api/v1/customers_controller.rb b/app/controllers/api/v1/customers_controller.rb index fd82a64ed16..cd1b87907b4 100644 --- a/app/controllers/api/v1/customers_controller.rb +++ b/app/controllers/api/v1/customers_controller.rb @@ -111,6 +111,7 @@ def create_params :timezone, :net_payment_term, :external_salesforce_id, + :finalize_zero_amount_invoice, integration_customers: [ :id, :external_customer_id, diff --git a/app/controllers/api/v1/organizations_controller.rb b/app/controllers/api/v1/organizations_controller.rb index ba957c62b30..d49e2cd5634 100644 --- a/app/controllers/api/v1/organizations_controller.rb +++ b/app/controllers/api/v1/organizations_controller.rb @@ -65,6 +65,7 @@ def input_params :webhook_url, :document_numbering, :document_number_prefix, + :finalize_zero_amount_invoice, email_settings: [], billing_configuration: [ :invoice_footer, diff --git a/app/models/customer.rb b/app/models/customer.rb index 62a00de716e..5a062b09264 100644 --- a/app/models/customer.rb +++ b/app/models/customer.rb @@ -9,6 +9,15 @@ class Customer < ApplicationRecord include Discard::Model self.discard_column = :deleted_at + FINALIZE_ZERO_AMOUNT_INVOICE_OPTIONS = [ + :inherit, + :skip, + :finalize + ].freeze + + attribute :finalize_zero_amount_invoice, :integer # rails 7.1 check the field exists when defining enum and when running the migration first time is not there + enum finalize_zero_amount_invoice: FINALIZE_ZERO_AMOUNT_INVOICE_OPTIONS, _prefix: :finalize_zero_amount_invoice + before_save :ensure_slug belongs_to :organization @@ -164,43 +173,44 @@ def ensure_slug # # Table name: customers # -# id :uuid not null, primary key -# address_line1 :string -# address_line2 :string -# city :string -# country :string -# currency :string -# deleted_at :datetime -# document_locale :string -# email :string -# invoice_grace_period :integer -# legal_name :string -# legal_number :string -# logo_url :string -# name :string -# net_payment_term :integer -# payment_provider :string -# payment_provider_code :string -# phone :string -# shipping_address_line1 :string -# shipping_address_line2 :string -# shipping_city :string -# shipping_country :string -# shipping_state :string -# shipping_zipcode :string -# slug :string -# state :string -# tax_identification_number :string -# timezone :string -# url :string -# vat_rate :float -# zipcode :string -# created_at :datetime not null -# updated_at :datetime not null -# external_id :string not null -# external_salesforce_id :string -# organization_id :uuid not null -# sequential_id :bigint +# id :uuid not null, primary key +# address_line1 :string +# address_line2 :string +# city :string +# country :string +# currency :string +# deleted_at :datetime +# document_locale :string +# email :string +# finalize_zero_amount_invoice :integer default(0), not null +# invoice_grace_period :integer +# legal_name :string +# legal_number :string +# logo_url :string +# name :string +# net_payment_term :integer +# payment_provider :string +# payment_provider_code :string +# phone :string +# shipping_address_line1 :string +# shipping_address_line2 :string +# shipping_city :string +# shipping_country :string +# shipping_state :string +# shipping_zipcode :string +# slug :string +# state :string +# tax_identification_number :string +# timezone :string +# url :string +# vat_rate :float +# zipcode :string +# created_at :datetime not null +# updated_at :datetime not null +# external_id :string not null +# external_salesforce_id :string +# organization_id :uuid not null +# sequential_id :bigint # # Indexes # diff --git a/app/models/invoice.rb b/app/models/invoice.rb index ca252ca0bf1..bd1c890aa3a 100644 --- a/app/models/invoice.rb +++ b/app/models/invoice.rb @@ -63,7 +63,7 @@ class Invoice < ApplicationRecord PAYMENT_STATUS = %i[pending succeeded failed].freeze VISIBLE_STATUS = {draft: 0, finalized: 1, voided: 2, failed: 4}.freeze - INVISIBLE_STATUS = {generating: 3, open: 5}.freeze + INVISIBLE_STATUS = {generating: 3, open: 5, closed: 6}.freeze STATUS = VISIBLE_STATUS.merge(INVISIBLE_STATUS).freeze enum invoice_type: INVOICE_TYPES @@ -77,6 +77,7 @@ class Invoice < ApplicationRecord state :finalized state :voided state :failed + state :closed event :finalize do transitions from: :draft, to: :finalized diff --git a/app/models/organization.rb b/app/models/organization.rb index 5dbb925db9c..4c15fe0d728 100644 --- a/app/models/organization.rb +++ b/app/models/organization.rb @@ -70,6 +70,7 @@ class Organization < ApplicationRecord validates :name, presence: true validates :timezone, timezone: true validates :webhook_url, url: true, allow_nil: true + validates :finalize_zero_amount_invoice, inclusion: {in: [true, false]} validate :validate_email_settings @@ -137,37 +138,38 @@ def validate_email_settings # # Table name: organizations # -# id :uuid not null, primary key -# address_line1 :string -# address_line2 :string -# api_key :string -# city :string -# clickhouse_aggregation :boolean default(FALSE), not null -# country :string -# custom_aggregation :boolean default(FALSE) -# default_currency :string default("USD"), not null -# document_locale :string default("en"), not null -# document_number_prefix :string -# document_numbering :integer default("per_customer"), not null -# email :string -# email_settings :string default([]), not null, is an Array -# eu_tax_management :boolean default(FALSE) -# invoice_footer :text -# invoice_grace_period :integer default(0), not null -# legal_name :string -# legal_number :string -# logo :string -# name :string not null -# net_payment_term :integer default(0), not null -# premium_integrations :string default([]), not null, is an Array -# state :string -# tax_identification_number :string -# timezone :string default("UTC"), not null -# vat_rate :float default(0.0), not null -# webhook_url :string -# zipcode :string -# created_at :datetime not null -# updated_at :datetime not null +# id :uuid not null, primary key +# address_line1 :string +# address_line2 :string +# api_key :string +# city :string +# clickhouse_aggregation :boolean default(FALSE), not null +# country :string +# custom_aggregation :boolean default(FALSE) +# default_currency :string default("USD"), not null +# document_locale :string default("en"), not null +# document_number_prefix :string +# document_numbering :integer default("per_customer"), not null +# email :string +# email_settings :string default([]), not null, is an Array +# eu_tax_management :boolean default(FALSE) +# finalize_zero_amount_invoice :boolean default(FALSE), not null +# invoice_footer :text +# invoice_grace_period :integer default(0), not null +# legal_name :string +# legal_number :string +# logo :string +# name :string not null +# net_payment_term :integer default(0), not null +# premium_integrations :string default([]), not null, is an Array +# state :string +# tax_identification_number :string +# timezone :string default("UTC"), not null +# vat_rate :float default(0.0), not null +# webhook_url :string +# zipcode :string +# created_at :datetime not null +# updated_at :datetime not null # # Indexes # diff --git a/app/serializers/v1/customer_serializer.rb b/app/serializers/v1/customer_serializer.rb index c823fd0f5d6..9fea8081c7e 100644 --- a/app/serializers/v1/customer_serializer.rb +++ b/app/serializers/v1/customer_serializer.rb @@ -29,6 +29,7 @@ def serialize applicable_timezone: model.applicable_timezone, net_payment_term: model.net_payment_term, external_salesforce_id: model.external_salesforce_id, + finalize_zero_amount_invoice: model.finalize_zero_amount_invoice, billing_configuration:, shipping_address: model.shipping_address } diff --git a/app/serializers/v1/organization_serializer.rb b/app/serializers/v1/organization_serializer.rb index 40cd993db69..6698c19838d 100644 --- a/app/serializers/v1/organization_serializer.rb +++ b/app/serializers/v1/organization_serializer.rb @@ -25,6 +25,7 @@ def serialize document_numbering: model.document_numbering, document_number_prefix: model.document_number_prefix, tax_identification_number: model.tax_identification_number, + finalize_zero_amount_invoice: model.finalize_zero_amount_invoice, billing_configuration: } diff --git a/app/services/customers/create_service.rb b/app/services/customers/create_service.rb index b6dad22ed1b..39dd9725943 100644 --- a/app/services/customers/create_service.rb +++ b/app/services/customers/create_service.rb @@ -16,6 +16,13 @@ def create_from_api(organization:, params:) ) end + unless valid_finalize_zero_amount_invoice?(params[:finalize_zero_amount_invoice]) + return result.single_validation_failure!( + field: :finalize_zero_amount_invoice, + error_code: 'invalid_value' + ) + end + unless valid_integration_customers_count?(integration_customers: params[:integration_customers]) return result.single_validation_failure!( field: :integration_customers, @@ -45,6 +52,7 @@ def create_from_api(organization:, params:) customer.legal_number = params[:legal_number] if params.key?(:legal_number) customer.net_payment_term = params[:net_payment_term] if params.key?(:net_payment_term) customer.external_salesforce_id = params[:external_salesforce_id] if params.key?(:external_salesforce_id) + customer.finalize_zero_amount_invoice = params[:finalize_zero_amount_invoice] if params.key?(:finalize_zero_amount_invoice) if params.key?(:tax_identification_number) customer.tax_identification_number = params[:tax_identification_number] end @@ -144,6 +152,10 @@ def create(**args) tax_identification_number: args[:tax_identification_number] ) + if args.key?(:finalize_zero_amount_invoice) + customer.finalize_zero_amount_invoice = args[:finalize_zero_amount_invoice] + end + assign_premium_attributes(customer, args) ActiveRecord::Base.transaction do @@ -187,11 +199,9 @@ def create(**args) private - def valid_email?(email) - return true if email.nil? - - email_regexp = /\A[^@\s]+@[^@\s]+\z/ - email_regexp.match?(email).present? + def valid_finalize_zero_amount_invoice?(value) + return true if value.nil? + Customer::FINALIZE_ZERO_AMOUNT_INVOICE_OPTIONS.include?(value.to_sym) end def valid_metadata_count?(metadata:) diff --git a/app/services/invoices/advance_charges_service.rb b/app/services/invoices/advance_charges_service.rb index ddadefb527c..8efbcbf11ab 100644 --- a/app/services/invoices/advance_charges_service.rb +++ b/app/services/invoices/advance_charges_service.rb @@ -65,7 +65,7 @@ def create_group_invoice Invoices::ComputeAmountsFromFees.call(invoice:) invoice.payment_status = :succeeded - invoice.status = :finalized + Invoices::TransitionToFinalStatusService.call(invoice:) invoice.save! end diff --git a/app/services/invoices/create_one_off_service.rb b/app/services/invoices/create_one_off_service.rb index 84d2f113659..a80d0390df4 100644 --- a/app/services/invoices/create_one_off_service.rb +++ b/app/services/invoices/create_one_off_service.rb @@ -35,8 +35,8 @@ def call Invoices::ComputeAmountsFromFees.call(invoice:, provider_taxes: result.fees_taxes) invoice.payment_status = invoice.total_amount_cents.positive? ? :pending : :succeeded - - invoice.finalized! + Invoices::TransitionToFinalStatusService.call(invoice:) + invoice.save! end Utils::SegmentTrack.invoice_created(invoice) 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 9ec09c91fa8..e976a965f1d 100644 --- a/app/services/invoices/create_pay_in_advance_charge_service.rb +++ b/app/services/invoices/create_pay_in_advance_charge_service.rb @@ -43,7 +43,8 @@ def call create_applied_prepaid_credit if should_create_applied_prepaid_credit? invoice.payment_status = invoice.total_amount_cents.positive? ? :pending : :succeeded - invoice.finalized! + Invoices::TransitionToFinalStatusService.call(invoice:) + invoice.save! end Utils::SegmentTrack.invoice_created(invoice) diff --git a/app/services/invoices/refresh_draft_and_finalize_service.rb b/app/services/invoices/refresh_draft_and_finalize_service.rb index 6a8fc3095c9..8c665c3bab6 100644 --- a/app/services/invoices/refresh_draft_and_finalize_service.rb +++ b/app/services/invoices/refresh_draft_and_finalize_service.rb @@ -22,7 +22,7 @@ def call refresh_result.raise_if_error! invoice.payment_due_date = payment_due_date - invoice.status = :finalized + Invoices::TransitionToFinalStatusService.call(invoice:) invoice.save! invoice.credit_notes.each(&:finalized!) diff --git a/app/services/invoices/subscription_service.rb b/app/services/invoices/subscription_service.rb index 438affb7365..947fc82d19e 100644 --- a/app/services/invoices/subscription_service.rb +++ b/app/services/invoices/subscription_service.rb @@ -28,6 +28,11 @@ def call fee_result = ActiveRecord::Base.transaction do invoice.status = invoice_status + if invoice_status == :finalized + Invoices::TransitionToFinalStatusService.call(invoice:) + else + invoice.status = :draft + end fee_result = Invoices::CalculateFeesService.call( invoice:, diff --git a/app/services/invoices/transition_to_final_status_service.rb b/app/services/invoices/transition_to_final_status_service.rb new file mode 100644 index 00000000000..2e798e1c7b6 --- /dev/null +++ b/app/services/invoices/transition_to_final_status_service.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true + +module Invoices + class TransitionToFinalStatusService < BaseService + def initialize(invoice:) + @invoice = invoice + @customer = @invoice.customer + @organization = @customer.organization + super + end + + def call + invoice.status = if should_finalize_invoice? + :finalized + else + :closed + end + result.invoice = invoice + result + end + + private + + attr_reader :invoice, :customer, :organization + + def should_finalize_invoice? + return true unless invoice.fees_amount_cents.zero? + customer_setting = customer.finalize_zero_amount_invoice + if customer_setting == 'inherit' + organization.finalize_zero_amount_invoice + else + customer_setting == 'finalize' + end + end + end +end diff --git a/app/services/organizations/update_service.rb b/app/services/organizations/update_service.rb index 17699ba77de..c262c990c3b 100644 --- a/app/services/organizations/update_service.rb +++ b/app/services/organizations/update_service.rb @@ -26,6 +26,7 @@ def call organization.net_payment_term = params[:net_payment_term] if params.key?(:net_payment_term) organization.document_numbering = params[:document_numbering] if params.key?(:document_numbering) organization.document_number_prefix = params[:document_number_prefix] if params.key?(:document_number_prefix) + organization.finalize_zero_amount_invoice = params[:finalize_zero_amount_invoice] if params.key?(:finalize_zero_amount_invoice) billing = params[:billing_configuration]&.to_h || {} organization.invoice_footer = billing[:invoice_footer] if billing.key?(:invoice_footer) diff --git a/db/migrate/20240821172352_add_finalize_zero_amount_invoice_to_organizations.rb b/db/migrate/20240821172352_add_finalize_zero_amount_invoice_to_organizations.rb new file mode 100644 index 00000000000..1ece8cf5596 --- /dev/null +++ b/db/migrate/20240821172352_add_finalize_zero_amount_invoice_to_organizations.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +class AddFinalizeZeroAmountInvoiceToOrganizations < ActiveRecord::Migration[7.1] + def change + add_column :organizations, :finalize_zero_amount_invoice, :boolean, default: true, null: false + end +end diff --git a/db/migrate/20240821174724_add_finalize_zero_amount_invoice_to_customers.rb b/db/migrate/20240821174724_add_finalize_zero_amount_invoice_to_customers.rb new file mode 100644 index 00000000000..25172195fe1 --- /dev/null +++ b/db/migrate/20240821174724_add_finalize_zero_amount_invoice_to_customers.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +class AddFinalizeZeroAmountInvoiceToCustomers < ActiveRecord::Migration[7.1] + def change + add_column :customers, :finalize_zero_amount_invoice, :integer, default: 0, null: false + end +end diff --git a/db/schema.rb b/db/schema.rb index 33b5180436d..62d848fff5c 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -438,6 +438,7 @@ t.string "shipping_zipcode" t.string "shipping_state" t.string "shipping_country" + t.integer "finalize_zero_amount_invoice", default: 0, null: false t.index ["deleted_at"], name: "index_customers_on_deleted_at" t.index ["external_id", "organization_id"], name: "index_customers_on_external_id_and_organization_id", unique: true, where: "(deleted_at IS NULL)" t.index ["organization_id"], name: "index_customers_on_organization_id" @@ -866,6 +867,7 @@ t.boolean "clickhouse_aggregation", default: false, null: false t.string "premium_integrations", default: [], null: false, array: true t.boolean "custom_aggregation", default: false + t.boolean "finalize_zero_amount_invoice", default: true, null: false t.index ["api_key"], name: "index_organizations_on_api_key", unique: true t.check_constraint "invoice_grace_period >= 0", name: "check_organizations_on_invoice_grace_period" t.check_constraint "net_payment_term >= 0", name: "check_organizations_on_net_payment_term" diff --git a/spec/models/customer_spec.rb b/spec/models/customer_spec.rb index 0d9d4377a35..a15695fffc7 100644 --- a/spec/models/customer_spec.rb +++ b/spec/models/customer_spec.rb @@ -16,6 +16,10 @@ it { is_expected.to have_one(:anrok_customer) } it { is_expected.to have_one(:xero_customer) } + it 'sets the default value to inherit' do + expect(customer.finalize_zero_amount_invoice).to eq "inherit" + end + describe 'validations' do subject(:customer) do described_class.new(organization:, external_id:) diff --git a/spec/models/invoice_spec.rb b/spec/models/invoice_spec.rb index e3afa65b3b2..f892910380c 100644 --- a/spec/models/invoice_spec.rb +++ b/spec/models/invoice_spec.rb @@ -23,8 +23,8 @@ it 'has fixed status mapping' do expect(described_class::VISIBLE_STATUS).to match(draft: 0, finalized: 1, voided: 2, failed: 4) - expect(described_class::INVISIBLE_STATUS).to match(generating: 3, open: 5) - expect(described_class::STATUS).to match(draft: 0, finalized: 1, voided: 2, generating: 3, failed: 4, open: 5) + expect(described_class::INVISIBLE_STATUS).to match(generating: 3, open: 5, closed: 6) + expect(described_class::STATUS).to match(draft: 0, finalized: 1, voided: 2, generating: 3, failed: 4, open: 5, closed: 6) end describe 'validation' do diff --git a/spec/models/organization_spec.rb b/spec/models/organization_spec.rb index 360d913a80e..a9396545f16 100644 --- a/spec/models/organization_spec.rb +++ b/spec/models/organization_spec.rb @@ -24,6 +24,10 @@ it { is_expected.to validate_inclusion_of(:default_currency).in_array(described_class.currency_list) } + it 'sets the default value to true' do + expect(organization.finalize_zero_amount_invoice).to eq true + end + it_behaves_like 'paper_trail traceable' describe 'Validations' do @@ -122,6 +126,13 @@ expect(organization.errors.first.attribute).to eq(:email_settings) expect(organization.errors.first.type).to eq(:unsupported_value) end + + it 'dont allow finalize_zero_amount_invoice with null value' do + expect(organization.finalize_zero_amount_invoice).to eq true + organization.finalize_zero_amount_invoice = nil + + expect(organization).not_to be_valid + end end describe 'Callbacks' do diff --git a/spec/requests/api/v1/customers_controller_spec.rb b/spec/requests/api/v1/customers_controller_spec.rb index bb2c7fe521d..59404368bfc 100644 --- a/spec/requests/api/v1/customers_controller_spec.rb +++ b/spec/requests/api/v1/customers_controller_spec.rb @@ -53,6 +53,21 @@ end end + context 'with finalize_zero_amount_invoice' do + let(:create_params) do + { + external_id: SecureRandom.uuid, + finalize_zero_amount_invoice: "skip" + } + end + + it 'returns a success' do + post_with_token(organization, '/api/v1/customers', {customer: create_params}) + expect(response).to have_http_status(:success) + expect(json[:customer][:finalize_zero_amount_invoice]).to eq("skip") + end + end + context 'with billing configuration' do around { |test| lago_premium!(&test) } diff --git a/spec/requests/api/v1/organizations_controller_spec.rb b/spec/requests/api/v1/organizations_controller_spec.rb index 34468d23335..7fbcec4ebad 100644 --- a/spec/requests/api/v1/organizations_controller_spec.rb +++ b/spec/requests/api/v1/organizations_controller_spec.rb @@ -23,6 +23,7 @@ webhook_url:, email_settings: ['invoice.finalized'], document_number_prefix: 'ORG-2', + finalize_zero_amount_invoice: false, billing_configuration: { invoice_footer: 'footer', invoice_grace_period: 3, @@ -47,6 +48,7 @@ expect(json[:organization][:webhook_urls]).to eq([webhook_url]) expect(json[:organization][:document_numbering]).to eq('per_customer') expect(json[:organization][:document_number_prefix]).to eq('ORG-2') + expect(json[:organization][:finalize_zero_amount_invoice]).to eq(false) # TODO(:timezone): Timezone update is turned off for now # expect(json[:organization][:timezone]).to eq(update_params[:timezone]) diff --git a/spec/serializers/v1/customer_serializer_spec.rb b/spec/serializers/v1/customer_serializer_spec.rb index a81882a1321..114212af0e8 100644 --- a/spec/serializers/v1/customer_serializer_spec.rb +++ b/spec/serializers/v1/customer_serializer_spec.rb @@ -45,6 +45,7 @@ expect(result['customer']['timezone']).to eq(customer.timezone) expect(result['customer']['applicable_timezone']).to eq(customer.applicable_timezone) expect(result['customer']['net_payment_term']).to eq(customer.net_payment_term) + expect(result['customer']['finalize_zero_amount_invoice']).to eq(customer.finalize_zero_amount_invoice) expect(result['customer']['billing_configuration']['payment_provider']).to eq(customer.payment_provider) expect(result['customer']['billing_configuration']['payment_provider_code']).to eq(customer.payment_provider_code) expect(result['customer']['billing_configuration']['invoice_grace_period']).to eq(customer.invoice_grace_period) diff --git a/spec/serializers/v1/organization_serializer_spec.rb b/spec/serializers/v1/organization_serializer_spec.rb index 588a891e8a1..2b7bf2f5799 100644 --- a/spec/serializers/v1/organization_serializer_spec.rb +++ b/spec/serializers/v1/organization_serializer_spec.rb @@ -37,7 +37,7 @@ expect(result['organization']['tax_identification_number']).to eq(org.tax_identification_number) expect(result['organization']['timezone']).to eq(org.timezone) expect(result['organization']['net_payment_term']).to eq(org.net_payment_term) - + expect(result['organization']['finalize_zero_amount_invoice']).to eq(org.finalize_zero_amount_invoice) expect(result['organization']['taxes'].count).to eq(1) end end diff --git a/spec/services/customers/create_service_spec.rb b/spec/services/customers/create_service_spec.rb index a5128106445..c19888392c9 100644 --- a/spec/services/customers/create_service_spec.rb +++ b/spec/services/customers/create_service_spec.rb @@ -195,6 +195,51 @@ end end + context 'with finalize_zero_amount_invoice' do + let(:create_args) do + { + external_id:, + finalize_zero_amount_invoice: "skip" + } + end + + it 'creates customer with finalize_zero_amount_invoice' do + result = customers_service.create_from_api( + organization:, + params: create_args + ) + + aggregate_failures do + expect(result).to be_success + + customer = result.customer + expect(customer.finalize_zero_amount_invoice).to eq("skip") + end + end + + context 'with incorrect value of finalize_zero_amount_invoice' do + let(:create_args) do + { + external_id:, + finalize_zero_amount_invoice: "bad value" + } + end + + it 'creates customer with finalize_zero_amount_invoice' do + result = customers_service.create_from_api( + organization:, + params: create_args + ) + + aggregate_failures do + expect(result.error).to be_a(BaseService::ValidationFailure) + expect(result.error.messages.keys).to include(:finalize_zero_amount_invoice) + expect(result.error.messages[:finalize_zero_amount_invoice]).to include('invalid_value') + end + end + end + end + context 'with premium features' do around { |test| lago_premium!(&test) } diff --git a/spec/services/invoices/advance_charges_service_spec.rb b/spec/services/invoices/advance_charges_service_spec.rb index b683e4d7673..e507560ef6e 100644 --- a/spec/services/invoices/advance_charges_service_spec.rb +++ b/spec/services/invoices/advance_charges_service_spec.rb @@ -43,6 +43,7 @@ def fee_boundaries before do allow(Invoices::Payments::CreateService).to receive(:call) + allow(Invoices::TransitionToFinalStatusService).to receive(:call).and_call_original end context 'with existing standalone fees' do @@ -83,6 +84,7 @@ def fee_boundaries expect(Invoices::GeneratePdfAndNotifyJob).to have_been_enqueued.with(invoice: result.invoice, email: false) expect(SendWebhookJob).to have_been_enqueued.with('invoice.created', result.invoice) expect(SegmentTrackJob).to have_been_enqueued.once + expect(Invoices::TransitionToFinalStatusService).to have_received(:call).with(invoice: result.invoice) end end end diff --git a/spec/services/invoices/create_one_off_service_spec.rb b/spec/services/invoices/create_one_off_service_spec.rb index 5531e06828a..9d00f229470 100644 --- a/spec/services/invoices/create_one_off_service_spec.rb +++ b/spec/services/invoices/create_one_off_service_spec.rb @@ -33,6 +33,7 @@ tax allow(SegmentTrackJob).to receive(:perform_later) + allow(Invoices::TransitionToFinalStatusService).to receive(:call).and_call_original CurrentContext.source = 'api' end @@ -58,6 +59,7 @@ expect(result.invoice.total_amount_cents).to eq(3360) expect(result.invoice).to be_finalized + expect(Invoices::TransitionToFinalStatusService).to have_received(:call).with(invoice: result.invoice) end end 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 7d1f888b480..c593dc9a3ba 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 @@ -61,6 +61,7 @@ .and_return(charge_result) allow(SegmentTrackJob).to receive(:perform_later) + allow(Invoices::TransitionToFinalStatusService).to receive(:call).and_call_original end it 'creates an invoice' do @@ -106,6 +107,7 @@ expect(result.invoice.total_amount_cents).to eq(12) + expect(Invoices::TransitionToFinalStatusService).to have_received(:call).with(invoice: result.invoice) expect(result.invoice).to be_finalized 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 eaaaae5e755..4e2d78483ca 100644 --- a/spec/services/invoices/refresh_draft_and_finalize_service_spec.rb +++ b/spec/services/invoices/refresh_draft_and_finalize_service_spec.rb @@ -42,11 +42,13 @@ allow(SegmentTrackJob).to receive(:perform_later) allow(Invoices::Payments::StripeCreateJob).to receive(:perform_later).and_call_original allow(Invoices::Payments::GocardlessCreateJob).to receive(:perform_later).and_call_original + allow(Invoices::TransitionToFinalStatusService).to receive(:call).and_call_original end it 'marks the invoice as finalized' do expect { finalize_service.call } .to change(invoice, :status).from('draft').to('finalized') + expect(Invoices::TransitionToFinalStatusService).to have_received(:call).with(invoice:) end it 'updates the issuing date' do diff --git a/spec/services/invoices/subscription_service_spec.rb b/spec/services/invoices/subscription_service_spec.rb index affdcf2cbe9..3db8549234f 100644 --- a/spec/services/invoices/subscription_service_spec.rb +++ b/spec/services/invoices/subscription_service_spec.rb @@ -46,6 +46,7 @@ allow(SegmentTrackJob).to receive(:perform_later) allow(Invoices::Payments::StripeCreateJob).to receive(:perform_later).and_call_original allow(Invoices::Payments::GocardlessCreateJob).to receive(:perform_later).and_call_original + allow(Invoices::TransitionToFinalStatusService).to receive(:call).and_call_original end it 'calls SegmentTrackJob' do @@ -102,6 +103,7 @@ expect(result.invoice.total_amount_cents).to eq(120) expect(result.invoice.version_number).to eq(4) + expect(Invoices::TransitionToFinalStatusService).to have_received(:call).with(invoice: result.invoice) expect(result.invoice).to be_finalized end end diff --git a/spec/services/invoices/transition_to_final_status_service_spec.rb b/spec/services/invoices/transition_to_final_status_service_spec.rb new file mode 100644 index 00000000000..03ef6f13396 --- /dev/null +++ b/spec/services/invoices/transition_to_final_status_service_spec.rb @@ -0,0 +1,84 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe Invoices::TransitionToFinalStatusService, type: :service do + subject { described_class.new(invoice: invoice) } + + let(:organization) { create(:organization, finalize_zero_amount_invoice: organization_setting) } + let(:customer) { create(:customer, organization:, finalize_zero_amount_invoice: customer_setting) } + let(:organization_setting) { 'true' } # default value + let(:customer_setting) { 'inherit' } # default value + let(:fees_amount_cents) { 100 } + let(:invoice) do + create( + :invoice, + organization:, + currency: 'EUR', + fees_amount_cents:, + issuing_date: Time.zone.now.beginning_of_month, + customer: customer + ) + end + + describe '#call' do + before { subject.call } + + context 'when invoice fees_amount_cents is not zero' do + it 'finalizes the invoice' do + expect(invoice.status).to eq('finalized') + end + + context 'with organization and customer settings defined to not finalize' do + let(:organization_setting) { 'false' } + let(:customer_setting) { 'skip' } + + it 'finalizes the invoice' do + expect(invoice.status).to eq('finalized') + end + end + end + + context 'when invoice fees_amount_cents is zero' do + let(:fees_amount_cents) { 0 } + + context 'with customer setting defined to finalize' do + let(:customer_setting) { 'finalize' } + let(:organization_setting) { 'false' } + + it 'finalizes the invoice' do + expect(invoice.status).to eq('finalized') + end + end + + context 'with customer setting defined to skip' do + let(:customer_setting) { 'skip' } + let(:organization_setting) { 'true' } + + it 'closes the invoice' do + expect(invoice.status).to eq('closed') + end + end + + context 'with customer setting defined to inherit' do + let(:customer_setting) { 'inherit' } + + context 'with organization setting to finalize' do + let(:organization_setting) { 'true' } + + it 'finalizes the invoice' do + expect(invoice.status).to eq('finalized') + end + end + + context 'with organization setting to skip' do + let(:organization_setting) { 'false' } + + it 'closes the invoice' do + expect(invoice.status).to eq('closed') + end + end + end + end + end +end diff --git a/spec/services/organizations/update_service_spec.rb b/spec/services/organizations/update_service_spec.rb index bc5e16dd3ab..b16eba2dd80 100644 --- a/spec/services/organizations/update_service_spec.rb +++ b/spec/services/organizations/update_service_spec.rb @@ -72,6 +72,18 @@ end end + context 'when finalize_zero_amount_invoice is sent' do + before { params[:finalize_zero_amount_invoice] = 'false' } + + it 'converts document_number_prefix to upcase version' do + result = update_service.call + + aggregate_failures do + expect(result.organization.finalize_zero_amount_invoice).to eq(false) + end + end + end + context 'when document_number_prefix is invalid' do before { params[:document_number_prefix] = 'aaaaaaaaaaaaaaa' }