auctify 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/MIT-LICENSE +20 -0
- data/README.md +229 -0
- data/Rakefile +34 -0
- data/app/assets/config/auctify_manifest.js +1 -0
- data/app/assets/javascripts/folio/console/main_app.coffee +1 -0
- data/app/assets/stylesheets/auctify/application.css +15 -0
- data/app/assets/stylesheets/auctify/bidder_registrations.css +4 -0
- data/app/assets/stylesheets/auctify/bids.css +4 -0
- data/app/assets/stylesheets/auctify/sales.css +4 -0
- data/app/assets/stylesheets/auctify/sales_packs.css +4 -0
- data/app/assets/stylesheets/folio/console/_main_app.sass +1 -0
- data/app/assets/stylesheets/scaffold.css +80 -0
- data/app/cells/folio/console/atoms/previews/main_app.coffee +1 -0
- data/app/concerns/auctify/behavior/base.rb +20 -0
- data/app/concerns/auctify/behavior/buyer.rb +16 -0
- data/app/concerns/auctify/behavior/item.rb +27 -0
- data/app/concerns/auctify/behavior/seller.rb +36 -0
- data/app/concerns/auctify/sale/auction_callbacks.rb +44 -0
- data/app/controllers/auctify/api/v1/auctions_controller.rb +61 -0
- data/app/controllers/auctify/api/v1/base_controller.rb +22 -0
- data/app/controllers/auctify/api/v1/console/bids_controller.rb +37 -0
- data/app/controllers/auctify/application_controller.rb +8 -0
- data/app/controllers/auctify/bidder_registrations_controller.rb +65 -0
- data/app/controllers/auctify/bids_controller.rb +64 -0
- data/app/controllers/auctify/sales_controller.rb +75 -0
- data/app/controllers/auctify/sales_packs_controller.rb +75 -0
- data/app/helpers/auctify/application_helper.rb +10 -0
- data/app/helpers/auctify/bidder_registrations_helper.rb +6 -0
- data/app/helpers/auctify/bids_helper.rb +6 -0
- data/app/helpers/auctify/sales_helper.rb +15 -0
- data/app/helpers/auctify/sales_packs_helper.rb +6 -0
- data/app/jobs/auctify/application_job.rb +6 -0
- data/app/jobs/auctify/bidding_closer_job.rb +23 -0
- data/app/jobs/auctify/bidding_is_close_to_end_notifier_job.rb +29 -0
- data/app/mailers/auctify/application_mailer.rb +8 -0
- data/app/models/auctify/application_record.rb +14 -0
- data/app/models/auctify/behaviors.rb +27 -0
- data/app/models/auctify/bid.rb +72 -0
- data/app/models/auctify/bidder_registration.rb +84 -0
- data/app/models/auctify/sale/auction.rb +404 -0
- data/app/models/auctify/sale/base.rb +210 -0
- data/app/models/auctify/sale/retail.rb +97 -0
- data/app/models/auctify/sales_pack.rb +101 -0
- data/app/serializers/auctify/sale/auction_serializer.rb +46 -0
- data/app/services/auctify/bids_appender.rb +241 -0
- data/app/services/auctify/service_base.rb +78 -0
- data/app/types/auctify/minimal_bids_ladder_type.rb +77 -0
- data/app/views/auctify/api/v1/console/bids/index.html.erb +1 -0
- data/app/views/auctify/api/v1/console/bids/show.html.erb +1 -0
- data/app/views/auctify/bidder_registrations/_form.html.erb +27 -0
- data/app/views/auctify/bidder_registrations/edit.html.erb +6 -0
- data/app/views/auctify/bidder_registrations/index.html.erb +33 -0
- data/app/views/auctify/bidder_registrations/new.html.erb +5 -0
- data/app/views/auctify/bidder_registrations/show.html.erb +24 -0
- data/app/views/auctify/bids/_form.html.erb +32 -0
- data/app/views/auctify/bids/edit.html.erb +6 -0
- data/app/views/auctify/bids/index.html.erb +31 -0
- data/app/views/auctify/bids/new.html.erb +5 -0
- data/app/views/auctify/bids/show.html.erb +19 -0
- data/app/views/auctify/sales/_form.html.erb +32 -0
- data/app/views/auctify/sales/edit.html.erb +6 -0
- data/app/views/auctify/sales/index.html.erb +36 -0
- data/app/views/auctify/sales/new.html.erb +5 -0
- data/app/views/auctify/sales/show.html.erb +19 -0
- data/app/views/auctify/sales_packs/_form.html.erb +57 -0
- data/app/views/auctify/sales_packs/edit.html.erb +6 -0
- data/app/views/auctify/sales_packs/index.html.erb +40 -0
- data/app/views/auctify/sales_packs/new.html.erb +5 -0
- data/app/views/auctify/sales_packs/show.html.erb +44 -0
- data/app/views/layouts/auctify/application.html.erb +15 -0
- data/config/locales/auctify.cs.yml +121 -0
- data/config/locales/default.cs.yml +220 -0
- data/config/locales/default.en.yml +215 -0
- data/config/routes.rb +23 -0
- data/db/migrate/20210212163000_create_auctify_sales.rb +16 -0
- data/db/migrate/20210216151350_add_type_to_auctify_sales.rb +7 -0
- data/db/migrate/20210217101246_add_state_to_auctify_sales.rb +7 -0
- data/db/migrate/20210217110819_add_prices_to_auctify_sales.rb +9 -0
- data/db/migrate/20210222103031_add_published_at_to_auctify_sale.rb +7 -0
- data/db/migrate/20210223090240_create_auctify_bidder_registrations.rb +15 -0
- data/db/migrate/20210223152139_create_auctify_bids.rb +13 -0
- data/db/migrate/20210225103227_add_bid_steps_ladder_to_auctify_sales.rb +7 -0
- data/db/migrate/20210412114319_add_reserve_price_to_auctify_sales.rb +7 -0
- data/db/migrate/20210416140603_create_auctify_sales_packs.rb +17 -0
- data/db/migrate/20210416141111_add_pack_reference_to_auctify_sales.rb +7 -0
- data/db/migrate/20210419075003_add_ends_at_to_auctify_sales.rb +7 -0
- data/db/migrate/20210419082800_add_position_to_auctify_sales.rb +8 -0
- data/db/migrate/20210419083321_add_indexes.rb +12 -0
- data/db/migrate/20210421093652_add_number_to_auctify_sales.rb +7 -0
- data/db/migrate/20210423071326_removing_polymorphic_assoc_sale_to_item.rb +7 -0
- data/db/migrate/20210423071534_add_currently_ends_at_to_auctify_sales.rb +7 -0
- data/db/migrate/20210427121353_change_column_published_on_auctify_sale.rb +15 -0
- data/db/migrate/20210427122315_add_featured_to_auctify_sales.rb +7 -0
- data/db/migrate/20210428045651_add_sales_bool_indices.rb +9 -0
- data/db/migrate/20210428064615_allow_seller_to_be_nil.rb +13 -0
- data/db/migrate/20210429051508_add_sales_pack_start_and_end.rb +15 -0
- data/db/migrate/20210430080419_add_sales_slug.rb +11 -0
- data/db/migrate/20210503095704_add_cancelled_to_auctify_bids.rb +7 -0
- data/db/migrate/20210504072041_add_commission_and_contract_to_auctify_sales.rb +8 -0
- data/db/migrate/20210506064100_add_winner_to_auctify_sales.rb +7 -0
- data/db/migrate/20210506072438_add_bids_count_to_auctify_sales.rb +7 -0
- data/db/migrate/20210507085845_rename_bids_count_to_applied_bids_count.rb +7 -0
- data/db/migrate/20210510063146_add_dont_confirm_bids_to_registrations.rb +7 -0
- data/db/migrate/20210511081556_add_index_on_cancelled_on.rb +7 -0
- data/db/migrate/20210512162942_add_sold_at_to_auctify_sales.rb +7 -0
- data/db/migrate/20210607113440_add_commission_in_percent_to_sales_packs.rb +7 -0
- data/db/migrate/20210617062509_add_index_to_auctify_sales_currently_ends_at.rb +7 -0
- data/db/migrate/20210625125732_add_current_winner_to_auctify_sales.rb +7 -0
- data/lib/auctify.rb +7 -0
- data/lib/auctify/configuration.rb +43 -0
- data/lib/auctify/engine.rb +25 -0
- data/lib/auctify/version.rb +5 -0
- data/lib/tasks/auctify_tasks.rake +23 -0
- metadata +442 -0
@@ -0,0 +1,75 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_dependency "auctify/application_controller"
|
4
|
+
|
5
|
+
module Auctify
|
6
|
+
class SalesPacksController < ApplicationController
|
7
|
+
before_action :set_sales_pack, only: [:show, :edit, :update, :destroy]
|
8
|
+
|
9
|
+
# GET /sales_packs
|
10
|
+
def index
|
11
|
+
@sales_packs = SalesPack.all
|
12
|
+
end
|
13
|
+
|
14
|
+
# GET /sales_packs/1
|
15
|
+
def show
|
16
|
+
end
|
17
|
+
|
18
|
+
# GET /sales_packs/new
|
19
|
+
def new
|
20
|
+
@sales_pack = SalesPack.new
|
21
|
+
end
|
22
|
+
|
23
|
+
# GET /sales_packs/1/edit
|
24
|
+
def edit
|
25
|
+
end
|
26
|
+
|
27
|
+
# POST /sales_packs
|
28
|
+
def create
|
29
|
+
@sales_pack = SalesPack.new(sales_pack_params)
|
30
|
+
|
31
|
+
if @sales_pack.save
|
32
|
+
redirect_to @sales_pack, notice: "Sales pack was successfully created."
|
33
|
+
else
|
34
|
+
render :new
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
# PATCH/PUT /sales_packs/1
|
39
|
+
def update
|
40
|
+
if @sales_pack.update(sales_pack_params)
|
41
|
+
redirect_to @sales_pack, notice: "Sales pack was successfully updated."
|
42
|
+
else
|
43
|
+
render :edit
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
# DELETE /sales_packs/1
|
48
|
+
def destroy
|
49
|
+
@sales_pack.destroy
|
50
|
+
redirect_to auctify_sales_packs_url, notice: "Sales pack was successfully destroyed."
|
51
|
+
end
|
52
|
+
|
53
|
+
private
|
54
|
+
# Use callbacks to share common setup or constraints between actions.
|
55
|
+
def set_sales_pack
|
56
|
+
@sales_pack = SalesPack.find(params[:id])
|
57
|
+
end
|
58
|
+
|
59
|
+
# Only allow a trusted parameter "white list" through.
|
60
|
+
def sales_pack_params
|
61
|
+
params.require(:sales_pack)
|
62
|
+
.permit(:title,
|
63
|
+
:description,
|
64
|
+
:position,
|
65
|
+
:slug,
|
66
|
+
:place,
|
67
|
+
:published,
|
68
|
+
:sales_interval,
|
69
|
+
:sales_beginning_hour,
|
70
|
+
:sales_beginning_minutes,
|
71
|
+
:start_date,
|
72
|
+
:end_date)
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
@@ -0,0 +1,10 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Auctify
|
4
|
+
module ApplicationHelper
|
5
|
+
include Auctify::Engine.routes.url_helpers
|
6
|
+
# line above somehow breaks url generation in forms from main_app
|
7
|
+
# so we nedd to ensure main_app supremacy
|
8
|
+
include Rails.application.routes.url_helpers
|
9
|
+
end
|
10
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Auctify
|
4
|
+
module SalesHelper
|
5
|
+
def auctify_id_options_for_select(type)
|
6
|
+
opts = []
|
7
|
+
|
8
|
+
Auctify::Behaviors.registered_classes_as(type).each do |klass|
|
9
|
+
opts += klass.all.collect { |obj| [obj.name, obj.auctify_id] }
|
10
|
+
end
|
11
|
+
|
12
|
+
opts
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Auctify
|
4
|
+
class BiddingCloserJob < Auctify::ApplicationJob
|
5
|
+
queue_as :default
|
6
|
+
|
7
|
+
def perform(auction_id:)
|
8
|
+
return if auction_id.blank?
|
9
|
+
begin
|
10
|
+
auction = Auctify::Sale::Auction.find(auction_id)
|
11
|
+
rescue ActiveRecord::RecordNotFound
|
12
|
+
return
|
13
|
+
end
|
14
|
+
|
15
|
+
if auction.currently_ends_at <= Time.current
|
16
|
+
auction.close_bidding! if auction.in_sale?
|
17
|
+
else
|
18
|
+
self.class.set(wait_until: auction.currently_ends_at)
|
19
|
+
.perform_later(auction_id: auction.id)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Auctify
|
4
|
+
class BiddingIsCloseToEndNotifierJob < Auctify::ApplicationJob
|
5
|
+
queue_as :default
|
6
|
+
|
7
|
+
def perform(auction_id:)
|
8
|
+
return if auction_id.blank?
|
9
|
+
return if Auctify.configuration.when_to_notify_bidders_before_end_of_bidding.nil?
|
10
|
+
|
11
|
+
begin
|
12
|
+
auction = Auctify::Sale::Auction.find(auction_id)
|
13
|
+
rescue ActiveRecord::RecordNotFound
|
14
|
+
return
|
15
|
+
end
|
16
|
+
|
17
|
+
notify_time = auction.ends_at - Auctify.configuration.when_to_notify_bidders_before_end_of_bidding
|
18
|
+
|
19
|
+
return unless auction.open_for_bids?
|
20
|
+
|
21
|
+
if notify_time <= Time.current
|
22
|
+
auction.before_bidding_is_close_to_end
|
23
|
+
else
|
24
|
+
self.class.set(wait_until: notify_time)
|
25
|
+
.perform_later(auction_id: auction.id)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Auctify
|
4
|
+
class ApplicationRecord < ActiveRecord::Base
|
5
|
+
include Folio::Filterable
|
6
|
+
include Folio::NillifyBlanks
|
7
|
+
include Folio::RecursiveSubclasses
|
8
|
+
include Folio::Sortable
|
9
|
+
include Folio::ToLabel
|
10
|
+
|
11
|
+
self.abstract_class = true
|
12
|
+
self.table_name_prefix = "auctify_"
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Auctify
|
4
|
+
module Behaviors
|
5
|
+
@@auctified_classes = {}
|
6
|
+
# I borrowed this from Devise
|
7
|
+
|
8
|
+
# Include the chosen auctify behaviors in your model:
|
9
|
+
#
|
10
|
+
# auctify_as :seller, :buyer
|
11
|
+
#
|
12
|
+
def auctify_as(*behaviors)
|
13
|
+
selected_behaviors = behaviors.map(&:to_sym).uniq
|
14
|
+
|
15
|
+
selected_behaviors.each do |bhv|
|
16
|
+
behavior = Auctify::Behavior::const_get(bhv.to_s.classify) # rubocop:disable Style/ColonMethodCall
|
17
|
+
include behavior
|
18
|
+
Rails.logger.info("Auctifiyng #{self} as #{bhv}: #{@@auctified_classes}")
|
19
|
+
@@auctified_classes[bhv] = ((@@auctified_classes[bhv] || []) + [self]).sort_by { |c| c.name }
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def self.registered_classes_as(behavior)
|
24
|
+
@@auctified_classes[behavior] || []
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,72 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Auctify
|
4
|
+
class Bid < ApplicationRecord
|
5
|
+
belongs_to :registration, class_name: "Auctify::BidderRegistration", inverse_of: :bids
|
6
|
+
|
7
|
+
scope :ordered, -> { order(price: :desc, id: :desc) }
|
8
|
+
scope :applied, -> { where(cancelled: false) }
|
9
|
+
scope :canceled, -> { where(cancelled: true) }
|
10
|
+
scope :with_limit, -> { where.not(max_price: nil) }
|
11
|
+
|
12
|
+
validate :price_is_not_bigger_then_max_price
|
13
|
+
|
14
|
+
def cancel!
|
15
|
+
update!(cancelled: true)
|
16
|
+
|
17
|
+
auction.recalculate_bidding!
|
18
|
+
end
|
19
|
+
|
20
|
+
def with_limit?
|
21
|
+
max_price.present?
|
22
|
+
end
|
23
|
+
|
24
|
+
def bade_at
|
25
|
+
created_at
|
26
|
+
end
|
27
|
+
|
28
|
+
def price_is_not_bigger_then_max_price
|
29
|
+
errors.add(:price, :must_be_lower_or_equal_max_price) if max_price && max_price < price
|
30
|
+
end
|
31
|
+
|
32
|
+
def bidder=(auctified_model)
|
33
|
+
errors.add(:bidder, :not_auctified) unless auctified_model.class.included_modules.include?(Auctify::Behavior::Buyer)
|
34
|
+
raise "There is already registration for this bid!" if registration.present?
|
35
|
+
@bidder = auctified_model
|
36
|
+
end
|
37
|
+
|
38
|
+
def bidder
|
39
|
+
@bidder ||= registration&.bidder
|
40
|
+
end
|
41
|
+
|
42
|
+
def auction
|
43
|
+
registration&.auction
|
44
|
+
end
|
45
|
+
|
46
|
+
def configuration
|
47
|
+
Auctify.configuration
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
# == Schema Information
|
53
|
+
#
|
54
|
+
# Table name: auctify_bids
|
55
|
+
#
|
56
|
+
# id :bigint(8) not null, primary key
|
57
|
+
# registration_id :integer not null
|
58
|
+
# price :decimal(12, 2) not null
|
59
|
+
# max_price :decimal(12, 2)
|
60
|
+
# created_at :datetime not null
|
61
|
+
# updated_at :datetime not null
|
62
|
+
# cancelled :boolean default(FALSE)
|
63
|
+
#
|
64
|
+
# Indexes
|
65
|
+
#
|
66
|
+
# index_auctify_bids_on_cancelled (cancelled)
|
67
|
+
# index_auctify_bids_on_registration_id (registration_id)
|
68
|
+
#
|
69
|
+
# Foreign Keys
|
70
|
+
#
|
71
|
+
# fk_rails_... (registration_id => auctify_bidder_registrations.id)
|
72
|
+
#
|
@@ -0,0 +1,84 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Auctify
|
4
|
+
class BidderRegistration < ApplicationRecord
|
5
|
+
include AASM
|
6
|
+
|
7
|
+
belongs_to :bidder, polymorphic: true
|
8
|
+
belongs_to :auction, class_name: "Auctify::Sale::Auction", inverse_of: :bidder_registrations
|
9
|
+
has_many :bids, class_name: "Auctify::Bid",
|
10
|
+
foreign_key: "registration_id",
|
11
|
+
inverse_of: :registration,
|
12
|
+
dependent: :restrict_with_error # destroy them manually first
|
13
|
+
|
14
|
+
has_many :ordered_applied_bids, -> { applied.ordered },
|
15
|
+
class_name: "Auctify::Bid",
|
16
|
+
foreign_key: "registration_id",
|
17
|
+
inverse_of: :registration,
|
18
|
+
dependent: :restrict_with_error # destroy them manually first
|
19
|
+
|
20
|
+
aasm do
|
21
|
+
state :pending, initial: true, color: "gray"
|
22
|
+
state :approved, color: "green"
|
23
|
+
state :rejected, color: "black"
|
24
|
+
|
25
|
+
event :approve do
|
26
|
+
transitions from: :pending, to: :approved
|
27
|
+
|
28
|
+
before do
|
29
|
+
self.handled_at = Time.current
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
event :unapprove do
|
34
|
+
transitions from: :approved, to: :pending
|
35
|
+
|
36
|
+
before do
|
37
|
+
self.handled_at = nil
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
event :reject do
|
42
|
+
transitions from: :pending, to: :rejected
|
43
|
+
|
44
|
+
before do
|
45
|
+
self.handled_at = Time.current
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
validate :auction_is_in_allowed_state, on: :create
|
51
|
+
|
52
|
+
private
|
53
|
+
def auction_is_in_allowed_state
|
54
|
+
unless auction && auction.allows_new_bidder_registrations?
|
55
|
+
errors.add(:auction, :auction_do_not_allow_new_registrations)
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
# == Schema Information
|
62
|
+
#
|
63
|
+
# Table name: auctify_bidder_registrations
|
64
|
+
#
|
65
|
+
# id :bigint(8) not null, primary key
|
66
|
+
# bidder_type :string not null
|
67
|
+
# bidder_id :integer not null
|
68
|
+
# auction_id :integer not null
|
69
|
+
# aasm_state :string default("pending"), not null
|
70
|
+
# handled_at :datetime
|
71
|
+
# created_at :datetime not null
|
72
|
+
# updated_at :datetime not null
|
73
|
+
# dont_confirm_bids :boolean default(FALSE)
|
74
|
+
#
|
75
|
+
# Indexes
|
76
|
+
#
|
77
|
+
# index_auctify_bidder_registrations_on_aasm_state (aasm_state)
|
78
|
+
# index_auctify_bidder_registrations_on_auction_id (auction_id)
|
79
|
+
# index_auctify_bidder_registrations_on_bidder (bidder_type,bidder_id)
|
80
|
+
#
|
81
|
+
# Foreign Keys
|
82
|
+
#
|
83
|
+
# fk_rails_... (auction_id => auctify_sales.id)
|
84
|
+
#
|
@@ -0,0 +1,404 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Auctify
|
4
|
+
module Sale
|
5
|
+
class Auction < Auctify::Sale::Base
|
6
|
+
include AASM
|
7
|
+
include Auctify::Sale::AuctionCallbacks
|
8
|
+
|
9
|
+
ATTRIBUTES_UNMUTABLE_AT_SOME_STATE = %i[ends_at offered_price]
|
10
|
+
DEPENDENT_ATTRIBUTES = {
|
11
|
+
ends_at: %i[currently_ends_at],
|
12
|
+
offered_price: %i[current_price]
|
13
|
+
}
|
14
|
+
|
15
|
+
attr_accessor :winning_bid
|
16
|
+
|
17
|
+
has_many :bidder_registrations, dependent: :destroy
|
18
|
+
has_many :bids, through: :bidder_registrations, dependent: :restrict_with_error # destroy them manually first
|
19
|
+
has_many :ordered_applied_bids, -> { applied.ordered },
|
20
|
+
through: :bidder_registrations,
|
21
|
+
source: :bids,
|
22
|
+
dependent: :restrict_with_error # destroy them manually first
|
23
|
+
|
24
|
+
belongs_to :winner, polymorphic: true, optional: true
|
25
|
+
belongs_to :current_winner, polymorphic: true, optional: true
|
26
|
+
|
27
|
+
validates :ends_at,
|
28
|
+
presence: true
|
29
|
+
|
30
|
+
scope :where_current_winner_is, ->(bidder) { where(current_winner: bidder) }
|
31
|
+
|
32
|
+
aasm do
|
33
|
+
state :offered, initial: true, color: "red"
|
34
|
+
state :accepted, color: "red"
|
35
|
+
state :refused, color: "dark"
|
36
|
+
state :in_sale, color: "yellow"
|
37
|
+
state :bidding_ended, color: "yellow"
|
38
|
+
state :auctioned_successfully, color: "green"
|
39
|
+
state :auctioned_unsuccessfully, color: "red"
|
40
|
+
state :sold, color: "green"
|
41
|
+
state :not_sold, color: "dark"
|
42
|
+
state :cancelled, color: "red"
|
43
|
+
|
44
|
+
event :accept_offer do
|
45
|
+
transitions from: :offered, to: :accepted
|
46
|
+
end
|
47
|
+
|
48
|
+
event :refuse_offer do
|
49
|
+
transitions from: :offered, to: :refused
|
50
|
+
end
|
51
|
+
|
52
|
+
event :start_sale do
|
53
|
+
before do
|
54
|
+
self.current_price = self.offered_price
|
55
|
+
self.currently_ends_at = self.ends_at
|
56
|
+
self.current_winner = nil
|
57
|
+
self.buyer = nil
|
58
|
+
end
|
59
|
+
|
60
|
+
transitions from: :accepted, to: :in_sale
|
61
|
+
|
62
|
+
after do
|
63
|
+
set_bidding_closer_job
|
64
|
+
set_bidding_is_close_to_end_job
|
65
|
+
after_start_sale
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
event :close_bidding do
|
70
|
+
transitions from: :in_sale, to: :bidding_ended
|
71
|
+
|
72
|
+
after do
|
73
|
+
self.winner = current_winner
|
74
|
+
now = Time.current
|
75
|
+
self.currently_ends_at = now if now < currently_ends_at
|
76
|
+
|
77
|
+
after_close_bidding
|
78
|
+
process_bidding_result! if configuration.autofinish_auction_after_bidding == true
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
event :sold_in_auction do
|
83
|
+
before do |*args|
|
84
|
+
params = args.first # expecting keys :buyer, :price
|
85
|
+
self.buyer = params[:buyer]
|
86
|
+
self.sold_price = params[:price]
|
87
|
+
self.sold_at = params[:sold_at] || currently_ends_at
|
88
|
+
end
|
89
|
+
|
90
|
+
transitions from: :bidding_ended, to: :auctioned_successfully, if: :valid?
|
91
|
+
|
92
|
+
after do
|
93
|
+
after_sold_in_auction
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
event :not_sold_in_auction do
|
98
|
+
transitions from: :bidding_ended, to: :auctioned_unsuccessfully, if: :no_winner?
|
99
|
+
|
100
|
+
after do
|
101
|
+
after_not_sold_in_auction
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
event :sell do
|
106
|
+
transitions from: :auctioned_successfully, to: :sold
|
107
|
+
end
|
108
|
+
|
109
|
+
event :end_sale do
|
110
|
+
transitions from: :auctioned_unsuccessfully, to: :not_sold
|
111
|
+
end
|
112
|
+
|
113
|
+
event :cancel do
|
114
|
+
transitions from: [:offered, :accepted], to: :cancelled
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
validate :buyer_vs_bidding_consistence
|
119
|
+
validate :forbidden_changes
|
120
|
+
|
121
|
+
after_create :autoregister_bidders
|
122
|
+
before_destroy :forbid_destroy_if_there_are_bids, prepend: true
|
123
|
+
|
124
|
+
def bidders
|
125
|
+
@bidders ||= bidder_registrations.collect { |br| br.bidder }.sort_by(&:name)
|
126
|
+
end
|
127
|
+
|
128
|
+
def published=(value)
|
129
|
+
super
|
130
|
+
|
131
|
+
if published?
|
132
|
+
accept_offer if offered?
|
133
|
+
start_sale if accepted?
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
def ends_at=(value)
|
138
|
+
super
|
139
|
+
|
140
|
+
self.currently_ends_at = value if currently_ends_at.present?
|
141
|
+
end
|
142
|
+
|
143
|
+
def offered_price=(value)
|
144
|
+
super
|
145
|
+
|
146
|
+
self.current_price = value if current_price.present?
|
147
|
+
end
|
148
|
+
|
149
|
+
def success?
|
150
|
+
return nil if offered? || accepted? || refused? || cancelled? || in_sale? # or raise error?
|
151
|
+
return true if auctioned_successfully? || sold?
|
152
|
+
return false if auctioned_unsuccessfully? || not_sold?
|
153
|
+
|
154
|
+
applied_bids_count.positive? && ((reserve_price || 0) <= current_price)
|
155
|
+
end
|
156
|
+
|
157
|
+
def bid!(bid)
|
158
|
+
ensure_registration(bid)
|
159
|
+
|
160
|
+
ActiveRecord::Base.transaction do
|
161
|
+
bid.created_at ||= Time.current
|
162
|
+
|
163
|
+
bap = Auctify::BidsAppender.call(auction: self, bid: bid)
|
164
|
+
|
165
|
+
bap.success? ? after_bid_appended(bap) : after_bid_not_appended(bap)
|
166
|
+
bap.success?
|
167
|
+
end
|
168
|
+
end
|
169
|
+
|
170
|
+
# callback from bid_appender
|
171
|
+
def succesfull_bid!(price:, winner:, time:)
|
172
|
+
return false if price < current_price || time.blank?
|
173
|
+
|
174
|
+
self.current_price = price
|
175
|
+
self.current_winner = winner
|
176
|
+
self.applied_bids_count = ordered_applied_bids.size
|
177
|
+
extend_end_time(time)
|
178
|
+
|
179
|
+
save!
|
180
|
+
end
|
181
|
+
|
182
|
+
def recalculate_bidding!
|
183
|
+
self.applied_bids_count = ordered_applied_bids.size
|
184
|
+
|
185
|
+
if applied_bids_count.zero?
|
186
|
+
self.current_price = offered_price
|
187
|
+
else
|
188
|
+
winning_price = bidding_result.winning_bid.price
|
189
|
+
self.current_price = winning_price if current_price > winning_price
|
190
|
+
end
|
191
|
+
|
192
|
+
save!
|
193
|
+
end
|
194
|
+
|
195
|
+
delegate :winning_bid, to: :bidding_result
|
196
|
+
def bidding_result
|
197
|
+
Auctify::BidsAppender.call(auction: self, bid: nil).result
|
198
|
+
end
|
199
|
+
|
200
|
+
def current_winner
|
201
|
+
bidding_result.winner
|
202
|
+
end
|
203
|
+
|
204
|
+
def current_winning_bid
|
205
|
+
bidding_result.winning_bid
|
206
|
+
end
|
207
|
+
|
208
|
+
def previous_winning_bid(relative_to_bid = nil)
|
209
|
+
return nil if bids.empty?
|
210
|
+
|
211
|
+
relative_to_bid ||= current_winning_bid
|
212
|
+
considered_bids = bids.ordered.drop_while { |b| b != relative_to_bid }
|
213
|
+
considered_bids.second
|
214
|
+
end
|
215
|
+
|
216
|
+
def current_minimal_bid
|
217
|
+
bidding_result.current_minimal_bid
|
218
|
+
end
|
219
|
+
|
220
|
+
def current_max_price_for(bidder)
|
221
|
+
last_bidder_mx_bid = ordered_applied_bids.with_limit
|
222
|
+
.detect { |bid| bid.bidder == bidder }
|
223
|
+
last_bidder_mx_bid.blank? ? 0 : last_bidder_mx_bid.max_price
|
224
|
+
end
|
225
|
+
|
226
|
+
def open_for_bids?
|
227
|
+
in_sale? && Time.current <= currently_ends_at
|
228
|
+
end
|
229
|
+
|
230
|
+
def opening_price
|
231
|
+
offered_price
|
232
|
+
end
|
233
|
+
|
234
|
+
def allows_new_bidder_registrations?
|
235
|
+
@allows_new_bidder_registrations ||= (in_sale? || accepted?)
|
236
|
+
end
|
237
|
+
|
238
|
+
def bidding_allowed_for?(bidder)
|
239
|
+
babm = bidding_allowed_by_method_for?(bidder)
|
240
|
+
babm.nil? ? true : babm # if no method defined => allow
|
241
|
+
end
|
242
|
+
|
243
|
+
def locked_for_modifications?
|
244
|
+
applied_bids_count.positive?
|
245
|
+
end
|
246
|
+
|
247
|
+
def set_bidding_closer_job
|
248
|
+
Auctify::BiddingCloserJob.set(wait_until: currently_ends_at)
|
249
|
+
.perform_later(auction_id: id)
|
250
|
+
end
|
251
|
+
|
252
|
+
private
|
253
|
+
def buyer_vs_bidding_consistence
|
254
|
+
return true if buyer.blank? && sold_price.blank?
|
255
|
+
|
256
|
+
unless buyer == winner
|
257
|
+
errors.add(:buyer,
|
258
|
+
:buyer_is_not_the_winner,
|
259
|
+
buyer: buyer.to_label,
|
260
|
+
winner: winner.to_label)
|
261
|
+
end
|
262
|
+
|
263
|
+
unless sold_price == bidding_result.won_price
|
264
|
+
errors.add(:sold_price,
|
265
|
+
:sold_price_is_not_from_bidding,
|
266
|
+
sold_price: sold_price,
|
267
|
+
won_price: bidding_result.won_price)
|
268
|
+
end
|
269
|
+
end
|
270
|
+
|
271
|
+
def no_winner?
|
272
|
+
return true if winner.blank?
|
273
|
+
errors.add(:buyer,
|
274
|
+
:there_is_a_buyer_for_not_sold_auction,
|
275
|
+
winner: winner.to_label)
|
276
|
+
false
|
277
|
+
end
|
278
|
+
|
279
|
+
def autoregister_bidders
|
280
|
+
class_names = configuration.autoregister_as_bidders_all_instances_of_classes.to_a
|
281
|
+
return if class_names.blank?
|
282
|
+
|
283
|
+
@allows_new_bidder_registrations = true
|
284
|
+
|
285
|
+
class_names.each do |class_name|
|
286
|
+
class_name.constantize.find_each { |bidder| create_registration(bidder) }
|
287
|
+
end
|
288
|
+
|
289
|
+
@allows_new_bidder_registrations = false
|
290
|
+
end
|
291
|
+
|
292
|
+
def ensure_registration(bid)
|
293
|
+
bid.registration = create_registration(bid.bidder) if autocreate_registration?(bid)
|
294
|
+
end
|
295
|
+
|
296
|
+
def autocreate_registration?(bid)
|
297
|
+
return false if bid.registration.present?
|
298
|
+
return true if configuration.autoregistering_for?(bid.bidder)
|
299
|
+
|
300
|
+
babm = bidding_allowed_by_method_for?(bid.bidder)
|
301
|
+
babm.nil? ? false : babm # if no method defined, do not create
|
302
|
+
end
|
303
|
+
|
304
|
+
def create_registration(bidder)
|
305
|
+
self.bidder_registrations.approved.create!(bidder: bidder, handled_at: Time.current)
|
306
|
+
end
|
307
|
+
|
308
|
+
def bidding_allowed_by_method_for?(bidder)
|
309
|
+
return false if bidder.blank?
|
310
|
+
bidder.try(:bidding_allowed?)
|
311
|
+
end
|
312
|
+
|
313
|
+
def extend_end_time(bid_time)
|
314
|
+
new_end_time = bid_time + Auctify.configuration.auction_prolonging_limit
|
315
|
+
self.currently_ends_at = [currently_ends_at, new_end_time].max
|
316
|
+
end
|
317
|
+
|
318
|
+
def process_bidding_result!
|
319
|
+
case success?
|
320
|
+
when true
|
321
|
+
sold_in_auction!(buyer: current_winner, price: current_price, sold_at: currently_ends_at)
|
322
|
+
when false
|
323
|
+
not_sold_in_auction!
|
324
|
+
else
|
325
|
+
# => nil
|
326
|
+
end
|
327
|
+
end
|
328
|
+
|
329
|
+
def set_bidding_is_close_to_end_job
|
330
|
+
configured_period = Auctify.configuration.when_to_notify_bidders_before_end_of_bidding
|
331
|
+
return if configured_period.blank?
|
332
|
+
notify_time = ends_at - configured_period
|
333
|
+
Auctify::BiddingIsCloseToEndNotifierJob.set(wait_until: notify_time)
|
334
|
+
.perform_later(auction_id: id)
|
335
|
+
end
|
336
|
+
|
337
|
+
def forbidden_changes
|
338
|
+
ATTRIBUTES_UNMUTABLE_AT_SOME_STATE.each do |att|
|
339
|
+
if changes[att].present? && locked_for_modifications?
|
340
|
+
errors.add(att, :no_modification_allowed_now)
|
341
|
+
write_attribute(att, changes[att].first)
|
342
|
+
DEPENDENT_ATTRIBUTES[att].each do |datt|
|
343
|
+
write_attribute(datt, changes[datt].first) if changes[datt].present?
|
344
|
+
end
|
345
|
+
end
|
346
|
+
end
|
347
|
+
end
|
348
|
+
|
349
|
+
def forbid_destroy_if_there_are_bids
|
350
|
+
errors.add(:base, :you_cannot_delete_auction_with_bids) if bids.any?
|
351
|
+
end
|
352
|
+
end
|
353
|
+
end
|
354
|
+
end
|
355
|
+
|
356
|
+
# == Schema Information
|
357
|
+
#
|
358
|
+
# Table name: auctify_sales
|
359
|
+
#
|
360
|
+
# id :bigint(8) not null, primary key
|
361
|
+
# seller_type :string
|
362
|
+
# seller_id :integer
|
363
|
+
# buyer_type :string
|
364
|
+
# buyer_id :integer
|
365
|
+
# item_id :integer not null
|
366
|
+
# created_at :datetime not null
|
367
|
+
# updated_at :datetime not null
|
368
|
+
# type :string default("Auctify::Sale::Base")
|
369
|
+
# aasm_state :string default("offered"), not null
|
370
|
+
# offered_price :decimal(, )
|
371
|
+
# current_price :decimal(, )
|
372
|
+
# sold_price :decimal(, )
|
373
|
+
# bid_steps_ladder :json
|
374
|
+
# reserve_price :decimal(, )
|
375
|
+
# pack_id :bigint(8)
|
376
|
+
# ends_at :datetime
|
377
|
+
# position :integer
|
378
|
+
# number :string
|
379
|
+
# currently_ends_at :datetime
|
380
|
+
# published :boolean default(FALSE)
|
381
|
+
# featured :boolean default(FALSE)
|
382
|
+
# slug :string
|
383
|
+
# contract_number :string
|
384
|
+
# commission_in_percent :integer
|
385
|
+
# winner_type :string
|
386
|
+
# winner_id :bigint(8)
|
387
|
+
# applied_bids_count :integer default(0)
|
388
|
+
# sold_at :datetime
|
389
|
+
# current_winner_type :string
|
390
|
+
# current_winner_id :bigint(8)
|
391
|
+
|
392
|
+
#
|
393
|
+
# Indexes
|
394
|
+
#
|
395
|
+
# index_auctify_sales_on_buyer_type_and_buyer_id (buyer_type,buyer_id)
|
396
|
+
# index_auctify_sales_on_currently_ends_at (currently_ends_at)
|
397
|
+
# index_auctify_sales_on_featured (featured)
|
398
|
+
# index_auctify_sales_on_pack_id (pack_id)
|
399
|
+
# index_auctify_sales_on_position (position)
|
400
|
+
# index_auctify_sales_on_published (published)
|
401
|
+
# index_auctify_sales_on_seller_type_and_seller_id (seller_type,seller_id)
|
402
|
+
# index_auctify_sales_on_slug (slug) UNIQUE
|
403
|
+
# index_auctify_sales_on_winner_type_and_winner_id (winner_type,winner_id)
|
404
|
+
#
|