Skip to content

.NET Challenge #7

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
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
49 changes: 49 additions & 0 deletions .NET/library/Controllers/LoanController.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
using Microsoft.AspNetCore.Mvc;
using OneBeyondApi.DataAccess;
using OneBeyondApi.Model;

namespace OneBeyondApi.Controllers
{
[ApiController]
[Route("[controller]")]
public class LoanController : ControllerBase
{
private readonly ILogger<AuthorController> _logger;
private readonly ILoanService _loanService;

public LoanController(ILogger<AuthorController> logger, ILoanService loanService)
{
_logger = logger;
_loanService = loanService;
}

[HttpGet]
[Route("OnLoan")]
public IList<OnLoanModel> Get()
{
return _loanService.GetAllLoaned();
}

[HttpPost]
[Route("ReturnLoan{bookStockGuid:required}")]
public Guid Post(Guid bookStockGuid)
{
return _loanService.ReturnBook(bookStockGuid);
}

[HttpPost]
[Route("ReserveBook")]
public Guid ReserveBook(Guid bookStockId, Guid borrowerId)
{
return _loanService.ReserveBook(bookStockId, borrowerId);
}

[HttpGet]
[Route("GetFirstAvailability")]
public DateTime GetFirstAvailbility(Guid? bookId, string? bookTitle)
{
return _loanService.GetFirstAvailableDate(bookId, bookTitle);
}

}
}
12 changes: 12 additions & 0 deletions .NET/library/DataAccess/ILoanService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
using OneBeyondApi.Model;

namespace OneBeyondApi.DataAccess
{
public interface ILoanService
{
List<OnLoanModel> GetAllLoaned();
Guid ReturnBook(Guid bookStockId);
Guid ReserveBook(Guid bookStockId, Guid borrowerId);
DateTime GetFirstAvailableDate(Guid? bookId, string bookTitle);
}
}
2 changes: 2 additions & 0 deletions .NET/library/DataAccess/LibraryContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,5 +13,7 @@ protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
public DbSet<Book> Books { get; set; }
public DbSet<BookStock> Catalogue { get; set; }
public DbSet<Borrower> Borrowers { get; set; }
public DbSet<Loan> Loans { get; set; }
public DbSet<Fine> Fines { get; set; }
}
}
211 changes: 211 additions & 0 deletions .NET/library/DataAccess/LoanService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,211 @@
using Microsoft.EntityFrameworkCore;
using OneBeyondApi.Model;

namespace OneBeyondApi.DataAccess
{
public class LoanService : ILoanService
{
int _finePerDay = 0;
int _defaultLoanDays = 0;

/// <summary>
/// Constructor
/// </summary>
/// <param name="finePerDay">Default value => 50, but can change in DI</param>
/// <param name="defaultLoanDays">Default value => 5, but can change in DI</param>
public LoanService(int finePerDay = 50, int defaultLoanDays = 5)
{
_finePerDay = finePerDay;
_defaultLoanDays = defaultLoanDays;
}

/// <summary>
/// Get all currently loaned book grouped by the Borrowers
/// </summary>
/// <returns>
/// A Grouped list of Books by the borrowers (the full details of the borrowers)
/// and the books they loaned with the stock id (to able to return it) and the title of the book
/// </returns>
public List<OnLoanModel> GetAllLoaned()
{
using var context = new LibraryContext();

var list = context.Catalogue
.Include(x => x.Book)
.Include(x => x.OnLoanTo)
.Where(x => x.OnLoanTo != null)
.GroupBy(x => x.OnLoanTo)
.Select(x => new OnLoanModel()
{
Borrower = x.Key,
Books = x.Select(b => new LoanedBook
{
BookStockId = b.Id,
Title = b.Book.Name
}).ToList()
}).ToList();

return list;
}

/// <summary>
/// Returns a borrowed book. Set the bookstock loandata to null
/// </summary>
/// <param name="bookStockId">The returned bookstock id</param>
/// <returns>
/// An empty guid if the return not succeded and the returned
/// bookstock id if the returns succseeded
/// </returns>
public Guid ReturnBook(Guid bookStockId)
{
Guid retVal = Guid.Empty;
using var context = new LibraryContext();

var book = context.Catalogue.Include(x => x.OnLoanTo).FirstOrDefault(x => x.Id == bookStockId);

if (book != null && book.OnLoanTo != null && book.LoanEndDate != null)
{
//If the book returned late => add fine
var lateDays = (DateTime.Today - book.LoanEndDate.Value).Days;
if (lateDays > 0)
{
context.Fines.Add(GetFine(book.OnLoanTo, lateDays));
}

book.LoanEndDate = null;
book.OnLoanTo = null;
if (context.SaveChanges() != 0)
{
retVal = book.Id;
}
}

return retVal;
}

/// <summary>
/// Reserve a book or loan it if available
/// </summary>
/// <param name="bookStockId"></param>
/// <returns></returns>
public Guid ReserveBook(Guid bookStockId, Guid borrowerId)
{
Guid retVal = Guid.Empty;
using var context = new LibraryContext();

var bookStock = context.Catalogue
.Include(x => x.Book).Include(x => x.OnLoanTo)
.FirstOrDefault(x => x.Id == bookStockId);

var borrower = context.Borrowers.Find(borrowerId);

if (bookStock != null && borrower != null)
{
//If the book is on Loan then reserve the first available date or else borrow now
if (bookStock.LoanEndDate.HasValue)
{
var lastReserve = context.Loans.Where(x => x.BookStock.Id == bookStockId)
.OrderByDescending(x => x.LoanEndDate).FirstOrDefault();

context.Loans.Add(AddLoanReserve(lastReserve, borrower, bookStock));
}
else
{
bookStock.LoanEndDate = DateTime.Now.AddDays(_defaultLoanDays);
bookStock.OnLoanTo = borrower;
}

context.SaveChanges();
retVal = bookStockId;
}

return retVal;
}

/// <summary>
/// Get the first loan date for a book
/// </summary>
/// <param name="bookId">The id of the needed book</param>
/// <param name="bookTitle">The title of the needed book</param>
/// <returns>Returns the first available date when a stock from a book is loanable,
/// if there is no stock for this book than returns DateTime.MaxValue
/// </returns>
public DateTime GetFirstAvailableDate(Guid? bookId, string bookTitle)
{
DateTime firstAvailableDate = DateTime.MaxValue;
using var context = new LibraryContext();
//Get all stock from a book
var bookStocks = context.Catalogue
.Include(x => x.Book)
.Where(x => bookId != null ? x.Book.Id == bookId : x.Book.Name == bookTitle)
.ToList();

//If one of the bookstock is available than the book is loanable today
if (bookStocks.Any(x => x.LoanEndDate == null))
{
firstAvailableDate = DateTime.Now;
}
else
{
//Get the first available date from the reservations grouped by bookstocks
var bookStockIds = bookStocks.Select(x => x.Id).ToList();
var firstAvailableDatesFromReservations = context.Loans.Include(x => x.BookStock)
.Where(x => bookStockIds.Contains(x.BookStock.Id))
.GroupBy(x => x.BookStock).Select(x => x.OrderByDescending(d => d.LoanEndDate).First()).ToList();

//If there is any reservation
if (firstAvailableDatesFromReservations.Count != 0)
{
//iterate through the booksStocks to find the first available date
foreach (var bookStock in bookStocks)
{
//if there is reservations for the bookStock than get the earliest availability
//otherwise get the current loan end date
var bookStockFirstAvailableDate = firstAvailableDatesFromReservations
.FirstOrDefault(x => x.BookStock.Id == bookStock.Id)?.LoanEndDate ?? bookStock.LoanEndDate.Value;

//if the earliest available reservation is earlier than the current eariliest than choose that value for the earliest date
firstAvailableDate = firstAvailableDate < bookStockFirstAvailableDate ? firstAvailableDate : bookStockFirstAvailableDate;
}
}
else
{
//The first available date from the current loans, it always has value
firstAvailableDate = bookStocks.OrderBy(x => x.LoanEndDate).First().LoanEndDate.Value;
}
}

return firstAvailableDate;
}

private Loan AddLoanReserve(Loan lastReserve, Borrower borrower, BookStock bookStock)
{
DateTime loanStart = lastReserve != null ?
lastReserve.LoanEndDate : bookStock.LoanEndDate.Value;

Loan loan = new()
{
Id = bookStock.Id,
Borrower = borrower,
BookStock = bookStock,
LoanStartDate = loanStart,
LoanEndDate = loanStart.AddDays(_defaultLoanDays)

};

return loan;
}

private Fine GetFine(Borrower borrower, int lateDays)
{
Fine fine = new()
{
Borrower = borrower,
PaidDate = DateTime.Now,
Amount = _finePerDay * lateDays
};

return fine;
}
}
}
16 changes: 16 additions & 0 deletions .NET/library/Model/Fine.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
namespace OneBeyondApi.Model
{
public class Fine
{
public Guid Id { get; set; }
public Borrower Borrower { get; set; }
/// <summary>
/// The fine for the late return
/// </summary>
public int Amount { get; set; }
/// <summary>
/// If a fine paid than this is the pay date
/// </summary>
public DateTime? PaidDate { get; set; }
}
}
18 changes: 18 additions & 0 deletions .NET/library/Model/Loan.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
namespace OneBeyondApi.Model
{
public class Loan
{
public Guid Id { get; set; }
public BookStock BookStock { get; set; }
public Borrower Borrower { get; set; }
/// <summary>
/// The planned/loaned date
/// </summary>
public DateTime LoanStartDate { get; set; }
/// <summary>
/// The planned end date for the loan
/// </summary>
public DateTime LoanEndDate { get; set; }

}
}
8 changes: 8 additions & 0 deletions .NET/library/Model/LoanedBook.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
namespace OneBeyondApi.Model
{
public class LoanedBook
{
public Guid BookStockId { get; set; }
public string Title { get; set; }
}
}
8 changes: 8 additions & 0 deletions .NET/library/Model/OnLoanModel.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
namespace OneBeyondApi.Model
{
public class OnLoanModel
{
public Borrower Borrower { get; set; }
public List<LoanedBook> Books { get; set; } = [];
}
}
1 change: 1 addition & 0 deletions .NET/library/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
builder.Services.AddScoped<IBookRepository, BookRepository>();
builder.Services.AddScoped<IBorrowerRepository, BorrowerRepository>();
builder.Services.AddScoped<ICatalogueRepository, CatalogueRepository>();
builder.Services.AddScoped<ILoanService>(_ => new LoanService(100, 7));

// Seed test data into memory DB
SeedData.SetInitialData();
Expand Down
22 changes: 20 additions & 2 deletions .NET/library/SeedData.cs
Original file line number Diff line number Diff line change
Expand Up @@ -76,11 +76,26 @@ public static void SetInitialData()
LoanEndDate = DateTime.Now.Date.AddDays(7)
};

var bookOnLoanWithReservation = new BookStock
{
Book = agileBook,
OnLoanTo = daveSmith,
LoanEndDate = DateTime.Now.Date.AddDays(3)
};

var reservationforBook1 = new Loan
{
BookStock = bookOnLoanWithReservation,
Borrower = daveSmith,
LoanStartDate = DateTime.Now.Date.AddDays(3),
LoanEndDate = DateTime.Now.Date.AddDays(10)
};

var rustBookStock = new BookStock
{
Book = rustBook,
OnLoanTo = null,
LoanEndDate = null
OnLoanTo = daveSmith,
LoanEndDate = DateTime.Now.Date.AddDays(-3)
};

using (var context = new LibraryContext())
Expand All @@ -101,6 +116,9 @@ public static void SetInitialData()
context.Catalogue.Add(bookNotOnLoan);
context.Catalogue.Add(bookOnLoanUntilNextWeek);
context.Catalogue.Add(rustBookStock);
context.Catalogue.Add(bookOnLoanWithReservation);

context.Loans.Add(reservationforBook1);

context.SaveChanges();

Expand Down
Binary file not shown.
Binary file not shown.
Binary file added .vs/OneBeyond/v17/.wsuo
Binary file not shown.
Loading