feat(inboxes): persist chatwoot working hours
This commit is contained in:
@@ -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 {
|
||||
|
||||
@@ -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
@@ -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" }
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
Reference in New Issue
Block a user