Support specifying payment method through apps (#1539)

* Support specifying payment method through apps

closes #1525
This is great if you want to offer items with an incentive to use a specific payment method without messing with the rates.
For example, you can have `Item A` which costs 25$ and your store is configured for USDT and BTC. You can create two items, with different prices, where Item A costs 20$ if you pay with bitcoin or 25$ if you paid in dirty fiat pegs

* make code cleaner

* Add Test

* fix test

* fix test
This commit is contained in:
Andrew Camilleri
2020-05-19 20:54:24 +02:00
committed by GitHub
parent 3d1122be7c
commit 5033cb3186
4 changed files with 68 additions and 1 deletions

View File

@@ -2265,14 +2265,17 @@ namespace BTCPayServer.Tests
[Fact]
[Trait("Integration", "Integration")]
[Trait("Altcoins", "Altcoins")]
public async Task CanUsePoSApp()
{
using (var tester = ServerTester.Create())
{
tester.ActivateLTC();
await tester.StartAsync();
var user = tester.NewAccount();
user.GrantAccess();
user.RegisterDerivationScheme("BTC");
user.RegisterDerivationScheme("LTC");
var apps = user.GetController<AppsController>();
var vm = Assert.IsType<CreateAppViewModel>(Assert.IsType<ViewResult>(apps.CreateApp().Result).Model);
vm.Name = "test";
@@ -2443,6 +2446,41 @@ noninventoryitem:
Assert.Equal(1,
appService.Parse(vmpos.Template, "BTC").Single(item => item.Id == "inventoryitem").Inventory);
}, 10000);
//test payment methods option
vmpos = Assert.IsType<UpdatePointOfSaleViewModel>(Assert
.IsType<ViewResult>(apps.UpdatePointOfSale(appId).Result).Model);
vmpos.Title = "hello";
vmpos.Currency = "BTC";
vmpos.Template = @"
btconly:
price: 1.0
title: good apple
payment_methods:
- BTC
normal:
price: 1.0";
Assert.IsType<RedirectToActionResult>(apps.UpdatePointOfSale(appId, vmpos).Result);
Assert.IsType<RedirectToActionResult>(publicApps
.ViewPointOfSale(appId, 1, null, null, null, null, "btconly").Result);
Assert.IsType<RedirectToActionResult>(publicApps
.ViewPointOfSale(appId, 1, null, null, null, null, "normal").Result);
invoices = user.BitPay.GetInvoices();
var normalInvoice = invoices.Single(invoice => invoice.ItemCode == "normal");
var btcOnlyInvoice = invoices.Single(invoice => invoice.ItemCode == "btconly");
Assert.Single(btcOnlyInvoice.CryptoInfo);
Assert.Equal("BTC",
btcOnlyInvoice.CryptoInfo.First().CryptoCode);
Assert.Equal(PaymentTypes.BTCLike.ToString(),
btcOnlyInvoice.CryptoInfo.First().PaymentType);
Assert.Equal(2, normalInvoice.CryptoInfo.Length);
Assert.Contains(
normalInvoice.CryptoInfo,
s => PaymentTypes.BTCLike.ToString() == s.PaymentType && new[] {"BTC", "LTC"}.Contains(
s.CryptoCode));
}
}

View File

@@ -17,6 +17,7 @@ using Microsoft.AspNetCore.Cors;
using Microsoft.AspNetCore.Http.Extensions;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using NBitpayClient;
using static BTCPayServer.Controllers.AppsController;
namespace BTCPayServer.Controllers
@@ -111,6 +112,7 @@ namespace BTCPayServer.Controllers
}
string title = null;
var price = 0.0m;
Dictionary<string, InvoiceSupportedTransactionCurrency> paymentMethods = null;
ViewPointOfSaleViewModel.Item choice = null;
if (!string.IsNullOrEmpty(choiceKey))
{
@@ -130,6 +132,12 @@ namespace BTCPayServer.Controllers
return RedirectToAction(nameof(ViewPointOfSale), new { appId = appId });
}
}
if (choice?.PaymentMethods?.Any() is true)
{
paymentMethods = choice?.PaymentMethods.ToDictionary(s => s,
s => new InvoiceSupportedTransactionCurrency() {Enabled = true});
}
}
else
{
@@ -182,6 +190,7 @@ namespace BTCPayServer.Controllers
ExtendedNotifications = true,
PosData = string.IsNullOrEmpty(posData) ? null : posData,
RedirectAutomatically = settings.RedirectAutomatically,
SupportedTransactionCurrencies = paymentMethods,
}, store, HttpContext.Request.GetAbsoluteRoot(),
new List<string>() { AppService.GetAppInternalTag(appId) },
cancellationToken);
@@ -270,6 +279,7 @@ namespace BTCPayServer.Controllers
var store = await _AppService.GetStore(app);
var title = settings.Title;
var price = request.Amount;
Dictionary<string, InvoiceSupportedTransactionCurrency> paymentMethods = null;
ViewPointOfSaleViewModel.Item choice = null;
if (!string.IsNullOrEmpty(request.ChoiceKey))
{
@@ -290,6 +300,13 @@ namespace BTCPayServer.Controllers
return NotFound("Option was out of stock");
}
}
if (choice?.PaymentMethods?.Any() is true)
{
paymentMethods = choice?.PaymentMethods.ToDictionary(s => s,
s => new InvoiceSupportedTransactionCurrency() {Enabled = true});
}
}
if (!isAdmin && (settings.EnforceTargetAmount && info.TargetAmount.HasValue && price >
@@ -311,6 +328,7 @@ namespace BTCPayServer.Controllers
NotificationURL = settings.NotificationUrl,
FullNotifications = true,
ExtendedNotifications = true,
SupportedTransactionCurrencies = paymentMethods,
RedirectURL = request.RedirectUrl ??
new Uri(new Uri( new Uri(HttpContext.Request.GetAbsoluteRoot()), _BtcPayServerOptions.RootPath), $"apps/{appId}/crowdfund").ToString()
}, store, HttpContext.Request.GetAbsoluteRoot(),

View File

@@ -21,6 +21,7 @@ namespace BTCPayServer.Models.AppViewModels
public string Title { get; set; }
public bool Custom { get; set; }
public int? Inventory { get; set; } = null;
public string[] PaymentMethods { get; set; }
}
public class CurrencyInfoData

View File

@@ -333,7 +333,9 @@ namespace BTCPayServer.Services.Apps
Formatted = Currencies.FormatCurrency(cc.Value.Value, currency)
}).Single(),
Custom = c.GetDetailString("custom") == "true",
Inventory = string.IsNullOrEmpty(c.GetDetailString("inventory")) ?(int?) null: int.Parse(c.GetDetailString("inventory"), CultureInfo.InvariantCulture)
Inventory = string.IsNullOrEmpty(c.GetDetailString("inventory")) ?(int?) null: int.Parse(c.GetDetailString("inventory"), CultureInfo.InvariantCulture),
PaymentMethods = c.GetDetailStringList("payment_methods")
})
.ToArray();
}
@@ -411,6 +413,14 @@ namespace BTCPayServer.Services.Apps
{
return GetDetail(field).FirstOrDefault()?.Value?.Value;
}
public string[] GetDetailStringList(string field)
{
if (!Value.Children.ContainsKey(field) || !( Value.Children[field] is YamlSequenceNode sequenceNode))
{
return null;
}
return sequenceNode.Children.Select(node => (node as YamlScalarNode)?.Value).Where( s => s!= null).ToArray();
}
}
private class PosScalar
{