mirror of
https://github.com/btcpayserver/btcpayserver.git
synced 2025-03-23 07:36:16 +01:00
Pluginize Webhooks and support Payouts (#5421)
Co-authored-by: d11n <mail@dennisreimann.de>
This commit is contained in:
parent
605741182d
commit
a97172cea6
47 changed files with 1265 additions and 706 deletions
3
.gitignore
vendored
3
.gitignore
vendored
|
@ -298,4 +298,5 @@ Packed Plugins
|
|||
Plugins/packed
|
||||
|
||||
BTCPayServer/wwwroot/swagger/v1/openapi.json
|
||||
BTCPayServer/appsettings.dev.json
|
||||
BTCPayServer/appsettings.dev.json
|
||||
BTCPayServer.Tests/monero_wallet
|
||||
|
|
|
@ -11,8 +11,7 @@ namespace BTCPayServer.Client.Models
|
|||
{
|
||||
public bool Everything { get; set; } = true;
|
||||
|
||||
[JsonProperty(ItemConverterType = typeof(Newtonsoft.Json.Converters.StringEnumConverter))]
|
||||
public WebhookEventType[] SpecificEvents { get; set; } = Array.Empty<WebhookEventType>();
|
||||
public string[] SpecificEvents { get; set; } = Array.Empty<string>();
|
||||
}
|
||||
|
||||
public bool Enabled { get; set; } = true;
|
||||
|
|
|
@ -9,7 +9,7 @@ namespace BTCPayServer.Client.Models
|
|||
{
|
||||
public class WebhookEvent
|
||||
{
|
||||
public readonly static JsonSerializerSettings DefaultSerializerSettings;
|
||||
public static readonly JsonSerializerSettings DefaultSerializerSettings;
|
||||
static WebhookEvent()
|
||||
{
|
||||
DefaultSerializerSettings = new JsonSerializerSettings();
|
||||
|
@ -45,8 +45,7 @@ namespace BTCPayServer.Client.Models
|
|||
}
|
||||
}
|
||||
public bool IsRedelivery { get; set; }
|
||||
[JsonConverter(typeof(StringEnumConverter))]
|
||||
public WebhookEventType Type { get; set; }
|
||||
public string Type { get; set; }
|
||||
[JsonConverter(typeof(NBitcoin.JsonConverters.DateTimeToUnixTimeConverter))]
|
||||
public DateTimeOffset Timestamp { get; set; }
|
||||
[JsonExtensionData]
|
||||
|
|
|
@ -1,13 +1,20 @@
|
|||
namespace BTCPayServer.Client.Models
|
||||
namespace BTCPayServer.Client.Models;
|
||||
|
||||
public static class WebhookEventType
|
||||
{
|
||||
public enum WebhookEventType
|
||||
{
|
||||
InvoiceCreated,
|
||||
InvoiceReceivedPayment,
|
||||
InvoiceProcessing,
|
||||
InvoiceExpired,
|
||||
InvoiceSettled,
|
||||
InvoiceInvalid,
|
||||
InvoicePaymentSettled,
|
||||
}
|
||||
public const string InvoiceCreated = nameof(InvoiceCreated);
|
||||
public const string InvoiceReceivedPayment = nameof(InvoiceReceivedPayment);
|
||||
public const string InvoiceProcessing = nameof(InvoiceProcessing);
|
||||
public const string InvoiceExpired = nameof(InvoiceExpired);
|
||||
public const string InvoiceSettled = nameof(InvoiceSettled);
|
||||
public const string InvoiceInvalid = nameof(InvoiceInvalid);
|
||||
public const string InvoicePaymentSettled = nameof(InvoicePaymentSettled);
|
||||
public const string PayoutCreated = nameof(PayoutCreated);
|
||||
public const string PayoutApproved = nameof(PayoutApproved);
|
||||
public const string PayoutUpdated = nameof(PayoutUpdated);
|
||||
public const string PaymentRequestUpdated = nameof(PaymentRequestUpdated);
|
||||
public const string PaymentRequestCreated = nameof(PaymentRequestCreated);
|
||||
public const string PaymentRequestArchived = nameof(PaymentRequestArchived);
|
||||
public const string PaymentRequestStatusChanged = nameof(PaymentRequestStatusChanged);
|
||||
|
||||
}
|
||||
|
|
|
@ -1,31 +1,64 @@
|
|||
using System;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Converters;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace BTCPayServer.Client.Models
|
||||
{
|
||||
public class WebhookInvoiceEvent : WebhookEvent
|
||||
public class WebhookPayoutEvent : StoreWebhookEvent
|
||||
{
|
||||
public WebhookPayoutEvent(string evtType, string storeId)
|
||||
{
|
||||
if (!evtType.StartsWith("payout", StringComparison.InvariantCultureIgnoreCase))
|
||||
throw new ArgumentException("Invalid event type", nameof(evtType));
|
||||
Type = evtType;
|
||||
StoreId = storeId;
|
||||
}
|
||||
|
||||
[JsonProperty(Order = 2)] public string PayoutId { get; set; }
|
||||
[JsonProperty(Order = 3)] public string PullPaymentId { get; set; }
|
||||
[JsonProperty(Order = 4)] [JsonConverter(typeof(StringEnumConverter))]public PayoutState PayoutState { get; set; }
|
||||
}
|
||||
public class WebhookPaymentRequestEvent : StoreWebhookEvent
|
||||
{
|
||||
public WebhookPaymentRequestEvent(string evtType, string storeId)
|
||||
{
|
||||
if (!evtType.StartsWith("paymentrequest", StringComparison.InvariantCultureIgnoreCase))
|
||||
throw new ArgumentException("Invalid event type", nameof(evtType));
|
||||
Type = evtType;
|
||||
StoreId = storeId;
|
||||
}
|
||||
|
||||
[JsonProperty(Order = 2)] public string PaymentRequestId { get; set; }
|
||||
[JsonProperty(Order = 3)] [JsonConverter(typeof(StringEnumConverter))]public PaymentRequestData.PaymentRequestStatus Status { get; set; }
|
||||
}
|
||||
|
||||
public abstract class StoreWebhookEvent : WebhookEvent
|
||||
{
|
||||
[JsonProperty(Order = 1)] public string StoreId { get; set; }
|
||||
}
|
||||
|
||||
public class WebhookInvoiceEvent : StoreWebhookEvent
|
||||
{
|
||||
public WebhookInvoiceEvent()
|
||||
{
|
||||
}
|
||||
|
||||
public WebhookInvoiceEvent(WebhookEventType evtType)
|
||||
{
|
||||
this.Type = evtType;
|
||||
public WebhookInvoiceEvent(string evtType, string storeId)
|
||||
{
|
||||
if (!evtType.StartsWith("invoice", StringComparison.InvariantCultureIgnoreCase))
|
||||
throw new ArgumentException("Invalid event type", nameof(evtType));
|
||||
Type = evtType;
|
||||
StoreId = storeId;
|
||||
}
|
||||
|
||||
[JsonProperty(Order = 1)] public string StoreId { get; set; }
|
||||
[JsonProperty(Order = 2)] public string InvoiceId { get; set; }
|
||||
[JsonProperty(Order = 3)] public JObject Metadata { get; set; }
|
||||
}
|
||||
|
||||
public class WebhookInvoiceSettledEvent : WebhookInvoiceEvent
|
||||
{
|
||||
public WebhookInvoiceSettledEvent()
|
||||
{
|
||||
}
|
||||
|
||||
public WebhookInvoiceSettledEvent(WebhookEventType evtType) : base(evtType)
|
||||
public WebhookInvoiceSettledEvent(string storeId) : base(WebhookEventType.InvoiceSettled, storeId)
|
||||
{
|
||||
}
|
||||
|
||||
|
@ -34,11 +67,7 @@ namespace BTCPayServer.Client.Models
|
|||
|
||||
public class WebhookInvoiceInvalidEvent : WebhookInvoiceEvent
|
||||
{
|
||||
public WebhookInvoiceInvalidEvent()
|
||||
{
|
||||
}
|
||||
|
||||
public WebhookInvoiceInvalidEvent(WebhookEventType evtType) : base(evtType)
|
||||
public WebhookInvoiceInvalidEvent(string storeId) : base(WebhookEventType.InvoiceInvalid, storeId)
|
||||
{
|
||||
}
|
||||
|
||||
|
@ -47,11 +76,7 @@ namespace BTCPayServer.Client.Models
|
|||
|
||||
public class WebhookInvoiceProcessingEvent : WebhookInvoiceEvent
|
||||
{
|
||||
public WebhookInvoiceProcessingEvent()
|
||||
{
|
||||
}
|
||||
|
||||
public WebhookInvoiceProcessingEvent(WebhookEventType evtType) : base(evtType)
|
||||
public WebhookInvoiceProcessingEvent(string storeId) : base(WebhookEventType.InvoiceProcessing, storeId)
|
||||
{
|
||||
}
|
||||
|
||||
|
@ -60,11 +85,7 @@ namespace BTCPayServer.Client.Models
|
|||
|
||||
public class WebhookInvoiceReceivedPaymentEvent : WebhookInvoiceEvent
|
||||
{
|
||||
public WebhookInvoiceReceivedPaymentEvent()
|
||||
{
|
||||
}
|
||||
|
||||
public WebhookInvoiceReceivedPaymentEvent(WebhookEventType evtType) : base(evtType)
|
||||
public WebhookInvoiceReceivedPaymentEvent(string type, string storeId) : base(type, storeId)
|
||||
{
|
||||
}
|
||||
|
||||
|
@ -76,22 +97,14 @@ namespace BTCPayServer.Client.Models
|
|||
|
||||
public class WebhookInvoicePaymentSettledEvent : WebhookInvoiceReceivedPaymentEvent
|
||||
{
|
||||
public WebhookInvoicePaymentSettledEvent()
|
||||
{
|
||||
}
|
||||
|
||||
public WebhookInvoicePaymentSettledEvent(WebhookEventType evtType) : base(evtType)
|
||||
public WebhookInvoicePaymentSettledEvent(string storeId) : base(WebhookEventType.InvoicePaymentSettled, storeId)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
public class WebhookInvoiceExpiredEvent : WebhookInvoiceEvent
|
||||
{
|
||||
public WebhookInvoiceExpiredEvent()
|
||||
{
|
||||
}
|
||||
|
||||
public WebhookInvoiceExpiredEvent(WebhookEventType evtType) : base(evtType)
|
||||
public WebhookInvoiceExpiredEvent(string storeId) : base(WebhookEventType.InvoiceExpired, storeId)
|
||||
{
|
||||
}
|
||||
|
||||
|
|
|
@ -2099,18 +2099,18 @@ namespace BTCPayServer.Tests
|
|||
//validation errors
|
||||
await AssertValidationError(new[] { nameof(CreateInvoiceRequest.Amount), $"{nameof(CreateInvoiceRequest.Checkout)}.{nameof(CreateInvoiceRequest.Checkout.PaymentTolerance)}", $"{nameof(CreateInvoiceRequest.Checkout)}.{nameof(CreateInvoiceRequest.Checkout.PaymentMethods)}[0]" }, async () =>
|
||||
{
|
||||
await client.CreateInvoice(user.StoreId, new CreateInvoiceRequest() { Amount = -1, Checkout = new CreateInvoiceRequest.CheckoutOptions() { PaymentTolerance = -2, PaymentMethods = new[] { "jasaas_sdsad" } } });
|
||||
await client.CreateInvoice(user.StoreId, new CreateInvoiceRequest { Amount = -1, Checkout = new CreateInvoiceRequest.CheckoutOptions { PaymentTolerance = -2, PaymentMethods = new[] { "jasaas_sdsad" } } });
|
||||
});
|
||||
|
||||
await AssertHttpError(403, async () =>
|
||||
{
|
||||
await viewOnly.CreateInvoice(user.StoreId,
|
||||
new CreateInvoiceRequest() { Currency = "helloinvalid", Amount = 1 });
|
||||
new CreateInvoiceRequest { Currency = "helloinvalid", Amount = 1 });
|
||||
});
|
||||
await user.RegisterDerivationSchemeAsync("BTC");
|
||||
string origOrderId = "testOrder";
|
||||
var newInvoice = await client.CreateInvoice(user.StoreId,
|
||||
new CreateInvoiceRequest()
|
||||
new CreateInvoiceRequest
|
||||
{
|
||||
Currency = "USD",
|
||||
Amount = 1,
|
||||
|
@ -2197,7 +2197,7 @@ namespace BTCPayServer.Tests
|
|||
|
||||
//list NonExisting Status
|
||||
var invoicesNonExistingStatus = await viewOnly.GetInvoices(user.StoreId,
|
||||
status: new[] { BTCPayServer.Client.Models.InvoiceStatus.Invalid });
|
||||
status: new[] { InvoiceStatus.Invalid });
|
||||
Assert.NotNull(invoicesNonExistingStatus);
|
||||
Assert.Empty(invoicesNonExistingStatus);
|
||||
|
||||
|
@ -2215,7 +2215,7 @@ namespace BTCPayServer.Tests
|
|||
|
||||
//update
|
||||
newInvoice = await client.CreateInvoice(user.StoreId,
|
||||
new CreateInvoiceRequest() { Currency = "USD", Amount = 1 });
|
||||
new CreateInvoiceRequest { Currency = "USD", Amount = 1 });
|
||||
Assert.Contains(InvoiceStatus.Settled, newInvoice.AvailableStatusesForManualMarking);
|
||||
Assert.Contains(InvoiceStatus.Invalid, newInvoice.AvailableStatusesForManualMarking);
|
||||
await client.MarkInvoiceStatus(user.StoreId, newInvoice.Id, new MarkInvoiceStatusRequest()
|
||||
|
@ -2227,7 +2227,7 @@ namespace BTCPayServer.Tests
|
|||
Assert.DoesNotContain(InvoiceStatus.Settled, newInvoice.AvailableStatusesForManualMarking);
|
||||
Assert.Contains(InvoiceStatus.Invalid, newInvoice.AvailableStatusesForManualMarking);
|
||||
newInvoice = await client.CreateInvoice(user.StoreId,
|
||||
new CreateInvoiceRequest() { Currency = "USD", Amount = 1 });
|
||||
new CreateInvoiceRequest { Currency = "USD", Amount = 1 });
|
||||
await client.MarkInvoiceStatus(user.StoreId, newInvoice.Id, new MarkInvoiceStatusRequest()
|
||||
{
|
||||
Status = InvoiceStatus.Invalid
|
||||
|
@ -2242,13 +2242,13 @@ namespace BTCPayServer.Tests
|
|||
await AssertHttpError(403, async () =>
|
||||
{
|
||||
await viewOnly.UpdateInvoice(user.StoreId, invoice.Id,
|
||||
new UpdateInvoiceRequest()
|
||||
new UpdateInvoiceRequest
|
||||
{
|
||||
Metadata = metadataForUpdate
|
||||
});
|
||||
});
|
||||
invoice = await client.UpdateInvoice(user.StoreId, invoice.Id,
|
||||
new UpdateInvoiceRequest()
|
||||
new UpdateInvoiceRequest
|
||||
{
|
||||
Metadata = metadataForUpdate
|
||||
});
|
||||
|
@ -2288,13 +2288,12 @@ namespace BTCPayServer.Tests
|
|||
await client.UnarchiveInvoice(user.StoreId, invoice.Id);
|
||||
Assert.NotNull(await client.GetInvoice(user.StoreId, invoice.Id));
|
||||
|
||||
|
||||
foreach (var marked in new[] { InvoiceStatus.Settled, InvoiceStatus.Invalid })
|
||||
{
|
||||
var inv = await client.CreateInvoice(user.StoreId,
|
||||
new CreateInvoiceRequest() { Currency = "USD", Amount = 100 });
|
||||
new CreateInvoiceRequest { Currency = "USD", Amount = 100 });
|
||||
await user.PayInvoice(inv.Id);
|
||||
await client.MarkInvoiceStatus(user.StoreId, inv.Id, new MarkInvoiceStatusRequest()
|
||||
await client.MarkInvoiceStatus(user.StoreId, inv.Id, new MarkInvoiceStatusRequest
|
||||
{
|
||||
Status = marked
|
||||
});
|
||||
|
@ -2322,13 +2321,12 @@ namespace BTCPayServer.Tests
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
newInvoice = await client.CreateInvoice(user.StoreId,
|
||||
new CreateInvoiceRequest()
|
||||
new CreateInvoiceRequest
|
||||
{
|
||||
Currency = "USD",
|
||||
Amount = 1,
|
||||
Checkout = new CreateInvoiceRequest.CheckoutOptions()
|
||||
Checkout = new CreateInvoiceRequest.CheckoutOptions
|
||||
{
|
||||
DefaultLanguage = "it-it ",
|
||||
RedirectURL = "http://toto.com/lol"
|
||||
|
|
|
@ -111,6 +111,7 @@ namespace BTCPayServer.Tests
|
|||
// Payment Request
|
||||
s.Driver.FindElement(By.Id("StoreNav-PaymentRequests")).Click();
|
||||
s.Driver.FindElement(By.Id("CreatePaymentRequest")).Click();
|
||||
Thread.Sleep(10000);
|
||||
s.Driver.FindElement(By.Id("Title")).SendKeys("Pay123");
|
||||
s.Driver.FindElement(By.Id("Amount")).SendKeys("700");
|
||||
new SelectElement(s.Driver.FindElement(By.Id("FormId"))).SelectByValue("Email");
|
||||
|
@ -461,6 +462,12 @@ namespace BTCPayServer.Tests
|
|||
using var s = CreateSeleniumTester();
|
||||
await s.StartAsync();
|
||||
s.RegisterNewUser(true);
|
||||
s.CreateNewStore();
|
||||
|
||||
// Store Emails without server fallback
|
||||
s.GoToStore(StoreNavPages.Emails);
|
||||
s.Driver.FindElement(By.Id("ConfigureEmailRules")).Click();
|
||||
Assert.Contains("You need to configure email settings before this feature works", s.Driver.PageSource);
|
||||
|
||||
// Server Emails
|
||||
s.Driver.Navigate().GoToUrl(s.Link("/server/emails"));
|
||||
|
@ -470,12 +477,11 @@ namespace BTCPayServer.Tests
|
|||
s.FindAlertMessage();
|
||||
}
|
||||
CanSetupEmailCore(s);
|
||||
s.CreateNewStore();
|
||||
|
||||
// Store Emails
|
||||
// Store Emails with server fallback
|
||||
s.GoToStore(StoreNavPages.Emails);
|
||||
s.Driver.FindElement(By.Id("ConfigureEmailRules")).Click();
|
||||
Assert.Contains("You need to configure email settings before this feature works", s.Driver.PageSource);
|
||||
Assert.Contains("Emails will be sent with the email settings of the server", s.Driver.PageSource);
|
||||
|
||||
s.GoToStore(StoreNavPages.Emails);
|
||||
CanSetupEmailCore(s);
|
||||
|
@ -485,10 +491,11 @@ namespace BTCPayServer.Tests
|
|||
Assert.Contains("There are no rules yet.", s.Driver.PageSource);
|
||||
Assert.DoesNotContain("id=\"SaveEmailRules\"", s.Driver.PageSource);
|
||||
Assert.DoesNotContain("You need to configure email settings before this feature works", s.Driver.PageSource);
|
||||
Assert.DoesNotContain("Emails will be sent with the email settings of the server", s.Driver.PageSource);
|
||||
|
||||
s.Driver.FindElement(By.Id("CreateEmailRule")).Click();
|
||||
var select = new SelectElement(s.Driver.FindElement(By.Id("Rules_0__Trigger")));
|
||||
select.SelectByText("InvoiceSettled", true);
|
||||
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!");
|
||||
|
@ -1418,13 +1425,6 @@ namespace BTCPayServer.Tests
|
|||
s.Driver.FindElement(By.Name("update")).Click();
|
||||
s.FindAlertMessage();
|
||||
s.Driver.FindElement(By.LinkText("Modify")).Click();
|
||||
foreach (var value in Enum.GetValues(typeof(WebhookEventType)))
|
||||
{
|
||||
// Here we make sure we did not forget an event type in the list
|
||||
// However, maybe some event should not appear here because not at the store level.
|
||||
// Fix as needed.
|
||||
Assert.Contains($"value=\"{value}\"", s.Driver.PageSource);
|
||||
}
|
||||
|
||||
// This one should be checked
|
||||
Assert.Contains("value=\"InvoiceProcessing\" checked", s.Driver.PageSource);
|
||||
|
@ -3052,13 +3052,18 @@ retry:
|
|||
{
|
||||
s.Driver.FindElement(By.Id("QuickFillDropdownToggle")).Click();
|
||||
s.Driver.FindElement(By.CssSelector("#quick-fill .dropdown-menu .dropdown-item:first-child")).Click();
|
||||
s.Driver.FindElement(By.Id("Settings_Login")).Clear();
|
||||
s.Driver.FindElement(By.Id("Settings_Login")).SendKeys("test@gmail.com");
|
||||
s.Driver.FindElement(By.CssSelector("button[value=\"Save\"]")).Submit();
|
||||
s.FindAlertMessage();
|
||||
s.Driver.FindElement(By.Id("Settings_Password")).Clear();
|
||||
s.Driver.FindElement(By.Id("Settings_Password")).SendKeys("mypassword");
|
||||
|
||||
s.Driver.FindElement(By.Id("Settings_From")).Clear();
|
||||
s.Driver.FindElement(By.Id("Settings_From")).SendKeys("Firstname Lastname <email@example.com>");
|
||||
s.Driver.FindElement(By.Id("Save")).SendKeys(Keys.Enter);
|
||||
Assert.Contains("Configured", s.Driver.PageSource);
|
||||
s.Driver.FindElement(By.Id("Settings_Login")).Clear();
|
||||
s.Driver.FindElement(By.Id("Settings_Login")).SendKeys("test_fix@gmail.com");
|
||||
s.Driver.FindElement(By.Id("Save")).SendKeys(Keys.Enter);
|
||||
Assert.Contains("Configured", s.Driver.PageSource);
|
||||
|
|
|
@ -487,7 +487,7 @@ namespace BTCPayServer.Tests
|
|||
}
|
||||
|
||||
public List<WebhookInvoiceEvent> WebhookEvents { get; set; } = new List<WebhookInvoiceEvent>();
|
||||
public TEvent AssertHasWebhookEvent<TEvent>(WebhookEventType eventType, Action<TEvent> assert) where TEvent : class
|
||||
public TEvent AssertHasWebhookEvent<TEvent>(string eventType, Action<TEvent> assert) where TEvent : class
|
||||
{
|
||||
int retry = 0;
|
||||
retry:
|
||||
|
@ -520,7 +520,7 @@ retry:
|
|||
}
|
||||
public async Task SetupWebhook()
|
||||
{
|
||||
FakeServer server = new FakeServer();
|
||||
var server = new FakeServer();
|
||||
await server.Start();
|
||||
var client = await CreateClient(Policies.CanModifyStoreWebhooks);
|
||||
var wh = await client.CreateWebhook(StoreId, new CreateStoreWebhookRequest()
|
||||
|
@ -536,7 +536,7 @@ retry:
|
|||
{
|
||||
var inv = await BitPay.GetInvoiceAsync(invoiceId);
|
||||
var net = parent.ExplorerNode.Network;
|
||||
this.parent.ExplorerNode.SendToAddress(BitcoinAddress.Create(inv.BitcoinAddress, net), inv.BtcDue);
|
||||
await parent.ExplorerNode.SendToAddressAsync(BitcoinAddress.Create(inv.BitcoinAddress, net), inv.BtcDue);
|
||||
await TestUtils.EventuallyAsync(async () =>
|
||||
{
|
||||
var localInvoice = await BitPay.GetInvoiceAsync(invoiceId, Facade.Merchant);
|
||||
|
|
|
@ -1299,7 +1299,7 @@ namespace BTCPayServer.Tests
|
|||
using var tester = CreateServerTester();
|
||||
await tester.StartAsync();
|
||||
var user = tester.NewAccount();
|
||||
user.GrantAccess();
|
||||
await user.GrantAccessAsync();
|
||||
user.RegisterDerivationScheme("BTC");
|
||||
|
||||
var rng = new Random();
|
||||
|
@ -1333,8 +1333,6 @@ namespace BTCPayServer.Tests
|
|||
var btcmethod = (await client.GetInvoicePaymentMethods(user.StoreId, invoice.Id))[0];
|
||||
var paid = btcSent;
|
||||
var invoiceAddress = BitcoinAddress.Create(btcmethod.Destination, cashCow.Network);
|
||||
|
||||
|
||||
var btc = new PaymentMethodId("BTC", PaymentTypes.BTCLike);
|
||||
var networkFee = (await tester.PayTester.InvoiceRepository.GetInvoice(invoice.Id))
|
||||
.GetPaymentMethods()[btc]
|
||||
|
@ -1346,9 +1344,7 @@ namespace BTCPayServer.Tests
|
|||
networkFee = 0.0m;
|
||||
}
|
||||
|
||||
cashCow.SendToAddress(invoiceAddress, paid);
|
||||
|
||||
|
||||
await cashCow.SendToAddressAsync(invoiceAddress, paid);
|
||||
await TestUtils.EventuallyAsync(async () =>
|
||||
{
|
||||
try
|
||||
|
@ -1952,11 +1948,11 @@ namespace BTCPayServer.Tests
|
|||
using var tester = CreateServerTester();
|
||||
await tester.StartAsync();
|
||||
var user = tester.NewAccount();
|
||||
user.GrantAccess();
|
||||
await user.GrantAccessAsync();
|
||||
user.RegisterDerivationScheme("BTC");
|
||||
await user.SetupWebhook();
|
||||
var invoice = user.BitPay.CreateInvoice(
|
||||
new Invoice()
|
||||
var invoice = await user.BitPay.CreateInvoiceAsync(
|
||||
new Invoice
|
||||
{
|
||||
Price = 5000.0m,
|
||||
TaxIncluded = 1000.0m,
|
||||
|
@ -2003,11 +1999,8 @@ namespace BTCPayServer.Tests
|
|||
Assert.Empty(user.BitPay.GetInvoices(invoice.InvoiceTime.UtcDateTime - TimeSpan.FromDays(5),
|
||||
invoice.InvoiceTime.DateTime - TimeSpan.FromDays(1)));
|
||||
|
||||
|
||||
var firstPayment = Money.Coins(0.04m);
|
||||
|
||||
var txFee = Money.Zero;
|
||||
|
||||
var cashCow = tester.ExplorerNode;
|
||||
|
||||
var invoiceAddress = BitcoinAddress.Create(invoice.BitcoinAddress, cashCow.Network);
|
||||
|
@ -2035,7 +2028,7 @@ namespace BTCPayServer.Tests
|
|||
secondPayment = localInvoice.BtcDue;
|
||||
});
|
||||
|
||||
cashCow.SendToAddress(invoiceAddress, secondPayment);
|
||||
await cashCow.SendToAddressAsync(invoiceAddress, secondPayment);
|
||||
|
||||
TestUtils.Eventually(() =>
|
||||
{
|
||||
|
@ -2049,7 +2042,7 @@ namespace BTCPayServer.Tests
|
|||
Assert.False((bool)((JValue)localInvoice.ExceptionStatus).Value);
|
||||
});
|
||||
|
||||
cashCow.Generate(1); //The user has medium speed settings, so 1 conf is enough to be confirmed
|
||||
await cashCow.GenerateAsync(1); //The user has medium speed settings, so 1 conf is enough to be confirmed
|
||||
|
||||
TestUtils.Eventually(() =>
|
||||
{
|
||||
|
@ -2057,7 +2050,7 @@ namespace BTCPayServer.Tests
|
|||
Assert.Equal("confirmed", localInvoice.Status);
|
||||
});
|
||||
|
||||
cashCow.Generate(5); //Now should be complete
|
||||
await cashCow.GenerateAsync(5); //Now should be complete
|
||||
|
||||
TestUtils.Eventually(() =>
|
||||
{
|
||||
|
@ -2066,7 +2059,7 @@ namespace BTCPayServer.Tests
|
|||
Assert.NotEqual(0.0m, localInvoice.Rate);
|
||||
});
|
||||
|
||||
invoice = user.BitPay.CreateInvoice(new Invoice()
|
||||
invoice = await user.BitPay.CreateInvoiceAsync(new Invoice
|
||||
{
|
||||
Price = 5000.0m,
|
||||
Currency = "USD",
|
||||
|
@ -2079,7 +2072,7 @@ namespace BTCPayServer.Tests
|
|||
}, Facade.Merchant);
|
||||
invoiceAddress = BitcoinAddress.Create(invoice.BitcoinAddress, cashCow.Network);
|
||||
|
||||
var txId = cashCow.SendToAddress(invoiceAddress, invoice.BtcDue + Money.Coins(1));
|
||||
var txId = await cashCow.SendToAddressAsync(invoiceAddress, invoice.BtcDue + Money.Coins(1));
|
||||
|
||||
TestUtils.Eventually(() =>
|
||||
{
|
||||
|
@ -2096,7 +2089,7 @@ namespace BTCPayServer.Tests
|
|||
Assert.Single(textSearchResult);
|
||||
});
|
||||
|
||||
cashCow.Generate(1);
|
||||
await cashCow.GenerateAsync(2);
|
||||
|
||||
TestUtils.Eventually(() =>
|
||||
{
|
||||
|
|
|
@ -146,9 +146,7 @@ namespace BTCPayServer.Controllers.Greenfield
|
|||
return PaymentRequestNotFound();
|
||||
}
|
||||
|
||||
var updatedPr = pr.First();
|
||||
updatedPr.Archived = true;
|
||||
await _paymentRequestRepository.CreateOrUpdatePaymentRequest(updatedPr);
|
||||
await _paymentRequestRepository.ArchivePaymentRequest(pr.First().Id);
|
||||
return Ok();
|
||||
}
|
||||
|
||||
|
|
|
@ -10,6 +10,7 @@ using BTCPayServer.Client;
|
|||
using BTCPayServer.Client.Models;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.HostedServices;
|
||||
using BTCPayServer.HostedServices.Webhooks;
|
||||
using BTCPayServer.Security;
|
||||
using BTCPayServer.Services.Stores;
|
||||
using Google.Apis.Auth.OAuth2;
|
||||
|
|
|
@ -12,6 +12,7 @@ using BTCPayServer.Services;
|
|||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Events;
|
||||
using BTCPayServer.HostedServices;
|
||||
using BTCPayServer.HostedServices.Webhooks;
|
||||
using BTCPayServer.Logging;
|
||||
using BTCPayServer.Payments;
|
||||
using BTCPayServer.Payments.Bitcoin;
|
||||
|
|
|
@ -3,7 +3,6 @@ using System.Linq;
|
|||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Abstractions.Constants;
|
||||
using BTCPayServer.Abstractions.Extensions;
|
||||
using BTCPayServer.Abstractions.Form;
|
||||
using BTCPayServer.Client;
|
||||
using BTCPayServer.Client.Models;
|
||||
|
@ -11,7 +10,6 @@ using BTCPayServer.Data;
|
|||
using BTCPayServer.Filters;
|
||||
using BTCPayServer.Forms;
|
||||
using BTCPayServer.Forms.Models;
|
||||
using BTCPayServer.Models;
|
||||
using BTCPayServer.Models.PaymentRequestViewModels;
|
||||
using BTCPayServer.PaymentRequest;
|
||||
using BTCPayServer.Services;
|
||||
|
@ -22,7 +20,6 @@ using BTCPayServer.Services.Stores;
|
|||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using PaymentRequestData = BTCPayServer.Data.PaymentRequestData;
|
||||
using StoreData = BTCPayServer.Data.StoreData;
|
||||
|
||||
|
@ -115,7 +112,8 @@ namespace BTCPayServer.Controllers
|
|||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
|
||||
var storeBlob = store.GetStoreBlob();
|
||||
var prInvoices = payReqId is null ? null : (await _PaymentRequestService.GetPaymentRequest(payReqId, GetUserId())).Invoices;
|
||||
var vm = new UpdatePaymentRequestViewModel(paymentRequest)
|
||||
{
|
||||
|
@ -123,7 +121,9 @@ namespace BTCPayServer.Controllers
|
|||
AmountAndCurrencyEditable = payReqId is null || !prInvoices.Any()
|
||||
};
|
||||
|
||||
vm.Currency ??= store.GetStoreBlob().DefaultCurrency;
|
||||
vm.Currency ??= storeBlob.DefaultCurrency;
|
||||
vm.HasEmailRules = storeBlob.EmailRules?.Any(rule =>
|
||||
rule.Trigger.Contains("PaymentRequest", StringComparison.InvariantCultureIgnoreCase));
|
||||
|
||||
return View(nameof(EditPaymentRequest), vm);
|
||||
}
|
||||
|
@ -162,10 +162,12 @@ namespace BTCPayServer.Controllers
|
|||
|
||||
if (!ModelState.IsValid)
|
||||
{
|
||||
var storeBlob = store.GetStoreBlob();
|
||||
viewModel.HasEmailRules = storeBlob.EmailRules?.Any(rule =>
|
||||
rule.Trigger.Contains("PaymentRequest", StringComparison.InvariantCultureIgnoreCase));
|
||||
return View(nameof(EditPaymentRequest), viewModel);
|
||||
}
|
||||
|
||||
|
||||
blob.Title = viewModel.Title;
|
||||
blob.Email = viewModel.Email;
|
||||
blob.Description = viewModel.Description;
|
||||
|
@ -413,13 +415,11 @@ namespace BTCPayServer.Controllers
|
|||
public async Task<IActionResult> TogglePaymentRequestArchival(string payReqId)
|
||||
{
|
||||
var store = GetCurrentStore();
|
||||
var result = await EditPaymentRequest(store.Id, payReqId);
|
||||
if (result is ViewResult viewResult)
|
||||
|
||||
var result = await _PaymentRequestRepository.ArchivePaymentRequest(payReqId, true);
|
||||
if(result is not null)
|
||||
{
|
||||
var model = (UpdatePaymentRequestViewModel)viewResult.Model;
|
||||
model.Archived = !model.Archived;
|
||||
await EditPaymentRequest(payReqId, model);
|
||||
TempData[WellKnownTempData.SuccessMessage] = model.Archived
|
||||
TempData[WellKnownTempData.SuccessMessage] = result.Value
|
||||
? "The payment request has been archived and will no longer appear in the payment request list by default again."
|
||||
: "The payment request has been unarchived and will appear in the payment request list by default.";
|
||||
return RedirectToAction("GetPaymentRequests", new { storeId = store.Id });
|
||||
|
|
|
@ -2,6 +2,7 @@ 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;
|
||||
|
@ -19,20 +20,25 @@ namespace BTCPayServer.Controllers
|
|||
public partial class UIStoresController
|
||||
{
|
||||
[HttpGet("{storeId}/emails")]
|
||||
public IActionResult StoreEmails(string storeId)
|
||||
public async Task<IActionResult> StoreEmails(string storeId)
|
||||
{
|
||||
var store = HttpContext.GetStoreData();
|
||||
if (store == null)
|
||||
return NotFound();
|
||||
|
||||
var blob = store.GetStoreBlob();
|
||||
var data = blob.EmailSettings;
|
||||
if (data?.IsComplete() is not true)
|
||||
var storeSetupComplete = blob.EmailSettings?.IsComplete() is true;
|
||||
if (!storeSetupComplete && !TempData.HasStatusMessage())
|
||||
{
|
||||
var emailSender = await _emailSenderFactory.GetEmailSender(store.Id) as StoreEmailSender;
|
||||
var hasServerFallback = await IsSetupComplete(emailSender?.FallbackSender);
|
||||
var message = hasServerFallback
|
||||
? "Emails will be sent with the email settings of the server"
|
||||
: "You need to configure email settings before this feature works";
|
||||
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 now</a>."
|
||||
Severity = hasServerFallback ? StatusMessageModel.StatusSeverity.Info : StatusMessageModel.StatusSeverity.Warning,
|
||||
Html = $"{message}. <a class='alert-link' href='{Url.Action("StoreEmailSettings", new { storeId })}'>Configure store email settings</a>."
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -44,17 +50,17 @@ namespace BTCPayServer.Controllers
|
|||
public async Task<IActionResult> StoreEmails(string storeId, StoreEmailRuleViewModel vm, string command)
|
||||
{
|
||||
vm.Rules ??= new List<StoreEmailRule>();
|
||||
int index = 0;
|
||||
int commandIndex = 0;
|
||||
var indSep = command.IndexOf(":", StringComparison.InvariantCultureIgnoreCase);
|
||||
if (indSep > 0)
|
||||
{
|
||||
var item = command[(indSep + 1)..];
|
||||
index = int.Parse(item, CultureInfo.InvariantCulture);
|
||||
commandIndex = int.Parse(item, CultureInfo.InvariantCulture);
|
||||
}
|
||||
|
||||
if (command.StartsWith("remove", StringComparison.InvariantCultureIgnoreCase))
|
||||
{
|
||||
vm.Rules.RemoveAt(index);
|
||||
vm.Rules.RemoveAt(commandIndex);
|
||||
}
|
||||
else if (command == "add")
|
||||
{
|
||||
|
@ -63,7 +69,7 @@ namespace BTCPayServer.Controllers
|
|||
return View(vm);
|
||||
}
|
||||
|
||||
for (var i = 0; index < vm.Rules.Count; index++)
|
||||
for (var i = 0; i < vm.Rules.Count; i++)
|
||||
{
|
||||
var rule = vm.Rules[i];
|
||||
if (!rule.CustomerEmail && string.IsNullOrEmpty(rule.To))
|
||||
|
@ -79,41 +85,50 @@ namespace BTCPayServer.Controllers
|
|||
|
||||
if (store == null)
|
||||
return NotFound();
|
||||
|
||||
string message = "";
|
||||
|
||||
// update rules
|
||||
var blob = store.GetStoreBlob();
|
||||
blob.EmailRules = vm.Rules;
|
||||
if (store.SetStoreBlob(blob))
|
||||
{
|
||||
await _Repo.UpdateStore(store);
|
||||
message += "Store email rules saved. ";
|
||||
}
|
||||
|
||||
if (command.StartsWith("test", StringComparison.InvariantCultureIgnoreCase))
|
||||
{
|
||||
var rule = vm.Rules[index];
|
||||
try
|
||||
{
|
||||
var emailSettings = blob.EmailSettings;
|
||||
using var client = await emailSettings.CreateSmtpClient();
|
||||
var message = emailSettings.CreateMailMessage(MailboxAddress.Parse(rule.To), "(test) " + rule.Subject, rule.Body, true);
|
||||
await client.SendAsync(message);
|
||||
await client.DisconnectAsync(true);
|
||||
TempData[WellKnownTempData.SuccessMessage] = $"Rule email saved and sent to {rule.To}. Please verify you received it.";
|
||||
|
||||
blob.EmailRules = vm.Rules;
|
||||
store.SetStoreBlob(blob);
|
||||
await _Repo.UpdateStore(store);
|
||||
var rule = vm.Rules[commandIndex];
|
||||
var emailSender = await _emailSenderFactory.GetEmailSender(store.Id);
|
||||
if (await IsSetupComplete(emailSender))
|
||||
{
|
||||
emailSender.SendEmail(MailboxAddress.Parse(rule.To), $"({store.StoreName} test) {rule.Subject}", rule.Body);
|
||||
message += $"Test email sent to {rule.To} — please verify you received it.";
|
||||
}
|
||||
else
|
||||
{
|
||||
message += "Complete the email setup to send test emails.";
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
TempData[WellKnownTempData.ErrorMessage] = "Error: " + ex.Message;
|
||||
TempData[WellKnownTempData.ErrorMessage] = message + "Error sending test email: " + ex.Message;
|
||||
return RedirectToAction("StoreEmails", new { storeId });
|
||||
}
|
||||
}
|
||||
else
|
||||
|
||||
if (!string.IsNullOrEmpty(message))
|
||||
{
|
||||
// UPDATE
|
||||
blob.EmailRules = vm.Rules;
|
||||
store.SetStoreBlob(blob);
|
||||
await _Repo.UpdateStore(store);
|
||||
TempData.SetStatusMessageModel(new StatusMessageModel
|
||||
{
|
||||
Severity = StatusMessageModel.StatusSeverity.Success,
|
||||
Message = "Store email rules saved"
|
||||
Message = message
|
||||
});
|
||||
}
|
||||
|
||||
return RedirectToAction("StoreEmails", new { storeId });
|
||||
}
|
||||
|
||||
|
@ -125,7 +140,7 @@ namespace BTCPayServer.Controllers
|
|||
public class StoreEmailRule
|
||||
{
|
||||
[Required]
|
||||
public WebhookEventType Trigger { get; set; }
|
||||
public string Trigger { get; set; }
|
||||
|
||||
public bool CustomerEmail { get; set; }
|
||||
|
||||
|
@ -209,5 +224,10 @@ namespace BTCPayServer.Controllers
|
|||
return RedirectToAction(nameof(StoreEmailSettings), new { storeId });
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task<bool> IsSetupComplete(IEmailSender emailSender)
|
||||
{
|
||||
return emailSender is not null && (await emailSender.GetEmailSettings())?.IsComplete() == true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -13,6 +13,7 @@ using BTCPayServer.Client;
|
|||
using BTCPayServer.Configuration;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.HostedServices;
|
||||
using BTCPayServer.HostedServices.Webhooks;
|
||||
using BTCPayServer.Models;
|
||||
using BTCPayServer.Models.StoreViewModels;
|
||||
using BTCPayServer.Payments;
|
||||
|
@ -22,6 +23,7 @@ using BTCPayServer.Security.Bitpay;
|
|||
using BTCPayServer.Services;
|
||||
using BTCPayServer.Services.Apps;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
using BTCPayServer.Services.Mails;
|
||||
using BTCPayServer.Services.Rates;
|
||||
using BTCPayServer.Services.Stores;
|
||||
using BTCPayServer.Services.Wallets;
|
||||
|
@ -68,7 +70,8 @@ namespace BTCPayServer.Controllers
|
|||
IOptions<LightningNetworkOptions> lightningNetworkOptions,
|
||||
IOptions<ExternalServicesOptions> externalServiceOptions,
|
||||
IHtmlHelper html,
|
||||
LightningClientFactoryService lightningClientFactoryService)
|
||||
LightningClientFactoryService lightningClientFactoryService,
|
||||
EmailSenderFactory emailSenderFactory)
|
||||
{
|
||||
_RateFactory = rateFactory;
|
||||
_Repo = repo;
|
||||
|
@ -93,6 +96,7 @@ namespace BTCPayServer.Controllers
|
|||
_BTCPayEnv = btcpayEnv;
|
||||
_externalServiceOptions = externalServiceOptions;
|
||||
_lightningClientFactoryService = lightningClientFactoryService;
|
||||
_emailSenderFactory = emailSenderFactory;
|
||||
Html = html;
|
||||
}
|
||||
|
||||
|
@ -116,6 +120,7 @@ namespace BTCPayServer.Controllers
|
|||
private readonly EventAggregator _EventAggregator;
|
||||
private readonly IOptions<ExternalServicesOptions> _externalServiceOptions;
|
||||
private readonly LightningClientFactoryService _lightningClientFactoryService;
|
||||
private readonly EmailSenderFactory _emailSenderFactory;
|
||||
|
||||
public string? GeneratedPairingCode { get; set; }
|
||||
public WebhookSender WebhookNotificationManager { get; }
|
||||
|
|
|
@ -37,6 +37,7 @@ public class BitcoinLikePayoutHandler : IPayoutHandler
|
|||
private readonly ApplicationDbContextFactory _dbContextFactory;
|
||||
private readonly NotificationSender _notificationSender;
|
||||
private readonly Logs Logs;
|
||||
private readonly EventAggregator _eventAggregator;
|
||||
private readonly TransactionLinkProviders _transactionLinkProviders;
|
||||
|
||||
public WalletRepository WalletRepository { get; }
|
||||
|
@ -48,6 +49,7 @@ public class BitcoinLikePayoutHandler : IPayoutHandler
|
|||
ApplicationDbContextFactory dbContextFactory,
|
||||
NotificationSender notificationSender,
|
||||
Logs logs,
|
||||
EventAggregator eventAggregator,
|
||||
TransactionLinkProviders transactionLinkProviders)
|
||||
{
|
||||
_btcPayNetworkProvider = btcPayNetworkProvider;
|
||||
|
@ -57,6 +59,7 @@ public class BitcoinLikePayoutHandler : IPayoutHandler
|
|||
_dbContextFactory = dbContextFactory;
|
||||
_notificationSender = notificationSender;
|
||||
this.Logs = logs;
|
||||
_eventAggregator = eventAggregator;
|
||||
_transactionLinkProviders = transactionLinkProviders;
|
||||
}
|
||||
|
||||
|
@ -329,7 +332,7 @@ public class BitcoinLikePayoutHandler : IPayoutHandler
|
|||
.Include(p => p.PullPaymentData)
|
||||
.Where(p => p.State == PayoutState.InProgress)
|
||||
.ToListAsync();
|
||||
|
||||
List<PayoutData> updatedPayouts = new List<PayoutData>();
|
||||
foreach (var payout in payouts)
|
||||
{
|
||||
var proof = ParseProof(payout) as PayoutTransactionOnChainBlob;
|
||||
|
@ -350,6 +353,7 @@ public class BitcoinLikePayoutHandler : IPayoutHandler
|
|||
{
|
||||
payout.State = PayoutState.Completed;
|
||||
proof.TransactionId = tx.TransactionHash;
|
||||
updatedPayouts.Add(payout);
|
||||
break;
|
||||
}
|
||||
else
|
||||
|
@ -364,6 +368,7 @@ public class BitcoinLikePayoutHandler : IPayoutHandler
|
|||
{
|
||||
payout.State = PayoutState.InProgress;
|
||||
proof.TransactionId = tx.TransactionHash;
|
||||
updatedPayouts.Add(payout);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
@ -376,6 +381,10 @@ public class BitcoinLikePayoutHandler : IPayoutHandler
|
|||
|
||||
if (proof.Candidates.Count == 0)
|
||||
{
|
||||
if (payout.State != PayoutState.AwaitingPayment)
|
||||
{
|
||||
updatedPayouts.Add(payout);
|
||||
}
|
||||
payout.State = PayoutState.AwaitingPayment;
|
||||
}
|
||||
else if (proof.TransactionId is null)
|
||||
|
@ -389,6 +398,10 @@ public class BitcoinLikePayoutHandler : IPayoutHandler
|
|||
}
|
||||
|
||||
await ctx.SaveChangesAsync();
|
||||
foreach (PayoutData payoutData in updatedPayouts)
|
||||
{
|
||||
_eventAggregator.Publish(new PayoutEvent(PayoutEvent.PayoutEventType.Updated,payoutData));
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
|
@ -466,9 +479,8 @@ public class BitcoinLikePayoutHandler : IPayoutHandler
|
|||
|
||||
proof.TransactionId ??= txId;
|
||||
SetProofBlob(payout, proof);
|
||||
|
||||
|
||||
await ctx.SaveChangesAsync();
|
||||
_eventAggregator.Publish(new PayoutEvent(PayoutEvent.PayoutEventType.Updated,payout));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
|
|
|
@ -7,6 +7,7 @@ using BTCPayServer.Abstractions.Constants;
|
|||
using BTCPayServer.Client;
|
||||
using BTCPayServer.Client.Models;
|
||||
using BTCPayServer.Configuration;
|
||||
using BTCPayServer.HostedServices;
|
||||
using BTCPayServer.Lightning;
|
||||
using BTCPayServer.Payments;
|
||||
using BTCPayServer.Payments.Lightning;
|
||||
|
@ -35,6 +36,7 @@ namespace BTCPayServer.Data.Payouts.LightningLike
|
|||
private readonly LightningClientFactoryService _lightningClientFactoryService;
|
||||
private readonly IOptions<LightningNetworkOptions> _options;
|
||||
private readonly IAuthorizationService _authorizationService;
|
||||
private readonly EventAggregator _eventAggregator;
|
||||
private readonly StoreRepository _storeRepository;
|
||||
|
||||
public UILightningLikePayoutController(ApplicationDbContextFactory applicationDbContextFactory,
|
||||
|
@ -44,7 +46,9 @@ namespace BTCPayServer.Data.Payouts.LightningLike
|
|||
BTCPayNetworkProvider btcPayNetworkProvider,
|
||||
StoreRepository storeRepository,
|
||||
LightningClientFactoryService lightningClientFactoryService,
|
||||
IOptions<LightningNetworkOptions> options, IAuthorizationService authorizationService)
|
||||
IOptions<LightningNetworkOptions> options,
|
||||
IAuthorizationService authorizationService,
|
||||
EventAggregator eventAggregator)
|
||||
{
|
||||
_applicationDbContextFactory = applicationDbContextFactory;
|
||||
_userManager = userManager;
|
||||
|
@ -55,6 +59,7 @@ namespace BTCPayServer.Data.Payouts.LightningLike
|
|||
_options = options;
|
||||
_storeRepository = storeRepository;
|
||||
_authorizationService = authorizationService;
|
||||
_eventAggregator = eventAggregator;
|
||||
}
|
||||
|
||||
private async Task<List<PayoutData>> GetPayouts(ApplicationDbContext dbContext, PaymentMethodId pmi,
|
||||
|
@ -214,6 +219,16 @@ namespace BTCPayServer.Data.Payouts.LightningLike
|
|||
}
|
||||
|
||||
await ctx.SaveChangesAsync();
|
||||
foreach (var payoutG in payouts)
|
||||
{
|
||||
foreach (PayoutData payout in payoutG)
|
||||
{
|
||||
if (payout.State != PayoutState.AwaitingPayment)
|
||||
{
|
||||
_eventAggregator.Publish(new PayoutEvent(null, payout));
|
||||
}
|
||||
}
|
||||
}
|
||||
return View("LightningPayoutResult", results);
|
||||
}
|
||||
public static async Task<(BOLT11PaymentRequest, ResultVM)> GetInvoiceFromLNURL(PayoutData payoutData,
|
||||
|
|
|
@ -5,10 +5,10 @@ using System.Linq;
|
|||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Client.Models;
|
||||
using BTCPayServer.HostedServices.Webhooks;
|
||||
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using SshNet.Security.Cryptography;
|
||||
|
||||
namespace BTCPayServer.Data
|
||||
{
|
||||
|
@ -16,9 +16,8 @@ namespace BTCPayServer.Data
|
|||
{
|
||||
public bool Everything { get; set; }
|
||||
|
||||
[JsonProperty(ItemConverterType = typeof(Newtonsoft.Json.Converters.StringEnumConverter))]
|
||||
public WebhookEventType[] SpecificEvents { get; set; } = Array.Empty<WebhookEventType>();
|
||||
public bool Match(WebhookEventType evt)
|
||||
public string[] SpecificEvents { get; set; } = Array.Empty<string>();
|
||||
public bool Match(string evt)
|
||||
{
|
||||
return Everything || SpecificEvents.Contains(evt);
|
||||
}
|
||||
|
@ -47,7 +46,7 @@ namespace BTCPayServer.Data
|
|||
}
|
||||
public T ReadRequestAs<T>()
|
||||
{
|
||||
return JsonConvert.DeserializeObject<T>(UTF8Encoding.UTF8.GetString(Request), HostedServices.WebhookSender.DefaultSerializerSettings);
|
||||
return JsonConvert.DeserializeObject<T>(UTF8Encoding.UTF8.GetString(Request), WebhookSender.DefaultSerializerSettings);
|
||||
}
|
||||
|
||||
public bool IsPruned()
|
||||
|
@ -78,14 +77,14 @@ namespace BTCPayServer.Data
|
|||
if (webhook.Blob is null)
|
||||
return null;
|
||||
else
|
||||
return JsonConvert.DeserializeObject<WebhookDeliveryBlob>(webhook.Blob, HostedServices.WebhookSender.DefaultSerializerSettings);
|
||||
return JsonConvert.DeserializeObject<WebhookDeliveryBlob>(webhook.Blob, WebhookSender.DefaultSerializerSettings);
|
||||
}
|
||||
public static void SetBlob(this WebhookDeliveryData webhook, WebhookDeliveryBlob blob)
|
||||
{
|
||||
if (blob is null)
|
||||
webhook.Blob = null;
|
||||
else
|
||||
webhook.Blob = JsonConvert.SerializeObject(blob, HostedServices.WebhookSender.DefaultSerializerSettings);
|
||||
webhook.Blob = JsonConvert.SerializeObject(blob, WebhookSender.DefaultSerializerSettings);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -22,17 +22,8 @@ namespace BTCPayServer.HostedServices
|
|||
{
|
||||
private const string TYPE = "pluginupdate";
|
||||
|
||||
internal class Handler : NotificationHandler<PluginUpdateNotification>
|
||||
internal class Handler(LinkGenerator linkGenerator, BTCPayServerOptions options) : NotificationHandler<PluginUpdateNotification>
|
||||
{
|
||||
private readonly LinkGenerator _linkGenerator;
|
||||
private readonly BTCPayServerOptions _options;
|
||||
|
||||
public Handler(LinkGenerator linkGenerator, BTCPayServerOptions options)
|
||||
{
|
||||
_linkGenerator = linkGenerator;
|
||||
_options = options;
|
||||
}
|
||||
|
||||
public override string NotificationType => TYPE;
|
||||
|
||||
public override (string identifier, string name)[] Meta
|
||||
|
@ -48,9 +39,9 @@ namespace BTCPayServer.HostedServices
|
|||
vm.Identifier = notification.Identifier;
|
||||
vm.Type = notification.NotificationType;
|
||||
vm.Body = $"New {notification.Name} plugin version {notification.Version} released!";
|
||||
vm.ActionLink = _linkGenerator.GetPathByAction(nameof(UIServerController.ListPlugins),
|
||||
vm.ActionLink = linkGenerator.GetPathByAction(nameof(UIServerController.ListPlugins),
|
||||
"UIServer",
|
||||
new {plugin = notification.PluginIdentifier}, _options.RootPath);
|
||||
new {plugin = notification.PluginIdentifier}, options.RootPath);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -79,34 +70,19 @@ namespace BTCPayServer.HostedServices
|
|||
public Dictionary<string, Version> LastVersions { get; set; }
|
||||
}
|
||||
|
||||
public class PluginUpdateFetcher : IPeriodicTask
|
||||
public class PluginUpdateFetcher(SettingsRepository settingsRepository, NotificationSender notificationSender, PluginService pluginService)
|
||||
: IPeriodicTask
|
||||
{
|
||||
public PluginUpdateFetcher(
|
||||
SettingsRepository settingsRepository,
|
||||
ILogger<PluginUpdateFetcher> logger, NotificationSender notificationSender, PluginService pluginService)
|
||||
{
|
||||
_settingsRepository = settingsRepository;
|
||||
_logger = logger;
|
||||
_notificationSender = notificationSender;
|
||||
_pluginService = pluginService;
|
||||
}
|
||||
|
||||
private readonly SettingsRepository _settingsRepository;
|
||||
private readonly ILogger<PluginUpdateFetcher> _logger;
|
||||
private readonly NotificationSender _notificationSender;
|
||||
private readonly PluginService _pluginService;
|
||||
|
||||
|
||||
public async Task Do(CancellationToken cancellationToken)
|
||||
{
|
||||
var dh = await _settingsRepository.GetSettingAsync<PluginVersionCheckerDataHolder>() ??
|
||||
var dh = await settingsRepository.GetSettingAsync<PluginVersionCheckerDataHolder>() ??
|
||||
new PluginVersionCheckerDataHolder();
|
||||
dh.LastVersions ??= new Dictionary<string, Version>();
|
||||
var disabledPlugins = _pluginService.GetDisabledPlugins();
|
||||
var disabledPlugins = pluginService.GetDisabledPlugins();
|
||||
|
||||
var installedPlugins =
|
||||
_pluginService.LoadedPlugins.ToDictionary(plugin => plugin.Identifier, plugin => plugin.Version);
|
||||
var remotePlugins = await _pluginService.GetRemotePlugins();
|
||||
pluginService.LoadedPlugins.ToDictionary(plugin => plugin.Identifier, plugin => plugin.Version);
|
||||
var remotePlugins = await pluginService.GetRemotePlugins();
|
||||
var remotePluginsList = remotePlugins
|
||||
.Where(pair => installedPlugins.ContainsKey(pair.Identifier) || disabledPlugins.Contains(pair.Name))
|
||||
.ToDictionary(plugin => plugin.Identifier, plugin => plugin.Version);
|
||||
|
@ -128,10 +104,10 @@ namespace BTCPayServer.HostedServices
|
|||
foreach (string pluginUpdate in notify)
|
||||
{
|
||||
var plugin = remotePlugins.First(p => p.Identifier == pluginUpdate);
|
||||
await _notificationSender.SendNotification(new AdminScope(), new PluginUpdateNotification(plugin));
|
||||
await notificationSender.SendNotification(new AdminScope(), new PluginUpdateNotification(plugin));
|
||||
}
|
||||
|
||||
await _settingsRepository.UpdateSetting(dh);
|
||||
await settingsRepository.UpdateSetting(dh);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -493,6 +493,7 @@ namespace BTCPayServer.HostedServices
|
|||
}
|
||||
payout.State = req.Request.State;
|
||||
await ctx.SaveChangesAsync();
|
||||
_eventAggregator.Publish(new PayoutEvent(null, payout));
|
||||
req.Completion.SetResult(MarkPayoutRequest.PayoutPaidResult.Ok);
|
||||
}
|
||||
catch (Exception ex)
|
||||
|
@ -711,6 +712,11 @@ namespace BTCPayServer.HostedServices
|
|||
}
|
||||
|
||||
await ctx.SaveChangesAsync();
|
||||
foreach (var keyValuePair in result.Where(pair => pair.Value == MarkPayoutRequest.PayoutPaidResult.Ok))
|
||||
{
|
||||
var payout = payouts.First(p => p.Id == keyValuePair.Key);
|
||||
_eventAggregator.Publish(new PayoutEvent(null, payout));
|
||||
}
|
||||
cancel.Completion.TrySetResult(result);
|
||||
}
|
||||
catch (Exception ex)
|
||||
|
@ -929,13 +935,13 @@ namespace BTCPayServer.HostedServices
|
|||
public JObject Metadata { get; set; }
|
||||
}
|
||||
|
||||
public record PayoutEvent(PayoutEvent.PayoutEventType Type,PayoutData Payout)
|
||||
public record PayoutEvent(PayoutEvent.PayoutEventType? Type, PayoutData Payout)
|
||||
{
|
||||
public enum PayoutEventType
|
||||
{
|
||||
Created,
|
||||
Approved
|
||||
Approved,
|
||||
Updated
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,20 +1,14 @@
|
|||
using System;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Client.Models;
|
||||
using BTCPayServer.Controllers;
|
||||
using BTCPayServer.Controllers.Greenfield;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Events;
|
||||
using BTCPayServer.Services;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
using BTCPayServer.HostedServices.Webhooks;
|
||||
using BTCPayServer.Services.Mails;
|
||||
using BTCPayServer.Services.Rates;
|
||||
using BTCPayServer.Services.Stores;
|
||||
using Microsoft.AspNetCore.Routing;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using InvoiceData = BTCPayServer.Client.Models.InvoiceData;
|
||||
|
||||
namespace BTCPayServer.HostedServices;
|
||||
|
||||
|
@ -22,82 +16,75 @@ public class StoreEmailRuleProcessorSender : EventHostedServiceBase
|
|||
{
|
||||
private readonly StoreRepository _storeRepository;
|
||||
private readonly EmailSenderFactory _emailSenderFactory;
|
||||
private readonly LinkGenerator _linkGenerator;
|
||||
private readonly CurrencyNameTable _currencyNameTable;
|
||||
|
||||
public StoreEmailRuleProcessorSender(StoreRepository storeRepository, EventAggregator eventAggregator,
|
||||
ILogger<InvoiceEventSaverService> logger,
|
||||
EmailSenderFactory emailSenderFactory,
|
||||
LinkGenerator linkGenerator,
|
||||
CurrencyNameTable currencyNameTable) : base(
|
||||
EmailSenderFactory emailSenderFactory) : base(
|
||||
eventAggregator, logger)
|
||||
{
|
||||
_storeRepository = storeRepository;
|
||||
_emailSenderFactory = emailSenderFactory;
|
||||
_linkGenerator = linkGenerator;
|
||||
_currencyNameTable = currencyNameTable;
|
||||
}
|
||||
|
||||
protected override void SubscribeToEvents()
|
||||
{
|
||||
Subscribe<InvoiceEvent>();
|
||||
Subscribe<WebhookSender.WebhookDeliveryRequest>();
|
||||
}
|
||||
|
||||
protected override async Task ProcessEvent(object evt, CancellationToken cancellationToken)
|
||||
{
|
||||
if (evt is InvoiceEvent invoiceEvent)
|
||||
if (evt is WebhookSender.WebhookDeliveryRequest webhookDeliveryRequest)
|
||||
{
|
||||
var type = WebhookSender.GetWebhookEvent(invoiceEvent);
|
||||
var type = webhookDeliveryRequest.WebhookEvent.Type;
|
||||
if (type is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var store = await _storeRepository.FindStore(invoiceEvent.Invoice.StoreId);
|
||||
|
||||
if (webhookDeliveryRequest.WebhookEvent is not StoreWebhookEvent storeWebhookEvent || storeWebhookEvent.StoreId is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
var store = await _storeRepository.FindStore(storeWebhookEvent.StoreId);
|
||||
if (store is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var blob = store.GetStoreBlob();
|
||||
if (blob.EmailRules?.Any() is true)
|
||||
{
|
||||
var actionableRules = blob.EmailRules.Where(rule => rule.Trigger == type.Type).ToList();
|
||||
var actionableRules = blob.EmailRules.Where(rule => rule.Trigger == type).ToList();
|
||||
if (actionableRules.Any())
|
||||
{
|
||||
var sender = await _emailSenderFactory.GetEmailSender(invoiceEvent.Invoice.StoreId);
|
||||
var sender = await _emailSenderFactory.GetEmailSender(storeWebhookEvent.StoreId);
|
||||
foreach (UIStoresController.StoreEmailRule actionableRule in actionableRules)
|
||||
{
|
||||
var recipients = (actionableRule.To?.Split(",", StringSplitOptions.RemoveEmptyEntries) ?? Array.Empty<string>())
|
||||
|
||||
|
||||
var request = new SendEmailRequest()
|
||||
{
|
||||
Subject = actionableRule.Subject, Body = actionableRule.Body, Email = actionableRule.To
|
||||
};
|
||||
request = await webhookDeliveryRequest.Interpolate(request, actionableRule);
|
||||
|
||||
|
||||
var recipients = (request?.Email?.Split(",", StringSplitOptions.RemoveEmptyEntries) ?? Array.Empty<string>())
|
||||
.Select(o =>
|
||||
{
|
||||
MailboxAddressValidator.TryParse(o, out var mb);
|
||||
return mb;
|
||||
})
|
||||
.Where(o => o != null)
|
||||
.ToList();
|
||||
if (actionableRule.CustomerEmail &&
|
||||
MailboxAddressValidator.TryParse(invoiceEvent.Invoice.Metadata.BuyerEmail, out var bmb))
|
||||
{
|
||||
recipients.Add(bmb);
|
||||
}
|
||||
var i = GreenfieldInvoiceController.ToModel(invoiceEvent.Invoice, _linkGenerator, null);
|
||||
sender.SendEmail(recipients.ToArray(), null, null, Interpolator(actionableRule.Subject, i),
|
||||
Interpolator(actionableRule.Body, i));
|
||||
.ToArray();
|
||||
|
||||
if(recipients.Length == 0)
|
||||
continue;
|
||||
|
||||
sender.SendEmail(recipients.ToArray(), null, null, request.Subject, request.Body);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private string Interpolator(string str, InvoiceData i)
|
||||
{
|
||||
//TODO: we should switch to https://dotnetfiddle.net/MoqJFk later
|
||||
return str.Replace("{Invoice.Id}", i.Id)
|
||||
.Replace("{Invoice.StoreId}", i.StoreId)
|
||||
.Replace("{Invoice.Price}",
|
||||
decimal.Round(i.Amount, _currencyNameTable.GetCurrencyData(i.Currency, true).Divisibility,
|
||||
MidpointRounding.ToEven).ToString(CultureInfo.InvariantCulture))
|
||||
.Replace("{Invoice.Currency}", i.Currency)
|
||||
.Replace("{Invoice.Status}", i.Status.ToString())
|
||||
.Replace("{Invoice.AdditionalStatus}", i.AdditionalStatus.ToString())
|
||||
.Replace("{Invoice.OrderId}", i.Metadata.ToObject<InvoiceMetadata>().OrderId);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,390 +0,0 @@
|
|||
#nullable enable
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Net.Http.Headers;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Channels;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Client.Models;
|
||||
using BTCPayServer.Controllers.Greenfield;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Events;
|
||||
using BTCPayServer.Logging;
|
||||
using BTCPayServer.Services;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
using BTCPayServer.Services.Stores;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using NBitcoin;
|
||||
using NBitcoin.DataEncoders;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace BTCPayServer.HostedServices
|
||||
{
|
||||
/// <summary>
|
||||
/// This class send webhook notifications
|
||||
/// It also make sure the events sent to a webhook are sent in order to the webhook
|
||||
/// </summary>
|
||||
public class WebhookSender : EventHostedServiceBase
|
||||
{
|
||||
readonly Encoding UTF8 = new UTF8Encoding(false);
|
||||
public readonly static JsonSerializerSettings DefaultSerializerSettings;
|
||||
|
||||
static WebhookSender()
|
||||
{
|
||||
DefaultSerializerSettings = WebhookEvent.DefaultSerializerSettings;
|
||||
}
|
||||
public const string OnionNamedClient = "greenfield-webhook.onion";
|
||||
public const string ClearnetNamedClient = "greenfield-webhook.clearnet";
|
||||
public const string LoopbackNamedClient = "greenfield-webhook.loopback";
|
||||
public static string[] AllClients = new[] { OnionNamedClient, ClearnetNamedClient, LoopbackNamedClient };
|
||||
private HttpClient GetClient(Uri uri)
|
||||
{
|
||||
return HttpClientFactory.CreateClient(uri.IsOnion() ? OnionNamedClient : uri.IsLoopback ? LoopbackNamedClient : ClearnetNamedClient);
|
||||
}
|
||||
class WebhookDeliveryRequest
|
||||
{
|
||||
public WebhookEvent WebhookEvent;
|
||||
public Data.WebhookDeliveryData Delivery;
|
||||
public WebhookBlob WebhookBlob;
|
||||
public string WebhookId;
|
||||
public WebhookDeliveryRequest(string webhookId, WebhookEvent webhookEvent, Data.WebhookDeliveryData delivery, WebhookBlob webhookBlob)
|
||||
{
|
||||
WebhookId = webhookId;
|
||||
WebhookEvent = webhookEvent;
|
||||
Delivery = delivery;
|
||||
WebhookBlob = webhookBlob;
|
||||
}
|
||||
}
|
||||
|
||||
MultiProcessingQueue _processingQueue = new MultiProcessingQueue();
|
||||
public StoreRepository StoreRepository { get; }
|
||||
public IHttpClientFactory HttpClientFactory { get; }
|
||||
|
||||
public WebhookSender(EventAggregator eventAggregator,
|
||||
StoreRepository storeRepository,
|
||||
IHttpClientFactory httpClientFactory,
|
||||
Logs logs) : base(eventAggregator, logs)
|
||||
{
|
||||
StoreRepository = storeRepository;
|
||||
HttpClientFactory = httpClientFactory;
|
||||
}
|
||||
|
||||
protected override void SubscribeToEvents()
|
||||
{
|
||||
Subscribe<InvoiceEvent>();
|
||||
}
|
||||
|
||||
public async Task<string?> Redeliver(string deliveryId)
|
||||
{
|
||||
var deliveryRequest = await CreateRedeliveryRequest(deliveryId);
|
||||
if (deliveryRequest is null)
|
||||
return null;
|
||||
EnqueueDelivery(deliveryRequest);
|
||||
return deliveryRequest.Delivery.Id;
|
||||
}
|
||||
|
||||
private async Task<WebhookDeliveryRequest?> CreateRedeliveryRequest(string deliveryId)
|
||||
{
|
||||
using var ctx = StoreRepository.CreateDbContext();
|
||||
var webhookDelivery = await ctx.WebhookDeliveries.AsNoTracking()
|
||||
.Where(o => o.Id == deliveryId)
|
||||
.Select(o => new
|
||||
{
|
||||
Webhook = o.Webhook,
|
||||
Delivery = o
|
||||
})
|
||||
.FirstOrDefaultAsync();
|
||||
if (webhookDelivery is null)
|
||||
return null;
|
||||
var oldDeliveryBlob = webhookDelivery.Delivery.GetBlob();
|
||||
var newDelivery = NewDelivery(webhookDelivery.Webhook.Id);
|
||||
var newDeliveryBlob = new WebhookDeliveryBlob();
|
||||
newDeliveryBlob.Request = oldDeliveryBlob.Request;
|
||||
var webhookEvent = newDeliveryBlob.ReadRequestAs<WebhookEvent>();
|
||||
if (webhookEvent.IsPruned())
|
||||
return null;
|
||||
webhookEvent.DeliveryId = newDelivery.Id;
|
||||
webhookEvent.WebhookId = webhookDelivery.Webhook.Id;
|
||||
// if we redelivered a redelivery, we still want the initial delivery here
|
||||
webhookEvent.OriginalDeliveryId ??= deliveryId;
|
||||
webhookEvent.IsRedelivery = true;
|
||||
newDeliveryBlob.Request = ToBytes(webhookEvent);
|
||||
newDelivery.SetBlob(newDeliveryBlob);
|
||||
return new WebhookDeliveryRequest(webhookDelivery.Webhook.Id, webhookEvent, newDelivery, webhookDelivery.Webhook.GetBlob());
|
||||
}
|
||||
|
||||
private WebhookEvent GetTestWebHook(string storeId, string webhookId, WebhookEventType webhookEventType, Data.WebhookDeliveryData delivery)
|
||||
{
|
||||
var webhookEvent = GetWebhookEvent(webhookEventType);
|
||||
webhookEvent.InvoiceId = "__test__" + Guid.NewGuid().ToString() + "__test__";
|
||||
webhookEvent.StoreId = storeId;
|
||||
webhookEvent.DeliveryId = delivery.Id;
|
||||
webhookEvent.WebhookId = webhookId;
|
||||
webhookEvent.OriginalDeliveryId = "__test__" + Guid.NewGuid().ToString() + "__test__";
|
||||
webhookEvent.IsRedelivery = false;
|
||||
webhookEvent.Timestamp = delivery.Timestamp;
|
||||
|
||||
return webhookEvent;
|
||||
}
|
||||
|
||||
public async Task<DeliveryResult> TestWebhook(string storeId, string webhookId, WebhookEventType webhookEventType, CancellationToken cancellationToken)
|
||||
{
|
||||
var delivery = NewDelivery(webhookId);
|
||||
var webhook = (await StoreRepository.GetWebhooks(storeId)).FirstOrDefault(w => w.Id == webhookId);
|
||||
var deliveryRequest = new WebhookDeliveryRequest(
|
||||
webhookId,
|
||||
GetTestWebHook(storeId, webhookId, webhookEventType, delivery),
|
||||
delivery,
|
||||
webhook.GetBlob()
|
||||
);
|
||||
return await SendDelivery(deliveryRequest, cancellationToken);
|
||||
}
|
||||
|
||||
protected override async Task ProcessEvent(object evt, CancellationToken cancellationToken)
|
||||
{
|
||||
if (evt is InvoiceEvent invoiceEvent)
|
||||
{
|
||||
var webhooks = await StoreRepository.GetWebhooks(invoiceEvent.Invoice.StoreId);
|
||||
foreach (var webhook in webhooks)
|
||||
{
|
||||
var webhookBlob = webhook.GetBlob();
|
||||
if (!(GetWebhookEvent(invoiceEvent) is WebhookInvoiceEvent webhookEvent))
|
||||
continue;
|
||||
if (!ShouldDeliver(webhookEvent.Type, webhookBlob))
|
||||
continue;
|
||||
Data.WebhookDeliveryData delivery = NewDelivery(webhook.Id);
|
||||
webhookEvent.InvoiceId = invoiceEvent.InvoiceId;
|
||||
webhookEvent.StoreId = invoiceEvent.Invoice.StoreId;
|
||||
webhookEvent.DeliveryId = delivery.Id;
|
||||
webhookEvent.WebhookId = webhook.Id;
|
||||
webhookEvent.OriginalDeliveryId = delivery.Id;
|
||||
webhookEvent.Metadata = invoiceEvent.Invoice.Metadata.ToJObject();
|
||||
webhookEvent.IsRedelivery = false;
|
||||
webhookEvent.Timestamp = delivery.Timestamp;
|
||||
var context = new WebhookDeliveryRequest(webhook.Id, webhookEvent, delivery, webhookBlob);
|
||||
EnqueueDelivery(context);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void EnqueueDelivery(WebhookDeliveryRequest context)
|
||||
{
|
||||
_processingQueue.Enqueue(context.WebhookId, (cancellationToken) => Process(context, cancellationToken));
|
||||
}
|
||||
|
||||
public static WebhookInvoiceEvent GetWebhookEvent(WebhookEventType webhookEventType)
|
||||
{
|
||||
switch (webhookEventType)
|
||||
{
|
||||
case WebhookEventType.InvoiceCreated:
|
||||
return new WebhookInvoiceEvent(WebhookEventType.InvoiceCreated);
|
||||
case WebhookEventType.InvoiceReceivedPayment:
|
||||
return new WebhookInvoiceReceivedPaymentEvent(WebhookEventType.InvoiceReceivedPayment);
|
||||
case WebhookEventType.InvoicePaymentSettled:
|
||||
return new WebhookInvoicePaymentSettledEvent(WebhookEventType.InvoicePaymentSettled);
|
||||
case WebhookEventType.InvoiceProcessing:
|
||||
return new WebhookInvoiceProcessingEvent(WebhookEventType.InvoiceProcessing);
|
||||
case WebhookEventType.InvoiceExpired:
|
||||
return new WebhookInvoiceExpiredEvent(WebhookEventType.InvoiceExpired);
|
||||
case WebhookEventType.InvoiceSettled:
|
||||
return new WebhookInvoiceSettledEvent(WebhookEventType.InvoiceSettled);
|
||||
case WebhookEventType.InvoiceInvalid:
|
||||
return new WebhookInvoiceInvalidEvent(WebhookEventType.InvoiceInvalid);
|
||||
default:
|
||||
return new WebhookInvoiceEvent(WebhookEventType.InvoiceCreated);
|
||||
}
|
||||
}
|
||||
|
||||
public static WebhookInvoiceEvent? GetWebhookEvent(InvoiceEvent invoiceEvent)
|
||||
{
|
||||
var eventCode = invoiceEvent.EventCode;
|
||||
switch (eventCode)
|
||||
{
|
||||
case InvoiceEventCode.Completed:
|
||||
case InvoiceEventCode.PaidAfterExpiration:
|
||||
return null;
|
||||
case InvoiceEventCode.Confirmed:
|
||||
case InvoiceEventCode.MarkedCompleted:
|
||||
return new WebhookInvoiceSettledEvent(WebhookEventType.InvoiceSettled)
|
||||
{
|
||||
ManuallyMarked = eventCode == InvoiceEventCode.MarkedCompleted
|
||||
};
|
||||
case InvoiceEventCode.Created:
|
||||
return new WebhookInvoiceEvent(WebhookEventType.InvoiceCreated);
|
||||
case InvoiceEventCode.Expired:
|
||||
return new WebhookInvoiceExpiredEvent(WebhookEventType.InvoiceExpired)
|
||||
{
|
||||
PartiallyPaid = invoiceEvent.PaidPartial
|
||||
};
|
||||
case InvoiceEventCode.FailedToConfirm:
|
||||
case InvoiceEventCode.MarkedInvalid:
|
||||
return new WebhookInvoiceInvalidEvent(WebhookEventType.InvoiceInvalid)
|
||||
{
|
||||
ManuallyMarked = eventCode == InvoiceEventCode.MarkedInvalid
|
||||
};
|
||||
case InvoiceEventCode.PaidInFull:
|
||||
return new WebhookInvoiceProcessingEvent(WebhookEventType.InvoiceProcessing)
|
||||
{
|
||||
OverPaid = invoiceEvent.Invoice.ExceptionStatus == InvoiceExceptionStatus.PaidOver,
|
||||
};
|
||||
case InvoiceEventCode.ReceivedPayment:
|
||||
return new WebhookInvoiceReceivedPaymentEvent(WebhookEventType.InvoiceReceivedPayment)
|
||||
{
|
||||
AfterExpiration = invoiceEvent.Invoice.Status.ToModernStatus() == InvoiceStatus.Expired || invoiceEvent.Invoice.Status.ToModernStatus() == InvoiceStatus.Invalid,
|
||||
PaymentMethod = invoiceEvent.Payment.GetPaymentMethodId().ToStringNormalized(),
|
||||
Payment = GreenfieldInvoiceController.ToPaymentModel(invoiceEvent.Invoice, invoiceEvent.Payment)
|
||||
};
|
||||
case InvoiceEventCode.PaymentSettled:
|
||||
return new WebhookInvoiceReceivedPaymentEvent(WebhookEventType.InvoicePaymentSettled)
|
||||
{
|
||||
AfterExpiration = invoiceEvent.Invoice.Status.ToModernStatus() == InvoiceStatus.Expired || invoiceEvent.Invoice.Status.ToModernStatus() == InvoiceStatus.Invalid,
|
||||
PaymentMethod = invoiceEvent.Payment.GetPaymentMethodId().ToStringNormalized(),
|
||||
Payment = GreenfieldInvoiceController.ToPaymentModel(invoiceEvent.Invoice, invoiceEvent.Payment),
|
||||
OverPaid = invoiceEvent.Invoice.ExceptionStatus == InvoiceExceptionStatus.PaidOver,
|
||||
};
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task Process(WebhookDeliveryRequest ctx, CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
var wh = (await StoreRepository.GetWebhook(ctx.WebhookId))?.GetBlob();
|
||||
if (wh is null || !ShouldDeliver(ctx.WebhookEvent.Type, wh))
|
||||
return;
|
||||
var result = await SendAndSaveDelivery(ctx, cancellationToken);
|
||||
if (ctx.WebhookBlob.AutomaticRedelivery &&
|
||||
!result.Success &&
|
||||
result.DeliveryId is not null)
|
||||
{
|
||||
var originalDeliveryId = result.DeliveryId;
|
||||
foreach (var wait in new[]
|
||||
{
|
||||
TimeSpan.FromSeconds(10),
|
||||
TimeSpan.FromMinutes(1),
|
||||
TimeSpan.FromMinutes(10),
|
||||
TimeSpan.FromMinutes(10),
|
||||
TimeSpan.FromMinutes(10),
|
||||
TimeSpan.FromMinutes(10),
|
||||
TimeSpan.FromMinutes(10),
|
||||
TimeSpan.FromMinutes(10),
|
||||
})
|
||||
{
|
||||
await Task.Delay(wait, cancellationToken);
|
||||
ctx = (await CreateRedeliveryRequest(originalDeliveryId))!;
|
||||
// This may have changed
|
||||
if (ctx is null || !ctx.WebhookBlob.AutomaticRedelivery ||
|
||||
!ShouldDeliver(ctx.WebhookEvent.Type, ctx.WebhookBlob))
|
||||
return;
|
||||
result = await SendAndSaveDelivery(ctx, cancellationToken);
|
||||
if (result.Success)
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch when (cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logs.PayServer.LogError(ex, "Unexpected error when processing a webhook");
|
||||
}
|
||||
}
|
||||
|
||||
private static bool ShouldDeliver(WebhookEventType type, WebhookBlob wh)
|
||||
{
|
||||
return wh.Active && wh.AuthorizedEvents.Match(type);
|
||||
}
|
||||
|
||||
public class DeliveryResult
|
||||
{
|
||||
public string? DeliveryId { get; set; }
|
||||
public bool Success { get; set; }
|
||||
public string? ErrorMessage { get; set; }
|
||||
}
|
||||
|
||||
private async Task<DeliveryResult> SendDelivery(WebhookDeliveryRequest ctx, CancellationToken cancellationToken)
|
||||
{
|
||||
var uri = new Uri(ctx.WebhookBlob.Url, UriKind.Absolute);
|
||||
var httpClient = GetClient(uri);
|
||||
using var request = new HttpRequestMessage();
|
||||
request.RequestUri = uri;
|
||||
request.Method = HttpMethod.Post;
|
||||
byte[] bytes = ToBytes(ctx.WebhookEvent);
|
||||
var content = new ByteArrayContent(bytes);
|
||||
content.Headers.ContentType = new MediaTypeHeaderValue("application/json");
|
||||
using var hmac = new System.Security.Cryptography.HMACSHA256(UTF8.GetBytes(ctx.WebhookBlob.Secret ?? string.Empty));
|
||||
var sig = Encoders.Hex.EncodeData(hmac.ComputeHash(bytes));
|
||||
content.Headers.Add("BTCPay-Sig", $"sha256={sig}");
|
||||
request.Content = content;
|
||||
var deliveryBlob = ctx.Delivery.GetBlob() ?? new WebhookDeliveryBlob();
|
||||
deliveryBlob.Request = bytes;
|
||||
try
|
||||
{
|
||||
using var response = await httpClient.SendAsync(request, cancellationToken);
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
deliveryBlob.Status = WebhookDeliveryStatus.HttpError;
|
||||
deliveryBlob.ErrorMessage = $"HTTP Error Code {(int)response.StatusCode}";
|
||||
}
|
||||
else
|
||||
{
|
||||
deliveryBlob.Status = WebhookDeliveryStatus.HttpSuccess;
|
||||
}
|
||||
deliveryBlob.HttpCode = (int)response.StatusCode;
|
||||
}
|
||||
catch (Exception ex) when (!CancellationToken.IsCancellationRequested)
|
||||
{
|
||||
deliveryBlob.Status = WebhookDeliveryStatus.Failed;
|
||||
deliveryBlob.ErrorMessage = ex.Message;
|
||||
}
|
||||
ctx.Delivery.SetBlob(deliveryBlob);
|
||||
|
||||
return new DeliveryResult()
|
||||
{
|
||||
Success = deliveryBlob.ErrorMessage is null,
|
||||
DeliveryId = ctx.Delivery.Id,
|
||||
ErrorMessage = deliveryBlob.ErrorMessage
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
private async Task<DeliveryResult> SendAndSaveDelivery(WebhookDeliveryRequest ctx, CancellationToken cancellationToken)
|
||||
{
|
||||
var result = await SendDelivery(ctx, cancellationToken);
|
||||
await StoreRepository.AddWebhookDelivery(ctx.Delivery);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private byte[] ToBytes(WebhookEvent webhookEvent)
|
||||
{
|
||||
var str = JsonConvert.SerializeObject(webhookEvent, Formatting.Indented, DefaultSerializerSettings);
|
||||
var bytes = UTF8.GetBytes(str);
|
||||
return bytes;
|
||||
}
|
||||
|
||||
private static Data.WebhookDeliveryData NewDelivery(string webhookId)
|
||||
{
|
||||
return new Data.WebhookDeliveryData
|
||||
{
|
||||
Id = Encoders.Base58.EncodeData(RandomUtils.GetBytes(16)),
|
||||
Timestamp = DateTimeOffset.UtcNow,
|
||||
WebhookId = webhookId
|
||||
};
|
||||
}
|
||||
|
||||
public override async Task StopAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
var stopping = _processingQueue.Abort(cancellationToken);
|
||||
await base.StopAsync(cancellationToken);
|
||||
await stopping;
|
||||
}
|
||||
}
|
||||
}
|
12
BTCPayServer/HostedServices/Webhooks/IWebhookProvider.cs
Normal file
12
BTCPayServer/HostedServices/Webhooks/IWebhookProvider.cs
Normal file
|
@ -0,0 +1,12 @@
|
|||
#nullable enable
|
||||
using System.Collections.Generic;
|
||||
using BTCPayServer.Client.Models;
|
||||
|
||||
namespace BTCPayServer.HostedServices.Webhooks;
|
||||
|
||||
public interface IWebhookProvider
|
||||
{
|
||||
public Dictionary<string,string> GetSupportedWebhookTypes();
|
||||
|
||||
public WebhookEvent CreateTestEvent(string type, params object[] args);
|
||||
}
|
|
@ -0,0 +1,53 @@
|
|||
using System;
|
||||
using System.Globalization;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Client.Models;
|
||||
using BTCPayServer.Controllers;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using WebhookDeliveryData = BTCPayServer.Data.WebhookDeliveryData;
|
||||
|
||||
namespace BTCPayServer.HostedServices.Webhooks;
|
||||
|
||||
public class InvoiceWebhookDeliveryRequest : WebhookSender.WebhookDeliveryRequest
|
||||
{
|
||||
public InvoiceEntity Invoice { get; }
|
||||
|
||||
public InvoiceWebhookDeliveryRequest(InvoiceEntity invoice, string webhookId, WebhookEvent webhookEvent,
|
||||
WebhookDeliveryData delivery, WebhookBlob webhookBlob) : base(webhookId, webhookEvent, delivery, webhookBlob)
|
||||
{
|
||||
Invoice = invoice;
|
||||
}
|
||||
|
||||
public override Task<SendEmailRequest> Interpolate(SendEmailRequest req,
|
||||
UIStoresController.StoreEmailRule storeEmailRule)
|
||||
{
|
||||
if (storeEmailRule.CustomerEmail &&
|
||||
MailboxAddressValidator.TryParse(Invoice.Metadata.BuyerEmail, out var bmb))
|
||||
{
|
||||
req.Email ??= string.Empty;
|
||||
req.Email += $",{bmb}";
|
||||
}
|
||||
|
||||
req.Subject = Interpolate(req.Subject);
|
||||
req.Body = Interpolate(req.Body);
|
||||
return Task.FromResult(req);
|
||||
}
|
||||
|
||||
private string Interpolate(string str)
|
||||
{
|
||||
var res = str.Replace("{Invoice.Id}", Invoice.Id)
|
||||
.Replace("{Invoice.StoreId}", Invoice.StoreId)
|
||||
.Replace("{Invoice.Price}", Invoice.Price.ToString(CultureInfo.InvariantCulture))
|
||||
.Replace("{Invoice.Currency}", Invoice.Currency)
|
||||
.Replace("{Invoice.Status}", Invoice.Status.ToString())
|
||||
.Replace("{Invoice.AdditionalStatus}", Invoice.ExceptionStatus.ToString())
|
||||
.Replace("{Invoice.OrderId}", Invoice.Metadata.OrderId);
|
||||
|
||||
|
||||
res = InterpolateJsonField(str, "Invoice.Metadata", Invoice.Metadata.ToJObject());
|
||||
return res;
|
||||
}
|
||||
|
||||
}
|
120
BTCPayServer/HostedServices/Webhooks/InvoiceWebhookProvider.cs
Normal file
120
BTCPayServer/HostedServices/Webhooks/InvoiceWebhookProvider.cs
Normal file
|
@ -0,0 +1,120 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using BTCPayServer.Client.Models;
|
||||
using BTCPayServer.Controllers.Greenfield;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Events;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using WebhookDeliveryData = BTCPayServer.Data.WebhookDeliveryData;
|
||||
|
||||
namespace BTCPayServer.HostedServices.Webhooks;
|
||||
|
||||
public class InvoiceWebhookProvider : WebhookProvider<InvoiceEvent>
|
||||
{
|
||||
public InvoiceWebhookProvider(WebhookSender webhookSender, EventAggregator eventAggregator,
|
||||
ILogger<InvoiceWebhookProvider> logger) : base(
|
||||
eventAggregator, logger, webhookSender)
|
||||
{
|
||||
}
|
||||
|
||||
public override Dictionary<string, string> GetSupportedWebhookTypes()
|
||||
{
|
||||
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"},
|
||||
};
|
||||
}
|
||||
|
||||
protected override WebhookSender.WebhookDeliveryRequest CreateDeliveryRequest(InvoiceEvent invoiceEvent,
|
||||
WebhookData webhook)
|
||||
{
|
||||
var webhookEvent = GetWebhookEvent(invoiceEvent)!;
|
||||
var webhookBlob = webhook?.GetBlob();
|
||||
webhookEvent.InvoiceId = invoiceEvent.InvoiceId;
|
||||
webhookEvent.StoreId = invoiceEvent.Invoice.StoreId;
|
||||
webhookEvent.Metadata = invoiceEvent.Invoice.Metadata.ToJObject();
|
||||
webhookEvent.WebhookId = webhook?.Id;
|
||||
webhookEvent.IsRedelivery = false;
|
||||
WebhookDeliveryData delivery = webhook is null? null: WebhookExtensions.NewWebhookDelivery(webhook.Id);
|
||||
if (delivery is not null)
|
||||
{
|
||||
webhookEvent.DeliveryId = delivery.Id;
|
||||
webhookEvent.OriginalDeliveryId = delivery.Id;
|
||||
webhookEvent.Timestamp = delivery.Timestamp;
|
||||
}
|
||||
return new InvoiceWebhookDeliveryRequest(invoiceEvent.Invoice, webhook?.Id, webhookEvent,
|
||||
delivery, webhookBlob);
|
||||
}
|
||||
|
||||
public override WebhookEvent CreateTestEvent(string type, params object[] args)
|
||||
{
|
||||
var storeId = args[0].ToString();
|
||||
return new WebhookInvoiceEvent(type, storeId)
|
||||
{
|
||||
InvoiceId = "__test__" + Guid.NewGuid() + "__test__"
|
||||
};
|
||||
}
|
||||
|
||||
protected override WebhookInvoiceEvent GetWebhookEvent(InvoiceEvent invoiceEvent)
|
||||
{
|
||||
var eventCode = invoiceEvent.EventCode;
|
||||
var storeId = invoiceEvent.Invoice.StoreId;
|
||||
switch (eventCode)
|
||||
{
|
||||
case InvoiceEventCode.Confirmed:
|
||||
case InvoiceEventCode.MarkedCompleted:
|
||||
return new WebhookInvoiceSettledEvent(storeId)
|
||||
{
|
||||
ManuallyMarked = eventCode == InvoiceEventCode.MarkedCompleted
|
||||
};
|
||||
case InvoiceEventCode.Created:
|
||||
return new WebhookInvoiceEvent(WebhookEventType.InvoiceCreated, storeId);
|
||||
case InvoiceEventCode.Expired:
|
||||
return new WebhookInvoiceExpiredEvent(storeId)
|
||||
{
|
||||
PartiallyPaid = invoiceEvent.PaidPartial
|
||||
};
|
||||
case InvoiceEventCode.FailedToConfirm:
|
||||
case InvoiceEventCode.MarkedInvalid:
|
||||
return new WebhookInvoiceInvalidEvent(storeId)
|
||||
{
|
||||
ManuallyMarked = eventCode == InvoiceEventCode.MarkedInvalid
|
||||
};
|
||||
case InvoiceEventCode.PaidInFull:
|
||||
return new WebhookInvoiceProcessingEvent(storeId)
|
||||
{
|
||||
OverPaid = invoiceEvent.Invoice.ExceptionStatus == InvoiceExceptionStatus.PaidOver
|
||||
};
|
||||
case InvoiceEventCode.ReceivedPayment:
|
||||
return new WebhookInvoiceReceivedPaymentEvent(WebhookEventType.InvoiceReceivedPayment, storeId)
|
||||
{
|
||||
AfterExpiration =
|
||||
invoiceEvent.Invoice.Status.ToModernStatus() == InvoiceStatus.Expired ||
|
||||
invoiceEvent.Invoice.Status.ToModernStatus() == InvoiceStatus.Invalid,
|
||||
PaymentMethod = invoiceEvent.Payment.GetPaymentMethodId().ToStringNormalized(),
|
||||
Payment = GreenfieldInvoiceController.ToPaymentModel(invoiceEvent.Invoice, invoiceEvent.Payment),
|
||||
StoreId = invoiceEvent.Invoice.StoreId
|
||||
};
|
||||
case InvoiceEventCode.PaymentSettled:
|
||||
return new WebhookInvoiceReceivedPaymentEvent(WebhookEventType.InvoicePaymentSettled, storeId)
|
||||
{
|
||||
AfterExpiration =
|
||||
invoiceEvent.Invoice.Status.ToModernStatus() == InvoiceStatus.Expired ||
|
||||
invoiceEvent.Invoice.Status.ToModernStatus() == InvoiceStatus.Invalid,
|
||||
PaymentMethod = invoiceEvent.Payment.GetPaymentMethodId().ToStringNormalized(),
|
||||
Payment = GreenfieldInvoiceController.ToPaymentModel(invoiceEvent.Invoice, invoiceEvent.Payment),
|
||||
OverPaid = invoiceEvent.Invoice.ExceptionStatus == InvoiceExceptionStatus.PaidOver,
|
||||
StoreId = invoiceEvent.Invoice.StoreId
|
||||
};
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,50 @@
|
|||
#nullable enable
|
||||
using System.Globalization;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Client.Models;
|
||||
using BTCPayServer.Controllers;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Services.PaymentRequests;
|
||||
using WebhookDeliveryData = BTCPayServer.Data.WebhookDeliveryData;
|
||||
|
||||
namespace BTCPayServer.HostedServices.Webhooks;
|
||||
|
||||
public class PaymentRequestWebhookDeliveryRequest : WebhookSender.WebhookDeliveryRequest
|
||||
{
|
||||
private readonly PaymentRequestEvent _evt;
|
||||
|
||||
public PaymentRequestWebhookDeliveryRequest(PaymentRequestEvent evt, string webhookId, WebhookEvent webhookEvent,
|
||||
WebhookDeliveryData delivery, WebhookBlob webhookBlob) : base(webhookId, webhookEvent, delivery, webhookBlob)
|
||||
{
|
||||
_evt = evt;
|
||||
}
|
||||
|
||||
public override Task<SendEmailRequest?> Interpolate(SendEmailRequest req,
|
||||
UIStoresController.StoreEmailRule storeEmailRule)
|
||||
{
|
||||
var blob = _evt.Data.GetBlob();
|
||||
if (storeEmailRule.CustomerEmail &&
|
||||
MailboxAddressValidator.TryParse(blob.Email, out var bmb))
|
||||
{
|
||||
req.Email ??= string.Empty;
|
||||
req.Email += $",{bmb}";
|
||||
}
|
||||
|
||||
req.Subject = Interpolate(req.Subject, blob);
|
||||
req.Body = Interpolate(req.Body, blob);
|
||||
return Task.FromResult(req)!;
|
||||
}
|
||||
|
||||
private string Interpolate(string str, PaymentRequestBaseData blob)
|
||||
{
|
||||
var res= str.Replace("{PaymentRequest.Id}", _evt.Data.Id)
|
||||
.Replace("{PaymentRequest.Price}", blob.Amount.ToString(CultureInfo.InvariantCulture))
|
||||
.Replace("{PaymentRequest.Currency}", blob.Currency)
|
||||
.Replace("{PaymentRequest.Title}", blob.Title)
|
||||
.Replace("{PaymentRequest.Description}", blob.Description)
|
||||
.Replace("{PaymentRequest.Status}", _evt.Data.Status.ToString());
|
||||
|
||||
res= InterpolateJsonField(res, "PaymentRequest.FormResponse", blob.FormResponse);
|
||||
return res;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,67 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using BTCPayServer.Client.Models;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Services.PaymentRequests;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using WebhookDeliveryData = BTCPayServer.Data.WebhookDeliveryData;
|
||||
|
||||
namespace BTCPayServer.HostedServices.Webhooks;
|
||||
|
||||
public class PaymentRequestWebhookProvider: WebhookProvider<PaymentRequestEvent>
|
||||
{
|
||||
public PaymentRequestWebhookProvider(EventAggregator eventAggregator, ILogger<PaymentRequestWebhookProvider> logger, WebhookSender webhookSender) : base(eventAggregator, logger, webhookSender)
|
||||
{
|
||||
}
|
||||
|
||||
public override Dictionary<string, string> GetSupportedWebhookTypes()
|
||||
{
|
||||
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"},
|
||||
};
|
||||
}
|
||||
|
||||
public override WebhookEvent CreateTestEvent(string type, object[] args)
|
||||
{
|
||||
var storeId = args[0].ToString();
|
||||
return new WebhookPayoutEvent(type, storeId)
|
||||
{
|
||||
PayoutId = "__test__" + Guid.NewGuid() + "__test__"
|
||||
};
|
||||
}
|
||||
|
||||
protected override WebhookPaymentRequestEvent GetWebhookEvent(PaymentRequestEvent evt)
|
||||
{
|
||||
return evt.Type switch
|
||||
{
|
||||
PaymentRequestEvent.Created => new WebhookPaymentRequestEvent(WebhookEventType.PaymentRequestCreated, evt.Data.StoreDataId),
|
||||
PaymentRequestEvent.Updated => new WebhookPaymentRequestEvent(WebhookEventType.PaymentRequestUpdated, evt.Data.StoreDataId),
|
||||
PaymentRequestEvent.Archived => new WebhookPaymentRequestEvent(WebhookEventType.PaymentRequestArchived, evt.Data.StoreDataId),
|
||||
PaymentRequestEvent.StatusChanged => new WebhookPaymentRequestEvent(WebhookEventType.PaymentRequestStatusChanged, evt.Data.StoreDataId),
|
||||
_ => null
|
||||
};
|
||||
}
|
||||
|
||||
protected override WebhookSender.WebhookDeliveryRequest CreateDeliveryRequest(PaymentRequestEvent paymentRequestEvent, WebhookData webhook)
|
||||
{
|
||||
var webhookBlob = webhook?.GetBlob();
|
||||
var webhookEvent = GetWebhookEvent(paymentRequestEvent)!;
|
||||
webhookEvent.StoreId = paymentRequestEvent.Data.StoreDataId;
|
||||
webhookEvent.PaymentRequestId = paymentRequestEvent.Data.Id;
|
||||
webhookEvent.Status = paymentRequestEvent.Data.Status;
|
||||
webhookEvent.WebhookId = webhook?.Id;
|
||||
webhookEvent.IsRedelivery = false;
|
||||
WebhookDeliveryData delivery = webhook is null? null: WebhookExtensions.NewWebhookDelivery(webhook.Id);
|
||||
if (delivery is not null)
|
||||
{
|
||||
webhookEvent.DeliveryId = delivery.Id;
|
||||
webhookEvent.OriginalDeliveryId = delivery.Id;
|
||||
webhookEvent.Timestamp = delivery.Timestamp;
|
||||
}
|
||||
return new PaymentRequestWebhookDeliveryRequest(paymentRequestEvent,webhook?.Id, webhookEvent, delivery, webhookBlob );
|
||||
}
|
||||
}
|
|
@ -0,0 +1,36 @@
|
|||
#nullable enable
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Client.Models;
|
||||
using BTCPayServer.Controllers;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Services;
|
||||
using WebhookDeliveryData = BTCPayServer.Data.WebhookDeliveryData;
|
||||
|
||||
namespace BTCPayServer.HostedServices.Webhooks;
|
||||
|
||||
public class PayoutWebhookDeliveryRequest(PayoutEvent evt, string? webhookId, WebhookEvent webhookEvent,
|
||||
WebhookDeliveryData? delivery, WebhookBlob? webhookBlob,
|
||||
BTCPayNetworkJsonSerializerSettings btcPayNetworkJsonSerializerSettings)
|
||||
: WebhookSender.WebhookDeliveryRequest(webhookId!, webhookEvent, delivery!, webhookBlob!)
|
||||
{
|
||||
public override Task<SendEmailRequest?> Interpolate(SendEmailRequest req,
|
||||
UIStoresController.StoreEmailRule storeEmailRule)
|
||||
{
|
||||
req.Subject = Interpolate(req.Subject);
|
||||
req.Body = Interpolate(req.Body);
|
||||
return Task.FromResult(req)!;
|
||||
}
|
||||
|
||||
private string Interpolate(string str)
|
||||
{
|
||||
var res= str.Replace("{Payout.Id}", evt.Payout.Id)
|
||||
.Replace("{Payout.PullPaymentId}", evt.Payout.PullPaymentDataId)
|
||||
.Replace("{Payout.Destination}", evt.Payout.Destination)
|
||||
.Replace("{Payout.State}", evt.Payout.State.ToString());
|
||||
|
||||
var blob = evt.Payout.GetBlob(btcPayNetworkJsonSerializerSettings);
|
||||
|
||||
res = InterpolateJsonField(res, "Payout.Metadata", blob.Metadata);
|
||||
return res;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,64 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using BTCPayServer.Client.Models;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Services;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace BTCPayServer.HostedServices.Webhooks;
|
||||
|
||||
public class PayoutWebhookProvider(EventAggregator eventAggregator, ILogger<PayoutWebhookProvider> logger,
|
||||
WebhookSender webhookSender, BTCPayNetworkJsonSerializerSettings btcPayNetworkJsonSerializerSettings)
|
||||
: WebhookProvider<PayoutEvent>(eventAggregator, logger, webhookSender)
|
||||
{
|
||||
protected override WebhookSender.WebhookDeliveryRequest CreateDeliveryRequest(PayoutEvent payoutEvent, WebhookData webhook)
|
||||
{
|
||||
var webhookBlob = webhook?.GetBlob();
|
||||
|
||||
var webhookEvent = GetWebhookEvent(payoutEvent)!;
|
||||
webhookEvent.StoreId = payoutEvent.Payout.StoreDataId;
|
||||
webhookEvent.PayoutId = payoutEvent.Payout.Id;
|
||||
webhookEvent.PayoutState = payoutEvent.Payout.State;
|
||||
webhookEvent.PullPaymentId = payoutEvent.Payout.PullPaymentDataId;
|
||||
webhookEvent.WebhookId = webhook?.Id;
|
||||
webhookEvent.IsRedelivery = false;
|
||||
Data.WebhookDeliveryData delivery = webhook is null? null: WebhookExtensions.NewWebhookDelivery(webhook.Id);
|
||||
if (delivery is not null)
|
||||
{
|
||||
webhookEvent.DeliveryId = delivery.Id;
|
||||
webhookEvent.OriginalDeliveryId = delivery.Id;
|
||||
webhookEvent.Timestamp = delivery.Timestamp;
|
||||
}
|
||||
return new PayoutWebhookDeliveryRequest(payoutEvent,webhook?.Id, webhookEvent, delivery, webhookBlob, btcPayNetworkJsonSerializerSettings);
|
||||
}
|
||||
|
||||
public override Dictionary<string, string> GetSupportedWebhookTypes()
|
||||
{
|
||||
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"}
|
||||
};
|
||||
}
|
||||
|
||||
public override WebhookEvent CreateTestEvent(string type, object[] args)
|
||||
{
|
||||
var storeId = args[0].ToString();
|
||||
return new WebhookPayoutEvent(type, storeId)
|
||||
{
|
||||
PayoutId = "__test__" + Guid.NewGuid() + "__test__"
|
||||
};
|
||||
}
|
||||
|
||||
protected override WebhookPayoutEvent GetWebhookEvent(PayoutEvent payoutEvent)
|
||||
{
|
||||
return payoutEvent.Type switch
|
||||
{
|
||||
PayoutEvent.PayoutEventType.Created => new WebhookPayoutEvent(WebhookEventType.PayoutCreated, payoutEvent.Payout.StoreDataId),
|
||||
PayoutEvent.PayoutEventType.Approved => new WebhookPayoutEvent(WebhookEventType.PayoutApproved, payoutEvent.Payout.StoreDataId),
|
||||
PayoutEvent.PayoutEventType.Updated => new WebhookPayoutEvent(WebhookEventType.PayoutUpdated, payoutEvent.Payout.StoreDataId),
|
||||
_ => null
|
||||
};
|
||||
}
|
||||
}
|
56
BTCPayServer/HostedServices/Webhooks/WebhookExtensions.cs
Normal file
56
BTCPayServer/HostedServices/Webhooks/WebhookExtensions.cs
Normal file
|
@ -0,0 +1,56 @@
|
|||
using System;
|
||||
using System.Net.Http;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Services;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using NBitcoin;
|
||||
using NBitcoin.DataEncoders;
|
||||
|
||||
namespace BTCPayServer.HostedServices.Webhooks;
|
||||
|
||||
public static class WebhookExtensions
|
||||
{
|
||||
public static Data.WebhookDeliveryData NewWebhookDelivery(string webhookId)
|
||||
{
|
||||
return new Data.WebhookDeliveryData
|
||||
{
|
||||
Id = Encoders.Base58.EncodeData(RandomUtils.GetBytes(16)),
|
||||
Timestamp = DateTimeOffset.UtcNow,
|
||||
WebhookId = webhookId
|
||||
};
|
||||
}
|
||||
|
||||
public static bool ShouldDeliver(this WebhookBlob wh, string type)
|
||||
{
|
||||
return wh.Active && wh.AuthorizedEvents.Match(type);
|
||||
}
|
||||
|
||||
public static IServiceCollection AddWebhooks(this IServiceCollection services)
|
||||
{
|
||||
services.AddSingleton<InvoiceWebhookProvider>();
|
||||
services.AddSingleton<IWebhookProvider>(o => o.GetRequiredService<InvoiceWebhookProvider>());
|
||||
services.AddHostedService(o => o.GetRequiredService<InvoiceWebhookProvider>());
|
||||
|
||||
services.AddSingleton<PayoutWebhookProvider>();
|
||||
services.AddSingleton<IWebhookProvider>(o => o.GetRequiredService<PayoutWebhookProvider>());
|
||||
services.AddHostedService(o => o.GetRequiredService<PayoutWebhookProvider>());
|
||||
|
||||
services.AddSingleton<PaymentRequestWebhookProvider>();
|
||||
services.AddSingleton<IWebhookProvider>(o => o.GetRequiredService<PaymentRequestWebhookProvider>());
|
||||
services.AddHostedService(o => o.GetRequiredService<PaymentRequestWebhookProvider>());
|
||||
|
||||
services.AddSingleton<WebhookSender>();
|
||||
services.AddSingleton<IHostedService, WebhookSender>(o => o.GetRequiredService<WebhookSender>());
|
||||
services.AddScheduledTask<CleanupWebhookDeliveriesTask>(TimeSpan.FromHours(6.0));
|
||||
services.AddHttpClient(WebhookSender.OnionNamedClient)
|
||||
.ConfigurePrimaryHttpMessageHandler<Socks5HttpClientHandler>();
|
||||
services.AddHttpClient(WebhookSender.LoopbackNamedClient)
|
||||
.ConfigurePrimaryHttpMessageHandler(_ => new HttpClientHandler
|
||||
{
|
||||
ServerCertificateCustomValidationCallback =
|
||||
HttpClientHandler.DangerousAcceptAnyServerCertificateValidator
|
||||
});
|
||||
return services;
|
||||
}
|
||||
}
|
45
BTCPayServer/HostedServices/Webhooks/WebhookProvider.cs
Normal file
45
BTCPayServer/HostedServices/Webhooks/WebhookProvider.cs
Normal file
|
@ -0,0 +1,45 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Client.Models;
|
||||
using BTCPayServer.Data;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace BTCPayServer.HostedServices.Webhooks;
|
||||
|
||||
public abstract class WebhookProvider<T>(EventAggregator eventAggregator, ILogger logger, WebhookSender webhookSender)
|
||||
: EventHostedServiceBase(eventAggregator, logger), IWebhookProvider
|
||||
{
|
||||
public abstract Dictionary<string, string> GetSupportedWebhookTypes();
|
||||
|
||||
protected abstract WebhookSender.WebhookDeliveryRequest CreateDeliveryRequest(T evt, WebhookData webhook);
|
||||
|
||||
public abstract WebhookEvent CreateTestEvent(string type, params object[] args);
|
||||
|
||||
protected abstract StoreWebhookEvent GetWebhookEvent(T evt);
|
||||
|
||||
protected override void SubscribeToEvents()
|
||||
{
|
||||
Subscribe<T>();
|
||||
base.SubscribeToEvents();
|
||||
}
|
||||
protected override async Task ProcessEvent(object evt, CancellationToken cancellationToken)
|
||||
{
|
||||
if (evt is T tEvt)
|
||||
{
|
||||
if (GetWebhookEvent(tEvt) is not { } webhookEvent)
|
||||
return;
|
||||
|
||||
var webhooks = await webhookSender.GetWebhooks(webhookEvent.StoreId, webhookEvent.Type);
|
||||
foreach (var webhook in webhooks)
|
||||
{
|
||||
webhookSender.EnqueueDelivery(CreateDeliveryRequest(tEvt, webhook));
|
||||
}
|
||||
|
||||
EventAggregator.Publish(CreateDeliveryRequest(tEvt, null));
|
||||
}
|
||||
|
||||
await base.ProcessEvent(evt, cancellationToken);
|
||||
}
|
||||
|
||||
}
|
345
BTCPayServer/HostedServices/Webhooks/WebhookSender.cs
Normal file
345
BTCPayServer/HostedServices/Webhooks/WebhookSender.cs
Normal file
|
@ -0,0 +1,345 @@
|
|||
#nullable enable
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Net.Http.Headers;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Client.Models;
|
||||
using BTCPayServer.Controllers;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Services.Stores;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using NBitcoin.DataEncoders;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace BTCPayServer.HostedServices.Webhooks
|
||||
{
|
||||
/// <summary>
|
||||
/// This class sends webhook notifications
|
||||
/// It also makes sure the events sent to a webhook are sent in order to the webhook
|
||||
/// </summary>
|
||||
public class WebhookSender : IHostedService
|
||||
{
|
||||
public const string OnionNamedClient = "greenfield-webhook.onion";
|
||||
public const string ClearnetNamedClient = "greenfield-webhook.clearnet";
|
||||
public const string LoopbackNamedClient = "greenfield-webhook.loopback";
|
||||
public static string[] AllClients = new[] {OnionNamedClient, ClearnetNamedClient, LoopbackNamedClient};
|
||||
|
||||
private readonly EventAggregator _eventAggregator;
|
||||
private readonly ApplicationDbContextFactory _dbContextFactory;
|
||||
private readonly ILogger<WebhookSender> _logger;
|
||||
private readonly IServiceProvider _serviceProvider;
|
||||
private readonly Encoding _utf8 = new UTF8Encoding(false);
|
||||
public static readonly JsonSerializerSettings DefaultSerializerSettings;
|
||||
|
||||
|
||||
private readonly MultiProcessingQueue _processingQueue = new();
|
||||
private StoreRepository StoreRepository { get; }
|
||||
private IHttpClientFactory HttpClientFactory { get; }
|
||||
|
||||
|
||||
static WebhookSender()
|
||||
{
|
||||
DefaultSerializerSettings = WebhookEvent.DefaultSerializerSettings;
|
||||
}
|
||||
|
||||
public WebhookSender(
|
||||
StoreRepository storeRepository,
|
||||
IHttpClientFactory httpClientFactory,
|
||||
ApplicationDbContextFactory dbContextFactory,
|
||||
ILogger<WebhookSender> logger,
|
||||
IServiceProvider serviceProvider,
|
||||
EventAggregator eventAggregator)
|
||||
{
|
||||
_dbContextFactory = dbContextFactory;
|
||||
_logger = logger;
|
||||
_serviceProvider = serviceProvider;
|
||||
_eventAggregator = eventAggregator;
|
||||
StoreRepository = storeRepository;
|
||||
HttpClientFactory = httpClientFactory;
|
||||
}
|
||||
|
||||
|
||||
private HttpClient GetClient(Uri uri)
|
||||
{
|
||||
return HttpClientFactory.CreateClient(uri.IsOnion() ? OnionNamedClient :
|
||||
uri.IsLoopback ? LoopbackNamedClient : ClearnetNamedClient);
|
||||
}
|
||||
|
||||
public class WebhookDeliveryRequest(string webhookId, WebhookEvent webhookEvent,
|
||||
Data.WebhookDeliveryData delivery, WebhookBlob webhookBlob)
|
||||
{
|
||||
public WebhookEvent WebhookEvent { get; } = webhookEvent;
|
||||
public Data.WebhookDeliveryData Delivery { get; } = delivery;
|
||||
public WebhookBlob WebhookBlob { get; } = webhookBlob;
|
||||
public string WebhookId { get; } = webhookId;
|
||||
|
||||
public virtual Task<SendEmailRequest?> Interpolate(SendEmailRequest req,
|
||||
UIStoresController.StoreEmailRule storeEmailRule)
|
||||
{
|
||||
return Task.FromResult(req)!;
|
||||
}
|
||||
|
||||
protected static string InterpolateJsonField(string str, string fieldName, JObject obj)
|
||||
{
|
||||
fieldName += ".";
|
||||
//find all instance of {fieldName*} instead str, then run obj.SelectToken(*) on it
|
||||
while (true)
|
||||
{
|
||||
|
||||
var start = str.IndexOf($"{{{fieldName}", StringComparison.InvariantCultureIgnoreCase);
|
||||
if(start == -1)
|
||||
break;
|
||||
start += fieldName.Length + 1;
|
||||
var end = str.IndexOf("}", start, StringComparison.InvariantCultureIgnoreCase);
|
||||
if(end == -1)
|
||||
break;
|
||||
var jsonpath = str.Substring(start, end - start);
|
||||
string? result = string.Empty;
|
||||
try
|
||||
{
|
||||
if (string.IsNullOrEmpty(jsonpath))
|
||||
{
|
||||
result = obj.ToString();
|
||||
}
|
||||
else
|
||||
{
|
||||
result = obj.SelectToken(jsonpath)?.ToString();
|
||||
}
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
// ignored
|
||||
}
|
||||
|
||||
str = str.Replace($"{{{fieldName}{jsonpath}}}", result);
|
||||
}
|
||||
|
||||
return str;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<string?> Redeliver(string deliveryId)
|
||||
{
|
||||
var deliveryRequest = await CreateRedeliveryRequest(deliveryId);
|
||||
if (deliveryRequest is null)
|
||||
return null;
|
||||
EnqueueDelivery(deliveryRequest);
|
||||
return deliveryRequest.Delivery.Id;
|
||||
}
|
||||
|
||||
private async Task<WebhookDeliveryRequest?> CreateRedeliveryRequest(string deliveryId)
|
||||
{
|
||||
await using var ctx = _dbContextFactory.CreateContext();
|
||||
var webhookDelivery = await ctx.WebhookDeliveries.AsNoTracking()
|
||||
.Where(o => o.Id == deliveryId)
|
||||
.Select(o => new {Webhook = o.Webhook, Delivery = o})
|
||||
.FirstOrDefaultAsync();
|
||||
if (webhookDelivery is null)
|
||||
return null;
|
||||
var oldDeliveryBlob = webhookDelivery.Delivery.GetBlob();
|
||||
var newDelivery = WebhookExtensions.NewWebhookDelivery(webhookDelivery.Webhook.Id);
|
||||
var newDeliveryBlob = new WebhookDeliveryBlob();
|
||||
newDeliveryBlob.Request = oldDeliveryBlob.Request;
|
||||
var webhookEvent = newDeliveryBlob.ReadRequestAs<WebhookEvent>();
|
||||
if (webhookEvent.IsPruned())
|
||||
return null;
|
||||
webhookEvent.DeliveryId = newDelivery.Id;
|
||||
webhookEvent.WebhookId = webhookDelivery.Webhook.Id;
|
||||
// if we redelivered a redelivery, we still want the initial delivery here
|
||||
webhookEvent.OriginalDeliveryId ??= deliveryId;
|
||||
webhookEvent.IsRedelivery = true;
|
||||
newDeliveryBlob.Request = ToBytes(webhookEvent);
|
||||
newDelivery.SetBlob(newDeliveryBlob);
|
||||
return new WebhookDeliveryRequest(webhookDelivery.Webhook.Id, webhookEvent, newDelivery,
|
||||
webhookDelivery.Webhook.GetBlob());
|
||||
}
|
||||
|
||||
private WebhookEvent GetTestWebHook(string storeId, string webhookId, string webhookEventType,
|
||||
Data.WebhookDeliveryData delivery)
|
||||
{
|
||||
var webhookProvider = _serviceProvider.GetServices<IWebhookProvider>()
|
||||
.FirstOrDefault(provider => provider.GetSupportedWebhookTypes().ContainsKey(webhookEventType));
|
||||
|
||||
if (webhookProvider is null)
|
||||
throw new ArgumentException($"Unknown webhook event type {webhookEventType}", webhookEventType);
|
||||
|
||||
var webhookEvent = webhookProvider.CreateTestEvent(webhookEventType, storeId);
|
||||
if(webhookEvent is null)
|
||||
throw new ArgumentException($"Webhook provider does not support tests");
|
||||
|
||||
webhookEvent.DeliveryId = delivery.Id;
|
||||
webhookEvent.WebhookId = webhookId;
|
||||
webhookEvent.OriginalDeliveryId = "__test__" + Guid.NewGuid() + "__test__";
|
||||
webhookEvent.IsRedelivery = false;
|
||||
webhookEvent.Timestamp = delivery.Timestamp;
|
||||
|
||||
return webhookEvent;
|
||||
}
|
||||
|
||||
public async Task<DeliveryResult> TestWebhook(string storeId, string webhookId, string webhookEventType,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var delivery = WebhookExtensions.NewWebhookDelivery(webhookId);
|
||||
var webhook = (await StoreRepository.GetWebhooks(storeId)).FirstOrDefault(w => w.Id == webhookId);
|
||||
var deliveryRequest = new WebhookDeliveryRequest(
|
||||
webhookId,
|
||||
GetTestWebHook(storeId, webhookId, webhookEventType, delivery),
|
||||
delivery,
|
||||
webhook.GetBlob()
|
||||
);
|
||||
return await SendDelivery(deliveryRequest, cancellationToken);
|
||||
}
|
||||
|
||||
public void EnqueueDelivery(WebhookDeliveryRequest context)
|
||||
{
|
||||
_processingQueue.Enqueue(context.WebhookId, (cancellationToken) => Process(context, cancellationToken));
|
||||
}
|
||||
private async Task Process(WebhookDeliveryRequest ctx, CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
var wh = (await StoreRepository.GetWebhook(ctx.WebhookId))?.GetBlob();
|
||||
if (wh is null || !wh.ShouldDeliver(ctx.WebhookEvent.Type))
|
||||
return;
|
||||
var result = await SendAndSaveDelivery(ctx, cancellationToken);
|
||||
if (ctx.WebhookBlob.AutomaticRedelivery &&
|
||||
!result.Success &&
|
||||
result.DeliveryId is not null)
|
||||
{
|
||||
var originalDeliveryId = result.DeliveryId;
|
||||
foreach (var wait in new[]
|
||||
{
|
||||
TimeSpan.FromSeconds(10), TimeSpan.FromMinutes(1), TimeSpan.FromMinutes(10),
|
||||
TimeSpan.FromMinutes(10), TimeSpan.FromMinutes(10), TimeSpan.FromMinutes(10),
|
||||
TimeSpan.FromMinutes(10), TimeSpan.FromMinutes(10),
|
||||
})
|
||||
{
|
||||
await Task.Delay(wait, cancellationToken);
|
||||
ctx = (await CreateRedeliveryRequest(originalDeliveryId))!;
|
||||
// This may have changed
|
||||
if (ctx is null || !ctx.WebhookBlob.AutomaticRedelivery ||
|
||||
!ctx.WebhookBlob.ShouldDeliver(ctx.WebhookEvent.Type))
|
||||
return;
|
||||
result = await SendAndSaveDelivery(ctx, cancellationToken);
|
||||
if (result.Success)
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch when (cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Unexpected error when processing a webhook");
|
||||
}
|
||||
}
|
||||
|
||||
public class DeliveryResult
|
||||
{
|
||||
public string? DeliveryId { get; set; }
|
||||
public bool Success { get; set; }
|
||||
public string? ErrorMessage { get; set; }
|
||||
}
|
||||
|
||||
private async Task<DeliveryResult> SendDelivery(WebhookDeliveryRequest ctx, CancellationToken cancellationToken)
|
||||
{
|
||||
var uri = new Uri(ctx.WebhookBlob.Url, UriKind.Absolute);
|
||||
var httpClient = GetClient(uri);
|
||||
using var request = new HttpRequestMessage();
|
||||
request.RequestUri = uri;
|
||||
request.Method = HttpMethod.Post;
|
||||
byte[] bytes = ToBytes(ctx.WebhookEvent);
|
||||
var content = new ByteArrayContent(bytes);
|
||||
content.Headers.ContentType = new MediaTypeHeaderValue("application/json");
|
||||
using var hmac =
|
||||
new System.Security.Cryptography.HMACSHA256(_utf8.GetBytes(ctx.WebhookBlob.Secret ?? string.Empty));
|
||||
var sig = Encoders.Hex.EncodeData(hmac.ComputeHash(bytes));
|
||||
content.Headers.Add("BTCPay-Sig", $"sha256={sig}");
|
||||
request.Content = content;
|
||||
var deliveryBlob = ctx.Delivery.GetBlob() ?? new WebhookDeliveryBlob();
|
||||
deliveryBlob.Request = bytes;
|
||||
try
|
||||
{
|
||||
using var response = await httpClient.SendAsync(request, cancellationToken);
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
deliveryBlob.Status = WebhookDeliveryStatus.HttpError;
|
||||
deliveryBlob.ErrorMessage = $"HTTP Error Code {(int)response.StatusCode}";
|
||||
}
|
||||
else
|
||||
{
|
||||
deliveryBlob.Status = WebhookDeliveryStatus.HttpSuccess;
|
||||
}
|
||||
|
||||
deliveryBlob.HttpCode = (int)response.StatusCode;
|
||||
}
|
||||
catch (Exception ex) when (!cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
deliveryBlob.Status = WebhookDeliveryStatus.Failed;
|
||||
deliveryBlob.ErrorMessage = ex.Message;
|
||||
}
|
||||
|
||||
ctx.Delivery.SetBlob(deliveryBlob);
|
||||
|
||||
return new DeliveryResult()
|
||||
{
|
||||
Success = deliveryBlob.ErrorMessage is null,
|
||||
DeliveryId = ctx.Delivery.Id,
|
||||
ErrorMessage = deliveryBlob.ErrorMessage
|
||||
};
|
||||
}
|
||||
|
||||
private async Task<DeliveryResult> SendAndSaveDelivery(WebhookDeliveryRequest ctx,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var result = await SendDelivery(ctx, cancellationToken);
|
||||
await StoreRepository.AddWebhookDelivery(ctx.Delivery);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private byte[] ToBytes(WebhookEvent webhookEvent)
|
||||
{
|
||||
var str = JsonConvert.SerializeObject(webhookEvent, Formatting.Indented, DefaultSerializerSettings);
|
||||
var bytes = _utf8.GetBytes(str);
|
||||
return bytes;
|
||||
}
|
||||
|
||||
public Task StartAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public async Task StopAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
var stopping = _processingQueue.Abort(cancellationToken);
|
||||
await stopping;
|
||||
}
|
||||
|
||||
public async Task<WebhookData[]> GetWebhooks(string invoiceStoreId, string? webhookEventType)
|
||||
{
|
||||
return (await StoreRepository.GetWebhooks(invoiceStoreId)).Where(data => webhookEventType is null || data.GetBlob().ShouldDeliver(webhookEventType)).ToArray();
|
||||
}
|
||||
|
||||
public async Task<UIStoresController.StoreEmailRule[]> GetEmailRules(string storeId,
|
||||
string type)
|
||||
{
|
||||
return ( await StoreRepository.FindStore(storeId))?.GetStoreBlob().EmailRules?.Where(rule => rule.Trigger ==type).ToArray() ?? Array.Empty<UIStoresController.StoreEmailRule>();
|
||||
}
|
||||
|
||||
public Dictionary<string, string> GetSupportedWebhookTypes()
|
||||
{
|
||||
return _serviceProvider.GetServices<IWebhookProvider>()
|
||||
.SelectMany(provider => provider.GetSupportedWebhookTypes()).ToDictionary(pair => pair.Key, pair => pair.Value);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -18,6 +18,7 @@ using BTCPayServer.Data;
|
|||
using BTCPayServer.Data.Payouts.LightningLike;
|
||||
using BTCPayServer.Forms;
|
||||
using BTCPayServer.HostedServices;
|
||||
using BTCPayServer.HostedServices.Webhooks;
|
||||
using BTCPayServer.Lightning;
|
||||
using BTCPayServer.Lightning.Charge;
|
||||
using BTCPayServer.Lightning.CLightning;
|
||||
|
@ -374,11 +375,8 @@ namespace BTCPayServer.Hosting
|
|||
services.AddSingleton<IHostedService, Services.NBXplorerConnectionFactory>(o => o.GetRequiredService<Services.NBXplorerConnectionFactory>());
|
||||
services.AddSingleton<HostedServices.CheckConfigurationHostedService>();
|
||||
services.AddSingleton<IHostedService, HostedServices.CheckConfigurationHostedService>(o => o.GetRequiredService<CheckConfigurationHostedService>());
|
||||
services.AddSingleton<HostedServices.WebhookSender>();
|
||||
services.AddSingleton<IHostedService, WebhookSender>(o => o.GetRequiredService<WebhookSender>());
|
||||
services.AddSingleton<IHostedService, StoreEmailRuleProcessorSender>();
|
||||
services.AddSingleton<IHostedService, PeriodicTaskLauncherHostedService>();
|
||||
services.AddScheduledTask<CleanupWebhookDeliveriesTask>(TimeSpan.FromHours(6.0));
|
||||
services.AddScheduledTask<GithubVersionFetcher>(TimeSpan.FromDays(1));
|
||||
services.AddScheduledTask<PluginUpdateFetcher>(TimeSpan.FromDays(1));
|
||||
|
||||
|
@ -386,17 +384,7 @@ namespace BTCPayServer.Hosting
|
|||
services.AddReportProvider<OnChainWalletReportProvider>();
|
||||
services.AddReportProvider<ProductsReportProvider>();
|
||||
services.AddReportProvider<PayoutsReportProvider>();
|
||||
|
||||
services.AddHttpClient(WebhookSender.OnionNamedClient)
|
||||
.ConfigurePrimaryHttpMessageHandler<Socks5HttpClientHandler>();
|
||||
services.AddHttpClient(WebhookSender.LoopbackNamedClient)
|
||||
.ConfigurePrimaryHttpMessageHandler(_ => new HttpClientHandler
|
||||
{
|
||||
ServerCertificateCustomValidationCallback =
|
||||
HttpClientHandler.DangerousAcceptAnyServerCertificateValidator
|
||||
});
|
||||
|
||||
|
||||
services.AddWebhooks();
|
||||
services.AddSingleton<BitcoinLikePayoutHandler>();
|
||||
services.AddSingleton<IPayoutHandler>(provider => provider.GetRequiredService<BitcoinLikePayoutHandler>());
|
||||
services.AddSingleton<IPayoutHandler>(provider => provider.GetRequiredService<LightningLikePayoutHandler>());
|
||||
|
|
|
@ -98,6 +98,7 @@ namespace BTCPayServer.Models.PaymentRequestViewModels
|
|||
|
||||
public Dictionary<string, object> FormResponse { get; set; }
|
||||
public bool AmountAndCurrencyEditable { get; set; } = true;
|
||||
public bool? HasEmailRules { get; set; }
|
||||
}
|
||||
|
||||
public class ViewPaymentRequestViewModel
|
||||
|
|
|
@ -30,7 +30,7 @@ namespace BTCPayServer.Models.StoreViewModels
|
|||
}
|
||||
public string Id { get; set; }
|
||||
public DateTimeOffset Time { get; set; }
|
||||
public WebhookEventType Type { get; private set; }
|
||||
public string Type { get; private set; }
|
||||
public bool Pruned { get; set; }
|
||||
public string WebhookId { get; set; }
|
||||
public bool Success { get; set; }
|
||||
|
@ -57,7 +57,7 @@ namespace BTCPayServer.Models.StoreViewModels
|
|||
public bool Active { get; set; }
|
||||
public bool AutomaticRedelivery { get; set; }
|
||||
public bool Everything { get; set; }
|
||||
public WebhookEventType[] Events { get; set; } = Array.Empty<WebhookEventType>();
|
||||
public string[] Events { get; set; } = Array.Empty<string>();
|
||||
[Uri]
|
||||
[Required]
|
||||
public string PayloadUrl { get; set; }
|
||||
|
|
|
@ -4,6 +4,6 @@ namespace BTCPayServer.Models.StoreViewModels
|
|||
{
|
||||
public class TestWebhookViewModel
|
||||
{
|
||||
public WebhookEventType Type { get; set; }
|
||||
public string Type { get; set; }
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,8 +16,8 @@ namespace BTCPayServer.PaymentRequest
|
|||
{
|
||||
public class PaymentRequestService
|
||||
{
|
||||
private readonly PaymentRequestRepository _PaymentRequestRepository;
|
||||
private readonly BTCPayNetworkProvider _BtcPayNetworkProvider;
|
||||
private readonly PaymentRequestRepository _paymentRequestRepository;
|
||||
private readonly BTCPayNetworkProvider _btcPayNetworkProvider;
|
||||
private readonly InvoiceRepository _invoiceRepository;
|
||||
private readonly CurrencyNameTable _currencies;
|
||||
private readonly TransactionLinkProviders _transactionLinkProviders;
|
||||
|
@ -31,8 +31,8 @@ namespace BTCPayServer.PaymentRequest
|
|||
CurrencyNameTable currencies,
|
||||
TransactionLinkProviders transactionLinkProviders)
|
||||
{
|
||||
_PaymentRequestRepository = paymentRequestRepository;
|
||||
_BtcPayNetworkProvider = btcPayNetworkProvider;
|
||||
_paymentRequestRepository = paymentRequestRepository;
|
||||
_btcPayNetworkProvider = btcPayNetworkProvider;
|
||||
_invoiceRepository = invoiceRepository;
|
||||
_currencies = currencies;
|
||||
_transactionLinkProviders = transactionLinkProviders;
|
||||
|
@ -41,7 +41,7 @@ namespace BTCPayServer.PaymentRequest
|
|||
|
||||
public async Task UpdatePaymentRequestStateIfNeeded(string id)
|
||||
{
|
||||
var pr = await _PaymentRequestRepository.FindPaymentRequest(id, null);
|
||||
var pr = await _paymentRequestRepository.FindPaymentRequest(id, null);
|
||||
await UpdatePaymentRequestStateIfNeeded(pr);
|
||||
}
|
||||
|
||||
|
@ -61,7 +61,7 @@ namespace BTCPayServer.PaymentRequest
|
|||
|
||||
if (currentStatus != Client.Models.PaymentRequestData.PaymentRequestStatus.Expired)
|
||||
{
|
||||
var invoices = await _PaymentRequestRepository.GetInvoicesForPaymentRequest(pr.Id);
|
||||
var invoices = await _paymentRequestRepository.GetInvoicesForPaymentRequest(pr.Id);
|
||||
var contributions = _invoiceRepository.GetContributionsByPaymentMethodId(blob.Currency, invoices, true);
|
||||
var allSettled = contributions.All(i => i.Value.States.All(s => s.IsSettled()));
|
||||
var isPaid = contributions.TotalCurrency >= blob.Amount;
|
||||
|
@ -81,13 +81,13 @@ namespace BTCPayServer.PaymentRequest
|
|||
if (currentStatus != pr.Status)
|
||||
{
|
||||
pr.Status = currentStatus;
|
||||
await _PaymentRequestRepository.UpdatePaymentRequestStatus(pr.Id, currentStatus);
|
||||
await _paymentRequestRepository.UpdatePaymentRequestStatus(pr.Id, currentStatus);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<ViewPaymentRequestViewModel> GetPaymentRequest(string id, string userId = null)
|
||||
{
|
||||
var pr = await _PaymentRequestRepository.FindPaymentRequest(id, userId);
|
||||
var pr = await _paymentRequestRepository.FindPaymentRequest(id, userId);
|
||||
if (pr == null)
|
||||
{
|
||||
return null;
|
||||
|
@ -95,7 +95,7 @@ namespace BTCPayServer.PaymentRequest
|
|||
|
||||
var blob = pr.GetBlob();
|
||||
|
||||
var invoices = await _PaymentRequestRepository.GetInvoicesForPaymentRequest(id);
|
||||
var invoices = await _paymentRequestRepository.GetInvoicesForPaymentRequest(id);
|
||||
var paymentStats = _invoiceRepository.GetContributionsByPaymentMethodId(blob.Currency, invoices, true);
|
||||
var amountDue = blob.Amount - paymentStats.TotalCurrency;
|
||||
var pendingInvoice = invoices.OrderByDescending(entity => entity.InvoiceTime)
|
||||
|
|
|
@ -41,7 +41,7 @@ public abstract class BaseAutomatedPayoutProcessor<T> : BaseAsyncService where T
|
|||
protected readonly BTCPayNetworkProvider _btcPayNetworkProvider;
|
||||
protected readonly PaymentMethodId PaymentMethodId;
|
||||
private readonly IPluginHookService _pluginHookService;
|
||||
private readonly EventAggregator _eventAggregator;
|
||||
protected readonly EventAggregator _eventAggregator;
|
||||
|
||||
protected BaseAutomatedPayoutProcessor(
|
||||
ILoggerFactory logger,
|
||||
|
@ -115,7 +115,13 @@ public abstract class BaseAutomatedPayoutProcessor<T> : BaseAsyncService where T
|
|||
Logs.PayServer.LogInformation(
|
||||
$"{payouts.Count} found to process. Starting (and after will sleep for {blob.Interval})");
|
||||
await Process(paymentMethod, payouts);
|
||||
|
||||
await context.SaveChangesAsync();
|
||||
|
||||
foreach (var payoutData in payouts.Where(payoutData => payoutData.State != PayoutState.AwaitingPayment))
|
||||
{
|
||||
_eventAggregator.Publish(new PayoutEvent(null, payoutData));
|
||||
}
|
||||
}
|
||||
|
||||
// Allow plugins do to something after automatic payout processing
|
||||
|
|
|
@ -27,7 +27,6 @@ namespace BTCPayServer.PayoutProcessors.OnChain
|
|||
private readonly BTCPayWalletProvider _btcPayWalletProvider;
|
||||
private readonly BTCPayNetworkJsonSerializerSettings _btcPayNetworkJsonSerializerSettings;
|
||||
private readonly BitcoinLikePayoutHandler _bitcoinLikePayoutHandler;
|
||||
private readonly EventAggregator _eventAggregator;
|
||||
|
||||
public OnChainAutomatedPayoutProcessor(
|
||||
ApplicationDbContextFactory applicationDbContextFactory,
|
||||
|
@ -51,7 +50,6 @@ namespace BTCPayServer.PayoutProcessors.OnChain
|
|||
_btcPayWalletProvider = btcPayWalletProvider;
|
||||
_btcPayNetworkJsonSerializerSettings = btcPayNetworkJsonSerializerSettings;
|
||||
_bitcoinLikePayoutHandler = bitcoinLikePayoutHandler;
|
||||
_eventAggregator = eventAggregator;
|
||||
WalletRepository = walletRepository;
|
||||
FeeProvider = feeProviderFactory.CreateFeeProvider(_btcPayNetworkProvider.GetNetwork(PaymentMethodId.CryptoCode));
|
||||
}
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
using System.Threading.Tasks;
|
||||
using MimeKit;
|
||||
|
||||
namespace BTCPayServer.Services.Mails
|
||||
|
@ -6,5 +7,6 @@ namespace BTCPayServer.Services.Mails
|
|||
{
|
||||
void SendEmail(MailboxAddress email, string subject, string message);
|
||||
void SendEmail(MailboxAddress[] email, MailboxAddress[] cc, MailboxAddress[] bcc, string subject, string message);
|
||||
Task<EmailSettings> GetEmailSettings();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,24 +9,41 @@ using Microsoft.EntityFrameworkCore;
|
|||
|
||||
namespace BTCPayServer.Services.PaymentRequests
|
||||
{
|
||||
public record PaymentRequestEvent
|
||||
{
|
||||
public const string Created = nameof(Created);
|
||||
public const string Updated = nameof(Updated);
|
||||
public const string Archived = nameof(Archived);
|
||||
public const string StatusChanged = nameof(StatusChanged);
|
||||
public PaymentRequestData Data { get; set; }
|
||||
public string Type { get; set; }
|
||||
|
||||
|
||||
}
|
||||
|
||||
public class PaymentRequestRepository
|
||||
{
|
||||
private readonly ApplicationDbContextFactory _ContextFactory;
|
||||
private readonly InvoiceRepository _InvoiceRepository;
|
||||
private readonly EventAggregator _eventAggregator;
|
||||
|
||||
public PaymentRequestRepository(ApplicationDbContextFactory contextFactory, InvoiceRepository invoiceRepository)
|
||||
public PaymentRequestRepository(ApplicationDbContextFactory contextFactory,
|
||||
InvoiceRepository invoiceRepository, EventAggregator eventAggregator)
|
||||
{
|
||||
_ContextFactory = contextFactory;
|
||||
_InvoiceRepository = invoiceRepository;
|
||||
_eventAggregator = eventAggregator;
|
||||
}
|
||||
|
||||
public async Task<PaymentRequestData> CreateOrUpdatePaymentRequest(PaymentRequestData entity)
|
||||
{
|
||||
await using var context = _ContextFactory.CreateContext();
|
||||
var added = false;
|
||||
if (string.IsNullOrEmpty(entity.Id))
|
||||
{
|
||||
entity.Id = Guid.NewGuid().ToString();
|
||||
await context.PaymentRequests.AddAsync(entity);
|
||||
added = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
|
@ -34,9 +51,37 @@ namespace BTCPayServer.Services.PaymentRequests
|
|||
}
|
||||
|
||||
await context.SaveChangesAsync();
|
||||
_eventAggregator.Publish(new PaymentRequestEvent()
|
||||
{
|
||||
Data = entity,
|
||||
Type = added ? PaymentRequestEvent.Created : PaymentRequestEvent.Updated
|
||||
});
|
||||
return entity;
|
||||
}
|
||||
|
||||
public async Task<bool?> ArchivePaymentRequest(string id, bool toggle = false)
|
||||
{
|
||||
|
||||
await using var context = _ContextFactory.CreateContext();
|
||||
var pr = await context.PaymentRequests.FindAsync(id);
|
||||
if(pr == null)
|
||||
return null;
|
||||
if(pr.Archived && !toggle)
|
||||
return pr.Archived;
|
||||
pr.Archived = !pr.Archived;
|
||||
await context.SaveChangesAsync();
|
||||
if (pr.Archived)
|
||||
{
|
||||
_eventAggregator.Publish(new PaymentRequestEvent()
|
||||
{
|
||||
Data = pr,
|
||||
Type = PaymentRequestEvent.Archived
|
||||
});
|
||||
}
|
||||
|
||||
return pr.Archived;
|
||||
}
|
||||
|
||||
public async Task<PaymentRequestData> FindPaymentRequest(string id, string userId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (string.IsNullOrEmpty(id))
|
||||
|
@ -44,7 +89,7 @@ namespace BTCPayServer.Services.PaymentRequests
|
|||
return null;
|
||||
}
|
||||
|
||||
using var context = _ContextFactory.CreateContext();
|
||||
await using var context = _ContextFactory.CreateContext();
|
||||
var result = await context.PaymentRequests.Include(x => x.StoreData)
|
||||
.Where(data =>
|
||||
string.IsNullOrEmpty(userId) ||
|
||||
|
@ -53,27 +98,23 @@ namespace BTCPayServer.Services.PaymentRequests
|
|||
return result;
|
||||
}
|
||||
|
||||
public async Task<bool> IsPaymentRequestAdmin(string paymentRequestId, string userId)
|
||||
{
|
||||
if (string.IsNullOrEmpty(userId) || string.IsNullOrEmpty(paymentRequestId))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
using var context = _ContextFactory.CreateContext();
|
||||
return await context.PaymentRequests.Include(x => x.StoreData)
|
||||
.AnyAsync(data =>
|
||||
data.Id == paymentRequestId &&
|
||||
(data.StoreData != null && data.StoreData.UserStores.Any(u => u.ApplicationUserId == userId)));
|
||||
}
|
||||
|
||||
public async Task UpdatePaymentRequestStatus(string paymentRequestId, Client.Models.PaymentRequestData.PaymentRequestStatus status, CancellationToken cancellationToken = default)
|
||||
{
|
||||
using var context = _ContextFactory.CreateContext();
|
||||
var invoiceData = await context.FindAsync<PaymentRequestData>(paymentRequestId);
|
||||
if (invoiceData == null)
|
||||
await using var context = _ContextFactory.CreateContext();
|
||||
var paymentRequestData = await context.FindAsync<PaymentRequestData>(paymentRequestId);
|
||||
if (paymentRequestData == null)
|
||||
return;
|
||||
invoiceData.Status = status;
|
||||
if( paymentRequestData.Status == status)
|
||||
return;
|
||||
paymentRequestData.Status = status;
|
||||
|
||||
await context.SaveChangesAsync(cancellationToken);
|
||||
|
||||
_eventAggregator.Publish(new PaymentRequestEvent()
|
||||
{
|
||||
Data = paymentRequestData,
|
||||
Type = PaymentRequestEvent.StatusChanged
|
||||
});
|
||||
}
|
||||
|
||||
public async Task<PaymentRequestData[]> FindPaymentRequests(PaymentRequestQuery query, CancellationToken cancellationToken = default)
|
||||
|
|
|
@ -24,7 +24,7 @@ namespace BTCPayServer.Services.Stores
|
|||
|
||||
public JsonSerializerSettings SerializerSettings { get; }
|
||||
|
||||
public ApplicationDbContext CreateDbContext()
|
||||
protected ApplicationDbContext CreateDbContext()
|
||||
{
|
||||
return _ContextFactory.CreateContext();
|
||||
}
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
@using BTCPayServer.Abstractions.Extensions
|
||||
@{
|
||||
var parsedModel = TempData.GetStatusMessageModel();
|
||||
}
|
||||
|
|
|
@ -7,7 +7,6 @@
|
|||
@inject FormDataService FormDataService
|
||||
@model BTCPayServer.Models.PaymentRequestViewModels.UpdatePaymentRequestViewModel
|
||||
@{
|
||||
|
||||
var checkoutFormOptions = await FormDataService.GetSelect(Model.StoreId, Model.FormId);
|
||||
|
||||
ViewData.SetActivePage(PaymentRequestsNavPages.Create, $"{(string.IsNullOrEmpty(Model.Id) ? "Create" : "Edit")} Payment Request", Model.Id);
|
||||
|
@ -85,7 +84,14 @@
|
|||
<label asp-for="Email" class="form-label"></label>
|
||||
<input type="email" asp-for="Email" placeholder="Firstname Lastname <email@example.com>" class="form-control" />
|
||||
<span asp-validation-for="Email" class="text-danger"></span>
|
||||
<div id="PaymentRequestEmailHelpBlock" class="form-text">The recipient's email. This will send notification mails to the recipient, as configured by the <a asp-action="StoreEmails" asp-controller="UIStores" asp-route-storeId="@Model.StoreId">email rules</a>, and include the email in the invoice export data.</div>
|
||||
<div id="PaymentRequestEmailHelpBlock" class="form-text">
|
||||
This will send notification mails to the recipient, as configured by the
|
||||
<a asp-action="StoreEmails" asp-controller="UIStores" asp-route-storeId="@Model.StoreId">email rules</a>.
|
||||
@if (Model.HasEmailRules is not true)
|
||||
{
|
||||
<div class="text-warning">No payment request related email rules have been configured for this store.</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label asp-for="FormId" class="form-label"></label>
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
@model EditWebhookViewModel
|
||||
@using BTCPayServer.Client.Models;
|
||||
@using BTCPayServer.HostedServices.Webhooks
|
||||
@inject WebhookSender WebhookSender
|
||||
@{
|
||||
Layout = "../Shared/_NavLayout.cshtml";
|
||||
ViewData.SetActivePage(StoreNavPages.Webhooks, "Webhook Settings", Context.GetStoreData().Id);
|
||||
|
@ -55,20 +57,11 @@
|
|||
</select>
|
||||
<div id="event-selector" class="collapse">
|
||||
<div class="pb-3">
|
||||
@foreach (var evt in new[]
|
||||
{
|
||||
("A new invoice has been created", WebhookEventType.InvoiceCreated),
|
||||
("A new payment has been received", WebhookEventType.InvoiceReceivedPayment),
|
||||
("A payment has been settled", WebhookEventType.InvoicePaymentSettled),
|
||||
("An invoice is processing", WebhookEventType.InvoiceProcessing),
|
||||
("An invoice has expired", WebhookEventType.InvoiceExpired),
|
||||
("An invoice has been settled", WebhookEventType.InvoiceSettled),
|
||||
("An invoice became invalid", WebhookEventType.InvoiceInvalid)
|
||||
})
|
||||
@foreach (var evt in WebhookSender.GetSupportedWebhookTypes())
|
||||
{
|
||||
<div class="form-check my-1">
|
||||
<input name="Events" id="@evt.Item2" value="@evt.Item2" @(Model.Events.Contains(evt.Item2) ? "checked" : "") type="checkbox" class="form-check-input" />
|
||||
<label for="@evt.Item2" class="form-check-label">@evt.Item1</label>
|
||||
<input name="Events" id="@evt.Key" value="@evt.Key" @(Model.Events.Contains(evt.Key) ? "checked" : "") type="checkbox" class="form-check-input" />
|
||||
<label for="@evt.Key" class="form-check-label">@evt.Value</label>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
@using BTCPayServer.Client.Models
|
||||
@using BTCPayServer.HostedServices.Webhooks
|
||||
@model BTCPayServer.Controllers.UIStoresController.StoreEmailRuleViewModel
|
||||
@inject WebhookSender WebhookSender
|
||||
|
||||
@{
|
||||
Layout = "../Shared/_NavLayout.cshtml";
|
||||
|
@ -54,7 +55,7 @@
|
|||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<select asp-for="Rules[index].Trigger" asp-items="@Html.GetEnumSelectList<WebhookEventType>()" class="form-select email-rule-trigger" required></select>
|
||||
<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">Choose what event sends the email.</div>
|
||||
</div>
|
||||
|
@ -78,17 +79,47 @@
|
|||
<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 d-flex gap-2">
|
||||
<div>Placeholders:</div>
|
||||
<div>
|
||||
<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>
|
||||
</div>
|
||||
<div class="form-text rounded bg-light p-2">
|
||||
<table class="table table-sm caption-top m-0">
|
||||
<caption class="text-muted p-0">Placeholders</caption>
|
||||
<tr>
|
||||
<th>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>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>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>
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
@using BTCPayServer.HostedServices.Webhooks
|
||||
@model EditWebhookViewModel
|
||||
@using BTCPayServer.Client.Models;
|
||||
@inject WebhookSender WebhookSender
|
||||
@{
|
||||
Layout = "../Shared/_NavLayout.cshtml";
|
||||
ViewData.SetActivePage(StoreNavPages.Webhooks, "Send a test event to a webhook endpoint", Context.GetStoreData().Id);
|
||||
|
@ -12,7 +13,7 @@
|
|||
<div class="form-group">
|
||||
<label for="Type" class="form-label">Event type</label>
|
||||
<select
|
||||
asp-items="Html.GetEnumSelectList<WebhookEventType>()"
|
||||
asp-items="@WebhookSender.GetSupportedWebhookTypes().Select(s => new SelectListItem(s.Value,s.Key))"
|
||||
name="Type"
|
||||
id="Type"
|
||||
class="form-select w-auto"
|
||||
|
|
Loading…
Add table
Reference in a new issue