feat(channels): align channel route inbox payloads
This commit is contained in:
@@ -0,0 +1,29 @@
|
||||
package v1
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
"gorm.io/gorm"
|
||||
|
||||
"github.com/gochat/gochat/internal/model"
|
||||
)
|
||||
|
||||
func createTestChannelInbox(t *testing.T, db *gorm.DB, accountID, channelID uint, name, channelType string, channelConfig map[string]any) *model.Inbox {
|
||||
t.Helper()
|
||||
|
||||
configJSON, err := json.Marshal(channelConfig)
|
||||
require.NoError(t, err)
|
||||
|
||||
inbox := &model.Inbox{
|
||||
AccountID: accountID,
|
||||
Name: name,
|
||||
ChannelType: channelType,
|
||||
ChannelID: channelID,
|
||||
Enabled: true,
|
||||
ChannelConfig: string(configJSON),
|
||||
}
|
||||
require.NoError(t, db.Create(inbox).Error)
|
||||
return inbox
|
||||
}
|
||||
@@ -12,7 +12,6 @@ package v1
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
@@ -71,10 +70,8 @@ type CreateEmailChannelRequest struct {
|
||||
// Create adds a new Email channel and creates the associated inbox.
|
||||
// POST /api/v1/accounts/:id/channels/email_channel
|
||||
func (h *EmailChannelHandler) Create(c *gin.Context) {
|
||||
accountIDStr := c.Param("id")
|
||||
accountID, err := strconv.ParseUint(accountIDStr, 10, 64)
|
||||
if err != nil {
|
||||
applogger.L().Errorf("Invalid account_id: %v", err)
|
||||
accountID := parseAccountIDParam(c)
|
||||
if accountID == 0 {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid account_id"})
|
||||
return
|
||||
}
|
||||
@@ -108,7 +105,7 @@ func (h *EmailChannelHandler) Create(c *gin.Context) {
|
||||
SMTPLogin: req.SMTPLogin,
|
||||
SMTPPassword: req.SMTPPassword,
|
||||
SMTPSSLMode: req.SMTPSSLMode,
|
||||
AccountID: uint(accountID),
|
||||
AccountID: accountID,
|
||||
}
|
||||
|
||||
if err := h.emailChannelSvc.Create(ctx, ch); err != nil {
|
||||
@@ -131,7 +128,8 @@ func (h *EmailChannelHandler) Create(c *gin.Context) {
|
||||
Enabled: true,
|
||||
}
|
||||
|
||||
inbox, err := h.inboxSvc.Create(ctx, uint(accountID), inboxReq)
|
||||
inboxReq.Channel = emailChannelConfig(req)
|
||||
inbox, err := h.inboxSvc.Create(ctx, accountID, inboxReq)
|
||||
if err != nil {
|
||||
applogger.L().Errorf("Failed to create inbox for Email channel: %v", err)
|
||||
// Rollback channel creation
|
||||
@@ -147,32 +145,32 @@ func (h *EmailChannelHandler) Create(c *gin.Context) {
|
||||
if err := h.emailChannelSvc.Update(ctx, ch); err != nil {
|
||||
applogger.L().Errorf("Failed to update Email channel with inbox_id: %v", err)
|
||||
}
|
||||
inbox, err = h.inboxSvc.BindChannel(ctx, accountID, inbox.ID, ch.ID, emailChannelConfig(req))
|
||||
if err != nil {
|
||||
applogger.L().Warnf("Failed to bind Email inbox channel config: %v", err)
|
||||
}
|
||||
|
||||
c.JSON(http.StatusCreated, gin.H{
|
||||
"channel": ch,
|
||||
"inbox": inbox,
|
||||
})
|
||||
c.JSON(http.StatusOK, serializeInbox(inbox))
|
||||
}
|
||||
|
||||
// Get retrieves an Email channel by ID.
|
||||
// GET /api/v1/accounts/:id/channels/email_channel/:em_id
|
||||
func (h *EmailChannelHandler) Get(c *gin.Context) {
|
||||
emIDStr := c.Param("em_id")
|
||||
emID, err := strconv.ParseUint(emIDStr, 10, 64)
|
||||
emID, err := parseUintParam(c, "em_id")
|
||||
if err != nil {
|
||||
applogger.L().Errorf("Invalid em_id: %v", err)
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid em_id"})
|
||||
return
|
||||
}
|
||||
|
||||
ch, err := h.emailChannelSvc.GetByID(c.Request.Context(), uint(emID))
|
||||
ch, err := h.emailChannelSvc.GetByID(c.Request.Context(), emID)
|
||||
if err != nil {
|
||||
applogger.L().Errorf("Failed to get Email channel: %v", err)
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "Email channel not found"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"channel": ch})
|
||||
c.JSON(http.StatusOK, h.serializeInboxForEmailChannel(c, ch))
|
||||
}
|
||||
|
||||
// UpdateEmailChannelRequest is the DTO for updating an Email channel.
|
||||
@@ -198,15 +196,13 @@ type UpdateEmailChannelRequest struct {
|
||||
// Update updates an Email channel configuration.
|
||||
// PATCH /api/v1/accounts/:id/channels/email_channel/:em_id
|
||||
func (h *EmailChannelHandler) Update(c *gin.Context) {
|
||||
accountIDStr := c.Param("id")
|
||||
accountID, err := strconv.ParseUint(accountIDStr, 10, 64)
|
||||
if err != nil {
|
||||
accountID := parseAccountIDParam(c)
|
||||
if accountID == 0 {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid account_id"})
|
||||
return
|
||||
}
|
||||
|
||||
emIDStr := c.Param("em_id")
|
||||
emID, err := strconv.ParseUint(emIDStr, 10, 64)
|
||||
emID, err := parseUintParam(c, "em_id")
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid em_id"})
|
||||
return
|
||||
@@ -214,7 +210,7 @@ func (h *EmailChannelHandler) Update(c *gin.Context) {
|
||||
|
||||
ctx := c.Request.Context()
|
||||
|
||||
ch, err := h.emailChannelSvc.GetByID(ctx, uint(emID))
|
||||
ch, err := h.emailChannelSvc.GetByID(ctx, emID)
|
||||
if err != nil {
|
||||
applogger.L().Errorf("Failed to get Email channel for update: %v", err)
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "Email channel not found"})
|
||||
@@ -222,7 +218,7 @@ func (h *EmailChannelHandler) Update(c *gin.Context) {
|
||||
}
|
||||
|
||||
// Verify account ownership
|
||||
if ch.AccountID != uint(accountID) {
|
||||
if ch.AccountID != accountID {
|
||||
c.JSON(http.StatusForbidden, gin.H{"error": "Email channel does not belong to this account"})
|
||||
return
|
||||
}
|
||||
@@ -292,29 +288,30 @@ func (h *EmailChannelHandler) Update(c *gin.Context) {
|
||||
updateReq := service.UpdateInboxRequest{
|
||||
Name: *req.InboxName,
|
||||
}
|
||||
if _, inboxErr := h.inboxSvc.Update(ctx, uint(accountID), ch.InboxID, updateReq); inboxErr != nil {
|
||||
if _, inboxErr := h.inboxSvc.Update(ctx, accountID, ch.InboxID, updateReq); inboxErr != nil {
|
||||
applogger.L().Warnf("Failed to update Email inbox name: %v", inboxErr)
|
||||
}
|
||||
}
|
||||
inbox, bindErr := h.inboxSvc.BindChannel(ctx, accountID, ch.InboxID, ch.ID, emailUpdateChannelConfig(req, ch))
|
||||
if bindErr != nil {
|
||||
applogger.L().Warnf("Failed to update Email inbox channel config: %v", bindErr)
|
||||
c.JSON(http.StatusOK, h.serializeInboxForEmailChannel(c, ch))
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"channel": ch,
|
||||
"message": "Email channel updated successfully",
|
||||
})
|
||||
c.JSON(http.StatusOK, serializeInbox(inbox))
|
||||
}
|
||||
|
||||
// Delete removes an Email channel and its associated inbox.
|
||||
// DELETE /api/v1/accounts/:id/channels/email_channel/:em_id
|
||||
func (h *EmailChannelHandler) Delete(c *gin.Context) {
|
||||
accountIDStr := c.Param("id")
|
||||
accountID, err := strconv.ParseUint(accountIDStr, 10, 64)
|
||||
if err != nil {
|
||||
accountID := parseAccountIDParam(c)
|
||||
if accountID == 0 {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid account_id"})
|
||||
return
|
||||
}
|
||||
|
||||
emIDStr := c.Param("em_id")
|
||||
emID, err := strconv.ParseUint(emIDStr, 10, 64)
|
||||
emID, err := parseUintParam(c, "em_id")
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid em_id"})
|
||||
return
|
||||
@@ -322,20 +319,20 @@ func (h *EmailChannelHandler) Delete(c *gin.Context) {
|
||||
|
||||
ctx := c.Request.Context()
|
||||
|
||||
ch, err := h.emailChannelSvc.GetByID(ctx, uint(emID))
|
||||
ch, err := h.emailChannelSvc.GetByID(ctx, emID)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "Email channel not found"})
|
||||
return
|
||||
}
|
||||
|
||||
// Verify account ownership
|
||||
if ch.AccountID != uint(accountID) {
|
||||
if ch.AccountID != accountID {
|
||||
c.JSON(http.StatusForbidden, gin.H{"error": "Email channel does not belong to this account"})
|
||||
return
|
||||
}
|
||||
|
||||
// Delete the channel record
|
||||
if err := h.emailChannelSvc.Delete(ctx, uint(emID)); err != nil {
|
||||
if err := h.emailChannelSvc.Delete(ctx, emID); err != nil {
|
||||
applogger.L().Errorf("Failed to delete Email channel: %v", err)
|
||||
c.JSON(http.StatusUnprocessableEntity, gin.H{"error": "failed to delete Email channel"})
|
||||
return
|
||||
@@ -343,34 +340,88 @@ func (h *EmailChannelHandler) Delete(c *gin.Context) {
|
||||
|
||||
// Delete the associated inbox
|
||||
if ch.InboxID > 0 {
|
||||
if delErr := h.inboxSvc.DeleteByAccount(ctx, uint(accountID), ch.InboxID); delErr != nil {
|
||||
if delErr := h.inboxSvc.DeleteByAccount(ctx, accountID, ch.InboxID); delErr != nil {
|
||||
applogger.L().Warnf("Failed to delete inbox for Email channel: %v", delErr)
|
||||
}
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"message": "Email channel deleted successfully"})
|
||||
c.Status(http.StatusOK)
|
||||
}
|
||||
|
||||
// List lists all Email channels for an account.
|
||||
// GET /api/v1/accounts/:id/channels/email_channel
|
||||
func (h *EmailChannelHandler) List(c *gin.Context) {
|
||||
accountIDStr := c.Param("id")
|
||||
accountID, err := strconv.ParseUint(accountIDStr, 10, 64)
|
||||
if err != nil {
|
||||
accountID := parseAccountIDParam(c)
|
||||
if accountID == 0 {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid account_id"})
|
||||
return
|
||||
}
|
||||
|
||||
ctx := c.Request.Context()
|
||||
|
||||
channels, err := h.emailChannelSvc.ListByAccount(ctx, uint(accountID))
|
||||
channels, err := h.emailChannelSvc.ListByAccount(ctx, accountID)
|
||||
if err != nil {
|
||||
applogger.L().Errorf("Failed to list Email channels: %v", err)
|
||||
c.JSON(http.StatusUnprocessableEntity, gin.H{"error": "failed to list Email channels"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"channels": channels})
|
||||
payload := make([]map[string]any, 0, len(channels))
|
||||
for i := range channels {
|
||||
payload = append(payload, h.serializeInboxForEmailChannel(c, &channels[i]))
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"payload": payload})
|
||||
}
|
||||
|
||||
func (h *EmailChannelHandler) serializeInboxForEmailChannel(c *gin.Context, ch *channelmodel.ChannelEmail) map[string]any {
|
||||
if ch != nil && ch.InboxID > 0 {
|
||||
if inbox, err := h.inboxSvc.GetByAccountAndID(c.Request.Context(), ch.AccountID, ch.InboxID); err == nil {
|
||||
return serializeInbox(inbox)
|
||||
}
|
||||
}
|
||||
return gin.H{"id": ch.ID, "account_id": ch.AccountID, "inbox_id": ch.InboxID, "email": ch.Email}
|
||||
}
|
||||
|
||||
func emailChannelConfig(req CreateEmailChannelRequest) map[string]any {
|
||||
return map[string]any{
|
||||
"email": req.Email,
|
||||
"mailbox_name": req.MailboxName,
|
||||
"domain": extractDomain(req.Email),
|
||||
"imap_enabled": req.IMAPEnabled,
|
||||
"imap_address": req.IMAPAddress,
|
||||
"imap_port": req.IMAPPort,
|
||||
"imap_login": req.IMAPLogin,
|
||||
"imap_password": req.IMAPPassword,
|
||||
"imap_ssl_mode": req.IMAPSSLMode,
|
||||
"imap_folder": req.IMAPFolder,
|
||||
"smtp_enabled": req.SMTPEnabled,
|
||||
"smtp_address": req.SMTPAddress,
|
||||
"smtp_port": req.SMTPPort,
|
||||
"smtp_login": req.SMTPLogin,
|
||||
"smtp_password": req.SMTPPassword,
|
||||
"smtp_ssl_mode": req.SMTPSSLMode,
|
||||
}
|
||||
}
|
||||
|
||||
func emailUpdateChannelConfig(_ UpdateEmailChannelRequest, ch *channelmodel.ChannelEmail) map[string]any {
|
||||
return map[string]any{
|
||||
"email": ch.Email,
|
||||
"mailbox_name": ch.MailboxName,
|
||||
"domain": ch.Domain,
|
||||
"imap_enabled": ch.IMAPEnabled,
|
||||
"imap_address": ch.IMAPAddress,
|
||||
"imap_port": ch.IMAPPort,
|
||||
"imap_login": ch.IMAPLogin,
|
||||
"imap_password": ch.IMAPPassword,
|
||||
"imap_ssl_mode": ch.IMAPSSLMode,
|
||||
"imap_folder": ch.IMAPFolder,
|
||||
"smtp_enabled": ch.SMTPEnabled,
|
||||
"smtp_address": ch.SMTPAddress,
|
||||
"smtp_port": ch.SMTPPort,
|
||||
"smtp_login": ch.SMTPLogin,
|
||||
"smtp_password": ch.SMTPPassword,
|
||||
"smtp_ssl_mode": ch.SMTPSSLMode,
|
||||
}
|
||||
}
|
||||
|
||||
// extractDomain extracts the domain portion from an email address.
|
||||
@@ -380,4 +431,4 @@ func extractDomain(email string) string {
|
||||
return parts[1]
|
||||
}
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,8 +14,8 @@ import (
|
||||
"gorm.io/driver/sqlite"
|
||||
"gorm.io/gorm"
|
||||
|
||||
channelmodel "github.com/gochat/gochat/internal/model/channel"
|
||||
"github.com/gochat/gochat/internal/model"
|
||||
channelmodel "github.com/gochat/gochat/internal/model/channel"
|
||||
|
||||
"github.com/gochat/gochat/internal/channel/whatsapp"
|
||||
"github.com/gochat/gochat/internal/repository"
|
||||
@@ -112,13 +112,16 @@ func TestEmailChannel_Create_Success(t *testing.T) {
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
router.ServeHTTP(w, req)
|
||||
|
||||
assert.Equal(t, http.StatusCreated, w.Code)
|
||||
assert.Equal(t, http.StatusOK, w.Code)
|
||||
|
||||
var resp map[string]interface{}
|
||||
require.NoError(t, json.Unmarshal(w.Body.Bytes(), &resp))
|
||||
channelData, ok := resp["channel"].(map[string]interface{})
|
||||
require.True(t, ok)
|
||||
assert.Equal(t, "support@example.com", channelData["email"])
|
||||
assert.Equal(t, "Support Email", resp["name"])
|
||||
assert.Equal(t, "Channel::Email", resp["channel_type"])
|
||||
assert.Equal(t, "support@example.com", resp["email"])
|
||||
assert.Equal(t, "imap.example.com", resp["imap_address"])
|
||||
require.NotContains(t, resp, "channel")
|
||||
require.NotContains(t, resp, "inbox")
|
||||
}
|
||||
|
||||
func TestEmailChannel_Create_InvalidAccountID(t *testing.T) {
|
||||
@@ -161,14 +164,19 @@ func TestEmailChannel_Get_Success(t *testing.T) {
|
||||
router := setupEmailTestRouter(handler)
|
||||
accountID := emailAccountIDUint(db)
|
||||
|
||||
// Seed a channel
|
||||
// Seed a channel and its associated inbox.
|
||||
ch := &channelmodel.ChannelEmail{
|
||||
Email: "get@example.com",
|
||||
AccountID: accountID,
|
||||
InboxID: 300,
|
||||
IMAPEnabled: true,
|
||||
}
|
||||
require.NoError(t, db.Create(ch).Error)
|
||||
inbox := createTestChannelInbox(t, db, accountID, ch.ID, "Get Email", "email", map[string]any{
|
||||
"email": "get@example.com",
|
||||
"imap_enabled": true,
|
||||
})
|
||||
ch.InboxID = inbox.ID
|
||||
require.NoError(t, db.Save(ch).Error)
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest(http.MethodGet, "/api/v1/accounts/"+strconv.FormatUint(uint64(accountID), 10)+"/inboxes/1/email_channels/"+strconv.FormatUint(uint64(ch.ID), 10), nil)
|
||||
@@ -177,9 +185,10 @@ func TestEmailChannel_Get_Success(t *testing.T) {
|
||||
assert.Equal(t, http.StatusOK, w.Code)
|
||||
var resp map[string]interface{}
|
||||
require.NoError(t, json.Unmarshal(w.Body.Bytes(), &resp))
|
||||
channelData, ok := resp["channel"].(map[string]interface{})
|
||||
require.True(t, ok)
|
||||
assert.Equal(t, "get@example.com", channelData["email"])
|
||||
assert.Equal(t, "Get Email", resp["name"])
|
||||
assert.Equal(t, "Channel::Email", resp["channel_type"])
|
||||
assert.Equal(t, "get@example.com", resp["email"])
|
||||
require.NotContains(t, resp, "channel")
|
||||
}
|
||||
|
||||
func TestEmailChannel_Get_InvalidID(t *testing.T) {
|
||||
@@ -216,14 +225,19 @@ func TestEmailChannel_Update_Success(t *testing.T) {
|
||||
router := setupEmailTestRouter(handler)
|
||||
accountID := emailAccountIDUint(db)
|
||||
|
||||
// Seed a channel
|
||||
// Seed a channel and its associated inbox.
|
||||
ch := &channelmodel.ChannelEmail{
|
||||
Email: "update@example.com",
|
||||
AccountID: accountID,
|
||||
InboxID: 301,
|
||||
IMAPEnabled: true,
|
||||
}
|
||||
require.NoError(t, db.Create(ch).Error)
|
||||
inbox := createTestChannelInbox(t, db, accountID, ch.ID, "Update Email", "email", map[string]any{
|
||||
"email": "update@example.com",
|
||||
"imap_enabled": true,
|
||||
})
|
||||
ch.InboxID = inbox.ID
|
||||
require.NoError(t, db.Save(ch).Error)
|
||||
|
||||
newEmail := "updated@example.com"
|
||||
body := UpdateEmailChannelRequest{
|
||||
@@ -239,9 +253,9 @@ func TestEmailChannel_Update_Success(t *testing.T) {
|
||||
assert.Equal(t, http.StatusOK, w.Code)
|
||||
var resp map[string]interface{}
|
||||
require.NoError(t, json.Unmarshal(w.Body.Bytes(), &resp))
|
||||
channelData, ok := resp["channel"].(map[string]interface{})
|
||||
require.True(t, ok)
|
||||
assert.Equal(t, "updated@example.com", channelData["email"])
|
||||
assert.Equal(t, "Channel::Email", resp["channel_type"])
|
||||
assert.Equal(t, "updated@example.com", resp["email"])
|
||||
require.NotContains(t, resp, "channel")
|
||||
}
|
||||
|
||||
func TestEmailChannel_Update_WrongAccount(t *testing.T) {
|
||||
@@ -251,9 +265,9 @@ func TestEmailChannel_Update_WrongAccount(t *testing.T) {
|
||||
|
||||
// Seed a channel owned by a different account
|
||||
ch := &channelmodel.ChannelEmail{
|
||||
Email: "other@example.com",
|
||||
AccountID: accountID + 999,
|
||||
InboxID: 302,
|
||||
Email: "other@example.com",
|
||||
AccountID: accountID + 999,
|
||||
InboxID: 302,
|
||||
}
|
||||
require.NoError(t, db.Create(ch).Error)
|
||||
|
||||
@@ -293,9 +307,9 @@ func TestEmailChannel_Delete_Success(t *testing.T) {
|
||||
|
||||
// Seed a channel
|
||||
ch := &channelmodel.ChannelEmail{
|
||||
Email: "delete@example.com",
|
||||
AccountID: accountID,
|
||||
InboxID: 303,
|
||||
Email: "delete@example.com",
|
||||
AccountID: accountID,
|
||||
InboxID: 303,
|
||||
}
|
||||
require.NoError(t, db.Create(ch).Error)
|
||||
|
||||
@@ -318,9 +332,9 @@ func TestEmailChannel_Delete_WrongAccount(t *testing.T) {
|
||||
|
||||
// Seed a channel owned by a different account
|
||||
ch := &channelmodel.ChannelEmail{
|
||||
Email: "otherdel@example.com",
|
||||
AccountID: accountID + 999,
|
||||
InboxID: 304,
|
||||
Email: "otherdel@example.com",
|
||||
AccountID: accountID + 999,
|
||||
InboxID: 304,
|
||||
}
|
||||
require.NoError(t, db.Create(ch).Error)
|
||||
|
||||
@@ -350,14 +364,18 @@ func TestEmailChannel_List_Success(t *testing.T) {
|
||||
router := setupEmailTestRouter(handler)
|
||||
accountID := emailAccountIDUint(db)
|
||||
|
||||
// Seed multiple channels
|
||||
// Seed multiple channels and their associated inboxes.
|
||||
for i := 0; i < 3; i++ {
|
||||
ch := &channelmodel.ChannelEmail{
|
||||
Email: "list" + strconv.Itoa(i) + "@example.com",
|
||||
AccountID: accountID,
|
||||
InboxID: uint(i + 400), // unique inbox IDs
|
||||
Email: "list" + strconv.Itoa(i) + "@example.com",
|
||||
AccountID: accountID,
|
||||
}
|
||||
require.NoError(t, db.Create(ch).Error)
|
||||
inbox := createTestChannelInbox(t, db, accountID, ch.ID, "Email List "+strconv.Itoa(i), "email", map[string]any{
|
||||
"email": ch.Email,
|
||||
})
|
||||
ch.InboxID = inbox.ID
|
||||
require.NoError(t, db.Save(ch).Error)
|
||||
}
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
@@ -367,7 +385,7 @@ func TestEmailChannel_List_Success(t *testing.T) {
|
||||
assert.Equal(t, http.StatusOK, w.Code)
|
||||
var resp map[string]interface{}
|
||||
require.NoError(t, json.Unmarshal(w.Body.Bytes(), &resp))
|
||||
channels, ok := resp["channels"].([]interface{})
|
||||
channels, ok := resp["payload"].([]interface{})
|
||||
require.True(t, ok)
|
||||
assert.Len(t, channels, 3)
|
||||
}
|
||||
@@ -395,7 +413,7 @@ func TestEmailChannel_List_Empty(t *testing.T) {
|
||||
assert.Equal(t, http.StatusOK, w.Code)
|
||||
var resp map[string]interface{}
|
||||
require.NoError(t, json.Unmarshal(w.Body.Bytes(), &resp))
|
||||
channels, ok := resp["channels"].([]interface{})
|
||||
channels, ok := resp["payload"].([]interface{})
|
||||
require.True(t, ok)
|
||||
assert.Len(t, channels, 0)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,7 +14,6 @@ package v1
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
@@ -56,19 +55,17 @@ func NewLINEChannelHandler(
|
||||
|
||||
// CreateLINEChannelRequest is the DTO for creating a LINE channel.
|
||||
type CreateLINEChannelRequest struct {
|
||||
ChannelID string `json:"channel_id" validate:"required"`
|
||||
Name string `json:"name" validate:"required"`
|
||||
ChannelID string `json:"channel_id" validate:"required"`
|
||||
Name string `json:"name" validate:"required"`
|
||||
ChannelAccessToken string `json:"channel_access_token" validate:"required"`
|
||||
ChannelSecret string `json:"channel_secret" validate:"required"`
|
||||
ChannelSecret string `json:"channel_secret" validate:"required"`
|
||||
}
|
||||
|
||||
// Create adds a new LINE channel and creates the associated inbox.
|
||||
// POST /api/v1/accounts/:id/channels/line_channel
|
||||
func (h *LINEChannelHandler) Create(c *gin.Context) {
|
||||
accountIDStr := c.Param("id")
|
||||
accountID, err := strconv.ParseUint(accountIDStr, 10, 64)
|
||||
if err != nil {
|
||||
applogger.L().Errorf("Invalid account_id: %v", err)
|
||||
accountID := parseAccountIDParam(c)
|
||||
if accountID == 0 {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid account_id"})
|
||||
return
|
||||
}
|
||||
@@ -85,7 +82,7 @@ func (h *LINEChannelHandler) Create(c *gin.Context) {
|
||||
}
|
||||
|
||||
ch := &channelmodel.ChannelLINE{
|
||||
AccountID: uint(accountID),
|
||||
AccountID: accountID,
|
||||
ChannelID: req.ChannelID,
|
||||
Name: req.Name,
|
||||
}
|
||||
@@ -101,8 +98,9 @@ func (h *LINEChannelHandler) Create(c *gin.Context) {
|
||||
Name: req.Name,
|
||||
ChannelType: "line",
|
||||
Enabled: true,
|
||||
Channel: lineCreateChannelConfig(req),
|
||||
}
|
||||
inbox, err := h.inboxSvc.Create(c.Request.Context(), uint(accountID), inboxReq)
|
||||
inbox, err := h.inboxSvc.Create(c.Request.Context(), accountID, inboxReq)
|
||||
if err != nil {
|
||||
applogger.L().Errorf("Failed to create inbox for LINE channel: %v", err)
|
||||
c.JSON(http.StatusUnprocessableEntity, gin.H{"error": "failed to create inbox"})
|
||||
@@ -113,11 +111,12 @@ inbox, err := h.inboxSvc.Create(c.Request.Context(), uint(accountID), inboxReq)
|
||||
if err := h.lineChannelSvc.Update(c.Request.Context(), ch); err != nil {
|
||||
applogger.L().Errorf("Failed to update LINE channel with inbox_id: %v", err)
|
||||
}
|
||||
inbox, err = h.inboxSvc.BindChannel(c.Request.Context(), accountID, inbox.ID, ch.ID, inboxReq.Channel)
|
||||
if err != nil {
|
||||
applogger.L().Warnf("Failed to bind LINE inbox channel config: %v", err)
|
||||
}
|
||||
|
||||
c.JSON(http.StatusCreated, gin.H{
|
||||
"channel": ch,
|
||||
"inbox": inbox,
|
||||
})
|
||||
c.JSON(http.StatusOK, serializeInbox(inbox))
|
||||
}
|
||||
|
||||
// === Get ===
|
||||
@@ -125,58 +124,56 @@ inbox, err := h.inboxSvc.Create(c.Request.Context(), uint(accountID), inboxReq)
|
||||
// Get retrieves a LINE channel by ID.
|
||||
// GET /api/v1/accounts/:id/channels/line_channel/:line_id
|
||||
func (h *LINEChannelHandler) Get(c *gin.Context) {
|
||||
lineIDStr := c.Param("line_id")
|
||||
lineID, err := strconv.ParseUint(lineIDStr, 10, 64)
|
||||
lineID, err := parseUintParam(c, "line_id")
|
||||
if err != nil {
|
||||
applogger.L().Errorf("Invalid line_id: %v", err)
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid line_id"})
|
||||
return
|
||||
}
|
||||
|
||||
ch, err := h.lineChannelSvc.GetByID(c.Request.Context(), uint(lineID))
|
||||
ch, err := h.lineChannelSvc.GetByID(c.Request.Context(), lineID)
|
||||
if err != nil {
|
||||
applogger.L().Errorf("Failed to get LINE channel: %v", err)
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "LINE channel not found"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"channel": ch})
|
||||
c.JSON(http.StatusOK, h.serializeInboxForLINEChannel(c, ch))
|
||||
}
|
||||
|
||||
// === Update ===
|
||||
|
||||
// UpdateLINEChannelRequest is the DTO for updating a LINE channel.
|
||||
type UpdateLINEChannelRequest struct {
|
||||
Name string `json:"name,omitempty"`
|
||||
Name string `json:"name,omitempty"`
|
||||
ChannelAccessToken string `json:"channel_access_token,omitempty"`
|
||||
ChannelSecret string `json:"channel_secret,omitempty"`
|
||||
ChannelSecret string `json:"channel_secret,omitempty"`
|
||||
}
|
||||
|
||||
// Update modifies an existing LINE channel.
|
||||
// PATCH /api/v1/accounts/:id/channels/line_channel/:line_id
|
||||
func (h *LINEChannelHandler) Update(c *gin.Context) {
|
||||
accountID, err := strconv.ParseUint(c.Param("id"), 10, 64)
|
||||
if err != nil {
|
||||
accountID := parseAccountIDParam(c)
|
||||
if accountID == 0 {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid account_id"})
|
||||
return
|
||||
}
|
||||
|
||||
lineIDStr := c.Param("line_id")
|
||||
lineID, err := strconv.ParseUint(lineIDStr, 10, 64)
|
||||
lineID, err := parseUintParam(c, "line_id")
|
||||
if err != nil {
|
||||
applogger.L().Errorf("Invalid line_id: %v", err)
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid line_id"})
|
||||
return
|
||||
}
|
||||
|
||||
ch, err := h.lineChannelSvc.GetByID(c.Request.Context(), uint(lineID))
|
||||
ch, err := h.lineChannelSvc.GetByID(c.Request.Context(), lineID)
|
||||
if err != nil {
|
||||
applogger.L().Errorf("Failed to get LINE channel: %v", err)
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "LINE channel not found"})
|
||||
return
|
||||
}
|
||||
|
||||
if ch.AccountID != uint(accountID) {
|
||||
if ch.AccountID != accountID {
|
||||
c.JSON(http.StatusForbidden, gin.H{"error": "channel does not belong to this account"})
|
||||
return
|
||||
}
|
||||
@@ -197,8 +194,19 @@ func (h *LINEChannelHandler) Update(c *gin.Context) {
|
||||
c.JSON(http.StatusUnprocessableEntity, gin.H{"error": "failed to update LINE channel"})
|
||||
return
|
||||
}
|
||||
if req.Name != "" {
|
||||
if _, inboxErr := h.inboxSvc.Update(c.Request.Context(), accountID, ch.InboxID, service.UpdateInboxRequest{Name: req.Name}); inboxErr != nil {
|
||||
applogger.L().Warnf("Failed to update LINE inbox name: %v", inboxErr)
|
||||
}
|
||||
}
|
||||
inbox, bindErr := h.inboxSvc.BindChannel(c.Request.Context(), accountID, ch.InboxID, ch.ID, lineUpdateChannelConfig(req, ch))
|
||||
if bindErr != nil {
|
||||
applogger.L().Warnf("Failed to update LINE inbox channel config: %v", bindErr)
|
||||
c.JSON(http.StatusOK, h.serializeInboxForLINEChannel(c, ch))
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"channel": ch})
|
||||
c.JSON(http.StatusOK, serializeInbox(inbox))
|
||||
}
|
||||
|
||||
// === Delete ===
|
||||
@@ -206,28 +214,27 @@ func (h *LINEChannelHandler) Update(c *gin.Context) {
|
||||
// Delete removes a LINE channel and its associated inbox.
|
||||
// DELETE /api/v1/accounts/:id/channels/line_channel/:line_id
|
||||
func (h *LINEChannelHandler) Delete(c *gin.Context) {
|
||||
accountID, err := strconv.ParseUint(c.Param("id"), 10, 64)
|
||||
if err != nil {
|
||||
accountID := parseAccountIDParam(c)
|
||||
if accountID == 0 {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid account_id"})
|
||||
return
|
||||
}
|
||||
|
||||
lineIDStr := c.Param("line_id")
|
||||
lineID, err := strconv.ParseUint(lineIDStr, 10, 64)
|
||||
lineID, err := parseUintParam(c, "line_id")
|
||||
if err != nil {
|
||||
applogger.L().Errorf("Invalid line_id: %v", err)
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid line_id"})
|
||||
return
|
||||
}
|
||||
|
||||
ch, err := h.lineChannelSvc.GetByID(c.Request.Context(), uint(lineID))
|
||||
ch, err := h.lineChannelSvc.GetByID(c.Request.Context(), lineID)
|
||||
if err != nil {
|
||||
applogger.L().Errorf("Failed to get LINE channel: %v", err)
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "LINE channel not found"})
|
||||
return
|
||||
}
|
||||
|
||||
if ch.AccountID != uint(accountID) {
|
||||
if ch.AccountID != accountID {
|
||||
c.JSON(http.StatusForbidden, gin.H{"error": "channel does not belong to this account"})
|
||||
return
|
||||
}
|
||||
@@ -239,13 +246,13 @@ func (h *LINEChannelHandler) Delete(c *gin.Context) {
|
||||
}
|
||||
}
|
||||
|
||||
if err := h.lineChannelSvc.Delete(c.Request.Context(), uint(lineID)); err != nil {
|
||||
if err := h.lineChannelSvc.Delete(c.Request.Context(), lineID); err != nil {
|
||||
applogger.L().Errorf("Failed to delete LINE channel: %v", err)
|
||||
c.JSON(http.StatusUnprocessableEntity, gin.H{"error": "failed to delete LINE channel"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"message": "LINE channel deleted"})
|
||||
c.Status(http.StatusOK)
|
||||
}
|
||||
|
||||
// === List ===
|
||||
@@ -253,10 +260,8 @@ func (h *LINEChannelHandler) Delete(c *gin.Context) {
|
||||
// List retrieves all LINE channels for an account.
|
||||
// GET /api/v1/accounts/:id/channels/line_channel
|
||||
func (h *LINEChannelHandler) List(c *gin.Context) {
|
||||
accountIDStr := c.Param("id")
|
||||
accountID, err := strconv.ParseUint(accountIDStr, 10, 64)
|
||||
if err != nil {
|
||||
applogger.L().Errorf("Invalid account_id: %v", err)
|
||||
accountID := parseAccountIDParam(c)
|
||||
if accountID == 0 {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid account_id"})
|
||||
return
|
||||
}
|
||||
@@ -268,5 +273,51 @@ func (h *LINEChannelHandler) List(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"channels": channels, "account_id": uint(accountID)})
|
||||
}
|
||||
payload := make([]map[string]any, 0, len(channels))
|
||||
for i := range channels {
|
||||
if channels[i].AccountID == accountID {
|
||||
payload = append(payload, h.serializeInboxForLINEChannel(c, &channels[i]))
|
||||
}
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"payload": payload})
|
||||
}
|
||||
|
||||
func (h *LINEChannelHandler) serializeInboxForLINEChannel(c *gin.Context, ch *channelmodel.ChannelLINE) map[string]any {
|
||||
if ch != nil && ch.InboxID > 0 {
|
||||
if inbox, err := h.inboxSvc.GetByAccountAndID(c.Request.Context(), ch.AccountID, ch.InboxID); err == nil {
|
||||
return serializeInbox(inbox)
|
||||
}
|
||||
}
|
||||
return gin.H{"id": ch.ID, "account_id": ch.AccountID, "inbox_id": ch.InboxID, "channel_id": ch.ChannelID, "name": ch.Name}
|
||||
}
|
||||
|
||||
func lineChannelConfigFromModel(ch *channelmodel.ChannelLINE) map[string]any {
|
||||
return map[string]any{
|
||||
"line_channel_id": ch.ChannelID,
|
||||
"channel_id": ch.ChannelID,
|
||||
}
|
||||
}
|
||||
|
||||
func lineCreateChannelConfig(req CreateLINEChannelRequest) map[string]any {
|
||||
return map[string]any{
|
||||
"line_channel_id": req.ChannelID,
|
||||
"line_channel_token": req.ChannelAccessToken,
|
||||
"line_channel_secret": req.ChannelSecret,
|
||||
"channel_id": req.ChannelID,
|
||||
"channel_access_token": req.ChannelAccessToken,
|
||||
"channel_secret": req.ChannelSecret,
|
||||
}
|
||||
}
|
||||
|
||||
func lineUpdateChannelConfig(req UpdateLINEChannelRequest, ch *channelmodel.ChannelLINE) map[string]any {
|
||||
config := lineChannelConfigFromModel(ch)
|
||||
if req.ChannelAccessToken != "" {
|
||||
config["line_channel_token"] = req.ChannelAccessToken
|
||||
config["channel_access_token"] = req.ChannelAccessToken
|
||||
}
|
||||
if req.ChannelSecret != "" {
|
||||
config["line_channel_secret"] = req.ChannelSecret
|
||||
config["channel_secret"] = req.ChannelSecret
|
||||
}
|
||||
return config
|
||||
}
|
||||
|
||||
@@ -14,8 +14,8 @@ import (
|
||||
"gorm.io/driver/sqlite"
|
||||
"gorm.io/gorm"
|
||||
|
||||
channelmodel "github.com/gochat/gochat/internal/model/channel"
|
||||
"github.com/gochat/gochat/internal/model"
|
||||
channelmodel "github.com/gochat/gochat/internal/model/channel"
|
||||
|
||||
"github.com/gochat/gochat/internal/channel/whatsapp"
|
||||
"github.com/gochat/gochat/internal/repository"
|
||||
@@ -108,13 +108,17 @@ func TestLINEChannel_Create_Success(t *testing.T) {
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
router.ServeHTTP(w, req)
|
||||
|
||||
assert.Equal(t, http.StatusCreated, w.Code)
|
||||
assert.Equal(t, http.StatusOK, w.Code)
|
||||
|
||||
var resp map[string]interface{}
|
||||
require.NoError(t, json.Unmarshal(w.Body.Bytes(), &resp))
|
||||
channelData, ok := resp["channel"].(map[string]interface{})
|
||||
require.True(t, ok)
|
||||
assert.Equal(t, "line_chan_123", channelData["channel_id"])
|
||||
assert.Equal(t, "LINE Official", resp["name"])
|
||||
assert.Equal(t, "Channel::Line", resp["channel_type"])
|
||||
assert.Equal(t, "line_chan_123", resp["line_channel_id"])
|
||||
assert.Equal(t, "secret_value", resp["line_channel_secret"])
|
||||
assert.Equal(t, "access_token_secret", resp["line_channel_token"])
|
||||
require.NotContains(t, resp, "channel")
|
||||
require.NotContains(t, resp, "inbox")
|
||||
}
|
||||
|
||||
func TestLINEChannel_Create_InvalidAccountID(t *testing.T) {
|
||||
@@ -160,14 +164,21 @@ func TestLINEChannel_Get_Success(t *testing.T) {
|
||||
router := setupLINETestRouter(handler)
|
||||
accountID := lineAccountIDUint(db)
|
||||
|
||||
// Seed a channel
|
||||
// Seed a channel and its associated inbox.
|
||||
ch := &channelmodel.ChannelLINE{
|
||||
ChannelID: "line_get_123",
|
||||
Name: "LINE Get Test",
|
||||
AccountID: accountID,
|
||||
InboxID: 700,
|
||||
}
|
||||
require.NoError(t, db.Create(ch).Error)
|
||||
inbox := createTestChannelInbox(t, db, accountID, ch.ID, "LINE Get Test", "line", map[string]any{
|
||||
"line_channel_id": "line_get_123",
|
||||
"line_channel_secret": "line-secret",
|
||||
"line_channel_token": "line-token",
|
||||
"channel_secret": "line-secret",
|
||||
})
|
||||
ch.InboxID = inbox.ID
|
||||
require.NoError(t, db.Save(ch).Error)
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest(http.MethodGet, "/api/v1/accounts/"+strconv.FormatUint(uint64(accountID), 10)+"/inboxes/1/line_channel/"+strconv.FormatUint(uint64(ch.ID), 10), nil)
|
||||
@@ -176,9 +187,10 @@ func TestLINEChannel_Get_Success(t *testing.T) {
|
||||
assert.Equal(t, http.StatusOK, w.Code)
|
||||
var resp map[string]interface{}
|
||||
require.NoError(t, json.Unmarshal(w.Body.Bytes(), &resp))
|
||||
channelData, ok := resp["channel"].(map[string]interface{})
|
||||
require.True(t, ok)
|
||||
assert.Equal(t, "line_get_123", channelData["channel_id"])
|
||||
assert.Equal(t, "LINE Get Test", resp["name"])
|
||||
assert.Equal(t, "Channel::Line", resp["channel_type"])
|
||||
assert.Equal(t, "line_get_123", resp["line_channel_id"])
|
||||
require.NotContains(t, resp, "channel")
|
||||
}
|
||||
|
||||
func TestLINEChannel_Get_InvalidID(t *testing.T) {
|
||||
@@ -215,14 +227,21 @@ func TestLINEChannel_Update_Success(t *testing.T) {
|
||||
router := setupLINETestRouter(handler)
|
||||
accountID := lineAccountIDUint(db)
|
||||
|
||||
// Seed a channel
|
||||
// Seed a channel and its associated inbox.
|
||||
ch := &channelmodel.ChannelLINE{
|
||||
ChannelID: "line_upd_123",
|
||||
Name: "LINE Update Test",
|
||||
AccountID: accountID,
|
||||
InboxID: 701,
|
||||
}
|
||||
require.NoError(t, db.Create(ch).Error)
|
||||
inbox := createTestChannelInbox(t, db, accountID, ch.ID, "LINE Update Test", "line", map[string]any{
|
||||
"line_channel_id": "line_upd_123",
|
||||
"line_channel_secret": "old-secret",
|
||||
"line_channel_token": "old-token",
|
||||
"channel_secret": "old-secret",
|
||||
})
|
||||
ch.InboxID = inbox.ID
|
||||
require.NoError(t, db.Save(ch).Error)
|
||||
|
||||
body := UpdateLINEChannelRequest{
|
||||
Name: "Updated LINE",
|
||||
@@ -237,9 +256,10 @@ func TestLINEChannel_Update_Success(t *testing.T) {
|
||||
assert.Equal(t, http.StatusOK, w.Code)
|
||||
var resp map[string]interface{}
|
||||
require.NoError(t, json.Unmarshal(w.Body.Bytes(), &resp))
|
||||
channelData, ok := resp["channel"].(map[string]interface{})
|
||||
require.True(t, ok)
|
||||
assert.Equal(t, "Updated LINE", channelData["name"])
|
||||
assert.Equal(t, "Updated LINE", resp["name"])
|
||||
assert.Equal(t, "line_upd_123", resp["line_channel_id"])
|
||||
assert.Equal(t, "old-secret", resp["line_channel_secret"])
|
||||
require.NotContains(t, resp, "channel")
|
||||
}
|
||||
|
||||
func TestLINEChannel_Update_WrongAccount(t *testing.T) {
|
||||
@@ -346,15 +366,22 @@ func TestLINEChannel_List_Success(t *testing.T) {
|
||||
router := setupLINETestRouter(handler)
|
||||
accountID := lineAccountIDUint(db)
|
||||
|
||||
// Seed multiple channels
|
||||
// Seed multiple channels and their associated inboxes.
|
||||
for i := 0; i < 3; i++ {
|
||||
ch := &channelmodel.ChannelLINE{
|
||||
ChannelID: "line_list_" + strconv.Itoa(i),
|
||||
Name: "LINE List " + strconv.Itoa(i),
|
||||
AccountID: accountID,
|
||||
InboxID: uint(i + 800), // unique inbox IDs
|
||||
}
|
||||
require.NoError(t, db.Create(ch).Error)
|
||||
inbox := createTestChannelInbox(t, db, accountID, ch.ID, ch.Name, "line", map[string]any{
|
||||
"line_channel_id": ch.ChannelID,
|
||||
"line_channel_secret": "line-secret-" + strconv.Itoa(i),
|
||||
"line_channel_token": "line-token-" + strconv.Itoa(i),
|
||||
"channel_secret": "line-secret-" + strconv.Itoa(i),
|
||||
})
|
||||
ch.InboxID = inbox.ID
|
||||
require.NoError(t, db.Save(ch).Error)
|
||||
}
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
@@ -364,7 +391,7 @@ func TestLINEChannel_List_Success(t *testing.T) {
|
||||
assert.Equal(t, http.StatusOK, w.Code)
|
||||
var resp map[string]interface{}
|
||||
require.NoError(t, json.Unmarshal(w.Body.Bytes(), &resp))
|
||||
channels, ok := resp["channels"].([]interface{})
|
||||
channels, ok := resp["payload"].([]interface{})
|
||||
require.True(t, ok)
|
||||
assert.Len(t, channels, 3)
|
||||
}
|
||||
@@ -392,7 +419,7 @@ func TestLINEChannel_List_Empty(t *testing.T) {
|
||||
assert.Equal(t, http.StatusOK, w.Code)
|
||||
var resp map[string]interface{}
|
||||
require.NoError(t, json.Unmarshal(w.Body.Bytes(), &resp))
|
||||
channels, ok := resp["channels"].([]interface{})
|
||||
channels, ok := resp["payload"].([]interface{})
|
||||
require.True(t, ok)
|
||||
assert.Len(t, channels, 0)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,7 +16,6 @@ import (
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
@@ -64,10 +63,8 @@ type CreateTwilioSMSChannelRequest struct {
|
||||
// Create adds a new Twilio SMS channel and creates the associated inbox.
|
||||
// POST /api/v1/accounts/:id/channels/twilio_channel
|
||||
func (h *TwilioChannelHandler) Create(c *gin.Context) {
|
||||
accountIDStr := c.Param("id")
|
||||
accountID, err := strconv.ParseUint(accountIDStr, 10, 64)
|
||||
if err != nil {
|
||||
applogger.L().Errorf("Invalid account_id: %v", err)
|
||||
accountID := parseAccountIDParam(c)
|
||||
if accountID == 0 {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid account_id"})
|
||||
return
|
||||
}
|
||||
@@ -92,7 +89,7 @@ func (h *TwilioChannelHandler) Create(c *gin.Context) {
|
||||
AccountSID: req.AccountSID,
|
||||
PhoneNumber: phoneNumber,
|
||||
MessagingServiceSID: req.MessagingServiceSID,
|
||||
AccountID: uint(accountID),
|
||||
AccountID: accountID,
|
||||
}
|
||||
|
||||
if err := h.twChannelSvc.Create(ctx, ch); err != nil {
|
||||
@@ -123,7 +120,7 @@ func (h *TwilioChannelHandler) Create(c *gin.Context) {
|
||||
},
|
||||
}
|
||||
|
||||
inbox, err := h.inboxSvc.Create(ctx, uint(accountID), inboxReq)
|
||||
inbox, err := h.inboxSvc.Create(ctx, accountID, inboxReq)
|
||||
if err != nil {
|
||||
applogger.L().Errorf("Failed to create inbox for Twilio SMS channel: %v", err)
|
||||
// Rollback channel creation
|
||||
@@ -139,6 +136,10 @@ func (h *TwilioChannelHandler) Create(c *gin.Context) {
|
||||
if err := h.twChannelSvc.Update(ctx, ch); err != nil {
|
||||
applogger.L().Errorf("Failed to update Twilio SMS channel with inbox_id: %v", err)
|
||||
}
|
||||
inbox, err = h.inboxSvc.BindChannel(ctx, accountID, inbox.ID, ch.ID, inboxReq.Channel)
|
||||
if err != nil {
|
||||
applogger.L().Warnf("Failed to bind Twilio inbox channel config: %v", err)
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, serializeInbox(inbox))
|
||||
}
|
||||
@@ -183,22 +184,21 @@ func bindTwilioCreateRequest(c *gin.Context) (CreateTwilioSMSChannelRequest, err
|
||||
// Get retrieves a Twilio SMS channel by ID.
|
||||
// GET /api/v1/accounts/:id/channels/twilio_channel/:tw_id
|
||||
func (h *TwilioChannelHandler) Get(c *gin.Context) {
|
||||
twIDStr := c.Param("tw_id")
|
||||
twID, err := strconv.ParseUint(twIDStr, 10, 64)
|
||||
twID, err := parseUintParam(c, "tw_id")
|
||||
if err != nil {
|
||||
applogger.L().Errorf("Invalid tw_id: %v", err)
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid tw_id"})
|
||||
return
|
||||
}
|
||||
|
||||
ch, err := h.twChannelSvc.GetByID(c.Request.Context(), uint(twID))
|
||||
ch, err := h.twChannelSvc.GetByID(c.Request.Context(), twID)
|
||||
if err != nil {
|
||||
applogger.L().Errorf("Failed to get Twilio SMS channel: %v", err)
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "Twilio SMS channel not found"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"channel": ch})
|
||||
c.JSON(http.StatusOK, h.serializeInboxForTwilioChannel(c, ch))
|
||||
}
|
||||
|
||||
// UpdateTwilioSMSChannelRequest is the DTO for updating a Twilio SMS channel.
|
||||
@@ -211,15 +211,13 @@ type UpdateTwilioSMSChannelRequest struct {
|
||||
// Update updates a Twilio SMS channel configuration.
|
||||
// PATCH /api/v1/accounts/:id/channels/twilio_channel/:tw_id
|
||||
func (h *TwilioChannelHandler) Update(c *gin.Context) {
|
||||
accountIDStr := c.Param("id")
|
||||
accountID, err := strconv.ParseUint(accountIDStr, 10, 64)
|
||||
if err != nil {
|
||||
accountID := parseAccountIDParam(c)
|
||||
if accountID == 0 {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid account_id"})
|
||||
return
|
||||
}
|
||||
|
||||
twIDStr := c.Param("tw_id")
|
||||
twID, err := strconv.ParseUint(twIDStr, 10, 64)
|
||||
twID, err := parseUintParam(c, "tw_id")
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid tw_id"})
|
||||
return
|
||||
@@ -227,7 +225,7 @@ func (h *TwilioChannelHandler) Update(c *gin.Context) {
|
||||
|
||||
ctx := c.Request.Context()
|
||||
|
||||
ch, err := h.twChannelSvc.GetByID(ctx, uint(twID))
|
||||
ch, err := h.twChannelSvc.GetByID(ctx, twID)
|
||||
if err != nil {
|
||||
applogger.L().Errorf("Failed to get Twilio SMS channel for update: %v", err)
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "Twilio SMS channel not found"})
|
||||
@@ -235,7 +233,7 @@ func (h *TwilioChannelHandler) Update(c *gin.Context) {
|
||||
}
|
||||
|
||||
// Verify account ownership
|
||||
if ch.AccountID != uint(accountID) {
|
||||
if ch.AccountID != accountID {
|
||||
c.JSON(http.StatusForbidden, gin.H{"error": "Twilio SMS channel does not belong to this account"})
|
||||
return
|
||||
}
|
||||
@@ -264,34 +262,30 @@ func (h *TwilioChannelHandler) Update(c *gin.Context) {
|
||||
updateReq := service.UpdateInboxRequest{
|
||||
Name: *req.InboxName,
|
||||
}
|
||||
if _, inboxErr := h.inboxSvc.Update(ctx, uint(accountID), ch.InboxID, updateReq); inboxErr != nil {
|
||||
if _, inboxErr := h.inboxSvc.Update(ctx, accountID, ch.InboxID, updateReq); inboxErr != nil {
|
||||
applogger.L().Warnf("Failed to update Twilio SMS inbox name: %v", inboxErr)
|
||||
}
|
||||
}
|
||||
inbox, bindErr := h.inboxSvc.BindChannel(ctx, accountID, ch.InboxID, ch.ID, twilioChannelConfigFromModel(ch))
|
||||
if bindErr != nil {
|
||||
applogger.L().Warnf("Failed to update Twilio inbox channel config: %v", bindErr)
|
||||
c.JSON(http.StatusOK, h.serializeInboxForTwilioChannel(c, ch))
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"id": ch.ID,
|
||||
"account_id": ch.AccountID,
|
||||
"inbox_id": ch.InboxID,
|
||||
"account_sid": ch.AccountSID,
|
||||
"phone_number": ch.PhoneNumber,
|
||||
"messaging_service_sid": ch.MessagingServiceSID,
|
||||
"message": "Twilio SMS channel updated successfully",
|
||||
})
|
||||
c.JSON(http.StatusOK, serializeInbox(inbox))
|
||||
}
|
||||
|
||||
// Delete removes a Twilio SMS channel and its associated inbox.
|
||||
// DELETE /api/v1/accounts/:id/channels/twilio_channel/:tw_id
|
||||
func (h *TwilioChannelHandler) Delete(c *gin.Context) {
|
||||
accountIDStr := c.Param("id")
|
||||
accountID, err := strconv.ParseUint(accountIDStr, 10, 64)
|
||||
if err != nil {
|
||||
accountID := parseAccountIDParam(c)
|
||||
if accountID == 0 {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid account_id"})
|
||||
return
|
||||
}
|
||||
|
||||
twIDStr := c.Param("tw_id")
|
||||
twID, err := strconv.ParseUint(twIDStr, 10, 64)
|
||||
twID, err := parseUintParam(c, "tw_id")
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid tw_id"})
|
||||
return
|
||||
@@ -299,20 +293,20 @@ func (h *TwilioChannelHandler) Delete(c *gin.Context) {
|
||||
|
||||
ctx := c.Request.Context()
|
||||
|
||||
ch, err := h.twChannelSvc.GetByID(ctx, uint(twID))
|
||||
ch, err := h.twChannelSvc.GetByID(ctx, twID)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "Twilio SMS channel not found"})
|
||||
return
|
||||
}
|
||||
|
||||
// Verify account ownership
|
||||
if ch.AccountID != uint(accountID) {
|
||||
if ch.AccountID != accountID {
|
||||
c.JSON(http.StatusForbidden, gin.H{"error": "Twilio SMS channel does not belong to this account"})
|
||||
return
|
||||
}
|
||||
|
||||
// Delete the channel record
|
||||
if err := h.twChannelSvc.Delete(ctx, uint(twID)); err != nil {
|
||||
if err := h.twChannelSvc.Delete(ctx, twID); err != nil {
|
||||
applogger.L().Errorf("Failed to delete Twilio SMS channel: %v", err)
|
||||
c.JSON(http.StatusUnprocessableEntity, gin.H{"error": "failed to delete Twilio SMS channel"})
|
||||
return
|
||||
@@ -320,32 +314,57 @@ func (h *TwilioChannelHandler) Delete(c *gin.Context) {
|
||||
|
||||
// Delete the associated inbox
|
||||
if ch.InboxID > 0 {
|
||||
if delErr := h.inboxSvc.DeleteByAccount(ctx, uint(accountID), ch.InboxID); delErr != nil {
|
||||
if delErr := h.inboxSvc.DeleteByAccount(ctx, accountID, ch.InboxID); delErr != nil {
|
||||
applogger.L().Warnf("Failed to delete inbox for Twilio SMS channel: %v", delErr)
|
||||
}
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"message": "Twilio SMS channel deleted successfully"})
|
||||
c.Status(http.StatusOK)
|
||||
}
|
||||
|
||||
// List lists all Twilio SMS channels for an account.
|
||||
// GET /api/v1/accounts/:id/channels/twilio_channel
|
||||
func (h *TwilioChannelHandler) List(c *gin.Context) {
|
||||
accountIDStr := c.Param("id")
|
||||
accountID, err := strconv.ParseUint(accountIDStr, 10, 64)
|
||||
if err != nil {
|
||||
accountID := parseAccountIDParam(c)
|
||||
if accountID == 0 {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid account_id"})
|
||||
return
|
||||
}
|
||||
|
||||
ctx := c.Request.Context()
|
||||
|
||||
channels, err := h.twChannelSvc.ListByAccount(ctx, uint(accountID))
|
||||
channels, err := h.twChannelSvc.ListByAccount(ctx, accountID)
|
||||
if err != nil {
|
||||
applogger.L().Errorf("Failed to list Twilio SMS channels: %v", err)
|
||||
c.JSON(http.StatusUnprocessableEntity, gin.H{"error": "failed to list Twilio SMS channels"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"channels": channels})
|
||||
payload := make([]map[string]any, 0, len(channels))
|
||||
for i := range channels {
|
||||
payload = append(payload, h.serializeInboxForTwilioChannel(c, &channels[i]))
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"payload": payload})
|
||||
}
|
||||
|
||||
func (h *TwilioChannelHandler) serializeInboxForTwilioChannel(c *gin.Context, ch *channelmodel.ChannelTwilioSMS) map[string]any {
|
||||
if ch != nil && ch.InboxID > 0 {
|
||||
if inbox, err := h.inboxSvc.GetByAccountAndID(c.Request.Context(), ch.AccountID, ch.InboxID); err == nil {
|
||||
return serializeInbox(inbox)
|
||||
}
|
||||
}
|
||||
return gin.H{"id": ch.ID, "account_id": ch.AccountID, "inbox_id": ch.InboxID, "account_sid": ch.AccountSID, "phone_number": ch.PhoneNumber, "messaging_service_sid": ch.MessagingServiceSID}
|
||||
}
|
||||
|
||||
func twilioChannelConfigFromModel(ch *channelmodel.ChannelTwilioSMS) map[string]any {
|
||||
medium := "sms"
|
||||
if strings.HasPrefix(ch.PhoneNumber, "whatsapp:") {
|
||||
medium = "whatsapp"
|
||||
}
|
||||
return map[string]any{
|
||||
"account_sid": ch.AccountSID,
|
||||
"phone_number": ch.PhoneNumber,
|
||||
"messaging_service_sid": ch.MessagingServiceSID,
|
||||
"medium": medium,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -170,14 +170,20 @@ func TestTwilioChannel_Get_Success(t *testing.T) {
|
||||
router := setupTwilioTestRouter(handler)
|
||||
accountID := twilioAccountIDUint(db)
|
||||
|
||||
// Seed a channel
|
||||
// Seed a channel and its associated inbox.
|
||||
ch := &channelmodel.ChannelTwilioSMS{
|
||||
AccountSID: "ACget123",
|
||||
PhoneNumber: "+15559999999",
|
||||
AccountID: accountID,
|
||||
InboxID: 200,
|
||||
}
|
||||
require.NoError(t, db.Create(ch).Error)
|
||||
inbox := createTestChannelInbox(t, db, accountID, ch.ID, "Get SMS", "twilio_sms", map[string]any{
|
||||
"account_sid": "ACget123",
|
||||
"phone_number": "+15559999999",
|
||||
"medium": "sms",
|
||||
})
|
||||
ch.InboxID = inbox.ID
|
||||
require.NoError(t, db.Save(ch).Error)
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest(http.MethodGet, "/api/v1/accounts/"+strconv.FormatUint(uint64(accountID), 10)+"/inboxes/1/twilio_sms_channels/"+strconv.FormatUint(uint64(ch.ID), 10), nil)
|
||||
@@ -186,9 +192,10 @@ func TestTwilioChannel_Get_Success(t *testing.T) {
|
||||
assert.Equal(t, http.StatusOK, w.Code)
|
||||
var resp map[string]interface{}
|
||||
require.NoError(t, json.Unmarshal(w.Body.Bytes(), &resp))
|
||||
channelData, ok := resp["channel"].(map[string]interface{})
|
||||
require.True(t, ok)
|
||||
assert.Equal(t, "ACget123", channelData["account_sid"])
|
||||
assert.Equal(t, "Get SMS", resp["name"])
|
||||
assert.Equal(t, "Channel::TwilioSms", resp["channel_type"])
|
||||
assert.Equal(t, "ACget123", resp["account_sid"])
|
||||
require.NotContains(t, resp, "channel")
|
||||
}
|
||||
|
||||
func TestTwilioChannel_Get_InvalidID(t *testing.T) {
|
||||
@@ -225,14 +232,20 @@ func TestTwilioChannel_Update_Success(t *testing.T) {
|
||||
router := setupTwilioTestRouter(handler)
|
||||
accountID := twilioAccountIDUint(db)
|
||||
|
||||
// Seed a channel
|
||||
// Seed a channel and its associated inbox.
|
||||
ch := &channelmodel.ChannelTwilioSMS{
|
||||
AccountSID: "ACupd123",
|
||||
PhoneNumber: "+15558888888",
|
||||
AccountID: accountID,
|
||||
InboxID: 201,
|
||||
}
|
||||
require.NoError(t, db.Create(ch).Error)
|
||||
inbox := createTestChannelInbox(t, db, accountID, ch.ID, "Update SMS", "twilio_sms", map[string]any{
|
||||
"account_sid": "ACupd123",
|
||||
"phone_number": "+15558888888",
|
||||
"medium": "sms",
|
||||
})
|
||||
ch.InboxID = inbox.ID
|
||||
require.NoError(t, db.Save(ch).Error)
|
||||
|
||||
newPhone := "+15557777777"
|
||||
body := UpdateTwilioSMSChannelRequest{
|
||||
@@ -360,15 +373,21 @@ func TestTwilioChannel_List_Success(t *testing.T) {
|
||||
router := setupTwilioTestRouter(handler)
|
||||
accountID := twilioAccountIDUint(db)
|
||||
|
||||
// Seed multiple channels
|
||||
// Seed multiple channels and their associated inboxes.
|
||||
for i := 0; i < 3; i++ {
|
||||
ch := &channelmodel.ChannelTwilioSMS{
|
||||
AccountSID: "AClist" + strconv.Itoa(i),
|
||||
PhoneNumber: "+1555" + strconv.Itoa(i) + "00000",
|
||||
AccountID: accountID,
|
||||
InboxID: uint(i + 100), // unique inbox IDs to satisfy uniqueIndex constraint
|
||||
}
|
||||
require.NoError(t, db.Create(ch).Error)
|
||||
inbox := createTestChannelInbox(t, db, accountID, ch.ID, "SMS List "+strconv.Itoa(i), "twilio_sms", map[string]any{
|
||||
"account_sid": ch.AccountSID,
|
||||
"phone_number": ch.PhoneNumber,
|
||||
"medium": "sms",
|
||||
})
|
||||
ch.InboxID = inbox.ID
|
||||
require.NoError(t, db.Save(ch).Error)
|
||||
}
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
@@ -378,7 +397,7 @@ func TestTwilioChannel_List_Success(t *testing.T) {
|
||||
assert.Equal(t, http.StatusOK, w.Code)
|
||||
var resp map[string]interface{}
|
||||
require.NoError(t, json.Unmarshal(w.Body.Bytes(), &resp))
|
||||
channels, ok := resp["channels"].([]interface{})
|
||||
channels, ok := resp["payload"].([]interface{})
|
||||
require.True(t, ok)
|
||||
assert.Len(t, channels, 3)
|
||||
}
|
||||
@@ -406,7 +425,7 @@ func TestTwilioChannel_List_Empty(t *testing.T) {
|
||||
assert.Equal(t, http.StatusOK, w.Code)
|
||||
var resp map[string]interface{}
|
||||
require.NoError(t, json.Unmarshal(w.Body.Bytes(), &resp))
|
||||
channels, ok := resp["channels"].([]interface{})
|
||||
channels, ok := resp["payload"].([]interface{})
|
||||
require.True(t, ok)
|
||||
assert.Len(t, channels, 0)
|
||||
}
|
||||
|
||||
@@ -234,6 +234,29 @@ func (s *InboxService) Update(ctx context.Context, accountID, id uint, req Updat
|
||||
return inbox, nil
|
||||
}
|
||||
|
||||
// BindChannel persists the channel id and channel_config for channel-specific
|
||||
// controllers that still create their dedicated channel record first.
|
||||
func (s *InboxService) BindChannel(ctx context.Context, accountID, id, channelID uint, channel map[string]any) (*model.Inbox, error) {
|
||||
inbox, err := s.repo.FindByAccountAndID(ctx, accountID, id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
inbox.ChannelID = channelID
|
||||
if len(channel) > 0 {
|
||||
config := parseChannelConfigMap(inbox.ChannelConfig)
|
||||
mergeInboxChannelConfig(config, channel)
|
||||
configJSON, err := json.Marshal(config)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
inbox.ChannelConfig = string(configJSON)
|
||||
}
|
||||
if err := s.repo.Update(ctx, inbox); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return inbox, nil
|
||||
}
|
||||
|
||||
func applyCreateInboxSettings(inbox *model.Inbox, req CreateInboxRequest) {
|
||||
if req.GreetingEnabled != nil {
|
||||
inbox.GreetingEnabled = *req.GreetingEnabled
|
||||
|
||||
Reference in New Issue
Block a user