Discounts
Este documento descreve todas as operações disponíveis para o agregado Discounts e seus recursos, seguindo os padrões de Domain-Driven Design (DDD) e Command Query Responsibility Segregation (CQRS).
Índice
- Discounts
- Índice
- Discounts (Descontos)
- DiscountEligibilities (Elegibilidades de Desconto)
- DiscountUsages (Uso de Descontos)
- Tipos e Enums
- Conceitos de DDD e CQRS
- Relacionamentos
- Validações de Domínio
- Observações Importantes
Discounts (Descontos)
Visão Geral
O agregado Discount representa cupons e promoções de desconto disponíveis no sistema. Os descontos podem ser: - Públicos: Disponíveis para todos os usuários - Privados: Associados a usuários específicos (UserDiscounts)
Os descontos são aplicados em pedidos (Orders) e possuem regras de validação complexas.
Commands (Comandos)
1. Criação de Desconto (CreateDiscount)
Contrato: CreateDiscountCommand
Campos Necessários:
Name(string, obrigatório) - Nome do descontoDescription(string, obrigatório) - Descrição do descontoCode(string, obrigatório) - Código do cupom de descontoValue(decimal, obrigatório) - Valor do descontoType(DiscountType enum, obrigatório) - Tipo do descontoPercentage- Desconto em porcentagem (ex: 10 = 10%)FixedAmount- Desconto em valor fixo (ex: 50.00 = R$ 50,00)StartDt(DateTime, obrigatório) - Data de início da validadeEndDt(DateTime, obrigatório) - Data de término da validadeIsActive(bool, obrigatório) - Indica se o desconto está ativoMaxUses(int?, opcional) - Número máximo de usos do descontoMinOrderValue(int?, opcional) - Valor mínimo do pedido para aplicar o descontoIsPublic(bool?, opcional) - Indica se o desconto é público (padrão: false)Observation(string, opcional) - Observações adicionaisAuthor(string, opcional) - Autor da operação
Validações:
Name: Não pode ser vazio; Tamanho máximo: 100 caracteresDescription: Tamanho máximo: 255 caracteresCode: Não pode ser vazio; Tamanho máximo: 50 caracteresValue: Deve ser maior que 0Type: Deve ser um valor válido do enum (Percentage ou FixedAmount)StartDt: Não pode ser vazio; Deve ser uma data futura (com tolerância de 5 minutos)EndDt: Não pode ser vazio; Deve ser posterior à data de inícioMaxUses: Quando fornecido, deve ser maior que 0MinOrderValue: Quando fornecido, deve ser maior que 0
Retorno: CreateDiscountResponse
- Id (Guid) - Identificador do desconto criado
Handler: CreateDiscountHandler
Eventos de Domínio:
DiscountCreatedEvent- Disparado quando um desconto é criado (se implementado)
Regras de Negócio:
- A data de término deve ser sempre posterior à data de início
- Descontos podem ser públicos (disponíveis para todos) ou privados (apenas para usuários específicos via UserDiscount)
- O código do cupom deve ser único (recomendado)
- Se
IsPublic = true, o desconto aparece em buscas públicas - Se
IsPublic = false, o desconto só pode ser usado por usuários que tenham UserDiscount associado
Exemplo de Uso:
{
"name": "Black Friday 2024",
"description": "Desconto especial para Black Friday",
"code": "BLACKFRIDAY2024",
"value": 20,
"type": "Percentage",
"startDt": "2024-11-24T00:00:00",
"endDt": "2024-11-30T23:59:59",
"isActive": true,
"maxUses": 1000,
"minOrderValue": 100,
"isPublic": true,
"observation": "Válido apenas para novos clientes",
"author": "marketing@sistema.com"
}
Exemplo de Resposta:
{
"id": "123e4567-e89b-12d3-a456-426614174000"
}
2. Atualização de Desconto (UpdateDiscount)
Contrato: UpdateDiscountCommand
Campos Necessários:
Id(Guid, obrigatório, JsonIgnore) - Identificador do desconto
Campos Opcionais (todos podem ser atualizados individualmente):
- Name (string, opcional) - Nome do desconto
- Description (string, opcional) - Descrição do desconto
- Code (string, opcional) - Código do cupom
- Value (decimal?, opcional) - Valor do desconto
- Type (DiscountType?, opcional) - Tipo do desconto
- StartDt (DateTime?, opcional) - Data de início
- EndDt (DateTime?, opcional) - Data de término
- IsActive (bool?, opcional) - Status ativo/inativo
- MaxUses (int?, opcional) - Número máximo de usos
- MinOrderValue (int?, opcional) - Valor mínimo do pedido
- IsPublic (bool?, opcional) - Indicador público/privado
- Observation (string, opcional) - Observações
- Author (string, opcional) - Autor da operação
Validações:
Id: Não pode ser vazioName: Quando fornecido, não pode ser vazio e deve ter no máximo 100 caracteresDescription: Quando fornecido, deve ter no máximo 255 caracteresCode: Quando fornecido, não pode ser vazio e deve ter no máximo 50 caracteresValue: Quando fornecido, deve ser maior que 0Type: Quando fornecido, deve ser um valor válido do enumStartDt: Quando fornecido, deve ser uma data futura (com tolerância de 5 minutos)EndDt: Quando fornecido, deve ser posterior à data de início (considerando StartDt atual ou novo)MaxUses: Quando fornecido, deve ser maior que 0MinOrderValue: Quando fornecido, deve ser maior que 0
Retorno: Void
Handler: UpdateDiscountHandler
Regras de Negócio:
- O desconto deve existir no sistema
- Apenas os campos fornecidos são atualizados (atualização parcial/patch)
- As validações de datas são aplicadas considerando os valores atuais e novos:
- Se alterar apenas EndDt, valida contra StartDt atual
- Se alterar apenas StartDt, valida contra EndDt atual
- Se alterar ambos, valida novo EndDt contra novo StartDt
- Cuidado ao atualizar descontos já em uso
- Recomenda-se criar novo desconto em vez de alterar um ativo
Exemplo de Uso:
{
"id": "123e4567-e89b-12d3-a456-426614174000",
"isActive": false,
"observation": "Desconto encerrado antes do previsto",
"author": "admin@sistema.com"
}
3. Exclusão de Desconto (DeleteDiscount)
Contrato: DeleteDiscountCommand
Campos Necessários:
Id(Guid, obrigatório) - Identificador do desconto
Validações:
Id: Não pode ser vazio
Retorno: Void
Handler: DeleteDiscountHandler
Regras de Negócio:
- O desconto deve existir no sistema
- Verifica se há dependências antes de excluir:
- UserDiscounts (descontos associados a usuários)
- Orders (pedidos que usaram o desconto)
- Se houver pedidos históricos, considere inativar em vez de excluir
Recomendação: Use UpdateDiscount com IsActive = false em vez de exclusão para manter histórico.
Queries (Consultas)
1. Buscar Desconto por ID (GetDiscountById)
Contrato: GetDiscountByIdQuery
Campos Necessários:
Id(Guid, obrigatório) - Identificador do desconto
Retorno: GetDiscountByIdResponse
- Id (Guid) - Identificador do desconto
- Name (string) - Nome do desconto
- Description (string) - Descrição
- Code (string) - Código do cupom
- Value (decimal) - Valor do desconto
- Type (DiscountType) - Tipo do desconto
- StartDt (DateTime) - Data de início
- EndDt (DateTime) - Data de término
- IsActive (bool) - Status ativo/inativo
- MaxUses (int?) - Máximo de usos
- MinOrderValue (int?) - Valor mínimo do pedido
- IsPublic (bool?) - Público/privado
- Observation (string) - Observações
- CreatedAt (DateTime) - Data de criação
- UpdatedAt (DateTime?) - Data da última atualização
- AuthorCreated (string) - Autor da criação
- AuthorUpdated (string) - Autor da última atualização
Handler: GetDiscountByIdHandler
Mapper: GetDiscountByIdMapper
Descrição:
- Retorna todos os dados do desconto especificado
- Inclui informações completas de configuração e auditoria
- Se o desconto não existir, lança exceção NotFound
Exemplo de Resposta:
{
"id": "123e4567-e89b-12d3-a456-426614174000",
"name": "Black Friday 2024",
"description": "Desconto especial para Black Friday",
"code": "BLACKFRIDAY2024",
"value": 20,
"type": "Percentage",
"startDt": "2024-11-24T00:00:00Z",
"endDt": "2024-11-30T23:59:59Z",
"isActive": true,
"maxUses": 1000,
"minOrderValue": 100,
"isPublic": true,
"observation": "Válido apenas para novos clientes",
"createdAt": "2024-11-01T10:00:00Z",
"updatedAt": null,
"authorCreated": "marketing@sistema.com",
"authorUpdated": null
}
2. Pesquisar Descontos (SearchDiscounts)
Contrato: SearchDiscountsQuery
Campos Opcionais:
- Type (DiscountType?, opcional) - Filtrar por tipo de desconto (Percentage ou FixedAmount)
- IsActive (bool?, opcional) - Filtrar por status ativo/inativo
- StartDt (DateTime?, opcional) - Filtrar por data de início
- EndDt (DateTime?, opcional) - Filtrar por data de término
- PageNumber (int) - Número da página (padrão: 1)
- PageSize (int) - Tamanho da página (padrão: 10)
Retorno: SearchDiscountsResponse
- Lista paginada de descontos que atendem aos critérios de filtro
- Metadados de paginação (total de registros, total de páginas, etc.)
Descrição:
- Retorna uma lista paginada de descontos conforme os filtros aplicados
- Ordenados por data de criação (mais recentes primeiro)
- Suporta paginação para grandes volumes de dados
Handler: SearchDiscountsHandler
Mapper: SearchDiscountsMapper
Validações:
PageNumber: Deve ser maior que 0PageSize: Deve estar entre 1 e 100
Regras de Negócio:
- Retorna descontos ordenados por data de criação (mais recentes primeiro)
- Suporta paginação para grandes volumes de dados
- Filtros podem ser combinados
Exemplo de Uso:
{
"type": "Percentage",
"isActive": true,
"pageNumber": 1,
"pageSize": 20
}
Exemplo de Resposta:
{
"discounts": [
{
"id": "123e4567-e89b-12d3-a456-426614174000",
"name": "Black Friday 2024",
"code": "BLACKFRIDAY2024",
"value": 20,
"type": "Percentage",
"isActive": true,
"startDt": "2024-11-24T00:00:00Z",
"endDt": "2024-11-30T23:59:59Z"
},
{
"id": "456e7890-e12b-34d5-b678-901234567890",
"name": "Desconto Natal",
"code": "NATAL2024",
"value": 15,
"type": "Percentage",
"isActive": true,
"startDt": "2024-12-01T00:00:00Z",
"endDt": "2024-12-25T23:59:59Z"
}
],
"totalRecords": 2,
"totalPages": 1,
"currentPage": 1,
"pageSize": 20
}
Tipos e Enums
DiscountType
public enum DiscountType
{
Percentage, // Desconto em porcentagem
FixedAmount // Desconto em valor fixo
}
Uso:
- Percentage: O valor representa a porcentagem de desconto (ex: 15 = 15%)
- FixedAmount: O valor representa o montante fixo de desconto (ex: 50.00 = R$ 50,00)
Conceitos de DDD e CQRS
Domain-Driven Design (DDD)
Aggregate Root:
- Discount é um agregado raiz independente
- Mantém consistência de suas propriedades e regras de negócio
- Possui coleções de entidades relacionadas:
- UserDiscounts - Descontos associados a usuários específicos
- Orders - Pedidos que utilizaram o desconto
- Constantes de domínio:
- MaxLengthName = 100
- MaxLengthDescription = 255
- MaxLengthCode = 50
Value Objects:
- Author - Representa o autor da operação
- Observation - Observações sobre o desconto
Domain Methods:
- ValidateDiscountUsage(decimal totalPrice)
- Valida se o desconto pode ser usado para um pedido específico
- Verifica:
- Status ativo
- Período de validade (data atual entre StartDt e EndDt)
- Limite de usos (MaxUses)
- Valor mínimo do pedido (MinOrderValue)
-
Lança
RuleViolationDomainExceptionse alguma regra for violada -
ValidateDiscountUsage()
- Versão sem validação de valor do pedido
-
Valida apenas status, período e limite de usos
-
Update(...)
- Atualiza propriedades do desconto
- Valida invariantes durante atualização
- Aplica regras de negócio (ex: EndDt > StartDt)
Invariantes: - Nome não pode exceder 100 caracteres - Descrição não pode exceder 255 caracteres - Código não pode exceder 50 caracteres - Data de término deve ser posterior à data de início - Valor deve ser maior que 0 - MaxUses, quando definido, deve ser maior que 0 - MinOrderValue, quando definido, deve ser maior que 0
Domain Exceptions:
- RuleViolationDomainException - Quando regras de negócio são violadas
- Desconto inativo
- Fora do período de validade
- Limite de usos atingido
- Valor do pedido abaixo do mínimo
- Validações de campos
CQRS (Command Query Responsibility Segregation)
Commands (Comandos):
- CreateDiscountCommand - Cria um novo desconto
- UpdateDiscountCommand - Atualiza um desconto existente
- DeleteDiscountCommand - Remove um desconto
- Todos os comandos passam por validação (FluentValidation)
- Cada comando tem um handler dedicado
- Comandos modificam o estado do sistema
Queries (Consultas):
- GetDiscountByIdQuery - Busca um desconto específico
- SearchDiscountsQuery - Pesquisa descontos com filtros e paginação
- Queries são otimizadas para leitura
- Não modificam o estado do sistema
- Cada query tem um handler e mapper dedicados
Validators:
- CreateDiscountValidator - Valida criação de descontos
- UpdateDiscountValidator - Valida atualização de descontos
- DeleteDiscountValidator - Valida exclusão de descontos
- SearchDiscountsValidator - Valida parâmetros de pesquisa
- Utilizam FluentValidation para regras declarativas
Relacionamentos
UserDiscount
Representa a associação entre um desconto e um usuário específico. - Permite descontos personalizados/exclusivos - Controla quantas vezes cada usuário usou o desconto - Usado para descontos privados (não públicos)
Order
Pedidos que utilizaram o desconto. - Mantém histórico de uso - Usado para calcular limite de usos - Permite auditoria e análise de efetividade
Validações de Domínio
Exemplo de Validação de Uso
public void ValidateDiscountUsage(decimal totalPrice)
{
if (!IsActive)
throw new RuleViolationDomainException("Discount is not active");
if (DateTime.Now < StartDt)
throw new RuleViolationDomainException($"Discount will only be valid from {StartDt:MM/dd/yyyy}");
if (DateTime.Now > EndDt)
throw new RuleViolationDomainException("Discount has expired");
if (MaxUses.HasValue && UserDiscounts.Count >= MaxUses.Value)
throw new RuleViolationDomainException("Discount usage limit has been reached");
if (totalPrice < MinOrderValue)
throw new RuleViolationDomainException($"Minimum order value to use this discount is {MinOrderValue.Value:C}");
}
Observações Importantes
- Período de Validade:
- StartDt e EndDt controlam quando o desconto pode ser usado
-
Tolerância de 5 minutos na criação permite ajustes de última hora
-
Tipos de Desconto:
- Percentage: Útil para promoções proporcionais (ex: "20% off")
-
FixedAmount: Útil para valores fixos (ex: "R$ 50 de desconto")
-
Limite de Usos:
MaxUses = null: IlimitadoMaxUses = 1: Uso único (cupom single-use)-
MaxUses > 1: Limite específico -
Valor Mínimo:
- Evita uso de descontos em pedidos pequenos
-
Protege margem de lucro
-
Público vs Privado:
- Público: Código disponível para todos, basta conhecer
-
Privado: Requer associação UserDiscount
-
Auditoria: Todas as operações registram autor e data
-
Atualização Parcial:
- UpdateDiscount permite alterar apenas campos necessários
-
Facilita ativação/desativação rápida
-
Códigos de Cupom:
- Use códigos únicos e descritivos
- Evite códigos facilmente adivinháveis para descontos privados
-
Considere prefixos por campanha (ex: BF2024-, NATAL-)
-
Estratégias Recomendadas:
- Descontos de boas-vindas: MaxUses=1, IsPublic=false, via UserDiscount
- Promoções sazonais: IsPublic=true, período limitado
- Descontos VIP: IsPublic=false, sem MaxUses, MinOrderValue alto
DiscountEligibilities (Elegibilidades de Desconto)
Visão Geral
O recurso DiscountEligibility gerencia quais usuários são elegíveis para usar descontos privados. Quando um desconto não é público (IsPublic = false), apenas usuários com elegibilidade associada podem utilizá-lo.
Commands (Comandos)
1. Adicionar Usuário Elegível (AddEligibleUser)
Contrato: AddEligibleUserCommand
Campos Necessários:
DiscountId(Guid, obrigatório) - Identificador do descontoUserId(Guid, obrigatório) - Identificador do usuárioExpiresAt(DateTime?, opcional) - Data de expiração da elegibilidadeObservation(string, opcional) - Observações adicionaisAuthor(string, opcional) - Autor da operação
Validações:
DiscountId: Não pode ser vazio; Desconto deve existirUserId: Não pode ser vazio; Usuário deve existirExpiresAt: Quando fornecido, deve ser uma data futura
Retorno: AddEligibleUserResponse
- Id (Guid) - Identificador da elegibilidade criada
Regras de Negócio:
- Um usuário não pode ter elegibilidade duplicada para o mesmo desconto
- A elegibilidade pode ter data de expiração independente do desconto
- Elegibilidade é criada com
IsActive = true
Exemplo de Uso:
{
"discountId": "123e4567-e89b-12d3-a456-426614174000",
"userId": "456e7890-e12b-34d5-b678-901234567890",
"expiresAt": "2024-12-31T23:59:59",
"observation": "Cliente VIP - acesso antecipado",
"author": "admin@sistema.com"
}
2. Remover Usuário Elegível (RemoveEligibleUser)
Contrato: RemoveEligibleUserCommand
Campos Necessários:
DiscountId(Guid, obrigatório) - Identificador do descontoUserId(Guid, obrigatório) - Identificador do usuário
Validações:
DiscountId: Não pode ser vazioUserId: Não pode ser vazio- Elegibilidade deve existir para a combinação desconto/usuário
Retorno: Void
Regras de Negócio:
- Remove a elegibilidade do usuário para o desconto especificado
- Operação é permanente
Queries (Consultas)
1. Buscar Elegibilidade por ID (GetDiscountEligibilityById)
Contrato: GetDiscountEligibilityByIdQuery
Parâmetros:
DiscountId(Guid, obrigatório) - Identificador do descontoUserId(Guid, obrigatório) - Identificador do usuário
Retorno: GetDiscountEligibilityByIdResponse
- DiscountId (Guid) - Identificador do desconto
- Name (string) - Nome do desconto
- Code (string) - Código do desconto
- User (object) - Dados do usuário
- Id (Guid) - Identificador do usuário
- Name (string) - Nome completo do usuário
- Email (string) - Email do usuário
- Phone (string) - Telefone do usuário
- IsActive (bool) - Status ativo/inativo
- ExpiresAt (DateTime?) - Data de expiração
- IsValid (bool) - Se a elegibilidade é válida (ativa e não expirada)
- Created (DateTime) - Data de criação
- Updated (DateTime?) - Data da última atualização
- Observation (string) - Observações
- AuthorCreated (string) - Autor da criação
- AuthorUpdated (string) - Autor da última atualização
Exemplo de Resposta:
{
"discountId": "123e4567-e89b-12d3-a456-426614174000",
"name": "Desconto VIP",
"code": "VIP2024",
"user": {
"id": "456e7890-e12b-34d5-b678-901234567890",
"name": "João Silva",
"email": "joao@email.com",
"phone": "+5511999999999"
},
"isActive": true,
"expiresAt": "2024-12-31T23:59:59Z",
"isValid": true,
"created": "2024-01-15T10:00:00Z",
"updated": null,
"observation": "Cliente VIP - acesso antecipado",
"authorCreated": "admin@sistema.com",
"authorUpdated": null
}
2. Pesquisar Elegibilidades (SearchDiscountEligibilities)
Contrato: SearchDiscountEligibilitiesQuery
Parâmetros:
DiscountId(Guid?, opcional) - Filtrar por descontoUserId(Guid?, opcional) - Filtrar por usuárioIsActive(bool?, opcional) - Filtrar por statusPageNumber(int, padrão: 1) - Número da páginaPageSize(int, padrão: 10) - Tamanho da página
Retorno: SearchDiscountEligibilitiesResponse
- Lista paginada de elegibilidades com os mesmos campos do GetById (exceto Observation)
Exemplo de Uso:
{
"discountId": "123e4567-e89b-12d3-a456-426614174000",
"isActive": true,
"pageNumber": 1,
"pageSize": 20
}
DiscountUsages (Uso de Descontos)
Visão Geral
O recurso DiscountUsage rastreia o histórico de uso de descontos por usuários. Cada vez que um usuário utiliza um desconto em um pedido, o registro de uso é atualizado.
Queries (Consultas)
1. Buscar Uso por ID (GetDiscountUsageById)
Contrato: GetDiscountUsageByIdQuery
Parâmetros:
DiscountId(Guid, obrigatório) - Identificador do descontoUserId(Guid, obrigatório) - Identificador do usuário
Retorno: GetDiscountUsageByIdResponse
- DiscountId (Guid) - Identificador do desconto
- Name (string) - Nome do desconto
- Code (string) - Código do desconto
- User (object) - Dados do usuário
- Id (Guid) - Identificador do usuário
- Name (string) - Nome completo do usuário
- Email (string) - Email do usuário
- Phone (string) - Telefone do usuário
- UsageCount (int) - Quantidade de vezes que o desconto foi usado
- LastUsedAt (DateTime?) - Data do último uso
- LastOrderId (Guid?) - ID do último pedido que usou o desconto
- Created (DateTime) - Data de criação do registro
- Updated (DateTime?) - Data da última atualização
- Observation (string) - Observações
- AuthorCreated (string) - Autor da criação
- AuthorUpdated (string) - Autor da última atualização
Exemplo de Resposta:
{
"discountId": "123e4567-e89b-12d3-a456-426614174000",
"name": "Black Friday 2024",
"code": "BLACKFRIDAY2024",
"user": {
"id": "456e7890-e12b-34d5-b678-901234567890",
"name": "Maria Santos",
"email": "maria@email.com",
"phone": "+5511888888888"
},
"usageCount": 3,
"lastUsedAt": "2024-11-29T15:30:00Z",
"lastOrderId": "789a0123-b45c-67d8-e901-234567890abc",
"created": "2024-11-24T10:00:00Z",
"updated": "2024-11-29T15:30:00Z",
"observation": null,
"authorCreated": "system",
"authorUpdated": "system"
}
2. Pesquisar Usos (SearchDiscountUsages)
Contrato: SearchDiscountUsagesQuery
Parâmetros:
DiscountId(Guid?, opcional) - Filtrar por descontoUserId(Guid?, opcional) - Filtrar por usuárioUsedAfter(DateTime?, opcional) - Filtrar usos após esta dataUsedBefore(DateTime?, opcional) - Filtrar usos antes desta dataPageNumber(int, padrão: 1) - Número da páginaPageSize(int, padrão: 10) - Tamanho da página
Retorno: SearchDiscountUsagesResponse
- Lista paginada de usos com os mesmos campos do GetById (exceto Observation)
Exemplo de Uso:
{
"discountId": "123e4567-e89b-12d3-a456-426614174000",
"usedAfter": "2024-11-01T00:00:00",
"usedBefore": "2024-11-30T23:59:59",
"pageNumber": 1,
"pageSize": 50
}
Descrição:
- Permite análise de uso de descontos por período
- Útil para relatórios de efetividade de campanhas
- Suporta filtros combinados para análises específicas