mirror of
https://github.com/btcpayserver/btcpayserver.git
synced 2025-02-23 14:40:36 +01:00
Merge branch 'payment-requests'
This commit is contained in:
commit
7a1b1b7e5e
36 changed files with 2856 additions and 17 deletions
|
@ -177,6 +177,7 @@ namespace BTCPayServer.Tests
|
||||||
var user = tester.NewAccount();
|
var user = tester.NewAccount();
|
||||||
user.GrantAccess();
|
user.GrantAccess();
|
||||||
user.RegisterDerivationScheme("BTC");
|
user.RegisterDerivationScheme("BTC");
|
||||||
|
user.ModifyStore(s => s.NetworkFeeMode = NetworkFeeMode.Never);
|
||||||
var apps = user.GetController<AppsController>();
|
var apps = user.GetController<AppsController>();
|
||||||
var vm = Assert.IsType<CreateAppViewModel>(Assert.IsType<ViewResult>(apps.CreateApp().Result).Model);
|
var vm = Assert.IsType<CreateAppViewModel>(Assert.IsType<ViewResult>(apps.CreateApp().Result).Model);
|
||||||
vm.Name = "test";
|
vm.Name = "test";
|
||||||
|
|
157
BTCPayServer.Tests/PaymentRequestTests.cs
Normal file
157
BTCPayServer.Tests/PaymentRequestTests.cs
Normal file
|
@ -0,0 +1,157 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Net.Http;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using BTCPayServer.Controllers;
|
||||||
|
using BTCPayServer.Data;
|
||||||
|
using BTCPayServer.Events;
|
||||||
|
using BTCPayServer.Models;
|
||||||
|
using BTCPayServer.Models.AppViewModels;
|
||||||
|
using BTCPayServer.Models.PaymentRequestViewModels;
|
||||||
|
using BTCPayServer.Models.StoreViewModels;
|
||||||
|
using BTCPayServer.Payments.Changelly;
|
||||||
|
using BTCPayServer.Payments.Changelly.Models;
|
||||||
|
using BTCPayServer.Services.Apps;
|
||||||
|
using BTCPayServer.Services.Invoices;
|
||||||
|
using BTCPayServer.Services.Rates;
|
||||||
|
using BTCPayServer.Services.Stores;
|
||||||
|
using BTCPayServer.Tests.Logging;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using NBitcoin;
|
||||||
|
using NBitpayClient;
|
||||||
|
using Xunit;
|
||||||
|
using Xunit.Abstractions;
|
||||||
|
|
||||||
|
namespace BTCPayServer.Tests
|
||||||
|
{
|
||||||
|
public class PaymentRequestTests
|
||||||
|
{
|
||||||
|
public PaymentRequestTests(ITestOutputHelper helper)
|
||||||
|
{
|
||||||
|
Logs.Tester = new XUnitLog(helper) {Name = "Tests"};
|
||||||
|
Logs.LogProvider = new XUnitLogProvider(helper);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
[Trait("Integration", "Integration")]
|
||||||
|
public void CanCreateViewUpdateAndDeletePaymentRequest()
|
||||||
|
{
|
||||||
|
using (var tester = ServerTester.Create())
|
||||||
|
{
|
||||||
|
tester.Start();
|
||||||
|
var user = tester.NewAccount();
|
||||||
|
user.GrantAccess();
|
||||||
|
user.RegisterDerivationScheme("BTC");
|
||||||
|
|
||||||
|
var user2 = tester.NewAccount();
|
||||||
|
user2.GrantAccess();
|
||||||
|
|
||||||
|
var paymentRequestController = user.GetController<PaymentRequestController>();
|
||||||
|
var guestpaymentRequestController = user2.GetController<PaymentRequestController>();
|
||||||
|
|
||||||
|
var request = new UpdatePaymentRequestViewModel()
|
||||||
|
{
|
||||||
|
Title = "original juice",
|
||||||
|
Currency = "BTC",
|
||||||
|
Amount = 1,
|
||||||
|
StoreId = user.StoreId,
|
||||||
|
Description = "description"
|
||||||
|
};
|
||||||
|
var id = (Assert
|
||||||
|
.IsType<RedirectToActionResult>(paymentRequestController.EditPaymentRequest(null, request).Result).RouteValues.Values.First().ToString());
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
//permission guard for guests editing
|
||||||
|
Assert
|
||||||
|
.IsType<NotFoundResult>(guestpaymentRequestController.EditPaymentRequest(id).Result);
|
||||||
|
|
||||||
|
request.Title = "update";
|
||||||
|
Assert.IsType<RedirectToActionResult>(paymentRequestController.EditPaymentRequest(id, request).Result);
|
||||||
|
|
||||||
|
Assert.Equal(request.Title, Assert.IsType<ViewPaymentRequestViewModel>( Assert.IsType<ViewResult>(paymentRequestController.ViewPaymentRequest(id).Result).Model).Title);
|
||||||
|
|
||||||
|
Assert.False(string.IsNullOrEmpty(id));
|
||||||
|
|
||||||
|
Assert.IsType<ViewPaymentRequestViewModel>(Assert
|
||||||
|
.IsType<ViewResult>(paymentRequestController.ViewPaymentRequest(id).Result).Model);
|
||||||
|
|
||||||
|
//Delete
|
||||||
|
|
||||||
|
Assert.IsType<ConfirmModel>(Assert
|
||||||
|
.IsType<ViewResult>(paymentRequestController.RemovePaymentRequestPrompt(id).Result).Model);
|
||||||
|
|
||||||
|
|
||||||
|
Assert.IsType<RedirectToActionResult>(paymentRequestController.RemovePaymentRequest(id).Result);
|
||||||
|
|
||||||
|
Assert
|
||||||
|
.IsType<NotFoundResult>(paymentRequestController.ViewPaymentRequest(id).Result);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
[Trait("Integration", "Integration")]
|
||||||
|
public async Task CanPayPaymentRequestWhenPossible()
|
||||||
|
{
|
||||||
|
using (var tester = ServerTester.Create())
|
||||||
|
{
|
||||||
|
tester.Start();
|
||||||
|
var user = tester.NewAccount();
|
||||||
|
user.GrantAccess();
|
||||||
|
user.RegisterDerivationScheme("BTC");
|
||||||
|
|
||||||
|
var paymentRequestController = user.GetController<PaymentRequestController>();
|
||||||
|
|
||||||
|
Assert.IsType<NotFoundResult>(await paymentRequestController.PayPaymentRequest(Guid.NewGuid().ToString()));
|
||||||
|
|
||||||
|
|
||||||
|
var request = new UpdatePaymentRequestViewModel()
|
||||||
|
{
|
||||||
|
Title = "original juice",
|
||||||
|
Currency = "BTC",
|
||||||
|
Amount = 1,
|
||||||
|
StoreId = user.StoreId,
|
||||||
|
Description = "description"
|
||||||
|
};
|
||||||
|
var response = Assert
|
||||||
|
.IsType<RedirectToActionResult>(paymentRequestController.EditPaymentRequest(null, request).Result)
|
||||||
|
.RouteValues.First();
|
||||||
|
|
||||||
|
var invoiceId = Assert
|
||||||
|
.IsType<OkObjectResult>(await paymentRequestController.PayPaymentRequest(response.Value.ToString(), false)).Value
|
||||||
|
.ToString();
|
||||||
|
|
||||||
|
var actionResult = Assert
|
||||||
|
.IsType<RedirectToActionResult>(await paymentRequestController.PayPaymentRequest(response.Value.ToString()));
|
||||||
|
|
||||||
|
Assert.Equal("Checkout", actionResult.ActionName);
|
||||||
|
Assert.Equal("Invoice", actionResult.ControllerName);
|
||||||
|
Assert.Contains(actionResult.RouteValues, pair => pair.Key == "Id" && pair.Value.ToString() == invoiceId);
|
||||||
|
|
||||||
|
var invoice = user.BitPay.GetInvoice(invoiceId, Facade.Merchant);
|
||||||
|
Assert.Equal(1, invoice.Price);
|
||||||
|
|
||||||
|
request = new UpdatePaymentRequestViewModel()
|
||||||
|
{
|
||||||
|
Title = "original juice with expiry",
|
||||||
|
Currency = "BTC",
|
||||||
|
Amount = 1,
|
||||||
|
ExpiryDate = DateTime.Today.Subtract( TimeSpan.FromDays(2)),
|
||||||
|
StoreId = user.StoreId,
|
||||||
|
Description = "description"
|
||||||
|
};
|
||||||
|
|
||||||
|
response = Assert
|
||||||
|
.IsType<RedirectToActionResult>(paymentRequestController.EditPaymentRequest(null, request).Result)
|
||||||
|
.RouteValues.First();
|
||||||
|
|
||||||
|
Assert
|
||||||
|
.IsType<BadRequestObjectResult>(await paymentRequestController.PayPaymentRequest(response.Value.ToString(), false));
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
319
BTCPayServer/Controllers/PaymentRequestController.cs
Normal file
319
BTCPayServer/Controllers/PaymentRequestController.cs
Normal file
|
@ -0,0 +1,319 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Globalization;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Security.Claims;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using BTCPayServer.Data;
|
||||||
|
using BTCPayServer.Filters;
|
||||||
|
using BTCPayServer.Models;
|
||||||
|
using BTCPayServer.Models.PaymentRequestViewModels;
|
||||||
|
using BTCPayServer.PaymentRequest;
|
||||||
|
using BTCPayServer.Payments;
|
||||||
|
using BTCPayServer.Payments.Lightning;
|
||||||
|
using BTCPayServer.Rating;
|
||||||
|
using BTCPayServer.Security;
|
||||||
|
using BTCPayServer.Services.Invoices;
|
||||||
|
using BTCPayServer.Services.PaymentRequests;
|
||||||
|
using BTCPayServer.Services.Rates;
|
||||||
|
using BTCPayServer.Services.Stores;
|
||||||
|
using Ganss.XSS;
|
||||||
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
using Microsoft.AspNetCore.Http.Extensions;
|
||||||
|
using Microsoft.AspNetCore.Identity;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Microsoft.AspNetCore.Mvc.Rendering;
|
||||||
|
using NBitpayClient;
|
||||||
|
|
||||||
|
namespace BTCPayServer.Controllers
|
||||||
|
{
|
||||||
|
[Route("payment-requests")]
|
||||||
|
[Authorize(AuthenticationSchemes = Policies.CookieAuthentication)]
|
||||||
|
public class PaymentRequestController : Controller
|
||||||
|
{
|
||||||
|
private readonly InvoiceController _InvoiceController;
|
||||||
|
private readonly UserManager<ApplicationUser> _UserManager;
|
||||||
|
private readonly StoreRepository _StoreRepository;
|
||||||
|
private readonly PaymentRequestRepository _PaymentRequestRepository;
|
||||||
|
private readonly PaymentRequestService _PaymentRequestService;
|
||||||
|
private readonly EventAggregator _EventAggregator;
|
||||||
|
private readonly CurrencyNameTable _Currencies;
|
||||||
|
private readonly HtmlSanitizer _htmlSanitizer;
|
||||||
|
|
||||||
|
public PaymentRequestController(
|
||||||
|
InvoiceController invoiceController,
|
||||||
|
UserManager<ApplicationUser> userManager,
|
||||||
|
StoreRepository storeRepository,
|
||||||
|
PaymentRequestRepository paymentRequestRepository,
|
||||||
|
PaymentRequestService paymentRequestService,
|
||||||
|
EventAggregator eventAggregator,
|
||||||
|
CurrencyNameTable currencies,
|
||||||
|
HtmlSanitizer htmlSanitizer)
|
||||||
|
{
|
||||||
|
_InvoiceController = invoiceController;
|
||||||
|
_UserManager = userManager;
|
||||||
|
_StoreRepository = storeRepository;
|
||||||
|
_PaymentRequestRepository = paymentRequestRepository;
|
||||||
|
_PaymentRequestService = paymentRequestService;
|
||||||
|
_EventAggregator = eventAggregator;
|
||||||
|
_Currencies = currencies;
|
||||||
|
_htmlSanitizer = htmlSanitizer;
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet]
|
||||||
|
[Route("")]
|
||||||
|
[BitpayAPIConstraint(false)]
|
||||||
|
public async Task<IActionResult> GetPaymentRequests(int skip = 0, int count = 50, string statusMessage = null)
|
||||||
|
{
|
||||||
|
var result = await _PaymentRequestRepository.FindPaymentRequests(new PaymentRequestQuery()
|
||||||
|
{
|
||||||
|
UserId = GetUserId(), Skip = skip, Count = count
|
||||||
|
});
|
||||||
|
return View(new ListPaymentRequestsViewModel()
|
||||||
|
{
|
||||||
|
Skip = skip,
|
||||||
|
StatusMessage = statusMessage,
|
||||||
|
Count = count,
|
||||||
|
Total = result.Total,
|
||||||
|
Items = result.Items.Select(data => new ViewPaymentRequestViewModel(data)).ToList()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet]
|
||||||
|
[Route("edit/{id?}")]
|
||||||
|
public async Task<IActionResult> EditPaymentRequest(string id, string statusMessage = null)
|
||||||
|
{
|
||||||
|
SelectList stores = null;
|
||||||
|
var data = await _PaymentRequestRepository.FindPaymentRequest(id, GetUserId());
|
||||||
|
if (data == null && !string.IsNullOrEmpty(id))
|
||||||
|
{
|
||||||
|
return NotFound();
|
||||||
|
}
|
||||||
|
|
||||||
|
stores = new SelectList(await _StoreRepository.GetStoresByUserId(GetUserId()), nameof(StoreData.Id),
|
||||||
|
nameof(StoreData.StoreName), data?.StoreDataId);
|
||||||
|
if (!stores.Any())
|
||||||
|
{
|
||||||
|
return RedirectToAction("GetPaymentRequests",
|
||||||
|
new
|
||||||
|
{
|
||||||
|
StatusMessage = "Error: You need to create at least one store before creating a payment request"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return View(new UpdatePaymentRequestViewModel(data)
|
||||||
|
{
|
||||||
|
Stores = stores,
|
||||||
|
StatusMessage = statusMessage
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost]
|
||||||
|
[Route("edit/{id?}")]
|
||||||
|
public async Task<IActionResult> EditPaymentRequest(string id, UpdatePaymentRequestViewModel viewModel)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(viewModel.Currency) ||
|
||||||
|
_Currencies.GetCurrencyData(viewModel.Currency, false) == null)
|
||||||
|
ModelState.AddModelError(nameof(viewModel.Currency), "Invalid currency");
|
||||||
|
|
||||||
|
var data = await _PaymentRequestRepository.FindPaymentRequest(id, GetUserId());
|
||||||
|
if (data == null && !string.IsNullOrEmpty(id))
|
||||||
|
{
|
||||||
|
return NotFound();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!ModelState.IsValid)
|
||||||
|
{
|
||||||
|
viewModel.Stores = new SelectList(await _StoreRepository.GetStoresByUserId(GetUserId()),
|
||||||
|
nameof(StoreData.Id),
|
||||||
|
nameof(StoreData.StoreName), data?.StoreDataId);
|
||||||
|
|
||||||
|
return View(viewModel);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data == null)
|
||||||
|
{
|
||||||
|
data = new PaymentRequestData();
|
||||||
|
}
|
||||||
|
|
||||||
|
data.StoreDataId = viewModel.StoreId;
|
||||||
|
var blob = data.GetBlob();
|
||||||
|
|
||||||
|
blob.Title = viewModel.Title;
|
||||||
|
blob.Email = viewModel.Email;
|
||||||
|
blob.Description = _htmlSanitizer.Sanitize(viewModel.Description);
|
||||||
|
blob.Amount = viewModel.Amount;
|
||||||
|
blob.ExpiryDate = viewModel.ExpiryDate;
|
||||||
|
blob.Currency = viewModel.Currency;
|
||||||
|
blob.EmbeddedCSS = viewModel.EmbeddedCSS;
|
||||||
|
blob.CustomCSSLink = viewModel.CustomCSSLink;
|
||||||
|
blob.AllowCustomPaymentAmounts = viewModel.AllowCustomPaymentAmounts;
|
||||||
|
|
||||||
|
data.SetBlob(blob);
|
||||||
|
data = await _PaymentRequestRepository.CreateOrUpdatePaymentRequest(data);
|
||||||
|
_EventAggregator.Publish(new PaymentRequestUpdated()
|
||||||
|
{
|
||||||
|
Data = data,
|
||||||
|
PaymentRequestId = data.Id
|
||||||
|
});
|
||||||
|
|
||||||
|
return RedirectToAction("EditPaymentRequest", new {id = data.Id, StatusMessage = "Saved"});
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet]
|
||||||
|
[Route("{id}/remove")]
|
||||||
|
[BitpayAPIConstraint(false)]
|
||||||
|
public async Task<IActionResult> RemovePaymentRequestPrompt(string id)
|
||||||
|
{
|
||||||
|
var data = await _PaymentRequestRepository.FindPaymentRequest(id, GetUserId());
|
||||||
|
if (data == null)
|
||||||
|
{
|
||||||
|
return NotFound();
|
||||||
|
}
|
||||||
|
|
||||||
|
var blob = data.GetBlob();
|
||||||
|
return View("Confirm", new ConfirmModel()
|
||||||
|
{
|
||||||
|
Title = $"Remove Payment Request",
|
||||||
|
Description = $"Are you sure to remove access to remove payment request '{blob.Title}' ?",
|
||||||
|
Action = "Delete"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost]
|
||||||
|
[Route("{id}/remove")]
|
||||||
|
[BitpayAPIConstraint(false)]
|
||||||
|
public async Task<IActionResult> RemovePaymentRequest(string id)
|
||||||
|
{
|
||||||
|
var result = await _PaymentRequestRepository.RemovePaymentRequest(id, GetUserId());
|
||||||
|
if (result)
|
||||||
|
{
|
||||||
|
return RedirectToAction("GetPaymentRequests",
|
||||||
|
new {StatusMessage = "Payment request successfully removed"});
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return RedirectToAction("GetPaymentRequests",
|
||||||
|
new
|
||||||
|
{
|
||||||
|
StatusMessage =
|
||||||
|
"Payment request could not be removed. Any request that has generated invoices cannot be removed."
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet]
|
||||||
|
[Route("{id}")]
|
||||||
|
[AllowAnonymous]
|
||||||
|
public async Task<IActionResult> ViewPaymentRequest(string id)
|
||||||
|
{
|
||||||
|
var result = await _PaymentRequestService.GetPaymentRequest(id, GetUserId());
|
||||||
|
if (result == null)
|
||||||
|
{
|
||||||
|
return NotFound();
|
||||||
|
}
|
||||||
|
|
||||||
|
return View(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet]
|
||||||
|
[Route("{id}/pay")]
|
||||||
|
[AllowAnonymous]
|
||||||
|
public async Task<IActionResult> PayPaymentRequest(string id, bool redirectToInvoice = true,
|
||||||
|
decimal? amount = null)
|
||||||
|
{
|
||||||
|
var result = ((await ViewPaymentRequest(id)) as ViewResult)?.Model as ViewPaymentRequestViewModel;
|
||||||
|
if (result == null)
|
||||||
|
{
|
||||||
|
return NotFound();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result.AmountDue <= 0)
|
||||||
|
{
|
||||||
|
if (redirectToInvoice)
|
||||||
|
{
|
||||||
|
return RedirectToAction("ViewPaymentRequest", new {Id = id});
|
||||||
|
}
|
||||||
|
|
||||||
|
return BadRequest("Payment Request has already been settled.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result.ExpiryDate.HasValue && DateTime.Now >= result.ExpiryDate)
|
||||||
|
{
|
||||||
|
if (redirectToInvoice)
|
||||||
|
{
|
||||||
|
return RedirectToAction("ViewPaymentRequest", new {Id = id});
|
||||||
|
}
|
||||||
|
|
||||||
|
return BadRequest("Payment Request has expired");
|
||||||
|
}
|
||||||
|
|
||||||
|
var statusesAllowedToDisplay = new List<InvoiceStatus>()
|
||||||
|
{
|
||||||
|
InvoiceStatus.New
|
||||||
|
};
|
||||||
|
var validInvoice = result.Invoices.FirstOrDefault(invoice =>
|
||||||
|
Enum.TryParse<InvoiceStatus>(invoice.Status, true, out var status) &&
|
||||||
|
statusesAllowedToDisplay.Contains(status));
|
||||||
|
|
||||||
|
if (validInvoice != null)
|
||||||
|
{
|
||||||
|
if (redirectToInvoice)
|
||||||
|
{
|
||||||
|
return RedirectToAction("Checkout", "Invoice", new {Id = validInvoice.Id});
|
||||||
|
}
|
||||||
|
|
||||||
|
return Ok(validInvoice.Id);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result.AllowCustomPaymentAmounts && amount != null)
|
||||||
|
{
|
||||||
|
var invoiceAmount = result.AmountDue < amount ? result.AmountDue : amount;
|
||||||
|
|
||||||
|
return await CreateInvoiceForPaymentRequest(id, redirectToInvoice, result, invoiceAmount);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
return await CreateInvoiceForPaymentRequest(id, redirectToInvoice, result);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<IActionResult> CreateInvoiceForPaymentRequest(string id,
|
||||||
|
bool redirectToInvoice,
|
||||||
|
ViewPaymentRequestViewModel result,
|
||||||
|
decimal? amount = null)
|
||||||
|
{
|
||||||
|
var pr = await _PaymentRequestRepository.FindPaymentRequest(id, null);
|
||||||
|
var blob = pr.GetBlob();
|
||||||
|
var store = pr.StoreData;
|
||||||
|
store.AdditionalClaims.Add(new Claim(Policies.CanCreateInvoice.Key, store.Id));
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var newInvoiceId = (await _InvoiceController.CreateInvoiceCore(new CreateInvoiceRequest()
|
||||||
|
{
|
||||||
|
OrderId = $"{PaymentRequestRepository.GetOrderIdForPaymentRequest(id)}",
|
||||||
|
Currency = blob.Currency,
|
||||||
|
Price = amount.GetValueOrDefault(result.AmountDue),
|
||||||
|
FullNotifications = true,
|
||||||
|
BuyerEmail = result.Email,
|
||||||
|
RedirectURL = Request.GetDisplayUrl().Replace("/pay", "", StringComparison.InvariantCulture),
|
||||||
|
}, store, HttpContext.Request.GetAbsoluteRoot(), new List<string>() { PaymentRequestRepository.GetInternalTag(id) })).Data.Id;
|
||||||
|
|
||||||
|
if (redirectToInvoice)
|
||||||
|
{
|
||||||
|
return RedirectToAction("Checkout", "Invoice", new {Id = newInvoiceId});
|
||||||
|
}
|
||||||
|
|
||||||
|
return Ok(newInvoiceId);
|
||||||
|
}
|
||||||
|
catch (BitpayHttpException e)
|
||||||
|
{
|
||||||
|
return BadRequest(e.Message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private string GetUserId()
|
||||||
|
{
|
||||||
|
return _UserManager.GetUserId(User);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -5,6 +5,7 @@ using System.Threading.Tasks;
|
||||||
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
|
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using BTCPayServer.Models;
|
using BTCPayServer.Models;
|
||||||
|
using BTCPayServer.Services.PaymentRequests;
|
||||||
using Microsoft.EntityFrameworkCore.Infrastructure.Internal;
|
using Microsoft.EntityFrameworkCore.Infrastructure.Internal;
|
||||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
|
|
||||||
|
@ -55,6 +56,11 @@ namespace BTCPayServer.Data
|
||||||
get; set;
|
get; set;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public DbSet<PaymentRequestData> PaymentRequests
|
||||||
|
{
|
||||||
|
get; set;
|
||||||
|
}
|
||||||
|
|
||||||
public DbSet<StoreData> Stores
|
public DbSet<StoreData> Stores
|
||||||
{
|
{
|
||||||
get; set;
|
get; set;
|
||||||
|
@ -204,6 +210,15 @@ namespace BTCPayServer.Data
|
||||||
o.UniqueId
|
o.UniqueId
|
||||||
#pragma warning restore CS0618
|
#pragma warning restore CS0618
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
builder.Entity<PaymentRequestData>()
|
||||||
|
.HasOne(o => o.StoreData)
|
||||||
|
.WithMany(i => i.PaymentRequests)
|
||||||
|
.OnDelete(DeleteBehavior.Cascade);
|
||||||
|
|
||||||
|
builder.Entity<PaymentRequestData>()
|
||||||
|
.HasIndex(o => o.Status);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,6 +21,7 @@ using BTCPayServer.Payments.Changelly;
|
||||||
using BTCPayServer.Payments.CoinSwitch;
|
using BTCPayServer.Payments.CoinSwitch;
|
||||||
using BTCPayServer.Security;
|
using BTCPayServer.Security;
|
||||||
using BTCPayServer.Rating;
|
using BTCPayServer.Rating;
|
||||||
|
using BTCPayServer.Services.PaymentRequests;
|
||||||
using BTCPayServer.Services.Mails;
|
using BTCPayServer.Services.Mails;
|
||||||
|
|
||||||
namespace BTCPayServer.Data
|
namespace BTCPayServer.Data
|
||||||
|
@ -42,6 +43,11 @@ namespace BTCPayServer.Data
|
||||||
get; set;
|
get; set;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public List<PaymentRequestData> PaymentRequests
|
||||||
|
{
|
||||||
|
get; set;
|
||||||
|
}
|
||||||
|
|
||||||
public List<InvoiceData> Invoices { get; set; }
|
public List<InvoiceData> Invoices { get; set; }
|
||||||
|
|
||||||
[Obsolete("Use GetDerivationStrategies instead")]
|
[Obsolete("Use GetDerivationStrategies instead")]
|
||||||
|
|
|
@ -60,7 +60,7 @@ namespace BTCPayServer.HostedServices
|
||||||
_Subscriptions.Add(_EventAggregator.Subscribe<T>(e => _Events.Writer.TryWrite(e)));
|
_Subscriptions.Add(_EventAggregator.Subscribe<T>(e => _Events.Writer.TryWrite(e)));
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task StartAsync(CancellationToken cancellationToken)
|
public virtual Task StartAsync(CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
_Subscriptions = new List<IEventAggregatorSubscription>();
|
_Subscriptions = new List<IEventAggregatorSubscription>();
|
||||||
SubscibeToEvents();
|
SubscibeToEvents();
|
||||||
|
@ -70,7 +70,7 @@ namespace BTCPayServer.HostedServices
|
||||||
}
|
}
|
||||||
Task _ProcessingEvents = Task.CompletedTask;
|
Task _ProcessingEvents = Task.CompletedTask;
|
||||||
|
|
||||||
public async Task StopAsync(CancellationToken cancellationToken)
|
public virtual async Task StopAsync(CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
_Subscriptions?.ForEach(subscription => subscription.Dispose());
|
_Subscriptions?.ForEach(subscription => subscription.Dispose());
|
||||||
_Cts?.Cancel();
|
_Cts?.Cancel();
|
||||||
|
|
|
@ -38,9 +38,11 @@ using BTCPayServer.Logging;
|
||||||
using BTCPayServer.HostedServices;
|
using BTCPayServer.HostedServices;
|
||||||
using Meziantou.AspNetCore.BundleTagHelpers;
|
using Meziantou.AspNetCore.BundleTagHelpers;
|
||||||
using System.Security.Claims;
|
using System.Security.Claims;
|
||||||
|
using BTCPayServer.PaymentRequest;
|
||||||
using BTCPayServer.Payments.Changelly;
|
using BTCPayServer.Payments.Changelly;
|
||||||
using BTCPayServer.Payments.Lightning;
|
using BTCPayServer.Payments.Lightning;
|
||||||
using BTCPayServer.Security;
|
using BTCPayServer.Security;
|
||||||
|
using BTCPayServer.Services.PaymentRequests;
|
||||||
using Microsoft.AspNetCore.Mvc.ModelBinding;
|
using Microsoft.AspNetCore.Mvc.ModelBinding;
|
||||||
using NBXplorer.DerivationStrategy;
|
using NBXplorer.DerivationStrategy;
|
||||||
using NicolasDorier.RateLimits;
|
using NicolasDorier.RateLimits;
|
||||||
|
@ -74,6 +76,7 @@ namespace BTCPayServer.Hosting
|
||||||
services.AddSingleton<BTCPayServerEnvironment>();
|
services.AddSingleton<BTCPayServerEnvironment>();
|
||||||
services.TryAddSingleton<TokenRepository>();
|
services.TryAddSingleton<TokenRepository>();
|
||||||
services.TryAddSingleton<EventAggregator>();
|
services.TryAddSingleton<EventAggregator>();
|
||||||
|
services.TryAddSingleton<PaymentRequestService>();
|
||||||
services.TryAddSingleton<CoinAverageSettings>();
|
services.TryAddSingleton<CoinAverageSettings>();
|
||||||
services.TryAddSingleton<ApplicationDbContextFactory>(o =>
|
services.TryAddSingleton<ApplicationDbContextFactory>(o =>
|
||||||
{
|
{
|
||||||
|
@ -150,6 +153,7 @@ namespace BTCPayServer.Hosting
|
||||||
services.TryAddSingleton<LanguageService>();
|
services.TryAddSingleton<LanguageService>();
|
||||||
services.TryAddSingleton<NBXplorerDashboard>();
|
services.TryAddSingleton<NBXplorerDashboard>();
|
||||||
services.TryAddSingleton<StoreRepository>();
|
services.TryAddSingleton<StoreRepository>();
|
||||||
|
services.TryAddSingleton<PaymentRequestRepository>();
|
||||||
services.TryAddSingleton<BTCPayWalletProvider>();
|
services.TryAddSingleton<BTCPayWalletProvider>();
|
||||||
services.TryAddSingleton<CurrencyNameTable>();
|
services.TryAddSingleton<CurrencyNameTable>();
|
||||||
services.TryAddSingleton<IFeeProviderFactory>(o => new NBXplorerFeeProviderFactory(o.GetRequiredService<ExplorerClientProvider>())
|
services.TryAddSingleton<IFeeProviderFactory>(o => new NBXplorerFeeProviderFactory(o.GetRequiredService<ExplorerClientProvider>())
|
||||||
|
@ -184,7 +188,7 @@ namespace BTCPayServer.Hosting
|
||||||
services.AddSingleton<IHostedService, RatesHostedService>();
|
services.AddSingleton<IHostedService, RatesHostedService>();
|
||||||
services.AddSingleton<IHostedService, BackgroundJobSchedulerHostedService>();
|
services.AddSingleton<IHostedService, BackgroundJobSchedulerHostedService>();
|
||||||
services.AddSingleton<IHostedService, AppHubStreamer>();
|
services.AddSingleton<IHostedService, AppHubStreamer>();
|
||||||
|
services.AddSingleton<IHostedService, PaymentRequestStreamer>();
|
||||||
services.AddSingleton<IBackgroundJobClient, BackgroundJobClient>();
|
services.AddSingleton<IBackgroundJobClient, BackgroundJobClient>();
|
||||||
services.AddTransient<IConfigureOptions<MvcOptions>, BTCPayClaimsFilter>();
|
services.AddTransient<IConfigureOptions<MvcOptions>, BTCPayClaimsFilter>();
|
||||||
|
|
||||||
|
@ -203,6 +207,7 @@ namespace BTCPayServer.Hosting
|
||||||
services.AddTransient<AccessTokenController>();
|
services.AddTransient<AccessTokenController>();
|
||||||
services.AddTransient<InvoiceController>();
|
services.AddTransient<InvoiceController>();
|
||||||
services.AddTransient<AppsPublicController>();
|
services.AddTransient<AppsPublicController>();
|
||||||
|
services.AddTransient<PaymentRequestController>();
|
||||||
// Add application services.
|
// Add application services.
|
||||||
services.AddSingleton<EmailSenderFactory>();
|
services.AddSingleton<EmailSenderFactory>();
|
||||||
// bundling
|
// bundling
|
||||||
|
|
|
@ -34,6 +34,7 @@ using Microsoft.Extensions.Options;
|
||||||
using Microsoft.AspNetCore.Mvc.Cors.Internal;
|
using Microsoft.AspNetCore.Mvc.Cors.Internal;
|
||||||
using Microsoft.AspNetCore.Server.Kestrel.Core;
|
using Microsoft.AspNetCore.Server.Kestrel.Core;
|
||||||
using System.Net;
|
using System.Net;
|
||||||
|
using BTCPayServer.PaymentRequest;
|
||||||
using Meziantou.AspNetCore.BundleTagHelpers;
|
using Meziantou.AspNetCore.BundleTagHelpers;
|
||||||
using BTCPayServer.Security;
|
using BTCPayServer.Security;
|
||||||
using BTCPayServer.Services.Apps;
|
using BTCPayServer.Services.Apps;
|
||||||
|
@ -165,6 +166,7 @@ namespace BTCPayServer.Hosting
|
||||||
app.UseSignalR(route =>
|
app.UseSignalR(route =>
|
||||||
{
|
{
|
||||||
route.MapHub<AppHub>("/apps/hub");
|
route.MapHub<AppHub>("/apps/hub");
|
||||||
|
route.MapHub<PaymentRequestHub>("/payment-requests/hub");
|
||||||
});
|
});
|
||||||
app.UseWebSockets();
|
app.UseWebSockets();
|
||||||
app.UseStatusCodePages();
|
app.UseStatusCodePages();
|
||||||
|
|
606
BTCPayServer/Migrations/20190121133309_AddPaymentRequests.Designer.cs
generated
Normal file
606
BTCPayServer/Migrations/20190121133309_AddPaymentRequests.Designer.cs
generated
Normal file
|
@ -0,0 +1,606 @@
|
||||||
|
// <auto-generated />
|
||||||
|
using System;
|
||||||
|
using BTCPayServer.Data;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||||
|
|
||||||
|
namespace BTCPayServer.Migrations
|
||||||
|
{
|
||||||
|
[DbContext(typeof(ApplicationDbContext))]
|
||||||
|
[Migration("20190121133309_AddPaymentRequests")]
|
||||||
|
partial class AddPaymentRequests
|
||||||
|
{
|
||||||
|
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||||
|
{
|
||||||
|
#pragma warning disable 612, 618
|
||||||
|
modelBuilder
|
||||||
|
.HasAnnotation("ProductVersion", "2.1.4-rtm-31024");
|
||||||
|
|
||||||
|
modelBuilder.Entity("BTCPayServer.Data.AddressInvoiceData", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("Address")
|
||||||
|
.ValueGeneratedOnAdd();
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset?>("CreatedTime");
|
||||||
|
|
||||||
|
b.Property<string>("InvoiceDataId");
|
||||||
|
|
||||||
|
b.HasKey("Address");
|
||||||
|
|
||||||
|
b.HasIndex("InvoiceDataId");
|
||||||
|
|
||||||
|
b.ToTable("AddressInvoices");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("BTCPayServer.Data.APIKeyData", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasMaxLength(50);
|
||||||
|
|
||||||
|
b.Property<string>("StoreId")
|
||||||
|
.HasMaxLength(50);
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("StoreId");
|
||||||
|
|
||||||
|
b.ToTable("ApiKeys");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("BTCPayServer.Data.AppData", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("Id")
|
||||||
|
.ValueGeneratedOnAdd();
|
||||||
|
|
||||||
|
b.Property<string>("AppType");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("Created");
|
||||||
|
|
||||||
|
b.Property<string>("Name");
|
||||||
|
|
||||||
|
b.Property<string>("Settings");
|
||||||
|
|
||||||
|
b.Property<string>("StoreDataId");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("StoreDataId");
|
||||||
|
|
||||||
|
b.ToTable("Apps");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("BTCPayServer.Data.HistoricalAddressInvoiceData", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("InvoiceDataId");
|
||||||
|
|
||||||
|
b.Property<string>("Address");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("Assigned");
|
||||||
|
|
||||||
|
b.Property<string>("CryptoCode");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset?>("UnAssigned");
|
||||||
|
|
||||||
|
b.HasKey("InvoiceDataId", "Address");
|
||||||
|
|
||||||
|
b.ToTable("HistoricalAddressInvoices");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("BTCPayServer.Data.InvoiceData", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("Id")
|
||||||
|
.ValueGeneratedOnAdd();
|
||||||
|
|
||||||
|
b.Property<byte[]>("Blob");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("Created");
|
||||||
|
|
||||||
|
b.Property<string>("CustomerEmail");
|
||||||
|
|
||||||
|
b.Property<string>("ExceptionStatus");
|
||||||
|
|
||||||
|
b.Property<string>("ItemCode");
|
||||||
|
|
||||||
|
b.Property<string>("OrderId");
|
||||||
|
|
||||||
|
b.Property<string>("Status");
|
||||||
|
|
||||||
|
b.Property<string>("StoreDataId");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("StoreDataId");
|
||||||
|
|
||||||
|
b.ToTable("Invoices");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("BTCPayServer.Data.InvoiceEventData", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("InvoiceDataId");
|
||||||
|
|
||||||
|
b.Property<string>("UniqueId");
|
||||||
|
|
||||||
|
b.Property<string>("Message");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("Timestamp");
|
||||||
|
|
||||||
|
b.HasKey("InvoiceDataId", "UniqueId");
|
||||||
|
|
||||||
|
b.ToTable("InvoiceEvents");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("BTCPayServer.Data.PairedSINData", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("Id")
|
||||||
|
.ValueGeneratedOnAdd();
|
||||||
|
|
||||||
|
b.Property<string>("Facade");
|
||||||
|
|
||||||
|
b.Property<string>("Label");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("PairingTime");
|
||||||
|
|
||||||
|
b.Property<string>("SIN");
|
||||||
|
|
||||||
|
b.Property<string>("StoreDataId");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("SIN");
|
||||||
|
|
||||||
|
b.HasIndex("StoreDataId");
|
||||||
|
|
||||||
|
b.ToTable("PairedSINData");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("BTCPayServer.Data.PairingCodeData", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("Id")
|
||||||
|
.ValueGeneratedOnAdd();
|
||||||
|
|
||||||
|
b.Property<DateTime>("DateCreated");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("Expiration");
|
||||||
|
|
||||||
|
b.Property<string>("Facade");
|
||||||
|
|
||||||
|
b.Property<string>("Label");
|
||||||
|
|
||||||
|
b.Property<string>("SIN");
|
||||||
|
|
||||||
|
b.Property<string>("StoreDataId");
|
||||||
|
|
||||||
|
b.Property<string>("TokenValue");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.ToTable("PairingCodes");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("BTCPayServer.Data.PaymentData", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("Id")
|
||||||
|
.ValueGeneratedOnAdd();
|
||||||
|
|
||||||
|
b.Property<bool>("Accounted");
|
||||||
|
|
||||||
|
b.Property<byte[]>("Blob");
|
||||||
|
|
||||||
|
b.Property<string>("InvoiceDataId");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("InvoiceDataId");
|
||||||
|
|
||||||
|
b.ToTable("Payments");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("BTCPayServer.Data.PendingInvoiceData", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("Id");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.ToTable("PendingInvoices");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("BTCPayServer.Data.RefundAddressesData", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("Id")
|
||||||
|
.ValueGeneratedOnAdd();
|
||||||
|
|
||||||
|
b.Property<byte[]>("Blob");
|
||||||
|
|
||||||
|
b.Property<string>("InvoiceDataId");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("InvoiceDataId");
|
||||||
|
|
||||||
|
b.ToTable("RefundAddresses");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("BTCPayServer.Data.SettingData", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("Id")
|
||||||
|
.ValueGeneratedOnAdd();
|
||||||
|
|
||||||
|
b.Property<string>("Value");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.ToTable("Settings");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("BTCPayServer.Data.StoreData", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("Id")
|
||||||
|
.ValueGeneratedOnAdd();
|
||||||
|
|
||||||
|
b.Property<string>("DefaultCrypto");
|
||||||
|
|
||||||
|
b.Property<string>("DerivationStrategies");
|
||||||
|
|
||||||
|
b.Property<string>("DerivationStrategy");
|
||||||
|
|
||||||
|
b.Property<int>("SpeedPolicy");
|
||||||
|
|
||||||
|
b.Property<byte[]>("StoreBlob");
|
||||||
|
|
||||||
|
b.Property<byte[]>("StoreCertificate");
|
||||||
|
|
||||||
|
b.Property<string>("StoreName");
|
||||||
|
|
||||||
|
b.Property<string>("StoreWebsite");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.ToTable("Stores");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("BTCPayServer.Data.UserStore", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("ApplicationUserId");
|
||||||
|
|
||||||
|
b.Property<string>("StoreDataId");
|
||||||
|
|
||||||
|
b.Property<string>("Role");
|
||||||
|
|
||||||
|
b.HasKey("ApplicationUserId", "StoreDataId");
|
||||||
|
|
||||||
|
b.HasIndex("StoreDataId");
|
||||||
|
|
||||||
|
b.ToTable("UserStore");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("BTCPayServer.Models.ApplicationUser", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("Id")
|
||||||
|
.ValueGeneratedOnAdd();
|
||||||
|
|
||||||
|
b.Property<int>("AccessFailedCount");
|
||||||
|
|
||||||
|
b.Property<string>("ConcurrencyStamp")
|
||||||
|
.IsConcurrencyToken();
|
||||||
|
|
||||||
|
b.Property<string>("Email")
|
||||||
|
.HasMaxLength(256);
|
||||||
|
|
||||||
|
b.Property<bool>("EmailConfirmed");
|
||||||
|
|
||||||
|
b.Property<bool>("LockoutEnabled");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset?>("LockoutEnd");
|
||||||
|
|
||||||
|
b.Property<string>("NormalizedEmail")
|
||||||
|
.HasMaxLength(256);
|
||||||
|
|
||||||
|
b.Property<string>("NormalizedUserName")
|
||||||
|
.HasMaxLength(256);
|
||||||
|
|
||||||
|
b.Property<string>("PasswordHash");
|
||||||
|
|
||||||
|
b.Property<string>("PhoneNumber");
|
||||||
|
|
||||||
|
b.Property<bool>("PhoneNumberConfirmed");
|
||||||
|
|
||||||
|
b.Property<bool>("RequiresEmailConfirmation");
|
||||||
|
|
||||||
|
b.Property<string>("SecurityStamp");
|
||||||
|
|
||||||
|
b.Property<bool>("TwoFactorEnabled");
|
||||||
|
|
||||||
|
b.Property<string>("UserName")
|
||||||
|
.HasMaxLength(256);
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("NormalizedEmail")
|
||||||
|
.HasName("EmailIndex");
|
||||||
|
|
||||||
|
b.HasIndex("NormalizedUserName")
|
||||||
|
.IsUnique()
|
||||||
|
.HasName("UserNameIndex");
|
||||||
|
|
||||||
|
b.ToTable("AspNetUsers");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("BTCPayServer.Services.PaymentRequests.PaymentRequestData", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("Id")
|
||||||
|
.ValueGeneratedOnAdd();
|
||||||
|
|
||||||
|
b.Property<byte[]>("Blob");
|
||||||
|
|
||||||
|
b.Property<int>("Status");
|
||||||
|
|
||||||
|
b.Property<string>("StoreDataId");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("Status");
|
||||||
|
|
||||||
|
b.HasIndex("StoreDataId");
|
||||||
|
|
||||||
|
b.ToTable("PaymentRequests");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("Id")
|
||||||
|
.ValueGeneratedOnAdd();
|
||||||
|
|
||||||
|
b.Property<string>("ConcurrencyStamp")
|
||||||
|
.IsConcurrencyToken();
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.HasMaxLength(256);
|
||||||
|
|
||||||
|
b.Property<string>("NormalizedName")
|
||||||
|
.HasMaxLength(256);
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("NormalizedName")
|
||||||
|
.IsUnique()
|
||||||
|
.HasName("RoleNameIndex");
|
||||||
|
|
||||||
|
b.ToTable("AspNetRoles");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd();
|
||||||
|
|
||||||
|
b.Property<string>("ClaimType");
|
||||||
|
|
||||||
|
b.Property<string>("ClaimValue");
|
||||||
|
|
||||||
|
b.Property<string>("RoleId")
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("RoleId");
|
||||||
|
|
||||||
|
b.ToTable("AspNetRoleClaims");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd();
|
||||||
|
|
||||||
|
b.Property<string>("ClaimType");
|
||||||
|
|
||||||
|
b.Property<string>("ClaimValue");
|
||||||
|
|
||||||
|
b.Property<string>("UserId")
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("UserId");
|
||||||
|
|
||||||
|
b.ToTable("AspNetUserClaims");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("LoginProvider");
|
||||||
|
|
||||||
|
b.Property<string>("ProviderKey");
|
||||||
|
|
||||||
|
b.Property<string>("ProviderDisplayName");
|
||||||
|
|
||||||
|
b.Property<string>("UserId")
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.HasKey("LoginProvider", "ProviderKey");
|
||||||
|
|
||||||
|
b.HasIndex("UserId");
|
||||||
|
|
||||||
|
b.ToTable("AspNetUserLogins");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<string>", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("UserId");
|
||||||
|
|
||||||
|
b.Property<string>("RoleId");
|
||||||
|
|
||||||
|
b.HasKey("UserId", "RoleId");
|
||||||
|
|
||||||
|
b.HasIndex("RoleId");
|
||||||
|
|
||||||
|
b.ToTable("AspNetUserRoles");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<string>", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("UserId");
|
||||||
|
|
||||||
|
b.Property<string>("LoginProvider");
|
||||||
|
|
||||||
|
b.Property<string>("Name");
|
||||||
|
|
||||||
|
b.Property<string>("Value");
|
||||||
|
|
||||||
|
b.HasKey("UserId", "LoginProvider", "Name");
|
||||||
|
|
||||||
|
b.ToTable("AspNetUserTokens");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("BTCPayServer.Data.AddressInvoiceData", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("BTCPayServer.Data.InvoiceData", "InvoiceData")
|
||||||
|
.WithMany("AddressInvoices")
|
||||||
|
.HasForeignKey("InvoiceDataId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("BTCPayServer.Data.APIKeyData", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("BTCPayServer.Data.StoreData", "StoreData")
|
||||||
|
.WithMany("APIKeys")
|
||||||
|
.HasForeignKey("StoreId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("BTCPayServer.Data.AppData", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("BTCPayServer.Data.StoreData", "StoreData")
|
||||||
|
.WithMany("Apps")
|
||||||
|
.HasForeignKey("StoreDataId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("BTCPayServer.Data.HistoricalAddressInvoiceData", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("BTCPayServer.Data.InvoiceData", "InvoiceData")
|
||||||
|
.WithMany("HistoricalAddressInvoices")
|
||||||
|
.HasForeignKey("InvoiceDataId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("BTCPayServer.Data.InvoiceData", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("BTCPayServer.Data.StoreData", "StoreData")
|
||||||
|
.WithMany("Invoices")
|
||||||
|
.HasForeignKey("StoreDataId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("BTCPayServer.Data.InvoiceEventData", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("BTCPayServer.Data.InvoiceData", "InvoiceData")
|
||||||
|
.WithMany("Events")
|
||||||
|
.HasForeignKey("InvoiceDataId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("BTCPayServer.Data.PairedSINData", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("BTCPayServer.Data.StoreData", "StoreData")
|
||||||
|
.WithMany("PairedSINs")
|
||||||
|
.HasForeignKey("StoreDataId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("BTCPayServer.Data.PaymentData", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("BTCPayServer.Data.InvoiceData", "InvoiceData")
|
||||||
|
.WithMany("Payments")
|
||||||
|
.HasForeignKey("InvoiceDataId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("BTCPayServer.Data.PendingInvoiceData", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("BTCPayServer.Data.InvoiceData", "InvoiceData")
|
||||||
|
.WithMany("PendingInvoices")
|
||||||
|
.HasForeignKey("Id")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("BTCPayServer.Data.RefundAddressesData", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("BTCPayServer.Data.InvoiceData", "InvoiceData")
|
||||||
|
.WithMany("RefundAddresses")
|
||||||
|
.HasForeignKey("InvoiceDataId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("BTCPayServer.Data.UserStore", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("BTCPayServer.Models.ApplicationUser", "ApplicationUser")
|
||||||
|
.WithMany("UserStores")
|
||||||
|
.HasForeignKey("ApplicationUserId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade);
|
||||||
|
|
||||||
|
b.HasOne("BTCPayServer.Data.StoreData", "StoreData")
|
||||||
|
.WithMany("UserStores")
|
||||||
|
.HasForeignKey("StoreDataId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("BTCPayServer.Services.PaymentRequests.PaymentRequestData", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("BTCPayServer.Data.StoreData", "StoreData")
|
||||||
|
.WithMany("PaymentRequests")
|
||||||
|
.HasForeignKey("StoreDataId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("RoleId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("BTCPayServer.Models.ApplicationUser")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("UserId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("BTCPayServer.Models.ApplicationUser")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("UserId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<string>", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("RoleId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade);
|
||||||
|
|
||||||
|
b.HasOne("BTCPayServer.Models.ApplicationUser")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("UserId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<string>", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("BTCPayServer.Models.ApplicationUser")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("UserId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade);
|
||||||
|
});
|
||||||
|
#pragma warning restore 612, 618
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
47
BTCPayServer/Migrations/20190121133309_AddPaymentRequests.cs
Normal file
47
BTCPayServer/Migrations/20190121133309_AddPaymentRequests.cs
Normal file
|
@ -0,0 +1,47 @@
|
||||||
|
using System;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
namespace BTCPayServer.Migrations
|
||||||
|
{
|
||||||
|
public partial class AddPaymentRequests : Migration
|
||||||
|
{
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "PaymentRequests",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<string>(nullable: false),
|
||||||
|
StoreDataId = table.Column<string>(nullable: true),
|
||||||
|
Status = table.Column<int>(nullable: false),
|
||||||
|
Blob = table.Column<byte[]>(nullable: true)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_PaymentRequests", x => x.Id);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_PaymentRequests_Stores_StoreDataId",
|
||||||
|
column: x => x.StoreDataId,
|
||||||
|
principalTable: "Stores",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_PaymentRequests_Status",
|
||||||
|
table: "PaymentRequests",
|
||||||
|
column: "Status");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_PaymentRequests_StoreDataId",
|
||||||
|
table: "PaymentRequests",
|
||||||
|
column: "StoreDataId");
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "PaymentRequests");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -328,6 +328,26 @@ namespace BTCPayServer.Migrations
|
||||||
b.ToTable("AspNetUsers");
|
b.ToTable("AspNetUsers");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("BTCPayServer.Services.PaymentRequests.PaymentRequestData", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("Id")
|
||||||
|
.ValueGeneratedOnAdd();
|
||||||
|
|
||||||
|
b.Property<byte[]>("Blob");
|
||||||
|
|
||||||
|
b.Property<int>("Status");
|
||||||
|
|
||||||
|
b.Property<string>("StoreDataId");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("Status");
|
||||||
|
|
||||||
|
b.HasIndex("StoreDataId");
|
||||||
|
|
||||||
|
b.ToTable("PaymentRequests");
|
||||||
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b =>
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b =>
|
||||||
{
|
{
|
||||||
b.Property<string>("Id")
|
b.Property<string>("Id")
|
||||||
|
@ -528,6 +548,14 @@ namespace BTCPayServer.Migrations
|
||||||
.OnDelete(DeleteBehavior.Cascade);
|
.OnDelete(DeleteBehavior.Cascade);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("BTCPayServer.Services.PaymentRequests.PaymentRequestData", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("BTCPayServer.Data.StoreData", "StoreData")
|
||||||
|
.WithMany("PaymentRequests")
|
||||||
|
.HasForeignKey("StoreDataId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade);
|
||||||
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
|
||||||
{
|
{
|
||||||
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole")
|
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole")
|
||||||
|
|
|
@ -0,0 +1,157 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
using BTCPayServer.Services.PaymentRequests;
|
||||||
|
using BTCPayServer.Services.Rates;
|
||||||
|
using Microsoft.AspNetCore.Mvc.Rendering;
|
||||||
|
|
||||||
|
namespace BTCPayServer.Models.PaymentRequestViewModels
|
||||||
|
{
|
||||||
|
public class ListPaymentRequestsViewModel
|
||||||
|
{
|
||||||
|
public int Skip { get; set; }
|
||||||
|
public int Count { get; set; }
|
||||||
|
|
||||||
|
public List<ViewPaymentRequestViewModel> Items { get; set; }
|
||||||
|
|
||||||
|
public string StatusMessage { get; set; }
|
||||||
|
public int Total { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class UpdatePaymentRequestViewModel
|
||||||
|
{
|
||||||
|
public UpdatePaymentRequestViewModel()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public UpdatePaymentRequestViewModel(PaymentRequestData data)
|
||||||
|
{
|
||||||
|
if (data == null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Id = data.Id;
|
||||||
|
StoreId = data.StoreDataId;
|
||||||
|
|
||||||
|
var blob = data.GetBlob();
|
||||||
|
Title = blob.Title;
|
||||||
|
Amount = blob.Amount;
|
||||||
|
Currency = blob.Currency;
|
||||||
|
Description = blob.Description;
|
||||||
|
ExpiryDate = blob.ExpiryDate;
|
||||||
|
Email = blob.Email;
|
||||||
|
CustomCSSLink = blob.CustomCSSLink;
|
||||||
|
EmbeddedCSS = blob.EmbeddedCSS;
|
||||||
|
AllowCustomPaymentAmounts = blob.AllowCustomPaymentAmounts;
|
||||||
|
}
|
||||||
|
|
||||||
|
public string Id { get; set; }
|
||||||
|
[Required] public string StoreId { get; set; }
|
||||||
|
[Required] public decimal Amount { get; set; }
|
||||||
|
|
||||||
|
[Display(Name = "The currency used for payment request. (e.g. BTC, LTC, USD, etc.)")]
|
||||||
|
public string Currency { get; set; }
|
||||||
|
|
||||||
|
[Display(Name = "Expiration Date")]
|
||||||
|
public DateTime? ExpiryDate { get; set; }
|
||||||
|
[Required] public string Title { get; set; }
|
||||||
|
public string Description { get; set; }
|
||||||
|
public string StatusMessage { get; set; }
|
||||||
|
|
||||||
|
public SelectList Stores { get; set; }
|
||||||
|
[EmailAddress]
|
||||||
|
public string Email { get; set; }
|
||||||
|
|
||||||
|
[MaxLength(500)]
|
||||||
|
[Display(Name = "Custom bootstrap CSS file")]
|
||||||
|
public string CustomCSSLink { get; set; }
|
||||||
|
|
||||||
|
[Display(Name = "Custom CSS Code")]
|
||||||
|
public string EmbeddedCSS { get; set; }
|
||||||
|
[Display(Name = "Allow payee to create invoices in their own denomination")]
|
||||||
|
public bool AllowCustomPaymentAmounts { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class ViewPaymentRequestViewModel
|
||||||
|
{
|
||||||
|
public ViewPaymentRequestViewModel(PaymentRequestData data)
|
||||||
|
{
|
||||||
|
Id = data.Id;
|
||||||
|
var blob = data.GetBlob();
|
||||||
|
Title = blob.Title;
|
||||||
|
Amount = blob.Amount;
|
||||||
|
Currency = blob.Currency;
|
||||||
|
Description = blob.Description;
|
||||||
|
ExpiryDate = blob.ExpiryDate;
|
||||||
|
Email = blob.Email;
|
||||||
|
EmbeddedCSS = blob.EmbeddedCSS;
|
||||||
|
CustomCSSLink = blob.CustomCSSLink;
|
||||||
|
AllowCustomPaymentAmounts = blob.AllowCustomPaymentAmounts;
|
||||||
|
switch (data.Status)
|
||||||
|
{
|
||||||
|
case PaymentRequestData.PaymentRequestStatus.Pending:
|
||||||
|
Status = ExpiryDate.HasValue ? $"Expires on {ExpiryDate.Value:g}" : "Pending";
|
||||||
|
IsPending = true;
|
||||||
|
break;
|
||||||
|
case PaymentRequestData.PaymentRequestStatus.Completed:
|
||||||
|
Status = "Settled";
|
||||||
|
break;
|
||||||
|
case PaymentRequestData.PaymentRequestStatus.Expired:
|
||||||
|
Status = "Expired";
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new ArgumentOutOfRangeException();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool AllowCustomPaymentAmounts { get; set; }
|
||||||
|
|
||||||
|
|
||||||
|
public string Email { get; set; }
|
||||||
|
|
||||||
|
public string Status { get; set; }
|
||||||
|
public bool IsPending { get; set; }
|
||||||
|
|
||||||
|
public decimal AmountCollected { get; set; }
|
||||||
|
public decimal AmountDue { get; set; }
|
||||||
|
public string AmountDueFormatted { get; set; }
|
||||||
|
public decimal Amount { get; set; }
|
||||||
|
public string Id { get; set; }
|
||||||
|
public string Currency { get; set; }
|
||||||
|
|
||||||
|
public DateTime? ExpiryDate { get; set; }
|
||||||
|
|
||||||
|
public string Title { get; set; }
|
||||||
|
public string Description { get; set; }
|
||||||
|
|
||||||
|
public string EmbeddedCSS { get; set; }
|
||||||
|
public string CustomCSSLink { get; set; }
|
||||||
|
|
||||||
|
public List<PaymentRequestInvoice> Invoices { get; set; } = new List<PaymentRequestInvoice>();
|
||||||
|
public DateTime LastUpdated { get; set; }
|
||||||
|
public CurrencyData CurrencyData { get; set; }
|
||||||
|
public string AmountCollectedFormatted { get; set; }
|
||||||
|
public string AmountFormatted { get; set; }
|
||||||
|
public bool AnyPendingInvoice { get; set; }
|
||||||
|
|
||||||
|
public class PaymentRequestInvoice
|
||||||
|
{
|
||||||
|
public string Id { get; set; }
|
||||||
|
public DateTime ExpiryDate { get; set; }
|
||||||
|
public decimal Amount { get; set; }
|
||||||
|
public string Status { get; set; }
|
||||||
|
|
||||||
|
public List<PaymentRequestInvoicePayment> Payments { get; set; }
|
||||||
|
public string Currency { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class PaymentRequestInvoicePayment
|
||||||
|
{
|
||||||
|
public string PaymentMethod { get; set; }
|
||||||
|
public decimal Amount { get; set; }
|
||||||
|
public string Link { get; set; }
|
||||||
|
public string Id { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
175
BTCPayServer/PaymentRequest/PaymentRequestHub.cs
Normal file
175
BTCPayServer/PaymentRequest/PaymentRequestHub.cs
Normal file
|
@ -0,0 +1,175 @@
|
||||||
|
using System;
|
||||||
|
using System.Linq;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using BTCPayServer.Controllers;
|
||||||
|
using BTCPayServer.Events;
|
||||||
|
using BTCPayServer.HostedServices;
|
||||||
|
using BTCPayServer.Logging;
|
||||||
|
using BTCPayServer.Payments;
|
||||||
|
using BTCPayServer.Services.PaymentRequests;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Microsoft.AspNetCore.SignalR;
|
||||||
|
using BTCPayServer.Services.Apps;
|
||||||
|
|
||||||
|
namespace BTCPayServer.PaymentRequest
|
||||||
|
{
|
||||||
|
public class PaymentRequestHub : Hub
|
||||||
|
{
|
||||||
|
private readonly PaymentRequestController _PaymentRequestController;
|
||||||
|
public const string InvoiceCreated = "InvoiceCreated";
|
||||||
|
public const string PaymentReceived = "PaymentReceived";
|
||||||
|
public const string InfoUpdated = "InfoUpdated";
|
||||||
|
public const string InvoiceError = "InvoiceError";
|
||||||
|
|
||||||
|
public PaymentRequestHub(PaymentRequestController paymentRequestController)
|
||||||
|
{
|
||||||
|
_PaymentRequestController = paymentRequestController;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task ListenToPaymentRequest(string paymentRequestId)
|
||||||
|
{
|
||||||
|
if (Context.Items.ContainsKey("pr-id"))
|
||||||
|
{
|
||||||
|
await Groups.RemoveFromGroupAsync(Context.ConnectionId, Context.Items["pr-id"].ToString());
|
||||||
|
Context.Items.Remove("pr-id");
|
||||||
|
}
|
||||||
|
|
||||||
|
Context.Items.Add("pr-id", paymentRequestId);
|
||||||
|
await Groups.AddToGroupAsync(Context.ConnectionId, paymentRequestId);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public async Task Pay(decimal? amount = null)
|
||||||
|
{
|
||||||
|
_PaymentRequestController.ControllerContext.HttpContext = Context.GetHttpContext();
|
||||||
|
var result =
|
||||||
|
await _PaymentRequestController.PayPaymentRequest(Context.Items["pr-id"].ToString(), false, amount);
|
||||||
|
switch (result)
|
||||||
|
{
|
||||||
|
case OkObjectResult okObjectResult:
|
||||||
|
await Clients.Caller.SendCoreAsync(InvoiceCreated, new[] {okObjectResult.Value.ToString()});
|
||||||
|
break;
|
||||||
|
case ObjectResult objectResult:
|
||||||
|
await Clients.Caller.SendCoreAsync(InvoiceError, new[] {objectResult.Value});
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
await Clients.Caller.SendCoreAsync(InvoiceError, System.Array.Empty<object>());
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class PaymentRequestStreamer : EventHostedServiceBase
|
||||||
|
{
|
||||||
|
private readonly IHubContext<PaymentRequestHub> _HubContext;
|
||||||
|
private readonly PaymentRequestRepository _PaymentRequestRepository;
|
||||||
|
private readonly PaymentRequestService _PaymentRequestService;
|
||||||
|
|
||||||
|
|
||||||
|
public PaymentRequestStreamer(EventAggregator eventAggregator,
|
||||||
|
IHubContext<PaymentRequestHub> hubContext,
|
||||||
|
PaymentRequestRepository paymentRequestRepository,
|
||||||
|
PaymentRequestService paymentRequestService) : base(eventAggregator)
|
||||||
|
{
|
||||||
|
_HubContext = hubContext;
|
||||||
|
_PaymentRequestRepository = paymentRequestRepository;
|
||||||
|
_PaymentRequestService = paymentRequestService;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override async Task StartAsync(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
await base.StartAsync(cancellationToken);
|
||||||
|
_CheckingPendingPayments = CheckingPendingPayments(cancellationToken)
|
||||||
|
.ContinueWith(_ => _CheckingPendingPayments = null, TaskScheduler.Default);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task CheckingPendingPayments(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
Logs.PayServer.LogInformation("Starting payment request expiration watcher");
|
||||||
|
var (total, items) = await _PaymentRequestRepository.FindPaymentRequests(new PaymentRequestQuery()
|
||||||
|
{
|
||||||
|
Status = new[] {PaymentRequestData.PaymentRequestStatus.Pending}
|
||||||
|
}, cancellationToken);
|
||||||
|
|
||||||
|
Logs.PayServer.LogInformation($"{total} pending payment requests being checked since last run");
|
||||||
|
await Task.WhenAll(items.Select(i => _PaymentRequestService.UpdatePaymentRequestStateIfNeeded(i))
|
||||||
|
.ToArray());
|
||||||
|
}
|
||||||
|
|
||||||
|
Task _CheckingPendingPayments;
|
||||||
|
|
||||||
|
public override async Task StopAsync(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
await base.StopAsync(cancellationToken);
|
||||||
|
await (_CheckingPendingPayments ?? Task.CompletedTask);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void SubscibeToEvents()
|
||||||
|
{
|
||||||
|
Subscribe<InvoiceEvent>();
|
||||||
|
Subscribe<PaymentRequestUpdated>();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override async Task ProcessEvent(object evt, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
if (evt is InvoiceEvent invoiceEvent)
|
||||||
|
{
|
||||||
|
foreach (var paymentId in PaymentRequestRepository.GetPaymentIdsFromInternalTags(invoiceEvent.Invoice))
|
||||||
|
{
|
||||||
|
if (invoiceEvent.Name == InvoiceEvent.ReceivedPayment)
|
||||||
|
{
|
||||||
|
await _PaymentRequestService.UpdatePaymentRequestStateIfNeeded(paymentId);
|
||||||
|
var data = invoiceEvent.Payment.GetCryptoPaymentData();
|
||||||
|
await _HubContext.Clients.Group(paymentId).SendCoreAsync(PaymentRequestHub.PaymentReceived,
|
||||||
|
new object[]
|
||||||
|
{
|
||||||
|
data.GetValue(),
|
||||||
|
invoiceEvent.Payment.GetCryptoCode(),
|
||||||
|
Enum.GetName(typeof(PaymentTypes),
|
||||||
|
invoiceEvent.Payment.GetPaymentMethodId().PaymentType)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
await InfoUpdated(paymentId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (evt is PaymentRequestUpdated updated)
|
||||||
|
{
|
||||||
|
await InfoUpdated(updated.PaymentRequestId);
|
||||||
|
|
||||||
|
var expiry = updated.Data.GetBlob().ExpiryDate;
|
||||||
|
if (updated.Data.Status == PaymentRequestData.PaymentRequestStatus.Pending &&
|
||||||
|
expiry.HasValue)
|
||||||
|
{
|
||||||
|
QueueExpiryTask(
|
||||||
|
updated.PaymentRequestId,
|
||||||
|
expiry.Value,
|
||||||
|
cancellationToken);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void QueueExpiryTask(string paymentRequestId, DateTime expiry, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
Task.Run(async () =>
|
||||||
|
{
|
||||||
|
var delay = expiry - DateTime.Now;
|
||||||
|
if (delay > TimeSpan.Zero)
|
||||||
|
await Task.Delay(delay, cancellationToken);
|
||||||
|
await _PaymentRequestService.UpdatePaymentRequestStateIfNeeded(paymentRequestId);
|
||||||
|
}, cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task InfoUpdated(string paymentRequestId)
|
||||||
|
{
|
||||||
|
var req = await _PaymentRequestService.GetPaymentRequest(paymentRequestId);
|
||||||
|
if (req != null)
|
||||||
|
{
|
||||||
|
await _HubContext.Clients.Group(paymentRequestId)
|
||||||
|
.SendCoreAsync(PaymentRequestHub.InfoUpdated, new object[] {req});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
136
BTCPayServer/PaymentRequest/PaymentRequestService.cs
Normal file
136
BTCPayServer/PaymentRequest/PaymentRequestService.cs
Normal file
|
@ -0,0 +1,136 @@
|
||||||
|
using System;
|
||||||
|
using System.Globalization;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using BTCPayServer.Models.PaymentRequestViewModels;
|
||||||
|
using BTCPayServer.Payments.Lightning;
|
||||||
|
using BTCPayServer.Services.Apps;
|
||||||
|
using BTCPayServer.Services.Invoices;
|
||||||
|
using BTCPayServer.Services.PaymentRequests;
|
||||||
|
using BTCPayServer.Services.Rates;
|
||||||
|
using Microsoft.AspNetCore.SignalR;
|
||||||
|
|
||||||
|
namespace BTCPayServer.PaymentRequest
|
||||||
|
{
|
||||||
|
public class PaymentRequestService
|
||||||
|
{
|
||||||
|
private readonly PaymentRequestRepository _PaymentRequestRepository;
|
||||||
|
private readonly BTCPayNetworkProvider _BtcPayNetworkProvider;
|
||||||
|
private readonly AppService _AppService;
|
||||||
|
private readonly CurrencyNameTable _currencies;
|
||||||
|
|
||||||
|
public PaymentRequestService(
|
||||||
|
IHubContext<PaymentRequestHub> hubContext,
|
||||||
|
PaymentRequestRepository paymentRequestRepository,
|
||||||
|
BTCPayNetworkProvider btcPayNetworkProvider,
|
||||||
|
AppService appService,
|
||||||
|
CurrencyNameTable currencies)
|
||||||
|
{
|
||||||
|
_PaymentRequestRepository = paymentRequestRepository;
|
||||||
|
_BtcPayNetworkProvider = btcPayNetworkProvider;
|
||||||
|
_AppService = appService;
|
||||||
|
_currencies = currencies;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task UpdatePaymentRequestStateIfNeeded(string id)
|
||||||
|
{
|
||||||
|
var pr = await _PaymentRequestRepository.FindPaymentRequest(id, null);
|
||||||
|
await UpdatePaymentRequestStateIfNeeded(pr);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task UpdatePaymentRequestStateIfNeeded(PaymentRequestData pr)
|
||||||
|
{
|
||||||
|
var blob = pr.GetBlob();
|
||||||
|
var currentStatus = pr.Status;
|
||||||
|
if (blob.ExpiryDate.HasValue)
|
||||||
|
{
|
||||||
|
if (blob.ExpiryDate.Value <= DateTimeOffset.UtcNow)
|
||||||
|
currentStatus = PaymentRequestData.PaymentRequestStatus.Expired;
|
||||||
|
}
|
||||||
|
else if (pr.Status == PaymentRequestData.PaymentRequestStatus.Pending)
|
||||||
|
{
|
||||||
|
var rateRules = pr.StoreData.GetStoreBlob().GetRateRules(_BtcPayNetworkProvider);
|
||||||
|
var invoices = await _PaymentRequestRepository.GetInvoicesForPaymentRequest(pr.Id);
|
||||||
|
var paymentStats = _AppService.GetCurrentContributionAmountStats(invoices, true);
|
||||||
|
var amountCollected =
|
||||||
|
await _AppService.GetCurrentContributionAmount(paymentStats, blob.Currency, rateRules);
|
||||||
|
if (amountCollected >= blob.Amount)
|
||||||
|
{
|
||||||
|
currentStatus = PaymentRequestData.PaymentRequestStatus.Completed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentStatus != pr.Status)
|
||||||
|
{
|
||||||
|
pr.Status = currentStatus;
|
||||||
|
await _PaymentRequestRepository.UpdatePaymentRequestStatus(pr.Id, currentStatus);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<ViewPaymentRequestViewModel> GetPaymentRequest(string id, string userId = null)
|
||||||
|
{
|
||||||
|
var pr = await _PaymentRequestRepository.FindPaymentRequest(id, null);
|
||||||
|
if (pr == null)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var blob = pr.GetBlob();
|
||||||
|
var rateRules = pr.StoreData.GetStoreBlob().GetRateRules(_BtcPayNetworkProvider);
|
||||||
|
|
||||||
|
var invoices = await _PaymentRequestRepository.GetInvoicesForPaymentRequest(id);
|
||||||
|
|
||||||
|
var paymentStats = _AppService.GetCurrentContributionAmountStats(invoices, true);
|
||||||
|
var amountCollected =
|
||||||
|
await _AppService.GetCurrentContributionAmount(paymentStats, blob.Currency, rateRules);
|
||||||
|
|
||||||
|
var amountDue = blob.Amount - amountCollected;
|
||||||
|
|
||||||
|
return new ViewPaymentRequestViewModel(pr)
|
||||||
|
{
|
||||||
|
AmountFormatted = _currencies.FormatCurrency(blob.Amount, blob.Currency),
|
||||||
|
AmountCollected = amountCollected,
|
||||||
|
AmountCollectedFormatted = _currencies.FormatCurrency(amountCollected, blob.Currency),
|
||||||
|
AmountDue = amountDue,
|
||||||
|
AmountDueFormatted = _currencies.FormatCurrency(amountDue, blob.Currency),
|
||||||
|
CurrencyData = _currencies.GetCurrencyData(blob.Currency, true),
|
||||||
|
LastUpdated = DateTime.Now,
|
||||||
|
AnyPendingInvoice = invoices.Any(entity => entity.Status == InvoiceStatus.New),
|
||||||
|
Invoices = invoices.Select(entity => new ViewPaymentRequestViewModel.PaymentRequestInvoice()
|
||||||
|
{
|
||||||
|
Id = entity.Id,
|
||||||
|
Amount = entity.ProductInformation.Price,
|
||||||
|
Currency = entity.ProductInformation.Currency,
|
||||||
|
ExpiryDate = entity.ExpirationTime.DateTime,
|
||||||
|
Status = entity.GetInvoiceState().ToString(),
|
||||||
|
Payments = entity.GetPayments().Select(paymentEntity =>
|
||||||
|
{
|
||||||
|
var paymentNetwork = _BtcPayNetworkProvider.GetNetwork(paymentEntity.GetCryptoCode());
|
||||||
|
var paymentData = paymentEntity.GetCryptoPaymentData();
|
||||||
|
string link = null;
|
||||||
|
string txId = null;
|
||||||
|
switch (paymentData)
|
||||||
|
{
|
||||||
|
case Payments.Bitcoin.BitcoinLikePaymentData onChainPaymentData:
|
||||||
|
txId = onChainPaymentData.Outpoint.Hash.ToString();
|
||||||
|
link = string.Format(CultureInfo.InvariantCulture, paymentNetwork.BlockExplorerLink,
|
||||||
|
txId);
|
||||||
|
break;
|
||||||
|
case LightningLikePaymentData lightningLikePaymentData:
|
||||||
|
txId = lightningLikePaymentData.BOLT11;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new ViewPaymentRequestViewModel.PaymentRequestInvoicePayment()
|
||||||
|
{
|
||||||
|
Amount = paymentData.GetValue(),
|
||||||
|
PaymentMethod = paymentEntity.GetPaymentMethodId().ToString(),
|
||||||
|
Link = link,
|
||||||
|
Id = txId
|
||||||
|
};
|
||||||
|
}).ToList()
|
||||||
|
}).ToList()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -47,7 +47,7 @@ namespace BTCPayServer.Services.Apps
|
||||||
{
|
{
|
||||||
if (evt is InvoiceEvent invoiceEvent)
|
if (evt is InvoiceEvent invoiceEvent)
|
||||||
{
|
{
|
||||||
foreach (var appId in AppService.GetAppInternalTags(invoiceEvent.Invoice.InternalTags))
|
foreach (var appId in AppService.GetAppInternalTags(invoiceEvent.Invoice))
|
||||||
{
|
{
|
||||||
if (invoiceEvent.Name == InvoiceEvent.ReceivedPayment)
|
if (invoiceEvent.Name == InvoiceEvent.ReceivedPayment)
|
||||||
{
|
{
|
||||||
|
|
|
@ -167,11 +167,9 @@ namespace BTCPayServer.Services.Apps
|
||||||
|
|
||||||
public static string GetCrowdfundOrderId(string appId) => $"crowdfund-app_{appId}";
|
public static string GetCrowdfundOrderId(string appId) => $"crowdfund-app_{appId}";
|
||||||
public static string GetAppInternalTag(string appId) => $"APP#{appId}";
|
public static string GetAppInternalTag(string appId) => $"APP#{appId}";
|
||||||
public static string[] GetAppInternalTags(IEnumerable<string> tags)
|
public static string[] GetAppInternalTags(InvoiceEntity invoice)
|
||||||
{
|
{
|
||||||
return tags == null ? Array.Empty<string>() : tags
|
return invoice.GetInternalTags("APP#");
|
||||||
.Where(t => t.StartsWith("APP#", StringComparison.InvariantCulture))
|
|
||||||
.Select(t => t.Substring("APP#".Length)).ToArray();
|
|
||||||
}
|
}
|
||||||
private async Task<InvoiceEntity[]> GetInvoicesForApp(AppData appData, DateTime? startDate = null)
|
private async Task<InvoiceEntity[]> GetInvoicesForApp(AppData appData, DateTime? startDate = null)
|
||||||
{
|
{
|
||||||
|
@ -346,7 +344,7 @@ namespace BTCPayServer.Services.Apps
|
||||||
|
|
||||||
// Else, we just sum the payments
|
// Else, we just sum the payments
|
||||||
return payments
|
return payments
|
||||||
.Select(pay => (Key: pay.GetPaymentMethodId().ToString(), Value: pay.GetCryptoPaymentData().GetValue()))
|
.Select(pay => (Key: pay.GetPaymentMethodId().ToString(), Value: pay.GetCryptoPaymentData().GetValue() - pay.NetworkFee))
|
||||||
.ToArray();
|
.ToArray();
|
||||||
})
|
})
|
||||||
.GroupBy(p => p.Key)
|
.GroupBy(p => p.Key)
|
||||||
|
|
|
@ -170,6 +170,13 @@ namespace BTCPayServer.Services.Invoices
|
||||||
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)]
|
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)]
|
||||||
public HashSet<string> InternalTags { get; set; } = new HashSet<string>();
|
public HashSet<string> InternalTags { get; set; } = new HashSet<string>();
|
||||||
|
|
||||||
|
public string[] GetInternalTags(string suffix)
|
||||||
|
{
|
||||||
|
return InternalTags == null ? Array.Empty<string>() : InternalTags
|
||||||
|
.Where(t => t.StartsWith(suffix, StringComparison.InvariantCulture))
|
||||||
|
.Select(t => t.Substring(suffix.Length)).ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
[Obsolete("Use GetDerivationStrategies instead")]
|
[Obsolete("Use GetDerivationStrategies instead")]
|
||||||
public string DerivationStrategy
|
public string DerivationStrategy
|
||||||
{
|
{
|
||||||
|
|
|
@ -0,0 +1,254 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using BTCPayServer.Data;
|
||||||
|
using BTCPayServer.Services.Invoices;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Internal;
|
||||||
|
using NBitcoin;
|
||||||
|
using NBXplorer;
|
||||||
|
using Newtonsoft.Json.Linq;
|
||||||
|
|
||||||
|
namespace BTCPayServer.Services.PaymentRequests
|
||||||
|
{
|
||||||
|
public class PaymentRequestRepository
|
||||||
|
{
|
||||||
|
private readonly ApplicationDbContextFactory _ContextFactory;
|
||||||
|
private readonly InvoiceRepository _InvoiceRepository;
|
||||||
|
|
||||||
|
public PaymentRequestRepository(ApplicationDbContextFactory contextFactory, InvoiceRepository invoiceRepository)
|
||||||
|
{
|
||||||
|
_ContextFactory = contextFactory;
|
||||||
|
_InvoiceRepository = invoiceRepository;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public async Task<PaymentRequestData> CreateOrUpdatePaymentRequest(PaymentRequestData entity)
|
||||||
|
{
|
||||||
|
using (var context = _ContextFactory.CreateContext())
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(entity.Id))
|
||||||
|
{
|
||||||
|
await context.PaymentRequests.AddAsync(entity);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
context.PaymentRequests.Update(entity);
|
||||||
|
}
|
||||||
|
|
||||||
|
await context.SaveChangesAsync();
|
||||||
|
return entity;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<PaymentRequestData> FindPaymentRequest(string id, string userId, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(id))
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
using (var context = _ContextFactory.CreateContext())
|
||||||
|
{
|
||||||
|
return await context.PaymentRequests.Include(x => x.StoreData)
|
||||||
|
.Where(data =>
|
||||||
|
string.IsNullOrEmpty(userId) ||
|
||||||
|
(data.StoreData != null && data.StoreData.UserStores.Any(u => u.ApplicationUserId == userId)))
|
||||||
|
.SingleOrDefaultAsync(x => x.Id == id, cancellationToken);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<bool> IsPaymentRequestAdmin(string paymentRequestId, string userId)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(userId) || string.IsNullOrEmpty(paymentRequestId))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
using (var context = _ContextFactory.CreateContext())
|
||||||
|
{
|
||||||
|
return await context.PaymentRequests.Include(x => x.StoreData)
|
||||||
|
.AnyAsync(data =>
|
||||||
|
data.Id == paymentRequestId &&
|
||||||
|
(data.StoreData != null && data.StoreData.UserStores.Any(u => u.ApplicationUserId == userId)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task UpdatePaymentRequestStatus(string paymentRequestId, PaymentRequestData.PaymentRequestStatus status, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
using (var context = _ContextFactory.CreateContext())
|
||||||
|
{
|
||||||
|
var invoiceData = await context.FindAsync<PaymentRequestData>(paymentRequestId);
|
||||||
|
if (invoiceData == null)
|
||||||
|
return;
|
||||||
|
invoiceData.Status = status;
|
||||||
|
await context.SaveChangesAsync(cancellationToken);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<(int Total, PaymentRequestData[] Items)> FindPaymentRequests(PaymentRequestQuery query, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
using (var context = _ContextFactory.CreateContext())
|
||||||
|
{
|
||||||
|
var queryable = context.PaymentRequests.Include(data => data.StoreData).AsQueryable();
|
||||||
|
if (!string.IsNullOrEmpty(query.StoreId))
|
||||||
|
{
|
||||||
|
queryable = queryable.Where(data =>
|
||||||
|
data.StoreDataId.Equals(query.StoreId, StringComparison.InvariantCulture));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (query.Status != null && query.Status.Any())
|
||||||
|
{
|
||||||
|
queryable = queryable.Where(data =>
|
||||||
|
query.Status.Contains(data.Status));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(query.UserId))
|
||||||
|
{
|
||||||
|
queryable = queryable.Where(i =>
|
||||||
|
i.StoreData != null && i.StoreData.UserStores.Any(u => u.ApplicationUserId == query.UserId));
|
||||||
|
}
|
||||||
|
|
||||||
|
var total = await queryable.CountAsync(cancellationToken);
|
||||||
|
|
||||||
|
if (query.Skip.HasValue)
|
||||||
|
{
|
||||||
|
queryable = queryable.Skip(query.Skip.Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (query.Count.HasValue)
|
||||||
|
{
|
||||||
|
queryable = queryable.Take(query.Count.Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (total, await queryable.ToArrayAsync(cancellationToken));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<bool> RemovePaymentRequest(string id, string userId)
|
||||||
|
{
|
||||||
|
using (var context = _ContextFactory.CreateContext())
|
||||||
|
{
|
||||||
|
var canDelete = !EnumerableExtensions.Any((await GetInvoicesForPaymentRequest(id)));
|
||||||
|
if (!canDelete) return false;
|
||||||
|
var pr = await FindPaymentRequest(id, userId);
|
||||||
|
if (pr == null)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
context.PaymentRequests.Remove(pr);
|
||||||
|
await context.SaveChangesAsync();
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<InvoiceEntity[]> GetInvoicesForPaymentRequest(string paymentRequestId,
|
||||||
|
InvoiceQuery invoiceQuery = null)
|
||||||
|
{
|
||||||
|
if (invoiceQuery == null)
|
||||||
|
{
|
||||||
|
invoiceQuery = new InvoiceQuery();
|
||||||
|
}
|
||||||
|
|
||||||
|
invoiceQuery.OrderId = new[] {GetOrderIdForPaymentRequest(paymentRequestId)};
|
||||||
|
return await _InvoiceRepository.GetInvoices(invoiceQuery);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string GetOrderIdForPaymentRequest(string paymentRequestId)
|
||||||
|
{
|
||||||
|
return $"PAY_REQUEST_{paymentRequestId}";
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string GetPaymentRequestIdFromOrderId(string invoiceOrderId)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(invoiceOrderId) ||
|
||||||
|
!invoiceOrderId.StartsWith("PAY_REQUEST_", StringComparison.InvariantCulture))
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return invoiceOrderId.Replace("PAY_REQUEST_", "", StringComparison.InvariantCulture);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string GetInternalTag(string id)
|
||||||
|
{
|
||||||
|
return $"PAYREQ#{id}";
|
||||||
|
}
|
||||||
|
public static string[] GetPaymentIdsFromInternalTags(InvoiceEntity invoiceEntity)
|
||||||
|
{
|
||||||
|
return invoiceEntity.GetInternalTags("PAYREQ#");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class PaymentRequestUpdated
|
||||||
|
{
|
||||||
|
public string PaymentRequestId { get; set; }
|
||||||
|
public PaymentRequestData Data { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class PaymentRequestQuery
|
||||||
|
{
|
||||||
|
public string StoreId { get; set; }
|
||||||
|
|
||||||
|
public PaymentRequestData.PaymentRequestStatus[] Status{ get; set; }
|
||||||
|
public string UserId { get; set; }
|
||||||
|
public int? Skip { get; set; }
|
||||||
|
public int? Count { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class PaymentRequestData
|
||||||
|
{
|
||||||
|
public string Id { get; set; }
|
||||||
|
public string StoreDataId { get; set; }
|
||||||
|
|
||||||
|
public StoreData StoreData { get; set; }
|
||||||
|
|
||||||
|
public PaymentRequestStatus Status { get; set; }
|
||||||
|
|
||||||
|
public byte[] Blob { get; set; }
|
||||||
|
|
||||||
|
public PaymentRequestBlob GetBlob()
|
||||||
|
{
|
||||||
|
var result = Blob == null
|
||||||
|
? new PaymentRequestBlob()
|
||||||
|
: JObject.Parse(ZipUtils.Unzip(Blob)).ToObject<PaymentRequestBlob>();
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool SetBlob(PaymentRequestBlob blob)
|
||||||
|
{
|
||||||
|
var original = new Serializer(Network.Main).ToString(GetBlob());
|
||||||
|
var newBlob = new Serializer(Network.Main).ToString(blob);
|
||||||
|
if (original == newBlob)
|
||||||
|
return false;
|
||||||
|
Blob = ZipUtils.Zip(newBlob);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public class PaymentRequestBlob
|
||||||
|
{
|
||||||
|
public decimal Amount { get; set; }
|
||||||
|
public string Currency { get; set; }
|
||||||
|
|
||||||
|
public DateTime? ExpiryDate { get; set; }
|
||||||
|
|
||||||
|
public string Title { get; set; }
|
||||||
|
public string Description { get; set; }
|
||||||
|
public string Email { get; set; }
|
||||||
|
|
||||||
|
public string EmbeddedCSS { get; set; }
|
||||||
|
public string CustomCSSLink { get; set; }
|
||||||
|
public bool AllowCustomPaymentAmounts { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum PaymentRequestStatus
|
||||||
|
{
|
||||||
|
Pending = 0,
|
||||||
|
Completed = 1,
|
||||||
|
Expired = 2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,7 +1,7 @@
|
||||||
@using BTCPayServer.Services.Apps
|
@using BTCPayServer.Services.Apps
|
||||||
@model ListAppsViewModel
|
@model ListAppsViewModel
|
||||||
@{
|
@{
|
||||||
ViewData["Title"] = "Stores";
|
ViewData["Title"] = "Apps";
|
||||||
}
|
}
|
||||||
|
|
||||||
<section>
|
<section>
|
||||||
|
|
|
@ -15,7 +15,7 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-footer">
|
<div class="modal-footer">PRS
|
||||||
<button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button>
|
<button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button>
|
||||||
<button type="button" class="js-product-save btn btn-primary">Save Changes</button>
|
<button type="button" class="js-product-save btn btn-primary">Save Changes</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
122
BTCPayServer/Views/PaymentRequest/EditPaymentRequest.cshtml
Normal file
122
BTCPayServer/Views/PaymentRequest/EditPaymentRequest.cshtml
Normal file
|
@ -0,0 +1,122 @@
|
||||||
|
@using BTCPayServer.Services.PaymentRequests
|
||||||
|
@model BTCPayServer.Models.PaymentRequestViewModels.UpdatePaymentRequestViewModel
|
||||||
|
@addTagHelper *, Meziantou.AspNetCore.BundleTagHelpers
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<div class="container">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-lg-12 text-center">
|
||||||
|
<h2 class="section-heading">@(string.IsNullOrEmpty(Model.Id) ? "Create" : "Edit") Payment Request</h2>
|
||||||
|
<hr class="primary">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-lg-12 text-center">
|
||||||
|
<partial name="_StatusMessage" for="StatusMessage"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-lg-12">
|
||||||
|
<form method="post">
|
||||||
|
<input type="hidden" asp-for="Id"/>
|
||||||
|
<div asp-validation-summary="ModelOnly" class="text-danger"></div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label asp-for="Title" class="control-label"></label>*
|
||||||
|
<input asp-for="Title" class="form-control"/>
|
||||||
|
<span asp-validation-for="Title" class="text-danger"></span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label asp-for="Amount" class="control-label"></label>*
|
||||||
|
<input type="number" step="any" asp-for="Amount" class="form-control"/>
|
||||||
|
<span asp-validation-for="Amount" class="text-danger"></span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label asp-for="Currency" class="control-label"></label>*
|
||||||
|
<input placeholder="BTC" asp-for="Currency" class="form-control"/>
|
||||||
|
<span asp-validation-for="Currency" class="text-danger"></span>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label asp-for="AllowCustomPaymentAmounts"></label>
|
||||||
|
<input asp-for="AllowCustomPaymentAmounts" type="checkbox" class="form-check"/>
|
||||||
|
<span asp-validation-for="AllowCustomPaymentAmounts" class="text-danger"></span>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
|
||||||
|
<label asp-for="StoreId" class="control-label"></label>
|
||||||
|
@if (string.IsNullOrEmpty(Model.Id))
|
||||||
|
{
|
||||||
|
<select asp-for="StoreId" asp-items="Model.Stores" class="form-control"></select>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<input type="hidden" asp-for="StoreId" value="@Model.StoreId"/>
|
||||||
|
<input type="text" class="form-control" value="@Model.Stores.Single(item => item.Value == Model.StoreId).Text" readonly/>
|
||||||
|
}
|
||||||
|
|
||||||
|
<span asp-validation-for="StoreId" class="text-danger"></span>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label asp-for="Email" class="control-label"></label>
|
||||||
|
<input type="email" asp-for="Email" class="form-control"></input>
|
||||||
|
<span asp-validation-for="Email" class="text-danger"></span>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label asp-for="ExpiryDate" class="control-label"></label>
|
||||||
|
<div class="input-group ">
|
||||||
|
<input asp-for="ExpiryDate" class="form-control datetime" min="today"/>
|
||||||
|
<div class="input-group-append only-for-js">
|
||||||
|
|
||||||
|
<button class="btn btn-secondary input-group-clear" type="button" title="Clear">
|
||||||
|
<span class=" fa fa-times"></span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<span asp-validation-for="ExpiryDate" class="text-danger"></span>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label asp-for="Description" class="control-label"></label>
|
||||||
|
<textarea asp-for="Description" class="form-control richtext"></textarea>
|
||||||
|
<span asp-validation-for="Description" class="text-danger"></span>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label asp-for="CustomCSSLink" class="control-label"></label>
|
||||||
|
<a href="https://docs.btcpayserver.org/development/theme#bootstrap-themes" target="_blank">
|
||||||
|
<span class="fa fa-question-circle-o" title="More information..."></span>
|
||||||
|
</a>
|
||||||
|
<input asp-for="CustomCSSLink" class="form-control"/>
|
||||||
|
<span asp-validation-for="CustomCSSLink" class="text-danger"></span>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label asp-for="EmbeddedCSS" class="control-label"></label>
|
||||||
|
<textarea asp-for="EmbeddedCSS" rows="10" cols="40" class="form-control"></textarea>
|
||||||
|
<span asp-validation-for="EmbeddedCSS" class="text-danger"></span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<button type="submit" class="btn btn-primary">Save</button>
|
||||||
|
@if (!string.IsNullOrEmpty(Model.Id))
|
||||||
|
{
|
||||||
|
<a class="btn btn-secondary" target="_blank" asp-action="ViewPaymentRequest" id="@Model.Id">View</a>
|
||||||
|
<a class="btn btn-secondary"
|
||||||
|
target="_blank"
|
||||||
|
asp-action="ListInvoices"
|
||||||
|
asp-controller="Invoice"
|
||||||
|
asp-route-searchterm="@($"orderid:{PaymentRequestRepository.GetOrderIdForPaymentRequest(Model.Id)}")">Invoices</a>
|
||||||
|
|
||||||
|
}
|
||||||
|
<a class="btn btn-secondary" target="_blank" asp-action="GetPaymentRequests">Back to list</a>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
@section Scripts {
|
||||||
|
|
||||||
|
<bundle name="wwwroot/bundles/payment-request-admin-bundle.min.js"></bundle>
|
||||||
|
<bundle name="wwwroot/bundles/payment-request-admin-bundle.min.css"></bundle>
|
||||||
|
}
|
88
BTCPayServer/Views/PaymentRequest/GetPaymentRequests.cshtml
Normal file
88
BTCPayServer/Views/PaymentRequest/GetPaymentRequests.cshtml
Normal file
|
@ -0,0 +1,88 @@
|
||||||
|
@using BTCPayServer.Services.PaymentRequests
|
||||||
|
@model BTCPayServer.Models.PaymentRequestViewModels.ListPaymentRequestsViewModel
|
||||||
|
|
||||||
|
@{
|
||||||
|
Layout = "_Layout";
|
||||||
|
}
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<div class="container">
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-lg-12 text-center">
|
||||||
|
<partial name="_StatusMessage" for="StatusMessage"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-lg-12 text-center">
|
||||||
|
<h2 class="section-heading">Payment Requests</h2>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row no-gutter" style="margin-bottom: 5px;">
|
||||||
|
<div class="col-lg-6">
|
||||||
|
<a asp-action="EditPaymentRequest" class="btn btn-primary" role="button"><span class="fa fa-plus"></span> Create a new payment request</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<table class="table table-sm table-responsive-md">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Title</th>
|
||||||
|
<th>Expiry</th>
|
||||||
|
<th class="text-right">Price</th>
|
||||||
|
<th class="text-right">Status</th>
|
||||||
|
<th class="text-right">Actions</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
@foreach (var item in Model.Items)
|
||||||
|
{
|
||||||
|
<tr>
|
||||||
|
<td>@item.Title</td>
|
||||||
|
<td>@(item.ExpiryDate?.ToString("g") ?? "No Expiry")</td>
|
||||||
|
<td class="text-right">@item.Amount @item.Currency</td>
|
||||||
|
<td class="text-right">@item.Status</td>
|
||||||
|
<td class="text-right">
|
||||||
|
<a asp-action="EditPaymentRequest" asp-route-id="@item.Id">Edit</a>
|
||||||
|
<span> - </span>
|
||||||
|
<a asp-action="ViewPaymentRequest" asp-route-id="@item.Id">View</a>
|
||||||
|
<span> - </span>
|
||||||
|
<a target="_blank" asp-action="ListInvoices" asp-controller="Invoice" asp-route-searchterm="@($"orderid:{PaymentRequestRepository.GetOrderIdForPaymentRequest(item.Id)}")">Invoices</a>
|
||||||
|
<span> - </span>
|
||||||
|
<a target="_blank" asp-action="PayPaymentRequest" asp-route-id="@item.Id">Pay</a>
|
||||||
|
<span> - </span>
|
||||||
|
<a asp-action="RemovePaymentRequestPrompt" asp-route-id="@item.Id">Remove</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<nav aria-label="...">
|
||||||
|
<ul class="pagination">
|
||||||
|
<li class="page-item @(Model.Skip == 0 ? "disabled" : null)">
|
||||||
|
<a class="page-link" tabindex="-1" href="@Url.Action("GetPaymentRequests", new
|
||||||
|
{
|
||||||
|
skip = Math.Max(0, Model.Skip - Model.Count),
|
||||||
|
count = Model.Count,
|
||||||
|
})">Previous</a>
|
||||||
|
</li>
|
||||||
|
<li class="page-item disabled">
|
||||||
|
<span class="page-link">@(Model.Skip + 1) to @(Model.Skip + Model.Count) of @Model.Total</span>
|
||||||
|
</li>
|
||||||
|
<li class="page-item @(Model.Total > (Model.Skip + Model.Count) ? null : "disabled")">
|
||||||
|
<a class="page-link" href="@Url.Action("GetPaymentRequests", new
|
||||||
|
{
|
||||||
|
skip = Model.Skip + Model.Count,
|
||||||
|
count = Model.Count,
|
||||||
|
})">Next</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</section>
|
163
BTCPayServer/Views/PaymentRequest/MinimalPaymentRequest.cshtml
Normal file
163
BTCPayServer/Views/PaymentRequest/MinimalPaymentRequest.cshtml
Normal file
|
@ -0,0 +1,163 @@
|
||||||
|
@model BTCPayServer.Models.PaymentRequestViewModels.ViewPaymentRequestViewModel
|
||||||
|
|
||||||
|
<div class="container">
|
||||||
|
<div class="row w-100 p-0 m-0" style="height: 100vh">
|
||||||
|
<div class="mx-auto my-auto w-100">
|
||||||
|
<div class="card">
|
||||||
|
<h1 class="card-header">
|
||||||
|
@Model.Title
|
||||||
|
<span class="text-muted float-right text-center">@Model.Status</span>
|
||||||
|
</h1>
|
||||||
|
<div class="card-body px-0 pt-0">
|
||||||
|
<div class="row mb-4">
|
||||||
|
<div class="col-sm-12 col-md-12 col-lg-6 ">
|
||||||
|
<ul class="w-100 list-group list-group-flush">
|
||||||
|
<li class="list-group-item">
|
||||||
|
<div class="d-flex justify-content-between">
|
||||||
|
<span class="h2 text-muted">Request amount:</span>
|
||||||
|
<span class="h2">@Model.AmountFormatted</span>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
<li class="list-group-item">
|
||||||
|
<div class="d-flex justify-content-between">
|
||||||
|
<span class="h2 text-muted">Paid so far:</span>
|
||||||
|
<span class="h2">@Model.AmountCollectedFormatted</span>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
<li class="list-group-item">
|
||||||
|
<div class="d-flex justify-content-between">
|
||||||
|
<span class="h2 text-muted">Amount due:</span>
|
||||||
|
<span class="h2">@Model.AmountDueFormatted</span>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<div class="w-100 p-2">@Html.Raw(Model.Description)</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<div class="col-sm-12 col-md-12 col-lg-6">
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table border-top-0 ">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th class=" border-top-0" scope="col">Invoice #</th>
|
||||||
|
<th class=" border-top-0">Price</th>
|
||||||
|
<th class=" border-top-0">Expiry</th>
|
||||||
|
<th class=" border-top-0">Status</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
@if (Model.Invoices == null && !Model.Invoices.Any())
|
||||||
|
{
|
||||||
|
<tr>
|
||||||
|
<td colspan="4" class="text-center">No payments made yet</td>
|
||||||
|
</tr>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
foreach (var invoice in Model.Invoices)
|
||||||
|
{
|
||||||
|
<tr class="bg-light">
|
||||||
|
<td scope="row">@invoice.Id</td>
|
||||||
|
<td>@invoice.Amount @invoice.Currency</td>
|
||||||
|
<td>@invoice.ExpiryDate.ToString("g")</td>
|
||||||
|
<td>@invoice.Status</td>
|
||||||
|
</tr>
|
||||||
|
if (invoice.Payments != null && invoice.Payments.Any())
|
||||||
|
{
|
||||||
|
<tr class="bg-light">
|
||||||
|
<td colspan="4" class=" px-2 py-1 border-top-0">
|
||||||
|
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-bordered">
|
||||||
|
<tr>
|
||||||
|
<th class="p-1" style="max-width: 300px">Tx Id</th>
|
||||||
|
<th class="p-1">Payment Method</th>
|
||||||
|
<th class="p-1">Amount</th>
|
||||||
|
<th class="p-1">Link</th>
|
||||||
|
</tr>
|
||||||
|
@foreach (var payment in invoice.Payments)
|
||||||
|
{
|
||||||
|
<tr class="d-flex">
|
||||||
|
<td class="p-1 m-0 d-print-none d-block" style="max-width: 300px">
|
||||||
|
<div style="width: 100%; overflow-x: auto; overflow-wrap: initial;">@payment.Id</div>
|
||||||
|
</td>
|
||||||
|
<td class="p-1 m-0 d-none d-print-table-cell" style="max-width: 150px;">
|
||||||
|
@payment.Id
|
||||||
|
</td>
|
||||||
|
<td class="p-1">@payment.PaymentMethod</td>
|
||||||
|
<td class="p-1">@payment.Amount</td>
|
||||||
|
<td class="p-1 d-print-none">
|
||||||
|
@if (!string.IsNullOrEmpty(payment.Link))
|
||||||
|
{
|
||||||
|
<a :href="@payment.Link" target="_blank">Link</a>
|
||||||
|
}
|
||||||
|
</td>
|
||||||
|
<td class="p-1 d-none d-print-table-cell" style="max-width: 150px;">
|
||||||
|
@payment.Link
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
}
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@if (Model.IsPending)
|
||||||
|
{
|
||||||
|
<tr>
|
||||||
|
<td colspan="4" class="text-center">
|
||||||
|
@if (Model.AllowCustomPaymentAmounts && !Model.AnyPendingInvoice)
|
||||||
|
{
|
||||||
|
<form method="get" asp-action="PayPaymentRequest">
|
||||||
|
|
||||||
|
<div class="input-group m-auto" style="max-width: 250px">
|
||||||
|
<input
|
||||||
|
class="form-control"
|
||||||
|
type="number"
|
||||||
|
name="amount"
|
||||||
|
|
||||||
|
value="@Model.AmountDue"
|
||||||
|
max="@Model.AmountDue"
|
||||||
|
step="any"
|
||||||
|
placeholder="Amount"
|
||||||
|
required>
|
||||||
|
<div class="input-group-append">
|
||||||
|
<span class='input-group-text'>@Model.Currency.ToUpper()</span>
|
||||||
|
<button
|
||||||
|
class="btn btn-primary"
|
||||||
|
type="submit">
|
||||||
|
Pay now
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<a class="btn btn-primary btn-lg d-print-none" asp-action="PayPaymentRequest">
|
||||||
|
Pay now
|
||||||
|
</a>
|
||||||
|
}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<div class="card-footer text-muted d-flex justify-content-between">
|
||||||
|
|
||||||
|
<div >Updated @Model.LastUpdated.ToString("g")</div>
|
||||||
|
<div >
|
||||||
|
<span class="text-muted">Powered by </span><a href="https://btcpayserver.org" target="_blank">BTCPay Server</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
213
BTCPayServer/Views/PaymentRequest/ViewPaymentRequest.cshtml
Normal file
213
BTCPayServer/Views/PaymentRequest/ViewPaymentRequest.cshtml
Normal file
|
@ -0,0 +1,213 @@
|
||||||
|
@model BTCPayServer.Models.PaymentRequestViewModels.ViewPaymentRequestViewModel
|
||||||
|
|
||||||
|
@addTagHelper *, Meziantou.AspNetCore.BundleTagHelpers
|
||||||
|
@inject BTCPayServer.HostedServices.CssThemeManager themeManager
|
||||||
|
@{
|
||||||
|
ViewData["Title"] = Model.Title;
|
||||||
|
Layout = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html class="h-100">
|
||||||
|
<head>
|
||||||
|
<title>@Model.Title</title>
|
||||||
|
<meta charset="utf-8"/>
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||||
|
<link href="@Context.Request.GetRelativePathOrAbsolute(themeManager.BootstrapUri)" rel="stylesheet"/>
|
||||||
|
@if (Model.CustomCSSLink != null)
|
||||||
|
{
|
||||||
|
<link href="@Model.CustomCSSLink" rel="stylesheet"/>
|
||||||
|
}
|
||||||
|
@if (!Context.Request.Query.ContainsKey("simple"))
|
||||||
|
{
|
||||||
|
<script type="text/javascript">
|
||||||
|
var srvModel = @Html.Raw(Json.Serialize(Model));
|
||||||
|
</script>
|
||||||
|
<bundle name="wwwroot/bundles/payment-request-bundle-1.min.js"></bundle>
|
||||||
|
<bundle name="wwwroot/bundles/payment-request-bundle-2.min.js"></bundle>
|
||||||
|
}
|
||||||
|
|
||||||
|
<bundle name="wwwroot/bundles/payment-request-bundle.min.css"></bundle>
|
||||||
|
@if (!string.IsNullOrEmpty(Model.EmbeddedCSS))
|
||||||
|
{
|
||||||
|
<style>
|
||||||
|
@Html.Raw(Model.EmbeddedCSS);
|
||||||
|
</style>
|
||||||
|
}
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
@if (Context.Request.Query.ContainsKey("simple"))
|
||||||
|
{
|
||||||
|
@await Html.PartialAsync("MinimalPaymentRequest", Model)
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<noscript>
|
||||||
|
@await Html.PartialAsync("MinimalPaymentRequest", Model)
|
||||||
|
</noscript>
|
||||||
|
|
||||||
|
<div class="container" id="app" v-cloak>
|
||||||
|
<div class="row w-100 p-0 m-0" style="height: 100vh">
|
||||||
|
<div class="mx-auto my-auto w-100">
|
||||||
|
<div class="card">
|
||||||
|
<h1 class="card-header">
|
||||||
|
{{srvModel.title}}
|
||||||
|
|
||||||
|
<span class="text-muted float-right text-center">
|
||||||
|
<template v-if="settled">Settled</template>
|
||||||
|
<template v-else>
|
||||||
|
<template v-if="ended">Request Expired</template>
|
||||||
|
<template v-else-if="endDiff">Expires in {{endDiff}}</template>
|
||||||
|
</template>
|
||||||
|
</span>
|
||||||
|
</h1>
|
||||||
|
<div class="card-body px-0 pt-0">
|
||||||
|
<div class="row mb-4">
|
||||||
|
<div class="col-sm-12 col-md-12 col-lg-6 ">
|
||||||
|
<ul class="w-100 list-group list-group-flush">
|
||||||
|
<li class="list-group-item">
|
||||||
|
<div class="d-flex justify-content-between">
|
||||||
|
<span class="h2 text-muted">Request amount:</span>
|
||||||
|
<span class="h2">{{srvModel.amountFormatted}}</span>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
<li class="list-group-item">
|
||||||
|
<div class="d-flex justify-content-between">
|
||||||
|
<span class="h2 text-muted">Paid so far:</span>
|
||||||
|
<span class="h2">{{srvModel.amountCollectedFormatted}}</span>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
<li class="list-group-item">
|
||||||
|
<div class="d-flex justify-content-between">
|
||||||
|
<span class="h2 text-muted">Amount due:</span>
|
||||||
|
<span class="h2">{{srvModel.amountDueFormatted}}</span>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<div v-html="srvModel.description" class="w-100 p-2"></div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<div class="col-sm-12 col-md-12 col-lg-6">
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table border-top-0 ">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th class=" border-top-0" scope="col">Invoice #</th>
|
||||||
|
<th class=" border-top-0">Price</th>
|
||||||
|
<th class=" border-top-0">Expiry</th>
|
||||||
|
<th class=" border-top-0">Status</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr v-if="!srvModel.invoices || srvModel.invoices.length == 0">
|
||||||
|
<td colspan="4" class="text-center">No payments made yet</td>
|
||||||
|
</tr>
|
||||||
|
<template v-else v-for="invoice of srvModel.invoices" :key="invoice.id">
|
||||||
|
<tr class="bg-light">
|
||||||
|
<td scope="row">{{invoice.id}}</td>
|
||||||
|
<td>{{invoice.amount}} {{invoice.currency}}</td>
|
||||||
|
<td>{{moment(invoice.expiryDate).format('L HH:mm')}}</td>
|
||||||
|
<td>{{invoice.status}}</td>
|
||||||
|
</tr>
|
||||||
|
<tr class="bg-light" v-if="invoice.payments && invoice.payments.length > 0">
|
||||||
|
<td colspan="4" class=" px-2 py-1 border-top-0">
|
||||||
|
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-bordered">
|
||||||
|
<tr>
|
||||||
|
<th class="p-1" style="max-width: 300px">Tx Id</th>
|
||||||
|
<th class="p-1">Payment Method</th>
|
||||||
|
<th class="p-1">Amount</th>
|
||||||
|
<th class="p-1">Link</th>
|
||||||
|
</tr>
|
||||||
|
<tr v-for="payment of invoice.payments">
|
||||||
|
<td class="p-1 m-0 d-print-none d-block" style="max-width: 300px">
|
||||||
|
<div style="width: 100%; overflow-x: auto; overflow-wrap: initial;">{{payment.id}}</div>
|
||||||
|
</td>
|
||||||
|
<td class="p-1 m-0 d-none d-print-table-cell" style="max-width: 150px;">
|
||||||
|
{{payment.id}}
|
||||||
|
</td>
|
||||||
|
<td class="p-1">{{formatPaymentMethod(payment.paymentMethod)}}</td>
|
||||||
|
<td class="p-1">{{payment.amount.noExponents()}}</td>
|
||||||
|
<td class="p-1 d-print-none">
|
||||||
|
<a v-if="payment.link" :href="payment.link" target="_blank">Link</a>
|
||||||
|
</td>
|
||||||
|
<td class="p-1 d-none d-print-table-cell" style="max-width: 150px;">
|
||||||
|
{{payment.link}}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</template>
|
||||||
|
<tr v-if="!ended && (srvModel.amountDue) > 0" class="d-print-none">
|
||||||
|
<td colspan="4" class="text-center">
|
||||||
|
|
||||||
|
<template v-if="srvModel.allowCustomPaymentAmounts && !srvModel.anyPendingInvoice">
|
||||||
|
<form v-on:submit="submitCustomAmountForm">
|
||||||
|
|
||||||
|
<div class="input-group m-auto" style="max-width: 250px">
|
||||||
|
<input
|
||||||
|
:readonly="!srvModel.allowCustomPaymentAmounts"
|
||||||
|
class="form-control"
|
||||||
|
type="number"
|
||||||
|
v-model="customAmount"
|
||||||
|
:max="srvModel.amountDue"
|
||||||
|
step="any"
|
||||||
|
placeholder="Amount"
|
||||||
|
required>
|
||||||
|
<div class="input-group-append">
|
||||||
|
<span class='input-group-text'>{{currency}}</span>
|
||||||
|
<button
|
||||||
|
class="btn btn-primary"
|
||||||
|
v-bind:class="{ 'btn-disabled': loading}"
|
||||||
|
:disabled="loading"
|
||||||
|
type="submit">
|
||||||
|
<div v-if="loading" class="spinner-grow spinner-grow-sm" role="status">
|
||||||
|
<span class="sr-only">Loading...</span>
|
||||||
|
</div>
|
||||||
|
Pay now
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
|
||||||
|
<button v-else class="btn btn-primary btn-lg " v-on:click="pay(null)"
|
||||||
|
:disabled="loading">
|
||||||
|
<div v-if="loading" class="spinner-grow spinner-grow-sm" role="status">
|
||||||
|
<span class="sr-only">Loading...</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
Pay now
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<div class="card-footer text-muted d-flex justify-content-between">
|
||||||
|
|
||||||
|
<div >
|
||||||
|
<span v-on:click="print" class="btn-link d-print-none" style="cursor: pointer"> <span class="fa fa-print"></span> Print</span>
|
||||||
|
<span>Updated {{lastUpdated}}</span>
|
||||||
|
</div>
|
||||||
|
<div >
|
||||||
|
<span class="text-muted">Powered by </span><a href="https://btcpayserver.org" target="_blank">BTCPay Server</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</body>
|
||||||
|
</html>
|
|
@ -66,6 +66,7 @@
|
||||||
<li class="nav-item"><a asp-area="" asp-controller="Apps" asp-action="ListApps" class="nav-link js-scroll-trigger">Apps</a></li>
|
<li class="nav-item"><a asp-area="" asp-controller="Apps" asp-action="ListApps" class="nav-link js-scroll-trigger">Apps</a></li>
|
||||||
<li class="nav-item"><a asp-area="" asp-controller="Wallets" asp-action="ListWallets" class="nav-link js-scroll-trigger">Wallets</a></li>
|
<li class="nav-item"><a asp-area="" asp-controller="Wallets" asp-action="ListWallets" class="nav-link js-scroll-trigger">Wallets</a></li>
|
||||||
<li class="nav-item"><a asp-area="" asp-controller="Invoice" asp-action="ListInvoices" class="nav-link js-scroll-trigger">Invoices</a></li>
|
<li class="nav-item"><a asp-area="" asp-controller="Invoice" asp-action="ListInvoices" class="nav-link js-scroll-trigger">Invoices</a></li>
|
||||||
|
<li class="nav-item"><a asp-area="" asp-controller="PaymentRequest" asp-action="GetPaymentRequests" class="nav-link js-scroll-trigger">Payment Requests</a></li>
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a asp-area="" asp-controller="Manage" asp-action="Index" title="Manage" class="nav-link js-scroll-trigger">My settings</a>
|
<a asp-area="" asp-controller="Manage" asp-action="Index" title="Manage" class="nav-link js-scroll-trigger">My settings</a>
|
||||||
</li>
|
</li>
|
||||||
|
|
|
@ -124,5 +124,54 @@
|
||||||
"wwwroot/vendor/bootstrap-vue/bootstrap-vue.css",
|
"wwwroot/vendor/bootstrap-vue/bootstrap-vue.css",
|
||||||
"wwwroot/crowdfund/**/*.css"
|
"wwwroot/crowdfund/**/*.css"
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
"outputFileName": "wwwroot/bundles/payment-request-admin-bundle.min.js",
|
||||||
|
"inputFiles": [
|
||||||
|
"wwwroot/vendor/summernote/summernote-bs4.js",
|
||||||
|
"wwwroot/vendor/flatpickr/flatpickr.js",
|
||||||
|
"wwwroot/payment-request-admin/**/*.js"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
"outputFileName": "wwwroot/bundles/payment-request-admin-bundle.min.css",
|
||||||
|
"inputFiles": [
|
||||||
|
"wwwroot/vendor/summernote/summernote-bs4.css",
|
||||||
|
"wwwroot/vendor/flatpickr/flatpickr.min.css"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
"outputFileName": "wwwroot/bundles/payment-request-bundle-1.min.js",
|
||||||
|
"inputFiles": [
|
||||||
|
"wwwroot/vendor/vuejs/vue.min.js",
|
||||||
|
"wwwroot/vendor/babel-polyfill/polyfill.min.js",
|
||||||
|
"wwwroot/vendor/vue-toasted/vue-toasted.min.js",
|
||||||
|
"wwwroot/vendor/bootstrap-vue/bootstrap-vue.js",
|
||||||
|
"wwwroot/vendor/signalr/signalr.js",
|
||||||
|
"wwwroot/vendor/animejs/anime.min.js",
|
||||||
|
"wwwroot/modal/btcpay.js",
|
||||||
|
"wwwroot/payment-request/**/*.js"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
"outputFileName": "wwwroot/bundles/payment-request-bundle-2.min.js",
|
||||||
|
"inputFiles": [
|
||||||
|
"wwwroot/vendor/moment/moment.js"
|
||||||
|
],
|
||||||
|
"minify": {
|
||||||
|
"enabled": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"outputFileName": "wwwroot/bundles/payment-request-bundle.min.css",
|
||||||
|
"inputFiles": [
|
||||||
|
"wwwroot/vendor/font-awesome/css/font-awesome.min.css",
|
||||||
|
"wwwroot/vendor/bootstrap-vue/bootstrap-vue.css",
|
||||||
|
"wwwroot/payment-request/**/*.css"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
|
@ -1,8 +1,19 @@
|
||||||
hljs.initHighlightingOnLoad();
|
hljs.initHighlightingOnLoad();
|
||||||
$(document).ready(function() {
|
$(document).ready(function() {
|
||||||
|
|
||||||
$(".richtext").summernote();
|
$(".richtext").summernote({
|
||||||
$(".datetime").flatpickr({
|
minHeight: 300
|
||||||
enableTime: true
|
});
|
||||||
|
$(".datetime").each(function(){
|
||||||
|
var element = $(this);
|
||||||
|
var min = element.attr("min");
|
||||||
|
var max = element.attr("max");
|
||||||
|
var defaultDate = element.attr("value");
|
||||||
|
element.flatpickr({
|
||||||
|
enableTime: true,
|
||||||
|
minDate: min,
|
||||||
|
maxDate: max,
|
||||||
|
defaultDate: defaultDate
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -190,14 +190,14 @@ addLoadEvent(function (ev) {
|
||||||
var mDiffH = moment(this.srvModel.endDate).diff(moment(), "hours");
|
var mDiffH = moment(this.srvModel.endDate).diff(moment(), "hours");
|
||||||
var mDiffM = moment(this.srvModel.endDate).diff(moment(), "minutes");
|
var mDiffM = moment(this.srvModel.endDate).diff(moment(), "minutes");
|
||||||
var mDiffS = moment(this.srvModel.endDate).diff(moment(), "seconds");
|
var mDiffS = moment(this.srvModel.endDate).diff(moment(), "seconds");
|
||||||
this.endDiff = mDiffD > 0? mDiffD + " Days" : mDiffH> 0? mDiffH + " Hours" : mDiffM> 0? mDiffM+ " Minutes" : mDiffS> 0? mDiffS + " Seconds": "";
|
this.endDiff = mDiffD > 0? mDiffD + " days" : mDiffH> 0? mDiffH + " hours" : mDiffM> 0? mDiffM+ " minutes" : mDiffS> 0? mDiffS + " seconds": "";
|
||||||
}
|
}
|
||||||
if(!this.started && this.srvModel.startDate){
|
if(!this.started && this.srvModel.startDate){
|
||||||
var mDiffD = moment(this.srvModel.startDate).diff(moment(), "days");
|
var mDiffD = moment(this.srvModel.startDate).diff(moment(), "days");
|
||||||
var mDiffH = moment(this.srvModel.startDate).diff(moment(), "hours");
|
var mDiffH = moment(this.srvModel.startDate).diff(moment(), "hours");
|
||||||
var mDiffM = moment(this.srvModel.startDate).diff(moment(), "minutes");
|
var mDiffM = moment(this.srvModel.startDate).diff(moment(), "minutes");
|
||||||
var mDiffS = moment(this.srvModel.startDate).diff(moment(), "seconds");
|
var mDiffS = moment(this.srvModel.startDate).diff(moment(), "seconds");
|
||||||
this.startDiff = mDiffD > 0? mDiffD + " Days" : mDiffH> 0? mDiffH + " Hours" : mDiffM> 0? mDiffM+ " Minutes" : mDiffS> 0? mDiffS + " Seconds": "";
|
this.startDiff = mDiffD > 0? mDiffD + " days" : mDiffH> 0? mDiffH + " hours" : mDiffM> 0? mDiffM+ " minutes" : mDiffS> 0? mDiffS + " seconds": "";
|
||||||
}
|
}
|
||||||
this.lastUpdated = moment(this.srvModel.info.lastUpdated).calendar();
|
this.lastUpdated = moment(this.srvModel.info.lastUpdated).calendar();
|
||||||
this.active = this.started && !this.ended;
|
this.active = this.started && !this.ended;
|
||||||
|
|
|
@ -14,3 +14,8 @@
|
||||||
width: 100%;
|
width: 100%;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.only-for-js{
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
|
@ -6,4 +6,11 @@
|
||||||
var dateString = localDate.toLocaleDateString() + " " + localDate.toLocaleTimeString();
|
var dateString = localDate.toLocaleDateString() + " " + localDate.toLocaleTimeString();
|
||||||
$(this).text(dateString);
|
$(this).text(dateString);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
$(".input-group-clear").on("click", function(){
|
||||||
|
$(this).parents(".input-group").find("input").val(null);
|
||||||
|
});
|
||||||
|
|
||||||
|
$(".only-for-js").show();
|
||||||
});
|
});
|
||||||
|
|
19
BTCPayServer/wwwroot/payment-request-admin/main.js
Normal file
19
BTCPayServer/wwwroot/payment-request-admin/main.js
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
$(document).ready(function() {
|
||||||
|
|
||||||
|
$(".richtext").summernote({
|
||||||
|
minHeight: 300
|
||||||
|
});
|
||||||
|
$(".datetime").each(function(){
|
||||||
|
var element = $(this);
|
||||||
|
var min = element.attr("min");
|
||||||
|
var max = element.attr("max");
|
||||||
|
var defaultDate = element.attr("value");
|
||||||
|
element.flatpickr({
|
||||||
|
enableTime: true,
|
||||||
|
minDate: min,
|
||||||
|
maxDate: max,
|
||||||
|
defaultDate: defaultDate
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
176
BTCPayServer/wwwroot/payment-request/app.js
Normal file
176
BTCPayServer/wwwroot/payment-request/app.js
Normal file
|
@ -0,0 +1,176 @@
|
||||||
|
var app = null;
|
||||||
|
var eventAggregator = new Vue();
|
||||||
|
|
||||||
|
function addLoadEvent(func) {
|
||||||
|
var oldonload = window.onload;
|
||||||
|
if (typeof window.onload != 'function') {
|
||||||
|
window.onload = func;
|
||||||
|
} else {
|
||||||
|
window.onload = function () {
|
||||||
|
if (oldonload) {
|
||||||
|
oldonload();
|
||||||
|
}
|
||||||
|
func();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
addLoadEvent(function (ev) {
|
||||||
|
Vue.use(Toasted);
|
||||||
|
|
||||||
|
|
||||||
|
app = new Vue({
|
||||||
|
el: '#app',
|
||||||
|
data: function () {
|
||||||
|
return {
|
||||||
|
srvModel: window.srvModel,
|
||||||
|
connectionStatus: "",
|
||||||
|
endDate: "",
|
||||||
|
endDateRelativeTime: "",
|
||||||
|
ended: false,
|
||||||
|
endDiff: "",
|
||||||
|
active: true,
|
||||||
|
lastUpdated: "",
|
||||||
|
loading: false,
|
||||||
|
timeoutState: "",
|
||||||
|
customAmount: null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
currency: function () {
|
||||||
|
return this.srvModel.currency.toUpperCase();
|
||||||
|
},
|
||||||
|
settled: function () {
|
||||||
|
return this.srvModel.amountDue <= 0;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
updateComputed: function () {
|
||||||
|
if (this.srvModel.expiryDate) {
|
||||||
|
var endDateM = moment(this.srvModel.expiryDate);
|
||||||
|
this.endDate = endDateM.format('MMMM Do YYYY');
|
||||||
|
this.endDateRelativeTime = endDateM.fromNow();
|
||||||
|
this.ended = endDateM.isBefore(moment());
|
||||||
|
|
||||||
|
} else {
|
||||||
|
this.ended = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.ended && this.srvModel.expiryDate) {
|
||||||
|
var mDiffD = moment(this.srvModel.expiryDate).diff(moment(), "days");
|
||||||
|
var mDiffH = moment(this.srvModel.expiryDate).diff(moment(), "hours");
|
||||||
|
var mDiffM = moment(this.srvModel.expiryDate).diff(moment(), "minutes");
|
||||||
|
var mDiffS = moment(this.srvModel.expiryDate).diff(moment(), "seconds");
|
||||||
|
this.endDiff = mDiffD > 0 ? mDiffD + " days" : mDiffH > 0 ? mDiffH + " hours" : mDiffM > 0 ? mDiffM + " minutes" : mDiffS > 0 ? mDiffS + " seconds" : "";
|
||||||
|
}
|
||||||
|
|
||||||
|
this.lastUpdated = moment(this.srvModel.lastUpdated).calendar();
|
||||||
|
this.active = !this.ended;
|
||||||
|
setTimeout(this.updateComputed, 1000);
|
||||||
|
},
|
||||||
|
setLoading: function (val) {
|
||||||
|
this.loading = val;
|
||||||
|
if (this.timeoutState) {
|
||||||
|
clearTimeout(this.timeoutState);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
pay: function (amount) {
|
||||||
|
this.setLoading(true);
|
||||||
|
var self = this;
|
||||||
|
self.timeoutState = setTimeout(function () {
|
||||||
|
self.setLoading(false);
|
||||||
|
}, 5000);
|
||||||
|
|
||||||
|
eventAggregator.$emit("pay", amount);
|
||||||
|
},
|
||||||
|
formatPaymentMethod: function (str) {
|
||||||
|
|
||||||
|
if (str.endsWith("LightningLike")) {
|
||||||
|
return str.replace("LightningLike", "Lightning")
|
||||||
|
}
|
||||||
|
return str;
|
||||||
|
|
||||||
|
},
|
||||||
|
print:function(){
|
||||||
|
window.print();
|
||||||
|
},
|
||||||
|
submitCustomAmountForm : function(e){
|
||||||
|
if (e) {
|
||||||
|
e.preventDefault();
|
||||||
|
}
|
||||||
|
if(this.srvModel.allowCustomPaymentAmounts && parseFloat(this.customAmount) < this.srvModel.amountDue){
|
||||||
|
this.pay(parseFloat(this.customAmount));
|
||||||
|
}else{
|
||||||
|
this.pay();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted: function () {
|
||||||
|
|
||||||
|
this.customAmount = (this.srvModel.amountDue || 0).noExponents();
|
||||||
|
hubListener.connect();
|
||||||
|
var self = this;
|
||||||
|
eventAggregator.$on("invoice-created", function (invoiceId) {
|
||||||
|
self.setLoading(false);
|
||||||
|
btcpay.setApiUrlPrefix(window.location.origin);
|
||||||
|
btcpay.showInvoice(invoiceId);
|
||||||
|
btcpay.showFrame();
|
||||||
|
});
|
||||||
|
eventAggregator.$on("invoice-error", function (error) {
|
||||||
|
self.setLoading(false);
|
||||||
|
var msg = "";
|
||||||
|
if (typeof error === "string") {
|
||||||
|
msg = error;
|
||||||
|
} else if (!error) {
|
||||||
|
msg = "Unknown Error";
|
||||||
|
} else {
|
||||||
|
msg = JSON.stringify(error);
|
||||||
|
}
|
||||||
|
|
||||||
|
Vue.toasted.show("Error creating invoice: " + msg, {
|
||||||
|
iconPack: "fontawesome",
|
||||||
|
icon: "exclamation-triangle",
|
||||||
|
fullWidth: false,
|
||||||
|
theme: "bubble",
|
||||||
|
type: "error",
|
||||||
|
position: "top-center",
|
||||||
|
duration: 10000
|
||||||
|
});
|
||||||
|
});
|
||||||
|
eventAggregator.$on("payment-received", function (amount, cryptoCode, type) {
|
||||||
|
var onChain = type.toLowerCase() === "btclike";
|
||||||
|
amount = parseFloat(amount).noExponents();
|
||||||
|
if (onChain) {
|
||||||
|
Vue.toasted.show('New payment of ' + amount + " " + cryptoCode + " " + (onChain ? "On Chain" : "LN "), {
|
||||||
|
iconPack: "fontawesome",
|
||||||
|
icon: "plus",
|
||||||
|
duration: 10000
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
Vue.toasted.show('New payment of ' + amount + " " + cryptoCode + " " + (onChain ? "On Chain" : "LN "), {
|
||||||
|
iconPack: "fontawesome",
|
||||||
|
icon: "bolt",
|
||||||
|
duration: 10000
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
});
|
||||||
|
eventAggregator.$on("info-updated", function (model) {
|
||||||
|
console.warn("UPDATED", self.srvModel, arguments);
|
||||||
|
self.srvModel = model;
|
||||||
|
});
|
||||||
|
eventAggregator.$on("connection-pending", function () {
|
||||||
|
self.connectionStatus = "pending";
|
||||||
|
});
|
||||||
|
eventAggregator.$on("connection-failed", function () {
|
||||||
|
self.connectionStatus = "failed";
|
||||||
|
});
|
||||||
|
eventAggregator.$on("connection-lost", function () {
|
||||||
|
self.connectionStatus = "connection lost";
|
||||||
|
});
|
||||||
|
this.updateComputed();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
17
BTCPayServer/wwwroot/payment-request/helpers/math.js
Normal file
17
BTCPayServer/wwwroot/payment-request/helpers/math.js
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
Number.prototype.noExponents= function(){
|
||||||
|
var data= String(this).split(/[eE]/);
|
||||||
|
if(data.length== 1) return data[0];
|
||||||
|
|
||||||
|
var z= '', sign= this<0? '-':'',
|
||||||
|
str= data[0].replace('.', ''),
|
||||||
|
mag= Number(data[1])+ 1;
|
||||||
|
|
||||||
|
if(mag<0){
|
||||||
|
z= sign + '0.';
|
||||||
|
while(mag++) z += '0';
|
||||||
|
return z + str.replace(/^\-/,'');
|
||||||
|
}
|
||||||
|
mag -= str.length;
|
||||||
|
while(mag--) z += '0';
|
||||||
|
return str + z;
|
||||||
|
};
|
48
BTCPayServer/wwwroot/payment-request/services/listener.js
Normal file
48
BTCPayServer/wwwroot/payment-request/services/listener.js
Normal file
|
@ -0,0 +1,48 @@
|
||||||
|
var hubListener = function () {
|
||||||
|
|
||||||
|
var connection = new signalR.HubConnectionBuilder().withUrl("/payment-requests/hub").build();
|
||||||
|
|
||||||
|
connection.onclose(function () {
|
||||||
|
eventAggregator.$emit("connection-lost");
|
||||||
|
console.error("Connection was closed. Attempting reconnect in 2s");
|
||||||
|
setTimeout(connect, 2000);
|
||||||
|
});
|
||||||
|
connection.on("PaymentReceived", function (amount, cryptoCode, type) {
|
||||||
|
eventAggregator.$emit("payment-received", amount, cryptoCode, type);
|
||||||
|
});
|
||||||
|
connection.on("InvoiceCreated", function (invoiceId) {
|
||||||
|
eventAggregator.$emit("invoice-created", invoiceId);
|
||||||
|
});
|
||||||
|
connection.on("InvoiceError", function (error) {
|
||||||
|
eventAggregator.$emit("invoice-error", error);
|
||||||
|
});
|
||||||
|
connection.on("InfoUpdated", function (model) {
|
||||||
|
eventAggregator.$emit("info-updated", model);
|
||||||
|
});
|
||||||
|
|
||||||
|
function connect() {
|
||||||
|
|
||||||
|
eventAggregator.$emit("connection-pending");
|
||||||
|
connection
|
||||||
|
.start()
|
||||||
|
.then(function () {
|
||||||
|
connection.invoke("ListenToPaymentRequest", srvModel.id);
|
||||||
|
|
||||||
|
})
|
||||||
|
.catch(function (err) {
|
||||||
|
eventAggregator.$emit("connection-failed");
|
||||||
|
console.error("Could not connect to backend. Retrying in 2s", err);
|
||||||
|
setTimeout(connect, 2000);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
eventAggregator.$on("pay", function (amount) {
|
||||||
|
connection.invoke("Pay", amount);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
return {
|
||||||
|
connect: connect
|
||||||
|
};
|
||||||
|
}();
|
||||||
|
|
7
BTCPayServer/wwwroot/payment-request/styles/main.css
Normal file
7
BTCPayServer/wwwroot/payment-request/styles/main.css
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
[v-cloak] > * {
|
||||||
|
display: none
|
||||||
|
}
|
||||||
|
|
||||||
|
[v-cloak]::before {
|
||||||
|
content: "loading…"
|
||||||
|
}
|
Loading…
Add table
Reference in a new issue