feat(inboxes): persist chatwoot working hours

This commit is contained in:
2026-06-05 05:19:36 +08:00
parent ff98202c38
commit 945782eb03
8 changed files with 347 additions and 75 deletions
+16
View File
@@ -7,6 +7,7 @@ import (
"strings"
"github.com/gin-gonic/gin"
"github.com/gochat/gochat/internal/repository"
"github.com/gochat/gochat/internal/service"
"github.com/gochat/gochat/pkg/response"
)
@@ -317,6 +318,8 @@ func applyCreateInboxForm(req *service.CreateInboxRequest, values map[string][]s
req.BusinessName = inboxStringPtr(value)
case "csat_config":
req.CsatConfig = inboxJSONObject(value)
case "working_hours":
req.WorkingHours = inboxWorkingHours(value)
}
}
}
@@ -369,6 +372,8 @@ func applyUpdateInboxForm(req *service.UpdateInboxRequest, values map[string][]s
req.BusinessName = inboxStringPtr(value)
case "csat_config":
req.CsatConfig = inboxJSONObject(value)
case "working_hours":
req.WorkingHours = inboxWorkingHours(value)
}
}
}
@@ -443,6 +448,17 @@ func inboxJSONObject(value string) map[string]any {
return data
}
func inboxWorkingHours(value string) []repository.WorkingHourUpdateParam {
if value == "" {
return nil
}
var data []repository.WorkingHourUpdateParam
if err := json.Unmarshal([]byte(value), &data); err != nil {
return nil
}
return data
}
// ========================================
// Member-action handlers (Chatwoot InboxesController member routes)
// ========================================
@@ -33,7 +33,7 @@ func TestInboxHandler_ChatwootSerializerParity(t *testing.T) {
_ = sqlDB.Close()
}
})
require.NoError(t, db.AutoMigrate(&model.Account{}, &model.Inbox{}))
require.NoError(t, db.AutoMigrate(&model.Account{}, &model.Inbox{}, &model.WorkingHour{}))
account := &model.Account{Name: "Inbox Parity", Locale: "en", Active: true}
require.NoError(t, db.Create(account).Error)
@@ -123,7 +123,7 @@ func TestInboxHandler_ChatwootCreateUpdateRequestBinding(t *testing.T) {
_ = sqlDB.Close()
}
})
require.NoError(t, db.AutoMigrate(&model.Account{}, &model.Inbox{}))
require.NoError(t, db.AutoMigrate(&model.Account{}, &model.Inbox{}, &model.WorkingHour{}))
account := &model.Account{Name: "Inbox Binding", Locale: "en", Active: true}
require.NoError(t, db.Create(account).Error)
@@ -206,6 +206,41 @@ func TestInboxHandler_ChatwootCreateUpdateRequestBinding(t *testing.T) {
require.Equal(t, "Rate this conversation", csatConfig["message"])
require.Equal(t, "Please rate us", csatConfig["button_text"])
require.ElementsMatch(t, []any{"vip", "priority"}, csatConfig["survey_rules"].(map[string]any)["values"].([]any))
workingHoursUpdate := inboxParityRequest(t, router, http.MethodPut, fmt.Sprintf("/api/v1/accounts/%d/inboxes/%d", account.ID, websiteID), map[string]any{
"working_hours_enabled": true,
"out_of_office_message": "We are away",
"timezone": "Asia/Shanghai",
"working_hours": []map[string]any{
{"day_of_week": 0, "closed_all_day": true, "open_hour": "", "open_minutes": "", "close_hour": "", "close_minutes": "", "open_all_day": false},
{"day_of_week": 1, "closed_all_day": false, "open_hour": "9", "open_minutes": "30", "close_hour": "18", "close_minutes": "0", "open_all_day": false},
{"day_of_week": 2, "closed_all_day": false, "open_hour": "", "open_minutes": "", "close_hour": "", "close_minutes": "", "open_all_day": true},
{"day_of_week": 3, "closed_all_day": true, "open_hour": "", "open_minutes": "", "close_hour": "", "close_minutes": "", "open_all_day": false},
{"day_of_week": 4, "closed_all_day": true, "open_hour": "", "open_minutes": "", "close_hour": "", "close_minutes": "", "open_all_day": false},
{"day_of_week": 5, "closed_all_day": true, "open_hour": "", "open_minutes": "", "close_hour": "", "close_minutes": "", "open_all_day": false},
{"day_of_week": 6, "closed_all_day": true, "open_hour": "", "open_minutes": "", "close_hour": "", "close_minutes": "", "open_all_day": false},
},
"channel": map[string]any{},
})
require.Equal(t, http.StatusOK, workingHoursUpdate.Code, workingHoursUpdate.Body.String())
workingHoursData := inboxParityObject(t, workingHoursUpdate)
require.Equal(t, true, workingHoursData["working_hours_enabled"])
require.Equal(t, "We are away", workingHoursData["out_of_office_message"])
require.Equal(t, "Asia/Shanghai", workingHoursData["timezone"])
workingHours := workingHoursData["working_hours"].([]any)
require.Len(t, workingHours, 7)
monday := workingHours[1].(map[string]any)
require.Equal(t, float64(1), monday["day_of_week"])
require.Equal(t, float64(9), monday["open_hour"])
require.Equal(t, float64(30), monday["open_minutes"])
tuesday := workingHours[2].(map[string]any)
require.Equal(t, true, tuesday["open_all_day"])
require.Equal(t, float64(0), tuesday["open_hour"])
require.Equal(t, float64(23), tuesday["close_hour"])
showUpdated := inboxParityRequest(t, router, http.MethodGet, fmt.Sprintf("/api/v1/accounts/%d/inboxes/%d", account.ID, websiteID), nil)
require.Equal(t, http.StatusOK, showUpdated.Code, showUpdated.Body.String())
require.Len(t, inboxParityObject(t, showUpdated)["working_hours"].([]any), 7)
}
func setupInboxParityRouter(db *gorm.DB) *gin.Engine {
+20 -1
View File
@@ -32,7 +32,7 @@ func serializeInbox(inbox *model.Inbox) map[string]any {
"enable_auto_assignment": inbox.EnableAutoAssignment,
"auto_assignment_config": configValue(config, "auto_assignment_config"),
"out_of_office_message": inbox.OutOfOfficeMessage,
"working_hours": configArray(config, "working_hours"),
"working_hours": serializeInboxWorkingHours(inbox, config),
"timezone": inbox.Timezone,
"callback_webhook_url": configValue(config, "callback_webhook_url"),
"allow_messages_after_resolved": inbox.AllowMessagesAfterResolved,
@@ -96,6 +96,25 @@ func serializeInbox(inbox *model.Inbox) map[string]any {
return payload
}
func serializeInboxWorkingHours(inbox *model.Inbox, config map[string]any) any {
if len(inbox.WorkingHours) == 0 {
return configArray(config, "working_hours")
}
payload := make([]any, 0, len(inbox.WorkingHours))
for _, wh := range inbox.WorkingHours {
payload = append(payload, map[string]any{
"day_of_week": wh.DayOfWeek,
"closed_all_day": wh.ClosedAllDay,
"open_hour": wh.OpenHour,
"open_minutes": wh.OpenMinutes,
"close_hour": wh.CloseHour,
"close_minutes": wh.CloseMinutes,
"open_all_day": wh.OpenAllDay,
})
}
return payload
}
func chatwootChannelType(channelType string) string {
if strings.HasPrefix(channelType, "Channel::") {
return channelType
+35 -31
View File
@@ -4,45 +4,49 @@ package model
// Reference: Chatwoot app/models/inbox.rb
//
// Chatwoot inbox attributes (permitted_params in InboxesController):
// name, avatar, greeting_enabled, greeting_message, enable_email_collect,
// csat_survey_enabled, enable_auto_assignment, working_hours_enabled,
// out_of_office_message, timezone, allow_messages_after_resolved,
// lock_to_single_conversation, portal_id, sender_name_type, business_name,
// csat_config (nested hash)
//
// name, avatar, greeting_enabled, greeting_message, enable_email_collect,
// csat_survey_enabled, enable_auto_assignment, working_hours_enabled,
// out_of_office_message, timezone, allow_messages_after_resolved,
// lock_to_single_conversation, portal_id, sender_name_type, business_name,
// csat_config (nested hash)
//
// Additional Chatwoot fields not in permitted_params but in model:
// channel_type, channel_id, account_id, email_address (for email channel),
// webhook_url (for API channel), identifier (unique slug)
//
// channel_type, channel_id, account_id, email_address (for email channel),
// webhook_url (for API channel), identifier (unique slug)
type Inbox struct {
Base
AccountID uint `gorm:"index;not null" json:"account_id"`
Name string `gorm:"size:255;not null" json:"name"`
ChannelType string `gorm:"size:50;index;not null" json:"channel_type"` // web_widget, facebook, twitter, whatsapp, telegram, email, api, etc
ChannelID uint `gorm:"index;not null" json:"channel_id"`
EnableAutoAssignment bool `gorm:"default:false" json:"enable_auto_assignment"`
AutoAssignmentLimit int `gorm:"default:0" json:"auto_assignment_limit"`
Enabled bool `gorm:"default:true" json:"enabled"`
ChannelConfig string `gorm:"type:text" json:"channel_config,omitempty"` // JSON-encoded per-inbox channel configuration
AccountID uint `gorm:"index;not null" json:"account_id"`
Name string `gorm:"size:255;not null" json:"name"`
ChannelType string `gorm:"size:50;index;not null" json:"channel_type"` // web_widget, facebook, twitter, whatsapp, telegram, email, api, etc
ChannelID uint `gorm:"index;not null" json:"channel_id"`
EnableAutoAssignment bool `gorm:"default:false" json:"enable_auto_assignment"`
AutoAssignmentLimit int `gorm:"default:0" json:"auto_assignment_limit"`
Enabled bool `gorm:"default:true" json:"enabled"`
ChannelConfig string `gorm:"type:text" json:"channel_config,omitempty"` // JSON-encoded per-inbox channel configuration
// Chatwoot inbox settings (from permitted_params)
GreetingEnabled bool `gorm:"default:false" json:"greeting_enabled"`
GreetingMessage string `gorm:"type:text" json:"greeting_message,omitempty"`
EnableEmailCollect bool `gorm:"default:true" json:"enable_email_collect"`
CsatSurveyEnabled bool `gorm:"default:false" json:"csat_survey_enabled"`
WorkingHoursEnabled bool `gorm:"default:false" json:"working_hours_enabled"`
OutOfOfficeMessage string `gorm:"type:text" json:"out_of_office_message,omitempty"`
Timezone string `gorm:"size:100" json:"timezone,omitempty"` // e.g. "Asia/Kolkata"
AllowMessagesAfterResolved bool `gorm:"default:true" json:"allow_messages_after_resolved"`
LockToSingleConversation bool `gorm:"default:false" json:"lock_to_single_conversation"`
SenderNameType string `gorm:"size:50;default:friendly_name" json:"sender_name_type,omitempty"` // friendly_name, business_name, random
BusinessName string `gorm:"size:255" json:"business_name,omitempty"`
CsatConfig string `gorm:"type:text" json:"csat_config,omitempty"` // JSON-encoded CSAT survey configuration
AvatarURL string `gorm:"size:1024" json:"avatar_url,omitempty"` // URL to inbox avatar image
PortalID *uint `gorm:"index" json:"portal_id,omitempty"` // FK to help-center portal (nullable)
GreetingEnabled bool `gorm:"default:false" json:"greeting_enabled"`
GreetingMessage string `gorm:"type:text" json:"greeting_message,omitempty"`
EnableEmailCollect bool `gorm:"default:true" json:"enable_email_collect"`
CsatSurveyEnabled bool `gorm:"default:false" json:"csat_survey_enabled"`
WorkingHoursEnabled bool `gorm:"default:false" json:"working_hours_enabled"`
OutOfOfficeMessage string `gorm:"type:text" json:"out_of_office_message,omitempty"`
Timezone string `gorm:"size:100" json:"timezone,omitempty"` // e.g. "Asia/Kolkata"
AllowMessagesAfterResolved bool `gorm:"default:true" json:"allow_messages_after_resolved"`
LockToSingleConversation bool `gorm:"default:false" json:"lock_to_single_conversation"`
SenderNameType string `gorm:"size:50;default:friendly_name" json:"sender_name_type,omitempty"` // friendly_name, business_name, random
BusinessName string `gorm:"size:255" json:"business_name,omitempty"`
CsatConfig string `gorm:"type:text" json:"csat_config,omitempty"` // JSON-encoded CSAT survey configuration
AvatarURL string `gorm:"size:1024" json:"avatar_url,omitempty"` // URL to inbox avatar image
PortalID *uint `gorm:"index" json:"portal_id,omitempty"` // FK to help-center portal (nullable)
// API inbox specific fields
WebhookURL string `gorm:"size:1024" json:"webhook_url,omitempty"` // webhook URL for API inboxes
Secret string `gorm:"size:255" json:"secret,omitempty"` // HMAC secret for API inbox webhook verification
Secret string `gorm:"size:255" json:"secret,omitempty"` // HMAC secret for API inbox webhook verification
WorkingHours []WorkingHour `gorm:"foreignKey:InboxID" json:"working_hours,omitempty"`
}
func (Inbox) TableName() string { return "inboxes" }
func (Inbox) TableName() string { return "inboxes" }
+13 -4
View File
@@ -27,7 +27,7 @@ func (r *InboxRepo) DB() *gorm.DB { return r.db }
// FindByID retrieves an inbox by primary key.
func (r *InboxRepo) FindByID(ctx context.Context, id uint) (*model.Inbox, error) {
var inbox model.Inbox
err := r.db.WithContext(ctx).First(&inbox, id).Error
err := r.withWorkingHours(r.db.WithContext(ctx)).First(&inbox, id).Error
if err != nil {
return nil, err
}
@@ -44,7 +44,7 @@ func (r *InboxRepo) FindByAccount(ctx context.Context, accountID uint, offset, l
return nil, 0, err
}
err := r.db.WithContext(ctx).Where("account_id = ?", accountID).
err := r.withWorkingHours(r.db.WithContext(ctx)).Where("account_id = ?", accountID).
Offset(offset).Limit(limit).Order("id DESC").
Find(&inboxes).Error
return inboxes, total, err
@@ -57,7 +57,7 @@ func (r *InboxRepo) Create(ctx context.Context, inbox *model.Inbox) error {
// Update modifies an existing inbox.
func (r *InboxRepo) Update(ctx context.Context, inbox *model.Inbox) error {
return r.db.WithContext(ctx).Save(inbox).Error
return r.db.WithContext(ctx).Omit("WorkingHours").Save(inbox).Error
}
// Delete soft-deletes an inbox.
@@ -68,13 +68,22 @@ func (r *InboxRepo) Delete(ctx context.Context, id uint) error {
// FindByAccountAndID retrieves an inbox scoped to an account.
func (r *InboxRepo) FindByAccountAndID(ctx context.Context, accountID, id uint) (*model.Inbox, error) {
var inbox model.Inbox
err := r.db.WithContext(ctx).Where("account_id = ? AND id = ?", accountID, id).First(&inbox).Error
err := r.withWorkingHours(r.db.WithContext(ctx)).Where("account_id = ? AND id = ?", accountID, id).First(&inbox).Error
if err != nil {
return nil, err
}
return &inbox, nil
}
func (r *InboxRepo) withWorkingHours(db *gorm.DB) *gorm.DB {
if r == nil || r.db == nil || !r.db.Migrator().HasTable(&model.WorkingHour{}) {
return db
}
return db.Preload("WorkingHours", func(tx *gorm.DB) *gorm.DB {
return tx.Order("day_of_week ASC")
})
}
// CountByAccount returns total number of inboxes in an account.
func (r *InboxRepo) CountByAccount(ctx context.Context, accountID uint) (int64, error) {
var total int64
+66 -2
View File
@@ -2,6 +2,8 @@ package repository
import (
"context"
"encoding/json"
"strconv"
"github.com/gochat/gochat/internal/model"
"gorm.io/gorm"
@@ -52,7 +54,12 @@ func (r *WorkingHourRepo) CreateDefaultWorkingHours(ctx context.Context, inboxID
{InboxID: inboxID, AccountID: accountID, DayOfWeek: 5, OpenHour: &nine, OpenMinutes: &zero, CloseHour: &seventeen, CloseMinutes: &zero, OpenAllDay: false},
{InboxID: inboxID, AccountID: accountID, DayOfWeek: 6, ClosedAllDay: true, OpenAllDay: false},
}
return r.db.WithContext(ctx).Create(&defaults).Error
for i := range defaults {
if err := r.db.WithContext(ctx).Create(&defaults[i]).Error; err != nil {
return err
}
}
return nil
}
// UpdateWorkingHours bulk-updates the weekly schedule for an inbox.
@@ -93,4 +100,61 @@ type WorkingHourUpdateParam struct {
OpenMinutes *int `json:"open_minutes"`
CloseHour *int `json:"close_hour"`
CloseMinutes *int `json:"close_minutes"`
}
}
func (p *WorkingHourUpdateParam) UnmarshalJSON(data []byte) error {
var raw map[string]interface{}
if err := json.Unmarshal(data, &raw); err != nil {
return err
}
p.DayOfWeek = intFromAny(raw["day_of_week"])
p.ClosedAllDay = boolFromAny(raw["closed_all_day"])
p.OpenAllDay = boolFromAny(raw["open_all_day"])
p.OpenHour = intPtrFromAny(raw["open_hour"])
p.OpenMinutes = intPtrFromAny(raw["open_minutes"])
p.CloseHour = intPtrFromAny(raw["close_hour"])
p.CloseMinutes = intPtrFromAny(raw["close_minutes"])
return nil
}
func intFromAny(value interface{}) int {
if ptr := intPtrFromAny(value); ptr != nil {
return *ptr
}
return 0
}
func intPtrFromAny(value interface{}) *int {
switch typed := value.(type) {
case nil:
return nil
case float64:
v := int(typed)
return &v
case int:
return &typed
case string:
if typed == "" || typed == "null" {
return nil
}
parsed, err := strconv.Atoi(typed)
if err != nil {
return nil
}
return &parsed
default:
return nil
}
}
func boolFromAny(value interface{}) bool {
switch typed := value.(type) {
case bool:
return typed
case string:
parsed, _ := strconv.ParseBool(typed)
return parsed
default:
return false
}
}
+107 -35
View File
@@ -73,24 +73,25 @@ func (s *InboxService) GetByAccountAndID(ctx context.Context, accountID, id uint
// CreateInboxRequest is the DTO for creating an inbox.
type CreateInboxRequest struct {
Name string `json:"name" validate:"omitempty,min=2"`
ChannelType string `json:"channel_type"`
Channel map[string]any `json:"channel,omitempty"`
Enabled bool `json:"enabled"`
EnableAutoAssignment bool `json:"enable_auto_assignment"`
GreetingEnabled *bool `json:"greeting_enabled,omitempty"`
GreetingMessage *string `json:"greeting_message,omitempty"`
EnableEmailCollect *bool `json:"enable_email_collect,omitempty"`
CsatSurveyEnabled *bool `json:"csat_survey_enabled,omitempty"`
WorkingHoursEnabled *bool `json:"working_hours_enabled,omitempty"`
OutOfOfficeMessage *string `json:"out_of_office_message,omitempty"`
Timezone *string `json:"timezone,omitempty"`
AllowMessagesAfterResolved *bool `json:"allow_messages_after_resolved,omitempty"`
LockToSingleConversation *bool `json:"lock_to_single_conversation,omitempty"`
PortalID *uint `json:"portal_id,omitempty"`
SenderNameType *string `json:"sender_name_type,omitempty"`
BusinessName *string `json:"business_name,omitempty"`
CsatConfig map[string]any `json:"csat_config,omitempty"`
Name string `json:"name" validate:"omitempty,min=2"`
ChannelType string `json:"channel_type"`
Channel map[string]any `json:"channel,omitempty"`
Enabled bool `json:"enabled"`
EnableAutoAssignment bool `json:"enable_auto_assignment"`
GreetingEnabled *bool `json:"greeting_enabled,omitempty"`
GreetingMessage *string `json:"greeting_message,omitempty"`
EnableEmailCollect *bool `json:"enable_email_collect,omitempty"`
CsatSurveyEnabled *bool `json:"csat_survey_enabled,omitempty"`
WorkingHoursEnabled *bool `json:"working_hours_enabled,omitempty"`
OutOfOfficeMessage *string `json:"out_of_office_message,omitempty"`
Timezone *string `json:"timezone,omitempty"`
AllowMessagesAfterResolved *bool `json:"allow_messages_after_resolved,omitempty"`
LockToSingleConversation *bool `json:"lock_to_single_conversation,omitempty"`
PortalID *uint `json:"portal_id,omitempty"`
SenderNameType *string `json:"sender_name_type,omitempty"`
BusinessName *string `json:"business_name,omitempty"`
CsatConfig map[string]any `json:"csat_config,omitempty"`
WorkingHours []repository.WorkingHourUpdateParam `json:"working_hours,omitempty"`
}
// Create creates a new inbox.
@@ -151,6 +152,14 @@ func (s *InboxService) Create(ctx context.Context, accountID uint, req CreateInb
return nil, err
}
applyCreateInboxSettings(inbox, req)
if err := s.ensureInboxWorkingHours(ctx, inbox); err != nil {
applogger.L().Warnf("Failed to initialize inbox working hours: %v", err)
}
if len(req.WorkingHours) > 0 {
if err := s.updateInboxWorkingHours(ctx, inbox, req.WorkingHours); err != nil {
return nil, err
}
}
if err := s.repo.Update(ctx, inbox); err != nil {
applogger.L().Errorf("Failed to persist inbox defaults: %v", err)
return nil, err
@@ -160,23 +169,24 @@ func (s *InboxService) Create(ctx context.Context, accountID uint, req CreateInb
// UpdateInboxRequest is the DTO for updating an inbox.
type UpdateInboxRequest struct {
Name string `json:"name,omitempty" validate:"omitempty,min=2"`
Enabled *bool `json:"enabled,omitempty"`
EnableAutoAssignment *bool `json:"enable_auto_assignment,omitempty"`
Channel map[string]any `json:"channel,omitempty"`
GreetingEnabled *bool `json:"greeting_enabled,omitempty"`
GreetingMessage *string `json:"greeting_message,omitempty"`
EnableEmailCollect *bool `json:"enable_email_collect,omitempty"`
CsatSurveyEnabled *bool `json:"csat_survey_enabled,omitempty"`
WorkingHoursEnabled *bool `json:"working_hours_enabled,omitempty"`
OutOfOfficeMessage *string `json:"out_of_office_message,omitempty"`
Timezone *string `json:"timezone,omitempty"`
AllowMessagesAfterResolved *bool `json:"allow_messages_after_resolved,omitempty"`
LockToSingleConversation *bool `json:"lock_to_single_conversation,omitempty"`
PortalID *uint `json:"portal_id,omitempty"`
SenderNameType *string `json:"sender_name_type,omitempty"`
BusinessName *string `json:"business_name,omitempty"`
CsatConfig map[string]any `json:"csat_config,omitempty"`
Name string `json:"name,omitempty" validate:"omitempty,min=2"`
Enabled *bool `json:"enabled,omitempty"`
EnableAutoAssignment *bool `json:"enable_auto_assignment,omitempty"`
Channel map[string]any `json:"channel,omitempty"`
GreetingEnabled *bool `json:"greeting_enabled,omitempty"`
GreetingMessage *string `json:"greeting_message,omitempty"`
EnableEmailCollect *bool `json:"enable_email_collect,omitempty"`
CsatSurveyEnabled *bool `json:"csat_survey_enabled,omitempty"`
WorkingHoursEnabled *bool `json:"working_hours_enabled,omitempty"`
OutOfOfficeMessage *string `json:"out_of_office_message,omitempty"`
Timezone *string `json:"timezone,omitempty"`
AllowMessagesAfterResolved *bool `json:"allow_messages_after_resolved,omitempty"`
LockToSingleConversation *bool `json:"lock_to_single_conversation,omitempty"`
PortalID *uint `json:"portal_id,omitempty"`
SenderNameType *string `json:"sender_name_type,omitempty"`
BusinessName *string `json:"business_name,omitempty"`
CsatConfig map[string]any `json:"csat_config,omitempty"`
WorkingHours []repository.WorkingHourUpdateParam `json:"working_hours,omitempty"`
}
// Update modifies an existing inbox.
@@ -212,6 +222,11 @@ func (s *InboxService) Update(ctx context.Context, accountID, id uint, req Updat
inbox.WebhookURL = webhookURL
}
}
if len(req.WorkingHours) > 0 {
if err := s.updateInboxWorkingHours(ctx, inbox, req.WorkingHours); err != nil {
return nil, err
}
}
if err := s.repo.Update(ctx, inbox); err != nil {
return nil, err
@@ -478,6 +493,63 @@ func mapString(values map[string]any, key string) string {
}
}
func (s *InboxService) ensureInboxWorkingHours(ctx context.Context, inbox *model.Inbox) error {
if s == nil || s.repo == nil || s.repo.DB() == nil {
return nil
}
db := s.repo.DB()
if !db.Migrator().HasTable(&model.WorkingHour{}) {
return nil
}
var count int64
if err := db.WithContext(ctx).Model(&model.WorkingHour{}).Where("inbox_id = ?", inbox.ID).Count(&count).Error; err != nil {
return err
}
if count > 0 {
return s.loadInboxWorkingHours(ctx, inbox)
}
if err := repository.NewWorkingHourRepo(db).CreateDefaultWorkingHours(ctx, inbox.ID, inbox.AccountID); err != nil {
return err
}
return s.loadInboxWorkingHours(ctx, inbox)
}
func (s *InboxService) updateInboxWorkingHours(ctx context.Context, inbox *model.Inbox, params []repository.WorkingHourUpdateParam) error {
if err := s.ensureInboxWorkingHours(ctx, inbox); err != nil {
return err
}
for i, param := range params {
temp := &model.WorkingHour{
InboxID: inbox.ID,
AccountID: inbox.AccountID,
DayOfWeek: param.DayOfWeek,
ClosedAllDay: param.ClosedAllDay,
OpenAllDay: param.OpenAllDay,
OpenHour: param.OpenHour,
OpenMinutes: param.OpenMinutes,
CloseHour: param.CloseHour,
CloseMinutes: param.CloseMinutes,
}
temp.EnsureOpenAllDayHours()
if errs := temp.Validate(); len(errs) > 0 {
return fmt.Errorf("validation error for day %d (entry %d): %v", param.DayOfWeek, i, errs)
}
}
if err := repository.NewWorkingHourRepo(s.repo.DB()).UpdateWorkingHours(ctx, inbox.ID, params); err != nil {
return err
}
return s.loadInboxWorkingHours(ctx, inbox)
}
func (s *InboxService) loadInboxWorkingHours(ctx context.Context, inbox *model.Inbox) error {
hours, err := repository.NewWorkingHourRepo(s.repo.DB()).FindByInbox(ctx, inbox.ID)
if err != nil {
return err
}
inbox.WorkingHours = hours
return nil
}
// Delete soft-deletes an inbox.
func (s *InboxService) Delete(ctx context.Context, id uint) error {
return s.repo.Delete(ctx, id)
@@ -0,0 +1,53 @@
package service
import (
"context"
"testing"
"github.com/stretchr/testify/require"
"gorm.io/driver/sqlite"
"gorm.io/gorm"
"gorm.io/gorm/logger"
"github.com/gochat/gochat/internal/model"
"github.com/gochat/gochat/internal/repository"
)
func TestWorkingHourService_IsOutOfOffice(t *testing.T) {
db, err := gorm.Open(sqlite.Open("file:working_hour_service?mode=memory&cache=shared"), &gorm.Config{
Logger: logger.Default.LogMode(logger.Silent),
})
require.NoError(t, err)
t.Cleanup(func() {
sqlDB, dbErr := db.DB()
if dbErr == nil {
_ = sqlDB.Close()
}
})
require.NoError(t, db.AutoMigrate(&model.Account{}, &model.Inbox{}, &model.WorkingHour{}))
account := &model.Account{Name: "Working Hours", Locale: "en", Active: true}
require.NoError(t, db.Create(account).Error)
inbox := &model.Inbox{AccountID: account.ID, Name: "Website", ChannelType: "web_widget", ChannelID: 1, Enabled: true, WorkingHoursEnabled: true, Timezone: "UTC"}
require.NoError(t, db.Create(inbox).Error)
whRepo := repository.NewWorkingHourRepo(db)
require.NoError(t, whRepo.CreateDefaultWorkingHours(context.Background(), inbox.ID, account.ID))
svc := NewWorkingHourService(whRepo, repository.NewInboxRepo(db), repository.NewAccountRepo(db))
closedWeek := make([]repository.WorkingHourUpdateParam, 0, 7)
for day := 0; day < 7; day++ {
closedWeek = append(closedWeek, repository.WorkingHourUpdateParam{DayOfWeek: day, ClosedAllDay: true})
}
_, err = svc.UpdateWeeklySchedule(context.Background(), inbox.ID, closedWeek)
require.NoError(t, err)
outOfOffice, err := svc.IsOutOfOffice(context.Background(), inbox.ID)
require.NoError(t, err)
require.True(t, outOfOffice)
require.NoError(t, db.Model(inbox).Update("working_hours_enabled", false).Error)
outOfOffice, err = svc.IsOutOfOffice(context.Background(), inbox.ID)
require.NoError(t, err)
require.False(t, outOfOffice)
}