rockauto_api 0.1.0
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 +7 -0
- data/lib/rockauto_api/cache.rb +34 -0
- data/lib/rockauto_api/client.rb +167 -0
- data/lib/rockauto_api/configuration.rb +23 -0
- data/lib/rockauto_api/endpoints/account.rb +159 -0
- data/lib/rockauto_api/endpoints/fitment.rb +61 -0
- data/lib/rockauto_api/endpoints/orders.rb +51 -0
- data/lib/rockauto_api/endpoints/part_categories.rb +83 -0
- data/lib/rockauto_api/endpoints/part_search.rb +165 -0
- data/lib/rockauto_api/endpoints/tools.rb +74 -0
- data/lib/rockauto_api/endpoints/vehicles.rb +86 -0
- data/lib/rockauto_api/errors.rb +10 -0
- data/lib/rockauto_api/models/account.rb +66 -0
- data/lib/rockauto_api/models/fitment.rb +22 -0
- data/lib/rockauto_api/models/order.rb +61 -0
- data/lib/rockauto_api/models/part.rb +75 -0
- data/lib/rockauto_api/models/tool.rb +41 -0
- data/lib/rockauto_api/models/vehicle.rb +76 -0
- data/lib/rockauto_api/parsers/fitment_parser.rb +40 -0
- data/lib/rockauto_api/parsers/html_helpers.rb +56 -0
- data/lib/rockauto_api/parsers/order_parser.rb +66 -0
- data/lib/rockauto_api/parsers/part_extractor.rb +79 -0
- data/lib/rockauto_api/railtie.rb +17 -0
- data/lib/rockauto_api/version.rb +5 -0
- data/lib/rockauto_api.rb +44 -0
- metadata +192 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: eff8195831ca08f029eb1128c3fbdc9a8bc5c6441a41d3eba61b2279add15d46
|
|
4
|
+
data.tar.gz: e961fb95855b0e0d7433ae5c9122ae3a755763e4a5ad99ce22bc0966759ebd9b
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: e5b8f220ea632303e38d8a34a9ef4bddc5481b2d2c618bc73de1bd286af05cfe8e6ad0fddf621c311cb175b38735a1bb7f04ba0edb3a192a015dbc574622a8d4
|
|
7
|
+
data.tar.gz: '078556f85c37110c00410c72602a46757c6e8d0fb344f74f1f3d8c14d3e5f0ad6a6870715d6ced9e3c060fe56d6dae593d41460b921aec8e3e64e8362d36ac2c'
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RockautoApi
|
|
4
|
+
class Cache
|
|
5
|
+
def initialize(store: nil)
|
|
6
|
+
@store = store
|
|
7
|
+
@memory = {}
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def fetch(key, ttl: 3600)
|
|
11
|
+
if @store
|
|
12
|
+
@store.fetch(key, expires_in: ttl) { yield }
|
|
13
|
+
else
|
|
14
|
+
@memory[key] ||= yield
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def delete(key)
|
|
19
|
+
if @store
|
|
20
|
+
@store.delete(key)
|
|
21
|
+
else
|
|
22
|
+
@memory.delete(key)
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def clear
|
|
27
|
+
if @store
|
|
28
|
+
@store.clear
|
|
29
|
+
else
|
|
30
|
+
@memory.clear
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RockautoApi
|
|
4
|
+
class Client
|
|
5
|
+
CATALOG_API_URL = "https://www.rockauto.com/catalog/catalogapi.php"
|
|
6
|
+
PARTSEARCH_URL = "https://www.rockauto.com/en/partsearch/"
|
|
7
|
+
ORDERSTATUS_URL = "https://www.rockauto.com/orderstatus/"
|
|
8
|
+
BASE_URL = "https://www.rockauto.com"
|
|
9
|
+
|
|
10
|
+
MOBILE_HEADERS = {
|
|
11
|
+
"User-Agent" => "Mozilla/5.0 (iPhone; CPU iPhone OS 17_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.1 Mobile/15E148 Safari/604.1",
|
|
12
|
+
"Accept" => "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
|
|
13
|
+
"Accept-Language" => "en-US,en;q=0.9",
|
|
14
|
+
"Sec-Fetch-Site" => "same-origin",
|
|
15
|
+
"Sec-Fetch-Mode" => "navigate",
|
|
16
|
+
"Sec-Fetch-Dest" => "document"
|
|
17
|
+
}.freeze
|
|
18
|
+
|
|
19
|
+
DESKTOP_HEADERS = {
|
|
20
|
+
"User-Agent" => "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/139.0.0.0 Safari/537.36",
|
|
21
|
+
"Accept" => "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8",
|
|
22
|
+
"Accept-Language" => "en-US,en;q=0.9",
|
|
23
|
+
"Sec-Ch-Ua" => '"Chromium";v="139", "Not;A=Brand";v="99"',
|
|
24
|
+
"Sec-Fetch-Site" => "same-origin",
|
|
25
|
+
"Sec-Fetch-Mode" => "navigate",
|
|
26
|
+
"Sec-Fetch-Dest" => "document"
|
|
27
|
+
}.freeze
|
|
28
|
+
|
|
29
|
+
INITIAL_COOKIES = {
|
|
30
|
+
"idlist" => "0",
|
|
31
|
+
"mkt_US" => "true",
|
|
32
|
+
"mkt_CA" => "false",
|
|
33
|
+
"mkt_MX" => "false",
|
|
34
|
+
"year_2005" => "true",
|
|
35
|
+
"ck" => "1"
|
|
36
|
+
}.freeze
|
|
37
|
+
|
|
38
|
+
attr_reader :cache, :authenticated
|
|
39
|
+
|
|
40
|
+
def initialize(mobile: nil, cache: nil)
|
|
41
|
+
mobile = RockautoApi.configuration&.default_mobile if mobile.nil?
|
|
42
|
+
headers = mobile ? MOBILE_HEADERS : DESKTOP_HEADERS
|
|
43
|
+
|
|
44
|
+
@conn = Faraday.new(url: BASE_URL) do |f|
|
|
45
|
+
f.request :url_encoded
|
|
46
|
+
f.use :cookie_jar
|
|
47
|
+
f.adapter Faraday.default_adapter
|
|
48
|
+
f.options.timeout = RockautoApi.configuration&.request_timeout || 30
|
|
49
|
+
headers.each { |k, v| f.headers[k] = v }
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
set_initial_cookies
|
|
53
|
+
@cache = cache || RockautoApi.configuration&.cache || RockautoApi::Cache.new
|
|
54
|
+
@session_initialized = false
|
|
55
|
+
@nck_token = nil
|
|
56
|
+
@jnck_token = nil
|
|
57
|
+
@authenticated = false
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def set_initial_cookies
|
|
61
|
+
INITIAL_COOKIES.each do |name, value|
|
|
62
|
+
@conn.headers["Cookie"] = "#{@conn.headers['Cookie']}; #{name}=#{value}" unless @conn.headers["Cookie"].to_s.include?("#{name}=")
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def init_session!
|
|
67
|
+
return if @session_initialized
|
|
68
|
+
|
|
69
|
+
resp = @conn.get("/")
|
|
70
|
+
@nck_token = Parsers::HtmlHelpers.extract_javascript_variable(resp.body, "_nck")
|
|
71
|
+
@jnck_token = @nck_token ? CGI.escape(@nck_token) : nil
|
|
72
|
+
@session_initialized = true
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def call_catalog_api(function, payload)
|
|
76
|
+
init_session!
|
|
77
|
+
|
|
78
|
+
data = {
|
|
79
|
+
"func" => function,
|
|
80
|
+
"payload" => payload.is_a?(String) ? payload : payload.to_json,
|
|
81
|
+
"api_json_request" => "1",
|
|
82
|
+
"sctchecked" => "1",
|
|
83
|
+
"scbeenloaded" => "false",
|
|
84
|
+
"curCartGroupID" => ""
|
|
85
|
+
}
|
|
86
|
+
data["_jnck"] = @jnck_token if @jnck_token
|
|
87
|
+
|
|
88
|
+
resp = Faraday.new(url: BASE_URL) do |f|
|
|
89
|
+
f.request :url_encoded
|
|
90
|
+
f.use :cookie_jar
|
|
91
|
+
f.adapter Faraday.default_adapter
|
|
92
|
+
f.options.timeout = RockautoApi.configuration&.request_timeout || 30
|
|
93
|
+
f.headers["X-Requested-With"] = "XMLHttpRequest"
|
|
94
|
+
f.headers["Content-Type"] = "application/x-www-form-urlencoded; charset=UTF-8"
|
|
95
|
+
f.headers["User-Agent"] = MOBILE_HEADERS["User-Agent"]
|
|
96
|
+
f.headers["Referer"] = "#{BASE_URL}/"
|
|
97
|
+
@conn.headers["Cookie"].to_s.split(";").each do |cookie|
|
|
98
|
+
name, val = cookie.strip.split("=", 2)
|
|
99
|
+
f.headers["Cookie"] = "#{f.headers['Cookie']}; #{name}=#{val}" if name && val
|
|
100
|
+
end
|
|
101
|
+
end.post("catalog/catalogapi.php", data)
|
|
102
|
+
|
|
103
|
+
JSON.parse(resp.body)
|
|
104
|
+
rescue Faraday::Error => e
|
|
105
|
+
raise NetworkError, "API request failed: #{e.message}"
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def post_with_csrf(url, form_data)
|
|
109
|
+
page_resp = @conn.get(url)
|
|
110
|
+
nck = Parsers::HtmlHelpers.extract_csrf_token(page_resp.body)
|
|
111
|
+
form_data["_nck"] = nck if nck
|
|
112
|
+
resp = @conn.post(url, form_data)
|
|
113
|
+
resp.body
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def simulate_navigation_context(make: nil, year: nil)
|
|
117
|
+
return unless make || year
|
|
118
|
+
|
|
119
|
+
if make
|
|
120
|
+
@conn.headers["Referer"] = "#{BASE_URL}/en/catalog/"
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
if year
|
|
124
|
+
@conn.headers["Cookie"] = "#{@conn.headers['Cookie']}; year_#{year}=true"
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
if make
|
|
128
|
+
payload = {
|
|
129
|
+
"jsn" => {
|
|
130
|
+
"make" => make,
|
|
131
|
+
"nodetype" => "make",
|
|
132
|
+
"loaded" => false,
|
|
133
|
+
"expand_after_load" => true,
|
|
134
|
+
"fetching" => true,
|
|
135
|
+
"max_group_index" => 363,
|
|
136
|
+
"mkt_US" => true,
|
|
137
|
+
"mkt_CA" => false,
|
|
138
|
+
"mkt_MX" => false
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
call_catalog_api("navnode_fetch", payload)
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def get(path)
|
|
146
|
+
resp = @conn.get(path)
|
|
147
|
+
resp.body
|
|
148
|
+
rescue Faraday::Error => e
|
|
149
|
+
raise NetworkError, "GET #{path} failed: #{e.message}"
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def post(path, body = nil)
|
|
153
|
+
resp = @conn.post(path, body)
|
|
154
|
+
resp.body
|
|
155
|
+
rescue Faraday::Error => e
|
|
156
|
+
raise NetworkError, "POST #{path} failed: #{e.message}"
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
include Endpoints::Vehicles
|
|
160
|
+
include Endpoints::PartCategories
|
|
161
|
+
include Endpoints::PartSearch
|
|
162
|
+
include Endpoints::Fitment
|
|
163
|
+
include Endpoints::Tools
|
|
164
|
+
include Endpoints::Orders
|
|
165
|
+
include Endpoints::Account
|
|
166
|
+
end
|
|
167
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RockautoApi
|
|
4
|
+
class Configuration
|
|
5
|
+
attr_accessor :default_mobile, :cache, :request_timeout, :user_agent, :credentials
|
|
6
|
+
|
|
7
|
+
def initialize
|
|
8
|
+
@default_mobile = true
|
|
9
|
+
@cache = nil
|
|
10
|
+
@request_timeout = 30
|
|
11
|
+
@user_agent = nil
|
|
12
|
+
@credentials = nil
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def email
|
|
16
|
+
credentials&.fetch(:email, nil)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def password
|
|
20
|
+
credentials&.fetch(:password, nil)
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RockautoApi
|
|
4
|
+
module Endpoints
|
|
5
|
+
module Account
|
|
6
|
+
LOGIN_URL = "https://www.rockauto.com/catalog/catalogapi.php"
|
|
7
|
+
PROFILE_URL = "/en/profile/"
|
|
8
|
+
ORDER_HISTORY_URL = "/en/orderhistory/"
|
|
9
|
+
|
|
10
|
+
def login(email, password)
|
|
11
|
+
payload = {
|
|
12
|
+
"jsn" => {
|
|
13
|
+
"email" => email,
|
|
14
|
+
"password" => password,
|
|
15
|
+
"keep_me_logged_in" => false
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
response = call_catalog_api("login", payload)
|
|
20
|
+
@authenticated = response["success"] == true || response.dig("response", "success") == true
|
|
21
|
+
@authenticated
|
|
22
|
+
rescue NetworkError
|
|
23
|
+
@authenticated = false
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def logout
|
|
27
|
+
response = call_catalog_api("logout", {})
|
|
28
|
+
@authenticated = false
|
|
29
|
+
true
|
|
30
|
+
rescue StandardError
|
|
31
|
+
@authenticated = false
|
|
32
|
+
true
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def authenticated?
|
|
36
|
+
@authenticated
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def get_saved_addresses
|
|
40
|
+
require_authentication!
|
|
41
|
+
html = get(PROFILE_URL)
|
|
42
|
+
doc = Nokogiri::HTML(html)
|
|
43
|
+
|
|
44
|
+
addresses = doc.css("table tr").map { |row|
|
|
45
|
+
cells = row.css("td").map { |c| c.text.strip }
|
|
46
|
+
next nil if cells.size < 5
|
|
47
|
+
|
|
48
|
+
Models::SavedAddress.new(
|
|
49
|
+
name: cells[0],
|
|
50
|
+
full_name: cells[1],
|
|
51
|
+
address_line1: cells[2],
|
|
52
|
+
city: cells[3],
|
|
53
|
+
state: cells[4],
|
|
54
|
+
postal_code: cells[5],
|
|
55
|
+
country: cells[6] || "US",
|
|
56
|
+
phone: cells[7]
|
|
57
|
+
)
|
|
58
|
+
}.compact
|
|
59
|
+
|
|
60
|
+
Models::SavedAddressesResult.new(
|
|
61
|
+
addresses: addresses,
|
|
62
|
+
count: addresses.size,
|
|
63
|
+
has_default: addresses.any? { |a| a.name&.downcase&.include?("default") }
|
|
64
|
+
)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def get_saved_vehicles
|
|
68
|
+
require_authentication!
|
|
69
|
+
html = get(PROFILE_URL)
|
|
70
|
+
doc = Nokogiri::HTML(html)
|
|
71
|
+
|
|
72
|
+
vehicles = doc.css("a[href*='carcode']").map { |a|
|
|
73
|
+
href = a["href"]
|
|
74
|
+
text = a.text.strip
|
|
75
|
+
match = href&.match(/carcode=(\d+)/)
|
|
76
|
+
|
|
77
|
+
parts = text.split(" ")
|
|
78
|
+
year = parts[0].to_i
|
|
79
|
+
|
|
80
|
+
Models::SavedVehicle.new(
|
|
81
|
+
year: year,
|
|
82
|
+
make: parts[1] || "",
|
|
83
|
+
model: parts[2] || "",
|
|
84
|
+
engine: parts[3..]&.join(" "),
|
|
85
|
+
carcode: match&.captures&.first,
|
|
86
|
+
display_name: text,
|
|
87
|
+
catalog_url: Parsers::HtmlHelpers.make_absolute_url(href)
|
|
88
|
+
)
|
|
89
|
+
}.compact
|
|
90
|
+
|
|
91
|
+
Models::SavedVehiclesResult.new(
|
|
92
|
+
vehicles: vehicles,
|
|
93
|
+
count: vehicles.size
|
|
94
|
+
)
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def get_account_activity
|
|
98
|
+
addresses = get_saved_addresses
|
|
99
|
+
vehicles = get_saved_vehicles
|
|
100
|
+
|
|
101
|
+
Models::AccountActivityResult.new(
|
|
102
|
+
saved_addresses: addresses,
|
|
103
|
+
saved_vehicles: vehicles,
|
|
104
|
+
order_history: nil,
|
|
105
|
+
has_discount_codes: false,
|
|
106
|
+
has_store_credit: false,
|
|
107
|
+
has_alerts: false
|
|
108
|
+
)
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def get_order_history(filter_params = {})
|
|
112
|
+
require_authentication!
|
|
113
|
+
html = get(ORDER_HISTORY_URL)
|
|
114
|
+
doc = Nokogiri::HTML(html)
|
|
115
|
+
|
|
116
|
+
orders = doc.css("table tr").map { |row|
|
|
117
|
+
cells = row.css("td").map { |c| c.text.strip }
|
|
118
|
+
next nil if cells.size < 3
|
|
119
|
+
next nil if cells[0].match?(/\A\s*(?:Order|Date|Status)\s*\z/i)
|
|
120
|
+
|
|
121
|
+
Models::OrderHistoryItem.new(
|
|
122
|
+
order_number: cells[0] || "",
|
|
123
|
+
date: cells[1],
|
|
124
|
+
status: cells[2],
|
|
125
|
+
total: cells[3],
|
|
126
|
+
vehicle: cells[4],
|
|
127
|
+
order_url: nil
|
|
128
|
+
)
|
|
129
|
+
}.compact
|
|
130
|
+
|
|
131
|
+
Models::OrderHistoryResult.new(
|
|
132
|
+
orders: orders,
|
|
133
|
+
count: orders.size,
|
|
134
|
+
filter_applied: filter_params.empty? ? nil : filter_params.to_s,
|
|
135
|
+
search_time: Time.now.iso8601
|
|
136
|
+
)
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def add_external_order(email_or_phone, order_number)
|
|
140
|
+
require_authentication!
|
|
141
|
+
form_data = {
|
|
142
|
+
"email" => email_or_phone.include?("@") ? email_or_phone : "",
|
|
143
|
+
"phone" => email_or_phone.include?("@") ? "" : email_or_phone,
|
|
144
|
+
"order_number" => order_number
|
|
145
|
+
}
|
|
146
|
+
post_with_csrf("/en/orderhistory/addorder/", form_data)
|
|
147
|
+
true
|
|
148
|
+
rescue StandardError
|
|
149
|
+
false
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
private
|
|
153
|
+
|
|
154
|
+
def require_authentication!
|
|
155
|
+
raise AuthenticationError, "Not authenticated. Call login(email, password) first." unless @authenticated
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
end
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RockautoApi
|
|
4
|
+
module Endpoints
|
|
5
|
+
module Fitment
|
|
6
|
+
def get_fitment_for_part(listing_data)
|
|
7
|
+
return default_result("", "") unless listing_data.is_a?(Hash) && !listing_data.empty?
|
|
8
|
+
|
|
9
|
+
car_data = listing_data["car"] || listing_data[:car] || {}
|
|
10
|
+
supp_data = listing_data["supplemental"] || listing_data[:supplemental] || {}
|
|
11
|
+
|
|
12
|
+
part_number = supp_data["partnumber"] || supp_data[:partnumber] || ""
|
|
13
|
+
brand = supp_data["catalogname"] || supp_data[:catalogname] || ""
|
|
14
|
+
|
|
15
|
+
cache_key = "rockauto:fitment:#{part_number}:#{brand}"
|
|
16
|
+
|
|
17
|
+
cache.fetch(cache_key, ttl: 604800) do
|
|
18
|
+
payload = {
|
|
19
|
+
"partData" => {
|
|
20
|
+
"groupindex" => listing_data["groupindex"] || listing_data[:groupindex] || "0",
|
|
21
|
+
"listing_data_essential" => {
|
|
22
|
+
"groupindex" => listing_data["groupindex"] || listing_data[:groupindex] || "0",
|
|
23
|
+
"carcode" => car_data["carcode"] || car_data[:carcode] || 0,
|
|
24
|
+
"parttype" => car_data["parttype"] || car_data[:parttype] || "",
|
|
25
|
+
"partkey" => car_data["partkey"] || car_data[:partkey] || ""
|
|
26
|
+
},
|
|
27
|
+
"listing_data_supplemental" => {
|
|
28
|
+
"partnumber" => part_number,
|
|
29
|
+
"catalogname" => brand,
|
|
30
|
+
"belongstolisting" => "2",
|
|
31
|
+
"sortgroup" => 0,
|
|
32
|
+
"sortgrouptext" => "",
|
|
33
|
+
"paramdesc" => "",
|
|
34
|
+
"showhide" => {}
|
|
35
|
+
},
|
|
36
|
+
"OptKey" => listing_data["optkey"] || listing_data[:optkey] || ""
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
response = call_catalog_api("getbuyersguide", payload)
|
|
41
|
+
html = response.dig("buyersguidepieces", "body")
|
|
42
|
+
|
|
43
|
+
Parsers::FitmentParser.parse(html, part_number: part_number, brand: brand)
|
|
44
|
+
end
|
|
45
|
+
rescue NetworkError, ParseError
|
|
46
|
+
default_result(part_number, brand)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
private
|
|
50
|
+
|
|
51
|
+
def default_result(part_number, brand)
|
|
52
|
+
Models::BuyersGuideResult.new(
|
|
53
|
+
part_number: part_number,
|
|
54
|
+
brand: brand,
|
|
55
|
+
fitments: [],
|
|
56
|
+
count: 0
|
|
57
|
+
)
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RockautoApi
|
|
4
|
+
module Endpoints
|
|
5
|
+
module Orders
|
|
6
|
+
def lookup_order_status(email_or_phone, order_number)
|
|
7
|
+
form_data = {
|
|
8
|
+
"email" => email_or_phone.include?("@") ? email_or_phone : "",
|
|
9
|
+
"phone" => email_or_phone.include?("@") ? "" : email_or_phone,
|
|
10
|
+
"order_number" => order_number
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
html = post_with_csrf("/orderstatus/", form_data)
|
|
14
|
+
parsed = Parsers::OrderParser.parse(html)
|
|
15
|
+
|
|
16
|
+
if parsed[:order_number]
|
|
17
|
+
Models::OrderStatusResult.new(
|
|
18
|
+
success: true,
|
|
19
|
+
order: Models::OrderStatus.new(**parsed),
|
|
20
|
+
error: nil
|
|
21
|
+
)
|
|
22
|
+
else
|
|
23
|
+
Models::OrderStatusResult.new(
|
|
24
|
+
success: false,
|
|
25
|
+
order: nil,
|
|
26
|
+
error: Models::OrderStatusError.new(
|
|
27
|
+
error_type: "not_found",
|
|
28
|
+
message: "Order not found",
|
|
29
|
+
order_number: order_number,
|
|
30
|
+
suggestions: ["Check the order number", "Try searching by email instead"]
|
|
31
|
+
)
|
|
32
|
+
)
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def request_order_list(method, contact)
|
|
37
|
+
case method.to_s
|
|
38
|
+
when "email"
|
|
39
|
+
post_with_csrf("/orderstatus/", { "send_email" => "1", "email" => contact })
|
|
40
|
+
when "sms"
|
|
41
|
+
post_with_csrf("/orderstatus/", { "send_sms" => "1", "phone" => contact })
|
|
42
|
+
else
|
|
43
|
+
false
|
|
44
|
+
end
|
|
45
|
+
true
|
|
46
|
+
rescue StandardError
|
|
47
|
+
false
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RockautoApi
|
|
4
|
+
module Endpoints
|
|
5
|
+
module PartCategories
|
|
6
|
+
def get_part_categories(make, year, model, carcode)
|
|
7
|
+
payload = {
|
|
8
|
+
"jsn" => {
|
|
9
|
+
"make" => make,
|
|
10
|
+
"year" => year.to_s,
|
|
11
|
+
"model" => model,
|
|
12
|
+
"carcode" => carcode,
|
|
13
|
+
"nodetype" => "model",
|
|
14
|
+
"loaded" => false,
|
|
15
|
+
"expand_after_load" => true,
|
|
16
|
+
"fetching" => true,
|
|
17
|
+
"max_group_index" => 0,
|
|
18
|
+
"mkt_US" => true,
|
|
19
|
+
"mkt_CA" => false,
|
|
20
|
+
"mkt_MX" => false
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
response = call_catalog_api("navnode_fetch", payload)
|
|
25
|
+
html = response.dig("html_fill_sections", "navchildren[]") || ""
|
|
26
|
+
|
|
27
|
+
categories = parse_categories_from_html(html)
|
|
28
|
+
|
|
29
|
+
Models::VehiclePartCategories.new(
|
|
30
|
+
make: make,
|
|
31
|
+
year: year,
|
|
32
|
+
model: model,
|
|
33
|
+
carcode: carcode,
|
|
34
|
+
categories: categories,
|
|
35
|
+
count: categories.size
|
|
36
|
+
)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def get_parts_by_category(make, year, model, carcode, category_group_name)
|
|
40
|
+
categories_result = get_part_categories(make, year, model, carcode)
|
|
41
|
+
category = categories_result.categories.find { |c| c.group_name == category_group_name }
|
|
42
|
+
|
|
43
|
+
return Models::VehiclePartsResult.new(
|
|
44
|
+
make: make, year: year, model: model, carcode: carcode,
|
|
45
|
+
category: category_group_name, parts: [], count: 0
|
|
46
|
+
) unless category&.href
|
|
47
|
+
|
|
48
|
+
html = get(category.href)
|
|
49
|
+
doc = Nokogiri::HTML(html)
|
|
50
|
+
parts = parse_parts_from_table(doc)
|
|
51
|
+
|
|
52
|
+
Models::VehiclePartsResult.new(
|
|
53
|
+
make: make, year: year, model: model, carcode: carcode,
|
|
54
|
+
category: category_group_name, parts: parts, count: parts.size
|
|
55
|
+
)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
private
|
|
59
|
+
|
|
60
|
+
def parse_categories_from_html(html)
|
|
61
|
+
doc = Nokogiri::HTML(html)
|
|
62
|
+
doc.css("a").map { |a|
|
|
63
|
+
text = a.text.strip
|
|
64
|
+
href = a["href"]
|
|
65
|
+
next nil if text.empty? || href.nil?
|
|
66
|
+
Models::PartCategory.new(
|
|
67
|
+
name: text,
|
|
68
|
+
group_name: text,
|
|
69
|
+
href: Parsers::HtmlHelpers.make_absolute_url(href)
|
|
70
|
+
)
|
|
71
|
+
}.compact
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def parse_parts_from_table(doc)
|
|
75
|
+
doc.css("table tr").map { |row|
|
|
76
|
+
cells = row.css("td")
|
|
77
|
+
next nil if cells.size < 3
|
|
78
|
+
Parsers::PartExtractor.extract_from_row(row)
|
|
79
|
+
}.compact
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|