Design Document — US 197021 (PR1: coezzion-service-showcase)
Requisitos: 197021-requisitos.md
Pesquisa: design-research-showcase-api.md
RFs cobertos: RF-01, RF-06, RF-07, RF-08, RF-09, RF-10, RF-11
Overview
O PR1 inverte a semântica de dateExpiration null na Vitrine de loja (hoje vira +31 dias) e torna o campo opcional na Vitrine por histórico, alinhando Store e Recommend ao padrão já usado na Vitrine da marca: persistir DateTime.MaxValue no Elasticsearch e expor null nos DTOs de leitura.
O fluxo permanece CQRS via ZZMediator: Controller → Command/Query → Handler → ElasticRepository. Não há migrations SQL; alterações concentram-se em Domain (normalizer), Commands/Validators Recommend, Handlers Store/Recommend e mapeamento de queries.
Contexto
A Spike 198299 confirmou viabilidade. O zzapp (PR2) enviará dateExpiration: null quando o vendedor limpar a data; a API não deve reaplicar default silencioso (RF-02, D4). O default +31 permanece responsabilidade da UI.
Decisões de Design
| ID | Decisão | Alternativa rejeitada | Racional | RF |
|---|---|---|---|---|
| D-API-1 | Classe estática ShowCaseExpirationNormalizer em Domain | Persistir null no ES | ES date_range exclui null; Brand já usa MaxValue | RF-06 |
| D-API-2 | Não refatorar Brand handlers neste PR | Extrair Brand para usar normalizer já | Reduz risco de regressão em zzportal | RF-05 |
| D-API-3 | ShowCaseExpirationRules.MaxDaysFromNow = 31 | Dois valores 30 e 31 | AS-IS único até PO (D3) | RF-08 |
| D-API-4 | FluentValidation com When(x => x.DateExpiration.HasValue) | Validar só no app | Defesa em profundidade (D8) | RF-08 |
| D-API-5 | ToApiExpiration em todos os DTOs Store/Recommend (D-API-10) | Só endpoints zzapp | Consistência RF-07; inclui GET público | RF-07 |
| D-API-6 | Manter query unicidade DateExpiration >= now | Query especial para MaxValue | MaxValue já satisfaz filtro | RF-09 |
| D-API-7 | Validators ByApp Store (Create + Update) com regras de data compartilhadas | Só handler / reutilizar validator vazio de outro command | CreateShowCaseValidator não cobre endpoint zzapp | RF-08 |
| D-API-8 | Cap inclusivo: date ≤ início do dia (hoje + 31) em ToBrazillianTime() | Now.AddDays(31) por instante | Paridade picker zzapp e ES now/d | RF-08 |
| D-API-9 | Mínimo: após início de amanhã (dia civil BR) | GreaterThan(DateTime.Now) AS-IS | Paridade picker zzapp D-APP-12 | RF-08 |
| D-API-10 | ToApiExpiration em todos os read DTOs Store/Recommend | Só endpoints zzapp | Evita MaxValue no JSON público | RF-07 |
| D-API-11 | Remover SetDates() no create Recommend; manter método no Domain | Apagar ElasticBaseIndex.SetDates | Único outro caller é Brand temp (fora do CA); no Recommend o +30 é descartado — só DateCreated importa | RF-06 |
Architecture
Diagrama de Fluxo
Camadas e Responsabilidades
| Camada | Componente | Tipo | Responsabilidade | RF |
|---|---|---|---|---|
| Domain | ShowCaseExpirationNormalizer | Novo | Persistência e API mapping | RF-06, RF-07 |
| Domain | ShowCaseExpirationRules | Novo | Constante MaxDaysFromNow | RF-08 |
| Domain | ShowCaseModel.SetDateExpiration | Modificado | Delega ao normalizer | RF-06 |
| API | Create/UpdateShowCaseRecommendCommand | Modificado | DateTime? | RF-01 |
| API | Create/UpdateShowCaseRecommendValidator | Modificado | Opcional + cap | RF-01, RF-08 |
| API | CreateShowCaseByAppValidator + UpdateShowCaseByAppValidator | Novo | Cap condicional Store ByApp (D-API-7) | RF-08 |
| API | ShowCaseCommandHandler | Modificado | Usa normalizer | RF-06 |
| API | ShowCaseRecommendCommandHandler | Modificado | Usa normalizer no create/update | RF-06, RF-09 |
| API | ShowCaseQueryHandler | Modificado | ToApiExpiration nos DTOs | RF-07 |
| API | ShowCaseRecommendQueryHandler | Modificado | Idem | RF-07 |
| UnitTest | Validators + Handlers + Queries | Modificado | Novos cenários | RF-11 |
Components and Interfaces
Novos arquivos
| Arquivo | Camada |
|---|---|
ShowCase.Domain/Common/ShowCaseExpirationNormalizer.cs | Domain |
ShowCase.Domain/Common/ShowCaseExpirationRules.cs | Domain |
ShowCase.UnitTest/Domain/ShowCaseExpirationNormalizerTests.cs | UnitTest |
Arquivos modificados
Ver inventário completo em design-research-showcase-api.md.
Interfaces propostas
// RF-06, RF-07
public static class ShowCaseExpirationNormalizer
{
public static DateTime NormalizeForPersistence(DateTime? dateExpiration)
{
// RF-06: null → MaxValue; data informada → valor normalizado
}
public static DateTime? ToApiExpiration(DateTime? storedExpiration)
{
// RF-07: MaxValue → null
}
}
// RF-08, D-API-8, D-API-9
public static class ShowCaseExpirationRules
{
public const int MaxDaysFromNow = 31; // D3: alinhar com PO
}
// Regras FluentValidation compartilhadas (ByApp Store + Recommend)
public static class ShowCaseExpirationValidationRules
{
// D-API-9: date > início de amanhã (BR)
// D-API-8: date <= início de hoje+31 (BR), inclusivo
}
Data Models
Commands (alteração)
// RF-01 — Recommend
public class CreateShowCaseRecommendCommand : ICommand<BaseResult>
{
// ... demais campos inalterados
public DateTime? DateExpiration { get; set; }
}
public class UpdateShowCaseRecommendCommand : ICommand<BaseResult>
{
public DateTime? DateExpiration { get; set; }
}
// Store — CreateShowCaseByAppCommand já possui DateTime?
Persistência Elasticsearch
| Entrada API | Valor ES DateExpiration | API response |
|---|---|---|
null | DateTime.MaxValue | null |
| ISO futura dentro do cap | valor informado | ISO |
| ISO passada / acima cap | — (400, não persiste) | — |
Error Handling
Tabela de Erros
| Cenário | Status HTTP | Mensagem (pt-BR) | RF |
|---|---|---|---|
DateExpiration no passado | 400 | Data de expiração deve ser futura! | RF-08 |
| Acima de MaxDaysFromNow | 400 | Data de expiração excede o máximo permitido | RF-08 |
| Recommend: segunda vitrine ativa mesmo CPF | 400 | Já existe uma vitrine ativa para este cliente nesta loja. | RF-09 |
| Vitrine não encontrada (update) | 400 | Vitrine não encontrada. | RF-03 |
| Sem permissão edit | 400 | Você não tem permissão para editar esta vitrine. | RF-03 |
Handler Pseudocode
// RF-06 — ShowCaseCommandHandler create/update Store
public async Task<BaseResult> Handle(CreateShowCaseByAppCommand command, ...)
{
// ... validações existentes
showcase.SetDateExpiration(
ShowCaseExpirationNormalizer.NormalizeForPersistence(command.DateExpiration));
// RF-06: null NÃO vira +31
await _showCaseRepository.InsertAsync(showcase);
}
// RF-06, RF-09 — ShowCaseRecommendCommandHandler create (D-API-11)
public async Task<BaseResult> Handle(CreateShowCaseRecommendCommand command, ...)
{
var existing = await _repo.FindActiveByUserCustomerCpfAndStoreAsync(...);
// RF-09: MaxValue conta como ativa — query ES inalterada
showcase.DateCreated = DateTime.Now.ToBrazillianTime(); // espelha Store create
showcase.DateExpiration =
ShowCaseExpirationNormalizer.NormalizeForPersistence(command.DateExpiration);
await _repo.InsertAsync(showcase);
}
// RF-07 — ShowCaseQueryHandler list
var dto = new ShowcaseSummaryDTO
{
DateExpiration = ShowCaseExpirationNormalizer.ToApiExpiration(showcase.DateExpiration)
};
Testing Strategy
Testes unitários
| Cenário | Entrada | Resultado esperado | RF |
|---|---|---|---|
| Normalize null | null | DateTime.MaxValue | RF-06 |
| Normalize data válida | now+7 | data informada | RF-06 |
| ToApi MaxValue | MaxValue | null | RF-07 |
| ToApi data finita | now+7 | now+7 | RF-07 |
| Create Recommend null | validator | válido | RF-01 |
| Create Recommend passado | now-1 | inválido | RF-08 |
| Create Recommend now+40 | — | inválido (cap 31) | RF-08 |
| Unicidade com MaxValue | vitrine ativa MaxValue + novo create | erro unicidade | RF-09 |
| Store create null | handler | ES MaxValue, não +31 | RF-06 |
Regressão manual
- Vitrine da marca inalterada (smoke zzportal)
- GET público vitrine never-expire após meses
- Product sync job não regredir (RF-05)
Dependências
| Dependência | Status | Nota |
|---|---|---|
| Requisitos RF-01–RF-11 | Disponível | 197021-requisitos.md |
| Alinhamento PO 30 vs 31 | Pendente | Constante única documentada |
| PR2 zzapp | Pendente | Contrato null=never deve ser publicado antes do merge app |
| Brand handlers | Disponível | Referência; não alterar |
Checklist de Qualidade
- Todos os RFs de API cobertos (RF-01, RF-06–RF-09, RF-11)
- Tratamento de erro para data passada e cap excedido
- Segue padrão Brand MaxValue sem alterar Brand
- Sem lock adicional (sem race condition nova)
- Response DTO consistente em list/detail Store e Recommend
- Nenhum ano 9999 exposto ao zzapp