mirror of
https://github.com/btcpayserver/btcpayserver.git
synced 2025-03-10 09:19:24 +01:00
Remove legacy confusing export (#5293)
This commit is contained in:
parent
445e1b7bd9
commit
2d38113c66
4 changed files with 9 additions and 355 deletions
|
@ -1700,109 +1700,6 @@ namespace BTCPayServer.Tests
|
|||
}
|
||||
}
|
||||
|
||||
[Fact(Timeout = LongRunningTestTimeout)]
|
||||
[Trait("Integration", "Integration")]
|
||||
public async Task CanExportInvoicesJson()
|
||||
{
|
||||
decimal GetFieldValue(string input, string fieldName)
|
||||
{
|
||||
var match = Regex.Match(input, $"\"{fieldName}\":([^,]*)");
|
||||
Assert.True(match.Success);
|
||||
return decimal.Parse(match.Groups[1].Value.Trim(), CultureInfo.InvariantCulture);
|
||||
}
|
||||
|
||||
async Task<object[]> GetExport(TestAccount account, string storeId = null)
|
||||
{
|
||||
var content = await account.GetController<UIInvoiceController>(false)
|
||||
.Export("json", storeId);
|
||||
var result = Assert.IsType<ContentResult>(content);
|
||||
Assert.Equal("application/json", result.ContentType);
|
||||
return JsonConvert.DeserializeObject<object[]>(result.Content ?? "[]");
|
||||
}
|
||||
|
||||
using var tester = CreateServerTester();
|
||||
await tester.StartAsync();
|
||||
var user = tester.NewAccount();
|
||||
await user.GrantAccessAsync();
|
||||
user.RegisterDerivationScheme("BTC");
|
||||
await user.SetNetworkFeeMode(NetworkFeeMode.Always);
|
||||
var invoice = await user.BitPay.CreateInvoiceAsync(
|
||||
new Invoice
|
||||
{
|
||||
Price = 10,
|
||||
Currency = "USD",
|
||||
PosData = "posData",
|
||||
OrderId = "orderId",
|
||||
ItemDesc = "Some \", description",
|
||||
FullNotifications = true
|
||||
}, Facade.Merchant);
|
||||
|
||||
var networkFee = new FeeRate(invoice.MinerFees["BTC"].SatoshiPerBytes).GetFee(100);
|
||||
var result = await GetExport(user);
|
||||
Assert.Single(result);
|
||||
|
||||
var cashCow = tester.ExplorerNode;
|
||||
var invoiceAddress = BitcoinAddress.Create(invoice.CryptoInfo[0].Address, cashCow.Network);
|
||||
var firstPayment = invoice.CryptoInfo[0].TotalDue - 3 * networkFee;
|
||||
cashCow.SendToAddress(invoiceAddress, firstPayment);
|
||||
Thread.Sleep(1000); // prevent race conditions, ordering payments
|
||||
// look if you can reduce thread sleep, this was min value for me
|
||||
|
||||
// should reduce invoice due by 0 USD because payment = network fee
|
||||
cashCow.SendToAddress(invoiceAddress, networkFee);
|
||||
Thread.Sleep(1000);
|
||||
|
||||
// pay remaining amount
|
||||
cashCow.SendToAddress(invoiceAddress, 4 * networkFee);
|
||||
Thread.Sleep(1000);
|
||||
|
||||
await TestUtils.EventuallyAsync(async () =>
|
||||
{
|
||||
var parsedJson = await GetExport(user);
|
||||
Assert.Equal(3, parsedJson.Length);
|
||||
|
||||
var invoiceDueAfterFirstPayment = 3 * networkFee.ToDecimal(MoneyUnit.BTC) * invoice.Rate;
|
||||
var pay1str = parsedJson[0].ToString();
|
||||
Assert.Contains("\"InvoiceItemDesc\": \"Some \\\", description\"", pay1str);
|
||||
Assert.Equal(invoiceDueAfterFirstPayment, GetFieldValue(pay1str, "InvoiceDue"));
|
||||
Assert.Contains("\"InvoicePrice\": 10.0", pay1str);
|
||||
Assert.Contains("\"ConversionRate\": 5000.0", pay1str);
|
||||
Assert.Contains($"\"InvoiceId\": \"{invoice.Id}\",", pay1str);
|
||||
|
||||
var pay2str = parsedJson[1].ToString();
|
||||
Assert.Equal(invoiceDueAfterFirstPayment, GetFieldValue(pay2str, "InvoiceDue"));
|
||||
|
||||
var pay3str = parsedJson[2].ToString();
|
||||
Assert.Contains("\"InvoiceDue\": 0", pay3str);
|
||||
});
|
||||
|
||||
// create an invoice for a new store and check responses with and without store id
|
||||
var otherUser = tester.NewAccount();
|
||||
await otherUser.GrantAccessAsync();
|
||||
otherUser.RegisterDerivationScheme("BTC");
|
||||
await otherUser.SetNetworkFeeMode(NetworkFeeMode.Always);
|
||||
var newInvoice = await otherUser.BitPay.CreateInvoiceAsync(
|
||||
new Invoice
|
||||
{
|
||||
Price = 21,
|
||||
Currency = "USD",
|
||||
PosData = "posData",
|
||||
OrderId = "orderId",
|
||||
ItemDesc = "Some \", description",
|
||||
FullNotifications = true
|
||||
}, Facade.Merchant);
|
||||
|
||||
await otherUser.PayInvoice(newInvoice.Id);
|
||||
Assert.Single(await GetExport(otherUser));
|
||||
Assert.Single(await GetExport(otherUser, otherUser.StoreId));
|
||||
Assert.Equal(3, (await GetExport(user, user.StoreId)).Length);
|
||||
Assert.Equal(3, (await GetExport(user)).Length);
|
||||
|
||||
await otherUser.AddOwner(user.UserId);
|
||||
Assert.Equal(4, (await GetExport(user)).Length);
|
||||
Assert.Single(await GetExport(user, otherUser.StoreId));
|
||||
}
|
||||
|
||||
[Fact(Timeout = LongRunningTestTimeout)]
|
||||
[Trait("Integration", "Integration")]
|
||||
public async Task CanChangeNetworkFeeMode()
|
||||
|
@ -1892,45 +1789,6 @@ namespace BTCPayServer.Tests
|
|||
}
|
||||
}
|
||||
|
||||
[Fact(Timeout = LongRunningTestTimeout)]
|
||||
[Trait("Integration", "Integration")]
|
||||
public async Task CanExportInvoicesCsv()
|
||||
{
|
||||
using var tester = CreateServerTester();
|
||||
await tester.StartAsync();
|
||||
var user = tester.NewAccount();
|
||||
await user.GrantAccessAsync();
|
||||
user.RegisterDerivationScheme("BTC");
|
||||
await user.SetNetworkFeeMode(NetworkFeeMode.Always);
|
||||
var invoice = user.BitPay.CreateInvoice(
|
||||
new Invoice
|
||||
{
|
||||
Price = 500,
|
||||
Currency = "USD",
|
||||
PosData = "posData",
|
||||
OrderId = "orderId",
|
||||
ItemDesc = "Some \", description",
|
||||
FullNotifications = true
|
||||
}, Facade.Merchant);
|
||||
|
||||
var cashCow = tester.ExplorerNode;
|
||||
var invoiceAddress = BitcoinAddress.Create(invoice.CryptoInfo[0].Address, cashCow.Network);
|
||||
var firstPayment = invoice.CryptoInfo[0].TotalDue - Money.Coins(0.001m);
|
||||
cashCow.SendToAddress(invoiceAddress, firstPayment);
|
||||
TestUtils.Eventually(() =>
|
||||
{
|
||||
var exportResultPaid =
|
||||
user.GetController<UIInvoiceController>().Export("csv").GetAwaiter().GetResult();
|
||||
var paidresult = Assert.IsType<ContentResult>(exportResultPaid);
|
||||
Assert.Equal("application/csv", paidresult.ContentType);
|
||||
Assert.Contains($",orderId,{invoice.Id},", paidresult.Content);
|
||||
Assert.Contains($",On-Chain,BTC,0.0991,0.0001,5000.0", paidresult.Content);
|
||||
Assert.Contains($",USD,5.00", paidresult.Content); // Seems hacky but some plateform does not render this decimal the same
|
||||
Assert.Contains("0,,\"Some \"\", description\",New (paidPartial),new,paidPartial",
|
||||
paidresult.Content);
|
||||
});
|
||||
}
|
||||
|
||||
[Fact(Timeout = LongRunningTestTimeout)]
|
||||
[Trait("Integration", "Integration")]
|
||||
public async Task CanCreateAndDeleteApps()
|
||||
|
|
|
@ -24,7 +24,6 @@ using BTCPayServer.Rating;
|
|||
using BTCPayServer.Services;
|
||||
using BTCPayServer.Services.Apps;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
using BTCPayServer.Services.Invoices.Export;
|
||||
using BTCPayServer.Services.Rates;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
@ -1182,42 +1181,6 @@ namespace BTCPayServer.Controllers
|
|||
};
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[Authorize(AuthenticationSchemes = AuthenticationSchemes.Cookie, Policy = Policies.CanViewInvoices)]
|
||||
[BitpayAPIConstraint(false)]
|
||||
public async Task<IActionResult> Export(string format, string? storeId = null, string? searchTerm = null, int timezoneOffset = 0)
|
||||
{
|
||||
var model = new InvoiceExport(_CurrencyNameTable);
|
||||
var fs = new SearchString(searchTerm);
|
||||
var storeIds = new HashSet<string>();
|
||||
if (storeId is not null)
|
||||
{
|
||||
storeIds.Add(storeId);
|
||||
}
|
||||
if (fs.GetFilterArray("storeid") is { } l)
|
||||
{
|
||||
foreach (var i in l)
|
||||
storeIds.Add(i);
|
||||
}
|
||||
|
||||
var apps = await _appService.GetAllApps(GetUserId(), false, storeId);
|
||||
InvoiceQuery invoiceQuery = GetInvoiceQuery(fs, apps, timezoneOffset);
|
||||
invoiceQuery.StoreId = storeIds.ToArray();
|
||||
invoiceQuery.Skip = 0;
|
||||
invoiceQuery.Take = int.MaxValue;
|
||||
var invoices = await _InvoiceRepository.GetInvoices(invoiceQuery);
|
||||
var res = model.Process(invoices, format);
|
||||
|
||||
var cd = new ContentDisposition
|
||||
{
|
||||
FileName = $"btcpay-export-{DateTime.UtcNow.ToString("yyyyMMdd-HHmmss", CultureInfo.InvariantCulture)}.{format}",
|
||||
Inline = true
|
||||
};
|
||||
Response.Headers.Add("Content-Disposition", cd.ToString());
|
||||
Response.Headers.Add("X-Content-Type-Options", "nosniff");
|
||||
return Content(res, "application/" + format);
|
||||
}
|
||||
|
||||
private SelectList GetPaymentMethodsSelectList()
|
||||
{
|
||||
var store = GetCurrentStore();
|
||||
|
|
|
@ -1,169 +0,0 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using BTCPayServer.Services.Rates;
|
||||
using CsvHelper.Configuration;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace BTCPayServer.Services.Invoices.Export
|
||||
{
|
||||
public class InvoiceExport
|
||||
{
|
||||
public BTCPayNetworkProvider Networks { get; }
|
||||
public CurrencyNameTable Currencies { get; }
|
||||
|
||||
public InvoiceExport(CurrencyNameTable currencies)
|
||||
{
|
||||
Currencies = currencies;
|
||||
}
|
||||
public string Process(InvoiceEntity[] invoices, string fileFormat)
|
||||
{
|
||||
var csvInvoices = new List<ExportInvoiceHolder>();
|
||||
foreach (var i in invoices)
|
||||
{
|
||||
csvInvoices.AddRange(convertFromDb(i));
|
||||
}
|
||||
|
||||
if (String.Equals(fileFormat, "json", StringComparison.OrdinalIgnoreCase))
|
||||
return processJson(csvInvoices);
|
||||
else if (String.Equals(fileFormat, "csv", StringComparison.OrdinalIgnoreCase))
|
||||
return processCsv(csvInvoices);
|
||||
else
|
||||
throw new Exception("Export format not supported");
|
||||
}
|
||||
|
||||
private string processJson(List<ExportInvoiceHolder> invoices)
|
||||
{
|
||||
var serializerSett = new JsonSerializerSettings { ReferenceLoopHandling = ReferenceLoopHandling.Ignore };
|
||||
var json = JsonConvert.SerializeObject(invoices, Formatting.Indented, serializerSett);
|
||||
|
||||
return json;
|
||||
}
|
||||
|
||||
private string processCsv(List<ExportInvoiceHolder> invoices)
|
||||
{
|
||||
using StringWriter writer = new StringWriter();
|
||||
using var csvWriter = new CsvHelper.CsvWriter(writer, new CsvConfiguration(CultureInfo.InvariantCulture), true);
|
||||
csvWriter.WriteHeader<ExportInvoiceHolder>();
|
||||
csvWriter.NextRecord();
|
||||
csvWriter.WriteRecords(invoices);
|
||||
csvWriter.Flush();
|
||||
return writer.ToString();
|
||||
}
|
||||
|
||||
private IEnumerable<ExportInvoiceHolder> convertFromDb(InvoiceEntity invoice)
|
||||
{
|
||||
var exportList = new List<ExportInvoiceHolder>();
|
||||
var currency = Currencies.GetNumberFormatInfo(invoice.Currency, true);
|
||||
var invoiceDue = invoice.Price;
|
||||
var payments = invoice.GetPayments(false);
|
||||
// Get invoices with payments
|
||||
if (payments.Count > 0)
|
||||
{
|
||||
foreach (var payment in payments)
|
||||
{
|
||||
var pdata = payment.GetCryptoPaymentData();
|
||||
invoiceDue -= payment.InvoicePaidAmount.Net;
|
||||
|
||||
var target = new ExportInvoiceHolder
|
||||
{
|
||||
ReceivedDate = payment.ReceivedTime.UtcDateTime,
|
||||
PaymentId = pdata.GetPaymentId(),
|
||||
CryptoCode = payment.Currency,
|
||||
ConversionRate = payment.Rate,
|
||||
PaymentType = payment.GetPaymentMethodId().PaymentType.ToPrettyString(),
|
||||
Destination = pdata.GetDestination(),
|
||||
Paid = payment.PaidAmount.Gross.ToString(CultureInfo.InvariantCulture),
|
||||
PaidCurrency = Math.Round(payment.InvoicePaidAmount.Gross, currency.NumberDecimalDigits).ToString(CultureInfo.InvariantCulture),
|
||||
// Adding NetworkFee because Paid doesn't take into account network fees
|
||||
// so if fee is 10000 satoshis, customer can essentially send infinite number of tx
|
||||
// and merchant effectivelly would receive 0 BTC, invoice won't be paid
|
||||
// while looking just at export you could sum Paid and assume merchant "received payments"
|
||||
NetworkFee = payment.NetworkFee.ToString(CultureInfo.InvariantCulture),
|
||||
InvoiceDue = Math.Round(invoiceDue, currency.NumberDecimalDigits),
|
||||
OrderId = invoice.Metadata.OrderId ?? string.Empty,
|
||||
StoreId = invoice.StoreId,
|
||||
InvoiceId = invoice.Id,
|
||||
InvoiceCreatedDate = invoice.InvoiceTime.UtcDateTime,
|
||||
InvoiceExpirationDate = invoice.ExpirationTime.UtcDateTime,
|
||||
InvoiceMonitoringDate = invoice.MonitoringExpiration.UtcDateTime,
|
||||
#pragma warning disable CS0618 // Type or member is obsolete
|
||||
InvoiceFullStatus = invoice.GetInvoiceState().ToString(),
|
||||
InvoiceStatus = invoice.StatusString,
|
||||
InvoiceExceptionStatus = invoice.ExceptionStatusString,
|
||||
#pragma warning restore CS0618 // Type or member is obsolete
|
||||
InvoiceItemCode = invoice.Metadata.ItemCode,
|
||||
InvoiceItemDesc = invoice.Metadata.ItemDesc,
|
||||
InvoicePrice = invoice.Price,
|
||||
InvoiceCurrency = invoice.Currency,
|
||||
BuyerEmail = invoice.Metadata.BuyerEmail,
|
||||
Accounted = payment.Accounted
|
||||
};
|
||||
|
||||
exportList.Add(target);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
var target = new ExportInvoiceHolder
|
||||
{
|
||||
InvoiceDue = Math.Round(invoiceDue, currency.NumberDecimalDigits),
|
||||
OrderId = invoice.Metadata.OrderId ?? string.Empty,
|
||||
StoreId = invoice.StoreId,
|
||||
InvoiceId = invoice.Id,
|
||||
InvoiceCreatedDate = invoice.InvoiceTime.UtcDateTime,
|
||||
InvoiceExpirationDate = invoice.ExpirationTime.UtcDateTime,
|
||||
InvoiceMonitoringDate = invoice.MonitoringExpiration.UtcDateTime,
|
||||
#pragma warning disable CS0618 // Type or member is obsolete
|
||||
InvoiceFullStatus = invoice.GetInvoiceState().ToString(),
|
||||
InvoiceStatus = invoice.StatusString,
|
||||
InvoiceExceptionStatus = invoice.ExceptionStatusString,
|
||||
#pragma warning restore CS0618 // Type or member is obsolete
|
||||
InvoiceItemCode = invoice.Metadata.ItemCode,
|
||||
InvoiceItemDesc = invoice.Metadata.ItemDesc,
|
||||
InvoicePrice = invoice.Price,
|
||||
InvoiceCurrency = invoice.Currency,
|
||||
BuyerEmail = invoice.Metadata.BuyerEmail
|
||||
};
|
||||
|
||||
exportList.Add(target);
|
||||
}
|
||||
|
||||
exportList = exportList.OrderBy(a => a.ReceivedDate).ToList();
|
||||
|
||||
return exportList;
|
||||
}
|
||||
}
|
||||
|
||||
public class ExportInvoiceHolder
|
||||
{
|
||||
public DateTime? ReceivedDate { get; set; }
|
||||
public string StoreId { get; set; }
|
||||
public string OrderId { get; set; }
|
||||
public string InvoiceId { get; set; }
|
||||
public DateTime InvoiceCreatedDate { get; set; }
|
||||
public DateTime InvoiceExpirationDate { get; set; }
|
||||
public DateTime InvoiceMonitoringDate { get; set; }
|
||||
|
||||
public string PaymentId { get; set; }
|
||||
public string Destination { get; set; }
|
||||
public string PaymentType { get; set; }
|
||||
public string CryptoCode { get; set; }
|
||||
public string Paid { get; set; }
|
||||
public string NetworkFee { get; set; }
|
||||
public decimal ConversionRate { get; set; }
|
||||
public string PaidCurrency { get; set; }
|
||||
public string InvoiceCurrency { get; set; }
|
||||
public decimal InvoiceDue { get; set; }
|
||||
public decimal InvoicePrice { get; set; }
|
||||
public string InvoiceItemCode { get; set; }
|
||||
public string InvoiceItemDesc { get; set; }
|
||||
public string InvoiceFullStatus { get; set; }
|
||||
public string InvoiceStatus { get; set; }
|
||||
public string InvoiceExceptionStatus { get; set; }
|
||||
public string BuyerEmail { get; set; }
|
||||
public bool Accounted { get; set; }
|
||||
}
|
||||
}
|
|
@ -1,10 +1,13 @@
|
|||
@using BTCPayServer.Client
|
||||
@using BTCPayServer.Client.Models
|
||||
@using BTCPayServer.Services
|
||||
|
||||
@inject DisplayFormatter DisplayFormatter
|
||||
@inject ReportService ReportService
|
||||
@inject PaymentMethodHandlerDictionary PaymentMethodHandlerDictionary
|
||||
@model InvoicesModel
|
||||
@{
|
||||
var reportNames = ReportService.ReportProviders.Select(p => p.Value.Name).OrderBy(c => c).ToArray();
|
||||
ViewData.SetActivePage(InvoiceNavPages.Index, "Invoices");
|
||||
var statusFilterCount = CountArrayFilter("status") + CountArrayFilter("exceptionstatus") + (HasBooleanFilter("includearchived") ? 1 : 0) + (HasBooleanFilter("unusual") ? 1 : 0);
|
||||
var hasDateFilter = HasArrayFilter("startdate") || HasArrayFilter("enddate");
|
||||
|
@ -116,9 +119,6 @@
|
|||
|
||||
document.addEventListener("DOMContentLoaded", function () {
|
||||
var timezoneOffset = new Date().getTimezoneOffset();
|
||||
$(".export-link, a.dropdown-item").each(function () {
|
||||
this.href = this.href.replace("timezoneoffset=0", "timezoneoffset=" + timezoneOffset);
|
||||
});
|
||||
|
||||
$("#invoices")
|
||||
.on("click", ".invoice-row .invoice-details-toggle", function (e) {
|
||||
|
@ -340,11 +340,13 @@
|
|||
</div>
|
||||
<div class="dropdown d-inline-flex align-items-center gap-3">
|
||||
<button class="btn btn-secondary dropdown-toggle dropdown-toggle-custom-caret order-xxl-1" type="button" id="ExportDropdownToggle" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||
Export
|
||||
Reports
|
||||
</button>
|
||||
<div class="dropdown-menu" aria-labelledby="ExportDropdownToggle">
|
||||
<a asp-action="Export" asp-route-timezoneoffset="0" asp-route-format="csv" asp-route-storeId="@Model.StoreId" asp-route-searchTerm="@Model.SearchTerm" class="dropdown-item export-link" target="_blank">CSV</a>
|
||||
<a asp-action="Export" asp-route-timezoneoffset="0" asp-route-format="json" asp-route-storeId="@Model.StoreId" asp-route-searchTerm="@Model.SearchTerm" class="dropdown-item export-link" target="_blank">JSON</a>
|
||||
@foreach (var report in reportNames)
|
||||
{
|
||||
<a asp-controller="UIReports" asp-action="StoreReports" asp-route-viewName="@report" asp-route-storeId="@Model.StoreId" class="dropdown-item export-link">@report</a>
|
||||
}
|
||||
</div>
|
||||
<a href="https://docs.btcpayserver.org/Accounting/" target="_blank" rel="noreferrer noopener" title="More information...">
|
||||
<vc:icon symbol="info" />
|
||||
|
|
Loading…
Add table
Reference in a new issue