Add support for supporter_note.* events

This commit is contained in:
Eric Schultz 2021-01-14 15:33:10 -06:00 committed by Eric Schultz
parent 5011f2d748
commit 70d18591c3
16 changed files with 346 additions and 91 deletions

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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>;

View file

@ -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';

View file

@ -0,0 +1,7 @@
// License: LGPL-3.0-or-later
import { IdType, HoudiniObject } from './common';
export interface User extends HoudiniObject {
object: 'user';
}

View file

@ -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)

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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