fix(app-store): handle reused flag and response pagination

This commit is contained in:
Muhsin
2026-06-04 08:29:48 +04:00
parent 13a1afc0d1
commit 17a0d05853
3 changed files with 87 additions and 30 deletions
-2
View File
@@ -18,8 +18,6 @@ class AppStoreConnect::Client
fresh_payloads = fresh_review_payloads(review_payloads, since)
reviews.concat(fresh_payloads)
break if since.present? && fresh_payloads.size < review_payloads.size
next_url = payload.dig('links', 'next')
break if next_url.blank?
end
@@ -0,0 +1,20 @@
class RepurposeMessageReplyToFlagForChannelAppStore < ActiveRecord::Migration[7.1]
def up
# The message_reply_to flag (deprecated) has been renamed to channel_app_store.
# Disable it on any accounts that had message_reply_to enabled so the repurposed
# flag starts from plan/default configuration instead of inheriting stale state.
Account.feature_channel_app_store.find_each(batch_size: 100) do |account|
account.disable_features(:channel_app_store)
account.save!(validate: false)
end
# Remove the stale message_reply_to entry from ACCOUNT_LEVEL_FEATURE_DEFAULTS.
# ConfigLoader only adds new flags; it never removes renamed ones.
config = InstallationConfig.find_by(name: 'ACCOUNT_LEVEL_FEATURE_DEFAULTS')
return if config&.value.blank?
config.value = config.value.reject { |feature| feature['name'] == 'message_reply_to' }
config.save!
GlobalConfig.clear_cache
end
end
+67 -28
View File
@@ -75,39 +75,17 @@ RSpec.describe AppStoreConnect::Client do
expect(review_payload['response']['id']).to eq('response-1')
end
it 'stops fetching when a scheduled sync reaches already synced reviews' do
it 'keeps fetching older pages so updated developer responses are not skipped' do
stub_request(:get, 'https://api.appstoreconnect.apple.com/v1/apps/123456789/customerReviews')
.with(query: { include: 'response', limit: '200', sort: '-createdDate' })
.to_return(
status: 200,
body: {
data: [
{
id: 'review-1',
type: 'customerReviews',
attributes: {
createdDate: '2026-05-20T10:00:00-00:00'
}
},
{
id: 'review-2',
type: 'customerReviews',
attributes: {
createdDate: '2026-05-19T10:00:00-00:00'
}
}
],
links: {
next: 'https://api.appstoreconnect.apple.com/v1/apps/123456789/customerReviews?page=2'
}
}.to_json,
headers: { 'Content-Type' => 'application/json' }
)
.to_return(app_store_response(mixed_freshness_reviews_page))
stub_request(:get, 'https://api.appstoreconnect.apple.com/v1/apps/123456789/customerReviews?page=2')
.to_return(app_store_response(older_reviews_page_with_updated_response))
review_payloads = described_class.new(channel: channel).fetch_reviews(since: Time.zone.parse('2026-05-20T00:00:00-00:00'))
expect(review_payloads.pluck('review').pluck('id')).to eq(['review-1'])
expect(WebMock).not_to have_requested(:get, 'https://api.appstoreconnect.apple.com/v1/apps/123456789/customerReviews?page=2')
expect(review_payloads.pluck('review').pluck('id')).to eq(%w[review-1 review-3])
expect(WebMock).to have_requested(:get, 'https://api.appstoreconnect.apple.com/v1/apps/123456789/customerReviews?page=2')
end
it 'includes older reviews when the developer response was updated after the sync cursor' do
@@ -187,6 +165,67 @@ RSpec.describe AppStoreConnect::Client do
expect(AppStoreConnect::TokenService).to have_received(:new).twice
end
def app_store_response(body)
{
status: 200,
body: body.to_json,
headers: { 'Content-Type' => 'application/json' }
}
end
def mixed_freshness_reviews_page
{
data: [
{
id: 'review-1',
type: 'customerReviews',
attributes: { createdDate: '2026-05-20T10:00:00-00:00' }
},
{
id: 'review-2',
type: 'customerReviews',
attributes: { createdDate: '2026-05-19T10:00:00-00:00' }
}
],
links: {
next: 'https://api.appstoreconnect.apple.com/v1/apps/123456789/customerReviews?page=2'
}
}
end
def older_reviews_page_with_updated_response
{
data: [
{
id: 'review-3',
type: 'customerReviews',
attributes: { createdDate: '2026-05-18T10:00:00-00:00' },
relationships: response_relationship('response-3')
}
],
included: [updated_response_payload]
}
end
def response_relationship(response_id)
{
response: {
data: { id: response_id, type: 'customerReviewResponses' }
}
}
end
def updated_response_payload
{
id: 'response-3',
type: 'customerReviewResponses',
attributes: {
responseBody: 'Updated response',
lastModifiedDate: '2026-05-20T11:00:00-00:00'
}
}
end
end
describe '#create_review_response' do