GreenField: Payment Requests CRUD (#1430)

* GreenField: Payment Requests CRUD

* fixes

* fix swagger

* fix swag

* rebase fixes

* Add new permissions for payment requests

* Adapt PR to archive

* fix tst

* add to contains policxy

* make decimals returned as string due to avoid shitty language parsing issues

* do not register decimal json converter as global

* fix cultureinfo for json covnerter

* pr changes

* add json convertet test

* fix json test

* fix rebase
This commit is contained in:
Andrew Camilleri 2020-05-19 19:59:23 +02:00 committed by GitHub
parent 338a0f9251
commit 5b3b96b372
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
21 changed files with 953 additions and 127 deletions

View file

@ -0,0 +1,59 @@
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<PaymentRequestData>> GetPaymentRequests(string storeId,
CancellationToken token = default)
{
var response =
await _httpClient.SendAsync(CreateHttpRequest($"api/v1/stores/{storeId}/payment-requests"), token);
return await HandleResponse<IEnumerable<PaymentRequestData>>(response);
}
public virtual async Task<PaymentRequestData> GetPaymentRequest(string storeId, string paymentRequestId,
CancellationToken token = default)
{
var response = await _httpClient.SendAsync(
CreateHttpRequest($"api/v1/stores/{storeId}/payment-requests/{paymentRequestId}"), token);
return await HandleResponse<PaymentRequestData>(response);
}
public virtual async Task ArchivePaymentRequest(string storeId, string paymentRequestId,
CancellationToken token = default)
{
var response = await _httpClient.SendAsync(
CreateHttpRequest($"api/v1/stores/{storeId}/payment-requests/{paymentRequestId}",
method: HttpMethod.Delete), token);
HandleResponse(response);
}
public virtual async Task<PaymentRequestData> CreatePaymentRequest(string storeId,
CreatePaymentRequestRequest request, CancellationToken token = default)
{
if (request == null)
throw new ArgumentNullException(nameof(request));
var response = await _httpClient.SendAsync(
CreateHttpRequest($"api/v1/stores/{storeId}/payment-requests", bodyPayload: request,
method: HttpMethod.Post), token);
return await HandleResponse<PaymentRequestData>(response);
}
public virtual async Task<PaymentRequestData> UpdatePaymentRequest(string storeId, string paymentRequestId,
UpdatePaymentRequestRequest request, CancellationToken token = default)
{
if (request == null)
throw new ArgumentNullException(nameof(request));
var response = await _httpClient.SendAsync(
CreateHttpRequest($"api/v1/stores/{storeId}/payment-requests/{paymentRequestId}", bodyPayload: request,
method: HttpMethod.Put), token);
return await HandleResponse<PaymentRequestData>(response);
}
}
}

View file

@ -0,0 +1,39 @@
using System;
using System.Globalization;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
namespace BTCPayServer.JsonConverters
{
public class DecimalStringJsonConverter : JsonConverter
{
public override bool CanConvert(Type objectType)
{
return (objectType == typeof(decimal) || objectType == typeof(decimal?));
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue,
JsonSerializer serializer)
{
JToken token = JToken.Load(reader);
switch (token.Type)
{
case JTokenType.Float:
case JTokenType.Integer:
case JTokenType.String:
return decimal.Parse(token.ToString(), CultureInfo.InvariantCulture);
case JTokenType.Null when objectType == typeof(decimal?):
return null;
default:
throw new JsonSerializationException("Unexpected token type: " +
token.Type);
}
}
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
if (value != null)
writer.WriteValue(((decimal)value).ToString(CultureInfo.InvariantCulture));
}
}
}

View file

@ -0,0 +1,6 @@
namespace BTCPayServer.Client.Models
{
public class CreatePaymentRequestRequest : PaymentRequestBaseData
{
}
}

View file

@ -1,6 +1,6 @@
namespace BTCPayServer.Client.Models
{
public class CreateStoreRequest : StoreBaseData
public class CreateStoreRequest: StoreBaseData
{
}
}

View file

@ -0,0 +1,21 @@
using System;
using BTCPayServer.JsonConverters;
using Newtonsoft.Json;
namespace BTCPayServer.Client.Models
{
public class PaymentRequestBaseData
{
[JsonProperty(ItemConverterType = typeof(DecimalStringJsonConverter))]
public decimal Amount { get; set; }
public string Currency { get; set; }
public DateTime? ExpiryDate { get; set; }
public string Title { get; set; }
public string Description { get; set; }
public string Email { get; set; }
public string EmbeddedCSS { get; set; }
public string CustomCSSLink { get; set; }
public bool AllowCustomPaymentAmounts { get; set; }
}
}

View file

@ -0,0 +1,19 @@
using System;
namespace BTCPayServer.Client.Models
{
public class PaymentRequestData : PaymentRequestBaseData
{
public PaymentRequestData.PaymentRequestStatus Status { get; set; }
public DateTimeOffset Created { get; set; }
public string Id { get; set; }
public bool Archived { get; set; }
public enum PaymentRequestStatus
{
Pending = 0,
Completed = 1,
Expired = 2
}
}
}

View file

@ -0,0 +1,6 @@
namespace BTCPayServer.Client.Models
{
public class UpdatePaymentRequestRequest : PaymentRequestBaseData
{
}
}

View file

@ -10,6 +10,8 @@ namespace BTCPayServer.Client
public const string CanModifyStoreSettings = "btcpay.store.canmodifystoresettings";
public const string CanViewStoreSettings = "btcpay.store.canviewstoresettings";
public const string CanCreateInvoice = "btcpay.store.cancreateinvoice";
public const string CanViewPaymentRequests = "btcpay.store.canviewpaymentrequests";
public const string CanModifyPaymentRequests = "btcpay.store.canmodifypaymentrequests";
public const string CanModifyProfile = "btcpay.user.canmodifyprofile";
public const string CanViewProfile = "btcpay.user.canviewprofile";
public const string CanCreateUser = "btcpay.server.cancreateuser";
@ -22,6 +24,8 @@ namespace BTCPayServer.Client
yield return CanModifyServerSettings;
yield return CanModifyStoreSettings;
yield return CanViewStoreSettings;
yield return CanViewPaymentRequests;
yield return CanModifyPaymentRequests;
yield return CanModifyProfile;
yield return CanViewProfile;
yield return CanCreateUser;
@ -135,13 +139,19 @@ namespace BTCPayServer.Client
return true;
if (this.Policy == subpolicy)
return true;
if (subpolicy == Policies.CanViewStoreSettings && this.Policy == Policies.CanModifyStoreSettings)
return true;
if (subpolicy == Policies.CanCreateInvoice && this.Policy == Policies.CanModifyStoreSettings)
return true;
if (subpolicy == Policies.CanViewProfile && this.Policy == Policies.CanModifyProfile)
return true;
return false;
switch (subpolicy)
{
case Policies.CanViewStoreSettings when this.Policy == Policies.CanModifyStoreSettings:
case Policies.CanCreateInvoice when this.Policy == Policies.CanModifyStoreSettings:
case Policies.CanViewProfile when this.Policy == Policies.CanModifyProfile:
case Policies.CanModifyPaymentRequests when this.Policy == Policies.CanModifyStoreSettings:
case Policies.CanViewPaymentRequests when this.Policy == Policies.CanModifyStoreSettings:
case Policies.CanViewPaymentRequests when this.Policy == Policies.CanViewStoreSettings:
case Policies.CanViewPaymentRequests when this.Policy == Policies.CanModifyPaymentRequests:
return true;
default:
return false;
}
}
public string StoreId { get; }

View file

@ -9,4 +9,7 @@
<PackageReference Include="Pomelo.EntityFrameworkCore.MySql" Version="3.1.1" />
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="3.1.1" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\BTCPayServer.Client\BTCPayServer.Client.csproj" />
</ItemGroup>
</Project>

View file

@ -1,6 +1,7 @@
using System;
using System;
using System.Collections.Generic;
using System.Text;
using BTCPayServer.Client.Models;
namespace BTCPayServer.Data
{
@ -16,31 +17,9 @@ namespace BTCPayServer.Data
public StoreData StoreData { get; set; }
public PaymentRequestStatus Status { get; set; }
public Client.Models.PaymentRequestData.PaymentRequestStatus Status { get; set; }
public byte[] Blob { get; set; }
public class PaymentRequestBlob
{
public decimal Amount { get; set; }
public string Currency { get; set; }
public DateTime? ExpiryDate { get; set; }
public string Title { get; set; }
public string Description { get; set; }
public string Email { get; set; }
public string EmbeddedCSS { get; set; }
public string CustomCSSLink { get; set; }
public bool AllowCustomPaymentAmounts { get; set; }
}
public enum PaymentRequestStatus
{
Pending = 0,
Completed = 1,
Expired = 2
}
}
}

View file

@ -1,14 +1,19 @@
using System;
using System.IO;
using System.Linq;
using System.Net.Http;
using System.Threading.Tasks;
using BTCPayServer.Client;
using BTCPayServer.Client.Models;
using BTCPayServer.JsonConverters;
using BTCPayServer.Services;
using BTCPayServer.Tests.Logging;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Xunit;
using Xunit.Abstractions;
using CreateApplicationUserRequest = BTCPayServer.Client.Models.CreateApplicationUserRequest;
using JsonReader = Newtonsoft.Json.JsonReader;
namespace BTCPayServer.Tests
{
@ -20,7 +25,7 @@ namespace BTCPayServer.Tests
public GreenfieldAPITests(ITestOutputHelper helper)
{
Logs.Tester = new XUnitLog(helper) { Name = "Tests" };
Logs.Tester = new XUnitLog(helper) {Name = "Tests"};
Logs.LogProvider = new XUnitLogProvider(helper);
}
@ -41,10 +46,10 @@ namespace BTCPayServer.Tests
Assert.NotNull(apiKeyData);
Assert.Equal(client.APIKey, apiKeyData.ApiKey);
Assert.Single(apiKeyData.Permissions);
//a client using Basic Auth has no business here
await AssertHttpError(401, async () => await clientBasic.GetCurrentAPIKeyInfo());
//revoke current api key
await client.RevokeCurrentAPIKeyInfo();
await AssertHttpError(401, async () => await client.GetCurrentAPIKeyInfo());
@ -52,6 +57,7 @@ namespace BTCPayServer.Tests
await AssertHttpError(401, async () => await clientBasic.RevokeCurrentAPIKeyInfo());
}
}
[Fact(Timeout = TestTimeout)]
[Trait("Integration", "Integration")]
public async Task CanCreateAndDeleteAPIKeyViaAPI()
@ -65,18 +71,19 @@ namespace BTCPayServer.Tests
var apiKey = await unrestricted.CreateAPIKey(new CreateApiKeyRequest()
{
Label = "Hello world",
Permissions = new Permission[] { Permission.Create(Policies.CanViewProfile) }
Permissions = new Permission[] {Permission.Create(Policies.CanViewProfile)}
});
Assert.Equal("Hello world", apiKey.Label);
var p = Assert.Single(apiKey.Permissions);
Assert.Equal(Policies.CanViewProfile, p.Policy);
var restricted = acc.CreateClientFromAPIKey(apiKey.ApiKey);
await AssertHttpError(403, async () => await restricted.CreateAPIKey(new CreateApiKeyRequest()
{
Label = "Hello world2",
Permissions = new Permission[] { Permission.Create(Policies.CanViewProfile) }
}));
await AssertHttpError(403,
async () => await restricted.CreateAPIKey(new CreateApiKeyRequest()
{
Label = "Hello world2",
Permissions = new Permission[] {Permission.Create(Policies.CanViewProfile)}
}));
await unrestricted.RevokeAPIKey(apiKey.ApiKey);
await AssertHttpError(404, async () => await unrestricted.RevokeAPIKey(apiKey.ApiKey));
@ -92,35 +99,54 @@ namespace BTCPayServer.Tests
tester.PayTester.DisableRegistration = true;
await tester.StartAsync();
var unauthClient = new BTCPayServerClient(tester.PayTester.ServerUri);
await AssertHttpError(400, async () => await unauthClient.CreateUser(new CreateApplicationUserRequest()));
await AssertHttpError(400, async () => await unauthClient.CreateUser(new CreateApplicationUserRequest() { Email = "test@gmail.com" }));
await AssertHttpError(400,
async () => await unauthClient.CreateUser(new CreateApplicationUserRequest()));
await AssertHttpError(400,
async () => await unauthClient.CreateUser(
new CreateApplicationUserRequest() {Email = "test@gmail.com"}));
// Pass too simple
await AssertHttpError(400, async () => await unauthClient.CreateUser(new CreateApplicationUserRequest() { Email = "test3@gmail.com", Password = "a" }));
await AssertHttpError(400,
async () => await unauthClient.CreateUser(
new CreateApplicationUserRequest() {Email = "test3@gmail.com", Password = "a"}));
// We have no admin, so it should work
var user1 = await unauthClient.CreateUser(new CreateApplicationUserRequest() { Email = "test@gmail.com", Password = "abceudhqw" });
var user1 = await unauthClient.CreateUser(
new CreateApplicationUserRequest() {Email = "test@gmail.com", Password = "abceudhqw"});
// We have no admin, so it should work
var user2 = await unauthClient.CreateUser(new CreateApplicationUserRequest() { Email = "test2@gmail.com", Password = "abceudhqw" });
var user2 = await unauthClient.CreateUser(
new CreateApplicationUserRequest() {Email = "test2@gmail.com", Password = "abceudhqw"});
// Duplicate email
await AssertHttpError(400, async () => await unauthClient.CreateUser(new CreateApplicationUserRequest() { Email = "test2@gmail.com", Password = "abceudhqw" }));
await AssertHttpError(400,
async () => await unauthClient.CreateUser(
new CreateApplicationUserRequest() {Email = "test2@gmail.com", Password = "abceudhqw"}));
// Let's make an admin
var admin = await unauthClient.CreateUser(new CreateApplicationUserRequest() { Email = "admin@gmail.com", Password = "abceudhqw", IsAdministrator = true });
var admin = await unauthClient.CreateUser(new CreateApplicationUserRequest()
{
Email = "admin@gmail.com", Password = "abceudhqw", IsAdministrator = true
});
// Creating a new user without proper creds is now impossible (unauthorized)
// Because if registration are locked and that an admin exists, we don't accept unauthenticated connection
await AssertHttpError(401, async () => await unauthClient.CreateUser(new CreateApplicationUserRequest() { Email = "test3@gmail.com", Password = "afewfoiewiou" }));
await AssertHttpError(401,
async () => await unauthClient.CreateUser(
new CreateApplicationUserRequest() {Email = "test3@gmail.com", Password = "afewfoiewiou"}));
// But should be ok with subscriptions unlocked
var settings = tester.PayTester.GetService<SettingsRepository>();
await settings.UpdateSetting<PoliciesSettings>(new PoliciesSettings() { LockSubscription = false });
await unauthClient.CreateUser(new CreateApplicationUserRequest() { Email = "test3@gmail.com", Password = "afewfoiewiou" });
await settings.UpdateSetting<PoliciesSettings>(new PoliciesSettings() {LockSubscription = false});
await unauthClient.CreateUser(
new CreateApplicationUserRequest() {Email = "test3@gmail.com", Password = "afewfoiewiou"});
// But it should be forbidden to create an admin without being authenticated
await AssertHttpError(403, async () => await unauthClient.CreateUser(new CreateApplicationUserRequest() { Email = "admin2@gmail.com", Password = "afewfoiewiou", IsAdministrator = true }));
await settings.UpdateSetting<PoliciesSettings>(new PoliciesSettings() { LockSubscription = true });
await AssertHttpError(403,
async () => await unauthClient.CreateUser(new CreateApplicationUserRequest()
{
Email = "admin2@gmail.com", Password = "afewfoiewiou", IsAdministrator = true
}));
await settings.UpdateSetting<PoliciesSettings>(new PoliciesSettings() {LockSubscription = true});
var adminAcc = tester.NewAccount();
adminAcc.UserId = admin.Id;
@ -128,32 +154,49 @@ namespace BTCPayServer.Tests
var adminClient = await adminAcc.CreateClient(Policies.CanModifyProfile);
// We should be forbidden to create a new user without proper admin permissions
await AssertHttpError(403, async () => await adminClient.CreateUser(new CreateApplicationUserRequest() { Email = "test4@gmail.com", Password = "afewfoiewiou" }));
await AssertHttpError(403, async () => await adminClient.CreateUser(new CreateApplicationUserRequest() { Email = "test4@gmail.com", Password = "afewfoiewiou", IsAdministrator = true }));
await AssertHttpError(403,
async () => await adminClient.CreateUser(
new CreateApplicationUserRequest() {Email = "test4@gmail.com", Password = "afewfoiewiou"}));
await AssertHttpError(403,
async () => await adminClient.CreateUser(new CreateApplicationUserRequest()
{
Email = "test4@gmail.com", Password = "afewfoiewiou", IsAdministrator = true
}));
// However, should be ok with the unrestricted permissions of an admin
adminClient = await adminAcc.CreateClient(Policies.Unrestricted);
await adminClient.CreateUser(new CreateApplicationUserRequest() { Email = "test4@gmail.com", Password = "afewfoiewiou" });
await adminClient.CreateUser(
new CreateApplicationUserRequest() {Email = "test4@gmail.com", Password = "afewfoiewiou"});
// Even creating new admin should be ok
await adminClient.CreateUser(new CreateApplicationUserRequest() { Email = "admin4@gmail.com", Password = "afewfoiewiou", IsAdministrator = true });
await adminClient.CreateUser(new CreateApplicationUserRequest()
{
Email = "admin4@gmail.com", Password = "afewfoiewiou", IsAdministrator = true
});
var user1Acc = tester.NewAccount();
user1Acc.UserId = user1.Id;
user1Acc.IsAdmin = false;
var user1Client = await user1Acc.CreateClient(Policies.CanModifyServerSettings);
// User1 trying to get server management would still fail to create user
await AssertHttpError(403, async () => await user1Client.CreateUser(new CreateApplicationUserRequest() { Email = "test8@gmail.com", Password = "afewfoiewiou" }));
await AssertHttpError(403,
async () => await user1Client.CreateUser(
new CreateApplicationUserRequest() {Email = "test8@gmail.com", Password = "afewfoiewiou"}));
// User1 should be able to create user if subscription unlocked
await settings.UpdateSetting<PoliciesSettings>(new PoliciesSettings() { LockSubscription = false });
await user1Client.CreateUser(new CreateApplicationUserRequest() { Email = "test8@gmail.com", Password = "afewfoiewiou" });
await settings.UpdateSetting<PoliciesSettings>(new PoliciesSettings() {LockSubscription = false});
await user1Client.CreateUser(
new CreateApplicationUserRequest() {Email = "test8@gmail.com", Password = "afewfoiewiou"});
// But not an admin
await AssertHttpError(403, async () => await user1Client.CreateUser(new CreateApplicationUserRequest() { Email = "admin8@gmail.com", Password = "afewfoiewiou", IsAdministrator = true }));
await AssertHttpError(403,
async () => await user1Client.CreateUser(new CreateApplicationUserRequest()
{
Email = "admin8@gmail.com", Password = "afewfoiewiou", IsAdministrator = true
}));
}
}
[Fact(Timeout = TestTimeout)]
[Trait("Integration", "Integration")]
public async Task StoresControllerTests()
@ -165,15 +208,15 @@ namespace BTCPayServer.Tests
user.GrantAccess();
await user.MakeAdmin();
var client = await user.CreateClient(Policies.Unrestricted);
//create store
var newStore = await client.CreateStore(new CreateStoreRequest() {Name = "A"});
//update store
var updatedStore = await client.UpdateStore(newStore.Id, new UpdateStoreRequest() {Name = "B"});
Assert.Equal("B", updatedStore.Name);
Assert.Equal("B", (await client.GetStore(newStore.Id)).Name);
//list stores
var stores = await client.GetStores();
var storeIds = stores.Select(data => data.Id);
@ -185,9 +228,9 @@ namespace BTCPayServer.Tests
//get store
var store = await client.GetStore(user.StoreId);
Assert.Equal(user.StoreId,store.Id);
Assert.Contains(store.Name,storeNames);
Assert.Equal(user.StoreId, store.Id);
Assert.Contains(store.Name, storeNames);
//remove store
await client.RemoveStore(newStore.Id);
await AssertHttpError(403, async () =>
@ -195,13 +238,14 @@ namespace BTCPayServer.Tests
await client.GetStore(newStore.Id);
});
Assert.Single(await client.GetStores());
newStore = await client.CreateStore(new CreateStoreRequest() {Name = "A"});
var scopedClient = await user.CreateClient(Permission.Create(Policies.CanViewStoreSettings, user.StoreId).ToString());
var scopedClient =
await user.CreateClient(Permission.Create(Policies.CanViewStoreSettings, user.StoreId).ToString());
Assert.Single(await scopedClient.GetStores());
}
}
private async Task AssertHttpError(int code, Func<Task> act)
{
var ex = await Assert.ThrowsAsync<HttpRequestException>(act);
@ -235,45 +279,40 @@ namespace BTCPayServer.Tests
await clientProfile.GetCurrentUser();
await clientBasic.GetCurrentUser();
await Assert.ThrowsAsync<HttpRequestException>(async () => await clientInsufficient.CreateUser(new CreateApplicationUserRequest()
{
Email = $"{Guid.NewGuid()}@g.com",
Password = Guid.NewGuid().ToString()
}));
await Assert.ThrowsAsync<HttpRequestException>(async () =>
await clientInsufficient.CreateUser(new CreateApplicationUserRequest()
{
Email = $"{Guid.NewGuid()}@g.com", Password = Guid.NewGuid().ToString()
}));
var newUser = await clientServer.CreateUser(new CreateApplicationUserRequest()
{
Email = $"{Guid.NewGuid()}@g.com",
Password = Guid.NewGuid().ToString()
Email = $"{Guid.NewGuid()}@g.com", Password = Guid.NewGuid().ToString()
});
Assert.NotNull(newUser);
var newUser2 = await clientBasic.CreateUser(new CreateApplicationUserRequest()
{
Email = $"{Guid.NewGuid()}@g.com",
Password = Guid.NewGuid().ToString()
Email = $"{Guid.NewGuid()}@g.com", Password = Guid.NewGuid().ToString()
});
Assert.NotNull(newUser2);
await Assert.ThrowsAsync<HttpRequestException>(async () => await clientServer.CreateUser(new CreateApplicationUserRequest()
{
Email = $"{Guid.NewGuid()}",
Password = Guid.NewGuid().ToString()
}));
await Assert.ThrowsAsync<HttpRequestException>(async () =>
await clientServer.CreateUser(new CreateApplicationUserRequest()
{
Email = $"{Guid.NewGuid()}", Password = Guid.NewGuid().ToString()
}));
await Assert.ThrowsAsync<HttpRequestException>(async () => await clientServer.CreateUser(new CreateApplicationUserRequest()
{
Email = $"{Guid.NewGuid()}@g.com",
}));
await Assert.ThrowsAsync<HttpRequestException>(async () => await clientServer.CreateUser(new CreateApplicationUserRequest()
{
Password = Guid.NewGuid().ToString()
}));
await Assert.ThrowsAsync<HttpRequestException>(async () =>
await clientServer.CreateUser(
new CreateApplicationUserRequest() {Email = $"{Guid.NewGuid()}@g.com",}));
await Assert.ThrowsAsync<HttpRequestException>(async () =>
await clientServer.CreateUser(
new CreateApplicationUserRequest() {Password = Guid.NewGuid().ToString()}));
}
}
[Fact(Timeout = TestTimeout)]
[Trait("Integration", "Integration")]
public async Task HealthControllerTests()
@ -288,7 +327,7 @@ namespace BTCPayServer.Tests
Assert.True(apiHealthData.Synchronized);
}
}
[Fact(Timeout = TestTimeout)]
[Trait("Integration", "Integration")]
public async Task ServerInfoControllerTests()
@ -303,19 +342,115 @@ namespace BTCPayServer.Tests
user.GrantAccess();
var clientBasic = await user.CreateClient();
var serverInfoData = await clientBasic.GetServerInfo();
Assert.NotNull(serverInfoData);
Assert.NotNull(serverInfoData.Version);
Assert.NotNull(serverInfoData.Onion);
Assert.NotNull(serverInfoData.Status);
Assert.True(serverInfoData.Status.FullySynched);
Assert.Contains("BTC", serverInfoData.SupportedPaymentMethods);
Assert.Contains("BTC_LightningLike", serverInfoData.SupportedPaymentMethods);
Assert.NotNull(serverInfoData.Status.SyncStatus);
Assert.Single(serverInfoData.Status.SyncStatus.Select(s => s.CryptoCode == "BTC"));
}
}
[Fact(Timeout = TestTimeout)]
[Trait("Integration", "Integration")]
public async Task PaymentControllerTests()
{
using (var tester = ServerTester.Create())
{
await tester.StartAsync();
var user = tester.NewAccount();
user.GrantAccess();
await user.MakeAdmin();
var client = await user.CreateClient(Policies.Unrestricted);
var viewOnly = await user.CreateClient(Policies.CanViewPaymentRequests);
//create payment request
//validation errors
await AssertHttpError(400, async () =>
{
await client.CreatePaymentRequest(user.StoreId, new CreatePaymentRequestRequest() {Title = "A"});
});
await AssertHttpError(400, async () =>
{
await client.CreatePaymentRequest(user.StoreId,
new CreatePaymentRequestRequest() {Title = "A", Currency = "BTC", Amount = 0});
});
await AssertHttpError(400, async () =>
{
await client.CreatePaymentRequest(user.StoreId,
new CreatePaymentRequestRequest() {Title = "A", Currency = "helloinvalid", Amount = 1});
});
await AssertHttpError(403, async () =>
{
await viewOnly.CreatePaymentRequest(user.StoreId,
new CreatePaymentRequestRequest() {Title = "A", Currency = "helloinvalid", Amount = 1});
});
var newPaymentRequest = await client.CreatePaymentRequest(user.StoreId,
new CreatePaymentRequestRequest() {Title = "A", Currency = "USD", Amount = 1});
//list payment request
var paymentRequests = await viewOnly.GetPaymentRequests(user.StoreId);
Assert.NotNull(paymentRequests);
Assert.Single(paymentRequests);
Assert.Equal(newPaymentRequest.Id, paymentRequests.First().Id);
//get payment request
var paymentRequest = await viewOnly.GetPaymentRequest(user.StoreId, newPaymentRequest.Id);
Assert.Equal(newPaymentRequest.Title, paymentRequest.Title);
//update payment request
var updateRequest = JObject.FromObject(paymentRequest).ToObject<UpdatePaymentRequestRequest>();
updateRequest.Title = "B";
await AssertHttpError(403, async () =>
{
await viewOnly.UpdatePaymentRequest(user.StoreId, paymentRequest.Id, updateRequest);
});
await client.UpdatePaymentRequest(user.StoreId, paymentRequest.Id, updateRequest);
paymentRequest = await client.GetPaymentRequest(user.StoreId, newPaymentRequest.Id);
Assert.Equal(updateRequest.Title, paymentRequest.Title);
//archive payment request
await AssertHttpError(403, async () =>
{
await viewOnly.ArchivePaymentRequest(user.StoreId, paymentRequest.Id);
});
await client.ArchivePaymentRequest(user.StoreId, paymentRequest.Id);
Assert.DoesNotContain(paymentRequest.Id,
(await client.GetPaymentRequests(user.StoreId)).Select(data => data.Id));
}
}
[Fact(Timeout = TestTimeout)]
[Trait("Fast", "Fast")]
public async Task DecimalStringJsonConverterTests()
{
JsonReader Get(string val)
{
return new JsonTextReader(new StringReader(val));
}
var jsonConverter = new DecimalStringJsonConverter();
Assert.True(jsonConverter.CanConvert(typeof(decimal)));
Assert.True(jsonConverter.CanConvert(typeof(decimal?)));
Assert.False(jsonConverter.CanConvert(typeof(double)));
Assert.False(jsonConverter.CanConvert(typeof(float)));
Assert.False(jsonConverter.CanConvert(typeof(int)));
Assert.False(jsonConverter.CanConvert(typeof(string)));
var numberJson = "1";
var numberDecimalJson = "1.2";
var stringJson = "\"1.2\"";
Assert.Equal(1m, jsonConverter.ReadJson(Get(numberJson), typeof(decimal), null, null));
Assert.Equal(1.2m, jsonConverter.ReadJson(Get(numberDecimalJson), typeof(decimal), null, null));
Assert.Null(jsonConverter.ReadJson(Get("null"), typeof(decimal?), null, null));
Assert.Throws<JsonSerializationException>(() =>
{
jsonConverter.ReadJson(Get("null"), typeof(decimal), 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));
}
}
}

View file

@ -0,0 +1,164 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using BTCPayServer.Client;
using BTCPayServer.Client.Models;
using BTCPayServer.Data;
using BTCPayServer.Security;
using BTCPayServer.Services.PaymentRequests;
using BTCPayServer.Services.Rates;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using PaymentRequestData = BTCPayServer.Data.PaymentRequestData;
namespace BTCPayServer.Controllers.GreenField
{
[ApiController]
[Authorize(AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
public class GreenFieldPaymentRequestsController : ControllerBase
{
private readonly PaymentRequestRepository _paymentRequestRepository;
private readonly CurrencyNameTable _currencyNameTable;
public GreenFieldPaymentRequestsController(PaymentRequestRepository paymentRequestRepository,
CurrencyNameTable currencyNameTable)
{
_paymentRequestRepository = paymentRequestRepository;
_currencyNameTable = currencyNameTable;
}
[Authorize(Policy = Policies.CanViewPaymentRequests, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
[HttpGet("~/api/v1/stores/{storeId}/payment-requests")]
public async Task<ActionResult<IEnumerable<PaymentRequestData>>> GetPaymentRequests(string storeId)
{
var prs = await _paymentRequestRepository.FindPaymentRequests(
new PaymentRequestQuery() {StoreId = storeId, IncludeArchived = false});
return Ok(prs.Items.Select(FromModel));
}
[Authorize(Policy = Policies.CanViewPaymentRequests, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
[HttpGet("~/api/v1/stores/{storeId}/payment-requests/{paymentRequestId}")]
public async Task<ActionResult<PaymentRequestData>> GetPaymentRequest(string storeId, string paymentRequestId)
{
var pr = await _paymentRequestRepository.FindPaymentRequests(
new PaymentRequestQuery() {StoreId = storeId, Ids = new[] {paymentRequestId}});
if (pr.Total == 0)
{
return NotFound();
}
return Ok(FromModel(pr.Items.First()));
}
[Authorize(Policy = Policies.CanModifyPaymentRequests,
AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
[HttpDelete("~/api/v1/stores/{storeId}/payment-requests/{paymentRequestId}")]
public async Task<ActionResult> ArchivePaymentRequest(string storeId, string paymentRequestId)
{
var pr = await _paymentRequestRepository.FindPaymentRequests(
new PaymentRequestQuery() {StoreId = storeId, Ids = new[] {paymentRequestId}, IncludeArchived = false});
if (pr.Total == 0)
{
return NotFound();
}
var updatedPr = pr.Items.First();
updatedPr.Archived = true;
await _paymentRequestRepository.CreateOrUpdatePaymentRequest(updatedPr);
return Ok();
}
[HttpPost("~/api/v1/stores/{storeId}/payment-requests")]
[Authorize(Policy = Policies.CanModifyPaymentRequests,
AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
public async Task<IActionResult> CreatePaymentRequest(string storeId,
CreatePaymentRequestRequest request)
{
var validationResult = Validate(request);
if (validationResult != null)
{
return validationResult;
}
var pr = new PaymentRequestData()
{
StoreDataId = storeId,
Status = Client.Models.PaymentRequestData.PaymentRequestStatus.Pending,
Created = DateTimeOffset.Now
};
pr.SetBlob(request);
pr = await _paymentRequestRepository.CreateOrUpdatePaymentRequest(pr);
return Ok(FromModel(pr));
}
[HttpPut("~/api/v1/stores/{storeId}/payment-requests/{paymentRequestId}")]
[Authorize(Policy = Policies.CanModifyPaymentRequests,
AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
public async Task<IActionResult> UpdatePaymentRequest(string storeId,
string paymentRequestId, [FromBody] UpdatePaymentRequestRequest request)
{
var validationResult = Validate(request);
if (validationResult != null)
{
return validationResult;
}
var pr = await _paymentRequestRepository.FindPaymentRequests(
new PaymentRequestQuery() {StoreId = storeId, Ids = new[] {paymentRequestId}});
if (pr.Total == 0)
{
return NotFound();
}
var updatedPr = pr.Items.First();
updatedPr.SetBlob(request);
return Ok(FromModel(await _paymentRequestRepository.CreateOrUpdatePaymentRequest(updatedPr)));
}
private IActionResult Validate(PaymentRequestBaseData data)
{
if (data is null)
return BadRequest();
if (data.Amount <= 0)
{
ModelState.AddModelError(nameof(data.Amount), "Please provide an amount greater than 0");
}
if (string.IsNullOrEmpty(data.Currency) ||
_currencyNameTable.GetCurrencyData(data.Currency, false) == null)
ModelState.AddModelError(nameof(data.Currency), "Invalid currency");
if (string.IsNullOrEmpty(data.Title))
ModelState.AddModelError(nameof(data.Title), "Title is required");
if (!string.IsNullOrEmpty(data.CustomCSSLink) && data.CustomCSSLink.Length > 500)
ModelState.AddModelError(nameof(data.CustomCSSLink), "CustomCSSLink is 500 chars max");
return !ModelState.IsValid ? BadRequest(new ValidationProblemDetails(ModelState)) : null;
}
private static Client.Models.PaymentRequestData FromModel(PaymentRequestData data)
{
var blob = data.GetBlob();
return new Client.Models.PaymentRequestData()
{
Created = data.Created,
Id = data.Id,
Status = data.Status,
Archived = data.Archived,
Amount = blob.Amount,
Currency = blob.Currency,
Description = blob.Description,
Title = blob.Title,
ExpiryDate = blob.ExpiryDate,
Email = blob.Email,
AllowCustomPaymentAmounts = blob.AllowCustomPaymentAmounts,
EmbeddedCSS = blob.EmbeddedCSS,
CustomCSSLink = blob.CustomCSSLink
};
}
}
}

View file

@ -355,7 +355,7 @@ namespace BTCPayServer.Controllers
{
{BTCPayServer.Client.Policies.Unrestricted, ("Unrestricted access", "The app will have unrestricted access to your account.")},
{BTCPayServer.Client.Policies.CanCreateUser, ("Create new users", "The app will be able to create new users on this server.")},
{BTCPayServer.Client.Policies.CanModifyStoreSettings, ("Modify your stores", "The app will be able to create, view and modify, delete and create new invoices on the all your stores.")},
{BTCPayServer.Client.Policies.CanModifyStoreSettings, ("Modify your stores", "The app will be able to view, modify, delete and create new invoices on all your stores.")},
{$"{BTCPayServer.Client.Policies.CanModifyStoreSettings}:", ("Manage selected stores", "The app will be able to view, modify, delete and create new invoices on the selected stores.")},
{BTCPayServer.Client.Policies.CanViewStoreSettings, ("View your stores", "The app will be able to view stores settings.")},
{$"{BTCPayServer.Client.Policies.CanViewStoreSettings}:", ("View your stores", "The app will be able to view the selected stores' settings.")},
@ -364,6 +364,10 @@ 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.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.")},
{$"{BTCPayServer.Client.Policies.CanViewPaymentRequests}:", ("View your payment requests", "The app will be able to view the selected stores' payment requests.")},
};
public string Title
{

View file

@ -1,22 +1,20 @@
using NBitcoin;
using BTCPayServer.Client.Models;
using NBXplorer;
using NBXplorer.DerivationStrategy;
using Newtonsoft.Json.Linq;
using static BTCPayServer.Data.PaymentRequestData;
namespace BTCPayServer.Data
{
public static class PaymentRequestDataExtensions
{
public static PaymentRequestBlob GetBlob(this PaymentRequestData paymentRequestData)
public static PaymentRequestBaseData GetBlob(this PaymentRequestData paymentRequestData)
{
var result = paymentRequestData.Blob == null
? new PaymentRequestBlob()
: JObject.Parse(ZipUtils.Unzip(paymentRequestData.Blob)).ToObject<PaymentRequestBlob>();
? new PaymentRequestBaseData()
: JObject.Parse(ZipUtils.Unzip(paymentRequestData.Blob)).ToObject<PaymentRequestBaseData>();
return result;
}
public static bool SetBlob(this PaymentRequestData paymentRequestData, PaymentRequestBlob blob)
public static bool SetBlob(this PaymentRequestData paymentRequestData, PaymentRequestBaseData blob)
{
var original = new Serializer(null).ToString(paymentRequestData.GetBlob());
var newBlob = new Serializer(null).ToString(blob);

View file

@ -20,6 +20,7 @@ using BTCPayServer.Security;
using Microsoft.AspNetCore.Server.Kestrel.Core;
using Microsoft.Net.Http.Headers;
using System.Net;
using BTCPayServer.JsonConverters;
using BTCPayServer.PaymentRequest;
using BTCPayServer.Services.Apps;
using BTCPayServer.Storage;

View file

@ -1,10 +1,12 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using BTCPayServer.Client.Models;
using BTCPayServer.Data;
using BTCPayServer.Services.PaymentRequests;
using BTCPayServer.Services.Rates;
using Microsoft.AspNetCore.Mvc.Rendering;
using PaymentRequestData = BTCPayServer.Data.PaymentRequestData;
namespace BTCPayServer.Models.PaymentRequestViewModels
{
@ -98,14 +100,14 @@ namespace BTCPayServer.Models.PaymentRequestViewModels
EmbeddedCSS = $"<style>{EmbeddedCSS}</style>";
switch (data.Status)
{
case PaymentRequestData.PaymentRequestStatus.Pending:
case Client.Models.PaymentRequestData.PaymentRequestStatus.Pending:
Status = ExpiryDate.HasValue ? $"Expires on {ExpiryDate.Value:g}" : "Pending";
IsPending = true;
break;
case PaymentRequestData.PaymentRequestStatus.Completed:
case Client.Models.PaymentRequestData.PaymentRequestStatus.Completed:
Status = "Settled";
break;
case PaymentRequestData.PaymentRequestStatus.Expired:
case Client.Models.PaymentRequestData.PaymentRequestStatus.Expired:
Status = "Expired";
break;
default:

View file

@ -16,6 +16,7 @@ using BTCPayServer.Data;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Routing;
using Microsoft.AspNetCore.Builder;
using PaymentRequestData = BTCPayServer.Client.Models.PaymentRequestData;
namespace BTCPayServer.PaymentRequest
{
@ -123,7 +124,7 @@ namespace BTCPayServer.PaymentRequest
Logs.PayServer.LogInformation("Starting payment request expiration watcher");
var (total, items) = await _PaymentRequestRepository.FindPaymentRequests(new PaymentRequestQuery()
{
Status = new[] {PaymentRequestData.PaymentRequestStatus.Pending}
Status = new[] {Client.Models.PaymentRequestData.PaymentRequestStatus.Pending}
}, cancellationToken);
Logs.PayServer.LogInformation($"{total} pending payment requests being checked since last run");
@ -172,7 +173,8 @@ namespace BTCPayServer.PaymentRequest
await InfoUpdated(updated.PaymentRequestId);
var expiry = updated.Data.GetBlob().ExpiryDate;
if (updated.Data.Status == PaymentRequestData.PaymentRequestStatus.Pending &&
if (updated.Data.Status ==
PaymentRequestData.PaymentRequestStatus.Pending &&
expiry.HasValue)
{
QueueExpiryTask(

View file

@ -48,16 +48,16 @@ namespace BTCPayServer.PaymentRequest
if (blob.ExpiryDate.HasValue)
{
if (blob.ExpiryDate.Value <= DateTimeOffset.UtcNow)
currentStatus = PaymentRequestData.PaymentRequestStatus.Expired;
currentStatus = Client.Models.PaymentRequestData.PaymentRequestStatus.Expired;
}
else if (pr.Status == PaymentRequestData.PaymentRequestStatus.Pending)
else if (pr.Status == Client.Models.PaymentRequestData.PaymentRequestStatus.Pending)
{
var rateRules = pr.StoreData.GetStoreBlob().GetRateRules(_BtcPayNetworkProvider);
var invoices = await _PaymentRequestRepository.GetInvoicesForPaymentRequest(pr.Id);
var contributions = _AppService.GetContributionsByPaymentMethodId(blob.Currency, invoices, true);
if (contributions.TotalCurrency >= blob.Amount)
{
currentStatus = PaymentRequestData.PaymentRequestStatus.Completed;
currentStatus = Client.Models.PaymentRequestData.PaymentRequestStatus.Completed;
}
}

View file

@ -42,6 +42,8 @@ namespace BTCPayServer.Security.GreenField
success = context.HasPermission(Permission.Create(requirement.Policy));
break;
case Policies.CanViewPaymentRequests:
case Policies.CanModifyPaymentRequests:
case Policies.CanViewStoreSettings:
case Policies.CanModifyStoreSettings:
var storeId = _HttpContext.GetImplicitStoreId();

View file

@ -1,5 +1,4 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
@ -77,7 +76,7 @@ namespace BTCPayServer.Services.PaymentRequests
}
}
public async Task UpdatePaymentRequestStatus(string paymentRequestId, PaymentRequestData.PaymentRequestStatus status, CancellationToken cancellationToken = default)
public async Task UpdatePaymentRequestStatus(string paymentRequestId, Client.Models.PaymentRequestData.PaymentRequestStatus status, CancellationToken cancellationToken = default)
{
using (var context = _ContextFactory.CreateContext())
{
@ -102,7 +101,7 @@ namespace BTCPayServer.Services.PaymentRequests
if (!string.IsNullOrEmpty(query.StoreId))
{
queryable = queryable.Where(data =>
data.StoreDataId.Equals(query.StoreId, StringComparison.InvariantCulture));
data.StoreDataId == query.StoreId);
}
if (query.Status != null && query.Status.Any())
@ -110,7 +109,13 @@ namespace BTCPayServer.Services.PaymentRequests
queryable = queryable.Where(data =>
query.Status.Contains(data.Status));
}
if (query.Ids != null && query.Ids.Any())
{
queryable = queryable.Where(data =>
query.Ids.Contains(data.Id));
}
if (!string.IsNullOrEmpty(query.UserId))
{
queryable = queryable.Where(i =>
@ -181,10 +186,11 @@ namespace BTCPayServer.Services.PaymentRequests
public class PaymentRequestQuery
{
public string StoreId { get; set; }
public bool IncludeArchived { get; set; } = true;
public PaymentRequestData.PaymentRequestStatus[] Status{ get; set; }
public bool IncludeArchived { get; set; } = true;
public Client.Models.PaymentRequestData.PaymentRequestStatus[] Status{ get; set; }
public string UserId { get; set; }
public int? Skip { get; set; }
public int? Count { get; set; }
public string[] Ids { get; set; }
}
}

View file

@ -0,0 +1,370 @@
{
"paths": {
"/api/v1/stores/{storeId}/payment-requests": {
"get": {
"tags": [
"Payment Requests"
],
"summary": "Get payment requests",
"parameters": [
{
"name": "storeId",
"in": "path",
"required": true,
"description": "The store to query",
"schema": { "type": "string" }
}
],
"description": "View information about the existing payment requests",
"operationId": "PaymentRequests_GetPaymentRequests",
"responses": {
"200": {
"description": "list of payment requests",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/PaymentRequestDataList"
}
}
}
}
},
"security": [
{
"API Key": [
"btcpay.store.canviewpaymentrequests"
],
"Basic": []
}
]
},
"post": {
"tags": [
"Payment Requests"
],
"summary": "Create a new payment request",
"parameters": [
{
"name": "storeId",
"in": "path",
"required": true,
"description": "The store to query",
"schema": { "type": "string" }
}
],
"description": "Create a new payment request",
"operationId": "PaymentRequests_CreatePaymentRequest",
"responses": {
"200": {
"description": "Information about the new payment request",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/PaymentRequestData"
}
}
}
},
"400": {
"description": "A list of errors that occurred when creating the payment request",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ValidationProblemDetails"
}
}
}
},
"403": {
"description": "If you are authenticated but forbidden to add new payment requests"
}
},
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/PaymentRequestBaseData"
}
}
}
},
"security": [
{
"API Key": [
"btcpay.store.canmodifypaymentrequests"
],
"Basic": []
}
]
}
},
"/api/v1/stores/{storeId}/payment-requests/{paymentRequestId}": {
"get": {
"tags": [
"Payment Requests"
],
"summary": "Get payment request",
"parameters": [
{
"name": "storeId",
"in": "path",
"required": true,
"description": "The store to fetch",
"schema": { "type": "string" }
},
{
"name": "paymentRequestId",
"in": "path",
"required": true,
"description": "The payment request to fetch",
"schema": { "type": "string" }
}
],
"description": "View information about the specified payment request",
"operationId": "PaymentRequests_GetPaymentRequests",
"responses": {
"200": {
"description": "specified payment request",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/PaymentRequestData"
}
}
}
},
"403": {
"description": "If you are authenticated but forbidden to view the specified payment request"
},
"404": {
"description": "The key is not found for this payment request"
}
},
"security": [
{
"API Key": [
"btcpay.store.canviewpaymentrequests"
],
"Basic": []
}
]
},
"delete": {
"tags": [
"Payment Requests"
],
"summary": "Archive payment request",
"description": "Archives the specified payment request.",
"operationId": "PaymentRequests_ArchivePaymentRequest",
"parameters": [
{
"name": "storeId",
"in": "path",
"required": true,
"description": "The store the payment request belongs to",
"schema": { "type": "string" }
},
{
"name": "paymentRequestId",
"in": "path",
"required": true,
"description": "The payment request to remove",
"schema": { "type": "string" }
}
],
"responses": {
"200": {
"description": "The payment request has been archived"
},
"400": {
"description": "A list of errors that occurred when archiving the payment request",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ValidationProblemDetails"
}
}
}
},
"403": {
"description": "If you are authenticated but forbidden to archive the specified payment request"
},
"404": {
"description": "The key is not found for this payment request"
}
},
"security": [
{
"API Key": [ "btcpay.store.canmodifypaymentrequests"],
"Basic": []
}
]
},
"put": {
"tags": [
"Payment Requests"
],
"summary": "Update payment request",
"parameters": [
{
"name": "storeId",
"in": "path",
"required": true,
"description": "The store to query",
"schema": { "type": "string" }
},
{
"name": "paymentRequestId",
"in": "path",
"required": true,
"description": "The payment request to remove",
"schema": { "type": "string" }
}
],
"description": "Update a payment request",
"operationId": "PaymentRequests_UpdatePaymentRequest",
"responses": {
"200": {
"description": "The updated payment request",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/PaymentRequestData"
}
}
}
},
"400": {
"description": "A list of errors that occurred when updating the payment request",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ValidationProblemDetails"
}
}
}
},
"403": {
"description": "If you are authenticated but forbidden to update the payment request"
}
},
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/PaymentRequestBaseData"
}
}
}
},
"security": [
{
"API Key": [
"btcpay.store.canmodifypaymentrequests"
],
"Basic": []
}
]
}
}
},
"components": {
"schemas": {
"PaymentRequestDataList": {
"type": "array",
"items": {
"$ref": "#/components/schemas/PaymentRequestData"
}
},
"PaymentRequestData": {
"type": "object",
"additionalProperties": {
"$ref": "#/components/schemas/PaymentRequestBaseData"
},
"properties": {
"id": {
"type": "string",
"description": "The id of the payment request",
"nullable": false
},
"status": {
"type": "string",
"enum": [ "Pending", "Completed" ,"Expired"],
"description": "The status of the payment request",
"nullable": false
},
"created": {
"type": "string",
"description": "The creation date of the payment request",
"nullable": false,
"format": "date-time"
}
}
},
"PaymentRequestBaseData":{
"type": "object",
"additionalProperties": false,
"properties": {
"amount": {
"type": "string",
"format": "decimal",
"minimum": 0,
"exclusiveMinimum": true,
"description": "The amount of the payment request",
"nullable": false
},
"currency": {
"type": "string",
"format": "ISO 4217 Currency code(BTC, EUR, USD, etc)",
"description": "The currency of the payment request",
"nullable": false
},
"email": {
"type": "string",
"description": "The email used in invoices generated by the payment request",
"nullable": true,
"format": "email"
},
"description": {
"type": "string",
"description": "The description of the payment request",
"nullable": true,
"format": "html"
},
"expiryDate": {
"type": "string",
"description": "The expiry date of the payment request",
"nullable": true,
"format": "date-time"
},
"embeddedCSS": {
"type": "string",
"description": "Custom CSS styling for the payment request",
"nullable": true,
"format": "css",
"maximum": 500
},
"customCSSLink": {
"type": "string",
"description": "Custom CSS link for styling the payment request",
"nullable": true,
"format": "uri"
},
"allowCustomPaymentAmounts": {
"type": "boolean",
"description": "Whether to allow users to create invoices that partially pay the payment request ",
"nullable": true
}
}
}
}
},
"tags": [
{
"name": "Payment Requests"
}
]
}