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 +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
|