Module: SpreeCmCommissioner::OrderStateMachine

Extended by:
ActiveSupport::Concern
Defined in:
app/models/concerns/spree_cm_commissioner/order_state_machine.rb

Overview

rubocop:disable Metrics/ModuleLength

Instance Method Summary collapse

Instance Method Details

#accepted_by(user) ⇒ Object

allow authorized user to accept all requested line items



144
145
146
147
148
149
150
151
152
# File 'app/models/concerns/spree_cm_commissioner/order_state_machine.rb', line 144

def accepted_by(user)
  transaction do
    accept!

    line_items.each do |line_item|
      line_item.accepted_by(user)
    end
  end
end

#allocate_resources ⇒ Object



388
389
390
# File 'app/models/concerns/spree_cm_commissioner/order_state_machine.rb', line 388

def allocate_resources
  SpreeCmCommissioner::VotingCreditAllocationJob.perform_later(order_id: id)
end

#can_accept_all? ⇒ Boolean

can_accepted? already use by ransack/visitor.rb

Returns:

  • (Boolean)


250
251
252
# File 'app/models/concerns/spree_cm_commissioner/order_state_machine.rb', line 250

def can_accept_all?
  approved? && requested?
end

#can_alert_request_to_vendor? ⇒ Boolean

Returns:

  • (Boolean)


258
259
260
# File 'app/models/concerns/spree_cm_commissioner/order_state_machine.rb', line 258

def can_alert_request_to_vendor?
  !accepted? && approved? && need_confirmation?
end

#can_reject_all? ⇒ Boolean

Returns:

  • (Boolean)


254
255
256
# File 'app/models/concerns/spree_cm_commissioner/order_state_machine.rb', line 254

def can_reject_all?
  approved? && requested?
end

#clear_cancellation_details ⇒ Object

Reset the cancellation info when an order is resumed. We only assign the values here; the resume transition already saves the order, so they get saved together with it (one validated save, no update_columns).



399
400
401
402
# File 'app/models/concerns/spree_cm_commissioner/order_state_machine.rb', line 399

def clear_cancellation_details
  self.canceled_at = nil
  self.canceler_id = nil
end

#de_allocate_resources ⇒ Object



392
393
394
# File 'app/models/concerns/spree_cm_commissioner/order_state_machine.rb', line 392

def de_allocate_resources
  SpreeCmCommissioner::VotingCreditDeAllocationJob.perform_later(order_id: id)
end

#decrement_user_request_orders_count ⇒ Object



117
118
119
120
121
# File 'app/models/concerns/spree_cm_commissioner/order_state_machine.rb', line 117

def decrement_user_request_orders_count
  return if user.nil?

  SpreeCmCommissioner::UserCountersService.decrement_request_orders_count(user)
end

#deliver_order_confirmation_email ⇒ Object

override



104
105
106
107
108
109
# File 'app/models/concerns/spree_cm_commissioner/order_state_machine.rb', line 104

def deliver_order_confirmation_email
  return if requested?
  return if any_app_only_products?

  super
end

#finalize_guests_and_bibs ⇒ Object



196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
# File 'app/models/concerns/spree_cm_commissioner/order_state_machine.rb', line 196

def finalize_guests_and_bibs
  ActiveRecord::Base.transaction do
    line_items.each do |line_item|
      line_item.generate_remaining_guests

      line_item.guests.each do |guest|
        result = SpreeCmCommissioner::Guests::Finalize.call(
          guest: guest,
          user: user,
          event: line_item.event,
          variant: line_item.variant
        )

        unless result.success?
          guest.errors.add(:base, result.error.value || result.error.to_s)
          raise(ActiveRecord::RecordInvalid, guest)
        end
      end
    end
  end

  true
rescue ActiveRecord::RecordInvalid => e
  e.record.errors.each do |msg|
    errors.add(:guests, msg)
  end

  false
end

#generate_completion_steps! ⇒ Object

This is called before complete, which is already wrapped by the transaction of unstock_inventory!



128
129
130
# File 'app/models/concerns/spree_cm_commissioner/order_state_machine.rb', line 128

def generate_completion_steps!
  line_items.each(&:generate_completion_steps!)
end

#increment_user_request_orders_count ⇒ Object



111
112
113
114
115
# File 'app/models/concerns/spree_cm_commissioner/order_state_machine.rb', line 111

def increment_user_request_orders_count
  return if user.nil?

  SpreeCmCommissioner::UserCountersService.increment_request_orders_count(user)
end

#need_confirmation? ⇒ Boolean

Returns:

  • (Boolean)


266
267
268
# File 'app/models/concerns/spree_cm_commissioner/order_state_machine.rb', line 266

def need_confirmation?
  line_items.any?(&:need_confirmation)
end

#notify_order_complete_app_notification_to_user ⇒ Object



318
319
320
321
322
# File 'app/models/concerns/spree_cm_commissioner/order_state_machine.rb', line 318

def notify_order_complete_app_notification_to_user
  return if products.all?(&:vote_package?) # disable app push notification for vote package orders

  SpreeCmCommissioner::OrderCompleteNotificationSender.call(order: self)
end

#notify_order_complete_telegram_notification_to_user ⇒ Object



324
325
326
327
328
# File 'app/models/concerns/spree_cm_commissioner/order_state_machine.rb', line 324

def notify_order_complete_telegram_notification_to_user
  return unless telegram_alert_enabled?

  SpreeCmCommissioner::OrderCompleteTelegramSenderJob.perform_later(order_id: id) if user_id.present?
end

#precalculate_conversion ⇒ Object



132
133
134
135
136
137
138
139
140
141
# File 'app/models/concerns/spree_cm_commissioner/order_state_machine.rb', line 132

def precalculate_conversion
  event_ids = line_items.pluck(:event_id).compact.uniq
  event_ids.each do |event_id|
    # Just create task, CRON job will pick it up and execute.
    SpreeCmCommissioner::MaintenanceTasks::Event.pending.create_or_find_by(
      maintainable_type: 'Spree::Taxon',
      maintainable_id: event_id
    )
  end
end

#product_completion_steps_exist? ⇒ Boolean

Returns:

  • (Boolean)


123
124
125
# File 'app/models/concerns/spree_cm_commissioner/order_state_machine.rb', line 123

def product_completion_steps_exist?
  product_completion_steps.any?
end

#rejected_by(user) ⇒ Object



226
227
228
229
230
231
232
233
234
# File 'app/models/concerns/spree_cm_commissioner/order_state_machine.rb', line 226

def rejected_by(user)
  transaction do
    reject!

    line_items.each do |line_item|
      line_item.rejected_by(user)
    end
  end
end

#requested_state? ⇒ Boolean

Returns:

  • (Boolean)


262
263
264
# File 'app/models/concerns/spree_cm_commissioner/order_state_machine.rb', line 262

def requested_state?
  request_state == 'requested'
end

#restock_inventory! ⇒ Object



181
182
183
184
185
186
187
188
189
190
191
192
193
194
# File 'app/models/concerns/spree_cm_commissioner/order_state_machine.rb', line 181

def restock_inventory!
  ActiveRecord::Base.transaction do
    restock_inventory_from_external_system!
    release_order_holds!
    restock_inventory_in_redis!
  end
rescue StandardError => e
  CmAppLogger.log(
    label: 'SpreeCmCommissioner::OrderStateMachine#restock_inventory! failed',
    data: { order_id: id, error: e.message, type: e.class.name, backtrace: e.backtrace&.first(5)&.join("\n") }
  )

  raise e
end

#run_order_complete_alerts ⇒ Object



369
370
371
372
373
374
375
376
377
# File 'app/models/concerns/spree_cm_commissioner/order_state_machine.rb', line 369

def run_order_complete_alerts
  return unless ENV['ORDER_INTEGRITY_TELEGRAM_ALERT_ENABLED'] == 'yes'
  return unless telegram_alert_enabled?

  # Allow async payment processors time to settle before checking for alerts
  SpreeCmCommissioner::TelegramAlerts::OrderIntegrityCheckJob
    .set(wait: 2.minutes)
    .perform_later(order_id: id)
end

#send_dynamic_field_notification_to_guests ⇒ Object



281
282
283
284
285
286
287
288
# File 'app/models/concerns/spree_cm_commissioner/order_state_machine.rb', line 281

def send_dynamic_field_notification_to_guests
  incomplete_guests = line_items.flat_map(&:guests).select(&:incomplete?)
  return if incomplete_guests.empty?

  incomplete_guests.each do |guest|
    SpreeCmCommissioner::Guests::SendDynamicFieldNotification.call(guest: guest)
  end
end

#send_order_accepted_app_notification_to_user ⇒ Object



334
335
336
# File 'app/models/concerns/spree_cm_commissioner/order_state_machine.rb', line 334

def send_order_accepted_app_notification_to_user
  SpreeCmCommissioner::OrderAcceptedNotificationSender.call(order: self)
end

#send_order_accepted_telegram_alert_to_store ⇒ Object



351
352
353
354
355
356
357
358
# File 'app/models/concerns/spree_cm_commissioner/order_state_machine.rb', line 351

def send_order_accepted_telegram_alert_to_store
  return unless telegram_alert_enabled?

  title = '✅ --- [ORDER ACCEPTED BY VENDOR] ---'
  chat_id = store.preferred_telegram_order_request_alert_chat_id
  factory = OrderTelegramMessageFactory.new(title: title, order: self)
  TelegramNotificationSenderJob.perform_later(chat_id: chat_id, message: factory.message, parse_mode: factory.parse_mode)
end

#send_order_complete_telegram_alert_to_store ⇒ Object



379
380
381
382
383
384
385
386
# File 'app/models/concerns/spree_cm_commissioner/order_state_machine.rb', line 379

def send_order_complete_telegram_alert_to_store
  return unless telegram_alert_enabled?

  title = '🎫 --- [NEW ORDER] ---'
  chat_id = store.preferred_telegram_order_alert_chat_id
  factory = OrderTelegramMessageFactory.new(title: title, order: self)
  TelegramNotificationSenderJob.perform_later(chat_id: chat_id, message: factory.message, parse_mode: factory.parse_mode)
end

#send_order_complete_telegram_alert_to_vendors ⇒ Object



290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
# File 'app/models/concerns/spree_cm_commissioner/order_state_machine.rb', line 290

def send_order_complete_telegram_alert_to_vendors
  return unless telegram_alert_enabled?

  vendor_list.each do |vendor|
    # `next`, not `return`: the mask is per-vendor, so one opted-out vendor on a
    # multi-vendor order must not suppress the others.
    next unless vendor.alerts_telegram_for_channel?(channel_key)

    chat_id = vendor.preferred_telegram_chat_id
    next if chat_id.blank?

    factory = OrderTelegramMessageFactory.new(title: vendor_alert_title(vendor), order: self, vendor: vendor)
    TelegramNotificationSenderJob.perform_later(chat_id: chat_id, message: factory.message, parse_mode: factory.parse_mode)
  end
end

#send_order_rejected_app_notification_to_user ⇒ Object



338
339
340
# File 'app/models/concerns/spree_cm_commissioner/order_state_machine.rb', line 338

def send_order_rejected_app_notification_to_user
  SpreeCmCommissioner::OrderRejectedNotificationSender.call(order: self)
end

#send_order_rejected_telegram_alert_to_store ⇒ Object



360
361
362
363
364
365
366
367
# File 'app/models/concerns/spree_cm_commissioner/order_state_machine.rb', line 360

def send_order_rejected_telegram_alert_to_store
  return unless telegram_alert_enabled?

  title = '❌ --- [ORDER REJECTED BY VENDOR] ---'
  chat_id = store.preferred_telegram_order_request_alert_chat_id
  factory = OrderTelegramMessageFactory.new(title: title, order: self)
  TelegramNotificationSenderJob.perform_later(chat_id: chat_id, message: factory.message, parse_mode: factory.parse_mode)
end

#send_order_request_telegram_confirmation_alert_to_vendor ⇒ Object



270
# File 'app/models/concerns/spree_cm_commissioner/order_state_machine.rb', line 270

def send_order_request_telegram_confirmation_alert_to_vendor; end

#send_order_requested_app_notification_to_user ⇒ Object



330
331
332
# File 'app/models/concerns/spree_cm_commissioner/order_state_machine.rb', line 330

def send_order_requested_app_notification_to_user
  SpreeCmCommissioner::OrderRequestedNotificationSender.call(order: self)
end

#send_order_requested_telegram_alert_to_store ⇒ Object



342
343
344
345
346
347
348
349
# File 'app/models/concerns/spree_cm_commissioner/order_state_machine.rb', line 342

def send_order_requested_telegram_alert_to_store
  return unless telegram_alert_enabled?

  title = '🔔 --- [NEW REQUESTED BY USER] ---'
  chat_id = store.preferred_telegram_order_request_alert_chat_id
  factory = OrderTelegramMessageFactory.new(title: title, order: self)
  TelegramNotificationSenderJob.perform_later(chat_id: chat_id, message: factory.message, parse_mode: factory.parse_mode)
end

#send_transaction_email_to_user ⇒ Object



236
237
238
239
240
241
242
243
# File 'app/models/concerns/spree_cm_commissioner/order_state_machine.rb', line 236

def send_transaction_email_to_user
  line_items.each do |line_item|
    next if line_item.event.nil?
    next unless line_item.enable_social_sharing_email?

    SpreeCmCommissioner::EventTransactionalMailer.send_to_participant(line_item.event_id, user.id).deliver_later
  end
end

#telegram_alert_enabled? ⇒ Boolean

Telegram order alerts are sent unless EVERY purchased product opts out. Default is true (see product_decorator), so existing orders are unaffected. Checked at enqueue time so disabled products create no telegram jobs at all.

Returns:

  • (Boolean)


275
276
277
278
279
# File 'app/models/concerns/spree_cm_commissioner/order_state_machine.rb', line 275

def telegram_alert_enabled?
  return @telegram_alert_enabled if defined?(@telegram_alert_enabled)

  @telegram_alert_enabled = line_items.any?(&:enable_telegram_alert?)
end

#unstock_inventory! ⇒ Object



154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
# File 'app/models/concerns/spree_cm_commissioner/order_state_machine.rb', line 154

def unstock_inventory!
  ActiveRecord::Base.transaction do
    yield if block_given? # Equal to block.call

    # After the transition is complete, the following code will execute first before proceeding to other `after_transition` callbacks.
    # This ensures that if `reserve_order_holds!` or `unstock_inventory_in_redis!` fails, the state will be rolled back,
    # and neither the `finalize!` method nor any notifications will be triggered.
    # The payment will be reversed in vPago gem, and `Spree::Checkout::Complete` will be called, which checks `order.reload.complete?`.
    # This is critical because if the order state is complete, the payment will be marked as paid.

    unstock_inventory_from_external_system!
    reserve_order_holds!
    unstock_inventory_in_redis!

    # We rollback only order state, and we keep payment state as it is.
    # We implement payment in vPago gem, and it will be reversed in the gem.
    # Some bank has api for refund, but some don't have the api to refund yet. So we keep the payment state as it is and refund manually.
  end
rescue StandardError => e
  CmAppLogger.log(
    label: 'SpreeCmCommissioner::OrderStateMachine#unstock_inventory! failed',
    data: { order_id: id, error: e.message, type: e.class.name, backtrace: e.backtrace&.first(5)&.join("\n") }
  )

  raise e
end

#user_has_email? ⇒ Boolean

Returns:

  • (Boolean)


245
246
247
# File 'app/models/concerns/spree_cm_commissioner/order_state_machine.rb', line 245

def user_has_email?
  user.present? && user.email.present?
end

#vendor_alert_title(vendor) ⇒ Object

White-label vendors announce their own brand; platform vendors stay BookMe+.

vendor.name is operator-supplied data interpolated into an HTML parse_mode message (TelegramMessageFactory#header wraps the title in ), so it MUST be escaped — Telegram rejects unescaped &, < and > with "400 can't parse entities", and TelegramNotificationSender swallows that error, so the alert would vanish silently.



312
313
314
315
316
# File 'app/models/concerns/spree_cm_commissioner/order_state_machine.rb', line 312

def vendor_alert_title(vendor)
  return '🎫 --- [NEW ORDER FROM BOOKME+] ---' unless vendor.tenant_present?

  "🎫 --- [NEW ORDER FROM #{CGI.escapeHTML(vendor.name.to_s)}] ---"
end