friendly_shipping 0.8.1 → 0.9.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (200) hide show
  1. checksums.yaml +4 -4
  2. data/.circleci/config.yml +1 -1
  3. data/.env.template +3 -0
  4. data/.env.test.local.template +6 -0
  5. data/.gitignore +1 -0
  6. data/.rubocop-relaxed.yml +7 -23
  7. data/.rubocop.yml +11 -2
  8. data/.rubocop_todo.yml +21 -0
  9. data/.yardopts +6 -0
  10. data/CHANGELOG.md +71 -0
  11. data/Gemfile +17 -0
  12. data/friendly_shipping.gemspec +4 -13
  13. data/lib/friendly_shipping/api_error.rb +6 -4
  14. data/lib/friendly_shipping/api_error_handler.rb +10 -6
  15. data/lib/friendly_shipping/api_failure.rb +4 -0
  16. data/lib/friendly_shipping/api_result.rb +16 -4
  17. data/lib/friendly_shipping/carrier.rb +28 -8
  18. data/lib/friendly_shipping/http_client.rb +25 -2
  19. data/lib/friendly_shipping/inflections.rb +10 -0
  20. data/lib/friendly_shipping/item_options.rb +3 -0
  21. data/lib/friendly_shipping/label.rb +41 -20
  22. data/lib/friendly_shipping/package_options.rb +21 -2
  23. data/lib/friendly_shipping/rate.rb +50 -15
  24. data/lib/friendly_shipping/request.rb +23 -7
  25. data/lib/friendly_shipping/response.rb +21 -6
  26. data/lib/friendly_shipping/services/rl/api_error.rb +33 -0
  27. data/lib/friendly_shipping/services/rl/bol_options.rb +107 -0
  28. data/lib/friendly_shipping/services/rl/bol_packages_serializer.rb +32 -0
  29. data/lib/friendly_shipping/services/rl/bol_structures_serializer.rb +31 -0
  30. data/lib/friendly_shipping/services/rl/item_options.rb +38 -0
  31. data/lib/friendly_shipping/services/rl/package_options.rb +40 -0
  32. data/lib/friendly_shipping/services/rl/parse_create_bol_response.rb +46 -0
  33. data/lib/friendly_shipping/services/rl/parse_invoice_response.rb +50 -0
  34. data/lib/friendly_shipping/services/rl/parse_print_bol_response.rb +47 -0
  35. data/lib/friendly_shipping/services/rl/parse_print_shipping_labels_response.rb +47 -0
  36. data/lib/friendly_shipping/services/rl/parse_rate_quote_response.rb +66 -0
  37. data/lib/friendly_shipping/services/rl/parse_transit_times_response.rb +76 -0
  38. data/lib/friendly_shipping/services/rl/rate_quote_options.rb +86 -0
  39. data/lib/friendly_shipping/services/rl/rate_quote_packages_serializer.rb +54 -0
  40. data/lib/friendly_shipping/services/rl/rate_quote_structures_serializer.rb +53 -0
  41. data/lib/friendly_shipping/services/rl/serialize_create_bol_request.rb +86 -0
  42. data/lib/friendly_shipping/services/rl/serialize_location.rb +46 -0
  43. data/lib/friendly_shipping/services/rl/serialize_rate_quote_request.rb +69 -0
  44. data/lib/friendly_shipping/services/rl/serialize_transit_times_request.rb +38 -0
  45. data/lib/friendly_shipping/services/rl/shipment_document.rb +40 -0
  46. data/lib/friendly_shipping/services/rl/shipment_information.rb +41 -0
  47. data/lib/friendly_shipping/services/rl/shipment_options.rb +50 -0
  48. data/lib/friendly_shipping/services/rl/shipping_methods.rb +28 -0
  49. data/lib/friendly_shipping/services/rl/structure_options.rb +13 -0
  50. data/lib/friendly_shipping/services/rl.rb +204 -0
  51. data/lib/friendly_shipping/services/ship_engine/api_error.rb +33 -0
  52. data/lib/friendly_shipping/services/ship_engine/customs_items_serializer.rb +36 -0
  53. data/lib/friendly_shipping/services/ship_engine/label_customs_options.rb +10 -7
  54. data/lib/friendly_shipping/services/ship_engine/label_item_options.rb +10 -5
  55. data/lib/friendly_shipping/services/ship_engine/label_options.rb +31 -14
  56. data/lib/friendly_shipping/services/ship_engine/label_package_options.rb +18 -11
  57. data/lib/friendly_shipping/services/ship_engine/parse_address_validation_response.rb +77 -0
  58. data/lib/friendly_shipping/services/ship_engine/parse_carrier_response.rb +9 -0
  59. data/lib/friendly_shipping/services/ship_engine/parse_label_response.rb +4 -0
  60. data/lib/friendly_shipping/services/ship_engine/{parse_rate_estimate_response.rb → parse_rate_estimates_response.rb} +26 -6
  61. data/lib/friendly_shipping/services/ship_engine/parse_rates_response.rb +101 -0
  62. data/lib/friendly_shipping/services/ship_engine/parse_void_response.rb +4 -0
  63. data/lib/friendly_shipping/services/ship_engine/rate_estimates_options.rb +17 -4
  64. data/lib/friendly_shipping/services/ship_engine/rates_item_options.rb +28 -0
  65. data/lib/friendly_shipping/services/ship_engine/rates_options.rb +61 -0
  66. data/lib/friendly_shipping/services/ship_engine/rates_package_options.rb +20 -0
  67. data/lib/friendly_shipping/services/ship_engine/serialize_address_residential_indicator.rb +27 -0
  68. data/lib/friendly_shipping/services/ship_engine/serialize_address_validation_request.rb +31 -0
  69. data/lib/friendly_shipping/services/ship_engine/serialize_label_shipment.rb +22 -27
  70. data/lib/friendly_shipping/services/ship_engine/serialize_rate_estimate_request.rb +41 -16
  71. data/lib/friendly_shipping/services/ship_engine/serialize_rates_request.rb +126 -0
  72. data/lib/friendly_shipping/services/ship_engine.rb +94 -21
  73. data/lib/friendly_shipping/services/ship_engine_ltl/api_error.rb +12 -0
  74. data/lib/friendly_shipping/services/ship_engine_ltl/item_options.rb +24 -5
  75. data/lib/friendly_shipping/services/ship_engine_ltl/package_options.rb +39 -4
  76. data/lib/friendly_shipping/services/ship_engine_ltl/parse_carrier_response.rb +4 -0
  77. data/lib/friendly_shipping/services/ship_engine_ltl/parse_quote_response.rb +10 -0
  78. data/lib/friendly_shipping/services/ship_engine_ltl/quote_options.rb +29 -11
  79. data/lib/friendly_shipping/services/ship_engine_ltl/serialize_packages.rb +7 -2
  80. data/lib/friendly_shipping/services/ship_engine_ltl/serialize_quote_request.rb +50 -16
  81. data/lib/friendly_shipping/services/ship_engine_ltl/serialize_structures.rb +42 -0
  82. data/lib/friendly_shipping/services/ship_engine_ltl/shipment_options.rb +47 -0
  83. data/lib/friendly_shipping/services/ship_engine_ltl/structure_options.rb +17 -0
  84. data/lib/friendly_shipping/services/ship_engine_ltl.rb +50 -30
  85. data/lib/friendly_shipping/services/tforce_freight/access_token.rb +43 -0
  86. data/lib/friendly_shipping/services/tforce_freight/api_error.rb +42 -0
  87. data/lib/friendly_shipping/services/tforce_freight/bol_options.rb +180 -0
  88. data/lib/friendly_shipping/services/tforce_freight/document_options.rb +100 -0
  89. data/lib/friendly_shipping/services/tforce_freight/generate_commodity_information.rb +92 -0
  90. data/lib/friendly_shipping/services/tforce_freight/generate_create_bol_request_hash.rb +165 -0
  91. data/lib/friendly_shipping/services/tforce_freight/generate_document_options_hash.rb +36 -0
  92. data/lib/friendly_shipping/services/tforce_freight/generate_handling_units_hash.rb +51 -0
  93. data/lib/friendly_shipping/services/tforce_freight/generate_location_hash.rb +25 -0
  94. data/lib/friendly_shipping/services/tforce_freight/generate_pickup_request_hash.rb +113 -0
  95. data/lib/friendly_shipping/services/tforce_freight/generate_rates_request_hash.rb +65 -0
  96. data/lib/friendly_shipping/services/tforce_freight/generate_reference_hash.rb +28 -0
  97. data/lib/friendly_shipping/services/tforce_freight/item_options.rb +93 -0
  98. data/lib/friendly_shipping/services/tforce_freight/package_options.rb +121 -0
  99. data/lib/friendly_shipping/services/tforce_freight/parse_create_bol_response.rb +94 -0
  100. data/lib/friendly_shipping/services/tforce_freight/parse_pickup_response.rb +45 -0
  101. data/lib/friendly_shipping/services/tforce_freight/parse_rates_response.rb +58 -0
  102. data/lib/friendly_shipping/services/tforce_freight/parse_shipment_document.rb +31 -0
  103. data/lib/friendly_shipping/services/tforce_freight/pickup_options.rb +82 -0
  104. data/lib/friendly_shipping/services/tforce_freight/rates_item_options.rb +12 -0
  105. data/lib/friendly_shipping/services/tforce_freight/rates_options.rb +162 -0
  106. data/lib/friendly_shipping/services/tforce_freight/rates_package_options.rb +12 -0
  107. data/lib/friendly_shipping/services/tforce_freight/shipment_document.rb +38 -0
  108. data/lib/friendly_shipping/services/tforce_freight/shipment_information.rb +104 -0
  109. data/lib/friendly_shipping/services/tforce_freight/shipment_options.rb +49 -0
  110. data/lib/friendly_shipping/services/tforce_freight/shipping_methods.rb +25 -0
  111. data/lib/friendly_shipping/services/tforce_freight/structure_options.rb +44 -0
  112. data/lib/friendly_shipping/services/tforce_freight.rb +202 -0
  113. data/lib/friendly_shipping/services/ups/label_options.rb +14 -2
  114. data/lib/friendly_shipping/services/ups/label_package_options.rb +1 -1
  115. data/lib/friendly_shipping/services/ups/parse_modifier_element.rb +29 -0
  116. data/lib/friendly_shipping/services/ups/parse_rate_response.rb +12 -0
  117. data/lib/friendly_shipping/services/ups/parse_time_in_transit_response.rb +1 -1
  118. data/lib/friendly_shipping/services/ups/rate_estimate_options.rb +14 -1
  119. data/lib/friendly_shipping/services/ups/serialize_rating_service_selection_request.rb +10 -1
  120. data/lib/friendly_shipping/services/ups/serialize_shipment_accept_request.rb +1 -1
  121. data/lib/friendly_shipping/services/ups/serialize_shipment_confirm_request.rb +2 -1
  122. data/lib/friendly_shipping/services/ups/shipping_methods.rb +1 -1
  123. data/lib/friendly_shipping/services/ups.rb +1 -1
  124. data/lib/friendly_shipping/services/ups_freight/api_error.rb +8 -5
  125. data/lib/friendly_shipping/services/ups_freight/generate_commodity_information.rb +65 -19
  126. data/lib/friendly_shipping/services/ups_freight/generate_freight_rate_request_hash.rb +3 -20
  127. data/lib/friendly_shipping/services/ups_freight/generate_freight_ship_request_hash.rb +2 -20
  128. data/lib/friendly_shipping/services/ups_freight/generate_handling_units_hash.rb +54 -0
  129. data/lib/friendly_shipping/services/ups_freight/label_options.rb +36 -10
  130. data/lib/friendly_shipping/services/ups_freight/label_structure_options.rb +13 -0
  131. data/lib/friendly_shipping/services/ups_freight/parse_freight_label_response.rb +2 -2
  132. data/lib/friendly_shipping/services/ups_freight/parse_shipment_document.rb +1 -1
  133. data/lib/friendly_shipping/services/ups_freight/rates_item_options.rb +18 -9
  134. data/lib/friendly_shipping/services/ups_freight/rates_options.rb +31 -21
  135. data/lib/friendly_shipping/services/ups_freight/rates_package_options.rb +79 -8
  136. data/lib/friendly_shipping/services/ups_freight/rates_structure_options.rb +46 -0
  137. data/lib/friendly_shipping/services/ups_freight/shipment_information.rb +7 -3
  138. data/lib/friendly_shipping/services/ups_freight/shipment_options.rb +49 -0
  139. data/lib/friendly_shipping/services/ups_freight.rb +3 -1
  140. data/lib/friendly_shipping/services/ups_json/access_token.rb +29 -0
  141. data/lib/friendly_shipping/services/ups_json/api_error.rb +29 -0
  142. data/lib/friendly_shipping/services/ups_json/generate_address_classification_payload.rb +29 -0
  143. data/lib/friendly_shipping/services/ups_json/generate_address_hash.rb +30 -0
  144. data/lib/friendly_shipping/services/ups_json/generate_labels_payload.rb +211 -0
  145. data/lib/friendly_shipping/services/ups_json/generate_package_hash.rb +76 -0
  146. data/lib/friendly_shipping/services/ups_json/generate_rates_payload.rb +86 -0
  147. data/lib/friendly_shipping/services/ups_json/generate_timings_payload.rb +44 -0
  148. data/lib/friendly_shipping/services/ups_json/label.rb +20 -0
  149. data/lib/friendly_shipping/services/ups_json/label_billing_options.rb +41 -0
  150. data/lib/friendly_shipping/services/ups_json/label_item_options.rb +77 -0
  151. data/lib/friendly_shipping/services/ups_json/label_options.rb +177 -0
  152. data/lib/friendly_shipping/services/ups_json/label_package_options.rb +51 -0
  153. data/lib/friendly_shipping/services/ups_json/parse_address_classification_response.rb +31 -0
  154. data/lib/friendly_shipping/services/ups_json/parse_json_response.rb +44 -0
  155. data/lib/friendly_shipping/services/ups_json/parse_labels_response.rb +71 -0
  156. data/lib/friendly_shipping/services/ups_json/parse_money_hash.rb +128 -0
  157. data/lib/friendly_shipping/services/ups_json/parse_rate_modifier_hash.rb +28 -0
  158. data/lib/friendly_shipping/services/ups_json/parse_rates_response.rb +105 -0
  159. data/lib/friendly_shipping/services/ups_json/parse_timings_response.rb +56 -0
  160. data/lib/friendly_shipping/services/ups_json/parse_void_response.rb +32 -0
  161. data/lib/friendly_shipping/services/ups_json/rates_item_options.rb +22 -0
  162. data/lib/friendly_shipping/services/ups_json/rates_options.rb +113 -0
  163. data/lib/friendly_shipping/services/ups_json/rates_package_options.rb +17 -0
  164. data/lib/friendly_shipping/services/ups_json/shipping_methods.rb +111 -0
  165. data/lib/friendly_shipping/services/ups_json/timings_options.rb +33 -0
  166. data/lib/friendly_shipping/services/ups_json.rb +216 -0
  167. data/lib/friendly_shipping/services/usps/choose_package_rate.rb +3 -3
  168. data/lib/friendly_shipping/services/usps/machinable_package.rb +1 -1
  169. data/lib/friendly_shipping/services/usps/parse_package_rate.rb +6 -6
  170. data/lib/friendly_shipping/services/usps/parse_time_in_transit_response.rb +6 -6
  171. data/lib/friendly_shipping/services/usps/rate_estimate_options.rb +1 -1
  172. data/lib/friendly_shipping/services/usps/rate_estimate_package_options.rb +6 -1
  173. data/lib/friendly_shipping/services/usps/serialize_rate_request.rb +2 -2
  174. data/lib/friendly_shipping/services/usps/shipping_methods.rb +4 -3
  175. data/lib/friendly_shipping/services/usps.rb +1 -1
  176. data/lib/friendly_shipping/services/usps_international/parse_package_rate.rb +3 -3
  177. data/lib/friendly_shipping/services/usps_international/rate_estimate_options.rb +1 -1
  178. data/lib/friendly_shipping/services/usps_international/serialize_rate_request.rb +1 -1
  179. data/lib/friendly_shipping/services/usps_international.rb +1 -1
  180. data/lib/friendly_shipping/services/usps_ship/access_token.rb +37 -0
  181. data/lib/friendly_shipping/services/usps_ship/api_error.rb +29 -0
  182. data/lib/friendly_shipping/services/usps_ship/machinable_package.rb +53 -0
  183. data/lib/friendly_shipping/services/usps_ship/parse_rate_estimates_response.rb +80 -0
  184. data/lib/friendly_shipping/services/usps_ship/parse_timings_response.rb +80 -0
  185. data/lib/friendly_shipping/services/usps_ship/rate_estimate_options.rb +45 -0
  186. data/lib/friendly_shipping/services/usps_ship/rate_estimate_package_options.rb +124 -0
  187. data/lib/friendly_shipping/services/usps_ship/serialize_rate_estimates_request.rb +55 -0
  188. data/lib/friendly_shipping/services/usps_ship/shipping_methods.rb +38 -0
  189. data/lib/friendly_shipping/services/{ship_engine_ltl/bad_request.rb → usps_ship/timing_options.rb} +2 -2
  190. data/lib/friendly_shipping/services/usps_ship.rb +199 -0
  191. data/lib/friendly_shipping/shipment_options.rb +13 -1
  192. data/lib/friendly_shipping/shipping_method.rb +38 -11
  193. data/lib/friendly_shipping/structure_options.rb +38 -0
  194. data/lib/friendly_shipping/timing.rb +42 -7
  195. data/lib/friendly_shipping/version.rb +1 -1
  196. data/lib/friendly_shipping.rb +6 -0
  197. metadata +140 -174
  198. data/lib/friendly_shipping/services/ship_engine/bad_request.rb +0 -29
  199. data/lib/friendly_shipping/services/ship_engine/bad_request_handler.rb +0 -33
  200. data/lib/friendly_shipping/services/ship_engine_ltl/bad_request_handler.rb +0 -33
@@ -5,14 +5,14 @@ module FriendlyShipping
5
5
  class Usps
6
6
  class ParsePackageRate
7
7
  # USPS returns all the info about a rate in a long string with a bit of gibberish.
8
- ESCAPING_AND_SYMBOLS = /<\S*>/.freeze
8
+ ESCAPING_AND_SYMBOLS = /<\S*>/
9
9
 
10
10
  # At the beginning of the long String, USPS keeps a copy of its own name. We know we're dealing with
11
11
  # them though, so we can filter that out, too.
12
- LEADING_USPS = /^USPS /.freeze
12
+ LEADING_USPS = /^USPS /
13
13
 
14
14
  # This combines all the things we want to filter out.
15
- SERVICE_NAME_SUBSTITUTIONS = /#{ESCAPING_AND_SYMBOLS}|#{LEADING_USPS}/.freeze
15
+ SERVICE_NAME_SUBSTITUTIONS = /#{ESCAPING_AND_SYMBOLS}|#{LEADING_USPS}/
16
16
 
17
17
  # Often we get a multitude of rates for the same service given some combination of
18
18
  # Box type and (see below) and "Hold for Pickup" service. This creates a regular expression
@@ -39,15 +39,15 @@ module FriendlyShipping
39
39
  }.map { |k, v| "(?<#{k}>#{v})" }.join("|").freeze
40
40
 
41
41
  # We use this for identifying rates that use the Hold for Pickup service.
42
- HOLD_FOR_PICKUP = /Hold for Pickup/i.freeze
42
+ HOLD_FOR_PICKUP = /Hold for Pickup/i
43
43
 
44
44
  # For most rate options, USPS will return how many business days it takes to deliver this
45
45
  # package in the format "{1,2,3}-Day". We can filter this out using the below Regex.
46
- DAYS_TO_DELIVERY = /(?<days>\d)-Day/.freeze
46
+ DAYS_TO_DELIVERY = /(?<days>\d)-Day/
47
47
 
48
48
  # When delivering to military ZIP codes, we don't actually get a timing estimate, but instead the string
49
49
  # "Military". We use this to indicate that this rate is for a military zip code in the rates' data Hash.
50
- MILITARY = /MILITARY/i.freeze
50
+ MILITARY = /MILITARY/i
51
51
 
52
52
  # The tags used in the rate node that we get information from.
53
53
  SERVICE_CODE_TAG = 'CLASSID'
@@ -51,10 +51,10 @@ module FriendlyShipping
51
51
  potential_shipping_method.name == MAIL_CLASSES[commitment_node.at('MailClass').text]
52
52
  end
53
53
  commitment_sequence = commitment_node.at('CommitmentSeq').text
54
- properties = COMMITMENT_SEQUENCES[commitment_sequence]
55
- next unless properties # Sometimes USPS returns an invalid CommitmentSeq
54
+ data = COMMITMENT_SEQUENCES[commitment_sequence]
55
+ next unless data # Sometimes USPS returns an invalid CommitmentSeq
56
56
 
57
- scheduled_delivery_time = properties.delete(:commitment_time)
57
+ scheduled_delivery_time = data.delete(:commitment_time)
58
58
  scheduled_delivery_date = commitment_node.at('SDD').text
59
59
  parsed_delivery_time = Time.parse("#{scheduled_delivery_date} #{scheduled_delivery_time}")
60
60
  guaranteed = commitment_node.at('IsGuaranteed').text == '1'
@@ -64,7 +64,7 @@ module FriendlyShipping
64
64
  pickup: effective_acceptance_date,
65
65
  delivery: parsed_delivery_time,
66
66
  guaranteed: guaranteed,
67
- properties: properties
67
+ data: data
68
68
  )
69
69
  end.compact
70
70
  end
@@ -80,7 +80,7 @@ module FriendlyShipping
80
80
  warning_text = commitment_node.xpath('HFPU//NonExpeditedTransMsg/Msg')&.text
81
81
  warning = warning_text unless warning_text.empty?
82
82
 
83
- properties = {
83
+ data = {
84
84
  commitment: commitment_node.at('SvcStdMsg')&.text,
85
85
  destination_type: NON_EXPEDITED_DESTINATION_TYPES[commitment_node.at('NonExpeditedDestType').text],
86
86
  warning: warning
@@ -95,7 +95,7 @@ module FriendlyShipping
95
95
  pickup: effective_acceptance_date,
96
96
  delivery: parsed_delivery_time,
97
97
  guaranteed: false,
98
- properties: properties
98
+ data: data
99
99
  )
100
100
  end.compact
101
101
  end
@@ -20,7 +20,7 @@ module FriendlyShipping
20
20
  package_options_class: FriendlyShipping::Services::Usps::RateEstimatePackageOptions,
21
21
  **kwargs
22
22
  )
23
- super(**kwargs.merge(package_options_class: package_options_class))
23
+ super(**kwargs.reverse_merge(package_options_class: package_options_class))
24
24
  end
25
25
  end
26
26
  end
@@ -42,8 +42,13 @@ module FriendlyShipping
42
42
  CONTAINERS.fetch(box_name)
43
43
  end
44
44
 
45
+ # @return [String, nil]
45
46
  def first_class_mail_type_code
46
- FIRST_CLASS_MAIL_TYPES[first_class_mail_type]
47
+ if %i[parcel package_service package_service_retail].include?(first_class_mail_type)
48
+ warn "[DEPRECATION] First Class `:#{first_class_mail_type}` has been replaced by Ground Advantage."
49
+ else
50
+ FIRST_CLASS_MAIL_TYPES[first_class_mail_type]
51
+ end
47
52
  end
48
53
 
49
54
  def service_code
@@ -15,7 +15,7 @@ module FriendlyShipping
15
15
  # @param [String] login The USPS login code
16
16
  # @param [FriendlyShipping::Services::Usps::RateEstimateOptions] options The options
17
17
  # object to use with this request.
18
- # @return Array<[FriendlyShipping::Rate]> A set of Rates that this package may be sent with
18
+ # @return [Array<FriendlyShipping::Rate>] A set of Rates that this package may be sent with
19
19
  def call(shipment:, login:, options:)
20
20
  xml_builder = Nokogiri::XML::Builder.new do |xml|
21
21
  xml.RateV4Request('USERID' => login) do
@@ -23,7 +23,7 @@ module FriendlyShipping
23
23
  package_options = options.options_for_package(package)
24
24
  xml.Package('ID' => index) do
25
25
  xml.Service(package_options.service_code)
26
- if package_options.first_class_mail_type
26
+ if package_options.first_class_mail_type && package_options.first_class_mail_type_code
27
27
  xml.FirstClassMailType(package_options.first_class_mail_type_code)
28
28
  end
29
29
  xml.ZipOrigination(shipment.origin.zip)
@@ -23,10 +23,11 @@ module FriendlyShipping
23
23
  FIRST_CLASS_MAIL_TYPES = {
24
24
  letter: 'LETTER',
25
25
  flat: 'FLAT',
26
- parcel: 'PARCEL',
26
+ parcel: 'PARCEL', # @deprecated
27
27
  post_card: 'POSTCARD',
28
- package_service: 'PACKAGE SERVICE',
29
- package_service_retail: 'PACKAGE SERVICE RETAIL'
28
+ large_post_card: 'LARGE POSTCARD',
29
+ package_service: 'PACKAGE SERVICE', # @deprecated
30
+ package_service_retail: 'PACKAGE SERVICE RETAIL' # @deprecated
30
31
  }.freeze
31
32
 
32
33
  CLASS_IDS = {
@@ -16,7 +16,7 @@ require 'friendly_shipping/services/usps/rate_estimate_options'
16
16
  module FriendlyShipping
17
17
  module Services
18
18
  class Usps
19
- include Dry::Monads[:result]
19
+ include Dry::Monads::Result::Mixin
20
20
 
21
21
  attr_reader :test, :login, :client
22
22
 
@@ -5,14 +5,14 @@ module FriendlyShipping
5
5
  class UspsInternational
6
6
  class ParsePackageRate
7
7
  # USPS returns all the info about a rate in a long string with a bit of gibberish.
8
- ESCAPING_AND_SYMBOLS = /&lt;\S*&gt;/.freeze
8
+ ESCAPING_AND_SYMBOLS = /&lt;\S*&gt;/
9
9
 
10
10
  # At the beginning of the long String, USPS keeps a copy of its own name. We know we're dealing with
11
11
  # them though, so we can filter that out, too.
12
- LEADING_USPS = /^USPS /.freeze
12
+ LEADING_USPS = /^USPS /
13
13
 
14
14
  # This combines all the things we want to filter out.
15
- SERVICE_NAME_SUBSTITUTIONS = /#{ESCAPING_AND_SYMBOLS}|#{LEADING_USPS}/.freeze
15
+ SERVICE_NAME_SUBSTITUTIONS = /#{ESCAPING_AND_SYMBOLS}|#{LEADING_USPS}/
16
16
 
17
17
  # Often we get a multitude of rates for the same service given some combination of
18
18
  # Box type and (see below) and "Hold for Pickup" service. This creates a regular expression
@@ -20,7 +20,7 @@ module FriendlyShipping
20
20
  package_options_class: FriendlyShipping::Services::UspsInternational::RateEstimatePackageOptions,
21
21
  **kwargs
22
22
  )
23
- super(**kwargs.merge(package_options_class: package_options_class))
23
+ super(**kwargs.reverse_merge(package_options_class: package_options_class))
24
24
  end
25
25
  end
26
26
  end
@@ -13,7 +13,7 @@ module FriendlyShipping
13
13
  # @param [String] login The USPS login code
14
14
  # @param [FriendlyShipping::Services::UspsInternational::RateEstimateOptions] options The options
15
15
  # object to use with this request.
16
- # @return Array<[FriendlyShipping::Rate]> A set of Rates that this package may be sent with
16
+ # @return [Array<FriendlyShipping::Rate>] A set of Rates that this package may be sent with
17
17
  def call(shipment:, login:, options:)
18
18
  xml_builder = Nokogiri::XML::Builder.new do |xml|
19
19
  xml.IntlRateV2Request('USERID' => login) do
@@ -9,7 +9,7 @@ require 'friendly_shipping/services/usps_international/rate_estimate_options'
9
9
  module FriendlyShipping
10
10
  module Services
11
11
  class UspsInternational
12
- include Dry::Monads[:result]
12
+ include Dry::Monads::Result::Mixin
13
13
 
14
14
  attr_reader :test, :login, :client
15
15
 
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'jwt'
4
+
5
+ module FriendlyShipping
6
+ module Services
7
+ class USPSShip
8
+ # Represents an access token returned by USPS Ship. The access token can be
9
+ # used to make API requests. Once it expires, a new token must be created.
10
+ class AccessToken
11
+ # @return [String] the token's type
12
+ attr_reader :token_type
13
+
14
+ # @return [Integer] the token's expiration
15
+ attr_reader :expires_in
16
+
17
+ # @return [String] the raw JWT token
18
+ attr_reader :raw_token
19
+
20
+ # @param token_type [String] the token's type (typically "Bearer")
21
+ # @param expires_in [Integer] the token's expiration
22
+ # @param raw_token [String] the raw JWT token
23
+ def initialize(token_type:, expires_in:, raw_token:)
24
+ @token_type = token_type
25
+ @expires_in = expires_in
26
+ @raw_token = raw_token
27
+ end
28
+
29
+ # Decodes and returns the raw JWT token.
30
+ # @return [Array<Hash>] the decoded token
31
+ def decoded_token
32
+ @_decoded_token = JWT.decode(raw_token, nil, false)
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'friendly_shipping/api_error'
4
+
5
+ module FriendlyShipping
6
+ module Services
7
+ class USPSShip
8
+ class ApiError < FriendlyShipping::ApiError
9
+ # @param [RestClient::Exception] cause
10
+ def initialize(cause)
11
+ super(cause, parse_message(cause))
12
+ end
13
+
14
+ private
15
+
16
+ # @param [RestClient::Exception] error
17
+ # @return [String]
18
+ def parse_message(error)
19
+ return error.message unless error.response
20
+
21
+ parsed_json = JSON.parse(error.response.body)
22
+ parsed_json.dig("error", "message")
23
+ rescue JSON::ParserError, KeyError => _e
24
+ nil
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FriendlyShipping
4
+ module Services
5
+ class USPSShip
6
+ # USPS has certain size and weight requirements for packages to be considered
7
+ # machinable. Machinable packages are generally less expensive to ship.
8
+ # @see https://pe.usps.com/BusinessMail101?ViewName=Parcels
9
+ #
10
+ class MachinablePackage
11
+ # @return [Physical::Package]
12
+ attr_reader :package
13
+
14
+ MIN_LENGTH = Measured::Length(6, :inches)
15
+ MIN_WIDTH = Measured::Length(3, :inches)
16
+ MIN_HEIGHT = Measured::Length(0.25, :inches)
17
+
18
+ MAX_LENGTH = Measured::Length(27, :inches)
19
+ MAX_WIDTH = Measured::Length(17, :inches)
20
+ MAX_HEIGHT = Measured::Length(17, :inches)
21
+
22
+ MAX_WEIGHT = Measured::Weight(25, :pounds)
23
+
24
+ # @param package [Physical::Package]
25
+ def initialize(package)
26
+ @package = package
27
+ end
28
+
29
+ # @return [Boolean]
30
+ def machinable?
31
+ at_least_minimum && at_most_maximum
32
+ end
33
+
34
+ private
35
+
36
+ # @return [Boolean]
37
+ def at_least_minimum
38
+ package.length >= MIN_LENGTH &&
39
+ package.width >= MIN_WIDTH &&
40
+ package.height >= MIN_HEIGHT
41
+ end
42
+
43
+ # @return [Boolean]
44
+ def at_most_maximum
45
+ package.length <= MAX_LENGTH &&
46
+ package.width <= MAX_WIDTH &&
47
+ package.height <= MAX_HEIGHT &&
48
+ package.weight <= MAX_WEIGHT
49
+ end
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,80 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FriendlyShipping
4
+ module Services
5
+ class USPSShip
6
+ class ParseRateEstimatesResponse
7
+ extend Dry::Monads::Result::Mixin
8
+ CURRENCY = Money::Currency.new('USD').freeze
9
+
10
+ class << self
11
+ # Parse a rate estimates response.
12
+ #
13
+ # @param request [Request] the request that was used to obtain this response
14
+ # @param response [Response] the response that USPS returned
15
+ # @return [Success<ApiResult<Array<Rate>>>, Failure<ApiFailure<String>>]
16
+ def call(request:, response:)
17
+ rates = JSON.parse(response.body)['rates'].map do |rate|
18
+ shipping_method = SHIPPING_METHODS.detect { |sm| sm.service_code == rate['mailClass'] }
19
+ amounts = { price: money(rate['price']) }
20
+
21
+ # Add any additional fees to the amounts hash
22
+ rate['fees'].each_with_object(amounts) do |fee, result|
23
+ result[fee['name']] = money(fee['price'])
24
+ end
25
+
26
+ FriendlyShipping::Rate.new(
27
+ amounts: amounts,
28
+ shipping_method: shipping_method,
29
+ data: {
30
+ description: rate['description'],
31
+ zone: rate['zone']
32
+ }
33
+ )
34
+ end
35
+
36
+ success(rates, request, response)
37
+ rescue JSON::ParserError, KeyError => e
38
+ failure(e.message, request, response)
39
+ end
40
+
41
+ private
42
+
43
+ # @param value [Numeric]
44
+ # @return [Money]
45
+ def money(value)
46
+ Money.new(value * CURRENCY.subunit_to_unit, CURRENCY)
47
+ end
48
+
49
+ # @param rates [Array<Rate>]
50
+ # @param request [Request]
51
+ # @param response [Response]
52
+ # @return [Success<ApiResult<Array<Rate>>]
53
+ def success(rates, request, response)
54
+ Success(
55
+ ApiResult.new(
56
+ rates,
57
+ original_request: request,
58
+ original_response: response
59
+ )
60
+ )
61
+ end
62
+
63
+ # @param message [String]
64
+ # @param request [Request]
65
+ # @param response [Response]
66
+ # @return [Failure<ApiFailure<String>>]
67
+ def failure(message, request, response)
68
+ Failure(
69
+ ApiFailure.new(
70
+ message,
71
+ original_request: request,
72
+ original_response: response
73
+ )
74
+ )
75
+ end
76
+ end
77
+ end
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,80 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'friendly_shipping/timing'
4
+
5
+ module FriendlyShipping
6
+ module Services
7
+ class USPSShip
8
+ class ParseTimingsResponse
9
+ extend Dry::Monads::Result::Mixin
10
+
11
+ class << self
12
+ # Parse a timings response.
13
+ #
14
+ # @param request [Request] the request that was used to obtain this response
15
+ # @param response [Response] the response that USPS returned
16
+ # @return [Success<ApiResult<Array<Timing>>>, Failure<ApiFailure<String>>]
17
+ def call(request:, response:)
18
+ timings = JSON.parse(response.body).map do |timing|
19
+ shipping_method = SHIPPING_METHODS.detect { |sm| sm.service_code == timing['mailClass'] }
20
+
21
+ # The delivery estimate is blank if an invalid destination zip code was used
22
+ delivery = timing.dig('delivery', 'scheduledDeliveryDateTime')
23
+ next unless delivery
24
+
25
+ FriendlyShipping::Timing.new(
26
+ shipping_method: shipping_method,
27
+ pickup: Time.parse(timing['acceptanceDateTime']),
28
+ delivery: Time.parse(delivery),
29
+ guaranteed: timing['delivery']['guaranteedDelivery'],
30
+ data: {
31
+ notes: timing['notes'],
32
+ service_standard: timing['serviceStandard'],
33
+ service_standard_message: timing['serviceStandardMessage']
34
+ }
35
+ )
36
+ end.compact
37
+
38
+ if timings.empty?
39
+ failure("No timings were returned. Is the destination zip correct?", request, response)
40
+ else
41
+ success(timings, request, response)
42
+ end
43
+ rescue JSON::ParserError => e
44
+ failure(e.message, request, response)
45
+ end
46
+
47
+ private
48
+
49
+ # @param timings [Array<Timing>]
50
+ # @param request [Request]
51
+ # @param response [Response]
52
+ # @return [Success<ApiResult<Array<Timing>>]
53
+ def success(timings, request, response)
54
+ Success(
55
+ ApiResult.new(
56
+ timings,
57
+ original_request: request,
58
+ original_response: response
59
+ )
60
+ )
61
+ end
62
+
63
+ # @param message [String]
64
+ # @param request [Request]
65
+ # @param response [Response]
66
+ # @return [Failure<ApiFailure<String>>]
67
+ def failure(message, request, response)
68
+ Failure(
69
+ ApiFailure.new(
70
+ message,
71
+ original_request: request,
72
+ original_response: response
73
+ )
74
+ )
75
+ end
76
+ end
77
+ end
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FriendlyShipping
4
+ module Services
5
+ class USPSShip
6
+ class RateEstimateOptions < FriendlyShipping::ShipmentOptions
7
+ DESTINATION_ENTRY_FACILITY_TYPES = {
8
+ none: "NONE",
9
+ destination_network_distribution_center: "DESTINATION_NETWORK_DISTRIBUTION_CENTER",
10
+ destination_sectional_center_facility: "DESTINATION_SECTIONAL_CENTER_FACILITY",
11
+ destination_delivery_unit: "DESTINATION_DELIVERY_UNIT",
12
+ destination_service_hub: "DESTINATION_SERVICE_HUB"
13
+ }.freeze
14
+
15
+ # @return [ShippingMethod]
16
+ attr_reader :shipping_method
17
+
18
+ # @return [String]
19
+ attr_reader :destination_entry_facility_type
20
+
21
+ # @return [#strftime]
22
+ attr_reader :mailing_date
23
+
24
+ # @param shipping_method [ShippingMethod] the shipping method for which we want a rate
25
+ # @param destination_entry_facility_type [Symbol] one of {DESTINATION_ENTRY_FACILITY_TYPES}
26
+ # @param mailing_date [#strftime] the date on which we want to ship
27
+ # @param package_options_class [Class] the class to use for package options
28
+ # @param kwargs [Hash]
29
+ # @option kwargs [Array<PackageOptions>] :package_options the options for packages in this shipment
30
+ def initialize(
31
+ shipping_method:,
32
+ destination_entry_facility_type: :none,
33
+ mailing_date: Date.today,
34
+ package_options_class: FriendlyShipping::Services::USPSShip::RateEstimatePackageOptions,
35
+ **kwargs
36
+ )
37
+ @shipping_method = shipping_method
38
+ @destination_entry_facility_type = DESTINATION_ENTRY_FACILITY_TYPES.fetch(destination_entry_facility_type)
39
+ @mailing_date = mailing_date
40
+ super(**kwargs.reverse_merge(package_options_class: package_options_class))
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,124 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'friendly_shipping/services/ups/rate_estimate_package_options'
4
+
5
+ module FriendlyShipping
6
+ module Services
7
+ class USPSShip
8
+ class RateEstimatePackageOptions < FriendlyShipping::PackageOptions
9
+ PROCESSING_CATEGORIES = {
10
+ letters: "LETTERS",
11
+ flats: "FLATS",
12
+ machinable: "MACHINABLE",
13
+ irregular: "IRREGULAR",
14
+ non_machinable: "NON_MACHINABLE"
15
+ }.freeze
16
+
17
+ RATE_INDICATORS = {
18
+ three_digit: "3D",
19
+ three_digit_dimensional_rectangular: "3N",
20
+ three_digit_dimensional_nonrectangular: "3R",
21
+ five_digit: "5D",
22
+ basic: "BA",
23
+ mixed_ndc: "BB",
24
+ ndc: "BM",
25
+ cubic_pricing_tier_1: "C1",
26
+ cubic_pricing_tier_2: "C2",
27
+ cubic_pricing_tier_3: "C3",
28
+ cubic_pricing_tier_4: "C4",
29
+ cubic_pricing_tier_5: "C5",
30
+ cubic_parcel: "CP",
31
+ usps_connect_local_mail: "CM",
32
+ ndc_2: "DC",
33
+ scf: "DE",
34
+ five_digit_2: "DF",
35
+ dimensional_nonrectangular: "DN",
36
+ dimensional_rectangular: "DR",
37
+ priority_mail_express_flat_rate_envelope_post_office_to_addressee: "E4",
38
+ priority_mail_express_legal_flat_rate_envelope: "E6",
39
+ priority_mail_express_legal_flat_rate_envelope_sunday_holiday: "E7",
40
+ legal_flat_rate_envelope: "FA",
41
+ medium_flat_rate_box_large_flat_rate_bag: "FB",
42
+ flat_rate_envelope: "FE",
43
+ padded_flat_rate_envelope: "FP",
44
+ small_flat_rate_box: "FS",
45
+ usps_connect_same_day_single_piece: "L1",
46
+ usps_connect_same_day_small_flat_rate_bag: "L2",
47
+ usps_connect_same_day_large_flat_rate_bag: "L3",
48
+ usps_connect_same_day_flat_rate_box: "L4",
49
+ usps_connect_same_day_oversized: "L5",
50
+ usps_connect_local_single_piece: "LC",
51
+ flat_rate_box: "LF",
52
+ large_flat_rate_bag: "LL",
53
+ usps_connect_local_oversized: "LO",
54
+ small_flat_rate_bag: "LS",
55
+ non_presorted: "NP",
56
+ full_tray_box: "O1",
57
+ half_tray_box: "O2",
58
+ emm_tray_box: "O3",
59
+ flat_tub_tray_box: "O4",
60
+ surface_transported_pallet: "O5",
61
+ full_pallet_box: "O6",
62
+ half_pallet_box: "O7",
63
+ oversized: "OS",
64
+ cubic_soft_pack_tier_1: "P5",
65
+ cubic_soft_pack_tier_2: "P6",
66
+ cubic_soft_pack_tier_3: "P7",
67
+ cubic_soft_pack_tier_4: "P8",
68
+ cubic_soft_pack_tier_5: "P9",
69
+ cubic_soft_pack_tier_6: "Q6",
70
+ cubic_soft_pack_tier_7: "Q7",
71
+ cubic_soft_pack_tier_8: "Q8",
72
+ cubic_soft_pack_tier_9: "Q9",
73
+ cubic_soft_pack_tier_10: "Q0",
74
+ priority_mail_express_single_piece: "PA",
75
+ large_flat_rate_box: "PL",
76
+ large_flat_rate_box_apo_fpo_dpo: "PM",
77
+ presorted: "PR",
78
+ usps_connect_next_day_single_piece: "R1",
79
+ usps_connect_next_day_dimensional_nonrectangular: "R2",
80
+ usps_connect_next_day_dimensional_rectangular: "R3",
81
+ usps_connect_next_day_oversized: "R4",
82
+ small_flat_rate_bag_2: "SB",
83
+ scf_dimensional_nonrectangular: "SN",
84
+ single_piece: "SP",
85
+ scf_dimensional_rectangular: "SR"
86
+ }.freeze
87
+
88
+ PRICE_TYPES = {
89
+ retail: "RETAIL",
90
+ commercial: "COMMERCIAL",
91
+ contract: "CONTRACT"
92
+ }.freeze
93
+
94
+ # @return [String]
95
+ attr_reader :processing_category
96
+
97
+ # @return [String]
98
+ attr_reader :rate_indicator
99
+
100
+ # @return [String]
101
+ attr_reader :price_type
102
+
103
+ # @param processing_category [Symbol] one of {PROCESSING_CATEGORIES}
104
+ # @param rate_indicator [Symbol] one of {RATE_INDICATORS}
105
+ # @param price_type [Symbol] one of {PRICE_TYPES}
106
+ # @param kwargs [Hash]
107
+ # @option [String] :package_id the ID for the package that belongs to these options
108
+ # @option [Array<ItemOptions>] :item_options the options for items in this package
109
+ # @option [Class] :item_options_class the class to use for item options when none are provided
110
+ def initialize(
111
+ processing_category: :machinable,
112
+ rate_indicator: :single_piece,
113
+ price_type: :retail,
114
+ **kwargs
115
+ )
116
+ @processing_category = PROCESSING_CATEGORIES.fetch(processing_category)
117
+ @rate_indicator = RATE_INDICATORS.fetch(rate_indicator)
118
+ @price_type = PRICE_TYPES.fetch(price_type)
119
+ super(**kwargs)
120
+ end
121
+ end
122
+ end
123
+ end
124
+ end