jam-cloud/ruby/lib/jam_ruby/models/sale.rb

777 lines
31 KiB
Ruby
Raw Normal View History

module JamRuby
# a sale is created every time someone tries to buy something
class Sale < ActiveRecord::Base
JAMTRACK_SALE = 'jamtrack'
LESSON_SALE = 'lesson'
2016-08-31 09:19:16 +00:00
POSA_SALE = 'posacard'
SOURCE_RECURLY = 'recurly'
SOURCE_IOS = 'ios'
2016-12-15 18:47:08 +00:00
SOURCE_PAYPAL = 'paypal'
2016-08-31 09:19:16 +00:00
belongs_to :retailer, class_name: 'JamRuby::Retailer'
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}
2016-08-31 09:19:16 +00:00
#validates :user
#validates :retailer
@@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
2016-01-28 17:31:57 +00:00
# The expectation is that this code would throw an exception (breaking the transaction that encompasses it),
# if it can't validate the receipt, or communicate with Apple at all, etc
#
# So, if this raises exceptions, you can handle them in the stubbed out begin/rescue in ApiJamTracksController#ios_order_placed
2016-02-09 12:46:18 +00:00
def self.validateIOSReceipt(receipt, price_data, user, sale)
2016-01-28 17:31:57 +00:00
# these are all 'in cents' (as painfully named to be very clear), and all expected to be integers
2016-02-07 14:54:49 +00:00
price = price_data['product_price'].to_f * 100.0
price_info = {
subtotal_in_cents: price,
total_in_cents: price,
tax_in_cents: nil,
currency: price_data['product_currency']
2016-02-07 14:54:49 +00:00
}
response = IosReceiptValidator.post('/verifyReceipt',
body: {'receipt-data' => receipt}.to_json,
headers: {'Content-Type' => 'application/json'})
2016-02-07 14:54:49 +00:00
json_resp = JSON.parse(response.body)
# https://developer.apple.com/library/ios/releasenotes/General/ValidateAppStoreReceipt/Chapters/ReceiptFields.html#//apple_ref/doc/uid/TP40010573-CH106-SW1
2016-02-09 11:00:39 +00:00
if 0 != json_resp['status']
2016-02-07 14:54:49 +00:00
err_msgs = {
21000 => 'The App Store could not read the JSON object you provided.',
21002 => 'The data in the receipt-data property was malformed or missing.',
21003 => 'The receipt could not be authenticated.',
21005 => 'The receipt server is not currently available.',
21007 => 'This receipt is from the test environment, but it was sent to the production environment for verification. Send it to the test environment instead.',
21008 => 'This receipt is from the production environment, but it was sent to the test environment for verification. Send it to the production environment instead.'
2016-02-07 14:54:49 +00:00
}
2016-02-09 11:00:39 +00:00
raise err_msgs[json_resp['status']]
2016-02-09 12:46:18 +00:00
else
receiptJson = SaleReceiptIOS.new
receiptJson.user = user
receiptJson.sale = sale
receiptJson.data_blob = json_resp
receiptJson.save!
2016-02-07 14:54:49 +00:00
end
2016-01-28 17:31:57 +00:00
price_info
end
2016-02-07 14:54:49 +00:00
def self.ios_purchase(current_user, jam_track, receipt, price_data)
2016-01-28 17:31:57 +00:00
jam_track_right = nil
# everything needs to go into a transaction! If anything goes wrong, we need to raise an exception to break it
Sale.transaction do
using_free_credit = current_user.redeem_free_credit
sale = create_jam_track_sale(current_user, SOURCE_IOS)
2016-01-28 17:31:57 +00:00
if sale.valid?
if using_free_credit
SaleLineItem.create_from_jam_track(current_user, sale, jam_track, using_free_credit)
sale.recurly_subtotal_in_cents = 0
sale.recurly_tax_in_cents = 0
sale.recurly_total_in_cents = 0
sale.recurly_currency = 'USD'
sale.save!
else
2016-02-09 12:46:18 +00:00
price_info = validateIOSReceipt(receipt, price_data, current_user, sale)
2016-01-28 17:31:57 +00:00
SaleLineItem.create_from_jam_track(current_user, sale, jam_track, using_free_credit)
sale.recurly_subtotal_in_cents = price_info[:subtotal_in_cents]
sale.recurly_tax_in_cents = price_info[:tax_in_cents]
sale.recurly_total_in_cents = price_info[:total_in_cents]
sale.recurly_currency = price_info[:currency]
sale.save!
end
else
# should not get out of testing. This would be very rare (i.e., from a big regression). Sale is always valid at this point.
raise "invalid sale object"
end
# if we make it this far, all is well!
2016-08-03 01:46:15 +00:00
jam_track_right = JamRuby::JamTrackRight.find_or_create_by({user_id: current_user.id, jam_track_id: jam_track.id}) do |jam_track_right|
2016-01-28 17:31:57 +00:00
jam_track_right.redeemed = using_free_credit
jam_track_right.version = jam_track.version
end
2015-11-29 01:38:39 +00:00
end
2016-01-28 17:31:57 +00:00
jam_track_right
2015-11-29 01:38:39 +00:00
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
2016-12-15 18:47:08 +00:00
def self.place_order(current_user, shopping_carts, paypal = false)
sales = []
2015-11-29 19:58:10 +00:00
2016-08-03 01:46:15 +00:00
#if Sale.is_mixed(shopping_carts)
# # the controller checks this too; this is just an extra-level of sanity checking
# return sales
#end
2016-12-15 18:47:08 +00:00
jam_track_sale = order_jam_tracks(current_user, shopping_carts, paypal)
sales << jam_track_sale if jam_track_sale
# TODO: process shopping_carts_subscriptions
sales
end
2015-11-29 19:58:10 +00:00
def self.is_only_freebie(shopping_carts)
free = true
2015-11-29 19:58:10 +00:00
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
2016-05-05 02:20:38 +00:00
def self.purchase_test_drive(current_user, lesson_package_type, booking = nil)
self.purchase_lesson(nil, current_user, booking, lesson_package_type)
end
def self.post_sale_test_failure
return true
end
2016-08-31 09:19:16 +00:00
def self.posa_activate(posa_card, retailer)
sale = nil
Sale.transaction(:requires_new => true) do
posa_card.activate(retailer)
if !posa_card.errors.any?
sale = create_posa_sale(retailer, posa_card)
SaleLineItem.create_from_posa_card(sale, retailer, posa_card)
sale.save
end
end
{sale: sale}
end
# this is easy to make generic, but right now, it just purchases lessons
def self.purchase_lesson(charge, current_user, lesson_booking, lesson_package_type, lesson_session = nil, lesson_package_purchase = nil, force = false)
stripe_charge = nil
sale = nil
purchase = nil
# everything needs to go into a transaction! If anything goes wrong, we need to raise an exception to break it
Sale.transaction(:requires_new => true) do
2016-08-31 09:19:16 +00:00
sale = create_lesson_sale(current_user)
2016-08-31 09:19:16 +00:00
if sale.valid?
2016-05-16 16:39:20 +00:00
2016-08-31 09:19:16 +00:00
if lesson_booking
lesson_booking.current_lesson = lesson_session
lesson_booking.current_purchase = lesson_package_purchase
end
2016-08-31 09:19:16 +00:00
sale_line_item = SaleLineItem.create_from_lesson_package(current_user, sale, lesson_package_type, lesson_booking)
2016-08-31 09:19:16 +00:00
price_info = charge_stripe_for_lesson(charge, current_user, lesson_booking, lesson_package_type, sale_line_item, lesson_session, lesson_package_purchase, force)
2016-08-31 09:19:16 +00:00
post_sale_test_failure
2016-08-31 09:19:16 +00:00
if price_info[:purchase] && price_info[:purchase].errors.any?
purchase = price_info[:purchase]
2016-08-31 09:19:16 +00:00
raise ActiveRecord::Rollback
end
if !sale_line_item.valid?
raise "invalid sale_line_item object for user #{current_user.email} and lesson_booking #{lesson_booking.id}"
end
2016-08-31 09:19:16 +00:00
# sale.source = 'stripe'
sale.recurly_subtotal_in_cents = price_info[:subtotal_in_cents]
sale.recurly_tax_in_cents = price_info[:tax_in_cents]
sale.recurly_total_in_cents = price_info[:total_in_cents]
sale.recurly_currency = price_info[:currency]
sale.stripe_charge_id = price_info[:charge_id]
sale.save
stripe_charge = price_info[:charge]
purchase = price_info[:purchase]
else
# should not get out of testing. This would be very rare (i.e., from a big regression). Sale is always valid at this point.
puts "invalid sale object"
raise "invalid sale object"
end
end
2016-05-16 16:39:20 +00:00
{sale: sale, stripe_charge: stripe_charge, purchase: purchase}
end
def self.charge_stripe_for_lesson(charge, current_user, lesson_booking, lesson_package_type, sale_line_item, lesson_session = nil, lesson_package_purchase = nil, force = false)
if lesson_package_purchase
target = lesson_package_purchase
elsif lesson_session
target = lesson_session
else
target = lesson_package_type
end
current_user.sync_stripe_customer
purchase = lesson_package_purchase
purchase = LessonPackagePurchase.create(current_user, lesson_booking, lesson_package_type) if purchase.nil?
if purchase.errors.any?
2016-05-05 02:20:38 +00:00
puts "purchase errors #{purchase.errors.inspect}"
price_info = {}
price_info[:purchase] = purchase
return price_info
end
if lesson_session
lesson_session.lesson_package_purchase_id = purchase.id
lesson_session.save!
end
subtotal_in_cents = purchase.price_in_cents
tax_percent = 0
if current_user.stripe_zip_code
lookup = ZipCodes.identify(current_user.stripe_zip_code)
if lookup && lookup[:state_code] == 'TX'
tax_percent = 0.0825
end
end
tax_in_cents = (subtotal_in_cents * tax_percent).round
total_in_cents = subtotal_in_cents + tax_in_cents
if lesson_session # not set if test drive
lesson_id = lesson_session.id
teacher_id = lesson_session.teacher.id
teacher_name = lesson_session.teacher.name
end
charge_id = charge.id if charge # not set if test drive
begin
metadata = {
lesson_package: purchase.id,
lesson_session: lesson_id,
teacher_id: teacher_id,
teacher_name: teacher_name,
charge: charge_id,
user: current_user.id,
tax: tax_in_cents
}
rescue Exception => e
metadata = {metaerror: true}
end
stripe_charge = Stripe::Charge.create(
:amount => total_in_cents,
:currency => "usd",
:customer => current_user.stripe_customer_id,
:description => target.stripe_description(lesson_booking),
:metadata => metadata
)
if charge
charge.stripe_charge = stripe_charge
end
sale_line_item.lesson_package_purchase = purchase
sale_line_item.save
price_info = {}
price_info[:subtotal_in_cents] = subtotal_in_cents
price_info[:tax_in_cents] = tax_in_cents
price_info[:total_in_cents] = total_in_cents
price_info[:currency] = 'USD'
price_info[:charge_id] = stripe_charge.id
price_info[:charge] = stripe_charge
price_info[:purchase] = purchase
price_info
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
2016-12-15 18:47:08 +00:00
def self.order_jam_tracks(current_user, shopping_carts, is_paypal)
2015-11-29 19:58:10 +00:00
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
sale = nil
Sale.transaction do
2016-12-15 18:47:08 +00:00
sale = create_jam_track_sale(current_user, is_paypal ? SOURCE_PAYPAL : SOURCE_RECURLY)
if sale.valid?
2015-11-29 19:58:10 +00:00
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
2015-11-29 19:58:10 +00:00
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
2016-12-15 18:47:08 +00:00
if is_paypal
sale.process_shopping_carts(current_user, shopping_carts)
2016-12-15 18:47:08 +00:00
paypal_auth = current_user.paypal_auth
2016-12-15 18:47:08 +00:00
@api = PayPal::SDK::Merchant::API.new
@get_express_checkout_details = @api.build_get_express_checkout_details({:Token => paypal_auth.token})
@response = @api.get_express_checkout_details(@get_express_checkout_details)
2016-12-15 18:47:08 +00:00
@@log.info("User #{current_user.email}, GetExpressCheckout: #{@response.inspect}")
tax = false
if @response.Ack == 'Success'
payerInfo = @response.GetExpressCheckoutDetailsResponseDetails.PayerInfo
if payerInfo.Address && ( payerInfo.Address.Country == 'US' && payerInfo.Address.StateOrProvince == 'TX')
# we need to ask for taxes
tax = true
end
end
2016-12-15 18:47:08 +00:00
tax_rate = tax ? 0.0825 : 0
total = current_user.shopping_cart_total.round(2)
tax_total = (total * tax_rate).round(2)
total = total + tax_total
total = total.round(2)
@do_express_checkout_payment = @api.build_do_express_checkout_payment({
:DoExpressCheckoutPaymentRequestDetails => {
:PaymentDetails =>
[
{
:OrderTotal => {
:currencyID => "USD",
:value => total
},
:PaymentAction => "Sale"
}
],
:Token => paypal_auth.token,
:PayerID => paypal_auth.uid, }})
@pay_response = @api.do_express_checkout_payment(@do_express_checkout_payment)
@@log.info("User #{current_user.email}, DoExpressCheckoutPayment: #{@pay_response.inspect}")
# #<PayPal::SDK::Merchant::DataTypes::DoExpressCheckoutPaymentResponseType:0x007fe511dd9b88
# @Timestamp=Sun, 11 Dec 2016 02:09:31 +0000, @Ack="Success",
# @CorrelationID="b28faf6bd90d9", @Version="117.0", @Build="000000",
# @DoExpressCheckoutPaymentResponseDetails=#<PayPal::SDK::Merchant::DataTypes::DoExpressCheckoutPaymentResponseDetailsType:0x007fe511dd38c8
# @Token="EC-7A4606566T700564B",
# @PaymentInfo=[#<PayPal::SDK::Merchant::DataTypes::PaymentInfoType:0x007fe511dd3008 @TransactionID="63C410710F2619403", @ParentTransactionID=nil,
# @ReceiptID=nil, @TransactionType="express-checkout", @PaymentType="instant", @PaymentDate=Sun, 11 Dec 2016 02:09:31 +0000,
# @GrossAmount=#<PayPal::SDK::Merchant::DataTypes::BasicAmountType:0x007fe511dd0c90 @currencyID="USD", @value="1.99">,
# @FeeAmount=#<PayPal::SDK::Merchant::DataTypes::BasicAmountType:0x007fe511dd04e8 @currencyID="USD", @value="0.36">,
# @TaxAmount=#<PayPal::SDK::Merchant::DataTypes::BasicAmountType:0x007fe511dcbe98 @currencyID="USD", @value="0.00">,
# @ExchangeRate=nil, @PaymentStatus="Completed", @PendingReason="none", @ReasonCode="none", @ProtectionEligibility="Eligible",
# @ProtectionEligibilityType="ItemNotReceivedEligible,UnauthorizedPaymentEligible", @SellerDetails=#<PayPal::SDK::Merchant::DataTypes::SellerDetailsType:0x007fe511dcb358 @SecureMerchantAccountID="6MB486RSBRMJ2">>],
# @SuccessPageRedirectRequested="false", @CoupledPaymentInfo=[#<PayPal::SDK::Merchant::DataTypes::CoupledPaymentInfoType:0x007fe511dca7a0>]>>
if @pay_response.Ack == 'Success'
details = @pay_response.DoExpressCheckoutPaymentResponseDetails.PaymentInfo[0]
sale.recurly_invoice_id = details.TransactionID
sale.recurly_invoice_number = details.ReceiptID
# now slap in all the real tax/purchase totals
2016-12-15 18:47:08 +00:00
sale.recurly_subtotal_in_cents = ((details.GrossAmount.value.to_f - details.TaxAmount.value.to_f) * 100).to_i
sale.recurly_tax_in_cents = (details.TaxAmount.value.to_f * 100).to_i
sale.recurly_total_in_cents = (details.GrossAmount.value.to_f * 100).to_i
sale.recurly_currency = details.GrossAmount.currencyID
unless sale.save
puts "Invalid sale (at end)."
raise PayPalClientError, "Invalid sale (at end)."
end
else
@@log.error("User #{current_user.email}, DoExpressCheckoutPayment: #{@pay_response.inspect}")
raise PayPalClientError, @pay_response.Errors[0].LongMessage
end
else
client = RecurlyClient.new
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
2016-12-15 18:47:08 +00:00
if !found_line_item
@@log.error("can't find line item #{sale_line_item.recurly_adjustment_uuid}")
puts "CANT FIND LINE ITEM"
end
end
2016-12-15 18:47:08 +00:00
unless sale.save
puts "Invalid sale (at end)."
raise RecurlyClientError, "Invalid sale (at end)."
end
2016-12-15 18:47:08 +00:00
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
rescue => e
puts "UNKNOWN E #{e}"
end
2016-12-15 18:47:08 +00:00
else
raise RecurlyClientError, "Could not find account to place order."
end
end
end
else
raise RecurlyClientError, "Invalid sale."
end
end
sale
end
2016-12-15 18:47:08 +00:00
def process_shopping_carts(current_user, shopping_carts, account = nil)
created_adjustments = []
begin
2015-11-29 19:58:10 +00:00
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
2016-12-15 18:47:08 +00:00
def process_shopping_cart(current_user, shopping_cart, recurly_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
2015-11-29 19:58:10 +00:00
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
2015-11-29 19:58:10 +00:00
2016-12-15 18:47:08 +00:00
if recurly_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)
2016-12-15 18:47:08 +00:00
created_adjustment = recurly_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?
2015-11-29 19:58:10 +00:00
@@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
2015-11-29 19:58:10 +00:00
if shopping_cart.is_jam_track?
jam_track = cart_product
2015-11-29 19:58:10 +00:00
# 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: current_user.id, jam_track_id: jam_track.id}) do |jam_track_right|
2015-11-29 19:58:10 +00:00
jam_track_right.redeemed = shopping_cart.free?
2015-11-30 23:54:17 +00:00
jam_track_right.version = jam_track.version
end
2015-11-29 19:58:10 +00:00
# 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?
current_user.redeem_free_credit
2015-11-29 19:58:10 +00:00
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
})
2015-11-29 19:58:10 +00:00
unless gift_card_purchase.save
raise RecurlyClientError.new(gift_card_purchase.errors)
end
end
2015-11-29 19:58:10 +00:00
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_now
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 is_lesson_sale?
sale_type == LESSON_SALE
end
2016-02-07 14:54:49 +00:00
def self.create_jam_track_sale(user, sale_source=nil)
sale = Sale.new
sale.user = user
2015-11-29 19:58:10 +00:00
sale.sale_type = JAMTRACK_SALE # gift cards and jam tracks are sold with this type of sale
sale.order_total = 0
2016-02-07 14:54:49 +00:00
sale.source = sale_source if sale_source
sale.save
sale
end
def self.create_lesson_sale(user)
sale = Sale.new
sale.user = user
sale.sale_type = LESSON_SALE # gift cards and jam tracks are sold with this type of sale
sale.order_total = 0
sale.save
sale
end
2016-08-31 09:19:16 +00:00
def self.create_posa_sale(retailer, posa_card)
sale = Sale.new
sale.retailer = retailer
sale.sale_type = POSA_SALE # gift cards and jam tracks are sold with this type of sale
sale.order_total = posa_card.product_info[:price]
2016-08-31 09:19:16 +00:00
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