Skip to content
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
78 changes: 65 additions & 13 deletions app/services/subscriptions/activate_service.rb
Original file line number Diff line number Diff line change
Expand Up @@ -53,10 +53,7 @@ def activate_from_incomplete
def gate_subscription
subscription.mark_as_incomplete!(timestamp)

EmitFixedChargeEventsService.call!(
subscriptions: [subscription],
timestamp: subscription.started_at + 1.second
)
emit_fixed_charge_events

after_commit do
bill_subscription(skip_charges: true) if subscription.payment_gated?
Expand All @@ -67,33 +64,88 @@ def gate_subscription
end

def activate_with_side_effects
if upgrade?
activate_for_upgrade
else
activate_standalone
end
end

def upgrade?
return false unless subscription.previous_subscription
return false if subscription.plan.id == subscription.previous_subscription.plan.id

subscription.plan.yearly_amount_cents >= subscription.previous_subscription.plan.yearly_amount_cents
end

def activate_standalone
subscription.mark_as_active!(timestamp)

EmitFixedChargeEventsService.call!(
subscriptions: [subscription],
timestamp: subscription.started_at + 1.second
)
emit_fixed_charge_events

after_commit do
bill_subscription(skip_charges: true)
notify_started
end
end

def activate_for_upgrade
Subscriptions::TerminateService.call(
subscription: subscription.previous_subscription,
upgrade: true
)

subscription.mark_as_active!(timestamp)

emit_fixed_charge_events

after_commit { notify_started }

bill_upgrade_subscriptions
end

def billable_subscriptions
@billable_subscriptions ||= begin
billable = [subscription.previous_subscription]
bill_new_in_advance = subscription.fixed_charges.pay_in_advance.any? ||
(subscription.plan.pay_in_advance? && !subscription.in_trial_period?)
billable << subscription if bill_new_in_advance
billable
end
end

def bill_upgrade_subscriptions
after_commit do
billing_at = Time.current + 1.second
BillSubscriptionJob.perform_later(billable_subscriptions, billing_at.to_i, invoicing_reason: :upgrading)
BillNonInvoiceableFeesJob.perform_later(billable_subscriptions, billing_at)
end
end

def notify_started
SendWebhookJob.perform_later("subscription.started", subscription)
Utils::ActivityLog.produce(subscription, "subscription.started")

# Skip Hubspot UpdateJob when activating during subscription creation —
# CreateService fires Hubspot::CreateJob after this, which captures the
# active state and avoids a redundant Update that would race with Create.
return if during_creation
return unless subscription.should_sync_hubspot_subscription?

if subscription.should_sync_hubspot_subscription?
if upgrade?
# The new upgrade subscription has no Hubspot record yet.
Integrations::Aggregator::Subscriptions::Hubspot::CreateJob.perform_later(subscription:)
elsif !during_creation
# Skip when activating during subscription creation — CreateService
# fires Hubspot::CreateJob after this, which captures the active state
# and avoids a redundant Update that would race with Create.
Integrations::Aggregator::Subscriptions::Hubspot::UpdateJob.perform_later(subscription:)
end
end

def emit_fixed_charge_events
EmitFixedChargeEventsService.call!(
subscriptions: [subscription],
timestamp: subscription.started_at + 1.second
)
end

def bill_subscription(skip_charges: false)
if subscription.plan.pay_in_advance? && !subscription.in_trial_period?
BillSubscriptionJob.perform_later(
Expand Down
54 changes: 6 additions & 48 deletions app/services/subscriptions/plan_upgrade_service.rb
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ def initialize(current_subscription:, plan:, params:)
def call
ActiveRecord::Base.transaction do
if current_subscription.starting_in_the_future?
apply_activation_rules if params[:activation_rules]
apply_activation_rules(current_subscription) if params[:activation_rules]
update_pending_subscription

result.subscription = current_subscription
Expand All @@ -29,30 +29,13 @@ def call

cancel_pending_subscription if pending_subscription?

# Group subscriptions for billing
billable_subscriptions = billable_subscriptions(new_subscription)
new_subscription.pending!

# Terminate current subscription as part of the upgrade process
Subscriptions::TerminateService.call(
subscription: current_subscription,
upgrade: true
)
apply_activation_rules(new_subscription) if params[:activation_rules].present?

new_subscription.mark_as_active!

EmitFixedChargeEventsService.call!(
subscriptions: [new_subscription],
timestamp: new_subscription.started_at + 1.second
)
Subscriptions::ActivateService.call!(subscription: new_subscription)

result.subscription = new_subscription

after_commit do
SendWebhookJob.perform_later("subscription.started", new_subscription)
Utils::ActivityLog.produce(new_subscription, "subscription.started")
end

bill_subscriptions(billable_subscriptions) if billable_subscriptions.any?
end

result
Expand Down Expand Up @@ -100,9 +83,9 @@ def update_pending_subscription
end
end

def apply_activation_rules
def apply_activation_rules(subscription)
Subscriptions::ActivationRules::ApplyService.call!(
subscription: current_subscription,
subscription:,
activation_rules: params[:activation_rules]
)
end
Expand All @@ -120,30 +103,5 @@ def pending_subscription?

current_subscription.next_subscription.pending?
end

def billable_subscriptions(new_subscription)
billable_subscriptions = if current_subscription.starting_in_the_future?
[]
elsif current_subscription.pending?
[]
elsif !current_subscription.terminated?
[current_subscription]
end.to_a

has_billable_fixed_charges = new_subscription.fixed_charges.pay_in_advance.any?
plan_billable = new_subscription.plan.pay_in_advance? && !new_subscription.in_trial_period?

billable_subscriptions << new_subscription if has_billable_fixed_charges || plan_billable

billable_subscriptions
end

def bill_subscriptions(billable_subscriptions)
after_commit do
billing_at = Time.current + 1.second
BillSubscriptionJob.perform_later(billable_subscriptions, billing_at.to_i, invoicing_reason: :upgrading)
BillNonInvoiceableFeesJob.perform_later(billable_subscriptions, billing_at)
end
end
end
end
3 changes: 3 additions & 0 deletions app/services/subscriptions/terminate_service.rb
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,9 @@ def terminate_and_start_next(timestamp:)
attr_reader :subscription, :async, :upgrade, :on_termination_credit_note, :on_termination_invoice

def cancel_next_subscription
# NOTE: Upgrade path: next_subscription is the new subscription we just persisted, not a stale scheduled change
return if upgrade

next_subscription = subscription.next_subscription
return if next_subscription.nil?

Expand Down
140 changes: 140 additions & 0 deletions spec/services/subscriptions/activate_service_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -380,4 +380,144 @@
expect(SendWebhookJob).not_to have_been_enqueued
end
end

context "when subscription comes from an upgrade" do
Comment thread
osmarluz marked this conversation as resolved.
let(:customer) { create(:customer, :with_hubspot_integration, organization:) }
let(:previous_plan) { create(:plan, organization:, amount_cents: 50) }
let(:plan) { create(:plan, organization:, amount_cents: 100) }
let(:previous_subscription) do
create(
:subscription,
organization:,
customer:,
plan: previous_plan,
status: :active,
started_at: 1.day.ago,
subscription_at: 1.day.ago
)
end
let(:subscription) do
create(
:subscription,
:pending,
organization:,
customer:,
plan:,
previous_subscription:,
subscription_at: Time.current
)
end

it "terminates the previous subscription" do
result

expect(previous_subscription.reload).to be_terminated
end

it "marks the new subscription as active" do
freeze_time do
expect(result.subscription).to be_active
expect(result.subscription.started_at).to eq(Time.current)
end
end

it "enqueues Hubspot::CreateJob for the new subscription" do
result

expect(Integrations::Aggregator::Subscriptions::Hubspot::CreateJob)
.to have_been_enqueued.with(subscription: subscription)
end

context "when subscription should not sync with hubspot" do
let(:customer) { create(:customer, organization:) }

it "does not enqueue Hubspot::CreateJob" do
result

expect(Integrations::Aggregator::Subscriptions::Hubspot::CreateJob).not_to have_been_enqueued
end
end

it "sends a subscription.started webhook for the new subscription" do
result

expect(SendWebhookJob).to have_been_enqueued.with("subscription.started", subscription)
end

it "produces a subscription.started activity log" do
result

expect(Utils::ActivityLog).to have_produced("subscription.started").with(subscription)
end

it "enqueues BillSubscriptionJob with invoicing_reason :upgrading" do
result

expect(BillSubscriptionJob).to have_been_enqueued
.with([previous_subscription], anything, invoicing_reason: :upgrading)
end

it "enqueues BillNonInvoiceableFeesJob" do
result

expect(BillNonInvoiceableFeesJob).to have_been_enqueued
end

context "when the new plan is pay in advance" do
let(:plan) { create(:plan, organization:, amount_cents: 100, pay_in_advance: true) }

it "includes both previous and new subscription in the upgrade bill" do
result

expect(BillSubscriptionJob).to have_been_enqueued
.with([previous_subscription, subscription], anything, invoicing_reason: :upgrading)
end
end

context "when activation_rules gate the new subscription" do
let(:plan) { create(:plan, organization:, amount_cents: 100, pay_in_advance: true) }
let(:subscription) do
create(
:subscription,
:pending,
:with_activation_rules,
organization:,
customer:,
plan:,
previous_subscription:,
subscription_at: Time.current
)
end

it "marks the new subscription as incomplete" do
expect(result.subscription).to be_incomplete
end

it "does not terminate the previous subscription" do
result

expect(previous_subscription.reload).to be_active
end

it "sends a subscription.incomplete webhook" do
result

expect(SendWebhookJob).to have_been_enqueued.with("subscription.incomplete", subscription)
end

it "enqueues BillSubscriptionJob for the incomplete subscription with skip_charges" do
result

expect(BillSubscriptionJob).to have_been_enqueued
.with([subscription], anything, invoicing_reason: :subscription_starting, skip_charges: true)
end

it "does not enqueue a BillSubscriptionJob with invoicing_reason :upgrading" do
result

expect(BillSubscriptionJob).not_to have_been_enqueued
.with(anything, anything, invoicing_reason: :upgrading)
end
end
end
end
Loading
Loading