mirror of
https://github.com/aljazceru/btcpayserver.git
synced 2025-12-17 14:04:26 +01:00
Fix: PaymentRequests created via API never expires (#6657)
This commit is contained in:
@@ -9,18 +9,18 @@ namespace BTCPayServer.Client;
|
|||||||
|
|
||||||
public partial class BTCPayServerClient
|
public partial class BTCPayServerClient
|
||||||
{
|
{
|
||||||
public virtual async Task<IEnumerable<PaymentRequestData>> GetPaymentRequests(string storeId,
|
public virtual async Task<IEnumerable<PaymentRequestBaseData>> GetPaymentRequests(string storeId,
|
||||||
bool includeArchived = false,
|
bool includeArchived = false,
|
||||||
CancellationToken token = default)
|
CancellationToken token = default)
|
||||||
{
|
{
|
||||||
return await SendHttpRequest<IEnumerable<PaymentRequestData>>($"api/v1/stores/{storeId}/payment-requests",
|
return await SendHttpRequest<IEnumerable<PaymentRequestBaseData>>($"api/v1/stores/{storeId}/payment-requests",
|
||||||
new Dictionary<string, object> { { nameof(includeArchived), includeArchived } }, HttpMethod.Get, token);
|
new Dictionary<string, object> { { nameof(includeArchived), includeArchived } }, HttpMethod.Get, token);
|
||||||
}
|
}
|
||||||
|
|
||||||
public virtual async Task<PaymentRequestData> GetPaymentRequest(string storeId, string paymentRequestId,
|
public virtual async Task<PaymentRequestBaseData> GetPaymentRequest(string storeId, string paymentRequestId,
|
||||||
CancellationToken token = default)
|
CancellationToken token = default)
|
||||||
{
|
{
|
||||||
return await SendHttpRequest<PaymentRequestData>($"api/v1/stores/{storeId}/payment-requests/{paymentRequestId}", null, HttpMethod.Get, token);
|
return await SendHttpRequest<PaymentRequestBaseData>($"api/v1/stores/{storeId}/payment-requests/{paymentRequestId}", null, HttpMethod.Get, token);
|
||||||
}
|
}
|
||||||
|
|
||||||
public virtual async Task ArchivePaymentRequest(string storeId, string paymentRequestId,
|
public virtual async Task ArchivePaymentRequest(string storeId, string paymentRequestId,
|
||||||
@@ -37,17 +37,17 @@ public partial class BTCPayServerClient
|
|||||||
return await SendHttpRequest<InvoiceData>($"api/v1/stores/{storeId}/payment-requests/{paymentRequestId}/pay", request, HttpMethod.Post, token);
|
return await SendHttpRequest<InvoiceData>($"api/v1/stores/{storeId}/payment-requests/{paymentRequestId}/pay", request, HttpMethod.Post, token);
|
||||||
}
|
}
|
||||||
|
|
||||||
public virtual async Task<PaymentRequestData> CreatePaymentRequest(string storeId,
|
public virtual async Task<PaymentRequestBaseData> CreatePaymentRequest(string storeId,
|
||||||
CreatePaymentRequestRequest request, CancellationToken token = default)
|
PaymentRequestBaseData request, CancellationToken token = default)
|
||||||
{
|
{
|
||||||
if (request == null) throw new ArgumentNullException(nameof(request));
|
if (request == null) throw new ArgumentNullException(nameof(request));
|
||||||
return await SendHttpRequest<PaymentRequestData>($"api/v1/stores/{storeId}/payment-requests", request, HttpMethod.Post, token);
|
return await SendHttpRequest<PaymentRequestBaseData>($"api/v1/stores/{storeId}/payment-requests", request, HttpMethod.Post, token);
|
||||||
}
|
}
|
||||||
|
|
||||||
public virtual async Task<PaymentRequestData> UpdatePaymentRequest(string storeId, string paymentRequestId,
|
public virtual async Task<PaymentRequestBaseData> UpdatePaymentRequest(string storeId, string paymentRequestId,
|
||||||
UpdatePaymentRequestRequest request, CancellationToken token = default)
|
PaymentRequestBaseData request, CancellationToken token = default)
|
||||||
{
|
{
|
||||||
if (request == null) throw new ArgumentNullException(nameof(request));
|
if (request == null) throw new ArgumentNullException(nameof(request));
|
||||||
return await SendHttpRequest<PaymentRequestData>($"api/v1/stores/{storeId}/payment-requests/{paymentRequestId}", request, HttpMethod.Put, token);
|
return await SendHttpRequest<PaymentRequestBaseData>($"api/v1/stores/{storeId}/payment-requests/{paymentRequestId}", request, HttpMethod.Put, token);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +0,0 @@
|
|||||||
namespace BTCPayServer.Client.Models
|
|
||||||
{
|
|
||||||
public class CreatePaymentRequestRequest : PaymentRequestBaseData
|
|
||||||
{
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -2,10 +2,18 @@ using System;
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using BTCPayServer.JsonConverters;
|
using BTCPayServer.JsonConverters;
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
|
using Newtonsoft.Json.Converters;
|
||||||
using Newtonsoft.Json.Linq;
|
using Newtonsoft.Json.Linq;
|
||||||
|
|
||||||
namespace BTCPayServer.Client.Models
|
namespace BTCPayServer.Client.Models
|
||||||
{
|
{
|
||||||
|
public enum PaymentRequestStatus
|
||||||
|
{
|
||||||
|
Pending,
|
||||||
|
Completed,
|
||||||
|
Expired,
|
||||||
|
Processing,
|
||||||
|
}
|
||||||
public class PaymentRequestBaseData
|
public class PaymentRequestBaseData
|
||||||
{
|
{
|
||||||
public string StoreId { get; set; }
|
public string StoreId { get; set; }
|
||||||
@@ -19,11 +27,17 @@ namespace BTCPayServer.Client.Models
|
|||||||
public string Email { get; set; }
|
public string Email { get; set; }
|
||||||
public bool AllowCustomPaymentAmounts { get; set; }
|
public bool AllowCustomPaymentAmounts { get; set; }
|
||||||
|
|
||||||
[JsonExtensionData]
|
[JsonConverter(typeof(StringEnumConverter))]
|
||||||
public IDictionary<string, JToken> AdditionalData { get; set; }
|
public PaymentRequestStatus Status { get; set; }
|
||||||
|
[JsonConverter(typeof(NBitcoin.JsonConverters.DateTimeToUnixTimeConverter))]
|
||||||
|
public DateTimeOffset CreatedTime { get; set; }
|
||||||
|
public string Id { get; set; }
|
||||||
|
public bool Archived { get; set; }
|
||||||
|
|
||||||
public string FormId { get; set; }
|
public string FormId { get; set; }
|
||||||
|
|
||||||
public JObject FormResponse { get; set; }
|
public JObject FormResponse { get; set; }
|
||||||
|
|
||||||
|
[JsonExtensionData]
|
||||||
|
public IDictionary<string, JToken> AdditionalData { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,23 +0,0 @@
|
|||||||
using System;
|
|
||||||
using Newtonsoft.Json;
|
|
||||||
using Newtonsoft.Json.Converters;
|
|
||||||
|
|
||||||
namespace BTCPayServer.Client.Models
|
|
||||||
{
|
|
||||||
public class PaymentRequestData : PaymentRequestBaseData
|
|
||||||
{
|
|
||||||
[JsonConverter(typeof(StringEnumConverter))]
|
|
||||||
public PaymentRequestStatus Status { get; set; }
|
|
||||||
[JsonConverter(typeof(NBitcoin.JsonConverters.DateTimeToUnixTimeConverter))]
|
|
||||||
public DateTimeOffset CreatedTime { get; set; }
|
|
||||||
public string Id { get; set; }
|
|
||||||
public bool Archived { get; set; }
|
|
||||||
public enum PaymentRequestStatus
|
|
||||||
{
|
|
||||||
Pending = 0,
|
|
||||||
Completed = 1,
|
|
||||||
Expired = 2,
|
|
||||||
Processing = 3
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
namespace BTCPayServer.Client.Models
|
|
||||||
{
|
|
||||||
public class UpdatePaymentRequestRequest : PaymentRequestBaseData
|
|
||||||
{
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -30,7 +30,7 @@ namespace BTCPayServer.Client.Models
|
|||||||
}
|
}
|
||||||
|
|
||||||
[JsonProperty(Order = 2)] public string PaymentRequestId { get; set; }
|
[JsonProperty(Order = 2)] public string PaymentRequestId { get; set; }
|
||||||
[JsonProperty(Order = 3)] [JsonConverter(typeof(StringEnumConverter))]public PaymentRequestData.PaymentRequestStatus Status { get; set; }
|
[JsonProperty(Order = 3)] [JsonConverter(typeof(StringEnumConverter))]public PaymentRequestStatus Status { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public abstract class StoreWebhookEvent : WebhookEvent
|
public abstract class StoreWebhookEvent : WebhookEvent
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.ComponentModel.DataAnnotations.Schema;
|
using System.ComponentModel.DataAnnotations.Schema;
|
||||||
|
using System.Globalization;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Reflection.Metadata;
|
using System.Reflection.Metadata;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
@@ -17,7 +18,7 @@ namespace BTCPayServer.Data
|
|||||||
public bool TryMigrate()
|
public bool TryMigrate()
|
||||||
{
|
{
|
||||||
#pragma warning disable CS0618 // Type or member is obsolete
|
#pragma warning disable CS0618 // Type or member is obsolete
|
||||||
if (Blob is null && Blob2 is not null)
|
if (Blob is (null or { Length: 0 }) && Blob2 is not null && Currency is not null)
|
||||||
return false;
|
return false;
|
||||||
if (Blob2 is null)
|
if (Blob2 is null)
|
||||||
{
|
{
|
||||||
@@ -28,11 +29,27 @@ namespace BTCPayServer.Data
|
|||||||
#pragma warning restore CS0618 // Type or member is obsolete
|
#pragma warning restore CS0618 // Type or member is obsolete
|
||||||
var jobj = JObject.Parse(Blob2);
|
var jobj = JObject.Parse(Blob2);
|
||||||
// Fixup some legacy payment requests
|
// Fixup some legacy payment requests
|
||||||
if (jobj["expiryDate"].Type == JTokenType.Date)
|
if (jobj["expiryDate"]?.Type == JTokenType.Date)
|
||||||
{
|
{
|
||||||
jobj["expiryDate"] = new JValue(NBitcoin.Utils.DateTimeToUnixTime(jobj["expiryDate"].Value<DateTime>()));
|
var date = NBitcoin.Utils.UnixTimeToDateTime(NBitcoin.Utils.DateTimeToUnixTime(jobj["expiryDate"].Value<DateTime>()));
|
||||||
Blob2 = jobj.ToString(Newtonsoft.Json.Formatting.None);
|
jobj.Remove("expiryDate");
|
||||||
|
Expiry = date;
|
||||||
}
|
}
|
||||||
|
else if (jobj["expiryDate"]?.Type == JTokenType.Integer)
|
||||||
|
{
|
||||||
|
var date = NBitcoin.Utils.UnixTimeToDateTime(jobj["expiryDate"].Value<long>());
|
||||||
|
jobj.Remove("expiryDate");
|
||||||
|
Expiry = date;
|
||||||
|
}
|
||||||
|
Currency = jobj["currency"].Value<string>();
|
||||||
|
Amount = jobj["amount"] switch
|
||||||
|
{
|
||||||
|
JValue jv when jv.Type == JTokenType.Float => jv.Value<decimal>(),
|
||||||
|
JValue jv when jv.Type == JTokenType.Integer => jv.Value<long>(),
|
||||||
|
JValue jv when jv.Type == JTokenType.String && decimal.TryParse(jv.Value<string>(), CultureInfo.InvariantCulture, out var d) => d,
|
||||||
|
_ => 0m
|
||||||
|
};
|
||||||
|
Blob2 = jobj.ToString(Newtonsoft.Json.Formatting.None);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,12 +8,15 @@ namespace BTCPayServer.Data
|
|||||||
{
|
{
|
||||||
public string Id { get; set; }
|
public string Id { get; set; }
|
||||||
public DateTimeOffset Created { get; set; }
|
public DateTimeOffset Created { get; set; }
|
||||||
|
public DateTimeOffset? Expiry { get; set; }
|
||||||
public string StoreDataId { get; set; }
|
public string StoreDataId { get; set; }
|
||||||
public bool Archived { get; set; }
|
public bool Archived { get; set; }
|
||||||
|
public string Currency { get; set; }
|
||||||
|
public decimal Amount { get; set; }
|
||||||
|
|
||||||
public StoreData StoreData { get; set; }
|
public StoreData StoreData { get; set; }
|
||||||
|
|
||||||
public Client.Models.PaymentRequestData.PaymentRequestStatus Status { get; set; }
|
public Client.Models.PaymentRequestStatus Status { get; set; }
|
||||||
|
|
||||||
[Obsolete("Use Blob2 instead")]
|
[Obsolete("Use Blob2 instead")]
|
||||||
public byte[] Blob { get; set; }
|
public byte[] Blob { get; set; }
|
||||||
@@ -35,6 +38,9 @@ namespace BTCPayServer.Data
|
|||||||
builder.Entity<PaymentRequestData>()
|
builder.Entity<PaymentRequestData>()
|
||||||
.Property(o => o.Blob2)
|
.Property(o => o.Blob2)
|
||||||
.HasColumnType("JSONB");
|
.HasColumnType("JSONB");
|
||||||
|
builder.Entity<PaymentRequestData>()
|
||||||
|
.Property(p => p.Status)
|
||||||
|
.HasConversion<string>();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
55
BTCPayServer.Data/Migrations/20250407133937_pr_expiry.cs
Normal file
55
BTCPayServer.Data/Migrations/20250407133937_pr_expiry.cs
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
using System;
|
||||||
|
using BTCPayServer.Data;
|
||||||
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace BTCPayServer.Migrations
|
||||||
|
{
|
||||||
|
[DbContext(typeof(ApplicationDbContext))]
|
||||||
|
[Migration("20250407133937_pr_expiry")]
|
||||||
|
public partial class pr_expiry : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.AddColumn<DateTimeOffset>(
|
||||||
|
name: "Expiry",
|
||||||
|
table: "PaymentRequests",
|
||||||
|
type: "timestamp with time zone",
|
||||||
|
nullable: true);
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<decimal>(
|
||||||
|
name: "Amount",
|
||||||
|
table: "PaymentRequests",
|
||||||
|
type: "numeric",
|
||||||
|
nullable: false,
|
||||||
|
defaultValue: 0);
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<string>(
|
||||||
|
name: "Currency",
|
||||||
|
table: "PaymentRequests",
|
||||||
|
type: "text",
|
||||||
|
nullable: true);
|
||||||
|
migrationBuilder.Sql("""
|
||||||
|
ALTER TABLE "PaymentRequests" ADD COLUMN "StatusNew" TEXT;
|
||||||
|
UPDATE "PaymentRequests" SET "StatusNew" = CASE
|
||||||
|
WHEN "Status" = 0 THEN 'Pending'
|
||||||
|
WHEN "Status" = 1 THEN 'Processing'
|
||||||
|
WHEN "Status" = 2 THEN 'Completed'
|
||||||
|
WHEN "Status" = 3 THEN 'Expired'
|
||||||
|
ELSE NULL
|
||||||
|
END;
|
||||||
|
ALTER TABLE "PaymentRequests" DROP COLUMN "Status";
|
||||||
|
ALTER TABLE "PaymentRequests" RENAME COLUMN "StatusNew" TO "Status";
|
||||||
|
ALTER TABLE "PaymentRequests" ALTER COLUMN "Status" SET NOT NULL;
|
||||||
|
""");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -18,7 +18,7 @@ namespace BTCPayServer.Migrations
|
|||||||
{
|
{
|
||||||
#pragma warning disable 612, 618
|
#pragma warning disable 612, 618
|
||||||
modelBuilder
|
modelBuilder
|
||||||
.HasAnnotation("ProductVersion", "8.0.7")
|
.HasAnnotation("ProductVersion", "8.0.11")
|
||||||
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||||
|
|
||||||
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||||
@@ -521,6 +521,9 @@ namespace BTCPayServer.Migrations
|
|||||||
b.Property<string>("Id")
|
b.Property<string>("Id")
|
||||||
.HasColumnType("text");
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<decimal>("Amount")
|
||||||
|
.HasColumnType("numeric");
|
||||||
|
|
||||||
b.Property<bool>("Archived")
|
b.Property<bool>("Archived")
|
||||||
.HasColumnType("boolean");
|
.HasColumnType("boolean");
|
||||||
|
|
||||||
@@ -535,8 +538,15 @@ namespace BTCPayServer.Migrations
|
|||||||
.HasColumnType("timestamp with time zone")
|
.HasColumnType("timestamp with time zone")
|
||||||
.HasDefaultValue(new DateTimeOffset(new DateTime(1970, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)));
|
.HasDefaultValue(new DateTimeOffset(new DateTime(1970, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)));
|
||||||
|
|
||||||
b.Property<int>("Status")
|
b.Property<string>("Currency")
|
||||||
.HasColumnType("integer");
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset?>("Expiry")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<string>("Status")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
b.Property<string>("StoreDataId")
|
b.Property<string>("StoreDataId")
|
||||||
.HasColumnType("text");
|
.HasColumnType("text");
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ using Microsoft.AspNetCore.Mvc;
|
|||||||
using Microsoft.Extensions.Configuration;
|
using Microsoft.Extensions.Configuration;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using Microsoft.Extensions.DependencyInjection.Extensions;
|
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||||
|
using Microsoft.Extensions.Hosting;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using NBitcoin;
|
using NBitcoin;
|
||||||
using NBXplorer;
|
using NBXplorer;
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ namespace BTCPayServer.Tests
|
|||||||
public class DatabaseTester
|
public class DatabaseTester
|
||||||
{
|
{
|
||||||
private readonly ILoggerFactory _loggerFactory;
|
private readonly ILoggerFactory _loggerFactory;
|
||||||
private readonly string dbname;
|
public readonly string dbname;
|
||||||
private string[] notAppliedMigrations;
|
private string[] notAppliedMigrations;
|
||||||
|
|
||||||
public DatabaseTester(ILog log, ILoggerFactory loggerFactory)
|
public DatabaseTester(ILog log, ILoggerFactory loggerFactory)
|
||||||
|
|||||||
@@ -2030,25 +2030,25 @@ namespace BTCPayServer.Tests
|
|||||||
//validation errors
|
//validation errors
|
||||||
await AssertValidationError(new[] { "Amount" }, async () =>
|
await AssertValidationError(new[] { "Amount" }, async () =>
|
||||||
{
|
{
|
||||||
await client.CreatePaymentRequest(user.StoreId, new CreatePaymentRequestRequest() { Title = "A" });
|
await client.CreatePaymentRequest(user.StoreId, new() { Title = "A" });
|
||||||
});
|
});
|
||||||
await AssertValidationError(new[] { "Amount" }, async () =>
|
await AssertValidationError(new[] { "Amount" }, async () =>
|
||||||
{
|
{
|
||||||
await client.CreatePaymentRequest(user.StoreId,
|
await client.CreatePaymentRequest(user.StoreId,
|
||||||
new CreatePaymentRequestRequest() { Title = "A", Currency = "BTC", Amount = 0 });
|
new() { Title = "A", Currency = "BTC", Amount = 0 });
|
||||||
});
|
});
|
||||||
await AssertValidationError(new[] { "Currency" }, async () =>
|
await AssertValidationError(new[] { "Currency" }, async () =>
|
||||||
{
|
{
|
||||||
await client.CreatePaymentRequest(user.StoreId,
|
await client.CreatePaymentRequest(user.StoreId,
|
||||||
new CreatePaymentRequestRequest() { Title = "A", Currency = "helloinvalid", Amount = 1 });
|
new() { Title = "A", Currency = "helloinvalid", Amount = 1 });
|
||||||
});
|
});
|
||||||
await AssertHttpError(403, async () =>
|
await AssertHttpError(403, async () =>
|
||||||
{
|
{
|
||||||
await viewOnly.CreatePaymentRequest(user.StoreId,
|
await viewOnly.CreatePaymentRequest(user.StoreId,
|
||||||
new CreatePaymentRequestRequest() { Title = "A", Currency = "helloinvalid", Amount = 1 });
|
new() { Title = "A", Currency = "helloinvalid", Amount = 1 });
|
||||||
});
|
});
|
||||||
var newPaymentRequest = await client.CreatePaymentRequest(user.StoreId,
|
var newPaymentRequest = await client.CreatePaymentRequest(user.StoreId,
|
||||||
new CreatePaymentRequestRequest() { Title = "A", Currency = "USD", Amount = 1 });
|
new() { Title = "A", Currency = "USD", Amount = 1 });
|
||||||
|
|
||||||
//list payment request
|
//list payment request
|
||||||
var paymentRequests = await viewOnly.GetPaymentRequests(user.StoreId);
|
var paymentRequests = await viewOnly.GetPaymentRequests(user.StoreId);
|
||||||
@@ -2063,7 +2063,7 @@ namespace BTCPayServer.Tests
|
|||||||
Assert.Equal(newPaymentRequest.StoreId, user.StoreId);
|
Assert.Equal(newPaymentRequest.StoreId, user.StoreId);
|
||||||
|
|
||||||
//update payment request
|
//update payment request
|
||||||
var updateRequest = JObject.FromObject(paymentRequest).ToObject<UpdatePaymentRequestRequest>();
|
var updateRequest = paymentRequest;
|
||||||
updateRequest.Title = "B";
|
updateRequest.Title = "B";
|
||||||
await AssertHttpError(403, async () =>
|
await AssertHttpError(403, async () =>
|
||||||
{
|
{
|
||||||
@@ -2086,7 +2086,7 @@ namespace BTCPayServer.Tests
|
|||||||
//let's test some payment stuff with the UI
|
//let's test some payment stuff with the UI
|
||||||
await user.RegisterDerivationSchemeAsync("BTC");
|
await user.RegisterDerivationSchemeAsync("BTC");
|
||||||
var paymentTestPaymentRequest = await client.CreatePaymentRequest(user.StoreId,
|
var paymentTestPaymentRequest = await client.CreatePaymentRequest(user.StoreId,
|
||||||
new CreatePaymentRequestRequest() { Amount = 0.1m, Currency = "BTC", Title = "Payment test title" });
|
new() { Amount = 0.1m, Currency = "BTC", Title = "Payment test title" });
|
||||||
|
|
||||||
var invoiceId = Assert.IsType<string>(Assert.IsType<OkObjectResult>(await user.GetController<UIPaymentRequestController>()
|
var invoiceId = Assert.IsType<string>(Assert.IsType<OkObjectResult>(await user.GetController<UIPaymentRequestController>()
|
||||||
.PayPaymentRequest(paymentTestPaymentRequest.Id, false)).Value);
|
.PayPaymentRequest(paymentTestPaymentRequest.Id, false)).Value);
|
||||||
@@ -2105,37 +2105,37 @@ namespace BTCPayServer.Tests
|
|||||||
{
|
{
|
||||||
Assert.Equal(Invoice.STATUS_PAID, (await user.BitPay.GetInvoiceAsync(invoiceId)).Status);
|
Assert.Equal(Invoice.STATUS_PAID, (await user.BitPay.GetInvoiceAsync(invoiceId)).Status);
|
||||||
if (!partialPayment)
|
if (!partialPayment)
|
||||||
Assert.Equal(PaymentRequestData.PaymentRequestStatus.Processing, (await client.GetPaymentRequest(user.StoreId, paymentTestPaymentRequest.Id)).Status);
|
Assert.Equal(PaymentRequestStatus.Processing, (await client.GetPaymentRequest(user.StoreId, paymentTestPaymentRequest.Id)).Status);
|
||||||
});
|
});
|
||||||
await tester.ExplorerNode.GenerateAsync(1);
|
await tester.ExplorerNode.GenerateAsync(1);
|
||||||
await TestUtils.EventuallyAsync(async () =>
|
await TestUtils.EventuallyAsync(async () =>
|
||||||
{
|
{
|
||||||
Assert.Equal(Invoice.STATUS_COMPLETE, (await user.BitPay.GetInvoiceAsync(invoiceId)).Status);
|
Assert.Equal(Invoice.STATUS_COMPLETE, (await user.BitPay.GetInvoiceAsync(invoiceId)).Status);
|
||||||
if (!partialPayment)
|
if (!partialPayment)
|
||||||
Assert.Equal(PaymentRequestData.PaymentRequestStatus.Completed, (await client.GetPaymentRequest(user.StoreId, paymentTestPaymentRequest.Id)).Status);
|
Assert.Equal(PaymentRequestStatus.Completed, (await client.GetPaymentRequest(user.StoreId, paymentTestPaymentRequest.Id)).Status);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
await Pay(invoiceId);
|
await Pay(invoiceId);
|
||||||
|
|
||||||
//Same thing, but with the API
|
//Same thing, but with the API
|
||||||
paymentTestPaymentRequest = await client.CreatePaymentRequest(user.StoreId,
|
paymentTestPaymentRequest = await client.CreatePaymentRequest(user.StoreId,
|
||||||
new CreatePaymentRequestRequest() { Amount = 0.1m, Currency = "BTC", Title = "Payment test title" });
|
new() { Amount = 0.1m, Currency = "BTC", Title = "Payment test title" });
|
||||||
var paidPrId = paymentTestPaymentRequest.Id;
|
var paidPrId = paymentTestPaymentRequest.Id;
|
||||||
var invoiceData = await client.PayPaymentRequest(user.StoreId, paymentTestPaymentRequest.Id, new PayPaymentRequestRequest());
|
var invoiceData = await client.PayPaymentRequest(user.StoreId, paymentTestPaymentRequest.Id, new PayPaymentRequestRequest());
|
||||||
await Pay(invoiceData.Id);
|
await Pay(invoiceData.Id);
|
||||||
|
|
||||||
// Can't update amount once invoice has been created
|
// Can't update amount once invoice has been created
|
||||||
await AssertValidationError(new[] { "Amount" }, () => client.UpdatePaymentRequest(user.StoreId, paymentTestPaymentRequest.Id, new UpdatePaymentRequestRequest()
|
await AssertValidationError(new[] { "Amount" }, () => client.UpdatePaymentRequest(user.StoreId, paymentTestPaymentRequest.Id, new()
|
||||||
{
|
{
|
||||||
Amount = 294m
|
Amount = 294m
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// Let's tests some unhappy path
|
// Let's tests some unhappy path
|
||||||
paymentTestPaymentRequest = await client.CreatePaymentRequest(user.StoreId,
|
paymentTestPaymentRequest = await client.CreatePaymentRequest(user.StoreId,
|
||||||
new CreatePaymentRequestRequest() { Amount = 0.1m, AllowCustomPaymentAmounts = false, Currency = "BTC", Title = "Payment test title" });
|
new() { Amount = 0.1m, AllowCustomPaymentAmounts = false, Currency = "BTC", Title = "Payment test title" });
|
||||||
await AssertValidationError(new[] { "Amount" }, () => client.PayPaymentRequest(user.StoreId, paymentTestPaymentRequest.Id, new PayPaymentRequestRequest() { Amount = -0.04m }));
|
await AssertValidationError(new[] { "Amount" }, () => client.PayPaymentRequest(user.StoreId, paymentTestPaymentRequest.Id, new PayPaymentRequestRequest() { Amount = -0.04m }));
|
||||||
await AssertValidationError(new[] { "Amount" }, () => client.PayPaymentRequest(user.StoreId, paymentTestPaymentRequest.Id, new PayPaymentRequestRequest() { Amount = 0.04m }));
|
await AssertValidationError(new[] { "Amount" }, () => client.PayPaymentRequest(user.StoreId, paymentTestPaymentRequest.Id, new PayPaymentRequestRequest() { Amount = 0.04m }));
|
||||||
await client.UpdatePaymentRequest(user.StoreId, paymentTestPaymentRequest.Id, new UpdatePaymentRequestRequest()
|
await client.UpdatePaymentRequest(user.StoreId, paymentTestPaymentRequest.Id, new()
|
||||||
{
|
{
|
||||||
Amount = 0.1m,
|
Amount = 0.1m,
|
||||||
AllowCustomPaymentAmounts = true,
|
AllowCustomPaymentAmounts = true,
|
||||||
@@ -2148,7 +2148,7 @@ namespace BTCPayServer.Tests
|
|||||||
var firstPaymentId = invoiceData.Id;
|
var firstPaymentId = invoiceData.Id;
|
||||||
await AssertAPIError("archived", () => client.PayPaymentRequest(user.StoreId, archivedPrId, new PayPaymentRequestRequest()));
|
await AssertAPIError("archived", () => client.PayPaymentRequest(user.StoreId, archivedPrId, new PayPaymentRequestRequest()));
|
||||||
|
|
||||||
await client.UpdatePaymentRequest(user.StoreId, paymentTestPaymentRequest.Id, new UpdatePaymentRequestRequest()
|
await client.UpdatePaymentRequest(user.StoreId, paymentTestPaymentRequest.Id, new()
|
||||||
{
|
{
|
||||||
Amount = 0.1m,
|
Amount = 0.1m,
|
||||||
AllowCustomPaymentAmounts = true,
|
AllowCustomPaymentAmounts = true,
|
||||||
@@ -2160,7 +2160,7 @@ namespace BTCPayServer.Tests
|
|||||||
await AssertAPIError("expired", () => client.PayPaymentRequest(user.StoreId, paymentTestPaymentRequest.Id, new PayPaymentRequestRequest()));
|
await AssertAPIError("expired", () => client.PayPaymentRequest(user.StoreId, paymentTestPaymentRequest.Id, new PayPaymentRequestRequest()));
|
||||||
await AssertAPIError("already-paid", () => client.PayPaymentRequest(user.StoreId, paidPrId, new PayPaymentRequestRequest()));
|
await AssertAPIError("already-paid", () => client.PayPaymentRequest(user.StoreId, paidPrId, new PayPaymentRequestRequest()));
|
||||||
|
|
||||||
await client.UpdatePaymentRequest(user.StoreId, paymentTestPaymentRequest.Id, new UpdatePaymentRequestRequest()
|
await client.UpdatePaymentRequest(user.StoreId, paymentTestPaymentRequest.Id, new()
|
||||||
{
|
{
|
||||||
Amount = 0.1m,
|
Amount = 0.1m,
|
||||||
AllowCustomPaymentAmounts = true,
|
AllowCustomPaymentAmounts = true,
|
||||||
|
|||||||
@@ -72,12 +72,11 @@ using Xunit;
|
|||||||
using Xunit.Abstractions;
|
using Xunit.Abstractions;
|
||||||
using Xunit.Sdk;
|
using Xunit.Sdk;
|
||||||
using CreateInvoiceRequest = BTCPayServer.Client.Models.CreateInvoiceRequest;
|
using CreateInvoiceRequest = BTCPayServer.Client.Models.CreateInvoiceRequest;
|
||||||
using CreatePaymentRequestRequest = BTCPayServer.Client.Models.CreatePaymentRequestRequest;
|
|
||||||
using MarkPayoutRequest = BTCPayServer.Client.Models.MarkPayoutRequest;
|
using MarkPayoutRequest = BTCPayServer.Client.Models.MarkPayoutRequest;
|
||||||
using PaymentRequestData = BTCPayServer.Client.Models.PaymentRequestData;
|
|
||||||
using RatesViewModel = BTCPayServer.Models.StoreViewModels.RatesViewModel;
|
using RatesViewModel = BTCPayServer.Models.StoreViewModels.RatesViewModel;
|
||||||
using Microsoft.Extensions.Caching.Memory;
|
using Microsoft.Extensions.Caching.Memory;
|
||||||
using PosViewType = BTCPayServer.Client.Models.PosViewType;
|
using PosViewType = BTCPayServer.Client.Models.PosViewType;
|
||||||
|
using BTCPayServer.PaymentRequest;
|
||||||
|
|
||||||
namespace BTCPayServer.Tests
|
namespace BTCPayServer.Tests
|
||||||
{
|
{
|
||||||
@@ -2087,7 +2086,7 @@ namespace BTCPayServer.Tests
|
|||||||
await user.AssertHasWebhookEvent(WebhookEventType.InvoiceInvalid, (WebhookInvoiceEvent x)=> Assert.Equal(invoice.Id, x.InvoiceId));
|
await user.AssertHasWebhookEvent(WebhookEventType.InvoiceInvalid, (WebhookInvoiceEvent x)=> Assert.Equal(invoice.Id, x.InvoiceId));
|
||||||
|
|
||||||
//payment request webhook test
|
//payment request webhook test
|
||||||
var pr = await client.CreatePaymentRequest(user.StoreId, new CreatePaymentRequestRequest()
|
var pr = await client.CreatePaymentRequest(user.StoreId, new ()
|
||||||
{
|
{
|
||||||
Amount = 100m,
|
Amount = 100m,
|
||||||
Currency = "USD",
|
Currency = "USD",
|
||||||
@@ -2100,7 +2099,7 @@ namespace BTCPayServer.Tests
|
|||||||
});
|
});
|
||||||
await user.AssertHasWebhookEvent(WebhookEventType.PaymentRequestCreated, (WebhookPaymentRequestEvent x)=> Assert.Equal(pr.Id, x.PaymentRequestId));
|
await user.AssertHasWebhookEvent(WebhookEventType.PaymentRequestCreated, (WebhookPaymentRequestEvent x)=> Assert.Equal(pr.Id, x.PaymentRequestId));
|
||||||
pr = await client.UpdatePaymentRequest(user.StoreId, pr.Id,
|
pr = await client.UpdatePaymentRequest(user.StoreId, pr.Id,
|
||||||
new UpdatePaymentRequestRequest() { Title = "test pr updated", Amount = 100m,
|
new() { Title = "test pr updated", Amount = 100m,
|
||||||
Currency = "USD",
|
Currency = "USD",
|
||||||
//TODO: this is a bug, we should not have these props in create request
|
//TODO: this is a bug, we should not have these props in create request
|
||||||
StoreId = user.StoreId,
|
StoreId = user.StoreId,
|
||||||
@@ -2113,7 +2112,7 @@ namespace BTCPayServer.Tests
|
|||||||
await client.MarkInvoiceStatus(user.StoreId, inv.Id, new MarkInvoiceStatusRequest() { Status = InvoiceStatus.Settled});
|
await client.MarkInvoiceStatus(user.StoreId, inv.Id, new MarkInvoiceStatusRequest() { Status = InvoiceStatus.Settled});
|
||||||
await user.AssertHasWebhookEvent(WebhookEventType.PaymentRequestStatusChanged, (WebhookPaymentRequestEvent x)=>
|
await user.AssertHasWebhookEvent(WebhookEventType.PaymentRequestStatusChanged, (WebhookPaymentRequestEvent x)=>
|
||||||
{
|
{
|
||||||
Assert.Equal(PaymentRequestData.PaymentRequestStatus.Completed, x.Status);
|
Assert.Equal(PaymentRequestStatus.Completed, x.Status);
|
||||||
Assert.Equal(pr.Id, x.PaymentRequestId);
|
Assert.Equal(pr.Id, x.PaymentRequestId);
|
||||||
});
|
});
|
||||||
await client.ArchivePaymentRequest(user.StoreId, pr.Id);
|
await client.ArchivePaymentRequest(user.StoreId, pr.Id);
|
||||||
@@ -2835,6 +2834,128 @@ namespace BTCPayServer.Tests
|
|||||||
Assert.Equal("coingecko", b.PreferredExchange);
|
Assert.Equal("coingecko", b.PreferredExchange);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact(Timeout = LongRunningTestTimeout)]
|
||||||
|
[Trait("Integration", "Integration")]
|
||||||
|
public async Task CanMigratePaymentRequests()
|
||||||
|
{
|
||||||
|
var tester = CreateDBTester();
|
||||||
|
await tester.MigrateUntil("20250407133937_pr_expiry");
|
||||||
|
using var ctx = tester.CreateContext();
|
||||||
|
var conn = ctx.Database.GetDbConnection();
|
||||||
|
await conn.ExecuteAsync("""
|
||||||
|
INSERT INTO "Stores" ("Id", "SpeedPolicy") VALUES ('7Nefq9u8DDYL56HAFskWdHNQ9ZCdfbkVWkH4xYhfzxiB', 0);
|
||||||
|
INSERT INTO "PaymentRequests" (
|
||||||
|
"Id", "StoreDataId", "Status", "Blob", "Created", "Archived", "Blob2"
|
||||||
|
) VALUES (
|
||||||
|
'03463aab-844e-4d60-872f-26310b856131',
|
||||||
|
'7Nefq9u8DDYL56HAFskWdHNQ9ZCdfbkVWkH4xYhfzxiB',
|
||||||
|
2,
|
||||||
|
NULL,
|
||||||
|
'2024-03-21 23:15:26.356677+00',
|
||||||
|
FALSE,
|
||||||
|
'{"email": "f.f@gmail.com", "title": "Online", "amount": 0.001, "formId": null, "storeId": null, "currency": "USD", "expiryDate": 1711066440, "description": null, "embeddedCSS": null, "formResponse": null, "customCSSLink": null, "allowCustomPaymentAmounts": false}'
|
||||||
|
);
|
||||||
|
|
||||||
|
INSERT INTO "PaymentRequests" (
|
||||||
|
"Id", "StoreDataId", "Status", "Blob", "Created", "Archived", "Blob2"
|
||||||
|
) VALUES (
|
||||||
|
'other',
|
||||||
|
'7Nefq9u8DDYL56HAFskWdHNQ9ZCdfbkVWkH4xYhfzxiB',
|
||||||
|
1,
|
||||||
|
NULL,
|
||||||
|
'2024-03-21 23:15:26.356677+00',
|
||||||
|
FALSE,
|
||||||
|
'{"email": "z.f@gmail.com", "title": "Online", "amount": 1, "formId": null, "storeId": null, "currency": "USD", "expiryDate": null, "description": null, "embeddedCSS": null, "formResponse": null, "customCSSLink": null, "allowCustomPaymentAmounts": false}'
|
||||||
|
);
|
||||||
|
|
||||||
|
INSERT INTO "PaymentRequests" (
|
||||||
|
"Id", "StoreDataId", "Status", "Blob", "Created", "Archived", "Blob2"
|
||||||
|
) VALUES (
|
||||||
|
'expired-bug',
|
||||||
|
'7Nefq9u8DDYL56HAFskWdHNQ9ZCdfbkVWkH4xYhfzxiB',
|
||||||
|
1,
|
||||||
|
NULL,
|
||||||
|
'2024-03-21 23:15:26.356677+00',
|
||||||
|
FALSE,
|
||||||
|
'{"email": "f.f@gmail.com", "title": "Online", "amount": 0.001, "formId": null, "storeId": null, "currency": "USD", "expiryDate": 1711066440, "description": null, "embeddedCSS": null, "formResponse": null, "customCSSLink": null, "allowCustomPaymentAmounts": false}'
|
||||||
|
);
|
||||||
|
|
||||||
|
""");
|
||||||
|
await tester.ContinueMigration();
|
||||||
|
// The blob isn't cleaned yet, this is the migrator who does that
|
||||||
|
await conn.QuerySingleAsync("""
|
||||||
|
SELECT * FROM "PaymentRequests"
|
||||||
|
WHERE "Id"= '03463aab-844e-4d60-872f-26310b856131'
|
||||||
|
AND "Status" = 'Completed'
|
||||||
|
AND "Blob2"->>'currency' IS NOT NULL
|
||||||
|
""");
|
||||||
|
|
||||||
|
await conn.QuerySingleAsync("""
|
||||||
|
SELECT * FROM "PaymentRequests"
|
||||||
|
WHERE "Id"= 'other'
|
||||||
|
AND "Status" = 'Processing'
|
||||||
|
""");
|
||||||
|
|
||||||
|
await conn.QuerySingleAsync("""
|
||||||
|
SELECT * FROM "PaymentRequests"
|
||||||
|
WHERE "Id"= 'expired-bug'
|
||||||
|
AND "Status" = 'Processing'
|
||||||
|
""");
|
||||||
|
|
||||||
|
var pr = ctx.PaymentRequests.First(r => r.Id == "03463aab-844e-4d60-872f-26310b856131");
|
||||||
|
Assert.Equal("USD", pr.Currency);
|
||||||
|
Assert.Equal(0.001m, pr.Amount);
|
||||||
|
Assert.Equal(1711066440U, NBitcoin.Utils.DateTimeToUnixTime(pr.Expiry.Value));
|
||||||
|
#pragma warning disable CS0618 // Type or member is obsolete
|
||||||
|
Assert.Null(pr.Blob);
|
||||||
|
#pragma warning restore CS0618 // Type or member is obsolete
|
||||||
|
|
||||||
|
using var serverTester = CreateServerTester();
|
||||||
|
serverTester.PayTester.Postgres = serverTester.PayTester.Postgres.Replace("btcpayserver", tester.dbname);
|
||||||
|
await serverTester.StartAsync();
|
||||||
|
var dbContext = serverTester.PayTester.GetService<ApplicationDbContextFactory>().CreateContext();
|
||||||
|
var migrator = serverTester.PayTester.GetService<PaymentRequestsMigratorHostedService>();
|
||||||
|
await TestUtils.EventuallyAsync(async () =>
|
||||||
|
{
|
||||||
|
Assert.True(await migrator.IsComplete());
|
||||||
|
});
|
||||||
|
var actualBlob2 = (string)(await conn.QuerySingleAsync("""
|
||||||
|
SELECT * FROM "PaymentRequests"
|
||||||
|
WHERE "Id"= '03463aab-844e-4d60-872f-26310b856131'
|
||||||
|
AND "Expiry" IS NOT NULL AND "Currency" IS NOT NULL AND "Amount"=0.001
|
||||||
|
""")).Blob2;
|
||||||
|
var expectedBlob2 = new JObject()
|
||||||
|
{
|
||||||
|
["email"] = "f.f@gmail.com",
|
||||||
|
["title"] = "Online"
|
||||||
|
};
|
||||||
|
Assert.Equal(JObject.Parse(actualBlob2), expectedBlob2);
|
||||||
|
|
||||||
|
actualBlob2 = (string)(await conn.QuerySingleAsync("""
|
||||||
|
SELECT * FROM "PaymentRequests"
|
||||||
|
WHERE "Id"= 'other'
|
||||||
|
AND "Expiry" IS NULL AND "Amount"=1
|
||||||
|
""")).Blob2;
|
||||||
|
expectedBlob2 = new JObject()
|
||||||
|
{
|
||||||
|
["email"] = "f.f@gmail.com",
|
||||||
|
["title"] = "Online"
|
||||||
|
};
|
||||||
|
Assert.Equal(JObject.Parse(actualBlob2), expectedBlob2);
|
||||||
|
|
||||||
|
// 'expired-bug' represents a PaymentRequest whose status isn't Expired even if its expiry is well past.
|
||||||
|
// Normally, on startup, the streamer should set the status to Expired
|
||||||
|
await TestUtils.EventuallyAsync(async () =>
|
||||||
|
{
|
||||||
|
var r = await conn.QuerySingleOrDefaultAsync("""
|
||||||
|
SELECT * FROM "PaymentRequests"
|
||||||
|
WHERE "Id"= 'expired-bug'
|
||||||
|
AND "Status" = 'Expired'
|
||||||
|
""");
|
||||||
|
Assert.NotNull(r);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
[Fact(Timeout = LongRunningTestTimeout)]
|
[Fact(Timeout = LongRunningTestTimeout)]
|
||||||
[Trait("Integration", "Integration")]
|
[Trait("Integration", "Integration")]
|
||||||
public async Task CanDoInvoiceMigrations()
|
public async Task CanDoInvoiceMigrations()
|
||||||
@@ -2889,6 +3010,7 @@ namespace BTCPayServer.Tests
|
|||||||
var handlers = tester.PayTester.GetService<PaymentMethodHandlerDictionary>();
|
var handlers = tester.PayTester.GetService<PaymentMethodHandlerDictionary>();
|
||||||
|
|
||||||
await acc.ImportOldInvoices();
|
await acc.ImportOldInvoices();
|
||||||
|
|
||||||
var dbContext = tester.PayTester.GetService<ApplicationDbContextFactory>().CreateContext();
|
var dbContext = tester.PayTester.GetService<ApplicationDbContextFactory>().CreateContext();
|
||||||
var invoiceMigrator = tester.PayTester.GetService<InvoiceBlobMigratorHostedService>();
|
var invoiceMigrator = tester.PayTester.GetService<InvoiceBlobMigratorHostedService>();
|
||||||
invoiceMigrator.BatchSize = 2;
|
invoiceMigrator.BatchSize = 2;
|
||||||
@@ -3164,9 +3286,11 @@ namespace BTCPayServer.Tests
|
|||||||
// Quick unrelated test on GetMonitoredInvoices
|
// Quick unrelated test on GetMonitoredInvoices
|
||||||
var invoiceRepo = tester.PayTester.GetService<InvoiceRepository>();
|
var invoiceRepo = tester.PayTester.GetService<InvoiceRepository>();
|
||||||
var monitored = Assert.Single(await invoiceRepo.GetMonitoredInvoices(PaymentMethodId.Parse("BTC-CHAIN")), i => i.Id == invoiceId);
|
var monitored = Assert.Single(await invoiceRepo.GetMonitoredInvoices(PaymentMethodId.Parse("BTC-CHAIN")), i => i.Id == invoiceId);
|
||||||
|
#pragma warning disable CS0618 // Type or member is obsolete
|
||||||
Assert.Single(monitored.Payments);
|
Assert.Single(monitored.Payments);
|
||||||
monitored = Assert.Single(await invoiceRepo.GetMonitoredInvoices(PaymentMethodId.Parse("BTC-CHAIN"), true), i => i.Id == invoiceId);
|
monitored = Assert.Single(await invoiceRepo.GetMonitoredInvoices(PaymentMethodId.Parse("BTC-CHAIN"), true), i => i.Id == invoiceId);
|
||||||
Assert.Single(monitored.Payments);
|
Assert.Single(monitored.Payments);
|
||||||
|
#pragma warning restore CS0618 // Type or member is obsolete
|
||||||
//
|
//
|
||||||
|
|
||||||
app = await client.CreatePointOfSaleApp(acc.StoreId, new PointOfSaleAppRequest
|
app = await client.CreatePointOfSaleApp(acc.StoreId, new PointOfSaleAppRequest
|
||||||
|
|||||||
@@ -17,6 +17,9 @@ using Microsoft.AspNetCore.Cors;
|
|||||||
using Microsoft.AspNetCore.Identity;
|
using Microsoft.AspNetCore.Identity;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Microsoft.AspNetCore.Routing;
|
using Microsoft.AspNetCore.Routing;
|
||||||
|
using TwentyTwenty.Storage;
|
||||||
|
using static System.Runtime.InteropServices.JavaScript.JSType;
|
||||||
|
using static QRCoder.PayloadGenerator;
|
||||||
using PaymentRequestData = BTCPayServer.Data.PaymentRequestData;
|
using PaymentRequestData = BTCPayServer.Data.PaymentRequestData;
|
||||||
|
|
||||||
namespace BTCPayServer.Controllers.Greenfield
|
namespace BTCPayServer.Controllers.Greenfield
|
||||||
@@ -53,7 +56,7 @@ namespace BTCPayServer.Controllers.Greenfield
|
|||||||
|
|
||||||
[Authorize(Policy = Policies.CanViewPaymentRequests, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
[Authorize(Policy = Policies.CanViewPaymentRequests, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
||||||
[HttpGet("~/api/v1/stores/{storeId}/payment-requests")]
|
[HttpGet("~/api/v1/stores/{storeId}/payment-requests")]
|
||||||
public async Task<ActionResult<IEnumerable<Client.Models.PaymentRequestData>>> GetPaymentRequests(string storeId, bool includeArchived = false)
|
public async Task<ActionResult<IEnumerable<Client.Models.PaymentRequestBaseData>>> GetPaymentRequests(string storeId, bool includeArchived = false)
|
||||||
{
|
{
|
||||||
var prs = await _paymentRequestRepository.FindPaymentRequests(
|
var prs = await _paymentRequestRepository.FindPaymentRequests(
|
||||||
new PaymentRequestQuery() { StoreId = storeId, IncludeArchived = includeArchived });
|
new PaymentRequestQuery() { StoreId = storeId, IncludeArchived = includeArchived });
|
||||||
@@ -149,26 +152,80 @@ namespace BTCPayServer.Controllers.Greenfield
|
|||||||
}
|
}
|
||||||
|
|
||||||
[HttpPost("~/api/v1/stores/{storeId}/payment-requests")]
|
[HttpPost("~/api/v1/stores/{storeId}/payment-requests")]
|
||||||
|
[HttpPut("~/api/v1/stores/{storeId}/payment-requests/{paymentRequestId}")]
|
||||||
[Authorize(Policy = Policies.CanModifyPaymentRequests,
|
[Authorize(Policy = Policies.CanModifyPaymentRequests,
|
||||||
AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
||||||
public async Task<IActionResult> CreatePaymentRequest(string storeId,
|
public async Task<IActionResult> CreateOrUpdatePaymentRequest(
|
||||||
CreatePaymentRequestRequest request)
|
[FromRoute] string storeId,
|
||||||
|
PaymentRequestBaseData request,
|
||||||
|
[FromRoute] string paymentRequestId = null)
|
||||||
{
|
{
|
||||||
var validationResult = await Validate(null, request);
|
if (request is null)
|
||||||
if (validationResult != null)
|
return BadRequest();
|
||||||
|
|
||||||
|
if (request.Amount <= 0)
|
||||||
{
|
{
|
||||||
return validationResult;
|
ModelState.AddModelError(nameof(request.Amount), "Please provide an amount greater than 0");
|
||||||
}
|
}
|
||||||
request.Currency ??= StoreData.GetStoreBlob().DefaultCurrency;
|
if (!string.IsNullOrEmpty(request.Currency) &&
|
||||||
var pr = new PaymentRequestData()
|
_currencyNameTable.GetCurrencyData(request.Currency, false) == null)
|
||||||
|
ModelState.AddModelError(nameof(request.Currency), "Invalid currency");
|
||||||
|
if (string.IsNullOrEmpty(request.Currency))
|
||||||
|
request.Currency = null;
|
||||||
|
if (string.IsNullOrEmpty(request.Title))
|
||||||
|
ModelState.AddModelError(nameof(request.Title), "Title is required");
|
||||||
|
|
||||||
|
PaymentRequestData pr;
|
||||||
|
if (paymentRequestId is not null)
|
||||||
|
{
|
||||||
|
pr = (await _paymentRequestRepository.FindPaymentRequests(
|
||||||
|
new PaymentRequestQuery() { StoreId = storeId, Ids = new[] { paymentRequestId } })).FirstOrDefault();
|
||||||
|
if (pr is null)
|
||||||
|
return PaymentRequestNotFound();
|
||||||
|
if ((pr.Amount != request.Amount && request.Amount != 0.0m) ||
|
||||||
|
(pr.Currency != request.Currency && request.Currency != null))
|
||||||
|
{
|
||||||
|
var prWithInvoices = await this.PaymentRequestService.GetPaymentRequest(paymentRequestId, GetUserId());
|
||||||
|
if (prWithInvoices.Invoices.Any())
|
||||||
|
{
|
||||||
|
ModelState.AddModelError(nameof(request.Amount), "Amount and currency are not editable once payment request has invoices");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (request.Amount != 0.0m)
|
||||||
|
pr.Amount = request.Amount;
|
||||||
|
if (request.Currency != null)
|
||||||
|
pr.Currency = request.Currency;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pr.Expiry = request.ExpiryDate;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
pr = new PaymentRequestData()
|
||||||
{
|
{
|
||||||
StoreDataId = storeId,
|
StoreDataId = storeId,
|
||||||
Status = Client.Models.PaymentRequestData.PaymentRequestStatus.Pending,
|
Status = Client.Models.PaymentRequestStatus.Pending,
|
||||||
Created = DateTimeOffset.UtcNow
|
Created = DateTimeOffset.UtcNow,
|
||||||
|
Amount = request.Amount,
|
||||||
|
Currency = request.Currency ?? StoreData.GetStoreBlob().DefaultCurrency,
|
||||||
|
Expiry = request.ExpiryDate,
|
||||||
};
|
};
|
||||||
request.FormResponse = null;
|
}
|
||||||
request.StoreId = storeId;
|
|
||||||
pr.SetBlob(request);
|
if (!ModelState.IsValid)
|
||||||
|
return this.CreateValidationError(ModelState);
|
||||||
|
|
||||||
|
var blob = pr.GetBlob();
|
||||||
|
pr.SetBlob(new()
|
||||||
|
{
|
||||||
|
AllowCustomPaymentAmounts = request.AllowCustomPaymentAmounts,
|
||||||
|
Description = request.Description,
|
||||||
|
Email = request.Email,
|
||||||
|
FormId = request.FormId,
|
||||||
|
Title = request.Title,
|
||||||
|
FormResponse = blob.FormId != request.FormId ? null : blob.FormResponse
|
||||||
|
});
|
||||||
pr = await _paymentRequestRepository.CreateOrUpdatePaymentRequest(pr);
|
pr = await _paymentRequestRepository.CreateOrUpdatePaymentRequest(pr);
|
||||||
return Ok(FromModel(pr));
|
return Ok(FromModel(pr));
|
||||||
}
|
}
|
||||||
@@ -176,80 +233,23 @@ namespace BTCPayServer.Controllers.Greenfield
|
|||||||
|
|
||||||
public PaymentRequestService PaymentRequestService { get; }
|
public PaymentRequestService PaymentRequestService { get; }
|
||||||
|
|
||||||
[HttpPut("~/api/v1/stores/{storeId}/payment-requests/{paymentRequestId}")]
|
|
||||||
[Authorize(Policy = Policies.CanModifyPaymentRequests,
|
|
||||||
AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
|
||||||
public async Task<IActionResult> UpdatePaymentRequest(string storeId,
|
|
||||||
string paymentRequestId, [FromBody] UpdatePaymentRequestRequest request)
|
|
||||||
{
|
|
||||||
var validationResult = await Validate(paymentRequestId, request);
|
|
||||||
if (validationResult != null)
|
|
||||||
{
|
|
||||||
return validationResult;
|
|
||||||
}
|
|
||||||
request.Currency ??= StoreData.GetStoreBlob().DefaultCurrency;
|
|
||||||
var pr = await _paymentRequestRepository.FindPaymentRequests(
|
|
||||||
new PaymentRequestQuery() { StoreId = storeId, Ids = new[] { paymentRequestId } });
|
|
||||||
if (pr.Length == 0)
|
|
||||||
{
|
|
||||||
return PaymentRequestNotFound();
|
|
||||||
}
|
|
||||||
|
|
||||||
var updatedPr = pr.First();
|
|
||||||
var blob = updatedPr.GetBlob();
|
|
||||||
request.FormResponse = blob.FormResponse;
|
|
||||||
request.StoreId = storeId;
|
|
||||||
updatedPr.SetBlob(request);
|
|
||||||
|
|
||||||
return Ok(FromModel(await _paymentRequestRepository.CreateOrUpdatePaymentRequest(updatedPr)));
|
|
||||||
}
|
|
||||||
private string GetUserId() => _userManager.GetUserId(User);
|
private string GetUserId() => _userManager.GetUserId(User);
|
||||||
|
|
||||||
private async Task<IActionResult> Validate(string id, PaymentRequestBaseData data)
|
private static Client.Models.PaymentRequestBaseData FromModel(PaymentRequestData data)
|
||||||
{
|
|
||||||
if (data is null)
|
|
||||||
return BadRequest();
|
|
||||||
|
|
||||||
if (id != null)
|
|
||||||
{
|
|
||||||
var pr = await this.PaymentRequestService.GetPaymentRequest(id, GetUserId());
|
|
||||||
if (pr.Amount != data.Amount)
|
|
||||||
{
|
|
||||||
if (pr.Invoices.Any())
|
|
||||||
ModelState.AddModelError(nameof(data.Amount), "Amount and currency are not editable once payment request has invoices");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (data.Amount <= 0)
|
|
||||||
{
|
|
||||||
ModelState.AddModelError(nameof(data.Amount), "Please provide an amount greater than 0");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(data.Currency) &&
|
|
||||||
_currencyNameTable.GetCurrencyData(data.Currency, false) == null)
|
|
||||||
ModelState.AddModelError(nameof(data.Currency), "Invalid currency");
|
|
||||||
if (string.IsNullOrEmpty(data.Currency))
|
|
||||||
data.Currency = null;
|
|
||||||
if (string.IsNullOrEmpty(data.Title))
|
|
||||||
ModelState.AddModelError(nameof(data.Title), "Title is required");
|
|
||||||
|
|
||||||
return !ModelState.IsValid ? this.CreateValidationError(ModelState) : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Client.Models.PaymentRequestData FromModel(PaymentRequestData data)
|
|
||||||
{
|
{
|
||||||
var blob = data.GetBlob();
|
var blob = data.GetBlob();
|
||||||
return new Client.Models.PaymentRequestData()
|
return new Client.Models.PaymentRequestBaseData()
|
||||||
{
|
{
|
||||||
CreatedTime = data.Created,
|
CreatedTime = data.Created,
|
||||||
Id = data.Id,
|
Id = data.Id,
|
||||||
StoreId = data.StoreDataId,
|
StoreId = data.StoreDataId,
|
||||||
Status = data.Status,
|
Status = data.Status,
|
||||||
Archived = data.Archived,
|
Archived = data.Archived,
|
||||||
Amount = blob.Amount,
|
Amount = data.Amount,
|
||||||
Currency = blob.Currency,
|
Currency = data.Currency,
|
||||||
Description = blob.Description,
|
Description = blob.Description,
|
||||||
Title = blob.Title,
|
Title = blob.Title,
|
||||||
ExpiryDate = blob.ExpiryDate,
|
ExpiryDate = data.Expiry,
|
||||||
Email = blob.Email,
|
Email = blob.Email,
|
||||||
AllowCustomPaymentAmounts = blob.AllowCustomPaymentAmounts,
|
AllowCustomPaymentAmounts = blob.AllowCustomPaymentAmounts,
|
||||||
FormResponse = blob.FormResponse,
|
FormResponse = blob.FormResponse,
|
||||||
|
|||||||
@@ -28,7 +28,6 @@ using InvoiceData = BTCPayServer.Client.Models.InvoiceData;
|
|||||||
using Language = BTCPayServer.Client.Models.Language;
|
using Language = BTCPayServer.Client.Models.Language;
|
||||||
using LightningAddressData = BTCPayServer.Client.Models.LightningAddressData;
|
using LightningAddressData = BTCPayServer.Client.Models.LightningAddressData;
|
||||||
using NotificationData = BTCPayServer.Client.Models.NotificationData;
|
using NotificationData = BTCPayServer.Client.Models.NotificationData;
|
||||||
using PaymentRequestData = BTCPayServer.Client.Models.PaymentRequestData;
|
|
||||||
using PayoutData = BTCPayServer.Client.Models.PayoutData;
|
using PayoutData = BTCPayServer.Client.Models.PayoutData;
|
||||||
using PayoutProcessorData = BTCPayServer.Client.Models.PayoutProcessorData;
|
using PayoutProcessorData = BTCPayServer.Client.Models.PayoutProcessorData;
|
||||||
using PullPaymentData = BTCPayServer.Client.Models.PullPaymentData;
|
using PullPaymentData = BTCPayServer.Client.Models.PullPaymentData;
|
||||||
@@ -596,16 +595,16 @@ namespace BTCPayServer.Controllers.Greenfield
|
|||||||
return Task.FromResult(GetFromActionResult<ApiHealthData>(GetController<GreenfieldHealthController>().GetHealth()));
|
return Task.FromResult(GetFromActionResult<ApiHealthData>(GetController<GreenfieldHealthController>().GetHealth()));
|
||||||
}
|
}
|
||||||
|
|
||||||
public override async Task<IEnumerable<PaymentRequestData>> GetPaymentRequests(string storeId,
|
public override async Task<IEnumerable<PaymentRequestBaseData>> GetPaymentRequests(string storeId,
|
||||||
bool includeArchived = false, CancellationToken token = default)
|
bool includeArchived = false, CancellationToken token = default)
|
||||||
{
|
{
|
||||||
return GetFromActionResult(await GetController<GreenfieldPaymentRequestsController>().GetPaymentRequests(storeId, includeArchived));
|
return GetFromActionResult(await GetController<GreenfieldPaymentRequestsController>().GetPaymentRequests(storeId, includeArchived));
|
||||||
}
|
}
|
||||||
|
|
||||||
public override async Task<PaymentRequestData> GetPaymentRequest(string storeId, string paymentRequestId,
|
public override async Task<PaymentRequestBaseData> GetPaymentRequest(string storeId, string paymentRequestId,
|
||||||
CancellationToken token = default)
|
CancellationToken token = default)
|
||||||
{
|
{
|
||||||
return GetFromActionResult<PaymentRequestData>(
|
return GetFromActionResult<PaymentRequestBaseData>(
|
||||||
await GetController<GreenfieldPaymentRequestsController>().GetPaymentRequest(storeId, paymentRequestId));
|
await GetController<GreenfieldPaymentRequestsController>().GetPaymentRequest(storeId, paymentRequestId));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -621,19 +620,19 @@ namespace BTCPayServer.Controllers.Greenfield
|
|||||||
await GetController<GreenfieldPaymentRequestsController>().PayPaymentRequest(storeId, paymentRequestId, request, token));
|
await GetController<GreenfieldPaymentRequestsController>().PayPaymentRequest(storeId, paymentRequestId, request, token));
|
||||||
}
|
}
|
||||||
|
|
||||||
public override async Task<PaymentRequestData> CreatePaymentRequest(string storeId,
|
public override async Task<PaymentRequestBaseData> CreatePaymentRequest(string storeId,
|
||||||
CreatePaymentRequestRequest request, CancellationToken token = default)
|
PaymentRequestBaseData request, CancellationToken token = default)
|
||||||
{
|
{
|
||||||
return GetFromActionResult<PaymentRequestData>(
|
return GetFromActionResult<PaymentRequestBaseData>(
|
||||||
await GetController<GreenfieldPaymentRequestsController>().CreatePaymentRequest(storeId, request));
|
await GetController<GreenfieldPaymentRequestsController>().CreateOrUpdatePaymentRequest(storeId, request));
|
||||||
}
|
}
|
||||||
|
|
||||||
public override async Task<PaymentRequestData> UpdatePaymentRequest(string storeId, string paymentRequestId,
|
public override async Task<PaymentRequestBaseData> UpdatePaymentRequest(string storeId, string paymentRequestId,
|
||||||
UpdatePaymentRequestRequest request,
|
PaymentRequestBaseData request,
|
||||||
CancellationToken token = default)
|
CancellationToken token = default)
|
||||||
{
|
{
|
||||||
return GetFromActionResult<PaymentRequestData>(
|
return GetFromActionResult<PaymentRequestBaseData>(
|
||||||
await GetController<GreenfieldPaymentRequestsController>().UpdatePaymentRequest(storeId, paymentRequestId, request));
|
await GetController<GreenfieldPaymentRequestsController>().CreateOrUpdatePaymentRequest(storeId, request, paymentRequestId));
|
||||||
}
|
}
|
||||||
|
|
||||||
public override async Task<ApiKeyData> GetCurrentAPIKeyInfo(CancellationToken token = default)
|
public override async Task<ApiKeyData> GetCurrentAPIKeyInfo(CancellationToken token = default)
|
||||||
|
|||||||
@@ -158,7 +158,7 @@ namespace BTCPayServer.Controllers
|
|||||||
new CreateInvoiceRequest
|
new CreateInvoiceRequest
|
||||||
{
|
{
|
||||||
Metadata = invoiceMetadata,
|
Metadata = invoiceMetadata,
|
||||||
Currency = prBlob.Currency,
|
Currency = prData.Currency,
|
||||||
Amount = amount,
|
Amount = amount,
|
||||||
Checkout = { RedirectURL = redirectUrl },
|
Checkout = { RedirectURL = redirectUrl },
|
||||||
Receipt = new InvoiceDataBase.ReceiptOptions { Enabled = false }
|
Receipt = new InvoiceDataBase.ReceiptOptions { Enabled = false }
|
||||||
|
|||||||
@@ -98,7 +98,7 @@ namespace BTCPayServer.Controllers
|
|||||||
StoreId = store.Id,
|
StoreId = store.Id,
|
||||||
Skip = model.Skip,
|
Skip = model.Skip,
|
||||||
Count = model.Count,
|
Count = model.Count,
|
||||||
Status = fs.GetFilterArray("status")?.Select(s => Enum.Parse<Client.Models.PaymentRequestData.PaymentRequestStatus>(s, true)).ToArray(),
|
Status = fs.GetFilterArray("status")?.Select(s => Enum.Parse<Client.Models.PaymentRequestStatus>(s, true)).ToArray(),
|
||||||
IncludeArchived = fs.GetFilterBool("includearchived") ?? false
|
IncludeArchived = fs.GetFilterBool("includearchived") ?? false
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -110,7 +110,7 @@ namespace BTCPayServer.Controllers
|
|||||||
var blob = data.GetBlob();
|
var blob = data.GetBlob();
|
||||||
return new ViewPaymentRequestViewModel(data)
|
return new ViewPaymentRequestViewModel(data)
|
||||||
{
|
{
|
||||||
AmountFormatted = _displayFormatter.Currency(blob.Amount, blob.Currency)
|
AmountFormatted = _displayFormatter.Currency(data.Amount, data.Currency)
|
||||||
};
|
};
|
||||||
}).ToList();
|
}).ToList();
|
||||||
|
|
||||||
@@ -180,7 +180,7 @@ namespace BTCPayServer.Controllers
|
|||||||
data.Archived = viewModel.Archived;
|
data.Archived = viewModel.Archived;
|
||||||
var blob = data.GetBlob();
|
var blob = data.GetBlob();
|
||||||
|
|
||||||
if (blob.Amount != viewModel.Amount && payReqId != null)
|
if (data.Amount != viewModel.Amount && payReqId != null)
|
||||||
{
|
{
|
||||||
var prInvoices = (await _PaymentRequestService.GetPaymentRequest(payReqId, GetUserId())).Invoices;
|
var prInvoices = (await _PaymentRequestService.GetPaymentRequest(payReqId, GetUserId())).Invoices;
|
||||||
if (prInvoices.Any())
|
if (prInvoices.Any())
|
||||||
@@ -198,9 +198,9 @@ namespace BTCPayServer.Controllers
|
|||||||
blob.Title = viewModel.Title;
|
blob.Title = viewModel.Title;
|
||||||
blob.Email = viewModel.Email;
|
blob.Email = viewModel.Email;
|
||||||
blob.Description = viewModel.Description;
|
blob.Description = viewModel.Description;
|
||||||
blob.Amount = viewModel.Amount;
|
data.Amount = viewModel.Amount;
|
||||||
blob.ExpiryDate = viewModel.ExpiryDate?.ToUniversalTime();
|
data.Expiry = viewModel.ExpiryDate?.ToUniversalTime();
|
||||||
blob.Currency = viewModel.Currency ?? store.GetStoreBlob().DefaultCurrency;
|
data.Currency = viewModel.Currency ?? store.GetStoreBlob().DefaultCurrency;
|
||||||
blob.AllowCustomPaymentAmounts = viewModel.AllowCustomPaymentAmounts;
|
blob.AllowCustomPaymentAmounts = viewModel.AllowCustomPaymentAmounts;
|
||||||
blob.FormId = viewModel.FormId;
|
blob.FormId = viewModel.FormId;
|
||||||
|
|
||||||
@@ -212,7 +212,6 @@ namespace BTCPayServer.Controllers
|
|||||||
}
|
}
|
||||||
|
|
||||||
data = await _PaymentRequestRepository.CreateOrUpdatePaymentRequest(data);
|
data = await _PaymentRequestRepository.CreateOrUpdatePaymentRequest(data);
|
||||||
_EventAggregator.Publish(new PaymentRequestUpdated { Data = data, PaymentRequestId = data.Id, });
|
|
||||||
|
|
||||||
TempData[WellKnownTempData.SuccessMessage] = isNewPaymentRequest
|
TempData[WellKnownTempData.SuccessMessage] = isNewPaymentRequest
|
||||||
? StringLocalizer["Payment request \"{0}\" created successfully", viewModel.Title].Value
|
? StringLocalizer["Payment request \"{0}\" created successfully", viewModel.Title].Value
|
||||||
|
|||||||
19
BTCPayServer/Data/PaymentRequestBlob.cs
Normal file
19
BTCPayServer/Data/PaymentRequestBlob.cs
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
using BTCPayServer.JsonConverters;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
using Newtonsoft.Json.Linq;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
namespace BTCPayServer.Data
|
||||||
|
{
|
||||||
|
public class PaymentRequestBlob
|
||||||
|
{
|
||||||
|
public string Title { get; set; }
|
||||||
|
public string Description { get; set; }
|
||||||
|
public string Email { get; set; }
|
||||||
|
public bool AllowCustomPaymentAmounts { get; set; }
|
||||||
|
|
||||||
|
public string FormId { get; set; }
|
||||||
|
|
||||||
|
public JObject FormResponse { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,20 +1,27 @@
|
|||||||
using System;
|
using System;
|
||||||
using BTCPayServer.Client.Models;
|
using BTCPayServer.Client.Models;
|
||||||
using NBXplorer;
|
using NBXplorer;
|
||||||
|
using Newtonsoft.Json;
|
||||||
using Newtonsoft.Json.Linq;
|
using Newtonsoft.Json.Linq;
|
||||||
|
|
||||||
namespace BTCPayServer.Data
|
namespace BTCPayServer.Data
|
||||||
{
|
{
|
||||||
public static class PaymentRequestDataExtensions
|
public static class PaymentRequestDataExtensions
|
||||||
{
|
{
|
||||||
public static PaymentRequestBaseData GetBlob(this PaymentRequestData paymentRequestData)
|
public static readonly JsonSerializerSettings DefaultSerializerSettings;
|
||||||
|
public static readonly JsonSerializer DefaultSerializer;
|
||||||
|
static PaymentRequestDataExtensions()
|
||||||
{
|
{
|
||||||
return paymentRequestData.HasTypedBlob<PaymentRequestBaseData>().GetBlob() ?? new PaymentRequestBaseData();
|
(DefaultSerializerSettings, DefaultSerializer) = BlobSerializer.CreateSerializer(null as NBitcoin.Network);
|
||||||
|
}
|
||||||
|
public static PaymentRequestBlob GetBlob(this PaymentRequestData paymentRequestData)
|
||||||
|
{
|
||||||
|
return paymentRequestData.HasTypedBlob<PaymentRequestBlob>().GetBlob(DefaultSerializerSettings) ?? new PaymentRequestBlob();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void SetBlob(this PaymentRequestData paymentRequestData, PaymentRequestBaseData blob)
|
public static void SetBlob(this PaymentRequestData paymentRequestData, PaymentRequestBlob blob)
|
||||||
{
|
{
|
||||||
paymentRequestData.HasTypedBlob<PaymentRequestBaseData>().SetBlob(blob);
|
paymentRequestData.HasTypedBlob<PaymentRequestBlob>().SetBlob(blob, DefaultSerializer);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ using System.Threading.Tasks;
|
|||||||
using BTCPayServer.Abstractions.Contracts;
|
using BTCPayServer.Abstractions.Contracts;
|
||||||
using BTCPayServer.Data;
|
using BTCPayServer.Data;
|
||||||
using BTCPayServer.Migrations;
|
using BTCPayServer.Migrations;
|
||||||
|
using BTCPayServer.PaymentRequest;
|
||||||
using BTCPayServer.Services.Invoices;
|
using BTCPayServer.Services.Invoices;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
@@ -15,20 +16,24 @@ namespace BTCPayServer.HostedServices
|
|||||||
{
|
{
|
||||||
public class PaymentRequestsMigratorHostedService : BlobMigratorHostedService<PaymentRequestData>
|
public class PaymentRequestsMigratorHostedService : BlobMigratorHostedService<PaymentRequestData>
|
||||||
{
|
{
|
||||||
|
private readonly PaymentRequestStreamer _paymentRequestStreamer;
|
||||||
|
|
||||||
public PaymentRequestsMigratorHostedService(
|
public PaymentRequestsMigratorHostedService(
|
||||||
ILogger<PaymentRequestsMigratorHostedService> logs,
|
ILogger<PaymentRequestsMigratorHostedService> logs,
|
||||||
ISettingsRepository settingsRepository,
|
ISettingsRepository settingsRepository,
|
||||||
|
PaymentRequestStreamer paymentRequestStreamer,
|
||||||
ApplicationDbContextFactory applicationDbContextFactory) : base(logs, settingsRepository, applicationDbContextFactory)
|
ApplicationDbContextFactory applicationDbContextFactory) : base(logs, settingsRepository, applicationDbContextFactory)
|
||||||
{
|
{
|
||||||
|
_paymentRequestStreamer = paymentRequestStreamer;
|
||||||
}
|
}
|
||||||
public override string SettingsKey => "PaymentRequestsMigration";
|
public override string SettingsKey => "PaymentRequestsMigration2";
|
||||||
|
|
||||||
protected override IQueryable<PaymentRequestData> GetQuery(ApplicationDbContext ctx, DateTimeOffset? progress)
|
protected override IQueryable<PaymentRequestData> GetQuery(ApplicationDbContext ctx, DateTimeOffset? progress)
|
||||||
{
|
{
|
||||||
#pragma warning disable CS0618 // Type or member is obsolete
|
#pragma warning disable CS0618 // Type or member is obsolete
|
||||||
var query = progress is DateTimeOffset last2 ?
|
var query = progress is DateTimeOffset last2 ?
|
||||||
ctx.PaymentRequests.Where(i => i.Created < last2 && !(i.Blob == null && i.Blob2 != null)) :
|
ctx.PaymentRequests.Where(i => i.Created < last2 && !((i.Blob == null || i.Blob.Length == 0) && i.Blob2 != null && i.Currency != null)) :
|
||||||
ctx.PaymentRequests.Where(i => !(i.Blob == null && i.Blob2 != null));
|
ctx.PaymentRequests.Where(i => !((i.Blob == null || i.Blob.Length == 0) && i.Blob2 != null && i.Currency != null));
|
||||||
return query.OrderByDescending(i => i.Created);
|
return query.OrderByDescending(i => i.Created);
|
||||||
#pragma warning restore CS0618 // Type or member is obsolete
|
#pragma warning restore CS0618 // Type or member is obsolete
|
||||||
}
|
}
|
||||||
@@ -38,6 +43,7 @@ namespace BTCPayServer.HostedServices
|
|||||||
Logs.LogInformation("Post-migration VACUUM (FULL, ANALYZE)");
|
Logs.LogInformation("Post-migration VACUUM (FULL, ANALYZE)");
|
||||||
await ctx.Database.ExecuteSqlRawAsync("VACUUM (FULL, ANALYZE) \"PaymentRequests\"", cancellationToken);
|
await ctx.Database.ExecuteSqlRawAsync("VACUUM (FULL, ANALYZE) \"PaymentRequests\"", cancellationToken);
|
||||||
Logs.LogInformation("Post-migration VACUUM (FULL, ANALYZE) finished");
|
Logs.LogInformation("Post-migration VACUUM (FULL, ANALYZE) finished");
|
||||||
|
_paymentRequestStreamer.CheckExpirable();
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override DateTimeOffset ProcessEntities(ApplicationDbContext ctx, List<PaymentRequestData> entities)
|
protected override DateTimeOffset ProcessEntities(ApplicationDbContext ctx, List<PaymentRequestData> entities)
|
||||||
@@ -46,6 +52,8 @@ namespace BTCPayServer.HostedServices
|
|||||||
// But Modified isn't set as it happens before the ctx is bound to the entity.
|
// But Modified isn't set as it happens before the ctx is bound to the entity.
|
||||||
foreach (var entity in entities)
|
foreach (var entity in entities)
|
||||||
{
|
{
|
||||||
|
// Make sure the blob is clean
|
||||||
|
entity.SetBlob(entity.GetBlob());
|
||||||
ctx.PaymentRequests.Entry(entity).State = EntityState.Modified;
|
ctx.PaymentRequests.Entry(entity).State = EntityState.Modified;
|
||||||
}
|
}
|
||||||
return entities[^1].Created;
|
return entities[^1].Created;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
#nullable enable
|
#nullable enable
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using BTCPayServer.Client.Models;
|
using BTCPayServer.Client.Models;
|
||||||
@@ -30,19 +30,20 @@ public class PaymentRequestWebhookDeliveryRequest : WebhookSender.WebhookDeliver
|
|||||||
req.Email += $",{bmb}";
|
req.Email += $",{bmb}";
|
||||||
}
|
}
|
||||||
|
|
||||||
req.Subject = Interpolate(req.Subject, blob);
|
req.Subject = Interpolate(req.Subject, _evt.Data);
|
||||||
req.Body = Interpolate(req.Body, blob);
|
req.Body = Interpolate(req.Body, _evt.Data);
|
||||||
return Task.FromResult(req)!;
|
return Task.FromResult(req)!;
|
||||||
}
|
}
|
||||||
|
|
||||||
private string Interpolate(string str, PaymentRequestBaseData blob)
|
private string Interpolate(string str, Data.PaymentRequestData data)
|
||||||
{
|
{
|
||||||
|
var blob = data.GetBlob();
|
||||||
var res = str.Replace("{PaymentRequest.Id}", _evt.Data.Id)
|
var res = str.Replace("{PaymentRequest.Id}", _evt.Data.Id)
|
||||||
.Replace("{PaymentRequest.Price}", blob.Amount.ToString(CultureInfo.InvariantCulture))
|
.Replace("{PaymentRequest.Price}", data.Amount.ToString(CultureInfo.InvariantCulture))
|
||||||
.Replace("{PaymentRequest.Currency}", blob.Currency)
|
.Replace("{PaymentRequest.Currency}", data.Currency)
|
||||||
.Replace("{PaymentRequest.Title}", blob.Title)
|
.Replace("{PaymentRequest.Title}", blob.Title)
|
||||||
.Replace("{PaymentRequest.Description}", blob.Description)
|
.Replace("{PaymentRequest.Description}", blob.Description)
|
||||||
.Replace("{PaymentRequest.Status}", _evt.Data.Status.ToString());
|
.Replace("{PaymentRequest.Status}", data.Status.ToString());
|
||||||
|
|
||||||
res = InterpolateJsonField(res, "PaymentRequest.FormResponse", blob.FormResponse);
|
res = InterpolateJsonField(res, "PaymentRequest.FormResponse", blob.FormResponse);
|
||||||
return res;
|
return res;
|
||||||
|
|||||||
@@ -92,6 +92,7 @@ namespace BTCPayServer.Hosting
|
|||||||
services.TryAddSingleton<LocalizerService>();
|
services.TryAddSingleton<LocalizerService>();
|
||||||
services.TryAddSingleton<ViewLocalizer>();
|
services.TryAddSingleton<ViewLocalizer>();
|
||||||
services.TryAddSingleton<IStringLocalizer>(o => o.GetRequiredService<IStringLocalizerFactory>().Create("",""));
|
services.TryAddSingleton<IStringLocalizer>(o => o.GetRequiredService<IStringLocalizerFactory>().Create("",""));
|
||||||
|
services.TryAddSingleton<DelayedTaskScheduler>();
|
||||||
|
|
||||||
services.AddSingleton<MvcNewtonsoftJsonOptions>(o => o.GetRequiredService<IOptions<MvcNewtonsoftJsonOptions>>().Value);
|
services.AddSingleton<MvcNewtonsoftJsonOptions>(o => o.GetRequiredService<IOptions<MvcNewtonsoftJsonOptions>>().Value);
|
||||||
services.AddSingleton<JsonSerializerSettings>(o => o.GetRequiredService<IOptions<MvcNewtonsoftJsonOptions>>().Value.SerializerSettings);
|
services.AddSingleton<JsonSerializerSettings>(o => o.GetRequiredService<IOptions<MvcNewtonsoftJsonOptions>>().Value.SerializerSettings);
|
||||||
@@ -431,7 +432,8 @@ o.GetRequiredService<IEnumerable<IPaymentLinkExtension>>().ToDictionary(o => o.P
|
|||||||
services.AddSingleton<IHostedService, TransactionLabelMarkerHostedService>();
|
services.AddSingleton<IHostedService, TransactionLabelMarkerHostedService>();
|
||||||
services.AddSingleton<IHostedService, UserEventHostedService>();
|
services.AddSingleton<IHostedService, UserEventHostedService>();
|
||||||
services.AddSingleton<IHostedService, DynamicDnsHostedService>();
|
services.AddSingleton<IHostedService, DynamicDnsHostedService>();
|
||||||
services.AddSingleton<IHostedService, PaymentRequestStreamer>();
|
services.AddSingleton<PaymentRequestStreamer>();
|
||||||
|
services.AddSingleton<IHostedService>(s => s.GetRequiredService<PaymentRequestStreamer>());
|
||||||
services.AddSingleton<IBackgroundJobClient, BackgroundJobClient>();
|
services.AddSingleton<IBackgroundJobClient, BackgroundJobClient>();
|
||||||
services.AddScoped<IAuthorizationHandler, CookieAuthorizationHandler>();
|
services.AddScoped<IAuthorizationHandler, CookieAuthorizationHandler>();
|
||||||
services.AddScoped<IAuthorizationHandler, BitpayAuthorizationHandler>();
|
services.AddScoped<IAuthorizationHandler, BitpayAuthorizationHandler>();
|
||||||
|
|||||||
@@ -43,10 +43,10 @@ namespace BTCPayServer.Models.PaymentRequestViewModels
|
|||||||
var blob = data.GetBlob();
|
var blob = data.GetBlob();
|
||||||
FormId = blob.FormId;
|
FormId = blob.FormId;
|
||||||
Title = blob.Title;
|
Title = blob.Title;
|
||||||
Amount = blob.Amount;
|
Amount = data.Amount;
|
||||||
Currency = blob.Currency;
|
Currency = data.Currency;
|
||||||
Description = blob.Description;
|
Description = blob.Description;
|
||||||
ExpiryDate = blob.ExpiryDate?.UtcDateTime;
|
ExpiryDate = data.Expiry?.UtcDateTime;
|
||||||
Email = blob.Email;
|
Email = blob.Email;
|
||||||
AllowCustomPaymentAmounts = blob.AllowCustomPaymentAmounts;
|
AllowCustomPaymentAmounts = blob.AllowCustomPaymentAmounts;
|
||||||
FormResponse = blob.FormResponse is null
|
FormResponse = blob.FormResponse is null
|
||||||
@@ -101,25 +101,25 @@ namespace BTCPayServer.Models.PaymentRequestViewModels
|
|||||||
var blob = data.GetBlob();
|
var blob = data.GetBlob();
|
||||||
Archived = data.Archived;
|
Archived = data.Archived;
|
||||||
Title = blob.Title;
|
Title = blob.Title;
|
||||||
Amount = blob.Amount;
|
Amount = data.Amount;
|
||||||
Currency = blob.Currency;
|
Currency = data.Currency;
|
||||||
Description = blob.Description;
|
Description = blob.Description;
|
||||||
ExpiryDate = blob.ExpiryDate?.UtcDateTime;
|
ExpiryDate = data.Expiry?.UtcDateTime;
|
||||||
Email = blob.Email;
|
Email = blob.Email;
|
||||||
AllowCustomPaymentAmounts = blob.AllowCustomPaymentAmounts;
|
AllowCustomPaymentAmounts = blob.AllowCustomPaymentAmounts;
|
||||||
switch (data.Status)
|
switch (data.Status)
|
||||||
{
|
{
|
||||||
case Client.Models.PaymentRequestData.PaymentRequestStatus.Pending:
|
case Client.Models.PaymentRequestStatus.Pending:
|
||||||
Status = "Pending";
|
Status = "Pending";
|
||||||
IsPending = true;
|
IsPending = true;
|
||||||
break;
|
break;
|
||||||
case Client.Models.PaymentRequestData.PaymentRequestStatus.Processing:
|
case Client.Models.PaymentRequestStatus.Processing:
|
||||||
Status = "Processing";
|
Status = "Processing";
|
||||||
break;
|
break;
|
||||||
case Client.Models.PaymentRequestData.PaymentRequestStatus.Completed:
|
case Client.Models.PaymentRequestStatus.Completed:
|
||||||
Status = "Settled";
|
Status = "Settled";
|
||||||
break;
|
break;
|
||||||
case Client.Models.PaymentRequestData.PaymentRequestStatus.Expired:
|
case Client.Models.PaymentRequestStatus.Expired:
|
||||||
Status = "Expired";
|
Status = "Expired";
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ using System.Linq;
|
|||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using BTCPayServer.Abstractions.Extensions;
|
using BTCPayServer.Abstractions.Extensions;
|
||||||
|
using BTCPayServer.Client.Models;
|
||||||
using BTCPayServer.Controllers;
|
using BTCPayServer.Controllers;
|
||||||
using BTCPayServer.Data;
|
using BTCPayServer.Data;
|
||||||
using BTCPayServer.Events;
|
using BTCPayServer.Events;
|
||||||
@@ -10,13 +11,13 @@ using BTCPayServer.HostedServices;
|
|||||||
using BTCPayServer.Logging;
|
using BTCPayServer.Logging;
|
||||||
using BTCPayServer.Services;
|
using BTCPayServer.Services;
|
||||||
using BTCPayServer.Services.PaymentRequests;
|
using BTCPayServer.Services.PaymentRequests;
|
||||||
|
using Google.Apis.Storage.v1.Data;
|
||||||
using Microsoft.AspNetCore.Builder;
|
using Microsoft.AspNetCore.Builder;
|
||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Microsoft.AspNetCore.Routing;
|
using Microsoft.AspNetCore.Routing;
|
||||||
using Microsoft.AspNetCore.SignalR;
|
using Microsoft.AspNetCore.SignalR;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using PaymentRequestData = BTCPayServer.Client.Models.PaymentRequestData;
|
|
||||||
|
|
||||||
namespace BTCPayServer.PaymentRequest
|
namespace BTCPayServer.PaymentRequest
|
||||||
{
|
{
|
||||||
@@ -108,61 +109,52 @@ namespace BTCPayServer.PaymentRequest
|
|||||||
private readonly PaymentRequestRepository _PaymentRequestRepository;
|
private readonly PaymentRequestRepository _PaymentRequestRepository;
|
||||||
private readonly PrettyNameProvider _prettyNameProvider;
|
private readonly PrettyNameProvider _prettyNameProvider;
|
||||||
private readonly PaymentRequestService _PaymentRequestService;
|
private readonly PaymentRequestService _PaymentRequestService;
|
||||||
|
private readonly DelayedTaskScheduler _delayedTaskScheduler;
|
||||||
|
|
||||||
public PaymentRequestStreamer(EventAggregator eventAggregator,
|
public PaymentRequestStreamer(EventAggregator eventAggregator,
|
||||||
IHubContext<PaymentRequestHub> hubContext,
|
IHubContext<PaymentRequestHub> hubContext,
|
||||||
PaymentRequestRepository paymentRequestRepository,
|
PaymentRequestRepository paymentRequestRepository,
|
||||||
PrettyNameProvider prettyNameProvider,
|
PrettyNameProvider prettyNameProvider,
|
||||||
PaymentRequestService paymentRequestService,
|
PaymentRequestService paymentRequestService,
|
||||||
|
DelayedTaskScheduler delayedTaskScheduler,
|
||||||
Logs logs) : base(eventAggregator, logs)
|
Logs logs) : base(eventAggregator, logs)
|
||||||
{
|
{
|
||||||
_HubContext = hubContext;
|
_HubContext = hubContext;
|
||||||
_PaymentRequestRepository = paymentRequestRepository;
|
_PaymentRequestRepository = paymentRequestRepository;
|
||||||
_prettyNameProvider = prettyNameProvider;
|
_prettyNameProvider = prettyNameProvider;
|
||||||
_PaymentRequestService = paymentRequestService;
|
_PaymentRequestService = paymentRequestService;
|
||||||
|
_delayedTaskScheduler = delayedTaskScheduler;
|
||||||
}
|
}
|
||||||
|
|
||||||
public override async Task StartAsync(CancellationToken cancellationToken)
|
public override async Task StartAsync(CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
|
this.PushEvent(new Starting());
|
||||||
await base.StartAsync(cancellationToken);
|
await base.StartAsync(cancellationToken);
|
||||||
_CheckingPendingPayments = CheckingPendingPayments(cancellationToken)
|
|
||||||
.ContinueWith(_ => _CheckingPendingPayments = null, TaskScheduler.Default);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task CheckingPendingPayments(CancellationToken cancellationToken)
|
internal void CheckExpirable()
|
||||||
{
|
{
|
||||||
Logs.PayServer.LogInformation("Starting payment request expiration watcher");
|
this.PushEvent(new Starting());
|
||||||
var items = await _PaymentRequestRepository.FindPaymentRequests(new PaymentRequestQuery
|
|
||||||
{
|
|
||||||
Status = new[]
|
|
||||||
{
|
|
||||||
PaymentRequestData.PaymentRequestStatus.Pending,
|
|
||||||
PaymentRequestData.PaymentRequestStatus.Processing
|
|
||||||
}
|
|
||||||
}, cancellationToken);
|
|
||||||
Logs.PayServer.LogInformation($"{items.Length} pending payment requests being checked since last run");
|
|
||||||
await Task.WhenAll(items.Select(i => _PaymentRequestService.UpdatePaymentRequestStateIfNeeded(i))
|
|
||||||
.ToArray());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Task _CheckingPendingPayments;
|
record Starting;
|
||||||
|
|
||||||
public override async Task StopAsync(CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
await base.StopAsync(cancellationToken);
|
|
||||||
await (_CheckingPendingPayments ?? Task.CompletedTask);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override void SubscribeToEvents()
|
protected override void SubscribeToEvents()
|
||||||
{
|
{
|
||||||
Subscribe<InvoiceEvent>();
|
Subscribe<InvoiceEvent>();
|
||||||
Subscribe<PaymentRequestUpdated>();
|
Subscribe<PaymentRequestEvent>();
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override async Task ProcessEvent(object evt, CancellationToken cancellationToken)
|
protected override async Task ProcessEvent(object evt, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
if (evt is InvoiceEvent invoiceEvent)
|
if (evt is Starting)
|
||||||
|
{
|
||||||
|
foreach (var req in await _PaymentRequestRepository.GetExpirablePaymentRequests(cancellationToken))
|
||||||
|
{
|
||||||
|
UpdateOnExpire(req);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (evt is InvoiceEvent invoiceEvent)
|
||||||
{
|
{
|
||||||
foreach (var paymentId in PaymentRequestRepository.GetPaymentIdsFromInternalTags(invoiceEvent.Invoice))
|
foreach (var paymentId in PaymentRequestRepository.GetPaymentIdsFromInternalTags(invoiceEvent.Invoice))
|
||||||
{
|
{
|
||||||
@@ -194,34 +186,28 @@ namespace BTCPayServer.PaymentRequest
|
|||||||
await InfoUpdated(paymentId);
|
await InfoUpdated(paymentId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if (evt is PaymentRequestUpdated updated)
|
else if (evt is PaymentRequestEvent updated)
|
||||||
{
|
{
|
||||||
await _PaymentRequestService.UpdatePaymentRequestStateIfNeeded(updated.PaymentRequestId);
|
await _PaymentRequestService.UpdatePaymentRequestStateIfNeeded(updated.Data.Id);
|
||||||
await InfoUpdated(updated.PaymentRequestId);
|
await InfoUpdated(updated.Data.Id);
|
||||||
|
|
||||||
var isPending = updated.Data.Status is
|
UpdateOnExpire(updated.Data);
|
||||||
PaymentRequestData.PaymentRequestStatus.Pending or
|
|
||||||
PaymentRequestData.PaymentRequestStatus.Processing;
|
|
||||||
var expiry = updated.Data.GetBlob().ExpiryDate;
|
|
||||||
if (isPending && expiry.HasValue)
|
|
||||||
{
|
|
||||||
QueueExpiryTask(
|
|
||||||
updated.PaymentRequestId,
|
|
||||||
expiry.Value.UtcDateTime,
|
|
||||||
cancellationToken);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void QueueExpiryTask(string paymentRequestId, DateTime expiry, CancellationToken cancellationToken)
|
private void UpdateOnExpire(Data.PaymentRequestData data)
|
||||||
{
|
{
|
||||||
Task.Run(async () =>
|
if (data is
|
||||||
{
|
{
|
||||||
var delay = expiry - DateTime.UtcNow;
|
Status: PaymentRequestStatus.Pending or PaymentRequestStatus.Processing,
|
||||||
if (delay > TimeSpan.Zero)
|
Expiry: { } e
|
||||||
await Task.Delay(delay, cancellationToken);
|
})
|
||||||
await _PaymentRequestService.UpdatePaymentRequestStateIfNeeded(paymentRequestId);
|
{
|
||||||
}, cancellationToken);
|
_delayedTaskScheduler.Schedule($"PAYREQ_{data.Id}", e, async () =>
|
||||||
|
{
|
||||||
|
await _PaymentRequestService.UpdatePaymentRequestStateIfNeeded(data.Id);
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task InfoUpdated(string paymentRequestId)
|
private async Task InfoUpdated(string paymentRequestId)
|
||||||
|
|||||||
@@ -52,28 +52,28 @@ namespace BTCPayServer.PaymentRequest
|
|||||||
{
|
{
|
||||||
var blob = pr.GetBlob();
|
var blob = pr.GetBlob();
|
||||||
var currentStatus = pr.Status;
|
var currentStatus = pr.Status;
|
||||||
if (blob.ExpiryDate.HasValue)
|
if (pr.Expiry.HasValue)
|
||||||
{
|
{
|
||||||
if (blob.ExpiryDate.Value <= DateTimeOffset.UtcNow)
|
if (pr.Expiry.Value <= DateTimeOffset.UtcNow)
|
||||||
currentStatus = Client.Models.PaymentRequestData.PaymentRequestStatus.Expired;
|
currentStatus = Client.Models.PaymentRequestStatus.Expired;
|
||||||
}
|
}
|
||||||
else if (currentStatus != Client.Models.PaymentRequestData.PaymentRequestStatus.Completed)
|
else if (currentStatus != Client.Models.PaymentRequestStatus.Completed)
|
||||||
{
|
{
|
||||||
currentStatus = Client.Models.PaymentRequestData.PaymentRequestStatus.Pending;
|
currentStatus = Client.Models.PaymentRequestStatus.Pending;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (currentStatus != Client.Models.PaymentRequestData.PaymentRequestStatus.Expired)
|
if (currentStatus != Client.Models.PaymentRequestStatus.Expired)
|
||||||
{
|
{
|
||||||
var invoices = await _paymentRequestRepository.GetInvoicesForPaymentRequest(pr.Id);
|
var invoices = await _paymentRequestRepository.GetInvoicesForPaymentRequest(pr.Id);
|
||||||
var contributions = _invoiceRepository.GetContributionsByPaymentMethodId(blob.Currency, invoices, true);
|
var contributions = _invoiceRepository.GetContributionsByPaymentMethodId(pr.Currency, invoices, true);
|
||||||
|
|
||||||
currentStatus =
|
currentStatus =
|
||||||
(PaidEnough: contributions.Total >= blob.Amount,
|
(PaidEnough: contributions.Total >= pr.Amount,
|
||||||
SettledEnough: contributions.TotalSettled >= blob.Amount) switch
|
SettledEnough: contributions.TotalSettled >= pr.Amount) switch
|
||||||
{
|
{
|
||||||
{ SettledEnough: true } => Client.Models.PaymentRequestData.PaymentRequestStatus.Completed,
|
{ SettledEnough: true } => Client.Models.PaymentRequestStatus.Completed,
|
||||||
{ PaidEnough: true } => Client.Models.PaymentRequestData.PaymentRequestStatus.Processing,
|
{ PaidEnough: true } => Client.Models.PaymentRequestStatus.Processing,
|
||||||
_ => Client.Models.PaymentRequestData.PaymentRequestStatus.Pending
|
_ => Client.Models.PaymentRequestStatus.Pending
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -94,20 +94,20 @@ namespace BTCPayServer.PaymentRequest
|
|||||||
|
|
||||||
var blob = pr.GetBlob();
|
var blob = pr.GetBlob();
|
||||||
var invoices = await _paymentRequestRepository.GetInvoicesForPaymentRequest(id);
|
var invoices = await _paymentRequestRepository.GetInvoicesForPaymentRequest(id);
|
||||||
var paymentStats = _invoiceRepository.GetContributionsByPaymentMethodId(blob.Currency, invoices, true);
|
var paymentStats = _invoiceRepository.GetContributionsByPaymentMethodId(pr.Currency, invoices, true);
|
||||||
var amountDue = blob.Amount - paymentStats.Total;
|
var amountDue = pr.Amount - paymentStats.Total;
|
||||||
var pendingInvoice = invoices.OrderByDescending(entity => entity.InvoiceTime)
|
var pendingInvoice = invoices.OrderByDescending(entity => entity.InvoiceTime)
|
||||||
.FirstOrDefault(entity => entity.Status == InvoiceStatus.New);
|
.FirstOrDefault(entity => entity.Status == InvoiceStatus.New);
|
||||||
|
|
||||||
return new ViewPaymentRequestViewModel(pr)
|
return new ViewPaymentRequestViewModel(pr)
|
||||||
{
|
{
|
||||||
Archived = pr.Archived,
|
Archived = pr.Archived,
|
||||||
AmountFormatted = _displayFormatter.Currency(blob.Amount, blob.Currency, DisplayFormatter.CurrencyFormat.Symbol),
|
AmountFormatted = _displayFormatter.Currency(pr.Amount, pr.Currency, DisplayFormatter.CurrencyFormat.Symbol),
|
||||||
AmountCollected = paymentStats.Total,
|
AmountCollected = paymentStats.Total,
|
||||||
AmountCollectedFormatted = _displayFormatter.Currency(paymentStats.Total, blob.Currency, DisplayFormatter.CurrencyFormat.Symbol),
|
AmountCollectedFormatted = _displayFormatter.Currency(paymentStats.Total, pr.Currency, DisplayFormatter.CurrencyFormat.Symbol),
|
||||||
AmountDue = amountDue,
|
AmountDue = amountDue,
|
||||||
AmountDueFormatted = _displayFormatter.Currency(amountDue, blob.Currency, DisplayFormatter.CurrencyFormat.Symbol),
|
AmountDueFormatted = _displayFormatter.Currency(amountDue, pr.Currency, DisplayFormatter.CurrencyFormat.Symbol),
|
||||||
CurrencyData = _currencies.GetCurrencyData(blob.Currency, true),
|
CurrencyData = _currencies.GetCurrencyData(pr.Currency, true),
|
||||||
LastUpdated = DateTime.UtcNow,
|
LastUpdated = DateTime.UtcNow,
|
||||||
FormId = blob.FormId,
|
FormId = blob.FormId,
|
||||||
FormSubmitted = blob.FormResponse is not null,
|
FormSubmitted = blob.FormResponse is not null,
|
||||||
@@ -126,7 +126,7 @@ namespace BTCPayServer.PaymentRequest
|
|||||||
{
|
{
|
||||||
Id = entity.Id,
|
Id = entity.Id,
|
||||||
Amount = entity.Price,
|
Amount = entity.Price,
|
||||||
AmountFormatted = _displayFormatter.Currency(entity.Price, blob.Currency, DisplayFormatter.CurrencyFormat.Symbol),
|
AmountFormatted = _displayFormatter.Currency(entity.Price, pr.Currency, DisplayFormatter.CurrencyFormat.Symbol),
|
||||||
Currency = entity.Currency,
|
Currency = entity.Currency,
|
||||||
ExpiryDate = entity.ExpirationTime.DateTime,
|
ExpiryDate = entity.ExpirationTime.DateTime,
|
||||||
State = state,
|
State = state,
|
||||||
|
|||||||
84
BTCPayServer/Services/DelayedTaskScheduler.cs
Normal file
84
BTCPayServer/Services/DelayedTaskScheduler.cs
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
#nullable enable
|
||||||
|
using System;
|
||||||
|
using System.Collections.Concurrent;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using System.Threading;
|
||||||
|
using Amazon.Runtime.Internal.Util;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
|
namespace BTCPayServer.Services
|
||||||
|
{
|
||||||
|
public class DelayedTaskScheduler : IDisposable
|
||||||
|
{
|
||||||
|
public DelayedTaskScheduler(ILogger<DelayedTaskScheduler> logger)
|
||||||
|
{
|
||||||
|
Logger = logger;
|
||||||
|
}
|
||||||
|
record class TimerState(string Key, Func<Task> Act);
|
||||||
|
private readonly Dictionary<string, Timer> _timers = new();
|
||||||
|
bool disposed = false;
|
||||||
|
|
||||||
|
public ILogger<DelayedTaskScheduler> Logger { get; }
|
||||||
|
|
||||||
|
public void Schedule(string key, DateTimeOffset executeAt, Func<Task> act)
|
||||||
|
{
|
||||||
|
lock (_timers)
|
||||||
|
{
|
||||||
|
if (disposed)
|
||||||
|
return;
|
||||||
|
_timers.TryGetValue(key, out var existing);
|
||||||
|
if (existing != null)
|
||||||
|
{
|
||||||
|
existing.Dispose();
|
||||||
|
_timers.Remove(key);
|
||||||
|
}
|
||||||
|
var due = executeAt - DateTimeOffset.UtcNow;
|
||||||
|
if (due < TimeSpan.Zero)
|
||||||
|
due = TimeSpan.Zero;
|
||||||
|
var timer = new Timer(TimerCallback, new TimerState(key, act), Timeout.Infinite, Timeout.Infinite);
|
||||||
|
_timers.Add(key, timer);
|
||||||
|
timer.Change((long)due.TotalMilliseconds, (long)Timeout.Infinite);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void TimerCallback(object? state)
|
||||||
|
{
|
||||||
|
var s = (TimerState)state!;
|
||||||
|
Task.Run(async () =>
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await s.Act();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Logger.LogError(ex, $"Error executing delayed task for key {s.Key}");
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
Timer? timer = null;
|
||||||
|
lock (_timers)
|
||||||
|
{
|
||||||
|
if (_timers.TryGetValue(s.Key, out timer))
|
||||||
|
{
|
||||||
|
_timers.Remove(s.Key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
timer?.Dispose();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
lock (_timers)
|
||||||
|
{
|
||||||
|
disposed = true;
|
||||||
|
foreach (var t in _timers.Values)
|
||||||
|
t.Dispose();
|
||||||
|
_timers.Clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -95,7 +95,7 @@ namespace BTCPayServer.Services.PaymentRequests
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task UpdatePaymentRequestStatus(string paymentRequestId, Client.Models.PaymentRequestData.PaymentRequestStatus status, CancellationToken cancellationToken = default)
|
public async Task UpdatePaymentRequestStatus(string paymentRequestId, Client.Models.PaymentRequestStatus status, CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
await using var context = _ContextFactory.CreateContext();
|
await using var context = _ContextFactory.CreateContext();
|
||||||
var paymentRequestData = await context.FindAsync<PaymentRequestData>(paymentRequestId);
|
var paymentRequestData = await context.FindAsync<PaymentRequestData>(paymentRequestId);
|
||||||
@@ -113,7 +113,17 @@ namespace BTCPayServer.Services.PaymentRequests
|
|||||||
Type = PaymentRequestEvent.StatusChanged
|
Type = PaymentRequestEvent.StatusChanged
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
public async Task<PaymentRequestData[]> GetExpirablePaymentRequests(CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
using var context = _ContextFactory.CreateContext();
|
||||||
|
var queryable = context.PaymentRequests.Include(data => data.StoreData).AsQueryable();
|
||||||
|
queryable =
|
||||||
|
queryable
|
||||||
|
.Where(data =>
|
||||||
|
(data.Status == Client.Models.PaymentRequestStatus.Pending || data.Status == Client.Models.PaymentRequestStatus.Processing) &&
|
||||||
|
data.Expiry != null);
|
||||||
|
return await queryable.ToArrayAsync(cancellationToken);
|
||||||
|
}
|
||||||
public async Task<PaymentRequestData[]> FindPaymentRequests(PaymentRequestQuery query, CancellationToken cancellationToken = default)
|
public async Task<PaymentRequestData[]> FindPaymentRequests(PaymentRequestQuery query, CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
using var context = _ContextFactory.CreateContext();
|
using var context = _ContextFactory.CreateContext();
|
||||||
@@ -202,17 +212,11 @@ namespace BTCPayServer.Services.PaymentRequests
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public class PaymentRequestUpdated
|
|
||||||
{
|
|
||||||
public string PaymentRequestId { get; set; }
|
|
||||||
public PaymentRequestData Data { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
public class PaymentRequestQuery
|
public class PaymentRequestQuery
|
||||||
{
|
{
|
||||||
public string StoreId { get; set; }
|
public string StoreId { get; set; }
|
||||||
public bool IncludeArchived { get; set; } = true;
|
public bool IncludeArchived { get; set; } = true;
|
||||||
public Client.Models.PaymentRequestData.PaymentRequestStatus[] Status { get; set; }
|
public Client.Models.PaymentRequestStatus[] Status { get; set; }
|
||||||
public string UserId { get; set; }
|
public string UserId { get; set; }
|
||||||
public int? Skip { get; set; }
|
public int? Skip { get; set; }
|
||||||
public int? Count { get; set; }
|
public int? Count { get; set; }
|
||||||
|
|||||||
Reference in New Issue
Block a user