mirror of
https://github.com/aljazceru/btcpayserver.git
synced 2026-01-31 11:54:24 +01:00
Merge pull request #6629 from btcpayserver/feat/email-rules-refactor
Email rules refactor
This commit is contained in:
@@ -828,19 +828,93 @@ namespace BTCPayServer.Tests
|
||||
Assert.DoesNotContain("You need to configure email settings before this feature works", s.Driver.PageSource);
|
||||
|
||||
s.Driver.FindElement(By.Id("CreateEmailRule")).Click();
|
||||
var select = new SelectElement(s.Driver.FindElement(By.Id("Rules_0__Trigger")));
|
||||
select.SelectByText("An invoice has been settled", true);
|
||||
s.Driver.FindElement(By.Id("Rules_0__To")).SendKeys("test@gmail.com");
|
||||
s.Driver.FindElement(By.Id("Rules_0__CustomerEmail")).Click();
|
||||
s.Driver.FindElement(By.Id("Rules_0__Subject")).SendKeys("Thanks!");
|
||||
var select = new SelectElement(s.Driver.FindElement(By.Id("Trigger")));
|
||||
select.SelectByValue("InvoicePaymentSettled");
|
||||
s.Driver.FindElement(By.Id("To")).SendKeys("test@gmail.com");
|
||||
s.Driver.FindElement(By.Id("CustomerEmail")).Click();
|
||||
s.Driver.FindElement(By.Id("Subject")).SendKeys("Thanks!");
|
||||
s.Driver.FindElement(By.ClassName("note-editable")).SendKeys("Your invoice is settled");
|
||||
s.Driver.FindElement(By.Id("SaveEmailRules")).Click();
|
||||
Assert.Contains("Store email rules saved", s.FindAlertMessage().Text);
|
||||
// we now have a rule
|
||||
Assert.DoesNotContain("There are no rules yet.", s.Driver.PageSource);
|
||||
Assert.Contains("test@gmail.com", s.Driver.PageSource);
|
||||
|
||||
s.GoToStore(StoreNavPages.Emails);
|
||||
Assert.True(s.Driver.FindElement(By.Id("IsCustomSMTP")).Selected);
|
||||
}
|
||||
|
||||
[Fact(Timeout = TestTimeout)]
|
||||
public async Task CanSetupEmailRules()
|
||||
{
|
||||
using var s = CreateSeleniumTester();
|
||||
await s.StartAsync();
|
||||
s.RegisterNewUser(true);
|
||||
s.CreateNewStore();
|
||||
|
||||
// Store Email Rules
|
||||
s.GoToStore(StoreNavPages.Emails);
|
||||
s.Driver.FindElement(By.Id("ConfigureEmailRules")).Click();
|
||||
Assert.Contains("There are no rules yet.", s.Driver.PageSource);
|
||||
Assert.Contains("You need to configure email settings before this feature works", s.Driver.PageSource);
|
||||
|
||||
// invoice created rule
|
||||
s.Driver.FindElement(By.Id("CreateEmailRule")).Click();
|
||||
var select = new SelectElement(s.Driver.FindElement(By.Id("Trigger")));
|
||||
select.SelectByValue("InvoiceCreated");
|
||||
s.Driver.FindElement(By.Id("To")).SendKeys("invoicecreated@gmail.com");
|
||||
s.Driver.FindElement(By.Id("CustomerEmail")).Click();
|
||||
s.Driver.FindElement(By.Id("SaveEmailRules")).Click();
|
||||
|
||||
// Ensure that the rule is created
|
||||
Assert.DoesNotContain("There are no rules yet.", s.Driver.PageSource);
|
||||
Assert.Contains("invoicecreated@gmail.com", s.Driver.PageSource);
|
||||
Assert.Contains("Invoice {Invoice.Id} created", s.Driver.PageSource);
|
||||
Assert.Contains("Yes", s.Driver.PageSource);
|
||||
|
||||
// payment request status changed rule
|
||||
s.Driver.FindElement(By.Id("CreateEmailRule")).Click();
|
||||
select = new SelectElement(s.Driver.FindElement(By.Id("Trigger")));
|
||||
select.SelectByValue("PaymentRequestStatusChanged");
|
||||
s.Driver.FindElement(By.Id("To")).SendKeys("statuschanged@gmail.com");
|
||||
s.Driver.FindElement(By.Id("Subject")).SendKeys("Status changed!");
|
||||
s.Driver.FindElement(By.ClassName("note-editable")).SendKeys("Your Payment Request Status is Changed");
|
||||
s.Driver.FindElement(By.Id("SaveEmailRules")).Click();
|
||||
|
||||
// Validate the second rule is added
|
||||
Assert.Contains("statuschanged@gmail.com", s.Driver.PageSource);
|
||||
Assert.Contains("Status changed!", s.Driver.PageSource);
|
||||
|
||||
// Select the second rule’s edit button
|
||||
var editButtons = s.Driver.FindElements(By.XPath("//a[contains(text(), 'Edit')]"));
|
||||
Assert.True(editButtons.Count >= 2, "Expected at least two edit buttons but found fewer.");
|
||||
|
||||
editButtons[1].Click(); // Clicks the second Edit button
|
||||
|
||||
// Modify the second rule from statuschanged@gmail.com to changedagain@gmail.com
|
||||
var toField = s.Driver.FindElement(By.Id("To"));
|
||||
toField.Clear();
|
||||
toField.SendKeys("changedagain@gmail.com");
|
||||
s.Driver.FindElement(By.Id("SaveEmailRules")).Click();
|
||||
|
||||
// Validate that the email is updated in the list of email rules
|
||||
Assert.Contains("changedagain@gmail.com", s.Driver.PageSource);
|
||||
Assert.DoesNotContain("statuschanged@gmail.com", s.Driver.PageSource);
|
||||
|
||||
// Delete both email rules
|
||||
var deleteLinks = s.Driver.FindElements(By.XPath("//a[contains(text(), 'Delete')]"));
|
||||
Assert.True(deleteLinks.Count == 2, "Expected exactly two delete buttons but found a different number.");
|
||||
|
||||
deleteLinks[0].Click();
|
||||
|
||||
deleteLinks = s.Driver.FindElements(By.XPath("//a[contains(text(), 'Delete')]")); // Refresh list
|
||||
Assert.True(deleteLinks.Count == 1, "Expected one delete button remaining.");
|
||||
|
||||
deleteLinks[0].Click();
|
||||
|
||||
// Validate that there are no more rules
|
||||
Assert.Contains("There are no rules yet.", s.Driver.PageSource);
|
||||
}
|
||||
|
||||
[Fact(Timeout = TestTimeout)]
|
||||
public async Task CanUseDynamicDns()
|
||||
{
|
||||
@@ -3883,7 +3957,6 @@ retry:
|
||||
s.Server.ActivateLightning();
|
||||
await s.StartAsync();
|
||||
await s.Server.EnsureChannelsSetup();
|
||||
var storeSettingsPaths = new [] {"settings", "rates", "checkout", "tokens", "users", "roles", "webhooks", "payout-processors", "payout-processors/onchain-automated/BTC", "payout-processors/lightning-automated/BTC", "emails", "email-settings", "forms"};
|
||||
|
||||
// Setup user, store and wallets
|
||||
s.RegisterNewUser();
|
||||
@@ -3916,6 +3989,10 @@ retry:
|
||||
s.AssertPageAccess(false, GetStorePath("lightning/BTC"));
|
||||
s.AssertPageAccess(false, GetStorePath("lightning/BTC/settings"));
|
||||
s.AssertPageAccess(false, GetStorePath("apps/create"));
|
||||
|
||||
var storeSettingsPaths = new [] {"settings", "rates", "checkout", "tokens", "users", "roles", "webhooks",
|
||||
"payout-processors", "payout-processors/onchain-automated/BTC", "payout-processors/lightning-automated/BTC",
|
||||
"emails/rules", "email-settings", "forms"};
|
||||
foreach (var path in storeSettingsPaths)
|
||||
{ // should have view access to settings, but no submit buttons or create links
|
||||
TestLogs.LogInformation($"Checking access to store page {path} as admin");
|
||||
@@ -3936,7 +4013,8 @@ retry:
|
||||
s.Server.ActivateLightning();
|
||||
await s.StartAsync();
|
||||
await s.Server.EnsureChannelsSetup();
|
||||
var storeSettingsPaths = new [] {"settings", "rates", "checkout", "tokens", "users", "roles", "webhooks", "payout-processors", "payout-processors/onchain-automated/BTC", "payout-processors/lightning-automated/BTC", "emails", "email-settings", "forms"};
|
||||
var storeSettingsPaths = new [] {"settings", "rates", "checkout", "tokens", "users", "roles", "webhooks", "payout-processors",
|
||||
"payout-processors/onchain-automated/BTC", "payout-processors/lightning-automated/BTC", "emails/rules", "email-settings", "forms"};
|
||||
|
||||
// Setup users
|
||||
var manager = s.RegisterNewUser();
|
||||
|
||||
@@ -19,154 +19,6 @@ namespace BTCPayServer.Controllers;
|
||||
|
||||
public partial class UIStoresController
|
||||
{
|
||||
[HttpGet("{storeId}/emails")]
|
||||
public async Task<IActionResult> StoreEmails(string storeId)
|
||||
{
|
||||
var store = HttpContext.GetStoreData();
|
||||
if (store == null)
|
||||
return NotFound();
|
||||
|
||||
var configured = await _emailSenderFactory.IsComplete(store.Id);
|
||||
if (!configured && !TempData.HasStatusMessage())
|
||||
{
|
||||
TempData.SetStatusMessageModel(new StatusMessageModel
|
||||
{
|
||||
Severity = StatusMessageModel.StatusSeverity.Warning,
|
||||
Html = "You need to configure email settings before this feature works." +
|
||||
$" <a class='alert-link' href='{Url.Action("StoreEmailSettings", new { storeId })}'>Configure store email settings</a>."
|
||||
});
|
||||
}
|
||||
|
||||
var vm = new StoreEmailRuleViewModel { Rules = store.GetStoreBlob().EmailRules ?? [] };
|
||||
return View(vm);
|
||||
}
|
||||
|
||||
[HttpPost("{storeId}/emails")]
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
|
||||
public async Task<IActionResult> StoreEmails(string storeId, StoreEmailRuleViewModel vm, string command)
|
||||
{
|
||||
vm.Rules ??= [];
|
||||
int commandIndex = 0;
|
||||
|
||||
var indSep = command.Split(':', StringSplitOptions.RemoveEmptyEntries);
|
||||
if (indSep.Length > 1)
|
||||
{
|
||||
commandIndex = int.Parse(indSep[1], CultureInfo.InvariantCulture);
|
||||
}
|
||||
|
||||
if (command.StartsWith("remove", StringComparison.InvariantCultureIgnoreCase))
|
||||
{
|
||||
vm.Rules.RemoveAt(commandIndex);
|
||||
}
|
||||
else if (command == "add")
|
||||
{
|
||||
vm.Rules.Add(new StoreEmailRule());
|
||||
|
||||
return View(vm);
|
||||
}
|
||||
|
||||
for (var i = 0; i < vm.Rules.Count; i++)
|
||||
{
|
||||
var rule = vm.Rules[i];
|
||||
|
||||
if (!string.IsNullOrEmpty(rule.To) && rule.To.Split(',', StringSplitOptions.RemoveEmptyEntries)
|
||||
.Any(s => !MailboxAddressValidator.TryParse(s, out _)))
|
||||
{
|
||||
ModelState.AddModelError($"{nameof(vm.Rules)}[{i}].{nameof(rule.To)}",
|
||||
StringLocalizer["Invalid mailbox address provided. Valid formats are: '{0}' or '{1}'", "test@example.com", "Firstname Lastname <test@example.com>"]);
|
||||
}
|
||||
else if (!rule.CustomerEmail && string.IsNullOrEmpty(rule.To))
|
||||
ModelState.AddModelError($"{nameof(vm.Rules)}[{i}].{nameof(rule.To)}",
|
||||
StringLocalizer["Either recipient or \"Send the email to the buyer\" is required"]);
|
||||
}
|
||||
|
||||
if (!ModelState.IsValid)
|
||||
{
|
||||
return View(vm);
|
||||
}
|
||||
|
||||
var store = HttpContext.GetStoreData();
|
||||
|
||||
if (store == null)
|
||||
return NotFound();
|
||||
|
||||
string message = "";
|
||||
|
||||
// update rules
|
||||
var blob = store.GetStoreBlob();
|
||||
blob.EmailRules = vm.Rules;
|
||||
if (store.SetStoreBlob(blob))
|
||||
{
|
||||
await _storeRepo.UpdateStore(store);
|
||||
message += StringLocalizer["Store email rules saved."] + " ";
|
||||
}
|
||||
|
||||
if (command.StartsWith("test", StringComparison.InvariantCultureIgnoreCase))
|
||||
{
|
||||
try
|
||||
{
|
||||
var rule = vm.Rules[commandIndex];
|
||||
if (await _emailSenderFactory.IsComplete(store.Id))
|
||||
{
|
||||
var recipients = rule.To.Split(",", StringSplitOptions.RemoveEmptyEntries)
|
||||
.Select(o =>
|
||||
{
|
||||
MailboxAddressValidator.TryParse(o, out var mb);
|
||||
return mb;
|
||||
})
|
||||
.Where(o => o != null)
|
||||
.ToArray();
|
||||
|
||||
var emailSender = await _emailSenderFactory.GetEmailSender(store.Id);
|
||||
emailSender.SendEmail(recipients.ToArray(), null, null, $"[TEST] {rule.Subject}", rule.Body);
|
||||
message += StringLocalizer["Test email sent — please verify you received it."];
|
||||
}
|
||||
else
|
||||
{
|
||||
message += StringLocalizer["Complete the email setup to send test emails."];
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
TempData[WellKnownTempData.ErrorMessage] = message + StringLocalizer["Error sending test email: {0}", ex.Message].Value;
|
||||
return RedirectToAction("StoreEmails", new { storeId });
|
||||
}
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(message))
|
||||
{
|
||||
TempData.SetStatusMessageModel(new StatusMessageModel
|
||||
{
|
||||
Severity = StatusMessageModel.StatusSeverity.Success,
|
||||
Message = message
|
||||
});
|
||||
}
|
||||
|
||||
return RedirectToAction("StoreEmails", new { storeId });
|
||||
}
|
||||
|
||||
public class StoreEmailRuleViewModel
|
||||
{
|
||||
public List<StoreEmailRule> Rules { get; set; }
|
||||
}
|
||||
|
||||
public class StoreEmailRule
|
||||
{
|
||||
[Required]
|
||||
public string Trigger { get; set; }
|
||||
|
||||
public bool CustomerEmail { get; set; }
|
||||
|
||||
|
||||
public string To { get; set; }
|
||||
|
||||
[Required]
|
||||
public string Subject { get; set; }
|
||||
|
||||
[Required]
|
||||
public string Body { get; set; }
|
||||
}
|
||||
|
||||
[HttpGet("{storeId}/email-settings")]
|
||||
public async Task<IActionResult> StoreEmailSettings(string storeId)
|
||||
{
|
||||
|
||||
151
BTCPayServer/Controllers/UIStoresController.EmailRules.cs
Normal file
151
BTCPayServer/Controllers/UIStoresController.EmailRules.cs
Normal file
@@ -0,0 +1,151 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Abstractions.Constants;
|
||||
using BTCPayServer.Abstractions.Extensions;
|
||||
using BTCPayServer.Abstractions.Models;
|
||||
using BTCPayServer.Client;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Models;
|
||||
using BTCPayServer.Services.Mails;
|
||||
using BTCPayServer.Services.Stores;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace BTCPayServer.Controllers
|
||||
{
|
||||
public partial class UIStoresController
|
||||
{
|
||||
[HttpGet("{storeId}/emails/rules")]
|
||||
public async Task<IActionResult> StoreEmailRulesList(string storeId)
|
||||
{
|
||||
var store = HttpContext.GetStoreData();
|
||||
if (store == null) return NotFound();
|
||||
|
||||
var configured = await _emailSenderFactory.IsComplete(store.Id);
|
||||
if (!configured && !TempData.HasStatusMessage())
|
||||
{
|
||||
TempData.SetStatusMessageModel(new StatusMessageModel
|
||||
{
|
||||
Severity = StatusMessageModel.StatusSeverity.Warning,
|
||||
Html = "You need to configure email settings before this feature works." +
|
||||
$" <a class='alert-link' href='{Url.Action("StoreEmailSettings", new { storeId })}'>Configure store email settings</a>."
|
||||
});
|
||||
}
|
||||
|
||||
var rules = store.GetStoreBlob().EmailRules ?? new List<StoreEmailRule>();
|
||||
return View("StoreEmailRulesList", rules);
|
||||
}
|
||||
|
||||
[HttpGet("{storeId}/emails/rules/create")]
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
|
||||
public IActionResult StoreEmailRulesCreate(string storeId)
|
||||
{
|
||||
return View("StoreEmailRulesManage", new StoreEmailRule());
|
||||
}
|
||||
|
||||
[HttpPost("{storeId}/emails/rules/create")]
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
|
||||
public async Task<IActionResult> StoreEmailRulesCreate(string storeId, StoreEmailRule model)
|
||||
{
|
||||
if (!ModelState.IsValid)
|
||||
return View("StoreEmailRulesManage", model);
|
||||
|
||||
var store = await _storeRepo.FindStore(storeId);
|
||||
if (store == null) return NotFound();
|
||||
|
||||
var blob = store.GetStoreBlob();
|
||||
var rulesList = blob.EmailRules ?? new List<StoreEmailRule>();
|
||||
rulesList.Add(new StoreEmailRule
|
||||
{
|
||||
Trigger = model.Trigger,
|
||||
CustomerEmail = model.CustomerEmail,
|
||||
To = model.To,
|
||||
Subject = model.Subject,
|
||||
Body = model.Body
|
||||
});
|
||||
|
||||
blob.EmailRules = rulesList;
|
||||
store.SetStoreBlob(blob);
|
||||
await _storeRepo.UpdateStore(store);
|
||||
|
||||
return RedirectToAction(nameof(StoreEmailRulesList), new { storeId });
|
||||
}
|
||||
|
||||
[HttpGet("{storeId}/emails/rules/{ruleIndex}/edit")]
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
|
||||
public IActionResult StoreEmailRulesEdit(string storeId, int ruleIndex)
|
||||
{
|
||||
var store = HttpContext.GetStoreData();
|
||||
if (store == null) return NotFound();
|
||||
|
||||
var rules = store.GetStoreBlob().EmailRules;
|
||||
if (rules == null || ruleIndex >= rules.Count) return NotFound();
|
||||
|
||||
var rule = rules[ruleIndex];
|
||||
return View("StoreEmailRulesManage", rule);
|
||||
}
|
||||
|
||||
[HttpPost("{storeId}/emails/rules/{ruleIndex}/edit")]
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
|
||||
public async Task<IActionResult> StoreEmailRulesEdit(string storeId, int ruleIndex, StoreEmailRule model)
|
||||
{
|
||||
if (!ModelState.IsValid)
|
||||
return View("StoreEmailRulesManage", model);
|
||||
|
||||
var store = await _storeRepo.FindStore(storeId);
|
||||
if (store == null) return NotFound();
|
||||
|
||||
var blob = store.GetStoreBlob();
|
||||
if (blob.EmailRules == null || ruleIndex >= blob.EmailRules.Count) return NotFound();
|
||||
|
||||
var rule = blob.EmailRules[ruleIndex];
|
||||
rule.Trigger = model.Trigger;
|
||||
rule.CustomerEmail = model.CustomerEmail;
|
||||
rule.To = model.To;
|
||||
rule.Subject = model.Subject;
|
||||
rule.Body = model.Body;
|
||||
store.SetStoreBlob(blob);
|
||||
await _storeRepo.UpdateStore(store);
|
||||
|
||||
return RedirectToAction(nameof(StoreEmailRulesList), new { storeId });
|
||||
}
|
||||
|
||||
[HttpPost("{storeId}/emails/rules/{ruleIndex}/delete")]
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
|
||||
public async Task<IActionResult> StoreEmailRulesDelete(string storeId, int ruleIndex)
|
||||
{
|
||||
var store = await _storeRepo.FindStore(storeId);
|
||||
if (store == null) return NotFound();
|
||||
|
||||
var blob = store.GetStoreBlob();
|
||||
if (blob.EmailRules == null || ruleIndex >= blob.EmailRules.Count) return NotFound();
|
||||
|
||||
blob.EmailRules.RemoveAt(ruleIndex);
|
||||
store.SetStoreBlob(blob);
|
||||
await _storeRepo.UpdateStore(store);
|
||||
|
||||
return RedirectToAction(nameof(StoreEmailRulesList), new { storeId });
|
||||
}
|
||||
|
||||
public class StoreEmailRule
|
||||
{
|
||||
[Required]
|
||||
public string Trigger { get; set; }
|
||||
|
||||
public bool CustomerEmail { get; set; }
|
||||
|
||||
public string To { get; set; }
|
||||
|
||||
[Required]
|
||||
public string Subject { get; set; }
|
||||
|
||||
[Required]
|
||||
public string Body { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,14 +1,21 @@
|
||||
#nullable enable
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Client.Models;
|
||||
using BTCPayServer.Controllers;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Events;
|
||||
using BTCPayServer.HostedServices.Webhooks;
|
||||
using BTCPayServer.Services;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using NBitcoin;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using WebhookDeliveryData = BTCPayServer.Data.WebhookDeliveryData;
|
||||
|
||||
namespace BTCPayServer.HostedServices;
|
||||
|
||||
@@ -239,4 +246,5 @@ public class PendingTransactionService(
|
||||
pt.State = PendingTransactionState.Broadcast;
|
||||
await ctx.SaveChangesAsync();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -22,13 +22,13 @@ public class InvoiceWebhookProvider : WebhookProvider<InvoiceEvent>
|
||||
{
|
||||
return new Dictionary<string, string>
|
||||
{
|
||||
{WebhookEventType.InvoiceCreated, "A new invoice has been created"},
|
||||
{WebhookEventType.InvoiceReceivedPayment, "A new payment has been received"},
|
||||
{WebhookEventType.InvoicePaymentSettled, "A payment has been settled"},
|
||||
{WebhookEventType.InvoiceProcessing, "An invoice is processing"},
|
||||
{WebhookEventType.InvoiceExpired, "An invoice has expired"},
|
||||
{WebhookEventType.InvoiceSettled, "An invoice has been settled"},
|
||||
{WebhookEventType.InvoiceInvalid, "An invoice became invalid"},
|
||||
{WebhookEventType.InvoiceCreated, "Invoice - Created"},
|
||||
{WebhookEventType.InvoiceReceivedPayment, "Invoice - Received Payment"},
|
||||
{WebhookEventType.InvoicePaymentSettled, "Invoice - Payment Settled"},
|
||||
{WebhookEventType.InvoiceProcessing, "Invoice - Is Processing"},
|
||||
{WebhookEventType.InvoiceExpired, "Invoice - Expired"},
|
||||
{WebhookEventType.InvoiceSettled, "Invoice - Is Settled"},
|
||||
{WebhookEventType.InvoiceInvalid, "Invoice - Became Invalid"},
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -18,10 +18,10 @@ public class PaymentRequestWebhookProvider: WebhookProvider<PaymentRequestEvent>
|
||||
{
|
||||
return new Dictionary<string, string>()
|
||||
{
|
||||
{WebhookEventType.PaymentRequestCreated, "Payment Request Created"},
|
||||
{WebhookEventType.PaymentRequestUpdated, "Payment Request Updated"},
|
||||
{WebhookEventType.PaymentRequestArchived, "Payment Request Archived"},
|
||||
{WebhookEventType.PaymentRequestStatusChanged, "Payment Request Status Changed"},
|
||||
{WebhookEventType.PaymentRequestCreated, "Payment Request - Created"},
|
||||
{WebhookEventType.PaymentRequestUpdated, "Payment Request - Updated"},
|
||||
{WebhookEventType.PaymentRequestArchived, "Payment Request - Archived"},
|
||||
{WebhookEventType.PaymentRequestStatusChanged, "Payment Request - Status Changed"},
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -36,9 +36,9 @@ public class PayoutWebhookProvider(EventAggregator eventAggregator, ILogger<Payo
|
||||
{
|
||||
return new Dictionary<string, string>()
|
||||
{
|
||||
{WebhookEventType.PayoutCreated, "A payout has been created"},
|
||||
{WebhookEventType.PayoutApproved, "A payout has been approved"},
|
||||
{WebhookEventType.PayoutUpdated, "A payout was updated"}
|
||||
{WebhookEventType.PayoutCreated, "Payout - Created"},
|
||||
{WebhookEventType.PayoutApproved, "Payout - Approved"},
|
||||
{WebhookEventType.PayoutUpdated, "Payout - Updated"}
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -523,6 +523,8 @@ namespace BTCPayServer.Services
|
||||
"Email password reset functionality is not configured for this server. Please contact the server administrator to assist with account recovery.": "",
|
||||
"email rules": "",
|
||||
"Email Rules": "",
|
||||
"Create Email Rule": "",
|
||||
"Edit Email Rule": "",
|
||||
"Email rules allow BTCPay Server to send customized emails from your store based on events.": "",
|
||||
"Email sent to {0}. Please verify you received it.": "",
|
||||
"Email Server": "",
|
||||
|
||||
70
BTCPayServer/Views/UIStores/StoreEmailRulesList.cshtml
Normal file
70
BTCPayServer/Views/UIStores/StoreEmailRulesList.cshtml
Normal file
@@ -0,0 +1,70 @@
|
||||
@using BTCPayServer.Client
|
||||
@using BTCPayServer.TagHelpers
|
||||
@using Microsoft.AspNetCore.Mvc.TagHelpers
|
||||
@model List<BTCPayServer.Controllers.UIStoresController.StoreEmailRule>
|
||||
|
||||
@{
|
||||
var storeId = Context.GetStoreData().Id;
|
||||
ViewData.SetActivePage(StoreNavPages.Emails, StringLocalizer["Email Rules"], storeId);
|
||||
}
|
||||
<div class="sticky-header">
|
||||
<nav aria-label="breadcrumb">
|
||||
<ol class="breadcrumb">
|
||||
<li class="breadcrumb-item">
|
||||
<a asp-action="StoreEmailSettings" asp-route-storeId="@storeId" text-translate="true">Emails</a>
|
||||
</li>
|
||||
<li class="breadcrumb-item active" aria-current="page">@ViewData["Title"]</li>
|
||||
</ol>
|
||||
<h2>@ViewData["Title"]</h2>
|
||||
</nav>
|
||||
<a id="CreateEmailRule" permission="@Policies.CanModifyStoreSettings" asp-action="StoreEmailRulesCreate" asp-route-storeId="@storeId"
|
||||
class="btn btn-primary" role="button">
|
||||
Create Email Rule
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<partial name="_StatusMessage" />
|
||||
<p text-translate="true">
|
||||
Email rules allow BTCPay Server to send customized emails from your store based on events.
|
||||
</p>
|
||||
|
||||
@if (Model.Any())
|
||||
{
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Trigger</th>
|
||||
<th>Customer Email</th>
|
||||
<th>To</th>
|
||||
<th>Subject</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach (var rule in Model.Select((value, index) => new { value, index }))
|
||||
{
|
||||
<tr>
|
||||
<td>@rule.value.Trigger</td>
|
||||
<td>@(rule.value.CustomerEmail ? "Yes" : "No")</td>
|
||||
<td>@rule.value.To</td>
|
||||
<td>@rule.value.Subject</td>
|
||||
<td>
|
||||
<a asp-action="StoreEmailRulesEdit" asp-route-storeId="@storeId" asp-route-ruleIndex="@rule.index">Edit</a>
|
||||
-
|
||||
<form asp-action="StoreEmailRulesDelete" asp-route-storeId="@storeId" asp-route-ruleIndex="@rule.index" method="post" style="display:inline;">
|
||||
<a href="#" class="text-danger" onclick="event.preventDefault(); this.closest('form').submit();">Delete</a>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<p class="text-secondary" text-translate="true">
|
||||
There are no rules yet.
|
||||
</p>
|
||||
}
|
||||
186
BTCPayServer/Views/UIStores/StoreEmailRulesManage.cshtml
Normal file
186
BTCPayServer/Views/UIStores/StoreEmailRulesManage.cshtml
Normal file
@@ -0,0 +1,186 @@
|
||||
@using BTCPayServer.HostedServices.Webhooks
|
||||
@using Microsoft.AspNetCore.Mvc.TagHelpers
|
||||
@model BTCPayServer.Controllers.UIStoresController.StoreEmailRule
|
||||
@inject WebhookSender WebhookSender
|
||||
|
||||
@{
|
||||
var storeId = Context.GetStoreData().Id;
|
||||
bool isEdit = Model.Trigger != null;
|
||||
ViewData.SetActivePage(StoreNavPages.Emails, StringLocalizer[isEdit ? "Edit Email Rule" : "Create Email Rule"], storeId);
|
||||
}
|
||||
|
||||
@section PageHeadContent {
|
||||
<link href="~/vendor/summernote/summernote-bs5.css" rel="stylesheet" asp-append-version="true" />
|
||||
}
|
||||
|
||||
<form asp-action="@(isEdit ? "StoreEmailRulesEdit" : "StoreEmailRulesCreate")" asp-route-storeId="@storeId" method="post">
|
||||
<div class="sticky-header">
|
||||
<nav aria-label="breadcrumb">
|
||||
<ol class="breadcrumb">
|
||||
<li class="breadcrumb-item">
|
||||
<a asp-action="StoreEmailSettings" asp-route-storeId="@storeId" text-translate="true">Emails</a>
|
||||
</li>
|
||||
<li class="breadcrumb-item">
|
||||
<a asp-action="StoreEmailRulesList" asp-route-storeId="@storeId" text-translate="true">Email Rules</a>
|
||||
</li>
|
||||
<li class="breadcrumb-item active" aria-current="page">@ViewData["Title"]</li>
|
||||
</ol>
|
||||
<h2>@ViewData["Title"]</h2>
|
||||
</nav>
|
||||
<div>
|
||||
<button id="SaveEmailRules" type="submit" class="btn btn-primary">Save</button>
|
||||
<a asp-action="StoreEmailRulesList" asp-route-storeId="@storeId" class="btn btn-secondary">Cancel</a>
|
||||
</div>
|
||||
</div>
|
||||
<partial name="_StatusMessage" />
|
||||
|
||||
<div class="form-group">
|
||||
<label asp-for="Trigger" class="form-label" data-required></label>
|
||||
<select asp-for="Trigger" asp-items="@WebhookSender.GetSupportedWebhookTypes()
|
||||
.OrderBy(a=>a.Value).Select(s => new SelectListItem(s.Value, s.Key))"
|
||||
class="form-select email-rule-trigger" required></select>
|
||||
<span asp-validation-for="Trigger" class="text-danger"></span>
|
||||
<div class="form-text" text-translate="true">Choose what event sends the email.</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label asp-for="To" class="form-label">Recipients</label>
|
||||
<input type="text" asp-for="To" class="form-control email-rule-to" />
|
||||
<span asp-validation-for="To" class="text-danger"></span>
|
||||
<div class="form-text" text-translate="true">Who to send the email to. For multiple emails, separate with a comma.</div>
|
||||
</div>
|
||||
|
||||
<div class="form-check mb-4">
|
||||
<input asp-for="CustomerEmail" type="checkbox" class="form-check-input email-rule-customer-email" />
|
||||
<label asp-for="CustomerEmail" class="form-check-label" text-translate="true">Send the email to the buyer, if email was provided to the invoice</label>
|
||||
<span asp-validation-for="CustomerEmail" class="text-danger"></span>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label asp-for="Subject" class="form-label" data-required></label>
|
||||
<input type="text" asp-for="Subject" class="form-control email-rule-subject" />
|
||||
<span asp-validation-for="Subject" class="text-danger"></span>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label asp-for="Body" class="form-label" data-required></label>
|
||||
<textarea asp-for="Body" class="form-control richtext email-rule-body" rows="4"></textarea>
|
||||
<span asp-validation-for="Body" class="text-danger"></span>
|
||||
<div class="form-text rounded bg-light p-2">
|
||||
<table class="table table-sm caption-top m-0">
|
||||
<caption class="text-muted p-0" text-translate="true">Placeholders</caption>
|
||||
<tr>
|
||||
<th text-translate="true">Invoice</th>
|
||||
<td>
|
||||
<code>{Invoice.Id}</code>,
|
||||
<code>{Invoice.StoreId}</code>,
|
||||
<code>{Invoice.Price}</code>,
|
||||
<code>{Invoice.Currency}</code>,
|
||||
<code>{Invoice.Status}</code>,
|
||||
<code>{Invoice.AdditionalStatus}</code>,
|
||||
<code>{Invoice.OrderId}</code>
|
||||
<code>{Invoice.Metadata}*</code>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th text-translate="true">Request</th>
|
||||
<td>
|
||||
<code>{PaymentRequest.Id}</code>,
|
||||
<code>{PaymentRequest.Price}</code>,
|
||||
<code>{PaymentRequest.Currency}</code>,
|
||||
<code>{PaymentRequest.Title}</code>,
|
||||
<code>{PaymentRequest.Description}</code>,
|
||||
<code>{PaymentRequest.Status}</code>
|
||||
<code>{PaymentRequest.FormResponse}*</code>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th text-translate="true">Payout</th>
|
||||
<td>
|
||||
<code>{Payout.Id}</code>,
|
||||
<code>{Payout.PullPaymentId}</code>,
|
||||
<code>{Payout.Destination}</code>,
|
||||
<code>{Payout.State}</code>
|
||||
<code>{Payout.Metadata}*</code>
|
||||
</td>
|
||||
</tr>
|
||||
<tr><td colspan="2">* These fields are JSON objects. You can access properties within them using <a href="https://www.newtonsoft.com/json/help/html/SelectToken.htm#SelectTokenJSONPath" rel="noreferrer noopener" target="_blank">this syntax</a>. One example is <code>{Invoice.Metadata.itemCode}</code></td></tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</form>
|
||||
|
||||
@section PageFootContent {
|
||||
<partial name="_ValidationScriptsPartial" />
|
||||
<script src="~/vendor/summernote/summernote-bs5.js" asp-append-version="true"></script>
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const templates = {
|
||||
InvoiceCreated: {
|
||||
subject: 'Invoice {Invoice.Id} created',
|
||||
body: 'Invoice {Invoice.Id} (Order Id: {Invoice.OrderId}) created.'
|
||||
},
|
||||
InvoiceReceivedPayment: {
|
||||
subject: 'Invoice {Invoice.Id} received payment',
|
||||
body: 'Invoice {Invoice.Id} (Order Id: {Invoice.OrderId}) received payment.'
|
||||
},
|
||||
InvoiceProcessing: {
|
||||
subject: 'Invoice {Invoice.Id} processing',
|
||||
body: 'Invoice {Invoice.Id} (Order Id: {Invoice.OrderId}) is processing.'
|
||||
},
|
||||
InvoiceExpired: {
|
||||
subject: 'Invoice {Invoice.Id} expired',
|
||||
body: 'Invoice {Invoice.Id} (Order Id: {Invoice.OrderId}) expired.'
|
||||
},
|
||||
InvoiceSettled: {
|
||||
subject: 'Invoice {Invoice.Id} settled',
|
||||
body: 'Invoice {Invoice.Id} (Order Id: {Invoice.OrderId}) is settled.'
|
||||
},
|
||||
InvoiceInvalid: {
|
||||
subject: 'Invoice {Invoice.Id} invalid',
|
||||
body: 'Invoice {Invoice.Id} (Order Id: {Invoice.OrderId}) invalid.'
|
||||
},
|
||||
InvoicePaymentSettled: {
|
||||
subject: 'Invoice {Invoice.Id} payment settled',
|
||||
body: 'Invoice {Invoice.Id} (Order Id: {Invoice.OrderId}) payment settled.'
|
||||
},
|
||||
};
|
||||
|
||||
const triggerSelect = document.querySelector('.email-rule-trigger');
|
||||
const subjectInput = document.querySelector('.email-rule-subject');
|
||||
const bodyTextarea = document.querySelector('.email-rule-body');
|
||||
|
||||
const isEmptyOrDefault = (value, type) => {
|
||||
const val = value.replace(/<.*?>/gi, '').trim();
|
||||
if (!val) return true;
|
||||
return Object.values(templates).some(t => t[type] === val);
|
||||
};
|
||||
|
||||
function applyTemplate() {
|
||||
const selectedTrigger = triggerSelect.value;
|
||||
if (templates[selectedTrigger]) {
|
||||
if (isEmptyOrDefault(subjectInput.value, 'subject')) {
|
||||
subjectInput.value = templates[selectedTrigger].subject;
|
||||
}
|
||||
if (isEmptyOrDefault(bodyTextarea.value, 'body')) {
|
||||
if ($(bodyTextarea).summernote) {
|
||||
$(bodyTextarea).summernote('reset');
|
||||
$(bodyTextarea).summernote('insertText', templates[selectedTrigger].body);
|
||||
} else {
|
||||
bodyTextarea.value = templates[selectedTrigger].body;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
triggerSelect.addEventListener('change', applyTemplate);
|
||||
|
||||
// Apply template on page load if a trigger is selected
|
||||
if (triggerSelect.value) {
|
||||
applyTemplate();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
}
|
||||
@@ -38,7 +38,8 @@
|
||||
<div class="mt-5" permission="@Policies.CanModifyStoreSettings">
|
||||
<h3 text-translate="true">Email Rules</h3>
|
||||
<p text-translate="true">Email rules allow BTCPay Server to send customized emails from your store based on events.</p>
|
||||
<a class="btn btn-secondary" asp-action="StoreEmails" asp-controller="UIStores" asp-route-storeId="@storeId" id="ConfigureEmailRules" permission="@Policies.CanModifyStoreSettings" text-translate="true">
|
||||
<a id="ConfigureEmailRules" class="btn btn-secondary" asp-controller="UIStores" asp-action="StoreEmailRulesList" asp-route-storeId="@storeId"
|
||||
permission="@Policies.CanViewStoreSettings" text-translate="true">
|
||||
Configure
|
||||
</a>
|
||||
</div>
|
||||
|
||||
@@ -1,211 +0,0 @@
|
||||
@using BTCPayServer.HostedServices.Webhooks
|
||||
@using Microsoft.AspNetCore.Mvc.TagHelpers
|
||||
@using BTCPayServer.Client
|
||||
@using BTCPayServer.Abstractions.TagHelpers
|
||||
@model BTCPayServer.Controllers.UIStoresController.StoreEmailRuleViewModel
|
||||
@inject WebhookSender WebhookSender
|
||||
|
||||
@{
|
||||
var storeId = Context.GetStoreData().Id;
|
||||
ViewData.SetActivePage(StoreNavPages.Emails, StringLocalizer["Email Rules"], storeId);
|
||||
}
|
||||
|
||||
@section PageHeadContent {
|
||||
<link href="~/vendor/summernote/summernote-bs5.css" rel="stylesheet" asp-append-version="true" />
|
||||
}
|
||||
|
||||
<form asp-action="StoreEmails" method="post" asp-route-storeId="@Context.GetStoreData().Id" permissioned="@Policies.CanModifyStoreSettings">
|
||||
<div class="sticky-header">
|
||||
<nav aria-label="breadcrumb">
|
||||
<ol class="breadcrumb">
|
||||
<li class="breadcrumb-item">
|
||||
<a asp-action="StoreEmailSettings" asp-route-storeId="@storeId" text-translate="true">Emails</a>
|
||||
</li>
|
||||
<li class="breadcrumb-item active" aria-current="page">@ViewData["Title"]</li>
|
||||
</ol>
|
||||
<h2>@ViewData["Title"]</h2>
|
||||
</nav>
|
||||
<div permission="@Policies.CanModifyStoreSettings">
|
||||
@if (Model.Rules.Any())
|
||||
{
|
||||
<button class="btn btn-primary" name="command" type="submit" value="save" id="SaveEmailRules" text-translate="true">
|
||||
Save
|
||||
</button>
|
||||
}
|
||||
<button class="btn btn-primary" name="command" type="submit" value="add" id="CreateEmailRule" text-translate="true">
|
||||
Create
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<partial name="_StatusMessage" />
|
||||
<p text-translate="true">
|
||||
Email rules allow BTCPay Server to send customized emails from your store based on events.
|
||||
</p>
|
||||
<div class="row">
|
||||
<div class="col-xxl-constrain">
|
||||
@if (!ViewContext.ModelState.IsValid)
|
||||
{
|
||||
<div asp-validation-summary="All"></div>
|
||||
}
|
||||
@if (Model.Rules.Any())
|
||||
{
|
||||
<ul class="list-group list-group-flush">
|
||||
@for (var index = 0; index < Model.Rules.Count; index++)
|
||||
{
|
||||
<li class="list-group-item py-4 px-0 email-rule">
|
||||
<div class="form-group">
|
||||
<div class="d-flex align-items-center justify-content-between gap-3">
|
||||
<label asp-for="Rules[index].Trigger" class="form-label" data-required></label>
|
||||
<div class="align-items-right">
|
||||
<button name="command" type="submit" value="test:@index" class="d-inline-block btn text-info btn-link p-0">
|
||||
<vc:icon symbol="actions-email" />
|
||||
<span text-translate="true">Test this email rule</span>
|
||||
</button>
|
||||
|
||||
<button name="command" type="submit" value="remove:@index" class="d-inline-block btn text-danger btn-link p-0">
|
||||
<vc:icon symbol="actions-remove" />
|
||||
<span text-translate="true">Remove this email rule</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<select asp-for="Rules[index].Trigger" asp-items="@WebhookSender.GetSupportedWebhookTypes().Select(s => new SelectListItem(s.Value, s.Key))" class="form-select email-rule-trigger" required></select>
|
||||
<span asp-validation-for="Rules[index].Trigger" class="text-danger"></span>
|
||||
<div class="form-text" text-translate="true">Choose what event sends the email.</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label asp-for="Rules[index].To" class="form-label">Recipients</label>
|
||||
<input type="text" asp-for="Rules[index].To" class="form-control email-rule-to" />
|
||||
<span asp-validation-for="Rules[index].To" class="text-danger"></span>
|
||||
<div class="form-text" text-translate="true">Who to send the email to. For multiple emails, separate with a comma.</div>
|
||||
</div>
|
||||
<div class="form-check mb-4">
|
||||
<input asp-for="Rules[index].CustomerEmail" type="checkbox" class="form-check-input email-rule-customer-email" />
|
||||
<label asp-for="Rules[index].CustomerEmail" class="form-check-label" text-translate="true">Send the email to the buyer, if email was provided to the invoice</label>
|
||||
<span asp-validation-for="Rules[index].CustomerEmail" class="text-danger"></span>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label asp-for="Rules[index].Subject" class="form-label" data-required></label>
|
||||
<input type="text" asp-for="Rules[index].Subject" class="form-control email-rule-subject" />
|
||||
<span asp-validation-for="Rules[index].Subject" class="text-danger"></span>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label asp-for="Rules[index].Body" class="form-label" data-required></label>
|
||||
<textarea asp-for="Rules[index].Body" class="form-control richtext email-rule-body" rows="4"></textarea>
|
||||
<span asp-validation-for="Rules[index].Body" class="text-danger"></span>
|
||||
<div class="form-text rounded bg-light p-2">
|
||||
<table class="table table-sm caption-top m-0">
|
||||
<caption class="text-muted p-0" text-translate="true">Placeholders</caption>
|
||||
<tr>
|
||||
<th text-translate="true">Invoice</th>
|
||||
<td>
|
||||
<code>{Invoice.Id}</code>,
|
||||
<code>{Invoice.StoreId}</code>,
|
||||
<code>{Invoice.Price}</code>,
|
||||
<code>{Invoice.Currency}</code>,
|
||||
<code>{Invoice.Status}</code>,
|
||||
<code>{Invoice.AdditionalStatus}</code>,
|
||||
<code>{Invoice.OrderId}</code>
|
||||
<code>{Invoice.Metadata}*</code>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th text-translate="true">Request</th>
|
||||
<td>
|
||||
<code>{PaymentRequest.Id}</code>,
|
||||
<code>{PaymentRequest.Price}</code>,
|
||||
<code>{PaymentRequest.Currency}</code>,
|
||||
<code>{PaymentRequest.Title}</code>,
|
||||
<code>{PaymentRequest.Description}</code>,
|
||||
<code>{PaymentRequest.Status}</code>
|
||||
<code>{PaymentRequest.FormResponse}*</code>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th text-translate="true">Payout</th>
|
||||
<td>
|
||||
<code>{Payout.Id}</code>,
|
||||
<code>{Payout.PullPaymentId}</code>,
|
||||
<code>{Payout.Destination}</code>,
|
||||
<code>{Payout.State}</code>
|
||||
<code>{Payout.Metadata}*</code>
|
||||
</td>
|
||||
</tr>
|
||||
<tr><td colspan="2">* These fields are JSON objects. You can access properties within them using <a href="https://www.newtonsoft.com/json/help/html/SelectToken.htm#SelectTokenJSONPath" rel="noreferrer noopener" target="_blank">this syntax</a>. One example is <code>{Invoice.Metadata.itemCode}</code></td></tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
}
|
||||
</ul>
|
||||
}
|
||||
else
|
||||
{
|
||||
<p class="text-secondary" text-translate="true">
|
||||
There are no rules yet.
|
||||
</p>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@section PageFootContent {
|
||||
<partial name="_ValidationScriptsPartial" />
|
||||
<script src="~/vendor/summernote/summernote-bs5.js" asp-append-version="true"></script>
|
||||
<script>
|
||||
(function () {
|
||||
const templates = {
|
||||
InvoiceCreated: {
|
||||
subject: 'Invoice {Invoice.Id} created',
|
||||
body: 'Invoice {Invoice.Id} (Order Id: {Invoice.OrderId}) created.'
|
||||
},
|
||||
InvoiceReceivedPayment: {
|
||||
subject: 'Invoice {Invoice.Id} received payment',
|
||||
body: 'Invoice {Invoice.Id} (Order Id: {Invoice.OrderId}) received payment.'
|
||||
},
|
||||
InvoiceProcessing: {
|
||||
subject: 'Invoice {Invoice.Id} processing',
|
||||
body: 'Invoice {Invoice.Id} (Order Id: {Invoice.OrderId}) is processing.'
|
||||
},
|
||||
InvoiceExpired: {
|
||||
subject: 'Invoice {Invoice.Id} expired',
|
||||
body: 'Invoice {Invoice.Id} (Order Id: {Invoice.OrderId}) expired.'
|
||||
},
|
||||
InvoiceSettled: {
|
||||
subject: 'Invoice {Invoice.Id} settled',
|
||||
body: 'Invoice {Invoice.Id} (Order Id: {Invoice.OrderId}) is settled.'
|
||||
},
|
||||
InvoiceInvalid: {
|
||||
subject: 'Invoice {Invoice.Id} invalid',
|
||||
body: 'Invoice {Invoice.Id} (Order Id: {Invoice.OrderId}) invalid.'
|
||||
},
|
||||
InvoicePaymentSettled: {
|
||||
subject: 'Invoice {Invoice.Id} payment settled',
|
||||
body: 'Invoice {Invoice.Id} (Order Id: {Invoice.OrderId}) payment settled.'
|
||||
},
|
||||
};
|
||||
const isEmptyOrDefault = (value, type) => {
|
||||
const val = value.replace(/<.*?>/gi, '').trim()
|
||||
if (!val) return true;
|
||||
return Object.values(templates).find(t => t[type] === val) != null;
|
||||
}
|
||||
const applyDefault = $trigger => {
|
||||
const $emailRule = $trigger.closest('.email-rule');
|
||||
const $subject = $emailRule.querySelector('.email-rule-subject');
|
||||
const $body = $emailRule.querySelector('.email-rule-body');
|
||||
const rule = $trigger.querySelector(`option[value='${$trigger.value}']`).innerText;
|
||||
const { subject, body } = templates[rule];
|
||||
if (isEmptyOrDefault($subject.value, 'subject') && subject) {
|
||||
$subject.value = subject;
|
||||
}
|
||||
if (isEmptyOrDefault($body.value, 'body') && body) {
|
||||
$($body).summernote('reset');
|
||||
$($body).summernote('insertText', body);
|
||||
}
|
||||
}
|
||||
delegate('change', '.email-rule-trigger', (e) => { applyDefault(e.target); })
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
document.querySelectorAll('.email-rule-trigger').forEach(applyDefault);
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
}
|
||||
Reference in New Issue
Block a user