2020-07-22 13:58:41 +02:00
|
|
|
using System;
|
|
|
|
using System.Linq;
|
|
|
|
using System.Threading.Tasks;
|
2020-11-17 13:46:23 +01:00
|
|
|
using BTCPayServer.Abstractions.Constants;
|
2020-07-22 13:58:41 +02:00
|
|
|
using BTCPayServer.Client;
|
2020-07-24 08:13:21 +02:00
|
|
|
using BTCPayServer.Client.Models;
|
2020-07-22 13:58:41 +02:00
|
|
|
using BTCPayServer.Payments;
|
2020-12-10 15:34:50 +01:00
|
|
|
using BTCPayServer.Services;
|
2020-07-22 13:58:41 +02:00
|
|
|
using BTCPayServer.Services.Invoices;
|
|
|
|
using Microsoft.AspNetCore.Authorization;
|
|
|
|
using Microsoft.AspNetCore.Cors;
|
|
|
|
using Microsoft.AspNetCore.Mvc;
|
2020-12-10 15:34:50 +01:00
|
|
|
using Microsoft.AspNetCore.Routing;
|
2020-07-22 13:58:41 +02:00
|
|
|
using NBitcoin;
|
|
|
|
using CreateInvoiceRequest = BTCPayServer.Client.Models.CreateInvoiceRequest;
|
|
|
|
using InvoiceData = BTCPayServer.Client.Models.InvoiceData;
|
|
|
|
|
|
|
|
namespace BTCPayServer.Controllers.GreenField
|
|
|
|
{
|
|
|
|
[ApiController]
|
|
|
|
[Authorize(AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
|
|
|
[EnableCors(CorsPolicies.All)]
|
|
|
|
public class GreenFieldInvoiceController : Controller
|
|
|
|
{
|
|
|
|
private readonly InvoiceController _invoiceController;
|
|
|
|
private readonly InvoiceRepository _invoiceRepository;
|
2020-12-10 15:34:50 +01:00
|
|
|
private readonly LinkGenerator _linkGenerator;
|
2020-07-22 13:58:41 +02:00
|
|
|
|
2020-12-10 15:34:50 +01:00
|
|
|
public LanguageService LanguageService { get; }
|
|
|
|
|
|
|
|
public GreenFieldInvoiceController(InvoiceController invoiceController, InvoiceRepository invoiceRepository, LinkGenerator linkGenerator, LanguageService languageService)
|
2020-07-22 13:58:41 +02:00
|
|
|
{
|
|
|
|
_invoiceController = invoiceController;
|
|
|
|
_invoiceRepository = invoiceRepository;
|
2020-12-10 15:34:50 +01:00
|
|
|
_linkGenerator = linkGenerator;
|
|
|
|
LanguageService = languageService;
|
2020-07-22 13:58:41 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
[Authorize(Policy = Policies.CanViewInvoices,
|
|
|
|
AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
2020-07-24 12:46:46 +02:00
|
|
|
[HttpGet("~/api/v1/stores/{storeId}/invoices")]
|
|
|
|
public async Task<IActionResult> GetInvoices(string storeId, bool includeArchived = false)
|
2020-07-22 13:58:41 +02:00
|
|
|
{
|
|
|
|
var store = HttpContext.GetStoreData();
|
|
|
|
if (store == null)
|
|
|
|
{
|
|
|
|
return NotFound();
|
|
|
|
}
|
|
|
|
|
2020-07-24 08:13:21 +02:00
|
|
|
var invoices =
|
|
|
|
await _invoiceRepository.GetInvoices(new InvoiceQuery()
|
|
|
|
{
|
2020-08-25 07:33:00 +02:00
|
|
|
StoreId = new[] { store.Id },
|
|
|
|
IncludeArchived = includeArchived
|
2020-07-24 08:13:21 +02:00
|
|
|
});
|
2020-07-22 13:58:41 +02:00
|
|
|
|
|
|
|
return Ok(invoices.Select(ToModel));
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
[Authorize(Policy = Policies.CanViewInvoices,
|
|
|
|
AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
|
|
|
[HttpGet("~/api/v1/stores/{storeId}/invoices/{invoiceId}")]
|
|
|
|
public async Task<IActionResult> GetInvoice(string storeId, string invoiceId)
|
|
|
|
{
|
|
|
|
var store = HttpContext.GetStoreData();
|
|
|
|
if (store == null)
|
|
|
|
{
|
|
|
|
return NotFound();
|
|
|
|
}
|
|
|
|
|
|
|
|
var invoice = await _invoiceRepository.GetInvoice(invoiceId, true);
|
2021-03-06 05:25:40 +01:00
|
|
|
if (invoice?.StoreId != store.Id)
|
2020-07-22 13:58:41 +02:00
|
|
|
{
|
|
|
|
return NotFound();
|
|
|
|
}
|
|
|
|
|
|
|
|
return Ok(ToModel(invoice));
|
|
|
|
}
|
|
|
|
|
|
|
|
[Authorize(Policy = Policies.CanModifyStoreSettings,
|
|
|
|
AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
|
|
|
[HttpDelete("~/api/v1/stores/{storeId}/invoices/{invoiceId}")]
|
|
|
|
public async Task<IActionResult> ArchiveInvoice(string storeId, string invoiceId)
|
|
|
|
{
|
|
|
|
var store = HttpContext.GetStoreData();
|
|
|
|
if (store == null)
|
|
|
|
{
|
|
|
|
return NotFound();
|
|
|
|
}
|
|
|
|
|
2020-07-24 08:13:21 +02:00
|
|
|
await _invoiceRepository.ToggleInvoiceArchival(invoiceId, true, storeId);
|
2020-07-22 13:58:41 +02:00
|
|
|
return Ok();
|
|
|
|
}
|
|
|
|
|
2020-12-12 07:15:34 +01:00
|
|
|
[Authorize(Policy = Policies.CanModifyStoreSettings,
|
|
|
|
AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
|
|
|
[HttpPut("~/api/v1/stores/{storeId}/invoices/{invoiceId}")]
|
|
|
|
public async Task<IActionResult> UpdateInvoice(string storeId, string invoiceId, UpdateInvoiceRequest request)
|
|
|
|
{
|
|
|
|
var store = HttpContext.GetStoreData();
|
|
|
|
if (store == null)
|
|
|
|
{
|
|
|
|
return NotFound();
|
|
|
|
}
|
|
|
|
|
|
|
|
var result = await _invoiceRepository.UpdateInvoiceMetadata(invoiceId, storeId, request.Metadata);
|
|
|
|
if (result != null)
|
|
|
|
{
|
|
|
|
return Ok(ToModel(result));
|
|
|
|
}
|
|
|
|
|
|
|
|
return NotFound();
|
|
|
|
}
|
|
|
|
|
2020-07-22 13:58:41 +02:00
|
|
|
[Authorize(Policy = Policies.CanCreateInvoice,
|
|
|
|
AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
|
|
|
[HttpPost("~/api/v1/stores/{storeId}/invoices")]
|
|
|
|
public async Task<IActionResult> CreateInvoice(string storeId, CreateInvoiceRequest request)
|
|
|
|
{
|
|
|
|
var store = HttpContext.GetStoreData();
|
|
|
|
if (store == null)
|
|
|
|
{
|
|
|
|
return NotFound();
|
|
|
|
}
|
|
|
|
|
|
|
|
if (request.Amount < 0.0m)
|
|
|
|
{
|
|
|
|
ModelState.AddModelError(nameof(request.Amount), "The amount should be 0 or more.");
|
|
|
|
}
|
|
|
|
|
2020-07-24 12:46:46 +02:00
|
|
|
if (string.IsNullOrEmpty(request.Currency))
|
|
|
|
{
|
|
|
|
ModelState.AddModelError(nameof(request.Currency), "Currency is required");
|
|
|
|
}
|
2020-12-08 06:54:24 +01:00
|
|
|
request.Checkout = request.Checkout ?? new CreateInvoiceRequest.CheckoutOptions();
|
2020-07-22 13:58:41 +02:00
|
|
|
if (request.Checkout.PaymentMethods?.Any() is true)
|
|
|
|
{
|
|
|
|
for (int i = 0; i < request.Checkout.PaymentMethods.Length; i++)
|
|
|
|
{
|
|
|
|
if (!PaymentMethodId.TryParse(request.Checkout.PaymentMethods[i], out _))
|
|
|
|
{
|
|
|
|
request.AddModelError(invoiceRequest => invoiceRequest.Checkout.PaymentMethods[i],
|
|
|
|
"Invalid payment method", this);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-08-26 07:01:39 +02:00
|
|
|
if (request.Checkout.Expiration != null && request.Checkout.Expiration < TimeSpan.FromSeconds(30.0))
|
2020-07-22 13:58:41 +02:00
|
|
|
{
|
2020-08-26 07:01:39 +02:00
|
|
|
request.AddModelError(invoiceRequest => invoiceRequest.Checkout.Expiration,
|
|
|
|
"Expiration time must be at least 30 seconds", this);
|
2020-07-22 13:58:41 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
if (request.Checkout.PaymentTolerance != null &&
|
|
|
|
(request.Checkout.PaymentTolerance < 0 || request.Checkout.PaymentTolerance > 100))
|
|
|
|
{
|
|
|
|
request.AddModelError(invoiceRequest => invoiceRequest.Checkout.PaymentTolerance,
|
|
|
|
"PaymentTolerance can only be between 0 and 100 percent", this);
|
|
|
|
}
|
|
|
|
|
2020-12-10 15:34:50 +01:00
|
|
|
if (request.Checkout.DefaultLanguage != null)
|
|
|
|
{
|
|
|
|
var lang = LanguageService.FindBestMatch(request.Checkout.DefaultLanguage);
|
|
|
|
if (lang == null)
|
|
|
|
{
|
|
|
|
request.AddModelError(invoiceRequest => invoiceRequest.Checkout.DefaultLanguage,
|
|
|
|
"The requested defaultLang does not exists, Browse the ~/misc/lang page of your BTCPay Server instance to see the list of supported languages.", this);
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
// Ensure this is good case
|
|
|
|
request.Checkout.DefaultLanguage = lang.Code;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-07-22 13:58:41 +02:00
|
|
|
if (!ModelState.IsValid)
|
|
|
|
return this.CreateValidationError(ModelState);
|
|
|
|
|
2020-07-24 12:46:46 +02:00
|
|
|
try
|
|
|
|
{
|
2020-08-26 07:01:39 +02:00
|
|
|
var invoice = await _invoiceController.CreateInvoiceCoreRaw(request, store,
|
2020-07-24 12:46:46 +02:00
|
|
|
Request.GetAbsoluteUri(""));
|
|
|
|
return Ok(ToModel(invoice));
|
|
|
|
}
|
|
|
|
catch (BitpayHttpException e)
|
|
|
|
{
|
2020-07-27 10:43:35 +02:00
|
|
|
return this.CreateAPIError(null, e.Message);
|
2020-07-24 12:46:46 +02:00
|
|
|
}
|
2020-07-22 13:58:41 +02:00
|
|
|
}
|
|
|
|
|
2020-07-24 08:13:21 +02:00
|
|
|
[Authorize(Policy = Policies.CanModifyStoreSettings,
|
|
|
|
AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
2020-07-27 10:43:35 +02:00
|
|
|
[HttpPost("~/api/v1/stores/{storeId}/invoices/{invoiceId}/status")]
|
|
|
|
public async Task<IActionResult> MarkInvoiceStatus(string storeId, string invoiceId,
|
|
|
|
MarkInvoiceStatusRequest request)
|
2020-07-24 08:13:21 +02:00
|
|
|
{
|
|
|
|
var store = HttpContext.GetStoreData();
|
|
|
|
if (store == null)
|
|
|
|
{
|
|
|
|
return NotFound();
|
|
|
|
}
|
|
|
|
|
2020-07-24 09:40:37 +02:00
|
|
|
var invoice = await _invoiceRepository.GetInvoice(invoiceId, true);
|
|
|
|
if (invoice.StoreId != store.Id)
|
|
|
|
{
|
|
|
|
return NotFound();
|
|
|
|
}
|
|
|
|
|
2020-07-27 10:43:35 +02:00
|
|
|
if (!await _invoiceRepository.MarkInvoiceStatus(invoice.Id, request.Status))
|
2020-07-24 09:40:37 +02:00
|
|
|
{
|
2020-07-27 10:43:35 +02:00
|
|
|
ModelState.AddModelError(nameof(request.Status),
|
2021-02-23 13:18:16 +01:00
|
|
|
"Status can only be marked to invalid or settled within certain conditions.");
|
2020-07-24 09:40:37 +02:00
|
|
|
}
|
|
|
|
|
2020-07-27 10:43:35 +02:00
|
|
|
if (!ModelState.IsValid)
|
|
|
|
return this.CreateValidationError(ModelState);
|
|
|
|
|
|
|
|
return await GetInvoice(storeId, invoiceId);
|
|
|
|
}
|
|
|
|
|
|
|
|
[Authorize(Policy = Policies.CanModifyStoreSettings,
|
|
|
|
AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
|
|
|
[HttpPost("~/api/v1/stores/{storeId}/invoices/{invoiceId}/unarchive")]
|
|
|
|
public async Task<IActionResult> UnarchiveInvoice(string storeId, string invoiceId)
|
|
|
|
{
|
|
|
|
var store = HttpContext.GetStoreData();
|
|
|
|
if (store == null)
|
|
|
|
{
|
|
|
|
return NotFound();
|
|
|
|
}
|
|
|
|
|
|
|
|
var invoice = await _invoiceRepository.GetInvoice(invoiceId, true);
|
|
|
|
if (invoice.StoreId != store.Id)
|
|
|
|
{
|
|
|
|
return NotFound();
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!invoice.Archived)
|
|
|
|
{
|
|
|
|
return this.CreateAPIError("already-unarchived", "Invoice is already unarchived");
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (!ModelState.IsValid)
|
|
|
|
return this.CreateValidationError(ModelState);
|
|
|
|
|
|
|
|
await _invoiceRepository.ToggleInvoiceArchival(invoiceId, false, storeId);
|
|
|
|
return await GetInvoice(storeId, invoiceId);
|
|
|
|
}
|
2020-09-10 13:54:36 +02:00
|
|
|
|
|
|
|
[Authorize(Policy = Policies.CanViewInvoices,
|
|
|
|
AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
|
|
|
[HttpGet("~/api/v1/stores/{storeId}/invoices/{invoiceId}/payment-methods")]
|
|
|
|
public async Task<IActionResult> GetInvoicePaymentMethods(string storeId, string invoiceId)
|
|
|
|
{
|
|
|
|
var store = HttpContext.GetStoreData();
|
|
|
|
if (store == null)
|
|
|
|
{
|
|
|
|
return NotFound();
|
|
|
|
}
|
2020-07-27 10:43:35 +02:00
|
|
|
|
2020-09-10 13:54:36 +02:00
|
|
|
var invoice = await _invoiceRepository.GetInvoice(invoiceId, true);
|
2020-12-17 06:43:43 +01:00
|
|
|
if (invoice?.StoreId != store.Id)
|
2020-09-10 13:54:36 +02:00
|
|
|
{
|
|
|
|
return NotFound();
|
|
|
|
}
|
2020-07-27 10:43:35 +02:00
|
|
|
|
2020-09-10 13:54:36 +02:00
|
|
|
return Ok(ToPaymentMethodModels(invoice));
|
|
|
|
}
|
|
|
|
|
|
|
|
private InvoicePaymentMethodDataModel[] ToPaymentMethodModels(InvoiceEntity entity)
|
|
|
|
{
|
|
|
|
return entity.GetPaymentMethods().Select(
|
|
|
|
method =>
|
|
|
|
{
|
|
|
|
var accounting = method.Calculate();
|
|
|
|
var details = method.GetPaymentMethodDetails();
|
|
|
|
var payments = method.ParentEntity.GetPayments().Where(paymentEntity =>
|
|
|
|
paymentEntity.GetPaymentMethodId() == method.GetId());
|
|
|
|
|
|
|
|
return new InvoicePaymentMethodDataModel()
|
|
|
|
{
|
|
|
|
PaymentMethod = method.GetId().ToStringNormalized(),
|
|
|
|
Destination = details.GetPaymentDestination(),
|
|
|
|
Rate = method.Rate,
|
|
|
|
Due = accounting.Due.ToDecimal(MoneyUnit.BTC),
|
|
|
|
TotalPaid = accounting.Paid.ToDecimal(MoneyUnit.BTC),
|
|
|
|
PaymentMethodPaid = accounting.CryptoPaid.ToDecimal(MoneyUnit.BTC),
|
|
|
|
Amount = accounting.Due.ToDecimal(MoneyUnit.BTC),
|
|
|
|
NetworkFee = accounting.NetworkFee.ToDecimal(MoneyUnit.BTC),
|
|
|
|
PaymentLink =
|
|
|
|
method.GetId().PaymentType.GetPaymentLink(method.Network, details, accounting.Due,
|
|
|
|
Request.GetAbsoluteRoot()),
|
|
|
|
Payments = payments.Select(paymentEntity =>
|
|
|
|
{
|
|
|
|
var data = paymentEntity.GetCryptoPaymentData();
|
|
|
|
return new InvoicePaymentMethodDataModel.Payment()
|
|
|
|
{
|
|
|
|
Destination = data.GetDestination(),
|
|
|
|
Id = data.GetPaymentId(),
|
|
|
|
Status = !paymentEntity.Accounted
|
|
|
|
? InvoicePaymentMethodDataModel.Payment.PaymentStatus.Invalid
|
2020-10-27 09:49:35 +01:00
|
|
|
: data.PaymentConfirmed(paymentEntity, entity.SpeedPolicy) ||
|
|
|
|
data.PaymentCompleted(paymentEntity)
|
2020-11-23 07:57:05 +01:00
|
|
|
? InvoicePaymentMethodDataModel.Payment.PaymentStatus.Settled
|
|
|
|
: InvoicePaymentMethodDataModel.Payment.PaymentStatus.Processing,
|
2020-09-10 13:54:36 +02:00
|
|
|
Fee = paymentEntity.NetworkFee,
|
|
|
|
Value = data.GetValue(),
|
|
|
|
ReceivedDate = paymentEntity.ReceivedTime.DateTime
|
|
|
|
};
|
|
|
|
}).ToList()
|
|
|
|
};
|
|
|
|
}).ToArray();
|
|
|
|
}
|
2020-07-24 09:40:37 +02:00
|
|
|
private InvoiceData ToModel(InvoiceEntity entity)
|
2020-07-22 13:58:41 +02:00
|
|
|
{
|
|
|
|
return new InvoiceData()
|
|
|
|
{
|
2020-08-26 14:31:08 +02:00
|
|
|
ExpirationTime = entity.ExpirationTime,
|
|
|
|
MonitoringExpiration = entity.MonitoringExpiration,
|
|
|
|
CreatedTime = entity.InvoiceTime,
|
2020-08-25 07:33:00 +02:00
|
|
|
Amount = entity.Price,
|
2020-07-22 13:58:41 +02:00
|
|
|
Id = entity.Id,
|
2020-12-10 15:34:50 +01:00
|
|
|
CheckoutLink = _linkGenerator.CheckoutLink(entity.Id, Request.Scheme, Request.Host, Request.PathBase),
|
2020-11-23 07:57:05 +01:00
|
|
|
Status = entity.Status.ToModernStatus(),
|
2020-07-27 10:43:35 +02:00
|
|
|
AdditionalStatus = entity.ExceptionStatus,
|
2020-08-25 07:33:00 +02:00
|
|
|
Currency = entity.Currency,
|
|
|
|
Metadata = entity.Metadata.ToJObject(),
|
2020-07-22 13:58:41 +02:00
|
|
|
Checkout = new CreateInvoiceRequest.CheckoutOptions()
|
|
|
|
{
|
2020-08-26 07:01:39 +02:00
|
|
|
Expiration = entity.ExpirationTime - entity.InvoiceTime,
|
|
|
|
Monitoring = entity.MonitoringExpiration - entity.ExpirationTime,
|
2020-07-22 13:58:41 +02:00
|
|
|
PaymentTolerance = entity.PaymentTolerance,
|
|
|
|
PaymentMethods =
|
2020-08-26 14:24:37 +02:00
|
|
|
entity.GetPaymentMethods().Select(method => method.GetId().ToStringNormalized()).ToArray(),
|
2020-12-10 15:34:50 +01:00
|
|
|
SpeedPolicy = entity.SpeedPolicy,
|
2021-03-02 03:50:01 +01:00
|
|
|
DefaultLanguage = entity.DefaultLanguage,
|
|
|
|
RedirectAutomatically = entity.RedirectAutomatically,
|
|
|
|
RedirectURL = entity.RedirectURLTemplate
|
2020-08-26 13:51:51 +02:00
|
|
|
}
|
2020-07-22 13:58:41 +02:00
|
|
|
};
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|