Class: Dscf::Marketplace::Order
- Inherits:
-
ApplicationRecord
- Object
- ActiveRecord::Base
- ApplicationRecord
- Dscf::Marketplace::Order
- Defined in:
- app/models/dscf/marketplace/order.rb
Instance Attribute Summary collapse
-
#acts_as_parent ⇒ Object
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).
Class Method Summary collapse
- .create_from_listing(listing, user, quantity, dropoff_address = nil, payment_method = nil) ⇒ Object
- .create_from_quotation(quotation, dropoff_address = nil, payment_method = nil) ⇒ Object
- .ransackable_associations(_auth_object = nil) ⇒ Object
-
.ransackable_attributes(_auth_object = nil) ⇒ Object
Ransack configuration for secure filtering.
Instance Method Summary collapse
-
#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 unrelatedwaiting_retailer_confirmationmeaning (the retailer signing off on supplier-proposed adjustments mid-fulfillment). - #all_items_validated? ⇒ Boolean
-
#awaiting_validation? ⇒ Boolean
Workflow helpers for Sprint 2 order validation + splitting.
-
#billable_order_items ⇒ Object
Lines that should count toward the order total / retailer-facing amount.
- #calculate_total_amount ⇒ Object
- #can_be_completed? ⇒ Boolean
-
#compute_aggregate_status ⇒ Object
The parent's status mirrors the furthest-behind live sub-order: it only advances once every live (non-cancelled) sub-order has advanced.
- #confirm! ⇒ Object
- #delivery? ⇒ Boolean
- #mark_splitting! ⇒ Object
- #mark_waiting_retailer_confirmation! ⇒ Object
- #mark_waiting_supplier_confirmation! ⇒ Object
- #otp_verified? ⇒ Boolean
-
#parent? ⇒ Boolean
Retailer-facing umbrella over per-business sub-orders.
- #requires_delivery_order? ⇒ Boolean
- #retailer_can_confirm? ⇒ Boolean
-
#self_pickup? ⇒ Boolean
Fulfillment type methods.
- #supplier ⇒ Object
- #supplier_confirmation_complete? ⇒ Boolean
-
#sync_aggregates! ⇒ Object
Recompute the parent's status/total from its sub-orders.
- #total_amount ⇒ Object
- #validation_summary ⇒ Object
Instance Attribute Details
#acts_as_parent ⇒ Object
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).
296 297 298 |
# File 'app/models/dscf/marketplace/order.rb', line 296 def agent_assisted? agent_id.present? end |
#all_items_validated? ⇒ 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.
252 253 254 |
# File 'app/models/dscf/marketplace/order.rb', line 252 def awaiting_validation? pending? || validating? || status.to_s == "processing" end |
#billable_order_items ⇒ Object
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_amount ⇒ Object
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
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_status ⇒ Object
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
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
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.
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
182 183 184 |
# File 'app/models/dscf/marketplace/order.rb', line 182 def requires_delivery_order? delivery? && !completed? end |
#retailer_can_confirm? ⇒ 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
174 175 176 |
# File 'app/models/dscf/marketplace/order.rb', line 174 def self_pickup? fulfillment_type == "self_pickup" end |
#supplier ⇒ Object
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
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_amount ⇒ Object
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_summary ⇒ Object
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 |