mirror of
https://github.com/aljazceru/btcpayserver.git
synced 2026-01-02 13:44:22 +01:00
Merge pull request #6642 from btcpayserver/feat/pay-request-improvements
Payment Request improvements
This commit is contained in:
@@ -37,6 +37,10 @@ namespace BTCPayServer.Client.Models
|
||||
public string Title { get; set; }
|
||||
public string Description { get; set; }
|
||||
public string Email { get; set; }
|
||||
/// <summary>
|
||||
/// Linking to invoices outside BTCPay Server using & user defined ids
|
||||
/// </summary>
|
||||
public string ReferenceId { get; set; }
|
||||
public bool AllowCustomPaymentAmounts { get; set; }
|
||||
|
||||
[JsonConverter(typeof(StringEnumConverter))]
|
||||
|
||||
@@ -17,5 +17,6 @@ public static class WebhookEventType
|
||||
public const string PaymentRequestCreated = nameof(PaymentRequestCreated);
|
||||
public const string PaymentRequestArchived = nameof(PaymentRequestArchived);
|
||||
public const string PaymentRequestStatusChanged = nameof(PaymentRequestStatusChanged);
|
||||
public const string PaymentRequestCompleted = nameof(PaymentRequestCompleted);
|
||||
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
using BTCPayServer.Client.Models;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
@@ -16,6 +17,11 @@ namespace BTCPayServer.Data
|
||||
public string Currency { get; set; }
|
||||
public decimal Amount { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Linking to invoices outside BTCPay Server using & user defined ids
|
||||
/// </summary>
|
||||
public string ReferenceId { get; set; }
|
||||
|
||||
public StoreData StoreData { get; set; }
|
||||
|
||||
public Client.Models.PaymentRequestStatus Status { get; set; }
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace BTCPayServer.Data.Migrations
|
||||
{
|
||||
[DbContext(typeof(ApplicationDbContext))]
|
||||
[Migration("20250407133937_AddingReferenceIdToPaymentRequest")]
|
||||
public partial class AddingReferenceIdToPaymentRequest : Migration
|
||||
{
|
||||
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "ReferenceId",
|
||||
table: "PaymentRequests",
|
||||
type: "TEXT",
|
||||
nullable: true);
|
||||
migrationBuilder.Sql("""
|
||||
CREATE UNIQUE INDEX IX_PaymentRequests_StoreDataId_ReferenceId
|
||||
ON "PaymentRequests" ("StoreDataId", "ReferenceId")
|
||||
WHERE "ReferenceId" IS NOT NULL;
|
||||
""");
|
||||
}
|
||||
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "ReferenceId",
|
||||
table: "PaymentRequests");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
// <auto-generated />
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using BTCPayServer.Data;
|
||||
@@ -544,6 +544,9 @@ namespace BTCPayServer.Migrations
|
||||
b.Property<DateTimeOffset?>("Expiry")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("ReferenceId")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("Status")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
@@ -2048,7 +2048,7 @@ namespace BTCPayServer.Tests
|
||||
new() { Title = "A", Currency = "helloinvalid", Amount = 1 });
|
||||
});
|
||||
var newPaymentRequest = await client.CreatePaymentRequest(user.StoreId,
|
||||
new() { Title = "A", Currency = "USD", Amount = 1 });
|
||||
new() { Title = "A", Currency = "USD", Amount = 1, ReferenceId = "1234"});
|
||||
|
||||
//list payment request
|
||||
var paymentRequests = await viewOnly.GetPaymentRequests(user.StoreId);
|
||||
@@ -2061,10 +2061,12 @@ namespace BTCPayServer.Tests
|
||||
var paymentRequest = await viewOnly.GetPaymentRequest(user.StoreId, newPaymentRequest.Id);
|
||||
Assert.Equal(newPaymentRequest.Title, paymentRequest.Title);
|
||||
Assert.Equal(newPaymentRequest.StoreId, user.StoreId);
|
||||
Assert.Equal(newPaymentRequest.ReferenceId, paymentRequest.ReferenceId);
|
||||
|
||||
//update payment request
|
||||
var updateRequest = paymentRequest;
|
||||
updateRequest.Title = "B";
|
||||
updateRequest.ReferenceId = "EmperorNicolasGeneralRockstar";
|
||||
await AssertHttpError(403, async () =>
|
||||
{
|
||||
await viewOnly.UpdatePaymentRequest(user.StoreId, paymentRequest.Id, updateRequest);
|
||||
@@ -2072,6 +2074,7 @@ namespace BTCPayServer.Tests
|
||||
await client.UpdatePaymentRequest(user.StoreId, paymentRequest.Id, updateRequest);
|
||||
paymentRequest = await client.GetPaymentRequest(user.StoreId, newPaymentRequest.Id);
|
||||
Assert.Equal(updateRequest.Title, paymentRequest.Title);
|
||||
Assert.Equal(updateRequest.ReferenceId, paymentRequest.ReferenceId);
|
||||
|
||||
//archive payment request
|
||||
await AssertHttpError(403, async () =>
|
||||
|
||||
@@ -43,12 +43,20 @@ namespace BTCPayServer.Tests
|
||||
Currency = "BTC",
|
||||
Amount = 1,
|
||||
StoreId = user.StoreId,
|
||||
Description = "description"
|
||||
Description = "description",
|
||||
ReferenceId = "custom-id-1"
|
||||
};
|
||||
var id = Assert
|
||||
.IsType<RedirectToActionResult>(await paymentRequestController.EditPaymentRequest(null, request))
|
||||
.RouteValues.Values.Last().ToString();
|
||||
|
||||
// Assert initial Title and ReferenceId
|
||||
var repo = tester.PayTester.GetService<PaymentRequestRepository>();
|
||||
var prData = await repo.FindPaymentRequest(id, user.UserId);
|
||||
Assert.NotNull(prData);
|
||||
Assert.Equal("original juice", prData.GetBlob().Title);
|
||||
Assert.Equal("custom-id-1", prData.ReferenceId);
|
||||
|
||||
paymentRequestController.HttpContext.SetPaymentRequestData(new PaymentRequestData { Id = id, StoreDataId = request.StoreId });
|
||||
|
||||
// Permission guard for guests editing
|
||||
@@ -56,8 +64,15 @@ namespace BTCPayServer.Tests
|
||||
.IsType<NotFoundResult>(await guestpaymentRequestController.EditPaymentRequest(user.StoreId, id));
|
||||
|
||||
request.Title = "update";
|
||||
request.ReferenceId = "custom-id-2";
|
||||
Assert.IsType<RedirectToActionResult>(await paymentRequestController.EditPaymentRequest(id, request));
|
||||
|
||||
// Assert updated Title and ReferenceId
|
||||
prData = await repo.FindPaymentRequest(id, user.UserId);
|
||||
Assert.NotNull(prData);
|
||||
Assert.Equal("update", prData.GetBlob().Title);
|
||||
Assert.Equal("custom-id-2", prData.ReferenceId);
|
||||
|
||||
Assert.Equal(request.Title,
|
||||
Assert.IsType<ViewPaymentRequestViewModel>(Assert
|
||||
.IsType<ViewResult>(await paymentRequestController.ViewPaymentRequest(id)).Model).Title);
|
||||
|
||||
@@ -205,7 +205,7 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
pr = new PaymentRequestData()
|
||||
{
|
||||
StoreDataId = storeId,
|
||||
Status = Client.Models.PaymentRequestStatus.Pending,
|
||||
Status = PaymentRequestStatus.Pending,
|
||||
Created = DateTimeOffset.UtcNow,
|
||||
Amount = request.Amount,
|
||||
Currency = request.Currency ?? StoreData.GetStoreBlob().DefaultCurrency,
|
||||
@@ -213,17 +213,19 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
};
|
||||
}
|
||||
|
||||
pr.ReferenceId = string.IsNullOrEmpty(request.ReferenceId) ? null : request.ReferenceId;
|
||||
|
||||
if (!ModelState.IsValid)
|
||||
return this.CreateValidationError(ModelState);
|
||||
|
||||
var blob = pr.GetBlob();
|
||||
pr.SetBlob(new()
|
||||
{
|
||||
Title = request.Title,
|
||||
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);
|
||||
@@ -251,6 +253,7 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
Title = blob.Title,
|
||||
ExpiryDate = data.Expiry,
|
||||
Email = blob.Email,
|
||||
ReferenceId = data.ReferenceId,
|
||||
AllowCustomPaymentAmounts = blob.AllowCustomPaymentAmounts,
|
||||
FormResponse = blob.FormResponse,
|
||||
FormId = blob.FormId
|
||||
|
||||
@@ -145,7 +145,7 @@ namespace BTCPayServer.Controllers
|
||||
amount = amountDue;
|
||||
var redirectUrl = _linkGenerator.PaymentRequestLink(id, request.Scheme, request.Host, request.PathBase);
|
||||
|
||||
JObject invoiceMetadata = prData.GetBlob()?.FormResponse ?? new JObject();
|
||||
JObject invoiceMetadata = prBlob.FormResponse ?? new JObject();
|
||||
invoiceMetadata.Merge(new InvoiceMetadata
|
||||
{
|
||||
OrderId = PaymentRequestRepository.GetOrderIdForPaymentRequest(id),
|
||||
@@ -163,7 +163,8 @@ namespace BTCPayServer.Controllers
|
||||
Checkout = { RedirectURL = redirectUrl },
|
||||
Receipt = new InvoiceDataBase.ReceiptOptions { Enabled = false }
|
||||
};
|
||||
|
||||
if (prData.ReferenceId is not null or "")
|
||||
invoiceRequest.AdditionalSearchTerms = [prData.ReferenceId];
|
||||
var additionalTags = new List<string> { PaymentRequestRepository.GetInternalTag(id) };
|
||||
return await CreateInvoiceCoreRaw(invoiceRequest, storeData, request.GetAbsoluteRoot(), additionalTags, cancellationToken);
|
||||
}
|
||||
|
||||
@@ -99,7 +99,8 @@ namespace BTCPayServer.Controllers
|
||||
Skip = model.Skip,
|
||||
Count = model.Count,
|
||||
Status = fs.GetFilterArray("status")?.Select(s => Enum.Parse<Client.Models.PaymentRequestStatus>(s, true)).ToArray(),
|
||||
IncludeArchived = fs.GetFilterBool("includearchived") ?? false
|
||||
IncludeArchived = fs.GetFilterBool("includearchived") ?? false,
|
||||
SearchText = model.SearchText
|
||||
});
|
||||
|
||||
model.Search = fs;
|
||||
@@ -206,6 +207,7 @@ namespace BTCPayServer.Controllers
|
||||
data.Amount = viewModel.Amount;
|
||||
data.Currency = viewModel.Currency ?? store.GetStoreBlob().DefaultCurrency;
|
||||
data.Expiry = viewModel.ExpiryDate?.ToUniversalTime();
|
||||
data.ReferenceId = viewModel.ReferenceId;
|
||||
blob.AllowCustomPaymentAmounts = viewModel.AllowCustomPaymentAmounts;
|
||||
blob.FormId = viewModel.FormId;
|
||||
|
||||
|
||||
@@ -47,7 +47,8 @@ public class PaymentRequestWebhookDeliveryRequest : WebhookSender.WebhookDeliver
|
||||
.Replace("{PaymentRequest.Currency}", data.Currency)
|
||||
.Replace("{PaymentRequest.Title}", blob.Title)
|
||||
.Replace("{PaymentRequest.Description}", blob.Description)
|
||||
.Replace("{PaymentRequest.Status}", data.Status.ToString());
|
||||
.Replace("{PaymentRequest.ReferenceId}", data.ReferenceId)
|
||||
.Replace("{PaymentRequest.Status}", _evt.Data.Status.ToString());
|
||||
|
||||
res = InterpolateJsonField(res, "PaymentRequest.FormResponse", blob.FormResponse);
|
||||
return res;
|
||||
|
||||
@@ -22,6 +22,7 @@ public class PaymentRequestWebhookProvider: WebhookProvider<PaymentRequestEvent>
|
||||
{WebhookEventType.PaymentRequestUpdated, "Payment Request - Updated"},
|
||||
{WebhookEventType.PaymentRequestArchived, "Payment Request - Archived"},
|
||||
{WebhookEventType.PaymentRequestStatusChanged, "Payment Request - Status Changed"},
|
||||
{WebhookEventType.PaymentRequestCompleted, "Payment Request - Completed"},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -42,6 +43,7 @@ public class PaymentRequestWebhookProvider: WebhookProvider<PaymentRequestEvent>
|
||||
PaymentRequestEvent.Updated => new WebhookPaymentRequestEvent(WebhookEventType.PaymentRequestUpdated, evt.Data.StoreDataId),
|
||||
PaymentRequestEvent.Archived => new WebhookPaymentRequestEvent(WebhookEventType.PaymentRequestArchived, evt.Data.StoreDataId),
|
||||
PaymentRequestEvent.StatusChanged => new WebhookPaymentRequestEvent(WebhookEventType.PaymentRequestStatusChanged, evt.Data.StoreDataId),
|
||||
PaymentRequestEvent.Completed => new WebhookPaymentRequestEvent(WebhookEventType.PaymentRequestCompleted, evt.Data.StoreDataId),
|
||||
_ => null
|
||||
};
|
||||
}
|
||||
|
||||
@@ -48,6 +48,7 @@ namespace BTCPayServer.Models.PaymentRequestViewModels
|
||||
Description = blob.Description;
|
||||
ExpiryDate = data.Expiry?.UtcDateTime;
|
||||
Email = blob.Email;
|
||||
ReferenceId = data.ReferenceId;
|
||||
AllowCustomPaymentAmounts = blob.AllowCustomPaymentAmounts;
|
||||
FormResponse = blob.FormResponse is null
|
||||
? null
|
||||
@@ -84,6 +85,9 @@ namespace BTCPayServer.Models.PaymentRequestViewModels
|
||||
[MailboxAddress]
|
||||
public string Email { get; set; }
|
||||
|
||||
[Display(Name = "Reference Id")]
|
||||
public string ReferenceId { get; set; }
|
||||
|
||||
[Display(Name = "Allow payee to create invoices with custom amounts")]
|
||||
public bool AllowCustomPaymentAmounts { get; set; }
|
||||
|
||||
@@ -106,6 +110,7 @@ namespace BTCPayServer.Models.PaymentRequestViewModels
|
||||
Description = blob.Description;
|
||||
ExpiryDate = data.Expiry?.UtcDateTime;
|
||||
Email = blob.Email;
|
||||
ReferenceId = data.ReferenceId;
|
||||
AllowCustomPaymentAmounts = blob.AllowCustomPaymentAmounts;
|
||||
switch (data.Status)
|
||||
{
|
||||
@@ -127,6 +132,7 @@ namespace BTCPayServer.Models.PaymentRequestViewModels
|
||||
}
|
||||
}
|
||||
public StoreBrandingViewModel StoreBranding { get; set; }
|
||||
public string ReferenceId { get; set; }
|
||||
public bool AllowCustomPaymentAmounts { get; set; }
|
||||
public string Email { get; set; }
|
||||
public string Status { get; set; }
|
||||
@@ -147,11 +153,11 @@ namespace BTCPayServer.Models.PaymentRequestViewModels
|
||||
#nullable enable
|
||||
public class InvoiceList : List<PaymentRequestInvoice>
|
||||
{
|
||||
static HashSet<InvoiceState> stateAllowedToDisplay = new HashSet<InvoiceState>
|
||||
{
|
||||
new InvoiceState(InvoiceStatus.New, InvoiceExceptionStatus.None),
|
||||
new InvoiceState(InvoiceStatus.New, InvoiceExceptionStatus.PaidPartial),
|
||||
};
|
||||
private static HashSet<InvoiceState> stateAllowedToDisplay =
|
||||
[
|
||||
new(InvoiceStatus.New, InvoiceExceptionStatus.None),
|
||||
new(InvoiceStatus.New, InvoiceExceptionStatus.PaidPartial)
|
||||
];
|
||||
public InvoiceList()
|
||||
{
|
||||
|
||||
@@ -195,7 +201,7 @@ namespace BTCPayServer.Models.PaymentRequestViewModels
|
||||
|
||||
public class PaymentRequestInvoicePayment
|
||||
{
|
||||
public static List<ViewPaymentRequestViewModel.PaymentRequestInvoicePayment>
|
||||
public static List<PaymentRequestInvoicePayment>
|
||||
GetViewModels(
|
||||
InvoiceEntity invoice,
|
||||
DisplayFormatter displayFormatter,
|
||||
@@ -215,7 +221,7 @@ namespace BTCPayServer.Models.PaymentRequestViewModels
|
||||
|
||||
string link = paymentMethodId is null ? null : txLinkProvider.GetTransactionLink(paymentMethodId, txId);
|
||||
|
||||
return new ViewPaymentRequestViewModel.PaymentRequestInvoicePayment
|
||||
return new PaymentRequestInvoicePayment
|
||||
{
|
||||
Amount = paymentEntity.PaidAmount.Gross,
|
||||
Paid = paymentEntity.InvoicePaidAmount.Net,
|
||||
|
||||
@@ -92,5 +92,29 @@ namespace BTCPayServer.Services
|
||||
pathBase: request.PathBase
|
||||
) ?? throw Bug();
|
||||
}
|
||||
|
||||
public string PaymentRequestByIdLink(string payReqId, HttpRequest request)
|
||||
{
|
||||
return LinkGenerator.GetUriByAction(
|
||||
action: nameof(UIPaymentRequestController.ViewPaymentRequest),
|
||||
controller: "UIPaymentRequest",
|
||||
values: new { payReqId },
|
||||
scheme: request.Scheme,
|
||||
host: request.Host,
|
||||
pathBase: request.PathBase
|
||||
) ?? throw Bug();
|
||||
}
|
||||
|
||||
public string PaymentRequestListLink(string storeId, HttpRequest request)
|
||||
{
|
||||
return LinkGenerator.GetUriByAction(
|
||||
action: nameof(UIPaymentRequestController.GetPaymentRequests),
|
||||
controller: "UIPaymentRequest",
|
||||
values: new { storeId },
|
||||
scheme: request.Scheme,
|
||||
host: request.Host,
|
||||
pathBase: request.PathBase
|
||||
) ?? throw Bug();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,10 +2,12 @@ using System;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Client.Models;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
using BTCPayServer.Services.Stores;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace BTCPayServer.Services.PaymentRequests
|
||||
{
|
||||
@@ -15,6 +17,7 @@ namespace BTCPayServer.Services.PaymentRequests
|
||||
public const string Updated = nameof(Updated);
|
||||
public const string Archived = nameof(Archived);
|
||||
public const string StatusChanged = nameof(StatusChanged);
|
||||
public const string Completed = nameof(Completed);
|
||||
public PaymentRequestData Data { get; set; }
|
||||
public string Type { get; set; }
|
||||
}
|
||||
@@ -99,9 +102,7 @@ namespace BTCPayServer.Services.PaymentRequests
|
||||
{
|
||||
await using var context = _ContextFactory.CreateContext();
|
||||
var paymentRequestData = await context.FindAsync<PaymentRequestData>(paymentRequestId);
|
||||
if (paymentRequestData == null)
|
||||
return;
|
||||
if( paymentRequestData.Status == status)
|
||||
if (paymentRequestData == null || paymentRequestData.Status == status)
|
||||
return;
|
||||
paymentRequestData.Status = status;
|
||||
|
||||
@@ -112,6 +113,15 @@ namespace BTCPayServer.Services.PaymentRequests
|
||||
Data = paymentRequestData,
|
||||
Type = PaymentRequestEvent.StatusChanged
|
||||
});
|
||||
|
||||
if (status == PaymentRequestStatus.Completed)
|
||||
{
|
||||
_eventAggregator.Publish(new PaymentRequestEvent()
|
||||
{
|
||||
Data = paymentRequestData,
|
||||
Type = PaymentRequestEvent.Completed
|
||||
});
|
||||
}
|
||||
}
|
||||
public async Task<PaymentRequestData[]> GetExpirablePaymentRequests(CancellationToken cancellationToken = default)
|
||||
{
|
||||
@@ -126,48 +136,43 @@ namespace BTCPayServer.Services.PaymentRequests
|
||||
}
|
||||
public async Task<PaymentRequestData[]> FindPaymentRequests(PaymentRequestQuery query, CancellationToken cancellationToken = default)
|
||||
{
|
||||
using var context = _ContextFactory.CreateContext();
|
||||
var queryable = context.PaymentRequests.Include(data => data.StoreData).AsQueryable();
|
||||
await using var context = _ContextFactory.CreateContext();
|
||||
IQueryable<PaymentRequestData> queryable = context.PaymentRequests.AsQueryable();
|
||||
|
||||
if (!string.IsNullOrEmpty(query.StoreId))
|
||||
queryable = queryable.Where(data => data.StoreDataId == query.StoreId);
|
||||
|
||||
if (!string.IsNullOrEmpty(query.SearchText))
|
||||
{
|
||||
if (string.IsNullOrEmpty(query.StoreId))
|
||||
throw new InvalidOperationException("PaymentRequestQuery.StoreId should be specified");
|
||||
// We are repeating the StoreId on purpose here, so Postgres can use the index
|
||||
queryable = context.PaymentRequests.Where(p => (p.StoreDataId == query.StoreId && p.ReferenceId == query.SearchText) || p.Id == query.SearchText);
|
||||
}
|
||||
|
||||
queryable = queryable.Include(data => data.StoreData);
|
||||
|
||||
if (!query.IncludeArchived)
|
||||
{
|
||||
queryable = queryable.Where(data => !data.Archived);
|
||||
}
|
||||
if (!string.IsNullOrEmpty(query.StoreId))
|
||||
{
|
||||
queryable = queryable.Where(data =>
|
||||
data.StoreDataId == query.StoreId);
|
||||
}
|
||||
|
||||
if (query.Status != null && query.Status.Any())
|
||||
{
|
||||
queryable = queryable.Where(data =>
|
||||
query.Status.Contains(data.Status));
|
||||
}
|
||||
queryable = queryable.Where(data => query.Status.Contains(data.Status));
|
||||
|
||||
if (query.Ids != null && query.Ids.Any())
|
||||
{
|
||||
queryable = queryable.Where(data =>
|
||||
query.Ids.Contains(data.Id));
|
||||
}
|
||||
queryable = queryable.Where(data => query.Ids.Contains(data.Id));
|
||||
|
||||
if (!string.IsNullOrEmpty(query.UserId))
|
||||
{
|
||||
queryable = queryable.Where(i =>
|
||||
i.StoreData != null && i.StoreData.UserStores.Any(u => u.ApplicationUserId == query.UserId));
|
||||
}
|
||||
queryable = queryable.Where(data =>
|
||||
data.StoreData.UserStores.Any(u => u.ApplicationUserId == query.UserId));
|
||||
|
||||
queryable = queryable.OrderByDescending(u => u.Created);
|
||||
|
||||
if (query.Skip.HasValue)
|
||||
{
|
||||
queryable = queryable.Skip(query.Skip.Value);
|
||||
}
|
||||
|
||||
if (query.Count.HasValue)
|
||||
{
|
||||
queryable = queryable.Take(query.Count.Value);
|
||||
}
|
||||
|
||||
var items = await queryable.ToArrayAsync(cancellationToken);
|
||||
return items;
|
||||
}
|
||||
@@ -221,5 +226,6 @@ namespace BTCPayServer.Services.PaymentRequests
|
||||
public int? Skip { get; set; }
|
||||
public int? Count { get; set; }
|
||||
public string[] Ids { get; set; }
|
||||
public string SearchText { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
@using BTCPayServer.Services.PaymentRequests
|
||||
@using System.Globalization
|
||||
@using BTCPayServer.Client
|
||||
@using BTCPayServer.Controllers
|
||||
@using BTCPayServer.Forms
|
||||
@using BTCPayServer.TagHelpers
|
||||
@using Microsoft.AspNetCore.Mvc.TagHelpers
|
||||
@@ -82,6 +83,11 @@
|
||||
<label asp-for="AllowCustomPaymentAmounts" class="form-check-label"></label>
|
||||
<span asp-validation-for="AllowCustomPaymentAmounts" class="text-danger"></span>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label asp-for="ReferenceId" class="form-label"></label>
|
||||
<input asp-for="ReferenceId" class="form-control" />
|
||||
<span asp-validation-for="ReferenceId" class="text-danger"></span>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label asp-for="ExpiryDate" class="form-label"></label>
|
||||
<div class="input-group ">
|
||||
@@ -99,7 +105,8 @@
|
||||
<input type="email" asp-for="Email" placeholder="@StringLocalizer["Firstname Lastname <email@example.com>"]" class="form-control" />
|
||||
<span asp-validation-for="Email" class="text-danger"></span>
|
||||
<div id="PaymentRequestEmailHelpBlock" class="form-text">
|
||||
@ViewLocalizer["This will send notification mails to the recipient, as configured by the {0}.", Html.ActionLink(StringLocalizer["email rules"], "StoreEmails", "UIStores", new { storeId = Model.StoreId })]
|
||||
@ViewLocalizer["This will send notification mails to the recipient, as configured by the {0}.",
|
||||
Html.ActionLink(StringLocalizer["email rules"], nameof(UIStoresController.StoreEmailRulesList), "UIStores", new { storeId = Model.StoreId })]
|
||||
@if (Model.HasEmailRules is not true)
|
||||
{
|
||||
<div class="info-note mt-1 text-warning" role="alert">
|
||||
|
||||
@@ -3,6 +3,9 @@
|
||||
@using Microsoft.AspNetCore.Mvc.TagHelpers
|
||||
@using BTCPayServer.Components
|
||||
@using BTCPayServer.Client
|
||||
@using BTCPayServer.Services
|
||||
@using BTCPayServer.TagHelpers
|
||||
@inject CallbackGenerator CallbackGenerator
|
||||
@model BTCPayServer.Models.PaymentRequestViewModels.ListPaymentRequestsViewModel
|
||||
@{
|
||||
Layout = "_Layout";
|
||||
@@ -54,7 +57,7 @@
|
||||
<input type="hidden" asp-for="Count" />
|
||||
<input type="hidden" asp-for="TimezoneOffset" />
|
||||
<input asp-for="SearchTerm" type="hidden" value="@Model.Search.WithoutSearchText()"/>
|
||||
<input asp-for="SearchText" class="form-control" placeholder="@StringLocalizer["Search…"]" />
|
||||
<input asp-for="SearchText" class="form-control" placeholder="@StringLocalizer["Search by Id..."]" />
|
||||
<div class="dropdown">
|
||||
<button id="StatusOptionsToggle" class="btn btn-secondary dropdown-toggle dropdown-toggle-custom-caret" type="button" data-bs-toggle="dropdown" aria-expanded="false">
|
||||
@if (statusFilterCount > 0)
|
||||
@@ -91,6 +94,7 @@
|
||||
</button>
|
||||
</div>
|
||||
</th>
|
||||
<th text-translate="true">Reference Id</th>
|
||||
<th text-translate="true">Status</th>
|
||||
<th class="amount-col" text-translate="true">Amount</th>
|
||||
<th></th>
|
||||
@@ -106,6 +110,9 @@
|
||||
<td class="date-col">
|
||||
@(item.ExpiryDate?.ToBrowserDate() ?? new HtmlString("<span class=\"text-muted\">No Expiry</span>"))
|
||||
</td>
|
||||
<td>
|
||||
@item.ReferenceId
|
||||
</td>
|
||||
<td>
|
||||
<span class="badge badge-@item.Status.ToLower() status-badge">@item.Status</span>
|
||||
</td>
|
||||
@@ -115,6 +122,11 @@
|
||||
<td class="text-end">
|
||||
<div class="d-inline-flex align-items-center gap-3">
|
||||
<a asp-action="ViewPaymentRequest" asp-route-payReqId="@item.Id" id="PaymentRequest-@item.Id" target="_blank" text-translate="true">View</a>
|
||||
<button type="button" class="btn btn-link p-0 clipboard-button"
|
||||
data-clipboard="@CallbackGenerator.PaymentRequestByIdLink(item.Id, this.Context.Request)"
|
||||
title="Copy Link">
|
||||
<vc:icon symbol="actions-copy" />
|
||||
</button>
|
||||
<div class="dropdown">
|
||||
<button class="btn btn-link dropdown-toggle p-0 dropdown-toggle-no-caret text-body" type="button" data-bs-toggle="dropdown" aria-expanded="false" id="ToggleActions-@item.Id">
|
||||
<vc:icon symbol="dots" />
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
@using BTCPayServer.Abstractions.TagHelpers
|
||||
@using BTCPayServer.Client.Models
|
||||
@using BTCPayServer.HostedServices.Webhooks
|
||||
@using BTCPayServer.Services
|
||||
@using BTCPayServer.TagHelpers
|
||||
@using Microsoft.AspNetCore.Mvc.TagHelpers
|
||||
@model BTCPayServer.Controllers.UIStoresController.StoreEmailRule
|
||||
@inject WebhookSender WebhookSender
|
||||
@@ -9,7 +12,6 @@
|
||||
var storeId = Context.GetStoreData().Id;
|
||||
bool isEdit = Model.Trigger != null;
|
||||
ViewData.SetActivePage(StoreNavPages.Emails, StringLocalizer[isEdit ? "Edit Email Rule" : "Create Email Rule"], storeId);
|
||||
var bitcoinWalletTransactions = CallbackGenerator.WalletTransactionsLink(new (storeId, "BTC"), this.Context.Request);
|
||||
}
|
||||
|
||||
@section PageHeadContent {
|
||||
@@ -94,6 +96,7 @@
|
||||
<code>{PaymentRequest.Currency}</code>,
|
||||
<code>{PaymentRequest.Title}</code>,
|
||||
<code>{PaymentRequest.Description}</code>,
|
||||
<code>{PaymentRequest.ReferenceId}</code>,
|
||||
<code>{PaymentRequest.Status}</code>
|
||||
<code>{PaymentRequest.FormResponse}*</code>
|
||||
</td>
|
||||
@@ -132,34 +135,41 @@
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const templates = {
|
||||
InvoiceCreated: {
|
||||
@WebhookEventType.InvoiceCreated: {
|
||||
subject: 'Invoice {Invoice.Id} created',
|
||||
body: 'Invoice {Invoice.Id} (Order Id: {Invoice.OrderId}) created.'
|
||||
},
|
||||
InvoiceReceivedPayment: {
|
||||
@WebhookEventType.InvoiceReceivedPayment: {
|
||||
subject: 'Invoice {Invoice.Id} received payment',
|
||||
body: 'Invoice {Invoice.Id} (Order Id: {Invoice.OrderId}) received payment.'
|
||||
},
|
||||
InvoiceProcessing: {
|
||||
@WebhookEventType.InvoiceProcessing: {
|
||||
subject: 'Invoice {Invoice.Id} processing',
|
||||
body: 'Invoice {Invoice.Id} (Order Id: {Invoice.OrderId}) is processing.'
|
||||
},
|
||||
InvoiceExpired: {
|
||||
@WebhookEventType.InvoiceExpired: {
|
||||
subject: 'Invoice {Invoice.Id} expired',
|
||||
body: 'Invoice {Invoice.Id} (Order Id: {Invoice.OrderId}) expired.'
|
||||
},
|
||||
InvoiceSettled: {
|
||||
@WebhookEventType.InvoiceSettled: {
|
||||
subject: 'Invoice {Invoice.Id} settled',
|
||||
body: 'Invoice {Invoice.Id} (Order Id: {Invoice.OrderId}) is settled.'
|
||||
},
|
||||
InvoiceInvalid: {
|
||||
@WebhookEventType.InvoiceInvalid: {
|
||||
subject: 'Invoice {Invoice.Id} invalid',
|
||||
body: 'Invoice {Invoice.Id} (Order Id: {Invoice.OrderId}) invalid.'
|
||||
},
|
||||
InvoicePaymentSettled: {
|
||||
@WebhookEventType.InvoicePaymentSettled: {
|
||||
subject: 'Invoice {Invoice.Id} payment settled',
|
||||
body: 'Invoice {Invoice.Id} (Order Id: {Invoice.OrderId}) payment settled.'
|
||||
},
|
||||
@{ var paymentRequestsLink = CallbackGenerator.PaymentRequestListLink(storeId, this.Context.Request); }
|
||||
@WebhookEventType.PaymentRequestCompleted: {
|
||||
subject: 'Payment Request {PaymentRequest.Title} {PaymentRequest.ReferenceId} Completed',
|
||||
body: 'The total of {PaymentRequest.Amount} {PaymentRequest.Currency} has been received and Payment Request {PaymentRequest.Id} is completed. ' +
|
||||
'\nReview the payment requests: ' + @Safe.Json(paymentRequestsLink)
|
||||
},
|
||||
@{ var bitcoinWalletTransactions = CallbackGenerator.WalletTransactionsLink(new (storeId, "BTC"), this.Context.Request); }
|
||||
@PendingTransactionWebhookProvider.PendingTransactionCreated : {
|
||||
subject: 'Pending Transaction {PendingTransaction.TrimmedId} Created',
|
||||
body: 'Review the transaction {PendingTransaction.Id} and sign it on: ' + @Safe.Json(bitcoinWalletTransactions)
|
||||
|
||||
@@ -461,6 +461,12 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"referenceId": {
|
||||
"type": "string",
|
||||
"description": "An optional user-defined identifier for this payment request.",
|
||||
"nullable": true,
|
||||
"example": "INV-123493"
|
||||
},
|
||||
"allowCustomPaymentAmounts": {
|
||||
"type": "boolean",
|
||||
"description": "Whether to allow users to create invoices that partially pay the payment request ",
|
||||
|
||||
Reference in New Issue
Block a user