auctify 1.0.0 → 1.1.2

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c19c38113580070b4a0e6d9914b22c042fb3024e2a96193a7004fd61859985c3
4
- data.tar.gz: '080f74640d3c40935a0faf304c120cb294e896e000ffa1cc822a769d077f3fbf'
3
+ metadata.gz: a1ff1f96493a447d685097bcd87d2f6b2798e6c2ae391591692ab840ae6ce2bc
4
+ data.tar.gz: '018847e67646b882ea6525767416a663dc19dd0ee1d87a0bdccb029215f8897f'
5
5
  SHA512:
6
- metadata.gz: b5dc577b91bd6ba035187bbf272f52f744a6ee7832c90c998e27c7eb63324c98b7bfa1eecb259f21522d0dae8accde3ef112a8166d58bc2ece2a6cbfd6edafa2
7
- data.tar.gz: 67f7b5f692569e542eaa5a59727e15c9e602a1d022c69156046acf7e343f4f5acecad42b1e7adfd44bc8b7bb317dcd293bb5aec7c4d9d6e28078b829e24dd8b7
6
+ metadata.gz: 8a185ca59647cb9361d3d315d6a62355a85cad1b28bc7276b4195ccdae2f9bafa74c308b9be00083c7031237830217184e29338083745f8372e811423dd4a878
7
+ data.tar.gz: 959b40425783d4d5d9770ba2d4b5c415684d8264713d537d2f8f9f1488208eae302cb60204c88870af764c9d5ccd279175918580e2268ffe8ae7d805ce9b4806
data/README.md CHANGED
@@ -65,7 +65,7 @@ If `item.owner` exists, it is used as `auction.seller` (or Select from all aucit
65
65
  - item can have category and user can select auction by categories.
66
66
  - SalePack can be `(un)published /public`
67
67
  - SalePack can be `open (adding items , bidding)/ closed(bidding ended)`
68
- - auctioneer_commission is in % and adds to sold_price in checkout
68
+ - auctioneer_commision_from_buyer is in % and adds to sold_price in checkout
69
69
  - auction have `start_time` and `end_time`
70
70
  - auction can be `highlighted` for cover pages
71
71
  - auction stores all bids history even those cancelled by admin
@@ -121,11 +121,14 @@ end
121
121
  ```
122
122
 
123
123
  3. ### Configure
124
+ Enqueuing of `BiddingCloserJob` (for each auction) is done by peridically performed `Auctify::EnsureAuctionsClosingJob`.
125
+ This is up to You how you run it, but interval between runs must be shorter than `Auctify.configuration.auction_prolonging_limit_in_seconds`.
126
+
124
127
  - optional
125
128
  ```ruby
126
129
  Auctify.configure do |config|
127
130
  config.autoregister_as_bidders_all_instances_of_classes = ["User"] # default is []
128
- config.auction_prolonging_limit = 10.minutes # default is 1.minute
131
+ config.auction_prolonging_limit_in_seconds = 10.minutes # default is 1.minute, can be overriden in `SalePack#auction_prolonging_limit_in_seconds` attribute
129
132
  config.auctioneer_commission_in_percent = 10 # so buyer will pay: auction.current_price * ((100 + 10)/100)
130
133
  config.autofinish_auction_after_bidding = true # after `auction.close_bidding!` immediatelly proces result to `auction.sold_in_auction!` or `auction.not_sold_in_auction!`; default false
131
134
  config.when_to_notify_bidders_before_end_of_bidding = 30.minutes # default `nil` => no notifying
@@ -7,21 +7,24 @@ module Auctify
7
7
  before_action :find_auction, except: [:index]
8
8
 
9
9
  def show
10
- render_record @auction
10
+ if params[:updated_at].present? && params[:updated_at].match?(/\A\d+\z/) && params[:updated_at].to_i == @auction.updated_at.to_i
11
+ render json: { current: true }, status: 200
12
+ else
13
+ render_record @auction
14
+ end
11
15
  end
12
16
 
13
17
  def bids
14
18
  if params[:confirmation] == "1"
15
19
  if @auction.bid!(new_bid)
16
- if params[:dont_confirm_bids] == "1"
17
- # use SQL update in case of some obscure invalid attributes
18
- current_user.bidder_registrations
19
- .where(auction: @auction)
20
- .update_all(dont_confirm_bids: true)
21
- end
22
-
23
- render_record @auction.reload, success: true
20
+ @auction.reload
21
+
22
+ store_dont_confirm_bids
23
+
24
+ render_record @auction, success: true, overbid_by_limit: overbid_by_limit?(new_bid)
24
25
  else
26
+ store_dont_confirm_bids
27
+
25
28
  render_record @auction, bid: new_bid, status: 400
26
29
  end
27
30
  else
@@ -50,11 +53,32 @@ module Auctify
50
53
  { bidder: current_user }
51
54
  end
52
55
 
53
- def render_record(auction, bid: nil, status: 200, success: nil)
56
+ def render_record(auction, bid: nil, status: 200, success: nil, overbid_by_limit: nil)
54
57
  render json: {
55
- data: cell("#{global_namespace_path}/auctify/auctions/form", auction, bid: bid, success: success).show
58
+ data: cell("#{global_namespace_path}/auctify/auctions/form",
59
+ auction,
60
+ bid: bid,
61
+ success: success,
62
+ overbid_by_limit: overbid_by_limit).show
56
63
  }, status: status
57
64
  end
65
+
66
+ def store_dont_confirm_bids
67
+ if params[:dont_confirm_bids] == "1"
68
+ # use SQL update in case of some obscure invalid attributes
69
+ bidder_regs = current_user.bidder_registrations.where(auction: @auction)
70
+ bidder_regs.update_all(dont_confirm_bids: true) if bidder_regs.present?
71
+ end
72
+ end
73
+
74
+ def overbid_by_limit?(new_bid)
75
+ winning_bid = @auction&.winning_bid
76
+ return false unless winning_bid
77
+ return false if new_bid.id.blank? || winning_bid.id.blank?
78
+ return false if winning_bid.registration_id == new_bid.registration_id # do not notify bidder about overbidding itself
79
+
80
+ winning_bid != new_bid
81
+ end
58
82
  end
59
83
  end
60
84
  end
@@ -46,8 +46,11 @@ module Auctify
46
46
 
47
47
  # DELETE /bidder_registrations/1
48
48
  def destroy
49
- @bidder_registration.destroy
50
- redirect_to auctify_bidder_registrations_url, notice: "Bidder registration was successfully destroyed."
49
+ if @bidder_registration.destroy
50
+ redirect_to auctify_bidder_registrations_url, notice: "Bidder registration was successfully destroyed."
51
+ else
52
+ render json: { errors: @bidder_registration.errors }
53
+ end
51
54
  end
52
55
 
53
56
  private
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Auctify
4
+ class AutobidFillerJob < Auctify::ApplicationJob
5
+ queue_as :default
6
+
7
+ def perform
8
+ reg_ids = Auctify::Bid.pluck(:registration_id).uniq
9
+ registrations_with_bids = Auctify::BidderRegistration.where(id: reg_ids)
10
+
11
+ registrations_with_bids.find_each { |reg| reg.fillup_autobid_flags! }
12
+ end
13
+ end
14
+ end
@@ -12,11 +12,11 @@ module Auctify
12
12
  return
13
13
  end
14
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)
15
+ return if Time.current < auction.currently_ends_at
16
+
17
+ Auctify::Sale::Auction.with_advisory_lock("closing_auction_#{auction_id}") do
18
+ # can wait unitl other BCJob release lock and than continue!
19
+ auction.close_bidding! if auction.reload.in_sale?
20
20
  end
21
21
  end
22
22
  end
@@ -14,7 +14,7 @@ module Auctify
14
14
  return
15
15
  end
16
16
 
17
- notify_time = auction.ends_at - Auctify.configuration.when_to_notify_bidders_before_end_of_bidding
17
+ notify_time = auction.bidding_is_close_to_end_notification_time
18
18
 
19
19
  return unless auction.open_for_bids?
20
20
 
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Auctify
4
+ class EnsureAuctionsClosingJob < ApplicationJob
5
+ queue_as :critical
6
+
7
+ def perform
8
+ auctions = ::Auctify::Sale::Auction.in_sale
9
+ .where("currently_ends_at <= ?", Time.current + checking_period_to_future)
10
+ auctions.each do |auction|
11
+ if auction.currently_ends_at <= Time.current
12
+ Auctify::BiddingCloserJob.perform_later(auction_id: auction.id)
13
+ else
14
+ Auctify::BiddingCloserJob.set(wait_until: auction.currently_ends_at).perform_later(auction_id: auction.id)
15
+ end
16
+ end
17
+ end
18
+
19
+ private
20
+ def checking_period_to_future
21
+ Auctify.configuration.auction_prolonging_limit_in_seconds || 5.minutes
22
+ end
23
+ end
24
+ end
@@ -10,6 +10,14 @@ module Auctify
10
10
  scope :with_limit, -> { where.not(max_price: nil) }
11
11
 
12
12
  validate :price_is_not_bigger_then_max_price
13
+ validate :price_is_rounded
14
+
15
+ def <=>(other)
16
+ r = (self.price <=> other.price)
17
+ r = (self.created_at <=> other.created_at) if r.zero?
18
+ r = (self.id <=> other.id) if r.zero?
19
+ r
20
+ end
13
21
 
14
22
  def cancel!
15
23
  update!(cancelled: true)
@@ -18,7 +26,11 @@ module Auctify
18
26
  end
19
27
 
20
28
  def with_limit?
21
- max_price.present?
29
+ limit.present?
30
+ end
31
+
32
+ def limit
33
+ max_price
22
34
  end
23
35
 
24
36
  def bade_at
@@ -29,6 +41,12 @@ module Auctify
29
41
  errors.add(:price, :must_be_lower_or_equal_max_price) if max_price && max_price < price
30
42
  end
31
43
 
44
+ def price_is_rounded
45
+ round_to = configuration.require_bids_to_be_rounded_to
46
+ errors.add(:price, :must_be_rounded_to, { round_to: round_to }) if price && (price != round_it_to(price, round_to))
47
+ errors.add(:max_price, :must_be_rounded_to, { round_to: round_to }) if max_price && (max_price != round_it_to(max_price, round_to))
48
+ end
49
+
32
50
  def bidder=(auctified_model)
33
51
  errors.add(:bidder, :not_auctified) unless auctified_model.class.included_modules.include?(Auctify::Behavior::Buyer)
34
52
  raise "There is already registration for this bid!" if registration.present?
@@ -46,6 +64,12 @@ module Auctify
46
64
  def configuration
47
65
  Auctify.configuration
48
66
  end
67
+
68
+ private
69
+ def round_it_to(amount, smallest_amount)
70
+ smallest_amount = smallest_amount.to_i
71
+ (smallest_amount * ((amount + (smallest_amount / 2)).to_i / smallest_amount))
72
+ end
49
73
  end
50
74
  end
51
75
 
@@ -60,6 +84,7 @@ end
60
84
  # created_at :datetime not null
61
85
  # updated_at :datetime not null
62
86
  # cancelled :boolean default(FALSE)
87
+ # autobid :boolean default(FALSE)
63
88
  #
64
89
  # Indexes
65
90
  #
@@ -49,6 +49,25 @@ module Auctify
49
49
 
50
50
  validate :auction_is_in_allowed_state, on: :create
51
51
 
52
+ def fillup_autobid_flags!
53
+ current_limit = 0
54
+ ordered_applied_bids.reverse_each do |bid|
55
+ if bid.with_limit?
56
+ if current_limit < bid.limit
57
+ # increase of limit
58
+ bid.autobid = false
59
+ current_limit = bid.limit
60
+ else
61
+ # same limit, same registration, younger bid => autobid
62
+ bid.autobid = true
63
+ end
64
+ else
65
+ bid.autobid = false
66
+ end
67
+ bid.save!(validate: false)
68
+ end
69
+ end
70
+
52
71
  private
53
72
  def auction_is_in_allowed_state
54
73
  unless auction && auction.allows_new_bidder_registrations?
@@ -60,8 +60,6 @@ module Auctify
60
60
  transitions from: :accepted, to: :in_sale
61
61
 
62
62
  after do
63
- set_bidding_closer_job
64
- set_bidding_is_close_to_end_job
65
63
  after_start_sale
66
64
  end
67
65
  end
@@ -119,6 +117,7 @@ module Auctify
119
117
  validate :forbidden_changes
120
118
 
121
119
  after_create :autoregister_bidders
120
+ after_save :create_jobs
122
121
  before_destroy :forbid_destroy_if_there_are_bids, prepend: true
123
122
 
124
123
  def bidders
@@ -217,9 +216,19 @@ module Auctify
217
216
  bidding_result.current_minimal_bid
218
217
  end
219
218
 
220
- def current_max_price_for(bidder)
221
- last_bidder_mx_bid = ordered_applied_bids.with_limit
222
- .detect { |bid| bid.bidder == bidder }
219
+ def minimal_bid_increase_amount_at(price, respect_first_bid: true)
220
+ return 0 if respect_first_bid && price == opening_price # first bid can equal opening price
221
+ return Auctify.configuration.require_bids_to_be_rounded_to if bid_steps_ladder.blank?
222
+
223
+ _range, increase_step = bid_steps_ladder.detect { |range, step| range.cover?(price) }
224
+ increase_step
225
+ end
226
+
227
+ def current_max_price_for(bidder, bids_array: nil)
228
+ bids_array ||= ordered_applied_bids.with_limit
229
+
230
+ last_bidder_mx_bid = bids_array.detect { |bid| !bid.max_price.nil? && bid.bidder == bidder }
231
+
223
232
  last_bidder_mx_bid.blank? ? 0 : last_bidder_mx_bid.max_price
224
233
  end
225
234
 
@@ -244,9 +253,8 @@ module Auctify
244
253
  applied_bids_count.positive?
245
254
  end
246
255
 
247
- def set_bidding_closer_job
248
- Auctify::BiddingCloserJob.set(wait_until: currently_ends_at)
249
- .perform_later(auction_id: id)
256
+ def auction_prolonging_limit_in_seconds
257
+ pack&.auction_prolonging_limit_in_seconds || Auctify.configuration.auction_prolonging_limit_in_seconds
250
258
  end
251
259
 
252
260
  private
@@ -311,7 +319,7 @@ module Auctify
311
319
  end
312
320
 
313
321
  def extend_end_time(bid_time)
314
- new_end_time = bid_time + Auctify.configuration.auction_prolonging_limit
322
+ new_end_time = bid_time + auction_prolonging_limit_in_seconds
315
323
  self.currently_ends_at = [currently_ends_at, new_end_time].max
316
324
  end
317
325
 
@@ -326,16 +334,17 @@ module Auctify
326
334
  end
327
335
  end
328
336
 
329
- def set_bidding_is_close_to_end_job
337
+ def bidding_is_close_to_end_notification_time
330
338
  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)
339
+ return nil if configured_period.blank?
340
+
341
+ ends_at - configured_period
335
342
  end
336
343
 
337
344
  def forbidden_changes
338
345
  ATTRIBUTES_UNMUTABLE_AT_SOME_STATE.each do |att|
346
+ next if configuration.allow_changes_on_auction_with_bids_for_attributes.include?(att.to_sym)
347
+
339
348
  if changes[att].present? && locked_for_modifications?
340
349
  errors.add(att, :no_modification_allowed_now)
341
350
  write_attribute(att, changes[att].first)
@@ -349,6 +358,20 @@ module Auctify
349
358
  def forbid_destroy_if_there_are_bids
350
359
  errors.add(:base, :you_cannot_delete_auction_with_bids) if bids.any?
351
360
  end
361
+
362
+ def create_jobs(force = false)
363
+ return unless in_sale?
364
+
365
+ currently_ends_at_changes = saved_changes["currently_ends_at"]
366
+
367
+ if force || currently_ends_at_changes.present?
368
+ if (notify_time = bidding_is_close_to_end_notification_time).present?
369
+ # remove_old job is unsupported in ActiveJob
370
+ Auctify::BiddingIsCloseToEndNotifierJob.set(wait_until: notify_time)
371
+ .perform_later(auction_id: id)
372
+ end
373
+ end
374
+ end
352
375
  end
353
376
  end
354
377
  end
@@ -357,37 +380,51 @@ end
357
380
  #
358
381
  # Table name: auctify_sales
359
382
  #
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)
383
+ # id :bigint(8) not null, primary key
384
+ # seller_type :string
385
+ # seller_id :integer
386
+ # buyer_type :string
387
+ # buyer_id :integer
388
+ # item_id :integer not null
389
+ # created_at :datetime not null
390
+ # updated_at :datetime not null
391
+ # type :string default("Auctify::Sale::Base")
392
+ # aasm_state :string default("offered"), not null
393
+ # offered_price :decimal(, )
394
+ # current_price :decimal(, )
395
+ # sold_price :decimal(, )
396
+ # bid_steps_ladder :json
397
+ # reserve_price :decimal(, )
398
+ # pack_id :bigint(8)
399
+ # ends_at :datetime
400
+ # position :integer
401
+ # number :string
402
+ # currently_ends_at :datetime
403
+ # published :boolean default(FALSE)
404
+ # slug :string
405
+ # contract_number :string
406
+ # seller_commission_in_percent :integer
407
+ # winner_type :string
408
+ # winner_id :bigint(8)
409
+ # applied_bids_count :integer default(0)
410
+ # sold_at :datetime
411
+ # current_winner_type :string
412
+ # current_winner_id :bigint(8)
413
+ # buyer_commission_in_percent :integer
414
+ # featured :integer
415
+ #
416
+ # Indexes
417
+ #
418
+ # index_auctify_sales_on_buyer_type_and_buyer_id (buyer_type,buyer_id)
419
+ # index_auctify_sales_on_currently_ends_at (currently_ends_at)
420
+ # index_auctify_sales_on_featured (featured)
421
+ # index_auctify_sales_on_pack_id (pack_id)
422
+ # index_auctify_sales_on_position (position)
423
+ # index_auctify_sales_on_published (published)
424
+ # index_auctify_sales_on_seller_type_and_seller_id (seller_type,seller_id)
425
+ # index_auctify_sales_on_slug (slug) UNIQUE
426
+ # index_auctify_sales_on_winner_type_and_winner_id (winner_type,winner_id)
427
+ #
391
428
 
392
429
  #
393
430
  # Indexes
@@ -4,8 +4,7 @@ module Auctify
4
4
  module Sale
5
5
  class Base < ApplicationRecord
6
6
  include Folio::FriendlyId
7
- include Folio::Positionable
8
- include Folio::Featurable::Basic
7
+ include Folio::Featurable::WithPosition
9
8
  include Folio::Publishable::Basic
10
9
 
11
10
  self.table_name = "auctify_sales"
@@ -33,6 +32,7 @@ module Auctify
33
32
  validate :validate_offered_price_when_published
34
33
 
35
34
  scope :not_sold, -> { where(sold_price: nil) }
35
+ scope :ordered, -> { order(currently_ends_at: :asc, id: :asc) }
36
36
 
37
37
  # need auction scopes here because of has_many :sales, class_name: "Auctify::Sale::Base"
38
38
  scope :auctions_open_for_bids, -> do
@@ -92,10 +92,14 @@ module Auctify
92
92
  save
93
93
  end
94
94
 
95
- def auctioneer_commission
95
+ def auctioneer_commision_from_seller
96
+ (seller_commission_in_percent || 0) * 0.01 * offered_price
97
+ end
98
+
99
+ def auctioneer_commision_from_buyer
96
100
  return nil if sold_price.nil?
97
101
 
98
- percent = commission_in_percent \
102
+ percent = buyer_commission_in_percent \
99
103
  || (pack&.commission_in_percent) \
100
104
  || Auctify.configuration.auctioneer_commission_in_percent
101
105
 
@@ -164,37 +168,38 @@ end
164
168
  #
165
169
  # Table name: auctify_sales
166
170
  #
167
- # id :bigint(8) not null, primary key
168
- # seller_type :string
169
- # seller_id :integer
170
- # buyer_type :string
171
- # buyer_id :integer
172
- # item_id :integer not null
173
- # created_at :datetime not null
174
- # updated_at :datetime not null
175
- # type :string default("Auctify::Sale::Base")
176
- # aasm_state :string default("offered"), not null
177
- # offered_price :decimal(, )
178
- # current_price :decimal(, )
179
- # sold_price :decimal(, )
180
- # bid_steps_ladder :json
181
- # reserve_price :decimal(, )
182
- # pack_id :bigint(8)
183
- # ends_at :datetime
184
- # position :integer
185
- # number :string
186
- # currently_ends_at :datetime
187
- # published :boolean default(FALSE)
188
- # featured :boolean default(FALSE)
189
- # slug :string
190
- # contract_number :string
191
- # commission_in_percent :integer
192
- # winner_type :string
193
- # winner_id :bigint(8)
194
- # applied_bids_count :integer default(0)
195
- # sold_at :datetime
196
- # current_winner_type :string
197
- # current_winner_id :bigint(8)
171
+ # id :bigint(8) not null, primary key
172
+ # seller_type :string
173
+ # seller_id :integer
174
+ # buyer_type :string
175
+ # buyer_id :integer
176
+ # item_id :integer not null
177
+ # created_at :datetime not null
178
+ # updated_at :datetime not null
179
+ # type :string default("Auctify::Sale::Base")
180
+ # aasm_state :string default("offered"), not null
181
+ # offered_price :decimal(, )
182
+ # current_price :decimal(, )
183
+ # sold_price :decimal(, )
184
+ # bid_steps_ladder :json
185
+ # reserve_price :decimal(, )
186
+ # pack_id :bigint(8)
187
+ # ends_at :datetime
188
+ # position :integer
189
+ # number :string
190
+ # currently_ends_at :datetime
191
+ # published :boolean default(FALSE)
192
+ # slug :string
193
+ # contract_number :string
194
+ # seller_commission_in_percent :integer
195
+ # winner_type :string
196
+ # winner_id :bigint(8)
197
+ # applied_bids_count :integer default(0)
198
+ # sold_at :datetime
199
+ # current_winner_type :string
200
+ # current_winner_id :bigint(8)
201
+ # buyer_commission_in_percent :integer
202
+ # featured :integer
198
203
  #
199
204
  # Indexes
200
205
  #
@@ -23,6 +23,12 @@ module Auctify
23
23
  end
24
24
 
25
25
  event :start_sale do
26
+ before do
27
+ self.current_price = self.offered_price
28
+ self.currently_ends_at = self.ends_at
29
+ self.buyer = nil
30
+ end
31
+
26
32
  transitions from: :accepted, to: :in_sale
27
33
  end
28
34
 
@@ -51,37 +57,38 @@ end
51
57
  #
52
58
  # Table name: auctify_sales
53
59
  #
54
- # id :bigint(8) not null, primary key
55
- # seller_type :string
56
- # seller_id :integer
57
- # buyer_type :string
58
- # buyer_id :integer
59
- # item_id :integer not null
60
- # created_at :datetime not null
61
- # updated_at :datetime not null
62
- # type :string default("Auctify::Sale::Base")
63
- # aasm_state :string default("offered"), not null
64
- # offered_price :decimal(, )
65
- # current_price :decimal(, )
66
- # sold_price :decimal(, )
67
- # bid_steps_ladder :json
68
- # reserve_price :decimal(, )
69
- # pack_id :bigint(8)
70
- # ends_at :datetime
71
- # position :integer
72
- # number :string
73
- # currently_ends_at :datetime
74
- # published :boolean default(FALSE)
75
- # featured :boolean default(FALSE)
76
- # slug :string
77
- # contract_number :string
78
- # commission_in_percent :integer
79
- # winner_type :string
80
- # winner_id :bigint(8)
81
- # applied_bids_count :integer default(0)
82
- # sold_at :datetime
83
- # current_winner_type :string
84
- # current_winner_id :bigint(8)
60
+ # id :bigint(8) not null, primary key
61
+ # seller_type :string
62
+ # seller_id :integer
63
+ # buyer_type :string
64
+ # buyer_id :integer
65
+ # item_id :integer not null
66
+ # created_at :datetime not null
67
+ # updated_at :datetime not null
68
+ # type :string default("Auctify::Sale::Base")
69
+ # aasm_state :string default("offered"), not null
70
+ # offered_price :decimal(, )
71
+ # current_price :decimal(, )
72
+ # sold_price :decimal(, )
73
+ # bid_steps_ladder :json
74
+ # reserve_price :decimal(, )
75
+ # pack_id :bigint(8)
76
+ # ends_at :datetime
77
+ # position :integer
78
+ # number :string
79
+ # currently_ends_at :datetime
80
+ # published :boolean default(FALSE)
81
+ # slug :string
82
+ # contract_number :string
83
+ # seller_commission_in_percent :integer
84
+ # winner_type :string
85
+ # winner_id :bigint(8)
86
+ # applied_bids_count :integer default(0)
87
+ # sold_at :datetime
88
+ # current_winner_type :string
89
+ # current_winner_id :bigint(8)
90
+ # buyer_commission_in_percent :integer
91
+ # featured :integer
85
92
  #
86
93
  # Indexes
87
94
  #
@@ -29,6 +29,7 @@ module Auctify
29
29
  numericality: { greater_than_or_equal: 0, less_than: 60 }
30
30
 
31
31
  validate :validate_start_and_end_dates
32
+ validate :sales_ends_in_pack_time_frame
32
33
 
33
34
  scope :ordered, -> { order(start_date: :desc, id: :desc) }
34
35
 
@@ -44,17 +45,40 @@ module Auctify
44
45
  end
45
46
 
46
47
  def dates_to_label
47
- if start_date && end_date
48
- if start_date.year == end_date.year
49
- if start_date.month == end_date.month
50
- "#{start_date.strftime('%-d.')}–#{end_date.strftime('%-d. %-m. %y')}"
51
- else
52
- "#{start_date.strftime('%-d. %-m.')} #{end_date.strftime('%-d. %-m. %y')}"
53
- end
48
+ return "" unless start_date && end_date
49
+
50
+ date_strings = []
51
+ if start_date.year == end_date.year
52
+ if start_date.month == end_date.month
53
+ # all inside same month
54
+ date_strings << start_date.strftime("%-d.")
54
55
  else
55
- "#{start_date.strftime('%-d. %-m. %y')} #{end_date.strftime('%-d. %-m. %y')}"
56
+ # all inside same year
57
+ date_strings << start_date.strftime("%-d. %-m.")
58
+ end
59
+ else
60
+ # crossing years border
61
+ date_strings << start_date.strftime("%-d. %-m. %Y")
62
+ end
63
+
64
+ date_strings << end_date.strftime("%-d. %-m. %Y")
65
+ date_strings.join(" – ")
66
+ end
67
+
68
+ def shift_sales_by_minutes!(shift_in_minutes)
69
+ self.transaction do
70
+ sales.each do |sale|
71
+ sale.update!(ends_at: sale.ends_at + shift_in_minutes.minutes)
72
+
73
+ validate_sale_ends_in_time_frame(sale)
74
+ raise ActiveRecord::RecordInvalid if errors[:sales].present?
56
75
  end
57
76
  end
77
+ sales.reload
78
+ end
79
+
80
+ def time_frame
81
+ (start_date.to_time..(end_date.to_time + 1.day))
58
82
  end
59
83
 
60
84
  private
@@ -64,11 +88,28 @@ module Auctify
64
88
  end
65
89
  end
66
90
 
91
+ def sales_ends_in_pack_time_frame
92
+ return if changes["start_date"].present? || changes["end_date"]
93
+
94
+ sales.select(:id, :slug, :ends_at).each do |sale|
95
+ validate_sale_ends_in_time_frame(sale)
96
+ end
97
+ end
98
+
67
99
  def set_commission
68
100
  return if self.commission_in_percent.present?
69
101
 
70
102
  self.commission_in_percent = Auctify.configuration.auctioneer_commission_in_percent
71
103
  end
104
+
105
+ def validate_sale_ends_in_time_frame(sale)
106
+ unless time_frame.cover?(sale.ends_at)
107
+ errors.add(:sales,
108
+ :sale_is_out_of_time_frame,
109
+ slug: sale.slug.blank? ? "##{sale.id}" : sale.slug,
110
+ ends_at_time: I18n.l(sale.ends_at))
111
+ end
112
+ end
72
113
  end
73
114
  end
74
115
 
@@ -76,22 +117,23 @@ end
76
117
  #
77
118
  # Table name: auctify_sales_packs
78
119
  #
79
- # id :bigint(8) not null, primary key
80
- # title :string
81
- # description :text
82
- # position :integer default(0)
83
- # slug :string
84
- # place :string
85
- # published :boolean default(FALSE)
86
- # created_at :datetime not null
87
- # updated_at :datetime not null
88
- # sales_count :integer default(0)
89
- # start_date :date
90
- # end_date :date
91
- # sales_interval :integer default(3)
92
- # sales_beginning_hour :integer default(20)
93
- # sales_beginning_minutes :integer default(0)
94
- # commission_in_percent :integer
120
+ # id :bigint(8) not null, primary key
121
+ # title :string
122
+ # description :text
123
+ # position :integer default(0)
124
+ # slug :string
125
+ # place :string
126
+ # published :boolean default(FALSE)
127
+ # created_at :datetime not null
128
+ # updated_at :datetime not null
129
+ # sales_count :integer default(0)
130
+ # start_date :date
131
+ # end_date :date
132
+ # sales_interval :integer default(3)
133
+ # sales_beginning_hour :integer default(20)
134
+ # sales_beginning_minutes :integer default(0)
135
+ # commission_in_percent :integer
136
+ # auction_prolonging_limit_in_seconds :integer
95
137
  #
96
138
  # Indexes
97
139
  #
@@ -46,8 +46,8 @@ module Auctify
46
46
  def set_price_for_limit_bid
47
47
  return unless bid.price.blank? && bid.with_limit?
48
48
 
49
- if changing_own_limit?
50
- bid.price = winning_bid.price
49
+ if changing_own_limit_when_winning?
50
+ bid.price = [bid.max_price, winning_bid.price].min
51
51
  elsif bid.max_price <= new_current_minimal_bid
52
52
  bid.price = bid.max_price
53
53
  else
@@ -57,8 +57,10 @@ module Auctify
57
57
 
58
58
  def approved_bid?
59
59
  @approved_bid ||= begin
60
+ bid.valid?
60
61
  check_bidder
61
- changing_own_limit? ? check_max_price_increasing : check_price_minimum
62
+ check_max_price_increasing
63
+ check_price_minimum
62
64
  check_same_bidder
63
65
  check_auction_state
64
66
 
@@ -94,7 +96,6 @@ module Auctify
94
96
 
95
97
  def new_current_minimal_bid
96
98
  return current_price if first_bid?
97
-
98
99
  increase_price(current_price)
99
100
  end
100
101
 
@@ -114,7 +115,7 @@ module Auctify
114
115
  end
115
116
 
116
117
  def first_bid?
117
- bids.empty?
118
+ winning_bid.nil?
118
119
  end
119
120
 
120
121
  def bids
@@ -131,11 +132,14 @@ module Auctify
131
132
  end
132
133
 
133
134
  def check_max_price_increasing
134
- bid.errors.add(:bidder, :you_can_only_increase_your_max_price) if bid.max_price.present? && (bid.max_price.to_i <= winning_bid.max_price.to_i)
135
+ return unless changing_own_limit_when_winning?
136
+
137
+ bid.errors.add(:bidder, :you_can_only_increase_your_max_price) if bid.max_price.to_i <= winning_bid.max_price.to_i
135
138
  end
136
139
 
137
140
  def check_price_minimum
138
- if bid.price < new_current_minimal_bid
141
+ minimum = changing_own_limit_when_winning? ? current_price : new_current_minimal_bid
142
+ if bid.price < minimum
139
143
  att = bid.with_limit? ? :max_price : :price
140
144
  bid.errors.add(att,
141
145
  :price_is_bellow_minimal_bid,
@@ -144,7 +148,7 @@ module Auctify
144
148
  end
145
149
 
146
150
  def check_same_bidder
147
- return if overbidding_yourself_allowed? || changing_own_limit?
151
+ return if overbidding_yourself_allowed? || changing_own_limit_when_winning?
148
152
 
149
153
  if winning_bid.present? && same_bidder?(winning_bid, bid)
150
154
  bid.errors.add(:bidder, :you_cannot_overbid_yourself)
@@ -159,7 +163,7 @@ module Auctify
159
163
  end
160
164
 
161
165
  def solve_winner(winning_bid, new_bid)
162
- return if winning_bid.blank? || changing_own_limit?
166
+ return if winning_bid.blank? || changing_own_limit_when_winning?
163
167
 
164
168
  solve_limits_fight(winning_bid, new_bid) if new_bid.with_limit? && winning_bid.with_limit?
165
169
  increase_bid_price(winning_bid, new_bid) if new_bid.with_limit? && !winning_bid.with_limit?
@@ -201,13 +205,11 @@ module Auctify
201
205
 
202
206
  @updated_win_bid = winning_bid.dup
203
207
  @updated_win_bid.price = [price, winning_bid.max_price].min
208
+ @updated_win_bid.autobid = true
204
209
  end
205
210
 
206
211
  def increase_price(price)
207
- return price + 1 if bid_steps_ladder.blank?
208
-
209
- _range, increase_step = bid_steps_ladder.detect { |range, step| range.cover?(price) }
210
- price + increase_step
212
+ price + auction.minimal_bid_increase_amount_at(price, respect_first_bid: false)
211
213
  end
212
214
 
213
215
  def increase_price_to(overcome:, ceil:)
@@ -222,11 +224,7 @@ module Auctify
222
224
  [running_price, ceil].min
223
225
  end
224
226
 
225
- def bid_steps_ladder
226
- @bid_steps_ladder ||= auction.bid_steps_ladder
227
- end
228
-
229
- def changing_own_limit?
227
+ def changing_own_limit_when_winning?
230
228
  bid.with_limit? && winning_bid.present? && (winning_bid.bidder == bid.bidder)
231
229
  end
232
230
 
@@ -34,14 +34,17 @@ cs:
34
34
  published: Zveřejněno
35
35
  sales_count: Počet položek
36
36
  sales: Položky
37
+ items: Předměty
37
38
  sales_interval: Časový rozestup mezi předměty v minutách
38
39
  sales_beginning_hour: Čas prvního předmětu (hodina)
39
40
  sales_beginning_minutes: Čas prvního předmětu (minuty)
40
41
  commission_in_percent: Provize aukční síně (procenta)
42
+ auction_prolonging_limit_in_seconds: Časový limit pro prodloužení aukce (sekundy)
41
43
  auctify/sale/base:
42
44
  buyer: Kupec
43
45
  seller: Prodejce
44
- item: Zboží
46
+ item: Předmět
47
+ pack: Prodejní balík
45
48
  aasm_state: Stav
46
49
  published: Zveřejněno
47
50
  number: Číslo
@@ -58,14 +61,25 @@ cs:
58
61
  updated_at: Změněna
59
62
  reserve_price: Rezervovaná cena
60
63
  ends_at: Předpokládaný konec
61
- pack_id: Aukce+
62
- pack: Aukce-
64
+ pack_id: Aukce
65
+ pack: Aukce
66
+ bidder_registrations: Registrace do aukce položky
67
+ bidders: Dražitelé
68
+ bids: Příhozy
69
+ winner: Vítěz dražby
70
+ current_winner: Aktuální výherce
63
71
  auctify/bid:
64
72
  price: Výše příhozu
65
- max_price: Maximální výše příhozu
73
+ max_price: Limit příhozů
66
74
  created_at: Přihozeno
67
75
  updated_at:
68
76
  registration: Registrace do aukce položky
77
+ auction: Položka aukce
78
+ bidder: Dražitel
79
+ auctify/bidder_registration:
80
+ bidder: Dražitel
81
+ auction: Položka aukce
82
+ bids: Příhozy
69
83
 
70
84
  errors:
71
85
  models:
@@ -98,6 +112,8 @@ cs:
98
112
  attributes:
99
113
  end_date:
100
114
  smaller_than_start_date: "musí být později než začátek"
115
+ sales:
116
+ sale_is_out_of_time_frame: "Položka '%{slug}' má čas konce (%{ends_at_time}) mimo rámec aukce"
101
117
  auctify/bid:
102
118
  not_confirmed: "Příhoz je potřeba potvrdit"
103
119
  attributes:
@@ -113,8 +129,10 @@ cs:
113
129
  price_is_bellow_opening_price: "je nižší než vyvolávací cena"
114
130
  price_is_bellow_minimal_bid: "je nižší než aktuální minimální příhoz %{minimal_bid}"
115
131
  must_be_lower_or_equal_max_price: "musí být nižší nebo rovna maximálnímu limitu"
132
+ must_be_rounded_to: "musí být zaokrouhlená na celé %{round_to} Kč"
116
133
  max_price:
117
134
  price_is_bellow_minimal_bid: "je nižší než aktuální minimální příhoz %{minimal_bid}"
135
+ must_be_rounded_to: "musí být zaokrouhlená na celé %{round_to} Kč"
118
136
  auctify/bidder_registration:
119
137
  attributes:
120
138
  auction:
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ class ChangeCommissionColumns < ActiveRecord::Migration[6.1]
4
+ def change
5
+ rename_column :auctify_sales, :commission_in_percent, :seller_commission_in_percent
6
+ add_column :auctify_sales, :buyer_commission_in_percent, :integer
7
+ end
8
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ class AddAuctionProlongingLimitToSalesPacks < ActiveRecord::Migration[6.1]
4
+ def change
5
+ add_column :auctify_sales_packs, :auction_prolonging_limit_in_seconds, :integer
6
+ end
7
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ class AddAutobidToAuctifyBids < ActiveRecord::Migration[6.1]
4
+ def up
5
+ add_column :auctify_bids, :autobid, :boolean, default: false
6
+ puts("RUN `Auctify::AutobidFillerJob.perform_later`")
7
+ end
8
+
9
+ def down
10
+ remove_column :auctify_bids, :autobid
11
+ end
12
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ class UpdateSalesFeaturable < ActiveRecord::Migration[6.1]
4
+ def up
5
+ rename_column :auctify_sales, :featured, :featured_tmp_boolean
6
+
7
+ add_column :auctify_sales, :featured, :integer, default: nil
8
+ add_index :auctify_sales, :featured
9
+
10
+ execute("UPDATE auctify_sales SET featured = 1 WHERE featured_tmp_boolean = TRUE;")
11
+
12
+ remove_column :auctify_sales, :featured_tmp_boolean
13
+ end
14
+
15
+ def down
16
+ rename_column :auctify_sales, :featured, :featured_tmp_integer
17
+
18
+ add_column :auctify_sales, :featured, :boolean, default: false
19
+ add_index :auctify_sales, :featured
20
+
21
+ execute("UPDATE auctify_sales SET featured = TRUE WHERE featured_tmp_integer IS NOT NULL;")
22
+
23
+ remove_column :auctify_sales, :featured_tmp_integer
24
+ end
25
+ end
@@ -3,23 +3,27 @@
3
3
  module Auctify
4
4
  class Configuration
5
5
  attr_accessor :autoregister_as_bidders_all_instances_of_classes,
6
- :auction_prolonging_limit,
6
+ :auction_prolonging_limit_in_seconds,
7
7
  :auctioneer_commission_in_percent,
8
8
  :autofinish_auction_after_bidding,
9
9
  :when_to_notify_bidders_before_end_of_bidding,
10
10
  :default_bid_steps_ladder,
11
- :restrict_overbidding_yourself_to_max_price_increasing
11
+ :restrict_overbidding_yourself_to_max_price_increasing,
12
+ :require_bids_to_be_rounded_to,
13
+ :allow_changes_on_auction_with_bids_for_attributes
12
14
 
13
15
 
14
16
  def initialize
15
17
  # set defaults here
16
18
  @autoregister_as_bidders_all_instances_of_classes = []
17
- @auction_prolonging_limit = 2.minutes
19
+ @auction_prolonging_limit_in_seconds = 2.minutes
18
20
  @auctioneer_commission_in_percent = 1 # %
19
21
  @autofinish_auction_after_bidding = false
20
22
  @when_to_notify_bidders_before_end_of_bidding = nil # no notifying
21
23
  @default_bid_steps_ladder = { 0.. => 1 }
22
24
  @restrict_overbidding_yourself_to_max_price_increasing = true
25
+ @require_bids_to_be_rounded_to = 1
26
+ @allow_changes_on_auction_with_bids_for_attributes = []
23
27
  end
24
28
 
25
29
  def autoregistering_for?(instance)
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "aasm"
4
+ require "with_advisory_lock"
4
5
  require_relative "../../app/models/auctify/behaviors"
5
6
 
6
7
  module Auctify
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Auctify
4
- VERSION = "1.0.0"
4
+ VERSION = "1.1.2"
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: auctify
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.0
4
+ version: 1.1.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - foton
8
- autorequire:
8
+ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2021-06-28 00:00:00.000000000 Z
11
+ date: 2022-04-19 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails
@@ -16,20 +16,14 @@ dependencies:
16
16
  requirements:
17
17
  - - "~>"
18
18
  - !ruby/object:Gem::Version
19
- version: 6.0.3
20
- - - ">="
21
- - !ruby/object:Gem::Version
22
- version: 6.0.3.7
19
+ version: 6.1.4
23
20
  type: :runtime
24
21
  prerelease: false
25
22
  version_requirements: !ruby/object:Gem::Requirement
26
23
  requirements:
27
24
  - - "~>"
28
25
  - !ruby/object:Gem::Version
29
- version: 6.0.3
30
- - - ">="
31
- - !ruby/object:Gem::Version
32
- version: 6.0.3.7
26
+ version: 6.1.4
33
27
  - !ruby/object:Gem::Dependency
34
28
  name: aasm
35
29
  requirement: !ruby/object:Gem::Requirement
@@ -58,6 +52,20 @@ dependencies:
58
52
  - - ">="
59
53
  - !ruby/object:Gem::Version
60
54
  version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: with_advisory_lock
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
61
69
  - !ruby/object:Gem::Dependency
62
70
  name: pg
63
71
  requirement: !ruby/object:Gem::Requirement
@@ -272,16 +280,16 @@ dependencies:
272
280
  name: selenium-webdriver
273
281
  requirement: !ruby/object:Gem::Requirement
274
282
  requirements:
275
- - - ">="
283
+ - - "~>"
276
284
  - !ruby/object:Gem::Version
277
- version: '0'
285
+ version: 3.142.7
278
286
  type: :development
279
287
  prerelease: false
280
288
  version_requirements: !ruby/object:Gem::Requirement
281
289
  requirements:
282
- - - ">="
290
+ - - "~>"
283
291
  - !ruby/object:Gem::Version
284
- version: '0'
292
+ version: 3.142.7
285
293
  - !ruby/object:Gem::Dependency
286
294
  name: webdrivers
287
295
  requirement: !ruby/object:Gem::Requirement
@@ -335,8 +343,10 @@ files:
335
343
  - app/helpers/auctify/sales_helper.rb
336
344
  - app/helpers/auctify/sales_packs_helper.rb
337
345
  - app/jobs/auctify/application_job.rb
346
+ - app/jobs/auctify/autobid_filler_job.rb
338
347
  - app/jobs/auctify/bidding_closer_job.rb
339
348
  - app/jobs/auctify/bidding_is_close_to_end_notifier_job.rb
349
+ - app/jobs/auctify/ensure_auctions_closing_job.rb
340
350
  - app/mailers/auctify/application_mailer.rb
341
351
  - app/models/auctify/application_record.rb
342
352
  - app/models/auctify/behaviors.rb
@@ -411,6 +421,10 @@ files:
411
421
  - db/migrate/20210607113440_add_commission_in_percent_to_sales_packs.rb
412
422
  - db/migrate/20210617062509_add_index_to_auctify_sales_currently_ends_at.rb
413
423
  - db/migrate/20210625125732_add_current_winner_to_auctify_sales.rb
424
+ - db/migrate/20210825155543_change_commission_columns.rb
425
+ - db/migrate/20210917082313_add_auction_prolonging_limit_to_sales_packs.rb
426
+ - db/migrate/20211022133253_add_autobid_to_auctify_bids.rb
427
+ - db/migrate/20211203064934_update_sales_featurable.rb
414
428
  - lib/auctify.rb
415
429
  - lib/auctify/configuration.rb
416
430
  - lib/auctify/engine.rb
@@ -420,7 +434,7 @@ homepage: https://github.com/sinfin/auctify
420
434
  licenses:
421
435
  - MIT
422
436
  metadata: {}
423
- post_install_message:
437
+ post_install_message:
424
438
  rdoc_options: []
425
439
  require_paths:
426
440
  - lib
@@ -435,8 +449,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
435
449
  - !ruby/object:Gem::Version
436
450
  version: '0'
437
451
  requirements: []
438
- rubygems_version: 3.2.11
439
- signing_key:
452
+ rubygems_version: 3.2.28
453
+ signing_key:
440
454
  specification_version: 4
441
455
  summary: Gem for adding auction behavior to models.
442
456
  test_files: []