mirror of
https://github.com/aljazceru/btcpayserver.git
synced 2026-01-31 03:44:29 +01:00
POS: Create Invoice action optionally responds with JSON (#6439)
* POS: Create Invoice action optionally responds with JSON We adapted this action, which is full of custom POS logic, for the app to avoid creating a separate API endpoint. * Add test and improve error handling
This commit is contained in:
@@ -2,6 +2,7 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Client.Models;
|
||||
using BTCPayServer.Controllers;
|
||||
@@ -802,5 +803,65 @@ g:
|
||||
Assert.Equal("new", topupInvoice.Status);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
[Fact]
|
||||
[Trait("Integration", "Integration")]
|
||||
public async Task CanUsePoSAppJsonEndpoint()
|
||||
{
|
||||
using var tester = CreateServerTester();
|
||||
await tester.StartAsync();
|
||||
var user = tester.NewAccount();
|
||||
await user.GrantAccessAsync();
|
||||
user.RegisterDerivationScheme("BTC");
|
||||
var apps = user.GetController<UIAppsController>();
|
||||
var pos = user.GetController<UIPointOfSaleController>();
|
||||
var vm = Assert.IsType<CreateAppViewModel>(Assert.IsType<ViewResult>(apps.CreateApp(user.StoreId)).Model);
|
||||
var appType = PointOfSaleAppType.AppType;
|
||||
vm.AppName = "test";
|
||||
vm.SelectedAppType = appType;
|
||||
var redirect = Assert.IsType<RedirectResult>(apps.CreateApp(user.StoreId, vm).Result);
|
||||
Assert.EndsWith("/settings/pos", redirect.Url);
|
||||
var appList = Assert.IsType<ListAppsViewModel>(Assert.IsType<ViewResult>(apps.ListApps(user.StoreId).Result).Model);
|
||||
var app = appList.Apps[0];
|
||||
var appData = new AppData { Id = app.Id, StoreDataId = app.StoreId, Name = app.AppName, AppType = appType };
|
||||
apps.HttpContext.SetAppData(appData);
|
||||
pos.HttpContext.SetAppData(appData);
|
||||
var vmpos = await pos.UpdatePointOfSale(app.Id).AssertViewModelAsync<UpdatePointOfSaleViewModel>();
|
||||
vmpos.Title = "App POS";
|
||||
vmpos.Currency = "EUR";
|
||||
Assert.IsType<RedirectToActionResult>(pos.UpdatePointOfSale(app.Id, vmpos).Result);
|
||||
|
||||
// Failing requests
|
||||
var (invoiceId1, error1) = await PosJsonRequest(tester, app.Id, "amount=-21&discount=10&tip=2");
|
||||
Assert.Null(invoiceId1);
|
||||
Assert.Equal("Negative amount is not allowed", error1);
|
||||
var (invoiceId2, error2) = await PosJsonRequest(tester, app.Id, "amount=21&discount=-10&tip=-2");
|
||||
Assert.Null(invoiceId2);
|
||||
Assert.Equal("Negative tip or discount is not allowed", error2);
|
||||
|
||||
// Successful request
|
||||
var (invoiceId3, error3) = await PosJsonRequest(tester, app.Id, "amount=21");
|
||||
Assert.NotNull(invoiceId3);
|
||||
Assert.Null(error3);
|
||||
|
||||
// Check generated invoice
|
||||
var invoices = await user.BitPay.GetInvoicesAsync();
|
||||
var invoice = invoices.First();
|
||||
Assert.Equal(invoiceId3, invoice.Id);
|
||||
Assert.Equal(21.00m, invoice.Price);
|
||||
Assert.Equal("EUR", invoice.Currency);
|
||||
}
|
||||
|
||||
private async Task<(string invoiceId, string error)> PosJsonRequest(ServerTester tester, string appId, string query)
|
||||
{
|
||||
var uriBuilder = new UriBuilder(tester.PayTester.ServerUri) { Path = $"/apps/{appId}/pos/light", Query = query };
|
||||
var request = new HttpRequestMessage(HttpMethod.Post, uriBuilder.Uri);
|
||||
request.Headers.Add("Accept", "application/json");
|
||||
var response = await tester.PayTester.HttpClient.SendAsync(request);
|
||||
var content = await response.Content.ReadAsStringAsync();
|
||||
var json = JObject.Parse(content);
|
||||
return (json["invoiceId"]?.Value<string>(), json["error"]?.Value<string>());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -160,17 +160,25 @@ namespace BTCPayServer.Plugins.PointOfSale.Controllers
|
||||
if (await Throttle(appId))
|
||||
return new TooManyRequestsResult(ZoneLimits.PublicInvoices);
|
||||
|
||||
// Distinguish JSON requests coming via the mobile app
|
||||
var wantsJson = Request.Headers.Accept.FirstOrDefault()?.StartsWith("application/json") is true;
|
||||
|
||||
var app = await _appService.GetApp(appId, PointOfSaleAppType.AppType);
|
||||
if (app == null)
|
||||
return wantsJson
|
||||
? Json(new { error = "App not found" })
|
||||
: NotFound();
|
||||
|
||||
// not allowing negative tips or discounts
|
||||
if (tip < 0 || discount < 0)
|
||||
return RedirectToAction(nameof(ViewPointOfSale), new { appId });
|
||||
return wantsJson
|
||||
? Json(new { error = "Negative tip or discount is not allowed" })
|
||||
: RedirectToAction(nameof(ViewPointOfSale), new { appId });
|
||||
|
||||
if (string.IsNullOrEmpty(choiceKey) && amount <= 0)
|
||||
return RedirectToAction(nameof(ViewPointOfSale), new { appId });
|
||||
|
||||
if (app == null)
|
||||
return NotFound();
|
||||
return wantsJson
|
||||
? Json(new { error = "Negative amount is not allowed" })
|
||||
: RedirectToAction(nameof(ViewPointOfSale), new { appId });
|
||||
|
||||
var settings = app.GetSettings<PointOfSaleSettings>();
|
||||
settings.DefaultView = settings.EnableShoppingCart ? PosViewType.Cart : settings.DefaultView;
|
||||
@@ -180,6 +188,7 @@ namespace BTCPayServer.Plugins.PointOfSale.Controllers
|
||||
{
|
||||
return RedirectToAction(nameof(ViewPointOfSale), new { appId, viewType });
|
||||
}
|
||||
|
||||
var jposData = TryParseJObject(posData);
|
||||
string title;
|
||||
decimal? price;
|
||||
@@ -235,9 +244,10 @@ namespace BTCPayServer.Plugins.PointOfSale.Controllers
|
||||
switch (itemChoice.Inventory)
|
||||
{
|
||||
case <= 0:
|
||||
return RedirectToAction(nameof(ViewPointOfSale), new { appId });
|
||||
case { } inventory when inventory < cartItem.Count:
|
||||
return RedirectToAction(nameof(ViewPointOfSale), new { appId });
|
||||
return wantsJson
|
||||
? Json(new { error = $"Inventory for {itemChoice.Title} exhausted: {itemChoice.Inventory} available" })
|
||||
: RedirectToAction(nameof(ViewPointOfSale), new { appId });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -262,8 +272,9 @@ namespace BTCPayServer.Plugins.PointOfSale.Controllers
|
||||
var store = await _appService.GetStore(app);
|
||||
var storeBlob = store.GetStoreBlob();
|
||||
var posFormId = settings.FormId;
|
||||
var formData = await FormDataService.GetForm(posFormId);
|
||||
|
||||
// skip forms feature for JSON requests (from the app)
|
||||
var formData = wantsJson ? null : await FormDataService.GetForm(posFormId);
|
||||
JObject formResponseJObject = null;
|
||||
switch (formData)
|
||||
{
|
||||
@@ -411,14 +422,16 @@ namespace BTCPayServer.Plugins.PointOfSale.Controllers
|
||||
meta.Merge(formResponseJObject);
|
||||
entity.Metadata = InvoiceMetadata.FromJObject(meta);
|
||||
});
|
||||
var data = new { invoiceId = invoice.Id };
|
||||
if (wantsJson)
|
||||
return Json(data);
|
||||
if (price is 0 && storeBlob.ReceiptOptions?.Enabled is true)
|
||||
{
|
||||
return RedirectToAction(nameof(UIInvoiceController.InvoiceReceipt), "UIInvoice", new { invoiceId = invoice.Id });
|
||||
}
|
||||
return RedirectToAction(nameof(UIInvoiceController.Checkout), "UIInvoice", new { invoiceId = invoice.Id });
|
||||
return RedirectToAction(nameof(UIInvoiceController.InvoiceReceipt), "UIInvoice", data);
|
||||
return RedirectToAction(nameof(UIInvoiceController.Checkout), "UIInvoice", data);
|
||||
}
|
||||
catch (BitpayHttpException e)
|
||||
{
|
||||
if (wantsJson) return Json(new { error = e.Message });
|
||||
TempData.SetStatusMessageModel(new StatusMessageModel
|
||||
{
|
||||
Html = e.Message.Replace("\n", "<br />", StringComparison.OrdinalIgnoreCase),
|
||||
|
||||
Reference in New Issue
Block a user