diff --git a/Gemfile b/Gemfile index cb3243ef..c79d7cc2 100644 --- a/Gemfile +++ b/Gemfile @@ -8,12 +8,13 @@ gem 'unicorn' gem 'foreman' gem 'crowdtilt', github: 'Crowdtilt/crowdtilt-gem' +gem 'stripe' gem 'devise', '~> 3.2.0' gem 'nokogiri' gem 'friendly_id', '~> 4.0.9' gem 'iso_country_codes' gem 'paperclip', '~> 3.0' -gem 'ckeditor' +gem 'ckeditor', '4.0.4' gem 'aws-sdk' gem 'active_model_serializers' gem 'momentjs-rails' diff --git a/Gemfile.lock b/Gemfile.lock index e56ab3ff..23973bc8 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -145,6 +145,7 @@ GEM net-scp (1.1.1) net-ssh (>= 2.6.5) net-ssh (2.6.7) + netrc (0.10.3) newrelic_rpm (3.6.3.111) nokogiri (1.5.9) orm_adapter (0.4.0) @@ -190,6 +191,9 @@ GEM rake (10.0.4) rdoc (3.12.2) json (~> 1.4) + rest-client (1.7.3) + mime-types (>= 1.16, < 3.0) + netrc (~> 0.7) rspec-core (2.13.1) rspec-expectations (2.13.0) diff-lcs (>= 1.1.3, < 2.0) @@ -219,6 +223,9 @@ GEM multi_json (~> 1.0) rack (~> 1.0) tilt (~> 1.1, != 1.3.0) + stripe (1.8.8) + multi_json (>= 1.0.4, < 2) + rest-client (~> 1.4) thor (0.18.1) thread_safe (0.1.3) atomic @@ -249,7 +256,7 @@ DEPENDENCIES aws-sdk bootstrap-sass (= 2.1) capybara - ckeditor + ckeditor (= 4.0.4) coffee-rails (~> 3.2.1) crowdtilt! devise (~> 3.2.0) @@ -273,5 +280,6 @@ DEPENDENCIES rspec-rails sass-rails (~> 3.2.3) shoulda + stripe uglifier (>= 1.0.3) unicorn diff --git a/Makefile b/Makefile new file mode 100644 index 00000000..59babbf5 --- /dev/null +++ b/Makefile @@ -0,0 +1,19 @@ +all: + RAILS_ENV=production bundle exec foreman start; + +stop: + cat tmp/unicorn.pid | xargs kill -QUIT ; + +restart: + make stop; make; + +logs: + tail -f log/unicorn.log; + +assets: + RAILS_ENV=production bundle exec foreman run rake assets:precompile + + +local: + bundle exec foreman run unicorn -p 5000 -c ./config/unicorn.rb + diff --git a/Procfile b/Procfile index 9c823741..b7578160 100644 --- a/Procfile +++ b/Procfile @@ -1 +1 @@ -web: bundle exec unicorn -p $PORT -c ./config/unicorn.rb +web: bundle exec unicorn -p $PORT -c ./config/unicorn.rb -D diff --git a/app/assets/images/app_icon_114px.png b/app/assets/images/app_icon_114px.png index 581bbd02..7896c64e 100644 Binary files a/app/assets/images/app_icon_114px.png and b/app/assets/images/app_icon_114px.png differ diff --git a/app/assets/images/app_icon_144px.png b/app/assets/images/app_icon_144px.png index 8b7e600b..d4817cf8 100644 Binary files a/app/assets/images/app_icon_144px.png and b/app/assets/images/app_icon_144px.png differ diff --git a/app/assets/images/app_icon_172px.png b/app/assets/images/app_icon_172px.png index 2bdba397..fb04d1ce 100644 Binary files a/app/assets/images/app_icon_172px.png and b/app/assets/images/app_icon_172px.png differ diff --git a/app/assets/images/apple-touch-icon-120x120.png b/app/assets/images/apple-touch-icon-120x120.png new file mode 100644 index 00000000..f358bad2 Binary files /dev/null and b/app/assets/images/apple-touch-icon-120x120.png differ diff --git a/app/assets/images/apple-touch-icon-152x152.png b/app/assets/images/apple-touch-icon-152x152.png new file mode 100644 index 00000000..159dd8cc Binary files /dev/null and b/app/assets/images/apple-touch-icon-152x152.png differ diff --git a/app/assets/images/apple-touch-icon-57x57.png b/app/assets/images/apple-touch-icon-57x57.png new file mode 100644 index 00000000..5053e49b Binary files /dev/null and b/app/assets/images/apple-touch-icon-57x57.png differ diff --git a/app/assets/images/apple-touch-icon-76x76.png b/app/assets/images/apple-touch-icon-76x76.png new file mode 100644 index 00000000..db781ff0 Binary files /dev/null and b/app/assets/images/apple-touch-icon-76x76.png differ diff --git a/app/assets/images/apple-touch-icon.png b/app/assets/images/apple-touch-icon.png new file mode 100644 index 00000000..5053e49b Binary files /dev/null and b/app/assets/images/apple-touch-icon.png differ diff --git a/app/assets/images/favicon.ico b/app/assets/images/favicon.ico new file mode 100644 index 00000000..55badc64 Binary files /dev/null and b/app/assets/images/favicon.ico differ diff --git a/app/assets/javascripts/campaigns.js.coffee b/app/assets/javascripts/campaigns.js.coffee index 1dddf008..6674a4e7 100644 --- a/app/assets/javascripts/campaigns.js.coffee +++ b/app/assets/javascripts/campaigns.js.coffee @@ -16,11 +16,39 @@ Crowdhoster.campaigns = $('html,body').animate({scrollTop: $('#header')[0].scrollHeight}) $('#quantity').on "change", (e) -> - quantity = $(this).val() + unit_price_at_qty = parseFloat($(":selected", this).attr('data-price-at-qty')); + quantity = parseInt($(this).val()) $amount = $('#amount') + functional_total = quantity + parseInt($(".their-qty").attr("data-campaign-qty")) new_amount = parseFloat($amount.attr('data-original')) * quantity + new_amount_display = unit_price_at_qty * quantity + $("#unit-price-at-qty").html("$" + unit_price_at_qty.toFixed(2)); $amount.val(new_amount) - $('#total').html(new_amount.toFixed(2).replace(/\B(?=(\d{3})+(?!\d))/g, ",");) + $('#total').html(new_amount_display.toFixed(2).replace(/\B(?=(\d{3})+(?!\d))/g, ",")) + $(".their-qty").html(quantity) + $(".total-qty").html(functional_total) + + # loop over the tiers and update them + current_tier = null + $(".tier-list .tier").each (i, item) -> + min = parseInt $(item).attr "data-min-qty" + $item = $(item); + $item.removeClass "list-group-item-success" + $item.removeClass "list-group-item-info" + $item.removeClass "active" + + # we havent reached this tier + if(functional_total < min) + $item.addClass "list-group-item-info" + $(".people-needed", $item).html "" + (min - functional_total) + " More Orders to Activate" + else + $item.addClass "list-group-item-success" + $(".people-needed", $item).html "Activated with "+min+" Orders" + current_tier = $item + + current_tier.addClass "active" + $(".people-needed", current_tier).html "Current Price
Activated with "+min+" Orders" + $('#amount').on "keyup", (e) -> $(this).addClass('edited') diff --git a/app/assets/stylesheets/application.css b/app/assets/stylesheets/application.scss similarity index 80% rename from app/assets/stylesheets/application.css rename to app/assets/stylesheets/application.scss index fc74c06a..a20ea0d2 100644 --- a/app/assets/stylesheets/application.css +++ b/app/assets/stylesheets/application.scss @@ -14,4 +14,20 @@ *= require jquery.ui.all *= require main *= require theme -*/ \ No newline at end of file +*/ + +#header { + font-size: 35px; + + div.container { + color: #ff8e1f; + + a { + color: #ff8e1f; + } + + a:hover { + text-decoration: none; + } + } +} \ No newline at end of file diff --git a/app/assets/stylesheets/primitives.css.scss b/app/assets/stylesheets/primitives.css.scss index fc007540..9e7582b0 100644 --- a/app/assets/stylesheets/primitives.css.scss +++ b/app/assets/stylesheets/primitives.css.scss @@ -10,7 +10,7 @@ $text_shadow: 0 1px 0px #fff; $font_size: 22px; // Lockitron uses ProximaNova Regular, but that costs money, so we're defaulting to Helveitca Neue instead. -$primary_font: "Helvetica Neue"; +$primary_font: "proxima-nova-alt"; $secondary_font: "Helvetica"; $tertiary_font: "Arial"; @@ -29,7 +29,7 @@ $tertiary_font: "Arial"; h1 { margin: 0 auto; - font-family: Helvetica; + font-family: $primary_font; font-size: 42px; font-weight: bold; color: $h1_color; @@ -76,7 +76,7 @@ h5 { } p { - font-family: "Helvetica Neue","Helvetica","Arial"; + font-family: $primary_font, $secondary_font, $tertiary_font; color: #6b6b6b; font-size: 18px; line-height: 1.5; diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 484ffdd2..321b058d 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -4,6 +4,15 @@ class ApplicationController < ActionController::Base before_filter :load_settings, :set_default_mailer_host after_filter :store_location + def capture + if(params.has_key?(:referral_code)) + ReferralCode.create(params[:referral_code]); + redirect_to root_url, :flash => { :info => "Thank you for you're interest, we'll contact you before the next sale starts!" } + else + redirect_to root_url, :flash => { :error => "We're sorry there seems to have been an issue with your submission. Please try again later." } + end + end + def load_settings @settings = Settings.first diff --git a/app/controllers/campaigns_controller.rb b/app/controllers/campaigns_controller.rb index d0093d98..cba004d9 100644 --- a/app/controllers/campaigns_controller.rb +++ b/app/controllers/campaigns_controller.rb @@ -62,8 +62,10 @@ def checkout_payment return end + @display_subtotal = @campaign.price_at_additional_qty(@quantity) * @quantity @fee = (@campaign.apply_processing_fee)? calculate_processing_fee(@amount * 100)/100.0 : 0 @total = @amount + @fee + @display_total = @display_subtotal + @fee end @@ -123,53 +125,76 @@ def checkout_process end @payment.reward = @reward if @reward + @payment.ip_addr = request.remote_ip @payment.save # Execute the payment via the Crowdtilt API, if it fails, redirect user begin - payment = { - amount: payment_params[:amount], - user_fee_amount: user_fee_amount, - admin_fee_amount: admin_fee_amount, - user_id: ct_user_id, - card_id: ct_card_id, - metadata: { - fullname: payment_params[:fullname], - email: payment_params[:email], - billing_postal_code: payment_params[:billing_postal_code], - quantity: payment_params[:quantity], - reward: @reward ? @reward.id : 0, - additional_info: payment_params[:additional_info] - } - } - @campaign.production_flag ? Crowdtilt.production(@settings) : Crowdtilt.sandbox - - logger.info "CROWDTILT API REQUEST: /campaigns/#{@campaign.ct_campaign_id}/payments" - logger.info payment - response = Crowdtilt.post('/campaigns/' + @campaign.ct_campaign_id + '/payments', {payment: payment}) - logger.info "CROWDTILT API RESPONSE:" - logger.info response - rescue Crowdtilt::ApiError => api_error - response = api_error.response - logger.error "API ERROR WITH POST TO /payments: #{response[:status]} #{response[:body]}" - error_attributes = {status: 'error'} - error_attributes[:ct_charge_request_id] = response[:body]['request_id'] if response[:body]['request_id'] - error_attributes[:ct_charge_request_error_id] = response[:body]['error_id'] if response[:body]['error_id'] - @payment.update_attributes(error_attributes) - redirect_to checkout_amount_url(@campaign), flash: { error: "There was an error processing your payment. Please try again or contact support by emailing open@tilt.com" } and return - rescue StandardError => exception - @payment.update_attributes({status: 'error'}) - logger.error "ERROR WITH POST TO /payments: #{exception.message}" - redirect_to checkout_amount_url(@campaign), flash: { error: "There was an error processing your payment. Please try again or contact support by emailing open@tilt.com" } and return + require "stripe" + Stripe.api_key = ENV['STRIPE_KEY'] + + response = Stripe::Charge.create( + :amount => payment_params[:amount], + :currency => "usd", + :source => { + :object => "card", + :number => params[:card_no], + :exp_month => params[:expiration_month], + :exp_year => params[:expiration_year], + :name => payment_params[:fullname], + :cvc => params[:security_code], + :address_zip => params[:billing_postal_code] + }, + :description => "Cincodebuyo campaign authorization", + :capture => false + ) + rescue => e + redirect_to checkout_amount_url(@campaign), flash: { error: "There was an error processing your payment. Please try again or contact support by emailing info@cincodebuyo.com" } and return + + + # payment = { + # amount: payment_params[:amount], + # user_fee_amount: user_fee_amount, + # admin_fee_amount: admin_fee_amount, + # user_id: ct_user_id, + # card_id: ct_card_id, + # metadata: { + # fullname: payment_params[:fullname], + # email: payment_params[:email], + # billing_postal_code: payment_params[:billing_postal_code], + # quantity: payment_params[:quantity], + # reward: @reward ? @reward.id : 0, + # additional_info: payment_params[:additional_info] + # } + # } + # @campaign.production_flag ? Crowdtilt.production(@settings) : Crowdtilt.sandbox + + # logger.info "CROWDTILT API REQUEST: /campaigns/#{@campaign.ct_campaign_id}/payments" + # logger.info payment + # response = Crowdtilt.post('/campaigns/' + @campaign.ct_campaign_id + '/payments', {payment: payment}) + # logger.info "CROWDTILT API RESPONSE:" + # logger.info response + # rescue Crowdtilt::ApiError => api_error + # response = api_error.response + # logger.error "API ERROR WITH POST TO /payments: #{response[:status]} #{response[:body]}" + # error_attributes = {status: 'error'} + # error_attributes[:ct_charge_request_id] = response[:body]['request_id'] if response[:body]['request_id'] + # error_attributes[:ct_charge_request_error_id] = response[:body]['error_id'] if response[:body]['error_id'] + # @payment.update_attributes(error_attributes) + # redirect_to checkout_amount_url(@campaign), flash: { error: "There was an error processing your payment. Please try again or contact support by emailing open@tilt.com" } and return + # rescue StandardError => exception + # @payment.update_attributes({status: 'error'}) + # logger.error "ERROR WITH POST TO /payments: #{exception.message}" + # redirect_to checkout_amount_url(@campaign), flash: { error: "There was an error processing your payment. Please try again or contact support by emailing open@tilt.com" } and return end # Sync payment data - @payment.update_api_data(response['payment']) - @payment.ct_charge_request_id = response['request_id'] + @payment.update_api_data(response) + @payment.ct_charge_request_id = response['id'] @payment.save # Sync campaign data - @campaign.update_api_data(response['payment']['campaign']) + @campaign.update_api_data(@payment.amount) @campaign.save # Send confirmation emails diff --git a/app/controllers/pages_controller.rb b/app/controllers/pages_controller.rb index 904e9033..0a910b5c 100644 --- a/app/controllers/pages_controller.rb +++ b/app/controllers/pages_controller.rb @@ -2,6 +2,11 @@ class PagesController < ApplicationController before_filter :check_init def index + # Check for referral code + if request[:referral_code] + cookies[:referral_code] = request[:referral_code] + end + if @settings.default_campaign && ((user_signed_in? && current_user.admin?) || @settings.default_campaign.published_flag) redirect_to campaign_home_url(@settings.default_campaign) else diff --git a/app/mixins/checkout_mixin.rb b/app/mixins/checkout_mixin.rb index f2e242cd..aa2366a6 100644 --- a/app/mixins/checkout_mixin.rb +++ b/app/mixins/checkout_mixin.rb @@ -21,6 +21,9 @@ def basic_payment_info(params) billing_postal_code: params[:billing_postal_code], quantity: params[:quantity].to_i, + #Referral + referred_by: params[:referred_by], + #Shipping Info address_one: params.has_key?(:address_one) ? params[:address_one] : '', address_two: params.has_key?(:address_two) ? params[:address_two] : '', diff --git a/app/models/campaign.rb b/app/models/campaign.rb index 5f310f48..c0352aca 100644 --- a/app/models/campaign.rb +++ b/app/models/campaign.rb @@ -4,6 +4,7 @@ class Campaign < ActiveRecord::Base has_many :faqs, dependent: :destroy, :order => 'sort_order' has_many :payments has_many :rewards + has_many :campaign_tiers, :order => 'min_users ASC' attr_accessible :name, :goal_type, :goal_dollars, :goal_orders, :expiration_date, :ct_campaign_id, :media_type, :main_image, :main_image_delete, :video_embed_id, :video_placeholder, :video_placeholder_delete, @@ -39,15 +40,79 @@ class Campaign < ActiveRecord::Base before_save :set_min_amount - def update_api_data(campaign) - self.ct_campaign_id = campaign['id'] - self.stats_number_of_contributions = campaign['stats']['number_of_contributions'] - self.stats_raised_amount = campaign['stats']['raised_amount']/100.0 - self.stats_tilt_percent = campaign['stats']['tilt_percent'] - self.stats_unique_contributors = campaign['stats']['unique_contributors'] - self.is_tilted = campaign['is_tilted'].to_i == 0 ? false : true - self.is_expired = campaign['is_expired'].to_i == 0 ? false : true - self.is_paid = campaign['is_paid'].to_i == 0 ? false : true + def update_api_data(amount) + # self.ct_campaign_id = campaign['id'] + # self.stats_raised_amount = amount / 100.0 + self.stats_number_of_contributions = self.orders + self.stats_unique_contributors = self.payments.length + self.stats_tilt_percent = 100 + self.is_tilted = true + self.is_paid = false + self.is_expired = self.expired? + end + + def stats_raised_amount + total = 0 + self.payments.each do |p| + if(p.status === "paid") + total = total + (p.amount / 100.0) + end + end + + total + (self.fake_users * self.fixed_payment_amount) + end + + def price_at_additional_qty(amount) + expected_orders = self.orders + amount + price = self.base_price + tier = self.tier_for_orders expected_orders + if(!tier.nil?) + price = tier.price_at_tier + end + + price + end + + def current_tier_price + price = self.base_price; + if(!self.current_tier.nil?) + price = self.current_tier.price_at_tier + end + + price + end + + def next_tier + tier = self.campaign_tiers.first + self.campaign_tiers.each do |t| + if(self.orders < t.min_users) + tier = t + break + end + end + + tier + end + + def until_next_tier + self.next_tier.min_users - self.orders + end + + def current_tier + self.tier_for_orders self.orders + end + + def tier_for_orders(order_amount) + max_users = 0; + tier = nil + self.campaign_tiers.each do |t| + if(order_amount >= t.min_users && max_users <= t.min_users) + tier = t + max_users = t.min_users + end + end + + tier end def set_goal @@ -57,7 +122,7 @@ def set_goal end def expired? - self.expiration_date < Time.current + self.expiration_date < Time.current or self.sold_out end def orders @@ -81,7 +146,7 @@ def number_of_contributions end def tilt_percent - (raised_amount / goal_dollars) * 100.0 + (self.stats_raised_amount / goal_dollars) * 100.0 end private diff --git a/app/models/campaign_tier.rb b/app/models/campaign_tier.rb new file mode 100644 index 00000000..c362eb9d --- /dev/null +++ b/app/models/campaign_tier.rb @@ -0,0 +1,21 @@ +class CampaignTier < ActiveRecord::Base + attr_accessible :campaign_id, :min_users, :price_at_tier + belongs_to :campaign + + def pct_off + ((self.campaign.base_price - self.price_at_tier) / self.campaign.base_price) * 100.0 + end + + def pct_complete + orders = self.campaign.orders + (1.0 * [orders, self.min_users].min / self.min_users) * 100.0 + end + + def complete + self.pct_complete >= 100 + end + + def remaining + return [0, self.min_users - self.campaign.orders].max + end +end diff --git a/app/models/payment.rb b/app/models/payment.rb index 7cec8d77..481a1d34 100644 --- a/app/models/payment.rb +++ b/app/models/payment.rb @@ -5,7 +5,9 @@ class Payment < ActiveRecord::Base :additional_info, :client_timestamp, :ct_charge_request_id, :ct_charge_request_error_id, :ct_tokenize_request_id, :ct_tokenize_request_error_id, - :ct_user_id + :ct_user_id, + :referred_by, + :ip_addr validates :fullname, :quantity, presence: true validates :email, presence: true, email: true @@ -56,12 +58,12 @@ def update_api_data(payment) self.ct_payment_id = payment['id'] self.status = payment['status'] self.amount = payment['amount'] - self.user_fee_amount = payment['user_fee_amount'] - self.admin_fee_amount = payment['admin_fee_amount'] - self.card_type = payment['card']['card_type'] - self.card_last_four = payment['card']['last_four'] - self.card_expiration_month = payment['card']['expiration_month'] - self.card_expiration_year = payment['card']['expiration_year'] + self.user_fee_amount = 0 + self.admin_fee_amount = 0 + self.card_type = payment['source']['brand'] + self.card_last_four = payment['source']['last4'] + self.card_expiration_month = payment['source']['exp_month'] + self.card_expiration_year = payment['source']['exp_year'] end def refund! @@ -78,4 +80,26 @@ def self.display_date(date) date.strftime("%m/%d/%Y") end + def referral_code + code = ReferralCode.where(email: self.email) + if(code.length > 0) + return code[0] + else + code = ReferralCode.create(code:('0'..'9').to_a.concat(('A'..'Z').to_a).concat(('a'..'z').to_a).shuffle[0,8].join, email: self.email, comment:"purchase #" + self.id.to_s) + code + end + end + + # return the referral code for the user who made this payment + # returns nil if no user is found or if they do not have a referral + # source + def get_user_referral_code() + user = User.where(email: self.email) + if(user.length > 0) + return user[0].referred_by + else + return nil + end + end + end diff --git a/app/models/referral_code.rb b/app/models/referral_code.rb new file mode 100644 index 00000000..7b5edb47 --- /dev/null +++ b/app/models/referral_code.rb @@ -0,0 +1,9 @@ +class ReferralCode < ActiveRecord::Base + attr_accessible :code, :comment, :email + + def before_save + if(self.code.nil?) + self.code = ('0'..'9').to_a.concat(('A'..'Z').to_a).concat(('a'..'z').to_a).shuffle[0,8].join + end + end +end diff --git a/app/models/user.rb b/app/models/user.rb index dfe1d134..f6241e0d 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -6,10 +6,9 @@ class User < ActiveRecord::Base # Setup accessible (or protected) attributes for your model attr_accessible :email, :password, :password_confirmation, :remember_me, :fullname, - :wants_admin_payment_notification + :wants_admin_payment_notification, :referred_by # Validate presence of user inputs. # (most in this model are handled by Devise -- email, password, and password_confirmation) validates :fullname, presence: true - end diff --git a/app/views/campaigns/checkout_amount.html.erb b/app/views/campaigns/checkout_amount.html.erb index 913e2c5c..adab47af 100644 --- a/app/views/campaigns/checkout_amount.html.erb +++ b/app/views/campaigns/checkout_amount.html.erb @@ -1,9 +1,9 @@
-
-
-

<%= @campaign.name %>

- -
+
+

<%= @campaign.name %>

+
+
+
<% if @campaign.payment_type == 'fixed' %> @@ -12,20 +12,20 @@

Please choose a quantity:


- <%= short_price(@campaign.fixed_payment_amount, '$', 2) %>  x   + <%= short_price(@campaign.price_at_additional_qty(1), '$', 2) %>  x   -   =  $<%= short_price(@campaign.fixed_payment_amount, '', 2) %> +   =  $<%= short_price(@campaign.price_at_additional_qty(1), '', 2) %>
@@ -81,18 +81,40 @@
<% end %> - - +
+
-
- -