Add btcpay rate provider, improve order notes, add btc as currency
This commit is contained in:
parent
ca62b7ed73
commit
44f4aea5f6
@ -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)}";
|
||||
|
||||
@ -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))
|
||||
{
|
||||
|
||||
@ -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"});
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user