Module: SpreeCmCommissioner::LineItemDecorator

Defined in:
app/models/spree_cm_commissioner/line_item_decorator.rb

Class Method Summary collapse

Instance Method Summary collapse

Class Method Details

.include_modules(base) ⇒ Object



82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
# File 'app/models/spree_cm_commissioner/line_item_decorator.rb', line 82

def self.include_modules(base)
  base.include Spree::Core::NumberGenerator.new(prefix: 'L')
  base.include SpreeCmCommissioner::StoreMetadata
  base.include SpreeCmCommissioner::LineItemIntegration
  base.include SpreeCmCommissioner::LineItemDurationable
  base.include SpreeCmCommissioner::LineItemsFilterScope
  base.include SpreeCmCommissioner::LineItemGuestsConcern
  base.include SpreeCmCommissioner::LineItemTransitable
  base.include SpreeCmCommissioner::LineItemSeatSelection
  base.include SpreeCmCommissioner::Integrations::IntegrationMappable
  base.include SpreeCmCommissioner::ProductType
  base.include SpreeCmCommissioner::ProductDelegation
  base.include SpreeCmCommissioner::LineItemOpenDatedTrippable
  base.include SpreeCmCommissioner::KycBitwise
end

.pending_guests_queryObject



102
103
104
105
106
107
# File 'app/models/spree_cm_commissioner/line_item_decorator.rb', line 102

def self.pending_guests_query
  lambda {
    left_outer_joins(:id_card)
      .where(upload_later: true, id_card: { front_image: nil })
  }
end

.prepended(base) ⇒ Object

rubocop:disable Metrics/MethodLength,Metrics/AbcSize



3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
# File 'app/models/spree_cm_commissioner/line_item_decorator.rb', line 3

def self.prepended(base) # rubocop:disable Metrics/MethodLength,Metrics/AbcSize
  include_modules(base)

  base.belongs_to :accepter, class_name: 'Spree::User', optional: true
  base.belongs_to :rejecter, class_name: 'Spree::User', optional: true
  base.belongs_to :event, class_name: 'Spree::Taxon', optional: true, inverse_of: :line_items

  base.has_one :google_wallet, class_name: 'SpreeCmCommissioner::GoogleWallet', through: :product

  base.has_many :option_types, through: :product

  base.has_many :inventory_items, lambda { |line_item|
    where(inventory_date: nil).or(where(inventory_date: line_item.date_range))
  }, through: :variant

  base.has_many :taxons, class_name: 'Spree::Taxon', through: :product

  base.has_many :guests, class_name: 'SpreeCmCommissioner::Guest', dependent: :destroy
  base.has_many :guests_with_blocks, -> { where.not(block_id: nil) }, class_name: 'SpreeCmCommissioner::Guest'
  base.has_many :reserved_blocks, through: :guests, class_name: 'SpreeCmCommissioner::ReservedBlock'
  base.has_many :pending_guests, pending_guests_query, class_name: 'SpreeCmCommissioner::Guest', dependent: :destroy
  base.has_many :product_completion_steps, class_name: 'SpreeCmCommissioner::ProductCompletionStep', through: :product

  base.has_many :saved_guests,
                through: :guests,
                source: :saved_guest,
                class_name: 'SpreeCmCommissioner::SavedGuest'

  base.before_validation -> { self.product_type = variant.product_type || product.product_type }, if: -> { product_type.nil? }

  base.before_save :update_vendor_id

  base.before_create :add_due_date, if: :subscription?
  base.validate :ensure_not_exceed_purchase_limits, if: -> { product.present? }
  base.validate :ensure_all_guests_have_blocks, if: :required_seat_selection?

  base.whitelisted_ransackable_associations |= %w[guests order]
  base.whitelisted_ransackable_attributes |= %w[number to_date from_date vendor_id]

  base.delegate :delivery_required?, :high_demand,
                to: :variant

  base. :completion_steps, :array

  # Set by DraftOrders::Create from the verified PrivateSale, never from params — the client
  # cannot ask for locked stock, the server decides which lines the token covers. Persisted
  # with the line item, so the same value answers AvailabilityValidator on this save and
  # Restock on a cancellation months later.
  base. :locked_stock, :boolean, default: false

  # Which sale unlocked this line's stock — nil when the line drew from public stock, even in
  # a mixed cart. Set alongside locked_stock, from the same verified PrivateSale.
  base. :private_sale_id, :integer

  base.accepts_nested_attributes_for :guests, allow_destroy: true

  def base.json_api_columns
    json_api_columns = column_names.reject { |c| c.match(/_id$|id|preferences|(.*)password|(.*)token|(.*)api_key/) }
    json_api_columns << :options_text
    json_api_columns << :vendor_id
  end

  def discontinue_on
    variant.discontinue_on || product.discontinue_on
  end

  def base.search_by_qr_data!(data)
    matches = data.match(/(R\d+)-([A-Za-z0-9_\-]+)-(L\d+)/)&.captures

    raise ActiveRecord::RecordNotFound, "Couldn't find Spree::LineItem with QR data: #{data}" unless matches

    order_number, order_token, line_item_id = matches
    line_item_id = line_item_id.delete('L').to_i

    Spree::LineItem.joins(:order)
                   .find_by(id: line_item_id, spree_orders: { number: order_number, token: order_token })
  end
end

Instance Method Details

#accepted?Boolean

Returns:

  • (Boolean)


174
175
176
# File 'app/models/spree_cm_commissioner/line_item_decorator.rb', line 174

def accepted?
  accepted_at.present?
end

#accepted_by(user) ⇒ Object



182
183
184
185
186
187
188
189
# File 'app/models/spree_cm_commissioner/line_item_decorator.rb', line 182

def accepted_by(user)
  return if accepted_at.present? && accepter_id.present?

  update(
    accepted_at: Time.current,
    accepter: user
  )
end

#allowed_self_check_in?Boolean

Returns:

  • (Boolean)


148
149
150
# File 'app/models/spree_cm_commissioner/line_item_decorator.rb', line 148

def allowed_self_check_in?
  allow_self_check_in?
end

#amount_per_guestObject



166
167
168
# File 'app/models/spree_cm_commissioner/line_item_decorator.rb', line 166

def amount_per_guest
  amount / quantity
end

#claim_scopeObject

Returns { claimable_id:, claimable_type: } or nil if unresolvable. Scope is determined by the Show's free_vote_limit_type from voting_config.



293
294
295
296
297
298
299
300
# File 'app/models/spree_cm_commissioner/line_item_decorator.rb', line 293

def claim_scope
  result = SpreeCmCommissioner::VotingCredits::ClaimableScopeResolver.new(
    product: product,
    voting_session_id: &.dig('voting_session_id')
  ).call

  result.success? ? result.value : nil
end

#commission_rateObject



170
171
172
# File 'app/models/spree_cm_commissioner/line_item_decorator.rb', line 170

def commission_rate
  product.commission_rate || vendor&.commission_rate || 0
end

#discontinue_onObject



65
66
67
# File 'app/models/spree_cm_commissioner/line_item_decorator.rb', line 65

def discontinue_on
  variant.discontinue_on || product.discontinue_on
end

#external_qr_dataObject

QR data for check-in. If external_wins? is true, use their QR data (they have their own check-in system). Otherwise, use our system's QR data. Only applicable to models with QR support (e.g., line_item, guest).



241
242
243
244
245
246
# File 'app/models/spree_cm_commissioner/line_item_decorator.rb', line 241

def external_qr_data
  return nil unless integration?

  mapping = external_wins_integration_mappings.first
  mapping.external_qr_data if mapping.present?
end

#generate_completion_stepsObject



248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
# File 'app/models/spree_cm_commissioner/line_item_decorator.rb', line 248

def generate_completion_steps
  generate_completion_steps!
  true
rescue StandardError => e
  CmAppLogger.error(
    label: "#{self.class.name}#generate_completion_steps",
    data: {
      line_item_id: id,
      order_id: order_id,
      error_class: e.class.name,
      error_message: e.message,
      backtrace: e.backtrace&.first(10)&.join("\n")
    }
  )
  errors.add(:completion_steps, e.message)
  false
end

#generate_completion_steps!Object



266
267
268
269
270
271
# File 'app/models/spree_cm_commissioner/line_item_decorator.rb', line 266

def generate_completion_steps!
  self.completion_steps = product_completion_steps.map do |completion_step|
    completion_step.construct_hash(line_item: self)
  end
  save!
end

#generate_png_qr(size = 120) ⇒ Object



212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
# File 'app/models/spree_cm_commissioner/line_item_decorator.rb', line 212

def generate_png_qr(size = 120)
  qrcode = RQRCode::QRCode.new(qr_data)
  qrcode.as_png(
    bit_depth: 1,
    border_modules: 1,
    color_mode: ChunkyPNG::COLOR_GRAYSCALE,
    color: 'black',
    file: nil,
    fill: 'white',
    module_px_size: 4,
    resize_exactly_to: false,
    resize_gte_to: false,
    size: size
  )
end

#generate_svg_qrObject



200
201
202
203
204
205
206
207
208
209
210
# File 'app/models/spree_cm_commissioner/line_item_decorator.rb', line 200

def generate_svg_qr
  qrcode = RQRCode::QRCode.new(qr_data)
  qrcode.as_svg(
    color: '000',
    shape_rendering: 'crispEdges',
    module_size: 5,
    standalone: true,
    use_path: true,
    viewbox: '0 0 20 10'
  )
end

#internal_qr_dataObject



235
236
237
# File 'app/models/spree_cm_commissioner/line_item_decorator.rb', line 235

def internal_qr_data
  "#{order.number}-#{order.token}-L#{id}"
end

#jwt_tokenObject



286
287
288
289
# File 'app/models/spree_cm_commissioner/line_item_decorator.rb', line 286

def jwt_token
  payload = { order_number: order.number, line_item_id: id }
  SpreeCmCommissioner::OrderJwtToken.encode(payload, order.token)
end

#kycObject



98
99
100
# File 'app/models/spree_cm_commissioner/line_item_decorator.rb', line 98

def kyc
  variant.kyc || product.kyc
end

#monthObject



282
283
284
# File 'app/models/spree_cm_commissioner/line_item_decorator.rb', line 282

def month
  to_date.strftime('%B %Y')
end

#qr_dataObject



228
229
230
231
232
233
# File 'app/models/spree_cm_commissioner/line_item_decorator.rb', line 228

def qr_data
  return external_qr_data if external_qr_data.present?
  return nil if order.nil?

  internal_qr_data
end

#rejected?Boolean

Returns:

  • (Boolean)


178
179
180
# File 'app/models/spree_cm_commissioner/line_item_decorator.rb', line 178

def rejected?
  rejected_at.present?
end

#rejected_by(user) ⇒ Object



191
192
193
194
195
196
197
198
# File 'app/models/spree_cm_commissioner/line_item_decorator.rb', line 191

def rejected_by(user)
  return if rejected_at.present? && rejecter_id.present?

  update(
    rejected_at: Time.current,
    rejecter: user
  )
end

#required_self_check_in_location?Boolean

Returns:

  • (Boolean)


152
153
154
# File 'app/models/spree_cm_commissioner/line_item_decorator.rb', line 152

def required_self_check_in_location?
  required_self_check_in_location
end

#should_hold_inventory?Boolean

Returns:

  • (Boolean)


156
157
158
159
160
161
162
163
164
# File 'app/models/spree_cm_commissioner/line_item_decorator.rb', line 156

def should_hold_inventory?
  return false unless variant.should_track_inventory?
  return false if required_seat_selection?

  return true if ENV.fetch('INVENTORY_HOLD_ENABLED', nil) == 'true'
  return true if product.enable_inventory_hold?

  false
end

#sufficient_stock?Boolean

override Ticket transfer line items reuse the seller's already-committed inventory, so stock checks must not block the receiver's checkout when the event is sold out.

Returns:

  • (Boolean)


276
277
278
279
280
# File 'app/models/spree_cm_commissioner/line_item_decorator.rb', line 276

def sufficient_stock?
  return true if order&.ticket_transfer?

  SpreeCmCommissioner::Stock::LineItemAvailabilityChecker.new(self).checker.can_supply?(quantity)
end

#update_priceObject

override to calculate price per date for permanent stock



132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
# File 'app/models/spree_cm_commissioner/line_item_decorator.rb', line 132

def update_price
  # Skip price updates for ticket transfer orders to preserve custom transfer pricing
  return if order&.ticket_transfer?
  return self.price = 0 if eligible_for_free_claim?

  base_price = variant.price_in(order.currency)
  self.price = if permanent_stock?
                 date_range.map do |date|
                   selected_price = variant.price_for_date(date: date, currency: order.currency) || base_price
                   selected_price.amount || 0
                 end.sum
               else
                 base_price.amount || 0
               end
end

#update_price_from_modifier(currency, opts) ⇒ Object

override to calculate price per date for permanent stock



110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
# File 'app/models/spree_cm_commissioner/line_item_decorator.rb', line 110

def update_price_from_modifier(currency, opts)
  self.currency = currency

  # these fields can be null during this process below.
  # it is needed for permanent_stock?, date_range to work.
  self.product_type = variant.product_type
  self.from_date = opts.delete(:from_date)&.to_datetime
  self.to_date = opts.delete(:to_date)&.to_datetime

  base_price = variant.price_in(currency)

  self.price = if permanent_stock?
                 date_range.map do |date|
                   selected_price = variant.price_for_date(date: date, currency: currency) || base_price
                   (selected_price.amount || 0) + variant.price_modifier_amount_in(currency, opts.merge(date: date))
                 end.sum
               else
                 (base_price.amount || 0) + variant.price_modifier_amount_in(currency, opts)
               end
end