summaryrefslogtreecommitdiff
path: root/engines
diff options
context:
space:
mode:
Diffstat (limited to 'engines')
-rw-r--r--engines/billing/Gemfile23
-rw-r--r--engines/billing/README.md49
-rw-r--r--engines/billing/Rakefile40
-rw-r--r--engines/billing/app/controllers/billing_admin_controller.rb29
-rw-r--r--engines/billing/app/controllers/billing_base_controller.rb22
-rw-r--r--engines/billing/app/controllers/credit_card_info_controller.rb35
-rw-r--r--engines/billing/app/controllers/customer_controller.rb64
-rw-r--r--engines/billing/app/controllers/payments_controller.rb34
-rw-r--r--engines/billing/app/controllers/subscriptions_controller.rb63
-rw-r--r--engines/billing/app/helpers/billing_helper.rb51
-rw-r--r--engines/billing/app/helpers/braintree_form_helper.rb64
-rw-r--r--engines/billing/app/helpers/braintree_helper.rb5
-rw-r--r--engines/billing/app/models/customer.rb58
-rw-r--r--engines/billing/app/views/billing_admin/show.html.haml7
-rw-r--r--engines/billing/app/views/credit_card_info/confirm.html.haml5
-rw-r--r--engines/billing/app/views/credit_card_info/edit.html.haml17
-rw-r--r--engines/billing/app/views/customer/_customer_data.html.haml16
-rw-r--r--engines/billing/app/views/customer/_transaction.html.haml0
-rw-r--r--engines/billing/app/views/customer/confirm.html.haml14
-rw-r--r--engines/billing/app/views/customer/edit.html.haml23
-rw-r--r--engines/billing/app/views/customer/new.html.haml24
-rw-r--r--engines/billing/app/views/customer/show.html.haml27
-rw-r--r--engines/billing/app/views/payments/_non_customer_fields.html.haml16
-rw-r--r--engines/billing/app/views/payments/_transaction_details.html.haml15
-rw-r--r--engines/billing/app/views/payments/confirm.html.haml26
-rw-r--r--engines/billing/app/views/payments/index.html.haml5
-rw-r--r--engines/billing/app/views/payments/new.html.haml17
-rw-r--r--engines/billing/app/views/subscriptions/_subscription_details.html.haml26
-rw-r--r--engines/billing/app/views/subscriptions/create.html.haml9
-rw-r--r--engines/billing/app/views/subscriptions/destroy.html.haml7
-rw-r--r--engines/billing/app/views/subscriptions/index.html.haml8
-rw-r--r--engines/billing/app/views/subscriptions/new.html.haml15
-rw-r--r--engines/billing/app/views/subscriptions/show.html.haml6
-rw-r--r--engines/billing/config/initializers/braintree.rb23
-rw-r--r--engines/billing/config/locales/en.yml11
-rw-r--r--engines/billing/config/routes.rb25
-rw-r--r--engines/billing/leap_web_billing.gemspec21
-rw-r--r--engines/billing/lib/braintree_test_app.rb36
-rw-r--r--engines/billing/lib/leap_web_billing.rb4
-rw-r--r--engines/billing/lib/leap_web_billing/engine.rb10
-rwxr-xr-xengines/billing/script/rails8
-rw-r--r--engines/billing/test/broken/admin_customer_test.rb31
-rw-r--r--engines/billing/test/broken/customer_creation_test.rb84
-rw-r--r--engines/billing/test/broken/subscription_test.rb49
-rw-r--r--engines/billing/test/factories.rb25
-rw-r--r--engines/billing/test/functional/customer_controller_test.rb124
-rw-r--r--engines/billing/test/functional/customers_controller_test.rb61
-rw-r--r--engines/billing/test/functional/payments_controller_test.rb50
-rw-r--r--engines/billing/test/functional/subscriptions_controller_test.rb16
-rw-r--r--engines/billing/test/support/braintree_integration_test.rb18
-rw-r--r--engines/billing/test/support/customer_test_helper.rb11
-rw-r--r--engines/billing/test/test_helper.rb15
-rw-r--r--engines/billing/test/unit/customer_test.rb38
-rw-r--r--engines/billing/test/unit/customer_with_payment_info_test.rb40
-rw-r--r--engines/support/Gemfile15
-rw-r--r--engines/support/README.md1
-rw-r--r--engines/support/Rakefile44
-rw-r--r--engines/support/app/assets/javascripts/tickets.js4
-rw-r--r--engines/support/app/controllers/tickets_controller.rb161
-rw-r--r--engines/support/app/designs/ticket/by_includes_post_by.js13
-rw-r--r--engines/support/app/designs/ticket/by_includes_post_by_and_created_at.js12
-rw-r--r--engines/support/app/designs/ticket/by_includes_post_by_and_is_open_and_created_at.js12
-rw-r--r--engines/support/app/designs/ticket/by_includes_post_by_and_is_open_and_updated_at.js12
-rw-r--r--engines/support/app/designs/ticket/by_includes_post_by_and_updated_at.js12
-rw-r--r--engines/support/app/helpers/auto_tickets_path_helper.rb54
-rw-r--r--engines/support/app/helpers/tickets_helper.rb76
-rw-r--r--engines/support/app/models/account_extension/tickets.rb13
-rw-r--r--engines/support/app/models/ticket.rb112
-rw-r--r--engines/support/app/models/ticket_comment.rb43
-rw-r--r--engines/support/app/models/ticket_selection.rb71
-rw-r--r--engines/support/app/views/tickets/_comment.html.haml20
-rw-r--r--engines/support/app/views/tickets/_edit_form.html.haml50
-rw-r--r--engines/support/app/views/tickets/_new_comment_form.html.haml13
-rw-r--r--engines/support/app/views/tickets/_tabs.html.haml23
-rw-r--r--engines/support/app/views/tickets/_ticket.html.haml6
-rw-r--r--engines/support/app/views/tickets/index.html.haml19
-rw-r--r--engines/support/app/views/tickets/new.html.haml17
-rw-r--r--engines/support/app/views/tickets/show.html.haml12
-rw-r--r--engines/support/config/initializers/account_lifecycle.rb3
-rw-r--r--engines/support/config/locales/en.yml22
-rw-r--r--engines/support/config/routes.rb8
-rw-r--r--engines/support/leap_web_help.gemspec18
-rw-r--r--engines/support/lib/leap_web_help.rb4
-rw-r--r--engines/support/lib/leap_web_help/engine.rb4
-rw-r--r--engines/support/lib/tasks/leap_web_help_tasks.rake4
-rwxr-xr-xengines/support/script/rails8
-rw-r--r--engines/support/test/factories.rb18
-rw-r--r--engines/support/test/functional/tickets_controller_test.rb284
-rw-r--r--engines/support/test/integration/create_ticket_test.rb64
-rw-r--r--engines/support/test/integration/navigation_test.rb9
-rw-r--r--engines/support/test/leap_web_help_test.rb7
-rw-r--r--engines/support/test/test_helper.rb15
-rw-r--r--engines/support/test/unit/account_extension_test.rb12
-rw-r--r--engines/support/test/unit/ticket_comment_test.rb59
-rw-r--r--engines/support/test/unit/ticket_test.rb88
95 files changed, 2962 insertions, 0 deletions
diff --git a/engines/billing/Gemfile b/engines/billing/Gemfile
new file mode 100644
index 0000000..30e9669
--- /dev/null
+++ b/engines/billing/Gemfile
@@ -0,0 +1,23 @@
+source "https://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/engines/billing/README.md b/engines/billing/README.md
new file mode 100644
index 0000000..3ef6153
--- /dev/null
+++ b/engines/billing/README.md
@@ -0,0 +1,49 @@
+Billing Engine
+====================
+
+Currently, this engine support billing via Braintree. More backends to come later.
+
+Configuration
+----------------------------------
+
+Start with a sandbox account, which you can get here: https://www.braintreepayments.com/get-started
+
+Once you have registered for the sandbox, logging in will show you three important variables you will need to configure:
+
+* merchantId
+* publicKey
+* privatekey
+
+To configure the billing engine, edit `config/config.yaml` like so:
+
+ production: (or "development", as you prefer)
+ billing:
+ braintree:
+ environment: sandbox
+ merchant_id: Ohp2aijaaqu6oJ4w
+ public_key: ahnar0UwLahwe6Ce
+ private_key: aemie2Geohgah2EaOad9DeeruW4Iegh4
+
+If deploying via puppet, the same data in webapp.json would like this:
+
+ "billing": {
+ "braintree": {
+ "environment": "sandbox",
+ "merchant_id": "Ohp2aijaaqu6oJ4w",
+ "public_key": "ahnar0UwLahwe6Ce",
+ "private_key": "aemie2Geohgah2EaOad9DeeruW4Iegh4"
+ }
+ }
+
+Now, you should be able to add charges to your own sandbox when you run the webapp.
+
+The acceptable values for `billing.braintree.environment` are: `development`, `qa`, `sandbox`, or `production`.
+
+Plans
+--------------------------------
+
+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/engines/billing/Rakefile b/engines/billing/Rakefile
new file mode 100644
index 0000000..52929c4
--- /dev/null
+++ b/engines/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/engines/billing/app/controllers/billing_admin_controller.rb b/engines/billing/app/controllers/billing_admin_controller.rb
new file mode 100644
index 0000000..e11d4ee
--- /dev/null
+++ b/engines/billing/app/controllers/billing_admin_controller.rb
@@ -0,0 +1,29 @@
+class BillingAdminController < BillingBaseController
+ before_filter :require_admin
+
+ def show
+
+ br_atleast_90_days = Braintree::Subscription.search do |search|
+ search.days_past_due >= 90
+ end
+ @past_due_atleast_90_days = braintree_resource_collection_to_array(br_atleast_90_days)
+
+ br_all_past_due = Braintree::Subscription.search do |search|
+ search.status.is Braintree::Subscription::Status::PastDue
+ #cannot search by balance.
+ end
+ @all_past_due = braintree_resource_collection_to_array(br_all_past_due)
+
+ end
+
+ private
+
+ def braintree_resource_collection_to_array(braintree_resource_collection)
+ array = []
+ braintree_resource_collection.each do |object|
+ array << object
+ end
+ array
+ end
+
+end
diff --git a/engines/billing/app/controllers/billing_base_controller.rb b/engines/billing/app/controllers/billing_base_controller.rb
new file mode 100644
index 0000000..0453677
--- /dev/null
+++ b/engines/billing/app/controllers/billing_base_controller.rb
@@ -0,0 +1,22 @@
+class BillingBaseController < ApplicationController
+ before_filter :assign_user
+
+ helper 'billing'
+
+ # required for navigation to work.
+ def assign_user
+ if params[:user_id]
+ @user = User.find(params[:user_id])
+ elsif params[:action] == "confirm"# confirms will come back with different ID set, so check for this first
+ # This is only for cases where an admin cannot apply action for customer, but should be all confirms
+ @user = current_user
+ elsif params[:id]
+ @user = User.find(params[:id])
+ else
+ # TODO
+ # hacky, what are cases where @user hasn't yet been set? certainly some cases with subscriptions and payments
+ @user = current_user
+ end
+ end
+
+end
diff --git a/engines/billing/app/controllers/credit_card_info_controller.rb b/engines/billing/app/controllers/credit_card_info_controller.rb
new file mode 100644
index 0000000..fbaa6f1
--- /dev/null
+++ b/engines/billing/app/controllers/credit_card_info_controller.rb
@@ -0,0 +1,35 @@
+class CreditCardInfoController < ApplicationController
+ before_filter :require_login, :set_user
+
+ def edit
+ @credit_card = Braintree::CreditCard.find(params[:id])
+ customer = Customer.find_by_user_id(@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
+ # this assumes anybody, even an admin, will not access for another user.
+ @user = current_user
+ end
+
+end
diff --git a/engines/billing/app/controllers/customer_controller.rb b/engines/billing/app/controllers/customer_controller.rb
new file mode 100644
index 0000000..6cbcb44
--- /dev/null
+++ b/engines/billing/app/controllers/customer_controller.rb
@@ -0,0 +1,64 @@
+class CustomerController < BillingBaseController
+ before_filter :require_login, :fetch_customer
+
+ def show
+ if @customer
+ @customer.with_braintree_data!
+ @default_cc = @customer.default_credit_card
+ @active_subscription = @customer.subscriptions
+ @transactions = @customer.braintree_customer.transactions
+ end
+ end
+
+ def new
+ if @customer.has_payment_info?
+ redirect_to edit_customer_path(@user), :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
+ access_denied unless @user == current_user # admins cannot do this for others
+ @tr_data = Braintree::TransparentRedirect.
+ create_customer_data(:redirect_url => confirm_customer_url)
+ end
+
+ def fetch_edit_transparent_redirect_data
+ access_denied unless @user == current_user # admins cannot do this for others
+ @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 fetch_customer
+ @customer = Customer.find_by_user_id(@user.id)
+ if @user == current_user
+ @customer ||= Customer.new(user: @user)
+ end
+ access_denied unless (@customer and (@customer.user == current_user)) or admin?
+ end
+end
diff --git a/engines/billing/app/controllers/payments_controller.rb b/engines/billing/app/controllers/payments_controller.rb
new file mode 100644
index 0000000..fce6570
--- /dev/null
+++ b/engines/billing/app/controllers/payments_controller.rb
@@ -0,0 +1,34 @@
+class PaymentsController < BillingBaseController
+ before_filter :require_login, :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
+ access_denied unless admin? or (@user == current_user)
+ customer = Customer.find_by_user_id(@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
+ @tr_data = Braintree::TransparentRedirect.transaction_data redirect_url: confirm_payment_url,
+ transaction: { type: "sale", options: {submit_for_settlement: true } }
+ end
+
+end
diff --git a/engines/billing/app/controllers/subscriptions_controller.rb b/engines/billing/app/controllers/subscriptions_controller.rb
new file mode 100644
index 0000000..f066b3c
--- /dev/null
+++ b/engines/billing/app/controllers/subscriptions_controller.rb
@@ -0,0 +1,63 @@
+class SubscriptionsController < BillingBaseController
+ before_filter :require_login
+ before_filter :fetch_subscription, :only => [:show, :destroy]
+ before_filter :confirm_cancel_subscription, :only => [:destroy]
+ before_filter :confirm_self_or_admin, :only => [:index]
+ before_filter :confirm_no_pending_active_pastdue_subscription, :only => [:new, :create]
+ # for now, admins cannot create or destroy subscriptions for others:
+ before_filter :confirm_self, :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] )
+ #if you want to test pastdue, can add :price => '2001', :trial_period => true,:trial_duration => 1,:trial_duration_unit => "day" and then wait a day
+ end
+
+ def destroy
+ @result = Braintree::Subscription.cancel params[:id]
+ end
+
+ def index
+ customer = Customer.find_by_user_id(@user.id)
+ @subscriptions = customer.subscriptions(nil, false)
+ end
+
+ private
+
+ def fetch_subscription
+ @subscription = Braintree::Subscription.find params[:id]
+ @credit_card = Braintree::CreditCard.find @subscription.payment_method_token
+ @subscription_customer_id = @credit_card.customer_id
+ current_user_customer = Customer.find_by_user_id(current_user.id)
+ access_denied unless admin? or (current_user_customer and current_user_customer.braintree_customer_id == @subscription_customer_id)
+
+ end
+
+ def confirm_cancel_subscription
+ access_denied unless view_context.allow_cancel_subscription(@subscription)
+ end
+
+ def confirm_no_pending_active_pastdue_subscription
+ @customer = Customer.find_by_user_id(@user.id)
+ if subscription = @customer.subscriptions # will return pending, active or pastdue subscription, if it exists
+ redirect_to user_subscription_path(@user, subscription.id), :notice => 'You already have a subscription'
+ end
+ end
+
+ def confirm_self
+ @user == current_user
+ end
+
+ def confirm_self_or_admin
+ access_denied unless confirm_self or admin?
+ end
+
+end
diff --git a/engines/billing/app/helpers/billing_helper.rb b/engines/billing/app/helpers/billing_helper.rb
new file mode 100644
index 0000000..b9e5e2e
--- /dev/null
+++ b/engines/billing/app/helpers/billing_helper.rb
@@ -0,0 +1,51 @@
+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 billing_top_link(user)
+ # for admins, top link will show special admin information, which has link to show their own customer information
+ if (admin? and user == current_user)
+ billing_admin_path
+ else
+ show_or_new_customer_link(user)
+ end
+ end
+
+ def show_or_new_customer_link(user)
+ # Link to show if user is admin viewing another user, or user is already a customer.
+ # Otherwise link to create a new customer.
+ if (admin? and (user != current_user)) or ((customer = Customer.find_by_user_id(user.id)) and customer.has_payment_info?)
+ show_customer_path(user)
+ else
+ new_customer_path
+ end
+ end
+
+ # a bit strange to put here, but we don't have a subscription model
+ def user_for_subscription(subscription)
+
+ if (transaction = subscription.transactions.first)
+ # much quicker, but will only work if there is already a transaction associated with subscription (should generally be)
+ braintree_customer_id = transaction.customer_details.id
+ else
+ credit_card = Braintree::CreditCard.find(subscription.payment_method_token)
+ braintree_customer_id = credit_card.customer_id
+ end
+
+ customer = Customer.find_by_braintree_customer_id(braintree_customer_id)
+ user = User.find(customer.user_id)
+
+ end
+
+ def allow_cancel_subscription(subscription)
+ ['Active', 'Pending'].include? subscription.status or (admin? and subscription.status == 'Past Due')
+ end
+
+end
diff --git a/engines/billing/app/helpers/braintree_form_helper.rb b/engines/billing/app/helpers/braintree_form_helper.rb
new file mode 100644
index 0000000..cb322fa
--- /dev/null
+++ b/engines/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/engines/billing/app/helpers/braintree_helper.rb b/engines/billing/app/helpers/braintree_helper.rb
new file mode 100644
index 0000000..2d18b6c
--- /dev/null
+++ b/engines/billing/app/helpers/braintree_helper.rb
@@ -0,0 +1,5 @@
+module BraintreeHelper
+
+
+end
+
diff --git a/engines/billing/app/models/customer.rb b/engines/billing/app/models/customer.rb
new file mode 100644
index 0000000..1acc7a5
--- /dev/null
+++ b/engines/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_pending_active_pastdue=true)
+ self.with_braintree_data!
+ return unless has_payment_info?
+
+ subscriptions = []
+ self.default_credit_card.subscriptions.each do |sub|
+ if only_pending_active_pastdue and ['Pending', 'Active','Past Due'].include? sub.status
+ return sub
+ else
+ subscriptions << sub
+ end
+ end
+ only_pending_active_pastdue ? nil : subscriptions
+ end
+
+end
diff --git a/engines/billing/app/views/billing_admin/show.html.haml b/engines/billing/app/views/billing_admin/show.html.haml
new file mode 100644
index 0000000..0382cf0
--- /dev/null
+++ b/engines/billing/app/views/billing_admin/show.html.haml
@@ -0,0 +1,7 @@
+%legend= t(:more_than_90_days_past_due)
+= render(:partial => "subscriptions/subscription_details", :collection => @past_due_atleast_90_days, :as => 'subscription', :locals => {:show_user => true}) || t(:none)
+%legend= t(:all_past_due)
+= render(:partial => "subscriptions/subscription_details", :collection => @all_past_due, :as => 'subscription', :locals => {:show_user => true}) || t(:none)
+
+%legend= t(:your_settings)
+= link_to 'view own billing settings', show_or_new_customer_link(current_user) \ No newline at end of file
diff --git a/engines/billing/app/views/credit_card_info/confirm.html.haml b/engines/billing/app/views/credit_card_info/confirm.html.haml
new file mode 100644
index 0000000..9dd8176
--- /dev/null
+++ b/engines/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/engines/billing/app/views/credit_card_info/edit.html.haml b/engines/billing/app/views/credit_card_info/edit.html.haml
new file mode 100644
index 0000000..bd86a4c
--- /dev/null
+++ b/engines/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(@user.id), :class => :btn
diff --git a/engines/billing/app/views/customer/_customer_data.html.haml b/engines/billing/app/views/customer/_customer_data.html.haml
new file mode 100644
index 0000000..e9df040
--- /dev/null
+++ b/engines/billing/app/views/customer/_customer_data.html.haml
@@ -0,0 +1,16 @@
+%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
+ - if current_user == @user
+ = link_to t(:edit_saved_data), edit_customer_path(@user.id), :class => :btn
diff --git a/engines/billing/app/views/customer/_transaction.html.haml b/engines/billing/app/views/customer/_transaction.html.haml
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/engines/billing/app/views/customer/_transaction.html.haml
diff --git a/engines/billing/app/views/customer/confirm.html.haml b/engines/billing/app/views/customer/confirm.html.haml
new file mode 100644
index 0000000..877a8ac
--- /dev/null
+++ b/engines/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(@user.id)
+= link_to 'View Customer Info', show_customer_path(@user.id), :class=> :btn \ No newline at end of file
diff --git a/engines/billing/app/views/customer/edit.html.haml b/engines/billing/app/views/customer/edit.html.haml
new file mode 100644
index 0000000..e882d53
--- /dev/null
+++ b/engines/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(@user), :class=> :btn
diff --git a/engines/billing/app/views/customer/new.html.haml b/engines/billing/app/views/customer/new.html.haml
new file mode 100644
index 0000000..e1f5ba9
--- /dev/null
+++ b/engines/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/engines/billing/app/views/customer/show.html.haml b/engines/billing/app/views/customer/show.html.haml
new file mode 100644
index 0000000..ec1779c
--- /dev/null
+++ b/engines/billing/app/views/customer/show.html.haml
@@ -0,0 +1,27 @@
+- if admin? and !@customer
+ = t(:no_saved_customer)
+- else
+ = render :partial => '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), user_payments_path(@user)
+ %legend= t(:subscriptions)
+ - if @active_subscription
+ = render :partial => "subscriptions/subscription_details", :locals => {:subscription => @active_subscription}
+ - else
+ %p
+ = t(:no_relevant_subscription)
+ - if current_user == @user
+ %p
+ .form-actions
+ = link_to t(:subscribe_to_plan), new_subscription_path, :class => :btn
+ %p
+ = link_to t(:all_subscriptions), user_subscriptions_path(@user)
+
+.form-actions
+ = link_to t(:make_donation), new_payment_path, :class => 'btn btn-primary'
diff --git a/engines/billing/app/views/payments/_non_customer_fields.html.haml b/engines/billing/app/views/payments/_non_customer_fields.html.haml
new file mode 100644
index 0000000..77cfe95
--- /dev/null
+++ b/engines/billing/app/views/payments/_non_customer_fields.html.haml
@@ -0,0 +1,16 @@
+= field_set_tag "Personal Information" 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/engines/billing/app/views/payments/_transaction_details.html.haml b/engines/billing/app/views/payments/_transaction_details.html.haml
new file mode 100644
index 0000000..85e4f6a
--- /dev/null
+++ b/engines/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 # should not have any of these
+ Not paid as part of subscription \ No newline at end of file
diff --git a/engines/billing/app/views/payments/confirm.html.haml b/engines/billing/app/views/payments/confirm.html.haml
new file mode 100644
index 0000000..45af3c9
--- /dev/null
+++ b/engines/billing/app/views/payments/confirm.html.haml
@@ -0,0 +1,26 @@
+%h1 Payment Result
+%div Thank you for your donation.
+%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 \ No newline at end of file
diff --git a/engines/billing/app/views/payments/index.html.haml b/engines/billing/app/views/payments/index.html.haml
new file mode 100644
index 0000000..7a89917
--- /dev/null
+++ b/engines/billing/app/views/payments/index.html.haml
@@ -0,0 +1,5 @@
+%h2=t :transaction_history
+- if (@transactions.count == 0)
+ = t(:no_transaction_history)
+- @transactions.each do |t|
+ = render :partial => "transaction_details", :locals => {:transaction => t} \ No newline at end of file
diff --git a/engines/billing/app/views/payments/new.html.haml b/engines/billing/app/views/payments/new.html.haml
new file mode 100644
index 0000000..e9a8273
--- /dev/null
+++ b/engines/billing/app/views/payments/new.html.haml
@@ -0,0 +1,17 @@
+%h1
+ = t(:Donation)
+- if logged_in?
+ = t(:donation_not_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 != 'success'
+ %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
+ = render :partial => 'non_customer_fields', :locals => {:f => f}
+ = hidden_field_tag :tr_data, @tr_data
+ = f.submit "Submit Donation", :class => 'btn btn-primary'
diff --git a/engines/billing/app/views/subscriptions/_subscription_details.html.haml b/engines/billing/app/views/subscriptions/_subscription_details.html.haml
new file mode 100644
index 0000000..6145c95
--- /dev/null
+++ b/engines/billing/app/views/subscriptions/_subscription_details.html.haml
@@ -0,0 +1,26 @@
+%p
+ - if local_assigns[:show_user]
+ User:
+ - user_to_show = user_for_subscription(subscription)
+ = link_to user_to_show.login, user_overview_path(user_to_show)
+ ID:
+ = link_to subscription.id, user_subscription_path(@user, subscription.id)
+ Balance:
+ - color = (subscription.balance > 0) ? "red" : ""
+ %font{:color => color}
+ = 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:
+ = number_to_currency(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/engines/billing/app/views/subscriptions/create.html.haml b/engines/billing/app/views/subscriptions/create.html.haml
new file mode 100644
index 0000000..2b6c5e9
--- /dev/null
+++ b/engines/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/engines/billing/app/views/subscriptions/destroy.html.haml b/engines/billing/app/views/subscriptions/destroy.html.haml
new file mode 100644
index 0000000..44b4333
--- /dev/null
+++ b/engines/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(@user), :class=> :btn \ No newline at end of file
diff --git a/engines/billing/app/views/subscriptions/index.html.haml b/engines/billing/app/views/subscriptions/index.html.haml
new file mode 100644
index 0000000..3d4e8fd
--- /dev/null
+++ b/engines/billing/app/views/subscriptions/index.html.haml
@@ -0,0 +1,8 @@
+%h2=t :all_subscriptions
+- pending_active_pastdue = false
+- @subscriptions.each do |s|
+ - if ['Pending', 'Active','Past Due'].include? s.status
+ - pending_active_pastdue = true
+ = render :partial => "subscription_details", :locals => {:subscription => s}
+- if !pending_active_pastdue and @user == current_user
+ = link_to 'subscribe to plan', new_subscription_path, :class => :btn \ No newline at end of file
diff --git a/engines/billing/app/views/subscriptions/new.html.haml b/engines/billing/app/views/subscriptions/new.html.haml
new file mode 100644
index 0000000..4183458
--- /dev/null
+++ b/engines/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/engines/billing/app/views/subscriptions/show.html.haml b/engines/billing/app/views/subscriptions/show.html.haml
new file mode 100644
index 0000000..2699db9
--- /dev/null
+++ b/engines/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), user_subscription_path(@user, @subscription.id), :confirm => t(:are_you_sure), :method => :delete, :class => 'btn btn-danger' if allow_cancel_subscription(@subscription)
diff --git a/engines/billing/config/initializers/braintree.rb b/engines/billing/config/initializers/braintree.rb
new file mode 100644
index 0000000..3d87f4c
--- /dev/null
+++ b/engines/billing/config/initializers/braintree.rb
@@ -0,0 +1,23 @@
+#
+# set logger
+#
+if APP_CONFIG[:logfile].blank?
+ require 'syslog/logger'
+ Braintree::Configuration.logger = ActiveSupport::TaggedLogging.new(Syslog::Logger.new('webapp'))
+else
+ Braintree::Configuration.logger = Logger.new('log/braintree.log')
+end
+
+#
+# You can set these per environment in config/config.yml:
+#
+# Environment must be one of: :development, :qa, :sandbox, :production
+#
+if billing = APP_CONFIG[:billing]
+ if braintree = billing[:braintree]
+ Braintree::Configuration.environment = braintree[:environment].downcase.to_sym
+ Braintree::Configuration.merchant_id = braintree[:merchant_id]
+ Braintree::Configuration.public_key = braintree[:public_key]
+ Braintree::Configuration.private_key = braintree[:private_key]
+ end
+end
diff --git a/engines/billing/config/locales/en.yml b/engines/billing/config/locales/en.yml
new file mode 100644
index 0000000..1300958
--- /dev/null
+++ b/engines/billing/config/locales/en.yml
@@ -0,0 +1,11 @@
+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"
+ donation_not_payment: "Note: This is a donation, and will not be applied towards your account."
+ no_relevant_subscription: "No subscription which is Active, Pending, or Past Due"
+ plan: "Plan"
+ description: "Description"
+ cost: "Cost"
+ free: "Free" \ No newline at end of file
diff --git a/engines/billing/config/routes.rb b/engines/billing/config/routes.rb
new file mode 100644
index 0000000..7263dff
--- /dev/null
+++ b/engines/billing/config/routes.rb
@@ -0,0 +1,25 @@
+Rails.application.routes.draw do
+
+ scope "(:locale)", :locale => MATCH_LOCALE do
+ match 'payments/new' => 'payments#new', :as => :new_payment
+ match 'payments/confirm' => 'payments#confirm', :as => :confirm_payment
+ resources :users do
+ resources :payments, :only => [:index]
+ resources :subscriptions, :only => [:index, :show, :destroy]
+ end
+
+ 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, :update] # index, show & destroy are within users path
+ match 'billing_admin' => 'billing_admin#show', :as => :billing_admin
+
+ #match 'transactions/:product_id/new' => 'transactions#new', :as => :new_transaction
+ #match 'transactions/confirm/:product_id' => 'transactions#confirm', :as => :confirm_transaction
+ end
+
+end
diff --git a/engines/billing/leap_web_billing.gemspec b/engines/billing/leap_web_billing.gemspec
new file mode 100644
index 0000000..ff11c98
--- /dev/null
+++ b/engines/billing/leap_web_billing.gemspec
@@ -0,0 +1,21 @@
+$:.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,lib}/**/*"] + ["Rakefile", "README.md"]
+ s.test_files = Dir["test/**/*"]
+
+ # s.add_dependency "braintree-rails", "~> 0.4.5"
+ s.add_dependency "braintree"
+ #s.add_dependency "carmen-rails"
+end
diff --git a/engines/billing/lib/braintree_test_app.rb b/engines/billing/lib/braintree_test_app.rb
new file mode 100644
index 0000000..41c327d
--- /dev/null
+++ b/engines/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/engines/billing/lib/leap_web_billing.rb b/engines/billing/lib/leap_web_billing.rb
new file mode 100644
index 0000000..288d846
--- /dev/null
+++ b/engines/billing/lib/leap_web_billing.rb
@@ -0,0 +1,4 @@
+require "leap_web_billing/engine"
+
+module LeapWebBilling
+end
diff --git a/engines/billing/lib/leap_web_billing/engine.rb b/engines/billing/lib/leap_web_billing/engine.rb
new file mode 100644
index 0000000..ab574f2
--- /dev/null
+++ b/engines/billing/lib/leap_web_billing/engine.rb
@@ -0,0 +1,10 @@
+# thou shall require all your dependencies in an engine.
+#require "braintree-rails"
+require "braintree"
+#require "carmen-rails"
+
+module LeapWebBilling
+ class Engine < ::Rails::Engine
+
+ end
+end
diff --git a/engines/billing/script/rails b/engines/billing/script/rails
new file mode 100755
index 0000000..8bd9c0a
--- /dev/null
+++ b/engines/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/engines/billing/test/broken/admin_customer_test.rb b/engines/billing/test/broken/admin_customer_test.rb
new file mode 100644
index 0000000..df92a0d
--- /dev/null
+++ b/engines/billing/test/broken/admin_customer_test.rb
@@ -0,0 +1,31 @@
+require 'test_helper'
+require 'fake_braintree'
+
+class AdminCustomerTest < BraintreeIntegrationTest
+
+ setup do
+ @admin = User.find_by_login('admin') || FactoryGirl.create(:user, login: 'admin')
+ @user = FactoryGirl.create(:user)
+ end
+
+ teardown do
+ @user.destroy if @user
+ @admin.destroy if @admin
+ end
+
+ test "check non customer as admin" do
+ login_as @admin
+ visit '/'
+ click_link 'Users'
+ click_link @user.login
+ click_link 'Billing Settings'
+ assert page.has_content? @user.email_address
+ assert page.has_content? 'No Saved Customer'
+ end
+
+ test "check customer as admin" do
+ skip "cannot check customer as admin"
+ # it would be good to have a test where an admin tries to view the 'Billing Settings' for another user.
+ # However, partially due to limitations of FakeBraintree, this doesn't seem pursuing at this time.
+ end
+end
diff --git a/engines/billing/test/broken/customer_creation_test.rb b/engines/billing/test/broken/customer_creation_test.rb
new file mode 100644
index 0000000..90319a9
--- /dev/null
+++ b/engines/billing/test/broken/customer_creation_test.rb
@@ -0,0 +1,84 @@
+require 'test_helper'
+require 'fake_braintree'
+
+class CustomerCreationTest < BraintreeIntegrationTest
+
+ setup do
+ @user = FactoryGirl.create(:user)
+ login_as @user
+ end
+
+ teardown do
+ @user.destroy
+ end
+
+ # Let's test both steps together with capybara
+ #
+ # This test is nice and clean but also a bit fragile:
+ # RackTest assumes all requests to be local. So we need
+ # BraintreeTestApp for the braintree transparent redirect to work.
+ #
+ # this mystifies me why this works. when i type the click_button line (and the
+ # customer.braintree_customer line) in the debugger, it gives a timeout,
+ # but it works fine embedded in the test.
+ test "create customer with braintree" do
+ visit '/'
+ click_link 'Billing Settings'
+ # i am a bit unclear why this works, as it seems there will be validation errors
+ assert_difference("Customer.count") do
+ click_button 'Save Payment Info' # this gives me a timeout
+ 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
+ # In skippped test below, we see this works even if the attributes are
+ # for a broken customer
+ 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
+
+
+ test "failed customer creation" do
+ skip "cannot get customer creation to fail"
+
+ FakeBraintree.decline_all_cards!
+
+ response = post_transparent_redirect :create_customer_data,
+ customer: FactoryGirl.attributes_for(:broken_customer),
+ redirect_url: confirm_customer_url
+
+ assert FakeBraintree.decline_all_cards?
+ assert_no_difference("Customer.count") do
+ post response['Location'] #this gives me a timeout when run alone
+ end
+ assert_nil Customer.find_by_user_id(@user.id)
+
+ 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/engines/billing/test/broken/subscription_test.rb b/engines/billing/test/broken/subscription_test.rb
new file mode 100644
index 0000000..cd010bd
--- /dev/null
+++ b/engines/billing/test/broken/subscription_test.rb
@@ -0,0 +1,49 @@
+require 'test_helper'
+require 'fake_braintree'
+
+class SubscriptionTest < BraintreeIntegrationTest
+ include CustomerTestHelper
+ include StubRecordHelper
+
+ setup do
+ @admin = User.find_by_login('admin') || FactoryGirl.create(:user, login: 'admin')
+ @customer = stub_customer
+ @braintree_customer = @customer.braintree_customer
+ response = Braintree::Subscription.create plan_id: '5',
+ payment_method_token: @braintree_customer.credit_cards.first.token,
+ price: '10'
+ @subscription = response.subscription
+ end
+
+ teardown do
+ @admin.destroy
+ end
+
+ test "admin can see all subscriptions for another" do
+ login_as @admin
+ @customer.stubs(:subscriptions).returns([@subscription])
+ @subscription.stubs(:balance).returns 0
+ visit user_subscriptions_path(@customer.user_id, :locale => nil)
+ assert page.has_content?("Subscriptions")
+ assert page.has_content?("Status: Active")
+ end
+
+ # test "user cannot see all subscriptions for other user" do
+ #end
+
+ #test "admin cannot add subscription for another" do
+ #end
+
+ #test "authenticated user can cancel own subscription" do
+ #end
+
+ #test "user cannot add subscription if they have active one" do
+ #end
+
+ #test "user can view own subscriptions"
+ #end
+
+ #test "admin can view another user's subscriptions" do
+ #end
+
+end
diff --git a/engines/billing/test/factories.rb b/engines/billing/test/factories.rb
new file mode 100644
index 0000000..87543b2
--- /dev/null
+++ b/engines/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/engines/billing/test/functional/customer_controller_test.rb b/engines/billing/test/functional/customer_controller_test.rb
new file mode 100644
index 0000000..d943e23
--- /dev/null
+++ b/engines/billing/test/functional/customer_controller_test.rb
@@ -0,0 +1,124 @@
+require 'test_helper'
+require 'fake_braintree'
+
+class CustomerControllerTest < ActionController::TestCase
+ include CustomerTestHelper
+
+ 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 = stub_customer
+ login customer.user
+ get :edit, id: customer.user.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 customer 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 = stub_customer
+ customer.expects(:save)
+ 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 customer creation" do
+ skip "can't get customer creation to fail"
+ login
+ FakeBraintree.decline_all_cards!
+ # what is prepare_confirmation ?? this method isn't found
+ 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 customer 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 customer update with stubbing" do
+ customer = stub_customer
+ 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/engines/billing/test/functional/customers_controller_test.rb b/engines/billing/test/functional/customers_controller_test.rb
new file mode 100644
index 0000000..46c33c9
--- /dev/null
+++ b/engines/billing/test/functional/customers_controller_test.rb
@@ -0,0 +1,61 @@
+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'
+ #this wasn't actually being used
+ #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.user), response.header['Location']
+ end
+
+
+ test "show" do
+ skip "show customer"
+ 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/engines/billing/test/functional/payments_controller_test.rb b/engines/billing/test/functional/payments_controller_test.rb
new file mode 100644
index 0000000..90b7582
--- /dev/null
+++ b/engines/billing/test/functional/payments_controller_test.rb
@@ -0,0 +1,50 @@
+require 'test_helper'
+require 'fake_braintree'
+
+class PaymentsControllerTest < ActionController::TestCase
+ include CustomerTestHelper
+
+ test "payment when unauthorized" do
+ 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/engines/billing/test/functional/subscriptions_controller_test.rb b/engines/billing/test/functional/subscriptions_controller_test.rb
new file mode 100644
index 0000000..a6a1057
--- /dev/null
+++ b/engines/billing/test/functional/subscriptions_controller_test.rb
@@ -0,0 +1,16 @@
+require 'test_helper'
+require 'fake_braintree'
+
+class SubscriptionsControllerTest < ActionController::TestCase
+ include CustomerTestHelper
+
+ test "destroy cancels subscription" do
+ customer = stub_customer
+ login customer.user
+ result = Braintree::Subscription.create plan_id: 'my_plan',
+ payment_method_token: customer.braintree_customer.credit_cards.first.token
+ subscription = result.subscription
+ delete :destroy, id: subscription.id, user_id: customer.user.id
+ assert_equal "Canceled", Braintree::Subscription.find(subscription.id).status
+ end
+end
diff --git a/engines/billing/test/support/braintree_integration_test.rb b/engines/billing/test/support/braintree_integration_test.rb
new file mode 100644
index 0000000..976c5a2
--- /dev/null
+++ b/engines/billing/test/support/braintree_integration_test.rb
@@ -0,0 +1,18 @@
+require 'capybara/rails'
+# require 'fake_braintree' - messes up other integration tests
+require 'braintree_test_app'
+
+class BraintreeIntegrationTest < BrowserIntegrationTest
+ include Warden::Test::Helpers
+
+ setup do
+ Warden.test_mode!
+ Rails.application.config.middleware.use BraintreeTestApp
+ end
+
+ teardown do
+ Warden.test_reset!
+ Rails.application.config.middleware.delete "BraintreeTestApp"
+ end
+
+end
diff --git a/engines/billing/test/support/customer_test_helper.rb b/engines/billing/test/support/customer_test_helper.rb
new file mode 100644
index 0000000..adac00a
--- /dev/null
+++ b/engines/billing/test/support/customer_test_helper.rb
@@ -0,0 +1,11 @@
+module CustomerTestHelper
+
+ def stub_customer(user = nil)
+ user ||= find_record :user
+ customer = stub_record :customer_with_payment_info,
+ user: user,
+ user_id: user.id
+ Customer.stubs(:find_by_user_id).with(user.id).returns(customer)
+ return customer
+ end
+end
diff --git a/engines/billing/test/test_helper.rb b/engines/billing/test/test_helper.rb
new file mode 100644
index 0000000..7ad3869
--- /dev/null
+++ b/engines/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/engines/billing/test/unit/customer_test.rb b/engines/billing/test/unit/customer_test.rb
new file mode 100644
index 0000000..6156f87
--- /dev/null
+++ b/engines/billing/test/unit/customer_test.rb
@@ -0,0 +1,38 @@
+require 'test_helper'
+
+class CustomerTest < ActiveSupport::TestCase
+ include StubRecordHelper
+
+ setup do
+ @user = find_record :user
+ @customer = FactoryGirl.build(:customer, user: @user)
+ 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/engines/billing/test/unit/customer_with_payment_info_test.rb b/engines/billing/test/unit/customer_with_payment_info_test.rb
new file mode 100644
index 0000000..0589a59
--- /dev/null
+++ b/engines/billing/test/unit/customer_with_payment_info_test.rb
@@ -0,0 +1,40 @@
+require 'test_helper'
+require 'fake_braintree'
+
+class CustomerWithPaymentInfoTest < ActiveSupport::TestCase
+ include StubRecordHelper
+
+ setup do
+ @user = find_record :user
+ @customer = FactoryGirl.build(:customer_with_payment_info, user: @user)
+ 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
+ @customer.destroy
+ 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/engines/support/Gemfile b/engines/support/Gemfile
new file mode 100644
index 0000000..ad7d29b
--- /dev/null
+++ b/engines/support/Gemfile
@@ -0,0 +1,15 @@
+source "https://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 leap_web_users.gemspec.
+# Bundler will treat runtime dependencies like base dependencies, and
+# development dependencies will be added by default to the :development group.
+gemspec
+
+# To use debugger
+# gem 'ruby-debug'
diff --git a/engines/support/README.md b/engines/support/README.md
new file mode 100644
index 0000000..c9573e6
--- /dev/null
+++ b/engines/support/README.md
@@ -0,0 +1 @@
+Implements a simple, clean, and easy to use help ticketing system. \ No newline at end of file
diff --git a/engines/support/Rakefile b/engines/support/Rakefile
new file mode 100644
index 0000000..0e73163
--- /dev/null
+++ b/engines/support/Rakefile
@@ -0,0 +1,44 @@
+#!/usr/bin/env rake
+
+require 'rake/packagetask'
+require 'rubygems/package_task'
+
+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 = 'LeapWebHelp'
+ rdoc.options << '--line-numbers'
+ rdoc.rdoc_files.include('README.rdoc')
+ rdoc.rdoc_files.include('lib/**/*.rb')
+end
+
+spec = eval(File.read('leap_web_help.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/engines/support/app/assets/javascripts/tickets.js b/engines/support/app/assets/javascripts/tickets.js
new file mode 100644
index 0000000..18537aa
--- /dev/null
+++ b/engines/support/app/assets/javascripts/tickets.js
@@ -0,0 +1,4 @@
+//$(document).ready(function () {
+// $.fn.editable.defaults.mode = 'inline';
+// $('#subject').editable();
+//}); \ No newline at end of file
diff --git a/engines/support/app/controllers/tickets_controller.rb b/engines/support/app/controllers/tickets_controller.rb
new file mode 100644
index 0000000..99357ab
--- /dev/null
+++ b/engines/support/app/controllers/tickets_controller.rb
@@ -0,0 +1,161 @@
+class TicketsController < ApplicationController
+ include AutoTicketsPathHelper
+
+ respond_to :html, :json
+ #has_scope :open, :type => boolean
+
+ before_filter :require_login, :only => [:index]
+ before_filter :fetch_ticket, :only => [:show, :update, :destroy]
+ before_filter :require_ticket_access, :only => [:show, :update, :destroy]
+ before_filter :fetch_user
+ before_filter :set_title
+
+ def new
+ @ticket = Ticket.new
+ @ticket.created_by = current_user.id
+ @ticket.comments.build
+ end
+
+ def create
+ @ticket = Ticket.new(params[:ticket])
+
+ #protecting posted_by isn't working, so this should protect it:
+ @ticket.comments.last.posted_by = current_user.id
+ @ticket.comments.last.private = false unless admin?
+ @ticket.created_by = current_user.id
+ if @ticket.save
+ flash[:notice] = t(:thing_was_successfully_created, :thing => t(:ticket))
+ if !logged_in?
+ flash[:notice] += " " + t(:access_ticket_text, :full_url => ticket_url(@ticket.id))
+ end
+ end
+ respond_with(@ticket, :location => auto_ticket_path(@ticket))
+ end
+
+ def show
+ @comment = TicketComment.new
+ if !@ticket
+ redirect_to auto_tickets_path, :alert => t(:no_such_thing, :thing => t(:ticket))
+ return
+ end
+ end
+
+ def update
+ if params[:button] == 'close'
+ @ticket.is_open = false
+ @ticket.save
+ redirect_to_tickets
+ elsif params[:button] == 'open'
+ @ticket.is_open = true
+ @ticket.save
+ redirect_to auto_ticket_path(@ticket)
+ else
+ @ticket.attributes = cleanup_ticket_params(params[:ticket])
+
+ if params[:button] == 'reply_and_close'
+ @ticket.close
+ end
+
+ if @ticket.comments_changed?
+ @ticket.comments.last.posted_by = current_user.id
+ @ticket.comments.last.private = false unless admin?
+ end
+
+ if @ticket.changed? and @ticket.save
+ flash[:notice] = t(:changes_saved)
+ redirect_to_tickets
+ else
+ flash[:error] = @ticket.errors.full_messages.join(". ") if @ticket.changed?
+ redirect_to auto_ticket_path(@ticket)
+ end
+ end
+ end
+
+ def index
+ @all_tickets = Ticket.search(search_options(params))
+ @tickets = @all_tickets.page(params[:page]).per(APP_CONFIG[:pagination_size])
+ end
+
+ def destroy
+ # should we allow non-admins to delete their own tickets? i don't think necessary.
+ @ticket.destroy if admin?
+ redirect_to auto_tickets_path
+ end
+
+ protected
+
+ def set_title
+ @title = t(:tickets)
+ end
+
+ private
+
+ #
+ # redirects to ticket index, if appropriate.
+ # otherwise, just redirects to @ticket
+ #
+ def redirect_to_tickets
+ if logged_in?
+ if params[:button] == t(:reply_and_close)
+ redirect_to auto_tickets_path
+ else
+ redirect_to auto_ticket_path(@ticket)
+ end
+ else
+ # if we are not logged in, there is no index to view
+ redirect_to auto_ticket_path(@ticket)
+ end
+ end
+
+ #
+ # unset comments hash if no new comment was typed
+ #
+ def cleanup_ticket_params(ticket)
+ if ticket && ticket[:comments_attributes]
+ if ticket[:comments_attributes].values.first[:body].blank?
+ ticket[:comments_attributes] = nil
+ end
+ end
+ return ticket
+ end
+
+ def fetch_ticket
+ @ticket = Ticket.find(params[:id])
+ if !@ticket
+ if admin?
+ redirect_to auto_tickets_path,
+ alert: t(:no_such_thing, thing: 'ticket')
+ else
+ access_denied
+ end
+ end
+ end
+
+ def require_ticket_access
+ access_denied unless ticket_access?
+ end
+
+ def ticket_access?
+ admin? or
+ @ticket.created_by.blank? or
+ current_user.id == @ticket.created_by
+ end
+
+ def fetch_user
+ if params[:user_id]
+ @user = User.find(params[:user_id])
+ end
+ end
+
+ #
+ # clean up params for ticket search
+ #
+ def search_options(params)
+ params.merge(
+ :admin_status => params[:user_id] ? 'mine' : 'all',
+ :user_id => @user ? @user.id : current_user.id,
+ :is_admin => admin?
+ )
+ end
+
+end
diff --git a/engines/support/app/designs/ticket/by_includes_post_by.js b/engines/support/app/designs/ticket/by_includes_post_by.js
new file mode 100644
index 0000000..2eeac89
--- /dev/null
+++ b/engines/support/app/designs/ticket/by_includes_post_by.js
@@ -0,0 +1,13 @@
+// TODO: This view is only used in tests--should we keep it?
+function(doc) {
+ var arr = {}
+ if (doc['type'] == 'Ticket' && doc.comments) {
+ doc.comments.forEach(function(comment){
+ if (comment.posted_by && !arr[comment.posted_by]) {
+ //don't add duplicates
+ arr[comment.posted_by] = true;
+ emit(comment.posted_by, 1);
+ }
+ });
+ }
+}
diff --git a/engines/support/app/designs/ticket/by_includes_post_by_and_created_at.js b/engines/support/app/designs/ticket/by_includes_post_by_and_created_at.js
new file mode 100644
index 0000000..72169b0
--- /dev/null
+++ b/engines/support/app/designs/ticket/by_includes_post_by_and_created_at.js
@@ -0,0 +1,12 @@
+function(doc) {
+ var arr = {}
+ if (doc['type'] == 'Ticket' && doc.comments) {
+ doc.comments.forEach(function(comment){
+ if (comment.posted_by && !arr[comment.posted_by]) {
+ //don't add duplicates
+ arr[comment.posted_by] = true;
+ emit([comment.posted_by, doc.created_at], 1);
+ }
+ });
+ }
+}
diff --git a/engines/support/app/designs/ticket/by_includes_post_by_and_is_open_and_created_at.js b/engines/support/app/designs/ticket/by_includes_post_by_and_is_open_and_created_at.js
new file mode 100644
index 0000000..33dfe0b
--- /dev/null
+++ b/engines/support/app/designs/ticket/by_includes_post_by_and_is_open_and_created_at.js
@@ -0,0 +1,12 @@
+function(doc) {
+ var arr = {}
+ if (doc['type'] == 'Ticket' && doc.comments) {
+ doc.comments.forEach(function(comment){
+ if (comment.posted_by && !arr[comment.posted_by]) {
+ //don't add duplicates
+ arr[comment.posted_by] = true;
+ emit([comment.posted_by, doc.is_open, doc.created_at], 1);
+ }
+ });
+ }
+}
diff --git a/engines/support/app/designs/ticket/by_includes_post_by_and_is_open_and_updated_at.js b/engines/support/app/designs/ticket/by_includes_post_by_and_is_open_and_updated_at.js
new file mode 100644
index 0000000..3bd2a74
--- /dev/null
+++ b/engines/support/app/designs/ticket/by_includes_post_by_and_is_open_and_updated_at.js
@@ -0,0 +1,12 @@
+function(doc) {
+ var arr = {}
+ if (doc['type'] == 'Ticket' && doc.comments) {
+ doc.comments.forEach(function(comment){
+ if (comment.posted_by && !arr[comment.posted_by]) {
+ //don't add duplicates
+ arr[comment.posted_by] = true;
+ emit([comment.posted_by, doc.is_open, doc.updated_at], 1);
+ }
+ });
+ }
+}
diff --git a/engines/support/app/designs/ticket/by_includes_post_by_and_updated_at.js b/engines/support/app/designs/ticket/by_includes_post_by_and_updated_at.js
new file mode 100644
index 0000000..2b4304f
--- /dev/null
+++ b/engines/support/app/designs/ticket/by_includes_post_by_and_updated_at.js
@@ -0,0 +1,12 @@
+function(doc) {
+ var arr = {}
+ if (doc['type'] == 'Ticket' && doc.comments) {
+ doc.comments.forEach(function(comment){
+ if (comment.posted_by && !arr[comment.posted_by]) {
+ //don't add duplicates
+ arr[comment.posted_by] = true;
+ emit([comment.posted_by, doc.updated_at], 1);
+ }
+ });
+ }
+}
diff --git a/engines/support/app/helpers/auto_tickets_path_helper.rb b/engines/support/app/helpers/auto_tickets_path_helper.rb
new file mode 100644
index 0000000..5638222
--- /dev/null
+++ b/engines/support/app/helpers/auto_tickets_path_helper.rb
@@ -0,0 +1,54 @@
+#
+# These "auto" forms of the normal ticket path route helpers allow us to do two things automatically:
+#
+# (1) include the user in the path if appropriate.
+# (2) retain the sort params, if appropriate.
+#
+# Tickets views with a user_id are limited to that user. For admins, they don't need a user_id for any ticket action.
+#
+# This is available both to the views and the tickets_controller.
+#
+module AutoTicketsPathHelper
+
+ protected
+
+ def auto_tickets_path(options={})
+ return unless options.class == Hash
+ options = ticket_view_options.merge options
+ if @user
+ user_tickets_path(@user, options)
+ else
+ tickets_path(options)
+ end
+ end
+
+ def auto_ticket_path(ticket, options={})
+ return unless ticket.persisted?
+ options = ticket_view_options.merge options
+ if @user
+ user_ticket_path(@user, ticket, options)
+ else
+ ticket_path(ticket, options)
+ end
+ end
+
+ def auto_new_ticket_path(options={})
+ return unless options.class == Hash
+ options = ticket_view_options.merge options
+ if @user
+ new_user_ticket_path(@user, options)
+ else
+ new_ticket_path(options)
+ end
+ end
+
+ private
+
+ def ticket_view_options
+ hsh = {}
+ hsh[:open_status] = params[:open_status] if params[:open_status] && !params[:open_status].empty?
+ hsh[:sort_order] = params[:sort_order] if params[:sort_order] && !params[:sort_order].empty?
+ hsh
+ end
+
+end
diff --git a/engines/support/app/helpers/tickets_helper.rb b/engines/support/app/helpers/tickets_helper.rb
new file mode 100644
index 0000000..7af50d6
--- /dev/null
+++ b/engines/support/app/helpers/tickets_helper.rb
@@ -0,0 +1,76 @@
+module TicketsHelper
+ #
+ # FORM HELPERS
+ #
+
+ #
+ # hidden fields that should be added to ever ticket form.
+ # these are use for proper redirection after successful actions.
+ #
+ def hidden_ticket_fields
+ haml_concat hidden_field_tag('open_status', params[:open_status])
+ haml_concat hidden_field_tag('sort_order', params[:sort_order])
+ haml_concat hidden_field_tag('user_id', params[:user_id])
+ ""
+ end
+
+ #
+ # PARAM HELPERS
+ #
+
+ def search_status
+ if action?(:index)
+ params[:open_status] || 'open'
+ else
+ nil
+ end
+ end
+
+ def search_order
+ params[:sort_order] || 'updated_at_desc'
+ end
+
+ #
+ # LINK HELPERS
+ #
+
+ def link_to_status(new_status)
+ if new_status == "open"
+ label = t(:open_tickets)
+ elsif new_status == "closed"
+ label = t(:closed_tickets)
+ elsif new_status == "all"
+ label = t(:all_tickets)
+ end
+ link_to label, auto_tickets_path(:open_status => new_status, :sort_order => search_order)
+ end
+
+ def link_to_order(order_field)
+ if search_order.start_with?(order_field)
+ # link for currently-filtered field. Link to other direction of this field.
+ if search_order.end_with? 'asc'
+ direction = 'desc'
+ icon_direction = 'up'
+ else
+ direction = 'asc'
+ icon_direction = 'down'
+ end
+ arrow = content_tag(:i, '', class: 'icon-arrow-'+ icon_direction)
+ else
+ # for not-currently-filtered field, don't display an arrow, and link to descending direction
+ arrow = ''
+ direction = 'desc'
+ end
+
+ if order_field == 'updated'
+ label = t(:updated)
+ elsif order_field == 'created'
+ label = t(:created)
+ end
+
+ link_to auto_tickets_path(:sort_order => order_field + '_at_' + direction, :open_status => search_status) do
+ arrow + label
+ end
+ end
+
+end
diff --git a/engines/support/app/models/account_extension/tickets.rb b/engines/support/app/models/account_extension/tickets.rb
new file mode 100644
index 0000000..f898b56
--- /dev/null
+++ b/engines/support/app/models/account_extension/tickets.rb
@@ -0,0 +1,13 @@
+module AccountExtension::Tickets
+ extend ActiveSupport::Concern
+
+ def destroy_with_tickets
+ Ticket.destroy_all_from(self.user)
+ destroy_without_tickets
+ end
+
+ included do
+ alias_method_chain :destroy, :tickets
+ end
+
+end
diff --git a/engines/support/app/models/ticket.rb b/engines/support/app/models/ticket.rb
new file mode 100644
index 0000000..bf5df53
--- /dev/null
+++ b/engines/support/app/models/ticket.rb
@@ -0,0 +1,112 @@
+#
+# TODO: thought i should reverse keys for descending, but that didn't work.
+# look into whether that should be tweaked, and whether it works okay with
+# pagination (seems to now...)
+#
+# TODO: better validation of email
+#
+# TODO: don't hardcode strings 'unknown user' and 'unauthenticated user'
+#
+class Ticket < CouchRest::Model::Base
+ use_database "tickets"
+
+ property :created_by, String, :protected => true # nil for anonymous tickets, should never be changed
+ property :regarding_user, String # may be nil or valid username
+ property :subject, String
+ property :email, String
+ property :is_open, TrueClass, :default => true
+ property :comments, [TicketComment]
+
+ timestamps!
+
+ design do
+ view :by_updated_at
+ view :by_created_at
+ view :by_created_by
+
+ view :by_is_open_and_created_at
+ view :by_is_open_and_updated_at
+
+ own_path = Pathname.new(File.dirname(__FILE__))
+ load_views(own_path.join('..', 'designs', 'ticket'))
+ end
+
+ validates :subject, :presence => true
+
+ # email can have three states:
+ # * nil - prefilled with created_by's email
+ # * "" - cleared
+ # * valid email address
+ validates :email, :allow_blank => true, :format => /\A(([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,}))?\Z/
+
+ def self.search(options = {})
+ @selection = TicketSelection.new(options)
+ @selection.tickets
+ end
+
+ def self.destroy_all_from(user)
+ self.by_created_by.key(user.id).each do |ticket|
+ ticket.destroy
+ end
+ end
+
+ def is_creator_validated?
+ created_by_user.is_a? User
+ end
+
+ def email
+ read_attribute(:email) || created_by_user.email
+ end
+
+ def regarding_user
+ read_attribute(:regarding_user) || created_by_user.login
+ end
+
+ def close
+ self.is_open = false
+ end
+
+ def reopen
+ self.is_open = true
+ end
+
+ def commenters
+ commenters = []
+ self.comments.each do |comment|
+ if comment.posted_by
+ if user = User.find(comment.posted_by)
+ commenters << user.login if user and !commenters.include?(user.login)
+ else
+ commenters << 'unknown user' if !commenters.include?('unknown user')
+ end
+ else
+ commenters << 'unauthenticated user' if !commenters.include?('unauthenticated user')
+ end
+ end
+ commenters.join(', ')
+ end
+
+ #
+ # update comments. User should be set by controller.
+ #
+ def comments_attributes=(attributes)
+ if attributes
+ comment = TicketComment.new(attributes.values.first)
+ comment.posted_at = Time.now
+ comments << comment
+ end
+ end
+
+ def created_by_user
+ if self.created_by
+ User.find(self.created_by) || AnonymousUser.new
+ else
+ AnonymousUser.new
+ end
+ end
+
+ def regarding_user_actual_user
+ User.find_by_login(self.regarding_user)
+ end
+
+end
diff --git a/engines/support/app/models/ticket_comment.rb b/engines/support/app/models/ticket_comment.rb
new file mode 100644
index 0000000..bed5237
--- /dev/null
+++ b/engines/support/app/models/ticket_comment.rb
@@ -0,0 +1,43 @@
+class TicketComment
+ include CouchRest::Model::Embeddable
+
+ #belongs_to :ticket #is this best way to do it? will want to access all of a tickets comments, so maybe this isn't the way?
+ property :posted_by, String#, :protected => true #Integer#this should be current_user if that is set, meaning the user is logged in #cannot have it be protected and set via comments_attributes=. also, if it is protected and we set in the tickets_controller, it gets unset. TODO---is this okay to have it not protected and manually check it? We do not users to be able to set this.
+ # if the current user is not set, then we could just say the comment comes from an 'unauthenticated user', which would be somebody with the secret URL
+ property :posted_at, Time#, :protected => true
+ #property :posted_verified, TrueClass, :protected => true #should be true if current_user is set when the comment is created
+ property :body, String
+ property :private, TrueClass # private comments are only viewable by admins #this is checked when set, to make sure it was set by an admin
+
+ # ? timestamps!
+ validates :body, :presence => true
+ #before_validation :set_time#, :set_posted_by
+
+ #design do
+ # view :by_posted_at
+ # view :by_body
+ #end
+
+ def is_comment_validated?
+ !!posted_by
+ end
+
+ def posted_by_user
+ User.find(posted_by) if posted_by
+ end
+
+=begin
+ #TODO.
+ #this is resetting all comments associated with the ticket:
+ def set_time
+ self.posted_at = Time.now
+ end
+=end
+
+=begin
+ def set_posted_by
+ self.posted_by = User.current if User.current
+ end
+=end
+
+end
diff --git a/engines/support/app/models/ticket_selection.rb b/engines/support/app/models/ticket_selection.rb
new file mode 100644
index 0000000..74d5b78
--- /dev/null
+++ b/engines/support/app/models/ticket_selection.rb
@@ -0,0 +1,71 @@
+class TicketSelection
+
+ #
+ # supported options:
+ #
+ # user_id: id of the user (uuid string)
+ # open_status: open | closed | all
+ # sort_order: updated_at_desc | updated_at_asc | created_at_desc | created_at_asc
+ # admin_status: mine | all
+ # is_admin: true | false
+ #
+ def initialize(options = {})
+ @user_id = options[:user_id].gsub /[^a-z0-9]/, ''
+ @open_status = allow options[:open_status], 'open', 'closed', 'all'
+ @sort_order = allow options[:sort_order], 'updated_at_desc', 'updated_at_asc', 'created_at_desc', 'created_at_asc'
+ @admin_status = allow options[:admin_status], 'mine', 'all'
+ @is_admin = allow options[:is_admin], false, true
+ end
+
+ def tickets
+ Ticket.send(finder_method).startkey(startkey).endkey(endkey).send(order)
+ end
+
+ protected
+
+ def allow(source, *allowed)
+ if allowed.include?(source)
+ source
+ else
+ allowed.first
+ end
+ end
+
+ def finder_method
+ method = 'by_'
+ method += 'includes_post_by_and_' if only_mine?
+ method += 'is_open_and_' if @open_status != 'all'
+ method += @sort_order.sub(/_(de|a)sc$/, '')
+ end
+
+ def startkey
+ startkeys = []
+ startkeys << @user_id if only_mine?
+ startkeys << (@open_status == 'open') if @open_status != 'all'
+ startkeys << 0
+ startkeys = startkeys.join if startkeys.length == 1 # want string not array if just one thing in array
+ startkeys
+ end
+
+ def endkey
+ endtime = Time.now + 2.days # TODO. this obviously isn't ideal
+ if self.startkey.is_a?(Array)
+ endkeys = self.startkey
+ endkeys.pop
+ endkeys << endtime
+ else
+ endtime
+ end
+ end
+
+ def order
+ # we have defined the ascending method to return the view itself:
+ (@sort_order.end_with? 'desc') ? 'descending' : 'ascending'
+ end
+
+
+ def only_mine?
+ !@is_admin || @admin_status == 'mine'
+ end
+
+end
diff --git a/engines/support/app/views/tickets/_comment.html.haml b/engines/support/app/views/tickets/_comment.html.haml
new file mode 100644
index 0000000..778ca13
--- /dev/null
+++ b/engines/support/app/views/tickets/_comment.html.haml
@@ -0,0 +1,20 @@
+- if admin? or !comment.private # only show comment if user is admin or comment is not private
+ %tr
+ %td.user
+ %div
+ %strong
+ - if comment.posted_by_user
+ = comment.posted_by_user.login
+ - else
+ = t(:anonymous)
+ %div= comment.posted_at.to_s(:short)
+ - if comment.posted_by_user && comment.posted_by_user.is_admin?
+ %div
+ %span.label.label-inverse
+ = t(:admin)
+ - if comment.private
+ %div
+ %span.label.label-important
+ = t(:private)
+ %td.comment
+ = simple_format(comment.body) \ No newline at end of file
diff --git a/engines/support/app/views/tickets/_edit_form.html.haml b/engines/support/app/views/tickets/_edit_form.html.haml
new file mode 100644
index 0000000..b8da779
--- /dev/null
+++ b/engines/support/app/views/tickets/_edit_form.html.haml
@@ -0,0 +1,50 @@
+:ruby
+ # created by user link
+ if @ticket.is_creator_validated?
+ created_by = link_to @ticket.created_by_user.login, @ticket.created_by_user
+ else
+ created_by = t(:anonymous)
+ end
+
+ # regarding user link
+ if admin?
+ if @ticket.regarding_user_actual_user
+ regarding_user_link = link_to @ticket.regarding_user_actual_user.login, @ticket.regarding_user_actual_user
+ else
+ regarding_user_link = "(#{t(:unknown)})"
+ end
+ else
+ regarding_user_link = ''
+ end
+
+= simple_form_for @ticket do |f|
+ = hidden_ticket_fields
+ %p.first
+ - if @ticket.is_open?
+ %span.label.label-info
+ %b{style: 'padding:10px'}= t(:open)
+ = f.button :loading, t(:close), value: 'close', class: 'btn-mini'
+ - else
+ %span.label.label-success
+ %b{style: 'padding:10px'}= t(:closed)
+ = f.button :loading, t(:open), value: 'open', class: 'btn-mini'
+ %span.label.label-clear= t(:created_by_on, :user => created_by, :time => @ticket.created_at.to_s(:short)).html_safe
+= simple_form_for @ticket do |f|
+ = hidden_ticket_fields
+ %div= t(:subject)
+ = f.text_field :subject, :class => 'large full-width'
+ .row-fluid
+ .span4
+ %div= t(:status)
+ = f.select :is_open, [[t(:open), "true"], [t(:closed), "false"]]
+ .span4
+ %div= t(:email)
+ = f.text_field :email
+ .span4
+ %div
+ = t(:regarding_account)
+ = regarding_user_link
+ = f.text_field :regarding_user
+ = f.button :loading, t(:save), :value => 'save'
+ - if admin?
+ = link_to t(:destroy), auto_ticket_path(@ticket), :confirm => t(:are_you_sure), :method => :delete, :class => 'btn'
diff --git a/engines/support/app/views/tickets/_new_comment_form.html.haml b/engines/support/app/views/tickets/_new_comment_form.html.haml
new file mode 100644
index 0000000..40c737f
--- /dev/null
+++ b/engines/support/app/views/tickets/_new_comment_form.html.haml
@@ -0,0 +1,13 @@
+-#
+-# for posting a new comment to an existing ticket.
+-#
+= simple_form_for @ticket, :html => {:class => 'slim'} do |f|
+ = hidden_ticket_fields
+ = f.simple_fields_for :comments, @comment, :wrapper => :none, :html => {:class => 'slim'} do |c|
+ = c.input :body, :label => false, :as => :text, :input_html => {:class => "full-width", :rows=> 5}
+ - if admin?
+ = c.input :private, :as => :boolean, :label => false, :inline_label => true
+ = f.button :loading, t(:post_reply), class: 'btn-primary', value: 'post_reply'
+ - if logged_in? && @ticket.is_open
+ = f.button :loading, t(:reply_and_close), value: 'reply_and_close'
+ = link_to t(:cancel), auto_tickets_path, :class => :btn
diff --git a/engines/support/app/views/tickets/_tabs.html.haml b/engines/support/app/views/tickets/_tabs.html.haml
new file mode 100644
index 0000000..445a909
--- /dev/null
+++ b/engines/support/app/views/tickets/_tabs.html.haml
@@ -0,0 +1,23 @@
+-#
+-# SORT ORDER TABS
+-#
+- unless action?(:new) or action?(:create)
+ %ul.nav.nav-pills.pull-right.slim
+ %li{:class=> ("active" if search_order.start_with? 'created_at')}
+ = link_to_order('created')
+ %li{:class=> ("active" if search_order.start_with? 'updated_at')}
+ = link_to_order('updated')
+
+-#
+-# STATUS FILTER TABS
+-#
+%ul.nav.nav-tabs
+ - if logged_in?
+ %li{:class => ("active" if search_status == 'open')}
+ = link_to_status 'open'
+ %li{:class => ("active" if search_status == 'closed')}
+ = link_to_status 'closed'
+ %li{:class => ("active" if search_status == 'all')}
+ = link_to_status 'all'
+ %li{:class => ("active" if action?(:new) || action?(:create))}
+ = link_to icon(:plus, :black) + t(:new_ticket), auto_new_ticket_path
diff --git a/engines/support/app/views/tickets/_ticket.html.haml b/engines/support/app/views/tickets/_ticket.html.haml
new file mode 100644
index 0000000..5bc33c8
--- /dev/null
+++ b/engines/support/app/views/tickets/_ticket.html.haml
@@ -0,0 +1,6 @@
+- url = auto_ticket_path(ticket)
+%tr
+ %td= link_to ticket.subject, url
+ %td= link_to ticket.created_at.to_s(:short), url
+ %td= link_to ticket.updated_at.to_s(:short), url
+ %td= ticket.commenters
diff --git a/engines/support/app/views/tickets/index.html.haml b/engines/support/app/views/tickets/index.html.haml
new file mode 100644
index 0000000..a4df6e3
--- /dev/null
+++ b/engines/support/app/views/tickets/index.html.haml
@@ -0,0 +1,19 @@
+- @show_navigation = params[:user_id].present?
+
+= render 'tickets/tabs'
+
+%table.table.table-striped.table-bordered
+ %thead
+ %tr
+ %th= t(:subject)
+ %th= t(:created)
+ %th= t(:updated)
+ %th= t(:voices)
+ %tbody
+ - if @tickets.any?
+ = render @tickets.all
+ - else
+ %tr
+ %td{:colspan=>4}= t(:none)
+
+= paginate @tickets
diff --git a/engines/support/app/views/tickets/new.html.haml b/engines/support/app/views/tickets/new.html.haml
new file mode 100644
index 0000000..3de5fe9
--- /dev/null
+++ b/engines/support/app/views/tickets/new.html.haml
@@ -0,0 +1,17 @@
+- @show_navigation = params[:user_id].present?
+
+= render 'tickets/tabs'
+
+- user = @user if admin?
+- user ||= current_user
+
+= simple_form_for @ticket, :validate => true, :html => {:class => 'form-horizontal'} do |f|
+ = hidden_ticket_fields
+ = f.input :subject
+ = f.input :email
+ = f.input :regarding_user
+ = f.simple_fields_for :comments, @comment do |c|
+ = c.input :body, :label => t(:description), :as => :text, :input_html => {:class => "full-width", :rows=> 5}
+ - if admin?
+ = c.input :private, :as => :boolean, :label => false, :inline_label => true
+ = f.button :wrapped, cancel: (logged_in? ? auto_tickets_path : home_path)
diff --git a/engines/support/app/views/tickets/show.html.haml b/engines/support/app/views/tickets/show.html.haml
new file mode 100644
index 0000000..4f3c127
--- /dev/null
+++ b/engines/support/app/views/tickets/show.html.haml
@@ -0,0 +1,12 @@
+- @show_navigation = params[:user_id].present?
+
+.ticket
+ = render 'tickets/edit_form'
+ %table.table.table-striped.table-bordered
+ %tbody
+ = render :partial => 'tickets/comment', :collection => @ticket.comments
+ %tr
+ %td.user
+ = current_user.login || t(:anonymous)
+ %td.comment
+ = render 'tickets/new_comment_form'
diff --git a/engines/support/config/initializers/account_lifecycle.rb b/engines/support/config/initializers/account_lifecycle.rb
new file mode 100644
index 0000000..d9f04c1
--- /dev/null
+++ b/engines/support/config/initializers/account_lifecycle.rb
@@ -0,0 +1,3 @@
+ActiveSupport.on_load(:account) do
+ include AccountExtension::Tickets
+end
diff --git a/engines/support/config/locales/en.yml b/engines/support/config/locales/en.yml
new file mode 100644
index 0000000..342adea
--- /dev/null
+++ b/engines/support/config/locales/en.yml
@@ -0,0 +1,22 @@
+en:
+ access_ticket_text: >
+ You can later access this ticket at the URL %{full_url}. You might want to bookmark this page to find it again.
+ Anybody with this URL will be able to access this ticket, so if you are on a shared computer you might want to
+ remove it from the browser history.
+ support_tickets: "Support Tickets"
+ all_tickets: "All Tickets"
+ my_tickets: "My Tickets"
+ open_tickets: "Open Tickets"
+ closed_tickets: "Closed Tickets"
+ new_ticket: "New Ticket"
+ tickets: "Tickets"
+ subject: "Subject"
+ destroy: "Destroy"
+ open: "Open"
+ closed: "Closed"
+ close: "Close"
+ post_reply: "Post Reply"
+ reply_and_close: "Reply and Close"
+ description: "Description"
+ ticket: "Ticket"
+ regarding_account: "Regarding Account" \ No newline at end of file
diff --git a/engines/support/config/routes.rb b/engines/support/config/routes.rb
new file mode 100644
index 0000000..23e0c11
--- /dev/null
+++ b/engines/support/config/routes.rb
@@ -0,0 +1,8 @@
+Rails.application.routes.draw do
+ scope "(:locale)", :locale => MATCH_LOCALE do
+ resources :tickets, :except => :edit
+ resources :users do
+ resources :tickets, :except => :edit
+ end
+ end
+end
diff --git a/engines/support/leap_web_help.gemspec b/engines/support/leap_web_help.gemspec
new file mode 100644
index 0000000..7b668d5
--- /dev/null
+++ b/engines/support/leap_web_help.gemspec
@@ -0,0 +1,18 @@
+$:.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_help"
+ s.version = LeapWeb::VERSION
+ s.authors = ["Jessib"]
+ s.email = ["jessib@leap.se"]
+ s.homepage = "http://www.leap.se"
+ s.summary = "Help Desk for LeapWeb"
+ s.description = "Managing Tickets for a Leap provider"
+
+ s.files = Dir["{app,config,lib}/**/*"] + ["Rakefile", "README.md"]
+ s.test_files = Dir["test/**/*"]
+
+end
diff --git a/engines/support/lib/leap_web_help.rb b/engines/support/lib/leap_web_help.rb
new file mode 100644
index 0000000..f5b04aa
--- /dev/null
+++ b/engines/support/lib/leap_web_help.rb
@@ -0,0 +1,4 @@
+require "leap_web_help/engine"
+
+module LeapWebHelp
+end
diff --git a/engines/support/lib/leap_web_help/engine.rb b/engines/support/lib/leap_web_help/engine.rb
new file mode 100644
index 0000000..dfa763f
--- /dev/null
+++ b/engines/support/lib/leap_web_help/engine.rb
@@ -0,0 +1,4 @@
+module LeapWebHelp
+ class Engine < ::Rails::Engine
+ end
+end
diff --git a/engines/support/lib/tasks/leap_web_help_tasks.rake b/engines/support/lib/tasks/leap_web_help_tasks.rake
new file mode 100644
index 0000000..1f38982
--- /dev/null
+++ b/engines/support/lib/tasks/leap_web_help_tasks.rake
@@ -0,0 +1,4 @@
+# desc "Explaining what the task does"
+# task :leap_web_help do
+# # Task goes here
+# end
diff --git a/engines/support/script/rails b/engines/support/script/rails
new file mode 100755
index 0000000..5676ab9
--- /dev/null
+++ b/engines/support/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_help/engine', __FILE__)
+
+require 'rails/all'
+require 'rails/engine/commands'
diff --git a/engines/support/test/factories.rb b/engines/support/test/factories.rb
new file mode 100644
index 0000000..be04f15
--- /dev/null
+++ b/engines/support/test/factories.rb
@@ -0,0 +1,18 @@
+FactoryGirl.define do
+
+ factory :ticket do
+ subject { Faker::Lorem.sentence }
+ email { Faker::Internet.email }
+
+ factory :ticket_with_comment do
+ comments_attributes do
+ { "0" => { "body" => Faker::Lorem.sentences.join(" ") } }
+ end
+ end
+
+ factory :ticket_with_creator do
+ created_by { FactoryGirl.create(:user).id }
+ end
+ end
+
+end
diff --git a/engines/support/test/functional/tickets_controller_test.rb b/engines/support/test/functional/tickets_controller_test.rb
new file mode 100644
index 0000000..fc4a6f8
--- /dev/null
+++ b/engines/support/test/functional/tickets_controller_test.rb
@@ -0,0 +1,284 @@
+require 'test_helper'
+
+class TicketsControllerTest < ActionController::TestCase
+
+ teardown do
+ # destroy all tickets that were created during the test
+ Ticket.all.each{|t| t.destroy}
+ end
+
+ test "should get index if logged in" do
+ login
+ get :index
+ assert_response :success
+ assert_not_nil assigns(:tickets)
+ end
+
+ test "no index if not logged in" do
+ get :index
+ assert_response :redirect
+ assert_nil assigns(:tickets)
+ end
+
+ test "should get new" do
+ get :new
+ assert_equal Ticket, assigns(:ticket).class
+ assert_response :success
+ end
+
+ test "unauthenticated tickets are visible" do
+ ticket = find_record :ticket, :created_by => nil
+ get :show, :id => ticket.id
+ assert_response :success
+ end
+
+ test "user tickets are not visible without login" do
+ user = find_record :user
+ ticket = find_record :ticket, :created_by => user.id
+ get :show, :id => ticket.id
+ assert_response :redirect
+ assert_redirected_to login_url
+ end
+
+ test "user tickets are visible to creator" do
+ user = find_record :user
+ ticket = find_record :ticket, :created_by => user.id
+ login user
+ get :show, :id => ticket.id
+ assert_response :success
+ end
+
+ test "other users tickets are not visible" do
+ other_user = find_record :user
+ ticket = find_record :ticket, :created_by => other_user.id
+ login
+ get :show, :id => ticket.id
+ assert_response :redirect
+ assert_redirected_to home_url
+ end
+
+ test "should create unauthenticated ticket" do
+ params = {:subject => "unauth ticket test subject", :comments_attributes => {"0" => {"body" =>"body of test ticket"}}}
+
+ assert_difference('Ticket.count') do
+ post :create, :ticket => params
+ end
+
+ assert_response :redirect
+ assert_nil assigns(:ticket).created_by
+
+ assert_equal 1, assigns(:ticket).comments.count
+ assert_nil assigns(:ticket).comments.first.posted_by
+
+ end
+
+ test "handle invalid ticket" do
+ params = {:subject => "unauth ticket test subject", :comments_attributes => {"0" => {"body" =>"body of test ticket"}}, :email => 'a'}
+
+ assert_no_difference('Ticket.count') do
+ post :create, :ticket => params
+ end
+
+ assert_template :new
+ assert_equal params[:subject], assigns(:ticket).subject
+ end
+
+ test "should create authenticated ticket" do
+
+ params = {:subject => "auth ticket test subject",:email => "", :comments_attributes => {"0" => {"body" =>"body of test ticket"}}}
+
+ login
+
+ assert_difference('Ticket.count') do
+ post :create, :ticket => params
+ end
+
+ assert_response :redirect
+
+ assert_not_nil assigns(:ticket).created_by
+ assert_equal assigns(:ticket).created_by, @current_user.id
+ assert_equal "", assigns(:ticket).email
+
+ assert_equal 1, assigns(:ticket).comments.count
+ assert_not_nil assigns(:ticket).comments.first.posted_by
+ assert_equal assigns(:ticket).comments.first.posted_by, @current_user.id
+ end
+
+ test "add comment to unauthenticated ticket" do
+ ticket = FactoryGirl.create :ticket, :created_by => nil
+
+ assert_difference('Ticket.find(ticket.id).comments.count') do
+ put :update, :id => ticket.id,
+ :ticket => {:comments_attributes => {"0" => {"body" =>"NEWER comment"}} }
+ end
+
+ assert_equal ticket, assigns(:ticket) # still same ticket, with different comments
+ assert_not_equal ticket.comments, assigns(:ticket).comments # ticket == assigns(:ticket), but they have different comments (which we want)
+
+ end
+
+
+ test "add comment to own authenticated ticket" do
+
+ login
+ ticket = FactoryGirl.create :ticket, :created_by => @current_user.id
+
+ #they should be able to comment if it is their ticket:
+ assert_difference('Ticket.find(ticket.id).comments.count') do
+ put :update, :id => ticket.id,
+ :ticket => {:comments_attributes => {"0" => {"body" =>"NEWER comment"}} }
+ end
+ assert_not_equal ticket.comments, assigns(:ticket).comments
+ assert_not_nil assigns(:ticket).comments.last.posted_by
+ assert_equal assigns(:ticket).comments.last.posted_by, @current_user.id
+
+ end
+
+
+ test "cannot comment if it is not your ticket" do
+
+ other_user = find_record :user
+ login :is_admin? => false, :email => nil
+ ticket = FactoryGirl.create :ticket, :created_by => other_user.id
+ # they should *not* be able to comment if it is not their ticket
+ put :update, :id => ticket.id, :ticket => {:comments_attributes => {"0" => {"body" =>"not allowed comment"}} }
+ assert_response :redirect
+ assert_access_denied
+
+ assert_equal ticket.comments.map(&:body), assigns(:ticket).comments.map(&:body)
+
+ end
+
+
+ test "admin add comment to authenticated ticket" do
+
+ other_user = find_record :user
+ login :is_admin? => true
+
+ ticket = FactoryGirl.create :ticket, :created_by => other_user.id
+
+ #admin should be able to comment:
+ assert_difference('Ticket.find(ticket.id).comments.count') do
+ put :update, :id => ticket.id,
+ :ticket => {:comments_attributes => {"0" => {"body" =>"NEWER comment"}} }
+ end
+ assert_not_equal ticket.comments, assigns(:ticket).comments
+ assert_not_nil assigns(:ticket).comments.last.posted_by
+ assert_equal assigns(:ticket).comments.last.posted_by, @current_user.id
+ end
+
+ test "tickets by admin" do
+ other_user = find_record :user
+ ticket = FactoryGirl.create :ticket, :created_by => other_user.id
+
+ login :is_admin? => true
+
+ get :index, {:admin_status => "all", :open_status => "open"}
+ assert assigns(:all_tickets).count > 0
+
+ # if we close one ticket, the admin should have 1 less open ticket
+ assert_difference('assigns[:all_tickets].count', -1) do
+ assigns(:tickets).first.close
+ assigns(:tickets).first.save
+ get :index, {:admin_status => "all", :open_status => "open"}
+ end
+ end
+
+
+ test "admin_status mine vs all" do
+ testticket = FactoryGirl.create :ticket
+ user = find_record :user
+ login :is_admin? => true, :email => nil
+
+ get :index, {:open_status => "open"}
+ assert assigns(:all_tickets).include?(testticket)
+ get :index, {:user_id => user.id, :open_status => "open"}
+ assert !assigns(:all_tickets).include?(testticket)
+ end
+
+ test "commenting on a ticket adds to tickets that are mine" do
+ testticket = FactoryGirl.create :ticket
+ user = find_record :admin_user
+ login user
+ get :index, {:user_id => user.id, :open_status => "open"}
+ assert_difference('assigns[:all_tickets].count') do
+ put :update, :id => testticket.id, :ticket => {:comments_attributes => {"0" => {"body" =>"NEWER comment"}}}
+ get :index, {:user_id => user.id, :open_status => "open"}
+ end
+
+ assert assigns(:all_tickets).include?(assigns(:ticket))
+ assert_not_nil assigns(:ticket).comments.last.posted_by
+ assert_equal assigns(:ticket).comments.last.posted_by, @current_user.id
+ end
+
+ test "admin ticket ordering" do
+ tickets = FactoryGirl.create_list :ticket, 2
+
+ login :is_admin? => true, :email => nil
+ get :index, {:admin_status => "all", :open_status => "open", :sort_order => 'created_at_desc'}
+
+ # this will consider all tickets, not just those on first page
+ first_tick = assigns(:all_tickets).all.first
+ last_tick = assigns(:all_tickets).all.last
+ assert first_tick.created_at > last_tick.created_at
+
+ # and now reverse order:
+ get :index, {:admin_status => "all", :open_status => "open", :sort_order => 'created_at_asc'}
+
+ assert_equal first_tick, assigns(:all_tickets).last
+ assert_equal last_tick, assigns(:all_tickets).first
+
+ assert_not_equal first_tick, assigns(:all_tickets).first
+ assert_not_equal last_tick, assigns(:all_tickets).last
+
+ end
+
+ test "tickets for regular user" do
+ login
+ ticket = FactoryGirl.create :ticket
+ other_ticket = FactoryGirl.create :ticket
+
+ put :update, :id => ticket.id,
+ :ticket => {:comments_attributes => {"0" => {"body" =>"NEWER comment"}} }
+ assert_not_nil assigns(:ticket).comments.last.posted_by
+ assert_equal assigns(:ticket).comments.last.posted_by, @current_user.id
+
+ get :index, {:open_status => "open"}
+ assert assigns(:all_tickets).count > 0
+ assert assigns(:all_tickets).include?(ticket)
+ assert !assigns(:all_tickets).include?(other_ticket)
+
+ # user should have one more ticket if a new tick gets a comment by this user
+ assert_difference('assigns[:all_tickets].count') do
+ put :update, :id => other_ticket.id, :ticket => {:comments_attributes => {"0" => {"body" =>"NEWER comment"}}}
+ get :index, {:open_status => "open"}
+ end
+ assert assigns(:all_tickets).include?(other_ticket)
+
+ # if we close one ticket, the user should have 1 less open ticket
+ assert_difference('assigns[:all_tickets].count', -1) do
+ other_ticket.reload
+ other_ticket.close
+ other_ticket.save
+ get :index, {:open_status => "open"}
+ end
+
+ number_open_tickets = assigns(:all_tickets).count
+
+ # look at closed tickets:
+ get :index, {:open_status => "closed"}
+ assert !assigns(:all_tickets).include?(ticket)
+ assert assigns(:all_tickets).include?(other_ticket)
+ number_closed_tickets = assigns(:all_tickets).count
+
+ # all tickets should equal closed + open
+ get :index, {:open_status => "all"}
+ assert assigns(:all_tickets).include?(ticket)
+ assert assigns(:all_tickets).include?(other_ticket)
+ assert_equal assigns(:all_tickets).count, number_closed_tickets + number_open_tickets
+
+
+ end
+
+end
+
diff --git a/engines/support/test/integration/create_ticket_test.rb b/engines/support/test/integration/create_ticket_test.rb
new file mode 100644
index 0000000..0f8453c
--- /dev/null
+++ b/engines/support/test/integration/create_ticket_test.rb
@@ -0,0 +1,64 @@
+require 'test_helper'
+
+class CreateTicketTest < BrowserIntegrationTest
+
+ test "can submit ticket anonymously" do
+ visit '/'
+ click_on 'Get Help'
+ fill_in 'Subject', with: 'test ticket'
+ fill_in 'Description', with: 'description of the problem goes here'
+ click_on 'Create Ticket'
+ assert page.has_content?("Ticket was successfully created.")
+ assert page.has_content?("You can later access this ticket at the URL")
+ assert page.has_content?(current_url)
+ assert ticket = Ticket.last
+ ticket.destroy
+ end
+
+ test "get help when creating ticket with invalid email" do
+ visit '/'
+ click_on 'Get Help'
+ fill_in 'Subject', with: 'test ticket'
+ fill_in 'Email', with: 'invalid data'
+ fill_in 'Regarding user', with: 'some user'
+ fill_in 'Description', with: 'description of the problem goes here'
+ click_on 'Create Ticket'
+ assert page.has_content?("is invalid")
+ assert_equal 'invalid data', find_field('Email').value
+ assert_equal 'some user', find_field('Regarding user').value
+ end
+
+ test "prefills fields" do
+ login FactoryGirl.create(:premium_user)
+ visit '/'
+ click_on "Support Tickets"
+ click_on "New Ticket"
+ email = "#{@user.login}@#{APP_CONFIG[:domain]}"
+ assert_equal email, find_field('Email').value
+ assert_equal @user.login, find_field('Regarding user').value
+ end
+
+ test "no prefill of email without email service" do
+ login
+ visit '/'
+ click_on "Support Tickets"
+ click_on "New Ticket"
+ assert_equal "", find_field('Email').value
+ assert_equal @user.login, find_field('Regarding user').value
+ end
+
+ test "cleared email field should remain clear" do
+ login FactoryGirl.create(:premium_user)
+ visit '/'
+ click_on "Support Tickets"
+ click_on "New Ticket"
+ fill_in 'Subject', with: 'test ticket'
+ fill_in 'Email', with: ''
+ fill_in 'Description', with: 'description of the problem goes here'
+ click_on 'Create Ticket'
+ ticket = Ticket.last
+ assert_equal "", ticket.email
+ ticket.destroy
+ end
+
+end
diff --git a/engines/support/test/integration/navigation_test.rb b/engines/support/test/integration/navigation_test.rb
new file mode 100644
index 0000000..eec8c0e
--- /dev/null
+++ b/engines/support/test/integration/navigation_test.rb
@@ -0,0 +1,9 @@
+require 'test_helper'
+
+class NavigationTest < ActionDispatch::IntegrationTest
+
+ # test "the truth" do
+ # assert true
+ # end
+end
+
diff --git a/engines/support/test/leap_web_help_test.rb b/engines/support/test/leap_web_help_test.rb
new file mode 100644
index 0000000..d74c087
--- /dev/null
+++ b/engines/support/test/leap_web_help_test.rb
@@ -0,0 +1,7 @@
+require 'test_helper'
+
+class LeapWebHelpTest < ActiveSupport::TestCase
+ test "truth" do
+ assert_kind_of Module, LeapWebHelp
+ end
+end
diff --git a/engines/support/test/test_helper.rb b/engines/support/test/test_helper.rb
new file mode 100644
index 0000000..fff9173
--- /dev/null
+++ b/engines/support/test/test_helper.rb
@@ -0,0 +1,15 @@
+# Configure Rails Environment
+ENV["RAILS_ENV"] = "test"
+
+require File.expand_path('../../../../test/dummy/config/environment', __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/engines/support/test/unit/account_extension_test.rb b/engines/support/test/unit/account_extension_test.rb
new file mode 100644
index 0000000..aba162c
--- /dev/null
+++ b/engines/support/test/unit/account_extension_test.rb
@@ -0,0 +1,12 @@
+require 'test_helper'
+
+class AccountExtensionTest < ActiveSupport::TestCase
+
+ test "destroying an account triggers ticket destruction" do
+ t = FactoryGirl.create :ticket_with_creator
+ u = t.created_by_user
+ Account.new(u).destroy
+ assert_equal nil, Ticket.find(t.id)
+ end
+
+end
diff --git a/engines/support/test/unit/ticket_comment_test.rb b/engines/support/test/unit/ticket_comment_test.rb
new file mode 100644
index 0000000..fe8cc95
--- /dev/null
+++ b/engines/support/test/unit/ticket_comment_test.rb
@@ -0,0 +1,59 @@
+require 'test_helper'
+
+class TicketCommentTest < ActiveSupport::TestCase
+ # test "the truth" do
+ # assert true
+ # end
+
+=begin
+ setup do
+ @sample_ticket = Ticket.create :title => 'test ticket'
+ @sample_ticket.save
+ end
+=end
+
+ test "create" do
+
+ comment2 = TicketComment.new :body => "help my email is broken!"
+ assert comment2.valid?
+ #assert_not_nil comment2.posted_at #?
+ #assert_nil comment2.posted_by #if not logged in #TODO
+
+ #comment.ticket = testticket #Ticket.find_by_title("testing")
+ #assert_equal testticket.title, comment.ticket.title
+
+ #tc.ticket = Ticket.find_by_title("test title")
+ #tc.ticket.title
+ end
+
+=begin
+ test "create authenticated comment" do
+ User.current = 4
+ comment2 = TicketComment.new :body => "help my email is broken!"
+ comment2.valid? #save # should not save comment
+ assert_not_nil comment2.posted_by
+ end
+=end
+
+ test "add comments" do
+ testticket = Ticket.create :subject => "testing"
+ assert_equal testticket.comments.count, 0
+ comment = TicketComment.new :body => "my email broke"
+ #assert comment.valid? #validating or saving necessary for setting posted_at
+ #assert_not_nil comment.posted_at
+
+ testticket.comments << comment
+ assert_equal testticket.comments.count, 1
+ sleep(1) # so first comment has earlier posted_at time
+ comment2 = TicketComment.new :body => "my email broke"
+ testticket.comments << comment2 #this should validate comment2
+ testticket.valid?
+ assert_equal testticket.comments.count, 2
+ testticket.reload.destroy
+ # where should posted_at be set?
+ #assert_not_nil comment.posted_at
+ #assert_not_nil testticket.comments.last.posted_at
+ #assert testticket.comments.first.posted_at < testticket.comments.last.posted_at
+ end
+
+end
diff --git a/engines/support/test/unit/ticket_test.rb b/engines/support/test/unit/ticket_test.rb
new file mode 100644
index 0000000..678d8dc
--- /dev/null
+++ b/engines/support/test/unit/ticket_test.rb
@@ -0,0 +1,88 @@
+require 'test_helper'
+
+class TicketTest < ActiveSupport::TestCase
+
+ test "ticket with default attribs is valid" do
+ t = FactoryGirl.build :ticket
+ assert t.valid?
+ end
+
+ test "ticket without email is valid" do
+ t = FactoryGirl.build :ticket, email: ""
+ assert t.valid?
+ end
+
+ test "ticket validates email format" do
+ t = FactoryGirl.build :ticket, email: "aswerssfd"
+ assert !t.valid?
+ end
+
+ test "ticket open states" do
+ t = FactoryGirl.build :ticket
+ assert t.is_open
+ t.close
+ assert !t.is_open
+ t.reopen
+ assert t.is_open
+ end
+
+ test "creation validated" do
+ user = FactoryGirl.create :user
+ @sample = Ticket.new
+ assert !@sample.is_creator_validated?
+ @sample.created_by = user.id
+ assert @sample.is_creator_validated?
+ end
+
+ test "destroy all tickets from a user" do
+ t = FactoryGirl.create :ticket_with_creator
+ u = t.created_by_user
+ Ticket.destroy_all_from(u)
+ assert_equal nil, Ticket.find(t.id)
+ end
+=begin
+# TODO: do once have current_user stuff in order
+ test "code if & only if not creator-validated" do
+ User.current_test = nil
+ t1 = Ticket.create :subject => 'test title'
+ assert_not_nil t1.code
+ assert_nil t1.created_by
+
+ User.current_test = 4
+ t2 = Ticket.create :subject => 'test title'
+ assert_nil t2.code
+ assert_not_nil t2.created_by
+ end
+=end
+
+
+ test "find tickets user commented on" do
+
+ # clear old tickets just in case
+ # this will cause RestClient::ResourceNotFound errors if there are multiple copies of the same ticket returned
+ Ticket.by_includes_post_by.key('123').each {|t| t.destroy}
+ # TODO: the by_includes_post_by view is only used for tests. Maybe we should get rid of it and change the test to including ordering?
+
+
+ testticket = Ticket.create :subject => "test retrieving commented tickets"
+ comment = TicketComment.new :body => "my email broke", :posted_by => "123"
+ assert_equal 0, testticket.comments.count
+ assert_equal [], Ticket.by_includes_post_by.key('123').all
+
+ testticket.comments << comment
+ testticket.save
+ assert_equal 1, testticket.reload.comments.count
+ assert_equal [testticket], Ticket.by_includes_post_by.key('123').all
+
+ comment = TicketComment.new :body => "another comment", :posted_by => "123"
+ testticket.comments << comment
+ testticket.save
+
+ # this will ensure that the ticket is only included once, even though the user has commented on the ticket twice:
+ assert_equal [testticket], Ticket.by_includes_post_by.key('123').all
+
+ testticket.destroy
+ assert_equal [], Ticket.by_includes_post_by.key('123').all;
+ end
+
+end