GreenField: Invoice API

This commit is contained in:
Kukks 2020-07-22 13:58:41 +02:00 committed by nicolas.dorier
parent 8239fd7e0e
commit 34e76494e3
No known key found for this signature in database
GPG Key ID: 6618763EF09186FE
11 changed files with 509 additions and 4 deletions

View File

@ -0,0 +1,142 @@
using System;
using BTCPayServer.JsonConverters;
using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
namespace BTCPayServer.Client.Models
{
public class CreateInvoiceRequest
{
[JsonProperty(ItemConverterType = typeof(DecimalDoubleStringJsonConverter))]
public decimal Amount
{
get;
set;
}
public string Currency
{
get;
set;
}
public ProductInformation Metadata { get; set; }
public BuyerInformation Customer { get; set; } = new BuyerInformation();
public CheckoutOptions Checkout { get; set; } = new CheckoutOptions();
public class CheckoutOptions
{
[JsonConverter(typeof(StringEnumConverter))]
public SpeedPolicy? SpeedPolicy { get; set; }
public string[] PaymentMethods { get; set; }
public bool? RedirectAutomatically { get; set; }
public string RedirectUri { get; set; }
public Uri WebHook { get; set; }
[JsonConverter(typeof(NBitcoin.JsonConverters.DateTimeToUnixTimeConverter))]
public DateTimeOffset? ExpirationTime { get; set; }
[JsonProperty(ItemConverterType = typeof(DecimalDoubleStringJsonConverter))]
public double? PaymentTolerance { get; set; }
}
public class BuyerInformation
{
[JsonProperty(PropertyName = "buyerName")]
public string BuyerName
{
get;
set;
}
[JsonProperty(PropertyName = "buyerEmail")]
public string BuyerEmail
{
get;
set;
}
[JsonProperty(PropertyName = "buyerCountry")]
public string BuyerCountry
{
get;
set;
}
[JsonProperty(PropertyName = "buyerZip")]
public string BuyerZip
{
get;
set;
}
[JsonProperty(PropertyName = "buyerState")]
public string BuyerState
{
get;
set;
}
[JsonProperty(PropertyName = "buyerCity")]
public string BuyerCity
{
get;
set;
}
[JsonProperty(PropertyName = "buyerAddress2")]
public string BuyerAddress2
{
get;
set;
}
[JsonProperty(PropertyName = "buyerAddress1")]
public string BuyerAddress1
{
get;
set;
}
[JsonProperty(PropertyName = "buyerPhone")]
public string BuyerPhone
{
get;
set;
}
}
public class ProductInformation
{
public string OrderId { get; set; }
public string PosData { get; set; }
public string ItemDesc
{
get;
set;
}
public string ItemCode
{
get;
set;
}
public bool Physical
{
get;
set;
}
public decimal? TaxIncluded
{
get;
set;
}
}
}
}

View File

@ -0,0 +1,70 @@
using System;
using System.Collections.Generic;
using BTCPayServer.JsonConverters;
using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
namespace BTCPayServer.Client.Models
{
public class InvoiceData : CreateInvoiceRequest
{
public string Id { get; set; }
public Dictionary<string, PaymentMethodDataModel> PaymentMethodData { get; set; }
public class PaymentMethodDataModel
{
public string Destination { get; set; }
public string PaymentLink { get; set; }
[JsonProperty(ItemConverterType = typeof(DecimalDoubleStringJsonConverter))]
public decimal Rate { get; set; }
[JsonProperty(ItemConverterType = typeof(DecimalDoubleStringJsonConverter))]
public decimal PaymentMethodPaid { get; set; }
[JsonProperty(ItemConverterType = typeof(DecimalDoubleStringJsonConverter))]
public decimal TotalPaid { get; set; }
[JsonProperty(ItemConverterType = typeof(DecimalDoubleStringJsonConverter))]
public decimal Due { get; set; }
[JsonProperty(ItemConverterType = typeof(DecimalDoubleStringJsonConverter))]
public decimal Amount { get; set; }
[JsonProperty(ItemConverterType = typeof(DecimalDoubleStringJsonConverter))]
public decimal NetworkFee { get; set; }
public List<Payment> Payments { get; set; }
public class Payment
{
public string Id { get; set; }
[JsonConverter(typeof(NBitcoin.JsonConverters.DateTimeToUnixTimeConverter))]
public DateTime ReceivedDate { get; set; }
[JsonProperty(ItemConverterType = typeof(DecimalDoubleStringJsonConverter))]
public decimal Value { get; set; }
[JsonProperty(ItemConverterType = typeof(DecimalDoubleStringJsonConverter))]
public decimal Fee { get; set; }
[JsonConverter(typeof(StringEnumConverter))]
public PaymentStatus Status { get; set; }
public string Destination { get; set; }
public enum PaymentStatus
{
Invalid,
AwaitingConfirmation,
AwaitingCompletion,
Complete
}
}
}
}
}

View File

@ -14,6 +14,7 @@ namespace BTCPayServer.Client
public const string CanModifyStoreSettings = "btcpay.store.canmodifystoresettings";
public const string CanModifyStoreSettingsUnscoped = "btcpay.store.canmodifystoresettings:";
public const string CanViewStoreSettings = "btcpay.store.canviewstoresettings";
public const string CanViewInvoices = "btcpay.store.canviewinvoices";
public const string CanCreateInvoice = "btcpay.store.cancreateinvoice";
public const string CanViewPaymentRequests = "btcpay.store.canviewpaymentrequests";
public const string CanModifyPaymentRequests = "btcpay.store.canmodifypaymentrequests";
@ -26,6 +27,7 @@ namespace BTCPayServer.Client
{
get
{
yield return CanViewInvoices;
yield return CanCreateInvoice;
yield return CanModifyServerSettings;
yield return CanModifyStoreSettings;
@ -153,6 +155,8 @@ namespace BTCPayServer.Client
return true;
switch (subpolicy)
{
case Policies.CanViewInvoices when this.Policy == Policies.CanModifyStoreSettings:
case Policies.CanViewInvoices when this.Policy == Policies.CanViewStoreSettings:
case Policies.CanViewStoreSettings when this.Policy == Policies.CanModifyStoreSettings:
case Policies.CanCreateInvoice when this.Policy == Policies.CanModifyStoreSettings:
case Policies.CanViewProfile when this.Policy == Policies.CanModifyProfile:

View File

@ -53,6 +53,7 @@ using Newtonsoft.Json.Schema;
using Xunit;
using Xunit.Abstractions;
using Xunit.Sdk;
using ProductInformation = BTCPayServer.Services.Invoices.ProductInformation;
using RatesViewModel = BTCPayServer.Models.StoreViewModels.RatesViewModel;
namespace BTCPayServer.Tests

View File

@ -0,0 +1,270 @@
using System;
using System.Linq;
using System.Threading.Tasks;
using BTCPayServer.Client;
using BTCPayServer.Payments;
using BTCPayServer.Security;
using BTCPayServer.Services.Invoices;
using BTCPayServer.Validation;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Cors;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using NBitcoin;
using NBitpayClient;
using CreateInvoiceRequest = BTCPayServer.Client.Models.CreateInvoiceRequest;
using InvoiceData = BTCPayServer.Client.Models.InvoiceData;
namespace BTCPayServer.Controllers.GreenField
{
[ApiController]
[Authorize(AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
[EnableCors(CorsPolicies.All)]
public class GreenFieldInvoiceController : Controller
{
private readonly InvoiceController _invoiceController;
private readonly InvoiceRepository _invoiceRepository;
public GreenFieldInvoiceController(InvoiceController invoiceController, InvoiceRepository invoiceRepository)
{
_invoiceController = invoiceController;
_invoiceRepository = invoiceRepository;
}
[Authorize(Policy = Policies.CanViewInvoices,
AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
[HttpGet("~/api/v1/stores/{storeId}/invoices/{invoiceId}")]
public async Task<IActionResult> GetInvoices(string storeId)
{
var store = HttpContext.GetStoreData();
if (store == null)
{
return NotFound();
}
var invoices = await _invoiceRepository.GetInvoices(new InvoiceQuery() {StoreId = new[] {store.Id}});
return Ok(invoices.Select(ToModel));
}
[Authorize(Policy = Policies.CanViewInvoices,
AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
[HttpGet("~/api/v1/stores/{storeId}/invoices/{invoiceId}")]
public async Task<IActionResult> GetInvoice(string storeId, string invoiceId)
{
var store = HttpContext.GetStoreData();
if (store == null)
{
return NotFound();
}
var invoice = await _invoiceRepository.GetInvoice(invoiceId, true);
if (invoice.StoreId != store.Id)
{
return NotFound();
}
return Ok(ToModel(invoice));
}
[Authorize(Policy = Policies.CanModifyStoreSettings,
AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
[HttpDelete("~/api/v1/stores/{storeId}/invoices/{invoiceId}")]
public async Task<IActionResult> ArchiveInvoice(string storeId, string invoiceId)
{
var store = HttpContext.GetStoreData();
if (store == null)
{
return NotFound();
}
var invoice = await _invoiceRepository.GetInvoice(invoiceId, true);
if (invoice.StoreId != store.Id)
{
return NotFound();
}
await _invoiceRepository.ToggleInvoiceArchival(invoiceId, true);
return Ok();
}
[Authorize(Policy = Policies.CanCreateInvoice,
AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
[HttpPost("~/api/v1/stores/{storeId}/invoices")]
public async Task<IActionResult> CreateInvoice(string storeId, CreateInvoiceRequest request)
{
var store = HttpContext.GetStoreData();
if (store == null)
{
return NotFound();
}
if (request.Amount < 0.0m)
{
ModelState.AddModelError(nameof(request.Amount), "The amount should be 0 or more.");
}
if (request.Checkout.PaymentMethods?.Any() is true)
{
for (int i = 0; i < request.Checkout.PaymentMethods.Length; i++)
{
if (!PaymentMethodId.TryParse(request.Checkout.PaymentMethods[i], out _))
{
request.AddModelError(invoiceRequest => invoiceRequest.Checkout.PaymentMethods[i],
"Invalid payment method", this);
}
}
}
if (!string.IsNullOrEmpty(request.Customer.BuyerEmail) &&
!EmailValidator.IsEmail(request.Customer.BuyerEmail))
{
request.AddModelError(invoiceRequest => invoiceRequest.Customer.BuyerEmail, "Invalid email address",
this);
}
if (request.Checkout.ExpirationTime != null && request.Checkout.ExpirationTime < DateTime.Now)
{
request.AddModelError(invoiceRequest => invoiceRequest.Checkout.ExpirationTime,
"Expiration time must be in the future", this);
}
if (request.Checkout.PaymentTolerance != null &&
(request.Checkout.PaymentTolerance < 0 || request.Checkout.PaymentTolerance > 100))
{
request.AddModelError(invoiceRequest => invoiceRequest.Checkout.PaymentTolerance,
"PaymentTolerance can only be between 0 and 100 percent", this);
}
if (!ModelState.IsValid)
return this.CreateValidationError(ModelState);
var invoice = await _invoiceController.CreateInvoiceCoreRaw(FromModel(request), store,
Request.GetAbsoluteUri(""));
return Ok(ToModel(invoice));
}
public InvoiceData ToModel(InvoiceEntity entity)
{
return new InvoiceData()
{
Amount = entity.ProductInformation.Price,
Id = entity.Id,
Currency = entity.ProductInformation.Currency,
Metadata =
new CreateInvoiceRequest.ProductInformation()
{
Physical = entity.ProductInformation.Physical,
ItemCode = entity.ProductInformation.ItemCode,
ItemDesc = entity.ProductInformation.ItemDesc,
OrderId = entity.OrderId,
PosData = entity.PosData,
TaxIncluded = entity.ProductInformation.TaxIncluded
},
Customer = new CreateInvoiceRequest.BuyerInformation()
{
BuyerAddress1 = entity.BuyerInformation.BuyerAddress1,
BuyerAddress2 = entity.BuyerInformation.BuyerAddress2,
BuyerCity = entity.BuyerInformation.BuyerCity,
BuyerCountry = entity.BuyerInformation.BuyerCountry,
BuyerEmail = entity.BuyerInformation.BuyerEmail,
BuyerName = entity.BuyerInformation.BuyerName,
BuyerPhone = entity.BuyerInformation.BuyerPhone,
BuyerState = entity.BuyerInformation.BuyerState,
BuyerZip = entity.BuyerInformation.BuyerZip
},
Checkout = new CreateInvoiceRequest.CheckoutOptions()
{
ExpirationTime = entity.ExpirationTime,
PaymentTolerance = entity.PaymentTolerance,
PaymentMethods =
entity.GetPaymentMethods().Select(method => method.GetId().ToString()).ToArray(),
RedirectAutomatically = entity.RedirectAutomatically,
RedirectUri = entity.RedirectURL.ToString(),
SpeedPolicy = entity.SpeedPolicy,
WebHook = entity.NotificationURL
},
PaymentMethodData = entity.GetPaymentMethods().ToDictionary(method => method.GetId().ToString(),
method =>
{
var accounting = method.Calculate();
var details = method.GetPaymentMethodDetails();
var payments = method.ParentEntity.GetPayments().Where(paymentEntity =>
paymentEntity.GetPaymentMethodId() == method.GetId());
return new InvoiceData.PaymentMethodDataModel()
{
Destination = details.GetPaymentDestination(),
Rate = method.Rate,
Due = accounting.Due.ToDecimal(MoneyUnit.BTC),
TotalPaid = accounting.Paid.ToDecimal(MoneyUnit.BTC),
PaymentMethodPaid = accounting.CryptoPaid.ToDecimal(MoneyUnit.BTC),
Amount = accounting.Due.ToDecimal(MoneyUnit.BTC),
NetworkFee = accounting.NetworkFee.ToDecimal(MoneyUnit.BTC),
PaymentLink =
method.GetId().PaymentType.GetPaymentLink(method.Network, details, accounting.Due,
Request.GetAbsoluteRoot()),
Payments = payments.Select(paymentEntity =>
{
var data = paymentEntity.GetCryptoPaymentData();
return new InvoiceData.PaymentMethodDataModel.Payment()
{
Destination = data.GetDestination(),
Id = data.GetPaymentId(),
Status = !paymentEntity.Accounted
? InvoiceData.PaymentMethodDataModel.Payment.PaymentStatus.Invalid
: data.PaymentCompleted(paymentEntity)
? InvoiceData.PaymentMethodDataModel.Payment.PaymentStatus.Complete
: data.PaymentConfirmed(paymentEntity, entity.SpeedPolicy)
? InvoiceData.PaymentMethodDataModel.Payment.PaymentStatus
.AwaitingCompletion
: InvoiceData.PaymentMethodDataModel.Payment.PaymentStatus
.AwaitingConfirmation,
Fee = paymentEntity.NetworkFee,
Value = data.GetValue(),
ReceivedDate = paymentEntity.ReceivedTime.DateTime
};
}).ToList()
};
})
};
}
public Models.CreateInvoiceRequest FromModel(CreateInvoiceRequest entity)
{
return new Models.CreateInvoiceRequest()
{
Buyer = new Buyer()
{
country = entity.Customer.BuyerCountry,
email = entity.Customer.BuyerEmail,
phone = entity.Customer.BuyerPhone,
zip = entity.Customer.BuyerZip,
Address1 = entity.Customer.BuyerAddress1,
Address2 = entity.Customer.BuyerAddress2,
City = entity.Customer.BuyerCity,
Name = entity.Customer.BuyerName,
State = entity.Customer.BuyerState,
},
Currency = entity.Currency,
Physical = entity.Metadata.Physical,
Price = entity.Amount,
Refundable = true,
ExtendedNotifications = true,
FullNotifications = true,
RedirectURL = entity.Checkout.RedirectUri,
RedirectAutomatically = entity.Checkout.RedirectAutomatically,
ItemCode = entity.Metadata.ItemCode,
ItemDesc = entity.Metadata.ItemDesc,
ExpirationTime = entity.Checkout.ExpirationTime,
TransactionSpeed = entity.Checkout.SpeedPolicy?.ToString(),
PaymentCurrencies = entity.Checkout.PaymentMethods,
TaxIncluded = entity.Metadata.TaxIncluded,
OrderId = entity.Metadata.OrderId,
NotificationURL = entity.Checkout.RedirectUri,
PosData = entity.Metadata.PosData
};
}
}
}

View File

@ -30,6 +30,8 @@ using NBitcoin;
using NBitpayClient;
using NBXplorer;
using Newtonsoft.Json.Linq;
using CreateInvoiceRequest = BTCPayServer.Models.CreateInvoiceRequest;
using ProductInformation = BTCPayServer.Services.Invoices.ProductInformation;
using StoreData = BTCPayServer.Data.StoreData;
namespace BTCPayServer.Controllers

View File

@ -23,7 +23,9 @@ using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using NBitpayClient;
using Newtonsoft.Json;
using BuyerInformation = BTCPayServer.Services.Invoices.BuyerInformation;
using CreateInvoiceRequest = BTCPayServer.Models.CreateInvoiceRequest;
using ProductInformation = BTCPayServer.Services.Invoices.ProductInformation;
using StoreData = BTCPayServer.Data.StoreData;
namespace BTCPayServer.Controllers
@ -72,7 +74,16 @@ namespace BTCPayServer.Controllers
}
internal async Task<DataWrapper<InvoiceResponse>> CreateInvoiceCore(CreateInvoiceRequest invoice, StoreData store, string serverUrl, List<string> additionalTags = null, CancellationToken cancellationToken = default)
internal async Task<DataWrapper<InvoiceResponse>> CreateInvoiceCore(CreateInvoiceRequest invoice,
StoreData store, string serverUrl, List<string> additionalTags = null,
CancellationToken cancellationToken = default)
{
var entity = await CreateInvoiceCoreRaw(invoice, store, serverUrl, additionalTags, cancellationToken);
var resp = entity.EntityToDTO();
return new DataWrapper<InvoiceResponse>(resp) {Facade = "pos/invoice"};
}
internal async Task<InvoiceEntity> CreateInvoiceCoreRaw(CreateInvoiceRequest invoice, StoreData store, string serverUrl, List<string> additionalTags = null, CancellationToken cancellationToken = default)
{
invoice.Currency = invoice.Currency?.ToUpperInvariant() ?? "USD";
InvoiceLogs logs = new InvoiceLogs();
@ -233,8 +244,7 @@ namespace BTCPayServer.Controllers
await _InvoiceRepository.AddInvoiceLogs(entity.Id, logs);
});
_EventAggregator.Publish(new Events.InvoiceEvent(entity, 1001, InvoiceEvent.Created));
var resp = entity.EntityToDTO();
return new DataWrapper<InvoiceResponse>(resp) { Facade = "pos/invoice" };
return entity;
}
private Task WhenAllFetched(InvoiceLogs logs, Dictionary<CurrencyPair, Task<RateResult>> fetchingByCurrencyPair)

View File

@ -363,6 +363,8 @@ 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.CanViewInvoices, ("View invoices", "The app will be able to view invoices.")},
{$"{BTCPayServer.Client.Policies.CanViewInvoices}:", ("View invoices", "The app will be able to view 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.")},

View File

@ -1,10 +1,13 @@
using System;
using System.Collections.Generic;
using BTCPayServer.Client.Models;
using BTCPayServer.Payments;
using BTCPayServer.Payments.Bitcoin;
using BTCPayServer.Services.Invoices;
using NBitcoin;
using Newtonsoft.Json;
using BuyerInformation = BTCPayServer.Services.Invoices.BuyerInformation;
using ProductInformation = BTCPayServer.Services.Invoices.ProductInformation;
namespace BTCPayServer.Models.InvoicingModels
{

View File

@ -465,7 +465,7 @@ namespace BTCPayServer.Services.Invoices
{
var minerInfo = new MinerFeeInfo();
minerInfo.TotalFee = accounting.NetworkFee.Satoshi;
minerInfo.SatoshiPerBytes = ((BitcoinLikeOnChainPaymentMethod)details).FeeRate
minerInfo.SatoshiPerBytes = ((BitcoinLikeOnChainPaymentMethod) details).FeeRate
.GetFee(1).Satoshi;
dto.MinerFees.TryAdd(cryptoInfo.CryptoCode, minerInfo);
cryptoInfo.PaymentUrls = new InvoicePaymentUrls()

View File

@ -14,6 +14,7 @@ using Microsoft.Extensions.Logging;
using NBitcoin;
using Newtonsoft.Json;
using Encoders = NBitcoin.DataEncoders.Encoders;
using InvoiceData = BTCPayServer.Data.InvoiceData;
namespace BTCPayServer.Services.Invoices
{