Webiant Logo Webiant Logo
  1. No results found.

    Try your search with a different keyword or use * as a wildcard.

BrevoManager.cs

using System.Net;
using System.Text.RegularExpressions;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Infrastructure;
using Microsoft.AspNetCore.Mvc.Routing;
using Newtonsoft.Json;
using Nop.Core;
using Nop.Core.Domain.Localization;
using Nop.Core.Domain.Messages;
using Nop.Core.Domain.Orders;
using Nop.Plugin.Misc.Brevo.Domain;
using Nop.Services.Common;
using Nop.Services.Configuration;
using Nop.Services.Customers;
using Nop.Services.Directory;
using Nop.Services.Installation;
using Nop.Services.Localization;
using Nop.Services.Logging;
using Nop.Services.Messages;
using Nop.Services.Stores;
using sib_api_v3_sdk.Api;
using sib_api_v3_sdk.Client;
using sib_api_v3_sdk.Model;
using static sib_api_v3_sdk.Model.GetAttributesAttributes;

namespace Nop.Plugin.Misc.Brevo.Services;

/// 
/// Represents Brevo manager
/// 
public partial class BrevoManager
{
    #region Fields

    protected readonly IActionContextAccessor _actionContextAccessor;
    protected readonly ICountryService _countryService;
    protected readonly ICustomerService _customerService;
    protected readonly IEmailAccountService _emailAccountService;
    protected readonly IGenericAttributeService _genericAttributeService;
    protected readonly ILanguageService _languageService;
    protected readonly ILogger _logger;
    protected readonly INewsLetterSubscriptionService _newsLetterSubscriptionService;
    protected readonly ISettingService _settingService;
    protected readonly IStateProvinceService _stateProvinceService;
    protected readonly IStoreService _storeService;
    protected readonly IUrlHelperFactory _urlHelperFactory;
    protected readonly IWebHelper _webHelper;
    protected readonly IWorkContext _workContext;

    #endregion

    #region Ctor

    public BrevoManager(IActionContextAccessor actionContextAccessor,
        ICountryService countryService,
        ICustomerService customerService,
        IEmailAccountService emailAccountService,
        IGenericAttributeService genericAttributeService,
        ILanguageService languageService,
        ILogger logger,
        INewsLetterSubscriptionService newsLetterSubscriptionService,
        ISettingService settingService,
        IStateProvinceService stateProvinceService,
        IStoreService storeService,
        IUrlHelperFactory urlHelperFactory,
        IWebHelper webHelper,
        IWorkContext workContext)
    {
        _actionContextAccessor = actionContextAccessor;
        _countryService = countryService;
        _customerService = customerService;
        _emailAccountService = emailAccountService;
        _genericAttributeService = genericAttributeService;
        _languageService = languageService;
        _logger = logger;
        _newsLetterSubscriptionService = newsLetterSubscriptionService;
        _settingService = settingService;
        _stateProvinceService = stateProvinceService;
        _storeService = storeService;
        _urlHelperFactory = urlHelperFactory;
        _webHelper = webHelper;
        _workContext = workContext;
    }

    #endregion

    #region Utilities

    /// 
    /// Handle function and get result
    /// 
    /// Result type
    /// Function
    /// Whether to log errors
    /// 
    /// A task that represents the asynchronous operation
    /// The task result contains the result; error if exists
    /// 
    private async Task<(TResult Result, string Error)> HandleFunctionAsync(Func> function, bool logErrors = true)
    {
        try
        {
            //whether plugin is configured
            var brevoSettings = await _settingService.LoadSettingAsync();
            if (string.IsNullOrEmpty(brevoSettings.ApiKey))
                throw new NopException("Plugin not configured");

            return (await function(), default);
        }
        catch (Exception exception)
        {
            var errorMessage = exception.Message;
            if (logErrors)
            {
                var logMessage = $"{BrevoDefaults.SystemName} error: {Environment.NewLine}{errorMessage}";
                await _logger.ErrorAsync(logMessage, exception, await _workContext.GetCurrentCustomerAsync());
            }

            return (default, errorMessage);
        }
    }

    /// 
    /// Prepare API client
    /// 
    /// 
    /// A task that represents the asynchronous operation
    /// The task result contains the aPI client
    /// 
    protected async Task CreateApiClientAsync(Func clientCtor) where TClient : IApiAccessor
    {
        //check whether plugin is configured to request services (validate API key)
        var brevoSettings = await _settingService.LoadSettingAsync();
        if (string.IsNullOrEmpty(brevoSettings.ApiKey))
            throw new NopException($"Plugin not configured");

        var apiConfiguration = new Configuration()
        {
            ApiKey = new Dictionary
            {
                [BrevoDefaults.ApiKeyHeader] = brevoSettings.ApiKey,
                [BrevoDefaults.PartnerKeyHeader] = brevoSettings.ApiKey
            },
            ApiKeyPrefix = new Dictionary { [BrevoDefaults.PartnerKeyHeader] = BrevoDefaults.PartnerName },
            UserAgent = BrevoDefaults.UserAgent
        };

        return clientCtor(apiConfiguration);
    }

    /// 
    /// Import contacts from passed stores to account
    /// 
    /// List of store identifiers
    /// 
    /// A task that represents the asynchronous operation
    /// The task result contains the list of messages
    /// 
    protected async Task> ImportContactsAsync(IList storeIds)
    {
        var messages = new List<(NotifyType, string)>();

        //import contacts to account
        try
        {
            //create API client
            var client = await CreateApiClientAsync(config => new ContactsApi(config));

            foreach (var storeId in storeIds)
            {
                //get list identifier from the settings
                var key = $"{nameof(BrevoSettings)}.{nameof(BrevoSettings.ListId)}";
                var listId = await _settingService.GetSettingByKeyAsync(key, storeId: storeId);
                if (listId == 0)
                {
                    await _logger.WarningAsync($"Brevo synchronization warning: List ID is empty for store #{storeId}");
                    messages.Add((NotifyType.Warning, $"List ID is empty for store #{storeId}"));
                    continue;
                }

                //try to get store subscriptions
                var subscriptions = await _newsLetterSubscriptionService.GetAllNewsLetterSubscriptionsAsync(storeId: storeId, isActive: true);
                if (!subscriptions.Any())
                {
                    await _logger.WarningAsync($"Brevo synchronization warning: There are no subscriptions for store #{storeId}");
                    messages.Add((NotifyType.Warning, $"There are no subscriptions for store #{storeId}"));
                    continue;
                }

                //get notification URL
                var urlHelper = _urlHelperFactory.GetUrlHelper(_actionContextAccessor.ActionContext);
                var notificationUrl = urlHelper.RouteUrl(BrevoDefaults.ImportContactsRoute, null, _webHelper.GetCurrentRequestProtocol());

                var name = string.Empty;

                switch (await GetAccountLanguageAsync())
                {
                    case BrevoAccountLanguage.French:
                        name =
                            $"{BrevoDefaults.FirstNameFrenchServiceAttribute};" +
                            $"{BrevoDefaults.LastNameFrenchServiceAttribute};";
                        break;
                    case BrevoAccountLanguage.German:
                        name =
                            $"{BrevoDefaults.FirstNameGermanServiceAttribute};" +
                            $"{BrevoDefaults.LastNameGermanServiceAttribute};";
                        break;
                    case BrevoAccountLanguage.Italian:
                        name =
                            $"{BrevoDefaults.FirstNameItalianServiceAttribute};" +
                            $"{BrevoDefaults.LastNameItalianServiceAttribute};";
                        break;
                    case BrevoAccountLanguage.Portuguese:
                        name =
                            $"{BrevoDefaults.FirstNamePortugueseServiceAttribute};" +
                            $"{BrevoDefaults.LastNamePortugueseServiceAttribute};";
                        break;
                    case BrevoAccountLanguage.Spanish:
                        name =
                            $"{BrevoDefaults.FirstNameSpanishServiceAttribute};" +
                            $"{BrevoDefaults.LastNameSpanishServiceAttribute};";
                        break;

                    case BrevoAccountLanguage.English:
                        name =
                            $"{BrevoDefaults.FirstNameServiceAttribute};" +
                            $"{BrevoDefaults.LastNameServiceAttribute};";
                        break;
                }

                var languages = await _languageService.GetAllLanguagesAsync(storeId: storeId);

                //prepare CSV 
                var title =
                    $"{BrevoDefaults.EmailServiceAttribute};" +
                    name +
                    $"{BrevoDefaults.UsernameServiceAttribute};" +
                    $"{BrevoDefaults.SMSServiceAttribute};" +
                    $"{BrevoDefaults.PhoneServiceAttribute};" +
                    $"{BrevoDefaults.CountryServiceAttribute};" +
                    $"{BrevoDefaults.StoreIdServiceAttribute};" +
                    $"{BrevoDefaults.GenderServiceAttribute};" +
                    $"{BrevoDefaults.DateOfBirthServiceAttribute};" +
                    $"{BrevoDefaults.CompanyServiceAttribute};" +
                    $"{BrevoDefaults.Address1ServiceAttribute};" +
                    $"{BrevoDefaults.Address2ServiceAttribute};" +
                    $"{BrevoDefaults.ZipCodeServiceAttribute};" +
                    $"{BrevoDefaults.CityServiceAttribute};" +
                    $"{BrevoDefaults.CountyServiceAttribute};" +
                    $"{BrevoDefaults.StateServiceAttribute};" +
                    $"{BrevoDefaults.FaxServiceAttribute};" +
                    $"{BrevoDefaults.LanguageAttribute};";
                var csv = await subscriptions.AggregateAwaitAsync(title, async (all, subscription) =>
                {
                    var firstName = string.Empty;
                    var lastName = string.Empty;
                    var phone = string.Empty;
                    var countryName = string.Empty;
                    var sms = string.Empty;
                    var gender = string.Empty;
                    var dateOfBirth = string.Empty;
                    var company = string.Empty;
                    var address1 = string.Empty;
                    var address2 = string.Empty;
                    var zipCode = string.Empty;
                    var city = string.Empty;
                    var county = string.Empty;
                    var state = string.Empty;
                    var fax = string.Empty;
                    Language language = null;

                    var customer = await _customerService.GetCustomerByEmailAsync(subscription.Email);
                    if (customer != null)
                    {
                        firstName = customer.FirstName;
                        lastName = customer.LastName;
                        phone = customer.Phone;
                        var countryId = customer.CountryId;
                        var country = await _countryService.GetCountryByIdAsync(countryId);
                        countryName = country?.Name;
                        var countryIsoCode = country?.NumericIsoCode ?? 0;
                        if (countryIsoCode > 0 && !string.IsNullOrEmpty(phone))
                        {
                            //use the first phone code only
                            var phoneCode = ISO3166.FromISOCode(countryIsoCode)
                                ?.DialCodes?.FirstOrDefault()?.Replace(" ", string.Empty) ?? string.Empty;
                            sms = phone.Replace($"+{phoneCode}", string.Empty);
                        }
                        gender = customer.Gender;
                        dateOfBirth = customer.DateOfBirth?.ToString("yyyy-MM-dd");
                        company = customer.Company;
                        address1 = customer.StreetAddress;
                        address2 = customer.StreetAddress2;
                        zipCode = customer.ZipPostalCode;
                        city = customer.City;
                        county = customer.County;
                        state = (await _stateProvinceService.GetStateProvinceByIdAsync(customer.StateProvinceId))?.Name;
                        fax = customer.Fax;
                    }

                    language = languages.FirstOrDefault(lang => lang.Id == (customer?.LanguageId ?? subscription.LanguageId))
                        ?? languages.FirstOrDefault();

                    return $"{all}\n" +
                           $"{subscription.Email};" +
                           $"{firstName};" +
                           $"{lastName};" +
                           $"{customer?.Username};" +
                           $"{sms};" +
                           $"{phone};" +
                           $"{countryName};" +
                           $"{subscription.StoreId};" +
                           $"{gender};" +
                           $"{dateOfBirth};" +
                           $"{company};" +
                           $"{address1};" +
                           $"{address2};" +
                           $"{zipCode};" +
                           $"{city};" +
                           $"{county};" +
                           $"{state};" +
                           $"{fax};" +
                           $"{language?.LanguageCulture};";
                });

                //prepare data to import
                var requestContactImport = new RequestContactImport
                {
                    NotifyUrl = notificationUrl,
                    FileBody = csv,
                    ListIds = [listId]
                };

                //start import
                await client.ImportContactsAsync(requestContactImport);
            }
        }
        catch (Exception exception)
        {
            //log full error
            await _logger.ErrorAsync($"Brevo synchronization error: {exception.Message}", exception, await _workContext.GetCurrentCustomerAsync());
            messages.Add((NotifyType.Error, $"Brevo synchronization error: {exception.Message}"));
        }

        return messages;
    }

    /// 
    /// Export contacts from account to passed stores
    /// 
    /// List of store identifiers
    /// 
    /// A task that represents the asynchronous operation
    /// The task result contains the list of messages
    /// 
    protected async Task> ExportContactsAsync(IList storeIds)
    {
        var messages = new List<(NotifyType, string)>();

        try
        {
            //create API client
            var client = await CreateApiClientAsync(config => new ContactsApi(config));

            foreach (var storeId in storeIds)
            {
                //get list identifier from the settings
                var key = $"{nameof(BrevoSettings)}.{nameof(BrevoSettings.ListId)}";
                var listId = await _settingService.GetSettingByKeyAsync(key, storeId: storeId, loadSharedValueIfNotFound: true);
                if (listId == 0)
                {
                    await _logger.WarningAsync($"Brevo synchronization warning: List ID is empty for store #{storeId}");
                    messages.Add((NotifyType.Warning, $"List ID is empty for store #{storeId}"));
                    continue;
                }

                //check whether there are contacts in the list
                var contacts = await client.GetContactsFromListAsync(listId);
                var template = new { contacts = new[] { new { email = string.Empty, emailBlacklisted = false } } };
                var contactObjects = JsonConvert.DeserializeAnonymousType(contacts.ToJson(), template);
                var blackListedEmails = contactObjects?.contacts?.Where(contact => contact.emailBlacklisted)
                    .Select(contact => contact.email).ToList() ?? new List();

                foreach (var email in blackListedEmails)
                {
                    //email in black list, so unsubscribe contact from all stores
                    foreach (var id in (await _storeService.GetAllStoresAsync()).Select(store => store.Id))
                    {
                        var subscription = await _newsLetterSubscriptionService.GetNewsLetterSubscriptionByEmailAndStoreIdAsync(email, id);
                        if (subscription != null)
                        {
                            subscription.Active = false;
                            await _newsLetterSubscriptionService.UpdateNewsLetterSubscriptionAsync(subscription, false);
                        }
                    }
                }
            }
        }
        catch (Exception exception)
        {
            //log full error
            await _logger.ErrorAsync($"Brevo synchronization error: {exception.Message}", exception, await _workContext.GetCurrentCustomerAsync());
            messages.Add((NotifyType.Error, $"Brevo synchronization error: {exception.Message}"));
        }

        return messages;
    }

    /// 
    /// Add new service attribute in account
    /// 
    /// Collection of attributes
    /// 
    /// A task that represents the asynchronous operation
    /// The task result contains the errors if exist
    /// 
    protected async Task CreateAttributesAsync(IList<(CategoryEnum Category, string Name, string Value, CreateAttribute.TypeEnum? Type)> attributes)
    {
        if (!attributes.Any())
            return string.Empty;

        try
        {
            //create API client
            var client = await CreateApiClientAsync(config => new AttributesApi(config));

            foreach (var attribute in attributes)
            {
                //prepare data
                var createAttribute = new CreateAttribute(attribute.Value, type: attribute.Type);

                //create attribute
                await client.CreateAttributeAsync(attribute.Category.ToString().ToLowerInvariant(), attribute.Name, createAttribute);
            }
        }
        catch (Exception exception)
        {
            //log full error
            await _logger.ErrorAsync($"Brevo error: {exception.Message}.", exception, await _workContext.GetCurrentCustomerAsync());
            return exception.Message;
        }

        return string.Empty;
    }

    [GeneratedRegex("(%[^\\%]*.%)")]
    private static partial Regex SpecialCharRegex();

    [GeneratedRegex("({{\\s*params\\..*?\\s*}})")]
    private static partial Regex ParamsRegex();

    #endregion

    #region Methods

    #region Synchronization

    /// 
    /// Synchronize contacts 
    /// 
    /// Whether it's a scheduled synchronization
    /// Store identifier; pass 0 to synchronize contacts for all stores
    /// 
    /// A task that represents the asynchronous operation
    /// The task result contains the list of messages
    /// 
    public async Task> SynchronizeAsync(bool synchronizationTask = true, int storeId = 0)
    {
        var messages = new List<(NotifyType, string)>();
        try
        {
            //whether plugin is configured
            var brevoSettings = await _settingService.LoadSettingAsync();
            if (!string.IsNullOrEmpty(brevoSettings.ApiKey))
            {
                //use only passed store identifier for the manual synchronization
                //use all store ids for the synchronization task
                var storeIds = !synchronizationTask
                        ? [storeId]
                    : new List { 0 }.Union((await _storeService.GetAllStoresAsync()).Select(store => store.Id)).ToList();

                var importMessages = await ImportContactsAsync(storeIds);
                messages.AddRange(importMessages);

                var exportMessages = await ExportContactsAsync(storeIds);
                messages.AddRange(exportMessages);
            }

        }
        catch (Exception exception)
        {
            //log full error
            await _logger.ErrorAsync($"Brevo synchronization error: {exception.Message}.", exception, await _workContext.GetCurrentCustomerAsync());
            messages.Add((NotifyType.Error, $"Brevo synchronization error: {exception.Message}"));
        }

        return messages;
    }

    /// 
    /// Subscribe new contact
    /// 
    /// Subscription
    /// A task that represents the asynchronous operation
    public async System.Threading.Tasks.Task SubscribeAsync(NewsLetterSubscription subscription)
    {
        try
        {
            //create API client
            var client = await CreateApiClientAsync(config => new ContactsApi(config));

            //try to get list identifier
            var key = $"{nameof(BrevoSettings)}.{nameof(BrevoSettings.ListId)}";
            var listId = await _settingService.GetSettingByKeyAsync(key, storeId: subscription.StoreId);
            if (listId == 0)
                listId = await _settingService.GetSettingByKeyAsync(key);
            if (listId == 0)
            {
                await _logger.WarningAsync($"Brevo synchronization warning: List ID is empty for store #{subscription.StoreId}");
                return;
            }

            GetExtendedContactDetails contactObject = null;
            try
            {
                contactObject = await client.GetContactInfoAsync(subscription.Email);
            }
            catch (ApiException apiException)
            {
                if (apiException.ErrorCode != 404)
                {
                    await _logger.ErrorAsync($"Brevo error: {apiException.Message}.", apiException, await _workContext.GetCurrentCustomerAsync());
                    return;
                }
            }

            //prepare attributes
            var firstName = string.Empty;
            var lastName = string.Empty;
            var phone = string.Empty;
            var sms = string.Empty;
            var countryName = string.Empty;
            var gender = string.Empty;
            var dateOfBirth = string.Empty;
            var company = string.Empty;
            var address1 = string.Empty;
            var address2 = string.Empty;
            var zipCode = string.Empty;
            var city = string.Empty;
            var county = string.Empty;
            var state = string.Empty;
            var fax = string.Empty;
            Language language = null;

            var customer = await _customerService.GetCustomerByEmailAsync(subscription.Email);
            if (customer != null)
            {
                firstName = customer.FirstName;
                lastName = customer.LastName;
                phone = customer.Phone;
                var countryId = customer.CountryId;
                var country = await _countryService.GetCountryByIdAsync(countryId);
                countryName = country?.Name;
                var countryIsoCode = country?.NumericIsoCode ?? 0;
                if (countryIsoCode > 0 && !string.IsNullOrEmpty(phone))
                {
                    //use the first phone code only
                    var phoneCode = ISO3166.FromISOCode(countryIsoCode)
                        ?.DialCodes?.FirstOrDefault()?.Replace(" ", string.Empty) ?? string.Empty;
                    sms = phone.Replace($"+{phoneCode}", string.Empty);
                }
                gender = customer.Gender;
                dateOfBirth = customer.DateOfBirth?.ToString("yyyy-MM-dd");
                company = customer.Company;
                address1 = customer.StreetAddress;
                address2 = customer.StreetAddress2;
                zipCode = customer.ZipPostalCode;
                city = customer.City;
                county = customer.County;
                state = (await _stateProvinceService.GetStateProvinceByIdAsync(customer.StateProvinceId))?.Name;
                fax = customer.Fax;
            }

            language = await _languageService.GetLanguageByIdAsync(customer?.LanguageId ?? subscription.LanguageId)
                ?? (await _languageService.GetAllLanguagesAsync(storeId: subscription.StoreId)).FirstOrDefault();

            var attributes = new Dictionary
            {
                [BrevoDefaults.UsernameServiceAttribute] = customer?.Username,
                [BrevoDefaults.SMSServiceAttribute] = sms,
                [BrevoDefaults.PhoneServiceAttribute] = phone,
                [BrevoDefaults.CountryServiceAttribute] = countryName,
                [BrevoDefaults.StoreIdServiceAttribute] = subscription.StoreId.ToString(),
                [BrevoDefaults.GenderServiceAttribute] = gender,
                [BrevoDefaults.DateOfBirthServiceAttribute] = dateOfBirth,
                [BrevoDefaults.CompanyServiceAttribute] = company,
                [BrevoDefaults.Address1ServiceAttribute] = address1,
                [BrevoDefaults.Address2ServiceAttribute] = address2,
                [BrevoDefaults.ZipCodeServiceAttribute] = zipCode,
                [BrevoDefaults.CityServiceAttribute] = city,
                [BrevoDefaults.CountyServiceAttribute] = county,
                [BrevoDefaults.StateServiceAttribute] = state,
                [BrevoDefaults.FaxServiceAttribute] = fax,
                [BrevoDefaults.LanguageAttribute] = language?.LanguageCulture
            };

            switch (await GetAccountLanguageAsync())
            {
                case BrevoAccountLanguage.French:
                    attributes.Add(BrevoDefaults.FirstNameFrenchServiceAttribute, firstName);
                    attributes.Add(BrevoDefaults.LastNameFrenchServiceAttribute, lastName);
                    break;
                case BrevoAccountLanguage.German:
                    attributes.Add(BrevoDefaults.FirstNameGermanServiceAttribute, firstName);
                    attributes.Add(BrevoDefaults.LastNameGermanServiceAttribute, lastName);
                    break;
                case BrevoAccountLanguage.Italian:
                    attributes.Add(BrevoDefaults.FirstNameItalianServiceAttribute, firstName);
                    attributes.Add(BrevoDefaults.LastNameItalianServiceAttribute, lastName);
                    break;
                case BrevoAccountLanguage.Portuguese:
                    attributes.Add(BrevoDefaults.FirstNamePortugueseServiceAttribute, firstName);
                    attributes.Add(BrevoDefaults.LastNamePortugueseServiceAttribute, lastName);
                    break;
                case BrevoAccountLanguage.Spanish:
                    attributes.Add(BrevoDefaults.FirstNameSpanishServiceAttribute, firstName);
                    attributes.Add(BrevoDefaults.LastNameSpanishServiceAttribute, lastName);
                    break;
                case BrevoAccountLanguage.English:
                    attributes.Add(BrevoDefaults.FirstNameServiceAttribute, firstName);
                    attributes.Add(BrevoDefaults.LastNameServiceAttribute, lastName);
                    break;
            }

            //Add new contact
            if (contactObject == null)
            {
                var createContact = new CreateContact
                {
                    Email = subscription.Email,
                    Attributes = attributes,
                    ListIds = [listId],
                    UpdateEnabled = true
                };
                await client.CreateContactAsync(createContact);
            }
            else
            {
                //update contact
                var updateContact = new UpdateContact
                {
                    Attributes = attributes,
                    ListIds = [listId],
                    EmailBlacklisted = false
                };
                await client.UpdateContactAsync(subscription.Email, updateContact);
            }
        }
        catch (Exception exception)
        {
            //log full error
            await _logger.ErrorAsync($"Brevo error: {exception.Message}.", exception, await _workContext.GetCurrentCustomerAsync());
        }
    }

    /// 
    /// Unsubscribe contact
    /// 
    /// Subscription
    /// A task that represents the asynchronous operation
    public async System.Threading.Tasks.Task UnsubscribeAsync(NewsLetterSubscription subscription)
    {
        try
        {
            //create API client
            var client = await CreateApiClientAsync(config => new ContactsApi(config));

            //try to get list identifier
            var key = $"{nameof(BrevoSettings)}.{nameof(BrevoSettings.ListId)}";
            var listId = await _settingService.GetSettingByKeyAsync(key, storeId: subscription.StoreId);
            if (listId == 0)
                listId = await _settingService.GetSettingByKeyAsync(key);
            if (listId == 0)
            {
                await _logger.WarningAsync($"Brevo synchronization warning: List ID is empty for store #{subscription.StoreId}");
                return;
            }

            //update contact
            var updateContact = new UpdateContact
            {
                UnlinkListIds = [listId]
            };
            await client.UpdateContactAsync(subscription.Email, updateContact);
        }
        catch (Exception exception)
        {
            //log full error
            await _logger.ErrorAsync($"Brevo error: {exception.Message}.", exception, await _workContext.GetCurrentCustomerAsync());
        }
    }

    /// 
    /// Unsubscribe contact
    /// 
    /// HTTP request
    /// A task that represents the asynchronous operation
    public async System.Threading.Tasks.Task HandleWebhookAsync(Microsoft.AspNetCore.Http.HttpRequest request)
    {
        await HandleFunctionAsync(async () =>
        {
            using var streamReader = new StreamReader(request.Body);
            var requestContent = await streamReader.ReadToEndAsync();

            //parse string to JSON object
            var unsubscriber = JsonConvert.DeserializeAnonymousType(requestContent,
                new { tag = (int?)0, email = string.Empty, date_event = string.Empty });

            //we pass the store identifier in the X-Mailin-Tag at sending emails, now get it here
            var storeId = unsubscriber?.tag;
            if (!storeId.HasValue)
                return true;

            //get subscription by email and store identifier
            var email = unsubscriber?.email;
            var subscription = await _newsLetterSubscriptionService.GetNewsLetterSubscriptionByEmailAndStoreIdAsync(email, storeId.Value);
            if (subscription == null)
                return true;

            //update subscription
            subscription.Active = false;
            await _newsLetterSubscriptionService.UpdateNewsLetterSubscriptionAsync(subscription);
            await _logger.InformationAsync($"{BrevoDefaults.SystemName} unsubscription: email {email}, store #{storeId}, date {unsubscriber?.date_event}");

            return true;
        });
    }

    /// 
    /// Create webhook to get notification about unsubscribed contacts
    /// 
    /// 
    /// A task that represents the asynchronous operation
    /// The task result contains the webhook id
    /// 
    public async Task GetUnsubscribeWebHookIdAsync()
    {
        try
        {
            //create API client
            var client = await CreateApiClientAsync(config => new WebhooksApi(config));

            //check whether webhook already exist
            var brevoSettings = await _settingService.LoadSettingAsync();
            if (brevoSettings.UnsubscribeWebhookId != 0)
            {
                await client.GetWebhookAsync(brevoSettings.UnsubscribeWebhookId);
                return brevoSettings.UnsubscribeWebhookId;
            }

            //or create new one
            var urlHelper = _urlHelperFactory.GetUrlHelper(_actionContextAccessor.ActionContext);
            var notificationUrl = urlHelper.RouteUrl(BrevoDefaults.UnsubscribeContactRoute, null, _webHelper.GetCurrentRequestProtocol());
            var webhook = new CreateWebhook(notificationUrl, "Unsubscribe event webhook",
                [CreateWebhook.EventsEnum.Unsubscribed], CreateWebhook.TypeEnum.Transactional);
            var result = await client.CreateWebhookAsync(webhook);

            return (int)result.Id;
        }
        catch (Exception exception)
        {
            //log full error
            await _logger.ErrorAsync($"Brevo error: {exception.Message}.", exception, await _workContext.GetCurrentCustomerAsync());
            return 0;
        }
    }

    /// 
    /// Update contact after completing order
    /// 
    /// Order
    /// A task that represents the asynchronous operation
    public async System.Threading.Tasks.Task UpdateContactAfterCompletingOrderAsync(Order order)
    {
        try
        {
            ArgumentNullException.ThrowIfNull(order);

            var customer = await _customerService.GetCustomerByIdAsync(order.CustomerId);
            if (customer.Email is null)
                return;

            //create API client
            var client = await CreateApiClientAsync(config => new ContactsApi(config));

            try
            {
                var contactInfo = await client.GetContactInfoAsync(customer.Email);
            }
            catch (ApiException apiException)
            {
                if (apiException.ErrorCode == 404)
                {
                    return;
                }
                else
                {
                    await _logger.ErrorAsync($"Brevo error: {apiException.Message}.", apiException, await _workContext.GetCurrentCustomerAsync());
                    return;
                }
            }

            //update contact
            var attributes = new Dictionary
            {
                [BrevoDefaults.IdServiceAttribute] = order.Id.ToString(),
                [BrevoDefaults.OrderIdServiceAttribute] = order.Id.ToString(),
                [BrevoDefaults.OrderDateServiceAttribute] = order.PaidDateUtc.ToString(),
                [BrevoDefaults.OrderTotalServiceAttribute] = order.OrderTotal.ToString()
            };
            var updateContact = new UpdateContact { Attributes = attributes };
            await client.UpdateContactAsync(customer.Email, updateContact);

        }
        catch (Exception exception)
        {
            //log full error
            await _logger.ErrorAsync($"Brevo error: {exception.Message}.", exception, await _workContext.GetCurrentCustomerAsync());
        }
    }

    #endregion

    #region Common

    /// 
    /// Get account information
    /// 
    /// 
    /// A task that represents the asynchronous operation
    /// The task result contains the account info; whether marketing automation is enabled, errors if exist
    /// 
    public async Task<(string Info, bool MarketingAutomationEnabled, string MAkey, string Errors)> GetAccountInfoAsync()
    {
        try
        {
            //create API client
            var client = await CreateApiClientAsync(config => new AccountApi(config));

            //get account
            var account = await client.GetAccountAsync();

            //prepare info
            var info = string.Format("First name: {1}{0}Last name: {2}{0}Email: {3}{0}Email credits: {4}{0}SMS credits: {5}{0}",
                Environment.NewLine,
                WebUtility.HtmlEncode(account.FirstName),
                WebUtility.HtmlEncode(account.LastName),
                WebUtility.HtmlEncode(account.Email),
                account.Plan.Where(plan => plan.Type != GetAccountPlan.TypeEnum.Sms).Sum(plan => plan.Credits),
                account.Plan.Where(plan => plan.Type == GetAccountPlan.TypeEnum.Sms).Sum(plan => plan.Credits));

            //get marketing automation tacker ID
            var key = account.MarketingAutomation?.Key ?? string.Empty;

            return (info, account.MarketingAutomation?.Enabled ?? false, key, null);
        }
        catch (Exception exception)
        {
            //log full error
            await _logger.ErrorAsync($"Brevo error: {exception.Message}.", exception, await _workContext.GetCurrentCustomerAsync());
            return (null, false, null, exception.Message);
        }
    }

    /// 
    /// Get available lists to synchronize contacts
    /// 
    /// 
    /// A task that represents the asynchronous operation
    /// The task result contains the list of id-name pairs of lists; errors if exist
    /// 
    public async Task<(IList<(string Id, string Name)> Lists, string Errors)> GetListsAsync()
    {
        var availableLists = new List<(string Id, string Name)>();

        try
        {
            //create API client
            var client = await CreateApiClientAsync(config => new ContactsApi(config));

            //get available lists
            var lists = await client.GetListsAsync(BrevoDefaults.DefaultSynchronizationListsLimit);

            //prepare id-name pairs
            var template = new { lists = new[] { new { id = string.Empty, name = string.Empty } } };
            var listObjects = JsonConvert.DeserializeAnonymousType(lists.ToJson(), template);
            if (listObjects?.lists != null)
            {
                foreach (var list in listObjects.lists)
                {
                    if (list != null)
                        availableLists.Add((list.id, list.name));
                }
            }
        }
        catch (Exception exception)
        {
            //log full error
            await _logger.ErrorAsync($"Brevo error: {exception.Message}.", exception, await _workContext.GetCurrentCustomerAsync());
            return (availableLists, exception.Message);
        }

        return (availableLists, null);
    }

    /// 
    /// Get available senders of transactional emails
    /// 
    /// 
    /// A task that represents the asynchronous operation
    /// The task result contains the list of id-name pairs of senders; errors if exist
    /// 
    public async Task<(IList<(string Id, string Name)> Lists, string Errors)> GetSendersAsync()
    {
        var availableSenders = new List<(string Id, string Name)>();

        try
        {
            //create API client
            var client = await CreateApiClientAsync(config => new SendersApi(config));

            //get available senderes
            var senders = await client.GetSendersAsync();

            //prepare id-name pairs
            foreach (var sender in senders.Senders)
            {
                availableSenders.Add((sender.Id.ToString(), $"{sender.Name} ({sender.Email})"));
            }
        }
        catch (Exception exception)
        {
            //log full error
            await _logger.ErrorAsync($"Brevo error: {exception.Message}.", exception, await _workContext.GetCurrentCustomerAsync());
            return (availableSenders, exception.Message);
        }

        return (availableSenders, null);
    }

    /// 
    /// Get account language
    /// 
    /// 
    /// A task that represents the asynchronous operation
    /// The task result contains the BrevoAccountLanguage
    /// 
    public async Task GetAccountLanguageAsync()
    {
        try
        {
            //create API client
            var client = await CreateApiClientAsync(config => new AttributesApi(config));

            var attributes = await client.GetAttributesAsync();
            var allAttribytes = attributes.Attributes.Select(s => s.Name).ToList();

            var defaultNameAttributes = new List
            {
                BrevoDefaults.FirstNameServiceAttribute,
                BrevoDefaults.LastNameServiceAttribute
            };
            if (defaultNameAttributes.All(attr => allAttribytes.Contains(attr)))
                return BrevoAccountLanguage.English;

            var frenchNameAttributes = new List
            {
                BrevoDefaults.FirstNameFrenchServiceAttribute,
                BrevoDefaults.LastNameFrenchServiceAttribute
            };
            if (frenchNameAttributes.All(attr => allAttribytes.Contains(attr)))
                return BrevoAccountLanguage.French;

            var italianNameAttributes = new List
            {
                BrevoDefaults.FirstNameItalianServiceAttribute,
                BrevoDefaults.LastNameItalianServiceAttribute
            };
            if (italianNameAttributes.All(attr => allAttribytes.Contains(attr)))
                return BrevoAccountLanguage.Italian;

            var spanishNameAttributes = new List
            {
                BrevoDefaults.FirstNameSpanishServiceAttribute,
                BrevoDefaults.LastNameSpanishServiceAttribute
            };
            if (spanishNameAttributes.All(attr => allAttribytes.Contains(attr)))
                return BrevoAccountLanguage.Spanish;

            var germanNameAttributes = new List
            {
                BrevoDefaults.FirstNameGermanServiceAttribute,
                BrevoDefaults.LastNameGermanServiceAttribute
            };
            if (germanNameAttributes.All(attr => allAttribytes.Contains(attr)))
                return BrevoAccountLanguage.German;

            var portugueseNameAttributes = new List
            {
                BrevoDefaults.FirstNamePortugueseServiceAttribute,
                BrevoDefaults.LastNamePortugueseServiceAttribute
            };
            if (portugueseNameAttributes.All(attr => allAttribytes.Contains(attr)))
                return BrevoAccountLanguage.Portuguese;

            //Create default customer names attribytes
            var initialAttributes = new List<(CategoryEnum category, string Name, string Value, CreateAttribute.TypeEnum? Type)>
            {
                (CategoryEnum.Normal, BrevoDefaults.FirstNameServiceAttribute, null, CreateAttribute.TypeEnum.Text),
                (CategoryEnum.Normal, BrevoDefaults.LastNameServiceAttribute, null, CreateAttribute.TypeEnum.Text)
            };
            //create attributes that are not already on account
            var newAttributes = new List<(CategoryEnum category, string Name, string Value, CreateAttribute.TypeEnum? Type)>();
            foreach (var attribute in initialAttributes)
            {
                if (!allAttribytes.Contains(attribute.Name))
                    newAttributes.Add(attribute);
            }

            await CreateAttributesAsync(newAttributes);

            return BrevoAccountLanguage.English;
        }
        catch (Exception exception)
        {
            //log full error
            await _logger.ErrorAsync($"Brevo error: {exception.Message}.", exception, await _workContext.GetCurrentCustomerAsync());
            return BrevoAccountLanguage.English;
        }
    }

    /// 
    /// Check and create missing attributes in account
    /// 
    /// 
    /// A task that represents the asynchronous operation
    /// The task result contains the errors if exist
    /// 
    public async Task PrepareAttributesAsync()
    {
        try
        {
            //create API client
            var client = await CreateApiClientAsync(config => new AttributesApi(config));

            var attributes = await client.GetAttributesAsync();
            var attributeNames = attributes.Attributes.Select(s => s.Name).ToList();

            //prepare attributes to create
            var initialAttributes = new List<(CategoryEnum category, string Name, string Value, CreateAttribute.TypeEnum? Type)>
            {
                (CategoryEnum.Normal, BrevoDefaults.UsernameServiceAttribute, null, CreateAttribute.TypeEnum.Text),
                (CategoryEnum.Normal, BrevoDefaults.PhoneServiceAttribute, null, CreateAttribute.TypeEnum.Text),
                (CategoryEnum.Normal, BrevoDefaults.CountryServiceAttribute, null, CreateAttribute.TypeEnum.Text),
                (CategoryEnum.Normal, BrevoDefaults.StoreIdServiceAttribute, null, CreateAttribute.TypeEnum.Text),
                (CategoryEnum.Normal, BrevoDefaults.GenderServiceAttribute, null, CreateAttribute.TypeEnum.Text),
                (CategoryEnum.Normal, BrevoDefaults.DateOfBirthServiceAttribute, null, CreateAttribute.TypeEnum.Text),
                (CategoryEnum.Normal, BrevoDefaults.CompanyServiceAttribute, null, CreateAttribute.TypeEnum.Text),
                (CategoryEnum.Normal, BrevoDefaults.Address1ServiceAttribute, null, CreateAttribute.TypeEnum.Text),
                (CategoryEnum.Normal, BrevoDefaults.Address2ServiceAttribute, null, CreateAttribute.TypeEnum.Text),
                (CategoryEnum.Normal, BrevoDefaults.ZipCodeServiceAttribute, null, CreateAttribute.TypeEnum.Text),
                (CategoryEnum.Normal, BrevoDefaults.CityServiceAttribute, null, CreateAttribute.TypeEnum.Text),
                (CategoryEnum.Normal, BrevoDefaults.CountyServiceAttribute, null, CreateAttribute.TypeEnum.Text),
                (CategoryEnum.Normal, BrevoDefaults.StateServiceAttribute, null, CreateAttribute.TypeEnum.Text),
                (CategoryEnum.Normal, BrevoDefaults.FaxServiceAttribute, null, CreateAttribute.TypeEnum.Text),
                (CategoryEnum.Normal, BrevoDefaults.LanguageAttribute, null, CreateAttribute.TypeEnum.Text),
                (CategoryEnum.Transactional, BrevoDefaults.OrderIdServiceAttribute, null, CreateAttribute.TypeEnum.Id),
                (CategoryEnum.Transactional, BrevoDefaults.OrderDateServiceAttribute, null, CreateAttribute.TypeEnum.Text),
                (CategoryEnum.Transactional, BrevoDefaults.OrderTotalServiceAttribute, null, CreateAttribute.TypeEnum.Float),
                (CategoryEnum.Calculated, BrevoDefaults.OrderTotalSumServiceAttribute, $"SUM[{BrevoDefaults.OrderTotalServiceAttribute}]", null),
                (CategoryEnum.Calculated, BrevoDefaults.OrderTotalMonthSumServiceAttribute, $"SUM[{BrevoDefaults.OrderTotalServiceAttribute},{BrevoDefaults.OrderDateServiceAttribute},>,NOW(-30)]", null),
                (CategoryEnum.Calculated, BrevoDefaults.OrderCountServiceAttribute, $"COUNT[{BrevoDefaults.OrderIdServiceAttribute}]", null),
                (CategoryEnum.Global, BrevoDefaults.AllOrderTotalSumServiceAttribute, $"SUM[{BrevoDefaults.OrderTotalSumServiceAttribute}]", null),
                (CategoryEnum.Global, BrevoDefaults.AllOrderTotalMonthSumServiceAttribute, $"SUM[{BrevoDefaults.OrderTotalMonthSumServiceAttribute}]", null),
                (CategoryEnum.Global, BrevoDefaults.AllOrderCountServiceAttribute, $"SUM[{BrevoDefaults.OrderCountServiceAttribute}]", null)
            };

            //create attributes that are not already on account
            var newAttributes = new List<(CategoryEnum category, string Name, string Value, CreateAttribute.TypeEnum? Type)>();
            foreach (var attribute in initialAttributes)
            {
                if (!attributeNames.Contains(attribute.Name))
                    newAttributes.Add(attribute);
            }

            return await CreateAttributesAsync(newAttributes);
        }
        catch (Exception exception)
        {
            //log full error
            await _logger.ErrorAsync($"Brevo error: {exception.Message}.", exception, await _workContext.GetCurrentCustomerAsync());
            return exception.Message;
        }
    }

    /// 
    /// Move message template tokens to transactional attributes
    /// 
    /// List of available message templates tokens
    /// 
    /// A task that represents the asynchronous operation
    /// The task result contains the errors if exist
    /// 
    public async Task PrepareTransactionalAttributesAsync(IList tokens)
    {
        try
        {
            //create API client
            var client = await CreateApiClientAsync(config => new AttributesApi(config));

            //get already existing transactional attributes
            var attributes = await client.GetAttributesAsync();
            var transactionalAttributes = attributes.Attributes
                .Where(attribute => attribute.Category == CategoryEnum.Transactional).ToList();

            //bring tokens to attributes format
            tokens = tokens.Select(token => token.Replace("%", "").Replace(".", "_").Replace("(s)", "-s-").ToUpperInvariant()).ToList();

            //get attributes that are not already on account
            tokens = tokens.Except(transactionalAttributes.Select(attribute => attribute.Name)).ToList();
            if (!tokens.Any())
                return string.Empty;

            //prepare attributes to create
            var newAttributes = new List<(CategoryEnum category, string Name, string Value, CreateAttribute.TypeEnum? Type)>();
            foreach (var token in tokens)
            {
                newAttributes.Add((CategoryEnum.Transactional, token, null, CreateAttribute.TypeEnum.Text));
            }

            return await CreateAttributesAsync(newAttributes);
        }
        catch (Exception exception)
        {
            //log full error
            await _logger.ErrorAsync($"Brevo error: {exception.Message}.", exception, await _workContext.GetCurrentCustomerAsync());
            return exception.Message;
        }
    }

    #endregion

    #region SMTP

    /// 
    /// Check whether SMTP is enabled on account
    /// 
    /// 
    /// A task that represents the asynchronous operation
    /// The task result contains the result of check; errors if exist
    /// 
    public async Task<(bool Enabled, string Errors)> SmtpIsEnabledAsync()
    {
        try
        {
            //create API client
            var client = await CreateApiClientAsync(config => new AccountApi(config));

            //get account
            var account = await client.GetAccountAsync();
            return (account.Relay?.Enabled ?? false, null);
        }
        catch (Exception exception)
        {
            //log full error
            await _logger.ErrorAsync($"Brevo error: {exception.Message}.", exception, await _workContext.GetCurrentCustomerAsync());
            return (false, exception.Message);
        }
    }

    /// 
    /// Get email account identifier
    /// 
    /// Sender identifier
    /// SMTP key
    /// 
    /// A task that represents the asynchronous operation
    /// The task result contains the email account identifier; errors if exist
    /// 
    public async Task<(int Id, string Errors)> GetEmailAccountIdAsync(string senderId, string smtpKey)
    {
        try
        {
            //create API clients
            var sendersClient = await CreateApiClientAsync(config => new SendersApi(config));
            var accountClient = await CreateApiClientAsync(config => new AccountApi(config));

            //get all available senders
            var senders = await sendersClient.GetSendersAsync();
            if (!senders.Senders.Any())
                return (0, "There are no senders");

            var currentSender = senders.Senders.FirstOrDefault(sender => sender.Id.ToString() == senderId);
            if (currentSender != null)
            {
                //try to find existing email account by name and email
                var emailAccount = (await _emailAccountService.GetAllEmailAccountsAsync())
                    .FirstOrDefault(account => account.DisplayName == currentSender.Name && account.Email == currentSender.Email);
                if (emailAccount != null)
                    return (emailAccount.Id, null);
            }

            //or create new one
            currentSender ??= senders.Senders.FirstOrDefault();
            var relay = (await accountClient.GetAccountAsync()).Relay;
            var newEmailAccount = new EmailAccount
            {
                Host = relay?.Data?.Relay,
                Port = relay?.Data?.Port ?? 0,
                Username = relay?.Data?.UserName,
                Password = smtpKey,
                EnableSsl = true,
                Email = currentSender.Email,
                DisplayName = currentSender.Name
            };
            await _emailAccountService.InsertEmailAccountAsync(newEmailAccount);

            return (newEmailAccount.Id, null);
        }
        catch (Exception exception)
        {
            //log full error
            await _logger.ErrorAsync($"Brevo error: {exception.Message}.", exception, await _workContext.GetCurrentCustomerAsync());
            return (0, exception.Message);
        }
    }

    /// 
    /// Get email template identifier
    /// 
    /// Current email template id
    /// Message template
    /// Email account
    /// 
    /// A task that represents the asynchronous operation
    /// The task result contains the email template identifier
    /// 
    public async Task GetTemplateIdAsync(int? templateId, MessageTemplate message, EmailAccount emailAccount)
    {
        try
        {
            //create API client
            var client = await CreateApiClientAsync(config => new TransactionalEmailsApi(config));

            //check whether email template already exists
            if (templateId > 0)
            {
                await client.GetSmtpTemplateAsync(templateId);
                return templateId;
            }

            //or create new one
            if (emailAccount == null)
                throw new NopException("Email account not configured");

            //the original body and subject of the email template are the same as that of the message template
            var body = message.Body.Replace("%if", "\"if\"").Replace("endif%", "\"endif\"");
            body = SpecialCharRegex().Replace(body, x => $"{{{{ params.{x.ToString().Replace("%", "").Replace(".", "_").ToUpperInvariant()} }}}}");
            var subject = message.Subject.Replace("%if", "\"if\"").Replace("endif%", "\"endif\"");
            subject = SpecialCharRegex().Replace(subject, x => $"{{{{ params.{x.ToString().Replace("%", "").Replace(".", "_").ToUpperInvariant()} }}}}");

            //create email template
            var createSmtpTemplate = new CreateSmtpTemplate(sender: new CreateSmtpTemplateSender(emailAccount.DisplayName, emailAccount.Email),
                templateName: message.Name, htmlContent: body, subject: subject, isActive: true);
            var emailTemplate = await client.CreateSmtpTemplateAsync(createSmtpTemplate);

            return (int?)emailTemplate.Id;
        }
        catch (Exception exception)
        {
            //log full error
            await _logger.ErrorAsync($"Brevo error: {exception.Message}.", exception, await _workContext.GetCurrentCustomerAsync());
            return null;
        }
    }

    /// 
    /// Convert Brevo email template to queued email
    /// 
    /// Email template identifier
    /// 
    /// A task that represents the asynchronous operation
    /// The task result contains the queued email
    /// 
    public async Task GetQueuedEmailFromTemplateAsync(int templateId)
    {
        try
        {
            //create API client
            var client = await CreateApiClientAsync(config => new TransactionalEmailsApi(config));

            if (templateId == 0)
                throw new NopException("Message template is empty");

            //get template
            var template = await client.GetSmtpTemplateAsync(templateId);

            //bring attributes to tokens format
            var subject = ParamsRegex().Replace(template.Subject, x => $"%{x.ToString().Replace("{", "").Replace("}", "").Replace("params.", "").Replace("_", ".").Trim()}%");
            subject = subject.Replace("\"if\"", "%if").Replace("\"endif\"", "endif%");
            var body = ParamsRegex().Replace(template.HtmlContent, x => $"%{x.ToString().Replace("{", "").Replace("}", "").Replace("params.", "").Replace("_", ".").Trim()}%");
            body = body.Replace("\"if\"", "%if").Replace("\"endif\"", "endif%");

            //map template to queued email
            return new QueuedEmail
            {
                Subject = subject,
                Body = body,
                FromName = template.Sender?.Name,
                From = template.Sender?.Email
            };
        }
        catch (Exception exception)
        {
            //log full error
            await _logger.ErrorAsync($"Brevo email sending error: {exception.Message}.", exception, await _workContext.GetCurrentCustomerAsync());
            return null;
        }
    }

    #endregion

    #region SMS

    /// 
    /// Send SMS 
    /// 
    /// Phone number of the receiver
    /// Name of sender
    /// Text
    /// A task that represents the asynchronous operation
    public async System.Threading.Tasks.Task SendSMSAsync(string to, string from, string text)
    {
        //whether SMS notifications enabled
        var brevoSettings = await _settingService.LoadSettingAsync();
        if (!brevoSettings.UseSmsNotifications)
            return;

        try
        {
            //check number and text
            if (string.IsNullOrEmpty(to) || string.IsNullOrEmpty(text))
                throw new NopException("Phone number or SMS text is empty");

            //create API client
            var client = await CreateApiClientAsync(config => new TransactionalSMSApi(config));

            //create SMS data
            var transactionalSms = new SendTransacSms(sender: from, recipient: to, content: text, type: SendTransacSms.TypeEnum.Transactional);

            //send SMS
            var sms = await client.SendTransacSmsAsync(transactionalSms);
            await _logger.InformationAsync($"Brevo SMS sent: {sms?.Reference ?? $"credits remaining {sms?.RemainingCredits?.ToString()}"}");
        }
        catch (Exception exception)
        {
            //log full error
            await _logger.ErrorAsync($"Brevo SMS sending error: {exception.Message}.", exception, await _workContext.GetCurrentCustomerAsync());
        }
    }

    /// 
    /// Send SMS campaign
    /// 
    /// Contact list identifier
    /// Name of sender
    /// Text
    /// A task that represents the asynchronous operation
    public async Task SendSMSCampaignAsync(int listId, string from, string text)
    {
        try
        {
            //check list and text
            if (listId == 0 || string.IsNullOrEmpty(text) || string.IsNullOrEmpty(from))
                throw new NopException("List or SMS text or sender name is empty");

            //create API client
            var client = await CreateApiClientAsync(config => new SMSCampaignsApi(config));

            //create SMS campaign
            var campaign = await client.CreateSmsCampaignAsync(new CreateSmsCampaign(name: CommonHelper.EnsureMaximumLength(text, 20),
                sender: from, content: text, recipients: new CreateSmsCampaignRecipients([listId])));

            //send campaign
            await client.SendSmsCampaignNowAsync(campaign.Id);
        }
        catch (Exception exception)
        {
            //log full error
            await _logger.ErrorAsync($"Brevo SMS sending error: {exception.Message}.", exception, await _workContext.GetCurrentCustomerAsync());
            return exception.Message;
        }

        return string.Empty;
    }

    #endregion

    #endregion
}