Sync email to Stripe when user changes email address (#2432)
* 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 * Address PR feedback: error handling and test coverage - Handle Stripe::InvalidRequestError in sync_customer_email_to_stripe (mirrors cancel method behavior for deleted customers) - Add test for deactivated owner (owner with nil identity) - Add test for deleted Stripe customer scenario
This commit is contained in:
@@ -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
|
||||
@@ -31,6 +31,10 @@ module Account::Billing
|
||||
reload_billing_waiver
|
||||
end
|
||||
|
||||
def owner_email_changed
|
||||
Account::SyncStripeCustomerEmailJob.perform_later(subscription) if subscription
|
||||
end
|
||||
|
||||
private
|
||||
def active_subscription
|
||||
if comped?
|
||||
|
||||
@@ -47,4 +47,21 @@ class Account::Subscription < SaasRecord
|
||||
# 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
|
||||
|
||||
@@ -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
|
||||
@@ -133,6 +133,7 @@ module Fizzy
|
||||
|
||||
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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user