Add btcpay rate provider, improve order notes, add btc as currency

This commit is contained in:
Kukks 2023-11-09 10:09:37 +01:00
parent ca62b7ed73
commit 44f4aea5f6
No known key found for this signature in database
GPG Key ID: 8E5530D9D1C93097
5 changed files with 131 additions and 76 deletions

View File

@ -31,7 +31,6 @@ namespace Smartstore.BTCPayServer.Controllers
public BtcPayAdminController(
BtcPayService btcPayService,
ILogger<BtcPayAdminController> logger,
LinkGenerator linkGenerator,
ICommonServices services,
IProviderManager providerManager,
@ -92,8 +91,12 @@ namespace Smartstore.BTCPayServer.Controllers
var uri = BTCPayServerClient.GenerateAuthorizeUri(btcpayUri,
new[]
{
Policies.CanCreateInvoice, Policies.CanModifyStoreWebhooks, Policies.CanViewStoreSettings,
Policies.CanCreateNonApprovedPullPayments
Policies.CanCreateInvoice, // create invoices for payment
Policies.CanViewInvoices, // fetch created invoices to check status
Policies.CanModifyStoreSettings, // able to mark an invoice invalid in case merchant wants to void the order
Policies.CanModifyStoreWebhooks, // able to create the webhook required automatically
Policies.CanViewStoreSettings, // able to fetch rates
Policies.CanCreateNonApprovedPullPayments // able to create refunds
},
true, true, ($"SmartStore{myStore.Id}", adminUrl));
return uri + $"&applicationName={HttpUtility.UrlEncode(myStore.Name)}";

View File

@ -8,6 +8,7 @@ using Newtonsoft.Json;
using Smartstore.BTCPayServer.Configuration;
using Smartstore.BTCPayServer.Providers;
using Smartstore.BTCPayServer.Services;
using Smartstore.Core;
using Smartstore.Core.Data;
using Smartstore.Web.Controllers;
@ -18,16 +19,16 @@ namespace Smartstore.BTCPayServer.Controllers
{
private readonly BtcPayService _btcPayService;
private readonly SmartDbContext _db;
private readonly BtcPaySettings _settings;
private readonly ICommonServices _services;
public BtcPayHookController(
BtcPaySettings settings,
SmartDbContext db,
ICommonServices services,
BtcPayService btcPayService)
{
_btcPayService = btcPayService;
_db = db;
_settings = settings;
_services = services;
}
@ -50,26 +51,24 @@ namespace Smartstore.BTCPayServer.Controllers
Logger.Error("Missing fields in request");
return StatusCode(StatusCodes.Status422UnprocessableEntity);
}
if (_settings.WebHookSecret is not null && !BtcPayService.CheckSecretKey(_settings.WebHookSecret, jsonStr, signature))
{
Logger.Error("Bad secret key");
return StatusCode(StatusCodes.Status400BadRequest);
}
var invoice = await _btcPayService.GetInvoice(_settings, webhookEvent.InvoiceId);
if( invoice is null)
{
Logger.Error("Invoice not found");
return StatusCode(StatusCodes.Status422UnprocessableEntity);
}
var order = await _db.Orders.FirstOrDefaultAsync(x =>
x.PaymentMethodSystemName == BTCPayPaymentProvider.SystemName &&
x.OrderGuid == new Guid(orderId));
if (order == null)
if (order is null)
{
Logger.Error("Order not found");
return StatusCode(StatusCodes.Status422UnprocessableEntity);
}
var settings = await _services.SettingFactory.LoadSettingsAsync<BtcPaySettings>(order.StoreId);
if (settings.WebHookSecret is not null && !BtcPayService.CheckSecretKey(settings.WebHookSecret, jsonStr, signature))
{
Logger.Error("Bad secret key");
return StatusCode(StatusCodes.Status400BadRequest);
}
var invoice = await _btcPayService.GetInvoice(settings, webhookEvent.InvoiceId);
if (await _btcPayService.UpdateOrderWithInvoice(order, invoice, webhookEvent))
{

View File

@ -1,6 +1,8 @@
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Mvc;
using Smartstore.BTCPayServer.Configuration;
using Smartstore.Core.Common;
using Smartstore.Core.Data;
using Smartstore.Engine.Modularity;
using Smartstore.Http;
@ -8,19 +10,48 @@ namespace Smartstore.BTCPayServer
{
internal class Module : ModuleBase, IConfigurable
{
private readonly SmartDbContext _smartDbContext;
public Module(SmartDbContext smartDbContext)
{
_smartDbContext = smartDbContext;
}
public override async Task InstallAsync(ModuleInstallationContext context)
{
await SaveSettingsAsync(new BtcPaySettings
{
BtcPayUrl = "",
ApiKey = "",
BtcPayStoreID = "",
WebHookSecret = ""
BtcPayUrl = "", ApiKey = "", BtcPayStoreID = "", WebHookSecret = ""
});
await ImportLanguageResourcesAsync();
await AddBTCCurrency(_smartDbContext);
await base.InstallAsync(context);
}
private async Task AddBTCCurrency(SmartDbContext smartDbContext)
{
try
{
await smartDbContext.Currencies.AddAsync(new Currency
{
DisplayLocale = "en-US",
Name = "Bitcoin",
CurrencyCode = "BTC",
CustomFormatting = "{0} ₿",
Published = true,
RoundNumDecimals = 8,
DisplayOrder = 1,
});
await smartDbContext.SaveChangesAsync();
}
catch (Exception e)
{
// ignored
}
}
public override async Task UninstallAsync()
{
await DeleteSettingsAsync<BtcPaySettings>();
@ -33,6 +64,6 @@ namespace Smartstore.BTCPayServer
public RouteInfo GetConfigurationRoute()
=> new("Configure", "BtcPayAdmin", new { area = "Admin" });
=> new("Configure", "BtcPayAdmin", new {area = "Admin"});
}
}
}

View File

@ -13,6 +13,7 @@ using Smartstore.Core;
using Smartstore.Core.Checkout.Cart;
using Smartstore.Core.Checkout.Orders;
using Smartstore.Core.Checkout.Payment;
using Smartstore.Core.Common;
using Smartstore.Core.Common.Services;
using Smartstore.Core.Configuration;
using Smartstore.Core.Data;
@ -27,7 +28,7 @@ namespace Smartstore.BTCPayServer.Providers
[SystemName("Smartstore.BTCPayServer")]
[FriendlyName("BTCPayServer")]
[Order(1)]
public class BTCPayPaymentProvider : PaymentMethodBase, IConfigurable
public class BTCPayPaymentProvider : PaymentMethodBase, IConfigurable, IExchangeRateProvider
{
// https://smartstore.atlassian.net/wiki/spaces/SMNET40/pages/1927643267/How+to+write+a+Payment+Plugin
@ -279,5 +280,22 @@ namespace Smartstore.BTCPayServer.Providers
return new VoidPaymentResult() {NewPaymentStatus = request.Order.PaymentStatus};
}
}
public async Task<IList<ExchangeRate>> GetCurrencyLiveRatesAsync(string exchangeRateCurrencyCode)
{
var updateDate = DateTime.UtcNow;
var currencies = await _db.Currencies.ToListAsync();
var myStore = _services.StoreContext.CurrentStore;
var settings = _settingFactory.LoadSettings<BtcPaySettings>(myStore.Id);
var client = _btcPayService.GetClient(settings);
var pairs = currencies.Select(currency => exchangeRateCurrencyCode + "_" + currency.CurrencyCode).ToArray();
var rates = await client.GetStoreRates(settings.BtcPayStoreID,pairs);
return rates.Where(result => result.Rate is not null).Select(result => new ExchangeRate()
{
CurrencyCode = result.CurrencyPair.Split("_")[1],
Rate = result.Rate.Value,
UpdatedOn = updateDate
}).ToList();
}
}
}

View File

@ -11,13 +11,11 @@ using Smartstore.Core.Checkout.Payment;
namespace Smartstore.BTCPayServer.Services
{
public class BtcPayService
{
private readonly IHttpClientFactory _httpClientFactory;
public BtcPayService(IHttpClientFactory httpClientFactory )
public BtcPayService(IHttpClientFactory httpClientFactory)
{
_httpClientFactory = httpClientFactory;
}
@ -25,12 +23,13 @@ namespace Smartstore.BTCPayServer.Services
public BTCPayServerClient GetClient(BtcPaySettings settings)
{
return new BTCPayServerClient(new Uri(settings.BtcPayUrl), settings.ApiKey,_httpClientFactory.CreateClient("BTCPayServer"));
return new BTCPayServerClient(new Uri(settings.BtcPayUrl), settings.ApiKey,
_httpClientFactory.CreateClient("BTCPayServer"));
}
public async Task<string> GetStoreId(BtcPaySettings settings)
{
return (await GetClient(settings).GetStores()).First().Id;
return (await GetClient(settings).GetStores()).First().Id;
}
public static bool CheckSecretKey(string key, string message, string signature)
@ -42,7 +41,6 @@ namespace Smartstore.BTCPayServer.Services
public async Task<InvoiceData> CreateInvoice(BtcPaySettings settings, PaymentDataModel paymentData)
{
var client = GetClient(settings);
var req = new CreateInvoiceRequest()
{
@ -55,7 +53,7 @@ namespace Smartstore.BTCPayServer.Services
RedirectAutomatically = true,
RequiresRefundEmail = false
},
Metadata = JObject.FromObject(new
Metadata = JObject.FromObject(new
{
buyerEmail = paymentData.BuyerEmail,
buyerName = paymentData.BuyerName,
@ -63,19 +61,15 @@ namespace Smartstore.BTCPayServer.Services
orderUrl = paymentData.OrderUrl,
itemDesc = paymentData.Description,
}),
Receipt = new InvoiceDataBase.ReceiptOptions()
{
Enabled = true,
}
Receipt = new InvoiceDataBase.ReceiptOptions() {Enabled = true,}
};
var invoice = await client.CreateInvoice(settings.BtcPayStoreID, req);
return invoice;
}
public async Task<string> CreateRefund(BtcPaySettings settings, RefundPaymentRequest refundRequest)
{
var client = GetClient(settings);
var invoice = await client.GetInvoicePaymentMethods(settings.BtcPayStoreID,
refundRequest.Order.AuthorizationTransactionId);
@ -103,7 +97,6 @@ namespace Smartstore.BTCPayServer.Services
refundRequest.Order.AuthorizationTransactionId, refundInvoiceRequest);
return refund.ViewLink;
}
public async Task<string> CreateWebHook(BtcPaySettings settings, string webHookUrl)
@ -116,36 +109,34 @@ namespace Smartstore.BTCPayServer.Services
await client.DeleteWebhook(settings.BtcPayStoreID, webhookData.Id);
}
var response = await client.CreateWebhook(settings.BtcPayStoreID, new CreateStoreWebhookRequest()
{
Url = webHookUrl,
Enabled = true,
AuthorizedEvents = new StoreWebhookBaseData.AuthorizedEventsData()
var response = await client.CreateWebhook(settings.BtcPayStoreID,
new CreateStoreWebhookRequest()
{
SpecificEvents = new[]
Url = webHookUrl,
Enabled = true,
AuthorizedEvents = new StoreWebhookBaseData.AuthorizedEventsData()
{
WebhookEventType.InvoiceReceivedPayment, WebhookEventType.InvoiceProcessing,
WebhookEventType.InvoiceExpired, WebhookEventType.InvoiceSettled,
WebhookEventType.InvoiceInvalid, WebhookEventType.InvoicePaymentSettled,
SpecificEvents = new[]
{
WebhookEventType.InvoiceReceivedPayment, WebhookEventType.InvoiceProcessing,
WebhookEventType.InvoiceExpired, WebhookEventType.InvoiceSettled,
WebhookEventType.InvoiceInvalid, WebhookEventType.InvoicePaymentSettled,
}
}
}
});
});
return response.Secret;
}
public async Task<InvoiceData> GetInvoice(BtcPaySettings settings, string invoiceId)
{
var client = GetClient(settings);
return await client.GetInvoice(settings.BtcPayStoreID, invoiceId);
}
public async Task<bool> UpdateOrderWithInvoice(BtcPaySettings settings ,Order order, string invoiceId)
public async Task<bool> UpdateOrderWithInvoice(BtcPaySettings settings, Order order, string invoiceId)
{
try
{
var invoice = await GetInvoice(settings, invoiceId);
return await UpdateOrderWithInvoice(order, invoice, null);
}
@ -158,7 +149,7 @@ namespace Smartstore.BTCPayServer.Services
return true;
}
}
public async Task<bool> UpdateOrderWithInvoice(Order order, InvoiceData invoiceData,
WebhookInvoiceEvent? webhookEvent)
{
@ -175,7 +166,7 @@ namespace Smartstore.BTCPayServer.Services
newPaymentStatus = PaymentStatus.Pending;
break;
case InvoiceStatus.Processing:
newPaymentStatus =PaymentStatus.Pending; // PaymentStatus.Authorized; smartstore will set the order to processing otherwise
newPaymentStatus = PaymentStatus.Authorized;
newOrderStatus = OrderStatus.Pending;
break;
case InvoiceStatus.Expired:
@ -208,40 +199,53 @@ namespace Smartstore.BTCPayServer.Services
updated = true;
}
var additionalMessage = GetAdditionalMessageFromWebhook(webhookEvent);
if (updated)
{
var aditionalData = GetAdditionalMessageFromWebhook(webhookEvent)?.message;
aditionalData = string.IsNullOrEmpty(aditionalData) ? "" : $" - {aditionalData}";
additionalMessage = string.IsNullOrEmpty(additionalMessage) ? "" : $" - {additionalMessage}";
order.AddOrderNote(
$"BTCPayServer: Order status updated to {newOrderStatus} and payment status to {newPaymentStatus} by BTCPay with invoice {invoiceData.Id}{aditionalData}",
$"BTCPayServer: Order status updated to {newOrderStatus} and payment status to {newPaymentStatus} by BTCPay with invoice {invoiceData.Id}{additionalMessage}",
false);
order.HasNewPaymentNotification = true;
if (order.PaymentStatus == PaymentStatus.Paid && !string.IsNullOrEmpty(order.CaptureTransactionResult))
if (order.PaymentStatus is PaymentStatus.Authorized or PaymentStatus.Paid &&
!string.IsNullOrEmpty(order.CaptureTransactionResult))
order.AddOrderNote(
$"BTCPayServer: Payment received. <a href='{order.CaptureTransactionResult}'>Click here for more information.</a>", true);
$"BTCPayServer: Payment received {(order.PaymentStatus is PaymentStatus.Authorized ? $"but waiting to confirm. <a href='{order.AuthorizationTransactionResult}'>Click here for more information.</a>" : $". <a href='{order.CaptureTransactionResult}'>Click here for more information.</a>")}", true);
return true;
}
if (!string.IsNullOrEmpty(additionalMessage))
{
order.AddOrderNote(
$"BTCPayServer: {additionalMessage}", false);
}
return false;
}
private (string message, bool customerFriendly)? GetAdditionalMessageFromWebhook(WebhookInvoiceEvent? webhookEvent)
private string? GetAdditionalMessageFromWebhook(WebhookInvoiceEvent? webhookEvent)
{
switch (webhookEvent?.Type)
{
case WebhookEventType.InvoiceReceivedPayment when webhookEvent.ReadAs<WebhookInvoiceReceivedPaymentEvent>() is { } receivedPaymentEvent:
return ($"Payment detected ({receivedPaymentEvent.PaymentMethod}: {receivedPaymentEvent.Payment.Value})" , false);
case WebhookEventType.InvoicePaymentSettled when webhookEvent.ReadAs<WebhookInvoicePaymentSettledEvent>() is { } receivedPaymentEvent:
return ($"Payment settled ({receivedPaymentEvent.PaymentMethod}: {receivedPaymentEvent.Payment.Value})" , false);
case WebhookEventType.InvoiceProcessing when webhookEvent.ReadAs<WebhookInvoiceProcessingEvent>() is { } receivedPaymentEvent && receivedPaymentEvent.OverPaid:
return ($"Invoice was overpaid." , false);
case WebhookEventType.InvoiceExpired when webhookEvent.ReadAs<WebhookInvoiceExpiredEvent>() is { } receivedPaymentEvent && receivedPaymentEvent.PartiallyPaid:
return ($"Invoice expired but was paid partially, please check." , false);
default: return null;
}
switch (webhookEvent?.Type)
{
case WebhookEventType.InvoiceReceivedPayment
when webhookEvent.ReadAs<WebhookInvoiceReceivedPaymentEvent>() is { } receivedPaymentEvent:
return
$"Payment detected ({receivedPaymentEvent.PaymentMethod}: {receivedPaymentEvent.Payment.Value})";
case WebhookEventType.InvoicePaymentSettled
when webhookEvent.ReadAs<WebhookInvoicePaymentSettledEvent>() is { } receivedPaymentEvent:
return
$"Payment settled ({receivedPaymentEvent.PaymentMethod}: {receivedPaymentEvent.Payment.Value})";
case WebhookEventType.InvoiceProcessing
when webhookEvent.ReadAs<WebhookInvoiceProcessingEvent>() is { } receivedPaymentEvent &&
receivedPaymentEvent.OverPaid:
return $"Invoice was overpaid.";
case WebhookEventType.InvoiceExpired
when webhookEvent.ReadAs<WebhookInvoiceExpiredEvent>() is { } receivedPaymentEvent &&
receivedPaymentEvent.PartiallyPaid:
return $"Invoice expired but was paid partially.";
default: return null;
}
}
}
}