2020-06-29 04:44:35 +02:00
|
|
|
using System;
|
2023-07-21 02:08:32 +02:00
|
|
|
using System.Collections.Generic;
|
2017-09-13 08:47:34 +02:00
|
|
|
using System.Linq;
|
2019-03-05 09:09:17 +01:00
|
|
|
using System.Threading;
|
2017-09-13 08:47:34 +02:00
|
|
|
using System.Threading.Tasks;
|
2020-11-17 13:46:23 +01:00
|
|
|
using BTCPayServer.Abstractions.Constants;
|
2022-03-02 18:28:12 +01:00
|
|
|
using BTCPayServer.Abstractions.Extensions;
|
2020-03-19 11:11:15 +01:00
|
|
|
using BTCPayServer.Client;
|
2023-07-21 02:08:32 +02:00
|
|
|
using BTCPayServer.Client.Models;
|
|
|
|
using BTCPayServer.Data;
|
2018-08-30 18:34:39 +02:00
|
|
|
using BTCPayServer.Filters;
|
2021-12-17 07:31:06 +01:00
|
|
|
using BTCPayServer.ModelBinders;
|
2018-08-30 18:34:39 +02:00
|
|
|
using BTCPayServer.Models;
|
2023-07-21 02:08:32 +02:00
|
|
|
using BTCPayServer.Payments;
|
|
|
|
using BTCPayServer.Security.Greenfield;
|
2017-10-20 21:06:37 +02:00
|
|
|
using BTCPayServer.Services.Invoices;
|
2018-05-11 10:16:18 +02:00
|
|
|
using Microsoft.AspNetCore.Authorization;
|
2018-08-30 18:34:39 +02:00
|
|
|
using Microsoft.AspNetCore.Mvc;
|
2023-07-21 02:08:32 +02:00
|
|
|
using NBitpayClient;
|
|
|
|
using StoreData = BTCPayServer.Data.StoreData;
|
2017-09-13 08:47:34 +02:00
|
|
|
|
|
|
|
namespace BTCPayServer.Controllers
|
|
|
|
{
|
2017-10-27 10:53:04 +02:00
|
|
|
[BitpayAPIConstraint]
|
2020-03-20 05:41:47 +01:00
|
|
|
[Authorize(Policies.CanCreateInvoice, AuthenticationSchemes = AuthenticationSchemes.Bitpay)]
|
2022-01-07 04:08:28 +01:00
|
|
|
public class BitpayInvoiceController : Controller
|
2017-09-13 08:47:34 +02:00
|
|
|
{
|
2022-01-07 04:32:00 +01:00
|
|
|
private readonly UIInvoiceController _InvoiceController;
|
2020-06-29 05:07:48 +02:00
|
|
|
private readonly InvoiceRepository _InvoiceRepository;
|
2017-10-13 10:46:19 +02:00
|
|
|
|
2022-01-07 04:32:00 +01:00
|
|
|
public BitpayInvoiceController(UIInvoiceController invoiceController,
|
2019-12-27 20:32:43 +01:00
|
|
|
InvoiceRepository invoiceRepository)
|
2017-10-27 10:53:04 +02:00
|
|
|
{
|
2019-05-29 16:33:31 +02:00
|
|
|
_InvoiceController = invoiceController;
|
2019-12-27 20:32:43 +01:00
|
|
|
_InvoiceRepository = invoiceRepository;
|
2017-10-27 10:53:04 +02:00
|
|
|
}
|
2017-10-13 10:46:19 +02:00
|
|
|
|
2017-10-27 10:53:04 +02:00
|
|
|
[HttpPost]
|
|
|
|
[Route("invoices")]
|
|
|
|
[MediaTypeConstraint("application/json")]
|
2020-08-25 07:33:00 +02:00
|
|
|
public async Task<DataWrapper<InvoiceResponse>> CreateInvoice([FromBody] BitpayCreateInvoiceRequest invoice, CancellationToken cancellationToken)
|
2017-10-27 10:53:04 +02:00
|
|
|
{
|
2019-02-21 11:36:05 +01:00
|
|
|
if (invoice == null)
|
|
|
|
throw new BitpayHttpException(400, "Invalid invoice");
|
2023-07-21 02:08:32 +02:00
|
|
|
return await CreateInvoiceCore(invoice, HttpContext.GetStoreData(), HttpContext.Request.GetAbsoluteRoot(), cancellationToken: cancellationToken);
|
2017-10-27 10:53:04 +02:00
|
|
|
}
|
2017-09-13 08:47:34 +02:00
|
|
|
|
2017-10-27 10:53:04 +02:00
|
|
|
[HttpGet]
|
|
|
|
[Route("invoices/{id}")]
|
2018-12-06 08:58:04 +01:00
|
|
|
public async Task<DataWrapper<InvoiceResponse>> GetInvoice(string id)
|
2017-10-27 10:53:04 +02:00
|
|
|
{
|
2018-12-06 08:58:04 +01:00
|
|
|
var invoice = (await _InvoiceRepository.GetInvoices(new InvoiceQuery()
|
|
|
|
{
|
2020-06-28 10:55:27 +02:00
|
|
|
InvoiceId = new[] { id },
|
2018-12-06 08:58:04 +01:00
|
|
|
StoreId = new[] { HttpContext.GetStoreData().Id }
|
|
|
|
})).FirstOrDefault();
|
2017-10-27 10:53:04 +02:00
|
|
|
if (invoice == null)
|
|
|
|
throw new BitpayHttpException(404, "Object not found");
|
2019-05-29 16:33:31 +02:00
|
|
|
return new DataWrapper<InvoiceResponse>(invoice.EntityToDTO());
|
2017-10-27 10:53:04 +02:00
|
|
|
}
|
|
|
|
[HttpGet]
|
|
|
|
[Route("invoices")]
|
2020-11-13 08:28:15 +01:00
|
|
|
public async Task<IActionResult> GetInvoices(
|
2017-10-27 10:53:04 +02:00
|
|
|
string token,
|
2021-12-17 07:31:06 +01:00
|
|
|
[ModelBinder(typeof(BitpayDateTimeOffsetModelBinder))]
|
2017-10-27 10:53:04 +02:00
|
|
|
DateTimeOffset? dateStart = null,
|
2021-12-17 07:31:06 +01:00
|
|
|
[ModelBinder(typeof(BitpayDateTimeOffsetModelBinder))]
|
2017-10-27 10:53:04 +02:00
|
|
|
DateTimeOffset? dateEnd = null,
|
|
|
|
string orderId = null,
|
|
|
|
string itemCode = null,
|
|
|
|
string status = null,
|
|
|
|
int? limit = null,
|
|
|
|
int? offset = null)
|
|
|
|
{
|
2023-07-21 02:08:32 +02:00
|
|
|
if (User.Identity?.AuthenticationType == Security.Bitpay.BitpayAuthenticationTypes.Anonymous)
|
2020-11-13 08:28:15 +01:00
|
|
|
return Forbid(Security.Bitpay.BitpayAuthenticationTypes.Anonymous);
|
2017-10-27 10:53:04 +02:00
|
|
|
if (dateEnd != null)
|
|
|
|
dateEnd = dateEnd.Value + TimeSpan.FromDays(1); //Should include the end day
|
2019-01-05 09:49:06 +01:00
|
|
|
|
2017-10-27 10:53:04 +02:00
|
|
|
var query = new InvoiceQuery()
|
|
|
|
{
|
2020-12-28 11:10:53 +01:00
|
|
|
Take = limit,
|
2017-10-27 10:53:04 +02:00
|
|
|
Skip = offset,
|
|
|
|
EndDate = dateEnd,
|
|
|
|
StartDate = dateStart,
|
2020-06-28 10:55:27 +02:00
|
|
|
OrderId = orderId == null ? null : new[] { orderId },
|
|
|
|
ItemCode = itemCode == null ? null : new[] { itemCode },
|
2018-04-26 04:01:59 +02:00
|
|
|
Status = status == null ? null : new[] { status },
|
2018-04-29 13:32:43 +02:00
|
|
|
StoreId = new[] { this.HttpContext.GetStoreData().Id }
|
2017-10-27 10:53:04 +02:00
|
|
|
};
|
2017-09-13 08:47:34 +02:00
|
|
|
|
2019-05-30 09:02:52 +02:00
|
|
|
var entities = (await _InvoiceRepository.GetInvoices(query))
|
2019-05-24 15:22:38 +02:00
|
|
|
.Select((o) => o.EntityToDTO()).ToArray();
|
2017-09-13 08:47:34 +02:00
|
|
|
|
2020-11-13 08:28:15 +01:00
|
|
|
return Json(DataWrapper.Create(entities));
|
2017-10-27 10:53:04 +02:00
|
|
|
}
|
2023-07-21 02:08:32 +02:00
|
|
|
|
|
|
|
internal async Task<DataWrapper<InvoiceResponse>> CreateInvoiceCore(BitpayCreateInvoiceRequest invoice,
|
|
|
|
StoreData store, string serverUrl, List<string> additionalTags = null,
|
|
|
|
CancellationToken cancellationToken = default, Action<InvoiceEntity> entityManipulator = null)
|
|
|
|
{
|
|
|
|
var entity = await CreateInvoiceCoreRaw(invoice, store, serverUrl, additionalTags, cancellationToken, entityManipulator);
|
|
|
|
var resp = entity.EntityToDTO();
|
|
|
|
return new DataWrapper<InvoiceResponse>(resp) { Facade = "pos/invoice" };
|
|
|
|
}
|
|
|
|
|
|
|
|
internal async Task<InvoiceEntity> CreateInvoiceCoreRaw(BitpayCreateInvoiceRequest invoice, StoreData store, string serverUrl, List<string> additionalTags = null, CancellationToken cancellationToken = default, Action<InvoiceEntity> entityManipulator = null)
|
|
|
|
{
|
|
|
|
var storeBlob = store.GetStoreBlob();
|
|
|
|
var entity = _InvoiceRepository.CreateNewInvoice(store.Id);
|
|
|
|
entity.ExpirationTime = invoice.ExpirationTime is { } v ? v : entity.InvoiceTime + storeBlob.InvoiceExpiration;
|
|
|
|
entity.MonitoringExpiration = entity.ExpirationTime + storeBlob.MonitoringExpiration;
|
|
|
|
if (entity.ExpirationTime - TimeSpan.FromSeconds(30.0) < entity.InvoiceTime)
|
|
|
|
{
|
|
|
|
throw new BitpayHttpException(400, "The expirationTime is set too soon");
|
|
|
|
}
|
|
|
|
if (entity.Price < 0.0m)
|
|
|
|
{
|
|
|
|
throw new BitpayHttpException(400, "The price should be 0 or more.");
|
|
|
|
}
|
|
|
|
if (entity.Price > GreenfieldConstants.MaxAmount)
|
|
|
|
{
|
|
|
|
throw new BitpayHttpException(400, $"The price should less than {GreenfieldConstants.MaxAmount}.");
|
|
|
|
}
|
|
|
|
entity.Metadata.OrderId = invoice.OrderId;
|
|
|
|
entity.Metadata.PosDataLegacy = invoice.PosData;
|
|
|
|
entity.ServerUrl = serverUrl;
|
|
|
|
entity.FullNotifications = invoice.FullNotifications || invoice.ExtendedNotifications;
|
|
|
|
entity.ExtendedNotifications = invoice.ExtendedNotifications;
|
|
|
|
entity.NotificationURLTemplate = invoice.NotificationURL;
|
|
|
|
entity.NotificationEmail = invoice.NotificationEmail;
|
|
|
|
if (additionalTags != null)
|
|
|
|
entity.InternalTags.AddRange(additionalTags);
|
|
|
|
FillBuyerInfo(invoice, entity);
|
|
|
|
|
|
|
|
var price = invoice.Price;
|
|
|
|
entity.Metadata.ItemCode = invoice.ItemCode;
|
|
|
|
entity.Metadata.ItemDesc = invoice.ItemDesc;
|
|
|
|
entity.Metadata.Physical = invoice.Physical;
|
|
|
|
entity.Metadata.TaxIncluded = invoice.TaxIncluded;
|
|
|
|
entity.Currency = invoice.Currency;
|
|
|
|
if (price is { } vv)
|
|
|
|
{
|
|
|
|
entity.Price = vv;
|
|
|
|
entity.Type = InvoiceType.Standard;
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
entity.Price = 0m;
|
|
|
|
entity.Type = InvoiceType.TopUp;
|
|
|
|
}
|
|
|
|
|
|
|
|
entity.StoreSupportUrl = storeBlob.StoreSupportUrl;
|
|
|
|
entity.RedirectURLTemplate = invoice.RedirectURL ?? store.StoreWebsite;
|
|
|
|
entity.RedirectAutomatically =
|
|
|
|
invoice.RedirectAutomatically.GetValueOrDefault(storeBlob.RedirectAutomatically);
|
|
|
|
entity.RequiresRefundEmail = invoice.RequiresRefundEmail;
|
|
|
|
entity.SpeedPolicy = ParseSpeedPolicy(invoice.TransactionSpeed, store.SpeedPolicy);
|
|
|
|
|
|
|
|
IPaymentFilter excludeFilter = null;
|
|
|
|
if (invoice.PaymentCurrencies?.Any() is true)
|
|
|
|
{
|
|
|
|
invoice.SupportedTransactionCurrencies ??=
|
|
|
|
new Dictionary<string, InvoiceSupportedTransactionCurrency>();
|
|
|
|
foreach (string paymentCurrency in invoice.PaymentCurrencies)
|
|
|
|
{
|
|
|
|
invoice.SupportedTransactionCurrencies.TryAdd(paymentCurrency,
|
|
|
|
new InvoiceSupportedTransactionCurrency() { Enabled = true });
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if (invoice.SupportedTransactionCurrencies != null && invoice.SupportedTransactionCurrencies.Count != 0)
|
|
|
|
{
|
|
|
|
var supportedTransactionCurrencies = invoice.SupportedTransactionCurrencies
|
|
|
|
.Where(c => c.Value.Enabled)
|
|
|
|
.Select(c => PaymentMethodId.TryParse(c.Key, out var p) ? p : null)
|
|
|
|
.Where(c => c != null)
|
|
|
|
.ToHashSet();
|
|
|
|
excludeFilter = PaymentFilter.Where(p => !supportedTransactionCurrencies.Contains(p));
|
|
|
|
}
|
|
|
|
entity.PaymentTolerance = storeBlob.PaymentTolerance;
|
|
|
|
entity.DefaultPaymentMethod = invoice.DefaultPaymentMethod;
|
|
|
|
entity.RequiresRefundEmail = invoice.RequiresRefundEmail;
|
|
|
|
|
|
|
|
return await _InvoiceController.CreateInvoiceCoreRaw(entity, store, excludeFilter, null, cancellationToken, entityManipulator);
|
|
|
|
}
|
|
|
|
|
|
|
|
private void FillBuyerInfo(BitpayCreateInvoiceRequest req, InvoiceEntity invoiceEntity)
|
|
|
|
{
|
|
|
|
var buyerInformation = invoiceEntity.Metadata;
|
|
|
|
buyerInformation.BuyerAddress1 = req.BuyerAddress1;
|
|
|
|
buyerInformation.BuyerAddress2 = req.BuyerAddress2;
|
|
|
|
buyerInformation.BuyerCity = req.BuyerCity;
|
|
|
|
buyerInformation.BuyerCountry = req.BuyerCountry;
|
|
|
|
buyerInformation.BuyerEmail = req.BuyerEmail;
|
|
|
|
buyerInformation.BuyerName = req.BuyerName;
|
|
|
|
buyerInformation.BuyerPhone = req.BuyerPhone;
|
|
|
|
buyerInformation.BuyerState = req.BuyerState;
|
|
|
|
buyerInformation.BuyerZip = req.BuyerZip;
|
|
|
|
var buyer = req.Buyer;
|
|
|
|
if (buyer == null)
|
|
|
|
return;
|
|
|
|
buyerInformation.BuyerAddress1 ??= buyer.Address1;
|
|
|
|
buyerInformation.BuyerAddress2 ??= buyer.Address2;
|
|
|
|
buyerInformation.BuyerCity ??= buyer.City;
|
|
|
|
buyerInformation.BuyerCountry ??= buyer.country;
|
|
|
|
buyerInformation.BuyerEmail ??= buyer.email;
|
|
|
|
buyerInformation.BuyerName ??= buyer.Name;
|
|
|
|
buyerInformation.BuyerPhone ??= buyer.phone;
|
|
|
|
buyerInformation.BuyerState ??= buyer.State;
|
|
|
|
buyerInformation.BuyerZip ??= buyer.zip;
|
|
|
|
}
|
|
|
|
private SpeedPolicy ParseSpeedPolicy(string transactionSpeed, SpeedPolicy defaultPolicy)
|
|
|
|
{
|
|
|
|
if (transactionSpeed == null)
|
|
|
|
return defaultPolicy;
|
|
|
|
var mappings = new Dictionary<string, SpeedPolicy>();
|
|
|
|
mappings.Add("low", SpeedPolicy.LowSpeed);
|
|
|
|
mappings.Add("low-medium", SpeedPolicy.LowMediumSpeed);
|
|
|
|
mappings.Add("medium", SpeedPolicy.MediumSpeed);
|
|
|
|
mappings.Add("high", SpeedPolicy.HighSpeed);
|
|
|
|
if (!mappings.TryGetValue(transactionSpeed, out SpeedPolicy policy))
|
|
|
|
policy = defaultPolicy;
|
|
|
|
return policy;
|
|
|
|
}
|
2017-10-27 10:53:04 +02:00
|
|
|
}
|
2017-09-13 08:47:34 +02:00
|
|
|
}
|