feat(game): #49 motor de conquistas dinâmicas por categoria
- GameRepo: EnsureCategoryAchievement, RemoveCategoryAchievementIfNotEarned, ListCategoriesWithBudget, GetMonthlySpendByCategory - GameService: OnCategoryBudgetSet, CheckCategoryBudgets - ListAchievements filtra conquistas por categorias do perfil (sem vazamento entre usuários) - NotifyAction chama CheckCategoryBudgets em transaction_created e import_confirmed - CategoryHandler injeta GameService e chama OnCategoryBudgetSet após Create/Update - 5 novos testes unitários cobrindo todos os cenários de AC
This commit is contained in:
@@ -2,6 +2,7 @@ package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"financeiro-carvalho/internal/model"
|
||||
@@ -21,6 +22,10 @@ type GameRepo interface {
|
||||
ListCosmetics(ctx context.Context) ([]model.Cosmetic, error)
|
||||
EquipCosmetic(ctx context.Context, cosmeticID int) error
|
||||
UnlockCosmeticsForLevel(ctx context.Context, level int) error
|
||||
EnsureCategoryAchievement(ctx context.Context, categoryID int, categoryName string) error
|
||||
RemoveCategoryAchievementIfNotEarned(ctx context.Context, categoryID int) error
|
||||
ListCategoriesWithBudget(ctx context.Context) ([]model.CategoryBudgetInfo, error)
|
||||
GetMonthlySpendByCategory(ctx context.Context, month string, categoryID int) (float64, error)
|
||||
}
|
||||
|
||||
type GameService struct {
|
||||
@@ -137,6 +142,33 @@ func (s *GameService) EquipCosmetic(ctx context.Context, cosmeticID int) error {
|
||||
return s.repo.EquipCosmetic(ctx, cosmeticID)
|
||||
}
|
||||
|
||||
// OnCategoryBudgetSet gera ou remove a conquista dinâmica ao salvar/limpar a meta mensal de uma categoria.
|
||||
func (s *GameService) OnCategoryBudgetSet(ctx context.Context, categoryID int, categoryName string, budget *float64) {
|
||||
if budget == nil {
|
||||
_ = s.repo.RemoveCategoryAchievementIfNotEarned(ctx, categoryID)
|
||||
return
|
||||
}
|
||||
_ = s.repo.EnsureCategoryAchievement(ctx, categoryID, categoryName)
|
||||
}
|
||||
|
||||
// CheckCategoryBudgets verifica todas as categorias com meta do perfil e desbloqueia conquistas quando gasto ≤ meta.
|
||||
func (s *GameService) CheckCategoryBudgets(ctx context.Context, month string) {
|
||||
cats, err := s.repo.ListCategoriesWithBudget(ctx)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
for _, cat := range cats {
|
||||
total, err := s.repo.GetMonthlySpendByCategory(ctx, month, cat.CategoryID)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
if total <= cat.MonthlyBudget {
|
||||
condition := fmt.Sprintf("budget_cat_%d", cat.CategoryID)
|
||||
s.tryUnlockAchievement(ctx, condition)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// NotifyAction is called by other handlers after key financial actions to award XP and update quests.
|
||||
func (s *GameService) NotifyAction(ctx context.Context, action string, extra map[string]any) {
|
||||
now := time.Now()
|
||||
@@ -153,6 +185,7 @@ func (s *GameService) NotifyAction(ctx context.Context, action string, extra map
|
||||
_ = s.repo.IncrementQuestCount(ctx, "weekly", weeklyPeriod, 1)
|
||||
}
|
||||
s.tryUnlockAchievement(ctx, "first_transaction")
|
||||
s.CheckCategoryBudgets(ctx, monthlyPeriod)
|
||||
|
||||
case "import_confirmed":
|
||||
count, _ := extra["count"].(int)
|
||||
@@ -162,6 +195,7 @@ func (s *GameService) NotifyAction(ctx context.Context, action string, extra map
|
||||
_, _ = s.AwardXP(ctx, "import_confirmed", 50, "Extrato importado")
|
||||
_ = s.repo.IncrementQuestCount(ctx, "weekly", weeklyPeriod, 1)
|
||||
s.tryUnlockAchievement(ctx, "first_import")
|
||||
s.CheckCategoryBudgets(ctx, monthlyPeriod)
|
||||
|
||||
case "savings_checked":
|
||||
pct, _ := extra["pct"].(float64)
|
||||
|
||||
Reference in New Issue
Block a user