Class: Dscf::Marketplace::Order

Inherits:
ApplicationRecord show all
Defined in:
app/models/dscf/marketplace/order.rb

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Instance Attribute Details

#acts_as_parentObject

Marks an order being built as a parent before its sub-orders exist (they are created after the parent inside the checkout transaction, so sub_orders.exists? alone can't answer "am I a parent?" on first save).



44
45
46
# File 'app/models/dscf/marketplace/order.rb', line 44

def acts_as_parent
  @acts_as_parent
end

Class Method Details

.create_from_listing(listing, user, quantity, dropoff_address = nil, payment_method = nil) ⇒ Object



115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
# File 'app/models/dscf/marketplace/order.rb', line 115

def self.create_from_listing(listing, user, quantity, dropoff_address = nil, payment_method = nil)
  return nil unless listing.visible? && quantity <= listing.quantity

  attributes = {
    order_type: :direct_listing,
    status: :validating,
    fulfillment_type: dropoff_address.present? ? :delivery : :self_pickup,
    listing: listing,
    user: user, # Keep for backward compatibility
    ordered_by: user,
    ordered_to: listing.business,
    dropoff_address: dropoff_address,
    total_amount: listing.price * quantity
  }
  attributes[:payment_method] = payment_method if payment_method.present?

  order = create!(attributes)

  order.order_items.create!(
    listing: listing,
    product: listing.supplier_product.product,
    unit: listing.supplier_product.product.unit,
    quantity: quantity,
    unit_price: listing.price,
    status: :pending,
    validation_status: :validated
  )

  order
end

.create_from_quotation(quotation, dropoff_address = nil, payment_method = nil) ⇒ Object



81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
# File 'app/models/dscf/marketplace/order.rb', line 81

def self.create_from_quotation(quotation, dropoff_address = nil, payment_method = nil)
  return nil unless quotation.accepted?

  attributes = {
    order_type: :rfq_based,
    status: :validating,
    fulfillment_type: dropoff_address.present? ? :delivery : :self_pickup,
    quotation: quotation,
    user: quotation.request_for_quotation.user, # Keep for backward compatibility
    ordered_by: quotation.request_for_quotation.user,
    ordered_to: quotation.business,
    dropoff_address: dropoff_address,
    total_amount: quotation.total_price
  }
  attributes[:payment_method] = payment_method if payment_method.present?

  order = create!(attributes)

  quotation.quotation_items.each do |item|
    order.order_items.create!(
      quotation_item: item,
      product: item.product,
      unit: item.unit,
      quantity: item.quantity,
      unit_price: item.unit_price,
      status: :pending,
      validation_status: :validated # initial from accepted quote; validation can re-run
    )
  end

  order
end

.ransackable_associations(_auth_object = nil) ⇒ Object



77
78
79
# File 'app/models/dscf/marketplace/order.rb', line 77

def self.ransackable_associations(_auth_object = nil)
  %w[quotation listing user ordered_by ordered_to delivery_order dropoff_address order_items parent_order sub_orders]
end

.ransackable_attributes(_auth_object = nil) ⇒ Object

Ransack configuration for secure filtering



73
74
75
# File 'app/models/dscf/marketplace/order.rb', line 73

def self.ransackable_attributes(_auth_object = nil)
  %w[id quotation_id listing_id user_id ordered_by_id ordered_to_id parent_order_id delivery_order_id dropoff_address_id agent_id order_type status fulfillment_type payment_method received_bank_name transaction_reference total_amount created_at updated_at]
end

Instance Method Details

#agent_assisted?Boolean

An agent-assisted order (agent_id present) must have its OTP verified by the retailer before it can enter the normal validate/confirm pipeline — this gate is intentionally orthogonal to status, which already has an unrelated waiting_retailer_confirmation meaning (the retailer signing off on supplier-proposed adjustments mid-fulfillment).

Returns:

  • (Boolean)


296
297
298
# File 'app/models/dscf/marketplace/order.rb', line 296

def agent_assisted?
  agent_id.present?
end

#all_items_validated?Boolean

Returns:

  • (Boolean)


256
257
258
259
260
# File 'app/models/dscf/marketplace/order.rb', line 256

def all_items_validated?
  # A cancelled line (removed during validation) is no longer part of what
  # needs to ship, so it shouldn't block Split from becoming available.
  order_items.reject(&:cancelled?).all? { |i| i.validation_status == "validated" }
end

#awaiting_validation?Boolean

Workflow helpers for Sprint 2 order validation + splitting. Note: validating? is the enum-generated predicate (true only when status == "validating"). Use this for "can still be run through validation" — covers the actual validating status plus the states an order can be created in (pending / legacy processing) before it does.

Returns:

  • (Boolean)


252
253
254
# File 'app/models/dscf/marketplace/order.rb', line 252

def awaiting_validation?
  pending? || validating? || status.to_s == "processing"
end

#billable_order_itemsObject

Lines that should count toward the order total / retailer-facing amount. A cancelled line (removed during validation, or a rejected supplier allocation) is not being fulfilled, so it shouldn't inflate what the retailer is asked to confirm and pay.



161
162
163
# File 'app/models/dscf/marketplace/order.rb', line 161

def billable_order_items
  order_items.reject(&:cancelled?)
end

#calculate_total_amountObject



205
206
207
208
209
210
211
# File 'app/models/dscf/marketplace/order.rb', line 205

def calculate_total_amount
  # A parent has no items; its total is synced from sub-orders — don't
  # zero it out on save.
  return if parent?

  self.total_amount = billable_order_items.sum { |item| item.quantity * item.unit_price }
end

#can_be_completed?Boolean

Returns:

  • (Boolean)


186
187
188
189
190
191
192
193
194
# File 'app/models/dscf/marketplace/order.rb', line 186

def can_be_completed?
  if self_pickup?
    # Self-pickup orders can be completed immediately after confirmation
    confirmed? || processing?
  else
    # Delivery orders require delivery_order association
    delivery_order.present? && delivery_order.delivered?
  end
end

#compute_aggregate_statusObject

The parent's status mirrors the furthest-behind live sub-order: it only advances once every live (non-cancelled) sub-order has advanced.



234
235
236
237
238
239
240
241
242
243
244
245
# File 'app/models/dscf/marketplace/order.rb', line 234

def compute_aggregate_status
  statuses = sub_orders.pluck(:status).map { |s| self.class.statuses.key(s) || s.to_s }
  live = statuses.reject { |s| s == "cancelled" }

  return :cancelled if live.empty?
  return :completed if live.all? { |s| s == "completed" }
  return :confirmed if live.all? { |s| %w[confirmed processing completed].include?(s) }
  return :waiting_supplier_confirmation if live.any? { |s| %w[waiting_supplier_confirmation splitting].include?(s) }
  return :waiting_retailer_confirmation if live.any? { |s| s == "waiting_retailer_confirmation" }

  :pending
end

#confirm!Object



196
197
198
199
200
201
202
203
# File 'app/models/dscf/marketplace/order.rb', line 196

def confirm!
  return false unless pending?

  update!(status: :confirmed)
  # Update order items without reloading to avoid association issues
  order_items.update_all(status: OrderItem.statuses[:confirmed])
  true
end

#delivery?Boolean

Returns:

  • (Boolean)


178
179
180
# File 'app/models/dscf/marketplace/order.rb', line 178

def delivery?
  fulfillment_type == "delivery"
end

#mark_splitting!Object



266
267
268
# File 'app/models/dscf/marketplace/order.rb', line 266

def mark_splitting!
  update!(status: :splitting)
end

#mark_waiting_retailer_confirmation!Object



274
275
276
# File 'app/models/dscf/marketplace/order.rb', line 274

def mark_waiting_retailer_confirmation!
  update!(status: :waiting_retailer_confirmation)
end

#mark_waiting_supplier_confirmation!Object



270
271
272
# File 'app/models/dscf/marketplace/order.rb', line 270

def mark_waiting_supplier_confirmation!
  update!(status: :waiting_supplier_confirmation)
end

#otp_verified?Boolean

Returns:

  • (Boolean)


300
301
302
303
304
# File 'app/models/dscf/marketplace/order.rb', line 300

def otp_verified?
  # A split (multi-business) agent order carries ONE OTP on the parent —
  # the retailer confirms the whole cart once; sub-orders inherit it.
  otp_verifications.verified.exists? || (parent_order.present? && parent_order.otp_verified?)
end

#parent?Boolean

Retailer-facing umbrella over per-business sub-orders. The virtual flag covers the parent's own creation (before sub-orders exist); afterwards the association is authoritative.

Returns:

  • (Boolean)


216
217
218
# File 'app/models/dscf/marketplace/order.rb', line 216

def parent?
  acts_as_parent.present? || (persisted? && sub_orders.exists?)
end

#requires_delivery_order?Boolean

Returns:

  • (Boolean)


182
183
184
# File 'app/models/dscf/marketplace/order.rb', line 182

def requires_delivery_order?
  delivery? && !completed?
end

#retailer_can_confirm?Boolean

Returns:

  • (Boolean)


278
279
280
# File 'app/models/dscf/marketplace/order.rb', line 278

def retailer_can_confirm?
  waiting_retailer_confirmation? || confirmed? # allow re-confirm safety
end

#self_pickup?Boolean

Fulfillment type methods

Returns:

  • (Boolean)


174
175
176
# File 'app/models/dscf/marketplace/order.rb', line 174

def self_pickup?
  fulfillment_type == "self_pickup"
end

#supplierObject



165
166
167
168
169
170
171
# File 'app/models/dscf/marketplace/order.rb', line 165

def supplier
  if rfq_based?
    quotation&.business
  elsif direct_listing?
    listing&.business || ordered_to
  end
end

#supplier_confirmation_complete?Boolean

Returns:

  • (Boolean)


282
283
284
285
286
287
288
289
# File 'app/models/dscf/marketplace/order.rb', line 282

def supplier_confirmation_complete?
  # A cancelled line (removed at validation time, before a source was ever
  # assigned, or rejected during confirmation) is done responding either
  # way — only a still-live line needs a source + confirmed status.
  order_items.all? do |i|
    i.cancelled? || (i.source_id.present? && i.status.to_s == "confirmed")
  end
end

#sync_aggregates!Object

Recompute the parent's status/total from its sub-orders. update_columns on purpose: aggregation must not re-trigger validations or callbacks.



222
223
224
225
226
227
228
229
230
# File 'app/models/dscf/marketplace/order.rb', line 222

def sync_aggregates!
  return unless parent?

  update_columns(
    status: self.class.statuses[compute_aggregate_status],
    total_amount: sub_orders.sum(:total_amount),
    updated_at: Time.current
  )
end

#total_amountObject



147
148
149
150
151
152
153
154
155
# File 'app/models/dscf/marketplace/order.rb', line 147

def total_amount
  # Return stored value if it exists and is greater than 0, otherwise calculate
  stored_value = super
  calculated_value = billable_order_items.sum { |item| item.quantity * item.unit_price }

  # Return stored value if it's set and valid, otherwise return calculated value
  return stored_value if stored_value.present? && stored_value > 0
  calculated_value
end

#validation_summaryObject



262
263
264
# File 'app/models/dscf/marketplace/order.rb', line 262

def validation_summary
  order_items.group_by(&:validation_status).transform_values(&:count)
end