Support for campaign_gift_option events

This commit is contained in:
Eric Schultz 2021-01-15 17:10:37 -06:00 committed by Eric Schultz
parent 0a1f07e76b
commit 6097ad68b7
10 changed files with 501 additions and 69 deletions

View file

@ -18,15 +18,12 @@ class CampaignGiftOptionsController < ApplicationController
end end
def create def create
campaign = current_campaign json_saved CreateCampaignGiftOption.create(current_campaign, campaign_gift_option_params),
json_saved CreateCampaignGiftOption.create(campaign, campaign_gift_option_params),
'Gift option successfully created!' 'Gift option successfully created!'
end end
def update def update
@campaign = current_campaign json_saved UpdateCampaignGiftOption.update(current_campaign_gift_option, campaign_gift_option_params), 'Successfully updated'
gift_option = @campaign.campaign_gift_options.find params[:id]
json_saved UpdateCampaignGiftOption.update(gift_option, campaign_gift_option_params), 'Successfully updated'
end end
# put /nonprofits/:nonprofit_id/campaigns/:campaign_id/campaign_gift_options/update_order # put /nonprofits/:nonprofit_id/campaigns/:campaign_id/campaign_gift_options/update_order
@ -37,9 +34,7 @@ class CampaignGiftOptionsController < ApplicationController
end end
def destroy def destroy
@campaign = current_campaign render_json { DeleteCampaignGiftOption.delete(current_campaign_gift_option) }
render_json { DeleteCampaignGiftOption.delete(@campaign, params[:id]) }
end end
private private
@ -47,4 +42,8 @@ class CampaignGiftOptionsController < ApplicationController
def campaign_gift_option_params def campaign_gift_option_params
params.require(:campaign_gift_option).permit(:amount_one_time, :amount_recurring, :amount_dollars, :description, :name, :campaign, :quantity, :to_ship, :order, :hide_contributions) params.require(:campaign_gift_option).permit(:amount_one_time, :amount_recurring, :amount_dollars, :description, :name, :campaign, :quantity, :to_ship, :order, :hide_contributions)
end end
def current_campaign_gift_option
campaign.campaign_gift_options.find(params[:id])
end
end end

View file

@ -195,4 +195,12 @@ class Campaign < ApplicationRecord
where('campaigns.id = ? OR campaigns.parent_campaign_id = ? ',campaign, campaign) where('campaigns.id = ? OR campaigns.parent_campaign_id = ? ',campaign, campaign)
end end
def to_builder(*expand)
Jbuilder.new do |json|
json.(self, :id, :name)
json.object "campaign"
json.nonprofit nonprofit.id
end
end
end end

View file

@ -3,6 +3,8 @@
# License: AGPL-3.0-or-later WITH WTO-AP-3.0-or-later # 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 # Full license explanation at https://github.com/houdiniproject/houdini/blob/master/LICENSE
class CampaignGiftOption < ApplicationRecord class CampaignGiftOption < ApplicationRecord
include ObjectEvent::ModelExtensions
object_eventable
# :amount_one_time, #int (cents) # :amount_one_time, #int (cents)
# :amount_recurring, #int (cents) # :amount_recurring, #int (cents)
# :amount_dollars, #str, gets converted to amount # :amount_dollars, #str, gets converted to amount
@ -23,6 +25,10 @@ class CampaignGiftOption < ApplicationRecord
validates :amount_one_time, presence: true, numericality: { only_integer: true }, unless: :amount_recurring validates :amount_one_time, presence: true, numericality: { only_integer: true }, unless: :amount_recurring
validates :amount_recurring, presence: true, numericality: { only_integer: true }, unless: :amount_one_time validates :amount_recurring, presence: true, numericality: { only_integer: true }, unless: :amount_one_time
after_create_commit :publish_created
after_update_commit :publish_updated
after_destroy_commit :publish_deleted
def total_gifts def total_gifts
campaign_gifts.count campaign_gifts.count
end end
@ -32,4 +38,71 @@ class CampaignGiftOption < ApplicationRecord
h[:total_gifts] = total_gifts h[:total_gifts] = total_gifts
h h
end end
def to_builder(*expand)
gift_option_amount = []
if amount_one_time
gift_option_amount.push({
amount:{
value_in_cents: amount_one_time,
currency: campaign.nonprofit.currency
}
})
end
if amount_recurring
gift_option_amount.push({
amount:{
value_in_cents: amount_recurring,
currency: campaign.nonprofit.currency
},
recurrence: {
type: 'monthly',
interval: 1
}
})
end
Jbuilder.new do |json|
json.(self, :id, :name, :description,
:hide_contributions, :order, :to_ship)
if quantity
json.quantity quantity
end
json.object 'campaign_gift_option'
json.deleted !persisted?
json.gift_option_amount gift_option_amount do |desc|
json.amount desc[:amount]
json.recurrence(desc[:recurrence]) if desc[:recurrence]
end
if expand.include? :nonprofit
json.nonprofit campaign.nonprofit.to_builder
else
json.nonprofit campaign.nonprofit.id
end
if expand.include? :campaign
json.campaign campaign.to_builder
else
json.campaign campaign.id
end
end
end
private
def publish_created
Houdini.event_publisher.announce(:campaign_gift_option_created, to_event('campaign_gift_option.created', :nonprofit, :campaign).attributes!)
end
def publish_updated
Houdini.event_publisher.announce(:campaign_gift_option_updated, to_event('campaign_gift_option.updated', :nonprofit, :campaign).attributes!)
end
def publish_deleted
Houdini.event_publisher.announce(:campaign_gift_option_deleted, to_event('campaign_gift_option.deleted', :nonprofit, :campaign).attributes!)
end
end end

View file

@ -0,0 +1,31 @@
// License: LGPL-3.0-or-later
import type { IdType, HoudiniObject, HoudiniEvent, Amount, RecurrenceRule } from '../../common';
import type Nonprofit from '..';
import type Campaign from '.';
interface OneTimeGiftOptionAmount {
amount: Amount;
}
interface RecurringGiftOptionAmount {
amount: Amount;
recurrence: RecurrenceRule;
}
export interface CampaignGiftOption extends HoudiniObject {
campaign: IdType | Campaign;
deleted: boolean;
description: string;
gift_option_amount: Array<RecurringGiftOptionAmount | OneTimeGiftOptionAmount>;
hide_contributions: boolean;
name: string;
nonprofit: IdType | Nonprofit;
object: "campaign_gift_option";
order: number;
quantity?: number;
to_ship: boolean;
}
export type CampaignGiftOptionCreated = HoudiniEvent<'campaign_gift_option.created', CampaignGiftOption>;
export type CampaignGiftOptionUpdated = HoudiniEvent<'campaign_gift_option.updated', CampaignGiftOption>;
export type CampaignGiftOptionDeleted = HoudiniEvent<'campaign_gift_option.deleted', CampaignGiftOption>;

View file

@ -0,0 +1,11 @@
// License: LGPL-3.0-or-later
import { IdType, HoudiniObject } from '../../common';
import Nonprofit from '../';
export default interface Campaign extends HoudiniObject {
name: string;
nonprofit: IdType | Nonprofit;
object: "campaign";
}
export * from './CampaignGiftOption';

View file

@ -18,6 +18,38 @@ export type Amount = { currency: string, value_in_cents: string };
*/ */
export type FlexibleAmount = Amount | string | number; export type FlexibleAmount = Amount | string | number;
/**
* A rule for something recurring. Used for recurring donations. Based on `ice_cube` gem format
*
* @example
* // Recur once a month, for 3 times
* { count: 3, interval: 1, type: 'monthly' }
* @example
* // Recur every other month, stop on June 1, 2021
* { interval: 2, type: 'monthly', until: new Date(2021, 6, 1) }
* @example
* // Recur every year
* { interval: 1, type: 'yearly' }
*/
export type RecurrenceRule = {
/**
* The number of times we should run the recurrence
*/
count?: number;
/**
* Interval of `type` for the event to recur
*/
interval: number;
/**
* The scale of the recurrence
*/
type: 'monthly' | 'year';
/**
* The the point after which the rule should not recur anymore.
*/
until?: Date;
};
/** /**
* Every object controlled by the Houdini event publisher must meet this standard interface * Every object controlled by the Houdini event publisher must meet this standard interface

View file

@ -3,30 +3,15 @@
# License: AGPL-3.0-or-later WITH WTO-AP-3.0-or-later # 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 # Full license explanation at https://github.com/houdiniproject/houdini/blob/master/LICENSE
module DeleteCampaignGiftOption module DeleteCampaignGiftOption
def self.delete(campaign, campaign_gift_option_id) def self.delete(campaign_gift_option)
ParamValidation.new({ campaign: campaign, CampaignGiftOption.transaction do
campaign_gift_option_id: campaign_gift_option_id }, if campaign_gift_option.campaign_gifts.any?
campaign: { raise ParamValidation::ValidationError.new("#{campaign_gift_option&.id} already has campaign gifts. It can't be deleted for safety reasons.", key: :campaign_gift_option_id)
required: true,
is_a: Campaign
},
campaign_gift_option_id: {
required: true,
is_integer: true
})
Qx.transaction do
cgo = campaign.campaign_gift_options.where('id = ? ', campaign_gift_option_id).first
unless cgo
raise ParamValidation::ValidationError.new("#{campaign_gift_option_id} is not a valid gift option for campaign #{campaign.id}", key: :campaign_gift_option_id)
end end
if cgo.campaign_gifts.any? campaign_gift_option.destroy
raise ParamValidation::ValidationError.new("#{campaign_gift_option_id} already has campaign gifts. It can't be deleted for safety reasons.", key: :campaign_gift_option_id)
end
cgo.destroy campaign_gift_option
return cgo
end end
end end
end end

View file

@ -127,7 +127,8 @@ describe CreateCampaignGift do
describe 'successful insert' do describe 'successful insert' do
let(:profile) { force_create(:profile, user: force_create(:user)) } let(:profile) { force_create(:profile, user: force_create(:user)) }
let(:campaign) { force_create(:campaign, profile: profile) } let(:nonprofit) {create(:nm_justice)}
let(:campaign) { force_create(:campaign, profile: profile, nonprofit: nonprofit) }
describe 'insert with no option quantity limit' do describe 'insert with no option quantity limit' do
let(:campaign_gift_option) { force_create(:campaign_gift_option, campaign: campaign, amount_recurring: 300, amount_one_time: 5000) } let(:campaign_gift_option) { force_create(:campaign_gift_option, campaign: campaign, amount_recurring: 300, amount_one_time: 5000) }

View file

@ -6,51 +6,16 @@ require 'rails_helper'
describe DeleteCampaignGiftOption do describe DeleteCampaignGiftOption do
describe '.delete' do describe '.delete' do
let(:nonprofit) { create(:nm_justice)}
let(:profile) { force_create(:profile, user: force_create(:user)) } let(:profile) { force_create(:profile, user: force_create(:user)) }
let(:campaign) { force_create(:campaign, profile: profile) } let(:campaign) { force_create(:campaign, profile: profile, nonprofit:nonprofit) }
let(:campaign_gift_option) { force_create(:campaign_gift_option, campaign: campaign) } let(:campaign_gift_option) { force_create(:campaign_gift_option, campaign: campaign) }
let(:campaign_gift) { force_create(:campaign_gift, campaign_gift_option: campaign_gift_option) } let(:campaign_gift) { force_create(:campaign_gift, campaign_gift_option: campaign_gift_option) }
describe 'param validation' do
it 'does basic validation' do
expect do
DeleteCampaignGiftOption.delete(nil,
nil)
end
.to(raise_error do |error|
expect(error).to be_a ParamValidation::ValidationError
expect_validation_errors(error.data, [
{
key: :campaign,
name: :required
},
{
key: :campaign,
name: :is_a
},
{
key: :campaign_gift_option_id,
name: :required
},
{
key: :campaign_gift_option_id,
name: :is_integer
}
])
end)
end
it 'does cgo verification' do
expect { DeleteCampaignGiftOption.delete(campaign, 5555) }.to(raise_error do |error|
expect(error).to be_a ParamValidation::ValidationError
expect_validation_errors(error.data, [{ key: :campaign_gift_option_id }])
end)
end
end
it 'cgo deletion is rejected because a gift has already been sold' do it 'cgo deletion is rejected because a gift has already been sold' do
campaign_gift_option campaign_gift_option
campaign_gift campaign_gift
expect { DeleteCampaignGiftOption.delete(campaign, campaign_gift_option.id) }.to(raise_error do |error| expect { DeleteCampaignGiftOption.delete(campaign_gift_option) }.to(raise_error do |error|
expect(error).to be_a ParamValidation::ValidationError expect(error).to be_a ParamValidation::ValidationError
expect_validation_errors(error.data, [{ key: :campaign_gift_option_id }]) expect_validation_errors(error.data, [{ key: :campaign_gift_option_id }])
expect(error.message).to eq("#{campaign_gift_option.id} already has campaign gifts. It can't be deleted for safety reasons.") expect(error.message).to eq("#{campaign_gift_option.id} already has campaign gifts. It can't be deleted for safety reasons.")
@ -62,7 +27,7 @@ describe DeleteCampaignGiftOption do
it 'cgo deletion succeeds' do it 'cgo deletion succeeds' do
Timecop.freeze(2020, 10, 12) do Timecop.freeze(2020, 10, 12) do
campaign_gift_option campaign_gift_option
result = DeleteCampaignGiftOption.delete(campaign, campaign_gift_option.id) result = DeleteCampaignGiftOption.delete(campaign_gift_option)
expect(result).to be_a CampaignGiftOption expect(result).to be_a CampaignGiftOption
expect(result.attributes).to eq campaign_gift_option.attributes expect(result.attributes).to eq campaign_gift_option.attributes
expect(CampaignGiftOption.any?).to eq false expect(CampaignGiftOption.any?).to eq false

View file

@ -0,0 +1,327 @@
# 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 CampaignGiftOption, type: :model do
include_context :shared_donation_charge_context
let(:name) {"CUSTOM GIFT OPTION"}
let(:amount_one_time) { 400}
let(:amount_recurring) { 100}
let(:description) { " Fun description!"}
let(:to_ship) { true}
let(:order) { 3}
let(:campaign_gift_option) do
campaign.campaign_gift_options.create(description: description,
amount_one_time: amount_one_time, amount_recurring: amount_recurring,
name: name, to_ship: to_ship, order: order
)
end
# campaign_gift_option with quantity but no to_ship
let(:campaign_gift_option_2) do
campaign.campaign_gift_options.create(description: description,
amount_one_time: amount_one_time, amount_recurring: amount_recurring,
name: name, quantity: 40, hide_contributions: true, order: order
)
end
describe 'validate' do
it 'has no errors on first example' do
expect(campaign_gift_option.errors).to be_empty
end
it 'has no errors on second example' do
expect(campaign_gift_option_2.errors).to be_empty
end
end
describe 'create' do
it 'announces create for first example' do
expect(Houdini.event_publisher).to receive(:announce).with(:campaign_create, anything).ordered
expect(Houdini.event_publisher).to receive(:announce).with(:campaign_gift_option_created, {
'id' => kind_of(String),
'object' => 'object_event',
'type' => 'campaign_gift_option.created',
'data' => {
'object' => {
'campaign' => {
'id' => campaign.id,
'name' => campaign.name,
'nonprofit' => nonprofit.id,
'object' => 'campaign'
},
'deleted' => false,
'description' => description,
'gift_option_amount' => [
{'amount' => {value_in_cents:amount_one_time, currency: nonprofit.currency}},
{
'amount' => {value_in_cents:amount_recurring, currency: nonprofit.currency},
'recurrence' => { interval: 1, type: 'monthly'}
}
],
'id'=> kind_of(Numeric),
'hide_contributions' => false,
'name' => name,
'nonprofit'=> {
'id' => nonprofit.id,
'name' => nonprofit.name,
'object' => 'nonprofit'
},
'object' => 'campaign_gift_option',
'order' => order,
'to_ship' => true
}
}
})
campaign_gift_option
end
it 'announces create for second example' do
expect(Houdini.event_publisher).to receive(:announce).with(:campaign_create, anything).ordered
expect(Houdini.event_publisher).to receive(:announce).with(:campaign_gift_option_created, {
'id' => kind_of(String),
'object' => 'object_event',
'type' => 'campaign_gift_option.created',
'data' => {
'object' => {
'campaign' => {
'id' => campaign.id,
'name' => campaign.name,
'nonprofit' => nonprofit.id,
'object' => 'campaign'
},
'deleted' => false,
'description' => description,
'gift_option_amount' => [
{'amount' => {value_in_cents:amount_one_time, currency: nonprofit.currency}},
{
'amount' => {value_in_cents:amount_recurring, currency: nonprofit.currency},
'recurrence' => { interval: 1, type: 'monthly'}
}
],
'id'=> kind_of(Numeric),
'hide_contributions' => true,
'name' => name,
'nonprofit'=> {
'id' => nonprofit.id,
'name' => nonprofit.name,
'object' => 'nonprofit'
},
'object' => 'campaign_gift_option',
'order' => order,
'quantity' => 40,
'to_ship' => false
}
}
})
campaign_gift_option_2
end
end
describe 'update' do
let(:cgo_update) do
campaign_gift_option.amount_one_time = nil
campaign_gift_option.save
campaign_gift_option
end
let(:cgo2_update) do
campaign_gift_option_2.amount_recurring = nil
campaign_gift_option_2.hide_contributions = false
campaign_gift_option_2.save
campaign_gift_option_2
end
it 'is without error on first example' do
expect(cgo_update.errors).to be_empty
end
it 'announces update for first example' do
expect(Houdini.event_publisher).to receive(:announce).with(:campaign_create, anything).ordered
expect(Houdini.event_publisher).to receive(:announce).with(:campaign_gift_option_created, anything).ordered
expect(Houdini.event_publisher).to receive(:announce).with(:campaign_gift_option_updated, {
'id' => kind_of(String),
'object' => 'object_event',
'type' => 'campaign_gift_option.updated',
'data' => {
'object' => {
'campaign' => {
'id' => campaign.id,
'name' => campaign.name,
'nonprofit' => nonprofit.id,
'object' => 'campaign'
},
'deleted' => false,
'description' => description,
'gift_option_amount' => [
{
'amount' => {value_in_cents:amount_recurring, currency: nonprofit.currency},
'recurrence' => { interval: 1, type: 'monthly'}
}
],
'id'=> kind_of(Numeric),
'hide_contributions' => false,
'name' => name,
'nonprofit'=> {
'id' => nonprofit.id,
'name' => nonprofit.name,
'object' => 'nonprofit'
},
'object' => 'campaign_gift_option',
'order' => order,
'to_ship' => true
}
}
}).ordered
cgo_update
end
it 'is without error on second example' do
expect(cgo_update.errors).to be_empty
end
it 'announces update for second example' do
expect(Houdini.event_publisher).to receive(:announce).with(:campaign_create, anything).ordered
expect(Houdini.event_publisher).to receive(:announce).with(:campaign_gift_option_created, anything).ordered
expect(Houdini.event_publisher).to receive(:announce).with(:campaign_gift_option_updated, {
'id' => kind_of(String),
'object' => 'object_event',
'type' => 'campaign_gift_option.updated',
'data' => {
'object' => {
'campaign' => {
'id' => campaign.id,
'name' => campaign.name,
'nonprofit' => nonprofit.id,
'object' => 'campaign'
},
'deleted' => false,
'description' => description,
'gift_option_amount' => [
{'amount' => {value_in_cents:amount_one_time, currency: nonprofit.currency}},
],
'id'=> kind_of(Numeric),
'hide_contributions' => false,
'name' => name,
'nonprofit'=> {
'id' => nonprofit.id,
'name' => nonprofit.name,
'object' => 'nonprofit'
},
'object' => 'campaign_gift_option',
'order' => order,
'quantity' => 40,
'to_ship' => false
}
}
}).ordered
cgo2_update
end
end
describe 'deleted' do
it 'is without error on first example' do
campaign_gift_option.destroy
expect(campaign_gift_option).to_not be_persisted
end
it 'announces deleted for first example' do
expect(Houdini.event_publisher).to receive(:announce).with(:campaign_create, anything).ordered
expect(Houdini.event_publisher).to receive(:announce).with(:campaign_gift_option_created, anything).ordered
expect(Houdini.event_publisher).to receive(:announce).with(:campaign_gift_option_deleted, {
'id' => kind_of(String),
'object' => 'object_event',
'type' => 'campaign_gift_option.deleted',
'data' => {
'object' => {
'campaign' => {
'id' => campaign.id,
'name' => campaign.name,
'nonprofit' => nonprofit.id,
'object' => 'campaign'
},
'deleted' => true,
'description' => description,
'gift_option_amount' => [
{'amount' => {value_in_cents:amount_one_time, currency: nonprofit.currency}},
{
'amount' => {value_in_cents:amount_recurring, currency: nonprofit.currency},
'recurrence' => { interval: 1, type: 'monthly'}
}
],
'id'=> kind_of(Numeric),
'hide_contributions' => false,
'name' => name,
'nonprofit'=> {
'id' => nonprofit.id,
'name' => nonprofit.name,
'object' => 'nonprofit'
},
'object' => 'campaign_gift_option',
'order' => order,
'to_ship' => true
}
}
})
campaign_gift_option.destroy
end
it 'is without error on second example' do
campaign_gift_option_2.destroy
expect(campaign_gift_option_2).to_not be_persisted
end
it 'announces deleted for second example' do
expect(Houdini.event_publisher).to receive(:announce).with(:campaign_create, anything).ordered
expect(Houdini.event_publisher).to receive(:announce).with(:campaign_gift_option_created, anything).ordered
expect(Houdini.event_publisher).to receive(:announce).with(:campaign_gift_option_deleted, {
'id' => kind_of(String),
'object' => 'object_event',
'type' => 'campaign_gift_option.deleted',
'data' => {
'object' => {
'campaign' => {
'id' => campaign.id,
'name' => campaign.name,
'nonprofit' => nonprofit.id,
'object' => 'campaign'
},
'deleted' => true,
'description' => description,
'gift_option_amount' => [
{'amount' => {value_in_cents:amount_one_time, currency: nonprofit.currency}},
{
'amount' => {value_in_cents:amount_recurring, currency: nonprofit.currency},
'recurrence' => { interval: 1, type: 'monthly'}
}
],
'id'=> kind_of(Numeric),
'hide_contributions' => true,
'name' => name,
'nonprofit'=> {
'id' => nonprofit.id,
'name' => nonprofit.name,
'object' => 'nonprofit'
},
'object' => 'campaign_gift_option',
'order' => order,
'quantity' => 40,
'to_ship' => false
}
}
})
campaign_gift_option_2.destroy
end
end
end