2020-06-29 04:44:35 +02:00
using System ;
2017-09-13 08:47:34 +02:00
using System.Collections.Generic ;
2020-06-24 10:51:00 +02:00
using System.Configuration ;
2017-09-13 08:47:34 +02:00
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-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 ;
2017-12-17 11:58:55 +01:00
using BTCPayServer.Events ;
2018-08-30 18:34:39 +02:00
using BTCPayServer.Filters ;
2018-12-10 07:34:48 +01:00
using BTCPayServer.Models ;
2018-08-30 18:34:39 +02:00
using BTCPayServer.Models.InvoicingModels ;
2018-02-18 18:38:03 +01:00
using BTCPayServer.Payments ;
2018-10-24 07:52:19 +02:00
using BTCPayServer.Payments.Changelly ;
2018-12-11 12:47:38 +01:00
using BTCPayServer.Payments.CoinSwitch ;
2018-03-30 07:34:14 +02:00
using BTCPayServer.Payments.Lightning ;
2018-04-29 19:33:42 +02:00
using BTCPayServer.Security ;
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-06-24 10:51:00 +02:00
using DBriize.Utils ;
using Google.Cloud.Storage.V1 ;
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 ;
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-06-24 10:51:00 +02:00
using TwentyTwenty.Storage ;
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
{
[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
2019-04-30 05:45:33 +02:00
var prodInfo = invoice . ProductInformation ;
2017-10-27 10:53:04 +02:00
var store = await _StoreRepository . FindStore ( invoice . StoreId ) ;
2019-04-30 05:45:33 +02:00
var model = new InvoiceDetailsModel ( )
2017-10-27 10:53:04 +02:00
{
StoreName = store . StoreName ,
StoreLink = Url . Action ( nameof ( StoresController . UpdateStore ) , "Stores" , new { storeId = store . Id } ) ,
Id = invoice . Id ,
2018-12-10 13:48:28 +01:00
State = invoice . GetInvoiceState ( ) . 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 ,
2017-10-27 10:53:04 +02:00
OrderId = invoice . OrderId ,
BuyerInformation = invoice . BuyerInformation ,
2019-04-30 05:45:33 +02:00
Fiat = _CurrencyNameTable . DisplayFormatCurrency ( prodInfo . Price , prodInfo . Currency ) ,
TaxIncluded = _CurrencyNameTable . DisplayFormatCurrency ( prodInfo . TaxIncluded , prodInfo . 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 ,
2017-10-27 10:53:04 +02:00
ProductInformation = invoice . ProductInformation ,
2018-01-14 13:48:23 +01:00
StatusException = invoice . ExceptionStatus ,
2018-11-27 07:13:09 +01:00
Events = invoice . Events ,
2020-05-07 12:50:07 +02:00
PosData = PosDataParser . ParsePosData ( invoice . PosData ) ,
2020-06-24 10:51:00 +02:00
Archived = invoice . Archived ,
CanRefund = CanRefund ( invoice . GetInvoiceState ( ) ) ,
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 )
{
return invoiceState . Status = = InvoiceStatus . Confirmed | |
invoiceState . Status = = InvoiceStatus . Complete | |
2020-06-28 10:55:27 +02:00
( ( invoiceState . Status = = InvoiceStatus . Expired | | invoiceState . Status = = InvoiceStatus . Invalid ) & &
2020-06-24 10:51:00 +02:00
( invoiceState . ExceptionStatus = = InvoiceExceptionStatus . PaidLate | |
invoiceState . ExceptionStatus = = InvoiceExceptionStatus . PaidOver | |
invoiceState . ExceptionStatus = = InvoiceExceptionStatus . PaidPartial ) ) ;
}
[HttpGet]
[Route("invoices/{invoiceId}/refund")]
[AllowAnonymous]
public async Task < IActionResult > Refund ( string invoiceId , CancellationToken cancellationToken )
{
using var ctx = _dbContextFactory . CreateContext ( ) ;
ctx . ChangeTracker . QueryTrackingBehavior = Microsoft . EntityFrameworkCore . QueryTrackingBehavior . NoTracking ;
var invoice = await ctx . Invoices . Include ( i = > i . Payments )
. Include ( i = > i . CurrentRefund )
. Include ( i = > i . CurrentRefund . PullPaymentData )
. Where ( i = > i . Id = = invoiceId )
. FirstOrDefaultAsync ( ) ;
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 ( ) ;
var options = invoice . GetBlob ( _NetworkProvider ) . GetPaymentMethods ( )
. Select ( o = > o . GetId ( ) )
. Select ( o = > o . CryptoCode )
. Where ( o = > _NetworkProvider . GetNetwork < BTCPayNetwork > ( o ) is BTCPayNetwork n & & ! n . ReadonlyWallet )
. Distinct ( )
. OrderBy ( o = > o )
. Select ( o = > new PaymentMethodId ( o , PaymentTypes . BTCLike ) )
. ToList ( ) ;
var defaultRefund = invoice . Payments . Select ( p = > p . GetBlob ( _NetworkProvider ) )
. Select ( p = > p . GetPaymentMethodId ( ) . CryptoCode )
. FirstOrDefault ( ) ;
// TODO: What if no option?
var refund = new RefundModel ( ) ;
refund . Title = "Select a payment method" ;
refund . AvailablePaymentMethods = new SelectList ( options , nameof ( PaymentMethodId . CryptoCode ) , nameof ( PaymentMethodId . CryptoCode ) ) ;
refund . SelectedPaymentMethod = defaultRefund ? ? options . Select ( o = > o . CryptoCode ) . First ( ) ;
// 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 )
{
model . RefundStep = RefundSteps . SelectRate ;
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 ( ) ;
var paymentMethodId = new PaymentMethodId ( model . SelectedPaymentMethod , PaymentTypes . BTCLike ) ;
var cdCurrency = _CurrencyNameTable . GetCurrencyData ( invoice . ProductInformation . Currency , true ) ;
var paymentMethodDivisibility = _CurrencyNameTable . GetCurrencyData ( paymentMethodId . CryptoCode , false ) ? . Divisibility ? ? 8 ;
if ( model . SelectedRefundOption is null )
{
model . Title = "What to refund?" ;
var paymentMethod = invoice . GetPaymentMethods ( ) [ paymentMethodId ] ;
var paidCurrency = Math . Round ( paymentMethod . Calculate ( ) . Paid . ToDecimal ( MoneyUnit . BTC ) * paymentMethod . Rate , cdCurrency . Divisibility ) ;
model . CryptoAmountThen = Math . Round ( paidCurrency / paymentMethod . Rate , paymentMethodDivisibility ) ;
model . RateThenText = _CurrencyNameTable . DisplayFormatCurrency ( model . CryptoAmountThen , paymentMethodId . CryptoCode , true ) ;
var rules = store . GetStoreBlob ( ) . GetRateRules ( _NetworkProvider ) ;
var rateResult = await _RateProvider . FetchRate ( new Rating . CurrencyPair ( paymentMethodId . CryptoCode , invoice . ProductInformation . 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 = _CurrencyNameTable . DisplayFormatCurrency ( model . CryptoAmountNow , paymentMethodId . CryptoCode , true ) ;
model . FiatAmount = paidCurrency ;
model . FiatText = _CurrencyNameTable . DisplayFormatCurrency ( model . FiatAmount , invoice . ProductInformation . Currency , true ) ;
return View ( model ) ;
}
else
{
var createPullPayment = new HostedServices . CreatePullPayment ( ) ;
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" :
createPullPayment . Currency = invoice . ProductInformation . Currency ;
createPullPayment . Amount = model . FiatAmount ;
break ;
default :
ModelState . AddModelError ( nameof ( model . SelectedRefundOption ) , "Invalid choice" ) ;
return View ( model ) ;
}
var ppId = await _paymentHostedService . CreatePullPayment ( createPullPayment ) ;
this . TempData . SetStatusMessageModel ( new StatusMessageModel ( )
{
Html = "Share this page with a customer so they can claim a refund <br />Once claimed you need to initiate a refund from Wallet > Payouts" ,
Severity = StatusMessageModel . StatusSeverity . Success
} ) ;
( await ctx . Invoices . FindAsync ( invoice . Id ) ) . CurrentRefundId = ppId ;
ctx . Refunds . Add ( new RefundData ( )
{
InvoiceDataId = invoice . Id ,
PullPaymentDataId = ppId
} ) ;
await ctx . SaveChangesAsync ( ) ;
// TODO: Having dedicated UI later on
return RedirectToAction ( nameof ( PullPaymentController . ViewPullPayment ) ,
"PullPayment" ,
new { pullPaymentId = ppId } ) ;
}
}
2019-05-07 16:32:47 +02:00
private InvoiceDetailsModel InvoicePopulatePayments ( InvoiceEntity invoice )
2019-04-30 05:45:33 +02:00
{
var model = new InvoiceDetailsModel ( ) ;
2020-05-07 12:50:07 +02:00
model . Archived = invoice . Archived ;
2019-08-24 16:10:13 +02:00
model . Payments = invoice . GetPayments ( ) ;
2019-05-24 15:22:38 +02:00
foreach ( var data in invoice . GetPaymentMethods ( ) )
2018-01-08 12:06:16 +01:00
{
2018-02-18 18:38:03 +01:00
var accounting = data . Calculate ( ) ;
2018-03-06 22:37:25 +01:00
var paymentMethodId = data . GetId ( ) ;
2018-01-08 12:06:16 +01:00
var cryptoPayment = new InvoiceDetailsModel . CryptoPayment ( ) ;
2020-06-28 10:55:27 +02:00
2019-08-24 16:10:13 +02:00
cryptoPayment . PaymentMethodId = paymentMethodId ;
2019-06-03 18:06:03 +02:00
cryptoPayment . PaymentMethod = paymentMethodId . ToPrettyString ( ) ;
2019-01-30 11:18:44 +01:00
cryptoPayment . Due = _CurrencyNameTable . DisplayFormatCurrency ( accounting . Due . ToDecimal ( MoneyUnit . BTC ) , paymentMethodId . CryptoCode ) ;
cryptoPayment . Paid = _CurrencyNameTable . DisplayFormatCurrency ( accounting . CryptoPaid . ToDecimal ( MoneyUnit . BTC ) , paymentMethodId . CryptoCode ) ;
cryptoPayment . Overpaid = _CurrencyNameTable . DisplayFormatCurrency ( accounting . OverpaidHelper . ToDecimal ( MoneyUnit . BTC ) , paymentMethodId . CryptoCode ) ;
2019-05-30 09:02:52 +02:00
var paymentMethodDetails = data . GetPaymentMethodDetails ( ) ;
2020-02-28 23:30:57 +01:00
cryptoPayment . Address = paymentMethodDetails . GetPaymentDestination ( ) ;
2018-05-16 12:46:11 +02:00
cryptoPayment . Rate = ExchangeRate ( data ) ;
2018-01-08 12:06:16 +01:00
model . CryptoPayments . Add ( cryptoPayment ) ;
}
2019-04-30 05:45:33 +02:00
return model ;
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
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")]
2018-11-09 08:09:09 +01:00
public async Task < IActionResult > Checkout ( string invoiceId , string id = null , string paymentMethodId = null ,
2020-06-28 10:55:27 +02:00
[FromQuery] string view = null )
2017-10-27 10:53:04 +02:00
{
//Keep compatibility with Bitpay
invoiceId = invoiceId ? ? id ;
id = invoiceId ;
2019-03-31 20:46:38 +02:00
//
2017-10-13 09:07:57 +02:00
2019-01-31 11:07:38 +01:00
var model = await GetInvoiceModel ( invoiceId , paymentMethodId = = null ? null : PaymentMethodId . Parse ( paymentMethodId ) ) ;
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 ;
2018-07-12 10:38:21 +02:00
_CSP . Add ( new ConsentSecurityPolicy ( "script-src" , "'unsafe-eval'" ) ) ; // Needed by Vue
2018-07-14 05:35:34 +02:00
if ( ! string . IsNullOrEmpty ( model . CustomCSSLink ) & &
2018-07-12 10:38:21 +02:00
Uri . TryCreate ( model . CustomCSSLink , UriKind . Absolute , out var uri ) )
{
_CSP . Clear ( ) ;
}
if ( ! string . IsNullOrEmpty ( model . CustomLogoLink ) & &
Uri . TryCreate ( model . CustomLogoLink , UriKind . Absolute , out uri ) )
{
_CSP . Clear ( ) ;
}
2017-10-27 10:53:04 +02:00
return View ( nameof ( Checkout ) , model ) ;
}
2017-10-21 05:24:28 +02:00
2019-03-31 20:46:38 +02:00
[HttpGet]
[Route("invoice-noscript")]
public async Task < IActionResult > CheckoutNoScript ( string invoiceId , string id = null , string paymentMethodId = null )
{
//Keep compatibility with Bitpay
invoiceId = invoiceId ? ? id ;
id = invoiceId ;
//
var model = await GetInvoiceModel ( invoiceId , paymentMethodId = = null ? null : PaymentMethodId . Parse ( paymentMethodId ) ) ;
if ( model = = null )
return NotFound ( ) ;
2019-04-05 04:48:52 +02:00
return View ( model ) ;
2019-03-31 20:46:38 +02:00
}
2019-01-31 11:07:38 +01:00
private async Task < PaymentModel > GetInvoiceModel ( string invoiceId , PaymentMethodId paymentMethodId )
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 ;
if ( paymentMethodId = = null )
2018-02-18 18:38:03 +01:00
{
2019-01-31 11:07:38 +01:00
paymentMethodId = store . GetDefaultPaymentId ( _NetworkProvider ) ;
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 ) ;
2019-01-31 11:07:38 +01:00
if ( network = = null & & isDefaultPaymentId )
2018-05-07 05:25:50 +02:00
{
2019-05-29 16:33:31 +02:00
//TODO: need to look into a better way for this as it does not scale
2019-05-29 11:43:50 +02:00
network = _NetworkProvider . GetAll ( ) . OfType < BTCPayNetwork > ( ) . FirstOrDefault ( ) ;
2018-05-07 05:25:50 +02:00
paymentMethodId = new PaymentMethodId ( network . CryptoCode , PaymentTypes . BTCLike ) ;
}
2018-01-12 08:30:34 +01:00
if ( invoice = = null | | network = = null )
2017-10-27 10:53:04 +02:00
return null ;
2018-02-19 07:09:05 +01:00
if ( ! 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 ;
2019-05-24 15:22:38 +02:00
var paymentMethodTemp = invoice . GetPaymentMethods ( )
2018-10-12 03:09:13 +02:00
. Where ( c = > paymentMethodId . CryptoCode = = c . GetId ( ) . CryptoCode )
2018-08-08 07:45:46 +02:00
. FirstOrDefault ( ) ;
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 ( ) ;
2019-05-24 15:22:38 +02:00
var dto = invoice . EntityToDTO ( ) ;
2018-02-19 07:09:05 +01:00
var cryptoInfo = dto . CryptoInfo . First ( o = > o . GetpaymentMethodId ( ) = = paymentMethodId ) ;
2018-03-23 09:27:48 +01:00
var storeBlob = store . GetStoreBlob ( ) ;
2017-12-03 14:14:08 +01:00
var currency = invoice . ProductInformation . Currency ;
2018-02-19 07:09:05 +01:00
var accounting = paymentMethod . Calculate ( ) ;
2018-10-24 07:52:19 +02:00
ChangellySettings changelly = ( storeBlob . ChangellySettings ! = null & & storeBlob . ChangellySettings . Enabled & &
storeBlob . ChangellySettings . IsConfigured ( ) )
? storeBlob . ChangellySettings
: null ;
2019-04-26 01:13:17 +02:00
2018-12-11 12:47:38 +01:00
CoinSwitchSettings coinswitch = ( storeBlob . CoinSwitchSettings ! = null & & storeBlob . CoinSwitchSettings . Enabled & &
storeBlob . CoinSwitchSettings . IsConfigured ( ) )
? storeBlob . CoinSwitchSettings
: null ;
2018-10-24 07:52:19 +02:00
var changellyAmountDue = changelly ! = null
? ( accounting . Due . ToDecimal ( MoneyUnit . BTC ) *
( 1 m + ( changelly . AmountMarkupPercentage / 100 m ) ) )
: ( decimal? ) null ;
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 ;
2017-10-27 10:53:04 +02:00
var model = new PaymentModel ( )
{
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 ( ) ,
2017-10-27 10:53:04 +02:00
OrderId = invoice . OrderId ,
InvoiceId = invoice . Id ,
2018-11-09 08:48:38 +01:00
DefaultLang = 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 ) ,
OrderAmount = ( accounting . TotalDue - accounting . NetworkFee ) . ShowMoney ( divisibility ) ,
2018-05-16 12:46:11 +02:00
OrderAmountFiat = OrderAmountFromInvoice ( network . CryptoCode , invoice . ProductInformation ) ,
2017-10-27 10:53:04 +02:00
CustomerEmail = invoice . RefundMail ,
2018-04-26 04:13:44 +02:00
RequiresRefundEmail = storeBlob . RequiresRefundEmail ,
2019-10-08 06:06:12 +02:00
ShowRecommendedFee = storeBlob . ShowRecommendedFee ,
FeeRate = paymentMethodDetails . GetFeeRate ( ) ,
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 ,
2017-10-27 10:53:04 +02:00
ItemDesc = invoice . ProductInformation . 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-03-30 07:34:14 +02:00
PeerInfo = ( paymentMethodDetails as LightningLikePaymentMethodDetails ) ? . NodeInfo ,
2018-02-25 16:48:12 +01:00
TxCount = accounting . TxRequired ,
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 ( ) ,
2018-04-13 21:10:06 +02:00
IsMultiCurrency = invoice . GetPayments ( ) . Select ( p = > p . GetPaymentMethodId ( ) ) . Concat ( new [ ] { paymentMethod . GetId ( ) } ) . Distinct ( ) . Count ( ) > 1 ,
2018-10-24 07:52:19 +02:00
ChangellyEnabled = changelly ! = null ,
ChangellyMerchantId = changelly ? . ChangellyMerchantId ,
ChangellyAmountDue = changellyAmountDue ,
2018-12-11 12:47:38 +01:00
CoinSwitchEnabled = coinswitch ! = null ,
2019-04-26 01:13:17 +02:00
CoinSwitchAmountMarkupPercentage = coinswitch ? . AmountMarkupPercentage ? ? 0 ,
2018-12-11 12:47:38 +01:00
CoinSwitchMerchantId = coinswitch ? . MerchantId ,
2018-12-18 19:01:58 +01:00
CoinSwitchMode = coinswitch ? . Mode ,
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
} ;
2017-09-13 08:47:34 +02:00
2019-09-11 07:49:06 +02:00
paymentMethodHandler . PreparePaymentModel ( model , dto , storeBlob ) ;
2019-12-04 21:21:33 +01:00
if ( model . IsLightning & & storeBlob . LightningAmountInSatoshi & & model . CryptoCode = = "Sats" )
{
model . Rate = _CurrencyNameTable . DisplayFormatCurrency ( paymentMethod . Rate / 100_000_000 , paymentMethod . ParentEntity . ProductInformation . Currency ) ;
}
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
2018-05-16 12:46:11 +02:00
private string OrderAmountFromInvoice ( string cryptoCode , ProductInformation productInformation )
{
// if invoice source currency is the same as currently display currency, no need for "order amount from invoice"
if ( cryptoCode = = productInformation . Currency )
return null ;
2018-10-09 16:30:06 +02:00
return _CurrencyNameTable . DisplayFormatCurrency ( productInformation . Price , productInformation . Currency ) ;
2018-05-16 12:46:11 +02:00
}
private string ExchangeRate ( PaymentMethod paymentMethod )
2018-01-08 12:06:16 +01:00
{
2018-02-19 07:09:05 +01:00
string currency = paymentMethod . ParentEntity . ProductInformation . 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")]
2018-02-19 07:09:05 +01:00
[Route("i/{invoiceId}/{paymentMethodId}/status")]
2019-01-28 08:24:11 +01:00
[Route("invoice/{invoiceId}/status")]
[Route("invoice/{invoiceId}/{paymentMethodId}/status")]
[Route("invoice/status")]
2018-02-19 07:09:05 +01:00
public async Task < IActionResult > GetStatus ( string invoiceId , string paymentMethodId = null )
2017-10-27 10:53:04 +02:00
{
2019-01-31 11:07:38 +01:00
var model = await GetInvoiceModel ( invoiceId , paymentMethodId = = null ? null : PaymentMethodId . Parse ( paymentMethodId ) ) ;
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")]
2017-12-17 11:58:55 +01:00
public async Task < IActionResult > GetStatusWebSocket ( string invoiceId )
{
if ( ! HttpContext . WebSockets . IsWebSocketRequest )
return NotFound ( ) ;
2018-12-06 09:05:27 +01:00
var invoice = await _InvoiceRepository . GetInvoice ( invoiceId ) ;
2019-04-26 01:13:17 +02:00
if ( invoice = = null | | invoice . Status = = InvoiceStatus . Complete | | invoice . Status = = InvoiceStatus . Invalid | | invoice . Status = = InvoiceStatus . Expired )
2017-12-17 11:58:55 +01:00
return NotFound ( ) ;
var webSocket = await HttpContext . WebSockets . AcceptWebSocketAsync ( ) ;
CompositeDisposable leases = new CompositeDisposable ( ) ;
try
{
2018-01-07 13:07:06 +01:00
leases . Add ( _EventAggregator . Subscribe < Events . InvoiceDataChangedEvent > ( async o = > await NotifySocket ( webSocket , o . InvoiceId , invoiceId ) ) ) ;
2018-02-16 17:34:40 +01:00
leases . Add ( _EventAggregator . Subscribe < Events . InvoiceNewAddressEvent > ( async o = > await NotifySocket ( webSocket , o . InvoiceId , invoiceId ) ) ) ;
2018-06-21 07:15:36 +02:00
leases . Add ( _EventAggregator . Subscribe < Events . InvoiceEvent > ( async o = > await NotifySocket ( webSocket , o . Invoice . Id , invoiceId ) ) ) ;
2017-12-17 11:58:55 +01:00
while ( true )
{
var message = await webSocket . ReceiveAsync ( DummyBuffer , default ( CancellationToken ) ) ;
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
2017-12-17 11:58:55 +01:00
ArraySegment < Byte > DummyBuffer = new ArraySegment < Byte > ( new Byte [ 1 ] ) ;
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)]
2019-04-26 01:13:17 +02:00
public async Task < IActionResult > ListInvoices ( string searchTerm = null , int skip = 0 , int count = 50 , int timezoneOffset = 0 )
2017-10-27 10:53:04 +02:00
{
2019-09-12 05:54:26 +02:00
var fs = new SearchString ( searchTerm ) ;
var storeIds = fs . GetFilterArray ( "storeid" ) ! = null ? fs . GetFilterArray ( "storeid" ) : new List < string > ( ) . ToArray ( ) ;
2018-10-12 03:09:13 +02:00
var model = new InvoicesModel
2017-10-27 10:53:04 +02:00
{
2018-10-12 03:09:13 +02:00
SearchTerm = searchTerm ,
2017-10-27 10:53:04 +02:00
Skip = skip ,
2018-10-12 03:09:13 +02:00
Count = count ,
2019-09-12 05:54:26 +02:00
StoreIds = storeIds ,
2019-04-26 01:13:17 +02:00
TimezoneOffset = timezoneOffset
2018-10-12 03:09:13 +02:00
} ;
2019-04-26 01:13:17 +02:00
InvoiceQuery invoiceQuery = GetInvoiceQuery ( searchTerm , timezoneOffset ) ;
2019-01-17 15:40:47 +01:00
var counting = _InvoiceRepository . GetInvoicesTotal ( invoiceQuery ) ;
2019-01-16 21:33:04 +01:00
invoiceQuery . Count = count ;
invoiceQuery . Skip = skip ;
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 ( )
{
2019-05-01 22:33:46 +02:00
Status = invoice . Status ,
StatusString = state . ToString ( ) ,
2018-12-10 13:48:28 +01:00
ShowCheckout = invoice . Status = = InvoiceStatus . New ,
2018-05-26 16:32:20 +02:00
Date = invoice . InvoiceTime ,
2017-10-27 10:53:04 +02:00
InvoiceId = invoice . Id ,
2018-02-28 11:03:23 +01:00
OrderId = invoice . OrderId ? ? string . Empty ,
2019-09-04 11:20:36 +02:00
RedirectUrl = invoice . RedirectURL ? . AbsoluteUri ? ? string . Empty ,
2019-01-30 11:01:18 +01:00
AmountCurrency = _CurrencyNameTable . DisplayFormatCurrency ( invoice . ProductInformation . Price , invoice . ProductInformation . 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
2019-04-26 01:13:17 +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)]
2019-04-26 01:13:17 +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 ;
2019-04-26 01:13:17 +02:00
invoiceQuery . Count = 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)]
public async Task < IActionResult > CreateInvoice ( )
{
2018-04-29 19:33:42 +02:00
var stores = new SelectList ( await _StoreRepository . GetStoresByUserId ( GetUserId ( ) ) , nameof ( StoreData . Id ) , nameof ( StoreData . StoreName ) , null ) ;
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
{
2018-04-05 08:44:27 +02:00
ModelState . AddModelError ( nameof ( model . StoreId ) , "You need to configure the derivation scheme in order to create an invoice" ) ;
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
{
2019-02-21 10:40:27 +01:00
var result = await CreateInvoiceCore ( new CreateInvoiceRequest ( )
2017-12-03 14:36:04 +01:00
{
Price = model . Amount . Value ,
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
2019-05-02 14:29:51 +02:00
} )
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!" ;
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
{
2018-05-03 18:46:52 +02:00
ModelState . TryAddModelError ( nameof ( model . Currency ) , $"Error: {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" )
{
await _InvoiceRepository . UpdatePaidInvoiceToInvalid ( invoiceId ) ;
2019-02-19 04:06:13 +01:00
_EventAggregator . Publish ( new InvoiceEvent ( invoice , 1008 , InvoiceEvent . MarkedInvalid ) ) ;
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
{
await _InvoiceRepository . UpdatePaidInvoiceToComplete ( invoiceId ) ;
2019-02-19 04:06:13 +01:00
_EventAggregator . Publish ( new InvoiceEvent ( invoice , 2008 , InvoiceEvent . MarkedCompleted ) ) ;
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 ; }
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 )
{
2019-04-26 01:13:17 +02:00
2018-11-27 07:13:09 +01:00
switch ( item . Value . Type )
{
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
{
result . Add ( $"{item.Key}[{i}]" , ParsePosData ( items [ i ] . ToString ( ) ) ) ;
}
break ;
case JTokenType . Object :
result . Add ( item . Key , ParsePosData ( item . Value . ToString ( ) ) ) ;
2018-11-27 07:13:09 +01:00
break ;
default :
result . Add ( item . Key , item . Value . ToString ( ) ) ;
break ;
}
2019-04-26 01:13:17 +02:00
2018-11-27 07:13:09 +01:00
}
}
catch
{
result . Add ( string . Empty , posData ) ;
}
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
}