Plano — US 198113 | Inativação completa no job de users
Data: 2026-07-09
Workitem: 198113
US: zzapp | Ajuste na rotina de desativação de usuário
Repo: coezzion-service-jobs
Decisões: context/grill-decisions.md
AS IS: context/as-is-analysis.md
Pesquisa stack: context/dapper-connection-research.md
Glossário: context/CONTEXT.md
1. Resumo
O job LogoutOldAppVersionUsers inativa usuários antigos de app via DeleteUserAsync, que hoje só faz:
UPDATE "Users"
SET "DateDeleted" = ..., "Status" = 2
WHERE "Id" = @userId;
Falta (US):
- Soft-delete em
{schema}."Sellers"(DateDeleted) - DELETE em
public."UserOrganization"para o user
Escopo: apenas o jobs (DeleteUserAsync + caller). Admin/store e limpeza legada ficam de fora.
2. Causa raiz (AS IS)
| Tabela | Esperado (inativação completa) | AS IS no job |
|---|---|---|
public."Users" | DateDeleted + Status=Inactive | ✅ |
{schema}."Sellers" | DateDeleted | ❌ |
public."UserOrganization" | remoção dos vínculos | ❌ |
Caller: LogoutUsersCommandHandler → único uso de DeleteUserAsync.
Schema do job: "arezzo" (argument Hangfire).
Banco: CoreDB único para Users, UserOrganization e Sellers (schemas distintos).
3. Decisões de design
| # | Escolha |
|---|---|
| Escopo | Só jobs DeleteUserAsync |
| Schema Sellers | request.Schema do job |
| Stack | Dapper + IDbConnectionFactory Write/CoreDb |
| Atomicidade | Multi-statement SQL + BeginTransaction |
| EF | Não |
| Legado | Não limpar já-inativados |
| Idempotência | WHERE DateDeleted IS NULL em Users/Sellers |
| User sem Seller | 0 rows = sucesso |
| Logout SQS | Manter após delete ok |
| Testes | Checklist SQL manual em staging |
4. Mudanças no backend (coezzion-service-jobs)
4.1 IUserRepository
Arquivo: src/Jobs.Domain/Interfaces/Data/Repositories/IUserRepository.cs
// Antes:
Task DeleteUserAsync(int userId);
// Depois:
Task DeleteUserAsync(int userId, string schema);
4.2 UserRepository.DeleteUserAsync
Arquivo: src/Jobs.Infrastructure/Data/Repositories/UserRepository.cs
- Validar
schemacom^[a-zA-Z_][a-zA-Z0-9_]*$(lançar se inválido — antes de abrir tx). - Connection:
GetConnectionAsync(EConnectionType.Write, EDatabaseType.CoreDb). Open+BeginTransaction.- Um
ExecuteAsynccom:
UPDATE public."Users"
SET "DateDeleted" = NOW() AT TIME ZONE 'America/Sao_Paulo',
"Status" = 2
WHERE "Id" = @userId
AND "DateDeleted" IS NULL;
UPDATE {schema}."Sellers"
SET "DateDeleted" = NOW() AT TIME ZONE 'America/Sao_Paulo'
WHERE "UserId" = @userId
AND "DateDeleted" IS NULL;
DELETE FROM public."UserOrganization"
WHERE "UsersId" = @userId;
Commit/ em falhaRollbacke rethrow.- Não setar Status em Sellers (coluna inexistente no modelo).
- Manter timezone BR igual ao AS IS.
Notas de implementação:
- Handler usa
Parallel.ForEachAsync(DOP 3): cada chamada abre connection/tx própria. - Coluna de vínculo:
"UsersId"(nãoUserId) emUserOrganization. - Qualificar
public."Users"explicitamente (connection CoreDb sem SearchPath de tenant).
4.3 LogoutUsersCommandHandler
Arquivo: src/Jobs.API/Application/Messages/Commands/LogoutUsersCommands/LogoutUsersCommandHandler.cs
// Antes:
await userRepository.DeleteUserAsync(user.UserId);
// Depois:
await userRepository.DeleteUserAsync(user.UserId, request.Schema);
Manter: critério canDeleteUser, log CSV, LogoutUserEvent após delete ok, tratamento de exception por user.
4.4 Não alterar
LogoutDeletedUserJob(já limpa token de users inativos)- Admin
DeleteUser/UpdateStatusUser/UpdateUser - Store
UpdateSellersByUser(referência de regra, não chamada HTTP) - Job de backfill legado
5. Arquivos críticos
| Arquivo | Ação |
|---|---|
Jobs.Domain/.../IUserRepository.cs | Assinatura + schema |
Jobs.Infrastructure/.../UserRepository.cs | SQL atômico Dapper + tx + validação schema |
Jobs.API/.../LogoutUsersCommandHandler.cs | Passar request.Schema |
Reuse: IDbConnectionFactory (Coezzion.Common), padrão multi-schema SQL já usado em reads do jobs.
6. Verification (checklist manual / staging)
Pré-condição
User de teste com:
public."Users"ativo (Status=1,DateDeletednull)- ≥1
arezzo."Sellers"comDateDeletednull - ≥1
public."UserOrganization"
Pós-execução (DeleteUserAsync / job)
SELECT "Id", "Status", "DateDeleted"
FROM public."Users" WHERE "Id" = @id;
-- expect: Status = 2, DateDeleted IS NOT NULL
SELECT "Id", "UserId", "StoreId", "DateDeleted"
FROM arezzo."Sellers" WHERE "UserId" = @id;
-- expect: todas com DateDeleted IS NOT NULL
SELECT COUNT(*) FROM public."UserOrganization" WHERE "UsersId" = @id;
-- expect: 0
Cenários extras
| Caso | Expectativa |
|---|---|
| User sem Seller | Users + UO ok; Sellers 0 rows |
| Reexecução | sem erro; UO = 0 |
| Schema inválido | exception antes/sem commit parcial |
| Logout SQS | evento ainda enviado quando delete ok |
7. Residual / follow-ups (fora desta US)
- Admin
DeleteUsernão seta Status nem limpa Sellers/UO - Users já inativos com Sellers/UO órfãos (sem backfill)
- Projeto de testes no jobs (hoje inexistente)
8. Ordem de implementação sugerida
- Alterar interface
IUserRepository - Implementar
DeleteUserAsynccom validação + tx + SQL batch - Atualizar
LogoutUsersCommandHandler - Validar checklist SQL em staging
- Deploy jobs