Requisitos — US 197021
User Story pai: US 197021 — Data de expiração opcional para vitrines
Spike relacionada: Spike 198299 — Resultado inicial
ADRs relacionados: —
Status: refinado
Sessão de grilling: decisões D1–D8 emcontext/grill-decisions.md(2026-07-09)
Visão geral
Permitir que o vendedor crie e edite Vitrine de loja (Minhas vitrines) e Vitrine por histórico (Histórico de compras) com prazo de expiração ou no estado Sem data de expiração, reduzindo retrabalho de recriação sem alterar regras de catálogo, jobs de produto ou compartilhamento.
A implementação reutiliza o padrão já existente na Vitrine da marca: persistir DateTime.MaxValue no Elasticsearch e expor null na API/UI. Escopo técnico: coezzion-service-showcase e coezzion_vendas_app. Vitrine da marca (zzportal), coezzion-showcase-web e jobs de expiração de vendedor permanecem fora.
Papéis
| Papel | Responsabilidade |
|---|---|
| Vendedor (zzapp) | Criar e editar vitrines Store e Recommend com ou sem dateExpiration |
coezzion-service-showcase | Normalizar dateExpiration, persistir MaxValue, validar cap, mapear DTOs, unicidade Recommend |
coezzion_vendas_app | Preencher default na UI, campo clearable, labels CA-4, enviar null explícito quando sem prazo |
| Cliente final (link público) | Acessar vitrine ativa sem prazo enquanto não soft-deletada |
Glossário
| Termo | Definição |
|---|---|
| Vitrine de loja (Store) | Vitrine em Minhas vitrines; índice arezzo-showcase |
| Vitrine por histórico (Recommendation) | Vitrine vinculada a cliente (CPF); índice arezzo-showcase-recommend |
| Sem data de expiração | Vitrine ativa sem prazo por data; API/UI: dateExpiration = null |
| Vitrine ativa | Não soft-deletada e não expirada (DateExpiration maior ou igual a agora, ou MaxValue) |
| Prazo padrão | Data sugerida na criação: +31 dias no AS-IS (alinhamento PO para 30 vs 31 pendente) |
Detalhe: context/CONTEXT.md.
Requisitos
RF-01 — Data de expiração opcional (CA-1)
User Story: Como vendedor, eu quero que o campo data de expiração não seja obrigatório na criação e edição de vitrines, para poder manter vitrines sem prazo quando fizer sentido.
Acceptance Criteria:
WHENo vendedor cria uma Vitrine de loja no zzappTHENo campodateExpirationSHALLser opcional no formulário e no payload de create.WHENo vendedor edita uma Vitrine de loja no zzappTHENo campodateExpirationSHALLser opcional no formulário e no payload de update.WHENo vendedor cria uma Vitrine por histórico no zzappTHENo campodateExpirationSHALLdeixar de ser obrigatório na UI e no commandCreateShowCaseRecommendCommand.WHENo vendedor edita uma Vitrine por histórico no zzappTHENo campodateExpirationSHALLdeixar de ser obrigatório na UI e no commandUpdateShowCaseRecommendCommand.IFo fluxo envolve Vitrine da marca ou Vitrine marca–vendedorTHENo sistemaSHALLmanter o comportamento atual sem alteração desta US.
Edge Cases:
- Recommend AS-IS rejeita create sem data via FluentValidation
NotEmpty— deve ser removido. - Store AS-IS aceita
DateTime?mas null vira +31d no handler — semântica deve mudar (ver RF-06).
RF-02 — Criação com default na UI (CA-2, D4)
User Story: Como vendedor, eu quero que novas vitrines já venham com o prazo padrão atual, mas possa limpar essa data, para manter o hábito atual e ainda optar por sem expiração.
Acceptance Criteria:
WHENo vendedor abre o formulário de criação de Vitrine de loja ou Vitrine por históricoTHENo zzappSHALLpré-preencherdateExpirationcom o prazo padrão atual (+31 dias no AS-IS).WHENo vendedor limpa o campo de data antes de salvar a criaçãoTHENo zzappSHALLenviardateExpiration: nullexplicitamente no request.IFo request de create chega comdateExpiration: nullTHENocoezzion-service-showcaseSHALLgravar sem expiração (MaxValue) eSHALL NOTreaplicar o prazo padrão no servidor.WHENo vendedor mantém o default pré-preenchido e salvaTHENo zzappSHALLenviar a data default no request e a vitrineSHALLexpirar na data informada.IFo vendedor informa uma data de expiração na criaçãoTHENo sistemaSHALLvalidar que a data é futura e respeita o teto máximo de N dias (ver RF-08).
Edge Cases:
- Create Minhas sem tocar no campo → envia default +N → expira normalmente.
- Create Minhas com clear → null → sem expiração.
RF-03 — Edição da expiração (CA-3)
User Story: Como vendedor, eu quero adicionar, alterar ou remover a data de expiração de vitrines existentes, para ajustar a vigência conforme o uso.
Acceptance Criteria:
WHENo vendedor edita uma vitrine Store ou Recommend que possuidateExpirationdefinidaTHENo zzappSHALLpermitir alterar a data respeitando o teto máximo (RF-08).WHENo vendedor remove a data de expiração na ediçãoTHENo zzappSHALLenviardateExpiration: nulle a vitrineSHALLpassar ao estado Sem data de expiração.WHENo vendedor edita uma vitrine sem data de expiraçãoTHENo zzappSHALLexibir o campo de data e permitir informar um novo prazo.WHENo update é processado comdateExpiration: nullTHENocoezzion-service-showcaseSHALLpersistirDateTime.MaxValueeSHALL NOTconverter null em +31 dias.IFo vendedor não possui permissão no fluxo de edição existenteTHENo sistemaSHALLmanter as regras de autorização atuais sem regressão.
Edge Cases:
- Edit: remove data de vitrine com prazo → MaxValue.
- Edit: adiciona data em vitrine sem prazo → data informada com validação de cap.
RF-04 — Identificação da vigência (CA-4)
User Story: Como vendedor, eu quero ver claramente quais vitrines não têm prazo, para distinguir vigência indefinida de data fixa.
Acceptance Criteria:
WHENa listagem de Minhas vitrines exibe um item comdateExpiration: nullna resposta da APITHENo zzappSHALLexibir o texto "Sem data de expiração" no lugar da data formatada.WHENo detalhe de uma Vitrine de loja possuidateExpiration: nullTHENo zzappSHALLexibir "Sem data de expiração".WHENa listagem ou detalhe de Vitrine por histórico possuidateExpiration: nullTHENo zzappSHALLexibir "Sem data de expiração" (reutilizandonullExpirationLabelonde já existir).WHENa API retornadateExpirationcom valor ISO válidoTHENo zzappSHALLcontinuar exibindo a data formatada como hoje.IFo documento no Elasticsearch contémDateExpiration = DateTime.MaxValueTHENa APISHALLmapear paranullna resposta eSHALL NOTexpor ano 9999 na UI.
Edge Cases:
- MaxValue no ES nunca deve aparecer como data legível no app.
RF-05 — Compatibilidade com regras existentes (CA-5, D7)
User Story: Como vendedor, eu quero que estoque, preços, disponibilidade e compartilhamento continuem funcionando, para não perder confiança no catálogo compartilhado.
Acceptance Criteria:
WHENuma vitrine Store ou Recommend está no estado Sem data de expiração e ativaTHENos jobs de sincronização de produto (ShowCaseProductJob, listeners de pedido)SHALLcontinuar atualizando estoque, preço e disponibilidade sem alteração de comportamento.WHENo vendedor compartilha ou copia o link de uma vitrine sem expiraçãoTHENo zzappSHALLmanter o fluxo de share/copy existente.WHENum cliente acessa o link público de vitrine sem expiraçãoTHENocoezzion-showcase-webSHALLexibir a vitrine enquanto a API retornar documento ativo.WHENjobs de status de Vitrine da marca executamTHENo sistemaSHALLmanter comportamento atual sem impacto em Store/Recommend.IFa vitrine possui prazo definido e a data passouTHENo sistemaSHALLmanter o comportamento AS-IS de exclusão da listagem e do GET público ativo.
Edge Cases:
- Link público em vitrine sem prazo após meses → continua acessível se não deletada e produtos ok.
- Link público após expiração de vitrine com prazo → "Vitrine não encontrada" (AS-IS).
RF-06 — Persistência sem expiração (D1)
User Story: Como time de plataforma, eu quero persistir "sem expiração" de forma compatível com Elasticsearch e filtros ativos, para evitar regressão em listagens e acesso público.
Acceptance Criteria:
WHENo handler de create ou update Store ou Recommend recebedateExpiration: nullTHENocoezzion-service-showcaseSHALLpersistirDateExpiration = DateTime.MaxValueno documento Elasticsearch.WHENo handler persiste qualquer vitrine Store ou RecommendTHENo sistemaSHALL NOTgravarnullno campo de data do Elasticsearch.WHENSetDateExpirationé invocado para Store com argumento nullTHENo métodoSHALL NOTaplicarDateCreated + 31 diascomo fallback.IFpossível sem alterar handlers de BrandTHENo serviçoSHALLextrair ou reutilizar helper de normalização never-expire alinhado ao padrão Brand.
Edge Cases:
- Null no ES em campo date_range pode sumir da listagem — mitigação obrigatória via MaxValue.
RF-07 — Contrato API de leitura (D1)
User Story: Como consumidor da API (zzapp), eu quero receber null quando a vitrine não expira, para renderizar a UI corretamente.
Acceptance Criteria:
WHENqualquer DTO de leitura Store ou Recommend serializa um documento comDateExpiration = DateTime.MaxValueTHENa APISHALLretornardateExpiration: null.WHENqualquer DTO de leitura serializa documento com data finitaTHENa APISHALLretornar a data em formato ISO consistente com o contrato atual.FOR ALLendpoints de listagem e detalhe Store/Recommend usados pelo zzappTHENo mapeamento MaxValue→nullSHALLser aplicado de forma uniforme.
Contrato de referência:
Request dateExpiration:
- iso-date = expira nessa data
- null = sem expiração (preferir null explícito)
Response dateExpiration:
- null = sem data de expiração
- iso-date = data
RF-08 — Validação de teto de dias (D8)
User Story: Como produto, eu quero que datas informadas respeitem o máximo permitido, para manter a política de prazo mesmo com expiração opcional.
Acceptance Criteria:
IFo request de create ou update Store ou Recommend informadateExpirationcom valor não-nuloTHENocoezzion-service-showcaseSHALLvalidar que a data é estritamente futura.IFo request informadateExpirationnão-nuloTHENo validatorSHALLrejeitar datas além do teto máximo N dias a partir de agora (constante única; AS-IS +31 até alinhamento PO).WHENa validação de teto falhaTHENa APISHALLretornar erro de validação sem persistir alteração.WHENo zzapp exibe o seletor de dataTHENo componenteSHALLmanter limite máximo na UI coerente com N dias.
Edge Cases:
- Divergência 30 (SP) vs 31 (AS-IS) — pendência PO; usar constante única documentada em código.
RF-09 — Unicidade de Recommendation (D6)
User Story: Como vendedor, eu quero que apenas uma vitrine ativa exista por cliente na loja, inclusive quando sem prazo, para evitar conflito de recomendações.
Acceptance Criteria:
WHENexiste Vitrine por histórico ativa comDateExpiration = DateTime.MaxValuepara a combinação userId + customerCpf + storeIdTHENo sistemaSHALLbloquear criação de segunda vitrine ativa para o mesmo cliente.WHENa verificação de unicidade avalia vitrines ativasTHENvitrine com MaxValueSHALLser considerada vitrine ativa.WHENa vitrine sem expiração é soft-deletada ou passa a ter data expiradaTHENo sistemaSHALLpermitir nova criação para o mesmo cliente conforme regras atuais.
Edge Cases:
- Recommend sem prazo "prende" cliente para sempre enquanto ativa — vendedor pode editar ou excluir.
RF-10 — Vitrines expiradas e migração (D5)
User Story: Como produto, eu quero limitar o escopo do MVP, para entregar valor sem reabrir fluxos de reativação em massa.
Acceptance Criteria:
WHENuma vitrine Store ou Recommend já expirou antes desta USTHENo sistemaSHALLcontinuar excluindo-a das listagens e gets ativos sem oferecer reativação no zzapp.WHENesta US é implantadaTHENo sistemaSHALL NOTexecutar migração em massa de vitrines existentes para sem expiração.WHENesta US é implantadaTHENo sistemaSHALL NOTintroduzir job de notificação "vai expirar" nem hard-delete de documentos expirados no ES.
RF-11 — Testes e deploy coordenado
User Story: Como desenvolvedor, eu quero cobertura de testes e deploy alinhado, para evitar regressão de contrato entre API e app.
Acceptance Criteria:
WHENa suíte de testes unitários docoezzion-service-showcaseexecuta após as mudançasTHENos casos null=never, data válida, data passada, max excedido e unicidade com MaxValueSHALLpassar.WHENtestes de serialização nocoezzion_vendas_appcobrem payloads comdateExpiration: nullTHENos testesSHALLpassar sem regressão.IFversões antigas do zzapp enviamnullesperando +31 no create StoreTHENo timeSHALLcoordenar deploy API + app ou avaliar feature flag antes do merge.WHENQA valida cenários da seção 8 do resultado inicialTHENtodos os critérios funcionais e técnicos listadosSHALLser atendidos.
Fora de Escopo
- Alterar regras ou UX de Vitrine da marca no zzportal (
coezzion-portal). - Exibir estado de expiração no
coezzion-showcase-web("expirou em…"). - Job de notificação de vitrine prestes a expirar.
- Listar ou reativar vitrines já expiradas no zzapp.
- Migração em massa de vitrines existentes para sem expiração.
- Hard-delete de documentos expirados no Elasticsearch.
Dependências
| Dependência | Descrição | Status |
|---|---|---|
| Spike 198299 | Plano técnico e decisões D1–D8 | Concluída |
| Alinhamento PO | 30 vs 31 dias (default e máximo) — D3 | Pendente (Fase 0) |
| Copy PO | Texto "Sem data de expiração" | Confirmado AS-IS app/portal |
PR1 coezzion-service-showcase | Contrato API fixo (RF-06, RF-07, RF-08) | Bloqueante recomendado para PR2 |
PR2 coezzion_vendas_app | UI Minhas + Histórico | Depende de PR1 ou contrato acordado |
| Brand handlers | Não alterar paths; helper compartilhado com cuidado | Decidido D7 |
| Feature 196064 | Parent no Azure DevOps (notificações vitrine) | Relacionada, fora do escopo desta US |
Rastreabilidade
| RF | CA / Decisão | Repo principal |
|---|---|---|
| RF-01 | CA-1, D2 | showcase API + zzapp |
| RF-02 | CA-2, D4 | zzapp + showcase API |
| RF-03 | CA-3 | zzapp + showcase API |
| RF-04 | CA-4 | zzapp + showcase API |
| RF-05 | CA-5, D7 | jobs (regressão), zzapp, showcase-web |
| RF-06 | D1 | showcase API |
| RF-07 | D1 | showcase API |
| RF-08 | D8 | showcase API + zzapp |
| RF-09 | D6 | showcase API |
| RF-10 | D5 | — |
| RF-11 | Plano spike §6–8 | showcase API + zzapp |