157 lines
5.5 KiB
Go
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"`
|
|
} |