nopcommerce/Services/BtcPayService.cs
2025-02-18 00:09:38 +00:00

287 lines
12 KiB
C#

using System;
using System.Linq;
using System.Security.Cryptography;
using System.Text;
using System.Threading.Tasks;
using BTCPayServer.Client;
using BTCPayServer.Client.Models;
using Newtonsoft.Json.Linq;
using Nop.Core.Domain.Orders;
using Nop.Core.Domain.Payments;
using Nop.Plugin.Payments.BTCPayServer.Models;
using Nop.Services.Orders;
using Nop.Services.Payments;
using IHttpClientFactory = System.Net.Http.IHttpClientFactory;
namespace Nop.Plugin.Payments.BTCPayServer.Services
{
public class BtcPayService
{
private readonly IHttpClientFactory _httpClientFactory;
private readonly IOrderService _orderService;
public BtcPayService(IOrderService orderService, IHttpClientFactory httpClientFactory)
{
_httpClientFactory = httpClientFactory;
_orderService = orderService;
}
public static bool CheckSecretKey(string key, string message, string signature)
{
var msgBytes = HMACSHA256.HashData(Encoding.UTF8.GetBytes(key), Encoding.UTF8.GetBytes(message));
string hashString = string.Empty;
foreach (byte x in msgBytes)
{
hashString += String.Format("{0:x2}", x);
}
return (hashString == signature);
}
public async Task<InvoiceData> CreateInvoice(BtcPaySettings settings, PaymentDataModel paymentData)
{
var client = GetClient(settings);
var req = new CreateInvoiceRequest()
{
Currency = paymentData.CurrencyCode,
Amount = paymentData.Amount,
Checkout = new InvoiceDataBase.CheckoutOptions()
{
DefaultLanguage = paymentData.Lang,
RedirectURL = paymentData.RedirectionURL,
RedirectAutomatically = true
},
Metadata = JObject.FromObject(new
{
buyerEmail = paymentData.BuyerEmail,
buyerName = paymentData.BuyerName,
orderId = paymentData.OrderID,
orderUrl = paymentData.OrderUrl,
itemDesc = paymentData.Description,
}),
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);
var pm = (invoice.FirstOrDefault(p => p.Payments.Any()) ?? invoice.First()).PaymentMethodId;
var refundInvoiceRequest = new RefundInvoiceRequest()
{
Name = "Refund order " + refundRequest.Order.OrderGuid,
PayoutMethodId = pm,
};
if (refundRequest.IsPartialRefund)
{
refundInvoiceRequest.Description = "Partial refund";
refundInvoiceRequest.RefundVariant = RefundVariant.Custom;
refundInvoiceRequest.CustomAmount = refundRequest.AmountToRefund;
refundInvoiceRequest.CustomCurrency = refundRequest.Order.CustomerCurrencyCode;
}
else
{
refundInvoiceRequest.Description = "Full";
refundInvoiceRequest.PayoutMethodId = "BTC";
refundInvoiceRequest.RefundVariant = RefundVariant.Fiat;
}
var refund = await client.RefundInvoice(settings.BtcPayStoreID,
refundRequest.Order.AuthorizationTransactionId, refundInvoiceRequest);
return refund.ViewLink;
}
public async Task<bool> UpdateOrderWithInvoice(BtcPaySettings settings, Order order, string invoiceId)
{
try
{
var invoice = await GetInvoice(settings, invoiceId);
return await UpdateOrderWithInvoice(order, invoice, null);
}
catch (Exception e)
{
order.PaymentStatus = PaymentStatus.Voided;
await _orderService.InsertOrderNoteAsync(new OrderNote
{
OrderId = order.Id,
Note = $"BTCPayServer: Error updating order status with invoice {invoiceId} - {e.Message}",
DisplayToCustomer = false,
CreatedOnUtc = DateTime.UtcNow
});
return true;
}
}
public async Task<bool> UpdateOrderWithInvoice(Order order, InvoiceData invoiceData,
WebhookInvoiceEvent? webhookEvent)
{
if (order.PaymentMethodSystemName != "Payments.BTCPayServer")
return false;
var newPaymentStatus = order.PaymentStatus;
var newOrderStatus = order.OrderStatus;
order.AuthorizationTransactionId = invoiceData.Id;
switch (invoiceData.Status)
{
case InvoiceStatus.New:
newPaymentStatus = PaymentStatus.Pending;
break;
case InvoiceStatus.Processing:
newPaymentStatus = PaymentStatus.Authorized;
newOrderStatus = OrderStatus.Pending;
break;
case InvoiceStatus.Expired:
newOrderStatus = OrderStatus.Cancelled;
newPaymentStatus = PaymentStatus.Voided;
break;
case InvoiceStatus.Invalid:
newPaymentStatus = PaymentStatus.Voided;
newOrderStatus = OrderStatus.Cancelled;
break;
case InvoiceStatus.Settled:
newPaymentStatus = PaymentStatus.Paid;
newOrderStatus = OrderStatus.Processing;
break;
default:
throw new ArgumentOutOfRangeException();
}
var updated = false;
if (newPaymentStatus != order.PaymentStatus)
{
order.PaymentStatus = newPaymentStatus;
updated = true;
}
if (newOrderStatus != order.OrderStatus && order.OrderStatus != OrderStatus.Complete)
{
order.OrderStatus = newOrderStatus;
updated = true;
}
var additionalMessage = GetAdditionalMessageFromWebhook(webhookEvent);
if (updated)
{
additionalMessage = string.IsNullOrEmpty(additionalMessage) ? "" : $" - {additionalMessage}";
await _orderService.InsertOrderNoteAsync(new OrderNote
{
OrderId = order.Id,
Note = $"BTCPayServer: Order status updated to {newOrderStatus} and payment status to {newPaymentStatus} by BTCPay with invoice {invoiceData.Id}{additionalMessage}",
DisplayToCustomer = false,
CreatedOnUtc = DateTime.UtcNow
});
if (order.PaymentStatus is PaymentStatus.Authorized or PaymentStatus.Paid &&
!string.IsNullOrEmpty(order.CaptureTransactionResult))
await _orderService.InsertOrderNoteAsync(new OrderNote
{
OrderId = order.Id,
Note = $"BTCPayServer: Payment {(order.PaymentStatus is PaymentStatus.Authorized ? $"received but waiting to confirm. Click here for more information: {order.AuthorizationTransactionResult}" : $"settled. Click here for more information: {order.CaptureTransactionResult}")}",
DisplayToCustomer = true,
CreatedOnUtc = DateTime.UtcNow
});
await _orderService.UpdateOrderAsync(order);
return true;
}
if (!string.IsNullOrEmpty(additionalMessage))
{
await _orderService.InsertOrderNoteAsync(new OrderNote
{
OrderId = order.Id,
Note = $"BTCPayServer: {additionalMessage}",
DisplayToCustomer = false,
CreatedOnUtc = DateTime.UtcNow
});
}
return false;
}
public async Task<InvoiceData> GetInvoice(BtcPaySettings settings, string invoiceId)
{
var client = GetClient(settings);
return await client.GetInvoice(settings.BtcPayStoreID, invoiceId);
}
public async Task<string> CreateWebHook(BtcPaySettings settings, string webHookUrl)
{
var client = GetClient(settings);
var existing = await client.GetWebhooks(settings.BtcPayStoreID);
var existingWebHook = existing.Where(x => x.Url == webHookUrl);
foreach (var webhookData in existingWebHook)
{
await client.DeleteWebhook(settings.BtcPayStoreID, webhookData.Id);
}
var response = await client.CreateWebhook(settings.BtcPayStoreID,
new CreateStoreWebhookRequest()
{
Url = webHookUrl,
Enabled = true,
AuthorizedEvents = new StoreWebhookBaseData.AuthorizedEventsData()
{
SpecificEvents = new[]
{
WebhookEventType.InvoiceReceivedPayment, WebhookEventType.InvoiceProcessing,
WebhookEventType.InvoiceExpired, WebhookEventType.InvoiceSettled,
WebhookEventType.InvoiceInvalid, WebhookEventType.InvoicePaymentSettled,
}
}
});
return response.Secret;
}
public BTCPayServerClient GetClient(BtcPaySettings settings)
{
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;
}
private string? GetAdditionalMessageFromWebhook(WebhookInvoiceEvent? webhookEvent)
{
switch (webhookEvent?.Type)
{
case WebhookEventType.InvoiceReceivedPayment
when webhookEvent.ReadAs<WebhookInvoiceReceivedPaymentEvent>() is { } receivedPaymentEvent:
return
$"Payment detected ({receivedPaymentEvent.PaymentMethodId}: {receivedPaymentEvent.Payment.Value})";
case WebhookEventType.InvoicePaymentSettled
when webhookEvent.ReadAs<WebhookInvoicePaymentSettledEvent>() is { } receivedPaymentEvent:
return
$"Payment settled ({receivedPaymentEvent.PaymentMethodId}: {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;
}
}
}
}