mirror of
https://github.com/aljazceru/btcpayserver.git
synced 2026-02-17 20:24:28 +01:00
Merge pull request #1760 from btcpayserver/api/invoice
GreenField: Invoice API
This commit is contained in:
71
BTCPayServer.Client/BTCPayServerClient.Invoices.cs
Normal file
71
BTCPayServer.Client/BTCPayServerClient.Invoices.cs
Normal file
@@ -0,0 +1,71 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Net.Http;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Client.Models;
|
||||
|
||||
namespace BTCPayServer.Client
|
||||
{
|
||||
public partial class BTCPayServerClient
|
||||
{
|
||||
public virtual async Task<IEnumerable<InvoiceData>> GetInvoices(string storeId, bool includeArchived = false,
|
||||
CancellationToken token = default)
|
||||
{
|
||||
var response =
|
||||
await _httpClient.SendAsync(
|
||||
CreateHttpRequest($"api/v1/stores/{storeId}/invoices",
|
||||
new Dictionary<string, object>() {{nameof(includeArchived), includeArchived}}), token);
|
||||
return await HandleResponse<IEnumerable<InvoiceData>>(response);
|
||||
}
|
||||
|
||||
public virtual async Task<InvoiceData> GetInvoice(string storeId, string invoiceId,
|
||||
CancellationToken token = default)
|
||||
{
|
||||
var response = await _httpClient.SendAsync(
|
||||
CreateHttpRequest($"api/v1/stores/{storeId}/invoices/{invoiceId}"), token);
|
||||
return await HandleResponse<InvoiceData>(response);
|
||||
}
|
||||
|
||||
public virtual async Task ArchiveInvoice(string storeId, string invoiceId,
|
||||
CancellationToken token = default)
|
||||
{
|
||||
var response = await _httpClient.SendAsync(
|
||||
CreateHttpRequest($"api/v1/stores/{storeId}/invoices/{invoiceId}",
|
||||
method: HttpMethod.Delete), token);
|
||||
await HandleResponse(response);
|
||||
}
|
||||
|
||||
public virtual async Task<InvoiceData> CreateInvoice(string storeId,
|
||||
CreateInvoiceRequest request, CancellationToken token = default)
|
||||
{
|
||||
if (request == null)
|
||||
throw new ArgumentNullException(nameof(request));
|
||||
var response = await _httpClient.SendAsync(
|
||||
CreateHttpRequest($"api/v1/stores/{storeId}/invoices", bodyPayload: request,
|
||||
method: HttpMethod.Post), token);
|
||||
return await HandleResponse<InvoiceData>(response);
|
||||
}
|
||||
|
||||
public virtual async Task<InvoiceData> MarkInvoiceStatus(string storeId, string invoiceId,
|
||||
MarkInvoiceStatusRequest request, CancellationToken token = default)
|
||||
{
|
||||
if (request == null)
|
||||
throw new ArgumentNullException(nameof(request));
|
||||
if (request.Status!= InvoiceStatus.Complete && request.Status!= InvoiceStatus.Invalid)
|
||||
throw new ArgumentOutOfRangeException(nameof(request.Status), "Status can only be Invalid or Complete");
|
||||
var response = await _httpClient.SendAsync(
|
||||
CreateHttpRequest($"api/v1/stores/{storeId}/invoices/{invoiceId}/status", bodyPayload: request,
|
||||
method: HttpMethod.Post), token);
|
||||
return await HandleResponse<InvoiceData>(response);
|
||||
}
|
||||
|
||||
public virtual async Task<InvoiceData> UnarchiveInvoice(string storeId, string invoiceId, CancellationToken token = default)
|
||||
{
|
||||
var response = await _httpClient.SendAsync(
|
||||
CreateHttpRequest($"api/v1/stores/{storeId}/invoices/{invoiceId}/unarchive",
|
||||
method: HttpMethod.Post), token);
|
||||
return await HandleResponse<InvoiceData>(response);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -10,10 +10,13 @@ namespace BTCPayServer.Client
|
||||
public partial class BTCPayServerClient
|
||||
{
|
||||
public virtual async Task<IEnumerable<PaymentRequestData>> GetPaymentRequests(string storeId,
|
||||
bool includeArchived = false,
|
||||
CancellationToken token = default)
|
||||
{
|
||||
var response =
|
||||
await _httpClient.SendAsync(CreateHttpRequest($"api/v1/stores/{storeId}/payment-requests"), token);
|
||||
await _httpClient.SendAsync(
|
||||
CreateHttpRequest($"api/v1/stores/{storeId}/payment-requests",
|
||||
new Dictionary<string, object>() {{nameof(includeArchived), includeArchived}}), token);
|
||||
return await HandleResponse<IEnumerable<PaymentRequestData>>(response);
|
||||
}
|
||||
|
||||
|
||||
@@ -4,13 +4,38 @@ using Newtonsoft.Json;
|
||||
|
||||
namespace BTCPayServer.Client.JsonConverters
|
||||
{
|
||||
public class TimeSpanJsonConverter : JsonConverter
|
||||
public abstract class TimeSpanJsonConverter : JsonConverter
|
||||
{
|
||||
public class Seconds : TimeSpanJsonConverter
|
||||
{
|
||||
protected override long ToLong(TimeSpan value)
|
||||
{
|
||||
return (long)value.TotalSeconds;
|
||||
}
|
||||
|
||||
protected override TimeSpan ToTimespan(long value)
|
||||
{
|
||||
return TimeSpan.FromSeconds(value);
|
||||
}
|
||||
}
|
||||
public class Minutes : TimeSpanJsonConverter
|
||||
{
|
||||
protected override long ToLong(TimeSpan value)
|
||||
{
|
||||
return (long)value.TotalMinutes;
|
||||
}
|
||||
protected override TimeSpan ToTimespan(long value)
|
||||
{
|
||||
return TimeSpan.FromMinutes(value);
|
||||
}
|
||||
}
|
||||
public override bool CanConvert(Type objectType)
|
||||
{
|
||||
return objectType == typeof(TimeSpan) || objectType == typeof(TimeSpan?);
|
||||
}
|
||||
|
||||
protected abstract TimeSpan ToTimespan(long value);
|
||||
protected abstract long ToLong(TimeSpan value);
|
||||
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
|
||||
{
|
||||
try
|
||||
@@ -24,11 +49,11 @@ namespace BTCPayServer.Client.JsonConverters
|
||||
}
|
||||
if (reader.TokenType != JsonToken.Integer)
|
||||
throw new JsonObjectException("Invalid timespan, expected integer", reader);
|
||||
return TimeSpan.FromSeconds((long)reader.Value);
|
||||
return ToTimespan((long)reader.Value);
|
||||
}
|
||||
catch
|
||||
{
|
||||
throw new JsonObjectException("Invalid locktime", reader);
|
||||
throw new JsonObjectException("Invalid timespan", reader);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,7 +61,7 @@ namespace BTCPayServer.Client.JsonConverters
|
||||
{
|
||||
if (value is TimeSpan s)
|
||||
{
|
||||
writer.WriteValue((long)s.TotalSeconds);
|
||||
writer.WriteValue(ToLong(s));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
7
BTCPayServer.Client/Models/AddCustomerEmailRequest.cs
Normal file
7
BTCPayServer.Client/Models/AddCustomerEmailRequest.cs
Normal file
@@ -0,0 +1,7 @@
|
||||
namespace BTCPayServer.Client.Models
|
||||
{
|
||||
public class AddCustomerEmailRequest
|
||||
{
|
||||
public string Email { get; set; }
|
||||
}
|
||||
}
|
||||
35
BTCPayServer.Client/Models/CreateInvoiceRequest.cs
Normal file
35
BTCPayServer.Client/Models/CreateInvoiceRequest.cs
Normal file
@@ -0,0 +1,35 @@
|
||||
using System;
|
||||
using BTCPayServer.Client.JsonConverters;
|
||||
using BTCPayServer.JsonConverters;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Converters;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace BTCPayServer.Client.Models
|
||||
{
|
||||
public class CreateInvoiceRequest
|
||||
{
|
||||
[JsonProperty(ItemConverterType = typeof(NumericStringJsonConverter))]
|
||||
public decimal Amount { get; set; }
|
||||
public string Currency { get; set; }
|
||||
public JObject Metadata { get; set; }
|
||||
public CheckoutOptions Checkout { get; set; } = new CheckoutOptions();
|
||||
|
||||
public class CheckoutOptions
|
||||
{
|
||||
[JsonConverter(typeof(StringEnumConverter))]
|
||||
public SpeedPolicy? SpeedPolicy { get; set; }
|
||||
|
||||
public string[] PaymentMethods { get; set; }
|
||||
|
||||
[JsonConverter(typeof(TimeSpanJsonConverter.Minutes))]
|
||||
[JsonProperty("expirationMinutes")]
|
||||
public TimeSpan? Expiration { get; set; }
|
||||
[JsonConverter(typeof(TimeSpanJsonConverter.Minutes))]
|
||||
[JsonProperty("monitoringMinutes")]
|
||||
public TimeSpan? Monitoring { get; set; }
|
||||
|
||||
public double? PaymentTolerance { get; set; }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -20,7 +20,7 @@ namespace BTCPayServer.Client.Models
|
||||
[JsonConverter(typeof(LightMoneyJsonConverter))]
|
||||
public LightMoney Amount { get; set; }
|
||||
public string Description { get; set; }
|
||||
[JsonConverter(typeof(JsonConverters.TimeSpanJsonConverter))]
|
||||
[JsonConverter(typeof(JsonConverters.TimeSpanJsonConverter.Seconds))]
|
||||
public TimeSpan Expiry { get; set; }
|
||||
public bool PrivateRouteHints { get; set; }
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ namespace BTCPayServer.Client.Models
|
||||
[JsonProperty(ItemConverterType = typeof(NumericStringJsonConverter))]
|
||||
public decimal Amount { get; set; }
|
||||
public string Currency { get; set; }
|
||||
[JsonConverter(typeof(TimeSpanJsonConverter))]
|
||||
[JsonConverter(typeof(TimeSpanJsonConverter.Seconds))]
|
||||
public TimeSpan? Period { get; set; }
|
||||
[JsonConverter(typeof(NBitcoin.JsonConverters.DateTimeToUnixTimeConverter))]
|
||||
public DateTimeOffset? ExpiresAt { get; set; }
|
||||
|
||||
23
BTCPayServer.Client/Models/InvoiceData.cs
Normal file
23
BTCPayServer.Client/Models/InvoiceData.cs
Normal file
@@ -0,0 +1,23 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using BTCPayServer.JsonConverters;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Converters;
|
||||
|
||||
namespace BTCPayServer.Client.Models
|
||||
{
|
||||
public class InvoiceData : CreateInvoiceRequest
|
||||
{
|
||||
public string Id { get; set; }
|
||||
[JsonConverter(typeof(StringEnumConverter))]
|
||||
public InvoiceStatus Status { get; set; }
|
||||
[JsonConverter(typeof(StringEnumConverter))]
|
||||
public InvoiceExceptionStatus AdditionalStatus { get; set; }
|
||||
[JsonConverter(typeof(NBitcoin.JsonConverters.DateTimeToUnixTimeConverter))]
|
||||
public DateTimeOffset MonitoringExpiration { get; set; }
|
||||
[JsonConverter(typeof(NBitcoin.JsonConverters.DateTimeToUnixTimeConverter))]
|
||||
public DateTimeOffset ExpirationTime { get; set; }
|
||||
[JsonConverter(typeof(NBitcoin.JsonConverters.DateTimeToUnixTimeConverter))]
|
||||
public DateTimeOffset CreatedTime { get; set; }
|
||||
}
|
||||
}
|
||||
12
BTCPayServer.Client/Models/InvoiceExceptionStatus.cs
Normal file
12
BTCPayServer.Client/Models/InvoiceExceptionStatus.cs
Normal file
@@ -0,0 +1,12 @@
|
||||
namespace BTCPayServer.Client.Models
|
||||
{
|
||||
public enum InvoiceExceptionStatus
|
||||
{
|
||||
None,
|
||||
PaidLate,
|
||||
PaidPartial,
|
||||
Marked,
|
||||
Invalid,
|
||||
PaidOver
|
||||
}
|
||||
}
|
||||
12
BTCPayServer.Client/Models/InvoiceStatus.cs
Normal file
12
BTCPayServer.Client/Models/InvoiceStatus.cs
Normal file
@@ -0,0 +1,12 @@
|
||||
namespace BTCPayServer.Client.Models
|
||||
{
|
||||
public enum InvoiceStatus
|
||||
{
|
||||
New,
|
||||
Paid,
|
||||
Expired,
|
||||
Invalid,
|
||||
Complete,
|
||||
Confirmed
|
||||
}
|
||||
}
|
||||
11
BTCPayServer.Client/Models/MarkInvoiceStatusRequest.cs
Normal file
11
BTCPayServer.Client/Models/MarkInvoiceStatusRequest.cs
Normal file
@@ -0,0 +1,11 @@
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Converters;
|
||||
|
||||
namespace BTCPayServer.Client.Models
|
||||
{
|
||||
public class MarkInvoiceStatusRequest
|
||||
{
|
||||
[JsonConverter(typeof(StringEnumConverter))]
|
||||
public InvoiceStatus Status { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -16,7 +16,7 @@ namespace BTCPayServer.Client.Models
|
||||
public string Currency { get; set; }
|
||||
[JsonConverter(typeof(NumericStringJsonConverter))]
|
||||
public decimal Amount { get; set; }
|
||||
[JsonConverter(typeof(TimeSpanJsonConverter))]
|
||||
[JsonConverter(typeof(TimeSpanJsonConverter.Seconds))]
|
||||
public TimeSpan? Period { get; set; }
|
||||
public bool Archived { get; set; }
|
||||
public string ViewLink { get; set; }
|
||||
|
||||
@@ -16,11 +16,11 @@ namespace BTCPayServer.Client.Models
|
||||
|
||||
public string Website { get; set; }
|
||||
|
||||
[JsonConverter(typeof(TimeSpanJsonConverter))]
|
||||
[JsonConverter(typeof(TimeSpanJsonConverter.Seconds))]
|
||||
[JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
|
||||
public TimeSpan InvoiceExpiration { get; set; } = TimeSpan.FromMinutes(15);
|
||||
|
||||
[JsonConverter(typeof(TimeSpanJsonConverter))]
|
||||
[JsonConverter(typeof(TimeSpanJsonConverter.Seconds))]
|
||||
[JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
|
||||
public TimeSpan MonitoringExpiration { get; set; } = TimeSpan.FromMinutes(60);
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ namespace BTCPayServer.Client
|
||||
public const string CanModifyStoreSettings = "btcpay.store.canmodifystoresettings";
|
||||
public const string CanModifyStoreSettingsUnscoped = "btcpay.store.canmodifystoresettings:";
|
||||
public const string CanViewStoreSettings = "btcpay.store.canviewstoresettings";
|
||||
public const string CanViewInvoices = "btcpay.store.canviewinvoices";
|
||||
public const string CanCreateInvoice = "btcpay.store.cancreateinvoice";
|
||||
public const string CanViewPaymentRequests = "btcpay.store.canviewpaymentrequests";
|
||||
public const string CanModifyPaymentRequests = "btcpay.store.canmodifypaymentrequests";
|
||||
@@ -26,6 +27,7 @@ namespace BTCPayServer.Client
|
||||
{
|
||||
get
|
||||
{
|
||||
yield return CanViewInvoices;
|
||||
yield return CanCreateInvoice;
|
||||
yield return CanModifyServerSettings;
|
||||
yield return CanModifyStoreSettings;
|
||||
@@ -153,6 +155,8 @@ namespace BTCPayServer.Client
|
||||
return true;
|
||||
switch (subpolicy)
|
||||
{
|
||||
case Policies.CanViewInvoices when this.Policy == Policies.CanModifyStoreSettings:
|
||||
case Policies.CanViewInvoices when this.Policy == Policies.CanViewStoreSettings:
|
||||
case Policies.CanViewStoreSettings when this.Policy == Policies.CanModifyStoreSettings:
|
||||
case Policies.CanCreateInvoice when this.Policy == Policies.CanModifyStoreSettings:
|
||||
case Policies.CanViewProfile when this.Policy == Policies.CanModifyProfile:
|
||||
|
||||
@@ -877,7 +877,7 @@ normal:
|
||||
InvoiceEntity invoiceEntity = new InvoiceEntity();
|
||||
invoiceEntity.Networks = networkProvider;
|
||||
invoiceEntity.Payments = new System.Collections.Generic.List<PaymentEntity>();
|
||||
invoiceEntity.ProductInformation = new ProductInformation() { Price = 100 };
|
||||
invoiceEntity.Price = 100;
|
||||
PaymentMethodDictionary paymentMethods = new PaymentMethodDictionary();
|
||||
paymentMethods.Add(new PaymentMethod() { Network = networkBTC, CryptoCode = "BTC", Rate = 10513.44m, }
|
||||
.SetPaymentMethodDetails(
|
||||
|
||||
@@ -89,7 +89,7 @@ namespace BTCPayServer.Tests
|
||||
s.Driver.FindElement(By.Id("AddApiKey")).Click();
|
||||
s.Driver.FindElement(By.CssSelector("button[value='btcpay.store.canmodifystoresettings:change-store-mode']")).Click();
|
||||
//there should be a store already by default in the dropdown
|
||||
var dropdown = s.Driver.FindElement(By.Name("PermissionValues[2].SpecificStores[0]"));
|
||||
var dropdown = s.Driver.FindElement(By.Name("PermissionValues[3].SpecificStores[0]"));
|
||||
var option = dropdown.FindElement(By.TagName("option"));
|
||||
var storeId = option.GetAttribute("value");
|
||||
option.Click();
|
||||
|
||||
@@ -17,9 +17,6 @@
|
||||
<PropertyGroup Condition="'$(CI_TESTS)' == 'true'">
|
||||
<DefineConstants>$(DefineConstants);SHORT_TIMEOUT</DefineConstants>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup>
|
||||
<DefineConstants>$(DefineConstants);ALTCOINS</DefineConstants>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.6.1" />
|
||||
|
||||
@@ -10,6 +10,7 @@ using BTCPayServer.Controllers;
|
||||
using BTCPayServer.Events;
|
||||
using BTCPayServer.JsonConverters;
|
||||
using BTCPayServer.Services;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
using BTCPayServer.Tests.Logging;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
@@ -17,6 +18,7 @@ using NBitcoin;
|
||||
using NBitpayClient;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using NUglify.Helpers;
|
||||
using Xunit;
|
||||
using Xunit.Abstractions;
|
||||
using CreateApplicationUserRequest = BTCPayServer.Client.Models.CreateApplicationUserRequest;
|
||||
@@ -732,6 +734,147 @@ namespace BTCPayServer.Tests
|
||||
}
|
||||
}
|
||||
|
||||
[Fact(Timeout = TestTimeout)]
|
||||
[Trait("Integration", "Integration")]
|
||||
public async Task InvoiceLegacyTests()
|
||||
{
|
||||
using (var tester = ServerTester.Create())
|
||||
{
|
||||
await tester.StartAsync();
|
||||
var user = tester.NewAccount();
|
||||
await user.GrantAccessAsync();
|
||||
user.RegisterDerivationScheme("BTC");
|
||||
var client = await user.CreateClient(Policies.Unrestricted);
|
||||
var oldBitpay = user.BitPay;
|
||||
|
||||
Logs.Tester.LogInformation("Let's create an invoice with bitpay API");
|
||||
var oldInvoice = await oldBitpay.CreateInvoiceAsync(new Invoice()
|
||||
{
|
||||
Currency = "BTC",
|
||||
Price = 1000.19392922m,
|
||||
BuyerAddress1 = "blah",
|
||||
Buyer = new Buyer()
|
||||
{
|
||||
Address2 = "blah2"
|
||||
},
|
||||
ItemCode = "code",
|
||||
ItemDesc = "desc",
|
||||
OrderId = "orderId",
|
||||
PosData = "posData"
|
||||
});
|
||||
|
||||
async Task<Client.Models.InvoiceData> AssertInvoiceMetadata()
|
||||
{
|
||||
Logs.Tester.LogInformation("Let's check if we can get invoice in the new format with the metadata");
|
||||
var newInvoice = await client.GetInvoice(user.StoreId, oldInvoice.Id);
|
||||
Assert.Equal("posData", newInvoice.Metadata["posData"].Value<string>());
|
||||
Assert.Equal("code", newInvoice.Metadata["itemCode"].Value<string>());
|
||||
Assert.Equal("desc", newInvoice.Metadata["itemDesc"].Value<string>());
|
||||
Assert.Equal("orderId", newInvoice.Metadata["orderId"].Value<string>());
|
||||
Assert.False(newInvoice.Metadata["physical"].Value<bool>());
|
||||
Assert.Null(newInvoice.Metadata["buyerCountry"]);
|
||||
Assert.Equal(1000.19392922m, newInvoice.Amount);
|
||||
Assert.Equal("BTC", newInvoice.Currency);
|
||||
return newInvoice;
|
||||
}
|
||||
|
||||
await AssertInvoiceMetadata();
|
||||
|
||||
Logs.Tester.LogInformation("Let's hack the Bitpay created invoice to be just like before this update. (Invoice V1)");
|
||||
var invoiceV1 = "{\r\n \"version\": 1,\r\n \"id\": \"" + oldInvoice.Id + "\",\r\n \"storeId\": \"" + user.StoreId + "\",\r\n \"orderId\": \"orderId\",\r\n \"speedPolicy\": 1,\r\n \"rate\": 1.0,\r\n \"invoiceTime\": 1598329634,\r\n \"expirationTime\": 1598330534,\r\n \"depositAddress\": \"mm83rVs8ZnZok1SkRBmXiwQSiPFgTgCKpD\",\r\n \"productInformation\": {\r\n \"itemDesc\": \"desc\",\r\n \"itemCode\": \"code\",\r\n \"physical\": false,\r\n \"price\": 1000.19392922,\r\n \"currency\": \"BTC\"\r\n },\r\n \"buyerInformation\": {\r\n \"buyerName\": null,\r\n \"buyerEmail\": null,\r\n \"buyerCountry\": null,\r\n \"buyerZip\": null,\r\n \"buyerState\": null,\r\n \"buyerCity\": null,\r\n \"buyerAddress2\": \"blah2\",\r\n \"buyerAddress1\": \"blah\",\r\n \"buyerPhone\": null\r\n },\r\n \"posData\": \"posData\",\r\n \"internalTags\": [],\r\n \"derivationStrategy\": null,\r\n \"derivationStrategies\": \"{\\\"BTC\\\":{\\\"signingKey\\\":\\\"tpubDD1AW2ruUxSsDa55NQYtNt7DQw9bqXx4K7r2aScySmjxHtsCZoxFTN3qCMcKLxgsRDMGSwk9qj1fBfi8jqSLenwyYkhDrmgaxQuvuKrTHEf\\\",\\\"source\\\":\\\"NBXplorer\\\",\\\"accountDerivation\\\":\\\"tpubDD1AW2ruUxSsDa55NQYtNt7DQw9bqXx4K7r2aScySmjxHtsCZoxFTN3qCMcKLxgsRDMGSwk9qj1fBfi8jqSLenwyYkhDrmgaxQuvuKrTHEf-[legacy]\\\",\\\"accountOriginal\\\":null,\\\"accountKeySettings\\\":[{\\\"rootFingerprint\\\":\\\"54d5044d\\\",\\\"accountKeyPath\\\":\\\"44'/1'/0'\\\",\\\"accountKey\\\":\\\"tpubDD1AW2ruUxSsDa55NQYtNt7DQw9bqXx4K7r2aScySmjxHtsCZoxFTN3qCMcKLxgsRDMGSwk9qj1fBfi8jqSLenwyYkhDrmgaxQuvuKrTHEf\\\"}],\\\"label\\\":null}}\",\r\n \"status\": \"new\",\r\n \"exceptionStatus\": \"\",\r\n \"payments\": [],\r\n \"refundable\": false,\r\n \"refundMail\": null,\r\n \"redirectURL\": null,\r\n \"redirectAutomatically\": false,\r\n \"txFee\": 0,\r\n \"fullNotifications\": false,\r\n \"notificationEmail\": null,\r\n \"notificationURL\": null,\r\n \"serverUrl\": \"http://127.0.0.1:8001\",\r\n \"cryptoData\": {\r\n \"BTC\": {\r\n \"rate\": 1.0,\r\n \"paymentMethod\": {\r\n \"networkFeeMode\": 0,\r\n \"networkFeeRate\": 100.0,\r\n \"payjoinEnabled\": false\r\n },\r\n \"feeRate\": 100.0,\r\n \"txFee\": 0,\r\n \"depositAddress\": \"mm83rVs8ZnZok1SkRBmXiwQSiPFgTgCKpD\"\r\n }\r\n },\r\n \"monitoringExpiration\": 1598416934,\r\n \"historicalAddresses\": null,\r\n \"availableAddressHashes\": null,\r\n \"extendedNotifications\": false,\r\n \"events\": null,\r\n \"paymentTolerance\": 0.0,\r\n \"archived\": false\r\n}";
|
||||
var db = tester.PayTester.GetService<Data.ApplicationDbContextFactory>();
|
||||
using var ctx = db.CreateContext();
|
||||
var dbInvoice = await ctx.Invoices.FindAsync(oldInvoice.Id);
|
||||
dbInvoice.Blob = ZipUtils.Zip(invoiceV1);
|
||||
await ctx.SaveChangesAsync();
|
||||
var newInvoice = await AssertInvoiceMetadata();
|
||||
|
||||
Logs.Tester.LogInformation("Now, let's create an invoice with the new API but with the same metadata as Bitpay");
|
||||
newInvoice.Metadata.Add("lol", "lol");
|
||||
newInvoice = await client.CreateInvoice(user.StoreId, new CreateInvoiceRequest()
|
||||
{
|
||||
Metadata = newInvoice.Metadata,
|
||||
Amount = 1000.19392922m,
|
||||
Currency = "BTC"
|
||||
});
|
||||
oldInvoice = await oldBitpay.GetInvoiceAsync(newInvoice.Id);
|
||||
await AssertInvoiceMetadata();
|
||||
Assert.Equal("lol", newInvoice.Metadata["lol"].Value<string>());
|
||||
}
|
||||
}
|
||||
|
||||
[Fact(Timeout = TestTimeout)]
|
||||
[Trait("Integration", "Integration")]
|
||||
public async Task InvoiceTests()
|
||||
{
|
||||
using (var tester = ServerTester.Create())
|
||||
{
|
||||
await tester.StartAsync();
|
||||
var user = tester.NewAccount();
|
||||
await user.GrantAccessAsync();
|
||||
await user.MakeAdmin();
|
||||
var client = await user.CreateClient(Policies.Unrestricted);
|
||||
var viewOnly = await user.CreateClient(Policies.CanViewInvoices);
|
||||
|
||||
//create
|
||||
|
||||
//validation errors
|
||||
await AssertValidationError(new[] { nameof(CreateInvoiceRequest.Currency), nameof(CreateInvoiceRequest.Amount), $"{nameof(CreateInvoiceRequest.Checkout)}.{nameof(CreateInvoiceRequest.Checkout.PaymentTolerance)}", $"{nameof(CreateInvoiceRequest.Checkout)}.{nameof(CreateInvoiceRequest.Checkout.PaymentMethods)}[0]" }, async () =>
|
||||
{
|
||||
await client.CreateInvoice(user.StoreId, new CreateInvoiceRequest() { Amount = -1, Checkout = new CreateInvoiceRequest.CheckoutOptions() { PaymentTolerance = -2, PaymentMethods = new[] { "jasaas_sdsad" } } });
|
||||
});
|
||||
|
||||
await AssertHttpError(403, async () =>
|
||||
{
|
||||
await viewOnly.CreateInvoice(user.StoreId,
|
||||
new CreateInvoiceRequest() { Currency = "helloinvalid", Amount = 1 });
|
||||
});
|
||||
await user.RegisterDerivationSchemeAsync("BTC");
|
||||
var newInvoice = await client.CreateInvoice(user.StoreId,
|
||||
new CreateInvoiceRequest() { Currency = "USD", Amount = 1, Metadata = JObject.Parse("{\"itemCode\": \"testitem\"}") });
|
||||
|
||||
//list
|
||||
var invoices = await viewOnly.GetInvoices(user.StoreId);
|
||||
|
||||
Assert.NotNull(invoices);
|
||||
Assert.Single(invoices);
|
||||
Assert.Equal(newInvoice.Id, invoices.First().Id);
|
||||
|
||||
//get payment request
|
||||
var invoice = await viewOnly.GetInvoice(user.StoreId, newInvoice.Id);
|
||||
Assert.Equal(newInvoice.Metadata, invoice.Metadata);
|
||||
|
||||
//update
|
||||
invoice = await viewOnly.GetInvoice(user.StoreId, newInvoice.Id);
|
||||
|
||||
await AssertValidationError(new[] { nameof(MarkInvoiceStatusRequest.Status) }, async () =>
|
||||
{
|
||||
await client.MarkInvoiceStatus(user.StoreId, invoice.Id, new MarkInvoiceStatusRequest()
|
||||
{
|
||||
Status = InvoiceStatus.Complete
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
//archive
|
||||
await AssertHttpError(403, async () =>
|
||||
{
|
||||
await viewOnly.ArchiveInvoice(user.StoreId, invoice.Id);
|
||||
});
|
||||
|
||||
await client.ArchiveInvoice(user.StoreId, invoice.Id);
|
||||
Assert.DoesNotContain(invoice.Id,
|
||||
(await client.GetInvoices(user.StoreId)).Select(data => data.Id));
|
||||
|
||||
//unarchive
|
||||
await client.UnarchiveInvoice(user.StoreId, invoice.Id);
|
||||
Assert.NotNull(await client.GetInvoice(user.StoreId, invoice.Id));
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
[Fact(Timeout = TestTimeout)]
|
||||
[Trait("Fast", "Fast")]
|
||||
public void NumericJsonConverterTests()
|
||||
@@ -762,10 +905,11 @@ namespace BTCPayServer.Tests
|
||||
Assert.Throws<JsonSerializationException>(() =>
|
||||
{
|
||||
jsonConverter.ReadJson(Get("null"), typeof(decimal), null, null);
|
||||
});Assert.Throws<JsonSerializationException>(() =>
|
||||
{
|
||||
jsonConverter.ReadJson(Get("null"), typeof(double), null, null);
|
||||
});
|
||||
Assert.Throws<JsonSerializationException>(() =>
|
||||
{
|
||||
jsonConverter.ReadJson(Get("null"), typeof(double), null, null);
|
||||
});
|
||||
Assert.Equal(1.2m, jsonConverter.ReadJson(Get(stringJson), typeof(decimal), null, null));
|
||||
Assert.Equal(1.2m, jsonConverter.ReadJson(Get(stringJson), typeof(decimal?), null, null));
|
||||
Assert.Equal(1.2, jsonConverter.ReadJson(Get(stringJson), typeof(double), null, null));
|
||||
|
||||
@@ -3,6 +3,7 @@ using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Client.Models;
|
||||
using BTCPayServer.Controllers;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Events;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Client.Models;
|
||||
using BTCPayServer.Controllers;
|
||||
using BTCPayServer.Models.PaymentRequestViewModels;
|
||||
using BTCPayServer.PaymentRequest;
|
||||
|
||||
@@ -151,6 +151,36 @@ namespace BTCPayServer.Tests
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Fast", "Fast")]
|
||||
public void CanParsePaymentMethodId()
|
||||
{
|
||||
var id = PaymentMethodId.Parse("BTC");
|
||||
var id1 = PaymentMethodId.Parse("BTC-OnChain");
|
||||
var id2 = PaymentMethodId.Parse("BTC-BTCLike");
|
||||
Assert.Equal(id, id1);
|
||||
Assert.Equal(id, id2);
|
||||
Assert.Equal("BTC", id.ToString());
|
||||
Assert.Equal("BTC", id.ToString());
|
||||
id = PaymentMethodId.Parse("LTC");
|
||||
Assert.Equal("LTC", id.ToString());
|
||||
Assert.Equal("LTC", id.ToStringNormalized());
|
||||
id = PaymentMethodId.Parse("LTC-offchain");
|
||||
id1 = PaymentMethodId.Parse("LTC-OffChain");
|
||||
id2 = PaymentMethodId.Parse("LTC-LightningLike");
|
||||
Assert.Equal(id, id1);
|
||||
Assert.Equal(id, id2);
|
||||
Assert.Equal("LTC_LightningLike", id.ToString());
|
||||
Assert.Equal("LTC-LightningNetwork", id.ToStringNormalized());
|
||||
#if ALTCOINS
|
||||
id = PaymentMethodId.Parse("XMR");
|
||||
id1 = PaymentMethodId.Parse("XMR-MoneroLike");
|
||||
Assert.Equal(id, id1);
|
||||
Assert.Equal("XMR_MoneroLike", id.ToString());
|
||||
Assert.Equal("XMR", id.ToStringNormalized());
|
||||
#endif
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Fast", "Fast")]
|
||||
public async Task CheckNoDeadLink()
|
||||
@@ -325,7 +355,7 @@ namespace BTCPayServer.Tests
|
||||
Assert.True(Torrc.TryParse(input, out torrc));
|
||||
Assert.Equal(expected, torrc.ToString());
|
||||
}
|
||||
|
||||
#if ALTCOINS
|
||||
[Fact]
|
||||
[Trait("Fast", "Fast")]
|
||||
public void CanCalculateCryptoDue()
|
||||
@@ -346,7 +376,7 @@ namespace BTCPayServer.Tests
|
||||
Rate = 5000,
|
||||
NextNetworkFee = Money.Coins(0.1m)
|
||||
});
|
||||
entity.ProductInformation = new ProductInformation() { Price = 5000 };
|
||||
entity.Price = 5000;
|
||||
|
||||
var paymentMethod = entity.GetPaymentMethods().TryGet("BTC", PaymentTypes.BTCLike);
|
||||
var accounting = paymentMethod.Calculate();
|
||||
@@ -396,7 +426,7 @@ namespace BTCPayServer.Tests
|
||||
|
||||
entity = new InvoiceEntity();
|
||||
entity.Networks = networkProvider;
|
||||
entity.ProductInformation = new ProductInformation() { Price = 5000 };
|
||||
entity.Price = 5000;
|
||||
PaymentMethodDictionary paymentMethods = new PaymentMethodDictionary();
|
||||
paymentMethods.Add(
|
||||
new PaymentMethod() { CryptoCode = "BTC", Rate = 1000, NextNetworkFee = Money.Coins(0.1m) });
|
||||
@@ -490,7 +520,7 @@ namespace BTCPayServer.Tests
|
||||
Assert.Equal(accounting.Paid, accounting.TotalDue);
|
||||
#pragma warning restore CS0618
|
||||
}
|
||||
|
||||
#endif
|
||||
[Fact]
|
||||
[Trait("Integration", "Integration")]
|
||||
public async Task CanUseTestWebsiteUI()
|
||||
@@ -545,7 +575,7 @@ namespace BTCPayServer.Tests
|
||||
Rate = 5000,
|
||||
NextNetworkFee = Money.Coins(0.1m)
|
||||
});
|
||||
entity.ProductInformation = new ProductInformation() { Price = 5000 };
|
||||
entity.Price = 5000;
|
||||
entity.PaymentTolerance = 0;
|
||||
|
||||
|
||||
|
||||
@@ -246,5 +246,5 @@
|
||||
<_ContentIncludedByDefault Remove="Views\Components\NotificationsDropdown\Default.cshtml" />
|
||||
</ItemGroup>
|
||||
|
||||
<ProjectExtensions><VisualStudio><UserProperties wwwroot_4swagger_4v1_4swagger_1template_1json__JsonSchema="https://raw.githubusercontent.com/OAI/OpenAPI-Specification/master/schemas/v3.0/schema.json" wwwroot_4swagger_4v1_4swagger_1template_1pull-payments_1json__JsonSchema="https://raw.githubusercontent.com/OAI/OpenAPI-Specification/master/schemas/v3.0/schema.json" wwwroot_4swagger_4v1_4swagger_1template_1serverinfo_1json__JsonSchema="https://raw.githubusercontent.com/OAI/OpenAPI-Specification/master/schemas/v3.0/schema.json" wwwroot_4swagger_4v1_4swagger_1template_1stores_1json__JsonSchema="https://raw.githubusercontent.com/OAI/OpenAPI-Specification/master/schemas/v3.0/schema.json" /></VisualStudio></ProjectExtensions>
|
||||
<ProjectExtensions><VisualStudio><UserProperties wwwroot_4swagger_4v1_4swagger_1template_1invoices_1json__JsonSchema="https://raw.githubusercontent.com/OAI/OpenAPI-Specification/master/schemas/v3.0/schema.json" wwwroot_4swagger_4v1_4swagger_1template_1json__JsonSchema="https://raw.githubusercontent.com/OAI/OpenAPI-Specification/master/schemas/v3.0/schema.json" wwwroot_4swagger_4v1_4swagger_1template_1pull-payments_1json__JsonSchema="https://raw.githubusercontent.com/OAI/OpenAPI-Specification/master/schemas/v3.0/schema.json" wwwroot_4swagger_4v1_4swagger_1template_1serverinfo_1json__JsonSchema="https://raw.githubusercontent.com/OAI/OpenAPI-Specification/master/schemas/v3.0/schema.json" wwwroot_4swagger_4v1_4swagger_1template_1stores_1json__JsonSchema="https://raw.githubusercontent.com/OAI/OpenAPI-Specification/master/schemas/v3.0/schema.json" /></VisualStudio></ProjectExtensions>
|
||||
</Project>
|
||||
|
||||
@@ -175,7 +175,7 @@ namespace BTCPayServer.Controllers
|
||||
var store = await _AppService.GetStore(app);
|
||||
try
|
||||
{
|
||||
var invoice = await _InvoiceController.CreateInvoiceCore(new CreateInvoiceRequest()
|
||||
var invoice = await _InvoiceController.CreateInvoiceCore(new BitpayCreateInvoiceRequest()
|
||||
{
|
||||
ItemCode = choice?.Id,
|
||||
ItemDesc = title,
|
||||
@@ -317,7 +317,7 @@ namespace BTCPayServer.Controllers
|
||||
|
||||
try
|
||||
{
|
||||
var invoice = await _InvoiceController.CreateInvoiceCore(new CreateInvoiceRequest()
|
||||
var invoice = await _InvoiceController.CreateInvoiceCore(new BitpayCreateInvoiceRequest()
|
||||
{
|
||||
OrderId = AppService.GetCrowdfundOrderId(appId),
|
||||
Currency = settings.TargetCurrency,
|
||||
|
||||
242
BTCPayServer/Controllers/GreenField/InvoiceController.cs
Normal file
242
BTCPayServer/Controllers/GreenField/InvoiceController.cs
Normal file
@@ -0,0 +1,242 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Client;
|
||||
using BTCPayServer.Client.Models;
|
||||
using BTCPayServer.Models.InvoicingModels;
|
||||
using BTCPayServer.Payments;
|
||||
using BTCPayServer.Security;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
using BTCPayServer.Validation;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Cors;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Conventions;
|
||||
using NBitcoin;
|
||||
using NBitpayClient;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using CreateInvoiceRequest = BTCPayServer.Client.Models.CreateInvoiceRequest;
|
||||
using InvoiceData = BTCPayServer.Client.Models.InvoiceData;
|
||||
|
||||
namespace BTCPayServer.Controllers.GreenField
|
||||
{
|
||||
[ApiController]
|
||||
[Authorize(AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
||||
[EnableCors(CorsPolicies.All)]
|
||||
public class GreenFieldInvoiceController : Controller
|
||||
{
|
||||
private readonly InvoiceController _invoiceController;
|
||||
private readonly InvoiceRepository _invoiceRepository;
|
||||
|
||||
public GreenFieldInvoiceController(InvoiceController invoiceController, InvoiceRepository invoiceRepository)
|
||||
{
|
||||
_invoiceController = invoiceController;
|
||||
_invoiceRepository = invoiceRepository;
|
||||
}
|
||||
|
||||
[Authorize(Policy = Policies.CanViewInvoices,
|
||||
AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
||||
[HttpGet("~/api/v1/stores/{storeId}/invoices")]
|
||||
public async Task<IActionResult> GetInvoices(string storeId, bool includeArchived = false)
|
||||
{
|
||||
var store = HttpContext.GetStoreData();
|
||||
if (store == null)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
var invoices =
|
||||
await _invoiceRepository.GetInvoices(new InvoiceQuery()
|
||||
{
|
||||
StoreId = new[] { store.Id },
|
||||
IncludeArchived = includeArchived
|
||||
});
|
||||
|
||||
return Ok(invoices.Select(ToModel));
|
||||
}
|
||||
|
||||
|
||||
[Authorize(Policy = Policies.CanViewInvoices,
|
||||
AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
||||
[HttpGet("~/api/v1/stores/{storeId}/invoices/{invoiceId}")]
|
||||
public async Task<IActionResult> GetInvoice(string storeId, string invoiceId)
|
||||
{
|
||||
var store = HttpContext.GetStoreData();
|
||||
if (store == null)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
var invoice = await _invoiceRepository.GetInvoice(invoiceId, true);
|
||||
if (invoice.StoreId != store.Id)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
return Ok(ToModel(invoice));
|
||||
}
|
||||
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings,
|
||||
AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
||||
[HttpDelete("~/api/v1/stores/{storeId}/invoices/{invoiceId}")]
|
||||
public async Task<IActionResult> ArchiveInvoice(string storeId, string invoiceId)
|
||||
{
|
||||
var store = HttpContext.GetStoreData();
|
||||
if (store == null)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
await _invoiceRepository.ToggleInvoiceArchival(invoiceId, true, storeId);
|
||||
return Ok();
|
||||
}
|
||||
|
||||
[Authorize(Policy = Policies.CanCreateInvoice,
|
||||
AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
||||
[HttpPost("~/api/v1/stores/{storeId}/invoices")]
|
||||
public async Task<IActionResult> CreateInvoice(string storeId, CreateInvoiceRequest request)
|
||||
{
|
||||
var store = HttpContext.GetStoreData();
|
||||
if (store == null)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
if (request.Amount < 0.0m)
|
||||
{
|
||||
ModelState.AddModelError(nameof(request.Amount), "The amount should be 0 or more.");
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(request.Currency))
|
||||
{
|
||||
ModelState.AddModelError(nameof(request.Currency), "Currency is required");
|
||||
}
|
||||
|
||||
if (request.Checkout.PaymentMethods?.Any() is true)
|
||||
{
|
||||
for (int i = 0; i < request.Checkout.PaymentMethods.Length; i++)
|
||||
{
|
||||
if (!PaymentMethodId.TryParse(request.Checkout.PaymentMethods[i], out _))
|
||||
{
|
||||
request.AddModelError(invoiceRequest => invoiceRequest.Checkout.PaymentMethods[i],
|
||||
"Invalid payment method", this);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (request.Checkout.Expiration != null && request.Checkout.Expiration < TimeSpan.FromSeconds(30.0))
|
||||
{
|
||||
request.AddModelError(invoiceRequest => invoiceRequest.Checkout.Expiration,
|
||||
"Expiration time must be at least 30 seconds", this);
|
||||
}
|
||||
|
||||
if (request.Checkout.PaymentTolerance != null &&
|
||||
(request.Checkout.PaymentTolerance < 0 || request.Checkout.PaymentTolerance > 100))
|
||||
{
|
||||
request.AddModelError(invoiceRequest => invoiceRequest.Checkout.PaymentTolerance,
|
||||
"PaymentTolerance can only be between 0 and 100 percent", this);
|
||||
}
|
||||
|
||||
if (!ModelState.IsValid)
|
||||
return this.CreateValidationError(ModelState);
|
||||
|
||||
try
|
||||
{
|
||||
var invoice = await _invoiceController.CreateInvoiceCoreRaw(request, store,
|
||||
Request.GetAbsoluteUri(""));
|
||||
return Ok(ToModel(invoice));
|
||||
}
|
||||
catch (BitpayHttpException e)
|
||||
{
|
||||
return this.CreateAPIError(null, e.Message);
|
||||
}
|
||||
}
|
||||
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings,
|
||||
AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
||||
[HttpPost("~/api/v1/stores/{storeId}/invoices/{invoiceId}/status")]
|
||||
public async Task<IActionResult> MarkInvoiceStatus(string storeId, string invoiceId,
|
||||
MarkInvoiceStatusRequest request)
|
||||
{
|
||||
var store = HttpContext.GetStoreData();
|
||||
if (store == null)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
var invoice = await _invoiceRepository.GetInvoice(invoiceId, true);
|
||||
if (invoice.StoreId != store.Id)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
if (!await _invoiceRepository.MarkInvoiceStatus(invoice.Id, request.Status))
|
||||
{
|
||||
ModelState.AddModelError(nameof(request.Status),
|
||||
"Status can only be marked to invalid or complete within certain conditions.");
|
||||
}
|
||||
|
||||
if (!ModelState.IsValid)
|
||||
return this.CreateValidationError(ModelState);
|
||||
|
||||
return await GetInvoice(storeId, invoiceId);
|
||||
}
|
||||
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings,
|
||||
AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
||||
[HttpPost("~/api/v1/stores/{storeId}/invoices/{invoiceId}/unarchive")]
|
||||
public async Task<IActionResult> UnarchiveInvoice(string storeId, string invoiceId)
|
||||
{
|
||||
var store = HttpContext.GetStoreData();
|
||||
if (store == null)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
var invoice = await _invoiceRepository.GetInvoice(invoiceId, true);
|
||||
if (invoice.StoreId != store.Id)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
if (!invoice.Archived)
|
||||
{
|
||||
return this.CreateAPIError("already-unarchived", "Invoice is already unarchived");
|
||||
}
|
||||
|
||||
|
||||
if (!ModelState.IsValid)
|
||||
return this.CreateValidationError(ModelState);
|
||||
|
||||
await _invoiceRepository.ToggleInvoiceArchival(invoiceId, false, storeId);
|
||||
return await GetInvoice(storeId, invoiceId);
|
||||
}
|
||||
|
||||
|
||||
private InvoiceData ToModel(InvoiceEntity entity)
|
||||
{
|
||||
return new InvoiceData()
|
||||
{
|
||||
ExpirationTime = entity.ExpirationTime,
|
||||
MonitoringExpiration = entity.MonitoringExpiration,
|
||||
CreatedTime = entity.InvoiceTime,
|
||||
Amount = entity.Price,
|
||||
Id = entity.Id,
|
||||
Status = entity.Status,
|
||||
AdditionalStatus = entity.ExceptionStatus,
|
||||
Currency = entity.Currency,
|
||||
Metadata = entity.Metadata.ToJObject(),
|
||||
Checkout = new CreateInvoiceRequest.CheckoutOptions()
|
||||
{
|
||||
Expiration = entity.ExpirationTime - entity.InvoiceTime,
|
||||
Monitoring = entity.MonitoringExpiration - entity.ExpirationTime,
|
||||
PaymentTolerance = entity.PaymentTolerance,
|
||||
PaymentMethods =
|
||||
entity.GetPaymentMethods().Select(method => method.GetId().ToStringNormalized()).ToArray(),
|
||||
SpeedPolicy = entity.SpeedPolicy
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -32,10 +32,10 @@ namespace BTCPayServer.Controllers.GreenField
|
||||
|
||||
[Authorize(Policy = Policies.CanViewPaymentRequests, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
||||
[HttpGet("~/api/v1/stores/{storeId}/payment-requests")]
|
||||
public async Task<ActionResult<IEnumerable<PaymentRequestData>>> GetPaymentRequests(string storeId)
|
||||
public async Task<ActionResult<IEnumerable<PaymentRequestData>>> GetPaymentRequests(string storeId, bool includeArchived = false)
|
||||
{
|
||||
var prs = await _paymentRequestRepository.FindPaymentRequests(
|
||||
new PaymentRequestQuery() { StoreId = storeId, IncludeArchived = false });
|
||||
new PaymentRequestQuery() { StoreId = storeId, IncludeArchived = includeArchived });
|
||||
return Ok(prs.Items.Select(FromModel));
|
||||
}
|
||||
|
||||
|
||||
@@ -124,8 +124,8 @@ namespace BTCPayServer.Controllers.GreenField
|
||||
ShowRecommendedFee = storeBlob.ShowRecommendedFee,
|
||||
RecommendedFeeBlockTarget = storeBlob.RecommendedFeeBlockTarget,
|
||||
DefaultLang = storeBlob.DefaultLang,
|
||||
MonitoringExpiration = TimeSpan.FromMinutes(storeBlob.MonitoringExpiration),
|
||||
InvoiceExpiration = TimeSpan.FromMinutes(storeBlob.InvoiceExpiration),
|
||||
MonitoringExpiration = storeBlob.MonitoringExpiration,
|
||||
InvoiceExpiration = storeBlob.InvoiceExpiration,
|
||||
LightningAmountInSatoshi = storeBlob.LightningAmountInSatoshi,
|
||||
CustomLogo = storeBlob.CustomLogo,
|
||||
CustomCSS = storeBlob.CustomCSS,
|
||||
@@ -160,8 +160,8 @@ namespace BTCPayServer.Controllers.GreenField
|
||||
blob.ShowRecommendedFee = restModel.ShowRecommendedFee;
|
||||
blob.RecommendedFeeBlockTarget = restModel.RecommendedFeeBlockTarget;
|
||||
blob.DefaultLang = restModel.DefaultLang;
|
||||
blob.MonitoringExpiration = (int)restModel.MonitoringExpiration.TotalMinutes;
|
||||
blob.InvoiceExpiration = (int)restModel.InvoiceExpiration.TotalMinutes;
|
||||
blob.MonitoringExpiration = restModel.MonitoringExpiration;
|
||||
blob.InvoiceExpiration = restModel.InvoiceExpiration;
|
||||
blob.LightningAmountInSatoshi = restModel.LightningAmountInSatoshi;
|
||||
blob.CustomLogo = restModel.CustomLogo;
|
||||
blob.CustomCSS = restModel.CustomCSS;
|
||||
|
||||
@@ -29,7 +29,7 @@ namespace BTCPayServer.Controllers
|
||||
[HttpPost]
|
||||
[Route("invoices")]
|
||||
[MediaTypeConstraint("application/json")]
|
||||
public async Task<DataWrapper<InvoiceResponse>> CreateInvoice([FromBody] CreateInvoiceRequest invoice, CancellationToken cancellationToken)
|
||||
public async Task<DataWrapper<InvoiceResponse>> CreateInvoice([FromBody] BitpayCreateInvoiceRequest invoice, CancellationToken cancellationToken)
|
||||
{
|
||||
if (invoice == null)
|
||||
throw new BitpayHttpException(400, "Invalid invoice");
|
||||
|
||||
@@ -30,6 +30,7 @@ using NBitcoin;
|
||||
using NBitpayClient;
|
||||
using NBXplorer;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using BitpayCreateInvoiceRequest = BTCPayServer.Models.BitpayCreateInvoiceRequest;
|
||||
using StoreData = BTCPayServer.Data.StoreData;
|
||||
|
||||
namespace BTCPayServer.Controllers
|
||||
@@ -52,7 +53,6 @@ namespace BTCPayServer.Controllers
|
||||
if (invoice == null)
|
||||
return NotFound();
|
||||
|
||||
var prodInfo = invoice.ProductInformation;
|
||||
var store = await _StoreRepository.FindStore(invoice.StoreId);
|
||||
var model = new InvoiceDetailsModel()
|
||||
{
|
||||
@@ -68,16 +68,14 @@ namespace BTCPayServer.Controllers
|
||||
CreatedDate = invoice.InvoiceTime,
|
||||
ExpirationDate = invoice.ExpirationTime,
|
||||
MonitoringDate = invoice.MonitoringExpiration,
|
||||
OrderId = invoice.OrderId,
|
||||
BuyerInformation = invoice.BuyerInformation,
|
||||
Fiat = _CurrencyNameTable.DisplayFormatCurrency(prodInfo.Price, prodInfo.Currency),
|
||||
TaxIncluded = _CurrencyNameTable.DisplayFormatCurrency(prodInfo.TaxIncluded, prodInfo.Currency),
|
||||
Fiat = _CurrencyNameTable.DisplayFormatCurrency(invoice.Price, invoice.Currency),
|
||||
TaxIncluded = _CurrencyNameTable.DisplayFormatCurrency(invoice.Metadata.TaxIncluded ?? 0.0m, invoice.Currency),
|
||||
NotificationUrl = invoice.NotificationURL?.AbsoluteUri,
|
||||
RedirectUrl = invoice.RedirectURL?.AbsoluteUri,
|
||||
ProductInformation = invoice.ProductInformation,
|
||||
TypedMetadata = invoice.Metadata,
|
||||
StatusException = invoice.ExceptionStatus,
|
||||
Events = invoice.Events,
|
||||
PosData = PosDataParser.ParsePosData(invoice.PosData),
|
||||
PosData = PosDataParser.ParsePosData(invoice.Metadata.PosData),
|
||||
Archived = invoice.Archived,
|
||||
CanRefund = CanRefund(invoice.GetInvoiceState()),
|
||||
};
|
||||
@@ -177,7 +175,7 @@ namespace BTCPayServer.Controllers
|
||||
if (!CanRefund(invoice.GetInvoiceState()))
|
||||
return NotFound();
|
||||
var paymentMethodId = new PaymentMethodId(model.SelectedPaymentMethod, PaymentTypes.BTCLike);
|
||||
var cdCurrency = _CurrencyNameTable.GetCurrencyData(invoice.ProductInformation.Currency, true);
|
||||
var cdCurrency = _CurrencyNameTable.GetCurrencyData(invoice.Currency, true);
|
||||
var paymentMethodDivisibility = _CurrencyNameTable.GetCurrencyData(paymentMethodId.CryptoCode, false)?.Divisibility ?? 8;
|
||||
if (model.SelectedRefundOption is null)
|
||||
{
|
||||
@@ -187,7 +185,7 @@ namespace BTCPayServer.Controllers
|
||||
model.CryptoAmountThen = Math.Round(paidCurrency / paymentMethod.Rate, paymentMethodDivisibility);
|
||||
model.RateThenText = _CurrencyNameTable.DisplayFormatCurrency(model.CryptoAmountThen, paymentMethodId.CryptoCode, true);
|
||||
var rules = store.GetStoreBlob().GetRateRules(_NetworkProvider);
|
||||
var rateResult = await _RateProvider.FetchRate(new Rating.CurrencyPair(paymentMethodId.CryptoCode, invoice.ProductInformation.Currency), rules, cancellationToken);
|
||||
var rateResult = await _RateProvider.FetchRate(new Rating.CurrencyPair(paymentMethodId.CryptoCode, invoice.Currency), rules, cancellationToken);
|
||||
//TODO: What if fetching rate failed?
|
||||
if (rateResult.BidAsk is null)
|
||||
{
|
||||
@@ -197,7 +195,7 @@ namespace BTCPayServer.Controllers
|
||||
model.CryptoAmountNow = Math.Round(paidCurrency / rateResult.BidAsk.Bid, paymentMethodDivisibility);
|
||||
model.CurrentRateText = _CurrencyNameTable.DisplayFormatCurrency(model.CryptoAmountNow, paymentMethodId.CryptoCode, true);
|
||||
model.FiatAmount = paidCurrency;
|
||||
model.FiatText = _CurrencyNameTable.DisplayFormatCurrency(model.FiatAmount, invoice.ProductInformation.Currency, true);
|
||||
model.FiatText = _CurrencyNameTable.DisplayFormatCurrency(model.FiatAmount, invoice.Currency, true);
|
||||
return View(model);
|
||||
}
|
||||
else
|
||||
@@ -217,7 +215,7 @@ namespace BTCPayServer.Controllers
|
||||
createPullPayment.Amount = model.CryptoAmountNow;
|
||||
break;
|
||||
case "Fiat":
|
||||
createPullPayment.Currency = invoice.ProductInformation.Currency;
|
||||
createPullPayment.Currency = invoice.Currency;
|
||||
createPullPayment.Amount = model.FiatAmount;
|
||||
break;
|
||||
default:
|
||||
@@ -403,7 +401,7 @@ namespace BTCPayServer.Controllers
|
||||
var dto = invoice.EntityToDTO();
|
||||
var cryptoInfo = dto.CryptoInfo.First(o => o.GetpaymentMethodId() == paymentMethodId);
|
||||
var storeBlob = store.GetStoreBlob();
|
||||
var currency = invoice.ProductInformation.Currency;
|
||||
var currency = invoice.Currency;
|
||||
var accounting = paymentMethod.Calculate();
|
||||
|
||||
ChangellySettings changelly = (storeBlob.ChangellySettings != null && storeBlob.ChangellySettings.Enabled &&
|
||||
@@ -425,12 +423,11 @@ namespace BTCPayServer.Controllers
|
||||
var paymentMethodHandler = _paymentMethodHandlerDictionary[paymentMethodId];
|
||||
|
||||
var divisibility = _CurrencyNameTable.GetNumberFormatInfo(paymentMethod.GetId().CryptoCode, false)?.CurrencyDecimalDigits;
|
||||
|
||||
var model = new PaymentModel()
|
||||
{
|
||||
CryptoCode = network.CryptoCode,
|
||||
RootPath = this.Request.PathBase.Value.WithTrailingSlash(),
|
||||
OrderId = invoice.OrderId,
|
||||
OrderId = invoice.Metadata.OrderId,
|
||||
InvoiceId = invoice.Id,
|
||||
DefaultLang = storeBlob.DefaultLang ?? "en",
|
||||
CustomCSSLink = storeBlob.CustomCSS,
|
||||
@@ -440,7 +437,7 @@ namespace BTCPayServer.Controllers
|
||||
BtcAddress = paymentMethodDetails.GetPaymentDestination(),
|
||||
BtcDue = accounting.Due.ShowMoney(divisibility),
|
||||
OrderAmount = (accounting.TotalDue - accounting.NetworkFee).ShowMoney(divisibility),
|
||||
OrderAmountFiat = OrderAmountFromInvoice(network.CryptoCode, invoice.ProductInformation),
|
||||
OrderAmountFiat = OrderAmountFromInvoice(network.CryptoCode, invoice),
|
||||
CustomerEmail = invoice.RefundMail,
|
||||
RequiresRefundEmail = storeBlob.RequiresRefundEmail,
|
||||
ShowRecommendedFee = storeBlob.ShowRecommendedFee,
|
||||
@@ -448,7 +445,7 @@ namespace BTCPayServer.Controllers
|
||||
ExpirationSeconds = Math.Max(0, (int)(invoice.ExpirationTime - DateTimeOffset.UtcNow).TotalSeconds),
|
||||
MaxTimeSeconds = (int)(invoice.ExpirationTime - invoice.InvoiceTime).TotalSeconds,
|
||||
MaxTimeMinutes = (int)(invoice.ExpirationTime - invoice.InvoiceTime).TotalMinutes,
|
||||
ItemDesc = invoice.ProductInformation.ItemDesc,
|
||||
ItemDesc = invoice.Metadata.ItemDesc,
|
||||
Rate = ExchangeRate(paymentMethod),
|
||||
MerchantRefLink = invoice.RedirectURL?.AbsoluteUri ?? "/",
|
||||
RedirectAutomatically = invoice.RedirectAutomatically,
|
||||
@@ -498,7 +495,7 @@ namespace BTCPayServer.Controllers
|
||||
paymentMethodHandler.PreparePaymentModel(model, dto, storeBlob);
|
||||
if (model.IsLightning && storeBlob.LightningAmountInSatoshi && model.CryptoCode == "Sats")
|
||||
{
|
||||
model.Rate = _CurrencyNameTable.DisplayFormatCurrency(paymentMethod.Rate / 100_000_000, paymentMethod.ParentEntity.ProductInformation.Currency);
|
||||
model.Rate = _CurrencyNameTable.DisplayFormatCurrency(paymentMethod.Rate / 100_000_000, paymentMethod.ParentEntity.Currency);
|
||||
}
|
||||
model.UISettings = paymentMethodHandler.GetCheckoutUISettings();
|
||||
model.PaymentMethodId = paymentMethodId.ToString();
|
||||
@@ -507,17 +504,17 @@ namespace BTCPayServer.Controllers
|
||||
return model;
|
||||
}
|
||||
|
||||
private string OrderAmountFromInvoice(string cryptoCode, ProductInformation productInformation)
|
||||
private string OrderAmountFromInvoice(string cryptoCode, InvoiceEntity invoiceEntity)
|
||||
{
|
||||
// if invoice source currency is the same as currently display currency, no need for "order amount from invoice"
|
||||
if (cryptoCode == productInformation.Currency)
|
||||
if (cryptoCode == invoiceEntity.Currency)
|
||||
return null;
|
||||
|
||||
return _CurrencyNameTable.DisplayFormatCurrency(productInformation.Price, productInformation.Currency);
|
||||
return _CurrencyNameTable.DisplayFormatCurrency(invoiceEntity.Price, invoiceEntity.Currency);
|
||||
}
|
||||
private string ExchangeRate(PaymentMethod paymentMethod)
|
||||
{
|
||||
string currency = paymentMethod.ParentEntity.ProductInformation.Currency;
|
||||
string currency = paymentMethod.ParentEntity.Currency;
|
||||
return _CurrencyNameTable.DisplayFormatCurrency(paymentMethod.Rate, currency);
|
||||
}
|
||||
|
||||
@@ -626,9 +623,9 @@ namespace BTCPayServer.Controllers
|
||||
ShowCheckout = invoice.Status == InvoiceStatus.New,
|
||||
Date = invoice.InvoiceTime,
|
||||
InvoiceId = invoice.Id,
|
||||
OrderId = invoice.OrderId ?? string.Empty,
|
||||
OrderId = invoice.Metadata.OrderId ?? string.Empty,
|
||||
RedirectUrl = invoice.RedirectURL?.AbsoluteUri ?? string.Empty,
|
||||
AmountCurrency = _CurrencyNameTable.DisplayFormatCurrency(invoice.ProductInformation.Price, invoice.ProductInformation.Currency),
|
||||
AmountCurrency = _CurrencyNameTable.DisplayFormatCurrency(invoice.Price, invoice.Currency),
|
||||
CanMarkInvalid = state.CanMarkInvalid(),
|
||||
CanMarkComplete = state.CanMarkComplete(),
|
||||
Details = InvoicePopulatePayments(invoice),
|
||||
@@ -730,7 +727,7 @@ namespace BTCPayServer.Controllers
|
||||
|
||||
try
|
||||
{
|
||||
var result = await CreateInvoiceCore(new CreateInvoiceRequest()
|
||||
var result = await CreateInvoiceCore(new BitpayCreateInvoiceRequest()
|
||||
{
|
||||
Price = model.Amount.Value,
|
||||
Currency = model.Currency,
|
||||
@@ -776,14 +773,12 @@ namespace BTCPayServer.Controllers
|
||||
}
|
||||
if (newState == "invalid")
|
||||
{
|
||||
await _InvoiceRepository.UpdatePaidInvoiceToInvalid(invoiceId);
|
||||
_EventAggregator.Publish(new InvoiceEvent(invoice, 1008, InvoiceEvent.MarkedInvalid));
|
||||
await _InvoiceRepository.MarkInvoiceStatus(invoiceId, InvoiceStatus.Invalid);
|
||||
model.StatusString = new InvoiceState("invalid", "marked").ToString();
|
||||
}
|
||||
else if (newState == "complete")
|
||||
{
|
||||
await _InvoiceRepository.UpdatePaidInvoiceToComplete(invoiceId);
|
||||
_EventAggregator.Publish(new InvoiceEvent(invoice, 2008, InvoiceEvent.MarkedCompleted));
|
||||
await _InvoiceRepository.MarkInvoiceStatus(invoiceId, InvoiceStatus.Complete);
|
||||
model.StatusString = new InvoiceState("complete", "marked").ToString();
|
||||
}
|
||||
|
||||
|
||||
@@ -21,9 +21,10 @@ using BTCPayServer.Services.Stores;
|
||||
using BTCPayServer.Validation;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
||||
using NBitpayClient;
|
||||
using Newtonsoft.Json;
|
||||
using CreateInvoiceRequest = BTCPayServer.Models.CreateInvoiceRequest;
|
||||
using BitpayCreateInvoiceRequest = BTCPayServer.Models.BitpayCreateInvoiceRequest;
|
||||
using StoreData = BTCPayServer.Data.StoreData;
|
||||
|
||||
namespace BTCPayServer.Controllers
|
||||
@@ -72,40 +73,37 @@ namespace BTCPayServer.Controllers
|
||||
}
|
||||
|
||||
|
||||
internal async Task<DataWrapper<InvoiceResponse>> CreateInvoiceCore(CreateInvoiceRequest invoice, StoreData store, string serverUrl, List<string> additionalTags = null, CancellationToken cancellationToken = default)
|
||||
internal async Task<DataWrapper<InvoiceResponse>> CreateInvoiceCore(BitpayCreateInvoiceRequest invoice,
|
||||
StoreData store, string serverUrl, List<string> additionalTags = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
invoice.Currency = invoice.Currency?.ToUpperInvariant() ?? "USD";
|
||||
InvoiceLogs logs = new InvoiceLogs();
|
||||
logs.Write("Creation of invoice starting");
|
||||
var entity = _InvoiceRepository.CreateNewInvoice();
|
||||
var entity = await CreateInvoiceCoreRaw(invoice, store, serverUrl, additionalTags, cancellationToken);
|
||||
var resp = entity.EntityToDTO();
|
||||
return new DataWrapper<InvoiceResponse>(resp) { Facade = "pos/invoice" };
|
||||
}
|
||||
|
||||
var getAppsTaggingStore = _InvoiceRepository.GetAppsTaggingStore(store.Id);
|
||||
internal async Task<InvoiceEntity> CreateInvoiceCoreRaw(BitpayCreateInvoiceRequest invoice, StoreData store, string serverUrl, List<string> additionalTags = null, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var storeBlob = store.GetStoreBlob();
|
||||
EmailAddressAttribute emailValidator = new EmailAddressAttribute();
|
||||
entity.ExpirationTime = invoice.ExpirationTime is DateTimeOffset v ? v : entity.InvoiceTime.AddMinutes(storeBlob.InvoiceExpiration);
|
||||
var entity = _InvoiceRepository.CreateNewInvoice();
|
||||
entity.ExpirationTime = invoice.ExpirationTime is DateTimeOffset v ? v : entity.InvoiceTime + storeBlob.InvoiceExpiration;
|
||||
entity.MonitoringExpiration = entity.ExpirationTime + storeBlob.MonitoringExpiration;
|
||||
if (entity.ExpirationTime - TimeSpan.FromSeconds(30.0) < entity.InvoiceTime)
|
||||
{
|
||||
throw new BitpayHttpException(400, "The expirationTime is set too soon");
|
||||
}
|
||||
entity.MonitoringExpiration = entity.ExpirationTime + TimeSpan.FromMinutes(storeBlob.MonitoringExpiration);
|
||||
entity.OrderId = invoice.OrderId;
|
||||
invoice.Currency = invoice.Currency?.ToUpperInvariant() ?? "USD";
|
||||
entity.Currency = invoice.Currency;
|
||||
entity.Metadata.OrderId = invoice.OrderId;
|
||||
entity.Metadata.PosData = invoice.PosData;
|
||||
entity.ServerUrl = serverUrl;
|
||||
entity.FullNotifications = invoice.FullNotifications || invoice.ExtendedNotifications;
|
||||
entity.ExtendedNotifications = invoice.ExtendedNotifications;
|
||||
entity.NotificationURLTemplate = invoice.NotificationURL;
|
||||
entity.NotificationEmail = invoice.NotificationEmail;
|
||||
entity.BuyerInformation = Map<CreateInvoiceRequest, BuyerInformation>(invoice);
|
||||
entity.PaymentTolerance = storeBlob.PaymentTolerance;
|
||||
if (additionalTags != null)
|
||||
entity.InternalTags.AddRange(additionalTags);
|
||||
//Another way of passing buyer info to support
|
||||
FillBuyerInfo(invoice.Buyer, entity.BuyerInformation);
|
||||
if (entity?.BuyerInformation?.BuyerEmail != null)
|
||||
{
|
||||
if (!EmailValidator.IsEmail(entity.BuyerInformation.BuyerEmail))
|
||||
throw new BitpayHttpException(400, "Invalid email");
|
||||
entity.RefundMail = entity.BuyerInformation.BuyerEmail;
|
||||
}
|
||||
FillBuyerInfo(invoice, entity);
|
||||
|
||||
var taxIncluded = invoice.TaxIncluded.HasValue ? invoice.TaxIncluded.Value : 0m;
|
||||
|
||||
@@ -120,22 +118,19 @@ namespace BTCPayServer.Controllers
|
||||
invoice.Price = Math.Max(0.0m, invoice.Price);
|
||||
invoice.TaxIncluded = Math.Max(0.0m, taxIncluded);
|
||||
invoice.TaxIncluded = Math.Min(taxIncluded, invoice.Price);
|
||||
|
||||
entity.ProductInformation = Map<CreateInvoiceRequest, ProductInformation>(invoice);
|
||||
|
||||
entity.Metadata.ItemCode = invoice.ItemCode;
|
||||
entity.Metadata.ItemDesc = invoice.ItemDesc;
|
||||
entity.Metadata.Physical = invoice.Physical;
|
||||
entity.Metadata.TaxIncluded = invoice.TaxIncluded;
|
||||
entity.Currency = invoice.Currency;
|
||||
entity.Price = invoice.Price;
|
||||
|
||||
entity.RedirectURLTemplate = invoice.RedirectURL ?? store.StoreWebsite;
|
||||
|
||||
entity.RedirectAutomatically =
|
||||
invoice.RedirectAutomatically.GetValueOrDefault(storeBlob.RedirectAutomatically);
|
||||
|
||||
entity.Status = InvoiceStatus.New;
|
||||
entity.SpeedPolicy = ParseSpeedPolicy(invoice.TransactionSpeed, store.SpeedPolicy);
|
||||
|
||||
HashSet<CurrencyPair> currencyPairsToFetch = new HashSet<CurrencyPair>();
|
||||
var rules = storeBlob.GetRateRules(_NetworkProvider);
|
||||
var excludeFilter = storeBlob.GetExcludedPaymentMethods(); // Here we can compose filters from other origin with PaymentFilter.Any()
|
||||
|
||||
IPaymentFilter excludeFilter = null;
|
||||
if (invoice.PaymentCurrencies?.Any() is true)
|
||||
{
|
||||
invoice.SupportedTransactionCurrencies ??=
|
||||
@@ -151,17 +146,67 @@ namespace BTCPayServer.Controllers
|
||||
var supportedTransactionCurrencies = invoice.SupportedTransactionCurrencies
|
||||
.Where(c => c.Value.Enabled)
|
||||
.Select(c => PaymentMethodId.TryParse(c.Key, out var p) ? p : null)
|
||||
.Where(c => c != null)
|
||||
.ToHashSet();
|
||||
excludeFilter = PaymentFilter.Or(excludeFilter,
|
||||
PaymentFilter.Where(p => !supportedTransactionCurrencies.Contains(p)));
|
||||
excludeFilter = PaymentFilter.Where(p => !supportedTransactionCurrencies.Contains(p));
|
||||
}
|
||||
entity.PaymentTolerance = storeBlob.PaymentTolerance;
|
||||
return await CreateInvoiceCoreRaw(entity, store, excludeFilter, cancellationToken);
|
||||
}
|
||||
|
||||
internal async Task<InvoiceEntity> CreateInvoiceCoreRaw(CreateInvoiceRequest invoice, StoreData store, string serverUrl, List<string> additionalTags = null, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var storeBlob = store.GetStoreBlob();
|
||||
var entity = _InvoiceRepository.CreateNewInvoice();
|
||||
entity.ExpirationTime = entity.InvoiceTime + (invoice.Checkout.Expiration ?? storeBlob.InvoiceExpiration);
|
||||
entity.MonitoringExpiration = entity.ExpirationTime + (invoice.Checkout.Monitoring ?? storeBlob.MonitoringExpiration);
|
||||
if (invoice.Metadata != null)
|
||||
entity.Metadata = InvoiceMetadata.FromJObject(invoice.Metadata);
|
||||
invoice.Checkout ??= new CreateInvoiceRequest.CheckoutOptions();
|
||||
entity.Currency = invoice.Currency;
|
||||
entity.Price = invoice.Amount;
|
||||
entity.SpeedPolicy = invoice.Checkout.SpeedPolicy ?? store.SpeedPolicy;
|
||||
IPaymentFilter excludeFilter = null;
|
||||
if (invoice.Checkout.PaymentMethods != null)
|
||||
{
|
||||
var supportedTransactionCurrencies = invoice.Checkout.PaymentMethods
|
||||
.Select(c => PaymentMethodId.TryParse(c, out var p) ? p : null)
|
||||
.ToHashSet();
|
||||
excludeFilter = PaymentFilter.Where(p => !supportedTransactionCurrencies.Contains(p));
|
||||
}
|
||||
entity.PaymentTolerance = invoice.Checkout.PaymentTolerance ?? storeBlob.PaymentTolerance;
|
||||
return await CreateInvoiceCoreRaw(entity, store, excludeFilter, cancellationToken);
|
||||
}
|
||||
|
||||
internal async Task<InvoiceEntity> CreateInvoiceCoreRaw(InvoiceEntity entity, StoreData store, IPaymentFilter invoicePaymentMethodFilter, CancellationToken cancellationToken = default)
|
||||
{
|
||||
InvoiceLogs logs = new InvoiceLogs();
|
||||
logs.Write("Creation of invoice starting");
|
||||
|
||||
var getAppsTaggingStore = _InvoiceRepository.GetAppsTaggingStore(store.Id);
|
||||
var storeBlob = store.GetStoreBlob();
|
||||
|
||||
if (entity.Metadata.BuyerEmail != null)
|
||||
{
|
||||
if (!EmailValidator.IsEmail(entity.Metadata.BuyerEmail))
|
||||
throw new BitpayHttpException(400, "Invalid email");
|
||||
entity.RefundMail = entity.Metadata.BuyerEmail;
|
||||
}
|
||||
entity.Status = InvoiceStatus.New;
|
||||
HashSet<CurrencyPair> currencyPairsToFetch = new HashSet<CurrencyPair>();
|
||||
var rules = storeBlob.GetRateRules(_NetworkProvider);
|
||||
var excludeFilter = storeBlob.GetExcludedPaymentMethods(); // Here we can compose filters from other origin with PaymentFilter.Any()
|
||||
if (invoicePaymentMethodFilter != null)
|
||||
{
|
||||
excludeFilter = PaymentFilter.Or(excludeFilter,
|
||||
invoicePaymentMethodFilter);
|
||||
}
|
||||
foreach (var network in store.GetSupportedPaymentMethods(_NetworkProvider)
|
||||
.Where(s => !excludeFilter.Match(s.PaymentId))
|
||||
.Select(c => _NetworkProvider.GetNetwork<BTCPayNetworkBase>(c.PaymentId.CryptoCode))
|
||||
.Where(c => c != null))
|
||||
{
|
||||
currencyPairsToFetch.Add(new CurrencyPair(network.CryptoCode, invoice.Currency));
|
||||
currencyPairsToFetch.Add(new CurrencyPair(network.CryptoCode, entity.Currency));
|
||||
//TODO: abstract
|
||||
if (storeBlob.LightningMaxValue != null)
|
||||
currencyPairsToFetch.Add(new CurrencyPair(network.CryptoCode, storeBlob.LightningMaxValue.Currency));
|
||||
@@ -173,7 +218,12 @@ namespace BTCPayServer.Controllers
|
||||
var fetchingByCurrencyPair = _RateProvider.FetchRates(currencyPairsToFetch, rateRules, cancellationToken);
|
||||
var fetchingAll = WhenAllFetched(logs, fetchingByCurrencyPair);
|
||||
|
||||
var supportedPaymentMethods = store.GetSupportedPaymentMethods(_NetworkProvider)
|
||||
List<ISupportedPaymentMethod> supported = new List<ISupportedPaymentMethod>();
|
||||
var paymentMethods = new PaymentMethodDictionary();
|
||||
|
||||
// This loop ends with .ToList so we are querying all payment methods at once
|
||||
// instead of sequentially to improve response time
|
||||
foreach (var o in store.GetSupportedPaymentMethods(_NetworkProvider)
|
||||
.Where(s => !excludeFilter.Match(s.PaymentId) && _paymentMethodHandlerDictionary.Support(s.PaymentId))
|
||||
.Select(c =>
|
||||
(Handler: _paymentMethodHandlerDictionary[c.PaymentId],
|
||||
@@ -183,10 +233,7 @@ namespace BTCPayServer.Controllers
|
||||
.Select(o =>
|
||||
(SupportedPaymentMethod: o.SupportedPaymentMethod,
|
||||
PaymentMethod: CreatePaymentMethodAsync(fetchingByCurrencyPair, o.Handler, o.SupportedPaymentMethod, o.Network, entity, store, logs)))
|
||||
.ToList();
|
||||
List<ISupportedPaymentMethod> supported = new List<ISupportedPaymentMethod>();
|
||||
var paymentMethods = new PaymentMethodDictionary();
|
||||
foreach (var o in supportedPaymentMethods)
|
||||
.ToList())
|
||||
{
|
||||
var paymentMethod = await o.PaymentMethod;
|
||||
if (paymentMethod == null)
|
||||
@@ -209,8 +256,6 @@ namespace BTCPayServer.Controllers
|
||||
|
||||
entity.SetSupportedPaymentMethods(supported);
|
||||
entity.SetPaymentMethods(paymentMethods);
|
||||
entity.PosData = invoice.PosData;
|
||||
|
||||
foreach (var app in await getAppsTaggingStore)
|
||||
{
|
||||
entity.InternalTags.Add(AppService.GetAppInternalTag(app.Id));
|
||||
@@ -232,9 +277,8 @@ namespace BTCPayServer.Controllers
|
||||
}
|
||||
await _InvoiceRepository.AddInvoiceLogs(entity.Id, logs);
|
||||
});
|
||||
_EventAggregator.Publish(new Events.InvoiceEvent(entity, 1001, InvoiceEvent.Created));
|
||||
var resp = entity.EntityToDTO();
|
||||
return new DataWrapper<InvoiceResponse>(resp) { Facade = "pos/invoice" };
|
||||
_EventAggregator.Publish(new Events.InvoiceEvent(entity, InvoiceEvent.Created));
|
||||
return entity;
|
||||
}
|
||||
|
||||
private Task WhenAllFetched(InvoiceLogs logs, Dictionary<CurrencyPair, Task<RateResult>> fetchingByCurrencyPair)
|
||||
@@ -263,7 +307,7 @@ namespace BTCPayServer.Controllers
|
||||
var logPrefix = $"{supportedPaymentMethod.PaymentId.ToPrettyString()}:";
|
||||
var storeBlob = store.GetStoreBlob();
|
||||
var preparePayment = handler.PreparePayment(supportedPaymentMethod, store, network);
|
||||
var rate = await fetchingByCurrencyPair[new CurrencyPair(network.CryptoCode, entity.ProductInformation.Currency)];
|
||||
var rate = await fetchingByCurrencyPair[new CurrencyPair(network.CryptoCode, entity.Currency)];
|
||||
if (rate.BidAsk == null)
|
||||
{
|
||||
return null;
|
||||
@@ -327,24 +371,30 @@ namespace BTCPayServer.Controllers
|
||||
return policy;
|
||||
}
|
||||
|
||||
private void FillBuyerInfo(Buyer buyer, BuyerInformation buyerInformation)
|
||||
private void FillBuyerInfo(BitpayCreateInvoiceRequest req, InvoiceEntity invoiceEntity)
|
||||
{
|
||||
var buyerInformation = invoiceEntity.Metadata;
|
||||
buyerInformation.BuyerAddress1 = req.BuyerAddress1;
|
||||
buyerInformation.BuyerAddress2 = req.BuyerAddress2;
|
||||
buyerInformation.BuyerCity = req.BuyerCity;
|
||||
buyerInformation.BuyerCountry = req.BuyerCountry;
|
||||
buyerInformation.BuyerEmail = req.BuyerEmail;
|
||||
buyerInformation.BuyerName = req.BuyerName;
|
||||
buyerInformation.BuyerPhone = req.BuyerPhone;
|
||||
buyerInformation.BuyerState = req.BuyerState;
|
||||
buyerInformation.BuyerZip = req.BuyerZip;
|
||||
var buyer = req.Buyer;
|
||||
if (buyer == null)
|
||||
return;
|
||||
buyerInformation.BuyerAddress1 = buyerInformation.BuyerAddress1 ?? buyer.Address1;
|
||||
buyerInformation.BuyerAddress2 = buyerInformation.BuyerAddress2 ?? buyer.Address2;
|
||||
buyerInformation.BuyerCity = buyerInformation.BuyerCity ?? buyer.City;
|
||||
buyerInformation.BuyerCountry = buyerInformation.BuyerCountry ?? buyer.country;
|
||||
buyerInformation.BuyerEmail = buyerInformation.BuyerEmail ?? buyer.email;
|
||||
buyerInformation.BuyerName = buyerInformation.BuyerName ?? buyer.Name;
|
||||
buyerInformation.BuyerPhone = buyerInformation.BuyerPhone ?? buyer.phone;
|
||||
buyerInformation.BuyerState = buyerInformation.BuyerState ?? buyer.State;
|
||||
buyerInformation.BuyerZip = buyerInformation.BuyerZip ?? buyer.zip;
|
||||
}
|
||||
|
||||
private TDest Map<TFrom, TDest>(TFrom data)
|
||||
{
|
||||
return JsonConvert.DeserializeObject<TDest>(JsonConvert.SerializeObject(data));
|
||||
buyerInformation.BuyerAddress1 ??= buyer.Address1;
|
||||
buyerInformation.BuyerAddress2 ??= buyer.Address2;
|
||||
buyerInformation.BuyerCity ??= buyer.City;
|
||||
buyerInformation.BuyerCountry ??= buyer.country;
|
||||
buyerInformation.BuyerEmail ??= buyer.email;
|
||||
buyerInformation.BuyerName ??= buyer.Name;
|
||||
buyerInformation.BuyerPhone ??= buyer.phone;
|
||||
buyerInformation.BuyerState ??= buyer.State;
|
||||
buyerInformation.BuyerZip ??= buyer.zip;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -363,6 +363,8 @@ namespace BTCPayServer.Controllers
|
||||
{BTCPayServer.Client.Policies.CanModifyProfile, ("Manage your profile", "The app will be able to view and modify your user profile.")},
|
||||
{BTCPayServer.Client.Policies.CanCreateInvoice, ("Create an invoice", "The app will be able to create new invoices.")},
|
||||
{$"{BTCPayServer.Client.Policies.CanCreateInvoice}:", ("Create an invoice", "The app will be able to create new invoices on the selected stores.")},
|
||||
{BTCPayServer.Client.Policies.CanViewInvoices, ("View invoices", "The app will be able to view invoices.")},
|
||||
{$"{BTCPayServer.Client.Policies.CanViewInvoices}:", ("View invoices", "The app will be able to view invoices on the selected stores.")},
|
||||
{BTCPayServer.Client.Policies.CanModifyPaymentRequests, ("Modify your payment requests", "The app will be able to view, modify, delete and create new payment requests on all your stores.")},
|
||||
{$"{BTCPayServer.Client.Policies.CanModifyPaymentRequests}:", ("Manage selected stores' payment requests", "The app will be able to view, modify, delete and create new payment requests on the selected stores.")},
|
||||
{BTCPayServer.Client.Policies.CanViewPaymentRequests, ("View your payment requests", "The app will be able to view payment requests.")},
|
||||
|
||||
@@ -3,6 +3,7 @@ using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Client.Models;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Events;
|
||||
using BTCPayServer.Filters;
|
||||
@@ -19,6 +20,9 @@ using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.Rendering;
|
||||
using Microsoft.AspNetCore.Routing;
|
||||
using BitpayCreateInvoiceRequest = BTCPayServer.Models.BitpayCreateInvoiceRequest;
|
||||
using PaymentRequestData = BTCPayServer.Data.PaymentRequestData;
|
||||
using StoreData = BTCPayServer.Data.StoreData;
|
||||
|
||||
namespace BTCPayServer.Controllers
|
||||
{
|
||||
@@ -256,7 +260,7 @@ namespace BTCPayServer.Controllers
|
||||
try
|
||||
{
|
||||
var redirectUrl = _linkGenerator.PaymentRequestLink(id, Request.Scheme, Request.Host, Request.PathBase);
|
||||
var newInvoiceId = (await _InvoiceController.CreateInvoiceCore(new CreateInvoiceRequest()
|
||||
var newInvoiceId = (await _InvoiceController.CreateInvoiceCore(new BitpayCreateInvoiceRequest()
|
||||
{
|
||||
OrderId = $"{PaymentRequestRepository.GetOrderIdForPaymentRequest(id)}",
|
||||
Currency = blob.Currency,
|
||||
@@ -303,9 +307,7 @@ namespace BTCPayServer.Controllers
|
||||
|
||||
foreach (var invoice in invoices)
|
||||
{
|
||||
await _InvoiceRepository.UpdatePaidInvoiceToInvalid(invoice.Id);
|
||||
_EventAggregator.Publish(new InvoiceEvent(await _InvoiceRepository.GetInvoice(invoice.Id), 1008,
|
||||
InvoiceEvent.MarkedInvalid));
|
||||
await _InvoiceRepository.MarkInvoiceStatus(invoice.Id, InvoiceStatus.Invalid);
|
||||
}
|
||||
|
||||
if (redirect)
|
||||
|
||||
@@ -57,7 +57,7 @@ namespace BTCPayServer.Controllers
|
||||
DataWrapper<InvoiceResponse> invoice = null;
|
||||
try
|
||||
{
|
||||
invoice = await _InvoiceController.CreateInvoiceCore(new CreateInvoiceRequest()
|
||||
invoice = await _InvoiceController.CreateInvoiceCore(new BitpayCreateInvoiceRequest()
|
||||
{
|
||||
Price = model.Price,
|
||||
Currency = model.Currency,
|
||||
|
||||
@@ -481,8 +481,8 @@ namespace BTCPayServer.Controllers
|
||||
vm.SpeedPolicy = store.SpeedPolicy;
|
||||
vm.CanDelete = _Repo.CanDeleteStores();
|
||||
AddPaymentMethods(store, storeBlob, vm);
|
||||
vm.MonitoringExpiration = storeBlob.MonitoringExpiration;
|
||||
vm.InvoiceExpiration = storeBlob.InvoiceExpiration;
|
||||
vm.MonitoringExpiration = (int)storeBlob.MonitoringExpiration.TotalMinutes;
|
||||
vm.InvoiceExpiration = (int)storeBlob.InvoiceExpiration.TotalMinutes;
|
||||
vm.LightningDescriptionTemplate = storeBlob.LightningDescriptionTemplate;
|
||||
vm.PaymentTolerance = storeBlob.PaymentTolerance;
|
||||
vm.PayJoinEnabled = storeBlob.PayJoinEnabled;
|
||||
@@ -579,8 +579,8 @@ namespace BTCPayServer.Controllers
|
||||
var blob = CurrentStore.GetStoreBlob();
|
||||
blob.AnyoneCanInvoice = model.AnyoneCanCreateInvoice;
|
||||
blob.NetworkFeeMode = model.NetworkFeeMode;
|
||||
blob.MonitoringExpiration = model.MonitoringExpiration;
|
||||
blob.InvoiceExpiration = model.InvoiceExpiration;
|
||||
blob.MonitoringExpiration = TimeSpan.FromMinutes(model.MonitoringExpiration);
|
||||
blob.InvoiceExpiration = TimeSpan.FromMinutes(model.InvoiceExpiration);
|
||||
blob.LightningDescriptionTemplate = model.LightningDescriptionTemplate ?? string.Empty;
|
||||
blob.PaymentTolerance = model.PaymentTolerance;
|
||||
var payjoinChanged = blob.PayJoinEnabled != model.PayJoinEnabled;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using BTCPayServer.Services.Invoices;
|
||||
using Microsoft.AspNetCore.Authorization.Infrastructure;
|
||||
|
||||
namespace BTCPayServer.Data
|
||||
{
|
||||
@@ -8,6 +9,17 @@ namespace BTCPayServer.Data
|
||||
{
|
||||
var entity = NBitcoin.JsonConverters.Serializer.ToObject<InvoiceEntity>(ZipUtils.Unzip(invoiceData.Blob), null);
|
||||
entity.Networks = networks;
|
||||
if (entity.Metadata is null)
|
||||
{
|
||||
if (entity.Version < InvoiceEntity.GreenfieldInvoices_Version)
|
||||
{
|
||||
entity.MigrateLegacyInvoice();
|
||||
}
|
||||
else
|
||||
{
|
||||
entity.Metadata = new InvoiceMetadata();
|
||||
}
|
||||
}
|
||||
return entity;
|
||||
}
|
||||
public static InvoiceState GetInvoiceState(this InvoiceData invoiceData)
|
||||
|
||||
@@ -195,7 +195,7 @@ namespace BTCPayServer.Data
|
||||
[JsonConverter(typeof(NumericStringJsonConverter))]
|
||||
public decimal MinimumClaim { get; set; }
|
||||
public PullPaymentView View { get; set; } = new PullPaymentView();
|
||||
[JsonConverter(typeof(TimeSpanJsonConverter))]
|
||||
[JsonConverter(typeof(TimeSpanJsonConverter.Seconds))]
|
||||
public TimeSpan? Period { get; set; }
|
||||
|
||||
[JsonProperty(ItemConverterType = typeof(PaymentMethodIdJsonConverter))]
|
||||
|
||||
@@ -3,6 +3,7 @@ using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using BTCPayServer.Client.JsonConverters;
|
||||
using BTCPayServer.Client.Models;
|
||||
using BTCPayServer.JsonConverters;
|
||||
using BTCPayServer.Payments;
|
||||
@@ -19,8 +20,8 @@ namespace BTCPayServer.Data
|
||||
{
|
||||
public StoreBlob()
|
||||
{
|
||||
InvoiceExpiration = 15;
|
||||
MonitoringExpiration = 1440;
|
||||
InvoiceExpiration = TimeSpan.FromMinutes(15);
|
||||
MonitoringExpiration = TimeSpan.FromDays(1);
|
||||
PaymentTolerance = 0;
|
||||
ShowRecommendedFee = true;
|
||||
RecommendedFeeBlockTarget = 1;
|
||||
@@ -66,17 +67,19 @@ namespace BTCPayServer.Data
|
||||
}
|
||||
|
||||
public string DefaultLang { get; set; }
|
||||
[DefaultValue(60)]
|
||||
[DefaultValue(typeof(TimeSpan), "1.00:00:00")]
|
||||
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Populate)]
|
||||
public int MonitoringExpiration
|
||||
[JsonConverter(typeof(TimeSpanJsonConverter.Minutes))]
|
||||
public TimeSpan MonitoringExpiration
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
[DefaultValue(15)]
|
||||
[DefaultValue(typeof(TimeSpan), "00:15:00")]
|
||||
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Populate)]
|
||||
public int InvoiceExpiration
|
||||
[JsonConverter(typeof(TimeSpanJsonConverter.Minutes))]
|
||||
public TimeSpan InvoiceExpiration
|
||||
{
|
||||
get;
|
||||
set;
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using System.Collections.Generic;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
|
||||
namespace BTCPayServer.Events
|
||||
@@ -15,11 +16,26 @@ namespace BTCPayServer.Events
|
||||
public const string FailedToConfirm = "invoice_failedToConfirm";
|
||||
public const string Confirmed = "invoice_confirmed";
|
||||
public const string Completed = "invoice_completed";
|
||||
|
||||
public static Dictionary<string, int> EventCodes = new Dictionary<string, int>()
|
||||
{
|
||||
{Created, 1001},
|
||||
{ReceivedPayment, 1002},
|
||||
{PaidInFull, 1003},
|
||||
{Expired, 1004},
|
||||
{Confirmed, 1005},
|
||||
{Completed, 1006},
|
||||
{MarkedInvalid, 1008},
|
||||
{FailedToConfirm, 1013},
|
||||
{PaidAfterExpiration, 1009},
|
||||
{ExpiredPaidPartial, 2000},
|
||||
{MarkedCompleted, 2008},
|
||||
};
|
||||
|
||||
public InvoiceEvent(InvoiceEntity invoice, int code, string name)
|
||||
public InvoiceEvent(InvoiceEntity invoice, string name)
|
||||
{
|
||||
Invoice = invoice;
|
||||
EventCode = code;
|
||||
EventCode = EventCodes[name];
|
||||
Name = name;
|
||||
}
|
||||
|
||||
|
||||
@@ -104,9 +104,8 @@ namespace BTCPayServer.HostedServices
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
if ((!string.IsNullOrEmpty(invoiceEvent.Invoice.ProductInformation.ItemCode) ||
|
||||
AppService.TryParsePosCartItems(invoiceEvent.Invoice.PosData, out cartItems)))
|
||||
if ((!string.IsNullOrEmpty(invoiceEvent.Invoice.Metadata.ItemCode) ||
|
||||
AppService.TryParsePosCartItems(invoiceEvent.Invoice.Metadata.PosData, out cartItems)))
|
||||
{
|
||||
var appIds = AppService.GetAppInternalTags(invoiceEvent.Invoice);
|
||||
|
||||
@@ -116,9 +115,9 @@ namespace BTCPayServer.HostedServices
|
||||
}
|
||||
|
||||
var items = cartItems ?? new Dictionary<string, int>();
|
||||
if (!string.IsNullOrEmpty(invoiceEvent.Invoice.ProductInformation.ItemCode))
|
||||
if (!string.IsNullOrEmpty(invoiceEvent.Invoice.Metadata.ItemCode))
|
||||
{
|
||||
items.TryAdd(invoiceEvent.Invoice.ProductInformation.ItemCode, 1);
|
||||
items.TryAdd(invoiceEvent.Invoice.Metadata.ItemCode, 1);
|
||||
}
|
||||
|
||||
_eventAggregator.Publish(new UpdateAppInventory()
|
||||
|
||||
@@ -5,6 +5,7 @@ using System.Net.Http;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Client.Models;
|
||||
using BTCPayServer.Events;
|
||||
using BTCPayServer.Payments;
|
||||
using BTCPayServer.Services;
|
||||
|
||||
@@ -4,6 +4,7 @@ using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Channels;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Client.Models;
|
||||
using BTCPayServer.Events;
|
||||
using BTCPayServer.Logging;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
@@ -65,9 +66,9 @@ namespace BTCPayServer.HostedServices
|
||||
await _InvoiceRepository.UnaffectAddress(invoice.Id);
|
||||
|
||||
invoice.Status = InvoiceStatus.Expired;
|
||||
context.Events.Add(new InvoiceEvent(invoice, 1004, InvoiceEvent.Expired));
|
||||
context.Events.Add(new InvoiceEvent(invoice, InvoiceEvent.Expired));
|
||||
if (invoice.ExceptionStatus == InvoiceExceptionStatus.PaidPartial)
|
||||
context.Events.Add(new InvoiceEvent(invoice, 2000, InvoiceEvent.ExpiredPaidPartial));
|
||||
context.Events.Add(new InvoiceEvent(invoice, InvoiceEvent.ExpiredPaidPartial));
|
||||
}
|
||||
|
||||
var payments = invoice.GetPayments().Where(p => p.Accounted).ToArray();
|
||||
@@ -81,7 +82,7 @@ namespace BTCPayServer.HostedServices
|
||||
{
|
||||
if (invoice.Status == InvoiceStatus.New)
|
||||
{
|
||||
context.Events.Add(new InvoiceEvent(invoice, 1003, InvoiceEvent.PaidInFull));
|
||||
context.Events.Add(new InvoiceEvent(invoice, InvoiceEvent.PaidInFull));
|
||||
invoice.Status = InvoiceStatus.Paid;
|
||||
invoice.ExceptionStatus = accounting.Paid > accounting.TotalDue ? InvoiceExceptionStatus.PaidOver : InvoiceExceptionStatus.None;
|
||||
await _InvoiceRepository.UnaffectAddress(invoice.Id);
|
||||
@@ -90,7 +91,7 @@ namespace BTCPayServer.HostedServices
|
||||
else if (invoice.Status == InvoiceStatus.Expired && invoice.ExceptionStatus != InvoiceExceptionStatus.PaidLate)
|
||||
{
|
||||
invoice.ExceptionStatus = InvoiceExceptionStatus.PaidLate;
|
||||
context.Events.Add(new InvoiceEvent(invoice, 1009, InvoiceEvent.PaidAfterExpiration));
|
||||
context.Events.Add(new InvoiceEvent(invoice, InvoiceEvent.PaidAfterExpiration));
|
||||
context.MarkDirty();
|
||||
}
|
||||
}
|
||||
@@ -136,7 +137,7 @@ namespace BTCPayServer.HostedServices
|
||||
(confirmedAccounting.Paid < accounting.MinimumTotalDue))
|
||||
{
|
||||
await _InvoiceRepository.UnaffectAddress(invoice.Id);
|
||||
context.Events.Add(new InvoiceEvent(invoice, 1013, InvoiceEvent.FailedToConfirm));
|
||||
context.Events.Add(new InvoiceEvent(invoice, InvoiceEvent.FailedToConfirm));
|
||||
invoice.Status = InvoiceStatus.Invalid;
|
||||
context.MarkDirty();
|
||||
}
|
||||
@@ -144,7 +145,7 @@ namespace BTCPayServer.HostedServices
|
||||
{
|
||||
await _InvoiceRepository.UnaffectAddress(invoice.Id);
|
||||
invoice.Status = InvoiceStatus.Confirmed;
|
||||
context.Events.Add(new InvoiceEvent(invoice, 1005, InvoiceEvent.Confirmed));
|
||||
context.Events.Add(new InvoiceEvent(invoice, InvoiceEvent.Confirmed));
|
||||
context.MarkDirty();
|
||||
}
|
||||
}
|
||||
@@ -154,7 +155,7 @@ namespace BTCPayServer.HostedServices
|
||||
var completedAccounting = paymentMethod.Calculate(p => p.GetCryptoPaymentData().PaymentCompleted(p));
|
||||
if (completedAccounting.Paid >= accounting.MinimumTotalDue)
|
||||
{
|
||||
context.Events.Add(new InvoiceEvent(invoice, 1006, InvoiceEvent.Completed));
|
||||
context.Events.Add(new InvoiceEvent(invoice, InvoiceEvent.Completed));
|
||||
invoice.Status = InvoiceStatus.Complete;
|
||||
context.MarkDirty();
|
||||
}
|
||||
|
||||
@@ -96,7 +96,7 @@ namespace BTCPayServer.Hosting
|
||||
var dbpath = Path.Combine(opts.DataDir, "InvoiceDB");
|
||||
if (!Directory.Exists(dbpath))
|
||||
Directory.CreateDirectory(dbpath);
|
||||
return new InvoiceRepository(dbContext, dbpath, o.GetRequiredService<BTCPayNetworkProvider>());
|
||||
return new InvoiceRepository(dbContext, dbpath, o.GetRequiredService<BTCPayNetworkProvider>(), o.GetService<EventAggregator>());
|
||||
});
|
||||
services.AddSingleton<BTCPayServerEnvironment>();
|
||||
services.TryAddSingleton<TokenRepository>();
|
||||
|
||||
@@ -2,10 +2,11 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using NBitpayClient;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace BTCPayServer.Models
|
||||
{
|
||||
public class CreateInvoiceRequest
|
||||
public class BitpayCreateInvoiceRequest
|
||||
{
|
||||
[JsonProperty(PropertyName = "buyer")]
|
||||
public Buyer Buyer { get; set; }
|
||||
@@ -1,5 +1,6 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using BTCPayServer.Client.Models;
|
||||
using BTCPayServer.Payments;
|
||||
using BTCPayServer.Payments.Bitcoin;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
@@ -73,22 +74,12 @@ namespace BTCPayServer.Models.InvoicingModels
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
public string OrderId
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
public string RefundEmail
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
public string TaxIncluded { get; set; }
|
||||
public BuyerInformation BuyerInformation
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
public string TransactionSpeed { get; set; }
|
||||
public object StoreName
|
||||
@@ -113,11 +104,7 @@ namespace BTCPayServer.Models.InvoicingModels
|
||||
get;
|
||||
set;
|
||||
}
|
||||
public ProductInformation ProductInformation
|
||||
{
|
||||
get;
|
||||
internal set;
|
||||
}
|
||||
public InvoiceMetadata TypedMetadata { get; set; }
|
||||
public AddressModel[] Addresses { get; set; }
|
||||
public DateTimeOffset MonitoringDate { get; internal set; }
|
||||
public List<Data.InvoiceEventData> Events { get; internal set; }
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using BTCPayServer.Client.Models;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
|
||||
namespace BTCPayServer.Models.InvoicingModels
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Client.Models;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Models.PaymentRequestViewModels;
|
||||
using BTCPayServer.Payments;
|
||||
@@ -9,6 +10,7 @@ using BTCPayServer.Services.Invoices;
|
||||
using BTCPayServer.Services.PaymentRequests;
|
||||
using BTCPayServer.Services.Rates;
|
||||
using Microsoft.AspNetCore.SignalR;
|
||||
using PaymentRequestData = BTCPayServer.Data.PaymentRequestData;
|
||||
|
||||
namespace BTCPayServer.PaymentRequest
|
||||
{
|
||||
@@ -99,9 +101,9 @@ namespace BTCPayServer.PaymentRequest
|
||||
Invoices = invoices.Select(entity => new ViewPaymentRequestViewModel.PaymentRequestInvoice()
|
||||
{
|
||||
Id = entity.Id,
|
||||
Amount = entity.ProductInformation.Price,
|
||||
AmountFormatted = _currencies.FormatCurrency(entity.ProductInformation.Price, blob.Currency),
|
||||
Currency = entity.ProductInformation.Currency,
|
||||
Amount = entity.Price,
|
||||
AmountFormatted = _currencies.FormatCurrency(entity.Price, blob.Currency),
|
||||
Currency = entity.Currency,
|
||||
ExpiryDate = entity.ExpirationTime.DateTime,
|
||||
Status = entity.GetInvoiceState().ToString(),
|
||||
Payments = entity
|
||||
|
||||
@@ -406,7 +406,7 @@ namespace BTCPayServer.Payments.Bitcoin
|
||||
invoice.SetPaymentMethod(paymentMethod);
|
||||
}
|
||||
wallet.InvalidateCache(strategy);
|
||||
_Aggregator.Publish(new InvoiceEvent(invoice, 1002, InvoiceEvent.ReceivedPayment) { Payment = payment });
|
||||
_Aggregator.Publish(new InvoiceEvent(invoice, InvoiceEvent.ReceivedPayment) { Payment = payment });
|
||||
return invoice;
|
||||
}
|
||||
public async Task StopAsync(CancellationToken cancellationToken)
|
||||
|
||||
@@ -48,7 +48,7 @@ namespace BTCPayServer.Payments.Lightning
|
||||
var storeBlob = store.GetStoreBlob();
|
||||
var test = GetNodeInfo(paymentMethod.PreferOnion, supportedPaymentMethod, network);
|
||||
var invoice = paymentMethod.ParentEntity;
|
||||
var due = Extensions.RoundUp(invoice.ProductInformation.Price / paymentMethod.Rate, network.Divisibility);
|
||||
var due = Extensions.RoundUp(invoice.Price / paymentMethod.Rate, network.Divisibility);
|
||||
var client = _lightningClientFactory.Create(supportedPaymentMethod.GetLightningUrl(), network);
|
||||
var expiry = invoice.ExpirationTime - DateTimeOffset.UtcNow;
|
||||
if (expiry < TimeSpan.Zero)
|
||||
@@ -58,8 +58,8 @@ namespace BTCPayServer.Payments.Lightning
|
||||
|
||||
string description = storeBlob.LightningDescriptionTemplate;
|
||||
description = description.Replace("{StoreName}", store.StoreName ?? "", StringComparison.OrdinalIgnoreCase)
|
||||
.Replace("{ItemDescription}", invoice.ProductInformation.ItemDesc ?? "", StringComparison.OrdinalIgnoreCase)
|
||||
.Replace("{OrderId}", invoice.OrderId ?? "", StringComparison.OrdinalIgnoreCase);
|
||||
.Replace("{ItemDescription}", invoice.Metadata.ItemDesc ?? "", StringComparison.OrdinalIgnoreCase)
|
||||
.Replace("{OrderId}", invoice.Metadata.OrderId ?? "", StringComparison.OrdinalIgnoreCase);
|
||||
using (var cts = new CancellationTokenSource(LIGHTNING_TIMEOUT))
|
||||
{
|
||||
try
|
||||
|
||||
@@ -329,7 +329,7 @@ namespace BTCPayServer.Payments.Lightning
|
||||
{
|
||||
var invoice = await invoiceRepository.GetInvoice(invoiceId);
|
||||
if (invoice != null)
|
||||
_eventAggregator.Publish(new InvoiceEvent(invoice, 1002, InvoiceEvent.ReceivedPayment) { Payment = payment });
|
||||
_eventAggregator.Publish(new InvoiceEvent(invoice, InvoiceEvent.ReceivedPayment) { Payment = payment });
|
||||
}
|
||||
return payment != null;
|
||||
}
|
||||
|
||||
@@ -454,7 +454,7 @@ namespace BTCPayServer.Payments.PayJoin
|
||||
$"The original transaction has already been accounted"));
|
||||
}
|
||||
await _btcPayWalletProvider.GetWallet(network).SaveOffchainTransactionAsync(ctx.OriginalTransaction);
|
||||
_eventAggregator.Publish(new InvoiceEvent(invoice, 1002, InvoiceEvent.ReceivedPayment) { Payment = payment });
|
||||
_eventAggregator.Publish(new InvoiceEvent(invoice,InvoiceEvent.ReceivedPayment) { Payment = payment });
|
||||
_eventAggregator.Publish(new UpdateTransactionLabel()
|
||||
{
|
||||
WalletId = new WalletId(invoice.StoreId, network.CryptoCode),
|
||||
|
||||
@@ -64,20 +64,38 @@ namespace BTCPayServer.Payments
|
||||
//BTCLike case is special because it is in legacy mode.
|
||||
return PaymentType == PaymentTypes.BTCLike ? CryptoCode : $"{CryptoCode}_{PaymentType}";
|
||||
}
|
||||
/// <summary>
|
||||
/// A string we can expose to Greenfield API, not subjected to internal legacy
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public string ToStringNormalized()
|
||||
{
|
||||
if (PaymentType == PaymentTypes.BTCLike)
|
||||
return CryptoCode;
|
||||
#if ALTCOINS
|
||||
if (CryptoCode == "XMR" && PaymentType == PaymentTypes.MoneroLike)
|
||||
return CryptoCode;
|
||||
#endif
|
||||
return $"{CryptoCode}-{PaymentType.ToStringNormalized()}";
|
||||
}
|
||||
|
||||
public string ToPrettyString()
|
||||
{
|
||||
return $"{CryptoCode} ({PaymentType.ToPrettyString()})";
|
||||
}
|
||||
|
||||
static char[] Separators = new[] { '_', '-' };
|
||||
public static bool TryParse(string str, out PaymentMethodId paymentMethodId)
|
||||
{
|
||||
str ??= "";
|
||||
paymentMethodId = null;
|
||||
var parts = str.Split('_', StringSplitOptions.RemoveEmptyEntries);
|
||||
var parts = str.Split(Separators, StringSplitOptions.RemoveEmptyEntries);
|
||||
if (parts.Length == 0 || parts.Length > 2)
|
||||
return false;
|
||||
PaymentType type = PaymentTypes.BTCLike;
|
||||
#if ALTCOINS
|
||||
if (parts[0].ToUpperInvariant() == "XMR")
|
||||
type = PaymentTypes.MoneroLike;
|
||||
#endif
|
||||
if (parts.Length == 2)
|
||||
{
|
||||
if (!PaymentTypes.TryParse(parts[1], out type))
|
||||
|
||||
@@ -19,6 +19,10 @@ namespace BTCPayServer.Payments
|
||||
|
||||
public override string ToPrettyString() => "On-Chain";
|
||||
public override string GetId() => "BTCLike";
|
||||
public override string ToStringNormalized()
|
||||
{
|
||||
return "OnChain";
|
||||
}
|
||||
|
||||
public override CryptoPaymentData DeserializePaymentData(BTCPayNetworkBase network, string str)
|
||||
{
|
||||
|
||||
@@ -17,7 +17,10 @@ namespace BTCPayServer.Payments
|
||||
|
||||
public override string ToPrettyString() => "Off-Chain";
|
||||
public override string GetId() => "LightningLike";
|
||||
|
||||
public override string ToStringNormalized()
|
||||
{
|
||||
return "LightningNetwork";
|
||||
}
|
||||
public override CryptoPaymentData DeserializePaymentData(BTCPayNetworkBase network, string str)
|
||||
{
|
||||
return ((BTCPayNetwork)network)?.ToObject<LightningLikePaymentData>(str);
|
||||
|
||||
@@ -22,6 +22,13 @@ namespace BTCPayServer.Payments
|
||||
/// </summary>
|
||||
public static LightningPaymentType LightningLike => LightningPaymentType.Instance;
|
||||
|
||||
#if ALTCOINS
|
||||
/// <summary>
|
||||
/// Monero payment
|
||||
/// </summary>
|
||||
public static MoneroPaymentType MoneroLike => MoneroPaymentType.Instance;
|
||||
#endif
|
||||
|
||||
public static bool TryParse(string paymentType, out PaymentType type)
|
||||
{
|
||||
switch (paymentType.ToLowerInvariant())
|
||||
@@ -36,7 +43,7 @@ namespace BTCPayServer.Payments
|
||||
break;
|
||||
#if ALTCOINS
|
||||
case "monerolike":
|
||||
type = MoneroPaymentType.Instance;
|
||||
type = PaymentTypes.MoneroLike;
|
||||
break;
|
||||
#endif
|
||||
default:
|
||||
@@ -61,6 +68,15 @@ namespace BTCPayServer.Payments
|
||||
return GetId();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A string we can expose to Greenfield API, not subjected to internal legacy
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public virtual string ToStringNormalized()
|
||||
{
|
||||
return ToString();
|
||||
}
|
||||
|
||||
public abstract string GetId();
|
||||
public abstract CryptoPaymentData DeserializePaymentData(BTCPayNetworkBase network, string str);
|
||||
public abstract string SerializePaymentData(BTCPayNetworkBase network, CryptoPaymentData paymentData);
|
||||
|
||||
@@ -14,7 +14,10 @@ namespace BTCPayServer.Services.Altcoins.Monero.Payments
|
||||
public override string ToPrettyString() => "On-Chain";
|
||||
|
||||
public override string GetId() => "MoneroLike";
|
||||
|
||||
public override string ToStringNormalized()
|
||||
{
|
||||
return "Monero";
|
||||
}
|
||||
|
||||
public override CryptoPaymentData DeserializePaymentData(BTCPayNetworkBase network, string str)
|
||||
{
|
||||
|
||||
@@ -143,7 +143,7 @@ namespace BTCPayServer.Services.Altcoins.Monero.Services
|
||||
}
|
||||
|
||||
_eventAggregator.Publish(
|
||||
new InvoiceEvent(invoice, 1002, InvoiceEvent.ReceivedPayment) { Payment = payment });
|
||||
new InvoiceEvent(invoice, InvoiceEvent.ReceivedPayment) { Payment = payment });
|
||||
}
|
||||
|
||||
private async Task UpdatePaymentStates(string cryptoCode, InvoiceEntity[] invoices)
|
||||
|
||||
@@ -4,6 +4,7 @@ using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Client.Models;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Models.AppViewModels;
|
||||
using BTCPayServer.Payments;
|
||||
@@ -20,6 +21,7 @@ using NUglify.Helpers;
|
||||
using YamlDotNet.RepresentationModel;
|
||||
using YamlDotNet.Serialization;
|
||||
using static BTCPayServer.Models.AppViewModels.ViewCrowdfundViewModel;
|
||||
using StoreData = BTCPayServer.Data.StoreData;
|
||||
|
||||
namespace BTCPayServer.Services.Apps
|
||||
{
|
||||
@@ -95,8 +97,8 @@ namespace BTCPayServer.Services.Apps
|
||||
var currentPayments = GetContributionsByPaymentMethodId(settings.TargetCurrency, completeInvoices, !settings.EnforceTargetAmount);
|
||||
|
||||
var perkCount = paidInvoices
|
||||
.Where(entity => !string.IsNullOrEmpty(entity.ProductInformation.ItemCode))
|
||||
.GroupBy(entity => entity.ProductInformation.ItemCode)
|
||||
.Where(entity => !string.IsNullOrEmpty(entity.Metadata.ItemCode))
|
||||
.GroupBy(entity => entity.Metadata.ItemCode)
|
||||
.ToDictionary(entities => entities.Key, entities => entities.Count());
|
||||
|
||||
var perks = Parse(settings.PerksTemplate, settings.TargetCurrency);
|
||||
@@ -329,12 +331,12 @@ namespace BTCPayServer.Services.Apps
|
||||
public Contributions GetContributionsByPaymentMethodId(string currency, InvoiceEntity[] invoices, bool softcap)
|
||||
{
|
||||
var contributions = invoices
|
||||
.Where(p => p.ProductInformation.Currency.Equals(currency, StringComparison.OrdinalIgnoreCase))
|
||||
.Where(p => p.Currency.Equals(currency, StringComparison.OrdinalIgnoreCase))
|
||||
.SelectMany(p =>
|
||||
{
|
||||
var contribution = new Contribution();
|
||||
contribution.PaymentMethodId = new PaymentMethodId(p.ProductInformation.Currency, PaymentTypes.BTCLike);
|
||||
contribution.CurrencyValue = p.ProductInformation.Price;
|
||||
contribution.PaymentMethodId = new PaymentMethodId(p.Currency, PaymentTypes.BTCLike);
|
||||
contribution.CurrencyValue = p.Price;
|
||||
contribution.Value = contribution.CurrencyValue;
|
||||
|
||||
// For hardcap, we count newly created invoices as part of the contributions
|
||||
|
||||
@@ -56,9 +56,8 @@ namespace BTCPayServer.Services.Invoices.Export
|
||||
private IEnumerable<ExportInvoiceHolder> convertFromDb(InvoiceEntity invoice)
|
||||
{
|
||||
var exportList = new List<ExportInvoiceHolder>();
|
||||
var currency = Currencies.GetNumberFormatInfo(invoice.ProductInformation.Currency, true);
|
||||
|
||||
var invoiceDue = invoice.ProductInformation.Price;
|
||||
var currency = Currencies.GetNumberFormatInfo(invoice.Currency, true);
|
||||
var invoiceDue = invoice.Price;
|
||||
// in this first version we are only exporting invoices that were paid
|
||||
foreach (var payment in invoice.GetPayments())
|
||||
{
|
||||
@@ -88,7 +87,7 @@ namespace BTCPayServer.Services.Invoices.Export
|
||||
// while looking just at export you could sum Paid and assume merchant "received payments"
|
||||
NetworkFee = payment.NetworkFee.ToString(CultureInfo.InvariantCulture),
|
||||
InvoiceDue = Math.Round(invoiceDue, currency.NumberDecimalDigits),
|
||||
OrderId = invoice.OrderId,
|
||||
OrderId = invoice.Metadata.OrderId ?? string.Empty,
|
||||
StoreId = invoice.StoreId,
|
||||
InvoiceId = invoice.Id,
|
||||
InvoiceCreatedDate = invoice.InvoiceTime.UtcDateTime,
|
||||
@@ -99,11 +98,11 @@ namespace BTCPayServer.Services.Invoices.Export
|
||||
InvoiceStatus = invoice.StatusString,
|
||||
InvoiceExceptionStatus = invoice.ExceptionStatusString,
|
||||
#pragma warning restore CS0618 // Type or member is obsolete
|
||||
InvoiceItemCode = invoice.ProductInformation.ItemCode,
|
||||
InvoiceItemDesc = invoice.ProductInformation.ItemDesc,
|
||||
InvoicePrice = invoice.ProductInformation.Price,
|
||||
InvoiceCurrency = invoice.ProductInformation.Currency,
|
||||
BuyerEmail = invoice.BuyerInformation?.BuyerEmail
|
||||
InvoiceItemCode = invoice.Metadata.ItemCode,
|
||||
InvoiceItemDesc = invoice.Metadata.ItemDesc,
|
||||
InvoicePrice = invoice.Price,
|
||||
InvoiceCurrency = invoice.Currency,
|
||||
BuyerEmail = invoice.Metadata.BuyerEmail
|
||||
};
|
||||
|
||||
exportList.Add(target);
|
||||
|
||||
@@ -2,6 +2,7 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
using System.Linq;
|
||||
using Amazon.Runtime.Internal.Util;
|
||||
using BTCPayServer.Client.Models;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.JsonConverters;
|
||||
@@ -9,17 +10,31 @@ using BTCPayServer.Models;
|
||||
using BTCPayServer.Payments;
|
||||
using BTCPayServer.Payments.Bitcoin;
|
||||
using Microsoft.AspNetCore.Http.Extensions;
|
||||
using Microsoft.CodeAnalysis;
|
||||
using NBitcoin;
|
||||
using NBitcoin.DataEncoders;
|
||||
using NBitpayClient;
|
||||
using NBXplorer;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using Newtonsoft.Json.Serialization;
|
||||
using YamlDotNet.Core.Tokens;
|
||||
using YamlDotNet.Serialization.NamingConventions;
|
||||
|
||||
namespace BTCPayServer.Services.Invoices
|
||||
{
|
||||
public class BuyerInformation
|
||||
public class InvoiceMetadata
|
||||
{
|
||||
public static readonly JsonSerializer MetadataSerializer;
|
||||
static InvoiceMetadata()
|
||||
{
|
||||
var seria = new JsonSerializer();
|
||||
seria.DefaultValueHandling = DefaultValueHandling.Ignore;
|
||||
seria.FloatParseHandling = FloatParseHandling.Decimal;
|
||||
seria.ContractResolver = new CamelCasePropertyNamesContractResolver();
|
||||
MetadataSerializer = seria;
|
||||
}
|
||||
public string OrderId { get; set; }
|
||||
[JsonProperty(PropertyName = "buyerName")]
|
||||
public string BuyerName
|
||||
{
|
||||
@@ -66,10 +81,7 @@ namespace BTCPayServer.Services.Invoices
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
}
|
||||
|
||||
public class ProductInformation
|
||||
{
|
||||
[JsonProperty(PropertyName = "itemDesc")]
|
||||
public string ItemDesc
|
||||
{
|
||||
@@ -81,35 +93,122 @@ namespace BTCPayServer.Services.Invoices
|
||||
get; set;
|
||||
}
|
||||
[JsonProperty(PropertyName = "physical")]
|
||||
public bool Physical
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
[JsonProperty(PropertyName = "price")]
|
||||
public decimal Price
|
||||
public bool? Physical
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
[JsonProperty(PropertyName = "taxIncluded", DefaultValueHandling = DefaultValueHandling.Ignore)]
|
||||
public decimal TaxIncluded
|
||||
public decimal? TaxIncluded
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
public string PosData { get; set; }
|
||||
[JsonExtensionData]
|
||||
public IDictionary<string, JToken> AdditionalData { get; set; }
|
||||
|
||||
[JsonProperty(PropertyName = "currency")]
|
||||
public string Currency
|
||||
public static InvoiceMetadata FromJObject(JObject jObject)
|
||||
{
|
||||
get; set;
|
||||
return jObject.ToObject<InvoiceMetadata>(MetadataSerializer);
|
||||
}
|
||||
public JObject ToJObject()
|
||||
{
|
||||
return JObject.FromObject(this, MetadataSerializer);
|
||||
}
|
||||
}
|
||||
|
||||
public class InvoiceEntity
|
||||
{
|
||||
class BuyerInformation
|
||||
{
|
||||
[JsonProperty(PropertyName = "buyerName")]
|
||||
public string BuyerName
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
[JsonProperty(PropertyName = "buyerEmail")]
|
||||
public string BuyerEmail
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
[JsonProperty(PropertyName = "buyerCountry")]
|
||||
public string BuyerCountry
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
[JsonProperty(PropertyName = "buyerZip")]
|
||||
public string BuyerZip
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
[JsonProperty(PropertyName = "buyerState")]
|
||||
public string BuyerState
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
[JsonProperty(PropertyName = "buyerCity")]
|
||||
public string BuyerCity
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
[JsonProperty(PropertyName = "buyerAddress2")]
|
||||
public string BuyerAddress2
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
[JsonProperty(PropertyName = "buyerAddress1")]
|
||||
public string BuyerAddress1
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
[JsonProperty(PropertyName = "buyerPhone")]
|
||||
public string BuyerPhone
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
}
|
||||
class ProductInformation
|
||||
{
|
||||
[JsonProperty(PropertyName = "itemDesc")]
|
||||
public string ItemDesc
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
[JsonProperty(PropertyName = "itemCode")]
|
||||
public string ItemCode
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
[JsonProperty(PropertyName = "physical")]
|
||||
public bool Physical
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
[JsonProperty(PropertyName = "price")]
|
||||
public decimal Price
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
[JsonProperty(PropertyName = "taxIncluded", DefaultValueHandling = DefaultValueHandling.Ignore)]
|
||||
public decimal TaxIncluded
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
[JsonProperty(PropertyName = "currency")]
|
||||
public string Currency
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
}
|
||||
[JsonIgnore]
|
||||
public BTCPayNetworkProvider Networks { get; set; }
|
||||
public const int InternalTagSupport_Version = 1;
|
||||
public const int Lastest_Version = 1;
|
||||
public const int GreenfieldInvoices_Version = 2;
|
||||
public const int Lastest_Version = 2;
|
||||
public int Version { get; set; }
|
||||
public string Id
|
||||
{
|
||||
@@ -120,11 +219,6 @@ namespace BTCPayServer.Services.Invoices
|
||||
get; set;
|
||||
}
|
||||
|
||||
public string OrderId
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
public SpeedPolicy SpeedPolicy
|
||||
{
|
||||
get; set;
|
||||
@@ -148,20 +242,20 @@ namespace BTCPayServer.Services.Invoices
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
public ProductInformation ProductInformation
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
public BuyerInformation BuyerInformation
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
public string PosData
|
||||
|
||||
public InvoiceMetadata Metadata
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
|
||||
public decimal Price { get; set; }
|
||||
public string Currency { get; set; }
|
||||
|
||||
[JsonExtensionData]
|
||||
public IDictionary<string, JToken> AdditionalData { get; set; }
|
||||
|
||||
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)]
|
||||
public HashSet<string> InternalTags { get; set; } = new HashSet<string>();
|
||||
|
||||
@@ -300,7 +394,7 @@ namespace BTCPayServer.Services.Invoices
|
||||
|
||||
private Uri FillPlaceholdersUri(string v)
|
||||
{
|
||||
var uriStr = (v ?? string.Empty).Replace("{OrderId}", System.Web.HttpUtility.UrlEncode(OrderId) ?? "", StringComparison.OrdinalIgnoreCase)
|
||||
var uriStr = (v ?? string.Empty).Replace("{OrderId}", System.Web.HttpUtility.UrlEncode(Metadata.OrderId) ?? "", StringComparison.OrdinalIgnoreCase)
|
||||
.Replace("{InvoiceId}", System.Web.HttpUtility.UrlEncode(Id) ?? "", StringComparison.OrdinalIgnoreCase);
|
||||
if (Uri.TryCreate(uriStr, UriKind.Absolute, out var uri) && (uri.Scheme == "http" || uri.Scheme == "https"))
|
||||
return uri;
|
||||
@@ -383,8 +477,8 @@ namespace BTCPayServer.Services.Invoices
|
||||
{
|
||||
Id = Id,
|
||||
StoreId = StoreId,
|
||||
OrderId = OrderId,
|
||||
PosData = PosData,
|
||||
OrderId = Metadata.OrderId,
|
||||
PosData = Metadata.PosData,
|
||||
CurrentTime = DateTimeOffset.UtcNow,
|
||||
InvoiceTime = InvoiceTime,
|
||||
ExpirationTime = ExpirationTime,
|
||||
@@ -392,7 +486,7 @@ namespace BTCPayServer.Services.Invoices
|
||||
Status = StatusString,
|
||||
ExceptionStatus = ExceptionStatus == InvoiceExceptionStatus.None ? new JValue(false) : new JValue(ExceptionStatusString),
|
||||
#pragma warning restore CS0618 // Type or member is obsolete
|
||||
Currency = ProductInformation.Currency,
|
||||
Currency = Currency,
|
||||
Flags = new Flags() { Refundable = Refundable },
|
||||
PaymentSubtotals = new Dictionary<string, decimal>(),
|
||||
PaymentTotals = new Dictionary<string, decimal>(),
|
||||
@@ -415,7 +509,7 @@ namespace BTCPayServer.Services.Invoices
|
||||
var address = details?.GetPaymentDestination();
|
||||
var exrates = new Dictionary<string, decimal>
|
||||
{
|
||||
{ ProductInformation.Currency, cryptoInfo.Rate }
|
||||
{ Currency, cryptoInfo.Rate }
|
||||
};
|
||||
|
||||
cryptoInfo.CryptoCode = cryptoCode;
|
||||
@@ -465,7 +559,7 @@ namespace BTCPayServer.Services.Invoices
|
||||
{
|
||||
var minerInfo = new MinerFeeInfo();
|
||||
minerInfo.TotalFee = accounting.NetworkFee.Satoshi;
|
||||
minerInfo.SatoshiPerBytes = ((BitcoinLikeOnChainPaymentMethod)details).FeeRate
|
||||
minerInfo.SatoshiPerBytes = ((BitcoinLikeOnChainPaymentMethod) details).FeeRate
|
||||
.GetFee(1).Satoshi;
|
||||
dto.MinerFees.TryAdd(cryptoInfo.CryptoCode, minerInfo);
|
||||
cryptoInfo.PaymentUrls = new InvoicePaymentUrls()
|
||||
@@ -502,29 +596,27 @@ namespace BTCPayServer.Services.Invoices
|
||||
|
||||
//dto.AmountPaid dto.MinerFees & dto.TransactionCurrency are not supported by btcpayserver as we have multi currency payment support per invoice
|
||||
|
||||
Populate(ProductInformation, dto);
|
||||
dto.ItemCode = Metadata.ItemCode;
|
||||
dto.ItemDesc = Metadata.ItemDesc;
|
||||
dto.TaxIncluded = Metadata.TaxIncluded ?? 0m;
|
||||
dto.Price = Price;
|
||||
dto.Currency = Currency;
|
||||
dto.Buyer = new JObject();
|
||||
dto.Buyer.Add(new JProperty("name", BuyerInformation.BuyerName));
|
||||
dto.Buyer.Add(new JProperty("address1", BuyerInformation.BuyerAddress1));
|
||||
dto.Buyer.Add(new JProperty("address2", BuyerInformation.BuyerAddress2));
|
||||
dto.Buyer.Add(new JProperty("locality", BuyerInformation.BuyerCity));
|
||||
dto.Buyer.Add(new JProperty("region", BuyerInformation.BuyerState));
|
||||
dto.Buyer.Add(new JProperty("postalCode", BuyerInformation.BuyerZip));
|
||||
dto.Buyer.Add(new JProperty("country", BuyerInformation.BuyerCountry));
|
||||
dto.Buyer.Add(new JProperty("phone", BuyerInformation.BuyerPhone));
|
||||
dto.Buyer.Add(new JProperty("email", string.IsNullOrWhiteSpace(BuyerInformation.BuyerEmail) ? RefundMail : BuyerInformation.BuyerEmail));
|
||||
dto.Buyer.Add(new JProperty("name", Metadata.BuyerName));
|
||||
dto.Buyer.Add(new JProperty("address1", Metadata.BuyerAddress1));
|
||||
dto.Buyer.Add(new JProperty("address2", Metadata.BuyerAddress2));
|
||||
dto.Buyer.Add(new JProperty("locality", Metadata.BuyerCity));
|
||||
dto.Buyer.Add(new JProperty("region", Metadata.BuyerState));
|
||||
dto.Buyer.Add(new JProperty("postalCode", Metadata.BuyerZip));
|
||||
dto.Buyer.Add(new JProperty("country", Metadata.BuyerCountry));
|
||||
dto.Buyer.Add(new JProperty("phone", Metadata.BuyerPhone));
|
||||
dto.Buyer.Add(new JProperty("email", string.IsNullOrWhiteSpace(Metadata.BuyerEmail) ? RefundMail : Metadata.BuyerEmail));
|
||||
|
||||
dto.Token = Encoders.Base58.EncodeData(RandomUtils.GetBytes(16)); //No idea what it is useful for
|
||||
dto.Guid = Guid.NewGuid().ToString();
|
||||
return dto;
|
||||
}
|
||||
|
||||
private void Populate<TFrom, TDest>(TFrom from, TDest dest)
|
||||
{
|
||||
var str = JsonConvert.SerializeObject(from);
|
||||
JsonConvert.PopulateObject(str, dest);
|
||||
}
|
||||
|
||||
internal bool Support(PaymentMethodId paymentMethodId)
|
||||
{
|
||||
var rates = GetPaymentMethods();
|
||||
@@ -602,26 +694,65 @@ namespace BTCPayServer.Services.Invoices
|
||||
{
|
||||
return new InvoiceState(Status, ExceptionStatus);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Invoice version < 1 were saving metadata directly under the InvoiceEntity
|
||||
/// object. But in version > 2, the metadata is saved under the InvoiceEntity.Metadata object
|
||||
/// This method is extracting metadata from the InvoiceEntity of version < 1 invoices and put them in InvoiceEntity.Metadata.
|
||||
/// </summary>
|
||||
internal void MigrateLegacyInvoice()
|
||||
{
|
||||
T TryParseMetadata<T>(string field) where T : class
|
||||
{
|
||||
if (AdditionalData.TryGetValue(field, out var token) && token is JObject obj)
|
||||
{
|
||||
return obj.ToObject<T>();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
if (TryParseMetadata<BuyerInformation>("buyerInformation") is BuyerInformation buyerInformation &&
|
||||
TryParseMetadata<ProductInformation>("productInformation") is ProductInformation productInformation)
|
||||
{
|
||||
var wellknown = new InvoiceMetadata()
|
||||
{
|
||||
BuyerAddress1 = buyerInformation.BuyerAddress1,
|
||||
BuyerAddress2 = buyerInformation.BuyerAddress2,
|
||||
BuyerCity = buyerInformation.BuyerCity,
|
||||
BuyerCountry = buyerInformation.BuyerCountry,
|
||||
BuyerEmail = buyerInformation.BuyerEmail,
|
||||
BuyerName = buyerInformation.BuyerName,
|
||||
BuyerPhone = buyerInformation.BuyerPhone,
|
||||
BuyerState = buyerInformation.BuyerState,
|
||||
BuyerZip = buyerInformation.BuyerZip,
|
||||
ItemCode = productInformation.ItemCode,
|
||||
ItemDesc = productInformation.ItemDesc,
|
||||
Physical = productInformation.Physical,
|
||||
TaxIncluded = productInformation.TaxIncluded
|
||||
};
|
||||
if (AdditionalData.TryGetValue("posData", out var token) &&
|
||||
token is JValue val &&
|
||||
val.Type == JTokenType.String)
|
||||
{
|
||||
wellknown.PosData = val.Value<string>();
|
||||
}
|
||||
if (AdditionalData.TryGetValue("orderId", out var token2) &&
|
||||
token2 is JValue val2 &&
|
||||
val2.Type == JTokenType.String)
|
||||
{
|
||||
wellknown.OrderId = val2.Value<string>();
|
||||
}
|
||||
Metadata = wellknown;
|
||||
Currency = productInformation.Currency;
|
||||
Price = productInformation.Price;
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new InvalidOperationException("Not a legacy invoice");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public enum InvoiceStatus
|
||||
{
|
||||
New,
|
||||
Paid,
|
||||
Expired,
|
||||
Invalid,
|
||||
Complete,
|
||||
Confirmed
|
||||
}
|
||||
public enum InvoiceExceptionStatus
|
||||
{
|
||||
None,
|
||||
PaidLate,
|
||||
PaidPartial,
|
||||
Marked,
|
||||
Invalid,
|
||||
PaidOver
|
||||
}
|
||||
|
||||
public class InvoiceState
|
||||
{
|
||||
static readonly Dictionary<string, InvoiceStatus> _StringToInvoiceStatus;
|
||||
@@ -853,7 +984,7 @@ namespace BTCPayServer.Services.Invoices
|
||||
paymentPredicate = paymentPredicate ?? new Func<PaymentEntity, bool>((p) => true);
|
||||
var paymentMethods = ParentEntity.GetPaymentMethods();
|
||||
|
||||
var totalDue = ParentEntity.ProductInformation.Price / Rate;
|
||||
var totalDue = ParentEntity.Price / Rate;
|
||||
var paid = 0m;
|
||||
var cryptoPaid = 0.0m;
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Client.Models;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Events;
|
||||
using BTCPayServer.Logging;
|
||||
using BTCPayServer.Models.InvoicingModels;
|
||||
using BTCPayServer.Payments;
|
||||
@@ -13,7 +14,9 @@ using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using NBitcoin;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using Encoders = NBitcoin.DataEncoders.Encoders;
|
||||
using InvoiceData = BTCPayServer.Data.InvoiceData;
|
||||
|
||||
namespace BTCPayServer.Services.Invoices
|
||||
{
|
||||
@@ -36,11 +39,12 @@ namespace BTCPayServer.Services.Invoices
|
||||
}
|
||||
|
||||
private readonly ApplicationDbContextFactory _ContextFactory;
|
||||
private readonly EventAggregator _eventAggregator;
|
||||
private readonly BTCPayNetworkProvider _Networks;
|
||||
private readonly CustomThreadPool _IndexerThread;
|
||||
|
||||
public InvoiceRepository(ApplicationDbContextFactory contextFactory, string dbreezePath,
|
||||
BTCPayNetworkProvider networks)
|
||||
BTCPayNetworkProvider networks, EventAggregator eventAggregator)
|
||||
{
|
||||
int retryCount = 0;
|
||||
retry:
|
||||
@@ -52,6 +56,7 @@ retry:
|
||||
_IndexerThread = new CustomThreadPool(1, "Invoice Indexer");
|
||||
_ContextFactory = contextFactory;
|
||||
_Networks = networks;
|
||||
_eventAggregator = eventAggregator;
|
||||
}
|
||||
|
||||
public InvoiceEntity CreateNewInvoice()
|
||||
@@ -61,6 +66,7 @@ retry:
|
||||
Networks = _Networks,
|
||||
Version = InvoiceEntity.Lastest_Version,
|
||||
InvoiceTime = DateTimeOffset.UtcNow,
|
||||
Metadata = new InvoiceMetadata()
|
||||
};
|
||||
}
|
||||
|
||||
@@ -158,11 +164,11 @@ retry:
|
||||
Id = invoice.Id,
|
||||
Created = invoice.InvoiceTime,
|
||||
Blob = ToBytes(invoice, null),
|
||||
OrderId = invoice.OrderId,
|
||||
OrderId = invoice.Metadata.OrderId,
|
||||
#pragma warning disable CS0618 // Type or member is obsolete
|
||||
Status = invoice.StatusString,
|
||||
#pragma warning restore CS0618 // Type or member is obsolete
|
||||
ItemCode = invoice.ProductInformation.ItemCode,
|
||||
ItemCode = invoice.Metadata.ItemCode,
|
||||
CustomerEmail = invoice.RefundMail,
|
||||
Archived = false
|
||||
});
|
||||
@@ -194,10 +200,9 @@ retry:
|
||||
|
||||
textSearch.Add(invoice.Id);
|
||||
textSearch.Add(invoice.InvoiceTime.ToString(CultureInfo.InvariantCulture));
|
||||
textSearch.Add(invoice.ProductInformation.Price.ToString(CultureInfo.InvariantCulture));
|
||||
textSearch.Add(invoice.OrderId);
|
||||
textSearch.Add(ToString(invoice.BuyerInformation, null));
|
||||
textSearch.Add(ToString(invoice.ProductInformation, null));
|
||||
textSearch.Add(invoice.Price.ToString(CultureInfo.InvariantCulture));
|
||||
textSearch.Add(invoice.Metadata.OrderId);
|
||||
textSearch.Add(ToString(invoice.Metadata, null));
|
||||
textSearch.Add(invoice.StoreId);
|
||||
|
||||
AddToTextSearch(invoice.Id, textSearch.ToArray());
|
||||
@@ -427,42 +432,68 @@ retry:
|
||||
}
|
||||
}
|
||||
|
||||
public async Task ToggleInvoiceArchival(string invoiceId, bool archived)
|
||||
public async Task ToggleInvoiceArchival(string invoiceId, bool archived, string storeId = null)
|
||||
{
|
||||
using (var context = _ContextFactory.CreateContext())
|
||||
{
|
||||
var invoiceData = await context.FindAsync<InvoiceData>(invoiceId).ConfigureAwait(false);
|
||||
if (invoiceData == null || invoiceData.Archived == archived)
|
||||
if (invoiceData == null || invoiceData.Archived == archived ||
|
||||
(storeId != null &&
|
||||
!invoiceData.StoreDataId.Equals(storeId, StringComparison.InvariantCultureIgnoreCase)))
|
||||
return;
|
||||
invoiceData.Archived = archived;
|
||||
await context.SaveChangesAsync().ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
public async Task UpdatePaidInvoiceToInvalid(string invoiceId)
|
||||
public async Task<bool> MarkInvoiceStatus(string invoiceId, InvoiceStatus status)
|
||||
{
|
||||
using (var context = _ContextFactory.CreateContext())
|
||||
{
|
||||
var invoiceData = await context.FindAsync<Data.InvoiceData>(invoiceId).ConfigureAwait(false);
|
||||
if (invoiceData == null || !invoiceData.GetInvoiceState().CanMarkInvalid())
|
||||
return;
|
||||
invoiceData.Status = "invalid";
|
||||
invoiceData.ExceptionStatus = "marked";
|
||||
await context.SaveChangesAsync().ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
public async Task UpdatePaidInvoiceToComplete(string invoiceId)
|
||||
{
|
||||
using (var context = _ContextFactory.CreateContext())
|
||||
{
|
||||
var invoiceData = await context.FindAsync<Data.InvoiceData>(invoiceId).ConfigureAwait(false);
|
||||
if (invoiceData == null || !invoiceData.GetInvoiceState().CanMarkComplete())
|
||||
return;
|
||||
invoiceData.Status = "complete";
|
||||
invoiceData.ExceptionStatus = "marked";
|
||||
await context.SaveChangesAsync().ConfigureAwait(false);
|
||||
var invoiceData = await GetInvoiceRaw(invoiceId);
|
||||
if (invoiceData == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
context.Attach(invoiceData);
|
||||
string eventName;
|
||||
switch (status)
|
||||
{
|
||||
case InvoiceStatus.Complete:
|
||||
if (!invoiceData.GetInvoiceState().CanMarkComplete())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
eventName = InvoiceEvent.MarkedCompleted;
|
||||
break;
|
||||
case InvoiceStatus.Invalid:
|
||||
if (!invoiceData.GetInvoiceState().CanMarkInvalid())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
eventName = InvoiceEvent.MarkedInvalid;
|
||||
break;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
|
||||
invoiceData.Status = status.ToString().ToLowerInvariant();
|
||||
invoiceData.ExceptionStatus = InvoiceExceptionStatus.Marked.ToString().ToLowerInvariant();
|
||||
_eventAggregator.Publish(new InvoiceEvent(ToEntity(invoiceData), eventName));
|
||||
await context.SaveChangesAsync();
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public async Task<InvoiceEntity> GetInvoice(string id, bool inludeAddressData = false)
|
||||
{
|
||||
var res = await GetInvoiceRaw(id, inludeAddressData);
|
||||
return res == null ? null : ToEntity(res);
|
||||
}
|
||||
|
||||
private async Task<InvoiceData> GetInvoiceRaw(string id, bool inludeAddressData = false)
|
||||
{
|
||||
using (var context = _ContextFactory.CreateContext())
|
||||
{
|
||||
@@ -478,7 +509,7 @@ retry:
|
||||
if (invoice == null)
|
||||
return null;
|
||||
|
||||
return ToEntity(invoice);
|
||||
return invoice;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -525,10 +556,9 @@ retry:
|
||||
{
|
||||
entity.Events = invoice.Events.OrderBy(c => c.Timestamp).ToList();
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(entity.RefundMail) && string.IsNullOrEmpty(entity.BuyerInformation.BuyerEmail))
|
||||
if (!string.IsNullOrEmpty(entity.RefundMail) && string.IsNullOrEmpty(entity.Metadata.BuyerEmail))
|
||||
{
|
||||
entity.BuyerInformation.BuyerEmail = entity.RefundMail;
|
||||
entity.Metadata.BuyerEmail = entity.RefundMail;
|
||||
}
|
||||
entity.Archived = invoice.Archived;
|
||||
return entity;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
@model InvoiceDetailsModel
|
||||
@model InvoiceDetailsModel
|
||||
@{
|
||||
ViewData["Title"] = "Invoice " + Model.Id;
|
||||
}
|
||||
@@ -93,7 +93,7 @@
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Order Id</th>
|
||||
<td>@Model.OrderId</td>
|
||||
<td>@Model.TypedMetadata.OrderId</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Total fiat due</th>
|
||||
@@ -114,39 +114,39 @@
|
||||
<table class="table table-sm table-responsive-md removetopborder">
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<td>@Model.BuyerInformation.BuyerName</td>
|
||||
<td>@Model.TypedMetadata.BuyerName</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Email</th>
|
||||
<td><a href="mailto:@Model.BuyerInformation.BuyerEmail">@Model.BuyerInformation.BuyerEmail</a></td>
|
||||
<td><a href="mailto:@Model.TypedMetadata.BuyerEmail">@Model.TypedMetadata.BuyerEmail</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Phone</th>
|
||||
<td>@Model.BuyerInformation.BuyerPhone</td>
|
||||
<td>@Model.TypedMetadata.BuyerPhone</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Address 1</th>
|
||||
<td>@Model.BuyerInformation.BuyerAddress1</td>
|
||||
<td>@Model.TypedMetadata.BuyerAddress1</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Address 2</th>
|
||||
<td>@Model.BuyerInformation.BuyerAddress2</td>
|
||||
<td>@Model.TypedMetadata.BuyerAddress2</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>City</th>
|
||||
<td>@Model.BuyerInformation.BuyerCity</td>
|
||||
<td>@Model.TypedMetadata.BuyerCity</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>State</th>
|
||||
<td>@Model.BuyerInformation.BuyerState</td>
|
||||
<td>@Model.TypedMetadata.BuyerState</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Country</th>
|
||||
<td>@Model.BuyerInformation.BuyerCountry</td>
|
||||
<td>@Model.TypedMetadata.BuyerCountry</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Zip</th>
|
||||
<td>@Model.BuyerInformation.BuyerZip</td>
|
||||
<td>@Model.TypedMetadata.BuyerZip</td>
|
||||
</tr>
|
||||
</table>
|
||||
@if (Model.PosData.Count == 0)
|
||||
@@ -155,11 +155,11 @@
|
||||
<table class="table table-sm table-responsive-md removetopborder">
|
||||
<tr>
|
||||
<th>Item code</th>
|
||||
<td>@Model.ProductInformation.ItemCode</td>
|
||||
<td>@Model.TypedMetadata.ItemCode</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Item Description</th>
|
||||
<td>@Model.ProductInformation.ItemDesc</td>
|
||||
<td>@Model.TypedMetadata.ItemDesc</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Price</th>
|
||||
@@ -182,11 +182,11 @@
|
||||
<table class="table table-sm table-responsive-md removetopborder">
|
||||
<tr>
|
||||
<th>Item code</th>
|
||||
<td>@Model.ProductInformation.ItemCode</td>
|
||||
<td>@Model.TypedMetadata.ItemCode</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Item Description</th>
|
||||
<td>@Model.ProductInformation.ItemDesc</td>
|
||||
<td>@Model.TypedMetadata.ItemDesc</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Price</th>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
@model (InvoiceDetailsModel Invoice, bool ShowAddress)
|
||||
@using BTCPayServer.Client.Models
|
||||
@model (InvoiceDetailsModel Invoice, bool ShowAddress)
|
||||
@{ var invoice = Model.Invoice; }
|
||||
@inject PaymentMethodHandlerDictionary PaymentMethodHandlerDictionary
|
||||
|
||||
|
||||
576
BTCPayServer/wwwroot/swagger/v1/swagger.template.invoices.json
Normal file
576
BTCPayServer/wwwroot/swagger/v1/swagger.template.invoices.json
Normal file
@@ -0,0 +1,576 @@
|
||||
{
|
||||
"paths": {
|
||||
"/api/v1/stores/{storeId}/invoices": {
|
||||
"get": {
|
||||
"tags": [
|
||||
"Invoices"
|
||||
],
|
||||
"summary": "Get invoices",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "storeId",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"description": "The store to query",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"description": "View information about the existing invoices",
|
||||
"operationId": "Invoices_GetInvoices",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "list of invoices",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/InvoiceDataList"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"API Key": [
|
||||
"btcpay.store.canviewinvoices"
|
||||
],
|
||||
"Basic": []
|
||||
}
|
||||
]
|
||||
},
|
||||
"post": {
|
||||
"tags": [
|
||||
"Invoices"
|
||||
],
|
||||
"summary": "Create a new invoice",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "storeId",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"description": "The store to query",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"description": "Create a new invoice",
|
||||
"operationId": "Invoices_CreateInvoice",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Information about the new invoice",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/InvoiceData"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "A list of errors that occurred when creating the invoice",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ValidationProblemDetails"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"403": {
|
||||
"description": "If you are authenticated but forbidden to add new invoices"
|
||||
}
|
||||
},
|
||||
"requestBody": {
|
||||
"required": true,
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/CreateInvoiceRequest"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"API Key": [
|
||||
"btcpay.store.cancreateinvoice"
|
||||
],
|
||||
"Basic": []
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"/api/v1/stores/{storeId}/invoices/{invoiceId}": {
|
||||
"get": {
|
||||
"tags": [
|
||||
"Invoices"
|
||||
],
|
||||
"summary": "Get invoice",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "storeId",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"description": "The store to fetch",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "invoiceId",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"description": "The invoice to fetch",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"description": "View information about the specified invoice",
|
||||
"operationId": "Invoices_GetInvoice",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "specified invoice",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/InvoiceData"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"403": {
|
||||
"description": "If you are authenticated but forbidden to view the specified invoie"
|
||||
},
|
||||
"404": {
|
||||
"description": "The key is not found for this invoice"
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"API Key": [
|
||||
"btcpay.store.canviewinvoices"
|
||||
],
|
||||
"Basic": []
|
||||
}
|
||||
]
|
||||
},
|
||||
"delete": {
|
||||
"tags": [
|
||||
"Invoices"
|
||||
],
|
||||
"summary": "Archive invoice",
|
||||
"description": "Archives the specified invoice.",
|
||||
"operationId": "Invoices_ArchiveInvoice",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "storeId",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"description": "The store the invoice belongs to",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "invoiceId",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"description": "The invoice to remove",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "The invoice has been archived"
|
||||
},
|
||||
"400": {
|
||||
"description": "A list of errors that occurred when archiving the invoice",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ValidationProblemDetails"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"403": {
|
||||
"description": "If you are authenticated but forbidden to archive the specified invoice"
|
||||
},
|
||||
"404": {
|
||||
"description": "The key is not found for this invoice"
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"API Key": [
|
||||
"btcpay.store.canmodifystoresettings"
|
||||
],
|
||||
"Basic": []
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"/api/v1/stores/{storeId}/invoices/{invoiceId}/status": {
|
||||
"post": {
|
||||
"tags": [
|
||||
"Invoices"
|
||||
],
|
||||
"summary": "Mark invoice status",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "storeId",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"description": "The store to query",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "invoiceId",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"description": "The invoice to update",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"description": "Mark an invoice as invalid or completed.",
|
||||
"operationId": "Invoices_MarkInvoiceStatus",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "The updated invoice",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/InvoiceData"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "A list of errors that occurred when updating the invoice",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ValidationProblemDetails"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"403": {
|
||||
"description": "If you are authenticated but forbidden to update the invoice"
|
||||
}
|
||||
},
|
||||
"requestBody": {
|
||||
"required": true,
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/MarkInvoiceStatusRequest"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"API Key": [
|
||||
"btcpay.store.canmodifystoresettings"
|
||||
],
|
||||
"Basic": []
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"/api/v1/stores/{storeId}/invoices/{invoiceId}/unarchive": {
|
||||
"post": {
|
||||
"tags": [
|
||||
"Invoices"
|
||||
],
|
||||
"summary": "Unarchive invoice",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "storeId",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"description": "The store to query",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "invoiceId",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"description": "The invoice to update",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"description": "Unarchive an invoice",
|
||||
"operationId": "Invoices_UnarchiveInvoice",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "The unarchived invoice",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/InvoiceData"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "A list of errors that occurred when updating the invoice",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ValidationProblemDetails"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"403": {
|
||||
"description": "If you are authenticated but forbidden to update the invoice"
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"API Key": [
|
||||
"btcpay.store.canmodifystoresettings"
|
||||
],
|
||||
"Basic": []
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"components": {
|
||||
"schemas": {
|
||||
"InvoiceDataList": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/InvoiceData"
|
||||
}
|
||||
},
|
||||
"MarkInvoiceStatusRequest": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"status": {
|
||||
"nullable": false,
|
||||
"description": "Mark an invoice as completed or invalid.",
|
||||
"oneOf": [
|
||||
{
|
||||
"$ref": "#/components/schemas/InvoiceStatusMark"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"AddCustomerEmailRequest": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"email": {
|
||||
"type": "string",
|
||||
"nullable": false,
|
||||
"description": "Sets the customer email, if it was not set before."
|
||||
}
|
||||
}
|
||||
},
|
||||
"InvoiceStatusMark": {
|
||||
"type": "string",
|
||||
"description": "",
|
||||
"x-enumNames": [
|
||||
"Invalid",
|
||||
"Complete"
|
||||
],
|
||||
"enum": [
|
||||
"Invalid",
|
||||
"Complete"
|
||||
]
|
||||
},
|
||||
"InvoiceStatus": {
|
||||
"type": "string",
|
||||
"description": "",
|
||||
"x-enumNames": [
|
||||
"New",
|
||||
"Paid",
|
||||
"Expired",
|
||||
"Invalid",
|
||||
"Complete",
|
||||
"Confirmed"
|
||||
],
|
||||
"enum": [
|
||||
"New",
|
||||
"Paid",
|
||||
"Expired",
|
||||
"Invalid",
|
||||
"Complete",
|
||||
"Confirmed"
|
||||
]
|
||||
},
|
||||
"InvoiceAdditionalStatus": {
|
||||
"type": "string",
|
||||
"description": "An additional status that describes why an invoice is in its current status.",
|
||||
"x-enumNames": [
|
||||
"None",
|
||||
"PaidLate",
|
||||
"PaidPartial",
|
||||
"Marked",
|
||||
"Invalid",
|
||||
"PaidOver"
|
||||
],
|
||||
"enum": [
|
||||
"None",
|
||||
"PaidLate",
|
||||
"PaidPartial",
|
||||
"Marked",
|
||||
"Invalid",
|
||||
"PaidOver"
|
||||
]
|
||||
},
|
||||
"InvoiceData": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/components/schemas/CreateInvoiceRequest"
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string",
|
||||
"description": "The identifier of the invoice"
|
||||
},
|
||||
"createdTime": {
|
||||
"type": "number",
|
||||
"format": "int64",
|
||||
"description": "The creation time of the invoice"
|
||||
},
|
||||
"expirationTime": {
|
||||
"type": "number",
|
||||
"format": "int64",
|
||||
"description": "The expiration time of the invoice"
|
||||
},
|
||||
"monitoringTime": {
|
||||
"type": "number",
|
||||
"format": "int64",
|
||||
"description": "The monitoring time of the invoice"
|
||||
},
|
||||
"status": {
|
||||
"$ref": "#/components/schemas/InvoiceStatus",
|
||||
"description": "The status of the invoice"
|
||||
},
|
||||
"additionalStatus": {
|
||||
"$ref": "#/components/schemas/InvoiceAdditionalStatus",
|
||||
"description": "a secondary status of the invoice"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"CreateInvoiceRequest": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"amount": {
|
||||
"type": "string",
|
||||
"format": "decimal",
|
||||
"description": "The amount of the invoice"
|
||||
},
|
||||
"currency": {
|
||||
"type": "string",
|
||||
"nullable": true,
|
||||
"description": "The currency the invoice will use"
|
||||
},
|
||||
"metadata": {
|
||||
"type": "string",
|
||||
"nullable": true,
|
||||
"description": "Additional information around the invoice that can be supplied."
|
||||
},
|
||||
"checkout": {
|
||||
"nullable": true,
|
||||
"oneOf": [
|
||||
{
|
||||
"$ref": "#/components/schemas/CheckoutOptions"
|
||||
}
|
||||
],
|
||||
"description": "Additional settings to customize the checkout flow"
|
||||
}
|
||||
}
|
||||
},
|
||||
"CheckoutOptions": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"speedPolicy": {
|
||||
"nullable": true,
|
||||
"oneOf": [
|
||||
{
|
||||
"$ref": "#/components/schemas/SpeedPolicy"
|
||||
}
|
||||
],
|
||||
"description": "This is a risk mitigation parameter for the merchant to configure how they want to fulfill orders depending on the number of block confirmations for the transaction made by the consumer on the selected cryptocurrency"
|
||||
},
|
||||
"paymentMethods": {
|
||||
"type": "array",
|
||||
"nullable": true,
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"description": "A specific set of payment methods to use for this invoice (ie. BTC, BTC-LightningNetwork). By default, select all payment methods activated in the store."
|
||||
},
|
||||
"expirationMinutes": {
|
||||
"type": "integer",
|
||||
"nullable": true,
|
||||
"description": "The number of minutes after which an invoice becomes expired. Default to the store's settings. (The default store settings is 15)"
|
||||
},
|
||||
"monitoringMinutes": {
|
||||
"type": "integer",
|
||||
"nullable": true,
|
||||
"description": "The number of minutes after an invoice expired after which we are still monitoring for incoming payments. Default to the store's settings. (The default store settings is 1440, 1 day)"
|
||||
},
|
||||
"paymentTolerance": {
|
||||
"type": "number",
|
||||
"format": "double",
|
||||
"nullable": true,
|
||||
"minimum": 0,
|
||||
"maximum": 100,
|
||||
"description": "A percentage determining whether to count the invoice as paid when the invoice is paid within the specified margin of error. Default to the store's settings. (The default store settings is 100)"
|
||||
}
|
||||
}
|
||||
},
|
||||
"SpeedPolicy": {
|
||||
"type": "string",
|
||||
"description": "",
|
||||
"x-enumNames": [
|
||||
"HighSpeed",
|
||||
"MediumSpeed",
|
||||
"LowSpeed",
|
||||
"LowMediumSpeed"
|
||||
],
|
||||
"enum": [
|
||||
"HighSpeed",
|
||||
"MediumSpeed",
|
||||
"LowSpeed",
|
||||
"LowMediumSpeed"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"tags": [
|
||||
{
|
||||
"name": "Invoices"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -122,7 +122,7 @@
|
||||
}
|
||||
],
|
||||
"description": "View information about the specified payment request",
|
||||
"operationId": "PaymentRequests_GetPaymentRequests",
|
||||
"operationId": "PaymentRequests_GetPaymentRequest",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "specified payment request",
|
||||
@@ -218,7 +218,7 @@
|
||||
"name": "paymentRequestId",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"description": "The payment request to remove",
|
||||
"description": "The payment request to update",
|
||||
"schema": { "type": "string" }
|
||||
}
|
||||
],
|
||||
|
||||
Reference in New Issue
Block a user