friendly_shipping 0.8.0 → 0.9.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (202) hide show
  1. checksums.yaml +4 -4
  2. data/.circleci/config.yml +1 -1
  3. data/.env.template +7 -0
  4. data/.env.test +4 -0
  5. data/.env.test.local.template +6 -0
  6. data/.gitignore +1 -0
  7. data/.rubocop-relaxed.yml +7 -23
  8. data/.rubocop.yml +11 -2
  9. data/.rubocop_todo.yml +21 -0
  10. data/.yardopts +6 -0
  11. data/CHANGELOG.md +82 -0
  12. data/Gemfile +17 -0
  13. data/friendly_shipping.gemspec +4 -13
  14. data/lib/friendly_shipping/api_error.rb +6 -4
  15. data/lib/friendly_shipping/api_error_handler.rb +10 -6
  16. data/lib/friendly_shipping/api_failure.rb +4 -0
  17. data/lib/friendly_shipping/api_result.rb +16 -4
  18. data/lib/friendly_shipping/carrier.rb +28 -8
  19. data/lib/friendly_shipping/http_client.rb +25 -2
  20. data/lib/friendly_shipping/inflections.rb +10 -0
  21. data/lib/friendly_shipping/item_options.rb +3 -0
  22. data/lib/friendly_shipping/label.rb +41 -20
  23. data/lib/friendly_shipping/package_options.rb +21 -2
  24. data/lib/friendly_shipping/rate.rb +50 -15
  25. data/lib/friendly_shipping/request.rb +23 -7
  26. data/lib/friendly_shipping/response.rb +21 -6
  27. data/lib/friendly_shipping/services/rl/api_error.rb +33 -0
  28. data/lib/friendly_shipping/services/rl/bol_options.rb +107 -0
  29. data/lib/friendly_shipping/services/rl/bol_packages_serializer.rb +32 -0
  30. data/lib/friendly_shipping/services/rl/bol_structures_serializer.rb +31 -0
  31. data/lib/friendly_shipping/services/rl/item_options.rb +38 -0
  32. data/lib/friendly_shipping/services/rl/package_options.rb +40 -0
  33. data/lib/friendly_shipping/services/rl/parse_create_bol_response.rb +46 -0
  34. data/lib/friendly_shipping/services/rl/parse_invoice_response.rb +50 -0
  35. data/lib/friendly_shipping/services/rl/parse_print_bol_response.rb +47 -0
  36. data/lib/friendly_shipping/services/rl/parse_print_shipping_labels_response.rb +47 -0
  37. data/lib/friendly_shipping/services/rl/parse_rate_quote_response.rb +66 -0
  38. data/lib/friendly_shipping/services/rl/parse_transit_times_response.rb +76 -0
  39. data/lib/friendly_shipping/services/rl/rate_quote_options.rb +86 -0
  40. data/lib/friendly_shipping/services/rl/rate_quote_packages_serializer.rb +54 -0
  41. data/lib/friendly_shipping/services/rl/rate_quote_structures_serializer.rb +53 -0
  42. data/lib/friendly_shipping/services/rl/serialize_create_bol_request.rb +86 -0
  43. data/lib/friendly_shipping/services/rl/serialize_location.rb +46 -0
  44. data/lib/friendly_shipping/services/rl/serialize_rate_quote_request.rb +69 -0
  45. data/lib/friendly_shipping/services/rl/serialize_transit_times_request.rb +38 -0
  46. data/lib/friendly_shipping/services/rl/shipment_document.rb +40 -0
  47. data/lib/friendly_shipping/services/rl/shipment_information.rb +41 -0
  48. data/lib/friendly_shipping/services/rl/shipment_options.rb +50 -0
  49. data/lib/friendly_shipping/services/rl/shipping_methods.rb +28 -0
  50. data/lib/friendly_shipping/services/rl/structure_options.rb +13 -0
  51. data/lib/friendly_shipping/services/rl.rb +204 -0
  52. data/lib/friendly_shipping/services/ship_engine/api_error.rb +33 -0
  53. data/lib/friendly_shipping/services/ship_engine/customs_items_serializer.rb +36 -0
  54. data/lib/friendly_shipping/services/ship_engine/label_customs_options.rb +10 -7
  55. data/lib/friendly_shipping/services/ship_engine/label_item_options.rb +10 -5
  56. data/lib/friendly_shipping/services/ship_engine/label_options.rb +31 -14
  57. data/lib/friendly_shipping/services/ship_engine/label_package_options.rb +18 -11
  58. data/lib/friendly_shipping/services/ship_engine/parse_address_validation_response.rb +77 -0
  59. data/lib/friendly_shipping/services/ship_engine/parse_carrier_response.rb +9 -0
  60. data/lib/friendly_shipping/services/ship_engine/parse_label_response.rb +4 -0
  61. data/lib/friendly_shipping/services/ship_engine/{parse_rate_estimate_response.rb → parse_rate_estimates_response.rb} +26 -6
  62. data/lib/friendly_shipping/services/ship_engine/parse_rates_response.rb +101 -0
  63. data/lib/friendly_shipping/services/ship_engine/parse_void_response.rb +4 -0
  64. data/lib/friendly_shipping/services/ship_engine/rate_estimates_options.rb +17 -4
  65. data/lib/friendly_shipping/services/ship_engine/rates_item_options.rb +28 -0
  66. data/lib/friendly_shipping/services/ship_engine/rates_options.rb +61 -0
  67. data/lib/friendly_shipping/services/ship_engine/rates_package_options.rb +20 -0
  68. data/lib/friendly_shipping/services/ship_engine/serialize_address_residential_indicator.rb +27 -0
  69. data/lib/friendly_shipping/services/ship_engine/serialize_address_validation_request.rb +31 -0
  70. data/lib/friendly_shipping/services/ship_engine/serialize_label_shipment.rb +22 -27
  71. data/lib/friendly_shipping/services/ship_engine/serialize_rate_estimate_request.rb +41 -16
  72. data/lib/friendly_shipping/services/ship_engine/serialize_rates_request.rb +126 -0
  73. data/lib/friendly_shipping/services/ship_engine.rb +94 -21
  74. data/lib/friendly_shipping/services/ship_engine_ltl/api_error.rb +12 -0
  75. data/lib/friendly_shipping/services/ship_engine_ltl/item_options.rb +50 -0
  76. data/lib/friendly_shipping/services/ship_engine_ltl/package_options.rb +50 -0
  77. data/lib/friendly_shipping/services/ship_engine_ltl/parse_carrier_response.rb +53 -0
  78. data/lib/friendly_shipping/services/ship_engine_ltl/parse_quote_response.rb +82 -0
  79. data/lib/friendly_shipping/services/ship_engine_ltl/quote_options.rb +52 -0
  80. data/lib/friendly_shipping/services/ship_engine_ltl/serialize_packages.rb +46 -0
  81. data/lib/friendly_shipping/services/ship_engine_ltl/serialize_quote_request.rb +143 -0
  82. data/lib/friendly_shipping/services/ship_engine_ltl/serialize_structures.rb +42 -0
  83. data/lib/friendly_shipping/services/ship_engine_ltl/shipment_options.rb +47 -0
  84. data/lib/friendly_shipping/services/ship_engine_ltl/structure_options.rb +17 -0
  85. data/lib/friendly_shipping/services/ship_engine_ltl.rb +153 -0
  86. data/lib/friendly_shipping/services/tforce_freight/access_token.rb +43 -0
  87. data/lib/friendly_shipping/services/tforce_freight/api_error.rb +42 -0
  88. data/lib/friendly_shipping/services/tforce_freight/bol_options.rb +180 -0
  89. data/lib/friendly_shipping/services/tforce_freight/document_options.rb +100 -0
  90. data/lib/friendly_shipping/services/tforce_freight/generate_commodity_information.rb +92 -0
  91. data/lib/friendly_shipping/services/tforce_freight/generate_create_bol_request_hash.rb +165 -0
  92. data/lib/friendly_shipping/services/tforce_freight/generate_document_options_hash.rb +36 -0
  93. data/lib/friendly_shipping/services/tforce_freight/generate_handling_units_hash.rb +51 -0
  94. data/lib/friendly_shipping/services/tforce_freight/generate_location_hash.rb +25 -0
  95. data/lib/friendly_shipping/services/tforce_freight/generate_pickup_request_hash.rb +113 -0
  96. data/lib/friendly_shipping/services/tforce_freight/generate_rates_request_hash.rb +65 -0
  97. data/lib/friendly_shipping/services/tforce_freight/generate_reference_hash.rb +28 -0
  98. data/lib/friendly_shipping/services/tforce_freight/item_options.rb +93 -0
  99. data/lib/friendly_shipping/services/tforce_freight/package_options.rb +121 -0
  100. data/lib/friendly_shipping/services/tforce_freight/parse_create_bol_response.rb +94 -0
  101. data/lib/friendly_shipping/services/tforce_freight/parse_pickup_response.rb +45 -0
  102. data/lib/friendly_shipping/services/tforce_freight/parse_rates_response.rb +58 -0
  103. data/lib/friendly_shipping/services/tforce_freight/parse_shipment_document.rb +31 -0
  104. data/lib/friendly_shipping/services/tforce_freight/pickup_options.rb +82 -0
  105. data/lib/friendly_shipping/services/tforce_freight/rates_item_options.rb +12 -0
  106. data/lib/friendly_shipping/services/tforce_freight/rates_options.rb +162 -0
  107. data/lib/friendly_shipping/services/tforce_freight/rates_package_options.rb +12 -0
  108. data/lib/friendly_shipping/services/tforce_freight/shipment_document.rb +38 -0
  109. data/lib/friendly_shipping/services/tforce_freight/shipment_information.rb +104 -0
  110. data/lib/friendly_shipping/services/tforce_freight/shipment_options.rb +49 -0
  111. data/lib/friendly_shipping/services/tforce_freight/shipping_methods.rb +25 -0
  112. data/lib/friendly_shipping/services/tforce_freight/structure_options.rb +44 -0
  113. data/lib/friendly_shipping/services/tforce_freight.rb +202 -0
  114. data/lib/friendly_shipping/services/ups/label_options.rb +14 -2
  115. data/lib/friendly_shipping/services/ups/label_package_options.rb +8 -4
  116. data/lib/friendly_shipping/services/ups/parse_modifier_element.rb +29 -0
  117. data/lib/friendly_shipping/services/ups/parse_rate_response.rb +12 -0
  118. data/lib/friendly_shipping/services/ups/parse_time_in_transit_response.rb +1 -1
  119. data/lib/friendly_shipping/services/ups/rate_estimate_options.rb +14 -1
  120. data/lib/friendly_shipping/services/ups/serialize_package_node.rb +11 -1
  121. data/lib/friendly_shipping/services/ups/serialize_rating_service_selection_request.rb +10 -1
  122. data/lib/friendly_shipping/services/ups/serialize_shipment_accept_request.rb +1 -1
  123. data/lib/friendly_shipping/services/ups/serialize_shipment_confirm_request.rb +8 -4
  124. data/lib/friendly_shipping/services/ups/shipping_methods.rb +1 -1
  125. data/lib/friendly_shipping/services/ups.rb +1 -1
  126. data/lib/friendly_shipping/services/ups_freight/api_error.rb +8 -5
  127. data/lib/friendly_shipping/services/ups_freight/generate_commodity_information.rb +65 -19
  128. data/lib/friendly_shipping/services/ups_freight/generate_freight_rate_request_hash.rb +3 -20
  129. data/lib/friendly_shipping/services/ups_freight/generate_freight_ship_request_hash.rb +2 -20
  130. data/lib/friendly_shipping/services/ups_freight/generate_handling_units_hash.rb +54 -0
  131. data/lib/friendly_shipping/services/ups_freight/generate_location_hash.rb +10 -6
  132. data/lib/friendly_shipping/services/ups_freight/label_options.rb +36 -10
  133. data/lib/friendly_shipping/services/ups_freight/label_structure_options.rb +13 -0
  134. data/lib/friendly_shipping/services/ups_freight/parse_freight_label_response.rb +2 -2
  135. data/lib/friendly_shipping/services/ups_freight/parse_shipment_document.rb +1 -1
  136. data/lib/friendly_shipping/services/ups_freight/rates_item_options.rb +18 -9
  137. data/lib/friendly_shipping/services/ups_freight/rates_options.rb +31 -21
  138. data/lib/friendly_shipping/services/ups_freight/rates_package_options.rb +79 -8
  139. data/lib/friendly_shipping/services/ups_freight/rates_structure_options.rb +46 -0
  140. data/lib/friendly_shipping/services/ups_freight/shipment_information.rb +7 -3
  141. data/lib/friendly_shipping/services/ups_freight/shipment_options.rb +49 -0
  142. data/lib/friendly_shipping/services/ups_freight.rb +3 -1
  143. data/lib/friendly_shipping/services/ups_json/access_token.rb +29 -0
  144. data/lib/friendly_shipping/services/ups_json/api_error.rb +29 -0
  145. data/lib/friendly_shipping/services/ups_json/generate_address_classification_payload.rb +29 -0
  146. data/lib/friendly_shipping/services/ups_json/generate_address_hash.rb +30 -0
  147. data/lib/friendly_shipping/services/ups_json/generate_labels_payload.rb +211 -0
  148. data/lib/friendly_shipping/services/ups_json/generate_package_hash.rb +76 -0
  149. data/lib/friendly_shipping/services/ups_json/generate_rates_payload.rb +86 -0
  150. data/lib/friendly_shipping/services/ups_json/generate_timings_payload.rb +44 -0
  151. data/lib/friendly_shipping/services/ups_json/label.rb +20 -0
  152. data/lib/friendly_shipping/services/ups_json/label_billing_options.rb +41 -0
  153. data/lib/friendly_shipping/services/ups_json/label_item_options.rb +77 -0
  154. data/lib/friendly_shipping/services/ups_json/label_options.rb +177 -0
  155. data/lib/friendly_shipping/services/ups_json/label_package_options.rb +51 -0
  156. data/lib/friendly_shipping/services/ups_json/parse_address_classification_response.rb +31 -0
  157. data/lib/friendly_shipping/services/ups_json/parse_json_response.rb +44 -0
  158. data/lib/friendly_shipping/services/ups_json/parse_labels_response.rb +71 -0
  159. data/lib/friendly_shipping/services/ups_json/parse_money_hash.rb +128 -0
  160. data/lib/friendly_shipping/services/ups_json/parse_rate_modifier_hash.rb +28 -0
  161. data/lib/friendly_shipping/services/ups_json/parse_rates_response.rb +105 -0
  162. data/lib/friendly_shipping/services/ups_json/parse_timings_response.rb +56 -0
  163. data/lib/friendly_shipping/services/ups_json/parse_void_response.rb +32 -0
  164. data/lib/friendly_shipping/services/ups_json/rates_item_options.rb +22 -0
  165. data/lib/friendly_shipping/services/ups_json/rates_options.rb +113 -0
  166. data/lib/friendly_shipping/services/ups_json/rates_package_options.rb +17 -0
  167. data/lib/friendly_shipping/services/ups_json/shipping_methods.rb +111 -0
  168. data/lib/friendly_shipping/services/ups_json/timings_options.rb +33 -0
  169. data/lib/friendly_shipping/services/ups_json.rb +216 -0
  170. data/lib/friendly_shipping/services/usps/choose_package_rate.rb +3 -3
  171. data/lib/friendly_shipping/services/usps/machinable_package.rb +1 -1
  172. data/lib/friendly_shipping/services/usps/parse_package_rate.rb +8 -7
  173. data/lib/friendly_shipping/services/usps/parse_time_in_transit_response.rb +12 -8
  174. data/lib/friendly_shipping/services/usps/rate_estimate_options.rb +1 -1
  175. data/lib/friendly_shipping/services/usps/rate_estimate_package_options.rb +6 -1
  176. data/lib/friendly_shipping/services/usps/serialize_rate_request.rb +2 -2
  177. data/lib/friendly_shipping/services/usps/shipping_methods.rb +8 -5
  178. data/lib/friendly_shipping/services/usps.rb +1 -1
  179. data/lib/friendly_shipping/services/usps_international/parse_package_rate.rb +5 -4
  180. data/lib/friendly_shipping/services/usps_international/rate_estimate_options.rb +1 -1
  181. data/lib/friendly_shipping/services/usps_international/serialize_rate_request.rb +3 -3
  182. data/lib/friendly_shipping/services/usps_international.rb +1 -1
  183. data/lib/friendly_shipping/services/usps_ship/access_token.rb +37 -0
  184. data/lib/friendly_shipping/services/usps_ship/api_error.rb +29 -0
  185. data/lib/friendly_shipping/services/usps_ship/machinable_package.rb +53 -0
  186. data/lib/friendly_shipping/services/usps_ship/parse_rate_estimates_response.rb +80 -0
  187. data/lib/friendly_shipping/services/usps_ship/parse_timings_response.rb +80 -0
  188. data/lib/friendly_shipping/services/usps_ship/rate_estimate_options.rb +45 -0
  189. data/lib/friendly_shipping/services/usps_ship/rate_estimate_package_options.rb +124 -0
  190. data/lib/friendly_shipping/services/usps_ship/serialize_rate_estimates_request.rb +55 -0
  191. data/lib/friendly_shipping/services/usps_ship/shipping_methods.rb +38 -0
  192. data/lib/friendly_shipping/services/usps_ship/timing_options.rb +9 -0
  193. data/lib/friendly_shipping/services/usps_ship.rb +199 -0
  194. data/lib/friendly_shipping/shipment_options.rb +13 -1
  195. data/lib/friendly_shipping/shipping_method.rb +38 -11
  196. data/lib/friendly_shipping/structure_options.rb +38 -0
  197. data/lib/friendly_shipping/timing.rb +42 -7
  198. data/lib/friendly_shipping/version.rb +1 -1
  199. data/lib/friendly_shipping.rb +7 -0
  200. metadata +149 -172
  201. data/lib/friendly_shipping/services/ship_engine/bad_request.rb +0 -29
  202. data/lib/friendly_shipping/services/ship_engine/bad_request_handler.rb +0 -33
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FriendlyShipping
4
+ module Services
5
+ class UpsJson
6
+ class ParseTimingsResponse
7
+ class << self
8
+ def call(request:, response:, shipment:)
9
+ parsed_response = ParseJsonResponse.call(
10
+ request: request,
11
+ response: response,
12
+ expected_root_key: 'emsResponse'
13
+ )
14
+ parsed_response.fmap do |parsing_result|
15
+ FriendlyShipping::ApiResult.new(
16
+ build_timings(parsing_result, shipment),
17
+ original_request: request,
18
+ original_response: response
19
+ )
20
+ end
21
+ end
22
+
23
+ def build_timings(timings_result, shipment)
24
+ service_timings = Array.wrap(timings_result.dig('emsResponse', 'services'))
25
+ service_timings.map do |timing|
26
+ service_description = timing['serviceLevelDescription']
27
+ shipping_method = SHIPPING_METHODS.detect do |potential_shipping_method|
28
+ service_description == potential_shipping_method.name &&
29
+ potential_shipping_method.origin_countries.map(&:code).include?(shipment.origin.country.code)
30
+ end
31
+ delivery_date = timing['deliveryDate']
32
+ delivery_time = timing['deliveryTime']
33
+ delivery = Time.parse("#{delivery_date} #{delivery_time}")
34
+ pickup_date = timing['pickupDate']
35
+ pickup_time = timing['pickupTime']
36
+ pickup = Time.parse("#{pickup_date} #{pickup_time}")
37
+
38
+ guaranteed = timing['guaranteeIndicator'] == '1'
39
+ business_transit_days = timing['businessTransitDays']
40
+
41
+ FriendlyShipping::Timing.new(
42
+ shipping_method: shipping_method,
43
+ pickup: pickup,
44
+ delivery: delivery,
45
+ guaranteed: guaranteed,
46
+ data: {
47
+ business_transit_days: business_transit_days
48
+ }
49
+ )
50
+ end
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FriendlyShipping
4
+ module Services
5
+ class UpsJson
6
+ class ParseVoidResponse
7
+ extend Dry::Monads::Result::Mixin
8
+
9
+ class << self
10
+ def call(request:, response:)
11
+ parsed_response = ParseJsonResponse.call(
12
+ request: request,
13
+ response: response,
14
+ expected_root_key: "VoidShipmentResponse"
15
+ )
16
+
17
+ parsed_response.bind do |void_response|
18
+ result = void_response["VoidShipmentResponse"]
19
+ Success(
20
+ FriendlyShipping::ApiResult.new(
21
+ result,
22
+ original_request: request,
23
+ original_response: response
24
+ )
25
+ )
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'friendly_shipping/item_options'
4
+
5
+ module FriendlyShipping
6
+ module Services
7
+ class UpsJson
8
+ # Represents item options for determining rates.
9
+ # @option commodity_code [String] This item's HS or NMFC code for international shipments.
10
+ # @option country_of_origin [String] This item's country of origin for international shipments.
11
+ class RatesItemOptions < FriendlyShipping::ItemOptions
12
+ attr_reader :commodity_code, :country_of_origin
13
+
14
+ def initialize(commodity_code: nil, country_of_origin: nil, **kwargs)
15
+ @commodity_code = commodity_code
16
+ @country_of_origin = country_of_origin
17
+ super(**kwargs)
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,113 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'friendly_shipping/services/ups_json/rates_package_options'
4
+
5
+ module FriendlyShipping
6
+ module Services
7
+ # Option container for rating a shipment via UPS
8
+ #
9
+ # Required:
10
+ #
11
+ # @option shipper_number [String] The shipper number of the origin of the shipment.
12
+ #
13
+ # Optional:
14
+ #
15
+ # @option carbon_neutral [Boolean] Ship with UPS' carbon neutral program
16
+ # @option customer_context [String ] Optional element to identify transactions between client and server
17
+ # @option customer_classification [Symbol] Which kind of rates to request. See UPS docs for more details. Default: `shipper_number`
18
+ # @option negotiated_rates [Boolean] if truthy negotiated rates will be requested from ups. Only valid if
19
+ # shipper account has negotiated rates. Default: false
20
+ # @option pickup_type [String] UPS pickup type. See UPS docs for more details. Default: `daily_pickup`
21
+ # @option pickup_date [Time] UPS pickup date/time. Default: nil
22
+ # @option saturday_delivery [Boolean] should we request Saturday delivery?. Default: false
23
+ # @option saturday_pickup [Boolean] should we request Saturday pickup?. Default: false
24
+ # @option shipping_method [FriendlyShipping::ShippingMethod] Request rates for a particular shipping method only?
25
+ # Default is `nil`, which translates to 'All shipping methods' (The "Shop" option in UPS parlance)
26
+ # @option sub_version [String] The UPS API sub-version to use for requests. Default: v2205
27
+ # @option with_time_in_transit [Boolean] Whether to request timing information alongside the rates
28
+ # @option package_options_class [Class] See FriendlyShipping::ShipmentOptions
29
+ #
30
+ class UpsJson
31
+ class RatesOptions < FriendlyShipping::ShipmentOptions
32
+ PICKUP_TYPE_CODES = {
33
+ daily_pickup: "01",
34
+ customer_counter: "03",
35
+ one_time_pickup: "06",
36
+ on_call_air: "07",
37
+ suggested_retail_rates: "11",
38
+ letter_center: "19",
39
+ air_service_center: "20"
40
+ }.freeze
41
+
42
+ CUSTOMER_CLASSIFICATION_CODES = {
43
+ shipper_number: "00",
44
+ daily_rates: "01",
45
+ retail_rates: "04",
46
+ regional_rates: "05",
47
+ general_rates: "06",
48
+ standard_rates: "53"
49
+ }.freeze
50
+
51
+ SUB_VERSIONS = %w[1 1601 1607 1701 1707 2108 2205].freeze
52
+
53
+ attr_reader :carbon_neutral,
54
+ :customer_context,
55
+ :destination_account,
56
+ :negotiated_rates,
57
+ :pickup_date,
58
+ :saturday_delivery,
59
+ :saturday_pickup,
60
+ :shipper,
61
+ :shipper_number,
62
+ :shipping_method,
63
+ :sub_version,
64
+ :with_time_in_transit
65
+
66
+ def initialize(
67
+ shipper_number:,
68
+ carbon_neutral: true,
69
+ customer_context: nil,
70
+ customer_classification: :daily_rates,
71
+ destination_account: nil,
72
+ negotiated_rates: false,
73
+ pickup_type: :daily_pickup,
74
+ pickup_date: nil,
75
+ saturday_delivery: false,
76
+ saturday_pickup: false,
77
+ shipper: nil,
78
+ shipping_method: nil,
79
+ sub_version: "2205",
80
+ with_time_in_transit: false,
81
+ package_options_class: RatesPackageOptions,
82
+ **kwargs
83
+ )
84
+ raise ArgumentError, "Invalid sub-version: #{sub_version}" unless sub_version.in?(SUB_VERSIONS)
85
+
86
+ @carbon_neutral = carbon_neutral
87
+ @customer_context = customer_context
88
+ @customer_classification = customer_classification
89
+ @destination_account = destination_account
90
+ @negotiated_rates = negotiated_rates
91
+ @shipper_number = shipper_number
92
+ @pickup_type = pickup_type
93
+ @pickup_date = pickup_date
94
+ @saturday_delivery = saturday_delivery
95
+ @saturday_pickup = saturday_pickup
96
+ @shipper = shipper
97
+ @shipping_method = shipping_method
98
+ @sub_version = sub_version
99
+ @with_time_in_transit = with_time_in_transit
100
+ super(**kwargs.reverse_merge(package_options_class: package_options_class))
101
+ end
102
+
103
+ def pickup_type_code
104
+ PICKUP_TYPE_CODES[@pickup_type]
105
+ end
106
+
107
+ def customer_classification_code
108
+ CUSTOMER_CLASSIFICATION_CODES[@customer_classification]
109
+ end
110
+ end
111
+ end
112
+ end
113
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FriendlyShipping
4
+ module Services
5
+ class UpsJson
6
+ class RatesPackageOptions < FriendlyShipping::PackageOptions
7
+ attr_reader :transmit_dimensions
8
+
9
+ def initialize(transmit_dimensions: true,
10
+ **kwargs)
11
+ @transmit_dimensions = transmit_dimensions
12
+ super(**kwargs.reverse_merge(item_options_class: RatesItemOptions))
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,111 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FriendlyShipping
4
+ module Services
5
+ class UpsJson
6
+ EU_COUNTRIES = %w(
7
+ AT BE BG CY CZ DE DK EE ES FI FR GB GR HR HU IE IT LT LU LV MT NL PO PT RO SE SI SK
8
+ ).map { |country_code| Carmen::Country.coded(country_code) }
9
+
10
+ class << self
11
+ private
12
+
13
+ def countries_by_code(code)
14
+ all_countries = Carmen::Country.all
15
+ covered_countries = EU_COUNTRIES + %w(US PR CA PL MX).map do |country_code|
16
+ Carmen::Country.coded(country_code)
17
+ end
18
+ other_countries = Carmen::Country.all - covered_countries
19
+ case code
20
+ when 'EU' then EU_COUNTRIES
21
+ when 'OTHER' then other_countries
22
+ when 'ALL' then all_countries
23
+ else
24
+ [Carmen::Country.coded(code)]
25
+ end
26
+ end
27
+ end
28
+
29
+ SHIPPING_METHODS = [
30
+ ['US', 'international', 'UPS Standard', '11'],
31
+ ['US', 'international', 'UPS Worldwide Express®', '07'],
32
+ ['US', 'international', 'UPS Worldwide Expedited®', '08'],
33
+ ['US', 'international', 'UPS Worldwide Express Plus®', '54'],
34
+ ['US', 'international', 'UPS Worldwide Saver', '65'],
35
+ ['US', 'domestic', 'UPS 2nd Day Air®', '02'],
36
+ ['US', 'domestic', 'UPS 2nd Day Air A.M.', '59'],
37
+ ['US', 'domestic', 'UPS 3 Day Select®', '12'],
38
+ ['US', 'domestic', 'UPS Ground', '03'],
39
+ ['US', 'domestic', 'UPS Next Day Air®', '01'],
40
+ ['US', 'domestic', 'UPS Next Day Air® Early', '14'],
41
+ ['US', 'domestic', 'UPS Next Day Air Saver®', '13'],
42
+ ['US', 'domestic', 'UPS SurePost Less than 1LB', '92'],
43
+ ['US', 'domestic', 'UPS SurePost 1LB or greater', '93'],
44
+ ['US', 'domestic', 'UPS SurePost BPM', '94'],
45
+ ['US', 'domestic', 'UPS SurePost Media Mail', '95'],
46
+ ['CA', 'domestic', 'UPS Expedited Canadian domestic shipments', '02'],
47
+ ['CA', 'domestic', 'UPS Express Saver Canadian domestic shipments', '13'],
48
+ ['CA', 'domestic', 'UPS 3 Day Select Shipments originating in Canada to CA and US 48', '12'],
49
+ ['CA', 'international', 'UPS 3 Day Select Shipments originating in Canada to CA and US 48', '12'],
50
+ ['CA', 'domestic', 'UPS Access Point Economy Canadian domestic shipments', '70'],
51
+ ['CA', 'domestic', 'UPS Express Canadian domestic shipments', '01'],
52
+ ['CA', 'domestic', 'UPS Express Early Canadian domestic shipments', '14'],
53
+ ['CA', 'international', 'UPS Express Saver International shipments originating in Canada', '65'],
54
+ ['CA', 'international', 'UPS Standard Shipments originating in Canada (Domestic and Int’l)', '11'],
55
+ ['CA', 'domestic', 'UPS Standard Shipments originating in Canada (Domestic and Int’l)', '11'],
56
+ ['CA', 'international', 'UPS Worldwide Expedited International shipments originating in Canada', '08'],
57
+ ['CA', 'international', 'UPS Worldwide Express International shipments originating in Canada', '07'],
58
+ ['CA', 'international', 'UPS Worldwide Express Plus International shipments originating in Canada', '54'],
59
+ ['CA', 'international', 'UPS Express Early Shipments originating in Canada to CA and US 48', '54'],
60
+ ['CA', 'domestic', 'UPS Express Early Shipments originating in Canada to CA and US 48', '54'],
61
+ ['EU', 'domestic', 'UPS Access Point Economy Shipments within the European Union', '70'],
62
+ ['EU', 'international', 'UPS Expedited Shipments originating in the European Union', '08'],
63
+ ['EU', 'international', 'UPS Express Shipments originating in the European Union', '07'],
64
+ ['EU', 'international', 'UPS Standard Shipments originating in the European Union', '11'],
65
+ ['EU', 'international', 'UPS Worldwide Express Plus Shipments originating in the European Union', '54'],
66
+ ['EU', 'international', 'UPS Worldwide Saver Shipments originating in the European Union', '65'],
67
+ ['MX', 'domestic', 'UPS Access Point Economy Shipments within Mexico', '70'],
68
+ ['MX', 'international', 'UPS Expedited Shipments originating in Mexico', '08'],
69
+ ['MX', 'international', 'UPS Express Shipments originating in Mexico', '07'],
70
+ ['MX', 'international', 'UPS Standard Shipments originating in Mexico', '11'],
71
+ ['MX', 'international', 'UPS Worldwide Express Plus Shipments originating in Mexico', '54'],
72
+ ['MX', 'international', 'UPS Worldwide Saver Shipments originating in Mexico', '65'],
73
+ ['PL', 'domestic', 'UPS Access Point Economy Polish domestic shipments', '70'],
74
+ ['PL', 'domestic', 'UPS Today Dedicated Courier Polish domestic shipments', '83'],
75
+ ['PL', 'domestic', 'UPS Today Express Polish domestic shipments', '85'],
76
+ ['PL', 'domestic', 'UPS Today Express Saver Polish domestic shipments', '86'],
77
+ ['PL', 'domestic', 'UPS Today Standard Polish domestic shipments', '82'],
78
+ ['PL', 'international', 'UPS Expedited Shipments originating in Poland', '08'],
79
+ ['PL', 'international', 'UPS Express Shipments originating in Poland', '07'],
80
+ ['PL', 'international', 'UPS Express Plus Shipments originating in Poland', '54'],
81
+ ['PL', 'international', 'UPS Express Saver Shipments originating in Poland', '65'],
82
+ ['PL', 'international', 'UPS Standard Shipments originating in Poland', '11'],
83
+ ['PR', 'domestic', 'UPS 2nd Day Air', '02'],
84
+ ['PR', 'domestic', 'UPS Ground', '03'],
85
+ ['PR', 'domestic', 'UPS Next Day Air', '01'],
86
+ ['PR', 'domestic', 'UPS Next Day Air Early', '14'],
87
+ ['PR', 'international', 'UPS Worldwide Expedited', '08'],
88
+ ['PR', 'international', 'UPS Worldwide Express', '07'],
89
+ ['PR', 'international', 'UPS Worldwide Express Plus', '54'],
90
+ ['PR', 'international', 'UPS Worldwide Saver', '65'],
91
+ ['DE', 'domestic', 'UPS Express 12:00 German domestic shipments', '74'],
92
+ ['OTHER', 'domestic', 'UPS Express', '07'],
93
+ ['OTHER', 'domestic', 'UPS Standard', '11'],
94
+ ['OTHER', 'international', 'UPS Worldwide Expedited', '08'],
95
+ ['OTHER', 'international', 'UPS Worldwide Express Plus', '54'],
96
+ ['OTHER', 'international', 'UPS Worldwide Saver', '65'],
97
+ ['ALL', 'international', 'UPS Worldwide Express Freight', '96'],
98
+ ['ALL', 'international', 'UPS Worldwide Express Freight Midday', '71']
99
+ ].freeze.map do |origin_country_code, dom_or_intl, name, code|
100
+ FriendlyShipping::ShippingMethod.new(
101
+ name: name,
102
+ service_code: code,
103
+ domestic: dom_or_intl == 'domestic',
104
+ international: dom_or_intl == 'international',
105
+ origin_countries: countries_by_code(origin_country_code),
106
+ multi_package: true
107
+ ).freeze
108
+ end
109
+ end
110
+ end
111
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FriendlyShipping
4
+ module Services
5
+ class UpsJson
6
+ # Options for getting timing information from UPS
7
+ # @attribute [Time] pickup When the shipment will be picked up
8
+ # @attribute [Money] invoice_total How much the items in the shipment are worth
9
+ # As this is not super important for getting timing information, we use a default
10
+ # value of 50 USD here.
11
+ # @attribute [Boolean] documents_only Does the shipment only contain documents?
12
+ # @attribute [String] customer_context A string to connect request and response in the calling code
13
+ class TimingsOptions
14
+ attr_reader :pickup,
15
+ :invoice_total,
16
+ :documents_only,
17
+ :customer_context
18
+
19
+ def initialize(
20
+ pickup: Time.now,
21
+ invoice_total: Money.new(5000, 'USD'),
22
+ documents_only: false,
23
+ customer_context: nil
24
+ )
25
+ @pickup = pickup
26
+ @invoice_total = invoice_total
27
+ @documents_only = documents_only
28
+ @customer_context = customer_context
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,216 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'dry/monads'
4
+ require 'friendly_shipping/http_client'
5
+ require 'friendly_shipping/services/ups_json/access_token'
6
+ require 'friendly_shipping/services/ups_json/api_error'
7
+ require 'friendly_shipping/services/ups_json/generate_address_classification_payload'
8
+ require 'friendly_shipping/services/ups_json/generate_labels_payload'
9
+ require 'friendly_shipping/services/ups_json/generate_rates_payload'
10
+ require 'friendly_shipping/services/ups_json/generate_timings_payload'
11
+ require 'friendly_shipping/services/ups_json/label'
12
+ require 'friendly_shipping/services/ups_json/label_options'
13
+ require 'friendly_shipping/services/ups_json/parse_address_classification_response'
14
+ require 'friendly_shipping/services/ups_json/parse_json_response'
15
+ require 'friendly_shipping/services/ups_json/parse_labels_response'
16
+ require 'friendly_shipping/services/ups_json/parse_money_hash'
17
+ require 'friendly_shipping/services/ups_json/parse_rate_modifier_hash'
18
+ require 'friendly_shipping/services/ups_json/parse_rates_response'
19
+ require 'friendly_shipping/services/ups_json/parse_timings_response'
20
+ require 'friendly_shipping/services/ups_json/parse_void_response'
21
+ require 'friendly_shipping/services/ups_json/rates_item_options'
22
+ require 'friendly_shipping/services/ups_json/rates_package_options'
23
+ require 'friendly_shipping/services/ups_json/rates_options'
24
+ require 'friendly_shipping/services/ups_json/shipping_methods'
25
+ require 'friendly_shipping/services/ups_json/timings_options'
26
+
27
+ module FriendlyShipping
28
+ module Services
29
+ class UpsJson
30
+ include Dry::Monads[:result]
31
+
32
+ attr_reader :access_token, :test, :client
33
+
34
+ CARRIER = FriendlyShipping::Carrier.new(
35
+ id: 'ups',
36
+ name: 'United Parcel Service',
37
+ code: 'ups',
38
+ shipping_methods: SHIPPING_METHODS
39
+ )
40
+
41
+ TEST_URL = 'https://wwwcie.ups.com'
42
+ LIVE_URL = 'https://onlinetools.ups.com'
43
+
44
+ def initialize(access_token:, test: true, client: nil)
45
+ @access_token = access_token
46
+ @test = test
47
+ error_handler = ApiErrorHandler.new(api_error_class: UpsJson::ApiError)
48
+ @client = client || HttpClient.new(error_handler: error_handler)
49
+ end
50
+
51
+ def carriers
52
+ Success([CARRIER])
53
+ end
54
+
55
+ # Creates an access token to be used for future API requests.
56
+ #
57
+ # @param client_id [String] the Client ID of your UPS application
58
+ # @param client_secret [String] the Client Secret of your UPS application
59
+ # @param merchant_id [String] the shipper number associated with your UPS application
60
+ # @param debug [Boolean] whether to append debug information to the API result
61
+ # @return [ApiResult<AccessToken>] the access token
62
+ def create_access_token(
63
+ client_id:,
64
+ client_secret:,
65
+ merchant_id:,
66
+ debug: false
67
+ )
68
+ request = FriendlyShipping::Request.new(
69
+ url: "#{base_url}/security/v1/oauth/token",
70
+ http_method: "POST",
71
+ body: "grant_type=client_credentials",
72
+ headers: {
73
+ Authorization: "Basic #{Base64.urlsafe_encode64("#{client_id}:#{client_secret}")}",
74
+ Content_Type: "application/x-www-form-urlencoded",
75
+ 'X-Merchant-Id': merchant_id,
76
+ Accept: "application/json"
77
+ },
78
+ debug: debug
79
+ )
80
+ client.post(request).fmap do |response|
81
+ hash = JSON.parse(response.body)
82
+ FriendlyShipping::ApiResult.new(
83
+ AccessToken.new(
84
+ expires_in: hash['expires_in'],
85
+ issued_at: hash['issued_at'],
86
+ raw_token: hash['access_token']
87
+ ),
88
+ original_request: request,
89
+ original_response: response
90
+ )
91
+ end
92
+ end
93
+
94
+ # Get rates for a shipment
95
+ # @param [Physical::Shipment] shipment The shipment we want to get rates for
96
+ # @param [FriendlyShipping::Services::UpsJson::RatesOptions] options What options
97
+ # to use for this rates request
98
+ # @return [Result<ApiResult<Array<Rate>>>] The rates returned from UPS encoded in a
99
+ # `FriendlyShipping::ApiResult` object.
100
+ def rates(shipment, options:, debug: false)
101
+ rate_or_shop = options.shipping_method ? "Rate" : "Shop"
102
+ url = "#{base_url}/api/rating/v#{options.sub_version || '1'}/#{rate_or_shop}"
103
+ headers = required_headers(access_token)
104
+ rates_request_body = GenerateRatesPayload.call(shipment: shipment, options: options).to_json
105
+
106
+ request = FriendlyShipping::Request.new(
107
+ url: url,
108
+ http_method: "POST",
109
+ headers: headers,
110
+ body: rates_request_body,
111
+ debug: debug
112
+ )
113
+
114
+ client.post(request).bind do |response|
115
+ ParseRatesResponse.call(response: response, request: request, shipment: shipment)
116
+ end
117
+ end
118
+
119
+ alias_method :rate_estimates, :rates
120
+
121
+ # Get timing information for a shipment
122
+ # @param [Physical::Shipment] shipment The shipment we want to estimate timings for
123
+ # @param [FriendlyShipping::Services::UpsJson::TimingOptions] options Options for this call
124
+ def timings(shipment, options:, debug: false)
125
+ url = "#{base_url}/api/shipments/v1/transittimes"
126
+ headers = required_headers(access_token).merge(
127
+ "transId" => SecureRandom.uuid,
128
+ "transactionSrc" => "testing" # this is a required field according to https://developer.ups.com/api/reference?loc=en_US#operation/TimeInTransit
129
+ )
130
+ timings_request_body = GenerateTimingsPayload.call(shipment: shipment, options: options).to_json
131
+
132
+ request = FriendlyShipping::Request.new(
133
+ url: url,
134
+ http_method: "POST",
135
+ headers: headers,
136
+ body: timings_request_body,
137
+ debug: debug
138
+ )
139
+
140
+ client.post(request).bind do |response|
141
+ ParseTimingsResponse.call(response: response, request: request, shipment: shipment)
142
+ end
143
+ end
144
+
145
+ # Generate labels for a shipment, aka by UPS as the Shipping Shipment api:
146
+ # https://developer.ups.com/api/reference?loc=en_US#tag/Shipping_other
147
+ # @param [Physical::Shipment] shipment The shipment we want to create labels for
148
+ # @param [FriendlyShipping::Services::UpsJson::LabelOptions] options Options for this call
149
+ # @return [Result<ApiResult<Array<Label>>>] The labels returned from UPS encoded in a
150
+ # `FriendlyShipping::ApiResult` object.
151
+ def labels(shipment, options:, debug: false)
152
+ url = "#{base_url}/api/shipments/v#{options.sub_version || '2205'}/ship"
153
+ # the RequestOption field in the payload is documented as doing city validation but it does not
154
+ url += "?additionaladdressvalidation=city" if options.validate_address
155
+ headers = required_headers(access_token)
156
+ body = GenerateLabelsPayload.call(shipment: shipment, options: options).to_json
157
+ request = FriendlyShipping::Request.new(url: url, http_method: "POST", headers: headers, body: body, debug: debug)
158
+
159
+ client.post(request).bind do |response|
160
+ ParseLabelsResponse.call(response: response, request: request)
161
+ end
162
+ end
163
+
164
+ # Classify an address.
165
+ # @param [Physical::Location] location The address we want to classify
166
+ # @return [Result<ApiResult<String>>] Either `"commercial"`, `"residential"`, or `"unknown"`
167
+ def address_classification(location, debug: false)
168
+ url = "#{base_url}/api/addressvalidation/v1/2"
169
+ headers = required_headers(access_token)
170
+ body = GenerateAddressClassificationPayload.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
+ ParseAddressClassificationResponse.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