diff --git a/app/services/app_store_connect/client.rb b/app/services/app_store_connect/client.rb index a5e0e3f3b..27fc85555 100644 --- a/app/services/app_store_connect/client.rb +++ b/app/services/app_store_connect/client.rb @@ -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 diff --git a/db/migrate/20260522080000_repurpose_message_reply_to_flag_for_channel_app_store.rb b/db/migrate/20260522080000_repurpose_message_reply_to_flag_for_channel_app_store.rb new file mode 100644 index 000000000..6ce52bbb5 --- /dev/null +++ b/db/migrate/20260522080000_repurpose_message_reply_to_flag_for_channel_app_store.rb @@ -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 diff --git a/spec/services/app_store_connect/client_spec.rb b/spec/services/app_store_connect/client_spec.rb index e87f1229f..ea3a60841 100644 --- a/spec/services/app_store_connect/client_spec.rb +++ b/spec/services/app_store_connect/client_spec.rb @@ -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