Skip to content

Commit f9fe9cc

Browse files
committed
feat: Introduce partial dates, localized author biographies and book descriptions, pre-release book status, and enhanced book search.
1 parent c21ba70 commit f9fe9cc

21 files changed

Lines changed: 929 additions & 112 deletions

File tree

Lines changed: 189 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,189 @@
1+
using BookStore.ApiService.Helpers;
2+
using BookStore.ApiService.Models;
3+
4+
namespace BookStore.ApiService.Tests.Helpers;
5+
6+
public class BookHelpersTests
7+
{
8+
[Fact]
9+
public void IsPreRelease_WithNullPublicationDate_ReturnsFalse()
10+
{
11+
// Arrange
12+
PartialDate? publicationDate = null;
13+
14+
// Act
15+
var result = BookHelpers.IsPreRelease(publicationDate);
16+
17+
// Assert
18+
Assert.False(result);
19+
}
20+
21+
[Fact]
22+
public void IsPreRelease_WithPastCompleteDate_ReturnsFalse()
23+
{
24+
// Arrange
25+
var publicationDate = new PartialDate(2020, 1, 15);
26+
27+
// Act
28+
var result = BookHelpers.IsPreRelease(publicationDate);
29+
30+
// Assert
31+
Assert.False(result);
32+
}
33+
34+
[Fact]
35+
public void IsPreRelease_WithFutureCompleteDate_ReturnsTrue()
36+
{
37+
// Arrange
38+
var futureYear = DateTimeOffset.UtcNow.Year + 2;
39+
var publicationDate = new PartialDate(futureYear, 6, 15);
40+
41+
// Act
42+
var result = BookHelpers.IsPreRelease(publicationDate);
43+
44+
// Assert
45+
Assert.True(result);
46+
}
47+
48+
[Fact]
49+
public void IsPreRelease_WithPastYearOnly_ReturnsFalse()
50+
{
51+
// Arrange
52+
var publicationDate = new PartialDate(2020);
53+
54+
// Act
55+
var result = BookHelpers.IsPreRelease(publicationDate);
56+
57+
// Assert
58+
Assert.False(result);
59+
}
60+
61+
[Fact]
62+
public void IsPreRelease_WithFutureYearOnly_ReturnsTrue()
63+
{
64+
// Arrange
65+
var futureYear = DateTimeOffset.UtcNow.Year + 1;
66+
var publicationDate = new PartialDate(futureYear);
67+
68+
// Act
69+
var result = BookHelpers.IsPreRelease(publicationDate);
70+
71+
// Assert
72+
Assert.True(result);
73+
}
74+
75+
[Fact]
76+
public void IsPreRelease_WithPastYearMonth_ReturnsFalse()
77+
{
78+
// Arrange
79+
var publicationDate = new PartialDate(2020, 6);
80+
81+
// Act
82+
var result = BookHelpers.IsPreRelease(publicationDate);
83+
84+
// Assert
85+
Assert.False(result);
86+
}
87+
88+
[Fact]
89+
public void IsPreRelease_WithFutureYearMonth_ReturnsTrue()
90+
{
91+
// Arrange
92+
var futureYear = DateTimeOffset.UtcNow.Year + 1;
93+
var publicationDate = new PartialDate(futureYear, 3);
94+
95+
// Act
96+
var result = BookHelpers.IsPreRelease(publicationDate);
97+
98+
// Assert
99+
Assert.True(result);
100+
}
101+
102+
[Fact]
103+
public void IsPreRelease_WithCurrentYearButFutureMonth_ReturnsTrue()
104+
{
105+
// Arrange
106+
var currentYear = DateTimeOffset.UtcNow.Year;
107+
var currentMonth = DateTimeOffset.UtcNow.Month;
108+
109+
// Skip test if we're in December (no future month available)
110+
if (currentMonth == 12)
111+
{
112+
return;
113+
}
114+
115+
var futureMonth = currentMonth + 1;
116+
var publicationDate = new PartialDate(currentYear, futureMonth);
117+
118+
// Act
119+
var result = BookHelpers.IsPreRelease(publicationDate);
120+
121+
// Assert
122+
Assert.True(result);
123+
}
124+
125+
[Fact]
126+
public void IsPreRelease_WithCurrentYearButPastMonth_ReturnsFalse()
127+
{
128+
// Arrange
129+
var currentYear = DateTimeOffset.UtcNow.Year;
130+
var currentMonth = DateTimeOffset.UtcNow.Month;
131+
132+
// Skip test if we're in January (no past month available)
133+
if (currentMonth == 1)
134+
{
135+
return;
136+
}
137+
138+
var pastMonth = currentMonth - 1;
139+
var publicationDate = new PartialDate(currentYear, pastMonth);
140+
141+
// Act
142+
var result = BookHelpers.IsPreRelease(publicationDate);
143+
144+
// Assert
145+
Assert.False(result);
146+
}
147+
148+
[Fact]
149+
public void IsPreRelease_WithTodayDate_ReturnsFalse()
150+
{
151+
// Arrange
152+
var today = DateOnly.FromDateTime(DateTimeOffset.UtcNow.DateTime);
153+
var publicationDate = new PartialDate(today.Year, today.Month, today.Day);
154+
155+
// Act
156+
var result = BookHelpers.IsPreRelease(publicationDate);
157+
158+
// Assert
159+
Assert.False(result);
160+
}
161+
162+
[Fact]
163+
public void IsPreRelease_WithTomorrowDate_ReturnsTrue()
164+
{
165+
// Arrange
166+
var tomorrow = DateOnly.FromDateTime(DateTimeOffset.UtcNow.DateTime).AddDays(1);
167+
var publicationDate = new PartialDate(tomorrow.Year, tomorrow.Month, tomorrow.Day);
168+
169+
// Act
170+
var result = BookHelpers.IsPreRelease(publicationDate);
171+
172+
// Assert
173+
Assert.True(result);
174+
}
175+
176+
[Fact]
177+
public void IsPreRelease_WithYesterdayDate_ReturnsFalse()
178+
{
179+
// Arrange
180+
var yesterday = DateOnly.FromDateTime(DateTimeOffset.UtcNow.DateTime).AddDays(-1);
181+
var publicationDate = new PartialDate(yesterday.Year, yesterday.Month, yesterday.Day);
182+
183+
// Act
184+
var result = BookHelpers.IsPreRelease(publicationDate);
185+
186+
// Assert
187+
Assert.False(result);
188+
}
189+
}

src/ApiService/BookStore.ApiService/Aggregates/AuthorAggregate.cs

Lines changed: 32 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,43 +1,44 @@
11
using BookStore.ApiService.Events;
2+
using BookStore.ApiService.Infrastructure;
23

34
namespace BookStore.ApiService.Aggregates;
45

56
public class AuthorAggregate
67
{
78
public Guid Id { get; private set; }
89
public string Name { get; private set; } = string.Empty;
9-
public string? Biography { get; private set; }
10+
public Dictionary<string, AuthorTranslation> Translations { get; private set; } = [];
1011
public bool IsDeleted { get; private set; }
1112

1213
// Marten uses this for rehydration
1314
void Apply(AuthorAdded @event)
1415
{
1516
Id = @event.Id;
1617
Name = @event.Name;
17-
Biography = @event.Biography;
18+
Translations = @event.Translations ?? [];
1819
IsDeleted = false;
1920
}
2021

2122
void Apply(AuthorUpdated @event)
2223
{
2324
Name = @event.Name;
24-
Biography = @event.Biography;
25+
Translations = @event.Translations ?? [];
2526
}
2627

2728
void Apply(AuthorSoftDeleted _) => IsDeleted = true;
2829

2930
void Apply(AuthorRestored _) => IsDeleted = false;
3031

3132
// Command methods
32-
public static AuthorAdded Create(Guid id, string name, string? biography)
33+
public static AuthorAdded Create(Guid id, string name, Dictionary<string, AuthorTranslation>? translations)
3334
{
3435
ValidateName(name);
35-
ValidateBiography(biography);
36+
ValidateTranslations(translations);
3637

37-
return new AuthorAdded(id, name, biography, DateTimeOffset.UtcNow);
38+
return new AuthorAdded(id, name, translations, DateTimeOffset.UtcNow);
3839
}
3940

40-
public AuthorUpdated Update(string name, string? biography)
41+
public AuthorUpdated Update(string name, Dictionary<string, AuthorTranslation>? translations)
4142
{
4243
// Business rule: cannot update deleted author
4344
if (IsDeleted)
@@ -46,9 +47,9 @@ public AuthorUpdated Update(string name, string? biography)
4647
}
4748

4849
ValidateName(name);
49-
ValidateBiography(biography);
50+
ValidateTranslations(translations);
5051

51-
return new AuthorUpdated(Id, name, biography, DateTimeOffset.UtcNow);
52+
return new AuthorUpdated(Id, name, translations, DateTimeOffset.UtcNow);
5253
}
5354

5455
// Validation helper methods
@@ -65,11 +66,30 @@ static void ValidateName(string name)
6566
}
6667
}
6768

68-
static void ValidateBiography(string? biography)
69+
static void ValidateTranslations(Dictionary<string, AuthorTranslation>? translations)
6970
{
70-
if (biography != null && biography.Length > 5000)
71+
if (translations == null || translations.Count == 0)
7172
{
72-
throw new ArgumentException("Biography cannot exceed 5000 characters", nameof(biography));
73+
return; // Translations are optional
74+
}
75+
76+
// Validate language codes
77+
if (!CultureValidator.ValidateTranslations(translations, out var invalidCodes))
78+
{
79+
throw new ArgumentException(
80+
$"Invalid language codes in biographies: {string.Join(", ", invalidCodes)}",
81+
nameof(translations));
82+
}
83+
84+
// Validate biography length for each translation
85+
foreach (var (languageCode, translation) in translations)
86+
{
87+
if (translation.Biography.Length > 5000)
88+
{
89+
throw new ArgumentException(
90+
$"Biography for language '{languageCode}' cannot exceed 5000 characters",
91+
nameof(translations));
92+
}
7393
}
7494
}
7595

0 commit comments

Comments
 (0)