accountability 0.1.1 → 0.2.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.
Files changed (52) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +86 -9
  3. data/app/assets/stylesheets/accountability/application.scss +0 -0
  4. data/app/controllers/accountability/accounts_controller.rb +2 -4
  5. data/app/controllers/accountability/billing_configurations_controller.rb +80 -0
  6. data/app/controllers/accountability/order_groups_controller.rb +2 -4
  7. data/app/controllers/accountability/payments_controller.rb +25 -0
  8. data/app/controllers/accountability/products_controller.rb +7 -5
  9. data/app/controllers/accountability/statements_controller.rb +19 -0
  10. data/app/controllers/accountability_controller.rb +47 -0
  11. data/app/helpers/accountability/{application_helper.rb → accountability_helper.rb} +1 -1
  12. data/app/helpers/accountability/billing_configurations_helper.rb +47 -0
  13. data/app/helpers/accountability/products_helper.rb +21 -0
  14. data/app/models/accountability/account.rb +31 -1
  15. data/app/models/accountability/application_record.rb +24 -0
  16. data/app/models/accountability/billing_configuration.rb +41 -0
  17. data/app/models/accountability/coupon.rb +2 -0
  18. data/app/models/accountability/credit.rb +53 -29
  19. data/app/models/accountability/debit.rb +10 -0
  20. data/app/models/accountability/inventory.rb +61 -0
  21. data/app/models/accountability/offerable.rb +28 -1
  22. data/app/models/accountability/offerable/scope.rb +48 -0
  23. data/app/models/accountability/order_item.rb +3 -7
  24. data/app/models/accountability/payment.rb +22 -3
  25. data/app/models/accountability/product.rb +45 -5
  26. data/app/models/accountability/statement.rb +46 -0
  27. data/app/models/accountability/{account/transactions.rb → transactions.rb} +14 -2
  28. data/app/models/concerns/accountability/active_merchant_interface.rb +15 -0
  29. data/app/models/concerns/accountability/active_merchant_interface/stripe_interface.rb +134 -0
  30. data/app/pdfs/statement_pdf.rb +91 -0
  31. data/app/views/accountability/accounts/show.html.haml +21 -9
  32. data/app/views/accountability/products/index.html.haml +17 -34
  33. data/app/views/accountability/products/new.html.haml +73 -0
  34. data/app/views/accountability/shared/_session_info.html.haml +24 -0
  35. data/config/locales/en.yml +19 -0
  36. data/db/migrate/20190814000455_create_accountability_tables.rb +43 -1
  37. data/lib/accountability.rb +2 -1
  38. data/lib/accountability/configuration.rb +22 -2
  39. data/lib/accountability/engine.rb +6 -1
  40. data/lib/accountability/extensions/acts_as_billable.rb +11 -0
  41. data/lib/accountability/rails/routes.rb +72 -0
  42. data/lib/accountability/types.rb +9 -0
  43. data/lib/accountability/types/billing_configuration_types.rb +57 -0
  44. data/lib/accountability/version.rb +1 -1
  45. data/lib/generators/accountability/install_generator.rb +47 -0
  46. data/lib/generators/accountability/templates/migration.rb.tt +155 -0
  47. data/lib/generators/accountability/templates/price_overrides_migration.rb.tt +13 -0
  48. metadata +73 -12
  49. data/app/assets/stylesheets/accountability/application.css +0 -15
  50. data/app/controllers/accountability/application_controller.rb +0 -45
  51. data/app/views/accountability/products/new.html.erb +0 -40
  52. data/lib/accountability/cartographer.rb +0 -28
@@ -0,0 +1,73 @@
1
+ .container
2
+ = link_to 'Back', accountability_products_path, class: 'btn btn-outline-primary float-right'
3
+
4
+ %h1.mb-5 New Product
5
+
6
+ - @product.errors.full_messages.each do |message|
7
+ .alert.alert-danger
8
+ = message
9
+
10
+ = form_with model: [:accountability, @product], local: true do |f|
11
+ .card.my-3
12
+ .card-body
13
+ -# WHEN I GET BACK - Add scope fields and stuff - they only show when @stage is set to 'final' from :create
14
+ -# .. also we should probably run validations on the fields we show .. or at least the category field
15
+
16
+ -# TODO: Add dev only instructions
17
+
18
+ %h3.mb-4 Product Info
19
+
20
+ .form-row
21
+ .form-group.col-md-4
22
+ = f.label :offerable_category
23
+ = f.select :offerable_category, source_class_options, { }, { disabled: disable_category_field?, class: 'form-control' }
24
+ .form-group.col-md-8
25
+ = f.label :name
26
+ = f.text_field :name, class: 'form-control'
27
+ .form-group.optional-fields.col-md-2
28
+ = f.label :sku
29
+ = f.text_field :sku, class: 'form-control'
30
+ .form-group.col-md-3
31
+ = f.label :price
32
+ = f.number_field :price, step: 2, class: 'form-control'
33
+ .form-group.col-md-4
34
+ = f.label :schedule
35
+ = f.select :schedule, schedule_options, {}, {class: 'form-control' }
36
+ .form-group.col-md-3
37
+ = f.label :quantity
38
+ = f.text_field :quantity, class: 'form-control'
39
+ .form-group.col-md-12
40
+ = f.label :description
41
+ = f.text_area :description, class: 'form-control'
42
+
43
+ - if @stage == 'final'
44
+
45
+ %hr
46
+ .card-body
47
+ %h3.mb-4 Details
48
+ .form-row
49
+ - @product.scopes.each do |scope|
50
+ .form-group.col-md-12
51
+ = f.label :offerable_category, scope.title.titleize
52
+ = f.select :offerable_category, scope_options(scope), { }, { class: 'form-control' }
53
+
54
+ %hr
55
+ .card-body
56
+ %h3.mb-4 Availability Options
57
+
58
+ .form-row
59
+ .form-group.optional-fields.col-md-12
60
+ = f.label :activation_date
61
+ = f.datetime_field :activation_date, class: 'form-control'
62
+ %em.small Product or service becomes available for purchase and use
63
+ .form-group.optional-fields.col-md-12
64
+ = f.label :expiration_date
65
+ = f.datetime_field :expiration_date, class: 'form-control'
66
+ %em.small Product or service is no longer available for purchase
67
+ .form-group.optional-fields.col-md-12
68
+ = f.label :termination_date
69
+ = f.datetime_field :termination_date, class: 'form-control'
70
+ %em.small Product or service is no longer available for use
71
+
72
+ = link_to 'Reset', new_accountability_product_path, class: 'btn btn-outline-secondary float-right'
73
+ = f.button 'Submit', type: :submit, name: :stage, value: @stage, class: 'btn btn-success'
@@ -0,0 +1,24 @@
1
+ .card.mb-5
2
+ .card-header
3
+ Session Info
4
+ .row.no-gutters
5
+ .col-md-6
6
+ .card-body.pb-1
7
+ %p
8
+ %strong.text-primary Billable Record:
9
+ = current_account&.billable_record_name
10
+
11
+ %p
12
+ %strong.text-primary Admin Session:
13
+ = admin_session? ? 'Yes' : 'No'
14
+
15
+ .col-md-6
16
+ .card-body.pb-1
17
+
18
+ %p
19
+ %strong.text-primary OrderGroup ID:
20
+ = current_order_group&.id
21
+
22
+ %p
23
+ %strong.text-primary Account ID:
24
+ = current_account&.id
@@ -0,0 +1,19 @@
1
+ en:
2
+ activerecord:
3
+ attributes:
4
+ 'accountability/product':
5
+ offerable_category: "Category"
6
+ schedule: "Billing Schedule"
7
+ quantity: "Bundle Size"
8
+ sku: "SKU"
9
+ accountability:
10
+ gateway:
11
+ errors:
12
+ unknown_gateway_error: "An unknown gateway error has occurred"
13
+ helpers:
14
+ label:
15
+ billing_configuration:
16
+ configuration_name: "Payment Method Name"
17
+ contact_email: "Billing Email"
18
+ contact_first_name: "First Name"
19
+ contact_last_name: "Last Name"
@@ -1,16 +1,24 @@
1
1
  # rubocop:disable Metrics/AbcSize
2
+ # rubocop:disable Metrics/LineLength
2
3
  # rubocop:disable Metrics/MethodLength
3
4
 
4
5
  class CreateAccountabilityTables < ActiveRecord::Migration[6.0]
5
6
  def change
6
7
  create_table :accountability_accounts do |t|
7
8
  t.belongs_to :billable, null: true, polymorphic: true
8
-
9
+ t.integer :statement_schedule, default: 0, null: false # end_of_month bi_weekly
9
10
  t.datetime :last_balanced_at, default: nil, null: true
10
11
 
11
12
  t.timestamps
12
13
  end
13
14
 
15
+ create_table :accountability_statements do |t|
16
+ t.belongs_to :account, null: false, index: { name: :index_account_on_statement }
17
+ t.datetime :end_date, default: nil, null: false
18
+
19
+ t.timestamps
20
+ end
21
+
14
22
  create_table :accountability_debits do |t|
15
23
  t.belongs_to :account, null: false, index: { name: :index_account_on_debit }
16
24
  t.belongs_to :payment, null: true, index: { name: :index_payment_on_debit }
@@ -22,13 +30,17 @@ class CreateAccountabilityTables < ActiveRecord::Migration[6.0]
22
30
  create_table :accountability_credits do |t|
23
31
  t.belongs_to :account, null: false, index: { name: :index_account_on_credit }
24
32
  t.belongs_to :order_item, null: false, index: { name: :index_order_item_on_credit }
33
+ t.belongs_to :statement, null: false, index: { name: :index_statement_on_credit }
25
34
  t.decimal :amount, default: 0.00, precision: 8, scale: 2, null: false
35
+ t.decimal :taxes, default: 0.00, precision: 8, scale: 2, null: false
26
36
 
27
37
  t.timestamps
28
38
  end
29
39
 
30
40
  create_table :accountability_payments do |t|
31
41
  t.belongs_to :account, null: false, index: { name: :index_account_on_payment }
42
+ t.belongs_to :billing_configuration, null: false, index: { name: :index_billing_configuration_on_payment }
43
+
32
44
  t.decimal :amount, default: 0.00, precision: 8, scale: 2, null: false
33
45
  t.integer :status, default: 0, null: false # Pending, Processing, Complete, Failed
34
46
 
@@ -72,6 +84,7 @@ class CreateAccountabilityTables < ActiveRecord::Migration[6.0]
72
84
 
73
85
  create_table :accountability_products do |t|
74
86
  t.boolean :public, default: true, null: false
87
+ t.boolean :tax_exempt, default: false, null: false
75
88
  t.decimal :price, default: 0.00, precision: 8, scale: 2, null: false
76
89
  t.integer :quantity, default: 1, null: false
77
90
  t.string :name, default: nil, null: false
@@ -105,13 +118,42 @@ class CreateAccountabilityTables < ActiveRecord::Migration[6.0]
105
118
  t.timestamps
106
119
  end
107
120
 
121
+ # rubocop:disable Rails/CreateTableWithTimestamps
108
122
  create_table :accountability_coupons_products, id: false do |t|
109
123
  t.belongs_to :product, index: { name: :index_product_on_product_coupon }
110
124
  t.belongs_to :coupon, index: { name: :index_coupon_on_product_coupon }
111
125
  end
126
+ # rubocop:enable Rails/CreateTableWithTimestamps
112
127
 
128
+ # rubocop:disable Rails/CreateTableWithTimestamps
113
129
  create_table :accountability_identities do |t|
114
130
  t.belongs_to :identifiable, polymorphic: true, index: { name: :index_identifiable_on_identity }
115
131
  end
132
+ # rubocop:enable Rails/CreateTableWithTimestamps
133
+
134
+ create_table :accountability_billing_configurations do |t|
135
+ t.belongs_to :account, null: false, index: { name: :index_account_on_billing_configuration }
136
+ t.boolean :primary, default: false, null: false
137
+ t.text :billing_address, default: nil, null: true # JSON serialized value object
138
+ t.text :active_merchant_data, default: nil, null: true # JSON serialized data for ActiveMerchant
139
+ t.integer :provider, default: 0, null: false # [unselected, stripe]
140
+ t.string :token, default: nil, null: true # Used to retrieve & charge the payment method from the provider
141
+ t.string :configuration_name, default: nil, null: true # A name for the payment method
142
+ t.string :contact_email, default: nil, null: true
143
+ t.string :contact_first_name, default: nil, null: true
144
+ t.string :contact_last_name, default: nil, null: true
145
+
146
+ t.timestamps
147
+ end
148
+
149
+ create_table :accountability_price_overrides do |t|
150
+ t.belongs_to :product, index: { name: :index_product_on_price_override }
151
+ t.belongs_to :offerable_source, null: false, polymorphic: true, index: { name: :index_offerable_source_on_price_override }
152
+
153
+ t.decimal :price, default: 0.00, precision: 8, scale: 2, null: false # The inventory item's new price
154
+ t.text :description, default: nil, null: true, limit: 10_000 # Optional field for describing adjustment rationale
155
+
156
+ t.timestamps
157
+ end
116
158
  end
117
159
  end
@@ -1,7 +1,8 @@
1
1
  require_dependency 'accountability/engine'
2
2
  require_dependency 'accountability/configuration'
3
- require_dependency 'accountability/cartographer'
3
+ require_dependency 'accountability/rails/routes'
4
4
  require_dependency 'accountability/extensions'
5
+ require_dependency 'accountability/types'
5
6
 
6
7
  module Accountability
7
8
  def self.configure(_tenant = :default)
@@ -1,8 +1,12 @@
1
1
  module Accountability
2
2
  class Configuration
3
3
  class << self
4
- attr_accessor :billable_identifier
5
- attr_writer :admin_checker
4
+ attr_accessor :logo_path, :payment_gateway, :dev_tools_enabled
5
+ attr_writer :tax_rate, :admin_checker, :billable_identifier, :billable_name_column
6
+
7
+ def tax_rate
8
+ @tax_rate || 0.0
9
+ end
6
10
 
7
11
  def admin_checker
8
12
  if @admin_checker.is_a? Proc
@@ -11,6 +15,22 @@ module Accountability
11
15
  -> { true }
12
16
  end
13
17
  end
18
+
19
+ def billable_identifier
20
+ if @billable_identifier.is_a? Proc
21
+ @billable_identifier
22
+ else
23
+ -> { @current_user }
24
+ end
25
+ end
26
+
27
+ def billable_name_column
28
+ @billable_name_column || :id
29
+ end
30
+
31
+ def dev_tools_enabled?
32
+ !!dev_tools_enabled
33
+ end
14
34
  end
15
35
  end
16
36
  end
@@ -2,8 +2,13 @@ module Accountability
2
2
  class Engine < ::Rails::Engine
3
3
  isolate_namespace Accountability
4
4
 
5
- ActiveSupport.on_load(:active_record) do
5
+ ActiveSupport.on_load :active_record do
6
6
  include Extensions
7
+ include Types
8
+ end
9
+
10
+ ActiveSupport.on_load :action_controller do
11
+ helper Accountability::BillingConfigurationsHelper
7
12
  end
8
13
  end
9
14
  end
@@ -5,8 +5,19 @@ module Accountability
5
5
 
6
6
  class_methods do
7
7
  def acts_as_billable
8
+ after_create :create_default_account
9
+
8
10
  has_many :accounts, as: :billable, class_name: 'Accountability::Account', dependent: :nullify
11
+
9
12
  self.acts_as = acts_as.dup << :billable
13
+
14
+ define_method :create_default_account do
15
+ accounts.first_or_create
16
+ end
17
+
18
+ define_method :default_account do
19
+ accounts.first
20
+ end
10
21
  end
11
22
  end
12
23
  end
@@ -0,0 +1,72 @@
1
+ require 'active_support/core_ext/hash'
2
+
3
+ module ActionDispatch::Routing
4
+ class Mapper
5
+ def accountability_for(_tenants, options = {})
6
+ options[:controllers] ||= {}
7
+ options[:routes] ||= {}
8
+
9
+ options[:controllers].reverse_merge! default_accountability_controller_options
10
+ options[:routes].reverse_merge! default_accountability_route_options
11
+
12
+ options[:path_prefix] ||= options[:path] if options[:path_prefix].nil?
13
+
14
+ define_accountability_routes(options)
15
+ end
16
+
17
+ def define_accountability_routes(options)
18
+ cartographer = -> { accountability_routes(options) }
19
+
20
+ cartographer.call
21
+ Accountability::Engine.routes.draw(&cartographer)
22
+ end
23
+
24
+ def accountability_routes(options)
25
+ scope path: options[:path_prefix], as: :accountability do
26
+ resources :accounts, controller: options.dig(:controllers, :accounts), path: options.dig(:routes, :accounts) do
27
+ resources :billing_configurations, controller: options.dig(:controllers, :billing_configurations), path: options.dig(:routes, :billing_configurations) do
28
+ member { patch :designate_as_primary }
29
+ end
30
+
31
+ resources :payments, only: :create, controller: 'accountability/payments', path: 'payments'
32
+ resources :statements, only: [], controller: 'accountability/statements', path: 'statements' do
33
+ member { get :download_pdf }
34
+ end
35
+ end
36
+
37
+ resources :products, controller: options.dig(:controllers, :products), path: options.dig(:routes, :products)
38
+
39
+ resources :order_groups, controller: options.dig(:controllers, :order_groups), path: options.dig(:routes, :order_groups) do
40
+ member { post :add_item }
41
+ end
42
+ end
43
+
44
+ unscoped_direct(:stripe_v3_javascript) { 'https://js.stripe.com/v3/' }
45
+ end
46
+
47
+ def default_accountability_controller_options
48
+ {
49
+ accounts: 'accountability/accounts',
50
+ products: 'accountability/products',
51
+ order_groups: 'accountability/order_groups',
52
+ billing_configurations: 'accountability/billing_configurations'
53
+ }
54
+ end
55
+
56
+ def default_accountability_route_options
57
+ {
58
+ accounts: 'accounts',
59
+ products: 'products',
60
+ order_groups: 'orders',
61
+ billing_configurations: 'billing_configurations'
62
+ }
63
+ end
64
+
65
+ # Mimic the functionality of Mapper#direct so that we can add a "direct" route from the cartographer proc even
66
+ # though we are in a scope because of #isolate_namespace.
67
+ # https://github.com/rails/rails/blob/6-0-stable/actionpack/lib/action_dispatch/routing/mapper.rb#L2119-L2125
68
+ def unscoped_direct(name, options = {}, &block)
69
+ @set.add_url_helper(name, options, &block)
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,9 @@
1
+ require 'active_merchant'
2
+ require_dependency 'accountability/types/billing_configuration_types'
3
+
4
+ module Accountability
5
+ module Types
6
+ include BillingConfigurationTypes
7
+ ActiveRecord::Type.register(:billing_address, Accountability::Types::BillingAddressType)
8
+ end
9
+ end
@@ -0,0 +1,57 @@
1
+ module Accountability
2
+ module Types
3
+ module BillingConfigurationTypes
4
+ class BillingAddress
5
+ include ActiveModel::Model
6
+ include ActiveModel::Attributes
7
+
8
+ attribute :address_1, :string
9
+ attribute :address_2, :string
10
+ attribute :zip, :integer
11
+ attribute :city, :string
12
+ attribute :state, :string
13
+ attribute :country, :string
14
+
15
+ validates :address_1, :city, :state, :country, presence: true
16
+ validates :zip, numericality: { only_integer: true }
17
+ end
18
+
19
+ class BillingAddressType < ActiveModel::Type::Value
20
+ def type
21
+ :text
22
+ end
23
+
24
+ def cast_value(value)
25
+ case value
26
+ when String
27
+ decoded_value = ActiveSupport::JSON.decode(value)
28
+ BillingAddress.new(decoded_value)
29
+ when Hash
30
+ BillingAddress.new(value)
31
+ when BillingAddress
32
+ value
33
+ end
34
+ end
35
+
36
+ # Only serialize the attributes that we have defined
37
+ # and keep extraneous things like "errors" or
38
+ # "validation_context" from slipping into the database.
39
+ def serialize(data)
40
+ case data
41
+ when Hash
42
+ billing_address = BillingAddress.new(data)
43
+ ActiveSupport::JSON.encode(billing_address.attributes)
44
+ when BillingAddress
45
+ ActiveSupport::JSON.encode(data.attributes)
46
+ else
47
+ super(data)
48
+ end
49
+ end
50
+
51
+ def changed_in_place?(raw_old_value, new_value)
52
+ cast_value(raw_old_value) != new_value
53
+ end
54
+ end
55
+ end
56
+ end
57
+ end
@@ -1,3 +1,3 @@
1
1
  module Accountability
2
- VERSION = '0.1.1'.freeze
2
+ VERSION = '0.2.1'.freeze
3
3
  end
@@ -0,0 +1,47 @@
1
+ require 'rails/generators/active_record'
2
+
3
+ module Accountability
4
+ module Generators
5
+ class InstallGenerator < Rails::Generators::Base
6
+ # Executes with: `rails generate accountability:install`
7
+ # Note: All public instance methods will be called sequentially
8
+
9
+ include ActiveRecord::Generators::Migration
10
+ source_root File.join(__dir__, 'templates')
11
+
12
+ # def create_initializer_file
13
+ # initializer_content = "Accountability.configure { |_config| }"
14
+ # create_file "config/initializers/accountability.rb", initializer_content
15
+ # end
16
+
17
+ def copy_migration
18
+ # Note: migration.rb is always up-to-date
19
+ if fresh_installation?
20
+ destination = 'db/migrate/create_accountability_tables.rb'
21
+ migration_template 'migration.rb', destination, migration_version: migration_version
22
+ return true
23
+ end
24
+
25
+ # Existing applications may need new migration files
26
+ if missing_price_overrides?
27
+ destination = 'db/migrate/create_accountability_price_overrides_tables.rb'
28
+ migration_template 'price_overrides_migration.rb', destination, migration_version: migration_version
29
+ end
30
+ end
31
+
32
+ def migration_version
33
+ "[#{ActiveRecord::VERSION::MAJOR}.#{ActiveRecord::VERSION::MINOR}]"
34
+ end
35
+
36
+ private
37
+
38
+ def fresh_installation?
39
+ !ActiveRecord::Base.connection.table_exists?('accountability_accounts')
40
+ end
41
+
42
+ def missing_price_overrides?
43
+ !ActiveRecord::Base.connection.table_exists?('accountability_price_overrides')
44
+ end
45
+ end
46
+ end
47
+ end