Skip to content

Commit

Permalink
feat(ProgressiveBilling): Update PDF template (#2465)
Browse files Browse the repository at this point in the history
## Context

AI companies want their users to pay before the end of a period if usage
skyrockets. The problem being that self-serve companies can overuse
their API without paying, triggering lots of costs on their side.

## Description

This PR updates the PDF templates to render:
- Progressive billing invoices
- Credits for previous progressive billing invoices
  • Loading branch information
vincent-pochet authored Aug 23, 2024
1 parent 53a6e0e commit 55db44d
Show file tree
Hide file tree
Showing 6 changed files with 117 additions and 12 deletions.
1 change: 1 addition & 0 deletions app/models/credit.rb
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ class Credit < ApplicationRecord

scope :coupon_kind, -> { where.not(applied_coupon_id: nil) }
scope :credit_note_kind, -> { where.not(credit_note_id: nil) }
scope :progressive_billing_invoice_kind, -> { where.not(progressive_billing_invoice_id: nil) }

def item_id
return coupon&.id if applied_coupon_id
Expand Down
2 changes: 2 additions & 0 deletions app/views/templates/invoices/v4.slim
Original file line number Diff line number Diff line change
Expand Up @@ -455,6 +455,8 @@ html
== SlimHelper.render('templates/invoices/v4/_credit', self)
- elsif subscriptions.count == 1
== SlimHelper.render('templates/invoices/v4/_subscription_details', self)
- elsif progressive_billing?
== SlimHelper.render('templates/invoices/v4/_progressive_billing_details', self)
- else
== SlimHelper.render('templates/invoices/v4/_subscriptions_summary', self)

Expand Down
96 changes: 96 additions & 0 deletions app/views/templates/invoices/v4/_progressive_billing_details.slim
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
- subscription = subscriptions.first
- invoice_subscription = invoice_subscription(subscription.id)

/ Subscription fee section
.invoice-resume.overflow-auto
table.invoice-resume-table width="100%"
tr.first_child
td.body-2 = I18n.t('invoice.fees_from_to_date', from_date: I18n.l(invoice_subscription.charges_from_datetime_in_customer_timezone&.to_date, format: :default), to_date: I18n.l(invoice_subscription.charges_to_datetime_in_customer_timezone&.to_date, format: :default))
td.body-2 = I18n.t('invoice.units')
td.body-2 = I18n.t('invoice.unit_price')
td.body-2 = I18n.t('invoice.tax_rate')
td.body-2 = I18n.t('invoice.amount')

/ Charge fees section for subscription invoice
- if subscription_fees(subscription.id).charge_kind.any?
/ Charges payed in arrears OR charges and plan payed in advance
- if subscription.plan.charges.any?
.invoice-resume.overflow-auto
table.invoice-resume-table width="100%"

/ Loop over all top level fees
- subscription_fees(subscription.id).charge_kind.positive_units.where(true_up_parent_fee: nil).joins(charge: :billable_metric).sort_by { |f| f.invoice_sorting_clause }.group_by(&:charge_id).each do |_charge_id, fees|
- fee = fees.first
- next if fee.charge.pay_in_advance?

/ Fees for filters
- if fees.all? { |f| f.charge_filter_id? } && fees.sum(&:units) > 0
- fees.select { |f| f.units.positive? }.each do |fee|
- if fee.amount_details.blank?
== SlimHelper.render('templates/invoices/v4/_default_fee_with_filters', fee)
- else
== SlimHelper.render('templates/invoices/v4/_fee_with_filters', fee)

/ True up fees attached to the fee
- fees.select { |f| f.true_up_fee.present? }.each do |fee|
== SlimHelper.render('templates/invoices/v4/_true_up_fee', fee)

/ Fees without filters
- else
- fees.sort_by { |f| f.invoice_sorting_clause }.each do |fee|
== SlimHelper.render('templates/invoices/v4/_fees_without_filters', fee)

/ Total section
.invoice-resume.overflow-auto
table.total-table width="100%"
- if progressive_billing_credit_amount_cents.positive?
- credits.progressive_billing_invoice_kind.order(created_at: :asc) do |credit|
tr
td.body-2
/ TODO(ProgressiveBilling): apply the right label
td.body-2 #{credit.item_name}
td.body-2 = '-' + MoneyHelper.format(credit.amount)

- if coupons_amount_cents.positive?
- credits.coupon_kind.order(created_at: :asc).each do |credit|
tr
td.body-2
td.body-2 #{credit.invoice_coupon_display_name}
td.body-2 = '-' + MoneyHelper.format(credit.amount)
tr
td.body-2
td.body-2 = I18n.t('invoice.sub_total_without_tax')
td.body-2 = MoneyHelper.format(sub_total_excluding_taxes_amount)
- if applied_taxes.present?
- applied_taxes.order(tax_rate: :desc).each do |applied_tax|
tr
td.body-2
- if applied_tax.applied_on_whole_invoice?
td.body-2 = I18n.t('invoice.tax_name_only.' + applied_tax.tax_code)
- else
td.body-2 = I18n.t('invoice.tax_name', name: applied_tax.tax_name, rate: applied_tax.tax_rate, amount: MoneyHelper.format(applied_tax.fees_amount))
td.body-2 = MoneyHelper.format(applied_tax.amount)
- else
tr
td.body-2
td.body-2 = I18n.t('invoice.tax_name_with_details', name: 'Tax', rate: 0)
td.body-2 = MoneyHelper.format(0.to_money(currency))
tr
td.body-2
td.body-2 = I18n.t('invoice.sub_total_with_tax')
td.body-2 = MoneyHelper.format(sub_total_including_taxes_amount)
- if credits.credit_note_kind.any?
tr
td.body-2
td.body-2 = I18n.t('invoice.credit_notes')
td.body-2 = '-' + MoneyHelper.format(credit_notes_amount)
- if subscription? && wallet_transactions.exists?
tr
td.body-2
td.body-2 = I18n.t('invoice.prepaid_credits')
td.body-2 = '-' + MoneyHelper.format(prepaid_credit_amount)
tr
td.body-2
td.body-1 = I18n.t('invoice.total_due')
td.body-1
= MoneyHelper.format(total_amount)
20 changes: 9 additions & 11 deletions app/views/templates/invoices/v4/_subscription_details.slim
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
- if subscription? || progressive_billing?
- if subscription?
- subscriptions.each do |subscription|
- invoice_subscription = invoice_subscription(subscription.id)
- subscription_fee = invoice_subscription.subscription_fee
Expand Down Expand Up @@ -52,16 +52,6 @@
- fees.sort_by { |f| f.invoice_sorting_clause }.each do |fee|
== SlimHelper.render('templates/invoices/v4/_fees_without_filters', fee)

/ Progressive billing fees
- if progressive_billing?
- fees.sort_by { |f| f.invoice_sorting_clause }.each do |fee|
tr
td.body-1 = fee.invoice_name
td.body-2 = fee.units
td.body-2 = MoneyHelper.format(fee.amount)
td.body-2 == TaxHelper.applied_taxes(fee)
td.body-2 = MoneyHelper.format(fee.amount)

/ Charge fees section for subscription invoice
- if subscription? && subscription_fees(subscription.id).charge_kind.any?
/ Charges payed in arrears OR charges and plan payed in advance
Expand Down Expand Up @@ -146,6 +136,14 @@
table.total-table width="100%"
- if subscriptions.count == 1
- unless credit?
- if progressive_billing_credit_amount_cents.positive?
- credits.progressive_billing_invoice_kind.order(created_at: :asc) do |credit|
tr
td.body-2
/ TODO(ProgressiveBilling): apply the right label
td.body-2 #{credit.item_name}
td.body-2 = '-' + MoneyHelper.format(credit.amount)

- if coupons_amount_cents.positive?
- credits.coupon_kind.order(created_at: :asc).each do |credit|
tr
Expand Down
8 changes: 8 additions & 0 deletions app/views/templates/invoices/v4/_subscriptions_summary.slim
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,14 @@ table.invoice-resume-table width="100%"

table.total-table width="100%"
- unless credit?
- if progressive_billing_credit_amount_cents.positive?
- credits.progressive_billing_invoice_kind.order(created_at: :asc) do |credit|
tr
td.body-2
/ TODO(ProgressiveBilling): apply the right label
td.body-2 #{credit.item_name}
td.body-2 = '-' + MoneyHelper.format(credit.amount)

- if coupons_amount_cents.positive?
- credits.coupon_kind.order(created_at: :asc).each do |credit|
tr
Expand Down
2 changes: 1 addition & 1 deletion spec/services/invoices/progressive_billing_service_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
let(:organization) { plan.organization }

let(:customer) { create(:customer, organization:) }
let(:subscription) { create(:subscription, plan:, customer:) }
let(:subscription) { create(:subscription, plan:, customer:, started_at: timestamp - 1.week) }
let(:lifetime_usage) { create(:lifetime_usage, subscription:, organization:) }

let(:timestamp) { Time.zone.parse('2024-08-22 10:00:00') }
Expand Down

0 comments on commit 55db44d

Please sign in to comment.