unit4-checkout 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: 3539766a2698101358c85167d1f34d563461cdc23b24098439e1948c2e815fba
4
- data.tar.gz: 40bc469e6c776039f1d2d424b73d9904e08546181af2f95a117fbd9b4e39d553
3
+ metadata.gz: 8fffe71c0723cfa6a79cc0061c0dd84bf59b2da58a5355a6711a4c4411f6e498
4
+ data.tar.gz: a7270ea6bcfdb6333afe24f16ed101962237bba4ff0c5c749add5cbe921f6a80
5
5
  SHA512:
6
- metadata.gz: 7abf3c6c31eeaff5105c9f966d8751e14e94aeb585c31a7a6c3e42321797429ce6ae1e6a7fcd083e30801df9b4795144be22967bb5d759c81ca94af447ce146d
7
- data.tar.gz: d5c3abb4a0ce0c7599fc5193d44d73560b7de229b1bdb8c9a1607ee71ab2dd569a860292487a9b05fac605082f950a0e6d001fc20a9e119940b03490c6e4a073
6
+ metadata.gz: 4149594b3b6a036a6c3bea54bc01e900f49989e4a71ec10a79d2d05a8e2ee97a9a959fb3877d82791c27658b7bff6a59b286460dd2636c4c79e2683eeb5d5eb8
7
+ data.tar.gz: a805ebc11314abbf80714c132cae8f77b914c5918abd3c3d42e6284045603e845c4d00e57f894dbba7b7308a570f4b2a8377120f342da064bba06d0281e628f6
data/.rubocop.yml CHANGED
@@ -11,3 +11,6 @@ Style/StringLiteralsInInterpolation:
11
11
 
12
12
  Layout/LineLength:
13
13
  Max: 120
14
+
15
+ Metrics/BlockLength:
16
+ IgnoredMethods: ['describe', 'context']
data/Gemfile.lock CHANGED
@@ -1,7 +1,9 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- unit4-checkout (0.2.0)
4
+ unit4-checkout (0.2.2)
5
+ activerecord (~> 7.0.3)
6
+ erb
5
7
 
6
8
  GEM
7
9
  remote: https://rubygems.org/
data/README.md CHANGED
@@ -1,9 +1,3 @@
1
- # Unit4::Checkout
2
-
3
- Welcome to your new gem! In this directory, you'll find the files you need to be able to package up your Ruby library into a gem. Put your Ruby code in the file `lib/unit4/checkout`. To experiment with that code, run `bin/console` for an interactive prompt.
4
-
5
- TODO: Delete this and the text above, and describe your gem
6
-
7
1
  ## Installation
8
2
 
9
3
  Install the gem and add to the application's Gemfile by executing:
@@ -16,14 +10,48 @@ If bundler is not being used to manage dependencies, install the gem by executin
16
10
 
17
11
  ## Usage
18
12
 
19
- TODO: Write usage instructions here
13
+ Please note the gem relies on a database connection to the "products" table. Please make sure you have that table created and products have the 'price' attribute (float, non-nil).
14
+
15
+ Create a Checkout instance with OPTIONAL promotional rules:
16
+
17
+ ```
18
+ co = Checkout.new(promotional_rules)
19
+ ```
20
+ The promotional rules need to have the following structure (note the "=>" after the item id that is used for a key):
21
+
22
+ ```
23
+ promotional_rules = { product_discounts: { "001" => { count: 2, price: 8.50 } },
24
+ total_price_discount: { price: 60.00, percent: 10 } }
25
+ ```
26
+
27
+ Scanning items:
28
+
29
+ ```
30
+ co.scan("001")
31
+ ```
32
+
33
+ Access the total price:
34
+
35
+ ```
36
+ co.total
37
+ ```
38
+
39
+ ## Testing
40
+
41
+ The gem has its own database config and database for testing that complies with the requested format
42
+
43
+ To run the test suite in the console using RSpec, run
44
+
45
+ ```
46
+ rspec
47
+ ```
20
48
 
21
- ## Development
49
+ ## Future work
22
50
 
23
- After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
51
+ Use a user supplied database instead of the "products" one. Different attribute names for price can be incorporated too.
24
52
 
25
- To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
53
+ Test the gem thoroughly against a plain Ruby project. Environment will either be (most likely) non-existent there so some changes in the `Connection.non_rails_db_config` will be needed.
26
54
 
27
55
  ## Contributing
28
56
 
29
- Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/unit4-checkout.
57
+ Bug reports and pull requests are welcome on GitHub at https://github.com/BoyanGeorgiev96/unit4-checkout.
@@ -0,0 +1,25 @@
1
+ # SQLite. Versions 3.8.0 and up are supported.
2
+ # gem install sqlite3
3
+ #
4
+ # Ensure the SQLite 3 gem is defined in your Gemfile
5
+ # gem "sqlite3"
6
+ #
7
+ default: &default
8
+ adapter: sqlite3
9
+ pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
10
+ timeout: 5000
11
+
12
+ development:
13
+ <<: *default
14
+ database: db/development.sqlite3
15
+
16
+ # Warning: The database defined as "test" will be erased and
17
+ # re-generated from your development database when you run "rake".
18
+ # Do not set this db to the same as development or production.
19
+ test:
20
+ <<: *default
21
+ database: db/test.sqlite3
22
+
23
+ production:
24
+ <<: *default
25
+ database: db/production.sqlite3
Binary file
@@ -1,7 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ # basket should probably be separated from the checkout, hence this class
1
4
  class Basket
2
5
  attr_accessor :items
3
6
 
4
7
  def initialize
5
8
  @items = {}
6
9
  end
7
- end
10
+ end
@@ -1,18 +1,30 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ # the main class that is used for scaning the items and applying the discounts
3
4
  class Checkout
4
5
  attr_reader :total
5
6
 
6
7
  def initialize(promotional_rules = {})
8
+ # custom exceptions for incorrect Checkout.new({...}) parameters
7
9
  raise PromotionalRulesTypeError, promotional_rules.class.name unless promotional_rules.is_a? Hash
8
10
  raise PromotionalRulesForbiddenKeys unless correct_keys?(promotional_rules)
9
11
 
10
12
  @promotional_rules = promotional_rules
11
13
  @total = 0
12
14
  @basket = Basket.new
15
+ @saved_prices = {}
13
16
  create_db_connection
14
17
  end
15
18
 
19
+ # no structure for the item variable item given, assumed id from sample data input
20
+ def scan(item)
21
+ add_to_basket(item)
22
+ calculate_total(item)
23
+ puts "Item with ID '#{item}' has been added to the basket successfully!"
24
+ end
25
+
26
+ private
27
+
16
28
  def correct_keys?(promotional_rules)
17
29
  (promotional_rules.keys - %i[product_discounts total_price_discount]).empty?
18
30
  end
@@ -21,31 +33,30 @@ class Checkout
21
33
  Connection.new
22
34
  end
23
35
 
24
- # maybe facilitate scanning multiple items at once
25
- # no structure for item given, assumed id
26
- def scan(item)
27
- # check for item id
28
- add_to_basket(item)
29
- calculate_total(item)
30
- puts "Item with ID '#{item}' has been added to the basket successfully!"
31
- end
36
+ # add to total according to several criteria:
37
+ # is the total already discounted, is the item eligible for a discount based on the number of times it got scanned
38
+ # the function also decides whether both discounts or only one is required
32
39
 
33
40
  def calculate_total(item)
34
41
  item_price = item_price(item)
42
+ # use a hash to store seen non-discounted prices so the database is not queried when we already have the info
43
+ @saved_prices[item] ||= item_price
35
44
  if @total_price_discount_applied_flag
36
45
  item_discounted?(item) ? apply_item_and_total_discount(item, item_price) : apply_total_discount_on_item(item_price)
37
46
  else
38
47
  item_discounted?(item) ? apply_item_discount(item, item_price) : @total += item_price
48
+ # apply total discount if the last item moved the price sent the discount threshold
39
49
  apply_total_discount if total_price_discounted?
40
50
  end
41
51
  @total = @total.round(2)
42
52
  end
43
53
 
44
54
  def item_price(item)
45
- # TODO: facilitate DB name different from products, i.e. ask gem user for db name???
46
- PriceQuery.new(item).find_price
55
+ # Future work: facilitate DB table name different from products, i.e. ask gem user for db name
56
+ PriceQuery.new(item).find_price(@saved_prices)
47
57
  end
48
58
 
59
+ # apply both types of discounts at the same time
49
60
  def apply_item_and_total_discount(item, item_price)
50
61
  item_prom_rules = @promotional_rules[:product_discounts][item]
51
62
  item_basket_count = @basket.items[item]
@@ -53,6 +64,9 @@ class Checkout
53
64
  @total += item_and_total(item_basket_count, item_prom_rules, total_discount, item_price)
54
65
  end
55
66
 
67
+ # if it is the first time the specific item has been discounted we discount all its other instances
68
+ # no need to keep track of every item in-memory, so we just use the total count of the item to remove
69
+ # the old sum for this specific item and add the new discounted one
56
70
  def item_and_total(item_basket_count, item_prom_rules, total_discount, item_price)
57
71
  if item_basket_count == item_prom_rules[:count]
58
72
  (item_basket_count * item_prom_rules[:price] - (item_basket_count - 1) * item_price) * total_discount
@@ -61,10 +75,13 @@ class Checkout
61
75
  end
62
76
  end
63
77
 
78
+ # used when only total discount is needed for a non-discounted scanned item, e.g. when the total sum is over $60.00
64
79
  def apply_total_discount_on_item(item_price)
65
80
  @total += item_price * (1 - @promotional_rules[:total_price_discount][:percent] / 100.00)
66
81
  end
67
82
 
83
+ # if item is not in the promotional rules returns false
84
+ # otherwise check if the item count in the basket is higher than the promotional rules threshold
68
85
  def item_discounted?(item)
69
86
  return false unless item_in_discounts?(item)
70
87
 
@@ -75,6 +92,7 @@ class Checkout
75
92
  @promotional_rules[:product_discounts] && @promotional_rules[:product_discounts][item]
76
93
  end
77
94
 
95
+ # only applies the specific item discount. same reasoning as the "item_and_total" function.
78
96
  def apply_item_discount(item, item_price)
79
97
  item_prom_rules = @promotional_rules[:product_discounts][item]
80
98
  item_basket_count = @basket.items[item]
@@ -3,6 +3,7 @@
3
3
  require "active_record"
4
4
  require "erb"
5
5
 
6
+ # creates the connection for both Rails and non-Rails applications
6
7
  class Connection
7
8
  def initialize
8
9
  db_config = setup_db_config
@@ -10,6 +11,7 @@ class Connection
10
11
  end
11
12
 
12
13
  def setup_db_config
14
+ # decide whether the application that uses the gem is a Rails one or a plain Ruby
13
15
  defined?(Rails) && defined?(Rails.env) ? rails_db_config : non_rails_db_config
14
16
  end
15
17
 
@@ -18,7 +20,10 @@ class Connection
18
20
  end
19
21
 
20
22
  def non_rails_db_config
21
- # TODO: use current database instead of development
23
+ # plain Ruby doesn't really have a sense of environments (unless a custom one has been set up)
24
+ # the development key will most likely be changed when the gem is used by a plain Ruby application
25
+ # the database db and config used here are samples from a Rails application, but this works with plain Ruby too
26
+ # Future work: test thoroughly on plain Ruby applications
22
27
  YAML.safe_load(ERB.new(File.read("./config/database.yml")).result, aliases: true)["development"]
23
28
  end
24
29
  end
@@ -1,13 +1,13 @@
1
- # frozen_string_literal: true
2
-
1
+ # custom exception for when the user does not provide a hash
3
2
  class PromotionalRulesTypeError < TypeError
4
3
  def initialize(class_name, msg = "expected a Hash, got ", exception_type = "custom")
5
- msg += class_name
4
+ msg << class_name
6
5
  @exception_type = exception_type
7
6
  super(msg)
8
7
  end
9
8
  end
10
9
 
10
+ # custom exception for when the hash has unaccceptable keys
11
11
  class PromotionalRulesForbiddenKeys < TypeError
12
12
  def initialize(msg = "expected Hash with optional keys 'product_discounts' and 'total_price_discount", exception_type = "custom")
13
13
  @exception_type = exception_type
@@ -1,15 +1,21 @@
1
- class PriceQuery
1
+ # frozen_string_literal: true
2
2
 
3
+ # used for querying the database for the non-discounted product price
4
+ class PriceQuery
3
5
  def initialize(item)
4
6
  @sanitized_query = prepare_sql_statement(item)
7
+ @item = item
5
8
  end
6
9
 
7
10
  def prepare_sql_statement(item)
8
- # TODO: keep seen items in cache or variable !!!!!!!!!
11
+ # Future work: facilitate DB table name different from products, i.e. ask gem user for db name
9
12
  ActiveRecord::Base.sanitize_sql_array(["SELECT 'products'.'price' FROM 'products' WHERE 'products'.'id' = ?", item])
10
13
  end
11
14
 
12
- def find_price
15
+ def find_price(saved_prices)
16
+ # grab the price from the hash if available
17
+ return saved_prices[@item] if saved_prices[@item]
18
+
13
19
  results = ActiveRecord::Base.connection.exec_query(@sanitized_query)
14
20
  results.rows.first.first if results.present?
15
21
  end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Unit4
4
4
  module Checkout
5
- VERSION = "0.2.1"
5
+ VERSION = "0.2.2"
6
6
  end
7
7
  end
@@ -8,8 +8,6 @@ require_relative "checkout/price_query"
8
8
  require_relative "checkout/exceptions"
9
9
 
10
10
  module Unit4
11
- module Checkout
12
- class Error < StandardError; end
13
- # Your code goes here...
14
- end
11
+ # the main module for the gem, no code really needed here with the current structure
12
+ module Checkout; end
15
13
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: unit4-checkout
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
  - Boyan Georgiev
@@ -9,7 +9,35 @@ autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
11
  date: 2022-07-22 00:00:00.000000000 Z
12
- dependencies: []
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: activerecord
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: 7.0.3
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: 7.0.3
27
+ - !ruby/object:Gem::Dependency
28
+ name: erb
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: 2.2.3
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: 2.2.3
13
41
  description: A Ruby gem that helps the user implement a checkout system by supplying
14
42
  only the promotional rules
15
43
  email:
@@ -24,6 +52,8 @@ files:
24
52
  - Gemfile.lock
25
53
  - README.md
26
54
  - Rakefile
55
+ - config/database.yml
56
+ - db/development.sqlite3
27
57
  - lib/unit4/checkout.rb
28
58
  - lib/unit4/checkout/basket.rb
29
59
  - lib/unit4/checkout/checkout.rb