From b639d2eec547d5aa99e51ddac10a5df2f8a75564 Mon Sep 17 00:00:00 2001 From: Rishabh Saxena Date: Wed, 27 Jul 2016 13:18:50 +0530 Subject: [PATCH 01/31] add stripe-gem and payment schema --- Gemfile | 3 +++ Gemfile.lock | 3 +++ db/migrate/20160606040848_create_payments.rb | 14 ++++++++++++++ ...610073948_add_payment_id_to_ticket_purchases.rb | 5 +++++ db/schema.rb | 12 ++++++++++++ 5 files changed, 37 insertions(+) create mode 100644 db/migrate/20160606040848_create_payments.rb create mode 100644 db/migrate/20160610073948_add_payment_id_to_ticket_purchases.rb diff --git a/Gemfile b/Gemfile index 837d5f23..14ef20ad 100644 --- a/Gemfile +++ b/Gemfile @@ -184,6 +184,9 @@ gem 'faker' # for seeds gem 'factory_girl_rails' +# for integrating Stripe payment gateway +gem 'stripe' + # Use guard and spring for testing in development group :development do # to launch specs when files are modified diff --git a/Gemfile.lock b/Gemfile.lock index b9d8b87a..770dcf66 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -481,6 +481,8 @@ GEM activesupport (>= 3.0) sprockets (>= 2.8, < 4.0) sqlite3 (1.3.9) + stripe (1.43.0) + rest-client (~> 1.4) term-ansicolor (1.3.2) tins (~> 1.0) thor (0.19.1) @@ -615,6 +617,7 @@ DEPENDENCIES shoulda-matchers spring-commands-rspec sqlite3 + stripe timecop transitions turbolinks diff --git a/db/migrate/20160606040848_create_payments.rb b/db/migrate/20160606040848_create_payments.rb new file mode 100644 index 00000000..b7428ca3 --- /dev/null +++ b/db/migrate/20160606040848_create_payments.rb @@ -0,0 +1,14 @@ +class CreatePayments < ActiveRecord::Migration + def change + create_table :payments do |t| + t.string :last4, null: false + t.integer :amount, null: false + t.string :authorization_code, null: false + t.integer :status, default: 0, null: false + t.integer :user_id, null: false + t.integer :conference_id, null: false + + t.timestamps null: false + end + end +end diff --git a/db/migrate/20160610073948_add_payment_id_to_ticket_purchases.rb b/db/migrate/20160610073948_add_payment_id_to_ticket_purchases.rb new file mode 100644 index 00000000..133a047b --- /dev/null +++ b/db/migrate/20160610073948_add_payment_id_to_ticket_purchases.rb @@ -0,0 +1,5 @@ +class AddPaymentIdToTicketPurchases < ActiveRecord::Migration + def change + add_column :ticket_purchases, :payment_id, :integer + end +end diff --git a/db/schema.rb b/db/schema.rb index 8494999a..836168d8 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -263,6 +263,17 @@ ActiveRecord::Schema.define(version: 20160704092023) do t.datetime "updated_at" end + create_table "payments", force: :cascade do |t| + t.string "last4", null: false + t.integer "amount", null: false + t.string "authorization_code", null: false + t.integer "status", default: 0, null: false + t.integer "user_id", null: false + t.integer "conference_id", null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + end + create_table "programs", force: :cascade do |t| t.integer "conference_id" t.integer "rating", default: 0 @@ -423,6 +434,7 @@ ActiveRecord::Schema.define(version: 20160704092023) do t.datetime "created_at" t.integer "quantity", default: 1 t.integer "user_id" + t.integer "payment_id" end create_table "tickets", force: :cascade do |t| From ee0a575a6f3d68dd66f8eff852c525d16250df82 Mon Sep 17 00:00:00 2001 From: Rishabh Saxena Date: Wed, 27 Jul 2016 19:27:39 +0530 Subject: [PATCH 02/31] add payment model and its associations --- app/models/conference.rb | 1 + app/models/payment.rb | 27 +++++++++++++++++++++++++++ app/models/user.rb | 1 + 3 files changed, 29 insertions(+) create mode 100644 app/models/payment.rb diff --git a/app/models/conference.rb b/app/models/conference.rb index 9079bd71..ad95e902 100644 --- a/app/models/conference.rb +++ b/app/models/conference.rb @@ -20,6 +20,7 @@ class Conference < ActiveRecord::Base has_one :program, dependent: :destroy has_one :venue, dependent: :destroy has_many :ticket_purchases, dependent: :destroy + has_many :payments, dependent: :destroy has_many :supporters, through: :ticket_purchases, source: :user has_many :tickets, dependent: :destroy diff --git a/app/models/payment.rb b/app/models/payment.rb new file mode 100644 index 00000000..ecc4db5d --- /dev/null +++ b/app/models/payment.rb @@ -0,0 +1,27 @@ +class Payment < ActiveRecord::Base + has_many :ticket_purchases + belongs_to :user + belongs_to :conference + + validates :last4, presence: true + validates :authorization_code, presence: true + validates :status, presence: true + validates :amount, presence: true, numericality: { greater_than: 0 } + validates :user_id, presence: true + validates :conference_id, presence: true + + enum status: { + unpaid: 0, + success: 1, + failure: 2 + } + + def self.purchase(gateway_response, user, conference) + create(last4: gateway_response[:source][:last4], + amount: gateway_response[:amount], + status: (gateway_response[:paid] ? 1 : 0), + authorization_code: gateway_response[:id], + user_id: user.id, + conference_id: conference.id) + end +end diff --git a/app/models/user.rb b/app/models/user.rb index d3c847a2..4878ed9a 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -38,6 +38,7 @@ class User < ActiveRecord::Base has_many :registrations, dependent: :destroy has_many :events_registrations, through: :registrations has_many :ticket_purchases, dependent: :destroy + has_many :payments, dependent: :destroy has_many :tickets, through: :ticket_purchases, source: :ticket has_many :votes, dependent: :destroy has_many :voted_events, through: :votes, source: :events From 8f39fc83d29cc4940c14ee10ad5772f13e29e26b Mon Sep 17 00:00:00 2001 From: Rishabh Saxena Date: Wed, 27 Jul 2016 19:37:02 +0530 Subject: [PATCH 03/31] add resource, ability and controller for payments --- app/controllers/payments_controller.rb | 48 ++++++++++++++++++++++++++ app/models/ability.rb | 1 + config/routes.rb | 1 + 3 files changed, 50 insertions(+) create mode 100644 app/controllers/payments_controller.rb diff --git a/app/controllers/payments_controller.rb b/app/controllers/payments_controller.rb new file mode 100644 index 00000000..161d2101 --- /dev/null +++ b/app/controllers/payments_controller.rb @@ -0,0 +1,48 @@ +class PaymentsController < ApplicationController + before_action :authenticate_user! + load_and_authorize_resource + load_resource :conference, find_by: :short_title + authorize_resource :conference_registrations, class: Registration + + def index + @payments = current_user.payments + end + + def new + @total_amount_to_pay = Ticket.total_price(@conference, current_user, paid: false) + @unpaid_ticket_purchases = current_user.ticket_purchases.unpaid.by_conference(@conference) + end + + def create + @unpaid_ticket_purchases = current_user.ticket_purchases.unpaid.by_conference(@conference) + @total_amount_to_pay = Ticket.total_price(@conference, current_user, paid: false) + + customer = Stripe::Customer.create( + :email => params[:stripeEmail], + :source => params[:stripeToken] + ) + + gateway_response = Stripe::Charge.create( + :customer => customer.id, + :amount => @total_amount_to_pay.cents, + :description => 'Rails Stripe customer', + :currency => @conference.tickets.first.price_currency + ) + + payment = Payment.purchase(gateway_response, current_user, @conference) + update_purchased_ticket_purchases(payment) + + redirect_to conference_conference_registration_path(@conference.short_title), + flash: { success: 'Thanks! You have purchased your tickets successfully.' } + + rescue Stripe::CardError => e + flash[:error] = e.message + render 'new' + end + +private + + def update_purchased_ticket_purchases(payment) + current_user.ticket_purchases.by_conference(@conference).unpaid.update_all(paid: true, payment_id: payment.id) + end +end diff --git a/app/models/ability.rb b/app/models/ability.rb index 2210a7cd..74b1e47d 100644 --- a/app/models/ability.rb +++ b/app/models/ability.rb @@ -81,6 +81,7 @@ class Ability can :index, Ticket can :manage, TicketPurchase, user_id: user.id + can [:new], Payment, user_id: user.id can [:create, :destroy], Subscription, user_id: user.id diff --git a/config/routes.rb b/config/routes.rb index 25c253e5..d1b79617 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -112,6 +112,7 @@ Osem::Application.routes.draw do resource :conference_registration, path: 'register' resources :tickets, only: [:index] resources :ticket_purchases, only: [:create, :destroy] + resources :payments, only: [:index, :new, :create] resource :subscriptions, only: [:create, :destroy] resource :schedule, only: [:show] do member do From 92dc0edff28ab396f5f84a8e1a9b7f7ee590e03c Mon Sep 17 00:00:00 2001 From: Rishabh Saxena Date: Wed, 27 Jul 2016 19:38:57 +0530 Subject: [PATCH 04/31] modify registration view and controller --- .../conference_registrations_controller.rb | 6 ++- .../conference_registrations/show.html.haml | 40 +++++++++---------- 2 files changed, 23 insertions(+), 23 deletions(-) diff --git a/app/controllers/conference_registrations_controller.rb b/app/controllers/conference_registrations_controller.rb index 95e5c447..176f7937 100644 --- a/app/controllers/conference_registrations_controller.rb +++ b/app/controllers/conference_registrations_controller.rb @@ -28,8 +28,10 @@ class ConferenceRegistrationsController < ApplicationController end def show - @total_price = Ticket.total_price(@conference, current_user) - @tickets = current_user.ticket_purchases.where(conference_id: @conference.id) + @total_price = Ticket.total_price(@conference, current_user, paid: true) + @tickets = current_user.ticket_purchases.by_conference(@conference).paid + @ticket_payments = @tickets.group_by(&:ticket_id) + @total_quantity = @tickets.group(:ticket_id).sum(:quantity) end def edit; end diff --git a/app/views/conference_registrations/show.html.haml b/app/views/conference_registrations/show.html.haml index 30cb281b..24923d07 100644 --- a/app/views/conference_registrations/show.html.haml +++ b/app/views/conference_registrations/show.html.haml @@ -89,29 +89,27 @@ %span.fa-stack %i.fa.fa-square-o.fa-stack-2x %i.fa.fa-ticket.fa-stack-1x - Tickets + Ticket Purchases -if @tickets.any? - = "(#{@total_price} #{@tickets.first.price.symbol})" + = "(#{@tickets.first.price.symbol}#{humanized_money @total_price})" %ul - - @tickets.each do |ticket| - %li - = ticket.quantity - = ticket.title - = word_pluralize(ticket.quantity, 'Ticket') - for - = humanized_money ticket.price - = ticket.price.symbol - = link_to conference_ticket_purchase_path(@conference.short_title, ticket.id), method: :delete, - id: "ticket-#{ticket.id}-delete", - class: 'btn btn-danger btn-xs', - data: { confirm: "Do you really want to delete the #{ticket.title} ticket for #{@conference.title}?" } do - %i.fa.fa-trash-o - %li - - if @tickets.any? - = link_to 'Buy more tickets', conference_tickets_path(@conference.short_title) - - else - You haven't bought any tickets. - = link_to 'Please buy some tickets to support us!', conference_tickets_path(@conference.short_title) + .col-md-12 + - @ticket_payments.each_pair do |ticket_id, tickets| + %li + = @total_quantity[ticket_id] + = tickets.first.title + = word_pluralize(@total_quantity[ticket_id], 'Ticket') + for + = tickets.first.price.symbol + = humanized_money tickets.first.price + %br + - if @tickets.any? + = link_to 'Buy more tickets', conference_tickets_path(@conference.short_title), class: "btn btn-default" + - else + You haven't bought any tickets. + = link_to 'Please buy some tickets to support us!', conference_tickets_path(@conference.short_title) + %p + (Your registration won't be complete without buying a ticket) .row .col-md-12 From 1894cea9752736d049401a4dc4988c9fe29ed8fa Mon Sep 17 00:00:00 2001 From: Rishabh Saxena Date: Wed, 27 Jul 2016 19:40:21 +0530 Subject: [PATCH 05/31] modify ticket purchases view --- app/controllers/ticket_purchases_controller.rb | 18 +++--------------- app/views/tickets/_ticket.html.haml | 6 +----- app/views/tickets/index.html.haml | 11 +++++------ 3 files changed, 9 insertions(+), 26 deletions(-) diff --git a/app/controllers/ticket_purchases_controller.rb b/app/controllers/ticket_purchases_controller.rb index 30d24d82..43f6d09a 100644 --- a/app/controllers/ticket_purchases_controller.rb +++ b/app/controllers/ticket_purchases_controller.rb @@ -4,11 +4,11 @@ class TicketPurchasesController < ApplicationController authorize_resource :conference_registrations, class: Registration def create + TicketPurchase.by_conference(@conference).unpaid.by_user(current_user).destroy_all message = TicketPurchase.purchase(@conference, current_user, params[:tickets][0]) if message.blank? - if current_user.ticket_purchases.any? - redirect_to conference_conference_registration_path(@conference.short_title), - notice: "Thank you for supporting #{@conference.title} by purchasing a ticket." + if current_user.ticket_purchases.by_conference(@conference).unpaid.any? + redirect_to new_conference_payment_path, notice: 'Please pay here to purchase tickets.' else redirect_to conference_conference_registration_path(@conference.short_title) end @@ -18,18 +18,6 @@ class TicketPurchasesController < ApplicationController end end - def destroy - @ticket_purchases = current_user.ticket_purchases.find(params[:id]) - if @ticket_purchases.destroy - redirect_to conference_conference_registration_path(@conference.short_title), - notice: 'Ticket successfully deleted.' - else - redirect_to conference_conference_registration_path(@conference.short_title), - error: 'An error prohibited deleting your purchase! '\ - "#{@ticket_purchases.errors.full_messages.join('. ')}." - end - end - private def ticket_purchase_params diff --git a/app/views/tickets/_ticket.html.haml b/app/views/tickets/_ticket.html.haml index 1a93f46b..e3985773 100644 --- a/app/views/tickets/_ticket.html.haml +++ b/app/views/tickets/_ticket.html.haml @@ -8,12 +8,8 @@ - unless ticket.description.blank? = markdown(ticket.description) %td.col-sm-1.col-md-1 - - if ticket.bought?(current_user) - = text_field_tag("tickets[][#{ticket.id}]", ticket.quantity_bought_by(current_user), + = text_field_tag("tickets[][#{ticket.id}]", 0, type: 'number', min: 0, class: "form-control quantity", 'data-id' => ticket.id) - - else - = text_field_tag("tickets[][#{ticket.id}]", 0, type: 'number', min: 0, - class: "form-control quantity", 'data-id' => ticket.id) %td.col-sm-1.col-md-1.text-center = ticket.price.symbol %span{id: "price_#{ticket.id}"} diff --git a/app/views/tickets/index.html.haml b/app/views/tickets/index.html.haml index fbc2df7c..8f80e1ac 100644 --- a/app/views/tickets/index.html.haml +++ b/app/views/tickets/index.html.haml @@ -5,10 +5,10 @@ %h1 Tickets %p.lead - If you like, support + Please choose your tickets for %strong = @conference.title - by buying a ticket* + here* =form_tag(conference_ticket_purchases_path, method: :post) do |f| %table.table.table-hover %thead @@ -35,12 +35,11 @@ .pull-right .btn-group-vertical = button_tag(type: 'submit', class: 'btn btn-success btn-lg') do - Support + Continue %i.fa.fa-shopping-cart - = link_to 'Continue without a Ticket!', conference_conference_registration_path(@conference.short_title), - class: 'btn btn-danger btn-sm' + = link_to 'Cancel registration', conference_conference_registration_path(@conference.short_title), method: :delete, class: 'btn btn-danger btn-sm' .row .col-md-13 %p.text-muted.text-center %small - * Buying a ticket is not mandatory. Checkout will be at the conference registration. + * Buying a ticket is mandatory. Your registration will not complete until you buy a ticket. From e2d42fbbbbdd0ef6bc8716f29547d34e1e65c331 Mon Sep 17 00:00:00 2001 From: Rishabh Saxena Date: Wed, 27 Jul 2016 19:41:42 +0530 Subject: [PATCH 06/31] modify ticket and ticket_purchases models --- app/models/ticket.rb | 19 +++++++++++-------- app/models/ticket_purchase.rb | 14 ++++++++------ 2 files changed, 19 insertions(+), 14 deletions(-) diff --git a/app/models/ticket.rb b/app/models/ticket.rb index 54f5c3d7..d2454b74 100644 --- a/app/models/ticket.rb +++ b/app/models/ticket.rb @@ -18,24 +18,27 @@ class Ticket < ActiveRecord::Base end def paid?(user) - ticket_purchases.find_by(user: user, paid: true).present? + ticket_purchases.paid.by_user(user).present? end - def quantity_bought_by(user) - result = ticket_purchases.where(user_id: user.id).first - result ? result.quantity : 0 + def quantity_bought_by(user, paid: false) + ticket_purchases.by_user(user).where(paid: paid).sum(:quantity) end - def total_price(user) - quantity_bought_by(user) * price + def unpaid?(user) + ticket_purchases.unpaid.by_user(user).present? end - def self.total_price(conference, user) + def total_price(user, paid: false) + quantity_bought_by(user, paid: paid) * price + end + + def self.total_price(conference, user, paid: false) tickets = Ticket.where(conference_id: conference.id) result = nil begin tickets.each do |ticket| - price = ticket.total_price(user) + price = ticket.total_price(user, paid: paid) if result result += price unless price.zero? else diff --git a/app/models/ticket_purchase.rb b/app/models/ticket_purchase.rb index 8b0cd555..b7a8951f 100644 --- a/app/models/ticket_purchase.rb +++ b/app/models/ticket_purchase.rb @@ -7,23 +7,24 @@ class TicketPurchase < ActiveRecord::Base validates_numericality_of :quantity, greater_than: 0 - validates_uniqueness_of :user_id, - scope: :ticket_id, - message: 'already bought this ticket!' - delegate :title, to: :ticket delegate :description, to: :ticket delegate :price, to: :ticket delegate :price_cents, to: :ticket delegate :price_currency, to: :ticket + scope :paid, -> { where(paid: true) } + scope :unpaid, -> { where(paid: false) } + scope :by_conference, -> (conference) { where(conference_id: conference.id) } + scope :by_user, -> (user) { where(user_id: user.id) } + def self.purchase(conference, user, purchases) errors = [] ActiveRecord::Base.transaction do conference.tickets.each do |ticket| quantity = purchases[ticket.id.to_s].to_i # if the user bought the ticket, just update the quantity - if ticket.bought?(user) + if ticket.bought?(user) && ticket.unpaid?(user) purchase = update_quantity(conference, quantity, ticket, user) else purchase = purchase_ticket(conference, quantity, ticket, user) @@ -48,7 +49,8 @@ class TicketPurchase < ActiveRecord::Base def self.update_quantity(conference, quantity, ticket, user) purchase = TicketPurchase.where(ticket_id: ticket.id, conference_id: conference.id, - user_id: user.id).first + user_id: user.id, + paid: false).first purchase.quantity = quantity if quantity > 0 purchase From ab262ecb4626c5685fe8237d1d06b5253208ae9f Mon Sep 17 00:00:00 2001 From: Rishabh Saxena Date: Wed, 27 Jul 2016 19:42:42 +0530 Subject: [PATCH 07/31] add payment views and styling --- app/assets/stylesheets/application.css | 1 + app/assets/stylesheets/osem-payments.css.scss | 25 +++++++++++++ app/views/payments/_payment.html.haml | 35 +++++++++++++++++++ app/views/payments/new.html.haml | 18 ++++++++++ 4 files changed, 79 insertions(+) create mode 100644 app/assets/stylesheets/osem-payments.css.scss create mode 100644 app/views/payments/_payment.html.haml create mode 100644 app/views/payments/new.html.haml diff --git a/app/assets/stylesheets/application.css b/app/assets/stylesheets/application.css index d0447354..ac01136f 100644 --- a/app/assets/stylesheets/application.css +++ b/app/assets/stylesheets/application.css @@ -14,4 +14,5 @@ *= require bootstrap-datetimepicker *= require leaflet *= require bootstrap3-switch + *= require osem-payments */ diff --git a/app/assets/stylesheets/osem-payments.css.scss b/app/assets/stylesheets/osem-payments.css.scss new file mode 100644 index 00000000..e0a05e8c --- /dev/null +++ b/app/assets/stylesheets/osem-payments.css.scss @@ -0,0 +1,25 @@ +.price-tags { + list-style-type: none; +} +.price-tags li { + line-height: 40px; + position: relative; + margin-right: -3rem; +} +.price-tags a { + background: #2f991d; + color: #fff; + font-size: 1.5rem; + padding: 9px 10px; + text-decoration: none; +} +.price-tags a:after { + content: ""; + float: left; + border-top: 20px solid transparent; + border-right: 20px solid #2f991d; + border-bottom: 20px solid transparent; +} +.stripe-button-el { + float: right; +} diff --git a/app/views/payments/_payment.html.haml b/app/views/payments/_payment.html.haml new file mode 100644 index 00000000..3b5de84a --- /dev/null +++ b/app/views/payments/_payment.html.haml @@ -0,0 +1,35 @@ +.div + + .col-md-12.table-responsive + %table.table.table-hover + %thead + %tr + %th Ticket + %th Quantity + %th Price + %th Total + %tbody + - @unpaid_ticket_purchases.each do |ticket| + %tr + %td + = ticket.title + %td + = ticket.quantity + %td + = humanized_money_with_symbol ticket.price + %td + = humanized_money_with_symbol ticket.quantity * ticket.price + += form_tag conference_payments_path do + %ul.price-tags.pull-right + %li + %a + = humanized_money_with_symbol @total_amount_to_pay + %script.stripe-button{"data-amount" => @total_amount_to_pay.cents, + "data-currency" => @total_amount_to_pay.currency, + "data-image" => image_url('suse.svg'), + "data-name" => ENV['OSEM_NAME'] || 'OSEM', + "data-description" => "book your tickets", + "data-key" => "#{Rails.configuration.stripe[:publishable_key]}", + "data-locale" => "auto", :src => "https://checkout.stripe.com/checkout.js"} + = link_to 'Edit Purchase', conference_tickets_path(@conference.short_title), class: 'btn btn-primary' diff --git a/app/views/payments/new.html.haml b/app/views/payments/new.html.haml new file mode 100644 index 00000000..1817f32c --- /dev/null +++ b/app/views/payments/new.html.haml @@ -0,0 +1,18 @@ +.container + .row + .col-xs-6.col-xs-offset-3 + %h1 + Payment Summary : + = humanized_money_with_symbol @total_amount_to_pay + - if @payment.errors.any? + .alert.alert-danger + %ul + - @payment.errors.full_messages.each do |msg| + %li= msg + .col-xs-8.col-xs-offset-2.well + = render partial: 'payment' + .row + .col-md-13 + %p.text-muted.text-center + %small + The payment is totally secure. Your credit card details will be sent directly to our payment processor. From a5bb038f618c377367ec486e323db325917e9222 Mon Sep 17 00:00:00 2001 From: Rishabh Saxena Date: Wed, 27 Jul 2016 19:44:32 +0530 Subject: [PATCH 08/31] modify tests for changed payment flow --- ...conference_registration_controller_spec.rb | 5 +- spec/features/ticket_purchases_spec.rb | 20 +---- spec/models/ticket_spec.rb | 81 ++++++++++++++----- 3 files changed, 66 insertions(+), 40 deletions(-) diff --git a/spec/controllers/conference_registration_controller_spec.rb b/spec/controllers/conference_registration_controller_spec.rb index f0087e80..377e884e 100644 --- a/spec/controllers/conference_registration_controller_spec.rb +++ b/spec/controllers/conference_registration_controller_spec.rb @@ -82,9 +82,8 @@ describe ConferenceRegistrationsController, type: :controller do get :show, conference_id: conference.short_title end - it 'assigns price of purchased tickets to total_price and purchased tickets to tickets' do - expect(assigns(:total_price)).to eq Money.new(10000, 'USD') - expect(assigns(:tickets)).to match_array [@purchased_ticket] + it 'does not assign price of purchased tickets to total_price and purchased tickets to tickets without payment' do + expect(assigns(:total_price)).to eq Money.new(0, 'USD') end end diff --git a/spec/features/ticket_purchases_spec.rb b/spec/features/ticket_purchases_spec.rb index ff6957f6..3d42473f 100644 --- a/spec/features/ticket_purchases_spec.rb +++ b/spec/features/ticket_purchases_spec.rb @@ -26,26 +26,12 @@ feature Registration do fill_in "tickets__#{ticket.id}", with: '2' expect(current_path).to eq(conference_tickets_path(conference.short_title)) - click_button 'Support' + click_button 'Continue' + expect(current_path).to eq(new_conference_payment_path(conference.short_title)) + expect(flash).to eq('Please pay here to purchase tickets.') purchase = TicketPurchase.where(user_id: participant.id, ticket_id: ticket.id).first expect(purchase.quantity).to eq(2) - expect(current_path).to eq(conference_conference_registration_path(conference.short_title)) - expect(flash). - to eq("Thank you for supporting #{conference.title} by purchasing a ticket.") - expect(page.has_content?("2 #{ticket.title} Tickets for 10")).to be true - end - - scenario 'deletes a purchased ticket', feature: true, js: true do - create(:registration, conference: conference, user: participant) - create(:ticket_purchase, conference: conference, user: participant, ticket: ticket, quantity: 4) - - visit conference_conference_registration_path(conference.short_title) - expect(page.has_content?("4 #{ticket.title} Tickets for 10")).to be true - - click_link "ticket-#{ticket.id}-delete" - expect(flash).to eq('Ticket successfully deleted.') - expect(TicketPurchase.count).to eq(0) end end end diff --git a/spec/models/ticket_spec.rb b/spec/models/ticket_spec.rb index fc67ff3d..631ca59a 100644 --- a/spec/models/ticket_spec.rb +++ b/spec/models/ticket_spec.rb @@ -82,31 +82,72 @@ describe Ticket do end end - describe '#quantity_bought_by' do - it 'returns the correct value if the user has bought this ticket' do - create(:ticket_purchase, - user: user, - ticket: ticket, - quantity: 20) - expect(ticket.quantity_bought_by(user)).to eq(20) + describe '#unpaid?' do + let!(:ticket_purchase) { create(:ticket_purchase, user: user, ticket: ticket) } + + context 'user has not paid' do + + it 'returns true' do + expect(ticket.unpaid?(user)).to eq(true) + end end - it 'returns zero if the user has not bought this ticket' do - expect(ticket.quantity_bought_by(user)).to eq(0) + context 'user has paid' do + before { ticket_purchase.update_attributes(paid: true) } + + it 'returns false' do + expect(ticket.unpaid?(user)).to eq(false) + end + end + end + + describe '#quantity_bought_by' do + context 'user has not paid' do + it 'returns the correct value if the user has bought this ticket' do + create(:ticket_purchase, + user: user, + ticket: ticket, + quantity: 20) + expect(ticket.quantity_bought_by(user, paid: false)).to eq(20) + end + + it 'returns zero if the user has not bought this ticket' do + expect(ticket.quantity_bought_by(user, paid: false)).to eq(0) + end + end + + context 'user has paid' do + let!(:ticket_purchase) { create(:ticket_purchase, user: user, ticket: ticket, quantity: 20) } + before { ticket_purchase.update_attributes(paid: true) } + + it 'returns the correct value if the user has bought and paid for this ticket' do + expect(ticket.quantity_bought_by(user, paid: true)).to eq(20) + end end end describe '#total_price' do - it 'returns the correct value if the user has bought this ticket' do - create(:ticket_purchase, - user: user, - ticket: ticket, - quantity: 20) - expect(ticket.total_price(user)).to eq(Money.new(100000, 'USD')) + context 'user has not paid' do + it 'returns the correct value if the user has bought this ticket' do + create(:ticket_purchase, + user: user, + ticket: ticket, + quantity: 20) + expect(ticket.total_price(user, paid: false)).to eq(Money.new(100000, 'USD')) + end + + it 'returns zero if the user has not bought this ticket' do + expect(ticket.total_price(user, paid: false)).to eq(Money.new(0, 'USD')) + end end - it 'returns zero if the user has not bought this ticket' do - expect(ticket.total_price(user)).to eq(Money.new(0, 'USD')) + context 'user has paid' do + let!(:ticket_purchase) { create(:ticket_purchase, user: user, ticket: ticket, quantity: 20) } + before { ticket_purchase.update_attributes(paid: true) } + + it 'returns the correct value if the user has bought this ticket' do + expect(ticket.total_price(user, paid: true)).to eq(Money.new(100000, 'USD')) + end end end @@ -116,7 +157,7 @@ describe Ticket do describe 'user has bought' do context 'no tickets' do it 'returns zero' do - expect(Ticket.total_price(conference, user)).to eq(Money.new(0, 'USD')) + expect(Ticket.total_price(conference, user, paid: false)).to eq(Money.new(0, 'USD')) end end @@ -126,7 +167,7 @@ describe Ticket do end it 'returns the correct total price' do - expect(Ticket.total_price(conference, user)).to eq(Money.new(100000, 'USD')) + expect(Ticket.total_price(conference, user, paid: false)).to eq(Money.new(100000, 'USD')) end end @@ -138,7 +179,7 @@ describe Ticket do it 'returns the correct total price' do total_price = Money.new(200000, 'USD') - expect(Ticket.total_price(conference, user)).to eq(total_price) + expect(Ticket.total_price(conference, user, paid: false)).to eq(total_price) end end end From 7b23018a0678b73d873b1fc4f17679aa3de6c349 Mon Sep 17 00:00:00 2001 From: Rishabh Saxena Date: Wed, 27 Jul 2016 19:44:55 +0530 Subject: [PATCH 09/31] add payment tests --- spec/factories/payments.rb | 9 ++++++++ spec/models/payment_spec.rb | 44 +++++++++++++++++++++++++++++++++++++ 2 files changed, 53 insertions(+) create mode 100644 spec/factories/payments.rb create mode 100644 spec/models/payment_spec.rb diff --git a/spec/factories/payments.rb b/spec/factories/payments.rb new file mode 100644 index 00000000..340d04d5 --- /dev/null +++ b/spec/factories/payments.rb @@ -0,0 +1,9 @@ +FactoryGirl.define do + factory :payment do + user + conference + last4 4242 + authorization_code 1234567890 + amount 10 + end +end diff --git a/spec/models/payment_spec.rb b/spec/models/payment_spec.rb new file mode 100644 index 00000000..3950e269 --- /dev/null +++ b/spec/models/payment_spec.rb @@ -0,0 +1,44 @@ +require 'spec_helper' + +describe Payment do + + context 'new payment' do + let(:payment) { create(:payment) } + it 'sets status to "unpaid" by default' do + expect(payment.status).to eq('unpaid') + end + end + + describe 'validations' do + it 'has a valid factory' do + expect(build(:payment)).to be_valid + end + + it { is_expected.to validate_presence_of(:last4) } + + it { is_expected.to validate_presence_of(:amount) } + + it { is_expected.to validate_presence_of(:authorization_code) } + + it { is_expected.to validate_presence_of(:status) } + + it { is_expected.to validate_presence_of(:user_id) } + + it { is_expected.to validate_presence_of(:conference_id) } + + it 'is not valid with a amount equals zero' do + should_not allow_value(0).for(:amount) + end + + it 'is not valid with a amount smaller than zero' do + should_not allow_value(-1).for(:amount) + end + + it 'is valid with a amount greater than zero' do + should allow_value(1).for(:amount) + end + + end + + describe 'self#purchase' +end From 47e16e5653fa5733d6651725b7e7833d32333ebc Mon Sep 17 00:00:00 2001 From: Rishabh Saxena Date: Wed, 27 Jul 2016 20:23:03 +0530 Subject: [PATCH 10/31] add stripe gateway config --- config/initializers/stripe.rb | 6 ++++++ config/secrets.yml.example | 10 ++++++++++ dotenv.example | 5 +++++ 3 files changed, 21 insertions(+) create mode 100644 config/initializers/stripe.rb diff --git a/config/initializers/stripe.rb b/config/initializers/stripe.rb new file mode 100644 index 00000000..22ff8359 --- /dev/null +++ b/config/initializers/stripe.rb @@ -0,0 +1,6 @@ +Rails.configuration.stripe = { + :publishable_key => ENV['STRIPE_PUBLISHABLE_KEY'] || Rails.application.secrets.stripe_publishable_key, + :secret_key => ENV['STRIPE_SECRET_KEY'] || Rails.application.secrets.stripe_secret_key +} + +Stripe.api_key = Rails.configuration.stripe[:secret_key] diff --git a/config/secrets.yml.example b/config/secrets.yml.example index 308bc858..8603a189 100644 --- a/config/secrets.yml.example +++ b/config/secrets.yml.example @@ -13,6 +13,11 @@ development: suse_key: 'sample' suse_secret: 'sample' + # Register on stripe and add TEST keys here + # https://dashboard.stripe.com/account/apikeys + stripe_publishable_key: '' + stripe_secret_key: '' + test: # Generate your own with rake secret # secret_key_base: '12345' @@ -56,3 +61,8 @@ production: # https://github.com/settings/applications github_key: '' github_secret: '' + + # Register on stripe and add LIVE keys here + # https://dashboard.stripe.com/account/apikeys + stripe_publishable_key: '' + stripe_secret_key: '' diff --git a/dotenv.example b/dotenv.example index 921020d1..ec2715ea 100644 --- a/dotenv.example +++ b/dotenv.example @@ -37,6 +37,11 @@ OSEM_FACEBOOK_SECRET='' OSEM_GITHUB_KEY='' OSEM_GITHUB_SECRET='' +# STRIPE Publishable/Secret keys +# test keys for development mode, live for production mode +STRIPE_PUBLISHABLE_KEY='' +STRIPE_SECRET_KEY='' + # Disable linting of factories in the test suite. # Speeds up turn around times of tests OSEM_FACTORY_LINT="false" From be6d046154dac7eaa682a1d9b9a007456404f4bd Mon Sep 17 00:00:00 2001 From: Rishabh Saxena Date: Thu, 28 Jul 2016 15:27:30 +0530 Subject: [PATCH 11/31] move stripe API call to model, schema changes --- app/controllers/payments_controller.rb | 35 ++++++++------------ app/models/payment.rb | 34 +++++++++++++++---- db/migrate/20160606040848_create_payments.rb | 6 ++-- db/schema.rb | 6 ++-- 4 files changed, 46 insertions(+), 35 deletions(-) diff --git a/app/controllers/payments_controller.rb b/app/controllers/payments_controller.rb index 161d2101..aa0c038a 100644 --- a/app/controllers/payments_controller.rb +++ b/app/controllers/payments_controller.rb @@ -17,32 +17,23 @@ class PaymentsController < ApplicationController @unpaid_ticket_purchases = current_user.ticket_purchases.unpaid.by_conference(@conference) @total_amount_to_pay = Ticket.total_price(@conference, current_user, paid: false) - customer = Stripe::Customer.create( - :email => params[:stripeEmail], - :source => params[:stripeToken] - ) + @payment = Payment.new payment_params.merge(user: current_user, conference: @conference) + @payment.purchase + @payment.save - gateway_response = Stripe::Charge.create( - :customer => customer.id, - :amount => @total_amount_to_pay.cents, - :description => 'Rails Stripe customer', - :currency => @conference.tickets.first.price_currency - ) + update_purchased_ticket_purchases - payment = Payment.purchase(gateway_response, current_user, @conference) - update_purchased_ticket_purchases(payment) - - redirect_to conference_conference_registration_path(@conference.short_title), - flash: { success: 'Thanks! You have purchased your tickets successfully.' } - - rescue Stripe::CardError => e - flash[:error] = e.message - render 'new' + redirect_to conference_conference_registration_path(@conference.short_title), flash: + { success: 'Thanks! You have purchased your tickets successfully.' } end -private + private - def update_purchased_ticket_purchases(payment) - current_user.ticket_purchases.by_conference(@conference).unpaid.update_all(paid: true, payment_id: payment.id) + def payment_params + params.permit :stripeEmail, :stripeToken + end + + def update_purchased_ticket_purchases + current_user.ticket_purchases.by_conference(@conference).unpaid.update_all(paid: true, payment_id: @payment.id) end end diff --git a/app/models/payment.rb b/app/models/payment.rb index ecc4db5d..95ed9a70 100644 --- a/app/models/payment.rb +++ b/app/models/payment.rb @@ -3,6 +3,9 @@ class Payment < ActiveRecord::Base belongs_to :user belongs_to :conference + attr_accessor :stripeEmail + attr_accessor :stripeToken + validates :last4, presence: true validates :authorization_code, presence: true validates :status, presence: true @@ -16,12 +19,29 @@ class Payment < ActiveRecord::Base failure: 2 } - def self.purchase(gateway_response, user, conference) - create(last4: gateway_response[:source][:last4], - amount: gateway_response[:amount], - status: (gateway_response[:paid] ? 1 : 0), - authorization_code: gateway_response[:id], - user_id: user.id, - conference_id: conference.id) + def amount_to_pay + Ticket.total_price(conference, user, paid: false).cents + end + + def purchase + customer = Stripe::Customer.create email: stripeEmail, + source: stripeToken, + description: user.name + + gateway_response = Stripe::Charge.create customer: customer.id, + receipt_email: stripeEmail, + description: 'ticket purchases', + amount: amount_to_pay, + currency: conference.tickets.first.price_currency + + self.amount = gateway_response[:amount] + self.last4 = gateway_response[:source][:last4] + self.authorization_code = gateway_response[:id] + self.status = 'success' + true + + rescue Stripe::CardError => e + flash[:error] = e.message + false end end diff --git a/db/migrate/20160606040848_create_payments.rb b/db/migrate/20160606040848_create_payments.rb index b7428ca3..6792ad02 100644 --- a/db/migrate/20160606040848_create_payments.rb +++ b/db/migrate/20160606040848_create_payments.rb @@ -1,9 +1,9 @@ class CreatePayments < ActiveRecord::Migration def change create_table :payments do |t| - t.string :last4, null: false - t.integer :amount, null: false - t.string :authorization_code, null: false + t.string :last4 + t.integer :amount + t.string :authorization_code t.integer :status, default: 0, null: false t.integer :user_id, null: false t.integer :conference_id, null: false diff --git a/db/schema.rb b/db/schema.rb index 836168d8..6213090c 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -264,9 +264,9 @@ ActiveRecord::Schema.define(version: 20160704092023) do end create_table "payments", force: :cascade do |t| - t.string "last4", null: false - t.integer "amount", null: false - t.string "authorization_code", null: false + t.string "last4" + t.integer "amount" + t.string "authorization_code" t.integer "status", default: 0, null: false t.integer "user_id", null: false t.integer "conference_id", null: false From 52c30af6425b56f5c1bc7cbddf5ba85ac439e0b0 Mon Sep 17 00:00:00 2001 From: Rishabh Saxena Date: Thu, 28 Jul 2016 15:28:43 +0530 Subject: [PATCH 12/31] generalise application icon for Stripe iFrame --- app/assets/images/OSEM_ICON.jpg | Bin 0 -> 22855 bytes app/views/payments/_payment.html.haml | 15 ++++++++------- spec/factories/payments.rb | 4 ++-- 3 files changed, 10 insertions(+), 9 deletions(-) create mode 100644 app/assets/images/OSEM_ICON.jpg diff --git a/app/assets/images/OSEM_ICON.jpg b/app/assets/images/OSEM_ICON.jpg new file mode 100644 index 0000000000000000000000000000000000000000..6a52c3b0e2378b6575023f691b6f9ed0e8b5abac GIT binary patch literal 22855 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 Y4dbQClVxI@$6kZHdGG(1Pxx*0A1vXuj{pDw literal 0 HcmV?d00001 diff --git a/app/views/payments/_payment.html.haml b/app/views/payments/_payment.html.haml index 3b5de84a..93692b9f 100644 --- a/app/views/payments/_payment.html.haml +++ b/app/views/payments/_payment.html.haml @@ -25,11 +25,12 @@ %li %a = humanized_money_with_symbol @total_amount_to_pay - %script.stripe-button{"data-amount" => @total_amount_to_pay.cents, - "data-currency" => @total_amount_to_pay.currency, - "data-image" => image_url('suse.svg'), - "data-name" => ENV['OSEM_NAME'] || 'OSEM', - "data-description" => "book your tickets", - "data-key" => "#{Rails.configuration.stripe[:publishable_key]}", - "data-locale" => "auto", :src => "https://checkout.stripe.com/checkout.js"} + %script.stripe-button{ src: "https://checkout.stripe.com/checkout.js", + data: { amount: @total_amount_to_pay.cents, + currency: @total_amount_to_pay.currency, + image: image_url('OSEM_ICON.jpg'), + name: ENV['OSEM_NAME'] || 'OSEM', + description: "book your tickets", + key: Rails.application.secrets.stripe_publishable_key, + locale: "auto"}} = link_to 'Edit Purchase', conference_tickets_path(@conference.short_title), class: 'btn btn-primary' diff --git a/spec/factories/payments.rb b/spec/factories/payments.rb index 340d04d5..3c718436 100644 --- a/spec/factories/payments.rb +++ b/spec/factories/payments.rb @@ -2,8 +2,8 @@ FactoryGirl.define do factory :payment do user conference - last4 4242 - authorization_code 1234567890 + last4 '4242' + authorization_code '1234567890' amount 10 end end From 1f9338735c513f0bf52bd77c614f021f46af0b12 Mon Sep 17 00:00:00 2001 From: Rishabh Saxena Date: Thu, 28 Jul 2016 20:42:04 +0530 Subject: [PATCH 13/31] improve Stripe error rescue. modify tests. --- app/controllers/payments_controller.rb | 17 +++++---- app/models/payment.rb | 36 ++++++++++---------- db/migrate/20160606040848_create_payments.rb | 2 +- db/schema.rb | 2 +- spec/factories/payments.rb | 5 ++- spec/models/payment_spec.rb | 17 +++++---- 6 files changed, 43 insertions(+), 36 deletions(-) diff --git a/app/controllers/payments_controller.rb b/app/controllers/payments_controller.rb index aa0c038a..86f646f6 100644 --- a/app/controllers/payments_controller.rb +++ b/app/controllers/payments_controller.rb @@ -17,14 +17,17 @@ class PaymentsController < ApplicationController @unpaid_ticket_purchases = current_user.ticket_purchases.unpaid.by_conference(@conference) @total_amount_to_pay = Ticket.total_price(@conference, current_user, paid: false) - @payment = Payment.new payment_params.merge(user: current_user, conference: @conference) - @payment.purchase - @payment.save + @payment = Payment.new payment_params.merge(amount: @total_amount_to_pay.cents, + user: current_user, + conference: @conference) - update_purchased_ticket_purchases - - redirect_to conference_conference_registration_path(@conference.short_title), flash: - { success: 'Thanks! You have purchased your tickets successfully.' } + if @payment.purchase && @payment.save + update_purchased_ticket_purchases + redirect_to conference_conference_registration_path(@conference.short_title), flash: + { success: 'Thanks! You have purchased your tickets successfully.' } + else + render :new + end end private diff --git a/app/models/payment.rb b/app/models/payment.rb index 95ed9a70..52df4071 100644 --- a/app/models/payment.rb +++ b/app/models/payment.rb @@ -6,8 +6,6 @@ class Payment < ActiveRecord::Base attr_accessor :stripeEmail attr_accessor :stripeToken - validates :last4, presence: true - validates :authorization_code, presence: true validates :status, presence: true validates :amount, presence: true, numericality: { greater_than: 0 } validates :user_id, presence: true @@ -24,24 +22,26 @@ class Payment < ActiveRecord::Base end def purchase - customer = Stripe::Customer.create email: stripeEmail, - source: stripeToken, - description: user.name + begin + customer = Stripe::Customer.create email: stripeEmail, + source: stripeToken, + description: user.name - gateway_response = Stripe::Charge.create customer: customer.id, - receipt_email: stripeEmail, - description: 'ticket purchases', - amount: amount_to_pay, - currency: conference.tickets.first.price_currency + gateway_response = Stripe::Charge.create customer: customer.id, + receipt_email: stripeEmail, + description: 'ticket purchases', + amount: amount_to_pay, + currency: conference.tickets.first.price_currency - self.amount = gateway_response[:amount] - self.last4 = gateway_response[:source][:last4] - self.authorization_code = gateway_response[:id] - self.status = 'success' - true + self.last4 = gateway_response[:source][:last4] + self.authorization_code = gateway_response[:id] + self.status = 'success' + true - rescue Stripe::CardError => e - flash[:error] = e.message - false + rescue => error + errors.add(:base, error.message) + self.status = 'failure' + false + end end end diff --git a/db/migrate/20160606040848_create_payments.rb b/db/migrate/20160606040848_create_payments.rb index 6792ad02..6c56bb5d 100644 --- a/db/migrate/20160606040848_create_payments.rb +++ b/db/migrate/20160606040848_create_payments.rb @@ -2,7 +2,7 @@ class CreatePayments < ActiveRecord::Migration def change create_table :payments do |t| t.string :last4 - t.integer :amount + t.integer :amount, null: false t.string :authorization_code t.integer :status, default: 0, null: false t.integer :user_id, null: false diff --git a/db/schema.rb b/db/schema.rb index 6213090c..6c3ceaef 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -265,7 +265,7 @@ ActiveRecord::Schema.define(version: 20160704092023) do create_table "payments", force: :cascade do |t| t.string "last4" - t.integer "amount" + t.integer "amount", null: false t.string "authorization_code" t.integer "status", default: 0, null: false t.integer "user_id", null: false diff --git a/spec/factories/payments.rb b/spec/factories/payments.rb index 3c718436..f0829c5c 100644 --- a/spec/factories/payments.rb +++ b/spec/factories/payments.rb @@ -2,8 +2,7 @@ FactoryGirl.define do factory :payment do user conference - last4 '4242' - authorization_code '1234567890' - amount 10 + status 'unpaid' + amount 1000 end end diff --git a/spec/models/payment_spec.rb b/spec/models/payment_spec.rb index 3950e269..e16a8f67 100644 --- a/spec/models/payment_spec.rb +++ b/spec/models/payment_spec.rb @@ -14,12 +14,8 @@ describe Payment do expect(build(:payment)).to be_valid end - it { is_expected.to validate_presence_of(:last4) } - it { is_expected.to validate_presence_of(:amount) } - it { is_expected.to validate_presence_of(:authorization_code) } - it { is_expected.to validate_presence_of(:status) } it { is_expected.to validate_presence_of(:user_id) } @@ -37,8 +33,17 @@ describe Payment do it 'is valid with a amount greater than zero' do should allow_value(1).for(:amount) end - end - describe 'self#purchase' + describe '#amount_to_pay' do + let!(:user) { create(:user) } + let!(:conference) { create(:conference) } + let(:ticket_1) { create(:ticket, price: 10, price_currency: 'USD', conference: conference) } + let(:payment) { create(:payment, user: user, conference: conference) } + + it ' returns correct unpaid amount' do + create(:ticket_purchase, ticket: ticket_1, user: user, quantity: 8) + expect(payment.amount_to_pay).to eq(8000) + end + end end From 112d2c6fff20ccbfa46daae9aaa6f625a9fe99c2 Mon Sep 17 00:00:00 2001 From: Rishabh Saxena Date: Fri, 29 Jul 2016 00:34:03 +0530 Subject: [PATCH 14/31] change camel case variable to ruby style. --- app/controllers/payments_controller.rb | 8 ++++-- app/models/payment.rb | 38 ++++++++++++-------------- app/views/payments/_payment.html.haml | 1 - 3 files changed, 23 insertions(+), 24 deletions(-) diff --git a/app/controllers/payments_controller.rb b/app/controllers/payments_controller.rb index 86f646f6..1a9cf150 100644 --- a/app/controllers/payments_controller.rb +++ b/app/controllers/payments_controller.rb @@ -14,10 +14,11 @@ class PaymentsController < ApplicationController end def create - @unpaid_ticket_purchases = current_user.ticket_purchases.unpaid.by_conference(@conference) @total_amount_to_pay = Ticket.total_price(@conference, current_user, paid: false) - @payment = Payment.new payment_params.merge(amount: @total_amount_to_pay.cents, + @payment = Payment.new payment_params.merge(stripe_customer_email: params[:stripeEmail], + stripe_customer_token: params[:stripeToken], + amount: @total_amount_to_pay.cents, user: current_user, conference: @conference) @@ -26,6 +27,7 @@ class PaymentsController < ApplicationController redirect_to conference_conference_registration_path(@conference.short_title), flash: { success: 'Thanks! You have purchased your tickets successfully.' } else + @unpaid_ticket_purchases = current_user.ticket_purchases.unpaid.by_conference(@conference) render :new end end @@ -33,7 +35,7 @@ class PaymentsController < ApplicationController private def payment_params - params.permit :stripeEmail, :stripeToken + params.permit :stripe_customer_email, :stripe_customer_token end def update_purchased_ticket_purchases diff --git a/app/models/payment.rb b/app/models/payment.rb index 52df4071..ef2792c3 100644 --- a/app/models/payment.rb +++ b/app/models/payment.rb @@ -3,8 +3,8 @@ class Payment < ActiveRecord::Base belongs_to :user belongs_to :conference - attr_accessor :stripeEmail - attr_accessor :stripeToken + attr_accessor :stripe_customer_email + attr_accessor :stripe_customer_token validates :status, presence: true validates :amount, presence: true, numericality: { greater_than: 0 } @@ -22,26 +22,24 @@ class Payment < ActiveRecord::Base end def purchase - begin - customer = Stripe::Customer.create email: stripeEmail, - source: stripeToken, - description: user.name + customer = Stripe::Customer.create email: stripe_customer_email, + source: stripe_customer_token, + description: user.name - gateway_response = Stripe::Charge.create customer: customer.id, - receipt_email: stripeEmail, - description: 'ticket purchases', - amount: amount_to_pay, - currency: conference.tickets.first.price_currency + gateway_response = Stripe::Charge.create customer: customer.id, + receipt_email: stripe_customer_email, + description: 'ticket purchases', + amount: amount_to_pay, + currency: conference.tickets.first.price_currency - self.last4 = gateway_response[:source][:last4] - self.authorization_code = gateway_response[:id] - self.status = 'success' - true + self.last4 = gateway_response[:source][:last4] + self.authorization_code = gateway_response[:id] + self.status = 'success' + true - rescue => error - errors.add(:base, error.message) - self.status = 'failure' - false - end + rescue => error + errors.add(:base, error.message) + self.status = 'failure' + false end end diff --git a/app/views/payments/_payment.html.haml b/app/views/payments/_payment.html.haml index 93692b9f..a8de023f 100644 --- a/app/views/payments/_payment.html.haml +++ b/app/views/payments/_payment.html.haml @@ -1,5 +1,4 @@ .div - .col-md-12.table-responsive %table.table.table-hover %thead From 72ef33cbe662c9c5dd138f659ad7f8c84898cd15 Mon Sep 17 00:00:00 2001 From: Rishabh Saxena Date: Thu, 4 Aug 2016 13:44:09 +0530 Subject: [PATCH 15/31] remove stripe#customer creation. remove amount validation. --- app/controllers/payments_controller.rb | 4 +--- app/models/payment.rb | 9 ++------- db/migrate/20160606040848_create_payments.rb | 2 +- db/schema.rb | 2 +- spec/models/payment_spec.rb | 14 -------------- 5 files changed, 5 insertions(+), 26 deletions(-) diff --git a/app/controllers/payments_controller.rb b/app/controllers/payments_controller.rb index 1a9cf150..121e0c4b 100644 --- a/app/controllers/payments_controller.rb +++ b/app/controllers/payments_controller.rb @@ -14,11 +14,8 @@ class PaymentsController < ApplicationController end def create - @total_amount_to_pay = Ticket.total_price(@conference, current_user, paid: false) - @payment = Payment.new payment_params.merge(stripe_customer_email: params[:stripeEmail], stripe_customer_token: params[:stripeToken], - amount: @total_amount_to_pay.cents, user: current_user, conference: @conference) @@ -27,6 +24,7 @@ class PaymentsController < ApplicationController redirect_to conference_conference_registration_path(@conference.short_title), flash: { success: 'Thanks! You have purchased your tickets successfully.' } else + @total_amount_to_pay = Ticket.total_price(@conference, current_user, paid: false) @unpaid_ticket_purchases = current_user.ticket_purchases.unpaid.by_conference(@conference) render :new end diff --git a/app/models/payment.rb b/app/models/payment.rb index ef2792c3..4ad4df85 100644 --- a/app/models/payment.rb +++ b/app/models/payment.rb @@ -7,7 +7,6 @@ class Payment < ActiveRecord::Base attr_accessor :stripe_customer_token validates :status, presence: true - validates :amount, presence: true, numericality: { greater_than: 0 } validates :user_id, presence: true validates :conference_id, presence: true @@ -22,13 +21,9 @@ class Payment < ActiveRecord::Base end def purchase - customer = Stripe::Customer.create email: stripe_customer_email, - source: stripe_customer_token, - description: user.name - - gateway_response = Stripe::Charge.create customer: customer.id, + gateway_response = Stripe::Charge.create source: stripe_customer_token, receipt_email: stripe_customer_email, - description: 'ticket purchases', + description: "ticket purchases(#{user.username})", amount: amount_to_pay, currency: conference.tickets.first.price_currency diff --git a/db/migrate/20160606040848_create_payments.rb b/db/migrate/20160606040848_create_payments.rb index 6c56bb5d..6792ad02 100644 --- a/db/migrate/20160606040848_create_payments.rb +++ b/db/migrate/20160606040848_create_payments.rb @@ -2,7 +2,7 @@ class CreatePayments < ActiveRecord::Migration def change create_table :payments do |t| t.string :last4 - t.integer :amount, null: false + t.integer :amount t.string :authorization_code t.integer :status, default: 0, null: false t.integer :user_id, null: false diff --git a/db/schema.rb b/db/schema.rb index 6c3ceaef..6213090c 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -265,7 +265,7 @@ ActiveRecord::Schema.define(version: 20160704092023) do create_table "payments", force: :cascade do |t| t.string "last4" - t.integer "amount", null: false + t.integer "amount" t.string "authorization_code" t.integer "status", default: 0, null: false t.integer "user_id", null: false diff --git a/spec/models/payment_spec.rb b/spec/models/payment_spec.rb index e16a8f67..4512fe80 100644 --- a/spec/models/payment_spec.rb +++ b/spec/models/payment_spec.rb @@ -14,25 +14,11 @@ describe Payment do expect(build(:payment)).to be_valid end - it { is_expected.to validate_presence_of(:amount) } - it { is_expected.to validate_presence_of(:status) } it { is_expected.to validate_presence_of(:user_id) } it { is_expected.to validate_presence_of(:conference_id) } - - it 'is not valid with a amount equals zero' do - should_not allow_value(0).for(:amount) - end - - it 'is not valid with a amount smaller than zero' do - should_not allow_value(-1).for(:amount) - end - - it 'is valid with a amount greater than zero' do - should allow_value(1).for(:amount) - end end describe '#amount_to_pay' do From a9ab1186f80f115f93b3e733580149e87adf0462 Mon Sep 17 00:00:00 2001 From: Rishabh Saxena Date: Fri, 5 Aug 2016 20:17:27 +0530 Subject: [PATCH 16/31] change rails initialisation. repair abilities. add ticket purchasing check. --- app/assets/images/OSEM_ICON.jpg | Bin 22855 -> 0 bytes app/controllers/payments_controller.rb | 4 ++-- app/controllers/ticket_purchases_controller.rb | 6 ++++-- app/models/ability.rb | 2 +- .../conference_registrations/show.html.haml | 2 +- app/views/payments/_payment.html.haml | 2 +- app/views/tickets/index.html.haml | 2 +- config/initializers/stripe.rb | 7 +------ spec/models/ability_spec.rb | 3 +++ 9 files changed, 14 insertions(+), 14 deletions(-) delete mode 100644 app/assets/images/OSEM_ICON.jpg diff --git a/app/assets/images/OSEM_ICON.jpg b/app/assets/images/OSEM_ICON.jpg deleted file mode 100644 index 6a52c3b0e2378b6575023f691b6f9ed0e8b5abac..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 22855 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 Y4dbQClVxI@$6kZHdGG(1Pxx*0A1vXuj{pDw diff --git a/app/controllers/payments_controller.rb b/app/controllers/payments_controller.rb index 121e0c4b..d637f1f5 100644 --- a/app/controllers/payments_controller.rb +++ b/app/controllers/payments_controller.rb @@ -21,8 +21,8 @@ class PaymentsController < ApplicationController if @payment.purchase && @payment.save update_purchased_ticket_purchases - redirect_to conference_conference_registration_path(@conference.short_title), flash: - { success: 'Thanks! You have purchased your tickets successfully.' } + redirect_to conference_conference_registration_path(@conference.short_title), + notice: 'Thanks! You have purchased your tickets successfully.' else @total_amount_to_pay = Ticket.total_price(@conference, current_user, paid: false) @unpaid_ticket_purchases = current_user.ticket_purchases.unpaid.by_conference(@conference) diff --git a/app/controllers/ticket_purchases_controller.rb b/app/controllers/ticket_purchases_controller.rb index 43f6d09a..4a334a08 100644 --- a/app/controllers/ticket_purchases_controller.rb +++ b/app/controllers/ticket_purchases_controller.rb @@ -8,9 +8,11 @@ class TicketPurchasesController < ApplicationController message = TicketPurchase.purchase(@conference, current_user, params[:tickets][0]) if message.blank? if current_user.ticket_purchases.by_conference(@conference).unpaid.any? - redirect_to new_conference_payment_path, notice: 'Please pay here to purchase tickets.' + redirect_to new_conference_payment_path, + notice: 'Please pay here to purchase tickets.' else - redirect_to conference_conference_registration_path(@conference.short_title) + redirect_to conference_tickets_path(@conference.short_title), + error: 'Please purchase atleast one ticket to continue.' end else redirect_to conference_conference_registration_path(@conference.short_title), diff --git a/app/models/ability.rb b/app/models/ability.rb index 74b1e47d..d986c1b0 100644 --- a/app/models/ability.rb +++ b/app/models/ability.rb @@ -81,7 +81,7 @@ class Ability can :index, Ticket can :manage, TicketPurchase, user_id: user.id - can [:new], Payment, user_id: user.id + can [:new, :create], Payment, user_id: user.id can [:create, :destroy], Subscription, user_id: user.id diff --git a/app/views/conference_registrations/show.html.haml b/app/views/conference_registrations/show.html.haml index 24923d07..9debda28 100644 --- a/app/views/conference_registrations/show.html.haml +++ b/app/views/conference_registrations/show.html.haml @@ -109,7 +109,7 @@ You haven't bought any tickets. = link_to 'Please buy some tickets to support us!', conference_tickets_path(@conference.short_title) %p - (Your registration won't be complete without buying a ticket) + (Your participation won't be valid without buying a ticket) .row .col-md-12 diff --git a/app/views/payments/_payment.html.haml b/app/views/payments/_payment.html.haml index a8de023f..953d8334 100644 --- a/app/views/payments/_payment.html.haml +++ b/app/views/payments/_payment.html.haml @@ -26,8 +26,8 @@ = humanized_money_with_symbol @total_amount_to_pay %script.stripe-button{ src: "https://checkout.stripe.com/checkout.js", data: { amount: @total_amount_to_pay.cents, + email: current_user.email, currency: @total_amount_to_pay.currency, - image: image_url('OSEM_ICON.jpg'), name: ENV['OSEM_NAME'] || 'OSEM', description: "book your tickets", key: Rails.application.secrets.stripe_publishable_key, diff --git a/app/views/tickets/index.html.haml b/app/views/tickets/index.html.haml index 8f80e1ac..f9fa9a5e 100644 --- a/app/views/tickets/index.html.haml +++ b/app/views/tickets/index.html.haml @@ -42,4 +42,4 @@ .col-md-13 %p.text-muted.text-center %small - * Buying a ticket is mandatory. Your registration will not complete until you buy a ticket. + * Buying a ticket is mandatory. Your participation will not be valid until you buy a ticket. diff --git a/config/initializers/stripe.rb b/config/initializers/stripe.rb index 22ff8359..fe10a25c 100644 --- a/config/initializers/stripe.rb +++ b/config/initializers/stripe.rb @@ -1,6 +1 @@ -Rails.configuration.stripe = { - :publishable_key => ENV['STRIPE_PUBLISHABLE_KEY'] || Rails.application.secrets.stripe_publishable_key, - :secret_key => ENV['STRIPE_SECRET_KEY'] || Rails.application.secrets.stripe_secret_key -} - -Stripe.api_key = Rails.configuration.stripe[:secret_key] +Stripe.api_key = Rails.application.secret.stripe_secret_key diff --git a/spec/models/ability_spec.rb b/spec/models/ability_spec.rb index 720aec33..df12e8b9 100644 --- a/spec/models/ability_spec.rb +++ b/spec/models/ability_spec.rb @@ -105,6 +105,9 @@ describe 'User' do it{ should be_able_to(:index, Ticket) } it{ should be_able_to(:manage, TicketPurchase.new(user_id: user.id)) } + it{ should be_able_to(:new, Payment.new(user_id: user.id)) } + it{ should be_able_to(:create, Payment.new(user_id: user.id)) } + it{ should be_able_to(:create, Subscription.new(user_id: user.id)) } it{ should be_able_to(:destroy, subscription) } From 048f20bcfc21c406b4351105d02dce27d3a03cb2 Mon Sep 17 00:00:00 2001 From: Rishabh Saxena Date: Fri, 5 Aug 2016 22:56:29 +0530 Subject: [PATCH 17/31] fix typo. change secrets.yml config. --- app/controllers/payments_controller.rb | 2 +- app/controllers/ticket_purchases_controller.rb | 2 +- config/initializers/stripe.rb | 2 +- config/secrets.yml.example | 4 ++-- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/app/controllers/payments_controller.rb b/app/controllers/payments_controller.rb index d637f1f5..39f6df58 100644 --- a/app/controllers/payments_controller.rb +++ b/app/controllers/payments_controller.rb @@ -22,7 +22,7 @@ class PaymentsController < ApplicationController if @payment.purchase && @payment.save update_purchased_ticket_purchases redirect_to conference_conference_registration_path(@conference.short_title), - notice: 'Thanks! You have purchased your tickets successfully.' + notice: 'Thanks! You have purchased your tickets successfully.' else @total_amount_to_pay = Ticket.total_price(@conference, current_user, paid: false) @unpaid_ticket_purchases = current_user.ticket_purchases.unpaid.by_conference(@conference) diff --git a/app/controllers/ticket_purchases_controller.rb b/app/controllers/ticket_purchases_controller.rb index 4a334a08..fb937dec 100644 --- a/app/controllers/ticket_purchases_controller.rb +++ b/app/controllers/ticket_purchases_controller.rb @@ -12,7 +12,7 @@ class TicketPurchasesController < ApplicationController notice: 'Please pay here to purchase tickets.' else redirect_to conference_tickets_path(@conference.short_title), - error: 'Please purchase atleast one ticket to continue.' + error: 'Please purchase at least one ticket to continue.' end else redirect_to conference_conference_registration_path(@conference.short_title), diff --git a/config/initializers/stripe.rb b/config/initializers/stripe.rb index fe10a25c..555cc16f 100644 --- a/config/initializers/stripe.rb +++ b/config/initializers/stripe.rb @@ -1 +1 @@ -Stripe.api_key = Rails.application.secret.stripe_secret_key +Stripe.api_key = Rails.application.secrets.stripe_secret_key diff --git a/config/secrets.yml.example b/config/secrets.yml.example index 8603a189..744267a8 100644 --- a/config/secrets.yml.example +++ b/config/secrets.yml.example @@ -64,5 +64,5 @@ production: # Register on stripe and add LIVE keys here # https://dashboard.stripe.com/account/apikeys - stripe_publishable_key: '' - stripe_secret_key: '' + stripe_publishable_key: <%= ENV['STRIPE_PUBLISHABLE_KEY'] %> + stripe_secret_key: <%= ENV['STRIPE_SECRET_KEY'] %> From 05e2a061cc40a02f75574651d7ed2bc853e1f931 Mon Sep 17 00:00:00 2001 From: Rishabh Saxena Date: Mon, 8 Aug 2016 22:43:57 +0530 Subject: [PATCH 18/31] add stripe key to development env --- config/secrets.yml.example | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/config/secrets.yml.example b/config/secrets.yml.example index 744267a8..e5c5013f 100644 --- a/config/secrets.yml.example +++ b/config/secrets.yml.example @@ -15,8 +15,8 @@ development: # Register on stripe and add TEST keys here # https://dashboard.stripe.com/account/apikeys - stripe_publishable_key: '' - stripe_secret_key: '' + stripe_publishable_key: <%= ENV['STRIPE_PUBLISHABLE_KEY'] %> + stripe_secret_key: <%= ENV['STRIPE_SECRET_KEY'] %> test: # Generate your own with rake secret From 4b0a5b11257342c0a54c59312846face826325ce Mon Sep 17 00:00:00 2001 From: Rishabh Saxena Date: Mon, 8 Aug 2016 23:36:16 +0530 Subject: [PATCH 19/31] move params merge to private method --- app/controllers/payments_controller.rb | 10 +++++----- app/views/payments/new.html.haml | 3 ++- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/app/controllers/payments_controller.rb b/app/controllers/payments_controller.rb index 39f6df58..a86c5f66 100644 --- a/app/controllers/payments_controller.rb +++ b/app/controllers/payments_controller.rb @@ -14,10 +14,7 @@ class PaymentsController < ApplicationController end def create - @payment = Payment.new payment_params.merge(stripe_customer_email: params[:stripeEmail], - stripe_customer_token: params[:stripeToken], - user: current_user, - conference: @conference) + @payment = Payment.new payment_params if @payment.purchase && @payment.save update_purchased_ticket_purchases @@ -33,7 +30,10 @@ class PaymentsController < ApplicationController private def payment_params - params.permit :stripe_customer_email, :stripe_customer_token + params.permit(:stripe_customer_email, :stripe_customer_token) + .merge(stripe_customer_email: params[:stripeEmail], + stripe_customer_token: params[:stripeToken], + user: current_user, conference: @conference) end def update_purchased_ticket_purchases diff --git a/app/views/payments/new.html.haml b/app/views/payments/new.html.haml index 1817f32c..0f12bf34 100644 --- a/app/views/payments/new.html.haml +++ b/app/views/payments/new.html.haml @@ -15,4 +15,5 @@ .col-md-13 %p.text-muted.text-center %small - The payment is totally secure. Your credit card details will be sent directly to our payment processor. + All payments are handled securely by our payment processor, + = link_to 'Stripe', 'https://stripe.com', target: '_blank' From 5c21d1c4d97e549ff0a4142967609c2325ea1021 Mon Sep 17 00:00:00 2001 From: Rishabh Saxena Date: Tue, 9 Aug 2016 02:29:44 +0530 Subject: [PATCH 20/31] add Payment#purchase tests --- Gemfile | 2 ++ Gemfile.lock | 6 ++++ app/models/payment.rb | 1 + spec/factories/payments.rb | 1 - spec/models/payment_spec.rb | 58 +++++++++++++++++++++++++++++++++++++ 5 files changed, 67 insertions(+), 1 deletion(-) diff --git a/Gemfile b/Gemfile index 14ef20ad..5a1cae6e 100644 --- a/Gemfile +++ b/Gemfile @@ -225,6 +225,8 @@ group :test do gem 'timecop' # for mocking external requests gem 'webmock' + # for mocking Stripe responses in tests + gem 'stripe-ruby-mock' end group :development, :test do diff --git a/Gemfile.lock b/Gemfile.lock index 770dcf66..98196af8 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -141,6 +141,7 @@ GEM safe_yaml (~> 1.0.0) currencies (0.4.2) daemons (1.1.9) + dante (0.2.0) database_cleaner (1.3.0) debug_inspector (0.0.2) debugger-linecache (1.2.0) @@ -483,6 +484,10 @@ GEM sqlite3 (1.3.9) stripe (1.43.0) rest-client (~> 1.4) + stripe-ruby-mock (2.3.0) + dante (>= 0.2.0) + multi_json (>= 1.0.0) + stripe (>= 1.31.0, <= 1.43) term-ansicolor (1.3.2) tins (~> 1.0) thor (0.19.1) @@ -618,6 +623,7 @@ DEPENDENCIES spring-commands-rspec sqlite3 stripe + stripe-ruby-mock timecop transitions turbolinks diff --git a/app/models/payment.rb b/app/models/payment.rb index 4ad4df85..9cab6592 100644 --- a/app/models/payment.rb +++ b/app/models/payment.rb @@ -27,6 +27,7 @@ class Payment < ActiveRecord::Base amount: amount_to_pay, currency: conference.tickets.first.price_currency + self.amount = gateway_response[:amount] self.last4 = gateway_response[:source][:last4] self.authorization_code = gateway_response[:id] self.status = 'success' diff --git a/spec/factories/payments.rb b/spec/factories/payments.rb index f0829c5c..4d4d3c39 100644 --- a/spec/factories/payments.rb +++ b/spec/factories/payments.rb @@ -3,6 +3,5 @@ FactoryGirl.define do user conference status 'unpaid' - amount 1000 end end diff --git a/spec/models/payment_spec.rb b/spec/models/payment_spec.rb index 4512fe80..1a2be88b 100644 --- a/spec/models/payment_spec.rb +++ b/spec/models/payment_spec.rb @@ -1,4 +1,5 @@ require 'spec_helper' +require 'stripe_mock' describe Payment do @@ -32,4 +33,61 @@ describe Payment do expect(payment.amount_to_pay).to eq(8000) end end + + describe '#purchase' do + let!(:user) { create(:user) } + let!(:conference) { create(:conference) } + let!(:ticket_1) { create(:ticket, price: 10, price_currency: 'USD', conference: conference) } + let!(:tickets) { {ticket_1.id.to_s => '2'} } + let(:stripe_helper) { StripeMock.create_test_helper } + + before { StripeMock.start } + after { StripeMock.stop } + + before { TicketPurchase.purchase(conference, user, tickets) } + let!(:payment) { create(:payment, user: user, conference: conference, stripe_customer_token: stripe_helper.generate_card_token, stripe_customer_email: user.email) } + + context 'when the payment is successful' do + before { payment.purchase } + + it 'assigns amount' do + expect(payment.amount).to eq(2000) + end + + it 'assigns last4' do + expect(payment.last4).to eq('4242') + end + + it "assigns 'success' to payment.status" do + expect(payment.status).to eq('success') + end + + it 'assigns authorization_code' do + expect(payment.authorization_code).to eq('test_ch_3') + end + end + + context 'if the payment is not successful' do + before { StripeMock.prepare_card_error(:invalid_number) } + + let(:payment) { create(:payment, user: user, conference: conference, stripe_customer_token: 'bogus_card_token', stripe_customer_email: user.email) } + + before { payment.purchase } + + context 'when the card is invalid' do + it 'returns false' do + payment_result = payment.purchase + expect(payment_result).to eq false + end + + it 'assigns "failure" to payment.status' do + expect(payment.status).to eq('failure') + end + + it 'adds errors' do + expect(payment.errors[:base].count).to eq(1) + end + end + end + end end From 922440fb771a2087e0591f3719e87d958ed504e5 Mon Sep 17 00:00:00 2001 From: Rishabh Saxena Date: Wed, 10 Aug 2016 13:53:14 +0530 Subject: [PATCH 21/31] improve payment view styling --- app/assets/stylesheets/osem-payments.css.scss | 22 ------------------- app/views/payments/_payment.html.haml | 7 ++---- 2 files changed, 2 insertions(+), 27 deletions(-) diff --git a/app/assets/stylesheets/osem-payments.css.scss b/app/assets/stylesheets/osem-payments.css.scss index e0a05e8c..cfa451b4 100644 --- a/app/assets/stylesheets/osem-payments.css.scss +++ b/app/assets/stylesheets/osem-payments.css.scss @@ -1,25 +1,3 @@ -.price-tags { - list-style-type: none; -} -.price-tags li { - line-height: 40px; - position: relative; - margin-right: -3rem; -} -.price-tags a { - background: #2f991d; - color: #fff; - font-size: 1.5rem; - padding: 9px 10px; - text-decoration: none; -} -.price-tags a:after { - content: ""; - float: left; - border-top: 20px solid transparent; - border-right: 20px solid #2f991d; - border-bottom: 20px solid transparent; -} .stripe-button-el { float: right; } diff --git a/app/views/payments/_payment.html.haml b/app/views/payments/_payment.html.haml index 953d8334..52b7063c 100644 --- a/app/views/payments/_payment.html.haml +++ b/app/views/payments/_payment.html.haml @@ -20,16 +20,13 @@ = humanized_money_with_symbol ticket.quantity * ticket.price = form_tag conference_payments_path do - %ul.price-tags.pull-right - %li - %a - = humanized_money_with_symbol @total_amount_to_pay %script.stripe-button{ src: "https://checkout.stripe.com/checkout.js", data: { amount: @total_amount_to_pay.cents, + label: "Pay #{humanized_money_with_symbol @total_amount_to_pay}", email: current_user.email, currency: @total_amount_to_pay.currency, name: ENV['OSEM_NAME'] || 'OSEM', description: "book your tickets", key: Rails.application.secrets.stripe_publishable_key, locale: "auto"}} - = link_to 'Edit Purchase', conference_tickets_path(@conference.short_title), class: 'btn btn-primary' + = link_to 'Edit Purchase', conference_tickets_path(@conference.short_title), class: 'btn btn-default' From a78804b94d39d32a6b1890f9a29cce974ccafc5e Mon Sep 17 00:00:00 2001 From: Rishabh Saxena Date: Wed, 10 Aug 2016 13:54:24 +0530 Subject: [PATCH 22/31] add more tests to check Stripe exceptions --- app/models/payment.rb | 2 +- spec/features/ticket_purchases_spec.rb | 13 ++++++- spec/models/payment_spec.rb | 52 ++++++++++++++++++++++++-- 3 files changed, 62 insertions(+), 5 deletions(-) diff --git a/app/models/payment.rb b/app/models/payment.rb index 9cab6592..e9386a35 100644 --- a/app/models/payment.rb +++ b/app/models/payment.rb @@ -33,7 +33,7 @@ class Payment < ActiveRecord::Base self.status = 'success' true - rescue => error + rescue Stripe::StripeError => error errors.add(:base, error.message) self.status = 'failure' false diff --git a/spec/features/ticket_purchases_spec.rb b/spec/features/ticket_purchases_spec.rb index 3d42473f..1d67c285 100644 --- a/spec/features/ticket_purchases_spec.rb +++ b/spec/features/ticket_purchases_spec.rb @@ -1,4 +1,5 @@ require 'spec_helper' +require 'stripe_mock' feature Registration do let!(:ticket) { create(:ticket) } @@ -8,15 +9,19 @@ feature Registration do context 'as a participant' do before(:each) do sign_in participant + StripeMock.start end after(:each) do + StripeMock.stop sign_out end + let(:stripe_helper) { StripeMock.create_test_helper } + context 'who is not registered' do - scenario 'purchases a ticket', feature: true, js: true do + scenario 'purchases and pays for a ticket succcessfully', feature: true, js: true do visit root_path click_link 'Register' @@ -32,6 +37,12 @@ feature Registration do expect(flash).to eq('Please pay here to purchase tickets.') purchase = TicketPurchase.where(user_id: participant.id, ticket_id: ticket.id).first expect(purchase.quantity).to eq(2) + + # token = stripe_helper.generate_card_token + # merge token, email and submit form + # page.execute_script("$('form').submit()") + + expect(current_path).to eq(conference_conference_registration_path(conference.short_title)) end end end diff --git a/spec/models/payment_spec.rb b/spec/models/payment_spec.rb index 1a2be88b..71277146 100644 --- a/spec/models/payment_spec.rb +++ b/spec/models/payment_spec.rb @@ -45,7 +45,7 @@ describe Payment do after { StripeMock.stop } before { TicketPurchase.purchase(conference, user, tickets) } - let!(:payment) { create(:payment, user: user, conference: conference, stripe_customer_token: stripe_helper.generate_card_token, stripe_customer_email: user.email) } + let(:payment) { create(:payment, user: user, conference: conference, stripe_customer_token: stripe_helper.generate_card_token, stripe_customer_email: user.email) } context 'when the payment is successful' do before { payment.purchase } @@ -68,8 +68,6 @@ describe Payment do end context 'if the payment is not successful' do - before { StripeMock.prepare_card_error(:invalid_number) } - let(:payment) { create(:payment, user: user, conference: conference, stripe_customer_token: 'bogus_card_token', stripe_customer_email: user.email) } before { payment.purchase } @@ -88,6 +86,54 @@ describe Payment do expect(payment.errors[:base].count).to eq(1) end end + + context 'when the connection to Stripe drops' do + it 'raises exception' do + StripeMock.prepare_error(Stripe::APIConnectionError.new) + expect{ Stripe::Charge.create }.to raise_error(Stripe::APIConnectionError) + expect{ payment.purchase }.not_to raise_error + end + end + + context 'when there is a Stripe API Error' do + it 'raises exception' do + StripeMock.prepare_error(Stripe::APIError.new) + expect{ Stripe::Charge.create }.to raise_error(Stripe::APIError) + expect{ payment.purchase }.not_to raise_error + end + end + + context 'when there is authentication error' do + it 'raises exception' do + StripeMock.prepare_error(Stripe::AuthenticationError.new) + expect{ Stripe::Charge.create }.to raise_error(Stripe::AuthenticationError) + expect{ payment.purchase }.not_to raise_error + end + end + + context 'when there is a card error' do + it 'raises exception' do + StripeMock.prepare_card_error(:card_declined) + expect{ Stripe::Charge.create }.to raise_error(Stripe::CardError) + expect{ payment.purchase }.not_to raise_error + end + end + + context 'when the request to Stripe is invalid' do + it 'raises exception' do + StripeMock.prepare_error(Stripe::InvalidRequestError.new('Your request is invalid.', code: 402)) + expect{ Stripe::Charge.create }.to raise_error(Stripe::InvalidRequestError) + expect{ payment.purchase }.not_to raise_error + end + end + + context 'when Stripe rate limit exceeds' do + it 'raises exception' do + StripeMock.prepare_error(Stripe::RateLimitError.new) + expect{ Stripe::Charge.create }.to raise_error(Stripe::RateLimitError) + expect{ payment.purchase }.not_to raise_error + end + end end end end From 6bd2e5e335258edb45a755f6031733e66b354728 Mon Sep 17 00:00:00 2001 From: Rishabh Saxena Date: Wed, 10 Aug 2016 23:19:36 +0530 Subject: [PATCH 23/31] refactor tests --- spec/features/ticket_purchases_spec.rb | 11 ----------- spec/models/payment_spec.rb | 6 ------ 2 files changed, 17 deletions(-) diff --git a/spec/features/ticket_purchases_spec.rb b/spec/features/ticket_purchases_spec.rb index 1d67c285..9ac82a63 100644 --- a/spec/features/ticket_purchases_spec.rb +++ b/spec/features/ticket_purchases_spec.rb @@ -1,5 +1,4 @@ require 'spec_helper' -require 'stripe_mock' feature Registration do let!(:ticket) { create(:ticket) } @@ -9,16 +8,12 @@ feature Registration do context 'as a participant' do before(:each) do sign_in participant - StripeMock.start end after(:each) do - StripeMock.stop sign_out end - let(:stripe_helper) { StripeMock.create_test_helper } - context 'who is not registered' do scenario 'purchases and pays for a ticket succcessfully', feature: true, js: true do @@ -37,12 +32,6 @@ feature Registration do expect(flash).to eq('Please pay here to purchase tickets.') purchase = TicketPurchase.where(user_id: participant.id, ticket_id: ticket.id).first expect(purchase.quantity).to eq(2) - - # token = stripe_helper.generate_card_token - # merge token, email and submit form - # page.execute_script("$('form').submit()") - - expect(current_path).to eq(conference_conference_registration_path(conference.short_title)) end end end diff --git a/spec/models/payment_spec.rb b/spec/models/payment_spec.rb index 71277146..6af28679 100644 --- a/spec/models/payment_spec.rb +++ b/spec/models/payment_spec.rb @@ -90,7 +90,6 @@ describe Payment do context 'when the connection to Stripe drops' do it 'raises exception' do StripeMock.prepare_error(Stripe::APIConnectionError.new) - expect{ Stripe::Charge.create }.to raise_error(Stripe::APIConnectionError) expect{ payment.purchase }.not_to raise_error end end @@ -98,7 +97,6 @@ describe Payment do context 'when there is a Stripe API Error' do it 'raises exception' do StripeMock.prepare_error(Stripe::APIError.new) - expect{ Stripe::Charge.create }.to raise_error(Stripe::APIError) expect{ payment.purchase }.not_to raise_error end end @@ -106,7 +104,6 @@ describe Payment do context 'when there is authentication error' do it 'raises exception' do StripeMock.prepare_error(Stripe::AuthenticationError.new) - expect{ Stripe::Charge.create }.to raise_error(Stripe::AuthenticationError) expect{ payment.purchase }.not_to raise_error end end @@ -114,7 +111,6 @@ describe Payment do context 'when there is a card error' do it 'raises exception' do StripeMock.prepare_card_error(:card_declined) - expect{ Stripe::Charge.create }.to raise_error(Stripe::CardError) expect{ payment.purchase }.not_to raise_error end end @@ -122,7 +118,6 @@ describe Payment do context 'when the request to Stripe is invalid' do it 'raises exception' do StripeMock.prepare_error(Stripe::InvalidRequestError.new('Your request is invalid.', code: 402)) - expect{ Stripe::Charge.create }.to raise_error(Stripe::InvalidRequestError) expect{ payment.purchase }.not_to raise_error end end @@ -130,7 +125,6 @@ describe Payment do context 'when Stripe rate limit exceeds' do it 'raises exception' do StripeMock.prepare_error(Stripe::RateLimitError.new) - expect{ Stripe::Charge.create }.to raise_error(Stripe::RateLimitError) expect{ payment.purchase }.not_to raise_error end end From 261d7dd0deb0bb4bada3f4fa86b13c8b6086edcd Mon Sep 17 00:00:00 2001 From: Rishabh Saxena Date: Thu, 11 Aug 2016 17:47:52 +0530 Subject: [PATCH 24/31] improve flash error collection improve views --- app/controllers/payments_controller.rb | 1 + .../ticket_purchases_controller.rb | 2 +- .../conference_registrations/show.html.haml | 26 +++++++++---------- app/views/payments/new.html.haml | 5 ---- 4 files changed, 15 insertions(+), 19 deletions(-) diff --git a/app/controllers/payments_controller.rb b/app/controllers/payments_controller.rb index a86c5f66..5b1259d1 100644 --- a/app/controllers/payments_controller.rb +++ b/app/controllers/payments_controller.rb @@ -23,6 +23,7 @@ class PaymentsController < ApplicationController else @total_amount_to_pay = Ticket.total_price(@conference, current_user, paid: false) @unpaid_ticket_purchases = current_user.ticket_purchases.unpaid.by_conference(@conference) + flash[:error] = @payment.errors.full_messages.to_sentence + ' Please try again with correct credentials.' render :new end end diff --git a/app/controllers/ticket_purchases_controller.rb b/app/controllers/ticket_purchases_controller.rb index fb937dec..2d9d269b 100644 --- a/app/controllers/ticket_purchases_controller.rb +++ b/app/controllers/ticket_purchases_controller.rb @@ -4,7 +4,7 @@ class TicketPurchasesController < ApplicationController authorize_resource :conference_registrations, class: Registration def create - TicketPurchase.by_conference(@conference).unpaid.by_user(current_user).destroy_all + current_user.ticket_purchases.by_conference(@conference).unpaid.destroy_all message = TicketPurchase.purchase(@conference, current_user, params[:tickets][0]) if message.blank? if current_user.ticket_purchases.by_conference(@conference).unpaid.any? diff --git a/app/views/conference_registrations/show.html.haml b/app/views/conference_registrations/show.html.haml index 9debda28..1567475f 100644 --- a/app/views/conference_registrations/show.html.haml +++ b/app/views/conference_registrations/show.html.haml @@ -85,12 +85,12 @@ - if @conference.tickets.any? .row .col-md-12 - %h4 - %span.fa-stack - %i.fa.fa-square-o.fa-stack-2x - %i.fa.fa-ticket.fa-stack-1x - Ticket Purchases - -if @tickets.any? + -if @tickets.any? + %h4 + %span.fa-stack + %i.fa.fa-square-o.fa-stack-2x + %i.fa.fa-ticket.fa-stack-1x + Ticket Purchases = "(#{@tickets.first.price.symbol}#{humanized_money @total_price})" %ul .col-md-12 @@ -103,13 +103,13 @@ = tickets.first.price.symbol = humanized_money tickets.first.price %br - - if @tickets.any? - = link_to 'Buy more tickets', conference_tickets_path(@conference.short_title), class: "btn btn-default" - - else - You haven't bought any tickets. - = link_to 'Please buy some tickets to support us!', conference_tickets_path(@conference.short_title) - %p - (Your participation won't be valid without buying a ticket) + - if @tickets.any? + = link_to 'Buy more tickets', conference_tickets_path(@conference.short_title), class: "btn btn-default" + - else + You haven't bought any tickets. + = link_to 'Please buy some tickets to support us!', conference_tickets_path(@conference.short_title) + %p + (Your participation won't be valid without buying a ticket) .row .col-md-12 diff --git a/app/views/payments/new.html.haml b/app/views/payments/new.html.haml index 0f12bf34..0401f785 100644 --- a/app/views/payments/new.html.haml +++ b/app/views/payments/new.html.haml @@ -4,11 +4,6 @@ %h1 Payment Summary : = humanized_money_with_symbol @total_amount_to_pay - - if @payment.errors.any? - .alert.alert-danger - %ul - - @payment.errors.full_messages.each do |msg| - %li= msg .col-xs-8.col-xs-offset-2.well = render partial: 'payment' .row From 6aecf2d4464976922dcc934443f6fb0608c35e8c Mon Sep 17 00:00:00 2001 From: Rishabh Saxena Date: Thu, 11 Aug 2016 23:17:31 +0530 Subject: [PATCH 25/31] modify conference registration view message --- app/views/conference_registrations/show.html.haml | 2 +- app/views/tickets/index.html.haml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/views/conference_registrations/show.html.haml b/app/views/conference_registrations/show.html.haml index 1567475f..30dff195 100644 --- a/app/views/conference_registrations/show.html.haml +++ b/app/views/conference_registrations/show.html.haml @@ -109,7 +109,7 @@ You haven't bought any tickets. = link_to 'Please buy some tickets to support us!', conference_tickets_path(@conference.short_title) %p - (Your participation won't be valid without buying a ticket) + (Your participation won't be valid without getting a ticket) .row .col-md-12 diff --git a/app/views/tickets/index.html.haml b/app/views/tickets/index.html.haml index f9fa9a5e..894a4178 100644 --- a/app/views/tickets/index.html.haml +++ b/app/views/tickets/index.html.haml @@ -42,4 +42,4 @@ .col-md-13 %p.text-muted.text-center %small - * Buying a ticket is mandatory. Your participation will not be valid until you buy a ticket. + * Getting a ticket is mandatory. Your participation will not be valid until you get a ticket. From 164101f2fed6c231e84d85cbbbf019cd34d30f7e Mon Sep 17 00:00:00 2001 From: Rishabh Saxena Date: Fri, 12 Aug 2016 13:21:47 +0530 Subject: [PATCH 26/31] change "purchase" to "get" in views --- app/controllers/payments_controller.rb | 2 +- app/controllers/ticket_purchases_controller.rb | 4 ++-- app/views/admin/tickets/index.html.haml | 2 +- app/views/conference/_tickets.html.haml | 4 ++-- app/views/conference_registrations/show.html.haml | 4 ++-- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/app/controllers/payments_controller.rb b/app/controllers/payments_controller.rb index 5b1259d1..db42a201 100644 --- a/app/controllers/payments_controller.rb +++ b/app/controllers/payments_controller.rb @@ -19,7 +19,7 @@ class PaymentsController < ApplicationController if @payment.purchase && @payment.save update_purchased_ticket_purchases redirect_to conference_conference_registration_path(@conference.short_title), - notice: 'Thanks! You have purchased your tickets successfully.' + notice: 'Thanks! Your ticket is booked successfully.' else @total_amount_to_pay = Ticket.total_price(@conference, current_user, paid: false) @unpaid_ticket_purchases = current_user.ticket_purchases.unpaid.by_conference(@conference) diff --git a/app/controllers/ticket_purchases_controller.rb b/app/controllers/ticket_purchases_controller.rb index 2d9d269b..a8749eed 100644 --- a/app/controllers/ticket_purchases_controller.rb +++ b/app/controllers/ticket_purchases_controller.rb @@ -9,10 +9,10 @@ class TicketPurchasesController < ApplicationController if message.blank? if current_user.ticket_purchases.by_conference(@conference).unpaid.any? redirect_to new_conference_payment_path, - notice: 'Please pay here to purchase tickets.' + notice: 'Please pay here to get tickets.' else redirect_to conference_tickets_path(@conference.short_title), - error: 'Please purchase at least one ticket to continue.' + error: 'Please get at least one ticket to continue.' end else redirect_to conference_conference_registration_path(@conference.short_title), diff --git a/app/views/admin/tickets/index.html.haml b/app/views/admin/tickets/index.html.haml index 2a2bac6d..486da0ef 100644 --- a/app/views/admin/tickets/index.html.haml +++ b/app/views/admin/tickets/index.html.haml @@ -3,7 +3,7 @@ .page-header %h1 Tickets %p.text-muted - Tickets to purchase during registration + Tickets to get during registration - if @conference.tickets.any? .row .col-md-12 diff --git a/app/views/conference/_tickets.html.haml b/app/views/conference/_tickets.html.haml index 41108b78..8a202295 100644 --- a/app/views/conference/_tickets.html.haml +++ b/app/views/conference/_tickets.html.haml @@ -5,7 +5,7 @@ Support =@conference.short_title %p.lead - To support our event you can purchase these tickets + To support our event you can get these tickets - @conference.tickets.each_slice(4) do |slice| .row.row-centered - slice.each do |ticket| @@ -20,6 +20,6 @@ = markdown(ticket.description) %p.text-center = link_to(conference_tickets_path(@conference.short_title), class: 'btn btn-success') do - Buy Ticket + Get Ticket = humanized_money_with_symbol ticket.price diff --git a/app/views/conference_registrations/show.html.haml b/app/views/conference_registrations/show.html.haml index 30dff195..591d0777 100644 --- a/app/views/conference_registrations/show.html.haml +++ b/app/views/conference_registrations/show.html.haml @@ -104,10 +104,10 @@ = humanized_money tickets.first.price %br - if @tickets.any? - = link_to 'Buy more tickets', conference_tickets_path(@conference.short_title), class: "btn btn-default" + = link_to 'Get more tickets', conference_tickets_path(@conference.short_title), class: "btn btn-default" - else You haven't bought any tickets. - = link_to 'Please buy some tickets to support us!', conference_tickets_path(@conference.short_title) + = link_to 'Please get some tickets to support us!', conference_tickets_path(@conference.short_title) %p (Your participation won't be valid without getting a ticket) From 060d65c430d4af388a0f36eabe4e64a6c3fe7f29 Mon Sep 17 00:00:00 2001 From: Rishabh Saxena Date: Tue, 16 Aug 2016 00:43:48 +0530 Subject: [PATCH 27/31] views/_payment: decrease script indentation --- app/views/payments/_payment.html.haml | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/app/views/payments/_payment.html.haml b/app/views/payments/_payment.html.haml index 52b7063c..56af80f7 100644 --- a/app/views/payments/_payment.html.haml +++ b/app/views/payments/_payment.html.haml @@ -21,12 +21,7 @@ = form_tag conference_payments_path do %script.stripe-button{ src: "https://checkout.stripe.com/checkout.js", - data: { amount: @total_amount_to_pay.cents, - label: "Pay #{humanized_money_with_symbol @total_amount_to_pay}", - email: current_user.email, - currency: @total_amount_to_pay.currency, - name: ENV['OSEM_NAME'] || 'OSEM', - description: "book your tickets", - key: Rails.application.secrets.stripe_publishable_key, - locale: "auto"}} + data: { amount: @total_amount_to_pay.cents, label: "Pay #{humanized_money_with_symbol @total_amount_to_pay}", + email: current_user.email, currency: @total_amount_to_pay.currency, name: ENV['OSEM_NAME'] || 'OSEM', + description: "book your tickets", key: Rails.application.secrets.stripe_publishable_key, locale: "auto"}} = link_to 'Edit Purchase', conference_tickets_path(@conference.short_title), class: 'btn btn-default' From 75e11223d247a00f8bfec12275f1a55355ef2863 Mon Sep 17 00:00:00 2001 From: Rishabh Saxena Date: Tue, 16 Aug 2016 00:46:11 +0530 Subject: [PATCH 28/31] spec/feature: add stripe integration tests --- config/secrets.yml.example | 5 +++ spec/features/ticket_purchases_spec.rb | 52 +++++++++++++++++++++++++- spec/support/external_request.rb | 2 +- 3 files changed, 57 insertions(+), 2 deletions(-) diff --git a/config/secrets.yml.example b/config/secrets.yml.example index e5c5013f..a8d67504 100644 --- a/config/secrets.yml.example +++ b/config/secrets.yml.example @@ -22,6 +22,11 @@ test: # Generate your own with rake secret # secret_key_base: '12345' + # Register on stripe and add TEST keys here + # https://dashboard.stripe.com/account/apikeys + stripe_publishable_key: <%= ENV['STRIPE_PUBLISHABLE_KEY'] %> + stripe_secret_key: <%= ENV['STRIPE_SECRET_KEY'] %> + production: # Generate your own with rake secret or use the environment # secret_key_base: <%= ENV["SECRET_KEY_BASE"] %> diff --git a/spec/features/ticket_purchases_spec.rb b/spec/features/ticket_purchases_spec.rb index 9ac82a63..8e021245 100644 --- a/spec/features/ticket_purchases_spec.rb +++ b/spec/features/ticket_purchases_spec.rb @@ -29,9 +29,59 @@ feature Registration do click_button 'Continue' expect(current_path).to eq(new_conference_payment_path(conference.short_title)) - expect(flash).to eq('Please pay here to purchase tickets.') + expect(flash).to eq('Please pay here to get tickets.') purchase = TicketPurchase.where(user_id: participant.id, ticket_id: ticket.id).first expect(purchase.quantity).to eq(2) + + find('.stripe-button-el').click + + stripe_iframe = all('iframe[name=stripe_checkout_app]').last + sleep(5) + Capybara.within_frame stripe_iframe do + expect(page).to have_content('book your tickets') + page.execute_script(%{ $('input#card_number').val('4242424242424242'); }) + page.execute_script(%{ $('input#cc-exp').val('08/22'); }) + page.execute_script(%{ $('input#cc-csc').val('123'); }) + page.execute_script(%{ $('#submitButton').click(); }) + sleep(30) + end + + expect(current_path).to eq(conference_conference_registration_path(conference.short_title)) + expect(page.has_content?("2 #{ticket.title} Tickets for $ 10")).to be true + end + + scenario 'purchases ticket but payment fails', feature: true, js: true do + visit root_path + click_link 'Register' + + expect(current_path).to eq(new_conference_conference_registration_path(conference.short_title)) + click_button 'Register' + + fill_in "tickets__#{ticket.id}", with: '2' + expect(current_path).to eq(conference_tickets_path(conference.short_title)) + + click_button 'Continue' + + expect(current_path).to eq(new_conference_payment_path(conference.short_title)) + expect(flash).to eq('Please pay here to get tickets.') + purchase = TicketPurchase.where(user_id: participant.id, ticket_id: ticket.id).first + expect(purchase.quantity).to eq(2) + + find('.stripe-button-el').click + + stripe_iframe = all('iframe[name=stripe_checkout_app]').last + sleep(5) + Capybara.within_frame stripe_iframe do + expect(page).to have_content('book your tickets') + page.execute_script(%{ $('input#card_number').val('4000000000000341'); }) + page.execute_script(%{ $('input#cc-exp').val('08/22'); }) + page.execute_script(%{ $('input#cc-csc').val('123'); }) + page.execute_script(%{ $('#submitButton').click(); }) + sleep(30) + end + + expect(current_path).to eq(conference_payments_path(conference.short_title)) + expect(flash).to eq('Your card was declined. Please try again with correct credentials.') end end end diff --git a/spec/support/external_request.rb b/spec/support/external_request.rb index 539fe701..4a0d21cf 100644 --- a/spec/support/external_request.rb +++ b/spec/support/external_request.rb @@ -1,6 +1,6 @@ # Mock external requests to youtube require 'webmock/rspec' -WebMock.disable_net_connect!(allow_localhost: true) +WebMock.allow_net_connect! RSpec.configure do |config| config.before(:each) do From 43ac7742a15b4789f587b09269f1d85155bd627b Mon Sep 17 00:00:00 2001 From: Rishabh Saxena Date: Tue, 16 Aug 2016 23:01:28 +0530 Subject: [PATCH 29/31] travis: provide Stripe API keys --- .travis.yml | 1 + spec/features/ticket_purchases_spec.rb | 5 +++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index f380c1b9..10af790e 100644 --- a/.travis.yml +++ b/.travis.yml @@ -18,6 +18,7 @@ notifications: on_failure: change before_script: - cp config/database.yml.example config/database.yml + - cp config/secrets.yml.example config/secrets.yml - RAILS_ENV=test bundle exec rake db:migrate --trace script: - 'bundle exec rubocop -Dc .rubocop.yml' diff --git a/spec/features/ticket_purchases_spec.rb b/spec/features/ticket_purchases_spec.rb index 8e021245..592400ab 100644 --- a/spec/features/ticket_purchases_spec.rb +++ b/spec/features/ticket_purchases_spec.rb @@ -43,9 +43,10 @@ feature Registration do page.execute_script(%{ $('input#cc-exp').val('08/22'); }) page.execute_script(%{ $('input#cc-csc').val('123'); }) page.execute_script(%{ $('#submitButton').click(); }) - sleep(30) + sleep(20) end + expect(page).to have_content("2 #{ticket.title} Tickets for $ 10") expect(current_path).to eq(conference_conference_registration_path(conference.short_title)) expect(page.has_content?("2 #{ticket.title} Tickets for $ 10")).to be true end @@ -77,7 +78,7 @@ feature Registration do page.execute_script(%{ $('input#cc-exp').val('08/22'); }) page.execute_script(%{ $('input#cc-csc').val('123'); }) page.execute_script(%{ $('#submitButton').click(); }) - sleep(30) + sleep(20) end expect(current_path).to eq(conference_payments_path(conference.short_title)) From cc09dd76bab3ba090daf0df5aae57871c9f2cd34 Mon Sep 17 00:00:00 2001 From: Rishabh Saxena Date: Wed, 17 Aug 2016 12:25:49 +0530 Subject: [PATCH 30/31] support/external_request: only allow stripe --- spec/features/ticket_purchases_spec.rb | 1 - spec/support/external_request.rb | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/spec/features/ticket_purchases_spec.rb b/spec/features/ticket_purchases_spec.rb index 592400ab..d7161961 100644 --- a/spec/features/ticket_purchases_spec.rb +++ b/spec/features/ticket_purchases_spec.rb @@ -46,7 +46,6 @@ feature Registration do sleep(20) end - expect(page).to have_content("2 #{ticket.title} Tickets for $ 10") expect(current_path).to eq(conference_conference_registration_path(conference.short_title)) expect(page.has_content?("2 #{ticket.title} Tickets for $ 10")).to be true end diff --git a/spec/support/external_request.rb b/spec/support/external_request.rb index 4a0d21cf..2de4ab08 100644 --- a/spec/support/external_request.rb +++ b/spec/support/external_request.rb @@ -1,6 +1,6 @@ # Mock external requests to youtube require 'webmock/rspec' -WebMock.allow_net_connect! +WebMock.disable_net_connect!(allow_localhost: true, allow: %r{stripe.com}) RSpec.configure do |config| config.before(:each) do From 4152cb242c1fe94b07aece86951f0024511488bd Mon Sep 17 00:00:00 2001 From: Rishabh Saxena Date: Wed, 17 Aug 2016 23:57:40 +0530 Subject: [PATCH 31/31] run stripe feature test only if key is set --- spec/features/ticket_purchases_spec.rb | 56 ++++++++++++++------------ spec/support/external_request.rb | 2 +- 2 files changed, 31 insertions(+), 27 deletions(-) diff --git a/spec/features/ticket_purchases_spec.rb b/spec/features/ticket_purchases_spec.rb index d7161961..d2eadfc9 100644 --- a/spec/features/ticket_purchases_spec.rb +++ b/spec/features/ticket_purchases_spec.rb @@ -33,21 +33,23 @@ feature Registration do purchase = TicketPurchase.where(user_id: participant.id, ticket_id: ticket.id).first expect(purchase.quantity).to eq(2) - find('.stripe-button-el').click + if Rails.application.secrets.stripe_publishable_key + find('.stripe-button-el').click - stripe_iframe = all('iframe[name=stripe_checkout_app]').last - sleep(5) - Capybara.within_frame stripe_iframe do - expect(page).to have_content('book your tickets') - page.execute_script(%{ $('input#card_number').val('4242424242424242'); }) - page.execute_script(%{ $('input#cc-exp').val('08/22'); }) - page.execute_script(%{ $('input#cc-csc').val('123'); }) - page.execute_script(%{ $('#submitButton').click(); }) - sleep(20) + stripe_iframe = all('iframe[name=stripe_checkout_app]').last + sleep(5) + Capybara.within_frame stripe_iframe do + expect(page).to have_content('book your tickets') + page.execute_script(%{ $('input#card_number').val('4242424242424242'); }) + page.execute_script(%{ $('input#cc-exp').val('08/22'); }) + page.execute_script(%{ $('input#cc-csc').val('123'); }) + page.execute_script(%{ $('#submitButton').click(); }) + sleep(20) + end + + expect(current_path).to eq(conference_conference_registration_path(conference.short_title)) + expect(page.has_content?("2 #{ticket.title} Tickets for $ 10")).to be true end - - expect(current_path).to eq(conference_conference_registration_path(conference.short_title)) - expect(page.has_content?("2 #{ticket.title} Tickets for $ 10")).to be true end scenario 'purchases ticket but payment fails', feature: true, js: true do @@ -67,21 +69,23 @@ feature Registration do purchase = TicketPurchase.where(user_id: participant.id, ticket_id: ticket.id).first expect(purchase.quantity).to eq(2) - find('.stripe-button-el').click + if Rails.application.secrets.stripe_publishable_key + find('.stripe-button-el').click - stripe_iframe = all('iframe[name=stripe_checkout_app]').last - sleep(5) - Capybara.within_frame stripe_iframe do - expect(page).to have_content('book your tickets') - page.execute_script(%{ $('input#card_number').val('4000000000000341'); }) - page.execute_script(%{ $('input#cc-exp').val('08/22'); }) - page.execute_script(%{ $('input#cc-csc').val('123'); }) - page.execute_script(%{ $('#submitButton').click(); }) - sleep(20) + stripe_iframe = all('iframe[name=stripe_checkout_app]').last + sleep(5) + Capybara.within_frame stripe_iframe do + expect(page).to have_content('book your tickets') + page.execute_script(%{ $('input#card_number').val('4000000000000341'); }) + page.execute_script(%{ $('input#cc-exp').val('08/22'); }) + page.execute_script(%{ $('input#cc-csc').val('123'); }) + page.execute_script(%{ $('#submitButton').click(); }) + sleep(20) + end + + expect(current_path).to eq(conference_payments_path(conference.short_title)) + expect(flash).to eq('Your card was declined. Please try again with correct credentials.') end - - expect(current_path).to eq(conference_payments_path(conference.short_title)) - expect(flash).to eq('Your card was declined. Please try again with correct credentials.') end end end diff --git a/spec/support/external_request.rb b/spec/support/external_request.rb index 2de4ab08..645067aa 100644 --- a/spec/support/external_request.rb +++ b/spec/support/external_request.rb @@ -1,6 +1,6 @@ # Mock external requests to youtube require 'webmock/rspec' -WebMock.disable_net_connect!(allow_localhost: true, allow: %r{stripe.com}) +WebMock.disable_net_connect!(allow_localhost: true, allow: /stripe.com/) RSpec.configure do |config| config.before(:each) do