mirror of
https://github.com/btcpayserver/btcpayserver.git
synced 2025-01-18 13:26:47 +01:00
Fix: Payments to Top-Up could be undetected due to race condition (#5568)
This commit is contained in:
parent
8da04fd7e2
commit
3fc687a2d4
@ -1,5 +1,6 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
@ -31,7 +32,9 @@ namespace BTCPayServer.Data
|
||||
public List<InvoiceSearchData> InvoiceSearchData { get; set; }
|
||||
public List<RefundData> Refunds { get; set; }
|
||||
|
||||
|
||||
[Timestamp]
|
||||
// With this, update of InvoiceData will fail if the row was modified by another process
|
||||
public uint XMin { get; set; }
|
||||
internal static void OnModelCreating(ModelBuilder builder, DatabaseFacade databaseFacade)
|
||||
{
|
||||
builder.Entity<InvoiceData>()
|
||||
|
@ -1,4 +1,4 @@
|
||||
// <auto-generated />
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using BTCPayServer.Data;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
@ -16,25 +16,7 @@ namespace BTCPayServer.Migrations
|
||||
protected override void BuildModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder.HasAnnotation("ProductVersion", "6.0.9");
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.AddressInvoiceData", b =>
|
||||
{
|
||||
b.Property<string>("Address")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTimeOffset?>("CreatedTime")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("InvoiceDataId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Address");
|
||||
|
||||
b.HasIndex("InvoiceDataId");
|
||||
|
||||
b.ToTable("AddressInvoices");
|
||||
});
|
||||
modelBuilder.HasAnnotation("ProductVersion", "8.0.0");
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.APIKeyData", b =>
|
||||
{
|
||||
@ -71,6 +53,24 @@ namespace BTCPayServer.Migrations
|
||||
b.ToTable("ApiKeys");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.AddressInvoiceData", b =>
|
||||
{
|
||||
b.Property<string>("Address")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTimeOffset?>("CreatedTime")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("InvoiceDataId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Address");
|
||||
|
||||
b.HasIndex("InvoiceDataId");
|
||||
|
||||
b.ToTable("AddressInvoices");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.AppData", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
@ -89,7 +89,7 @@ namespace BTCPayServer.Migrations
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Settings")
|
||||
.HasColumnType("JSONB");
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("StoreDataId")
|
||||
.HasColumnType("TEXT");
|
||||
@ -305,6 +305,11 @@ namespace BTCPayServer.Migrations
|
||||
b.Property<string>("StoreDataId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<uint>("XMin")
|
||||
.IsConcurrencyToken()
|
||||
.ValueGeneratedOnAddOrUpdate()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("Created");
|
||||
@ -781,31 +786,6 @@ namespace BTCPayServer.Migrations
|
||||
b.ToTable("Stores");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.StoredFile", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("ApplicationUserId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("FileName")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("StorageFileName")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("Timestamp")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("ApplicationUserId");
|
||||
|
||||
b.ToTable("Files");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.StoreRole", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
@ -863,6 +843,31 @@ namespace BTCPayServer.Migrations
|
||||
b.ToTable("StoreWebhooks");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.StoredFile", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("ApplicationUserId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("FileName")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("StorageFileName")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("Timestamp")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("ApplicationUserId");
|
||||
|
||||
b.ToTable("Files");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.U2FDevice", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
@ -1171,16 +1176,6 @@ namespace BTCPayServer.Migrations
|
||||
b.ToTable("AspNetUserTokens", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.AddressInvoiceData", b =>
|
||||
{
|
||||
b.HasOne("BTCPayServer.Data.InvoiceData", "InvoiceData")
|
||||
.WithMany("AddressInvoices")
|
||||
.HasForeignKey("InvoiceDataId")
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
b.Navigation("InvoiceData");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.APIKeyData", b =>
|
||||
{
|
||||
b.HasOne("BTCPayServer.Data.StoreData", "StoreData")
|
||||
@ -1198,6 +1193,16 @@ namespace BTCPayServer.Migrations
|
||||
b.Navigation("User");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.AddressInvoiceData", b =>
|
||||
{
|
||||
b.HasOne("BTCPayServer.Data.InvoiceData", "InvoiceData")
|
||||
.WithMany("AddressInvoices")
|
||||
.HasForeignKey("InvoiceDataId")
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
b.Navigation("InvoiceData");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.AppData", b =>
|
||||
{
|
||||
b.HasOne("BTCPayServer.Data.StoreData", "StoreData")
|
||||
@ -1408,15 +1413,6 @@ namespace BTCPayServer.Migrations
|
||||
b.Navigation("PullPaymentData");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.StoredFile", b =>
|
||||
{
|
||||
b.HasOne("BTCPayServer.Data.ApplicationUser", "ApplicationUser")
|
||||
.WithMany("StoredFiles")
|
||||
.HasForeignKey("ApplicationUserId");
|
||||
|
||||
b.Navigation("ApplicationUser");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.StoreRole", b =>
|
||||
{
|
||||
b.HasOne("BTCPayServer.Data.StoreData", "StoreData")
|
||||
@ -1457,6 +1453,15 @@ namespace BTCPayServer.Migrations
|
||||
b.Navigation("Webhook");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.StoredFile", b =>
|
||||
{
|
||||
b.HasOne("BTCPayServer.Data.ApplicationUser", "ApplicationUser")
|
||||
.WithMany("StoredFiles")
|
||||
.HasForeignKey("ApplicationUserId");
|
||||
|
||||
b.Navigation("ApplicationUser");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.U2FDevice", b =>
|
||||
{
|
||||
b.HasOne("BTCPayServer.Data.ApplicationUser", "ApplicationUser")
|
||||
|
@ -105,8 +105,15 @@ namespace BTCPayServer.Tests
|
||||
|
||||
public void MineBlockOnInvoiceCheckout()
|
||||
{
|
||||
Driver.FindElement(By.CssSelector("#mine-block button")).Click();
|
||||
|
||||
retry:
|
||||
try
|
||||
{
|
||||
Driver.FindElement(By.CssSelector("#mine-block button")).Click();
|
||||
}
|
||||
catch (StaleElementReferenceException)
|
||||
{
|
||||
goto retry;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -1149,7 +1149,6 @@ namespace BTCPayServer.Tests
|
||||
|
||||
// Contribute
|
||||
s.Driver.FindElement(By.Id("crowdfund-body-header-cta")).Click();
|
||||
Thread.Sleep(1000);
|
||||
s.Driver.WaitUntilAvailable(By.Name("btcpay"));
|
||||
|
||||
var frameElement = s.Driver.FindElement(By.Name("btcpay"));
|
||||
|
@ -395,7 +395,7 @@ namespace BTCPayServer.Tests
|
||||
BOLT11PaymentRequest.Parse(newBolt11, Network.RegTest).MinimumAmount.ToDecimal(LightMoneyUnit.BTC));
|
||||
}, 40000);
|
||||
|
||||
TestLogs.LogInformation($"Paying invoice {newInvoice.Id} remaining due amount {newInvoice.BtcDue.GetValue((BTCPayNetwork) tester.DefaultNetwork)} via lightning");
|
||||
TestLogs.LogInformation($"Paying invoice {newInvoice.Id} remaining due amount {newInvoice.BtcDue.GetValue((BTCPayNetwork)tester.DefaultNetwork)} via lightning");
|
||||
var evt = await tester.WaitForEvent<InvoiceDataChangedEvent>(async () =>
|
||||
{
|
||||
await tester.SendLightningPaymentAsync(newInvoice);
|
||||
@ -1301,11 +1301,8 @@ namespace BTCPayServer.Tests
|
||||
var user = tester.NewAccount();
|
||||
await user.GrantAccessAsync();
|
||||
user.RegisterDerivationScheme("BTC");
|
||||
|
||||
await tester.ExplorerNode.EnsureGenerateAsync(1);
|
||||
var rng = new Random();
|
||||
var seed = rng.Next();
|
||||
rng = new Random(seed);
|
||||
TestLogs.LogInformation("Seed: " + seed);
|
||||
foreach (var networkFeeMode in Enum.GetValues(typeof(NetworkFeeMode)).Cast<NetworkFeeMode>())
|
||||
{
|
||||
await user.SetNetworkFeeMode(networkFeeMode);
|
||||
@ -1318,7 +1315,7 @@ namespace BTCPayServer.Tests
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task AssertTopUpBtcPrice(ServerTester tester, TestAccount user, Money btcSent, decimal expectedPriceWithoutNetworkFee, NetworkFeeMode networkFeeMode)
|
||||
private async Task AssertTopUpBtcPrice(ServerTester tester, TestAccount user, Money btcSent, decimal expectedPriceWithoutNetworkFee, NetworkFeeMode networkFeeMode)
|
||||
{
|
||||
var cashCow = tester.ExplorerNode;
|
||||
// First we try payment with a merchant having only BTC
|
||||
@ -1343,7 +1340,6 @@ namespace BTCPayServer.Tests
|
||||
{
|
||||
networkFee = 0.0m;
|
||||
}
|
||||
|
||||
await cashCow.SendToAddressAsync(invoiceAddress, paid);
|
||||
await TestUtils.EventuallyAsync(async () =>
|
||||
{
|
||||
@ -1822,7 +1818,7 @@ namespace BTCPayServer.Tests
|
||||
Assert.Empty(appList2.Apps);
|
||||
Assert.Equal("test", appList.Apps[0].AppName);
|
||||
Assert.Equal(apps.CreatedAppId, appList.Apps[0].Id);
|
||||
|
||||
|
||||
Assert.True(app.Role.ToPermissionSet(app.StoreId).Contains(Policies.CanModifyStoreSettings, app.StoreId));
|
||||
Assert.Equal(user.StoreId, appList.Apps[0].StoreId);
|
||||
Assert.IsType<NotFoundResult>(apps2.DeleteApp(appList.Apps[0].Id));
|
||||
@ -2399,11 +2395,11 @@ namespace BTCPayServer.Tests
|
||||
|
||||
var url = lnMethod.GetExternalLightningUrl();
|
||||
var kv = LightningConnectionStringHelper.ExtractValues(url, out var connType);
|
||||
Assert.Equal(LightningConnectionType.Charge,connType);
|
||||
Assert.Equal(LightningConnectionType.Charge, connType);
|
||||
var client = Assert.IsType<ChargeClient>(tester.PayTester.GetService<LightningClientFactoryService>()
|
||||
.Create(url, tester.NetworkProvider.GetNetwork<BTCPayNetwork>("BTC")));
|
||||
var auth = Assert.IsType<ChargeAuthentication.UserPasswordAuthentication>(client.ChargeAuthentication);
|
||||
|
||||
|
||||
Assert.Equal("pass", auth.NetworkCredential.Password);
|
||||
Assert.Equal("usr", auth.NetworkCredential.UserName);
|
||||
|
||||
@ -2829,7 +2825,7 @@ namespace BTCPayServer.Tests
|
||||
var app = await client.CreatePointOfSaleApp(acc.StoreId, new CreatePointOfSaleAppRequest()
|
||||
{
|
||||
AppName = "Static",
|
||||
DefaultView = Client.Models.PosViewType.Static,
|
||||
DefaultView = Client.Models.PosViewType.Static,
|
||||
Template = new PointOfSaleSettings().Template
|
||||
});
|
||||
var resp = await posController.ViewPointOfSale(app.Id, choiceKey: "green-tea");
|
||||
@ -2839,7 +2835,7 @@ namespace BTCPayServer.Tests
|
||||
app = await client.CreatePointOfSaleApp(acc.StoreId, new CreatePointOfSaleAppRequest()
|
||||
{
|
||||
AppName = "Cart",
|
||||
DefaultView = Client.Models.PosViewType.Cart,
|
||||
DefaultView = Client.Models.PosViewType.Cart,
|
||||
Template = new PointOfSaleSettings().Template
|
||||
});
|
||||
resp = await posController.ViewPointOfSale(app.Id, posData: new JObject()
|
||||
|
@ -50,6 +50,7 @@ namespace BTCPayServer.Data
|
||||
|
||||
public static StoreBlob GetStoreBlob(this StoreData storeData)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(storeData);
|
||||
var result = storeData.StoreBlob == null ? new StoreBlob() : new Serializer(null).ToObject<StoreBlob>(storeData.StoreBlob);
|
||||
if (result.PreferredExchange == null)
|
||||
result.PreferredExchange = result.GetRecommendedExchange();
|
||||
|
@ -38,11 +38,10 @@ namespace BTCPayServer.HostedServices
|
||||
|
||||
public bool Dirty => _dirty;
|
||||
|
||||
bool _isBlobUpdated;
|
||||
public bool IsBlobUpdated => _isBlobUpdated;
|
||||
public void BlobUpdated()
|
||||
public bool IsPriceUpdated { get; private set; }
|
||||
public void PriceUpdated()
|
||||
{
|
||||
_isBlobUpdated = true;
|
||||
IsPriceUpdated = true;
|
||||
}
|
||||
}
|
||||
|
||||
@ -104,7 +103,7 @@ namespace BTCPayServer.HostedServices
|
||||
var payment = invoice.GetPayments(true).First();
|
||||
invoice.Price = payment.InvoicePaidAmount.Net;
|
||||
invoice.UpdateTotals();
|
||||
context.BlobUpdated();
|
||||
context.PriceUpdated();
|
||||
}
|
||||
else
|
||||
{
|
||||
@ -291,9 +290,9 @@ namespace BTCPayServer.HostedServices
|
||||
await _invoiceRepository.UpdateInvoiceStatus(invoice.Id, invoice.GetInvoiceState());
|
||||
updateContext.Events.Insert(0, new InvoiceDataChangedEvent(invoice));
|
||||
}
|
||||
if (updateContext.IsBlobUpdated)
|
||||
if (updateContext.IsPriceUpdated)
|
||||
{
|
||||
await _invoiceRepository.UpdateInvoicePrice(invoice.Id, invoice);
|
||||
await _invoiceRepository.UpdateInvoicePrice(invoice.Id, invoice.Price);
|
||||
}
|
||||
|
||||
foreach (var evt in updateContext.Events)
|
||||
|
@ -79,13 +79,13 @@ namespace BTCPayServer.Services.Apps
|
||||
|
||||
public async Task<object?> GetInfo(string appId)
|
||||
{
|
||||
var appData = await GetApp(appId, null);
|
||||
var appData = await GetApp(appId, null, includeStore: true);
|
||||
if (appData is null)
|
||||
return null;
|
||||
var appType = GetAppType(appData.AppType);
|
||||
if (appType is null)
|
||||
return null;
|
||||
return appType.GetInfo(appData);
|
||||
return await appType.GetInfo(appData);
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<ItemStats>> GetItemStats(AppData appData)
|
||||
|
@ -11,6 +11,7 @@ using BTCPayServer.Events;
|
||||
using BTCPayServer.Logging;
|
||||
using BTCPayServer.Models.InvoicingModels;
|
||||
using BTCPayServer.Payments;
|
||||
using Dapper;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using NBitcoin;
|
||||
using Newtonsoft.Json;
|
||||
@ -130,32 +131,50 @@ namespace BTCPayServer.Services.Invoices
|
||||
|
||||
public async Task UpdateInvoice(string invoiceId, UpdateCustomerModel data)
|
||||
{
|
||||
using var ctx = _applicationDbContextFactory.CreateContext();
|
||||
var invoiceData = await ctx.Invoices.FindAsync(invoiceId).ConfigureAwait(false);
|
||||
if (invoiceData == null)
|
||||
return;
|
||||
if (invoiceData.CustomerEmail == null && data.Email != null)
|
||||
retry:
|
||||
using (var ctx = _applicationDbContextFactory.CreateContext())
|
||||
{
|
||||
invoiceData.CustomerEmail = data.Email;
|
||||
AddToTextSearch(ctx, invoiceData, invoiceData.CustomerEmail);
|
||||
var invoiceData = await ctx.Invoices.FindAsync(invoiceId);
|
||||
if (invoiceData == null)
|
||||
return;
|
||||
if (invoiceData.CustomerEmail == null && data.Email != null)
|
||||
{
|
||||
invoiceData.CustomerEmail = data.Email;
|
||||
AddToTextSearch(ctx, invoiceData, invoiceData.CustomerEmail);
|
||||
}
|
||||
try
|
||||
{
|
||||
await ctx.SaveChangesAsync().ConfigureAwait(false);
|
||||
}
|
||||
catch (DbUpdateConcurrencyException)
|
||||
{
|
||||
goto retry;
|
||||
}
|
||||
}
|
||||
await ctx.SaveChangesAsync().ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task UpdateInvoiceExpiry(string invoiceId, TimeSpan seconds)
|
||||
{
|
||||
await using var ctx = _applicationDbContextFactory.CreateContext();
|
||||
var invoiceData = await ctx.Invoices.FindAsync(invoiceId);
|
||||
var invoice = invoiceData.GetBlob(_btcPayNetworkProvider);
|
||||
var expiry = DateTimeOffset.Now + seconds;
|
||||
invoice.ExpirationTime = expiry;
|
||||
invoice.MonitoringExpiration = expiry.AddHours(1);
|
||||
invoiceData.SetBlob(invoice);
|
||||
|
||||
await ctx.SaveChangesAsync();
|
||||
|
||||
_eventAggregator.Publish(new InvoiceDataChangedEvent(invoice));
|
||||
_ = InvoiceNeedUpdateEventLater(invoiceId, seconds);
|
||||
retry:
|
||||
await using (var ctx = _applicationDbContextFactory.CreateContext())
|
||||
{
|
||||
var invoiceData = await ctx.Invoices.FindAsync(invoiceId);
|
||||
var invoice = invoiceData.GetBlob(_btcPayNetworkProvider);
|
||||
var expiry = DateTimeOffset.Now + seconds;
|
||||
invoice.ExpirationTime = expiry;
|
||||
invoice.MonitoringExpiration = expiry.AddHours(1);
|
||||
invoiceData.SetBlob(invoice);
|
||||
try
|
||||
{
|
||||
await ctx.SaveChangesAsync();
|
||||
}
|
||||
catch (DbUpdateConcurrencyException)
|
||||
{
|
||||
goto retry;
|
||||
}
|
||||
_eventAggregator.Publish(new InvoiceDataChangedEvent(invoice));
|
||||
_ = InvoiceNeedUpdateEventLater(invoiceId, seconds);
|
||||
}
|
||||
}
|
||||
|
||||
async Task InvoiceNeedUpdateEventLater(string invoiceId, TimeSpan expirationIn)
|
||||
@ -166,13 +185,23 @@ namespace BTCPayServer.Services.Invoices
|
||||
|
||||
public async Task ExtendInvoiceMonitor(string invoiceId)
|
||||
{
|
||||
using var ctx = _applicationDbContextFactory.CreateContext();
|
||||
var invoiceData = await ctx.Invoices.FindAsync(invoiceId);
|
||||
retry:
|
||||
using (var ctx = _applicationDbContextFactory.CreateContext())
|
||||
{
|
||||
var invoiceData = await ctx.Invoices.FindAsync(invoiceId);
|
||||
|
||||
var invoice = invoiceData.GetBlob(_btcPayNetworkProvider);
|
||||
invoice.MonitoringExpiration = invoice.MonitoringExpiration.AddHours(1);
|
||||
invoiceData.SetBlob(invoice);
|
||||
await ctx.SaveChangesAsync();
|
||||
var invoice = invoiceData.GetBlob(_btcPayNetworkProvider);
|
||||
invoice.MonitoringExpiration = invoice.MonitoringExpiration.AddHours(1);
|
||||
invoiceData.SetBlob(invoice);
|
||||
try
|
||||
{
|
||||
await ctx.SaveChangesAsync();
|
||||
}
|
||||
catch (DbUpdateConcurrencyException)
|
||||
{
|
||||
goto retry;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public async Task CreateInvoiceAsync(InvoiceEntity invoice, string[] additionalSearchTerms = null)
|
||||
@ -279,62 +308,81 @@ namespace BTCPayServer.Services.Invoices
|
||||
|
||||
public async Task<bool> NewPaymentDetails(string invoiceId, IPaymentMethodDetails paymentMethodDetails, BTCPayNetworkBase network)
|
||||
{
|
||||
await using var context = _applicationDbContextFactory.CreateContext();
|
||||
var invoice = (await context.Invoices.Where(i => i.Id == invoiceId).ToListAsync()).FirstOrDefault();
|
||||
if (invoice == null)
|
||||
return false;
|
||||
retry:
|
||||
await using (var context = _applicationDbContextFactory.CreateContext())
|
||||
{
|
||||
var invoice = (await context.Invoices.Where(i => i.Id == invoiceId).ToListAsync()).FirstOrDefault();
|
||||
if (invoice == null)
|
||||
return false;
|
||||
|
||||
var invoiceEntity = invoice.GetBlob(_btcPayNetworkProvider);
|
||||
var paymentMethod = invoiceEntity.GetPaymentMethod(network, paymentMethodDetails.GetPaymentType());
|
||||
if (paymentMethod == null)
|
||||
return false;
|
||||
var invoiceEntity = invoice.GetBlob(_btcPayNetworkProvider);
|
||||
var paymentMethod = invoiceEntity.GetPaymentMethod(network, paymentMethodDetails.GetPaymentType());
|
||||
if (paymentMethod == null)
|
||||
return false;
|
||||
|
||||
var existingPaymentMethod = paymentMethod.GetPaymentMethodDetails();
|
||||
paymentMethod.SetPaymentMethodDetails(paymentMethodDetails);
|
||||
var existingPaymentMethod = paymentMethod.GetPaymentMethodDetails();
|
||||
paymentMethod.SetPaymentMethodDetails(paymentMethodDetails);
|
||||
#pragma warning disable CS0618
|
||||
if (network.IsBTC)
|
||||
{
|
||||
invoiceEntity.DepositAddress = paymentMethod.DepositAddress;
|
||||
}
|
||||
if (network.IsBTC)
|
||||
{
|
||||
invoiceEntity.DepositAddress = paymentMethod.DepositAddress;
|
||||
}
|
||||
#pragma warning restore CS0618
|
||||
invoiceEntity.SetPaymentMethod(paymentMethod);
|
||||
invoice.SetBlob(invoiceEntity);
|
||||
|
||||
await context.AddressInvoices.AddAsync(new AddressInvoiceData()
|
||||
{
|
||||
InvoiceDataId = invoiceId,
|
||||
CreatedTime = DateTimeOffset.UtcNow
|
||||
}
|
||||
.Set(GetDestination(paymentMethod), paymentMethod.GetId()));
|
||||
|
||||
AddToTextSearch(context, invoice, paymentMethodDetails.GetPaymentDestination());
|
||||
await context.SaveChangesAsync();
|
||||
return true;
|
||||
}
|
||||
|
||||
public async Task UpdateInvoicePaymentMethod(string invoiceId, PaymentMethod paymentMethod)
|
||||
{
|
||||
using var context = _applicationDbContextFactory.CreateContext();
|
||||
var invoice = await context.Invoices.FindAsync(invoiceId);
|
||||
if (invoice == null)
|
||||
return;
|
||||
var network = paymentMethod.Network;
|
||||
var invoiceEntity = invoice.GetBlob(_btcPayNetworkProvider);
|
||||
var newDetails = paymentMethod.GetPaymentMethodDetails();
|
||||
var existing = invoiceEntity.GetPaymentMethod(paymentMethod.GetId());
|
||||
if (existing.GetPaymentMethodDetails().GetPaymentDestination() != newDetails.GetPaymentDestination() && newDetails.Activated)
|
||||
{
|
||||
invoiceEntity.SetPaymentMethod(paymentMethod);
|
||||
invoice.SetBlob(invoiceEntity);
|
||||
await context.AddressInvoices.AddAsync(new AddressInvoiceData()
|
||||
{
|
||||
InvoiceDataId = invoiceId,
|
||||
CreatedTime = DateTimeOffset.UtcNow
|
||||
}
|
||||
.Set(GetDestination(paymentMethod), paymentMethod.GetId()));
|
||||
.Set(GetDestination(paymentMethod), paymentMethod.GetId()));
|
||||
|
||||
AddToTextSearch(context, invoice, paymentMethodDetails.GetPaymentDestination());
|
||||
try
|
||||
{
|
||||
await context.SaveChangesAsync();
|
||||
}
|
||||
catch (DbUpdateConcurrencyException)
|
||||
{
|
||||
goto retry;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task UpdateInvoicePaymentMethod(string invoiceId, PaymentMethod paymentMethod)
|
||||
{
|
||||
retry:
|
||||
using (var context = _applicationDbContextFactory.CreateContext())
|
||||
{
|
||||
var invoice = await context.Invoices.FindAsync(invoiceId);
|
||||
if (invoice == null)
|
||||
return;
|
||||
var network = paymentMethod.Network;
|
||||
var invoiceEntity = invoice.GetBlob(_btcPayNetworkProvider);
|
||||
var newDetails = paymentMethod.GetPaymentMethodDetails();
|
||||
var existing = invoiceEntity.GetPaymentMethod(paymentMethod.GetId());
|
||||
if (existing.GetPaymentMethodDetails().GetPaymentDestination() != newDetails.GetPaymentDestination() && newDetails.Activated)
|
||||
{
|
||||
await context.AddressInvoices.AddAsync(new AddressInvoiceData()
|
||||
{
|
||||
InvoiceDataId = invoiceId,
|
||||
CreatedTime = DateTimeOffset.UtcNow
|
||||
}
|
||||
.Set(GetDestination(paymentMethod), paymentMethod.GetId()));
|
||||
}
|
||||
invoiceEntity.SetPaymentMethod(paymentMethod);
|
||||
invoice.SetBlob(invoiceEntity);
|
||||
AddToTextSearch(context, invoice, paymentMethod.GetPaymentMethodDetails().GetPaymentDestination());
|
||||
try
|
||||
{
|
||||
await context.SaveChangesAsync();
|
||||
}
|
||||
catch (DbUpdateConcurrencyException)
|
||||
{
|
||||
goto retry;
|
||||
}
|
||||
}
|
||||
invoiceEntity.SetPaymentMethod(paymentMethod);
|
||||
invoice.SetBlob(invoiceEntity);
|
||||
AddToTextSearch(context, invoice, paymentMethod.GetPaymentMethodDetails().GetPaymentDestination());
|
||||
await context.SaveChangesAsync();
|
||||
}
|
||||
|
||||
public async Task AddPendingInvoiceIfNotPresent(string invoiceId)
|
||||
@ -389,26 +437,38 @@ namespace BTCPayServer.Services.Invoices
|
||||
public async Task UpdateInvoiceStatus(string invoiceId, InvoiceState invoiceState)
|
||||
{
|
||||
using var context = _applicationDbContextFactory.CreateContext();
|
||||
var invoiceData = await context.FindAsync<Data.InvoiceData>(invoiceId).ConfigureAwait(false);
|
||||
if (invoiceData == null)
|
||||
return;
|
||||
invoiceData.Status = InvoiceState.ToString(invoiceState.Status);
|
||||
invoiceData.ExceptionStatus = InvoiceState.ToString(invoiceState.ExceptionStatus);
|
||||
await context.SaveChangesAsync().ConfigureAwait(false);
|
||||
await context.Database.GetDbConnection()
|
||||
.ExecuteAsync("UPDATE \"Invoices\" SET \"Status\"=@status, \"ExceptionStatus\"=@exstatus WHERE \"Id\"=@id",
|
||||
new
|
||||
{
|
||||
id = invoiceId,
|
||||
status = InvoiceState.ToString(invoiceState.Status),
|
||||
exstatus = InvoiceState.ToString(invoiceState.ExceptionStatus)
|
||||
});
|
||||
}
|
||||
internal async Task UpdateInvoicePrice(string invoiceId, InvoiceEntity invoice)
|
||||
internal async Task UpdateInvoicePrice(string invoiceId, decimal price)
|
||||
{
|
||||
if (invoice.Type != InvoiceType.TopUp)
|
||||
throw new ArgumentException("The invoice type should be TopUp to be able to update invoice price", nameof(invoice));
|
||||
using var context = _applicationDbContextFactory.CreateContext();
|
||||
var invoiceData = await context.FindAsync<Data.InvoiceData>(invoiceId).ConfigureAwait(false);
|
||||
if (invoiceData == null)
|
||||
return;
|
||||
var blob = invoiceData.GetBlob(_btcPayNetworkProvider);
|
||||
blob.Price = invoice.Price;
|
||||
AddToTextSearch(context, invoiceData, new[] { invoice.Price.ToString(CultureInfo.InvariantCulture) });
|
||||
invoiceData.SetBlob(blob);
|
||||
await context.SaveChangesAsync().ConfigureAwait(false);
|
||||
retry:
|
||||
using (var context = _applicationDbContextFactory.CreateContext())
|
||||
{
|
||||
var invoiceData = await context.FindAsync<Data.InvoiceData>(invoiceId).ConfigureAwait(false);
|
||||
if (invoiceData == null)
|
||||
return;
|
||||
var blob = invoiceData.GetBlob(_btcPayNetworkProvider);
|
||||
if (blob.Type != InvoiceType.TopUp)
|
||||
throw new ArgumentException("The invoice type should be TopUp to be able to update invoice price", nameof(invoiceId));
|
||||
blob.Price = price;
|
||||
AddToTextSearch(context, invoiceData, new[] { price.ToString(CultureInfo.InvariantCulture) });
|
||||
invoiceData.SetBlob(blob);
|
||||
try
|
||||
{
|
||||
await context.SaveChangesAsync();
|
||||
}
|
||||
catch (DbUpdateConcurrencyException)
|
||||
{
|
||||
goto retry;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public async Task MassArchive(string[] invoiceIds, bool archive = true)
|
||||
@ -436,37 +496,47 @@ namespace BTCPayServer.Services.Invoices
|
||||
}
|
||||
public async Task<InvoiceEntity> UpdateInvoiceMetadata(string invoiceId, string storeId, JObject metadata)
|
||||
{
|
||||
using var context = _applicationDbContextFactory.CreateContext();
|
||||
var invoiceData = await GetInvoiceRaw(invoiceId, context);
|
||||
if (invoiceData == null || (storeId != null &&
|
||||
!invoiceData.StoreDataId.Equals(storeId,
|
||||
StringComparison.InvariantCultureIgnoreCase)))
|
||||
return null;
|
||||
var blob = invoiceData.GetBlob(_btcPayNetworkProvider);
|
||||
|
||||
var newMetadata = InvoiceMetadata.FromJObject(metadata);
|
||||
var oldOrderId = blob.Metadata.OrderId;
|
||||
var newOrderId = newMetadata.OrderId;
|
||||
|
||||
if (newOrderId != oldOrderId)
|
||||
retry:
|
||||
using (var context = _applicationDbContextFactory.CreateContext())
|
||||
{
|
||||
// OrderId is saved in 2 places: (1) the invoice table and (2) in the metadata field. We are updating both for consistency.
|
||||
invoiceData.OrderId = newOrderId;
|
||||
var invoiceData = await GetInvoiceRaw(invoiceId, context);
|
||||
if (invoiceData == null || (storeId != null &&
|
||||
!invoiceData.StoreDataId.Equals(storeId,
|
||||
StringComparison.InvariantCultureIgnoreCase)))
|
||||
return null;
|
||||
var blob = invoiceData.GetBlob(_btcPayNetworkProvider);
|
||||
|
||||
if (oldOrderId != null && (newOrderId is null || !newOrderId.Equals(oldOrderId, StringComparison.InvariantCulture)))
|
||||
var newMetadata = InvoiceMetadata.FromJObject(metadata);
|
||||
var oldOrderId = blob.Metadata.OrderId;
|
||||
var newOrderId = newMetadata.OrderId;
|
||||
|
||||
if (newOrderId != oldOrderId)
|
||||
{
|
||||
RemoveFromTextSearch(context, invoiceData, oldOrderId);
|
||||
// OrderId is saved in 2 places: (1) the invoice table and (2) in the metadata field. We are updating both for consistency.
|
||||
invoiceData.OrderId = newOrderId;
|
||||
|
||||
if (oldOrderId != null && (newOrderId is null || !newOrderId.Equals(oldOrderId, StringComparison.InvariantCulture)))
|
||||
{
|
||||
RemoveFromTextSearch(context, invoiceData, oldOrderId);
|
||||
}
|
||||
if (newOrderId != null)
|
||||
{
|
||||
AddToTextSearch(context, invoiceData, new[] { newOrderId });
|
||||
}
|
||||
}
|
||||
if (newOrderId != null)
|
||||
|
||||
blob.Metadata = newMetadata;
|
||||
invoiceData.SetBlob(blob);
|
||||
try
|
||||
{
|
||||
AddToTextSearch(context, invoiceData, new[] { newOrderId });
|
||||
await context.SaveChangesAsync();
|
||||
}
|
||||
catch (DbUpdateConcurrencyException)
|
||||
{
|
||||
goto retry;
|
||||
}
|
||||
return ToEntity(invoiceData);
|
||||
}
|
||||
|
||||
blob.Metadata = newMetadata;
|
||||
invoiceData.SetBlob(blob);
|
||||
await context.SaveChangesAsync().ConfigureAwait(false);
|
||||
return ToEntity(invoiceData);
|
||||
}
|
||||
public async Task<bool> MarkInvoiceStatus(string invoiceId, InvoiceStatus status)
|
||||
{
|
||||
|
Loading…
Reference in New Issue
Block a user