Skip to main content

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

IDDecisãoAlternativa rejeitadaRacionalRF
D-API-1Classe estática ShowCaseExpirationNormalizer em DomainPersistir null no ESES date_range exclui null; Brand já usa MaxValueRF-06
D-API-2Não refatorar Brand handlers neste PRExtrair Brand para usar normalizer jáReduz risco de regressão em zzportalRF-05
D-API-3ShowCaseExpirationRules.MaxDaysFromNow = 31Dois valores 30 e 31AS-IS único até PO (D3)RF-08
D-API-4FluentValidation com When(x => x.DateExpiration.HasValue)Validar só no appDefesa em profundidade (D8)RF-08
D-API-5ToApiExpiration em todos os DTOs Store/Recommend (D-API-10)Só endpoints zzappConsistência RF-07; inclui GET públicoRF-07
D-API-6Manter query unicidade DateExpiration >= nowQuery especial para MaxValueMaxValue já satisfaz filtroRF-09
D-API-7Validators ByApp Store (Create + Update) com regras de data compartilhadasSó handler / reutilizar validator vazio de outro commandCreateShowCaseValidator não cobre endpoint zzappRF-08
D-API-8Cap inclusivo: date ≤ início do dia (hoje + 31) em ToBrazillianTime()Now.AddDays(31) por instanteParidade picker zzapp e ES now/dRF-08
D-API-9Mínimo: após início de amanhã (dia civil BR)GreaterThan(DateTime.Now) AS-ISParidade picker zzapp D-APP-12RF-08
D-API-10ToApiExpiration em todos os read DTOs Store/RecommendSó endpoints zzappEvita MaxValue no JSON públicoRF-07
D-API-11Remover SetDates() no create Recommend; manter método no DomainApagar ElasticBaseIndex.SetDatesÚnico outro caller é Brand temp (fora do CA); no Recommend o +30 é descartado — só DateCreated importaRF-06

Architecture

Diagrama de Fluxo

Camadas e Responsabilidades

CamadaComponenteTipoResponsabilidadeRF
DomainShowCaseExpirationNormalizerNovoPersistência e API mappingRF-06, RF-07
DomainShowCaseExpirationRulesNovoConstante MaxDaysFromNowRF-08
DomainShowCaseModel.SetDateExpirationModificadoDelega ao normalizerRF-06
APICreate/UpdateShowCaseRecommendCommandModificadoDateTime?RF-01
APICreate/UpdateShowCaseRecommendValidatorModificadoOpcional + capRF-01, RF-08
APICreateShowCaseByAppValidator + UpdateShowCaseByAppValidatorNovoCap condicional Store ByApp (D-API-7)RF-08
APIShowCaseCommandHandlerModificadoUsa normalizerRF-06
APIShowCaseRecommendCommandHandlerModificadoUsa normalizer no create/updateRF-06, RF-09
APIShowCaseQueryHandlerModificadoToApiExpiration nos DTOsRF-07
APIShowCaseRecommendQueryHandlerModificadoIdemRF-07
UnitTestValidators + Handlers + QueriesModificadoNovos cenáriosRF-11

Components and Interfaces

Novos arquivos

ArquivoCamada
ShowCase.Domain/Common/ShowCaseExpirationNormalizer.csDomain
ShowCase.Domain/Common/ShowCaseExpirationRules.csDomain
ShowCase.UnitTest/Domain/ShowCaseExpirationNormalizerTests.csUnitTest

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 APIValor ES DateExpirationAPI response
nullDateTime.MaxValuenull
ISO futura dentro do capvalor informadoISO
ISO passada / acima cap— (400, não persiste)

Error Handling

Tabela de Erros

CenárioStatus HTTPMensagem (pt-BR)RF
DateExpiration no passado400Data de expiração deve ser futura!RF-08
Acima de MaxDaysFromNow400Data de expiração excede o máximo permitidoRF-08
Recommend: segunda vitrine ativa mesmo CPF400Já existe uma vitrine ativa para este cliente nesta loja.RF-09
Vitrine não encontrada (update)400Vitrine não encontrada.RF-03
Sem permissão edit400Você 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árioEntradaResultado esperadoRF
Normalize nullnullDateTime.MaxValueRF-06
Normalize data válidanow+7data informadaRF-06
ToApi MaxValueMaxValuenullRF-07
ToApi data finitanow+7now+7RF-07
Create Recommend nullvalidatorválidoRF-01
Create Recommend passadonow-1inválidoRF-08
Create Recommend now+40inválido (cap 31)RF-08
Unicidade com MaxValuevitrine ativa MaxValue + novo createerro unicidadeRF-09
Store create nullhandlerES MaxValue, não +31RF-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ênciaStatusNota
Requisitos RF-01–RF-11Disponível197021-requisitos.md
Alinhamento PO 30 vs 31PendenteConstante única documentada
PR2 zzappPendenteContrato null=never deve ser publicado antes do merge app
Brand handlersDisponívelReferê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