Auctify
Rails engine for auctions of items. Can be used on any ActiveRecord models.
Objects
:item- object/model, which should be auctioned (frommain_app):user- source forseller,owner,buyerandbidder(frommain_app)sale- sale of:item; can be directretail, byauctionor some other typeauctionfor one:item(it is Forward auction) (v češtině "Položka aukce")seller- person/company which sells:itemin specificsalebidder- registered person/user allowed to bid in specificauctionbuyer- person/company which buys:itemin specificsale( eg. winner of auction)bid- one bid of onebidderinauctionsales_pack- pack of sales(auctions), mostly time framed, Can have physical location or be online. (v češtině "Aukce")auctioneer- (needed?) company organizingauction
Relations
:item1 : 0-Nsale, (sale time periods cannot overlap):itemN : 1owner(can change in time)saleN : 1sellersaleN : 1buyersale::auction1 : Nbidders(troughbidder_registrations) 1 : Nbidssales_pack0-1 : Nsales(sales_packis optional forsale)auctionN : 1auctioneer(?)
Classes
Sales
Sale::Auction- full body auction of one item for many registered buyer (as bidders)Sale::Retail- direct sale of item to one buyer
TODO
- generator to copy auction's views to main app (so they can be modified) (engine checks main_app views first)
Notices
sale belongs_to item polymorphic , STI typed
sale belongs_to seller polymorphic
sale belongs_to buyer polymorphic
auction has_many bidder_registrations (?bidders?)
bidder_registration belongs buyer polymorphic
If item.owner exists, it is used as auction.seller (or Select from all aucitfied sellers)
Features required
- if bidder adds bid in
:prolonging_limitminutes before auction ends, end time is extended forbid.time + prolonging_limit(so auction end when there are no bids in last:prolonging_limitminutes) - for each auction, there can be set of rules for minimal
bidaccording to currentauctioned_price(default can be(1..Infinity) => 1) real exampleminimal_bids = { (0...5_000) => 100, (5_000...20_000) => 500, (20_000...100_000) => 1_000, (100_000...500_000) => 5_000, (500_000...1_000_000) => 10_000, (1_000_000...2_000_000) => 50_000, (2_000_000..) => 100_000 } - first bid do not increase
auctioned_priceit stays onbase_price - bidder cannot "overbid" themselves (no following bids from same bidder)
- auctioneer can define format of auction numbers (eg. "YY#####") and sales_pack numbers
- there should be ability to follow
auction(notification before end) oritem.author(new item in auction) - item can have category and user can select auction by categories.
- SalePack can be
(un)published /public - SalePack can be
open (adding items , bidding)/ closed(bidding ended) - auctioneer_commision_from_buyer is in % and adds to sold_price in checkout
- auction have
start_timeandend_time - auction can be
highlightedfor cover pages - auction stores all bids history even those cancelled by admin
- there should be two types of bid
- one with maximal price (amount => maximal bid price; placed in small bids as needed), system will increase bid automagicaly unless it reaches maximum
- second direct bid (amount => bid price; immediattely placed)
- sales numbering: YY#### (210001, 210002, …, 259999)
Usage
In ActiveRecord model use class method auctify_as with params :buyer,:seller, :item.
class User < ApplicationRecord
auctify_as :buyer, :seller # this will add method like `sales`, `puchases` ....
end
class Painting < ApplicationRecord
auctify_as :item # this will add method like `sales` ....
end
Auctify expects that auctifyied model instances responds to to_label and :item to reponds to owner (which should lead to object auctified as :seller)!
Installation
Add gem
Add this line to your application's Gemfile:
gem 'auctify'And then execute:
$ bundleOr install it yourself as:
$ gem install auctifyAuctify classes
class User < ApplicationRecord auctify_as :buyer, :seller # this will add method like `sales`, `puchases` .... end class Painting < ApplicationRecord auctify_as :item # this will add method like `sales` .... endConfigure
Enqueuing of
BiddingCloserJob(for each auction) is done by peridically performedAuctify::EnsureAuctionsClosingJob. This is up to You how you run it, but interval between runs must be shorter thanAuctify.configuration.auction_prolonging_limit_in_seconds.
- optional
```ruby
Auctify.configure do |config|
config.autoregister_as_bidders_all_instances_of_classes = ["User"] # default is []
config.auction_prolonging_limit_in_seconds = 10.minutes # default is 1.minute, can be overriden in `SalePack#auction_prolonging_limit_in_seconds` attribute
config.auctioneer_commission_in_percent = 10 # so buyer will pay: auction.current_price * ((100 + 10)/100)
config.autofinish_auction_after_bidding = true # after `auction.close_bidding!` immediatelly proces result to `auction.sold_in_auction!` or `auction.not_sold_in_auction!`; default false
config.when_to_notify_bidders_before_end_of_bidding = 30.minutes # default `nil` => no notifying
config.restrict_overbidding_yourself_to_max_price_increasing = false # default is `true` so only bids with `max_price` can be applied if You are winner.
end
```
If model autified as `:buyer` responds to `:bidding_allowed?` , check is done before each `auction.bid!`. Also if `buyer.bidding_allowed? => true` , registration to auction is created on first bid.
Callbacks
For available auction callback methods to override see
app/concerns/auctify/sale/auction_callbacks.rbUse directly
banksy = User.find_by(nickname: "Banksy") bidder1 = User.find_by(nickname: "Bidder1") bidder2 = User.find_by(nickname: "Bidder2") piece = Painting.find_by(title: "Love is in the bin") piece.owner == banksy # => (not actually) :true auction = banksy.offer_to_sale!(piece, { in: :auction, price: 100 }) auction.offered? # => :true auction.item == piece # => :true auction.seller == banksy # => :true auction.offered_price # => 100.0 banksy.sales # => [auction] banksy.auction_sales # => [auction] banksy.retail_sales # => [] pieces.sales # => [auction] auction.bidder_registrations # => [] unless config.autoregister_as_bidders_all_instances_of_classes is set auction.bidder_registrations.create(bidder: bidder1) # => error, not allowed ("Aukce aktuálně nepovoluje nové registrace") auction.accept_offer! b1_reg = auction.bidder_registrations.create(bidder: bidder1) b2_reg = auction.bidder_registrations.create(bidder: bidder2) auction.bidder_registrations.size # => 2 auction.current_price # => nil auction.start_sale! auction.current_price # => 100.0 aucion.bid!(Auctify::Bid.new(registration: b1_reg, price: nil, max_price: 150)) # auction.bid_appended! is called after succesfull bid, You can override it # auction.bid_not_appended!(errors) is called after unsuccesfull bid, You can override it auction.current_price # => 100.0 auction.bidding_result.winner # => bidder1 auction.bidding_result.current_price # => 100.0 auction.bidding_result.current_minimal_bid # => 101.0 `auction.bid_steps_ladder` is empty, we increasing by 1 aucion.bid!(Auctify::Bid.new(registration: b2_reg, price: 145, max_price: nil)) # some auto bidding is done auction.current_price # => 146.0 auction.bidding_result.winner # => bidder1 auction.bidding_result.current_price # => 146.0 auction.bidding_result.current_minimal_bid # => 147.0 auction.winner # => nil aucion.bid!(Auctify::Bid.new(registration: b2_reg, price: 149, max_price: 155)) # some auto bidding is done auction.current_price # => 151.0 auction.bidding_result.winner # => bidder2 auction.bidding_result.current_price # => 151.0 auction.bidding_result.current_minimal_bid # => 152.0 auction.close_bidding! auction.bidding_ended? # => true auction.buyer # => nil auction.winner # => bidder2 auction.sold_in_auction!(buyer: bidder2, price: 149, sold_at: currently_ends_at) # it is verified against bids! auction.auctioned_successfully? # => true auction.buyer # => bidder2 # when all negotiations went well auction.sell! auction.sold? # => trueLook into tests
test/models/auctify/sale/auction_bidding_test.rbandtest/services/auctify/bid_appender_test.rbfor more info about bidding process.To protect accidential deletions, many associations are binded with
dependent: restrict_with_error. Correct order ofdeletion isbids=>sales=>sales_packs.
Monitor it
Auctify should add some metrics for Prometheus (using Yabeda gem). Exposing them on /metrics path.
group :auctify do
counter :bids_count, comment: "A counter of applied bids"
gauge :diff_in_closing_time_seconds,
comment: "Difference between auction.currently_ends_at and actual sale end time by job"
gauge :time_between_bids, comment: "Time period between last two bids", tags: [:auction_slug]
end
See lib/yabeda_config.rb for current setup.
Note: diff_in_closing_time_seconds is fill in auction.close_bidding!. At normal setup this is done in background job, so value is maintained in BJ process not rails app.
Eg. if You use sidekiq for backgoud jobs, You will need yabeda-sidekiq gem and then value vwill be displayed at port 9394 (your.app:9394/metrics).
Contributing
Contribution directions go here.
License
The gem is available as open source under the terms of the MIT License.