Files
gochat/backend/internal/repository/working_hour_repo.go
T
rogee aeddedf2a3 Reorganize repo: backend/, deploy/, docs/ layout + AGENTS.md
Restructure the monorepo into clear top-level directories:
- backend/: Go module root (cmd, internal, pkg, configs, migrations,
  docs/swagger, scripts, tests, go.mod, Makefile, .air.toml)
- deploy/: Docker (Dockerfile, docker-compose*), quickstart, fluentd
- docs/: project documentation + reports/ (moved from repo root)
- AGENTS.md: new AI coding-agent guide at repo root

Update all references to the new layout:
- Dockerfile: COPY backend/go.mod, COPY backend/ (context = repo root)
- docker-compose files: context ../.., dockerfile deploy/docker/Dockerfile,
  env_file ../../.env, volume mounts ../../backend:/app
- deploy/quickstart/compose.yaml: dockerfile deploy/docker/Dockerfile
- CI: working-directory: backend for go commands, file deploy/docker/Dockerfile,
  coverage path backend/coverage.out, health_check backend/scripts/
- backend/Makefile: docker target uses -f ../deploy/docker/Dockerfile ../
- README: architecture tree, quickstart, config paths updated

Move root stray scripts (rename_models.*, run_m11_tests.sh, verify_build.sh,
gorm_bool_main.go) to backend/scripts/legacy/. All moves via git mv to
preserve history. Build, vet, SQLite tests, and docker compose config verified.
2026-07-07 14:44:12 +08:00

161 lines
5.2 KiB
Go

package repository
import (
"context"
"encoding/json"
"strconv"
"github.com/gochat/gochat/internal/model"
"gorm.io/gorm"
)
// WorkingHourRepo implements GORM repository for WorkingHour.
type WorkingHourRepo struct {
db *gorm.DB
}
func NewWorkingHourRepo(db *gorm.DB) *WorkingHourRepo {
return &WorkingHourRepo{db: db}
}
// FindByInbox returns all 7 working hours for an inbox, ordered by day_of_week.
func (r *WorkingHourRepo) FindByInbox(ctx context.Context, inboxID uint) ([]model.WorkingHour, error) {
var hours []model.WorkingHour
err := r.db.WithContext(ctx).Where("inbox_id = ?", inboxID).Order("day_of_week ASC").Find(&hours).Error
return hours, err
}
// FindByInboxAndDay returns the working hour for a specific inbox and day_of_week.
func (r *WorkingHourRepo) FindByInboxAndDay(ctx context.Context, inboxID uint, dayOfWeek int) (*model.WorkingHour, error) {
var wh model.WorkingHour
err := r.db.WithContext(ctx).Where("inbox_id = ? AND day_of_week = ?", inboxID, dayOfWeek).First(&wh).Error
if err != nil {
return nil, err
}
return &wh, nil
}
// CreateDefaultWorkingHours creates 7 default working hour records for a new inbox.
// Reference: Chatwoot out_of_offisable.rb — after_create :create_default_working_hours
// - Sunday(0): closed_all_day
// - Mon-Fri(1-5): 9:00-17:00
// - Saturday(6): closed_all_day
func (r *WorkingHourRepo) CreateDefaultWorkingHours(ctx context.Context, inboxID, accountID uint) error {
nine := 9
zero := 0
seventeen := 17
defaults := []model.WorkingHour{
{InboxID: inboxID, AccountID: accountID, DayOfWeek: 0, ClosedAllDay: true, OpenAllDay: false},
{InboxID: inboxID, AccountID: accountID, DayOfWeek: 1, OpenHour: &nine, OpenMinutes: &zero, CloseHour: &seventeen, CloseMinutes: &zero, OpenAllDay: false},
{InboxID: inboxID, AccountID: accountID, DayOfWeek: 2, OpenHour: &nine, OpenMinutes: &zero, CloseHour: &seventeen, CloseMinutes: &zero, OpenAllDay: false},
{InboxID: inboxID, AccountID: accountID, DayOfWeek: 3, OpenHour: &nine, OpenMinutes: &zero, CloseHour: &seventeen, CloseMinutes: &zero, OpenAllDay: false},
{InboxID: inboxID, AccountID: accountID, DayOfWeek: 4, OpenHour: &nine, OpenMinutes: &zero, CloseHour: &seventeen, CloseMinutes: &zero, OpenAllDay: false},
{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},
}
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.
// Reference: Chatwoot out_of_offisable.rb — update_working_hours(params)
// - For each param entry, find the existing working_hour by day_of_week and update its fields.
// - Runs in a transaction.
func (r *WorkingHourRepo) UpdateWorkingHours(ctx context.Context, inboxID uint, params []WorkingHourUpdateParam) error {
return r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
for _, p := range params {
var wh model.WorkingHour
if err := tx.Where("inbox_id = ? AND day_of_week = ?", inboxID, p.DayOfWeek).First(&wh).Error; err != nil {
return err
}
wh.ClosedAllDay = p.ClosedAllDay
wh.OpenAllDay = p.OpenAllDay
wh.OpenHour = p.OpenHour
wh.OpenMinutes = p.OpenMinutes
wh.CloseHour = p.CloseHour
wh.CloseMinutes = p.CloseMinutes
wh.EnsureOpenAllDayHours()
if err := tx.Save(&wh).Error; err != nil {
return err
}
}
return nil
})
}
// WorkingHourUpdateParam represents a single day's update in a bulk update request.
// Reference: Chatwoot OFFISABLE_ATTRS
type WorkingHourUpdateParam 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"`
}
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
}
}