From 70d18591c3122e5c8f54510d0fe9f12b2b95f1bc Mon Sep 17 00:00:00 2001 From: Eric Schultz Date: Thu, 14 Jan 2021 15:33:10 -0600 Subject: [PATCH] Add support for supporter_note.* events --- .../nonprofits/supporter_notes_controller.rb | 50 ++++---- app/models/activity.rb | 12 ++ app/models/supporter.rb | 7 ++ app/models/supporter_note.rb | 57 ++++++++- app/models/user.rb | 7 ++ .../Nonprofit/Supporter/SupporterNote.ts | 18 +++ .../Nonprofit/Supporter/index.ts | 14 +++ docs/event_definitions/User.ts | 7 ++ lib/insert/insert_activities.rb | 34 ++---- lib/insert/insert_supporter_notes.rb | 28 ++--- lib/pay_recurring_donation.rb | 2 +- lib/update/update_activities.rb | 16 +-- lib/update/update_recurring_donations.rb | 2 +- lib/update/update_supporter_notes.rb | 31 ++--- .../lib/insert/insert_supporter_notes_spec.rb | 38 ++++++ spec/models/supporter_note_spec.rb | 114 ++++++++++++++++++ 16 files changed, 346 insertions(+), 91 deletions(-) create mode 100644 docs/event_definitions/Nonprofit/Supporter/SupporterNote.ts create mode 100644 docs/event_definitions/Nonprofit/Supporter/index.ts create mode 100644 docs/event_definitions/User.ts create mode 100644 spec/lib/insert/insert_supporter_notes_spec.rb create mode 100644 spec/models/supporter_note_spec.rb diff --git a/app/controllers/nonprofits/supporter_notes_controller.rb b/app/controllers/nonprofits/supporter_notes_controller.rb index 3b3cf531..b1547eb3 100644 --- a/app/controllers/nonprofits/supporter_notes_controller.rb +++ b/app/controllers/nonprofits/supporter_notes_controller.rb @@ -2,36 +2,36 @@ # 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 -module Nonprofits - class SupporterNotesController < ApplicationController - include Controllers::Nonprofit::Current + +class Nonprofits::SupporterNotesController < ApplicationController + include Controllers::Supporter::Current include Controllers::Nonprofit::Authorization - before_action :authenticate_nonprofit_user!, except: [:create] + before_action :authenticate_nonprofit_user!, except: [:create] - # post /nonprofits/:nonprofit_id/supporters/:supporter_id/supporter_notes - def create - params[:supporter_note][:user_id] ||= current_user&.id - render_json { InsertSupporterNotes.create([supporter_params[:supporter_note]]) } - end + # post /nonprofits/:nonprofit_id/supporters/:supporter_id/supporter_notes + def create + render_json { InsertSupporterNotes.create(supporter_params[:supporter_note]) } + end - # put /nonprofits/:nonprofit_id/supporters/:supporter_id/supporter_notes/:id - def update - params[:supporter_note][:user_id] ||= current_user&.id - params[:supporter_note][:id] = params[:id] - render_json { UpdateSupporterNotes.update(supporter_params[:supporter_note]) } - end + # put /nonprofits/:nonprofit_id/supporters/:supporter_id/supporter_notes/:id + def update + render_json { UpdateSupporterNotes.update(current_supporter_note, + supporter_params[:supporter_note].merge({user_id: current_user&.id})) } + end - # delete /nonprofits/:nonprofit_id/supporters/:supporter_id/supporter_notes/:id - def destroy - render_json { UpdateSupporterNotes.delete(params[:id]) } - end + # delete /nonprofits/:nonprofit_id/supporters/:supporter_id/supporter_notes/:id + def destroy + render_json { UpdateSupporterNotes.delete(current_supporter_note) } + end - private - - def supporter_params - params.require(:supporter_note) + private + + def current_supporter_note + current_supporter.supporter_notes.includes(:activities).find(params[:id]) + end - end - end + def supporter_params + params.require(:supporter_note).require(:content) + end end diff --git a/app/models/activity.rb b/app/models/activity.rb index e3ba6f91..d64a93ed 100644 --- a/app/models/activity.rb +++ b/app/models/activity.rb @@ -3,4 +3,16 @@ # 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 class Activity < ApplicationRecord + belongs_to :attachment, :polymorphic => true + belongs_to :supporter + belongs_to :nonprofit + belongs_to :user + + def json_data=(data) + write_attribute :json_data, JSON::generate(data) + end + + def json_data + JSON::parse(read_attribute :json_data) + end end diff --git a/app/models/supporter.rb b/app/models/supporter.rb index 3220e852..a0c623c9 100644 --- a/app/models/supporter.rb +++ b/app/models/supporter.rb @@ -81,6 +81,13 @@ class Supporter < ApplicationRecord h end + def to_builder(*expand) + Jbuilder.new do |json| + json.object "supporter" + json.id id + end + end + def full_address Format::Address.full_address(address, city, state_code) end diff --git a/app/models/supporter_note.rb b/app/models/supporter_note.rb index 1d966d41..16463b50 100644 --- a/app/models/supporter_note.rb +++ b/app/models/supporter_note.rb @@ -3,15 +3,70 @@ # 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 class SupporterNote < ApplicationRecord + include ObjectEvent::ModelExtensions + object_eventable # :content, # :supporter_id, :supporter belongs_to :supporter has_many :activities, as: :attachment, dependent: :destroy + belongs_to :user validates :content, length: { minimum: 1 } validates :supporter_id, presence: true + # 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 + + def to_builder(*expand) + Jbuilder.new do |json| + json.(self, :id, :deleted, :content) + json.object 'supporter_note' + + if expand.include? :nonprofit + json.nonprofit supporter.nonprofit.to_builder + else + json.nonprofit supporter.nonprofit.id + end + + if expand.include? :supporter + json.supporter supporter.to_builder + else + json.supporter supporter.id + end + + if expand.include? :user + json.user user&.to_builder + else + json.user user&.id + end + end + end + + private + def publish_create + Houdini.event_publisher.announce(:supporter_note_created, to_event('supporter_note.created', :supporter, :nonprofit, :user).attributes!) + end + + def publish_updated + if !deleted + Houdini.event_publisher.announce(:supporter_note_updated, to_event('supporter_note.updated', :supporter, :nonprofit, :user).attributes!) + end + end + + def publish_deleted + Houdini.event_publisher.announce(:supporter_note_deleted, to_event('supporter_note.deleted', :supporter, :nonprofit, :user).attributes!) + end end diff --git a/app/models/user.rb b/app/models/user.rb index ea90603d..9fdb48d7 100755 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -98,4 +98,11 @@ class User < ApplicationRecord # self.geocode # self.save end + + def to_builder(*expand) + Jbuilder.new do |json| + json.object "user" + json.id id + end + end end diff --git a/docs/event_definitions/Nonprofit/Supporter/SupporterNote.ts b/docs/event_definitions/Nonprofit/Supporter/SupporterNote.ts new file mode 100644 index 00000000..39a2b1e8 --- /dev/null +++ b/docs/event_definitions/Nonprofit/Supporter/SupporterNote.ts @@ -0,0 +1,18 @@ +// License: LGPL-3.0-or-later +import type { IdType, HoudiniObject, HoudiniEvent } from '../../common'; +import type Nonprofit from '..'; +import Supporter from '.'; +import type { User } from '../../User'; + +export interface SupporterNote extends HoudiniObject { + content: string; + deleted: boolean; + nonprofit: IdType | Nonprofit; + object: "supporter_note"; + supporter: IdType | Supporter; + user: IdType | User; +} + +export type SupporterNoteCreated = HoudiniEvent<'supporter_note.created', SupporterNote>; +export type SupporterNoteUpdated = HoudiniEvent<'supporter_note.updated', SupporterNote>; +export type SupporterNoteDeleted = HoudiniEvent<'supporter_note.deleted', SupporterNote>; \ No newline at end of file diff --git a/docs/event_definitions/Nonprofit/Supporter/index.ts b/docs/event_definitions/Nonprofit/Supporter/index.ts new file mode 100644 index 00000000..59f8222c --- /dev/null +++ b/docs/event_definitions/Nonprofit/Supporter/index.ts @@ -0,0 +1,14 @@ +// License: LGPL-3.0-or-later +import type { IdType, HoudiniObject } from '../../common'; +import type Nonprofit from '../'; + +export default interface Supporter extends HoudiniObject { + deleted: boolean; + email: string; + name: string; + nonprofit: IdType | Nonprofit; + object: "supporter"; + organization: string; +} + +export * from './SupporterNote'; diff --git a/docs/event_definitions/User.ts b/docs/event_definitions/User.ts new file mode 100644 index 00000000..423b5831 --- /dev/null +++ b/docs/event_definitions/User.ts @@ -0,0 +1,7 @@ +// License: LGPL-3.0-or-later +import { IdType, HoudiniObject } from './common'; + +export interface User extends HoudiniObject { + object: 'user'; +} + diff --git a/lib/insert/insert_activities.rb b/lib/insert/insert_activities.rb index 5ff75c1b..7d8451f5 100644 --- a/lib/insert/insert_activities.rb +++ b/lib/insert/insert_activities.rb @@ -163,27 +163,19 @@ module InsertActivities .left_join(:users, 'users.id=supporter_emails.user_id') end - def self.for_supporter_notes(ids) - insert_supporter_notes_expr - .and_where('supporter_notes.id IN ($ids)', ids: ids) - .execute - end - - def self.insert_supporter_notes_expr - Qx.insert_into(:activities, insert_cols.concat(['user_id'])) - .select(defaults.concat([ - 'supporter_notes.supporter_id', - "'SupporterEmail' AS attachment_type", - 'supporter_notes.id AS attachment_id', - 'supporters.nonprofit_id', - 'supporter_notes.created_at AS date', - "json_build_object('content', supporter_notes.content, 'user_email', users.email)", - "'SupporterNote' AS kind", - 'users.id AS user_id' - ])) - .from(:supporter_notes) - .join('supporters', 'supporters.id=supporter_notes.supporter_id') - .add_left_join(:users, 'users.id=supporter_notes.user_id') + def self.for_supporter_notes(notes) + notes.map do |note| + note.activities.create(supporter: note.supporter, + nonprofit: note.supporter.nonprofit, + date: note.created_at, + kind: 'SupporterNote', + user: note.user, + json_data: { + content: note.content, + user_email: note.user&.email + } + ) + end end def self.for_offsite_donations(payment_ids) diff --git a/lib/insert/insert_supporter_notes.rb b/lib/insert/insert_supporter_notes.rb index ee8ab7ab..6a388ed8 100644 --- a/lib/insert/insert_supporter_notes.rb +++ b/lib/insert/insert_supporter_notes.rb @@ -2,23 +2,21 @@ # 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 'param_validation' -require 'qx' module InsertSupporterNotes - def self.create(notes) - ParamValidation.new(notes, - root: { array_of_hashes: { - supporter_id: { required: true, is_integer: true }, - user_id: { required: true, is_integer: true }, - content: { required: true } - } }) - inserted = Qx.insert_into(:supporter_notes) - .values(notes) - .timestamps - .returning('*') - .execute - InsertActivities.for_supporter_notes(inserted.map { |h| h['id'] }) + #note_supporter_users : array of hashes + # each hash: + # supporter: Supporter new note should belong to + # user: User creating the note + # note: parameters to pass into the note + def self.create(*note_supporter_users) + inserted = nil + ActiveRecord::Base.transaction do + inserted = note_supporter_users.map do |nsu| + nsu[:supporter].supporter_notes.create(nsu[:note].merge({user: nsu[:user]})) + end + InsertActivities.for_supporter_notes(inserted) + end inserted end end diff --git a/lib/pay_recurring_donation.rb b/lib/pay_recurring_donation.rb index 8369b041..c9ad7338 100644 --- a/lib/pay_recurring_donation.rb +++ b/lib/pay_recurring_donation.rb @@ -86,7 +86,7 @@ module PayRecurringDonation rd.save! result['recurring_donation'] = rd Houdini.event_publisher.announce(:recurring_donation_payment_failed, donation) - InsertSupporterNotes.create([{ content: "This supporter had a payment failure for their recurring donation with ID #{rd_id}", supporter_id: donation['supporter_id'], user_id: 540 }]) + InsertSupporterNotes.create({supporter:Supporter.find(donation['supporter_id']), user: User.find(540), note:{ content: "This supporter had a payment failure for their recurring donation with ID #{rd_id}"}}) end result end diff --git a/lib/update/update_activities.rb b/lib/update/update_activities.rb index 2c525d50..30354d37 100644 --- a/lib/update/update_activities.rb +++ b/lib/update/update_activities.rb @@ -5,17 +5,9 @@ require 'qx' module UpdateActivities - def self.for_supporter_notes(note) - user_email = Qx.select('email') - .from(:users) - .where(id: note[:user_id]) - .execute - .first['email'] - - Qx.update(:activities) - .set(json_data: { content: note[:content], user_email: user_email }.to_json) - .timestamps - .where(attachment_id: note[:id]) - .execute + def self.for_supporter_notes(supporter_note) + user_email = supporter_note.user.email + supporter_note.activities.update_all(json_data: { content: supporter_note.content, user_email: user_email }.to_json, + updated_at: DateTime.now) end end diff --git a/lib/update/update_recurring_donations.rb b/lib/update/update_recurring_donations.rb index 44d9905d..701f3a68 100644 --- a/lib/update/update_recurring_donations.rb +++ b/lib/update/update_recurring_donations.rb @@ -96,7 +96,7 @@ module UpdateRecurringDonations .where('id=$id', id: rd_id.to_i) ) rd = QueryRecurringDonations.fetch_for_edit(rd_id)['recurring_donation'] - InsertSupporterNotes.create([{ supporter_id: rd['supporter_id'], content: "This supporter's recurring donation for $#{Format::Currency.cents_to_dollars(rd['amount'])} was cancelled by #{rd['cancelled_by']} on #{Format::Date.simple(rd['cancelled_at'])}", user_id: 540 }]) + InsertSupporterNotes.create({ supporter: Supporter.find(rd['supporter_id']), user: nil, note: {content: "This supporter's recurring donation for $#{Format::Currency.cents_to_dollars(rd['amount'])} was cancelled by #{rd['cancelled_by']} on #{Format::Date.simple(rd['cancelled_at'])}"}}) unless dont_notify_nonprofit RecurringDonationCancelledJob.perform_later(Donation.find(rd['donation_id'])) end diff --git a/lib/update/update_supporter_notes.rb b/lib/update/update_supporter_notes.rb index 27c5f5f3..176c70f7 100644 --- a/lib/update/update_supporter_notes.rb +++ b/lib/update/update_supporter_notes.rb @@ -2,25 +2,26 @@ # 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 'qx' - module UpdateSupporterNotes - def self.update(note) - Qx.update(:supporter_notes) - .set(content: note[:content], user_id: note[:user_id]) - .timestamps - .where(id: note[:id]) - .execute - UpdateActivities.for_supporter_notes(note) + # should this get put into a callback on SupporterNote? Probably but + # not sure how right now. + def self.update(supporter_note, params) + ActiveRecord::Base.transaction do + supporter_note.update params + supporter_note.save! + UpdateActivities.for_supporter_notes(note) + end end # sets the deleted column to true on supporter_notes (soft delete) # and then does a hard delete on the associated activity - def self.delete(id) - Qx.update(:supporter_notes) - .set(deleted: true) - .where(id: id) - .execute - Qx.delete_from(:activities).where(attachment_id: id).execute + # + # should this get put into a callback on SupporterNote? Probably but + # not sure how right now. + def self.delete(supporter_note) + ActiveRecord::Base.transaction do + supporter_note.discard! + supporter_note.activities.destroy_all + end end end diff --git a/spec/lib/insert/insert_supporter_notes_spec.rb b/spec/lib/insert/insert_supporter_notes_spec.rb new file mode 100644 index 00000000..189c626c --- /dev/null +++ b/spec/lib/insert/insert_supporter_notes_spec.rb @@ -0,0 +1,38 @@ +# 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' + +describe InsertSupporterNotes do + include_context :shared_rd_donation_value_context + let(:content) { "CONTENT"} + let(:content_2) {"CONTENT 2"} + let(:sn_first) {SupporterNote.first } + let(:sn_last) {SupporterNote.last } + it '.create' do + InsertSupporterNotes.create({supporter:supporter, user: user, note: {content: content}}, + {supporter:supporter, user: user, note: {content: content_2}}) + expect(SupporterNote.count).to eq 2 + expect(sn_first.attributes.except('id')).to eq({ + 'content' => content, + 'user_id' => user.id, + 'deleted' => false, + 'supporter_id' => supporter.id, + 'created_at' => Time.now, + 'updated_at' => Time.now + }) + + expect(sn_first.activities.count).to eq 1 + + expect(sn_last.attributes.except('id')).to eq({ + 'content' => content_2, + 'user_id' => user.id, + 'deleted' => false, + 'supporter_id' => supporter.id, + 'created_at' => Time.now, + 'updated_at' => Time.now + }) + expect(sn_last.activities.count).to eq 1 + end +end diff --git a/spec/models/supporter_note_spec.rb b/spec/models/supporter_note_spec.rb new file mode 100644 index 00000000..757e033f --- /dev/null +++ b/spec/models/supporter_note_spec.rb @@ -0,0 +1,114 @@ +# 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 SupporterNote, type: :model do + include_context :shared_donation_charge_context + let(:content) { "CONTENT"} + let(:content2) {"CONTENT2"} + + let(:supporter_note) { supporter.supporter_notes.create(content: content, user: user) } + it 'creates' do + expect(supporter_note.errors).to be_empty + end + + it 'announces created' do + expect(Houdini.event_publisher).to receive(:announce).with(:supporter_note_created, { + 'id' => kind_of(String), + 'object' => 'object_event', + 'type' => 'supporter_note.created', + 'data' => { + 'object' => { + 'id'=> kind_of(Numeric), + 'deleted' => false, + 'content' => content, + 'nonprofit'=> { + 'id' => nonprofit.id, + 'name' => nonprofit.name, + 'object' => 'nonprofit' + }, + 'object' => 'supporter_note', + 'user' => { + 'id' => user.id, + 'object' => 'user' + }, + 'supporter' => { + 'id' => supporter.id, + 'object' => 'supporter' + } + } + } + }) + + supporter_note + end + + it 'announces updated' do + 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), + 'object' => 'object_event', + 'type' => 'supporter_note.updated', + 'data' => { + 'object' => { + 'id'=> kind_of(Numeric), + 'deleted' => false, + 'content' => content2, + 'nonprofit'=> { + 'id' => nonprofit.id, + 'name' => nonprofit.name, + 'object' => 'nonprofit' + }, + 'object' => 'supporter_note', + 'user' => { + 'id' => user.id, + 'object' => 'user' + }, + 'supporter' => { + 'id' => supporter.id, + 'object' => 'supporter' + } + } + } + }).ordered + + supporter_note + supporter_note.content = content2 + supporter_note.save! + end + + it 'announces deleted' do + 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), + 'object' => 'object_event', + 'type' => 'supporter_note.deleted', + 'data' => { + 'object' => { + 'id'=> kind_of(Numeric), + 'deleted' => true, + 'content' => content, + 'nonprofit'=> { + 'id' => nonprofit.id, + 'name' => nonprofit.name, + 'object' => 'nonprofit' + }, + 'object' => 'supporter_note', + 'user' => { + 'id' => user.id, + 'object' => 'user' + }, + 'supporter' => { + 'id' => supporter.id, + 'object' => 'supporter' + } + } + } + }).ordered + + supporter_note.discard! + + end +end