427 lines
15 KiB
Ruby
427 lines
15 KiB
Ruby
module JamRuby
|
|
|
|
# a sale is created every time someone tries to buy something
|
|
class Sale < ActiveRecord::Base
|
|
|
|
JAMTRACK_SALE = 'jamtrack'
|
|
|
|
belongs_to :user, class_name: 'JamRuby::User'
|
|
has_many :sale_line_items, class_name: 'JamRuby::SaleLineItem'
|
|
|
|
has_many :recurly_transactions, class_name: 'JamRuby::RecurlyTransactionWebHook', inverse_of: :sale, foreign_key: 'invoice_id', primary_key: 'recurly_invoice_id'
|
|
|
|
validates :order_total, numericality: {only_integer: false}
|
|
validates :user, presence: true
|
|
|
|
@@log = Logging.logger[Sale]
|
|
|
|
def self.index(user, params = {})
|
|
|
|
limit = params[:per_page]
|
|
limit ||= 20
|
|
limit = limit.to_i
|
|
|
|
query = Sale.limit(limit)
|
|
.includes([:recurly_transactions, :sale_line_items])
|
|
.where('sales.user_id' => user.id)
|
|
.order('sales.created_at DESC')
|
|
|
|
current_page = params[:page].nil? ? 1 : params[:page].to_i
|
|
next_page = current_page + 1
|
|
|
|
# will_paginate gem
|
|
query = query.paginate(:page => current_page, :per_page => limit)
|
|
|
|
if query.length == 0 # no more results
|
|
{ query: query, next_page: nil}
|
|
elsif query.length < limit # no more results
|
|
{ query: query, next_page: nil}
|
|
else
|
|
{ query: query, next_page: next_page }
|
|
end
|
|
|
|
end
|
|
|
|
def state
|
|
original_total = self.recurly_total_in_cents
|
|
|
|
is_voided = false
|
|
refund_total = 0
|
|
|
|
recurly_transactions.each do |transaction|
|
|
if transaction.is_voided?
|
|
is_voided = true
|
|
else
|
|
|
|
end
|
|
|
|
if transaction.is_refund?
|
|
refund_total = refund_total + transaction.amount_in_cents
|
|
end
|
|
end
|
|
|
|
# if refund_total is > 0, then you have a refund.
|
|
# if voided is true, then in theory the whole thing has been refunded
|
|
{
|
|
voided: is_voided,
|
|
original_total: original_total,
|
|
refund_total: refund_total
|
|
}
|
|
end
|
|
|
|
|
|
def self.ios_purchase(current_user, jam_track, receipt)
|
|
jam_track_right = JamRuby::JamTrackRight.find_or_create_by_user_id_and_jam_track_id(current_user.id, jam_track.id) do |jam_track_right|
|
|
jam_track_right.redeemed = false
|
|
end
|
|
end
|
|
|
|
# place_order will create one or more sales based on the contents of shopping_carts for the current user
|
|
# individual subscriptions will end up create their own sale (you can't have N subscriptions in one sale--recurly limitation)
|
|
# jamtracks however can be piled onto the same sale as adjustments (VRFS-3028)
|
|
# so this method may create 1 or more sales, , where 2 or more sales can occur if there are more than one subscriptions or subscription + jamtrack
|
|
def self.place_order(current_user, shopping_carts)
|
|
|
|
sales = []
|
|
|
|
|
|
if Sale.is_mixed(shopping_carts)
|
|
# the controller checks this too; this is just an extra-level of sanity checking
|
|
return sales
|
|
end
|
|
|
|
jam_track_sale = order_jam_tracks(current_user, shopping_carts)
|
|
sales << jam_track_sale if jam_track_sale
|
|
|
|
# TODO: process shopping_carts_subscriptions
|
|
|
|
sales
|
|
end
|
|
|
|
|
|
def self.is_only_freebie(shopping_carts)
|
|
free = true
|
|
shopping_carts.each do |cart|
|
|
free = cart.product_info[:free]
|
|
|
|
if !free
|
|
break
|
|
end
|
|
end
|
|
free
|
|
end
|
|
|
|
# we don't allow mixed shopping carts :/
|
|
def self.is_mixed(shopping_carts)
|
|
free = false
|
|
non_free = false
|
|
shopping_carts.each do |cart|
|
|
if cart.product_info[:free]
|
|
free = true
|
|
else
|
|
non_free = true
|
|
end
|
|
end
|
|
free && non_free
|
|
end
|
|
|
|
# this method will either return a valid sale, or throw a RecurlyClientError or ActiveRecord validation error (save! failed)
|
|
# it may return an nil sale if the JamTrack(s) specified by the shopping carts are already owned
|
|
def self.order_jam_tracks(current_user, shopping_carts)
|
|
|
|
shopping_carts_jam_tracks = []
|
|
shopping_carts_subscriptions = []
|
|
shopping_carts_gift_cards = []
|
|
|
|
shopping_carts.each do |shopping_cart|
|
|
if shopping_cart.is_jam_track?
|
|
shopping_carts_jam_tracks << shopping_cart
|
|
elsif shopping_cart.is_gift_card?
|
|
shopping_carts_gift_cards << shopping_cart
|
|
else
|
|
# XXX: this may have to be revisited when we actually have something other than JamTracks for puchase
|
|
raise "unknown shopping cart type #{shopping_cart.cart_type}"
|
|
shopping_carts_subscriptions << shopping_cart
|
|
end
|
|
end
|
|
|
|
client = RecurlyClient.new
|
|
|
|
sale = nil
|
|
Sale.transaction do
|
|
sale = create_jam_track_sale(current_user)
|
|
|
|
if sale.valid?
|
|
if is_only_freebie(shopping_carts)
|
|
sale.process_shopping_carts(current_user, shopping_carts, nil)
|
|
|
|
sale.recurly_subtotal_in_cents = 0
|
|
sale.recurly_tax_in_cents = 0
|
|
sale.recurly_total_in_cents = 0
|
|
sale.recurly_currency = 'USD'
|
|
|
|
if sale.sale_line_items.count == 0
|
|
@@log.info("no sale line items associated with sale")
|
|
# we must have ditched some of the sale items. let's just abort this sale
|
|
sale.destroy
|
|
sale = nil
|
|
return sale
|
|
end
|
|
|
|
sale.sale_line_items.each do |sale_line_item|
|
|
sale_line_item = sale.sale_line_items[0]
|
|
sale_line_item.recurly_tax_in_cents = 0
|
|
sale_line_item.recurly_total_in_cents = 0
|
|
sale_line_item.recurly_currency = 'USD'
|
|
sale_line_item.recurly_discount_in_cents = 0
|
|
end
|
|
sale.save
|
|
|
|
else
|
|
|
|
account = client.get_account(current_user)
|
|
if account.present?
|
|
|
|
purge_pending_adjustments(account)
|
|
|
|
created_adjustments = sale.process_shopping_carts(current_user, shopping_carts, account)
|
|
|
|
# now invoice the sale ... almost done
|
|
|
|
begin
|
|
invoice = account.invoice!
|
|
sale.recurly_invoice_id = invoice.uuid
|
|
sale.recurly_invoice_number = invoice.invoice_number
|
|
|
|
# now slap in all the real tax/purchase totals
|
|
sale.recurly_subtotal_in_cents = invoice.subtotal_in_cents
|
|
sale.recurly_tax_in_cents = invoice.tax_in_cents
|
|
sale.recurly_total_in_cents = invoice.total_in_cents
|
|
sale.recurly_currency = invoice.currency
|
|
|
|
# and resolve against sale_line_items
|
|
sale.sale_line_items.each do |sale_line_item|
|
|
found_line_item = false
|
|
invoice.line_items.each do |line_item|
|
|
if line_item.uuid == sale_line_item.recurly_adjustment_uuid
|
|
sale_line_item.recurly_tax_in_cents = line_item.tax_in_cents
|
|
sale_line_item.recurly_total_in_cents =line_item.total_in_cents
|
|
sale_line_item.recurly_currency = line_item.currency
|
|
sale_line_item.recurly_discount_in_cents = line_item.discount_in_cents
|
|
found_line_item = true
|
|
break
|
|
end
|
|
|
|
end
|
|
|
|
if !found_line_item
|
|
@@log.error("can't find line item #{sale_line_item.recurly_adjustment_uuid}")
|
|
puts "CANT FIND LINE ITEM"
|
|
end
|
|
end
|
|
|
|
unless sale.save
|
|
raise RecurlyClientError, "Invalid sale (at end)."
|
|
end
|
|
rescue Recurly::Resource::Invalid => e
|
|
# this exception is thrown by invoice! if the invoice is invalid
|
|
sale.rollback_adjustments(current_user, created_adjustments)
|
|
sale = nil
|
|
raise ActiveRecord::Rollback # kill all db activity, but don't break outside logic
|
|
end
|
|
else
|
|
raise RecurlyClientError, "Could not find account to place order."
|
|
end
|
|
end
|
|
else
|
|
raise RecurlyClientError, "Invalid sale."
|
|
end
|
|
end
|
|
sale
|
|
end
|
|
|
|
def process_shopping_carts(current_user, shopping_carts, account)
|
|
|
|
created_adjustments = []
|
|
|
|
begin
|
|
shopping_carts.each do |shopping_cart|
|
|
process_shopping_cart(current_user, shopping_cart, account, created_adjustments)
|
|
end
|
|
rescue Recurly::Error, NoMethodError => x
|
|
# rollback any adjustments created if error
|
|
rollback_adjustments(user, created_adjustments)
|
|
raise RecurlyClientError, x.to_s
|
|
rescue Exception => e
|
|
# rollback any adjustments created if error
|
|
rollback_adjustments(user, created_adjustments)
|
|
raise e
|
|
end
|
|
|
|
created_adjustments
|
|
end
|
|
|
|
|
|
def process_shopping_cart(current_user, shopping_cart, account, created_adjustments)
|
|
recurly_adjustment_uuid = nil
|
|
recurly_adjustment_credit_uuid = nil
|
|
|
|
# we do this because of ShoppingCart.remove_jam_track_from_cart; if it occurs, which should be rare, we need fresh shopping cart info
|
|
shopping_cart.reload
|
|
|
|
# get the JamTrack in this shopping cart
|
|
cart_product = shopping_cart.cart_product
|
|
|
|
if shopping_cart.is_jam_track?
|
|
jam_track = cart_product
|
|
if jam_track.right_for_user(current_user)
|
|
# if the user already owns the JamTrack, we should just skip this cart item, and destroy it
|
|
# if this occurs, we have to reload every shopping_cart as we iterate. so, we do at the top of the loop
|
|
ShoppingCart.remove_jam_track_from_cart(current_user, shopping_cart)
|
|
return
|
|
end
|
|
end
|
|
|
|
|
|
|
|
if account
|
|
# ask the shopping cart to create the correct Recurly adjustment attributes for a JamTrack
|
|
adjustments = shopping_cart.create_adjustment_attributes(current_user)
|
|
|
|
adjustments.each do |adjustment|
|
|
|
|
# create the adjustment at Recurly (this may not look like it, but it is a REST API)
|
|
created_adjustment = account.adjustments.new(adjustment)
|
|
created_adjustment.save
|
|
|
|
# if the adjustment could not be made, bail
|
|
raise RecurlyClientError.new(created_adjustment.errors) if created_adjustment.errors.any?
|
|
|
|
# keep track of adjustments we created for this order, in case we have to roll them back
|
|
created_adjustments << created_adjustment
|
|
|
|
if ShoppingCart.is_product_purchase?(adjustment)
|
|
# this was a normal product adjustment, so track it as such
|
|
recurly_adjustment_uuid = created_adjustment.uuid
|
|
else
|
|
# this was a 'credit' adjustment, so track it as such
|
|
recurly_adjustment_credit_uuid = created_adjustment.uuid
|
|
end
|
|
end
|
|
end
|
|
|
|
|
|
# create one sale line item for every jam track
|
|
sale_line_item = SaleLineItem.create_from_shopping_cart(self, shopping_cart, nil, recurly_adjustment_uuid, recurly_adjustment_credit_uuid)
|
|
|
|
# if the sale line item is invalid, blow up the transaction
|
|
unless sale_line_item.valid?
|
|
@@log.error("sale item invalid! #{sale_line_item.errors.inspect}")
|
|
puts("sale item invalid! #{sale_line_item.errors.inspect}")
|
|
Stats.write('web.recurly.purchase.sale_invalid', {message: sale_line_item.errors.to_s, value: 1})
|
|
raise RecurlyClientError.new(sale_line_item.errors)
|
|
end
|
|
|
|
if shopping_cart.is_jam_track?
|
|
jam_track = cart_product
|
|
|
|
# create a JamTrackRight (this needs to be in a transaction too to make sure we don't make these by accident)
|
|
jam_track_right = JamRuby::JamTrackRight.find_or_create_by_user_id_and_jam_track_id(current_user.id, jam_track.id) do |jam_track_right|
|
|
jam_track_right.redeemed = shopping_cart.free?
|
|
end
|
|
|
|
# also if the purchase was a free one, then:
|
|
# first, mark the free has_redeemable_jamtrack field if that's still true
|
|
# and if still they have more free things, then redeem the giftable_jamtracks
|
|
if shopping_cart.free?
|
|
if user.has_redeemable_jamtrack
|
|
User.where(id: current_user.id).update_all(has_redeemable_jamtrack: false)
|
|
current_user.has_redeemable_jamtrack = false
|
|
else
|
|
User.where(id: current_user.id).update_all(gifted_jamtracks: current_user.gifted_jamtracks - 1)
|
|
current_user.gifted_jamtracks = current_user.gifted_jamtracks - 1
|
|
end
|
|
end
|
|
|
|
|
|
# this can't go in the block above, as it's here to fix bad subscription UUIDs in an update path
|
|
if jam_track_right.recurly_adjustment_uuid != recurly_adjustment_uuid
|
|
jam_track_right.recurly_adjustment_uuid = recurly_adjustment_uuid
|
|
jam_track_right.recurly_adjustment_credit_uuid = recurly_adjustment_credit_uuid
|
|
unless jam_track_right.save
|
|
raise RecurlyClientError.new(jam_track_right.errors)
|
|
end
|
|
end
|
|
|
|
# blow up the transaction if the JamTrackRight did not get created
|
|
raise RecurlyClientError.new(jam_track_right.errors) if jam_track_right.errors.any?
|
|
|
|
elsif shopping_cart.is_gift_card?
|
|
gift_card_type = cart_product
|
|
raise "gift card is null" if gift_card_type.nil?
|
|
raise if current_user.nil?
|
|
|
|
shopping_cart.quantity.times do |item|
|
|
gift_card_purchase = GiftCardPurchase.new(
|
|
{
|
|
user: current_user,
|
|
gift_card_type: gift_card_type
|
|
})
|
|
|
|
unless gift_card_purchase.save
|
|
raise RecurlyClientError.new(gift_card_purchase.errors)
|
|
end
|
|
end
|
|
|
|
else
|
|
raise 'unknown shopping cart type: ' + shopping_cart.cart_type
|
|
end
|
|
|
|
# delete the shopping cart; it's been dealt with
|
|
shopping_cart.destroy if shopping_cart
|
|
end
|
|
|
|
|
|
def rollback_adjustments(current_user, adjustments)
|
|
begin
|
|
adjustments.each { |adjustment| adjustment.destroy }
|
|
rescue Exception => e
|
|
AdminMailer.alerts({
|
|
subject: "ACTION REQUIRED: #{current_user.email} did not have all of his adjustments destroyed in rollback",
|
|
body: "go delete any adjustments on the account that don't belong. error: #{e}\n\nAdjustments: #{adjustments.inspect}"
|
|
}).deliver
|
|
|
|
end
|
|
end
|
|
|
|
def self.purge_pending_adjustments(account)
|
|
account.adjustments.pending.find_each do |adjustment|
|
|
# we only pre-emptively destroy pending adjustments if they appear to be created by the server
|
|
adjustment.destroy if ShoppingCart.is_server_pending_adjustment?(adjustment)
|
|
end
|
|
end
|
|
|
|
def is_jam_track_sale?
|
|
sale_type == JAMTRACK_SALE
|
|
end
|
|
|
|
def self.create_jam_track_sale(user)
|
|
sale = Sale.new
|
|
sale.user = user
|
|
sale.sale_type = JAMTRACK_SALE # gift cards and jam tracks are sold with this type of sale
|
|
sale.order_total = 0
|
|
sale.save
|
|
sale
|
|
end
|
|
|
|
# this checks just jamtrack sales appropriately
|
|
def self.check_integrity_of_jam_track_sales
|
|
Sale.select([:total, :voided]).find_by_sql(
|
|
"SELECT COUNT(sales.id) AS total,
|
|
COUNT(CASE WHEN transactions.transaction_type = '#{RecurlyTransactionWebHook::VOID}' THEN 1 ELSE null END) voided
|
|
FROM sales
|
|
LEFT OUTER JOIN recurly_transaction_web_hooks as transactions ON invoice_id = sales.recurly_invoice_id
|
|
WHERE sale_type = '#{JAMTRACK_SALE}'")
|
|
end
|
|
end
|
|
end |