accountability 0.1.0 → 0.1.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +60 -17
- data/app/assets/config/accountability_manifest.js +1 -0
- data/app/assets/stylesheets/{acts_as_billable → accountability}/application.css +1 -1
- data/app/controllers/accountability/accounts_controller.rb +19 -0
- data/app/controllers/accountability/application_controller.rb +45 -0
- data/app/controllers/accountability/order_groups_controller.rb +65 -0
- data/app/controllers/accountability/products_controller.rb +56 -0
- data/app/helpers/accountability/application_helper.rb +19 -0
- data/app/models/accountability/account.rb +28 -0
- data/app/models/accountability/account/transactions.rb +43 -0
- data/app/models/accountability/application_record.rb +3 -0
- data/app/models/accountability/coupon.rb +39 -0
- data/app/models/accountability/credit.rb +46 -0
- data/app/models/accountability/debit.rb +14 -0
- data/app/models/accountability/deduction.rb +17 -0
- data/app/models/accountability/discount.rb +36 -0
- data/app/models/accountability/offerable.rb +76 -0
- data/app/models/accountability/order_group.rb +52 -0
- data/app/models/accountability/order_item.rb +88 -0
- data/app/models/accountability/payment.rb +8 -0
- data/app/models/accountability/product.rb +53 -0
- data/app/views/accountability/accounts/index.html.haml +13 -0
- data/app/views/accountability/accounts/show.html.haml +12 -0
- data/app/views/accountability/order_groups/new.html.haml +1 -0
- data/app/views/accountability/order_groups/show.html.haml +16 -0
- data/app/views/accountability/products/edit.html.erb +1 -0
- data/app/views/accountability/products/index.html.haml +39 -0
- data/app/views/accountability/products/new.html.erb +40 -0
- data/app/views/accountability/products/show.html.erb +1 -0
- data/app/views/layouts/acts_as_billable/application.html.erb +2 -2
- data/config/routes.rb +1 -3
- data/db/migrate/20190814000455_create_accountability_tables.rb +117 -0
- data/lib/accountability.rb +10 -0
- data/lib/accountability/cartographer.rb +28 -0
- data/lib/accountability/configuration.rb +16 -0
- data/lib/accountability/engine.rb +9 -0
- data/lib/accountability/extensions.rb +15 -0
- data/lib/accountability/extensions/acts_as_billable.rb +14 -0
- data/lib/accountability/extensions/acts_as_offerable.rb +36 -0
- data/lib/accountability/version.rb +3 -0
- metadata +40 -14
- data/app/assets/config/acts_as_billable_manifest.js +0 -1
- data/app/controllers/acts_as_billable/application_controller.rb +0 -3
- data/app/controllers/acts_as_billable/pages_controller.rb +0 -5
- data/app/helpers/acts_as_billable/application_helper.rb +0 -2
- data/app/models/acts_as_billable/application_record.rb +0 -3
- data/app/models/acts_as_billable/product.rb +0 -2
- data/app/views/acts_as_billable/pages/dashboard.html.erb +0 -1
- data/db/migrate/20190814000455_create_acts_as_billable_tables.rb +0 -51
- data/lib/acts_as_billable.rb +0 -5
- data/lib/acts_as_billable/engine.rb +0 -3
- data/lib/acts_as_billable/version.rb +0 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: cc188f9e13eb05ae4a5f773cc2100210cb23fa06c04eebca820db8fe9bfedce2
|
4
|
+
data.tar.gz: 3b4180229a7be03688f3002db38a8afd792e440f45dec8fc6452a636eb434c4f
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 3e59ae904e993a85976d614dc95a3c7a3ac6d64d4c1439ffceb45e3ce02e3d4a3ec05edb8fe3ec4eab68a8ed9fad8acda2f3dddbf79ea04f86559298d1e59111
|
7
|
+
data.tar.gz: 693b8d283aea6758b5701c6cda8860036901c2d9a3075b138853582c76a4ba1ba4943134fda7f3497080e3391f4afbc1443fa060a09e024c2be3035de90545de
|
data/README.md
CHANGED
@@ -1,28 +1,71 @@
|
|
1
|
-
#
|
2
|
-
|
1
|
+
# Accountability
|
2
|
+
An extensible Rails library for easy product & billing management.
|
3
3
|
|
4
4
|
## Usage
|
5
|
-
|
5
|
+
### Installing the gem
|
6
|
+
To get started, add Accountability to your application's Gemfile.
|
7
|
+
```ruby
|
8
|
+
gem 'accountability'
|
9
|
+
```
|
6
10
|
|
7
|
-
|
8
|
-
|
11
|
+
### Adding routes
|
12
|
+
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`.
|
9
13
|
|
10
14
|
```ruby
|
11
|
-
|
15
|
+
accountability_views_for :admin, :public, path: '/billing'
|
12
16
|
```
|
13
17
|
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
+
The `accountability_views_for` routing helper can take any number of tenant names as arguments. Specifying tenants is only necessary if your application needs to use Accountability multiple times (such as an app with several e-commerce sites).
|
19
|
+
|
20
|
+
This will generate the following routes:
|
21
|
+
* `/billing` - Redirects to products page
|
22
|
+
* `/billing/products`
|
23
|
+
* `/billing/orders`
|
24
|
+
* `/billing/accounts`
|
25
|
+
|
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:
|
28
|
+
```ruby
|
29
|
+
class Basket < ApplicationRecord
|
30
|
+
acts_as_offerable
|
31
|
+
|
32
|
+
...
|
33
|
+
```
|
34
|
+
You can now visit the `/billing/products/new` page and select "Basket" category.
|
18
35
|
|
19
|
-
|
20
|
-
```
|
21
|
-
|
36
|
+
If you want to have multiple offerable categories on the same model, you can set a custom category name:
|
37
|
+
```ruby
|
38
|
+
class Basket < ApplicationRecord
|
39
|
+
has_offerable :basket
|
40
|
+
has_offerable :bucket
|
41
|
+
|
42
|
+
...
|
22
43
|
```
|
23
44
|
|
24
|
-
|
25
|
-
|
45
|
+
Note that `has_offerable` is an alias of `acts_as_offerable`.
|
46
|
+
|
47
|
+
For additional ways to define offerable content, see the "Advanced Usage" section.
|
48
|
+
### Configuration
|
49
|
+
## Advanced Usage
|
50
|
+
### Defining Offerable Content
|
51
|
+
#### Scopes
|
52
|
+
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
|
+
```ruby
|
54
|
+
class Basket < ApplicationRecord
|
55
|
+
enum style: %i[small large narrow deep]
|
56
|
+
|
57
|
+
acts_as_offerable do |offer|
|
58
|
+
offer.add_scope :style, title: 'Size', options: %i[small large]
|
59
|
+
end
|
60
|
+
end
|
61
|
+
```
|
62
|
+
|
63
|
+
#### Callbacks
|
64
|
+
#### Multi-Tenancy
|
65
|
+
#### Dynamic Pricing
|
66
|
+
AKA traits
|
26
67
|
|
27
|
-
##
|
28
|
-
|
68
|
+
## TODO
|
69
|
+
- [ ] Finish implementing multi-tenanting features
|
70
|
+
- [ ] Add support for controller overrides
|
71
|
+
- [ ] Implement product creation workflow in views
|
@@ -0,0 +1 @@
|
|
1
|
+
//= link_directory ../stylesheets/accountability .css
|
@@ -10,6 +10,6 @@
|
|
10
10
|
* files in this directory. Styles in this file should be added after the last require_* statement.
|
11
11
|
* It is generally better to create a new file per style scope.
|
12
12
|
*
|
13
|
-
* require_tree
|
13
|
+
* require_tree accountability
|
14
14
|
*= require_self
|
15
15
|
*/
|
@@ -0,0 +1,19 @@
|
|
1
|
+
require_dependency 'accountability/application_controller'
|
2
|
+
|
3
|
+
module Accountability
|
4
|
+
class AccountsController < ApplicationController
|
5
|
+
before_action :set_account, except: %i[index new create]
|
6
|
+
|
7
|
+
def index
|
8
|
+
@accounts = Account.all
|
9
|
+
end
|
10
|
+
|
11
|
+
def show; end
|
12
|
+
|
13
|
+
private
|
14
|
+
|
15
|
+
def set_account
|
16
|
+
@account = Account.find(params[:id])
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
module Accountability
|
2
|
+
class ApplicationController < ::ApplicationController
|
3
|
+
protect_from_forgery with: :exception
|
4
|
+
|
5
|
+
private
|
6
|
+
|
7
|
+
def track_order
|
8
|
+
# Check if session is billable (billable_identifier proc returns )
|
9
|
+
billable_record = instance_exec(&Configuration.billable_identifier)
|
10
|
+
|
11
|
+
if billable_record.present?
|
12
|
+
track_user_session(billable_record)
|
13
|
+
else
|
14
|
+
track_guest_session
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
def track_user_session(billable_record)
|
19
|
+
raise 'Record not billable' unless billable_record.acts_as.billable?
|
20
|
+
|
21
|
+
current_account = billable_record.accounts.first_or_create!
|
22
|
+
session[:current_account_id] = current_account.id
|
23
|
+
|
24
|
+
if current_order_group&.unassigned?
|
25
|
+
current_order_group.assign_account! current_account
|
26
|
+
else
|
27
|
+
current_order_group = current_account.order_groups.pending.first_or_create!
|
28
|
+
session[:current_order_group_id] = current_order_group.id
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def track_guest_session
|
33
|
+
# Check if the order already belongs to someone
|
34
|
+
return if current_order_group&.unassigned?
|
35
|
+
|
36
|
+
current_order_group = OrderGroup.create!
|
37
|
+
session[:current_order_group_id] = current_order_group.id
|
38
|
+
end
|
39
|
+
|
40
|
+
def current_order_group
|
41
|
+
order_group_id = session[:current_order_group_id]
|
42
|
+
OrderGroup.find_by(id: order_group_id)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,65 @@
|
|
1
|
+
require_dependency 'accountability/application_controller'
|
2
|
+
|
3
|
+
module Accountability
|
4
|
+
class OrderGroupsController < ApplicationController
|
5
|
+
before_action :set_order_group, except: %i[new create]
|
6
|
+
before_action :track_order, except: %i[index]
|
7
|
+
|
8
|
+
def index
|
9
|
+
@order_groups = OrderGroup.all
|
10
|
+
end
|
11
|
+
|
12
|
+
def new
|
13
|
+
redirect_to accountability_order_group_path(current_order_group)
|
14
|
+
end
|
15
|
+
|
16
|
+
def show; end
|
17
|
+
|
18
|
+
def edit; end
|
19
|
+
|
20
|
+
def create
|
21
|
+
@order_group = OrderGroup.new(order_group_params)
|
22
|
+
|
23
|
+
if @order_group.save
|
24
|
+
redirect_to accountability_order_groups_path, notice: 'Successfully created new order_group'
|
25
|
+
else
|
26
|
+
render :new
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def update
|
31
|
+
if @order_group.update(order_group_params)
|
32
|
+
redirect_to accountability_order_groups_path, notice: 'Successfully updated order_group'
|
33
|
+
else
|
34
|
+
render :edit
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def destroy
|
39
|
+
if @order_group.destroy
|
40
|
+
redirect_to accountability_order_groups_path, notice: 'Successfully destroyed order_group'
|
41
|
+
else
|
42
|
+
redirect_to accountability_order_groups_path, alert: 'There was an issue destroying order_group'
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def add_item
|
47
|
+
product = Product.find(params[:product_id])
|
48
|
+
if @order_group.add_item! product
|
49
|
+
redirect_to accountability_order_group_path(current_order_group), notice: 'Successfully added to cart'
|
50
|
+
else
|
51
|
+
redirect_back fallback_location: accountability_order_groups_path, alert: 'Failed to add to cart'
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
private
|
56
|
+
|
57
|
+
def set_order_group
|
58
|
+
@order_group = OrderGroup.find(params[:id])
|
59
|
+
end
|
60
|
+
|
61
|
+
def order_group_params
|
62
|
+
params.require(:order_group).permit
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
require_dependency 'accountability/application_controller'
|
2
|
+
|
3
|
+
module Accountability
|
4
|
+
class ProductsController < ApplicationController
|
5
|
+
before_action :track_order, only: :index
|
6
|
+
before_action :set_product, except: %i[index new create]
|
7
|
+
|
8
|
+
def index
|
9
|
+
@products = Product.all
|
10
|
+
end
|
11
|
+
|
12
|
+
def new
|
13
|
+
@product = Product.new
|
14
|
+
end
|
15
|
+
|
16
|
+
def show; end
|
17
|
+
|
18
|
+
def edit; end
|
19
|
+
|
20
|
+
def create
|
21
|
+
@product = Product.new(product_params)
|
22
|
+
|
23
|
+
if @product.save
|
24
|
+
redirect_to accountability_products_path, notice: 'Successfully created new product'
|
25
|
+
else
|
26
|
+
render :new
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def update
|
31
|
+
if @product.update(product_params)
|
32
|
+
redirect_to accountability_products_path, notice: 'Successfully updated product'
|
33
|
+
else
|
34
|
+
render :edit
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def destroy
|
39
|
+
if @product.destroy
|
40
|
+
redirect_to accountability_products_path, notice: 'Successfully destroyed product'
|
41
|
+
else
|
42
|
+
redirect_to accountability_products_path, alert: 'There was an issue destroying product'
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
private
|
47
|
+
|
48
|
+
def set_product
|
49
|
+
@product = Product.find(params[:id])
|
50
|
+
end
|
51
|
+
|
52
|
+
def product_params
|
53
|
+
params.require(:product).permit(:name, :sku, :price, :description, :source_class, :source_trait, :source_scope)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module Accountability
|
2
|
+
module ApplicationHelper
|
3
|
+
def admin_session?
|
4
|
+
instance_exec(&Configuration.admin_checker)
|
5
|
+
end
|
6
|
+
|
7
|
+
def current_account
|
8
|
+
return if session[:current_account_id].nil?
|
9
|
+
|
10
|
+
Account.find_by(id: session[:current_account_id])
|
11
|
+
end
|
12
|
+
|
13
|
+
def current_order_group
|
14
|
+
return if session[:current_order_group_id].nil?
|
15
|
+
|
16
|
+
OrderGroup.find_by(id: session[:current_order_group_id])
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
# The Account class acts as a ledger. It compares the accrued credits (expenses) and debits (deposits).
|
2
|
+
|
3
|
+
module Accountability
|
4
|
+
class Account < ApplicationRecord
|
5
|
+
belongs_to :billable, polymorphic: true
|
6
|
+
|
7
|
+
has_many :order_groups, dependent: :destroy
|
8
|
+
has_many :order_items, through: :order_groups, inverse_of: :account
|
9
|
+
has_many :purchased_order_groups, -> { complete }, class_name: 'OrderGroup', inverse_of: :account
|
10
|
+
has_many :purchased_order_items, through: :purchased_order_groups, source: :order_items, inverse_of: :account
|
11
|
+
has_many :payments, dependent: :nullify
|
12
|
+
has_many :credits, dependent: :destroy
|
13
|
+
has_many :debits, dependent: :destroy
|
14
|
+
|
15
|
+
def balance
|
16
|
+
accrued_credits = credits.sum(:amount)
|
17
|
+
accrued_debits = debits.sum(:amount)
|
18
|
+
|
19
|
+
accrued_debits - accrued_credits
|
20
|
+
end
|
21
|
+
|
22
|
+
def transactions
|
23
|
+
associated_credits = credits.includes(:product, :deductions)
|
24
|
+
|
25
|
+
Transactions.new(debits: debits, credits: associated_credits)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
require 'forwardable'
|
2
|
+
|
3
|
+
module Accountability
|
4
|
+
class Account::Transactions
|
5
|
+
extend Forwardable
|
6
|
+
|
7
|
+
attr_accessor :transactions
|
8
|
+
|
9
|
+
def_delegators :transactions, :each, :sort_by
|
10
|
+
|
11
|
+
def initialize(debits: [], credits: [])
|
12
|
+
debit_transactions = debits.map do |debit|
|
13
|
+
Transaction.new(:debit, record: debit, amount: debit.amount, description: 'Payment')
|
14
|
+
end
|
15
|
+
|
16
|
+
credit_transactions = credits.map do |credit|
|
17
|
+
Transaction.new(:credit, record: credit, amount: credit.amount, description: credit.product_name)
|
18
|
+
end
|
19
|
+
|
20
|
+
@transactions = debit_transactions + credit_transactions
|
21
|
+
end
|
22
|
+
|
23
|
+
class Transaction
|
24
|
+
attr_accessor :type, :record, :description, :amount, :date
|
25
|
+
|
26
|
+
def initialize(type, record:, amount:, description:, date: nil)
|
27
|
+
@type = type
|
28
|
+
@record = record
|
29
|
+
@amount = amount
|
30
|
+
@description = description
|
31
|
+
@date = date.presence || record.created_at
|
32
|
+
end
|
33
|
+
|
34
|
+
def debit?
|
35
|
+
type == :debit
|
36
|
+
end
|
37
|
+
|
38
|
+
def credit?
|
39
|
+
type == :credit
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
class Accountability::Coupon < ApplicationRecord
|
2
|
+
has_and_belongs_to_many :products
|
3
|
+
has_many :discounts, dependent: :restrict_with_error
|
4
|
+
has_many :deductions, through: :discounts, inverse_of: :coupon
|
5
|
+
|
6
|
+
validates :name, :amount, presence: true
|
7
|
+
|
8
|
+
def usable?
|
9
|
+
return false if used_up?
|
10
|
+
|
11
|
+
active?
|
12
|
+
end
|
13
|
+
|
14
|
+
def active?
|
15
|
+
return false if activation_date.blank?
|
16
|
+
return false if expired?
|
17
|
+
|
18
|
+
activation_date.past?
|
19
|
+
end
|
20
|
+
|
21
|
+
def expired?
|
22
|
+
return false if expiration_date.blank?
|
23
|
+
return false if terminated?
|
24
|
+
|
25
|
+
expiration_date.past?
|
26
|
+
end
|
27
|
+
|
28
|
+
def terminated?
|
29
|
+
return false if termination_date.blank?
|
30
|
+
|
31
|
+
termination_date.past?
|
32
|
+
end
|
33
|
+
|
34
|
+
def used_up?
|
35
|
+
return false if limit.nil?
|
36
|
+
|
37
|
+
discounts.count >= limit
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
# A Credit represents a single charge to an Account
|
2
|
+
# To preserve data integrity, credits should never be modified
|
3
|
+
|
4
|
+
class Accountability::Credit < ApplicationRecord
|
5
|
+
before_validation :set_amount
|
6
|
+
|
7
|
+
belongs_to :account
|
8
|
+
belongs_to :order_item
|
9
|
+
has_many :deductions, dependent: :destroy
|
10
|
+
has_one :product, through: :order_item, inverse_of: :credits
|
11
|
+
|
12
|
+
validate :validate_amount_unchanged
|
13
|
+
|
14
|
+
delegate :name, to: :product, prefix: true
|
15
|
+
|
16
|
+
def base_price
|
17
|
+
if new_record?
|
18
|
+
order_item.default_price
|
19
|
+
else
|
20
|
+
amount + total_deductions
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def total_deductions
|
25
|
+
if persisted?
|
26
|
+
deductions.sum(:amount)
|
27
|
+
else
|
28
|
+
deductions.map(&:amount).sum
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
private
|
33
|
+
|
34
|
+
def set_amount
|
35
|
+
return if persisted?
|
36
|
+
|
37
|
+
self.amount = base_price - total_deductions
|
38
|
+
end
|
39
|
+
|
40
|
+
def validate_amount_unchanged
|
41
|
+
return if new_record?
|
42
|
+
return unless amount_changed?
|
43
|
+
|
44
|
+
errors.add(:amount, 'cannot be changed')
|
45
|
+
end
|
46
|
+
end
|