This commit is contained in:
Rishabh Saxena 2016-08-10 06:18:19 +00:00 committed by GitHub
commit 211410b6f0
33 changed files with 621 additions and 91 deletions

View file

@ -183,6 +183,9 @@ gem 'faker'
# for seeds # for seeds
gem 'factory_girl_rails' gem 'factory_girl_rails'
# for online payments
gem 'activemerchant'
# Use guard and spring for testing in development # Use guard and spring for testing in development
group :development do group :development do
# to launch specs when files are modified # to launch specs when files are modified

View file

@ -26,6 +26,11 @@ GEM
activejob (4.2.5.2) activejob (4.2.5.2)
activesupport (= 4.2.5.2) activesupport (= 4.2.5.2)
globalid (>= 0.3.0) globalid (>= 0.3.0)
activemerchant (1.59.0)
activesupport (>= 3.2.14, < 5.1)
builder (>= 2.1.2, < 4.0.0)
i18n (>= 0.6.9)
nokogiri (~> 1.4)
activemodel (4.2.5.2) activemodel (4.2.5.2)
activesupport (= 4.2.5.2) activesupport (= 4.2.5.2)
builder (~> 3.1) builder (~> 3.1)
@ -529,6 +534,7 @@ PLATFORMS
DEPENDENCIES DEPENDENCIES
active_model_serializers active_model_serializers
activemerchant
activeuuid activeuuid
acts_as_commentable_with_threading acts_as_commentable_with_threading
acts_as_list acts_as_list

Binary file not shown.

After

Width:  |  Height:  |  Size: 4 KiB

View file

@ -28,8 +28,10 @@ class ConferenceRegistrationsController < ApplicationController
end end
def show def show
@total_price = Ticket.total_price(@conference, current_user) @total_price = Ticket.total_price(@conference, current_user, paid: true)
@tickets = current_user.ticket_purchases.where(conference_id: @conference.id) @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 end
def edit; end def edit; end

View file

@ -0,0 +1,38 @@
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)
end
def create
@payment = Payment.new(payment_params)
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
@total_amount_to_pay = Ticket.total_price(@conference, current_user, paid: false)
render 'new'
end
end
private
def update_purchased_ticket_purchases
current_user.ticket_purchases.by_conference(@conference).unpaid.update_all(paid: true, payment_id: @payment.id)
end
def payment_params
params.require(:payment)
.permit(:full_name, :credit_card_number, :expiration_month, :expiration_year, :card_verification_value, :amount)
.merge(user: current_user, conference: @conference)
end
end

View file

@ -4,11 +4,11 @@ class TicketPurchasesController < ApplicationController
authorize_resource :conference_registrations, class: Registration authorize_resource :conference_registrations, class: Registration
def create def create
TicketPurchase.by_conference(@conference).unpaid.by_user(current_user).destroy_all
message = TicketPurchase.purchase(@conference, current_user, params[:tickets][0]) message = TicketPurchase.purchase(@conference, current_user, params[:tickets][0])
if message.blank? if message.blank?
if current_user.ticket_purchases.any? if current_user.ticket_purchases.by_conference(@conference).unpaid.any?
redirect_to conference_conference_registration_path(@conference.short_title), redirect_to new_conference_payment_path, notice: 'Please pay here to purchase tickets.'
notice: "Thank you for supporting #{@conference.title} by purchasing a ticket."
else else
redirect_to conference_conference_registration_path(@conference.short_title) redirect_to conference_conference_registration_path(@conference.short_title)
end end

View file

@ -0,0 +1,9 @@
module PaymentsHelper
def months
(1..12).collect{|n| ["#{n} - #{Date::MONTHNAMES[n]}", n]}
end
def years
(Date.current.year..Date.current.year + 15)
end
end

View file

@ -81,6 +81,7 @@ class Ability
can :index, Ticket can :index, Ticket
can :manage, TicketPurchase, user_id: user.id can :manage, TicketPurchase, user_id: user.id
can [:new], Payment, user_id: user.id
can [:create, :destroy], Subscription, user_id: user.id can [:create, :destroy], Subscription, user_id: user.id

View file

@ -20,6 +20,7 @@ class Conference < ActiveRecord::Base
has_one :program, dependent: :destroy has_one :program, dependent: :destroy
has_one :venue, dependent: :destroy has_one :venue, dependent: :destroy
has_many :ticket_purchases, dependent: :destroy has_many :ticket_purchases, dependent: :destroy
has_many :payments, dependent: :destroy
has_many :supporters, through: :ticket_purchases, source: :user has_many :supporters, through: :ticket_purchases, source: :user
has_many :tickets, dependent: :destroy has_many :tickets, dependent: :destroy

59
app/models/payment.rb Normal file
View file

@ -0,0 +1,59 @@
class Payment < ActiveRecord::Base
has_many :ticket_purchases
belongs_to :user
belongs_to :conference
attr_accessor :credit_card_number
attr_accessor :credit_card_type
attr_accessor :card_verification_value
attr_accessor :expiration_month
attr_accessor :expiration_year
validates :full_name, presence: true
validates :credit_card_number, presence: true
validates :card_verification_value, presence: true, length: { minimum: 3, maximum: 4 }
validates :expiration_month, presence: true, numericality: { greater_than_or_equal_to: 1, less_than_or_equal_to: 12 }
validates :expiration_year, 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 credit_card
@credit_card ||= ActiveMerchant::Billing::CreditCard.new(
name: full_name,
number: credit_card_number,
month: expiration_month,
year: expiration_year,
verification_value: card_verification_value
)
end
def amount_to_pay
Ticket.total_price(conference, user, paid: false).cents
end
def purchase
gateway_response = begin
GATEWAY.purchase(amount_to_pay, credit_card, currency: conference.tickets.first.price_currency)
rescue
ActiveMerchant::Billing::Response.new(false, 'Unable to receive any response from the payment gateway.')
end
if gateway_response.success?
self.last4 = credit_card.display_number
self.authorization_code = gateway_response.authorization
self.status = 'success'
else
errors.add(:base, gateway_response.message)
self.status = 'failure'
end
success?
end
end

View file

@ -18,24 +18,27 @@ class Ticket < ActiveRecord::Base
end end
def paid?(user) def paid?(user)
ticket_purchases.find_by(user: user, paid: true).present? ticket_purchases.paid.by_user(user).present?
end end
def quantity_bought_by(user) def quantity_bought_by(user, paid: false)
result = ticket_purchases.where(user_id: user.id).first ticket_purchases.by_user(user).where(paid: paid).sum(:quantity)
result ? result.quantity : 0
end end
def total_price(user) def unpaid?(user)
quantity_bought_by(user) * price ticket_purchases.unpaid.by_user(user).present?
end 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) tickets = Ticket.where(conference_id: conference.id)
result = nil result = nil
begin begin
tickets.each do |ticket| tickets.each do |ticket|
price = ticket.total_price(user) price = ticket.total_price(user, paid: paid)
if result if result
result += price unless price.zero? result += price unless price.zero?
else else

View file

@ -7,23 +7,24 @@ class TicketPurchase < ActiveRecord::Base
validates_numericality_of :quantity, greater_than: 0 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 :title, to: :ticket
delegate :description, to: :ticket delegate :description, to: :ticket
delegate :price, to: :ticket delegate :price, to: :ticket
delegate :price_cents, to: :ticket delegate :price_cents, to: :ticket
delegate :price_currency, 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) def self.purchase(conference, user, purchases)
errors = [] errors = []
ActiveRecord::Base.transaction do ActiveRecord::Base.transaction do
conference.tickets.each do |ticket| conference.tickets.each do |ticket|
quantity = purchases[ticket.id.to_s].to_i quantity = purchases[ticket.id.to_s].to_i
# if the user bought the ticket, just update the quantity # 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) purchase = update_quantity(conference, quantity, ticket, user)
else else
purchase = purchase_ticket(conference, quantity, ticket, user) purchase = purchase_ticket(conference, quantity, ticket, user)
@ -48,7 +49,8 @@ class TicketPurchase < ActiveRecord::Base
def self.update_quantity(conference, quantity, ticket, user) def self.update_quantity(conference, quantity, ticket, user)
purchase = TicketPurchase.where(ticket_id: ticket.id, purchase = TicketPurchase.where(ticket_id: ticket.id,
conference_id: conference.id, conference_id: conference.id,
user_id: user.id).first user_id: user.id,
paid: false).first
purchase.quantity = quantity if quantity > 0 purchase.quantity = quantity if quantity > 0
purchase purchase

View file

@ -38,6 +38,7 @@ class User < ActiveRecord::Base
has_many :registrations, dependent: :destroy has_many :registrations, dependent: :destroy
has_many :events_registrations, through: :registrations has_many :events_registrations, through: :registrations
has_many :ticket_purchases, dependent: :destroy has_many :ticket_purchases, dependent: :destroy
has_many :payments, dependent: :destroy
has_many :tickets, through: :ticket_purchases, source: :ticket has_many :tickets, through: :ticket_purchases, source: :ticket
has_many :votes, dependent: :destroy has_many :votes, dependent: :destroy
has_many :voted_events, through: :votes, source: :events has_many :voted_events, through: :votes, source: :events

View file

@ -89,29 +89,27 @@
%span.fa-stack %span.fa-stack
%i.fa.fa-square-o.fa-stack-2x %i.fa.fa-square-o.fa-stack-2x
%i.fa.fa-ticket.fa-stack-1x %i.fa.fa-ticket.fa-stack-1x
Tickets Ticket Purchases
-if @tickets.any? -if @tickets.any?
= "(#{@total_price} #{@tickets.first.price.symbol})" = "(#{@tickets.first.price.symbol}#{humanized_money @total_price})"
%ul %ul
- @tickets.each do |ticket| .col-md-12
- @ticket_payments.each_pair do |ticket_id, tickets|
%li %li
= ticket.quantity = @total_quantity[ticket_id]
= ticket.title = tickets.first.title
= word_pluralize(ticket.quantity, 'Ticket') = word_pluralize(@total_quantity[ticket_id], 'Ticket')
for for
= humanized_money ticket.price = tickets.first.price.symbol
= ticket.price.symbol = humanized_money tickets.first.price
= link_to conference_ticket_purchase_path(@conference.short_title, ticket.id), method: :delete, %br
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? - if @tickets.any?
= link_to 'Buy more tickets', conference_tickets_path(@conference.short_title) = link_to 'Buy more tickets', conference_tickets_path(@conference.short_title), class: "btn btn-default"
- else - else
You haven't bought any tickets. You haven't bought any tickets.
= link_to 'Please buy some tickets to support us!', conference_tickets_path(@conference.short_title) = 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 .row
.col-md-12 .col-md-12

View file

@ -0,0 +1,25 @@
= semantic_form_for(@payment, url: conference_payments_path) do |f|
.form-group
= f.label :full_name, 'Full name (as on card)'
= f.text_field :full_name, class: "form-control", placeholder: "John Doe", id: "full_name"
%span.pull-right
%img.img-responsive{:src => image_url('credit_card.png')}
.form-group
= f.label :credit_card_number, "Credit Card number (without spaces)"
.input-group
= f.text_field :credit_card_number, class: "form-control", placeholder: "XXXX XXXX XXXX XXXX", id: "credit_card_number"
%span.input-group-addon
%i.fa.fa-credit-card
.form-group.col-md-6
= f.label :expiration_month
= f.select :expiration_month, months, {}, class: "form-control", id: "expiration_month"
.form-group.col-md-6
= f.label :expiration_year
= f.select :expiration_year, years, {}, class: "form-control", id: "expiration_year"
.form-group.col-md-10
= f.label :card_verification_value, 'Security Code (3 on back, AmEx: 4 on front)'
= f.text_field :card_verification_value, class: "form-control", placeholder: "XXX", id: "card_verification_value"
= f.number_field :amount, value: @total_amount_to_pay, class: "form-control", type: 'hidden'
.form-group.text-center
= f.submit "Pay #{number_to_currency @total_amount_to_pay, unit: @total_amount_to_pay.symbol}", class: "btn btn-primary", id: "Charge Card"
= link_to "Cancel", conference_conference_registration_path, class: "btn btn-danger"

View file

@ -0,0 +1,14 @@
.container
.row
.col-xs-6.col-xs-offset-3
%h1
Buy tickets for
= @total_amount_to_pay.symbol
= humanized_money @total_amount_to_pay
- if @payment.errors.any?
.alert.alert-danger
%ul
- @payment.errors.full_messages.each do |msg|
%li= msg
.col-xs-6.col-xs-offset-3.well
= render partial: 'payment'

View file

@ -8,12 +8,8 @@
- unless ticket.description.blank? - unless ticket.description.blank?
= markdown(ticket.description) = markdown(ticket.description)
%td.col-sm-1.col-md-1 %td.col-sm-1.col-md-1
- if ticket.bought?(current_user) = text_field_tag("tickets[][#{ticket.id}]", 0,
= text_field_tag("tickets[][#{ticket.id}]", ticket.quantity_bought_by(current_user),
type: 'number', min: 0, class: "form-control quantity", 'data-id' => ticket.id) 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 %td.col-sm-1.col-md-1.text-center
= ticket.price.symbol = ticket.price.symbol
%span{id: "price_#{ticket.id}"} %span{id: "price_#{ticket.id}"}

View file

@ -5,10 +5,10 @@
%h1 %h1
Tickets Tickets
%p.lead %p.lead
If you like, support Please choose your tickets for
%strong %strong
= @conference.title = @conference.title
by buying a ticket* here*
=form_tag(conference_ticket_purchases_path, method: :post) do |f| =form_tag(conference_ticket_purchases_path, method: :post) do |f|
%table.table.table-hover %table.table.table-hover
%thead %thead
@ -35,12 +35,11 @@
.pull-right .pull-right
.btn-group-vertical .btn-group-vertical
= button_tag(type: 'submit', class: 'btn btn-success btn-lg') do = button_tag(type: 'submit', class: 'btn btn-success btn-lg') do
Support Continue
%i.fa.fa-shopping-cart %i.fa.fa-shopping-cart
= link_to 'Continue without a Ticket!', conference_conference_registration_path(@conference.short_title), = link_to 'Cancel registration', conference_conference_registration_path(@conference.short_title), method: :delete, class: 'btn btn-danger btn-sm'
class: 'btn btn-danger btn-sm'
.row .row
.col-md-13 .col-md-13
%p.text-muted.text-center %p.text-muted.text-center
%small %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.

View file

@ -96,4 +96,10 @@ Osem::Application.configure do
end end
end end
#Initialize Payment Gateway with valid credentials
ActiveMerchant::Billing::Base.mode = :test
::GATEWAY = ActiveMerchant::Billing::StripeGateway.new(:login => ENV['OSEM_GATEWAY_TEST_SECRET_KEY'])
end end

View file

@ -83,4 +83,8 @@ Osem::Application.configure do
# Set the secret_key_base from the env, if not set by any other means # Set the secret_key_base from the env, if not set by any other means
config.secret_key_base ||= ENV["SECRET_KEY_BASE"] config.secret_key_base ||= ENV["SECRET_KEY_BASE"]
# Initialize Payment Gateway with valid credentials
ActiveMerchant::Billing::Base.mode = :test
::GATEWAY = ActiveMerchant::Billing::StripeGateway.new(:login => ENV['OSEM_GATEWAY_LIVE_SECRET_KEY'])
end end

View file

@ -52,4 +52,9 @@ Osem::Application.configure do
ActiveSupport::Deprecation.silenced = true ActiveSupport::Deprecation.silenced = true
end end
# Initialize Payment Gateway with valid credentials
ActiveMerchant::Billing::Base.mode = :test
::GATEWAY = ActiveMerchant::Billing::BogusGateway.new
end end

View file

@ -111,6 +111,7 @@ Osem::Application.routes.draw do
resource :conference_registration, path: 'register' resource :conference_registration, path: 'register'
resources :tickets, only: [:index] resources :tickets, only: [:index]
resources :ticket_purchases, only: [:create, :destroy] resources :ticket_purchases, only: [:create, :destroy]
resources :payments, only: [:index, :new, :create]
resource :subscriptions, only: [:create, :destroy] resource :subscriptions, only: [:create, :destroy]
member do member do

View file

@ -0,0 +1,15 @@
class CreatePayments < ActiveRecord::Migration
def change
create_table :payments do |t|
t.string :full_name, null: false
t.string :last4
t.integer :amount, null: false
t.string :authorization_code
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

View file

@ -0,0 +1,5 @@
class AddPaymentIdToTicketPurchases < ActiveRecord::Migration
def change
add_column :ticket_purchases, :payment_id, :integer
end
end

View file

@ -250,6 +250,18 @@ ActiveRecord::Schema.define(version: 20160624151257) do
t.datetime "updated_at" t.datetime "updated_at"
end end
create_table "payments", force: :cascade do |t|
t.string "full_name", null: false
t.string "last4"
t.integer "amount", null: false
t.string "authorization_code"
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| create_table "programs", force: :cascade do |t|
t.integer "conference_id" t.integer "conference_id"
t.integer "rating", default: 0 t.integer "rating", default: 0
@ -399,6 +411,7 @@ ActiveRecord::Schema.define(version: 20160624151257) do
t.datetime "created_at" t.datetime "created_at"
t.integer "quantity", default: 1 t.integer "quantity", default: 1
t.integer "user_id" t.integer "user_id"
t.integer "payment_id"
end end
create_table "tickets", force: :cascade do |t| create_table "tickets", force: :cascade do |t|

View file

@ -40,9 +40,8 @@ describe ConferenceRegistrationsController, type: :controller do
get :show, conference_id: conference.short_title get :show, conference_id: conference.short_title
end end
it 'assigns price of purchased tickets to total_price and purchased tickets to tickets' do 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(10000, 'USD') expect(assigns(:total_price)).to eq Money.new(0, 'USD')
expect(assigns(:tickets)).to match_array [@purchased_ticket]
end end
end end

View file

@ -0,0 +1,20 @@
FactoryGirl.define do
factory :payment do
user
conference
full_name { Faker::Hipster.word.to_s }
credit_card_number '4242424242424111'
card_verification_value '123'
expiration_month 6
expiration_year { Date.current.year + 2 }
amount 10
end
trait :invalid_credit_card do
credit_card_number '4242424242424222'
end
trait :exception_credit_card do
credit_card_number '4242424242424333'
end
end

View file

@ -0,0 +1,112 @@
require 'spec_helper'
feature Registration do
let!(:ticket) { create(:ticket) }
let!(:conference) { create(:conference, title: 'ExampleCon', tickets: [ticket], registration_period: create(:registration_period, start_date: 3.days.ago)) }
let!(:participant) { create(:user) }
context 'as a participant' do
before(:each) do
sign_in participant
end
after(:each) do
sign_out
end
context 'who is not registered' do
scenario 'purchases and pays for a ticket, with gateway producing error', 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 purchase tickets.')
purchase = TicketPurchase.where(user_id: participant.id, ticket_id: ticket.id).first
expect(purchase.quantity).to eq(2)
fill_in 'full_name', with: 'foo'
select Date.current.year + 2, from: 'expiration_year'
fill_in 'card_verification_value', with: '123'
fill_in 'credit_card_number', with: '4242424242423333'
click_button 'Charge Card'
expect(Payment.count).to eq(0)
expect(current_path).to eq(conference_conference_registration_path(conference.short_title))
end
scenario 'purchases and pays for a ticket, with card producing a transaction failure', 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 purchase tickets.')
purchase = TicketPurchase.where(user_id: participant.id, ticket_id: ticket.id).first
expect(purchase.quantity).to eq(2)
fill_in 'full_name', with: 'foo'
select Date.current.year + 2, from: 'expiration_year'
fill_in 'card_verification_value', with: '123'
fill_in 'credit_card_number', with: '4242424242422222'
click_button 'Charge Card'
expect(Payment.count).to eq(0)
expect(current_path).to eq(conference_conference_registration_path(conference.short_title))
end
scenario 'purchases and pays for a ticket successfully', 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 purchase tickets.')
purchase = TicketPurchase.where(user_id: participant.id, ticket_id: ticket.id).first
expect(purchase.quantity).to eq(2)
fill_in 'full_name', with: 'foo'
select Date.current.year + 2, from: 'expiration_year'
fill_in 'card_verification_value', with: '123'
fill_in 'credit_card_number', with: '4242424242421111'
click_button 'Charge Card'
expect(current_path).to eq(conference_conference_registration_path(conference.short_title))
expect(Payment.count).to eq(1)
payment = Payment.where(user_id: participant, conference_id: conference.id).first
expect(payment.amount).to eq(20)
expect(payment.status).to eq('success')
expect(payment.first_name).to eq('foo')
expect(payment.last_name).to eq('bar')
expect(payment.last4).not_to be_empty
expect(payment.authorization_code).not_to be_empty
expect(flash).to eq('Thanks! You have purchased your tickets successfully.')
end
end
end
end

View file

@ -26,26 +26,12 @@ feature Registration do
fill_in "tickets__#{ticket.id}", with: '2' fill_in "tickets__#{ticket.id}", with: '2'
expect(current_path).to eq(conference_tickets_path(conference.short_title)) 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 purchase = TicketPurchase.where(user_id: participant.id, ticket_id: ticket.id).first
expect(purchase.quantity).to eq(2) 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 end
end end

View file

@ -0,0 +1,18 @@
require 'spec_helper'
describe PaymentsHelper, type: :helper do
describe '#months' do
it 'returns the correct strings for months' do
expect(months).to match_array(Array([['1 - January', 1], ['2 - February', 2], ['3 - March', 3],
['4 - April', 4], ['5 - May', 5], ['6 - June', 6],
['7 - July', 7], ['8 - August', 8], ['9 - September', 9],
['10 - October', 10], ['11 - November', 11], ['12 - December', 12]]))
end
end
describe '#years' do
it 'returns the correct set of options' do
expect(years).to match_array(Array(Date.current.year..Date.current.year + 15))
end
end
end

0
spec/models/conference_spec.rb Executable file → Normal file
View file

148
spec/models/payment_spec.rb Normal file
View file

@ -0,0 +1,148 @@
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(:full_name) }
it { is_expected.to validate_presence_of(:credit_card_number) }
it { is_expected.to validate_presence_of(:card_verification_value) }
it { is_expected.to validate_presence_of(:expiration_month) }
it { is_expected.to validate_presence_of(:expiration_year) }
it { is_expected.to validate_presence_of(:amount) }
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 '#credit_card' do
let(:payment) { create(:payment) }
it 'assigns correct "month"' do
expect(payment.credit_card.month).to eq(6)
end
it 'assigns correct "year"' do
expect(payment.credit_card.year).to eq(Date.current.year + 2)
end
it 'assigns correct "verification_value"' do
expect(payment.credit_card.verification_value).to eq('123')
end
it 'assigns correct "card_number"' do
expect(payment.credit_card.display_number).to eq('XXXX-XXXX-XXXX-4111')
end
end
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
describe '#purchase' do
let!(:user) { create(:user) }
let!(:ticket_1) { create(:ticket) }
let!(:conference) { create(:conference, tickets: [ticket_1]) }
let!(:payment) { create(:payment, user: user, conference: conference) }
let!(:tickets) { {ticket_1.id.to_s => '1'} }
before { TicketPurchase.purchase(conference, user, tickets) }
context 'when the payment is successful' do
before { payment.purchase }
it 'returns true' do
payment_result = payment.purchase
expect(payment_result).to eq true
end
it "assigns 'success' to payment.status" do
expect(payment.status).to eq('success')
end
it 'assigns last4' do
expect(payment.last4).to eq('XXXX-XXXX-XXXX-4111')
end
it 'assigns authorization_code' do
expect(payment.authorization_code).to eq('53433')
end
end
context 'if the payment is not successful' do
before { payment.purchase }
let(:payment) { create(:payment, :invalid_credit_card) }
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
context 'when there is a connection problem with the gateway' do
let(:payment) { create(:payment, :exception_credit_card) }
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

View file

@ -82,31 +82,72 @@ describe Ticket do
end end
end end
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
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 describe '#quantity_bought_by' do
context 'user has not paid' do
it 'returns the correct value if the user has bought this ticket' do it 'returns the correct value if the user has bought this ticket' do
create(:ticket_purchase, create(:ticket_purchase,
user: user, user: user,
ticket: ticket, ticket: ticket,
quantity: 20) quantity: 20)
expect(ticket.quantity_bought_by(user)).to eq(20) expect(ticket.quantity_bought_by(user, paid: false)).to eq(20)
end end
it 'returns zero if the user has not bought this ticket' do it 'returns zero if the user has not bought this ticket' do
expect(ticket.quantity_bought_by(user)).to eq(0) 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
end end
describe '#total_price' do describe '#total_price' do
context 'user has not paid' do
it 'returns the correct value if the user has bought this ticket' do it 'returns the correct value if the user has bought this ticket' do
create(:ticket_purchase, create(:ticket_purchase,
user: user, user: user,
ticket: ticket, ticket: ticket,
quantity: 20) quantity: 20)
expect(ticket.total_price(user)).to eq(Money.new(100000, 'USD')) expect(ticket.total_price(user, paid: false)).to eq(Money.new(100000, 'USD'))
end end
it 'returns zero if the user has not bought this ticket' do it 'returns zero if the user has not bought this ticket' do
expect(ticket.total_price(user)).to eq(Money.new(0, 'USD')) expect(ticket.total_price(user, paid: false)).to eq(Money.new(0, 'USD'))
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 this ticket' do
expect(ticket.total_price(user, paid: true)).to eq(Money.new(100000, 'USD'))
end
end end
end end
@ -116,7 +157,7 @@ describe Ticket do
describe 'user has bought' do describe 'user has bought' do
context 'no tickets' do context 'no tickets' do
it 'returns zero' 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
end end
@ -126,7 +167,7 @@ describe Ticket do
end end
it 'returns the correct total price' do 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
end end
@ -138,7 +179,7 @@ describe Ticket do
it 'returns the correct total price' do it 'returns the correct total price' do
total_price = Money.new(200000, 'USD') 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 end
end end