I’ve worked with enough SaaS products to know that prices are a bit of a pain in the butt. At first, they seem like numbers, but then you have your first Black Friday sale and begin to understand how tedious it is to display discounts throughout a website.
I decided to end this battle once and for all while working on my latest project, OpenGraph+, by directing Claude Code to build a Price object that makes working with prices and discounts straightforward in the Superfeature gem.
Let’s look at the basics.
Basic usage
Creating prices is straightforward:
price = Superfeature::Price.new(49.99)
price.amount # => BigDecimal("49.99")
price.to_f # => 49.99
price.to_i # => 49
There’s also a handy Price() method for when you want something more concise:
price = Price(29)
price.to_formatted_s # => "29.00"
price.to_formatted_s(decimals: 0) # => "29"
Discounts
This is where things get interesting. You can apply discounts in several ways:
price = Price(100)
price.discount_fixed(20).amount # => 80.0
price.discount_percent(25).amount # => 75.0
price.discount_to(79).amount # => 79.0
Superfeature also parses discount strings naturally. “20%” applies a percentage reduction, “$15” subtracts dollars, and a plain number defaults to dollars:
price = Price(100)
price.apply_discount("20%").amount # => 80.0
price.apply_discount("$15").amount # => 85.0
Chaining and price history
Operations return new Price instances, so you can chain them together. What’s nice is that each price remembers where it came from:
final = Price(100).discount_percent(20).round_up(9)
final.amount # => 89
final.previous.amount # => 80
final.original.amount # => 100
You can also track cumulative savings across multiple operations:
price = Price(100).apply_discount("20%").apply_discount("$10")
price.savings.fixed # => 30.0
price.savings.percent # => 30.0
Rounding to psychological price points
Ever notice how prices always end in .99 or .95? Superfeature makes this easy:
Price(50).round(9) # => Price(49)
Price(50).round_up(9) # => Price(59)
Price(50).round(0.99) # => Price(49.99)
I always want my prices to end with $19, $29, etc. so I call Price(50).round(9).
Custom discount objects
If you have a Coupon model in your app, you can make it work directly with Price by implementing to_discount:
class Coupon < ApplicationRecord
def to_discount
if percent_off.present?
Superfeature::Discount::Percent.new(percent_off)
else
Superfeature::Discount::Fixed.new(amount_off)
end
end
end
# Then use it directly
price = Price(100).apply_discount(coupon)
Rendering prices in views
Here’s how you might pass a price into a partial to display the original price, discount, and sale price:
<%%= render "products/price", price: @product.price.apply_discount("20%") %>
And in the partial:
<%% if price.discounted? %>
<div class="flex items-center gap-2">
<span class="line-through text-gray-500"><%%= number_to_currency(price.original.amount) %></span>
<span class="text-red-600 font-bold"><%%= number_to_currency(price.amount) %></span>
<span class="bg-red-100 text-red-800 text-sm px-2 py-1 rounded">Save <%%= price.savings.percent.round %>%%</span>
</div>
<%% else %>
<span><%%= number_to_currency(price.amount) %></span>
<%% end %>
Much more readable than passing in the full price, sale price, and discount into a partial!
