Migrate Payouts to new format (#5989)

* Migrate Payouts to new format

* Rename PayoutData column to PayoutMethodId
This commit is contained in:
Nicolas Dorier 2024-06-28 20:07:53 +09:00 committed by GitHub
parent c56b660c92
commit a295e123bc
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
21 changed files with 303 additions and 151 deletions

View file

@ -37,6 +37,10 @@ namespace BTCPayServer.Data
{
paymentData.Migrate();
}
else if (entity is PayoutData payoutData && payoutData.Currency is null)
{
payoutData.Migrate();
}
return entity;
}
}

View 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];
}
}
}

View file

@ -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

View file

@ -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)
{
}
}
}

View file

@ -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)");

View file

@ -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;
}

View file

@ -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)
{

View file

@ -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()

View file

@ -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

View file

@ -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
});

View file

@ -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
};
}

View file

@ -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)

View 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");
});
}
}

View file

@ -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;
}
}

View file

@ -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;
}
}

View file

@ -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
});
}

View file

@ -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")));

View file

@ -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)

View file

@ -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 ;
}
}

View file

@ -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);

View file

@ -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);
}