Skip to content

Commit 291ad2b

Browse files
authored
Merge pull request #1275 from Crown-Commercial-Service/feature/nrmi-384-fix-api-syncs
Feature/nrmi 384 fix api syncs
2 parents 100d71f + 1b9f6f4 commit 291ad2b

5 files changed

Lines changed: 157 additions & 64 deletions

File tree

app/jobs/inactive_urn_list_api_sync_job.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
class InactiveUrnListApiSyncJob < ApplicationJob
22
def perform
3-
rows = UrnLists::ApiClient.new.fetch_inactive_customers
3+
rows = UrnLists::ApiClient.new.fetch_inactive_rows
44

55
UrnLists::ImportInactiveCustomers.new(rows: rows).call
66
end

app/jobs/urn_list_api_sync_job.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ class UrnListApiSyncJob < ApplicationJob
22
def perform
33
urn_list = UrnList.create!(aasm_state: :pending, source: 'api_import')
44

5-
rows = UrnLists::ApiClient.new.fetch_customers
5+
rows = UrnLists::ApiClient.new.fetch_rows
66
count = UrnLists::ImportCustomers.new(rows: rows).call
77

88
urn_list.update!(

app/services/urn_lists/api_client.rb

Lines changed: 74 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -6,45 +6,71 @@ module UrnLists
66
class ApiClient
77
class ApiError < StandardError; end
88

9-
def fetch_customers
10-
token = fetch_access_token
11-
fetch_urn_list(token)
9+
TOP_COUNT = 1000
10+
11+
def fetch_rows
12+
fetch_paginated_rows(
13+
base_url: active_urns_url,
14+
params: {
15+
'api-version' => '2016-10-01',
16+
'sp' => '/triggers/manual/run',
17+
'sv' => '1.0',
18+
'filter' => "Published eq 'True'"
19+
},
20+
error_message: 'Failed to fetch URN list'
21+
)
1222
end
1323

14-
def fetch_inactive_customers
15-
token = fetch_access_token
16-
fetch_inactive_urn_list(token)
24+
def fetch_inactive_rows
25+
fetch_paginated_rows(
26+
base_url: inactive_urns_url,
27+
params: {
28+
'api-version' => '2016-10-01',
29+
'sp' => '/triggers/manual/run',
30+
'sv' => '1.0'
31+
},
32+
error_message: 'Failed to fetch inactive URN list'
33+
)
1734
end
1835

1936
private
2037

21-
def fetch_access_token
22-
uri = URI.parse(ENV.fetch('MDM_API_TOKEN_URL'))
38+
def fetch_paginated_rows(base_url:, params:, error_message:)
39+
token = fetch_access_token
2340

24-
response = Net::HTTP.post_form(uri, {
25-
grant_type: 'client_credentials',
26-
client_id: ENV.fetch('MDM_API_CLIENT_ID'),
27-
client_secret: ENV.fetch('MDM_API_CLIENT_SECRET'),
28-
scope: ENV.fetch('MDM_API_SCOPE')
29-
})
41+
all_rows = []
42+
skip = 0
3043

31-
raise ApiError, "Failed to fetch access token: #{response.code}" unless response.is_a?(Net::HTTPSuccess)
44+
loop do
45+
rows = fetch_page(
46+
token: token,
47+
base_url: base_url,
48+
params: params,
49+
top_count: TOP_COUNT,
50+
skip: skip,
51+
error_message: error_message
52+
)
3253

33-
body = JSON.parse(response.body)
34-
body.fetch('access_token')
35-
end
54+
break if rows.empty?
3655

37-
def fetch_urn_list(token)
38-
base_url = 'https://apim.crowncommercial.gov.uk/website-data/manual/paths/invoke/%5Batt%5D.%5Bvw_RMIActiveURNList%5D/'
39-
params = {
40-
'api-version' => '2016-10-01',
41-
'sp' => '/triggers/manual/run',
42-
'sv' => '1.0',
43-
'filter' => "Published eq 'True'"
44-
}
56+
all_rows.concat(rows)
57+
break if rows.size < TOP_COUNT
4558

59+
skip += TOP_COUNT
60+
end
61+
62+
all_rows
63+
end
64+
65+
# rubocop:disable Metrics/ParameterLists
66+
def fetch_page(token:, base_url:, params:, top_count:, skip:, error_message:)
4667
uri = URI(base_url)
47-
uri.query = URI.encode_www_form(params)
68+
uri.query = URI.encode_www_form(
69+
params.merge(
70+
'TopCount' => top_count,
71+
'SkipCount' => skip
72+
)
73+
)
4874

4975
request = Net::HTTP::Get.new(uri.to_s)
5076
request['Authorization'] = "Bearer #{token}"
@@ -54,43 +80,42 @@ def fetch_urn_list(token)
5480
http.request(request)
5581
end
5682

57-
raise ApiError, "Failed to fetch URN list: #{response.code}" unless response.is_a?(Net::HTTPSuccess)
83+
raise ApiError, "#{error_message}: #{response.code}" unless response.is_a?(Net::HTTPSuccess)
5884

5985
rows = JSON.parse(response.body)
6086
validate_rows!(rows)
6187
rows
6288
end
89+
# rubocop:enable Metrics/ParameterLists
6390

64-
def fetch_inactive_urn_list(token)
65-
base_url = 'https://apim.crowncommercial.gov.uk/website-data/manual/paths/invoke/%5Batt%5D.%5Bvw_RMIInactiveURNList%5D/'
66-
params = {
67-
'api-version' => '2016-10-01',
68-
'sp' => '/triggers/manual/run',
69-
'sv' => '1.0'
70-
}
71-
72-
uri = URI(base_url)
73-
uri.query = URI.encode_www_form(params)
74-
75-
request = Net::HTTP::Get.new(uri.to_s)
76-
request['Authorization'] = "Bearer #{token}"
77-
request['Accept'] = 'application/json'
91+
def fetch_access_token
92+
uri = URI.parse(ENV.fetch('MDM_API_TOKEN_URL'))
7893

79-
response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: uri.scheme == 'https') do |http|
80-
http.request(request)
81-
end
94+
response = Net::HTTP.post_form(uri, {
95+
grant_type: 'client_credentials',
96+
client_id: ENV.fetch('MDM_API_CLIENT_ID'),
97+
client_secret: ENV.fetch('MDM_API_CLIENT_SECRET'),
98+
scope: ENV.fetch('MDM_API_SCOPE')
99+
})
82100

83-
raise ApiError, "Failed to fetch inactive URN list: #{response.code}" unless response.is_a?(Net::HTTPSuccess)
101+
raise ApiError, "Failed to fetch access token: #{response.code}" unless response.is_a?(Net::HTTPSuccess)
84102

85-
rows = JSON.parse(response.body)
86-
validate_rows!(rows)
87-
rows
103+
body = JSON.parse(response.body)
104+
body.fetch('access_token')
88105
end
89106

90107
def validate_rows!(rows)
91108
return if rows.is_a?(Array) && rows.all? { |row| row.is_a?(Hash) }
92109

93110
raise ApiError, 'Invalid URN list format: expected an array of objects'
94111
end
112+
113+
def active_urns_url
114+
'https://apim.crowncommercial.gov.uk/website-data/manual/paths/invoke/%5Batt%5D.%5Bvw_RMIActiveURNList%5D/'
115+
end
116+
117+
def inactive_urns_url
118+
'https://apim.crowncommercial.gov.uk/website-data/manual/paths/invoke/%5Batt%5D.%5Bvw_RMIInactiveURNList%5D/'
119+
end
95120
end
96121
end

spec/jobs/urn_list_api_sync_job_spec.rb

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323
]
2424
end
2525

26-
let(:api_client_service) { double('UrnLists::ApiClient', fetch_customers: rows) }
26+
let(:api_client_service) { double('UrnLists::ApiClient', fetch_rows: rows) }
2727
let(:import_customers_service) { double('UrnLists::ImportCustomers', call: rows.count) }
2828

2929
before do
@@ -36,7 +36,7 @@
3636
described_class.perform_now
3737
end.to change(UrnList, :count).by(1)
3838

39-
expect(api_client_service).to have_received(:fetch_customers)
39+
expect(api_client_service).to have_received(:fetch_rows)
4040
expect(import_customers_service).to have_received(:call)
4141

4242
urn_list = UrnList.last
@@ -47,7 +47,7 @@
4747
end
4848

4949
it 'marks the urn list as failed when the api call fails' do
50-
allow(api_client_service).to receive(:fetch_customers).and_raise(StandardError.new('token failed'))
50+
allow(api_client_service).to receive(:fetch_rows).and_raise(StandardError.new('token failed'))
5151

5252
expect do
5353
described_class.perform_now

spec/services/urn_lists/api_client_spec.rb

Lines changed: 78 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,20 @@
11
require 'rails_helper'
22

33
RSpec.describe UrnLists::ApiClient do
4-
describe '#fetch_customers' do
4+
describe '#fetch_rows' do
5+
let(:top_count) { described_class::TOP_COUNT }
6+
57
before do
68
stub_request(:post, 'https://example.com/oauth/token')
79
.with(
810
body: { 'client_id' => 'test_client_id', 'client_secret' => 'test_client_secret',
911
'grant_type' => 'client_credentials', 'scope' => 'test_scope' },
1012
headers: {
1113
'Accept' => '*/*',
12-
'Accept-Encoding' => 'gzip;q=1.0,deflate;q=0.6,identity;q=0.3',
13-
'Content-Type' => 'application/x-www-form-urlencoded',
14-
'Host' => 'example.com',
15-
'User-Agent' => 'Ruby'
14+
'Accept-Encoding' => 'gzip;q=1.0,deflate;q=0.6,identity;q=0.3',
15+
'Content-Type' => 'application/x-www-form-urlencoded',
16+
'Host' => 'example.com',
17+
'User-Agent' => 'Ruby'
1618
}
1719
)
1820
.to_return(
@@ -21,13 +23,13 @@
2123
headers: { 'Content-Type' => 'application/json' }
2224
)
2325

24-
stub_request(:get, "https://apim.crowncommercial.gov.uk/website-data/manual/paths/invoke/%5Batt%5D.%5Bvw_RMIActiveURNList%5D/?api-version=2016-10-01&filter=Published%20eq%20'True'&sp=/triggers/manual/run&sv=1.0")
26+
stub_request(:get, "https://apim.crowncommercial.gov.uk/website-data/manual/paths/invoke/%5Batt%5D.%5Bvw_RMIActiveURNList%5D/?SkipCount=0&TopCount=1000&api-version=2016-10-01&filter=Published%20eq%20'True'&sp=/triggers/manual/run&sv=1.0")
2527
.with(
2628
headers: {
2729
'Accept' => 'application/json',
28-
'Accept-Encoding' => 'gzip;q=1.0,deflate;q=0.6,identity;q=0.3',
29-
'Authorization' => 'Bearer abc123',
30-
'User-Agent' => 'Ruby'
30+
'Accept-Encoding' => 'gzip;q=1.0,deflate;q=0.6,identity;q=0.3',
31+
'Authorization' => 'Bearer abc123',
32+
'User-Agent' => 'Ruby'
3133
}
3234
)
3335
.to_return(
@@ -53,7 +55,7 @@
5355

5456
it 'fetches and returns customer data' do
5557
client = described_class.new
56-
customers = client.fetch_customers
58+
customers = client.fetch_rows
5759

5860
expect(customers.size).to eq(1)
5961
expect(customers.first['urn']).to eq(10009655)
@@ -62,5 +64,71 @@
6264
expect(customers.first['sector']).to eq('central_government')
6365
expect(customers.first['published']).to eq(true)
6466
end
67+
68+
context 'when the API returns multiple pages' do
69+
let(:first_page_rows) do
70+
Array.new(top_count) do |i|
71+
{
72+
urn: 10009655 + i,
73+
name: "Customer #{i}",
74+
postcode: 'L3 9PP',
75+
sector: 'central_government',
76+
published: true
77+
}
78+
end
79+
end
80+
81+
let(:second_page_rows) do
82+
[
83+
{
84+
urn: 10009655 + top_count,
85+
name: "Customer #{top_count}",
86+
postcode: 'L3 9PP',
87+
sector: 'central_government',
88+
published: true
89+
}
90+
]
91+
end
92+
93+
before do
94+
stub_request(:get, "https://apim.crowncommercial.gov.uk/website-data/manual/paths/invoke/%5Batt%5D.%5Bvw_RMIActiveURNList%5D/?TopCount=#{top_count}&SkipCount=0&api-version=2016-10-01&filter=Published%20eq%20'True'&sp=/triggers/manual/run&sv=1.0")
95+
.with(
96+
headers: {
97+
'Accept' => 'application/json',
98+
'Accept-Encoding' => 'gzip;q=1.0,deflate;q=0.6,identity;q=0.3',
99+
'Authorization' => 'Bearer abc123',
100+
'User-Agent' => 'Ruby'
101+
}
102+
)
103+
.to_return(
104+
status: 200,
105+
body: first_page_rows.to_json,
106+
headers: { 'Content-Type' => 'application/json' }
107+
)
108+
109+
stub_request(:get, "https://apim.crowncommercial.gov.uk/website-data/manual/paths/invoke/%5Batt%5D.%5Bvw_RMIActiveURNList%5D/?TopCount=#{top_count}&SkipCount=#{top_count}&api-version=2016-10-01&filter=Published%20eq%20'True'&sp=/triggers/manual/run&sv=1.0")
110+
.with(
111+
headers: {
112+
'Accept' => 'application/json',
113+
'Accept-Encoding' => 'gzip;q=1.0,deflate;q=0.6,identity;q=0.3',
114+
'Authorization' => 'Bearer abc123',
115+
'User-Agent' => 'Ruby'
116+
}
117+
)
118+
.to_return(
119+
status: 200,
120+
body: second_page_rows.to_json,
121+
headers: { 'Content-Type' => 'application/json' }
122+
)
123+
end
124+
125+
it 'fetches each page and returns combined rows' do
126+
rows = described_class.new.fetch_rows
127+
128+
expect(rows.count).to eq(top_count + 1)
129+
expect(rows.first['urn']).to eq(10009655)
130+
expect(rows.last['urn']).to eq(10009655 + top_count)
131+
end
132+
end
65133
end
66134
end

0 commit comments

Comments
 (0)