Files
carvalho-finances/apps/api/internal/service/game.go
T
Mlcavalho1andClaude Sonnet 4.6 1d59cb2a0e feat: #45 meta de poupança configurável + conquistas genéricas
- Nova tabela user_settings com savings_goal_pct (default 40%)
- API GET /api/settings, PUT /api/settings
- DashboardService inclui savings_goal_pct no payload
- DashboardHandler dispara savings_checked com goal configurado
- GameService.NotifyAction: checa pct >= goal (sem hardcode de 40%)
- SettingsView: painel META DE POUPANÇA com input configurável
- HomeView: widget mostra meta dinâmica (META: X%) e usa savingsGoal
- stores/settings.ts: store com fetch e update

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
2026-05-28 22:32:08 -03:00

179 lines
5.3 KiB
Go

package service
import (
"context"
"time"
"financeiro-carvalho/internal/model"
)
type GameRepo interface {
GetProfile(ctx context.Context) (*model.PlayerProfile, error)
AddXP(ctx context.Context, eventType string, amount int, description string) (*model.PlayerProfile, error)
ListActiveQuests(ctx context.Context) ([]model.PlayerQuest, error)
EnsurePlayerQuest(ctx context.Context, questID int, period string) (int, error)
IncrementQuestCount(ctx context.Context, questType, period string, delta int) error
UpdateQuestPct(ctx context.Context, period string, pct float64) error
ClaimQuest(ctx context.Context, playerQuestID int) (int, error)
ListAchievements(ctx context.Context) ([]model.Achievement, error)
IsAchievementUnlocked(ctx context.Context, condition string) (bool, error)
UnlockAchievement(ctx context.Context, condition string) (int, error)
ListCosmetics(ctx context.Context) ([]model.Cosmetic, error)
EquipCosmetic(ctx context.Context, cosmeticID int) error
UnlockCosmeticsForLevel(ctx context.Context, level int) error
}
type GameService struct {
repo GameRepo
}
func NewGameService(repo GameRepo) *GameService {
return &GameService{repo: repo}
}
func (s *GameService) GetProfile(ctx context.Context) (*model.PlayerProfile, error) {
return s.repo.GetProfile(ctx)
}
func (s *GameService) AwardXP(ctx context.Context, eventType string, amount int, description string) (*model.PlayerProfile, error) {
prevProfile, _ := s.repo.GetProfile(ctx)
profile, err := s.repo.AddXP(ctx, eventType, amount, description)
if err != nil {
return nil, err
}
// unlock cosmetics if levelled up
if prevProfile != nil && profile.Level > prevProfile.Level {
_ = s.repo.UnlockCosmeticsForLevel(ctx, profile.Level)
if profile.Level >= 5 {
s.tryUnlockAchievement(ctx, "level_5")
}
if profile.Level >= 10 {
s.tryUnlockAchievement(ctx, "level_10")
}
}
return profile, nil
}
func (s *GameService) tryUnlockAchievement(ctx context.Context, condition string) {
unlocked, _ := s.repo.IsAchievementUnlocked(ctx, condition)
if unlocked {
return
}
xp, err := s.repo.UnlockAchievement(ctx, condition)
if err == nil && xp > 0 {
_, _ = s.repo.AddXP(ctx, "achievement_"+condition, xp, "Achievement desbloqueado")
}
}
func (s *GameService) CheckAndUnlockAchievement(ctx context.Context, condition string) {
s.tryUnlockAchievement(ctx, condition)
}
func (s *GameService) GetSummary(ctx context.Context) (*model.GameSummary, error) {
profile, err := s.repo.GetProfile(ctx)
if err != nil {
return nil, err
}
quests, err := s.repo.ListActiveQuests(ctx)
if err != nil {
return nil, err
}
if quests == nil {
quests = []model.PlayerQuest{}
}
achievements, err := s.repo.ListAchievements(ctx)
if err != nil {
return nil, err
}
recent := []model.Achievement{}
for _, a := range achievements {
if a.Earned {
recent = append(recent, a)
if len(recent) >= 3 {
break
}
}
}
cosmetics, err := s.repo.ListCosmetics(ctx)
if err != nil {
return nil, err
}
equipped := []model.Cosmetic{}
for _, c := range cosmetics {
if c.Equipped {
equipped = append(equipped, c)
}
}
return &model.GameSummary{
Profile: *profile,
ActiveQuests: quests,
RecentAchievements: recent,
EquippedCosmetics: equipped,
}, nil
}
func (s *GameService) ClaimQuest(ctx context.Context, playerQuestID int) error {
xp, err := s.repo.ClaimQuest(ctx, playerQuestID)
if err != nil || xp == 0 {
return err
}
_, err = s.AwardXP(ctx, "quest_claimed", xp, "Quest concluída")
return err
}
func (s *GameService) ListAllAchievements(ctx context.Context) ([]model.Achievement, error) {
return s.repo.ListAchievements(ctx)
}
func (s *GameService) ListCosmetics(ctx context.Context) ([]model.Cosmetic, error) {
return s.repo.ListCosmetics(ctx)
}
func (s *GameService) EquipCosmetic(ctx context.Context, cosmeticID int) error {
return s.repo.EquipCosmetic(ctx, cosmeticID)
}
// 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()
monthlyPeriod := now.Format("2006-01")
weeklyPeriod := now.Format("2006") + "-W" + now.Format("01")
dailyPeriod := now.Format("2006-01-02")
switch action {
case "transaction_created":
_, _ = s.AwardXP(ctx, "transaction_created", 5, "Transação registrada")
if catID, ok := extra["category_id"]; ok && catID != nil {
_, _ = s.AwardXP(ctx, "transaction_categorized", 10, "Transação categorizada")
_ = s.repo.IncrementQuestCount(ctx, "daily", dailyPeriod, 1)
_ = s.repo.IncrementQuestCount(ctx, "weekly", weeklyPeriod, 1)
}
s.tryUnlockAchievement(ctx, "first_transaction")
case "import_confirmed":
count, _ := extra["count"].(int)
if count < 1 {
count = 1
}
_, _ = s.AwardXP(ctx, "import_confirmed", 50, "Extrato importado")
_ = s.repo.IncrementQuestCount(ctx, "weekly", weeklyPeriod, 1)
s.tryUnlockAchievement(ctx, "first_import")
case "savings_checked":
pct, _ := extra["pct"].(float64)
goal, _ := extra["goal"].(float64)
if goal <= 0 {
goal = 40
}
_ = s.repo.UpdateQuestPct(ctx, monthlyPeriod, pct)
if pct >= goal {
_, _ = s.AwardXP(ctx, "savings_goal_met", 200, "Meta de poupança atingida!")
s.tryUnlockAchievement(ctx, "first_40pct")
}
}
}