houdini/lib/insert/insert_donation.rb
2020-06-15 10:26:57 -05:00

260 lines
10 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 InsertDonation
# Make a one-time donation (call InsertRecurringDonation.with_stripe to create a recurring donation)
# In data, pass in:
# amount, card_id, nonprofit_id, supporter_id
# designation, dedication
# recurring_donation if is recurring
def self.with_stripe(data, current_user = nil)
data = data.with_indifferent_access
ParamValidation.new(data, common_param_validations
.merge(token: { required: true, format: UUID::Regex }))
source_token = QuerySourceToken.get_and_increment_source_token(data[:token], current_user)
tokenizable = source_token.tokenizable
QuerySourceToken.validate_source_token_type(source_token)
entities = RetrieveActiveRecordItems.retrieve_from_keys(data, Supporter => :supporter_id, Nonprofit => :nonprofit_id)
entities = entities.merge(RetrieveActiveRecordItems.retrieve_from_keys(data, { Campaign => :campaign_id, Event => :event_id, Profile => :profile_id }, true))
validate_entities(entities)
## 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
data['card_id'] = tokenizable.id
result = {}
data[:date] = Time.now
data = data.except(:old_donation).except('old_donation')
result = result.merge(insert_charge(data))
if result['charge']['status'] == 'failed'
raise ChargeError, result['charge']['failure_message']
end
# Create the donation record
result['donation'] = insert_donation(data, entities)
update_donation_keys(result)
result['activity'] = InsertActivities.for_one_time_donations([result['payment'].id])
Houdini.event_publisher.announce(:donation_create, result['donation'], result['donation'].supporter.locale)
result
end
# Update the charge to have the payment and donation id
# Update the payment to have the donation id
def self.update_donation_keys(result)
result['charge'].donation = result['donation']
result['charge'].save!
result['payment'].donation = result['donation']
result['payment'].save!
end
# Insert a donation made from an offsite payment
# Creates an offsite payment, payment, and donation
# pass in amount, nonprofit_id, supporter_id, check_number
# also pass in offsite_payment sub-hash (can be empty)
def self.offsite(data)
ParamValidation.new(data, common_param_validations.merge(offsite_payment: { is_hash: true }))
entities = RetrieveActiveRecordItems.retrieve_from_keys(data, Supporter => :supporter_id, Nonprofit => :nonprofit_id)
entities = entities.merge(RetrieveActiveRecordItems.retrieve_from_keys(data, { Campaign => :campaign_id, Event => :event_id, Profile => :profile_id }, true))
validate_entities(entities)
data = date_from_data(data)
result = { 'donation' => insert_donation(data.except('offsite_payment'), entities) }
result['payment'] = insert_payment('OffsitePayment', 0, result['donation']['id'], data)
result['offsite_payment'] = Psql.execute(
Qexpr.new.insert(:offsite_payments, [
(data['offsite_payment'] || {}).merge(
gross_amount: data['amount'],
nonprofit_id: data['nonprofit_id'],
supporter_id: data['supporter_id'],
donation_id: result['donation']['id'],
payment_id: result['payment']['id'],
date: data['date']
)
]).returning('*')
).first
result['activity'] = InsertActivities.for_offsite_donations([result['payment']['id']])
WeMoveExecuteForDonationsJob.perform_later(result['donation'])
{ status: 200, json: result }
end
def self.with_sepa(data)
data = data.with_indifferent_access
ParamValidation.new(data, common_param_validations
.merge(direct_debit_detail_id: { required: true, is_reference: true }))
entities = RetrieveActiveRecordItems.retrieve_from_keys(data, Supporter => :supporter_id, Nonprofit => :nonprofit_id)
entities = entities.merge(RetrieveActiveRecordItems.retrieve_from_keys(data, { Campaign => :campaign_id, Event => :event_id, Profile => :profile_id }, true))
result = {}
data[:date] = Time.now
result = result.merge(insert_charge(data))
result['donation'] = insert_donation(data, entities)
update_donation_keys(result)
Houdini.event_publisher.announce(:donation_create, result['donation'], locale_for_supporter(result['donation'].supporter.id))
# do this for making test consistent
result['activity'] = {}
result
end
private
def self.get_nonprofit_data(nonprofit_id)
Psql.execute(
Qexpr.new.select(:statement, :name).from(:nonprofits)
.where('id=$id', id: nonprofit_id)
).first
end
def self.insert_charge(data)
payment_provider = payment_provider(data)
nonprofit_data = get_nonprofit_data(data['nonprofit_id'])
kind = data['recurring_donation'] ? 'RecurringDonation' : 'Donation'
if payment_provider == :credit_card
return InsertCharge.with_stripe(
donation_id: data['donation_id'],
kind: kind,
towards: data['designation'],
metadata: { kind: kind, nonprofit_id: data['nonprofit_id'] },
statement: "Donation #{nonprofit_data['statement'] || nonprofit_data['name']}",
amount: data['amount'],
nonprofit_id: data['nonprofit_id'],
supporter_id: data['supporter_id'],
card_id: data['card_id'],
old_donation: data['old_donation'] ? true : false
)
elsif payment_provider == :sepa
return InsertCharge.with_sepa(
donation_id: data['donation_id'],
kind: kind,
towards: data['designation'],
metadata: { kind: kind, nonprofit_id: data['nonprofit_id'] },
statement: "Donation #{nonprofit_data['statement'] || nonprofit_data['name']}",
amount: data['amount'],
nonprofit_id: data['nonprofit_id'],
supporter_id: data['supporter_id'],
direct_debit_detail_id: data['direct_debit_detail_id']
)
end
end
# Insert a payment row for a donation
def self.insert_payment(kind, fee_total, donation_id, data)
Psql.execute(
Qexpr.new.insert(:payments, [{
donation_id: donation_id,
gross_amount: data['amount'],
nonprofit_id: data['nonprofit_id'],
supporter_id: data['supporter_id'],
refund_total: 0,
date: data['date'],
towards: data['designation'],
kind: kind,
fee_total: fee_total,
net_amount: data['amount'] - fee_total
}]).returning('*')
).first
end
# Insert a donation row
def self.insert_donation(data, entities)
d = Donation.new
d.date = data['date']
d.anonymous = data['anonymous']
d.designation = data['designation']
d.dedication = data['dedication']
d.comment = data['comment']
d.amount = data['amount']
d.card = Card.find(data['card_id']) if data['card_id']
d.direct_debit_detail = DirectDebitDetail.find(data['direct_debit_detail_id']) if data['direct_debit_detail_id']
d.nonprofit = entities[:nonprofit_id]
d.supporter = entities[:supporter_id]
d.profile = entities[:profile_id] || nil
d.campaign = entities[:campaign_id] || nil
d.event = entities[:event_id] || nil
d.payment_provider = payment_provider(data).to_s
d.save!
d
end
# Return either the parsed DateTime from a date in data, or right now
def self.date_from_data(data)
data.merge('date' => data['date'].blank? ? Time.current : Chronic.parse(data['date']))
end
def self.locale_for_supporter(supporter_id)
Psql.execute(
Qexpr.new.select(:locale).from(:supporters)
.where('id=$id', id: supporter_id)
).first['locale']
end
def self.payment_provider(data)
if data[:card_id] || data['card_id']
:credit_card
elsif data[:direct_debit_detail_id] || data['direct_debit_detail_id']
:sepa
end
end
def self.parse_date(date)
date.blank? ? Time.current : Chronic.parse(date)
end
def self.common_param_validations
{
amount: { required: true, is_integer: true },
nonprofit_id: { required: true, is_reference: true },
supporter_id: { required: true, is_reference: true },
designation: { is_a: String },
dedication: { is_a: String },
campaign_id: { is_reference: true },
event_id: { is_reference: true }
}
end
def self.validate_entities(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
if entities[:campaign_id]&.deleted
raise ParamValidation::ValidationError.new("Campaign #{entities[:campaign_id].id} is deleted", key: :campaign_id)
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
### if we have campaign, does it belong to nonprofit
if entities[:campaign_id] && entities[:campaign_id].nonprofit != entities[:nonprofit_id]
raise ParamValidation::ValidationError.new("Campaign #{entities[:campaign_id].id} does not belong to nonprofit #{entities[:nonprofit_id]}", key: :campaign_id)
end
## if we have event, does it belong to nonprofit
if entities[:event_id] && 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
end
end