package service import ( "context" "fmt" "log" "sync" "time" "apskel-pos-be/internal/constants" "apskel-pos-be/internal/entities" "apskel-pos-be/internal/models" "apskel-pos-be/internal/processor" "apskel-pos-be/internal/repository" "github.com/google/uuid" ) const ( defaultCheckInterval = 1 * time.Hour OmsetMillionRupiah = 1_000_000.0 ) // OmsetMilestoneScheduler periodically checks each organization's total omset // and sends a notification to owner/admin users when a milestone is reached. // // NOTE: Milestone tracking is in-memory; notifications may re-trigger after a restart. // For persistent tracking, persist the notified state in the database. type OmsetMilestoneScheduler struct { orgRepo *repository.OrganizationRepositoryImpl userRepo *repository.UserRepositoryImpl notificationProc processor.NotificationProcessor mu sync.Mutex notified map[string]bool // "orgID:milestone" -> already notified stopCh chan struct{} } func NewOmsetMilestoneScheduler( orgRepo *repository.OrganizationRepositoryImpl, userRepo *repository.UserRepositoryImpl, notificationProc processor.NotificationProcessor, ) *OmsetMilestoneScheduler { return &OmsetMilestoneScheduler{ orgRepo: orgRepo, userRepo: userRepo, notificationProc: notificationProc, notified: make(map[string]bool), stopCh: make(chan struct{}), } } // Start begins the periodic milestone check in a background goroutine. func (s *OmsetMilestoneScheduler) Start(interval time.Duration) { if interval <= 0 { interval = defaultCheckInterval } go func() { // Perform an initial check immediately. s.checkAllOrganizations() ticker := time.NewTicker(interval) defer ticker.Stop() for { select { case <-ticker.C: s.checkAllOrganizations() case <-s.stopCh: log.Println("Omset milestone scheduler stopped") return } } }() log.Println("Omset milestone scheduler started") } // Stop signals the scheduler to stop. func (s *OmsetMilestoneScheduler) Stop() { close(s.stopCh) } func (s *OmsetMilestoneScheduler) checkAllOrganizations() { ctx := context.Background() orgs, _, err := s.orgRepo.List(ctx, nil, 1000, 0) if err != nil { log.Printf("OmsetMilestoneScheduler: failed to list organizations: %v", err) return } for _, org := range orgs { s.checkOrganization(ctx, org) } } func (s *OmsetMilestoneScheduler) checkOrganization(ctx context.Context, org *entities.Organization) { totalOmset, err := s.orgRepo.GetTotalOmset(ctx, org.ID) if err != nil { log.Printf("OmsetMilestoneScheduler: failed to get total omset for org %s: %v", org.ID, err) return } milestones := []float64{OmsetMillionRupiah} for _, milestone := range milestones { if totalOmset < milestone { continue } key := fmt.Sprintf("%s:%.0f", org.ID.String(), milestone) s.mu.Lock() if s.notified[key] { s.mu.Unlock() continue } s.notified[key] = true s.mu.Unlock() s.sendMilestoneNotification(ctx, org, totalOmset, milestone) } } func (s *OmsetMilestoneScheduler) sendMilestoneNotification(ctx context.Context, org *entities.Organization, totalOmset float64, milestone float64) { users, err := s.userRepo.GetByOrganizationID(ctx, org.ID) if err != nil { log.Printf("OmsetMilestoneScheduler: failed to get users for org %s: %v", org.ID, err) return } // Notify owner and admin users. var receiverIDs []uuid.UUID for _, user := range users { roleStr := string(user.Role) if roleStr == string(constants.RoleOwner) || roleStr == string(constants.RoleAdmin) { receiverIDs = append(receiverIDs, user.ID) } } if len(receiverIDs) == 0 { return } orgID := org.ID title := "🎉 Selamat! Omset Telah Mencapai 1 Juta Rupiah" body := fmt.Sprintf("Organisasi %s telah mencapai omset Rp %.0f. Terus tingkatkan prestasinya!", org.Name, totalOmset) notifReq := &models.SendNotificationRequest{ Title: title, Body: body, Type: "milestone", Category: "omset_milestone", NotifiableType: "organization", NotifiableID: &orgID, ReceiverIDs: receiverIDs, Data: map[string]interface{}{ "organization_id": org.ID.String(), "total_omset": totalOmset, "milestone": milestone, }, } if _, err := s.notificationProc.Send(ctx, notifReq); err != nil { log.Printf("OmsetMilestoneScheduler: failed to send notification for org %s: %v", org.ID, err) } else { log.Printf("OmsetMilestoneScheduler: sent milestone notification to org %s (omset: %.0f)", org.ID, totalOmset) } }