mirror of
https://github.com/btcpayserver/btcpayserver.git
synced 2025-02-20 13:34:37 +01:00
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:
parent
338a0f9251
commit
5b3b96b372
21 changed files with 953 additions and 127 deletions
59
BTCPayServer.Client/BTCPayServerClient.PaymentRequests.cs
Normal file
59
BTCPayServer.Client/BTCPayServerClient.PaymentRequests.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
namespace BTCPayServer.Client.Models
|
||||
{
|
||||
public class CreatePaymentRequestRequest : PaymentRequestBaseData
|
||||
{
|
||||
}
|
||||
}
|
|
@ -1,6 +1,6 @@
|
|||
namespace BTCPayServer.Client.Models
|
||||
{
|
||||
public class CreateStoreRequest : StoreBaseData
|
||||
public class CreateStoreRequest: StoreBaseData
|
||||
{
|
||||
}
|
||||
}
|
||||
|
|
21
BTCPayServer.Client/Models/PaymentRequestBaseData.cs
Normal file
21
BTCPayServer.Client/Models/PaymentRequestBaseData.cs
Normal 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; }
|
||||
}
|
||||
}
|
19
BTCPayServer.Client/Models/PaymentRequestData.cs
Normal file
19
BTCPayServer.Client/Models/PaymentRequestData.cs
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
namespace BTCPayServer.Client.Models
|
||||
{
|
||||
public class UpdatePaymentRequestRequest : PaymentRequestBaseData
|
||||
{
|
||||
}
|
||||
}
|
|
@ -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; }
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
164
BTCPayServer/Controllers/GreenField/PaymentRequestsController.cs
Normal file
164
BTCPayServer/Controllers/GreenField/PaymentRequestsController.cs
Normal 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
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
{
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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; }
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
]
|
||||
}
|
Loading…
Add table
Reference in a new issue