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-955 Add service to find billable subscriptions #46

Merged
merged 5 commits into from
Apr 1, 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
1 change: 1 addition & 0 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ ruby '3.0.1'

gem 'bcrypt'
gem 'bootsnap', require: false
gem 'clockwork', require: false
gem 'graphql'
gem 'graphql-pagination'
gem 'jwt'
Expand Down
4 changes: 4 additions & 0 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,9 @@ GEM
msgpack (~> 1.2)
builder (3.2.4)
byebug (11.1.3)
clockwork (3.0.0)
activesupport
tzinfo
coffee-rails (5.0.0)
coffee-script (>= 2.2.0)
railties (>= 5.2.0)
Expand Down Expand Up @@ -256,6 +259,7 @@ DEPENDENCIES
bcrypt
bootsnap
byebug
clockwork
coffee-rails
debug
dotenv
Expand Down
9 changes: 9 additions & 0 deletions app/jobs/bill_subscription_job.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# frozen_string_literal: true

class BillSubscriptionJob < ApplicationJob
queue_as 'billing'

def perform(subscription, timestamp)
# TODO
end
end
11 changes: 11 additions & 0 deletions app/jobs/clock/subscriptions_biller_job.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# frozen_string_literal: true

module Clock
class SubscriptionsBillerJob < ApplicationJob
queue_as 'clock'

def perform
BillingService.new.call
end
end
end
3 changes: 1 addition & 2 deletions app/models/plan.rb
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,7 @@ class Plan < ApplicationRecord

BILLING_PERIODS = %i[
beginning_of_period
end_of_period
subscruption_date
subscription_date
].freeze

enum frequency: FREQUENCIES
Expand Down
72 changes: 72 additions & 0 deletions app/services/billing_service.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
# frozen_string_literal: true

class BillingService
def call
# Keep track of billing time for retry and tracking purpose
billing_timestamp = Time.zone.now.to_i

billable_subscriptions.find_each do |subscription|
BillSubscriptionJob
.set(wait: rand(240).minutes)
.perform_later(subscription, billing_timestamp)
end
end

private

# Retrieve list of subscription that should be billed today
def billable_subscriptions
sql = []
today = Time.zone.now

# =================================
# Billed on the beginning of period
# =================================
# We are on the first day of the month
if today.day == 1
# Billed monthly
sql << Subscription.active.joins(:plan)
.merge(Plan.monthly.beginning_of_period)
.select(:id).to_sql

# We are on the first day of the year
if today.month == 1
# Billed yearly
sql << Subscription.active.joins(:plan)
.merge(Plan.yearly.beginning_of_period)
.select(:id).to_sql
end
end

# =================================
# Billed on the subscription anniversary
# =================================

# Billed monthly
days = [today.day]

# If today is the last day of the month and month count less than 31 days,
# we need to take all days up to 31 into account
((today.day + 1)..31).each { |day| days << day } if today.day == today.end_of_month.day

sql << Subscription.active.joins(:plan)
.merge(Plan.monthly.subscription_date)
.where('DATE_PART(\'day\', subscriptions.started_at) IN (?)', days)
.select(:id).to_sql

# Billed yearly
days = [today.day]

# If we are not in leap year and we are on 28/02 take 29/02 into account
days << 29 if !Date.leap?(today.year) && today.day == 28 && today.month == 2

sql << Subscription.active.joins(:plan)
.merge(Plan.yearly.subscription_date)
.where('DATE_PART(\'month\', subscriptions.started_at) = ?', today.month)
.where('DATE_PART(\'day\', subscriptions.started_at) IN (?)', days)
.select(:id).to_sql

# Query subscriptions by ids
Subscription.where("id in (#{sql.join(' UNION ')})")
end
end
19 changes: 19 additions & 0 deletions clock.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# frozen_string_literal: true

require 'clockwork'
require './config/boot'
require './config/environment'

module Clockwork
handler do |job, time|
puts "Running #{job} at #{time}"
end

error_handler do |error|
# TODO: plug error handler here
end

every(1.day, 'schedule:bill_customers', at: '01:00') do
Clock::SubscriptionsBillerJob.perform_later
end
end
2 changes: 2 additions & 0 deletions config/environments/test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -59,4 +59,6 @@
# config.action_view.annotate_rendered_view_with_filenames = true

Dotenv.load

config.active_job.queue_adapter = :test
end
4 changes: 3 additions & 1 deletion config/sidekiq.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,10 @@ timeout: 25
retry: 1
queues:
- default
- clock
- billing

production:
concurrency: 10
staging:
concurrency: 10
concurrency: 10
3 changes: 1 addition & 2 deletions schema.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,7 @@ type BillableMetricCollection {

enum BillingPeriodEnum {
beginning_of_period
end_of_period
subscruption_date
subscription_date
}

type Charge {
Expand Down
8 changes: 1 addition & 7 deletions schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -266,13 +266,7 @@
"deprecationReason": null
},
{
"name": "end_of_period",
"description": null,
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "subscruption_date",
"name": "subscription_date",
"description": null,
"isDeprecated": false,
"deprecationReason": null
Expand Down
3 changes: 2 additions & 1 deletion spec/factories/plan_factory.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,11 @@

FactoryBot.define do
factory :plan do
organization
name { Faker::TvShows::SiliconValley.app }
code { Faker::Name.name.underscore }
frequency { 'monthly' }
billing_period { 'end_of_period' }
billing_period { 'beginning_of_period' }
pro_rata { false }
amount_cents { 100 }
amount_currency { 'EUR' }
Expand Down
1 change: 1 addition & 0 deletions spec/factories/subscription_factory.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,6 @@
factory :subscription do
customer
plan
status { :active }
end
end
8 changes: 4 additions & 4 deletions spec/graphql/mutations/plans/create_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@
name: 'New Plan',
code: 'new_plan',
frequency: 'monthly',
billingPeriod: 'end_of_period',
billingPeriod: 'beginning_of_period',
proRata: false,
amountCents: 200,
amountCurrency: 'EUR',
Expand Down Expand Up @@ -69,7 +69,7 @@
expect(result_data['name']).to eq('New Plan')
expect(result_data['code']).to eq('new_plan')
expect(result_data['frequency']).to eq('monthly')
expect(result_data['billingPeriod']).to eq('end_of_period')
expect(result_data['billingPeriod']).to eq('beginning_of_period')
expect(result_data['proRata']).to eq(false)
expect(result_data['amountCents']).to eq(200)
expect(result_data['amountCurrency']).to eq('EUR')
Expand All @@ -87,7 +87,7 @@
name: 'New Plan',
code: 'new_plan',
frequency: 'monthly',
billingPeriod: 'end_of_period',
billingPeriod: 'beginning_of_period',
proRata: false,
amountCents: 200,
amountCurrency: 'EUR',
Expand All @@ -110,7 +110,7 @@
name: 'New Plan',
code: 'new_plan',
frequency: 'monthly',
billingPeriod: 'end_of_period',
billingPeriod: 'beginning_of_period',
proRata: false,
amountCents: 200,
amountCurrency: 'EUR',
Expand Down
6 changes: 3 additions & 3 deletions spec/graphql/mutations/plans/update_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@
name: 'Updated plan',
code: 'new_plan',
frequency: 'monthly',
billingPeriod: 'end_of_period',
billingPeriod: 'beginning_of_period',
proRata: false,
amountCents: 200,
amountCurrency: 'EUR',
Expand Down Expand Up @@ -70,7 +70,7 @@
expect(result_data['name']).to eq('Updated plan')
expect(result_data['code']).to eq('new_plan')
expect(result_data['frequency']).to eq('monthly')
expect(result_data['billingPeriod']).to eq('end_of_period')
expect(result_data['billingPeriod']).to eq('beginning_of_period')
expect(result_data['proRata']).to eq(false)
expect(result_data['amountCents']).to eq(200)
expect(result_data['amountCurrency']).to eq('EUR')
Expand All @@ -88,7 +88,7 @@
name: 'Updated plan',
code: 'new_plan',
frequency: 'monthly',
billingPeriod: 'end_of_period',
billingPeriod: 'beginning_of_period',
proRata: false,
amountCents: 200,
amountCurrency: 'EUR',
Expand Down
1 change: 1 addition & 0 deletions spec/rails_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
config.include FactoryBot::Syntax::Methods
config.include GraphQLHelper, type: :graphql
config.include ApiHelper, type: :request
config.include ActiveSupport::Testing::TimeHelpers

# Remove this line if you're not using ActiveRecord or ActiveRecord fixtures
config.fixture_path = "#{::Rails.root}/spec/fixtures"
Expand Down
Loading