2015-04-03 20:34:12 +00:00
module JamRuby
# a sale is created every time someone tries to buy something
class Sale < ActiveRecord :: Base
2015-04-10 20:19:08 +00:00
JAMTRACK_SALE = 'jamtrack'
2016-04-06 02:23:15 +00:00
LESSON_SALE = 'lesson'
2016-08-31 09:19:16 +00:00
POSA_SALE = 'posacard'
2020-10-09 22:22:20 +00:00
SUBSCRIPTION_SALE = 'subscription'
2015-04-10 20:19:08 +00:00
2016-02-09 15:12:10 +00:00
SOURCE_RECURLY = 'recurly'
SOURCE_IOS = 'ios'
2016-12-15 18:47:08 +00:00
SOURCE_PAYPAL = 'paypal'
2016-02-09 15:12:10 +00:00
2016-08-31 09:19:16 +00:00
belongs_to :retailer , class_name : 'JamRuby::Retailer'
2015-04-03 20:34:12 +00:00
belongs_to :user , class_name : 'JamRuby::User'
has_many :sale_line_items , class_name : 'JamRuby::SaleLineItem'
2015-04-12 18:45:26 +00:00
has_many :recurly_transactions , class_name : 'JamRuby::RecurlyTransactionWebHook' , inverse_of : :sale , foreign_key : 'invoice_id' , primary_key : 'recurly_invoice_id'
2015-04-10 20:19:08 +00:00
validates :order_total , numericality : { only_integer : false }
2016-08-31 09:19:16 +00:00
#validates :user
#validates :retailer
2015-04-03 20:34:12 +00:00
2015-04-12 18:45:26 +00:00
@@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 )
2016-04-06 02:23:15 +00:00
if query . length == 0 # no more results
{ query : query , next_page : nil }
elsif query . length < limit # no more results
{ query : query , next_page : nil }
2015-04-12 18:45:26 +00:00
else
2016-04-06 02:23:15 +00:00
{ query : query , next_page : next_page }
2015-04-12 18:45:26 +00:00
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
2015-04-10 20:19:08 +00:00
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 = {
2016-07-10 01:48:22 +00:00
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
}
2016-07-10 01:48:22 +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 = {
2016-07-10 01:48:22 +00:00
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
2015-04-10 20:19:08 +00:00
2017-02-05 20:42:51 +00:00
def self . ios_purchase ( current_user , jam_track , receipt , price_data , variant )
if variant . nil?
variant = ShoppingCart :: JAMTRACK_STREAM
end
# see if we should bail because we already own these rights
jam_track_right = jam_track . right_for_user ( current_user )
if ! jam_track_right . nil? && jam_track_right . can_download
# if the user already has full rights to the JamTrack, there is nothing else to do in this path
return jam_track_right
end
2017-02-06 04:07:08 +00:00
if ! jam_track_right . nil? && ( ! jam_track_right . can_download && variant == ShoppingCart :: JAMTRACK_STREAM )
2017-02-05 20:42:51 +00:00
# if the user does have the track, but isn't upgrading it, bail
return jam_track_right
end
2016-01-28 17:31:57 +00:00
# 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
2016-02-09 15:12:10 +00:00
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 15:12:10 +00:00
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
2017-02-05 20:42:51 +00:00
if variant == ShoppingCart :: JAMTRACK_DOWNLOAD || variant == ShoppingCart :: JAMTRACK_FULL
jam_track_right . can_download = true
jam_track_right . save
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
2015-04-10 20:19:08 +00:00
# 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 )
2015-04-10 20:19:08 +00:00
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
2015-04-10 20:19:08 +00:00
2016-12-15 18:47:08 +00:00
jam_track_sale = order_jam_tracks ( current_user , shopping_carts , paypal )
2015-04-10 20:19:08 +00:00
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 )
2015-11-13 13:12:58 +00:00
free = true
2015-11-29 19:58:10 +00:00
shopping_carts . each do | cart |
2015-11-13 13:12:58 +00:00
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
2015-05-15 17:34:35 +00:00
end
2017-07-10 02:21:29 +00:00
def self . purchase_test_drive ( current_user , lesson_package_type , booking = nil , posa_card = nil )
self . purchase_lesson ( nil , current_user , booking , lesson_package_type , nil , nil , false , posa_card )
2016-04-06 02:23:15 +00:00
end
2016-07-10 01:48:22 +00:00
def self . post_sale_test_failure
return true
2016-04-06 02:23:15 +00:00
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
2020-10-09 22:22:20 +00:00
def self . purchase_subscription ( current_user , recurly_token , plan_code )
sale = nil
Sale . transaction ( :requires_new = > true ) do
current_user . recurly_token = recurly_token
current_user . subscription_plan_code = plan_code
sale = create_subscription_sale ( current_user )
if sale . valid?
client = RecurlyClient . new
2020-11-30 00:24:28 +00:00
# this is handled in update_payment now
#account = client.find_or_create_account(current_user, nil, recurly_token)
#client.update_billing_info_from_token(current_user, account, recurly_token)
2020-11-21 22:14:37 +00:00
2020-11-30 00:24:28 +00:00
account = client . get_account ( current_user )
2020-10-09 22:22:20 +00:00
if account . present?
recurly_response = client . create_subscription ( current_user , plan_code , account )
current_user . recurly_subscription_id = recurly_response . uuid
current_user . save ( validate : false )
SaleLineItem . create_from_subscription ( current_user , sale , plan_code , recurly_response )
sale . recurly_subtotal_in_cents = recurly_response . unit_amount_in_cents
sale . recurly_tax_in_cents = recurly_response . tax_in_cents
sale . recurly_total_in_cents = sale . recurly_subtotal_in_cents + sale . recurly_tax_in_cents
sale . recurly_currency = recurly_response . currency
sale . save ( validate : false )
else
2020-11-21 22:14:37 +00:00
puts " Could not find account to place order. "
2020-10-09 22:22:20 +00:00
raise RecurlyClientError , " Could not find account to place order. "
end
end
end
{ sale : sale }
end
2016-04-06 02:23:15 +00:00
# this is easy to make generic, but right now, it just purchases lessons
2017-07-10 02:21:29 +00:00
def self . purchase_lesson ( charge , current_user , lesson_booking , lesson_package_type , lesson_session = nil , lesson_package_purchase = nil , force = false , posa_card = nil )
2016-04-06 02:23:15 +00:00
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-04-06 02:23:15 +00:00
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-04-06 02:23:15 +00:00
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-04-06 02:23:15 +00:00
2017-07-10 02:21:29 +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 , posa_card )
2016-04-06 02:23:15 +00:00
2016-08-31 09:19:16 +00:00
post_sale_test_failure
2016-04-06 02:23:15 +00:00
2016-08-31 09:19:16 +00:00
if price_info [ :purchase ] && price_info [ :purchase ] . errors . any?
2016-07-10 01:48:22 +00:00
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 } "
2016-07-10 01:48:22 +00:00
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
2016-04-06 02:23:15 +00:00
end
2016-05-16 16:39:20 +00:00
2016-04-06 02:23:15 +00:00
{ sale : sale , stripe_charge : stripe_charge , purchase : purchase }
end
2017-07-10 02:21:29 +00:00
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 , posa_card = nil )
2016-04-06 02:23:15 +00:00
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
2017-07-10 02:21:29 +00:00
purchase = LessonPackagePurchase . create ( current_user , lesson_booking , lesson_package_type , nil , nil , posa_card ) if purchase . nil?
2016-04-06 02:23:15 +00:00
if purchase . errors . any?
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
2016-10-03 02:51:34 +00:00
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
2016-07-10 01:48:22 +00:00
charge_id = charge . id if charge # not set if test drive
2016-10-03 02:51:34 +00:00
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
2016-04-06 02:23:15 +00:00
stripe_charge = Stripe :: Charge . create (
:amount = > total_in_cents ,
:currency = > " usd " ,
:customer = > current_user . stripe_customer_id ,
2016-07-10 01:48:22 +00:00
:description = > target . stripe_description ( lesson_booking ) ,
2016-10-03 02:51:34 +00:00
:metadata = > metadata
2016-04-06 02:23:15 +00:00
)
2016-07-10 01:48:22 +00:00
if charge
charge . stripe_charge = stripe_charge
end
2016-04-06 02:23:15 +00:00
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
2015-04-10 20:19:08 +00:00
# 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
2015-04-10 20:19:08 +00:00
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 )
2015-04-10 20:19:08 +00:00
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 )
2015-05-15 17:34:35 +00:00
sale . recurly_subtotal_in_cents = 0
sale . recurly_tax_in_cents = 0
sale . recurly_total_in_cents = 0
sale . recurly_currency = 'USD'
2015-08-19 14:24:14 +00:00
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
2015-05-15 17:34:35 +00:00
sale . save
else
2016-12-15 18:47:08 +00:00
if is_paypal
sale . process_shopping_carts ( current_user , shopping_carts )
2015-05-15 17:34:35 +00:00
2016-12-15 18:47:08 +00:00
paypal_auth = current_user . paypal_auth
2015-05-15 17:34:35 +00:00
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 )
2015-05-15 17:34:35 +00:00
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
2015-05-15 17:34:35 +00:00
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
2015-05-15 17:34:35 +00:00
# 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
2015-05-15 17:34:35 +00:00
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
2015-04-10 20:19:08 +00:00
end
2016-12-15 18:47:08 +00:00
unless sale . save
puts " Invalid sale (at end). "
raise RecurlyClientError , " Invalid sale (at end). "
2015-05-15 17:34:35 +00:00
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 } "
2015-04-13 20:54:23 +00:00
end
2016-12-15 18:47:08 +00:00
else
raise RecurlyClientError , " Could not find account to place order. "
2015-04-10 20:19:08 +00:00
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 )
2015-04-10 20:19:08 +00:00
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 )
2015-04-10 20:19:08 +00:00
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 )
2015-04-10 20:19:08 +00:00
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
2017-02-05 20:42:51 +00:00
if jam_track . right_for_user ( current_user , shopping_cart . variant )
2015-11-29 19:58:10 +00:00
# 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
2015-04-10 20:19:08 +00:00
end
2015-11-29 19:58:10 +00:00
2016-12-15 18:47:08 +00:00
if recurly_account
2015-05-15 17:34:35 +00:00
# ask the shopping cart to create the correct Recurly adjustment attributes for a JamTrack
adjustments = shopping_cart . create_adjustment_attributes ( current_user )
2015-04-10 20:19:08 +00:00
2015-05-15 17:34:35 +00:00
adjustments . each do | adjustment |
2015-04-10 20:19:08 +00:00
2015-05-15 17:34:35 +00:00
# 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 )
2015-05-15 17:34:35 +00:00
created_adjustment . save
2015-04-10 20:19:08 +00:00
2015-05-15 17:34:35 +00:00
# if the adjustment could not be made, bail
raise RecurlyClientError . new ( created_adjustment . errors ) if created_adjustment . errors . any?
2015-04-10 20:19:08 +00:00
2015-05-15 17:34:35 +00:00
# keep track of adjustments we created for this order, in case we have to roll them back
created_adjustments << created_adjustment
2015-04-10 20:19:08 +00:00
2015-05-15 17:34:35 +00:00
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
2015-04-10 20:19:08 +00:00
end
end
2015-05-15 17:34:35 +00:00
2015-04-10 20:19:08 +00:00
# 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 } " )
2015-04-10 20:19:08 +00:00
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-04-10 20:19:08 +00:00
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)
2016-07-17 15:16:27 +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 |
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
2015-11-13 13:12:58 +00:00
end
2015-05-15 17:34:35 +00:00
2017-02-05 20:42:51 +00:00
# deal with variant behavior
if shopping_cart . purchasing_downloadable_rights?
jam_track_right . can_download = true
jam_track_right . save
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?
2016-01-03 17:38:30 +00:00
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-04-10 20:19:08 +00:00
2015-11-29 19:58:10 +00:00
unless gift_card_purchase . save
raise RecurlyClientError . new ( gift_card_purchase . errors )
end
2015-04-10 20:19:08 +00:00
end
2015-11-29 19:58:10 +00:00
else
raise 'unknown shopping cart type: ' + shopping_cart . cart_type
2015-04-10 20:19:08 +00:00
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 \n Adjustments: #{ adjustments . inspect } "
2016-07-17 15:16:27 +00:00
} ) . deliver_now
2015-04-10 20:19:08 +00:00
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
2016-04-06 02:23:15 +00:00
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 )
2015-04-03 20:34:12 +00:00
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
2015-04-03 20:34:12 +00:00
sale . order_total = 0
2016-02-07 14:54:49 +00:00
sale . source = sale_source if sale_source
2015-04-03 20:34:12 +00:00
sale . save
sale
end
2016-04-06 02:23:15 +00:00
def self . create_lesson_sale ( user )
sale = Sale . new
sale . user = user
2020-10-09 22:22:20 +00:00
sale . sale_type = LESSON_SALE
sale . order_total = 0
sale . save
sale
end
def self . create_subscription_sale ( user )
sale = Sale . new
sale . user = user
sale . sale_type = SUBSCRIPTION_SALE
2016-04-06 02:23:15 +00:00
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
2020-10-09 22:22:20 +00:00
sale . sale_type = POSA_SALE
2016-10-03 02:51:34 +00:00
sale . order_total = posa_card . product_info [ :price ]
2016-08-31 09:19:16 +00:00
sale . save
sale
end
2015-04-10 20:19:08 +00:00
# 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,
2015-04-03 20:34:12 +00:00
COUNT ( CASE WHEN transactions . transaction_type = '#{RecurlyTransactionWebHook::VOID}' THEN 1 ELSE null END ) voided
2015-04-10 20:19:08 +00:00
FROM sales
LEFT OUTER JOIN recurly_transaction_web_hooks as transactions ON invoice_id = sales . recurly_invoice_id
WHERE sale_type = '#{JAMTRACK_SALE}' " )
2015-04-03 20:34:12 +00:00
end
end
2016-01-03 17:38:30 +00:00
end