using LibraryManagement.Shared.Models; using DbConnect.Data; using DbConnect.Entities; using Microsoft.EntityFrameworkCore; using Backend.Features.Loyalty; using Backend.Features.Notification; namespace Backend.Features.Borrowings { public interface IBorrowingService { Task BorrowBookAsync(Guid userId, int bookId); Task RequestReturnAsync(Guid borrowingId); Task ReturnBookAsync(Guid borrowingId); Task> GetUserBorrowingsAsync(Guid userId); Task> GetAllBorrowingsAsync(); } public class BorrowingService : IBorrowingService { private readonly AppDbContext _context; private readonly ILoyaltyService _loyaltyService; private readonly INotificationService _notificationService; private const decimal FinePerDay = 500; public BorrowingService(AppDbContext context, ILoyaltyService loyaltyService, INotificationService notificationService) { _context = context; _loyaltyService = loyaltyService; _notificationService = notificationService; } public async Task BorrowBookAsync(Guid userId, int bookId) { // 1. Check for Active Membership var subscription = await _context.UserSubscriptions .Include(s => s.Membership) .Where(s => s.UserId == userId && s.IsActive && s.ExpiryDate > DateTime.UtcNow) .FirstOrDefaultAsync(); if (subscription == null) { throw new Exception("Active membership required to borrow books."); } // 2. Check for Overdue Books or Unpaid Fines var hasBlockers = await _context.Borrowings .AnyAsync(b => b.UserId == userId && ((b.Status == "Borrowed" && b.DueDate < DateTime.UtcNow) || (b.FineAmount > 0 && !b.IsFinePaid))); if (hasBlockers) { throw new Exception("Borrowing blocked: You have overdue books or unpaid fines."); } // 3. Check MaxBooks limit var activeBorrowCount = await _context.Borrowings .CountAsync(b => b.UserId == userId && b.Status == "Borrowed"); if (activeBorrowCount >= subscription.Membership.MaxBooks) { throw new Exception($"Borrowing limit reached: You can only borrow {subscription.Membership.MaxBooks} books at a time."); } // 4. Check Book Availability var book = await _context.Books.FindAsync(bookId); if (book == null || !book.IsActive || book.AvailableCopies <= 0) { throw new Exception("Book is currently unavailable."); } // 5. Create Borrowing Record var borrowing = new Borrowing { UserId = userId, BookId = bookId, BorrowDate = DateTime.UtcNow, DueDate = DateTime.UtcNow.AddDays(subscription.Membership.BorrowingDays), Status = "Borrowed", FineAmount = 0, IsFinePaid = false }; // 6. Update Book Stock book.AvailableCopies--; if (book.AvailableCopies == 0) { book.Status = "Out Of Stock"; } _context.Borrowings.Add(borrowing); await _context.SaveChangesAsync(); // Load relations for mapping await _context.Entry(borrowing).Reference(b => b.Book).LoadAsync(); await _context.Entry(borrowing).Reference(b => b.User).LoadAsync(); return MapToDto(borrowing); } public async Task RequestReturnAsync(Guid borrowingId) { var borrowing = await _context.Borrowings .Include(b => b.Book) .Include(b => b.User) .FirstOrDefaultAsync(b => b.Id == borrowingId); if (borrowing == null) throw new Exception("Borrowing record not found."); if (borrowing.Status != "Borrowed") throw new Exception("Only borrowed books can be requested for return."); borrowing.Status = "PendingReturn"; await _context.SaveChangesAsync(); return MapToDto(borrowing); } public async Task ReturnBookAsync(Guid borrowingId) { var borrowing = await _context.Borrowings .Include(b => b.Book) .Include(b => b.User) .FirstOrDefaultAsync(b => b.Id == borrowingId); if (borrowing == null) throw new Exception("Borrowing record not found."); if (borrowing.Status == "Returned") throw new Exception("Book has already been returned."); var now = DateTime.UtcNow; borrowing.ReturnDate = now; borrowing.Status = "Returned"; // Calculate Fine if (now > borrowing.DueDate) { var overdueDays = (now.Date - borrowing.DueDate.Date).Days; if (overdueDays > 0) { borrowing.FineAmount = overdueDays * FinePerDay; } } // Update Book Stock var book = borrowing.Book; book.AvailableCopies++; book.Status = "Available"; await _context.SaveChangesAsync(); // Wishlist Notification Trigger try { var interestedUsers = await _context.WishlistItems .Include(w => w.User) .Where(w => w.BookId == book.Id) .ToListAsync(); foreach (var wishlistEntry in interestedUsers) { await _notificationService.SendAndSaveNotificationAsync( wishlistEntry.UserId, wishlistEntry.User.FcmToken, "Book Available!", $"Good news! '{book.Title}' is now available for borrowing. Grab it before someone else does!", "Success", "/Books", "View Book" ); } } catch (Exception ex) { Console.WriteLine($"Error sending wishlist notifications: {ex.Message}"); } // Loyalty Integration: Send RETURN event string externalUserId = borrowing.UserId.ToString(); string userMobile = borrowing.User?.PhoneNumber ?? "0000000000"; string userEmail = borrowing.User?.Email ?? "No Email"; await _loyaltyService.ProcessEventAsync( externalUserId: externalUserId, eventKey: "RETURN", eventValue: 0, referenceId: $"RET-{borrowing.Id}", description: $"Returned Book: {book?.Title}", email: userEmail, mobile: userMobile ); return MapToDto(borrowing); } public async Task> GetUserBorrowingsAsync(Guid userId) { var borrowings = await _context.Borrowings .Include(b => b.Book) .Include(b => b.User) .Where(b => b.UserId == userId) .OrderByDescending(b => b.BorrowDate) .ToListAsync(); return borrowings.Select(MapToDto); } public async Task> GetAllBorrowingsAsync() { var borrowings = await _context.Borrowings .Include(b => b.Book) .Include(b => b.User) .OrderByDescending(b => b.BorrowDate) .ToListAsync(); return borrowings.Select(MapToDto); } private static BorrowingDto MapToDto(Borrowing b) { return new BorrowingDto { Id = b.Id, UserId = b.UserId, UserEmail = b.User?.Email ?? "Unknown", BookId = b.BookId, BookTitle = b.Book?.Title ?? "Unknown", BorrowDate = b.BorrowDate, DueDate = b.DueDate, ReturnDate = b.ReturnDate, Status = b.Status, FineAmount = b.FineAmount, IsFinePaid = b.IsFinePaid }; } } }