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
@@ -0,0 +1,14 @@
1
+ class Accountability::Debit < ApplicationRecord
2
+ before_validation :set_amount
3
+
4
+ belongs_to :account
5
+ belongs_to :payment, optional: true
6
+
7
+ private
8
+
9
+ def set_amount
10
+ return unless amount.zero?
11
+
12
+ self.amount = payment.amount
13
+ end
14
+ end
@@ -0,0 +1,17 @@
1
+ class Accountability::Deduction < ApplicationRecord
2
+ belongs_to :discount
3
+ belongs_to :credit
4
+ has_one :coupon, through: :discount, inverse_of: :deductions
5
+
6
+ after_initialize :set_amount
7
+
8
+ delegate :name, to: :coupon, prefix: true
9
+
10
+ private
11
+
12
+ def set_amount
13
+ return if amount.positive?
14
+
15
+ self.amount = discount.expected_savings
16
+ end
17
+ end
@@ -0,0 +1,36 @@
1
+ class Accountability::Discount < ApplicationRecord
2
+ belongs_to :coupon
3
+ belongs_to :order_item
4
+ has_many :deductions, dependent: :nullify
5
+
6
+ delegate :amount, to: :coupon, prefix: true
7
+
8
+ def apply(credit)
9
+ return unless usable?
10
+
11
+ credit.deductions.new(discount: self)
12
+ end
13
+
14
+ def usages
15
+ deductions.count
16
+ end
17
+
18
+ def usable?
19
+ return false if used_up?
20
+
21
+ coupon.usable?
22
+ end
23
+
24
+ def expected_savings
25
+ percent_off = coupon_amount / 100.00
26
+ order_item.default_price * percent_off
27
+ end
28
+
29
+ private
30
+
31
+ def used_up?
32
+ return false if coupon.usage_cap.blank?
33
+
34
+ usages >= coupon.usage_cap
35
+ end
36
+ end
@@ -0,0 +1,76 @@
1
+ # TODO: Reconsider using names as keys, maybe just arrays?
2
+
3
+ class Accountability::Offerable
4
+ cattr_accessor :collection, default: {}
5
+ attr_accessor :tenant, :category, :class_name, :trait, :scopes, :properties, :callbacks
6
+
7
+ def initialize(category, tenant: :default, trait: nil, class_name:)
8
+ @category = category
9
+ @tenant = tenant
10
+ @class_name = class_name
11
+ @trait = trait
12
+ @scopes = {}
13
+ @properties = {}
14
+ @callbacks = {}
15
+ end
16
+
17
+ def self.add(category, tenant: :default, trait: nil, class_name:)
18
+ category = category.to_s.underscore.downcase.to_sym
19
+ offerable = new(category, tenant: tenant, trait: trait, class_name: class_name)
20
+ collection[category] = offerable
21
+
22
+ offerable
23
+ end
24
+
25
+ def add_scope(name, title: name, options: :auto)
26
+ scopes[name] = { title: title.to_s, options: options, category: category }
27
+
28
+ self
29
+ end
30
+
31
+ def add_property(name, title: name, position: nil)
32
+ position = properties.size.next if position.nil?
33
+ properties[name] = { title: title.to_s, position: position }
34
+ self
35
+ end
36
+
37
+ def add_properties(*names)
38
+ names.each do |name|
39
+ add_property(name)
40
+ end
41
+
42
+ self
43
+ end
44
+
45
+ def add_callback(method_name, **options)
46
+ tense, event = options.slice(:before, :after).flatten
47
+ trigger = "#{tense}_#{event}".to_sym
48
+
49
+ params = if options[:with_options].present?
50
+ options[:with_options].to_h { |option| [option, option] }
51
+ else
52
+ [*options[:with]]
53
+ end
54
+
55
+ callbacks[trigger] ||= []
56
+ callbacks[trigger] << { method_name: method_name, params: params }
57
+
58
+ self
59
+ end
60
+
61
+ def trait?
62
+ trait.present?
63
+ end
64
+
65
+ private
66
+
67
+ # TODO: Abstract this out
68
+ def current_tenant
69
+ :default
70
+ end
71
+ end
72
+
73
+ # Load all models so offerable items are registered
74
+ Dir[Rails.root + 'app/models/*.rb'].each do |path|
75
+ require_dependency path
76
+ end
@@ -0,0 +1,52 @@
1
+ # OrderGroups are like shopping carts - they group together purchases (ordered items) that were made together.
2
+ # We wanted to name this 'Order', but for obvious reasons that wouldn't work out
3
+ # In the future we can use this for tracking referrals and contracts.
4
+
5
+ module Accountability
6
+ class OrderGroup < ApplicationRecord
7
+ belongs_to :account, optional: true
8
+
9
+ has_many :order_items, dependent: :destroy
10
+
11
+ enum status: %i[pending complete abandoned]
12
+
13
+ validates :account, presence: true, if: :complete?
14
+
15
+ def checkout!
16
+ trigger_callback :before_checkout
17
+
18
+ transaction do
19
+ complete!
20
+ accrue_credits!
21
+
22
+ trigger_callback :after_checkout
23
+ end
24
+ end
25
+
26
+ def accrue_credits!
27
+ return unless complete?
28
+
29
+ order_items.each(&:accrue_credit!)
30
+ end
31
+
32
+ def add_item!(product)
33
+ order_items.create! product: product
34
+ end
35
+
36
+ def unassigned?
37
+ account.nil?
38
+ end
39
+
40
+ def assign_account!(account)
41
+ update! account: account
42
+ end
43
+
44
+ private
45
+
46
+ def trigger_callback(trigger)
47
+ order_items.each do |item|
48
+ item.trigger_callback trigger
49
+ end
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,88 @@
1
+ # An OrderItem represents a Product that has been (or is being) purchased
2
+ # They are stored in an OrderGroup, which acts like a shopping cart
3
+
4
+ class Accountability::OrderItem < ApplicationRecord
5
+ belongs_to :product
6
+ belongs_to :order_group
7
+ has_one :account, through: :order_group
8
+ has_many :credits, dependent: :destroy
9
+ has_many :discounts, dependent: :destroy
10
+
11
+ serialize :source_scope, Hash
12
+
13
+ delegate :name, to: :product, prefix: true
14
+
15
+ def accrue_credit!
16
+ return unless accruing?
17
+
18
+ credit = credits.new account: account
19
+ discounts.each { |discount| discount.apply(credit) }
20
+ credit.save!
21
+ end
22
+
23
+ def terminate!(date: Time.current)
24
+ update termination_date: date
25
+ end
26
+
27
+ def terminated?
28
+ return false if termination_date.nil?
29
+
30
+ termination_date.past?
31
+ end
32
+
33
+ def accruing?
34
+ return false unless accruable?
35
+ return true if credits.none?
36
+
37
+ threshold = case product.schedule
38
+ when 'weekly' then 1.week.ago
39
+ when 'monthly' then 1.month.ago
40
+ when 'annually' then 1.year.ago
41
+ end
42
+
43
+ last_accruement_date.before? threshold
44
+ end
45
+
46
+ def accruable?
47
+ return false if terminated?
48
+ return false if account.nil?
49
+
50
+ order_group.complete?
51
+ end
52
+
53
+ def last_accruement_date
54
+ credits.maximum(:created_at)
55
+ end
56
+
57
+ def default_price
58
+ product.price
59
+ end
60
+
61
+ def trigger_callback(trigger)
62
+ source_records.each do |record|
63
+ next unless product.callbacks.has_key? trigger
64
+
65
+ product.callbacks[trigger].each do |callback|
66
+ data = { billable: account.billable, offerable_category: product.offerable_template }
67
+
68
+ arguments = callback[:params]
69
+ keyword_arguments = arguments.extract_options!
70
+
71
+ arguments = data.values_at(*arguments)
72
+ keyword_arguments = keyword_arguments.to_h { |keyword, data_type| [keyword, data[data_type]] }
73
+
74
+ params = arguments
75
+ params << keyword_arguments if keyword_arguments.present?
76
+
77
+ record.public_send(callback[:method_name], *params)
78
+ end
79
+ end
80
+ end
81
+
82
+ def source_records
83
+ return [] if source_scope.empty?
84
+ return [] if product.source_class.nil?
85
+
86
+ product.source_class.where(**source_scope)
87
+ end
88
+ end
@@ -0,0 +1,8 @@
1
+ # TODO: Destroy associated debit when marked as failed
2
+
3
+ class Accountability::Payment < ApplicationRecord
4
+ belongs_to :account
5
+ has_one :debit
6
+
7
+ enum status: %i[pending processing complete failed]
8
+ end
@@ -0,0 +1,53 @@
1
+ # A Product defines any product/service/offering available for users to buy.
2
+ # "Private" products can be made for special offers, one-time deals, or contracts.
3
+ # Terminology:
4
+ # Activation Date - Date at which record becomes usable
5
+ # Expired - No longer available, but still usable by prior adopters
6
+ # Terminated - No longer available - even by prior adopters
7
+
8
+ module Accountability
9
+ class Product < ApplicationRecord
10
+ has_and_belongs_to_many :coupons
11
+ has_many :order_items, dependent: :restrict_with_error
12
+ has_many :credits, through: :order_items, inverse_of: :product
13
+
14
+ serialize :source_scope, Hash
15
+
16
+ enum schedule: %i[one_time weekly monthly annually]
17
+
18
+ def inventory
19
+ return [] if source_class.nil?
20
+
21
+ source_class.where(**source_scope)
22
+ end
23
+
24
+ def offerable_template
25
+ return if offerable_category.nil?
26
+ return @offerable if @offerable.present?
27
+
28
+ offerable = Offerable.collection[offerable_category.to_sym]
29
+
30
+ return offerable if new_record?
31
+
32
+ if offerable.present?
33
+ @offerable = offerable
34
+ else
35
+ raise_offerable_not_found
36
+ end
37
+ end
38
+
39
+ def source_class
40
+ return if offerable_template.nil?
41
+
42
+ offerable_template.class_name.constantize
43
+ end
44
+
45
+ delegate :callbacks, to: :offerable_template
46
+ end
47
+
48
+ private
49
+
50
+ def raise_offerable_not_found
51
+ raise 'Offerable not found'
52
+ end
53
+ end
@@ -0,0 +1,13 @@
1
+ %h1 Accounts
2
+
3
+ %br
4
+
5
+ - @accounts.each do |account|
6
+ %h2
7
+ %b Billable ID:
8
+ = account.billable.id
9
+ %p
10
+ %b Balance:
11
+ = account.balance
12
+ = link_to 'Show', accountability_account_path(account)
13
+ %hr
@@ -0,0 +1,12 @@
1
+ %h1 Account Overview
2
+
3
+ %br
4
+
5
+ %h2
6
+ %b Billable ID:
7
+ = @account.billable.id
8
+ %p
9
+ %b Balance:
10
+ = @account.balance
11
+
12
+ = link_to 'Back', accountability_accounts_path
@@ -0,0 +1,16 @@
1
+ %h1 Shopping Cart
2
+ = link_to 'Back', accountability_products_path
3
+
4
+ %p
5
+ %b Cart ID:
6
+ = @order_group.id
7
+
8
+ - @order_group.order_items.includes(:product).each do |order_item|
9
+ %hr
10
+ %b= order_item.product_name
11
+ %br
12
+ %p
13
+ Price:
14
+ = order_item.default_price
15
+
16
+ = button_to 'Check Out', '#'
@@ -0,0 +1 @@
1
+ <h1>Product#edit</h1>
@@ -0,0 +1,39 @@
1
+ %h1 Products
2
+
3
+ = link_to 'Shopping Cart', accountability_order_group_path(current_order_group)
4
+ |
5
+ = link_to 'New Product', new_accountability_product_path
6
+
7
+ %p
8
+ %b User is admin:
9
+ = admin_session? ? 'yep' : 'nope'
10
+
11
+ -# Temporary - we shouldn't assume they're on Devise
12
+ %p
13
+ %b Email:
14
+ = current_user&.email
15
+
16
+ %p
17
+ %b Account ID (session):
18
+ = session[:current_account_id]
19
+
20
+ %p
21
+ %b Account (db):
22
+ = current_account
23
+
24
+ %p
25
+ %b OrderGroup ID (db):
26
+ = current_order_group&.id
27
+
28
+ %br
29
+ %br
30
+ %br
31
+ %br
32
+ %br
33
+ %hr
34
+
35
+ - @products.each do |product|
36
+ %h2= product.name
37
+ %p= product.description
38
+ = button_to 'Add to cart', add_item_accountability_order_group_path(current_order_group, product_id: product.id)
39
+ %hr