Class: Edi::Walmart::ShipWithWalmart

Inherits:
Object
  • Object
show all
Defined in:
app/services/edi/walmart/ship_with_walmart.rb

Overview

Client for Walmart's "Ship with Walmart" (SWW) API
Enables purchasing discounted shipping labels directly from Walmart

See: https://developer.walmart.com/us-marketplace/docs/buy-shipping

Flow:

  1. get_carriers - Get list of supported carriers (USPS, FedEx)
  2. get_package_types - Get package types for a specific carrier
  3. get_shipping_estimates - Get rate quotes for a shipment
  4. create_label - Purchase a shipping label
  5. download_label - Download the label PDF
  6. discard_label - Void/cancel a label if needed

Defined Under Namespace

Classes: CarriersResult, DiscardResult, DownloadResult, EstimatesResult, LabelResult, LabelsResult, PackageTypesResult

Constant Summary collapse

BASE_PATH =
'/v3/shipping/labels'

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(orchestrator) ⇒ ShipWithWalmart

Returns a new instance of ShipWithWalmart.



41
42
43
44
45
46
# File 'app/services/edi/walmart/ship_with_walmart.rb', line 41

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

Instance Attribute Details

#api_logObject (readonly)

Returns accumulated API call log entries for this client instance
Each entry contains: timestamp, operation, url, method, request_payload, response_status, response_body, success, error



50
51
52
# File 'app/services/edi/walmart/ship_with_walmart.rb', line 50

def api_log
  @api_log
end

#loggerObject (readonly)

Returns the value of attribute logger.



17
18
19
# File 'app/services/edi/walmart/ship_with_walmart.rb', line 17

def logger
  @logger
end

#orchestratorObject (readonly)

Returns the value of attribute orchestrator.



17
18
19
# File 'app/services/edi/walmart/ship_with_walmart.rb', line 17

def orchestrator
  @orchestrator
end

#transportObject (readonly)

Returns the value of attribute transport.



17
18
19
# File 'app/services/edi/walmart/ship_with_walmart.rb', line 17

def transport
  @transport
end

Instance Method Details

#create_label(options) ⇒ LabelResult

POST /v3/shipping/labels
Create/purchase a shipping label

Parameters:

  • options (Hash)

    Label creation options

Options Hash (options):

  • :purchase_order_id (String)

    Walmart PO number

  • :from_address (Hash)

    Origin address

  • :to_address (Hash)

    Destination address

  • :package (Hash)

    Package dimensions and weight

  • :carrier_id (String)

    Carrier (e.g., 'USPS', 'FEDEX')

  • :service_type (String)

    Service type from estimate

Returns:



170
171
172
173
174
175
176
177
178
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
233
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
288
289
# File 'app/services/edi/walmart/ship_with_walmart.rb', line 170

def create_label(options)
  url = build_url('')
  payload = build_label_payload(options)

  logger.info("[SWW] Creating shipping label for PO: #{options[:purchase_order_id]}")
  logger.info("[SWW] Label options: #{options.inspect}")
  logger.info("[SWW] Label payload: #{payload.to_json}")

  # Per Walmart API docs, Accept header determines response format
  # For label creation, we want JSON to get labelId and trackingNumber
  headers = { 'Accept' => 'application/json' }
  result = transport.send_data(payload.to_json, url, 'POST', headers)

  # Log full response for debugging
  if result[:http_result]
    logger.info("[SWW] Response status: #{result[:http_result].code}")
    logger.info("[SWW] Response headers: #{result[:http_result].headers.inspect}")
    logger.info("[SWW] Response body (first 2000 chars): #{result[:http_result].body.to_s.first(2000)}")
    logger.info("[SWW] Full payload sent: #{payload.to_json}")
  end

  if result[:success]
    response_data = parse_response(result[:http_result])
    # Response is wrapped in a "data" object
    data = response_data[:data] || response_data

    log_api_call(
      operation: 'create_label',
      url: url,
      method: 'POST',
      request_payload: payload,
      http_result: result[:http_result],
      success: true,
      error: nil
    )

    LabelResult.new(
      success: true,
      label_id: data[:labelId] || data[:label_id], # May not be present in response
      tracking_number: data[:trackingNo] || data[:trackingNumber],
      carrier: data[:carrierName] || data[:carrier],
      service_type: data[:carrierServiceType] || data[:serviceType],
      error: nil
    )
  else
    # Check if this is a 409 error - sometimes Walmart returns label data even with 409
    http_code = result[:http_result]&.code&.to_i
    if http_code == 409
      # Try to parse response body - sometimes 409 responses include label data
      begin
        response_data = parse_response(result[:http_result])
        data = response_data[:data] || response_data

        # Check if we got label data despite 409 status
        if data[:trackingNo].present? || data[:trackingNumber].present?
          logger.info("[SWW] 409 response contains label data: tracking=#{data[:trackingNo] || data[:trackingNumber]}, carrier=#{data[:carrierName] || data[:carrier]}")
          log_api_call(
            operation: 'create_label',
            url: url,
            method: 'POST',
            request_payload: payload,
            http_result: result[:http_result],
            success: true,
            error: '409 but contained label data'
          )
          return LabelResult.new(
            success: true,
            label_id: data[:labelId] || data[:label_id],
            tracking_number: data[:trackingNo] || data[:trackingNumber],
            carrier: data[:carrierName] || data[:carrier],
            service_type: data[:carrierServiceType] || data[:serviceType],
            error: nil
          )
        end
      rescue StandardError => e
        logger.warn("[SWW] Could not parse 409 response body: #{e.message}")
      end
    end

    error_msg = extract_error(result[:http_result])
    logger.error("[SWW] Label creation failed: #{error_msg}")
    log_api_call(
      operation: 'create_label',
      url: url,
      method: 'POST',
      request_payload: payload,
      http_result: result[:http_result],
      success: false,
      error: error_msg
    )
    LabelResult.new(
      success: false,
      label_id: nil,
      tracking_number: nil,
      carrier: nil,
      service_type: nil,
      error: error_msg
    )
  end
rescue StandardError => e
  logger.error("[SWW] create_label error: #{e.message}")
  logger.error("[SWW] Backtrace: #{e.backtrace.first(5).join("\n")}")
  log_api_call(
    operation: 'create_label',
    url: url,
    method: 'POST',
    request_payload: payload,
    http_result: nil,
    success: false,
    error: e.message
  )
  LabelResult.new(
    success: false,
    label_id: nil,
    tracking_number: nil,
    carrier: nil,
    service_type: nil,
    error: e.message
  )
end

#discard_label(carrier, tracking_number) ⇒ DiscardResult

DELETE /v3/shipping/labels/carriers/carrierShortName/trackings/trackingNo
Void/discard a shipping label

Per Walmart API docs, labels are voided by carrier + tracking number

Parameters:

  • carrier (String)

    Carrier short name (e.g., 'USPS', 'FEDEX')

  • tracking_number (String)

    The tracking number to void

Returns:



408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
# File 'app/services/edi/walmart/ship_with_walmart.rb', line 408

def discard_label(carrier, tracking_number)
  carrier_short = normalize_carrier_name(carrier)
  url = build_url("/carriers/#{carrier_short}/trackings/#{tracking_number}")

  logger.info("[SWW] Discarding label for carrier: #{carrier_short}, tracking: #{tracking_number}")

  result = transport.send_data('', url, 'DELETE')

  if result[:success]
    log_api_call(
      operation: 'discard_label',
      url: url,
      method: 'DELETE',
      request_payload: { carrier: carrier_short, tracking_number: tracking_number },
      http_result: result[:http_result],
      success: true,
      error: nil
    )
    DiscardResult.new(success: true, error: nil)
  else
    error_msg = extract_error(result[:http_result])
    log_api_call(
      operation: 'discard_label',
      url: url,
      method: 'DELETE',
      request_payload: { carrier: carrier_short, tracking_number: tracking_number },
      http_result: result[:http_result],
      success: false,
      error: error_msg
    )
    DiscardResult.new(success: false, error: error_msg)
  end
rescue StandardError => e
  logger.error("[SWW] discard_label error: #{e.message}")
  log_api_call(
    operation: 'discard_label',
    url: url,
    method: 'DELETE',
    request_payload: { carrier: carrier_short, tracking_number: tracking_number },
    http_result: nil,
    success: false,
    error: e.message
  )
  DiscardResult.new(success: false, error: e.message)
end

#download_label(carrier, tracking_number) ⇒ DownloadResult

GET /v3/shipping/labels/carriers/carrierShortName/trackings/trackingNo
Download a shipping label PDF

Per Walmart API docs, labels are retrieved by carrier + tracking number
Accept header determines format: 'application/json,application/pdf' for PDF

Parameters:

  • carrier (String)

    Carrier short name (e.g., 'USPS', 'FEDEX')

  • tracking_number (String)

    The tracking number from create_label

Returns:



300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
# File 'app/services/edi/walmart/ship_with_walmart.rb', line 300

def download_label(carrier, tracking_number)
  carrier_short = normalize_carrier_name(carrier)
  url = build_url("/carriers/#{carrier_short}/trackings/#{tracking_number}")

  logger.info("[SWW] Downloading label for carrier: #{carrier_short}, tracking: #{tracking_number}")

  # Request PDF format per Walmart API docs
  # Accept header must be one of: application/pdf, image/png, or application/zpl
  # Cannot include application/json in the Accept header for download endpoint
  # Note: transport layer expects lowercase 'accept' key
  headers = { 'accept' => 'application/pdf' }
  result = transport.send_data('', url, 'GET', headers)

  if result[:success]
    http_result = result[:http_result]
    content_type = http_result.headers['Content-Type'] || 'application/pdf'

    # If response is JSON, it might contain base64-encoded label data
    # Otherwise, it's a binary PDF/image/ZPL file
    if content_type.include?('application/json')
      begin
        data = parse_response(http_result)
        label_data = data[:labelData] || data[:label] || http_result.body.to_s
      rescue JSON::ParserError
        # If JSON parsing fails, treat as binary data
        label_data = http_result.body.to_s
      end
    else
      # Binary format (PDF, PNG, ZPL) - return raw body
      label_data = http_result.body.to_s
    end

    # Don't log the actual PDF binary data, just metadata
    log_api_call(
      operation: 'download_label',
      url: url,
      method: 'GET',
      request_payload: { carrier: carrier_short, tracking_number: tracking_number },
      http_result: nil, # Don't store binary PDF in log
      success: true,
      error: nil
    )
    @api_log.last[:response_status] = http_result.code.to_i
    @api_log.last[:response_body] = "(PDF binary data, #{label_data.bytesize} bytes)"

    DownloadResult.new(
      success: true,
      label_data: label_data,
      content_type: content_type,
      error: nil
    )
  else
    error_msg = extract_error(result[:http_result])
    log_api_call(
      operation: 'download_label',
      url: url,
      method: 'GET',
      request_payload: { carrier: carrier_short, tracking_number: tracking_number },
      http_result: result[:http_result],
      success: false,
      error: error_msg
    )
    DownloadResult.new(success: false, label_data: nil, content_type: nil, error: error_msg)
  end
rescue StandardError => e
  logger.error("[SWW] download_label error: #{e.message}")
  log_api_call(
    operation: 'download_label',
    url: url,
    method: 'GET',
    request_payload: { carrier: carrier_short, tracking_number: tracking_number },
    http_result: nil,
    success: false,
    error: e.message
  )
  DownloadResult.new(success: false, label_data: nil, content_type: nil, error: e.message)
end

#download_label_by_id(label_id, purchase_order_id = nil) ⇒ DownloadResult

Legacy method for backward compatibility when only label_id is available
Uses get_labels_by_po to find carrier and tracking, then downloads

Parameters:

  • label_id (String)

    The label ID from create_label

  • purchase_order_id (String) (defaults to: nil)

    Optional PO ID for label lookup

Returns:



384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
# File 'app/services/edi/walmart/ship_with_walmart.rb', line 384

def download_label_by_id(label_id, purchase_order_id = nil)
  logger.warn('[SWW] download_label_by_id is deprecated. Use download_label(carrier, tracking_number) instead.')

  # If we have a PO, try to find the label details
  if purchase_order_id.present?
    labels_result = get_labels_by_purchase_order(purchase_order_id)
    if labels_result.success
      label = labels_result.labels.find { |l| l[:labelId] == label_id }
      return download_label(label[:carrier], label[:trackingNumber]) if label && label[:trackingNumber] && label[:carrier]
    end
  end

  DownloadResult.new(success: false, label_data: nil, content_type: nil,
                     error: 'Cannot download label by ID. Use download_label(carrier, tracking_number) or provide purchase_order_id.')
end

#get_carriersCarriersResult

GET /v3/shipping/labels/carriers
Returns list of supported carriers for Ship with Walmart

Returns:



56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
# File 'app/services/edi/walmart/ship_with_walmart.rb', line 56

def get_carriers
  url = build_url('/carriers')
  result = transport.send_data('', url, 'GET')

  if result[:success]
    data = parse_response(result[:http_result])
    carriers = data[:carriers] || []
    CarriersResult.new(success: true, carriers: carriers, error: nil)
  else
    CarriersResult.new(success: false, carriers: [], error: extract_error(result[:http_result]))
  end
rescue StandardError => e
  logger.error("[SWW] get_carriers error: #{e.message}")
  CarriersResult.new(success: false, carriers: [], error: e.message)
end

#get_labels_by_purchase_order(purchase_order_id) ⇒ Object



463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
# File 'app/services/edi/walmart/ship_with_walmart.rb', line 463

def get_labels_by_purchase_order(purchase_order_id)
  url = build_url("/purchase-orders/#{purchase_order_id}")

  logger.info("[SWW] Getting labels for PO: #{purchase_order_id}")

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

  if result[:success]
    data = parse_response(result[:http_result])
    logger.info("[SWW] get_labels_by_purchase_order response data structure: #{data.keys.inspect}")
    logger.info("[SWW] get_labels_by_purchase_order full response (first 1000 chars): #{data.to_json.first(1000)}")
    # Response can be: { labels: [...] }, { data: [...] }, or { data: { labels: [...] } }
    labels = if data[:labels].is_a?(Array)
               data[:labels]
             elsif data[:data].is_a?(Array)
               data[:data]
             elsif data[:data].is_a?(Hash)
               data[:data][:labels] || []
             else
               []
             end
    logger.info("[SWW] get_labels_by_purchase_order parsed #{labels.length} label(s)")
    log_api_call(
      operation: 'get_labels_by_purchase_order',
      url: url,
      method: 'GET',
      request_payload: { purchase_order_id: purchase_order_id },
      http_result: result[:http_result],
      success: true,
      error: nil
    )
    LabelsResult.new(success: true, labels: labels, error: nil)
  else
    error_msg = extract_error(result[:http_result])
    logger.warn("[SWW] get_labels_by_purchase_order failed: #{error_msg}")
    log_api_call(
      operation: 'get_labels_by_purchase_order',
      url: url,
      method: 'GET',
      request_payload: { purchase_order_id: purchase_order_id },
      http_result: result[:http_result],
      success: false,
      error: error_msg
    )
    LabelsResult.new(success: false, labels: [], error: error_msg)
  end
rescue StandardError => e
  logger.error("[SWW] get_labels_by_purchase_order error: #{e.message}")
  logger.error(e.backtrace.first(5).join("\n"))
  log_api_call(
    operation: 'get_labels_by_purchase_order',
    url: url,
    method: 'GET',
    request_payload: { purchase_order_id: purchase_order_id },
    http_result: nil,
    success: false,
    error: e.message
  )
  LabelsResult.new(success: false, labels: [], error: e.message)
end

#get_package_types(carrier_id) ⇒ PackageTypesResult

GET /v3/shipping/labels/carriers/carrierId/package-types
Returns package types available for a specific carrier

Parameters:

  • carrier_id (String)

    The carrier ID (e.g., 'USPS', 'FEDEX')

Returns:



77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
# File 'app/services/edi/walmart/ship_with_walmart.rb', line 77

def get_package_types(carrier_id)
  url = build_url("/carriers/#{carrier_id}/package-types")
  result = transport.send_data('', url, 'GET')

  if result[:success]
    data = parse_response(result[:http_result])
    package_types = data[:packageTypes] || []
    PackageTypesResult.new(success: true, package_types: package_types, error: nil)
  else
    PackageTypesResult.new(success: false, package_types: [], error: extract_error(result[:http_result]))
  end
rescue StandardError => e
  logger.error("[SWW] get_package_types error: #{e.message}")
  PackageTypesResult.new(success: false, package_types: [], error: e.message)
end

#get_shipping_estimates(options) ⇒ EstimatesResult

POST /v3/shipping/labels/shipping-estimates
Get shipping rate estimates based on package and address details

Parameters:

  • options (Hash)

    Shipping estimate options

Options Hash (options):

  • :from_address (Hash)

    Origin address

  • :to_address (Hash)

    Destination address

  • :packages (Array<Hash>)

    Package dimensions and weights

  • :deliver_by_date (Date)

    Required delivery date

Returns:



102
103
104
105
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
144
145
146
147
148
149
150
151
152
153
154
155
156
157
# File 'app/services/edi/walmart/ship_with_walmart.rb', line 102

def get_shipping_estimates(options)
  active_transport, url = production_rates_override || [transport, build_url('/shipping-estimates')]
  payload = build_estimate_payload(options)

  logger.info('[SWW] Requesting shipping estimates')
  logger.info("[SWW] Using production API for rates: #{active_transport != transport}") if active_transport != transport
  logger.info("[SWW] Estimate payload boxItems: #{payload[:boxItems].inspect}")
  logger.info("[SWW] Estimate payload addresses: from=#{payload[:fromAddress][:postalCode]}, to=#{payload[:toAddress][:postalCode]}")
  logger.info("[SWW] Estimate payload dates: shipBy=#{payload[:shipByDate]}, deliverBy=#{payload[:deliverByDate]}")
  logger.debug("[SWW] Full estimate payload: #{payload.to_json}")

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

  if result[:success]
    data = parse_response(result[:http_result])
    logger.info("[SWW] Estimate response data keys: #{data.keys.inspect}")
    logger.info("[SWW] Estimate response (first 1500 chars): #{data.to_json.first(1500)}")
    estimates = normalize_estimates(data)
    logger.info("[SWW] Normalized #{estimates.size} estimate(s)")
    log_api_call(
      operation: 'get_shipping_estimates',
      url: url,
      method: 'POST',
      request_payload: payload,
      http_result: result[:http_result],
      success: true,
      error: nil
    )
    EstimatesResult.new(success: true, estimates: estimates, error: nil)
  else
    error_msg = extract_error(result[:http_result])
    logger.error("[SWW] Estimate request failed: #{error_msg}")
    log_api_call(
      operation: 'get_shipping_estimates',
      url: url,
      method: 'POST',
      request_payload: payload,
      http_result: result[:http_result],
      success: false,
      error: error_msg
    )
    EstimatesResult.new(success: false, estimates: [], error: error_msg)
  end
rescue StandardError => e
  logger.error("[SWW] get_shipping_estimates error: #{e.message}")
  log_api_call(
    operation: 'get_shipping_estimates',
    url: url,
    method: 'POST',
    request_payload: payload,
    http_result: nil,
    success: false,
    error: e.message
  )
  EstimatesResult.new(success: false, estimates: [], error: e.message)
end