Skip to content

Commit

Permalink
Implement Credits::ProgressiveBillingService and add it to Invoices::…
Browse files Browse the repository at this point in the history
…CalculateFeesService (#2443)

## 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

Adds a service for calculating how much to deduct from the subscription
invoice based on previously billed progressive invoices.
  • Loading branch information
nudded authored Aug 20, 2024
1 parent 1ed2c92 commit 0b9395b
Show file tree
Hide file tree
Showing 8 changed files with 377 additions and 38 deletions.
73 changes: 37 additions & 36 deletions app/models/invoice.rb
Original file line number Diff line number Diff line change
Expand Up @@ -403,42 +403,43 @@ def status_changed_to_finalized?
#
# Table name: invoices
#
# id :uuid not null, primary key
# coupons_amount_cents :bigint default(0), not null
# credit_notes_amount_cents :bigint default(0), not null
# currency :string
# fees_amount_cents :bigint default(0), not null
# file :string
# invoice_type :integer default("subscription"), not null
# issuing_date :date
# negative_amount_cents :bigint default(0), not null
# net_payment_term :integer default(0), not null
# number :string default(""), not null
# payment_attempts :integer default(0), not null
# payment_dispute_lost_at :datetime
# payment_due_date :date
# payment_overdue :boolean default(FALSE)
# payment_status :integer default("pending"), not null
# prepaid_credit_amount_cents :bigint default(0), not null
# ready_for_payment_processing :boolean default(TRUE), not null
# ready_to_be_refreshed :boolean default(FALSE), not null
# skip_charges :boolean default(FALSE), not null
# status :integer default("finalized"), not null
# sub_total_excluding_taxes_amount_cents :bigint default(0), not null
# sub_total_including_taxes_amount_cents :bigint default(0), not null
# taxes_amount_cents :bigint default(0), not null
# taxes_rate :float default(0.0), not null
# timezone :string default("UTC"), not null
# total_amount_cents :bigint default(0), not null
# version_number :integer default(4), not null
# voided_at :datetime
# created_at :datetime not null
# updated_at :datetime not null
# customer_id :uuid
# organization_id :uuid not null
# organization_sequential_id :integer default(0), not null
# payable_group_id :uuid
# sequential_id :integer
# id :uuid not null, primary key
# coupons_amount_cents :bigint default(0), not null
# credit_notes_amount_cents :bigint default(0), not null
# currency :string
# fees_amount_cents :bigint default(0), not null
# file :string
# invoice_type :integer default("subscription"), not null
# issuing_date :date
# negative_amount_cents :bigint default(0), not null
# net_payment_term :integer default(0), not null
# number :string default(""), not null
# payment_attempts :integer default(0), not null
# payment_dispute_lost_at :datetime
# payment_due_date :date
# payment_overdue :boolean default(FALSE)
# payment_status :integer default("pending"), not null
# prepaid_credit_amount_cents :bigint default(0), not null
# progressive_billing_credit_amount_cents :bigint default(0), not null
# ready_for_payment_processing :boolean default(TRUE), not null
# ready_to_be_refreshed :boolean default(FALSE), not null
# skip_charges :boolean default(FALSE), not null
# status :integer default("finalized"), not null
# sub_total_excluding_taxes_amount_cents :bigint default(0), not null
# sub_total_including_taxes_amount_cents :bigint default(0), not null
# taxes_amount_cents :bigint default(0), not null
# taxes_rate :float default(0.0), not null
# timezone :string default("UTC"), not null
# total_amount_cents :bigint default(0), not null
# version_number :integer default(4), not null
# voided_at :datetime
# created_at :datetime not null
# updated_at :datetime not null
# customer_id :uuid
# organization_id :uuid not null
# organization_sequential_id :integer default(0), not null
# payable_group_id :uuid
# sequential_id :integer
#
# Indexes
#
Expand Down
69 changes: 69 additions & 0 deletions app/services/credits/progressive_billing_service.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
# frozen_string_literal: true

module Credits
class ProgressiveBillingService < BaseService
def initialize(invoice:)
@invoice = invoice
super
end

def call
result.credits = []
return result unless should_create_progressive_billing_credit?

invoice.invoice_subscriptions.each do |invoice_subscription|
subscription = invoice_subscription.subscription
progressive_billing_invoices = subscription
.invoices
.progressive_billing
.finalized
.where(issuing_date: invoice_subscription.charges_from_datetime...invoice_subscription.charges_to_datetime)
.order(issuing_date: :asc)

total_subscription_amount = invoice.fees.charge.where(subscription: subscription).sum(:amount_cents)

remaining_to_credit = total_subscription_amount

progressive_billing_invoices.each do |progressive_billing_invoice|
amount_to_credit = progressive_billing_invoice.fees.progressive_billing.sum(:amount_cents)

if amount_to_credit > remaining_to_credit
# TODO: create credit note for (amount_to_credit - remaining_credit)
invoice.negative_amount_cents -= (amount_to_credit - remaining_to_credit)
amount_to_credit = remaining_to_credit
end

if amount_to_credit.positive?
credit = Credit.create!(
invoice:,
progressive_billing_invoice:,
amount_cents: amount_to_credit,
amount_currency: invoice.currency,
before_taxes: true
)

invoice.sub_total_excluding_taxes_amount_cents -= credit.amount_cents
invoice.progressive_billing_credit_amount_cents += credit.amount_cents
result.credits << credit

remaining_to_credit -= amount_to_credit
end
end
end
result
end

private

def should_create_progressive_billing_credit?
invoice.invoice_subscriptions.any? do |invoice_subscription|
invoice_subscription.subscription.invoices.progressive_billing
.finalized
.where(issuing_date: invoice_subscription.charges_from_datetime...invoice_subscription.charges_to_datetime)
.exists?
end
end

attr_reader :invoice
end
end
1 change: 1 addition & 0 deletions app/services/invoices/calculate_fees_service.rb
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ def call
invoice.sub_total_excluding_taxes_amount_cents = invoice.fees.sum(:amount_cents) -
invoice.coupons_amount_cents

Credits::ProgressiveBillingService.call(invoice:)
Credits::AppliedCouponsService.call(invoice:) if should_create_coupon_credit?
Invoices::ComputeAmountsFromFees.call(invoice:)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,10 @@ def call
usage_thresholds.each do |usage_threshold|
SendWebhookJob.perform_later('subscription.usage_threshold_reached', subscription, usage_threshold:)
end
Invoices::ProgressiveBillingService.call(usage_thresholds:, lifetime_usage:).raise_if_error!
invoice_result = Invoices::ProgressiveBillingService.call(usage_thresholds:, lifetime_usage:).raise_if_error!
result.invoice = invoice_result.invoice
end
result
end

private
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# frozen_string_literal: true

class AddProgressiveBillingCreditAmountCentsToInvoices < ActiveRecord::Migration[7.1]
def change
add_column :invoices, :progressive_billing_credit_amount_cents, :bigint, default: 0, null: false
end
end
3 changes: 2 additions & 1 deletion db/schema.rb

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

Loading

0 comments on commit 0b9395b

Please sign in to comment.