btcpayserver/BTCPayServer/Services/Invoices/InvoiceRepository.cs

929 lines
38 KiB
C#
Raw Normal View History

2017-09-13 15:47:34 +09:00
using System;
using System.Collections.Generic;
2020-06-28 17:55:27 +09:00
using System.Globalization;
2017-09-13 15:47:34 +09:00
using System.Linq;
using System.Threading;
2017-09-13 15:47:34 +09:00
using System.Threading.Tasks;
2022-03-02 18:05:16 +01:00
using BTCPayServer.Abstractions.Extensions;
using BTCPayServer.Client.Models;
2020-06-28 17:55:27 +09:00
using BTCPayServer.Data;
2020-07-24 09:40:37 +02:00
using BTCPayServer.Events;
2018-01-18 18:33:26 +09:00
using BTCPayServer.Logging;
2020-06-28 17:55:27 +09:00
using BTCPayServer.Models.InvoicingModels;
using BTCPayServer.Payments;
2020-06-28 17:55:27 +09:00
using Microsoft.EntityFrameworkCore;
using NBitcoin;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
2019-12-24 08:20:44 +01:00
using Encoders = NBitcoin.DataEncoders.Encoders;
2020-07-22 13:58:41 +02:00
using InvoiceData = BTCPayServer.Data.InvoiceData;
2017-09-13 15:47:34 +09:00
2017-10-20 14:06:37 -05:00
namespace BTCPayServer.Services.Invoices
2017-09-13 15:47:34 +09:00
{
public class InvoiceRepository
{
internal static JsonSerializerSettings DefaultSerializerSettings;
static InvoiceRepository()
{
DefaultSerializerSettings = new JsonSerializerSettings();
NBitcoin.JsonConverters.Serializer.RegisterFrontConverters(DefaultSerializerSettings);
}
private readonly ApplicationDbContextFactory _applicationDbContextFactory;
2020-07-24 09:40:37 +02:00
private readonly EventAggregator _eventAggregator;
private readonly BTCPayNetworkProvider _btcPayNetworkProvider;
public InvoiceRepository(ApplicationDbContextFactory contextFactory,
BTCPayNetworkProvider networks, EventAggregator eventAggregator)
{
_applicationDbContextFactory = contextFactory;
_btcPayNetworkProvider = networks;
2020-07-24 09:40:37 +02:00
_eventAggregator = eventAggregator;
}
2020-11-13 14:01:51 +09:00
public async Task<Data.WebhookDeliveryData> GetWebhookDelivery(string invoiceId, string deliveryId)
2020-11-06 20:42:26 +09:00
{
using var ctx = _applicationDbContextFactory.CreateContext();
2020-11-06 20:42:26 +09:00
return await ctx.InvoiceWebhookDeliveries
.Where(d => d.InvoiceId == invoiceId && d.DeliveryId == deliveryId)
.Select(d => d.Delivery)
.FirstOrDefaultAsync();
}
public InvoiceEntity CreateNewInvoice(string storeId)
{
return new InvoiceEntity()
{
Id = Encoders.Base58.EncodeData(RandomUtils.GetBytes(16)),
StoreId = storeId,
Networks = _btcPayNetworkProvider,
Version = InvoiceEntity.Lastest_Version,
// Truncating was an unintended side effect of previous code. Might want to remove that one day
InvoiceTime = DateTimeOffset.UtcNow.TruncateMilliSeconds(),
Metadata = new InvoiceMetadata(),
#pragma warning disable CS0618
Payments = new List<PaymentEntity>()
#pragma warning restore CS0618
};
}
public async Task<bool> RemovePendingInvoice(string invoiceId)
{
2022-01-14 17:50:29 +09:00
using var ctx = _applicationDbContextFactory.CreateContext();
ctx.PendingInvoices.Remove(new PendingInvoiceData() { Id = invoiceId });
try
{
2022-01-14 17:50:29 +09:00
await ctx.SaveChangesAsync();
return true;
}
2022-01-14 17:50:29 +09:00
catch (DbUpdateException) { return false; }
}
public async Task<IEnumerable<InvoiceEntity>> GetInvoicesFromAddresses(string[] addresses)
{
2022-01-14 17:50:29 +09:00
using var db = _applicationDbContextFactory.CreateContext();
return (await db.AddressInvoices
.Include(a => a.InvoiceData.Payments)
#pragma warning disable CS0618
.Where(a => addresses.Contains(a.Address))
#pragma warning restore CS0618
.Select(a => a.InvoiceData)
2022-01-14 17:50:29 +09:00
.ToListAsync()).Select(ToEntity);
}
public async Task<InvoiceEntity[]> GetPendingInvoices(bool includeAddressData = false, bool skipNoPaymentInvoices = false, CancellationToken cancellationToken = default)
{
using var ctx = _applicationDbContextFactory.CreateContext();
var q = ctx.PendingInvoices.AsQueryable();
q = q.Include(o => o.InvoiceData)
.ThenInclude(o => o.Payments);
if (includeAddressData)
q = q.Include(o => o.InvoiceData)
.ThenInclude(o => o.AddressInvoices);
if (skipNoPaymentInvoices)
q = q.Where(i => i.InvoiceData.Payments.Any());
return (await q.Select(o => o.InvoiceData).ToArrayAsync(cancellationToken)).Select(ToEntity).ToArray();
}
public async Task<string[]> GetPendingInvoiceIds()
{
2022-01-14 17:50:29 +09:00
using var ctx = _applicationDbContextFactory.CreateContext();
return await ctx.PendingInvoices.AsQueryable().Select(data => data.Id).ToArrayAsync();
}
2020-11-13 14:01:51 +09:00
public async Task<List<Data.WebhookDeliveryData>> GetWebhookDeliveries(string invoiceId)
2020-11-06 20:42:26 +09:00
{
using var ctx = _applicationDbContextFactory.CreateContext();
2020-11-06 20:42:26 +09:00
return await ctx.InvoiceWebhookDeliveries
.Include(s => s.Delivery).ThenInclude(s => s.Webhook)
2020-11-06 20:42:26 +09:00
.Where(s => s.InvoiceId == invoiceId)
.Select(s => s.Delivery)
.OrderByDescending(s => s.Timestamp)
.ToListAsync();
}
public async Task<AppData[]> GetAppsTaggingStore(string storeId)
{
ArgumentNullException.ThrowIfNull(storeId);
2022-01-14 17:50:29 +09:00
using var ctx = _applicationDbContextFactory.CreateContext();
return await ctx.Apps.Where(a => a.StoreDataId == storeId && a.TagAllInvoices).ToArrayAsync();
}
public async Task UpdateInvoice(string invoiceId, UpdateCustomerModel data)
{
2022-01-14 17:50:29 +09:00
using var ctx = _applicationDbContextFactory.CreateContext();
var invoiceData = await ctx.Invoices.FindAsync(invoiceId).ConfigureAwait(false);
if (invoiceData == null)
return;
if (invoiceData.CustomerEmail == null && data.Email != null)
{
2022-01-14 17:50:29 +09:00
invoiceData.CustomerEmail = data.Email;
AddToTextSearch(ctx, invoiceData, invoiceData.CustomerEmail);
}
2022-01-14 17:50:29 +09:00
await ctx.SaveChangesAsync().ConfigureAwait(false);
}
Checkout v2 finetuning (#4276) * Indent all JSON files with two spaces * Upgrade Vue.js * Cheat mode improvements * Show payment details in case of expired invoice * Add logo size recommendation * Show clipboard copy hint cursor * Improve info area and wording * Update BIP21 wording * Invoice details adjustments * Remove form; switch payment methods via AJAX * UI updates * Decrease paddings to gain space * Tighten up padding between logo mark and the store title text * Add drop-shadow to the containers * Wording * Cheating improvements * Improve footer spacing * Cheating improvements * Display addresses * More improvements * Expire invoices * Customize invoice expiry * Footer improvements * Remove theme switch * Remove non-existing sourcemap references * Move inline JS to checkout.js file * Plugin compatibility See Kukks/btcpayserver#8 * Test fix * Upgrade vue-i18next * Extract translations into a separate file * Round QR code borders * Remove "Pay with Bitcoin" title in BIP21 case * Add copy hint to payment details * Cheating: Reduce margins * Adjust dt color * Hide addresses for first iteration * Improve View Details button * Make info section collapsible * Revert original en locale file * Checkout v2 tests * Result view link fixes * Fix BIP21 + lazy payment methods case * More result page link improvements * minor visual improvements * Update clipboard code Remove fallback for old browsers. https://caniuse.com/?search=navigator.clipboard * Transition copy symbol * Update info text color * Invert dark neutral colors Simplifies the dark theme quite a bit. * copy adjustments * updates QR border-radius * Add option to remove logo * More checkout v2 test cases * JS improvements * Remove leftovers * Update test * Fix links * Update tests * Update plugins integration * Remove obsolete url code * Minor view update * Update JS to not use arrow functions * Remove FormId from Checkout Appearance settings * Add English-only hint and feedback link * Checkout Appearance: Make options clearer, remove Custom CSS for v2 * Clipboard copy full URL instead of just address/BOLT11 * Upgrade JS libs, add content checks * Add test for BIP21 setting with zero amount invoice Co-authored-by: dstrukt <gfxdsign@gmail.com>
2022-11-24 00:53:32 +01:00
public async Task UpdateInvoiceExpiry(string invoiceId, TimeSpan seconds)
{
await using var ctx = _applicationDbContextFactory.CreateContext();
var invoiceData = await ctx.Invoices.FindAsync(invoiceId);
var invoice = invoiceData.GetBlob(_btcPayNetworkProvider);
var expiry = DateTimeOffset.Now + seconds;
invoice.ExpirationTime = expiry;
invoice.MonitoringExpiration = expiry.AddHours(1);
invoiceData.SetBlob(invoice);
Checkout v2 finetuning (#4276) * Indent all JSON files with two spaces * Upgrade Vue.js * Cheat mode improvements * Show payment details in case of expired invoice * Add logo size recommendation * Show clipboard copy hint cursor * Improve info area and wording * Update BIP21 wording * Invoice details adjustments * Remove form; switch payment methods via AJAX * UI updates * Decrease paddings to gain space * Tighten up padding between logo mark and the store title text * Add drop-shadow to the containers * Wording * Cheating improvements * Improve footer spacing * Cheating improvements * Display addresses * More improvements * Expire invoices * Customize invoice expiry * Footer improvements * Remove theme switch * Remove non-existing sourcemap references * Move inline JS to checkout.js file * Plugin compatibility See Kukks/btcpayserver#8 * Test fix * Upgrade vue-i18next * Extract translations into a separate file * Round QR code borders * Remove "Pay with Bitcoin" title in BIP21 case * Add copy hint to payment details * Cheating: Reduce margins * Adjust dt color * Hide addresses for first iteration * Improve View Details button * Make info section collapsible * Revert original en locale file * Checkout v2 tests * Result view link fixes * Fix BIP21 + lazy payment methods case * More result page link improvements * minor visual improvements * Update clipboard code Remove fallback for old browsers. https://caniuse.com/?search=navigator.clipboard * Transition copy symbol * Update info text color * Invert dark neutral colors Simplifies the dark theme quite a bit. * copy adjustments * updates QR border-radius * Add option to remove logo * More checkout v2 test cases * JS improvements * Remove leftovers * Update test * Fix links * Update tests * Update plugins integration * Remove obsolete url code * Minor view update * Update JS to not use arrow functions * Remove FormId from Checkout Appearance settings * Add English-only hint and feedback link * Checkout Appearance: Make options clearer, remove Custom CSS for v2 * Clipboard copy full URL instead of just address/BOLT11 * Upgrade JS libs, add content checks * Add test for BIP21 setting with zero amount invoice Co-authored-by: dstrukt <gfxdsign@gmail.com>
2022-11-24 00:53:32 +01:00
await ctx.SaveChangesAsync();
Checkout v2 finetuning (#4276) * Indent all JSON files with two spaces * Upgrade Vue.js * Cheat mode improvements * Show payment details in case of expired invoice * Add logo size recommendation * Show clipboard copy hint cursor * Improve info area and wording * Update BIP21 wording * Invoice details adjustments * Remove form; switch payment methods via AJAX * UI updates * Decrease paddings to gain space * Tighten up padding between logo mark and the store title text * Add drop-shadow to the containers * Wording * Cheating improvements * Improve footer spacing * Cheating improvements * Display addresses * More improvements * Expire invoices * Customize invoice expiry * Footer improvements * Remove theme switch * Remove non-existing sourcemap references * Move inline JS to checkout.js file * Plugin compatibility See Kukks/btcpayserver#8 * Test fix * Upgrade vue-i18next * Extract translations into a separate file * Round QR code borders * Remove "Pay with Bitcoin" title in BIP21 case * Add copy hint to payment details * Cheating: Reduce margins * Adjust dt color * Hide addresses for first iteration * Improve View Details button * Make info section collapsible * Revert original en locale file * Checkout v2 tests * Result view link fixes * Fix BIP21 + lazy payment methods case * More result page link improvements * minor visual improvements * Update clipboard code Remove fallback for old browsers. https://caniuse.com/?search=navigator.clipboard * Transition copy symbol * Update info text color * Invert dark neutral colors Simplifies the dark theme quite a bit. * copy adjustments * updates QR border-radius * Add option to remove logo * More checkout v2 test cases * JS improvements * Remove leftovers * Update test * Fix links * Update tests * Update plugins integration * Remove obsolete url code * Minor view update * Update JS to not use arrow functions * Remove FormId from Checkout Appearance settings * Add English-only hint and feedback link * Checkout Appearance: Make options clearer, remove Custom CSS for v2 * Clipboard copy full URL instead of just address/BOLT11 * Upgrade JS libs, add content checks * Add test for BIP21 setting with zero amount invoice Co-authored-by: dstrukt <gfxdsign@gmail.com>
2022-11-24 00:53:32 +01:00
_eventAggregator.Publish(new InvoiceDataChangedEvent(invoice));
_ = InvoiceNeedUpdateEventLater(invoiceId, seconds);
}
Checkout v2 finetuning (#4276) * Indent all JSON files with two spaces * Upgrade Vue.js * Cheat mode improvements * Show payment details in case of expired invoice * Add logo size recommendation * Show clipboard copy hint cursor * Improve info area and wording * Update BIP21 wording * Invoice details adjustments * Remove form; switch payment methods via AJAX * UI updates * Decrease paddings to gain space * Tighten up padding between logo mark and the store title text * Add drop-shadow to the containers * Wording * Cheating improvements * Improve footer spacing * Cheating improvements * Display addresses * More improvements * Expire invoices * Customize invoice expiry * Footer improvements * Remove theme switch * Remove non-existing sourcemap references * Move inline JS to checkout.js file * Plugin compatibility See Kukks/btcpayserver#8 * Test fix * Upgrade vue-i18next * Extract translations into a separate file * Round QR code borders * Remove "Pay with Bitcoin" title in BIP21 case * Add copy hint to payment details * Cheating: Reduce margins * Adjust dt color * Hide addresses for first iteration * Improve View Details button * Make info section collapsible * Revert original en locale file * Checkout v2 tests * Result view link fixes * Fix BIP21 + lazy payment methods case * More result page link improvements * minor visual improvements * Update clipboard code Remove fallback for old browsers. https://caniuse.com/?search=navigator.clipboard * Transition copy symbol * Update info text color * Invert dark neutral colors Simplifies the dark theme quite a bit. * copy adjustments * updates QR border-radius * Add option to remove logo * More checkout v2 test cases * JS improvements * Remove leftovers * Update test * Fix links * Update tests * Update plugins integration * Remove obsolete url code * Minor view update * Update JS to not use arrow functions * Remove FormId from Checkout Appearance settings * Add English-only hint and feedback link * Checkout Appearance: Make options clearer, remove Custom CSS for v2 * Clipboard copy full URL instead of just address/BOLT11 * Upgrade JS libs, add content checks * Add test for BIP21 setting with zero amount invoice Co-authored-by: dstrukt <gfxdsign@gmail.com>
2022-11-24 00:53:32 +01:00
async Task InvoiceNeedUpdateEventLater(string invoiceId, TimeSpan expirationIn)
{
await Task.Delay(expirationIn);
_eventAggregator.Publish(new InvoiceNeedUpdateEvent(invoiceId));
}
public async Task ExtendInvoiceMonitor(string invoiceId)
{
2022-01-14 17:50:29 +09:00
using var ctx = _applicationDbContextFactory.CreateContext();
var invoiceData = await ctx.Invoices.FindAsync(invoiceId);
2022-01-14 17:50:29 +09:00
var invoice = invoiceData.GetBlob(_btcPayNetworkProvider);
invoice.MonitoringExpiration = invoice.MonitoringExpiration.AddHours(1);
invoiceData.SetBlob(invoice);
2022-01-14 17:50:29 +09:00
await ctx.SaveChangesAsync();
}
public async Task CreateInvoiceAsync(InvoiceEntity invoice, string[] additionalSearchTerms = null)
{
var textSearch = new HashSet<string>();
using (var context = _applicationDbContextFactory.CreateContext())
{
var invoiceData = new InvoiceData
{
StoreDataId = invoice.StoreId,
Id = invoice.Id,
Created = invoice.InvoiceTime,
OrderId = invoice.Metadata.OrderId,
#pragma warning disable CS0618 // Type or member is obsolete
Status = invoice.StatusString,
#pragma warning restore CS0618 // Type or member is obsolete
ItemCode = invoice.Metadata.ItemCode,
2020-05-07 12:50:07 +02:00
CustomerEmail = invoice.RefundMail,
Archived = false
};
invoiceData.SetBlob(invoice);
await context.Invoices.AddAsync(invoiceData);
foreach (var paymentMethod in invoice.GetPaymentMethods())
{
if (paymentMethod.Network == null)
throw new InvalidOperationException("CryptoCode unsupported");
var details = paymentMethod.GetPaymentMethodDetails();
if (!details.Activated)
{
continue;
}
var paymentDestination = details.GetPaymentDestination();
string address = GetDestination(paymentMethod);
2023-04-25 08:51:38 +09:00
if (address != null)
{
2023-04-25 08:51:38 +09:00
await context.AddressInvoices.AddAsync(new AddressInvoiceData()
{
InvoiceDataId = invoice.Id,
CreatedTime = DateTimeOffset.UtcNow,
}.Set(address, paymentMethod.GetId()));
}
if (paymentDestination != null)
{
textSearch.Add(paymentDestination);
}
textSearch.Add(paymentMethod.Calculate().TotalDue.ToString());
}
await context.PendingInvoices.AddAsync(new PendingInvoiceData() { Id = invoice.Id });
textSearch.Add(invoice.Id);
textSearch.Add(invoice.InvoiceTime.ToString(CultureInfo.InvariantCulture));
2021-08-03 17:03:00 +09:00
if (!invoice.IsUnsetTopUp())
textSearch.Add(invoice.Price.ToString(CultureInfo.InvariantCulture));
textSearch.Add(invoice.Metadata.OrderId);
textSearch.Add(invoice.StoreId);
textSearch.Add(invoice.Metadata.BuyerEmail);
if (additionalSearchTerms != null)
{
textSearch.AddRange(additionalSearchTerms);
}
AddToTextSearch(context, invoiceData, textSearch.ToArray());
await context.SaveChangesAsync().ConfigureAwait(false);
}
2020-06-24 17:51:00 +09:00
}
public async Task AddInvoiceLogs(string invoiceId, InvoiceLogs logs)
{
await using var context = _applicationDbContextFactory.CreateContext();
foreach (var log in logs.ToList())
{
await context.InvoiceEvents.AddAsync(new InvoiceEventData()
{
Severity = log.Severity,
InvoiceDataId = invoiceId,
Message = log.Log,
Timestamp = log.Timestamp,
UniqueId = Encoders.Hex.EncodeData(RandomUtils.GetBytes(10))
});
}
await context.SaveChangesAsync().ConfigureAwait(false);
}
private string GetDestination(PaymentMethod paymentMethod)
{
2018-02-19 11:31:34 +09:00
// For legacy reason, BitcoinLikeOnChain is putting the hashes of addresses in database
if (paymentMethod.GetId().PaymentType == Payments.PaymentTypes.BTCLike)
{
var network = (BTCPayNetwork)paymentMethod.Network;
var details =
(Payments.Bitcoin.BitcoinLikeOnChainPaymentMethod)paymentMethod.GetPaymentMethodDetails();
if (!details.Activated)
{
return null;
}
return details.GetDepositAddress(network.NBitcoinNetwork).ScriptPubKey.Hash.ToString();
}
2018-02-19 11:31:34 +09:00
///////////////
return paymentMethod.GetPaymentMethodDetails().GetPaymentDestination();
}
public async Task<bool> NewPaymentDetails(string invoiceId, IPaymentMethodDetails paymentMethodDetails, BTCPayNetworkBase network)
{
await using var context = _applicationDbContextFactory.CreateContext();
var invoice = (await context.Invoices.Where(i => i.Id == invoiceId).ToListAsync()).FirstOrDefault();
if (invoice == null)
return false;
var invoiceEntity = invoice.GetBlob(_btcPayNetworkProvider);
var paymentMethod = invoiceEntity.GetPaymentMethod(network, paymentMethodDetails.GetPaymentType());
if (paymentMethod == null)
return false;
var existingPaymentMethod = paymentMethod.GetPaymentMethodDetails();
paymentMethod.SetPaymentMethodDetails(paymentMethodDetails);
#pragma warning disable CS0618
if (network.IsBTC)
{
invoiceEntity.DepositAddress = paymentMethod.DepositAddress;
}
#pragma warning restore CS0618
invoiceEntity.SetPaymentMethod(paymentMethod);
invoice.SetBlob(invoiceEntity);
await context.AddressInvoices.AddAsync(new AddressInvoiceData()
{
InvoiceDataId = invoiceId,
CreatedTime = DateTimeOffset.UtcNow
}
.Set(GetDestination(paymentMethod), paymentMethod.GetId()));
AddToTextSearch(context, invoice, paymentMethodDetails.GetPaymentDestination());
await context.SaveChangesAsync();
return true;
2020-01-06 13:57:32 +01:00
}
public async Task UpdateInvoicePaymentMethod(string invoiceId, PaymentMethod paymentMethod)
{
2022-01-14 17:50:29 +09:00
using var context = _applicationDbContextFactory.CreateContext();
var invoice = await context.Invoices.FindAsync(invoiceId);
if (invoice == null)
return;
var network = paymentMethod.Network;
var invoiceEntity = invoice.GetBlob(_btcPayNetworkProvider);
var newDetails = paymentMethod.GetPaymentMethodDetails();
var existing = invoiceEntity.GetPaymentMethod(paymentMethod.GetId());
if (existing.GetPaymentMethodDetails().GetPaymentDestination() != newDetails.GetPaymentDestination() && newDetails.Activated)
{
2022-01-14 17:50:29 +09:00
await context.AddressInvoices.AddAsync(new AddressInvoiceData()
{
2022-01-14 17:50:29 +09:00
InvoiceDataId = invoiceId,
CreatedTime = DateTimeOffset.UtcNow
}
2022-01-14 17:50:29 +09:00
.Set(GetDestination(paymentMethod), paymentMethod.GetId()));
}
2022-01-14 17:50:29 +09:00
invoiceEntity.SetPaymentMethod(paymentMethod);
invoice.SetBlob(invoiceEntity);
2022-01-14 17:50:29 +09:00
AddToTextSearch(context, invoice, paymentMethod.GetPaymentMethodDetails().GetPaymentDestination());
await context.SaveChangesAsync();
}
public async Task AddPendingInvoiceIfNotPresent(string invoiceId)
{
2022-01-14 17:50:29 +09:00
using var context = _applicationDbContextFactory.CreateContext();
if (!context.PendingInvoices.Any(a => a.Id == invoiceId))
{
2022-01-14 17:50:29 +09:00
context.PendingInvoices.Add(new PendingInvoiceData() { Id = invoiceId });
try
{
2022-01-14 17:50:29 +09:00
await context.SaveChangesAsync();
}
2022-01-14 17:50:29 +09:00
catch (DbUpdateException) { } // Already exists
}
}
public async Task AddInvoiceEvent(string invoiceId, object evt, InvoiceEventData.EventSeverity severity)
2018-01-14 21:48:23 +09:00
{
await using var context = _applicationDbContextFactory.CreateContext();
await context.InvoiceEvents.AddAsync(new InvoiceEventData()
2018-01-14 21:48:23 +09:00
{
Severity = severity,
InvoiceDataId = invoiceId,
Message = evt.ToString(),
Timestamp = DateTimeOffset.UtcNow,
UniqueId = Encoders.Hex.EncodeData(RandomUtils.GetBytes(10))
});
try
{
await context.SaveChangesAsync();
2018-01-14 21:48:23 +09:00
}
catch (DbUpdateException) { } // Probably the invoice does not exists anymore
2018-01-14 21:48:23 +09:00
}
public static void AddToTextSearch(ApplicationDbContext context, InvoiceData invoice, params string[] terms)
{
var filteredTerms = terms.Where(t => !string.IsNullOrWhiteSpace(t)
&& (invoice.InvoiceSearchData == null || invoice.InvoiceSearchData.All(data => data.Value != t)))
.Distinct()
.Select(s => new InvoiceSearchData() { InvoiceDataId = invoice.Id, Value = s.Truncate(512) });
context.AddRange(filteredTerms);
}
public static void RemoveFromTextSearch(ApplicationDbContext context, InvoiceData invoice,
string term)
{
var query = context.InvoiceSearches.AsQueryable();
var filteredQuery = query.Where(st => st.InvoiceDataId.Equals(invoice.Id) && st.Value.Equals(term));
context.InvoiceSearches.RemoveRange(filteredQuery);
}
public async Task UpdateInvoiceStatus(string invoiceId, InvoiceState invoiceState)
{
2022-01-14 17:50:29 +09:00
using var context = _applicationDbContextFactory.CreateContext();
var invoiceData = await context.FindAsync<Data.InvoiceData>(invoiceId).ConfigureAwait(false);
if (invoiceData == null)
return;
invoiceData.Status = InvoiceState.ToString(invoiceState.Status);
invoiceData.ExceptionStatus = InvoiceState.ToString(invoiceState.ExceptionStatus);
await context.SaveChangesAsync().ConfigureAwait(false);
}
2021-08-03 17:03:00 +09:00
internal async Task UpdateInvoicePrice(string invoiceId, InvoiceEntity invoice)
{
if (invoice.Type != InvoiceType.TopUp)
throw new ArgumentException("The invoice type should be TopUp to be able to update invoice price", nameof(invoice));
2022-01-14 17:50:29 +09:00
using var context = _applicationDbContextFactory.CreateContext();
var invoiceData = await context.FindAsync<Data.InvoiceData>(invoiceId).ConfigureAwait(false);
if (invoiceData == null)
return;
var blob = invoiceData.GetBlob(_btcPayNetworkProvider);
blob.Price = invoice.Price;
AddToTextSearch(context, invoiceData, new[] { invoice.Price.ToString(CultureInfo.InvariantCulture) });
invoiceData.SetBlob(blob);
2022-01-14 17:50:29 +09:00
await context.SaveChangesAsync().ConfigureAwait(false);
2021-08-03 17:03:00 +09:00
}
public async Task MassArchive(string[] invoiceIds, bool archive = true)
{
2022-01-14 17:50:29 +09:00
using var context = _applicationDbContextFactory.CreateContext();
var items = context.Invoices.Where(a => invoiceIds.Contains(a.Id));
foreach (InvoiceData invoice in items)
{
invoice.Archived = archive;
}
2022-01-14 17:50:29 +09:00
await context.SaveChangesAsync();
}
2020-07-24 08:13:21 +02:00
public async Task ToggleInvoiceArchival(string invoiceId, bool archived, string storeId = null)
2020-05-07 12:50:07 +02:00
{
2022-01-14 17:50:29 +09:00
using var context = _applicationDbContextFactory.CreateContext();
var invoiceData = await context.FindAsync<InvoiceData>(invoiceId).ConfigureAwait(false);
if (invoiceData == null || invoiceData.Archived == archived ||
(storeId != null &&
!invoiceData.StoreDataId.Equals(storeId, StringComparison.InvariantCultureIgnoreCase)))
return;
invoiceData.Archived = archived;
await context.SaveChangesAsync().ConfigureAwait(false);
2020-05-07 12:50:07 +02:00
}
public async Task<InvoiceEntity> UpdateInvoiceMetadata(string invoiceId, string storeId, JObject metadata)
{
2022-01-14 17:50:29 +09:00
using var context = _applicationDbContextFactory.CreateContext();
var invoiceData = await GetInvoiceRaw(invoiceId, context);
if (invoiceData == null || (storeId != null &&
!invoiceData.StoreDataId.Equals(storeId,
StringComparison.InvariantCultureIgnoreCase)))
return null;
var blob = invoiceData.GetBlob(_btcPayNetworkProvider);
var newMetadata = InvoiceMetadata.FromJObject(metadata);
var oldOrderId = blob.Metadata.OrderId;
var newOrderId = newMetadata.OrderId;
if (newOrderId != oldOrderId)
{
// OrderId is saved in 2 places: (1) the invoice table and (2) in the metadata field. We are updating both for consistency.
invoiceData.OrderId = newOrderId;
if (oldOrderId != null && (newOrderId is null || !newOrderId.Equals(oldOrderId, StringComparison.InvariantCulture)))
{
RemoveFromTextSearch(context, invoiceData, oldOrderId);
}
if (newOrderId != null)
{
AddToTextSearch(context, invoiceData, new[] { newOrderId });
}
}
blob.Metadata = newMetadata;
invoiceData.SetBlob(blob);
2022-01-14 17:50:29 +09:00
await context.SaveChangesAsync().ConfigureAwait(false);
return ToEntity(invoiceData);
}
2020-07-24 09:40:37 +02:00
public async Task<bool> MarkInvoiceStatus(string invoiceId, InvoiceStatus status)
{
using (var context = _applicationDbContextFactory.CreateContext())
{
var invoiceData = await GetInvoiceRaw(invoiceId, context);
2020-07-24 09:40:37 +02:00
if (invoiceData == null)
{
return false;
}
2020-07-24 12:46:46 +02:00
context.Attach(invoiceData);
2020-07-24 09:40:37 +02:00
string eventName;
string legacyStatus;
2020-07-24 09:40:37 +02:00
switch (status)
{
case InvoiceStatus.Settled:
2020-07-24 09:40:37 +02:00
if (!invoiceData.GetInvoiceState().CanMarkComplete())
{
return false;
}
eventName = InvoiceEvent.MarkedCompleted;
legacyStatus = InvoiceStatusLegacy.Complete.ToString();
2020-07-24 09:40:37 +02:00
break;
case InvoiceStatus.Invalid:
if (!invoiceData.GetInvoiceState().CanMarkInvalid())
{
return false;
}
eventName = InvoiceEvent.MarkedInvalid;
legacyStatus = InvoiceStatusLegacy.Invalid.ToString();
2020-07-24 09:40:37 +02:00
break;
default:
return false;
}
2020-07-24 12:46:46 +02:00
invoiceData.Status = legacyStatus.ToLowerInvariant();
2020-07-24 09:40:37 +02:00
invoiceData.ExceptionStatus = InvoiceExceptionStatus.Marked.ToString().ToLowerInvariant();
try
{
await context.SaveChangesAsync();
}
finally
{
_eventAggregator.Publish(new InvoiceEvent(ToEntity(invoiceData), eventName));
}
}
2020-07-24 09:40:37 +02:00
return true;
}
2020-07-24 12:46:46 +02:00
2021-10-23 22:28:50 -07:00
public async Task<InvoiceEntity> GetInvoice(string id, bool includeAddressData = false)
2020-07-24 12:46:46 +02:00
{
2022-01-14 17:50:29 +09:00
using var context = _applicationDbContextFactory.CreateContext();
var res = await GetInvoiceRaw(id, context, includeAddressData);
return res == null ? null : ToEntity(res);
2020-07-24 12:46:46 +02:00
}
public async Task<InvoiceEntity[]> GetInvoices(string[] invoiceIds)
{
var invoiceIdSet = invoiceIds.ToHashSet();
2022-01-14 17:50:29 +09:00
using var context = _applicationDbContextFactory.CreateContext();
IQueryable<InvoiceData> query =
2022-01-14 17:50:29 +09:00
context
.Invoices
.Include(o => o.Payments)
.Where(o => invoiceIdSet.Contains(o.Id));
2022-01-14 17:50:29 +09:00
return (await query.ToListAsync()).Select(o => ToEntity(o)).ToArray();
}
2020-07-24 12:46:46 +02:00
2021-10-23 22:28:50 -07:00
private async Task<InvoiceData> GetInvoiceRaw(string id, ApplicationDbContext dbContext, bool includeAddressData = false)
{
IQueryable<InvoiceData> query =
dbContext
.Invoices
2020-06-25 13:32:13 +09:00
.Include(o => o.Payments);
2021-10-23 22:28:50 -07:00
if (includeAddressData)
2022-05-23 11:27:09 +09:00
query = query.Include(o => o.AddressInvoices);
query = query.Where(i => i.Id == id);
var invoice = (await query.ToListAsync()).FirstOrDefault();
return invoice;
}
public InvoiceEntity ToEntity(InvoiceData invoice)
{
var entity = invoice.GetBlob(_btcPayNetworkProvider);
PaymentMethodDictionary paymentMethods = null;
2018-01-10 18:30:45 +09:00
#pragma warning disable CS0618
entity.Payments = invoice.Payments.Select(p =>
2017-11-06 00:31:02 -08:00
{
var paymentEntity = p.GetBlob(_btcPayNetworkProvider);
if (paymentEntity is null)
return null;
// PaymentEntity on version 0 does not have their own fee, because it was assumed that the payment method have fixed fee.
// We want to hide this legacy detail in InvoiceRepository, so we fetch the fee from the PaymentMethod and assign it to the PaymentEntity.
if (paymentEntity.Version == 0)
{
if (paymentMethods == null)
paymentMethods = entity.GetPaymentMethods();
var paymentMethodDetails = paymentMethods.TryGet(paymentEntity.GetPaymentMethodId())?.GetPaymentMethodDetails();
if (paymentMethodDetails != null) // == null should never happen, but we never know.
2019-01-07 15:35:18 +09:00
paymentEntity.NetworkFee = paymentMethodDetails.GetNextNetworkFee();
}
2017-11-06 00:31:02 -08:00
return paymentEntity;
})
.Where(p => p != null)
.OrderBy(a => a.ReceivedTime).ToList();
2018-01-10 18:30:45 +09:00
#pragma warning restore CS0618
var state = invoice.GetInvoiceState();
entity.ExceptionStatus = state.ExceptionStatus;
entity.Status = state.Status;
entity.RefundMail = invoice.CustomerEmail;
2017-11-06 00:31:02 -08:00
if (invoice.AddressInvoices != null)
{
entity.AvailableAddressHashes = invoice.AddressInvoices.Select(a => a.GetAddress() + a.GetPaymentMethodId()).ToHashSet();
2017-11-06 00:31:02 -08:00
}
if (invoice.Events != null)
2018-01-14 21:48:23 +09:00
{
entity.Events = invoice.Events.OrderBy(c => c.Timestamp).ToList();
}
if (invoice.Refunds != null)
{
entity.Refunds = invoice.Refunds.OrderBy(c => c.PullPaymentData.StartDate).ToList();
}
if (!string.IsNullOrEmpty(entity.RefundMail) && string.IsNullOrEmpty(entity.Metadata.BuyerEmail))
{
entity.Metadata.BuyerEmail = entity.RefundMail;
}
2020-05-07 12:50:07 +02:00
entity.Archived = invoice.Archived;
entity.UpdateTotals();
return entity;
}
private IQueryable<InvoiceData> GetInvoiceQuery(ApplicationDbContext context, InvoiceQuery queryObject)
{
IQueryable<InvoiceData> query = queryObject.UserId is null
? context.Invoices
: context.UserStore
.Where(u => u.ApplicationUserId == queryObject.UserId)
.SelectMany(c => c.StoreData.Invoices);
2020-05-07 12:50:07 +02:00
if (!queryObject.IncludeArchived)
{
query = query.Where(i => !i.Archived);
}
2020-06-28 17:55:27 +09:00
if (queryObject.InvoiceId is { Length: > 0 })
{
if (queryObject.InvoiceId.Length > 1)
{
var idSet = queryObject.InvoiceId.ToHashSet().ToArray();
query = query.Where(i => idSet.Contains(i.Id));
}
else
{
var invoiceId = queryObject.InvoiceId.First();
query = query.Where(i => i.Id == invoiceId);
}
}
2020-06-28 17:55:27 +09:00
if (queryObject.StoreId is { Length: > 0 })
{
if (queryObject.StoreId.Length > 1)
{
var stores = queryObject.StoreId.ToHashSet().ToArray();
query = query.Where(i => stores.Contains(i.StoreDataId));
}
// Big performant improvement to use Where rather than Contains when possible
// In our test, the first gives 720.173 ms vs 40.735 ms
else
{
var storeId = queryObject.StoreId.First();
query = query.Where(i => i.StoreDataId == storeId);
}
}
if (!string.IsNullOrEmpty(queryObject.TextSearch))
{
var text = queryObject.TextSearch.Truncate(512);
2021-10-06 12:53:41 +09:00
#pragma warning disable CA1310 // Specify StringComparison
query = query.Where(i => i.InvoiceSearchData.Any(data => data.Value.StartsWith(text)));
2021-10-06 12:53:41 +09:00
#pragma warning restore CA1310 // Specify StringComparison
}
if (queryObject.StartDate != null)
query = query.Where(i => queryObject.StartDate.Value <= i.Created);
if (queryObject.EndDate != null)
query = query.Where(i => i.Created <= queryObject.EndDate.Value);
if (queryObject.OrderId is { Length: > 0 })
{
2023-08-10 21:23:18 +03:00
var orderIdSet = queryObject.OrderId.ToHashSet().ToArray();
query = query.Where(i => orderIdSet.Contains(i.OrderId));
}
if (queryObject.ItemCode is { Length: > 0 })
{
2023-08-10 21:23:18 +03:00
var itemCodeSet = queryObject.ItemCode.ToHashSet().ToArray();
query = query.Where(i => itemCodeSet.Contains(i.ItemCode));
}
2023-08-10 21:23:18 +03:00
var statusSet = queryObject.Status is { Length: > 0 }
? queryObject.Status.Select(s => s.ToLowerInvariant()).ToHashSet()
: new HashSet<string>();
var exceptionStatusSet = queryObject.ExceptionStatus is { Length: > 0 }
? queryObject.ExceptionStatus.Select(NormalizeExceptionStatus).ToHashSet()
: new HashSet<string>();
// We make sure here that the old filters still work
if (statusSet.Contains("paid"))
statusSet.Add("processing");
if (statusSet.Contains("processing"))
statusSet.Add("paid");
if (statusSet.Contains("confirmed"))
{
2023-08-10 21:23:18 +03:00
statusSet.Add("complete");
statusSet.Add("settled");
}
if (statusSet.Contains("settled"))
{
statusSet.Add("complete");
statusSet.Add("confirmed");
}
if (statusSet.Contains("complete"))
{
statusSet.Add("settled");
statusSet.Add("confirmed");
}
2023-08-10 21:23:18 +03:00
if (statusSet.Any() || exceptionStatusSet.Any())
{
2023-08-10 21:23:18 +03:00
query = query.Where(i => statusSet.Contains(i.Status) || exceptionStatusSet.Contains(i.ExceptionStatus));
}
2018-05-06 13:16:39 +09:00
2023-08-10 21:23:18 +03:00
if (queryObject.Unusual != null)
{
2023-08-10 21:23:18 +03:00
var unusual = queryObject.Unusual.Value;
query = query.Where(i => unusual == (i.Status == "invalid" || !string.IsNullOrEmpty(i.ExceptionStatus)));
}
query = query.OrderByDescending(q => q.Created);
if (queryObject.Skip != null)
query = query.Skip(queryObject.Skip.Value);
if (queryObject.Take != null)
query = query.Take(queryObject.Take.Value);
return query;
}
public Task<InvoiceEntity[]> GetInvoices(InvoiceQuery queryObject)
{
return GetInvoices(queryObject, default);
}
public async Task<InvoiceEntity[]> GetInvoices(InvoiceQuery queryObject, CancellationToken cancellationToken)
{
await using var context = _applicationDbContextFactory.CreateContext();
2022-01-14 17:50:29 +09:00
var query = GetInvoiceQuery(context, queryObject);
query = query.Include(o => o.Payments);
if (queryObject.IncludeAddresses)
2022-05-23 11:27:09 +09:00
query = query.Include(o => o.AddressInvoices);
2022-01-14 17:50:29 +09:00
if (queryObject.IncludeEvents)
query = query.Include(o => o.Events);
if (queryObject.IncludeRefunds)
query = query.Include(o => o.Refunds).ThenInclude(refundData => refundData.PullPaymentData);
var data = await query.ToArrayAsync(cancellationToken).ConfigureAwait(false);
2022-01-14 17:50:29 +09:00
return data.Select(ToEntity).ToArray();
}
public async Task<int> GetInvoiceCount(InvoiceQuery queryObject)
{
await using var context = _applicationDbContextFactory.CreateContext();
return await GetInvoiceQuery(context, queryObject).CountAsync();
}
private string NormalizeExceptionStatus(string status)
{
status = status.ToLowerInvariant();
switch (status)
{
case "paidover":
case "over":
case "overpaid":
status = "paidOver";
break;
case "paidlate":
case "late":
status = "paidLate";
break;
case "paidpartial":
case "underpaid":
case "partial":
status = "paidPartial";
break;
}
return status;
}
Transfer Processors (#3476) * Automated Transfer processors This PR introduces a few things: * Payouts can now be directly nested under a store instead of through a pull payment. * The Wallet Send screen now has an option to "schedule" instead of simply creating a transaction. When you click on schedule, all transaction destinations are converted into approved payouts. Any options relating to fees or coin selection are discarded. * There is a new concept introduced, called "Transfer Processors". Transfer Processors are services for stores that process payouts that are awaiting payment. Each processor specifies which payment methods it can handle. BTCPay Server will have some forms of transfer processors baked in but it has been designed to allow the Plugin System to provide additional processors. * The initial transfer processors provided are "automated processors", for on chain and lightning payment methods. They can be configured to process payouts every X amount of minutes. For on-chain, this means payments are batched into one transaction, resulting in more efficient and cheaper fees for processing. * * fix build * extract * remove magic string stuff * fix error message when scheduling * Paginate migration * add payout count to payment method tab * remove unused var * add protip * optimzie payout migration dramatically * Remove useless double condition * Fix bunch of warnings * Remove warning * Remove warnigns * Rename to Payout processors * fix typo Co-authored-by: Nicolas Dorier <nicolas.dorier@gmail.com>
2022-04-24 05:19:34 +02:00
public static T FromBytes<T>(byte[] blob, BTCPayNetworkBase network = null)
{
return network == null
? JsonConvert.DeserializeObject<T>(ZipUtils.Unzip(blob), DefaultSerializerSettings)
: network.ToObject<T>(ZipUtils.Unzip(blob));
}
public static string ToJsonString<T>(T data, BTCPayNetworkBase network)
{
return network == null ? JsonConvert.SerializeObject(data, DefaultSerializerSettings) : network.ToString(data);
}
2023-04-10 11:07:03 +09:00
public InvoiceStatistics GetContributionsByPaymentMethodId(string currency, InvoiceEntity[] invoices, bool softcap)
{
var contributions = invoices
.Where(p => p.Currency.Equals(currency, StringComparison.OrdinalIgnoreCase))
.SelectMany(p =>
{
var contribution = new InvoiceStatistics.Contribution
{
PaymentMethodId = new PaymentMethodId(p.Currency, PaymentTypes.BTCLike),
CurrencyValue = p.Price,
States = new [] { p.GetInvoiceState() }
};
contribution.Value = contribution.CurrencyValue;
// For hardcap, we count newly created invoices as part of the contributions
if (!softcap && p.Status == InvoiceStatusLegacy.New)
return new[] { contribution };
// If the user get a donation via other mean, he can register an invoice manually for such amount
// then mark the invoice as complete
var payments = p.GetPayments(true);
if (payments.Count == 0 &&
p.ExceptionStatus == InvoiceExceptionStatus.Marked &&
p.Status == InvoiceStatusLegacy.Complete)
return new[] { contribution };
contribution.CurrencyValue = 0m;
contribution.Value = 0m;
// If an invoice has been marked invalid, remove the contribution
if (p.ExceptionStatus == InvoiceExceptionStatus.Marked &&
p.Status == InvoiceStatusLegacy.Invalid)
return new[] { contribution };
// Else, we just sum the payments
return payments
.Select(pay =>
{
var paymentMethodContribution = new InvoiceStatistics.Contribution
{
PaymentMethodId = pay.GetPaymentMethodId(),
CurrencyValue = pay.InvoicePaidAmount.Net,
Value = pay.PaidAmount.Net,
States = new [] { pay.InvoiceEntity.GetInvoiceState() }
};
return paymentMethodContribution;
})
.ToArray();
})
.GroupBy(p => p.PaymentMethodId)
.ToDictionary(p => p.Key, p => new InvoiceStatistics.Contribution
{
PaymentMethodId = p.Key,
States = p.SelectMany(v => v.States),
Value = p.Select(v => v.Value).Sum(),
CurrencyValue = p.Select(v => v.CurrencyValue).Sum()
});
return new InvoiceStatistics(contributions);
}
}
public class InvoiceQuery
{
public string[] StoreId
{
get; set;
}
public string UserId
{
get; set;
}
public string TextSearch
{
get; set;
}
public DateTimeOffset? StartDate
{
get; set;
}
public DateTimeOffset? EndDate
{
get; set;
}
public int? Skip
{
get; set;
}
public int? Take
{
get; set;
}
2019-01-06 10:00:55 +01:00
public string[] OrderId
{
get; set;
}
2019-01-06 10:00:55 +01:00
public string[] ItemCode
{
get; set;
}
2018-05-06 13:16:39 +09:00
public bool? Unusual { get; set; }
public string[] Status
{
get; set;
}
public string[] ExceptionStatus
{
get; set;
}
public string[] InvoiceId
{
get;
set;
}
public bool IncludeAddresses { get; set; }
2018-01-14 21:48:23 +09:00
public bool IncludeEvents { get; set; }
2020-05-07 12:50:07 +02:00
public bool IncludeArchived { get; set; } = true;
public bool IncludeRefunds { get; set; }
}
2023-04-10 11:07:03 +09:00
public class InvoiceStatistics : Dictionary<PaymentMethodId, InvoiceStatistics.Contribution>
{
public InvoiceStatistics(IEnumerable<KeyValuePair<PaymentMethodId, Contribution>> collection) : base(collection)
{
TotalCurrency = Values.Select(v => v.CurrencyValue).Sum();
}
public decimal TotalCurrency { get; }
2023-04-10 11:07:03 +09:00
public class Contribution
{
public PaymentMethodId PaymentMethodId { get; set; }
public IEnumerable<InvoiceState> States { get; set; }
public decimal Value { get; set; }
public decimal CurrencyValue { get; set; }
}
}
2017-09-13 15:47:34 +09:00
}