grandnode/Services/BtcPayService.cs
2025-02-27 00:07:27 +00:00

306 lines
14 KiB
C#

using BTCPayServer.Client;
using BTCPayServer.Client.Models;
using Grand.Business.Core.Interfaces.Checkout.Orders;
using Grand.Business.Core.Interfaces.Checkout.Payments;
using Grand.Business.Core.Utilities.Checkout;
using Grand.Domain.Orders;
using Grand.Domain.Payments;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json.Linq;
using Payments.BTCPayServer.Models;
using System.Net.Http;
using System.Security.Cryptography;
namespace Payments.BTCPayServer.Services
{
public class BtcPayService
{
private readonly IHttpClientFactory _httpClientFactory;
private readonly IOrderService _orderService;
private readonly IPaymentTransactionService _paymentTransactionService;
private readonly ILogger<BtcPayService> _logger;
public BtcPayService(
IOrderService orderService,
IPaymentTransactionService paymentTransactionService,
ILogger<BtcPayService> logger,
IHttpClientFactory httpClientFactory)
{
_httpClientFactory = httpClientFactory;
_orderService = orderService;
_logger = logger;
_paymentTransactionService = paymentTransactionService;
}
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.PaymentTransaction.AuthorizationTransactionId);
var pm = (invoice.FirstOrDefault(p => p.Payments.Any()) ?? invoice.First()).PaymentMethodId;
var refundInvoiceRequest = new RefundInvoiceRequest() {
Name = "Refund order " + refundRequest.PaymentTransaction.OrderGuid,
PayoutMethodId = pm,
};
if (refundRequest.IsPartialRefund)
{
refundInvoiceRequest.Description = "Partial refund";
refundInvoiceRequest.RefundVariant = RefundVariant.Custom;
refundInvoiceRequest.CustomAmount = (decimal) refundRequest.AmountToRefund;
refundInvoiceRequest.CustomCurrency = refundRequest.PaymentTransaction.CurrencyCode;
}
else
{
refundInvoiceRequest.Description = "Full";
refundInvoiceRequest.PayoutMethodId = "BTC";
refundInvoiceRequest.RefundVariant = RefundVariant.Fiat;
}
var refund = await client.RefundInvoice(settings.BtcPayStoreID,
refundRequest.PaymentTransaction.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.PaymentStatusId = PaymentStatus.Voided;
await _orderService.InsertOrderNote(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.PaymentStatusId;
var newOrderStatus = order.OrderStatusId;
var tagAuthorizationTransactionId = $"AuthorizationTransactionId#{invoiceData.Id}";
if (!order.OrderTagExists(new OrderTag() { Name = tagAuthorizationTransactionId }))
order.OrderTags.Add(tagAuthorizationTransactionId);
switch (invoiceData.Status)
{
case InvoiceStatus.New:
newPaymentStatus = PaymentStatus.Pending;
break;
case InvoiceStatus.Processing:
newPaymentStatus = PaymentStatus.Authorized;
newOrderStatus = (int)OrderStatusSystem.Pending;
break;
case InvoiceStatus.Expired:
newOrderStatus = (int)OrderStatusSystem.Cancelled;
newPaymentStatus = PaymentStatus.Voided;
break;
case InvoiceStatus.Invalid:
newPaymentStatus = PaymentStatus.Voided;
newOrderStatus = (int)OrderStatusSystem.Cancelled;
break;
case InvoiceStatus.Settled:
newPaymentStatus = PaymentStatus.Paid;
newOrderStatus = (int)OrderStatusSystem.Processing;
break;
default:
throw new ArgumentOutOfRangeException();
}
var updated = false;
if (newPaymentStatus != order.PaymentStatusId)
{
order.PaymentStatusId = newPaymentStatus;
updated = true;
}
if (newOrderStatus != order.OrderStatusId && order.OrderStatusId != (int)OrderStatusSystem.Complete)
{
order.OrderStatusId = newOrderStatus;
updated = true;
}
if (newPaymentStatus == PaymentStatus.Paid)
{
try
{
var paymentTransaction = await _paymentTransactionService.GetOrderByGuid(order.OrderGuid);
paymentTransaction.PaidAmount = order.PaidAmount;
paymentTransaction.AuthorizationTransactionId = invoiceData.Id;
paymentTransaction.TransactionStatus = Grand.Domain.Payments.TransactionStatus.Paid;
await _paymentTransactionService.UpdatePaymentTransaction(paymentTransaction);
}
catch (Exception ex)
{
_logger.LogError("UpdateOrderWithInvoice() - paymentTransaction: " + ex.Message);
}
}
var additionalMessage = GetAdditionalMessageFromWebhook(webhookEvent);
if (updated)
{
additionalMessage = string.IsNullOrEmpty(additionalMessage) ? "" : $" - {additionalMessage}";
await _orderService.InsertOrderNote(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
});
var sCaptureTransactionResult = order.OrderTags.First(a => a.StartsWith("CaptureTransactionResult#")).Split("#")[1];
if (order.PaymentStatusId is PaymentStatus.Authorized or PaymentStatus.Paid &&
!string.IsNullOrEmpty(sCaptureTransactionResult)) {
var sAuthorizationTransactionResult = order.OrderTags.First(a => a.StartsWith("AuthorizationTransactionResult#")).Split("#")[1];
await _orderService.InsertOrderNote(new OrderNote {
OrderId = order.Id,
Note = $"BTCPayServer: Payment {(order.PaymentStatusId is PaymentStatus.Authorized ? $"received but waiting to confirm. <a href='{sAuthorizationTransactionResult}'>Click here for more information.</a>" : $"settled. <a href='{sCaptureTransactionResult}'>Click here for more information.</a>")}",
DisplayToCustomer = true,
CreatedOnUtc = DateTime.UtcNow
});
}
await _orderService.UpdateOrder(order);
return true;
}
if (!string.IsNullOrEmpty(additionalMessage))
{
await _orderService.InsertOrderNote(new OrderNote {
OrderId = order.Id,
Note = $"BTCPayServer: {additionalMessage}",
DisplayToCustomer = false,
CreatedOnUtc = DateTime.UtcNow
});
}
return false;
}
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 async Task<InvoiceData> GetInvoice(BtcPaySettings settings, string invoiceId)
{
var client = GetClient(settings);
return await client.GetInvoice(settings.BtcPayStoreID, invoiceId);
}
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;
}
}
}
}