auctify 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|
+
#
|