unit4-checkout 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: 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