accountability 0.2.1 → 0.2.2

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f6693805b727a73f703afae26091f1dbbfdaf8361204ae9fa52ba1b4932fd7d0
4
- data.tar.gz: b0b4f44c7be9a0e0ea9f5ada14fcfd552048a054ba2ece199fdcf7043dce88b2
3
+ metadata.gz: 25f8546f25aae663976cd6cd712b38b444a385a9b5b609bf9f0fa0f1f62e0bb0
4
+ data.tar.gz: 278fb73d9c9d2f9d3a5c7ed609514b82cceb9db892508cb5bda6464b8e959607
5
5
  SHA512:
6
- metadata.gz: da038e1ec8fbf770e59d58cc06ac0e8503ec2e65951ca39ebb968970d2c71a96e598ff1ba334e2217cb802ff1eb64344cc8e8200450cbb1a668e48e3e61bf725
7
- data.tar.gz: 1114f3d2273f1f23a8deff9bcd78b7f075b6dfe15796f1aa5e82d91209d0367f5fc9416a783dcc7659f1b01395c687719288b6cfb6f5e4643441db5276457adf
6
+ metadata.gz: 4bfd0bf5a59e3a8804a96739d416f9dbbb6ade1a56d6bdc821a207eafc58fe783086511001f40262b9c7175e30f8a17078a23af4e1ea1f79d18a1558650b1ab3
7
+ data.tar.gz: 869170f02d04650f64beefa7cc616189420575ceb497ba7ae97b3970f635ce29c1c555bc66bc1157831cfc02f9dc6c4bf97c99b3097c8a1ae2da5b883f2363e6
data/README.md CHANGED
@@ -102,6 +102,19 @@ config.tax_rate = 9.53
102
102
 
103
103
  Note that products can be marked as tax exempt.
104
104
 
105
+ #### Country Whitelist
106
+ To limit which countries your application accepts credit cards from, you can define a whitelist.
107
+
108
+ The whitelist must be an array of [two-character ISO country codes](https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2#Officially_assigned_code_elements).
109
+
110
+ ```ruby
111
+ config.country_whitelist = %w[US CA MX]
112
+ ```
113
+
114
+ When a `country_whitelist` is specified, the BillingConfiguration's country field will be validated for inclusion.
115
+
116
+ The first country listed in the whitelist will be used as the default value for all new BillingConfiguration records.
117
+
105
118
  #### Debugger tools
106
119
  To print helpful session information in the views such as the currently tracked billable entity, enable the dev tools.
107
120
 
@@ -142,7 +155,24 @@ end
142
155
  #### Dynamic Pricing
143
156
  AKA traits
144
157
 
158
+ ## Contributing
159
+ The only current contribution guideline is that we ask contributors to include detailed commit descriptions and avoid pushing merge commits.
160
+
161
+ ### Versioning
162
+ The version number listed in `lib/accountability/version.rb` is the version being actively developed.
163
+
164
+ It is formatted `MAJOR.MINOR.TINY`:
165
+ - MAJOR - Will be set to `1` once ready for public use, at which point we will switch to semantic versioning.
166
+ - MINOR - Is bumped prior to implementing breaking changes.
167
+ - TINY - Is bumped after implementing new features or other meaningful changes.
168
+
169
+ Rebuild the gem after updating the `version.rb` file.
170
+ ```bash
171
+ gem build accountability.gemspec
172
+ ```
173
+
145
174
  ## TODO
146
175
  - [ ] Finish implementing multi-tenanting features
147
- - [ ] Add support for controller overrides
148
- - [ ] Implement product creation workflow in views
176
+ - [ ] Update views to support full E-commerce functionality out of the box
177
+ - [ ] Add test coverage
178
+ - [ ] Add test helpers
@@ -53,6 +53,9 @@ module Accountability
53
53
  end
54
54
 
55
55
  def updated_billing_elements
56
+ return unless partial_exists? 'configurations', within: 'accountability/accounts/billing_configurations'
57
+ return unless partial_exists? 'payment_form', within: 'accountability/accounts'
58
+
56
59
  configurations_partial = 'accountability/accounts/billing_configurations/configurations'
57
60
  payment_form_partial = 'accountability/accounts/payment_form'
58
61
 
@@ -16,12 +16,15 @@ module Accountability
16
16
  def edit; end
17
17
 
18
18
  def create
19
- @order_group = OrderGroup.new(order_group_params)
19
+ @order_group = params[:order_group].present? ? OrderGroup.new(order_group_params) : OrderGroup.new
20
+ source_scope = params.to_unsafe_h[:source_scope]&.symbolize_keys
20
21
 
21
22
  if @order_group.save
22
- redirect_to accountability_order_groups_path, notice: 'Successfully created new order_group'
23
+ @order_group.add_item!(params[:product_id], source_scope: source_scope) if params[:product_id].present?
24
+
25
+ redirect_to accountability_order_group_path(@order_group), notice: 'Successfully created new order_group'
23
26
  else
24
- render :new
27
+ redirect_back fallback_location: root_path, alert: 'Failed to create cart'
25
28
  end
26
29
  end
27
30
 
@@ -43,7 +46,9 @@ module Accountability
43
46
 
44
47
  def add_item
45
48
  product = Product.find(params[:product_id])
46
- if @order_group.add_item! product
49
+ source_scope = params.to_unsafe_h[:source_scope]&.symbolize_keys
50
+
51
+ if @order_group.add_item! product, source_scope: source_scope
47
52
  redirect_to accountability_order_group_path(current_order_group), notice: 'Successfully added to cart'
48
53
  else
49
54
  redirect_back fallback_location: accountability_order_groups_path, alert: 'Failed to add to cart'
@@ -44,4 +44,8 @@ class AccountabilityController < ApplicationController
44
44
  order_group_id = session[:current_order_group_id]
45
45
  Accountability::OrderGroup.find_by(id: order_group_id)
46
46
  end
47
+
48
+ def partial_exists?(partial_name, within:)
49
+ helpers.lookup_context.template_exists?(partial_name, within, true)
50
+ end
47
51
  end
@@ -1,7 +1,7 @@
1
1
  module Accountability
2
2
  class BillingConfiguration < ApplicationRecord
3
3
  include Accountability::ActiveMerchantInterface
4
- after_initialize :set_provider, if: :new_record?
4
+ after_initialize :set_provider, :set_default_country, if: :new_record?
5
5
 
6
6
  belongs_to :account
7
7
  has_many :payments, dependent: :restrict_with_error
@@ -37,5 +37,12 @@ module Accountability
37
37
  def set_provider
38
38
  self.provider = Configuration.payment_gateway[:provider]
39
39
  end
40
+
41
+ def set_default_country
42
+ return unless self.billing_address.present?
43
+ return unless Configuration.country_whitelist.present?
44
+
45
+ self.billing_address.country = Configuration.country_whitelist.first
46
+ end
40
47
  end
41
48
  end
@@ -8,7 +8,7 @@ require 'forwardable'
8
8
  #
9
9
  # Usage:
10
10
  # inventory = Inventory.new(product)
11
- # inventory.active.count
11
+ # inventory.available.count
12
12
 
13
13
  module Accountability
14
14
  class Inventory
@@ -25,10 +25,14 @@ module Accountability
25
25
  end
26
26
 
27
27
  def collection
28
- records = source_class.where(**source_scope)
29
- records = records.public_send(offerable_template.whitelist) if scope_availability?
28
+ source_records.map { |record| InventoryItem.new(record: record, product: product, inventory: self) }
29
+ end
30
30
 
31
- records.map { |record| InventoryItem.new(record: record, product: product) }
31
+ def source_records
32
+ records = source_class.where(**source_scope).includes(:price_overrides)
33
+ records = records.public_send(offerable_template.whitelist) if scope_availability?
34
+ records = records.order(@order_params) if @order_params.present?
35
+ records
32
36
  end
33
37
 
34
38
  def available
@@ -37,18 +41,47 @@ module Accountability
37
41
  self
38
42
  end
39
43
 
44
+ def order(order_params)
45
+ @order_params = order_params
46
+
47
+ self
48
+ end
49
+
50
+ def available_source_ids
51
+ return @available_source_ids if @available_source_ids.present?
52
+
53
+ @available_source_ids = Inventory.new(product, available_only: true).source_records.ids
54
+ end
55
+
40
56
  private
41
57
 
42
58
  class InventoryItem
43
- attr_accessor :record, :product
59
+ attr_accessor :record, :product, :inventory
44
60
 
45
- def initialize(record:, product:)
61
+ def initialize(record:, product:, inventory:)
46
62
  @record = record
47
63
  @product = product
64
+ @inventory = inventory
65
+
66
+ # Expose source record through offerable's name
67
+ record_alias = product.offerable_category.to_sym
68
+ alias :"#{record_alias}" record
48
69
  end
49
70
 
50
71
  def price
51
- product.price
72
+ # Iterating pre-loaded content is faster with Ruby than an N+1 in SQL
73
+ price_override = record.price_overrides.find { |override| override.product_id == product.id }
74
+ price_override&.price || product.price
75
+ end
76
+
77
+ def available?
78
+ return @available unless @available.nil?
79
+
80
+ @available = inventory.available_source_ids.include? record.id
81
+ end
82
+
83
+ def unavailable?
84
+ !available?
52
85
  end
53
86
  end
54
87
 
@@ -29,8 +29,11 @@ module Accountability
29
29
  order_items.each(&:accrue_credit!)
30
30
  end
31
31
 
32
- def add_item!(product)
33
- order_items.create! product: product
32
+ # The `product` parameter accepts Product, String, and Integer objects
33
+ def add_item!(product, source_scope: nil)
34
+ product = Product.find(product) unless product.is_a? Product
35
+
36
+ order_items.create! product: product, source_scope: source_scope.presence
34
37
  end
35
38
 
36
39
  def unassigned?
@@ -51,7 +51,15 @@ class Accountability::OrderItem < ApplicationRecord
51
51
  end
52
52
 
53
53
  def default_price
54
- product.price
54
+ price_override&.price || product.price
55
+ end
56
+
57
+ def price_override
58
+ return @price_override unless @price_override.nil?
59
+ return unless source_records.one?
60
+
61
+ # Memoize as `false` if nil to avoid re-running query
62
+ @price_override = product.price_overrides.find_by(offerable_source: source_records) || false
55
63
  end
56
64
 
57
65
  def trigger_callback(trigger)
@@ -76,9 +84,11 @@ class Accountability::OrderItem < ApplicationRecord
76
84
  end
77
85
 
78
86
  def source_records
87
+ return @source_records unless @source_records.nil?
88
+
79
89
  return [] if source_scope.empty?
80
90
  return [] if product.source_class.nil?
81
91
 
82
- product.source_class.where(**source_scope)
92
+ @source_records = product.source_class.where(**source_scope)
83
93
  end
84
94
  end
@@ -0,0 +1,8 @@
1
+ module Accountability
2
+ class PriceOverride < ApplicationRecord
3
+ belongs_to :offerable_source, polymorphic: true
4
+ belongs_to :product
5
+
6
+ validates :price, presence: true, numericality: { greater_than_or_equal_to: 0 }
7
+ end
8
+ end
@@ -13,6 +13,7 @@ module Accountability
13
13
 
14
14
  has_and_belongs_to_many :coupons
15
15
  has_many :order_items, dependent: :restrict_with_error
16
+ has_many :price_overrides, dependent: :destroy
16
17
  has_many :credits, through: :order_items, inverse_of: :product
17
18
 
18
19
  serialize :source_scope, Hash
@@ -46,6 +46,8 @@ module Accountability
46
46
  private
47
47
 
48
48
  def store_card_in_gateway(gateway = initialize_payment_gateway)
49
+ raise 'No token found' if token.blank?
50
+
49
51
  gateway.store(token, description: configuration_name, email: contact_email, set_default: true)
50
52
  end
51
53
 
@@ -7,6 +7,8 @@ en:
7
7
  quantity: "Bundle Size"
8
8
  sku: "SKU"
9
9
  accountability:
10
+ errors:
11
+ not_permitted: 'is not permitted'
10
12
  gateway:
11
13
  errors:
12
14
  unknown_gateway_error: "An unknown gateway error has occurred"
@@ -1,7 +1,7 @@
1
1
  module Accountability
2
2
  class Configuration
3
3
  class << self
4
- attr_accessor :logo_path, :payment_gateway, :dev_tools_enabled
4
+ attr_accessor :logo_path, :payment_gateway, :dev_tools_enabled, :country_whitelist
5
5
  attr_writer :tax_rate, :admin_checker, :billable_identifier, :billable_name_column
6
6
 
7
7
  def tax_rate
@@ -14,6 +14,11 @@ module Accountability
14
14
  end
15
15
 
16
16
  self.acts_as = acts_as.dup << :offerable
17
+
18
+ if reflections['price_overrides'].blank?
19
+ has_many :price_overrides, class_name: 'Accountability::PriceOverride',
20
+ as: :offerable_source, dependent: :destroy
21
+ end
17
22
  end
18
23
 
19
24
  alias_method :acts_as_offerable, :has_offerable
@@ -14,6 +14,14 @@ module Accountability
14
14
 
15
15
  validates :address_1, :city, :state, :country, presence: true
16
16
  validates :zip, numericality: { only_integer: true }
17
+ validate :validate_country_whitelisted
18
+
19
+ def validate_country_whitelisted
20
+ return unless Configuration.country_whitelist.present?
21
+ return if country.blank?
22
+
23
+ errors.add(:country, :not_permitted) unless Configuration.country_whitelist.include? country
24
+ end
17
25
  end
18
26
 
19
27
  class BillingAddressType < ActiveModel::Type::Value
@@ -1,3 +1,3 @@
1
1
  module Accountability
2
- VERSION = '0.2.1'.freeze
2
+ VERSION = '0.2.2'.freeze
3
3
  end
@@ -9,10 +9,10 @@ module Accountability
9
9
  include ActiveRecord::Generators::Migration
10
10
  source_root File.join(__dir__, 'templates')
11
11
 
12
- # def create_initializer_file
13
- # initializer_content = "Accountability.configure { |_config| }"
14
- # create_file "config/initializers/accountability.rb", initializer_content
15
- # end
12
+ def create_initializer_file
13
+ initializer_content = "Accountability.configure { |_config| }"
14
+ create_file "config/initializers/accountability.rb", initializer_content
15
+ end
16
16
 
17
17
  def copy_migration
18
18
  # Note: migration.rb is always up-to-date
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: accountability
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.1
4
+ version: 0.2.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Joshua Stowers
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2020-03-23 00:00:00.000000000 Z
11
+ date: 2020-03-31 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: active_accountability_merchant
@@ -116,6 +116,7 @@ files:
116
116
  - app/models/accountability/order_group.rb
117
117
  - app/models/accountability/order_item.rb
118
118
  - app/models/accountability/payment.rb
119
+ - app/models/accountability/price_override.rb
119
120
  - app/models/accountability/product.rb
120
121
  - app/models/accountability/statement.rb
121
122
  - app/models/accountability/transactions.rb