diff --git a/saas/app/controllers/stripe/webhooks_controller.rb b/saas/app/controllers/stripe/webhooks_controller.rb
index 14c50c74d..22d8d4e0c 100644
--- a/saas/app/controllers/stripe/webhooks_controller.rb
+++ b/saas/app/controllers/stripe/webhooks_controller.rb
@@ -48,7 +48,8 @@ class Stripe::WebhooksController < ApplicationController
stripe_subscription_id: stripe_subscription.id,
status: stripe_subscription.status,
current_period_end: current_period_end_for(stripe_subscription),
- cancel_at: stripe_subscription.cancel_at ? Time.at(stripe_subscription.cancel_at) : nil
+ cancel_at: stripe_subscription.cancel_at ? Time.at(stripe_subscription.cancel_at) : nil,
+ next_amount_due_in_cents: next_amount_due_for(stripe_subscription)
}
yield subscription_properties if block_given?
@@ -66,4 +67,13 @@ class Stripe::WebhooksController < ApplicationController
timestamp = stripe_subscription.items.data.first&.current_period_end
Time.at(timestamp) if timestamp
end
+
+ def next_amount_due_for(stripe_subscription)
+ return nil if stripe_subscription.status == "canceled"
+
+ preview = Stripe::Invoice.create_preview(customer: stripe_subscription.customer, subscription: stripe_subscription.id)
+ preview.amount_due
+ rescue Stripe::InvalidRequestError
+ nil
+ end
end
diff --git a/saas/app/helpers/subscriptions_helper.rb b/saas/app/helpers/subscriptions_helper.rb
index 32dd45402..2c05b68eb 100644
--- a/saas/app/helpers/subscriptions_helper.rb
+++ b/saas/app/helpers/subscriptions_helper.rb
@@ -5,7 +5,7 @@ module SubscriptionsHelper
elsif subscription.canceled?
"Your Fizzy subscription ended on"
else
- "Your next payment of $#{ subscription.plan.price } will be billed on".html_safe
+ "Your next payment of #{ number_to_currency(subscription.next_amount_due) } will be billed on".html_safe
end
end
end
diff --git a/saas/app/models/account/subscription.rb b/saas/app/models/account/subscription.rb
index 937d11bcc..0eac71b02 100644
--- a/saas/app/models/account/subscription.rb
+++ b/saas/app/models/account/subscription.rb
@@ -14,4 +14,8 @@ class Account::Subscription < SaasRecord
def to_be_canceled?
active? && cancel_at.present?
end
+
+ def next_amount_due
+ next_amount_due_in_cents ? next_amount_due_in_cents / 100.0 : plan.price
+ end
end
diff --git a/saas/db/migrate/20251215170000_add_next_amount_due_in_cents_to_account_subscriptions.rb b/saas/db/migrate/20251215170000_add_next_amount_due_in_cents_to_account_subscriptions.rb
new file mode 100644
index 000000000..28838b314
--- /dev/null
+++ b/saas/db/migrate/20251215170000_add_next_amount_due_in_cents_to_account_subscriptions.rb
@@ -0,0 +1,5 @@
+class AddNextAmountDueInCentsToAccountSubscriptions < ActiveRecord::Migration[8.2]
+ def change
+ add_column :account_subscriptions, :next_amount_due_in_cents, :integer
+ end
+end
diff --git a/saas/db/saas_schema.rb b/saas/db/saas_schema.rb
index 15dea28fc..59c78f4cb 100644
--- a/saas/db/saas_schema.rb
+++ b/saas/db/saas_schema.rb
@@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.
-ActiveRecord::Schema[8.2].define(version: 2025_12_15_160000) do
+ActiveRecord::Schema[8.2].define(version: 2025_12_15_170000) do
create_table "account_billing_waivers", id: :uuid, charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
t.uuid "account_id", null: false
t.datetime "created_at", null: false
@@ -31,6 +31,7 @@ ActiveRecord::Schema[8.2].define(version: 2025_12_15_160000) do
t.datetime "cancel_at"
t.datetime "created_at", null: false
t.datetime "current_period_end"
+ t.integer "next_amount_due_in_cents"
t.string "plan_key"
t.string "status"
t.string "stripe_customer_id", null: false
diff --git a/saas/test/controllers/stripe/webhooks_controller_test.rb b/saas/test/controllers/stripe/webhooks_controller_test.rb
index c1c8ac4d0..92f6b7508 100644
--- a/saas/test/controllers/stripe/webhooks_controller_test.rb
+++ b/saas/test/controllers/stripe/webhooks_controller_test.rb
@@ -29,6 +29,7 @@ class Stripe::WebhooksControllerTest < ActionDispatch::IntegrationTest
Stripe::Webhook.stubs(:construct_event).returns(event)
Stripe::Subscription.stubs(:retrieve).returns(stripe_sub)
+ Stripe::Invoice.stubs(:create_preview).returns(OpenStruct.new(amount_due: 1999))
post stripe_webhooks_path
@@ -38,7 +39,7 @@ class Stripe::WebhooksControllerTest < ActionDispatch::IntegrationTest
assert_equal "active", @subscription.status
end
- test "subscription updated changes status" do
+ test "subscription updated changes status and syncs next amount due" do
@subscription.update!(stripe_subscription_id: "sub_123", status: "active")
stripe_sub = OpenStruct.new(
@@ -53,11 +54,14 @@ class Stripe::WebhooksControllerTest < ActionDispatch::IntegrationTest
Stripe::Webhook.stubs(:construct_event).returns(event)
Stripe::Subscription.stubs(:retrieve).returns(stripe_sub)
+ Stripe::Invoice.stubs(:create_preview).returns(OpenStruct.new(amount_due: 1999))
post stripe_webhooks_path
assert_response :ok
- assert_equal "past_due", @subscription.reload.status
+ @subscription.reload
+ assert_equal "past_due", @subscription.status
+ assert_equal 1999, @subscription.next_amount_due_in_cents
end
test "subscription deleted cancels subscription" do
@@ -82,6 +86,7 @@ class Stripe::WebhooksControllerTest < ActionDispatch::IntegrationTest
@subscription.reload
assert_equal "canceled", @subscription.status
assert_nil @subscription.stripe_subscription_id
+ assert_nil @subscription.next_amount_due_in_cents
end
private