Class: Edi::Amazon::ConfirmMessageProcessor

Inherits:
BaseEdiService show all
Defined in:
app/services/edi/amazon/confirm_message_processor.rb

Constant Summary collapse

CARRIER_TO_CARRIER_CODE_MAP_HASH =
{
  'Canadapost': 'Canada Post',
  'Canpar': 'Canpar',
  'FedEx': 'FedEx',
  'Purolator': 'PUROLATOR',
  'Ups': 'UPS',
  'UPS': 'UPS',
  'Usps': 'USPS',
  'USPS': 'USPS',
  'RlCarriers': 'R+L',
  'AmazonSeller': 'Amazon'
}

Constants included from Edi::AddressAbbreviator

Edi::AddressAbbreviator::MAX_LENGTH

Instance Attribute Summary

Attributes inherited from BaseEdiService

#orchestrator

Instance Method Summary collapse

Methods inherited from BaseEdiService

#duplicate_po_already_notified?, #initialize, #mark_duplicate_po_as_notified, #report_order_creation_issues, #safe_process_edi_communication_log

Methods included from Edi::AddressAbbreviator

#abbreviate_street, #collect_street_originals, #record_address_abbreviation_notes

Methods inherited from BaseService

#initialize, #log_debug, #log_error, #log_info, #log_warning, #logger, #options, #tagged_logger

Constructor Details

This class inherits a constructor from Edi::BaseEdiService

Instance Method Details

#acknowledge_order(order) ⇒ Object



21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
# File 'app/services/edi/amazon/confirm_message_processor.rb', line 21

def acknowledge_order(order)
  delivery = order.deliveries.first
  # Ship confirm when we label via Heatwave (including Amazon Buy Shipping labels purchased through our system).
  # When using Veeqo or Buy Amazon Shipping directly through Seller Central, it already handles the ship confirmation.
  return unless delivery.ship_labeled_via_heatwave?

  # Buy Shipping V2 purchaseShipment already notifies Amazon the order is shipped.
  # A separate confirmShipment is redundant and fails with "PackageToUpdateNotFound".
  if delivery.carrier == 'AmazonSeller'
    Rails.logger.info("[Amazon ConfirmMessageProcessor] Skipping confirmShipment for AMZBS order #{order.id} — purchaseShipment already confirmed")
    return
  end

  order_hash = JSON.parse(order.edi_original_order_message).with_indifferent_access
  shipment_item_groups = map_order_items_to_shipments(delivery, order_hash)
  return if shipment_item_groups.empty?

  shipment_item_groups.map do |shipment, order_items|
    m = build_acknowledge_message(shipment, order_items)
    process(m, :order_acknowledge, order)
  end
end

#back_order(order) ⇒ Object



133
134
135
# File 'app/services/edi/amazon/confirm_message_processor.rb', line 133

def back_order(order)
  # do nothing for Amazon SC.
end

#build_acknowledge_message(shipment, order_items) ⇒ Object



106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
# File 'app/services/edi/amazon/confirm_message_processor.rb', line 106

def build_acknowledge_message(shipment, order_items)
  # For Amazon Buy Shipping labels, use the actual carrier from metadata
  effective_carrier = if shipment.carrier == 'AmazonSeller' && shipment.respond_to?(:amz_carrier) && shipment.amz_carrier.present?
                        shipment.amz_carrier
                      else
                        shipment.carrier
                      end

  {
    packageDetail: {
      packageReferenceId: shipment.id.to_s,
      carrierCode: map_carrier_code_from_hw_carrier(effective_carrier),
      carrierName: effective_carrier,
      shippingMethod: shipment.delivery.shipping_method_friendly,
      trackingNumber: shipment.tracking_number,
      shipDate: (shipment.delivery.shipped_date || shipment.delivery.ship_labeled_at || Time.now)&.iso8601,
      orderItems: order_items
    },
    codCollectionMethod: "DirectPayment",
    marketplaceId: orchestrator.marketplace
  }
end

#cancel_order(order) ⇒ Object



137
138
139
# File 'app/services/edi/amazon/confirm_message_processor.rb', line 137

def cancel_order(order)
  # do nothing for Amazon SC.
end

#confirm_invoice(invoice) ⇒ Object



141
142
143
# File 'app/services/edi/amazon/confirm_message_processor.rb', line 141

def confirm_invoice(invoice)
  # do nothing for Amazon SC.
end

#map_carrier_code_from_hw_carrier(carrier) ⇒ Object



129
130
131
# File 'app/services/edi/amazon/confirm_message_processor.rb', line 129

def map_carrier_code_from_hw_carrier(carrier)
  CARRIER_TO_CARRIER_CODE_MAP_HASH[carrier.to_sym] || 'Other'
end

#map_order_items_to_shipments(delivery, order_hash) ⇒ Object

Maps each Amazon order item to exactly one shipment/tracking number, then groups
by shipment. This is item-centric rather than shipment-centric, which correctly
handles multibox kits (single item shipped across multiple packages).

The SP-API confirmShipment endpoint requires each orderItemId to appear in exactly
one package confirmation. Sending the same item in multiple calls causes 400 errors
("order already fulfilled"). This method ensures every item gets confirmed exactly once.

Algorithm:

  1. For each completed shipment, count how many components of each parent line item it contains
  2. Assign each parent to the shipment with the most components (its "primary" package)
  3. Group parents by their primary shipment

Returns: { Shipment => [{ orderItemId:, quantity: }, ...] }



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
# File 'app/services/edi/amazon/confirm_message_processor.rb', line 58

def map_order_items_to_shipments(delivery, order_hash)
  all_shipments = delivery.shipments.completed.top_level
                         .includes(shipment_contents: { line_item: :parent }).to_a
  return {} if all_shipments.empty?

  shipment_by_id = all_shipments.index_by(&:id)

  # Count components of each parent line item in each shipment.
  # For non-kit items (no parent), the line item itself acts as the parent.
  parent_shipment_component_counts = Hash.new { |h, k| h[k] = Hash.new(0) }
  all_shipments.each do |shipment|
    shipment.shipment_contents.each do |sc|
      parent_id = (sc.line_item.parent || sc.line_item).id
      parent_shipment_component_counts[parent_id][shipment.id] += sc.quantity
    end
  end

  parent_line_items = delivery.line_items.goods.parents_only.to_a
  groups = Hash.new { |h, k| h[k] = [] }

  parent_line_items.each do |parent_li|
    counts = parent_shipment_component_counts[parent_li.id]
    next if counts.empty?

    best_shipment_id = counts.max_by { |_sid, count| count }.first
    shipment = shipment_by_id[best_shipment_id]
    next unless shipment

    order_item_id = resolve_amazon_order_item_id(parent_li, order_hash)
    next unless order_item_id

    groups[shipment] << {
      orderItemId: order_item_id,
      quantity: parent_li.quantity
    }
  end

  groups
end

#process(confirm_message, category, order = nil) ⇒ Object



145
146
147
148
149
150
151
152
153
154
155
156
157
# File 'app/services/edi/amazon/confirm_message_processor.rb', line 145

def process(confirm_message, category, order = nil)
  edi_log = nil
  EdiCommunicationLog.transaction do
    edi_log = EdiCommunicationLog.create! partner: orchestrator.partner,
                                        category: category,
                                        data: confirm_message.to_json,
                                        data_type: 'json',
                                        file_info: { lines_confirmed: confirm_message[:packageDetail][:orderItems].size },
                                        transmit_datetime: Time.current
  end
  edi_log.edi_documents.create!(order: order) if order.present?
  edi_log
end

#resolve_amazon_order_item_id(parent_li, order_hash) ⇒ Object



98
99
100
101
102
103
104
# File 'app/services/edi/amazon/confirm_message_processor.rb', line 98

def resolve_amazon_order_item_id(parent_li, order_hash)
  order_item_id = parent_li.edi_reference
  if order_item_id&.include?(order_hash[:AmazonOrderId])
    order_item_id = order_hash[:OrderItems][(parent_li.edi_line_number || 1) - 1]&.dig(:OrderItemId)
  end
  order_item_id
end