Cancel shopify order when invoice payment fails (#6027)

* Cancel shopify order when invoice payment fails

* void correctly and invoice logs
This commit is contained in:
Andrew Camilleri 2024-06-03 15:02:27 +02:00 committed by GitHub
parent 4307fa24a7
commit c5aca1b7f7
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 61 additions and 9 deletions

View file

@ -1,6 +1,8 @@
using System;
using System.Linq;
using System.Threading.Tasks;
using BTCPayServer.Data;
using BTCPayServer.Logging;
using BTCPayServer.Plugins.Shopify.ApiModels;
namespace BTCPayServer.Plugins.Shopify
@ -15,8 +17,9 @@ namespace BTCPayServer.Plugins.Shopify
}
private static string[] _keywords = new[] { "bitcoin", "btc", "btcpayserver", "btcpay server" };
public async Task<TransactionsCreateResp> Process(string orderId, string invoiceId, string currency, string amountCaptured, bool success)
public async Task<InvoiceLogs> Process(string orderId, string invoiceId, string currency, string amountCaptured, bool success)
{
var result = new InvoiceLogs();
currency = currency.ToUpperInvariant().Trim();
var existingShopifyOrderTransactions = (await _client.TransactionsList(orderId)).transactions;
@ -24,7 +27,8 @@ namespace BTCPayServer.Plugins.Shopify
var baseParentTransaction = existingShopifyOrderTransactions.FirstOrDefault(holder => _keywords.Any(a => holder.gateway.Contains(a, StringComparison.InvariantCultureIgnoreCase)));
if (baseParentTransaction is null)
{
return null;
result.Write("Couldn't find the order on Shopify.", InvoiceEventData.EventSeverity.Error);
return result;
}
//technically, this exploit should not be possible as we use internal invoice tags to verify that the invoice was created by our controlled, dedicated endpoint.
@ -34,7 +38,8 @@ namespace BTCPayServer.Plugins.Shopify
// malicious attacker could potentially exploit this by creating invoice
// in different currency and paying that one, registering order on Shopify as paid
// so if currency is supplied and is different from parent transaction currency we just won't register
return null;
result.Write("Currency mismatch on Shopify.", InvoiceEventData.EventSeverity.Error);
return result;
}
var kind = "capture";
@ -60,11 +65,20 @@ namespace BTCPayServer.Plugins.Shopify
kind = "void";
parentId = successfulCaptures.Last().id;
status = "success";
result.Write("A transaction was previously recorded against the Shopify order. Creating a void transaction.", InvoiceEventData.EventSeverity.Warning);
}else if (!success)
{
kind = "void";
status = "success";
result.Write("Attempting to void the payment on Shopify order due to failure in payment.", InvoiceEventData.EventSeverity.Warning);
}
//if we are working with a success registration, but can see that we have already had a successful transaction saved, get outta here
else if (success && successfulCaptures.Length > 0 && (successfulCaptures.Length - refunds.Length) > 0)
{
return null;
result.Write("A transaction was previously recorded against the Shopify order. Skipping.", InvoiceEventData.EventSeverity.Warning);
return result;
}
var createTransaction = new TransactionsCreateReq
{
@ -81,7 +95,33 @@ namespace BTCPayServer.Plugins.Shopify
}
};
var createResp = await _client.TransactionCreate(orderId, createTransaction);
return createResp;
if (createResp.transaction is null)
{
result.Write("Failed to register the transaction on Shopify.", InvoiceEventData.EventSeverity.Error);
}
else
{
result.Write($"Successfully registered the transaction on Shopify. tx status:{createResp.transaction.status}, kind: {createResp.transaction.kind}, order id:{createResp.transaction.order_id}", InvoiceEventData.EventSeverity.Info);
}
if (!success)
{
try
{
await _client.CancelOrder(orderId);
result.Write("Cancelling the Shopify order.", InvoiceEventData.EventSeverity.Warning);
}
catch (Exception e)
{
result.Write($"Failed to cancel the Shopify order. {e.Message}", InvoiceEventData.EventSeverity.Error);
}
}
return result;
}
}
}

View file

@ -35,10 +35,10 @@ namespace BTCPayServer.Plugins.Shopify
}
private HttpRequestMessage CreateRequest(string shopName, HttpMethod method, string action,
string relativeUrl = null)
string relativeUrl = null, string apiVersion = "2020-07")
{
var url =
$"https://{(shopName.Contains('.', StringComparison.InvariantCulture) ? shopName : $"{shopName}.myshopify.com")}/{relativeUrl ?? ("admin/api/2020-07/" + action)}";
$"https://{(shopName.Contains('.', StringComparison.InvariantCulture) ? shopName : $"{shopName}.myshopify.com")}/{relativeUrl ?? ($"admin/api/{apiVersion}/" + action)}";
var req = new HttpRequestMessage(method, url);
return req;
}
@ -115,6 +115,15 @@ namespace BTCPayServer.Plugins.Shopify
return JObject.Parse(strResp)["order"].ToObject<ShopifyOrder>();
}
public async Task<ShopifyOrder> CancelOrder(string orderId)
{
var req = CreateRequest(_credentials.ShopName, HttpMethod.Post,
$"orders/{orderId}/close.json", null, "2024-04");
var strResp = await SendRequest(req);
return JObject.Parse(strResp)["order"].ToObject<ShopifyOrder>();
}
public async Task<long> OrdersCount()
{

View file

@ -19,14 +19,17 @@ namespace BTCPayServer.Plugins.Shopify
public class ShopifyOrderMarkerHostedService : EventHostedServiceBase
{
private readonly StoreRepository _storeRepository;
private readonly InvoiceRepository _invoiceRepository;
private readonly IHttpClientFactory _httpClientFactory;
public ShopifyOrderMarkerHostedService(EventAggregator eventAggregator,
StoreRepository storeRepository,
InvoiceRepository invoiceRepository,
IHttpClientFactory httpClientFactory,
Logs logs) : base(eventAggregator, logs)
{
_storeRepository = storeRepository;
_invoiceRepository = invoiceRepository;
_httpClientFactory = httpClientFactory;
}
@ -93,8 +96,8 @@ namespace BTCPayServer.Plugins.Shopify
invoice.Price.ToString(CultureInfo.InvariantCulture), success);
if (resp != null)
{
Logs.PayServer.LogInformation($"Registered order transaction {invoice.Price}{invoice.Currency} on Shopify. " +
$"Triggered by invoiceId: {invoice.Id}, Shopify orderId: {shopifyOrderId}, Success: {success}");
await _invoiceRepository.AddInvoiceLogs(invoice.Id, resp);
}
}
catch (Exception ex)