Giter VIP home page Giter VIP logo

prezzo's Introduction

Prezzo Build Status Code Climate

Toolbox to create complex pricing models

Installation

Add this line to your application's Gemfile:

gem "prezzo"

And then execute:

$ bundle

Or install it yourself as:

$ gem install prezzo

Usage

Prezzo::Context

The Prezzo::Context is a source of data for your calculators. Basically, it receives a hash of params and it validates its content, in order to make the calculations safe.

e.g.:

module Uber
  class Context
    include Prezzo::Context
    CATEGORIES = ["UberX", "UberXL", "UberBlack"].freeze

    validations do
      required(:category).filled(included_in?: CATEGORIES)
      required(:distance).filled(:float?)
      required(:total_cars).filled(:int?)
      required(:available_cars).filled(:int?)
    end
  end
end

context = Uber::Context.new(category: "UberBlack", ...)

# when valid
context.valid?
#=> true

# when invalid
context.valid?
#=> false

context.errors
# { distance: ["must be a float"]}

Prezzo::Calculator

The Prezzo::Calculator is a simple interface for injecting dependencies on your calculators and calculating the price. Basically, it makes it possible to receive the context, an Hash of parameters containing the necessary information to calculate your price or a Prezzo::Context.

e.g.:

require "prezzo"

module Uber
  class PricePerDistanceCalculator
    include Prezzo::Calculator

    def calculate
      price_per_kilometer * distance
    end

    def price_per_kilometer
      1.30
    end

    def distance
      context.fetch(:distance)
    end
  end
end

context = Uber::Context.new(distance: 10.0)
Uber::PricePerDistanceCalculator.new(context).calculate
#=> 20.0

Context Validation

If you initialize the context with a hash, it will skip the validation, however, any object that responds to .valid? will attempt a validation, and it will fail if valid? returns false.

Prezzo::Composable

The Prezzo::Composable module is an abstraction that provides a nice way of injecting other calculators define how the price will be composed with all of those calculators.

e.g.:

require "prezzo"

module Uber
  class RidePriceCalculator
    include Prezzo::Calculator
    include Prezzo::Composable

    composed_by base_fare: BaseFareCalculator,
                price_per_distance: PricePerDistanceCalculator,

    def calculate
      base_fare + price_per_distance
    end
  end
end

context = Uber::Context.new(distance: 10.0)
Uber::RidePriceCalculator.new(context).calculate
#=> 47.3

Prezzo::Explainable

The Prezzo::Explainable module is an abstraction that provides a nice way of representing how the price was composed.

e.g.:

require "prezzo"

module Uber
  class RidePriceCalculator
    include Prezzo::Calculator
    include Prezzo::Composable
    include Prezzo::Explainable

    composed_by base_fare: BaseFareCalculator,
                price_per_distance: PricePerDistanceCalculator,
    explain_with :base_fare, :price_per_distance

    def calculate
      base_fare + price_per_distance
    end
  end
end

context = Uber::Context.new(distance: 10.0)
Uber::RidePriceCalculator.new(context).explain
#=> { total: 25.6, components: { base_fare: 4.3, price_per_distance: 21.3 } }

Multiline explain_with

explain_with can be splitted into several lines.

class RidePriceCalculator
  include Prezzo::Explainable

  explain_with :base_fare
  explain_with :price_per_distance
end

explain_with with the recursive: false option

class FooCalculator
  include Prezzo::Calculator
  include Prezzo::Explainable

  explain_with :bar, :baz

  def calculate
    bar + baz
  end

  def bar
    10
  end

  def baz
    20
  end
end

class QuxCalculator
  include Prezzo::Calculator
  include Prezzo::Composable
  include Prezzo::Explainable

  composed_by foo: FooCalculator

  explain_with :foo, recursive: false

  def calculate
    foo + 5
  end
end

QuxCalculator#explain now produces

{
  total: 35,
  components: {
    foo: 30
  }
}

but not

{
  total: 35,
  components: {
    foo: {
      total: 30,
      components: {
        bar: 10,
        baz: 20
      }
    }
  }
}

Check the full Uber pricing for more complete example with many calculators and factors.

Development

After checking out the repo, run make to install dependencies. Then, run make spec to run the tests. You can also run make console for an interactive prompt that will allow you to experiment.

To install this gem onto your local machine, run make install. To release a new version, update the version number in version.rb, and then run make release, which will create a git tag for the version, push git commits and tags, and push the .gem file to rubygems.org.

Contributing

Please consider reading out Contributing Guide.

This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the Contributor Covenant code of conduct.

prezzo's People

Contributors

damireh avatar juanibiapina avatar marceloboeira avatar

Stargazers

 avatar  avatar  avatar

Watchers

 avatar  avatar  avatar

prezzo's Issues

Prezzo::Composable: Calculators cache

Instead of instantiating the calculators everytime you call the "calculator_name" method, cache all the instances and just call the calculate method. Because at some point when I start to "explain" it I would like to not create instances all over again, yet just cache the instances and call calculate again.

Prezzo::Context: Nested context as objects

It would be nice to access the attributes as an object

class Contract
  include Prezzo::Context

  validations do
    required(:foo).schema do
      required(:bar)
    end
  end
end

context = Contract.new(foo: { bar: "a" })
context.foo.bar 
# => a

Prezzo::RSpec::Calculator: Integration with RSpec

Integration to make it easy to test with RSpec.

require "prezzo/rspec"

RSpec.describe Uber::Calulator::PricePerDistance, type: :calculator do
  let(:params) do
    {    
      foo: bar,
      boo: far
    }
  end

  context "when foo" do
    it "returns foo" do
      expect(calculation).to eq(31.5) 
    end
  end
end

calculation is a syntax sugar for calculator.calculate

let(:calculator) { described_class.new(params) }
let(:calculation) { calculator.calculate }

Composed calculators

Provide a way to calculate price based on composition of other calculators, in order to create distinct classes doing their single responsibility, but also having control of the whole flow. The calculators are available for the composed calculator as methods.

class UberPriceCalculator
  include Prezzo::Calculator
  include Prezzo::Composed
  
  composed_by price_per_distance: PricePerDistanceCalculator, 
              base_fare: BaseFareCalculator,
              capacity_multiplier: CapacityMultiplierCalculator

  def calculate
    # the calculators are available here as methods, 
    # you can use as you want to compose the price
    (price_per_distance + base_fare) * capacity_multiplier
  end
end

Basically, when you run "calculate" what will happen is that all the calculators will be initialized and will have a sytax sugar interface to be accessed, like:

price_per_distance 
# 1.0

is actually running:

def price_per_distance
  PricePerDistanceCalculator.new(context).calculate
end

Prezzo::Composable: Use multiple times the same calculator with different params

Specify the section of the context that you want to send as a parameter...

require "prezzo"

module Foo
  class BarPriceCalculator
    include Prezzo::Calculator
    include Prezzo::Composed

    composed_by foo_origin: FooCalculator, { context.fetch(:origin) }
                foo_destination: FooCalculator, { context.fetch(:destination) }

    def calculate
      foo_origin + foo_destination
    end
  end
end

Defined Context Mixin

Create an initializable context that can be validated and all the dependencies are mapped.

e.g.:

class UberContext
  include Prezzo::Context
    
  validate do
    required(:distance, Float)
    optional(:available_cars, Integer)   
  end
end

# On the client level 
ride = Ride.find(..)

context = UberContext.new(distance: ride.distance)
calculator = RidePriceCalculator.new(context)
calculator.calculate
# => 34.2

And also validates, if by any change there is something missing:

# ...
context = UberContext.new(distance: nil)
context.valid? 
# => false
calculator = RidePriceCalculator.new(context)
calculator.calculate
# exception -> Invalid Context, "distance is nil, should be a Float"
# => 34.2

References:
https://github.com/hanami/validations

Explainable

It would be interesting to track down all the factors included on composing the price.

The Explainable could be included both in the standard calculator or in the composed one.

Usage:

class PricePerDistanceCalculator
  include Prezzo::Calculator
  include Prezzo::Explainable
  
  explain_with :distance, :price_per_kilometer, total: :calculator

  def calculate
    distance * price_per_kilometer
  end

  private

  def distance
    31.2
  end

  def price_per_kilometer
    12.3
  end
end
calculator = PricePerDistanceCalculator.new
calculator.calculate
# => 383.76

calculator.explain
{ 
  distance: 31.2,
  price_per_kilometer: 12.3,
  total: 383.76
}

Also possible to use it with theComposed calculator, with context and such...

class UberPriceCalculator
  include Prezzo::Calculator
  include Prezzo::Composed
  include Prezzo::Explainable
  
  composed_by price_per_distance: PricePerDistanceCalculator, 
              base_fare: BaseFareCalculator,
              capacity_multiplier: CapacityMultiplierCalculator
  
  explain_with :context, :calculators

  def calculate
    (base_fare + price_per_distance) * capacity_multiplier
  end
end
calculator = UberPriceCalculator.new(context)
calculator.calculate
# => 383.76
calculator.explain
{
  total: 383.76,
  context: {
    type: "UberBlack"
    distance: 35.3,
    foo: ...
  },
  calculators: {
    base_fare:  13.2, #not explainable
    price_per_distance: {
      distance: 35.3,
      total: 302.0
    }
  }
}

Prezzo::Explainable: Explainable instances conflict

When you have 2 explainable classes, they are conflicting with each other, the last one to be interpreted overrides the @@__methods variable and then all the classes start to explain with the last one's methods.

e.g.:

    class ExplainedCalculator
      include Prezzo::Calculator
      include Prezzo::Composed
      include Prezzo::Explainable

      composed_by foo: FooCalculator,
                  bar: BarCalculator
      explain_with :foo, :bar

      def calculate
        foo + bar
      end
    end

    class ExplainedClass
      include Prezzo::Explainable
      explain_with :boo, :far

      def boo
        10.0
      end

      def far
        20.0
      end
    end

 a = ExplainedCalculator.new(calculation_context) 
 b = ExplainedClass.new 

a.explain
NoMethodError:
       undefined method `boo' for #<ExplainedCalculator:0x007fbc2cd01390 @context={}>
       Did you mean?  foo

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.