friendly_shipping 0.8.1 → 0.10.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (217) 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 +17 -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/README.md +1 -1
  13. data/friendly_shipping.gemspec +8 -17
  14. data/lib/friendly_shipping/access_token.rb +24 -0
  15. data/lib/friendly_shipping/api_error.rb +8 -4
  16. data/lib/friendly_shipping/api_error_handler.rb +11 -9
  17. data/lib/friendly_shipping/api_failure.rb +2 -7
  18. data/lib/friendly_shipping/api_result.rb +26 -4
  19. data/lib/friendly_shipping/carrier.rb +28 -8
  20. data/lib/friendly_shipping/http_client.rb +25 -8
  21. data/lib/friendly_shipping/inflections.rb +10 -0
  22. data/lib/friendly_shipping/item_options.rb +3 -0
  23. data/lib/friendly_shipping/label.rb +41 -22
  24. data/lib/friendly_shipping/package_options.rb +21 -2
  25. data/lib/friendly_shipping/rate.rb +50 -15
  26. data/lib/friendly_shipping/request.rb +23 -7
  27. data/lib/friendly_shipping/response.rb +21 -6
  28. data/lib/friendly_shipping/services/rl/api_error.rb +31 -0
  29. data/lib/friendly_shipping/services/rl/bol_options.rb +103 -0
  30. data/lib/friendly_shipping/services/rl/bol_packages_serializer.rb +32 -0
  31. data/lib/friendly_shipping/services/rl/bol_structures_serializer.rb +31 -0
  32. data/lib/friendly_shipping/services/rl/item_options.rb +36 -0
  33. data/lib/friendly_shipping/services/rl/package_options.rb +38 -0
  34. data/lib/friendly_shipping/services/rl/parse_create_bol_response.rb +43 -0
  35. data/lib/friendly_shipping/services/rl/parse_invoice_response.rb +47 -0
  36. data/lib/friendly_shipping/services/rl/parse_print_bol_response.rb +44 -0
  37. data/lib/friendly_shipping/services/rl/parse_print_shipping_labels_response.rb +44 -0
  38. data/lib/friendly_shipping/services/rl/parse_rate_quote_response.rb +93 -0
  39. data/lib/friendly_shipping/services/rl/parse_transit_times_response.rb +73 -0
  40. data/lib/friendly_shipping/services/rl/rate_quote_options.rb +82 -0
  41. data/lib/friendly_shipping/services/rl/rate_quote_packages_serializer.rb +54 -0
  42. data/lib/friendly_shipping/services/rl/rate_quote_structures_serializer.rb +53 -0
  43. data/lib/friendly_shipping/services/rl/serialize_create_bol_request.rb +86 -0
  44. data/lib/friendly_shipping/services/rl/serialize_location.rb +53 -0
  45. data/lib/friendly_shipping/services/rl/serialize_rate_quote_request.rb +69 -0
  46. data/lib/friendly_shipping/services/rl/serialize_transit_times_request.rb +38 -0
  47. data/lib/friendly_shipping/services/rl/shipment_document.rb +40 -0
  48. data/lib/friendly_shipping/services/rl/shipment_information.rb +41 -0
  49. data/lib/friendly_shipping/services/rl/shipment_options.rb +48 -0
  50. data/lib/friendly_shipping/services/rl/shipping_methods.rb +28 -0
  51. data/lib/friendly_shipping/services/rl/structure_options.rb +13 -0
  52. data/lib/friendly_shipping/services/rl.rb +187 -0
  53. data/lib/friendly_shipping/services/ship_engine/api_error.rb +31 -0
  54. data/lib/friendly_shipping/services/ship_engine/customs_items_serializer.rb +36 -0
  55. data/lib/friendly_shipping/services/ship_engine/label_customs_options.rb +10 -7
  56. data/lib/friendly_shipping/services/ship_engine/label_item_options.rb +10 -7
  57. data/lib/friendly_shipping/services/ship_engine/label_options.rb +30 -17
  58. data/lib/friendly_shipping/services/ship_engine/label_package_options.rb +18 -14
  59. data/lib/friendly_shipping/services/ship_engine/parse_address_validation_response.rb +75 -0
  60. data/lib/friendly_shipping/services/ship_engine/parse_carrier_response.rb +9 -2
  61. data/lib/friendly_shipping/services/ship_engine/parse_label_response.rb +4 -2
  62. data/lib/friendly_shipping/services/ship_engine/{parse_rate_estimate_response.rb → parse_rate_estimates_response.rb} +27 -9
  63. data/lib/friendly_shipping/services/ship_engine/parse_rates_response.rb +99 -0
  64. data/lib/friendly_shipping/services/ship_engine/parse_void_response.rb +5 -1
  65. data/lib/friendly_shipping/services/ship_engine/rate_estimates_options.rb +17 -6
  66. data/lib/friendly_shipping/services/ship_engine/rates_item_options.rb +26 -0
  67. data/lib/friendly_shipping/services/ship_engine/rates_options.rb +59 -0
  68. data/lib/friendly_shipping/services/ship_engine/rates_package_options.rb +18 -0
  69. data/lib/friendly_shipping/services/ship_engine/serialize_address_residential_indicator.rb +27 -0
  70. data/lib/friendly_shipping/services/ship_engine/serialize_address_validation_request.rb +31 -0
  71. data/lib/friendly_shipping/services/ship_engine/serialize_label_shipment.rb +22 -27
  72. data/lib/friendly_shipping/services/ship_engine/serialize_rate_estimate_request.rb +41 -16
  73. data/lib/friendly_shipping/services/ship_engine/serialize_rates_request.rb +126 -0
  74. data/lib/friendly_shipping/services/ship_engine.rb +89 -33
  75. data/lib/friendly_shipping/services/ship_engine_ltl/api_error.rb +12 -0
  76. data/lib/friendly_shipping/services/ship_engine_ltl/item_options.rb +24 -5
  77. data/lib/friendly_shipping/services/ship_engine_ltl/package_options.rb +39 -4
  78. data/lib/friendly_shipping/services/ship_engine_ltl/parse_carrier_response.rb +4 -2
  79. data/lib/friendly_shipping/services/ship_engine_ltl/parse_quote_response.rb +10 -2
  80. data/lib/friendly_shipping/services/ship_engine_ltl/quote_options.rb +29 -11
  81. data/lib/friendly_shipping/services/ship_engine_ltl/serialize_packages.rb +7 -2
  82. data/lib/friendly_shipping/services/ship_engine_ltl/serialize_quote_request.rb +50 -16
  83. data/lib/friendly_shipping/services/ship_engine_ltl/serialize_structures.rb +42 -0
  84. data/lib/friendly_shipping/services/ship_engine_ltl/shipment_options.rb +47 -0
  85. data/lib/friendly_shipping/services/ship_engine_ltl/structure_options.rb +17 -0
  86. data/lib/friendly_shipping/services/ship_engine_ltl.rb +47 -39
  87. data/lib/friendly_shipping/services/tforce_freight/access_token.rb +28 -0
  88. data/lib/friendly_shipping/services/tforce_freight/api_error.rb +40 -0
  89. data/lib/friendly_shipping/services/tforce_freight/bol_options.rb +180 -0
  90. data/lib/friendly_shipping/services/tforce_freight/document_options.rb +100 -0
  91. data/lib/friendly_shipping/services/tforce_freight/generate_commodity_information.rb +92 -0
  92. data/lib/friendly_shipping/services/tforce_freight/generate_create_bol_request_hash.rb +153 -0
  93. data/lib/friendly_shipping/services/tforce_freight/generate_document_options_hash.rb +36 -0
  94. data/lib/friendly_shipping/services/tforce_freight/generate_handling_units_hash.rb +51 -0
  95. data/lib/friendly_shipping/services/tforce_freight/generate_location_hash.rb +25 -0
  96. data/lib/friendly_shipping/services/tforce_freight/generate_pickup_request_hash.rb +111 -0
  97. data/lib/friendly_shipping/services/tforce_freight/generate_rates_request_hash.rb +63 -0
  98. data/lib/friendly_shipping/services/tforce_freight/generate_reference_hash.rb +28 -0
  99. data/lib/friendly_shipping/services/tforce_freight/item_options.rb +93 -0
  100. data/lib/friendly_shipping/services/tforce_freight/package_options.rb +119 -0
  101. data/lib/friendly_shipping/services/tforce_freight/parse_create_bol_response.rb +90 -0
  102. data/lib/friendly_shipping/services/tforce_freight/parse_pickup_response.rb +45 -0
  103. data/lib/friendly_shipping/services/tforce_freight/parse_rates_response.rb +50 -0
  104. data/lib/friendly_shipping/services/tforce_freight/parse_shipment_document.rb +29 -0
  105. data/lib/friendly_shipping/services/tforce_freight/pickup_options.rb +82 -0
  106. data/lib/friendly_shipping/services/tforce_freight/rates_item_options.rb +10 -0
  107. data/lib/friendly_shipping/services/tforce_freight/rates_options.rb +159 -0
  108. data/lib/friendly_shipping/services/tforce_freight/rates_package_options.rb +10 -0
  109. data/lib/friendly_shipping/services/tforce_freight/shipment_document.rb +38 -0
  110. data/lib/friendly_shipping/services/tforce_freight/shipment_information.rb +104 -0
  111. data/lib/friendly_shipping/services/tforce_freight/shipment_options.rb +47 -0
  112. data/lib/friendly_shipping/services/tforce_freight/shipping_methods.rb +25 -0
  113. data/lib/friendly_shipping/services/tforce_freight/structure_options.rb +44 -0
  114. data/lib/friendly_shipping/services/tforce_freight.rb +176 -0
  115. data/lib/friendly_shipping/services/ups/label_item_options.rb +0 -2
  116. data/lib/friendly_shipping/services/ups/label_options.rb +14 -5
  117. data/lib/friendly_shipping/services/ups/label_package_options.rb +1 -3
  118. data/lib/friendly_shipping/services/ups/parse_address_validation_response.rb +1 -1
  119. data/lib/friendly_shipping/services/ups/parse_modifier_element.rb +29 -0
  120. data/lib/friendly_shipping/services/ups/parse_rate_response.rb +11 -3
  121. data/lib/friendly_shipping/services/ups/parse_shipment_accept_response.rb +0 -3
  122. data/lib/friendly_shipping/services/ups/parse_shipment_confirm_response.rb +0 -2
  123. data/lib/friendly_shipping/services/ups/parse_time_in_transit_response.rb +1 -1
  124. data/lib/friendly_shipping/services/ups/parse_void_shipment_response.rb +0 -2
  125. data/lib/friendly_shipping/services/ups/parse_xml_response.rb +1 -1
  126. data/lib/friendly_shipping/services/ups/rate_estimate_options.rb +14 -3
  127. data/lib/friendly_shipping/services/ups/rate_estimate_package_options.rb +0 -2
  128. data/lib/friendly_shipping/services/ups/serialize_access_request.rb +0 -2
  129. data/lib/friendly_shipping/services/ups/serialize_rating_service_selection_request.rb +10 -5
  130. data/lib/friendly_shipping/services/ups/serialize_shipment_accept_request.rb +1 -1
  131. data/lib/friendly_shipping/services/ups/serialize_shipment_confirm_request.rb +2 -4
  132. data/lib/friendly_shipping/services/ups/shipping_methods.rb +1 -1
  133. data/lib/friendly_shipping/services/ups.rb +2 -24
  134. data/lib/friendly_shipping/services/ups_freight/api_error.rb +8 -7
  135. data/lib/friendly_shipping/services/ups_freight/generate_commodity_information.rb +65 -19
  136. data/lib/friendly_shipping/services/ups_freight/generate_freight_rate_request_hash.rb +2 -23
  137. data/lib/friendly_shipping/services/ups_freight/generate_freight_ship_request_hash.rb +1 -27
  138. data/lib/friendly_shipping/services/ups_freight/generate_handling_units_hash.rb +54 -0
  139. data/lib/friendly_shipping/services/ups_freight/label_options.rb +36 -10
  140. data/lib/friendly_shipping/services/ups_freight/label_structure_options.rb +13 -0
  141. data/lib/friendly_shipping/services/ups_freight/parse_freight_label_response.rb +2 -5
  142. data/lib/friendly_shipping/services/ups_freight/parse_freight_rate_response.rb +0 -4
  143. data/lib/friendly_shipping/services/ups_freight/parse_shipment_document.rb +1 -3
  144. data/lib/friendly_shipping/services/ups_freight/rates_item_options.rb +18 -9
  145. data/lib/friendly_shipping/services/ups_freight/rates_options.rb +30 -24
  146. data/lib/friendly_shipping/services/ups_freight/rates_package_options.rb +79 -10
  147. data/lib/friendly_shipping/services/ups_freight/rates_structure_options.rb +44 -0
  148. data/lib/friendly_shipping/services/ups_freight/shipment_information.rb +7 -3
  149. data/lib/friendly_shipping/services/ups_freight/shipment_options.rb +47 -0
  150. data/lib/friendly_shipping/services/ups_freight.rb +2 -20
  151. data/lib/friendly_shipping/services/ups_json/access_token.rb +20 -0
  152. data/lib/friendly_shipping/services/ups_json/api_error.rb +27 -0
  153. data/lib/friendly_shipping/services/ups_json/generate_address_classification_payload.rb +29 -0
  154. data/lib/friendly_shipping/services/ups_json/generate_address_hash.rb +30 -0
  155. data/lib/friendly_shipping/services/ups_json/generate_city_state_lookup_payload.rb +23 -0
  156. data/lib/friendly_shipping/services/ups_json/generate_labels_payload.rb +208 -0
  157. data/lib/friendly_shipping/services/ups_json/generate_package_hash.rb +76 -0
  158. data/lib/friendly_shipping/services/ups_json/generate_rates_payload.rb +84 -0
  159. data/lib/friendly_shipping/services/ups_json/generate_timings_payload.rb +44 -0
  160. data/lib/friendly_shipping/services/ups_json/label.rb +20 -0
  161. data/lib/friendly_shipping/services/ups_json/label_billing_options.rb +41 -0
  162. data/lib/friendly_shipping/services/ups_json/label_item_options.rb +75 -0
  163. data/lib/friendly_shipping/services/ups_json/label_options.rb +174 -0
  164. data/lib/friendly_shipping/services/ups_json/label_package_options.rb +49 -0
  165. data/lib/friendly_shipping/services/ups_json/parse_address_classification_response.rb +31 -0
  166. data/lib/friendly_shipping/services/ups_json/parse_city_state_lookup_response.rb +44 -0
  167. data/lib/friendly_shipping/services/ups_json/parse_json_response.rb +51 -0
  168. data/lib/friendly_shipping/services/ups_json/parse_labels_response.rb +71 -0
  169. data/lib/friendly_shipping/services/ups_json/parse_money_hash.rb +128 -0
  170. data/lib/friendly_shipping/services/ups_json/parse_rate_modifier_hash.rb +28 -0
  171. data/lib/friendly_shipping/services/ups_json/parse_rates_response.rb +107 -0
  172. data/lib/friendly_shipping/services/ups_json/parse_timings_response.rb +56 -0
  173. data/lib/friendly_shipping/services/ups_json/parse_void_response.rb +32 -0
  174. data/lib/friendly_shipping/services/ups_json/rates_item_options.rb +20 -0
  175. data/lib/friendly_shipping/services/ups_json/rates_options.rb +111 -0
  176. data/lib/friendly_shipping/services/ups_json/rates_package_options.rb +17 -0
  177. data/lib/friendly_shipping/services/ups_json/shipping_methods.rb +111 -0
  178. data/lib/friendly_shipping/services/ups_json/timings_options.rb +33 -0
  179. data/lib/friendly_shipping/services/ups_json.rb +216 -0
  180. data/lib/friendly_shipping/services/usps/choose_package_rate.rb +3 -3
  181. data/lib/friendly_shipping/services/usps/machinable_package.rb +1 -1
  182. data/lib/friendly_shipping/services/usps/parse_package_rate.rb +6 -6
  183. data/lib/friendly_shipping/services/usps/parse_rate_response.rb +0 -4
  184. data/lib/friendly_shipping/services/usps/parse_time_in_transit_response.rb +6 -9
  185. data/lib/friendly_shipping/services/usps/parse_xml_response.rb +1 -1
  186. data/lib/friendly_shipping/services/usps/rate_estimate_options.rb +1 -3
  187. data/lib/friendly_shipping/services/usps/rate_estimate_package_options.rb +6 -3
  188. data/lib/friendly_shipping/services/usps/serialize_rate_request.rb +2 -4
  189. data/lib/friendly_shipping/services/usps/shipping_methods.rb +4 -3
  190. data/lib/friendly_shipping/services/usps.rb +2 -13
  191. data/lib/friendly_shipping/services/usps_international/parse_package_rate.rb +3 -3
  192. data/lib/friendly_shipping/services/usps_international/parse_rate_response.rb +0 -3
  193. data/lib/friendly_shipping/services/usps_international/rate_estimate_options.rb +1 -3
  194. data/lib/friendly_shipping/services/usps_international/rate_estimate_package_options.rb +0 -2
  195. data/lib/friendly_shipping/services/usps_international/serialize_rate_request.rb +1 -3
  196. data/lib/friendly_shipping/services/usps_international.rb +2 -6
  197. data/lib/friendly_shipping/services/usps_ship/access_token.rb +22 -0
  198. data/lib/friendly_shipping/services/usps_ship/api_error.rb +27 -0
  199. data/lib/friendly_shipping/services/usps_ship/machinable_package.rb +55 -0
  200. data/lib/friendly_shipping/services/usps_ship/parse_rate_estimates_response.rb +80 -0
  201. data/lib/friendly_shipping/services/usps_ship/parse_timings_response.rb +82 -0
  202. data/lib/friendly_shipping/services/usps_ship/rate_estimate_options.rb +45 -0
  203. data/lib/friendly_shipping/services/usps_ship/rate_estimate_package_options.rb +122 -0
  204. data/lib/friendly_shipping/services/usps_ship/serialize_rate_estimates_request.rb +53 -0
  205. data/lib/friendly_shipping/services/usps_ship/shipping_methods.rb +38 -0
  206. data/lib/friendly_shipping/services/{ship_engine_ltl/bad_request.rb → usps_ship/timing_options.rb} +2 -2
  207. data/lib/friendly_shipping/services/usps_ship.rb +185 -0
  208. data/lib/friendly_shipping/shipment_options.rb +13 -1
  209. data/lib/friendly_shipping/shipping_method.rb +38 -11
  210. data/lib/friendly_shipping/structure_options.rb +38 -0
  211. data/lib/friendly_shipping/timing.rb +42 -7
  212. data/lib/friendly_shipping/version.rb +1 -1
  213. data/lib/friendly_shipping.rb +20 -15
  214. metadata +143 -174
  215. data/lib/friendly_shipping/services/ship_engine/bad_request.rb +0 -29
  216. data/lib/friendly_shipping/services/ship_engine/bad_request_handler.rb +0 -33
  217. data/lib/friendly_shipping/services/ship_engine_ltl/bad_request_handler.rb +0 -33
@@ -0,0 +1,216 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+
5
+ module FriendlyShipping
6
+ module Services
7
+ class UpsJson
8
+ include Dry::Monads[:result]
9
+
10
+ attr_reader :access_token, :test, :client
11
+
12
+ CARRIER = FriendlyShipping::Carrier.new(
13
+ id: 'ups',
14
+ name: 'United Parcel Service',
15
+ code: 'ups',
16
+ shipping_methods: SHIPPING_METHODS
17
+ )
18
+
19
+ TEST_URL = 'https://wwwcie.ups.com'
20
+ LIVE_URL = 'https://onlinetools.ups.com'
21
+
22
+ def initialize(access_token:, test: true, client: nil)
23
+ @access_token = access_token
24
+ @test = test
25
+ error_handler = ApiErrorHandler.new(api_error_class: UpsJson::ApiError)
26
+ @client = client || HttpClient.new(error_handler: error_handler)
27
+ end
28
+
29
+ def carriers
30
+ Success([CARRIER])
31
+ end
32
+
33
+ # Creates an access token to be used for future API requests.
34
+ #
35
+ # @param client_id [String] the Client ID of your UPS application
36
+ # @param client_secret [String] the Client Secret of your UPS application
37
+ # @param merchant_id [String] the shipper number associated with your UPS application
38
+ # @param debug [Boolean] whether to append debug information to the API result
39
+ # @return [ApiResult<AccessToken>] the access token
40
+ def create_access_token(
41
+ client_id:,
42
+ client_secret:,
43
+ merchant_id:,
44
+ debug: false
45
+ )
46
+ request = FriendlyShipping::Request.new(
47
+ url: "#{base_url}/security/v1/oauth/token",
48
+ http_method: "POST",
49
+ body: "grant_type=client_credentials",
50
+ headers: {
51
+ Authorization: "Basic #{Base64.urlsafe_encode64("#{client_id}:#{client_secret}")}",
52
+ Content_Type: "application/x-www-form-urlencoded",
53
+ 'X-Merchant-Id': merchant_id,
54
+ Accept: "application/json"
55
+ },
56
+ debug: debug
57
+ )
58
+ client.post(request).fmap do |response|
59
+ hash = JSON.parse(response.body)
60
+ FriendlyShipping::ApiResult.new(
61
+ AccessToken.new(
62
+ expires_in: hash['expires_in'],
63
+ issued_at: hash['issued_at'],
64
+ raw_token: hash['access_token']
65
+ ),
66
+ original_request: request,
67
+ original_response: response
68
+ )
69
+ end
70
+ end
71
+
72
+ # Get rates for a shipment
73
+ # @param [Physical::Shipment] shipment The shipment we want to get rates for
74
+ # @param [FriendlyShipping::Services::UpsJson::RatesOptions] options What options
75
+ # to use for this rates request
76
+ # @return [Result<ApiResult<Array<Rate>>>] The rates returned from UPS encoded in a
77
+ # `FriendlyShipping::ApiResult` object.
78
+ def rates(shipment, options:, debug: false)
79
+ rate_or_shop = options.shipping_method ? "Rate" : "Shop"
80
+ url = "#{base_url}/api/rating/v#{options.sub_version || '1'}/#{rate_or_shop}"
81
+ headers = required_headers(access_token)
82
+ rates_request_body = GenerateRatesPayload.call(shipment: shipment, options: options).to_json
83
+
84
+ request = FriendlyShipping::Request.new(
85
+ url: url,
86
+ http_method: "POST",
87
+ headers: headers,
88
+ body: rates_request_body,
89
+ debug: debug
90
+ )
91
+
92
+ client.post(request).bind do |response|
93
+ ParseRatesResponse.call(response: response, request: request, shipment: shipment)
94
+ end
95
+ end
96
+
97
+ alias_method :rate_estimates, :rates
98
+
99
+ # Get timing information for a shipment
100
+ # @param [Physical::Shipment] shipment The shipment we want to estimate timings for
101
+ # @param [FriendlyShipping::Services::UpsJson::TimingOptions] options Options for this call
102
+ def timings(shipment, options:, debug: false)
103
+ url = "#{base_url}/api/shipments/v1/transittimes"
104
+ headers = required_headers(access_token).merge(
105
+ "transId" => SecureRandom.uuid,
106
+ "transactionSrc" => "testing" # this is a required field according to https://developer.ups.com/api/reference?loc=en_US#operation/TimeInTransit
107
+ )
108
+ timings_request_body = GenerateTimingsPayload.call(shipment: shipment, options: options).to_json
109
+
110
+ request = FriendlyShipping::Request.new(
111
+ url: url,
112
+ http_method: "POST",
113
+ headers: headers,
114
+ body: timings_request_body,
115
+ debug: debug
116
+ )
117
+
118
+ client.post(request).bind do |response|
119
+ ParseTimingsResponse.call(response: response, request: request, shipment: shipment)
120
+ end
121
+ end
122
+
123
+ # Generate labels for a shipment, aka by UPS as the Shipping Shipment api:
124
+ # https://developer.ups.com/api/reference?loc=en_US#tag/Shipping_other
125
+ # @param [Physical::Shipment] shipment The shipment we want to create labels for
126
+ # @param [FriendlyShipping::Services::UpsJson::LabelOptions] options Options for this call
127
+ # @return [Result<ApiResult<Array<Label>>>] The labels returned from UPS encoded in a
128
+ # `FriendlyShipping::ApiResult` object.
129
+ def labels(shipment, options:, debug: false)
130
+ url = "#{base_url}/api/shipments/v#{options.sub_version || '2205'}/ship"
131
+ # the RequestOption field in the payload is documented as doing city validation but it does not
132
+ url += "?additionaladdressvalidation=city" if options.validate_address
133
+ headers = required_headers(access_token)
134
+ body = GenerateLabelsPayload.call(shipment: shipment, options: options).to_json
135
+ request = FriendlyShipping::Request.new(url: url, http_method: "POST", headers: headers, body: body, debug: debug)
136
+
137
+ client.post(request).bind do |response|
138
+ ParseLabelsResponse.call(response: response, request: request)
139
+ end
140
+ end
141
+
142
+ # Classify an address.
143
+ # @param [Physical::Location] location The address we want to classify
144
+ # @return [Result<ApiResult<String>>] Either `"commercial"`, `"residential"`, or `"unknown"`
145
+ def address_classification(location, debug: false)
146
+ url = "#{base_url}/api/addressvalidation/v1/2"
147
+ headers = required_headers(access_token)
148
+ body = GenerateAddressClassificationPayload.call(location: location).to_json
149
+
150
+ request = FriendlyShipping::Request.new(
151
+ url: url,
152
+ http_method: "POST",
153
+ headers: headers,
154
+ body: body,
155
+ debug: debug
156
+ )
157
+
158
+ client.post(request).bind do |response|
159
+ ParseAddressClassificationResponse.call(response: response, request: request)
160
+ end
161
+ end
162
+
163
+ # Find city and state for a given ZIP code
164
+ # @param [Physical::Location] location A location object with country and ZIP code set
165
+ # @return [Result<ApiResult<Array<Physical::Location>>>] The response data from UPS encoded in a
166
+ # `Physical::Location` object. Country, City and ZIP code will be set, everything else nil.
167
+ def city_state_lookup(location, debug: false)
168
+ url = "#{base_url}/api/addressvalidation/v2/1"
169
+ headers = required_headers(access_token)
170
+ body = GenerateCityStateLookupPayload.call(location: location).to_json
171
+
172
+ request = FriendlyShipping::Request.new(
173
+ url: url,
174
+ http_method: "POST",
175
+ headers: headers,
176
+ body: body,
177
+ debug: debug
178
+ )
179
+
180
+ client.post(request).bind do |response|
181
+ ParseCityStateLookupResponse.call(response: response, request: request)
182
+ end
183
+ end
184
+
185
+ # Void a label, aka by UPS as the Shipping Void Shipment api:
186
+ # https://developer.ups.com/api/reference?loc=en_US#tag/Shipping_other
187
+ # @param [Label] label The label to be voided
188
+ # @return [Result<ApiResult>] The VoidShipmentResponse body from UPS.
189
+ def void(label, debug: false)
190
+ # The docs say to use both the shipment_id and tracking number, but the tracking number seems to work alone.
191
+ # url = "#{base_url}/api/shipments/v1/void/cancel/#{label.shipment_id}?trackingNumber=#{label.tracking_number}"
192
+ url = "#{base_url}/api/shipments/v1/void/cancel/#{label.tracking_number}"
193
+ headers = required_headers(access_token)
194
+ request = FriendlyShipping::Request.new(url: url, http_method: "DELETE", headers: headers, debug: debug)
195
+
196
+ client.delete(request).bind do |response|
197
+ ParseVoidResponse.call(response: response, request: request)
198
+ end
199
+ end
200
+
201
+ private
202
+
203
+ def required_headers(access_token)
204
+ {
205
+ "Authorization" => "Bearer #{access_token}",
206
+ "Content-Type" => "application/json",
207
+ "Accept" => "application/json"
208
+ }
209
+ end
210
+
211
+ def base_url
212
+ test ? TEST_URL : LIVE_URL
213
+ end
214
+ end
215
+ end
216
+ end
@@ -7,13 +7,13 @@ module FriendlyShipping
7
7
  class CannotDetermineRate < StandardError; end
8
8
  # Some shipping rates use 'Flat Rate Boxes', indicating that
9
9
  # they are available for ALL flat rate boxes.
10
- FLAT_RATE_BOX = /Flat Rate Box/i.freeze
10
+ FLAT_RATE_BOX = /Flat Rate Box/i
11
11
 
12
12
  # Select the corresponding rate for a package from all the rates USPS returns to us
13
13
  #
14
14
  # @param [FriendlyShipping::ShippingMethod] shipping_method The shipping method we want to filter by
15
- # @param [Physical::Package] package The package we want to match with a rate
16
- # @param [Array<FriendlyShipping::Rate>] The rates we select from
15
+ # @param [Array<FriendlyShipping::Rate>] rates The rates we select from
16
+ # @param [FriendlyShipping::PackageOptions] package_options The package options we want to match with a rate
17
17
  #
18
18
  # @return [FriendlyShipping::Rate] The rate that most closely matches our package
19
19
  def self.call(shipping_method, rates, package_options)
@@ -21,7 +21,7 @@ module FriendlyShipping
21
21
 
22
22
  MAX_WEIGHT = Measured::Weight(25, :pounds)
23
23
 
24
- # @param [Physical::Package]
24
+ # @param [Physical::Package] package
25
25
  def initialize(package)
26
26
  @package = package
27
27
  end
@@ -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 = /&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
@@ -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'
@@ -1,9 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'friendly_shipping/services/usps/parse_xml_response'
4
- require 'friendly_shipping/services/usps/parse_package_rate'
5
- require 'friendly_shipping/services/usps/choose_package_rate'
6
-
7
3
  module FriendlyShipping
8
4
  module Services
9
5
  class Usps
@@ -1,8 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'friendly_shipping/services/usps/parse_xml_response'
4
- require 'friendly_shipping/timing'
5
-
6
3
  module FriendlyShipping
7
4
  module Services
8
5
  class Usps
@@ -51,10 +48,10 @@ module FriendlyShipping
51
48
  potential_shipping_method.name == MAIL_CLASSES[commitment_node.at('MailClass').text]
52
49
  end
53
50
  commitment_sequence = commitment_node.at('CommitmentSeq').text
54
- properties = COMMITMENT_SEQUENCES[commitment_sequence]
55
- next unless properties # Sometimes USPS returns an invalid CommitmentSeq
51
+ data = COMMITMENT_SEQUENCES[commitment_sequence]
52
+ next unless data # Sometimes USPS returns an invalid CommitmentSeq
56
53
 
57
- scheduled_delivery_time = properties.delete(:commitment_time)
54
+ scheduled_delivery_time = data.delete(:commitment_time)
58
55
  scheduled_delivery_date = commitment_node.at('SDD').text
59
56
  parsed_delivery_time = Time.parse("#{scheduled_delivery_date} #{scheduled_delivery_time}")
60
57
  guaranteed = commitment_node.at('IsGuaranteed').text == '1'
@@ -64,7 +61,7 @@ module FriendlyShipping
64
61
  pickup: effective_acceptance_date,
65
62
  delivery: parsed_delivery_time,
66
63
  guaranteed: guaranteed,
67
- properties: properties
64
+ data: data
68
65
  )
69
66
  end.compact
70
67
  end
@@ -80,7 +77,7 @@ module FriendlyShipping
80
77
  warning_text = commitment_node.xpath('HFPU//NonExpeditedTransMsg/Msg')&.text
81
78
  warning = warning_text unless warning_text.empty?
82
79
 
83
- properties = {
80
+ data = {
84
81
  commitment: commitment_node.at('SvcStdMsg')&.text,
85
82
  destination_type: NON_EXPEDITED_DESTINATION_TYPES[commitment_node.at('NonExpeditedDestType').text],
86
83
  warning: warning
@@ -95,7 +92,7 @@ module FriendlyShipping
95
92
  pickup: effective_acceptance_date,
96
93
  delivery: parsed_delivery_time,
97
94
  guaranteed: false,
98
- properties: properties
95
+ data: data
99
96
  )
100
97
  end.compact
101
98
  end
@@ -36,7 +36,7 @@ module FriendlyShipping
36
36
 
37
37
  def wrap_failure(failure, request, response)
38
38
  Failure(
39
- FriendlyShipping::ApiFailure.new(
39
+ FriendlyShipping::ApiResult.new(
40
40
  failure,
41
41
  original_request: request,
42
42
  original_response: response
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'friendly_shipping/services/usps/rate_estimate_package_options'
4
-
5
3
  module FriendlyShipping
6
4
  module Services
7
5
  # Option container for rating a shipment via USPS
@@ -20,7 +18,7 @@ module FriendlyShipping
20
18
  package_options_class: FriendlyShipping::Services::Usps::RateEstimatePackageOptions,
21
19
  **kwargs
22
20
  )
23
- super(**kwargs.merge(package_options_class: package_options_class))
21
+ super(**kwargs.reverse_merge(package_options_class: package_options_class))
24
22
  end
25
23
  end
26
24
  end
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'friendly_shipping/services/ups/rate_estimate_package_options'
4
-
5
3
  module FriendlyShipping
6
4
  module Services
7
5
  # Options for one package when rating
@@ -42,8 +40,13 @@ module FriendlyShipping
42
40
  CONTAINERS.fetch(box_name)
43
41
  end
44
42
 
43
+ # @return [String, nil]
45
44
  def first_class_mail_type_code
46
- FIRST_CLASS_MAIL_TYPES[first_class_mail_type]
45
+ if %i[parcel package_service package_service_retail].include?(first_class_mail_type)
46
+ warn "[DEPRECATION] First Class `:#{first_class_mail_type}` has been replaced by Ground Advantage."
47
+ else
48
+ FIRST_CLASS_MAIL_TYPES[first_class_mail_type]
49
+ end
47
50
  end
48
51
 
49
52
  def service_code
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'friendly_shipping/services/usps/machinable_package'
4
-
5
3
  module FriendlyShipping
6
4
  module Services
7
5
  class Usps
@@ -15,7 +13,7 @@ module FriendlyShipping
15
13
  # @param [String] login The USPS login code
16
14
  # @param [FriendlyShipping::Services::Usps::RateEstimateOptions] options The options
17
15
  # object to use with this request.
18
- # @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
19
17
  def call(shipment:, login:, options:)
20
18
  xml_builder = Nokogiri::XML::Builder.new do |xml|
21
19
  xml.RateV4Request('USERID' => login) do
@@ -23,7 +21,7 @@ module FriendlyShipping
23
21
  package_options = options.options_for_package(package)
24
22
  xml.Package('ID' => index) do
25
23
  xml.Service(package_options.service_code)
26
- if package_options.first_class_mail_type
24
+ if package_options.first_class_mail_type && package_options.first_class_mail_type_code
27
25
  xml.FirstClassMailType(package_options.first_class_mail_type_code)
28
26
  end
29
27
  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 = {
@@ -1,22 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'friendly_shipping/http_client'
4
- require 'friendly_shipping/services/usps/shipping_methods'
5
- require 'friendly_shipping/services/usps/serialize_address_validation_request'
6
- require 'friendly_shipping/services/usps/serialize_city_state_lookup_request'
7
- require 'friendly_shipping/services/usps/serialize_rate_request'
8
- require 'friendly_shipping/services/usps/serialize_time_in_transit_request'
9
- require 'friendly_shipping/services/usps/parse_address_validation_response'
10
- require 'friendly_shipping/services/usps/parse_city_state_lookup_response'
11
- require 'friendly_shipping/services/usps/parse_rate_response'
12
- require 'friendly_shipping/services/usps/parse_time_in_transit_response'
13
- require 'friendly_shipping/services/usps/timing_options'
14
- require 'friendly_shipping/services/usps/rate_estimate_options'
3
+ require 'nokogiri'
15
4
 
16
5
  module FriendlyShipping
17
6
  module Services
18
7
  class Usps
19
- include Dry::Monads[:result]
8
+ include Dry::Monads::Result::Mixin
20
9
 
21
10
  attr_reader :test, :login, :client
22
11
 
@@ -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
@@ -1,8 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'friendly_shipping/services/usps/parse_xml_response'
4
- require 'friendly_shipping/services/usps_international/parse_package_rate'
5
-
6
3
  module FriendlyShipping
7
4
  module Services
8
5
  class UspsInternational
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'friendly_shipping/services/usps_international/rate_estimate_package_options'
4
-
5
3
  module FriendlyShipping
6
4
  module Services
7
5
  # Option container for rating a shipment via USPS
@@ -20,7 +18,7 @@ module FriendlyShipping
20
18
  package_options_class: FriendlyShipping::Services::UspsInternational::RateEstimatePackageOptions,
21
19
  **kwargs
22
20
  )
23
- super(**kwargs.merge(package_options_class: package_options_class))
21
+ super(**kwargs.reverse_merge(package_options_class: package_options_class))
24
22
  end
25
23
  end
26
24
  end
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'friendly_shipping/services/ups/rate_estimate_package_options'
4
-
5
3
  module FriendlyShipping
6
4
  module Services
7
5
  # Options for one package when rating
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'friendly_shipping/services/usps/machinable_package'
4
-
5
3
  module FriendlyShipping
6
4
  module Services
7
5
  class UspsInternational
@@ -13,7 +11,7 @@ module FriendlyShipping
13
11
  # @param [String] login The USPS login code
14
12
  # @param [FriendlyShipping::Services::UspsInternational::RateEstimateOptions] options The options
15
13
  # object to use with this request.
16
- # @return Array<[FriendlyShipping::Rate]> A set of Rates that this package may be sent with
14
+ # @return [Array<FriendlyShipping::Rate>] A set of Rates that this package may be sent with
17
15
  def call(shipment:, login:, options:)
18
16
  xml_builder = Nokogiri::XML::Builder.new do |xml|
19
17
  xml.IntlRateV2Request('USERID' => login) do
@@ -1,15 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'friendly_shipping/http_client'
4
- require 'friendly_shipping/services/usps_international/shipping_methods'
5
- require 'friendly_shipping/services/usps_international/serialize_rate_request'
6
- require 'friendly_shipping/services/usps_international/parse_rate_response'
7
- require 'friendly_shipping/services/usps_international/rate_estimate_options'
3
+ require 'nokogiri'
8
4
 
9
5
  module FriendlyShipping
10
6
  module Services
11
7
  class UspsInternational
12
- include Dry::Monads[:result]
8
+ include Dry::Monads::Result::Mixin
13
9
 
14
10
  attr_reader :test, :login, :client
15
11
 
@@ -0,0 +1,22 @@
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 < FriendlyShipping::AccessToken
11
+ # @return [String] the token's type
12
+ attr_reader :token_type
13
+
14
+ # @param token_type [String] the token's type (typically "Bearer")
15
+ def initialize(token_type:, **other_kwargs)
16
+ @token_type = token_type
17
+ super(**other_kwargs)
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FriendlyShipping
4
+ module Services
5
+ class USPSShip
6
+ class ApiError < FriendlyShipping::ApiError
7
+ # @param [RestClient::Exception] cause
8
+ def initialize(cause)
9
+ super(cause, parse_message(cause))
10
+ end
11
+
12
+ private
13
+
14
+ # @param [RestClient::Exception] error
15
+ # @return [String]
16
+ def parse_message(error)
17
+ return error.message unless error.response
18
+
19
+ parsed_json = JSON.parse(error.response.body)
20
+ parsed_json.dig("error", "message")
21
+ rescue JSON::ParserError, KeyError => _e
22
+ nil
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,55 @@
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(22, :inches)
19
+ MAX_WIDTH = Measured::Length(18, :inches)
20
+ MAX_HEIGHT = Measured::Length(15, :inches)
21
+
22
+ MIN_WEIGHT = Measured::Weight(6, :ounces)
23
+ MAX_WEIGHT = Measured::Weight(25, :pounds)
24
+
25
+ # @param package [Physical::Package]
26
+ def initialize(package)
27
+ @package = package
28
+ end
29
+
30
+ # @return [Boolean]
31
+ def machinable?
32
+ at_least_minimum && at_most_maximum
33
+ end
34
+
35
+ private
36
+
37
+ # @return [Boolean]
38
+ def at_least_minimum
39
+ package.length >= MIN_LENGTH &&
40
+ package.width >= MIN_WIDTH &&
41
+ package.height >= MIN_HEIGHT &&
42
+ package.weight >= MIN_WEIGHT
43
+ end
44
+
45
+ # @return [Boolean]
46
+ def at_most_maximum
47
+ package.length <= MAX_LENGTH &&
48
+ package.width <= MAX_WIDTH &&
49
+ package.height <= MAX_HEIGHT &&
50
+ package.weight <= MAX_WEIGHT
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end