auctify 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
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
+ #