Files
gochat/backend/internal/repository/conversationassignee/gate.go
T
Rogeeandrogee 6c78820a1f H-338: close H-335 release blockers (#59)
* H-16: align takeover with channel AI workflow (#2)

* feat(conversations): complete manual AI takeover

* fix(conversations): align AI takeover flow with channel AI

* fix(conversations): close takeover review gaps

---------

Co-authored-by: Rogee <rogee@ipao.vip>

* feat(shangwutong): sync customer names back to channel (#3)

Co-authored-by: Rogee <rogee@ipao.vip>

* fix(shangwutong): close contact sync review gaps (#4)

Co-authored-by: Rogee <rogee@ipao.vip>

* H-28: harden Shangwutong CID sync (#5)

* fix(shangwutong): close contact sync review gaps

* fix(shangwutong): harden CID sync boundaries

---------

Co-authored-by: Rogee <rogee@ipao.vip>

* fix(conversations): sync AI takeover exit in realtime (#6)

Co-authored-by: Rogee <rogee@ipao.vip>

* test(shangwutong): cover CID rename reliability (#7)

Co-authored-by: Rogee <rogee@ipao.vip>

* H-43: fix WEB Captain takeover E2E flow (#8)

* test(shangwutong): cover CID rename reliability

* H-43: fix WEB Captain takeover flow

* H-48: preserve compatible provider model

* H-49: make Captain takeover atomic

* H-50: prevent duplicate widget initialization

---------

Co-authored-by: Rogee <rogee@ipao.vip>

* H-55: make Captain bindings atomic (#9)

Co-authored-by: Rogee <rogee@ipao.vip>

* H-60: harden Captain migration rollback and concurrency

* chore(agent): baseline — uncommitted work from the local directory

* H-335: add safe Captain skills and user deactivation

* H-338: close auth and Captain review blockers

* H-338: close assignment and session races

* H-338: close assignment and websocket invalidation gaps

* H-338: enforce assignment write invariants

---------

Co-authored-by: Rogee <rogee@ipao.vip>
2026-08-20 10:21:19 +08:00

53 lines
1.8 KiB
Go

package conversationassignee
import (
"context"
"errors"
"github.com/gochat/gochat/internal/model"
"gorm.io/gorm"
)
var ErrNotEligible = errors.New("assignee is not an active agent or administrator in this account")
func Update(ctx context.Context, db *gorm.DB, accountID, conversationID, assigneeID uint, extraUpdates map[string]any) error {
updates := make(map[string]any, len(extraUpdates)+1)
for key, value := range extraUpdates {
updates[key] = value
}
updates["assignee_id"] = nil
query := db.WithContext(ctx).Model(&model.Conversation{}).
Where("conversations.id = ? AND conversations.account_id = ?", conversationID, accountID)
if assigneeID != 0 {
updates["assignee_id"] = assigneeID
query = query.Where("EXISTS (?)", eligible(db, assigneeID))
}
result := query.Updates(updates)
if result.Error != nil {
return result.Error
}
if assigneeID != 0 && result.RowsAffected != 1 {
return ErrNotEligible
}
return nil
}
func AutoAssign(ctx context.Context, db *gorm.DB, accountID, inboxID, conversationID, assigneeID uint) (bool, error) {
result := db.WithContext(ctx).Model(&model.Conversation{}).
Where("conversations.id = ? AND conversations.account_id = ?", conversationID, accountID).
Where("conversations.inbox_id = ? AND conversations.status = ? AND conversations.assignee_id IS NULL", inboxID, model.ConversationStatusOpen).
Where("EXISTS (?)", eligible(db, assigneeID)).
Update("assignee_id", assigneeID)
return result.RowsAffected == 1, result.Error
}
func eligible(db *gorm.DB, assigneeID uint) *gorm.DB {
return db.Table("account_users").Select("1").
Joins("JOIN users ON users.id = account_users.user_id").
Where("account_users.account_id = conversations.account_id").
Where("account_users.user_id = ?", assigneeID).
Where("account_users.role IN ? AND users.active = ?", []string{"agent", "administrator"}, true)
}