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.
Files changed (115) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.md +229 -0
  4. data/Rakefile +34 -0
  5. data/app/assets/config/auctify_manifest.js +1 -0
  6. data/app/assets/javascripts/folio/console/main_app.coffee +1 -0
  7. data/app/assets/stylesheets/auctify/application.css +15 -0
  8. data/app/assets/stylesheets/auctify/bidder_registrations.css +4 -0
  9. data/app/assets/stylesheets/auctify/bids.css +4 -0
  10. data/app/assets/stylesheets/auctify/sales.css +4 -0
  11. data/app/assets/stylesheets/auctify/sales_packs.css +4 -0
  12. data/app/assets/stylesheets/folio/console/_main_app.sass +1 -0
  13. data/app/assets/stylesheets/scaffold.css +80 -0
  14. data/app/cells/folio/console/atoms/previews/main_app.coffee +1 -0
  15. data/app/concerns/auctify/behavior/base.rb +20 -0
  16. data/app/concerns/auctify/behavior/buyer.rb +16 -0
  17. data/app/concerns/auctify/behavior/item.rb +27 -0
  18. data/app/concerns/auctify/behavior/seller.rb +36 -0
  19. data/app/concerns/auctify/sale/auction_callbacks.rb +44 -0
  20. data/app/controllers/auctify/api/v1/auctions_controller.rb +61 -0
  21. data/app/controllers/auctify/api/v1/base_controller.rb +22 -0
  22. data/app/controllers/auctify/api/v1/console/bids_controller.rb +37 -0
  23. data/app/controllers/auctify/application_controller.rb +8 -0
  24. data/app/controllers/auctify/bidder_registrations_controller.rb +65 -0
  25. data/app/controllers/auctify/bids_controller.rb +64 -0
  26. data/app/controllers/auctify/sales_controller.rb +75 -0
  27. data/app/controllers/auctify/sales_packs_controller.rb +75 -0
  28. data/app/helpers/auctify/application_helper.rb +10 -0
  29. data/app/helpers/auctify/bidder_registrations_helper.rb +6 -0
  30. data/app/helpers/auctify/bids_helper.rb +6 -0
  31. data/app/helpers/auctify/sales_helper.rb +15 -0
  32. data/app/helpers/auctify/sales_packs_helper.rb +6 -0
  33. data/app/jobs/auctify/application_job.rb +6 -0
  34. data/app/jobs/auctify/bidding_closer_job.rb +23 -0
  35. data/app/jobs/auctify/bidding_is_close_to_end_notifier_job.rb +29 -0
  36. data/app/mailers/auctify/application_mailer.rb +8 -0
  37. data/app/models/auctify/application_record.rb +14 -0
  38. data/app/models/auctify/behaviors.rb +27 -0
  39. data/app/models/auctify/bid.rb +72 -0
  40. data/app/models/auctify/bidder_registration.rb +84 -0
  41. data/app/models/auctify/sale/auction.rb +404 -0
  42. data/app/models/auctify/sale/base.rb +210 -0
  43. data/app/models/auctify/sale/retail.rb +97 -0
  44. data/app/models/auctify/sales_pack.rb +101 -0
  45. data/app/serializers/auctify/sale/auction_serializer.rb +46 -0
  46. data/app/services/auctify/bids_appender.rb +241 -0
  47. data/app/services/auctify/service_base.rb +78 -0
  48. data/app/types/auctify/minimal_bids_ladder_type.rb +77 -0
  49. data/app/views/auctify/api/v1/console/bids/index.html.erb +1 -0
  50. data/app/views/auctify/api/v1/console/bids/show.html.erb +1 -0
  51. data/app/views/auctify/bidder_registrations/_form.html.erb +27 -0
  52. data/app/views/auctify/bidder_registrations/edit.html.erb +6 -0
  53. data/app/views/auctify/bidder_registrations/index.html.erb +33 -0
  54. data/app/views/auctify/bidder_registrations/new.html.erb +5 -0
  55. data/app/views/auctify/bidder_registrations/show.html.erb +24 -0
  56. data/app/views/auctify/bids/_form.html.erb +32 -0
  57. data/app/views/auctify/bids/edit.html.erb +6 -0
  58. data/app/views/auctify/bids/index.html.erb +31 -0
  59. data/app/views/auctify/bids/new.html.erb +5 -0
  60. data/app/views/auctify/bids/show.html.erb +19 -0
  61. data/app/views/auctify/sales/_form.html.erb +32 -0
  62. data/app/views/auctify/sales/edit.html.erb +6 -0
  63. data/app/views/auctify/sales/index.html.erb +36 -0
  64. data/app/views/auctify/sales/new.html.erb +5 -0
  65. data/app/views/auctify/sales/show.html.erb +19 -0
  66. data/app/views/auctify/sales_packs/_form.html.erb +57 -0
  67. data/app/views/auctify/sales_packs/edit.html.erb +6 -0
  68. data/app/views/auctify/sales_packs/index.html.erb +40 -0
  69. data/app/views/auctify/sales_packs/new.html.erb +5 -0
  70. data/app/views/auctify/sales_packs/show.html.erb +44 -0
  71. data/app/views/layouts/auctify/application.html.erb +15 -0
  72. data/config/locales/auctify.cs.yml +121 -0
  73. data/config/locales/default.cs.yml +220 -0
  74. data/config/locales/default.en.yml +215 -0
  75. data/config/routes.rb +23 -0
  76. data/db/migrate/20210212163000_create_auctify_sales.rb +16 -0
  77. data/db/migrate/20210216151350_add_type_to_auctify_sales.rb +7 -0
  78. data/db/migrate/20210217101246_add_state_to_auctify_sales.rb +7 -0
  79. data/db/migrate/20210217110819_add_prices_to_auctify_sales.rb +9 -0
  80. data/db/migrate/20210222103031_add_published_at_to_auctify_sale.rb +7 -0
  81. data/db/migrate/20210223090240_create_auctify_bidder_registrations.rb +15 -0
  82. data/db/migrate/20210223152139_create_auctify_bids.rb +13 -0
  83. data/db/migrate/20210225103227_add_bid_steps_ladder_to_auctify_sales.rb +7 -0
  84. data/db/migrate/20210412114319_add_reserve_price_to_auctify_sales.rb +7 -0
  85. data/db/migrate/20210416140603_create_auctify_sales_packs.rb +17 -0
  86. data/db/migrate/20210416141111_add_pack_reference_to_auctify_sales.rb +7 -0
  87. data/db/migrate/20210419075003_add_ends_at_to_auctify_sales.rb +7 -0
  88. data/db/migrate/20210419082800_add_position_to_auctify_sales.rb +8 -0
  89. data/db/migrate/20210419083321_add_indexes.rb +12 -0
  90. data/db/migrate/20210421093652_add_number_to_auctify_sales.rb +7 -0
  91. data/db/migrate/20210423071326_removing_polymorphic_assoc_sale_to_item.rb +7 -0
  92. data/db/migrate/20210423071534_add_currently_ends_at_to_auctify_sales.rb +7 -0
  93. data/db/migrate/20210427121353_change_column_published_on_auctify_sale.rb +15 -0
  94. data/db/migrate/20210427122315_add_featured_to_auctify_sales.rb +7 -0
  95. data/db/migrate/20210428045651_add_sales_bool_indices.rb +9 -0
  96. data/db/migrate/20210428064615_allow_seller_to_be_nil.rb +13 -0
  97. data/db/migrate/20210429051508_add_sales_pack_start_and_end.rb +15 -0
  98. data/db/migrate/20210430080419_add_sales_slug.rb +11 -0
  99. data/db/migrate/20210503095704_add_cancelled_to_auctify_bids.rb +7 -0
  100. data/db/migrate/20210504072041_add_commission_and_contract_to_auctify_sales.rb +8 -0
  101. data/db/migrate/20210506064100_add_winner_to_auctify_sales.rb +7 -0
  102. data/db/migrate/20210506072438_add_bids_count_to_auctify_sales.rb +7 -0
  103. data/db/migrate/20210507085845_rename_bids_count_to_applied_bids_count.rb +7 -0
  104. data/db/migrate/20210510063146_add_dont_confirm_bids_to_registrations.rb +7 -0
  105. data/db/migrate/20210511081556_add_index_on_cancelled_on.rb +7 -0
  106. data/db/migrate/20210512162942_add_sold_at_to_auctify_sales.rb +7 -0
  107. data/db/migrate/20210607113440_add_commission_in_percent_to_sales_packs.rb +7 -0
  108. data/db/migrate/20210617062509_add_index_to_auctify_sales_currently_ends_at.rb +7 -0
  109. data/db/migrate/20210625125732_add_current_winner_to_auctify_sales.rb +7 -0
  110. data/lib/auctify.rb +7 -0
  111. data/lib/auctify/configuration.rb +43 -0
  112. data/lib/auctify/engine.rb +25 -0
  113. data/lib/auctify/version.rb +5 -0
  114. data/lib/tasks/auctify_tasks.rake +23 -0
  115. 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,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Auctify
4
+ module BidderRegistrationsHelper
5
+ end
6
+ end
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Auctify
4
+ module BidsHelper
5
+ end
6
+ 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,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Auctify
4
+ module SalesPacksHelper
5
+ end
6
+ end
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Auctify
4
+ class ApplicationJob < ActiveJob::Base
5
+ end
6
+ 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,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Auctify
4
+ class ApplicationMailer < ActionMailer::Base
5
+ default from: "from@example.com"
6
+ layout "mailer"
7
+ end
8
+ 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
+ #