freight_kit 0.1.20 → 0.1.22

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (466) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile +0 -2
  3. data/README.md +6 -5
  4. data/Rakefile +4 -0
  5. data/VERSION +1 -1
  6. data/configuration/locations/amazon_warehouses.yml +3330 -0
  7. data/freight_kit-0.1.21.gem +0 -0
  8. data/freight_kit.gemspec +11 -22
  9. data/lib/freight_kit/address_normalizer.rb +24 -0
  10. data/lib/freight_kit/amazon_warehouse.rb +33 -0
  11. data/lib/freight_kit/api_clients/soap_client.rb +33 -10
  12. data/lib/freight_kit/carrier.rb +248 -302
  13. data/lib/freight_kit/carriers/a1_express.rb +25 -0
  14. data/lib/freight_kit/carriers/a_duie_pyle.rb +30 -0
  15. data/lib/freight_kit/carriers/aaa_cooper.rb +11 -0
  16. data/lib/freight_kit/carriers/aaa_expedited_services.rb +10 -0
  17. data/lib/freight_kit/carriers/ab_brothers_usa_inc.rb +12 -0
  18. data/lib/freight_kit/carriers/aberdeen_express_inc.rb +28 -0
  19. data/lib/freight_kit/carriers/abf_freight_system.rb +475 -0
  20. data/lib/freight_kit/carriers/access_transport.rb +10 -0
  21. data/lib/freight_kit/carriers/action_express.rb +10 -0
  22. data/lib/freight_kit/carriers/adams_and_companies.rb +10 -0
  23. data/lib/freight_kit/carriers/advance_distribution.rb +28 -0
  24. data/lib/freight_kit/carriers/alcon_transport.rb +10 -0
  25. data/lib/freight_kit/carriers/all_freight.rb +10 -0
  26. data/lib/freight_kit/carriers/alliance_transportation_group.rb +10 -0
  27. data/lib/freight_kit/carriers/allstate_transport.rb +10 -0
  28. data/lib/freight_kit/carriers/aloha_freight_forwarders.rb +31 -0
  29. data/lib/freight_kit/carriers/amado_trucking.rb +31 -0
  30. data/lib/freight_kit/carriers/american_fast_freight.rb +28 -0
  31. data/lib/freight_kit/carriers/amigos_service_transportation.rb +10 -0
  32. data/lib/freight_kit/carriers/anderson_towing_and_transport.rb +10 -0
  33. data/lib/freight_kit/carriers/apex_transporting.rb +10 -0
  34. data/lib/freight_kit/carriers/archer_cargo.rb +35 -0
  35. data/lib/freight_kit/carriers/armstrong_transport_group.rb +10 -0
  36. data/lib/freight_kit/carriers/autobahn_logistics.rb +10 -0
  37. data/lib/freight_kit/carriers/averitt_express.rb +30 -0
  38. data/lib/freight_kit/carriers/ba_transportation.rb +10 -0
  39. data/lib/freight_kit/carriers/balkin_express.rb +10 -0
  40. data/lib/freight_kit/carriers/bay_area_express.rb +10 -0
  41. data/lib/freight_kit/carriers/bc_freightways.rb +11 -0
  42. data/lib/freight_kit/carriers/benjamin_best_freight_inc.rb +37 -0
  43. data/lib/freight_kit/carriers/bennett_trucking.rb +10 -0
  44. data/lib/freight_kit/carriers/best_overnite_express.rb +78 -0
  45. data/lib/freight_kit/carriers/best_trucking.rb +10 -0
  46. data/lib/freight_kit/carriers/bestway_transport.rb +10 -0
  47. data/lib/freight_kit/carriers/bryan_systems.rb +10 -0
  48. data/lib/freight_kit/carriers/c_and_w_express.rb +10 -0
  49. data/lib/freight_kit/carriers/campbell_trucking.rb +10 -0
  50. data/lib/freight_kit/carriers/carde_transportation.rb +10 -0
  51. data/lib/freight_kit/carriers/cardinal_logistics.rb +28 -0
  52. data/lib/freight_kit/carriers/cdc_transport.rb +10 -0
  53. data/lib/freight_kit/carriers/cdo_express_diversified.rb +10 -0
  54. data/lib/freight_kit/carriers/cedar_supply_trucking.rb +10 -0
  55. data/lib/freight_kit/carriers/central_freight_lines.rb +12 -0
  56. data/lib/freight_kit/carriers/central_transport.rb +12 -0
  57. data/lib/freight_kit/carriers/chicago_suburban_express.rb +28 -0
  58. data/lib/freight_kit/carriers/city_logistics.rb +11 -0
  59. data/lib/freight_kit/carriers/clear_lane_freight_systems.rb +474 -0
  60. data/lib/freight_kit/carriers/cleopatra_trucking.rb +10 -0
  61. data/lib/freight_kit/carriers/clifford_transportation.rb +10 -0
  62. data/lib/freight_kit/carriers/cms_domestic.rb +25 -0
  63. data/lib/freight_kit/carriers/cnd_logistics.rb +10 -0
  64. data/lib/freight_kit/carriers/coast_to_coast_transport.rb +27 -0
  65. data/lib/freight_kit/carriers/countrywide_carriers.rb +10 -0
  66. data/lib/freight_kit/carriers/cpl_express.rb +31 -0
  67. data/lib/freight_kit/carriers/crosby_trucking_service.rb +29 -0
  68. data/lib/freight_kit/carriers/cross_country_freight_solutions.rb +408 -0
  69. data/lib/freight_kit/carriers/crowley.rb +10 -0
  70. data/lib/freight_kit/carriers/csa_transportation.rb +40 -0
  71. data/lib/freight_kit/carriers/csx_transportation.rb +40 -0
  72. data/lib/freight_kit/carriers/custom_global_logistics.rb +14 -0
  73. data/lib/freight_kit/carriers/d_and_a_transport.rb +10 -0
  74. data/lib/freight_kit/carriers/dak_transport.rb +10 -0
  75. data/lib/freight_kit/carriers/dash_courier_and_logistics_north_carolina.rb +10 -0
  76. data/lib/freight_kit/carriers/daylight_transport.rb +29 -0
  77. data/lib/freight_kit/carriers/dayton_freight_lines.rb +30 -0
  78. data/lib/freight_kit/carriers/dbr_logistics.rb +10 -0
  79. data/lib/freight_kit/carriers/deep_south_freight.rb +27 -0
  80. data/lib/freight_kit/carriers/delta_express.rb +10 -0
  81. data/lib/freight_kit/carriers/delta_transport.rb +10 -0
  82. data/lib/freight_kit/carriers/delta_trucking_logistics.rb +10 -0
  83. data/lib/freight_kit/carriers/demco_express.rb +10 -0
  84. data/lib/freight_kit/carriers/dependable_global_express.rb +10 -0
  85. data/lib/freight_kit/carriers/dependable_hawaiian_express.rb +24 -0
  86. data/lib/freight_kit/carriers/dependable_highway_express.rb +573 -0
  87. data/lib/freight_kit/carriers/desert_coastal_transport.rb +10 -0
  88. data/lib/freight_kit/carriers/dhl.rb +11 -0
  89. data/lib/freight_kit/carriers/diamond_line_delivery.rb +174 -0
  90. data/lib/freight_kit/carriers/ding_tai_trucking.rb +10 -0
  91. data/lib/freight_kit/carriers/diversified_transfer_and_storage.rb +29 -0
  92. data/lib/freight_kit/carriers/dohrn_transfer_company.rb +30 -0
  93. data/lib/freight_kit/carriers/dot_line_transportation.rb +29 -0
  94. data/lib/freight_kit/carriers/drt_transportation.rb +13 -0
  95. data/lib/freight_kit/carriers/dugan_truck_line.rb +29 -0
  96. data/lib/freight_kit/carriers/dupont.rb +10 -0
  97. data/lib/freight_kit/carriers/e_transport_carriers.rb +29 -0
  98. data/lib/freight_kit/carriers/echo_global_logistics.rb +13 -0
  99. data/lib/freight_kit/carriers/ecklund_logistics.rb +10 -0
  100. data/lib/freight_kit/carriers/el_emplumado_transport.rb +10 -0
  101. data/lib/freight_kit/carriers/elite_carriers.rb +28 -0
  102. data/lib/freight_kit/carriers/emerald_express.rb +10 -0
  103. data/lib/freight_kit/carriers/era_transport.rb +10 -0
  104. data/lib/freight_kit/carriers/estes_express_lines.rb +25 -0
  105. data/lib/freight_kit/carriers/euro_kam.rb +28 -0
  106. data/lib/freight_kit/carriers/ew_riverside_trucking.rb +10 -0
  107. data/lib/freight_kit/carriers/express_2000_inc.rb +12 -0
  108. data/lib/freight_kit/carriers/express_services_international.rb +10 -0
  109. data/lib/freight_kit/carriers/f_and_a_express.rb +27 -0
  110. data/lib/freight_kit/carriers/fairmont_logistics.rb +13 -0
  111. data/lib/freight_kit/carriers/fargo_cargo.rb +10 -0
  112. data/lib/freight_kit/carriers/fast_freight_transportation.rb +10 -0
  113. data/lib/freight_kit/carriers/fast_load_transport.rb +10 -0
  114. data/lib/freight_kit/carriers/fed_ex_express.rb +30 -0
  115. data/lib/freight_kit/carriers/fed_ex_ground.rb +24 -0
  116. data/lib/freight_kit/carriers/fed_ex_ltl_economy.rb +11 -0
  117. data/lib/freight_kit/carriers/fed_ex_ltl_priority.rb +11 -0
  118. data/lib/freight_kit/carriers/first_trucking_express.rb +10 -0
  119. data/lib/freight_kit/carriers/flash_trucking.rb +10 -0
  120. data/lib/freight_kit/carriers/fort_transportation_and_service_company_inc.rb +37 -0
  121. data/lib/freight_kit/carriers/forward_air.rb +781 -0
  122. data/lib/freight_kit/carriers/frahtex.rb +10 -0
  123. data/lib/freight_kit/carriers/frozen_food_express.rb +10 -0
  124. data/lib/freight_kit/carriers/gholia_bros_trucking.rb +36 -0
  125. data/lib/freight_kit/carriers/global_cargo.rb +10 -0
  126. data/lib/freight_kit/carriers/global_logistics_and_transportation.rb +10 -0
  127. data/lib/freight_kit/carriers/gls_freight.rb +32 -0
  128. data/lib/freight_kit/carriers/go_trucking_services.rb +10 -0
  129. data/lib/freight_kit/carriers/gold_eagle_transportation.rb +11 -0
  130. data/lib/freight_kit/carriers/golden_drop_trucking.rb +10 -0
  131. data/lib/freight_kit/carriers/gonzalez_transportation.rb +10 -0
  132. data/lib/freight_kit/carriers/gps_transport.rb +10 -0
  133. data/lib/freight_kit/carriers/greene_enterprise.rb +10 -0
  134. data/lib/freight_kit/carriers/gsa_international.rb +10 -0
  135. data/lib/freight_kit/carriers/gsp_express.rb +24 -0
  136. data/lib/freight_kit/carriers/gsv_transportation.rb +10 -0
  137. data/lib/freight_kit/carriers/gta_transportation.rb +10 -0
  138. data/lib/freight_kit/carriers/guam_freight_services.rb +10 -0
  139. data/lib/freight_kit/carriers/harrah_transportation.rb +23 -0
  140. data/lib/freight_kit/carriers/her_trucking.rb +10 -0
  141. data/lib/freight_kit/carriers/hercules_freight.rb +20 -0
  142. data/lib/freight_kit/carriers/highlander_xpress.rb +36 -0
  143. data/lib/freight_kit/carriers/hn_transport.rb +10 -0
  144. data/lib/freight_kit/carriers/honolulu_freight_service.rb +10 -0
  145. data/lib/freight_kit/carriers/hot_line_freight_system_inc.rb +12 -0
  146. data/lib/freight_kit/carriers/hotshot_freight_of_texas.rb +13 -0
  147. data/lib/freight_kit/carriers/houser_transport.rb +27 -0
  148. data/lib/freight_kit/carriers/infiniti_freight_logistics.rb +28 -0
  149. data/lib/freight_kit/carriers/infinity_trucking.rb +10 -0
  150. data/lib/freight_kit/carriers/interstate_tma.rb +10 -0
  151. data/lib/freight_kit/carriers/intrade_industries.rb +10 -0
  152. data/lib/freight_kit/carriers/jasper_trucking.rb +28 -0
  153. data/lib/freight_kit/carriers/jb_hunt.rb +11 -0
  154. data/lib/freight_kit/carriers/jfj_transportation.rb +8 -8
  155. data/lib/freight_kit/carriers/jkb_express.rb +10 -0
  156. data/lib/freight_kit/carriers/jlm_transport.rb +10 -0
  157. data/lib/freight_kit/carriers/johansson_transport.rb +10 -0
  158. data/lib/freight_kit/carriers/jones_express.rb +10 -0
  159. data/lib/freight_kit/carriers/jp_logistics.rb +41 -0
  160. data/lib/freight_kit/carriers/just_in_time_cargo.rb +29 -0
  161. data/lib/freight_kit/carriers/kengkok_times.rb +10 -0
  162. data/lib/freight_kit/carriers/kings_express.rb +10 -0
  163. data/lib/freight_kit/carriers/klc_transportation.rb +10 -0
  164. data/lib/freight_kit/carriers/km_trucking.rb +10 -0
  165. data/lib/freight_kit/carriers/kma_logistics.rb +10 -0
  166. data/lib/freight_kit/carriers/krc_logistics.rb +10 -0
  167. data/lib/freight_kit/carriers/ktx.rb +10 -0
  168. data/lib/freight_kit/carriers/landstar.rb +25 -0
  169. data/lib/freight_kit/carriers/lanter_distributing_llc.rb +30 -0
  170. data/lib/freight_kit/carriers/ld_trucking.rb +10 -0
  171. data/lib/freight_kit/carriers/legacy_logistics_solutions.rb +10 -0
  172. data/lib/freight_kit/carriers/leonards_express.rb +10 -0
  173. data/lib/freight_kit/carriers/liberty_linehaul_west.rb +28 -0
  174. data/lib/freight_kit/carriers/liberty_transport.rb +10 -0
  175. data/lib/freight_kit/carriers/lightning_express.rb +10 -0
  176. data/lib/freight_kit/carriers/lj_express.rb +24 -0
  177. data/lib/freight_kit/carriers/lkp_enterprises.rb +10 -0
  178. data/lib/freight_kit/carriers/ltl_pros.rb +28 -0
  179. data/lib/freight_kit/carriers/m_and_m_and_sons.rb +10 -0
  180. data/lib/freight_kit/carriers/mag_lines_transport.rb +10 -0
  181. data/lib/freight_kit/carriers/magic_messenger.rb +24 -0
  182. data/lib/freight_kit/carriers/majors_transit.rb +10 -0
  183. data/lib/freight_kit/carriers/map_global_logistics.rb +10 -0
  184. data/lib/freight_kit/carriers/matths_trucking.rb +11 -0
  185. data/lib/freight_kit/carriers/mav_canadian_transport.rb +13 -0
  186. data/lib/freight_kit/carriers/mc_systems.rb +35 -0
  187. data/lib/freight_kit/carriers/mega_fleet.rb +10 -0
  188. data/lib/freight_kit/carriers/mega_trans_group.rb +28 -0
  189. data/lib/freight_kit/carriers/meta_express.rb +10 -0
  190. data/lib/freight_kit/carriers/mia_transportation.rb +10 -0
  191. data/lib/freight_kit/carriers/midwest_motor_express.rb +14 -0
  192. data/lib/freight_kit/carriers/midwest_transport_and_trucking.rb +10 -0
  193. data/lib/freight_kit/carriers/mikade_delivery_service.rb +10 -0
  194. data/lib/freight_kit/carriers/mohoff_freight_lines.rb +23 -0
  195. data/lib/freight_kit/carriers/montata_travelers_express.rb +10 -0
  196. data/lib/freight_kit/carriers/morgan_van_lines.rb +29 -0
  197. data/lib/freight_kit/carriers/moto_transportation.rb +24 -0
  198. data/lib/freight_kit/carriers/mountain_valley_express.rb +197 -0
  199. data/lib/freight_kit/carriers/ms_freight.rb +27 -0
  200. data/lib/freight_kit/carriers/mvg_transport.rb +10 -0
  201. data/lib/freight_kit/carriers/n_and_m_transfer_co_inc.rb +29 -0
  202. data/lib/freight_kit/carriers/nabot_transport_solutions.rb +10 -0
  203. data/lib/freight_kit/carriers/national_freight_xpress.rb +10 -0
  204. data/lib/freight_kit/carriers/navigation_group.rb +10 -0
  205. data/lib/freight_kit/carriers/nelson_trucking.rb +10 -0
  206. data/lib/freight_kit/carriers/network_1_transportation.rb +10 -0
  207. data/lib/freight_kit/carriers/new_century_transport.rb +28 -0
  208. data/lib/freight_kit/carriers/new_penn_motor_express.rb +11 -0
  209. data/lib/freight_kit/carriers/nieves_transportation.rb +10 -0
  210. data/lib/freight_kit/carriers/nolan_transportation_group.rb +26 -0
  211. data/lib/freight_kit/carriers/northland_trucking.rb +10 -0
  212. data/lib/freight_kit/carriers/northwest_carrier_logistics.rb +27 -0
  213. data/lib/freight_kit/carriers/nt_usa.rb +10 -0
  214. data/lib/freight_kit/carriers/nu_line_transportation.rb +10 -0
  215. data/lib/freight_kit/carriers/numark_transportation.rb +80 -0
  216. data/lib/freight_kit/carriers/nvk_transport.rb +10 -0
  217. data/lib/freight_kit/carriers/ny_transport.rb +10 -0
  218. data/lib/freight_kit/carriers/oak_harbor.rb +28 -0
  219. data/lib/freight_kit/carriers/oakline_transportation.rb +10 -0
  220. data/lib/freight_kit/carriers/old_dominion_freight_line.rb +30 -0
  221. data/lib/freight_kit/carriers/on_trac.rb +555 -0
  222. data/lib/freight_kit/carriers/one_hundred_one_transport.rb +10 -0
  223. data/lib/freight_kit/carriers/one_way_delivery.rb +10 -0
  224. data/lib/freight_kit/carriers/oracle_transportation_solutions.rb +10 -0
  225. data/lib/freight_kit/carriers/orange_courier.rb +27 -0
  226. data/lib/freight_kit/carriers/ortas_trucking.rb +10 -0
  227. data/lib/freight_kit/carriers/pac_trux_transportation.rb +10 -0
  228. data/lib/freight_kit/carriers/pacer.rb +10 -0
  229. data/lib/freight_kit/carriers/pacific_logistics.rb +10 -0
  230. data/lib/freight_kit/carriers/pacific_states_truck_lines.rb +10 -0
  231. data/lib/freight_kit/carriers/paradise_distributing.rb +10 -0
  232. data/lib/freight_kit/carriers/paramount_freight_systems.rb +10 -0
  233. data/lib/freight_kit/carriers/peninsula_truck_lines.rb +206 -0
  234. data/lib/freight_kit/carriers/pg_transport.rb +10 -0
  235. data/lib/freight_kit/carriers/pgs_360.rb +10 -0
  236. data/lib/freight_kit/carriers/pilot_freight_services.rb +10 -0
  237. data/lib/freight_kit/carriers/pious_trans.rb +27 -0
  238. data/lib/freight_kit/carriers/pitt_ohio_express.rb +30 -0
  239. data/lib/freight_kit/carriers/pki_logistics.rb +12 -0
  240. data/lib/freight_kit/carriers/plana_transportation.rb +28 -0
  241. data/lib/freight_kit/carriers/prestige.rb +10 -0
  242. data/lib/freight_kit/carriers/pro_trans_solutions.rb +28 -0
  243. data/lib/freight_kit/carriers/puma_logistics.rb +10 -0
  244. data/lib/freight_kit/carriers/pwc_international.rb +10 -0
  245. data/lib/freight_kit/carriers/quality_logistics.rb +10 -0
  246. data/lib/freight_kit/carriers/queen_star_trucking.rb +27 -0
  247. data/lib/freight_kit/carriers/r_and_l_carriers.rb +11 -0
  248. data/lib/freight_kit/carriers/rac_transport.rb +10 -0
  249. data/lib/freight_kit/carriers/re_logistics.rb +23 -0
  250. data/lib/freight_kit/carriers/reddaway.rb +11 -0
  251. data/lib/freight_kit/carriers/reefer_van_network.rb +13 -0
  252. data/lib/freight_kit/carriers/refrigerated_trucking_and_ltl_transportation_services.rb +10 -0
  253. data/lib/freight_kit/carriers/ridgeline_express.rb +10 -0
  254. data/lib/freight_kit/carriers/rist_transport.rb +29 -0
  255. data/lib/freight_kit/carriers/river_city_transport.rb +10 -0
  256. data/lib/freight_kit/carriers/roadrunner_transportation_services.rb +669 -0
  257. data/lib/freight_kit/carriers/roadtex.rb +13 -0
  258. data/lib/freight_kit/carriers/ronald_transport_services.rb +10 -0
  259. data/lib/freight_kit/carriers/rose_trucking.rb +24 -0
  260. data/lib/freight_kit/carriers/rp_and_j_transport.rb +24 -0
  261. data/lib/freight_kit/carriers/rpm_transportation.rb +11 -0
  262. data/lib/freight_kit/carriers/rude_transportation.rb +30 -0
  263. data/lib/freight_kit/carriers/s_and_s_logistics.rb +10 -0
  264. data/lib/freight_kit/carriers/saia.rb +456 -362
  265. data/lib/freight_kit/carriers/sb_transportation.rb +27 -0
  266. data/lib/freight_kit/carriers/schneider_national.rb +29 -0
  267. data/lib/freight_kit/carriers/schwarz_trucking.rb +10 -0
  268. data/lib/freight_kit/carriers/sewell_motor_express.rb +29 -0
  269. data/lib/freight_kit/carriers/shawnee_trucking.rb +28 -0
  270. data/lib/freight_kit/carriers/shi_logistics.rb +10 -0
  271. data/lib/freight_kit/carriers/sinbad_transport.rb +10 -0
  272. data/lib/freight_kit/carriers/skyway.rb +10 -0
  273. data/lib/freight_kit/carriers/smith_cargo_transportation_services.rb +23 -0
  274. data/lib/freight_kit/carriers/sna_transportation.rb +10 -0
  275. data/lib/freight_kit/carriers/solid_group.rb +10 -0
  276. data/lib/freight_kit/carriers/solution_carrier_express.rb +10 -0
  277. data/lib/freight_kit/carriers/southeastern_freight_lines.rb +441 -0
  278. data/lib/freight_kit/carriers/southwestern_motor_transport.rb +30 -0
  279. data/lib/freight_kit/carriers/span_alaska.rb +10 -0
  280. data/lib/freight_kit/carriers/specialize_transportation.rb +27 -0
  281. data/lib/freight_kit/carriers/ssi_refrigerated_express.rb +10 -0
  282. data/lib/freight_kit/carriers/standard_forwarding_llc.rb +28 -0
  283. data/lib/freight_kit/carriers/star_american_enterprise.rb +10 -0
  284. data/lib/freight_kit/carriers/sterling_transportation.rb +10 -0
  285. data/lib/freight_kit/carriers/stg.rb +163 -0
  286. data/lib/freight_kit/carriers/supervan_service.rb +10 -0
  287. data/lib/freight_kit/carriers/sutton_transport_inc.rb +12 -0
  288. data/lib/freight_kit/carriers/swift_transportation.rb +28 -0
  289. data/lib/freight_kit/carriers/t_force_worldwide.rb +605 -0
  290. data/lib/freight_kit/carriers/tax_airfreight_inc.rb +35 -0
  291. data/lib/freight_kit/carriers/tforce_freight_inc.rb +30 -0
  292. data/lib/freight_kit/carriers/the_custom_companies.rb +162 -0
  293. data/lib/freight_kit/carriers/thk_trucking.rb +28 -0
  294. data/lib/freight_kit/carriers/tiger_cool_express.rb +11 -0
  295. data/lib/freight_kit/carriers/tili_logistics.rb +10 -0
  296. data/lib/freight_kit/carriers/tmt_industries.rb +10 -0
  297. data/lib/freight_kit/carriers/top_ten_express.rb +28 -0
  298. data/lib/freight_kit/carriers/total_quality_logistics.rb +339 -0
  299. data/lib/freight_kit/carriers/total_transportation.rb +258 -0
  300. data/lib/freight_kit/carriers/trans_international_trucking.rb +29 -0
  301. data/lib/freight_kit/carriers/treasure_transportation.rb +10 -0
  302. data/lib/freight_kit/carriers/tri_state_services.rb +10 -0
  303. data/lib/freight_kit/carriers/tritex_freighters.rb +10 -0
  304. data/lib/freight_kit/carriers/triways.rb +10 -0
  305. data/lib/freight_kit/carriers/tsi_kansas.rb +29 -0
  306. data/lib/freight_kit/carriers/u_and_r_transportation.rb +23 -0
  307. data/lib/freight_kit/carriers/union_pacific.rb +11 -0
  308. data/lib/freight_kit/carriers/united_freight_logistics.rb +10 -0
  309. data/lib/freight_kit/carriers/unlimited_distance_transportation.rb +10 -0
  310. data/lib/freight_kit/carriers/up_streamline.rb +10 -0
  311. data/lib/freight_kit/carriers/ups_freight.rb +12 -0
  312. data/lib/freight_kit/carriers/us_freightways.rb +10 -0
  313. data/lib/freight_kit/carriers/usa_truck.rb +28 -0
  314. data/lib/freight_kit/carriers/usf_holland.rb +11 -0
  315. data/lib/freight_kit/carriers/valley_cartage.rb +11 -0
  316. data/lib/freight_kit/carriers/vc_transport.rb +10 -0
  317. data/lib/freight_kit/carriers/vision_express.rb +11 -0
  318. data/lib/freight_kit/carriers/vision_logistics_group.rb +10 -0
  319. data/lib/freight_kit/carriers/vmg_transportation_group.rb +10 -0
  320. data/lib/freight_kit/carriers/volpe_express.rb +10 -0
  321. data/lib/freight_kit/carriers/ward_trucking.rb +29 -0
  322. data/lib/freight_kit/carriers/wdwb.rb +10 -0
  323. data/lib/freight_kit/carriers/western_flyer_express.rb +28 -0
  324. data/lib/freight_kit/carriers/western_regional_delivery_service.rb +265 -0
  325. data/lib/freight_kit/carriers/western_specialized_trucking.rb +22 -0
  326. data/lib/freight_kit/carriers/win_distribution.rb +28 -0
  327. data/lib/freight_kit/carriers/wlb_transport.rb +10 -0
  328. data/lib/freight_kit/carriers/x_trux.rb +29 -0
  329. data/lib/freight_kit/carriers/xpo_logistics.rb +415 -0
  330. data/lib/freight_kit/carriers/xpress_global_systems.rb +30 -0
  331. data/lib/freight_kit/carriers/xylo_trucking.rb +10 -0
  332. data/lib/freight_kit/carriers/yrc_freight.rb +24 -0
  333. data/lib/freight_kit/carriers/zentner_transportation.rb +28 -0
  334. data/lib/freight_kit/carriers.rb +331 -34
  335. data/lib/freight_kit/countries.rb +22 -0
  336. data/lib/freight_kit/error.rb +0 -2
  337. data/lib/freight_kit/errors/document_not_found_error.rb +0 -2
  338. data/lib/freight_kit/errors/expired_credentials_error.rb +0 -2
  339. data/lib/freight_kit/errors/http_error.rb +0 -2
  340. data/lib/freight_kit/errors/invalid_credentials_error.rb +0 -2
  341. data/lib/freight_kit/errors/rate_limit_error.rb +12 -0
  342. data/lib/freight_kit/errors/response_error.rb +0 -2
  343. data/lib/freight_kit/errors/shipment_not_found_error.rb +0 -2
  344. data/lib/freight_kit/errors/unserviceable_accessorials_error.rb +1 -5
  345. data/lib/freight_kit/errors/unserviceable_error.rb +0 -2
  346. data/lib/freight_kit/helpers/documentable.rb +0 -2
  347. data/lib/freight_kit/helpers/pickupable.rb +3 -14
  348. data/lib/freight_kit/helpers/rateable.rb +0 -2
  349. data/lib/freight_kit/helpers/trackable.rb +0 -2
  350. data/lib/freight_kit/http_client/cache_store.rb +2 -4
  351. data/lib/freight_kit/http_client/httpi_adapter.rb +37 -0
  352. data/lib/freight_kit/http_client/log_device.rb +0 -2
  353. data/lib/freight_kit/http_client/response.rb +0 -2
  354. data/lib/freight_kit/http_client/revalidation.rb +0 -2
  355. data/lib/freight_kit/http_client/safe_retries.rb +17 -3
  356. data/lib/freight_kit/http_client/zstd.rb +2 -4
  357. data/lib/freight_kit/http_client.rb +21 -21
  358. data/lib/freight_kit/model.rb +1 -3
  359. data/lib/freight_kit/models/address.rb +15 -0
  360. data/lib/freight_kit/models/contact.rb +3 -4
  361. data/lib/freight_kit/models/country.rb +20 -0
  362. data/lib/freight_kit/models/credential.rb +3 -18
  363. data/lib/freight_kit/models/date_time.rb +0 -2
  364. data/lib/freight_kit/models/document_response.rb +0 -2
  365. data/lib/freight_kit/models/label.rb +0 -2
  366. data/lib/freight_kit/models/location.rb +10 -25
  367. data/lib/freight_kit/models/pickup_response.rb +0 -2
  368. data/lib/freight_kit/models/price.rb +0 -2
  369. data/lib/freight_kit/models/rate.rb +0 -2
  370. data/lib/freight_kit/models/rate_response.rb +0 -2
  371. data/lib/freight_kit/models/region.rb +19 -0
  372. data/lib/freight_kit/models/response.rb +0 -2
  373. data/lib/freight_kit/models/shipment.rb +5 -14
  374. data/lib/freight_kit/models/shipment_event.rb +0 -2
  375. data/lib/freight_kit/models/terminal.rb +10 -0
  376. data/lib/freight_kit/models/tracking_response.rb +0 -2
  377. data/lib/freight_kit/package.rb +52 -64
  378. data/lib/freight_kit/package_item.rb +14 -14
  379. data/lib/freight_kit/packaging.rb +6 -26
  380. data/lib/freight_kit/platform.rb +0 -32
  381. data/lib/freight_kit/platforms/carrier_logistics.rb +326 -345
  382. data/lib/freight_kit/platforms/dsdc.rb +82 -0
  383. data/lib/freight_kit/platforms/next_tms/graphql_client/mutation/quote_create.rb +32 -0
  384. data/lib/freight_kit/platforms/next_tms/graphql_client/mutation/shipment_create.rb +29 -0
  385. data/lib/freight_kit/platforms/next_tms/graphql_client/query/broker_carrier.rb +37 -0
  386. data/lib/freight_kit/platforms/next_tms/graphql_client/query/broker_carriers.rb +45 -0
  387. data/lib/freight_kit/platforms/next_tms/graphql_client/query/broker_customer.rb +54 -0
  388. data/lib/freight_kit/platforms/next_tms/graphql_client/query/broker_customers.rb +62 -0
  389. data/lib/freight_kit/platforms/next_tms/graphql_client/query/carrier.rb +22 -0
  390. data/lib/freight_kit/platforms/next_tms/graphql_client/query/carriers.rb +30 -0
  391. data/lib/freight_kit/platforms/next_tms/graphql_client/query/customer.rb +20 -0
  392. data/lib/freight_kit/platforms/next_tms/graphql_client/query/customers.rb +28 -0
  393. data/lib/freight_kit/platforms/next_tms/graphql_client/query/invoice.rb +31 -0
  394. data/lib/freight_kit/platforms/next_tms/graphql_client/query/invoices.rb +39 -0
  395. data/lib/freight_kit/platforms/next_tms/graphql_client/query/shipment.rb +102 -0
  396. data/lib/freight_kit/platforms/next_tms/graphql_client/query/shipment_documents.rb +29 -0
  397. data/lib/freight_kit/platforms/next_tms/graphql_client/query/shipments.rb +110 -0
  398. data/lib/freight_kit/platforms/next_tms/graphql_client.rb +22 -0
  399. data/lib/freight_kit/platforms/next_tms/orm/address.rb +22 -0
  400. data/lib/freight_kit/platforms/next_tms/orm/base.rb +120 -0
  401. data/lib/freight_kit/platforms/next_tms/orm/broker_carrier.rb +18 -0
  402. data/lib/freight_kit/platforms/next_tms/orm/broker_customer.rb +22 -0
  403. data/lib/freight_kit/platforms/next_tms/orm/carrier.rb +14 -0
  404. data/lib/freight_kit/platforms/next_tms/orm/collection.rb +335 -0
  405. data/lib/freight_kit/platforms/next_tms/orm/customer.rb +12 -0
  406. data/lib/freight_kit/platforms/next_tms/orm/invoice.rb +21 -0
  407. data/lib/freight_kit/platforms/next_tms/orm/record_not_found_error.rb +9 -0
  408. data/lib/freight_kit/platforms/next_tms/orm/shipment.rb +50 -0
  409. data/lib/freight_kit/platforms/next_tms/orm.rb +38 -0
  410. data/lib/freight_kit/platforms/next_tms.rb +452 -0
  411. data/lib/freight_kit/platforms/the_great_information_factory.rb +492 -425
  412. data/lib/freight_kit/regions.rb +27 -0
  413. data/lib/freight_kit/shipment_packer.rb +36 -38
  414. data/lib/freight_kit/tariff/overlength_rule.rb +1 -5
  415. data/lib/freight_kit/tariff.rb +0 -2
  416. data/lib/freight_kit.rb +103 -11
  417. data/schemas/next_tms.json +110710 -0
  418. metadata +443 -55
  419. data/configuration/carriers/abfs.yml +0 -124
  420. data/configuration/carriers/btvp.yml +0 -84
  421. data/configuration/carriers/ccyq.yml +0 -121
  422. data/configuration/carriers/clni.yml +0 -113
  423. data/configuration/carriers/cnwy.yml +0 -113
  424. data/configuration/carriers/ctbv.yml +0 -117
  425. data/configuration/carriers/dcha.yml +0 -105
  426. data/configuration/carriers/dlds.yml +0 -111
  427. data/configuration/carriers/dphe.yml +0 -130
  428. data/configuration/carriers/drrq.yml +0 -131
  429. data/configuration/carriers/fcsy.yml +0 -102
  430. data/configuration/carriers/fwda.yml +0 -137
  431. data/configuration/carriers/jfj_transportation.yml +0 -2
  432. data/configuration/carriers/mtvl.yml +0 -12
  433. data/configuration/carriers/numk.yml +0 -14
  434. data/configuration/carriers/otcl.yml +0 -124
  435. data/configuration/carriers/pens.yml +0 -22
  436. data/configuration/carriers/rdfs.yml +0 -142
  437. data/configuration/carriers/saia.yml +0 -129
  438. data/configuration/carriers/sefl.yml +0 -115
  439. data/configuration/carriers/totl.yml +0 -111
  440. data/configuration/carriers/tqyl.yml +0 -28
  441. data/configuration/carriers/wrds.yml +0 -20
  442. data/configuration/platforms/carrier_logistics.yml +0 -25
  443. data/configuration/platforms/next.yml +0 -12
  444. data/configuration/platforms/the_great_information_factory.yml +0 -122
  445. data/lib/freight_kit/carriers/abfs.rb +0 -421
  446. data/lib/freight_kit/carriers/btvp.rb +0 -29
  447. data/lib/freight_kit/carriers/ccyq.rb +0 -310
  448. data/lib/freight_kit/carriers/clni.rb +0 -396
  449. data/lib/freight_kit/carriers/cnwy.rb +0 -327
  450. data/lib/freight_kit/carriers/ctbv.rb +0 -53
  451. data/lib/freight_kit/carriers/dcha.rb +0 -76
  452. data/lib/freight_kit/carriers/dlds.rb +0 -49
  453. data/lib/freight_kit/carriers/dphe.rb +0 -472
  454. data/lib/freight_kit/carriers/drrq.rb +0 -580
  455. data/lib/freight_kit/carriers/fcsy.rb +0 -57
  456. data/lib/freight_kit/carriers/fwda.rb +0 -744
  457. data/lib/freight_kit/carriers/mtvl.rb +0 -34
  458. data/lib/freight_kit/carriers/numk.rb +0 -58
  459. data/lib/freight_kit/carriers/otcl.rb +0 -520
  460. data/lib/freight_kit/carriers/pens.rb +0 -204
  461. data/lib/freight_kit/carriers/rdfs.rb +0 -521
  462. data/lib/freight_kit/carriers/sefl.rb +0 -342
  463. data/lib/freight_kit/carriers/totl.rb +0 -172
  464. data/lib/freight_kit/carriers/tqyl.rb +0 -337
  465. data/lib/freight_kit/carriers/wrds.rb +0 -246
  466. data/lib/freight_kit/platforms/next.rb +0 -101
@@ -0,0 +1,21 @@
1
+ module FreightKit
2
+ module Platforms
3
+ class NextTMS
4
+ module ORM
5
+ # What the API key's customer owes for a shipment. {NextTMS#invoice} downloads its document.
6
+ class Invoice < Base
7
+ def balance_cents = @balance_cents ||= attribute(:balance_cents)
8
+ def balance_currency = @balance_currency ||= attribute(:balance_currency)
9
+ def due_on = @due_on ||= date(:due_on)
10
+ def issued_on = @issued_on ||= date(:issued_on)
11
+ # The load ID
12
+ def number = @number ||= attribute(:number)
13
+ def paid_at = @paid_at ||= time(:paid_at)
14
+ def shipment = @shipment ||= fetch_association(:shipment)
15
+ def total_cents = @total_cents ||= attribute(:total_cents)
16
+ def total_currency = @total_currency ||= attribute(:total_currency)
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,9 @@
1
+ module FreightKit
2
+ module Platforms
3
+ class NextTMS
4
+ module ORM
5
+ class RecordNotFoundError < Error; end
6
+ end
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,50 @@
1
+ module FreightKit
2
+ module Platforms
3
+ class NextTMS
4
+ module ORM
5
+ class Shipment < Base
6
+ class << self
7
+ def association_type_mapping = super.merge(receiver_address: 'Address', shipper_address: 'Address')
8
+ end
9
+
10
+ def bol = @bol ||= attribute(:bol)
11
+ def booked_at = @booked_at ||= time(:booked_at)
12
+ def broker_carrier = @broker_carrier ||= fetch_association(:broker_carrier)
13
+ def broker_customer = @broker_customer ||= fetch_association(:broker_customer)
14
+ def canceled_at = @canceled_at ||= time(:canceled_at)
15
+ def created_at = @created_at ||= time(:created_at)
16
+ def customer_reference = @customer_reference ||= attribute(:customer_reference)
17
+ def delivered_at = @delivered_at ||= time(:delivered_at)
18
+ def delivery_appointment_at = @delivery_appointment_at ||= time(:delivery_appointment_at)
19
+ def delivery_appointment_on = @delivery_appointment_on ||= date(:delivery_appointment_on)
20
+ def delivery_eta_on = @delivery_eta_on ||= date(:delivery_eta_on)
21
+ def departed_origin_terminal_at = @departed_origin_terminal_at ||= time(:departed_origin_terminal_at)
22
+ def dispatched_at = @dispatched_at ||= time(:dispatched_at)
23
+
24
+ # The carrier's scans, oldest first, as hashes with `occurred_at` and `type_code`, a {ShipmentEvent#type_code}
25
+ def events = @events ||= Array(attribute(:events))
26
+
27
+ def load_id = @load_id ||= attribute(:load_id)
28
+ def lost_on = @lost_on ||= date(:lost_on)
29
+ def out_for_delivery_at = @out_for_delivery_at ||= time(:out_for_delivery_at)
30
+ def pending_appointment_since = @pending_appointment_since ||= date(:pending_appointment_since)
31
+ def picked_up_at = @picked_up_at ||= time(:picked_up_at)
32
+ def pickup_driver_assigned_at = @pickup_driver_assigned_at ||= time(:pickup_driver_assigned_at)
33
+ def pickup_on = @pickup_on ||= date(:pickup_on)
34
+ def pro = @pro ||= attribute(:pro)
35
+ def receiver_address = @receiver_address ||= fetch_association(:receiver_address)
36
+ def receiver_name = @receiver_name ||= attribute(:receiver_name)
37
+ def shipper_address = @shipper_address ||= fetch_association(:shipper_address)
38
+ def shipper_bol = @shipper_bol ||= attribute(:shipper_bol)
39
+ def shipper_name = @shipper_name ||= attribute(:shipper_name)
40
+ def shipper_reference = @shipper_reference ||= attribute(:shipper_reference)
41
+ def status = @status ||= attribute(:status)&.to_sym
42
+ def total_charges_cents = @total_charges_cents ||= attribute(:total_charges_cents)
43
+ def total_charges_currency = @total_charges_currency ||= attribute(:total_charges_currency)
44
+ def truckload? = attribute(:truckload)
45
+ def updated_at = @updated_at ||= time(:updated_at)
46
+ end
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,38 @@
1
+ module FreightKit
2
+ module Platforms
3
+ class NextTMS
4
+ # Every {NextTMS} subclass gets its own copy of these models, bound to its domain, e.g.
5
+ #
6
+ # FreightKit::Carriers::JFJTransportation::ORM.credentials = FreightKit::Credential.new(type: :api_key, api_key:)
7
+ # FreightKit::Carriers::JFJTransportation::ORM::Shipment.where(status: :delivered).order(delivered_at: :desc)
8
+ module ORM
9
+ MODELS = %i[Address BrokerCarrier BrokerCustomer Carrier Customer Invoice Shipment].freeze
10
+
11
+ # Methods of a carrier's ORM namespace, e.g. `FreightKit::Carriers::JFJTransportation::ORM`.
12
+ module Namespace
13
+ attr_reader :credentials, :platform_class
14
+
15
+ def credentials=(credentials)
16
+ @credentials = credentials
17
+ @platform = nil
18
+ end
19
+
20
+ def platform = @platform ||= platform_class.new(credentials || raise(ArgumentError, "#{name}.credentials is not set"))
21
+ end
22
+
23
+ class << self
24
+ # @param platform_class [Class<NextTMS>]
25
+ # @return [Module] `platform_class::ORM`, with a subclass of each of {MODELS}.
26
+ def define(platform_class)
27
+ namespace = Module.new.extend(Namespace)
28
+ namespace.instance_variable_set(:@platform_class, platform_class)
29
+ platform_class.const_set(:ORM, namespace)
30
+
31
+ MODELS.each { |model| namespace.const_set(model, Class.new(const_get(model)).tap { |orm_class| orm_class.orm = namespace }) }
32
+ namespace
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,452 @@
1
+ module FreightKit
2
+ module Platforms
3
+ # Carriers and brokers that run Next TMS. Every tenant serves the same GraphQL API from its own domain, which each
4
+ # subclass's {ORM} queries, e.g. `FreightKit::Carriers::JFJTransportation::ORM::Shipment.find_by!(id: 1234)`.
5
+ #
6
+ # Subclasses set `API[:domain]`. Requests authenticate with an API key issued in Next TMS (prefixed `tms_`), and act
7
+ # with the permissions of its owner. The API hides fields that the owner cannot see, and the queries select what a
8
+ # customer sees, so they work with a customer's or a broker user's key, but not a broker carrier's.
9
+ class NextTMS < Platform
10
+ class << self
11
+ def inherited(subclass)
12
+ super
13
+ ORM.define(subclass)
14
+ end
15
+
16
+ # The TMS prices overlength fees into its rates
17
+ def overlength_fees_require_tariff? = false
18
+
19
+ # Both are the load ID
20
+ def pickup_number_is_tracking_number? = true
21
+ def required_credential_types = %i[api_key]
22
+
23
+ # The TMS takes FreightKit's accessorial codes, and each customer's broker sets which it prices, so only the quote
24
+ # can tell, with `UNSERVICEABLE_ACCESSORIALS`.
25
+ def serviceable_accessorials?(_accessorials) = true
26
+
27
+ def services = %i[standard]
28
+ end
29
+
30
+ # How long cached GraphQL results stay in {FreightKit.cache}.
31
+ class_attribute :cache_expires_in, default: 5.minutes
32
+
33
+ API = { endpoints: { graphql: '/graphql' }, use_ssl: true }.freeze
34
+ # Shipment milestones, as {ShipmentEvent#type_code} => [{ORM::Shipment} reader, address the event happens at], in the
35
+ # order they happen, which orders milestones with the same time
36
+ EVENTS = {
37
+ pickup_driver_assigned: %i[pickup_driver_assigned_at shipper_address],
38
+ picked_up: %i[picked_up_at shipper_address],
39
+ departed: %i[departed_origin_terminal_at shipper_address],
40
+ pending_delivery_appointment: %i[pending_appointment_since receiver_address],
41
+ out_for_delivery: %i[out_for_delivery_at receiver_address],
42
+ delivered: %i[delivered_at receiver_address],
43
+ lost: [:lost_on, nil]
44
+ }.freeze
45
+ JSON_HEADERS = { Accept: 'application/json', 'Content-Type': 'application/json' }.freeze
46
+ # Seconds between checks for labels, which the TMS generates in background jobs after booking
47
+ LABEL_WAITS = [0.5, 1, 2, 4].freeze
48
+
49
+ # Quotes wait on every carrier's rating API, so they take minutes rather than HTTPX's default of 60 seconds
50
+ READ_TIMEOUT = 300
51
+
52
+ # Downloads the shipment's bill of lading.
53
+ #
54
+ # @param tracking_number [String] The load ID, or the carrier's PRO.
55
+ # @return [DocumentResponse] With a {DocumentNotFoundError} when the shipment has none yet.
56
+ def bol(tracking_number) = document(tracking_number, :BOL)
57
+
58
+ # Quotes the shipment, then books the rate of the carrier with `scac`, or the cheapest rate without one. The TMS
59
+ # keeps only the local wall-clock time of the delivery and pickup hours.
60
+ #
61
+ # @return [PickupResponse] With the load ID as the pickup number, and the labels, which are empty when the TMS did not
62
+ # generate them within {LABEL_WAITS}. With an {UnserviceableError} when no rate matches, or an
63
+ # {UnserviceableAccessorialsError} when the customer cannot get an accessorial.
64
+ # @raise [ResponseError] With `UNBOOKABLE_RATE` when the rate expired, or the customer can no longer use the carrier.
65
+ # The booking uses the quote's addresses, so it never gets `RATE_OUTDATED`.
66
+ def create_pickup(delivery_from:, delivery_to:, pickup_from:, pickup_to:, scac:, shipment:, **)
67
+ pickup_on = pickup_date(shipment, pickup_from)
68
+ rates = quote(shipment, pickup_on)[:rates].select { |rate| rate[:charge_cents] }
69
+ rate = scac.present? ? rates.find { |option| option[:scac] == scac } : rates.min_by { |option| option[:charge_cents] }
70
+ error = UnserviceableError.new("No rate from #{scac.presence || name}")
71
+ return PickupResponse.new(error:, request: last_request) if rate.nil?
72
+
73
+ input = {
74
+ customer_reference: shipment.po_number,
75
+ delivery_hours_end_at: delivery_to&.iso8601,
76
+ delivery_hours_start_at: delivery_from&.iso8601,
77
+ pickup_hours_end_at: pickup_to&.iso8601,
78
+ pickup_hours_start_at: pickup_from&.iso8601,
79
+ pickup_on:,
80
+ rate_id: rate[:id],
81
+ receiver: location_input(shipment.destination),
82
+ shipper: location_input(shipment.origin),
83
+ shipper_reference: shipment.order_number
84
+ }
85
+ data = mutate(:shipment_create, **input).dig(:shipment_create, :shipment)
86
+ pickup_response = PickupResponse.new(pickup_number: data[:load_id].to_s, request: last_request, response: data)
87
+
88
+ pickup_response.tap { |response| response.labels = fetch_labels(data) }
89
+ rescue UnserviceableAccessorialsError => e
90
+ PickupResponse.new(error: e, request: last_request)
91
+ end
92
+
93
+ # Sends a document for {GraphQLClient::HTTPAdapter}. Keys are camelized here and underscored in {#query}, so
94
+ # everything else uses snake_case symbols.
95
+ #
96
+ # @return [Hash] The parsed JSON response.
97
+ def execute(document, operation_name: nil, variables: {})
98
+ api = self.class::API
99
+ variables = variables.deep_symbolize_keys
100
+ request = {
101
+ body: { operationName: operation_name, query: document, variables: camelize_keys(variables) }.to_json,
102
+ headers: JSON_HEADERS.merge(authorization_headers),
103
+ method: :post,
104
+ url: "#{base_url}#{api.dig(:endpoints, :graphql)}",
105
+ variables:
106
+ }
107
+
108
+ save_request(request)
109
+ body = commit(request)
110
+ raise ResponseError, "Unexpected response: #{body.to_s.truncate(200)}" unless body.is_a?(Hash)
111
+
112
+ body
113
+ end
114
+
115
+ # Quotes the shipment with every carrier that the TMS offers the API key's customer.
116
+ #
117
+ # @return [RateResponse] With the quote's load ID as each rate's `estimate_reference`, and an {UnserviceableError}
118
+ # when no carrier quoted, or an {UnserviceableAccessorialsError} when the customer cannot get an accessorial.
119
+ def find_rates(shipment:)
120
+ data = quote(shipment, pickup_date(shipment))
121
+ quote_number = data.dig(:shipment, :load_id).to_s
122
+ rates = data[:rates].filter_map { |rate| build_rate(rate, quote_number, shipment) if rate[:charge_cents] }
123
+
124
+ RateResponse.new(error: (UnserviceableError.new if rates.empty?), rates:, request: last_request, response: data)
125
+ rescue UnserviceableAccessorialsError => e
126
+ RateResponse.new(error: e, request: last_request)
127
+ end
128
+
129
+ # Builds events from the carrier's scans, or from the shipment's milestones when the carrier sent none.
130
+ #
131
+ # @param tracking_number [String] The load ID, or the carrier's PRO.
132
+ # @return [TrackingResponse] With a {ShipmentNotFoundError} when no shipment has the number.
133
+ def find_tracking_info(tracking_number)
134
+ tracking_response = TrackingResponse.new(carrier: self, carrier_name: name, tracking_number:)
135
+ shipment = find_shipment(tracking_number)
136
+ tracking_response.assign_attributes(request: last_request, response: shipment&.attributes)
137
+ return tracking_response.tap { |response| response.error = ShipmentNotFoundError.new } if shipment.nil?
138
+
139
+ shipment_events = scan_events(shipment).presence || milestone_events(shipment)
140
+
141
+ # Appointments without a time have only a date
142
+ delivery_appointment = shipment.delivery_appointment_at || shipment.delivery_appointment_on
143
+
144
+ tracking_response.assign_attributes(
145
+ actual_delivery_date: shipment.delivered_at&.then { |value| date_time(value, shipment.receiver_address) },
146
+ destination: shipment.receiver_address&.to_location,
147
+ estimated_delivery_date: shipment.delivery_eta_on&.then { |value| DateTime.new(local_date: value) },
148
+ origin: shipment.shipper_address&.to_location,
149
+ scheduled_delivery_date: delivery_appointment&.then { |value| date_time(value, shipment.receiver_address) },
150
+ ship_time: shipment.picked_up_at,
151
+ shipment_events:,
152
+ shipper_address: shipment.shipper_address&.to_location,
153
+ status: shipment_events.last&.type_code,
154
+ status_code: shipment.status&.to_s,
155
+ status_description: shipment.status&.to_s&.humanize
156
+ )
157
+
158
+ tracking_response
159
+ end
160
+
161
+ def find_tracking_number_from_pickup_number(pickup_number, _date) = pickup_number
162
+
163
+ # Downloads the invoice that the API key's customer received. {ORM::Invoice} lists them.
164
+ #
165
+ # @param tracking_number [String] The load ID, or the carrier's PRO.
166
+ # @return [DocumentResponse] With a {DocumentNotFoundError} when the shipment is not invoiced yet.
167
+ def invoice(tracking_number) = document(tracking_number, :INVOICE)
168
+
169
+ # Runs a {GraphQLClient::Mutation} as the API key's user, never from {FreightKit.cache}.
170
+ #
171
+ # @param operation [Symbol] The mutation, e.g. `:quote_create` for {GraphQLClient::Mutation::QuoteCreate}.
172
+ # @param input [Hash] The input, with snake_case keys.
173
+ # @return [Hash] Like {#query}.
174
+ # @raise [ResponseError] Like {#query}, e.g. with `UNBOOKABLE_RATE`.
175
+ def mutate(operation, **input) = commit_query(GraphQLClient::Mutation.const_get(operation.to_s.camelize), { input: input.compact })
176
+
177
+ # Downloads the carrier's proof of delivery.
178
+ #
179
+ # @param tracking_number [String] The load ID, or the carrier's PRO.
180
+ # @return [DocumentResponse] With a {DocumentNotFoundError} when the shipment has none yet.
181
+ def pod(tracking_number) = document(tracking_number, :POD)
182
+
183
+ # Runs a {GraphQLClient::Query} as the API key's user.
184
+ #
185
+ # @param operation [Symbol] The query, e.g. `:shipment` for {GraphQLClient::Query::Shipment}.
186
+ # @param cache [Boolean] Whether to reuse a result from {FreightKit.cache} for {.cache_expires_in}. Keys include a
187
+ # digest of the API key, so users with different permissions never share results.
188
+ # @param variables [Hash] Variables, with snake_case keys.
189
+ # @return [Hash] The response's `data`, with snake_case symbol keys.
190
+ # @raise [InvalidCredentialsError] When the tenant rejects the API key, e.g. because it expired.
191
+ # @raise [RateLimitError] When the API key sent too many requests (300 per minute), and {HTTPClient} did not retry
192
+ # because `retry_after` is too long.
193
+ # @raise [ResponseError] When the response has errors, e.g. `PAGE_OVERFLOW` or `UNAUTHORIZED`.
194
+ def query(operation, cache: false, **variables)
195
+ graphql_class = GraphQLClient::Query.const_get(operation.to_s.camelize)
196
+ variables = variables.compact
197
+ return commit_query(graphql_class, variables) unless cache
198
+
199
+ digest = Digest::SHA256.hexdigest([authorization_headers.to_json, operation, variables.to_json].join("\0"))
200
+ FreightKit.cache.fetch("freight_kit/next_tms/#{self.class::API[:domain]}/#{digest}", expires_in: cache_expires_in) do
201
+ commit_query(graphql_class, variables)
202
+ end
203
+ end
204
+
205
+ # Downloads the bill of lading that the carrier signed at pickup. Customers cannot see it.
206
+ #
207
+ # @param tracking_number [String] The load ID, or the carrier's PRO.
208
+ # @return [DocumentResponse] With a {DocumentNotFoundError} when the shipment has none, or the API key cannot see it.
209
+ def scanned_bol(tracking_number) = document(tracking_number, :SCANNED_BOL)
210
+
211
+ # @param tracking_number [String] The load ID, or the carrier's PRO.
212
+ # @return [Boolean] Whether a shipment that the API key can see has the number.
213
+ def valid_tracking_number?(tracking_number) = shipments_by_tracking_number(tracking_number.to_s).any?
214
+
215
+ protected
216
+
217
+ # The country comes from the region, e.g. `US_CA`
218
+ def address_input(location)
219
+ region_iso31662 = "#{location.country&.code(:alpha2)}_#{location.province}"
220
+ address = { city: location.city, region_iso31662:, street: location.address1, street2: location.address2 }
221
+
222
+ address.merge(zip_code: location.postal_code).compact
223
+ end
224
+
225
+ def authorization_headers = { 'X-API-Key': fetch_credential(:api_key).api_key }
226
+
227
+ def base_url
228
+ api = self.class::API
229
+
230
+ "#{api[:use_ssl] ? 'https' : 'http'}://#{api[:domain]}"
231
+ end
232
+
233
+ def build_rate(data, quote_number, shipment)
234
+ Rate.new(
235
+ carrier: self,
236
+ carrier_name: data[:carrier_name],
237
+ currency: data[:charge_currency],
238
+ estimate_reference: quote_number,
239
+ expires_at: data[:expires_at]&.then { |value| Time.iso8601(value) },
240
+ prices: [Price.new(blame: :api, cents: data[:charge_cents], description: 'Total')],
241
+ scac: data[:scac],
242
+ service_name: :standard,
243
+ shipment:,
244
+ transit_days: data[:transit_days],
245
+ with_excessive_length_fees: rates_with_excessive_length_fees?
246
+ )
247
+ end
248
+
249
+ def camelize_keys(hash) = hash.deep_transform_keys { |key| key.to_s.camelize(:lower) }
250
+
251
+ def commit(request)
252
+ response = HTTPClient.request(
253
+ request[:method],
254
+ request[:url],
255
+ **request.slice(:body, :headers),
256
+ proxy_url:,
257
+ read_timeout: READ_TIMEOUT
258
+ )
259
+
260
+ raise InvalidCredentialsError, "HTTP #{response.code}: #{response}" if [401, 403].include?(response.code)
261
+ raise RateLimitError.new(body: response.body, code: 429, retry_after: response.headers['retry-after']&.to_i) if response.code == 429
262
+ raise HTTPError.new(body: response.body, code: response.code) unless (200..299).cover?(response.code)
263
+
264
+ response.parsed_response
265
+ end
266
+
267
+ def commit_query(graphql_class, variables)
268
+ response = GraphQLClient::Client.query(graphql_class::DETAILS, context: { platform: self }, variables:)
269
+
270
+ parse_response(response.original_hash)
271
+ end
272
+
273
+ def date_time(value, address)
274
+ return DateTime.new(local_date: value) if value.is_a?(Date)
275
+
276
+ DateTime.new(date_time_with_zone: value.in_time_zone(address&.time_zone_name || 'UTC'))
277
+ end
278
+
279
+ def document(tracking_number, kind)
280
+ data = find_shipment_data(tracking_number, :shipment_documents)
281
+ document_response = DocumentResponse.new(request: last_request, response: data)
282
+ return document_response.tap { |response| response.error = ShipmentNotFoundError.new } if data.nil?
283
+
284
+ document = data[:documents].find { |item| item[:kind] == kind.to_s }
285
+ return document_response.tap { |response| response.error = DocumentNotFoundError.new } if document.nil?
286
+
287
+ response = download(document[:url])
288
+ document_response.tap { |item| item.assign_attributes(content_type: response.content_type, data: response.body) }
289
+ end
290
+
291
+ # URLs are signed and expire after 5 minutes, so they are downloaded right away, without the API key.
292
+ def download(url)
293
+ response = HTTPClient.get(URI.join(base_url, url).to_s, proxy_url:)
294
+ raise HTTPError.new(body: response.body, code: response.code) unless response.code == 200
295
+
296
+ response
297
+ end
298
+
299
+ # A booking stands without labels, so they are empty after the last wait or any failure, rather than an error that
300
+ # a retry would book twice.
301
+ def fetch_labels(shipment_data)
302
+ documents = shipment_data[:documents]
303
+
304
+ [*LABEL_WAITS, nil].each do |seconds|
305
+ labels = documents.select { |document| document[:kind] == 'LABEL' }
306
+ return labels.map { |label| Label.new(data: download(label[:url]).body) } if labels.any? || seconds.nil?
307
+
308
+ sleep(seconds)
309
+ documents = find_shipment_data(shipment_data[:load_id], :shipment_documents)&.dig(:documents).to_a
310
+ end
311
+ rescue StandardError => e
312
+ HTTPClient.logger.warn("Next TMS load #{shipment_data[:load_id]} booked without labels: #{e.class}: #{e.message}")
313
+ []
314
+ end
315
+
316
+ def find_shipment(tracking_number)
317
+ data = find_shipment_data(tracking_number)
318
+
319
+ self.class::ORM::Shipment.new(data[:id], loaded: true, **data.except(:id)) if data
320
+ end
321
+
322
+ # Customers sometimes track with the carrier's PRO instead of the load ID, which wins when both match. The API
323
+ # scopes shipments to the API key's account. Queries with this instance's credentials, unlike {ORM}, which uses
324
+ # `ORM.credentials`.
325
+ #
326
+ # @raise [ResponseError] When the number is the PRO of several shipments.
327
+ def find_shipment_data(tracking_number, operation = :shipments)
328
+ load_id = load_id(tracking_number)
329
+ items = shipments_by_tracking_number(tracking_number.to_s, operation)
330
+ data = (items.find { |item| item[:load_id] == load_id } if load_id) || (items.first if items.one?)
331
+ raise ResponseError, "#{tracking_number} is the PRO of several shipments, track with the load ID" if data.nil? && items.many?
332
+
333
+ data
334
+ end
335
+
336
+ # Load IDs are GraphQL Ints, so a longer number can only be a PRO
337
+ def load_id(tracking_number) = Integer(tracking_number.to_s, 10, exception: false)&.then { |id| id if id.between?(1, (2**31) - 1) }
338
+
339
+ def local_date(date_time) = date_time.date_time_with_zone&.to_date || date_time.local_date || date_time.local_date_time&.to_date
340
+
341
+ def location_input(location)
342
+ contact = location.contact
343
+
344
+ {
345
+ address: address_input(location),
346
+ contact_email: contact&.email,
347
+ contact_name: contact&.name,
348
+ contact_phone: contact&.phone,
349
+ name: contact&.company_name
350
+ }.compact
351
+ end
352
+
353
+ def milestone_events(shipment)
354
+ milestones = EVENTS.filter_map do |type_code, (reader, address)|
355
+ value = shipment.public_send(reader)
356
+ [type_code, value, address && shipment.public_send(address)] unless value.nil?
357
+ end
358
+
359
+ sort_milestones(milestones).map do |type_code, value, address|
360
+ ShipmentEvent.new(date_time: date_time(value, address), location: address&.to_location, type_code:)
361
+ end
362
+ end
363
+
364
+ # The weight is of the whole line, and the dimensions of one piece
365
+ def package_input(package)
366
+ type = package.packaging.type
367
+
368
+ {
369
+ declared_freight_class: package.declared_freight_class&.to_f,
370
+ description: package.description,
371
+ hazmat: package.hazmat?,
372
+ height: package.height(:inches).ceil.nonzero?,
373
+ length: package.length(:inches).ceil.nonzero?,
374
+ nmfc: package.nmfc,
375
+ packaging: (type == :luggage ? :piece : type).to_s,
376
+ quantity: package.quantity,
377
+ weight: package.pounds(:total).ceil,
378
+ width: package.width(:inches).ceil.nonzero?
379
+ }.compact
380
+ end
381
+
382
+ def parse_response(body)
383
+ body = body.deep_transform_keys { |key| key.to_s.underscore.to_sym }
384
+ errors = Array(body[:errors])
385
+ refused = errors.find { |error| error.dig(:extensions, :code) == 'UNSERVICEABLE_ACCESSORIALS' }&.dig(:extensions)
386
+ raise UnserviceableAccessorialsError.new(accessorials: refused[:accessorial_codes].map(&:to_sym)) if refused
387
+
388
+ messages = errors.map { |error| [error.dig(:extensions, :code), error[:message]].compact.join(': ') }
389
+ raise ResponseError, messages.join(', ') if messages.any?
390
+
391
+ body[:data].to_h
392
+ end
393
+
394
+ # Quotes and bookings use the same date, so the booked rate is for the pickup day
395
+ def pickup_date(shipment, pickup_from = nil) = pickup_from&.to_date || shipment.pickup_at&.then { |date_time| local_date(date_time) }
396
+
397
+ def quote(shipment, pickup_on)
398
+ packages = shipment.packages
399
+
400
+ input = {
401
+ accessorial_codes: Array(shipment.accessorials).map(&:to_s),
402
+ declared_freight_classes: packages.all?(&:declared_freight_class),
403
+ packages: packages.map { |package| package_input(package) },
404
+ pickup_on:,
405
+ receiver_address: address_input(shipment.destination),
406
+ shipper_address: address_input(shipment.origin)
407
+ }
408
+
409
+ mutate(:quote_create, **input)[:quote_create]
410
+ end
411
+
412
+ # The TMS sorts scans by time, and records their type codes from FreightKit tracking. Scans without a time are skipped.
413
+ def scan_events(shipment)
414
+ shipment.events.filter_map do |event|
415
+ next if event[:occurred_at].nil?
416
+
417
+ country = event[:country_alpha2]&.then { |code| ActiveUtils::Country.find(code) }
418
+ province = event[:region_iso31662]&.split(/[-_]/, 2)&.last
419
+ location = Location.new(city: event[:city], country:, postal_code: event[:zip_code], province:)
420
+ date_time = DateTime.new(date_time_with_zone: Time.iso8601(event[:occurred_at]).in_time_zone(location.time_zone || 'UTC'))
421
+
422
+ ShipmentEvent.new(date_time:, location:, type_code: event[:type_code].to_sym)
423
+ end
424
+ end
425
+
426
+ # The load ID comes first, since other shipments can have it as their PRO. Two PROs are enough to tell a unique PRO
427
+ # from one that several carriers use.
428
+ def shipments_by_tracking_number(tracking_number, operation = :shipments)
429
+ load_id = load_id(tracking_number)
430
+ items = query(operation, filter: { load_id_eq: load_id }, page_size: 1).dig(:shipments, :items) if load_id
431
+ return items if items.present?
432
+
433
+ conditions = [{ pro_eq: tracking_number }, { tracking_carrier_pro_eq: tracking_number }]
434
+ query(operation, filter: { or: conditions }, page_size: 2).dig(:shipments, :items)
435
+ end
436
+
437
+ # Timed milestones follow their timestamps, then {EVENTS}. Each date-only milestone comes after every milestone of its
438
+ # local day or earlier, e.g. a shipment lost on the day it was picked up.
439
+ def sort_milestones(milestones)
440
+ dated, timed = milestones.partition { |(_, value)| value.is_a?(Date) }
441
+ timed = timed.sort_by.with_index { |(_, value), index| [value.to_time, index] }
442
+
443
+ dated.sort_by.with_index { |(_, value), index| [value, index] }.each_with_object(timed) do |milestone, sorted|
444
+ index = sorted.rindex do |(_, value, address)|
445
+ (value.is_a?(Date) ? value : value.in_time_zone(address&.time_zone_name || 'UTC').to_date) <= milestone[1]
446
+ end
447
+ sorted.insert(index ? index + 1 : 0, milestone)
448
+ end
449
+ end
450
+ end
451
+ end
452
+ end