Skip to content
Open
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
331 changes: 331 additions & 0 deletions .github/copilot-instructions.md

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,6 @@ public enum CommonActions
SearchPatrons = 4,
RenewPatronMembership = 8,
ReturnLoanedBook = 16,
ExtendLoanedBook = 32
ExtendLoanedBook = 32,
SearchBooks = 64
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
using Library.ApplicationCore.Entities;
using Library.ApplicationCore.Enums;
using Library.Console;
using Library.Infrastructure.Data;

public class ConsoleApp
{
Expand All @@ -16,13 +17,15 @@ public class ConsoleApp
ILoanRepository _loanRepository;
ILoanService _loanService;
IPatronService _patronService;
JsonData _jsonData;

public ConsoleApp(ILoanService loanService, IPatronService patronService, IPatronRepository patronRepository, ILoanRepository loanRepository)
public ConsoleApp(ILoanService loanService, IPatronService patronService, IPatronRepository patronRepository, ILoanRepository loanRepository, JsonData jsonData)
{
_patronRepository = patronRepository;
_loanRepository = loanRepository;
_loanService = loanService;
_patronService = patronService;
_jsonData = jsonData;
}

public async Task Run()
Expand All @@ -43,6 +46,15 @@ public async Task Run()
case ConsoleState.LoanDetails:
_currentState = await LoanDetails();
break;
case ConsoleState.SearchBooks:
_currentState = await SearchBooks();
break;
case ConsoleState.Quit:
Console.WriteLine("Goodbye!");
return;
default:
Console.WriteLine("Unknown application state.");
return;
}
}
}
Expand Down Expand Up @@ -92,6 +104,8 @@ static void PrintPatronsList(List<Patron> matchingPatrons)
}
}



async Task<ConsoleState> PatronSearchResults()
{
CommonActions options = CommonActions.Select | CommonActions.SearchPatrons | CommonActions.Quit;
Expand Down Expand Up @@ -130,15 +144,18 @@ static CommonActions ReadInputOptions(CommonActions options, out int optionNumbe
{
Console.WriteLine();
WriteInputOptions(options);
string? userInput = Console.ReadLine();
string? userInput = Console.ReadLine()?.Trim().ToLowerInvariant();

action = userInput switch
{
"q" when options.HasFlag(CommonActions.Quit) => CommonActions.Quit,
"quit" when options.HasFlag(CommonActions.Quit) => CommonActions.Quit,
"s" when options.HasFlag(CommonActions.SearchPatrons) => CommonActions.SearchPatrons,
"search" when options.HasFlag(CommonActions.SearchPatrons) => CommonActions.SearchPatrons,
"m" when options.HasFlag(CommonActions.RenewPatronMembership) => CommonActions.RenewPatronMembership,
"e" when options.HasFlag(CommonActions.ExtendLoanedBook) => CommonActions.ExtendLoanedBook,
"r" when options.HasFlag(CommonActions.ReturnLoanedBook) => CommonActions.ReturnLoanedBook,
"b" when options.HasFlag(CommonActions.SearchBooks) => CommonActions.SearchBooks,
_ when int.TryParse(userInput, out optionNumber) => CommonActions.Select,
_ => CommonActions.Repeat
};
Expand Down Expand Up @@ -178,6 +195,11 @@ static void WriteInputOptions(CommonActions options)
{
Console.WriteLine("Or type a number to select a list item.");
}
if (options.HasFlag(CommonActions.SearchBooks))
{
Console.WriteLine(" - \"b\" to search for books");
}

}

async Task<ConsoleState> PatronDetails()
Expand All @@ -193,7 +215,7 @@ async Task<ConsoleState> PatronDetails()
loanNumber++;
}

CommonActions options = CommonActions.SearchPatrons | CommonActions.Quit | CommonActions.Select | CommonActions.RenewPatronMembership;
CommonActions options = CommonActions.SearchPatrons | CommonActions.Quit | CommonActions.Select | CommonActions.RenewPatronMembership | CommonActions.SearchBooks;
CommonActions action = ReadInputOptions(options, out int selectedLoanNumber);
if (action == CommonActions.Select)
{
Expand Down Expand Up @@ -225,10 +247,67 @@ async Task<ConsoleState> PatronDetails()
selectedPatronDetails = (await _patronRepository.GetPatron(selectedPatronDetails.Id))!;
return ConsoleState.PatronDetails;
}
else if (action == CommonActions.SearchBooks)
{
return await SearchBooks();
}

throw new InvalidOperationException("An input option is not handled.");
}

//new method BM@ACC
async Task<ConsoleState> SearchBooks()
{
string? bookTitle = null;
while (string.IsNullOrWhiteSpace(bookTitle))
{
Console.Write("Enter a book title to search for: ");
bookTitle = Console.ReadLine();
}

await _jsonData.EnsureDataLoaded();

var book = _jsonData.Books!.FirstOrDefault(b => string.Equals(b.Title, bookTitle, StringComparison.OrdinalIgnoreCase));
if (book == null)
{
Console.WriteLine($"No book found with the title \"{bookTitle}\".");
return ConsoleState.PatronDetails;
}

var bookItem = _jsonData.BookItems!.FirstOrDefault(bi => bi.BookId == book.Id);
if (bookItem == null)
{
Console.WriteLine($"No book item found for the title \"{book.Title}\".");
return ConsoleState.PatronDetails;
}

var loan = _jsonData.Loans!.FirstOrDefault(l => l.BookItemId == bookItem.Id && l.ReturnDate == null);
if (loan == null)
{
Console.WriteLine($"\"{book.Title}\" is available for loan.");
}
else
{
Console.WriteLine($"\"{book.Title}\" is on loan to another patron. The return due date is {loan.DueDate}.");
}

return ConsoleState.PatronDetails;
}


//new method BM@ACC
static string ReadBookTitle()
{
string? title = null;
while (string.IsNullOrWhiteSpace(title))
{
Console.Write("Enter a book title to search: ");
title = Console.ReadLine();
}

return title;
}

async Task<ConsoleState> LoanDetails()
{
Console.WriteLine($"Book title: {selectedLoanDetails.BookItem!.Book!.Title}");
Expand Down Expand Up @@ -271,4 +350,6 @@ async Task<ConsoleState> LoanDetails()

throw new InvalidOperationException("An input option is not handled.");
}


}
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,6 @@ public enum ConsoleState
PatronSearchResults,
PatronDetails,
LoanDetails,
Quit
Quit,
SearchBooks
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
using System.ComponentModel;
using Library.ApplicationCore.Enums;

namespace Library.UnitTests.ApplicationCore.Enums;

public class EnumHelperTests
{
[Fact]
public void GetDescription_ReturnsDescription_WhenAttributeExists()
{
// Arrange
var value = SampleStatus.Active;

// Act
var description = EnumHelper.GetDescription(value);

// Assert
Assert.Equal("Active state", description);
}

[Fact]
public void GetDescription_ReturnsEnumName_WhenAttributeIsMissing()
{
// Arrange
var value = SampleStatus.Inactive;

// Act
var description = EnumHelper.GetDescription(value);

// Assert
Assert.Equal(nameof(SampleStatus.Inactive), description);
}

private enum SampleStatus
{
[Description("Active state")]
Active,

Inactive
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
using Library.ApplicationCore;
using Library.ApplicationCore.Entities;
using Library.Infrastructure.Data;
using Microsoft.Extensions.Configuration;
using NSubstitute;

namespace UnitTests.Infrastructure.JsonLoanRepositoryTests;

public class GetLoanTest
{
private readonly ILoanRepository _mockLoanRepository;
private readonly JsonLoanRepository _jsonLoanRepository;
private readonly IConfiguration _configuration;
private readonly JsonData _jsonData;

public GetLoanTest()
{
_mockLoanRepository = Substitute.For<ILoanRepository>();

var jsonDirectory = Path.Combine(AppContext.BaseDirectory, "Json");
if (!Directory.Exists(jsonDirectory))
{
var projectRoot = Path.GetFullPath(Path.Combine(AppContext.BaseDirectory, "..", "..", "..", "..", ".."));
jsonDirectory = Path.Combine(projectRoot, "src", "Library.Console", "Json");
}

_configuration = new ConfigurationBuilder()
.AddInMemoryCollection(new Dictionary<string, string?>
{
["JsonPaths:Authors"] = Path.Combine(jsonDirectory, "Authors.json"),
["JsonPaths:Books"] = Path.Combine(jsonDirectory, "Books.json"),
["JsonPaths:BookItems"] = Path.Combine(jsonDirectory, "BookItems.json"),
["JsonPaths:Patrons"] = Path.Combine(jsonDirectory, "Patrons.json"),
["JsonPaths:Loans"] = Path.Combine(jsonDirectory, "Loans.json")
})
.Build();

_jsonData = new JsonData(_configuration);
_jsonLoanRepository = new JsonLoanRepository(_jsonData);
}

[Fact(DisplayName = "JsonLoanRepository.GetLoan: Returns loan when ID is found")]
public async Task GetLoan_ReturnsLoanWhenIdIsFound()
{
var loanId = 1;
var expectedLoan = new Loan
{
Id = loanId,
BookItemId = 17,
PatronId = 22,
LoanDate = DateTime.Parse("2023-12-08T00:40:43.1808862"),
DueDate = DateTime.Parse("2023-12-22T00:40:43.1808862"),
ReturnDate = null
};

_mockLoanRepository.GetLoan(loanId).Returns(expectedLoan);

var actualLoan = await _jsonLoanRepository.GetLoan(loanId);

Assert.NotNull(actualLoan);
Assert.Equal(expectedLoan.Id, actualLoan!.Id);
}

[Fact(DisplayName = "JsonLoanRepository.GetLoan: Returns null when ID is not found")]
public async Task GetLoan_ReturnsNullWhenIdIsNotFound()
{
var loanId = 999;
_mockLoanRepository.GetLoan(loanId).Returns((Loan?)null);

var actualLoan = await _jsonLoanRepository.GetLoan(loanId);

Assert.Null(actualLoan);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,14 @@

<ItemGroup>
<ProjectReference Include="..\..\src\Library.ApplicationCore\Library.ApplicationCore.csproj" />
<ProjectReference Include="..\..\src\Library.Infrastructure\Library.Infrastructure.csproj" />
</ItemGroup>

<ItemGroup>
<None Include="..\..\src\Library.Console\Json\**\*">
<Link>Json\%(RecursiveDir)%(FileName)%(Extension)</Link>
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>

</Project>
Original file line number Diff line number Diff line change
@@ -1,27 +1,48 @@
using System.ComponentModel;
using System.Reflection;

namespace Library.ApplicationCore.Enums;
namespace Library.ApplicationCore.Enums;

public static class EnumHelper
{
public static string GetDescription(Enum value)
{
if (value == null)
return string.Empty;

FieldInfo fieldInfo = value.GetType().GetField(value.ToString())!;
private static readonly IReadOnlyDictionary<LoanExtensionStatus, string> LoanExtensionDescriptions =
new Dictionary<LoanExtensionStatus, string>
{
[LoanExtensionStatus.Success] = "Book loan extension was successful.",
[LoanExtensionStatus.LoanNotFound] = "Loan not found.",
[LoanExtensionStatus.LoanExpired] = "Cannot extend book loan as it already has expired. Return the book instead.",
[LoanExtensionStatus.MembershipExpired] = "Cannot extend book loan due to expired patron's membership.",
[LoanExtensionStatus.LoanReturned] = "Cannot extend book loan as the book is already returned.",
[LoanExtensionStatus.Error] = "Cannot extend book loan due to an error."
};

DescriptionAttribute[] attributes =
(DescriptionAttribute[])fieldInfo.GetCustomAttributes(typeof(DescriptionAttribute), false);
private static readonly IReadOnlyDictionary<LoanReturnStatus, string> LoanReturnDescriptions =
new Dictionary<LoanReturnStatus, string>
{
[LoanReturnStatus.Success] = "Book was successfully returned.",
[LoanReturnStatus.LoanNotFound] = "Loan not found.",
[LoanReturnStatus.AlreadyReturned] = "Cannot return book as the book is already returned.",
[LoanReturnStatus.Error] = "Cannot return book due to an error."
};

if (attributes != null && attributes.Length > 0)
private static readonly IReadOnlyDictionary<MembershipRenewalStatus, string> MembershipRenewalDescriptions =
new Dictionary<MembershipRenewalStatus, string>
{
return attributes[0].Description;
}
else
[MembershipRenewalStatus.Success] = "Membership renewal was successful.",
[MembershipRenewalStatus.PatronNotFound] = "Patron not found.",
[MembershipRenewalStatus.TooEarlyToRenew] = "It is too early to renew the membership.",
[MembershipRenewalStatus.LoanNotReturned] = "Cannot renew membership due to an outstanding loan.",
[MembershipRenewalStatus.Error] = "Cannot renew membership due to an error."
};

public static string GetDescription(Enum? value)
{
if (value is null)
return string.Empty;

return value switch
{
return value.ToString();
}
LoanExtensionStatus extensionStatus when LoanExtensionDescriptions.TryGetValue(extensionStatus, out var extensionDescription) => extensionDescription,
LoanReturnStatus returnStatus when LoanReturnDescriptions.TryGetValue(returnStatus, out var returnDescription) => returnDescription,
MembershipRenewalStatus membershipStatus when MembershipRenewalDescriptions.TryGetValue(membershipStatus, out var membershipDescription) => membershipDescription,
_ => value.ToString() ?? string.Empty
};
}
}
Loading