Class: Edi::Amazon::ConfirmMessageProcessor

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

Overview

Service object: confirm message processor.

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',
  # ShipEngine LTL carriers — values verified against Amazon's
  # acceptable-CarrierCode enumeration (amzn-base release_1_9 XSD,
  # checked 2026-07-18): 'Conway', 'Saia', 'R+L', 'Roadrunner', and
  # 'Fedex Freight' (Amazon's casing) are listed; SCACs (CNWY…) are not.
  ShipengineXpo: 'Conway',
  ShipengineSaia: 'Saia',
  ShipengineRlCarriers: 'R+L',
  ShipengineRoadrunner: 'Roadrunner',
  ShipengineFedExFreight: 'Fedex Freight',
  ShipengineFedExFreightEconomy: 'Fedex Freight'
}.freeze

Constants included from RequestIdentifiable

RequestIdentifiable::REQUEST_ID_HEADERS

Constants included from Edi::AddressAbbreviator

Edi::AddressAbbreviator::MAX_LENGTH

Instance Attribute Summary

Attributes inherited from BaseEdiService

#orchestrator

Attributes inherited from BaseService

#options

Instance Method Summary collapse

Methods inherited from BaseEdiService

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

Methods included from RequestIdentifiable

#partner_request_id

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, #tagged_logger

Constructor Details

This class inherits a constructor from Edi::BaseEdiService

Instance Method Details

#acknowledge_order(order) ⇒ Object



32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
# File 'app/services/edi/amazon/confirm_message_processor.rb', line 32

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



152
153
154
# File 'app/services/edi/amazon/confirm_message_processor.rb', line 152

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

#build_acknowledge_message(shipment, order_items) ⇒ 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
145
146
# File 'app/services/edi/amazon/confirm_message_processor.rb', line 115

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

  carrier_code = map_carrier_code_from_hw_carrier(effective_carrier)

  {
    packageDetail: {
      packageReferenceId: shipment.id.to_s,
      carrierCode: carrier_code,
      # carrierName is only required when carrierCode is 'Other'. Echo the
      # mapped code when we have one; otherwise send the internal carrier
      # name minus the 'Shipengine' integration prefix (buyer-facing).
      carrierName: carrier_code == 'Other' ? effective_carrier.to_s.delete_prefix('Shipengine') : carrier_code,
      shippingMethod: shipment.delivery.shipping_method_friendly,
      # display_tracking_number resolves the delivery-level PRO for ShipEngine
      # LTL (the per-pallet tracking_number is blank). Known gap: some LTL
      # carriers assign the PRO asynchronously after booking, and Amazon
      # requires trackingNumber — see
      # doc/tasks/202607181042_LTL_ASYNC_PRO_EDI_SHIP_CONFIRM_GAP.md.
      trackingNumber: shipment.display_tracking_number,
      shipDate: (shipment.delivery.shipped_date || shipment.delivery.ship_labeled_at || Time.zone.now)&.iso8601,
      orderItems: order_items
    },
    codCollectionMethod: "DirectPayment",
    marketplaceId: orchestrator.marketplace
  }
end

#cancel_order(order) ⇒ Object



156
157
158
# File 'app/services/edi/amazon/confirm_message_processor.rb', line 156

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

#confirm_invoice(invoice) ⇒ Object



160
161
162
# File 'app/services/edi/amazon/confirm_message_processor.rb', line 160

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

#map_carrier_code_from_hw_carrier(carrier) ⇒ Object



148
149
150
# File 'app/services/edi/amazon/confirm_message_processor.rb', line 148

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) ⇒ Hash{Shipment => Array<Hash>}

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:

  • (Hash{Shipment => Array<Hash>})

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



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

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



164
165
166
167
168
169
170
171
172
173
174
175
176
# File 'app/services/edi/amazon/confirm_message_processor.rb', line 164

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



109
110
111
112
113
# File 'app/services/edi/amazon/confirm_message_processor.rb', line 109

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