A flexible Ruby gem for managing shopping baskets with support for products, delivery charges, and discount strategies.
- đź›’ Shopping Basket Management: Add products to a basket and calculate totals
- 📦 Product Catalogue: Manage product inventory with codes, names, and prices
- đźšš Flexible Delivery Charges: Configurable delivery charge rules based on order total
- 🎯 Discount Strategies: Extensible discount strategy pattern for promotional offers
- đź§Ş Test Coverage: Comprehensive test suite with RSpec
- đź”§ Extensible Design: Easy to extend with custom rules and behaviors
- We're dealing with an unspecified currency that uses 2 decimal places for cents
- We currently will only support having one discount strategy and one delivery charge rule per basket
- Delivery calculations are only done based on the total price after discount and is not dependent on what items are in the basket
- Discount calculations depend on the items/item combinations in the basket
Add this line to your application's Gemfile:
gem 'bakool'And then execute:
bundle installOr install it yourself as:
gem install bakool- Clone the repository:
git clone https://github.com/fadhil-luqman/bakool.git
cd bakool- Install dependencies:
bundle install- Build and install the gem locally:
bundle exec rake installrequire 'bakool'
# Create a basket with default catalogue and rules
# Note: Delivery is free by default (no delivery charges applied)
basket = Bakool::Basket.new
# Add products by their codes
basket.add("R01") # Red Widget
basket.add("G01") # Green Widget
basket.add("B01") # Blue Widget
# Calculate total (includes delivery charges and discounts)
total = basket.total
puts "Total: $#{total}"The gem comes with a default catalogue containing:
| Code | Name | Price |
|---|---|---|
| R01 | Red Widget | $32.95 |
| G01 | Green Widget | $24.95 |
| B01 | Blue Widget | $7.95 |
You can create your own custom catalogue with your own products. This is useful when you want to use different products or pricing than the default catalogue.
# Create a new empty catalogue
catalogue = Bakool::Catalogue.new
# Add products to your catalogue
catalogue.add_product(Bakool::Product.new("Laptop", "LAP01", 999.99))
catalogue.add_product(Bakool::Product.new("Mouse", "MOU01", 25.50))
catalogue.add_product(Bakool::Product.new("Keyboard", "KEY01", 75.00))
catalogue.add_product(Bakool::Product.new("Monitor", "MON01", 299.99))
# Create a basket with your custom catalogue
basket = Bakool::Basket.new(catalogue: catalogue)
# Add products using your custom codes
basket.add("LAP01") # Laptop
basket.add("MOU01") # Mouse
basket.add("KEY01") # Keyboard
# Calculate total
total = basket.total
puts "Total: $#{total}"# Create a custom catalogue for an electronics store
electronics_catalogue = Bakool::Catalogue.new
# Add electronics products
electronics_catalogue.add_product(Bakool::Product.new("iPhone 15", "IPH15", 799.99))
electronics_catalogue.add_product(Bakool::Product.new("AirPods Pro", "AIRPODS", 249.99))
electronics_catalogue.add_product(Bakool::Product.new("MacBook Air", "MBA", 1199.99))
electronics_catalogue.add_product(Bakool::Product.new("iPad Air", "IPAD", 599.99))
# Create custom delivery charge rule for electronics
electronics_delivery = Bakool::DeliveryChargeRule.new(lambda do |order_total|
if order_total < 10000 then 995 # $9.95 for orders under $100
elsif order_total < 50000 then 495 # $4.95 for orders under $500
else 0 # Free delivery for orders $500+
end
end)
# Create custom discount for AirPods (buy one get one 25% off)
class AirPodsDiscount < Bakool::Discount
def calculate(basket)
airpods = basket.items.filter { |item| item.code == "AIRPODS" }
if airpods.count >= 2
# Apply 25% discount to every second AirPod
(airpods.count / 2) * (airpods.first.price_in_cents * 0.25)
else
0
end
end
end
# Create basket with custom catalogue, delivery, and discount
basket = Bakool::Basket.new(
catalogue: electronics_catalogue,
delivery_charge_rule: electronics_delivery,
discount: AirPodsDiscount.new
)
# Add items
basket.add("IPH15") # iPhone 15
basket.add("AIRPODS") # AirPods Pro
basket.add("AIRPODS") # Second AirPods Pro (25% off)
basket.add("MBA") # MacBook Air
# Calculate total
total = basket.total
puts "Total: $#{total}"Creates a new empty catalogue.
catalogue = Bakool::Catalogue.newAdds a product to the catalogue.
product(Bakool::Product): Product object to add
catalogue.add_product(Bakool::Product.new("Product Name", "CODE", 29.99))Returns a catalogue with the default products (Red Widget, Green Widget, Blue Widget).
default_catalogue = Bakool::Catalogue.default_catalogueBy default, delivery is free (no delivery charges are applied). You can create custom delivery charge rules to implement your own pricing logic.
# Create custom delivery charge rule
delivery_rule = Bakool::DeliveryChargeRule.new(lambda do |order_total|
if order_total < 5000 then 495 # $4.95 for orders under $50
elsif order_total < 9000 then 295 # $2.95 for orders under $90
else 0 # Free delivery for orders $90+
end
end)
basket = Bakool::Basket.new(delivery_charge_rule: delivery_rule)The gem uses the Strategy Pattern for discounts, making it easy to implement and extend different discount types.
# Use the default discount (no discount)
basket = Bakool::Basket.new(discount: Bakool::DefaultDiscount.new)
# Use 50% off second same item discount for Red Widgets
discount = Bakool::FiftyPercentOff2ndSameItemDiscount.new("R01")
basket = Bakool::Basket.new(discount: discount)# Create a custom discount strategy
class BuyOneGetOneFreeDiscount < Bakool::Discount
def initialize(item_code)
@item_code = item_code
end
def calculate(basket)
items = basket.items.filter { |item| item.code == @item_code }
if items.count >= 2
# Apply 100% discount to every second item
(items.count / 2) * items.first.price_in_cents
else
0
end
end
end
# Use the custom discount
discount = BuyOneGetOneFreeDiscount.new("R01")
basket = Bakool::Basket.new(discount: discount)# Create basket with custom rules
delivery_rule = Bakool::DeliveryChargeRule.new(lambda do |order_total|
if order_total < 5000 then 495
elsif order_total < 9000 then 295
else 0
end
end)
# Use 50% off second Red Widget discount
discount = Bakool::FiftyPercentOff2ndSameItemDiscount.new("R01")
basket = Bakool::Basket.new(
delivery_charge_rule: delivery_rule,
discount: discount
)
# Add items
basket.add("R01") # Red Widget
basket.add("R01") # Second Red Widget (50% off)
basket.add("G01") # Green Widget
# Calculate total
total = basket.total
puts "Total: $#{total}" # Output: Total: $54.37The main class for managing shopping baskets.
Creates a new basket instance.
catalogue(optional): Product catalogue (defaults toBakool::Catalogue.default_catalogue)delivery_charge_rule(optional): Delivery charge calculation rule (defaults toBakool::DeliveryChargeRule.default_delivery_charge_rule)discount_rule(optional): Legacy discount rule (defaults toBakool::DiscountRule.default_discount_rule)discount(optional): Discount strategy (defaults toBakool::DefaultDiscount.default_discount)
Adds a product to the basket by its code.
product_code(String): The product code to add- Raises
Bakool::InvalidProductCodeErrorif the product code doesn't exist
Calculates the total price including delivery charges and discounts.
- Returns:
Float- Total price in dollars
Returns the items in the basket.
- Returns:
Array- Array of Product objects
Represents a product in the catalogue.
Creates a new product.
name(String): Product name (required)code(String): Product code (required)price(Float): Product price in dollars (default: 0)
Manages the product inventory.
Creates a new empty catalogue.
catalogue = Bakool::Catalogue.newAdds a product to the catalogue.
product(Bakool::Product): Product object to add
catalogue.add_product(Bakool::Product.new("Product Name", "CODE", 29.99))Returns a catalogue with default products (Red Widget, Green Widget, Blue Widget).
default_catalogue = Bakool::Catalogue.default_catalogueHandles delivery charge calculations.
Creates a new delivery charge rule.
func(Proc, optional): Function that takes order total in cents and returns delivery charge in cents
Calculates delivery charge for an order total.
order_total(Integer): Order total in cents- Returns:
Integer- Delivery charge in cents
The discount system uses the Strategy Pattern for flexible and extensible discount calculations.
Base class for all discount strategies.
Calculates discount for a basket.
basket(Bakool::Basket): Basket object- Returns:
Integer- Discount amount in cents
Default discount strategy that applies no discount.
discount = Bakool::DefaultDiscount.new
# or
discount = Bakool::DefaultDiscount.default_discountApplies 50% discount to the second item of the same type.
# 50% off second Red Widget
discount = Bakool::FiftyPercentOff2ndSameItemDiscount.new("R01")To create a custom discount strategy, inherit from the Bakool::Discount base class:
class CustomDiscount < Bakool::Discount
def initialize(parameters)
# Initialize your discount strategy
end
def calculate(basket)
# Implement your discount logic
# Return discount amount in cents
end
endAfter 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.
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.
Run the test suite:
bundle exec rspecThe gem includes custom error classes:
Bakool::InvalidProductCodeError: Raised when trying to add a product with an invalid code
Bug reports and pull requests are welcome on GitHub at https://github.com/fadhil-luqman/bakool. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the code of conduct.
- Fork the repository
- Create a feature branch (
git checkout -b feature/amazing-feature) - Commit your changes (
git commit -m 'Add some amazing feature') - Push to the branch (
git push origin feature/amazing-feature) - Open a Pull Request
The gem is available as open source under the terms of the MIT License.
Everyone interacting in the Bakool project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the code of conduct.