accountability 0.1.1 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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