2021-08-23 08:13:26 +02:00
#nullable enable
2020-06-29 04:44:35 +02:00
using System ;
2017-09-13 08:47:34 +02:00
using System.Collections.Generic ;
2018-08-30 18:34:39 +02:00
using System.Linq ;
2017-09-13 08:47:34 +02:00
using System.Text ;
2019-03-05 09:09:17 +01:00
using System.Threading ;
2017-09-13 08:47:34 +02:00
using System.Threading.Tasks ;
2023-07-24 15:57:24 +02:00
using BTCPayServer.Abstractions.Contracts ;
2022-11-02 10:41:19 +01:00
using BTCPayServer.Abstractions.Extensions ;
2020-05-23 21:13:18 +02:00
using BTCPayServer.Client.Models ;
2023-11-29 10:51:40 +01:00
using BTCPayServer.Services ;
2017-09-13 08:47:34 +02:00
using BTCPayServer.Data ;
2019-01-06 10:12:45 +01:00
using BTCPayServer.Events ;
2020-06-24 10:51:00 +02:00
using BTCPayServer.HostedServices ;
2023-12-01 10:50:05 +01:00
using BTCPayServer.HostedServices.Webhooks ;
2018-08-30 18:34:39 +02:00
using BTCPayServer.Logging ;
using BTCPayServer.Payments ;
2022-12-08 05:16:18 +01:00
using BTCPayServer.Payments.Bitcoin ;
2018-08-30 18:34:39 +02:00
using BTCPayServer.Rating ;
using BTCPayServer.Security ;
2023-06-16 03:47:58 +02:00
using BTCPayServer.Security.Greenfield ;
2019-02-19 05:04:58 +01:00
using BTCPayServer.Services.Apps ;
2017-10-20 21:06:37 +02:00
using BTCPayServer.Services.Invoices ;
2022-11-02 10:41:19 +01:00
using BTCPayServer.Services.PaymentRequests ;
2017-09-15 09:06:57 +02:00
using BTCPayServer.Services.Rates ;
2018-08-30 18:34:39 +02:00
using BTCPayServer.Services.Stores ;
2023-03-20 02:46:46 +01:00
using Microsoft.AspNetCore.Authorization ;
2022-11-02 10:41:19 +01:00
using Microsoft.AspNetCore.Http ;
2018-08-30 18:34:39 +02:00
using Microsoft.AspNetCore.Identity ;
using Microsoft.AspNetCore.Mvc ;
2022-07-06 14:14:55 +02:00
using Microsoft.AspNetCore.Routing ;
2022-12-08 05:16:18 +01:00
using NBitcoin ;
2023-02-25 15:34:49 +01:00
using Newtonsoft.Json.Linq ;
2020-05-23 21:13:18 +02:00
using StoreData = BTCPayServer . Data . StoreData ;
2024-04-04 09:31:04 +02:00
using Serilog.Filters ;
using PeterO.Numbers ;
2024-05-01 03:22:07 +02:00
using BTCPayServer.Payouts ;
2017-09-13 08:47:34 +02:00
namespace BTCPayServer.Controllers
{
2019-10-06 08:47:46 +02:00
[Filters.BitpayAPIConstraint(false)]
2022-01-07 04:32:00 +01:00
public partial class UIInvoiceController : Controller
2017-10-27 10:53:04 +02:00
{
2020-06-29 05:07:48 +02:00
readonly InvoiceRepository _InvoiceRepository ;
2022-12-08 05:16:18 +01:00
private readonly WalletRepository _walletRepository ;
2020-06-29 05:07:48 +02:00
readonly RateFetcher _RateProvider ;
readonly StoreRepository _StoreRepository ;
readonly UserManager < ApplicationUser > _UserManager ;
private readonly CurrencyNameTable _CurrencyNameTable ;
2023-03-13 02:12:58 +01:00
private readonly DisplayFormatter _displayFormatter ;
2020-06-29 05:07:48 +02:00
readonly EventAggregator _EventAggregator ;
readonly BTCPayNetworkProvider _NetworkProvider ;
2024-05-01 03:22:07 +02:00
private readonly PayoutMethodHandlerDictionary _payoutHandlers ;
2024-04-04 09:31:04 +02:00
private readonly PaymentMethodHandlerDictionary _handlers ;
2024-05-13 15:29:42 +02:00
private readonly DefaultRulesCollection _defaultRules ;
2020-06-24 10:51:00 +02:00
private readonly ApplicationDbContextFactory _dbContextFactory ;
private readonly PullPaymentHostedService _paymentHostedService ;
2021-07-27 08:17:56 +02:00
private readonly LanguageService _languageService ;
2022-02-10 04:24:28 +01:00
private readonly ExplorerClientProvider _ExplorerClients ;
private readonly UIWalletsController _walletsController ;
2022-12-08 05:16:18 +01:00
private readonly InvoiceActivator _invoiceActivator ;
2022-07-06 14:14:55 +02:00
private readonly LinkGenerator _linkGenerator ;
2023-03-20 02:46:46 +01:00
private readonly IAuthorizationService _authorizationService ;
2023-11-29 10:51:40 +01:00
private readonly TransactionLinkProviders _transactionLinkProviders ;
2024-04-04 09:31:04 +02:00
private readonly Dictionary < PaymentMethodId , IPaymentModelExtension > _paymentModelExtensions ;
private readonly PaymentMethodViewProvider _viewProvider ;
2024-08-30 01:34:23 +02:00
private readonly PrettyNameProvider _prettyName ;
2023-05-19 03:42:09 +02:00
private readonly AppService _appService ;
2023-07-24 15:57:24 +02:00
private readonly IFileService _fileService ;
2024-05-09 02:18:02 +02:00
private readonly UriResolver _uriResolver ;
2020-11-06 12:42:26 +01:00
2022-01-11 05:15:48 +01:00
public WebhookSender WebhookNotificationManager { get ; }
2020-11-06 12:42:26 +01:00
2022-01-07 04:32:00 +01:00
public UIInvoiceController (
2018-02-20 04:45:04 +01:00
InvoiceRepository invoiceRepository ,
2022-12-08 05:16:18 +01:00
WalletRepository walletRepository ,
2023-03-13 02:12:58 +01:00
DisplayFormatter displayFormatter ,
2017-10-27 11:58:43 +02:00
CurrencyNameTable currencyNameTable ,
2017-10-27 10:53:04 +02:00
UserManager < ApplicationUser > userManager ,
2018-08-22 09:53:40 +02:00
RateFetcher rateProvider ,
2017-10-27 10:53:04 +02:00
StoreRepository storeRepository ,
2017-12-17 11:58:55 +01:00
EventAggregator eventAggregator ,
2018-07-12 10:38:21 +02:00
ContentSecurityPolicies csp ,
2019-05-24 08:38:47 +02:00
BTCPayNetworkProvider networkProvider ,
2024-05-01 03:22:07 +02:00
PayoutMethodHandlerDictionary payoutHandlers ,
2020-06-24 10:51:00 +02:00
PaymentMethodHandlerDictionary paymentMethodHandlerDictionary ,
ApplicationDbContextFactory dbContextFactory ,
2020-11-06 12:42:26 +01:00
PullPaymentHostedService paymentHostedService ,
2022-01-11 05:15:48 +01:00
WebhookSender webhookNotificationManager ,
2022-02-10 04:24:28 +01:00
LanguageService languageService ,
ExplorerClientProvider explorerClients ,
2022-07-06 14:14:55 +02:00
UIWalletsController walletsController ,
2022-12-08 05:16:18 +01:00
InvoiceActivator invoiceActivator ,
2023-03-20 02:46:46 +01:00
LinkGenerator linkGenerator ,
2023-05-19 03:42:09 +02:00
AppService appService ,
2023-07-24 15:57:24 +02:00
IFileService fileService ,
2024-05-09 02:18:02 +02:00
UriResolver uriResolver ,
2024-05-13 15:29:42 +02:00
DefaultRulesCollection defaultRules ,
2023-11-29 10:51:40 +01:00
IAuthorizationService authorizationService ,
2024-04-04 09:31:04 +02:00
TransactionLinkProviders transactionLinkProviders ,
Dictionary < PaymentMethodId , IPaymentModelExtension > paymentModelExtensions ,
2024-08-30 01:34:23 +02:00
PaymentMethodViewProvider viewProvider ,
PrettyNameProvider prettyName )
2017-10-27 10:53:04 +02:00
{
2023-03-13 02:12:58 +01:00
_displayFormatter = displayFormatter ;
2017-10-27 11:58:43 +02:00
_CurrencyNameTable = currencyNameTable ? ? throw new ArgumentNullException ( nameof ( currencyNameTable ) ) ;
2017-10-27 10:53:04 +02:00
_StoreRepository = storeRepository ? ? throw new ArgumentNullException ( nameof ( storeRepository ) ) ;
_InvoiceRepository = invoiceRepository ? ? throw new ArgumentNullException ( nameof ( invoiceRepository ) ) ;
2022-12-08 05:16:18 +01:00
_walletRepository = walletRepository ;
2018-05-02 20:32:42 +02:00
_RateProvider = rateProvider ? ? throw new ArgumentNullException ( nameof ( rateProvider ) ) ;
2017-10-27 10:53:04 +02:00
_UserManager = userManager ;
2017-12-17 11:58:55 +01:00
_EventAggregator = eventAggregator ;
2017-12-21 07:52:04 +01:00
_NetworkProvider = networkProvider ;
2024-05-01 03:22:07 +02:00
this . _payoutHandlers = payoutHandlers ;
2024-04-04 09:31:04 +02:00
_handlers = paymentMethodHandlerDictionary ;
2020-06-24 10:51:00 +02:00
_dbContextFactory = dbContextFactory ;
_paymentHostedService = paymentHostedService ;
2020-11-06 12:42:26 +01:00
WebhookNotificationManager = webhookNotificationManager ;
2021-07-27 08:17:56 +02:00
_languageService = languageService ;
2022-02-10 04:24:28 +01:00
this . _ExplorerClients = explorerClients ;
_walletsController = walletsController ;
2022-12-08 05:16:18 +01:00
_invoiceActivator = invoiceActivator ;
2022-07-06 14:14:55 +02:00
_linkGenerator = linkGenerator ;
2023-03-20 02:46:46 +01:00
_authorizationService = authorizationService ;
2023-11-29 10:51:40 +01:00
_transactionLinkProviders = transactionLinkProviders ;
2024-04-04 09:31:04 +02:00
_paymentModelExtensions = paymentModelExtensions ;
_viewProvider = viewProvider ;
2024-08-30 01:34:23 +02:00
_prettyName = prettyName ;
2023-07-24 15:57:24 +02:00
_fileService = fileService ;
2024-05-09 02:18:02 +02:00
_uriResolver = uriResolver ;
2024-05-13 15:29:42 +02:00
_defaultRules = defaultRules ;
2023-05-19 03:42:09 +02:00
_appService = appService ;
2017-10-27 10:53:04 +02:00
}
2017-09-13 08:47:34 +02:00
2023-04-24 11:04:46 +02:00
internal async Task < InvoiceEntity > CreatePaymentRequestInvoice ( Data . PaymentRequestData prData , decimal? amount , decimal amountDue , StoreData storeData , HttpRequest request , CancellationToken cancellationToken )
2022-11-02 10:41:19 +01:00
{
2023-04-24 11:04:46 +02:00
var id = prData . Id ;
var prBlob = prData . GetBlob ( ) ;
if ( prBlob . AllowCustomPaymentAmounts & & amount ! = null )
amount = Math . Min ( amountDue , amount . Value ) ;
2022-11-02 10:41:19 +01:00
else
2023-04-24 11:04:46 +02:00
amount = amountDue ;
var redirectUrl = _linkGenerator . PaymentRequestLink ( id , request . Scheme , request . Host , request . PathBase ) ;
2022-11-02 10:41:19 +01:00
2023-04-26 14:06:42 +02:00
JObject invoiceMetadata = prData . GetBlob ( ) ? . FormResponse ? ? new JObject ( ) ;
2023-04-24 11:04:46 +02:00
invoiceMetadata . Merge ( new InvoiceMetadata
{
OrderId = PaymentRequestRepository . GetOrderIdForPaymentRequest ( id ) ,
PaymentRequestId = id ,
2024-04-04 09:31:04 +02:00
BuyerEmail = invoiceMetadata . TryGetValue ( "buyerEmail" , out var formEmail ) & & formEmail . Type = = JTokenType . String ? formEmail . Value < string > ( ) :
2023-04-26 14:06:42 +02:00
string . IsNullOrEmpty ( prBlob . Email ) ? null : prBlob . Email
2023-04-24 11:04:46 +02:00
} . ToJObject ( ) , new JsonMergeSettings ( ) { MergeNullValueHandling = MergeNullValueHandling . Ignore } ) ;
2022-11-02 10:41:19 +01:00
var invoiceRequest =
new CreateInvoiceRequest
{
2023-04-24 11:04:46 +02:00
Metadata = invoiceMetadata ,
Currency = prBlob . Currency ,
2022-11-02 10:41:19 +01:00
Amount = amount ,
2022-11-25 03:04:34 +01:00
Checkout = { RedirectURL = redirectUrl } ,
Receipt = new InvoiceDataBase . ReceiptOptions { Enabled = false }
2022-11-02 10:41:19 +01:00
} ;
2023-04-24 11:04:46 +02:00
var additionalTags = new List < string > { PaymentRequestRepository . GetInternalTag ( id ) } ;
2022-11-02 10:41:19 +01:00
return await CreateInvoiceCoreRaw ( invoiceRequest , storeData , request . GetAbsoluteRoot ( ) , additionalTags , cancellationToken ) ;
}
2023-03-17 03:56:32 +01:00
[NonAction]
public async Task < InvoiceEntity > CreateInvoiceCoreRaw ( CreateInvoiceRequest invoice , StoreData store , string serverUrl , List < string > ? additionalTags = null , CancellationToken cancellationToken = default , Action < InvoiceEntity > ? entityManipulator = null )
2020-08-26 07:01:39 +02:00
{
var storeBlob = store . GetStoreBlob ( ) ;
2023-07-19 11:47:32 +02:00
var entity = _InvoiceRepository . CreateNewInvoice ( store . Id ) ;
2021-10-29 14:50:18 +02:00
entity . ServerUrl = serverUrl ;
2020-08-26 07:01:39 +02:00
entity . ExpirationTime = entity . InvoiceTime + ( invoice . Checkout . Expiration ? ? storeBlob . InvoiceExpiration ) ;
entity . MonitoringExpiration = entity . ExpirationTime + ( invoice . Checkout . Monitoring ? ? storeBlob . MonitoringExpiration ) ;
2022-07-06 14:14:55 +02:00
entity . ReceiptOptions = invoice . Receipt ? ? new InvoiceDataBase . ReceiptOptions ( ) ;
2020-08-26 07:01:39 +02:00
if ( invoice . Metadata ! = null )
entity . Metadata = InvoiceMetadata . FromJObject ( invoice . Metadata ) ;
invoice . Checkout ? ? = new CreateInvoiceRequest . CheckoutOptions ( ) ;
entity . Currency = invoice . Currency ;
2021-08-03 10:03:00 +02:00
if ( invoice . Amount is decimal v )
{
entity . Price = v ;
entity . Type = InvoiceType . Standard ;
}
else
{
entity . Price = 0.0 m ;
entity . Type = InvoiceType . TopUp ;
}
2020-08-26 07:01:39 +02:00
entity . SpeedPolicy = invoice . Checkout . SpeedPolicy ? ? store . SpeedPolicy ;
2020-12-10 15:34:50 +01:00
entity . DefaultLanguage = invoice . Checkout . DefaultLanguage ;
2024-04-04 09:31:04 +02:00
if ( invoice . Checkout . DefaultPaymentMethod is not null & & PaymentMethodId . TryParse ( invoice . Checkout . DefaultPaymentMethod , out var paymentMethodId ) )
{
entity . DefaultPaymentMethod = paymentMethodId ;
}
2021-03-01 14:34:07 +01:00
entity . RedirectAutomatically = invoice . Checkout . RedirectAutomatically ? ? storeBlob . RedirectAutomatically ;
2023-04-24 16:58:58 +02:00
entity . LazyPaymentMethods = invoice . Checkout . LazyPaymentMethods ? ? storeBlob . LazyPaymentMethods ;
2021-08-23 08:13:26 +02:00
IPaymentFilter ? excludeFilter = null ;
2020-08-26 07:01:39 +02:00
if ( invoice . Checkout . PaymentMethods ! = null )
{
var supportedTransactionCurrencies = invoice . Checkout . PaymentMethods
. Select ( c = > PaymentMethodId . TryParse ( c , out var p ) ? p : null )
. ToHashSet ( ) ;
excludeFilter = PaymentFilter . Where ( p = > ! supportedTransactionCurrencies . Contains ( p ) ) ;
2019-02-21 11:34:11 +01:00
}
2020-08-26 07:01:39 +02:00
entity . PaymentTolerance = invoice . Checkout . PaymentTolerance ? ? storeBlob . PaymentTolerance ;
2020-12-09 15:20:13 +01:00
entity . RedirectURLTemplate = invoice . Checkout . RedirectURL ? . Trim ( ) ;
2020-09-21 12:33:46 +02:00
if ( additionalTags ! = null )
entity . InternalTags . AddRange ( additionalTags ) ;
2022-07-22 13:29:34 +02:00
return await CreateInvoiceCoreRaw ( entity , store , excludeFilter , invoice . AdditionalSearchTerms , cancellationToken , entityManipulator ) ;
2020-08-26 07:01:39 +02:00
}
2019-02-21 11:34:11 +01:00
2023-01-06 14:18:07 +01:00
internal async Task < InvoiceEntity > CreateInvoiceCoreRaw ( InvoiceEntity entity , StoreData store , IPaymentFilter ? invoicePaymentMethodFilter , string [ ] ? additionalSearchTerms = null , CancellationToken cancellationToken = default , Action < InvoiceEntity > ? entityManipulator = null )
2020-08-26 07:01:39 +02:00
{
InvoiceLogs logs = new InvoiceLogs ( ) ;
2020-08-28 08:49:13 +02:00
logs . Write ( "Creation of invoice starting" , InvoiceEventData . EventSeverity . Info ) ;
2021-10-20 16:17:40 +02:00
var storeBlob = store . GetStoreBlob ( ) ;
if ( string . IsNullOrEmpty ( entity . Currency ) )
entity . Currency = storeBlob . DefaultCurrency ;
entity . Currency = entity . Currency . Trim ( ) . ToUpperInvariant ( ) ;
2023-06-16 03:47:58 +02:00
entity . Price = Math . Min ( GreenfieldConstants . MaxAmount , entity . Price ) ;
2021-07-30 11:46:49 +02:00
entity . Price = Math . Max ( 0.0 m , entity . Price ) ;
var currencyInfo = _CurrencyNameTable . GetNumberFormatInfo ( entity . Currency , false ) ;
if ( currencyInfo ! = null )
{
entity . Price = entity . Price . RoundToSignificant ( currencyInfo . CurrencyDecimalDigits ) ;
}
if ( entity . Metadata . TaxIncluded is decimal taxIncluded )
{
if ( currencyInfo ! = null )
{
taxIncluded = taxIncluded . RoundToSignificant ( currencyInfo . CurrencyDecimalDigits ) ;
}
taxIncluded = Math . Max ( 0.0 m , taxIncluded ) ;
taxIncluded = Math . Min ( taxIncluded , entity . Price ) ;
entity . Metadata . TaxIncluded = taxIncluded ;
}
2020-08-26 07:01:39 +02:00
var getAppsTaggingStore = _InvoiceRepository . GetAppsTaggingStore ( store . Id ) ;
2024-05-15 00:49:53 +02:00
entity . Status = InvoiceStatus . New ;
2023-07-19 11:47:32 +02:00
entity . UpdateTotals ( ) ;
2020-03-10 08:33:50 +01:00
2020-08-26 07:01:39 +02:00
2024-04-04 09:31:04 +02:00
var creationContext = new InvoiceCreationContext ( store , storeBlob , entity , logs , _handlers , invoicePaymentMethodFilter ) ;
creationContext . SetLazyActivation ( entity . LazyPaymentMethods ) ;
foreach ( var term in additionalSearchTerms ? ? Array . Empty < string > ( ) )
creationContext . AdditionalSearchTerms . Add ( term ) ;
2018-03-25 18:57:44 +02:00
2024-04-04 09:31:04 +02:00
if ( entity . Type = = InvoiceType . TopUp | | entity . Price ! = 0 m )
2018-03-25 18:57:44 +02:00
{
2024-04-04 09:31:04 +02:00
await creationContext . BeforeFetchingRates ( ) ;
await FetchRates ( creationContext , cancellationToken ) ;
2018-03-25 18:57:44 +02:00
2024-04-04 09:31:04 +02:00
await creationContext . CreatePaymentPrompts ( ) ;
var contexts = creationContext . PaymentMethodContexts
. Where ( s = > s . Value . Status is PaymentMethodContext . ContextStatus . WaitingForActivation or PaymentMethodContext . ContextStatus . Created )
. Select ( s = > s . Value )
. ToList ( ) ;
if ( contexts . Count = = 0 )
2021-09-06 17:23:41 +02:00
{
StringBuilder errors = new StringBuilder ( ) ;
2024-04-04 09:31:04 +02:00
if ( ! store . GetPaymentMethodConfigs ( _handlers ) . Any ( ) )
2021-09-06 17:23:41 +02:00
errors . AppendLine (
"Warning: No wallet has been linked to your BTCPay Store. See the following link for more information on how to connect your store and wallet. (https://docs.btcpayserver.org/WalletSetup/)" ) ;
2022-07-08 03:23:26 +02:00
else
2022-08-16 09:04:13 +02:00
errors . AppendLine ( "Warning: You have payment methods configured but none of them match any of the requested payment methods or the rate is not available. See logs below:" ) ;
2021-09-06 17:23:41 +02:00
foreach ( var error in logs . ToList ( ) )
{
errors . AppendLine ( error . ToString ( ) ) ;
}
throw new BitpayHttpException ( 400 , errors . ToString ( ) ) ;
}
2024-04-04 09:31:04 +02:00
entity . SetPaymentPrompts ( new PaymentPromptDictionary ( contexts . Select ( c = > c . Prompt ) ) ) ;
2021-09-06 17:23:41 +02:00
}
2024-04-04 09:31:04 +02:00
else
{
entity . SetPaymentPrompts ( new PaymentPromptDictionary ( ) ) ;
}
2019-02-19 04:48:08 +01:00
foreach ( var app in await getAppsTaggingStore )
{
2019-02-19 05:04:58 +01:00
entity . InternalTags . Add ( AppService . GetAppInternalTag ( app . Id ) ) ;
2019-02-19 04:48:08 +01:00
}
2022-07-22 13:29:34 +02:00
if ( entityManipulator ! = null )
{
entityManipulator . Invoke ( entity ) ;
}
2019-04-03 08:00:09 +02:00
using ( logs . Measure ( "Saving invoice" ) )
{
2024-04-04 09:31:04 +02:00
await _InvoiceRepository . CreateInvoiceAsync ( creationContext ) ;
await creationContext . ActivatingPaymentPrompt ( ) ;
2019-04-03 08:00:09 +02:00
}
2024-04-04 09:31:04 +02:00
_ = _InvoiceRepository . AddInvoiceLogs ( entity . Id , logs ) ;
2020-07-24 09:40:37 +02:00
_EventAggregator . Publish ( new Events . InvoiceEvent ( entity , InvoiceEvent . Created ) ) ;
2020-07-22 13:58:41 +02:00
return entity ;
2017-10-27 10:53:04 +02:00
}
2017-09-13 08:47:34 +02:00
2024-04-04 09:31:04 +02:00
private async Task FetchRates ( InvoiceCreationContext context , CancellationToken cancellationToken )
2018-03-28 15:37:01 +02:00
{
2024-05-13 15:29:42 +02:00
var rateRules = context . StoreBlob . GetRateRules ( _defaultRules ) ;
2024-04-04 09:31:04 +02:00
await context . FetchingRates ( _RateProvider , rateRules , cancellationToken ) ;
2018-03-28 15:37:01 +02:00
}
2017-10-27 10:53:04 +02:00
}
2017-09-13 08:47:34 +02:00
}