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.
- checksums.yaml +4 -4
- data/README.md +86 -9
- data/app/assets/stylesheets/accountability/application.scss +0 -0
- data/app/controllers/accountability/accounts_controller.rb +2 -4
- data/app/controllers/accountability/billing_configurations_controller.rb +80 -0
- data/app/controllers/accountability/order_groups_controller.rb +2 -4
- data/app/controllers/accountability/payments_controller.rb +25 -0
- data/app/controllers/accountability/products_controller.rb +7 -5
- data/app/controllers/accountability/statements_controller.rb +19 -0
- data/app/controllers/accountability_controller.rb +47 -0
- data/app/helpers/accountability/{application_helper.rb → accountability_helper.rb} +1 -1
- data/app/helpers/accountability/billing_configurations_helper.rb +47 -0
- data/app/helpers/accountability/products_helper.rb +21 -0
- data/app/models/accountability/account.rb +31 -1
- data/app/models/accountability/application_record.rb +24 -0
- data/app/models/accountability/billing_configuration.rb +41 -0
- data/app/models/accountability/coupon.rb +2 -0
- data/app/models/accountability/credit.rb +53 -29
- data/app/models/accountability/debit.rb +10 -0
- data/app/models/accountability/inventory.rb +61 -0
- data/app/models/accountability/offerable.rb +28 -1
- data/app/models/accountability/offerable/scope.rb +48 -0
- data/app/models/accountability/order_item.rb +3 -7
- data/app/models/accountability/payment.rb +22 -3
- data/app/models/accountability/product.rb +45 -5
- data/app/models/accountability/statement.rb +46 -0
- data/app/models/accountability/{account/transactions.rb → transactions.rb} +14 -2
- data/app/models/concerns/accountability/active_merchant_interface.rb +15 -0
- data/app/models/concerns/accountability/active_merchant_interface/stripe_interface.rb +134 -0
- data/app/pdfs/statement_pdf.rb +91 -0
- data/app/views/accountability/accounts/show.html.haml +21 -9
- data/app/views/accountability/products/index.html.haml +17 -34
- data/app/views/accountability/products/new.html.haml +73 -0
- data/app/views/accountability/shared/_session_info.html.haml +24 -0
- data/config/locales/en.yml +19 -0
- data/db/migrate/20190814000455_create_accountability_tables.rb +43 -1
- data/lib/accountability.rb +2 -1
- data/lib/accountability/configuration.rb +22 -2
- data/lib/accountability/engine.rb +6 -1
- data/lib/accountability/extensions/acts_as_billable.rb +11 -0
- data/lib/accountability/rails/routes.rb +72 -0
- data/lib/accountability/types.rb +9 -0
- data/lib/accountability/types/billing_configuration_types.rb +57 -0
- data/lib/accountability/version.rb +1 -1
- data/lib/generators/accountability/install_generator.rb +47 -0
- data/lib/generators/accountability/templates/migration.rb.tt +155 -0
- data/lib/generators/accountability/templates/price_overrides_migration.rb.tt +13 -0
- metadata +73 -12
- data/app/assets/stylesheets/accountability/application.css +0 -15
- data/app/controllers/accountability/application_controller.rb +0 -45
- data/app/views/accountability/products/new.html.erb +0 -40
- data/lib/accountability/cartographer.rb +0 -28
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: f6693805b727a73f703afae26091f1dbbfdaf8361204ae9fa52ba1b4932fd7d0
|
4
|
+
data.tar.gz: b0b4f44c7be9a0e0ea9f5ada14fcfd552048a054ba2ece199fdcf7043dce88b2
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
-
|
27
|
-
|
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
|
-
|
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
|
-
###
|
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
|
-
###
|
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
|
File without changes
|
@@ -1,8 +1,6 @@
|
|
1
|
-
require_dependency 'accountability/application_controller'
|
2
|
-
|
3
1
|
module Accountability
|
4
|
-
class AccountsController <
|
5
|
-
before_action :set_account, except: %i[index
|
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 <
|
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 <
|
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
|
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
|
@@ -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
|