2021-07-14 04:40:18 -07:00
#nullable enable
2020-06-28 21:44:35 -05:00
using System ;
2017-09-13 15:47:34 +09:00
using System.Collections.Generic ;
using System.Linq ;
2017-12-17 19:58:55 +09:00
using System.Net.WebSockets ;
using System.Threading ;
2018-08-30 11:34:39 -05:00
using System.Threading.Tasks ;
2020-11-17 13:46:23 +01:00
using BTCPayServer.Abstractions.Constants ;
2024-06-14 16:50:17 +02:00
using BTCPayServer.Abstractions.Converters ;
2020-11-17 13:46:23 +01:00
using BTCPayServer.Abstractions.Extensions ;
using BTCPayServer.Abstractions.Models ;
2020-03-19 19:11:15 +09:00
using BTCPayServer.Client ;
2020-05-23 21:13:18 +02:00
using BTCPayServer.Client.Models ;
2018-08-30 11:34:39 -05:00
using BTCPayServer.Data ;
using BTCPayServer.Filters ;
2020-09-02 11:24:18 +02:00
using BTCPayServer.HostedServices ;
2022-02-10 12:24:28 +09:00
using BTCPayServer.Models ;
2023-07-20 09:03:39 +02:00
using BTCPayServer.Models.AppViewModels ;
2018-08-30 11:34:39 -05:00
using BTCPayServer.Models.InvoicingModels ;
2022-07-06 14:14:55 +02:00
using BTCPayServer.Models.PaymentRequestViewModels ;
2018-02-19 02:38:03 +09:00
using BTCPayServer.Payments ;
2024-04-04 16:31:04 +09:00
using BTCPayServer.Payments.Bitcoin ;
using BTCPayServer.Payments.Lightning ;
2024-05-01 10:22:07 +09:00
using BTCPayServer.Payouts ;
2020-09-02 11:24:18 +02:00
using BTCPayServer.Rating ;
2023-03-13 02:12:58 +01:00
using BTCPayServer.Services ;
2021-10-27 07:32:56 -07:00
using BTCPayServer.Services.Apps ;
2018-08-30 11:34:39 -05:00
using BTCPayServer.Services.Invoices ;
2020-09-02 11:24:18 +02:00
using BTCPayServer.Services.Rates ;
2018-08-30 11:34:39 -05:00
using Microsoft.AspNetCore.Authorization ;
using Microsoft.AspNetCore.Mvc ;
using Microsoft.AspNetCore.Mvc.Rendering ;
2022-07-06 14:14:55 +02:00
using Microsoft.AspNetCore.Routing ;
2020-06-24 17:51:00 +09:00
using Microsoft.EntityFrameworkCore ;
2018-08-30 11:34:39 -05:00
using NBitcoin ;
using NBXplorer ;
2018-11-27 07:13:09 +01:00
using Newtonsoft.Json.Linq ;
2020-05-23 21:13:18 +02:00
using StoreData = BTCPayServer . Data . StoreData ;
2017-09-13 15:47:34 +09:00
namespace BTCPayServer.Controllers
{
2022-01-07 12:32:00 +09:00
public partial class UIInvoiceController
2017-10-27 17:53:04 +09:00
{
2023-02-25 23:34:49 +09:00
static UIInvoiceController ( )
{
InvoiceAdditionalDataExclude =
typeof ( InvoiceMetadata )
. GetProperties ( )
. Select ( p = > p . Name )
. ToHashSet ( StringComparer . OrdinalIgnoreCase ) ;
InvoiceAdditionalDataExclude . Remove ( nameof ( InvoiceMetadata . PosData ) ) ;
}
static readonly HashSet < string > InvoiceAdditionalDataExclude ;
2021-11-15 09:35:03 +01:00
[HttpGet("invoices/{invoiceId}/deliveries/{deliveryId}/request")]
2020-11-06 20:42:26 +09:00
[Authorize(AuthenticationSchemes = AuthenticationSchemes.Cookie)]
public async Task < IActionResult > WebhookDelivery ( string invoiceId , string deliveryId )
{
2021-12-16 17:37:19 +01:00
var invoice = ( await _InvoiceRepository . GetInvoices ( new InvoiceQuery
2020-11-06 20:42:26 +09:00
{
InvoiceId = new [ ] { invoiceId } ,
UserId = GetUserId ( )
} ) ) . FirstOrDefault ( ) ;
if ( invoice is null )
return NotFound ( ) ;
var delivery = await _InvoiceRepository . GetWebhookDelivery ( invoiceId , deliveryId ) ;
if ( delivery is null )
return NotFound ( ) ;
2021-11-15 09:35:03 +01:00
return File ( delivery . GetBlob ( ) . Request , "application/json" ) ;
2020-11-06 20:42:26 +09:00
}
2021-12-31 16:59:02 +09:00
2021-11-15 09:35:03 +01:00
[HttpPost("invoices/{invoiceId}/deliveries/{deliveryId}/redeliver")]
2020-11-06 20:42:26 +09:00
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
public async Task < IActionResult > RedeliverWebhook ( string storeId , string invoiceId , string deliveryId )
{
var invoice = ( await _InvoiceRepository . GetInvoices ( new InvoiceQuery ( )
{
InvoiceId = new [ ] { invoiceId } ,
StoreId = new [ ] { storeId } ,
UserId = GetUserId ( )
} ) ) . FirstOrDefault ( ) ;
if ( invoice is null )
return NotFound ( ) ;
var delivery = await _InvoiceRepository . GetWebhookDelivery ( invoiceId , deliveryId ) ;
if ( delivery is null )
return NotFound ( ) ;
var newDeliveryId = await WebhookNotificationManager . Redeliver ( deliveryId ) ;
if ( newDeliveryId is null )
return NotFound ( ) ;
TempData [ WellKnownTempData . SuccessMessage ] = "Successfully planned a redelivery" ;
return RedirectToAction ( nameof ( Invoice ) ,
new
{
invoiceId
} ) ;
}
2021-11-15 09:35:03 +01:00
[HttpGet("invoices/{invoiceId}")]
2022-07-07 05:47:59 -07:00
[HttpGet("/stores/{storeId}/invoices/${invoiceId}")]
2022-07-06 14:14:55 +02:00
[Authorize(Policy = Policies.CanViewInvoices, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
2018-01-08 20:06:16 +09:00
public async Task < IActionResult > Invoice ( string invoiceId )
2017-10-27 17:53:04 +09:00
{
2022-06-15 04:17:10 +02:00
var invoice = ( await _InvoiceRepository . GetInvoices ( new InvoiceQuery
2017-10-27 17:53:04 +09:00
{
2020-06-28 17:55:27 +09:00
InvoiceId = new [ ] { invoiceId } ,
2018-12-06 16:58:04 +09:00
UserId = GetUserId ( ) ,
2018-01-14 21:48:23 +09:00
IncludeAddresses = true ,
2020-05-07 12:50:07 +02:00
IncludeArchived = true ,
2022-06-03 12:08:16 +02:00
IncludeRefunds = true ,
2017-10-27 17:53:04 +09:00
} ) ) . FirstOrDefault ( ) ;
if ( invoice = = null )
return NotFound ( ) ;
2017-10-13 00:25:45 +09:00
2017-10-27 17:53:04 +09:00
var store = await _StoreRepository . FindStore ( invoice . StoreId ) ;
2022-06-15 04:17:10 +02:00
if ( store = = null )
return NotFound ( ) ;
2022-07-06 14:14:55 +02:00
var receipt = InvoiceDataBase . ReceiptOptions . Merge ( store . GetStoreBlob ( ) . ReceiptOptions , invoice . ReceiptOptions ) ;
2021-10-05 22:49:57 -07:00
var invoiceState = invoice . GetInvoiceState ( ) ;
2023-02-25 23:34:49 +09:00
var metaData = PosDataParser . ParsePosData ( invoice . Metadata . ToJObject ( ) ) ;
2023-02-25 14:38:28 +01:00
var additionalData = metaData
2023-02-25 23:34:49 +09:00
. Where ( dict = > ! InvoiceAdditionalDataExclude . Contains ( dict . Key ) )
2023-04-10 11:07:03 +09:00
. ToDictionary ( dict = > dict . Key , dict = > dict . Value ) ;
2024-04-04 16:31:04 +09:00
2021-12-11 04:32:23 +01:00
var model = new InvoiceDetailsModel
2017-10-27 17:53:04 +09:00
{
2020-11-06 20:42:26 +09:00
StoreId = store . Id ,
2017-10-27 17:53:04 +09:00
StoreName = store . StoreName ,
2022-01-20 12:52:31 +01:00
StoreLink = Url . Action ( nameof ( UIStoresController . GeneralSettings ) , "UIStores" , new { storeId = store . Id } ) ,
2022-01-07 12:32:00 +09:00
PaymentRequestLink = Url . Action ( nameof ( UIPaymentRequestController . ViewPaymentRequest ) , "UIPaymentRequest" , new { payReqId = invoice . Metadata . PaymentRequestId } ) ,
2017-10-27 17:53:04 +09:00
Id = invoice . Id ,
2022-01-11 21:49:56 +09:00
State = invoiceState ,
2018-05-13 15:09:17 +09:00
TransactionSpeed = invoice . SpeedPolicy = = SpeedPolicy . HighSpeed ? "high" :
2018-05-11 22:12:45 +09:00
invoice . SpeedPolicy = = SpeedPolicy . MediumSpeed ? "medium" :
invoice . SpeedPolicy = = SpeedPolicy . LowMediumSpeed ? "low-medium" :
"low" ,
2017-10-27 17:53:04 +09:00
CreatedDate = invoice . InvoiceTime ,
ExpirationDate = invoice . ExpirationTime ,
2018-01-08 20:06:16 +09:00
MonitoringDate = invoice . MonitoringExpiration ,
2023-03-13 02:12:58 +01:00
Fiat = _displayFormatter . Currency ( invoice . Price , invoice . Currency ) ,
2022-06-03 12:08:16 +02:00
TaxIncluded = invoice . Metadata . TaxIncluded is null
? null
2023-03-13 02:12:58 +01:00
: _displayFormatter . Currency ( invoice . Metadata . TaxIncluded ? ? 0.0 m , invoice . Currency ) ,
2019-09-05 11:41:51 +09:00
NotificationUrl = invoice . NotificationURL ? . AbsoluteUri ,
2019-09-05 11:55:31 +09:00
RedirectUrl = invoice . RedirectURL ? . AbsoluteUri ,
2020-08-25 14:33:00 +09:00
TypedMetadata = invoice . Metadata ,
2018-01-14 21:48:23 +09:00
StatusException = invoice . ExceptionStatus ,
2024-04-25 14:09:01 +09:00
Events = await _InvoiceRepository . GetInvoiceLogs ( invoice . Id ) ,
2023-02-25 14:38:28 +01:00
Metadata = metaData ,
2020-06-24 17:51:00 +09:00
Archived = invoice . Archived ,
2023-10-11 16:12:45 +02:00
HasRefund = invoice . Refunds . Any ( ) ,
2022-11-28 00:53:08 -08:00
CanRefund = invoiceState . CanRefund ( ) ,
2022-06-03 12:08:16 +02:00
Refunds = invoice . Refunds ,
2024-05-15 07:49:53 +09:00
ShowCheckout = invoice . Status = = InvoiceStatus . New ,
ShowReceipt = invoice . Status = = InvoiceStatus . Settled & & ( invoice . ReceiptOptions ? . Enabled ? ? receipt . Enabled is true ) ,
2020-11-06 20:42:26 +09:00
Deliveries = ( await _InvoiceRepository . GetWebhookDeliveries ( invoiceId ) )
. Select ( c = > new Models . StoreViewModels . DeliveryViewModel ( c ) )
2023-10-11 16:12:45 +02:00
. ToList ( )
2017-10-27 17:53:04 +09:00
} ;
2019-04-29 22:45:33 -05:00
2019-05-07 23:32:47 +09:00
var details = InvoicePopulatePayments ( invoice ) ;
2019-04-29 22:45:33 -05:00
model . CryptoPayments = details . CryptoPayments ;
2019-08-24 16:10:13 +02:00
model . Payments = details . Payments ;
2022-10-05 20:59:05 -07:00
model . Overpaid = details . Overpaid ;
2023-10-10 05:28:00 +02:00
model . StillDue = details . StillDue ;
model . HasRates = details . HasRates ;
2024-04-04 16:31:04 +09:00
2024-04-24 10:22:00 +02:00
if ( additionalData . TryGetValue ( "receiptData" , out object? receiptData ) )
2023-08-10 14:57:54 +03:00
{
2024-04-24 10:22:00 +02:00
model . ReceiptData = ( Dictionary < string , object > ) receiptData ;
2023-08-10 14:57:54 +03:00
additionalData . Remove ( "receiptData" ) ;
}
2024-04-04 16:31:04 +09:00
2023-08-26 13:48:48 +02:00
if ( additionalData . ContainsKey ( "posData" ) & & additionalData [ "posData" ] is string posData )
{
// overwrite with parsed JSON if possible
try
{
additionalData [ "posData" ] = PosDataParser . ParsePosData ( JObject . Parse ( posData ) ) ;
}
catch ( Exception )
{
additionalData [ "posData" ] = posData ;
}
}
2024-04-04 16:31:04 +09:00
2023-08-10 14:57:54 +03:00
model . AdditionalData = additionalData ;
2021-12-31 16:59:02 +09:00
2019-04-29 22:45:33 -05:00
return View ( model ) ;
}
2023-01-06 14:18:07 +01:00
2022-07-06 14:14:55 +02:00
[HttpGet("i/{invoiceId}/receipt")]
2023-06-22 08:57:29 +02:00
public async Task < IActionResult > InvoiceReceipt ( string invoiceId , [ FromQuery ] bool print = false )
2022-07-06 14:14:55 +02:00
{
var i = await _InvoiceRepository . GetInvoice ( invoiceId ) ;
if ( i is null )
return NotFound ( ) ;
var store = await _StoreRepository . GetStoreByInvoiceId ( i . Id ) ;
if ( store is null )
return NotFound ( ) ;
var receipt = InvoiceDataBase . ReceiptOptions . Merge ( store . GetStoreBlob ( ) . ReceiptOptions , i . ReceiptOptions ) ;
2023-01-06 14:18:07 +01:00
if ( receipt . Enabled is not true )
2023-04-26 09:45:35 +02:00
{
if ( i . RedirectURL is not null )
{
return Redirect ( i . RedirectURL . ToString ( ) ) ;
2024-04-04 16:31:04 +09:00
}
2023-01-06 14:18:07 +01:00
return NotFound ( ) ;
2022-12-19 15:51:05 +01:00
2023-04-26 09:45:35 +02:00
}
2022-12-19 15:51:05 +01:00
var storeBlob = store . GetStoreBlob ( ) ;
var vm = new InvoiceReceiptViewModel
{
InvoiceId = i . Id ,
OrderId = i . Metadata ? . OrderId ,
2024-07-09 16:56:34 +02:00
RedirectUrl = i . RedirectURL ? . AbsoluteUri ? ? i . Metadata ? . OrderUrl ,
2024-05-15 07:49:53 +09:00
Status = i . Status ,
2022-12-19 15:51:05 +01:00
Currency = i . Currency ,
Timestamp = i . InvoiceTime ,
StoreName = store . StoreName ,
2024-05-09 02:18:02 +02:00
StoreBranding = await StoreBrandingViewModel . CreateAsync ( Request , _uriResolver , storeBlob ) ,
2022-12-19 15:51:05 +01:00
ReceiptOptions = receipt
} ;
2023-04-10 11:07:03 +09:00
2024-05-15 07:49:53 +09:00
if ( i . Status ! = InvoiceStatus . Settled )
2022-07-06 14:14:55 +02:00
{
2022-12-19 15:51:05 +01:00
return View ( vm ) ;
2022-07-06 14:14:55 +02:00
}
2024-04-24 10:22:00 +02:00
var metaData = PosDataParser . ParsePosData ( i . Metadata ? . ToJObject ( ) ) ;
var additionalData = metaData
. Where ( dict = > ! InvoiceAdditionalDataExclude . Contains ( dict . Key ) )
. ToDictionary ( dict = > dict . Key , dict = > dict . Value ) ;
// Split receipt data into cart and additional data
if ( additionalData . TryGetValue ( "receiptData" , out object? combinedReceiptData ) )
{
var receiptData = new Dictionary < string , object > ( ( Dictionary < string , object > ) combinedReceiptData , StringComparer . OrdinalIgnoreCase ) ;
string [ ] cartKeys = [ "cart" , "subtotal" , "discount" , "tip" , "total" ] ;
// extract cart data and lowercase keys to handle data uniformly in PosData partial
if ( receiptData . Keys . Any ( key = > cartKeys . Contains ( key . ToLowerInvariant ( ) ) ) )
{
vm . CartData = new Dictionary < string , object > ( ) ;
foreach ( var key in cartKeys )
{
if ( ! receiptData . ContainsKey ( key ) ) continue ;
// add it to cart data and remove it from the general data
vm . CartData . Add ( key . ToLowerInvariant ( ) , receiptData [ key ] ) ;
receiptData . Remove ( key ) ;
}
}
2024-07-09 16:56:34 +02:00
// assign the rest to additional data and remove empty values
2024-04-24 10:22:00 +02:00
if ( receiptData . Any ( ) )
{
2024-07-09 16:56:34 +02:00
vm . AdditionalData = receiptData
. Where ( x = > ! string . IsNullOrEmpty ( x . Value . ToString ( ) ) )
. ToDictionary ( x = > x . Key , x = > x . Value ) ;
2024-04-24 10:22:00 +02:00
}
}
2023-01-06 14:18:07 +01:00
2024-04-04 16:31:04 +09:00
var payments = ViewPaymentRequestViewModel . PaymentRequestInvoicePayment . GetViewModels ( i , _displayFormatter , _transactionLinkProviders , _handlers ) ;
2023-08-23 10:43:34 +09:00
vm . Amount = i . PaidAmount . Net ;
2022-12-19 15:51:05 +01:00
vm . Payments = receipt . ShowPayments is false ? null : payments ;
2023-06-22 08:57:29 +02:00
return View ( print ? "InvoiceReceiptPrint" : "InvoiceReceipt" , vm ) ;
2022-07-06 14:14:55 +02:00
}
2023-03-27 07:07:12 +02:00
2021-11-15 09:35:03 +01:00
[HttpGet("invoices/{invoiceId}/refund")]
2023-03-20 02:46:46 +01:00
[Authorize(Policy = Policies.CanCreateNonApprovedPullPayments, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
2024-05-01 10:22:07 +09:00
public async Task < IActionResult > Refund ( string invoiceId , CancellationToken cancellationToken )
2020-06-24 17:51:00 +09:00
{
2021-11-04 08:21:01 +01:00
await using var ctx = _dbContextFactory . CreateContext ( ) ;
2021-11-15 09:35:03 +01:00
ctx . ChangeTracker . QueryTrackingBehavior = QueryTrackingBehavior . NoTracking ;
2020-06-24 17:51:00 +09:00
var invoice = await ctx . Invoices . Include ( i = > i . Payments )
2023-11-21 12:52:40 +09:00
. Include ( i = > i . Refunds ) . ThenInclude ( i = > i . PullPaymentData )
2021-11-04 08:21:01 +01:00
. Include ( i = > i . StoreData )
. ThenInclude ( data = > data . UserStores )
2020-06-24 17:51:00 +09:00
. Where ( i = > i . Id = = invoiceId )
2021-11-04 08:21:01 +01:00
. FirstOrDefaultAsync ( cancellationToken ) ;
2020-06-24 17:51:00 +09:00
if ( invoice is null )
return NotFound ( ) ;
2023-11-21 12:52:40 +09:00
var currentRefund = invoice . Refunds . OrderByDescending ( r = > r . PullPaymentData . StartDate ) . FirstOrDefault ( ) ;
if ( currentRefund ? . PullPaymentDataId is null & & GetUserId ( ) is null )
2020-06-24 17:51:00 +09:00
return NotFound ( ) ;
2022-11-28 00:53:08 -08:00
if ( ! invoice . GetInvoiceState ( ) . CanRefund ( ) )
2020-06-24 17:51:00 +09:00
return NotFound ( ) ;
2023-11-21 12:52:40 +09:00
if ( currentRefund ? . PullPaymentDataId is string ppId & & ! currentRefund . PullPaymentData . Archived )
2020-06-24 17:51:00 +09:00
{
// TODO: Having dedicated UI later on
2022-01-07 12:32:00 +09:00
return RedirectToAction ( nameof ( UIPullPaymentController . ViewPullPayment ) ,
"UIPullPayment" ,
2020-06-24 17:51:00 +09:00
new { pullPaymentId = ppId } ) ;
}
2021-12-31 16:59:02 +09:00
2024-05-01 10:22:07 +09:00
var payoutMethodIds = _payoutHandlers . GetSupportedPayoutMethods ( this . GetCurrentStore ( ) ) ;
if ( ! payoutMethodIds . Any ( ) )
2020-06-24 17:51:00 +09:00
{
2022-06-30 11:25:02 +02:00
var vm = new RefundModel { Title = "No matching payment method" } ;
2023-01-06 14:18:07 +01:00
ModelState . AddModelError ( nameof ( vm . AvailablePaymentMethods ) ,
2022-06-30 11:25:02 +02:00
"There are no payment methods available to provide refunds with for this invoice." ) ;
return View ( "_RefundModal" , vm ) ;
2021-11-15 09:35:03 +01:00
}
2021-12-31 16:59:02 +09:00
2024-05-01 10:22:07 +09:00
// Find the most similar payment method to the one used for the invoice
var defaultRefund =
2024-07-04 16:43:30 +09:00
invoice . GetClosestPayoutMethodId ( payoutMethodIds ) ;
2022-06-30 11:25:02 +02:00
2021-12-16 15:05:47 +01:00
var refund = new RefundModel
{
2022-06-02 10:08:55 +02:00
Title = "Payment method" ,
2021-12-16 15:05:47 +01:00
AvailablePaymentMethods =
2024-05-01 10:22:07 +09:00
new SelectList ( payoutMethodIds . Select ( id = > new SelectListItem ( id . ToString ( ) , id . ToString ( ) ) ) ,
2021-12-16 15:05:47 +01:00
"Value" , "Text" ) ,
2024-05-01 10:22:07 +09:00
SelectedPayoutMethod = defaultRefund ? . ToString ( ) ? ? payoutMethodIds . First ( ) . ToString ( )
2021-12-16 15:05:47 +01:00
} ;
2021-11-15 09:35:03 +01:00
// Nothing to select, skip to next
if ( refund . AvailablePaymentMethods . Count ( ) = = 1 )
{
return await Refund ( invoiceId , refund , cancellationToken ) ;
2020-06-24 17:51:00 +09:00
}
2022-06-02 10:08:55 +02:00
return View ( "_RefundModal" , refund ) ;
2020-06-24 17:51:00 +09:00
}
2021-12-31 16:59:02 +09:00
2021-11-15 09:35:03 +01:00
[HttpPost("invoices/{invoiceId}/refund")]
2023-03-20 02:46:46 +01:00
[Authorize(Policy = Policies.CanCreateNonApprovedPullPayments, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
2020-06-24 17:51:00 +09:00
public async Task < IActionResult > Refund ( string invoiceId , RefundModel model , CancellationToken cancellationToken )
{
2022-04-28 13:50:28 +02:00
await using var ctx = _dbContextFactory . CreateContext ( ) ;
2021-12-16 17:37:19 +01:00
2021-12-20 15:15:32 +01:00
var invoice = GetCurrentInvoice ( ) ;
if ( invoice = = null )
2020-06-24 17:51:00 +09:00
return NotFound ( ) ;
2021-12-11 04:32:23 +01:00
2022-11-28 00:53:08 -08:00
if ( ! invoice . GetInvoiceState ( ) . CanRefund ( ) )
2020-06-24 17:51:00 +09:00
return NotFound ( ) ;
2021-12-31 16:59:02 +09:00
2021-12-20 15:15:32 +01:00
var store = GetCurrentStore ( ) ;
2024-05-01 10:22:07 +09:00
var pmi = PayoutMethodId . Parse ( model . SelectedPayoutMethod ) ;
2021-12-20 15:15:32 +01:00
var cdCurrency = _CurrencyNameTable . GetCurrencyData ( invoice . Currency , true ) ;
2020-09-02 11:24:18 +02:00
RateRules rules ;
RateResult rateResult ;
CreatePullPayment createPullPayment ;
2024-05-01 10:22:07 +09:00
var pmis = _payoutHandlers . GetSupportedPayoutMethods ( store ) ;
if ( ! pmis . Contains ( pmi ) )
2023-05-11 10:33:33 +02:00
{
2024-05-01 10:22:07 +09:00
ModelState . AddModelError ( nameof ( model . SelectedPayoutMethod ) , $"Invalid payout method" ) ;
return View ( "_RefundModal" , model ) ;
2023-05-11 10:33:33 +02:00
}
2024-05-01 10:22:07 +09:00
2024-07-04 16:43:30 +09:00
var paymentMethodId = invoice . GetClosestPaymentMethodId ( [ pmi ] ) ;
2024-05-01 10:22:07 +09:00
var paymentMethod = paymentMethodId is null ? null : invoice . GetPaymentPrompt ( paymentMethodId ) ;
if ( paymentMethod ? . Currency is null )
2023-05-11 10:33:33 +02:00
{
2024-05-01 10:22:07 +09:00
ModelState . AddModelError ( nameof ( model . SelectedPayoutMethod ) , $"Invalid payout method" ) ;
2024-04-04 16:31:04 +09:00
return View ( "_RefundModal" , model ) ;
2023-05-11 10:33:33 +02:00
}
2024-04-04 16:31:04 +09:00
var accounting = paymentMethod . Calculate ( ) ;
2024-07-04 16:43:30 +09:00
var cryptoPaid = accounting . Paid ;
var dueAmount = accounting . TotalDue ;
// If no payment, but settled and marked, assume it has been fully paid
if ( cryptoPaid is 0 & & invoice is { Status : InvoiceStatus . Settled , ExceptionStatus : InvoiceExceptionStatus . Marked } )
{
cryptoPaid = accounting . TotalDue ;
dueAmount = 0 ;
}
2024-05-01 10:22:07 +09:00
var paymentMethodCurrency = paymentMethod . Currency ;
2023-05-11 10:33:33 +02:00
2024-04-04 16:31:04 +09:00
var isPaidOver = invoice . ExceptionStatus = = InvoiceExceptionStatus . PaidOver ;
decimal? overpaidAmount = isPaidOver ? Math . Round ( cryptoPaid - dueAmount , paymentMethod . Divisibility ) : null ;
int ppDivisibility = paymentMethod . Divisibility ;
2020-09-02 11:24:18 +02:00
switch ( model . RefundStep )
2020-06-24 17:51:00 +09:00
{
2020-09-02 11:24:18 +02:00
case RefundSteps . SelectPaymentMethod :
model . RefundStep = RefundSteps . SelectRate ;
2022-06-02 10:08:55 +02:00
model . Title = "How much to refund?" ;
2021-11-15 09:35:03 +01:00
2024-04-04 16:31:04 +09:00
var paidCurrency = Math . Round ( cryptoPaid * paymentMethod . Rate , cdCurrency . Divisibility ) ;
model . CryptoAmountThen = cryptoPaid . RoundToSignificant ( paymentMethod . Divisibility ) ;
model . RateThenText = _displayFormatter . Currency ( model . CryptoAmountThen , paymentMethodCurrency ) ;
2024-05-13 22:29:42 +09:00
rules = store . GetStoreBlob ( ) . GetRateRules ( _defaultRules ) ;
2024-04-04 16:31:04 +09:00
rateResult = await _RateProvider . FetchRate (
2024-04-30 11:31:15 +02:00
new CurrencyPair ( paymentMethodCurrency , invoice . Currency ) , rules , new StoreIdRateContext ( store . Id ) ,
2024-04-04 16:31:04 +09:00
cancellationToken ) ;
//TODO: What if fetching rate failed?
if ( rateResult . BidAsk is null )
2020-09-02 11:24:18 +02:00
{
2024-04-04 16:31:04 +09:00
ModelState . AddModelError ( nameof ( model . SelectedRefundOption ) ,
$"Impossible to fetch rate: {rateResult.EvaluatedRule}" ) ;
return View ( "_RefundModal" , model ) ;
2020-09-02 11:24:18 +02:00
}
2024-04-04 16:31:04 +09:00
model . CryptoAmountNow = Math . Round ( paidCurrency / rateResult . BidAsk . Bid , paymentMethod . Divisibility ) ;
model . CurrentRateText = _displayFormatter . Currency ( model . CryptoAmountNow , paymentMethodCurrency ) ;
model . FiatAmount = paidCurrency ;
model . CryptoCode = paymentMethodCurrency ;
model . CryptoDivisibility = paymentMethod . Divisibility ;
2023-05-11 10:33:33 +02:00
model . InvoiceDivisibility = cdCurrency . Divisibility ;
model . InvoiceCurrency = invoice . Currency ;
2022-06-02 10:08:55 +02:00
model . CustomAmount = model . FiatAmount ;
model . CustomCurrency = invoice . Currency ;
2023-05-11 10:33:33 +02:00
model . SubtractPercentage = 0 ;
model . OverpaidAmount = overpaidAmount ;
2024-04-04 16:31:04 +09:00
model . OverpaidAmountText = overpaidAmount ! = null ? _displayFormatter . Currency ( overpaidAmount . Value , paymentMethodCurrency ) : null ;
2023-03-13 02:12:58 +01:00
model . FiatText = _displayFormatter . Currency ( model . FiatAmount , invoice . Currency ) ;
2022-06-02 10:08:55 +02:00
return View ( "_RefundModal" , model ) ;
2021-12-31 16:59:02 +09:00
2020-09-02 11:24:18 +02:00
case RefundSteps . SelectRate :
2021-12-16 17:37:19 +01:00
createPullPayment = new CreatePullPayment
{
2021-12-31 16:59:02 +09:00
Name = $"Refund {invoice.Id}" ,
2024-05-01 10:22:07 +09:00
PayoutMethodIds = new [ ] { pmi } ,
2022-01-24 20:17:09 +09:00
StoreId = invoice . StoreId ,
2022-06-02 10:08:55 +02:00
BOLT11Expiration = store . GetStoreBlob ( ) . RefundBOLT11Expiration
2021-12-16 17:37:19 +01:00
} ;
2023-03-20 02:46:46 +01:00
var authorizedForAutoApprove = ( await
_authorizationService . AuthorizeAsync ( User , invoice . StoreId , Policies . CanCreatePullPayments ) )
. Succeeded ;
2023-05-11 10:33:33 +02:00
if ( model . SubtractPercentage is < 0 or > 100 )
{
ModelState . AddModelError ( nameof ( model . SubtractPercentage ) , "Percentage must be a numeric value between 0 and 100" ) ;
}
if ( ! ModelState . IsValid )
{
return View ( "_RefundModal" , model ) ;
}
2024-04-04 16:31:04 +09:00
2021-12-16 17:37:19 +01:00
switch ( model . SelectedRefundOption )
2021-12-31 16:59:02 +09:00
{
case "RateThen" :
2024-04-04 16:31:04 +09:00
createPullPayment . Currency = paymentMethodCurrency ;
2021-12-31 16:59:02 +09:00
createPullPayment . Amount = model . CryptoAmountThen ;
2023-03-20 02:46:46 +01:00
createPullPayment . AutoApproveClaims = authorizedForAutoApprove ;
2021-12-31 16:59:02 +09:00
break ;
2023-01-06 14:18:07 +01:00
2021-12-31 16:59:02 +09:00
case "CurrentRate" :
2024-04-04 16:31:04 +09:00
createPullPayment . Currency = paymentMethodCurrency ;
2021-12-31 16:59:02 +09:00
createPullPayment . Amount = model . CryptoAmountNow ;
2023-03-20 02:46:46 +01:00
createPullPayment . AutoApproveClaims = authorizedForAutoApprove ;
2021-12-31 16:59:02 +09:00
break ;
2023-01-06 14:18:07 +01:00
2021-12-31 16:59:02 +09:00
case "Fiat" :
2024-04-04 16:31:04 +09:00
ppDivisibility = cdCurrency . Divisibility ;
2021-12-31 16:59:02 +09:00
createPullPayment . Currency = invoice . Currency ;
createPullPayment . Amount = model . FiatAmount ;
2022-04-28 13:50:28 +02:00
createPullPayment . AutoApproveClaims = false ;
2021-12-31 16:59:02 +09:00
break ;
2024-04-04 16:31:04 +09:00
2023-05-11 10:33:33 +02:00
case "OverpaidAmount" :
model . Title = "How much to refund?" ;
model . RefundStep = RefundSteps . SelectRate ;
2024-04-04 16:31:04 +09:00
2023-06-16 03:52:52 +02:00
if ( ! isPaidOver )
2023-05-11 10:33:33 +02:00
{
ModelState . AddModelError ( nameof ( model . SelectedRefundOption ) , "Invoice is not overpaid" ) ;
}
if ( overpaidAmount = = null )
{
ModelState . AddModelError ( nameof ( model . SelectedRefundOption ) , "Overpaid amount cannot be calculated" ) ;
}
if ( ! ModelState . IsValid )
{
2023-06-16 03:52:52 +02:00
return View ( "_RefundModal" , model ) ;
2023-05-11 10:33:33 +02:00
}
2024-04-04 16:31:04 +09:00
createPullPayment . Currency = paymentMethodCurrency ;
2023-05-11 10:33:33 +02:00
createPullPayment . Amount = overpaidAmount ! . Value ;
createPullPayment . AutoApproveClaims = true ;
break ;
2023-01-06 14:18:07 +01:00
2021-12-31 16:59:02 +09:00
case "Custom" :
model . Title = "How much to refund?" ;
2022-06-02 10:08:55 +02:00
model . RefundStep = RefundSteps . SelectRate ;
2023-01-06 14:18:07 +01:00
2022-06-02 10:08:55 +02:00
if ( model . CustomAmount < = 0 )
{
model . AddModelError ( refundModel = > refundModel . CustomAmount , "Amount must be greater than 0" , this ) ;
}
if ( string . IsNullOrEmpty ( model . CustomCurrency ) | |
_CurrencyNameTable . GetCurrencyData ( model . CustomCurrency , false ) = = null )
{
ModelState . AddModelError ( nameof ( model . CustomCurrency ) , "Invalid currency" ) ;
}
if ( ! ModelState . IsValid )
{
return View ( "_RefundModal" , model ) ;
}
2020-09-02 11:24:18 +02:00
2024-05-13 22:29:42 +09:00
rules = store . GetStoreBlob ( ) . GetRateRules ( _defaultRules ) ;
2022-06-02 10:08:55 +02:00
rateResult = await _RateProvider . FetchRate (
2024-04-30 11:31:15 +02:00
new CurrencyPair ( paymentMethodCurrency , model . CustomCurrency ) , rules , new StoreIdRateContext ( store . Id ) ,
2022-06-02 10:08:55 +02:00
cancellationToken ) ;
2023-01-06 14:18:07 +01:00
2022-06-02 10:08:55 +02:00
//TODO: What if fetching rate failed?
if ( rateResult . BidAsk is null )
{
ModelState . AddModelError ( nameof ( model . SelectedRefundOption ) ,
$"Impossible to fetch rate: {rateResult.EvaluatedRule}" ) ;
return View ( "_RefundModal" , model ) ;
}
2021-12-20 15:15:32 +01:00
2022-06-02 10:08:55 +02:00
createPullPayment . Currency = model . CustomCurrency ;
createPullPayment . Amount = model . CustomAmount ;
2024-04-04 16:31:04 +09:00
createPullPayment . AutoApproveClaims = authorizedForAutoApprove & & paymentMethodCurrency = = model . CustomCurrency ;
2022-06-02 10:08:55 +02:00
break ;
2023-01-06 14:18:07 +01:00
2022-06-02 10:08:55 +02:00
default :
ModelState . AddModelError ( nameof ( model . SelectedRefundOption ) , "Please select an option before proceeding" ) ;
return View ( "_RefundModal" , model ) ;
2020-09-02 11:24:18 +02:00
}
break ;
2023-01-06 14:18:07 +01:00
2020-09-02 11:24:18 +02:00
default :
throw new ArgumentOutOfRangeException ( ) ;
2020-06-24 17:51:00 +09:00
}
2020-09-02 11:24:18 +02:00
2023-05-11 10:33:33 +02:00
// reduce by percentage
if ( model . SubtractPercentage is > 0 and < = 100 )
{
var reduceByAmount = createPullPayment . Amount * ( model . SubtractPercentage / 100 ) ;
2024-04-04 16:31:04 +09:00
createPullPayment . Amount = Math . Round ( createPullPayment . Amount - reduceByAmount , ppDivisibility ) ;
2023-05-11 10:33:33 +02:00
}
2020-09-02 11:24:18 +02:00
var ppId = await _paymentHostedService . CreatePullPayment ( createPullPayment ) ;
2022-06-02 10:08:55 +02:00
TempData . SetStatusMessageModel ( new StatusMessageModel
2020-09-02 11:24:18 +02:00
{
2021-10-29 14:50:18 +02:00
Html = "Refund successfully created!<br />Share the link to this page with a customer.<br />The customer needs to enter their address and claim the refund.<br />Once a customer claims the refund, you will get a notification and would need to approve and initiate it from your Store > Payouts." ,
2020-09-02 11:24:18 +02:00
Severity = StatusMessageModel . StatusSeverity . Success
} ) ;
2023-11-21 14:11:17 +09:00
2022-06-02 10:08:55 +02:00
ctx . Refunds . Add ( new RefundData
2020-09-02 11:24:18 +02:00
{
2021-12-20 15:15:32 +01:00
InvoiceDataId = invoice . Id ,
2020-09-02 11:24:18 +02:00
PullPaymentDataId = ppId
} ) ;
await ctx . SaveChangesAsync ( cancellationToken ) ;
2023-01-06 14:18:07 +01:00
2020-09-02 11:24:18 +02:00
// TODO: Having dedicated UI later on
2022-01-07 12:32:00 +09:00
return RedirectToAction ( nameof ( UIPullPaymentController . ViewPullPayment ) ,
"UIPullPayment" ,
2020-09-02 11:24:18 +02:00
new { pullPaymentId = ppId } ) ;
2020-06-24 17:51:00 +09:00
}
2019-05-07 23:32:47 +09:00
private InvoiceDetailsModel InvoicePopulatePayments ( InvoiceEntity invoice )
2019-04-29 22:45:33 -05:00
{
2022-10-05 20:59:05 -07:00
var overpaid = false ;
2023-10-10 05:28:00 +02:00
var stillDue = false ;
var hasRates = false ;
2022-10-05 20:59:05 -07:00
var model = new InvoiceDetailsModel
2018-01-08 20:06:16 +09:00
{
2020-10-17 08:57:21 +02:00
Archived = invoice . Archived ,
2021-05-14 16:16:19 +09:00
Payments = invoice . GetPayments ( false ) ,
2024-04-04 16:31:04 +09:00
CryptoPayments = invoice . GetPaymentPrompts ( ) . Select (
2020-10-17 08:57:21 +02:00
data = >
{
var accounting = data . Calculate ( ) ;
2024-04-04 16:31:04 +09:00
var paymentMethodId = data . PaymentMethodId ;
var hasPayment = accounting . PaymentMethodPaid > 0 ;
2023-07-19 18:47:32 +09:00
var overpaidAmount = accounting . OverpaidHelper ;
2024-04-04 16:31:04 +09:00
var rate = ExchangeRate ( data . Currency , data ) ;
if ( rate is not null )
hasRates = true ;
if ( hasPayment & & overpaidAmount > 0 )
overpaid = true ;
if ( hasPayment & & accounting . Due > 0 )
stillDue = true ;
2022-10-05 20:59:05 -07:00
2020-10-17 08:57:21 +02:00
return new InvoiceDetailsModel . CryptoPayment
{
2023-10-10 05:28:00 +02:00
Rate = rate ,
PaymentMethodRaw = data ,
2020-10-17 08:57:21 +02:00
PaymentMethodId = paymentMethodId ,
2024-04-04 16:31:04 +09:00
PaymentMethod = paymentMethodId . ToString ( ) ,
TotalDue = _displayFormatter . Currency ( accounting . TotalDue , data . Currency ) ,
Due = hasPayment ? _displayFormatter . Currency ( accounting . Due , data . Currency ) : null ,
Paid = hasPayment ? _displayFormatter . Currency ( accounting . PaymentMethodPaid , data . Currency ) : null ,
Overpaid = hasPayment ? _displayFormatter . Currency ( overpaidAmount , data . Currency ) : null ,
Address = data . Destination
2020-10-17 08:57:21 +02:00
} ;
2023-10-10 05:28:00 +02:00
} ) . ToList ( ) ,
Overpaid = overpaid ,
StillDue = stillDue ,
HasRates = hasRates
2020-10-17 08:57:21 +02:00
} ;
2022-10-05 20:59:05 -07:00
return model ;
2017-10-27 17:53:04 +09:00
}
2017-09-13 15:47:34 +09:00
2020-05-07 12:50:07 +02:00
[HttpPost("invoices/{invoiceId}/archive")]
2022-02-02 20:24:22 +09:00
[Authorize(AuthenticationSchemes = AuthenticationSchemes.Cookie, Policy = Policies.CanViewInvoices)]
2020-05-07 12:50:07 +02:00
[BitpayAPIConstraint(false)]
public async Task < IActionResult > ToggleArchive ( string invoiceId )
{
2021-12-16 17:37:19 +01:00
var invoice = ( await _InvoiceRepository . GetInvoices ( new InvoiceQuery
2020-05-07 12:50:07 +02:00
{
2020-06-28 17:55:27 +09:00
InvoiceId = new [ ] { invoiceId } ,
UserId = GetUserId ( ) ,
2022-06-03 12:08:16 +02:00
IncludeAddresses = false ,
2020-06-28 17:55:27 +09:00
IncludeArchived = true ,
2020-05-07 12:50:07 +02:00
} ) ) . FirstOrDefault ( ) ;
if ( invoice = = null )
return NotFound ( ) ;
await _InvoiceRepository . ToggleInvoiceArchival ( invoiceId , ! invoice . Archived ) ;
TempData . SetStatusMessageModel ( new StatusMessageModel ( )
{
Severity = StatusMessageModel . StatusSeverity . Success ,
Message = invoice . Archived ? "The invoice has been unarchived and will appear in the invoice list by default again." : "The invoice has been archived and will no longer appear in the invoice list by default."
} ) ;
2020-06-28 17:55:27 +09:00
return RedirectToAction ( nameof ( invoice ) , new { invoiceId } ) ;
2020-05-07 12:50:07 +02:00
}
2020-06-28 17:55:27 +09:00
2020-07-14 19:58:52 -07:00
[HttpPost]
2022-02-02 20:24:22 +09:00
[Authorize(AuthenticationSchemes = AuthenticationSchemes.Cookie, Policy = Policies.CanViewInvoices)]
2021-12-11 04:32:23 +01:00
public async Task < IActionResult > MassAction ( string command , string [ ] selectedItems , string? storeId = null )
2020-07-14 19:58:52 -07:00
{
2023-11-02 08:12:28 +01:00
IActionResult NotSupported ( string err )
2020-07-14 19:58:52 -07:00
{
2023-11-02 08:12:28 +01:00
TempData [ WellKnownTempData . ErrorMessage ] = err ;
return RedirectToAction ( nameof ( ListInvoices ) , new { storeId } ) ;
}
if ( selectedItems . Length = = 0 )
return NotSupported ( "No invoice has been selected" ) ;
switch ( command )
{
case "archive" :
await _InvoiceRepository . MassArchive ( selectedItems ) ;
TempData [ WellKnownTempData . SuccessMessage ] = $"{selectedItems.Length} invoice{(selectedItems.Length == 1 ? "" : " s ")} archived." ;
break ;
case "unarchive" :
await _InvoiceRepository . MassArchive ( selectedItems , false ) ;
TempData [ WellKnownTempData . SuccessMessage ] = $"{selectedItems.Length} invoice{(selectedItems.Length == 1 ? "" : " s ")} unarchived." ;
break ;
2024-02-20 18:42:38 +09:00
case "cpfp" when storeId is not null :
2023-11-02 08:12:28 +01:00
var network = _NetworkProvider . DefaultNetwork ;
var explorer = _ExplorerClients . GetExplorerClient ( network ) ;
if ( explorer is null )
return NotSupported ( "This feature is only available to BTC wallets" ) ;
if ( ! GetCurrentStore ( ) . HasPermission ( GetUserId ( ) , Policies . CanModifyStoreSettings ) )
return Forbid ( ) ;
2024-04-04 16:31:04 +09:00
var derivationScheme = ( this . GetCurrentStore ( ) . GetDerivationSchemeSettings ( _handlers , network . CryptoCode ) ) ? . AccountDerivation ;
2023-11-02 08:12:28 +01:00
if ( derivationScheme is null )
return NotSupported ( "This feature is only available to BTC wallets" ) ;
2024-04-04 16:31:04 +09:00
var btc = PaymentTypes . CHAIN . GetPaymentMethodId ( "BTC" ) ;
2023-11-02 08:12:28 +01:00
var bumpableAddresses = ( await GetAddresses ( selectedItems ) )
2024-04-04 16:31:04 +09:00
. Where ( p = > p . GetPaymentMethodId ( ) = = btc )
2023-11-02 08:12:28 +01:00
. Select ( p = > p . GetAddress ( ) ) . ToHashSet ( ) ;
var utxos = await explorer . GetUTXOsAsync ( derivationScheme ) ;
var bumpableUTXOs = utxos . GetUnspentUTXOs ( ) . Where ( u = > u . Confirmations = = 0 & & bumpableAddresses . Contains ( u . ScriptPubKey . Hash . ToString ( ) ) ) . ToArray ( ) ;
var parameters = new MultiValueDictionary < string , string > ( ) ;
foreach ( var utxo in bumpableUTXOs )
{
parameters . Add ( $"outpoints[]" , utxo . Outpoint . ToString ( ) ) ;
}
return View ( "PostRedirect" , new PostRedirectViewModel
{
AspController = "UIWallets" ,
AspAction = nameof ( UIWalletsController . WalletCPFP ) ,
RouteParameters = {
{ "walletId" , new WalletId ( storeId , network . CryptoCode ) . ToString ( ) } ,
{ "returnUrl" , Url . Action ( nameof ( ListInvoices ) , new { storeId } ) }
} ,
FormParameters = parameters ,
} ) ;
2020-07-14 19:58:52 -07:00
}
2021-12-11 04:32:23 +01:00
return RedirectToAction ( nameof ( ListInvoices ) , new { storeId } ) ;
2020-07-14 19:58:52 -07:00
}
2022-02-10 12:24:28 +09:00
private async Task < AddressInvoiceData [ ] > GetAddresses ( string [ ] selectedItems )
{
using var ctx = _dbContextFactory . CreateContext ( ) ;
return await ctx . AddressInvoices . Where ( i = > selectedItems . Contains ( i . InvoiceDataId ) ) . ToArrayAsync ( ) ;
}
2021-11-15 09:35:03 +01:00
[HttpGet("i/{invoiceId}")]
[HttpGet("i/{invoiceId}/{paymentMethodId}")]
[HttpGet("invoice")]
2017-10-27 17:53:04 +09:00
[AcceptMediaTypeConstraint("application/bitcoin-paymentrequest", false)]
2022-11-02 10:21:33 +01:00
[XFrameOptions(null)]
[ReferrerPolicy("origin")]
2021-07-14 04:40:18 -07:00
public async Task < IActionResult > Checkout ( string? invoiceId , string? id = null , string? paymentMethodId = null ,
[FromQuery] string? view = null , [ FromQuery ] string? lang = null )
2017-10-27 17:53:04 +09:00
{
2022-11-02 10:21:33 +01:00
// Keep compatibility with Bitpay
invoiceId ? ? = id ;
2023-01-06 14:18:07 +01:00
2021-07-29 20:29:34 +09:00
if ( invoiceId is null )
return NotFound ( ) ;
2023-01-06 14:18:07 +01:00
2020-12-10 23:34:50 +09:00
var model = await GetInvoiceModel ( invoiceId , paymentMethodId = = null ? null : PaymentMethodId . Parse ( paymentMethodId ) , lang ) ;
2022-07-22 06:21:41 +02:00
if ( model = = null )
2017-10-27 17:53:04 +09:00
return NotFound ( ) ;
2017-10-20 22:24:28 -05:00
2018-11-09 01:09:09 -06:00
if ( view = = "modal" )
2022-07-22 06:21:41 +02:00
model . IsModal = true ;
2024-04-05 17:43:38 +02:00
return View ( model ) ;
2017-10-27 17:53:04 +09:00
}
2021-12-31 16:59:02 +09:00
2021-11-15 09:35:03 +01:00
[HttpGet("invoice-noscript")]
2021-07-14 04:40:18 -07:00
public async Task < IActionResult > CheckoutNoScript ( string? invoiceId , string? id = null , string? paymentMethodId = null , [ FromQuery ] string? lang = null )
2019-03-31 13:46:38 -05:00
{
//Keep compatibility with Bitpay
invoiceId = invoiceId ? ? id ;
//
2021-07-29 20:29:34 +09:00
if ( invoiceId is null )
return NotFound ( ) ;
var model = await GetInvoiceModel ( invoiceId , paymentMethodId is null ? null : PaymentMethodId . Parse ( paymentMethodId ) , lang ) ;
2022-07-22 06:21:41 +02:00
if ( model = = null )
2019-03-31 13:46:38 -05:00
return NotFound ( ) ;
2022-07-22 06:21:41 +02:00
return View ( model ) ;
2019-03-31 13:46:38 -05:00
}
2022-07-22 06:21:41 +02:00
private async Task < PaymentModel ? > GetInvoiceModel ( string invoiceId , PaymentMethodId ? paymentMethodId , string? lang )
2017-10-27 17:53:04 +09:00
{
2018-12-06 17:05:27 +09:00
var invoice = await _InvoiceRepository . GetInvoice ( invoiceId ) ;
2018-01-10 18:33:05 +09:00
if ( invoice = = null )
2022-07-22 06:21:41 +02:00
return null ;
2023-01-06 14:18:07 +01:00
2018-01-09 11:41:07 +09:00
var store = await _StoreRepository . FindStore ( invoice . StoreId ) ;
2022-06-15 04:17:10 +02:00
if ( store = = null )
2022-07-22 06:21:41 +02:00
return null ;
2023-01-06 14:18:07 +01:00
2019-01-31 19:07:38 +09:00
bool isDefaultPaymentId = false ;
2022-11-29 03:19:23 +01:00
var storeBlob = store . GetStoreBlob ( ) ;
2023-04-24 19:26:56 +09:00
2024-04-04 16:31:04 +09:00
var displayedPaymentMethods = invoice . GetPaymentPrompts ( ) . Select ( p = > p . PaymentMethodId ) . ToList ( ) ;
2023-04-24 19:26:56 +09:00
2024-04-04 16:31:04 +09:00
var btcId = PaymentTypes . CHAIN . GetPaymentMethodId ( "BTC" ) ;
var lnurlId = PaymentTypes . LNURL . GetPaymentMethodId ( "BTC" ) ;
var lnId = PaymentTypes . LN . GetPaymentMethodId ( "BTC" ) ;
2023-04-24 19:26:56 +09:00
// Exclude Lightning if OnChainWithLnInvoiceFallback is active and we have both payment methods
2023-04-25 11:20:10 +02:00
if ( storeBlob is { OnChainWithLnInvoiceFallback : true } & &
2023-04-24 19:26:56 +09:00
displayedPaymentMethods . Contains ( btcId ) )
2018-02-19 02:38:03 +09:00
{
2023-04-24 19:26:56 +09:00
displayedPaymentMethods . Remove ( lnId ) ;
displayedPaymentMethods . Remove ( lnurlId ) ;
}
2023-01-06 14:18:07 +01:00
2023-04-24 19:26:56 +09:00
// BOLT11 doesn't really support payment without amount
if ( invoice . IsUnsetTopUp ( ) )
displayedPaymentMethods . Remove ( lnId ) ;
2023-01-06 14:18:07 +01:00
2023-04-24 19:26:56 +09:00
// Exclude lnurl if bolt11 is available
if ( displayedPaymentMethods . Contains ( lnId ) & & displayedPaymentMethods . Contains ( lnurlId ) )
displayedPaymentMethods . Remove ( lnurlId ) ;
2024-04-04 16:31:04 +09:00
2023-04-25 08:51:38 +09:00
if ( paymentMethodId is not null & & ! displayedPaymentMethods . Contains ( paymentMethodId ) )
2023-04-24 19:26:56 +09:00
paymentMethodId = null ;
if ( paymentMethodId is null )
{
2024-04-04 16:31:04 +09:00
PaymentMethodId ? invoicePaymentId = invoice . DefaultPaymentMethod ;
2021-10-18 16:56:47 +09:00
PaymentMethodId ? storePaymentId = store . GetDefaultPaymentId ( ) ;
2022-01-14 17:48:15 +09:00
if ( invoicePaymentId is not null )
2021-10-18 16:56:47 +09:00
{
2023-04-24 19:26:56 +09:00
if ( displayedPaymentMethods . Contains ( invoicePaymentId ) )
2021-10-18 16:56:47 +09:00
paymentMethodId = invoicePaymentId ;
}
2022-01-14 17:48:15 +09:00
if ( paymentMethodId is null & & storePaymentId is not null )
2021-10-18 16:56:47 +09:00
{
2023-04-24 19:26:56 +09:00
if ( displayedPaymentMethods . Contains ( storePaymentId ) )
2021-10-18 16:56:47 +09:00
paymentMethodId = storePaymentId ;
}
2022-01-14 17:48:15 +09:00
if ( paymentMethodId is null & & invoicePaymentId is not null )
2021-10-18 16:56:47 +09:00
{
2023-04-24 19:26:56 +09:00
paymentMethodId = invoicePaymentId . FindNearest ( displayedPaymentMethods ) ;
2021-10-18 16:56:47 +09:00
}
2022-01-14 17:48:15 +09:00
if ( paymentMethodId is null & & storePaymentId is not null )
2021-10-18 16:56:47 +09:00
{
2023-04-24 19:26:56 +09:00
paymentMethodId = storePaymentId . FindNearest ( displayedPaymentMethods ) ;
2021-10-18 16:56:47 +09:00
}
if ( paymentMethodId is null )
{
2024-04-04 16:31:04 +09:00
var defaultBTC = PaymentTypes . CHAIN . GetPaymentMethodId ( _NetworkProvider . DefaultNetwork . CryptoCode ) ;
var defaultLNURLPay = PaymentTypes . LNURL . GetPaymentMethodId ( _NetworkProvider . DefaultNetwork . CryptoCode ) ;
paymentMethodId = displayedPaymentMethods . FirstOrDefault ( e = > e = = defaultBTC ) ? ?
displayedPaymentMethods . FirstOrDefault ( e = > e = = defaultLNURLPay ) ? ?
2023-04-24 19:26:56 +09:00
displayedPaymentMethods . FirstOrDefault ( ) ;
2021-10-18 16:56:47 +09:00
}
2019-01-31 19:07:38 +09:00
isDefaultPaymentId = true ;
2018-01-12 16:30:34 +09:00
}
2021-11-15 13:51:36 +09:00
if ( paymentMethodId is null )
2022-07-22 06:21:41 +02:00
return null ;
2024-04-04 16:31:04 +09:00
if ( ! invoice . Support ( paymentMethodId ) )
2018-01-12 16:30:34 +09:00
{
2019-01-31 19:07:38 +09:00
if ( ! isDefaultPaymentId )
2022-07-22 06:21:41 +02:00
return null ;
2021-04-07 06:08:42 +02:00
var paymentMethodTemp = invoice
2024-04-04 16:31:04 +09:00
. GetPaymentPrompts ( )
. Where ( p = > displayedPaymentMethods . Contains ( p . PaymentMethodId ) )
2023-04-24 19:26:56 +09:00
. FirstOrDefault ( ) ;
2021-11-15 13:51:36 +09:00
if ( paymentMethodTemp is null )
2022-07-22 06:21:41 +02:00
return null ;
2024-04-04 16:31:04 +09:00
paymentMethodId = paymentMethodTemp . PaymentMethodId ;
2018-01-12 16:30:34 +09:00
}
2024-04-04 16:31:04 +09:00
if ( ! _handlers . TryGetValue ( paymentMethodId , out var handler ) )
return null ;
2023-04-24 19:26:56 +09:00
// We activate the default payment method, and also those which aren't displayed (as they can't be set as default)
bool activated = false ;
2024-04-04 16:31:04 +09:00
PaymentPrompt ? prompt = null ;
foreach ( var pm in invoice . GetPaymentPrompts ( ) )
2021-04-07 06:08:42 +02:00
{
2024-04-04 16:31:04 +09:00
var pmi = pm . PaymentMethodId ;
if ( pmi = = paymentMethodId )
prompt = pm ;
2023-04-24 19:26:56 +09:00
if ( pmi ! = paymentMethodId | | ! displayedPaymentMethods . Contains ( pmi ) )
continue ;
2024-04-04 16:31:04 +09:00
if ( ! pm . Activated )
2021-09-24 00:00:55 +09:00
{
2024-04-04 16:31:04 +09:00
if ( await _invoiceActivator . ActivateInvoicePaymentMethod ( invoice . Id , pmi ) )
2023-04-24 19:26:56 +09:00
{
activated = true ;
}
2021-09-24 00:00:55 +09:00
}
2021-04-07 06:08:42 +02:00
}
2024-04-04 16:31:04 +09:00
if ( prompt is null )
return null ;
2023-04-24 19:26:56 +09:00
if ( activated )
return await GetInvoiceModel ( invoiceId , paymentMethodId , lang ) ;
2023-01-06 14:18:07 +01:00
2024-04-04 16:31:04 +09:00
var accounting = prompt . Calculate ( ) ;
2021-07-27 08:17:56 +02:00
switch ( lang ? . ToLowerInvariant ( ) )
{
case "auto" :
case null when storeBlob . AutoDetectLanguage :
2023-04-17 10:53:45 +09:00
lang = _languageService . AutoDetectLanguageUsingHeader ( HttpContext . Request . Headers , null ) ? . Code ;
2021-07-27 08:17:56 +02:00
break ;
case { } langs when ! string . IsNullOrEmpty ( langs ) :
2021-12-31 16:59:02 +09:00
{
lang = _languageService . FindLanguage ( langs ) ? . Code ;
break ;
}
2021-07-27 08:17:56 +02:00
}
lang ? ? = storeBlob . DefaultLang ;
2021-08-03 17:03:00 +09:00
2022-07-22 06:21:41 +02:00
var receiptEnabled = InvoiceDataBase . ReceiptOptions . Merge ( storeBlob . ReceiptOptions , invoice . ReceiptOptions ) . Enabled is true ;
2023-01-06 14:18:07 +01:00
var receiptUrl = receiptEnabled ? _linkGenerator . GetUriByAction (
2022-11-02 10:21:33 +01:00
nameof ( InvoiceReceipt ) ,
2022-07-22 06:21:41 +02:00
"UIInvoice" ,
2023-01-06 14:18:07 +01:00
new { invoiceId } ,
2022-07-22 06:21:41 +02:00
Request . Scheme ,
Request . Host ,
Request . PathBase ) : null ;
2022-11-02 10:21:33 +01:00
2023-04-25 11:20:10 +02:00
var isAltcoinsBuild = false ;
2022-11-02 10:21:33 +01:00
#if ALTCOINS
2023-05-11 10:38:40 +02:00
isAltcoinsBuild = true ;
2022-11-02 10:21:33 +01:00
#endif
2023-04-25 11:20:10 +02:00
2023-05-11 10:38:40 +02:00
var orderId = invoice . Metadata . OrderId ;
var supportUrl = ! string . IsNullOrEmpty ( storeBlob . StoreSupportUrl )
? storeBlob . StoreSupportUrl
. Replace ( "{OrderId}" , string . IsNullOrEmpty ( orderId ) ? string . Empty : Uri . EscapeDataString ( orderId ) )
. Replace ( "{InvoiceId}" , Uri . EscapeDataString ( invoice . Id ) )
: null ;
2024-04-04 16:31:04 +09:00
string GetPaymentMethodName ( PaymentMethodId paymentMethodId )
{
_paymentModelExtensions . TryGetValue ( paymentMethodId , out var extension ) ;
return extension ? . DisplayName ? ? paymentMethodId . ToString ( ) ;
}
string GetPaymentMethodImage ( PaymentMethodId paymentMethodId )
{
_paymentModelExtensions . TryGetValue ( paymentMethodId , out var extension ) ;
return extension ? . Image ? ? "" ;
}
2023-04-25 11:20:10 +02:00
var model = new PaymentModel
{
2024-04-04 16:31:04 +09:00
Activated = prompt . Activated ,
PaymentMethodName = GetPaymentMethodName ( paymentMethodId ) ,
CryptoCode = prompt . Currency ,
2021-11-15 09:35:03 +01:00
RootPath = Request . PathBase . Value . WithTrailingSlash ( ) ,
2023-05-11 10:38:40 +02:00
OrderId = orderId ,
InvoiceId = invoiceId ,
2020-12-10 23:34:50 +09:00
DefaultLang = lang ? ? invoice . DefaultLanguage ? ? storeBlob . DefaultLang ? ? "en" ,
2023-04-04 10:45:40 +09:00
ShowPayInWalletButton = storeBlob . ShowPayInWalletButton ,
ShowStoreHeader = storeBlob . ShowStoreHeader ,
2024-05-09 02:18:02 +02:00
StoreBranding = await StoreBrandingViewModel . CreateAsync ( Request , _uriResolver , storeBlob ) ,
2020-03-26 18:26:06 -05:00
HtmlTitle = storeBlob . HtmlTitle ? ? "BTCPay Invoice" ,
2023-03-13 02:09:56 +01:00
CelebratePayment = storeBlob . CelebratePayment ,
2023-02-22 07:53:14 +01:00
OnChainWithLnInvoiceFallback = storeBlob . OnChainWithLnInvoiceFallback ,
2024-04-04 16:31:04 +09:00
CryptoImage = Request . GetRelativePathOrAbsolute ( GetPaymentMethodImage ( paymentMethodId ) ) ,
BtcAddress = prompt . Destination ,
2023-07-19 18:47:32 +09:00
BtcDue = accounting . ShowMoney ( accounting . Due ) ,
BtcPaid = accounting . ShowMoney ( accounting . Paid ) ,
2020-11-06 11:09:17 +01:00
InvoiceCurrency = invoice . Currency ,
2024-04-04 16:31:04 +09:00
OrderAmount = accounting . ShowMoney ( accounting . TotalDue - accounting . PaymentMethodFee ) ,
2021-08-03 17:03:00 +09:00
IsUnsetTopUp = invoice . IsUnsetTopUp ( ) ,
2024-04-04 16:31:04 +09:00
CustomerEmail = invoice . Metadata . BuyerEmail ,
2017-10-27 17:53:04 +09:00
ExpirationSeconds = Math . Max ( 0 , ( int ) ( invoice . ExpirationTime - DateTimeOffset . UtcNow ) . TotalSeconds ) ,
2023-01-16 12:45:19 +01:00
DisplayExpirationTimer = ( int ) storeBlob . DisplayExpirationTimer . TotalSeconds ,
2017-10-27 17:53:04 +09:00
MaxTimeSeconds = ( int ) ( invoice . ExpirationTime - invoice . InvoiceTime ) . TotalSeconds ,
2018-01-20 00:33:37 +09:00
MaxTimeMinutes = ( int ) ( invoice . ExpirationTime - invoice . InvoiceTime ) . TotalMinutes ,
2020-08-25 14:33:00 +09:00
ItemDesc = invoice . Metadata . ItemDesc ,
2024-04-04 16:31:04 +09:00
Rate = ExchangeRate ( prompt . Currency , prompt , DisplayFormatter . CurrencyFormat . Symbol ) ,
2023-01-06 14:18:07 +01:00
MerchantRefLink = invoice . RedirectURL ? . AbsoluteUri ? ? receiptUrl ? ? "/" ,
2022-07-06 14:14:55 +02:00
ReceiptLink = receiptUrl ,
2019-04-11 11:08:42 +02:00
RedirectAutomatically = invoice . RedirectAutomatically ,
2017-10-27 17:53:04 +09:00
StoreName = store . StoreName ,
2023-05-11 10:38:40 +02:00
StoreSupportUrl = supportUrl ,
2018-02-26 00:48:12 +09:00
TxCount = accounting . TxRequired ,
2021-07-06 00:43:49 +02:00
TxCountForFee = storeBlob . NetworkFeeMode switch
{
NetworkFeeMode . Always = > accounting . TxRequired ,
NetworkFeeMode . MultiplePaymentsOnly = > accounting . TxRequired - 1 ,
NetworkFeeMode . Never = > 0 ,
_ = > throw new NotImplementedException ( )
} ,
2023-03-27 12:12:11 +02:00
RequiredConfirmations = invoice . SpeedPolicy switch
{
SpeedPolicy . HighSpeed = > 0 ,
SpeedPolicy . MediumSpeed = > 1 ,
SpeedPolicy . LowMediumSpeed = > 2 ,
SpeedPolicy . LowSpeed = > 6 ,
_ = > null
} ,
2024-04-04 16:31:04 +09:00
ReceivedConfirmations = handler is BitcoinLikePaymentHandler bh ? invoice . GetAllBitcoinPaymentData ( bh , false ) . FirstOrDefault ( ) ? . ConfirmationCount : null ,
2024-05-15 07:49:53 +09:00
Status = invoice . Status . ToString ( ) ,
2024-04-04 16:31:04 +09:00
NetworkFee = prompt . PaymentMethodFee ,
IsMultiCurrency = invoice . GetPayments ( false ) . Select ( p = > p . PaymentMethodId ) . Concat ( new [ ] { prompt . PaymentMethodId } ) . Distinct ( ) . Count ( ) > 1 ,
2018-10-24 07:52:19 +02:00
StoreId = store . Id ,
2024-04-04 16:31:04 +09:00
AvailableCryptos = invoice . GetPaymentPrompts ( )
2019-05-29 14:33:31 +00:00
. Select ( kv = >
2018-03-19 09:45:54 +09:00
{
2024-04-04 16:31:04 +09:00
var handler = _handlers [ kv . PaymentMethodId ] ;
var pmName = GetPaymentMethodName ( kv . PaymentMethodId ) ;
2022-11-02 10:21:33 +01:00
return new PaymentModel . AvailableCrypto
2019-05-29 14:33:31 +00:00
{
2024-04-04 16:31:04 +09:00
Displayed = displayedPaymentMethods . Contains ( kv . PaymentMethodId ) ,
PaymentMethodId = kv . PaymentMethodId . ToString ( ) ,
CryptoCode = kv . Currency ,
2023-04-25 11:20:10 +02:00
PaymentMethodName = isAltcoinsBuild
? pmName
: pmName . Replace ( "Bitcoin (" , "" ) . Replace ( ")" , "" ) . Replace ( "Lightning " , "" ) ,
2024-04-04 16:31:04 +09:00
IsLightning = handler is ILightningPaymentHandler ,
CryptoImage = Request . GetRelativePathOrAbsolute ( GetPaymentMethodImage ( kv . PaymentMethodId ) ) ,
2019-05-29 14:33:31 +00:00
Link = Url . Action ( nameof ( Checkout ) ,
new
{
2021-11-15 09:35:03 +01:00
invoiceId ,
2024-04-04 16:31:04 +09:00
paymentMethodId = kv . PaymentMethodId . ToString ( )
2019-05-29 14:33:31 +00:00
} )
} ;
2018-03-19 09:45:54 +09:00
} ) . Where ( c = > c . CryptoImage ! = "/" )
2022-04-20 10:20:39 +09:00
. OrderByDescending ( a = > a . CryptoCode = = _NetworkProvider . DefaultNetwork . CryptoCode ) . ThenBy ( a = > a . PaymentMethodName ) . ThenBy ( a = > a . IsLightning ? 1 : 0 )
2018-07-13 22:35:34 -05:00
. ToList ( )
2017-10-27 17:53:04 +09:00
} ;
2024-04-04 16:31:04 +09:00
if ( _paymentModelExtensions . TryGetValue ( paymentMethodId , out var extension ) )
extension . ModifyPaymentModel ( new PaymentModelContext ( model , store , storeBlob , invoice , Url , prompt , handler ) ) ;
model . UISettings = _viewProvider . TryGetViewViewModel ( prompt , "CheckoutUI" ) ? . View as CheckoutUIPaymentMethodSettings ;
2019-05-29 14:33:31 +00:00
model . PaymentMethodId = paymentMethodId . ToString ( ) ;
2023-03-13 02:12:58 +01:00
model . OrderAmountFiat = OrderAmountFromInvoice ( model . CryptoCode , invoice , DisplayFormatter . CurrencyFormat . Symbol ) ;
2023-07-24 15:57:24 +02:00
2024-04-04 16:31:04 +09:00
foreach ( var paymentPrompt in invoice . GetPaymentPrompts ( ) )
{
var vvm = _viewProvider . TryGetViewViewModel ( paymentPrompt , "CheckoutUI" ) ;
if ( vvm ? . View is CheckoutUIPaymentMethodSettings { ExtensionPartial : { } partial } )
{
model . ExtensionPartials . Add ( partial ) ;
}
}
2023-07-24 15:57:24 +02:00
if ( storeBlob . PlaySoundOnPayment )
{
2024-05-09 02:18:02 +02:00
model . PaymentSoundUrl = storeBlob . PaymentSoundUrl is null
2024-04-05 17:43:38 +02:00
? string . Concat ( Request . GetAbsoluteRootUri ( ) . ToString ( ) , "checkout/payment.mp3" )
2024-05-09 02:18:02 +02:00
: await _uriResolver . Resolve ( Request . GetAbsoluteRootUri ( ) , storeBlob . PaymentSoundUrl ) ;
2024-04-05 17:43:38 +02:00
model . ErrorSoundUrl = string . Concat ( Request . GetAbsoluteRootUri ( ) . ToString ( ) , "checkout/error.mp3" ) ;
model . NfcReadSoundUrl = string . Concat ( Request . GetAbsoluteRootUri ( ) . ToString ( ) , "checkout/nfcread.mp3" ) ;
2023-07-24 15:57:24 +02:00
}
2024-04-04 16:31:04 +09:00
2017-10-27 17:53:04 +09:00
var expiration = TimeSpan . FromSeconds ( model . ExpirationSeconds ) ;
2018-04-18 18:23:39 +09:00
model . TimeLeft = expiration . PrettyPrint ( ) ;
2022-07-22 06:21:41 +02:00
return model ;
2017-10-27 17:53:04 +09:00
}
2017-09-13 15:47:34 +09:00
2023-03-13 02:12:58 +01:00
private string? OrderAmountFromInvoice ( string cryptoCode , InvoiceEntity invoiceEntity , DisplayFormatter . CurrencyFormat format = DisplayFormatter . CurrencyFormat . Code )
2018-05-16 05:46:11 -05:00
{
2023-03-13 02:12:58 +01:00
var currency = invoiceEntity . Currency ;
var crypto = cryptoCode . ToUpperInvariant ( ) ; // uppercase to make comparison easier, might be "sats"
2023-04-10 11:07:03 +09:00
2018-05-16 05:46:11 -05:00
// if invoice source currency is the same as currently display currency, no need for "order amount from invoice"
2023-03-13 02:12:58 +01:00
if ( crypto = = currency | | ( crypto = = "SATS" & & currency = = "BTC" ) | | ( crypto = = "BTC" & & currency = = "SATS" ) )
2018-05-16 05:46:11 -05:00
return null ;
2023-03-13 02:12:58 +01:00
return _displayFormatter . Currency ( invoiceEntity . Price , currency , format ) ;
2018-05-16 05:46:11 -05:00
}
2023-04-10 11:07:03 +09:00
2024-04-04 16:31:04 +09:00
private string? ExchangeRate ( string cryptoCode , PaymentPrompt paymentMethod , DisplayFormatter . CurrencyFormat format = DisplayFormatter . CurrencyFormat . Code )
2018-01-08 20:06:16 +09:00
{
2023-03-13 02:12:58 +01:00
var currency = paymentMethod . ParentEntity . Currency ;
var crypto = cryptoCode . ToUpperInvariant ( ) ; // uppercase to make comparison easier, might be "sats"
2023-04-10 11:07:03 +09:00
2023-03-13 02:12:58 +01:00
if ( crypto = = currency | | ( crypto = = "SATS" & & currency = = "BTC" ) | | ( crypto = = "BTC" & & currency = = "SATS" ) )
return null ;
2023-04-10 11:07:03 +09:00
2023-03-13 02:12:58 +01:00
return _displayFormatter . Currency ( paymentMethod . Rate , currency , format ) ;
2018-05-09 22:39:13 -05:00
}
2018-01-08 20:06:16 +09:00
2021-11-15 09:35:03 +01:00
[HttpGet("i/{invoiceId}/status")]
[HttpGet("i/{invoiceId}/{implicitPaymentMethodId}/status")]
[HttpGet("invoice/{invoiceId}/status")]
[HttpGet("invoice/{invoiceId}/{implicitPaymentMethodId}/status")]
[HttpGet("invoice/status")]
2021-10-30 13:57:24 +09:00
public async Task < IActionResult > GetStatus ( string invoiceId , string? paymentMethodId = null , string? implicitPaymentMethodId = null , [ FromQuery ] string? lang = null )
2017-10-27 17:53:04 +09:00
{
2021-10-30 13:57:24 +09:00
if ( string . IsNullOrEmpty ( paymentMethodId ) )
paymentMethodId = implicitPaymentMethodId ;
2020-12-10 23:34:50 +09:00
var model = await GetInvoiceModel ( invoiceId , paymentMethodId = = null ? null : PaymentMethodId . Parse ( paymentMethodId ) , lang ) ;
2022-07-22 06:21:41 +02:00
if ( model = = null )
2017-10-27 17:53:04 +09:00
return NotFound ( ) ;
return Json ( model ) ;
}
2017-09-13 15:47:34 +09:00
2024-05-15 09:17:45 +09:00
[Route("i/{invoiceId}/status/ws")]
[Route("i/{invoiceId}/{paymentMethodId}/status/ws")]
[Route("invoice/{invoiceId}/status/ws")]
[Route("invoice/{invoiceId}/{paymentMethodId}/status")]
[Route("invoice/status/ws")]
2021-10-05 13:37:30 +09:00
public async Task < IActionResult > GetStatusWebSocket ( string invoiceId , CancellationToken cancellationToken )
2017-12-17 19:58:55 +09:00
{
if ( ! HttpContext . WebSockets . IsWebSocketRequest )
return NotFound ( ) ;
2018-12-06 17:05:27 +09:00
var invoice = await _InvoiceRepository . GetInvoice ( invoiceId ) ;
2024-05-15 07:49:53 +09:00
if ( invoice = = null | | invoice . Status = = InvoiceStatus . Settled | | invoice . Status = = InvoiceStatus . Invalid | | invoice . Status = = InvoiceStatus . Expired )
2017-12-17 19:58:55 +09:00
return NotFound ( ) ;
var webSocket = await HttpContext . WebSockets . AcceptWebSocketAsync ( ) ;
CompositeDisposable leases = new CompositeDisposable ( ) ;
try
{
2021-10-06 13:22:55 +09:00
leases . Add ( _EventAggregator . SubscribeAsync < Events . InvoiceDataChangedEvent > ( async o = > await NotifySocket ( webSocket , o . InvoiceId , invoiceId ) ) ) ;
leases . Add ( _EventAggregator . SubscribeAsync < Events . InvoiceNewPaymentDetailsEvent > ( async o = > await NotifySocket ( webSocket , o . InvoiceId , invoiceId ) ) ) ;
leases . Add ( _EventAggregator . SubscribeAsync < Events . InvoiceEvent > ( async o = > await NotifySocket ( webSocket , o . Invoice . Id , invoiceId ) ) ) ;
2017-12-17 19:58:55 +09:00
while ( true )
{
2021-11-15 09:35:03 +01:00
var message = await webSocket . ReceiveAndPingAsync ( DummyBuffer ) ;
2017-12-17 19:58:55 +09:00
if ( message . MessageType = = WebSocketMessageType . Close )
break ;
}
}
2020-06-28 17:55:27 +09:00
catch ( WebSocketException ) { }
2017-12-17 19:58:55 +09:00
finally
{
leases . Dispose ( ) ;
2018-02-13 03:27:36 +09:00
await webSocket . CloseSocket ( ) ;
2017-12-17 19:58:55 +09:00
}
2017-12-25 21:52:27 +09:00
return new EmptyResult ( ) ;
2017-12-17 19:58:55 +09:00
}
2018-01-08 20:06:16 +09:00
2020-06-28 22:07:48 -05:00
readonly ArraySegment < Byte > DummyBuffer = new ArraySegment < Byte > ( new Byte [ 1 ] ) ;
2021-10-23 14:47:15 +09:00
public string? CreatedInvoiceId ;
2021-10-20 23:17:40 +09:00
2017-12-17 19:58:55 +09:00
private async Task NotifySocket ( WebSocket webSocket , string invoiceId , string expectedId )
{
if ( invoiceId ! = expectedId | | webSocket . State ! = WebSocketState . Open )
return ;
2020-01-12 15:32:26 +09:00
using CancellationTokenSource cts = new CancellationTokenSource ( ) ;
2017-12-17 19:58:55 +09:00
cts . CancelAfter ( 5000 ) ;
try
{
await webSocket . SendAsync ( DummyBuffer , WebSocketMessageType . Binary , true , cts . Token ) ;
}
2018-01-12 18:32:46 +09:00
catch { try { webSocket . Dispose ( ) ; } catch { } }
2017-12-17 19:58:55 +09:00
}
2021-11-15 09:35:03 +01:00
[HttpPost("i/{invoiceId}/UpdateCustomer")]
[HttpPost("invoice/UpdateCustomer")]
2020-06-28 17:55:27 +09:00
public async Task < IActionResult > UpdateCustomer ( string invoiceId , [ FromBody ] UpdateCustomerModel data )
2017-10-27 17:53:04 +09:00
{
if ( ! ModelState . IsValid )
{
return BadRequest ( ModelState ) ;
}
await _InvoiceRepository . UpdateInvoice ( invoiceId , data ) . ConfigureAwait ( false ) ;
2019-03-31 11:48:53 -05:00
return Ok ( "{}" ) ;
2017-10-27 17:53:04 +09:00
}
2017-09-13 15:47:34 +09:00
2021-12-11 04:32:23 +01:00
[HttpGet("/stores/{storeId}/invoices")]
2021-11-15 09:35:03 +01:00
[HttpGet("invoices")]
2022-01-13 23:50:33 +09:00
[Authorize(AuthenticationSchemes = AuthenticationSchemes.Cookie, Policy = Policies.CanViewInvoices)]
2017-10-27 17:53:04 +09:00
[BitpayAPIConstraint(false)]
2021-12-31 08:36:38 +01:00
public async Task < IActionResult > ListInvoices ( InvoicesModel ? model = null )
2017-10-27 17:53:04 +09:00
{
2020-07-30 09:10:10 -05:00
model = this . ParseListQuery ( model ? ? new InvoicesModel ( ) ) ;
2023-05-19 03:42:09 +02:00
var timezoneOffset = model . TimezoneOffset ? ? 0 ;
var searchTerm = string . IsNullOrEmpty ( model . SearchText ) ? model . SearchTerm : $"{model.SearchText},{model.SearchTerm}" ;
var fs = new SearchString ( searchTerm , timezoneOffset ) ;
2022-03-01 15:15:12 +01:00
string? storeId = model . StoreId ;
2022-02-21 14:53:48 +09:00
var storeIds = new HashSet < string > ( ) ;
if ( storeId is not null )
{
storeIds . Add ( storeId ) ;
}
2023-05-19 03:42:09 +02:00
if ( fs . GetFilterArray ( "storeid" ) is { } l )
{
foreach ( var i in l )
storeIds . Add ( i ) ;
}
model . Search = fs ;
2024-05-23 13:22:16 +02:00
model . SearchText = fs . TextCombined ;
2023-07-20 09:03:39 +02:00
var apps = await _appService . GetAllApps ( GetUserId ( ) , false , storeId ) ;
InvoiceQuery invoiceQuery = GetInvoiceQuery ( fs , apps , timezoneOffset ) ;
2023-05-19 03:42:09 +02:00
invoiceQuery . StoreId = storeIds . ToArray ( ) ;
2020-12-28 11:10:53 +01:00
invoiceQuery . Take = model . Count ;
2020-07-30 09:10:10 -05:00
invoiceQuery . Skip = model . Skip ;
2022-07-01 06:26:00 +02:00
invoiceQuery . IncludeRefunds = true ;
2024-04-04 16:31:04 +09:00
2019-01-16 21:33:04 +01:00
var list = await _InvoiceRepository . GetInvoices ( invoiceQuery ) ;
2019-04-25 18:13:17 -05:00
2023-05-19 03:42:09 +02:00
// Apps
model . Apps = apps . Select ( a = > new InvoiceAppModel
{
Id = a . Id ,
AppName = a . AppName ,
2023-07-20 09:03:39 +02:00
AppType = a . AppType
2023-05-19 03:42:09 +02:00
} ) . ToList ( ) ;
2022-01-11 00:14:34 -08:00
2018-10-11 20:09:13 -05:00
foreach ( var invoice in list )
2017-10-27 17:53:04 +09:00
{
2018-12-10 15:34:48 +09:00
var state = invoice . GetInvoiceState ( ) ;
2023-05-19 03:42:09 +02:00
model . Invoices . Add ( new InvoiceModel
2017-10-27 17:53:04 +09:00
{
2020-10-08 08:42:45 +02:00
Status = state ,
2024-05-15 07:49:53 +09:00
ShowCheckout = invoice . Status = = InvoiceStatus . New ,
2018-05-26 09:32:20 -05:00
Date = invoice . InvoiceTime ,
2017-10-27 17:53:04 +09:00
InvoiceId = invoice . Id ,
2020-08-25 14:33:00 +09:00
OrderId = invoice . Metadata . OrderId ? ? string . Empty ,
2019-09-04 18:20:36 +09:00
RedirectUrl = invoice . RedirectURL ? . AbsoluteUri ? ? string . Empty ,
2023-03-13 02:12:58 +01:00
Amount = invoice . Price ,
Currency = invoice . Currency ,
2018-12-10 15:34:48 +09:00
CanMarkInvalid = state . CanMarkInvalid ( ) ,
2021-11-26 16:13:41 +02:00
CanMarkSettled = state . CanMarkComplete ( ) ,
2020-05-07 12:50:07 +02:00
Details = InvoicePopulatePayments ( invoice ) ,
2023-10-11 16:12:45 +02:00
HasRefund = invoice . Refunds . Any ( )
2017-10-27 17:53:04 +09:00
} ) ;
}
return View ( model ) ;
}
2017-09-13 15:47:34 +09:00
2023-07-20 09:03:39 +02:00
private InvoiceQuery GetInvoiceQuery ( SearchString fs , ListAppsViewModel . ListAppViewModel [ ] apps , int timezoneOffset = 0 )
2018-10-11 20:09:13 -05:00
{
2023-07-20 09:03:39 +02:00
var textSearch = fs . TextSearch ;
if ( fs . GetFilterArray ( "appid" ) is { } appIds )
{
var appsById = apps . ToDictionary ( a = > a . Id ) ;
var searchTexts = appIds . Select ( a = > appsById . TryGet ( a ) ) . Where ( a = > a ! = null )
. Select ( a = > AppService . GetAppSearchTerm ( a . AppType , a . Id ) )
. ToList ( ) ;
searchTexts . Add ( fs . TextSearch ) ;
textSearch = string . Join ( ' ' , searchTexts . Where ( t = > ! string . IsNullOrEmpty ( t ) ) . ToList ( ) ) ;
}
2023-05-19 03:42:09 +02:00
return new InvoiceQuery
2018-10-11 20:09:13 -05:00
{
2023-07-20 09:03:39 +02:00
TextSearch = textSearch ,
2018-10-11 20:09:13 -05:00
UserId = GetUserId ( ) ,
2019-04-25 18:13:17 -05:00
Unusual = fs . GetFilterBool ( "unusual" ) ,
2020-05-07 12:50:07 +02:00
IncludeArchived = fs . GetFilterBool ( "includearchived" ) ? ? false ,
2019-04-25 18:13:17 -05:00
Status = fs . GetFilterArray ( "status" ) ,
ExceptionStatus = fs . GetFilterArray ( "exceptionstatus" ) ,
StoreId = fs . GetFilterArray ( "storeid" ) ,
ItemCode = fs . GetFilterArray ( "itemcode" ) ,
OrderId = fs . GetFilterArray ( "orderid" ) ,
StartDate = fs . GetFilterDate ( "startdate" , timezoneOffset ) ,
EndDate = fs . GetFilterDate ( "enddate" , timezoneOffset )
2019-01-16 21:33:04 +01:00
} ;
2018-10-11 20:09:13 -05:00
}
2021-12-11 04:32:23 +01:00
[HttpGet("/stores/{storeId}/invoices/create")]
2021-11-15 09:35:03 +01:00
[HttpGet("invoices/create")]
2024-03-14 10:25:40 +01:00
[Authorize(Policy = Policies.CanCreateInvoice, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
2017-10-27 17:53:04 +09:00
[BitpayAPIConstraint(false)]
2021-07-14 04:40:18 -07:00
public async Task < IActionResult > CreateInvoice ( InvoicesModel ? model = null )
2017-10-27 17:53:04 +09:00
{
2024-01-11 16:25:56 +01:00
if ( string . IsNullOrEmpty ( model ? . StoreId ) )
2022-04-11 10:50:30 +02:00
{
TempData [ WellKnownTempData . ErrorMessage ] = "You need to select a store before creating an invoice." ;
return RedirectToAction ( nameof ( UIHomeController . Index ) , "UIHome" ) ;
}
2019-05-02 14:29:51 +02:00
2024-03-14 10:25:40 +01:00
var store = await _StoreRepository . FindStore ( model . StoreId ) ;
2024-01-11 16:25:56 +01:00
if ( store = = null )
return NotFound ( ) ;
2024-04-04 16:31:04 +09:00
if ( ! store . AnyPaymentMethodAvailable ( ) )
2024-01-11 16:25:56 +01:00
{
return NoPaymentMethodResult ( store . Id ) ;
}
2024-04-04 16:31:04 +09:00
2024-01-11 16:25:56 +01:00
var storeBlob = store . GetStoreBlob ( ) ;
2021-11-15 09:35:03 +01:00
var vm = new CreateInvoiceModel
{
2022-04-11 10:50:30 +02:00
StoreId = model . StoreId ,
2024-01-11 16:25:56 +01:00
Currency = storeBlob . DefaultCurrency ,
AvailablePaymentMethods = GetPaymentMethodsSelectList ( store )
2021-11-15 09:35:03 +01:00
} ;
return View ( vm ) ;
2017-10-27 17:53:04 +09:00
}
2017-09-13 15:47:34 +09:00
2021-12-11 04:32:23 +01:00
[HttpPost("/stores/{storeId}/invoices/create")]
2021-11-15 09:35:03 +01:00
[HttpPost("invoices/create")]
2020-03-20 13:41:47 +09:00
[Authorize(Policy = Policies.CanCreateInvoice, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
2017-10-27 17:53:04 +09:00
[BitpayAPIConstraint(false)]
2019-03-05 17:09:17 +09:00
public async Task < IActionResult > CreateInvoice ( CreateInvoiceModel model , CancellationToken cancellationToken )
2017-10-27 17:53:04 +09:00
{
2019-10-12 20:35:30 +09:00
var store = HttpContext . GetStoreData ( ) ;
2024-04-04 16:31:04 +09:00
if ( ! store . AnyPaymentMethodAvailable ( ) )
2024-01-11 16:25:56 +01:00
{
return NoPaymentMethodResult ( store . Id ) ;
}
2024-04-04 16:31:04 +09:00
2022-11-02 10:21:33 +01:00
var storeBlob = store . GetStoreBlob ( ) ;
2024-01-11 16:25:56 +01:00
model . AvailablePaymentMethods = GetPaymentMethodsSelectList ( store ) ;
2023-01-06 14:18:07 +01:00
2023-11-02 08:13:48 +01:00
JObject ? metadataObj = null ;
if ( ! string . IsNullOrEmpty ( model . Metadata ) )
{
try
{
metadataObj = JObject . Parse ( model . Metadata ) ;
}
2023-11-28 10:26:35 +01:00
catch ( Exception )
2023-11-02 08:13:48 +01:00
{
ModelState . AddModelError ( nameof ( model . Metadata ) , "Metadata was not valid JSON" ) ;
}
}
2024-04-04 16:31:04 +09:00
2017-10-27 17:53:04 +09:00
if ( ! ModelState . IsValid )
{
return View ( model ) ;
}
2017-12-03 22:36:04 +09:00
try
2017-10-27 17:53:04 +09:00
{
2023-11-02 08:13:48 +01:00
var metadata = metadataObj is null ? new InvoiceMetadata ( ) : InvoiceMetadata . FromJObject ( metadataObj ) ;
if ( ! string . IsNullOrEmpty ( model . OrderId ) )
{
metadata . OrderId = model . OrderId ;
}
if ( ! string . IsNullOrEmpty ( model . ItemDesc ) )
{
metadata . ItemDesc = model . ItemDesc ;
}
if ( ! string . IsNullOrEmpty ( model . BuyerEmail ) )
{
metadata . BuyerEmail = model . BuyerEmail ;
}
2023-07-21 09:08:32 +09:00
var result = await CreateInvoiceCoreRaw ( new CreateInvoiceRequest ( )
2017-12-03 22:36:04 +09:00
{
2023-07-21 09:08:32 +09:00
Amount = model . Amount ,
2017-12-03 22:36:04 +09:00
Currency = model . Currency ,
2023-11-02 08:13:48 +01:00
Metadata = metadata . ToJObject ( ) ,
2024-04-04 16:31:04 +09:00
Checkout = new ( )
2023-07-21 09:08:32 +09:00
{
RedirectURL = store . StoreWebsite ,
DefaultPaymentMethod = model . DefaultPaymentMethod ,
PaymentMethods = model . SupportedTransactionCurrencies ? . ToArray ( )
} ,
} , store , HttpContext . Request . GetAbsoluteRoot ( ) ,
entityManipulator : ( entity ) = >
{
entity . NotificationURLTemplate = model . NotificationUrl ;
entity . FullNotifications = true ;
entity . NotificationEmail = model . NotificationEmail ;
entity . ExtendedNotifications = model . NotificationEmail ! = null ;
} ,
cancellationToken : cancellationToken ) ;
TempData [ WellKnownTempData . SuccessMessage ] = $"Invoice {result.Id} just created!" ;
CreatedInvoiceId = result . Id ;
return RedirectToAction ( nameof ( Invoice ) , new { storeId = result . StoreId , invoiceId = result . Id } ) ;
2017-12-03 22:36:04 +09:00
}
2018-05-04 01:46:52 +09:00
catch ( BitpayHttpException ex )
2017-12-03 22:36:04 +09:00
{
2021-11-15 09:35:03 +01:00
TempData . SetStatusMessageModel ( new StatusMessageModel ( )
2021-11-04 17:12:17 +09:00
{
Severity = StatusMessageModel . StatusSeverity . Error ,
Message = ex . Message
} ) ;
2017-12-03 22:36:04 +09:00
return View ( model ) ;
}
2017-10-27 17:53:04 +09:00
}
2017-09-13 15:47:34 +09:00
2017-11-05 21:15:52 -06:00
[HttpPost]
2018-12-10 15:34:48 +09:00
[Route("invoices/{invoiceId}/changestate/{newState}")]
2022-01-11 21:49:56 +09:00
[Route("stores/{storeId}/invoices/{invoiceId}/changestate/{newState}")]
2022-02-02 20:24:22 +09:00
[Authorize(AuthenticationSchemes = AuthenticationSchemes.Cookie, Policy = Policies.CanViewInvoices)]
2017-11-05 21:15:52 -06:00
[BitpayAPIConstraint(false)]
2019-05-03 14:54:40 -05:00
public async Task < IActionResult > ChangeInvoiceState ( string invoiceId , string newState )
2017-11-05 21:15:52 -06:00
{
2021-12-16 17:37:19 +01:00
var invoice = ( await _InvoiceRepository . GetInvoices ( new InvoiceQuery
2018-12-06 17:08:28 +09:00
{
2020-06-28 17:55:27 +09:00
InvoiceId = new [ ] { invoiceId } ,
2018-12-06 17:08:28 +09:00
UserId = GetUserId ( )
} ) ) . FirstOrDefault ( ) ;
2019-05-03 14:54:40 -05:00
var model = new InvoiceStateChangeModel ( ) ;
2018-06-21 14:15:36 +09:00
if ( invoice = = null )
2019-05-03 14:54:40 -05:00
{
model . NotFound = true ;
return NotFound ( model ) ;
}
2018-12-10 15:34:48 +09:00
if ( newState = = "invalid" )
{
2020-07-24 09:40:37 +02:00
await _InvoiceRepository . MarkInvoiceStatus ( invoiceId , InvoiceStatus . Invalid ) ;
2024-05-15 07:49:53 +09:00
model . StatusString = new InvoiceState ( InvoiceStatus . Invalid , InvoiceExceptionStatus . Marked ) . ToString ( ) ;
2018-12-10 15:34:48 +09:00
}
2021-11-26 16:13:41 +02:00
else if ( newState = = "settled" )
2018-12-10 15:34:48 +09:00
{
2020-11-23 15:57:05 +09:00
await _InvoiceRepository . MarkInvoiceStatus ( invoiceId , InvoiceStatus . Settled ) ;
2024-05-15 07:49:53 +09:00
model . StatusString = new InvoiceState ( InvoiceStatus . Settled , InvoiceExceptionStatus . Marked ) . ToString ( ) ;
2018-12-10 15:34:48 +09:00
}
2019-05-03 14:54:40 -05:00
return Json ( model ) ;
}
public class InvoiceStateChangeModel
{
public bool NotFound { get ; set ; }
2021-07-29 20:29:34 +09:00
public string? StatusString { get ; set ; }
2017-11-05 21:15:52 -06:00
}
2021-12-20 15:15:32 +01:00
private StoreData GetCurrentStore ( ) = > HttpContext . GetStoreData ( ) ;
2021-12-31 16:59:02 +09:00
2021-12-20 15:15:32 +01:00
private InvoiceEntity GetCurrentInvoice ( ) = > HttpContext . GetInvoiceData ( ) ;
2021-12-16 17:37:19 +01:00
2023-11-28 23:20:03 +09:00
private string GetUserId ( ) = > _UserManager . GetUserId ( User ) ! ;
2018-11-27 07:13:09 +01:00
2024-01-11 16:25:56 +01:00
private SelectList GetPaymentMethodsSelectList ( StoreData store )
{
var excludeFilter = store . GetStoreBlob ( ) . GetExcludedPaymentMethods ( ) ;
2024-04-04 16:31:04 +09:00
return new SelectList ( store . GetPaymentMethodConfigs ( )
. Where ( s = > ! excludeFilter . Match ( s . Key ) )
. Select ( method = > new SelectListItem ( method . Key . ToString ( ) , method . Key . ToString ( ) ) ) ,
2024-01-11 16:25:56 +01:00
nameof ( SelectListItem . Value ) ,
nameof ( SelectListItem . Text ) ) ;
}
private IActionResult NoPaymentMethodResult ( string storeId )
{
TempData . SetStatusMessageModel ( new StatusMessageModel
{
Severity = StatusMessageModel . StatusSeverity . Error ,
Html = $"To create an invoice, you need to <a href='{Url.Action(nameof(UIStoresController.SetupWallet), " UIStores ", new { cryptoCode = _NetworkProvider.DefaultNetwork.CryptoCode, storeId })}' class='alert-link'>set up a wallet</a> first" ,
AllowDismiss = false
} ) ;
return RedirectToAction ( nameof ( ListInvoices ) , new { storeId } ) ;
}
2017-10-27 17:53:04 +09:00
}
2017-09-13 15:47:34 +09:00
}