Skip to content

Commit

Permalink
WIP create customers and invoices with billing_entity relation
Browse files Browse the repository at this point in the history
  • Loading branch information
annvelents committed Feb 3, 2025
1 parent 9317d6b commit ece1f15
Show file tree
Hide file tree
Showing 18 changed files with 70 additions and 49 deletions.
2 changes: 1 addition & 1 deletion app/graphql/mutations/customers/create.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion app/models/billing_entity.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 2 additions & 1 deletion app/models/customer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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?
Expand Down
29 changes: 16 additions & 13 deletions app/models/error_detail.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
#
Expand Down
1 change: 1 addition & 0 deletions app/models/integrations/base_integration.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
53 changes: 28 additions & 25 deletions app/models/invoice.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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

Expand All @@ -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?
Expand Down
1 change: 1 addition & 0 deletions app/models/organization.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions app/services/customers/create_service.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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,
Expand Down
1 change: 1 addition & 0 deletions app/services/error_details/create_service.rb
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ def create_error_details!
new_error = ErrorDetail.create!(
owner:,
organization:,
billing_entity:,
error_code: params[:error_code],
details: params[:details]
)
Expand Down
3 changes: 2 additions & 1 deletion app/services/invoices/add_on_service.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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:,
Expand Down Expand Up @@ -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
7 changes: 4 additions & 3 deletions app/services/invoices/advance_charges_service.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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]
)
Expand Down
3 changes: 2 additions & 1 deletion app/services/invoices/apply_taxes_service.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
2 changes: 2 additions & 0 deletions app/services/invoices/create_generating_service.rb
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ def call
invoice = Invoice.create!(
id: invoice_id || SecureRandom.uuid,
organization:,
billing_entity:
customer:,
invoice_type:,
currency:,
Expand All @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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: {
Expand Down
2 changes: 1 addition & 1 deletion app/services/invoices/subscription_service.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions db/schema.rb

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit ece1f15

Please sign in to comment.