Class: Quote::ConvertToOrder
- Inherits:
-
Object
- Object
- Quote::ConvertToOrder
- Defined in:
- app/services/quote/convert_to_order.rb
Defined Under Namespace
Classes: QuoteUnpurchasable
Instance Attribute Summary collapse
-
#errors ⇒ Object
readonly
Returns the value of attribute errors.
-
#logger ⇒ Object
readonly
Returns the value of attribute logger.
-
#options ⇒ Object
readonly
Returns the value of attribute options.
-
#quote ⇒ Object
readonly
Returns the value of attribute quote.
Class Method Summary collapse
- .can_convert?(quote) ⇒ Boolean
- .can_convert_errors?(quote) ⇒ Boolean
-
.check_quote_line_items_room_integrity(quote) ⇒ Object
Check if all quote line items belonging to a room have the same exact items and quantities as the room configuration's line items.
Instance Method Summary collapse
- #append_core_data(order) ⇒ Object
- #append_deliveries(order) ⇒ Object
- #append_quote_discounts(order, line_item_map) ⇒ Object
- #append_quote_lines(order) ⇒ Object
- #can_convert? ⇒ Boolean
- #clear_shipping_address(order) ⇒ Object
- #convert(existing_order = nil, txid = nil) ⇒ Object
-
#initialize(quote, options = {}) ⇒ ConvertToOrder
constructor
A new instance of ConvertToOrder.
-
#run_pre_check ⇒ Object
Todo, add other conditions here.
Constructor Details
#initialize(quote, options = {}) ⇒ ConvertToOrder
Returns a new instance of ConvertToOrder.
6 7 8 9 10 11 |
# File 'app/services/quote/convert_to_order.rb', line 6 def initialize(quote, = {}) @quote = quote @options = @logger = [:logger] || Rails.logger @errors = [] end |
Instance Attribute Details
#errors ⇒ Object (readonly)
Returns the value of attribute errors.
2 3 4 |
# File 'app/services/quote/convert_to_order.rb', line 2 def errors @errors end |
#logger ⇒ Object (readonly)
Returns the value of attribute logger.
2 3 4 |
# File 'app/services/quote/convert_to_order.rb', line 2 def logger @logger end |
#options ⇒ Object (readonly)
Returns the value of attribute options.
2 3 4 |
# File 'app/services/quote/convert_to_order.rb', line 2 def @options end |
#quote ⇒ Object (readonly)
Returns the value of attribute quote.
2 3 4 |
# File 'app/services/quote/convert_to_order.rb', line 2 def quote @quote end |
Class Method Details
.can_convert?(quote) ⇒ Boolean
13 14 15 |
# File 'app/services/quote/convert_to_order.rb', line 13 def self.can_convert?(quote) new(quote).can_convert? end |
.can_convert_errors?(quote) ⇒ Boolean
17 18 19 |
# File 'app/services/quote/convert_to_order.rb', line 17 def self.can_convert_errors?(quote) new(quote).run_pre_check end |
.check_quote_line_items_room_integrity(quote) ⇒ Object
Check if all quote line items belonging to a room have the same exact items and quantities as the room configuration's line items
38 39 40 41 42 43 44 45 46 47 |
# File 'app/services/quote/convert_to_order.rb', line 38 def self.check_quote_line_items_room_integrity(quote) errors = [] quote.room_configurations.each do |room| room_line_items = room.line_items.order(:item_id, :quantity).pluck(:item_id, :quantity) quote_line_items = quote.line_items.where(room_configuration_id: room.id).order(:item_id, :quantity).pluck(:item_id, :quantity) errors << "Room configuration #{room.reference_number} does not match quote line items linked to same room" if room_line_items != quote_line_items end errors end |
Instance Method Details
#append_core_data(order) ⇒ Object
106 107 108 109 110 111 112 113 114 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 |
# File 'app/services/quote/convert_to_order.rb', line 106 def append_core_data(order) order.opportunity = @quote.opportunity order.shipping_method = @quote.shipping_method # order.shipping_account_number = (@quote.chosen_shipping_method.shipping_account_number rescue nil) # order.shipping_cost = @quote.shipping_cost if @quote.contact.present? order.contact_id = @quote.contact.id if @quote.contact.active_spiff_enrollment.present? and @quote.contact.active_spiff_enrollment.is_active? order.spiff_rep_id = @quote.contact.id order.spiff_enrollment = @quote.contact.active_spiff_enrollment end end order.shipping_address = @quote.shipping_address # This is NOT necessary and potentially problematic || @quote.customer.shipping_address # Always inherit the customer's pricing program order.pricing_program_discount = @quote.customer.pricing_program_discount order.pricing_program_description = @quote.customer.pricing_program_description order.max_discount_override = @quote.max_discount_override order.override_coupon_date = @quote.override_coupon_date order.override_coupon_date_without_limits = @quote.override_coupon_date_without_limits order.customer ||= @quote.opportunity.customer order.currency ||= @quote.currency order. ||= @quote. order.single_origin = @quote.single_origin order.bill_shipping_to_customer = @quote.bill_shipping_to_customer order.saturday_delivery = @quote.saturday_delivery order.signature_confirmation = @quote.signature_confirmation order.ltl_freight = @quote.ltl_freight order.ltl_freight_guaranteed = @quote.ltl_freight_guaranteed order.quote = @quote # Set defaults tracking email order.set_default_tracking_email(force: true) order.source = @quote.opportunity.source order.rma = @quote.rma order.override_line_lock = @quote.override_line_lock order.tax_exempt = @quote.tax_exempt unless @quote.tax_exempt.nil? end |
#append_deliveries(order) ⇒ Object
234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 |
# File 'app/services/quote/convert_to_order.rb', line 234 def append_deliveries(order) return if order.deliveries.present? # If our original quote delivery is not complete, then simply recalculate need_refresh = !(@quote.line_items.size == @quote.deliveries.sum { |d| d.line_items.size }) ErrorReporting.warning("Quote line items do not match its deliveries while converting quote: #{@quote.reference_number}, ID: #{@quote.id}, refreshing deliveries before conversion to order!") if need_refresh if need_refresh || @quote.deliveries.any? { |dq| dq.shipping_costs.empty? } # Full refresh for our order logger.info 'Empty shipping cost, refreshing deliveries' order.refresh_deliveries_quoting(true) logger.debug("Refresh Deliveries complete", delivery_count: @quote.deliveries.size) else # Iterate and copy @quote.deliveries.each do |dq| new_dq_attrs = dq.attributes.dup.symbolize_keys.except(:id, :quote_id, :selected_shipping_cost_id) logger.debug("Copying delivery", delivery_id: dq.id, description: dq.delivery_description) ndq = order.deliveries.create! new_dq_attrs # Clone shipping costs dq.shipping_costs.each do |osc| logger.debug("Adding shipping cost", shipping_cost_id: osc.id) nsc = osc.dup ndq.shipping_costs << nsc # nsc.save! end # Update shipping line in order orig_shipping_line = dq.line_items.detect(&:is_shipping?) # Remap our lines to the new delivery quotes order.line_items.active_lines.select { |li| li.original_delivery_id == dq.id }.each do |li| if li.is_shipping? existing_shipping_option_id = orig_shipping_line&.shipping_cost&.shipping_option_id li.shipping_cost = ndq.shipping_costs.detect { |sc| sc.shipping_option_id == existing_shipping_option_id } end li.delivery = ndq # remap li.save! end # Clone shipments dq.shipments.each do |shp| logger.info "Adding shipment #{shp.id} to new delivery" nshp = shp.dup nshp.container_code = nil # do this in case of quote pre-pack assigning a container code because duplicate container codes will prevent saving - Ramie ndq.shipments << nshp end # Ensure selected shipping cost is remapped to new delivery next unless dq.selected_shipping_cost&.shipping_option.present? ndq.reload ndq.selected_shipping_cost_id = ndq.shipping_costs.detect { |sc| sc.shipping_option_id == dq.selected_shipping_cost.shipping_option_id }&.id ndq.save end end order.deliveries.size end |
#append_quote_discounts(order, line_item_map) ⇒ Object
179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 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 225 226 227 228 229 230 231 232 |
# File 'app/services/quote/convert_to_order.rb', line 179 def append_quote_discounts(order, line_item_map) # Build a discount_id map from quote discounts to order discounts # We need to save discounts immediately so they have IDs for line_discounts to reference discount_map = {} @quote.discounts.active.each do |d| logger.info " * Copying discount id #{d.id}" if order.discounts.any? { |od| od.coupon_id == d.coupon_id } logger.info " * Skipping discount because coupon #{d.coupon_id} already in target discounts" else # only add discounts if coupon not already in there # Use dup to copy the discount, then save it immediately so it has an ID copy_discount = d.dup copy_discount.itemizable = order copy_discount.save! discount_map[d.id] = copy_discount.id logger.info " * Created discount id #{copy_discount.id} for coupon #{d.coupon_id}" end end # Now copy line_discounts and map them to the new order's line items # This is critical - without line_discounts, the discount amounts aren't actually applied to line items logger.info ' * Copying line_discounts to properly apply discount amounts' line_item_map.each do |quote_line_item_id, order_line_item| quote_line_item = @quote.line_items.find { |li| li.id == quote_line_item_id } next unless quote_line_item # Ensure the order line item is persisted before creating child records # This can happen if order.save! didn't cascade properly (e.g., existing order with validation quirks) order_line_item.save! unless order_line_item.persisted? quote_line_item.line_discounts.each do |ld| new_discount_id = discount_map[ld.discount_id] if new_discount_id.blank? # Fallback: find or create discount on order by coupon_id logger.info " * Discount #{ld.discount_id} not found in map, looking up by coupon_id #{ld.coupon_id}" order_discount = order.discounts.find_by(coupon_id: ld.coupon_id) if order_discount.blank? logger.info " * Creating missing discount for coupon #{ld.coupon_id}" order_discount = Discount.create!(itemizable: order, coupon_id: ld.coupon_id, amount: 0) end new_discount_id = order_discount.id end logger.info " * Creating line_discount: line_item #{order_line_item.id}, coupon #{ld.coupon_id}, amount #{ld.amount}, discount_id #{new_discount_id}" order_line_item.line_discounts.create!( amount: ld.amount, coupon_id: ld.coupon_id, discount_id: new_discount_id ) end end end |
#append_quote_lines(order) ⇒ Object
145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 |
# File 'app/services/quote/convert_to_order.rb', line 145 def append_quote_lines(order) # this will copy all lines from rooms line_item_map = {} # Track quote line item ID => new order line item mapping logger.tagged("ConvertToOrder:#{@quote.reference_number}/#{@quote.id}") do logger.debug("Converting quote to order", quote_id: @quote.id, delivery_count: @quote.deliveries.size, line_item_count: @quote.line_items.size) @quote.line_items.parents_only.each do |li| logger.tagged("Line:#{li.id}") do li_attrs = li.attributes.dup.symbolize_keys logger.debug("Processing line item", line_item_id: li.id, sku: li.sku) li_attrs = li_attrs.except(:id, :resource_type, :resource_id, :delivery_id, :tax_total, :tax_rate, :tax_type, :resource_tax_rate_id, :original_delivery_id, :children_count, :parent_id) li_attrs[:original_delivery_id] = li[:delivery_id] # Super important! li_attrs[:delivery_id] = nil # FIX: Ensure discounted_price is set if it was incorrectly left at 0 # This handles legacy line items created before the set_default_pricing fix if li_attrs[:discounted_price].to_f.zero? && li_attrs[:price].to_f.positive? li_attrs[:discounted_price] = li_attrs[:price] end logger.debug("Created new line item", original_id: li.id) new_li = order.line_items.build li_attrs line_item_map[li.id] = new_li # Track the mapping for discount copying end end end line_item_map # Return the mapping so discounts can be properly linked end |
#can_convert? ⇒ Boolean
21 22 23 24 |
# File 'app/services/quote/convert_to_order.rb', line 21 def can_convert? run_pre_check @errors.blank? end |
#clear_shipping_address(order) ⇒ Object
289 290 291 292 293 294 295 296 297 298 299 |
# File 'app/services/quote/convert_to_order.rb', line 289 def clear_shipping_address(order) # remove shipping lines order.line_items.active_lines.each do |li| # must do this first because we are in before_save context, even a destroyed line item will fail the validation on order save li.delivery_id = nil li.destroy if li.is_shipping? end order.shipping_address_id = nil order.recalculate_shipping = false order.do_not_detect_shipping = true end |
#convert(existing_order = nil, txid = nil) ⇒ Object
49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 |
# File 'app/services/quote/convert_to_order.rb', line 49 def convert(existing_order = nil, txid = nil) logger.info "To Order method called for quote #{@quote.id}, txid: #{txid}" raise QuoteUnpurchasable.new(@errors.to_sentence.capitalize) unless can_convert? order = existing_order || Order.new order.transaction do # This will save our petunias if a double submit happens from the same link, database will enforce Referential integrity order.txid = txid append_core_data(order) order.do_not_set_totals = true # Don't do any fancy calculation yet, need our discounts still order.save! line_item_map = append_quote_lines(order) order.do_not_set_totals = true # Don't do any fancy calculation yet order.save! existing_rc_ids = order.room_configuration_ids.to_set new_rooms = @quote.room_configurations.reject { |rc| existing_rc_ids.include?(rc.id) } order.room_configurations += new_rooms if new_rooms.any? # deliveries in quoting logger.debug " * quote has #{@quote.deliveries.count} deliveries via count and #{@quote.deliveries.length} via length" logger.debug " * @quote.deliveries.any?: #{@quote.deliveries.any?}, @quote.deliveries.present?: #{@quote.deliveries.present?}" if @quote.deliveries.any? and @quote.shipping_address.present? append_deliveries(order) else logger.info ' * quote has no delivery quotes, removing shipping address and shipping lines if any' clear_shipping_address(order) end append_quote_discounts(order, line_item_map) order.do_not_set_totals = nil # Reset from earlier order.force_total_reset = true # Definitely order.do_not_detect_shipping = true # Will have been done previously already order.recalculate_shipping = false # Copy over the min profit margin from the quote order.min_profit_markup = @quote.min_profit_markup yield(order) if block_given? order.save! order.commit_line_items(3.working.days.since(Date.current)) @quote.complete! unless @quote.complete? end order end |
#run_pre_check ⇒ Object
Todo, add other conditions here
27 28 29 30 31 32 33 34 35 |
# File 'app/services/quote/convert_to_order.rb', line 27 def run_pre_check @errors = [] inactive_items = quote.line_items.parents_only.joins(:item, :catalog_item).merge(CatalogItem.inactive).distinct.pluck('items.sku') @errors << "Quote contains discontinued items (#{inactive_items.join(', ')})" if inactive_items.present? @errors << 'Quote must be in a complete state' unless quote.complete? @errors << "Quote has already been converted and has active orders (#{quote.orders.active.pluck(:reference_number).join(', ')})" if quote.orders.active.present? @errors += self.class.check_quote_line_items_room_integrity(@quote) @errors end |