fix(app-store): sync updated review responses

This commit is contained in:
Muhsin
2026-06-03 20:23:26 +04:00
parent 48a0fa1d67
commit b93cb5b654
4 changed files with 103 additions and 10 deletions
@@ -20,7 +20,7 @@ class Inboxes::FetchAppStoreReviewsJob < ApplicationJob
channel.fetch_reviews.each do |review_payload|
::AppStore::ReviewBuilder.new(review_payload: review_payload, channel: channel).perform
synced_dates << parsed_review_created_at(review_payload)
synced_dates << payload_synced_at(review_payload)
rescue StandardError => e
failed = true
ChatwootExceptionTracker.new(e, account: channel.account).capture_exception
@@ -28,11 +28,18 @@ class Inboxes::FetchAppStoreReviewsJob < ApplicationJob
return if failed
synced_dates.compact.max
[channel.last_synced_at, synced_dates.compact.max].compact.max
end
def parsed_review_created_at(review_payload)
Time.zone.parse(review_payload.dig('review', 'attributes', 'createdDate').to_s)
def payload_synced_at(review_payload)
[
parsed_timestamp(review_payload.dig('review', 'attributes', 'createdDate')),
parsed_timestamp(review_payload.dig('response', 'attributes', 'lastModifiedDate'))
].compact.max
end
def parsed_timestamp(value)
Time.zone.parse(value.to_s)
rescue StandardError
nil
end
+14 -6
View File
@@ -62,16 +62,24 @@ class AppStoreConnect::Client
def fresh_review_payloads(review_payloads, since)
return review_payloads if since.blank?
review_payloads.select { |review_payload| review_created_after?(review_payload, since) }
review_payloads.select { |review_payload| review_updated_after?(review_payload, since) }
end
def review_created_after?(review_payload, since)
created_at = Time.zone.parse(review_payload.dig('review', 'attributes', 'createdDate').to_s)
return true if created_at.blank?
def review_updated_after?(review_payload, since)
review_updated_at = [
parsed_timestamp(review_payload.dig('review', 'attributes', 'createdDate')),
parsed_timestamp(review_payload.dig('response', 'attributes', 'lastModifiedDate'))
].compact.max
created_at > since
return true if review_updated_at.blank?
review_updated_at >= since
end
def parsed_timestamp(value)
Time.zone.parse(value.to_s)
rescue StandardError
true
nil
end
def get(path, query = {})
@@ -51,6 +51,41 @@ RSpec.describe Inboxes::FetchAppStoreReviewsJob do
expect(channel.reload.last_synced_at).to eq(Time.zone.parse('2026-05-20T11:00:00-00:00'))
end
it 'updates the sync timestamp to the response update date when it is newer than the review date' do
payload = review_payload.deep_dup
payload['response'] = {
'id' => 'response-1',
'attributes' => {
'lastModifiedDate' => '2026-05-20T12:00:00-00:00'
}
}
allow(channel).to receive(:fetch_reviews).and_return([payload])
allow(AppStore::ReviewBuilder).to receive(:new).and_return(review_builder)
described_class.perform_now(channel)
expect(channel.reload.last_synced_at).to eq(Time.zone.parse('2026-05-20T12:00:00-00:00'))
end
it 'does not move the sync timestamp backwards when processing an older response update' do
channel.update!(last_synced_at: Time.zone.parse('2026-05-20T13:00:00-00:00'))
payload = review_payload.deep_dup
payload['response'] = {
'id' => 'response-1',
'attributes' => {
'lastModifiedDate' => '2026-05-20T12:00:00-00:00'
}
}
allow(channel).to receive(:fetch_reviews).and_return([payload])
allow(AppStore::ReviewBuilder).to receive(:new).and_return(review_builder)
described_class.perform_now(channel)
expect(channel.reload.last_synced_at).to eq(Time.zone.parse('2026-05-20T13:00:00-00:00'))
end
it 'captures per-review errors and continues syncing' do
exception_tracker = instance_double(ChatwootExceptionTracker, capture_exception: true)
@@ -110,6 +110,49 @@ RSpec.describe AppStoreConnect::Client do
expect(WebMock).not_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
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-19T10:00:00-00:00'
},
relationships: {
response: {
data: {
id: 'response-1',
type: 'customerReviewResponses'
}
}
}
}
],
included: [
{
id: 'response-1',
type: 'customerReviewResponses',
attributes: {
responseBody: 'Updated response',
lastModifiedDate: '2026-05-20T11:00:00-00:00'
}
}
]
}.to_json,
headers: { 'Content-Type' => 'application/json' }
)
review_payloads = described_class.new(channel: channel).fetch_reviews(since: Time.zone.parse('2026-05-20T10:00:00-00:00'))
expect(review_payloads.pluck('review').pluck('id')).to eq(['review-1'])
expect(review_payloads.first['response']['attributes']['responseBody']).to eq('Updated response')
end
it 'fetches a fresh cached token for each request' do
first_token_service = instance_double(AppStoreConnect::TokenService, token: 'first-token')
second_token_service = instance_double(AppStoreConnect::TokenService, token: 'second-token')