Payments should use composite key (#6240)

* Payments should use composite key

* Invert PK for InvoiceAddress
This commit is contained in:
Nicolas Dorier 2024-09-23 17:06:56 +09:00 committed by GitHub
parent 36a5d0ee3f
commit 3cf1aa00fa
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
16 changed files with 98 additions and 36 deletions

View file

@ -82,7 +82,7 @@ namespace BTCPayServer.Data
PairingCodeData.OnModelCreating(builder);
//PayjoinLock.OnModelCreating(builder);
PaymentRequestData.OnModelCreating(builder, Database);
PaymentData.OnModelCreating(builder, Database);
PaymentData.OnModelCreating(builder);
PayoutData.OnModelCreating(builder, Database);
//PlannedTransaction.OnModelCreating(builder);
PullPaymentData.OnModelCreating(builder, Database);

View file

@ -22,5 +22,6 @@
<None Remove="DBScripts\002.RefactorPayouts.sql" />
<None Remove="DBScripts\003.RefactorPendingInvoicesPayments.sql" />
<None Remove="DBScripts\004.MonitoredInvoices.sql" />
<None Remove="DBScripts\005.PaymentsRenaming.sql" />
</ItemGroup>
</Project>

View file

@ -0,0 +1,17 @@
DROP FUNCTION get_monitored_invoices;
CREATE OR REPLACE FUNCTION get_monitored_invoices(payment_method_id TEXT)
RETURNS TABLE (invoice_id TEXT, payment_id TEXT, payment_method_id TEXT) AS $$
WITH cte AS (
-- Get all the invoices which are pending. Even if no payments.
SELECT i."Id" invoice_id, p."Id" payment_id, p."PaymentMethodId" payment_method_id FROM "Invoices" i LEFT JOIN "Payments" p ON i."Id" = p."InvoiceDataId"
WHERE is_pending(i."Status")
UNION ALL
-- For invoices not pending, take all of those which have pending payments
SELECT i."Id", p."Id", p."PaymentMethodId" payment_method_id FROM "Invoices" i INNER JOIN "Payments" p ON i."Id" = p."InvoiceDataId"
WHERE is_pending(p."Status") AND NOT is_pending(i."Status"))
SELECT cte.* FROM cte
LEFT JOIN "Payments" p ON cte.payment_id=p."Id" AND cte.payment_id=p."PaymentMethodId"
LEFT JOIN "Invoices" i ON cte.invoice_id=i."Id"
WHERE (p."PaymentMethodId" IS NOT NULL AND p."PaymentMethodId" = payment_method_id) OR
(p."PaymentMethodId" IS NULL AND get_prompt(i."Blob2", payment_method_id) IS NOT NULL AND (get_prompt(i."Blob2", payment_method_id)->'activated')::BOOLEAN IS NOT FALSE);
$$ LANGUAGE SQL STABLE;

View file

@ -19,7 +19,7 @@ namespace BTCPayServer.Data
.WithMany(i => i.AddressInvoices).OnDelete(DeleteBehavior.Cascade);
builder.Entity<AddressInvoiceData>()
#pragma warning disable CS0618
.HasKey(o => new { o.PaymentMethodId, o.Address });
.HasKey(o => new { o.Address, o.PaymentMethodId });
#pragma warning restore CS0618
}
}

View file

@ -44,9 +44,9 @@ namespace BTCPayServer.Data
}
var cryptoCode = blob["cryptoCode"].Value<string>();
Type = cryptoCode + "_" + blob["cryptoPaymentDataType"].Value<string>();
Type = MigrationExtensions.MigratePaymentMethodId(Type);
var divisibility = MigrationExtensions.GetDivisibility(Type);
PaymentMethodId = cryptoCode + "_" + blob["cryptoPaymentDataType"].Value<string>();
PaymentMethodId = MigrationExtensions.MigratePaymentMethodId(PaymentMethodId);
var divisibility = MigrationExtensions.GetDivisibility(PaymentMethodId);
Currency = blob["cryptoCode"].Value<string>();
blob.Remove("cryptoCode");
blob.Remove("cryptoPaymentDataType");

View file

@ -27,13 +27,15 @@ namespace BTCPayServer.Data
[Obsolete("Use Blob2 instead")]
public byte[] Blob { get; set; }
public string Blob2 { get; set; }
public string Type { get; set; }
public string PaymentMethodId { get; set; }
[Obsolete("Use Status instead")]
public bool? Accounted { get; set; }
public PaymentStatus? Status { get; set; }
public static bool IsPending(PaymentStatus? status) => throw new NotSupportedException();
internal static void OnModelCreating(ModelBuilder builder, DatabaseFacade databaseFacade)
internal static void OnModelCreating(ModelBuilder builder)
{
builder.Entity<PaymentData>()
.HasKey(o => new { o.Id, o.PaymentMethodId });
builder.Entity<PaymentData>()
.HasOne(o => o.InvoiceData)
.WithMany(i => i.Payments).OnDelete(DeleteBehavior.Cascade);

View file

@ -33,13 +33,13 @@ namespace BTCPayServer.Migrations
WHEN STRPOS((string_to_array("Address", '#'))[2], '_MoneroLike') > 0 THEN replace((string_to_array("Address", '#'))[2],'_MoneroLike','-CHAIN')
WHEN STRPOS((string_to_array("Address", '#'))[2], '_ZcashLike') > 0 THEN replace((string_to_array("Address", '#'))[2],'_ZcashLike','-CHAIN')
ELSE '' END;
ALTER TABLE "AddressInvoices" DROP COLUMN IF EXISTS "CreatedTime";
DELETE FROM "AddressInvoices" WHERE "PaymentMethodId" = '';
""");
migrationBuilder.AddPrimaryKey(
name: "PK_AddressInvoices",
table: "AddressInvoices",
columns: new[] { "PaymentMethodId", "Address" });
columns: new[] { "Address", "PaymentMethodId" });
migrationBuilder.Sql("VACUUM (ANALYZE) \"AddressInvoices\";", true);
}

View file

@ -0,0 +1,39 @@
using BTCPayServer.Data;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace BTCPayServer.Migrations
{
[DbContext(typeof(ApplicationDbContext))]
[Migration("20240923065254_refactorpayments")]
[DBScript("005.PaymentsRenaming.sql")]
public partial class refactorpayments : DBScriptsMigration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropPrimaryKey(
name: "PK_Payments",
table: "Payments");
migrationBuilder.RenameColumn(
name: "Type",
table: "Payments",
newName: "PaymentMethodId");
migrationBuilder.Sql("UPDATE \"Payments\" SET \"PaymentMethodId\"='' WHERE \"PaymentMethodId\" IS NULL;");
migrationBuilder.AddPrimaryKey(
name: "PK_Payments",
table: "Payments",
columns: new[] { "Id", "PaymentMethodId" });
base.Up(migrationBuilder);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
}
}
}

View file

@ -1,6 +1,4 @@
using System;
using BTCPayServer.Data;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
@ -9,17 +7,20 @@ using Microsoft.EntityFrameworkCore.Migrations;
namespace BTCPayServer.Migrations
{
[DbContext(typeof(ApplicationDbContext))]
[Migration("20240405052858_cleanup_address_invoices")]
public partial class cleanup_address_invoices : Migration
[Migration("20240923071444_temprefactor2")]
public partial class temprefactor2 : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.Sql(@"
DELETE FROM ""AddressInvoices"" WHERE ""Address"" LIKE '%_LightningLike';
ALTER TABLE ""AddressInvoices"" DROP COLUMN IF EXISTS ""CreatedTime"";
");
migrationBuilder.Sql(@"VACUUM (FULL, ANALYZE) ""AddressInvoices"";", true);
migrationBuilder.DropPrimaryKey(
name: "PK_AddressInvoices",
table: "AddressInvoices");
migrationBuilder.AddPrimaryKey(
name: "PK_AddressInvoices",
table: "AddressInvoices",
columns: new[] { "Address", "PaymentMethodId" });
}
/// <inheritdoc />

View file

@ -60,16 +60,16 @@ namespace BTCPayServer.Migrations
modelBuilder.Entity("BTCPayServer.Data.AddressInvoiceData", b =>
{
b.Property<string>("PaymentMethodId")
b.Property<string>("Address")
.HasColumnType("text");
b.Property<string>("Address")
b.Property<string>("PaymentMethodId")
.HasColumnType("text");
b.Property<string>("InvoiceDataId")
.HasColumnType("text");
b.HasKey("PaymentMethodId", "Address");
b.HasKey("Address", "PaymentMethodId");
b.HasIndex("InvoiceDataId");
@ -482,6 +482,9 @@ namespace BTCPayServer.Migrations
b.Property<string>("Id")
.HasColumnType("text");
b.Property<string>("PaymentMethodId")
.HasColumnType("text");
b.Property<bool?>("Accounted")
.HasColumnType("boolean");
@ -506,10 +509,7 @@ namespace BTCPayServer.Migrations
b.Property<string>("Status")
.HasColumnType("text");
b.Property<string>("Type")
.HasColumnType("text");
b.HasKey("Id");
b.HasKey("Id", "PaymentMethodId");
b.HasIndex("InvoiceDataId");

View file

@ -698,11 +698,13 @@ retry:
await writer.FlushAsync();
}
isHeader = true;
using (var writer = db.BeginTextImport("COPY \"Payments\" (\"Id\",\"Blob\",\"InvoiceDataId\",\"Accounted\",\"Blob2\",\"Type\") FROM STDIN DELIMITER ',' CSV HEADER"))
using (var writer = db.BeginTextImport("COPY \"Payments\" (\"Id\",\"Blob\",\"InvoiceDataId\",\"Accounted\",\"Blob2\",\"PaymentMethodId\") FROM STDIN DELIMITER ',' CSV HEADER"))
{
foreach (var invoice in oldPayments)
{
var localPayment = invoice.Replace("3sgUCCtUBg6S8LJkrbdfAWbsJMqByFLfvSqjG6xKBWEd", storeId);
// Old data could have Type to null.
localPayment += "BTC-CHAIN";
await writer.WriteLineAsync(localPayment);
}
await writer.FlushAsync();

View file

@ -604,7 +604,7 @@
},
"expectedProperties": {
"Created": "04/23/2019 18:27:56 +00:00",
"Type": "BTC-CHAIN",
"PaymentMethodId": "BTC-CHAIN",
"Currency": "BTC",
"Status": "Settled",
"Amount": "0.07299962",
@ -634,7 +634,7 @@
},
"expectedProperties": {
"Created": "10/01/2018 14:13:22 +00:00",
"Type": "BTC-CHAIN",
"PaymentMethodId": "BTC-CHAIN",
"Currency": "BTC",
"Status": "Settled",
"Amount": "0.00017863",
@ -666,7 +666,7 @@
"Created": "03/21/2024 07:24:35 +00:00",
"CreatedInMs": "1711005875969",
"Amount": "0.00000001",
"Type": "BTC-LNURL",
"PaymentMethodId": "BTC-LNURL",
"Currency": "BTC"
}
},
@ -697,7 +697,7 @@
"Created": "03/20/2024 22:39:08 +00:00",
"CreatedInMs": "1710974348741",
"Amount": "0.00197864",
"Type": "BTC-CHAIN",
"PaymentMethodId": "BTC-CHAIN",
"Currency": "BTC",
"Status": "Settled",
"Accounted": null

View file

@ -674,7 +674,7 @@ namespace BTCPayServer.Controllers
private async Task<HashSet<string>> GetAddresses(PaymentMethodId paymentMethodId, string[] selectedItems)
{
using var ctx = _dbContextFactory.CreateContext();
return new HashSet<string>(await ctx.AddressInvoices.Where(i => i.PaymentMethodId == paymentMethodId.ToString() && selectedItems.Contains(i.InvoiceDataId)).Select(i => i.Address).ToArrayAsync());
return new HashSet<string>(await ctx.AddressInvoices.Where(i => selectedItems.Contains(i.InvoiceDataId) && i.PaymentMethodId == paymentMethodId.ToString()).Select(i => i.Address).ToArrayAsync());
}
[HttpGet("i/{invoiceId}")]

View file

@ -34,13 +34,13 @@ namespace BTCPayServer.Data
}
public static PaymentData SetBlob(this PaymentData paymentData, PaymentMethodId paymentMethodId, PaymentBlob blob)
{
paymentData.Type = paymentMethodId.ToString();
paymentData.PaymentMethodId = paymentMethodId.ToString();
paymentData.Blob2 = JToken.FromObject(blob, InvoiceDataExtensions.DefaultSerializer).ToString(Newtonsoft.Json.Formatting.None);
return paymentData;
}
public static PaymentMethodId GetPaymentMethodId(this PaymentData paymentData)
{
return PaymentMethodId.Parse(paymentData.Type);
return PaymentMethodId.Parse(paymentData.PaymentMethodId);
}
public static PaymentEntity GetBlob(this PaymentData paymentData)
{

View file

@ -72,7 +72,7 @@ namespace BTCPayServer.Services.Invoices
using var db = _applicationDbContextFactory.CreateContext();
var row = (await db.AddressInvoices
.Include(a => a.InvoiceData.Payments)
.Where(a => a.PaymentMethodId == paymentMethodId.ToString() && a.Address == address)
.Where(a => a.Address == address && a.PaymentMethodId == paymentMethodId.ToString())
.Select(a => a.InvoiceData)
.FirstOrDefaultAsync());
return row is null ? null : ToEntity(row);
@ -101,7 +101,7 @@ namespace BTCPayServer.Services.Invoices
COALESCE(array_agg(to_jsonb(p)) FILTER (WHERE p."Id" IS NOT NULL), '{}') as payments,
(array_agg(to_jsonb(i)))[1] as invoice
FROM get_monitored_invoices(@pmi) m
LEFT JOIN "Payments" p ON p."Id" = m.payment_id
LEFT JOIN "Payments" p ON p."Id" = m.payment_id AND p."PaymentMethodId" = m.payment_method_id
LEFT JOIN "Invoices" i ON i."Id" = m.invoice_id
LEFT JOIN "AddressInvoices" ai ON i."Id" = ai."InvoiceDataId"
WHERE ai."PaymentMethodId" = @pmi

View file

@ -49,7 +49,7 @@ namespace BTCPayServer.Services.Invoices
if (invoice == null)
return null;
invoiceEntity = invoice.GetBlob();
var pmi = PaymentMethodId.Parse(paymentData.Type);
var pmi = PaymentMethodId.Parse(paymentData.PaymentMethodId);
PaymentPrompt paymentMethod = invoiceEntity.GetPaymentPrompt(pmi);
if (paymentMethod is null || !_handlers.TryGetValue(pmi, out var handler))
return null;