Can delete stores

This commit is contained in:
nicolas.dorier
2018-07-19 19:31:17 +09:00
parent c3ea63c6ce
commit ce17e3212a
23 changed files with 1026 additions and 45 deletions

View File

@@ -120,7 +120,7 @@ namespace BTCPayServer.Tests
.Build(); .Build();
_Host.Start(); _Host.Start();
InvoiceRepository = (InvoiceRepository)_Host.Services.GetService(typeof(InvoiceRepository)); InvoiceRepository = (InvoiceRepository)_Host.Services.GetService(typeof(InvoiceRepository));
StoreRepository = (StoreRepository)_Host.Services.GetService(typeof(StoreRepository));
var rateProvider = (BTCPayRateProviderFactory)_Host.Services.GetService(typeof(BTCPayRateProviderFactory)); var rateProvider = (BTCPayRateProviderFactory)_Host.Services.GetService(typeof(BTCPayRateProviderFactory));
rateProvider.DirectProviders.Clear(); rateProvider.DirectProviders.Clear();
@@ -152,6 +152,7 @@ namespace BTCPayServer.Tests
internal set; internal set;
} }
public InvoiceRepository InvoiceRepository { get; private set; } public InvoiceRepository InvoiceRepository { get; private set; }
public StoreRepository StoreRepository { get; private set; }
public Uri IntegratedLightning { get; internal set; } public Uri IntegratedLightning { get; internal set; }
public bool InContainer { get; internal set; } public bool InContainer { get; internal set; }

View File

@@ -12,6 +12,7 @@ using System.Linq;
using System.Threading; using System.Threading;
using NBitpayClient; using NBitpayClient;
using System.Globalization; using System.Globalization;
using Xunit.Sdk;
namespace BTCPayServer.Tests.Lnd namespace BTCPayServer.Tests.Lnd
{ {
@@ -100,7 +101,7 @@ namespace BTCPayServer.Tests.Lnd
var waitTask3 = listener.WaitInvoice(waitToken); var waitTask3 = listener.WaitInvoice(waitToken);
await Task.Delay(100); await Task.Delay(100);
listener.Dispose(); listener.Dispose();
Assert.Throws<TaskCanceledException>(()=> waitTask3.GetAwaiter().GetResult()); Assert.Throws<TaskCanceledException>(() => waitTask3.GetAwaiter().GetResult());
} }
[Fact] [Fact]
@@ -114,11 +115,29 @@ namespace BTCPayServer.Tests.Lnd
Payment_request = merchantInvoice.BOLT11 Payment_request = merchantInvoice.BOLT11
}); });
var invoice = await InvoiceClient.GetInvoice(merchantInvoice.Id); await EventuallyAsync(async () =>
{
Assert.True(invoice.PaidAt.HasValue); var invoice = await InvoiceClient.GetInvoice(merchantInvoice.Id);
Assert.True(invoice.PaidAt.HasValue);
});
} }
private async Task EventuallyAsync(Func<Task> act)
{
CancellationTokenSource cts = new CancellationTokenSource(20000);
while (true)
{
try
{
await act();
break;
}
catch (XunitException) when (!cts.Token.IsCancellationRequested)
{
await Task.Delay(500);
}
}
}
public async Task<LnrpcChannel> EnsureLightningChannelAsync() public async Task<LnrpcChannel> EnsureLightningChannelAsync()
{ {

View File

@@ -214,9 +214,14 @@ namespace BTCPayServer.Tests
{ {
get; set; get; set;
} }
public List<string> Stores { get; internal set; } = new List<string>();
public void Dispose() public void Dispose()
{ {
foreach(var store in Stores)
{
Xunit.Assert.True(PayTester.StoreRepository.DeleteStore(store).GetAwaiter().GetResult());
}
if (PayTester != null) if (PayTester != null)
PayTester.Dispose(); PayTester.Dispose();
} }

View File

@@ -65,6 +65,7 @@ namespace BTCPayServer.Tests
var store = this.GetController<UserStoresController>(); var store = this.GetController<UserStoresController>();
await store.CreateStore(new CreateStoreViewModel() { Name = "Test Store" }); await store.CreateStore(new CreateStoreViewModel() { Name = "Test Store" });
StoreId = store.CreatedStoreId; StoreId = store.CreatedStoreId;
parent.Stores.Add(StoreId);
} }
public BTCPayNetwork SupportedNetwork { get; set; } public BTCPayNetwork SupportedNetwork { get; set; }

View File

@@ -162,7 +162,8 @@ namespace BTCPayServer.Controllers
using (var ctx = _ContextFactory.CreateContext()) using (var ctx = _ContextFactory.CreateContext())
{ {
return await ctx.Apps return await ctx.Apps
.Where(us => us.Id == appId && us.AppType == appType.ToString()) .Where(us => us.Id == appId &&
us.AppType == appType.ToString())
.FirstOrDefaultAsync(); .FirstOrDefaultAsync();
} }
} }

View File

@@ -122,8 +122,6 @@ namespace BTCPayServer.Controllers
HashSet<CurrencyPair> currencyPairsToFetch = new HashSet<CurrencyPair>(); HashSet<CurrencyPair> currencyPairsToFetch = new HashSet<CurrencyPair>();
var rules = storeBlob.GetRateRules(_NetworkProvider); var rules = storeBlob.GetRateRules(_NetworkProvider);
await UpdateCLightningConnectionStringIfNeeded(store);
foreach (var network in store.GetSupportedPaymentMethods(_NetworkProvider) foreach (var network in store.GetSupportedPaymentMethods(_NetworkProvider)
.Select(c => _NetworkProvider.GetNetwork(c.PaymentId.CryptoCode)) .Select(c => _NetworkProvider.GetNetwork(c.PaymentId.CryptoCode))
.Where(c => c != null)) .Where(c => c != null))
@@ -213,22 +211,6 @@ namespace BTCPayServer.Controllers
return new DataWrapper<InvoiceResponse>(resp) { Facade = "pos/invoice" }; return new DataWrapper<InvoiceResponse>(resp) { Facade = "pos/invoice" };
} }
private async Task UpdateCLightningConnectionStringIfNeeded(StoreData store)
{
bool needUpdate = false;
foreach (var method in store.GetSupportedPaymentMethods(_NetworkProvider).OfType<Payments.Lightning.LightningSupportedPaymentMethod>())
{
var lightning = method.GetLightningUrl();
if (lightning.IsLegacy)
{
method.SetLightningUrl(lightning);
needUpdate = true;
}
}
if(needUpdate)
await _StoreRepository.UpdateStore(store);
}
private async Task<PaymentMethod> CreatePaymentMethodAsync(Dictionary<CurrencyPair, Task<RateResult>> fetchingByCurrencyPair, IPaymentMethodHandler handler, ISupportedPaymentMethod supportedPaymentMethod, BTCPayNetwork network, InvoiceEntity entity, StoreData store) private async Task<PaymentMethod> CreatePaymentMethodAsync(Dictionary<CurrencyPair, Task<RateResult>> fetchingByCurrencyPair, IPaymentMethodHandler handler, ISupportedPaymentMethod supportedPaymentMethod, BTCPayNetwork network, InvoiceEntity entity, StoreData store)
{ {
var storeBlob = store.GetStoreBlob(); var storeBlob = store.GetStoreBlob();

View File

@@ -19,5 +19,7 @@ namespace BTCPayServer.Data
{ {
get; set; get; set;
} }
public StoreData StoreData { get; set; }
} }
} }

View File

@@ -102,14 +102,27 @@ namespace BTCPayServer.Data
{ {
base.OnModelCreating(builder); base.OnModelCreating(builder);
builder.Entity<InvoiceData>() builder.Entity<InvoiceData>()
.HasIndex(o => o.StoreDataId); .HasOne(o => o.StoreData)
.WithMany(a => a.Invoices).OnDelete(DeleteBehavior.Cascade);
builder.Entity<InvoiceData>().HasIndex(o => o.StoreDataId);
builder.Entity<PaymentData>() builder.Entity<PaymentData>()
.HasIndex(o => o.InvoiceDataId); .HasOne(o => o.InvoiceData)
.WithMany(i => i.Payments).OnDelete(DeleteBehavior.Cascade);
builder.Entity<PaymentData>()
.HasIndex(o => o.InvoiceDataId);
builder.Entity<RefundAddressesData>()
.HasOne(o => o.InvoiceData)
.WithMany(i => i.RefundAddresses).OnDelete(DeleteBehavior.Cascade);
builder.Entity<RefundAddressesData>() builder.Entity<RefundAddressesData>()
.HasIndex(o => o.InvoiceDataId); .HasIndex(o => o.InvoiceDataId);
builder.Entity<UserStore>()
.HasOne(o => o.StoreData)
.WithMany(i => i.UserStores).OnDelete(DeleteBehavior.Cascade);
builder.Entity<UserStore>() builder.Entity<UserStore>()
.HasKey(t => new .HasKey(t => new
{ {
@@ -117,9 +130,16 @@ namespace BTCPayServer.Data
t.StoreDataId t.StoreDataId
}); });
builder.Entity<APIKeyData>()
.HasOne(o => o.StoreData)
.WithMany(i => i.APIKeys)
.HasForeignKey(i => i.StoreId).OnDelete(DeleteBehavior.Cascade);
builder.Entity<APIKeyData>() builder.Entity<APIKeyData>()
.HasIndex(o => o.StoreId); .HasIndex(o => o.StoreId);
builder.Entity<AppData>()
.HasOne(o => o.StoreData)
.WithMany(i => i.Apps).OnDelete(DeleteBehavior.Cascade);
builder.Entity<AppData>() builder.Entity<AppData>()
.HasOne(a => a.StoreData); .HasOne(a => a.StoreData);
@@ -133,6 +153,10 @@ namespace BTCPayServer.Data
.WithMany(t => t.UserStores) .WithMany(t => t.UserStores)
.HasForeignKey(pt => pt.StoreDataId); .HasForeignKey(pt => pt.StoreDataId);
builder.Entity<AddressInvoiceData>()
.HasOne(o => o.InvoiceData)
.WithMany(i => i.AddressInvoices).OnDelete(DeleteBehavior.Cascade);
builder.Entity<AddressInvoiceData>() builder.Entity<AddressInvoiceData>()
#pragma warning disable CS0618 #pragma warning disable CS0618
.HasKey(o => o.Address); .HasKey(o => o.Address);
@@ -141,12 +165,24 @@ namespace BTCPayServer.Data
builder.Entity<PairingCodeData>() builder.Entity<PairingCodeData>()
.HasKey(o => o.Id); .HasKey(o => o.Id);
builder.Entity<PendingInvoiceData>()
.HasOne(o => o.InvoiceData)
.WithMany(o => o.PendingInvoices)
.HasForeignKey(o => o.Id).OnDelete(DeleteBehavior.Cascade);
builder.Entity<PairedSINData>()
.HasOne(o => o.StoreData)
.WithMany(i => i.PairedSINs).OnDelete(DeleteBehavior.Cascade);
builder.Entity<PairedSINData>(b => builder.Entity<PairedSINData>(b =>
{ {
b.HasIndex(o => o.SIN); b.HasIndex(o => o.SIN);
b.HasIndex(o => o.StoreDataId); b.HasIndex(o => o.StoreDataId);
}); });
builder.Entity<HistoricalAddressInvoiceData>()
.HasOne(o => o.InvoiceData)
.WithMany(i => i.HistoricalAddressInvoices).OnDelete(DeleteBehavior.Cascade);
builder.Entity<HistoricalAddressInvoiceData>() builder.Entity<HistoricalAddressInvoiceData>()
.HasKey(o => new .HasKey(o => new
{ {
@@ -156,6 +192,10 @@ namespace BTCPayServer.Data
#pragma warning restore CS0618 #pragma warning restore CS0618
}); });
builder.Entity<InvoiceEventData>()
.HasOne(o => o.InvoiceData)
.WithMany(i => i.Events).OnDelete(DeleteBehavior.Cascade);
builder.Entity<InvoiceEventData>() builder.Entity<InvoiceEventData>()
.HasKey(o => new .HasKey(o => new
{ {

View File

@@ -29,6 +29,15 @@ namespace BTCPayServer.Data
_Type = type; _Type = type;
} }
public DatabaseType Type
{
get
{
return _Type;
}
}
public ApplicationDbContext CreateContext() public ApplicationDbContext CreateContext()
{ {
var builder = new DbContextOptionsBuilder<ApplicationDbContext>(); var builder = new DbContextOptionsBuilder<ApplicationDbContext>();

View File

@@ -12,6 +12,11 @@ namespace BTCPayServer.Data
get; set; get; set;
} }
public InvoiceData InvoiceData
{
get; set;
}
/// <summary> /// <summary>
/// Some crypto currencies share same address prefix /// Some crypto currencies share same address prefix
/// For not having exceptions thrown by two address on different network, we suffix by "#CRYPTOCODE" /// For not having exceptions thrown by two address on different network, we suffix by "#CRYPTOCODE"

View File

@@ -80,5 +80,6 @@ namespace BTCPayServer.Data
{ {
get; set; get; set;
} }
public List<PendingInvoiceData> PendingInvoices { get; set; }
} }
} }

View File

@@ -11,6 +11,10 @@ namespace BTCPayServer.Data
{ {
get; set; get; set;
} }
public InvoiceData InvoiceData
{
get; set;
}
public string UniqueId { get; internal set; } public string UniqueId { get; internal set; }
public DateTimeOffset Timestamp public DateTimeOffset Timestamp
{ {

View File

@@ -21,6 +21,9 @@ namespace BTCPayServer.Data
{ {
get; set; get; set;
} }
public StoreData StoreData { get; set; }
public string Label public string Label
{ {
get; get;

View File

@@ -11,5 +11,6 @@ namespace BTCPayServer.Data
{ {
get; set; get; set;
} }
public InvoiceData InvoiceData { get; set; }
} }
} }

View File

@@ -34,12 +34,13 @@ namespace BTCPayServer.Data
{ {
get; set; get; set;
} }
public List<AppData> Apps public List<AppData> Apps
{ {
get; set; get; set;
} }
public List<InvoiceData> Invoices { get; set; }
[Obsolete("Use GetDerivationStrategies instead")] [Obsolete("Use GetDerivationStrategies instead")]
public string DerivationStrategy public string DerivationStrategy
{ {
@@ -192,6 +193,8 @@ namespace BTCPayServer.Data
} }
[Obsolete("Use GetDefaultCrypto instead")] [Obsolete("Use GetDefaultCrypto instead")]
public string DefaultCrypto { get; set; } public string DefaultCrypto { get; set; }
public List<PairedSINData> PairedSINs { get; set; }
public IEnumerable<APIKeyData> APIKeys { get; set; }
#pragma warning disable CS0618 #pragma warning disable CS0618
public string GetDefaultCrypto() public string GetDefaultCrypto()

View File

@@ -0,0 +1,96 @@
using System;
using Microsoft.Extensions.Logging;
using Microsoft.EntityFrameworkCore;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using BTCPayServer.Data;
using BTCPayServer.Services;
using BTCPayServer.Services.Stores;
using BTCPayServer.Logging;
namespace BTCPayServer.HostedServices
{
public class MigratorHostedService : BaseAsyncService
{
private ApplicationDbContextFactory _DBContextFactory;
private StoreRepository _StoreRepository;
private BTCPayNetworkProvider _NetworkProvider;
private SettingsRepository _Settings;
public MigratorHostedService(
BTCPayNetworkProvider networkProvider,
StoreRepository storeRepository,
ApplicationDbContextFactory dbContextFactory,
SettingsRepository settingsRepository)
{
_DBContextFactory = dbContextFactory;
_StoreRepository = storeRepository;
_NetworkProvider = networkProvider;
_Settings = settingsRepository;
}
internal override Task[] InitializeTasks()
{
return new[]
{
Update()
};
}
private async Task Update()
{
try
{
var settings = (await _Settings.GetSettingAsync<MigrationSettings>()) ?? new MigrationSettings();
if (!settings.DeprecatedLightningConnectionStringCheck)
{
await DepracatedLightningConnectionStringCheck();
settings.DeprecatedLightningConnectionStringCheck = true;
await _Settings.UpdateSetting(settings);
}
if (!settings.UnreachableStoreCheck)
{
await UnreachableStoreCheck();
settings.UnreachableStoreCheck = true;
await _Settings.UpdateSetting(settings);
}
}
catch(Exception ex)
{
Logs.PayServer.LogError(ex, "Error on the MigratorHostedService");
throw;
}
}
private async Task UnreachableStoreCheck()
{
using (var ctx = _DBContextFactory.CreateContext())
{
foreach (var store in await ctx.Stores.Where(s => s.UserStores.Count() == 0).ToArrayAsync())
{
ctx.Stores.Remove(store);
}
await ctx.SaveChangesAsync();
}
}
private async Task DepracatedLightningConnectionStringCheck()
{
using (var ctx = _DBContextFactory.CreateContext())
{
foreach (var store in await ctx.Stores.ToArrayAsync())
{
foreach (var method in store.GetSupportedPaymentMethods(_NetworkProvider).OfType<Payments.Lightning.LightningSupportedPaymentMethod>())
{
var lightning = method.GetLightningUrl();
if (lightning.IsLegacy)
{
method.SetLightningUrl(lightning);
store.SetSupportedPaymentMethod(method.PaymentId, method);
}
}
}
await ctx.SaveChangesAsync();
}
}
}
}

View File

@@ -106,6 +106,7 @@ namespace BTCPayServer.Hosting
services.AddSingleton<CssThemeManager>(); services.AddSingleton<CssThemeManager>();
services.Configure<MvcOptions>((o) => { o.Filters.Add(new ContentSecurityPolicyCssThemeManager()); }); services.Configure<MvcOptions>((o) => { o.Filters.Add(new ContentSecurityPolicyCssThemeManager()); });
services.AddSingleton<IHostedService, CssThemeManagerHostedService>(); services.AddSingleton<IHostedService, CssThemeManagerHostedService>();
services.AddSingleton<IHostedService, MigratorHostedService>();
services.AddSingleton<Payments.IPaymentMethodHandler<DerivationStrategy>, Payments.Bitcoin.BitcoinLikePaymentHandler>(); services.AddSingleton<Payments.IPaymentMethodHandler<DerivationStrategy>, Payments.Bitcoin.BitcoinLikePaymentHandler>();
services.AddSingleton<IHostedService, Payments.Bitcoin.NBXplorerListener>(); services.AddSingleton<IHostedService, Payments.Bitcoin.NBXplorerListener>();

View File

@@ -0,0 +1,578 @@
// <auto-generated />
using System;
using BTCPayServer.Data;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
namespace BTCPayServer.Migrations
{
[DbContext(typeof(ApplicationDbContext))]
[Migration("20180719095626_CanDeleteStores")]
partial class CanDeleteStores
{
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "2.1.0-rtm-30799");
modelBuilder.Entity("BTCPayServer.Data.AddressInvoiceData", b =>
{
b.Property<string>("Address")
.ValueGeneratedOnAdd();
b.Property<DateTimeOffset?>("CreatedTime");
b.Property<string>("InvoiceDataId");
b.HasKey("Address");
b.HasIndex("InvoiceDataId");
b.ToTable("AddressInvoices");
});
modelBuilder.Entity("BTCPayServer.Data.APIKeyData", b =>
{
b.Property<string>("Id")
.ValueGeneratedOnAdd()
.HasMaxLength(50);
b.Property<string>("StoreId")
.HasMaxLength(50);
b.HasKey("Id");
b.HasIndex("StoreId");
b.ToTable("ApiKeys");
});
modelBuilder.Entity("BTCPayServer.Data.AppData", b =>
{
b.Property<string>("Id")
.ValueGeneratedOnAdd();
b.Property<string>("AppType");
b.Property<DateTimeOffset>("Created");
b.Property<string>("Name");
b.Property<string>("Settings");
b.Property<string>("StoreDataId");
b.HasKey("Id");
b.HasIndex("StoreDataId");
b.ToTable("Apps");
});
modelBuilder.Entity("BTCPayServer.Data.HistoricalAddressInvoiceData", b =>
{
b.Property<string>("InvoiceDataId");
b.Property<string>("Address");
b.Property<DateTimeOffset>("Assigned");
b.Property<string>("CryptoCode");
b.Property<DateTimeOffset?>("UnAssigned");
b.HasKey("InvoiceDataId", "Address");
b.ToTable("HistoricalAddressInvoices");
});
modelBuilder.Entity("BTCPayServer.Data.InvoiceData", b =>
{
b.Property<string>("Id")
.ValueGeneratedOnAdd();
b.Property<byte[]>("Blob");
b.Property<DateTimeOffset>("Created");
b.Property<string>("CustomerEmail");
b.Property<string>("ExceptionStatus");
b.Property<string>("ItemCode");
b.Property<string>("OrderId");
b.Property<string>("Status");
b.Property<string>("StoreDataId");
b.HasKey("Id");
b.HasIndex("StoreDataId");
b.ToTable("Invoices");
});
modelBuilder.Entity("BTCPayServer.Data.InvoiceEventData", b =>
{
b.Property<string>("InvoiceDataId");
b.Property<string>("UniqueId");
b.Property<string>("Message");
b.Property<DateTimeOffset>("Timestamp");
b.HasKey("InvoiceDataId", "UniqueId");
b.ToTable("InvoiceEvents");
});
modelBuilder.Entity("BTCPayServer.Data.PairedSINData", b =>
{
b.Property<string>("Id")
.ValueGeneratedOnAdd();
b.Property<string>("Facade");
b.Property<string>("Label");
b.Property<DateTimeOffset>("PairingTime");
b.Property<string>("SIN");
b.Property<string>("StoreDataId");
b.HasKey("Id");
b.HasIndex("SIN");
b.HasIndex("StoreDataId");
b.ToTable("PairedSINData");
});
modelBuilder.Entity("BTCPayServer.Data.PairingCodeData", b =>
{
b.Property<string>("Id")
.ValueGeneratedOnAdd();
b.Property<DateTime>("DateCreated");
b.Property<DateTimeOffset>("Expiration");
b.Property<string>("Facade");
b.Property<string>("Label");
b.Property<string>("SIN");
b.Property<string>("StoreDataId");
b.Property<string>("TokenValue");
b.HasKey("Id");
b.ToTable("PairingCodes");
});
modelBuilder.Entity("BTCPayServer.Data.PaymentData", b =>
{
b.Property<string>("Id")
.ValueGeneratedOnAdd();
b.Property<bool>("Accounted");
b.Property<byte[]>("Blob");
b.Property<string>("InvoiceDataId");
b.HasKey("Id");
b.HasIndex("InvoiceDataId");
b.ToTable("Payments");
});
modelBuilder.Entity("BTCPayServer.Data.PendingInvoiceData", b =>
{
b.Property<string>("Id");
b.HasKey("Id");
b.ToTable("PendingInvoices");
});
modelBuilder.Entity("BTCPayServer.Data.RefundAddressesData", b =>
{
b.Property<string>("Id")
.ValueGeneratedOnAdd();
b.Property<byte[]>("Blob");
b.Property<string>("InvoiceDataId");
b.HasKey("Id");
b.HasIndex("InvoiceDataId");
b.ToTable("RefundAddresses");
});
modelBuilder.Entity("BTCPayServer.Data.SettingData", b =>
{
b.Property<string>("Id")
.ValueGeneratedOnAdd();
b.Property<string>("Value");
b.HasKey("Id");
b.ToTable("Settings");
});
modelBuilder.Entity("BTCPayServer.Data.StoreData", b =>
{
b.Property<string>("Id")
.ValueGeneratedOnAdd();
b.Property<string>("DefaultCrypto");
b.Property<string>("DerivationStrategies");
b.Property<string>("DerivationStrategy");
b.Property<int>("SpeedPolicy");
b.Property<byte[]>("StoreBlob");
b.Property<byte[]>("StoreCertificate");
b.Property<string>("StoreName");
b.Property<string>("StoreWebsite");
b.HasKey("Id");
b.ToTable("Stores");
});
modelBuilder.Entity("BTCPayServer.Data.UserStore", b =>
{
b.Property<string>("ApplicationUserId");
b.Property<string>("StoreDataId");
b.Property<string>("Role");
b.HasKey("ApplicationUserId", "StoreDataId");
b.HasIndex("StoreDataId");
b.ToTable("UserStore");
});
modelBuilder.Entity("BTCPayServer.Models.ApplicationUser", b =>
{
b.Property<string>("Id")
.ValueGeneratedOnAdd();
b.Property<int>("AccessFailedCount");
b.Property<string>("ConcurrencyStamp")
.IsConcurrencyToken();
b.Property<string>("Email")
.HasMaxLength(256);
b.Property<bool>("EmailConfirmed");
b.Property<bool>("LockoutEnabled");
b.Property<DateTimeOffset?>("LockoutEnd");
b.Property<string>("NormalizedEmail")
.HasMaxLength(256);
b.Property<string>("NormalizedUserName")
.HasMaxLength(256);
b.Property<string>("PasswordHash");
b.Property<string>("PhoneNumber");
b.Property<bool>("PhoneNumberConfirmed");
b.Property<bool>("RequiresEmailConfirmation");
b.Property<string>("SecurityStamp");
b.Property<bool>("TwoFactorEnabled");
b.Property<string>("UserName")
.HasMaxLength(256);
b.HasKey("Id");
b.HasIndex("NormalizedEmail")
.HasName("EmailIndex");
b.HasIndex("NormalizedUserName")
.IsUnique()
.HasName("UserNameIndex");
b.ToTable("AspNetUsers");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b =>
{
b.Property<string>("Id")
.ValueGeneratedOnAdd();
b.Property<string>("ConcurrencyStamp")
.IsConcurrencyToken();
b.Property<string>("Name")
.HasMaxLength(256);
b.Property<string>("NormalizedName")
.HasMaxLength(256);
b.HasKey("Id");
b.HasIndex("NormalizedName")
.IsUnique()
.HasName("RoleNameIndex");
b.ToTable("AspNetRoles");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd();
b.Property<string>("ClaimType");
b.Property<string>("ClaimValue");
b.Property<string>("RoleId")
.IsRequired();
b.HasKey("Id");
b.HasIndex("RoleId");
b.ToTable("AspNetRoleClaims");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd();
b.Property<string>("ClaimType");
b.Property<string>("ClaimValue");
b.Property<string>("UserId")
.IsRequired();
b.HasKey("Id");
b.HasIndex("UserId");
b.ToTable("AspNetUserClaims");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
{
b.Property<string>("LoginProvider");
b.Property<string>("ProviderKey");
b.Property<string>("ProviderDisplayName");
b.Property<string>("UserId")
.IsRequired();
b.HasKey("LoginProvider", "ProviderKey");
b.HasIndex("UserId");
b.ToTable("AspNetUserLogins");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<string>", b =>
{
b.Property<string>("UserId");
b.Property<string>("RoleId");
b.HasKey("UserId", "RoleId");
b.HasIndex("RoleId");
b.ToTable("AspNetUserRoles");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<string>", b =>
{
b.Property<string>("UserId");
b.Property<string>("LoginProvider");
b.Property<string>("Name");
b.Property<string>("Value");
b.HasKey("UserId", "LoginProvider", "Name");
b.ToTable("AspNetUserTokens");
});
modelBuilder.Entity("BTCPayServer.Data.AddressInvoiceData", b =>
{
b.HasOne("BTCPayServer.Data.InvoiceData", "InvoiceData")
.WithMany("AddressInvoices")
.HasForeignKey("InvoiceDataId")
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("BTCPayServer.Data.APIKeyData", b =>
{
b.HasOne("BTCPayServer.Data.StoreData", "StoreData")
.WithMany("APIKeys")
.HasForeignKey("StoreId")
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("BTCPayServer.Data.AppData", b =>
{
b.HasOne("BTCPayServer.Data.StoreData", "StoreData")
.WithMany("Apps")
.HasForeignKey("StoreDataId")
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("BTCPayServer.Data.HistoricalAddressInvoiceData", b =>
{
b.HasOne("BTCPayServer.Data.InvoiceData", "InvoiceData")
.WithMany("HistoricalAddressInvoices")
.HasForeignKey("InvoiceDataId")
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("BTCPayServer.Data.InvoiceData", b =>
{
b.HasOne("BTCPayServer.Data.StoreData", "StoreData")
.WithMany("Invoices")
.HasForeignKey("StoreDataId")
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("BTCPayServer.Data.InvoiceEventData", b =>
{
b.HasOne("BTCPayServer.Data.InvoiceData", "InvoiceData")
.WithMany("Events")
.HasForeignKey("InvoiceDataId")
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("BTCPayServer.Data.PairedSINData", b =>
{
b.HasOne("BTCPayServer.Data.StoreData", "StoreData")
.WithMany("PairedSINs")
.HasForeignKey("StoreDataId")
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("BTCPayServer.Data.PaymentData", b =>
{
b.HasOne("BTCPayServer.Data.InvoiceData", "InvoiceData")
.WithMany("Payments")
.HasForeignKey("InvoiceDataId")
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("BTCPayServer.Data.PendingInvoiceData", b =>
{
b.HasOne("BTCPayServer.Data.InvoiceData", "InvoiceData")
.WithMany("PendingInvoices")
.HasForeignKey("Id")
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("BTCPayServer.Data.RefundAddressesData", b =>
{
b.HasOne("BTCPayServer.Data.InvoiceData", "InvoiceData")
.WithMany("RefundAddresses")
.HasForeignKey("InvoiceDataId")
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("BTCPayServer.Data.UserStore", b =>
{
b.HasOne("BTCPayServer.Models.ApplicationUser", "ApplicationUser")
.WithMany("UserStores")
.HasForeignKey("ApplicationUserId")
.OnDelete(DeleteBehavior.Cascade);
b.HasOne("BTCPayServer.Data.StoreData", "StoreData")
.WithMany("UserStores")
.HasForeignKey("StoreDataId")
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
{
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole")
.WithMany()
.HasForeignKey("RoleId")
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b =>
{
b.HasOne("BTCPayServer.Models.ApplicationUser")
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
{
b.HasOne("BTCPayServer.Models.ApplicationUser")
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<string>", b =>
{
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole")
.WithMany()
.HasForeignKey("RoleId")
.OnDelete(DeleteBehavior.Cascade);
b.HasOne("BTCPayServer.Models.ApplicationUser")
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<string>", b =>
{
b.HasOne("BTCPayServer.Models.ApplicationUser")
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade);
});
#pragma warning restore 612, 618
}
}
}

View File

@@ -0,0 +1,169 @@
using Microsoft.EntityFrameworkCore.Migrations;
namespace BTCPayServer.Migrations
{
public partial class CanDeleteStores : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropForeignKey(
name: "FK_AddressInvoices_Invoices_InvoiceDataId",
table: "AddressInvoices");
migrationBuilder.DropForeignKey(
name: "FK_Apps_Stores_StoreDataId",
table: "Apps");
migrationBuilder.DropForeignKey(
name: "FK_Invoices_Stores_StoreDataId",
table: "Invoices");
migrationBuilder.DropForeignKey(
name: "FK_Payments_Invoices_InvoiceDataId",
table: "Payments");
migrationBuilder.DropForeignKey(
name: "FK_RefundAddresses_Invoices_InvoiceDataId",
table: "RefundAddresses");
migrationBuilder.AddForeignKey(
name: "FK_AddressInvoices_Invoices_InvoiceDataId",
table: "AddressInvoices",
column: "InvoiceDataId",
principalTable: "Invoices",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
migrationBuilder.AddForeignKey(
name: "FK_ApiKeys_Stores_StoreId",
table: "ApiKeys",
column: "StoreId",
principalTable: "Stores",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
migrationBuilder.AddForeignKey(
name: "FK_Apps_Stores_StoreDataId",
table: "Apps",
column: "StoreDataId",
principalTable: "Stores",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
migrationBuilder.AddForeignKey(
name: "FK_Invoices_Stores_StoreDataId",
table: "Invoices",
column: "StoreDataId",
principalTable: "Stores",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
migrationBuilder.AddForeignKey(
name: "FK_PairedSINData_Stores_StoreDataId",
table: "PairedSINData",
column: "StoreDataId",
principalTable: "Stores",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
migrationBuilder.AddForeignKey(
name: "FK_Payments_Invoices_InvoiceDataId",
table: "Payments",
column: "InvoiceDataId",
principalTable: "Invoices",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
migrationBuilder.AddForeignKey(
name: "FK_PendingInvoices_Invoices_Id",
table: "PendingInvoices",
column: "Id",
principalTable: "Invoices",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
migrationBuilder.AddForeignKey(
name: "FK_RefundAddresses_Invoices_InvoiceDataId",
table: "RefundAddresses",
column: "InvoiceDataId",
principalTable: "Invoices",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropForeignKey(
name: "FK_AddressInvoices_Invoices_InvoiceDataId",
table: "AddressInvoices");
migrationBuilder.DropForeignKey(
name: "FK_ApiKeys_Stores_StoreId",
table: "ApiKeys");
migrationBuilder.DropForeignKey(
name: "FK_Apps_Stores_StoreDataId",
table: "Apps");
migrationBuilder.DropForeignKey(
name: "FK_Invoices_Stores_StoreDataId",
table: "Invoices");
migrationBuilder.DropForeignKey(
name: "FK_PairedSINData_Stores_StoreDataId",
table: "PairedSINData");
migrationBuilder.DropForeignKey(
name: "FK_Payments_Invoices_InvoiceDataId",
table: "Payments");
migrationBuilder.DropForeignKey(
name: "FK_PendingInvoices_Invoices_Id",
table: "PendingInvoices");
migrationBuilder.DropForeignKey(
name: "FK_RefundAddresses_Invoices_InvoiceDataId",
table: "RefundAddresses");
migrationBuilder.AddForeignKey(
name: "FK_AddressInvoices_Invoices_InvoiceDataId",
table: "AddressInvoices",
column: "InvoiceDataId",
principalTable: "Invoices",
principalColumn: "Id",
onDelete: ReferentialAction.Restrict);
migrationBuilder.AddForeignKey(
name: "FK_Apps_Stores_StoreDataId",
table: "Apps",
column: "StoreDataId",
principalTable: "Stores",
principalColumn: "Id",
onDelete: ReferentialAction.Restrict);
migrationBuilder.AddForeignKey(
name: "FK_Invoices_Stores_StoreDataId",
table: "Invoices",
column: "StoreDataId",
principalTable: "Stores",
principalColumn: "Id",
onDelete: ReferentialAction.Restrict);
migrationBuilder.AddForeignKey(
name: "FK_Payments_Invoices_InvoiceDataId",
table: "Payments",
column: "InvoiceDataId",
principalTable: "Invoices",
principalColumn: "Id",
onDelete: ReferentialAction.Restrict);
migrationBuilder.AddForeignKey(
name: "FK_RefundAddresses_Invoices_InvoiceDataId",
table: "RefundAddresses",
column: "InvoiceDataId",
principalTable: "Invoices",
principalColumn: "Id",
onDelete: ReferentialAction.Restrict);
}
}
}

View File

@@ -1,13 +1,9 @@
// <auto-generated /> // <auto-generated />
using System;
using BTCPayServer.Data; using BTCPayServer.Data;
using BTCPayServer.Services.Invoices;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Metadata; using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage;
using Microsoft.EntityFrameworkCore.Storage.Internal;
using System;
namespace BTCPayServer.Migrations namespace BTCPayServer.Migrations
{ {
@@ -18,7 +14,7 @@ namespace BTCPayServer.Migrations
{ {
#pragma warning disable 612, 618 #pragma warning disable 612, 618
modelBuilder modelBuilder
.HasAnnotation("ProductVersion", "2.0.2-rtm-10011"); .HasAnnotation("ProductVersion", "2.1.0-rtm-30799");
modelBuilder.Entity("BTCPayServer.Data.AddressInvoiceData", b => modelBuilder.Entity("BTCPayServer.Data.AddressInvoiceData", b =>
{ {
@@ -202,8 +198,7 @@ namespace BTCPayServer.Migrations
modelBuilder.Entity("BTCPayServer.Data.PendingInvoiceData", b => modelBuilder.Entity("BTCPayServer.Data.PendingInvoiceData", b =>
{ {
b.Property<string>("Id") b.Property<string>("Id");
.ValueGeneratedOnAdd();
b.HasKey("Id"); b.HasKey("Id");
@@ -442,19 +437,29 @@ namespace BTCPayServer.Migrations
{ {
b.HasOne("BTCPayServer.Data.InvoiceData", "InvoiceData") b.HasOne("BTCPayServer.Data.InvoiceData", "InvoiceData")
.WithMany("AddressInvoices") .WithMany("AddressInvoices")
.HasForeignKey("InvoiceDataId"); .HasForeignKey("InvoiceDataId")
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("BTCPayServer.Data.APIKeyData", b =>
{
b.HasOne("BTCPayServer.Data.StoreData", "StoreData")
.WithMany("APIKeys")
.HasForeignKey("StoreId")
.OnDelete(DeleteBehavior.Cascade);
}); });
modelBuilder.Entity("BTCPayServer.Data.AppData", b => modelBuilder.Entity("BTCPayServer.Data.AppData", b =>
{ {
b.HasOne("BTCPayServer.Data.StoreData", "StoreData") b.HasOne("BTCPayServer.Data.StoreData", "StoreData")
.WithMany("Apps") .WithMany("Apps")
.HasForeignKey("StoreDataId"); .HasForeignKey("StoreDataId")
.OnDelete(DeleteBehavior.Cascade);
}); });
modelBuilder.Entity("BTCPayServer.Data.HistoricalAddressInvoiceData", b => modelBuilder.Entity("BTCPayServer.Data.HistoricalAddressInvoiceData", b =>
{ {
b.HasOne("BTCPayServer.Data.InvoiceData") b.HasOne("BTCPayServer.Data.InvoiceData", "InvoiceData")
.WithMany("HistoricalAddressInvoices") .WithMany("HistoricalAddressInvoices")
.HasForeignKey("InvoiceDataId") .HasForeignKey("InvoiceDataId")
.OnDelete(DeleteBehavior.Cascade); .OnDelete(DeleteBehavior.Cascade);
@@ -463,30 +468,49 @@ namespace BTCPayServer.Migrations
modelBuilder.Entity("BTCPayServer.Data.InvoiceData", b => modelBuilder.Entity("BTCPayServer.Data.InvoiceData", b =>
{ {
b.HasOne("BTCPayServer.Data.StoreData", "StoreData") b.HasOne("BTCPayServer.Data.StoreData", "StoreData")
.WithMany() .WithMany("Invoices")
.HasForeignKey("StoreDataId"); .HasForeignKey("StoreDataId")
.OnDelete(DeleteBehavior.Cascade);
}); });
modelBuilder.Entity("BTCPayServer.Data.InvoiceEventData", b => modelBuilder.Entity("BTCPayServer.Data.InvoiceEventData", b =>
{ {
b.HasOne("BTCPayServer.Data.InvoiceData") b.HasOne("BTCPayServer.Data.InvoiceData", "InvoiceData")
.WithMany("Events") .WithMany("Events")
.HasForeignKey("InvoiceDataId") .HasForeignKey("InvoiceDataId")
.OnDelete(DeleteBehavior.Cascade); .OnDelete(DeleteBehavior.Cascade);
}); });
modelBuilder.Entity("BTCPayServer.Data.PairedSINData", b =>
{
b.HasOne("BTCPayServer.Data.StoreData", "StoreData")
.WithMany("PairedSINs")
.HasForeignKey("StoreDataId")
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("BTCPayServer.Data.PaymentData", b => modelBuilder.Entity("BTCPayServer.Data.PaymentData", b =>
{ {
b.HasOne("BTCPayServer.Data.InvoiceData", "InvoiceData") b.HasOne("BTCPayServer.Data.InvoiceData", "InvoiceData")
.WithMany("Payments") .WithMany("Payments")
.HasForeignKey("InvoiceDataId"); .HasForeignKey("InvoiceDataId")
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("BTCPayServer.Data.PendingInvoiceData", b =>
{
b.HasOne("BTCPayServer.Data.InvoiceData", "InvoiceData")
.WithMany("PendingInvoices")
.HasForeignKey("Id")
.OnDelete(DeleteBehavior.Cascade);
}); });
modelBuilder.Entity("BTCPayServer.Data.RefundAddressesData", b => modelBuilder.Entity("BTCPayServer.Data.RefundAddressesData", b =>
{ {
b.HasOne("BTCPayServer.Data.InvoiceData", "InvoiceData") b.HasOne("BTCPayServer.Data.InvoiceData", "InvoiceData")
.WithMany("RefundAddresses") .WithMany("RefundAddresses")
.HasForeignKey("InvoiceDataId"); .HasForeignKey("InvoiceDataId")
.OnDelete(DeleteBehavior.Cascade);
}); });
modelBuilder.Entity("BTCPayServer.Data.UserStore", b => modelBuilder.Entity("BTCPayServer.Data.UserStore", b =>

View File

@@ -207,6 +207,8 @@ namespace BTCPayServer.Payments.Bitcoin
async Task<InvoiceEntity> UpdatePaymentStates(BTCPayWallet wallet, string invoiceId) async Task<InvoiceEntity> UpdatePaymentStates(BTCPayWallet wallet, string invoiceId)
{ {
var invoice = await _InvoiceRepository.GetInvoice(null, invoiceId, false); var invoice = await _InvoiceRepository.GetInvoice(null, invoiceId, false);
if (invoice == null)
return null;
List<PaymentEntity> updatedPaymentEntities = new List<PaymentEntity>(); List<PaymentEntity> updatedPaymentEntities = new List<PaymentEntity>();
var transactions = await wallet.GetTransactions(GetAllBitcoinPaymentData(invoice) var transactions = await wallet.GetTransactions(GetAllBitcoinPaymentData(invoice)
.Select(p => p.Outpoint.Hash) .Select(p => p.Outpoint.Hash)
@@ -315,6 +317,8 @@ namespace BTCPayServer.Payments.Bitcoin
foreach (var invoiceId in invoices) foreach (var invoiceId in invoices)
{ {
var invoice = await _InvoiceRepository.GetInvoice(null, invoiceId, true); var invoice = await _InvoiceRepository.GetInvoice(null, invoiceId, true);
if (invoice == null)
continue;
var alreadyAccounted = GetAllBitcoinPaymentData(invoice).Select(p => p.Outpoint).ToHashSet(); var alreadyAccounted = GetAllBitcoinPaymentData(invoice).Select(p => p.Outpoint).ToHashSet();
var strategy = GetDerivationStrategy(invoice, network); var strategy = GetDerivationStrategy(invoice, network);
if (strategy == null) if (strategy == null)
@@ -332,8 +336,12 @@ namespace BTCPayServer.Payments.Bitcoin
var payment = await _InvoiceRepository.AddPayment(invoice.Id, coin.Timestamp, paymentData, network.CryptoCode).ConfigureAwait(false); var payment = await _InvoiceRepository.AddPayment(invoice.Id, coin.Timestamp, paymentData, network.CryptoCode).ConfigureAwait(false);
alreadyAccounted.Add(coin.Coin.Outpoint); alreadyAccounted.Add(coin.Coin.Outpoint);
if (payment != null) if (payment != null)
{
invoice = await ReceivedPayment(wallet, invoice, payment, strategy); invoice = await ReceivedPayment(wallet, invoice, payment, strategy);
totalPayment++; if(invoice == null)
continue;
totalPayment++;
}
} }
} }
return totalPayment; return totalPayment;
@@ -350,6 +358,8 @@ namespace BTCPayServer.Payments.Bitcoin
{ {
var paymentData = (BitcoinLikePaymentData)payment.GetCryptoPaymentData(); var paymentData = (BitcoinLikePaymentData)payment.GetCryptoPaymentData();
invoice = (await UpdatePaymentStates(wallet, invoice.Id)); invoice = (await UpdatePaymentStates(wallet, invoice.Id));
if (invoice == null)
return null;
var paymentMethod = invoice.GetPaymentMethod(wallet.Network, PaymentTypes.BTCLike, _ExplorerClients.NetworkProviders); var paymentMethod = invoice.GetPaymentMethod(wallet.Network, PaymentTypes.BTCLike, _ExplorerClients.NetworkProviders);
if (paymentMethod != null && if (paymentMethod != null &&
paymentMethod.GetPaymentMethodDetails() is BitcoinLikeOnChainPaymentMethod btc && paymentMethod.GetPaymentMethodDetails() is BitcoinLikeOnChainPaymentMethod btc &&

View File

@@ -0,0 +1,13 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace BTCPayServer.Services
{
public class MigrationSettings
{
public bool UnreachableStoreCheck { get; set; }
public bool DeprecatedLightningConnectionStringCheck { get; set; }
}
}

View File

@@ -169,5 +169,18 @@ namespace BTCPayServer.Services.Stores
await ctx.SaveChangesAsync().ConfigureAwait(false); await ctx.SaveChangesAsync().ConfigureAwait(false);
} }
} }
public async Task<bool> DeleteStore(string storeId)
{
using (var ctx = _ContextFactory.CreateContext())
{
var store = await ctx.Stores.FindAsync(storeId);
if (store == null)
return false;
ctx.Stores.Remove(store);
await ctx.SaveChangesAsync();
return true;
}
}
} }
} }