lib/active_shipping/carriers/fedex.rb in active_shipping-1.1.3 vs lib/active_shipping/carriers/fedex.rb in active_shipping-1.2.0

- old
+ new

@@ -66,10 +66,18 @@ 'dropbox' => 'DROP_BOX', 'business_service_center' => 'BUSINESS_SERVICE_CENTER', 'station' => 'STATION' } + SIGNATURE_OPTION_CODES = { + adult: 'ADULT', # 21 years plus + direct: 'DIRECT', # A person at the delivery address + indirect: 'INDIRECT', # A person at the delivery address, or a neighbor, or a signed note for fedex on the door + none_required: 'NO_SIGNATURE_REQUIRED', + default_for_service: 'SERVICE_DEFAULT' + } + PAYMENT_TYPES = { 'sender' => 'SENDER', 'recipient' => 'RECIPIENT', 'third_party' => 'THIRDPARTY', 'collect' => 'COLLECT' @@ -151,14 +159,131 @@ tracking_request = build_tracking_request(tracking_number, options) xml = commit(save_request(tracking_request), (options[:test] || false)) parse_tracking_response(xml, options) end + + # Get Shipping labels + def create_shipment(origin, destination, packages, options = {}) + options = @options.update(options) + packages = Array(packages) + raise Error, "Multiple packages are not supported yet." if packages.length > 1 + + request = build_shipment_request(origin, destination, packages, options) + logger.debug(request) if logger + + logger.debug(confirm_response) if logger + + response = commit(save_request(request), (options[:test] || false)) + parse_ship_response(response) + end + protected + def build_shipment_request(origin, destination, packages, options = {}) + imperial = location_uses_imperial(origin) + + xml_builder = Nokogiri::XML::Builder.new do |xml| + xml.ProcessShipmentRequest(xmlns: 'http://fedex.com/ws/ship/v13') do + build_request_header(xml) + build_version_node(xml, 'ship', 13, 0 ,0) + + xml.RequestedShipment do + xml.ShipTimestamp(ship_timestamp(options[:turn_around_time]).iso8601(0)) + xml.DropoffType('REGULAR_PICKUP') + xml.ServiceType(options[:service_type] || 'FEDEX_GROUND') + xml.PackagingType('YOUR_PACKAGING') + + xml.Shipper do + build_contact_address_nodes(xml, options[:shipper] || origin) + end + + xml.Recipient do + build_contact_address_nodes(xml, destination) + end + + xml.Origin do + build_contact_address_nodes(xml, origin) + end + + xml.ShippingChargesPayment do + xml.PaymentType('SENDER') + xml.Payor do + build_shipment_responsible_party_node(xml, options[:shipper] || origin) + end + end + + xml.LabelSpecification do + xml.LabelFormatType('COMMON2D') + xml.ImageType('PNG') + xml.LabelStockType('PAPER_7X4.75') + end + + xml.RateRequestTypes('ACCOUNT') + + xml.PackageCount(packages.size) + packages.each do |package| + xml.RequestedPackageLineItems do + xml.GroupPackageCount(1) + build_package_weight_node(xml, package, imperial) + build_package_dimensions_node(xml, package, imperial) + + # Reference Numbers + reference_numbers = Array(package.options[:reference_numbers]) + if reference_numbers.size > 0 + xml.CustomerReferences do + reference_numbers.each do |reference_number_info| + xml.CustomerReferenceType(reference_number_info[:type] || "CUSTOMER_REFERENCE") + xml.Value(reference_number_info[:value]) + end + end + end + + xml.SpecialServicesRequested do + xml.SpecialServiceTypes("SIGNATURE_OPTION") + xml.SignatureOptionDetail do + xml.OptionType(SIGNATURE_OPTION_CODES[package.options[:signature_option] || :default_for_service]) + end + end + end + end + end + end + end + xml_builder.to_xml + end + + def build_contact_address_nodes(xml, location) + xml.Contact do + xml.PersonName(location.name) + xml.CompanyName(location.company) + xml.PhoneNumber(location.phone) + end + xml.Address do + xml.StreetLines(location.address1) if location.address1 + xml.StreetLines(location.address2) if location.address2 + xml.City(location.city) if location.city + xml.StateOrProvinceCode(location.state) + xml.PostalCode(location.postal_code) + xml.CountryCode(location.country_code(:alpha2)) + xml.Residential('true') if location.residential? + end + end + + def build_shipment_responsible_party_node(xml, origin) + xml.ResponsibleParty do + xml.AccountNumber(@options[:account]) + xml.Contact do + xml.PersonName(origin.name) + xml.CompanyName(origin.company) + xml.PhoneNumber(origin.phone) + end + end + end + def build_rate_request(origin, destination, packages, options = {}) - imperial = %w(US LR MM).include?(origin.country_code(:alpha2)) + imperial = location_uses_imperial(origin) xml_builder = Nokogiri::XML::Builder.new do |xml| xml.RateRequest(xmlns: 'http://fedex.com/ws/rate/v13') do build_request_header(xml) build_version_node(xml, 'crs', 13, 0 ,0) @@ -339,35 +464,39 @@ xml = build_document(response, 'RateReply') success = response_success?(xml) message = response_message(xml) - rate_estimates = xml.root.css('> RateReplyDetails').map do |rated_shipment| - service_code = rated_shipment.at('ServiceType').text - is_saturday_delivery = rated_shipment.at('AppliedOptions').try(:text) == 'SATURDAY_DELIVERY' - service_type = is_saturday_delivery ? "#{service_code}_SATURDAY_DELIVERY" : service_code + if success + rate_estimates = xml.root.css('> RateReplyDetails').map do |rated_shipment| + service_code = rated_shipment.at('ServiceType').text + is_saturday_delivery = rated_shipment.at('AppliedOptions').try(:text) == 'SATURDAY_DELIVERY' + service_type = is_saturday_delivery ? "#{service_code}_SATURDAY_DELIVERY" : service_code - transit_time = rated_shipment.at('TransitTime').text if service_code == "FEDEX_GROUND" - max_transit_time = rated_shipment.at('MaximumTransitTime').try(:text) if service_code == "FEDEX_GROUND" + transit_time = rated_shipment.at('TransitTime').text if service_code == "FEDEX_GROUND" + max_transit_time = rated_shipment.at('MaximumTransitTime').try(:text) if service_code == "FEDEX_GROUND" - delivery_timestamp = rated_shipment.at('DeliveryTimestamp').try(:text) + delivery_timestamp = rated_shipment.at('DeliveryTimestamp').try(:text) - delivery_range = delivery_range_from(transit_time, max_transit_time, delivery_timestamp, options) + delivery_range = delivery_range_from(transit_time, max_transit_time, delivery_timestamp, options) - currency = rated_shipment.at('RatedShipmentDetails/ShipmentRateDetail/TotalNetCharge/Currency').text - RateEstimate.new(origin, destination, @@name, - self.class.service_name_for_code(service_type), - :service_code => service_code, - :total_price => rated_shipment.at('RatedShipmentDetails/ShipmentRateDetail/TotalNetCharge/Amount').text.to_f, - :currency => currency, - :packages => packages, - :delivery_range => delivery_range) - end + currency = rated_shipment.at('RatedShipmentDetails/ShipmentRateDetail/TotalNetCharge/Currency').text + RateEstimate.new(origin, destination, @@name, + self.class.service_name_for_code(service_type), + :service_code => service_code, + :total_price => rated_shipment.at('RatedShipmentDetails/ShipmentRateDetail/TotalNetCharge/Amount').text.to_f, + :currency => currency, + :packages => packages, + :delivery_range => delivery_range) + end - if rate_estimates.empty? - success = false - message = "No shipping rates could be found for the destination address" if message.blank? + if rate_estimates.empty? + success = false + message = "No shipping rates could be found for the destination address" if message.blank? + end + else + rate_estimates = [] end RateResponse.new(success, message, Hash.from_xml(response), :rates => rate_estimates, :xml => response, :request => last_request, :log_xml => options[:log_xml]) end @@ -381,10 +510,23 @@ end delivery_range end + def parse_ship_response(response) + xml = build_document(response, 'ProcessShipmentReply') + success = response_success?(xml) + message = response_message(xml) + + response_info = Hash.from_xml(response) + tracking_number = xml.css("CompletedPackageDetails TrackingIds TrackingNumber").text + base_64_image = xml.css("Label Image").text + + labels = [Label.new(tracking_number, Base64.decode64(base_64_image))] + LabelResponse.new(success, message, response_info, {labels: labels}) + end + def business_days_from(date, days) future_date = date count = 0 while count < days @@ -574,8 +716,12 @@ raise ActiveShipping::ResponseContentError.new(StandardError.new('Invalid document'), xml) end document rescue Nokogiri::XML::SyntaxError => e raise ActiveShipping::ResponseContentError.new(e, xml) + end + + def location_uses_imperial(location) + %w(US LR MM).include?(location.country_code(:alpha2)) end end end