accountability 0.1.1 → 0.2.1

Sign up to get free protection for your applications and to get access to all the features.
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