2021-07-14 13:40:18 +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 ;
using System.Globalization ;
using System.Linq ;
2018-11-30 09:04:26 +01:00
using System.Net.Mime ;
2017-12-17 11:58:55 +01:00
using System.Net.WebSockets ;
using System.Threading ;
2018-08-30 18:34:39 +02:00
using System.Threading.Tasks ;
2020-11-17 13:46:23 +01:00
using BTCPayServer.Abstractions.Constants ;
using BTCPayServer.Abstractions.Extensions ;
using BTCPayServer.Abstractions.Models ;
2020-03-19 11:11:15 +01:00
using BTCPayServer.Client ;
2020-05-23 21:13:18 +02:00
using BTCPayServer.Client.Models ;
2018-08-30 18:34:39 +02:00
using BTCPayServer.Data ;
using BTCPayServer.Filters ;
2020-09-02 11:24:18 +02:00
using BTCPayServer.HostedServices ;
2021-10-18 10:42:53 +02:00
using BTCPayServer.Logging ;
2018-08-30 18:34:39 +02:00
using BTCPayServer.Models.InvoicingModels ;
2018-02-18 18:38:03 +01:00
using BTCPayServer.Payments ;
2020-09-02 11:24:18 +02:00
using BTCPayServer.Rating ;
2021-10-27 16:32:56 +02:00
using BTCPayServer.Services.Apps ;
2018-08-30 18:34:39 +02:00
using BTCPayServer.Services.Invoices ;
2018-11-30 09:04:26 +01:00
using BTCPayServer.Services.Invoices.Export ;
2020-09-02 11:24:18 +02:00
using BTCPayServer.Services.Rates ;
2018-08-30 18:34:39 +02:00
using Microsoft.AspNetCore.Authorization ;
using Microsoft.AspNetCore.Mvc ;
using Microsoft.AspNetCore.Mvc.Rendering ;
2020-06-24 10:51:00 +02:00
using Microsoft.AspNetCore.Routing ;
using Microsoft.EntityFrameworkCore ;
2021-10-18 10:42:53 +02:00
using Microsoft.Extensions.Logging ;
2018-08-30 18:34:39 +02:00
using NBitcoin ;
using NBitpayClient ;
using NBXplorer ;
2018-11-27 07:13:09 +01:00
using Newtonsoft.Json.Linq ;
2020-08-25 07:33:00 +02:00
using BitpayCreateInvoiceRequest = BTCPayServer . Models . BitpayCreateInvoiceRequest ;
2020-05-23 21:13:18 +02:00
using StoreData = BTCPayServer . Data . StoreData ;
2017-09-13 08:47:34 +02:00
namespace BTCPayServer.Controllers
{
2017-10-27 10:53:04 +02:00
public partial class InvoiceController
{
2020-11-06 12:42:26 +01:00
[HttpGet]
[Route("invoices/{invoiceId}/deliveries/{deliveryId}/request")]
[Authorize(AuthenticationSchemes = AuthenticationSchemes.Cookie)]
public async Task < IActionResult > WebhookDelivery ( string invoiceId , string deliveryId )
{
var invoice = ( await _InvoiceRepository . GetInvoices ( new InvoiceQuery ( )
{
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 ( ) ;
return this . File ( delivery . GetBlob ( ) . Request , "application/json" ) ;
}
[HttpPost]
[Route("invoices/{invoiceId}/deliveries/{deliveryId}/redeliver")]
[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
} ) ;
}
2017-10-27 10:53:04 +02:00
[HttpGet]
[Route("invoices/{invoiceId}")]
2019-10-12 13:35:30 +02:00
[Authorize(AuthenticationSchemes = AuthenticationSchemes.Cookie)]
2018-01-08 12:06:16 +01:00
public async Task < IActionResult > Invoice ( string invoiceId )
2017-10-27 10:53:04 +02:00
{
var invoice = ( await _InvoiceRepository . GetInvoices ( new InvoiceQuery ( )
{
2020-06-28 10:55:27 +02:00
InvoiceId = new [ ] { invoiceId } ,
2018-12-06 08:58:04 +01:00
UserId = GetUserId ( ) ,
2018-01-14 13:48:23 +01:00
IncludeAddresses = true ,
2020-05-07 12:50:07 +02:00
IncludeEvents = true ,
IncludeArchived = true ,
2017-10-27 10:53:04 +02:00
} ) ) . FirstOrDefault ( ) ;
if ( invoice = = null )
return NotFound ( ) ;
2017-10-12 17:25:45 +02:00
2017-10-27 10:53:04 +02:00
var store = await _StoreRepository . FindStore ( invoice . StoreId ) ;
2021-10-06 07:49:57 +02:00
var invoiceState = invoice . GetInvoiceState ( ) ;
2019-04-30 05:45:33 +02:00
var model = new InvoiceDetailsModel ( )
2017-10-27 10:53:04 +02:00
{
2020-11-06 12:42:26 +01:00
StoreId = store . Id ,
2017-10-27 10:53:04 +02:00
StoreName = store . StoreName ,
2021-10-29 08:25:43 +02:00
StoreLink = Url . Action ( nameof ( StoresController . PaymentMethods ) , "Stores" , new { storeId = store . Id } ) ,
2021-07-14 13:43:13 +02:00
PaymentRequestLink = Url . Action ( nameof ( PaymentRequestController . ViewPaymentRequest ) , "PaymentRequest" , new { id = invoice . Metadata . PaymentRequestId } ) ,
2017-10-27 10:53:04 +02:00
Id = invoice . Id ,
2021-10-06 07:49:57 +02:00
State = invoiceState . ToString ( ) ,
2018-05-13 08:09:17 +02:00
TransactionSpeed = invoice . SpeedPolicy = = SpeedPolicy . HighSpeed ? "high" :
2018-05-11 15:12:45 +02:00
invoice . SpeedPolicy = = SpeedPolicy . MediumSpeed ? "medium" :
invoice . SpeedPolicy = = SpeedPolicy . LowMediumSpeed ? "low-medium" :
"low" ,
2017-10-27 10:53:04 +02:00
RefundEmail = invoice . RefundMail ,
CreatedDate = invoice . InvoiceTime ,
ExpirationDate = invoice . ExpirationTime ,
2018-01-08 12:06:16 +01:00
MonitoringDate = invoice . MonitoringExpiration ,
2020-08-25 07:33:00 +02:00
Fiat = _CurrencyNameTable . DisplayFormatCurrency ( invoice . Price , invoice . Currency ) ,
TaxIncluded = _CurrencyNameTable . DisplayFormatCurrency ( invoice . Metadata . TaxIncluded ? ? 0.0 m , invoice . Currency ) ,
2019-09-05 04:41:51 +02:00
NotificationUrl = invoice . NotificationURL ? . AbsoluteUri ,
2019-09-05 04:55:31 +02:00
RedirectUrl = invoice . RedirectURL ? . AbsoluteUri ,
2020-08-25 07:33:00 +02:00
TypedMetadata = invoice . Metadata ,
2018-01-14 13:48:23 +01:00
StatusException = invoice . ExceptionStatus ,
2018-11-27 07:13:09 +01:00
Events = invoice . Events ,
2020-08-25 07:33:00 +02:00
PosData = PosDataParser . ParsePosData ( invoice . Metadata . PosData ) ,
2020-06-24 10:51:00 +02:00
Archived = invoice . Archived ,
2021-10-06 07:49:57 +02:00
CanRefund = CanRefund ( invoiceState ) ,
2021-06-24 12:52:41 +02:00
ShowCheckout = invoice . Status = = InvoiceStatusLegacy . New ,
2020-11-06 12:42:26 +01:00
Deliveries = ( await _InvoiceRepository . GetWebhookDeliveries ( invoiceId ) )
. Select ( c = > new Models . StoreViewModels . DeliveryViewModel ( c ) )
2021-10-06 07:49:57 +02:00
. ToList ( ) ,
CanMarkInvalid = invoiceState . CanMarkInvalid ( ) ,
CanMarkComplete = invoiceState . CanMarkComplete ( ) ,
2017-10-27 10:53:04 +02:00
} ;
2019-05-29 16:33:31 +02:00
model . Addresses = invoice . HistoricalAddresses . Select ( h = >
new InvoiceDetailsModel . AddressModel
{
Destination = h . GetAddress ( ) ,
2019-06-03 18:06:03 +02:00
PaymentMethod = h . GetPaymentMethodId ( ) . ToPrettyString ( ) ,
2019-05-29 16:33:31 +02:00
Current = ! h . UnAssigned . HasValue
} ) . ToArray ( ) ;
2019-04-30 05:45:33 +02:00
2019-05-07 16:32:47 +02:00
var details = InvoicePopulatePayments ( invoice ) ;
2019-04-30 05:45:33 +02:00
model . CryptoPayments = details . CryptoPayments ;
2019-08-24 16:10:13 +02:00
model . Payments = details . Payments ;
2019-04-30 05:45:33 +02:00
return View ( model ) ;
}
2020-06-24 10:51:00 +02:00
bool CanRefund ( InvoiceState invoiceState )
{
2020-11-23 07:57:05 +01:00
return invoiceState . Status = = InvoiceStatusLegacy . Confirmed | |
invoiceState . Status = = InvoiceStatusLegacy . Complete | |
( invoiceState . Status = = InvoiceStatusLegacy . Expired & &
2020-06-24 10:51:00 +02:00
( invoiceState . ExceptionStatus = = InvoiceExceptionStatus . PaidLate | |
invoiceState . ExceptionStatus = = InvoiceExceptionStatus . PaidOver | |
2020-07-14 08:42:37 +02:00
invoiceState . ExceptionStatus = = InvoiceExceptionStatus . PaidPartial ) ) | |
2020-11-23 07:57:05 +01:00
invoiceState . Status = = InvoiceStatusLegacy . Invalid ;
2020-06-24 10:51:00 +02:00
}
[HttpGet]
[Route("invoices/{invoiceId}/refund")]
[AllowAnonymous]
2021-10-18 05:37:59 +02:00
public async Task < IActionResult > Refund ( [ FromServices ] IEnumerable < IPayoutHandler > payoutHandlers , string invoiceId , CancellationToken cancellationToken )
2020-06-24 10:51:00 +02:00
{
2021-11-04 08:21:01 +01:00
await using var ctx = _dbContextFactory . CreateContext ( ) ;
2020-06-24 10:51:00 +02:00
ctx . ChangeTracker . QueryTrackingBehavior = Microsoft . EntityFrameworkCore . QueryTrackingBehavior . NoTracking ;
var invoice = await ctx . Invoices . Include ( i = > i . Payments )
. Include ( i = > i . CurrentRefund )
2021-11-04 08:21:01 +01:00
. Include ( i = > i . StoreData )
. ThenInclude ( data = > data . UserStores )
2020-06-24 10:51:00 +02:00
. Include ( i = > i . CurrentRefund . PullPaymentData )
. Where ( i = > i . Id = = invoiceId )
2021-11-04 08:21:01 +01:00
. FirstOrDefaultAsync ( cancellationToken ) ;
2020-06-24 10:51:00 +02:00
if ( invoice is null )
return NotFound ( ) ;
if ( invoice . CurrentRefund ? . PullPaymentDataId is null & & GetUserId ( ) is null )
return NotFound ( ) ;
if ( ! CanRefund ( invoice . GetInvoiceState ( ) ) )
return NotFound ( ) ;
if ( invoice . CurrentRefund ? . PullPaymentDataId is string ppId & & ! invoice . CurrentRefund . PullPaymentData . Archived )
{
// TODO: Having dedicated UI later on
return RedirectToAction ( nameof ( PullPaymentController . ViewPullPayment ) ,
"PullPayment" ,
new { pullPaymentId = ppId } ) ;
}
else
{
var paymentMethods = invoice . GetBlob ( _NetworkProvider ) . GetPaymentMethods ( ) ;
2021-10-18 05:37:59 +02:00
var pmis = paymentMethods . Select ( method = > method . GetId ( ) ) . ToList ( ) ;
2021-11-04 08:21:01 +01:00
var options = ( await payoutHandlers . GetSupportedPaymentMethods ( invoice . StoreData ) ) . Where ( id = > pmis . Contains ( id ) ) . ToList ( ) ;
if ( ! options . Any ( ) )
{
TempData . SetStatusMessageModel ( new StatusMessageModel ( )
{
Severity = StatusMessageModel . StatusSeverity . Error ,
Message = "There were no payment methods available to provide refunds with for this invoice."
} ) ;
return RedirectToAction ( nameof ( Invoice ) , new { invoiceId } ) ;
}
2020-08-09 14:43:13 +02:00
var defaultRefund = invoice . Payments
. Select ( p = > p . GetBlob ( _NetworkProvider ) )
2020-08-14 17:50:36 +02:00
. Select ( p = > p ? . GetPaymentMethodId ( ) )
2021-10-18 05:37:59 +02:00
. FirstOrDefault ( p = > p ! = null & & options . Contains ( p ) ) ;
2020-06-24 10:51:00 +02:00
// TODO: What if no option?
var refund = new RefundModel ( ) ;
refund . Title = "Select a payment method" ;
2021-10-29 13:46:24 +02:00
refund . AvailablePaymentMethods =
new SelectList ( options . Select ( id = > new SelectListItem ( id . ToPrettyString ( ) , id . ToString ( ) ) ) , "Value" , "Text" ) ;
refund . SelectedPaymentMethod = defaultRefund ? . ToString ( ) ? ? options . First ( ) . ToString ( ) ;
2020-06-24 10:51:00 +02:00
// Nothing to select, skip to next
if ( refund . AvailablePaymentMethods . Count ( ) = = 1 )
{
return await Refund ( invoiceId , refund , cancellationToken ) ;
}
return View ( refund ) ;
}
}
[HttpPost]
[Route("invoices/{invoiceId}/refund")]
[Authorize(AuthenticationSchemes = AuthenticationSchemes.Cookie)]
public async Task < IActionResult > Refund ( string invoiceId , RefundModel model , CancellationToken cancellationToken )
{
using var ctx = _dbContextFactory . CreateContext ( ) ;
var invoice = await _InvoiceRepository . GetInvoice ( invoiceId ) ;
if ( invoice is null )
return NotFound ( ) ;
var store = await _StoreRepository . FindStore ( invoice . StoreId , GetUserId ( ) ) ;
if ( store is null )
return NotFound ( ) ;
if ( ! CanRefund ( invoice . GetInvoiceState ( ) ) )
return NotFound ( ) ;
2021-10-18 05:37:59 +02:00
var paymentMethodId = PaymentMethodId . Parse ( model . SelectedPaymentMethod ) ;
2020-08-25 07:33:00 +02:00
var cdCurrency = _CurrencyNameTable . GetCurrencyData ( invoice . Currency , true ) ;
2020-06-24 10:51:00 +02:00
var paymentMethodDivisibility = _CurrencyNameTable . GetCurrencyData ( paymentMethodId . CryptoCode , false ) ? . Divisibility ? ? 8 ;
2020-09-02 11:24:18 +02:00
RateRules rules ;
RateResult rateResult ;
CreatePullPayment createPullPayment ;
switch ( model . RefundStep )
2020-06-24 10:51:00 +02:00
{
2020-09-02 11:24:18 +02:00
case RefundSteps . SelectPaymentMethod :
model . RefundStep = RefundSteps . SelectRate ;
model . Title = "What to refund?" ;
2021-10-29 14:50:18 +02:00
var pms = invoice . GetPaymentMethods ( ) ;
var paymentMethod = pms . SingleOrDefault ( method = > method . GetId ( ) = = paymentMethodId ) ;
//TODO: Make this clean
if ( paymentMethod is null & & paymentMethodId . PaymentType = = LightningPaymentType . Instance )
{
paymentMethod = pms [ new PaymentMethodId ( paymentMethodId . CryptoCode , PaymentTypes . LNURLPay ) ] ;
}
2021-08-31 08:08:33 +02:00
var cryptoPaid = paymentMethod . Calculate ( ) . Paid . ToDecimal ( MoneyUnit . BTC ) ;
2020-09-02 11:24:18 +02:00
var paidCurrency =
2021-08-31 08:08:33 +02:00
Math . Round ( cryptoPaid * paymentMethod . Rate ,
2020-09-02 11:24:18 +02:00
cdCurrency . Divisibility ) ;
2021-08-31 08:08:33 +02:00
model . CryptoAmountThen = cryptoPaid . RoundToSignificant ( paymentMethodDivisibility ) ;
2020-09-02 11:24:18 +02:00
model . RateThenText =
2021-08-03 10:03:00 +02:00
_CurrencyNameTable . DisplayFormatCurrency ( model . CryptoAmountThen , paymentMethodId . CryptoCode ) ;
2020-09-02 11:24:18 +02:00
rules = store . GetStoreBlob ( ) . GetRateRules ( _NetworkProvider ) ;
rateResult = await _RateProvider . FetchRate (
new Rating . CurrencyPair ( paymentMethodId . CryptoCode , invoice . Currency ) , rules ,
cancellationToken ) ;
//TODO: What if fetching rate failed?
if ( rateResult . BidAsk is null )
{
ModelState . AddModelError ( nameof ( model . SelectedRefundOption ) ,
$"Impossible to fetch rate: {rateResult.EvaluatedRule}" ) ;
return View ( model ) ;
}
model . CryptoAmountNow = Math . Round ( paidCurrency / rateResult . BidAsk . Bid , paymentMethodDivisibility ) ;
model . CurrentRateText =
2021-08-03 10:03:00 +02:00
_CurrencyNameTable . DisplayFormatCurrency ( model . CryptoAmountNow , paymentMethodId . CryptoCode ) ;
2020-09-02 11:24:18 +02:00
model . FiatAmount = paidCurrency ;
2021-08-03 10:03:00 +02:00
model . FiatText = _CurrencyNameTable . DisplayFormatCurrency ( model . FiatAmount , invoice . Currency ) ;
2020-06-24 10:51:00 +02:00
return View ( model ) ;
2020-09-02 11:24:18 +02:00
case RefundSteps . SelectRate :
createPullPayment = new HostedServices . CreatePullPayment ( ) ;
2020-06-24 10:51:00 +02:00
createPullPayment . Name = $"Refund {invoice.Id}" ;
createPullPayment . PaymentMethodIds = new [ ] { paymentMethodId } ;
createPullPayment . StoreId = invoice . StoreId ;
switch ( model . SelectedRefundOption )
{
case "RateThen" :
createPullPayment . Currency = paymentMethodId . CryptoCode ;
createPullPayment . Amount = model . CryptoAmountThen ;
break ;
case "CurrentRate" :
createPullPayment . Currency = paymentMethodId . CryptoCode ;
createPullPayment . Amount = model . CryptoAmountNow ;
break ;
case "Fiat" :
2020-08-25 07:33:00 +02:00
createPullPayment . Currency = invoice . Currency ;
2020-06-24 10:51:00 +02:00
createPullPayment . Amount = model . FiatAmount ;
break ;
2020-09-02 11:24:18 +02:00
case "Custom" :
model . Title = "How much to refund?" ;
model . CustomCurrency = invoice . Currency ;
model . CustomAmount = model . FiatAmount ;
model . RefundStep = RefundSteps . SelectCustomAmount ;
return View ( model ) ;
2020-06-24 10:51:00 +02:00
default :
ModelState . AddModelError ( nameof ( model . SelectedRefundOption ) , "Invalid choice" ) ;
return View ( model ) ;
}
2020-09-02 11:24:18 +02:00
break ;
case RefundSteps . SelectCustomAmount :
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 ( model ) ;
}
rules = store . GetStoreBlob ( ) . GetRateRules ( _NetworkProvider ) ;
rateResult = await _RateProvider . FetchRate (
new Rating . CurrencyPair ( paymentMethodId . CryptoCode , model . CustomCurrency ) , rules ,
cancellationToken ) ;
//TODO: What if fetching rate failed?
if ( rateResult . BidAsk is null )
{
ModelState . AddModelError ( nameof ( model . SelectedRefundOption ) ,
$"Impossible to fetch rate: {rateResult.EvaluatedRule}" ) ;
return View ( model ) ;
}
createPullPayment = new HostedServices . CreatePullPayment ( ) ;
createPullPayment . Name = $"Refund {invoice.Id}" ;
createPullPayment . PaymentMethodIds = new [ ] { paymentMethodId } ;
createPullPayment . StoreId = invoice . StoreId ;
createPullPayment . Currency = model . CustomCurrency ;
createPullPayment . Amount = model . CustomAmount ;
break ;
default :
throw new ArgumentOutOfRangeException ( ) ;
2020-06-24 10:51:00 +02:00
}
2020-09-02 11:24:18 +02:00
var ppId = await _paymentHostedService . CreatePullPayment ( createPullPayment ) ;
this . TempData . SetStatusMessageModel ( new StatusMessageModel ( )
{
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
} ) ;
2021-10-06 04:25:21 +02:00
( await ctx . Invoices . FindAsync ( new [ ] { invoice . Id } , cancellationToken : cancellationToken ) ) . CurrentRefundId = ppId ;
2020-09-02 11:24:18 +02:00
ctx . Refunds . Add ( new RefundData ( )
{
InvoiceDataId = invoice . Id ,
PullPaymentDataId = ppId
} ) ;
await ctx . SaveChangesAsync ( cancellationToken ) ;
// TODO: Having dedicated UI later on
return RedirectToAction ( nameof ( PullPaymentController . ViewPullPayment ) ,
"PullPayment" ,
new { pullPaymentId = ppId } ) ;
2020-06-24 10:51:00 +02:00
}
2019-05-07 16:32:47 +02:00
private InvoiceDetailsModel InvoicePopulatePayments ( InvoiceEntity invoice )
2019-04-30 05:45:33 +02:00
{
2020-10-17 08:57:21 +02:00
return new InvoiceDetailsModel
2018-01-08 12:06:16 +01:00
{
2020-10-17 08:57:21 +02:00
Archived = invoice . Archived ,
2021-05-14 09:16:19 +02:00
Payments = invoice . GetPayments ( false ) ,
2020-10-17 08:57:21 +02:00
CryptoPayments = invoice . GetPaymentMethods ( ) . Select (
data = >
{
var accounting = data . Calculate ( ) ;
var paymentMethodId = data . GetId ( ) ;
return new InvoiceDetailsModel . CryptoPayment
{
PaymentMethodId = paymentMethodId ,
PaymentMethod = paymentMethodId . ToPrettyString ( ) ,
Due = _CurrencyNameTable . DisplayFormatCurrency ( accounting . Due . ToDecimal ( MoneyUnit . BTC ) ,
paymentMethodId . CryptoCode ) ,
Paid = _CurrencyNameTable . DisplayFormatCurrency (
accounting . CryptoPaid . ToDecimal ( MoneyUnit . BTC ) ,
paymentMethodId . CryptoCode ) ,
Overpaid = _CurrencyNameTable . DisplayFormatCurrency (
accounting . OverpaidHelper . ToDecimal ( MoneyUnit . BTC ) , paymentMethodId . CryptoCode ) ,
Address = data . GetPaymentMethodDetails ( ) . GetPaymentDestination ( ) ,
2021-10-25 08:18:02 +02:00
Rate = ExchangeRate ( data ) ,
PaymentMethodRaw = data
2020-10-17 08:57:21 +02:00
} ;
} ) . ToList ( )
} ;
2017-10-27 10:53:04 +02:00
}
2017-09-13 08:47:34 +02:00
2020-05-07 12:50:07 +02:00
[HttpPost("invoices/{invoiceId}/archive")]
[Authorize(AuthenticationSchemes = AuthenticationSchemes.Cookie)]
[BitpayAPIConstraint(false)]
public async Task < IActionResult > ToggleArchive ( string invoiceId )
{
var invoice = ( await _InvoiceRepository . GetInvoices ( new InvoiceQuery ( )
{
2020-06-28 10:55:27 +02:00
InvoiceId = new [ ] { invoiceId } ,
UserId = GetUserId ( ) ,
IncludeAddresses = true ,
IncludeEvents = true ,
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 10:55:27 +02:00
return RedirectToAction ( nameof ( invoice ) , new { invoiceId } ) ;
2020-05-07 12:50:07 +02:00
}
2020-06-28 10:55:27 +02:00
2020-07-15 04:58:52 +02:00
[HttpPost]
public async Task < IActionResult > MassAction ( string command , string [ ] selectedItems )
{
if ( selectedItems ! = null )
{
switch ( command )
{
case "archive" :
await _InvoiceRepository . MassArchive ( selectedItems ) ;
TempData [ WellKnownTempData . SuccessMessage ] = $"{selectedItems.Length} invoice(s) archived." ;
break ;
}
}
return RedirectToAction ( nameof ( ListInvoices ) ) ;
}
2017-10-27 10:53:04 +02:00
[HttpGet]
[Route("i/{invoiceId}")]
2018-02-19 07:09:05 +01:00
[Route("i/{invoiceId}/{paymentMethodId}")]
2017-10-27 10:53:04 +02:00
[Route("invoice")]
[AcceptMediaTypeConstraint("application/bitcoin-paymentrequest", false)]
[XFrameOptionsAttribute(null)]
2018-07-11 19:38:08 +02:00
[ReferrerPolicyAttribute("origin")]
2021-07-14 13:40:18 +02: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 10:53:04 +02:00
{
//Keep compatibility with Bitpay
invoiceId = invoiceId ? ? id ;
2019-03-31 20:46:38 +02:00
//
2021-07-29 13:29:34 +02:00
if ( invoiceId is null )
return NotFound ( ) ;
2020-12-10 15:34:50 +01:00
var model = await GetInvoiceModel ( invoiceId , paymentMethodId = = null ? null : PaymentMethodId . Parse ( paymentMethodId ) , lang ) ;
2017-10-27 10:53:04 +02:00
if ( model = = null )
return NotFound ( ) ;
2017-10-21 05:24:28 +02:00
2018-11-09 08:09:09 +01:00
if ( view = = "modal" )
model . IsModal = true ;
2017-10-27 10:53:04 +02:00
return View ( nameof ( Checkout ) , model ) ;
}
2021-10-11 04:11:02 +02:00
2019-03-31 20:46:38 +02:00
[HttpGet]
[Route("invoice-noscript")]
2021-07-14 13:40:18 +02:00
public async Task < IActionResult > CheckoutNoScript ( string? invoiceId , string? id = null , string? paymentMethodId = null , [ FromQuery ] string? lang = null )
2019-03-31 20:46:38 +02:00
{
//Keep compatibility with Bitpay
invoiceId = invoiceId ? ? id ;
//
2021-07-29 13:29:34 +02:00
if ( invoiceId is null )
return NotFound ( ) ;
var model = await GetInvoiceModel ( invoiceId , paymentMethodId is null ? null : PaymentMethodId . Parse ( paymentMethodId ) , lang ) ;
2019-03-31 20:46:38 +02:00
if ( model = = null )
return NotFound ( ) ;
2019-04-05 04:48:52 +02:00
return View ( model ) ;
2019-03-31 20:46:38 +02:00
}
2021-07-29 13:29:34 +02:00
private async Task < PaymentModel ? > GetInvoiceModel ( string invoiceId , PaymentMethodId ? paymentMethodId , string? lang )
2017-10-27 10:53:04 +02:00
{
2018-12-06 09:05:27 +01:00
var invoice = await _InvoiceRepository . GetInvoice ( invoiceId ) ;
2018-01-10 10:33:05 +01:00
if ( invoice = = null )
return null ;
2018-01-09 03:41:07 +01:00
var store = await _StoreRepository . FindStore ( invoice . StoreId ) ;
2019-01-31 11:07:38 +01:00
bool isDefaultPaymentId = false ;
2021-07-29 13:29:34 +02:00
if ( paymentMethodId is null )
2018-02-18 18:38:03 +01:00
{
2021-10-18 09:56:47 +02:00
var enabledPaymentIds = store . GetEnabledPaymentIds ( _NetworkProvider ) ? ? Array . Empty < PaymentMethodId > ( ) ;
PaymentMethodId ? invoicePaymentId = invoice . GetDefaultPaymentMethod ( ) ;
PaymentMethodId ? storePaymentId = store . GetDefaultPaymentId ( ) ;
if ( invoicePaymentId is PaymentMethodId )
{
if ( enabledPaymentIds . Contains ( invoicePaymentId ) )
paymentMethodId = invoicePaymentId ;
}
if ( paymentMethodId is null & & storePaymentId is PaymentMethodId )
{
if ( enabledPaymentIds . Contains ( storePaymentId ) )
paymentMethodId = storePaymentId ;
}
if ( paymentMethodId is null & & invoicePaymentId is PaymentMethodId )
{
paymentMethodId = invoicePaymentId . FindNearest ( enabledPaymentIds ) ;
}
if ( paymentMethodId is null & & storePaymentId is PaymentMethodId )
{
paymentMethodId = storePaymentId . FindNearest ( enabledPaymentIds ) ;
}
if ( paymentMethodId is null )
{
2021-11-04 10:21:38 +01:00
paymentMethodId = enabledPaymentIds . FirstOrDefault ( e = > e . CryptoCode = = "BTC" & & e . PaymentType = = PaymentTypes . BTCLike ) ? ?
enabledPaymentIds . FirstOrDefault ( e = > e . CryptoCode = = "BTC" & & e . PaymentType = = PaymentTypes . LightningLike ) ? ?
enabledPaymentIds . First ( ) ;
2021-10-18 09:56:47 +02:00
}
2019-01-31 11:07:38 +01:00
isDefaultPaymentId = true ;
2018-01-12 08:30:34 +01:00
}
2019-09-30 10:32:43 +02:00
BTCPayNetworkBase network = _NetworkProvider . GetNetwork < BTCPayNetworkBase > ( paymentMethodId . CryptoCode ) ;
2021-10-18 09:56:47 +02:00
if ( network is null | | ! invoice . Support ( paymentMethodId ) )
2018-01-12 08:30:34 +01:00
{
2019-01-31 11:07:38 +01:00
if ( ! isDefaultPaymentId )
2018-01-12 08:30:34 +01:00
return null ;
2021-04-07 06:08:42 +02:00
var paymentMethodTemp = invoice
. GetPaymentMethods ( )
. FirstOrDefault ( c = > paymentMethodId . CryptoCode = = c . GetId ( ) . CryptoCode ) ;
2018-08-08 07:45:46 +02:00
if ( paymentMethodTemp = = null )
2019-05-24 15:22:38 +02:00
paymentMethodTemp = invoice . GetPaymentMethods ( ) . First ( ) ;
2018-02-19 07:09:05 +01:00
network = paymentMethodTemp . Network ;
paymentMethodId = paymentMethodTemp . GetId ( ) ;
2018-01-12 08:30:34 +01:00
}
2018-01-09 03:41:07 +01:00
2019-06-07 06:24:36 +02:00
var paymentMethod = invoice . GetPaymentMethod ( paymentMethodId ) ;
2018-02-19 07:09:05 +01:00
var paymentMethodDetails = paymentMethod . GetPaymentMethodDetails ( ) ;
2021-04-07 06:08:42 +02:00
if ( ! paymentMethodDetails . Activated )
{
2021-09-23 17:00:55 +02:00
if ( await _InvoiceRepository . ActivateInvoicePaymentMethod ( _EventAggregator , _NetworkProvider ,
_paymentMethodHandlerDictionary , store , invoice , paymentMethod . GetId ( ) ) )
{
return await GetInvoiceModel ( invoiceId , paymentMethodId , lang ) ;
}
2021-04-07 06:08:42 +02:00
}
2019-05-24 15:22:38 +02:00
var dto = invoice . EntityToDTO ( ) ;
2018-03-23 09:27:48 +01:00
var storeBlob = store . GetStoreBlob ( ) ;
2018-02-19 07:09:05 +01:00
var accounting = paymentMethod . Calculate ( ) ;
2018-10-24 07:52:19 +02:00
2019-06-04 03:11:52 +02:00
var paymentMethodHandler = _paymentMethodHandlerDictionary [ paymentMethodId ] ;
2020-06-28 10:55:27 +02:00
2020-05-02 18:28:35 +02:00
var divisibility = _CurrencyNameTable . GetNumberFormatInfo ( paymentMethod . GetId ( ) . CryptoCode , false ) ? . CurrencyDecimalDigits ;
2021-07-27 08:17:56 +02:00
switch ( lang ? . ToLowerInvariant ( ) )
{
case "auto" :
case null when storeBlob . AutoDetectLanguage :
lang = _languageService . AutoDetectLanguageUsingHeader ( HttpContext . Request . Headers , null ) . Code ;
break ;
case { } langs when ! string . IsNullOrEmpty ( langs ) :
{
lang = _languageService . FindLanguage ( langs ) ? . Code ;
break ;
}
}
lang ? ? = storeBlob . DefaultLang ;
2021-08-03 10:03:00 +02:00
2017-10-27 10:53:04 +02:00
var model = new PaymentModel ( )
{
2021-04-07 06:08:42 +02:00
Activated = paymentMethodDetails . Activated ,
2018-01-09 03:41:07 +01:00
CryptoCode = network . CryptoCode ,
2019-03-08 16:48:33 +01:00
RootPath = this . Request . PathBase . Value . WithTrailingSlash ( ) ,
2020-08-25 07:33:00 +02:00
OrderId = invoice . Metadata . OrderId ,
2017-10-27 10:53:04 +02:00
InvoiceId = invoice . Id ,
2020-12-10 15:34:50 +01:00
DefaultLang = lang ? ? invoice . DefaultLanguage ? ? storeBlob . DefaultLang ? ? "en" ,
2019-11-06 04:01:29 +01:00
CustomCSSLink = storeBlob . CustomCSS ,
CustomLogoLink = storeBlob . CustomLogo ,
2020-03-27 00:26:06 +01:00
HtmlTitle = storeBlob . HtmlTitle ? ? "BTCPay Invoice" ,
2019-05-31 08:00:32 +02:00
CryptoImage = Request . GetRelativePathOrAbsolute ( paymentMethodHandler . GetCryptoImage ( paymentMethodId ) ) ,
2018-02-19 07:09:05 +01:00
BtcAddress = paymentMethodDetails . GetPaymentDestination ( ) ,
2020-05-03 18:04:34 +02:00
BtcDue = accounting . Due . ShowMoney ( divisibility ) ,
2020-11-06 11:09:17 +01:00
InvoiceCurrency = invoice . Currency ,
2020-05-03 18:04:34 +02:00
OrderAmount = ( accounting . TotalDue - accounting . NetworkFee ) . ShowMoney ( divisibility ) ,
2021-08-03 10:03:00 +02:00
IsUnsetTopUp = invoice . IsUnsetTopUp ( ) ,
2020-08-25 07:33:00 +02:00
OrderAmountFiat = OrderAmountFromInvoice ( network . CryptoCode , invoice ) ,
2017-10-27 10:53:04 +02:00
CustomerEmail = invoice . RefundMail ,
2021-10-27 16:32:56 +02:00
RequiresRefundEmail = invoice . RequiresRefundEmail ? ? storeBlob . RequiresRefundEmail ,
2017-10-27 10:53:04 +02:00
ExpirationSeconds = Math . Max ( 0 , ( int ) ( invoice . ExpirationTime - DateTimeOffset . UtcNow ) . TotalSeconds ) ,
MaxTimeSeconds = ( int ) ( invoice . ExpirationTime - invoice . InvoiceTime ) . TotalSeconds ,
2018-01-19 16:33:37 +01:00
MaxTimeMinutes = ( int ) ( invoice . ExpirationTime - invoice . InvoiceTime ) . TotalMinutes ,
2020-08-25 07:33:00 +02:00
ItemDesc = invoice . Metadata . ItemDesc ,
2018-05-16 12:46:11 +02:00
Rate = ExchangeRate ( paymentMethod ) ,
2019-09-04 11:20:36 +02:00
MerchantRefLink = invoice . RedirectURL ? . AbsoluteUri ? ? "/" ,
2019-04-11 11:08:42 +02:00
RedirectAutomatically = invoice . RedirectAutomatically ,
2017-10-27 10:53:04 +02:00
StoreName = store . StoreName ,
2018-02-25 16:48:12 +01: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 ( )
} ,
2020-05-03 18:04:34 +02:00
BtcPaid = accounting . Paid . ShowMoney ( divisibility ) ,
2018-12-10 13:48:28 +01:00
#pragma warning disable CS0618 // Type or member is obsolete
Status = invoice . StatusString ,
#pragma warning restore CS0618 // Type or member is obsolete
2019-01-07 07:35:18 +01:00
NetworkFee = paymentMethodDetails . GetNextNetworkFee ( ) ,
2021-05-14 09:16:19 +02:00
IsMultiCurrency = invoice . GetPayments ( false ) . Select ( p = > p . GetPaymentMethodId ( ) ) . Concat ( new [ ] { paymentMethod . GetId ( ) } ) . Distinct ( ) . Count ( ) > 1 ,
2018-10-24 07:52:19 +02:00
StoreId = store . Id ,
2019-05-24 15:22:38 +02:00
AvailableCryptos = invoice . GetPaymentMethods ( )
2018-02-18 18:38:03 +01:00
. Where ( i = > i . Network ! = null )
2019-05-29 16:33:31 +02:00
. Select ( kv = >
2018-03-19 01:45:54 +01:00
{
2019-05-29 16:33:31 +02:00
var availableCryptoPaymentMethodId = kv . GetId ( ) ;
2019-06-04 03:11:52 +02:00
var availableCryptoHandler = _paymentMethodHandlerDictionary [ availableCryptoPaymentMethodId ] ;
2019-05-29 16:33:31 +02:00
return new PaymentModel . AvailableCrypto ( )
{
PaymentMethodId = kv . GetId ( ) . ToString ( ) ,
2020-01-16 07:01:01 +01:00
CryptoCode = kv . Network ? . CryptoCode ? ? kv . GetId ( ) . CryptoCode ,
2019-05-29 16:33:31 +02:00
PaymentMethodName = availableCryptoHandler . GetPaymentMethodName ( availableCryptoPaymentMethodId ) ,
IsLightning =
kv . GetId ( ) . PaymentType = = PaymentTypes . LightningLike ,
2019-05-31 08:00:32 +02:00
CryptoImage = Request . GetRelativePathOrAbsolute ( availableCryptoHandler . GetCryptoImage ( availableCryptoPaymentMethodId ) ) ,
2019-05-29 16:33:31 +02:00
Link = Url . Action ( nameof ( Checkout ) ,
new
{
invoiceId = invoiceId ,
paymentMethodId = kv . GetId ( ) . ToString ( )
} )
} ;
2018-03-19 01:45:54 +01:00
} ) . Where ( c = > c . CryptoImage ! = "/" )
2018-07-19 06:53:00 +02:00
. OrderByDescending ( a = > a . CryptoCode = = "BTC" ) . ThenBy ( a = > a . PaymentMethodName ) . ThenBy ( a = > a . IsLightning ? 1 : 0 )
2018-07-14 05:35:34 +02:00
. ToList ( )
2017-10-27 10:53:04 +02:00
} ;
2020-11-06 11:09:17 +01:00
paymentMethodHandler . PreparePaymentModel ( model , dto , storeBlob , paymentMethod ) ;
2019-08-25 15:50:11 +02:00
model . UISettings = paymentMethodHandler . GetCheckoutUISettings ( ) ;
2019-05-29 16:33:31 +02:00
model . PaymentMethodId = paymentMethodId . ToString ( ) ;
2017-10-27 10:53:04 +02:00
var expiration = TimeSpan . FromSeconds ( model . ExpirationSeconds ) ;
2018-04-18 11:23:39 +02:00
model . TimeLeft = expiration . PrettyPrint ( ) ;
2017-10-27 10:53:04 +02:00
return model ;
}
2017-09-13 08:47:34 +02:00
2021-07-14 13:40:18 +02:00
private string? OrderAmountFromInvoice ( string cryptoCode , InvoiceEntity invoiceEntity )
2018-05-16 12:46:11 +02:00
{
// if invoice source currency is the same as currently display currency, no need for "order amount from invoice"
2020-08-25 07:33:00 +02:00
if ( cryptoCode = = invoiceEntity . Currency )
2018-05-16 12:46:11 +02:00
return null ;
2020-08-25 07:33:00 +02:00
return _CurrencyNameTable . DisplayFormatCurrency ( invoiceEntity . Price , invoiceEntity . Currency ) ;
2018-05-16 12:46:11 +02:00
}
private string ExchangeRate ( PaymentMethod paymentMethod )
2018-01-08 12:06:16 +01:00
{
2020-08-25 07:33:00 +02:00
string currency = paymentMethod . ParentEntity . Currency ;
2018-10-09 16:30:06 +02:00
return _CurrencyNameTable . DisplayFormatCurrency ( paymentMethod . Rate , currency ) ;
2018-05-10 05:39:13 +02:00
}
2018-01-08 12:06:16 +01:00
2017-10-27 10:53:04 +02:00
[HttpGet]
[Route("i/{invoiceId}/status")]
2021-10-30 06:57:24 +02:00
[Route("i/{invoiceId}/{implicitPaymentMethodId}/status")]
2019-01-28 08:24:11 +01:00
[Route("invoice/{invoiceId}/status")]
2021-10-30 06:57:24 +02:00
[Route("invoice/{invoiceId}/{implicitPaymentMethodId}/status")]
2019-01-28 08:24:11 +01:00
[Route("invoice/status")]
2021-10-30 06:57:24 +02:00
public async Task < IActionResult > GetStatus ( string invoiceId , string? paymentMethodId = null , string? implicitPaymentMethodId = null , [ FromQuery ] string? lang = null )
2017-10-27 10:53:04 +02:00
{
2021-10-30 06:57:24 +02:00
if ( string . IsNullOrEmpty ( paymentMethodId ) )
paymentMethodId = implicitPaymentMethodId ;
2020-12-10 15:34:50 +01:00
var model = await GetInvoiceModel ( invoiceId , paymentMethodId = = null ? null : PaymentMethodId . Parse ( paymentMethodId ) , lang ) ;
2017-10-27 10:53:04 +02:00
if ( model = = null )
return NotFound ( ) ;
return Json ( model ) ;
}
2017-09-13 08:47:34 +02:00
2017-12-17 11:58:55 +01:00
[HttpGet]
[Route("i/{invoiceId}/status/ws")]
2019-01-28 08:24:11 +01:00
[Route("i/{invoiceId}/{paymentMethodId}/status/ws")]
[Route("invoice/{invoiceId}/status/ws")]
[Route("invoice/{invoiceId}/{paymentMethodId}/status")]
[Route("invoice/status/ws")]
2021-10-05 06:37:30 +02:00
public async Task < IActionResult > GetStatusWebSocket ( string invoiceId , CancellationToken cancellationToken )
2017-12-17 11:58:55 +01:00
{
if ( ! HttpContext . WebSockets . IsWebSocketRequest )
return NotFound ( ) ;
2018-12-06 09:05:27 +01:00
var invoice = await _InvoiceRepository . GetInvoice ( invoiceId ) ;
2020-11-23 07:57:05 +01:00
if ( invoice = = null | | invoice . Status = = InvoiceStatusLegacy . Complete | | invoice . Status = = InvoiceStatusLegacy . Invalid | | invoice . Status = = InvoiceStatusLegacy . Expired )
2017-12-17 11:58:55 +01:00
return NotFound ( ) ;
var webSocket = await HttpContext . WebSockets . AcceptWebSocketAsync ( ) ;
CompositeDisposable leases = new CompositeDisposable ( ) ;
try
{
2021-10-06 06:22:55 +02: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 11:58:55 +01:00
while ( true )
{
2021-10-05 06:37:30 +02:00
var message = await webSocket . ReceiveAndPingAsync ( DummyBuffer , default ( CancellationToken ) ) ;
2017-12-17 11:58:55 +01:00
if ( message . MessageType = = WebSocketMessageType . Close )
break ;
}
}
2020-06-28 10:55:27 +02:00
catch ( WebSocketException ) { }
2017-12-17 11:58:55 +01:00
finally
{
leases . Dispose ( ) ;
2018-02-12 19:27:36 +01:00
await webSocket . CloseSocket ( ) ;
2017-12-17 11:58:55 +01:00
}
2017-12-25 13:52:27 +01:00
return new EmptyResult ( ) ;
2017-12-17 11:58:55 +01:00
}
2018-01-08 12:06:16 +01:00
2020-06-29 05:07:48 +02:00
readonly ArraySegment < Byte > DummyBuffer = new ArraySegment < Byte > ( new Byte [ 1 ] ) ;
2021-10-23 07:47:15 +02:00
public string? CreatedInvoiceId ;
2021-10-20 16:17:40 +02:00
2017-12-17 11:58:55 +01:00
private async Task NotifySocket ( WebSocket webSocket , string invoiceId , string expectedId )
{
if ( invoiceId ! = expectedId | | webSocket . State ! = WebSocketState . Open )
return ;
2020-01-12 07:32:26 +01:00
using CancellationTokenSource cts = new CancellationTokenSource ( ) ;
2017-12-17 11:58:55 +01:00
cts . CancelAfter ( 5000 ) ;
try
{
await webSocket . SendAsync ( DummyBuffer , WebSocketMessageType . Binary , true , cts . Token ) ;
}
2018-01-12 10:32:46 +01:00
catch { try { webSocket . Dispose ( ) ; } catch { } }
2017-12-17 11:58:55 +01:00
}
2017-10-27 10:53:04 +02:00
[HttpPost]
[Route("i/{invoiceId}/UpdateCustomer")]
2019-01-28 08:24:11 +01:00
[Route("invoice/UpdateCustomer")]
2020-06-28 10:55:27 +02:00
public async Task < IActionResult > UpdateCustomer ( string invoiceId , [ FromBody ] UpdateCustomerModel data )
2017-10-27 10:53:04 +02:00
{
if ( ! ModelState . IsValid )
{
return BadRequest ( ModelState ) ;
}
await _InvoiceRepository . UpdateInvoice ( invoiceId , data ) . ConfigureAwait ( false ) ;
2019-03-31 18:48:53 +02:00
return Ok ( "{}" ) ;
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")]
2019-10-12 13:35:30 +02:00
[Authorize(AuthenticationSchemes = AuthenticationSchemes.Cookie)]
2017-10-27 10:53:04 +02:00
[BitpayAPIConstraint(false)]
2021-07-14 13:40:18 +02:00
public async Task < IActionResult > ListInvoices ( InvoicesModel ? model = null )
2017-10-27 10:53:04 +02:00
{
2020-07-30 16:10:10 +02:00
model = this . ParseListQuery ( model ? ? new InvoicesModel ( ) ) ;
2020-07-19 23:20:28 +02:00
2020-07-30 16:10:10 +02:00
var fs = new SearchString ( model . SearchTerm ) ;
2019-09-12 05:54:26 +02:00
var storeIds = fs . GetFilterArray ( "storeid" ) ! = null ? fs . GetFilterArray ( "storeid" ) : new List < string > ( ) . ToArray ( ) ;
2020-07-30 16:10:10 +02:00
model . StoreIds = storeIds ;
InvoiceQuery invoiceQuery = GetInvoiceQuery ( model . SearchTerm , model . TimezoneOffset ? ? 0 ) ;
2019-01-17 15:40:47 +01:00
var counting = _InvoiceRepository . GetInvoicesTotal ( invoiceQuery ) ;
2020-12-28 11:10:53 +01:00
invoiceQuery . Take = model . Count ;
2020-07-30 16:10:10 +02:00
invoiceQuery . Skip = model . Skip ;
2019-01-16 21:33:04 +01:00
var list = await _InvoiceRepository . GetInvoices ( invoiceQuery ) ;
2019-04-26 01:13:17 +02:00
2018-10-12 03:09:13 +02:00
foreach ( var invoice in list )
2017-10-27 10:53:04 +02:00
{
2018-12-10 07:34:48 +01:00
var state = invoice . GetInvoiceState ( ) ;
2017-10-27 10:53:04 +02:00
model . Invoices . Add ( new InvoiceModel ( )
{
2020-10-08 08:42:45 +02:00
Status = state ,
2020-11-23 07:57:05 +01:00
ShowCheckout = invoice . Status = = InvoiceStatusLegacy . New ,
2018-05-26 16:32:20 +02:00
Date = invoice . InvoiceTime ,
2017-10-27 10:53:04 +02:00
InvoiceId = invoice . Id ,
2020-08-25 07:33:00 +02:00
OrderId = invoice . Metadata . OrderId ? ? string . Empty ,
2019-09-04 11:20:36 +02:00
RedirectUrl = invoice . RedirectURL ? . AbsoluteUri ? ? string . Empty ,
2020-08-25 07:33:00 +02:00
AmountCurrency = _CurrencyNameTable . DisplayFormatCurrency ( invoice . Price , invoice . Currency ) ,
2018-12-10 07:34:48 +01:00
CanMarkInvalid = state . CanMarkInvalid ( ) ,
2019-04-30 05:45:33 +02:00
CanMarkComplete = state . CanMarkComplete ( ) ,
2020-05-07 12:50:07 +02:00
Details = InvoicePopulatePayments ( invoice ) ,
2017-10-27 10:53:04 +02:00
} ) ;
}
2019-01-17 15:40:47 +01:00
model . Total = await counting ;
2017-10-27 10:53:04 +02:00
return View ( model ) ;
}
2017-09-13 08:47:34 +02:00
2021-07-14 13:40:18 +02:00
private InvoiceQuery GetInvoiceQuery ( string? searchTerm = null , int timezoneOffset = 0 )
2018-10-12 03:09:13 +02:00
{
2019-04-26 01:13:17 +02:00
var fs = new SearchString ( searchTerm ) ;
2019-01-16 21:33:04 +01:00
var invoiceQuery = new InvoiceQuery ( )
2018-10-12 03:09:13 +02:00
{
2019-04-26 01:13:17 +02:00
TextSearch = fs . TextSearch ,
2018-10-12 03:09:13 +02:00
UserId = GetUserId ( ) ,
2019-04-26 01:13:17 +02:00
Unusual = fs . GetFilterBool ( "unusual" ) ,
2020-05-07 12:50:07 +02:00
IncludeArchived = fs . GetFilterBool ( "includearchived" ) ? ? false ,
2019-04-26 01:13:17 +02: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
} ;
return invoiceQuery ;
2018-10-12 03:09:13 +02:00
}
2018-11-30 08:22:39 +01:00
[HttpGet]
2019-10-12 13:35:30 +02:00
[Authorize(AuthenticationSchemes = AuthenticationSchemes.Cookie)]
2018-11-30 08:22:39 +01:00
[BitpayAPIConstraint(false)]
2021-07-14 13:40:18 +02:00
public async Task < IActionResult > Export ( string format , string? searchTerm = null , int timezoneOffset = 0 )
2018-11-30 08:22:39 +01:00
{
2019-06-07 06:24:36 +02:00
var model = new InvoiceExport ( _CurrencyNameTable ) ;
2018-11-30 08:22:39 +01:00
2019-04-26 01:13:17 +02:00
InvoiceQuery invoiceQuery = GetInvoiceQuery ( searchTerm , timezoneOffset ) ;
2019-01-16 21:33:04 +01:00
invoiceQuery . Skip = 0 ;
2020-12-28 11:10:53 +01:00
invoiceQuery . Take = int . MaxValue ;
2019-01-16 21:33:04 +01:00
var invoices = await _InvoiceRepository . GetInvoices ( invoiceQuery ) ;
2018-11-30 08:22:39 +01:00
var res = model . Process ( invoices , format ) ;
2018-11-30 09:04:26 +01:00
var cd = new ContentDisposition
{
2018-11-30 09:51:23 +01:00
FileName = $"btcpay-export-{DateTime.UtcNow.ToString(" yyyyMMdd - HHmmss ", CultureInfo.InvariantCulture)}.{format}" ,
2018-11-30 09:04:26 +01:00
Inline = true
} ;
Response . Headers . Add ( "Content-Disposition" , cd . ToString ( ) ) ;
Response . Headers . Add ( "X-Content-Type-Options" , "nosniff" ) ;
2018-11-30 08:22:39 +01:00
return Content ( res , "application/" + format ) ;
}
2019-05-24 08:38:47 +02:00
private SelectList GetPaymentMethodsSelectList ( )
{
2019-05-29 16:33:31 +02:00
return new SelectList ( _paymentMethodHandlerDictionary . Distinct ( ) . SelectMany ( handler = >
handler . GetSupportedPaymentMethods ( )
2019-06-03 18:06:03 +02:00
. Select ( id = > new SelectListItem ( id . ToPrettyString ( ) , id . ToString ( ) ) ) ) ,
2019-05-24 08:38:47 +02:00
nameof ( SelectListItem . Value ) ,
nameof ( SelectListItem . Text ) ) ;
}
2019-05-29 16:33:31 +02:00
2017-10-27 10:53:04 +02:00
[HttpGet]
[Route("invoices/create")]
2019-10-12 13:35:30 +02:00
[Authorize(AuthenticationSchemes = AuthenticationSchemes.Cookie)]
2017-10-27 10:53:04 +02:00
[BitpayAPIConstraint(false)]
2021-07-14 13:40:18 +02:00
public async Task < IActionResult > CreateInvoice ( InvoicesModel ? model = null )
2017-10-27 10:53:04 +02:00
{
2021-07-14 13:40:18 +02:00
var stores = new SelectList (
await _StoreRepository . GetStoresByUserId ( GetUserId ( ) ) ,
nameof ( StoreData . Id ) ,
nameof ( StoreData . StoreName ) ,
new SearchString ( model ? . SearchTerm ) . GetFilterArray ( "storeid" ) ? . ToArray ( ) . FirstOrDefault ( )
) ;
2020-01-12 07:32:26 +01:00
if ( ! stores . Any ( ) )
2017-10-27 10:53:04 +02:00
{
2019-10-31 04:29:59 +01:00
TempData [ WellKnownTempData . ErrorMessage ] = "You need to create at least one store before creating a transaction" ;
2018-03-23 08:24:57 +01:00
return RedirectToAction ( nameof ( UserStoresController . ListStores ) , "UserStores" ) ;
2017-10-27 10:53:04 +02:00
}
2019-05-02 14:29:51 +02:00
2019-05-24 08:38:47 +02:00
return View ( new CreateInvoiceModel ( ) { Stores = stores , AvailablePaymentMethods = GetPaymentMethodsSelectList ( ) } ) ;
2017-10-27 10:53:04 +02:00
}
2017-09-13 08:47:34 +02:00
2017-10-27 10:53:04 +02:00
[HttpPost]
[Route("invoices/create")]
2020-03-20 05:41:47 +01:00
[Authorize(Policy = Policies.CanCreateInvoice, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
2017-10-27 10:53:04 +02:00
[BitpayAPIConstraint(false)]
2019-03-05 09:09:17 +01:00
public async Task < IActionResult > CreateInvoice ( CreateInvoiceModel model , CancellationToken cancellationToken )
2017-10-27 10:53:04 +02:00
{
2018-04-29 19:33:42 +02:00
var stores = await _StoreRepository . GetStoresByUserId ( GetUserId ( ) ) ;
model . Stores = new SelectList ( stores , nameof ( StoreData . Id ) , nameof ( StoreData . StoreName ) , model . StoreId ) ;
2019-05-24 08:38:47 +02:00
model . AvailablePaymentMethods = GetPaymentMethodsSelectList ( ) ;
2019-10-12 13:35:30 +02:00
var store = HttpContext . GetStoreData ( ) ;
2017-10-27 10:53:04 +02:00
if ( ! ModelState . IsValid )
{
return View ( model ) ;
}
2019-10-31 04:29:59 +01:00
2020-01-12 07:32:26 +01:00
if ( ! store . GetSupportedPaymentMethods ( _NetworkProvider ) . Any ( ) )
2017-10-27 10:53:04 +02:00
{
2021-09-13 07:24:10 +02:00
TempData . SetStatusMessageModel ( new StatusMessageModel ( )
{
Severity = StatusMessageModel . StatusSeverity . Error ,
2021-10-29 08:25:43 +02:00
Html = $"To create an invoice, you need to <a href='{Url.Action(nameof(StoresController.PaymentMethods), " Stores ", new { storeId = store.Id })}' class='alert-link'>set up your wallet</a> first" ,
2021-09-13 07:24:10 +02:00
AllowDismiss = false
} ) ;
2018-04-05 08:44:27 +02:00
return View ( model ) ;
2018-03-23 08:24:57 +01:00
}
2019-05-03 20:49:58 +02:00
2017-12-03 14:36:04 +01:00
try
2017-10-27 10:53:04 +02:00
{
2020-08-25 07:33:00 +02:00
var result = await CreateInvoiceCore ( new BitpayCreateInvoiceRequest ( )
2017-12-03 14:36:04 +01:00
{
2021-08-03 10:03:00 +02:00
Price = model . Amount ,
2017-12-03 14:36:04 +01:00
Currency = model . Currency ,
PosData = model . PosData ,
OrderId = model . OrderId ,
//RedirectURL = redirect + "redirect",
NotificationURL = model . NotificationUrl ,
ItemDesc = model . ItemDesc ,
FullNotifications = true ,
BuyerEmail = model . BuyerEmail ,
2019-05-02 14:29:51 +02:00
SupportedTransactionCurrencies = model . SupportedTransactionCurrencies ? . ToDictionary ( s = > s , s = > new InvoiceSupportedTransactionCurrency ( )
{
2019-05-03 20:49:58 +02:00
Enabled = true
2021-08-23 06:55:06 +02:00
} ) ,
DefaultPaymentMethod = model . DefaultPaymentMethod ,
2021-10-10 08:54:25 +02:00
NotificationEmail = model . NotificationEmail ,
2021-10-27 16:32:56 +02:00
ExtendedNotifications = model . NotificationEmail ! = null ,
RequiresRefundEmail = model . RequiresRefundEmail = = RequiresRefundEmail . InheritFromStore
? store . GetStoreBlob ( ) . RequiresRefundEmail
: model . RequiresRefundEmail = = RequiresRefundEmail . On
2019-03-05 09:09:17 +01:00
} , store , HttpContext . Request . GetAbsoluteRoot ( ) , cancellationToken : cancellationToken ) ;
2017-10-12 17:25:45 +02:00
2019-10-31 04:29:59 +01:00
TempData [ WellKnownTempData . SuccessMessage ] = $"Invoice {result.Data.Id} just created!" ;
2021-10-20 16:17:40 +02:00
CreatedInvoiceId = result . Data . Id ;
2017-12-03 14:36:04 +01:00
return RedirectToAction ( nameof ( ListInvoices ) ) ;
}
2018-05-03 18:46:52 +02:00
catch ( BitpayHttpException ex )
2017-12-03 14:36:04 +01:00
{
2021-11-04 09:12:17 +01:00
this . TempData . SetStatusMessageModel ( new StatusMessageModel ( )
{
Severity = StatusMessageModel . StatusSeverity . Error ,
Message = ex . Message
} ) ;
2017-12-03 14:36:04 +01:00
return View ( model ) ;
}
2017-10-27 10:53:04 +02:00
}
2017-09-13 08:47:34 +02:00
2017-11-06 04:15:52 +01:00
[HttpPost]
2018-12-10 07:34:48 +01:00
[Route("invoices/{invoiceId}/changestate/{newState}")]
2019-10-12 13:35:30 +02:00
[Authorize(AuthenticationSchemes = AuthenticationSchemes.Cookie)]
2017-11-06 04:15:52 +01:00
[BitpayAPIConstraint(false)]
2019-05-03 21:54:40 +02:00
public async Task < IActionResult > ChangeInvoiceState ( string invoiceId , string newState )
2017-11-06 04:15:52 +01:00
{
2018-12-06 09:08:28 +01:00
var invoice = ( await _InvoiceRepository . GetInvoices ( new InvoiceQuery ( )
{
2020-06-28 10:55:27 +02:00
InvoiceId = new [ ] { invoiceId } ,
2018-12-06 09:08:28 +01:00
UserId = GetUserId ( )
} ) ) . FirstOrDefault ( ) ;
2019-05-03 21:54:40 +02:00
var model = new InvoiceStateChangeModel ( ) ;
2018-06-21 07:15:36 +02:00
if ( invoice = = null )
2019-05-03 21:54:40 +02:00
{
model . NotFound = true ;
return NotFound ( model ) ;
}
2018-12-10 07:34:48 +01:00
if ( newState = = "invalid" )
{
2020-07-24 09:40:37 +02:00
await _InvoiceRepository . MarkInvoiceStatus ( invoiceId , InvoiceStatus . Invalid ) ;
2019-05-03 21:54:40 +02:00
model . StatusString = new InvoiceState ( "invalid" , "marked" ) . ToString ( ) ;
2018-12-10 07:34:48 +01:00
}
2019-04-26 01:13:17 +02:00
else if ( newState = = "complete" )
2018-12-10 07:34:48 +01:00
{
2020-11-23 07:57:05 +01:00
await _InvoiceRepository . MarkInvoiceStatus ( invoiceId , InvoiceStatus . Settled ) ;
2019-05-03 21:54:40 +02:00
model . StatusString = new InvoiceState ( "complete" , "marked" ) . ToString ( ) ;
2018-12-10 07:34:48 +01:00
}
2019-05-03 21:54:40 +02:00
return Json ( model ) ;
}
public class InvoiceStateChangeModel
{
public bool NotFound { get ; set ; }
2021-07-29 13:29:34 +02:00
public string? StatusString { get ; set ; }
2017-11-06 04:15:52 +01:00
}
2017-10-27 10:53:04 +02:00
private string GetUserId ( )
{
return _UserManager . GetUserId ( User ) ;
}
2018-11-27 07:13:09 +01:00
public class PosDataParser
{
2019-01-26 05:26:49 +01:00
public static Dictionary < string , object > ParsePosData ( string posData )
2018-11-27 07:13:09 +01:00
{
2019-04-26 01:13:17 +02:00
var result = new Dictionary < string , object > ( ) ;
2018-11-27 07:13:09 +01:00
if ( string . IsNullOrEmpty ( posData ) )
{
return result ;
}
2019-04-26 01:13:17 +02:00
2018-11-27 07:13:09 +01:00
try
{
2019-04-26 01:13:17 +02:00
var jObject = JObject . Parse ( posData ) ;
2018-11-27 07:13:09 +01:00
foreach ( var item in jObject )
{
2021-07-29 13:29:34 +02:00
switch ( item . Value ? . Type )
2018-11-27 07:13:09 +01:00
{
case JTokenType . Array :
2019-01-26 05:26:49 +01:00
var items = item . Value . AsEnumerable ( ) . ToList ( ) ;
2020-01-12 07:32:26 +01:00
for ( var i = 0 ; i < items . Count ; i + + )
2019-01-26 05:26:49 +01:00
{
2021-04-28 09:49:10 +02:00
result . TryAdd ( $"{item.Key}[{i}]" , ParsePosData ( items [ i ] . ToString ( ) ) ) ;
2019-01-26 05:26:49 +01:00
}
break ;
case JTokenType . Object :
2021-04-28 09:49:10 +02:00
result . TryAdd ( item . Key , ParsePosData ( item . Value . ToString ( ) ) ) ;
2018-11-27 07:13:09 +01:00
break ;
2021-07-29 13:29:34 +02:00
case null :
break ;
2018-11-27 07:13:09 +01:00
default :
2021-04-28 09:49:10 +02:00
result . TryAdd ( item . Key , item . Value . ToString ( ) ) ;
2018-11-27 07:13:09 +01:00
break ;
}
2019-04-26 01:13:17 +02:00
2018-11-27 07:13:09 +01:00
}
}
catch
{
2021-04-28 09:49:10 +02:00
result . TryAdd ( string . Empty , posData ) ;
2018-11-27 07:13:09 +01:00
}
return result ;
}
}
2019-04-26 01:13:17 +02:00
2017-10-27 10:53:04 +02:00
}
2017-09-13 08:47:34 +02:00
}