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 +4 -4
- data/.rubocop.yml +3 -0
- data/Gemfile.lock +3 -1
- data/README.md +39 -11
- data/config/database.yml +25 -0
- data/db/development.sqlite3 +0 -0
- data/lib/unit4/checkout/basket.rb +4 -1
- data/lib/unit4/checkout/checkout.rb +28 -10
- data/lib/unit4/checkout/connection.rb +6 -1
- data/lib/unit4/checkout/exceptions.rb +3 -3
- data/lib/unit4/checkout/price_query.rb +9 -3
- data/lib/unit4/checkout/version.rb +1 -1
- data/lib/unit4/checkout.rb +2 -4
- metadata +32 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 8fffe71c0723cfa6a79cc0061c0dd84bf59b2da58a5355a6711a4c4411f6e498
|
4
|
+
data.tar.gz: a7270ea6bcfdb6333afe24f16ed101962237bba4ff0c5c749add5cbe921f6a80
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 4149594b3b6a036a6c3bea54bc01e900f49989e4a71ec10a79d2d05a8e2ee97a9a959fb3877d82791c27658b7bff6a59b286460dd2636c4c79e2683eeb5d5eb8
|
7
|
+
data.tar.gz: a805ebc11314abbf80714c132cae8f77b914c5918abd3c3d42e6284045603e845c4d00e57f894dbba7b7308a570f4b2a8377120f342da064bba06d0281e628f6
|
data/.rubocop.yml
CHANGED
data/Gemfile.lock
CHANGED
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
|
-
|
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
|
-
##
|
49
|
+
## Future work
|
22
50
|
|
23
|
-
|
51
|
+
Use a user supplied database instead of the "products" one. Different attribute names for price can be incorporated too.
|
24
52
|
|
25
|
-
|
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/
|
57
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/BoyanGeorgiev96/unit4-checkout.
|
data/config/database.yml
ADDED
@@ -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,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
|
-
#
|
25
|
-
#
|
26
|
-
|
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
|
-
#
|
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
|
-
#
|
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
|
-
#
|
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
|
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
|
-
|
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
|
-
#
|
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
|
data/lib/unit4/checkout.rb
CHANGED
@@ -8,8 +8,6 @@ require_relative "checkout/price_query"
|
|
8
8
|
require_relative "checkout/exceptions"
|
9
9
|
|
10
10
|
module Unit4
|
11
|
-
module
|
12
|
-
|
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.
|
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
|