Remove legacy confusing export (#5293)

This commit is contained in:
Nicolas Dorier 2023-09-12 16:33:37 +09:00 committed by GitHub
parent 445e1b7bd9
commit 2d38113c66
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 9 additions and 355 deletions

View file

@ -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()

View file

@ -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();

View file

@ -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; }
}
}

View file

@ -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) {
@ -335,16 +335,18 @@
{
<button type="submit" asp-action="MassAction" class="dropdown-item" name="command" value="unarchive" id="ActionsDropdownUnarchive">Unarchive</button>
}
<button id="BumpFee" type="submit" permission="@Policies.CanModifyStoreSettings" class="dropdown-item" name="command" value="cpfp">Bump fee</button>
<button id="BumpFee" type="submit" permission="@Policies.CanModifyStoreSettings" class="dropdown-item" name="command" value="cpfp">Bump fee</button>
</div>
</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" />