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
Original file line number Diff line number Diff line change
@@ -1,16 +1,11 @@
using Library.ApplicationCore;
using Library.ApplicationCore.Entities;
using Library.ApplicationCore.Enums;
using Library.Console;

public class ConsoleApp

{
ConsoleState _currentState = ConsoleState.PatronSearch;

List<Patron> matchingPatrons = new List<Patron>();

Patron? selectedPatronDetails = null;
Loan selectedLoanDetails = null!;
Loan? selectedLoanDetails = null;

IPatronRepository _patronRepository;
ILoanRepository _loanRepository;
Expand All @@ -27,23 +22,17 @@ public ConsoleApp(ILoanService loanService, IPatronService patronService, IPatro

public async Task Run()
{
while (true)
while (_currentState != ConsoleState.Quit)
{
switch (_currentState)
_currentState = _currentState switch
{
case ConsoleState.PatronSearch:
_currentState = await PatronSearch();
break;
case ConsoleState.PatronSearchResults:
_currentState = await PatronSearchResults();
break;
case ConsoleState.PatronDetails:
_currentState = await PatronDetails();
break;
case ConsoleState.LoanDetails:
_currentState = await LoanDetails();
break;
}
ConsoleState.PatronSearch => await PatronSearch(),
ConsoleState.PatronSearchResults => await PatronSearchResults(),
ConsoleState.PatronDetails => await PatronDetails(),
ConsoleState.LoanDetails => await LoanDetails(),
ConsoleState.Quit => ConsoleState.Quit,
_ => throw new InvalidOperationException($"Unexpected state: {_currentState}")
};
}
}

Expand All @@ -53,7 +42,6 @@ async Task<ConsoleState> PatronSearch()

matchingPatrons = await _patronRepository.SearchPatrons(searchInput);

// Guard-style clauses for edge cases
if (matchingPatrons.Count > 20)
{
Console.WriteLine("More than 20 patrons satisfy the search, please provide more specific input...");
Expand Down Expand Up @@ -96,12 +84,20 @@ async Task<ConsoleState> PatronSearchResults()
{
CommonActions options = CommonActions.Select | CommonActions.SearchPatrons | CommonActions.Quit;
CommonActions action = ReadInputOptions(options, out int selectedPatronNumber);

if (action == CommonActions.Select)
{
if (selectedPatronNumber >= 1 && selectedPatronNumber <= matchingPatrons.Count)
{
var selectedPatron = matchingPatrons.ElementAt(selectedPatronNumber - 1);
selectedPatronDetails = await _patronRepository.GetPatron(selectedPatron.Id)!;
selectedPatronDetails = await _patronRepository.GetPatron(selectedPatron.Id);

if (selectedPatronDetails is null)
{
Console.WriteLine("The selected patron could not be loaded. Please try again.");
return ConsoleState.PatronSearchResults;
}

return ConsoleState.PatronDetails;
}
else
Expand Down Expand Up @@ -148,6 +144,7 @@ _ when int.TryParse(userInput, out optionNumber) => CommonActions.Select,
Console.WriteLine("Invalid input. Please try again.");
}
} while (action == CommonActions.Repeat);

return action;
}

Expand Down Expand Up @@ -182,6 +179,12 @@ static void WriteInputOptions(CommonActions options)

async Task<ConsoleState> PatronDetails()
{
if (selectedPatronDetails is null)
{
Console.WriteLine("No patron details are available.");
return ConsoleState.PatronSearch;
}

Console.WriteLine($"Name: {selectedPatronDetails.Name}");
Console.WriteLine($"Membership Expiration: {selectedPatronDetails.MembershipEnd}");
Console.WriteLine();
Expand All @@ -195,12 +198,20 @@ async Task<ConsoleState> PatronDetails()

CommonActions options = CommonActions.SearchPatrons | CommonActions.Quit | CommonActions.Select | CommonActions.RenewPatronMembership;
CommonActions action = ReadInputOptions(options, out int selectedLoanNumber);

if (action == CommonActions.Select)
{
if (selectedLoanNumber >= 1 && selectedLoanNumber <= selectedPatronDetails.Loans.Count())
{
var selectedLoan = selectedPatronDetails.Loans.ElementAt(selectedLoanNumber - 1);
selectedLoanDetails = selectedPatronDetails.Loans.Where(l => l.Id == selectedLoan.Id).Single();
selectedLoanDetails = selectedPatronDetails.Loans.SingleOrDefault(l => l.Id == selectedLoan.Id);

if (selectedLoanDetails is null)
{
Console.WriteLine("The selected loan could not be found.");
return ConsoleState.PatronDetails;
}

return ConsoleState.LoanDetails;
}
else
Expand All @@ -221,8 +232,14 @@ async Task<ConsoleState> PatronDetails()
{
var status = await _patronService.RenewMembership(selectedPatronDetails.Id);
Console.WriteLine(EnumHelper.GetDescription(status));
// reloading after renewing membership
selectedPatronDetails = (await _patronRepository.GetPatron(selectedPatronDetails.Id))!;

selectedPatronDetails = await _patronRepository.GetPatron(selectedPatronDetails.Id);
if (selectedPatronDetails is null)
{
Console.WriteLine("Failed to reload patron details after renewal.");
return ConsoleState.PatronSearch;
}

return ConsoleState.PatronDetails;
}

Expand All @@ -231,6 +248,18 @@ async Task<ConsoleState> PatronDetails()

async Task<ConsoleState> LoanDetails()
{
if (selectedPatronDetails is null)
{
Console.WriteLine("No patron details are available.");
return ConsoleState.PatronSearch;
}

if (selectedLoanDetails is null)
{
Console.WriteLine("No loan details are available.");
return ConsoleState.PatronDetails;
}

Console.WriteLine($"Book title: {selectedLoanDetails.BookItem!.Book!.Title}");
Console.WriteLine($"Book Author: {selectedLoanDetails.BookItem!.Book!.Author!.Name}");
Console.WriteLine($"Due date: {selectedLoanDetails.DueDate}");
Expand All @@ -245,19 +274,34 @@ async Task<ConsoleState> LoanDetails()
var status = await _loanService.ExtendLoan(selectedLoanDetails.Id);
Console.WriteLine(EnumHelper.GetDescription(status));

// reload loan after extending
selectedPatronDetails = (await _patronRepository.GetPatron(selectedPatronDetails.Id))!;
selectedLoanDetails = (await _loanRepository.GetLoan(selectedLoanDetails.Id))!;
selectedPatronDetails = await _patronRepository.GetPatron(selectedPatronDetails.Id);
if (selectedPatronDetails is null)
{
Console.WriteLine("Failed to reload patron details after extending the loan.");
return ConsoleState.PatronSearch;
}

selectedLoanDetails = await _loanRepository.GetLoan(selectedLoanDetails.Id);
if (selectedLoanDetails is null)
{
Console.WriteLine("Failed to reload loan details after extending the loan.");
return ConsoleState.PatronDetails;
}

return ConsoleState.LoanDetails;
}
else if (action == CommonActions.ReturnLoanedBook)
{
var status = await _loanService.ReturnLoan(selectedLoanDetails.Id);

Console.WriteLine(EnumHelper.GetDescription(status));
_currentState = ConsoleState.LoanDetails;
// reload loan after returning

selectedLoanDetails = await _loanRepository.GetLoan(selectedLoanDetails.Id);
if (selectedLoanDetails is null)
{
Console.WriteLine("Failed to reload loan details after return.");
return ConsoleState.PatronDetails;
}

return ConsoleState.LoanDetails;
}
else if (action == CommonActions.Quit)
Expand All @@ -271,4 +315,4 @@ 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 @@ -4,11 +4,18 @@

public class LoanService : ILoanService
{
private ILoanRepository _loanRepository;
private readonly ILoanRepository _loanRepository;
private readonly Func<DateTime> _nowProvider;

public LoanService(ILoanRepository loanRepository)
: this(loanRepository, () => DateTime.Now)
{
}

public LoanService(ILoanRepository loanRepository, Func<DateTime> nowProvider)
{
_loanRepository = loanRepository;
_nowProvider = nowProvider;
}

public async Task<LoanReturnStatus> ReturnLoan(int loanId)
Expand Down Expand Up @@ -46,14 +53,16 @@ public async Task<LoanExtensionStatus> ExtendLoan(int loanId)
if (loan == null)
return LoanExtensionStatus.LoanNotFound;

var now = _nowProvider();

// Check if patron's membership is expired
if (loan.Patron!.MembershipEnd < DateTime.Now)
if (loan.Patron!.MembershipEnd <= now)
return LoanExtensionStatus.MembershipExpired;

if (loan.ReturnDate != null)
return LoanExtensionStatus.LoanReturned;

if (loan.DueDate < DateTime.Now)
if (loan.DueDate <= now)
return LoanExtensionStatus.LoanExpired;

loan.DueDate = loan.DueDate.AddDays(ExtendByDays);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -101,4 +101,70 @@ public async Task ExtendLoan_ReturnsLoanExpired()
Assert.Equal(LoanExtensionStatus.LoanExpired, extensionStatus);
Assert.Equal(loanDueDate, loan.DueDate);
}
}
[Fact(DisplayName = "LoanService.ExtendLoan: Returns LoanExpired when due date is exactly the current time")]
public async Task ExtendLoan_ReturnsLoanExpiredWhenDueDateIsExactlyNow()
{
// Arrange
var now = new DateTime(2026, 1, 1, 12, 0, 0);
var patron = PatronFactory.CreateCurrentPatron();
var loan = LoanFactory.CreateCurrentLoanForPatron(patron);
var loanId = loan.Id;
loan.DueDate = now;

var serviceWithFixedClock = new LoanService(_mockLoanRepository, () => now);
_mockLoanRepository.GetLoan(loanId).Returns(loan);

// Act
LoanExtensionStatus extensionStatus = await serviceWithFixedClock.ExtendLoan(loanId);

// Assert
Assert.Equal(LoanExtensionStatus.LoanExpired, extensionStatus);
Assert.Equal(now, loan.DueDate);
}

[Fact(DisplayName = "LoanService.ExtendLoan: Returns MembershipExpired when membership ends exactly at the current time")]
public async Task ExtendLoan_ReturnsMembershipExpiredWhenMembershipEndsExactlyNow()
{
// Arrange
var now = new DateTime(2026, 1, 1, 12, 0, 0);
var patron = PatronFactory.CreateCurrentPatron();
patron.MembershipEnd = now;
var loan = LoanFactory.CreateCurrentLoanForPatron(patron);
var loanId = loan.Id;
var loanDueDate = loan.DueDate;

var serviceWithFixedClock = new LoanService(_mockLoanRepository, () => now);
_mockLoanRepository.GetLoan(loanId).Returns(loan);

// Act
LoanExtensionStatus extensionStatus = await serviceWithFixedClock.ExtendLoan(loanId);

// Assert
Assert.Equal(LoanExtensionStatus.MembershipExpired, extensionStatus);
Assert.Equal(loanDueDate, loan.DueDate);
}

[Fact(DisplayName = "LoanService.ExtendLoan: Returns Error when UpdateLoan throws exception")]
public async Task ExtendLoan_ReturnsErrorWhenUpdateLoanThrowsException()
{
// Arrange
var patron = PatronFactory.CreateCurrentPatron();
var loan = LoanFactory.CreateCurrentLoanForPatron(patron);
var loanId = loan.Id;
var loanDueDate = loan.DueDate;

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

_mockLoanRepository
.When(x => x.UpdateLoan(loan))
.Do(_ => throw new Exception("Update failed"));

// Act
LoanExtensionStatus extensionStatus = await _loanService.ExtendLoan(loanId);

// Assert
Assert.Equal(LoanExtensionStatus.Error, extensionStatus);
Assert.Equal(loanDueDate.AddDays(LoanService.ExtendByDays), loan.DueDate);
await _mockLoanRepository.Received(1).UpdateLoan(loan);
}
}
Loading