Skip to content

Commit

Permalink
Feat(invoice_custom_sections): api controllers (#3007)
Browse files Browse the repository at this point in the history
## Context

API controllers to support invoice_custom_sections functionality

## Description

Added InvoiceCustomSectionsController and updated customers controller
with two new actions
  • Loading branch information
annvelents authored Jan 14, 2025
1 parent 4a0f0c4 commit e92601d
Show file tree
Hide file tree
Showing 53 changed files with 583 additions and 69 deletions.
6 changes: 4 additions & 2 deletions app/controllers/api/v1/customers_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,7 @@ def create_params
:net_payment_term,
:external_salesforce_id,
:finalize_zero_amount_invoice,
:skip_invoice_custom_sections,
integration_customers: [
:id,
:external_customer_id,
Expand Down Expand Up @@ -156,7 +157,8 @@ def create_params
:state,
:country
],
tax_codes: []
tax_codes: [],
invoice_custom_section_codes: []
)
end

Expand All @@ -165,7 +167,7 @@ def render_customer(customer)
json: ::V1::CustomerSerializer.new(
customer,
root_name: 'customer',
includes: %i[taxes integration_customers]
includes: %i[taxes integration_customers applicable_invoice_custom_sections]
)
)
end
Expand Down
2 changes: 1 addition & 1 deletion app/controllers/api/v1/invoices_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -241,7 +241,7 @@ def render_invoice(invoice)
json: ::V1::InvoiceSerializer.new(
invoice,
root_name: 'invoice',
includes: %i[customer integration_customers subscriptions fees credits metadata applied_taxes error_details]
includes: %i[customer integration_customers subscriptions fees credits metadata applied_taxes error_details applied_invoice_custom_sections]
)
)
end
Expand Down
11 changes: 7 additions & 4 deletions app/graphql/resolvers/invoice_custom_sections_resolver.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
2 changes: 1 addition & 1 deletion app/models/api_key.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ class ApiKey < ApplicationRecord
RESOURCES = %w[
add_on analytic billable_metric coupon applied_coupon credit_note customer_usage
customer event fee invoice organization payment_request plan subscription lifetime_usage
tax wallet wallet_transaction webhook_endpoint webhook_jwt_public_key
tax wallet wallet_transaction webhook_endpoint webhook_jwt_public_key invoice_custom_section
].freeze

MODES = %w[read write].freeze
Expand Down
2 changes: 1 addition & 1 deletion app/models/customer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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?
Expand Down
4 changes: 3 additions & 1 deletion app/models/invoice_custom_section.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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 }

Expand Down
12 changes: 11 additions & 1 deletion app/serializers/v1/customer_serializer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -34,13 +34,15 @@ def serialize
external_salesforce_id: model.external_salesforce_id,
finalize_zero_amount_invoice: model.finalize_zero_amount_invoice,
billing_configuration:,
shipping_address: model.shipping_address
shipping_address: model.shipping_address,
skip_invoice_custom_sections: model.skip_invoice_custom_sections
}

payload = payload.merge(metadata)
payload = payload.merge(taxes) if include?(:taxes)
payload = payload.merge(vies_check) if include?(:vies_check)
payload = payload.merge(integration_customers) if include?(:integration_customers)
payload = payload.merge(applicable_invoice_custom_sections) if include?(:applicable_invoice_custom_sections)

payload
end
Expand Down Expand Up @@ -98,5 +100,13 @@ def integration_customers
collection_name: 'integration_customers'
).serialize
end

def applicable_invoice_custom_sections
::CollectionSerializer.new(
model.applicable_invoice_custom_sections,
::V1::InvoiceCustomSectionSerializer,
collection_name: 'applicable_invoice_custom_sections'
).serialize
end
end
end
18 changes: 18 additions & 0 deletions app/serializers/v1/invoice_custom_section_serializer.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# frozen_string_literal: true

module V1
class InvoiceCustomSectionSerializer < ModelSerializer
def serialize
{
lago_id: model.id,
code: model.code,
name: model.name,
description: model.description,
details: model.details,
display_name: model.display_name,
applied_to_organization: model.selected_for_organization?,
organization_id: model.organization_id
}
end
end
end
9 changes: 9 additions & 0 deletions app/serializers/v1/invoice_serializer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Original file line number Diff line number Diff line change
@@ -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
6 changes: 6 additions & 0 deletions app/services/customers/create_service.rb
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,12 @@ def create_from_api(organization:, params:)
taxes_result.raise_if_error!
end

Customers::ManageInvoiceCustomSectionsService.call(
customer:,
skip_invoice_custom_sections: params[:skip_invoice_custom_sections],
section_codes: params[:invoice_custom_section_codes]
).raise_if_error!

if new_customer && params[:metadata]
params[:metadata].each { |m| create_metadata(customer:, args: m) }
elsif params[:metadata]
Expand Down
25 changes: 18 additions & 7 deletions app/services/customers/manage_invoice_custom_sections_service.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,27 +2,30 @@

module Customers
class ManageInvoiceCustomSectionsService < BaseService
def initialize(customer:, skip_invoice_custom_sections:, section_ids:)
def initialize(customer:, skip_invoice_custom_sections:, section_ids: nil, section_codes: nil)
@customer = customer
@section_ids = section_ids
@section_codes = section_codes
@skip_invoice_custom_sections = skip_invoice_custom_sections

super
end

def call
return result.not_found_failure!(resource: "customer") unless customer
raise_invalid_params if skip_invoice_custom_sections && section_ids.present?
return fail_with_double_selection if !section_ids.nil? && !section_codes.nil?
return fail_with_invalid_params if skip_invoice_custom_sections && !(section_ids || section_codes).nil?

ActiveRecord::Base.transaction do
unless skip_invoice_custom_sections.nil?
if !skip_invoice_custom_sections.nil?
customer.selected_invoice_custom_sections = [] if !!skip_invoice_custom_sections
customer.skip_invoice_custom_sections = skip_invoice_custom_sections
end

unless section_ids.nil?
if !section_ids.nil? || !section_codes.nil?
customer.skip_invoice_custom_sections = false
return result if customer.applicable_invoice_custom_sections.ids == section_ids
return result if customer.selected_invoice_custom_sections.ids == section_ids ||
customer.selected_invoice_custom_sections.map(&:code) == section_codes

assign_selected_sections
end
Expand All @@ -35,14 +38,22 @@ def call

private

attr_reader :customer, :section_ids, :skip_invoice_custom_sections
attr_reader :customer, :section_ids, :skip_invoice_custom_sections, :section_codes

def raise_invalid_params
def fail_with_double_selection
result.validation_failure!(errors: {invoice_custom_sections: ['section_ids_and_section_codes_sent_together']})
end

def fail_with_invalid_params
result.validation_failure!(errors: {invoice_custom_sections: ['skip_sections_and_selected_ids_sent_together']})
end

def assign_selected_sections
# Note: when assigning organization's sections, an empty array will be sent
if section_ids.nil?
return customer.selected_invoice_custom_sections = customer.organization.invoice_custom_sections.where(code: section_codes)
end

customer.selected_invoice_custom_sections = customer.organization.invoice_custom_sections.where(id: section_ids)
end
end
Expand Down
2 changes: 2 additions & 0 deletions app/services/invoice_custom_sections/update_service.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ def initialize(invoice_custom_section:, update_params:, selected: false)
end

def call
return result.not_found_failure!(resource: 'invoice_custom_section') unless invoice_custom_section

invoice_custom_section.update!(update_params)
if selected
Organizations::SelectInvoiceCustomSectionService.call(section: invoice_custom_section)
Expand Down
1 change: 1 addition & 0 deletions app/services/invoices/add_on_service.rb
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ def create

create_add_on_fee(invoice)
compute_amounts(invoice)
Invoices::ApplyInvoiceCustomSectionsService.call(invoice:)

invoice.save!

Expand Down
1 change: 1 addition & 0 deletions app/services/invoices/advance_charges_service.rb
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ def create_group_invoice
end

Invoices::ComputeAmountsFromFees.call(invoice:)
Invoices::ApplyInvoiceCustomSectionsService.call(invoice:)

invoice.payment_status = :succeeded
Invoices::TransitionToFinalStatusService.call(invoice:)
Expand Down
34 changes: 34 additions & 0 deletions app/services/invoices/apply_invoice_custom_sections_service.rb
Original file line number Diff line number Diff line change
@@ -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
1 change: 1 addition & 0 deletions app/services/invoices/create_one_off_service.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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!
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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:)
Expand Down
1 change: 1 addition & 0 deletions app/services/invoices/paid_credit_service.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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!
Expand Down
1 change: 1 addition & 0 deletions app/services/invoices/progressive_billing_service.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
3 changes: 3 additions & 0 deletions app/services/invoices/refresh_draft_service.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down Expand Up @@ -121,6 +122,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
Expand All @@ -129,6 +131,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
Expand Down
1 change: 1 addition & 0 deletions app/services/invoices/subscription_service.rb
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ def call
recurring:,
context:
)
Invoices::ApplyInvoiceCustomSectionsService.call(invoice:)

set_invoice_generated_status unless invoice.pending?
invoice.save!
Expand Down
2 changes: 1 addition & 1 deletion app/services/webhooks/invoices/add_on_created_service.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
2 changes: 1 addition & 1 deletion app/services/webhooks/invoices/created_service.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
2 changes: 1 addition & 1 deletion app/services/webhooks/invoices/one_off_created_service.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
Loading

0 comments on commit e92601d

Please sign in to comment.