accountability 0.1.1 → 0.2.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (52) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +86 -9
  3. data/app/assets/stylesheets/accountability/application.scss +0 -0
  4. data/app/controllers/accountability/accounts_controller.rb +2 -4
  5. data/app/controllers/accountability/billing_configurations_controller.rb +80 -0
  6. data/app/controllers/accountability/order_groups_controller.rb +2 -4
  7. data/app/controllers/accountability/payments_controller.rb +25 -0
  8. data/app/controllers/accountability/products_controller.rb +7 -5
  9. data/app/controllers/accountability/statements_controller.rb +19 -0
  10. data/app/controllers/accountability_controller.rb +47 -0
  11. data/app/helpers/accountability/{application_helper.rb → accountability_helper.rb} +1 -1
  12. data/app/helpers/accountability/billing_configurations_helper.rb +47 -0
  13. data/app/helpers/accountability/products_helper.rb +21 -0
  14. data/app/models/accountability/account.rb +31 -1
  15. data/app/models/accountability/application_record.rb +24 -0
  16. data/app/models/accountability/billing_configuration.rb +41 -0
  17. data/app/models/accountability/coupon.rb +2 -0
  18. data/app/models/accountability/credit.rb +53 -29
  19. data/app/models/accountability/debit.rb +10 -0
  20. data/app/models/accountability/inventory.rb +61 -0
  21. data/app/models/accountability/offerable.rb +28 -1
  22. data/app/models/accountability/offerable/scope.rb +48 -0
  23. data/app/models/accountability/order_item.rb +3 -7
  24. data/app/models/accountability/payment.rb +22 -3
  25. data/app/models/accountability/product.rb +45 -5
  26. data/app/models/accountability/statement.rb +46 -0
  27. data/app/models/accountability/{account/transactions.rb → transactions.rb} +14 -2
  28. data/app/models/concerns/accountability/active_merchant_interface.rb +15 -0
  29. data/app/models/concerns/accountability/active_merchant_interface/stripe_interface.rb +134 -0
  30. data/app/pdfs/statement_pdf.rb +91 -0
  31. data/app/views/accountability/accounts/show.html.haml +21 -9
  32. data/app/views/accountability/products/index.html.haml +17 -34
  33. data/app/views/accountability/products/new.html.haml +73 -0
  34. data/app/views/accountability/shared/_session_info.html.haml +24 -0
  35. data/config/locales/en.yml +19 -0
  36. data/db/migrate/20190814000455_create_accountability_tables.rb +43 -1
  37. data/lib/accountability.rb +2 -1
  38. data/lib/accountability/configuration.rb +22 -2
  39. data/lib/accountability/engine.rb +6 -1
  40. data/lib/accountability/extensions/acts_as_billable.rb +11 -0
  41. data/lib/accountability/rails/routes.rb +72 -0
  42. data/lib/accountability/types.rb +9 -0
  43. data/lib/accountability/types/billing_configuration_types.rb +57 -0
  44. data/lib/accountability/version.rb +1 -1
  45. data/lib/generators/accountability/install_generator.rb +47 -0
  46. data/lib/generators/accountability/templates/migration.rb.tt +155 -0
  47. data/lib/generators/accountability/templates/price_overrides_migration.rb.tt +13 -0
  48. metadata +73 -12
  49. data/app/assets/stylesheets/accountability/application.css +0 -15
  50. data/app/controllers/accountability/application_controller.rb +0 -45
  51. data/app/views/accountability/products/new.html.erb +0 -40
  52. data/lib/accountability/cartographer.rb +0 -28
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: cc188f9e13eb05ae4a5f773cc2100210cb23fa06c04eebca820db8fe9bfedce2
4
- data.tar.gz: 3b4180229a7be03688f3002db38a8afd792e440f45dec8fc6452a636eb434c4f
3
+ metadata.gz: f6693805b727a73f703afae26091f1dbbfdaf8361204ae9fa52ba1b4932fd7d0
4
+ data.tar.gz: b0b4f44c7be9a0e0ea9f5ada14fcfd552048a054ba2ece199fdcf7043dce88b2
5
5
  SHA512:
6
- metadata.gz: 3e59ae904e993a85976d614dc95a3c7a3ac6d64d4c1439ffceb45e3ce02e3d4a3ec05edb8fe3ec4eab68a8ed9fad8acda2f3dddbf79ea04f86559298d1e59111
7
- data.tar.gz: 693b8d283aea6758b5701c6cda8860036901c2d9a3075b138853582c76a4ba1ba4943134fda7f3497080e3391f4afbc1443fa060a09e024c2be3035de90545de
6
+ metadata.gz: da038e1ec8fbf770e59d58cc06ac0e8503ec2e65951ca39ebb968970d2c71a96e598ff1ba334e2217cb802ff1eb64344cc8e8200450cbb1a668e48e3e61bf725
7
+ data.tar.gz: 1114f3d2273f1f23a8deff9bcd78b7f075b6dfe15796f1aa5e82d91209d0367f5fc9416a783dcc7659f1b01395c687719288b6cfb6f5e4643441db5276457adf
data/README.md CHANGED
@@ -8,6 +8,14 @@ To get started, add Accountability to your application's Gemfile.
8
8
  gem 'accountability'
9
9
  ```
10
10
 
11
+ Next you must run Bundler, generate the install files, and run migrations.
12
+
13
+ ```bash
14
+ bundle install
15
+ rails generate accountability:install
16
+ rake db:migrate
17
+ ```
18
+
11
19
  ### Adding routes
12
20
  You must specify a path to mount Accountability's engine to from inside your `config/routes.rb` file. In this example, we will mount everything to `/billing`.
13
21
 
@@ -23,17 +31,35 @@ This will generate the following routes:
23
31
  * `/billing/orders`
24
32
  * `/billing/accounts`
25
33
 
26
- ### Creating products
27
- In order to make a `Product`, at least one model must indicate that it is "offerable." For example, let's say we want to sell baskets:
34
+ **Note:** Accountability does not have its own `ApplicationController` and will use yours instead. This means that your layouts file will be used. Prepend path helpers with `main_app.` to prevent links from breaking within Accountability's default views.
35
+
36
+ ### Defining billable models
37
+ Billable models (such as a User, Customer, or Organization) need to be declared in order to accrue credits and make payments.
38
+
39
+ ```ruby
40
+ class User < ApplicationRecord
41
+ acts_as_billable
42
+
43
+ ...
44
+ ```
45
+
46
+ By default, Accountability identifies the billable entity from the `@current_user` variable. This can be changed from the [initializer file](Customizing configuration options).
47
+
48
+ ### Defining products
49
+ A "product" associates an "offerable" model in your application with a SKU, name, price, description, and instructions for querying available inventory.
50
+
51
+ For example, let's say we want to sell baskets:
52
+
28
53
  ```ruby
29
54
  class Basket < ApplicationRecord
30
55
  acts_as_offerable
31
56
 
32
57
  ...
33
58
  ```
34
- You can now visit the `/billing/products/new` page and select "Basket" category.
35
59
 
36
- If you want to have multiple offerable categories on the same model, you can set a custom category name:
60
+ You can now visit the `/billing/products/new` page and select the "Basket" category to create a new _product_.
61
+
62
+ To define additional offerables on the same model, you can set a custom category name:
37
63
  ```ruby
38
64
  class Basket < ApplicationRecord
39
65
  has_offerable :basket
@@ -41,14 +67,50 @@ class Basket < ApplicationRecord
41
67
 
42
68
  ...
43
69
  ```
44
-
45
70
  Note that `has_offerable` is an alias of `acts_as_offerable`.
46
71
 
47
72
  For additional ways to define offerable content, see the "Advanced Usage" section.
48
- ### Configuration
73
+ ### Customizing configuration options
74
+ To customize Accountability, create a `config/initializers/accountability.rb` file:
75
+ ```ruby
76
+ Accountability.configure do |config|
77
+ # Customize Accountability settings here
78
+ end
79
+ ```
80
+
81
+ #### Customer identification
82
+ By default, Accountability will reference the `@current_user` variable when identifying a billable user.
83
+ This will work for most applications using Devise with a User model representing customers.
84
+
85
+ You can customize this behavior by defining either a proc or lamda that returns an instance of any "billable" record. A nil response will trigger a new guest session.
86
+
87
+ You can optionally specify one of the billable record's attributes to reference as a user-friendly name in the views. The ID is used by default.
88
+
89
+ ```ruby
90
+ config.billable_identifier = -> { current_user&.organization }
91
+ config.billable_name_column = :full_name
92
+ ```
93
+
94
+ #### Tax rates
95
+ Currently, tax rates are defined statically as a percentage of the product's price. Feel free to open a PR if you require something more complex.
96
+
97
+ The default value is `0.0`.
98
+
99
+ ```ruby
100
+ config.tax_rate = 9.53
101
+ ```
102
+
103
+ Note that products can be marked as tax exempt.
104
+
105
+ #### Debugger tools
106
+ To print helpful session information in the views such as the currently tracked billable entity, enable the dev tools.
107
+
108
+ ```ruby
109
+ config.dev_tools_enabled = true
110
+ ```
111
+
49
112
  ## Advanced Usage
50
- ### Defining Offerable Content
51
- #### Scopes
113
+ ### Product Scopes
52
114
  Scoping options can be defined to constrain products to a narrower set of records. Let's say that we want to sell both large and small baskets:
53
115
  ```ruby
54
116
  class Basket < ApplicationRecord
@@ -58,7 +120,22 @@ class Basket < ApplicationRecord
58
120
  offer.add_scope :style, title: 'Size', options: %i[small large]
59
121
  end
60
122
  end
61
- ```
123
+ ```
124
+
125
+ ### Inventory Whitelist
126
+ To hide records from the inventory without de-scoping them from the product, you can specify an existing ActiveRecord scope to define the available inventory with.
127
+
128
+ This can be useful for excluding inventory that is sold, reserved, or otherwise unavailable.
129
+
130
+ ```ruby
131
+ class Basket < ApplicationRecord
132
+ scope :in_warehouse, -> { where arrived_at_warehouse: true }
133
+
134
+ acts_as_offerable do |offer|
135
+ offer.inventory_whitelist :in_warehouse
136
+ end
137
+ end
138
+ ```
62
139
 
63
140
  #### Callbacks
64
141
  #### Multi-Tenancy
@@ -1,8 +1,6 @@
1
- require_dependency 'accountability/application_controller'
2
-
3
1
  module Accountability
4
- class AccountsController < ApplicationController
5
- before_action :set_account, except: %i[index new create]
2
+ class AccountsController < AccountabilityController
3
+ before_action :set_account, except: %i[index]
6
4
 
7
5
  def index
8
6
  @accounts = Account.all
@@ -0,0 +1,80 @@
1
+ module Accountability
2
+ class BillingConfigurationsController < AccountabilityController
3
+ before_action :set_billing_configuration, except: %i[new create]
4
+ before_action :set_account, only: %i[create update designate_as_primary]
5
+
6
+ def show; end
7
+
8
+ def new; end
9
+
10
+ def create
11
+ bc_params = billing_configuration_params
12
+ @billing_configuration = @account.build_billing_configuration_with_active_merchant_data(bc_params,
13
+ verify_card: true)
14
+ if @billing_configuration.save
15
+ message = 'Credit card successfully added.'
16
+ render json: { status: :success, message: message, updated_elements: updated_billing_elements }
17
+ else
18
+ render json: { status: :error, errors: @billing_configuration.errors }
19
+ end
20
+ end
21
+
22
+ def edit; end
23
+
24
+ def update
25
+ @billing_configuration.update billing_configuration_params
26
+
27
+ if @billing_configuration.save
28
+ message = 'Configuration Updated'
29
+ render json: { status: :success, message: message, updated_elements: updated_billing_elements }
30
+ else
31
+ render json: { status: :error, errors: @billing_configuration.errors }
32
+ end
33
+ end
34
+
35
+ def destroy
36
+ if @billing_configuration.destroy
37
+ render json: {
38
+ status: :success,
39
+ message: 'Payment Method Destroyed'
40
+ }
41
+ else
42
+ render json: { status: :error, errors: @billing_configuration.errors }
43
+ end
44
+ end
45
+
46
+ def designate_as_primary
47
+ if @billing_configuration.primary!
48
+ message = 'Payment Method Set As Primary'
49
+ render json: { status: :success, message: message, updated_elements: updated_billing_elements }
50
+ else
51
+ render json: { status: :error, errors: @billing_configuration.errors }
52
+ end
53
+ end
54
+
55
+ def updated_billing_elements
56
+ configurations_partial = 'accountability/accounts/billing_configurations/configurations'
57
+ payment_form_partial = 'accountability/accounts/payment_form'
58
+
59
+ {
60
+ configurations: render_to_string(partial: configurations_partial, layout: false, locals: { account: @account }),
61
+ payment_form: render_to_string(partial: payment_form_partial, layout: false, locals: { account: @account })
62
+ }
63
+ end
64
+
65
+ private
66
+
67
+ def set_billing_configuration
68
+ @billing_configuration = BillingConfiguration.find(params[:id])
69
+ end
70
+
71
+ def set_account
72
+ @account = Account.find(params[:account_id])
73
+ end
74
+
75
+ def billing_configuration_params
76
+ params.require(:billing_configuration).permit(:token, :configuration_name, :provider, :contact_email,
77
+ :contact_first_name, :contact_last_name, billing_address: {})
78
+ end
79
+ end
80
+ end
@@ -1,8 +1,6 @@
1
- require_dependency 'accountability/application_controller'
2
-
3
1
  module Accountability
4
- class OrderGroupsController < ApplicationController
5
- before_action :set_order_group, except: %i[new create]
2
+ class OrderGroupsController < AccountabilityController
3
+ before_action :set_order_group, except: %i[index new create]
6
4
  before_action :track_order, except: %i[index]
7
5
 
8
6
  def index
@@ -0,0 +1,25 @@
1
+ module Accountability
2
+ class PaymentsController < AccountabilityController
3
+ before_action :set_account
4
+
5
+ def create
6
+ @payment = @account.payments.new(payment_params)
7
+
8
+ if @payment.save
9
+ redirect_to accountability_account_path(@account), notice: 'Payment was completed successfully'
10
+ else
11
+ render json: { status: :error, errors: @payment.errors }
12
+ end
13
+ end
14
+
15
+ private
16
+
17
+ def payment_params
18
+ params.require(:payment).permit(:amount, :billing_configuration_id)
19
+ end
20
+
21
+ def set_account
22
+ @account = Account.find(params[:account_id])
23
+ end
24
+ end
25
+ end
@@ -1,7 +1,5 @@
1
- require_dependency 'accountability/application_controller'
2
-
3
1
  module Accountability
4
- class ProductsController < ApplicationController
2
+ class ProductsController < AccountabilityController
5
3
  before_action :track_order, only: :index
6
4
  before_action :set_product, except: %i[index new create]
7
5
 
@@ -11,6 +9,7 @@ module Accountability
11
9
 
12
10
  def new
13
11
  @product = Product.new
12
+ @stage = 'initial'
14
13
  end
15
14
 
16
15
  def show; end
@@ -20,7 +19,10 @@ module Accountability
20
19
  def create
21
20
  @product = Product.new(product_params)
22
21
 
23
- if @product.save
22
+ if params[:stage] == 'initial'
23
+ @stage = 'final'
24
+ render :new
25
+ elsif @product.save
24
26
  redirect_to accountability_products_path, notice: 'Successfully created new product'
25
27
  else
26
28
  render :new
@@ -50,7 +52,7 @@ module Accountability
50
52
  end
51
53
 
52
54
  def product_params
53
- params.require(:product).permit(:name, :sku, :price, :description, :source_class, :source_trait, :source_scope)
55
+ params.require(:product).permit(:name, :sku, :price, :description, :source_class, :source_trait, :source_scope, :offerable_category)
54
56
  end
55
57
  end
56
58
  end
@@ -0,0 +1,19 @@
1
+ module Accountability
2
+ class StatementsController < AccountabilityController
3
+ before_action :set_statement
4
+
5
+ def download_pdf
6
+ end_date = @statement.end_date.strftime('%B %-d, %Y')
7
+ filename = "Billing Statement - #{end_date}.pdf"
8
+ pdf = StatementPdf.new(@statement)
9
+
10
+ send_data pdf.render, filename: filename, type: 'application/pdf'
11
+ end
12
+
13
+ private
14
+
15
+ def set_statement
16
+ @statement = Statement.find(params[:id])
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,47 @@
1
+ # TODO: Set the parent class dynamically (Accountability.parent_controller.constantize)
2
+
3
+ class AccountabilityController < ApplicationController
4
+ helper Accountability::Engine.helpers
5
+
6
+ protect_from_forgery with: :exception
7
+
8
+ private
9
+
10
+ def track_order
11
+ # Check if session is billable (billable_identifier proc returns a record)
12
+ billable_record = instance_exec(&Accountability::Configuration.billable_identifier)
13
+
14
+ if billable_record.present?
15
+ track_user_session(billable_record)
16
+ else
17
+ track_guest_session
18
+ end
19
+ end
20
+
21
+ def track_user_session(billable_record)
22
+ raise 'Record not billable' unless billable_record.acts_as.billable?
23
+
24
+ current_account = billable_record.accounts.first_or_create!
25
+ session[:current_account_id] = current_account.id
26
+
27
+ if current_order_group&.unassigned?
28
+ current_order_group.assign_account! current_account
29
+ else
30
+ current_order_group = current_account.order_groups.pending.first_or_create!
31
+ session[:current_order_group_id] = current_order_group.id
32
+ end
33
+ end
34
+
35
+ def track_guest_session
36
+ # Check if the order already belongs to someone
37
+ return if current_order_group&.unassigned?
38
+
39
+ current_order_group = Accountability::OrderGroup.create!
40
+ session[:current_order_group_id] = current_order_group.id
41
+ end
42
+
43
+ def current_order_group
44
+ order_group_id = session[:current_order_group_id]
45
+ Accountability::OrderGroup.find_by(id: order_group_id)
46
+ end
47
+ end
@@ -1,5 +1,5 @@
1
1
  module Accountability
2
- module ApplicationHelper
2
+ module AccountabilityHelper
3
3
  def admin_session?
4
4
  instance_exec(&Configuration.admin_checker)
5
5
  end
@@ -0,0 +1,47 @@
1
+ module Accountability
2
+ module BillingConfigurationsHelper
3
+ def payment_gateway_configuration_javascript
4
+ provider = Accountability::Configuration.payment_gateway[:provider]
5
+ case provider
6
+ when :stripe
7
+ stripe_gateway_javascript
8
+ else
9
+ raise NotImplementedError, "No JavaScript tag defined for #{provider}"
10
+ end
11
+ end
12
+
13
+ def billing_address_preview(billing_address)
14
+ [billing_address.address_1, "#{billing_address.state}, #{billing_address.zip}"].join(' ')
15
+ end
16
+
17
+ private
18
+
19
+ def stripe_gateway_javascript
20
+ snippets = [stripe_gateway_include, stripe_gateway_config]
21
+ join_javascript_snippets(snippets)
22
+ end
23
+
24
+ def stripe_gateway_include
25
+ javascript_include_tag stripe_v3_javascript_url
26
+ end
27
+
28
+ def stripe_gateway_config
29
+ publishable_key = Accountability::Configuration.payment_gateway.dig(:authentication, :publishable_key)
30
+ script = <<~SCRIPT
31
+ ACTIVE_MERCHANT_CONFIG = {
32
+ STRIPE_PUBLISHABLE_KEY: "#{publishable_key}"
33
+ }
34
+ SCRIPT
35
+ content_tag(:script, format_javascript(script), type: 'text/javascript')
36
+ end
37
+
38
+ def format_javascript(javascript)
39
+ tabbed_out_js = javascript.split("\n").map { |line| " #{line}" }.join("\n")
40
+ "\n#{tabbed_out_js}\n".html_safe # rubocop:disable Rails/OutputSafety
41
+ end
42
+
43
+ def join_javascript_snippets(snippets = [])
44
+ snippets.join("\n").html_safe # rubocop:disable Rails/OutputSafety
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,21 @@
1
+ module Accountability
2
+ module ProductsHelper
3
+ def source_class_options
4
+ options = Accountability::Offerable.collection.stringify_keys
5
+ options.keys.map { |offerable_name| [offerable_name.titleize, offerable_name] }
6
+ end
7
+
8
+ def schedule_options
9
+ options = Accountability::Product.schedules
10
+ options.keys.map { |schedule_name| [schedule_name.titleize, schedule_name] }
11
+ end
12
+
13
+ def scope_options(scope)
14
+ scope.options.map { |option| [option.to_s.titleize, option] }
15
+ end
16
+
17
+ def disable_category_field?
18
+ @stage != 'initial'
19
+ end
20
+ end
21
+ end