summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--DEVELOP.md3
-rw-r--r--Gemfile7
-rw-r--r--README.md1
-rw-r--r--app/views/home/index.html.haml4
-rw-r--r--app/views/layouts/_navigation.html.haml1
-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
-rw-r--r--common_dependencies.rb1
-rw-r--r--config/environments/development.rb4
-rw-r--r--core/leap_web_core.gemspec2
-rw-r--r--lib/leap_web.rb1
-rw-r--r--lib/tasks/task_helper.rb2
-rw-r--r--users/app/controllers/v1/sessions_controller.rb2
-rw-r--r--users/app/views/overviews/show.html.haml1
59 files changed, 1257 insertions, 5 deletions
diff --git a/DEVELOP.md b/DEVELOP.md
index e19b827..a35ce06 100644
--- a/DEVELOP.md
+++ b/DEVELOP.md
@@ -10,12 +10,13 @@ Some tips on modifying the views:
Leap Web consists of different Engines. They live in their own subdirectory and are included through bundler via their path. This way changes to the engines immediately affect the server as if they were in the main `app` directory.
-Currently Leap Web consists of 4 Engines:
+Currently Leap Web consists of 5 Engines:
* [core](https://github.com/leapcode/leap_web/blob/master/core) - ships some dependencies that are used accross all engines. This might be removed at some point.
* [users](https://github.com/leapcode/leap_web/blob/master/users) - user registration and authorization
* [certs](https://github.com/leapcode/leap_web/blob/master/certs) - Cert distribution for the EIP client
* [help](https://github.com/leapcode/leap_web/blob/master/help) - Help ticket management
+* [billing](https://github.com/leapcode/leap_web/blob/master/billing) - Billing System
## Creating a new engine ##
diff --git a/Gemfile b/Gemfile
index 56cbf62..a562f73 100644
--- a/Gemfile
+++ b/Gemfile
@@ -10,12 +10,19 @@ gem "leap_web_core", :path => 'core'
gem 'leap_web_users', :path => 'users'
gem 'leap_web_certs', :path => 'certs'
gem 'leap_web_help', :path => 'help'
+gem 'leap_web_billing', :path => 'billing'
# To use debugger
gem 'debugger', :platforms => :mri_19
# ruby 1.8 is not supported anymore
# gem 'ruby-debug', :platforms => :mri_18
+
+group :test do
+ gem 'fake_braintree', require: false
+ gem 'capybara', require: false
+end
+
# unreleased so far ... but leap_web_certs need it
gem 'certificate_authority', :git => 'git://github.com/cchandler/certificate_authority.git'
diff --git a/README.md b/README.md
index 8e59c76..cfdad33 100644
--- a/README.md
+++ b/README.md
@@ -7,6 +7,7 @@ LEAP Web
* Admin interface to manage users.
* Client certificate distribution and renewal.
* User support help tickets.
+* Billing
This web application is written in Ruby on Rails 3, using CouchDB as the backend data store.
diff --git a/app/views/home/index.html.haml b/app/views/home/index.html.haml
index 0d1ac73..258ccec 100644
--- a/app/views/home/index.html.haml
+++ b/app/views/home/index.html.haml
@@ -12,4 +12,6 @@
.row-fluid
%hr
%p
- = link_to "fetch a cert", cert_path
+ = link_to "fetch a cert", cert_pat
+- # TODO: will want link to donation (anonymous payment), which is new_payment_path
+
diff --git a/app/views/layouts/_navigation.html.haml b/app/views/layouts/_navigation.html.haml
index 2f79a22..cd37ffa 100644
--- a/app/views/layouts/_navigation.html.haml
+++ b/app/views/layouts/_navigation.html.haml
@@ -3,4 +3,5 @@
= link_to_navigation t(:account_settings), edit_user_path(@user), :active => controller?(:users)
= link_to_navigation t(:email_settings), edit_user_email_settings_path(@user), :active => controller?(:email_settings)
= link_to_navigation t(:support_tickets), auto_tickets_path, :active => controller?(:tickets)
+ = link_to_navigation t(:billing_settings), show_or_new_customer_link(@user), :active => controller?(:customer, :payments, :subscriptions, :credit_card_info)
= link_to_navigation t(:logout), logout_path, :method => :delete
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
diff --git a/common_dependencies.rb b/common_dependencies.rb
index 085a898..4790ea4 100644
--- a/common_dependencies.rb
+++ b/common_dependencies.rb
@@ -9,6 +9,7 @@ group :test do
gem 'poltergeist'
# required for save_and_open_page in integration tests
# gem 'launchy'
+ gem 'fake_braintree' #depends on rspec?
end
group :test, :development do
diff --git a/config/environments/development.rb b/config/environments/development.rb
index 9ff9476..adde3ba 100644
--- a/config/environments/development.rb
+++ b/config/environments/development.rb
@@ -28,4 +28,8 @@ LeapWeb::Application.configure do
# Expands the lines which load the assets
config.assets.debug = true
+
+ # hacky, but otherwise getting certificate error, and doesn't seem dangerous in development mode:
+ OpenSSL::SSL::VERIFY_PEER = OpenSSL::SSL::VERIFY_NONE
+
end
diff --git a/core/leap_web_core.gemspec b/core/leap_web_core.gemspec
index e572200..a29db87 100644
--- a/core/leap_web_core.gemspec
+++ b/core/leap_web_core.gemspec
@@ -19,7 +19,7 @@ Gem::Specification.new do |s|
s.add_dependency "couchrest", "~> 1.1.3"
s.add_dependency "couchrest_model", "~> 2.0.0.beta2"
- s.add_dependency "couchrest_session_store", "~> 0.0.9"
+ s.add_dependency "couchrest_session_store", "~> 0.1.3"
s.add_dependency "json"
end
diff --git a/lib/leap_web.rb b/lib/leap_web.rb
index 31294dc..9495fc6 100644
--- a/lib/leap_web.rb
+++ b/lib/leap_web.rb
@@ -1,3 +1,4 @@
require 'leap_web_core'
require 'leap_web_certs'
require 'leap_web_users'
+# do we want billing and help here?
diff --git a/lib/tasks/task_helper.rb b/lib/tasks/task_helper.rb
index 26e60bc..81dcbe4 100644
--- a/lib/tasks/task_helper.rb
+++ b/lib/tasks/task_helper.rb
@@ -2,7 +2,7 @@ require File.expand_path('../../../lib/leap_web/version', __FILE__)
module TaskHelper
- ENGINES = %w(users certs help)
+ ENGINES = %w(users certs help billing)
def putsys(cmd)
puts cmd
diff --git a/users/app/controllers/v1/sessions_controller.rb b/users/app/controllers/v1/sessions_controller.rb
index e3459d6..295c327 100644
--- a/users/app/controllers/v1/sessions_controller.rb
+++ b/users/app/controllers/v1/sessions_controller.rb
@@ -35,7 +35,7 @@ module V1
protected
def login_response
- handshake = session.delete(:handshake)
+ handshake = session.delete(:handshake) || {}
handshake.to_hash.merge(:id => current_user.id, :token => @token.id)
end
diff --git a/users/app/views/overviews/show.html.haml b/users/app/views/overviews/show.html.haml
index 898cfa0..c6b079d 100644
--- a/users/app/views/overviews/show.html.haml
+++ b/users/app/views/overviews/show.html.haml
@@ -19,3 +19,4 @@
%li= icon('user') + link_to(t(:overview_account), edit_user_path(@user))
%li= icon('envelope') + link_to(t(:overview_email), edit_user_email_settings_path(@user))
%li= icon('question-sign') + link_to(t(:overview_tickets), user_tickets_path(@user))
+ %li= icon('shopping-cart') + link_to(t(:overview_billing), show_or_new_customer_link(@user))