accountability 0.2.1 → 0.2.2

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