mirror of
https://github.com/aljazceru/btcpayserver.git
synced 2025-12-18 14:34:23 +01:00
Add endpoint for creating POS app
This commit is contained in:
23
BTCPayServer.Client/BTCPayServerClient.Apps.cs
Normal file
23
BTCPayServer.Client/BTCPayServerClient.Apps.cs
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
using System;
|
||||||
|
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<PointOfSaleAppData> CreatePointOfSaleApp(string storeId,
|
||||||
|
CreatePointOfSaleAppRequest request, CancellationToken token = default)
|
||||||
|
{
|
||||||
|
if (request == null)
|
||||||
|
throw new ArgumentNullException(nameof(request));
|
||||||
|
var response = await _httpClient.SendAsync(
|
||||||
|
CreateHttpRequest($"api/v1/stores/{storeId}/apps/pos", bodyPayload: request,
|
||||||
|
method: HttpMethod.Post), token);
|
||||||
|
return await HandleResponse<PointOfSaleAppData>(response);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,8 +2,28 @@ namespace BTCPayServer.Client.Models
|
|||||||
{
|
{
|
||||||
public class CreateAppRequest
|
public class CreateAppRequest
|
||||||
{
|
{
|
||||||
public string StoreId { get; set; }
|
|
||||||
public string AppName { get; set; }
|
public string AppName { get; set; }
|
||||||
public string AppType { get; set; }
|
public string AppType { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public class CreatePointOfSaleAppRequest : CreateAppRequest
|
||||||
|
{
|
||||||
|
public string Currency { get; set; } = null;
|
||||||
|
public string Title { get; set; } = null;
|
||||||
|
public string Description { get; set; } = null;
|
||||||
|
public string Template { get; set; } = null;
|
||||||
|
public string DefaultView { get; set; } = null;
|
||||||
|
public bool ShowCustomAmount { get; set; } = true;
|
||||||
|
public bool ShowDiscount { get; set; } = true;
|
||||||
|
public bool EnableTips { get; set; } = true;
|
||||||
|
public string CustomAmountPayButtonText { get; set; } = null;
|
||||||
|
public string FixedAmountPayButtonText { get; set; } = null;
|
||||||
|
public string TipText { get; set; } = null;
|
||||||
|
public string CustomCSSLink { get; set; } = null;
|
||||||
|
public string NotificationUrl { get; set; } = null;
|
||||||
|
public string RedirectUrl { get; set; } = null;
|
||||||
|
public bool? RedirectAutomatically { get; set; } = null;
|
||||||
|
public bool? RequiresRefundEmail { get; set; } = null;
|
||||||
|
public string EmbeddedCSS { get; set; } = null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
20
BTCPayServer.Client/Models/PointOfSaleAppData.cs
Normal file
20
BTCPayServer.Client/Models/PointOfSaleAppData.cs
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
using System;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
|
||||||
|
namespace BTCPayServer.Client.Models
|
||||||
|
{
|
||||||
|
public class AppDataBase
|
||||||
|
{
|
||||||
|
public string Id { get; set; }
|
||||||
|
public string AppType { get; set; }
|
||||||
|
public string Name { get; set; }
|
||||||
|
public string StoreId { get; set; }
|
||||||
|
[JsonConverter(typeof(NBitcoin.JsonConverters.DateTimeToUnixTimeConverter))]
|
||||||
|
public DateTimeOffset Created { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class PointOfSaleAppData : AppDataBase
|
||||||
|
{
|
||||||
|
// We can add POS specific things here later
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -174,6 +174,22 @@ namespace BTCPayServer.Tests
|
|||||||
await AssertAPIError("apikey-not-found", () => unrestricted.RevokeAPIKey(apiKey.ApiKey));
|
await AssertAPIError("apikey-not-found", () => unrestricted.RevokeAPIKey(apiKey.ApiKey));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact(Timeout = TestTimeout)]
|
||||||
|
[Trait("Integration", "Integration")]
|
||||||
|
public async Task CanCreatePointOfSaleAppViaAPI()
|
||||||
|
{
|
||||||
|
using var tester = CreateServerTester();
|
||||||
|
await tester.StartAsync();
|
||||||
|
var user = tester.NewAccount();
|
||||||
|
await user.RegisterDerivationSchemeAsync("BTC");
|
||||||
|
var client = await user.CreateClient();
|
||||||
|
var app = await client.CreatePointOfSaleApp(user.StoreId, new CreatePointOfSaleAppRequest() { AppName = "test app from API" });
|
||||||
|
|
||||||
|
Assert.Equal("test app from API", app.Name);
|
||||||
|
Assert.Equal(user.StoreId, app.StoreId);
|
||||||
|
Assert.Equal("PointOfSale", app.AppType);
|
||||||
|
}
|
||||||
|
|
||||||
[Fact(Timeout = TestTimeout)]
|
[Fact(Timeout = TestTimeout)]
|
||||||
[Trait("Integration", "Integration")]
|
[Trait("Integration", "Integration")]
|
||||||
public async Task CanDeleteUsersViaApi()
|
public async Task CanDeleteUsersViaApi()
|
||||||
|
|||||||
@@ -34,54 +34,75 @@ namespace BTCPayServer.Controllers.Greenfield
|
|||||||
_storeRepository = storeRepository;
|
_storeRepository = storeRepository;
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpPost("~/api/v1/apps")]
|
[HttpPost("~/api/v1/stores/{storeId}/apps/pos")]
|
||||||
[Authorize(Policy = Policies.CanModifyStoreSettingsUnscoped, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
||||||
public async Task<IActionResult> CreateApp(CreateAppRequest request)
|
public async Task<IActionResult> CreatePointOfSaleApp(string storeId, CreatePointOfSaleAppRequest request)
|
||||||
{
|
{
|
||||||
var validationResult = await Validate(request);
|
var validationResult = Validate(request);
|
||||||
if (validationResult != null)
|
if (validationResult != null)
|
||||||
{
|
{
|
||||||
return validationResult;
|
return validationResult;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var defaultCurrency = (await _storeRepository.FindStore(storeId)).GetStoreBlob().DefaultCurrency;
|
||||||
var appData = new AppData
|
var appData = new AppData
|
||||||
{
|
{
|
||||||
StoreDataId = request.StoreId,
|
StoreDataId = storeId,
|
||||||
Name = request.AppName,
|
Name = request.AppName,
|
||||||
AppType = request.AppType
|
AppType = AppType.PointOfSale.ToString()
|
||||||
};
|
};
|
||||||
|
|
||||||
var defaultCurrency = (await _storeRepository.FindStore(request.StoreId)).GetStoreBlob().DefaultCurrency;
|
Enum.TryParse(request.DefaultView, out PosViewType defaultView);
|
||||||
Enum.TryParse(request.AppType, out AppType appType);
|
|
||||||
switch (appType)
|
appData.SetSettings(new PointOfSaleSettings
|
||||||
{
|
{
|
||||||
case AppType.Crowdfund:
|
Title = request.Title,
|
||||||
var emptyCrowdfund = new CrowdfundSettings { TargetCurrency = defaultCurrency };
|
DefaultView = defaultView,
|
||||||
appData.SetSettings(emptyCrowdfund);
|
ShowCustomAmount = request.ShowCustomAmount,
|
||||||
break;
|
ShowDiscount = request.ShowDiscount,
|
||||||
case AppType.PointOfSale:
|
EnableTips = request.EnableTips,
|
||||||
var empty = new PointOfSaleSettings { Currency = defaultCurrency };
|
Currency = request.Currency ?? defaultCurrency,
|
||||||
appData.SetSettings(empty);
|
Template = request.Template,
|
||||||
break;
|
ButtonText = request.FixedAmountPayButtonText,
|
||||||
}
|
CustomButtonText = request.CustomAmountPayButtonText,
|
||||||
|
CustomTipText = request.TipText,
|
||||||
|
CustomCSSLink = request.CustomCSSLink,
|
||||||
|
NotificationUrl = request.NotificationUrl,
|
||||||
|
RedirectUrl = request.RedirectUrl,
|
||||||
|
Description = request.Description,
|
||||||
|
EmbeddedCSS = request.EmbeddedCSS,
|
||||||
|
RedirectAutomatically = request.RedirectAutomatically,
|
||||||
|
RequiresRefundEmail = request.RequiresRefundEmail == true ?
|
||||||
|
RequiresRefundEmail.On :
|
||||||
|
request.RequiresRefundEmail == false ?
|
||||||
|
RequiresRefundEmail.Off :
|
||||||
|
RequiresRefundEmail.InheritFromStore,
|
||||||
|
});
|
||||||
|
|
||||||
await _appService.UpdateOrCreateApp(appData);
|
await _appService.UpdateOrCreateApp(appData);
|
||||||
|
|
||||||
return Ok(appData);
|
return Ok(ToModel(appData));
|
||||||
}
|
}
|
||||||
|
|
||||||
async private Task<IActionResult?> Validate(CreateAppRequest request)
|
private PointOfSaleAppData ToModel(AppData appData)
|
||||||
|
{
|
||||||
|
return new PointOfSaleAppData
|
||||||
|
{
|
||||||
|
Id = appData.Id,
|
||||||
|
AppType = appData.AppType,
|
||||||
|
Name = appData.Name,
|
||||||
|
StoreId = appData.StoreDataId,
|
||||||
|
Created = appData.Created
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private IActionResult? Validate(CreateAppRequest request)
|
||||||
{
|
{
|
||||||
if (request is null)
|
if (request is null)
|
||||||
{
|
{
|
||||||
return BadRequest();
|
return BadRequest();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!Enum.TryParse(request.AppType, out AppType appType))
|
|
||||||
{
|
|
||||||
ModelState.AddModelError(nameof(request.AppType), "Invalid app type");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (string.IsNullOrEmpty(request.AppName))
|
if (string.IsNullOrEmpty(request.AppName))
|
||||||
{
|
{
|
||||||
ModelState.AddModelError(nameof(request.AppName), "App name is missing");
|
ModelState.AddModelError(nameof(request.AppName), "App name is missing");
|
||||||
@@ -91,12 +112,6 @@ namespace BTCPayServer.Controllers.Greenfield
|
|||||||
ModelState.AddModelError(nameof(request.AppName), "Name can only be between 1 and 50 characters");
|
ModelState.AddModelError(nameof(request.AppName), "Name can only be between 1 and 50 characters");
|
||||||
}
|
}
|
||||||
|
|
||||||
var store = await _storeRepository.FindStore(request.StoreId);
|
|
||||||
if (store == null)
|
|
||||||
{
|
|
||||||
ModelState.AddModelError(nameof(request.StoreId), "Store with provided ID not found");
|
|
||||||
}
|
|
||||||
|
|
||||||
return !ModelState.IsValid ? this.CreateValidationError(ModelState) : null;
|
return !ModelState.IsValid ? this.CreateValidationError(ModelState) : null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
221
BTCPayServer/wwwroot/swagger/v1/swagger.template.apps.json
Normal file
221
BTCPayServer/wwwroot/swagger/v1/swagger.template.apps.json
Normal file
@@ -0,0 +1,221 @@
|
|||||||
|
{
|
||||||
|
"paths": {
|
||||||
|
"/api/v1/stores/{storeId}/apps/pos": {
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"name": "storeId",
|
||||||
|
"in": "path",
|
||||||
|
"required": true,
|
||||||
|
"description": "The store ID",
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"post": {
|
||||||
|
"operationId": "Apps_CreatePointOfSaleApp",
|
||||||
|
"summary": "Create a new Point of Sale app",
|
||||||
|
"description": "Point of Sale apps allows accepting payments for items in a virtual store",
|
||||||
|
"requestBody": {
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"appName": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "The name of the app (shown in admin UI)",
|
||||||
|
"nullable": false
|
||||||
|
},
|
||||||
|
"title": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "The title of the app (shown to the user)",
|
||||||
|
"nullable": true
|
||||||
|
},
|
||||||
|
"description": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "The description of the app",
|
||||||
|
"nullable": true
|
||||||
|
},
|
||||||
|
"template": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Template for items available in the app",
|
||||||
|
"nullable": true
|
||||||
|
},
|
||||||
|
"defaultView": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Template for items available in the app",
|
||||||
|
"nullable": true,
|
||||||
|
"x-enumNames": [
|
||||||
|
"Static",
|
||||||
|
"Cart",
|
||||||
|
"Light",
|
||||||
|
"Print"
|
||||||
|
],
|
||||||
|
"enum": [
|
||||||
|
"Static",
|
||||||
|
"Cart",
|
||||||
|
"Light",
|
||||||
|
"Print"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"currency": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Currency to use for the app. Defaults to the currency used by the store if not specified",
|
||||||
|
"example": "BTC",
|
||||||
|
"nullable": true
|
||||||
|
},
|
||||||
|
"showCustomAmount": {
|
||||||
|
"type": "boolean",
|
||||||
|
"description": "Whether to include a special item in the store which allows user to input a custom payment amount",
|
||||||
|
"default": true,
|
||||||
|
"nullable": true
|
||||||
|
},
|
||||||
|
"showDiscount": {
|
||||||
|
"type": "boolean",
|
||||||
|
"description": "Whether to allow user to input a discount amount. Applies to Cart view only. Not recommended for customer self-checkout",
|
||||||
|
"default": true,
|
||||||
|
"nullable": true
|
||||||
|
},
|
||||||
|
"enableTips": {
|
||||||
|
"type": "boolean",
|
||||||
|
"description": "Whether to allow user to input a tip amount. Applies to Cart and Light views only",
|
||||||
|
"default": true,
|
||||||
|
"nullable": true
|
||||||
|
},
|
||||||
|
"customAmountPayButtonText": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Payment button text which appears for items which allow user to input a custom amount",
|
||||||
|
"default": "Pay",
|
||||||
|
"nullable": true
|
||||||
|
},
|
||||||
|
"fixedAmountPayButtonText": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Payment button text which appears for items which have a fixed price",
|
||||||
|
"default": "Buy for {PRICE_HERE}",
|
||||||
|
"nullable": true
|
||||||
|
},
|
||||||
|
"tipText": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Prompt which appears next to the tip amount field if tipping is enabled",
|
||||||
|
"default": "Do you want to leave a tip?",
|
||||||
|
"nullable": true
|
||||||
|
},
|
||||||
|
"customCSSLink": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Link to a custom CSS stylesheet to be used in the app",
|
||||||
|
"nullable": true
|
||||||
|
},
|
||||||
|
"embeddedCSS": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Custom CSS to embed into the app",
|
||||||
|
"nullable": true
|
||||||
|
},
|
||||||
|
"notificationUrl": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Callback notification url to POST to once when invoice is paid for and once when there are enough blockchain confirmations",
|
||||||
|
"nullable": true
|
||||||
|
},
|
||||||
|
"redirectUrl": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "URL to redirect user to once invoice is paid",
|
||||||
|
"nullable": true
|
||||||
|
},
|
||||||
|
"redirectAutomatically": {
|
||||||
|
"type": "boolean",
|
||||||
|
"description": "Whether to redirect user to redirect URL automatically once invoice is paid. Defaults to what is set in the store settings",
|
||||||
|
"nullable": true
|
||||||
|
},
|
||||||
|
"requiresRefundEmail": {
|
||||||
|
"type": "boolean",
|
||||||
|
"description": "Whether to redirect user to redirect URL automatically once invoice is paid. Defaults to what is set in the store settings",
|
||||||
|
"nullable": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "Created app details",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/PointOfSaleAppData"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"422": {
|
||||||
|
"description": "Unable to validate the request",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/ValidationProblemDetails"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"tags": [
|
||||||
|
"POS",
|
||||||
|
"Point of Sale"
|
||||||
|
],
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"API_Key": [
|
||||||
|
"btcpay.store.canmodifystoresettings"
|
||||||
|
],
|
||||||
|
"Basic": []
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"components": {
|
||||||
|
"schemas": {
|
||||||
|
"PointOfSaleAppData": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"id": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Id of the app",
|
||||||
|
"example": "3ki4jsAkN4u9rv1PUzj1odX4Nx7s"
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Name given to the app when it was created",
|
||||||
|
"example": "my test app"
|
||||||
|
},
|
||||||
|
"storeId": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Id of the store to which the app belongs",
|
||||||
|
"example": "9CiNzKoANXxmk5ayZngSXrHTiVvvgCrwrpFQd4m2K776"
|
||||||
|
},
|
||||||
|
"created": {
|
||||||
|
"type": "integer",
|
||||||
|
"example": 1651554744,
|
||||||
|
"description": "UNIX timestamp for when the app was created"
|
||||||
|
},
|
||||||
|
"appType": {
|
||||||
|
"type": "string",
|
||||||
|
"example": "PointOfSale",
|
||||||
|
"description": "Type of the app which was created (will always \"PointOfSale\" in this case"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"tags": [
|
||||||
|
{
|
||||||
|
"name": "App"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "POS"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Point of Sale"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user