mirror of
https://github.com/aljazceru/btcpayserver.git
synced 2025-12-18 06:24:24 +01:00
Prune webhook data from database
This commit is contained in:
committed by
Andrew Camilleri
parent
418b476725
commit
4e03c2523a
@@ -1,4 +1,5 @@
|
|||||||
using System;
|
using System;
|
||||||
|
using System.Data.Common;
|
||||||
using BTCPayServer.Abstractions.Models;
|
using BTCPayServer.Abstractions.Models;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.EntityFrameworkCore.Metadata;
|
using Microsoft.EntityFrameworkCore.Metadata;
|
||||||
@@ -21,7 +22,6 @@ namespace BTCPayServer.Abstractions.Contracts
|
|||||||
}
|
}
|
||||||
|
|
||||||
public abstract T CreateContext();
|
public abstract T CreateContext();
|
||||||
|
|
||||||
class CustomNpgsqlMigrationsSqlGenerator : NpgsqlMigrationsSqlGenerator
|
class CustomNpgsqlMigrationsSqlGenerator : NpgsqlMigrationsSqlGenerator
|
||||||
{
|
{
|
||||||
#pragma warning disable EF1001 // Internal EF Core API usage.
|
#pragma warning disable EF1001 // Internal EF Core API usage.
|
||||||
|
|||||||
@@ -51,7 +51,8 @@ namespace BTCPayServer.Client
|
|||||||
{
|
{
|
||||||
if (message.StatusCode == System.Net.HttpStatusCode.UnprocessableEntity)
|
if (message.StatusCode == System.Net.HttpStatusCode.UnprocessableEntity)
|
||||||
{
|
{
|
||||||
var err = JsonConvert.DeserializeObject<Models.GreenfieldValidationError[]>(await message.Content.ReadAsStringAsync());
|
var aa = await message.Content.ReadAsStringAsync();
|
||||||
|
var err = JsonConvert.DeserializeObject<Models.GreenfieldValidationError[]>(aa);
|
||||||
throw new GreenfieldValidationException(err);
|
throw new GreenfieldValidationException(err);
|
||||||
}
|
}
|
||||||
if (message.StatusCode == System.Net.HttpStatusCode.Forbidden)
|
if (message.StatusCode == System.Net.HttpStatusCode.Forbidden)
|
||||||
|
|||||||
@@ -51,6 +51,10 @@ namespace BTCPayServer.Client.Models
|
|||||||
public DateTimeOffset Timestamp { get; set; }
|
public DateTimeOffset Timestamp { get; set; }
|
||||||
[JsonExtensionData]
|
[JsonExtensionData]
|
||||||
public IDictionary<string, JToken> AdditionalData { get; set; }
|
public IDictionary<string, JToken> AdditionalData { get; set; }
|
||||||
|
public bool IsPruned()
|
||||||
|
{
|
||||||
|
return DeliveryId is null;
|
||||||
|
}
|
||||||
public T ReadAs<T>()
|
public T ReadAs<T>()
|
||||||
{
|
{
|
||||||
var str = JsonConvert.SerializeObject(this, DefaultSerializerSettings);
|
var str = JsonConvert.SerializeObject(this, DefaultSerializerSettings);
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ using Microsoft.EntityFrameworkCore.Infrastructure;
|
|||||||
|
|
||||||
namespace BTCPayServer.Data
|
namespace BTCPayServer.Data
|
||||||
{
|
{
|
||||||
public class WebhookDeliveryData : IHasBlobUntyped
|
public class WebhookDeliveryData
|
||||||
{
|
{
|
||||||
[Key]
|
[Key]
|
||||||
[MaxLength(25)]
|
[MaxLength(25)]
|
||||||
@@ -17,10 +17,8 @@ namespace BTCPayServer.Data
|
|||||||
|
|
||||||
[Required]
|
[Required]
|
||||||
public DateTimeOffset Timestamp { get; set; }
|
public DateTimeOffset Timestamp { get; set; }
|
||||||
[Obsolete("Use Blob2 instead")]
|
public string Blob { get; set; }
|
||||||
public byte[] Blob { get; set; }
|
public bool Pruned { get; set; }
|
||||||
public string Blob2 { get; set; }
|
|
||||||
|
|
||||||
|
|
||||||
internal static void OnModelCreating(ModelBuilder builder, DatabaseFacade databaseFacade)
|
internal static void OnModelCreating(ModelBuilder builder, DatabaseFacade databaseFacade)
|
||||||
{
|
{
|
||||||
@@ -28,11 +26,11 @@ namespace BTCPayServer.Data
|
|||||||
.HasOne(o => o.Webhook)
|
.HasOne(o => o.Webhook)
|
||||||
.WithMany(a => a.Deliveries).OnDelete(DeleteBehavior.Cascade);
|
.WithMany(a => a.Deliveries).OnDelete(DeleteBehavior.Cascade);
|
||||||
builder.Entity<WebhookDeliveryData>().HasIndex(o => o.WebhookId);
|
builder.Entity<WebhookDeliveryData>().HasIndex(o => o.WebhookId);
|
||||||
|
builder.Entity<WebhookDeliveryData>().HasIndex(o => o.Timestamp);
|
||||||
if (databaseFacade.IsNpgsql())
|
if (databaseFacade.IsNpgsql())
|
||||||
{
|
{
|
||||||
builder.Entity<WebhookDeliveryData>()
|
builder.Entity<WebhookDeliveryData>()
|
||||||
.Property(o => o.Blob2)
|
.Property(o => o.Blob)
|
||||||
.HasColumnType("JSONB");
|
.HasColumnType("JSONB");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,83 @@
|
|||||||
|
using System;
|
||||||
|
using BTCPayServer.Data;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||||
|
using NBitcoin;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace BTCPayServer.Migrations
|
||||||
|
{
|
||||||
|
[DbContext(typeof(ApplicationDbContext))]
|
||||||
|
[Migration("20230529135505_WebhookDeliveriesCleanup")]
|
||||||
|
public partial class WebhookDeliveriesCleanup : Migration
|
||||||
|
{
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
if (migrationBuilder.IsNpgsql())
|
||||||
|
{
|
||||||
|
migrationBuilder.Sql("DROP TABLE IF EXISTS \"InvoiceWebhookDeliveries\", \"WebhookDeliveries\";");
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "WebhookDeliveries",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<string>(type: "TEXT", nullable: false),
|
||||||
|
WebhookId = table.Column<string>(type: "TEXT", nullable: false),
|
||||||
|
Timestamp = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false),
|
||||||
|
Pruned = table.Column<bool>(type: "BOOLEAN", nullable: false),
|
||||||
|
Blob = table.Column<string>(type: "JSONB", nullable: false)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_WebhookDeliveries", x => x.Id);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_WebhookDeliveries_Webhooks_WebhookId",
|
||||||
|
column: x => x.WebhookId,
|
||||||
|
principalTable: "Webhooks",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_WebhookDeliveries_WebhookId",
|
||||||
|
table: "WebhookDeliveries",
|
||||||
|
column: "WebhookId");
|
||||||
|
migrationBuilder.Sql("CREATE INDEX \"IX_WebhookDeliveries_Timestamp\" ON \"WebhookDeliveries\"(\"Timestamp\") WHERE \"Pruned\" IS FALSE");
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "InvoiceWebhookDeliveries",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
InvoiceId = table.Column<string>(type: "TEXT", nullable: false),
|
||||||
|
DeliveryId = table.Column<string>(type: "TEXT", nullable: false)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_InvoiceWebhookDeliveries", x => new { x.InvoiceId, x.DeliveryId });
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_InvoiceWebhookDeliveries_WebhookDeliveries_DeliveryId",
|
||||||
|
column: x => x.DeliveryId,
|
||||||
|
principalTable: "WebhookDeliveries",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_InvoiceWebhookDeliveries_Invoices_InvoiceId",
|
||||||
|
column: x => x.InvoiceId,
|
||||||
|
principalTable: "Invoices",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1019,12 +1019,12 @@ namespace BTCPayServer.Migrations
|
|||||||
.HasMaxLength(25)
|
.HasMaxLength(25)
|
||||||
.HasColumnType("TEXT");
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
b.Property<byte[]>("Blob")
|
b.Property<string>("Blob")
|
||||||
.HasColumnType("BLOB");
|
|
||||||
|
|
||||||
b.Property<string>("Blob2")
|
|
||||||
.HasColumnType("TEXT");
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<bool>("Pruned")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
b.Property<DateTimeOffset>("Timestamp")
|
b.Property<DateTimeOffset>("Timestamp")
|
||||||
.HasColumnType("TEXT");
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
@@ -1035,6 +1035,8 @@ namespace BTCPayServer.Migrations
|
|||||||
|
|
||||||
b.HasKey("Id");
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("Timestamp");
|
||||||
|
|
||||||
b.HasIndex("WebhookId");
|
b.HasIndex("WebhookId");
|
||||||
|
|
||||||
b.ToTable("WebhookDeliveries");
|
b.ToTable("WebhookDeliveries");
|
||||||
|
|||||||
@@ -1432,7 +1432,7 @@ namespace BTCPayServer.Tests
|
|||||||
Assert.False(hook.AutomaticRedelivery);
|
Assert.False(hook.AutomaticRedelivery);
|
||||||
Assert.Equal(fakeServer.ServerUri.AbsoluteUri, hook.Url);
|
Assert.Equal(fakeServer.ServerUri.AbsoluteUri, hook.Url);
|
||||||
}
|
}
|
||||||
using var tester = CreateServerTester();
|
using var tester = CreateServerTester(newDb: true);
|
||||||
using var fakeServer = new FakeServer();
|
using var fakeServer = new FakeServer();
|
||||||
await fakeServer.Start();
|
await fakeServer.Start();
|
||||||
await tester.StartAsync();
|
await tester.StartAsync();
|
||||||
@@ -1509,6 +1509,14 @@ namespace BTCPayServer.Tests
|
|||||||
clientProfile = await user.CreateClient(Policies.CanModifyStoreSettings, Policies.CanCreateInvoice);
|
clientProfile = await user.CreateClient(Policies.CanModifyStoreSettings, Policies.CanCreateInvoice);
|
||||||
await clientProfile.GetWebhookDeliveryRequest(user.StoreId, hook.Id, newDeliveryId);
|
await clientProfile.GetWebhookDeliveryRequest(user.StoreId, hook.Id, newDeliveryId);
|
||||||
|
|
||||||
|
|
||||||
|
TestLogs.LogInformation("Can prune deliveries");
|
||||||
|
var cleanup = tester.PayTester.GetService<HostedServices.CleanupWebhookDeliveriesTask>();
|
||||||
|
cleanup.BatchSize = 1;
|
||||||
|
cleanup.PruneAfter = TimeSpan.Zero;
|
||||||
|
await cleanup.Do(default);
|
||||||
|
await AssertHttpError(409, () => clientProfile.RedeliverWebhook(user.StoreId, hook.Id, delivery.Id));
|
||||||
|
|
||||||
TestLogs.LogInformation("Testing corner cases");
|
TestLogs.LogInformation("Testing corner cases");
|
||||||
Assert.Null(await clientProfile.GetWebhookDeliveryRequest(user.StoreId, "lol", newDeliveryId));
|
Assert.Null(await clientProfile.GetWebhookDeliveryRequest(user.StoreId, "lol", newDeliveryId));
|
||||||
Assert.Null(await clientProfile.GetWebhookDeliveryRequest(user.StoreId, hook.Id, "lol"));
|
Assert.Null(await clientProfile.GetWebhookDeliveryRequest(user.StoreId, hook.Id, "lol"));
|
||||||
|
|||||||
@@ -153,15 +153,24 @@ namespace BTCPayServer.Controllers.Greenfield
|
|||||||
var delivery = await StoreRepository.GetWebhookDelivery(CurrentStoreId, webhookId, deliveryId);
|
var delivery = await StoreRepository.GetWebhookDelivery(CurrentStoreId, webhookId, deliveryId);
|
||||||
if (delivery is null)
|
if (delivery is null)
|
||||||
return WebhookDeliveryNotFound();
|
return WebhookDeliveryNotFound();
|
||||||
|
if (delivery.GetBlob().IsPruned())
|
||||||
|
return WebhookDeliveryPruned();
|
||||||
return this.Ok(new JValue(await WebhookSender.Redeliver(deliveryId)));
|
return this.Ok(new JValue(await WebhookSender.Redeliver(deliveryId)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private IActionResult WebhookDeliveryPruned()
|
||||||
|
{
|
||||||
|
return this.CreateAPIError(409, "webhookdelivery-pruned", "This webhook delivery has been pruned, so it can't be redelivered");
|
||||||
|
}
|
||||||
|
|
||||||
[HttpGet("~/api/v1/stores/{storeId}/webhooks/{webhookId}/deliveries/{deliveryId}/request")]
|
[HttpGet("~/api/v1/stores/{storeId}/webhooks/{webhookId}/deliveries/{deliveryId}/request")]
|
||||||
public async Task<IActionResult> GetDeliveryRequest(string storeId, string webhookId, string deliveryId)
|
public async Task<IActionResult> GetDeliveryRequest(string storeId, string webhookId, string deliveryId)
|
||||||
{
|
{
|
||||||
var delivery = await StoreRepository.GetWebhookDelivery(CurrentStoreId, webhookId, deliveryId);
|
var delivery = await StoreRepository.GetWebhookDelivery(CurrentStoreId, webhookId, deliveryId);
|
||||||
if (delivery is null)
|
if (delivery is null)
|
||||||
return WebhookDeliveryNotFound();
|
return WebhookDeliveryNotFound();
|
||||||
|
if (delivery.GetBlob().IsPruned())
|
||||||
|
return WebhookDeliveryPruned();
|
||||||
return File(delivery.GetBlob().Request, "application/json");
|
return File(delivery.GetBlob().Request, "application/json");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.ComponentModel;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
@@ -29,12 +30,30 @@ namespace BTCPayServer.Data
|
|||||||
[JsonConverter(typeof(Newtonsoft.Json.Converters.StringEnumConverter))]
|
[JsonConverter(typeof(Newtonsoft.Json.Converters.StringEnumConverter))]
|
||||||
public WebhookDeliveryStatus Status { get; set; }
|
public WebhookDeliveryStatus Status { get; set; }
|
||||||
public int? HttpCode { get; set; }
|
public int? HttpCode { get; set; }
|
||||||
|
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)]
|
||||||
public string ErrorMessage { get; set; }
|
public string ErrorMessage { get; set; }
|
||||||
|
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)]
|
||||||
public byte[] Request { get; set; }
|
public byte[] Request { get; set; }
|
||||||
|
public void Prune()
|
||||||
|
{
|
||||||
|
var request = JObject.Parse(UTF8Encoding.UTF8.GetString(Request));
|
||||||
|
foreach (var prop in request.Properties().ToList())
|
||||||
|
{
|
||||||
|
if (prop.Name == "type")
|
||||||
|
continue;
|
||||||
|
prop.Remove();
|
||||||
|
}
|
||||||
|
Request = UTF8Encoding.UTF8.GetBytes(request.ToString(Formatting.None));
|
||||||
|
}
|
||||||
public T ReadRequestAs<T>()
|
public T ReadRequestAs<T>()
|
||||||
{
|
{
|
||||||
return JsonConvert.DeserializeObject<T>(UTF8Encoding.UTF8.GetString(Request), HostedServices.WebhookSender.DefaultSerializerSettings);
|
return JsonConvert.DeserializeObject<T>(UTF8Encoding.UTF8.GetString(Request), HostedServices.WebhookSender.DefaultSerializerSettings);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public bool IsPruned()
|
||||||
|
{
|
||||||
|
return ReadRequestAs<WebhookEvent>().IsPruned();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
public class WebhookBlob
|
public class WebhookBlob
|
||||||
{
|
{
|
||||||
@@ -56,11 +75,17 @@ namespace BTCPayServer.Data
|
|||||||
}
|
}
|
||||||
public static WebhookDeliveryBlob GetBlob(this WebhookDeliveryData webhook)
|
public static WebhookDeliveryBlob GetBlob(this WebhookDeliveryData webhook)
|
||||||
{
|
{
|
||||||
return webhook.HasTypedBlob<WebhookDeliveryBlob>().GetBlob(HostedServices.WebhookSender.DefaultSerializerSettings);
|
if (webhook.Blob is null)
|
||||||
|
return null;
|
||||||
|
else
|
||||||
|
return JsonConvert.DeserializeObject<WebhookDeliveryBlob>(webhook.Blob, HostedServices.WebhookSender.DefaultSerializerSettings);
|
||||||
}
|
}
|
||||||
public static void SetBlob(this WebhookDeliveryData webhook, WebhookDeliveryBlob blob)
|
public static void SetBlob(this WebhookDeliveryData webhook, WebhookDeliveryBlob blob)
|
||||||
{
|
{
|
||||||
webhook.HasTypedBlob<WebhookDeliveryBlob>().SetBlob(blob, HostedServices.WebhookSender.DefaultSerializerSettings);
|
if (blob is null)
|
||||||
|
webhook.Blob = null;
|
||||||
|
else
|
||||||
|
webhook.Blob = JsonConvert.SerializeObject(blob, HostedServices.WebhookSender.DefaultSerializerSettings);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ using System.Threading.Tasks;
|
|||||||
using BTCPayServer.BIP78.Sender;
|
using BTCPayServer.BIP78.Sender;
|
||||||
using BTCPayServer.Configuration;
|
using BTCPayServer.Configuration;
|
||||||
using BTCPayServer.Data;
|
using BTCPayServer.Data;
|
||||||
|
using BTCPayServer.HostedServices;
|
||||||
using BTCPayServer.Lightning;
|
using BTCPayServer.Lightning;
|
||||||
using BTCPayServer.Logging;
|
using BTCPayServer.Logging;
|
||||||
using BTCPayServer.Models;
|
using BTCPayServer.Models;
|
||||||
@@ -25,6 +26,7 @@ using BTCPayServer.Services.Wallets;
|
|||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Microsoft.Extensions.Configuration;
|
using Microsoft.Extensions.Configuration;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using NBitcoin;
|
using NBitcoin;
|
||||||
using NBitcoin.Payment;
|
using NBitcoin.Payment;
|
||||||
@@ -115,6 +117,14 @@ namespace BTCPayServer
|
|||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static IServiceCollection AddScheduledTask<T>(this IServiceCollection services, TimeSpan every)
|
||||||
|
where T : class, IPeriodicTask
|
||||||
|
{
|
||||||
|
services.AddSingleton<T>();
|
||||||
|
services.AddTransient<ScheduledTask>(o => new ScheduledTask(typeof(T), every));
|
||||||
|
return services;
|
||||||
|
}
|
||||||
|
|
||||||
public static PaymentMethodId GetpaymentMethodId(this InvoiceCryptoInfo info)
|
public static PaymentMethodId GetpaymentMethodId(this InvoiceCryptoInfo info)
|
||||||
{
|
{
|
||||||
return new PaymentMethodId(info.CryptoCode, PaymentTypes.Parse(info.PaymentType));
|
return new PaymentMethodId(info.CryptoCode, PaymentTypes.Parse(info.PaymentType));
|
||||||
|
|||||||
61
BTCPayServer/HostedServices/CleanupWebhookDeliveriesTask.cs
Normal file
61
BTCPayServer/HostedServices/CleanupWebhookDeliveriesTask.cs
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
using System;
|
||||||
|
using Dapper;
|
||||||
|
using System.Data.Common;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using BTCPayServer.Data;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
namespace BTCPayServer.HostedServices
|
||||||
|
{
|
||||||
|
public class CleanupWebhookDeliveriesTask : IPeriodicTask
|
||||||
|
{
|
||||||
|
public CleanupWebhookDeliveriesTask(ApplicationDbContextFactory dbContextFactory)
|
||||||
|
{
|
||||||
|
DbContextFactory = dbContextFactory;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ApplicationDbContextFactory DbContextFactory { get; }
|
||||||
|
public int BatchSize { get; set; } = 500;
|
||||||
|
public TimeSpan PruneAfter { get; set; } = TimeSpan.FromDays(60);
|
||||||
|
|
||||||
|
public async Task Do(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
await using var ctx = DbContextFactory.CreateContext();
|
||||||
|
if (!ctx.Database.IsNpgsql())
|
||||||
|
return;
|
||||||
|
var conn = ctx.Database.GetDbConnection();
|
||||||
|
bool pruned = false;
|
||||||
|
int offset = 0;
|
||||||
|
retry:
|
||||||
|
var rows = await conn.QueryAsync<WebhookDeliveryData>(@"
|
||||||
|
SELECT ""Id"", ""Blob""
|
||||||
|
FROM ""WebhookDeliveries""
|
||||||
|
WHERE ((now() - ""Timestamp"") > @PruneAfter) AND ""Pruned"" IS FALSE
|
||||||
|
ORDER BY ""Timestamp""
|
||||||
|
LIMIT @BatchSize OFFSET @offset
|
||||||
|
", new { PruneAfter, BatchSize, offset });
|
||||||
|
|
||||||
|
foreach (var d in rows)
|
||||||
|
{
|
||||||
|
var blob = d.GetBlob();
|
||||||
|
blob.Prune();
|
||||||
|
d.SetBlob(blob);
|
||||||
|
d.Pruned = true;
|
||||||
|
pruned = true;
|
||||||
|
}
|
||||||
|
if (pruned)
|
||||||
|
{
|
||||||
|
pruned = false;
|
||||||
|
await conn.ExecuteAsync("UPDATE \"WebhookDeliveries\" SET \"Blob\"=@Blob::JSONB, \"Pruned\"=@Pruned WHERE \"Id\"=@Id", rows);
|
||||||
|
if (rows.Count() == BatchSize)
|
||||||
|
{
|
||||||
|
offset += BatchSize;
|
||||||
|
goto retry;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
10
BTCPayServer/HostedServices/IPeriodicTask.cs
Normal file
10
BTCPayServer/HostedServices/IPeriodicTask.cs
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
using System.Threading.Tasks;
|
||||||
|
using System.Threading;
|
||||||
|
|
||||||
|
namespace BTCPayServer.HostedServices
|
||||||
|
{
|
||||||
|
public interface IPeriodicTask
|
||||||
|
{
|
||||||
|
Task Do(CancellationToken cancellationToken);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,92 @@
|
|||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Microsoft.Extensions.Hosting;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using System;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Channels;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace BTCPayServer.HostedServices
|
||||||
|
{
|
||||||
|
public class PeriodicTaskLauncherHostedService : IHostedService
|
||||||
|
{
|
||||||
|
public PeriodicTaskLauncherHostedService(IServiceProvider serviceProvider, ILoggerFactory loggerFactory)
|
||||||
|
{
|
||||||
|
ServiceProvider = serviceProvider;
|
||||||
|
Logger = loggerFactory.CreateLogger("BTCPayServer.PeriodicTasks");
|
||||||
|
}
|
||||||
|
|
||||||
|
public IServiceProvider ServiceProvider { get; }
|
||||||
|
public ILogger Logger { get; }
|
||||||
|
|
||||||
|
Channel<ScheduledTask> jobs = Channel.CreateBounded<ScheduledTask>(100);
|
||||||
|
CancellationTokenSource cts;
|
||||||
|
public Task StartAsync(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
cts = new CancellationTokenSource();
|
||||||
|
foreach (var task in ServiceProvider.GetServices<ScheduledTask>())
|
||||||
|
jobs.Writer.TryWrite(task);
|
||||||
|
|
||||||
|
loop = Task.WhenAll(Enumerable.Range(0, 3).Select(_ => Loop(cts.Token)).ToArray());
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
Task loop;
|
||||||
|
private async Task Loop(CancellationToken token)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await foreach (var job in jobs.Reader.ReadAllAsync(token))
|
||||||
|
{
|
||||||
|
if (job.NextScheduled <= DateTimeOffset.UtcNow)
|
||||||
|
{
|
||||||
|
var t = (IPeriodicTask)ServiceProvider.GetService(job.PeriodicTaskType);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await t.Do(token);
|
||||||
|
}
|
||||||
|
catch when (token.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Logger.LogError(ex, $"Unhandled error in periodic task {job.PeriodicTaskType.Name}");
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
job.NextScheduled = DateTimeOffset.UtcNow + job.Every;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ = Wait(job, token);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch when (token.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task Wait(ScheduledTask job, CancellationToken token)
|
||||||
|
{
|
||||||
|
var timeToWait = job.NextScheduled - DateTimeOffset.UtcNow;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await Task.Delay(timeToWait, token);
|
||||||
|
}
|
||||||
|
catch { }
|
||||||
|
while (await jobs.Writer.WaitToWriteAsync())
|
||||||
|
{
|
||||||
|
if (jobs.Writer.TryWrite(job))
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task StopAsync(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
cts?.Cancel();
|
||||||
|
jobs.Writer.TryComplete();
|
||||||
|
if (loop is not null)
|
||||||
|
await loop;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
16
BTCPayServer/HostedServices/ScheduledTask.cs
Normal file
16
BTCPayServer/HostedServices/ScheduledTask.cs
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
using System;
|
||||||
|
|
||||||
|
namespace BTCPayServer.HostedServices
|
||||||
|
{
|
||||||
|
public class ScheduledTask
|
||||||
|
{
|
||||||
|
public ScheduledTask(Type periodicTypeTask, TimeSpan every)
|
||||||
|
{
|
||||||
|
PeriodicTaskType = periodicTypeTask;
|
||||||
|
Every = every;
|
||||||
|
}
|
||||||
|
public Type PeriodicTaskType { get; set; }
|
||||||
|
public TimeSpan Every { get; set; } = TimeSpan.FromMinutes(5.0);
|
||||||
|
public DateTimeOffset NextScheduled { get; set; } = DateTimeOffset.UtcNow;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -105,6 +105,8 @@ namespace BTCPayServer.HostedServices
|
|||||||
var newDeliveryBlob = new WebhookDeliveryBlob();
|
var newDeliveryBlob = new WebhookDeliveryBlob();
|
||||||
newDeliveryBlob.Request = oldDeliveryBlob.Request;
|
newDeliveryBlob.Request = oldDeliveryBlob.Request;
|
||||||
var webhookEvent = newDeliveryBlob.ReadRequestAs<WebhookEvent>();
|
var webhookEvent = newDeliveryBlob.ReadRequestAs<WebhookEvent>();
|
||||||
|
if (webhookEvent.IsPruned())
|
||||||
|
return null;
|
||||||
webhookEvent.DeliveryId = newDelivery.Id;
|
webhookEvent.DeliveryId = newDelivery.Id;
|
||||||
webhookEvent.WebhookId = webhookDelivery.Webhook.Id;
|
webhookEvent.WebhookId = webhookDelivery.Webhook.Id;
|
||||||
// if we redelivered a redelivery, we still want the initial delivery here
|
// if we redelivered a redelivery, we still want the initial delivery here
|
||||||
|
|||||||
@@ -350,6 +350,9 @@ namespace BTCPayServer.Hosting
|
|||||||
services.AddSingleton<HostedServices.WebhookSender>();
|
services.AddSingleton<HostedServices.WebhookSender>();
|
||||||
services.AddSingleton<IHostedService, WebhookSender>(o => o.GetRequiredService<WebhookSender>());
|
services.AddSingleton<IHostedService, WebhookSender>(o => o.GetRequiredService<WebhookSender>());
|
||||||
services.AddSingleton<IHostedService, StoreEmailRuleProcessorSender>();
|
services.AddSingleton<IHostedService, StoreEmailRuleProcessorSender>();
|
||||||
|
services.AddSingleton<IHostedService, PeriodicTaskLauncherHostedService>();
|
||||||
|
services.AddScheduledTask<CleanupWebhookDeliveriesTask>(TimeSpan.FromHours(6.0));
|
||||||
|
|
||||||
services.AddHttpClient(WebhookSender.OnionNamedClient)
|
services.AddHttpClient(WebhookSender.OnionNamedClient)
|
||||||
.ConfigurePrimaryHttpMessageHandler<Socks5HttpClientHandler>();
|
.ConfigurePrimaryHttpMessageHandler<Socks5HttpClientHandler>();
|
||||||
services.AddHttpClient(WebhookSender.LoopbackNamedClient)
|
services.AddHttpClient(WebhookSender.LoopbackNamedClient)
|
||||||
|
|||||||
@@ -213,6 +213,9 @@ namespace BTCPayServer.Hosting
|
|||||||
{
|
{
|
||||||
var typeMapping = t.EntityTypeMappings.Single();
|
var typeMapping = t.EntityTypeMappings.Single();
|
||||||
var query = (IQueryable<object>)otherContext.GetType().GetMethod("Set", new Type[0])!.MakeGenericMethod(typeMapping.EntityType.ClrType).Invoke(otherContext, null)!;
|
var query = (IQueryable<object>)otherContext.GetType().GetMethod("Set", new Type[0])!.MakeGenericMethod(typeMapping.EntityType.ClrType).Invoke(otherContext, null)!;
|
||||||
|
if (t.Name == "WebhookDeliveries" ||
|
||||||
|
t.Name == "InvoiceWebhookDeliveries")
|
||||||
|
continue;
|
||||||
Logger.LogInformation($"Migrating table: " + t.Name);
|
Logger.LogInformation($"Migrating table: " + t.Name);
|
||||||
List<PropertyInfo> datetimeProperties = new List<PropertyInfo>();
|
List<PropertyInfo> datetimeProperties = new List<PropertyInfo>();
|
||||||
foreach (var col in t.Columns)
|
foreach (var col in t.Columns)
|
||||||
|
|||||||
@@ -22,13 +22,16 @@ namespace BTCPayServer.Models.StoreViewModels
|
|||||||
Success = blob.Status == WebhookDeliveryStatus.HttpSuccess;
|
Success = blob.Status == WebhookDeliveryStatus.HttpSuccess;
|
||||||
ErrorMessage = blob.ErrorMessage ?? "Success";
|
ErrorMessage = blob.ErrorMessage ?? "Success";
|
||||||
Time = s.Timestamp;
|
Time = s.Timestamp;
|
||||||
Type = blob.ReadRequestAs<WebhookEvent>().Type;
|
var evt = blob.ReadRequestAs<WebhookEvent>();
|
||||||
|
Type = evt.Type;
|
||||||
|
Pruned = evt.IsPruned();
|
||||||
WebhookId = s.Id;
|
WebhookId = s.Id;
|
||||||
PayloadUrl = s.Webhook?.GetBlob().Url;
|
PayloadUrl = s.Webhook?.GetBlob().Url;
|
||||||
}
|
}
|
||||||
public string Id { get; set; }
|
public string Id { get; set; }
|
||||||
public DateTimeOffset Time { get; set; }
|
public DateTimeOffset Time { get; set; }
|
||||||
public WebhookEventType Type { get; private set; }
|
public WebhookEventType Type { get; private set; }
|
||||||
|
public bool Pruned { get; set; }
|
||||||
public string WebhookId { get; set; }
|
public string WebhookId { get; set; }
|
||||||
public bool Success { get; set; }
|
public bool Success { get; set; }
|
||||||
public string ErrorMessage { get; set; }
|
public string ErrorMessage { get; set; }
|
||||||
|
|||||||
@@ -9,9 +9,19 @@
|
|||||||
@section PageHeadContent {
|
@section PageHeadContent {
|
||||||
<meta name="robots" content="noindex,nofollow">
|
<meta name="robots" content="noindex,nofollow">
|
||||||
<style>
|
<style>
|
||||||
#posData td > table:last-child { margin-bottom: 0 !important; }
|
#posData td > table:last-child {
|
||||||
#posData table > tbody > tr:first-child > td > h4 { margin-top: 0 !important; }
|
margin-bottom: 0 !important;
|
||||||
.invoice-information { display: flex; flex-wrap: wrap; gap: var(--btcpay-space-xl) var(--btcpay-space-xxl); }
|
}
|
||||||
|
|
||||||
|
#posData table > tbody > tr:first-child > td > h4 {
|
||||||
|
margin-top: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.invoice-information {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: var(--btcpay-space-xl) var(--btcpay-space-xxl);
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -163,7 +173,7 @@
|
|||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
<h4 class="modal-title" id="RefundTitle">Issue Refund</h4>
|
<h4 class="modal-title" id="RefundTitle">Issue Refund</h4>
|
||||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close">
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close">
|
||||||
<vc:icon symbol="close"/>
|
<vc:icon symbol="close" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
@@ -211,7 +221,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<partial name="_StatusMessage"/>
|
<partial name="_StatusMessage" />
|
||||||
|
|
||||||
<div class="invoice-details">
|
<div class="invoice-details">
|
||||||
<div class="invoice-information mb-5">
|
<div class="invoice-information mb-5">
|
||||||
@@ -341,58 +351,59 @@
|
|||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
<div class="d-flex flex-column gap-5">
|
<div class="d-flex flex-column gap-5">
|
||||||
@if (!string.IsNullOrEmpty(Model.TypedMetadata.ItemCode) ||
|
@if (!string.IsNullOrEmpty(Model.TypedMetadata.ItemCode) ||
|
||||||
!string.IsNullOrEmpty(Model.TypedMetadata.ItemDesc) ||
|
!string.IsNullOrEmpty(Model.TypedMetadata.ItemDesc) ||
|
||||||
Model.TypedMetadata.TaxIncluded is not null)
|
Model.TypedMetadata.TaxIncluded is not null)
|
||||||
{
|
|
||||||
<div>
|
|
||||||
<h3 class="mb-3">
|
|
||||||
<span>Product Information</span>
|
|
||||||
<a href="https://docs.btcpayserver.org/Development/InvoiceMetadata/" target="_blank" rel="noreferrer noopener">
|
|
||||||
<vc:icon symbol="info" />
|
|
||||||
</a>
|
|
||||||
</h3>
|
|
||||||
<table class="table mb-0">
|
|
||||||
@if (!string.IsNullOrEmpty(Model.TypedMetadata.ItemCode))
|
|
||||||
{
|
|
||||||
<tr>
|
|
||||||
<th class="fw-semibold">Item code</th>
|
|
||||||
<td>@Model.TypedMetadata.ItemCode</td>
|
|
||||||
</tr>
|
|
||||||
}
|
|
||||||
@if (!string.IsNullOrEmpty(Model.TypedMetadata.ItemDesc))
|
|
||||||
{
|
|
||||||
<tr>
|
|
||||||
<th class="fw-semibold">Item Description</th>
|
|
||||||
<td>@Model.TypedMetadata.ItemDesc</td>
|
|
||||||
</tr>
|
|
||||||
}
|
|
||||||
@if (Model.TaxIncluded is not null)
|
|
||||||
{
|
|
||||||
<tr>
|
|
||||||
<th class="fw-semibold">Tax Included</th>
|
|
||||||
<td>@Model.TaxIncluded</td>
|
|
||||||
</tr>
|
|
||||||
}
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
@if (Model.TypedMetadata.BuyerName is not null ||
|
|
||||||
Model.TypedMetadata.BuyerEmail is not null ||
|
|
||||||
Model.TypedMetadata.BuyerPhone is not null ||
|
|
||||||
Model.TypedMetadata.BuyerAddress1 is not null ||
|
|
||||||
Model.TypedMetadata.BuyerAddress2 is not null ||
|
|
||||||
Model.TypedMetadata.BuyerCity is not null ||
|
|
||||||
Model.TypedMetadata.BuyerState is not null ||
|
|
||||||
Model.TypedMetadata.BuyerCountry is not null ||
|
|
||||||
Model.TypedMetadata.BuyerZip is not null)
|
|
||||||
{
|
{
|
||||||
<div>
|
<div>
|
||||||
<h3 class="mb-3"><span>Buyer Information</span>
|
<h3 class="mb-3">
|
||||||
<a href="https://docs.btcpayserver.org/Development/InvoiceMetadata/" target="_blank" rel="noreferrer noopener">
|
<span>Product Information</span>
|
||||||
<vc:icon symbol="info" />
|
<a href="https://docs.btcpayserver.org/Development/InvoiceMetadata/" target="_blank" rel="noreferrer noopener">
|
||||||
</a>
|
<vc:icon symbol="info" />
|
||||||
</h3>
|
</a>
|
||||||
|
</h3>
|
||||||
|
<table class="table mb-0">
|
||||||
|
@if (!string.IsNullOrEmpty(Model.TypedMetadata.ItemCode))
|
||||||
|
{
|
||||||
|
<tr>
|
||||||
|
<th class="fw-semibold">Item code</th>
|
||||||
|
<td>@Model.TypedMetadata.ItemCode</td>
|
||||||
|
</tr>
|
||||||
|
}
|
||||||
|
@if (!string.IsNullOrEmpty(Model.TypedMetadata.ItemDesc))
|
||||||
|
{
|
||||||
|
<tr>
|
||||||
|
<th class="fw-semibold">Item Description</th>
|
||||||
|
<td>@Model.TypedMetadata.ItemDesc</td>
|
||||||
|
</tr>
|
||||||
|
}
|
||||||
|
@if (Model.TaxIncluded is not null)
|
||||||
|
{
|
||||||
|
<tr>
|
||||||
|
<th class="fw-semibold">Tax Included</th>
|
||||||
|
<td>@Model.TaxIncluded</td>
|
||||||
|
</tr>
|
||||||
|
}
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
@if (Model.TypedMetadata.BuyerName is not null ||
|
||||||
|
Model.TypedMetadata.BuyerEmail is not null ||
|
||||||
|
Model.TypedMetadata.BuyerPhone is not null ||
|
||||||
|
Model.TypedMetadata.BuyerAddress1 is not null ||
|
||||||
|
Model.TypedMetadata.BuyerAddress2 is not null ||
|
||||||
|
Model.TypedMetadata.BuyerCity is not null ||
|
||||||
|
Model.TypedMetadata.BuyerState is not null ||
|
||||||
|
Model.TypedMetadata.BuyerCountry is not null ||
|
||||||
|
Model.TypedMetadata.BuyerZip is not null)
|
||||||
|
{
|
||||||
|
<div>
|
||||||
|
<h3 class="mb-3">
|
||||||
|
<span>Buyer Information</span>
|
||||||
|
<a href="https://docs.btcpayserver.org/Development/InvoiceMetadata/" target="_blank" rel="noreferrer noopener">
|
||||||
|
<vc:icon symbol="info" />
|
||||||
|
</a>
|
||||||
|
</h3>
|
||||||
<table class="table mb-0">
|
<table class="table mb-0">
|
||||||
@if (Model.TypedMetadata.BuyerName is not null)
|
@if (Model.TypedMetadata.BuyerName is not null)
|
||||||
{
|
{
|
||||||
@@ -465,12 +476,12 @@
|
|||||||
@if (Model.AdditionalData.Any())
|
@if (Model.AdditionalData.Any())
|
||||||
{
|
{
|
||||||
<div>
|
<div>
|
||||||
<h3 class="mb-3">
|
<h3 class="mb-3">
|
||||||
<span>Additional Information</span>
|
<span>Additional Information</span>
|
||||||
<a href="https://docs.btcpayserver.org/Development/InvoiceMetadata/" target="_blank" rel="noreferrer noopener">
|
<a href="https://docs.btcpayserver.org/Development/InvoiceMetadata/" target="_blank" rel="noreferrer noopener">
|
||||||
<vc:icon symbol="info" />
|
<vc:icon symbol="info" />
|
||||||
</a>
|
</a>
|
||||||
</h3>
|
</h3>
|
||||||
<partial name="PosData" model="(Model.AdditionalData, 1)" />
|
<partial name="PosData" model="(Model.AdditionalData, 1)" />
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
@@ -478,7 +489,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h3 class="mb-3">Invoice Summary</h3>
|
<h3 class="mb-3">Invoice Summary</h3>
|
||||||
<partial name="ListInvoicesPaymentsPartial" model="(Model, true)"/>
|
<partial name="ListInvoicesPaymentsPartial" model="(Model, true)" />
|
||||||
|
|
||||||
@if (Model.Deliveries.Any())
|
@if (Model.Deliveries.Any())
|
||||||
{
|
{
|
||||||
@@ -487,46 +498,49 @@
|
|||||||
<div class="table-responsive-xl">
|
<div class="table-responsive-xl">
|
||||||
<table class="table table-hover mb-5">
|
<table class="table table-hover mb-5">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Status</th>
|
<th>Status</th>
|
||||||
<th>ID</th>
|
<th>ID</th>
|
||||||
<th>Type</th>
|
<th>Type</th>
|
||||||
<th>Url</th>
|
<th>Url</th>
|
||||||
<th>Date</th>
|
<th>Date</th>
|
||||||
<th class="text-end">Action</th>
|
<th class="text-end">Action</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
@foreach (var delivery in Model.Deliveries)
|
@foreach (var delivery in Model.Deliveries)
|
||||||
{
|
{
|
||||||
<tr>
|
<tr>
|
||||||
<form asp-action="RedeliverWebhook"
|
<form asp-action="RedeliverWebhook"
|
||||||
asp-route-storeId="@Model.StoreId"
|
asp-route-storeId="@Model.StoreId"
|
||||||
asp-route-invoiceId="@Model.Id"
|
asp-route-invoiceId="@Model.Id"
|
||||||
asp-route-deliveryId="@delivery.Id"
|
asp-route-deliveryId="@delivery.Id"
|
||||||
method="post">
|
method="post">
|
||||||
<td>
|
<td>
|
||||||
<span>
|
<span>
|
||||||
@if (delivery.Success)
|
@if (delivery.Success)
|
||||||
{
|
{
|
||||||
<span class="fa fa-check text-success" title="Success"></span>
|
<span class="fa fa-check text-success" title="Success"></span>
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
<span class="fa fa-times text-danger" title="@delivery.ErrorMessage"></span>
|
<span class="fa fa-times text-danger" title="@delivery.ErrorMessage"></span>
|
||||||
}
|
}
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
|
@if (!delivery.Pruned)
|
||||||
|
{
|
||||||
<span>
|
<span>
|
||||||
<a asp-action="WebhookDelivery"
|
<a asp-action="WebhookDelivery"
|
||||||
asp-route-invoiceId="@Model.Id"
|
asp-route-invoiceId="@Model.Id"
|
||||||
asp-route-deliveryId="@delivery.Id"
|
asp-route-deliveryId="@delivery.Id"
|
||||||
class="delivery-content"
|
class="delivery-content"
|
||||||
target="_blank">
|
target="_blank">
|
||||||
@delivery.Id
|
@delivery.Id
|
||||||
</a>
|
</a>
|
||||||
</span>
|
</span>
|
||||||
|
}
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<span>@delivery.Type</span>
|
<span>@delivery.Type</span>
|
||||||
@@ -538,19 +552,21 @@
|
|||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<span>
|
<span>
|
||||||
@delivery.Time.ToBrowserDate()
|
@delivery.Time.ToBrowserDate()
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
<td class="text-end">
|
<td class="text-end">
|
||||||
|
@if (!delivery.Pruned) {
|
||||||
<button id="#redeliver-@delivery.Id"
|
<button id="#redeliver-@delivery.Id"
|
||||||
type="submit"
|
type="submit"
|
||||||
class="btn btn-link p-0 redeliver">
|
class="btn btn-link p-0 redeliver">
|
||||||
Redeliver
|
Redeliver
|
||||||
</button>
|
</button>
|
||||||
|
}
|
||||||
</td>
|
</td>
|
||||||
</form>
|
</form>
|
||||||
</tr>
|
</tr>
|
||||||
}
|
}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
@@ -564,36 +580,36 @@
|
|||||||
<div class="table-responsive-xl">
|
<div class="table-responsive-xl">
|
||||||
<table class="table table-hover mb-5">
|
<table class="table table-hover mb-5">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Pull Payment</th>
|
<th>Pull Payment</th>
|
||||||
<th>Amount</th>
|
<th>Amount</th>
|
||||||
<th>Date</th>
|
<th>Date</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
@foreach (var refund in Model.Refunds)
|
@foreach (var refund in Model.Refunds)
|
||||||
{
|
{
|
||||||
var blob = refund.PullPaymentData.GetBlob();
|
var blob = refund.PullPaymentData.GetBlob();
|
||||||
<tr>
|
<tr>
|
||||||
|
|
||||||
<td>
|
<td>
|
||||||
<span>
|
<span>
|
||||||
<a asp-action="ViewPullPayment" asp-controller="UIPullPayment"
|
<a asp-action="ViewPullPayment" asp-controller="UIPullPayment"
|
||||||
asp-route-pullPaymentId="@refund.PullPaymentDataId"
|
asp-route-pullPaymentId="@refund.PullPaymentDataId"
|
||||||
class="delivery-content"
|
class="delivery-content"
|
||||||
target="_blank">
|
target="_blank">
|
||||||
@refund.PullPaymentData.Id
|
@refund.PullPaymentData.Id
|
||||||
</a>
|
</a>
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<span>@blob.Limit @blob.Currency</span>
|
<span>@blob.Limit @blob.Currency</span>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<span> @refund.PullPaymentData.StartDate.ToBrowserDate() </span>
|
<span> @refund.PullPaymentData.StartDate.ToBrowserDate() </span>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
}
|
}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
@@ -603,19 +619,19 @@
|
|||||||
<h3 class="mb-0">Events</h3>
|
<h3 class="mb-0">Events</h3>
|
||||||
<table class="table table-hover mt-3 mb-4">
|
<table class="table table-hover mt-3 mb-4">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Date</th>
|
<th>Date</th>
|
||||||
<th>Message</th>
|
<th>Message</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
@foreach (var evt in Model.Events)
|
@foreach (var evt in Model.Events)
|
||||||
{
|
{
|
||||||
<tr class="text-@evt.GetCssClass()">
|
<tr class="text-@evt.GetCssClass()">
|
||||||
<td>@evt.Timestamp.ToBrowserDate()</td>
|
<td>@evt.Timestamp.ToBrowserDate()</td>
|
||||||
<td>@evt.Message</td>
|
<td>@evt.Message</td>
|
||||||
</tr>
|
</tr>
|
||||||
}
|
}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@@ -104,6 +104,7 @@
|
|||||||
{
|
{
|
||||||
<span class="d-flex align-items-center fa fa-times text-danger" title="@delivery.ErrorMessage"></span>
|
<span class="d-flex align-items-center fa fa-times text-danger" title="@delivery.ErrorMessage"></span>
|
||||||
}
|
}
|
||||||
|
@if (!delivery.Pruned) {
|
||||||
<span class="ms-3">
|
<span class="ms-3">
|
||||||
<a asp-action="WebhookDelivery"
|
<a asp-action="WebhookDelivery"
|
||||||
asp-route-storeId="@this.Context.GetRouteValue("storeId")"
|
asp-route-storeId="@this.Context.GetRouteValue("storeId")"
|
||||||
@@ -113,6 +114,7 @@
|
|||||||
@delivery.Id
|
@delivery.Id
|
||||||
</a>
|
</a>
|
||||||
</span>
|
</span>
|
||||||
|
}
|
||||||
</span>
|
</span>
|
||||||
<span class="d-flex align-items-center">
|
<span class="d-flex align-items-center">
|
||||||
<strong class="d-flex align-items-center text-muted small">
|
<strong class="d-flex align-items-center text-muted small">
|
||||||
|
|||||||
@@ -397,6 +397,9 @@
|
|||||||
},
|
},
|
||||||
"404": {
|
"404": {
|
||||||
"description": "The delivery does not exists."
|
"description": "The delivery does not exists."
|
||||||
|
},
|
||||||
|
"409": {
|
||||||
|
"description": "`webhookdelivery-pruned`: This webhook delivery has been pruned, so it can't be redelivered."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"security": [
|
"security": [
|
||||||
@@ -459,6 +462,9 @@
|
|||||||
},
|
},
|
||||||
"404": {
|
"404": {
|
||||||
"description": "The delivery does not exists."
|
"description": "The delivery does not exists."
|
||||||
|
},
|
||||||
|
"409": {
|
||||||
|
"description": "`webhookdelivery-pruned`: This webhook delivery has been pruned, so it can't be redelivered."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"security": [
|
"security": [
|
||||||
|
|||||||
Reference in New Issue
Block a user