Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,7 @@
if (newSuggestions > 0 && IsInQuietHours(user, utcNow)
&& !await HasRecentSuggestionNotification(user.Id, utcNow, ct))
{
await CreateSuggestionNotification(user.Id, newSuggestions, ct);
await CreateSuggestionNotification(user, newSuggestions, ct);
}

user.MarkCalendarSyncSuccess(utcNow);
Expand Down Expand Up @@ -258,10 +258,13 @@
{
user.MarkCalendarSyncReconnectRequired(errorCode);

var isPortuguese = LocaleHelper.IsPortuguese(user.Language);
var notification = Notification.Create(
user.Id,
"Google Calendar disconnected",
"Auto-sync paused. Reconnect to resume.",
isPortuguese ? "Google Calendar desconectado" : "Google Calendar disconnected",
isPortuguese
? "A sincroniza莽茫o autom谩tica est谩 pausada. Reconecte para retomar."
: "Auto-sync paused. Reconnect to resume.",
url: "/calendar-sync");
await deps.NotificationRepository.AddAsync(notification, ct);

Expand All @@ -278,15 +281,19 @@
ct);
}

private async Task CreateSuggestionNotification(Guid userId, int count, CancellationToken ct)
private async Task CreateSuggestionNotification(User user, int count, CancellationToken ct)
{
var title = count == 1
? "1 new calendar event"
: $"{count} new calendar events";
var isPortuguese = LocaleHelper.IsPortuguese(user.Language);
var title = isPortuguese
? (count == 1 ? "1 novo evento do calend谩rio" : $"{count} novos eventos do calend谩rio")

Check warning on line 288 in src/Orbit.Application/Calendar/Commands/RunCalendarAutoSyncCommand.cs

View workflow job for this annotation

GitHub Actions / SonarCloud Analysis

Extract this nested ternary operation into an independent statement.

Check warning on line 288 in src/Orbit.Application/Calendar/Commands/RunCalendarAutoSyncCommand.cs

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Extract this nested ternary operation into an independent statement.

See more on https://sonarcloud.io/project/issues?id=thomasluizon_orbit-api&issues=AZ8pef2oYv7Icn6YaZdL&open=AZ8pef2oYv7Icn6YaZdL&pullRequest=279
: (count == 1 ? "1 new calendar event" : $"{count} new calendar events");

Check warning on line 289 in src/Orbit.Application/Calendar/Commands/RunCalendarAutoSyncCommand.cs

View workflow job for this annotation

GitHub Actions / SonarCloud Analysis

Extract this nested ternary operation into an independent statement.

Check warning on line 289 in src/Orbit.Application/Calendar/Commands/RunCalendarAutoSyncCommand.cs

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Extract this nested ternary operation into an independent statement.

See more on https://sonarcloud.io/project/issues?id=thomasluizon_orbit-api&issues=AZ8pef2oYv7Icn6YaZdM&open=AZ8pef2oYv7Icn6YaZdM&pullRequest=279
var body = isPortuguese
? "Toque para revisar e importar"
: "Tap to review and import";
var notification = Notification.Create(
userId,
user.Id,
title,
"Tap to review and import",
body,
url: "/calendar-sync?mode=review");
await deps.NotificationRepository.AddAsync(notification, ct);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -719,6 +719,116 @@ public async Task Handle_ConcurrencyConflictThenSuccess_RetryResolvesToSuccess()
await _unitOfWork.Received(2).SaveChangesAsync(Arg.Any<CancellationToken>());
}

[Fact]
public async Task Handle_Success_SuggestionNotification_EnglishByDefault()
{
var user = CreateEnabledProUser();
StubUser(user);
_tokenService.TryRefreshAsync(user, Arg.Any<CancellationToken>())
.Returns(new GoogleTokenRefreshOutcome("new_access", GoogleTokenRefreshResult.Success, null));
_fetcher.FetchAsync(Arg.Any<string>(), Arg.Any<IReadOnlyCollection<string>?>(), Arg.Any<DateTime?>(), Arg.Any<CancellationToken>())
.Returns(new List<CalendarEventItem>
{
new("evt_a", "Daily standup", null, "2026-04-10", "09:00", "09:30", true, null, []),
new("evt_b", "Review", null, "2026-04-11", "10:00", "11:00", true, null, [])
});

Notification? captured = null;
await _notificationRepo.AddAsync(Arg.Do<Notification>(n => captured = n), Arg.Any<CancellationToken>());

await _handler.Handle(new RunCalendarAutoSyncCommand(user.Id), default);

captured.Should().NotBeNull();
captured!.Url.Should().Be("/calendar-sync?mode=review");
captured.Title.Should().Be("2 new calendar events");
captured.Body.Should().Be("Tap to review and import");
}

[Fact]
public async Task Handle_Success_SuggestionNotification_PortuguesePlural()
{
var user = CreateEnabledProUser();
user.SetLanguage("pt-BR");
StubUser(user);
_tokenService.TryRefreshAsync(user, Arg.Any<CancellationToken>())
.Returns(new GoogleTokenRefreshOutcome("new_access", GoogleTokenRefreshResult.Success, null));
_fetcher.FetchAsync(Arg.Any<string>(), Arg.Any<IReadOnlyCollection<string>?>(), Arg.Any<DateTime?>(), Arg.Any<CancellationToken>())
.Returns(new List<CalendarEventItem>
{
new("evt_a", "Daily standup", null, "2026-04-10", "09:00", "09:30", true, null, []),
new("evt_b", "Review", null, "2026-04-11", "10:00", "11:00", true, null, [])
});

Notification? captured = null;
await _notificationRepo.AddAsync(Arg.Do<Notification>(n => captured = n), Arg.Any<CancellationToken>());

await _handler.Handle(new RunCalendarAutoSyncCommand(user.Id), default);

captured.Should().NotBeNull();
captured!.Title.Should().Be("2 novos eventos do calend谩rio");
captured.Body.Should().Be("Toque para revisar e importar");
}

[Fact]
public async Task Handle_Success_SuggestionNotification_PortugueseSingular()
{
var user = CreateEnabledProUser();
user.SetLanguage("pt-BR");
StubUser(user);
_tokenService.TryRefreshAsync(user, Arg.Any<CancellationToken>())
.Returns(new GoogleTokenRefreshOutcome("new_access", GoogleTokenRefreshResult.Success, null));
_fetcher.FetchAsync(Arg.Any<string>(), Arg.Any<IReadOnlyCollection<string>?>(), Arg.Any<DateTime?>(), Arg.Any<CancellationToken>())
.Returns(new List<CalendarEventItem>
{
new("evt_a", "Daily standup", null, "2026-04-10", "09:00", "09:30", true, null, [])
});

Notification? captured = null;
await _notificationRepo.AddAsync(Arg.Do<Notification>(n => captured = n), Arg.Any<CancellationToken>());

await _handler.Handle(new RunCalendarAutoSyncCommand(user.Id), default);

captured.Should().NotBeNull();
captured!.Title.Should().Be("1 novo evento do calend谩rio");
}

[Fact]
public async Task Handle_ReconnectRequired_Notification_EnglishByDefault()
{
var user = CreateEnabledProUser();
StubUser(user);
_tokenService.TryRefreshAsync(user, Arg.Any<CancellationToken>())
.Returns(new GoogleTokenRefreshOutcome(null, GoogleTokenRefreshResult.RefreshTokenInvalid, "invalid_grant"));

Notification? captured = null;
await _notificationRepo.AddAsync(Arg.Do<Notification>(n => captured = n), Arg.Any<CancellationToken>());

await _handler.Handle(new RunCalendarAutoSyncCommand(user.Id), default);

captured.Should().NotBeNull();
captured!.Title.Should().Be("Google Calendar disconnected");
captured.Body.Should().Be("Auto-sync paused. Reconnect to resume.");
}

[Fact]
public async Task Handle_ReconnectRequired_Notification_Portuguese()
{
var user = CreateEnabledProUser();
user.SetLanguage("pt-BR");
StubUser(user);
_tokenService.TryRefreshAsync(user, Arg.Any<CancellationToken>())
.Returns(new GoogleTokenRefreshOutcome(null, GoogleTokenRefreshResult.RefreshTokenInvalid, "invalid_grant"));

Notification? captured = null;
await _notificationRepo.AddAsync(Arg.Do<Notification>(n => captured = n), Arg.Any<CancellationToken>());

await _handler.Handle(new RunCalendarAutoSyncCommand(user.Id), default);

captured.Should().NotBeNull();
captured!.Title.Should().Be("Google Calendar desconectado");
captured.Body.Should().Be("A sincroniza莽茫o autom谩tica est谩 pausada. Reconecte para retomar.");
}

private sealed class FakeUniqueViolationException : DbException
{
public override string SqlState => "23505";
Expand Down
Loading