H-337: fix Web Channel availability and realtime delivery (#60)

* H-337: fix Web Channel availability and realtime delivery

* fix(widget): preserve realtime sender and activity contracts

* fix(widget): keep realtime sender payloads consistent

* fix(widget): make public message persistence atomic

* fix(inbox): keep availability projection out of schema

---------

Co-authored-by: Rogee <rogee@ipao.vip>
This commit is contained in:
Rogee
2026-08-20 14:30:05 +08:00
committed by GitHub
co-authored by rogee
parent 55b56ee172
commit 3d9817c9f5
17 changed files with 769 additions and 247 deletions
@@ -41,10 +41,29 @@ func (r *InboxMemberRepo) FindByInboxAndUser(ctx context.Context, inboxID, userI
// FindByInbox retrieves all members (agents) assigned to an inbox.
func (r *InboxMemberRepo) FindByInbox(ctx context.Context, inboxID uint) ([]model.InboxMember, error) {
var ims []model.InboxMember
err := r.withAgentPreloads(r.db.WithContext(ctx)).Where("inbox_id = ?", inboxID).
Order("id ASC").Find(&ims).Error
return ims, err
type inboxMemberWithAvailability struct {
model.InboxMember `gorm:"embedded"`
AccountAvailability string `gorm:"column:account_availability"`
}
var rows []inboxMemberWithAvailability
err := r.withAgentPreloads(r.db.WithContext(ctx)).
Model(&model.InboxMember{}).
Select("inbox_members.*, COALESCE(account_users.availability, 'offline') AS account_availability").
Joins("LEFT JOIN inboxes ON inboxes.id = inbox_members.inbox_id").
Joins("LEFT JOIN account_users ON account_users.account_id = inboxes.account_id AND account_users.user_id = inbox_members.user_id AND account_users.deleted_at IS NULL").
Where("inbox_members.inbox_id = ?", inboxID).
Order("inbox_members.id ASC").Find(&rows).Error
if err != nil {
return nil, err
}
ims := make([]model.InboxMember, len(rows))
for i := range rows {
ims[i] = rows[i].InboxMember
ims[i].AccountAvailability = rows[i].AccountAvailability
}
return ims, nil
}
// FindByUser retrieves all inboxes a user is assigned to.
@@ -103,11 +103,12 @@ func TestInboxMemberRepo_FindByInbox(t *testing.T) {
db := setupTestDB(t, &model.InboxMember{})
repo := NewInboxMemberRepo(db)
_, user1, inbox := createInboxMemberPrereqs(t, db)
account, user1, inbox := createInboxMemberPrereqs(t, db)
// Create a second user
user2 := &model.User{Name: "IMTestUser2", Email: "im2@example.com", Provider: "email"}
require.NoError(t, db.Create(user2).Error)
require.NoError(t, db.Create(&model.AccountUser{AccountID: account.ID, UserID: user1.ID, Role: "agent", Availability: "online"}).Error)
im1 := &model.InboxMember{InboxID: inbox.ID, UserID: user1.ID, Role: "agent"}
require.NoError(t, db.Create(im1).Error)
@@ -121,6 +122,8 @@ func TestInboxMemberRepo_FindByInbox(t *testing.T) {
// Verify User is preloaded
assert.Equal(t, "IMTestUser", members[0].User.Name)
assert.Equal(t, "IMTestUser2", members[1].User.Name)
assert.Equal(t, "online", members[0].AccountAvailability)
assert.Equal(t, "offline", members[1].AccountAvailability)
// Verify ordering by ID ASC
assert.Equal(t, im1.ID, members[0].ID)
@@ -240,4 +243,4 @@ func TestInboxMemberRepo_DeleteByInbox(t *testing.T) {
members, err := repo.FindByInbox(context.Background(), inbox.ID)
assert.NoError(t, err)
assert.Len(t, members, 0)
}
}