using System.Net; using System.Text; using System.Text.RegularExpressions; using brevo_csharp.Api; using brevo_csharp.Client; using brevo_csharp.Model; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Infrastructure; using Microsoft.AspNetCore.Mvc.Routing; using Microsoft.Net.Http.Headers; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using Nop.Core; using Nop.Core.Domain.Localization; using Nop.Core.Domain.Messages; 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 static brevo_csharp.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 (!IsConfigured(brevoSettings)) 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 (!IsConfigured(brevoSettings)) 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 ContactsApi(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(Core.Domain.Orders.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 /// /// Check whether the plugin is configured /// /// Plugin settings /// Result public static bool IsConfigured(BrevoSettings settings) { //API key is required to request remote services return !string.IsNullOrEmpty(settings?.ApiKey); } /// /// 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); } } /// /// Set partner value /// /// True if partner successfully set; otherwise false public async Task SetPartnerAsync() { try { var stores = (await _storeService.GetAllStoresAsync()).ToList(); var storeCredentials = new Dictionary(); foreach (var store in stores) { var bSettings = await _settingService.LoadSettingAsync(store.Id); var apiKey = bSettings.ApiKey; if (!string.IsNullOrEmpty(apiKey) && !storeCredentials.Where(s => s.Value == apiKey).Any()) storeCredentials.Add(store.Url, apiKey); } //whether plugin is configured if (!storeCredentials.Any()) return false; foreach (var storeCredential in storeCredentials) { await HttpBrevoClientAsync(storeCredential.Key, storeCredential.Value); } } catch (Exception exception) { //log full error _logger.Error($"Brevo error: {exception.Message}.", exception, await _workContext.GetCurrentCustomerAsync()); return false; } return true; async System.Threading.Tasks.Task HttpBrevoClientAsync(string storeUrl, string apiKey) { //create API client var httpClient = new HttpClient { //configure client BaseAddress = new Uri(BrevoDefaults.AccountApiUrl), Timeout = TimeSpan.FromSeconds(10), }; //Default Request Headers needed to be added in the HttpClient Object httpClient.DefaultRequestHeaders.Add(BrevoDefaults.ApiKeyHeader, apiKey); httpClient.DefaultRequestHeaders.Add(BrevoDefaults.SibPluginHeader, BrevoDefaults.PluginVersion); httpClient.DefaultRequestHeaders.Add(HeaderNames.UserAgent, BrevoDefaults.UserAgentAccountAPI); httpClient.DefaultRequestHeaders.Add(HeaderNames.Accept, MimeTypes.ApplicationJson); var requestObject = new JObject { { "partnerName", BrevoDefaults.PartnerName }, { "active", true }, { "plugin_version", "1.0.0" }, { "shop_version", NopVersion.FULL_VERSION }, { "shop_url", storeUrl }, { "created_at", DateTime.UtcNow }, { "activated_at", DateTime.UtcNow }, { "type", "sib" } }; var requestString = JsonConvert.SerializeObject(requestObject); var requestContent = new StringContent(requestString, Encoding.Default, MimeTypes.ApplicationJson); var requestMessage = new HttpRequestMessage(HttpMethod.Post, "partner/information") { Content = requestContent }; var httpResponse = await httpClient.SendAsync(requestMessage); httpResponse.EnsureSuccessStatusCode(); } } /// /// 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 ContactsApi(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 ContactsApi(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 ContactsApi(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 }