Class: Edi::Amazon::ShippingLabelPurchaser

Inherits:
MarketplaceLabelPurchaser show all
Defined in:
app/services/edi/amazon/shipping_label_purchaser.rb

Overview

Service for purchasing shipping labels via Amazon Buy Shipping API (Shipping V2)

This service handles the full label lifecycle:

  • Purchase a label using a rate from Amazon Buy Shipping
  • Attach the inline label PDF to the shipment
  • Populate tracking information
  • Cancel/void labels via Amazon's cancel endpoint

Amazon Buy Shipping returns label data inline during purchaseShipment,
unlike Walmart SWW which requires a separate download step.

Examples:

purchaser = Edi::Amazon::ShippingLabelPurchaser.new(shipment)
result = purchaser.purchase_label(rate)
if result.success
  puts "Tracking: #{result.tracking_number}"
end

Instance Attribute Summary collapse

Attributes inherited from MarketplaceLabelPurchaser

#delivery, #logger, #order, #shipment

Instance Method Summary collapse

Methods inherited from MarketplaceLabelPurchaser

#attach_label_pdf, #failure_result, for_delivery, for_order, for_shipment, #map_carrier_name, marketplace_carrier?, marketplace_name, #success_result, supports_marketplace_labels?

Constructor Details

#initialize(shipment, options = {}) ⇒ ShippingLabelPurchaser

Returns a new instance of ShippingLabelPurchaser.



25
26
27
28
# File 'app/services/edi/amazon/shipping_label_purchaser.rb', line 25

def initialize(shipment, options = {})
  super
  @orchestrator = Edi::Amazon::Orchestrator.new(order.edi_orchestrator_partner.to_sym)
end

Instance Attribute Details

#orchestratorObject (readonly)

Returns the value of attribute orchestrator.



23
24
25
# File 'app/services/edi/amazon/shipping_label_purchaser.rb', line 23

def orchestrator
  @orchestrator
end

Instance Method Details

#marketplace_nameString

Returns:

  • (String)


31
32
33
# File 'app/services/edi/amazon/shipping_label_purchaser.rb', line 31

def marketplace_name
  'Amazon'
end

#move_early_label_to_shipmentUpload?

Move early label PDF from order uploads to shipment uploads

Returns:



183
184
185
186
187
188
189
190
191
192
193
194
195
196
# File 'app/services/edi/amazon/shipping_label_purchaser.rb', line 183

def move_early_label_to_shipment
  early_upload = order.early_label_upload
  return nil unless early_upload

  early_upload.update!(
    resource_type: 'Shipment',
    resource_id: shipment.id,
    category: 'ship_label_pdf'
  )
  early_upload
rescue StandardError => e
  logger.warn("[AMZ-BS LabelPurchaser] Failed to move early label upload: #{e.message}")
  nil
end

#purchase_label(rate) ⇒ PurchaseResult

Purchase a shipping label using an Amazon Buy Shipping rate

Parameters:

  • rate (Hash)

    The rate hash from Shipping::AmazonSeller.find_rates
    Must include :amz_request_token and :amz_rate_id (or :rate_data with these)

Returns:



40
41
42
43
44
45
46
47
48
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
105
106
# File 'app/services/edi/amazon/shipping_label_purchaser.rb', line 40

def purchase_label(rate)
  logger.info("[AMZ-BS LabelPurchaser] Purchasing label for shipment #{shipment.id}, order: #{order.edi_po_number}")

  # Idempotency: if this shipment already has an Amazon shipment ID, a previous
  # purchase succeeded but post-purchase steps (label attach, state transition) may
  # have failed. Reuse the existing purchase instead of buying a second label.
  if shipment.amz_shipment_id.present?
    logger.info("[AMZ-BS LabelPurchaser] Shipment #{shipment.id} already has amz_shipment_id=#{shipment.amz_shipment_id}, recovering")
    return recover_existing_purchase
  end

  if order.has_early_purchased_label? && order.early_label_carrier.present?
    return use_early_purchased_label(rate)
  end

  rate = rate.respond_to?(:with_indifferent_access) ? rate.with_indifferent_access : rate
  rate_data = (rate[:rate_data] || rate)
  rate_data = rate_data.respond_to?(:with_indifferent_access) ? rate_data.with_indifferent_access : rate_data
  request_token = rate_data[:amz_request_token] || rate[:amz_request_token]
  rate_id = rate_data[:amz_rate_id] || rate[:amz_rate_id] || rate[:rate_id]

  return failure_result('Rate missing request_token (token may have expired, re-fetch rates)') unless request_token.present?
  return failure_result("Rate missing rate_id: #{rate.inspect}") unless rate_id.present?

  shipper = build_amazon_shipper
  label_opts = {
    request_token: request_token,
    rate_id: rate_id,
    carrier_id: rate_data[:amz_carrier_id] || rate[:amz_carrier_id],
    carrier_name: rate_data[:amz_carrier_name] || rate[:amz_carrier_name],
    service_name: rate_data[:amz_service_name] || rate[:amz_service_name],
    amz_supported_document_specifications: rate_data[:amz_supported_document_specifications] || rate[:amz_supported_document_specifications],
    available_value_added_services: rate_data[:available_value_added_services] ||
      rate[:available_value_added_services] ||
      rate_data.dig(:raw, :availableValueAddedServiceGroups) ||
      rate_data.dig('raw', 'availableValueAddedServiceGroups')
  }

  per_pkg = rate_data[:per_package_rates]
  if per_pkg.is_a?(Array) && per_pkg.size > 1
    label_opts[:per_package_rates] = per_pkg.map { |pr| pr.respond_to?(:symbolize_keys) ? pr.symbolize_keys : pr }
  end

  label_result = shipper.create_label(label_opts)

  unless label_result[:success]
    return failure_result(label_result[:error])
  end

  update_shipment_tracking(label_result[:tracking_number], label_result[:carrier], label_result[:shipment_id])
  (label_result)

  label_upload = attach_purchased_label(label_result)
  attach_additional_labels(label_result[:additional_labels]) if label_result[:additional_labels].present?

  success_result(
    label_id: label_result[:shipment_id],
    tracking_number: label_result[:tracking_number],
    carrier: label_result[:carrier],
    service_type: label_result[:service_name],
    label_upload: label_upload
  )
rescue StandardError => e
  logger.error("[AMZ-BS LabelPurchaser] Error: #{e.message}")
  logger.error(e.backtrace.first(5).join("\n"))
  failure_result(e.message)
end

#use_early_purchased_label(rate) ⇒ PurchaseResult

Use an early-purchased label instead of purchasing a new one
Moves the label from the order to the shipment

Parameters:

  • rate (Hash)

    The rate hash (used for logging)

Returns:



154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
# File 'app/services/edi/amazon/shipping_label_purchaser.rb', line 154

def use_early_purchased_label(rate)
  logger.info("[AMZ-BS LabelPurchaser] Using early-purchased label for shipment #{shipment.id}")

  tracking_number = order.early_label_tracking_number
  carrier = order.early_label_carrier
  label_id = order.early_label_sww_label_id # reuses same generic field
  service_type = order.early_label_service_type

  label_upload = move_early_label_to_shipment

  update_shipment_tracking(tracking_number, carrier, label_id)
  (tracking_number, carrier, service_type, label_id)

  order.update!(purchase_label_early: false)

  success_result(
    label_id: label_id,
    tracking_number: tracking_number,
    carrier: carrier,
    service_type: service_type,
    label_upload: label_upload
  )
rescue StandardError => e
  logger.error("[AMZ-BS LabelPurchaser] Error using early-purchased label: #{e.message}")
  failure_result("Failed to use early-purchased label: #{e.message}. Please try purchasing a new label.")
end

#validate_order!Object (protected)

Raises:

  • (ArgumentError)


200
201
202
203
# File 'app/services/edi/amazon/shipping_label_purchaser.rb', line 200

def validate_order!
  super
  raise ArgumentError, 'Shipment is not for an Amazon marketplace order' unless order.edi_orchestrator_partner&.start_with?('amazon_seller')
end

#void_labelHash

Cancel a previously purchased Amazon Buy Shipping label
Amazon uses the shipment_id to cancel (not carrier + tracking like Walmart)

Returns:

  • (Hash)

    Result with :success and :error keys



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
144
145
146
147
# File 'app/services/edi/amazon/shipping_label_purchaser.rb', line 112

def void_label
  amz_shipment_id = shipment.amz_shipment_id

  unless amz_shipment_id.present?
    logger.warn("[AMZ-BS LabelPurchaser] No amz_shipment_id on shipment #{shipment.id} — clearing all label fields")
    shipment.update(
      tracking_number: nil,
      carrier: nil,
      amz_shipment_id: nil,
      amz_metadata: nil
    )
    return { success: true, error: nil }
  end

  logger.info("[AMZ-BS LabelPurchaser] Canceling shipment #{amz_shipment_id} for shipment #{shipment.id}")

  amz_client = ShipWithAmazon.new(orchestrator)
  result = amz_client.cancel_shipment(amz_shipment_id)

  unless result.success
    return { success: false, error: result.error }
  end

  cancel_additional_package_shipments(amz_client)

  shipment.update(
    tracking_number: nil,
    carrier: nil,
    amz_shipment_id: nil,
    amz_metadata: nil
  )
  { success: true, error: nil }
rescue StandardError => e
  logger.error("[AMZ-BS LabelPurchaser] Void error: #{e.message}")
  { success: false, error: e.message }
end