Skip to content
Closed
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
42 changes: 25 additions & 17 deletions app/services/subscriptions/activate_service.rb
Original file line number Diff line number Diff line change
Expand Up @@ -42,11 +42,15 @@ def activate_from_pending
def activate_from_incomplete
return if subscription.activation_rules.rejected.exists?

subscription.mark_as_active!(timestamp)
if upgrade?
activate_for_upgrade
else
subscription.mark_as_active!(timestamp)

after_commit do
bill_subscription if subscription.activation_rules.payment.none?
notify_started
after_commit do
bill_subscription if subscription.activation_rules.payment.none?
notify_started
end
end
end

Expand Down Expand Up @@ -90,35 +94,39 @@ def activate_standalone
end

def activate_for_upgrade
from_incomplete = subscription.incomplete?

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

subscription.mark_as_active!(timestamp)

emit_fixed_charge_events
billable_subscriptions = [subscription.previous_subscription]

# When from_incomplete, the new subscription was already billed and its fixed-charge
# events emitted during gate_subscription — only the previous needs billing.
unless from_incomplete
emit_fixed_charge_events
billable_subscriptions << subscription if bill_in_advance?
end

after_commit { notify_started }

bill_upgrade_subscriptions
bill_upgrade_subscriptions(billable_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
def bill_in_advance?
subscription.fixed_charges.pay_in_advance.any? ||
(subscription.plan.pay_in_advance? && !subscription.in_trial_period?)
end

def bill_upgrade_subscriptions
def bill_upgrade_subscriptions(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)
BillSubscriptionJob.perform_later(subscriptions, billing_at.to_i, invoicing_reason: :upgrading)
BillNonInvoiceableFeesJob.perform_later(subscriptions, billing_at)
end
end

Expand Down
103 changes: 103 additions & 0 deletions spec/scenarios/subscriptions/payment_gated_activation_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -328,4 +328,107 @@ def success_body_for(invoice)
expect(invoice.reload).to be_finalized
end
end

describe "plan upgrade with payment successful" do
let(:previous_plan) do
create(:plan, organization:, interval: "monthly", pay_in_advance: false, amount_cents: 500)
end
let(:upgrade_external_id) { "upgrade-sub-#{SecureRandom.hex(4)}" }

it "gates the upgrade, then terminates previous and activates new on payment success" do
# Stage 1: Create initial active subscription on cheaper pay-in-arrears plan (no rules)
create_subscription({
external_customer_id: customer.external_id,
external_id: upgrade_external_id,
plan_code: previous_plan.code,
billing_time: "calendar"
})
perform_all_enqueued_jobs

previous_subscription = customer.subscriptions.sole
expect(previous_subscription).to be_active
expect(previous_subscription.plan).to eq(previous_plan)

# Stage 2: Upgrade to pricier pay-in-advance plan with payment activation rule
create_subscription({
external_customer_id: customer.external_id,
external_id: upgrade_external_id,
plan_code: plan.code,
billing_time: "calendar",
activation_rules: [{type: "payment", timeout_hours: 48}]
})
perform_all_enqueued_jobs

new_subscription = customer.subscriptions.where.not(id: previous_subscription.id).sole
expect(previous_subscription.reload).to be_active
expect(new_subscription).to be_incomplete
expect(new_subscription.previous_subscription).to eq(previous_subscription)
expect(new_subscription.activation_rules.sole).to be_pending

invoice = new_subscription.invoices.sole
expect(invoice).to be_open
expect(invoice.fees.subscription.count).to eq(1)

# Stage 3: Stripe webhook — payment succeeded → upgrade completes
expect { simulate_stripe_webhook(status: "succeeded") }
.to have_performed_job(BillSubscriptionJob)
.with([previous_subscription], anything, invoicing_reason: :upgrading)

previous_subscription.reload
new_subscription.reload
expect(previous_subscription).to be_terminated
expect(new_subscription).to be_active
expect(new_subscription.activated_at).to be_present
expect(new_subscription.activation_rules.sole).to be_satisfied
expect(invoice.reload).to be_finalized
end
end

describe "plan upgrade with payment failure" do
let(:previous_plan) do
create(:plan, organization:, interval: "monthly", pay_in_advance: false, amount_cents: 500)
end
let(:upgrade_external_id) { "upgrade-sub-#{SecureRandom.hex(4)}" }

it "cancels the new subscription and leaves the previous untouched" do
# Stage 1: initial active subscription on cheaper plan
create_subscription({
external_customer_id: customer.external_id,
external_id: upgrade_external_id,
plan_code: previous_plan.code,
billing_time: "calendar"
})
perform_all_enqueued_jobs

previous_subscription = customer.subscriptions.sole
expect(previous_subscription).to be_active

# Stage 2: gated upgrade
create_subscription({
external_customer_id: customer.external_id,
external_id: upgrade_external_id,
plan_code: plan.code,
billing_time: "calendar",
activation_rules: [{type: "payment", timeout_hours: 48}]
})
perform_all_enqueued_jobs

new_subscription = customer.subscriptions.where.not(id: previous_subscription.id).sole
expect(new_subscription).to be_incomplete

invoice = new_subscription.invoices.sole
expect(invoice).to be_open

# Stage 3: Stripe webhook — payment failed
simulate_stripe_webhook(status: "failed")

previous_subscription.reload
new_subscription.reload
expect(new_subscription).to be_canceled
expect(new_subscription.cancelation_reason).to eq("payment_failed")
expect(new_subscription.activation_rules.sole).to be_failed
expect(previous_subscription).to be_active
expect(invoice.reload).to be_closed
end
end
end
71 changes: 71 additions & 0 deletions spec/services/subscriptions/activate_service_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -322,6 +322,77 @@
.to have_been_enqueued.with(subscription:)
end
end

context "when subscription comes from an upgrade" do
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, pay_in_advance: true) }
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, :incomplete, :with_activation_rules,
activation_rules_config: [{type: "payment", timeout_hours: 48, status: "satisfied"}],
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.activated_at).to eq(Time.current)
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 for the previous subscription only with :upgrading" do
result

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

it "enqueues BillNonInvoiceableFeesJob for the previous subscription only" do
result

expect(BillNonInvoiceableFeesJob).to have_been_enqueued.with([previous_subscription], anything)
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
end
end

context "when subscription is incomplete with no payment rules (future non-payment rule resolved)" do
Expand Down
Loading