Merge pull request #1180 from trheyi/main
Enhance messenger service with optional message type support for temp…
This commit is contained in:
commit
625abf5499
19 changed files with 839 additions and 124 deletions
|
|
@ -300,69 +300,305 @@ func (m *Service) SendWithProvider(ctx context.Context, providerName string, mes
|
||||||
}
|
}
|
||||||
|
|
||||||
// SendT sends a message using a template
|
// SendT sends a message using a template
|
||||||
func (m *Service) SendT(ctx context.Context, channel string, templateID string, data types.TemplateData) error {
|
// messageType is optional - if not specified, the first available template type will be used
|
||||||
// Get providers for the channel
|
func (m *Service) SendT(ctx context.Context, channel string, templateID string, data types.TemplateData, messageType ...types.MessageType) error {
|
||||||
providers := m.GetProviders(channel)
|
m.mutex.RLock()
|
||||||
if len(providers) == 0 {
|
defer m.mutex.RUnlock()
|
||||||
return fmt.Errorf("no providers available for channel: %s", channel)
|
|
||||||
|
// Determine which message type to use
|
||||||
|
var msgType types.MessageType
|
||||||
|
if len(messageType) > 0 {
|
||||||
|
// Use specified message type
|
||||||
|
msgType = messageType[0]
|
||||||
|
} else {
|
||||||
|
// Get available template types and use the first one
|
||||||
|
availableTypes := template.Global.GetAvailableTypes(templateID)
|
||||||
|
if len(availableTypes) == 0 {
|
||||||
|
return fmt.Errorf("template not found: %s", templateID)
|
||||||
|
}
|
||||||
|
// Convert TemplateType to MessageType
|
||||||
|
msgType = templateTypeToMessageType(availableTypes[0])
|
||||||
}
|
}
|
||||||
|
|
||||||
// Use the first available provider's SendT method
|
// Get provider for this channel and message type
|
||||||
provider := providers[0]
|
providerName := m.getProviderForChannel(channel, string(msgType))
|
||||||
return provider.SendT(ctx, templateID, data)
|
if providerName == "" {
|
||||||
|
return fmt.Errorf("no provider configured for channel %s with message type %s", channel, msgType)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the provider
|
||||||
|
provider, exists := m.providers[providerName]
|
||||||
|
if !exists {
|
||||||
|
return fmt.Errorf("provider not found: %s", providerName)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert MessageType back to TemplateType
|
||||||
|
templateType := messageTypeToTemplateType(msgType)
|
||||||
|
|
||||||
|
// Call provider's SendT method
|
||||||
|
return provider.SendT(ctx, templateID, templateType, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
// templateTypeToMessageType converts TemplateType to MessageType
|
||||||
|
func templateTypeToMessageType(templateType types.TemplateType) types.MessageType {
|
||||||
|
switch templateType {
|
||||||
|
case types.TemplateTypeMail:
|
||||||
|
return types.MessageTypeEmail
|
||||||
|
case types.TemplateTypeSMS:
|
||||||
|
return types.MessageTypeSMS
|
||||||
|
case types.TemplateTypeWhatsApp:
|
||||||
|
return types.MessageTypeWhatsApp
|
||||||
|
default:
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// messageTypeToTemplateType converts MessageType to TemplateType
|
||||||
|
func messageTypeToTemplateType(messageType types.MessageType) types.TemplateType {
|
||||||
|
switch messageType {
|
||||||
|
case types.MessageTypeEmail:
|
||||||
|
return types.TemplateTypeMail
|
||||||
|
case types.MessageTypeSMS:
|
||||||
|
return types.TemplateTypeSMS
|
||||||
|
case types.MessageTypeWhatsApp:
|
||||||
|
return types.TemplateTypeWhatsApp
|
||||||
|
default:
|
||||||
|
return ""
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// SendTWithProvider sends a message using a template and specific provider
|
// SendTWithProvider sends a message using a template and specific provider
|
||||||
func (m *Service) SendTWithProvider(ctx context.Context, providerName string, templateID string, data types.TemplateData) error {
|
// messageType is optional - if not specified, the first available template type will be used
|
||||||
|
func (m *Service) SendTWithProvider(ctx context.Context, providerName string, templateID string, data types.TemplateData, messageType ...types.MessageType) error {
|
||||||
// Get provider
|
// Get provider
|
||||||
provider, exists := m.providers[providerName]
|
provider, exists := m.providers[providerName]
|
||||||
if !exists {
|
if !exists {
|
||||||
return fmt.Errorf("provider not found: %s", providerName)
|
return fmt.Errorf("provider not found: %s", providerName)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Use the provider's SendT method directly
|
// Determine which message type to use
|
||||||
return provider.SendT(ctx, templateID, data)
|
var msgType types.MessageType
|
||||||
|
if len(messageType) > 0 {
|
||||||
|
// Use specified message type
|
||||||
|
msgType = messageType[0]
|
||||||
|
} else {
|
||||||
|
// Get available template types and use the first one
|
||||||
|
availableTypes := template.Global.GetAvailableTypes(templateID)
|
||||||
|
if len(availableTypes) == 0 {
|
||||||
|
return fmt.Errorf("template not found: %s", templateID)
|
||||||
|
}
|
||||||
|
// Convert TemplateType to MessageType
|
||||||
|
msgType = templateTypeToMessageType(availableTypes[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert MessageType to TemplateType
|
||||||
|
templateType := messageTypeToTemplateType(msgType)
|
||||||
|
|
||||||
|
// Call provider's SendT method
|
||||||
|
return provider.SendT(ctx, templateID, templateType, data)
|
||||||
}
|
}
|
||||||
|
|
||||||
// SendTBatch sends multiple messages using templates in batch
|
// SendTBatch sends multiple messages using templates in batch
|
||||||
func (m *Service) SendTBatch(ctx context.Context, channel string, templateID string, dataList []types.TemplateData) error {
|
// messageType is optional - if not specified, the first available template type will be used
|
||||||
// Get providers for the channel
|
func (m *Service) SendTBatch(ctx context.Context, channel string, templateID string, dataList []types.TemplateData, messageType ...types.MessageType) error {
|
||||||
providers := m.GetProviders(channel)
|
m.mutex.RLock()
|
||||||
if len(providers) == 0 {
|
defer m.mutex.RUnlock()
|
||||||
return fmt.Errorf("no providers available for channel: %s", channel)
|
|
||||||
|
if len(dataList) == 0 {
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Use the first available provider's SendTBatch method
|
// Determine which message type to use
|
||||||
provider := providers[0]
|
var msgType types.MessageType
|
||||||
return provider.SendTBatch(ctx, templateID, dataList)
|
if len(messageType) > 0 {
|
||||||
|
// Use specified message type
|
||||||
|
msgType = messageType[0]
|
||||||
|
} else {
|
||||||
|
// Get available template types and use the first one
|
||||||
|
availableTypes := template.Global.GetAvailableTypes(templateID)
|
||||||
|
if len(availableTypes) == 0 {
|
||||||
|
return fmt.Errorf("template not found: %s", templateID)
|
||||||
|
}
|
||||||
|
// Convert TemplateType to MessageType
|
||||||
|
msgType = templateTypeToMessageType(availableTypes[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get provider for this channel and message type
|
||||||
|
providerName := m.getProviderForChannel(channel, string(msgType))
|
||||||
|
if providerName == "" {
|
||||||
|
return fmt.Errorf("no provider configured for channel %s with message type %s", channel, msgType)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the provider
|
||||||
|
provider, exists := m.providers[providerName]
|
||||||
|
if !exists {
|
||||||
|
return fmt.Errorf("provider not found: %s", providerName)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert MessageType to TemplateType
|
||||||
|
templateType := messageTypeToTemplateType(msgType)
|
||||||
|
|
||||||
|
// Get the template
|
||||||
|
tmpl, err := template.Global.GetTemplate(templateID, templateType)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("template not found: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert all template data to messages
|
||||||
|
messages := make([]*types.Message, 0, len(dataList))
|
||||||
|
for _, data := range dataList {
|
||||||
|
message, err := tmpl.ToMessage(data)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to convert template to message: %w", err)
|
||||||
|
}
|
||||||
|
messages = append(messages, message)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send batch using the provider
|
||||||
|
return provider.SendBatch(ctx, messages)
|
||||||
}
|
}
|
||||||
|
|
||||||
// SendTBatchWithProvider sends multiple messages using templates and specific provider in batch
|
// SendTBatchWithProvider sends multiple messages using templates and specific provider in batch
|
||||||
func (m *Service) SendTBatchWithProvider(ctx context.Context, providerName string, templateID string, dataList []types.TemplateData) error {
|
// messageType is optional - if not specified, the first available template type will be used
|
||||||
|
func (m *Service) SendTBatchWithProvider(ctx context.Context, providerName string, templateID string, dataList []types.TemplateData, messageType ...types.MessageType) error {
|
||||||
// Get provider
|
// Get provider
|
||||||
provider, exists := m.providers[providerName]
|
provider, exists := m.providers[providerName]
|
||||||
if !exists {
|
if !exists {
|
||||||
return fmt.Errorf("provider not found: %s", providerName)
|
return fmt.Errorf("provider not found: %s", providerName)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Use the provider's SendTBatch method directly
|
if len(dataList) == 0 {
|
||||||
return provider.SendTBatch(ctx, templateID, dataList)
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determine which message type to use
|
||||||
|
var msgType types.MessageType
|
||||||
|
if len(messageType) > 0 {
|
||||||
|
// Use specified message type
|
||||||
|
msgType = messageType[0]
|
||||||
|
} else {
|
||||||
|
// Get available template types and use the first one
|
||||||
|
availableTypes := template.Global.GetAvailableTypes(templateID)
|
||||||
|
if len(availableTypes) == 0 {
|
||||||
|
return fmt.Errorf("template not found: %s", templateID)
|
||||||
|
}
|
||||||
|
// Convert TemplateType to MessageType
|
||||||
|
msgType = templateTypeToMessageType(availableTypes[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert MessageType to TemplateType
|
||||||
|
templateType := messageTypeToTemplateType(msgType)
|
||||||
|
|
||||||
|
// Get the template
|
||||||
|
tmpl, err := template.Global.GetTemplate(templateID, templateType)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("template not found: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert all template data to messages
|
||||||
|
messages := make([]*types.Message, 0, len(dataList))
|
||||||
|
for _, data := range dataList {
|
||||||
|
message, err := tmpl.ToMessage(data)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to convert template to message: %w", err)
|
||||||
|
}
|
||||||
|
messages = append(messages, message)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send batch using the provider
|
||||||
|
return provider.SendBatch(ctx, messages)
|
||||||
}
|
}
|
||||||
|
|
||||||
// SendTBatchMixed sends multiple messages using different templates with different data
|
// SendTBatchMixed sends multiple messages using different templates with different data
|
||||||
|
// Each TemplateRequest can optionally specify its MessageType
|
||||||
func (m *Service) SendTBatchMixed(ctx context.Context, channel string, templateRequests []types.TemplateRequest) error {
|
func (m *Service) SendTBatchMixed(ctx context.Context, channel string, templateRequests []types.TemplateRequest) error {
|
||||||
// Get providers for the channel
|
m.mutex.RLock()
|
||||||
providers := m.GetProviders(channel)
|
defer m.mutex.RUnlock()
|
||||||
if len(providers) == 0 {
|
|
||||||
return fmt.Errorf("no providers available for channel: %s", channel)
|
if len(templateRequests) == 0 {
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Use the first available provider's SendTBatchMixed method
|
// Group messages by provider
|
||||||
provider := providers[0]
|
providerMessages := make(map[string][]*types.Message)
|
||||||
return provider.SendTBatchMixed(ctx, templateRequests)
|
|
||||||
|
// Process each template request
|
||||||
|
for _, request := range templateRequests {
|
||||||
|
// Determine message type
|
||||||
|
var msgType types.MessageType
|
||||||
|
if request.MessageType != nil {
|
||||||
|
// Use specified message type
|
||||||
|
msgType = *request.MessageType
|
||||||
|
} else {
|
||||||
|
// Get available template types and use the first one
|
||||||
|
availableTypes := template.Global.GetAvailableTypes(request.TemplateID)
|
||||||
|
if len(availableTypes) == 0 {
|
||||||
|
return fmt.Errorf("template not found: %s", request.TemplateID)
|
||||||
|
}
|
||||||
|
// Convert TemplateType to MessageType
|
||||||
|
msgType = templateTypeToMessageType(availableTypes[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get provider for this channel and message type
|
||||||
|
providerName := m.getProviderForChannel(channel, string(msgType))
|
||||||
|
if providerName == "" {
|
||||||
|
return fmt.Errorf("no provider configured for channel %s with message type %s", channel, msgType)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify provider exists
|
||||||
|
if _, exists := m.providers[providerName]; !exists {
|
||||||
|
return fmt.Errorf("provider not found: %s", providerName)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert MessageType to TemplateType
|
||||||
|
templateType := messageTypeToTemplateType(msgType)
|
||||||
|
|
||||||
|
// Get the template
|
||||||
|
tmpl, err := template.Global.GetTemplate(request.TemplateID, templateType)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("template %s not found: %w", request.TemplateID, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert template to message
|
||||||
|
message, err := tmpl.ToMessage(request.Data)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to convert template %s to message: %w", request.TemplateID, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add to provider's message list
|
||||||
|
providerMessages[providerName] = append(providerMessages[providerName], message)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send batches to each provider
|
||||||
|
var errors []string
|
||||||
|
for providerName, messages := range providerMessages {
|
||||||
|
// Check if context is cancelled
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return fmt.Errorf("batch send cancelled: %w", ctx.Err())
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
|
||||||
|
provider, exists := m.providers[providerName]
|
||||||
|
if !exists {
|
||||||
|
errors = append(errors, fmt.Sprintf("provider not found: %s", providerName))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
err := provider.SendBatch(ctx, messages)
|
||||||
|
if err != nil {
|
||||||
|
errors = append(errors, fmt.Sprintf("provider %s: %v", providerName, err))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(errors) > 0 {
|
||||||
|
return fmt.Errorf("batch send errors: %s", strings.Join(errors, "; "))
|
||||||
|
}
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// SendTBatchMixedWithProvider sends multiple messages using different templates with different data and specific provider
|
// SendTBatchMixedWithProvider sends multiple messages using different templates with different data and specific provider
|
||||||
|
// Each TemplateRequest can optionally specify its MessageType
|
||||||
func (m *Service) SendTBatchMixedWithProvider(ctx context.Context, providerName string, templateRequests []types.TemplateRequest) error {
|
func (m *Service) SendTBatchMixedWithProvider(ctx context.Context, providerName string, templateRequests []types.TemplateRequest) error {
|
||||||
// Get provider
|
// Get provider
|
||||||
provider, exists := m.providers[providerName]
|
provider, exists := m.providers[providerName]
|
||||||
|
|
@ -370,8 +606,49 @@ func (m *Service) SendTBatchMixedWithProvider(ctx context.Context, providerName
|
||||||
return fmt.Errorf("provider not found: %s", providerName)
|
return fmt.Errorf("provider not found: %s", providerName)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Use the provider's SendTBatchMixed method directly
|
if len(templateRequests) == 0 {
|
||||||
return provider.SendTBatchMixed(ctx, templateRequests)
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert all template requests to messages
|
||||||
|
messages := make([]*types.Message, 0, len(templateRequests))
|
||||||
|
|
||||||
|
for _, request := range templateRequests {
|
||||||
|
// Determine message type
|
||||||
|
var msgType types.MessageType
|
||||||
|
if request.MessageType != nil {
|
||||||
|
// Use specified message type
|
||||||
|
msgType = *request.MessageType
|
||||||
|
} else {
|
||||||
|
// Get available template types and use the first one
|
||||||
|
availableTypes := template.Global.GetAvailableTypes(request.TemplateID)
|
||||||
|
if len(availableTypes) == 0 {
|
||||||
|
return fmt.Errorf("template not found: %s", request.TemplateID)
|
||||||
|
}
|
||||||
|
// Convert TemplateType to MessageType
|
||||||
|
msgType = templateTypeToMessageType(availableTypes[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert MessageType to TemplateType
|
||||||
|
templateType := messageTypeToTemplateType(msgType)
|
||||||
|
|
||||||
|
// Get the template
|
||||||
|
tmpl, err := template.Global.GetTemplate(request.TemplateID, templateType)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("template %s not found: %w", request.TemplateID, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert template to message
|
||||||
|
message, err := tmpl.ToMessage(request.Data)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to convert template %s to message: %w", request.TemplateID, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
messages = append(messages, message)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send batch using the provider
|
||||||
|
return provider.SendBatch(ctx, messages)
|
||||||
}
|
}
|
||||||
|
|
||||||
// SendBatch sends multiple messages in batch
|
// SendBatch sends multiple messages in batch
|
||||||
|
|
|
||||||
252
messenger/messenger_sendt_test.go
Normal file
252
messenger/messenger_sendt_test.go
Normal file
|
|
@ -0,0 +1,252 @@
|
||||||
|
package messenger
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
"github.com/yaoapp/yao/config"
|
||||||
|
"github.com/yaoapp/yao/messenger/template"
|
||||||
|
"github.com/yaoapp/yao/messenger/types"
|
||||||
|
"github.com/yaoapp/yao/test"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TestSendT_TemplateTypeSelection tests that SendT correctly selects template type and provider based on channel configuration
|
||||||
|
func TestSendT_TemplateTypeSelection(t *testing.T) {
|
||||||
|
// Prepare test environment
|
||||||
|
test.Prepare(t, config.Conf, "YAO_TEST_APPLICATION")
|
||||||
|
defer test.Clean()
|
||||||
|
|
||||||
|
// Load messenger
|
||||||
|
err := Load(config.Conf)
|
||||||
|
require.NoError(t, err, "Load messenger should succeed")
|
||||||
|
|
||||||
|
// Load templates
|
||||||
|
err = template.LoadTemplates()
|
||||||
|
require.NoError(t, err, "Load templates should succeed")
|
||||||
|
|
||||||
|
service, ok := Instance.(*Service)
|
||||||
|
require.True(t, ok, "Instance should be of type *Service")
|
||||||
|
|
||||||
|
// Test 1: Get available types for a template
|
||||||
|
availableTypes := template.Global.GetAvailableTypes("en.invite_member")
|
||||||
|
t.Logf("Available types for en.invite_member: %v", availableTypes)
|
||||||
|
|
||||||
|
// Test 2: SendT without specifying messageType (should use first available)
|
||||||
|
ctx := context.Background()
|
||||||
|
data := types.TemplateData{
|
||||||
|
"to": []string{"test@example.com"},
|
||||||
|
"team_name": "Test Team",
|
||||||
|
"inviter_name": "Test User",
|
||||||
|
"invite_link": "https://example.com/invite/test",
|
||||||
|
}
|
||||||
|
|
||||||
|
// Note: This will fail with actual sending due to test credentials, but we're testing the logic
|
||||||
|
err = service.SendT(ctx, "default", "en.invite_member", data)
|
||||||
|
if err != nil {
|
||||||
|
t.Logf("SendT failed (expected with test credentials): %v", err)
|
||||||
|
// Should not be template-not-found or provider-not-found error
|
||||||
|
assert.NotContains(t, err.Error(), "template not found", "Should not be template error")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test 3: SendT with explicit messageType
|
||||||
|
err = service.SendT(ctx, "default", "en.invite_member", data, types.MessageTypeEmail)
|
||||||
|
if err != nil {
|
||||||
|
t.Logf("SendT with explicit type failed (expected with test credentials): %v", err)
|
||||||
|
assert.NotContains(t, err.Error(), "template not found", "Should not be template error")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestGetProviderForChannel tests the provider selection logic
|
||||||
|
func TestGetProviderForChannel(t *testing.T) {
|
||||||
|
// Prepare test environment
|
||||||
|
test.Prepare(t, config.Conf, "YAO_TEST_APPLICATION")
|
||||||
|
defer test.Clean()
|
||||||
|
|
||||||
|
// Load messenger
|
||||||
|
err := Load(config.Conf)
|
||||||
|
require.NoError(t, err, "Load messenger should succeed")
|
||||||
|
|
||||||
|
service, ok := Instance.(*Service)
|
||||||
|
require.True(t, ok, "Instance should be of type *Service")
|
||||||
|
|
||||||
|
// Test provider selection for different channels and message types
|
||||||
|
tests := []struct {
|
||||||
|
channel string
|
||||||
|
messageType string
|
||||||
|
expected string // Expected provider name
|
||||||
|
}{
|
||||||
|
{"default", "email", "primary"},
|
||||||
|
{"default", "sms", "unified"},
|
||||||
|
{"default", "whatsapp", "unified"},
|
||||||
|
{"promotions", "email", "marketing"},
|
||||||
|
{"alerts", "email", "reliable"},
|
||||||
|
{"notifications", "email", "primary"},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.channel+"_"+tt.messageType, func(t *testing.T) {
|
||||||
|
providerName := service.getProviderForChannel(tt.channel, tt.messageType)
|
||||||
|
assert.Equal(t, tt.expected, providerName,
|
||||||
|
"Channel %s with message type %s should use provider %s",
|
||||||
|
tt.channel, tt.messageType, tt.expected)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestTemplateTypeConversion tests the conversion between MessageType and TemplateType
|
||||||
|
func TestTemplateTypeConversion(t *testing.T) {
|
||||||
|
// Test templateTypeToMessageType
|
||||||
|
tests := []struct {
|
||||||
|
templateType types.TemplateType
|
||||||
|
expected types.MessageType
|
||||||
|
}{
|
||||||
|
{types.TemplateTypeMail, types.MessageTypeEmail},
|
||||||
|
{types.TemplateTypeSMS, types.MessageTypeSMS},
|
||||||
|
{types.TemplateTypeWhatsApp, types.MessageTypeWhatsApp},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
result := templateTypeToMessageType(tt.templateType)
|
||||||
|
assert.Equal(t, tt.expected, result,
|
||||||
|
"TemplateType %s should convert to MessageType %s",
|
||||||
|
tt.templateType, tt.expected)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test messageTypeToTemplateType
|
||||||
|
reverseTests := []struct {
|
||||||
|
messageType types.MessageType
|
||||||
|
expected types.TemplateType
|
||||||
|
}{
|
||||||
|
{types.MessageTypeEmail, types.TemplateTypeMail},
|
||||||
|
{types.MessageTypeSMS, types.TemplateTypeSMS},
|
||||||
|
{types.MessageTypeWhatsApp, types.TemplateTypeWhatsApp},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range reverseTests {
|
||||||
|
result := messageTypeToTemplateType(tt.messageType)
|
||||||
|
assert.Equal(t, tt.expected, result,
|
||||||
|
"MessageType %s should convert to TemplateType %s",
|
||||||
|
tt.messageType, tt.expected)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestSendTBatch tests the batch sending with template type selection
|
||||||
|
func TestSendTBatch(t *testing.T) {
|
||||||
|
// Prepare test environment
|
||||||
|
test.Prepare(t, config.Conf, "YAO_TEST_APPLICATION")
|
||||||
|
defer test.Clean()
|
||||||
|
|
||||||
|
// Load messenger
|
||||||
|
err := Load(config.Conf)
|
||||||
|
require.NoError(t, err, "Load messenger should succeed")
|
||||||
|
|
||||||
|
// Load templates
|
||||||
|
err = template.LoadTemplates()
|
||||||
|
require.NoError(t, err, "Load templates should succeed")
|
||||||
|
|
||||||
|
service, ok := Instance.(*Service)
|
||||||
|
require.True(t, ok, "Instance should be of type *Service")
|
||||||
|
|
||||||
|
// Test batch sending
|
||||||
|
ctx := context.Background()
|
||||||
|
dataList := []types.TemplateData{
|
||||||
|
{
|
||||||
|
"to": []string{"user1@example.com"},
|
||||||
|
"team_name": "Test Team",
|
||||||
|
"inviter_name": "Test User",
|
||||||
|
"invite_link": "https://example.com/invite/test1",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"to": []string{"user2@example.com"},
|
||||||
|
"team_name": "Test Team",
|
||||||
|
"inviter_name": "Test User",
|
||||||
|
"invite_link": "https://example.com/invite/test2",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test without explicit messageType
|
||||||
|
err = service.SendTBatch(ctx, "default", "en.invite_member", dataList)
|
||||||
|
if err != nil {
|
||||||
|
t.Logf("SendTBatch failed (expected with test credentials): %v", err)
|
||||||
|
assert.NotContains(t, err.Error(), "template not found", "Should not be template error")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test with explicit messageType
|
||||||
|
err = service.SendTBatch(ctx, "default", "en.invite_member", dataList, types.MessageTypeEmail)
|
||||||
|
if err != nil {
|
||||||
|
t.Logf("SendTBatch with explicit type failed (expected with test credentials): %v", err)
|
||||||
|
assert.NotContains(t, err.Error(), "template not found", "Should not be template error")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestSendTBatchMixed tests mixed template batch sending
|
||||||
|
func TestSendTBatchMixed(t *testing.T) {
|
||||||
|
// Prepare test environment
|
||||||
|
test.Prepare(t, config.Conf, "YAO_TEST_APPLICATION")
|
||||||
|
defer test.Clean()
|
||||||
|
|
||||||
|
// Load messenger
|
||||||
|
err := Load(config.Conf)
|
||||||
|
require.NoError(t, err, "Load messenger should succeed")
|
||||||
|
|
||||||
|
// Load templates
|
||||||
|
err = template.LoadTemplates()
|
||||||
|
require.NoError(t, err, "Load templates should succeed")
|
||||||
|
|
||||||
|
service, ok := Instance.(*Service)
|
||||||
|
require.True(t, ok, "Instance should be of type *Service")
|
||||||
|
|
||||||
|
// Test mixed batch sending
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
// Test without specifying MessageType in requests
|
||||||
|
requests := []types.TemplateRequest{
|
||||||
|
{
|
||||||
|
TemplateID: "en.invite_member",
|
||||||
|
Data: types.TemplateData{
|
||||||
|
"to": []string{"user1@example.com"},
|
||||||
|
"team_name": "Test Team",
|
||||||
|
"inviter_name": "Test User",
|
||||||
|
"invite_link": "https://example.com/invite/test1",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
TemplateID: "en.invite_member",
|
||||||
|
Data: types.TemplateData{
|
||||||
|
"to": []string{"user2@example.com"},
|
||||||
|
"team_name": "Test Team",
|
||||||
|
"inviter_name": "Test User",
|
||||||
|
"invite_link": "https://example.com/invite/test2",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
err = service.SendTBatchMixed(ctx, "default", requests)
|
||||||
|
if err != nil {
|
||||||
|
t.Logf("SendTBatchMixed failed (expected with test credentials): %v", err)
|
||||||
|
assert.NotContains(t, err.Error(), "template not found", "Should not be template error")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test with explicit MessageType in requests
|
||||||
|
emailType := types.MessageTypeEmail
|
||||||
|
requestsWithType := []types.TemplateRequest{
|
||||||
|
{
|
||||||
|
TemplateID: "en.invite_member",
|
||||||
|
MessageType: &emailType,
|
||||||
|
Data: types.TemplateData{
|
||||||
|
"to": []string{"user1@example.com"},
|
||||||
|
"team_name": "Test Team",
|
||||||
|
"inviter_name": "Test User",
|
||||||
|
"invite_link": "https://example.com/invite/test1",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
err = service.SendTBatchMixed(ctx, "default", requestsWithType)
|
||||||
|
if err != nil {
|
||||||
|
t.Logf("SendTBatchMixed with explicit type failed (expected with test credentials): %v", err)
|
||||||
|
assert.NotContains(t, err.Error(), "template not found", "Should not be template error")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -210,9 +210,9 @@ func (p *Provider) SendBatch(ctx context.Context, messages []*types.Message) err
|
||||||
}
|
}
|
||||||
|
|
||||||
// SendT sends a message using a template
|
// SendT sends a message using a template
|
||||||
func (p *Provider) SendT(ctx context.Context, templateID string, data types.TemplateData) error {
|
func (p *Provider) SendT(ctx context.Context, templateID string, templateType types.TemplateType, data types.TemplateData) error {
|
||||||
// Get template from provider's template manager (mailer supports mail templates)
|
// Get template from provider's template manager with specified type
|
||||||
template, err := p.getTemplate(templateID, types.TemplateTypeMail)
|
template, err := p.getTemplate(templateID, templateType)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("template not found: %w", err)
|
return fmt.Errorf("template not found: %w", err)
|
||||||
}
|
}
|
||||||
|
|
@ -228,9 +228,9 @@ func (p *Provider) SendT(ctx context.Context, templateID string, data types.Temp
|
||||||
}
|
}
|
||||||
|
|
||||||
// SendTBatch sends multiple messages using templates in batch
|
// SendTBatch sends multiple messages using templates in batch
|
||||||
func (p *Provider) SendTBatch(ctx context.Context, templateID string, dataList []types.TemplateData) error {
|
func (p *Provider) SendTBatch(ctx context.Context, templateID string, templateType types.TemplateType, dataList []types.TemplateData) error {
|
||||||
// Get template from provider's template manager (mailer supports mail templates)
|
// Get template from provider's template manager with specified type
|
||||||
template, err := p.getTemplate(templateID, types.TemplateTypeMail)
|
template, err := p.getTemplate(templateID, templateType)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("template not found: %w", err)
|
return fmt.Errorf("template not found: %w", err)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -50,7 +50,7 @@ func TestSendTBatch_Success(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Test SendTBatch - should fail because template manager is nil
|
// Test SendTBatch - should fail because template manager is nil
|
||||||
err = provider.SendTBatch(context.Background(), "en.invite_member", dataList)
|
err = provider.SendTBatch(context.Background(), "en.invite_member", types.TemplateTypeMail, dataList)
|
||||||
assert.Error(t, err)
|
assert.Error(t, err)
|
||||||
assert.Contains(t, err.Error(), "template manager not available")
|
assert.Contains(t, err.Error(), "template manager not available")
|
||||||
}
|
}
|
||||||
|
|
@ -95,7 +95,7 @@ func TestSendTBatch_ContextTimeout(t *testing.T) {
|
||||||
time.Sleep(2 * time.Nanosecond)
|
time.Sleep(2 * time.Nanosecond)
|
||||||
|
|
||||||
// Test SendTBatch with expired context
|
// Test SendTBatch with expired context
|
||||||
err = provider.SendTBatch(ctx, "en.invite_member", dataList)
|
err = provider.SendTBatch(ctx, "en.invite_member", types.TemplateTypeMail, dataList)
|
||||||
assert.Error(t, err)
|
assert.Error(t, err)
|
||||||
// Error could be either "template manager not available" or "context deadline exceeded"
|
// Error could be either "template manager not available" or "context deadline exceeded"
|
||||||
t.Logf("Error: %v", err)
|
t.Logf("Error: %v", err)
|
||||||
|
|
|
||||||
|
|
@ -48,7 +48,7 @@ func TestSendT_TemplateNotImplemented(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Test that SendT returns "template not found" error (template system is working)
|
// Test that SendT returns "template not found" error (template system is working)
|
||||||
err = provider.SendT(ctx, "en.invite_member.mail", templateData)
|
err = provider.SendT(ctx, "en.invite_member.mail", types.TemplateTypeMail, templateData)
|
||||||
assert.Error(t, err)
|
assert.Error(t, err)
|
||||||
assert.Contains(t, err.Error(), "template not found")
|
assert.Contains(t, err.Error(), "template not found")
|
||||||
}
|
}
|
||||||
|
|
@ -88,7 +88,7 @@ func TestSendT_ContextTimeout(t *testing.T) {
|
||||||
"invite_link": "https://example.com/invite/123",
|
"invite_link": "https://example.com/invite/123",
|
||||||
}
|
}
|
||||||
|
|
||||||
err = provider.SendT(ctx, "en.invite_member.mail", templateData)
|
err = provider.SendT(ctx, "en.invite_member.mail", types.TemplateTypeMail, templateData)
|
||||||
assert.Error(t, err)
|
assert.Error(t, err)
|
||||||
|
|
||||||
// Verify it's a context timeout error or not implemented error
|
// Verify it's a context timeout error or not implemented error
|
||||||
|
|
@ -173,6 +173,6 @@ func BenchmarkSendT(b *testing.B) {
|
||||||
b.ResetTimer()
|
b.ResetTimer()
|
||||||
for i := 0; i < b.N; i++ {
|
for i := 0; i < b.N; i++ {
|
||||||
// This will return "not implemented" error, but we're measuring the overhead
|
// This will return "not implemented" error, but we're measuring the overhead
|
||||||
_ = provider.SendT(ctx, "en.invite_member.mail", templateData)
|
_ = provider.SendT(ctx, "en.invite_member.mail", types.TemplateTypeMail, templateData)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -94,9 +94,9 @@ func (p *Provider) SendBatch(ctx context.Context, messages []*types.Message) err
|
||||||
}
|
}
|
||||||
|
|
||||||
// SendT sends a message using a template
|
// SendT sends a message using a template
|
||||||
func (p *Provider) SendT(ctx context.Context, templateID string, data types.TemplateData) error {
|
func (p *Provider) SendT(ctx context.Context, templateID string, templateType types.TemplateType, data types.TemplateData) error {
|
||||||
// Get template from provider's template manager
|
// Get template from provider's template manager with specified type
|
||||||
template, err := p.getTemplate(templateID, types.TemplateTypeMail) // Mailgun supports email
|
template, err := p.getTemplate(templateID, templateType)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("template not found: %w", err)
|
return fmt.Errorf("template not found: %w", err)
|
||||||
}
|
}
|
||||||
|
|
@ -112,9 +112,9 @@ func (p *Provider) SendT(ctx context.Context, templateID string, data types.Temp
|
||||||
}
|
}
|
||||||
|
|
||||||
// SendTBatch sends multiple messages using templates in batch
|
// SendTBatch sends multiple messages using templates in batch
|
||||||
func (p *Provider) SendTBatch(ctx context.Context, templateID string, dataList []types.TemplateData) error {
|
func (p *Provider) SendTBatch(ctx context.Context, templateID string, templateType types.TemplateType, dataList []types.TemplateData) error {
|
||||||
// Get template from provider's template manager
|
// Get template from provider's template manager with specified type
|
||||||
template, err := p.getTemplate(templateID, types.TemplateTypeMail) // Mailgun supports email
|
template, err := p.getTemplate(templateID, templateType)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("template not found: %w", err)
|
return fmt.Errorf("template not found: %w", err)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -46,7 +46,7 @@ func TestSendTBatch_Success(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Test SendTBatch - should fail because template manager is nil
|
// Test SendTBatch - should fail because template manager is nil
|
||||||
err = provider.SendTBatch(context.Background(), "en.invite_member", dataList)
|
err = provider.SendTBatch(context.Background(), "en.invite_member", types.TemplateTypeMail, dataList)
|
||||||
assert.Error(t, err)
|
assert.Error(t, err)
|
||||||
assert.Contains(t, err.Error(), "template manager not available")
|
assert.Contains(t, err.Error(), "template manager not available")
|
||||||
}
|
}
|
||||||
|
|
@ -87,7 +87,7 @@ func TestSendTBatch_ContextTimeout(t *testing.T) {
|
||||||
time.Sleep(2 * time.Nanosecond)
|
time.Sleep(2 * time.Nanosecond)
|
||||||
|
|
||||||
// Test SendTBatch with expired context
|
// Test SendTBatch with expired context
|
||||||
err = provider.SendTBatch(ctx, "en.invite_member", dataList)
|
err = provider.SendTBatch(ctx, "en.invite_member", types.TemplateTypeMail, dataList)
|
||||||
assert.Error(t, err)
|
assert.Error(t, err)
|
||||||
// Error could be either "template manager not available" or "context deadline exceeded"
|
// Error could be either "template manager not available" or "context deadline exceeded"
|
||||||
t.Logf("Error: %v", err)
|
t.Logf("Error: %v", err)
|
||||||
|
|
|
||||||
|
|
@ -41,7 +41,7 @@ func TestSendT_TemplateNotImplemented(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Test that SendT returns "template manager not available" error
|
// Test that SendT returns "template manager not available" error
|
||||||
err = provider.SendT(ctx, "en.invite_member", templateData)
|
err = provider.SendT(ctx, "en.invite_member", types.TemplateTypeMail, templateData)
|
||||||
assert.Error(t, err)
|
assert.Error(t, err)
|
||||||
assert.Contains(t, err.Error(), "template manager not available")
|
assert.Contains(t, err.Error(), "template manager not available")
|
||||||
}
|
}
|
||||||
|
|
@ -80,7 +80,7 @@ func TestSendT_ContextTimeout(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Test that SendT handles context timeout
|
// Test that SendT handles context timeout
|
||||||
err = provider.SendT(ctx, "en.invite_member", templateData)
|
err = provider.SendT(ctx, "en.invite_member", types.TemplateTypeMail, templateData)
|
||||||
assert.Error(t, err)
|
assert.Error(t, err)
|
||||||
|
|
||||||
// Verify it's a context timeout error or template manager error
|
// Verify it's a context timeout error or template manager error
|
||||||
|
|
|
||||||
|
|
@ -126,9 +126,9 @@ func (p *Provider) SendBatch(ctx context.Context, messages []*types.Message) err
|
||||||
}
|
}
|
||||||
|
|
||||||
// SendT sends a message using a template
|
// SendT sends a message using a template
|
||||||
func (p *Provider) SendT(ctx context.Context, templateID string, data types.TemplateData) error {
|
func (p *Provider) SendT(ctx context.Context, templateID string, templateType types.TemplateType, data types.TemplateData) error {
|
||||||
// Get template from provider's template manager
|
// Get template from provider's template manager with specified type
|
||||||
template, err := p.getTemplate(templateID, types.TemplateTypeSMS) // Twilio primarily supports SMS
|
template, err := p.getTemplate(templateID, templateType)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("template not found: %w", err)
|
return fmt.Errorf("template not found: %w", err)
|
||||||
}
|
}
|
||||||
|
|
@ -144,9 +144,9 @@ func (p *Provider) SendT(ctx context.Context, templateID string, data types.Temp
|
||||||
}
|
}
|
||||||
|
|
||||||
// SendTBatch sends multiple messages using templates in batch
|
// SendTBatch sends multiple messages using templates in batch
|
||||||
func (p *Provider) SendTBatch(ctx context.Context, templateID string, dataList []types.TemplateData) error {
|
func (p *Provider) SendTBatch(ctx context.Context, templateID string, templateType types.TemplateType, dataList []types.TemplateData) error {
|
||||||
// Get template from provider's template manager
|
// Get template from provider's template manager with specified type
|
||||||
template, err := p.getTemplate(templateID, types.TemplateTypeSMS) // Twilio primarily supports SMS
|
template, err := p.getTemplate(templateID, templateType)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("template not found: %w", err)
|
return fmt.Errorf("template not found: %w", err)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -46,7 +46,7 @@ func TestSendTBatch_Success(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Test SendTBatch - should fail because template manager is nil
|
// Test SendTBatch - should fail because template manager is nil
|
||||||
err = provider.SendTBatch(context.Background(), "en.invite_member", dataList)
|
err = provider.SendTBatch(context.Background(), "en.invite_member", types.TemplateTypeSMS, dataList)
|
||||||
assert.Error(t, err)
|
assert.Error(t, err)
|
||||||
assert.Contains(t, err.Error(), "template manager not available")
|
assert.Contains(t, err.Error(), "template manager not available")
|
||||||
}
|
}
|
||||||
|
|
@ -87,7 +87,7 @@ func TestSendTBatch_ContextTimeout(t *testing.T) {
|
||||||
time.Sleep(2 * time.Nanosecond)
|
time.Sleep(2 * time.Nanosecond)
|
||||||
|
|
||||||
// Test SendTBatch with expired context
|
// Test SendTBatch with expired context
|
||||||
err = provider.SendTBatch(ctx, "en.invite_member", dataList)
|
err = provider.SendTBatch(ctx, "en.invite_member", types.TemplateTypeSMS, dataList)
|
||||||
assert.Error(t, err)
|
assert.Error(t, err)
|
||||||
// Error could be either "template manager not available" or "context deadline exceeded"
|
// Error could be either "template manager not available" or "context deadline exceeded"
|
||||||
t.Logf("Error: %v", err)
|
t.Logf("Error: %v", err)
|
||||||
|
|
|
||||||
|
|
@ -40,7 +40,7 @@ func TestSendT_TemplateNotImplemented(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Test that SendT returns "template manager not available" error
|
// Test that SendT returns "template manager not available" error
|
||||||
err = provider.SendT(ctx, "en.invite_member", templateData)
|
err = provider.SendT(ctx, "en.invite_member", types.TemplateTypeSMS, templateData)
|
||||||
assert.Error(t, err)
|
assert.Error(t, err)
|
||||||
assert.Contains(t, err.Error(), "template manager not available")
|
assert.Contains(t, err.Error(), "template manager not available")
|
||||||
}
|
}
|
||||||
|
|
@ -78,7 +78,7 @@ func TestSendT_ContextTimeout(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Test that SendT handles context timeout
|
// Test that SendT handles context timeout
|
||||||
err = provider.SendT(ctx, "en.invite_member", templateData)
|
err = provider.SendT(ctx, "en.invite_member", types.TemplateTypeSMS, templateData)
|
||||||
assert.Error(t, err)
|
assert.Error(t, err)
|
||||||
|
|
||||||
// Verify it's a context timeout error or template manager error
|
// Verify it's a context timeout error or template manager error
|
||||||
|
|
|
||||||
|
|
@ -52,6 +52,31 @@ func (m *Manager) GetAllTemplates() map[string]map[types.TemplateType]*types.Tem
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetAvailableTypes returns all available template types for a given templateID
|
||||||
|
// Returns types in a consistent order: mail, sms, whatsapp
|
||||||
|
func (m *Manager) GetAvailableTypes(templateID string) []types.TemplateType {
|
||||||
|
m.mutex.RLock()
|
||||||
|
defer m.mutex.RUnlock()
|
||||||
|
|
||||||
|
if templates, exists := m.templates[templateID]; exists {
|
||||||
|
// Return in preferred order
|
||||||
|
var result []types.TemplateType
|
||||||
|
preferredOrder := []types.TemplateType{
|
||||||
|
types.TemplateTypeMail,
|
||||||
|
types.TemplateTypeSMS,
|
||||||
|
types.TemplateTypeWhatsApp,
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, templateType := range preferredOrder {
|
||||||
|
if _, exists := templates[templateType]; exists {
|
||||||
|
result = append(result, templateType)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
return []types.TemplateType{}
|
||||||
|
}
|
||||||
|
|
||||||
// ReloadTemplates reloads all templates from disk
|
// ReloadTemplates reloads all templates from disk
|
||||||
func (m *Manager) ReloadTemplates() error {
|
func (m *Manager) ReloadTemplates() error {
|
||||||
m.mutex.Lock()
|
m.mutex.Lock()
|
||||||
|
|
@ -224,16 +249,16 @@ func parseMailTemplate(content string) (subject, body, html string, err error) {
|
||||||
// Extract subject from <Subject> tag
|
// Extract subject from <Subject> tag
|
||||||
subject = strings.TrimSpace(doc.Find("Subject").Text())
|
subject = strings.TrimSpace(doc.Find("Subject").Text())
|
||||||
|
|
||||||
// Extract body content from <body> tag
|
// Extract body content from <Content> tag (using custom tag to avoid HTML parser auto-conversion)
|
||||||
bodySelection := doc.Find("body")
|
bodySelection := doc.Find("Content")
|
||||||
if bodySelection.Length() == 0 {
|
if bodySelection.Length() == 0 {
|
||||||
return "", "", "", fmt.Errorf("no <body> tag found in mail template")
|
return "", "", "", fmt.Errorf("no <Content> tag found in mail template")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get the HTML content of the body tag
|
// Get the HTML content of the Content tag
|
||||||
body, err = bodySelection.Html()
|
body, err = bodySelection.Html()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", "", "", fmt.Errorf("failed to extract body HTML: %w", err)
|
return "", "", "", fmt.Errorf("failed to extract content HTML: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
body = strings.TrimSpace(body)
|
body = strings.TrimSpace(body)
|
||||||
|
|
|
||||||
|
|
@ -13,13 +13,16 @@ type Provider interface {
|
||||||
// SendBatch sends multiple messages in batch
|
// SendBatch sends multiple messages in batch
|
||||||
SendBatch(ctx context.Context, messages []*Message) error
|
SendBatch(ctx context.Context, messages []*Message) error
|
||||||
|
|
||||||
// SendT sends a message using a template (optional - providers may return "not implemented" error)
|
// SendT sends a message using a template with specified type
|
||||||
SendT(ctx context.Context, templateID string, data TemplateData) error
|
// templateType specifies which template variant to use (mail, sms, whatsapp)
|
||||||
|
SendT(ctx context.Context, templateID string, templateType TemplateType, data TemplateData) error
|
||||||
|
|
||||||
// SendTBatch sends multiple messages using the same template with different data (optional - providers may return "not implemented" error)
|
// SendTBatch sends multiple messages using the same template with different data
|
||||||
SendTBatch(ctx context.Context, templateID string, dataList []TemplateData) error
|
// templateType specifies which template variant to use (mail, sms, whatsapp)
|
||||||
|
SendTBatch(ctx context.Context, templateID string, templateType TemplateType, dataList []TemplateData) error
|
||||||
|
|
||||||
// SendTBatchMixed sends multiple messages using different templates with different data (optional - providers may return "not implemented" error)
|
// SendTBatchMixed sends multiple messages using different templates with different data
|
||||||
|
// Each TemplateRequest can optionally specify its own MessageType
|
||||||
SendTBatchMixed(ctx context.Context, templateRequests []TemplateRequest) error
|
SendTBatchMixed(ctx context.Context, templateRequests []TemplateRequest) error
|
||||||
|
|
||||||
// TriggerWebhook processes webhook requests and converts to Message
|
// TriggerWebhook processes webhook requests and converts to Message
|
||||||
|
|
@ -50,16 +53,20 @@ type Messenger interface {
|
||||||
SendWithProvider(ctx context.Context, providerName string, message *Message) error
|
SendWithProvider(ctx context.Context, providerName string, message *Message) error
|
||||||
|
|
||||||
// SendT sends a message using a template
|
// SendT sends a message using a template
|
||||||
SendT(ctx context.Context, channel string, templateID string, data TemplateData) error
|
// messageType is optional - if not specified, the first available template type will be used
|
||||||
|
SendT(ctx context.Context, channel string, templateID string, data TemplateData, messageType ...MessageType) error
|
||||||
|
|
||||||
// SendTWithProvider sends a message using a template and specific provider
|
// SendTWithProvider sends a message using a template and specific provider
|
||||||
SendTWithProvider(ctx context.Context, providerName string, templateID string, data TemplateData) error
|
// messageType is optional - if not specified, the first available template type will be used
|
||||||
|
SendTWithProvider(ctx context.Context, providerName string, templateID string, data TemplateData, messageType ...MessageType) error
|
||||||
|
|
||||||
// SendTBatch sends multiple messages using the same template with different data
|
// SendTBatch sends multiple messages using the same template with different data
|
||||||
SendTBatch(ctx context.Context, channel string, templateID string, dataList []TemplateData) error
|
// messageType is optional - if not specified, the first available template type will be used
|
||||||
|
SendTBatch(ctx context.Context, channel string, templateID string, dataList []TemplateData, messageType ...MessageType) error
|
||||||
|
|
||||||
// SendTBatchWithProvider sends multiple messages using the same template with different data and specific provider
|
// SendTBatchWithProvider sends multiple messages using the same template with different data and specific provider
|
||||||
SendTBatchWithProvider(ctx context.Context, providerName string, templateID string, dataList []TemplateData) error
|
// messageType is optional - if not specified, the first available template type will be used
|
||||||
|
SendTBatchWithProvider(ctx context.Context, providerName string, templateID string, dataList []TemplateData, messageType ...MessageType) error
|
||||||
|
|
||||||
// SendTBatchMixed sends multiple messages using different templates with different data
|
// SendTBatchMixed sends multiple messages using different templates with different data
|
||||||
SendTBatchMixed(ctx context.Context, channel string, templateRequests []TemplateRequest) error
|
SendTBatchMixed(ctx context.Context, channel string, templateRequests []TemplateRequest) error
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,20 @@ const (
|
||||||
TemplateTypeWhatsApp TemplateType = "whatsapp"
|
TemplateTypeWhatsApp TemplateType = "whatsapp"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// templateTypeToMessageType converts TemplateType to MessageType
|
||||||
|
func templateTypeToMessageType(templateType TemplateType) MessageType {
|
||||||
|
switch templateType {
|
||||||
|
case TemplateTypeMail:
|
||||||
|
return MessageTypeEmail
|
||||||
|
case TemplateTypeSMS:
|
||||||
|
return MessageTypeSMS
|
||||||
|
case TemplateTypeWhatsApp:
|
||||||
|
return MessageTypeWhatsApp
|
||||||
|
default:
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Template represents a message template
|
// Template represents a message template
|
||||||
type Template struct {
|
type Template struct {
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
|
|
@ -71,9 +85,12 @@ func (t *Template) ToMessage(data TemplateData) (*Message, error) {
|
||||||
return nil, fmt.Errorf("template data must include 'to' field with recipients")
|
return nil, fmt.Errorf("template data must include 'to' field with recipients")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Convert TemplateType to MessageType
|
||||||
|
messageType := templateTypeToMessageType(t.Type)
|
||||||
|
|
||||||
// Create message
|
// Create message
|
||||||
message := &Message{
|
message := &Message{
|
||||||
Type: MessageType(t.Type),
|
Type: messageType,
|
||||||
Subject: subject,
|
Subject: subject,
|
||||||
Body: body,
|
Body: body,
|
||||||
HTML: html,
|
HTML: html,
|
||||||
|
|
|
||||||
|
|
@ -128,6 +128,7 @@ type Features struct {
|
||||||
|
|
||||||
// TemplateRequest represents a request to send a message using a specific template
|
// TemplateRequest represents a request to send a message using a specific template
|
||||||
type TemplateRequest struct {
|
type TemplateRequest struct {
|
||||||
TemplateID string `json:"template_id"`
|
TemplateID string `json:"template_id"`
|
||||||
Data TemplateData `json:"data"`
|
Data TemplateData `json:"data"`
|
||||||
|
MessageType *MessageType `json:"message_type,omitempty"` // Optional: if not specified, will use first available template type
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -419,7 +419,7 @@ func GetTeamConfig(locale string) *TeamConfig {
|
||||||
|
|
||||||
// Normalize language code to lowercase
|
// Normalize language code to lowercase
|
||||||
if locale != "" {
|
if locale != "" {
|
||||||
locale = strings.ToLower(locale)
|
locale = strings.TrimSpace(strings.ToLower(locale))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Try to get the specific locale configuration
|
// Try to get the specific locale configuration
|
||||||
|
|
@ -427,7 +427,12 @@ func GetTeamConfig(locale string) *TeamConfig {
|
||||||
return config
|
return config
|
||||||
}
|
}
|
||||||
|
|
||||||
// If no specific locale, try to get any available configuration
|
// If no specific locale, try to get "en" as default
|
||||||
|
if config, exists := teamConfigs["en"]; exists {
|
||||||
|
return config
|
||||||
|
}
|
||||||
|
|
||||||
|
// If "en" is not available, try to get any available configuration
|
||||||
for _, config := range teamConfigs {
|
for _, config := range teamConfigs {
|
||||||
return config
|
return config
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,7 @@ import (
|
||||||
messengertypes "github.com/yaoapp/yao/messenger/types"
|
messengertypes "github.com/yaoapp/yao/messenger/types"
|
||||||
"github.com/yaoapp/yao/openapi/oauth"
|
"github.com/yaoapp/yao/openapi/oauth"
|
||||||
"github.com/yaoapp/yao/openapi/response"
|
"github.com/yaoapp/yao/openapi/response"
|
||||||
|
"github.com/yaoapp/yao/share"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Team Invitation Management Handlers
|
// Team Invitation Management Handlers
|
||||||
|
|
@ -118,6 +119,9 @@ func GinInvitationGet(c *gin.Context) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Extract base URL from request
|
||||||
|
requestBaseURL := getRequestBaseURL(c)
|
||||||
|
|
||||||
// Call business logic
|
// Call business logic
|
||||||
invitationData, err := invitationGet(c.Request.Context(), authInfo.UserID, teamID, invitationID)
|
invitationData, err := invitationGet(c.Request.Context(), authInfo.UserID, teamID, invitationID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -145,8 +149,8 @@ func GinInvitationGet(c *gin.Context) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Convert to response format
|
// Convert to response format (with requestBaseURL for building full invitation link)
|
||||||
invitation := mapToInvitationDetailResponse(invitationData)
|
invitation := mapToInvitationDetailResponse(invitationData, requestBaseURL)
|
||||||
c.JSON(http.StatusOK, invitation)
|
c.JSON(http.StatusOK, invitation)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -184,14 +188,18 @@ func GinInvitationCreate(c *gin.Context) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Extract base URL from request
|
||||||
|
requestBaseURL := getRequestBaseURL(c)
|
||||||
|
|
||||||
// Prepare invitation data
|
// Prepare invitation data
|
||||||
invitationData := maps.MapStrAny{
|
invitationData := maps.MapStrAny{
|
||||||
"user_id": req.UserID,
|
"user_id": req.UserID,
|
||||||
"email": req.Email,
|
"email": req.Email,
|
||||||
"member_type": req.MemberType,
|
"member_type": req.MemberType,
|
||||||
"role_id": req.RoleID,
|
"role_id": req.RoleID,
|
||||||
"message": req.Message,
|
"message": req.Message,
|
||||||
"expiry": req.Expiry,
|
"expiry": req.Expiry,
|
||||||
|
"request_base_url": requestBaseURL,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Prepare settings
|
// Prepare settings
|
||||||
|
|
@ -205,6 +213,11 @@ func GinInvitationCreate(c *gin.Context) {
|
||||||
settings.SendEmail = *req.SendEmail
|
settings.SendEmail = *req.SendEmail
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Add locale from top-level field (for backward compatibility)
|
||||||
|
if req.Locale != "" {
|
||||||
|
settings.Locale = req.Locale
|
||||||
|
}
|
||||||
|
|
||||||
// Add settings to invitation data
|
// Add settings to invitation data
|
||||||
if settings.SendEmail || settings.Locale != "" {
|
if settings.SendEmail || settings.Locale != "" {
|
||||||
invitationData["settings"] = settings
|
invitationData["settings"] = settings
|
||||||
|
|
@ -258,8 +271,8 @@ func GinInvitationCreate(c *gin.Context) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Convert to InvitationResponse
|
// Convert to InvitationResponse (with requestBaseURL for building full invitation link)
|
||||||
invitationResp := convertToInvitationResponse(invitation)
|
invitationResp := convertToInvitationResponse(invitation, requestBaseURL)
|
||||||
|
|
||||||
// Return created invitation with full details (including token)
|
// Return created invitation with full details (including token)
|
||||||
c.JSON(http.StatusCreated, invitationResp)
|
c.JSON(http.StatusCreated, invitationResp)
|
||||||
|
|
@ -290,7 +303,7 @@ func GinInvitationResend(c *gin.Context) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Call business logic
|
// Call business logic
|
||||||
err := invitationResend(c.Request.Context(), authInfo.UserID, teamID, invitationID)
|
err := invitationResend(c.Request.Context(), authInfo.UserID, teamID, invitationID, getRequestBaseURL(c))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error("Failed to resend invitation: %v", err)
|
log.Error("Failed to resend invitation: %v", err)
|
||||||
// Check error type for appropriate response
|
// Check error type for appropriate response
|
||||||
|
|
@ -526,8 +539,8 @@ func ProcessInvitationResend(process *process.Process) interface{} {
|
||||||
ctx = context.Background()
|
ctx = context.Background()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Call business logic
|
// Call business logic (no requestBaseURL available in process context)
|
||||||
err := invitationResend(ctx, userIDStr, teamID, invitationID)
|
err := invitationResend(ctx, userIDStr, teamID, invitationID, "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
exception.New("failed to resend invitation: %s", 500, err.Error()).Throw()
|
exception.New("failed to resend invitation: %s", 500, err.Error()).Throw()
|
||||||
}
|
}
|
||||||
|
|
@ -573,6 +586,75 @@ func ProcessInvitationDelete(process *process.Process) interface{} {
|
||||||
|
|
||||||
// Private Business Logic Functions (internal use only)
|
// Private Business Logic Functions (internal use only)
|
||||||
|
|
||||||
|
// getAdminRoot returns the admin root path from share.App configuration
|
||||||
|
// Similar to service.setupAdminRoot but without caching to avoid circular dependencies
|
||||||
|
func getAdminRoot() string {
|
||||||
|
adminRoot := "/yao/"
|
||||||
|
if share.App.AdminRoot != "" {
|
||||||
|
root := strings.TrimPrefix(share.App.AdminRoot, "/")
|
||||||
|
root = strings.TrimSuffix(root, "/")
|
||||||
|
adminRoot = fmt.Sprintf("/%s/", root)
|
||||||
|
}
|
||||||
|
return adminRoot
|
||||||
|
}
|
||||||
|
|
||||||
|
// getRequestBaseURL extracts the base URL from the gin context request
|
||||||
|
// Returns: scheme://host (e.g., "https://example.com" or "http://localhost:8000")
|
||||||
|
func getRequestBaseURL(c *gin.Context) string {
|
||||||
|
if c == nil || c.Request == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
scheme := "http"
|
||||||
|
if c.Request.TLS != nil {
|
||||||
|
scheme = "https"
|
||||||
|
}
|
||||||
|
// Check X-Forwarded-Proto header
|
||||||
|
if proto := c.GetHeader("X-Forwarded-Proto"); proto != "" {
|
||||||
|
scheme = proto
|
||||||
|
}
|
||||||
|
|
||||||
|
host := c.Request.Host
|
||||||
|
if host == "" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Sprintf("%s://%s", scheme, host)
|
||||||
|
}
|
||||||
|
|
||||||
|
// buildInvitationLink constructs a full invitation link from invitation_id, token and team configuration
|
||||||
|
// This is a centralized function to ensure consistency across email sending and link generation
|
||||||
|
// Format:
|
||||||
|
// - With team config baseURL: {base_url}/{invitation_id}/{token}
|
||||||
|
// - With requestBaseURL (from HTTP request): {scheme}://{host}{AdminRoot}team/invite/{invitation_id}/{token}
|
||||||
|
// - Without any baseURL (fallback): {AdminRoot}team/invite/{invitation_id}/{token}
|
||||||
|
func buildInvitationLink(invitationID, token string, teamConfig *TeamConfig, requestBaseURL string) string {
|
||||||
|
// Priority 1: Use team config baseURL if specified
|
||||||
|
if teamConfig != nil && teamConfig.Invite != nil && teamConfig.Invite.BaseURL != "" {
|
||||||
|
baseURL := teamConfig.Invite.BaseURL
|
||||||
|
// Ensure baseURL ends with /
|
||||||
|
if !strings.HasSuffix(baseURL, "/") {
|
||||||
|
baseURL = baseURL + "/"
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("%s%s/%s", baseURL, invitationID, token)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get admin root from configuration
|
||||||
|
adminRoot := getAdminRoot()
|
||||||
|
// Ensure adminRoot doesn't end with / for URL construction
|
||||||
|
adminRoot = strings.TrimSuffix(adminRoot, "/")
|
||||||
|
|
||||||
|
// Priority 2: Use request baseURL with AdminRoot
|
||||||
|
if requestBaseURL != "" {
|
||||||
|
// Ensure requestBaseURL doesn't end with /
|
||||||
|
requestBaseURL = strings.TrimSuffix(requestBaseURL, "/")
|
||||||
|
return fmt.Sprintf("%s%s/team/invite/%s/%s", requestBaseURL, adminRoot, invitationID, token)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Priority 3: Fallback to relative path with AdminRoot
|
||||||
|
return fmt.Sprintf("%s/team/invite/%s/%s", adminRoot, invitationID, token)
|
||||||
|
}
|
||||||
|
|
||||||
// invitationList handles the business logic for listing team invitations
|
// invitationList handles the business logic for listing team invitations
|
||||||
func invitationList(ctx context.Context, userID, teamID string, page, pagesize int, status string) (maps.MapStr, error) {
|
func invitationList(ctx context.Context, userID, teamID string, page, pagesize int, status string) (maps.MapStr, error) {
|
||||||
// Check if user has access to the team (read permission: owner or member)
|
// Check if user has access to the team (read permission: owner or member)
|
||||||
|
|
@ -762,6 +844,10 @@ func invitationCreate(ctx context.Context, userID, teamID string, invitationData
|
||||||
return "", fmt.Errorf("failed to parse expiry duration: %w", err)
|
return "", fmt.Errorf("failed to parse expiry duration: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Save request_base_url and settings before database operation (they will be lost in DB)
|
||||||
|
requestBaseURL := toString(invitationData["request_base_url"])
|
||||||
|
savedSettings := invitationData["settings"] // Save settings reference
|
||||||
|
|
||||||
// Set invitation-specific fields
|
// Set invitation-specific fields
|
||||||
invitationData["team_id"] = teamID
|
invitationData["team_id"] = teamID
|
||||||
if invitationData["member_type"] == nil || invitationData["member_type"] == "" {
|
if invitationData["member_type"] == nil || invitationData["member_type"] == "" {
|
||||||
|
|
@ -792,21 +878,34 @@ func invitationCreate(ctx context.Context, userID, teamID string, invitationData
|
||||||
|
|
||||||
// Send email if requested (shouldSendEmail was already determined earlier)
|
// Send email if requested (shouldSendEmail was already determined earlier)
|
||||||
if shouldSendEmail {
|
if shouldSendEmail {
|
||||||
err = sendInvitationEmail(ctx, inviteeEmail, inviterName, teamName, token, invitationID, invitationData)
|
// Use the saved requestBaseURL and settings (not from invitationData, as they were lost in DB operation)
|
||||||
if err != nil {
|
// Send email asynchronously to improve user experience
|
||||||
log.Error("Failed to send invitation email: %v", err)
|
go func() {
|
||||||
// Don't fail the invitation creation if email fails
|
// Use background context for async operation
|
||||||
// The invitation link can still be shared manually
|
bgCtx := context.Background()
|
||||||
} else {
|
|
||||||
log.Info("Invitation email sent to %s for team %s (invitation_id: %s)", inviteeEmail, teamName, invitationID)
|
// Ensure request_base_url and settings are in invitationData for email sending
|
||||||
}
|
emailData := maps.MapStrAny{}
|
||||||
|
for k, v := range invitationData {
|
||||||
|
emailData[k] = v
|
||||||
|
}
|
||||||
|
emailData["request_base_url"] = requestBaseURL
|
||||||
|
emailData["settings"] = savedSettings // Restore settings
|
||||||
|
|
||||||
|
err := sendInvitationEmail(bgCtx, inviteeEmail, inviterName, teamName, token, invitationID, emailData)
|
||||||
|
if err != nil {
|
||||||
|
log.Error("Failed to send invitation email: %v", err)
|
||||||
|
} else {
|
||||||
|
log.Info("Invitation email sent to %s for team %s (invitation_id: %s)", inviteeEmail, teamName, invitationID)
|
||||||
|
}
|
||||||
|
}()
|
||||||
}
|
}
|
||||||
|
|
||||||
return invitationID, nil
|
return invitationID, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// invitationResend handles the business logic for resending a team invitation
|
// invitationResend handles the business logic for resending a team invitation
|
||||||
func invitationResend(ctx context.Context, userID, teamID, invitationID string) error {
|
func invitationResend(ctx context.Context, userID, teamID, invitationID, requestBaseURL string) error {
|
||||||
// Check if user has access to the team (write permission: owner only)
|
// Check if user has access to the team (write permission: owner only)
|
||||||
isOwner, _, err := checkTeamAccess(ctx, teamID, userID)
|
isOwner, _, err := checkTeamAccess(ctx, teamID, userID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -889,6 +988,7 @@ func invitationResend(ctx context.Context, userID, teamID, invitationID string)
|
||||||
// Update the invitation data for email sending
|
// Update the invitation data for email sending
|
||||||
invitationData["invitation_token"] = newToken
|
invitationData["invitation_token"] = newToken
|
||||||
invitationData["invitation_expires_at"] = newExpiryTime
|
invitationData["invitation_expires_at"] = newExpiryTime
|
||||||
|
invitationData["request_base_url"] = requestBaseURL
|
||||||
|
|
||||||
// Get email from invitation data
|
// Get email from invitation data
|
||||||
var inviteeEmail string
|
var inviteeEmail string
|
||||||
|
|
@ -902,13 +1002,18 @@ func invitationResend(ctx context.Context, userID, teamID, invitationID string)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Send new invitation email if email is available
|
// Send new invitation email if email is available (asynchronously)
|
||||||
if inviteeEmail != "" {
|
if inviteeEmail != "" {
|
||||||
err = sendInvitationEmail(ctx, inviteeEmail, inviterName, teamName, newToken, invitationID, invitationData)
|
go func() {
|
||||||
if err != nil {
|
// Use background context for async operation
|
||||||
log.Error("Failed to resend invitation email: %v", err)
|
bgCtx := context.Background()
|
||||||
// Don't fail the resend if email fails
|
err := sendInvitationEmail(bgCtx, inviteeEmail, inviterName, teamName, newToken, invitationID, invitationData)
|
||||||
}
|
if err != nil {
|
||||||
|
log.Error("Failed to resend invitation email: %v", err)
|
||||||
|
} else {
|
||||||
|
log.Info("Invitation email resent to %s for team %s (invitation_id: %s)", inviteeEmail, teamName, invitationID)
|
||||||
|
}
|
||||||
|
}()
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
|
@ -1044,6 +1149,7 @@ func sendInvitationEmail(ctx context.Context, email, inviterName, teamName, toke
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get team config for email template and channel
|
// Get team config for email template and channel
|
||||||
|
// Note: GetTeamConfig will normalize locale internally (trim, lowercase, etc.)
|
||||||
teamConfig := GetTeamConfig(locale)
|
teamConfig := GetTeamConfig(locale)
|
||||||
if teamConfig == nil || teamConfig.Invite == nil {
|
if teamConfig == nil || teamConfig.Invite == nil {
|
||||||
return fmt.Errorf("team configuration not found for locale: %s", locale)
|
return fmt.Errorf("team configuration not found for locale: %s", locale)
|
||||||
|
|
@ -1069,35 +1175,41 @@ func sendInvitationEmail(ctx context.Context, email, inviterName, teamName, toke
|
||||||
// Get custom message from invitation data
|
// Get custom message from invitation data
|
||||||
customMessage := toString(invitationData["message"])
|
customMessage := toString(invitationData["message"])
|
||||||
|
|
||||||
|
// Get request base URL from invitation data (if provided)
|
||||||
|
requestBaseURL := toString(invitationData["request_base_url"])
|
||||||
|
|
||||||
|
// Build invitation link using centralized helper function
|
||||||
|
invitationLink := buildInvitationLink(invitationID, token, teamConfig, requestBaseURL)
|
||||||
|
|
||||||
// Prepare template data for messenger
|
// Prepare template data for messenger
|
||||||
templateData := messengertypes.TemplateData{
|
templateData := messengertypes.TemplateData{
|
||||||
"to": email,
|
"to": email,
|
||||||
"inviter_name": inviterName,
|
"inviter_name": inviterName,
|
||||||
"team_name": teamName,
|
"team_name": teamName,
|
||||||
"invitation_id": invitationID,
|
"invitation_id": invitationID,
|
||||||
"token": token,
|
"invitation_link": invitationLink, // Full invitation link
|
||||||
"message": customMessage,
|
"token": token, // Keep token for backward compatibility
|
||||||
"role_id": toString(invitationData["role_id"]),
|
"message": customMessage,
|
||||||
"expires_at": toString(invitationData["invitation_expires_at"]),
|
"role_id": toString(invitationData["role_id"]),
|
||||||
|
"expires_at": toString(invitationData["invitation_expires_at"]),
|
||||||
}
|
}
|
||||||
|
|
||||||
// Send email using messenger template
|
// Send email using messenger template
|
||||||
err := messenger.Instance.SendT(ctx, channel, emailTemplate, templateData)
|
err := messenger.Instance.SendT(ctx, channel, emailTemplate, templateData, messengertypes.MessageTypeEmail)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to send invitation email: %w", err)
|
return fmt.Errorf("failed to send invitation email: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Info("Invitation email sent to %s for team %s (invitation_id: %s)", email, teamName, invitationID)
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// convertToInvitationResponse converts a map to InvitationResponse (alias for mapToInvitationResponse)
|
// convertToInvitationResponse converts a map to InvitationResponse (alias for mapToInvitationResponse)
|
||||||
func convertToInvitationResponse(data maps.MapStrAny) InvitationResponse {
|
func convertToInvitationResponse(data maps.MapStrAny, requestBaseURL string) InvitationResponse {
|
||||||
return mapToInvitationResponse(maps.MapStr(data))
|
return mapToInvitationResponse(maps.MapStr(data), requestBaseURL)
|
||||||
}
|
}
|
||||||
|
|
||||||
// mapToInvitationResponse converts a map to InvitationResponse
|
// mapToInvitationResponse converts a map to InvitationResponse
|
||||||
func mapToInvitationResponse(data maps.MapStr) InvitationResponse {
|
func mapToInvitationResponse(data maps.MapStr, requestBaseURL string) InvitationResponse {
|
||||||
invitation := InvitationResponse{
|
invitation := InvitationResponse{
|
||||||
ID: toInt64(data["id"]),
|
ID: toInt64(data["id"]),
|
||||||
InvitationID: toString(data["invitation_id"]),
|
InvitationID: toString(data["invitation_id"]),
|
||||||
|
|
@ -1116,9 +1228,13 @@ func mapToInvitationResponse(data maps.MapStr) InvitationResponse {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add settings if available
|
// Add settings if available
|
||||||
|
locale := "en" // Default locale
|
||||||
if settings, ok := data["settings"]; ok {
|
if settings, ok := data["settings"]; ok {
|
||||||
if invSettings, ok := settings.(*InvitationSettings); ok {
|
if invSettings, ok := settings.(*InvitationSettings); ok {
|
||||||
invitation.Settings = invSettings
|
invitation.Settings = invSettings
|
||||||
|
if invSettings.Locale != "" {
|
||||||
|
locale = invSettings.Locale
|
||||||
|
}
|
||||||
} else if settingsMap, ok := settings.(map[string]interface{}); ok {
|
} else if settingsMap, ok := settings.(map[string]interface{}); ok {
|
||||||
// Convert map to InvitationSettings
|
// Convert map to InvitationSettings
|
||||||
invSettings := &InvitationSettings{
|
invSettings := &InvitationSettings{
|
||||||
|
|
@ -1126,16 +1242,25 @@ func mapToInvitationResponse(data maps.MapStr) InvitationResponse {
|
||||||
Locale: toString(settingsMap["locale"]),
|
Locale: toString(settingsMap["locale"]),
|
||||||
}
|
}
|
||||||
invitation.Settings = invSettings
|
invitation.Settings = invSettings
|
||||||
|
if invSettings.Locale != "" {
|
||||||
|
locale = invSettings.Locale
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Build invitation link if token is available
|
||||||
|
if invitation.InvitationToken != "" && invitation.InvitationID != "" {
|
||||||
|
teamConfig := GetTeamConfig(locale)
|
||||||
|
invitation.InvitationLink = buildInvitationLink(invitation.InvitationID, invitation.InvitationToken, teamConfig, requestBaseURL)
|
||||||
|
}
|
||||||
|
|
||||||
return invitation
|
return invitation
|
||||||
}
|
}
|
||||||
|
|
||||||
// mapToInvitationDetailResponse converts a map to InvitationDetailResponse
|
// mapToInvitationDetailResponse converts a map to InvitationDetailResponse
|
||||||
func mapToInvitationDetailResponse(data maps.MapStr) InvitationDetailResponse {
|
func mapToInvitationDetailResponse(data maps.MapStr, requestBaseURL string) InvitationDetailResponse {
|
||||||
invitation := InvitationDetailResponse{
|
invitation := InvitationDetailResponse{
|
||||||
InvitationResponse: mapToInvitationResponse(data),
|
InvitationResponse: mapToInvitationResponse(data, requestBaseURL),
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add user info if available (could be joined from user table)
|
// Add user info if available (could be joined from user table)
|
||||||
|
|
|
||||||
|
|
@ -29,6 +29,10 @@ func GinTeamConfig(c *gin.Context) {
|
||||||
locale = "en" // default locale
|
locale = "en" // default locale
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Clean locale: remove whitespace and special characters
|
||||||
|
locale = strings.TrimSpace(locale)
|
||||||
|
locale = strings.Trim(locale, "?&=")
|
||||||
|
|
||||||
config := GetTeamConfig(locale)
|
config := GetTeamConfig(locale)
|
||||||
if config == nil {
|
if config == nil {
|
||||||
c.JSON(http.StatusNotFound, gin.H{"error": "Team configuration not found"})
|
c.JSON(http.StatusNotFound, gin.H{"error": "Team configuration not found"})
|
||||||
|
|
|
||||||
|
|
@ -315,6 +315,7 @@ type InvitationResponse struct {
|
||||||
InvitedBy string `json:"invited_by"`
|
InvitedBy string `json:"invited_by"`
|
||||||
InvitedAt string `json:"invited_at"`
|
InvitedAt string `json:"invited_at"`
|
||||||
InvitationToken string `json:"invitation_token,omitempty"`
|
InvitationToken string `json:"invitation_token,omitempty"`
|
||||||
|
InvitationLink string `json:"invitation_link,omitempty"` // Full invitation link
|
||||||
InvitationExpiresAt string `json:"invitation_expires_at,omitempty"`
|
InvitationExpiresAt string `json:"invitation_expires_at,omitempty"`
|
||||||
Message string `json:"message,omitempty"`
|
Message string `json:"message,omitempty"`
|
||||||
Settings *InvitationSettings `json:"settings,omitempty"`
|
Settings *InvitationSettings `json:"settings,omitempty"`
|
||||||
|
|
@ -339,6 +340,7 @@ type CreateInvitationRequest struct {
|
||||||
Message string `json:"message,omitempty"`
|
Message string `json:"message,omitempty"`
|
||||||
Expiry string `json:"expiry,omitempty"` // Custom expiry duration (e.g., "1d", "8h"), defaults to team config
|
Expiry string `json:"expiry,omitempty"` // Custom expiry duration (e.g., "1d", "8h"), defaults to team config
|
||||||
SendEmail *bool `json:"send_email,omitempty"` // Whether to send email (defaults to false)
|
SendEmail *bool `json:"send_email,omitempty"` // Whether to send email (defaults to false)
|
||||||
|
Locale string `json:"locale,omitempty"` // Language code for email template (e.g., "zh-CN", "en")
|
||||||
Settings *InvitationSettings `json:"settings,omitempty"`
|
Settings *InvitationSettings `json:"settings,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue