mirror of
https://github.com/btcpayserver/btcpayserver.git
synced 2025-02-20 13:34:37 +01:00
Migrate Payouts to new format (#5989)
* Migrate Payouts to new format * Rename PayoutData column to PayoutMethodId
This commit is contained in:
parent
c56b660c92
commit
a295e123bc
21 changed files with 303 additions and 151 deletions
|
@ -37,6 +37,10 @@ namespace BTCPayServer.Data
|
|||
{
|
||||
paymentData.Migrate();
|
||||
}
|
||||
else if (entity is PayoutData payoutData && payoutData.Currency is null)
|
||||
{
|
||||
payoutData.Migrate();
|
||||
}
|
||||
return entity;
|
||||
}
|
||||
}
|
||||
|
|
18
BTCPayServer.Data/Data/PayoutData.Migration.cs
Normal file
18
BTCPayServer.Data/Data/PayoutData.Migration.cs
Normal file
|
@ -0,0 +1,18 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace BTCPayServer.Data
|
||||
{
|
||||
public partial class PayoutData
|
||||
{
|
||||
public void Migrate()
|
||||
{
|
||||
PayoutMethodId = MigrationExtensions.MigratePaymentMethodId(PayoutMethodId);
|
||||
// Could only be BTC-LN or BTC-CHAIN, so we extract the crypto currency
|
||||
Currency = PayoutMethodId.Split('-')[0];
|
||||
}
|
||||
}
|
||||
}
|
|
@ -10,7 +10,7 @@ using NBitcoin;
|
|||
|
||||
namespace BTCPayServer.Data
|
||||
{
|
||||
public class PayoutData
|
||||
public partial class PayoutData
|
||||
{
|
||||
[Key]
|
||||
[MaxLength(30)]
|
||||
|
@ -18,12 +18,13 @@ namespace BTCPayServer.Data
|
|||
public DateTimeOffset Date { get; set; }
|
||||
public string PullPaymentDataId { get; set; }
|
||||
public string StoreDataId { get; set; }
|
||||
public string Currency { get; set; }
|
||||
public PullPaymentData PullPaymentData { get; set; }
|
||||
[MaxLength(20)]
|
||||
public PayoutState State { get; set; }
|
||||
[MaxLength(20)]
|
||||
[Required]
|
||||
public string PaymentMethodId { get; set; }
|
||||
public string PayoutMethodId { get; set; }
|
||||
public string Blob { get; set; }
|
||||
public string Proof { get; set; }
|
||||
#nullable enable
|
||||
|
|
|
@ -0,0 +1,29 @@
|
|||
using BTCPayServer.Data;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace BTCPayServer.Migrations
|
||||
{
|
||||
[DbContext(typeof(ApplicationDbContext))]
|
||||
[Migration("20240520042729_payoutsmigration")]
|
||||
public partial class payoutsmigration : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "Currency",
|
||||
table: "Payouts",
|
||||
type: "text",
|
||||
nullable: true);
|
||||
migrationBuilder.RenameColumn("PaymentMethodId", "Payouts", "PayoutMethodId");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
// <auto-generated />
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using BTCPayServer.Data;
|
||||
|
@ -567,13 +567,16 @@ namespace BTCPayServer.Migrations
|
|||
b.Property<string>("Blob")
|
||||
.HasColumnType("JSONB");
|
||||
|
||||
b.Property<string>("Currency")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<DateTimeOffset>("Date")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("Destination")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("PaymentMethodId")
|
||||
b.Property<string>("PayoutMethodId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(20)
|
||||
.HasColumnType("character varying(20)");
|
||||
|
|
|
@ -372,9 +372,8 @@ namespace BTCPayServer.Controllers.Greenfield
|
|||
Metadata = blob.Metadata?? new JObject(),
|
||||
};
|
||||
model.Destination = blob.Destination;
|
||||
model.PaymentMethod = p.PaymentMethodId;
|
||||
var currency = this._payoutHandlers.TryGet(p.GetPayoutMethodId())?.Currency;
|
||||
model.CryptoCode = currency;
|
||||
model.PaymentMethod = p.PayoutMethodId;
|
||||
model.CryptoCode = p.Currency;
|
||||
model.PaymentProof = p.GetProofBlobJson();
|
||||
return model;
|
||||
}
|
||||
|
|
|
@ -179,7 +179,7 @@ namespace BTCPayServer
|
|||
lightningHandler.CreateLightningClient(pm);
|
||||
var payResult = await UILightningLikePayoutController.TrypayBolt(client,
|
||||
claimResponse.PayoutData.GetBlob(_btcPayNetworkJsonSerializerSettings),
|
||||
claimResponse.PayoutData, result, payoutHandler.Currency, cancellationToken);
|
||||
claimResponse.PayoutData, result, cancellationToken);
|
||||
|
||||
switch (payResult.Result)
|
||||
{
|
||||
|
|
|
@ -118,7 +118,7 @@ namespace BTCPayServer.Controllers
|
|||
Currency = blob.Currency,
|
||||
Status = entity.Entity.State,
|
||||
Destination = entity.Blob.Destination,
|
||||
PaymentMethod = PaymentMethodId.Parse(entity.Entity.PaymentMethodId),
|
||||
PaymentMethod = PaymentMethodId.Parse(entity.Entity.PayoutMethodId),
|
||||
Link = entity.ProofBlob?.Link,
|
||||
TransactionId = entity.ProofBlob?.Id
|
||||
}).ToList()
|
||||
|
|
|
@ -509,14 +509,14 @@ namespace BTCPayServer.Controllers
|
|||
vm.PullPaymentName = (await ctx.PullPayments.FindAsync(pullPaymentId)).GetBlob().Name;
|
||||
}
|
||||
|
||||
vm.PayoutMethodCount = (await payoutRequest.GroupBy(data => data.PaymentMethodId)
|
||||
vm.PayoutMethodCount = (await payoutRequest.GroupBy(data => data.PayoutMethodId)
|
||||
.Select(datas => new { datas.Key, Count = datas.Count() }).ToListAsync())
|
||||
.ToDictionary(datas => datas.Key, arg => arg.Count);
|
||||
|
||||
if (vm.PayoutMethodId != null)
|
||||
{
|
||||
var pmiStr = vm.PayoutMethodId;
|
||||
payoutRequest = payoutRequest.Where(p => p.PaymentMethodId == pmiStr);
|
||||
payoutRequest = payoutRequest.Where(p => p.PayoutMethodId == pmiStr);
|
||||
}
|
||||
vm.PayoutStateCount = payoutRequest.GroupBy(data => data.State)
|
||||
.Select(e => new { e.Key, Count = e.Count() })
|
||||
|
@ -563,7 +563,6 @@ namespace BTCPayServer.Controllers
|
|||
{
|
||||
payoutSourceLink = Url.Action("ViewPullPayment", "UIPullPayment", new { pullPaymentId = item.PullPayment?.Id });
|
||||
}
|
||||
var pCurrency = _payoutHandlers.TryGet(PayoutMethodId.Parse(item.Payout.PaymentMethodId))?.Currency;
|
||||
|
||||
var m = new PayoutsModel.PayoutModel
|
||||
{
|
||||
|
@ -572,7 +571,7 @@ namespace BTCPayServer.Controllers
|
|||
SourceLink = payoutSourceLink,
|
||||
Date = item.Payout.Date,
|
||||
PayoutId = item.Payout.Id,
|
||||
Amount = _displayFormatter.Currency(payoutBlob.Amount, ppBlob?.Currency ?? pCurrency),
|
||||
Amount = _displayFormatter.Currency(payoutBlob.Amount, ppBlob?.Currency ?? item.Payout.Currency),
|
||||
Destination = payoutBlob.Destination
|
||||
};
|
||||
var handler = _payoutHandlers
|
||||
|
|
|
@ -227,7 +227,7 @@ public class BitcoinLikePayoutHandler : IPayoutHandler, IHasNetwork
|
|||
Stores = new[] { storeId },
|
||||
PayoutIds = payoutIds
|
||||
}, context)).Where(data =>
|
||||
PayoutMethodId.TryParse(data.PaymentMethodId, out var payoutMethodId) &&
|
||||
PayoutMethodId.TryParse(data.PayoutMethodId, out var payoutMethodId) &&
|
||||
payoutMethodId == PayoutMethodId)
|
||||
.Select(data => (data, ParseProof(data) as PayoutTransactionOnChainBlob)).Where(tuple => tuple.Item2 != null && tuple.Item2.TransactionId != null && tuple.Item2.Accounted == false);
|
||||
foreach (var valueTuple in payouts)
|
||||
|
@ -252,7 +252,7 @@ public class BitcoinLikePayoutHandler : IPayoutHandler, IHasNetwork
|
|||
Stores = new[] { storeId },
|
||||
PayoutIds = payoutIds
|
||||
}, context)).Where(data =>
|
||||
PayoutMethodId.TryParse(data.PaymentMethodId, out var payoutMethodId) &&
|
||||
PayoutMethodId.TryParse(data.PayoutMethodId, out var payoutMethodId) &&
|
||||
payoutMethodId == PayoutMethodId)
|
||||
.Select(data => (data, ParseProof(data) as PayoutTransactionOnChainBlob)).Where(tuple => tuple.Item2 != null && tuple.Item2.TransactionId != null && tuple.Item2.Accounted == true);
|
||||
foreach (var valueTuple in payouts)
|
||||
|
@ -285,7 +285,7 @@ public class BitcoinLikePayoutHandler : IPayoutHandler, IHasNetwork
|
|||
ctx.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking;
|
||||
var payouts = await ctx.Payouts.Include(data => data.PullPaymentData)
|
||||
.Where(data => payoutIds.Contains(data.Id)
|
||||
&& PayoutMethodId.ToString() == data.PaymentMethodId
|
||||
&& PayoutMethodId.ToString() == data.PayoutMethodId
|
||||
&& data.State == PayoutState.AwaitingPayment)
|
||||
.ToListAsync();
|
||||
|
||||
|
@ -428,7 +428,7 @@ public class BitcoinLikePayoutHandler : IPayoutHandler, IHasNetwork
|
|||
.Include(o => o.StoreData)
|
||||
.Include(o => o.PullPaymentData)
|
||||
.Where(p => p.State == PayoutState.AwaitingPayment)
|
||||
.Where(p => p.PaymentMethodId == paymentMethodId.ToString())
|
||||
.Where(p => p.PayoutMethodId == paymentMethodId.ToString())
|
||||
#pragma warning disable CA1307 // Specify StringComparison
|
||||
.Where(p => destination.Equals(p.Destination))
|
||||
#pragma warning restore CA1307 // Specify StringComparison
|
||||
|
@ -474,7 +474,7 @@ public class BitcoinLikePayoutHandler : IPayoutHandler, IHasNetwork
|
|||
await _notificationSender.SendNotification(new StoreScope(payout.StoreDataId),
|
||||
new ExternalPayoutTransactionNotification()
|
||||
{
|
||||
PaymentMethod = payout.PaymentMethodId,
|
||||
PaymentMethod = payout.PayoutMethodId,
|
||||
PayoutId = payout.Id,
|
||||
StoreId = payout.StoreDataId
|
||||
});
|
||||
|
|
|
@ -86,7 +86,7 @@ namespace BTCPayServer.Data.Payouts.LightningLike
|
|||
.Where(data =>
|
||||
payoutIds.Contains(data.Id) &&
|
||||
data.State == PayoutState.AwaitingPayment &&
|
||||
data.PaymentMethodId == pmiStr)
|
||||
data.PayoutMethodId == pmiStr)
|
||||
.ToListAsync())
|
||||
.Where(payout =>
|
||||
{
|
||||
|
@ -185,13 +185,13 @@ namespace BTCPayServer.Data.Payouts.LightningLike
|
|||
}
|
||||
else
|
||||
{
|
||||
result = await TrypayBolt(client, blob, payoutData, lnurlResult.Item1, payoutHandler.Currency, cancellationToken);
|
||||
result = await TrypayBolt(client, blob, payoutData, lnurlResult.Item1, cancellationToken);
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case BoltInvoiceClaimDestination item1:
|
||||
result = await TrypayBolt(client, blob, payoutData, item1.PaymentRequest, payoutHandler.Currency, cancellationToken);
|
||||
result = await TrypayBolt(client, blob, payoutData, item1.PaymentRequest, cancellationToken);
|
||||
|
||||
break;
|
||||
default:
|
||||
|
@ -276,8 +276,7 @@ namespace BTCPayServer.Data.Payouts.LightningLike
|
|||
}
|
||||
|
||||
public static async Task<ResultVM> TrypayBolt(
|
||||
ILightningClient lightningClient, PayoutBlob payoutBlob, PayoutData payoutData, BOLT11PaymentRequest bolt11PaymentRequest,
|
||||
string payoutCurrency, CancellationToken cancellationToken)
|
||||
ILightningClient lightningClient, PayoutBlob payoutBlob, PayoutData payoutData, BOLT11PaymentRequest bolt11PaymentRequest, CancellationToken cancellationToken)
|
||||
{
|
||||
var boltAmount = bolt11PaymentRequest.MinimumAmount.ToDecimal(LightMoneyUnit.BTC);
|
||||
if (boltAmount > payoutBlob.CryptoAmount)
|
||||
|
@ -287,7 +286,7 @@ namespace BTCPayServer.Data.Payouts.LightningLike
|
|||
{
|
||||
PayoutId = payoutData.Id,
|
||||
Result = PayResult.Error,
|
||||
Message = $"The BOLT11 invoice amount ({boltAmount} {payoutCurrency}) did not match the payout's amount ({payoutBlob.CryptoAmount.GetValueOrDefault()} {payoutCurrency})",
|
||||
Message = $"The BOLT11 invoice amount ({boltAmount} {payoutData.Currency}) did not match the payout's amount ({payoutBlob.CryptoAmount.GetValueOrDefault()} {payoutData.Currency})",
|
||||
Destination = payoutBlob.Destination
|
||||
};
|
||||
}
|
||||
|
|
|
@ -31,7 +31,7 @@ namespace BTCPayServer.Data
|
|||
|
||||
public static PayoutMethodId GetPayoutMethodId(this PayoutData data)
|
||||
{
|
||||
return PayoutMethodId.TryParse(data.PaymentMethodId, out var pmi) ? pmi : null;
|
||||
return PayoutMethodId.TryParse(data.PayoutMethodId, out var pmi) ? pmi : null;
|
||||
}
|
||||
|
||||
public static string GetPayoutSource(this PayoutData data, BTCPayNetworkJsonSerializerSettings jsonSerializerSettings)
|
||||
|
|
118
BTCPayServer/HostedServices/BlobMigratorHostedService.cs
Normal file
118
BTCPayServer/HostedServices/BlobMigratorHostedService.cs
Normal file
|
@ -0,0 +1,118 @@
|
|||
#nullable enable
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Reflection.Metadata;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Abstractions.Contracts;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.WindowsAzure.Storage.Table;
|
||||
using NBitcoin;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using static BTCPayServer.Controllers.UIInvoiceController;
|
||||
|
||||
namespace BTCPayServer.HostedServices;
|
||||
|
||||
public abstract class BlobMigratorHostedService<TEntity> : IHostedService
|
||||
{
|
||||
public abstract string SettingsKey { get; }
|
||||
internal class Settings
|
||||
{
|
||||
[JsonConverter(typeof(NBitcoin.JsonConverters.DateTimeToUnixTimeConverter))]
|
||||
public DateTimeOffset? Progress { get; set; }
|
||||
public bool Complete { get; set; }
|
||||
}
|
||||
Task? _Migrating;
|
||||
TaskCompletionSource _Cts = new TaskCompletionSource();
|
||||
public BlobMigratorHostedService(
|
||||
ILogger logs,
|
||||
ISettingsRepository settingsRepository,
|
||||
ApplicationDbContextFactory applicationDbContextFactory)
|
||||
{
|
||||
Logs = logs;
|
||||
SettingsRepository = settingsRepository;
|
||||
ApplicationDbContextFactory = applicationDbContextFactory;
|
||||
}
|
||||
|
||||
public ILogger Logs { get; }
|
||||
public ISettingsRepository SettingsRepository { get; }
|
||||
public ApplicationDbContextFactory ApplicationDbContextFactory { get; }
|
||||
|
||||
public Task StartAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
_Migrating = Migrate(cancellationToken);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
public int BatchSize { get; set; } = 1000;
|
||||
|
||||
private async Task Migrate(CancellationToken cancellationToken)
|
||||
{
|
||||
var settings = await SettingsRepository.GetSettingAsync<Settings>(SettingsKey) ?? new Settings();
|
||||
if (settings.Complete is true)
|
||||
return;
|
||||
if (settings.Progress is DateTimeOffset last)
|
||||
Logs.LogInformation($"Migrating from {last}");
|
||||
else
|
||||
Logs.LogInformation("Migrating from the beginning");
|
||||
|
||||
int batchSize = BatchSize;
|
||||
while (!cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
retry:
|
||||
List<TEntity> entities;
|
||||
DateTimeOffset progress;
|
||||
await using (var ctx = ApplicationDbContextFactory.CreateContext())
|
||||
{
|
||||
var query = GetQuery(ctx, settings?.Progress).Take(batchSize);
|
||||
entities = await query.ToListAsync(cancellationToken);
|
||||
if (entities.Count == 0)
|
||||
{
|
||||
await SettingsRepository.UpdateSetting<Settings>(new Settings() { Complete = true }, SettingsKey);
|
||||
Logs.LogInformation("Migration completed");
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
progress = ProcessEntities(ctx, entities);
|
||||
await ctx.SaveChangesAsync();
|
||||
batchSize = BatchSize;
|
||||
}
|
||||
catch (DbUpdateConcurrencyException)
|
||||
{
|
||||
batchSize /= 2;
|
||||
batchSize = Math.Max(1, batchSize);
|
||||
goto retry;
|
||||
}
|
||||
}
|
||||
settings = new Settings() { Progress = progress };
|
||||
await SettingsRepository.UpdateSetting<Settings>(settings, SettingsKey);
|
||||
}
|
||||
}
|
||||
protected abstract IQueryable<TEntity> GetQuery(ApplicationDbContext ctx, DateTimeOffset? progress);
|
||||
protected abstract DateTimeOffset ProcessEntities(ApplicationDbContext ctx, List<TEntity> entities);
|
||||
public async Task ResetMigration()
|
||||
{
|
||||
await SettingsRepository.UpdateSetting<Settings>(new Settings(), SettingsKey);
|
||||
}
|
||||
public async Task<bool> IsComplete()
|
||||
{
|
||||
return (await SettingsRepository.GetSettingAsync<Settings>(SettingsKey)) is { Complete: true };
|
||||
}
|
||||
|
||||
public Task StopAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
_Cts.TrySetCanceled();
|
||||
return (_Migrating ?? Task.CompletedTask).ContinueWith(t =>
|
||||
{
|
||||
if (t.IsFaulted)
|
||||
Logs.LogError(t.Exception, "Error while migrating");
|
||||
});
|
||||
}
|
||||
}
|
|
@ -20,136 +20,62 @@ using static BTCPayServer.Controllers.UIInvoiceController;
|
|||
|
||||
namespace BTCPayServer.HostedServices;
|
||||
|
||||
public class InvoiceBlobMigratorHostedService : IHostedService
|
||||
public class InvoiceBlobMigratorHostedService : BlobMigratorHostedService<InvoiceData>
|
||||
{
|
||||
const string SettingsKey = "InvoiceBlobMigratorHostedService.Settings";
|
||||
|
||||
private readonly PaymentMethodHandlerDictionary _handlers;
|
||||
|
||||
internal class Settings
|
||||
{
|
||||
[JsonConverter(typeof(NBitcoin.JsonConverters.DateTimeToUnixTimeConverter))]
|
||||
public DateTimeOffset? Progress { get; set; }
|
||||
public bool Complete { get; set; }
|
||||
}
|
||||
Task? _Migrating;
|
||||
TaskCompletionSource _Cts = new TaskCompletionSource();
|
||||
public InvoiceBlobMigratorHostedService(
|
||||
ILogger<InvoiceBlobMigratorHostedService> logs,
|
||||
ISettingsRepository settingsRepository,
|
||||
ApplicationDbContextFactory applicationDbContextFactory,
|
||||
PaymentMethodHandlerDictionary handlers)
|
||||
PaymentMethodHandlerDictionary handlers) : base(logs, settingsRepository, applicationDbContextFactory)
|
||||
{
|
||||
Logs = logs;
|
||||
SettingsRepository = settingsRepository;
|
||||
ApplicationDbContextFactory = applicationDbContextFactory;
|
||||
_handlers = handlers;
|
||||
}
|
||||
|
||||
public ILogger<InvoiceBlobMigratorHostedService> Logs { get; }
|
||||
public ISettingsRepository SettingsRepository { get; }
|
||||
public ApplicationDbContextFactory ApplicationDbContextFactory { get; }
|
||||
|
||||
public Task StartAsync(CancellationToken cancellationToken)
|
||||
public override string SettingsKey => "InvoicesMigration";
|
||||
protected override IQueryable<InvoiceData> GetQuery(ApplicationDbContext ctx, DateTimeOffset? progress)
|
||||
{
|
||||
_Migrating = Migrate(cancellationToken);
|
||||
return Task.CompletedTask;
|
||||
var query = progress is DateTimeOffset last2 ?
|
||||
ctx.Invoices.Include(o => o.Payments).Where(i => i.Created < last2 && i.Currency == null) :
|
||||
ctx.Invoices.Include(o => o.Payments).Where(i => i.Currency == null);
|
||||
return query.OrderByDescending(i => i.Created);
|
||||
}
|
||||
public int BatchSize { get; set; } = 1000;
|
||||
|
||||
private async Task Migrate(CancellationToken cancellationToken)
|
||||
protected override DateTimeOffset ProcessEntities(ApplicationDbContext ctx, List<InvoiceData> invoices)
|
||||
{
|
||||
var settings = await SettingsRepository.GetSettingAsync<Settings>(SettingsKey) ?? new Settings();
|
||||
if (settings.Complete is true)
|
||||
return;
|
||||
if (settings.Progress is DateTimeOffset last)
|
||||
Logs.LogInformation($"Migrating invoices JSON Blobs from {last}");
|
||||
else
|
||||
Logs.LogInformation("Migrating invoices JSON Blobs from the beginning");
|
||||
|
||||
int batchSize = BatchSize;
|
||||
while (!cancellationToken.IsCancellationRequested)
|
||||
// Those clean up the JSON blobs, and mark entities as modified
|
||||
foreach (var inv in invoices)
|
||||
{
|
||||
retry:
|
||||
List<InvoiceData> invoices;
|
||||
await using (var ctx = ApplicationDbContextFactory.CreateContext())
|
||||
var blob = inv.GetBlob();
|
||||
var prompts = blob.GetPaymentPrompts();
|
||||
foreach (var p in prompts)
|
||||
{
|
||||
var query = settings.Progress is DateTimeOffset last2 ?
|
||||
ctx.Invoices.Include(o => o.Payments).Where(i => i.Created < last2 && i.Currency == null) :
|
||||
ctx.Invoices.Include(o => o.Payments).Where(i => i.Currency == null);
|
||||
query = query.OrderByDescending(i => i.Created).Take(batchSize);
|
||||
invoices = await query.ToListAsync(cancellationToken);
|
||||
if (invoices.Count == 0)
|
||||
if (_handlers.TryGetValue(p.PaymentMethodId, out var handler) && p.Details is not (null or { Type: JTokenType.Null }))
|
||||
{
|
||||
await SettingsRepository.UpdateSetting<Settings>(new Settings() { Complete = true }, SettingsKey);
|
||||
Logs.LogInformation("Migration of invoices JSON Blobs completed");
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
// Those clean up the JSON blobs, and mark entities as modified
|
||||
foreach (var inv in invoices)
|
||||
{
|
||||
var blob = inv.GetBlob();
|
||||
var prompts = blob.GetPaymentPrompts();
|
||||
foreach (var p in prompts)
|
||||
{
|
||||
if (_handlers.TryGetValue(p.PaymentMethodId, out var handler) && p.Details is not (null or { Type: JTokenType.Null }))
|
||||
{
|
||||
p.Details = JToken.FromObject(handler.ParsePaymentPromptDetails(p.Details), handler.Serializer);
|
||||
}
|
||||
}
|
||||
blob.SetPaymentPrompts(prompts);
|
||||
inv.SetBlob(blob);
|
||||
foreach (var pay in inv.Payments)
|
||||
{
|
||||
var paymentEntity = pay.GetBlob();
|
||||
if (_handlers.TryGetValue(paymentEntity.PaymentMethodId, out var handler) && paymentEntity.Details is not (null or { Type: JTokenType.Null }))
|
||||
{
|
||||
paymentEntity.Details = JToken.FromObject(handler.ParsePaymentDetails(paymentEntity.Details), handler.Serializer);
|
||||
}
|
||||
pay.SetBlob(paymentEntity);
|
||||
}
|
||||
}
|
||||
foreach (var entry in ctx.ChangeTracker.Entries<InvoiceData>())
|
||||
{
|
||||
entry.State = EntityState.Modified;
|
||||
}
|
||||
foreach (var entry in ctx.ChangeTracker.Entries<PaymentData>())
|
||||
{
|
||||
entry.State = EntityState.Modified;
|
||||
}
|
||||
await ctx.SaveChangesAsync();
|
||||
batchSize = BatchSize;
|
||||
}
|
||||
catch (DbUpdateConcurrencyException)
|
||||
{
|
||||
batchSize /= 2;
|
||||
batchSize = Math.Max(1, batchSize);
|
||||
goto retry;
|
||||
p.Details = JToken.FromObject(handler.ParsePaymentPromptDetails(p.Details), handler.Serializer);
|
||||
}
|
||||
}
|
||||
settings = new Settings() { Progress = invoices[^1].Created };
|
||||
await SettingsRepository.UpdateSetting<Settings>(settings, SettingsKey);
|
||||
blob.SetPaymentPrompts(prompts);
|
||||
inv.SetBlob(blob);
|
||||
foreach (var pay in inv.Payments)
|
||||
{
|
||||
var paymentEntity = pay.GetBlob();
|
||||
if (_handlers.TryGetValue(paymentEntity.PaymentMethodId, out var handler) && paymentEntity.Details is not (null or { Type: JTokenType.Null }))
|
||||
{
|
||||
paymentEntity.Details = JToken.FromObject(handler.ParsePaymentDetails(paymentEntity.Details), handler.Serializer);
|
||||
}
|
||||
pay.SetBlob(paymentEntity);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public async Task ResetMigration()
|
||||
{
|
||||
await SettingsRepository.UpdateSetting<Settings>(new Settings(), SettingsKey);
|
||||
}
|
||||
public async Task<bool> IsComplete()
|
||||
{
|
||||
return (await SettingsRepository.GetSettingAsync<Settings>(SettingsKey)) is { Complete: true };
|
||||
}
|
||||
|
||||
public Task StopAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
_Cts.TrySetCanceled();
|
||||
return (_Migrating ?? Task.CompletedTask).ContinueWith(t =>
|
||||
foreach (var entry in ctx.ChangeTracker.Entries<InvoiceData>())
|
||||
{
|
||||
if (t.IsFaulted)
|
||||
Logs.LogError(t.Exception, "Error while migrating invoices JSON Blobs");
|
||||
});
|
||||
entry.State = EntityState.Modified;
|
||||
}
|
||||
foreach (var entry in ctx.ChangeTracker.Entries<PaymentData>())
|
||||
{
|
||||
entry.State = EntityState.Modified;
|
||||
}
|
||||
return invoices[^1].Created;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,53 @@
|
|||
#nullable enable
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Reflection.Metadata;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using AngleSharp.Dom;
|
||||
using BTCPayServer.Abstractions.Contracts;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
using Google.Apis.Logging;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using NBitcoin;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using static BTCPayServer.Controllers.UIInvoiceController;
|
||||
|
||||
namespace BTCPayServer.HostedServices;
|
||||
|
||||
public class PayoutBlobMigratorHostedService : BlobMigratorHostedService<PayoutData>
|
||||
{
|
||||
|
||||
private readonly PaymentMethodHandlerDictionary _handlers;
|
||||
|
||||
public PayoutBlobMigratorHostedService(
|
||||
ILogger<PayoutBlobMigratorHostedService> logs,
|
||||
ISettingsRepository settingsRepository,
|
||||
ApplicationDbContextFactory applicationDbContextFactory,
|
||||
PaymentMethodHandlerDictionary handlers) : base(logs, settingsRepository, applicationDbContextFactory)
|
||||
{
|
||||
_handlers = handlers;
|
||||
}
|
||||
|
||||
public override string SettingsKey => "PayoutsMigration";
|
||||
protected override IQueryable<PayoutData> GetQuery(ApplicationDbContext ctx, DateTimeOffset? progress)
|
||||
{
|
||||
var query = progress is DateTimeOffset last2 ?
|
||||
ctx.Payouts.Where(i => i.Date < last2 && i.Currency == null) :
|
||||
ctx.Payouts.Where(i => i.Currency == null);
|
||||
return query.OrderByDescending(i => i);
|
||||
}
|
||||
protected override DateTimeOffset ProcessEntities(ApplicationDbContext ctx, List<PayoutData> payouts)
|
||||
{
|
||||
foreach (var entry in ctx.ChangeTracker.Entries<PayoutData>())
|
||||
{
|
||||
entry.State = EntityState.Modified;
|
||||
}
|
||||
return payouts[^1].Date;
|
||||
}
|
||||
}
|
|
@ -217,11 +217,11 @@ namespace BTCPayServer.HostedServices
|
|||
if (payoutQuery.PayoutMethods.Length == 1)
|
||||
{
|
||||
var pm = payoutQuery.PayoutMethods[0];
|
||||
query = query.Where(data => pm == data.PaymentMethodId);
|
||||
query = query.Where(data => pm == data.PayoutMethodId);
|
||||
}
|
||||
else
|
||||
{
|
||||
query = query.Where(data => payoutQuery.PayoutMethods.Contains(data.PaymentMethodId));
|
||||
query = query.Where(data => payoutQuery.PayoutMethods.Contains(data.PayoutMethodId));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -459,7 +459,7 @@ namespace BTCPayServer.HostedServices
|
|||
return;
|
||||
}
|
||||
|
||||
if (!PayoutMethodId.TryParse(payout.PaymentMethodId, out var paymentMethod))
|
||||
if (!PayoutMethodId.TryParse(payout.PayoutMethodId, out var paymentMethod))
|
||||
{
|
||||
req.Completion.SetResult(new PayoutApproval.ApprovalResult(PayoutApproval.Result.NotFound, null));
|
||||
return;
|
||||
|
@ -644,9 +644,10 @@ namespace BTCPayServer.HostedServices
|
|||
Date = now,
|
||||
State = PayoutState.AwaitingApproval,
|
||||
PullPaymentDataId = req.ClaimRequest.PullPaymentId,
|
||||
PaymentMethodId = req.ClaimRequest.PayoutMethodId.ToString(),
|
||||
PayoutMethodId = req.ClaimRequest.PayoutMethodId.ToString(),
|
||||
Destination = req.ClaimRequest.Destination.Id,
|
||||
StoreDataId = req.ClaimRequest.StoreId ?? pp?.StoreId
|
||||
StoreDataId = req.ClaimRequest.StoreId ?? pp?.StoreId,
|
||||
Currency = payoutHandler.Currency
|
||||
};
|
||||
var payoutBlob = new PayoutBlob()
|
||||
{
|
||||
|
@ -693,7 +694,7 @@ namespace BTCPayServer.HostedServices
|
|||
StoreId = payout.StoreDataId,
|
||||
Currency = ppBlob?.Currency ?? _handlers.TryGetNetwork(req.ClaimRequest.PayoutMethodId)?.NBXplorerNetwork.CryptoCode,
|
||||
Status = payout.State,
|
||||
PaymentMethod = payout.PaymentMethodId,
|
||||
PaymentMethod = payout.PayoutMethodId,
|
||||
PayoutId = payout.Id
|
||||
});
|
||||
}
|
||||
|
|
|
@ -577,6 +577,9 @@ o.GetRequiredService<IEnumerable<IPaymentLinkExtension>>().ToDictionary(o => o.P
|
|||
services.AddSingleton<InvoiceBlobMigratorHostedService>();
|
||||
services.AddSingleton<IHostedService, InvoiceBlobMigratorHostedService>(o => o.GetRequiredService<InvoiceBlobMigratorHostedService>());
|
||||
|
||||
services.AddSingleton<PayoutBlobMigratorHostedService>();
|
||||
services.AddSingleton<IHostedService, PayoutBlobMigratorHostedService>(o => o.GetRequiredService<PayoutBlobMigratorHostedService>());
|
||||
|
||||
// Broken
|
||||
// Providers.Add("argoneum", new ArgoneumRateProvider(_httpClientFactory?.CreateClient("EXCHANGE_ARGONEUM")));
|
||||
|
||||
|
|
|
@ -85,7 +85,7 @@ public class LightningPendingPayoutListener : BaseAsyncService
|
|||
}
|
||||
|
||||
foreach (IGrouping<string, PayoutData> payoutByStoreByPaymentMethod in payoutByStore.GroupBy(data =>
|
||||
data.PaymentMethodId))
|
||||
data.PayoutMethodId))
|
||||
{
|
||||
var pmi = PaymentMethodId.Parse(payoutByStoreByPaymentMethod.Key);
|
||||
var pm = store.GetPaymentMethodConfigs(_handlers)
|
||||
|
|
|
@ -146,6 +146,6 @@ public class LightningAutomatedPayoutProcessor : BaseAutomatedPayoutProcessor<Li
|
|||
{
|
||||
return (await UILightningLikePayoutController.TrypayBolt(lightningClient, payoutBlob, payoutData,
|
||||
bolt11PaymentRequest,
|
||||
_payoutHandler.Currency, CancellationToken)).Result is PayResult.Ok ;
|
||||
CancellationToken)).Result is PayResult.Ok ;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -54,8 +54,7 @@ public class PayoutsReportProvider : ReportProvider
|
|||
data.Add(payout.Date);
|
||||
data.Add(payout.GetPayoutSource(_btcPayNetworkJsonSerializerSettings));
|
||||
data.Add(payout.State.ToString());
|
||||
string? payoutCurrency;
|
||||
if (PayoutMethodId.TryParse(payout.PaymentMethodId, out var pmi))
|
||||
if (PayoutMethodId.TryParse(payout.PayoutMethodId, out var pmi))
|
||||
{
|
||||
var handler = _handlers.TryGet(pmi);
|
||||
if (handler is LightningLikePayoutHandler)
|
||||
|
@ -64,17 +63,16 @@ public class PayoutsReportProvider : ReportProvider
|
|||
data.Add("On-Chain");
|
||||
else
|
||||
data.Add(pmi.ToString());
|
||||
payoutCurrency = handler?.Currency;
|
||||
}
|
||||
else
|
||||
continue;
|
||||
|
||||
var ppBlob = payout.PullPaymentData?.GetBlob();
|
||||
var currency = ppBlob?.Currency ?? payoutCurrency;
|
||||
var currency = ppBlob?.Currency ?? payout.Currency;
|
||||
if (currency is null)
|
||||
continue;
|
||||
data.Add(payoutCurrency);
|
||||
data.Add(blob.CryptoAmount.HasValue && payoutCurrency is not null ? _displayFormatter.ToFormattedAmount(blob.CryptoAmount.Value, payoutCurrency) : null);
|
||||
data.Add(payout.Currency);
|
||||
data.Add(blob.CryptoAmount is decimal v ? _displayFormatter.ToFormattedAmount(v, payout.Currency) : null);
|
||||
data.Add(currency);
|
||||
data.Add(_displayFormatter.ToFormattedAmount(blob.Amount, currency));
|
||||
data.Add(blob.Destination);
|
||||
|
|
|
@ -67,7 +67,7 @@ namespace BTCPayServer.Services.Reporting
|
|||
var conn = ctx.Database.GetDbConnection();
|
||||
var rows = await conn.QueryAsync(
|
||||
"""
|
||||
SELECT i."Created", i."Id" AS "InvoiceId", p."State", p."PaymentMethodId", pp."Id" AS "PullPaymentId", pp."Blob" AS "ppBlob", p."Blob" AS "pBlob" FROM "Invoices" i
|
||||
SELECT i."Created", i."Id" AS "InvoiceId", p."State", p."PayoutMethodId", p."Currency" AS "PayoutCurrency", pp."Id" AS "PullPaymentId", pp."Blob" AS "ppBlob", p."Blob" AS "pBlob" FROM "Invoices" i
|
||||
JOIN "Refunds" r ON r."InvoiceDataId"= i."Id"
|
||||
JOIN "PullPayments" pp ON r."PullPaymentDataId"=pp."Id"
|
||||
LEFT JOIN "Payouts" p ON p."PullPaymentDataId"=pp."Id"
|
||||
|
@ -104,7 +104,8 @@ namespace BTCPayServer.Services.Reporting
|
|||
if (r.pBlob is null)
|
||||
return null;
|
||||
Data.PayoutData p = new Data.PayoutData();
|
||||
p.PaymentMethodId = r.PaymentMethodId;
|
||||
p.PayoutMethodId = r.PayoutMethodId;
|
||||
p.Currency = (string)r.PayoutCurrency;
|
||||
p.Blob = (string)r.pBlob;
|
||||
return p.GetBlob(_serializerSettings);
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue