Example #1
0
            public async Task <int> Handle(ProvisionTenantCommand request, CancellationToken cancellationToken)
            {
                using (var transaction = await context.Database.BeginTransactionAsync())
                {
                    var sqlTransaction = transaction.GetDbTransaction();

                    await context.PaidTimeOffPolicies.AddRangeAsync(new[]
                    {
                        new PaidTimeOffPolicyEntity {
                            TenantId = request.TenantId, EmployeeLevel = 1, Name = "Standard 1", IsDefaultForEmployeeLevel = true, MaxPtoHours = 320.0m, PtoAccrualRate = 10.0m
                        },
                        new PaidTimeOffPolicyEntity {
                            TenantId = request.TenantId, EmployeeLevel = 2, Name = "Standard 2", IsDefaultForEmployeeLevel = true, MaxPtoHours = 320.0m, PtoAccrualRate = 10.6666m
                        },
                        new PaidTimeOffPolicyEntity {
                            TenantId = request.TenantId, EmployeeLevel = 3, Name = "Standard 3", IsDefaultForEmployeeLevel = true, MaxPtoHours = 320.0m, PtoAccrualRate = 11.3333m
                        },
                        new PaidTimeOffPolicyEntity {
                            TenantId = request.TenantId, EmployeeLevel = 4, Name = "Standard 4", IsDefaultForEmployeeLevel = true, MaxPtoHours = 320.0m, PtoAccrualRate = 12.0m
                        },
                        new PaidTimeOffPolicyEntity {
                            TenantId = request.TenantId, EmployeeLevel = 5, Name = "Standard 5", IsDefaultForEmployeeLevel = true, MaxPtoHours = 320.0m, PtoAccrualRate = 12.6666m
                        },
                        new PaidTimeOffPolicyEntity {
                            TenantId = request.TenantId, EmployeeLevel = 6, Name = "Standard 6", IsDefaultForEmployeeLevel = true, MaxPtoHours = 320.0m, PtoAccrualRate = 13.3333m
                        },
                        new PaidTimeOffPolicyEntity {
                            TenantId = request.TenantId, EmployeeLevel = 7, Name = "Standard 7", IsDefaultForEmployeeLevel = true, MaxPtoHours = 320.0m, PtoAccrualRate = 14.0m
                        },
                        new PaidTimeOffPolicyEntity {
                            TenantId = request.TenantId, EmployeeLevel = 8, Name = "Standard 8", IsDefaultForEmployeeLevel = true, MaxPtoHours = 320.0m, PtoAccrualRate = 14.6666m
                        },
                        new PaidTimeOffPolicyEntity {
                            TenantId = request.TenantId, EmployeeLevel = 8, Name = "Unlimited 8", IsDefaultForEmployeeLevel = false, AllowsUnlimitedPto = true
                        },
                        new PaidTimeOffPolicyEntity {
                            TenantId = request.TenantId, EmployeeLevel = 9, Name = "Standard 9", IsDefaultForEmployeeLevel = true, MaxPtoHours = 320.0m, PtoAccrualRate = 15.3333m
                        },
                        new PaidTimeOffPolicyEntity {
                            TenantId = request.TenantId, EmployeeLevel = 9, Name = "Unlimited 9", IsDefaultForEmployeeLevel = false, AllowsUnlimitedPto = true
                        },
                        new PaidTimeOffPolicyEntity {
                            TenantId = request.TenantId, EmployeeLevel = 10, Name = "Standard 10", IsDefaultForEmployeeLevel = true, MaxPtoHours = 320.0m, PtoAccrualRate = 16.0m
                        },
                        new PaidTimeOffPolicyEntity {
                            TenantId = request.TenantId, EmployeeLevel = 10, Name = "Unlimited 10", IsDefaultForEmployeeLevel = false, AllowsUnlimitedPto = true
                        },
                    });

                    await context.SaveChangesAsync();

                    await transaction.CommitAsync();

                    return(request.TenantId);
                }
            }
        public async Task SeedAllAsync(CancellationToken cancellationToken)
        {
            if (context.PaidTimeOffPolicies.Any())
            {
                logger.LogInformation("Database has already been initialized with basic data. Nothing to do.");
                return;
            }

            await SeedPtoPoliciesAsync();
            await SeedCurrenciesAsync();

            await context.SaveChangesAsync(cancellationToken);
        }
        private async Task CreateDefaultTenantsAsync(DbTransaction sqlTransaction, CustomerEntity customer, IEnumerable <TenantViewModel> tenants)
        {
            await facade.TurnOffIdentityIncrementAsync(nameof(IApplicationWriteDbContext.Tenants), sqlTransaction);

            foreach (var tenant in tenants.Select(t => tenantViewModelToDbEntityMapper.Map(t)))
            {
                tenant.CustomerId = customer.Id;
                await context.Tenants.AddAsync(tenant);

                await context.TenantAspNetUsers.AddAsync(new TenantAspNetUserEntity { AspNetUsersId = customer.AspNetUsersId, TenantId = tenant.Id });
            }
            await context.SaveChangesAsync();

            await facade.TurnOnIdentityIncrementAsync(nameof(IApplicationWriteDbContext.Tenants), sqlTransaction);
        }
            public async Task <TenantViewModel> Handle(AddOrUpdateTenantCommand request, CancellationToken cancellationToken)
            {
                var isNew = false;

                // PRESENTATION/APPLICATION LAYER
                var tenantViewModel = request.Tenant;

                // PERSISTENCE LAYER
                using (var transaction = await context.Database.BeginTransactionAsync())
                {
                    var sqlTransaction = transaction.GetDbTransaction();

                    TenantEntity tenantEntity = null;

                    // Check for existing slug/key on a different tenant.
                    var slug = tenantViewModel.Slug.Sluggify();
                    var id   = await facade.QueryFirstOrDefaultAsync <int?>(
                        @"SELECT TOP 1 Id FROM Tenants WITH(NOLOCK) WHERE Slug = @slug AND (@Id IS NULL OR Id <> @Id)",
                        new { slug, tenantViewModel.Id }, sqlTransaction);

                    if (id != null)
                    {
                        throw new ApplicationLayerException("Tenant slug already in use by another tenant.");
                    }
                    else
                    {
                        tenantViewModel.Slug = slug;
                    }

                    // Update.
                    if (tenantViewModel.Id != null)
                    {
                        var tenantId = (int)tenantViewModel.Id;
                        tenantEntity = context.Tenants.Find(tenantId);
                        if (tenantEntity == null)
                        {
                            throw new ApplicationLayerException($"Tenant with ID {tenantId} is in the customer's access list, but does not seem to exist.");
                        }
                        mapper.Map(tenantViewModel, tenantEntity);
                    }
                    else
                    {
                        // Add.
                        var customerId = await facade.QueryFirstOrDefaultAsync <int>("SELECT TOP 1 Id FROM Customers WHERE AspNetUsersId = @AspNetUsersId", request, sqlTransaction);

                        tenantEntity            = mapper.Map(tenantViewModel);
                        tenantEntity.CustomerId = customerId;
                        await context.Tenants.AddAsync(tenantEntity);

                        isNew = true;
                    }
                    await context.SaveChangesAsync();

                    tenantViewModel.Id = tenantEntity.Id;
                    if (isNew)
                    {
                        await context.TenantAspNetUsers.AddAsync(new TenantAspNetUserEntity { AspNetUsersId = request.AspNetUsersId, TenantId = tenantEntity.Id });

                        await context.SaveChangesAsync();
                    }
                    await transaction.CommitAsync();
                }
                return(tenantViewModel);
            }
Example #5
0
            public async Task <RegisterOrUpdateEmployeeCommand> Handle(RegisterOrUpdateEmployeeCommand request, CancellationToken cancellationToken)
            {
                // Map the view model to a domain entity. We are now in the realm of business entities and logic.
                var employee = employeeVmToDomainEntityMapper.MapToDomainEntity(request);

                var isNewEmployee = employee.Id == null;

                // Look up manager from DB.
                EmployeeEntity managerEntity = null;

                if (request.ManagerId != null)
                {
                    managerEntity = await context.Employees.FindAsync(request.ManagerId);

                    if (managerEntity == null)
                    {
                        throw new EmployeeException($"Manager with ID {request.ManagerId} not found.");
                    }
                    // Convert to domain entity as well.
                    employee = employee.WithManager(employeeDomainToDbEntityMapper.MapToDomainEntity(managerEntity));
                }

                // Look up subordinates and map those to domain entities.
                var subordinateIds      = request.SubordinateIds.ToArray();
                var subordinateEntities = await(from e in context.Employees.AsNoTracking() where subordinateIds.Contains(e.Id) select e).ToArrayAsync();

                employee = employee.WithSubordinates(from e in subordinateEntities select employeeDomainToDbEntityMapper.MapToDomainEntity(e));

                // Get PTO policy for this employee and map to domain entity.
                var policyEntity = await context.PaidTimeOffPolicies.AsNoTracking().FirstOrDefaultAsync(p => p.Id == request.PaidTimeOffPolicyId);

                if (policyEntity == null)
                {
                    throw new EmployeeException($"PTO policy not found.");
                }
                var policy = ptoPolicyDomainToDbEntityMapper.MapToDomainEntity(policyEntity);

                employee = employee.WithPaidTimeOffPolicy(policy);

                // Check the validity of the employee's PTO hours and other business logic. All business rule validation is still happening within the Domain layer...
                employee.AssertAggregates();
                employee.VerifyEmployeeManagerAndSubordinates();
                if (isNewEmployee)
                {
                    employee.VerifyStartingPtoHoursAreValid();
                }
                else
                {
                    employee.VerifyPtoHoursAreValid();
                }
                // End business logic.

                // Convert back to persistence entity.
                var employeeEntity = employeeDomainToDbEntityMapper.MapToDbEntity(employee);

                employeeEntity.TenantId = TenantId;
                var entry = await context.Employees.AddAsync(employeeEntity);

                if (!isNewEmployee)
                {
                    entry.State = EntityState.Modified;
                }

                // Delete previous entity relationships.
                context.EmployeeManagers.RemoveRange(context.EmployeeManagers.Where(em => em.EmployeeId == employee.Id));
                context.EmployeeManagers.RemoveRange(context.EmployeeManagers.Where(em => em.ManagerId == employee.Id));

                // Commit transaction to DB.
                await context.SaveChangesAsync(cancellationToken);

                // Create new relationships.
                if (employee.Manager != null)
                {
                    await context.EmployeeManagers.AddAsync(new EmployeeManagerEntity { TenantId = TenantId, EmployeeId = employeeEntity.Id, ManagerId = managerEntity.Id });
                }
                await context.EmployeeManagers.AddRangeAsync(from s in subordinateEntities select new EmployeeManagerEntity {
                    TenantId = TenantId, EmployeeId = s.Id, ManagerId = employeeEntity.Id
                });

                // Commit transaction to DB.
                if (context.HasChanges)
                {
                    await context.SaveChangesAsync(cancellationToken);
                }

                // Set ID of domain entity.
                employee.Id = employeeEntity.Id;

                // Kick off domain events.
                employee.CreateEmployeeRegisteredEvent(dateTimeService);
                await employee.DispatchDomainEventsAsync();

                // Map from domain entity back to VM (command) and return that.
                return(employeeVmToDomainEntityMapper.MapToViewModel(employee));
            }
Example #6
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);
            }