From 470f603c4e8563e8362fd584dfbd3d79066db223 Mon Sep 17 00:00:00 2001 From: Nguyen Quang Chien Date: Thu, 1 Jun 2023 17:32:59 +0700 Subject: [PATCH 01/13] add: base --- src/Api/Program.cs | 1 - .../Documents/Commands/BorrowDocument.cs | 59 +++++++++++++++++++ 2 files changed, 59 insertions(+), 1 deletion(-) create mode 100644 src/Application/Documents/Commands/BorrowDocument.cs diff --git a/src/Api/Program.cs b/src/Api/Program.cs index 829f0873..512cce29 100644 --- a/src/Api/Program.cs +++ b/src/Api/Program.cs @@ -18,7 +18,6 @@ builder.Services.AddApplicationServices(); builder.Services.AddInfrastructureServices(builder.Configuration); builder.Services.AddApiServices(); - var app = builder.Build(); app.UseInfrastructure(builder.Configuration); diff --git a/src/Application/Documents/Commands/BorrowDocument.cs b/src/Application/Documents/Commands/BorrowDocument.cs new file mode 100644 index 00000000..5135e2a6 --- /dev/null +++ b/src/Application/Documents/Commands/BorrowDocument.cs @@ -0,0 +1,59 @@ +using Application.Common.Models.Dtos.Physical; +using MediatR; +using Application.Common.Interfaces; +using Application.Common.Exceptions; +using Application.Common.Models.Dtos.Physical; +using AutoMapper; +using Microsoft.EntityFrameworkCore; +namespace Application.Documents.Commands; + +public class BorrowDocument +{ + public class Validator : AbstractValidator + { + public Validator() + { + RuleLevelCascadeMode = CascadeMode.Stop; + + } + } + + public record Command : IRequest + { + public Guid DocumentId { get; set; } + public Guid BorrowerId { get; set; } + public DateTime BorrowTo { get; set; } + public string Reason { get; init; } = null!; + } + + public class CommandHandler : IRequestHandler + { + private readonly IApplicationDbContext _context; + private readonly IMapper _mapper; + + public CommandHandler(IApplicationDbContext context, IMapper mapper) + { + _context = context; + _mapper = mapper; + } + public async Task Handle(Command request, CancellationToken cancellationToken) + { + var user = await _context.Users.FirstOrDefaultAsync(x => x.Id == request.BorrowerId, cancellationToken); + if (user is null) + { + throw new KeyNotFoundException("User does not exist."); + } + + var document = await _context.Documents.FirstOrDefaultAsync(x => x.Id == request.BorrowerId, cancellationToken); + if (document is null) + { + throw new KeyNotFoundException("Document does not exist."); + } + + if (DateTime.Now > request.BorrowTo) + { + throw new ConflictException("Due date cannot be in the past."); + } + } + } +} \ No newline at end of file From 96b0679584fee98aaff6f719cee801eeb88a0879 Mon Sep 17 00:00:00 2001 From: Nguyen Quang Chien Date: Thu, 1 Jun 2023 22:25:11 +0700 Subject: [PATCH 02/13] add: request borrow --- .../Documents/Commands/BorrowDocumentTests.cs | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 tests/Application.Tests.Integration/Documents/Commands/BorrowDocumentTests.cs diff --git a/tests/Application.Tests.Integration/Documents/Commands/BorrowDocumentTests.cs b/tests/Application.Tests.Integration/Documents/Commands/BorrowDocumentTests.cs new file mode 100644 index 00000000..11bfcbac --- /dev/null +++ b/tests/Application.Tests.Integration/Documents/Commands/BorrowDocumentTests.cs @@ -0,0 +1,6 @@ +namespace Application.Tests.Integration.Documents.Commands; + +public class BorrowDocumentTests +{ + +} \ No newline at end of file From 0ff5cc9d1ca16eca80515ecf2bb121abe85e5d26 Mon Sep 17 00:00:00 2001 From: Nguyen Quang Chien Date: Thu, 1 Jun 2023 22:26:24 +0700 Subject: [PATCH 03/13] add: a couple more things --- .../Documents/Commands/BorrowDocument.cs | 23 ++- .../Documents/Commands/BorrowDocumentTests.cs | 163 +++++++++++++++++- 2 files changed, 183 insertions(+), 3 deletions(-) diff --git a/src/Application/Documents/Commands/BorrowDocument.cs b/src/Application/Documents/Commands/BorrowDocument.cs index 5135e2a6..fd48b0fe 100644 --- a/src/Application/Documents/Commands/BorrowDocument.cs +++ b/src/Application/Documents/Commands/BorrowDocument.cs @@ -2,9 +2,12 @@ using MediatR; using Application.Common.Interfaces; using Application.Common.Exceptions; -using Application.Common.Models.Dtos.Physical; using AutoMapper; +using Domain.Entities.Physical; +using FluentValidation; using Microsoft.EntityFrameworkCore; +using NodaTime; + namespace Application.Documents.Commands; public class BorrowDocument @@ -15,6 +18,8 @@ public Validator() { RuleLevelCascadeMode = CascadeMode.Stop; + RuleFor(x => x.Reason) + .MaximumLength(512).WithMessage("Reason cannot exceed 512 characters."); } } @@ -44,7 +49,7 @@ public async Task Handle(Command request, CancellationToken cancellat throw new KeyNotFoundException("User does not exist."); } - var document = await _context.Documents.FirstOrDefaultAsync(x => x.Id == request.BorrowerId, cancellationToken); + var document = await _context.Documents.FirstOrDefaultAsync(x => x.Id == request.DocumentId, cancellationToken); if (document is null) { throw new KeyNotFoundException("Document does not exist."); @@ -54,6 +59,20 @@ public async Task Handle(Command request, CancellationToken cancellat { throw new ConflictException("Due date cannot be in the past."); } + + var entity = new Borrow() + { + Borrower = user, + Document = document, + BorrowTime = LocalDateTime.FromDateTime(DateTime.Now), + DueTime = LocalDateTime.FromDateTime(request.BorrowTo), + Reason = request.Reason, + }; + + var result = await _context.Borrows.AddAsync(entity, cancellationToken); + await _context.SaveChangesAsync(cancellationToken); + + return _mapper.Map(result.Entity); } } } \ No newline at end of file diff --git a/tests/Application.Tests.Integration/Documents/Commands/BorrowDocumentTests.cs b/tests/Application.Tests.Integration/Documents/Commands/BorrowDocumentTests.cs index 11bfcbac..40b3b51b 100644 --- a/tests/Application.Tests.Integration/Documents/Commands/BorrowDocumentTests.cs +++ b/tests/Application.Tests.Integration/Documents/Commands/BorrowDocumentTests.cs @@ -1,6 +1,167 @@ +using Application.Common.Exceptions; +using Application.Common.Mappings; +using Application.Documents.Commands; +using AutoMapper; +using Domain.Entities.Physical; +using FluentAssertions; +using Infrastructure.Persistence; +using Microsoft.Extensions.DependencyInjection; +using Xunit; + namespace Application.Tests.Integration.Documents.Commands; -public class BorrowDocumentTests +public class BorrowDocumentTests : BaseClassFixture { + public BorrowDocumentTests(CustomApiFactory apiFactory) : base(apiFactory) + { + } + + [Fact] + public async Task ShouldCreateBorrowRequest_WhenBorrowDetailsAreValid() + { + // Arrange + using var scope = ScopeFactory.CreateScope(); + var context = scope.ServiceProvider.GetRequiredService(); + var department = CreateDepartment(); + var document = CreateNDocuments(1).First(); + var folder = CreateFolder(document); + var locker = CreateLocker(folder); + var room = CreateRoom(department, locker); + var user = CreateUser("Employee", "random"); + user.Department = department; + await context.AddAsync(room); + await context.AddAsync(user); + await context.SaveChangesAsync(); + + var command = new BorrowDocument.Command() + { + BorrowerId = user.Id, + DocumentId = document.Id, + BorrowTo = DateTime.Now.AddDays(1), + Reason = "khoa on ko?", + }; + + // Act + var result = await SendAsync(command); + + // Assert + result.Should().NotBeNull(); + result.Borrower.Id.Should().Be(user.Id); + result.Document.Id.Should().Be(document.Id); + result.DueTime.Should().Be(command.BorrowTo); + result.Reason.Should().Be(command.Reason); + + // Cleanup + Remove(await FindAsync(result.Id)); + Remove(document); + Remove(folder); + Remove(locker); + Remove(room); + Remove(user); + Remove(department); + } + + [Fact] + public async Task ShouldThrowKeyNotFoundException_WhenUserDoesNotExist() + { + // Arrange + var department = CreateDepartment(); + var document = CreateNDocuments(1).First(); + var folder = CreateFolder(document); + var locker = CreateLocker(folder); + var room = CreateRoom(department, locker); + await AddAsync(room); + + var command = new BorrowDocument.Command() + { + BorrowerId = Guid.NewGuid(), + DocumentId = document.Id, + BorrowTo = DateTime.Now.AddDays(1), + Reason = "khoa on ko?", + }; + + // Act + var action = async () => await SendAsync(command); + + // Assert + await action.Should().ThrowAsync() + .WithMessage("User does not exist."); + + // Cleanup + Remove(document); + Remove(folder); + Remove(locker); + Remove(room); + Remove(department); + } + + [Fact] + public async Task ShouldThrowKeyNotFoundException_WhenDocumentDoesNotExist() + { + // Arrange + var department = CreateDepartment(); + var user = CreateUser("Employee", "random"); + user.Department = department; + await AddAsync(user); + + var command = new BorrowDocument.Command() + { + BorrowerId = user.Id, + DocumentId = Guid.NewGuid(), + BorrowTo = DateTime.Now.AddDays(1), + Reason = "khoa on ko?", + }; + + // Act + var action = async () => await SendAsync(command); + + // Assert + await action.Should().ThrowAsync() + .WithMessage("Document does not exist."); + + // Cleanup + Remove(user); + Remove(department); + } + [Fact] + public async Task ShouldThrowConflictException_WhenDueDateIsInThePast() + { + // Arrange + using var scope = ScopeFactory.CreateScope(); + var context = scope.ServiceProvider.GetRequiredService(); + var department = CreateDepartment(); + var document = CreateNDocuments(1).First(); + var folder = CreateFolder(document); + var locker = CreateLocker(folder); + var room = CreateRoom(department, locker); + var user = CreateUser("Employee", "random"); + user.Department = department; + await context.AddAsync(room); + await context.AddAsync(user); + await context.SaveChangesAsync(); + + var command = new BorrowDocument.Command() + { + BorrowerId = user.Id, + DocumentId = document.Id, + BorrowTo = DateTime.Now.AddDays(-1), + Reason = "khoa on ko?", + }; + + // Act + var action = async () => await SendAsync(command); + + // Assert + await action.Should().ThrowAsync() + .WithMessage("Due date cannot be in the past."); + + // Cleanup + Remove(document); + Remove(folder); + Remove(locker); + Remove(room); + Remove(user); + Remove(department); + } } \ No newline at end of file From cc9519f90a3a3ca4956c35a5e1e0af7dfc153983 Mon Sep 17 00:00:00 2001 From: Nguyen Quang Chien Date: Fri, 2 Jun 2023 15:59:14 +0700 Subject: [PATCH 04/13] add: base for borrow --- src/Api/Controllers/BorrowsController.cs | 61 ++++++++ .../Requests/Borrows/BorrowDocumentRequest.cs | 24 +++ ...lBorrowRequestsPaginatedQueryParameters.cs | 20 +++ ...GetAllDocumentsPaginatedQueryParameters.cs | 22 +-- .../GetAllFoldersPaginatedQueryParameters.cs | 22 +-- .../GetAllLockersPaginatedQueryParameters.cs | 22 +-- .../Requests/PaginatedQueryParameters.cs | 25 ++++ .../GetAllRoomsPaginatedQueryParameters.cs | 22 +-- .../GetAllUsersPaginatedQueryParameters.cs | 22 +-- .../Commands/BorrowDocument.cs | 29 ++-- .../Queries/GetAllBorrowRequestsPaginated.cs | 140 ++++++++++++++++++ .../Queries/GetAllDocumentsPaginated.cs | 4 +- .../Folders/Queries/GetAllFoldersPaginated.cs | 2 +- src/Domain/Entities/Physical/Borrow.cs | 1 + .../Documents/Commands/BorrowDocumentTests.cs | 1 + 15 files changed, 299 insertions(+), 118 deletions(-) create mode 100644 src/Api/Controllers/BorrowsController.cs create mode 100644 src/Api/Controllers/Payload/Requests/Borrows/BorrowDocumentRequest.cs create mode 100644 src/Api/Controllers/Payload/Requests/Borrows/GetAllBorrowRequestsPaginatedQueryParameters.cs create mode 100644 src/Api/Controllers/Payload/Requests/PaginatedQueryParameters.cs rename src/Application/{Documents => Borrows}/Commands/BorrowDocument.cs (74%) create mode 100644 src/Application/Borrows/Queries/GetAllBorrowRequestsPaginated.cs diff --git a/src/Api/Controllers/BorrowsController.cs b/src/Api/Controllers/BorrowsController.cs new file mode 100644 index 00000000..48cee17b --- /dev/null +++ b/src/Api/Controllers/BorrowsController.cs @@ -0,0 +1,61 @@ +using Api.Controllers.Payload.Requests.Borrows; +using Application.Borrows.Commands; +using Application.Borrows.Queries; +using Application.Common.Models; +using Application.Common.Models.Dtos.Physical; +using Application.Identity; +using Infrastructure.Identity.Authorization; +using Microsoft.AspNetCore.Mvc; + +namespace Api.Controllers; + +public class BorrowsController : ApiControllerBase +{ + /// + /// Borrow a document request + /// + /// Borrow document details + /// A BorrowDto of the requested borrow + [RequiresRole(IdentityData.Roles.Employee)] + [HttpPost] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesResponseType(StatusCodes.Status409Conflict)] + public async Task>> BorrowDocument([FromBody] BorrowDocumentRequest request) + { + var command = new BorrowDocument.Command() + { + BorrowerId = request.BorrowerId, + DocumentId = request.DocumentId, + BorrowTo = request.BorrowTo, + Reason = request.Reason, + }; + var result = await Mediator.Send(command); + return Ok(Result.Succeed(result)); + } + + [RequiresRole(IdentityData.Roles.Admin, IdentityData.Roles.Staff)] + [HttpGet] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesResponseType(StatusCodes.Status409Conflict)] + public async Task>>> GetAllBorrowRequestsPaginated( + [FromQuery] GetAllBorrowRequestsPaginatedQueryParameters queryParameters) + { + var command = new GetAllBorrowRequestsPaginated.Query() + { + RoomId = queryParameters.RoomId, + LockerId = queryParameters.LockerId, + FolderId = queryParameters.FolderId, + SearchTerm = queryParameters.SearchTerm, + Page = queryParameters.Page, + Size = queryParameters.Size, + SortBy = queryParameters.SortBy, + SortOrder = queryParameters.SortOrder, + }; + var result = await Mediator.Send(command); + return Ok(Result>.Succeed(result)); + } +} \ No newline at end of file diff --git a/src/Api/Controllers/Payload/Requests/Borrows/BorrowDocumentRequest.cs b/src/Api/Controllers/Payload/Requests/Borrows/BorrowDocumentRequest.cs new file mode 100644 index 00000000..53acee4f --- /dev/null +++ b/src/Api/Controllers/Payload/Requests/Borrows/BorrowDocumentRequest.cs @@ -0,0 +1,24 @@ +namespace Api.Controllers.Payload.Requests.Borrows; + +/// +/// Request details to borrow a document +/// +public class BorrowDocumentRequest +{ + /// + /// Id of the document to be borrowed + /// + public Guid DocumentId { get; set; } + /// + /// Id of the borrower + /// + public Guid BorrowerId { get; set; } + /// + /// Borrow to + /// + public DateTime BorrowTo { get; set; } + /// + /// Reason of borrowing + /// + public string Reason { get; set; } = null!; +} \ No newline at end of file diff --git a/src/Api/Controllers/Payload/Requests/Borrows/GetAllBorrowRequestsPaginatedQueryParameters.cs b/src/Api/Controllers/Payload/Requests/Borrows/GetAllBorrowRequestsPaginatedQueryParameters.cs new file mode 100644 index 00000000..961fbb80 --- /dev/null +++ b/src/Api/Controllers/Payload/Requests/Borrows/GetAllBorrowRequestsPaginatedQueryParameters.cs @@ -0,0 +1,20 @@ +namespace Api.Controllers.Payload.Requests.Borrows; + +/// +/// Query parameters for getting all borrow requests with pagination +/// +public class GetAllBorrowRequestsPaginatedQueryParameters : PaginatedQueryParameters +{ + /// + /// Id of the room to retrieve borrow requests in + /// + public Guid? RoomId { get; set; } + /// + /// Id of the locker to retrieve borrow requests in + /// + public Guid? LockerId { get; set; } + /// + /// Id of the folder to retrieve borrow requests in + /// + public Guid? FolderId { get; set; } +} \ No newline at end of file diff --git a/src/Api/Controllers/Payload/Requests/Documents/GetAllDocumentsPaginatedQueryParameters.cs b/src/Api/Controllers/Payload/Requests/Documents/GetAllDocumentsPaginatedQueryParameters.cs index 7f134c04..52b9016a 100644 --- a/src/Api/Controllers/Payload/Requests/Documents/GetAllDocumentsPaginatedQueryParameters.cs +++ b/src/Api/Controllers/Payload/Requests/Documents/GetAllDocumentsPaginatedQueryParameters.cs @@ -3,7 +3,7 @@ namespace Api.Controllers.Payload.Requests.Documents; /// /// Query parameters for getting all documents with pagination /// -public class GetAllDocumentsPaginatedQueryParameters +public class GetAllDocumentsPaginatedQueryParameters : PaginatedQueryParameters { /// /// Id of the room to find documents in @@ -17,24 +17,4 @@ public class GetAllDocumentsPaginatedQueryParameters /// Id of the folder to find documents in /// public Guid? FolderId { get; set; } - /// - /// Search term - /// - public string? SearchTerm { get; set; } - /// - /// Page number - /// - public int? Page { get; set; } - /// - /// Size number - /// - public int? Size { get; set; } - /// - /// Sort criteria - /// - public string? SortBy { get; set; } - /// - /// Sort direction - /// - public string? SortOrder { get; set; } } \ No newline at end of file diff --git a/src/Api/Controllers/Payload/Requests/Folders/GetAllFoldersPaginatedQueryParameters.cs b/src/Api/Controllers/Payload/Requests/Folders/GetAllFoldersPaginatedQueryParameters.cs index ae6399ab..a7815f26 100644 --- a/src/Api/Controllers/Payload/Requests/Folders/GetAllFoldersPaginatedQueryParameters.cs +++ b/src/Api/Controllers/Payload/Requests/Folders/GetAllFoldersPaginatedQueryParameters.cs @@ -3,7 +3,7 @@ namespace Api.Controllers.Payload.Requests.Folders; /// /// Query parameters for getting all folders with pagination /// -public class GetAllFoldersPaginatedQueryParameters +public class GetAllFoldersPaginatedQueryParameters : PaginatedQueryParameters { /// /// Id of the room to find folders in @@ -13,24 +13,4 @@ public class GetAllFoldersPaginatedQueryParameters /// Id of the locker to find folders in /// public Guid? LockerId { get; set; } - /// - /// Search term - /// - public string? SearchTerm { get; init; } - /// - /// Page number - /// - public int? Page { get; set; } - /// - /// Size number - /// - public int? Size { get; set; } - /// - /// Sort criteria - /// - public string? SortBy { get; set; } - /// - /// Sort direction - /// - public string? SortOrder { get; set; } } \ No newline at end of file diff --git a/src/Api/Controllers/Payload/Requests/Lockers/GetAllLockersPaginatedQueryParameters.cs b/src/Api/Controllers/Payload/Requests/Lockers/GetAllLockersPaginatedQueryParameters.cs index 9f701ed4..889c3e0d 100644 --- a/src/Api/Controllers/Payload/Requests/Lockers/GetAllLockersPaginatedQueryParameters.cs +++ b/src/Api/Controllers/Payload/Requests/Lockers/GetAllLockersPaginatedQueryParameters.cs @@ -5,30 +5,10 @@ namespace Api.Controllers.Payload.Requests.Lockers; /// /// Query parameters for getting all lockers with pagination /// -public class GetAllLockersPaginatedQueryParameters +public class GetAllLockersPaginatedQueryParameters : PaginatedQueryParameters { /// /// Id of the room to find lockers in /// public Guid? RoomId { get; set; } - /// - /// Search term - /// - public string? SearchTerm { get; set; } - /// - /// Page number - /// - public int? Page { get; set; } - /// - /// Size number - /// - public int? Size { get; set; } - /// - /// Sort criteria - /// - public string? SortBy { get; set; } - /// - /// Sort direction - /// - public string? SortOrder { get; set; } } \ No newline at end of file diff --git a/src/Api/Controllers/Payload/Requests/PaginatedQueryParameters.cs b/src/Api/Controllers/Payload/Requests/PaginatedQueryParameters.cs new file mode 100644 index 00000000..b18143d3 --- /dev/null +++ b/src/Api/Controllers/Payload/Requests/PaginatedQueryParameters.cs @@ -0,0 +1,25 @@ +namespace Api.Controllers.Payload.Requests; + +public class PaginatedQueryParameters +{ + /// + /// Search term + /// + public string? SearchTerm { get; set; } + /// + /// Page number + /// + public int? Page { get; set; } + /// + /// Size number + /// + public int? Size { get; set; } + /// + /// Sort criteria + /// + public string? SortBy { get; set; } + /// + /// Sort direction + /// + public string? SortOrder { get; set; } +} \ No newline at end of file diff --git a/src/Api/Controllers/Payload/Requests/Rooms/GetAllRoomsPaginatedQueryParameters.cs b/src/Api/Controllers/Payload/Requests/Rooms/GetAllRoomsPaginatedQueryParameters.cs index 89a7bbb6..8ddf2f92 100644 --- a/src/Api/Controllers/Payload/Requests/Rooms/GetAllRoomsPaginatedQueryParameters.cs +++ b/src/Api/Controllers/Payload/Requests/Rooms/GetAllRoomsPaginatedQueryParameters.cs @@ -3,26 +3,6 @@ namespace Api.Controllers.Payload.Requests.Rooms; /// /// Query parameters for getting all rooms with pagination /// -public class GetAllRoomsPaginatedQueryParameters +public class GetAllRoomsPaginatedQueryParameters : PaginatedQueryParameters { - /// - /// Search term - /// - public string? SearchTerm { get; set; } - /// - /// Page number - /// - public int? Page { get; set; } - /// - /// Size number - /// - public int? Size { get; set; } - /// - /// Sort criteria - /// - public string? SortBy { get; set; } - /// - /// Sort direction - /// - public string? SortOrder { get; set; } } \ No newline at end of file diff --git a/src/Api/Controllers/Payload/Requests/Users/GetAllUsersPaginatedQueryParameters.cs b/src/Api/Controllers/Payload/Requests/Users/GetAllUsersPaginatedQueryParameters.cs index 9f47d022..50d5683f 100644 --- a/src/Api/Controllers/Payload/Requests/Users/GetAllUsersPaginatedQueryParameters.cs +++ b/src/Api/Controllers/Payload/Requests/Users/GetAllUsersPaginatedQueryParameters.cs @@ -3,30 +3,10 @@ namespace Api.Controllers.Payload.Requests.Users; /// /// Query parameters for getting all users with pagination /// -public class GetAllUsersPaginatedQueryParameters +public class GetAllUsersPaginatedQueryParameters : PaginatedQueryParameters { /// /// Id of the department to find users in /// public Guid? DepartmentId { get; set; } - /// - /// Search term - /// - public string? SearchTerm { get; set; } - /// - /// Page number - /// - public int? Page { get; set; } - /// - /// Size number - /// - public int? Size { get; set; } - /// - /// Sort criteria - /// - public string? SortBy { get; set; } - /// - /// Sort direction - /// - public string? SortOrder { get; set; } } \ No newline at end of file diff --git a/src/Application/Documents/Commands/BorrowDocument.cs b/src/Application/Borrows/Commands/BorrowDocument.cs similarity index 74% rename from src/Application/Documents/Commands/BorrowDocument.cs rename to src/Application/Borrows/Commands/BorrowDocument.cs index fd48b0fe..1d1c9bf1 100644 --- a/src/Application/Documents/Commands/BorrowDocument.cs +++ b/src/Application/Borrows/Commands/BorrowDocument.cs @@ -1,14 +1,14 @@ -using Application.Common.Models.Dtos.Physical; -using MediatR; -using Application.Common.Interfaces; using Application.Common.Exceptions; +using Application.Common.Interfaces; +using Application.Common.Models.Dtos.Physical; using AutoMapper; using Domain.Entities.Physical; using FluentValidation; +using MediatR; using Microsoft.EntityFrameworkCore; using NodaTime; -namespace Application.Documents.Commands; +namespace Application.Borrows.Commands; public class BorrowDocument { @@ -25,9 +25,9 @@ public Validator() public record Command : IRequest { - public Guid DocumentId { get; set; } - public Guid BorrowerId { get; set; } - public DateTime BorrowTo { get; set; } + public Guid DocumentId { get; init; } + public Guid BorrowerId { get; init; } + public DateTime BorrowTo { get; init; } public string Reason { get; init; } = null!; } @@ -43,23 +43,32 @@ public CommandHandler(IApplicationDbContext context, IMapper mapper) } public async Task Handle(Command request, CancellationToken cancellationToken) { - var user = await _context.Users.FirstOrDefaultAsync(x => x.Id == request.BorrowerId, cancellationToken); + var user = await _context.Users + .Include(x => x.Department) + .FirstOrDefaultAsync(x => x.Id == request.BorrowerId, cancellationToken); if (user is null) { throw new KeyNotFoundException("User does not exist."); } - var document = await _context.Documents.FirstOrDefaultAsync(x => x.Id == request.DocumentId, cancellationToken); + var document = await _context.Documents + .Include(x => x.Department) + .FirstOrDefaultAsync(x => x.Id == request.DocumentId, cancellationToken); if (document is null) { throw new KeyNotFoundException("Document does not exist."); } + + if (document.Department!.Id != user.Department!.Id) + { + throw new ConflictException("User is not allowed to borrow this document."); + } if (DateTime.Now > request.BorrowTo) { throw new ConflictException("Due date cannot be in the past."); } - + var entity = new Borrow() { Borrower = user, diff --git a/src/Application/Borrows/Queries/GetAllBorrowRequestsPaginated.cs b/src/Application/Borrows/Queries/GetAllBorrowRequestsPaginated.cs new file mode 100644 index 00000000..4c4a3f90 --- /dev/null +++ b/src/Application/Borrows/Queries/GetAllBorrowRequestsPaginated.cs @@ -0,0 +1,140 @@ +using Application.Common.Exceptions; +using Application.Common.Extensions; +using Application.Common.Interfaces; +using Application.Common.Models; +using Application.Common.Models.Dtos.Physical; +using AutoMapper; +using FluentValidation; +using MediatR; +using Microsoft.EntityFrameworkCore; + +namespace Application.Borrows.Queries; + +public class GetAllBorrowRequestsPaginated +{ + public class Validator : AbstractValidator + { + public Validator() + { + RuleLevelCascadeMode = CascadeMode.Stop; + + RuleFor(x => x.RoomId) + .Must((query, roomId) => roomId is null + ? query.LockerId is null && query.FolderId is null + : query.LockerId is not null || query.FolderId is null) + .WithMessage("Container orientation is not consistent"); + } + } + + public record Query : IRequest> + { + public Guid? RoomId { get; init; } + public Guid? LockerId { get; init; } + public Guid? FolderId { get; init; } + public string? SearchTerm { get; init; } + public int? Page { get; init; } + public int? Size { get; init; } + public string? SortBy { get; init; } + public string? SortOrder { get; init; } + } + + public class QueryHandler : IRequestHandler> + { + private readonly IApplicationDbContext _context; + private readonly IMapper _mapper; + + public QueryHandler(IApplicationDbContext context, IMapper mapper) + { + _context = context; + _mapper = mapper; + } + + public async Task> Handle(Query request, + CancellationToken cancellationToken) + { + var borrows = _context.Borrows.AsQueryable(); + var roomExists = request.RoomId is not null; + var lockerExists = request.LockerId is not null; + var folderExists = request.FolderId is not null; + + borrows = borrows + .Include(x => x.Document) + .ThenInclude(y => y.Department) + .Include(x => x.Document) + .ThenInclude(y => y.Folder) + .ThenInclude(z => z!.Locker) + .ThenInclude(t => t.Room) + .ThenInclude(s => s.Department); + + if (folderExists) + { + var folder = await _context.Folders + .Include(x => x.Locker) + .ThenInclude(y => y.Room) + .FirstOrDefaultAsync(x => x.Id == request.FolderId + && x.IsAvailable, cancellationToken); + if (folder is null) + { + throw new KeyNotFoundException("Folder does not exist."); + } + + if (folder.Locker.Id != request.LockerId + || folder.Locker.Room.Id != request.RoomId) + { + throw new ConflictException("Either locker or room does not match folder."); + } + + borrows = borrows + .Where(x => x.Document.Folder!.Id == request.FolderId); + } + else if (lockerExists) + { + var locker = await _context.Lockers + .Include(x => x.Room) + .FirstOrDefaultAsync(x => x.Id == request.LockerId + && x.IsAvailable, cancellationToken); + if (locker is null) + { + throw new KeyNotFoundException("Locker does not exist."); + } + + if (locker.Room.Id != request.RoomId) + { + throw new ConflictException("Room does not match locker."); + } + + borrows = borrows.Where(x => x.Document.Folder!.Locker.Id == request.LockerId); + } + else if (roomExists) + { + var room = await _context.Rooms + .FirstOrDefaultAsync(x => x.Id == request.RoomId + && x.IsAvailable, cancellationToken); + if (room is null) + { + throw new KeyNotFoundException("Room does not exist."); + } + + borrows = borrows.Where(x => x.Document.Folder!.Locker.Room.Id == request.RoomId); + } + + var sortBy = request.SortBy; + if (sortBy is null || !sortBy.MatchesPropertyName()) + { + sortBy = nameof(BorrowDto.Id); + } + var sortOrder = request.SortOrder ?? "asc"; + var pageNumber = request.Page is null or <= 0 ? 1 : request.Page; + var sizeNumber = request.Size is null or <= 0 ? 5 : request.Size; + + var list = await borrows + .Paginate(pageNumber.Value, sizeNumber.Value) + .OrderByCustom(sortBy, sortOrder) + .ToListAsync(cancellationToken); + + var result = _mapper.Map>(list); + + return new PaginatedList(result, result.Count, pageNumber.Value, sizeNumber.Value); + } + } +} \ No newline at end of file diff --git a/src/Application/Documents/Queries/GetAllDocumentsPaginated.cs b/src/Application/Documents/Queries/GetAllDocumentsPaginated.cs index e3695cb9..6b9bd7e2 100644 --- a/src/Application/Documents/Queries/GetAllDocumentsPaginated.cs +++ b/src/Application/Documents/Queries/GetAllDocumentsPaginated.cs @@ -129,9 +129,9 @@ public async Task> Handle(Query request, } var sortBy = request.SortBy; - if (sortBy is null || !sortBy.MatchesPropertyName()) + if (sortBy is null || !sortBy.MatchesPropertyName()) { - sortBy = nameof(LockerDto.Id); + sortBy = nameof(DocumentDto.Id); } var sortOrder = request.SortOrder ?? "asc"; var pageNumber = request.Page is null or <= 0 ? 1 : request.Page; diff --git a/src/Application/Folders/Queries/GetAllFoldersPaginated.cs b/src/Application/Folders/Queries/GetAllFoldersPaginated.cs index 9a4b149f..87394f3c 100644 --- a/src/Application/Folders/Queries/GetAllFoldersPaginated.cs +++ b/src/Application/Folders/Queries/GetAllFoldersPaginated.cs @@ -23,7 +23,7 @@ public Validator() RuleFor(x => x.RoomId) .Must((query, roomId) => roomId is not null || query.LockerId is null); - } + } } public record Query : IRequest> diff --git a/src/Domain/Entities/Physical/Borrow.cs b/src/Domain/Entities/Physical/Borrow.cs index a6f90f9a..9a3a7661 100644 --- a/src/Domain/Entities/Physical/Borrow.cs +++ b/src/Domain/Entities/Physical/Borrow.cs @@ -10,4 +10,5 @@ public class Borrow : BaseEntity public LocalDateTime BorrowTime { get; set; } public LocalDateTime DueTime { get; set; } public string Reason { get; set; } = null!; + public bool IsApproved { get; set; } } \ No newline at end of file diff --git a/tests/Application.Tests.Integration/Documents/Commands/BorrowDocumentTests.cs b/tests/Application.Tests.Integration/Documents/Commands/BorrowDocumentTests.cs index 40b3b51b..1a6ebe3a 100644 --- a/tests/Application.Tests.Integration/Documents/Commands/BorrowDocumentTests.cs +++ b/tests/Application.Tests.Integration/Documents/Commands/BorrowDocumentTests.cs @@ -1,3 +1,4 @@ +using Application.Borrows.Commands; using Application.Common.Exceptions; using Application.Common.Mappings; using Application.Documents.Commands; From b2510d223128573b03b124a49b0df6d15202ab6b Mon Sep 17 00:00:00 2001 From: Nguyen Quang Chien Date: Fri, 2 Jun 2023 16:12:24 +0700 Subject: [PATCH 05/13] update: divide get all borrow requests as admin and staff --- src/Api/ConfigureServices.cs | 3 ++ src/Api/Controllers/BorrowsController.cs | 53 +++++++++++++++---- ...equestsPaginatedAsAdminQueryParameters.cs} | 4 +- ...RequestsPaginatedAsStaffQueryParameters.cs | 16 ++++++ src/Api/Services/CurrentUserService.cs | 43 +++++++++++++++ .../Borrows/Commands/BorrowDocument.cs | 1 + .../Common/Interfaces/ICurrentUserService.cs | 4 ++ .../Configurations/BorrowConfiguration.cs | 3 ++ 8 files changed, 114 insertions(+), 13 deletions(-) rename src/Api/Controllers/Payload/Requests/Borrows/{GetAllBorrowRequestsPaginatedQueryParameters.cs => GetAllBorrowRequestsPaginatedAsAdminQueryParameters.cs} (83%) create mode 100644 src/Api/Controllers/Payload/Requests/Borrows/GetAllBorrowRequestsPaginatedAsStaffQueryParameters.cs diff --git a/src/Api/ConfigureServices.cs b/src/Api/ConfigureServices.cs index 39dabb10..4c4f747f 100644 --- a/src/Api/ConfigureServices.cs +++ b/src/Api/ConfigureServices.cs @@ -1,6 +1,8 @@ using System.Reflection; using Api.Middlewares; using Api.Policies; +using Api.Services; +using Application.Common.Interfaces; using Microsoft.AspNetCore.Mvc.ApplicationModels; using Microsoft.OpenApi.Models; @@ -53,6 +55,7 @@ private static IServiceCollection AddServices(this IServiceCollection services) { // In order for ExceptionMiddleware to work services.AddScoped(); + services.AddScoped(); return services; } diff --git a/src/Api/Controllers/BorrowsController.cs b/src/Api/Controllers/BorrowsController.cs index 48cee17b..77c769f6 100644 --- a/src/Api/Controllers/BorrowsController.cs +++ b/src/Api/Controllers/BorrowsController.cs @@ -1,6 +1,7 @@ using Api.Controllers.Payload.Requests.Borrows; using Application.Borrows.Commands; using Application.Borrows.Queries; +using Application.Common.Interfaces; using Application.Common.Models; using Application.Common.Models.Dtos.Physical; using Application.Identity; @@ -11,6 +12,11 @@ namespace Api.Controllers; public class BorrowsController : ApiControllerBase { + private readonly ICurrentUserService _currentUserService; + public BorrowsController(ICurrentUserService currentUserService) + { + _currentUserService = currentUserService; + } /// /// Borrow a document request /// @@ -35,25 +41,50 @@ public async Task>> BorrowDocument([FromBody] Bor return Ok(Result.Succeed(result)); } - [RequiresRole(IdentityData.Roles.Admin, IdentityData.Roles.Staff)] + [RequiresRole(IdentityData.Roles.Staff)] + [HttpGet("staff")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesResponseType(StatusCodes.Status409Conflict)] + public async Task>>> GetAllBorrowRequestsAsStaffPaginated( + [FromQuery] GetAllBorrowRequestsPaginatedAsStaffQueryParameters asAdminQueryParameters) + { + var roomId = _currentUserService.GetCurrentRoomForStaff(); + var command = new GetAllBorrowRequestsPaginated.Query() + { + RoomId = roomId, + LockerId = asAdminQueryParameters.LockerId, + FolderId = asAdminQueryParameters.FolderId, + SearchTerm = asAdminQueryParameters.SearchTerm, + Page = asAdminQueryParameters.Page, + Size = asAdminQueryParameters.Size, + SortBy = asAdminQueryParameters.SortBy, + SortOrder = asAdminQueryParameters.SortOrder, + }; + var result = await Mediator.Send(command); + return Ok(Result>.Succeed(result)); + } + + [RequiresRole(IdentityData.Roles.Admin)] [HttpGet] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status403Forbidden)] [ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesResponseType(StatusCodes.Status409Conflict)] - public async Task>>> GetAllBorrowRequestsPaginated( - [FromQuery] GetAllBorrowRequestsPaginatedQueryParameters queryParameters) + public async Task>>> GetAllBorrowRequestsAsAdminPaginated( + [FromQuery] GetAllBorrowRequestsPaginatedAsAdminQueryParameters asAdminQueryParameters) { var command = new GetAllBorrowRequestsPaginated.Query() { - RoomId = queryParameters.RoomId, - LockerId = queryParameters.LockerId, - FolderId = queryParameters.FolderId, - SearchTerm = queryParameters.SearchTerm, - Page = queryParameters.Page, - Size = queryParameters.Size, - SortBy = queryParameters.SortBy, - SortOrder = queryParameters.SortOrder, + RoomId = asAdminQueryParameters.RoomId, + LockerId = asAdminQueryParameters.LockerId, + FolderId = asAdminQueryParameters.FolderId, + SearchTerm = asAdminQueryParameters.SearchTerm, + Page = asAdminQueryParameters.Page, + Size = asAdminQueryParameters.Size, + SortBy = asAdminQueryParameters.SortBy, + SortOrder = asAdminQueryParameters.SortOrder, }; var result = await Mediator.Send(command); return Ok(Result>.Succeed(result)); diff --git a/src/Api/Controllers/Payload/Requests/Borrows/GetAllBorrowRequestsPaginatedQueryParameters.cs b/src/Api/Controllers/Payload/Requests/Borrows/GetAllBorrowRequestsPaginatedAsAdminQueryParameters.cs similarity index 83% rename from src/Api/Controllers/Payload/Requests/Borrows/GetAllBorrowRequestsPaginatedQueryParameters.cs rename to src/Api/Controllers/Payload/Requests/Borrows/GetAllBorrowRequestsPaginatedAsAdminQueryParameters.cs index 961fbb80..bf925811 100644 --- a/src/Api/Controllers/Payload/Requests/Borrows/GetAllBorrowRequestsPaginatedQueryParameters.cs +++ b/src/Api/Controllers/Payload/Requests/Borrows/GetAllBorrowRequestsPaginatedAsAdminQueryParameters.cs @@ -1,9 +1,9 @@ namespace Api.Controllers.Payload.Requests.Borrows; /// -/// Query parameters for getting all borrow requests with pagination +/// Query parameters for getting all borrow requests with pagination as admin /// -public class GetAllBorrowRequestsPaginatedQueryParameters : PaginatedQueryParameters +public class GetAllBorrowRequestsPaginatedAsAdminQueryParameters : PaginatedQueryParameters { /// /// Id of the room to retrieve borrow requests in diff --git a/src/Api/Controllers/Payload/Requests/Borrows/GetAllBorrowRequestsPaginatedAsStaffQueryParameters.cs b/src/Api/Controllers/Payload/Requests/Borrows/GetAllBorrowRequestsPaginatedAsStaffQueryParameters.cs new file mode 100644 index 00000000..bfbc512e --- /dev/null +++ b/src/Api/Controllers/Payload/Requests/Borrows/GetAllBorrowRequestsPaginatedAsStaffQueryParameters.cs @@ -0,0 +1,16 @@ +namespace Api.Controllers.Payload.Requests.Borrows; + +/// +/// Query parameters for getting all borrow requests with pagination as staff +/// +public class GetAllBorrowRequestsPaginatedAsStaffQueryParameters : PaginatedQueryParameters +{ + /// + /// Id of the locker to retrieve borrow requests in + /// + public Guid? LockerId { get; set; } + /// + /// Id of the folder to retrieve borrow requests in + /// + public Guid? FolderId { get; set; } +} \ No newline at end of file diff --git a/src/Api/Services/CurrentUserService.cs b/src/Api/Services/CurrentUserService.cs index d8b6c481..d690dcbd 100644 --- a/src/Api/Services/CurrentUserService.cs +++ b/src/Api/Services/CurrentUserService.cs @@ -1,5 +1,7 @@ using System.IdentityModel.Tokens.Jwt; using Application.Common.Interfaces; +using Domain.Entities; +using Microsoft.EntityFrameworkCore; namespace Api.Services; @@ -51,4 +53,45 @@ public string GetRole() return user.Department?.Name; } + + public User GetCurrentUser() + { + var userName = _httpContextAccessor.HttpContext!.User.Claims + .FirstOrDefault(x => x.Type.Equals(JwtRegisteredClaimNames.Sub)); + if (userName is null) + { + throw new UnauthorizedAccessException(); + } + + var user = _context.Users.FirstOrDefault(x => x.Username.Equals(userName)); + + if (user is null) + { + throw new UnauthorizedAccessException(); + } + + return user; + } + + public Guid? GetCurrentRoomForStaff() + { + var userName = _httpContextAccessor.HttpContext!.User.Claims + .FirstOrDefault(x => x.Type.Equals(JwtRegisteredClaimNames.Sub)); + if (userName is null) + { + throw new UnauthorizedAccessException(); + } + + var staff = _context.Staffs + .Include(x => x.User) + .Include(x => x.Room) + .FirstOrDefault(x => x.User.Username.Equals(userName)); + + if (staff is null) + { + throw new UnauthorizedAccessException(); + } + + return staff.Room!.Id; + } } \ No newline at end of file diff --git a/src/Application/Borrows/Commands/BorrowDocument.cs b/src/Application/Borrows/Commands/BorrowDocument.cs index 1d1c9bf1..0799b5d1 100644 --- a/src/Application/Borrows/Commands/BorrowDocument.cs +++ b/src/Application/Borrows/Commands/BorrowDocument.cs @@ -76,6 +76,7 @@ public async Task Handle(Command request, CancellationToken cancellat BorrowTime = LocalDateTime.FromDateTime(DateTime.Now), DueTime = LocalDateTime.FromDateTime(request.BorrowTo), Reason = request.Reason, + IsApproved = false, }; var result = await _context.Borrows.AddAsync(entity, cancellationToken); diff --git a/src/Application/Common/Interfaces/ICurrentUserService.cs b/src/Application/Common/Interfaces/ICurrentUserService.cs index ae870b98..1a4715f9 100644 --- a/src/Application/Common/Interfaces/ICurrentUserService.cs +++ b/src/Application/Common/Interfaces/ICurrentUserService.cs @@ -1,7 +1,11 @@ +using Domain.Entities; + namespace Application.Common.Interfaces; public interface ICurrentUserService { string GetRole(); string? GetDepartment(); + User GetCurrentUser(); + Guid? GetCurrentRoomForStaff(); } \ No newline at end of file diff --git a/src/Infrastructure/Persistence/Configurations/BorrowConfiguration.cs b/src/Infrastructure/Persistence/Configurations/BorrowConfiguration.cs index 88bd292f..ca0841a7 100644 --- a/src/Infrastructure/Persistence/Configurations/BorrowConfiguration.cs +++ b/src/Infrastructure/Persistence/Configurations/BorrowConfiguration.cs @@ -30,5 +30,8 @@ public void Configure(EntityTypeBuilder builder) builder.Property(x => x.Reason) .IsRequired(); + + builder.Property(x => x.IsApproved) + .IsRequired(); } } \ No newline at end of file From 46b6b8570deed1a5c7ca901424e75e7e4742d32b Mon Sep 17 00:00:00 2001 From: Nguyen Quang Chien Date: Sat, 3 Jun 2023 07:37:33 +0700 Subject: [PATCH 06/13] just update my progress here --- ...uestsPaginatedAsEmployeeQueryParameters.cs | 6 ++ .../Borrows/Commands/ApproveBorrowRequest.cs | 51 +++++++++++++ .../Borrows/Commands/CheckoutDocument.cs | 6 ++ .../Borrows/Commands/RejectBorrowRequest.cs | 6 ++ .../GetAllBorrowRequestsSpecificPaginated.cs | 73 +++++++++++++++++++ src/Domain/Statuses/BorrowRequestStatus.cs | 6 ++ src/Domain/Statuses/DocumentStatus.cs | 6 ++ 7 files changed, 154 insertions(+) create mode 100644 src/Api/Controllers/Payload/Requests/Borrows/GetAllBorrowRequestsPaginatedAsEmployeeQueryParameters.cs create mode 100644 src/Application/Borrows/Commands/ApproveBorrowRequest.cs create mode 100644 src/Application/Borrows/Commands/CheckoutDocument.cs create mode 100644 src/Application/Borrows/Commands/RejectBorrowRequest.cs create mode 100644 src/Application/Borrows/Queries/GetAllBorrowRequestsSpecificPaginated.cs create mode 100644 src/Domain/Statuses/BorrowRequestStatus.cs create mode 100644 src/Domain/Statuses/DocumentStatus.cs diff --git a/src/Api/Controllers/Payload/Requests/Borrows/GetAllBorrowRequestsPaginatedAsEmployeeQueryParameters.cs b/src/Api/Controllers/Payload/Requests/Borrows/GetAllBorrowRequestsPaginatedAsEmployeeQueryParameters.cs new file mode 100644 index 00000000..465033e2 --- /dev/null +++ b/src/Api/Controllers/Payload/Requests/Borrows/GetAllBorrowRequestsPaginatedAsEmployeeQueryParameters.cs @@ -0,0 +1,6 @@ +namespace Api.Controllers.Payload.Requests.Borrows; + +public class GetAllBorrowRequestsPaginatedAsEmployeeQueryParameters +{ + +} \ No newline at end of file diff --git a/src/Application/Borrows/Commands/ApproveBorrowRequest.cs b/src/Application/Borrows/Commands/ApproveBorrowRequest.cs new file mode 100644 index 00000000..171955b9 --- /dev/null +++ b/src/Application/Borrows/Commands/ApproveBorrowRequest.cs @@ -0,0 +1,51 @@ +using Application.Common.Exceptions; +using Application.Common.Interfaces; +using Application.Common.Models.Dtos.Physical; +using AutoMapper; +using Domain.Entities.Physical; +using MediatR; +using Microsoft.EntityFrameworkCore; +using NodaTime; + +namespace Application.Borrows.Commands; + +public class ApproveRequest +{ + public record Command : IRequest + { + public Guid BorrowId { get; init; } + } + + public class CommandHandler : IRequestHandler + { + private readonly IApplicationDbContext _context; + private readonly IMapper _mapper; + + public CommandHandler(IApplicationDbContext context, IMapper mapper) + { + _context = context; + _mapper = mapper; + } + public async Task Handle(Command request, CancellationToken cancellationToken) + { + var borrowRequest = await _context.Borrows + .Include(x => x.Borrower) + .Include(x => x.Document) + .FirstOrDefaultAsync(x => x.Id == request.BorrowId, cancellationToken); + if (borrowRequest is null) + { + throw new KeyNotFoundException("Borrow request does not exist."); + } + + if (borrowRequest.IsApproved) + { + throw new ConflictException("Borrow request is already approved."); + } + + borrowRequest.IsApproved = true; + var result = _context.Borrows.Update(borrowRequest); + await _context.SaveChangesAsync(cancellationToken); + return _mapper.Map(result.Entity); + } + } +} \ No newline at end of file diff --git a/src/Application/Borrows/Commands/CheckoutDocument.cs b/src/Application/Borrows/Commands/CheckoutDocument.cs new file mode 100644 index 00000000..4e218b70 --- /dev/null +++ b/src/Application/Borrows/Commands/CheckoutDocument.cs @@ -0,0 +1,6 @@ +namespace Application.Borrows.Commands; + +public class CheckoutDocument +{ + +} \ No newline at end of file diff --git a/src/Application/Borrows/Commands/RejectBorrowRequest.cs b/src/Application/Borrows/Commands/RejectBorrowRequest.cs new file mode 100644 index 00000000..ecb35870 --- /dev/null +++ b/src/Application/Borrows/Commands/RejectBorrowRequest.cs @@ -0,0 +1,6 @@ +namespace Application.Borrows.Commands; + +public class RejectBorrowRequest +{ + +} \ No newline at end of file diff --git a/src/Application/Borrows/Queries/GetAllBorrowRequestsSpecificPaginated.cs b/src/Application/Borrows/Queries/GetAllBorrowRequestsSpecificPaginated.cs new file mode 100644 index 00000000..87d7d480 --- /dev/null +++ b/src/Application/Borrows/Queries/GetAllBorrowRequestsSpecificPaginated.cs @@ -0,0 +1,73 @@ +using Application.Common.Extensions; +using Application.Common.Interfaces; +using Application.Common.Models; +using Application.Common.Models.Dtos.Physical; +using AutoMapper; +using MediatR; +using Microsoft.EntityFrameworkCore; + +namespace Application.Borrows.Queries; + +public class GetAllBorrowRequestsForDocumentPaginated +{ + public record Query : IRequest> + { + public Guid? DocumentId { get; set; } + public Guid? EmployeeId { get; set; } + public int? Page { get; init; } + public int? Size { get; init; } + public string? SortBy { get; init; } + public string? SortOrder { get; init; } + } + + public class QueryHandler : IRequestHandler> + { + private readonly IApplicationDbContext _context; + private readonly IMapper _mapper; + + public QueryHandler(IApplicationDbContext context, IMapper mapper) + { + _context = context; + _mapper = mapper; + } + + public async Task> Handle(Query request, + CancellationToken cancellationToken) + { + var borrows = _context.Borrows.AsQueryable(); + + borrows = borrows + .Include(x => x.Borrower) + .Include(x => x.Document) + .ThenInclude(y => y.Department) + .Include(x => x.Document) + .ThenInclude(y => y.Folder) + .ThenInclude(z => z!.Locker) + .ThenInclude(t => t.Room) + .ThenInclude(s => s.Department); + + if (request.EmployeeId is not null) + { + borrows = borrows.Where(x => x.Borrower.Id == request.EmployeeId); + } + + var sortBy = request.SortBy; + if (sortBy is null || !sortBy.MatchesPropertyName()) + { + sortBy = nameof(BorrowDto.Status); + } + var sortOrder = request.SortOrder ?? "asc"; + var pageNumber = request.Page is null or <= 0 ? 1 : request.Page; + var sizeNumber = request.Size is null or <= 0 ? 5 : request.Size; + + var list = await borrows + .Paginate(pageNumber.Value, sizeNumber.Value) + .OrderByCustom(sortBy, sortOrder) + .ToListAsync(cancellationToken); + + var result = _mapper.Map>(list); + + return new PaginatedList(result, result.Count, pageNumber.Value, sizeNumber.Value); + } + } +} \ No newline at end of file diff --git a/src/Domain/Statuses/BorrowRequestStatus.cs b/src/Domain/Statuses/BorrowRequestStatus.cs new file mode 100644 index 00000000..37367dc9 --- /dev/null +++ b/src/Domain/Statuses/BorrowRequestStatus.cs @@ -0,0 +1,6 @@ +namespace Domain.Statuses; + +public class BorrowRequestStatus +{ + +} \ No newline at end of file diff --git a/src/Domain/Statuses/DocumentStatus.cs b/src/Domain/Statuses/DocumentStatus.cs new file mode 100644 index 00000000..a83a3e47 --- /dev/null +++ b/src/Domain/Statuses/DocumentStatus.cs @@ -0,0 +1,6 @@ +namespace Domain.Statuses; + +public class DocumentStatus +{ + +} \ No newline at end of file From 6b6c381cfb2b1c4a032585b3b042fc054578e75d Mon Sep 17 00:00:00 2001 From: Nguyen Quang Chien Date: Sat, 3 Jun 2023 07:37:47 +0700 Subject: [PATCH 07/13] update once more? --- src/Api/Controllers/BorrowsController.cs | 145 +++++- .../Requests/Borrows/BorrowDocumentRequest.cs | 4 +- ...RequestsPaginatedAsAdminQueryParameters.cs | 12 +- ...uestsPaginatedAsEmployeeQueryParameters.cs | 7 +- ...RequestsPaginatedAsStaffQueryParameters.cs | 8 - ...GetAllDocumentsPaginatedQueryParameters.cs | 4 + .../GetAllFoldersPaginatedQueryParameters.cs | 4 + .../GetAllLockersPaginatedQueryParameters.cs | 4 + .../Requests/PaginatedQueryParameters.cs | 4 - .../GetAllRoomsPaginatedQueryParameters.cs | 4 + .../GetAllStaffsPaginatedQueryParameters.cs | 18 +- .../GetAllUsersPaginatedQueryParameters.cs | 4 + src/Api/Services/CurrentUserService.cs | 28 +- .../Borrows/Commands/ApproveBorrowRequest.cs | 43 +- .../Borrows/Commands/BorrowDocument.cs | 62 ++- .../Borrows/Commands/RejectBorrowRequest.cs | 44 ++ .../Queries/GetAllBorrowRequestsPaginated.cs | 70 +-- .../GetAllBorrowRequestsSpecificPaginated.cs | 2 +- .../Common/Interfaces/ICurrentUserService.cs | 1 + .../Common/Models/Dtos/Physical/BorrowDto.cs | 18 +- .../Models/Dtos/Physical/DocumentDto.cs | 9 + .../Documents/Commands/ImportDocument.cs | 4 +- src/Domain/Entities/Physical/Borrow.cs | 4 +- src/Domain/Entities/Physical/Document.cs | 2 + src/Domain/Statuses/BorrowRequestStatus.cs | 12 +- src/Domain/Statuses/DocumentStatus.cs | 7 +- .../Configurations/BorrowConfiguration.cs | 2 +- .../Configurations/DocumentConfiguration.cs | 3 + .../20230602224631_UpdateBorrow.Designer.cs | 486 ++++++++++++++++++ .../Migrations/20230602224631_UpdateBorrow.cs | 41 ++ .../ApplicationDbContextModelSnapshot.cs | 9 + src/Infrastructure/Services/MailService.cs | 2 +- .../Documents/Commands/BorrowDocumentTests.cs | 4 +- 33 files changed, 910 insertions(+), 161 deletions(-) create mode 100644 src/Infrastructure/Persistence/Migrations/20230602224631_UpdateBorrow.Designer.cs create mode 100644 src/Infrastructure/Persistence/Migrations/20230602224631_UpdateBorrow.cs diff --git a/src/Api/Controllers/BorrowsController.cs b/src/Api/Controllers/BorrowsController.cs index 77c769f6..e2add571 100644 --- a/src/Api/Controllers/BorrowsController.cs +++ b/src/Api/Controllers/BorrowsController.cs @@ -30,10 +30,12 @@ public BorrowsController(ICurrentUserService currentUserService) [ProducesResponseType(StatusCodes.Status409Conflict)] public async Task>> BorrowDocument([FromBody] BorrowDocumentRequest request) { + var borrowerId = _currentUserService.GetCurrentUser().Id; var command = new BorrowDocument.Command() { - BorrowerId = request.BorrowerId, + BorrowerId = borrowerId, DocumentId = request.DocumentId, + BorrowFrom = request.BorrowFrom, BorrowTo = request.BorrowTo, Reason = request.Reason, }; @@ -42,51 +44,148 @@ public async Task>> BorrowDocument([FromBody] Bor } [RequiresRole(IdentityData.Roles.Staff)] - [HttpGet("staff")] + [HttpGet("staffs")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status403Forbidden)] [ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesResponseType(StatusCodes.Status409Conflict)] - public async Task>>> GetAllBorrowRequestsAsStaffPaginated( - [FromQuery] GetAllBorrowRequestsPaginatedAsStaffQueryParameters asAdminQueryParameters) + public async Task>>> GetAllRequestsAsStaffPaginated( + [FromQuery] GetAllBorrowRequestsPaginatedAsStaffQueryParameters queryParameters) { - var roomId = _currentUserService.GetCurrentRoomForStaff(); + var departmentId = _currentUserService.GetCurrentDepartmentForStaff(); var command = new GetAllBorrowRequestsPaginated.Query() { - RoomId = roomId, - LockerId = asAdminQueryParameters.LockerId, - FolderId = asAdminQueryParameters.FolderId, - SearchTerm = asAdminQueryParameters.SearchTerm, - Page = asAdminQueryParameters.Page, - Size = asAdminQueryParameters.Size, - SortBy = asAdminQueryParameters.SortBy, - SortOrder = asAdminQueryParameters.SortOrder, + DepartmentId = departmentId, + Page = queryParameters.Page, + Size = queryParameters.Size, + SortBy = queryParameters.SortBy, + SortOrder = queryParameters.SortOrder, }; var result = await Mediator.Send(command); return Ok(Result>.Succeed(result)); } + /// + /// Get all borrow requests as admin paginated + /// + /// + /// [RequiresRole(IdentityData.Roles.Admin)] [HttpGet] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status403Forbidden)] [ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesResponseType(StatusCodes.Status409Conflict)] - public async Task>>> GetAllBorrowRequestsAsAdminPaginated( - [FromQuery] GetAllBorrowRequestsPaginatedAsAdminQueryParameters asAdminQueryParameters) + public async Task>>> GetAllRequestsAsAdminPaginated( + [FromQuery] GetAllBorrowRequestsPaginatedAsAdminQueryParameters queryParameters) { var command = new GetAllBorrowRequestsPaginated.Query() { - RoomId = asAdminQueryParameters.RoomId, - LockerId = asAdminQueryParameters.LockerId, - FolderId = asAdminQueryParameters.FolderId, - SearchTerm = asAdminQueryParameters.SearchTerm, - Page = asAdminQueryParameters.Page, - Size = asAdminQueryParameters.Size, - SortBy = asAdminQueryParameters.SortBy, - SortOrder = asAdminQueryParameters.SortOrder, + DepartmentId = queryParameters.DepartmentId, + Page = queryParameters.Page, + Size = queryParameters.Size, + SortBy = queryParameters.SortBy, + SortOrder = queryParameters.SortOrder, + }; + var result = await Mediator.Send(command); + return Ok(Result>.Succeed(result)); + } + + /// + /// Get all borrow requests as admin paginated + /// + /// + /// + [RequiresRole(IdentityData.Roles.Employee)] + [HttpGet("employees")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesResponseType(StatusCodes.Status409Conflict)] + public async Task>>> GetAllRequestsAsEmployeePaginated( + [FromQuery] GetAllBorrowRequestsPaginatedAsEmployeeQueryParameters queryParameters) + { + var userId = _currentUserService.GetCurrentUser().Id; + var command = new GetAllBorrowRequestsSpecificPaginated.Query() + { + EmployeeId = userId, + DocumentId = queryParameters.DocumentId, + Page = queryParameters.Page, + Size = queryParameters.Size, + SortBy = queryParameters.SortBy, + SortOrder = queryParameters.SortOrder, + }; + var result = await Mediator.Send(command); + return Ok(Result>.Succeed(result)); + } + + /// + /// Get all borrow requests for a document paginated + /// + /// + /// + /// + [RequiresRole(IdentityData.Roles.Employee)] + [HttpGet("documents/{documentId:guid}")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesResponseType(StatusCodes.Status409Conflict)] + public async Task>>> GetAllRequestsForDocumentPaginated( + [FromRoute] Guid documentId, + [FromQuery] GetAllBorrowRequestsPaginatedAsEmployeeQueryParameters queryParameters) + { + var command = new GetAllBorrowRequestsSpecificPaginated.Query() + { + DocumentId = documentId, + Page = queryParameters.Page, + Size = queryParameters.Size, + SortBy = queryParameters.SortBy, + SortOrder = queryParameters.SortOrder, }; var result = await Mediator.Send(command); return Ok(Result>.Succeed(result)); } + + /// + /// Approve a borrow request + /// + /// Id of the borrow request to be approved + /// A BorrowDto of the approved borrow request + [RequiresRole(IdentityData.Roles.Staff)] + [HttpPost("approve/{borrowId:guid}")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesResponseType(StatusCodes.Status409Conflict)] + public async Task>> ApproveRequest([FromRoute] Guid borrowId) + { + var command = new ApproveBorrowRequest.Command() + { + BorrowId = borrowId, + }; + var result = await Mediator.Send(command); + return Ok(Result.Succeed(result)); + } + + /// + /// Reject a borrow request + /// + /// Id of the borrow request to be rejected + /// A BorrowDto of the rejected borrow request + [RequiresRole(IdentityData.Roles.Staff)] + [HttpPost("reject/{borrowId:guid}")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesResponseType(StatusCodes.Status409Conflict)] + public async Task>> RejectRequest([FromRoute] Guid borrowId) + { + var command = new RejectBorrowRequest.Command() + { + BorrowId = borrowId, + }; + var result = await Mediator.Send(command); + return Ok(Result.Succeed(result)); + } } \ No newline at end of file diff --git a/src/Api/Controllers/Payload/Requests/Borrows/BorrowDocumentRequest.cs b/src/Api/Controllers/Payload/Requests/Borrows/BorrowDocumentRequest.cs index 53acee4f..f3a0085d 100644 --- a/src/Api/Controllers/Payload/Requests/Borrows/BorrowDocumentRequest.cs +++ b/src/Api/Controllers/Payload/Requests/Borrows/BorrowDocumentRequest.cs @@ -10,9 +10,9 @@ public class BorrowDocumentRequest /// public Guid DocumentId { get; set; } /// - /// Id of the borrower + /// Borrow from /// - public Guid BorrowerId { get; set; } + public DateTime BorrowFrom { get; set; } /// /// Borrow to /// diff --git a/src/Api/Controllers/Payload/Requests/Borrows/GetAllBorrowRequestsPaginatedAsAdminQueryParameters.cs b/src/Api/Controllers/Payload/Requests/Borrows/GetAllBorrowRequestsPaginatedAsAdminQueryParameters.cs index bf925811..a2457eaa 100644 --- a/src/Api/Controllers/Payload/Requests/Borrows/GetAllBorrowRequestsPaginatedAsAdminQueryParameters.cs +++ b/src/Api/Controllers/Payload/Requests/Borrows/GetAllBorrowRequestsPaginatedAsAdminQueryParameters.cs @@ -6,15 +6,7 @@ namespace Api.Controllers.Payload.Requests.Borrows; public class GetAllBorrowRequestsPaginatedAsAdminQueryParameters : PaginatedQueryParameters { /// - /// Id of the room to retrieve borrow requests in + /// Id of the department to get borrow requests in /// - public Guid? RoomId { get; set; } - /// - /// Id of the locker to retrieve borrow requests in - /// - public Guid? LockerId { get; set; } - /// - /// Id of the folder to retrieve borrow requests in - /// - public Guid? FolderId { get; set; } + public Guid? DepartmentId { get; set; } } \ No newline at end of file diff --git a/src/Api/Controllers/Payload/Requests/Borrows/GetAllBorrowRequestsPaginatedAsEmployeeQueryParameters.cs b/src/Api/Controllers/Payload/Requests/Borrows/GetAllBorrowRequestsPaginatedAsEmployeeQueryParameters.cs index 465033e2..922651ed 100644 --- a/src/Api/Controllers/Payload/Requests/Borrows/GetAllBorrowRequestsPaginatedAsEmployeeQueryParameters.cs +++ b/src/Api/Controllers/Payload/Requests/Borrows/GetAllBorrowRequestsPaginatedAsEmployeeQueryParameters.cs @@ -1,6 +1,9 @@ namespace Api.Controllers.Payload.Requests.Borrows; -public class GetAllBorrowRequestsPaginatedAsEmployeeQueryParameters +/// +/// Query parameters for getting all borrow requests with pagination as employee +/// +public class GetAllBorrowRequestsPaginatedAsEmployeeQueryParameters : PaginatedQueryParameters { - + public Guid? DocumentId { get; set; } } \ No newline at end of file diff --git a/src/Api/Controllers/Payload/Requests/Borrows/GetAllBorrowRequestsPaginatedAsStaffQueryParameters.cs b/src/Api/Controllers/Payload/Requests/Borrows/GetAllBorrowRequestsPaginatedAsStaffQueryParameters.cs index bfbc512e..de3fcd51 100644 --- a/src/Api/Controllers/Payload/Requests/Borrows/GetAllBorrowRequestsPaginatedAsStaffQueryParameters.cs +++ b/src/Api/Controllers/Payload/Requests/Borrows/GetAllBorrowRequestsPaginatedAsStaffQueryParameters.cs @@ -5,12 +5,4 @@ namespace Api.Controllers.Payload.Requests.Borrows; /// public class GetAllBorrowRequestsPaginatedAsStaffQueryParameters : PaginatedQueryParameters { - /// - /// Id of the locker to retrieve borrow requests in - /// - public Guid? LockerId { get; set; } - /// - /// Id of the folder to retrieve borrow requests in - /// - public Guid? FolderId { get; set; } } \ No newline at end of file diff --git a/src/Api/Controllers/Payload/Requests/Documents/GetAllDocumentsPaginatedQueryParameters.cs b/src/Api/Controllers/Payload/Requests/Documents/GetAllDocumentsPaginatedQueryParameters.cs index 52b9016a..7b97fc8f 100644 --- a/src/Api/Controllers/Payload/Requests/Documents/GetAllDocumentsPaginatedQueryParameters.cs +++ b/src/Api/Controllers/Payload/Requests/Documents/GetAllDocumentsPaginatedQueryParameters.cs @@ -17,4 +17,8 @@ public class GetAllDocumentsPaginatedQueryParameters : PaginatedQueryParameters /// Id of the folder to find documents in /// public Guid? FolderId { get; set; } + /// + /// Search term + /// + public string? SearchTerm { get; set; } } \ No newline at end of file diff --git a/src/Api/Controllers/Payload/Requests/Folders/GetAllFoldersPaginatedQueryParameters.cs b/src/Api/Controllers/Payload/Requests/Folders/GetAllFoldersPaginatedQueryParameters.cs index a7815f26..298a30b0 100644 --- a/src/Api/Controllers/Payload/Requests/Folders/GetAllFoldersPaginatedQueryParameters.cs +++ b/src/Api/Controllers/Payload/Requests/Folders/GetAllFoldersPaginatedQueryParameters.cs @@ -5,6 +5,10 @@ namespace Api.Controllers.Payload.Requests.Folders; /// public class GetAllFoldersPaginatedQueryParameters : PaginatedQueryParameters { + /// + /// Search term + /// + public string? SearchTerm { get; set; } /// /// Id of the room to find folders in /// diff --git a/src/Api/Controllers/Payload/Requests/Lockers/GetAllLockersPaginatedQueryParameters.cs b/src/Api/Controllers/Payload/Requests/Lockers/GetAllLockersPaginatedQueryParameters.cs index 889c3e0d..25372261 100644 --- a/src/Api/Controllers/Payload/Requests/Lockers/GetAllLockersPaginatedQueryParameters.cs +++ b/src/Api/Controllers/Payload/Requests/Lockers/GetAllLockersPaginatedQueryParameters.cs @@ -11,4 +11,8 @@ public class GetAllLockersPaginatedQueryParameters : PaginatedQueryParameters /// Id of the room to find lockers in /// public Guid? RoomId { get; set; } + /// + /// Search term + /// + public string? SearchTerm { get; set; } } \ No newline at end of file diff --git a/src/Api/Controllers/Payload/Requests/PaginatedQueryParameters.cs b/src/Api/Controllers/Payload/Requests/PaginatedQueryParameters.cs index b18143d3..d29c3838 100644 --- a/src/Api/Controllers/Payload/Requests/PaginatedQueryParameters.cs +++ b/src/Api/Controllers/Payload/Requests/PaginatedQueryParameters.cs @@ -2,10 +2,6 @@ namespace Api.Controllers.Payload.Requests; public class PaginatedQueryParameters { - /// - /// Search term - /// - public string? SearchTerm { get; set; } /// /// Page number /// diff --git a/src/Api/Controllers/Payload/Requests/Rooms/GetAllRoomsPaginatedQueryParameters.cs b/src/Api/Controllers/Payload/Requests/Rooms/GetAllRoomsPaginatedQueryParameters.cs index 8ddf2f92..390ae4be 100644 --- a/src/Api/Controllers/Payload/Requests/Rooms/GetAllRoomsPaginatedQueryParameters.cs +++ b/src/Api/Controllers/Payload/Requests/Rooms/GetAllRoomsPaginatedQueryParameters.cs @@ -5,4 +5,8 @@ namespace Api.Controllers.Payload.Requests.Rooms; /// public class GetAllRoomsPaginatedQueryParameters : PaginatedQueryParameters { + /// + /// Search term + /// + public string? SearchTerm { get; set; } } \ No newline at end of file diff --git a/src/Api/Controllers/Payload/Requests/Staffs/GetAllStaffsPaginatedQueryParameters.cs b/src/Api/Controllers/Payload/Requests/Staffs/GetAllStaffsPaginatedQueryParameters.cs index 53fcbb87..323ec479 100644 --- a/src/Api/Controllers/Payload/Requests/Staffs/GetAllStaffsPaginatedQueryParameters.cs +++ b/src/Api/Controllers/Payload/Requests/Staffs/GetAllStaffsPaginatedQueryParameters.cs @@ -3,26 +3,10 @@ namespace Api.Controllers.Payload.Requests.Staffs; /// /// Query parameters for getting all staffs with pagination /// -public class GetAllStaffsPaginatedQueryParameters +public class GetAllStaffsPaginatedQueryParameters : PaginatedQueryParameters { /// /// Search term /// public string? SearchTerm { get; set; } - /// - /// Page number - /// - public int? Page { get; set; } - /// - /// Size number - /// - public int? Size { get; set; } - /// - /// Sort criteria - /// - public string? SortBy { get; set; } - /// - /// Sort direction - /// - public string? SortOrder { get; set; } } \ No newline at end of file diff --git a/src/Api/Controllers/Payload/Requests/Users/GetAllUsersPaginatedQueryParameters.cs b/src/Api/Controllers/Payload/Requests/Users/GetAllUsersPaginatedQueryParameters.cs index 50d5683f..47db40c8 100644 --- a/src/Api/Controllers/Payload/Requests/Users/GetAllUsersPaginatedQueryParameters.cs +++ b/src/Api/Controllers/Payload/Requests/Users/GetAllUsersPaginatedQueryParameters.cs @@ -9,4 +9,8 @@ public class GetAllUsersPaginatedQueryParameters : PaginatedQueryParameters /// Id of the department to find users in /// public Guid? DepartmentId { get; set; } + /// + /// Search term + /// + public string? SearchTerm { get; set; } } \ No newline at end of file diff --git a/src/Api/Services/CurrentUserService.cs b/src/Api/Services/CurrentUserService.cs index d690dcbd..5a808885 100644 --- a/src/Api/Services/CurrentUserService.cs +++ b/src/Api/Services/CurrentUserService.cs @@ -19,7 +19,7 @@ public CurrentUserService(IHttpContextAccessor httpContextAccessor, IApplication public string GetRole() { var userName = _httpContextAccessor.HttpContext!.User.Claims - .FirstOrDefault(x => x.Type.Equals(JwtRegisteredClaimNames.Sub)); + .FirstOrDefault(x => x.Type.Equals("http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier"))!.Value; if (userName is null) { throw new UnauthorizedAccessException(); @@ -57,7 +57,7 @@ public string GetRole() public User GetCurrentUser() { var userName = _httpContextAccessor.HttpContext!.User.Claims - .FirstOrDefault(x => x.Type.Equals(JwtRegisteredClaimNames.Sub)); + .FirstOrDefault(x => x.Type.Equals("http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier"))!.Value; if (userName is null) { throw new UnauthorizedAccessException(); @@ -76,7 +76,7 @@ public User GetCurrentUser() public Guid? GetCurrentRoomForStaff() { var userName = _httpContextAccessor.HttpContext!.User.Claims - .FirstOrDefault(x => x.Type.Equals(JwtRegisteredClaimNames.Sub)); + .FirstOrDefault(x => x.Type.Equals("http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier"))!.Value; if (userName is null) { throw new UnauthorizedAccessException(); @@ -94,4 +94,26 @@ public User GetCurrentUser() return staff.Room!.Id; } + + public Guid? GetCurrentDepartmentForStaff() + { + var userName = _httpContextAccessor.HttpContext!.User.Claims + .FirstOrDefault(x => x.Type.Equals("http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier"))!.Value; + if (userName is null) + { + throw new UnauthorizedAccessException(); + } + + var staff = _context.Staffs + .Include(x => x.User) + .Include(x => x.Room) + .FirstOrDefault(x => x.User.Username.Equals(userName)); + + if (staff is null) + { + throw new UnauthorizedAccessException(); + } + + return staff.Room!.DepartmentId; + } } \ No newline at end of file diff --git a/src/Application/Borrows/Commands/ApproveBorrowRequest.cs b/src/Application/Borrows/Commands/ApproveBorrowRequest.cs index 171955b9..526e94fc 100644 --- a/src/Application/Borrows/Commands/ApproveBorrowRequest.cs +++ b/src/Application/Borrows/Commands/ApproveBorrowRequest.cs @@ -2,30 +2,31 @@ using Application.Common.Interfaces; using Application.Common.Models.Dtos.Physical; using AutoMapper; -using Domain.Entities.Physical; +using Domain.Statuses; using MediatR; using Microsoft.EntityFrameworkCore; using NodaTime; namespace Application.Borrows.Commands; -public class ApproveRequest +public class ApproveBorrowRequest { public record Command : IRequest { public Guid BorrowId { get; init; } } - + public class CommandHandler : IRequestHandler { private readonly IApplicationDbContext _context; private readonly IMapper _mapper; - + public CommandHandler(IApplicationDbContext context, IMapper mapper) { _context = context; _mapper = mapper; } + public async Task Handle(Command request, CancellationToken cancellationToken) { var borrowRequest = await _context.Borrows @@ -37,12 +38,40 @@ public async Task Handle(Command request, CancellationToken cancellat throw new KeyNotFoundException("Borrow request does not exist."); } - if (borrowRequest.IsApproved) + if (borrowRequest.Document.Status is DocumentStatus.Lost) { - throw new ConflictException("Borrow request is already approved."); + borrowRequest.Status = BorrowRequestStatus.NotProcessable; + _context.Borrows.Update(borrowRequest); + await _context.SaveChangesAsync(cancellationToken); + throw new ConflictException("Document is lost. Request is unprocessable."); } - borrowRequest.IsApproved = true; + if (borrowRequest.Status is not BorrowRequestStatus.Pending + && borrowRequest.Status is not BorrowRequestStatus.Rejected) + { + throw new ConflictException("Request cannot be approved."); + } + + var localDateTimeNow = LocalDateTime.FromDateTime(DateTime.Now); + var existedBorrow = await _context.Borrows + .FirstOrDefaultAsync(x => + x.Document.Id == borrowRequest.Document.Id + && ((x.DueTime > localDateTimeNow + && x.BorrowTime < localDateTimeNow) + || x.Status == BorrowRequestStatus.Overdue), cancellationToken); + + if (existedBorrow is not null) + { + if (existedBorrow?.Status + is BorrowRequestStatus.Approved + or BorrowRequestStatus.CheckedOut + && borrowRequest.BorrowTime < existedBorrow.DueTime) + { + throw new ConflictException("This document cannot be borrowed."); + } + } + + borrowRequest.Status = BorrowRequestStatus.Approved; var result = _context.Borrows.Update(borrowRequest); await _context.SaveChangesAsync(cancellationToken); return _mapper.Map(result.Entity); diff --git a/src/Application/Borrows/Commands/BorrowDocument.cs b/src/Application/Borrows/Commands/BorrowDocument.cs index 0799b5d1..3c45f0da 100644 --- a/src/Application/Borrows/Commands/BorrowDocument.cs +++ b/src/Application/Borrows/Commands/BorrowDocument.cs @@ -3,6 +3,7 @@ using Application.Common.Models.Dtos.Physical; using AutoMapper; using Domain.Entities.Physical; +using Domain.Statuses; using FluentValidation; using MediatR; using Microsoft.EntityFrameworkCore; @@ -20,6 +21,12 @@ public Validator() RuleFor(x => x.Reason) .MaximumLength(512).WithMessage("Reason cannot exceed 512 characters."); + + RuleFor(x => x.BorrowFrom) + .GreaterThan(DateTime.Now).WithMessage("Borrow date cannot be in the past."); + + RuleFor(x => x.BorrowTo) + .GreaterThan(DateTime.Now).WithMessage("Due date cannot be in the past."); } } @@ -27,20 +34,22 @@ public record Command : IRequest { public Guid DocumentId { get; init; } public Guid BorrowerId { get; init; } + public DateTime BorrowFrom { get; init; } public DateTime BorrowTo { get; init; } public string Reason { get; init; } = null!; } - + public class CommandHandler : IRequestHandler { private readonly IApplicationDbContext _context; private readonly IMapper _mapper; - + public CommandHandler(IApplicationDbContext context, IMapper mapper) { _context = context; _mapper = mapper; } + public async Task Handle(Command request, CancellationToken cancellationToken) { var user = await _context.Users @@ -51,6 +60,16 @@ public async Task Handle(Command request, CancellationToken cancellat throw new KeyNotFoundException("User does not exist."); } + if (user.IsActive is false) + { + throw new ConflictException("User is not active."); + } + + if (user.IsActivated is false) + { + throw new ConflictException("User is not activated."); + } + var document = await _context.Documents .Include(x => x.Department) .FirstOrDefaultAsync(x => x.Id == request.DocumentId, cancellationToken); @@ -59,16 +78,47 @@ public async Task Handle(Command request, CancellationToken cancellat throw new KeyNotFoundException("Document does not exist."); } + if (document.Status is DocumentStatus.Lost) + { + throw new ConflictException("Document is lost."); + } + if (document.Department!.Id != user.Department!.Id) { throw new ConflictException("User is not allowed to borrow this document."); } + + // getting out a request of that document which is either not due or overdue + // if the request is in time, meaning not overdue, + // then check if its due date is less than the borrow request date, if not then check + // if it's already been approved, checked out or lost, meaning + var localDateTimeNow = LocalDateTime.FromDateTime(DateTime.Now); + var existedBorrow = await _context.Borrows + .Include(x => x.Borrower) + .FirstOrDefaultAsync(x => + x.Document.Id == request.DocumentId + && ((x.DueTime > localDateTimeNow + && x.BorrowTime < localDateTimeNow) + || x.Status == BorrowRequestStatus.Overdue), cancellationToken); - if (DateTime.Now > request.BorrowTo) + if (existedBorrow is not null) { - throw new ConflictException("Due date cannot be in the past."); + // Does not make sense if the same person go up and want to borrow the same document again + // even if the borrow day will be after the due day + if (existedBorrow.Borrower.Id == request.BorrowerId) + { + throw new ConflictException("This document is already requested borrow from the same user."); + } + + if (existedBorrow.Status + is BorrowRequestStatus.Approved + or BorrowRequestStatus.CheckedOut + && LocalDateTime.FromDateTime(request.BorrowFrom) < existedBorrow.DueTime) + { + throw new ConflictException("This document cannot be borrowed."); + } } - + var entity = new Borrow() { Borrower = user, @@ -76,7 +126,7 @@ public async Task Handle(Command request, CancellationToken cancellat BorrowTime = LocalDateTime.FromDateTime(DateTime.Now), DueTime = LocalDateTime.FromDateTime(request.BorrowTo), Reason = request.Reason, - IsApproved = false, + Status = BorrowRequestStatus.Pending, }; var result = await _context.Borrows.AddAsync(entity, cancellationToken); diff --git a/src/Application/Borrows/Commands/RejectBorrowRequest.cs b/src/Application/Borrows/Commands/RejectBorrowRequest.cs index ecb35870..e1290e83 100644 --- a/src/Application/Borrows/Commands/RejectBorrowRequest.cs +++ b/src/Application/Borrows/Commands/RejectBorrowRequest.cs @@ -1,6 +1,50 @@ +using Application.Common.Exceptions; +using Application.Common.Interfaces; +using Application.Common.Models.Dtos.Physical; +using AutoMapper; +using Domain.Statuses; +using MediatR; +using Microsoft.EntityFrameworkCore; + namespace Application.Borrows.Commands; public class RejectBorrowRequest { + public record Command : IRequest + { + public Guid BorrowId { get; init; } + } + public class CommandHandler : IRequestHandler + { + private readonly IApplicationDbContext _context; + private readonly IMapper _mapper; + + public CommandHandler(IApplicationDbContext context, IMapper mapper) + { + _context = context; + _mapper = mapper; + } + public async Task Handle(Command request, CancellationToken cancellationToken) + { + var borrowRequest = await _context.Borrows + .Include(x => x.Borrower) + .Include(x => x.Document) + .FirstOrDefaultAsync(x => x.Id == request.BorrowId, cancellationToken); + if (borrowRequest is null) + { + throw new KeyNotFoundException("Borrow request does not exist."); + } + + if (borrowRequest.Status is not BorrowRequestStatus.Pending) + { + throw new ConflictException("Request cannot be rejected."); + } + + borrowRequest.Status = BorrowRequestStatus.Rejected; + var result = _context.Borrows.Update(borrowRequest); + await _context.SaveChangesAsync(cancellationToken); + return _mapper.Map(result.Entity); + } + } } \ No newline at end of file diff --git a/src/Application/Borrows/Queries/GetAllBorrowRequestsPaginated.cs b/src/Application/Borrows/Queries/GetAllBorrowRequestsPaginated.cs index 4c4a3f90..9fd4ccd3 100644 --- a/src/Application/Borrows/Queries/GetAllBorrowRequestsPaginated.cs +++ b/src/Application/Borrows/Queries/GetAllBorrowRequestsPaginated.cs @@ -17,21 +17,12 @@ public class Validator : AbstractValidator public Validator() { RuleLevelCascadeMode = CascadeMode.Stop; - - RuleFor(x => x.RoomId) - .Must((query, roomId) => roomId is null - ? query.LockerId is null && query.FolderId is null - : query.LockerId is not null || query.FolderId is null) - .WithMessage("Container orientation is not consistent"); } } public record Query : IRequest> { - public Guid? RoomId { get; init; } - public Guid? LockerId { get; init; } - public Guid? FolderId { get; init; } - public string? SearchTerm { get; init; } + public Guid? DepartmentId { get; set; } public int? Page { get; init; } public int? Size { get; init; } public string? SortBy { get; init; } @@ -53,11 +44,9 @@ public async Task> Handle(Query request, CancellationToken cancellationToken) { var borrows = _context.Borrows.AsQueryable(); - var roomExists = request.RoomId is not null; - var lockerExists = request.LockerId is not null; - var folderExists = request.FolderId is not null; borrows = borrows + .Include(x => x.Borrower) .Include(x => x.Document) .ThenInclude(y => y.Department) .Include(x => x.Document) @@ -66,62 +55,15 @@ public async Task> Handle(Query request, .ThenInclude(t => t.Room) .ThenInclude(s => s.Department); - if (folderExists) - { - var folder = await _context.Folders - .Include(x => x.Locker) - .ThenInclude(y => y.Room) - .FirstOrDefaultAsync(x => x.Id == request.FolderId - && x.IsAvailable, cancellationToken); - if (folder is null) - { - throw new KeyNotFoundException("Folder does not exist."); - } - - if (folder.Locker.Id != request.LockerId - || folder.Locker.Room.Id != request.RoomId) - { - throw new ConflictException("Either locker or room does not match folder."); - } - - borrows = borrows - .Where(x => x.Document.Folder!.Id == request.FolderId); - } - else if (lockerExists) - { - var locker = await _context.Lockers - .Include(x => x.Room) - .FirstOrDefaultAsync(x => x.Id == request.LockerId - && x.IsAvailable, cancellationToken); - if (locker is null) - { - throw new KeyNotFoundException("Locker does not exist."); - } - - if (locker.Room.Id != request.RoomId) - { - throw new ConflictException("Room does not match locker."); - } - - borrows = borrows.Where(x => x.Document.Folder!.Locker.Id == request.LockerId); - } - else if (roomExists) + if (request.DepartmentId is not null) { - var room = await _context.Rooms - .FirstOrDefaultAsync(x => x.Id == request.RoomId - && x.IsAvailable, cancellationToken); - if (room is null) - { - throw new KeyNotFoundException("Room does not exist."); - } - - borrows = borrows.Where(x => x.Document.Folder!.Locker.Room.Id == request.RoomId); + borrows = borrows.Where(x => x.Document.Department!.Id == request.DepartmentId); } - + var sortBy = request.SortBy; if (sortBy is null || !sortBy.MatchesPropertyName()) { - sortBy = nameof(BorrowDto.Id); + sortBy = nameof(BorrowDto.Status); } var sortOrder = request.SortOrder ?? "asc"; var pageNumber = request.Page is null or <= 0 ? 1 : request.Page; diff --git a/src/Application/Borrows/Queries/GetAllBorrowRequestsSpecificPaginated.cs b/src/Application/Borrows/Queries/GetAllBorrowRequestsSpecificPaginated.cs index 87d7d480..8c9b6172 100644 --- a/src/Application/Borrows/Queries/GetAllBorrowRequestsSpecificPaginated.cs +++ b/src/Application/Borrows/Queries/GetAllBorrowRequestsSpecificPaginated.cs @@ -8,7 +8,7 @@ namespace Application.Borrows.Queries; -public class GetAllBorrowRequestsForDocumentPaginated +public class GetAllBorrowRequestsSpecificPaginated { public record Query : IRequest> { diff --git a/src/Application/Common/Interfaces/ICurrentUserService.cs b/src/Application/Common/Interfaces/ICurrentUserService.cs index 1a4715f9..0c4aed4b 100644 --- a/src/Application/Common/Interfaces/ICurrentUserService.cs +++ b/src/Application/Common/Interfaces/ICurrentUserService.cs @@ -8,4 +8,5 @@ public interface ICurrentUserService string? GetDepartment(); User GetCurrentUser(); Guid? GetCurrentRoomForStaff(); + Guid? GetCurrentDepartmentForStaff(); } \ No newline at end of file diff --git a/src/Application/Common/Models/Dtos/Physical/BorrowDto.cs b/src/Application/Common/Models/Dtos/Physical/BorrowDto.cs index bb171be9..49afc176 100644 --- a/src/Application/Common/Models/Dtos/Physical/BorrowDto.cs +++ b/src/Application/Common/Models/Dtos/Physical/BorrowDto.cs @@ -8,18 +8,28 @@ namespace Application.Common.Models.Dtos.Physical; public class BorrowDto : IMapFrom { public Guid Id { get; set; } - public UserDto Borrower { get; set; } - public DocumentDto Document { get; set; } + public Guid BorrowerId { get; set; } + public Guid DocumentId { get; set; } public DateTime BorrowTime { get; set; } public DateTime DueTime { get; set; } - public string Reason { get; set; } + public DateTime ActualReturnTime { get; set; } + public string Reason { get; set; } = null!; + public string Status { get; set; } = null!; public void Mapping(Profile profile) { profile.CreateMap() + .ForMember(dest => dest.BorrowerId, + opt => opt.MapFrom(src => src.Borrower.Id)) + .ForMember(dest => dest.DocumentId, + opt => opt.MapFrom(src => src.Document.Id)) + .ForMember(dest => dest.Status, + opt => opt.MapFrom(src => src.Status.ToString())) .ForMember(dest => dest.BorrowTime, opt => opt.MapFrom(src => src.BorrowTime.ToDateTimeUnspecified())) .ForMember(dest => dest.DueTime, - opt => opt.MapFrom(src => src.DueTime.ToDateTimeUnspecified())); + opt => opt.MapFrom(src => src.DueTime.ToDateTimeUnspecified())) + .ForMember(dest => dest.ActualReturnTime, + opt => opt.MapFrom(src => src.ActualReturnTime.ToDateTimeUnspecified())); } } \ No newline at end of file diff --git a/src/Application/Common/Models/Dtos/Physical/DocumentDto.cs b/src/Application/Common/Models/Dtos/Physical/DocumentDto.cs index 23f0f4ec..994b3ac8 100644 --- a/src/Application/Common/Models/Dtos/Physical/DocumentDto.cs +++ b/src/Application/Common/Models/Dtos/Physical/DocumentDto.cs @@ -1,5 +1,6 @@ using Application.Common.Mappings; using Application.Users.Queries; +using AutoMapper; using Domain.Entities.Physical; namespace Application.Common.Models.Dtos.Physical; @@ -13,4 +14,12 @@ public class DocumentDto : IMapFrom public DepartmentDto? Department { get; set; } public UserDto? Importer { get; set; } public FolderDto? Folder { get; set; } + public string Status { get; set; } + + public void Mapping(Profile profile) + { + profile.CreateMap() + .ForMember(dest => dest.Status, + opt => opt.MapFrom(src => src.Status.ToString())); + } } \ No newline at end of file diff --git a/src/Application/Documents/Commands/ImportDocument.cs b/src/Application/Documents/Commands/ImportDocument.cs index 16e9c6a7..d33d4942 100644 --- a/src/Application/Documents/Commands/ImportDocument.cs +++ b/src/Application/Documents/Commands/ImportDocument.cs @@ -3,6 +3,7 @@ using Application.Common.Models.Dtos.Physical; using AutoMapper; using Domain.Entities.Physical; +using Domain.Statuses; using MediatR; using Microsoft.EntityFrameworkCore; @@ -68,7 +69,8 @@ public async Task Handle(Command request, CancellationToken cancell DocumentType = request.DocumentType.Trim(), Importer = importer, Department = importer.Department, - Folder = folder + Folder = folder, + Status = DocumentStatus.Issued, }; var result = await _context.Documents.AddAsync(entity, cancellationToken); diff --git a/src/Domain/Entities/Physical/Borrow.cs b/src/Domain/Entities/Physical/Borrow.cs index 9a3a7661..8b3076ac 100644 --- a/src/Domain/Entities/Physical/Borrow.cs +++ b/src/Domain/Entities/Physical/Borrow.cs @@ -1,4 +1,5 @@ using Domain.Common; +using Domain.Statuses; using NodaTime; namespace Domain.Entities.Physical; @@ -9,6 +10,7 @@ public class Borrow : BaseEntity public Document Document { get; set; } = null!; public LocalDateTime BorrowTime { get; set; } public LocalDateTime DueTime { get; set; } + public LocalDateTime ActualReturnTime { get; set; } public string Reason { get; set; } = null!; - public bool IsApproved { get; set; } + public BorrowRequestStatus Status { get; set; } } \ No newline at end of file diff --git a/src/Domain/Entities/Physical/Document.cs b/src/Domain/Entities/Physical/Document.cs index 2729d8ea..81313b56 100644 --- a/src/Domain/Entities/Physical/Document.cs +++ b/src/Domain/Entities/Physical/Document.cs @@ -1,4 +1,5 @@ using Domain.Common; +using Domain.Statuses; namespace Domain.Entities.Physical; @@ -10,4 +11,5 @@ public class Document : BaseEntity public Department? Department { get; set; } public User? Importer { get; set; } public Folder? Folder { get; set; } + public DocumentStatus Status { get; set; } } \ No newline at end of file diff --git a/src/Domain/Statuses/BorrowRequestStatus.cs b/src/Domain/Statuses/BorrowRequestStatus.cs index 37367dc9..15c19a0b 100644 --- a/src/Domain/Statuses/BorrowRequestStatus.cs +++ b/src/Domain/Statuses/BorrowRequestStatus.cs @@ -1,6 +1,14 @@ namespace Domain.Statuses; -public class BorrowRequestStatus +public enum BorrowRequestStatus { - + Approved, + Pending, + Rejected, + Overdue, + Cancelled, + CheckedOut, + Returned, + Lost, + NotProcessable, } \ No newline at end of file diff --git a/src/Domain/Statuses/DocumentStatus.cs b/src/Domain/Statuses/DocumentStatus.cs index a83a3e47..133c1cd1 100644 --- a/src/Domain/Statuses/DocumentStatus.cs +++ b/src/Domain/Statuses/DocumentStatus.cs @@ -1,6 +1,9 @@ namespace Domain.Statuses; -public class DocumentStatus +public enum DocumentStatus { - + Issued, + Available, + Borrowed, + Lost, } \ No newline at end of file diff --git a/src/Infrastructure/Persistence/Configurations/BorrowConfiguration.cs b/src/Infrastructure/Persistence/Configurations/BorrowConfiguration.cs index ca0841a7..78697f26 100644 --- a/src/Infrastructure/Persistence/Configurations/BorrowConfiguration.cs +++ b/src/Infrastructure/Persistence/Configurations/BorrowConfiguration.cs @@ -31,7 +31,7 @@ public void Configure(EntityTypeBuilder builder) builder.Property(x => x.Reason) .IsRequired(); - builder.Property(x => x.IsApproved) + builder.Property(x => x.Status) .IsRequired(); } } \ No newline at end of file diff --git a/src/Infrastructure/Persistence/Configurations/DocumentConfiguration.cs b/src/Infrastructure/Persistence/Configurations/DocumentConfiguration.cs index 79e66f71..4c7033c5 100644 --- a/src/Infrastructure/Persistence/Configurations/DocumentConfiguration.cs +++ b/src/Infrastructure/Persistence/Configurations/DocumentConfiguration.cs @@ -38,5 +38,8 @@ public void Configure(EntityTypeBuilder builder) .WithMany() .HasForeignKey("ImporterId") .IsRequired(false); + + builder.Property(x => x.Status) + .IsRequired(); } } \ No newline at end of file diff --git a/src/Infrastructure/Persistence/Migrations/20230602224631_UpdateBorrow.Designer.cs b/src/Infrastructure/Persistence/Migrations/20230602224631_UpdateBorrow.Designer.cs new file mode 100644 index 00000000..ffae741e --- /dev/null +++ b/src/Infrastructure/Persistence/Migrations/20230602224631_UpdateBorrow.Designer.cs @@ -0,0 +1,486 @@ +// +using System; +using Infrastructure.Persistence; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using NodaTime; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Infrastructure.Persistence.Migrations +{ + [DbContext(typeof(ApplicationDbContext))] + [Migration("20230602224631_UpdateBorrow")] + partial class UpdateBorrow + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "7.0.5") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Domain.Entities.Department", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.HasKey("Id"); + + b.HasAlternateKey("Name"); + + b.ToTable("Departments"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Borrow", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ActualReturnTime") + .HasColumnType("timestamp without time zone"); + + b.Property("BorrowTime") + .HasColumnType("timestamp without time zone"); + + b.Property("BorrowerId") + .HasColumnType("uuid"); + + b.Property("DocumentId") + .HasColumnType("uuid"); + + b.Property("DueTime") + .HasColumnType("timestamp without time zone"); + + b.Property("Reason") + .IsRequired() + .HasColumnType("text"); + + b.Property("Status") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("BorrowerId"); + + b.HasIndex("DocumentId"); + + b.ToTable("Borrows"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Document", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("DepartmentId") + .HasColumnType("uuid"); + + b.Property("Description") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("DocumentType") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("FolderId") + .HasColumnType("uuid"); + + b.Property("ImporterId") + .HasColumnType("uuid"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.HasKey("Id"); + + b.HasIndex("DepartmentId"); + + b.HasIndex("FolderId"); + + b.HasIndex("ImporterId"); + + b.ToTable("Documents"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Folder", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Capacity") + .HasColumnType("integer"); + + b.Property("Description") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("IsAvailable") + .HasColumnType("boolean"); + + b.Property("LockerId") + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("NumberOfDocuments") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("LockerId"); + + b.ToTable("Folders"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Locker", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Capacity") + .HasColumnType("integer"); + + b.Property("Description") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("IsAvailable") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("NumberOfFolders") + .HasColumnType("integer"); + + b.Property("RoomId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("RoomId"); + + b.ToTable("Lockers"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Room", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Capacity") + .HasColumnType("integer"); + + b.Property("DepartmentId") + .HasColumnType("uuid"); + + b.Property("Description") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("IsAvailable") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("NumberOfLockers") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasAlternateKey("Name"); + + b.HasIndex("DepartmentId") + .IsUnique(); + + b.ToTable("Rooms"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Staff", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("UserId"); + + b.Property("RoomId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("RoomId") + .IsUnique(); + + b.ToTable("Staffs"); + }); + + modelBuilder.Entity("Domain.Entities.RefreshToken", b => + { + b.Property("Token") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreationDateTime") + .HasColumnType("timestamp without time zone"); + + b.Property("ExpiryDateTime") + .HasColumnType("timestamp without time zone"); + + b.Property("IsInvalidated") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("IsUsed") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("JwtId") + .IsRequired() + .HasColumnType("text"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Token"); + + b.HasIndex("UserId"); + + b.ToTable("RefreshTokens"); + }); + + modelBuilder.Entity("Domain.Entities.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Created") + .HasColumnType("timestamp without time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("DepartmentId") + .HasColumnType("uuid"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(320) + .HasColumnType("character varying(320)"); + + b.Property("FirstName") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("IsActivated") + .HasColumnType("boolean"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("LastModified") + .HasColumnType("timestamp without time zone"); + + b.Property("LastModifiedBy") + .HasColumnType("uuid"); + + b.Property("LastName") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("PasswordHash") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("Position") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("Role") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("Username") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.HasKey("Id"); + + b.HasIndex("DepartmentId"); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Borrow", b => + { + b.HasOne("Domain.Entities.User", "Borrower") + .WithMany() + .HasForeignKey("BorrowerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Domain.Entities.Physical.Document", "Document") + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Borrower"); + + b.Navigation("Document"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Document", b => + { + b.HasOne("Domain.Entities.Department", "Department") + .WithMany() + .HasForeignKey("DepartmentId"); + + b.HasOne("Domain.Entities.Physical.Folder", "Folder") + .WithMany("Documents") + .HasForeignKey("FolderId"); + + b.HasOne("Domain.Entities.User", "Importer") + .WithMany() + .HasForeignKey("ImporterId"); + + b.Navigation("Department"); + + b.Navigation("Folder"); + + b.Navigation("Importer"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Folder", b => + { + b.HasOne("Domain.Entities.Physical.Locker", "Locker") + .WithMany("Folders") + .HasForeignKey("LockerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Locker"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Locker", b => + { + b.HasOne("Domain.Entities.Physical.Room", "Room") + .WithMany("Lockers") + .HasForeignKey("RoomId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Room"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Room", b => + { + b.HasOne("Domain.Entities.Department", "Department") + .WithOne("Room") + .HasForeignKey("Domain.Entities.Physical.Room", "DepartmentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Department"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Staff", b => + { + b.HasOne("Domain.Entities.User", "User") + .WithOne() + .HasForeignKey("Domain.Entities.Physical.Staff", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Domain.Entities.Physical.Room", "Room") + .WithOne("Staff") + .HasForeignKey("Domain.Entities.Physical.Staff", "RoomId"); + + b.Navigation("Room"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.RefreshToken", b => + { + b.HasOne("Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.User", b => + { + b.HasOne("Domain.Entities.Department", "Department") + .WithMany() + .HasForeignKey("DepartmentId"); + + b.Navigation("Department"); + }); + + modelBuilder.Entity("Domain.Entities.Department", b => + { + b.Navigation("Room"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Folder", b => + { + b.Navigation("Documents"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Locker", b => + { + b.Navigation("Folders"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Room", b => + { + b.Navigation("Lockers"); + + b.Navigation("Staff"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Infrastructure/Persistence/Migrations/20230602224631_UpdateBorrow.cs b/src/Infrastructure/Persistence/Migrations/20230602224631_UpdateBorrow.cs new file mode 100644 index 00000000..5e7277f8 --- /dev/null +++ b/src/Infrastructure/Persistence/Migrations/20230602224631_UpdateBorrow.cs @@ -0,0 +1,41 @@ +using Microsoft.EntityFrameworkCore.Migrations; +using NodaTime; + +#nullable disable + +namespace Infrastructure.Persistence.Migrations +{ + /// + public partial class UpdateBorrow : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "Status", + table: "Documents", + type: "integer", + nullable: false, + defaultValue: 0); + + migrationBuilder.AddColumn( + name: "ActualReturnTime", + table: "Borrows", + type: "timestamp without time zone", + nullable: false, + defaultValue: new NodaTime.LocalDateTime(1, 1, 1, 0, 0)); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "Status", + table: "Documents"); + + migrationBuilder.DropColumn( + name: "ActualReturnTime", + table: "Borrows"); + } + } +} diff --git a/src/Infrastructure/Persistence/Migrations/ApplicationDbContextModelSnapshot.cs b/src/Infrastructure/Persistence/Migrations/ApplicationDbContextModelSnapshot.cs index 49740741..26a343b3 100644 --- a/src/Infrastructure/Persistence/Migrations/ApplicationDbContextModelSnapshot.cs +++ b/src/Infrastructure/Persistence/Migrations/ApplicationDbContextModelSnapshot.cs @@ -47,6 +47,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) .ValueGeneratedOnAdd() .HasColumnType("uuid"); + b.Property("ActualReturnTime") + .HasColumnType("timestamp without time zone"); + b.Property("BorrowTime") .HasColumnType("timestamp without time zone"); @@ -63,6 +66,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) .IsRequired() .HasColumnType("text"); + b.Property("Status") + .HasColumnType("integer"); + b.HasKey("Id"); b.HasIndex("BorrowerId"); @@ -96,6 +102,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("ImporterId") .HasColumnType("uuid"); + b.Property("Status") + .HasColumnType("integer"); + b.Property("Title") .IsRequired() .HasMaxLength(64) diff --git a/src/Infrastructure/Services/MailService.cs b/src/Infrastructure/Services/MailService.cs index 8357655f..3a9c7020 100644 --- a/src/Infrastructure/Services/MailService.cs +++ b/src/Infrastructure/Services/MailService.cs @@ -51,7 +51,7 @@ public bool SendResetPasswordHtmlMail(string userEmail, string password) request.AddHeader("Content-Type", "application/json"); request.AddParameter("application/json", json, ParameterType.RequestBody); var response = client.Execute(request); - + Console.WriteLine(response.Content); return response.IsSuccessStatusCode; } } \ No newline at end of file diff --git a/tests/Application.Tests.Integration/Documents/Commands/BorrowDocumentTests.cs b/tests/Application.Tests.Integration/Documents/Commands/BorrowDocumentTests.cs index 1a6ebe3a..dbfd34a3 100644 --- a/tests/Application.Tests.Integration/Documents/Commands/BorrowDocumentTests.cs +++ b/tests/Application.Tests.Integration/Documents/Commands/BorrowDocumentTests.cs @@ -47,8 +47,8 @@ public async Task ShouldCreateBorrowRequest_WhenBorrowDetailsAreValid() // Assert result.Should().NotBeNull(); - result.Borrower.Id.Should().Be(user.Id); - result.Document.Id.Should().Be(document.Id); + result.BorrowerId.Should().Be(user.Id); + result.DocumentId.Should().Be(document.Id); result.DueTime.Should().Be(command.BorrowTo); result.Reason.Should().Be(command.Reason); From d629e3346d89f89c02854f0e0805e4e00982cfc5 Mon Sep 17 00:00:00 2001 From: Nguyen Quang Chien Date: Sat, 3 Jun 2023 09:33:00 +0700 Subject: [PATCH 08/13] add: base ground for borrow endpoints --- src/Api/Controllers/BorrowsController.cs | 74 +++++++- ...estsPaginatedForDocumentQueryParameters.cs | 6 + .../Requests/Borrows/UpdateBorrowRequest.cs | 8 + .../Borrows/Commands/ApproveBorrowRequest.cs | 4 +- .../Borrows/Commands/BorrowDocument.cs | 6 +- .../Borrows/Commands/CancelBorrowRequest.cs | 51 ++++++ .../Borrows/Commands/CheckoutDocument.cs | 54 +++++- .../Borrows/Commands/ReturnDocument.cs | 61 +++++++ .../Borrows/Commands/UpdateBorrow.cs | 85 +++++++++ .../GetAllBorrowRequestsSpecificPaginated.cs | 5 + .../Common/Extensions/QueryableExtensions.cs | 2 +- .../Documents/Commands/BorrowDocumentTests.cs | 168 ------------------ 12 files changed, 347 insertions(+), 177 deletions(-) create mode 100644 src/Api/Controllers/Payload/Requests/Borrows/GetAllBorrowRequestsPaginatedForDocumentQueryParameters.cs create mode 100644 src/Api/Controllers/Payload/Requests/Borrows/UpdateBorrowRequest.cs create mode 100644 src/Application/Borrows/Commands/CancelBorrowRequest.cs create mode 100644 src/Application/Borrows/Commands/ReturnDocument.cs create mode 100644 src/Application/Borrows/Commands/UpdateBorrow.cs delete mode 100644 tests/Application.Tests.Integration/Documents/Commands/BorrowDocumentTests.cs diff --git a/src/Api/Controllers/BorrowsController.cs b/src/Api/Controllers/BorrowsController.cs index e2add571..c2dd3572 100644 --- a/src/Api/Controllers/BorrowsController.cs +++ b/src/Api/Controllers/BorrowsController.cs @@ -7,6 +7,7 @@ using Application.Identity; using Infrastructure.Identity.Authorization; using Microsoft.AspNetCore.Mvc; +using UpdateBorrowRequest = Api.Controllers.Payload.Requests.Borrows.UpdateBorrowRequest; namespace Api.Controllers; @@ -92,7 +93,7 @@ public async Task>>> GetAllRequests } /// - /// Get all borrow requests as admin paginated + /// Get all borrow requests as employee paginated /// /// /// @@ -133,7 +134,7 @@ public async Task>>> GetAllRequests [ProducesResponseType(StatusCodes.Status409Conflict)] public async Task>>> GetAllRequestsForDocumentPaginated( [FromRoute] Guid documentId, - [FromQuery] GetAllBorrowRequestsPaginatedAsEmployeeQueryParameters queryParameters) + [FromQuery] GetAllBorrowRequestsPaginatedForDocumentQueryParameters queryParameters) { var command = new GetAllBorrowRequestsSpecificPaginated.Query() { @@ -188,4 +189,73 @@ public async Task>> RejectRequest([FromRoute] Gui var result = await Mediator.Send(command); return Ok(Result.Succeed(result)); } + + /// + /// Check out a borrow request + /// + /// Id of the borrow request to be checked out + /// A BorrowDto of the checked out borrow request + [RequiresRole(IdentityData.Roles.Staff)] + [HttpPost("checkout/{borrowId:guid}")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesResponseType(StatusCodes.Status409Conflict)] + public async Task>> Checkout([FromRoute] Guid borrowId) + { + var command = new CheckoutDocument.Command() + { + BorrowId = borrowId, + }; + var result = await Mediator.Send(command); + return Ok(Result.Succeed(result)); + } + + /// + /// Return document + /// + /// Id of the document to be returned + /// A BorrowDto of the returned document borrow request + [RequiresRole(IdentityData.Roles.Staff)] + [HttpPost("return/{documentId:guid}")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesResponseType(StatusCodes.Status409Conflict)] + public async Task>> Return([FromRoute] Guid documentId) + { + var command = new ReturnDocument.Command() + { + DocumentId = documentId, + }; + var result = await Mediator.Send(command); + return Ok(Result.Succeed(result)); + } + + /// + /// Update borrow request + /// + /// Id of the borrow request to be updated + /// Update borrow details + /// A BorrowDto of the updated borrow request + [RequiresRole(IdentityData.Roles.Employee)] + [HttpPut("{borrowId:guid}")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesResponseType(StatusCodes.Status409Conflict)] + public async Task>> Update( + [FromRoute] Guid borrowId, + [FromBody] UpdateBorrowRequest request) + { + var command = new UpdateBorrow.Command() + { + BorrowId = borrowId, + BorrowFrom = request.BorrowFrom, + BorrowTo = request.BorrowTo, + Reason = request.Reason, + }; + var result = await Mediator.Send(command); + return Ok(Result.Succeed(result)); + } } \ No newline at end of file diff --git a/src/Api/Controllers/Payload/Requests/Borrows/GetAllBorrowRequestsPaginatedForDocumentQueryParameters.cs b/src/Api/Controllers/Payload/Requests/Borrows/GetAllBorrowRequestsPaginatedForDocumentQueryParameters.cs new file mode 100644 index 00000000..a44619fe --- /dev/null +++ b/src/Api/Controllers/Payload/Requests/Borrows/GetAllBorrowRequestsPaginatedForDocumentQueryParameters.cs @@ -0,0 +1,6 @@ +namespace Api.Controllers.Payload.Requests.Borrows; + +public class GetAllBorrowRequestsPaginatedForDocumentQueryParameters : PaginatedQueryParameters +{ + +} \ No newline at end of file diff --git a/src/Api/Controllers/Payload/Requests/Borrows/UpdateBorrowRequest.cs b/src/Api/Controllers/Payload/Requests/Borrows/UpdateBorrowRequest.cs new file mode 100644 index 00000000..dbbef730 --- /dev/null +++ b/src/Api/Controllers/Payload/Requests/Borrows/UpdateBorrowRequest.cs @@ -0,0 +1,8 @@ +namespace Api.Controllers.Payload.Requests.Borrows; + +public class UpdateBorrowRequest +{ + public DateTime BorrowFrom { get; init; } + public DateTime BorrowTo { get; init; } + public string Reason { get; init; } = null!; +} \ No newline at end of file diff --git a/src/Application/Borrows/Commands/ApproveBorrowRequest.cs b/src/Application/Borrows/Commands/ApproveBorrowRequest.cs index 526e94fc..9d7b345d 100644 --- a/src/Application/Borrows/Commands/ApproveBorrowRequest.cs +++ b/src/Application/Borrows/Commands/ApproveBorrowRequest.cs @@ -56,8 +56,8 @@ public async Task Handle(Command request, CancellationToken cancellat var existedBorrow = await _context.Borrows .FirstOrDefaultAsync(x => x.Document.Id == borrowRequest.Document.Id - && ((x.DueTime > localDateTimeNow - && x.BorrowTime < localDateTimeNow) + && x.Id != borrowRequest.Id + && ((x.DueTime > localDateTimeNow) || x.Status == BorrowRequestStatus.Overdue), cancellationToken); if (existedBorrow is not null) diff --git a/src/Application/Borrows/Commands/BorrowDocument.cs b/src/Application/Borrows/Commands/BorrowDocument.cs index 3c45f0da..25ad6c1b 100644 --- a/src/Application/Borrows/Commands/BorrowDocument.cs +++ b/src/Application/Borrows/Commands/BorrowDocument.cs @@ -78,9 +78,9 @@ public async Task Handle(Command request, CancellationToken cancellat throw new KeyNotFoundException("Document does not exist."); } - if (document.Status is DocumentStatus.Lost) + if (document.Status is not DocumentStatus.Available) { - throw new ConflictException("Document is lost."); + throw new ConflictException("Document is not available."); } if (document.Department!.Id != user.Department!.Id) @@ -123,7 +123,7 @@ or BorrowRequestStatus.CheckedOut { Borrower = user, Document = document, - BorrowTime = LocalDateTime.FromDateTime(DateTime.Now), + BorrowTime = LocalDateTime.FromDateTime(request.BorrowFrom), DueTime = LocalDateTime.FromDateTime(request.BorrowTo), Reason = request.Reason, Status = BorrowRequestStatus.Pending, diff --git a/src/Application/Borrows/Commands/CancelBorrowRequest.cs b/src/Application/Borrows/Commands/CancelBorrowRequest.cs new file mode 100644 index 00000000..2f6e629d --- /dev/null +++ b/src/Application/Borrows/Commands/CancelBorrowRequest.cs @@ -0,0 +1,51 @@ +using Application.Common.Exceptions; +using Application.Common.Interfaces; +using Application.Common.Models.Dtos.Physical; +using AutoMapper; +using Domain.Statuses; +using MediatR; +using Microsoft.EntityFrameworkCore; + +namespace Application.Borrows.Commands; + +public class CancelBorrowRequest +{ + public record Command : IRequest + { + public Guid BorrowId { get; init; } + } + + public class CommandHandler : IRequestHandler + { + private readonly IApplicationDbContext _context; + private readonly IMapper _mapper; + + public CommandHandler(IApplicationDbContext context, IMapper mapper) + { + _context = context; + _mapper = mapper; + } + + public async Task Handle(Command request, CancellationToken cancellationToken) + { + var borrowRequest = await _context.Borrows + .Include(x => x.Borrower) + .Include(x => x.Document) + .FirstOrDefaultAsync(x => x.Id == request.BorrowId, cancellationToken); + if (borrowRequest is null) + { + throw new KeyNotFoundException("Borrow request does not exist."); + } + + if (borrowRequest.Status is not (BorrowRequestStatus.Approved or BorrowRequestStatus.Pending)) + { + throw new ConflictException("Request cannot be checked out."); + } + + borrowRequest.Status = BorrowRequestStatus.Cancelled; + var result = _context.Borrows.Update(borrowRequest); + await _context.SaveChangesAsync(cancellationToken); + return _mapper.Map(result.Entity); + } + } +} \ No newline at end of file diff --git a/src/Application/Borrows/Commands/CheckoutDocument.cs b/src/Application/Borrows/Commands/CheckoutDocument.cs index 4e218b70..6578225f 100644 --- a/src/Application/Borrows/Commands/CheckoutDocument.cs +++ b/src/Application/Borrows/Commands/CheckoutDocument.cs @@ -1,6 +1,58 @@ +using Application.Common.Exceptions; +using Application.Common.Interfaces; +using Application.Common.Models.Dtos.Physical; +using AutoMapper; +using Domain.Statuses; +using MediatR; +using Microsoft.EntityFrameworkCore; + namespace Application.Borrows.Commands; public class CheckoutDocument { - + public record Command : IRequest + { + public Guid BorrowId { get; init; } + } + + public class CommandHandler : IRequestHandler + { + private readonly IApplicationDbContext _context; + private readonly IMapper _mapper; + + public CommandHandler(IApplicationDbContext context, IMapper mapper) + { + _context = context; + _mapper = mapper; + } + + public async Task Handle(Command request, CancellationToken cancellationToken) + { + var borrowRequest = await _context.Borrows + .Include(x => x.Borrower) + .Include(x => x.Document) + .FirstOrDefaultAsync(x => x.Id == request.BorrowId, cancellationToken); + if (borrowRequest is null) + { + throw new KeyNotFoundException("Borrow request does not exist."); + } + + if (borrowRequest.Document.Status is not DocumentStatus.Available) + { + throw new ConflictException("Document is not available."); + } + + if (borrowRequest.Status is not BorrowRequestStatus.Approved) + { + throw new ConflictException("Request cannot be checked out."); + } + + borrowRequest.Status = BorrowRequestStatus.CheckedOut; + borrowRequest.Document.Status = DocumentStatus.Borrowed; + var result = _context.Borrows.Update(borrowRequest); + _context.Documents.Update(borrowRequest.Document); + await _context.SaveChangesAsync(cancellationToken); + return _mapper.Map(result.Entity); + } + } } \ No newline at end of file diff --git a/src/Application/Borrows/Commands/ReturnDocument.cs b/src/Application/Borrows/Commands/ReturnDocument.cs new file mode 100644 index 00000000..918a979d --- /dev/null +++ b/src/Application/Borrows/Commands/ReturnDocument.cs @@ -0,0 +1,61 @@ +using Application.Common.Exceptions; +using Application.Common.Interfaces; +using Application.Common.Models.Dtos.Physical; +using AutoMapper; +using Domain.Statuses; +using MediatR; +using Microsoft.EntityFrameworkCore; +using NodaTime; + +namespace Application.Borrows.Commands; + +public class ReturnDocument +{ + public record Command : IRequest + { + public Guid DocumentId { get; init; } + } + + public class CommandHandler : IRequestHandler + { + private readonly IApplicationDbContext _context; + private readonly IMapper _mapper; + + public CommandHandler(IApplicationDbContext context, IMapper mapper) + { + _context = context; + _mapper = mapper; + } + + public async Task Handle(Command request, CancellationToken cancellationToken) + { + var borrowRequest = await _context.Borrows + .Include(x => x.Borrower) + .Include(x => x.Document) + .FirstOrDefaultAsync(x => x.Document.Id == request.DocumentId + && x.Status == BorrowRequestStatus.CheckedOut, cancellationToken); + if (borrowRequest is null) + { + throw new KeyNotFoundException("Borrow request does not exist."); + } + + if (borrowRequest.Document.Status is not DocumentStatus.Borrowed) + { + throw new ConflictException("Document is not borrowed."); + } + + if (borrowRequest.Status is not BorrowRequestStatus.CheckedOut) + { + throw new ConflictException("Request cannot be made."); + } + + borrowRequest.Status = BorrowRequestStatus.Returned; + borrowRequest.Document.Status = DocumentStatus.Available; + borrowRequest.ActualReturnTime = LocalDateTime.FromDateTime(DateTime.Now); + var result = _context.Borrows.Update(borrowRequest); + _context.Documents.Update(borrowRequest.Document); + await _context.SaveChangesAsync(cancellationToken); + return _mapper.Map(result.Entity); + } + } +} \ No newline at end of file diff --git a/src/Application/Borrows/Commands/UpdateBorrow.cs b/src/Application/Borrows/Commands/UpdateBorrow.cs new file mode 100644 index 00000000..71710074 --- /dev/null +++ b/src/Application/Borrows/Commands/UpdateBorrow.cs @@ -0,0 +1,85 @@ +using Application.Common.Exceptions; +using Application.Common.Interfaces; +using Application.Common.Models.Dtos.Physical; +using AutoMapper; +using Domain.Entities.Physical; +using Domain.Statuses; +using MediatR; +using Microsoft.EntityFrameworkCore; +using NodaTime; + +namespace Application.Borrows.Commands; + +public class UpdateBorrow +{ + public record Command : IRequest + { + public Guid BorrowId { get; init; } + public DateTime BorrowFrom { get; init; } + public DateTime BorrowTo { get; init; } + public string Reason { get; init; } = null!; + } + + public class CommandHandler : IRequestHandler + { + private readonly IApplicationDbContext _context; + private readonly IMapper _mapper; + + public CommandHandler(IApplicationDbContext context, IMapper mapper) + { + _context = context; + _mapper = mapper; + } + + public async Task Handle(Command request, CancellationToken cancellationToken) + { + var borrowRequest = await _context.Borrows + .Include(x => x.Borrower) + .Include(x => x.Document) + .FirstOrDefaultAsync(x => x.Id == request.BorrowId, cancellationToken); + if (borrowRequest is null) + { + throw new KeyNotFoundException("Borrow request does not exist."); + } + + if (borrowRequest.Status is not BorrowRequestStatus.Pending) + { + throw new ConflictException("Cannot update borrow request."); + } + + if (borrowRequest.Document.Status is DocumentStatus.Lost) + { + throw new ConflictException("Document is lost."); + } + + var localDateTimeNow = LocalDateTime.FromDateTime(DateTime.Now); + var existedBorrow = await _context.Borrows + .Include(x => x.Borrower) + .FirstOrDefaultAsync(x => + x.Document.Id == borrowRequest.Document.Id + && x.Id != borrowRequest.Id + && ((x.DueTime > localDateTimeNow) + || x.Status == BorrowRequestStatus.Overdue), cancellationToken); + + if (existedBorrow is not null) + { + if (existedBorrow.Status + is BorrowRequestStatus.Approved + or BorrowRequestStatus.CheckedOut + && LocalDateTime.FromDateTime(request.BorrowFrom) < existedBorrow.DueTime) + { + throw new ConflictException("This document cannot be borrowed."); + } + } + + borrowRequest.BorrowTime = LocalDateTime.FromDateTime(request.BorrowFrom); + borrowRequest.DueTime = LocalDateTime.FromDateTime(request.BorrowTo); + borrowRequest.Reason = request.Reason; + + var result = _context.Borrows.Update(borrowRequest); + await _context.SaveChangesAsync(cancellationToken); + + return _mapper.Map(result.Entity); + } + } +} \ No newline at end of file diff --git a/src/Application/Borrows/Queries/GetAllBorrowRequestsSpecificPaginated.cs b/src/Application/Borrows/Queries/GetAllBorrowRequestsSpecificPaginated.cs index 8c9b6172..5757ff8c 100644 --- a/src/Application/Borrows/Queries/GetAllBorrowRequestsSpecificPaginated.cs +++ b/src/Application/Borrows/Queries/GetAllBorrowRequestsSpecificPaginated.cs @@ -51,6 +51,11 @@ public async Task> Handle(Query request, borrows = borrows.Where(x => x.Borrower.Id == request.EmployeeId); } + if (request.DocumentId is not null) + { + borrows = borrows.Where(x => x.Document.Id == request.DocumentId); + } + var sortBy = request.SortBy; if (sortBy is null || !sortBy.MatchesPropertyName()) { diff --git a/src/Application/Common/Extensions/QueryableExtensions.cs b/src/Application/Common/Extensions/QueryableExtensions.cs index 2272a400..a670cca7 100644 --- a/src/Application/Common/Extensions/QueryableExtensions.cs +++ b/src/Application/Common/Extensions/QueryableExtensions.cs @@ -23,6 +23,6 @@ public static IQueryable OrderByCustom(this IQueryable Paginate(this IQueryable items, int page, int size) { - return items.Skip((page - 1) * page).Take(size); + return items.Skip((page - 1) * size).Take(size); } } \ No newline at end of file diff --git a/tests/Application.Tests.Integration/Documents/Commands/BorrowDocumentTests.cs b/tests/Application.Tests.Integration/Documents/Commands/BorrowDocumentTests.cs deleted file mode 100644 index dbfd34a3..00000000 --- a/tests/Application.Tests.Integration/Documents/Commands/BorrowDocumentTests.cs +++ /dev/null @@ -1,168 +0,0 @@ -using Application.Borrows.Commands; -using Application.Common.Exceptions; -using Application.Common.Mappings; -using Application.Documents.Commands; -using AutoMapper; -using Domain.Entities.Physical; -using FluentAssertions; -using Infrastructure.Persistence; -using Microsoft.Extensions.DependencyInjection; -using Xunit; - -namespace Application.Tests.Integration.Documents.Commands; - -public class BorrowDocumentTests : BaseClassFixture -{ - public BorrowDocumentTests(CustomApiFactory apiFactory) : base(apiFactory) - { - } - - [Fact] - public async Task ShouldCreateBorrowRequest_WhenBorrowDetailsAreValid() - { - // Arrange - using var scope = ScopeFactory.CreateScope(); - var context = scope.ServiceProvider.GetRequiredService(); - var department = CreateDepartment(); - var document = CreateNDocuments(1).First(); - var folder = CreateFolder(document); - var locker = CreateLocker(folder); - var room = CreateRoom(department, locker); - var user = CreateUser("Employee", "random"); - user.Department = department; - await context.AddAsync(room); - await context.AddAsync(user); - await context.SaveChangesAsync(); - - var command = new BorrowDocument.Command() - { - BorrowerId = user.Id, - DocumentId = document.Id, - BorrowTo = DateTime.Now.AddDays(1), - Reason = "khoa on ko?", - }; - - // Act - var result = await SendAsync(command); - - // Assert - result.Should().NotBeNull(); - result.BorrowerId.Should().Be(user.Id); - result.DocumentId.Should().Be(document.Id); - result.DueTime.Should().Be(command.BorrowTo); - result.Reason.Should().Be(command.Reason); - - // Cleanup - Remove(await FindAsync(result.Id)); - Remove(document); - Remove(folder); - Remove(locker); - Remove(room); - Remove(user); - Remove(department); - } - - [Fact] - public async Task ShouldThrowKeyNotFoundException_WhenUserDoesNotExist() - { - // Arrange - var department = CreateDepartment(); - var document = CreateNDocuments(1).First(); - var folder = CreateFolder(document); - var locker = CreateLocker(folder); - var room = CreateRoom(department, locker); - await AddAsync(room); - - var command = new BorrowDocument.Command() - { - BorrowerId = Guid.NewGuid(), - DocumentId = document.Id, - BorrowTo = DateTime.Now.AddDays(1), - Reason = "khoa on ko?", - }; - - // Act - var action = async () => await SendAsync(command); - - // Assert - await action.Should().ThrowAsync() - .WithMessage("User does not exist."); - - // Cleanup - Remove(document); - Remove(folder); - Remove(locker); - Remove(room); - Remove(department); - } - - [Fact] - public async Task ShouldThrowKeyNotFoundException_WhenDocumentDoesNotExist() - { - // Arrange - var department = CreateDepartment(); - var user = CreateUser("Employee", "random"); - user.Department = department; - await AddAsync(user); - - var command = new BorrowDocument.Command() - { - BorrowerId = user.Id, - DocumentId = Guid.NewGuid(), - BorrowTo = DateTime.Now.AddDays(1), - Reason = "khoa on ko?", - }; - - // Act - var action = async () => await SendAsync(command); - - // Assert - await action.Should().ThrowAsync() - .WithMessage("Document does not exist."); - - // Cleanup - Remove(user); - Remove(department); - } - - [Fact] - public async Task ShouldThrowConflictException_WhenDueDateIsInThePast() - { - // Arrange - using var scope = ScopeFactory.CreateScope(); - var context = scope.ServiceProvider.GetRequiredService(); - var department = CreateDepartment(); - var document = CreateNDocuments(1).First(); - var folder = CreateFolder(document); - var locker = CreateLocker(folder); - var room = CreateRoom(department, locker); - var user = CreateUser("Employee", "random"); - user.Department = department; - await context.AddAsync(room); - await context.AddAsync(user); - await context.SaveChangesAsync(); - - var command = new BorrowDocument.Command() - { - BorrowerId = user.Id, - DocumentId = document.Id, - BorrowTo = DateTime.Now.AddDays(-1), - Reason = "khoa on ko?", - }; - - // Act - var action = async () => await SendAsync(command); - - // Assert - await action.Should().ThrowAsync() - .WithMessage("Due date cannot be in the past."); - - // Cleanup - Remove(document); - Remove(folder); - Remove(locker); - Remove(room); - Remove(user); - Remove(department); - } -} \ No newline at end of file From 5d59bbf6c2f349fbefd06a04a8828edd8adae950 Mon Sep 17 00:00:00 2001 From: Nguyen Quang Chien Date: Sat, 3 Jun 2023 18:38:28 +0700 Subject: [PATCH 09/13] add: validation and fix simple logic in borrowing document --- src/Application/Borrows/Commands/BorrowDocument.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Application/Borrows/Commands/BorrowDocument.cs b/src/Application/Borrows/Commands/BorrowDocument.cs index 25ad6c1b..602003bc 100644 --- a/src/Application/Borrows/Commands/BorrowDocument.cs +++ b/src/Application/Borrows/Commands/BorrowDocument.cs @@ -23,8 +23,9 @@ public Validator() .MaximumLength(512).WithMessage("Reason cannot exceed 512 characters."); RuleFor(x => x.BorrowFrom) - .GreaterThan(DateTime.Now).WithMessage("Borrow date cannot be in the past."); - + .GreaterThan(DateTime.Now).WithMessage("Borrow date cannot be in the past.") + .Must((command, borrowTime) => borrowTime < command.BorrowTo).WithMessage("Due date cannot be before borrow date."); + RuleFor(x => x.BorrowTo) .GreaterThan(DateTime.Now).WithMessage("Due date cannot be in the past."); } @@ -97,8 +98,7 @@ public async Task Handle(Command request, CancellationToken cancellat .Include(x => x.Borrower) .FirstOrDefaultAsync(x => x.Document.Id == request.DocumentId - && ((x.DueTime > localDateTimeNow - && x.BorrowTime < localDateTimeNow) + && ((x.DueTime > localDateTimeNow) || x.Status == BorrowRequestStatus.Overdue), cancellationToken); if (existedBorrow is not null) From abca5b3e996dbd0ca26db86bd013ef68877e6b3f Mon Sep 17 00:00:00 2001 From: Nguyen Quang Chien Date: Sat, 3 Jun 2023 19:06:19 +0700 Subject: [PATCH 10/13] refactor: a bit --- src/Api/Controllers/BorrowsController.cs | 9 ++++++--- ...orrowRequestsPaginatedAsAdminQueryParameters.cs | 2 ++ ...orrowRequestsPaginatedAsStaffQueryParameters.cs | 2 ++ .../Borrows/Commands/CancelBorrowRequest.cs | 2 +- .../Queries/GetAllBorrowRequestsPaginated.cs | 14 +++++++++++++- .../GetAllBorrowRequestsSpecificPaginated.cs | 4 ++-- 6 files changed, 26 insertions(+), 7 deletions(-) diff --git a/src/Api/Controllers/BorrowsController.cs b/src/Api/Controllers/BorrowsController.cs index c2dd3572..0c7b34c1 100644 --- a/src/Api/Controllers/BorrowsController.cs +++ b/src/Api/Controllers/BorrowsController.cs @@ -7,7 +7,6 @@ using Application.Identity; using Infrastructure.Identity.Authorization; using Microsoft.AspNetCore.Mvc; -using UpdateBorrowRequest = Api.Controllers.Payload.Requests.Borrows.UpdateBorrowRequest; namespace Api.Controllers; @@ -57,6 +56,8 @@ public async Task>>> GetAllRequests var command = new GetAllBorrowRequestsPaginated.Query() { DepartmentId = departmentId, + EmployeeId = queryParameters.EmployeeId, + DocumentId = queryParameters.DocumentId, Page = queryParameters.Page, Size = queryParameters.Size, SortBy = queryParameters.SortBy, @@ -83,6 +84,8 @@ public async Task>>> GetAllRequests var command = new GetAllBorrowRequestsPaginated.Query() { DepartmentId = queryParameters.DepartmentId, + EmployeeId = queryParameters.EmployeeId, + DocumentId = queryParameters.DocumentId, Page = queryParameters.Page, Size = queryParameters.Size, SortBy = queryParameters.SortBy, @@ -107,7 +110,7 @@ public async Task>>> GetAllRequests [FromQuery] GetAllBorrowRequestsPaginatedAsEmployeeQueryParameters queryParameters) { var userId = _currentUserService.GetCurrentUser().Id; - var command = new GetAllBorrowRequestsSpecificPaginated.Query() + var command = new GetAllBorrowRequestsPaginated.Query() { EmployeeId = userId, DocumentId = queryParameters.DocumentId, @@ -136,7 +139,7 @@ public async Task>>> GetAllRequests [FromRoute] Guid documentId, [FromQuery] GetAllBorrowRequestsPaginatedForDocumentQueryParameters queryParameters) { - var command = new GetAllBorrowRequestsSpecificPaginated.Query() + var command = new GetAllBorrowRequestsPaginated.Query() { DocumentId = documentId, Page = queryParameters.Page, diff --git a/src/Api/Controllers/Payload/Requests/Borrows/GetAllBorrowRequestsPaginatedAsAdminQueryParameters.cs b/src/Api/Controllers/Payload/Requests/Borrows/GetAllBorrowRequestsPaginatedAsAdminQueryParameters.cs index a2457eaa..8ea8eca1 100644 --- a/src/Api/Controllers/Payload/Requests/Borrows/GetAllBorrowRequestsPaginatedAsAdminQueryParameters.cs +++ b/src/Api/Controllers/Payload/Requests/Borrows/GetAllBorrowRequestsPaginatedAsAdminQueryParameters.cs @@ -9,4 +9,6 @@ public class GetAllBorrowRequestsPaginatedAsAdminQueryParameters : PaginatedQuer /// Id of the department to get borrow requests in /// public Guid? DepartmentId { get; set; } + public Guid? DocumentId { get; set; } + public Guid? EmployeeId { get; set; } } \ No newline at end of file diff --git a/src/Api/Controllers/Payload/Requests/Borrows/GetAllBorrowRequestsPaginatedAsStaffQueryParameters.cs b/src/Api/Controllers/Payload/Requests/Borrows/GetAllBorrowRequestsPaginatedAsStaffQueryParameters.cs index de3fcd51..d9656766 100644 --- a/src/Api/Controllers/Payload/Requests/Borrows/GetAllBorrowRequestsPaginatedAsStaffQueryParameters.cs +++ b/src/Api/Controllers/Payload/Requests/Borrows/GetAllBorrowRequestsPaginatedAsStaffQueryParameters.cs @@ -5,4 +5,6 @@ namespace Api.Controllers.Payload.Requests.Borrows; /// public class GetAllBorrowRequestsPaginatedAsStaffQueryParameters : PaginatedQueryParameters { + public Guid? DocumentId { get; set; } + public Guid? EmployeeId { get; set; } } \ No newline at end of file diff --git a/src/Application/Borrows/Commands/CancelBorrowRequest.cs b/src/Application/Borrows/Commands/CancelBorrowRequest.cs index 2f6e629d..97f111d4 100644 --- a/src/Application/Borrows/Commands/CancelBorrowRequest.cs +++ b/src/Application/Borrows/Commands/CancelBorrowRequest.cs @@ -39,7 +39,7 @@ public async Task Handle(Command request, CancellationToken cancellat if (borrowRequest.Status is not (BorrowRequestStatus.Approved or BorrowRequestStatus.Pending)) { - throw new ConflictException("Request cannot be checked out."); + throw new ConflictException("Request cannot be cancelled."); } borrowRequest.Status = BorrowRequestStatus.Cancelled; diff --git a/src/Application/Borrows/Queries/GetAllBorrowRequestsPaginated.cs b/src/Application/Borrows/Queries/GetAllBorrowRequestsPaginated.cs index 9fd4ccd3..0ebacbe2 100644 --- a/src/Application/Borrows/Queries/GetAllBorrowRequestsPaginated.cs +++ b/src/Application/Borrows/Queries/GetAllBorrowRequestsPaginated.cs @@ -22,7 +22,9 @@ public Validator() public record Query : IRequest> { - public Guid? DepartmentId { get; set; } + public Guid? DepartmentId { get; init; } + public Guid? DocumentId { get; init; } + public Guid? EmployeeId { get; init; } public int? Page { get; init; } public int? Size { get; init; } public string? SortBy { get; init; } @@ -60,6 +62,16 @@ public async Task> Handle(Query request, borrows = borrows.Where(x => x.Document.Department!.Id == request.DepartmentId); } + if (request.EmployeeId is not null) + { + borrows = borrows.Where(x => x.Borrower.Id == request.EmployeeId); + } + + if (request.DocumentId is not null) + { + borrows = borrows.Where(x => x.Document.Id == request.DocumentId); + } + var sortBy = request.SortBy; if (sortBy is null || !sortBy.MatchesPropertyName()) { diff --git a/src/Application/Borrows/Queries/GetAllBorrowRequestsSpecificPaginated.cs b/src/Application/Borrows/Queries/GetAllBorrowRequestsSpecificPaginated.cs index 5757ff8c..9710833d 100644 --- a/src/Application/Borrows/Queries/GetAllBorrowRequestsSpecificPaginated.cs +++ b/src/Application/Borrows/Queries/GetAllBorrowRequestsSpecificPaginated.cs @@ -12,8 +12,8 @@ public class GetAllBorrowRequestsSpecificPaginated { public record Query : IRequest> { - public Guid? DocumentId { get; set; } - public Guid? EmployeeId { get; set; } + public Guid? DocumentId { get; init; } + public Guid? EmployeeId { get; init; } public int? Page { get; init; } public int? Size { get; init; } public string? SortBy { get; init; } From ccd42b7bbb4c21c232a5f10899ad737a9c4cd6a5 Mon Sep 17 00:00:00 2001 From: Nguyen Quang Chien Date: Sat, 3 Jun 2023 21:14:50 +0700 Subject: [PATCH 11/13] final touch --- src/Api/Controllers/DocumentsController.cs | 4 +- src/Api/Controllers/RoomsController.cs | 2 +- src/Api/Services/CurrentUserService.cs | 5 + .../Borrows/Commands/UpdateBorrow.cs | 19 + .../Queries/GetAllBorrowRequestsPaginated.cs | 2 +- .../GetAllBorrowRequestsSpecificPaginated.cs | 1 + .../Queries/GetAllDocumentsPaginated.cs | 2 +- .../Folders/Queries/GetAllFoldersPaginated.cs | 2 +- .../Lockers/Queries/GetAllLockersPaginated.cs | 2 +- .../Rooms/Queries/GetAllRoomsPaginated.cs | 2 +- src/Application/Staffs/Commands/AddStaff.cs | 47 +- .../Identity/IdentityService.cs | 4 +- ...000009_RoomMustHaveADepartment.Designer.cs | 477 ------------------ .../00000000000009_RoomMustHaveADepartment.cs | 22 - ...> 00000000000010_UpdateBorrow.Designer.cs} | 2 +- .../Migrations/00000000000010_UpdateBorrow.cs | 124 +++++ .../Migrations/20230602224631_UpdateBorrow.cs | 41 -- src/Infrastructure/Services/MailService.cs | 1 - 18 files changed, 197 insertions(+), 562 deletions(-) delete mode 100644 src/Infrastructure/Persistence/Migrations/00000000000009_RoomMustHaveADepartment.Designer.cs delete mode 100644 src/Infrastructure/Persistence/Migrations/00000000000009_RoomMustHaveADepartment.cs rename src/Infrastructure/Persistence/Migrations/{20230602224631_UpdateBorrow.Designer.cs => 00000000000010_UpdateBorrow.Designer.cs} (99%) create mode 100644 src/Infrastructure/Persistence/Migrations/00000000000010_UpdateBorrow.cs delete mode 100644 src/Infrastructure/Persistence/Migrations/20230602224631_UpdateBorrow.cs diff --git a/src/Api/Controllers/DocumentsController.cs b/src/Api/Controllers/DocumentsController.cs index 5c56986b..8d817d70 100644 --- a/src/Api/Controllers/DocumentsController.cs +++ b/src/Api/Controllers/DocumentsController.cs @@ -105,7 +105,7 @@ public async Task>> Import([FromBody] ImportDoc /// Id of the document to be updated /// Update document details /// A DocumentDto of the updated document - [RequiresRole(IdentityData.Roles.Admin)] + [RequiresRole(IdentityData.Roles.Admin, IdentityData.Roles.Staff)] [HttpPut("{documentId:guid}")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status403Forbidden)] @@ -128,7 +128,7 @@ public async Task>> Update([FromRoute] Guid doc /// Delete a document /// /// Id of the document to be deleted - /// A DocumentDto of the deleted document + /// A DocumentDto of the deleted document [HttpDelete("{documentId:guid}")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status403Forbidden)] diff --git a/src/Api/Controllers/RoomsController.cs b/src/Api/Controllers/RoomsController.cs index 5ecb848d..fa5198ee 100644 --- a/src/Api/Controllers/RoomsController.cs +++ b/src/Api/Controllers/RoomsController.cs @@ -61,7 +61,7 @@ public async Task>>> GetAllPaginated( /// Get empty containers paginated details /// A paginated list of EmptyLockerDto [RequiresRole(IdentityData.Roles.Staff)] - [HttpPost("empty-containers")] + [HttpPost("empty-containers/{roomId:guid}")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status403Forbidden)] [ProducesResponseType(StatusCodes.Status404NotFound)] diff --git a/src/Api/Services/CurrentUserService.cs b/src/Api/Services/CurrentUserService.cs index 5a808885..0075bedb 100644 --- a/src/Api/Services/CurrentUserService.cs +++ b/src/Api/Services/CurrentUserService.cs @@ -113,6 +113,11 @@ public User GetCurrentUser() { throw new UnauthorizedAccessException(); } + + if (staff.Room is null) + { + throw new UnauthorizedAccessException(); + } return staff.Room!.DepartmentId; } diff --git a/src/Application/Borrows/Commands/UpdateBorrow.cs b/src/Application/Borrows/Commands/UpdateBorrow.cs index 71710074..fe39c471 100644 --- a/src/Application/Borrows/Commands/UpdateBorrow.cs +++ b/src/Application/Borrows/Commands/UpdateBorrow.cs @@ -4,6 +4,7 @@ using AutoMapper; using Domain.Entities.Physical; using Domain.Statuses; +using FluentValidation; using MediatR; using Microsoft.EntityFrameworkCore; using NodaTime; @@ -12,6 +13,24 @@ namespace Application.Borrows.Commands; public class UpdateBorrow { + public class Validator : AbstractValidator + { + public Validator() + { + RuleLevelCascadeMode = CascadeMode.Stop; + + RuleFor(x => x.Reason) + .MaximumLength(512).WithMessage("Reason cannot exceed 512 characters."); + + RuleFor(x => x.BorrowFrom) + .GreaterThan(DateTime.Now).WithMessage("Borrow date cannot be in the past.") + .Must((command, borrowTime) => borrowTime < command.BorrowTo).WithMessage("Due date cannot be before borrow date."); + + RuleFor(x => x.BorrowTo) + .GreaterThan(DateTime.Now).WithMessage("Due date cannot be in the past."); + } + } + public record Command : IRequest { public Guid BorrowId { get; init; } diff --git a/src/Application/Borrows/Queries/GetAllBorrowRequestsPaginated.cs b/src/Application/Borrows/Queries/GetAllBorrowRequestsPaginated.cs index 0ebacbe2..bc5c138e 100644 --- a/src/Application/Borrows/Queries/GetAllBorrowRequestsPaginated.cs +++ b/src/Application/Borrows/Queries/GetAllBorrowRequestsPaginated.cs @@ -82,8 +82,8 @@ public async Task> Handle(Query request, var sizeNumber = request.Size is null or <= 0 ? 5 : request.Size; var list = await borrows - .Paginate(pageNumber.Value, sizeNumber.Value) .OrderByCustom(sortBy, sortOrder) + .Paginate(pageNumber.Value, sizeNumber.Value) .ToListAsync(cancellationToken); var result = _mapper.Map>(list); diff --git a/src/Application/Borrows/Queries/GetAllBorrowRequestsSpecificPaginated.cs b/src/Application/Borrows/Queries/GetAllBorrowRequestsSpecificPaginated.cs index 9710833d..a645079f 100644 --- a/src/Application/Borrows/Queries/GetAllBorrowRequestsSpecificPaginated.cs +++ b/src/Application/Borrows/Queries/GetAllBorrowRequestsSpecificPaginated.cs @@ -8,6 +8,7 @@ namespace Application.Borrows.Queries; +[Obsolete] public class GetAllBorrowRequestsSpecificPaginated { public record Query : IRequest> diff --git a/src/Application/Documents/Queries/GetAllDocumentsPaginated.cs b/src/Application/Documents/Queries/GetAllDocumentsPaginated.cs index a66b8cfd..de7c13cf 100644 --- a/src/Application/Documents/Queries/GetAllDocumentsPaginated.cs +++ b/src/Application/Documents/Queries/GetAllDocumentsPaginated.cs @@ -133,8 +133,8 @@ public async Task> Handle(Query request, var sizeNumber = request.Size is null or <= 0 ? 5 : request.Size; var list = await documents - .Paginate(pageNumber.Value, sizeNumber.Value) .OrderByCustom(sortBy, sortOrder) + .Paginate(pageNumber.Value, sizeNumber.Value) .ToListAsync(cancellationToken); var result = _mapper.Map>(list); diff --git a/src/Application/Folders/Queries/GetAllFoldersPaginated.cs b/src/Application/Folders/Queries/GetAllFoldersPaginated.cs index 87394f3c..f98ce633 100644 --- a/src/Application/Folders/Queries/GetAllFoldersPaginated.cs +++ b/src/Application/Folders/Queries/GetAllFoldersPaginated.cs @@ -105,8 +105,8 @@ public async Task> Handle(Query request, CancellationTo var sizeNumber = request.Size is null or <= 0 ? 5 : request.Size; var list = await folders - .Paginate(pageNumber.Value, sizeNumber.Value) .OrderByCustom(sortBy, sortOrder) + .Paginate(pageNumber.Value, sizeNumber.Value) .ToListAsync(cancellationToken); var result = _mapper.Map>(list); diff --git a/src/Application/Lockers/Queries/GetAllLockersPaginated.cs b/src/Application/Lockers/Queries/GetAllLockersPaginated.cs index 8485fffe..5e2a93ff 100644 --- a/src/Application/Lockers/Queries/GetAllLockersPaginated.cs +++ b/src/Application/Lockers/Queries/GetAllLockersPaginated.cs @@ -61,8 +61,8 @@ public async Task> Handle(Query request, CancellationTo var sizeNumber = request.Size is null or <= 0 ? 5 : request.Size; var list = await lockers - .Paginate(pageNumber.Value, sizeNumber.Value) .OrderByCustom(sortBy, sortOrder) + .Paginate(pageNumber.Value, sizeNumber.Value) .ToListAsync(cancellationToken); var result = _mapper.Map>(list); diff --git a/src/Application/Rooms/Queries/GetAllRoomsPaginated.cs b/src/Application/Rooms/Queries/GetAllRoomsPaginated.cs index e261e6b7..31fddbb8 100644 --- a/src/Application/Rooms/Queries/GetAllRoomsPaginated.cs +++ b/src/Application/Rooms/Queries/GetAllRoomsPaginated.cs @@ -54,8 +54,8 @@ public async Task> Handle(Query request, CancellationToke var sizeNumber = request.Size is null or <= 0 ? 5 : request.Size; var list = await rooms - .Paginate(pageNumber.Value, sizeNumber.Value) .OrderByCustom(sortBy, sortOrder) + .Paginate(pageNumber.Value, sizeNumber.Value) .ToListAsync(cancellationToken); var result = _mapper.Map>(list); diff --git a/src/Application/Staffs/Commands/AddStaff.cs b/src/Application/Staffs/Commands/AddStaff.cs index c3415e00..0d78536f 100644 --- a/src/Application/Staffs/Commands/AddStaff.cs +++ b/src/Application/Staffs/Commands/AddStaff.cs @@ -1,3 +1,4 @@ +using Application.Common.Exceptions; using Application.Common.Interfaces; using Application.Common.Models.Dtos.Physical; using AutoMapper; @@ -19,13 +20,13 @@ public class CommandHandler : IRequestHandler { private readonly IApplicationDbContext _context; private readonly IMapper _mapper; - + public CommandHandler(IApplicationDbContext context, IMapper mapper) { _context = context; _mapper = mapper; } - + public async Task Handle(Command request, CancellationToken cancellationToken) { var user = await _context.Users.FirstOrDefaultAsync(x => x.Id == request.UserId, cancellationToken); @@ -36,16 +37,40 @@ public async Task Handle(Command request, CancellationToken cancellati var room = await _context.Rooms.FirstOrDefaultAsync(x => x.Id == request.RoomId, cancellationToken); - var staff = new Staff + if (room is null) + { + throw new KeyNotFoundException("Room does not exist."); + } + + var existedStaff = await _context.Staffs + .Include(x => x.Room) + .Include(x => x.User) + .FirstOrDefaultAsync(x => x.Id == user.Id, cancellationToken); + if (existedStaff is not null) { - Id = user.Id, - User = user, - Room = room - }; - - var result = await _context.Staffs.AddAsync(staff, cancellationToken); - await _context.SaveChangesAsync(cancellationToken); - return _mapper.Map(result.Entity); + if (existedStaff.Room is not null) + { + throw new ConflictException("This user is already a staff."); + } + + existedStaff.Room = room; + var result = _context.Staffs.Update(existedStaff); + await _context.SaveChangesAsync(cancellationToken); + return _mapper.Map(result.Entity); + } + else + { + var staff = new Staff + { + Id = user.Id, + User = user, + Room = room + }; + + var result = await _context.Staffs.AddAsync(staff, cancellationToken); + await _context.SaveChangesAsync(cancellationToken); + return _mapper.Map(result.Entity); + } } } } \ No newline at end of file diff --git a/src/Infrastructure/Identity/IdentityService.cs b/src/Infrastructure/Identity/IdentityService.cs index 7f879fdd..1947979e 100644 --- a/src/Infrastructure/Identity/IdentityService.cs +++ b/src/Infrastructure/Identity/IdentityService.cs @@ -185,7 +185,9 @@ public async Task RefreshTokenAsync(string token, string r public async Task<(AuthenticationResult, UserDto)> LoginAsync(string email, string password) { - var user = _context.Users.FirstOrDefault(x => x.Email!.Equals(email)); + var user = _context.Users + .Include(x => x.Department) + .FirstOrDefault(x => x.Email!.Equals(email)); if (user is null || !user.PasswordHash.Equals(SecurityUtil.Hash(password))) { diff --git a/src/Infrastructure/Persistence/Migrations/00000000000009_RoomMustHaveADepartment.Designer.cs b/src/Infrastructure/Persistence/Migrations/00000000000009_RoomMustHaveADepartment.Designer.cs deleted file mode 100644 index a20e242e..00000000 --- a/src/Infrastructure/Persistence/Migrations/00000000000009_RoomMustHaveADepartment.Designer.cs +++ /dev/null @@ -1,477 +0,0 @@ -// -using System; -using Infrastructure.Persistence; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Migrations; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -using NodaTime; -using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; - -#nullable disable - -namespace Infrastructure.Persistence.Migrations -{ - [DbContext(typeof(ApplicationDbContext))] - [Migration("20230528182741_RoomMustHaveADepartment")] - partial class RoomMustHaveADepartment - { - /// - protected override void BuildTargetModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder - .HasAnnotation("ProductVersion", "7.0.5") - .HasAnnotation("Relational:MaxIdentifierLength", 63); - - NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); - - modelBuilder.Entity("Domain.Entities.Department", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("Name") - .IsRequired() - .HasMaxLength(64) - .HasColumnType("character varying(64)"); - - b.HasKey("Id"); - - b.HasAlternateKey("Name"); - - b.ToTable("Departments"); - }); - - modelBuilder.Entity("Domain.Entities.Physical.Borrow", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("BorrowTime") - .HasColumnType("timestamp without time zone"); - - b.Property("BorrowerId") - .HasColumnType("uuid"); - - b.Property("DocumentId") - .HasColumnType("uuid"); - - b.Property("DueTime") - .HasColumnType("timestamp without time zone"); - - b.Property("Reason") - .IsRequired() - .HasColumnType("text"); - - b.HasKey("Id"); - - b.HasIndex("BorrowerId"); - - b.HasIndex("DocumentId"); - - b.ToTable("Borrows"); - }); - - modelBuilder.Entity("Domain.Entities.Physical.Document", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("DepartmentId") - .HasColumnType("uuid"); - - b.Property("Description") - .HasMaxLength(256) - .HasColumnType("character varying(256)"); - - b.Property("DocumentType") - .IsRequired() - .HasMaxLength(64) - .HasColumnType("character varying(64)"); - - b.Property("FolderId") - .HasColumnType("uuid"); - - b.Property("ImporterId") - .HasColumnType("uuid"); - - b.Property("Title") - .IsRequired() - .HasMaxLength(64) - .HasColumnType("character varying(64)"); - - b.HasKey("Id"); - - b.HasIndex("DepartmentId"); - - b.HasIndex("FolderId"); - - b.HasIndex("ImporterId"); - - b.ToTable("Documents"); - }); - - modelBuilder.Entity("Domain.Entities.Physical.Folder", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("Capacity") - .HasColumnType("integer"); - - b.Property("Description") - .HasMaxLength(256) - .HasColumnType("character varying(256)"); - - b.Property("IsAvailable") - .HasColumnType("boolean"); - - b.Property("LockerId") - .HasColumnType("uuid"); - - b.Property("Name") - .IsRequired() - .HasMaxLength(64) - .HasColumnType("character varying(64)"); - - b.Property("NumberOfDocuments") - .HasColumnType("integer"); - - b.HasKey("Id"); - - b.HasIndex("LockerId"); - - b.ToTable("Folders"); - }); - - modelBuilder.Entity("Domain.Entities.Physical.Locker", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("Capacity") - .HasColumnType("integer"); - - b.Property("Description") - .HasMaxLength(256) - .HasColumnType("character varying(256)"); - - b.Property("IsAvailable") - .HasColumnType("boolean"); - - b.Property("Name") - .IsRequired() - .HasMaxLength(64) - .HasColumnType("character varying(64)"); - - b.Property("NumberOfFolders") - .HasColumnType("integer"); - - b.Property("RoomId") - .HasColumnType("uuid"); - - b.HasKey("Id"); - - b.HasIndex("RoomId"); - - b.ToTable("Lockers"); - }); - - modelBuilder.Entity("Domain.Entities.Physical.Room", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("Capacity") - .HasColumnType("integer"); - - b.Property("DepartmentId") - .HasColumnType("uuid"); - - b.Property("Description") - .HasMaxLength(256) - .HasColumnType("character varying(256)"); - - b.Property("IsAvailable") - .HasColumnType("boolean"); - - b.Property("Name") - .IsRequired() - .HasMaxLength(64) - .HasColumnType("character varying(64)"); - - b.Property("NumberOfLockers") - .HasColumnType("integer"); - - b.HasKey("Id"); - - b.HasAlternateKey("Name"); - - b.HasIndex("DepartmentId") - .IsUnique(); - - b.ToTable("Rooms"); - }); - - modelBuilder.Entity("Domain.Entities.Physical.Staff", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid") - .HasColumnName("UserId"); - - b.Property("RoomId") - .HasColumnType("uuid"); - - b.HasKey("Id"); - - b.HasIndex("RoomId") - .IsUnique(); - - b.ToTable("Staffs"); - }); - - modelBuilder.Entity("Domain.Entities.RefreshToken", b => - { - b.Property("Token") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("CreationDateTime") - .HasColumnType("timestamp without time zone"); - - b.Property("ExpiryDateTime") - .HasColumnType("timestamp without time zone"); - - b.Property("IsInvalidated") - .ValueGeneratedOnAdd() - .HasColumnType("boolean") - .HasDefaultValue(false); - - b.Property("IsUsed") - .ValueGeneratedOnAdd() - .HasColumnType("boolean") - .HasDefaultValue(false); - - b.Property("JwtId") - .IsRequired() - .HasColumnType("text"); - - b.Property("UserId") - .HasColumnType("uuid"); - - b.HasKey("Token"); - - b.HasIndex("UserId"); - - b.ToTable("RefreshTokens"); - }); - - modelBuilder.Entity("Domain.Entities.User", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("Created") - .HasColumnType("timestamp without time zone"); - - b.Property("CreatedBy") - .HasColumnType("uuid"); - - b.Property("DepartmentId") - .HasColumnType("uuid"); - - b.Property("Email") - .IsRequired() - .HasMaxLength(320) - .HasColumnType("character varying(320)"); - - b.Property("FirstName") - .HasMaxLength(50) - .HasColumnType("character varying(50)"); - - b.Property("IsActivated") - .HasColumnType("boolean"); - - b.Property("IsActive") - .HasColumnType("boolean"); - - b.Property("LastModified") - .HasColumnType("timestamp without time zone"); - - b.Property("LastModifiedBy") - .HasColumnType("uuid"); - - b.Property("LastName") - .HasMaxLength(50) - .HasColumnType("character varying(50)"); - - b.Property("PasswordHash") - .IsRequired() - .HasMaxLength(64) - .HasColumnType("character varying(64)"); - - b.Property("Position") - .HasMaxLength(64) - .HasColumnType("character varying(64)"); - - b.Property("Role") - .IsRequired() - .HasMaxLength(64) - .HasColumnType("character varying(64)"); - - b.Property("Username") - .IsRequired() - .HasMaxLength(50) - .HasColumnType("character varying(50)"); - - b.HasKey("Id"); - - b.HasIndex("DepartmentId"); - - b.ToTable("Users"); - }); - - modelBuilder.Entity("Domain.Entities.Physical.Borrow", b => - { - b.HasOne("Domain.Entities.User", "Borrower") - .WithMany() - .HasForeignKey("BorrowerId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("Domain.Entities.Physical.Document", "Document") - .WithMany() - .HasForeignKey("DocumentId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Borrower"); - - b.Navigation("Document"); - }); - - modelBuilder.Entity("Domain.Entities.Physical.Document", b => - { - b.HasOne("Domain.Entities.Department", "Department") - .WithMany() - .HasForeignKey("DepartmentId"); - - b.HasOne("Domain.Entities.Physical.Folder", "Folder") - .WithMany("Documents") - .HasForeignKey("FolderId"); - - b.HasOne("Domain.Entities.User", "Importer") - .WithMany() - .HasForeignKey("ImporterId"); - - b.Navigation("Department"); - - b.Navigation("Folder"); - - b.Navigation("Importer"); - }); - - modelBuilder.Entity("Domain.Entities.Physical.Folder", b => - { - b.HasOne("Domain.Entities.Physical.Locker", "Locker") - .WithMany("Folders") - .HasForeignKey("LockerId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Locker"); - }); - - modelBuilder.Entity("Domain.Entities.Physical.Locker", b => - { - b.HasOne("Domain.Entities.Physical.Room", "Room") - .WithMany("Lockers") - .HasForeignKey("RoomId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Room"); - }); - - modelBuilder.Entity("Domain.Entities.Physical.Room", b => - { - b.HasOne("Domain.Entities.Department", "Department") - .WithOne("Room") - .HasForeignKey("Domain.Entities.Physical.Room", "DepartmentId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Department"); - }); - - modelBuilder.Entity("Domain.Entities.Physical.Staff", b => - { - b.HasOne("Domain.Entities.User", "User") - .WithOne() - .HasForeignKey("Domain.Entities.Physical.Staff", "Id") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("Domain.Entities.Physical.Room", "Room") - .WithOne("Staff") - .HasForeignKey("Domain.Entities.Physical.Staff", "RoomId"); - - b.Navigation("Room"); - - b.Navigation("User"); - }); - - modelBuilder.Entity("Domain.Entities.RefreshToken", b => - { - b.HasOne("Domain.Entities.User", "User") - .WithMany() - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("User"); - }); - - modelBuilder.Entity("Domain.Entities.User", b => - { - b.HasOne("Domain.Entities.Department", "Department") - .WithMany() - .HasForeignKey("DepartmentId"); - - b.Navigation("Department"); - }); - - modelBuilder.Entity("Domain.Entities.Department", b => - { - b.Navigation("Room"); - }); - - modelBuilder.Entity("Domain.Entities.Physical.Folder", b => - { - b.Navigation("Documents"); - }); - - modelBuilder.Entity("Domain.Entities.Physical.Locker", b => - { - b.Navigation("Folders"); - }); - - modelBuilder.Entity("Domain.Entities.Physical.Room", b => - { - b.Navigation("Lockers"); - - b.Navigation("Staff"); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/src/Infrastructure/Persistence/Migrations/00000000000009_RoomMustHaveADepartment.cs b/src/Infrastructure/Persistence/Migrations/00000000000009_RoomMustHaveADepartment.cs deleted file mode 100644 index a9fa4a85..00000000 --- a/src/Infrastructure/Persistence/Migrations/00000000000009_RoomMustHaveADepartment.cs +++ /dev/null @@ -1,22 +0,0 @@ -using Microsoft.EntityFrameworkCore.Migrations; - -#nullable disable - -namespace Infrastructure.Persistence.Migrations -{ - /// - public partial class RoomMustHaveADepartment : Migration - { - /// - protected override void Up(MigrationBuilder migrationBuilder) - { - - } - - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - - } - } -} diff --git a/src/Infrastructure/Persistence/Migrations/20230602224631_UpdateBorrow.Designer.cs b/src/Infrastructure/Persistence/Migrations/00000000000010_UpdateBorrow.Designer.cs similarity index 99% rename from src/Infrastructure/Persistence/Migrations/20230602224631_UpdateBorrow.Designer.cs rename to src/Infrastructure/Persistence/Migrations/00000000000010_UpdateBorrow.Designer.cs index ffae741e..39077769 100644 --- a/src/Infrastructure/Persistence/Migrations/20230602224631_UpdateBorrow.Designer.cs +++ b/src/Infrastructure/Persistence/Migrations/00000000000010_UpdateBorrow.Designer.cs @@ -13,7 +13,7 @@ namespace Infrastructure.Persistence.Migrations { [DbContext(typeof(ApplicationDbContext))] - [Migration("20230602224631_UpdateBorrow")] + [Migration("20230603132557_UpdateBorrow")] partial class UpdateBorrow { /// diff --git a/src/Infrastructure/Persistence/Migrations/00000000000010_UpdateBorrow.cs b/src/Infrastructure/Persistence/Migrations/00000000000010_UpdateBorrow.cs new file mode 100644 index 00000000..1cf4d397 --- /dev/null +++ b/src/Infrastructure/Persistence/Migrations/00000000000010_UpdateBorrow.cs @@ -0,0 +1,124 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; +using NodaTime; + +#nullable disable + +namespace Infrastructure.Persistence.Migrations +{ + /// + public partial class UpdateBorrow : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_Departments_Rooms_RoomId", + table: "Departments"); + + migrationBuilder.DropIndex( + name: "IX_Departments_RoomId", + table: "Departments"); + + migrationBuilder.DropColumn( + name: "RoomId", + table: "Departments"); + + migrationBuilder.AlterColumn( + name: "DepartmentId", + table: "Rooms", + type: "uuid", + nullable: false, + defaultValue: new Guid("00000000-0000-0000-0000-000000000000"), + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true); + + migrationBuilder.AddColumn( + name: "Status", + table: "Documents", + type: "integer", + nullable: false, + defaultValue: 0); + + migrationBuilder.AddColumn( + name: "ActualReturnTime", + table: "Borrows", + type: "timestamp without time zone", + nullable: false, + defaultValue: new NodaTime.LocalDateTime(1, 1, 1, 0, 0)); + + migrationBuilder.AddColumn( + name: "Status", + table: "Borrows", + type: "integer", + nullable: false, + defaultValue: 0); + + migrationBuilder.CreateIndex( + name: "IX_Rooms_DepartmentId", + table: "Rooms", + column: "DepartmentId", + unique: true); + + migrationBuilder.AddForeignKey( + name: "FK_Rooms_Departments_DepartmentId", + table: "Rooms", + column: "DepartmentId", + principalTable: "Departments", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_Rooms_Departments_DepartmentId", + table: "Rooms"); + + migrationBuilder.DropIndex( + name: "IX_Rooms_DepartmentId", + table: "Rooms"); + + migrationBuilder.DropColumn( + name: "Status", + table: "Documents"); + + migrationBuilder.DropColumn( + name: "ActualReturnTime", + table: "Borrows"); + + migrationBuilder.DropColumn( + name: "Status", + table: "Borrows"); + + migrationBuilder.AlterColumn( + name: "DepartmentId", + table: "Rooms", + type: "uuid", + nullable: true, + oldClrType: typeof(Guid), + oldType: "uuid"); + + migrationBuilder.AddColumn( + name: "RoomId", + table: "Departments", + type: "uuid", + nullable: true); + + migrationBuilder.CreateIndex( + name: "IX_Departments_RoomId", + table: "Departments", + column: "RoomId", + unique: true); + + migrationBuilder.AddForeignKey( + name: "FK_Departments_Rooms_RoomId", + table: "Departments", + column: "RoomId", + principalTable: "Rooms", + principalColumn: "Id"); + } + } +} diff --git a/src/Infrastructure/Persistence/Migrations/20230602224631_UpdateBorrow.cs b/src/Infrastructure/Persistence/Migrations/20230602224631_UpdateBorrow.cs deleted file mode 100644 index 5e7277f8..00000000 --- a/src/Infrastructure/Persistence/Migrations/20230602224631_UpdateBorrow.cs +++ /dev/null @@ -1,41 +0,0 @@ -using Microsoft.EntityFrameworkCore.Migrations; -using NodaTime; - -#nullable disable - -namespace Infrastructure.Persistence.Migrations -{ - /// - public partial class UpdateBorrow : Migration - { - /// - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.AddColumn( - name: "Status", - table: "Documents", - type: "integer", - nullable: false, - defaultValue: 0); - - migrationBuilder.AddColumn( - name: "ActualReturnTime", - table: "Borrows", - type: "timestamp without time zone", - nullable: false, - defaultValue: new NodaTime.LocalDateTime(1, 1, 1, 0, 0)); - } - - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropColumn( - name: "Status", - table: "Documents"); - - migrationBuilder.DropColumn( - name: "ActualReturnTime", - table: "Borrows"); - } - } -} diff --git a/src/Infrastructure/Services/MailService.cs b/src/Infrastructure/Services/MailService.cs index 3a9c7020..32feba59 100644 --- a/src/Infrastructure/Services/MailService.cs +++ b/src/Infrastructure/Services/MailService.cs @@ -51,7 +51,6 @@ public bool SendResetPasswordHtmlMail(string userEmail, string password) request.AddHeader("Content-Type", "application/json"); request.AddParameter("application/json", json, ParameterType.RequestBody); var response = client.Execute(request); - Console.WriteLine(response.Content); return response.IsSuccessStatusCode; } } \ No newline at end of file From e937a56e756aaf7c0069299e16fdda59fd11edeb Mon Sep 17 00:00:00 2001 From: Nguyen Quang Chien Date: Sat, 3 Jun 2023 21:34:21 +0700 Subject: [PATCH 12/13] fix my wholie life --- docker-compose.test.yml | 2 +- .../Folders/Commands/DisableFolderTests.cs | 113 +++--------------- .../Lockers/Commands/AddLockerTests.cs | 57 ++------- 3 files changed, 29 insertions(+), 143 deletions(-) diff --git a/docker-compose.test.yml b/docker-compose.test.yml index 993b5ad1..a1e8f565 100644 --- a/docker-compose.test.yml +++ b/docker-compose.test.yml @@ -9,7 +9,7 @@ services: - '8888:80' environment: - ASPNETCORE_ENVIRONMENT=Testing - - PROFILE_DatabaseSettings__ConnectionString=Server=database;Port=5432;Database=mytestdb;User ID=profiletester;Password=supasupasecured; + - PROFILE_DatabaseSettings__ConnectionString=Server=database;Port=5432;Database=mytestdb;User ID=profiletester;Password=supasupasecured;Include Error Detail=true depends_on: database: condition: service_started diff --git a/tests/Application.Tests.Integration/Folders/Commands/DisableFolderTests.cs b/tests/Application.Tests.Integration/Folders/Commands/DisableFolderTests.cs index 02a9e97b..33aa34c1 100644 --- a/tests/Application.Tests.Integration/Folders/Commands/DisableFolderTests.cs +++ b/tests/Application.Tests.Integration/Folders/Commands/DisableFolderTests.cs @@ -16,36 +16,12 @@ public DisableFolderTests(CustomApiFactory apiFactory) : base(apiFactory) public async Task ShouldDisableFolder_WhenFolderHaveNoDocument() { // Arrange - var room = new Room() - { - Id = Guid.NewGuid(), - Capacity = 24, - IsAvailable = true, - Name = "Kamito's room", - NumberOfLockers = 1, - }; - - var locker = new Locker() - { - Id = Guid.NewGuid(), - Capacity = 47, - Name = "fqwlkjdb sajdbqwk", - IsAvailable = true, - Room = room, - NumberOfFolders = 1, - }; - - var folder = new Folder() - { - Id = Guid.NewGuid(), - Capacity = 12, - IsAvailable = true, - Name = "A Random name", - NumberOfDocuments = 0, - Locker = locker - }; + var department = CreateDepartment(); + var folder = CreateFolder(); + var locker = CreateLocker(folder); + var room = CreateRoom(department, locker); + await AddAsync(room); - await AddAsync(folder); var disableFolderCommand = new DisableFolder.Command() { FolderId = folder.Id @@ -84,35 +60,12 @@ await result.Should().ThrowAsync() public async Task ShouldThrowInvalidOperationException_WhenFolderIsAlreadyDisabled() { // Arrange - var room = new Room() - { - Id = Guid.NewGuid(), - Capacity = 24, - IsAvailable = true, - Name = "Kamito's room!", - NumberOfLockers = 1, - }; - - var locker = new Locker() - { - Id = Guid.NewGuid(), - Capacity = 47, - Name = "fqwlkjdb sajdbqwk!", - IsAvailable = true, - Room = room, - NumberOfFolders = 1, - }; - - var folder = new Folder() - { - Id = Guid.NewGuid(), - Capacity = 12, - IsAvailable = false, - Name = "A Random name!", - NumberOfDocuments = 0, - Locker = locker - }; - await AddAsync(folder); + var department = CreateDepartment(); + var folder = CreateFolder(); + folder.IsAvailable = false; + var locker = CreateLocker(folder); + var room = CreateRoom(department, locker); + await AddAsync(room); var disableFolderCommand = new DisableFolder.Command() { FolderId = folder.Id @@ -135,44 +88,12 @@ await result.Should().ThrowAsync() public async Task ShouldThrowInvalidOperationException_WhenFolderHasDocuments() { // Arrange - var room = new Room() - { - Id = Guid.NewGuid(), - Capacity = 24, - IsAvailable = true, - Name = "Kamito's room!!safqwf!!", - NumberOfLockers = 1, - }; - - var locker = new Locker() - { - Id = Guid.NewGuid(), - Capacity = 47, - Name = "fqwlkjdb sawfqfw wfqwfjdbqwk!", - IsAvailable = true, - Room = room, - NumberOfFolders = 1, - }; - - var folder = new Folder() - { - Id = Guid.NewGuid(), - Capacity = 12, - IsAvailable = true, - Name = "A Randasfwqfawfqom name!", - NumberOfDocuments = 1, - Locker = locker - }; - - var document = new Document() - { - DocumentType = "fqwkfwqbfk", - Id = Guid.NewGuid(), - Title = "wjqk ljfqwjlf qwkhjf ;qikwf", - Folder = folder - }; - - await AddAsync(document); + var department = CreateDepartment(); + var document = CreateNDocuments(1).First(); + var folder = CreateFolder(document); + var locker = CreateLocker(folder); + var room = CreateRoom(department, locker); + await AddAsync(room); var disableFolderCommand = new DisableFolder.Command() { FolderId = folder.Id diff --git a/tests/Application.Tests.Integration/Lockers/Commands/AddLockerTests.cs b/tests/Application.Tests.Integration/Lockers/Commands/AddLockerTests.cs index 2da1413f..bf8fe481 100644 --- a/tests/Application.Tests.Integration/Lockers/Commands/AddLockerTests.cs +++ b/tests/Application.Tests.Integration/Lockers/Commands/AddLockerTests.cs @@ -18,15 +18,8 @@ public AddLockerTests(CustomApiFactory apiFactory) : base(apiFactory) public async Task ShouldReturnLocker_WhenCreateDetailsAreValid() { // Arrange - var room = new Room() - { - Id = Guid.NewGuid(), - Name = new Faker().Name.JobArea(), - Description = new Faker().Lorem.Sentence(), - Capacity = 3, - IsAvailable = true, - NumberOfLockers = 0, - }; + var department = CreateDepartment(); + var room = CreateRoom(department); await AddAsync(room); @@ -62,15 +55,8 @@ public async Task ShouldReturnLocker_WhenCreateDetailsAreValid() public async Task ShouldThrowConflictException_WhenLockerAlreadyExistsInTheSameRoom() { // Arrange - var room = new Room() - { - Id = Guid.NewGuid(), - Name = new Faker().Name.JobArea(), - Description = new Faker().Lorem.Sentence(), - Capacity = 3, - IsAvailable = true, - NumberOfLockers = 0, - }; + var department = CreateDepartment(); + var room = CreateRoom(department); await AddAsync(room); @@ -99,27 +85,13 @@ public async Task ShouldThrowConflictException_WhenLockerAlreadyExistsInTheSameR public async Task ShouldReturnLocker_WhenLockersHasSameNameButInDifferentRooms() { // Arrange - var room1 = new Room() - { - Id = Guid.NewGuid(), - Name = new Faker().Name.JobArea(), - Description = new Faker().Lorem.Sentence(), - Capacity = 3, - IsAvailable = true, - NumberOfLockers = 0, - }; + var department1 = CreateDepartment(); + var room1 = CreateRoom(department1); await AddAsync(room1); - var room2 = new Room() - { - Id = Guid.NewGuid(), - Name = new Faker().Name.JobArea(), - Description = new Faker().Lorem.Sentence(), - Capacity = 3, - IsAvailable = true, - NumberOfLockers = 0, - }; + var department2 = CreateDepartment(); + var room2 = CreateRoom(department2); await AddAsync(room2); @@ -166,16 +138,9 @@ public async Task ShouldReturnLocker_WhenLockersHasSameNameButInDifferentRooms() public async Task ShouldThrowLimitExceededException_WhenGoingOverCapacity() { // Arrange - var room = new Room() - { - Id = Guid.NewGuid(), - Name = new Faker().Name.JobArea(), - Description = new Faker().Lorem.Sentence(), - Capacity = 1, - IsAvailable = true, - NumberOfLockers = 0, - }; - + var department = CreateDepartment(); + var room = CreateRoom(department); + room.Capacity = 1; await AddAsync(room); var addLockerCommand = new AddLocker.Command() From 7f909c0d5a1d32ad950d9bf288d83cc7ae84d352 Mon Sep 17 00:00:00 2001 From: Nguyen Quang Chien Date: Sat, 3 Jun 2023 21:40:05 +0700 Subject: [PATCH 13/13] jk im good --- .../Folders/Commands/DisableFolderTests.cs | 4 ++++ .../Lockers/Commands/AddLockerTests.cs | 6 ++++++ 2 files changed, 10 insertions(+) diff --git a/tests/Application.Tests.Integration/Folders/Commands/DisableFolderTests.cs b/tests/Application.Tests.Integration/Folders/Commands/DisableFolderTests.cs index 33aa34c1..cf2628d7 100644 --- a/tests/Application.Tests.Integration/Folders/Commands/DisableFolderTests.cs +++ b/tests/Application.Tests.Integration/Folders/Commands/DisableFolderTests.cs @@ -1,5 +1,6 @@ using Application.Common.Exceptions; using Application.Folders.Commands; +using Domain.Entities; using Domain.Entities.Physical; using FluentAssertions; using Xunit; @@ -37,6 +38,7 @@ public async Task ShouldDisableFolder_WhenFolderHaveNoDocument() Remove(folder); Remove(locker); Remove(room); + Remove(await FindAsync(department.Id)); } [Fact] @@ -82,6 +84,7 @@ await result.Should().ThrowAsync() Remove(folder); Remove(locker); Remove(room); + Remove(await FindAsync(department.Id)); } [Fact] @@ -111,5 +114,6 @@ await result.Should().ThrowAsync() Remove(folder); Remove(locker); Remove(room); + Remove(await FindAsync(department.Id)); } } \ No newline at end of file diff --git a/tests/Application.Tests.Integration/Lockers/Commands/AddLockerTests.cs b/tests/Application.Tests.Integration/Lockers/Commands/AddLockerTests.cs index bf8fe481..b3ff5025 100644 --- a/tests/Application.Tests.Integration/Lockers/Commands/AddLockerTests.cs +++ b/tests/Application.Tests.Integration/Lockers/Commands/AddLockerTests.cs @@ -1,6 +1,7 @@ using Application.Common.Exceptions; using Application.Lockers.Commands; using Bogus; +using Domain.Entities; using Domain.Entities.Physical; using Domain.Exceptions; using FluentAssertions; @@ -49,6 +50,7 @@ public async Task ShouldReturnLocker_WhenCreateDetailsAreValid() // Cleanup var roomEntity = await FindAsync(room.Id); Remove(roomEntity); + Remove(await FindAsync(department.Id)); } [Fact] @@ -79,6 +81,7 @@ public async Task ShouldThrowConflictException_WhenLockerAlreadyExistsInTheSameR // Cleanup var roomEntity = await FindAsync(room.Id); Remove(roomEntity); + Remove(await FindAsync(department.Id)); } [Fact] @@ -132,6 +135,8 @@ public async Task ShouldReturnLocker_WhenLockersHasSameNameButInDifferentRooms() var room2Entity = await FindAsync(room2.Id); Remove(room1Entity); Remove(room2Entity); + Remove(await FindAsync(department1.Id)); + Remove(await FindAsync(department2.Id)); } [Fact] @@ -170,5 +175,6 @@ public async Task ShouldThrowLimitExceededException_WhenGoingOverCapacity() // Cleanup var roomEntity = await FindAsync(room.Id); Remove(roomEntity); + Remove(await FindAsync(department.Id)); } } \ No newline at end of file