Skip to content

Commit 6ccb0d1

Browse files
committed
Sync owner email to Stripe when changed
When an account owner changes their email address, update the corresponding Stripe customer record via a background job. Also handles ownership transfers: when a user becomes the account owner, their email is synced to Stripe. Responsibility chain: - User::NotifiesAccountOfEmailChange triggers on owner identity change or when a user becomes owner - Account::Billing#owner_email_changed enqueues sync job - Account::SyncStripeCustomerEmailJob performs the update with polynomial backoff retries - Account::Subscription#sync_customer_email_to_stripe calls Stripe API
1 parent 4efa4da commit 6ccb0d1

File tree

8 files changed

+164
-0
lines changed

8 files changed

+164
-0
lines changed
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
class Account::SyncStripeCustomerEmailJob < ApplicationJob
2+
queue_as :default
3+
retry_on Stripe::StripeError, wait: :polynomially_longer
4+
5+
def perform(subscription)
6+
subscription.sync_customer_email_to_stripe
7+
end
8+
end

saas/app/models/account/billing.rb

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,10 @@ def uncomp
3131
reload_billing_waiver
3232
end
3333

34+
def owner_email_changed
35+
Account::SyncStripeCustomerEmailJob.perform_later(subscription) if subscription
36+
end
37+
3438
private
3539
def active_subscription
3640
if comped?

saas/app/models/account/subscription.rb

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,4 +47,18 @@ def cancel
4747
# Subscription already deleted/canceled in Stripe - treat as success
4848
Rails.logger.warn "Stripe subscription #{stripe_subscription_id} not found during cancel: #{e.message}"
4949
end
50+
51+
def sync_customer_email_to_stripe
52+
if stripe_customer_id && (email = owner_email)
53+
Stripe::Customer.update(stripe_customer_id, email: email)
54+
end
55+
end
56+
57+
private
58+
# Account owner email for Stripe customer record. Returns nil when:
59+
# - No owner exists (ownership being transferred, account in limbo)
60+
# - Owner has no identity (deactivated user)
61+
def owner_email
62+
account.users.owner.first&.identity&.email_address
63+
end
5064
end
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
module User::NotifiesAccountOfEmailChange
2+
extend ActiveSupport::Concern
3+
4+
included do
5+
after_update :notify_account_of_owner_change, if: :account_owner_changed?
6+
end
7+
8+
private
9+
# Account owner changed when:
10+
# - The current owner changed their email
11+
# - A user just became the owner (ownership transfer)
12+
def account_owner_changed?
13+
owner? && identity && (saved_change_to_identity_id? || became_owner?)
14+
end
15+
16+
def became_owner?
17+
saved_change_to_role? && role_before_last_save != "owner"
18+
end
19+
20+
def notify_account_of_owner_change
21+
account.owner_email_changed
22+
end
23+
end

saas/lib/fizzy/saas/engine.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,7 @@ class Engine < ::Rails::Engine
133133

134134
config.to_prepare do
135135
::Account.include Account::Billing, Account::Limited
136+
::User.include User::NotifiesAccountOfEmailChange
136137
::Signup.prepend Fizzy::Saas::Signup
137138
CardsController.include(Card::LimitedCreation)
138139
Cards::PublishesController.include(Card::LimitedPublishing)

saas/test/models/account/billing_test.rb

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,4 +68,26 @@ class Account::BillingTest < ActiveSupport::TestCase
6868

6969
account.incinerate
7070
end
71+
72+
test "owner_email_changed enqueues sync job when subscription exists" do
73+
account = accounts(:"37s")
74+
account.create_subscription!(
75+
stripe_customer_id: "cus_test",
76+
plan_key: "monthly_v1",
77+
status: "active"
78+
)
79+
80+
assert_enqueued_with(job: Account::SyncStripeCustomerEmailJob, args: [ account.subscription ]) do
81+
account.owner_email_changed
82+
end
83+
end
84+
85+
test "owner_email_changed does nothing without subscription" do
86+
account = accounts(:initech)
87+
account.subscription&.destroy
88+
89+
assert_no_enqueued_jobs do
90+
account.owner_email_changed
91+
end
92+
end
7193
end

saas/test/models/account/subscription_test.rb

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,4 +117,45 @@ class Account::SubscriptionTest < ActiveSupport::TestCase
117117
subscription.cancel
118118
end
119119
end
120+
121+
test "sync_customer_email_to_stripe updates Stripe customer with owner email" do
122+
account = accounts(:"37s")
123+
owner = account.users.find_by(role: :owner) || account.users.first.tap { |u| u.update!(role: :owner) }
124+
subscription = account.create_subscription!(
125+
stripe_customer_id: "cus_test",
126+
plan_key: "monthly_v1",
127+
status: "active"
128+
)
129+
130+
Stripe::Customer.expects(:update).with("cus_test", email: owner.identity.email_address).once
131+
132+
subscription.sync_customer_email_to_stripe
133+
end
134+
135+
test "sync_customer_email_to_stripe does nothing without stripe_customer_id" do
136+
account = accounts(:"37s")
137+
subscription = account.build_subscription(
138+
stripe_customer_id: nil,
139+
plan_key: "free_v1",
140+
status: "active"
141+
)
142+
143+
Stripe::Customer.expects(:update).never
144+
145+
subscription.sync_customer_email_to_stripe
146+
end
147+
148+
test "sync_customer_email_to_stripe does nothing without owner" do
149+
account = accounts(:"37s")
150+
account.users.update_all(role: :member)
151+
subscription = account.create_subscription!(
152+
stripe_customer_id: "cus_test",
153+
plan_key: "monthly_v1",
154+
status: "active"
155+
)
156+
157+
Stripe::Customer.expects(:update).never
158+
159+
subscription.sync_customer_email_to_stripe
160+
end
120161
end
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
require "test_helper"
2+
3+
class User::NotifiesAccountOfEmailChangeTest < ActiveSupport::TestCase
4+
setup do
5+
@account = accounts(:"37s")
6+
@owner = @account.users.find_by(role: :owner) || @account.users.first.tap { |u| u.update!(role: :owner) }
7+
@member = @account.users.where.not(id: @owner.id).first || @account.users.create!(
8+
name: "Member",
9+
identity: Identity.create!(email_address: "[email protected]"),
10+
role: :member
11+
)
12+
end
13+
14+
test "notifies account when owner changes email" do
15+
@account.expects(:owner_email_changed).once
16+
17+
new_identity = Identity.create!(email_address: "[email protected]")
18+
@owner.update!(identity: new_identity)
19+
end
20+
21+
test "does not notify account when non-owner changes email" do
22+
@account.expects(:owner_email_changed).never
23+
24+
new_identity = Identity.create!(email_address: "[email protected]")
25+
@member.update!(identity: new_identity)
26+
end
27+
28+
test "does not notify account when owner is deactivated" do
29+
@account.expects(:owner_email_changed).never
30+
31+
@owner.update!(identity: nil)
32+
end
33+
34+
test "does not notify account when identity unchanged" do
35+
@account.expects(:owner_email_changed).never
36+
37+
@owner.update!(name: "New Name")
38+
end
39+
40+
test "notifies account when user becomes owner" do
41+
@account.expects(:owner_email_changed).once
42+
43+
@member.update!(role: :owner)
44+
end
45+
46+
test "does not notify account when owner becomes member" do
47+
@account.expects(:owner_email_changed).never
48+
49+
@owner.update!(role: :member)
50+
end
51+
end

0 commit comments

Comments
 (0)