205 lines
5.7 KiB
Go
205 lines
5.7 KiB
Go
package service
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"log"
|
|
"math"
|
|
"sync"
|
|
"time"
|
|
|
|
"apskel-pos-be/internal/constants"
|
|
"apskel-pos-be/internal/models"
|
|
"apskel-pos-be/internal/processor"
|
|
"apskel-pos-be/internal/repository"
|
|
|
|
"github.com/google/uuid"
|
|
)
|
|
|
|
const (
|
|
defaultCheckInterval = 5 * time.Minute
|
|
OmsetMillionRupiah = 1_000_000.0
|
|
)
|
|
|
|
// 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, …).
|
|
//
|
|
// 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 // "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),
|
|
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 on startup.
|
|
s.checkAllOutlets()
|
|
|
|
ticker := time.NewTicker(interval)
|
|
defer ticker.Stop()
|
|
|
|
for {
|
|
select {
|
|
case <-ticker.C:
|
|
s.checkAllOutlets()
|
|
case <-s.stopCh:
|
|
log.Println("Omset milestone scheduler stopped")
|
|
return
|
|
}
|
|
}
|
|
}()
|
|
|
|
log.Printf("Omset milestone scheduler started (interval: %s)", interval)
|
|
}
|
|
|
|
// Stop signals the scheduler to stop.
|
|
func (s *OmsetMilestoneScheduler) Stop() {
|
|
close(s.stopCh)
|
|
}
|
|
|
|
func (s *OmsetMilestoneScheduler) checkAllOutlets() {
|
|
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 {
|
|
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
|
|
}
|
|
|
|
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] {
|
|
s.mu.Unlock()
|
|
continue
|
|
}
|
|
s.notified[key] = true
|
|
s.mu.Unlock()
|
|
|
|
milestone := float64(n) * OmsetMillionRupiah
|
|
s.sendMilestoneNotification(ctx, organizationID, outletID, outletName, todayOmset, milestone, n)
|
|
}
|
|
}
|
|
|
|
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", organizationID, err)
|
|
return
|
|
}
|
|
|
|
var receiverIDs []uuid.UUID
|
|
for _, u := range users {
|
|
role := string(u.Role)
|
|
if role == string(constants.RoleOwner) || role == string(constants.RoleManager) {
|
|
receiverIDs = append(receiverIDs, u.ID)
|
|
}
|
|
}
|
|
|
|
if len(receiverIDs) == 0 {
|
|
return
|
|
}
|
|
|
|
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: "outlet",
|
|
NotifiableID: &outletID,
|
|
ReceiverIDs: receiverIDs,
|
|
Data: map[string]interface{}{
|
|
"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 outlet %s: %v", outletID, err)
|
|
} else {
|
|
log.Printf("OmsetMilestoneScheduler: sent milestone x%d (Rp %.0f) for outlet %s (today omset: %.0f)",
|
|
multiple, milestone, outletName, todayOmset)
|
|
}
|
|
}
|