accountability 0.1.0 → 0.1.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 +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
@@ -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,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 @@
|
|
1
|
+
%h1 Add to cart
|
@@ -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
|