Project

auctify

0.01
No release in over a year
Gem for adding auction behavior to models.
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
 Project Readme

Auctify

Rails engine for auctions of items. Can be used on any ActiveRecord models.

Objects

  • :item - object/model, which should be auctioned (from main_app)
  • :user - source for seller, owner, buyer and bidder (from main_app)
  • sale - sale of :item; can be direct retail, by auction or some other type
  • auction for one :item (it is Forward auction) (v češtině "Položka aukce")
  • seller - person/company which sells :item in specific sale
  • bidder - registered person/user allowed to bid in specific auction
  • buyer - person/company which buys :item in specific sale ( eg. winner of auction)
  • bid - one bid of one bidder in auction
  • sales_pack - pack of sales(auctions), mostly time framed, Can have physical location or be online. (v češtině "Aukce")
  • auctioneer - (needed?) company organizing auction

Relations

  • :item 1 : 0-N sale, (sale time periods cannot overlap)
  • :item N : 1 owner (can change in time)
  • sale N : 1 seller
  • sale N : 1 buyer
  • sale::auction 1 : N bidders (trough bidder_registrations) 1 : N bids
  • sales_pack 0-1 : N sales (sales_pack is optional for sale)
  • auction N : 1 auctioneer (?)

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_limit minutes before auction ends, end time is extended for bid.time + prolonging_limit (so auction end when there are no bids in last :prolonging_limit minutes)
  • for each auction, there can be set of rules for minimal bid according to current auctioned_price (default can be (1..Infinity) => 1) real example
    minimal_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_price it stays on base_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) or item.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_time and end_time
  • auction can be highlighted for 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

  1. Add gem

    Add this line to your application's Gemfile:

    gem 'auctify'

    And then execute:

    $ bundle

    Or install it yourself as:

    $ gem install auctify
  2. Auctify 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` ....
    end
  3. Configure

    Enqueuing of BiddingCloserJob (for each auction) is done by peridically performed Auctify::EnsureAuctionsClosingJob. This is up to You how you run it, but interval between runs must be shorter than Auctify.configuration.auction_prolonging_limit_in_seconds.

    • optional
      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.
        config.maximal_increase_of_price_proc = Proc.new { |auction| auction.current_price + 5 * auction.offered_price } # default is returning `nil` -> no restrictons
      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.

  4. Callbacks

For available auction callback methods to override see `app/concerns/auctify/sale/auction_callbacks.rb`
  1. Use 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? # => true

    Look into tests test/models/auctify/sale/auction_bidding_test.rb and test/services/auctify/bid_appender_test.rb for more info about bidding process.

    To protect accidential deletions, many associations are binded with dependent: restrict_with_error. Correct order ofdeletion is bids => 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.