2017-09-13 08:47:34 +02:00
|
|
|
|
using BTCPayServer.Data;
|
|
|
|
|
using BTCPayServer.Filters;
|
|
|
|
|
using BTCPayServer.Models.InvoicingModels;
|
2017-10-20 21:06:37 +02:00
|
|
|
|
using BTCPayServer.Services.Invoices;
|
2017-09-13 16:50:36 +02:00
|
|
|
|
using Microsoft.AspNetCore.Authorization;
|
2017-10-13 10:18:32 +02:00
|
|
|
|
using Microsoft.AspNetCore.Cors;
|
2017-09-13 08:47:34 +02:00
|
|
|
|
using Microsoft.AspNetCore.Mvc;
|
2017-09-13 17:13:22 +02:00
|
|
|
|
using Microsoft.AspNetCore.Mvc.Rendering;
|
2017-09-13 08:47:34 +02:00
|
|
|
|
using NBitcoin;
|
|
|
|
|
using NBitpayClient;
|
|
|
|
|
using System;
|
|
|
|
|
using System.Collections.Generic;
|
|
|
|
|
using System.Globalization;
|
|
|
|
|
using System.Linq;
|
|
|
|
|
using System.Security.Claims;
|
|
|
|
|
using System.Text;
|
|
|
|
|
using System.Threading.Tasks;
|
2017-12-03 14:36:04 +01:00
|
|
|
|
using BTCPayServer.Services.Rates;
|
2017-12-17 11:58:55 +01:00
|
|
|
|
using System.Net.WebSockets;
|
|
|
|
|
using System.Threading;
|
|
|
|
|
using BTCPayServer.Events;
|
2018-01-07 13:48:00 +01:00
|
|
|
|
using NBXplorer;
|
2018-02-18 18:38:03 +01:00
|
|
|
|
using BTCPayServer.Payments;
|
2018-03-30 07:34:14 +02:00
|
|
|
|
using BTCPayServer.Payments.Lightning;
|
2018-04-29 19:33:42 +02:00
|
|
|
|
using BTCPayServer.Security;
|
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}")]
|
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()
|
|
|
|
|
{
|
|
|
|
|
UserId = GetUserId(),
|
2018-01-08 12:06:16 +01:00
|
|
|
|
InvoiceId = invoiceId,
|
2018-01-14 13:48:23 +01:00
|
|
|
|
IncludeAddresses = true,
|
|
|
|
|
IncludeEvents = true
|
2017-10-27 10:53:04 +02:00
|
|
|
|
})).FirstOrDefault();
|
|
|
|
|
if (invoice == null)
|
|
|
|
|
return NotFound();
|
2017-10-12 17:25:45 +02:00
|
|
|
|
|
2017-12-21 07:52:04 +01:00
|
|
|
|
var dto = invoice.EntityToDTO(_NetworkProvider);
|
2017-10-27 10:53:04 +02:00
|
|
|
|
var store = await _StoreRepository.FindStore(invoice.StoreId);
|
2017-12-21 10:01:26 +01:00
|
|
|
|
|
2017-10-27 10:53:04 +02:00
|
|
|
|
InvoiceDetailsModel model = new InvoiceDetailsModel()
|
|
|
|
|
{
|
|
|
|
|
StoreName = store.StoreName,
|
|
|
|
|
StoreLink = Url.Action(nameof(StoresController.UpdateStore), "Stores", new { storeId = store.Id }),
|
|
|
|
|
Id = invoice.Id,
|
|
|
|
|
Status = invoice.Status,
|
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,
|
2018-05-13 08:09:17 +02:00
|
|
|
|
Fiat = FormatCurrency((decimal)dto.Price, dto.Currency, _CurrencyNameTable),
|
2017-10-27 10:53:04 +02:00
|
|
|
|
NotificationUrl = invoice.NotificationURL,
|
2018-01-14 07:01:09 +01:00
|
|
|
|
RedirectUrl = invoice.RedirectURL,
|
2017-10-27 10:53:04 +02:00
|
|
|
|
ProductInformation = invoice.ProductInformation,
|
2018-01-14 13:48:23 +01:00
|
|
|
|
StatusException = invoice.ExceptionStatus,
|
|
|
|
|
Events = invoice.Events
|
2017-10-27 10:53:04 +02:00
|
|
|
|
};
|
2017-10-12 17:25:45 +02:00
|
|
|
|
|
2018-02-19 07:09:05 +01:00
|
|
|
|
foreach (var data in invoice.GetPaymentMethods(null))
|
2018-01-08 12:06:16 +01:00
|
|
|
|
{
|
2018-02-19 07:09:05 +01:00
|
|
|
|
var cryptoInfo = dto.CryptoInfo.First(o => o.GetpaymentMethodId() == data.GetId());
|
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();
|
2018-03-06 22:37:25 +01:00
|
|
|
|
cryptoPayment.PaymentMethod = ToString(paymentMethodId);
|
|
|
|
|
cryptoPayment.Due = accounting.Due.ToString() + $" {paymentMethodId.CryptoCode}";
|
|
|
|
|
cryptoPayment.Paid = accounting.CryptoPaid.ToString() + $" {paymentMethodId.CryptoCode}";
|
2018-05-03 19:01:43 +02:00
|
|
|
|
cryptoPayment.Overpaid = (accounting.DueUncapped > Money.Zero ? Money.Zero : -accounting.DueUncapped).ToString() + $" {paymentMethodId.CryptoCode}";
|
2018-02-18 18:38:03 +01:00
|
|
|
|
|
2018-02-19 07:09:05 +01:00
|
|
|
|
var onchainMethod = data.GetPaymentMethodDetails() as Payments.Bitcoin.BitcoinLikeOnChainPaymentMethod;
|
2018-03-19 01:45:54 +01:00
|
|
|
|
if (onchainMethod != null)
|
|
|
|
|
{
|
2018-03-17 09:49:42 +01:00
|
|
|
|
cryptoPayment.Address = onchainMethod.DepositAddress;
|
2018-02-18 18:38:03 +01:00
|
|
|
|
}
|
2018-05-16 12:46:11 +02:00
|
|
|
|
cryptoPayment.Rate = ExchangeRate(data);
|
2018-01-08 12:06:16 +01:00
|
|
|
|
cryptoPayment.PaymentUrl = cryptoInfo.PaymentUrls.BIP21;
|
|
|
|
|
model.CryptoPayments.Add(cryptoPayment);
|
|
|
|
|
}
|
|
|
|
|
|
2018-04-18 15:27:01 +02:00
|
|
|
|
var onChainPayments = invoice
|
2018-01-10 10:30:45 +01:00
|
|
|
|
.GetPayments()
|
2018-04-18 15:27:01 +02:00
|
|
|
|
.Select<PaymentEntity, Task<object>>(async payment =>
|
2017-10-27 10:53:04 +02:00
|
|
|
|
{
|
2018-01-08 12:06:16 +01:00
|
|
|
|
var paymentNetwork = _NetworkProvider.GetNetwork(payment.GetCryptoCode());
|
2018-04-18 15:27:01 +02:00
|
|
|
|
var paymentData = payment.GetCryptoPaymentData();
|
|
|
|
|
if (paymentData is Payments.Bitcoin.BitcoinLikePaymentData onChainPaymentData)
|
2018-02-17 18:19:35 +01:00
|
|
|
|
{
|
2018-04-18 15:27:01 +02:00
|
|
|
|
var m = new InvoiceDetailsModel.Payment();
|
|
|
|
|
m.Crypto = payment.GetPaymentMethodId().CryptoCode;
|
|
|
|
|
m.DepositAddress = onChainPaymentData.Output.ScriptPubKey.GetDestinationAddress(paymentNetwork.NBitcoinNetwork);
|
|
|
|
|
|
|
|
|
|
int confirmationCount = 0;
|
|
|
|
|
if ((onChainPaymentData.ConfirmationCount < paymentNetwork.MaxTrackedConfirmation && payment.Accounted)
|
|
|
|
|
&& (onChainPaymentData.Legacy || invoice.MonitoringExpiration < DateTimeOffset.UtcNow)) // The confirmation count in the paymentData is not up to date
|
|
|
|
|
{
|
|
|
|
|
confirmationCount = (await ((ExplorerClientProvider)_ServiceProvider.GetService(typeof(ExplorerClientProvider))).GetExplorerClient(payment.GetCryptoCode())?.GetTransactionAsync(onChainPaymentData.Outpoint.Hash))?.Confirmations ?? 0;
|
|
|
|
|
onChainPaymentData.ConfirmationCount = confirmationCount;
|
|
|
|
|
payment.SetCryptoPaymentData(onChainPaymentData);
|
|
|
|
|
await _InvoiceRepository.UpdatePayments(new List<PaymentEntity> { payment });
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
confirmationCount = onChainPaymentData.ConfirmationCount;
|
|
|
|
|
}
|
|
|
|
|
if (confirmationCount >= paymentNetwork.MaxTrackedConfirmation)
|
|
|
|
|
{
|
|
|
|
|
m.Confirmations = "At least " + (paymentNetwork.MaxTrackedConfirmation);
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
m.Confirmations = confirmationCount.ToString(CultureInfo.InvariantCulture);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
m.TransactionId = onChainPaymentData.Outpoint.Hash.ToString();
|
|
|
|
|
m.ReceivedTime = payment.ReceivedTime;
|
|
|
|
|
m.TransactionLink = string.Format(CultureInfo.InvariantCulture, paymentNetwork.BlockExplorerLink, m.TransactionId);
|
|
|
|
|
m.Replaced = !payment.Accounted;
|
|
|
|
|
return m;
|
2018-02-17 18:19:35 +01:00
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
2018-04-18 15:27:01 +02:00
|
|
|
|
var lightningPaymentData = (Payments.Lightning.LightningLikePaymentData)paymentData;
|
|
|
|
|
return new InvoiceDetailsModel.OffChainPayment()
|
|
|
|
|
{
|
|
|
|
|
Crypto = paymentNetwork.CryptoCode,
|
|
|
|
|
BOLT11 = lightningPaymentData.BOLT11
|
|
|
|
|
};
|
2018-02-17 18:19:35 +01:00
|
|
|
|
}
|
2017-10-27 10:53:04 +02:00
|
|
|
|
})
|
|
|
|
|
.ToArray();
|
2018-04-18 15:27:01 +02:00
|
|
|
|
await Task.WhenAll(onChainPayments);
|
2018-03-19 01:45:54 +01:00
|
|
|
|
model.Addresses = invoice.HistoricalAddresses.Select(h => new InvoiceDetailsModel.AddressModel
|
2018-03-06 22:37:25 +01:00
|
|
|
|
{
|
|
|
|
|
Destination = h.GetAddress(),
|
|
|
|
|
PaymentMethod = ToString(h.GetPaymentMethodId()),
|
|
|
|
|
Current = !h.UnAssigned.HasValue
|
|
|
|
|
}).ToArray();
|
2018-04-18 15:27:01 +02:00
|
|
|
|
model.OnChainPayments = onChainPayments.Select(p => p.GetAwaiter().GetResult()).OfType<InvoiceDetailsModel.Payment>().ToList();
|
|
|
|
|
model.OffChainPayments = onChainPayments.Select(p => p.GetAwaiter().GetResult()).OfType<InvoiceDetailsModel.OffChainPayment>().ToList();
|
2017-10-27 10:53:04 +02:00
|
|
|
|
model.StatusMessage = StatusMessage;
|
|
|
|
|
return View(model);
|
|
|
|
|
}
|
2017-09-13 08:47:34 +02:00
|
|
|
|
|
2018-03-06 22:37:25 +01:00
|
|
|
|
private string ToString(PaymentMethodId paymentMethodId)
|
|
|
|
|
{
|
|
|
|
|
var type = paymentMethodId.PaymentType.ToString();
|
|
|
|
|
switch (paymentMethodId.PaymentType)
|
|
|
|
|
{
|
|
|
|
|
case PaymentTypes.BTCLike:
|
|
|
|
|
type = "On-Chain";
|
|
|
|
|
break;
|
|
|
|
|
case PaymentTypes.LightningLike:
|
|
|
|
|
type = "Off-Chain";
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
return $"{paymentMethodId.CryptoCode} ({type})";
|
|
|
|
|
}
|
|
|
|
|
|
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-02-19 07:09:05 +01:00
|
|
|
|
public async Task<IActionResult> Checkout(string invoiceId, string id = null, string paymentMethodId = null)
|
2017-10-27 10:53:04 +02:00
|
|
|
|
{
|
|
|
|
|
//Keep compatibility with Bitpay
|
|
|
|
|
invoiceId = invoiceId ?? id;
|
|
|
|
|
id = invoiceId;
|
|
|
|
|
////
|
2017-10-13 09:07:57 +02:00
|
|
|
|
|
2018-02-19 07:09:05 +01:00
|
|
|
|
var model = await GetInvoiceModel(invoiceId, paymentMethodId);
|
2017-10-27 10:53:04 +02:00
|
|
|
|
if (model == null)
|
|
|
|
|
return NotFound();
|
2017-10-21 05:24:28 +02:00
|
|
|
|
|
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
|
|
|
|
|
2018-02-19 07:09:05 +01:00
|
|
|
|
private async Task<PaymentModel> GetInvoiceModel(string invoiceId, string paymentMethodIdStr)
|
2017-10-27 10:53:04 +02:00
|
|
|
|
{
|
|
|
|
|
var invoice = await _InvoiceRepository.GetInvoice(null, 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);
|
2018-01-12 08:30:34 +01:00
|
|
|
|
bool isDefaultCrypto = false;
|
2018-02-19 07:09:05 +01:00
|
|
|
|
if (paymentMethodIdStr == null)
|
2018-02-18 18:38:03 +01:00
|
|
|
|
{
|
2018-02-19 07:09:05 +01:00
|
|
|
|
paymentMethodIdStr = store.GetDefaultCrypto();
|
2018-01-12 08:30:34 +01:00
|
|
|
|
isDefaultCrypto = true;
|
|
|
|
|
}
|
2018-02-18 18:38:03 +01:00
|
|
|
|
|
2018-02-19 07:09:05 +01:00
|
|
|
|
var paymentMethodId = PaymentMethodId.Parse(paymentMethodIdStr);
|
|
|
|
|
var network = _NetworkProvider.GetNetwork(paymentMethodId.CryptoCode);
|
2018-05-07 05:25:50 +02:00
|
|
|
|
if (network == null && isDefaultCrypto)
|
|
|
|
|
{
|
|
|
|
|
network = _NetworkProvider.GetAll().FirstOrDefault();
|
|
|
|
|
paymentMethodId = new PaymentMethodId(network.CryptoCode, PaymentTypes.BTCLike);
|
|
|
|
|
paymentMethodIdStr = paymentMethodId.ToString();
|
|
|
|
|
}
|
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
|
|
|
|
{
|
2018-03-19 01:45:54 +01:00
|
|
|
|
if (!isDefaultCrypto)
|
2018-01-12 08:30:34 +01:00
|
|
|
|
return null;
|
2018-02-19 07:09:05 +01:00
|
|
|
|
var paymentMethodTemp = invoice.GetPaymentMethods(_NetworkProvider).First();
|
|
|
|
|
network = paymentMethodTemp.Network;
|
|
|
|
|
paymentMethodId = paymentMethodTemp.GetId();
|
2018-05-07 05:25:50 +02:00
|
|
|
|
paymentMethodIdStr = paymentMethodId.ToString();
|
2018-01-12 08:30:34 +01:00
|
|
|
|
}
|
2018-01-09 03:41:07 +01:00
|
|
|
|
|
2018-02-19 07:09:05 +01:00
|
|
|
|
var paymentMethod = invoice.GetPaymentMethod(paymentMethodId, _NetworkProvider);
|
|
|
|
|
var paymentMethodDetails = paymentMethod.GetPaymentMethodDetails();
|
2017-12-21 07:52:04 +01:00
|
|
|
|
var dto = invoice.EntityToDTO(_NetworkProvider);
|
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();
|
2017-10-27 10:53:04 +02:00
|
|
|
|
var model = new PaymentModel()
|
|
|
|
|
{
|
2018-01-09 03:41:07 +01:00
|
|
|
|
CryptoCode = network.CryptoCode,
|
2018-02-19 07:13:45 +01:00
|
|
|
|
PaymentMethodId = paymentMethodId.ToString(),
|
2018-07-13 22:58:59 +02:00
|
|
|
|
PaymentMethodName = GetPaymentMethodName(paymentMethodId.ToString()),
|
|
|
|
|
CryptoImage = "/" + GetImage(paymentMethodId, network),
|
2018-03-24 15:43:02 +01:00
|
|
|
|
IsLightning = paymentMethodId.PaymentType == PaymentTypes.LightningLike,
|
2017-10-27 10:53:04 +02:00
|
|
|
|
ServerUrl = HttpContext.Request.GetAbsoluteRoot(),
|
|
|
|
|
OrderId = invoice.OrderId,
|
|
|
|
|
InvoiceId = invoice.Id,
|
2018-03-23 17:58:11 +01:00
|
|
|
|
DefaultLang = storeBlob.DefaultLang ?? "en-US",
|
2018-05-03 23:51:04 +02:00
|
|
|
|
HtmlTitle = storeBlob.HtmlTitle ?? "BTCPay Invoice",
|
2018-03-27 08:14:50 +02:00
|
|
|
|
CustomCSSLink = storeBlob.CustomCSS?.AbsoluteUri,
|
|
|
|
|
CustomLogoLink = storeBlob.CustomLogo?.AbsoluteUri,
|
2018-02-19 07:09:05 +01:00
|
|
|
|
BtcAddress = paymentMethodDetails.GetPaymentDestination(),
|
2017-12-21 10:01:26 +01:00
|
|
|
|
BtcDue = accounting.Due.ToString(),
|
2018-05-11 19:15:26 +02:00
|
|
|
|
OrderAmount = (accounting.TotalDue - accounting.NetworkFee).ToString(),
|
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,
|
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),
|
2017-10-27 10:53:04 +02:00
|
|
|
|
MerchantRefLink = invoice.RedirectURL ?? "/",
|
|
|
|
|
StoreName = store.StoreName,
|
2018-02-25 16:48:12 +01:00
|
|
|
|
InvoiceBitcoinUrl = paymentMethodId.PaymentType == PaymentTypes.BTCLike ? cryptoInfo.PaymentUrls.BIP21 :
|
|
|
|
|
paymentMethodId.PaymentType == PaymentTypes.LightningLike ? cryptoInfo.PaymentUrls.BOLT11 :
|
|
|
|
|
throw new NotSupportedException(),
|
2018-03-30 07:34:14 +02:00
|
|
|
|
PeerInfo = (paymentMethodDetails as LightningLikePaymentMethodDetails)?.NodeInfo,
|
2018-02-25 16:48:12 +01:00
|
|
|
|
InvoiceBitcoinUrlQR = paymentMethodId.PaymentType == PaymentTypes.BTCLike ? cryptoInfo.PaymentUrls.BIP21 :
|
|
|
|
|
paymentMethodId.PaymentType == PaymentTypes.LightningLike ? cryptoInfo.PaymentUrls.BOLT11.ToUpperInvariant() :
|
|
|
|
|
throw new NotSupportedException(),
|
|
|
|
|
TxCount = accounting.TxRequired,
|
2017-12-21 10:01:26 +01:00
|
|
|
|
BtcPaid = accounting.Paid.ToString(),
|
2018-01-09 03:41:07 +01:00
|
|
|
|
Status = invoice.Status,
|
2018-04-13 21:10:06 +02:00
|
|
|
|
NetworkFee = paymentMethodDetails.GetTxFee(),
|
|
|
|
|
IsMultiCurrency = invoice.GetPayments().Select(p => p.GetPaymentMethodId()).Concat(new[] { paymentMethod.GetId() }).Distinct().Count() > 1,
|
2018-03-23 09:27:48 +01:00
|
|
|
|
AllowCoinConversion = storeBlob.AllowCoinConversion,
|
2018-02-19 07:09:05 +01:00
|
|
|
|
AvailableCryptos = invoice.GetPaymentMethods(_NetworkProvider)
|
2018-02-18 18:38:03 +01:00
|
|
|
|
.Where(i => i.Network != null)
|
2018-03-19 01:45:54 +01:00
|
|
|
|
.Select(kv => new PaymentModel.AvailableCrypto()
|
|
|
|
|
{
|
|
|
|
|
PaymentMethodId = kv.GetId().ToString(),
|
2018-07-13 22:58:59 +02:00
|
|
|
|
PaymentMethodName = GetPaymentMethodName(kv.GetId().ToString()),
|
2018-03-19 01:45:54 +01:00
|
|
|
|
CryptoImage = "/" + GetImage(kv.GetId(), kv.Network),
|
|
|
|
|
Link = Url.Action(nameof(Checkout), new { invoiceId = invoiceId, paymentMethodId = kv.GetId().ToString() })
|
|
|
|
|
}).Where(c => c.CryptoImage != "/")
|
2018-07-14 05:35:34 +02:00
|
|
|
|
.OrderBy(a => a.PaymentMethodName)
|
|
|
|
|
.ToList()
|
2017-10-27 10:53:04 +02:00
|
|
|
|
};
|
2017-09-13 08:47:34 +02:00
|
|
|
|
|
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-07-14 05:35:34 +02:00
|
|
|
|
private static readonly Dictionary<string, string> CURRENCY_NAMES = new Dictionary<string, string>
|
2018-07-13 22:58:59 +02:00
|
|
|
|
{
|
|
|
|
|
{"BTC", "Bitcoin (BTC)" },
|
|
|
|
|
{"BTC_LightningLike", "Bitcoin (BTC) Lightning" },
|
|
|
|
|
{"LTC", "Litecoin (LTC)" },
|
|
|
|
|
{"LTC_LightningLike", "Litecoin (LTC) Lightning" },
|
|
|
|
|
{"DOGE", "Dogecoin (DOGE)" },
|
|
|
|
|
};
|
|
|
|
|
private string GetPaymentMethodName(string key)
|
|
|
|
|
{
|
|
|
|
|
return CURRENCY_NAMES[key];
|
|
|
|
|
}
|
|
|
|
|
|
2018-02-25 16:48:12 +01:00
|
|
|
|
private string GetImage(PaymentMethodId paymentMethodId, BTCPayNetwork network)
|
|
|
|
|
{
|
|
|
|
|
return (paymentMethodId.PaymentType == PaymentTypes.BTCLike ? Url.Content(network.CryptoImagePath) : Url.Content(network.LightningImagePath));
|
|
|
|
|
}
|
|
|
|
|
|
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;
|
|
|
|
|
|
|
|
|
|
return FormatCurrency(productInformation.Price, productInformation.Currency, _CurrencyNameTable);
|
|
|
|
|
}
|
|
|
|
|
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-05-13 08:09:17 +02:00
|
|
|
|
return FormatCurrency(paymentMethod.Rate, currency, _CurrencyNameTable);
|
2018-01-08 12:06:16 +01:00
|
|
|
|
}
|
2018-05-16 12:46:11 +02:00
|
|
|
|
|
2018-05-13 08:09:17 +02:00
|
|
|
|
public static string FormatCurrency(decimal price, string currency, CurrencyNameTable currencies)
|
2018-01-08 12:06:16 +01:00
|
|
|
|
{
|
2018-05-20 16:37:18 +02:00
|
|
|
|
var provider = currencies.GetNumberFormatInfo(currency, true);
|
|
|
|
|
var currencyData = currencies.GetCurrencyData(currency, true);
|
2018-05-13 08:09:17 +02:00
|
|
|
|
var divisibility = currencyData.Divisibility;
|
|
|
|
|
while (true)
|
|
|
|
|
{
|
|
|
|
|
var rounded = decimal.Round(price, divisibility, MidpointRounding.AwayFromZero);
|
|
|
|
|
if ((Math.Abs(rounded - price) / price) < 0.001m)
|
|
|
|
|
{
|
|
|
|
|
price = rounded;
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
divisibility++;
|
|
|
|
|
}
|
2018-05-16 03:19:48 +02:00
|
|
|
|
if (divisibility != provider.CurrencyDecimalDigits)
|
2018-05-13 08:09:17 +02:00
|
|
|
|
{
|
|
|
|
|
provider = (NumberFormatInfo)provider.Clone();
|
|
|
|
|
provider.CurrencyDecimalDigits = divisibility;
|
|
|
|
|
}
|
2018-07-14 05:35:34 +02:00
|
|
|
|
|
2018-05-16 14:19:48 +02:00
|
|
|
|
if (currencyData.Crypto)
|
2018-05-16 12:46:11 +02:00
|
|
|
|
return price.ToString("C", provider);
|
|
|
|
|
else
|
|
|
|
|
return price.ToString("C", provider) + $" ({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")]
|
|
|
|
|
public async Task<IActionResult> GetStatus(string invoiceId, string paymentMethodId = null)
|
2017-10-27 10:53:04 +02:00
|
|
|
|
{
|
2018-02-19 07:09:05 +01:00
|
|
|
|
var model = await GetInvoiceModel(invoiceId, 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")]
|
|
|
|
|
public async Task<IActionResult> GetStatusWebSocket(string invoiceId)
|
|
|
|
|
{
|
|
|
|
|
if (!HttpContext.WebSockets.IsWebSocketRequest)
|
|
|
|
|
return NotFound();
|
|
|
|
|
var invoice = await _InvoiceRepository.GetInvoice(null, invoiceId);
|
|
|
|
|
if (invoice == null || invoice.Status == "complete" || invoice.Status == "invalid" || invoice.Status == "expired")
|
|
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
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;
|
|
|
|
|
CancellationTokenSource cts = new CancellationTokenSource();
|
|
|
|
|
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")]
|
|
|
|
|
public async Task<IActionResult> UpdateCustomer(string invoiceId, [FromBody]UpdateCustomerModel data)
|
|
|
|
|
{
|
|
|
|
|
if (!ModelState.IsValid)
|
|
|
|
|
{
|
|
|
|
|
return BadRequest(ModelState);
|
|
|
|
|
}
|
|
|
|
|
await _InvoiceRepository.UpdateInvoice(invoiceId, data).ConfigureAwait(false);
|
|
|
|
|
return Ok();
|
|
|
|
|
}
|
2017-09-13 08:47:34 +02:00
|
|
|
|
|
2017-10-27 10:53:04 +02:00
|
|
|
|
[HttpGet]
|
|
|
|
|
[Route("invoices")]
|
2018-04-30 15:00:43 +02:00
|
|
|
|
[Authorize(AuthenticationSchemes = Policies.CookieAuthentication)]
|
2017-10-27 10:53:04 +02:00
|
|
|
|
[BitpayAPIConstraint(false)]
|
2018-04-09 10:53:43 +02:00
|
|
|
|
public async Task<IActionResult> ListInvoices(string searchTerm = null, int skip = 0, int count = 50)
|
2017-10-27 10:53:04 +02:00
|
|
|
|
{
|
|
|
|
|
var model = new InvoicesModel();
|
2017-12-03 15:35:52 +01:00
|
|
|
|
var filterString = new SearchString(searchTerm);
|
2017-10-27 10:53:04 +02:00
|
|
|
|
foreach (var invoice in await _InvoiceRepository.GetInvoices(new InvoiceQuery()
|
|
|
|
|
{
|
2017-12-03 15:35:52 +01:00
|
|
|
|
TextSearch = filterString.TextSearch,
|
2017-10-27 10:53:04 +02:00
|
|
|
|
Count = count,
|
|
|
|
|
Skip = skip,
|
2017-12-03 15:35:52 +01:00
|
|
|
|
UserId = GetUserId(),
|
2018-05-06 06:16:39 +02:00
|
|
|
|
Unusual = !filterString.Filters.ContainsKey("unusual") ? null
|
|
|
|
|
: !bool.TryParse(filterString.Filters["unusual"].First(), out var r) ? (bool?)null
|
|
|
|
|
: r,
|
2018-04-26 04:01:59 +02:00
|
|
|
|
Status = filterString.Filters.ContainsKey("status") ? filterString.Filters["status"].ToArray() : null,
|
2018-05-05 16:25:09 +02:00
|
|
|
|
ExceptionStatus = filterString.Filters.ContainsKey("exceptionstatus") ? filterString.Filters["exceptionstatus"].ToArray() : null,
|
2018-04-26 04:01:59 +02:00
|
|
|
|
StoreId = filterString.Filters.ContainsKey("storeid") ? filterString.Filters["storeid"].ToArray() : null
|
2017-10-27 10:53:04 +02:00
|
|
|
|
}))
|
|
|
|
|
{
|
|
|
|
|
model.SearchTerm = searchTerm;
|
|
|
|
|
model.Invoices.Add(new InvoiceModel()
|
|
|
|
|
{
|
2018-05-05 16:25:09 +02:00
|
|
|
|
Status = invoice.Status + (invoice.ExceptionStatus == null ? string.Empty : $" ({invoice.ExceptionStatus})"),
|
|
|
|
|
ShowCheckout = invoice.Status == "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,
|
2018-03-13 01:13:16 +01:00
|
|
|
|
RedirectUrl = invoice.RedirectURL ?? string.Empty,
|
2017-10-27 10:53:04 +02:00
|
|
|
|
AmountCurrency = $"{invoice.ProductInformation.Price.ToString(CultureInfo.InvariantCulture)} {invoice.ProductInformation.Currency}"
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
model.Skip = skip;
|
|
|
|
|
model.Count = count;
|
|
|
|
|
model.StatusMessage = StatusMessage;
|
|
|
|
|
return View(model);
|
|
|
|
|
}
|
2017-09-13 08:47:34 +02:00
|
|
|
|
|
2017-10-27 10:53:04 +02:00
|
|
|
|
[HttpGet]
|
|
|
|
|
[Route("invoices/create")]
|
2018-04-30 15:00:43 +02:00
|
|
|
|
[Authorize(AuthenticationSchemes = Policies.CookieAuthentication)]
|
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);
|
2017-10-27 10:53:04 +02:00
|
|
|
|
if (stores.Count() == 0)
|
|
|
|
|
{
|
|
|
|
|
StatusMessage = "Error: 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
|
|
|
|
}
|
|
|
|
|
return View(new CreateInvoiceModel() { Stores = stores });
|
|
|
|
|
}
|
2017-09-13 08:47:34 +02:00
|
|
|
|
|
2017-10-27 10:53:04 +02:00
|
|
|
|
[HttpPost]
|
|
|
|
|
[Route("invoices/create")]
|
2018-04-30 15:00:43 +02:00
|
|
|
|
[Authorize(AuthenticationSchemes = Policies.CookieAuthentication)]
|
2017-10-27 10:53:04 +02:00
|
|
|
|
[BitpayAPIConstraint(false)]
|
|
|
|
|
public async Task<IActionResult> CreateInvoice(CreateInvoiceModel model)
|
|
|
|
|
{
|
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);
|
|
|
|
|
var store = stores.FirstOrDefault(s => s.Id == model.StoreId);
|
2018-05-13 08:09:17 +02:00
|
|
|
|
if (store == null)
|
2018-04-29 19:33:42 +02:00
|
|
|
|
{
|
|
|
|
|
ModelState.AddModelError(nameof(model.StoreId), "Store not found");
|
|
|
|
|
}
|
2017-10-27 10:53:04 +02:00
|
|
|
|
if (!ModelState.IsValid)
|
|
|
|
|
{
|
|
|
|
|
return View(model);
|
|
|
|
|
}
|
2018-03-23 08:24:57 +01:00
|
|
|
|
StatusMessage = null;
|
2018-04-29 19:33:42 +02:00
|
|
|
|
if (!store.HasClaim(Policies.CanModifyStoreSettings.Key))
|
2018-03-23 08:24:57 +01:00
|
|
|
|
{
|
2018-04-05 08:44:27 +02:00
|
|
|
|
ModelState.AddModelError(nameof(model.StoreId), "You need to be owner of this store to create an invoice");
|
|
|
|
|
return View(model);
|
2018-03-23 08:24:57 +01:00
|
|
|
|
}
|
2018-04-05 08:44:27 +02:00
|
|
|
|
|
2018-02-20 04:45:04 +01:00
|
|
|
|
if (store.GetSupportedPaymentMethods(_NetworkProvider).Count() == 0)
|
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
|
|
|
|
}
|
|
|
|
|
|
2018-04-18 15:27:01 +02:00
|
|
|
|
if (StatusMessage != null)
|
2018-03-23 08:24:57 +01:00
|
|
|
|
{
|
2017-10-27 10:53:04 +02:00
|
|
|
|
return RedirectToAction(nameof(StoresController.UpdateStore), "Stores", new
|
|
|
|
|
{
|
|
|
|
|
storeId = store.Id
|
|
|
|
|
});
|
|
|
|
|
}
|
2017-12-03 14:36:04 +01:00
|
|
|
|
|
|
|
|
|
try
|
2017-10-27 10:53:04 +02:00
|
|
|
|
{
|
2017-12-03 14:36:04 +01:00
|
|
|
|
var result = await CreateInvoiceCore(new Invoice()
|
|
|
|
|
{
|
|
|
|
|
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,
|
|
|
|
|
}, store, HttpContext.Request.GetAbsoluteRoot());
|
2017-10-12 17:25:45 +02:00
|
|
|
|
|
2017-12-03 14:36:04 +01:00
|
|
|
|
StatusMessage = $"Invoice {result.Data.Id} just created!";
|
|
|
|
|
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-10-27 10:53:04 +02:00
|
|
|
|
[HttpPost]
|
2018-04-30 15:00:43 +02:00
|
|
|
|
[Authorize(AuthenticationSchemes = Policies.CookieAuthentication)]
|
2017-10-27 10:53:04 +02:00
|
|
|
|
[BitpayAPIConstraint(false)]
|
|
|
|
|
public IActionResult SearchInvoice(InvoicesModel invoices)
|
|
|
|
|
{
|
|
|
|
|
return RedirectToAction(nameof(ListInvoices), new
|
|
|
|
|
{
|
|
|
|
|
searchTerm = invoices.SearchTerm,
|
|
|
|
|
skip = invoices.Skip,
|
|
|
|
|
count = invoices.Count,
|
|
|
|
|
});
|
|
|
|
|
}
|
2017-09-13 08:47:34 +02:00
|
|
|
|
|
2017-11-06 04:15:52 +01:00
|
|
|
|
[HttpPost]
|
|
|
|
|
[Route("invoices/invalidatepaid")]
|
2018-04-30 15:00:43 +02:00
|
|
|
|
[Authorize(AuthenticationSchemes = Policies.CookieAuthentication)]
|
2017-11-06 04:15:52 +01:00
|
|
|
|
[BitpayAPIConstraint(false)]
|
|
|
|
|
public async Task<IActionResult> InvalidatePaidInvoice(string invoiceId)
|
|
|
|
|
{
|
2018-06-21 07:15:36 +02:00
|
|
|
|
var invoice = await _InvoiceRepository.GetInvoice(null, invoiceId);
|
|
|
|
|
if (invoice == null)
|
|
|
|
|
return NotFound();
|
2017-11-06 04:15:52 +01:00
|
|
|
|
await _InvoiceRepository.UpdatePaidInvoiceToInvalid(invoiceId);
|
2018-06-21 07:15:36 +02:00
|
|
|
|
_EventAggregator.Publish(new InvoiceEvent(invoice.EntityToDTO(_NetworkProvider), 1008, "invoice_markedInvalid"));
|
2017-11-06 04:15:52 +01:00
|
|
|
|
return RedirectToAction(nameof(ListInvoices));
|
|
|
|
|
}
|
|
|
|
|
|
2017-10-27 10:53:04 +02:00
|
|
|
|
[TempData]
|
|
|
|
|
public string StatusMessage
|
|
|
|
|
{
|
|
|
|
|
get;
|
|
|
|
|
set;
|
|
|
|
|
}
|
2017-09-13 08:47:34 +02:00
|
|
|
|
|
2017-10-27 10:53:04 +02:00
|
|
|
|
private string GetUserId()
|
|
|
|
|
{
|
|
|
|
|
return _UserManager.GetUserId(User);
|
|
|
|
|
}
|
|
|
|
|
}
|
2017-09-13 08:47:34 +02:00
|
|
|
|
}
|