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
1 change: 1 addition & 0 deletions src/Orbit.Domain/Common/DomainErrors.cs
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ public static class DomainErrors
public static readonly AppError OneTimeTaskHasEndDate = new("ONE_TIME_TASK_HAS_END_DATE", "One-time tasks cannot have an end date.");
public static readonly AppError EndDateBeforeStartDate = new("END_DATE_BEFORE_START", "End date must be on or after the start date.");
public static readonly AppError MaxScheduledReminders = new("MAX_SCHEDULED_REMINDERS", "A habit can have at most {0} scheduled reminders.");
public static readonly AppError MaxReminderTimes = new("MAX_REMINDER_TIMES", "A habit can have at most {0} reminder times.");
public static readonly AppError DuplicateScheduledReminders = new("DUPLICATE_SCHEDULED_REMINDERS", "Scheduled reminders must not contain duplicate entries.");
public static readonly AppError EmojiTooLong = new("EMOJI_TOO_LONG", "Habit emoji must not exceed {0} characters.");

Expand Down
10 changes: 9 additions & 1 deletion src/Orbit.Domain/Entities/Habit.cs
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,10 @@ public static Result<Habit> Create(HabitCreateParams p)
if (reminderValidation is not null)
return Result.Failure<Habit>(reminderValidation);

var reminderTimesValidation = HabitInvariants.ValidateReminderTimes(p.ReminderTimes);
if (reminderTimesValidation is not null)
return Result.Failure<Habit>(reminderTimesValidation);

return Result.Success(new Habit
{
UserId = p.UserId,
Expand Down Expand Up @@ -342,7 +346,11 @@ public Result Update(HabitUpdateParams p)
if (emojiValidation is not null)
return emojiValidation;

return HabitInvariants.ValidateScheduledReminders(p.ScheduledReminders);
var scheduledReminderValidation = HabitInvariants.ValidateScheduledReminders(p.ScheduledReminders);
if (scheduledReminderValidation is not null)
return scheduledReminderValidation;

return HabitInvariants.ValidateReminderTimes(p.ReminderTimes);
}

private void ApplyRequiredUpdates(HabitUpdateParams p)
Expand Down
11 changes: 11 additions & 0 deletions src/Orbit.Domain/Entities/HabitInvariants.cs
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,17 @@ internal static class HabitInvariants
return null;
}

public static AppError? ValidateReminderTimes(IReadOnlyList<int>? reminderTimes)
{
if (reminderTimes is null)
return null;

if (reminderTimes.Count > DomainConstants.MaxReminderTimes)
return DomainErrors.MaxReminderTimes.Format(DomainConstants.MaxReminderTimes);

return null;
}

public static AppError? ValidateEmoji(string? emoji)
{
if (emoji is null)
Expand Down
14 changes: 12 additions & 2 deletions src/Orbit.Domain/Entities/PendingClarification.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,18 @@
public string QuickActionsJson { get; private set; } = "[]";
public DateTime CreatedAtUtc { get; private set; }
public DateTime ExpiresAtUtc { get; private set; }
// EF Core unmaps read-only auto-properties (would DropColumn); keep the private setter so this column stays mapped. https://github.com/thomasluizon/orbit-api/pull/389
public DateTime? ResolvedAtUtc { get; private set; }

#pragma warning disable CS0649 // Written only at the SQL layer via ExecuteUpdate and read back via reflection on materialization; there is no C# writer, which is what removes the S1144 unused-private-setter finding. https://github.com/thomasluizon/orbit-api/pull/390
private DateTime? _resolvedAtUtc;

Check warning on line 23 in src/Orbit.Domain/Entities/PendingClarification.cs

View workflow job for this annotation

GitHub Actions / SonarCloud Analysis

Remove unassigned field '_resolvedAtUtc', or set its value.

Check warning on line 23 in src/Orbit.Domain/Entities/PendingClarification.cs

View workflow job for this annotation

GitHub Actions / SonarCloud Analysis

Remove unassigned field '_resolvedAtUtc', or set its value.

Check warning on line 23 in src/Orbit.Domain/Entities/PendingClarification.cs

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Remove unassigned field '_resolvedAtUtc', or set its value.

See more on https://sonarcloud.io/project/issues?id=thomasluizon_orbit-api&issues=AZ9eXA9uPHxFEQyEwZNJ&open=AZ9eXA9uPHxFEQyEwZNJ&pullRequest=396
#pragma warning restore CS0649

/// <summary>
/// UTC instant this clarification was resolved; null while still open. Exposed as a read-only
/// property over an explicitly-mapped backing field (see ConfigurePendingClarificationEntity) so
/// the column stays mapped without a private setter -- read-only auto-properties get dropped by
/// EF convention. The one-shot resolve flips it atomically via ExecuteUpdate. https://github.com/thomasluizon/orbit-api/pull/390
/// </summary>
public DateTime? ResolvedAtUtc => _resolvedAtUtc;

private PendingClarification() { }

Expand Down
14 changes: 12 additions & 2 deletions src/Orbit.Domain/Entities/Report.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,18 @@
public Guid? CheerId { get; private set; }
public ReportStatus Status { get; private set; }
public DateTime CreatedAtUtc { get; private set; }
// EF Core unmaps read-only auto-properties (would DropColumn); keep the private setter so this column stays mapped. https://github.com/thomasluizon/orbit-api/pull/389
public DateTime? ReviewedAtUtc { get; private set; }

#pragma warning disable CS0649 // EF writes this backing field via reflection on materialization; there is no C# writer, which is what removes the S1144 unused-private-setter finding. https://github.com/thomasluizon/orbit-api/pull/390
private DateTime? _reviewedAtUtc;

Check warning on line 17 in src/Orbit.Domain/Entities/Report.cs

View workflow job for this annotation

GitHub Actions / SonarCloud Analysis

Remove unassigned field '_reviewedAtUtc', or set its value.

Check warning on line 17 in src/Orbit.Domain/Entities/Report.cs

View workflow job for this annotation

GitHub Actions / SonarCloud Analysis

Remove unassigned field '_reviewedAtUtc', or set its value.

Check warning on line 17 in src/Orbit.Domain/Entities/Report.cs

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Remove unassigned field '_reviewedAtUtc', or set its value.

See more on https://sonarcloud.io/project/issues?id=thomasluizon_orbit-api&issues=AZ9eXA4OPHxFEQyEwZNI&open=AZ9eXA4OPHxFEQyEwZNI&pullRequest=396
#pragma warning restore CS0649

/// <summary>
/// UTC instant an admin reviewed this report; null until reviewed. Exposed as a read-only
/// property over an explicitly-mapped backing field (see ConfigureReportEntity) so the column
/// stays mapped without a private setter -- read-only auto-properties get dropped by EF
/// convention. https://github.com/thomasluizon/orbit-api/pull/390
/// </summary>
public DateTime? ReviewedAtUtc => _reviewedAtUtc;

private Report() { }

Expand Down
2 changes: 2 additions & 0 deletions src/Orbit.Infrastructure/Persistence/OrbitDbContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -402,6 +402,7 @@ private static void ConfigurePendingClarificationEntity(ModelBuilder modelBuilde
entity.Property(item => item.Question).HasMaxLength(500);
entity.Property(item => item.PartialArgumentsJson).HasColumnType(JsonbColumnType);
entity.Property(item => item.QuickActionsJson).HasColumnType(JsonbColumnType);
entity.Property(item => item.ResolvedAtUtc).HasField("_resolvedAtUtc");
entity.HasOne<User>().WithMany().HasForeignKey(item => item.UserId).OnDelete(DeleteBehavior.Cascade);
});
}
Expand Down Expand Up @@ -565,6 +566,7 @@ private static void ConfigureReportEntity(ModelBuilder modelBuilder)
entity.Property(r => r.Reason).HasConversion<string>().HasMaxLength(32);
entity.Property(r => r.Status).HasConversion<string>().HasMaxLength(32);
entity.Property(r => r.Details).HasMaxLength(DomainConstants.MaxReportDetailsLength);
entity.Property(r => r.ReviewedAtUtc).HasField("_reviewedAtUtc");
entity.HasOne<User>().WithMany().HasForeignKey(r => r.ReporterId).OnDelete(DeleteBehavior.Restrict);
entity.HasOne<User>().WithMany().HasForeignKey(r => r.ReportedUserId).OnDelete(DeleteBehavior.Restrict);
entity.HasOne<Cheer>().WithMany().HasForeignKey(r => r.CheerId).OnDelete(DeleteBehavior.SetNull);
Expand Down
51 changes: 51 additions & 0 deletions tests/Orbit.Domain.Tests/Entities/HabitTests.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using FluentAssertions;
using Orbit.Domain.Common;
using Orbit.Domain.Entities;
using Orbit.Domain.Enums;
using Orbit.Domain.ValueObjects;
Expand Down Expand Up @@ -921,6 +922,56 @@ public void Update_ScheduledReminders_Null_KeepsExisting()
habit.ScheduledReminders.Should().HaveCount(1);
}

[Fact]
public void Create_ReminderTimes_AtLimit_ReturnsSuccess()
{
var reminderTimes = Enumerable.Range(0, DomainConstants.MaxReminderTimes).ToList();

var result = Habit.Create(new HabitCreateParams(ValidUserId, "Exercise", FrequencyUnit.Day, 1, DueDate: DateOnly.FromDateTime(DateTime.UtcNow),
ReminderTimes: reminderTimes));

result.IsSuccess.Should().BeTrue();
result.Value.ReminderTimes.Should().HaveCount(DomainConstants.MaxReminderTimes);
}

[Fact]
public void Create_ReminderTimes_OverLimit_ReturnsFailure()
{
var reminderTimes = Enumerable.Range(0, DomainConstants.MaxReminderTimes + 1).ToList();

var result = Habit.Create(new HabitCreateParams(ValidUserId, "Exercise", FrequencyUnit.Day, 1, DueDate: DateOnly.FromDateTime(DateTime.UtcNow),
ReminderTimes: reminderTimes));

result.IsFailure.Should().BeTrue();
result.Error.Should().Contain($"at most {DomainConstants.MaxReminderTimes} reminder times");
}

[Fact]
public void Update_ReminderTimes_AtLimit_ReturnsSuccess()
{
var habit = CreateValidHabit();
var reminderTimes = Enumerable.Range(0, DomainConstants.MaxReminderTimes).ToList();

var result = habit.Update(new HabitUpdateParams("Exercise", null, FrequencyUnit.Day, 1, null, false, null,
ReminderTimes: reminderTimes));

result.IsSuccess.Should().BeTrue();
habit.ReminderTimes.Should().HaveCount(DomainConstants.MaxReminderTimes);
}

[Fact]
public void Update_ReminderTimes_OverLimit_ReturnsFailure()
{
var habit = CreateValidHabit();
var reminderTimes = Enumerable.Range(0, DomainConstants.MaxReminderTimes + 1).ToList();

var result = habit.Update(new HabitUpdateParams("Exercise", null, FrequencyUnit.Day, 1, null, false, null,
ReminderTimes: reminderTimes));

result.IsFailure.Should().BeTrue();
result.Error.Should().Contain($"at most {DomainConstants.MaxReminderTimes} reminder times");
}

[Fact]
public void Update_AllOptionalFields_Applied()
{
Expand Down
Loading