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.
- checksums.yaml +4 -4
- data/Gemfile +0 -2
- data/README.md +6 -5
- data/Rakefile +4 -0
- data/VERSION +1 -1
- data/configuration/locations/amazon_warehouses.yml +3330 -0
- data/freight_kit-0.1.21.gem +0 -0
- data/freight_kit.gemspec +11 -22
- data/lib/freight_kit/address_normalizer.rb +24 -0
- data/lib/freight_kit/amazon_warehouse.rb +33 -0
- data/lib/freight_kit/api_clients/soap_client.rb +33 -10
- data/lib/freight_kit/carrier.rb +248 -302
- data/lib/freight_kit/carriers/a1_express.rb +25 -0
- data/lib/freight_kit/carriers/a_duie_pyle.rb +30 -0
- data/lib/freight_kit/carriers/aaa_cooper.rb +11 -0
- data/lib/freight_kit/carriers/aaa_expedited_services.rb +10 -0
- data/lib/freight_kit/carriers/ab_brothers_usa_inc.rb +12 -0
- data/lib/freight_kit/carriers/aberdeen_express_inc.rb +28 -0
- data/lib/freight_kit/carriers/abf_freight_system.rb +475 -0
- data/lib/freight_kit/carriers/access_transport.rb +10 -0
- data/lib/freight_kit/carriers/action_express.rb +10 -0
- data/lib/freight_kit/carriers/adams_and_companies.rb +10 -0
- data/lib/freight_kit/carriers/advance_distribution.rb +28 -0
- data/lib/freight_kit/carriers/alcon_transport.rb +10 -0
- data/lib/freight_kit/carriers/all_freight.rb +10 -0
- data/lib/freight_kit/carriers/alliance_transportation_group.rb +10 -0
- data/lib/freight_kit/carriers/allstate_transport.rb +10 -0
- data/lib/freight_kit/carriers/aloha_freight_forwarders.rb +31 -0
- data/lib/freight_kit/carriers/amado_trucking.rb +31 -0
- data/lib/freight_kit/carriers/american_fast_freight.rb +28 -0
- data/lib/freight_kit/carriers/amigos_service_transportation.rb +10 -0
- data/lib/freight_kit/carriers/anderson_towing_and_transport.rb +10 -0
- data/lib/freight_kit/carriers/apex_transporting.rb +10 -0
- data/lib/freight_kit/carriers/archer_cargo.rb +35 -0
- data/lib/freight_kit/carriers/armstrong_transport_group.rb +10 -0
- data/lib/freight_kit/carriers/autobahn_logistics.rb +10 -0
- data/lib/freight_kit/carriers/averitt_express.rb +30 -0
- data/lib/freight_kit/carriers/ba_transportation.rb +10 -0
- data/lib/freight_kit/carriers/balkin_express.rb +10 -0
- data/lib/freight_kit/carriers/bay_area_express.rb +10 -0
- data/lib/freight_kit/carriers/bc_freightways.rb +11 -0
- data/lib/freight_kit/carriers/benjamin_best_freight_inc.rb +37 -0
- data/lib/freight_kit/carriers/bennett_trucking.rb +10 -0
- data/lib/freight_kit/carriers/best_overnite_express.rb +78 -0
- data/lib/freight_kit/carriers/best_trucking.rb +10 -0
- data/lib/freight_kit/carriers/bestway_transport.rb +10 -0
- data/lib/freight_kit/carriers/bryan_systems.rb +10 -0
- data/lib/freight_kit/carriers/c_and_w_express.rb +10 -0
- data/lib/freight_kit/carriers/campbell_trucking.rb +10 -0
- data/lib/freight_kit/carriers/carde_transportation.rb +10 -0
- data/lib/freight_kit/carriers/cardinal_logistics.rb +28 -0
- data/lib/freight_kit/carriers/cdc_transport.rb +10 -0
- data/lib/freight_kit/carriers/cdo_express_diversified.rb +10 -0
- data/lib/freight_kit/carriers/cedar_supply_trucking.rb +10 -0
- data/lib/freight_kit/carriers/central_freight_lines.rb +12 -0
- data/lib/freight_kit/carriers/central_transport.rb +12 -0
- data/lib/freight_kit/carriers/chicago_suburban_express.rb +28 -0
- data/lib/freight_kit/carriers/city_logistics.rb +11 -0
- data/lib/freight_kit/carriers/clear_lane_freight_systems.rb +474 -0
- data/lib/freight_kit/carriers/cleopatra_trucking.rb +10 -0
- data/lib/freight_kit/carriers/clifford_transportation.rb +10 -0
- data/lib/freight_kit/carriers/cms_domestic.rb +25 -0
- data/lib/freight_kit/carriers/cnd_logistics.rb +10 -0
- data/lib/freight_kit/carriers/coast_to_coast_transport.rb +27 -0
- data/lib/freight_kit/carriers/countrywide_carriers.rb +10 -0
- data/lib/freight_kit/carriers/cpl_express.rb +31 -0
- data/lib/freight_kit/carriers/crosby_trucking_service.rb +29 -0
- data/lib/freight_kit/carriers/cross_country_freight_solutions.rb +408 -0
- data/lib/freight_kit/carriers/crowley.rb +10 -0
- data/lib/freight_kit/carriers/csa_transportation.rb +40 -0
- data/lib/freight_kit/carriers/csx_transportation.rb +40 -0
- data/lib/freight_kit/carriers/custom_global_logistics.rb +14 -0
- data/lib/freight_kit/carriers/d_and_a_transport.rb +10 -0
- data/lib/freight_kit/carriers/dak_transport.rb +10 -0
- data/lib/freight_kit/carriers/dash_courier_and_logistics_north_carolina.rb +10 -0
- data/lib/freight_kit/carriers/daylight_transport.rb +29 -0
- data/lib/freight_kit/carriers/dayton_freight_lines.rb +30 -0
- data/lib/freight_kit/carriers/dbr_logistics.rb +10 -0
- data/lib/freight_kit/carriers/deep_south_freight.rb +27 -0
- data/lib/freight_kit/carriers/delta_express.rb +10 -0
- data/lib/freight_kit/carriers/delta_transport.rb +10 -0
- data/lib/freight_kit/carriers/delta_trucking_logistics.rb +10 -0
- data/lib/freight_kit/carriers/demco_express.rb +10 -0
- data/lib/freight_kit/carriers/dependable_global_express.rb +10 -0
- data/lib/freight_kit/carriers/dependable_hawaiian_express.rb +24 -0
- data/lib/freight_kit/carriers/dependable_highway_express.rb +573 -0
- data/lib/freight_kit/carriers/desert_coastal_transport.rb +10 -0
- data/lib/freight_kit/carriers/dhl.rb +11 -0
- data/lib/freight_kit/carriers/diamond_line_delivery.rb +174 -0
- data/lib/freight_kit/carriers/ding_tai_trucking.rb +10 -0
- data/lib/freight_kit/carriers/diversified_transfer_and_storage.rb +29 -0
- data/lib/freight_kit/carriers/dohrn_transfer_company.rb +30 -0
- data/lib/freight_kit/carriers/dot_line_transportation.rb +29 -0
- data/lib/freight_kit/carriers/drt_transportation.rb +13 -0
- data/lib/freight_kit/carriers/dugan_truck_line.rb +29 -0
- data/lib/freight_kit/carriers/dupont.rb +10 -0
- data/lib/freight_kit/carriers/e_transport_carriers.rb +29 -0
- data/lib/freight_kit/carriers/echo_global_logistics.rb +13 -0
- data/lib/freight_kit/carriers/ecklund_logistics.rb +10 -0
- data/lib/freight_kit/carriers/el_emplumado_transport.rb +10 -0
- data/lib/freight_kit/carriers/elite_carriers.rb +28 -0
- data/lib/freight_kit/carriers/emerald_express.rb +10 -0
- data/lib/freight_kit/carriers/era_transport.rb +10 -0
- data/lib/freight_kit/carriers/estes_express_lines.rb +25 -0
- data/lib/freight_kit/carriers/euro_kam.rb +28 -0
- data/lib/freight_kit/carriers/ew_riverside_trucking.rb +10 -0
- data/lib/freight_kit/carriers/express_2000_inc.rb +12 -0
- data/lib/freight_kit/carriers/express_services_international.rb +10 -0
- data/lib/freight_kit/carriers/f_and_a_express.rb +27 -0
- data/lib/freight_kit/carriers/fairmont_logistics.rb +13 -0
- data/lib/freight_kit/carriers/fargo_cargo.rb +10 -0
- data/lib/freight_kit/carriers/fast_freight_transportation.rb +10 -0
- data/lib/freight_kit/carriers/fast_load_transport.rb +10 -0
- data/lib/freight_kit/carriers/fed_ex_express.rb +30 -0
- data/lib/freight_kit/carriers/fed_ex_ground.rb +24 -0
- data/lib/freight_kit/carriers/fed_ex_ltl_economy.rb +11 -0
- data/lib/freight_kit/carriers/fed_ex_ltl_priority.rb +11 -0
- data/lib/freight_kit/carriers/first_trucking_express.rb +10 -0
- data/lib/freight_kit/carriers/flash_trucking.rb +10 -0
- data/lib/freight_kit/carriers/fort_transportation_and_service_company_inc.rb +37 -0
- data/lib/freight_kit/carriers/forward_air.rb +781 -0
- data/lib/freight_kit/carriers/frahtex.rb +10 -0
- data/lib/freight_kit/carriers/frozen_food_express.rb +10 -0
- data/lib/freight_kit/carriers/gholia_bros_trucking.rb +36 -0
- data/lib/freight_kit/carriers/global_cargo.rb +10 -0
- data/lib/freight_kit/carriers/global_logistics_and_transportation.rb +10 -0
- data/lib/freight_kit/carriers/gls_freight.rb +32 -0
- data/lib/freight_kit/carriers/go_trucking_services.rb +10 -0
- data/lib/freight_kit/carriers/gold_eagle_transportation.rb +11 -0
- data/lib/freight_kit/carriers/golden_drop_trucking.rb +10 -0
- data/lib/freight_kit/carriers/gonzalez_transportation.rb +10 -0
- data/lib/freight_kit/carriers/gps_transport.rb +10 -0
- data/lib/freight_kit/carriers/greene_enterprise.rb +10 -0
- data/lib/freight_kit/carriers/gsa_international.rb +10 -0
- data/lib/freight_kit/carriers/gsp_express.rb +24 -0
- data/lib/freight_kit/carriers/gsv_transportation.rb +10 -0
- data/lib/freight_kit/carriers/gta_transportation.rb +10 -0
- data/lib/freight_kit/carriers/guam_freight_services.rb +10 -0
- data/lib/freight_kit/carriers/harrah_transportation.rb +23 -0
- data/lib/freight_kit/carriers/her_trucking.rb +10 -0
- data/lib/freight_kit/carriers/hercules_freight.rb +20 -0
- data/lib/freight_kit/carriers/highlander_xpress.rb +36 -0
- data/lib/freight_kit/carriers/hn_transport.rb +10 -0
- data/lib/freight_kit/carriers/honolulu_freight_service.rb +10 -0
- data/lib/freight_kit/carriers/hot_line_freight_system_inc.rb +12 -0
- data/lib/freight_kit/carriers/hotshot_freight_of_texas.rb +13 -0
- data/lib/freight_kit/carriers/houser_transport.rb +27 -0
- data/lib/freight_kit/carriers/infiniti_freight_logistics.rb +28 -0
- data/lib/freight_kit/carriers/infinity_trucking.rb +10 -0
- data/lib/freight_kit/carriers/interstate_tma.rb +10 -0
- data/lib/freight_kit/carriers/intrade_industries.rb +10 -0
- data/lib/freight_kit/carriers/jasper_trucking.rb +28 -0
- data/lib/freight_kit/carriers/jb_hunt.rb +11 -0
- data/lib/freight_kit/carriers/jfj_transportation.rb +8 -8
- data/lib/freight_kit/carriers/jkb_express.rb +10 -0
- data/lib/freight_kit/carriers/jlm_transport.rb +10 -0
- data/lib/freight_kit/carriers/johansson_transport.rb +10 -0
- data/lib/freight_kit/carriers/jones_express.rb +10 -0
- data/lib/freight_kit/carriers/jp_logistics.rb +41 -0
- data/lib/freight_kit/carriers/just_in_time_cargo.rb +29 -0
- data/lib/freight_kit/carriers/kengkok_times.rb +10 -0
- data/lib/freight_kit/carriers/kings_express.rb +10 -0
- data/lib/freight_kit/carriers/klc_transportation.rb +10 -0
- data/lib/freight_kit/carriers/km_trucking.rb +10 -0
- data/lib/freight_kit/carriers/kma_logistics.rb +10 -0
- data/lib/freight_kit/carriers/krc_logistics.rb +10 -0
- data/lib/freight_kit/carriers/ktx.rb +10 -0
- data/lib/freight_kit/carriers/landstar.rb +25 -0
- data/lib/freight_kit/carriers/lanter_distributing_llc.rb +30 -0
- data/lib/freight_kit/carriers/ld_trucking.rb +10 -0
- data/lib/freight_kit/carriers/legacy_logistics_solutions.rb +10 -0
- data/lib/freight_kit/carriers/leonards_express.rb +10 -0
- data/lib/freight_kit/carriers/liberty_linehaul_west.rb +28 -0
- data/lib/freight_kit/carriers/liberty_transport.rb +10 -0
- data/lib/freight_kit/carriers/lightning_express.rb +10 -0
- data/lib/freight_kit/carriers/lj_express.rb +24 -0
- data/lib/freight_kit/carriers/lkp_enterprises.rb +10 -0
- data/lib/freight_kit/carriers/ltl_pros.rb +28 -0
- data/lib/freight_kit/carriers/m_and_m_and_sons.rb +10 -0
- data/lib/freight_kit/carriers/mag_lines_transport.rb +10 -0
- data/lib/freight_kit/carriers/magic_messenger.rb +24 -0
- data/lib/freight_kit/carriers/majors_transit.rb +10 -0
- data/lib/freight_kit/carriers/map_global_logistics.rb +10 -0
- data/lib/freight_kit/carriers/matths_trucking.rb +11 -0
- data/lib/freight_kit/carriers/mav_canadian_transport.rb +13 -0
- data/lib/freight_kit/carriers/mc_systems.rb +35 -0
- data/lib/freight_kit/carriers/mega_fleet.rb +10 -0
- data/lib/freight_kit/carriers/mega_trans_group.rb +28 -0
- data/lib/freight_kit/carriers/meta_express.rb +10 -0
- data/lib/freight_kit/carriers/mia_transportation.rb +10 -0
- data/lib/freight_kit/carriers/midwest_motor_express.rb +14 -0
- data/lib/freight_kit/carriers/midwest_transport_and_trucking.rb +10 -0
- data/lib/freight_kit/carriers/mikade_delivery_service.rb +10 -0
- data/lib/freight_kit/carriers/mohoff_freight_lines.rb +23 -0
- data/lib/freight_kit/carriers/montata_travelers_express.rb +10 -0
- data/lib/freight_kit/carriers/morgan_van_lines.rb +29 -0
- data/lib/freight_kit/carriers/moto_transportation.rb +24 -0
- data/lib/freight_kit/carriers/mountain_valley_express.rb +197 -0
- data/lib/freight_kit/carriers/ms_freight.rb +27 -0
- data/lib/freight_kit/carriers/mvg_transport.rb +10 -0
- data/lib/freight_kit/carriers/n_and_m_transfer_co_inc.rb +29 -0
- data/lib/freight_kit/carriers/nabot_transport_solutions.rb +10 -0
- data/lib/freight_kit/carriers/national_freight_xpress.rb +10 -0
- data/lib/freight_kit/carriers/navigation_group.rb +10 -0
- data/lib/freight_kit/carriers/nelson_trucking.rb +10 -0
- data/lib/freight_kit/carriers/network_1_transportation.rb +10 -0
- data/lib/freight_kit/carriers/new_century_transport.rb +28 -0
- data/lib/freight_kit/carriers/new_penn_motor_express.rb +11 -0
- data/lib/freight_kit/carriers/nieves_transportation.rb +10 -0
- data/lib/freight_kit/carriers/nolan_transportation_group.rb +26 -0
- data/lib/freight_kit/carriers/northland_trucking.rb +10 -0
- data/lib/freight_kit/carriers/northwest_carrier_logistics.rb +27 -0
- data/lib/freight_kit/carriers/nt_usa.rb +10 -0
- data/lib/freight_kit/carriers/nu_line_transportation.rb +10 -0
- data/lib/freight_kit/carriers/numark_transportation.rb +80 -0
- data/lib/freight_kit/carriers/nvk_transport.rb +10 -0
- data/lib/freight_kit/carriers/ny_transport.rb +10 -0
- data/lib/freight_kit/carriers/oak_harbor.rb +28 -0
- data/lib/freight_kit/carriers/oakline_transportation.rb +10 -0
- data/lib/freight_kit/carriers/old_dominion_freight_line.rb +30 -0
- data/lib/freight_kit/carriers/on_trac.rb +555 -0
- data/lib/freight_kit/carriers/one_hundred_one_transport.rb +10 -0
- data/lib/freight_kit/carriers/one_way_delivery.rb +10 -0
- data/lib/freight_kit/carriers/oracle_transportation_solutions.rb +10 -0
- data/lib/freight_kit/carriers/orange_courier.rb +27 -0
- data/lib/freight_kit/carriers/ortas_trucking.rb +10 -0
- data/lib/freight_kit/carriers/pac_trux_transportation.rb +10 -0
- data/lib/freight_kit/carriers/pacer.rb +10 -0
- data/lib/freight_kit/carriers/pacific_logistics.rb +10 -0
- data/lib/freight_kit/carriers/pacific_states_truck_lines.rb +10 -0
- data/lib/freight_kit/carriers/paradise_distributing.rb +10 -0
- data/lib/freight_kit/carriers/paramount_freight_systems.rb +10 -0
- data/lib/freight_kit/carriers/peninsula_truck_lines.rb +206 -0
- data/lib/freight_kit/carriers/pg_transport.rb +10 -0
- data/lib/freight_kit/carriers/pgs_360.rb +10 -0
- data/lib/freight_kit/carriers/pilot_freight_services.rb +10 -0
- data/lib/freight_kit/carriers/pious_trans.rb +27 -0
- data/lib/freight_kit/carriers/pitt_ohio_express.rb +30 -0
- data/lib/freight_kit/carriers/pki_logistics.rb +12 -0
- data/lib/freight_kit/carriers/plana_transportation.rb +28 -0
- data/lib/freight_kit/carriers/prestige.rb +10 -0
- data/lib/freight_kit/carriers/pro_trans_solutions.rb +28 -0
- data/lib/freight_kit/carriers/puma_logistics.rb +10 -0
- data/lib/freight_kit/carriers/pwc_international.rb +10 -0
- data/lib/freight_kit/carriers/quality_logistics.rb +10 -0
- data/lib/freight_kit/carriers/queen_star_trucking.rb +27 -0
- data/lib/freight_kit/carriers/r_and_l_carriers.rb +11 -0
- data/lib/freight_kit/carriers/rac_transport.rb +10 -0
- data/lib/freight_kit/carriers/re_logistics.rb +23 -0
- data/lib/freight_kit/carriers/reddaway.rb +11 -0
- data/lib/freight_kit/carriers/reefer_van_network.rb +13 -0
- data/lib/freight_kit/carriers/refrigerated_trucking_and_ltl_transportation_services.rb +10 -0
- data/lib/freight_kit/carriers/ridgeline_express.rb +10 -0
- data/lib/freight_kit/carriers/rist_transport.rb +29 -0
- data/lib/freight_kit/carriers/river_city_transport.rb +10 -0
- data/lib/freight_kit/carriers/roadrunner_transportation_services.rb +669 -0
- data/lib/freight_kit/carriers/roadtex.rb +13 -0
- data/lib/freight_kit/carriers/ronald_transport_services.rb +10 -0
- data/lib/freight_kit/carriers/rose_trucking.rb +24 -0
- data/lib/freight_kit/carriers/rp_and_j_transport.rb +24 -0
- data/lib/freight_kit/carriers/rpm_transportation.rb +11 -0
- data/lib/freight_kit/carriers/rude_transportation.rb +30 -0
- data/lib/freight_kit/carriers/s_and_s_logistics.rb +10 -0
- data/lib/freight_kit/carriers/saia.rb +456 -362
- data/lib/freight_kit/carriers/sb_transportation.rb +27 -0
- data/lib/freight_kit/carriers/schneider_national.rb +29 -0
- data/lib/freight_kit/carriers/schwarz_trucking.rb +10 -0
- data/lib/freight_kit/carriers/sewell_motor_express.rb +29 -0
- data/lib/freight_kit/carriers/shawnee_trucking.rb +28 -0
- data/lib/freight_kit/carriers/shi_logistics.rb +10 -0
- data/lib/freight_kit/carriers/sinbad_transport.rb +10 -0
- data/lib/freight_kit/carriers/skyway.rb +10 -0
- data/lib/freight_kit/carriers/smith_cargo_transportation_services.rb +23 -0
- data/lib/freight_kit/carriers/sna_transportation.rb +10 -0
- data/lib/freight_kit/carriers/solid_group.rb +10 -0
- data/lib/freight_kit/carriers/solution_carrier_express.rb +10 -0
- data/lib/freight_kit/carriers/southeastern_freight_lines.rb +441 -0
- data/lib/freight_kit/carriers/southwestern_motor_transport.rb +30 -0
- data/lib/freight_kit/carriers/span_alaska.rb +10 -0
- data/lib/freight_kit/carriers/specialize_transportation.rb +27 -0
- data/lib/freight_kit/carriers/ssi_refrigerated_express.rb +10 -0
- data/lib/freight_kit/carriers/standard_forwarding_llc.rb +28 -0
- data/lib/freight_kit/carriers/star_american_enterprise.rb +10 -0
- data/lib/freight_kit/carriers/sterling_transportation.rb +10 -0
- data/lib/freight_kit/carriers/stg.rb +163 -0
- data/lib/freight_kit/carriers/supervan_service.rb +10 -0
- data/lib/freight_kit/carriers/sutton_transport_inc.rb +12 -0
- data/lib/freight_kit/carriers/swift_transportation.rb +28 -0
- data/lib/freight_kit/carriers/t_force_worldwide.rb +605 -0
- data/lib/freight_kit/carriers/tax_airfreight_inc.rb +35 -0
- data/lib/freight_kit/carriers/tforce_freight_inc.rb +30 -0
- data/lib/freight_kit/carriers/the_custom_companies.rb +162 -0
- data/lib/freight_kit/carriers/thk_trucking.rb +28 -0
- data/lib/freight_kit/carriers/tiger_cool_express.rb +11 -0
- data/lib/freight_kit/carriers/tili_logistics.rb +10 -0
- data/lib/freight_kit/carriers/tmt_industries.rb +10 -0
- data/lib/freight_kit/carriers/top_ten_express.rb +28 -0
- data/lib/freight_kit/carriers/total_quality_logistics.rb +339 -0
- data/lib/freight_kit/carriers/total_transportation.rb +258 -0
- data/lib/freight_kit/carriers/trans_international_trucking.rb +29 -0
- data/lib/freight_kit/carriers/treasure_transportation.rb +10 -0
- data/lib/freight_kit/carriers/tri_state_services.rb +10 -0
- data/lib/freight_kit/carriers/tritex_freighters.rb +10 -0
- data/lib/freight_kit/carriers/triways.rb +10 -0
- data/lib/freight_kit/carriers/tsi_kansas.rb +29 -0
- data/lib/freight_kit/carriers/u_and_r_transportation.rb +23 -0
- data/lib/freight_kit/carriers/union_pacific.rb +11 -0
- data/lib/freight_kit/carriers/united_freight_logistics.rb +10 -0
- data/lib/freight_kit/carriers/unlimited_distance_transportation.rb +10 -0
- data/lib/freight_kit/carriers/up_streamline.rb +10 -0
- data/lib/freight_kit/carriers/ups_freight.rb +12 -0
- data/lib/freight_kit/carriers/us_freightways.rb +10 -0
- data/lib/freight_kit/carriers/usa_truck.rb +28 -0
- data/lib/freight_kit/carriers/usf_holland.rb +11 -0
- data/lib/freight_kit/carriers/valley_cartage.rb +11 -0
- data/lib/freight_kit/carriers/vc_transport.rb +10 -0
- data/lib/freight_kit/carriers/vision_express.rb +11 -0
- data/lib/freight_kit/carriers/vision_logistics_group.rb +10 -0
- data/lib/freight_kit/carriers/vmg_transportation_group.rb +10 -0
- data/lib/freight_kit/carriers/volpe_express.rb +10 -0
- data/lib/freight_kit/carriers/ward_trucking.rb +29 -0
- data/lib/freight_kit/carriers/wdwb.rb +10 -0
- data/lib/freight_kit/carriers/western_flyer_express.rb +28 -0
- data/lib/freight_kit/carriers/western_regional_delivery_service.rb +265 -0
- data/lib/freight_kit/carriers/western_specialized_trucking.rb +22 -0
- data/lib/freight_kit/carriers/win_distribution.rb +28 -0
- data/lib/freight_kit/carriers/wlb_transport.rb +10 -0
- data/lib/freight_kit/carriers/x_trux.rb +29 -0
- data/lib/freight_kit/carriers/xpo_logistics.rb +415 -0
- data/lib/freight_kit/carriers/xpress_global_systems.rb +30 -0
- data/lib/freight_kit/carriers/xylo_trucking.rb +10 -0
- data/lib/freight_kit/carriers/yrc_freight.rb +24 -0
- data/lib/freight_kit/carriers/zentner_transportation.rb +28 -0
- data/lib/freight_kit/carriers.rb +331 -34
- data/lib/freight_kit/countries.rb +22 -0
- data/lib/freight_kit/error.rb +0 -2
- data/lib/freight_kit/errors/document_not_found_error.rb +0 -2
- data/lib/freight_kit/errors/expired_credentials_error.rb +0 -2
- data/lib/freight_kit/errors/http_error.rb +0 -2
- data/lib/freight_kit/errors/invalid_credentials_error.rb +0 -2
- data/lib/freight_kit/errors/rate_limit_error.rb +12 -0
- data/lib/freight_kit/errors/response_error.rb +0 -2
- data/lib/freight_kit/errors/shipment_not_found_error.rb +0 -2
- data/lib/freight_kit/errors/unserviceable_accessorials_error.rb +1 -5
- data/lib/freight_kit/errors/unserviceable_error.rb +0 -2
- data/lib/freight_kit/helpers/documentable.rb +0 -2
- data/lib/freight_kit/helpers/pickupable.rb +3 -14
- data/lib/freight_kit/helpers/rateable.rb +0 -2
- data/lib/freight_kit/helpers/trackable.rb +0 -2
- data/lib/freight_kit/http_client/cache_store.rb +2 -4
- data/lib/freight_kit/http_client/httpi_adapter.rb +37 -0
- data/lib/freight_kit/http_client/log_device.rb +0 -2
- data/lib/freight_kit/http_client/response.rb +0 -2
- data/lib/freight_kit/http_client/revalidation.rb +0 -2
- data/lib/freight_kit/http_client/safe_retries.rb +17 -3
- data/lib/freight_kit/http_client/zstd.rb +2 -4
- data/lib/freight_kit/http_client.rb +21 -21
- data/lib/freight_kit/model.rb +1 -3
- data/lib/freight_kit/models/address.rb +15 -0
- data/lib/freight_kit/models/contact.rb +3 -4
- data/lib/freight_kit/models/country.rb +20 -0
- data/lib/freight_kit/models/credential.rb +3 -18
- data/lib/freight_kit/models/date_time.rb +0 -2
- data/lib/freight_kit/models/document_response.rb +0 -2
- data/lib/freight_kit/models/label.rb +0 -2
- data/lib/freight_kit/models/location.rb +10 -25
- data/lib/freight_kit/models/pickup_response.rb +0 -2
- data/lib/freight_kit/models/price.rb +0 -2
- data/lib/freight_kit/models/rate.rb +0 -2
- data/lib/freight_kit/models/rate_response.rb +0 -2
- data/lib/freight_kit/models/region.rb +19 -0
- data/lib/freight_kit/models/response.rb +0 -2
- data/lib/freight_kit/models/shipment.rb +5 -14
- data/lib/freight_kit/models/shipment_event.rb +0 -2
- data/lib/freight_kit/models/terminal.rb +10 -0
- data/lib/freight_kit/models/tracking_response.rb +0 -2
- data/lib/freight_kit/package.rb +52 -64
- data/lib/freight_kit/package_item.rb +14 -14
- data/lib/freight_kit/packaging.rb +6 -26
- data/lib/freight_kit/platform.rb +0 -32
- data/lib/freight_kit/platforms/carrier_logistics.rb +326 -345
- data/lib/freight_kit/platforms/dsdc.rb +82 -0
- data/lib/freight_kit/platforms/next_tms/graphql_client/mutation/quote_create.rb +32 -0
- data/lib/freight_kit/platforms/next_tms/graphql_client/mutation/shipment_create.rb +29 -0
- data/lib/freight_kit/platforms/next_tms/graphql_client/query/broker_carrier.rb +37 -0
- data/lib/freight_kit/platforms/next_tms/graphql_client/query/broker_carriers.rb +45 -0
- data/lib/freight_kit/platforms/next_tms/graphql_client/query/broker_customer.rb +54 -0
- data/lib/freight_kit/platforms/next_tms/graphql_client/query/broker_customers.rb +62 -0
- data/lib/freight_kit/platforms/next_tms/graphql_client/query/carrier.rb +22 -0
- data/lib/freight_kit/platforms/next_tms/graphql_client/query/carriers.rb +30 -0
- data/lib/freight_kit/platforms/next_tms/graphql_client/query/customer.rb +20 -0
- data/lib/freight_kit/platforms/next_tms/graphql_client/query/customers.rb +28 -0
- data/lib/freight_kit/platforms/next_tms/graphql_client/query/invoice.rb +31 -0
- data/lib/freight_kit/platforms/next_tms/graphql_client/query/invoices.rb +39 -0
- data/lib/freight_kit/platforms/next_tms/graphql_client/query/shipment.rb +102 -0
- data/lib/freight_kit/platforms/next_tms/graphql_client/query/shipment_documents.rb +29 -0
- data/lib/freight_kit/platforms/next_tms/graphql_client/query/shipments.rb +110 -0
- data/lib/freight_kit/platforms/next_tms/graphql_client.rb +22 -0
- data/lib/freight_kit/platforms/next_tms/orm/address.rb +22 -0
- data/lib/freight_kit/platforms/next_tms/orm/base.rb +120 -0
- data/lib/freight_kit/platforms/next_tms/orm/broker_carrier.rb +18 -0
- data/lib/freight_kit/platforms/next_tms/orm/broker_customer.rb +22 -0
- data/lib/freight_kit/platforms/next_tms/orm/carrier.rb +14 -0
- data/lib/freight_kit/platforms/next_tms/orm/collection.rb +335 -0
- data/lib/freight_kit/platforms/next_tms/orm/customer.rb +12 -0
- data/lib/freight_kit/platforms/next_tms/orm/invoice.rb +21 -0
- data/lib/freight_kit/platforms/next_tms/orm/record_not_found_error.rb +9 -0
- data/lib/freight_kit/platforms/next_tms/orm/shipment.rb +50 -0
- data/lib/freight_kit/platforms/next_tms/orm.rb +38 -0
- data/lib/freight_kit/platforms/next_tms.rb +452 -0
- data/lib/freight_kit/platforms/the_great_information_factory.rb +492 -425
- data/lib/freight_kit/regions.rb +27 -0
- data/lib/freight_kit/shipment_packer.rb +36 -38
- data/lib/freight_kit/tariff/overlength_rule.rb +1 -5
- data/lib/freight_kit/tariff.rb +0 -2
- data/lib/freight_kit.rb +103 -11
- data/schemas/next_tms.json +110710 -0
- metadata +443 -55
- data/configuration/carriers/abfs.yml +0 -124
- data/configuration/carriers/btvp.yml +0 -84
- data/configuration/carriers/ccyq.yml +0 -121
- data/configuration/carriers/clni.yml +0 -113
- data/configuration/carriers/cnwy.yml +0 -113
- data/configuration/carriers/ctbv.yml +0 -117
- data/configuration/carriers/dcha.yml +0 -105
- data/configuration/carriers/dlds.yml +0 -111
- data/configuration/carriers/dphe.yml +0 -130
- data/configuration/carriers/drrq.yml +0 -131
- data/configuration/carriers/fcsy.yml +0 -102
- data/configuration/carriers/fwda.yml +0 -137
- data/configuration/carriers/jfj_transportation.yml +0 -2
- data/configuration/carriers/mtvl.yml +0 -12
- data/configuration/carriers/numk.yml +0 -14
- data/configuration/carriers/otcl.yml +0 -124
- data/configuration/carriers/pens.yml +0 -22
- data/configuration/carriers/rdfs.yml +0 -142
- data/configuration/carriers/saia.yml +0 -129
- data/configuration/carriers/sefl.yml +0 -115
- data/configuration/carriers/totl.yml +0 -111
- data/configuration/carriers/tqyl.yml +0 -28
- data/configuration/carriers/wrds.yml +0 -20
- data/configuration/platforms/carrier_logistics.yml +0 -25
- data/configuration/platforms/next.yml +0 -12
- data/configuration/platforms/the_great_information_factory.yml +0 -122
- data/lib/freight_kit/carriers/abfs.rb +0 -421
- data/lib/freight_kit/carriers/btvp.rb +0 -29
- data/lib/freight_kit/carriers/ccyq.rb +0 -310
- data/lib/freight_kit/carriers/clni.rb +0 -396
- data/lib/freight_kit/carriers/cnwy.rb +0 -327
- data/lib/freight_kit/carriers/ctbv.rb +0 -53
- data/lib/freight_kit/carriers/dcha.rb +0 -76
- data/lib/freight_kit/carriers/dlds.rb +0 -49
- data/lib/freight_kit/carriers/dphe.rb +0 -472
- data/lib/freight_kit/carriers/drrq.rb +0 -580
- data/lib/freight_kit/carriers/fcsy.rb +0 -57
- data/lib/freight_kit/carriers/fwda.rb +0 -744
- data/lib/freight_kit/carriers/mtvl.rb +0 -34
- data/lib/freight_kit/carriers/numk.rb +0 -58
- data/lib/freight_kit/carriers/otcl.rb +0 -520
- data/lib/freight_kit/carriers/pens.rb +0 -204
- data/lib/freight_kit/carriers/rdfs.rb +0 -521
- data/lib/freight_kit/carriers/sefl.rb +0 -342
- data/lib/freight_kit/carriers/totl.rb +0 -172
- data/lib/freight_kit/carriers/tqyl.rb +0 -337
- data/lib/freight_kit/carriers/wrds.rb +0 -246
- 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,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
|