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 ;
2023-04-10 19:35:01 +09:00
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 ;
2020-05-23 21:13:18 +02:00
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 ;
2018-02-19 11:06:08 +09:00
using BTCPayServer.Payments ;
2023-12-20 18:41:28 +09:00
using Dapper ;
2020-06-28 17:55:27 +09:00
using Microsoft.EntityFrameworkCore ;
using NBitcoin ;
2020-08-25 14:53:01 +09:00
using Newtonsoft.Json ;
2020-08-25 14:33:00 +09:00
using Newtonsoft.Json.Linq ;
2024-04-25 14:09:01 +09:00
using Npgsql ;
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
{
2020-12-28 11:10:53 +01:00
public class InvoiceRepository
2017-10-27 17:53:04 +09:00
{
2023-02-21 15:06:34 +09:00
internal static JsonSerializerSettings DefaultSerializerSettings ;
2020-08-25 14:53:01 +09:00
static InvoiceRepository ( )
{
DefaultSerializerSettings = new JsonSerializerSettings ( ) ;
NBitcoin . JsonConverters . Serializer . RegisterFrontConverters ( DefaultSerializerSettings ) ;
}
2017-10-27 17:53:04 +09:00
2021-10-05 11:10:41 +02:00
private readonly ApplicationDbContextFactory _applicationDbContextFactory ;
2020-07-24 09:40:37 +02:00
private readonly EventAggregator _eventAggregator ;
2019-05-29 14:33:31 +00:00
2020-12-28 11:10:53 +01:00
public InvoiceRepository ( ApplicationDbContextFactory contextFactory ,
2024-04-04 16:31:04 +09:00
EventAggregator eventAggregator )
2017-10-27 17:53:04 +09:00
{
2021-10-05 11:10:41 +02:00
_applicationDbContextFactory = contextFactory ;
2020-07-24 09:40:37 +02:00
_eventAggregator = eventAggregator ;
2019-05-29 14:33:31 +00:00
}
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
{
2021-10-05 11:10:41 +02: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 ( ) ;
}
2023-07-19 18:47:32 +09:00
public InvoiceEntity CreateNewInvoice ( string storeId )
2019-05-29 14:33:31 +00:00
{
return new InvoiceEntity ( )
{
2023-07-19 18:47:32 +09:00
Id = Encoders . Base58 . EncodeData ( RandomUtils . GetBytes ( 16 ) ) ,
StoreId = storeId ,
2019-05-29 14:33:31 +00:00
Version = InvoiceEntity . Lastest_Version ,
2023-07-19 18:47:32 +09:00
// 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
2019-05-29 14:33:31 +00:00
} ;
2017-10-27 17:53:04 +09:00
}
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
2017-10-27 17:53:04 +09:00
{
2022-01-14 17:50:29 +09:00
await ctx . SaveChangesAsync ( ) ;
return true ;
2017-10-27 17:53:04 +09:00
}
2022-01-14 17:50:29 +09:00
catch ( DbUpdateException ) { return false ; }
2017-10-27 17:53:04 +09:00
}
2019-09-21 17:07:48 +02:00
public async Task < IEnumerable < InvoiceEntity > > GetInvoicesFromAddresses ( string [ ] addresses )
2017-10-27 17:53:04 +09:00
{
2024-04-04 16:31:04 +09:00
if ( addresses . Length is 0 )
return Array . Empty < InvoiceEntity > ( ) ;
2022-01-14 17:50:29 +09:00
using var db = _applicationDbContextFactory . CreateContext ( ) ;
2024-04-04 16:31:04 +09:00
if ( addresses . Length = = 1 )
{
var address = addresses [ 0 ] ;
return ( await db . AddressInvoices
2022-01-14 17:50:29 +09:00
. Include ( a = > a . InvoiceData . Payments )
2024-04-04 16:31:04 +09:00
. Where ( a = > a . Address = = address )
2019-09-21 17:07:48 +02:00
. Select ( a = > a . InvoiceData )
2022-01-14 17:50:29 +09:00
. ToListAsync ( ) ) . Select ( ToEntity ) ;
2024-04-04 16:31:04 +09:00
}
else
{
return ( await db . AddressInvoices
. Include ( a = > a . InvoiceData . Payments )
. Where ( a = > addresses . Contains ( a . Address ) )
. Select ( a = > a . InvoiceData )
. ToListAsync ( ) ) . Select ( ToEntity ) ;
}
2017-10-27 17:53:04 +09:00
}
2023-04-10 19:35:01 +09:00
public async Task < InvoiceEntity [ ] > GetPendingInvoices ( bool includeAddressData = false , bool skipNoPaymentInvoices = false , CancellationToken cancellationToken = default )
2022-05-23 15:15:26 +09:00
{
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 ( ) ) ;
2023-04-10 19:35:01 +09:00
return ( await q . Select ( o = > o . InvoiceData ) . ToArrayAsync ( cancellationToken ) ) . Select ( ToEntity ) . ToArray ( ) ;
2022-05-23 15:15:26 +09:00
}
public async Task < string [ ] > GetPendingInvoiceIds ( )
2017-10-27 17:53:04 +09:00
{
2022-01-14 17:50:29 +09:00
using var ctx = _applicationDbContextFactory . CreateContext ( ) ;
return await ctx . PendingInvoices . AsQueryable ( ) . Select ( data = > data . Id ) . ToArrayAsync ( ) ;
2017-10-27 17:53:04 +09:00
}
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
{
2021-10-05 11:10:41 +02:00
using var ctx = _applicationDbContextFactory . CreateContext ( ) ;
2020-11-06 20:42:26 +09:00
return await ctx . InvoiceWebhookDeliveries
2021-10-30 09:40:26 -04:00
. 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 ( ) ;
}
2019-02-19 12:48:48 +09:00
public async Task < AppData [ ] > GetAppsTaggingStore ( string storeId )
{
2021-12-28 17:39:54 +09:00
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 ( ) ;
2019-02-19 12:48:48 +09:00
}
2017-10-27 17:53:04 +09:00
public async Task UpdateInvoice ( string invoiceId , UpdateCustomerModel data )
{
2024-04-25 14:09:01 +09:00
retry :
2023-12-20 18:41:28 +09:00
using ( var ctx = _applicationDbContextFactory . CreateContext ( ) )
2017-10-27 17:53:04 +09:00
{
2023-12-20 18:41:28 +09:00
var invoiceData = await ctx . Invoices . FindAsync ( invoiceId ) ;
if ( invoiceData = = null )
return ;
2024-04-04 16:31:04 +09:00
var blob = invoiceData . GetBlob ( ) ;
if ( blob . Metadata . BuyerEmail = = null & & data . Email ! = null )
2023-12-20 18:41:28 +09:00
{
2024-04-04 16:31:04 +09:00
if ( MailboxAddressValidator . IsMailboxAddress ( data . Email ) )
{
blob . Metadata . BuyerEmail = data . Email ;
invoiceData . SetBlob ( blob ) ;
AddToTextSearch ( ctx , invoiceData , blob . Metadata . BuyerEmail ) ;
}
2023-12-20 18:41:28 +09:00
}
try
{
await ctx . SaveChangesAsync ( ) . ConfigureAwait ( false ) ;
}
catch ( DbUpdateConcurrencyException )
{
goto retry ;
}
2017-10-27 17:53:04 +09:00
}
}
2022-11-24 00:53:32 +01:00
public async Task UpdateInvoiceExpiry ( string invoiceId , TimeSpan seconds )
{
2024-04-25 14:09:01 +09:00
retry :
2023-12-20 18:41:28 +09:00
await using ( var ctx = _applicationDbContextFactory . CreateContext ( ) )
{
var invoiceData = await ctx . Invoices . FindAsync ( invoiceId ) ;
2024-04-04 16:31:04 +09:00
var invoice = invoiceData . GetBlob ( ) ;
2023-12-20 18:41:28 +09:00
var expiry = DateTimeOffset . Now + seconds ;
invoice . ExpirationTime = expiry ;
invoice . MonitoringExpiration = expiry . AddHours ( 1 ) ;
invoiceData . SetBlob ( invoice ) ;
try
{
await ctx . SaveChangesAsync ( ) ;
}
catch ( DbUpdateConcurrencyException )
{
goto retry ;
}
_eventAggregator . Publish ( new InvoiceDataChangedEvent ( invoice ) ) ;
_ = InvoiceNeedUpdateEventLater ( invoiceId , seconds ) ;
}
2022-11-24 00:53:32 +01:00
}
2023-01-06 14:18:07 +01:00
2022-11-24 00:53:32 +01:00
async Task InvoiceNeedUpdateEventLater ( string invoiceId , TimeSpan expirationIn )
{
await Task . Delay ( expirationIn ) ;
_eventAggregator . Publish ( new InvoiceNeedUpdateEvent ( invoiceId ) ) ;
}
2019-05-25 17:20:17 -05:00
public async Task ExtendInvoiceMonitor ( string invoiceId )
{
2024-04-25 14:09:01 +09:00
retry :
2023-12-20 18:41:28 +09:00
using ( var ctx = _applicationDbContextFactory . CreateContext ( ) )
{
var invoiceData = await ctx . Invoices . FindAsync ( invoiceId ) ;
2019-05-25 17:20:17 -05:00
2024-04-04 16:31:04 +09:00
var invoice = invoiceData . GetBlob ( ) ;
2023-12-20 18:41:28 +09:00
invoice . MonitoringExpiration = invoice . MonitoringExpiration . AddHours ( 1 ) ;
invoiceData . SetBlob ( invoice ) ;
try
{
await ctx . SaveChangesAsync ( ) ;
}
catch ( DbUpdateConcurrencyException )
{
goto retry ;
}
}
2019-05-25 17:20:17 -05:00
}
2024-04-04 16:31:04 +09:00
public async Task CreateInvoiceAsync ( InvoiceCreationContext creationContext )
2017-10-27 17:53:04 +09:00
{
2024-04-04 16:31:04 +09:00
var invoice = creationContext . InvoiceEntity ;
2021-07-14 23:32:20 +09:00
var textSearch = new HashSet < string > ( ) ;
2021-10-05 11:10:41 +02:00
using ( var context = _applicationDbContextFactory . CreateContext ( ) )
2017-10-27 17:53:04 +09:00
{
2023-07-20 09:03:39 +02:00
var invoiceData = new InvoiceData
2017-10-27 17:53:04 +09:00
{
2023-07-19 18:47:32 +09:00
StoreDataId = invoice . StoreId ,
2017-10-27 17:53:04 +09:00
Id = invoice . Id ,
2020-08-25 14:33:00 +09:00
OrderId = invoice . Metadata . OrderId ,
2018-12-10 21:48:28 +09:00
#pragma warning disable CS0618 // Type or member is obsolete
Status = invoice . StatusString ,
#pragma warning restore CS0618 // Type or member is obsolete
2020-08-25 14:33:00 +09:00
ItemCode = invoice . Metadata . ItemCode ,
2020-05-07 12:50:07 +02:00
Archived = false
2020-12-28 11:10:53 +01:00
} ;
2023-02-21 15:06:34 +09:00
invoiceData . SetBlob ( invoice ) ;
2020-12-28 11:10:53 +01:00
await context . Invoices . AddAsync ( invoiceData ) ;
2024-04-04 16:31:04 +09:00
foreach ( var ctx in creationContext . PaymentMethodContexts . Where ( p = > p . Value . Status is PaymentMethodContext . ContextStatus . Created or PaymentMethodContext . ContextStatus . WaitingForActivation ) )
2017-10-27 17:53:04 +09:00
{
2024-04-04 16:31:04 +09:00
foreach ( var trackedDestination in ctx . Value . TrackedDestinations )
2017-12-21 15:52:04 +09:00
{
2023-04-25 08:51:38 +09:00
await context . AddressInvoices . AddAsync ( new AddressInvoiceData ( )
{
InvoiceDataId = invoice . Id ,
2024-04-04 16:31:04 +09:00
Address = trackedDestination
} ) ;
2023-04-25 08:51:38 +09:00
}
2024-04-04 16:31:04 +09:00
}
foreach ( var prompt in invoice . GetPaymentPrompts ( ) )
{
if ( prompt . Destination ! = null )
textSearch . Add ( prompt . Destination ) ;
if ( prompt . Activated )
textSearch . Add ( prompt . Calculate ( ) . TotalDue . ToString ( ) ) ;
2017-12-21 15:52:04 +09:00
}
2020-12-28 11:10:53 +01:00
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 ) ) ;
2020-12-28 11:10:53 +01:00
textSearch . Add ( invoice . Metadata . OrderId ) ;
textSearch . Add ( invoice . StoreId ) ;
textSearch . Add ( invoice . Metadata . BuyerEmail ) ;
2024-04-04 16:31:04 +09:00
textSearch . AddRange ( creationContext . GetAllSearchTerms ( ) ) ;
2020-12-28 11:10:53 +01:00
AddToTextSearch ( context , invoiceData , textSearch . ToArray ( ) ) ;
2017-10-27 17:53:04 +09:00
await context . SaveChangesAsync ( ) . ConfigureAwait ( false ) ;
}
2020-06-24 17:51:00 +09:00
}
2019-04-03 14:38:35 +09:00
public async Task AddInvoiceLogs ( string invoiceId , InvoiceLogs logs )
{
2021-10-05 11:10:41 +02:00
await using var context = _applicationDbContextFactory . CreateContext ( ) ;
2024-04-25 14:09:01 +09:00
var db = context . Database . GetDbConnection ( ) ;
var data = logs . ToList ( ) . Select ( log = > new InvoiceEventData ( )
2019-04-03 14:38:35 +09:00
{
2024-04-25 14:09:01 +09:00
Severity = log . Severity ,
InvoiceDataId = invoiceId ,
Message = log . Log ,
Timestamp = log . Timestamp
} ) . ToArray ( ) ;
await db . ExecuteAsync ( InsertInvoiceEvent , data ) ;
}
public async Task < InvoiceEventData [ ] > GetInvoiceLogs ( string invoiceId )
{
await using var context = _applicationDbContextFactory . CreateContext ( ) ;
var db = context . Database . GetDbConnection ( ) ;
return ( await db . QueryAsync < InvoiceEventData > ( "SELECT * FROM \"InvoiceEvents\" WHERE \"InvoiceDataId\"=@InvoiceDataId ORDER BY \"Timestamp\"" , new { InvoiceDataId = invoiceId } ) ) . ToArray ( ) ;
2019-04-03 14:38:35 +09:00
}
2024-04-04 16:31:04 +09:00
public Task UpdatePaymentDetails ( string invoiceId , IPaymentMethodHandler handler , object details )
2018-02-19 02:38:03 +09:00
{
2024-04-04 16:31:04 +09:00
return UpdatePaymentDetails ( invoiceId , handler . PaymentMethodId , details is null ? null : JToken . FromObject ( details , handler . Serializer ) ) ;
2024-04-25 14:09:01 +09:00
2018-02-19 02:38:03 +09:00
}
2024-04-04 16:31:04 +09:00
public async Task UpdatePaymentDetails ( string invoiceId , PaymentMethodId paymentMethodId , JToken details )
2017-10-27 17:53:04 +09:00
{
2024-04-25 14:09:01 +09:00
retry :
2024-04-04 16:31:04 +09:00
using ( var context = _applicationDbContextFactory . CreateContext ( ) )
2023-12-20 18:41:28 +09:00
{
2024-04-04 16:31:04 +09:00
try
2023-12-20 18:41:28 +09:00
{
2024-04-04 16:31:04 +09:00
var invoice = await context . Invoices . FindAsync ( invoiceId ) ;
if ( invoice = = null )
return ;
var invoiceEntity = invoice . GetBlob ( ) ;
var prompt = invoiceEntity . GetPaymentPrompt ( paymentMethodId ) ;
if ( prompt = = null )
return ;
prompt . Details = details ;
invoiceEntity . SetPaymentPrompt ( paymentMethodId , prompt ) ;
invoice . SetBlob ( invoiceEntity ) ;
await context . SaveChangesAsync ( ) ;
2023-12-20 18:41:28 +09:00
}
2024-04-04 16:31:04 +09:00
catch ( DbUpdateConcurrencyException )
2023-12-20 18:41:28 +09:00
{
2024-04-04 16:31:04 +09:00
goto retry ;
2023-12-20 18:41:28 +09:00
}
2024-04-04 16:31:04 +09:00
}
}
public async Task UpdatePrompt ( string invoiceId , PaymentPrompt prompt )
{
retry :
using ( var context = _applicationDbContextFactory . CreateContext ( ) )
{
2023-12-20 18:41:28 +09:00
try
{
2024-04-04 16:31:04 +09:00
var invoice = await context . Invoices . FindAsync ( invoiceId ) ;
if ( invoice = = null )
return ;
var invoiceEntity = invoice . GetBlob ( ) ;
var existingPrompt = invoiceEntity . GetPaymentPrompt ( prompt . PaymentMethodId ) ;
if ( existingPrompt = = null )
return ;
invoiceEntity . SetPaymentPrompt ( prompt . PaymentMethodId , prompt ) ;
invoice . SetBlob ( invoiceEntity ) ;
2023-12-20 18:41:28 +09:00
await context . SaveChangesAsync ( ) ;
}
catch ( DbUpdateConcurrencyException )
{
goto retry ;
}
}
2020-01-06 13:57:32 +01:00
}
2024-04-04 16:31:04 +09:00
public async Task NewPaymentPrompt ( string invoiceId , PaymentMethodContext paymentPromptContext )
2021-04-07 06:08:42 +02:00
{
2024-04-04 16:31:04 +09:00
var prompt = paymentPromptContext . Prompt ;
2024-04-25 14:09:01 +09:00
retry :
2023-12-20 18:41:28 +09:00
using ( var context = _applicationDbContextFactory . CreateContext ( ) )
2021-04-07 06:08:42 +02:00
{
2023-12-20 18:41:28 +09:00
var invoice = await context . Invoices . FindAsync ( invoiceId ) ;
if ( invoice = = null )
return ;
2024-04-04 16:31:04 +09:00
var invoiceEntity = invoice . GetBlob ( ) ;
var newDetails = prompt . Details ;
var existing = invoiceEntity . GetPaymentPrompt ( prompt . PaymentMethodId ) ;
if ( existing . Destination ! = prompt . Destination & & prompt . Activated & & prompt . Destination is not null )
2021-04-07 06:08:42 +02:00
{
2024-04-04 16:31:04 +09:00
foreach ( var tracked in paymentPromptContext . TrackedDestinations )
2023-12-20 18:41:28 +09:00
{
2024-04-04 16:31:04 +09:00
await context . AddressInvoices . AddAsync ( new AddressInvoiceData ( )
{
InvoiceDataId = invoiceId ,
Address = tracked
} ) ;
2023-12-20 18:41:28 +09:00
}
2024-04-04 16:31:04 +09:00
AddToTextSearch ( context , invoice , prompt . Destination ) ;
2023-12-20 18:41:28 +09:00
}
2024-04-04 16:31:04 +09:00
var search = new List < String > ( ) ;
search . Add ( prompt . Calculate ( ) . TotalDue . ToString ( ) ) ;
invoiceEntity . SetPaymentPrompt ( prompt . PaymentMethodId , prompt ) ;
2023-12-20 18:41:28 +09:00
invoice . SetBlob ( invoiceEntity ) ;
2024-04-04 16:31:04 +09:00
AddToTextSearch ( context , invoice , search . Concat ( paymentPromptContext . AdditionalSearchTerms ) . Distinct ( ) . ToArray ( ) ) ;
2023-12-20 18:41:28 +09:00
try
{
await context . SaveChangesAsync ( ) ;
}
catch ( DbUpdateConcurrencyException )
{
goto retry ;
2021-04-07 06:08:42 +02:00
}
}
}
2019-05-25 17:30:27 -05:00
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 ) )
2019-05-25 17:30:27 -05:00
{
2022-01-14 17:50:29 +09:00
context . PendingInvoices . Add ( new PendingInvoiceData ( ) { Id = invoiceId } ) ;
try
2019-05-25 17:30:27 -05:00
{
2022-01-14 17:50:29 +09:00
await context . SaveChangesAsync ( ) ;
2019-05-25 17:30:27 -05:00
}
2022-01-14 17:50:29 +09:00
catch ( DbUpdateException ) { } // Already exists
2019-05-25 17:30:27 -05:00
}
}
2024-04-25 14:09:01 +09:00
const string InsertInvoiceEvent = "INSERT INTO \"InvoiceEvents\" (\"InvoiceDataId\", \"Severity\", \"Message\", \"Timestamp\") VALUES (@InvoiceDataId, @Severity, @Message, @Timestamp)" ;
2020-08-28 08:49:13 +02:00
public async Task AddInvoiceEvent ( string invoiceId , object evt , InvoiceEventData . EventSeverity severity )
2018-01-14 21:48:23 +09:00
{
2021-10-05 11:10:41 +02:00
await using var context = _applicationDbContextFactory . CreateContext ( ) ;
2024-04-25 14:09:01 +09:00
var conn = context . Database . GetDbConnection ( ) ;
2020-08-28 08:49:13 +02:00
try
{
2024-04-25 14:09:01 +09:00
await conn . ExecuteAsync ( InsertInvoiceEvent ,
new InvoiceEventData ( )
{
Severity = severity ,
InvoiceDataId = invoiceId ,
Message = evt . ToString ( ) ,
Timestamp = DateTimeOffset . UtcNow
} ) ;
}
catch ( Npgsql . NpgsqlException ex ) when ( ex . SqlState = = PostgresErrorCodes . ForeignKeyViolation )
{
// Invoice does not exists
2018-01-14 21:48:23 +09:00
}
}
2020-12-28 11:10:53 +01:00
public static void AddToTextSearch ( ApplicationDbContext context , InvoiceData invoice , params string [ ] terms )
2017-10-27 17:53:04 +09:00
{
2020-12-28 11:10:53 +01:00
var filteredTerms = terms . Where ( t = > ! string . IsNullOrWhiteSpace ( t )
& & ( invoice . InvoiceSearchData = = null | | invoice . InvoiceSearchData . All ( data = > data . Value ! = t ) ) )
. Distinct ( )
2021-07-08 05:59:44 +02:00
. Select ( s = > new InvoiceSearchData ( ) { InvoiceDataId = invoice . Id , Value = s . Truncate ( 512 ) } ) ;
2020-12-28 11:10:53 +01:00
context . AddRange ( filteredTerms ) ;
2017-10-27 17:53:04 +09:00
}
2022-03-03 15:15:10 +01:00
public static void RemoveFromTextSearch ( ApplicationDbContext context , InvoiceData invoice ,
string term )
{
var query = context . InvoiceSearches . AsQueryable ( ) ;
2023-01-06 14:18:07 +01:00
var filteredQuery = query . Where ( st = > st . InvoiceDataId . Equals ( invoice . Id ) & & st . Value . Equals ( term ) ) ;
2022-03-03 15:15:10 +01:00
context . InvoiceSearches . RemoveRange ( filteredQuery ) ;
}
2018-12-10 21:48:28 +09:00
public async Task UpdateInvoiceStatus ( string invoiceId , InvoiceState invoiceState )
2017-10-27 17:53:04 +09:00
{
2022-01-14 17:50:29 +09:00
using var context = _applicationDbContextFactory . CreateContext ( ) ;
2023-12-20 18:41:28 +09:00
await context . Database . GetDbConnection ( )
. ExecuteAsync ( "UPDATE \"Invoices\" SET \"Status\"=@status, \"ExceptionStatus\"=@exstatus WHERE \"Id\"=@id" ,
new
{
id = invoiceId ,
status = InvoiceState . ToString ( invoiceState . Status ) ,
exstatus = InvoiceState . ToString ( invoiceState . ExceptionStatus )
} ) ;
2017-10-27 17:53:04 +09:00
}
2023-12-20 18:41:28 +09:00
internal async Task UpdateInvoicePrice ( string invoiceId , decimal price )
2021-08-03 17:03:00 +09:00
{
2024-04-25 14:09:01 +09:00
retry :
2023-12-20 18:41:28 +09:00
using ( var context = _applicationDbContextFactory . CreateContext ( ) )
{
var invoiceData = await context . FindAsync < Data . InvoiceData > ( invoiceId ) . ConfigureAwait ( false ) ;
if ( invoiceData = = null )
return ;
2024-04-04 16:31:04 +09:00
var blob = invoiceData . GetBlob ( ) ;
2023-12-20 18:41:28 +09:00
if ( blob . Type ! = InvoiceType . TopUp )
throw new ArgumentException ( "The invoice type should be TopUp to be able to update invoice price" , nameof ( invoiceId ) ) ;
blob . Price = price ;
AddToTextSearch ( context , invoiceData , new [ ] { price . ToString ( CultureInfo . InvariantCulture ) } ) ;
invoiceData . SetBlob ( blob ) ;
try
{
await context . SaveChangesAsync ( ) ;
}
catch ( DbUpdateConcurrencyException )
{
goto retry ;
}
}
2021-08-03 17:03:00 +09:00
}
2017-10-27 17:53:04 +09:00
2022-01-11 00:14:34 -08:00
public async Task MassArchive ( string [ ] invoiceIds , bool archive = true )
2020-07-14 19:58:52 -07:00
{
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 ;
2020-07-14 19:58:52 -07:00
}
2022-01-14 17:50:29 +09:00
await context . SaveChangesAsync ( ) ;
2020-07-14 19:58:52 -07:00
}
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
}
2020-12-28 11:10:53 +01:00
public async Task < InvoiceEntity > UpdateInvoiceMetadata ( string invoiceId , string storeId , JObject metadata )
2020-12-12 07:15:34 +01:00
{
2023-12-20 18:41:28 +09:00
retry :
using ( var context = _applicationDbContextFactory . CreateContext ( ) )
2022-03-03 15:15:10 +01:00
{
2023-12-20 18:41:28 +09:00
var invoiceData = await GetInvoiceRaw ( invoiceId , context ) ;
if ( invoiceData = = null | | ( storeId ! = null & &
! invoiceData . StoreDataId . Equals ( storeId ,
StringComparison . InvariantCultureIgnoreCase ) ) )
return null ;
2024-04-04 16:31:04 +09:00
var blob = invoiceData . GetBlob ( ) ;
2023-12-20 18:41:28 +09:00
var newMetadata = InvoiceMetadata . FromJObject ( metadata ) ;
var oldOrderId = blob . Metadata . OrderId ;
var newOrderId = newMetadata . OrderId ;
2023-01-06 14:18:07 +01:00
2023-12-20 18:41:28 +09:00
if ( newOrderId ! = oldOrderId )
2022-03-03 15:15:10 +01:00
{
2023-12-20 18:41:28 +09:00
// 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 ) ;
try
{
await context . SaveChangesAsync ( ) ;
2022-03-03 15:15:10 +01:00
}
2023-12-20 18:41:28 +09:00
catch ( DbUpdateConcurrencyException )
2023-01-06 14:18:07 +01:00
{
2023-12-20 18:41:28 +09:00
goto retry ;
2022-03-25 13:59:41 +01:00
}
2023-12-20 18:41:28 +09:00
return ToEntity ( invoiceData ) ;
2022-03-03 15:15:10 +01:00
}
2020-12-12 07:15:34 +01:00
}
2020-07-24 09:40:37 +02:00
public async Task < bool > MarkInvoiceStatus ( string invoiceId , InvoiceStatus status )
2017-11-05 21:15:52 -06:00
{
2021-10-05 11:10:41 +02:00
using ( var context = _applicationDbContextFactory . CreateContext ( ) )
2017-11-05 21:15:52 -06:00
{
2021-01-16 10:00:19 +02:00
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 ;
2020-11-23 15:57:05 +09:00
string legacyStatus ;
2020-07-24 09:40:37 +02:00
switch ( status )
{
2020-11-23 15:57:05 +09:00
case InvoiceStatus . Settled :
2020-07-24 09:40:37 +02:00
if ( ! invoiceData . GetInvoiceState ( ) . CanMarkComplete ( ) )
{
return false ;
}
eventName = InvoiceEvent . MarkedCompleted ;
2020-11-23 15:57:05 +09:00
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 ;
2020-11-23 15:57:05 +09:00
legacyStatus = InvoiceStatusLegacy . Invalid . ToString ( ) ;
2020-07-24 09:40:37 +02:00
break ;
default :
return false ;
}
2020-07-24 12:46:46 +02:00
2020-11-23 15:57:05 +09:00
invoiceData . Status = legacyStatus . ToLowerInvariant ( ) ;
2020-07-24 09:40:37 +02:00
invoiceData . ExceptionStatus = InvoiceExceptionStatus . Marked . ToString ( ) . ToLowerInvariant ( ) ;
2023-06-07 17:57:03 +02:00
try
{
await context . SaveChangesAsync ( ) ;
}
finally
{
_eventAggregator . Publish ( new InvoiceEvent ( ToEntity ( invoiceData ) , eventName ) ) ;
}
2017-11-05 21:15:52 -06:00
}
2020-07-24 09:40:37 +02:00
return true ;
2017-11-05 21:15:52 -06:00
}
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
}
2020-10-28 23:21:46 +09: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 ( ) ;
2023-07-20 09:03:39 +02:00
IQueryable < InvoiceData > query =
2022-01-14 17:50:29 +09:00
context
. Invoices
. Include ( o = > o . Payments )
. Where ( o = > invoiceIdSet . Contains ( o . Id ) ) ;
2020-10-28 23:21:46 +09:00
2022-01-14 17:50:29 +09:00
return ( await query . ToListAsync ( ) ) . Select ( o = > ToEntity ( o ) ) . ToArray ( ) ;
2020-10-28 23:21:46 +09:00
}
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 )
2017-10-27 17:53:04 +09:00
{
2023-07-20 09:03:39 +02:00
IQueryable < InvoiceData > query =
2021-01-16 10:00:19 +02:00
dbContext
2017-10-27 17:53:04 +09:00
. 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 ) ;
2021-01-16 10:00:19 +02:00
query = query . Where ( i = > i . Id = = id ) ;
2017-10-27 17:53:04 +09:00
2021-01-16 10:00:19 +02:00
var invoice = ( await query . ToListAsync ( ) ) . FirstOrDefault ( ) ;
return invoice ;
2017-10-27 17:53:04 +09:00
}
2023-07-20 09:03:39 +02:00
public InvoiceEntity ToEntity ( InvoiceData invoice )
2017-10-27 17:53:04 +09:00
{
2024-04-04 16:31:04 +09:00
return invoice . GetBlob ( ) ;
2017-10-27 17:53:04 +09:00
}
2023-07-20 09:03:39 +02:00
private IQueryable < InvoiceData > GetInvoiceQuery ( ApplicationDbContext context , InvoiceQuery queryObject )
2017-10-27 17:53:04 +09:00
{
2023-07-20 09:03:39 +02:00
IQueryable < InvoiceData > query = queryObject . UserId is null
2020-07-27 13:27:47 +09:00
? context . Invoices
2020-12-28 11:10:53 +01:00
: context . UserStore
. Where ( u = > u . ApplicationUserId = = queryObject . UserId )
. SelectMany ( c = > c . StoreData . Invoices ) ;
2019-01-16 21:33:04 +01:00
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
2023-07-20 09:03:39 +02:00
if ( queryObject . InvoiceId is { Length : > 0 } )
2017-10-27 17:53:04 +09:00
{
2022-05-23 15:15:26 +09:00
if ( queryObject . InvoiceId . Length > 1 )
{
2023-08-10 14:34:09 +03:00
var idSet = queryObject . InvoiceId . ToHashSet ( ) . ToArray ( ) ;
query = query . Where ( i = > idSet . Contains ( i . Id ) ) ;
2022-05-23 15:15:26 +09:00
}
else
{
var invoiceId = queryObject . InvoiceId . First ( ) ;
query = query . Where ( i = > i . Id = = invoiceId ) ;
}
2019-01-16 21:33:04 +01:00
}
2020-06-28 17:55:27 +09:00
2023-07-20 09:03:39 +02:00
if ( queryObject . StoreId is { Length : > 0 } )
2019-01-16 21:33:04 +01:00
{
2022-04-30 12:54:44 +09:00
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 ) ;
}
2019-01-16 21:33:04 +01:00
}
2017-10-27 17:53:04 +09:00
2019-01-16 21:33:04 +01:00
if ( ! string . IsNullOrEmpty ( queryObject . TextSearch ) )
{
2021-07-08 05:59:44 +02:00
var text = queryObject . TextSearch . Truncate ( 512 ) ;
2021-10-06 12:53:41 +09:00
#pragma warning disable CA1310 // Specify StringComparison
2021-07-08 05:59:44 +02:00
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
2019-01-16 21:33:04 +01:00
}
2017-10-27 17:53:04 +09:00
2019-01-16 21:33:04 +01:00
if ( queryObject . StartDate ! = null )
query = query . Where ( i = > queryObject . StartDate . Value < = i . Created ) ;
2017-10-27 17:53:04 +09:00
2019-01-16 21:33:04 +01:00
if ( queryObject . EndDate ! = null )
query = query . Where ( i = > i . Created < = queryObject . EndDate . Value ) ;
2017-10-27 17:53:04 +09:00
2023-07-20 09:03:39 +02:00
if ( queryObject . OrderId is { Length : > 0 } )
2019-01-16 21:33:04 +01:00
{
2023-08-10 21:23:18 +03:00
var orderIdSet = queryObject . OrderId . ToHashSet ( ) . ToArray ( ) ;
query = query . Where ( i = > orderIdSet . Contains ( i . OrderId ) ) ;
2019-01-16 21:33:04 +01:00
}
2023-07-20 09:03:39 +02:00
if ( queryObject . ItemCode is { Length : > 0 } )
2019-01-16 21:33:04 +01:00
{
2023-08-10 21:23:18 +03:00
var itemCodeSet = queryObject . ItemCode . ToHashSet ( ) . ToArray ( ) ;
query = query . Where ( i = > itemCodeSet . Contains ( i . ItemCode ) ) ;
2019-01-16 21:33:04 +01:00
}
2017-10-27 17:53:04 +09:00
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" ) )
2019-01-16 21:33:04 +01:00
{
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" ) ;
2019-01-16 21:33:04 +01:00
}
2017-10-27 17:53:04 +09:00
2023-08-10 21:23:18 +03:00
if ( statusSet . Any ( ) | | exceptionStatusSet . Any ( ) )
2019-01-16 21:33:04 +01:00
{
2023-08-10 21:23:18 +03:00
query = query . Where ( i = > statusSet . Contains ( i . Status ) | | exceptionStatusSet . Contains ( i . ExceptionStatus ) ) ;
2019-01-16 21:33:04 +01:00
}
2018-05-06 13:16:39 +09:00
2023-08-10 21:23:18 +03:00
if ( queryObject . Unusual ! = null )
2019-01-16 21:33:04 +01:00
{
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 ) ) ) ;
2019-01-16 21:33:04 +01:00
}
2018-05-05 23:25:09 +09:00
2024-03-05 16:10:54 +09:00
if ( queryObject . OrderByDesc )
query = query . OrderByDescending ( q = > q . Created ) ;
else
query = query . OrderBy ( q = > q . Created ) ;
2017-10-27 17:53:04 +09:00
2019-01-16 21:33:04 +01:00
if ( queryObject . Skip ! = null )
query = query . Skip ( queryObject . Skip . Value ) ;
2017-10-27 17:53:04 +09:00
2020-12-28 11:10:53 +01:00
if ( queryObject . Take ! = null )
query = query . Take ( queryObject . Take . Value ) ;
2024-04-25 14:09:01 +09:00
2019-01-16 21:33:04 +01:00
return query ;
}
2023-07-24 09:24:32 +09:00
public Task < InvoiceEntity [ ] > GetInvoices ( InvoiceQuery queryObject )
{
return GetInvoices ( queryObject , default ) ;
}
public async Task < InvoiceEntity [ ] > GetInvoices ( InvoiceQuery queryObject , CancellationToken cancellationToken )
2019-01-16 21:33:04 +01:00
{
2023-07-20 09:03:39 +02:00
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-06-03 12:08:16 +02:00
if ( queryObject . IncludeRefunds )
query = query . Include ( o = > o . Refunds ) . ThenInclude ( refundData = > refundData . PullPaymentData ) ;
2024-03-05 16:10:54 +09:00
var data = await query . AsNoTracking ( ) . ToArrayAsync ( cancellationToken ) . ConfigureAwait ( false ) ;
2022-01-14 17:50:29 +09:00
return data . Select ( ToEntity ) . ToArray ( ) ;
2017-10-27 17:53:04 +09:00
}
2024-04-04 16:31:04 +09:00
public async Task AddSearchTerms ( string invoiceId , List < string > searchTerms )
{
if ( searchTerms is null or { Count : 0 } )
return ;
await using var context = _applicationDbContextFactory . CreateContext ( ) ;
foreach ( var t in searchTerms )
{
context . InvoiceSearches . Add ( new InvoiceSearchData ( )
{
InvoiceDataId = invoiceId ,
Value = t
} ) ;
await context . SaveChangesAsync ( ) ;
}
}
2023-09-13 02:02:02 +02:00
public async Task < int > GetInvoiceCount ( InvoiceQuery queryObject )
{
await using var context = _applicationDbContextFactory . CreateContext ( ) ;
return await GetInvoiceQuery ( context , queryObject ) . CountAsync ( ) ;
}
2017-10-27 17:53:04 +09:00
2018-05-05 23:25:09 +09:00
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 ;
}
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 ) ) ;
}
2023-03-17 03:56:32 +01:00
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
2023-03-17 03:56:32 +01:00
public InvoiceStatistics GetContributionsByPaymentMethodId ( string currency , InvoiceEntity [ ] invoices , bool softcap )
{
var contributions = invoices
. Where ( p = > p . Currency . Equals ( currency , StringComparison . OrdinalIgnoreCase ) )
. SelectMany ( p = >
{
2023-09-19 03:10:13 +02:00
var contribution = new InvoiceStatistics . Contribution
{
2024-04-04 16:31:04 +09:00
GroupKey = p . Currency ,
Currency = p . Currency ,
2023-09-19 03:10:13 +02:00
CurrencyValue = p . Price ,
2024-04-04 16:31:04 +09:00
Settled = p . GetInvoiceState ( ) . IsSettled ( )
2023-09-19 03:10:13 +02:00
} ;
2023-03-17 03:56:32 +01:00
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 = 0 m ;
contribution . Value = 0 m ;
// 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 = >
{
2023-09-19 03:10:13 +02:00
var paymentMethodContribution = new InvoiceStatistics . Contribution
{
2024-04-04 16:31:04 +09:00
GroupKey = GetGroupKey ( pay ) ,
Currency = pay . Currency ,
2023-09-19 03:10:13 +02:00
CurrencyValue = pay . InvoicePaidAmount . Net ,
Value = pay . PaidAmount . Net ,
2024-04-04 16:31:04 +09:00
Divisibility = pay . Divisibility ,
Settled = p . GetInvoiceState ( ) . IsSettled ( )
2023-09-19 03:10:13 +02:00
} ;
2023-03-17 03:56:32 +01:00
return paymentMethodContribution ;
} )
. ToArray ( ) ;
} )
2024-04-04 16:31:04 +09:00
. GroupBy ( p = > p . GroupKey )
2023-09-19 03:10:13 +02:00
. ToDictionary ( p = > p . Key , p = > new InvoiceStatistics . Contribution
2023-03-17 03:56:32 +01:00
{
2024-04-04 16:31:04 +09:00
Currency = p . Key ,
Settled = p . All ( v = > v . Settled ) ,
Divisibility = p . Max ( p = > p . Divisibility ) ,
2023-03-17 03:56:32 +01:00
Value = p . Select ( v = > v . Value ) . Sum ( ) ,
CurrencyValue = p . Select ( v = > v . CurrencyValue ) . Sum ( )
} ) ;
return new InvoiceStatistics ( contributions ) ;
}
2024-04-04 16:31:04 +09:00
private string GetGroupKey ( PaymentEntity pay )
{
// If BTC-CHAIN, group by BTC
if ( PaymentTypes . CHAIN . GetPaymentMethodId ( pay . Currency ) = = pay . PaymentMethodId )
return pay . Currency ;
// If BTC-LN|LNURL, group them together by BTC-LN
if ( PaymentTypes . LN . GetPaymentMethodId ( pay . Currency ) = = pay . PaymentMethodId | |
PaymentTypes . LNURL . GetPaymentMethodId ( pay . Currency ) = = pay . PaymentMethodId )
return PaymentTypes . LN . GetPaymentMethodId ( pay . Currency ) . ToString ( ) ;
// Else just group by payment method id
return pay . PaymentMethodId . ToString ( ) ;
}
2017-10-27 17:53:04 +09:00
}
public class InvoiceQuery
{
2018-04-26 11:01:59 +09:00
public string [ ] StoreId
2017-10-27 17:53:04 +09:00
{
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 ;
}
2020-12-28 11:10:53 +01:00
public int? Take
2017-10-27 17:53:04 +09:00
{
get ; set ;
}
2019-01-06 10:00:55 +01:00
public string [ ] OrderId
2017-10-27 17:53:04 +09:00
{
get ; set ;
}
2019-01-06 10:00:55 +01:00
public string [ ] ItemCode
2017-10-27 17:53:04 +09:00
{
get ; set ;
}
2018-05-06 13:16:39 +09:00
public bool? Unusual { get ; set ; }
2018-04-26 11:01:59 +09:00
public string [ ] Status
2017-10-27 17:53:04 +09:00
{
get ; set ;
}
2018-05-05 23:25:09 +09:00
public string [ ] ExceptionStatus
{
get ; set ;
}
2019-09-21 17:07:48 +02:00
public string [ ] InvoiceId
2017-10-27 17:53:04 +09:00
{
get ;
set ;
}
2018-01-08 20:06:16 +09:00
public bool IncludeAddresses { get ; set ; }
2020-05-07 12:50:07 +02:00
public bool IncludeArchived { get ; set ; } = true ;
2022-06-03 12:08:16 +02:00
public bool IncludeRefunds { get ; set ; }
2024-03-05 16:10:54 +09:00
public bool OrderByDesc { get ; set ; } = true ;
2017-10-27 17:53:04 +09:00
}
2023-04-10 11:07:03 +09:00
2024-04-04 16:31:04 +09:00
public class InvoiceStatistics : Dictionary < string , InvoiceStatistics . Contribution >
2023-03-17 03:56:32 +01:00
{
2024-04-04 16:31:04 +09:00
public InvoiceStatistics ( IEnumerable < KeyValuePair < string , Contribution > > collection ) : base ( collection )
2023-03-17 03:56:32 +01:00
{
TotalCurrency = Values . Select ( v = > v . CurrencyValue ) . Sum ( ) ;
}
public decimal TotalCurrency { get ; }
2023-04-10 11:07:03 +09:00
2023-03-17 03:56:32 +01:00
public class Contribution
{
2024-04-04 16:31:04 +09:00
public string Currency { get ; set ; }
public int Divisibility { get ; set ; }
public bool Settled { get ; set ; }
2023-03-17 03:56:32 +01:00
public decimal Value { get ; set ; }
public decimal CurrencyValue { get ; set ; }
2024-04-04 16:31:04 +09:00
public string GroupKey { get ; set ; }
2023-03-17 03:56:32 +01:00
}
}
2017-09-13 15:47:34 +09:00
}