Add ticket_level object events

This commit is contained in:
Eric Schultz 2021-01-11 16:34:48 -06:00 committed by Eric Schultz
parent 4466df6b0d
commit 2893a2bf47
14 changed files with 594 additions and 8 deletions

View file

@ -46,6 +46,6 @@ class TicketLevelsController < ApplicationController
end
def ticket_level_params
params.require(:ticket_level).permit(:amount, :amount_dollars, :name, :description, :quantity, :deleted, :event_id, :admin_only, :limit, :order)
params.require(:ticket_level).permit(:amount, :amount_dollars, :name, :description, :quantity, :event_id, :admin_only, :limit, :order)
end
end

View file

@ -101,6 +101,16 @@ class Event < ApplicationRecord
self
end
def to_builder(*expand)
Jbuilder.new do |json|
json.(self, :id, :name)
json.object "event"
json.nonprofit expand.include?(:nonprofit) && nonprofit ?
nonprofit.to_builder :
nonprofit && nonprofit.id
end
end
def url
"#{nonprofit.url}/events/#{slug}"
end

View file

@ -10,4 +10,17 @@ class EventDiscount < ApplicationRecord
belongs_to :event
has_many :tickets
def to_builder(*expand)
Jbuilder.new do |json|
json.(self, :id, :name)
if event
if expand.include? :event
json.event event.to_builder
else
json.event event.id
end
end
end
end
end

View file

@ -247,6 +247,13 @@ class Nonprofit < ApplicationRecord
Houdini.intl.all_currencies[currency.downcase.to_sym][:symbol]
end
def to_builder(*expand)
Jbuilder.new do |json|
json.(self, :id, :name)
json.object 'nonprofit'
end
end
private
def build_admin_role
role = user.roles.build(host: self, name: 'nonprofit_admin')

View file

@ -44,10 +44,7 @@ class TagMaster < ApplicationRecord
tag.(self, :id, :name, :deleted)
tag.object 'tag_master'
if expand.include? :nonprofit && nonprofit
tag.nonprofit do
tag.id nonprofit.id
tag.name nonprofit.name
end
tag.nonprofit nonprofit.to_builder
else
tag.nonprofit nonprofit && nonprofit.id
end

View file

@ -13,7 +13,15 @@ class TicketLevel < ApplicationRecord
# :admin_only, #bool, only admins can create tickets for this level
# :limit, #int: for limiting the number of tickets to be sold
# :order #int: order in which to be displayed
# TODO replace with Discard gem
define_model_callbacks :discard
after_discard :publish_delete
after_create :publish_create
after_update :publish_updated
attr_accessor :amount_dollars
has_many :tickets
@ -28,4 +36,76 @@ class TicketLevel < ApplicationRecord
self.amount = Format::Currency.dollars_to_cents(amount_dollars) if amount_dollars.present?
self.amount = 0 if amount.nil?
end
# 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, :name, :deleted, :order, :limit, :description)
json.object 'ticket_level'
json.amount do
json.value_in_cents amount || 0
json.currency event.nonprofit.currency
end
json.available_to admin_only ? 'admins' : 'everyone'
if event
if event.nonprofit
if expand.include? :nonprofit
json.nonprofit event.nonprofit.to_builder
else
json.nonprofit event.nonprofit.id
end
else
json.nonprofit nil
end
if expand.include? :event
json.event event.to_builder
else
json.event event.id
end
if expand.include? :event_discounts
json.event_discounts event.event_discounts do |disc|
disc.to_builder
end
else
json.event_discounts event.event_discounts.pluck(:id)
end
end
end
end
private
def publish_create
Houdini.event_publisher.announce(:ticket_level_created, to_event('ticket_level.created', :event, :nonprofit).attributes!)
end
def publish_updated
# we don't run update when we've really just discarded
unless deleted
Houdini.event_publisher.announce(:ticket_level_updated, to_event('ticket_level.updated', :event, :nonprofit).attributes!)
end
end
def publish_delete
Houdini.event_publisher.announce(:ticket_level_deleted, to_event('ticket_level.deleted', :event, :nonprofit).attributes!)
end
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 to_builder(*expand)
end
end
end
end

View file

@ -0,0 +1,15 @@
// License: LGPL-3.0-or-later
import type { IdType, HoudiniObject } from '../../common';
import type Nonprofit from '..';
import type Event from '.';
import type { TicketLevel } from './TicketLevel';
/**
* Describes an EventDiscount (shell)
*/
export interface EventDiscount extends HoudiniObject {
event: IdType | Event;
nonprofit: IdType | Nonprofit;
object: "event_discount";
ticket_levels: IdType[] | TicketLevel[];
}

View file

@ -0,0 +1,44 @@
// License: LGPL-3.0-or-later
import { IdType, HoudiniObject, HoudiniEvent, Amount } from '../../common';
import Nonprofit from '..';
import Event from './';
import { EventDiscount } from './EventDiscount';
/**
* Describes a single ticket level for an event. Each Ticket is associated with a TicketLevel
*/
export interface TicketLevel extends HoudiniObject {
/** The cost of one ticket of the given amount */
amount: Amount;
/**
* Who can see and/or buy the ticket. 'everyone' is every visitor, 'admins' means event
* and nonprofit admins only
*/
available_to: 'everyone' | 'admins';
deleted: boolean;
description: string;
event: IdType | Event;
/**
* at some time, event discounts will be associated with a given ticket level.
* For now, this returns all of the discounts for the event though.
*/
event_discounts: IdType[] | EventDiscount[];
/**
* the max number of tickets to be sold on this TicketLevel, null means unlimited.
* If you edit this and decide to lower the limit below the number of tickets currently sold,
* we don't remove the tickets already there.
*/
limit?: number;
/**
* Nice readable name fo the ticket level
*/
name: string;
nonprofit: IdType | Nonprofit;
object: "ticket_level";
/** order to be displayed */
order: number;
}
export type TicketLevelCreated = HoudiniEvent<'ticket_level.created', TicketLevel>;
export type TicketLevelUpdated = HoudiniEvent<'ticket_level.updated', TicketLevel>;
export type TicketLevelDeleted = HoudiniEvent<'ticket_level.deleted', TicketLevel>;

View file

@ -0,0 +1,14 @@
// License: LGPL-3.0-or-later
import { IdType, HoudiniObject } from '../../common';
import Nonprofit from '../';
export default interface Event extends HoudiniObject {
end_date: Date;
name: string;
nonprofit: IdType | Nonprofit;
object: "event";
start_date: Date;
}
export * from './TicketLevel';
export * from './EventDiscount';

View file

@ -6,6 +6,19 @@
*/
export type IdType = number;
/**
* Describes a monetary value in the minimum unit for this current. Corresponds to Money class in
* Ruby and Typescript
*/
export type Amount = { currency: string, value_in_cents: string };
/**
* A more flexible version of Amount. In cases where we can assume what the currency is,
* we don't actually require you to provide it. Probably will be used most by APIs
*/
export type FlexibleAmount = Amount | string | number;
/**
* Every object controlled by the Houdini event publisher must meet this standard interface
* and will inherit from it.

View file

@ -33,4 +33,48 @@ describe TicketLevelsController, type: :controller do
end
end
end
describe 'verify deleted doesnt get passed through on update' do
include_context :shared_user_context
include_context :shared_donation_charge_context
let(:ticket_level_name) {"TICKET LEVEL"}
let(:order) { 3}
let(:free_amount) { 0}
let(:non_free_amount) {7500}
let(:ticket_limit) {4}
let(:description) {"Description"}
let(:ticket_level_2) {
event.ticket_levels.create(
name: ticket_level_name,
limit: nil,
admin_only: false,
order: order,
amount: non_free_amount,
description: description
)
}
it 'updates safely' do
input = {
nonprofit_id: nonprofit.id,
event_id: event.id,
id: ticket_level_2.id,
ticket_level: {
name: ticket_level_name,
limit: nil,
admin_only: false,
order: order,
amount: 0,
description: description,
deleted: true
}
}
sign_in user_as_np_admin
put :update, params: input, xhr: true
expect(response).to have_http_status :ok
ticket_level_2.reload
expect(ticket_level_2.deleted).to eq false
expect(ticket_level_2.amount).to eq 0
end
end
end

View file

@ -286,7 +286,8 @@ describe InsertTickets do
success_expectations
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_create, any_args)
expect(Houdini.event_publisher).to receive(:announce).with(:ticket_level_created, any_args).ordered
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)
expected = generate_expected_tickets(payment_id: result['payment'].id,

View file

@ -37,8 +37,8 @@ describe QueryTicketLevels do
end
describe '.verify_tickets_available' do
let(:ticket_level_1) { force_create(:ticket_level, limit: 3) }
let(:ticket_level_2) { force_create(:ticket_level, limit: 2) }
let(:ticket_level_1) { force_create(:ticket_level, limit: 3, event: event) }
let(:ticket_level_2) { force_create(:ticket_level, limit: 2, event: event) }
let(:tickets) do
[
force_create(:ticket, ticket_level: ticket_level_1, quantity: 1),

View file

@ -0,0 +1,348 @@
# 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 TicketLevel, type: :model do
include_context :shared_donation_charge_context
let(:ticket_level_name) {"TICKET LEVEL"}
let(:order) { 3}
let(:free_amount) { 0}
let(:non_free_amount) {7500}
let(:ticket_limit) {4}
let(:description) {"Description"}
let(:ticket_level_1) { event.ticket_levels.create(
name: ticket_level_name,
limit: ticket_limit,
admin_only: true,
order: order,
amount: free_amount,
description: description
) }
let(:ticket_level_2) {
event.ticket_levels.create(
name: ticket_level_name,
limit: nil,
admin_only: false,
order: order,
amount: non_free_amount,
description: description
)
}
describe 'create' do
describe 'ticket_level_1' do
it 'is without error' do
expect(ticket_level_1.errors).to be_empty
end
it 'announces create' do
expect(Houdini.event_publisher).to receive(:announce).with(:ticket_level_created, {
'id' => kind_of(String),
'object' => 'object_event',
'type' => 'ticket_level.created',
'data' => {
'object' => {
'amount' => {'value_in_cents' => 0, 'currency' => 'usd'},
'available_to' => 'admins',
'deleted' => false,
'description' => description,
'event' => {
'id' => event.id,
'name' => event.name,
'object' => 'event',
'nonprofit' => nonprofit.id
},
'id'=> kind_of(Numeric),
'limit' => 4,
'name' => ticket_level_name,
'nonprofit'=> {
'id' => nonprofit.id,
'name' => nonprofit.name,
'object' => 'nonprofit'
},
'event_discounts' => [],
'object' => 'ticket_level',
'order' => order
}
}
})
ticket_level_1
end
end
describe 'ticket_level_2' do
it 'is without error' do
expect(ticket_level_2.errors).to be_empty
end
it 'announces create' do
expect(Houdini.event_publisher).to receive(:announce).with(:ticket_level_created, {
'id' => kind_of(String),
'object' => 'object_event',
'type' => 'ticket_level.created',
'data' => {
'object' => {
'amount' => {'value_in_cents' => non_free_amount, 'currency' => 'usd'},
'available_to' => 'everyone',
'deleted' => false,
'description' => description,
'event' => {
'id' => event.id,
'name' => event.name,
'object' => 'event',
'nonprofit' => nonprofit.id
},
'id'=> kind_of(Numeric),
'limit' => nil,
'name' => ticket_level_name,
'nonprofit'=> {
'id' => nonprofit.id,
'name' => nonprofit.name,
'object' => 'nonprofit'
},
'event_discounts' => [],
'object' => 'ticket_level',
'order' => order
}
}
})
ticket_level_2
end
end
end
describe 'update' do
describe 'ticket_level_1' do
it 'is without error' do
ticket_level_1.amount = 5000
ticket_level_1.save
expect(ticket_level_1.errors).to be_empty
end
it 'announces updated' do
expect(Houdini.event_publisher).to receive(:announce).with(:ticket_level_created, anything).ordered
expect(Houdini.event_publisher).to receive(:announce).with(:ticket_level_updated, {
'id' => kind_of(String),
'object' => 'object_event',
'type' => 'ticket_level.updated',
'data' => {
'object' => {
'amount' => {'value_in_cents' => 5000, 'currency' => 'usd'},
'available_to' => 'admins',
'deleted' => false,
'description' => description,
'event' => {
'id' => event.id,
'name' => event.name,
'object' => 'event',
'nonprofit' => nonprofit.id
},
'id'=> kind_of(Numeric),
'limit' => 4,
'name' => ticket_level_name,
'nonprofit'=> {
'id' => nonprofit.id,
'name' => nonprofit.name,
'object' => 'nonprofit'
},
'event_discounts' => [],
'object' => 'ticket_level',
'order' => order
}
}
})
ticket_level_1
ticket_level_1.amount = 5000
ticket_level_1.save
end
end
describe 'ticket_level_2' do
it 'is without error' do
ticket_level_2.amount = 0
ticket_level_2.save
expect(ticket_level_2.errors).to be_empty
end
it 'announces updated' do
expect(Houdini.event_publisher).to receive(:announce).with(:ticket_level_created, anything).ordered
expect(Houdini.event_publisher).to receive(:announce).with(:ticket_level_updated, {
'id' => kind_of(String),
'object' => 'object_event',
'type' => 'ticket_level.updated',
'data' => {
'object' => {
'amount' => {'value_in_cents' => 0, 'currency' => 'usd'},
'available_to' => 'everyone',
'deleted' => false,
'description' => description,
'event' => {
'id' => event.id,
'name' => event.name,
'object' => 'event',
'nonprofit' => nonprofit.id
},
'id'=> kind_of(Numeric),
'limit' => nil,
'name' => ticket_level_name,
'nonprofit'=> {
'id' => nonprofit.id,
'name' => nonprofit.name,
'object' => 'nonprofit'
},
'event_discounts' => [],
'object' => 'ticket_level',
'order' => order
}
}
})
ticket_level_2
ticket_level_2.amount = 0
ticket_level_2.save
end
end
end
describe 'deleted' do
describe 'ticket_level_1' do
it 'is without error' do
ticket_level_1.discard!
expect(ticket_level_1.deleted).to eq true
end
it 'announces deleted' do
expect(Houdini.event_publisher).to receive(:announce).with(:ticket_level_created, anything).ordered
expect(Houdini.event_publisher).to receive(:announce).with(:ticket_level_deleted, {
'id' => kind_of(String),
'object' => 'object_event',
'type' => 'ticket_level.deleted',
'data' => {
'object' => {
'amount' => {'value_in_cents' => 0, 'currency' => 'usd'},
'available_to' => 'admins',
'deleted' => true,
'description' => description,
'event' => {
'id' => event.id,
'name' => event.name,
'object' => 'event',
'nonprofit' => nonprofit.id
},
'id'=> kind_of(Numeric),
'limit' => 4,
'name' => ticket_level_name,
'nonprofit'=> {
'id' => nonprofit.id,
'name' => nonprofit.name,
'object' => 'nonprofit'
},
'event_discounts' => [],
'object' => 'ticket_level',
'order' => order
}
}
}).ordered
ticket_level_1.discard!
end
end
describe 'ticket_level_2' do
it 'is without error' do
expect(ticket_level_2.errors).to be_empty
end
it 'announces deleted' do
expect(Houdini.event_publisher).to receive(:announce).with(:ticket_level_created, anything).ordered
expect(Houdini.event_publisher).to receive(:announce).with(:ticket_level_deleted, {
'id' => kind_of(String),
'object' => 'object_event',
'type' => 'ticket_level.deleted',
'data' => {
'object' => {
'amount' => {'value_in_cents' => non_free_amount, 'currency' => 'usd'},
'available_to' => 'everyone',
'deleted' => true,
'description' => description,
'event' => {
'id' => event.id,
'name' => event.name,
'object' => 'event',
'nonprofit' => nonprofit.id
},
'id'=> kind_of(Numeric),
'limit' => nil,
'name' => ticket_level_name,
'nonprofit'=> {
'id' => nonprofit.id,
'name' => nonprofit.name,
'object' => 'nonprofit'
},
'event_discounts' => [],
'object' => 'ticket_level',
'order' => order
}
}
})
ticket_level_2.discard!
end
end
end
# it 'creates' do
# expect(tag_master.errors).to be_empty
# end
# it 'announces create' do
# expect(Houdini.event_publisher).to receive(:announce).with(:tag_master_created, {
# 'id' => kind_of(String),
# 'object' => 'event',
# 'type' => 'tag_master.created',
# 'data' => {
# 'object' => {
# 'id'=> kind_of(Numeric),
# 'deleted' => false,
# 'name' => name,
# 'nonprofit'=> nonprofit.id,
# 'object' => 'tag_master'
# }
# }
# })
# tag_master
# end
# it 'announces deleted' do
# expect(Houdini.event_publisher).to receive(:announce).with(:tag_master_created, anything).ordered
# expect(Houdini.event_publisher).to receive(:announce).with(:tag_master_deleted, {
# 'id' => kind_of(String),
# 'object' => 'event',
# 'type' => 'tag_master.deleted',
# 'data' => {
# 'object' => {
# 'id'=> kind_of(Numeric),
# 'deleted' => true,
# 'name' => name,
# 'nonprofit'=> nonprofit.id,
# 'object' => 'tag_master'
# }
# }
# }).ordered
# tag_master.discard!
# end
end