saasy 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGELOG.md +114 -0
- data/Gemfile +26 -0
- data/README.md +118 -0
- data/Rakefile +38 -0
- data/app/controllers/accounts_controller.rb +68 -0
- data/app/controllers/billings_controller.rb +25 -0
- data/app/controllers/invitations_controller.rb +65 -0
- data/app/controllers/memberships_controller.rb +45 -0
- data/app/controllers/plans_controller.rb +24 -0
- data/app/controllers/profiles_controller.rb +19 -0
- data/app/helpers/limits_helper.rb +13 -0
- data/app/mailers/billing_mailer.rb +53 -0
- data/app/mailers/invitation_mailer.rb +18 -0
- data/app/models/invitation.rb +113 -0
- data/app/models/limit.rb +49 -0
- data/app/models/membership.rb +26 -0
- data/app/models/permission.rb +19 -0
- data/app/models/signup.rb +163 -0
- data/app/views/accounts/_account.html.erb +9 -0
- data/app/views/accounts/_blank_slate.html.erb +6 -0
- data/app/views/accounts/_projects.html.erb +12 -0
- data/app/views/accounts/_subnav.html.erb +10 -0
- data/app/views/accounts/edit.html.erb +34 -0
- data/app/views/accounts/index.html.erb +9 -0
- data/app/views/accounts/new.html.erb +36 -0
- data/app/views/billing_mailer/completed_trial.text.erb +13 -0
- data/app/views/billing_mailer/expiring_trial.text.erb +15 -0
- data/app/views/billing_mailer/new_unactivated.text.erb +1 -0
- data/app/views/billing_mailer/problem.html.erb +13 -0
- data/app/views/billing_mailer/problem.text.erb +14 -0
- data/app/views/billing_mailer/receipt.html.erb +41 -0
- data/app/views/billing_mailer/receipt.text.erb +25 -0
- data/app/views/billings/_form.html.erb +8 -0
- data/app/views/billings/edit.html.erb +13 -0
- data/app/views/billings/show.html.erb +29 -0
- data/app/views/invitation_mailer/invitation.text.erb +6 -0
- data/app/views/invitations/new.html.erb +17 -0
- data/app/views/invitations/show.html.erb +22 -0
- data/app/views/layouts/saucy.html.erb +36 -0
- data/app/views/limits/_meter.html.erb +13 -0
- data/app/views/memberships/edit.html.erb +21 -0
- data/app/views/memberships/index.html.erb +17 -0
- data/app/views/plans/_plan.html.erb +32 -0
- data/app/views/plans/_terms.html.erb +15 -0
- data/app/views/plans/edit.html.erb +33 -0
- data/app/views/plans/index.html.erb +12 -0
- data/app/views/profiles/_inputs.html.erb +5 -0
- data/app/views/profiles/edit.html.erb +36 -0
- data/app/views/projects/_form.html.erb +36 -0
- data/app/views/projects/edit.html.erb +22 -0
- data/app/views/projects/index.html.erb +28 -0
- data/app/views/projects/new.html.erb +13 -0
- data/app/views/projects/show.html.erb +0 -0
- data/app/views/shared/_project_dropdown.html.erb +55 -0
- data/app/views/shared/_saucy_javascript.html.erb +33 -0
- data/config/locales/en.yml +37 -0
- data/config/routes.rb +19 -0
- data/features/run_features.feature +83 -0
- data/features/step_definitions/clearance_steps.rb +45 -0
- data/features/step_definitions/rails_steps.rb +73 -0
- data/features/step_definitions/saucy_steps.rb +8 -0
- data/features/support/env.rb +4 -0
- data/features/support/file.rb +11 -0
- data/lib/generators/saucy/base.rb +18 -0
- data/lib/generators/saucy/features/features_generator.rb +91 -0
- data/lib/generators/saucy/features/templates/README +3 -0
- data/lib/generators/saucy/features/templates/factories.rb +71 -0
- data/lib/generators/saucy/features/templates/features/edit_profile.feature +9 -0
- data/lib/generators/saucy/features/templates/features/edit_project_permissions.feature +37 -0
- data/lib/generators/saucy/features/templates/features/edit_user_permissions.feature +47 -0
- data/lib/generators/saucy/features/templates/features/manage_account.feature +35 -0
- data/lib/generators/saucy/features/templates/features/manage_billing.feature +93 -0
- data/lib/generators/saucy/features/templates/features/manage_plan.feature +143 -0
- data/lib/generators/saucy/features/templates/features/manage_projects.feature +139 -0
- data/lib/generators/saucy/features/templates/features/manage_users.feature +142 -0
- data/lib/generators/saucy/features/templates/features/new_account.feature +33 -0
- data/lib/generators/saucy/features/templates/features/project_dropdown.feature +77 -0
- data/lib/generators/saucy/features/templates/features/sign_up.feature +32 -0
- data/lib/generators/saucy/features/templates/features/sign_up_paid.feature +65 -0
- data/lib/generators/saucy/features/templates/features/trial_plans.feature +82 -0
- data/lib/generators/saucy/features/templates/step_definitions/account_steps.rb +30 -0
- data/lib/generators/saucy/features/templates/step_definitions/braintree_steps.rb +25 -0
- data/lib/generators/saucy/features/templates/step_definitions/cron_steps.rb +23 -0
- data/lib/generators/saucy/features/templates/step_definitions/email_steps.rb +40 -0
- data/lib/generators/saucy/features/templates/step_definitions/factory_girl_steps.rb +1 -0
- data/lib/generators/saucy/features/templates/step_definitions/html_steps.rb +51 -0
- data/lib/generators/saucy/features/templates/step_definitions/plan_steps.rb +16 -0
- data/lib/generators/saucy/features/templates/step_definitions/project_steps.rb +4 -0
- data/lib/generators/saucy/features/templates/step_definitions/session_steps.rb +37 -0
- data/lib/generators/saucy/features/templates/step_definitions/user_steps.rb +100 -0
- data/lib/generators/saucy/features/templates/support/braintree.rb +5 -0
- data/lib/generators/saucy/install/install_generator.rb +40 -0
- data/lib/generators/saucy/install/templates/controllers/projects_controller.rb +3 -0
- data/lib/generators/saucy/install/templates/create_saucy_tables.rb +115 -0
- data/lib/generators/saucy/install/templates/models/account.rb +3 -0
- data/lib/generators/saucy/install/templates/models/plan.rb +3 -0
- data/lib/generators/saucy/install/templates/models/project.rb +3 -0
- data/lib/generators/saucy/specs/specs_generator.rb +20 -0
- data/lib/generators/saucy/specs/templates/support/braintree.rb +5 -0
- data/lib/generators/saucy/views/views_generator.rb +23 -0
- data/lib/saucy.rb +10 -0
- data/lib/saucy/account.rb +132 -0
- data/lib/saucy/account_authorization.rb +67 -0
- data/lib/saucy/configuration.rb +29 -0
- data/lib/saucy/engine.rb +35 -0
- data/lib/saucy/fake_braintree.rb +134 -0
- data/lib/saucy/layouts.rb +36 -0
- data/lib/saucy/plan.rb +54 -0
- data/lib/saucy/project.rb +125 -0
- data/lib/saucy/projects_controller.rb +94 -0
- data/lib/saucy/railties/tasks.rake +28 -0
- data/lib/saucy/routing_extensions.rb +121 -0
- data/lib/saucy/subscription.rb +237 -0
- data/lib/saucy/user.rb +30 -0
- data/spec/controllers/accounts_controller_spec.rb +228 -0
- data/spec/controllers/application_controller_spec.rb +32 -0
- data/spec/controllers/invitations_controller_spec.rb +215 -0
- data/spec/controllers/memberships_controller_spec.rb +117 -0
- data/spec/controllers/plans_controller_spec.rb +13 -0
- data/spec/controllers/profiles_controller_spec.rb +48 -0
- data/spec/controllers/projects_controller_spec.rb +216 -0
- data/spec/environment.rb +95 -0
- data/spec/layouts_spec.rb +21 -0
- data/spec/mailers/billing_mailer_spec.rb +68 -0
- data/spec/mailers/invitiation_mailer_spec.rb +19 -0
- data/spec/models/account_spec.rb +218 -0
- data/spec/models/invitation_spec.rb +320 -0
- data/spec/models/limit_spec.rb +70 -0
- data/spec/models/membership_spec.rb +37 -0
- data/spec/models/permission_spec.rb +30 -0
- data/spec/models/plan_spec.rb +81 -0
- data/spec/models/project_spec.rb +223 -0
- data/spec/models/signup_spec.rb +177 -0
- data/spec/models/subscription_spec.rb +481 -0
- data/spec/models/user_spec.rb +72 -0
- data/spec/route_extensions_spec.rb +51 -0
- data/spec/saucy_spec.rb +62 -0
- data/spec/scaffold/config/routes.rb +5 -0
- data/spec/spec_helper.rb +39 -0
- data/spec/support/authentication_helpers.rb +81 -0
- data/spec/support/authorization_helpers.rb +56 -0
- data/spec/support/braintree.rb +7 -0
- data/spec/support/clearance_matchers.rb +55 -0
- data/spec/support/notifications.rb +57 -0
- data/spec/views/accounts/_account.html.erb_spec.rb +37 -0
- metadata +325 -0
@@ -0,0 +1,67 @@
|
|
1
|
+
module Saucy
|
2
|
+
module AccountAuthorization
|
3
|
+
extend ActiveSupport::Concern
|
4
|
+
|
5
|
+
included do
|
6
|
+
helper_method :current_account, :current_project, :current_account?, :current_project?
|
7
|
+
include InstanceMethods
|
8
|
+
end
|
9
|
+
|
10
|
+
module InstanceMethods
|
11
|
+
protected
|
12
|
+
|
13
|
+
def current_account
|
14
|
+
::Account.find_by_keyword!(params[:account_id])
|
15
|
+
end
|
16
|
+
|
17
|
+
def current_project
|
18
|
+
current_account.projects.find_by_keyword!(params[:project_id])
|
19
|
+
end
|
20
|
+
|
21
|
+
def current_project?
|
22
|
+
params[:project_id].present?
|
23
|
+
end
|
24
|
+
|
25
|
+
def current_account?
|
26
|
+
params[:account_id].present?
|
27
|
+
end
|
28
|
+
|
29
|
+
def authorize_admin
|
30
|
+
unless current_user.admin_of?(current_account)
|
31
|
+
deny_access("You must be an admin to access that page.")
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def authorize_member
|
36
|
+
unless current_user.member_of?(current_project)
|
37
|
+
deny_access("You do not have permission for this project.")
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def ensure_active_account
|
42
|
+
if current_account?
|
43
|
+
if current_account.past_due?
|
44
|
+
redirect_unusable_account account_billing_path(current_account),
|
45
|
+
"past_due"
|
46
|
+
end
|
47
|
+
if current_account.expired?
|
48
|
+
redirect_unusable_account edit_account_plan_path(current_account),
|
49
|
+
"expired"
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def redirect_unusable_account(path, failure)
|
55
|
+
role = current_user.admin_of?(current_account) ? 'admin' : 'user'
|
56
|
+
flash[:alert] = t("saucy.errors.#{failure}.#{role}")
|
57
|
+
redirect_to path
|
58
|
+
end
|
59
|
+
|
60
|
+
def ensure_account_within_limit(limit_name)
|
61
|
+
if !Limit.can_add_one?(limit_name, current_account)
|
62
|
+
redirect_to :back, :alert => t("saucy.errors.limited", :default => "You are at your limit of %{name} for your current plan.", :name => limit_name)
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
require 'saucy/layouts'
|
2
|
+
|
3
|
+
module Saucy
|
4
|
+
class Configuration
|
5
|
+
cattr_reader :layouts
|
6
|
+
cattr_accessor :manager_email_address
|
7
|
+
cattr_accessor :support_email_address
|
8
|
+
cattr_accessor :merchant_account_id
|
9
|
+
cattr_accessor :observers
|
10
|
+
|
11
|
+
def initialize
|
12
|
+
@@manager_email_address = 'manager@example.com'
|
13
|
+
@@support_email_address = 'support@example.com'
|
14
|
+
@@layouts = Layouts.new
|
15
|
+
@@observers = []
|
16
|
+
end
|
17
|
+
|
18
|
+
def self.observe(observer)
|
19
|
+
@@observers << observer
|
20
|
+
end
|
21
|
+
|
22
|
+
def self.notify(event, data)
|
23
|
+
@@observers.each do |observer|
|
24
|
+
observer.send(event, data)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
data/lib/saucy/engine.rb
ADDED
@@ -0,0 +1,35 @@
|
|
1
|
+
require "saucy"
|
2
|
+
require "rails"
|
3
|
+
require "braintree"
|
4
|
+
|
5
|
+
module Saucy
|
6
|
+
class Engine < Rails::Engine
|
7
|
+
config.saucy = Configuration.new
|
8
|
+
|
9
|
+
initializer :braintree_logger, :after => :initialize_logger do
|
10
|
+
Braintree::Configuration.logger = Rails.logger
|
11
|
+
end
|
12
|
+
|
13
|
+
initializer :filter_credit_card_info do
|
14
|
+
Rails.configuration.filter_parameters += [:password,
|
15
|
+
:card_number,
|
16
|
+
:cardholder_name,
|
17
|
+
:verification_code,
|
18
|
+
:expiration_month,
|
19
|
+
:expiration_year]
|
20
|
+
end
|
21
|
+
|
22
|
+
initializer 'limits.helper' do |app|
|
23
|
+
ActionView::Base.send :include, LimitsHelper
|
24
|
+
end
|
25
|
+
|
26
|
+
{:short_date => "%x"}.each do |k, v|
|
27
|
+
Time::DATE_FORMATS[k] = v
|
28
|
+
end
|
29
|
+
|
30
|
+
rake_tasks do
|
31
|
+
load "saucy/railties/tasks.rake"
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
@@ -0,0 +1,134 @@
|
|
1
|
+
require 'braintree'
|
2
|
+
|
3
|
+
Braintree::Configuration.environment = :production
|
4
|
+
Braintree::Configuration.merchant_id = "xxx"
|
5
|
+
Braintree::Configuration.public_key = "xxx"
|
6
|
+
Braintree::Configuration.private_key = "xxx"
|
7
|
+
|
8
|
+
require 'digest/md5'
|
9
|
+
require 'sham_rack'
|
10
|
+
|
11
|
+
class FakeBraintree
|
12
|
+
cattr_accessor :customers, :subscriptions, :failures, :transaction
|
13
|
+
@@customers = {}
|
14
|
+
@@subscriptions = {}
|
15
|
+
@@failures = {}
|
16
|
+
@@transaction = {}
|
17
|
+
|
18
|
+
def self.clear!
|
19
|
+
@@customers = {}
|
20
|
+
@@subscriptions = {}
|
21
|
+
@@failures = {}
|
22
|
+
@@transaction = {}
|
23
|
+
end
|
24
|
+
|
25
|
+
def self.failure?(card_number)
|
26
|
+
self.failures.include?(card_number)
|
27
|
+
end
|
28
|
+
|
29
|
+
def self.failure_response(card_number)
|
30
|
+
failure = self.failures[card_number]
|
31
|
+
failure["errors"] ||= { "errors" => [] }
|
32
|
+
{ "message" => failure["message"], "verification" => { "status" => failure["status"], "processor_response_text" => failure["message"], "processor-response-code" => failure["code"], "gateway_rejection_reason" => "cvv", "cvv_response_code" => failure["code"] }, "errors" => failure["errors"], "params" => {}}
|
33
|
+
end
|
34
|
+
|
35
|
+
def self.generated_transaction
|
36
|
+
{"status_history"=>[{"timestamp"=>Time.now, "amount"=>FakeBraintree.transaction[:amount], "transaction_source"=>"CP", "user"=>"copycopter", "status"=>"authorized"}, {"timestamp"=>Time.now, "amount"=>FakeBraintree.transaction[:amount], "transaction_source"=>"CP", "user"=>"copycopter", "status"=>FakeBraintree.transaction[:status]}], "created_at"=>(FakeBraintree.transaction[:created_at] || Time.now), "currency_iso_code"=>"USD", "settlement_batch_id"=>nil, "processor_authorization_code"=>"ZKB4VJ", "avs_postal_code_response_code"=>"I", "order_id"=>nil, "updated_at"=>Time.now, "refunded_transaction_id"=>nil, "amount"=>FakeBraintree.transaction[:amount], "credit_card"=>{"last_4"=>"1111", "card_type"=>"Visa", "token"=>"8yq7", "customer_location"=>"US", "expiration_year"=>"2013", "expiration_month"=>"02", "bin"=>"411111", "cardholder_name"=>"Chad Lee Pytel"}, "refund_id"=>nil, "add_ons"=>[], "shipping"=>{"region"=>nil, "company"=>nil, "country_name"=>nil, "extended_address"=>nil, "postal_code"=>nil, "id"=>nil, "street_address"=>nil, "country_code_numeric"=>nil, "last_name"=>nil, "locality"=>nil, "country_code_alpha2"=>nil, "country_code_alpha3"=>nil, "first_name"=>nil}, "id"=>"49sbx6", "merchant_account_id"=>"Thoughtbot", "type"=>"sale", "cvv_response_code"=>"I", "subscription_id"=>FakeBraintree.transaction[:subscription_id], "custom_fields"=>"\n ", "discounts"=>[], "billing"=>{"region"=>nil, "company"=>nil, "country_name"=>nil, "extended_address"=>nil, "postal_code"=>nil, "id"=>nil, "street_address"=>nil, "country_code_numeric"=>nil, "last_name"=>nil, "locality"=>nil, "country_code_alpha2"=>nil, "country_code_alpha3"=>nil, "first_name"=>nil}, "processor_response_code"=>"1000", "refund_ids"=>[], "customer"=>{"company"=>nil, "id"=>"108427", "last_name"=>nil, "fax"=>nil, "phone"=>nil, "website"=>nil, "first_name"=>nil, "email"=>"cpytel@thoughtbot.com"}, "avs_error_response_code"=>nil, "processor_response_text"=>"Approved", "avs_street_address_response_code"=>"I", "status"=>FakeBraintree.transaction[:status], "gateway_rejection_reason"=>nil}
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
ShamRack.at("www.braintreegateway.com", 443).sinatra do
|
41
|
+
set :show_exceptions, false
|
42
|
+
set :dump_errors, true
|
43
|
+
set :raise_errors, true
|
44
|
+
disable :logging
|
45
|
+
|
46
|
+
post "/merchants/:merchant_id/customers" do
|
47
|
+
customer = Hash.from_xml(request.body).delete("customer")
|
48
|
+
if !FakeBraintree.failure?(customer["credit_card"]["number"])
|
49
|
+
customer["id"] ||= Digest::MD5.hexdigest("#{params[:merchant_id]}#{Time.now.to_f}")
|
50
|
+
customer["merchant-id"] = params[:merchant_id]
|
51
|
+
if customer["credit_card"] && customer["credit_card"].is_a?(Hash)
|
52
|
+
customer["credit_card"].delete("__content__")
|
53
|
+
if !customer["credit_card"].empty?
|
54
|
+
customer["credit_card"]["last_4"] = customer["credit_card"].delete("number")[-4..-1]
|
55
|
+
customer["credit_card"]["token"] = Digest::MD5.hexdigest("#{customer['merchant_id']}#{customer['id']}#{Time.now.to_f}")
|
56
|
+
credit_card = customer.delete("credit_card")
|
57
|
+
customer["credit_cards"] = [credit_card]
|
58
|
+
end
|
59
|
+
end
|
60
|
+
FakeBraintree.customers[customer["id"]] = customer
|
61
|
+
[201, { "Content-Encoding" => "gzip" }, ActiveSupport::Gzip.compress(customer.to_xml(:root => 'customer'))]
|
62
|
+
else
|
63
|
+
[422, { "Content-Encoding" => "gzip" }, ActiveSupport::Gzip.compress(FakeBraintree.failure_response(customer["credit_card"]["number"]).to_xml(:root => 'api_error_response'))]
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
get "/merchants/:merchant_id/customers/:id" do
|
68
|
+
customer = FakeBraintree.customers[params[:id]]
|
69
|
+
[200, { "Content-Encoding" => "gzip" }, ActiveSupport::Gzip.compress(customer.to_xml(:root => 'customer'))]
|
70
|
+
end
|
71
|
+
|
72
|
+
put "/merchants/:merchant_id/customers/:id" do
|
73
|
+
customer = Hash.from_xml(request.body).delete("customer")
|
74
|
+
if !FakeBraintree.failure?(customer["credit_card"]["number"])
|
75
|
+
customer["id"] = params[:id]
|
76
|
+
customer["merchant-id"] = params[:merchant_id]
|
77
|
+
if customer["credit_card"] && customer["credit_card"].is_a?(Hash)
|
78
|
+
customer["credit_card"].delete("__content__")
|
79
|
+
if !customer["credit_card"].empty?
|
80
|
+
customer["credit_card"]["last_4"] = customer["credit_card"].delete("number")[-4..-1]
|
81
|
+
customer["credit_card"]["token"] = Digest::MD5.hexdigest("#{customer['merchant_id']}#{customer['id']}#{Time.now.to_f}")
|
82
|
+
credit_card = customer.delete("credit_card")
|
83
|
+
customer["credit_cards"] = [credit_card]
|
84
|
+
end
|
85
|
+
end
|
86
|
+
FakeBraintree.customers[params["id"]] = customer
|
87
|
+
[200, { "Content-Encoding" => "gzip" }, ActiveSupport::Gzip.compress(customer.to_xml(:root => 'customer'))]
|
88
|
+
else
|
89
|
+
[422, { "Content-Encoding" => "gzip" }, ActiveSupport::Gzip.compress(FakeBraintree.failure_response(customer["credit_card"]["number"]).to_xml(:root => 'api_error_response'))]
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
delete "/merchants/:merchant_id/customers/:id" do
|
94
|
+
FakeBraintree.customers[params["id"]] = nil
|
95
|
+
[200, { "Content-Encoding" => "gzip" }, ActiveSupport::Gzip.compress("")]
|
96
|
+
end
|
97
|
+
|
98
|
+
post "/merchants/:merchant_id/subscriptions" do
|
99
|
+
"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<subscription>\n <plan-id type=\"integer\">2</plan-id>\n <payment-method-token>b22x</payment-method-token>\n</subscription>\n"
|
100
|
+
subscription = Hash.from_xml(request.body).delete("subscription")
|
101
|
+
subscription["id"] ||= Digest::MD5.hexdigest("#{subscription["payment_method_token"]}#{Time.now.to_f}")
|
102
|
+
subscription["transactions"] = []
|
103
|
+
subscription["add_ons"] = []
|
104
|
+
subscription["discounts"] = []
|
105
|
+
subscription["next_billing_date"] = 1.month.from_now
|
106
|
+
subscription["status"] = Braintree::Subscription::Status::Active
|
107
|
+
FakeBraintree.subscriptions[subscription["id"]] = subscription
|
108
|
+
[201, { "Content-Encoding" => "gzip" }, ActiveSupport::Gzip.compress(subscription.to_xml(:root => 'subscription'))]
|
109
|
+
end
|
110
|
+
|
111
|
+
get "/merchants/:merchant_id/subscriptions/:id" do
|
112
|
+
subscription = FakeBraintree.subscriptions[params[:id]]
|
113
|
+
[200, { "Content-Encoding" => "gzip" }, ActiveSupport::Gzip.compress(subscription.to_xml(:root => 'subscription'))]
|
114
|
+
end
|
115
|
+
|
116
|
+
put "/merchants/:merchant_id/subscriptions/:id" do
|
117
|
+
subscription = Hash.from_xml(request.body).delete("subscription")
|
118
|
+
subscription["transactions"] = []
|
119
|
+
subscription["add_ons"] = []
|
120
|
+
subscription["discounts"] = []
|
121
|
+
FakeBraintree.subscriptions[params["id"]] = subscription
|
122
|
+
[200, { "Content-Encoding" => "gzip" }, ActiveSupport::Gzip.compress(subscription.to_xml(:root => 'subscription'))]
|
123
|
+
end
|
124
|
+
|
125
|
+
post "/merchants/:merchant_id/transactions/advanced_search_ids" do
|
126
|
+
# "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<search>\n <created-at>\n <min type=\"datetime\">2011-01-10T14:14:26Z</min>\n </created-at>\n</search>\n"
|
127
|
+
[200, { "Content-Encoding" => "gzip" }, ActiveSupport::Gzip.compress("<search-results>\n <page-size type=\"integer\">50</page-size>\n <ids type=\"array\">\n <item>49sbx6</item>\n </ids>\n</search-results>\n")]
|
128
|
+
end
|
129
|
+
|
130
|
+
post "/merchants/:merchant_id/transactions/advanced_search" do
|
131
|
+
# "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<search>\n <ids type=\"array\">\n <item>49sbx6</item>\n </ids>\n <created-at>\n <min type=\"datetime\">2011-01-10T14:14:26Z</min>\n </created-at>\n</search>\n"
|
132
|
+
[200, { "Content-Encoding" => "gzip" }, ActiveSupport::Gzip.compress(FakeBraintree.generated_transaction.to_xml)]
|
133
|
+
end
|
134
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
module Saucy
|
2
|
+
class Layouts
|
3
|
+
def initialize
|
4
|
+
@controllers = {}
|
5
|
+
end
|
6
|
+
|
7
|
+
def method_missing(controller_name, *args, &block)
|
8
|
+
@controllers[controller_name.to_s] ||= Controller.new
|
9
|
+
end
|
10
|
+
|
11
|
+
def self.to_proc
|
12
|
+
lambda do |controller|
|
13
|
+
controller_name = controller.controller_name
|
14
|
+
action_name = controller.action_name
|
15
|
+
Saucy::Configuration.layouts.send(controller_name).send(action_name)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
private
|
20
|
+
|
21
|
+
class Controller
|
22
|
+
def initialize
|
23
|
+
@actions = {}
|
24
|
+
end
|
25
|
+
|
26
|
+
def method_missing(method_name, *args, &block)
|
27
|
+
action_name = method_name.to_s
|
28
|
+
if action_name.sub!(/=$/, '')
|
29
|
+
@actions[action_name] = args.first
|
30
|
+
else
|
31
|
+
@actions[action_name] ||= "saucy"
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
data/lib/saucy/plan.rb
ADDED
@@ -0,0 +1,54 @@
|
|
1
|
+
module Saucy
|
2
|
+
module Plan
|
3
|
+
extend ActiveSupport::Concern
|
4
|
+
|
5
|
+
included do
|
6
|
+
has_many :accounts
|
7
|
+
has_many :limits
|
8
|
+
|
9
|
+
validates_presence_of :name
|
10
|
+
|
11
|
+
def self.ordered
|
12
|
+
order('price desc')
|
13
|
+
end
|
14
|
+
|
15
|
+
def self.paid_by_price
|
16
|
+
paid.ordered
|
17
|
+
end
|
18
|
+
|
19
|
+
def self.trial
|
20
|
+
free.first
|
21
|
+
end
|
22
|
+
|
23
|
+
def self.paid
|
24
|
+
where('price > 0')
|
25
|
+
end
|
26
|
+
|
27
|
+
def self.free
|
28
|
+
where('price = 0')
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
module InstanceMethods
|
33
|
+
def free?
|
34
|
+
price.zero?
|
35
|
+
end
|
36
|
+
|
37
|
+
def billed?
|
38
|
+
!free?
|
39
|
+
end
|
40
|
+
|
41
|
+
def can_add_more?(limit, amount)
|
42
|
+
limits.numbered.named(limit).value > amount
|
43
|
+
end
|
44
|
+
|
45
|
+
def allows?(limit)
|
46
|
+
limits.boolean.named(limit).allowed?
|
47
|
+
end
|
48
|
+
|
49
|
+
def limit(limit_name)
|
50
|
+
limits.named(limit_name)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
@@ -0,0 +1,125 @@
|
|
1
|
+
module Saucy
|
2
|
+
module Project
|
3
|
+
extend ActiveSupport::Concern
|
4
|
+
|
5
|
+
included do
|
6
|
+
belongs_to :account
|
7
|
+
has_many :permissions, :dependent => :destroy
|
8
|
+
has_many :users, :through => :permissions
|
9
|
+
|
10
|
+
validates_presence_of :account_id, :keyword, :name
|
11
|
+
|
12
|
+
validates_uniqueness_of :keyword, :scope => :account_id
|
13
|
+
|
14
|
+
validates_format_of :keyword,
|
15
|
+
:with => %r{^[a-z0-9_-]+$},
|
16
|
+
:message => "must be only lower case letters or underscores."
|
17
|
+
|
18
|
+
|
19
|
+
validate :ensure_account_within_limit, :on => :update
|
20
|
+
|
21
|
+
after_create :setup_memberships
|
22
|
+
after_update :update_memberships
|
23
|
+
|
24
|
+
attr_protected :account, :account_id
|
25
|
+
|
26
|
+
# We have to define these here instead of mixing them in,
|
27
|
+
# because ActiveRecord does the same.
|
28
|
+
|
29
|
+
def user_ids=(new_user_ids)
|
30
|
+
@new_user_ids = new_user_ids.reject { |user_id| user_id.blank? }
|
31
|
+
end
|
32
|
+
|
33
|
+
def users
|
34
|
+
if new_record?
|
35
|
+
permissions.map { |permission| permission.membership.user }
|
36
|
+
else
|
37
|
+
permissions.includes(:user).map { |permission| permission.user }
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def user_ids
|
42
|
+
users.map(&:id)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
module ClassMethods
|
47
|
+
def visible_to(user)
|
48
|
+
where(['projects.id IN(?)', user.project_ids])
|
49
|
+
end
|
50
|
+
|
51
|
+
def archived
|
52
|
+
where(:archived => true)
|
53
|
+
end
|
54
|
+
|
55
|
+
def active
|
56
|
+
where(:archived => false)
|
57
|
+
end
|
58
|
+
|
59
|
+
def by_name
|
60
|
+
order("projects.name")
|
61
|
+
end
|
62
|
+
|
63
|
+
def build_with_default_permissions
|
64
|
+
new.assign_default_permissions
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
module InstanceMethods
|
69
|
+
def to_param
|
70
|
+
keyword
|
71
|
+
end
|
72
|
+
|
73
|
+
def has_member?(user)
|
74
|
+
permissions.
|
75
|
+
joins(:membership).
|
76
|
+
exists?(:memberships => { :user_id => user.id })
|
77
|
+
end
|
78
|
+
|
79
|
+
def assign_default_permissions
|
80
|
+
account.memberships.where(:admin => true).each do |membership|
|
81
|
+
self.permissions.build(:membership => membership)
|
82
|
+
end
|
83
|
+
self
|
84
|
+
end
|
85
|
+
|
86
|
+
private
|
87
|
+
|
88
|
+
def setup_memberships
|
89
|
+
@new_user_ids ||= []
|
90
|
+
@new_user_ids += admin_user_ids
|
91
|
+
removed_user_ids = self.user_ids - @new_user_ids
|
92
|
+
added_user_ids = @new_user_ids - self.user_ids
|
93
|
+
|
94
|
+
permissions.where(:user_id => removed_user_ids).destroy_all
|
95
|
+
added_user_ids.each do |added_user_id|
|
96
|
+
membership =
|
97
|
+
account.memberships.where(:user_id => added_user_id).first
|
98
|
+
permissions.create!(:membership => membership)
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
def update_memberships
|
103
|
+
setup_memberships if @new_user_ids
|
104
|
+
end
|
105
|
+
|
106
|
+
def admin_user_ids
|
107
|
+
account.
|
108
|
+
memberships.
|
109
|
+
where(:admin => true).
|
110
|
+
select(:user_id).
|
111
|
+
map(&:user_id)
|
112
|
+
end
|
113
|
+
|
114
|
+
def ensure_account_within_limit
|
115
|
+
message = "You are at your limit of %{name} for your current plan."
|
116
|
+
if archived_changed? && !archived? && !Limit.can_add_one?("projects", account)
|
117
|
+
errors.add(:archived, I18n.t("saucy.errors.limited", :default => message, :name => 'projects'))
|
118
|
+
end
|
119
|
+
if account_id_changed? && !Limit.can_add_one?("projects", account)
|
120
|
+
errors.add(:account_id, I18n.t("saucy.errors.limited", :default => message, :name => 'projects'))
|
121
|
+
end
|
122
|
+
end
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|