Module: SpreeCmCommissioner::OrderDecorator

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

Constant Summary collapse

SPREE_CHANNEL =
'spree'.freeze
GOOGLE_FORM_CHANNEL =
'google_form'.freeze
TELEGRAM_CHANNEL =
'telegram'.freeze
TICKET_TRANSFER_CHANNEL =
'ticket_transfer'.freeze
ALLOWED_CHANNEL_PREFIXES =
[SPREE_CHANNEL, GOOGLE_FORM_CHANNEL, TELEGRAM_CHANNEL, TICKET_TRANSFER_CHANNEL].freeze

Class Method Summary collapse

Instance Method Summary collapse

Class Method Details

.prepended(base) ⇒ Object

rubocop:disable Metrics/MethodLength



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

def self.prepended(base) # rubocop:disable Metrics/MethodLength
  base.include SpreeCmCommissioner::StoreMetadata
  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.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.validates :promo_total, base::MONEY_VALIDATION
  base.validate :validate_channel_prefix, if: :channel_changed?
  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

#after_resumeObject

override



75
76
77
78
# File 'app/models/spree_cm_commissioner/order_decorator.rb', line 75

def after_resume
  super
  update_columns(canceled_at: nil, canceler_id: nil) # rubocop:disable Rails/SkipsModelValidations
end

#any_app_only_products?Boolean

Returns:

  • (Boolean)


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

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



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

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



81
82
83
84
85
86
87
88
89
90
91
# File 'app/models/spree_cm_commissioner/order_decorator.rb', line 81

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

#collect_payment_methods(store = nil) ⇒ Object

override



153
154
155
156
157
158
# File 'app/models/spree_cm_commissioner/order_decorator.rb', line 153

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



160
161
162
163
# File 'app/models/spree_cm_commissioner/order_decorator.rb', line 160

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.



322
323
324
325
326
327
328
329
# File 'app/models/spree_cm_commissioner/order_decorator.rb', line 322

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_eligbleObject

assume check is default payment method for subscription



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

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

#customer_addressObject



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

def customer_address
  bill_address || ship_address
end

#delivery_required?Boolean

override

Returns:

  • (Boolean)


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

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

#display_outstanding_balanceObject



308
309
310
# File 'app/models/spree_cm_commissioner/order_decorator.rb', line 308

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

#enable_send_receipt?Boolean

Returns:

  • (Boolean)


304
305
306
# File 'app/models/spree_cm_commissioner/order_decorator.rb', line 304

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

#generate_png_qr(size = 120) ⇒ Object



280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
# File 'app/models/spree_cm_commissioner/order_decorator.rb', line 280

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



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

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

#guests_in_transfer?Boolean

Returns:

  • (Boolean)


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

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

#insufficient_stock_linesObject

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



141
142
143
144
# File 'app/models/spree_cm_commissioner/order_decorator.rb', line 141

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

#jwt_qr_dataObject



300
301
302
# File 'app/models/spree_cm_commissioner/order_decorator.rb', line 300

def jwt_qr_data
  SpreeCmCommissioner::Orders::JwtToken::Generate.call(order: self).value[:token]
end

#mark_as_archiveObject



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

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)


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

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

#payment_fulfilled?Boolean

Returns:

  • (Boolean)


135
136
137
# File 'app/models/spree_cm_commissioner/order_decorator.rb', line 135

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

#payment_hostObject

override spree_vpago method



313
314
315
# File 'app/models/spree_cm_commissioner/order_decorator.rb', line 313

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

#payment_required?Boolean

overrided

Returns:

  • (Boolean)


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

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)


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

def purchased_from_tenant?
  !tenant_id.nil?
end

#qr_dataObject



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

def qr_data
  "#{number}-#{token}"
end

#restart_checkout_flowObject

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.


113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
# File 'app/models/spree_cm_commissioner/order_decorator.rb', line 113

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_emailObject

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.



97
98
99
100
101
# File 'app/models/spree_cm_commissioner/order_decorator.rb', line 97

def send_cancel_email
  return if imported_order.present?

  super
end

#subscription?Boolean

Returns:

  • (Boolean)


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

def subscription?
  subscription.present?
end

#ticket_seller_user?Boolean

Returns:

  • (Boolean)


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

def ticket_seller_user?
  return false if user.nil?

  user.has_spree_role?('ticket_seller')
end

#valid_promotion_idsObject

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



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

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