Addressing PR review issues

This commit is contained in:
rockstardev
2025-04-03 23:00:58 -05:00
committed by nicolas.dorier
parent 586a952480
commit c3998fdf34
17 changed files with 112 additions and 49 deletions

View File

@@ -37,8 +37,10 @@ namespace BTCPayServer.Client.Models
public string Title { get; set; } public string Title { get; set; }
public string Description { get; set; } public string Description { get; set; }
public string Email { get; set; } public string Email { get; set; }
// Allow payment requests to be linked to invoices outside BTCPay Server using Reference Number /// <summary>
public string ReferenceNumber { get; set; } /// Linking to invoices outside BTCPay Server using & user defined ids
/// </summary>
public string ReferenceId { get; set; }
public bool AllowCustomPaymentAmounts { get; set; } public bool AllowCustomPaymentAmounts { get; set; }
[JsonConverter(typeof(StringEnumConverter))] [JsonConverter(typeof(StringEnumConverter))]

View File

@@ -1,4 +1,5 @@
using System; using System;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema; using System.ComponentModel.DataAnnotations.Schema;
using BTCPayServer.Client.Models; using BTCPayServer.Client.Models;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
@@ -16,6 +17,11 @@ namespace BTCPayServer.Data
public string Currency { get; set; } public string Currency { get; set; }
public decimal Amount { 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 StoreData StoreData { get; set; }
public Client.Models.PaymentRequestStatus Status { get; set; } public Client.Models.PaymentRequestStatus Status { get; set; }

View File

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

View File

@@ -1,4 +1,4 @@
// <auto-generated /> // <auto-generated />
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using BTCPayServer.Data; using BTCPayServer.Data;
@@ -544,6 +544,9 @@ namespace BTCPayServer.Migrations
b.Property<DateTimeOffset?>("Expiry") b.Property<DateTimeOffset?>("Expiry")
.HasColumnType("timestamp with time zone"); .HasColumnType("timestamp with time zone");
b.Property<string>("ReferenceId")
.HasColumnType("text");
b.Property<string>("Status") b.Property<string>("Status")
.IsRequired() .IsRequired()
.HasColumnType("text"); .HasColumnType("text");

View File

@@ -2048,7 +2048,7 @@ namespace BTCPayServer.Tests
new() { 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() { Title = "A", Currency = "USD", Amount = 1 }); new() { Title = "A", Currency = "USD", Amount = 1, ReferenceId = "1234"});
//list payment request //list payment request
var paymentRequests = await viewOnly.GetPaymentRequests(user.StoreId); var paymentRequests = await viewOnly.GetPaymentRequests(user.StoreId);
@@ -2061,10 +2061,12 @@ namespace BTCPayServer.Tests
var paymentRequest = await viewOnly.GetPaymentRequest(user.StoreId, newPaymentRequest.Id); var paymentRequest = await viewOnly.GetPaymentRequest(user.StoreId, newPaymentRequest.Id);
Assert.Equal(newPaymentRequest.Title, paymentRequest.Title); Assert.Equal(newPaymentRequest.Title, paymentRequest.Title);
Assert.Equal(newPaymentRequest.StoreId, user.StoreId); Assert.Equal(newPaymentRequest.StoreId, user.StoreId);
Assert.Equal(newPaymentRequest.ReferenceId, paymentRequest.ReferenceId);
//update payment request //update payment request
var updateRequest = paymentRequest; var updateRequest = paymentRequest;
updateRequest.Title = "B"; updateRequest.Title = "B";
updateRequest.ReferenceId = "EmperorNicolasGeneralRockstar";
await AssertHttpError(403, async () => await AssertHttpError(403, async () =>
{ {
await viewOnly.UpdatePaymentRequest(user.StoreId, paymentRequest.Id, updateRequest); await viewOnly.UpdatePaymentRequest(user.StoreId, paymentRequest.Id, updateRequest);
@@ -2072,6 +2074,7 @@ namespace BTCPayServer.Tests
await client.UpdatePaymentRequest(user.StoreId, paymentRequest.Id, updateRequest); await client.UpdatePaymentRequest(user.StoreId, paymentRequest.Id, updateRequest);
paymentRequest = await client.GetPaymentRequest(user.StoreId, newPaymentRequest.Id); paymentRequest = await client.GetPaymentRequest(user.StoreId, newPaymentRequest.Id);
Assert.Equal(updateRequest.Title, paymentRequest.Title); Assert.Equal(updateRequest.Title, paymentRequest.Title);
Assert.Equal(updateRequest.ReferenceId, paymentRequest.ReferenceId);
//archive payment request //archive payment request
await AssertHttpError(403, async () => await AssertHttpError(403, async () =>

View File

@@ -43,12 +43,20 @@ namespace BTCPayServer.Tests
Currency = "BTC", Currency = "BTC",
Amount = 1, Amount = 1,
StoreId = user.StoreId, StoreId = user.StoreId,
Description = "description" Description = "description",
ReferenceId = "custom-id-1"
}; };
var id = Assert var id = Assert
.IsType<RedirectToActionResult>(await paymentRequestController.EditPaymentRequest(null, request)) .IsType<RedirectToActionResult>(await paymentRequestController.EditPaymentRequest(null, request))
.RouteValues.Values.Last().ToString(); .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 }); paymentRequestController.HttpContext.SetPaymentRequestData(new PaymentRequestData { Id = id, StoreDataId = request.StoreId });
// Permission guard for guests editing // Permission guard for guests editing
@@ -56,8 +64,15 @@ namespace BTCPayServer.Tests
.IsType<NotFoundResult>(await guestpaymentRequestController.EditPaymentRequest(user.StoreId, id)); .IsType<NotFoundResult>(await guestpaymentRequestController.EditPaymentRequest(user.StoreId, id));
request.Title = "update"; request.Title = "update";
request.ReferenceId = "custom-id-2";
Assert.IsType<RedirectToActionResult>(await paymentRequestController.EditPaymentRequest(id, request)); 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.Equal(request.Title,
Assert.IsType<ViewPaymentRequestViewModel>(Assert Assert.IsType<ViewPaymentRequestViewModel>(Assert
.IsType<ViewResult>(await paymentRequestController.ViewPaymentRequest(id)).Model).Title); .IsType<ViewResult>(await paymentRequestController.ViewPaymentRequest(id)).Model).Title);

View File

@@ -205,7 +205,7 @@ namespace BTCPayServer.Controllers.Greenfield
pr = new PaymentRequestData() pr = new PaymentRequestData()
{ {
StoreDataId = storeId, StoreDataId = storeId,
Status = Client.Models.PaymentRequestStatus.Pending, Status = PaymentRequestStatus.Pending,
Created = DateTimeOffset.UtcNow, Created = DateTimeOffset.UtcNow,
Amount = request.Amount, Amount = request.Amount,
Currency = request.Currency ?? StoreData.GetStoreBlob().DefaultCurrency, 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) if (!ModelState.IsValid)
return this.CreateValidationError(ModelState); return this.CreateValidationError(ModelState);
var blob = pr.GetBlob(); var blob = pr.GetBlob();
pr.SetBlob(new() pr.SetBlob(new()
{ {
Title = request.Title,
AllowCustomPaymentAmounts = request.AllowCustomPaymentAmounts, AllowCustomPaymentAmounts = request.AllowCustomPaymentAmounts,
Description = request.Description, Description = request.Description,
Email = request.Email, Email = request.Email,
FormId = request.FormId, FormId = request.FormId,
Title = request.Title,
FormResponse = blob.FormId != request.FormId ? null : blob.FormResponse FormResponse = blob.FormId != request.FormId ? null : blob.FormResponse
}); });
pr = await _paymentRequestRepository.CreateOrUpdatePaymentRequest(pr); pr = await _paymentRequestRepository.CreateOrUpdatePaymentRequest(pr);
@@ -251,6 +253,7 @@ namespace BTCPayServer.Controllers.Greenfield
Title = blob.Title, Title = blob.Title,
ExpiryDate = data.Expiry, ExpiryDate = data.Expiry,
Email = blob.Email, Email = blob.Email,
ReferenceId = data.ReferenceId,
AllowCustomPaymentAmounts = blob.AllowCustomPaymentAmounts, AllowCustomPaymentAmounts = blob.AllowCustomPaymentAmounts,
FormResponse = blob.FormResponse, FormResponse = blob.FormResponse,
FormId = blob.FormId FormId = blob.FormId

View File

@@ -163,8 +163,8 @@ namespace BTCPayServer.Controllers
Checkout = { RedirectURL = redirectUrl }, Checkout = { RedirectURL = redirectUrl },
Receipt = new InvoiceDataBase.ReceiptOptions { Enabled = false } Receipt = new InvoiceDataBase.ReceiptOptions { Enabled = false }
}; };
if (prBlob.ReferenceNumber is not null or "") if (prData.ReferenceId is not null or "")
invoiceRequest.AdditionalSearchTerms = new[] { prBlob.ReferenceNumber }; invoiceRequest.AdditionalSearchTerms = [prData.ReferenceId];
var additionalTags = new List<string> { PaymentRequestRepository.GetInternalTag(id) }; var additionalTags = new List<string> { PaymentRequestRepository.GetInternalTag(id) };
return await CreateInvoiceCoreRaw(invoiceRequest, storeData, request.GetAbsoluteRoot(), additionalTags, cancellationToken); return await CreateInvoiceCoreRaw(invoiceRequest, storeData, request.GetAbsoluteRoot(), additionalTags, cancellationToken);
} }

View File

@@ -207,7 +207,7 @@ namespace BTCPayServer.Controllers
data.Amount = viewModel.Amount; data.Amount = viewModel.Amount;
data.Currency = viewModel.Currency ?? store.GetStoreBlob().DefaultCurrency; data.Currency = viewModel.Currency ?? store.GetStoreBlob().DefaultCurrency;
data.Expiry = viewModel.ExpiryDate?.ToUniversalTime(); data.Expiry = viewModel.ExpiryDate?.ToUniversalTime();
blob.ReferenceNumber = viewModel.ReferenceNumber; data.ReferenceId = viewModel.ReferenceId;
blob.AllowCustomPaymentAmounts = viewModel.AllowCustomPaymentAmounts; blob.AllowCustomPaymentAmounts = viewModel.AllowCustomPaymentAmounts;
blob.FormId = viewModel.FormId; blob.FormId = viewModel.FormId;

View File

@@ -47,7 +47,7 @@ public class PaymentRequestWebhookDeliveryRequest : WebhookSender.WebhookDeliver
.Replace("{PaymentRequest.Currency}", data.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.ReferenceNumber}", blob.ReferenceNumber) .Replace("{PaymentRequest.ReferenceId}", data.ReferenceId)
.Replace("{PaymentRequest.Status}", _evt.Data.Status.ToString()); .Replace("{PaymentRequest.Status}", _evt.Data.Status.ToString());
res = InterpolateJsonField(res, "PaymentRequest.FormResponse", blob.FormResponse); res = InterpolateJsonField(res, "PaymentRequest.FormResponse", blob.FormResponse);

View File

@@ -48,7 +48,7 @@ namespace BTCPayServer.Models.PaymentRequestViewModels
Description = blob.Description; Description = blob.Description;
ExpiryDate = data.Expiry?.UtcDateTime; ExpiryDate = data.Expiry?.UtcDateTime;
Email = blob.Email; Email = blob.Email;
ReferenceNumber = blob.ReferenceNumber; ReferenceId = data.ReferenceId;
AllowCustomPaymentAmounts = blob.AllowCustomPaymentAmounts; AllowCustomPaymentAmounts = blob.AllowCustomPaymentAmounts;
FormResponse = blob.FormResponse is null FormResponse = blob.FormResponse is null
? null ? null
@@ -85,10 +85,9 @@ namespace BTCPayServer.Models.PaymentRequestViewModels
[MailboxAddress] [MailboxAddress]
public string Email { get; set; } public string Email { get; set; }
[Display(Name = "Reference Number")] [Display(Name = "Reference Id")]
[MaxLength(50)] public string ReferenceId { get; set; }
public string ReferenceNumber { get; set; }
[Display(Name = "Allow payee to create invoices with custom amounts")] [Display(Name = "Allow payee to create invoices with custom amounts")]
public bool AllowCustomPaymentAmounts { get; set; } public bool AllowCustomPaymentAmounts { get; set; }
@@ -111,7 +110,7 @@ namespace BTCPayServer.Models.PaymentRequestViewModels
Description = blob.Description; Description = blob.Description;
ExpiryDate = data.Expiry?.UtcDateTime; ExpiryDate = data.Expiry?.UtcDateTime;
Email = blob.Email; Email = blob.Email;
ReferenceNumber = blob.ReferenceNumber; ReferenceId = data.ReferenceId;
AllowCustomPaymentAmounts = blob.AllowCustomPaymentAmounts; AllowCustomPaymentAmounts = blob.AllowCustomPaymentAmounts;
switch (data.Status) switch (data.Status)
{ {
@@ -133,7 +132,7 @@ namespace BTCPayServer.Models.PaymentRequestViewModels
} }
} }
public StoreBrandingViewModel StoreBranding { get; set; } public StoreBrandingViewModel StoreBranding { get; set; }
public string ReferenceNumber { get; set; } public string ReferenceId { get; set; }
public bool AllowCustomPaymentAmounts { get; set; } public bool AllowCustomPaymentAmounts { get; set; }
public string Email { get; set; } public string Email { get; set; }
public string Status { get; set; } public string Status { get; set; }

View File

@@ -92,7 +92,7 @@ namespace BTCPayServer.Services
pathBase: request.PathBase pathBase: request.PathBase
) ?? throw Bug(); ) ?? throw Bug();
} }
public string PaymentRequestByIdLink(string payReqId, HttpRequest request) public string PaymentRequestByIdLink(string payReqId, HttpRequest request)
{ {
return LinkGenerator.GetUriByAction( return LinkGenerator.GetUriByAction(
@@ -104,7 +104,7 @@ namespace BTCPayServer.Services
pathBase: request.PathBase pathBase: request.PathBase
) ?? throw Bug(); ) ?? throw Bug();
} }
public string PaymentRequestListLink(string storeId, HttpRequest request) public string PaymentRequestListLink(string storeId, HttpRequest request)
{ {
return LinkGenerator.GetUriByAction( return LinkGenerator.GetUriByAction(

View File

@@ -1,10 +1,9 @@
using System; using System;
using System.Linq; using System.Linq;
using System.Text.Json;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using BTCPayServer.Client.Models;
using BTCPayServer.Data; using BTCPayServer.Data;
using BTCPayServer.Lightning.Eclair;
using BTCPayServer.Services.Invoices; using BTCPayServer.Services.Invoices;
using BTCPayServer.Services.Stores; using BTCPayServer.Services.Stores;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
@@ -115,13 +114,13 @@ namespace BTCPayServer.Services.PaymentRequests
Type = PaymentRequestEvent.StatusChanged Type = PaymentRequestEvent.StatusChanged
}); });
if (status == Client.Models.PaymentRequestData.PaymentRequestStatus.Completed) if (status == PaymentRequestStatus.Completed)
{ {
_eventAggregator.Publish(new PaymentRequestEvent() _eventAggregator.Publish(new PaymentRequestEvent()
{ {
Data = paymentRequestData, Data = paymentRequestData,
Type = PaymentRequestEvent.Completed Type = PaymentRequestEvent.Completed
}); });
} }
} }
public async Task<PaymentRequestData[]> GetExpirablePaymentRequests(CancellationToken cancellationToken = default) public async Task<PaymentRequestData[]> GetExpirablePaymentRequests(CancellationToken cancellationToken = default)
@@ -138,30 +137,23 @@ namespace BTCPayServer.Services.PaymentRequests
public async Task<PaymentRequestData[]> FindPaymentRequests(PaymentRequestQuery query, CancellationToken cancellationToken = default) public async Task<PaymentRequestData[]> FindPaymentRequests(PaymentRequestQuery query, CancellationToken cancellationToken = default)
{ {
await using var context = _ContextFactory.CreateContext(); await using var context = _ContextFactory.CreateContext();
IQueryable<PaymentRequestData> queryable; 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.SearchText))
{ {
var searchText = query.SearchText; if (string.IsNullOrEmpty(query.StoreId))
throw new InvalidOperationException("PaymentRequestQuery.StoreId should be specified");
queryable = context.PaymentRequests.FromSqlInterpolated($@" // We are repeating the StoreId on purpose here, so Postgres can use the index
SELECT * FROM public.""PaymentRequests"" queryable = context.PaymentRequests.Where(p => (p.StoreDataId == query.StoreId && p.ReferenceId == query.SearchText) || p.Id == query.SearchText);
WHERE ""Blob2"" ->> 'referenceNumber' = {searchText}
OR ""Blob2"" ->> 'title' ILIKE {'%' + searchText + '%'}
");
}
else
{
queryable = context.PaymentRequests.AsQueryable();
} }
queryable = queryable.Include(data => data.StoreData); queryable = queryable.Include(data => data.StoreData);
if (!query.IncludeArchived) if (!query.IncludeArchived)
queryable = queryable.Where(data => !data.Archived); 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()) 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));
@@ -171,7 +163,7 @@ namespace BTCPayServer.Services.PaymentRequests
if (!string.IsNullOrEmpty(query.UserId)) if (!string.IsNullOrEmpty(query.UserId))
queryable = queryable.Where(data => queryable = queryable.Where(data =>
data.StoreData != null && data.StoreData.UserStores.Any(u => u.ApplicationUserId == query.UserId)); data.StoreData.UserStores.Any(u => u.ApplicationUserId == query.UserId));
queryable = queryable.OrderByDescending(u => u.Created); queryable = queryable.OrderByDescending(u => u.Created);
@@ -180,7 +172,7 @@ namespace BTCPayServer.Services.PaymentRequests
if (query.Count.HasValue) if (query.Count.HasValue)
queryable = queryable.Take(query.Count.Value); queryable = queryable.Take(query.Count.Value);
var items = await queryable.ToArrayAsync(cancellationToken); var items = await queryable.ToArrayAsync(cancellationToken);
return items; return items;
} }

View File

@@ -84,9 +84,9 @@
<span asp-validation-for="AllowCustomPaymentAmounts" class="text-danger"></span> <span asp-validation-for="AllowCustomPaymentAmounts" class="text-danger"></span>
</div> </div>
<div class="form-group"> <div class="form-group">
<label asp-for="ReferenceNumber" class="form-label"></label> <label asp-for="ReferenceId" class="form-label"></label>
<input asp-for="ReferenceNumber" class="form-control" /> <input asp-for="ReferenceId" class="form-control" />
<span asp-validation-for="ReferenceNumber" class="text-danger"></span> <span asp-validation-for="ReferenceId" class="text-danger"></span>
</div> </div>
<div class="form-group"> <div class="form-group">
<label asp-for="ExpiryDate" class="form-label"></label> <label asp-for="ExpiryDate" class="form-label"></label>

View File

@@ -57,7 +57,7 @@
<input type="hidden" asp-for="Count" /> <input type="hidden" asp-for="Count" />
<input type="hidden" asp-for="TimezoneOffset" /> <input type="hidden" asp-for="TimezoneOffset" />
<input asp-for="SearchTerm" type="hidden" value="@Model.Search.WithoutSearchText()"/> <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"> <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"> <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) @if (statusFilterCount > 0)
@@ -94,7 +94,7 @@
</button> </button>
</div> </div>
</th> </th>
<th text-translate="true">Number</th> <th text-translate="true">Reference Id</th>
<th text-translate="true">Status</th> <th text-translate="true">Status</th>
<th class="amount-col" text-translate="true">Amount</th> <th class="amount-col" text-translate="true">Amount</th>
<th></th> <th></th>
@@ -111,7 +111,7 @@
@(item.ExpiryDate?.ToBrowserDate() ?? new HtmlString("<span class=\"text-muted\">No Expiry</span>")) @(item.ExpiryDate?.ToBrowserDate() ?? new HtmlString("<span class=\"text-muted\">No Expiry</span>"))
</td> </td>
<td> <td>
@item.ReferenceNumber @item.ReferenceId
</td> </td>
<td> <td>
<span class="badge badge-@item.Status.ToLower() status-badge">@item.Status</span> <span class="badge badge-@item.Status.ToLower() status-badge">@item.Status</span>

View File

@@ -96,7 +96,7 @@
<code>{PaymentRequest.Currency}</code>, <code>{PaymentRequest.Currency}</code>,
<code>{PaymentRequest.Title}</code>, <code>{PaymentRequest.Title}</code>,
<code>{PaymentRequest.Description}</code>, <code>{PaymentRequest.Description}</code>,
<code>{PaymentRequest.ReferenceNumber}</code>, <code>{PaymentRequest.ReferenceId}</code>,
<code>{PaymentRequest.Status}</code> <code>{PaymentRequest.Status}</code>
<code>{PaymentRequest.FormResponse}*</code> <code>{PaymentRequest.FormResponse}*</code>
</td> </td>
@@ -165,7 +165,7 @@
}, },
@{ var paymentRequestsLink = CallbackGenerator.PaymentRequestListLink(storeId, this.Context.Request); } @{ var paymentRequestsLink = CallbackGenerator.PaymentRequestListLink(storeId, this.Context.Request); }
@WebhookEventType.PaymentRequestCompleted: { @WebhookEventType.PaymentRequestCompleted: {
subject: 'Payment Request {PaymentRequest.Title} {PaymentRequest.ReferenceNumber} Completed', 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. ' + 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) '\nReview the payment requests: ' + @Safe.Json(paymentRequestsLink)
}, },

View File

@@ -461,6 +461,12 @@
} }
] ]
}, },
"referenceId": {
"type": "string",
"description": "An optional user-defined identifier for this payment request.",
"nullable": true,
"example": "INV-123493"
},
"allowCustomPaymentAmounts": { "allowCustomPaymentAmounts": {
"type": "boolean", "type": "boolean",
"description": "Whether to allow users to create invoices that partially pay the payment request ", "description": "Whether to allow users to create invoices that partially pay the payment request ",