diff --git a/.NET/library/Controllers/LoanController.cs b/.NET/library/Controllers/LoanController.cs new file mode 100644 index 0000000..cf9c0b3 --- /dev/null +++ b/.NET/library/Controllers/LoanController.cs @@ -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 _logger; + private readonly ILoanService _loanService; + + public LoanController(ILogger logger, ILoanService loanService) + { + _logger = logger; + _loanService = loanService; + } + + [HttpGet] + [Route("OnLoan")] + public IList 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); + } + + } +} diff --git a/.NET/library/DataAccess/ILoanService.cs b/.NET/library/DataAccess/ILoanService.cs new file mode 100644 index 0000000..2344a5b --- /dev/null +++ b/.NET/library/DataAccess/ILoanService.cs @@ -0,0 +1,12 @@ +using OneBeyondApi.Model; + +namespace OneBeyondApi.DataAccess +{ + public interface ILoanService + { + List GetAllLoaned(); + Guid ReturnBook(Guid bookStockId); + Guid ReserveBook(Guid bookStockId, Guid borrowerId); + DateTime GetFirstAvailableDate(Guid? bookId, string bookTitle); + } +} diff --git a/.NET/library/DataAccess/LibraryContext.cs b/.NET/library/DataAccess/LibraryContext.cs index 91510e0..6c8533b 100644 --- a/.NET/library/DataAccess/LibraryContext.cs +++ b/.NET/library/DataAccess/LibraryContext.cs @@ -13,5 +13,7 @@ protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) public DbSet Books { get; set; } public DbSet Catalogue { get; set; } public DbSet Borrowers { get; set; } + public DbSet Loans { get; set; } + public DbSet Fines { get; set; } } } diff --git a/.NET/library/DataAccess/LoanService.cs b/.NET/library/DataAccess/LoanService.cs new file mode 100644 index 0000000..d00773e --- /dev/null +++ b/.NET/library/DataAccess/LoanService.cs @@ -0,0 +1,211 @@ +using Microsoft.EntityFrameworkCore; +using OneBeyondApi.Model; + +namespace OneBeyondApi.DataAccess +{ + public class LoanService : ILoanService + { + int _finePerDay = 0; + int _defaultLoanDays = 0; + + /// + /// Constructor + /// + /// Default value => 50, but can change in DI + /// Default value => 5, but can change in DI + public LoanService(int finePerDay = 50, int defaultLoanDays = 5) + { + _finePerDay = finePerDay; + _defaultLoanDays = defaultLoanDays; + } + + /// + /// Get all currently loaned book grouped by the Borrowers + /// + /// + /// 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 + /// + public List 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; + } + + /// + /// Returns a borrowed book. Set the bookstock loandata to null + /// + /// The returned bookstock id + /// + /// An empty guid if the return not succeded and the returned + /// bookstock id if the returns succseeded + /// + 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; + } + + /// + /// Reserve a book or loan it if available + /// + /// + /// + 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; + } + + /// + /// Get the first loan date for a book + /// + /// The id of the needed book + /// The title of the needed book + /// 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 + /// + 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; + } + } +} diff --git a/.NET/library/Model/Fine.cs b/.NET/library/Model/Fine.cs new file mode 100644 index 0000000..b969fc7 --- /dev/null +++ b/.NET/library/Model/Fine.cs @@ -0,0 +1,16 @@ +namespace OneBeyondApi.Model +{ + public class Fine + { + public Guid Id { get; set; } + public Borrower Borrower { get; set; } + /// + /// The fine for the late return + /// + public int Amount { get; set; } + /// + /// If a fine paid than this is the pay date + /// + public DateTime? PaidDate { get; set; } + } +} diff --git a/.NET/library/Model/Loan.cs b/.NET/library/Model/Loan.cs new file mode 100644 index 0000000..f8ba255 --- /dev/null +++ b/.NET/library/Model/Loan.cs @@ -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; } + /// + /// The planned/loaned date + /// + public DateTime LoanStartDate { get; set; } + /// + /// The planned end date for the loan + /// + public DateTime LoanEndDate { get; set; } + + } +} diff --git a/.NET/library/Model/LoanedBook.cs b/.NET/library/Model/LoanedBook.cs new file mode 100644 index 0000000..08f7aa4 --- /dev/null +++ b/.NET/library/Model/LoanedBook.cs @@ -0,0 +1,8 @@ +namespace OneBeyondApi.Model +{ + public class LoanedBook + { + public Guid BookStockId { get; set; } + public string Title { get; set; } + } +} diff --git a/.NET/library/Model/OnLoanModel.cs b/.NET/library/Model/OnLoanModel.cs new file mode 100644 index 0000000..68ccb2e --- /dev/null +++ b/.NET/library/Model/OnLoanModel.cs @@ -0,0 +1,8 @@ +namespace OneBeyondApi.Model +{ + public class OnLoanModel + { + public Borrower Borrower { get; set; } + public List Books { get; set; } = []; + } +} diff --git a/.NET/library/Program.cs b/.NET/library/Program.cs index ca5d07b..63982f5 100644 --- a/.NET/library/Program.cs +++ b/.NET/library/Program.cs @@ -8,6 +8,7 @@ builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); +builder.Services.AddScoped(_ => new LoanService(100, 7)); // Seed test data into memory DB SeedData.SetInitialData(); diff --git a/.NET/library/SeedData.cs b/.NET/library/SeedData.cs index a8218dd..1b9fb17 100644 --- a/.NET/library/SeedData.cs +++ b/.NET/library/SeedData.cs @@ -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()) @@ -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(); diff --git a/.vs/OneBeyond/CopilotIndices/17.12.38.29086/CodeChunks.db b/.vs/OneBeyond/CopilotIndices/17.12.38.29086/CodeChunks.db new file mode 100644 index 0000000..aa5d6e1 Binary files /dev/null and b/.vs/OneBeyond/CopilotIndices/17.12.38.29086/CodeChunks.db differ diff --git a/.vs/OneBeyond/CopilotIndices/17.12.38.29086/SemanticSymbols.db b/.vs/OneBeyond/CopilotIndices/17.12.38.29086/SemanticSymbols.db new file mode 100644 index 0000000..527188c Binary files /dev/null and b/.vs/OneBeyond/CopilotIndices/17.12.38.29086/SemanticSymbols.db differ diff --git a/.vs/OneBeyond/v17/.wsuo b/.vs/OneBeyond/v17/.wsuo new file mode 100644 index 0000000..b37de70 Binary files /dev/null and b/.vs/OneBeyond/v17/.wsuo differ diff --git a/.vs/OneBeyond/v17/DocumentLayout.json b/.vs/OneBeyond/v17/DocumentLayout.json new file mode 100644 index 0000000..9d75fda --- /dev/null +++ b/.vs/OneBeyond/v17/DocumentLayout.json @@ -0,0 +1,229 @@ +{ + "Version": 1, + "WorkspaceRootPath": "C:\\Projects\\OneBeyond\\", + "Documents": [], + "DocumentGroupContainers": [ + { + "Orientation": 0, + "VerticalTabListWidth": 256, + "DocumentGroups": [ + { + "DockedWidth": 949, + "SelectedChildIndex": -1, + "Children": [ + { + "$type": "Bookmark", + "Name": "ST:3:0:{809f6ff3-8092-454a-8003-6d4091f9b5bb}" + }, + { + "$type": "Bookmark", + "Name": "ST:1:0:{80454082-9ab8-47d4-af23-82bf6739e2a9}" + }, + { + "$type": "Bookmark", + "Name": "ST:0:0:{809f6ff3-8092-454a-8003-6d4091f9b5bb}" + }, + { + "$type": "Bookmark", + "Name": "ST:2:0:{80454082-9ab8-47d4-af23-82bf6739e2a9}" + }, + { + "$type": "Bookmark", + "Name": "ST:4:0:{80454082-9ab8-47d4-af23-82bf6739e2a9}" + }, + { + "$type": "Bookmark", + "Name": "ST:17:0:{2456bd12-ecf7-4988-a4a6-67d49173f565}" + }, + { + "$type": "Bookmark", + "Name": "ST:128:0:{13143d73-808b-4a2f-bebb-ccbe9c93fe37}" + }, + { + "$type": "Bookmark", + "Name": "ST:20:0:{2456bd12-ecf7-4988-a4a6-67d49173f565}" + }, + { + "$type": "Bookmark", + "Name": "ST:19:0:{2456bd12-ecf7-4988-a4a6-67d49173f565}" + }, + { + "$type": "Bookmark", + "Name": "ST:18:0:{2456bd12-ecf7-4988-a4a6-67d49173f565}" + }, + { + "$type": "Bookmark", + "Name": "ST:25:0:{2456bd12-ecf7-4988-a4a6-67d49173f565}" + }, + { + "$type": "Bookmark", + "Name": "ST:26:0:{2456bd12-ecf7-4988-a4a6-67d49173f565}" + }, + { + "$type": "Bookmark", + "Name": "ST:27:0:{2456bd12-ecf7-4988-a4a6-67d49173f565}" + }, + { + "$type": "Bookmark", + "Name": "ST:1:0:{809f6ff3-8092-454a-8003-6d4091f9b5bb}" + }, + { + "$type": "Bookmark", + "Name": "ST:5:0:{809f6ff3-8092-454a-8003-6d4091f9b5bb}" + }, + { + "$type": "Bookmark", + "Name": "ST:34:0:{2456bd12-ecf7-4988-a4a6-67d49173f565}" + }, + { + "$type": "Bookmark", + "Name": "ST:35:0:{2456bd12-ecf7-4988-a4a6-67d49173f565}" + }, + { + "$type": "Bookmark", + "Name": "ST:37:0:{2456bd12-ecf7-4988-a4a6-67d49173f565}" + }, + { + "$type": "Bookmark", + "Name": "ST:38:0:{2456bd12-ecf7-4988-a4a6-67d49173f565}" + }, + { + "$type": "Bookmark", + "Name": "ST:39:0:{2456bd12-ecf7-4988-a4a6-67d49173f565}" + }, + { + "$type": "Bookmark", + "Name": "ST:40:0:{2456bd12-ecf7-4988-a4a6-67d49173f565}" + }, + { + "$type": "Bookmark", + "Name": "ST:41:0:{2456bd12-ecf7-4988-a4a6-67d49173f565}" + }, + { + "$type": "Bookmark", + "Name": "ST:42:0:{2456bd12-ecf7-4988-a4a6-67d49173f565}" + }, + { + "$type": "Bookmark", + "Name": "ST:6:0:{809f6ff3-8092-454a-8003-6d4091f9b5bb}" + }, + { + "$type": "Bookmark", + "Name": "ST:44:0:{2456bd12-ecf7-4988-a4a6-67d49173f565}" + }, + { + "$type": "Bookmark", + "Name": "ST:30:0:{2456bd12-ecf7-4988-a4a6-67d49173f565}" + }, + { + "$type": "Bookmark", + "Name": "ST:31:0:{2456bd12-ecf7-4988-a4a6-67d49173f565}" + }, + { + "$type": "Bookmark", + "Name": "ST:21:0:{2456bd12-ecf7-4988-a4a6-67d49173f565}" + }, + { + "$type": "Bookmark", + "Name": "ST:22:0:{2456bd12-ecf7-4988-a4a6-67d49173f565}" + }, + { + "$type": "Bookmark", + "Name": "ST:32:0:{2456bd12-ecf7-4988-a4a6-67d49173f565}" + }, + { + "$type": "Bookmark", + "Name": "ST:33:0:{2456bd12-ecf7-4988-a4a6-67d49173f565}" + }, + { + "$type": "Bookmark", + "Name": "ST:2:0:{809f6ff3-8092-454a-8003-6d4091f9b5bb}" + }, + { + "$type": "Bookmark", + "Name": "ST:0:0:{80454082-9ab8-47d4-af23-82bf6739e2a9}" + }, + { + "$type": "Bookmark", + "Name": "ST:129:0:{1fc202d4-d401-403c-9834-5b218574bb67}" + }, + { + "$type": "Bookmark", + "Name": "ST:128:0:{1fc202d4-d401-403c-9834-5b218574bb67}" + }, + { + "$type": "Bookmark", + "Name": "ST:129:0:{116d2292-e37d-41cd-a077-ebacac4c8cc4}" + }, + { + "$type": "Bookmark", + "Name": "ST:131:0:{13b12e3e-c1b4-4539-9371-4fe9a0d523fc}" + }, + { + "$type": "Bookmark", + "Name": "ST:132:0:{1fc202d4-d401-403c-9834-5b218574bb67}" + }, + { + "$type": "Bookmark", + "Name": "ST:130:0:{13b12e3e-c1b4-4539-9371-4fe9a0d523fc}" + }, + { + "$type": "Bookmark", + "Name": "ST:1924764005:0:{13143d73-808b-4a2f-bebb-ccbe9c93fe37}" + }, + { + "$type": "Bookmark", + "Name": "ST:131:0:{1fc202d4-d401-403c-9834-5b218574bb67}" + }, + { + "$type": "Bookmark", + "Name": "ST:128:0:{13b12e3e-c1b4-4539-9371-4fe9a0d523fc}" + } + ] + }, + { + "DockedWidth": 410, + "SelectedChildIndex": -1, + "Children": [ + { + "$type": "Bookmark", + "Name": "ST:0:0:{d78612c7-9962-4b83-95d9-268046dad23a}" + }, + { + "$type": "Bookmark", + "Name": "ST:0:0:{34e76e81-ee4a-11d0-ae2e-00a0c90fffc3}" + }, + { + "$type": "Bookmark", + "Name": "ST:0:0:{be4d7042-ba3f-11d2-840e-00c04f9902c1}" + }, + { + "$type": "Bookmark", + "Name": "ST:0:0:{605322a2-17ae-43f4-b60f-766556e46c87}" + }, + { + "$type": "Bookmark", + "Name": "ST:0:0:{ecb7191a-597b-41f5-9843-03a4cf275dde}" + }, + { + "$type": "Bookmark", + "Name": "ST:0:0:{3822e751-eb69-4b0e-b301-595a9e4c74d5}" + }, + { + "$type": "Bookmark", + "Name": "ST:0:0:{007a3a6b-b5b2-454d-a2bd-cf929f989be2}" + }, + { + "$type": "Bookmark", + "Name": "ST:0:0:{ca8cc5c7-0231-406a-95cd-aa5ed6ac0190}" + }, + { + "$type": "Bookmark", + "Name": "ST:0:0:{0ad07096-bba9-4900-a651-0598d26f6d24}" + } + ] + } + ] + } + ] +} \ No newline at end of file diff --git a/.vs/VSWorkspaceState.json b/.vs/VSWorkspaceState.json new file mode 100644 index 0000000..3201359 --- /dev/null +++ b/.vs/VSWorkspaceState.json @@ -0,0 +1,7 @@ +{ + "ExpandedNodes": [ + "" + ], + "SelectedNode": "\\OneBeyondApi.sln", + "PreviewInSolutionExplorer": false +} \ No newline at end of file