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

245 lines
8.9 KiB
Go

package service
import (
"context"
"fmt"
"github.com/gochat/gochat/internal/model"
"github.com/gochat/gochat/internal/repository"
applogger "github.com/gochat/gochat/pkg/logger"
pkgvalidator "github.com/gochat/gochat/pkg/validator"
)
// SlaPolicyService implements business logic for SLA Policy operations.
// Reference: Chatwoot app/controllers/api/v1/sla_policies_controller.rb
type SlaPolicyService struct {
slaPolicyRepo *repository.SlaPolicyRepo
appliedSlaRepo *repository.AppliedSlaRepo
slaEventRepo *repository.SlaEventRepo
slaPolicyInboxRepo *repository.SlaPolicyInboxRepo
}
// NewSlaPolicyService creates a new SlaPolicy service.
func NewSlaPolicyService(
slaPolicyRepo *repository.SlaPolicyRepo,
appliedSlaRepo *repository.AppliedSlaRepo,
slaEventRepo *repository.SlaEventRepo,
slaPolicyInboxRepo *repository.SlaPolicyInboxRepo,
) *SlaPolicyService {
return &SlaPolicyService{
slaPolicyRepo: slaPolicyRepo,
appliedSlaRepo: appliedSlaRepo,
slaEventRepo: slaEventRepo,
slaPolicyInboxRepo: slaPolicyInboxRepo,
}
}
// CreateSlaPolicyRequest is the DTO for creating a SLA policy.
// Reference: Chatwoot sla_policies_controller.rb#permitted_params — nested under "sla_policy" key
// Fields: name, description, first_response_time_threshold, next_response_time_threshold,
// resolution_time_threshold, only_during_business_hours
type CreateSlaPolicyRequest struct {
Name string `json:"name" validate:"required"`
Description string `json:"description,omitempty"`
FirstResponseTimeThreshold int `json:"first_response_time_threshold" validate:"min=0"`
NextResponseTimeThreshold int `json:"next_response_time_threshold" validate:"min=0"`
ResolutionTimeThreshold int `json:"resolution_time_threshold" validate:"min=0"`
OnlyDuringBusinessHours bool `json:"only_during_business_hours"`
}
// SlaPolicyCreateWrapper wraps CreateSlaPolicyRequest under the "sla_policy" JSON key.
// Chatwoot requires params.require(:sla_policy), so the request body must be:
// { "sla_policy": { "name": "...", "first_response_time_threshold": 60, ... } }
type SlaPolicyCreateWrapper struct {
SlaPolicy CreateSlaPolicyRequest `json:"sla_policy"`
}
// UpdateSlaPolicyRequest is the DTO for updating a SLA policy.
// Reference: same permitted_params as Create, all optional for update
type UpdateSlaPolicyRequest struct {
Name string `json:"name,omitempty"`
Description string `json:"description,omitempty"`
FirstResponseTimeThreshold *int `json:"first_response_time_threshold,omitempty" validate:"omitempty,min=0"`
NextResponseTimeThreshold *int `json:"next_response_time_threshold,omitempty" validate:"omitempty,min=0"`
ResolutionTimeThreshold *int `json:"resolution_time_threshold,omitempty" validate:"omitempty,min=0"`
OnlyDuringBusinessHours *bool `json:"only_during_business_hours,omitempty"`
}
// SlaPolicyUpdateWrapper wraps UpdateSlaPolicyRequest under the "sla_policy" JSON key.
type SlaPolicyUpdateWrapper struct {
SlaPolicy UpdateSlaPolicyRequest `json:"sla_policy"`
}
// Create creates a new SLA policy for an account.
func (s *SlaPolicyService) Create(ctx context.Context, accountID uint, req *CreateSlaPolicyRequest) (*model.SlaPolicy, error) {
if err := pkgvalidator.ValidateStruct(req); err != nil {
return nil, fmt.Errorf("validation error: %w", err)
}
policy := &model.SlaPolicy{
AccountID: accountID,
Name: req.Name,
Description: req.Description,
FirstResponseTimeThreshold: req.FirstResponseTimeThreshold,
NextResponseTimeThreshold: req.NextResponseTimeThreshold,
ResolutionTimeThreshold: req.ResolutionTimeThreshold,
OnlyDuringBusinessHours: req.OnlyDuringBusinessHours,
}
if err := s.slaPolicyRepo.Create(ctx, policy); err != nil {
return nil, fmt.Errorf("create sla policy: %w", err)
}
applogger.L().Infof("SLA policy %d created for account %d", policy.ID, accountID)
return policy, nil
}
// Get retrieves a SLA policy by ID.
func (s *SlaPolicyService) Get(ctx context.Context, accountID, policyID uint) (*model.SlaPolicy, error) {
policy, err := s.slaPolicyRepo.FindByID(ctx, policyID)
if err != nil {
return nil, fmt.Errorf("sla policy not found: %w", err)
}
if policy.AccountID != accountID {
return nil, fmt.Errorf("sla policy %d does not belong to account %d", policyID, accountID)
}
return policy, nil
}
// List retrieves all SLA policies for an account.
func (s *SlaPolicyService) List(ctx context.Context, accountID uint) ([]model.SlaPolicy, error) {
return s.slaPolicyRepo.FindByAccount(ctx, accountID)
}
// Update updates a SLA policy.
func (s *SlaPolicyService) Update(ctx context.Context, accountID, policyID uint, req *UpdateSlaPolicyRequest) (*model.SlaPolicy, error) {
policy, err := s.Get(ctx, accountID, policyID)
if err != nil {
return nil, err
}
if req.Name != "" {
policy.Name = req.Name
}
if req.Description != "" {
policy.Description = req.Description
}
if req.FirstResponseTimeThreshold != nil {
policy.FirstResponseTimeThreshold = *req.FirstResponseTimeThreshold
}
if req.NextResponseTimeThreshold != nil {
policy.NextResponseTimeThreshold = *req.NextResponseTimeThreshold
}
if req.ResolutionTimeThreshold != nil {
policy.ResolutionTimeThreshold = *req.ResolutionTimeThreshold
}
if req.OnlyDuringBusinessHours != nil {
policy.OnlyDuringBusinessHours = *req.OnlyDuringBusinessHours
}
if err := s.slaPolicyRepo.Update(ctx, policy); err != nil {
return nil, fmt.Errorf("update sla policy: %w", err)
}
applogger.L().Infof("SLA policy %d updated for account %d", policyID, accountID)
return policy, nil
}
// Delete deletes a SLA policy (and its inbox associations).
func (s *SlaPolicyService) Delete(ctx context.Context, accountID, policyID uint) error {
policy, err := s.Get(ctx, accountID, policyID)
if err != nil {
return err
}
// Remove all inbox associations first
if err := s.slaPolicyInboxRepo.DeleteBySlaPolicy(ctx, policy.ID); err != nil {
return fmt.Errorf("delete inbox associations: %w", err)
}
if err := s.slaPolicyRepo.Delete(ctx, policy.ID); err != nil {
return fmt.Errorf("delete sla policy: %w", err)
}
applogger.L().Infof("SLA policy %d deleted for account %d", policyID, accountID)
return nil
}
// GetAppliedSlaMetrics retrieves SLA metrics for a conversation.
// Returns the applied SLA and its events.
func (s *SlaPolicyService) GetAppliedSlaMetrics(ctx context.Context, accountID, conversationID uint) (*model.AppliedSLA, []model.SlaEvent, error) {
applied, err := s.appliedSlaRepo.FindByConversation(ctx, conversationID)
if err != nil {
return nil, nil, fmt.Errorf("applied sla not found: %w", err)
}
if applied.AccountID != accountID {
return nil, nil, fmt.Errorf("applied sla does not belong to account %d", accountID)
}
events, err := s.slaEventRepo.FindByAppliedSla(ctx, applied.ID)
if err != nil {
return nil, nil, fmt.Errorf("sla events not found: %w", err)
}
return applied, events, nil
}
// GetAppliedSlaDownload retrieves all applied SLAs for an account (for export/download).
func (s *SlaPolicyService) GetAppliedSlaDownload(ctx context.Context, accountID uint) ([]model.AppliedSLA, error) {
return s.appliedSlaRepo.FindByAccount(ctx, accountID)
}
// ListInboxes retrieves all inboxes associated with a SLA policy.
func (s *SlaPolicyService) ListInboxes(ctx context.Context, accountID, policyID uint) ([]model.SlaPolicyInbox, error) {
policy, err := s.Get(ctx, accountID, policyID)
if err != nil {
return nil, err
}
return s.slaPolicyInboxRepo.FindBySlaPolicy(ctx, policy.ID)
}
// AddInbox associates an inbox with a SLA policy.
func (s *SlaPolicyService) AddInbox(ctx context.Context, accountID, policyID, inboxID uint) (*model.SlaPolicyInbox, error) {
policy, err := s.Get(ctx, accountID, policyID)
if err != nil {
return nil, err
}
spi := &model.SlaPolicyInbox{
SlaPolicyID: policy.ID,
InboxID: inboxID,
AccountID: accountID,
}
if err := s.slaPolicyInboxRepo.Create(ctx, spi); err != nil {
return nil, fmt.Errorf("add inbox to sla policy: %w", err)
}
applogger.L().Infof("Inbox %d added to SLA policy %d", inboxID, policyID)
return spi, nil
}
// RemoveInbox removes an inbox association from a SLA policy.
func (s *SlaPolicyService) RemoveInbox(ctx context.Context, accountID, policyID, inboxID uint) error {
policy, err := s.Get(ctx, accountID, policyID)
if err != nil {
return err
}
spInboxes, err := s.slaPolicyInboxRepo.FindBySlaPolicy(ctx, policy.ID)
if err != nil {
return fmt.Errorf("find inbox associations: %w", err)
}
for _, spi := range spInboxes {
if spi.InboxID == inboxID {
if err := s.slaPolicyInboxRepo.Delete(ctx, spi.ID); err != nil {
return fmt.Errorf("remove inbox from sla policy: %w", err)
}
applogger.L().Infof("Inbox %d removed from SLA policy %d", inboxID, policyID)
return nil
}
}
return fmt.Errorf("inbox %d not associated with sla policy %d", inboxID, policyID)
}