Skip to content
Open
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
10 changes: 8 additions & 2 deletions app/controllers/admin/sponsors_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -85,8 +85,14 @@ def audit_contact_subscription(contact)
return unless contact.previous_changes.key?(:mailing_list_consent)

contact_audit_key = contact.mailing_list_consent ? 'subscribe' : 'unsubscribe'
Auditor::Audit.new(@sponsor, "sponsor.admin_contact_#{contact_audit_key}", current_user, contact)
.log_with_note(contact.email)

MemberActivityRecorder.record(
actor: current_user,
key: "sponsor.admin_contact_#{contact_audit_key}",
trackable: @sponsor,
recipient: contact,
contact_email: contact.email
)
end

def filter_search_params
Expand Down
8 changes: 6 additions & 2 deletions app/controllers/contact_preferences_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,11 @@ def subscription_preference(contact)
end

def audit_contact_subscription(contact)
Auditor::Audit.new(contact.sponsor, subscription_preference(contact), contact)
.log_with_note(contact.email)
MemberActivityRecorder.record(
actor: contact,
key: subscription_preference(contact),
trackable: contact.sponsor,
contact_email: contact.email
)
end
end
10 changes: 8 additions & 2 deletions app/services/member_activity_recorder.rb
Original file line number Diff line number Diff line change
Expand Up @@ -42,10 +42,16 @@ class MemberActivityRecorder
# historical activity is ever backfilled.
TRACKING_STARTED_ON = Date.new(2026, 9, 8).freeze

def self.record(actor:, key:, trackable: nil, recipient: nil)
def self.record(actor:, key:, trackable: nil, recipient: nil, contact_email: nil)
Rails.logger.warn("MemberActivityRecorder: unregistered key '#{key}'") unless KEYS.include?(key)
trackable ||= actor
PublicActivity::Activity.create!(owner: actor, key:, trackable:, recipient:)
PublicActivity::Activity.create!(
owner: actor,
key:,
trackable:,
recipient:,
parameters: contact_email ? { note: contact_email } : {}
)
rescue StandardError => e
Rails.logger.warn("MemberActivityRecorder failed (#{key}): #{e.class}: #{e.message}")
end
Expand Down
82 changes: 82 additions & 0 deletions spec/controllers/contact_preferences_controller_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
require 'rails_helper'

RSpec.describe ContactPreferencesController do
let(:contact) { Fabricate(:contact, mailing_list_consent: false) }

before do
allow(ContactMailingListService).to receive(:sync)
end

describe 'GET #show' do
it 'sets the contact for a valid token' do
get :show, params: { token: contact.token }

expect(response).to have_http_status(:success)
expect(controller.instance_variable_get(:@contact)).to eq(contact)
expect(flash[:notice]).to be_nil
end

it 'shows an invalid-session notice for an unknown token' do
get :show, params: { token: 'unknown-token' }

expect(response).to have_http_status(:success)
expect(flash[:notice]).to eq(I18n.t('messages.invalid_session'))
end
end

describe 'PATCH #update' do
it 'passes the contact and email to the activity recorder' do
expect(MemberActivityRecorder).to receive(:record).with(
actor: contact,
key: 'sponsor.contact_subscribe',
trackable: contact.sponsor,
contact_email: contact.email
)

patch :update, params: {
contact: { token: contact.token, mailing_list_consent: '1' }
}
end

it 'subscribes the contact and records the consent activity' do
patch :update, params: {
contact: { token: contact.token, mailing_list_consent: '1' }
}

expect(contact.reload.mailing_list_consent).to be(true)
expect(ContactMailingListService).to have_received(:sync).with(contact)
expect(response).to redirect_to(
contact_preferences_path(token: contact.token, notice: I18n.t('messages.contact_preferences.updated'))
)

activity = PublicActivity::Activity.find_by!(key: 'sponsor.contact_subscribe')
expect(activity.owner).to eq(contact)
expect(activity.trackable).to eq(contact.sponsor)
expect(activity.parameters).to include(note: contact.email)
end

it 'unsubscribes the contact and records the consent activity' do
contact.update!(mailing_list_consent: true)

patch :update, params: {
contact: { token: contact.token, mailing_list_consent: '0' }
}

expect(contact.reload.mailing_list_consent).to be(false)
expect(ContactMailingListService).to have_received(:sync).with(contact)

activity = PublicActivity::Activity.find_by!(key: 'sponsor.contact_unsubscribe')
expect(activity.owner).to eq(contact)
expect(activity.trackable).to eq(contact.sponsor)
expect(activity.parameters).to include(note: contact.email)
end

it 'returns not found when the submitted token is unknown' do
patch :update, params: {
contact: { token: 'unknown-token', mailing_list_consent: '1' }
}

expect(response).to have_http_status(:not_found)
end
end
end
22 changes: 18 additions & 4 deletions spec/features/admin/sponsor_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -161,8 +161,15 @@
context 'with activities' do
scenario 'when there are activities' do
contact = sponsor.contacts.first
audit = Auditor::Audit.new(sponsor, 'sponsor.admin_contact_subscribe', manager, contact)
audit.log_with_note(contact.email)
contact_audit_key = contact.mailing_list_consent ? 'subscribe' : 'unsubscribe'

MemberActivityRecorder.record(
actor: manager,
key: "sponsor.admin_contact_#{contact_audit_key}",
trackable: sponsor,
recipient: contact,
contact_email: contact.email
)

visit admin_sponsor_path(sponsor)

Expand All @@ -173,8 +180,15 @@

scenario 'when an activity is associated with a deleted contact' do
contact = sponsor.contacts.first
audit = Auditor::Audit.new(sponsor, 'sponsor.admin_contact_subscribe', manager, contact)
audit.log_with_note(contact.email)
contact_audit_key = contact.mailing_list_consent ? 'subscribe' : 'unsubscribe'

MemberActivityRecorder.record(
actor: manager,
key: "sponsor.admin_contact_#{contact_audit_key}",
trackable: sponsor,
recipient: contact,
contact_email: contact.email
)

contact.delete

Expand Down
Loading