fix log error and omset tracker scheduled

This commit is contained in:
Efril
2026-06-03 22:01:58 +07:00
parent 957c1ae53d
commit 328336ea5a
7 changed files with 145 additions and 49 deletions
+79 -46
View File
@@ -4,11 +4,11 @@ import (
"context"
"fmt"
"log"
"math"
"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"
@@ -17,32 +17,38 @@ import (
)
const (
defaultCheckInterval = 1 * time.Hour
defaultCheckInterval = 5 * time.Minute
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.
// OmsetMilestoneScheduler periodically checks each outlet's omset for the
// current calendar day and sends a notification every time it crosses a new
// multiple of OmsetMillionRupiah (1 jt, 2 jt, 3 jt, …).
//
// NOTE: Milestone tracking is in-memory; notifications may re-trigger after a restart.
// For persistent tracking, persist the notified state in the database.
// The notified state is keyed by "outletID:YYYY-MM-DD:N" so each multiple is
// only notified once per day. State resets naturally on the next day (new key).
// NOTE: state is in-memory; a server restart within the same day may re-send
// notifications for already-crossed milestones.
type OmsetMilestoneScheduler struct {
orgRepo *repository.OrganizationRepositoryImpl
outletRepo *repository.OutletRepositoryImpl
userRepo *repository.UserRepositoryImpl
notificationProc processor.NotificationProcessor
mu sync.Mutex
notified map[string]bool // "orgID:milestone" -> already notified
notified map[string]bool // "outletID:YYYY-MM-DD:N" -> already notified
stopCh chan struct{}
}
func NewOmsetMilestoneScheduler(
orgRepo *repository.OrganizationRepositoryImpl,
outletRepo *repository.OutletRepositoryImpl,
userRepo *repository.UserRepositoryImpl,
notificationProc processor.NotificationProcessor,
) *OmsetMilestoneScheduler {
return &OmsetMilestoneScheduler{
orgRepo: orgRepo,
outletRepo: outletRepo,
userRepo: userRepo,
notificationProc: notificationProc,
notified: make(map[string]bool),
@@ -57,8 +63,8 @@ func (s *OmsetMilestoneScheduler) Start(interval time.Duration) {
}
go func() {
// Perform an initial check immediately.
s.checkAllOrganizations()
// Perform an initial check immediately on startup.
s.checkAllOutlets()
ticker := time.NewTicker(interval)
defer ticker.Stop()
@@ -66,7 +72,7 @@ func (s *OmsetMilestoneScheduler) Start(interval time.Duration) {
for {
select {
case <-ticker.C:
s.checkAllOrganizations()
s.checkAllOutlets()
case <-s.stopCh:
log.Println("Omset milestone scheduler stopped")
return
@@ -74,7 +80,7 @@ func (s *OmsetMilestoneScheduler) Start(interval time.Duration) {
}
}()
log.Println("Omset milestone scheduler started")
log.Printf("Omset milestone scheduler started (interval: %s)", interval)
}
// Stop signals the scheduler to stop.
@@ -82,7 +88,7 @@ func (s *OmsetMilestoneScheduler) Stop() {
close(s.stopCh)
}
func (s *OmsetMilestoneScheduler) checkAllOrganizations() {
func (s *OmsetMilestoneScheduler) checkAllOutlets() {
ctx := context.Background()
orgs, _, err := s.orgRepo.List(ctx, nil, 1000, 0)
@@ -92,25 +98,38 @@ func (s *OmsetMilestoneScheduler) checkAllOrganizations() {
}
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 {
outlets, err := s.outletRepo.GetByOrganizationID(ctx, org.ID)
if err != nil {
log.Printf("OmsetMilestoneScheduler: failed to list outlets for org %s: %v", org.ID, err)
continue
}
key := fmt.Sprintf("%s:%.0f", org.ID.String(), milestone)
for _, outlet := range outlets {
if !outlet.IsActive {
continue
}
s.checkOutlet(ctx, org.ID, outlet.ID, outlet.Name)
}
}
}
func (s *OmsetMilestoneScheduler) checkOutlet(ctx context.Context, organizationID, outletID uuid.UUID, outletName string) {
todayOmset, err := s.outletRepo.GetTodayOmset(ctx, outletID)
if err != nil {
log.Printf("OmsetMilestoneScheduler: failed to get today's omset for outlet %s: %v", outletID, err)
return
}
if todayOmset < OmsetMillionRupiah {
return
}
// How many full multiples of 1 juta have been crossed today?
crossedMultiple := int(math.Floor(todayOmset / OmsetMillionRupiah))
today := time.Now().Format("2006-01-02")
for n := 1; n <= crossedMultiple; n++ {
key := fmt.Sprintf("%s:%s:%d", outletID.String(), today, n)
s.mu.Lock()
if s.notified[key] {
@@ -120,23 +139,31 @@ func (s *OmsetMilestoneScheduler) checkOrganization(ctx context.Context, org *en
s.notified[key] = true
s.mu.Unlock()
s.sendMilestoneNotification(ctx, org, totalOmset, milestone)
milestone := float64(n) * OmsetMillionRupiah
s.sendMilestoneNotification(ctx, organizationID, outletID, outletName, todayOmset, milestone, n)
}
}
func (s *OmsetMilestoneScheduler) sendMilestoneNotification(ctx context.Context, org *entities.Organization, totalOmset float64, milestone float64) {
users, err := s.userRepo.GetByOrganizationID(ctx, org.ID)
func (s *OmsetMilestoneScheduler) sendMilestoneNotification(
ctx context.Context,
organizationID, outletID uuid.UUID,
outletName string,
todayOmset, milestone float64,
multiple int,
) {
// Fetch all users in the org, then filter to owner and manager only.
// These roles are not assigned to a specific outlet, so we query by org.
users, err := s.userRepo.GetByOrganizationID(ctx, organizationID)
if err != nil {
log.Printf("OmsetMilestoneScheduler: failed to get users for org %s: %v", org.ID, err)
log.Printf("OmsetMilestoneScheduler: failed to get users for org %s: %v", organizationID, 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)
for _, u := range users {
role := string(u.Role)
if role == string(constants.RoleOwner) || role == string(constants.RoleManager) {
receiverIDs = append(receiverIDs, u.ID)
}
}
@@ -144,28 +171,34 @@ func (s *OmsetMilestoneScheduler) sendMilestoneNotification(ctx context.Context,
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)
title := fmt.Sprintf("🎉 Omset %s Hari Ini Mencapai Rp %.0f!", outletName, milestone)
body := fmt.Sprintf(
"Selamat! Omset outlet %s hari ini sudah menembus Rp %.0f (total hari ini: Rp %.0f). Terus semangat!",
outletName, milestone, todayOmset,
)
notifReq := &models.SendNotificationRequest{
Title: title,
Body: body,
Type: "milestone",
Category: "omset_milestone",
NotifiableType: "organization",
NotifiableID: &orgID,
NotifiableType: "outlet",
NotifiableID: &outletID,
ReceiverIDs: receiverIDs,
Data: map[string]interface{}{
"organization_id": org.ID.String(),
"total_omset": totalOmset,
"organization_id": organizationID.String(),
"outlet_id": outletID.String(),
"outlet_name": outletName,
"today_omset": todayOmset,
"milestone": milestone,
"multiple": multiple,
},
}
if _, err := s.notificationProc.Send(ctx, notifReq); err != nil {
log.Printf("OmsetMilestoneScheduler: failed to send notification for org %s: %v", org.ID, err)
log.Printf("OmsetMilestoneScheduler: failed to send notification for outlet %s: %v", outletID, err)
} else {
log.Printf("OmsetMilestoneScheduler: sent milestone notification to org %s (omset: %.0f)", org.ID, totalOmset)
log.Printf("OmsetMilestoneScheduler: sent milestone x%d (Rp %.0f) for outlet %s (today omset: %.0f)",
multiple, milestone, outletName, todayOmset)
}
}