# Text Message

## Description

Message providers in FlexBase expose a unified interface (`IFlexTextMessageProvider`) for sending SMS and notifications across providers like Twilio, Firebase, and AWS SNS.

Your application code should depend on `IFlexTextMessageProvider`.

## Important concepts

* **`IFlexTextMessageProvider` is the contract**: send via `SendSmsAsync(...)`, `SendPushAsync(...)`, or `SendAsync(...)`.
* **Provider bridge**: generated infrastructure commonly registers `IFlexTextMessageProviderBridge` (which also implements `IFlexTextMessageProvider`) so behavior can be overridden safely.
* **Channel support is provider-specific**: use `SupportedChannels` / `SupportsChannel(...)` to validate capability.

## Configuration in DI

Only register the provider—Flex auto-wires generated PostBus handlers/plugins that consume the interface.

```csharp
public static class OtherApplicationServicesConfig
{
	public static IServiceCollection AddOtherApplicationServices(
		this IServiceCollection services,
		IConfiguration configuration)
	{
		// Pick ONE (or register multiple with different compositions).
		services.AddFlexConsoleMessageProvider(configuration);
		// services.AddFlexTwilioMessageProvider(configuration);
		// services.AddFlexFirebaseMessageProvider(configuration);
		// services.AddFlexAwsSnsMessageProvider(configuration);
		return services;
	}
}
```

## appsettings.json

Message provider configuration is read from `FlexBase:Providers:Message:<Provider>`.

```json
{
    "FlexBase": {
	"Providers": {
	  "Message": {
		"Console": {
		  "DefaultChannel": "Sms",
		  "DefaultSenderId": "MYAPP",
		  "SimulateDelay": true,
		  "SimulatedDelay": "00:00:00.500"
		},
		"Twilio": {
		  "AccountSid": "<store-in-secrets>",
		  "AuthToken": "<store-in-secrets>",
		  "DefaultFromNumber": "+15551234567",
		  "MessagingServiceSid": null,
		  "MaxRetries": 3,
		  "Timeout": "00:00:30"
		},
		"Firebase": {
		  "ProjectId": "<your-project-id>",
		  "CredentialsJsonPath": null,
		  "CredentialsJson": null,
		  "DefaultTopic": null,
		  "MaxRetries": 3,
		  "Timeout": "00:00:30"
		},
		"AwsSns": {
		  "AccessKeyId": "<store-in-secrets>",
		  "SecretAccessKey": "<store-in-secrets>",
		  "Region": "us-east-1",
		  "DefaultSenderId": "MYAPP",
		  "DefaultTopicArn": null,
		  "SmsType": "Transactional",
		  "MaxRetries": 3,
		  "Timeout": "00:00:30"
		}
	  }
	}
    }
}
```

## Examples (template-based)

This mirrors the generated PostBus handler template shape. You do **not** register the handler manually.

```csharp
using Microsoft.Extensions.Logging;
using Sumeru.Flex;
using System.Threading.Tasks;

namespace {YourApplication}.PostBusHandlers.Message;

public partial class SendSmsHandler : ISendSmsHandler
{
	protected string EventCondition = string.Empty;

	protected readonly ILogger<SendSmsHandler> _logger;
	protected readonly IFlexHost _flexHost;
	protected readonly IFlexTextMessageProvider _messageProvider;

	protected FlexAppContextBridge? _flexAppContext;

	public SendSmsHandler(
		ILogger<SendSmsHandler> logger,
		IFlexHost flexHost,
		IFlexTextMessageProvider messageProvider)
	{
		_logger = logger;
		_flexHost = flexHost;
		_messageProvider = messageProvider;
	}

	public virtual async Task Execute(SendSmsCommand cmd, IFlexServiceBusContext serviceBusContext)
	{
		_flexAppContext = cmd.Dto.GetAppContext();  //do not remove this line

		var result = await _messageProvider.SendSmsAsync(
			phoneNumber: cmd.Dto.PhoneNumber,
			content: cmd.Dto.Content,
			senderId: cmd.Dto.SenderId);

		cmd.Dto.MessageId = result.MessageId;
		cmd.Dto.IsSuccess = result.IsSuccess;

		await this.Fire(EventCondition, serviceBusContext);
	}
}
```

## Provider considerations

* Validate channel support via `SupportsChannel(...)` before sending.
* Keep secrets (Twilio/AuthToken, AWS keys, Firebase credentials) out of source control. \["action"] = "verify", \["userId"] = 12345 }, Metadata = new Dictionary\<string, string> // Tracking metadata { \["campaign"] = "signup", \["source"] = "web" } };

  var result = await \_messageProvider.SendAsync(message); }

````

### Templated Messages

```csharp
public async Task SendTemplatedSmsAsync(string phoneNumber, string userName)
{
    await _messageProvider.SendTemplatedAsync(
        recipient: phoneNumber,
        templateId: "order_shipped",
        templateVariables: new Dictionary<string, string>
        {
            ["userName"] = userName,
            ["orderNumber"] = "ORD-12345",
            ["carrier"] = "FedEx",
            ["trackingUrl"] = "https://track.example.com/123"
        },
        channel: FlexMessageChannel.Sms
    );
}
````

### Bulk Messaging

```csharp
// Send same message to multiple recipients
public async Task SendBulkSmsAsync(List<string> phoneNumbers, string message)
{
    var result = await _messageProvider.SendToManyAsync(
        recipients: phoneNumbers,
        content: message,
        channel: FlexMessageChannel.Sms
    );

    _logger.LogInformation(
        "Sent SMS to {SuccessCount}/{TotalCount} recipients",
        result.SuccessCount,
        result.TotalCount
    );

    // Log failures
    foreach (var failure in result.Results.Where(r => !r.IsSuccess))
    {
        _logger.LogWarning(
            "Failed to send to {Recipient}: {Error}",
            failure.Recipient,
            failure.ErrorMessage
        );
    }
}

// Send different messages
public async Task SendPersonalizedMessagesAsync(List<User> users)
{
    var messages = users.Select(user => new FlexMessage
    {
        Recipients = new List<string> { user.PhoneNumber },
        Content = $"Hello {user.Name}, your order is ready!",
        Channel = FlexMessageChannel.Sms
    });

    var result = await _messageProvider.SendBulkAsync(messages);
}
```

### Slack/Teams Integration

```csharp
// Slack message
public async Task SendSlackNotificationAsync(string channel, string message)
{
    var slackMessage = new FlexMessage
    {
        Recipients = new List<string> { channel },  // "#general" or "U1234567890"
        Content = "Deployment to production completed successfully! ✅",
        Channel = FlexMessageChannel.Slack,
        Data = new Dictionary<string, object>
        {
            ["blocks"] = new[]
            {
                new { type = "header", text = new { type = "plain_text", text = "Deployment Complete" } },
                new { type = "section", text = new { type = "mrkdwn", text = "*Version:* 1.2.3\n*Environment:* Production" } }
            }
        }
    };

    await _messageProvider.SendAsync(slackMessage);
}

// Teams message
public async Task SendTeamsNotificationAsync(string channelId, string message)
{
    var teamsMessage = new FlexMessage
    {
        Recipients = new List<string> { channelId },
        Title = "Build Failed",
        Content = "The build for branch `main` has failed.",
        Channel = FlexMessageChannel.Teams,
        Priority = FlexMessagePriority.Urgent
    };

    await _messageProvider.SendAsync(teamsMessage);
}
```

## Key Points to Consider

### Best Practices

1. **Validate Recipients** - Use `ValidateRecipient()` before sending
2. **Handle Delivery Status** - Check `IsSuccess` and log failures
3. **Use Console in Dev** - Avoid sending real messages during development
4. **Set Priorities** - Use `High` priority for time-sensitive messages
5. **Include Expiry** - Set `ExpiresAt` for time-sensitive content
6. **Handle Rate Limits** - Implement retry logic for `RATE_LIMITED` errors
7. **Monitor Costs** - Track `Cost` in results for paid providers

### Validation

```csharp
public async Task<bool> SendIfValidAsync(string phoneNumber, string message)
{
    if (!_messageProvider.ValidateRecipient(phoneNumber, FlexMessageChannel.Sms))
    {
        _logger.LogWarning("Invalid phone number: {PhoneNumber}", phoneNumber);
        return false;
    }

    var result = await _messageProvider.SendSmsAsync(phoneNumber, message);
    return result.IsSuccess;
}

// Check channel support
public async Task SendToSupportedChannelAsync(string recipient, string message)
{
    if (_messageProvider.SupportsChannel(FlexMessageChannel.WhatsApp))
    {
        await _messageProvider.SendAsync(recipient, message, FlexMessageChannel.WhatsApp);
    }
    else
    {
        // Fallback to SMS
        await _messageProvider.SendSmsAsync(recipient, message);
    }
}
```

### Error Handling

```csharp
public async Task<bool> SendWithRetryAsync(FlexMessage message, int maxRetries = 3)
{
    for (int attempt = 1; attempt <= maxRetries; attempt++)
    {
        try
        {
            var result = await _messageProvider.SendAsync(message);
            
            if (result.IsSuccess)
            {
                return true;
            }

            switch (result.ErrorCode)
            {
                case "INVALID_RECIPIENT":
                    _logger.LogError("Invalid recipient, not retrying");
                    return false;
                
                case "RATE_LIMITED":
                    _logger.LogWarning("Rate limited, waiting before retry");
                    await Task.Delay(TimeSpan.FromSeconds(5 * attempt));
                    break;
                
                case "INSUFFICIENT_FUNDS":
                    _logger.LogError("Insufficient account balance");
                    return false;
                
                default:
                    _logger.LogWarning(
                        "Send failed (attempt {Attempt}/{Max}): {Error}",
                        attempt, maxRetries, result.ErrorMessage
                    );
                    break;
            }
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Unexpected error sending message");
            
            if (attempt == maxRetries)
                throw;
            
            await Task.Delay(TimeSpan.FromSeconds(2 * attempt));
        }
    }

    return false;
}
```

### Testing Connection

```csharp
// In startup/health check
public async Task<bool> VerifyMessageServiceAsync()
{
    var isConnected = await _messageProvider.TestConnectionAsync();
    
    if (!isConnected)
    {
        _logger.LogError("Message provider is not configured correctly");
        return false;
    }

    _logger.LogInformation(
        "Message provider connected: {Provider}, Channels: {Channels}",
        _messageProvider.ProviderName,
        string.Join(", ", _messageProvider.SupportedChannels)
    );
    
    return true;
}
```

## Examples

### Complete Notification Service

```csharp
public class NotificationService : INotificationService
{
    private readonly IFlexTextMessageProvider _messageProvider;
    private readonly ILogger<NotificationService> _logger;

    public NotificationService(
        IFlexTextMessageProvider messageProvider,
        ILogger<NotificationService> logger)
    {
        _messageProvider = messageProvider;
        _logger = logger;
    }

    public async Task<bool> SendVerificationCodeAsync(
        string phoneNumber,
        string code)
    {
        try
        {
            var result = await _messageProvider.SendSmsAsync(
                phoneNumber: phoneNumber,
                content: $"Your verification code is: {code}. Valid for 10 minutes.",
                senderId: "MYAPP"
            );

            if (result.IsSuccess)
            {
                _logger.LogInformation(
                    "Verification code sent to {PhoneNumber} (MessageId: {MessageId})",
                    phoneNumber, result.MessageId
                );
                return true;
            }

            _logger.LogError(
                "Failed to send verification code to {PhoneNumber}: {Error}",
                phoneNumber, result.ErrorMessage
            );
            return false;
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Error sending verification code to {PhoneNumber}", phoneNumber);
            return false;
        }
    }

    public async Task SendOrderNotificationAsync(Order order)
    {
        var message = $"Your order #{order.Id} has been confirmed. Total: ${order.Total:F2}";

        // Send SMS if phone number available
        if (!string.IsNullOrEmpty(order.User.PhoneNumber))
        {
            await _messageProvider.SendSmsAsync(order.User.PhoneNumber, message);
        }

        // Send push notification if device token available
        if (!string.IsNullOrEmpty(order.User.DeviceToken))
        {
            await _messageProvider.SendPushAsync(
                order.User.DeviceToken,
                "Order Confirmed",
                message,
                new Dictionary<string, object>
                {
                    ["orderId"] = order.Id,
                    ["action"] = "view_order"
                }
            );
        }
    }
}
```

### Multi-Channel Alert System

```csharp
public class AlertService
{
    private readonly IFlexTextMessageProvider _messageProvider;

	public AlertService(IFlexTextMessageProvider messageProvider)
		=> _messageProvider = messageProvider;

    public async Task SendCriticalAlertAsync(string title, string message)
    {
        var tasks = new List<Task>();

        // SMS to on-call engineers
        tasks.Add(_messageProvider.SendToManyAsync(
            GetOnCallPhoneNumbers(),
            $"[CRITICAL] {title}: {message}",
            FlexMessageChannel.Sms
        ));

        // Slack notification
        tasks.Add(_messageProvider.SendAsync(
            "#alerts",
            $"🚨 *{title}*\n{message}",
            FlexMessageChannel.Slack
        ));

        // Teams notification
        tasks.Add(_messageProvider.SendAsync(
            GetTeamsChannelId(),
            message,
            FlexMessageChannel.Teams
        ));

        await Task.WhenAll(tasks);
    }
}
```

## Testing

```csharp
public class MessageServiceTests
{
    [Fact]
    public async Task SendSmsAsync_WithValidNumber_Succeeds()
    {
        // Arrange
        var provider = new FlexConsoleTextMessageProvider(
            Options.Create(new FlexConsoleMessageOptions())
        );

        // Act
        var result = await provider.SendSmsAsync(
            "+1234567890",
            "Test message"
        );

        // Assert
        Assert.True(result.IsSuccess);
        Assert.NotNull(result.MessageId);
    }

    [Fact]
    public void ValidateRecipient_WithInvalidNumber_ReturnsFalse()
    {
        // Arrange
        var provider = new FlexConsoleTextMessageProvider(
            Options.Create(new FlexConsoleMessageOptions())
        );

        // Act
        var isValid = provider.ValidateRecipient("invalid", FlexMessageChannel.Sms);

        // Assert
        Assert.False(isValid);
    }
}
```

## See Also

* [Email Providers](/data-and-providers/providers/e-mail.md) - Email sending
* [FlexMessage Provider Reference](https://github.com/sumeru-flexbase/flexbase-docs/blob/main/ReferenceDocs/README_FlexMessageProvider.md) - Detailed API documentation


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://docs.flexbase.in/data-and-providers/providers/text-message.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
