242 lines
9.6 KiB
Ruby
242 lines
9.6 KiB
Ruby
# 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
|
|
|
|
module InsertTickets
|
|
# Will generate rows for payment, offsite_payment or charge, tickets, activities
|
|
# pass in:
|
|
# data: {
|
|
# tickets: [{quantity, ticket_level_id}],
|
|
# event_id,
|
|
# nonprofit_id,
|
|
# supporter_id,
|
|
# event_discount_id,
|
|
# card_id, (if a charge)
|
|
# offsite_payment: {kind, check_number},
|
|
# kind (offsite, charge, or free)
|
|
# }
|
|
def self.create(data)
|
|
data = data.with_indifferent_access
|
|
ParamValidation.new(data,
|
|
tickets: { required: true, is_array: true },
|
|
nonprofit_id: { required: true, is_reference: true },
|
|
supporter_id: { required: true, is_reference: true },
|
|
event_id: { required: true, is_reference: true },
|
|
event_discount_id: { is_reference: true },
|
|
kind: { included_in: %w[free charge offsite] },
|
|
token: { format: UUID::Regex },
|
|
offsite_payment: { is_hash: true })
|
|
|
|
data[:tickets].each do |t|
|
|
ParamValidation.new(t, quantity: { is_integer: true, required: true, min: 1 }, ticket_level_id: { is_reference: true, required: true })
|
|
end
|
|
|
|
ParamValidation.new(data[:offsite_payment], kind: { included_in: %w[cash check] }) if data[:offsite_payment] && !data[:offsite_payment][:kind].blank?
|
|
|
|
entities = RetrieveActiveRecordItems.retrieve_from_keys(data, Supporter => :supporter_id, Nonprofit => :nonprofit_id, Event => :event_id)
|
|
|
|
entities.merge!(RetrieveActiveRecordItems.retrieve_from_keys(data, { EventDiscount => :event_discount_id }, true))
|
|
|
|
tl_entities = get_ticket_level_entities(data)
|
|
|
|
validate_entities(entities, tl_entities)
|
|
|
|
# verify that enough tickets_available
|
|
QueryTicketLevels.verify_tickets_available(data[:tickets])
|
|
|
|
gross_amount = QueryTicketLevels.gross_amount_from_tickets(data[:tickets], data[:event_discount_id])
|
|
|
|
result = {}
|
|
|
|
trx = entities[:supporter_id].transactions.build(amount: gross_amount)
|
|
subtrx = nil
|
|
if gross_amount > 0
|
|
# Create offsite payment for tickets
|
|
if data[:kind] == 'offsite'
|
|
current_user = data[:current_user]
|
|
# offsite can only come from valid nonprofit users
|
|
unless current_user && QueryRoles.is_authorized_for_nonprofit?(current_user.id, entities[:nonprofit_id].id)
|
|
raise AuthenticationError
|
|
end
|
|
|
|
# create payment and offsite payment
|
|
result['payment'] = create_payment(entities, gross_amount)
|
|
result['offsite_payment'] = create_offsite_payment(entities, gross_amount, data, result['payment'])
|
|
subtrx = trx.build_subtransaction(
|
|
subtransactable: OfflineTransaction.new(amount: gross_amount),
|
|
subtransaction_payments:[
|
|
SubtransactionPayment.new(
|
|
paymentable: OfflineTransactionCharge.new(payment: Payment.find(result['payment']['id'])))
|
|
],
|
|
created: data['date']
|
|
);
|
|
|
|
# Create charge for tickets
|
|
elsif data['kind'] == 'charge' || !data['kind']
|
|
source_token = QuerySourceToken.get_and_increment_source_token(data[:token], nil)
|
|
QuerySourceToken.validate_source_token_type(source_token)
|
|
tokenizable = source_token.tokenizable
|
|
## does the card belong to the supporter?
|
|
if tokenizable.holder != entities[:supporter_id]
|
|
raise ParamValidation::ValidationError.new("Supporter #{entities[:supporter_id].id} does not own card #{tokenizable.id}", key: :token)
|
|
end
|
|
|
|
result = result.merge(InsertCharge.with_stripe(
|
|
kind: 'Ticket',
|
|
towards: entities[:event_id].name,
|
|
metadata: { kind: 'Ticket', event_id: entities[:event_id].id, nonprofit_id: entities[:nonprofit_id].id },
|
|
statement: "Tickets #{entities[:event_id].name}",
|
|
amount: gross_amount,
|
|
nonprofit_id: entities[:nonprofit_id].id,
|
|
supporter_id: entities[:supporter_id].id,
|
|
card_id: tokenizable.id
|
|
))
|
|
if result['charge']['status'] == 'failed'
|
|
raise ChargeError, result['charge']['failure_message']
|
|
else
|
|
subtrx = trx.build_subtransaction(
|
|
subtransactable: StripeTransaction.new(amount: gross_amount),
|
|
subtransaction_payments:[
|
|
SubtransactionPayment.new(
|
|
paymentable: StripeCharge.new(payment: Payment.find(result['payment']['id'])))
|
|
],
|
|
created: Time.current
|
|
);
|
|
end
|
|
else
|
|
raise ParamValidation::ValidationError.new("Ticket costs money but you didn't pay.", key: :kind)
|
|
end
|
|
end
|
|
|
|
ticket_purchase = trx.ticket_purchases.build(event: entities[:event_id])
|
|
# Generate the bid ids
|
|
data['tickets'] = generate_bid_ids(entities[:event_id].id, tl_entities)
|
|
|
|
result['tickets'] = generated_ticket_entities(data['tickets'], result, entities)
|
|
result['tickets'].each do |legacy_ticket|
|
|
|
|
legacy_ticket.quantity.times do
|
|
ticket_purchase.ticket_to_legacy_tickets.build(ticket: legacy_ticket)
|
|
end
|
|
end
|
|
|
|
# Create the activity rows for the tickets
|
|
InsertActivities.for_tickets(result['tickets'].map(&:id))
|
|
|
|
ticket_ids = result['tickets'].map(&:id)
|
|
charge_id = result['charge'] ? result['charge'].id : nil
|
|
trx.save!
|
|
ticket_purchase.save!
|
|
if (subtrx)
|
|
subtrx.save!
|
|
subtrx.subtransaction_payments.each{|stp| stp.publish_created}
|
|
subtrx.publish_created
|
|
end
|
|
ticket_purchase.ticket_to_legacy_tickets.each{|i| i.publish_created}
|
|
ticket_purchase.publish_created
|
|
trx.publish_created
|
|
|
|
result
|
|
end
|
|
|
|
# Generate a set of 'bid ids' (ids for each ticket scoped within the event)
|
|
def self.generate_bid_ids(event_id, tickets)
|
|
# Generate the bid ids
|
|
last_bid_id = Psql.execute(
|
|
Qexpr.new.select('COUNT(*)').from(:tickets)
|
|
.where('event_id=$id', id: event_id)
|
|
).first['count'].to_i
|
|
tickets.zip(last_bid_id + 1..last_bid_id + tickets.count).map { |h, id| h.merge('bid_id' => id) }
|
|
end
|
|
|
|
# not really needed but used for breaking into the unit test and getting the IDs
|
|
def self.generated_ticket_entities(ticket_data, result, entities)
|
|
ticket_data.map do |ticket_request|
|
|
t = Ticket.new
|
|
t.quantity = ticket_request['quantity']
|
|
t.ticket_level = ticket_request['ticket_level_id']
|
|
t.event = entities[:event_id]
|
|
t.supporter = entities[:supporter_id]
|
|
t.payment = result['payment']
|
|
t.charge = result['charge']
|
|
t.bid_id = ticket_request['bid_id']
|
|
t.event_discount = entities[:event_discount_id]
|
|
t.save!
|
|
t
|
|
end.to_a
|
|
end
|
|
|
|
def self.validate_entities(entities, tl_entities)
|
|
## is supporter deleted? If supporter is deleted, we error!
|
|
if entities[:supporter_id].deleted
|
|
raise ParamValidation::ValidationError.new("Supporter #{entities[:supporter_id].id} is deleted", key: :supporter_id)
|
|
end
|
|
|
|
if entities[:event_id].deleted
|
|
raise ParamValidation::ValidationError.new("Event #{entities[:event_id].id} is deleted", key: :event_id)
|
|
end
|
|
|
|
# verify that enough tickets_available
|
|
tl_entities.each do |i|
|
|
if i[:ticket_level_id].deleted
|
|
raise ParamValidation::ValidationError.new("Ticket level #{i[:ticket_level_id].id} is deleted", key: :tickets)
|
|
end
|
|
|
|
if i[:ticket_level_id].event != entities[:event_id]
|
|
raise ParamValidation::ValidationError.new("Ticket level #{i[:ticket_level_id].id} does not belong to event #{entities[:event_id]}", key: :tickets)
|
|
end
|
|
end
|
|
|
|
# Does the supporter belong to the nonprofit?
|
|
if entities[:supporter_id].nonprofit != entities[:nonprofit_id]
|
|
raise ParamValidation::ValidationError.new("Supporter #{entities[:supporter_id].id} does not belong to nonprofit #{entities[:nonprofit_id].id}", key: :supporter_id)
|
|
end
|
|
|
|
## does event belong to nonprofit
|
|
if entities[:event_id].nonprofit != entities[:nonprofit_id]
|
|
raise ParamValidation::ValidationError.new("Event #{entities[:event_id].id} does not belong to nonprofit #{entities[:nonprofit_id]}", key: :event_id)
|
|
end
|
|
|
|
if entities[:event_discount_id] && entities[:event_discount_id].event != entities[:event_id]
|
|
raise ParamValidation::ValidationError.new("Event discount #{entities[:event_discount_id].id} does not belong to event #{entities[:event_id].id}", key: :event_discount_id)
|
|
end
|
|
end
|
|
|
|
def self.get_ticket_level_entities(data)
|
|
data[:tickets].map do |i|
|
|
{
|
|
quantity: i[:quantity],
|
|
ticket_level_id: RetrieveActiveRecordItems.retrieve_from_keys(i, TicketLevel => :ticket_level_id)[:ticket_level_id]
|
|
}
|
|
end.to_a
|
|
end
|
|
|
|
def self.create_payment(entities, gross_amount)
|
|
p = Payment.new
|
|
p.gross_amount = gross_amount
|
|
p.nonprofit = entities[:nonprofit_id]
|
|
p.supporter = entities[:supporter_id]
|
|
p.refund_total = 0
|
|
p.date = Time.current
|
|
p.towards = entities[:event_id].name
|
|
p.fee_total = 0
|
|
p.net_amount = gross_amount
|
|
p.kind = 'OffsitePayment'
|
|
p.save!
|
|
p
|
|
end
|
|
|
|
def self.create_offsite_payment(entities, gross_amount, data, payment)
|
|
p = OffsitePayment.new
|
|
p.gross_amount = gross_amount
|
|
p.nonprofit = entities[:nonprofit_id]
|
|
p.supporter = entities[:supporter_id]
|
|
p.date = Time.current
|
|
p.payment = payment
|
|
p.kind = data['offsite_payment']['kind']
|
|
p.check_number = data['offsite_payment']['check_number']
|
|
p.save!
|
|
p
|
|
end
|
|
end
|