Skip to content

Commit

Permalink
Feat(invoice_custom_sections): use custom sections in pdf (#3013)
Browse files Browse the repository at this point in the history
Use applied invoice custom sections in PDFs and include them in invoice
payload
Also includes fixes found after QA
  • Loading branch information
annvelents authored Jan 14, 2025
1 parent 901fa19 commit 2c5755f
Show file tree
Hide file tree
Showing 26 changed files with 144 additions and 31 deletions.
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 subscriptions fees credits metadata applied_taxes error_details]
includes: %i[customer 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/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
6 changes: 2 additions & 4 deletions app/serializers/v1/invoice_custom_section_serializer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
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
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
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
3 changes: 3 additions & 0 deletions app/views/templates/invoices/v3.slim
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
15 changes: 15 additions & 0 deletions app/views/templates/invoices/v3/_custom_sections.slim
Original file line number Diff line number Diff line change
@@ -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)
3 changes: 3 additions & 0 deletions app/views/templates/invoices/v3/charge.slim
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 3 additions & 0 deletions app/views/templates/invoices/v3/one_off.slim
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
18 changes: 12 additions & 6 deletions app/views/templates/invoices/v4.slim
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,9 @@ html


/* ----------------------- variable ----------------------- */
:root {
--border-color: #D9DEE7;
}

@font-face {
font-family: 'Inter var';
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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%;
}
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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;
}

Expand Down Expand Up @@ -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
Expand Down
15 changes: 15 additions & 0 deletions app/views/templates/invoices/v4/_custom_sections.slim
Original file line number Diff line number Diff line change
@@ -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)
3 changes: 3 additions & 0 deletions app/views/templates/invoices/v4/charge.slim
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 3 additions & 0 deletions app/views/templates/invoices/v4/one_off.slim
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
4 changes: 2 additions & 2 deletions spec/models/customer_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
2 changes: 1 addition & 1 deletion spec/requests/api/v1/customers_controller_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
3 changes: 2 additions & 1 deletion spec/requests/api/v1/invoices_controller_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 2 additions & 4 deletions spec/serializers/v1/invoice_custom_section_serializer_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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
2 changes: 1 addition & 1 deletion spec/services/invoices/refresh_draft_service_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down

0 comments on commit 2c5755f

Please sign in to comment.