public async Task <PaidTimeOffRequestValidationResult> Handle(ValidateRequestedPaidTimeOffHoursQuery request, CancellationToken cancellationToken)
            {
                var viewModel        = request.ValidationRequest;
                var tentativeRequest = mapper.Map(viewModel);
                var today            = DateTime.Today;
                var parms            = new { request.AspNetUsersId, viewModel.EndDate, viewModel.StartDate, viewModel.ForEmployeeId, viewModel.HoursRequested, viewModel.TenantId };

                // TODO: add domain validation/authorization if submitting on behalf of another employee (i.e. manager is submitting).
                var paidTimeOffPolicy = await facade.QueryFirstOrDefaultAsync <PaidTimeOffPolicy>(@"
                    SELECT TOP 1 p.[Id]
                          ,p.[AllowsUnlimitedPto]
                          ,p.[EmployeeLevel]
                          ,p.[IsDefaultForEmployeeLevel]
                          ,p.[MaxPtoHours]
                          ,p.[Name]
                          ,p.[PtoAccrualRate]
                      FROM PaidTimeOffPolicies p WITH(NOLOCK)
                      JOIN Employees e WITH(NOLOCK) ON e.PaidTimeOffPolicyId = p.Id AND e.TenantId = p.TenantId
                      WHERE ((@ForEmployeeId IS NULL AND e.AspNetUsersId = @AspNetUsersId) OR e.Id = @ForEmployeeId) AND e.TenantId = @TenantId
                ", parms, cancellationToken : cancellationToken);

                if (paidTimeOffPolicy == null)
                {
                    throw new NotFoundException("PTO policy not found or invalid.");
                }
                var existingRequests = await facade.QueryAsync <PaidTimeOffRequest>(@"
                    SELECT p.[Id]
                          ,p.[ApprovalStatus]
                          ,p.[EndDate]
                          ,p.[ForEmployeeId]
                          ,p.[HoursRequested]
                          ,p.[StartDate]
                          ,p.[Paid]
                          ,p.[SubmittedById]
                      FROM [PaidTimeOffRequests] p WITH(NOLOCK)
                      JOIN Employees e WITH(NOLOCK) ON e.Id = p.ForEmployeeId AND e.TenantId = p.TenantId
                      WHERE ((@ForEmployeeId IS NULL AND e.AspNetUsersId = @AspNetUsersId) OR e.Id = @ForEmployeeId) AND e.TenantId = @TenantId
                ", parms, cancellationToken : cancellationToken);

                return(paidTimeOffRequestService.ValidatePaidTimeOffRequest(tentativeRequest, existingRequests, paidTimeOffPolicy, today));
            }
Example #2
0
            public async Task <PaidTimeOffRequestValidationResult> Handle(ValidateRequestedPaidTimeOffHoursQuery request, CancellationToken cancellationToken)
            {
                var tentativeRequest = mapper.Map(request.ValidationRequest);
                var today            = DateTime.Today;

                var paidTimeOffPolicy = await facade.QueryFirstOrDefaultAsync <PaidTimeOffPolicy>(@"
                    SELECT TOP 1 p.[Id]
                          ,p.[AllowsUnlimitedPto]
                          ,p.[EmployeeLevel]
                          ,p.[IsDefaultForEmployeeLevel]
                          ,p.[MaxPtoHours]
                          ,p.[Name]
                          ,p.[PtoAccrualRate]
                      FROM PaidTimeOffPolicies p WITH(NOLOCK)
                      JOIN Employees e WITH(NOLOCK) ON e.PaidTimeOffPolicyId = p.Id AND e.TenantId = p.TenantId
                      WHERE e.Id = @ForEmployeeId AND e.TenantId = @TenantId
                ", request.ValidationRequest, cancellationToken : cancellationToken);

                if (paidTimeOffPolicy == null)
                {
                    throw new NotFoundException("PTO policy not found or invalid.");
                }
                var existingRequests = await facade.QueryAsync <PaidTimeOffRequest>(@"
                    SELECT [Id]
                          ,[ApprovalStatus]
                          ,[EndDate]
                          ,[ForEmployeeId]
                          ,[HoursRequested]
                          ,[StartDate]
                          ,[Paid]
                          ,[SubmittedById]
                      FROM [PaidTimeOffRequests] WITH(NOLOCK)
                      WHERE @ForEmployeeId = @ForEmployeeId AND TenantId = @TenantId
                ", request.ValidationRequest, cancellationToken : cancellationToken);

                return(paidTimeOffRequestService.ValidatePaidTimeOffRequest(tentativeRequest, existingRequests, paidTimeOffPolicy, today));
            }
Example #3
0
            public async Task <SubmitNewPaidTimeOffRequestViewModel> Handle(SubmitNewPaidTimeOffRequestCommand request, CancellationToken cancellationToken)
            {
                var today = DateTime.Today;

                // PRESENTATION/APPLICATION LAYER
                var timeOffRequestViewModel = request.PaidTimeOffRequest;

                timeOffRequestViewModel.StartDate = timeOffRequestViewModel.StartDate.Date;
                timeOffRequestViewModel.EndDate   = timeOffRequestViewModel.EndDate.Date;

                // PERSISTENCE LAYER

                var submittedByEntity = await facade.QueryFirstOrDefaultAsync <EmployeeEntity>(@"SELECT TOP 1 * FROM Employees WITH(NOLOCK) WHERE AspNetUsersId = @AspNetUsersId AND TenantId = @TenantId", new { request.AspNetUsersId, request.PaidTimeOffRequest.TenantId });

                var forEmployeeEntity =
                    timeOffRequestViewModel.ForEmployeeId == null?
                    context.Employees.Include(e => e.PaidTimeOffPolicy).Include(e => e.ForPaidTimeOffRequests).FirstOrDefault(e => e.AspNetUsersId == request.AspNetUsersId && e.TenantId == request.PaidTimeOffRequest.TenantId) :
                        context.Employees.Include(e => e.PaidTimeOffPolicy).Include(e => e.ForPaidTimeOffRequests).FirstOrDefault(e => e.Id == timeOffRequestViewModel.ForEmployeeId && e.TenantId == request.PaidTimeOffRequest.TenantId)
                ;

                // TODO: if submitting on behalf of another employee, use the domain layer to validate that they have the privileges to do so.

                if (
                    forEmployeeEntity == null ||
                    submittedByEntity == null)
                {
                    throw new ApplicationLayerException(@"Unable to query employees while trying to create time off request aggregate.");
                }

                // DOMAIN LAYER
                var forEmployee         = mapper.MapDbEntityToDomainEntity <EmployeeEntity, Employee>(forEmployeeEntity);
                var submittedByEmployee = mapper.MapDbEntityToDomainEntity <EmployeeEntity, Employee>(submittedByEntity);
                var paidTimeOffPolicy   = mapper.MapDbEntityToDomainEntity <PaidTimeOffPolicyEntity, PaidTimeOffPolicy>(forEmployeeEntity.PaidTimeOffPolicy);
                var existingRequests    = (from req in forEmployeeEntity.ForPaidTimeOffRequests select mapper.MapDbEntityToDomainEntity <PaidTimeOffRequestEntity, PaidTimeOffRequest>(req)).ToList();

                // Build up the Domain aggregate entity so that complex business logic can be executed against it. In an enterprise (non-demo) solution this
                // would likely involve special rules involving accrued hours, whether the company allows going negative in PTO hours, managerial overrides,
                // etc. The point is that the entities, and the logic which operates against them, are separate from view models and database persistence models.
                var submittedRequest =
                    mapper.MapViewModelToDomainEntity <SubmitNewPaidTimeOffRequestViewModel, PaidTimeOffRequest>(request.PaidTimeOffRequest)
                    .WithForEmployee(forEmployee)
                    .WithSubmittedBy(submittedByEmployee)
                    .WithPaidTimeOffPolicy(paidTimeOffPolicy);

                // Ensure the aggregate is in a valid state with which to perform business logic.
                submittedRequest.ValidateAggregate();

                // Perform basic validation against the request to make sure that the employee is OK to submit this paid time off request. Once again, the logic
                // inside the service is naive and overly-simplistic. A real-life solution would be much more involved.
                var validationResult = paidTimeOffRequestService.ValidatePaidTimeOffRequest(submittedRequest, existingRequests, paidTimeOffPolicy, today);

                timeOffRequestViewModel.Result = validationResult;
                if (validationResult != PaidTimeOffRequestValidationResult.OK)
                {
                    return(timeOffRequestViewModel);
                }

                // Submit the time off request (changes Domain state and creates event).
                submittedRequest = submittedRequest.Submit();

                // PERSISTENCE LAYER
                var paidTimeOffRequestEntity = mapper.MapDomainEntityToDbEntity <PaidTimeOffRequest, PaidTimeOffRequestEntity>(submittedRequest);

                paidTimeOffRequestEntity.TenantId = timeOffRequestViewModel.TenantId;
                await context.PaidTimeOffRequests.AddAsync(paidTimeOffRequestEntity, cancellationToken);

                await context.SaveChangesAsync(cancellationToken);

                // PRESENTATION LAYER
                request.PaidTimeOffRequest.CreatedPaidTimeOffRequest = mapper.MapDomainEntityToViewModel <PaidTimeOffRequest, PaidTimeOffRequestViewModel>(submittedRequest);

                // DISPATCH DOMAIN EVENTS
                await submittedRequest.DispatchDomainEventsAsync();

                return(request.PaidTimeOffRequest);
            }