diff --git a/app/graphql/mutations/customers/create.rb b/app/graphql/mutations/customers/create.rb index d4e0a49c722..0975a835be8 100644 --- a/app/graphql/mutations/customers/create.rb +++ b/app/graphql/mutations/customers/create.rb @@ -18,7 +18,7 @@ class Create < BaseMutation def resolve(**args) result = ::Customers::CreateService .new(context[:current_user]) - .create(**args.merge(organization_id: current_organization.id)) + .create(**args.merge(organization_id: current_organization.id, billing_entity_id: current_organization.billing_entities.first.id)) result.success? ? result.customer : result_error(result) end diff --git a/app/models/billing_entity.rb b/app/models/billing_entity.rb index 16e896884f1..0dbb1e16c2e 100644 --- a/app/models/billing_entity.rb +++ b/app/models/billing_entity.rb @@ -36,7 +36,7 @@ class BillingEntity < ApplicationRecord has_one :salesforce_integration, class_name: "Integrations::SalesforceIntegration" # this one needs to be done via applied_dunning_campaign_id - has_one :applied_dunning_campaign + has_one :applied_dunning_campaign, class_name: "DunningCampaign", foreign_key: :applied_dunning_campaign_id has_many :invoice_custom_section_selections has_many :selected_invoice_custom_sections, through: :invoice_custom_section_selections, source: :invoice_custom_section diff --git a/app/models/customer.rb b/app/models/customer.rb index d41687f3287..c8daded9d99 100644 --- a/app/models/customer.rb +++ b/app/models/customer.rb @@ -35,6 +35,7 @@ class Customer < ApplicationRecord before_save :ensure_slug belongs_to :organization + belongs_to :billing_entity belongs_to :applied_dunning_campaign, optional: true, class_name: "DunningCampaign" has_many :subscriptions @@ -142,7 +143,7 @@ def applicable_net_payment_term def applicable_invoice_custom_sections return [] if skip_invoice_custom_sections? - selected_invoice_custom_sections.order(:name).presence || organization.selected_invoice_custom_sections.order(:name) + selected_invoice_custom_sections.order(:name).presence || billing_entity.selected_invoice_custom_sections.order(:name) end def editable? diff --git a/app/models/error_detail.rb b/app/models/error_detail.rb index ec87a980e68..31393572de3 100644 --- a/app/models/error_detail.rb +++ b/app/models/error_detail.rb @@ -7,6 +7,7 @@ class ErrorDetail < ApplicationRecord belongs_to :owner, polymorphic: true belongs_to :organization + belongs_to :billing_entity ERROR_CODES = %w[not_provided tax_error tax_voiding_error] enum :error_code, ERROR_CODES @@ -16,22 +17,24 @@ class ErrorDetail < ApplicationRecord # # Table name: error_details # -# id :uuid not null, primary key -# deleted_at :datetime -# details :jsonb not null -# error_code :integer default("not_provided"), not null -# owner_type :string not null -# created_at :datetime not null -# updated_at :datetime not null -# organization_id :uuid not null -# owner_id :uuid not null +# id :uuid not null, primary key +# deleted_at :datetime +# details :jsonb not null +# error_code :integer default("not_provided"), not null +# owner_type :string not null +# created_at :datetime not null +# updated_at :datetime not null +# billing_entity_id :uuid +# organization_id :uuid not null +# owner_id :uuid not null # # Indexes # -# index_error_details_on_deleted_at (deleted_at) -# index_error_details_on_error_code (error_code) -# index_error_details_on_organization_id (organization_id) -# index_error_details_on_owner (owner_type,owner_id) +# index_error_details_on_billing_entity_id (billing_entity_id) +# index_error_details_on_deleted_at (deleted_at) +# index_error_details_on_error_code (error_code) +# index_error_details_on_organization_id (organization_id) +# index_error_details_on_owner (owner_type,owner_id) # # Foreign Keys # diff --git a/app/models/integrations/base_integration.rb b/app/models/integrations/base_integration.rb index 6b97dec6bb2..436b01dc233 100644 --- a/app/models/integrations/base_integration.rb +++ b/app/models/integrations/base_integration.rb @@ -11,6 +11,7 @@ class BaseIntegration < ApplicationRecord INTEGRATION_ACCOUNTING_TYPES = %w[Integrations::NetsuiteIntegration Integrations::XeroIntegration].freeze belongs_to :organization + belongs_to :billing_entity has_many :integration_items, dependent: :destroy, foreign_key: :integration_id has_many :integration_resources, dependent: :destroy, foreign_key: :integration_id diff --git a/app/models/invoice.rb b/app/models/invoice.rb index c990cb8354c..94ef56c932c 100644 --- a/app/models/invoice.rb +++ b/app/models/invoice.rb @@ -11,11 +11,13 @@ class Invoice < ApplicationRecord CREDIT_NOTES_MIN_VERSION = 2 COUPON_BEFORE_VAT_VERSION = 3 - before_save :ensure_organization_sequential_id, if: -> { organization.per_organization? && !self_billed } + # before_save :ensure_organization_sequential_id, if: -> { organization.per_organization? && !self_billed } + before_save :ensure_billing_entity_sequential_id, if: -> { billing_entity.per_entity? && !self_billed } before_save :ensure_number belongs_to :customer, -> { with_discarded } belongs_to :organization + belongs_to :billing_entity has_many :fees has_many :credits @@ -353,7 +355,8 @@ def document_invoice_name return I18n.t("invoice.self_billed.document_name") if self_billed? return I18n.t('invoice.prepaid_credit_invoice') if credit? - if %w[AU AE ID NZ].include?(organization.country) + # if %w[AU AE ID NZ].include?(organization.country) + if %w[AU AE ID NZ].include?(billing_entity.country) return I18n.t('invoice.paid_tax_invoice') if advance_charges? return I18n.t('invoice.document_tax_name') end @@ -374,62 +377,62 @@ def void_invoice! end def ensure_number - self.number = "#{organization.document_number_prefix}-DRAFT" if number.blank? && !status_changed_to_finalized? + self.number = "#{billing_entity.document_number_prefix}-DRAFT" if number.blank? && !status_changed_to_finalized? return unless status_changed_to_finalized? - if organization.per_customer? || self_billed + if billing_entity.per_customer? || self_billed # NOTE: Example of expected customer slug format is ORG_PREFIX-005 - customer_slug = "#{organization.document_number_prefix}-#{format("%03d", customer.sequential_id)}" + customer_slug = "#{billing_entity.document_number_prefix}-#{format("%03d", customer.sequential_id)}" formatted_sequential_id = format('%03d', sequential_id) self.number = "#{customer_slug}-#{formatted_sequential_id}" else - org_formatted_sequential_id = format('%03d', organization_sequential_id) - formatted_year_and_month = Time.now.in_time_zone(organization.timezone || 'UTC').strftime('%Y%m') + org_formatted_sequential_id = format('%03d', billing_entity_sequential_id) + formatted_year_and_month = Time.now.in_time_zone(billing_entity.timezone || 'UTC').strftime('%Y%m') - self.number = "#{organization.document_number_prefix}-#{formatted_year_and_month}-#{org_formatted_sequential_id}" + self.number = "#{billing_entity.document_number_prefix}-#{formatted_year_and_month}-#{org_formatted_sequential_id}" end end - def ensure_organization_sequential_id - return if organization_sequential_id.present? && organization_sequential_id.positive? + def ensure_billing_entity_sequential_id + return if billing_entity_sequential_id.present? && billing_entity_sequential_id.positive? return unless status_changed_to_finalized? - self.organization_sequential_id = generate_organization_sequential_id + self.billing_entity_sequential_id = generate_billing_entity_sequential_id end - def generate_organization_sequential_id - timezone = organization.timezone || 'UTC' - organization_sequence_scope = organization.invoices.with_generated_number.where( + def generate_billing_entity_sequential_id + timezone = billing_entity.timezone || 'UTC' + billing_entity_sequence_scope = billing_entity.invoices.with_generated_number.where( "date_trunc('month', created_at::timestamptz AT TIME ZONE ?)::date = ?", timezone, Time.now.in_time_zone(timezone).beginning_of_month.to_date ).non_self_billed result = Invoice.with_advisory_lock( - organization_id, + billing_entity_id, transaction: true, timeout_seconds: 10.seconds ) do # If previous invoice had different numbering, base sequential id is the total number of invoices - organization_sequential_id = if switched_from_customer_numbering? - organization.invoices.non_self_billed.with_generated_number.count + billing_entity_sequential_id = if switched_from_customer_numbering? + billing_entity.invoices.non_self_billed.with_generated_number.count else - organization + billing_entity .invoices .non_self_billed - .where.not(organization_sequential_id: 0) - .order(organization_sequential_id: :desc) + .where.not(billing_entity_sequential_id: 0) + .order(billing_entity_sequential_id: :desc) .limit(1) - .pick(:organization_sequential_id) || 0 + .pick(:billing_entity_sequential_id) || 0 end # NOTE: Start with the most recent sequential id and find first available sequential id that haven't occurred loop do - organization_sequential_id += 1 + billing_entity_sequential_id += 1 - break organization_sequential_id unless organization_sequence_scope.exists?(organization_sequential_id:) + break billing_entity_sequential_id unless billing_entity_sequence_scope.exists?(billing_entity_sequential_id:) end end @@ -440,11 +443,11 @@ def generate_organization_sequential_id end def switched_from_customer_numbering? - last_invoice = organization.invoices.non_self_billed.order(created_at: :desc).with_generated_number.first + last_invoice = billing_entity.invoices.non_self_billed.order(created_at: :desc).with_generated_number.first return false unless last_invoice - last_invoice&.organization_sequential_id&.zero? + last_invoice&.billing_entity_sequential_id&.zero? end def status_changed_to_finalized? diff --git a/app/models/organization.rb b/app/models/organization.rb index 18df63cf193..60f3c49aec9 100644 --- a/app/models/organization.rb +++ b/app/models/organization.rb @@ -10,6 +10,7 @@ class Organization < ApplicationRecord "credit_note.created" ].freeze + has_many :billing_entities has_many :api_keys has_many :memberships has_many :users, through: :memberships diff --git a/app/services/customers/create_service.rb b/app/services/customers/create_service.rb index e20c16472f2..bb530cdfe46 100644 --- a/app/services/customers/create_service.rb +++ b/app/services/customers/create_service.rb @@ -32,6 +32,7 @@ def create_from_api(organization:, params:) ActiveRecord::Base.transaction do customer.name = params[:name] if params.key?(:name) + customer.billing_entity_id = params[:billing_entity_id] if params.key?(:billing_entity_id) customer.country = params[:country]&.upcase if params.key?(:country) customer.address_line1 = params[:address_line1] if params.key?(:address_line1) customer.address_line2 = params[:address_line2] if params.key?(:address_line2) @@ -142,6 +143,7 @@ def create(**args) customer = Customer.new( organization_id: args[:organization_id], + billing_entity_id: args[:billing_entity_id], external_id: args[:external_id], name: args[:name], country: args[:country]&.upcase, diff --git a/app/services/error_details/create_service.rb b/app/services/error_details/create_service.rb index 951d96b6800..0305a1b174c 100644 --- a/app/services/error_details/create_service.rb +++ b/app/services/error_details/create_service.rb @@ -15,6 +15,7 @@ def create_error_details! new_error = ErrorDetail.create!( owner:, organization:, + billing_entity:, error_code: params[:error_code], details: params[:details] ) diff --git a/app/services/invoices/add_on_service.rb b/app/services/invoices/add_on_service.rb index bc98bdd1110..efc2292ae61 100644 --- a/app/services/invoices/add_on_service.rb +++ b/app/services/invoices/add_on_service.rb @@ -14,6 +14,7 @@ def create ActiveRecord::Base.transaction do invoice = Invoice.create!( organization: customer.organization, + billing_entity: customer.billing_entity, customer:, issuing_date:, payment_due_date:, @@ -94,7 +95,7 @@ def payment_due_date def should_deliver_email? License.premium? && - customer.organization.email_settings.include?('invoice.finalized') + customer.billing_entity.email_settings.include?('invoice.finalized') end end end diff --git a/app/services/invoices/advance_charges_service.rb b/app/services/invoices/advance_charges_service.rb index a5ade782fde..158c3ae85de 100644 --- a/app/services/invoices/advance_charges_service.rb +++ b/app/services/invoices/advance_charges_service.rb @@ -8,6 +8,7 @@ def initialize(initial_subscriptions:, billing_at:) @customer = initial_subscriptions&.first&.customer @organization = customer&.organization + @billing_entity = customer&.billing_entity @currency = initial_subscriptions&.first&.plan&.amount_currency super @@ -35,12 +36,12 @@ def call private - attr_accessor :initial_subscriptions, :billing_at, :customer, :organization, :currency + attr_accessor :initial_subscriptions, :billing_at, :customer, :organization, :billing_entity, :currency def subscriptions - return [] unless organization + return [] unless billing_entity - @subscriptions ||= organization.subscriptions.where( + @subscriptions ||= billing_entity.subscriptions.where( external_id: initial_subscriptions.pluck(:external_id).uniq, status: [:active, :terminated] ) diff --git a/app/services/invoices/apply_taxes_service.rb b/app/services/invoices/apply_taxes_service.rb index 2a85824a1dd..c6c55cb3de4 100644 --- a/app/services/invoices/apply_taxes_service.rb +++ b/app/services/invoices/apply_taxes_service.rb @@ -52,9 +52,10 @@ def call attr_reader :invoice delegate :organization, to: :invoice + delegate :billing_entity, to: :invoice def applicable_taxes - organization.taxes.where(id: indexed_fees.keys) + billing_entity.taxes.where(id: indexed_fees.keys) end # NOTE: indexes the invoice fees by taxes. diff --git a/app/services/invoices/create_generating_service.rb b/app/services/invoices/create_generating_service.rb index 4cc02e60c01..55248400778 100644 --- a/app/services/invoices/create_generating_service.rb +++ b/app/services/invoices/create_generating_service.rb @@ -21,6 +21,7 @@ def call invoice = Invoice.create!( id: invoice_id || SecureRandom.uuid, organization:, + billing_entity:, customer:, invoice_type:, currency:, @@ -45,6 +46,7 @@ def call attr_accessor :customer, :invoice_type, :currency, :datetime, :charge_in_advance, :skip_charges, :invoice_id delegate :organization, to: :customer + delegate :billing_entity, to: :customer # NOTE: accounting date must be in customer timezone def issuing_date 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 8b6e32c5d3d..a03c2ec7124 100644 --- a/app/services/invoices/create_pay_in_advance_charge_service.rb +++ b/app/services/invoices/create_pay_in_advance_charge_service.rb @@ -107,7 +107,7 @@ def deliver_webhooks end def should_deliver_email? - License.premium? && customer.organization.email_settings.include?('invoice.finalized') + License.premium? && customer.billing_entity.email_settings.include?('invoice.finalized') end def wallet @@ -151,6 +151,7 @@ def create_error_detail(code) error_result = ErrorDetails::CreateService.call( owner: invoice, organization: invoice.organization, + billing_entity: invoice.billing_entity, params: { error_code: :tax_error, details: { diff --git a/app/services/invoices/subscription_service.rb b/app/services/invoices/subscription_service.rb index b0d8a401dc6..f9d87f8aec1 100644 --- a/app/services/invoices/subscription_service.rb +++ b/app/services/invoices/subscription_service.rb @@ -139,7 +139,7 @@ def set_invoice_generated_status def should_deliver_finalized_email? License.premium? && - customer.organization.email_settings.include?("invoice.finalized") + customer.billing_entity.email_settings.include?("invoice.finalized") end def flag_lifetime_usage_for_refresh diff --git a/db/migrate/20250131093431_add_references_to_billing_entities.rb b/db/migrate/20250131093431_add_references_to_billing_entities.rb index ca5838f12dd..906825b4ec5 100644 --- a/db/migrate/20250131093431_add_references_to_billing_entities.rb +++ b/db/migrate/20250131093431_add_references_to_billing_entities.rb @@ -16,5 +16,6 @@ def change add_reference :cached_aggregations, :billing_entity, index: {algorithm: :concurrently}, type: :uuid add_reference :data_exports, :billing_entity, index: {algorithm: :concurrently}, type: :uuid add_reference :invoice_custom_section_selections, :billing_entity, index: {algorithm: :concurrently}, type: :uuid + add_reference :error_details, :billing_entity, index: {algorithm: :concurrently}, type: :uuid end end diff --git a/db/migrate/20250131111555_populate_billing_entity_for_organizations.rb b/db/migrate/20250131111555_populate_billing_entity_for_organizations.rb index e83f22ae5dc..43661e7f990 100644 --- a/db/migrate/20250131111555_populate_billing_entity_for_organizations.rb +++ b/db/migrate/20250131111555_populate_billing_entity_for_organizations.rb @@ -62,7 +62,7 @@ def change organization.cached_aggregations.update_all(billing_entity_id: billing_entity.id) organization.data_exports.update_all(billing_entity_id: billing_entity.id) organization.invoice_custom_section_selections.update_all(billing_entity_id: billing_entity.id) - + ErrorDetail.where(organization_id: organization.id).update_all(billing_entity_id: billing_entity.id) organization.taxes.applied_to_organization.each do |tax| billing_entity.taxes << tax diff --git a/db/schema.rb b/db/schema.rb index bbb0d956200..cfc35350fdf 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -640,6 +640,8 @@ t.datetime "created_at", null: false t.datetime "updated_at", null: false t.integer "error_code", default: 0, null: false + t.uuid "billing_entity_id" + t.index ["billing_entity_id"], name: "index_error_details_on_billing_entity_id" t.index ["deleted_at"], name: "index_error_details_on_deleted_at" t.index ["error_code"], name: "index_error_details_on_error_code" t.index ["organization_id"], name: "index_error_details_on_organization_id"