Class: Edi::Amazon::ShipWithAmazon

Inherits:
Object
  • Object
show all
Defined in:
app/services/edi/amazon/ship_with_amazon.rb

Overview

Client for Amazon's Buy Shipping API (Shipping V2)
Enables purchasing shipping labels with Amazon Buy Shipping protections

See: https://developer-docs.shipping.amazon.com/apis/docs/shipping-api-v2-reference

Flow:

  1. get_rates - Get eligible shipping service offerings for a shipment
  2. purchase_shipment - Purchase a label using a rate from get_rates
  3. get_shipment_documents - Download the label PDF
  4. cancel_shipment - Void/cancel a purchased shipment
  5. get_tracking - Get tracking info for a purchased shipment

Defined Under Namespace

Classes: CancelResult, DocumentResult, PurchaseResult, RatesResult, TrackingResult

Constant Summary collapse

BASE_PATH =
'/shipping/v2'
BUSINESS_ID_MAP =

Amazon Shipping business IDs per marketplace region

{
  amazon_seller_central_us: 'AmazonShipping_US',
  amazon_seller_central_ca: 'AmazonShipping_US', # CA uses NA endpoint with US business ID
  amazon_seller_central_uk: 'AmazonShipping_UK',
  amazon_seller_central_fr: 'AmazonShipping_FR',
  amazon_seller_central_de: 'AmazonShipping_FR', # DE routes through FR business
  amazon_seller_central_es: 'AmazonShipping_ES',
  amazon_seller_central_it: 'AmazonShipping_IT'
}.freeze

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(orchestrator) ⇒ ShipWithAmazon

Returns a new instance of ShipWithAmazon.



49
50
51
52
53
54
# File 'app/services/edi/amazon/ship_with_amazon.rb', line 49

def initialize(orchestrator)
  @orchestrator = orchestrator
  @transport = Transport::HttpSellerApiConnection.new(profile: orchestrator.transporter_profile)
  @logger = Rails.logger
  @api_log = []
end

Instance Attribute Details

#api_logObject (readonly)

Returns the value of attribute api_log.



18
19
20
# File 'app/services/edi/amazon/ship_with_amazon.rb', line 18

def api_log
  @api_log
end

#loggerObject (readonly)

Returns the value of attribute logger.



18
19
20
# File 'app/services/edi/amazon/ship_with_amazon.rb', line 18

def logger
  @logger
end

#orchestratorObject (readonly)

Returns the value of attribute orchestrator.



18
19
20
# File 'app/services/edi/amazon/ship_with_amazon.rb', line 18

def orchestrator
  @orchestrator
end

#transportObject (readonly)

Returns the value of attribute transport.



18
19
20
# File 'app/services/edi/amazon/ship_with_amazon.rb', line 18

def transport
  @transport
end

Instance Method Details

#cancel_shipment(shipment_id) ⇒ CancelResult

PUT /shipping/v2/shipments/shipmentId/cancel
Cancels a purchased shipment

Parameters:

  • shipment_id (String)

    The shipment ID to cancel

Returns:



216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
# File 'app/services/edi/amazon/ship_with_amazon.rb', line 216

def cancel_shipment(shipment_id)
  url = build_url("/shipments/#{shipment_id}/cancel")

  logger.info("[AMZ-BS] Canceling shipment: #{shipment_id}")

  result = transport.send_data('', url, 'PUT', shipping_headers)

  if result[:success]
    log_api_call(operation: 'cancel_shipment', url: url, method: 'PUT',
                 http_result: result[:http_result], success: true)
    CancelResult.new(success: true, error: nil)
  else
    error_msg = extract_error(result[:http_result])
    log_api_call(operation: 'cancel_shipment', url: url, method: 'PUT',
                 http_result: result[:http_result], success: false, error: error_msg)
    CancelResult.new(success: false, error: error_msg)
  end
rescue StandardError => e
  logger.error("[AMZ-BS] cancel_shipment error: #{e.message}")
  log_api_call(operation: 'cancel_shipment', url: url, method: 'PUT',
               success: false, error: e.message)
  CancelResult.new(success: false, error: e.message)
end

#get_rates(options) ⇒ RatesResult

POST /shipping/v2/shipments/rates
Returns eligible shipping service offerings for a shipment

For on-Amazon orders (channelType: AMAZON), shipTo is not required.
Amazon already knows the delivery address from the order.

Parameters:

  • options (Hash)

    Rate request options

Options Hash (options):

  • :amazon_order_id (String)

    The Amazon order ID

  • :ship_from (Hash)

    Origin address

  • :packages (Array<Hash>)

    Package dimensions, weights, and items

  • :ship_date (String)

    Optional ship date (ISO 8601)

Returns:



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
107
108
109
# File 'app/services/edi/amazon/ship_with_amazon.rb', line 68

def get_rates(options)
  url = build_url('/shipments/rates')
  payload = build_rates_payload(options)

  logger.info("[AMZ-BS] Requesting rates for order: #{options[:amazon_order_id]}")
  logger.debug("[AMZ-BS] Rates payload: #{payload.to_json}")

  result = transport.send_data(payload.to_json, url, 'POST', shipping_headers)

  if result[:success]
    data = parse_response(result[:http_result])
    payload_data = data[:payload] || data
    rates = normalize_rates(payload_data[:rates] || [])
    ineligible = payload_data[:ineligibleRates] || []

    logger.info("[AMZ-BS] Got #{rates.size} eligible rate(s), #{ineligible.size} ineligible")
    log_api_call(operation: 'get_rates', url: url, method: 'POST',
                 request_payload: payload.to_json, http_result: result[:http_result],
                 success: true)

    RatesResult.new(
      success: true,
      request_token: payload_data[:requestToken],
      rates: rates,
      ineligible_rates: ineligible,
      error: nil
    )
  else
    error_msg = extract_error(result[:http_result])
    logger.error("[AMZ-BS] get_rates failed: #{error_msg}")
    log_api_call(operation: 'get_rates', url: url, method: 'POST',
                 request_payload: payload.to_json, http_result: result[:http_result],
                 success: false, error: error_msg)
    RatesResult.new(success: false, request_token: nil, rates: [], ineligible_rates: [], error: error_msg)
  end
rescue StandardError => e
  logger.error("[AMZ-BS] get_rates error: #{e.message}")
  logger.error("[AMZ-BS] Backtrace: #{e.backtrace.first(5).join("\n")}")
  log_api_call(operation: 'get_rates', url: url, method: 'POST',
               request_payload: payload&.to_json, success: false, error: e.message)
  RatesResult.new(success: false, request_token: nil, rates: [], ineligible_rates: [], error: e.message)
end

#get_shipment_documents(shipment_id, package_client_reference_id, format: 'PDF') ⇒ DocumentResult

GET /shipping/v2/shipments/shipmentId/documents
Returns shipping documents (label PDF) for a package

Parameters:

  • shipment_id (String)

    The shipment ID from purchase_shipment

  • package_client_reference_id (String)

    Package reference from get_rates

  • format (String) (defaults to: 'PDF')

    Document format: PDF, PNG, or ZPL (default: PDF)

Returns:



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

def get_shipment_documents(shipment_id, package_client_reference_id, format: 'PDF')
  url = build_url("/shipments/#{shipment_id}/documents")
  url += "?packageClientReferenceId=#{CGI.escape(package_client_reference_id)}&format=#{CGI.escape(format)}"

  logger.info("[AMZ-BS] Getting documents for shipment: #{shipment_id}")

  result = transport.send_data('', url, 'GET', shipping_headers)

  if result[:success]
    data = parse_response(result[:http_result])
    payload_data = data[:payload] || data
    pkg_doc = payload_data.dig(:packageDocumentDetail, :packageDocuments)
    label_data = extract_label_from_documents(pkg_doc)

    log_api_call(operation: 'get_shipment_documents', url: url, method: 'GET',
                 http_result: result[:http_result], success: true)
    DocumentResult.new(success: true, label_data: label_data, format: format, error: nil)
  else
    error_msg = extract_error(result[:http_result])
    log_api_call(operation: 'get_shipment_documents', url: url, method: 'GET',
                 http_result: result[:http_result], success: false, error: error_msg)
    DocumentResult.new(success: false, label_data: nil, format: nil, error: error_msg)
  end
rescue StandardError => e
  logger.error("[AMZ-BS] get_shipment_documents error: #{e.message}")
  log_api_call(operation: 'get_shipment_documents', url: url, method: 'GET',
               success: false, error: e.message)
  DocumentResult.new(success: false, label_data: nil, format: nil, error: e.message)
end

#get_tracking(tracking_id, carrier_id) ⇒ TrackingResult

GET /shipping/v2/tracking
Returns tracking information for a purchased shipment

Parameters:

  • tracking_id (String)

    The carrier tracking number

  • carrier_id (String)

    The Amazon carrier ID (e.g., 'AMZN_US')

Returns:



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

def get_tracking(tracking_id, carrier_id)
  url = build_url("/tracking?trackingId=#{CGI.escape(tracking_id)}&carrierId=#{CGI.escape(carrier_id)}")

  logger.info("[AMZ-BS] Getting tracking for: #{tracking_id}")

  result = transport.send_data('', url, 'GET', shipping_headers)

  if result[:success]
    data = parse_response(result[:http_result])
    payload_data = data[:payload] || data

    log_api_call(operation: 'get_tracking', url: url, method: 'GET',
                 http_result: result[:http_result], success: true)
    TrackingResult.new(
      success: true,
      tracking_id: payload_data[:trackingId],
      status: payload_data.dig(:summary, :status),
      event_history: payload_data[:eventHistory] || [],
      promised_delivery_date: payload_data[:promisedDeliveryDate],
      error: nil
    )
  else
    error_msg = extract_error(result[:http_result])
    log_api_call(operation: 'get_tracking', url: url, method: 'GET',
                 http_result: result[:http_result], success: false, error: error_msg)
    TrackingResult.new(success: false, error: error_msg)
  end
rescue StandardError => e
  logger.error("[AMZ-BS] get_tracking error: #{e.message}")
  log_api_call(operation: 'get_tracking', url: url, method: 'GET',
               success: false, error: e.message)
  TrackingResult.new(success: false, error: e.message)
end

#purchase_shipment(options) ⇒ PurchaseResult

POST /shipping/v2/shipments
Purchases a shipping service and returns label documents

Must be called within 10 minutes of get_rates for EXTERNAL channel.
For AMAZON channel, the window may be longer but should still be prompt.

Parameters:

  • options (Hash)

    Purchase options

Options Hash (options):

  • :request_token (String)

    Token from get_rates response

  • :rate_id (String)

    Rate ID from get_rates response

  • :document_specification (Hash)

    Label format preferences

Returns:



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

def purchase_shipment(options)
  url = build_url('/shipments')
  payload = build_purchase_payload(options)

  logger.info("[AMZ-BS] Purchasing shipment, rate_id: #{options[:rate_id]}")
  logger.debug("[AMZ-BS] Purchase payload: #{payload.to_json}")

  result = transport.send_data(payload.to_json, url, 'POST', shipping_headers)

  if result[:success]
    data = parse_response(result[:http_result])
    payload_data = data[:payload] || data

    shipment_id = payload_data[:shipmentId]
    promise = payload_data[:promise]

    pkg_detail = payload_data[:packageDocumentDetails]&.first || {}
    tracking_number = pkg_detail[:trackingId]
    label_data = extract_label_from_documents(pkg_detail[:packageDocuments])

    logger.info("[AMZ-BS] Shipment purchased: #{shipment_id}, tracking: #{tracking_number}")
    log_api_call(operation: 'purchase_shipment', url: url, method: 'POST',
                 request_payload: payload.to_json, http_result: result[:http_result],
                 success: true)

    PurchaseResult.new(
      success: true,
      shipment_id: shipment_id,
      tracking_number: tracking_number,
      carrier_id: options[:carrier_id],
      carrier_name: options[:carrier_name],
      service_name: options[:service_name],
      label_data: label_data,
      promise: promise,
      error: nil
    )
  else
    error_msg = extract_error(result[:http_result])
    logger.error("[AMZ-BS] purchase_shipment failed: #{error_msg}")
    log_api_call(operation: 'purchase_shipment', url: url, method: 'POST',
                 request_payload: payload.to_json, http_result: result[:http_result],
                 success: false, error: error_msg)
    PurchaseResult.new(success: false, error: error_msg)
  end
rescue StandardError => e
  logger.error("[AMZ-BS] purchase_shipment error: #{e.message}")
  logger.error("[AMZ-BS] Backtrace: #{e.backtrace.first(5).join("\n")}")
  log_api_call(operation: 'purchase_shipment', url: url, method: 'POST',
               request_payload: payload&.to_json, success: false, error: e.message)
  PurchaseResult.new(success: false, error: e.message)
end