Files
gochat/internal/service/working_hour_service.go
T
2026-06-04 15:44:48 +08:00

157 lines
5.5 KiB
Go

package service
import (
"context"
"fmt"
"time"
"github.com/gochat/gochat/internal/model"
"github.com/gochat/gochat/internal/repository"
)
// WorkingHourService implements business logic for working hours / out-of-office detection.
// Reference: Chatwoot app/models/concerns/out_of_offisable.rb
type WorkingHourService struct {
whRepo *repository.WorkingHourRepo
inboxRepo *repository.InboxRepo
accountRepo *repository.AccountRepo
}
func NewWorkingHourService(whRepo *repository.WorkingHourRepo, inboxRepo *repository.InboxRepo, accountRepo *repository.AccountRepo) *WorkingHourService {
return &WorkingHourService{whRepo: whRepo, inboxRepo: inboxRepo, accountRepo: accountRepo}
}
// IsOutOfOffice checks whether an inbox is currently out of office.
// Reference: Chatwoot out_of_offisable.rb — out_of_office?
// - Returns true if: working_hours_enabled AND today's working hour says closed_now
func (s *WorkingHourService) IsOutOfOffice(ctx context.Context, inboxID uint) (bool, error) {
inbox, err := s.inboxRepo.FindByID(ctx, inboxID)
if err != nil {
return false, fmt.Errorf("inbox not found: %w", err)
}
// Chatwoot: "working_hours_enabled?" must be true to check out-of-office
if !inbox.WorkingHoursEnabled {
return false, nil // working hours not enabled → always "in office"
}
// Get today's day_of_week in the inbox's timezone
// Chatwoot: Time.zone.now.in_time_zone(inbox.timezone).to_date.wday
tz := time.UTC
if inbox.Timezone != "" {
loc, err := time.LoadLocation(inbox.Timezone)
if err == nil {
tz = loc
}
}
now := time.Now().In(tz)
dayOfWeek := int(now.Weekday()) // Sunday=0 in Go, matching Chatwoot
// Find the working hour for today
wh, err := s.whRepo.FindByInboxAndDay(ctx, inboxID, dayOfWeek)
if err != nil {
// No working hour record for this day → assume closed
return true, nil
}
return s.isClosedNow(wh, now, tz), nil
}
// isClosedNow checks whether a specific WorkingHour indicates the inbox is closed right now.
// Reference: Chatwoot working_hour.rb — closed_now?
// - closed_all_day → always closed
// - open_all_day → never closed
// - otherwise: check if current time is outside [open_time, close_time)
func (s *WorkingHourService) isClosedNow(wh *model.WorkingHour, now time.Time, tz *time.Location) bool {
if wh.ClosedAllDay {
return true
}
if wh.OpenAllDay {
return false
}
if wh.OpenHour == nil || wh.CloseHour == nil {
return true // missing data → assume closed
}
// Chatwoot: open_at?(time) and closed_now? logic
// open_time = today at open_hour:open_minutes in inbox timezone
// close_time = today at close_hour:close_minutes in inbox timezone
openMinutes := *wh.OpenHour * 60 + *wh.OpenMinutes
closeMinutes := *wh.CloseHour * 60 + *wh.CloseMinutes
currentMinutes := now.Hour() * 60 + now.Minute()
// closed_now: current time is NOT within open_time..close_time
// Chatwoot: open_at?(time) → time >= open_time && time < close_time
return currentMinutes < openMinutes || currentMinutes >= closeMinutes
}
// GetWeeklySchedule returns the 7-day working hours schedule for an inbox.
// Reference: Chatwoot out_of_offisable.rb — weekly_schedule
func (s *WorkingHourService) GetWeeklySchedule(ctx context.Context, inboxID uint) ([]WeeklyScheduleEntry, error) {
hours, err := s.whRepo.FindByInbox(ctx, inboxID)
if err != nil {
return nil, err
}
entries := make([]WeeklyScheduleEntry, 0, len(hours))
for _, wh := range hours {
entry := WeeklyScheduleEntry{
DayOfWeek: wh.DayOfWeek,
ClosedAllDay: wh.ClosedAllDay,
OpenAllDay: wh.OpenAllDay,
OpenHour: wh.OpenHour,
OpenMinutes: wh.OpenMinutes,
CloseHour: wh.CloseHour,
CloseMinutes: wh.CloseMinutes,
}
entries = append(entries, entry)
}
return entries, nil
}
// UpdateWeeklySchedule bulk-updates the working hours schedule.
// Reference: Chatwoot out_of_offisable.rb — update_working_hours(params)
// - Validates each entry before saving
// - Runs updates in a transaction
func (s *WorkingHourService) UpdateWeeklySchedule(ctx context.Context, inboxID uint, params []repository.WorkingHourUpdateParam) ([]WeeklyScheduleEntry, error) {
// Validate each entry first
for i, p := range params {
temp := &model.WorkingHour{
DayOfWeek: p.DayOfWeek,
ClosedAllDay: p.ClosedAllDay,
OpenAllDay: p.OpenAllDay,
OpenHour: p.OpenHour,
OpenMinutes: p.OpenMinutes,
CloseHour: p.CloseHour,
CloseMinutes: p.CloseMinutes,
}
temp.EnsureOpenAllDayHours()
if errs := temp.Validate(); len(errs) > 0 {
return nil, fmt.Errorf("validation error for day %d (entry %d): %v", p.DayOfWeek, i, errs)
}
}
if err := s.whRepo.UpdateWorkingHours(ctx, inboxID, params); err != nil {
return nil, fmt.Errorf("update working hours: %w", err)
}
return s.GetWeeklySchedule(ctx, inboxID)
}
// InitDefaultWorkingHours creates the 7 default working hours when an inbox is created.
// Reference: Chatwoot out_of_offisable.rb — after_create :create_default_working_hours
func (s *WorkingHourService) InitDefaultWorkingHours(ctx context.Context, inboxID, accountID uint) error {
return s.whRepo.CreateDefaultWorkingHours(ctx, inboxID, accountID)
}
// WeeklyScheduleEntry is the JSON response format for working hours.
// Reference: Chatwoot out_of_offisable.rb — weekly_schedule
type WeeklyScheduleEntry struct {
DayOfWeek int `json:"day_of_week"`
ClosedAllDay bool `json:"closed_all_day"`
OpenAllDay bool `json:"open_all_day"`
OpenHour *int `json:"open_hour"`
OpenMinutes *int `json:"open_minutes"`
CloseHour *int `json:"close_hour"`
CloseMinutes *int `json:"close_minutes"`
}