fix: Show cancellation message when subscription is pending cancellation (#752)

* fix: Show cancellation message when subscription is pending cancellation

When a subscription is cancelled via Stripe, the UI incorrectly showed
"Your contribution continues on..." instead of reflecting the cancellation
status. This fix adds tracking of `cancel_at_period_end` from Stripe webhooks
and displays "Your contribution ends on..." when a subscription has been
cancelled but is still active until the billing period ends.

https://claude.ai/code/session_01Y8ELTdK1k9o315iSq43TRN

* chore: Update schema.rb with cancel_at_period_end column

https://claude.ai/code/session_01Y8ELTdK1k9o315iSq43TRN

* Schema version

---------

Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
Juan José Mata
2026-01-23 18:55:51 +01:00
committed by GitHub
parent 8b3ebd7988
commit 6b5a5b1877
8 changed files with 71 additions and 3 deletions

View File

@@ -53,6 +53,10 @@ module Family::Subscribeable
subscription&.current_period_ends_at
end
def subscription_pending_cancellation?
subscription&.pending_cancellation?
end
def start_subscription!(stripe_subscription_id)
if subscription.present?
subscription.update!(status: "active", stripe_id: stripe_subscription_id)

View File

@@ -10,7 +10,8 @@ class Provider::Stripe::SubscriptionEventProcessor < Provider::Stripe::EventProc
interval: subscription_details.plan.interval,
amount: subscription_details.plan.amount / 100.0, # Stripe returns cents, we report dollars
currency: subscription_details.plan.currency.upcase,
current_period_ends_at: Time.at(subscription_details.current_period_end)
current_period_ends_at: Time.at(subscription_details.current_period_end),
cancel_at_period_end: subscription.cancel_at_period_end
)
end

View File

@@ -35,4 +35,8 @@ class Subscription < ApplicationRecord
"Open demo"
end
end
def pending_cancellation?
active? && cancel_at_period_end?
end
end

View File

@@ -16,7 +16,11 @@
<span>Currently on the <span class="font-medium"><%= @family.subscription.name %></span>.</span> <br />
<% if @family.next_payment_date %>
<span><%= t("views.settings.payments.renewal", date: l(@family.next_payment_date, format: :long)) %></span>
<% if @family.subscription_pending_cancellation? %>
<span><%= t("views.settings.payments.cancellation", date: l(@family.next_payment_date, format: :long)) %></span>
<% else %>
<span><%= t("views.settings.payments.renewal", date: l(@family.next_payment_date, format: :long)) %></span>
<% end %>
<% end %>
</p>
<% elsif @family.trialing? %>

View File

@@ -4,6 +4,7 @@ en:
settings:
payments:
renewal: "Your contribution continues on %{date}."
cancellation: "Your contribution ends on %{date}."
settings:
ai_prompts:
show:

View File

@@ -0,0 +1,5 @@
class AddCancelAtPeriodEndToSubscriptions < ActiveRecord::Migration[7.2]
def change
add_column :subscriptions, :cancel_at_period_end, :boolean, default: false, null: false
end
end

3
db/schema.rb generated
View File

@@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema[7.2].define(version: 2026_01_22_160000) do
ActiveRecord::Schema[7.2].define(version: 2026_01_23_000000) do
# These are extensions that must be enabled in order to support this database
enable_extension "pgcrypto"
enable_extension "plpgsql"
@@ -1259,6 +1259,7 @@ ActiveRecord::Schema[7.2].define(version: 2026_01_22_160000) do
t.datetime "trial_ends_at"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.boolean "cancel_at_period_end", default: false, null: false
t.index ["family_id"], name: "index_subscriptions_on_family_id", unique: true
end

View File

@@ -13,6 +13,7 @@ class Provider::Stripe::SubscriptionEventProcessorTest < ActiveSupport::TestCase
id: test_subscription_id,
status: "active",
customer: test_customer_id,
cancel_at_period_end: false,
items: {
data: [
{
@@ -53,5 +54,52 @@ class Provider::Stripe::SubscriptionEventProcessorTest < ActiveSupport::TestCase
assert_equal 9, family.subscription.amount
assert_equal "USD", family.subscription.currency
assert family.subscription.current_period_ends_at > 20.days.from_now
assert_equal false, family.subscription.cancel_at_period_end
end
test "handles subscription cancellation at period end" do
test_customer_id = "test-customer-id-cancel"
test_subscription_id = "test-subscription-id-cancel"
mock_event = JSON.parse({
type: "customer.subscription.updated",
data: {
object: {
id: test_subscription_id,
status: "active",
customer: test_customer_id,
cancel_at_period_end: true,
items: {
data: [
{
current_period_end: 1.month.from_now.to_i,
plan: {
interval: "month",
amount: 900,
currency: "usd"
}
}
]
}
}
}
}.to_json, object_class: OpenStruct)
family = Family.create!(
name: "Test Cancelling Family",
stripe_customer_id: test_customer_id
)
family.start_subscription!(test_subscription_id)
processor = Provider::Stripe::SubscriptionEventProcessor.new(mock_event)
processor.process
family.reload
assert_equal "active", family.subscription.status
assert_equal true, family.subscription.cancel_at_period_end
assert family.subscription.pending_cancellation?
assert family.subscription_pending_cancellation?
end
end