Skip to content
This repository has been archived by the owner on Jul 12, 2024. It is now read-only.

Commit

Permalink
feat(customer): add customer use case
Browse files Browse the repository at this point in the history
  • Loading branch information
foxminchan committed May 14, 2024
1 parent 8954efa commit f643ede
Show file tree
Hide file tree
Showing 22 changed files with 324 additions and 2 deletions.
3 changes: 2 additions & 1 deletion src/RookieShop.ApiService/Filters/IdempotencyFilter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@ public sealed class IdempotencyFilter(IRedisService redisService) : IEndpointFil
if (string.IsNullOrEmpty(requestId))
{
context.HttpContext.Response.StatusCode = StatusCodes.Status400BadRequest;
throw new ValidationException("X-Idempotency-Key header is required for POST and PATCH requests.");
throw new ValidationException(
$"{HeaderName.IdempotencyKey} header is required for POST and PATCH requests.");
}

var cacheKey = $"{requestMethod}:{requestPath}:{requestId}";
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
using Ardalis.Result;
using RookieShop.Domain.Entities.CustomerAggregator.Enums;
using RookieShop.Domain.Entities.CustomerAggregator.Primitives;
using RookieShop.Domain.SharedKernel;

namespace RookieShop.Application.Customers.Commands.Create;

public sealed record CreateCustomerCommand(string Name, string Email, string Phone, Gender Gender, string? AccountId)
: ICommand<Result<CustomerId>>;
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
using System.Text.Json;
using Ardalis.Result;
using Microsoft.Extensions.Logging;
using RookieShop.Domain.Entities.CustomerAggregator;
using RookieShop.Domain.Entities.CustomerAggregator.Primitives;
using RookieShop.Domain.SharedKernel;

namespace RookieShop.Application.Customers.Commands.Create;

public sealed class CreateCustomerHandler(IRepository<Customer> repository, ILogger<CreateCustomerHandler> logger)
: ICommandHandler<CreateCustomerCommand, Result<CustomerId>>
{
public async Task<Result<CustomerId>> Handle(CreateCustomerCommand request, CancellationToken cancellationToken)
{
Customer customer = new(request.Name, request.Email, request.Phone, request.Gender, request.AccountId);

logger.LogInformation("[{Command}] - Creating customer {@Customer}", nameof(CreateCustomerCommand),
JsonSerializer.Serialize(customer));

var result = await repository.AddAsync(customer, cancellationToken);

return result.Id;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
using FluentValidation;
using RookieShop.Domain.Constants;

namespace RookieShop.Application.Customers.Commands.Create;

public sealed class CreateCustomerValidator : AbstractValidator<CreateCustomerCommand>
{
public CreateCustomerValidator()
{
RuleFor(x => x.Name)
.NotEmpty()
.MaximumLength(DataLength.Medium);

RuleFor(x => x.Email)
.NotEmpty()
.MaximumLength(DataLength.Medium)
.EmailAddress();

RuleFor(x => x.Phone)
.NotEmpty()
.MaximumLength(DataLength.Small);

RuleFor(x => x.Gender)
.IsInEnum();

RuleFor(x => x.AccountId)
.MaximumLength(DataLength.Medium);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
using Ardalis.Result;
using RookieShop.Domain.Entities.CustomerAggregator.Primitives;
using RookieShop.Domain.SharedKernel;

namespace RookieShop.Application.Customers.Commands.Delete;

public sealed record DeleteCustomerCommand(CustomerId Id) : ICommand<Result>;
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
using Ardalis.GuardClauses;
using Ardalis.Result;
using RookieShop.Domain.Entities.CustomerAggregator;
using RookieShop.Domain.SharedKernel;

namespace RookieShop.Application.Customers.Commands.Delete;

public sealed class DeleteCustomerHandler(IRepository<Customer> repository)
: ICommandHandler<DeleteCustomerCommand, Result>
{
public async Task<Result> Handle(DeleteCustomerCommand request, CancellationToken cancellationToken)
{
var customer = await repository.GetByIdAsync(request.Id, cancellationToken);

Guard.Against.NotFound(request.Id, customer);

await repository.DeleteAsync(customer, cancellationToken);

return Result.Success();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
using FluentValidation;

namespace RookieShop.Application.Customers.Commands.Delete;

public sealed class DeleteCustomerValidator : AbstractValidator<DeleteCustomerCommand>
{
public DeleteCustomerValidator() => RuleFor(x => x.Id).NotEmpty();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
using Ardalis.Result;
using RookieShop.Application.Customers.DTOs;
using RookieShop.Domain.Entities.CustomerAggregator.Enums;
using RookieShop.Domain.Entities.CustomerAggregator.Primitives;
using RookieShop.Domain.SharedKernel;

namespace RookieShop.Application.Customers.Commands.Update;

public sealed record UpdateCustomerCommand(
CustomerId Id,
string Name,
string Email,
string Phone,
Gender Gender,
string? AccountId) : ICommand<Result<CustomerDto>>;
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
using System.Text.Json;
using Ardalis.GuardClauses;
using Ardalis.Result;
using Microsoft.Extensions.Logging;
using RookieShop.Application.Customers.Commands.Create;
using RookieShop.Application.Customers.DTOs;
using RookieShop.Domain.Entities.CustomerAggregator;
using RookieShop.Domain.SharedKernel;

namespace RookieShop.Application.Customers.Commands.Update;

public sealed class UpdateCustomerHandler(IRepository<Customer> repository, ILogger<CreateCustomerHandler> logger)
: ICommandHandler<UpdateCustomerCommand, Result<CustomerDto>>
{
public async Task<Result<CustomerDto>> Handle(UpdateCustomerCommand request, CancellationToken cancellationToken)
{
var customer = await repository.GetByIdAsync(request.Id, cancellationToken);

Guard.Against.NotFound(request.Id, customer);

customer.Update(request.Name, request.Email, request.Phone, request.Gender, request.AccountId);

logger.LogInformation("[{Command}] - Updating customer {@Customer}", nameof(UpdateCustomerCommand),
JsonSerializer.Serialize(customer));

await repository.UpdateAsync(customer, cancellationToken);

return customer.ToCustomerDto();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
using FluentValidation;
using RookieShop.Domain.Constants;

namespace RookieShop.Application.Customers.Commands.Update;

public sealed class UpdateCustomerValidator : AbstractValidator<UpdateCustomerCommand>
{
public UpdateCustomerValidator()
{
RuleFor(x => x.Id)
.NotEmpty();

RuleFor(x => x.Name)
.NotEmpty()
.MaximumLength(DataLength.Medium);

RuleFor(x => x.Email)
.NotEmpty()
.MaximumLength(DataLength.Medium)
.EmailAddress();

RuleFor(x => x.Phone)
.NotEmpty()
.MaximumLength(DataLength.Small);

RuleFor(x => x.Gender)
.IsInEnum();

RuleFor(x => x.AccountId)
.MaximumLength(DataLength.Medium);
}
}
12 changes: 12 additions & 0 deletions src/RookieShop.Application/Customers/DTOs/CustomerDto.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
using RookieShop.Domain.Entities.CustomerAggregator.Enums;
using RookieShop.Domain.Entities.CustomerAggregator.Primitives;

namespace RookieShop.Application.Customers.DTOs;

public sealed record CustomerDto(
CustomerId Id,
string Name,
string Email,
string Phone,
Gender Gender,
string? AccountId);
12 changes: 12 additions & 0 deletions src/RookieShop.Application/Customers/DTOs/DomainToDtoMapper.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
using RookieShop.Domain.Entities.CustomerAggregator;

namespace RookieShop.Application.Customers.DTOs;

public static class DomainToDtoMapper
{
public static CustomerDto ToCustomerDto(this Customer customer) =>
new(customer.Id, customer.Name, customer.Email, customer.Phone, customer.Gender, customer.AccountId);

public static IEnumerable<CustomerDto> ToCustomerDto(this IEnumerable<Customer> customers) =>
customers.Select(ToCustomerDto);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
using Ardalis.GuardClauses;
using Ardalis.Result;
using RookieShop.Application.Customers.DTOs;
using RookieShop.Domain.Entities.CustomerAggregator;
using RookieShop.Domain.Entities.CustomerAggregator.Specifications;
using RookieShop.Domain.SharedKernel;

namespace RookieShop.Application.Customers.Queries.Get;

public sealed class GetCustomerHandler(IReadRepository<Customer> repository)
: IQueryHandler<GetCustomerQuery, Result<CustomerDto>>
{
public async Task<Result<CustomerDto>> Handle(GetCustomerQuery request, CancellationToken cancellationToken)
{
CustomerByIdSpec spec = new(request.Id);

var customer = await repository.FirstOrDefaultAsync(spec, cancellationToken);

Guard.Against.NotFound(request.Id, customer);

return customer.ToCustomerDto();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
using Ardalis.Result;
using RookieShop.Application.Customers.DTOs;
using RookieShop.Domain.Entities.CustomerAggregator.Primitives;
using RookieShop.Domain.SharedKernel;

namespace RookieShop.Application.Customers.Queries.Get;

public sealed record GetCustomerQuery(CustomerId Id) : IQuery<Result<CustomerDto>>;
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
using FluentValidation;

namespace RookieShop.Application.Customers.Queries.Get;

public sealed class GetCustomerValidator : AbstractValidator<GetCustomerQuery>
{
public GetCustomerValidator() => RuleFor(x => x.Id).NotEmpty();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
using Ardalis.Result;
using RookieShop.Application.Customers.DTOs;
using RookieShop.Domain.Entities.CustomerAggregator;
using RookieShop.Domain.Entities.CustomerAggregator.Specifications;
using RookieShop.Domain.SharedKernel;

namespace RookieShop.Application.Customers.Queries.List;

public sealed class ListCustomersHandler(IReadRepository<Customer> repository)
: IQueryHandler<ListCustomersQuery, PagedResult<IEnumerable<CustomerDto>>>
{
public async Task<PagedResult<IEnumerable<CustomerDto>>> Handle(ListCustomersQuery request,
CancellationToken cancellationToken)
{
CustomersFilterSpec spec = new(request.PageIndex, request.PageSize, request.Name);

var customers = await repository.ListAsync(spec, cancellationToken);

var totalRecords = await repository.CountAsync(cancellationToken);

var totalPages = (int)Math.Ceiling(totalRecords / (double)request.PageSize);

var pagedInfo = new PagedInfo(request.PageIndex, request.PageSize, totalPages, totalRecords);

return new(pagedInfo, customers.ToCustomerDto());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
using Ardalis.Result;
using RookieShop.Application.Customers.DTOs;
using RookieShop.Domain.SharedKernel;

namespace RookieShop.Application.Customers.Queries.List;

public sealed record ListCustomersQuery(int PageIndex, int PageSize, string? Name)
: IQuery<PagedResult<IEnumerable<CustomerDto>>>;
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
using FluentValidation;

namespace RookieShop.Application.Customers.Queries.List;

public sealed class ListCustomersValidator : AbstractValidator<ListCustomersQuery>
{
public ListCustomersValidator()
{
RuleFor(x => x.PageIndex).GreaterThanOrEqualTo(1);
RuleFor(x => x.PageSize).GreaterThanOrEqualTo(0);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ namespace RookieShop.Domain.Entities.CategoryAggregator.Specifications;

public sealed class CategoriesFilterSpec : Specification<Category>
{
public CategoriesFilterSpec(int pageIndex = 1, int pageSize = 0)
public CategoriesFilterSpec(int pageIndex, int pageSize)
{
if (pageSize == 0) pageSize = int.MaxValue;

Expand Down
9 changes: 9 additions & 0 deletions src/RookieShop.Domain/Entities/CustomerAggregator/Customer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -32,4 +32,13 @@ public Customer(string name, string email, string phone, Gender gender, string?
public string? AccountId { get; set; }
public ICollection<Order>? Orders { get; set; } = [];
public bool IsDeleted { get; set; } = false;

public void Update(string name, string email, string phone, Gender gender, string? accountId)
{
Name = Guard.Against.NullOrEmpty(name);
Email = Guard.Against.NullOrEmpty(email);
Phone = Guard.Against.NullOrEmpty(phone);
Gender = gender;
AccountId = accountId;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
using Ardalis.Specification;
using RookieShop.Domain.Entities.CustomerAggregator.Primitives;

namespace RookieShop.Domain.Entities.CustomerAggregator.Specifications;

public sealed class CustomerByIdSpec : Specification<Customer>
{
public CustomerByIdSpec(CustomerId customerId) => Query.Where(customer => customer.Id == customerId);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
using Ardalis.Specification;

namespace RookieShop.Domain.Entities.CustomerAggregator.Specifications;

public sealed class CustomersFilterSpec : Specification<Customer>
{
public CustomersFilterSpec(int pageIndex, int pageSize, string? name)
{
if (pageSize == 0) pageSize = int.MaxValue;

Query
.Skip((pageIndex - 1) * pageSize)
.Take(pageSize)
.OrderBy(customer => customer.Name);

if (!string.IsNullOrWhiteSpace(name)) Query.Where(customer => customer.Name.Contains(name));
}
}

0 comments on commit f643ede

Please sign in to comment.