diff --git a/app/controllers/nonprofits/supporters_controller.rb b/app/controllers/nonprofits/supporters_controller.rb index 25d85651..4009be86 100644 --- a/app/controllers/nonprofits/supporters_controller.rb +++ b/app/controllers/nonprofits/supporters_controller.rb @@ -5,7 +5,8 @@ module Nonprofits class SupportersController < ApplicationController include Controllers::Nonprofit::Current - include Controllers::Nonprofit::Authorization + include Controllers::Nonprofit::Authorization + include Controllers::Supporter::Current before_action :authenticate_nonprofit_user!, except: %i[new create] @@ -54,13 +55,12 @@ module Nonprofits end def email_address - render json: Supporter.find(params[:supporter_id]).email + render json: current_supporter.email end def full_contact - begin - s = Supporter.find params[:id] - if s.method_defined? :full_contact_infos && (fc = s.full_contact_infos.first) + begin + if current_supporter.method_defined? :full_contact_infos && (fc = current_supporter.full_contact_infos.first) render json: { full_contact: QueryFullContactInfos.fetch_associated_tables(fc.id) } else render json: { full_contact: nil } @@ -76,13 +76,12 @@ module Nonprofits # post /nonprofits/:nonprofit_id/supporters def create - render_json { InsertSupporter.create_or_update(params[:nonprofit_id], create_supporter_params.to_h) } + render_json { InsertSupporter.create_or_update(nonprofit, create_supporter_params.to_h) } end # put /nonprofits/:nonprofit_id/supporters/:id def update - @supporter = current_nonprofit.supporters.find(params[:id]) - json_saved UpdateSupporter.from_info(@supporter, update_supporter_params[:supporter]) + json_saved UpdateSupporter.from_info(current_supporter, update_supporter_params[:supporter]) end def bulk_delete diff --git a/app/models/supporter.rb b/app/models/supporter.rb index a0c623c9..29368f81 100644 --- a/app/models/supporter.rb +++ b/app/models/supporter.rb @@ -55,6 +55,22 @@ class Supporter < ApplicationRecord validates :nonprofit, presence: true scope :not_deleted, -> { where(deleted: false) } + # TODO replace with Discard gem + define_model_callbacks :discard + + after_discard :publish_deleted + + after_create_commit :publish_create + after_update_commit :publish_updated + + # TODO replace with discard gem + def discard! + run_callbacks(:discard) do + self.deleted = true + save! + end + end + geocoded_by :full_address reverse_geocoded_by :latitude, :longitude do |obj, results| geo = results.first @@ -82,15 +98,96 @@ class Supporter < ApplicationRecord end def to_builder(*expand) + supporter_addresses = [self] Jbuilder.new do |json| json.object "supporter" - json.id id + json.(self, :id, :name, :organization, :phone, :anonymous, :deleted) + if expand.include? :supporter_address + json.supporter_addresses supporter_addresses do |i| + json.merge! i.to_supporter_address_builder.attributes! + end + else + json.supporter_addresses [id] + end + + if expand.include? :nonprofit + json.nonprofit nonprofit.to_builder + else + json.nonprofit nonprofit.id + end + + unless merged_into.nil? + if expand.include? :merged_into + json.merged_into merged_into.to_builder + else + json.merged_into merged_into.id + end + else + json.merged_into nil + end + end + end + + def to_supporter_address_builder(*expand) + Jbuilder.new do |json| + json.(self, :id, :address, :state_code, :city, :country, :zip_code, :deleted) + json.object 'supporter_address' + if expand.include? :supporter + json.supporter to_builder + else + json.supporter id + end end end def full_address Format::Address.full_address(address, city, state_code) end + + private + + ADDRESS_ATTRIBUTES = [:address, :city, :state_code, :zip_code, :country] + + def supporter_address_updated? + ADDRESS_ATTRIBUTES.any?{|attrib| saved_change_to_attribute?(attrib)} + end + + def nonsupporter_address_updated? + (saved_changes.keys.map{|i| i.to_sym} - ADDRESS_ATTRIBUTES).any? + end + + def publish_create + Houdini.event_publisher.announce(:supporter_created, to_event('supporter.created', :supporter_address, :nonprofit).attributes!) + Houdini.event_publisher.announce(:supporter_address_created, to_event('supporter_address.created', :supporter, :nonprofit).attributes!) + end + + def publish_updated + if !deleted + if nonsupporter_address_updated? + Houdini.event_publisher.announce(:supporter_updated, to_event('supporter.updated', :supporter_address, :nonprofit).attributes!) + end + if supporter_address_updated? + Houdini.event_publisher.announce(:supporter_address_updated, to_event('supporter_address.updated', :supporter, :nonprofit).attributes!) + end + end + end + + def publish_deleted + Houdini.event_publisher.announce(:supporter_deleted, to_event('supporter.deleted', :supporter_address, :nonprofit, :merged_into).attributes!) + Houdini.event_publisher.announce(:supporter_address_deleted, to_event('supporter_address.deleted', :supporter, :nonprofit).attributes!) + end + + # we do something custom here since Supporter and SupporterAddress are in the same model + def to_event(event_type, *expand) + Jbuilder.new do |event| + event.id SecureRandom.uuid + event.object 'object_event' + event.type event_type + event.data do + event.object event_type.start_with?('supporter_address') ? to_supporter_address_builder(*expand) : to_builder(*expand) + end + end + end end ActiveSupport.run_load_hooks(:houdini_supporter, Supporter) \ No newline at end of file diff --git a/lib/insert/insert_supporter.rb b/lib/insert/insert_supporter.rb index 22c1d577..0af25a22 100644 --- a/lib/insert/insert_supporter.rb +++ b/lib/insert/insert_supporter.rb @@ -7,9 +7,7 @@ require 'qexpr' require 'i18n' module InsertSupporter - def self.create_or_update(np_id, data, update = false) - ParamValidation.new(data.merge(np_id: np_id), - np_id: { required: true, is_integer: true }) + def self.create_or_update(nonprofit, data, update = false) address_keys = %w[name address city country state_code] custom_fields = data['customFields'] data = HashWithIndifferentAccess.new(Format::RemoveDiacritics.from_hash(data, address_keys)) @@ -17,7 +15,7 @@ module InsertSupporter supporter = Qx.select('*').from(:supporters) .where('name = $n AND email = $e', n: data[:name], e: data[:email]) - .and_where('nonprofit_id=$id', id: np_id) + .and_where('nonprofit_id=$id', id: nonprofit.id) .and_where('coalesce(deleted, FALSE)=FALSE') .execute.last if supporter && update @@ -28,20 +26,13 @@ module InsertSupporter .timestamps .execute.last else - supporter = Qx.insert_into(:supporters) - .values(defaults(data).merge(nonprofit_id: np_id)) - .returning('*') - .timestamps - .execute.last + supporter = nonprofit.supporters.create(defaults(data)) end if custom_fields - InsertCustomFieldJoins.find_or_create(np_id, [supporter['id']], custom_fields) + InsertCustomFieldJoins.find_or_create(nonprofit.id, [supporter.id], custom_fields) end - # GeocodeModel.delay.supporter(supporter['id']) - Houdini.event_publisher.announce(:supporter_create, Supporter.find(supporter['id'])) - supporter end diff --git a/spec/lib/insert/insert_donation_spec.rb b/spec/lib/insert/insert_donation_spec.rb index 1425dcc7..f2258b9e 100644 --- a/spec/lib/insert/insert_donation_spec.rb +++ b/spec/lib/insert/insert_donation_spec.rb @@ -117,6 +117,8 @@ describe InsertDonation do end it 'process campaign donation' do + expect(Houdini.event_publisher).to receive(:announce).with(:supporter_created, anything) + expect(Houdini.event_publisher).to receive(:announce).with(:supporter_address_created, anything) expect(Houdini.event_publisher).to receive(:announce).with(:campaign_create, any_args) process_campaign_donation(sepa: true) { InsertDonation.with_sepa(amount: charge_amount, nonprofit_id: nonprofit.id, supporter_id: supporter.id, direct_debit_detail_id: direct_debit_detail.id, campaign_id: campaign.id, date: (Time.now + 1.day).to_s, dedication: 'dedication', designation: 'designation') } end diff --git a/spec/lib/insert/insert_tickets_spec.rb b/spec/lib/insert/insert_tickets_spec.rb index 196ce35d..7a62cd0a 100644 --- a/spec/lib/insert/insert_tickets_spec.rb +++ b/spec/lib/insert/insert_tickets_spec.rb @@ -287,6 +287,8 @@ describe InsertTickets do expect(QueryRoles).to receive(:is_authorized_for_nonprofit?).with(user.id, nonprofit.id).and_return true result = nil expect(Houdini.event_publisher).to receive(:announce).with(:ticket_level_created, any_args).ordered + expect(Houdini.event_publisher).to receive(:announce).with(:supporter_created, anything) + expect(Houdini.event_publisher).to receive(:announce).with(:supporter_address_created, anything) expect(Houdini.event_publisher).to receive(:announce).with(:ticket_create, any_args).ordered result = InsertTickets.create(tickets: [{ quantity: 1, ticket_level_id: ticket_level.id }], nonprofit_id: nonprofit.id, supporter_id: supporter.id, token: source_token.token, event_id: event.id, kind: 'offsite', offsite_payment: { kind: 'check', check_number: 'fake_checknumber' }, current_user: user) diff --git a/spec/lib/retrieve/retrieve_active_record_items_spec.rb b/spec/lib/retrieve/retrieve_active_record_items_spec.rb index c68dda40..66b62097 100644 --- a/spec/lib/retrieve/retrieve_active_record_items_spec.rb +++ b/spec/lib/retrieve/retrieve_active_record_items_spec.rb @@ -6,7 +6,7 @@ require 'rails_helper' describe RetrieveActiveRecordItems do describe '.retrieve' do - let(:item) { force_create(:supporter) } + let(:item) { force_create(:supporter, nonprofit: item2) } let(:item2) { force_create(:nm_justice) } it 'raises if not a class for key' do expect { RetrieveActiveRecordItems.retrieve('item' => 1) }.to raise_error(ArgumentError) @@ -41,7 +41,7 @@ describe RetrieveActiveRecordItems do end describe '.retrieve_from_keys' do - let(:item) { force_create(:supporter) } + let(:item) { force_create(:supporter, nonprofit: item2) } let(:item2) { force_create(:nm_justice) } it 'raises if not a class for key' do expect { RetrieveActiveRecordItems.retrieve_from_keys({}, 'item' => 1) }.to raise_error(ArgumentError) diff --git a/spec/lib/update/update_recurring_donations_spec.rb b/spec/lib/update/update_recurring_donations_spec.rb index 2c812494..0d8e17ae 100644 --- a/spec/lib/update/update_recurring_donations_spec.rb +++ b/spec/lib/update/update_recurring_donations_spec.rb @@ -9,7 +9,7 @@ describe UpdateRecurringDonations do describe '.cancel' do let(:np) { force_create(:nm_justice) } - let(:s) { force_create(:supporter) } + let(:s) { force_create(:supporter, nonprofit: np) } let(:donation) { force_create(:donation, nonprofit_id: np.id, supporter_id: s.id) } let(:email) { 'test@test.com' } let!(:rd) do diff --git a/spec/models/supporter_note_spec.rb b/spec/models/supporter_note_spec.rb index 757e033f..e0ddb66f 100644 --- a/spec/models/supporter_note_spec.rb +++ b/spec/models/supporter_note_spec.rb @@ -10,11 +10,29 @@ RSpec.describe SupporterNote, type: :model do let(:content2) {"CONTENT2"} let(:supporter_note) { supporter.supporter_notes.create(content: content, user: user) } + + let(:supporter_base) do + { + 'anonymous' => false, + 'deleted' => false, + 'name' => name, + 'organization' => nil, + 'phone' => nil, + 'supporter_addresses' => [kind_of(Numeric)], + 'id'=> kind_of(Numeric), + 'merged_into' => nil, + 'nonprofit'=> nonprofit.id, + 'object' => 'supporter' + } + end + it 'creates' do expect(supporter_note.errors).to be_empty end it 'announces created' do + expect(Houdini.event_publisher).to receive(:announce).with(:supporter_created, anything) + expect(Houdini.event_publisher).to receive(:announce).with(:supporter_address_created, anything) expect(Houdini.event_publisher).to receive(:announce).with(:supporter_note_created, { 'id' => kind_of(String), 'object' => 'object_event', @@ -34,10 +52,7 @@ RSpec.describe SupporterNote, type: :model do 'id' => user.id, 'object' => 'user' }, - 'supporter' => { - 'id' => supporter.id, - 'object' => 'supporter' - } + 'supporter' => supporter_base.merge({'name' => "Fake Supporter Name"}) } } }) @@ -46,6 +61,8 @@ RSpec.describe SupporterNote, type: :model do end it 'announces updated' do + expect(Houdini.event_publisher).to receive(:announce).with(:supporter_created, anything) + expect(Houdini.event_publisher).to receive(:announce).with(:supporter_address_created, anything) expect(Houdini.event_publisher).to receive(:announce).with(:supporter_note_created, anything).ordered expect(Houdini.event_publisher).to receive(:announce).with(:supporter_note_updated, { 'id' => kind_of(String), @@ -66,10 +83,7 @@ RSpec.describe SupporterNote, type: :model do 'id' => user.id, 'object' => 'user' }, - 'supporter' => { - 'id' => supporter.id, - 'object' => 'supporter' - } + 'supporter' => supporter_base.merge({'name' => "Fake Supporter Name"}) } } }).ordered @@ -80,6 +94,8 @@ RSpec.describe SupporterNote, type: :model do end it 'announces deleted' do + expect(Houdini.event_publisher).to receive(:announce).with(:supporter_created, anything) + expect(Houdini.event_publisher).to receive(:announce).with(:supporter_address_created, anything) expect(Houdini.event_publisher).to receive(:announce).with(:supporter_note_created, anything).ordered expect(Houdini.event_publisher).to receive(:announce).with(:supporter_note_deleted, { 'id' => kind_of(String), @@ -100,10 +116,7 @@ RSpec.describe SupporterNote, type: :model do 'id' => user.id, 'object' => 'user' }, - 'supporter' => { - 'id' => supporter.id, - 'object' => 'supporter' - } + 'supporter' => supporter_base.merge({'name' => "Fake Supporter Name"}) } } }).ordered diff --git a/spec/models/supporter_spec.rb b/spec/models/supporter_spec.rb new file mode 100644 index 00000000..1aed4b69 --- /dev/null +++ b/spec/models/supporter_spec.rb @@ -0,0 +1,198 @@ +# frozen_string_literal: true + +# License: AGPL-3.0-or-later WITH WTO-AP-3.0-or-later +# Full license explanation at https://github.com/houdiniproject/houdini/blob/master/LICENSE +require 'rails_helper' + +RSpec.describe Supporter, type: :model do + include_context :shared_donation_charge_context + let(:name) {"CUSTOM SSUPPORTER"} + let(:merged_into_supporter_name) {"I've been merged into!"} + let(:address) { "address for supporter"} + let(:supporter) { nonprofit.supporters.create(name: name, address: address)} + let(:merged_supporter) {nonprofit.supporters.create(name: name, address: address, merged_into: merged_into_supporter, deleted: true) } + let(:merged_into_supporter) {nonprofit.supporters.create(name: merged_into_supporter_name, address: address) } + + let(:supporter_base) do + { + 'anonymous' => false, + 'deleted' => false, + 'name' => name, + 'organization' => nil, + 'phone' => nil, + 'supporter_addresses' => [kind_of(Numeric)], + 'id'=> kind_of(Numeric), + 'merged_into' => nil, + 'nonprofit'=> nonprofit.id, + 'object' => 'supporter' + } + end + + let(:supporter_address_base) do + { + 'id' => kind_of(Numeric), + 'deleted' => false, + 'address' => address, + 'city' => nil, + 'state_code' => nil, + 'zip_code' => nil, + 'country' => 'United States', + 'object' => 'supporter_address', + 'supporter' => kind_of(Numeric) + } + end + + let(:nonprofit_base) do + { + 'id' => nonprofit.id, + 'name' => nonprofit.name, + 'object' => 'nonprofit' + } + end + + describe 'supporter' do + it 'created' do + + supporter_result = supporter_base.merge({ + 'supporter_addresses' => [ + supporter_address_base + ], + 'nonprofit' => nonprofit_base + }) + + expect(Houdini.event_publisher).to receive(:announce).with(:supporter_created, { + 'id' => kind_of(String), + 'object' => 'object_event', + 'type' => 'supporter.created', + 'data' => { + 'object' => supporter_result + } + }).ordered + + expect(Houdini.event_publisher).to receive(:announce).with(:supporter_address_created, anything).ordered + + supporter + end + + it 'deletes' do + expect(Houdini.event_publisher).to receive(:announce).with(:supporter_created,anything).ordered + expect(Houdini.event_publisher).to receive(:announce).with(:supporter_address_created, anything).ordered + expect(Houdini.event_publisher).to_not receive(:announce).with(:supporter_updated) + expect(Houdini.event_publisher).to_not receive(:announce).with(:supporter_address_updated) + + supporter_result = supporter_base.merge({ + 'deleted' => true, + 'supporter_addresses' => [ + supporter_address_base.merge({'deleted' => true}) + ], + 'nonprofit' => nonprofit_base + }) + + expect(Houdini.event_publisher).to receive(:announce).with(:supporter_deleted, { + 'id' => kind_of(String), + 'object' => 'object_event', + 'type' => 'supporter.deleted', + 'data' => { + 'object' => supporter_result + } + }).ordered + + + expect(Houdini.event_publisher).to receive(:announce).with(:supporter_address_deleted,anything).ordered + + supporter.discard! + end + end + + describe 'supporter_address events' do + it 'creates' do + expect(Houdini.event_publisher).to receive(:announce).with(:supporter_created, anything).ordered + + + + expect(Houdini.event_publisher).to receive(:announce).with(:supporter_address_created, { + 'id' => kind_of(String), + 'object' => 'object_event', + 'type' => 'supporter_address.created', + 'data' => { + 'object' => supporter_address_base.merge({ + 'supporter' => supporter_base + }) + } + }).ordered + + supporter + end + + it 'deletes' do + expect(Houdini.event_publisher).to receive(:announce).with(:supporter_created,anything).ordered + expect(Houdini.event_publisher).to receive(:announce).with(:supporter_address_created, anything).ordered + expect(Houdini.event_publisher).to_not receive(:announce).with(:supporter_updated) + expect(Houdini.event_publisher).to_not receive(:announce).with(:supporter_address_updated) + + expect(Houdini.event_publisher).to receive(:announce).with(:supporter_deleted, anything) + + + supporter_address_result = supporter_address_base.merge({ + 'deleted' => true, + 'supporter'=> supporter_base.merge({'deleted' => true}) + }) + + expect(Houdini.event_publisher).to receive(:announce).with(:supporter_address_deleted, { + 'id' => kind_of(String), + 'object' => 'object_event', + 'type' => 'supporter_address.deleted', + 'data' => { + 'object' => supporter_address_result + } + }).ordered + + + supporter.discard! + end + end + + describe 'supporter and supporter_address events update events are separate' do + it 'only fires supporter on supporter only change' do + expect(Houdini.event_publisher).to receive(:announce).with(:supporter_created, anything).ordered + expect(Houdini.event_publisher).to receive(:announce).with(:supporter_address_created, anything).ordered + + expect(Houdini.event_publisher).to receive(:announce).with(:supporter_updated, { 'id' => kind_of(String), + 'object' => 'object_event', + 'type' => 'supporter.updated', + 'data' => { + 'object' => supporter_base.merge({ + 'name' => merged_into_supporter_name, + 'supporter_addresses' => [ + supporter_address_base + ], + 'nonprofit' => nonprofit_base + }) + + }}).ordered + expect(Houdini.event_publisher).to_not receive(:announce).with(:supporter_address_updated, anything) + + supporter.update(name: merged_into_supporter_name) + end + + it 'only fires supporter_address on supporter_address only change' do + expect(Houdini.event_publisher).to receive(:announce).with(:supporter_created, anything).ordered + expect(Houdini.event_publisher).to receive(:announce).with(:supporter_address_created, anything).ordered + + expect(Houdini.event_publisher).to receive(:announce).with(:supporter_address_updated, { + 'id' => kind_of(String), + 'object' => 'object_event', + 'type' => 'supporter_address.updated', + 'data' => { + 'object' => supporter_address_base.merge({ + 'city' => 'new_city', + 'supporter'=> supporter_base + }) + }}).ordered + + #expect(Houdini.event_publisher).to_not receive(:announce).with(:supporter_updated, anything) + + supporter.update(city: 'new_city') + end + end +end \ No newline at end of file