Skip to content
This repository has been archived by the owner on Jun 13, 2018. It is now read-only.

Add USPS Track/Confirm Fields request call (rev=1) to USPS Carrier #159

Closed
wants to merge 7 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
130 changes: 125 additions & 5 deletions lib/active_shipping/shipping/carriers/usps.rb
@@ -1,5 +1,6 @@
# -*- encoding: utf-8 -*-
require 'cgi'
require 'socket'

module ActiveMerchant
module Shipping
Expand Down Expand Up @@ -138,16 +139,29 @@ class USPS < Carrier
RESPONSE_ERROR_MESSAGES = [
/There is no record of that mail item/,
/This Information has not been included in this Test Server\./,
/Delivery status information is not available/
/Delivery status information is not available/,
/That's not a valid number\./
]

def initialize(options = {})
super
@ip_addr = @options[:ip_address] || IPSocket.getaddress(Socket.gethostname)
end

def find_tracking_info(tracking_number, options={})
options = @options.update(options)
tracking_request = build_tracking_request(tracking_number, options)
response = commit(:track, tracking_request, (options[:test] || false))
parse_tracking_response(response, options)
end

def find_tracking_info_with_fields(tracking_numbers, options={})
options = @options.update(options)
tracking_request = build_tracking_with_fields_request(tracking_numbers)
response = commit(:track, tracking_request, (options[:test] || false))
parse_tracking_with_fields_response(response)
end

def self.size_code_for(package)
if package.inches(:max) <= 12
'REGULAR'
Expand Down Expand Up @@ -227,6 +241,18 @@ def build_tracking_request(tracking_number, options={})
URI.encode(xml_request.to_s)
end

def build_tracking_with_fields_request(tracking_numbers)
# Using revision 1 api for new functionality
xml_request = XmlNode.new('TrackFieldRequest', 'USERID' => @options[:login]) do |root_node|
root_node << XmlNode.new('Revision', 1)
# NOTE not sure of purpose and use of the client ip address
root_node << XmlNode.new('ClientIp', @ip_addr)
root_node << XmlNode.new('SourceId', @options[:login]) # usps login as internal user
tracking_numbers.each { |track_num| root_node << XmlNode.new('TrackID', :ID => track_num) }
end
URI.encode(xml_request.to_s)
end

def us_rates(origin, destination, packages, options={})
request = build_us_rate_request(packages, origin.zip, destination.zip, options)
# never use test mode; rate requests just won't work on test servers
Expand Down Expand Up @@ -535,9 +561,99 @@ def parse_tracking_response(response, options)
:actual_delivery_date => actual_delivery_date
)
end

def parse_tracking_with_fields_response(response)
xml = REXML::Document.new(response)

# We have a document level error
if !!xml.elements['Error']
description = xml.elements['Error/Description'].get_text.to_s
raise ResponseError.new(description)
end

tracking_responses = []
tracking_nodes = xml.elements.collect('TrackResponse/TrackInfo') { |e| e }

tracking_nodes.each do |tracking_info|
tracking_number = tracking_info.attributes['ID'].to_s
tracking_details = tracking_info.elements.collect('TrackDetail'){ |e| e }
tracking_summary = tracking_info.elements['TrackSummary']

success = !(tracking_info.elements['Error'] || summary_no_record?(tracking_summary))
message = response_message(tracking_info)
shipment_events = []
status, status_category, expected_delivery_date, actual_delivery_date = nil, nil, nil, nil

if success
previous_event_time = Time.now
tracking_details.each do |event|
ship_event = extract_track_with_fields_event(event, previous_event_time)
previous_event_time = ship_event.time
shipment_events << ship_event
end
summary_ship_event = extract_track_with_fields_event(tracking_summary, previous_event_time)
if !summary_ship_event.nil?
shipment_events << summary_ship_event
status = summary_ship_event.status
actual_delivery_date = summary_ship_event.time if summary_ship_event.delivered?
end

shipment_events = shipment_events.sort_by(&:time)
status_category = tracking_info.elements['StatusCategory'].get_text.to_s
status_category = status_category.downcase.gsub("\s", "_").to_sym

if tracking_info.elements['ExpectedDeliveryDate']
if tracking_info.elements['ExpectedDeliveryTime']
time = Time.parse("#{tracking_info.elements['ExpectedDeliveryDate'].get_text.to_s} #{tracking_info.elements['ExpectedDeliveryTime'].get_text.to_s}")
else
time = Time.parse("#{tracking_info.elements['ExpectedDeliveryDate'].get_text.to_s}")
end
expected_delivery_date = Time.utc(time.year, time.month, time.mday, time.hour, time.min, time.sec)
end
end

tracking_responses << TrackingResponse.new(success, message, Hash.from_xml(tracking_info.to_s),
:carrier => @@name,
:xml => response,
:request => last_request,
:shipment_events => shipment_events,
:tracking_number => tracking_number,
:status => status,
:status_category => status_category,
:actual_delivery_date => actual_delivery_date,
:scheduled_delivery_date => expected_delivery_date
)
end

tracking_responses
end

# pass default event_time to fill if none available in event in order to maintain sort order
def extract_track_with_fields_event(track_event_node, default_event_time)
event = nil
status_name = track_event_node.elements['Event'].get_text.to_s
datetime_str = "#{track_event_node.elements['EventDate'].get_text.to_s} #{track_event_node.elements['EventTime'].get_text.to_s}"

if !datetime_str.nil? && datetime_str.length > 1
time = Time.parse(datetime_str)
zoneless_datetime = Time.utc(time.year, time.month, time.mday, time.hour, time.min, time.sec)
else
zoneless_datetime = default_event_time
end

country = !!track_event_node.elements['EventCountry'] || track_event_node.elements['EventCountry'].blank? ? 'USA' : track_event_node.elements['EventCountry'].get_text.to_s

location = Location.new(city: track_event_node.elements['EventCity'].get_text.to_s,
state: track_event_node.elements['EventState'].get_text.to_s,
postal_code: track_event_node.elements['EventZIPCode'].get_text.to_s,
country: country)
event = ShipmentEvent.new(status_name, zoneless_datetime, location)

event
end

def track_summary_node(document)
document.elements['*/*/TrackSummary']
document.elements['*/*/TrackSummary'] || document.elements['TrackSummary']
end

def error_description_node(document)
Expand All @@ -559,14 +675,18 @@ def has_error?(document)
def no_record?(document)
summary_node = track_summary_node(document)
if summary_node
summary = summary_node.get_text.to_s
RESPONSE_ERROR_MESSAGES.detect { |re| summary =~ re }
summary =~ /There is no record of that mail item/ || summary =~ /This Information has not been included in this Test Server\./
summary_no_record?(summary_node)
else
false
end
end

def summary_no_record?(summary_node)
summary = summary_node.get_text.to_s
RESPONSE_ERROR_MESSAGES.detect { |re| summary =~ re }
summary =~ /There is no record of that mail item/ || summary =~ /This Information has not been included in this Test Server\./
end

def tracking_info_error?(document)
document.elements['*/TrackInfo/Error']
end
Expand Down
2 changes: 2 additions & 0 deletions lib/active_shipping/shipping/tracking_response.rb
Expand Up @@ -7,6 +7,7 @@ class TrackingResponse < Response
attr_reader :status # symbol
attr_reader :status_code # string
attr_reader :status_description #string
attr_reader :status_category #symbol
attr_reader :ship_time # time
attr_reader :scheduled_delivery_date # time
attr_reader :actual_delivery_date # time
Expand All @@ -21,6 +22,7 @@ def initialize(success, message, params = {}, options = {})
@status = options[:status]
@status_code = options[:status_code]
@status_description = options[:status_description]
@status_category = options[:status_category]
@ship_time = options[:ship_time]
@scheduled_delivery_date = options[:scheduled_delivery_date]
@actual_delivery_date = options[:actual_delivery_date]
Expand Down
2 changes: 1 addition & 1 deletion lib/active_shipping/version.rb
@@ -1,3 +1,3 @@
module ActiveShipping
VERSION = "0.12.0"
VERSION = "0.17.0goodmouth"
end
3 changes: 2 additions & 1 deletion test/fixtures.yml
@@ -1,5 +1,6 @@
usps:
login: APIKey
login: ApiKey
ip_address: 141.214.14.1
ups:
key: XmlAccessKey
login: UPSDotComLogin
Expand Down
145 changes: 145 additions & 0 deletions test/fixtures/xml/usps/tracking_w_fields_response.xml
@@ -0,0 +1,145 @@
<?xml version="1.0" encoding="UTF-8"?>
<TrackResponse>
<TrackInfo ID="9400110200882182234434">
<Class>First-Class Package Service</Class>
<ClassOfMailCode>FC</ClassOfMailCode>
<DestinationCity>SAINT ALBANS</DestinationCity>
<DestinationState>WV</DestinationState>
<DestinationZip>25177</DestinationZip>
<EmailEnabled>true</EmailEnabled>
<ExpectedDeliveryDate>May 15, 2014</ExpectedDeliveryDate>
<KahalaIndicator>false</KahalaIndicator>
<MailTypeCode>DM</MailTypeCode>
<MPDATE>2014-05-12 21:42:26.000000</MPDATE>
<MPSUFFIX>675849201</MPSUFFIX>
<OriginCity>INDIANAPOLIS</OriginCity>
<OriginState>IN</OriginState>
<OriginZip>46222</OriginZip>
<PodEnabled>false</PodEnabled>
<RestoreEnabled>false</RestoreEnabled>
<RreEnabled>false</RreEnabled>
<Service>USPS Tracking&lt;SUP>&amp;#153;&lt;/SUP></Service>
<ServiceTypeCode>001</ServiceTypeCode>
<Status>Delivered</Status>
<StatusCategory>Delivered</StatusCategory>
<StatusSummary>Your item was delivered at 4:42 pm on May 14, 2014 in SAINT ALBANS, WV 25177.</StatusSummary>
<TABLECODE>T</TABLECODE>
<TrackSummary>
<EventTime>4:42 pm</EventTime>
<EventDate>May 14, 2014</EventDate>
<Event>Delivered</Event>
<EventCity>SAINT ALBANS</EventCity>
<EventState>WV</EventState>
<EventZIPCode>25177</EventZIPCode>
<EventCountry/>
<FirmName/>
<Name/>
<AuthorizedAgent>false</AuthorizedAgent>
<EventCode>01</EventCode>
</TrackSummary>
<TrackDetail>
<EventTime>7:37 am</EventTime>
<EventDate>May 14, 2014</EventDate>
<Event>Out for Delivery</Event>
<EventCity>SAINT ALBANS</EventCity>
<EventState>WV</EventState>
<EventZIPCode>25177</EventZIPCode>
<EventCountry/>
<FirmName/>
<Name/>
<AuthorizedAgent>false</AuthorizedAgent>
<EventCode>OF</EventCode>
</TrackDetail>
<TrackDetail>
<EventTime>7:27 am</EventTime>
<EventDate>May 14, 2014</EventDate>
<Event>Sorting Complete</Event>
<EventCity>SAINT ALBANS</EventCity>
<EventState>WV</EventState>
<EventZIPCode>25177</EventZIPCode>
<EventCountry/>
<FirmName/>
<Name/>
<AuthorizedAgent>false</AuthorizedAgent>
<EventCode>PC</EventCode>
</TrackDetail>
<TrackDetail>
<EventTime>5:33 am</EventTime>
<EventDate>May 14, 2014</EventDate>
<Event>Arrival at Post Office</Event>
<EventCity>SAINT ALBANS</EventCity>
<EventState>WV</EventState>
<EventZIPCode>25177</EventZIPCode>
<EventCountry/>
<FirmName/>
<Name/>
<AuthorizedAgent>false</AuthorizedAgent>
<EventCode>07</EventCode>
</TrackDetail>
<TrackDetail>
<EventTime>7:59 pm</EventTime>
<EventDate>May 13, 2014</EventDate>
<Event>Depart USPS Sort Facility</Event>
<EventCity>CHARLESTON</EventCity>
<EventState>WV</EventState>
<EventZIPCode>25350</EventZIPCode>
<EventCountry/>
<FirmName/>
<Name/>
<AuthorizedAgent>false</AuthorizedAgent>
<EventCode>EF</EventCode>
</TrackDetail>
<TrackDetail>
<EventTime>5:38 pm</EventTime>
<EventDate>May 13, 2014</EventDate>
<Event>Processed through USPS Sort Facility</Event>
<EventCity>CHARLESTON</EventCity>
<EventState>WV</EventState>
<EventZIPCode>25350</EventZIPCode>
<EventCountry/>
<FirmName/>
<Name/>
<AuthorizedAgent>false</AuthorizedAgent>
<EventCode>10</EventCode>
</TrackDetail>
<TrackDetail>
<EventTime/>
<EventDate>May 13, 2014</EventDate>
<Event>Electronic Shipping Info Received</Event>
<EventCity/>
<EventState/>
<EventZIPCode/>
<EventCountry/>
<FirmName/>
<Name/>
<AuthorizedAgent>false</AuthorizedAgent>
<EventCode>MA</EventCode>
</TrackDetail>
<TrackDetail>
<EventTime>10:39 pm</EventTime>
<EventDate>May 12, 2014</EventDate>
<Event>Processed at USPS Origin Sort Facility</Event>
<EventCity>INDIANAPOLIS</EventCity>
<EventState>IN</EventState>
<EventZIPCode>46241</EventZIPCode>
<EventCountry/>
<FirmName/>
<Name/>
<AuthorizedAgent>false</AuthorizedAgent>
<EventCode>10</EventCode>
</TrackDetail>
<TrackDetail>
<EventTime>9:24 pm</EventTime>
<EventDate>May 12, 2014</EventDate>
<Event>Accepted at USPS Origin Sort Facility</Event>
<EventCity>INDIANAPOLIS</EventCity>
<EventState>IN</EventState>
<EventZIPCode>46222</EventZIPCode>
<EventCountry/>
<FirmName/>
<Name/>
<AuthorizedAgent>false</AuthorizedAgent>
<EventCode>OA</EventCode>
</TrackDetail>
</TrackInfo>
</TrackResponse>