Add support for supporter_note.* events
This commit is contained in:
parent
5011f2d748
commit
70d18591c3
16 changed files with 346 additions and 91 deletions
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
18
docs/event_definitions/Nonprofit/Supporter/SupporterNote.ts
Normal file
18
docs/event_definitions/Nonprofit/Supporter/SupporterNote.ts
Normal 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>;
|
14
docs/event_definitions/Nonprofit/Supporter/index.ts
Normal file
14
docs/event_definitions/Nonprofit/Supporter/index.ts
Normal 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';
|
7
docs/event_definitions/User.ts
Normal file
7
docs/event_definitions/User.ts
Normal file
|
@ -0,0 +1,7 @@
|
|||
// License: LGPL-3.0-or-later
|
||||
import { IdType, HoudiniObject } from './common';
|
||||
|
||||
export interface User extends HoudiniObject {
|
||||
object: 'user';
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
38
spec/lib/insert/insert_supporter_notes_spec.rb
Normal file
38
spec/lib/insert/insert_supporter_notes_spec.rb
Normal 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
|
114
spec/models/supporter_note_spec.rb
Normal file
114
spec/models/supporter_note_spec.rb
Normal 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
|
Loading…
Reference in a new issue