Skip to content

Commit

Permalink
feat(data): Add usage_date to daily_usages (#2987)
Browse files Browse the repository at this point in the history
## Description

The goal of this PR is to add `usage_date` column to the `daily_usages`
table.
It will help fetching the correct usage for a specific date, without
computing the `refreshed_at` values.

Next PR will remove use of `refreshed_at` in favor of `usage_date`.
  • Loading branch information
rsempe authored Dec 22, 2024
1 parent c771177 commit b595ddd
Show file tree
Hide file tree
Showing 12 changed files with 95 additions and 46 deletions.
4 changes: 2 additions & 2 deletions app/jobs/clock/compute_all_daily_usages_job.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,15 @@ class ComputeAllDailyUsagesJob < ApplicationJob
include SentryCronConcern

queue_as do
if ActiveModel::Type::Boolean.new.cast(ENV['SIDEKIQ_CLOCK'])
if ActiveModel::Type::Boolean.new.cast(ENV["SIDEKIQ_CLOCK"])
:clock_worker
else
:clock
end
end

def perform
DailyUsages::ComputeAllService.call
DailyUsages::ComputeAllService.call(timestamp: Time.current)
end
end
end
2 changes: 1 addition & 1 deletion app/jobs/daily_usages/compute_job.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

module DailyUsages
class ComputeJob < ApplicationJob
queue_as 'low_priority'
queue_as "low_priority"

def perform(subscription, timestamp:)
DailyUsages::ComputeService.call(subscription:, timestamp:).raise_if_error!
Expand Down
2 changes: 2 additions & 0 deletions app/models/daily_usage.rb
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ class DailyUsage < ApplicationRecord
# refreshed_at :datetime not null
# to_datetime :datetime not null
# usage :jsonb not null
# usage_date :date
# usage_diff :jsonb not null
# created_at :datetime not null
# updated_at :datetime not null
Expand All @@ -37,6 +38,7 @@ class DailyUsage < ApplicationRecord
# index_daily_usages_on_customer_id (customer_id)
# index_daily_usages_on_organization_id (organization_id)
# index_daily_usages_on_subscription_id (subscription_id)
# index_daily_usages_on_usage_date (usage_date)
#
# Foreign Keys
#
Expand Down
9 changes: 6 additions & 3 deletions app/services/daily_usages/compute_service.rb
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,8 @@ def call
usage: ::V1::Customers::UsageSerializer.new(current_usage, includes: %i[charges_usage]).serialize,
from_datetime: current_usage.from_datetime,
to_datetime: current_usage.to_datetime,
refreshed_at: timestamp
refreshed_at: timestamp,
usage_date: date_in_timezone - 1.day
)

daily_usage.usage_diff = diff_usage(daily_usage)
Expand Down Expand Up @@ -73,8 +74,6 @@ def diff_usage(daily_usage)
end

def subscription_billing_day?
date_in_timezone = timestamp.in_time_zone(customer.applicable_timezone).to_date

previous_billing_date_in_timezone = Subscriptions::DatesService
.new_instance(subscription, timestamp, current_usage: true)
.previous_beginning_of_period
Expand All @@ -83,5 +82,9 @@ def subscription_billing_day?

date_in_timezone == previous_billing_date_in_timezone
end

def date_in_timezone
@date_in_timezone ||= timestamp.in_time_zone(customer.applicable_timezone).to_date
end
end
end
3 changes: 2 additions & 1 deletion app/services/daily_usages/fill_from_invoice_service.rb
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,8 @@ def call
usage: ::V1::Customers::UsageSerializer.new(usage, includes: %i[charges_usage]).serialize,
from_datetime: invoice_subscription.from_datetime,
to_datetime: invoice_subscription.to_datetime,
refreshed_at: invoice_subscription.timestamp
refreshed_at: invoice_subscription.timestamp,
usage_date: invoice_subscription.charges_to_datetime
)

daily_usage.usage_diff = diff_usage(daily_usage)
Expand Down
4 changes: 3 additions & 1 deletion app/services/daily_usages/fill_history_service.rb
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ def call
datetime = date.in_time_zone(subscription.customer.applicable_timezone).beginning_of_day.utc

next if date == Time.zone.today ||
subscription.daily_usages.where(usage_date: datetime.to_date - 1.day).exists? ||
DailyUsage.refreshed_at_in_timezone(datetime).where(subscription_id: subscription.id).exists?

Timecop.thread_safe = true
Expand All @@ -44,7 +45,8 @@ def call
from_datetime: usage.from_datetime,
to_datetime: usage.to_datetime,
refreshed_at: datetime,
usage_diff: {}
usage_diff: {},
usage_date: datetime.to_date - 1.day
)

if date != from
Expand Down
9 changes: 9 additions & 0 deletions db/migrate/20241219145642_add_usage_date_to_daily_usages.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# frozen_string_literal: true

class AddUsageDateToDailyUsages < ActiveRecord::Migration[7.1]
def change
safety_assured do
add_column :daily_usages, :usage_date, :date
end
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# frozen_string_literal: true

class AddIndexOnUsageDateToDailyUsages < ActiveRecord::Migration[7.1]
disable_ddl_transaction!

def change
safety_assured do
add_index :daily_usages, :usage_date, algorithm: :concurrently
end
end
end
2 changes: 2 additions & 0 deletions db/schema.rb

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

13 changes: 7 additions & 6 deletions spec/jobs/clock/compute_all_daily_usages_job_spec.rb
Original file line number Diff line number Diff line change
@@ -1,17 +1,18 @@
# frozen_string_literal: true

require 'rails_helper'
require "rails_helper"

RSpec.describe Clock::ComputeAllDailyUsagesJob, type: :job do
subject(:compute_job) { described_class }

describe '.perform' do
describe ".perform" do
before { allow(DailyUsages::ComputeAllService).to receive(:call) }

it 'removes all old webhooks' do
compute_job.perform_now

expect(DailyUsages::ComputeAllService).to have_received(:call)
it "calls DailyUsages::ComputeAllService" do
freeze_time do
compute_job.perform_now
expect(DailyUsages::ComputeAllService).to have_received(:call).with(timestamp: Time.current)
end
end
end
end
51 changes: 33 additions & 18 deletions spec/services/daily_usages/compute_service_spec.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# frozen_string_literal: true

require 'rails_helper'
require "rails_helper"

RSpec.describe DailyUsages::ComputeService, type: :service do
subject(:compute_service) { described_class.new(subscription:, timestamp:) }
Expand All @@ -12,10 +12,10 @@
create(:subscription, :calendar, customer:, plan:, started_at: 1.year.ago, subscription_at: 1.year.ago)
end

let(:timestamp) { Time.zone.parse('2024-10-22 00:05:00') }
let(:timestamp) { Time.zone.parse("2024-10-22 00:05:00") }

describe '#call' do
it 'creates a daily usage', aggregate_failures: true do
describe "#call" do
it "creates a daily usage", aggregate_failures: true do
travel_to(timestamp) do
expect { compute_service.call }.to change(DailyUsage, :count).by(1)

Expand All @@ -26,51 +26,52 @@
subscription_id: subscription.id,
external_subscription_id: subscription.external_id,
usage: Hash,
usage_diff: Hash
usage_diff: Hash,
usage_date: Date.parse("2024-10-21")
)
expect(daily_usage.refreshed_at).to match_datetime(timestamp)
expect(daily_usage.from_datetime).to match_datetime(timestamp.beginning_of_month)
expect(daily_usage.to_datetime).to match_datetime(timestamp.end_of_month)
end
end

context 'when a daily usage already exists' do
context "when a daily usage already exists" do
let(:existing_daily_usage) do
create(:daily_usage, subscription:, organization:, customer:, refreshed_at: timestamp)
end

before { existing_daily_usage }

it 'returns the existing daily usage', aggregate_failure: true do
it "returns the existing daily usage", aggregate_failure: true do
result = compute_service.call

expect(result).to be_success
expect(result.daily_usage).to eq(existing_daily_usage)
end

context 'when the organization has a timezone' do
let(:organization) { create(:organization, timezone: 'America/Sao_Paulo') }
context "when the organization has a timezone" do
let(:organization) { create(:organization, timezone: "America/Sao_Paulo") }

let(:existing_daily_usage) do
create(:daily_usage, subscription:, organization:, customer:, refreshed_at: timestamp - 4.hours)
end

it 'takes the timezone into account' do
it "takes the timezone into account" do
result = compute_service.call

expect(result).to be_success
expect(result.daily_usage).to eq(existing_daily_usage)
end
end

context 'when the customer has a timezone' do
let(:customer) { create(:customer, organization:, timezone: 'America/Sao_Paulo') }
context "when the customer has a timezone" do
let(:customer) { create(:customer, organization:, timezone: "America/Sao_Paulo") }

let(:existing_daily_usage) do
create(:daily_usage, subscription:, organization:, customer:, refreshed_at: timestamp - 4.hours)
end

it 'takes the timezone into account' do
it "takes the timezone into account" do
result = compute_service.call

expect(result).to be_success
Expand All @@ -79,26 +80,26 @@
end
end

context 'when timestamp is on subscription billing day' do
context "when timestamp is on subscription billing day" do
let(:subscription) do
create(:subscription, :anniversary, customer:, plan:, started_at: 1.year.ago, subscription_at: 1.year.ago)
end

let(:timestamp) { subscription.subscription_at + 1.year }

it 'does not create a daily usage' do
it "does not create a daily usage" do
expect { compute_service.call }.not_to change(DailyUsage, :count)
end
end

context 'when subscription is terminated after the timestamp' do
context "when subscription is terminated after the timestamp" do
let(:subscription) do
create(:subscription, :terminated, :calendar, customer:, plan:, started_at: 1.year.ago)
end

let(:timestamp) { subscription.terminated_at - 1.day }

it 'creates a daily usage', aggregate_failures: true do
it "creates a daily usage", aggregate_failures: true do
result = compute_service.call

expect(result).to be_success
Expand All @@ -110,12 +111,26 @@
subscription_id: subscription.id,
external_subscription_id: subscription.external_id,
usage: Hash,
usage_diff: Hash
usage_diff: Hash,
usage_date: timestamp.to_date - 1.day
)
expect(daily_usage.refreshed_at).to match_datetime(timestamp)
expect(daily_usage.from_datetime).to match_datetime(timestamp.beginning_of_month)
expect(daily_usage.to_datetime).to match_datetime(subscription.terminated_at)
end
end

context "with customer timezone" do
let(:customer) { create(:customer, organization:, timezone: "Australia/Sydney") }
let(:timestamp) { Time.zone.parse("2024-10-21 15:05:00") }

it "creates a daily usage with expected usage_date" do
expect { compute_service.call }.to change(DailyUsage, :count).by(1)

daily_usage = DailyUsage.order(created_at: :asc).last
# Timestamp is 22 Oct 2024 02:05:00 AEDT
expect(daily_usage.usage_date).to eq(Date.parse("2024-10-21"))
end
end
end
end
31 changes: 17 additions & 14 deletions spec/services/daily_usages/fill_from_invoice_service_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@

let(:subscriptions) { [subscription] }

let(:timestamp) { Time.zone.parse('2025-01-01T01:00:00') }
let(:timestamp) { Time.zone.parse("2025-01-01T01:00:00") }

let(:invoice) do
create(
Expand All @@ -28,10 +28,10 @@
subscription:,
invoice:,
timestamp:,
from_datetime: Time.zone.parse('2024-12-01T00:00:00'),
to_datetime: Time.zone.parse('2024-12-31T23:59:59'),
charges_from_datetime: Time.zone.parse('2024-12-01T00:00:00'),
charges_to_datetime: Time.zone.parse('2024-12-31T23:59:59')
from_datetime: Time.zone.parse("2024-12-01T00:00:00"),
to_datetime: Time.zone.parse("2024-12-31T23:59:59"),
charges_from_datetime: Time.zone.parse("2024-12-01T00:00:00"),
charges_to_datetime: Time.zone.parse("2024-12-31T23:59:59")
)
end

Expand All @@ -51,7 +51,8 @@
from_datetime: invoice_subscription.from_datetime,
to_datetime: invoice_subscription.to_datetime,
refreshed_at: invoice_subscription.timestamp,
usage_diff: Hash
usage_diff: Hash,
usage_date: invoice_subscription.charges_to_datetime.to_date
)
end

Expand All @@ -65,7 +66,8 @@
external_subscription_id: subscription.external_id,
from_datetime: invoice_subscription.from_datetime,
to_datetime: invoice_subscription.to_datetime,
refreshed_at: invoice_subscription.timestamp
refreshed_at: invoice_subscription.timestamp,
usage_date: invoice_subscription.charges_to_datetime.to_date
)
end

Expand All @@ -74,7 +76,7 @@
end
end

context 'when multiples subscriptions are passed to the service' do
context "when multiples subscriptions are passed to the service" do
let(:subscription2) { create(:subscription, customer:) }
let(:subscriptions) { [subscription, subscription2] }

Expand All @@ -84,10 +86,10 @@
subscription: subscription2,
invoice:,
timestamp:,
from_datetime: Time.zone.parse('2024-12-01T00:00:00'),
to_datetime: Time.zone.parse('2024-12-31T23:59:59'),
charges_from_datetime: Time.zone.parse('2024-12-01T00:00:00'),
charges_to_datetime: Time.zone.parse('2024-12-31T23:59:59')
from_datetime: Time.zone.parse("2024-12-01T00:00:00"),
to_datetime: Time.zone.parse("2024-12-31T23:59:59"),
charges_from_datetime: Time.zone.parse("2024-12-01T00:00:00"),
charges_to_datetime: Time.zone.parse("2024-12-31T23:59:59")
)
end

Expand All @@ -97,7 +99,7 @@
expect { fill_service.call }.to change(DailyUsage, :count).by(2)
end

context 'when only one subscription has to be updated' do
context "when only one subscription has to be updated" do
let(:subscriptions) { [subscription] }

it "creates daily usages for the subscriptions" do
Expand All @@ -113,7 +115,8 @@
from_datetime: invoice_subscription.from_datetime,
to_datetime: invoice_subscription.to_datetime,
refreshed_at: invoice_subscription.timestamp,
usage_diff: Hash
usage_diff: Hash,
usage_date: invoice_subscription.charges_to_datetime.to_date
)
end
end
Expand Down

0 comments on commit b595ddd

Please sign in to comment.