diff --git a/BTCPayServer/Services/Shopify/OrderTransactionRegisterLogic.cs b/BTCPayServer/Services/Shopify/OrderTransactionRegisterLogic.cs new file mode 100644 index 000000000..eab6bca34 --- /dev/null +++ b/BTCPayServer/Services/Shopify/OrderTransactionRegisterLogic.cs @@ -0,0 +1,57 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Newtonsoft.Json.Linq; + +namespace BTCPayServer.Services.Shopify +{ + public class OrderTransactionRegisterLogic + { + private readonly ShopifyApiClient _client; + + public OrderTransactionRegisterLogic(ShopifyApiClient client) + { + _client = client; + } + + public async Task Process(string orderId, string currency = null, string amountCaptured = null) + { + dynamic resp = await _client.TransactionsList(orderId); + + JArray transactions = resp.transactions; + if (transactions != null && transactions.Count >= 1) + { + dynamic transaction = transactions[0]; + + if (currency != null && currency.Equals(transaction.currency, StringComparison.OrdinalIgnoreCase)) + { + // because of parent_id present, currency will always be the one from parent transaction + // 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; + } + + var createTransaction = new TransactionCreate + { + transaction = new TransactionCreate.DataHolder + { + parent_id = transaction.id, + currency = transaction.currency, + amount = amountCaptured ?? transaction.amount, + kind = "capture", + gateway = "BTCPayServer", + source = "external" + } + }; + + dynamic createResp = await _client.TransactionCreate(orderId, createTransaction); + return createResp; + } + + return null; + } + } +} diff --git a/BTCPayServer/Services/Shopify/ShopifyOrderMarkerHostedService.cs b/BTCPayServer/Services/Shopify/ShopifyOrderMarkerHostedService.cs new file mode 100644 index 000000000..c6d429304 --- /dev/null +++ b/BTCPayServer/Services/Shopify/ShopifyOrderMarkerHostedService.cs @@ -0,0 +1,89 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Net.Http; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using BTCPayServer.Data; +using BTCPayServer.Logging; +using BTCPayServer.Services.Invoices; +using BTCPayServer.Services.Stores; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using NBXplorer; + +namespace BTCPayServer.Services.Shopify +{ + public class ShopifyOrderMarkerHostedService : IHostedService + { + private readonly EventAggregator _eventAggregator; + private readonly StoreRepository _storeRepository; + private readonly IHttpClientFactory _httpClientFactory; + + public ShopifyOrderMarkerHostedService(EventAggregator eventAggregator, StoreRepository storeRepository, IHttpClientFactory httpClientFactory) + { + _eventAggregator = eventAggregator; + _storeRepository = storeRepository; + _httpClientFactory = httpClientFactory; + } + + private CancellationTokenSource _Cts; + private readonly CompositeDisposable leases = new CompositeDisposable(); + + public Task StartAsync(CancellationToken cancellationToken) + { + _Cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); + + leases.Add(_eventAggregator.Subscribe(async b => + { + var invoice = b.Invoice; + var shopifyOrderId = invoice.Metadata?.OrderId; + if (invoice.Status == Client.Models.InvoiceStatus.Paid && shopifyOrderId != null) + { + var storeData = await _storeRepository.FindStore(invoice.StoreId); + var storeBlob = storeData.GetStoreBlob(); + + if (storeBlob.Shopify?.IntegratedAt.HasValue == true) + { + var client = createShopifyApiClient(storeBlob.Shopify); + + try + { + var logic = new OrderTransactionRegisterLogic(client); + await logic.Process(shopifyOrderId, invoice.Currency, invoice.Price.ToString(CultureInfo.InvariantCulture)); + Logs.PayServer.LogInformation("Registered order transaction on Shopify. " + + $"Triggered by invoiceId: {invoice.Id}, Shopify orderId: {shopifyOrderId}"); + } + catch (Exception ex) + { + Logs.PayServer.LogError(ex, $"Shopify error while trying to register order transaction. " + + $"Triggered by invoiceId: {invoice.Id}, Shopify orderId: {shopifyOrderId}"); + } + } + } + })); + return Task.CompletedTask; + } + + private ShopifyApiClient createShopifyApiClient(StoreBlob.ShopifyDataHolder shopify) + { + return new ShopifyApiClient(_httpClientFactory, null, new ShopifyApiClientCredentials + { + ShopName = shopify.ShopName, + ApiKey = shopify.ApiKey, + ApiPassword = shopify.Password, + SharedSecret = shopify.SharedSecret + }); + } + + public Task StopAsync(CancellationToken cancellationToken) + { + _Cts?.Cancel(); + leases.Dispose(); + + return Task.CompletedTask; + } + } +}