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
@@ -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
|