diff --git a/src/Orbit.Application/Habits/Queries/GetHabitScheduleQuery.cs b/src/Orbit.Application/Habits/Queries/GetHabitScheduleQuery.cs index f0631776..53e0c6f5 100644 --- a/src/Orbit.Application/Habits/Queries/GetHabitScheduleQuery.cs +++ b/src/Orbit.Application/Habits/Queries/GetHabitScheduleQuery.cs @@ -214,6 +214,8 @@ private async Task>> HandleScheduled var dateFrom = request.DateFrom.Value; var dateTo = request.DateTo.Value; + if (dateTo.DayNumber - dateFrom.DayNumber > AppConstants.MaxInstanceHorizonDays) + dateTo = dateFrom.AddDays(AppConstants.MaxInstanceHorizonDays); var filtered = HabitScheduleFilters.FilterScheduledHabits(topLevel, dateFrom, dateTo, request.IncludeOverdue, lookup, weekStartDay); diff --git a/src/Orbit.Infrastructure/Services/StripeBillingService.cs b/src/Orbit.Infrastructure/Services/StripeBillingService.cs index 46f3da05..d9112513 100644 --- a/src/Orbit.Infrastructure/Services/StripeBillingService.cs +++ b/src/Orbit.Infrastructure/Services/StripeBillingService.cs @@ -75,8 +75,10 @@ public async Task CreateCheckoutSessionAsync( options.AllowPromotionCodes = true; } + var idempotencyKey = $"orbit-checkout-{userId}-{priceId}-{referralCouponId ?? "std"}"; var session = await StripeRetryPolicy.ExecuteWithRetryAsync( - () => clients.CheckoutSessions.CreateAsync(options, cancellationToken: cancellationToken), + () => clients.CheckoutSessions.CreateAsync( + options, new RequestOptions { IdempotencyKey = idempotencyKey }, cancellationToken), cancellationToken); return session.Url; } @@ -90,12 +92,13 @@ public async Task CreatePortalSessionAsync(string customerId, string ret { try { + var idempotencyKey = $"orbit-portal-{customerId}"; var session = await StripeRetryPolicy.ExecuteWithRetryAsync( () => clients.PortalSessions.CreateAsync(new Stripe.BillingPortal.SessionCreateOptions { Customer = customerId, ReturnUrl = returnUrl, - }, cancellationToken: cancellationToken), + }, new RequestOptions { IdempotencyKey = idempotencyKey }, cancellationToken), cancellationToken); return session.Url; } diff --git a/tests/Orbit.Application.Tests/Queries/Habits/GetHabitScheduleQueryHandlerTests.cs b/tests/Orbit.Application.Tests/Queries/Habits/GetHabitScheduleQueryHandlerTests.cs index 7a3df403..e21cd7fe 100644 --- a/tests/Orbit.Application.Tests/Queries/Habits/GetHabitScheduleQueryHandlerTests.cs +++ b/tests/Orbit.Application.Tests/Queries/Habits/GetHabitScheduleQueryHandlerTests.cs @@ -83,6 +83,26 @@ public async Task Handle_SingleDailyHabitInRange_ReturnsHabit() result.Value.Items[0].Title.Should().Be("Test Habit"); } + [Fact] + public async Task Handle_RangeExceedingHorizon_BoundsScheduledDatesAndInstancesConsistently() + { + var habit = CreateTestHabit(dueDate: Today); + SetupHabits(habit); + var query = new GetHabitScheduleQuery(UserId, Today, Today.AddDays(200)); + + var result = await _handler.Handle(query, CancellationToken.None); + + result.IsSuccess.Should().BeTrue(); + result.Value.Items.Should().HaveCount(1); + var item = result.Value.Items[0]; + var horizonEnd = Today.AddDays(AppConstants.MaxInstanceHorizonDays); + + item.ScheduledDates.Should().NotBeEmpty(); + item.ScheduledDates.Max().Should().Be(horizonEnd); + item.Instances.Should().NotBeEmpty(); + item.Instances.Select(i => i.Date).Max().Should().Be(horizonEnd); + } + [Fact] public async Task Handle_OneTimeTaskOverdue_WhenIncludeOverdue_ReturnsAsOverdue() {