From 55db44db2e5dc9f428d363af9d5a7f36a49bd03a Mon Sep 17 00:00:00 2001 From: Vincent Pochet Date: Fri, 23 Aug 2024 09:44:09 +0200 Subject: [PATCH] feat(ProgressiveBilling): Update PDF template (#2465) ## 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 --- app/models/credit.rb | 1 + app/views/templates/invoices/v4.slim | 2 + .../v4/_progressive_billing_details.slim | 96 +++++++++++++++++++ .../invoices/v4/_subscription_details.slim | 20 ++-- .../invoices/v4/_subscriptions_summary.slim | 8 ++ .../progressive_billing_service_spec.rb | 2 +- 6 files changed, 117 insertions(+), 12 deletions(-) create mode 100644 app/views/templates/invoices/v4/_progressive_billing_details.slim diff --git a/app/models/credit.rb b/app/models/credit.rb index d20adb6d62c..9c828a59280 100644 --- a/app/models/credit.rb +++ b/app/models/credit.rb @@ -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 diff --git a/app/views/templates/invoices/v4.slim b/app/views/templates/invoices/v4.slim index 9bba2865499..55ef494e608 100644 --- a/app/views/templates/invoices/v4.slim +++ b/app/views/templates/invoices/v4.slim @@ -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) diff --git a/app/views/templates/invoices/v4/_progressive_billing_details.slim b/app/views/templates/invoices/v4/_progressive_billing_details.slim new file mode 100644 index 00000000000..d083ed124f3 --- /dev/null +++ b/app/views/templates/invoices/v4/_progressive_billing_details.slim @@ -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) diff --git a/app/views/templates/invoices/v4/_subscription_details.slim b/app/views/templates/invoices/v4/_subscription_details.slim index 16d7e54506a..13589ed8557 100644 --- a/app/views/templates/invoices/v4/_subscription_details.slim +++ b/app/views/templates/invoices/v4/_subscription_details.slim @@ -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 @@ -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 @@ -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 diff --git a/app/views/templates/invoices/v4/_subscriptions_summary.slim b/app/views/templates/invoices/v4/_subscriptions_summary.slim index 896d139c55a..7b3be0b6ba3 100644 --- a/app/views/templates/invoices/v4/_subscriptions_summary.slim +++ b/app/views/templates/invoices/v4/_subscriptions_summary.slim @@ -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 diff --git a/spec/services/invoices/progressive_billing_service_spec.rb b/spec/services/invoices/progressive_billing_service_spec.rb index 52b3b6bfede..dd7babd731e 100644 --- a/spec/services/invoices/progressive_billing_service_spec.rb +++ b/spec/services/invoices/progressive_billing_service_spec.rb @@ -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') }