Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: T-965 Generate fees for charges #62

Merged
merged 7 commits into from
Apr 12, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions app/models/event.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,7 @@ class Event < ApplicationRecord

validates :transaction_id, presence: true, uniqueness: { scope: :organization_id }
validates :code, presence: true

scope :from_date, ->(from_date) { where('events.timestamp >= ?', from_date.beginning_of_day) }
scope :to_date, ->(to_date) { where('events.timestamp <= ?', to_date.end_of_day) }
end
1 change: 1 addition & 0 deletions app/models/fee.rb
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ class Fee < ApplicationRecord
validates :vat_amount_currency, inclusion: { in: currency_list }

scope :subscription_kind, -> { where(charge_id: nil) }
scope :charge_kind, -> { where.not(charge_id: nil) }

def subscription_fee?
charge_id.blank?
Expand Down
23 changes: 23 additions & 0 deletions app/services/billable_metrics/aggregations/base_service.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# frozen_string_literal: true

module BillableMetrics
module Aggregations
class BaseService < ::BaseService
def initialize(billable_metric:, subscription:)
super(nil)
@billable_metric = billable_metric
@subscription = subscription
end

def aggregate(from_date:, to_date:)
raise NotImplementedError
end

protected

attr_accessor :billable_metric, :subscription

delegate :customer, to: :subscription
end
end
end
19 changes: 19 additions & 0 deletions app/services/billable_metrics/aggregations/count_service.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# frozen_string_literal: true

module BillableMetrics
module Aggregations
class CountService < BillableMetrics::Aggregations::BaseService
def aggregate(from_date:, to_date:)
# TODO: different behavior for one shot and recurring events

result.aggregation = customer.events
.from_date(from_date)
.to_date(to_date)
.where(code: billable_metric.code)
.count

result
end
end
end
end
20 changes: 20 additions & 0 deletions app/services/charges/charge_models/base_service.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# frozen_string_literal: true

module Charges
module ChargeModels
class BaseService < ::BaseService
def initialize(charge:)
super(nil)
@charge = charge
end

def apply(value:)
raise NotImplementedError
end

protected

attr_accessor :charge
end
end
end
12 changes: 12 additions & 0 deletions app/services/charges/charge_models/standard_service.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# frozen_string_literal: true

module Charges
module ChargeModels
class StandardService < Charges::ChargeModels::BaseService
def apply(value:)
result.amount_cents = value * charge.amount_cents
result
end
end
end
end
85 changes: 85 additions & 0 deletions app/services/fees/charge_service.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
# frozen_string_literal: true

module Fees
class ChargeService < BaseService
def initialize(invoice:, charge:)
@invoice = invoice
@charge = charge
super(nil)
end

def create
return result if already_billed?

new_amount_cents = compute_amount

new_fee = Fee.new(
invoice: invoice,
subscription: subscription,
charge: charge,
amount_cents: new_amount_cents,
amount_currency: charge.amount_currency,
vat_rate: charge.vat_rate,
)

new_fee.compute_vat
new_fee.save!

result.fee = new_fee
result
rescue ActiveRecord::RecordInvalid => e
result.fail_with_validations!(e.record)
end

private

attr_accessor :invoice, :charge

delegate :plan, :subscription, to: :invoice
delegate :billable_metric, to: :charge

def compute_amount
aggregated_events = aggregator.aggregate(from_date: invoice.from_date, to_date: invoice.to_date)
return result.fail!('aggregation_failure') unless aggregated_events.success?

amount_result = charge_model.apply(value: aggregated_events.aggregation)
return result.fail!('charge_model_failure') unless amount_result.success?

amount_result.amount_cents
end

def already_billed?
existing_fee = invoice.fees.find_by(charge_id: charge.id)
return false unless existing_fee

result.fee = existing_fee
true
end

def aggregator
return @aggregator if @aggregator

aggregator_service = case billable_metric.aggregation_type.to_sym
when :count_agg
BillableMetrics::Aggregations::CountService
else
raise NotImplementedError
end

@aggregator = aggregator_service.new(billable_metric: billable_metric, subscription: subscription)
end

def charge_model
return @charge_model if @charge_model

model_service = case charge.charge_model.to_sym
vincent-pochet marked this conversation as resolved.
Show resolved Hide resolved
when :standard
Charges::ChargeModels::StandardService
else
raise NotImplementedError
end

@charge_model = model_service.new(charge: charge)
end
end
end
34 changes: 25 additions & 9 deletions app/services/invoices/create_service.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,18 +10,22 @@ def initialize(subscription:, timestamp:)
end

def create
invoice = Invoice.find_or_create_by!(
subscription: subscription,
from_date: from_date,
to_date: to_date,
issuing_date: issuing_date,
)
ActiveRecord::Base.transaction do
invoice = Invoice.find_or_create_by!(
subscription: subscription,
from_date: from_date,
to_date: to_date,
issuing_date: issuing_date,
)

Fees::SubscriptionService.new(invoice).create
create_subscription_fee(invoice)
create_charges_fees(invoice)

compute_amounts(invoice)
compute_amounts(invoice)

result.invoice = invoice
end

result.invoice = invoice
result
rescue ActiveRecord::RecordInvalid => e
result.fail_with_validations!(e.record)
Expand Down Expand Up @@ -91,5 +95,17 @@ def compute_amounts(invoice)

invoice.save!
end

def create_subscription_fee(invoice)
fee_result = Fees::SubscriptionService.new(invoice).create
result.throw_error unless fee_result.success?
end

def create_charges_fees(invoice)
subscription.plan.charges.each do |charge|
fee_result = Fees::ChargeService.new(invoice: invoice, charge: charge).create
result.throw_error unless fee_result.success?
end
end
end
end
4 changes: 3 additions & 1 deletion spec/factories/charge_factory.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,10 @@

amount_cents { Faker::Number.between(from: 100, to: 500) }
amount_currency { 'EUR' }

vat_rate { 20 }

pro_rata { false }
charge_model { 'standard' }

factory :one_time_charge do
frequency { :one_time }
Expand Down
51 changes: 51 additions & 0 deletions spec/services/billable_metrics/aggregations/count_service_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
# frozen_string_literal: true

require 'rails_helper'

RSpec.describe BillableMetrics::Aggregations::CountService, type: :service do
subject(:count_service) do
described_class.new(billable_metric: billable_metric, subscription: subscription)
end

let(:subscription) { create(:subscription) }
let(:organization) { subscription.organization }
let(:customer) { subscription.customer }

let(:billable_metric) do
create(
:billable_metric,
organization: organization,
aggregation_type: 'count_agg',
billable_period: 'one_shot',
)
end

let(:from_date) { Time.zone.today - 1.month }
let(:to_date) { Time.zone.today }

before do
create_list(
:event,
4,
code: billable_metric.code,
customer: customer,
timestamp: Time.zone.now,
)
end

it 'aggregates the events' do
result = count_service.aggregate(from_date: from_date, to_date: to_date)

expect(result.aggregation).to eq(4)
end

context 'when events are out of bounds' do
let(:to_date) { Time.zone.now - 2.days }

it 'does not take events into account' do
result = count_service.aggregate(from_date: from_date, to_date: to_date)

expect(result.aggregation).to eq(0)
end
end
end
19 changes: 19 additions & 0 deletions spec/services/charges/charge_models/standard_service_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# frozen_string_literal: true

require 'rails_helper'

RSpec.describe Charges::ChargeModels::StandardService, type: :service do
subject(:standard_service) { described_class.new(charge: charge) }

let(:charge) do
create(
:one_time_charge,
amount_cents: 500,
charge_model: 'standard',
)
end

it 'apply the charge model to the value' do
expect(standard_service.apply(value: 10).amount_cents).to eq(5000)
end
end
49 changes: 49 additions & 0 deletions spec/services/fees/charge_service_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
# frozen_string_literal: true

require 'rails_helper'

RSpec.describe Fees::ChargeService do
subject(:charge_subscription_service) do
described_class.new(invoice: invoice, charge: charge)
end

let(:subscription) { create(:subscription) }
let(:invoice) { create(:invoice, subscription: subscription) }
let(:billable_metric) { create(:billable_metric, aggregation_type: 'count_agg') }
let(:charge) { create(:one_time_charge, plan: subscription.plan, charge_model: 'standard') }

describe '.create' do
it 'creates a fee' do
result = charge_subscription_service.create

expect(result).to be_success

created_fee = result.fee

aggregate_failures do
expect(created_fee.id).not_to be_nil
expect(created_fee.invoice_id).to eq(invoice.id)
expect(created_fee.charge_id).to eq(charge.id)
expect(created_fee.amount_cents).to eq(0)
expect(created_fee.amount_currency).to eq('EUR')
expect(created_fee.vat_amount_cents).to eq(0)
expect(created_fee.vat_rate).to eq(20.0)
end
end

context 'when fee already exists on the period' do
before do
create(
:fee,
charge: charge,
subscription: subscription,
invoice: invoice,
)
end

it 'does not create a new fee' do
expect { charge_subscription_service.create }.not_to change(Fee, :count)
end
end
end
end
Loading