summaryrefslogtreecommitdiff
path: root/billing
diff options
context:
space:
mode:
Diffstat (limited to 'billing')
-rw-r--r--billing/Gemfile23
-rw-r--r--billing/README.rdoc24
-rw-r--r--billing/Rakefile40
-rw-r--r--billing/app/controllers/billing_base_controller.rb11
-rw-r--r--billing/app/controllers/credit_card_info_controller.rb34
-rw-r--r--billing/app/controllers/customer_controller.rb61
-rw-r--r--billing/app/controllers/payments_controller.rb46
-rw-r--r--billing/app/controllers/subscriptions_controller.rb45
-rw-r--r--billing/app/helpers/billing_helper.rb20
-rw-r--r--billing/app/helpers/braintree_form_helper.rb64
-rw-r--r--billing/app/helpers/braintree_helper.rb5
-rw-r--r--billing/app/models/customer.rb58
-rw-r--r--billing/app/views/credit_card_info/confirm.html.haml5
-rw-r--r--billing/app/views/credit_card_info/edit.html.haml17
-rw-r--r--billing/app/views/customer/_transaction.html.haml0
-rw-r--r--billing/app/views/customer/confirm.html.haml14
-rw-r--r--billing/app/views/customer/edit.html.haml23
-rw-r--r--billing/app/views/customer/new.html.haml24
-rw-r--r--billing/app/views/customer/show.html.haml22
-rw-r--r--billing/app/views/payments/_customer_data.html.haml15
-rw-r--r--billing/app/views/payments/_non_customer_fields.html.haml16
-rw-r--r--billing/app/views/payments/_transaction_details.html.haml15
-rw-r--r--billing/app/views/payments/confirm.html.haml29
-rw-r--r--billing/app/views/payments/index.html.haml2
-rw-r--r--billing/app/views/payments/new.html.haml18
-rw-r--r--billing/app/views/subscriptions/_subscription_details.html.haml19
-rw-r--r--billing/app/views/subscriptions/create.html.haml9
-rw-r--r--billing/app/views/subscriptions/destroy.html.haml7
-rw-r--r--billing/app/views/subscriptions/index.html.haml7
-rw-r--r--billing/app/views/subscriptions/new.html.haml15
-rw-r--r--billing/app/views/subscriptions/show.html.haml6
-rw-r--r--billing/config/initializers/braintree.rb11
-rw-r--r--billing/config/locales/en.yml5
-rw-r--r--billing/config/routes.rb20
-rw-r--r--billing/leap_web_billing.gemspec22
-rw-r--r--billing/lib/braintree_test_app.rb36
-rw-r--r--billing/lib/leap_web_billing.rb4
-rw-r--r--billing/lib/leap_web_billing/engine.rb13
-rwxr-xr-xbilling/script/rails8
-rw-r--r--billing/test/factories.rb25
-rw-r--r--billing/test/functional/customer_controller_test.rb121
-rw-r--r--billing/test/functional/customers_controller_test.rb59
-rw-r--r--billing/test/functional/payments_controller_test.rb64
-rw-r--r--billing/test/integration/customer_creation_test.rb63
-rw-r--r--billing/test/test_helper.rb15
-rw-r--r--billing/test/unit/customer_test.rb36
-rw-r--r--billing/test/unit/customer_with_payment_info_test.rb37
47 files changed, 1233 insertions, 0 deletions
diff --git a/billing/Gemfile b/billing/Gemfile
new file mode 100644
index 0000000..68ea51b
--- /dev/null
+++ b/billing/Gemfile
@@ -0,0 +1,23 @@
+source "http://rubygems.org"
+
+eval(File.read(File.dirname(__FILE__) + '/../common_dependencies.rb'))
+eval(File.read(File.dirname(__FILE__) + '/../ui_dependencies.rb'))
+
+# We require leap_web_core from here so we can use the path option.
+gem "leap_web_core", :path => '../core'
+
+# Declare your gem's dependencies in billing.gemspec.
+# Bundler will treat runtime dependencies like base dependencies, and
+# development dependencies will be added by default to the :development group.
+gemspec
+
+# jquery-rails is used by the dummy application
+#gem "jquery-rails"
+
+# Declare any dependencies that are still in development here instead of in
+# your gemspec. These might include edge Rails or gems from your path or
+# Git. Remember to move these dependencies to your gemspec before releasing
+# your gem to rubygems.org.
+
+# To use debugger
+# gem 'debugger'
diff --git a/billing/README.rdoc b/billing/README.rdoc
new file mode 100644
index 0000000..357c02e
--- /dev/null
+++ b/billing/README.rdoc
@@ -0,0 +1,24 @@
+= Billing
+
+This project rocks and uses MIT-LICENSE.
+
+To set up your own Braintree Sandbox, create an account at:
+https://www.braintreepayments.com/get-started
+
+Login.
+In the top right, navigate to your username, and then 'My User' -> 'API Keys'
+
+Click the button to generate a new API key, and then click the 'View' link to the right of the key.
+
+There is a section to copy a snippet of code. Select 'Ruby' in the dropdown, and then the button to the right to copy this code to your clipboard.
+Then, paste the contents of the clipboard into config/initializers/braintree.rb
+
+You should not check the private key into version control.
+
+Now, you should be able to add charges to your own Sandbox when you run the webapp locally.
+
+You also will want to add a Plan to your Sandbox. Within the Braintree Sandbox, navigate to 'Recurring Billing' -> 'Plans'. From here, you can add a new Plan. The values of the test plan are not important, but the ID will be displayed, so should pick something descriptive.
+
+Here are credit cared numbers to try in the Sandbox:
+
+https://www.braintreepayments.com/docs/ruby/reference/sandbox \ No newline at end of file
diff --git a/billing/Rakefile b/billing/Rakefile
new file mode 100644
index 0000000..52929c4
--- /dev/null
+++ b/billing/Rakefile
@@ -0,0 +1,40 @@
+#!/usr/bin/env rake
+begin
+ require 'bundler/setup'
+rescue LoadError
+ puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
+end
+begin
+ require 'rdoc/task'
+rescue LoadError
+ require 'rdoc/rdoc'
+ require 'rake/rdoctask'
+ RDoc::Task = Rake::RDocTask
+end
+
+RDoc::Task.new(:rdoc) do |rdoc|
+ rdoc.rdoc_dir = 'rdoc'
+ rdoc.title = 'LeapWebBilling'
+ rdoc.options << '--line-numbers'
+ rdoc.rdoc_files.include('README.rdoc')
+ rdoc.rdoc_files.include('lib/**/*.rb')
+end
+
+spec = eval(File.read('leap_web_billing.gemspec'))
+Gem::PackageTask.new(spec) do |p|
+ p.gem_spec = spec
+end
+
+Bundler::GemHelper.install_tasks
+
+require 'rake/testtask'
+
+Rake::TestTask.new(:test) do |t|
+ t.libs << 'lib'
+ t.libs << 'test'
+ t.pattern = 'test/**/*_test.rb'
+ t.verbose = false
+end
+
+
+task :default => :test
diff --git a/billing/app/controllers/billing_base_controller.rb b/billing/app/controllers/billing_base_controller.rb
new file mode 100644
index 0000000..dc15194
--- /dev/null
+++ b/billing/app/controllers/billing_base_controller.rb
@@ -0,0 +1,11 @@
+class BillingBaseController < ApplicationController
+ before_filter :assign_user
+
+ helper 'billing'
+
+ # required for navigation to work.
+ def assign_user
+ @user = current_user
+ end
+
+end
diff --git a/billing/app/controllers/credit_card_info_controller.rb b/billing/app/controllers/credit_card_info_controller.rb
new file mode 100644
index 0000000..75865fe
--- /dev/null
+++ b/billing/app/controllers/credit_card_info_controller.rb
@@ -0,0 +1,34 @@
+class CreditCardInfoController < ApplicationController
+ before_filter :authorize, :set_user
+
+ def edit
+ @credit_card = Braintree::CreditCard.find(params[:id])
+ customer = Customer.find_by_user_id(current_user.id)
+ if customer and customer.braintree_customer_id == @credit_card.customer_id
+ @tr_data = Braintree::TransparentRedirect.
+ update_credit_card_data(:redirect_url => confirm_credit_card_info_url,
+ :payment_method_token => @credit_card.token)
+ else
+ access_denied
+ end
+
+ end
+
+ def confirm
+ @result = Braintree::TransparentRedirect.confirm(request.query_string)
+ if @result.success?
+ render :action => "confirm"
+ else
+ @credit_card = Braintree::CreditCard.find(@result.params[:payment_method_token])
+ render :action => "edit"
+ end
+ end
+
+
+ private
+
+ def set_user
+ @user = current_user
+ end
+
+end
diff --git a/billing/app/controllers/customer_controller.rb b/billing/app/controllers/customer_controller.rb
new file mode 100644
index 0000000..14ea8a7
--- /dev/null
+++ b/billing/app/controllers/customer_controller.rb
@@ -0,0 +1,61 @@
+class CustomerController < BillingBaseController
+ before_filter :authorize
+ def show
+ customer.with_braintree_data!
+ @default_cc = customer.default_credit_card #TODO not actually right way
+ @active_subscription = customer.subscriptions
+ @transactions = customer.braintree_customer.transactions
+ end
+
+ def new
+ if customer.has_payment_info?
+ redirect_to edit_customer_path(customer), :notice => 'Here is your saved customer data'
+ else
+ fetch_new_transparent_redirect_data
+ end
+ end
+
+ def edit
+ fetch_edit_transparent_redirect_data
+ end
+
+ def confirm
+ @result = Braintree::TransparentRedirect.confirm(request.query_string)
+
+ if @result.success?
+ customer.braintree_customer = @result.customer
+ customer.save
+ render :action => "confirm"
+ elsif customer.has_payment_info?
+ fetch_edit_transparent_redirect_data
+ render :action => "edit"
+ else
+ fetch_new_transparent_redirect_data
+ render :action => "new"
+ end
+ end
+
+ protected
+
+ def fetch_new_transparent_redirect_data
+ @tr_data = Braintree::TransparentRedirect.
+ create_customer_data(:redirect_url => confirm_customer_url)
+ end
+
+ def fetch_edit_transparent_redirect_data
+ customer.with_braintree_data!
+ @default_cc = customer.default_credit_card
+ @tr_data = Braintree::TransparentRedirect.
+ update_customer_data(:redirect_url => confirm_customer_url,
+ :customer_id => customer.braintree_customer_id) ##??
+ end
+
+ def customer
+ @customer ||= Customer.find(params[:id]) if params[:id] # edit, show
+ @customer ||= Customer.find_by_user_id(current_user.id) # confirm
+ @customer ||= Customer.new(user: current_user)
+ # TODO will want case for admins, presumably
+ access_denied unless @customer.user == current_user
+ return @customer
+ end
+end
diff --git a/billing/app/controllers/payments_controller.rb b/billing/app/controllers/payments_controller.rb
new file mode 100644
index 0000000..224b78e
--- /dev/null
+++ b/billing/app/controllers/payments_controller.rb
@@ -0,0 +1,46 @@
+class PaymentsController < BillingBaseController
+ before_filter :authorize, :only => [:index]
+
+ def new
+ fetch_transparent_redirect
+ end
+
+ def confirm
+ @result = Braintree::TransparentRedirect.confirm(request.query_string)
+ if @result.success?
+ render :action => "confirm"
+ else
+ fetch_transparent_redirect
+ render :action => "new"
+ end
+ end
+
+ def index
+ customer = Customer.find_by_user_id(current_user.id)
+ braintree_data = Braintree::Customer.find(customer.braintree_customer_id)
+ # these will be ordered by created_at descending, per http://stackoverflow.com/questions/16425475/
+ @transactions = braintree_data.transactions
+ end
+
+ protected
+
+
+ def fetch_transparent_redirect
+ if @user = current_user #set user for navigation
+ if @customer = Customer.find_by_user_id(current_user.id)
+ @customer.with_braintree_data!
+ braintree_customer_id = @customer.braintree_customer_id
+ @default_cc = @customer.default_credit_card
+ else
+ # TODO: this requires user to add self to vault before making payment. Is that desired functionality?
+ redirect_to new_customer_path, :notice => 'Before making payment, please add your customer data'
+ end
+ end
+
+ # TODO: What is this supposed to do if braintree_customer_id was not set yet?
+ # Response: it can be used to make a payment that is not attributed to any customer (ie, a donation)
+ @tr_data = Braintree::TransparentRedirect.transaction_data redirect_url: confirm_payment_url,
+ transaction: { type: "sale", customer_id: braintree_customer_id, options: {submit_for_settlement: true } }
+ end
+
+end
diff --git a/billing/app/controllers/subscriptions_controller.rb b/billing/app/controllers/subscriptions_controller.rb
new file mode 100644
index 0000000..38dbff1
--- /dev/null
+++ b/billing/app/controllers/subscriptions_controller.rb
@@ -0,0 +1,45 @@
+class SubscriptionsController < BillingBaseController
+ before_filter :authorize
+ before_filter :fetch_subscription, :only => [:show, :destroy]
+ before_filter :confirm_no_active_subscription, :only => [:new, :create]
+
+ def new
+ # don't show link to subscribe if they are already subscribed?
+ credit_card = @customer.default_credit_card #safe to assume default?
+ @payment_method_token = credit_card.token
+ @plans = Braintree::Plan.all
+ end
+
+ # show has no content, so not needed at this point.
+
+ def create
+ @result = Braintree::Subscription.create( :payment_method_token => params[:payment_method_token], :plan_id => params[:plan_id] )
+ end
+
+ def destroy
+ @result = Braintree::Subscription.cancel params[:id]
+ end
+
+ def index
+ customer = Customer.find_by_user_id(current_user.id)
+ @subscriptions = customer.subscriptions(nil, false)
+ end
+
+ private
+
+ def fetch_subscription
+ @subscription = Braintree::Subscription.find params[:id]
+ @subscription_customer_id = @subscription.transactions.first.customer_details.id #all of subscriptions transactions should have same customer
+ @customer = Customer.find_by_user_id(current_user.id)
+ access_denied unless @customer and @customer.braintree_customer_id == @subscription_customer_id
+ # TODO: will presumably want to allow admins to view/cancel subscriptions for all users
+ end
+
+ def confirm_no_active_subscription
+ @customer = Customer.find_by_user_id(current_user.id)
+ if subscription = @customer.subscriptions # will return active subscription, if it exists
+ redirect_to subscription_path(subscription.id), :notice => 'You already have an active subscription'
+ end
+ end
+
+end
diff --git a/billing/app/helpers/billing_helper.rb b/billing/app/helpers/billing_helper.rb
new file mode 100644
index 0000000..7ec9285
--- /dev/null
+++ b/billing/app/helpers/billing_helper.rb
@@ -0,0 +1,20 @@
+module BillingHelper
+
+ def braintree_form_for(object, options = {}, &block)
+ options.reverse_merge! params: @result && @result.params[object],
+ errors: @result && @result.errors.for(object),
+ builder: BraintreeFormHelper::BraintreeFormBuilder,
+ url: Braintree::TransparentRedirect.url
+
+ form_for object, options, &block
+ end
+
+ def show_or_new_customer_link(user)
+ if (customer = Customer.find_by_user_id(user.id)) and customer.has_payment_info?
+ show_customer_path(user)
+ else
+ new_customer_path
+ end
+ end
+
+end
diff --git a/billing/app/helpers/braintree_form_helper.rb b/billing/app/helpers/braintree_form_helper.rb
new file mode 100644
index 0000000..cb322fa
--- /dev/null
+++ b/billing/app/helpers/braintree_form_helper.rb
@@ -0,0 +1,64 @@
+module BraintreeFormHelper
+ class BraintreeFormBuilder < ActionView::Helpers::FormBuilder
+ include ActionView::Helpers::AssetTagHelper
+ include ActionView::Helpers::TagHelper
+
+ def initialize(object_name, object, template, options, proc)
+ super
+ @braintree_params = @options[:params]
+ @braintree_errors = @options[:errors]
+ @braintree_existing = @options[:existing]
+ end
+
+ def fields_for(record_name, *args, &block)
+ options = args.extract_options!
+ options[:builder] = BraintreeFormBuilder
+ options[:params] = @braintree_params && @braintree_params[record_name]
+ options[:errors] = @braintree_errors && @braintree_errors.for(record_name)
+ new_args = args + [options]
+ super record_name, *new_args, &block
+ end
+
+ def text_field(method, options = {})
+ has_errors = @braintree_errors && @braintree_errors.on(method).any?
+ field = super(method, options.merge(:value => determine_value(method)))
+ result = content_tag("div", field, :class => has_errors ? "fieldWithErrors" : "")
+ result.safe_concat validation_errors(method)
+ result
+ end
+
+ protected
+
+ def determine_value(method)
+ if @braintree_params
+ @braintree_params[method]
+ elsif @braintree_existing
+
+ if @braintree_existing.kind_of?(Braintree::CreditCard)
+
+ case method
+ when :number
+ method = :masked_number
+ when :cvv
+ return nil
+ end
+ end
+
+ @braintree_existing.send(method)
+ else
+ nil
+ end
+ end
+
+ def validation_errors(method)
+ if @braintree_errors && @braintree_errors.on(method).any?
+ @braintree_errors.on(method).map do |error|
+ content_tag("div", ERB::Util.h(error.message), {:style => "color: red;"})
+ end.join
+ else
+ ""
+ end
+ end
+ end
+end
+
diff --git a/billing/app/helpers/braintree_helper.rb b/billing/app/helpers/braintree_helper.rb
new file mode 100644
index 0000000..2d18b6c
--- /dev/null
+++ b/billing/app/helpers/braintree_helper.rb
@@ -0,0 +1,5 @@
+module BraintreeHelper
+
+
+end
+
diff --git a/billing/app/models/customer.rb b/billing/app/models/customer.rb
new file mode 100644
index 0000000..f01c300
--- /dev/null
+++ b/billing/app/models/customer.rb
@@ -0,0 +1,58 @@
+class Customer < CouchRest::Model::Base
+
+ FIELDS = [:first_name, :last_name, :phone, :website, :company, :fax, :addresses, :credit_cards, :custom_fields]
+ attr_accessor *FIELDS
+
+ use_database "customers"
+ belongs_to :user
+ belongs_to :braintree_customer
+
+ # Braintree::Customer - stored on braintrees servers - we only have the id.
+ def braintree_customer
+ @braintree_customer ||= Braintree::Customer.find(braintree_customer_id)
+ end
+
+ validates :user, presence: true
+
+ design do
+ view :by_user_id
+ view :by_braintree_customer_id
+ end
+
+ def has_payment_info?
+ !!braintree_customer_id
+ end
+
+ # from braintree_ruby_examples/rails3_tr_devise and should be tweaked
+ def with_braintree_data!
+ return self unless has_payment_info?
+
+ FIELDS.each do |field|
+ send(:"#{field}=", braintree_customer.send(field))
+ end
+ self
+ end
+
+ def default_credit_card
+ return unless has_payment_info?
+
+ credit_cards.find { |cc| cc.default? }
+ end
+
+ # based on 2nd parameter, either returns the single active subscription (or nil if there isn't one), or an array of all subsciptions
+ def subscriptions(braintree_data=nil, only_active=true)
+ self.with_braintree_data!
+ return unless has_payment_info?
+
+ subscriptions = []
+ self.default_credit_card.subscriptions.each do |sub|
+ if only_active and sub.status == 'Active'
+ return sub
+ else
+ subscriptions << sub
+ end
+ end
+ only_active ? nil : subscriptions
+ end
+
+end
diff --git a/billing/app/views/credit_card_info/confirm.html.haml b/billing/app/views/credit_card_info/confirm.html.haml
new file mode 100644
index 0000000..9dd8176
--- /dev/null
+++ b/billing/app/views/credit_card_info/confirm.html.haml
@@ -0,0 +1,5 @@
+%h1 Payment Info Confirmation
+%p Your payment information was successfully saved.
+%dl
+ %dt Credit Card
+ %dd= @result.credit_card.masked_number
diff --git a/billing/app/views/credit_card_info/edit.html.haml b/billing/app/views/credit_card_info/edit.html.haml
new file mode 100644
index 0000000..39269ca
--- /dev/null
+++ b/billing/app/views/credit_card_info/edit.html.haml
@@ -0,0 +1,17 @@
+%h1 Change Credit Card
+- if @result
+ #total-errors{:style => "color:red;"}
+ = h(@result.errors.size)
+ error(s)
+= braintree_form_for :credit_card, :existing => @credit_card do |f|
+ = field_set_tag "Credit Card" do
+ %dl
+ %dt= f.label :number, 'Number'
+ %dd= f.text_field :number
+ %dt= f.label :expiration_date, 'Expiration Date (MM/YY)'
+ %dd= f.text_field :expiration_date
+ %dt= f.label :cvv, 'CVV'
+ %dd= f.text_field :cvv
+ = hidden_field_tag :tr_data, @tr_data
+ = f.submit 'Save Payment Info', :class => :btn
+ = link_to t(:cancel), edit_customer_path(@credit_card.customer_id), :class => :btn
diff --git a/billing/app/views/customer/_transaction.html.haml b/billing/app/views/customer/_transaction.html.haml
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/billing/app/views/customer/_transaction.html.haml
diff --git a/billing/app/views/customer/confirm.html.haml b/billing/app/views/customer/confirm.html.haml
new file mode 100644
index 0000000..5551622
--- /dev/null
+++ b/billing/app/views/customer/confirm.html.haml
@@ -0,0 +1,14 @@
+%h1 Payment Info Confirmation
+%p Your payment information was successfully saved.
+%dl
+ %dt First Name
+ %dd= @result.customer.first_name
+ %dt Last Name
+ %dd= @result.customer.last_name
+ %dt Phone
+ %dd= @result.customer.phone
+ %dt Credit Card
+ - @result.customer.credit_cards.each do |cc|
+ %dd= cc.masked_number
+- customer = Customer.find_by_user_id(current_user.id)
+= link_to 'View Customer Info', show_customer_path(customer.braintree_customer_id), :class=> :btn \ No newline at end of file
diff --git a/billing/app/views/customer/edit.html.haml b/billing/app/views/customer/edit.html.haml
new file mode 100644
index 0000000..8a232c5
--- /dev/null
+++ b/billing/app/views/customer/edit.html.haml
@@ -0,0 +1,23 @@
+- if @result
+ #total-errors{:style => "color:red;"}
+ = h(@result.errors.size)
+ error(s)
+= braintree_form_for :customer, existing: @customer do |f|
+ = field_set_tag "Customer" do
+ %dl
+ %dt= f.label :first_name, 'First Name'
+ %dd= f.text_field :first_name
+ %dt= f.label :last_name, 'Last Name'
+ %dd= f.text_field :last_name
+ %dt= f.label :phone, 'Phone'
+ %dd= f.text_field :phone
+ - if @default_cc
+ = # todo, as they will need a credit card, so not sure about conditional?
+ %dt= t(:stored_credit_card)
+ %dd
+ = @default_cc.masked_number
+ = link_to t(:change_credit_card), edit_credit_card_info_path(:id => @default_cc.token), :class => :btn
+ = hidden_field_tag :tr_data, @tr_data
+ .form-actions
+ = f.submit t(:save_customer_info), :class => 'btn btn-primary'
+ = link_to t(:cancel), show_customer_path(@customer), :class=> :btn
diff --git a/billing/app/views/customer/new.html.haml b/billing/app/views/customer/new.html.haml
new file mode 100644
index 0000000..e1f5ba9
--- /dev/null
+++ b/billing/app/views/customer/new.html.haml
@@ -0,0 +1,24 @@
+- if @result
+ #total-errors{:style => "color:red;"}
+ = h(@result.errors.size)
+ error(s)
+= braintree_form_for :customer do |f|
+ = field_set_tag "Customer" do
+ %dl
+ %dt= f.label :first_name, 'First Name'
+ %dd= f.text_field :first_name
+ %dt= f.label :last_name, 'Last Name'
+ %dd= f.text_field :last_name
+ %dt= f.label :phone, 'Phone'
+ %dd= f.text_field :phone
+ = field_set_tag "Credit Card" do
+ - f.fields_for :credit_card do |cc|
+ %dl
+ %dt= cc.label :number, 'Number'
+ %dd= cc.text_field :number
+ %dt= cc.label :expiration_date, 'Expiration Date (MM/YY)'
+ %dd= cc.text_field :expiration_date
+ %dt= cc.label :cvv, 'CVV'
+ %dd= cc.text_field :cvv
+ = hidden_field_tag :tr_data, @tr_data
+ = f.submit 'Save Payment Info'
diff --git a/billing/app/views/customer/show.html.haml b/billing/app/views/customer/show.html.haml
new file mode 100644
index 0000000..639d180
--- /dev/null
+++ b/billing/app/views/customer/show.html.haml
@@ -0,0 +1,22 @@
+.form-actions
+ = link_to t(:make_payment), new_payment_path, :class => 'btn btn-primary'
+= render :partial => 'payments/customer_data'
+%legend= t(:last_three_transactions)
+- counter = 0
+= # these will be ordered with most recently created first, per http://stackoverflow.com/questions/16425475/
+- @transactions.each do |t|
+ - break if counter > 2 # not ruby-like, but object is a Braintree::ResourceCollection so limited methods available
+ = render :partial => "payments/transaction_details", :locals => {:transaction => t}
+ - counter += 1
+= link_to t(:transaction_history), payments_path
+%legend= t(:subscriptions)
+- if @active_subscription
+ = render :partial => "subscriptions/subscription_details", :locals => {:subscription => @active_subscription}
+- else
+ %p
+ = t(:no_active_subscription)
+ %p
+ .form-actions
+ = link_to t(:subscribe_to_plan), new_subscription_path, :class => :btn
+%p
+ = link_to t(:all_subscriptions), subscriptions_path
diff --git a/billing/app/views/payments/_customer_data.html.haml b/billing/app/views/payments/_customer_data.html.haml
new file mode 100644
index 0000000..87b8209
--- /dev/null
+++ b/billing/app/views/payments/_customer_data.html.haml
@@ -0,0 +1,15 @@
+%legend= t(:customer_information)
+%dl
+ %dt First Name
+ %dd= @customer.first_name
+ %dt Last Name
+ %dd= @customer.last_name
+ %dt Phone
+ %dd= @customer.phone
+%legend= t(:credit_card_information)
+%dl
+ %dt Number
+ %dd= @default_cc.masked_number
+ %dt Expiration Date
+ %dd= @default_cc.expiration_date
+ = link_to t(:edit_saved_data), edit_customer_path(@customer), :class => :btn
diff --git a/billing/app/views/payments/_non_customer_fields.html.haml b/billing/app/views/payments/_non_customer_fields.html.haml
new file mode 100644
index 0000000..99b420d
--- /dev/null
+++ b/billing/app/views/payments/_non_customer_fields.html.haml
@@ -0,0 +1,16 @@
+= field_set_tag "Customer" do
+ = f.fields_for :customer do |c|
+ %div= c.label :first_name, "First Name"
+ %div= c.text_field :first_name
+ %div= c.label :last_name, "Last Name"
+ %div= c.text_field :last_name
+ %div= c.label :email, "Email"
+ %div= c.text_field :email
+= field_set_tag "Credit Card" do
+ = f.fields_for :credit_card do |c|
+ %div= c.label :number, "Number"
+ %div= c.text_field :number
+ %div= c.label :expiration_date, "Expiration Date (MM/YY)"
+ %div= c.text_field :expiration_date
+ %div= c.label :cvv, "CVV"
+ %div= c.text_field :cvv \ No newline at end of file
diff --git a/billing/app/views/payments/_transaction_details.html.haml b/billing/app/views/payments/_transaction_details.html.haml
new file mode 100644
index 0000000..030639e
--- /dev/null
+++ b/billing/app/views/payments/_transaction_details.html.haml
@@ -0,0 +1,15 @@
+%p
+ = transaction.id
+ Type:
+ = transaction.type
+ Amount:
+ = number_to_currency(transaction.amount)
+ Status:
+ = transaction.status
+ Date
+ = transaction.created_at.strftime("%Y-%m-%d")
+ - if sub_start = transaction.subscription_details.billing_period_start_date
+ From subscription which started
+ = sub_start
+ - else
+ Not paid as part of subscription \ No newline at end of file
diff --git a/billing/app/views/payments/confirm.html.haml b/billing/app/views/payments/confirm.html.haml
new file mode 100644
index 0000000..9479eb9
--- /dev/null
+++ b/billing/app/views/payments/confirm.html.haml
@@ -0,0 +1,29 @@
+%h1 Payment Result
+%div Thank you for your payment.
+%h2 Transaction Details
+%table
+ %tr
+ %td Amount
+ %td
+ $#{@result.transaction.amount}
+ %tr
+ %td Transaction ID:
+ %td= @result.transaction.id
+ %tr
+ %td First Name:
+ %td= h @result.transaction.customer_details.first_name
+ %tr
+ %td Last Name:
+ %td= h @result.transaction.customer_details.last_name
+ %tr
+ %td Email:
+ %td= h @result.transaction.customer_details.email
+ %tr
+ %td Credit Card:
+ %td= h @result.transaction.credit_card_details.masked_number
+ %tr
+ %td Card Type:
+ %td= h @result.transaction.credit_card_details.card_type
+- if current_user
+ - customer = Customer.find_by_user_id(current_user.id)
+ = link_to 'View Customer Info', show_customer_path(customer.braintree_customer_id), :class=> :btn \ No newline at end of file
diff --git a/billing/app/views/payments/index.html.haml b/billing/app/views/payments/index.html.haml
new file mode 100644
index 0000000..a3ad067
--- /dev/null
+++ b/billing/app/views/payments/index.html.haml
@@ -0,0 +1,2 @@
+- @transactions.each do |t|
+ = render :partial => "transaction_details", :locals => {:transaction => t} \ No newline at end of file
diff --git a/billing/app/views/payments/new.html.haml b/billing/app/views/payments/new.html.haml
new file mode 100644
index 0000000..4523e13
--- /dev/null
+++ b/billing/app/views/payments/new.html.haml
@@ -0,0 +1,18 @@
+%h1
+ = t(:payment)
+- if @result and @result.errors.size > 0
+ %div{:style => "color: red;"}
+ = h @result.errors.size
+ error(s)
+- if @result and @result.transaction and @result.transaction.status == 'processor_declined'
+ %div{:style => "color: red;"}
+ = t(:processor_declined)
+= braintree_form_for :transaction, :html => {:autocomplete => "off"} do |f|
+ = f.label :amount, t(:amount)
+ = f.text_field :amount
+ - if !@customer
+ = render :partial => 'non_customer_fields', :locals => {:f => f}
+ - else
+ = render :partial => 'customer_data'
+ = hidden_field_tag :tr_data, @tr_data
+ = f.submit "Submit Payment", :class => 'btn btn-primary'
diff --git a/billing/app/views/subscriptions/_subscription_details.html.haml b/billing/app/views/subscriptions/_subscription_details.html.haml
new file mode 100644
index 0000000..fb18210
--- /dev/null
+++ b/billing/app/views/subscriptions/_subscription_details.html.haml
@@ -0,0 +1,19 @@
+%p
+ = link_to subscription.id, subscription_path(subscription.id)
+ Balance:
+ = number_to_currency(subscription.balance)
+ Bill on:
+ = subscription.billing_day_of_month
+ Start date:
+ = subscription.first_billing_date
+ Paid through:
+ = subscription.paid_through_date
+ Plan:
+ = subscription.plan_id
+ Price:
+ = subscription.price
+ - color = (subscription.status == 'Active') ? "green" : "red"
+ Status:
+ %font{:color => color}
+ = subscription.status
+ - # would be good to get plan name but not sure if that is possible? \ No newline at end of file
diff --git a/billing/app/views/subscriptions/create.html.haml b/billing/app/views/subscriptions/create.html.haml
new file mode 100644
index 0000000..2b6c5e9
--- /dev/null
+++ b/billing/app/views/subscriptions/create.html.haml
@@ -0,0 +1,9 @@
+- if @result.success?
+ %h1
+ Subscription Status
+ = @result.subscription.status
+ = render :partial => "subscription_details", :locals => {:subscription => @result.subscription}
+- else
+ %h1
+ Error:
+ = @result.message \ No newline at end of file
diff --git a/billing/app/views/subscriptions/destroy.html.haml b/billing/app/views/subscriptions/destroy.html.haml
new file mode 100644
index 0000000..e7ed6e8
--- /dev/null
+++ b/billing/app/views/subscriptions/destroy.html.haml
@@ -0,0 +1,7 @@
+- if @result.success?
+ Subscription destroyed
+- else
+ Error:
+ = @result.message
+%p
+ = link_to 'Customer Information', show_customer_path(@customer.braintree_customer_id), :class=> :btn \ No newline at end of file
diff --git a/billing/app/views/subscriptions/index.html.haml b/billing/app/views/subscriptions/index.html.haml
new file mode 100644
index 0000000..0e84619
--- /dev/null
+++ b/billing/app/views/subscriptions/index.html.haml
@@ -0,0 +1,7 @@
+- active = false
+- @subscriptions.each do |s|
+ - if s.status == 'Active'
+ - active = true
+ = render :partial => "subscription_details", :locals => {:subscription => s}
+- if !active
+ = link_to 'subscribe to plan', new_subscription_path, :class => :btn \ No newline at end of file
diff --git a/billing/app/views/subscriptions/new.html.haml b/billing/app/views/subscriptions/new.html.haml
new file mode 100644
index 0000000..4183458
--- /dev/null
+++ b/billing/app/views/subscriptions/new.html.haml
@@ -0,0 +1,15 @@
+- if @payment_method_token
+ %h1
+ Subscribe to plan
+ = #currently just one plan
+ = @plans[0].name
+ = number_to_currency(@plans[0].price)
+ = simple_form_for :subscription, :url => :subscriptions do |f|
+ = hidden_field_tag :payment_method_token, @payment_method_token
+ = hidden_field_tag :plan_id, @plans[0].id
+ .form-actions
+ = f.submit t(:subscribe), :class => 'btn btn-primary'
+- else
+ = t(:must_create_customer)
+ %p
+ = link_to t(:create_new_customer), new_customer_path
diff --git a/billing/app/views/subscriptions/show.html.haml b/billing/app/views/subscriptions/show.html.haml
new file mode 100644
index 0000000..10eb667
--- /dev/null
+++ b/billing/app/views/subscriptions/show.html.haml
@@ -0,0 +1,6 @@
+%h1
+ - if @subscription.status == 'Active'
+ Current
+ Subscription
+= render :partial => "subscription_details", :locals => {:subscription => @subscription}
+= link_to t(:cancel_subscription), subscription_path, :confirm => t(:are_you_sure), :method => :delete, :class => 'btn btn-danger' if @subscription.status == 'Active' # permission check or should that just be on show?
diff --git a/billing/config/initializers/braintree.rb b/billing/config/initializers/braintree.rb
new file mode 100644
index 0000000..523dbce
--- /dev/null
+++ b/billing/config/initializers/braintree.rb
@@ -0,0 +1,11 @@
+require 'braintree_test_app'
+
+Braintree::Configuration.logger = Logger.new('log/braintree.log')
+Braintree::Configuration.environment = :sandbox
+Braintree::Configuration.merchant_id = "bwrdyczvjspmxjhb"
+Braintree::Configuration.public_key = "jmw58nbmjg84prbp"
+Braintree::Configuration.private_key = "SET_ME"
+
+if Rails.env.test?
+ Rails.application.config.middleware.use BraintreeTestApp
+end
diff --git a/billing/config/locales/en.yml b/billing/config/locales/en.yml
new file mode 100644
index 0000000..5245b17
--- /dev/null
+++ b/billing/config/locales/en.yml
@@ -0,0 +1,5 @@
+en:
+ create_new_customer: "Create a new Braintree Customer"
+ must_create_customer: "You must store a customer in braintree before subscribing to a plan"
+ subscribe: "Subscribe"
+ save_customer_info: "Save Customer Information" \ No newline at end of file
diff --git a/billing/config/routes.rb b/billing/config/routes.rb
new file mode 100644
index 0000000..1bb32df
--- /dev/null
+++ b/billing/config/routes.rb
@@ -0,0 +1,20 @@
+Rails.application.routes.draw do
+
+ match 'payments/new' => 'payments#new', :as => :new_payment
+ match 'payments/confirm' => 'payments#confirm', :as => :confirm_payment
+ resources :payments, :only => [:index]
+
+ resources :customer, :only => [:new, :edit]
+ resources :credit_card_info, :only => [:edit]
+
+ match 'customer/confirm/' => 'customer#confirm', :as => :confirm_customer
+ match 'customer/show/:id' => 'customer#show', :as => :show_customer
+ match 'credit_card_info/confirm' => 'credit_card_info#confirm', :as => :confirm_credit_card_info
+
+ resources :subscriptions, :only => [:new, :create, :index, :show, :update, :destroy]
+
+ #match 'transactions/:product_id/new' => 'transactions#new', :as => :new_transaction
+ #match 'transactions/confirm/:product_id' => 'transactions#confirm', :as => :confirm_transaction
+
+
+end
diff --git a/billing/leap_web_billing.gemspec b/billing/leap_web_billing.gemspec
new file mode 100644
index 0000000..17f7f92
--- /dev/null
+++ b/billing/leap_web_billing.gemspec
@@ -0,0 +1,22 @@
+$:.push File.expand_path("../lib", __FILE__)
+
+require File.expand_path('../../lib/leap_web/version.rb', __FILE__)
+
+# Describe your gem and declare its dependencies:
+Gem::Specification.new do |s|
+ s.name = "leap_web_billing"
+ s.version = LeapWeb::VERSION
+ s.authors = ["Jessib"]
+ s.email = ["jessib@leap.se"]
+ s.homepage = "http://www.leap.se"
+ s.summary = "Billing for LeapWeb"
+ s.description = "Billing System for a Leap provider"
+
+ s.files = Dir["{app,config,db,lib}/**/*"] + ["Rakefile", "Readme.md"]
+ s.test_files = Dir["test/**/*"]
+
+ s.add_dependency "leap_web_core", LeapWeb::VERSION
+ # s.add_dependency "braintree-rails", "~> 0.4.5"
+ s.add_dependency "braintree"
+ #s.add_dependency "carmen-rails"
+end
diff --git a/billing/lib/braintree_test_app.rb b/billing/lib/braintree_test_app.rb
new file mode 100644
index 0000000..41c327d
--- /dev/null
+++ b/billing/lib/braintree_test_app.rb
@@ -0,0 +1,36 @@
+# RackTest assumes all requests to be local.
+# Braintree requests need to go out to a different server though.
+# So we use a middleware to catch these and send them out again.
+
+class BraintreeTestApp
+ def initialize(app)
+ @app = app
+ end
+
+ def call(env)
+ @env = env
+ config = Braintree::Configuration.instantiate
+ if request.path =~ /\/merchants\/#{config.merchant_id}\/transparent_redirect_requests$/
+ #proxy post to braintree
+ uri = URI.parse(config.protocol + "://" + config.server + ":" +
+ config.port.to_s + request.path)
+ http = Net::HTTP.new(uri.host, uri.port)
+ res = http.post(uri.path, request.body.read)
+
+ if res.code == "303"
+ header_hash = res.header.to_hash
+ header_hash["location"].first.gsub!("http://localhost:3000/", "http://www.example.com/")
+ [303, {"location" => header_hash["location"].first}, ""]
+ else
+ raise "unexpected response from Braintree: expected a 303"
+ end
+ else
+ @app.call(env)
+ end
+ end
+
+ def request
+ @request = Rack::Request.new(@env)
+ end
+end
+
diff --git a/billing/lib/leap_web_billing.rb b/billing/lib/leap_web_billing.rb
new file mode 100644
index 0000000..288d846
--- /dev/null
+++ b/billing/lib/leap_web_billing.rb
@@ -0,0 +1,4 @@
+require "leap_web_billing/engine"
+
+module LeapWebBilling
+end
diff --git a/billing/lib/leap_web_billing/engine.rb b/billing/lib/leap_web_billing/engine.rb
new file mode 100644
index 0000000..6d76add
--- /dev/null
+++ b/billing/lib/leap_web_billing/engine.rb
@@ -0,0 +1,13 @@
+# thou shall require all your dependencies in an engine.
+require "leap_web_core"
+require "leap_web_core/ui_dependencies"
+
+#require "braintree-rails"
+require "braintree"
+#require "carmen-rails"
+
+module LeapWebBilling
+ class Engine < ::Rails::Engine
+
+ end
+end
diff --git a/billing/script/rails b/billing/script/rails
new file mode 100755
index 0000000..8bd9c0a
--- /dev/null
+++ b/billing/script/rails
@@ -0,0 +1,8 @@
+#!/usr/bin/env ruby
+# This command will automatically be run when you run "rails" with Rails 3 gems installed from the root of your application.
+
+ENGINE_ROOT = File.expand_path('../..', __FILE__)
+ENGINE_PATH = File.expand_path('../../lib/leap_web_billing/engine', __FILE__)
+
+require 'rails/all'
+require 'rails/engine/commands'
diff --git a/billing/test/factories.rb b/billing/test/factories.rb
new file mode 100644
index 0000000..87543b2
--- /dev/null
+++ b/billing/test/factories.rb
@@ -0,0 +1,25 @@
+FactoryGirl.define do
+
+ TEST_CC_NUMBER = %w(4111 1111 1111 1111).join
+
+ factory :customer do
+ user
+
+ factory :customer_with_payment_info do
+ braintree_customer
+ end
+ end
+
+ factory :braintree_customer, class: Braintree::Customer do
+ first_name 'Big'
+ last_name 'Spender'
+ credit_card number: TEST_CC_NUMBER, expiration_date: '04/2016'
+ initialize_with { Braintree::Customer.create(attributes).customer }
+ skip_create
+
+ factory :broken_customer do
+ credit_card number: '123456', expiration_date: '04/2016'
+ end
+ end
+
+end
diff --git a/billing/test/functional/customer_controller_test.rb b/billing/test/functional/customer_controller_test.rb
new file mode 100644
index 0000000..9bf2b5e
--- /dev/null
+++ b/billing/test/functional/customer_controller_test.rb
@@ -0,0 +1,121 @@
+require 'test_helper'
+require 'fake_braintree'
+
+class CustomerControllerTest < ActionController::TestCase
+
+ test "new assigns redirect url" do
+ login
+ get :new
+
+ assert_response :success
+ assert assigns(:tr_data)
+ tr_data = Braintree::Util.parse_query_string(assigns(:tr_data))
+ assert_equal confirm_customer_url, tr_data[:redirect_url]
+ end
+
+ test "new requires login" do
+ get :new
+
+ assert_response :redirect
+ assert_redirected_to login_path
+ end
+
+ test "edit uses params[:id]" do
+ customer = FactoryGirl.create :customer_with_payment_info
+ login customer.user
+ get :edit, id: customer.id
+
+ assert_response :success
+ assert assigns(:tr_data)
+ tr_data = Braintree::Util.parse_query_string(assigns(:tr_data))
+ assert_equal customer.braintree_customer_id, tr_data[:customer_id]
+ assert_equal confirm_customer_url, tr_data[:redirect_url]
+ end
+
+ test "confirm user creation" do
+ login
+ Braintree::TransparentRedirect.expects(:confirm).returns(success_response)
+ # to_confirm = prepare_confirmation :create_customer_data,
+ # customer: FactoryGirl.attributes_for(:braintree_customer),
+ # redirect_url: confirm_customer_url
+
+ assert_difference("Customer.count") do
+ post :confirm, braintree: :query
+ end
+
+ assert_response :success
+ assert result = assigns(:result)
+ assert result.success?
+ assert result.customer.id
+ end
+
+ test "customer update" do
+ customer = FactoryGirl.create :customer_with_payment_info
+ login customer.user
+ Braintree::TransparentRedirect.expects(:confirm).
+ returns(success_response(customer))
+
+ assert_no_difference("Customer.count") do
+ post :confirm, query: :from_braintree
+ end
+
+ assert_response :success
+ assert result = assigns(:result)
+ assert result.success?
+ assert_equal customer.braintree_customer, result.customer
+ end
+
+ test "failed user creation" do
+ skip "can't get user creation to fail"
+ login
+ FakeBraintree.decline_all_cards!
+ to_confirm = prepare_confirmation :create_customer_data,
+ customer: FactoryGirl.attributes_for(:broken_customer),
+ redirect_url: confirm_customer_url
+ post :confirm, to_confirm
+
+ FakeBraintree.clear!
+ assert_response :success
+ assert result = assigns(:result)
+ assert !result.success?
+ end
+
+ test "failed user creation with stubbing" do
+ login
+ Braintree::TransparentRedirect.expects(:confirm).returns(failure_response)
+ post :confirm, bla: :blub
+
+ assert_response :success
+ assert_template :new
+ end
+
+ test "failed user update with stubbing" do
+ customer = FactoryGirl.create :customer_with_payment_info
+ login customer.user
+ Braintree::TransparentRedirect.expects(:confirm).returns(failure_response)
+ post :confirm, bla: :blub
+
+ assert_response :success
+ assert_template :edit
+ end
+
+ def failure_response
+ stub success?: false,
+ errors: stub(for: nil, size: 0),
+ params: {}
+ end
+
+ def success_response(customer = nil)
+ stub success?: true,
+ customer: braintree_customer(customer)
+ end
+
+ def braintree_customer(customer)
+ if customer
+ customer.braintree_customer
+ else
+ FactoryGirl.build :braintree_customer
+ end
+ end
+
+end
diff --git a/billing/test/functional/customers_controller_test.rb b/billing/test/functional/customers_controller_test.rb
new file mode 100644
index 0000000..58b6155
--- /dev/null
+++ b/billing/test/functional/customers_controller_test.rb
@@ -0,0 +1,59 @@
+require 'test_helper'
+require 'fake_braintree'
+
+class CustomersControllerTest < ActionController::TestCase
+ tests CustomerController
+
+ setup do
+ @user = FactoryGirl.create :user
+ @other_user = FactoryGirl.create :user
+ FakeBraintree.clear!
+ FakeBraintree.verify_all_cards!
+ testid = 'testid'
+ FakeBraintree::Customer.new({:credit_cards => [{:number=>"5105105105105100", :expiration_date=>"05/2013"}]}, {:id => testid, :merchant_id => Braintree::Configuration.merchant_id})
+ # any reason to call the create instance method on the FakeBraintree::Customer ?
+ @customer = Customer.new(:user_id => @other_user.id)
+ @customer.braintree_customer_id = testid
+ @customer.save
+
+ end
+
+ teardown do
+ @user.destroy
+ @other_user.destroy
+ @customer.destroy
+ end
+
+ test "no access if not logged in" do
+ get :new
+ assert_access_denied(true, false)
+ get :show, :id => @customer.braintree_customer_id
+ assert_access_denied(true, false)
+ get :edit, :id => @customer.braintree_customer_id
+ assert_access_denied(true, false)
+ end
+
+
+ test "should get new if logged in and not customer" do
+ login @user
+ get :new
+ assert_not_nil assigns(:tr_data)
+ assert_response :success
+ end
+
+ test "new should direct edit if user is already a customer" do
+ login @other_user
+ get :new
+ assert_response :redirect
+ assert_equal edit_customer_url(@customer), response.header['Location']
+ end
+
+
+ test "show" do
+ login @other_user
+ # Below will fail, as when we go to fetch the customer data, Braintree::Customer.find(params[:id]) won't find the customer as it is a FakeBraintree customer.
+ #get :show, :id => @customer.braintree_customer_id
+
+ end
+
+end
diff --git a/billing/test/functional/payments_controller_test.rb b/billing/test/functional/payments_controller_test.rb
new file mode 100644
index 0000000..8f3bfe7
--- /dev/null
+++ b/billing/test/functional/payments_controller_test.rb
@@ -0,0 +1,64 @@
+require 'test_helper'
+require 'fake_braintree'
+
+class PaymentsControllerTest < ActionController::TestCase
+
+ test "payment when unauthorized" do
+ get :new
+ assert_not_nil assigns(:tr_data)
+ assert_response :success
+ end
+
+ test "authenticated user must create account before making payment" do
+ login
+ get :new
+ assert_response :redirect
+ assert_equal new_customer_url, response.header['Location']
+ end
+
+ test "payment when authenticated as customer" do
+ customer = FactoryGirl.create :customer_with_payment_info
+ login customer.user
+ get :new
+ assert_not_nil assigns(:tr_data)
+ assert_response :success
+ end
+
+ test "successful confirmation renders confirm" do
+ Braintree::TransparentRedirect.expects(:confirm).returns(success_response)
+ get :confirm
+
+ assert_response :success
+ assert_template :confirm
+ end
+
+ test "failed confirmation renders new" do
+ Braintree::TransparentRedirect.expects(:confirm).returns(failure_response)
+ get :confirm
+
+ assert_response :success
+ assert_not_nil assigns(:tr_data)
+ assert_template :new
+ end
+
+ def failure_response
+ stub success?: false,
+ errors: stub(for: nil, size: 0),
+ params: {},
+ transaction: stub(status: nil)
+ end
+
+ def success_response
+ stub success?: true,
+ transaction: stub_transaction
+ end
+
+ # that's what you get when not following the law of demeter...
+ def stub_transaction
+ stub amount: "100.00",
+ id: "ASDF",
+ customer_details: FactoryGirl.build(:braintree_customer),
+ credit_card_details: FactoryGirl.build(:braintree_customer).credit_cards.first
+ end
+
+end
diff --git a/billing/test/integration/customer_creation_test.rb b/billing/test/integration/customer_creation_test.rb
new file mode 100644
index 0000000..3ab2e4f
--- /dev/null
+++ b/billing/test/integration/customer_creation_test.rb
@@ -0,0 +1,63 @@
+require 'test_helper'
+require 'fake_braintree'
+require 'capybara/rails'
+
+class CustomerCreationTest < ActionDispatch::IntegrationTest
+ include Warden::Test::Helpers
+ include Capybara::DSL
+
+ setup do
+ Warden.test_mode!
+ @user = FactoryGirl.create(:user)
+ login_as @user
+ end
+
+ teardown do
+ Warden.test_reset!
+ end
+
+ # Let's test both steps together with capybara
+ #
+ # This test is nice and clean but also a bit fragil:
+ # RackTest assumes all requests to be local. So we need
+ # BraintreeTestApp for the braintree transparent redirect to work.
+ test "create customer with braintree" do
+ visit '/customer/new'
+ assert_difference("Customer.count") do
+ click_button 'Save Payment Info'
+ end
+
+ assert customer = Customer.find_by_user_id(@user.id)
+ assert customer.braintree_customer
+ end
+
+ # We only test the confirmation here.
+ # The request to Braintree is triggered outside of rails
+ test "successfully confirms customer creation" do
+ response = post_transparent_redirect :create_customer_data,
+ customer: FactoryGirl.attributes_for(:braintree_customer),
+ redirect_url: confirm_customer_url
+
+ assert_difference("Customer.count") do
+ post response['Location']
+ end
+
+ assert_equal 200, status
+ assert customer = Customer.find_by_user_id(@user.id)
+ assert customer.braintree_customer
+ end
+
+ def post_transparent_redirect(type, data)
+ params = data.dup
+ params[:tr_data] = Braintree::TransparentRedirect.send(type, params)
+ post_transparent_redirect_params(params)
+ end
+
+ def post_transparent_redirect_params(params)
+ uri = URI.parse(Braintree::TransparentRedirect.url)
+ Net::HTTP.start(uri.host, uri.port) do |http|
+ http.post(uri.path, Rack::Utils.build_nested_query(params))
+ end
+ end
+
+end
diff --git a/billing/test/test_helper.rb b/billing/test/test_helper.rb
new file mode 100644
index 0000000..1e26a31
--- /dev/null
+++ b/billing/test/test_helper.rb
@@ -0,0 +1,15 @@
+# Configure Rails Environment
+ENV["RAILS_ENV"] = "test"
+
+require File.expand_path("../dummy/config/environment.rb", __FILE__)
+require "rails/test_help"
+
+Rails.backtrace_cleaner.remove_silencers!
+
+# Load support files
+Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each { |f| require f }
+
+# Load fixtures from the engine
+if ActiveSupport::TestCase.method_defined?(:fixture_path=)
+ ActiveSupport::TestCase.fixture_path = File.expand_path("../fixtures", __FILE__)
+end
diff --git a/billing/test/unit/customer_test.rb b/billing/test/unit/customer_test.rb
new file mode 100644
index 0000000..2ea0b5e
--- /dev/null
+++ b/billing/test/unit/customer_test.rb
@@ -0,0 +1,36 @@
+require 'test_helper'
+
+class CustomerTest < ActiveSupport::TestCase
+
+ setup do
+ @customer = FactoryGirl.build(:customer)
+ end
+
+ test "test set of attributes should be valid" do
+ @customer.valid?
+ assert_equal Hash.new, @customer.errors.messages
+ end
+
+ test "customer belongs to user" do
+ assert_equal User, @customer.user.class
+ end
+
+ test "user validation" do
+ @customer.user = nil
+ assert !@customer.valid?
+ end
+
+ test "has no payment info" do
+ assert !@customer.braintree_customer_id
+ assert !@customer.has_payment_info?
+ end
+
+ test "with no braintree data" do
+ assert_equal @customer, @customer.with_braintree_data!
+ end
+
+ test "without default credit card" do
+ assert_nil @customer.default_credit_card
+ end
+
+end
diff --git a/billing/test/unit/customer_with_payment_info_test.rb b/billing/test/unit/customer_with_payment_info_test.rb
new file mode 100644
index 0000000..ca89e65
--- /dev/null
+++ b/billing/test/unit/customer_with_payment_info_test.rb
@@ -0,0 +1,37 @@
+require 'test_helper'
+require 'fake_braintree'
+
+class CustomerWithPaymentInfoTest < ActiveSupport::TestCase
+
+ setup do
+ @customer = FactoryGirl.build(:customer_with_payment_info)
+ end
+
+ test "has payment_info" do
+ assert @customer.braintree_customer_id
+ assert @customer.has_payment_info?
+ end
+
+ test "constructs customer with braintree data" do
+ @customer.with_braintree_data!
+ assert_equal 'Big', @customer.first_name
+ assert_equal 'Spender', @customer.last_name
+ assert_equal 1, @customer.credit_cards.size
+ assert_equal Hash.new, @customer.custom_fields
+ end
+
+ test "can access braintree_customer after reload" do
+ @customer.save
+ @customer = Customer.find_by_user_id(@customer.user_id)
+ @customer.with_braintree_data!
+ assert_equal 'Big', @customer.first_name
+ assert_equal 'Spender', @customer.last_name
+ assert_equal 1, @customer.credit_cards.size
+ assert_equal Hash.new, @customer.custom_fields
+ end
+
+ test "sets default_credit_card" do
+ @customer.with_braintree_data!
+ assert_equal @customer.credit_cards.first, @customer.default_credit_card
+ end
+end