Module: SpreeCmCommissioner::OrderDecorator

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

Class Method Summary collapse

Instance Method Summary collapse

Class Method Details

.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
# File 'app/models/spree_cm_commissioner/order_decorator.rb', line 3

def self.prepended(base) # rubocop:disable Metrics/MethodLength,Metrics/AbcSize
  base.include SpreeCmCommissioner::OrderChannelBitwise
  base.include SpreeCmCommissioner::
  base.include SpreeCmCommissioner::PhoneNumberSanitizer
  base.include SpreeCmCommissioner::OrderIntegration
  base.include SpreeCmCommissioner::OrderHoldable
  base.include SpreeCmCommissioner::OrderStateMachine
  base.include SpreeCmCommissioner::RouteOrderCountable
  base.include SpreeCmCommissioner::OrderScopes
  base.include SpreeCmCommissioner::TicketTransferable
  base.include SpreeCmCommissioner::OrderQrCodeable

  base.before_create :link_by_phone_number
  base.before_create :associate_customer
  base.before_create :set_tenant_id
  base.after_commit :increment_route_order_count, on: :create

  base. :preload_trip_ids, :array, default: []
  base. :preload_main_trip_ids, :array, default: []
  base. :order_options, :array, default: []

  base.money_methods :service_fee_total

  base.validates :promo_total, base::MONEY_VALIDATION
  base.validates :phone_number, presence: true, if: :require_phone_number

  base.has_one :invoice, dependent: :destroy, class_name: 'SpreeCmCommissioner::Invoice'
  base.has_one :customer, class_name: 'SpreeCmCommissioner::Customer', through: :subscription
  base.has_one :imported_order,
               class_name: 'SpreeCmCommissioner::ImportedOrder',
               dependent: :destroy,
               inverse_of: :order

  base.belongs_to :tenant, class_name: 'SpreeCmCommissioner::Tenant', optional: true
  base.belongs_to :subscription, class_name: 'SpreeCmCommissioner::Subscription', optional: true

  base.has_many :taxons, class_name: 'Spree::Taxon', through: :customer
  base.has_many :vendors, through: :products, class_name: 'Spree::Vendor'
  base.has_many :taxons, through: :products, class_name: 'Spree::Taxon'
  base.has_many :guests, through: :line_items, class_name: 'SpreeCmCommissioner::Guest'

  base.has_many :saved_guests,
                -> { reorder(nil).distinct },
                through: :guests,
                source: :saved_guest,
                class_name: 'SpreeCmCommissioner::SavedGuest'

  base.has_many :blocks, through: :guests, class_name: 'SpreeCmCommissioner::Block', source: :block
  base.has_many :reserved_blocks, through: :guests, class_name: 'SpreeCmCommissioner::ReservedBlock'
  base.has_many :guest_card_classes, class_name: 'SpreeCmCommissioner::GuestCardClass', through: :variants
  base.has_many :product_completion_steps, class_name: 'SpreeCmCommissioner::ProductCompletionStep', through: :line_items

  base.delegate :customer, to: :user, allow_nil: true

  base.whitelisted_ransackable_associations |= %w[customer taxon payments guests invoice]
  base.whitelisted_ransackable_attributes |= %w[intel_phone_number phone_number email number state]

  base.accepts_nested_attributes_for :saved_guests, allow_destroy: true

  def base.search_by_qr_data!(data)
    token = data.match(/^R\d{9,}-([A-Za-z0-9_\-]+)$/)&.captures

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

    find_by!(token: token)
  end
end

Instance Method Details

#any_app_only_products? ⇒ Boolean

Returns:

  • (Boolean)


176
177
178
179
180
181
182
# File 'app/models/spree_cm_commissioner/order_decorator.rb', line 176

def any_app_only_products?
  if products.loaded?
    products.any?(&:purchasable_on_app?)
  else
    products.exists?(purchasable_on: :app)
  end
end

#associate_user!(user, override_email = true) ⇒ Object

overrided add phone_number trigger when update customer detail



187
188
189
190
191
192
193
194
195
196
197
198
# File 'app/models/spree_cm_commissioner/order_decorator.rb', line 187

def associate_user!(user, override_email = true) # rubocop:disable Style/OptionalBooleanParameter
  self.user           = user
  self.email          = user.email if override_email
  self.phone_number   = user.phone_number if user.phone_number.present?
  self.created_by   ||= user
  self.bill_address ||= user.bill_address.try(:clone)
  self.ship_address ||= user.ship_address.try(:clone)

  changes = slice(:user_id, :email, :phone_number, :created_by_id, :bill_address_id, :ship_address_id)

  self.class.unscoped.where(id: self).update_all(changes) # rubocop:disable Rails/SkipsModelValidations
end

#canceled_by(user, cancellation_reason: nil) ⇒ Object

override to allow storing cancellation reason in one DB write



72
73
74
75
76
77
78
79
80
81
82
# File 'app/models/spree_cm_commissioner/order_decorator.rb', line 72

def canceled_by(user, cancellation_reason: nil)
  transaction do
    cancel!

    update_columns( # rubocop:disable Rails/SkipsModelValidations
      canceler_id: user.id,
      canceled_at: Time.current,
      internal_note: cancellation_reason
    )
  end
end

#clear_has_incomplete_guest_info_cache ⇒ Object

Clears this order's own cache plus the purchaser's cross-order aggregate cache — anything that can change the former can also change the latter.



284
285
286
287
# File 'app/models/spree_cm_commissioner/order_decorator.rb', line 284

def clear_has_incomplete_guest_info_cache
  Rails.cache.delete(incomplete_guest_info_cache_key)
  user&.clear_has_incomplete_guest_info_cache
end

#collect_payment_methods(store = nil) ⇒ Object

override



144
145
146
147
148
149
# File 'app/models/spree_cm_commissioner/order_decorator.rb', line 144

def collect_payment_methods(store = nil)
  return super if user.blank?
  return super unless user.early_adopter?

  collect_payment_methods_for_early_adopter(store)
end

#collect_payment_methods_for_early_adopter(store = nil) ⇒ Object



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

def collect_payment_methods_for_early_adopter(store = nil)
  store ||= self.store
  store.payment_methods.available_on_frontend_for_early_adopter.select { |pm| pm.available_for_order?(self) }
end

#connected_line_item_ids(direction: nil) ⇒ Object

Returns arrays of connected line_item IDs grouped by connected_trip_id. Example: [[1,2,3], [4,5], [6]]

Line items with the same connected_trip_id are grouped together. Line items without a group_id are returned as singletons.



311
312
313
314
315
316
317
318
# File 'app/models/spree_cm_commissioner/order_decorator.rb', line 311

def connected_line_item_ids(direction: nil)
  scoped = line_items
  scoped = scoped.select { |li| li.direction == direction } if direction.present?

  scoped.group_by { |li| li.connected_trip_id || li.id }
        .values
        .map { |group| group.map(&:id).sort }
end

#create_default_payment_if_eligble ⇒ Object

assume check is default payment method for subscription



221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
# File 'app/models/spree_cm_commissioner/order_decorator.rb', line 221

def create_default_payment_if_eligble
  return unless subscription?

  if covered_by_store_credit
    payment_method = Spree::PaymentMethod::StoreCredit.available_on_back_end.first_or_create! do |method|
      method.name ||= 'StoreCredit'
      method.stores = [Spree::Store.default] if method.stores.empty?
    end
    source_id = user.store_credit_ids.last
    source_type = 'Spree::StoreCredit'
  else
    payment_method = Spree::PaymentMethod::Check.available_on_back_end.first_or_create! do |method|
      method.name ||= 'Invoice'
      method.stores = [Spree::Store.default] if method.stores.empty?
    end
  end
  payments.create!(
    payment_method: payment_method,
    amount: total,
    source_id: source_id,
    source_type: source_type
  )
  Spree::Checkout::Advance.call(order: self)
end

#create_service_fee_charge! ⇒ Object

Mirrors create_tax_charge! (core/app/models/spree/order.rb) — wired into the same checkout transitions via SpreeCmCommissioner::OrderStateMachine, plus called directly from TransitOrder::Create and DraftOrders::Create, which build orders outside the checkout state machine entirely.



165
166
167
# File 'app/models/spree_cm_commissioner/order_decorator.rb', line 165

def create_service_fee_charge!
  SpreeCmCommissioner::ServiceFeeRule.adjust(self, line_items)
end

#customer_address ⇒ Object



255
256
257
# File 'app/models/spree_cm_commissioner/order_decorator.rb', line 255

def customer_address
  bill_address || ship_address
end

#delivery_required? ⇒ Boolean

override

Returns:

  • (Boolean)


157
158
159
# File 'app/models/spree_cm_commissioner/order_decorator.rb', line 157

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

#display_outstanding_balance ⇒ Object



297
298
299
# File 'app/models/spree_cm_commissioner/order_decorator.rb', line 297

def display_outstanding_balance
  Spree::Money.new(outstanding_balance, currency: currency).to_s
end

#enable_send_receipt? ⇒ Boolean

Returns:

  • (Boolean)


293
294
295
# File 'app/models/spree_cm_commissioner/order_decorator.rb', line 293

def enable_send_receipt?
  products.any?(&:enable_send_receipt?)
end

#guests_in_transfer? ⇒ Boolean

Returns:

  • (Boolean)


263
264
265
# File 'app/models/spree_cm_commissioner/order_decorator.rb', line 263

def guests_in_transfer?
  guests.exists?(state: i[locked transferred])
end

#has_incomplete_guest_info? ⇒ Boolean

rubocop:disable Naming/PredicateName

Returns:

  • (Boolean)


267
268
269
270
271
272
273
274
275
276
277
278
279
280
# File 'app/models/spree_cm_commissioner/order_decorator.rb', line 267

def has_incomplete_guest_info? # rubocop:disable Naming/PredicateName
  # expires_in is a safety net, not the primary invalidation path — clear_has_incomplete_guest_info_cache
  # is still called explicitly wherever the underlying guest data can change. This just bounds
  # how long a missed invalidation could leave a stale value in place.
  #
  # Reads the persisted data_fill_stage column via a join instead of Guest#incomplete? (which
  # recomputes live per guest). post_registration is the only stage that matters here: a
  # completed order already forces pre_registration complete (guest_data_fill_stage_concern.rb),
  # and during_check_in is check-in crew's concern, not something this banner surfaces —
  # matches find_orders_with_incomplete_guest_info in orders_controller_decorator.rb.
  Rails.cache.fetch(incomplete_guest_info_cache_key, expires_in: 1.hour) do
    guests.post_registration.exists?
  end
end

#incomplete_guest_info_cache_key ⇒ Object



289
290
291
# File 'app/models/spree_cm_commissioner/order_decorator.rb', line 289

def incomplete_guest_info_cache_key
  "order/#{id}/has_incomplete_guest_info"
end

#insufficient_stock_lines ⇒ Object

override spree use this method to check stock availability & consider whether :order can continue to next state.



132
133
134
135
# File 'app/models/spree_cm_commissioner/order_decorator.rb', line 132

def insufficient_stock_lines
  checker = SpreeCmCommissioner::Stock::OrderAvailabilityChecker.new(self)
  checker.insufficient_stock_lines
end

#mark_as_archive ⇒ Object



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

def mark_as_archive
  released = begin
    release_order_holds!
    true
  rescue StandardError
    false
  end

  update(archived_at: Time.current) if released
end

#order_completed? ⇒ Boolean

Returns:

  • (Boolean)


259
260
261
# File 'app/models/spree_cm_commissioner/order_decorator.rb', line 259

def order_completed?
  complete? && need_confirmation? == false
end

#payment_fulfilled? ⇒ Boolean

Returns:

  • (Boolean)


126
127
128
# File 'app/models/spree_cm_commissioner/order_decorator.rb', line 126

def payment_fulfilled?
  payment_state.present? && payment_state == 'paid'
end

#payment_host ⇒ Object

override spree_vpago method



302
303
304
# File 'app/models/spree_cm_commissioner/order_decorator.rb', line 302

def payment_host
  tenant&.payment_url.presence || tenant&.formatted_url.presence || ENV.fetch('PAYMENT_BASE_URL')
end

#payment_required? ⇒ Boolean

overrided

Returns:

  • (Boolean)


170
171
172
173
174
# File 'app/models/spree_cm_commissioner/order_decorator.rb', line 170

def payment_required?
  return false if need_confirmation?

  super
end

#purchased_from_tenant? ⇒ Boolean

Returns true when the order was created under a tenant context.

Returns:

  • (Boolean)


251
252
253
# File 'app/models/spree_cm_commissioner/order_decorator.rb', line 251

def purchased_from_tenant?
  !tenant_id.nil?
end

#restart_checkout_flow ⇒ Object

override spree_core behavior to intentionally avoid calling next!.

Goal: keep the order state at 'cart' when restarting checkout, especially when seats were held during the address step.

Flow summary:

  • User goes from cart -> address: we hold seats.
  • User navigates back from address: we call this method to cancel the holds. We do NOT call next! here; otherwise the order state machine would trigger hold_order_holds again and re-hold seats unnecessarily.


104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
# File 'app/models/spree_cm_commissioner/order_decorator.rb', line 104

def restart_checkout_flow
  ActiveRecord::Base.transaction do
    release_order_holds!
    log_state_changes(state_name: 'cart', old_state: state, new_state: 'cart')
    update_columns( # rubocop:disable Rails/SkipsModelValidations
      state: 'cart',
      updated_at: Time.current
    )
  end
rescue StandardError => e
  CmAppLogger.error(
    label: 'SpreeCmCommissioner::OrderDecorator#restart_checkout_flow failed',
    data: {
      order_id: id,
      error_class: e.class.name,
      error_message: e.message,
      backtrace: e.backtrace&.first(5)&.join("\n")
    }
  )
  raise
end

#send_cancel_email ⇒ Object

override: imported orders are bulk-loaded from CSV and the contact email often belongs to a third party (organizer, importer) rather than the buyer, so the cancellation email would notify the wrong recipient. Normal orders still receive the cancel email via the spree_emails decorator.



88
89
90
91
92
# File 'app/models/spree_cm_commissioner/order_decorator.rb', line 88

def send_cancel_email
  return if imported_order.present?

  super
end

#subscription? ⇒ Boolean

Returns:

  • (Boolean)


246
247
248
# File 'app/models/spree_cm_commissioner/order_decorator.rb', line 246

def subscription?
  subscription.present?
end

#ticket_seller_user? ⇒ Boolean

Returns:

  • (Boolean)


137
138
139
140
141
# File 'app/models/spree_cm_commissioner/order_decorator.rb', line 137

def ticket_seller_user?
  return false if user.nil?

  user.has_spree_role?('ticket_seller')
end

#valid_promotion_ids ⇒ Object

overrided avoid raise error when source_id is nil. https://github.com/channainfo/commissioner/pull/843



214
215
216
217
218
# File 'app/models/spree_cm_commissioner/order_decorator.rb', line 214

def valid_promotion_ids
  all_adjustments.eligible.nonzero.promotion
                 .where.not(source_id: nil)
                 .map { |a| a.source.promotion_id }.uniq
end