accountability 0.1.0 → 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (53) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +60 -17
  3. data/app/assets/config/accountability_manifest.js +1 -0
  4. data/app/assets/stylesheets/{acts_as_billable → accountability}/application.css +1 -1
  5. data/app/controllers/accountability/accounts_controller.rb +19 -0
  6. data/app/controllers/accountability/application_controller.rb +45 -0
  7. data/app/controllers/accountability/order_groups_controller.rb +65 -0
  8. data/app/controllers/accountability/products_controller.rb +56 -0
  9. data/app/helpers/accountability/application_helper.rb +19 -0
  10. data/app/models/accountability/account.rb +28 -0
  11. data/app/models/accountability/account/transactions.rb +43 -0
  12. data/app/models/accountability/application_record.rb +3 -0
  13. data/app/models/accountability/coupon.rb +39 -0
  14. data/app/models/accountability/credit.rb +46 -0
  15. data/app/models/accountability/debit.rb +14 -0
  16. data/app/models/accountability/deduction.rb +17 -0
  17. data/app/models/accountability/discount.rb +36 -0
  18. data/app/models/accountability/offerable.rb +76 -0
  19. data/app/models/accountability/order_group.rb +52 -0
  20. data/app/models/accountability/order_item.rb +88 -0
  21. data/app/models/accountability/payment.rb +8 -0
  22. data/app/models/accountability/product.rb +53 -0
  23. data/app/views/accountability/accounts/index.html.haml +13 -0
  24. data/app/views/accountability/accounts/show.html.haml +12 -0
  25. data/app/views/accountability/order_groups/new.html.haml +1 -0
  26. data/app/views/accountability/order_groups/show.html.haml +16 -0
  27. data/app/views/accountability/products/edit.html.erb +1 -0
  28. data/app/views/accountability/products/index.html.haml +39 -0
  29. data/app/views/accountability/products/new.html.erb +40 -0
  30. data/app/views/accountability/products/show.html.erb +1 -0
  31. data/app/views/layouts/acts_as_billable/application.html.erb +2 -2
  32. data/config/routes.rb +1 -3
  33. data/db/migrate/20190814000455_create_accountability_tables.rb +117 -0
  34. data/lib/accountability.rb +10 -0
  35. data/lib/accountability/cartographer.rb +28 -0
  36. data/lib/accountability/configuration.rb +16 -0
  37. data/lib/accountability/engine.rb +9 -0
  38. data/lib/accountability/extensions.rb +15 -0
  39. data/lib/accountability/extensions/acts_as_billable.rb +14 -0
  40. data/lib/accountability/extensions/acts_as_offerable.rb +36 -0
  41. data/lib/accountability/version.rb +3 -0
  42. metadata +40 -14
  43. data/app/assets/config/acts_as_billable_manifest.js +0 -1
  44. data/app/controllers/acts_as_billable/application_controller.rb +0 -3
  45. data/app/controllers/acts_as_billable/pages_controller.rb +0 -5
  46. data/app/helpers/acts_as_billable/application_helper.rb +0 -2
  47. data/app/models/acts_as_billable/application_record.rb +0 -3
  48. data/app/models/acts_as_billable/product.rb +0 -2
  49. data/app/views/acts_as_billable/pages/dashboard.html.erb +0 -1
  50. data/db/migrate/20190814000455_create_acts_as_billable_tables.rb +0 -51
  51. data/lib/acts_as_billable.rb +0 -5
  52. data/lib/acts_as_billable/engine.rb +0 -3
  53. data/lib/acts_as_billable/version.rb +0 -3
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6b8115dd4e33d3e29a0ab8c565cf65a2abff5abf19672a31f69fedcf5e460917
4
- data.tar.gz: 5d728906709560706013b88cf94bfcaa264e85fe1f902bff332fa0a697b9bc7e
3
+ metadata.gz: cc188f9e13eb05ae4a5f773cc2100210cb23fa06c04eebca820db8fe9bfedce2
4
+ data.tar.gz: 3b4180229a7be03688f3002db38a8afd792e440f45dec8fc6452a636eb434c4f
5
5
  SHA512:
6
- metadata.gz: 4bba25e227a5aeb7f621fdbdf2fdcc77225900e8746db22b7876a067fbe778e55bec4ee84e2c978ac99ff5b143d5886a99d603c11f33d154e17db70d4ff5c1cb
7
- data.tar.gz: eac570ee48dd6c4443f417c414d3c2da83252f562e094385440ce38fc08c989098630a02a8b25e19d9f687b40956f442d655a5d42b6987ae003cd5a218042944
6
+ metadata.gz: 3e59ae904e993a85976d614dc95a3c7a3ac6d64d4c1439ffceb45e3ce02e3d4a3ec05edb8fe3ec4eab68a8ed9fad8acda2f3dddbf79ea04f86559298d1e59111
7
+ data.tar.gz: 693b8d283aea6758b5701c6cda8860036901c2d9a3075b138853582c76a4ba1ba4943134fda7f3497080e3391f4afbc1443fa060a09e024c2be3035de90545de
data/README.md CHANGED
@@ -1,28 +1,71 @@
1
- # ActsAsBillable
2
- Short description and motivation.
1
+ # Accountability
2
+ An extensible Rails library for easy product & billing management.
3
3
 
4
4
  ## Usage
5
- How to use my plugin.
5
+ ### Installing the gem
6
+ To get started, add Accountability to your application's Gemfile.
7
+ ```ruby
8
+ gem 'accountability'
9
+ ```
6
10
 
7
- ## Installation
8
- Add this line to your application's Gemfile:
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
- gem 'acts_as_billable'
15
+ accountability_views_for :admin, :public, path: '/billing'
12
16
  ```
13
17
 
14
- And then execute:
15
- ```bash
16
- $ bundle
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
- Or install it yourself as:
20
- ```bash
21
- $ gem install acts_as_billable
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
- ## Contributing
25
- Contribution directions go here.
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
- ## License
28
- The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
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 acts_as_billable
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,3 @@
1
+ class Accountability::ApplicationRecord < ActiveRecord::Base
2
+ self.abstract_class = true
3
+ 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