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
8 changes: 8 additions & 0 deletions saas/app/jobs/account/sync_stripe_customer_email_job.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
class Account::SyncStripeCustomerEmailJob < ApplicationJob
queue_as :default
retry_on Stripe::StripeError, wait: :polynomially_longer

def perform(subscription)
subscription.sync_customer_email_to_stripe
end
end
4 changes: 4 additions & 0 deletions saas/app/models/account/billing.rb
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,10 @@ def uncomp
reload_billing_waiver
end

def owner_email_changed
Account::SyncStripeCustomerEmailJob.perform_later(subscription) if subscription
end

private
def active_subscription
if comped?
Expand Down
17 changes: 17 additions & 0 deletions saas/app/models/account/subscription.rb
Original file line number Diff line number Diff line change
Expand Up @@ -47,4 +47,21 @@ def cancel
# Subscription already deleted/canceled in Stripe - treat as success
Rails.logger.warn "Stripe subscription #{stripe_subscription_id} not found during cancel: #{e.message}"
end

def sync_customer_email_to_stripe
if stripe_customer_id && (email = owner_email)
Stripe::Customer.update(stripe_customer_id, email: email)
end
rescue Stripe::InvalidRequestError => e
# Customer already deleted in Stripe - treat as success
Rails.logger.warn "Stripe customer #{stripe_customer_id} not found during email sync: #{e.message}"
end

private
# Account owner email for Stripe customer record. Returns nil when:
# - No owner exists (ownership being transferred, account in limbo)
# - Owner has no identity (deactivated user)
def owner_email
account.users.owner.first&.identity&.email_address
end
end
23 changes: 23 additions & 0 deletions saas/app/models/user/notifies_account_of_email_change.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
module User::NotifiesAccountOfEmailChange
extend ActiveSupport::Concern

included do
after_update :notify_account_of_owner_change, if: :account_owner_changed?
end

private
# Account owner changed when:
# - The current owner changed their email
# - A user just became the owner (ownership transfer)
def account_owner_changed?
owner? && identity && (saved_change_to_identity_id? || became_owner?)
end

def became_owner?
saved_change_to_role? && role_before_last_save != "owner"
end

def notify_account_of_owner_change
account.owner_email_changed
end
end
1 change: 1 addition & 0 deletions saas/lib/fizzy/saas/engine.rb
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,7 @@ class Engine < ::Rails::Engine

config.to_prepare do
::Account.include Account::Billing, Account::Limited
::User.include User::NotifiesAccountOfEmailChange
::Signup.prepend Fizzy::Saas::Signup
CardsController.include(Card::LimitedCreation)
Cards::PublishesController.include(Card::LimitedPublishing)
Expand Down
22 changes: 22 additions & 0 deletions saas/test/models/account/billing_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -68,4 +68,26 @@ class Account::BillingTest < ActiveSupport::TestCase

account.incinerate
end

test "owner_email_changed enqueues sync job when subscription exists" do
account = accounts(:"37s")
account.create_subscription!(
stripe_customer_id: "cus_test",
plan_key: "monthly_v1",
status: "active"
)

assert_enqueued_with(job: Account::SyncStripeCustomerEmailJob, args: [ account.subscription ]) do
account.owner_email_changed
end
end

test "owner_email_changed does nothing without subscription" do
account = accounts(:initech)
account.subscription&.destroy

assert_no_enqueued_jobs do
account.owner_email_changed
end
end
end
74 changes: 74 additions & 0 deletions saas/test/models/account/subscription_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -117,4 +117,78 @@ class Account::SubscriptionTest < ActiveSupport::TestCase
subscription.cancel
end
end

test "sync_customer_email_to_stripe updates Stripe customer with owner email" do
account = accounts(:"37s")
owner = account.users.find_by(role: :owner) || account.users.first.tap { |u| u.update!(role: :owner) }
subscription = account.create_subscription!(
stripe_customer_id: "cus_test",
plan_key: "monthly_v1",
status: "active"
)

Stripe::Customer.expects(:update).with("cus_test", email: owner.identity.email_address).once

subscription.sync_customer_email_to_stripe
end

test "sync_customer_email_to_stripe does nothing without stripe_customer_id" do
account = accounts(:"37s")
subscription = account.build_subscription(
stripe_customer_id: nil,
plan_key: "free_v1",
status: "active"
)

Stripe::Customer.expects(:update).never

subscription.sync_customer_email_to_stripe
end

test "sync_customer_email_to_stripe does nothing without owner" do
account = accounts(:"37s")
account.users.update_all(role: :member)
subscription = account.create_subscription!(
stripe_customer_id: "cus_test",
plan_key: "monthly_v1",
status: "active"
)

Stripe::Customer.expects(:update).never

subscription.sync_customer_email_to_stripe
end

test "sync_customer_email_to_stripe does nothing when owner has no identity" do
account = accounts(:"37s")
owner = account.users.find_by(role: :owner) || account.users.first.tap { |u| u.update!(role: :owner) }
owner.update_column(:identity_id, nil)
subscription = account.create_subscription!(
stripe_customer_id: "cus_test",
plan_key: "monthly_v1",
status: "active"
)

Stripe::Customer.expects(:update).never

subscription.sync_customer_email_to_stripe
end

test "sync_customer_email_to_stripe treats deleted customer as success" do
account = accounts(:"37s")
account.users.find_by(role: :owner) || account.users.first.tap { |u| u.update!(role: :owner) }
subscription = account.create_subscription!(
stripe_customer_id: "cus_deleted",
plan_key: "monthly_v1",
status: "active"
)

Stripe::Customer.stubs(:update).raises(
Stripe::InvalidRequestError.new("No such customer", {})
)

assert_nothing_raised do
subscription.sync_customer_email_to_stripe
end
end
end
51 changes: 51 additions & 0 deletions saas/test/models/user/notifies_account_of_email_change_test.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
require "test_helper"

class User::NotifiesAccountOfEmailChangeTest < ActiveSupport::TestCase
setup do
@account = accounts(:"37s")
@owner = @account.users.find_by(role: :owner) || @account.users.first.tap { |u| u.update!(role: :owner) }
@member = @account.users.where.not(id: @owner.id).first || @account.users.create!(
name: "Member",
identity: Identity.create!(email_address: "member@example.com"),
role: :member
)
end

test "notifies account when owner changes email" do
@account.expects(:owner_email_changed).once

new_identity = Identity.create!(email_address: "new-owner@example.com")
@owner.update!(identity: new_identity)
end

test "does not notify account when non-owner changes email" do
@account.expects(:owner_email_changed).never

new_identity = Identity.create!(email_address: "new-member@example.com")
@member.update!(identity: new_identity)
end

test "does not notify account when owner is deactivated" do
@account.expects(:owner_email_changed).never

@owner.update!(identity: nil)
end

test "does not notify account when identity unchanged" do
@account.expects(:owner_email_changed).never

@owner.update!(name: "New Name")
end

test "notifies account when user becomes owner" do
@account.expects(:owner_email_changed).once

@member.update!(role: :owner)
end

test "does not notify account when owner becomes member" do
@account.expects(:owner_email_changed).never

@owner.update!(role: :member)
end
end