diff --git a/BTCPayServerPlugins.sln b/BTCPayServerPlugins.sln
index ba4fe8b..9d72191 100644
--- a/BTCPayServerPlugins.sln
+++ b/BTCPayServerPlugins.sln
@@ -49,6 +49,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BTCPayServer.Tests", "submo
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BTCPayServer.Plugins.Tests", "BTCPayServer.Plugins.Tests\BTCPayServer.Plugins.Tests.csproj", "{C6E671F6-5417-4F2F-A5A2-0A7D307BA1F7}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BTCPayServer.Plugins.DataErasure", "Plugins\BTCPayServer.Plugins.DataErasure\BTCPayServer.Plugins.DataErasure.csproj", "{034D1487-81C2-4250-A26E-162579C43C18}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -233,6 +235,14 @@ Global
{C6E671F6-5417-4F2F-A5A2-0A7D307BA1F7}.Altcoins-Debug|Any CPU.Build.0 = Debug|Any CPU
{C6E671F6-5417-4F2F-A5A2-0A7D307BA1F7}.Altcoins-Release|Any CPU.ActiveCfg = Debug|Any CPU
{C6E671F6-5417-4F2F-A5A2-0A7D307BA1F7}.Altcoins-Release|Any CPU.Build.0 = Debug|Any CPU
+ {034D1487-81C2-4250-A26E-162579C43C18}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {034D1487-81C2-4250-A26E-162579C43C18}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {034D1487-81C2-4250-A26E-162579C43C18}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {034D1487-81C2-4250-A26E-162579C43C18}.Release|Any CPU.Build.0 = Release|Any CPU
+ {034D1487-81C2-4250-A26E-162579C43C18}.Altcoins-Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {034D1487-81C2-4250-A26E-162579C43C18}.Altcoins-Debug|Any CPU.Build.0 = Debug|Any CPU
+ {034D1487-81C2-4250-A26E-162579C43C18}.Altcoins-Release|Any CPU.ActiveCfg = Debug|Any CPU
+ {034D1487-81C2-4250-A26E-162579C43C18}.Altcoins-Release|Any CPU.Build.0 = Debug|Any CPU
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{B19C9F52-DC47-466D-8B5C-2D202B7B003F} = {9E04ECE9-E304-4FF2-9CBC-83256E6C6962}
diff --git a/Plugins/BTCPayServer.Plugins.DataErasure/BTCPayServer.Plugins.DataErasure.csproj b/Plugins/BTCPayServer.Plugins.DataErasure/BTCPayServer.Plugins.DataErasure.csproj
new file mode 100644
index 0000000..b030fe6
--- /dev/null
+++ b/Plugins/BTCPayServer.Plugins.DataErasure/BTCPayServer.Plugins.DataErasure.csproj
@@ -0,0 +1,41 @@
+
+
+
+ net6.0
+ 10
+
+
+
+
+ Data Erasure
+ Allows you to erase user data from invoices after a period of time.
+ 1.0.0
+
+
+
+ true
+ false
+ true
+
+
+
+
+
+ StaticWebAssetsEnabled=false
+ false
+ runtime;native;build;buildTransitive;contentFiles
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Plugins/BTCPayServer.Plugins.DataErasure/DataErasureController.cs b/Plugins/BTCPayServer.Plugins.DataErasure/DataErasureController.cs
new file mode 100644
index 0000000..56ae86f
--- /dev/null
+++ b/Plugins/BTCPayServer.Plugins.DataErasure/DataErasureController.cs
@@ -0,0 +1,60 @@
+using System.Threading.Tasks;
+using BTCPayServer.Abstractions.Constants;
+using BTCPayServer.Client;
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Mvc;
+
+namespace BTCPayServer.Plugins.DataErasure
+{
+ [Authorize(AuthenticationSchemes = AuthenticationSchemes.Cookie)]
+ [Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
+ [Route("plugins/{storeId}/DataErasure")]
+ public class DataErasureController : Controller
+ {
+ private readonly DataErasureService _dataErasureService;
+ public DataErasureController(DataErasureService dataErasureService)
+ {
+ _dataErasureService = dataErasureService;
+ }
+
+ [HttpGet("")]
+ public async Task Update(string storeId)
+ {
+ var vm = await _dataErasureService.Get(storeId) ?? new DataErasureSettings();
+
+ return View(vm);
+ }
+
+ [HttpPost("")]
+ public async Task Update(string storeId, DataErasureSettings vm,
+ string command)
+ {
+ if (_dataErasureService.IsRunning)
+ {
+ TempData["ErrorMessage"] = "Data erasure is currently running and cannot be changed. Please try again later.";
+ }
+
+
+ if (vm.Enabled)
+ {
+ if (!ModelState.IsValid)
+ {
+ return View(vm);
+ }
+ }
+
+
+ switch (command)
+ {
+
+ case "save":
+ await _dataErasureService.Set(storeId, vm);
+ TempData["SuccessMessage"] = "Data erasure settings modified";
+ return RedirectToAction(nameof(Update), new {storeId});
+
+ default:
+ return View(vm);
+ }
+ }
+ }
+}
diff --git a/Plugins/BTCPayServer.Plugins.DataErasure/DataErasurePlugin.cs b/Plugins/BTCPayServer.Plugins.DataErasure/DataErasurePlugin.cs
new file mode 100644
index 0000000..f017f2d
--- /dev/null
+++ b/Plugins/BTCPayServer.Plugins.DataErasure/DataErasurePlugin.cs
@@ -0,0 +1,23 @@
+using BTCPayServer.Abstractions.Contracts;
+using BTCPayServer.Abstractions.Models;
+using BTCPayServer.Abstractions.Services;
+using Microsoft.Extensions.DependencyInjection;
+
+namespace BTCPayServer.Plugins.DataErasure
+{
+ public class DataErasurePlugin : BaseBTCPayServerPlugin
+ {
+ public override IBTCPayServerPlugin.PluginDependency[] Dependencies { get; } =
+ {
+ new() { Identifier = nameof(BTCPayServer), Condition = ">=1.8.0" }
+ };
+ public override void Execute(IServiceCollection applicationBuilder)
+ {
+ applicationBuilder.AddSingleton();
+ applicationBuilder.AddHostedService( sp => sp.GetRequiredService());
+ applicationBuilder.AddSingleton(new UIExtension("DataErasure/DataErasureNav",
+ "store-integrations-nav"));
+ base.Execute(applicationBuilder);
+ }
+ }
+}
diff --git a/Plugins/BTCPayServer.Plugins.DataErasure/DataErasureService.cs b/Plugins/BTCPayServer.Plugins.DataErasure/DataErasureService.cs
new file mode 100644
index 0000000..44c5ec1
--- /dev/null
+++ b/Plugins/BTCPayServer.Plugins.DataErasure/DataErasureService.cs
@@ -0,0 +1,132 @@
+using System;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using BTCPayServer.Abstractions.Contracts;
+using BTCPayServer.Services.Invoices;
+using Microsoft.Extensions.Hosting;
+using Microsoft.Extensions.Logging;
+
+namespace BTCPayServer.Plugins.DataErasure
+{
+ public class DataErasureService : IHostedService
+ {
+ private readonly IStoreRepository _storeRepository;
+ private readonly ILogger _logger;
+ private readonly InvoiceRepository _invoiceRepository;
+
+ public DataErasureService(IStoreRepository storeRepository, ILogger logger,
+ InvoiceRepository invoiceRepository)
+ {
+ _storeRepository = storeRepository;
+ _logger = logger;
+ _invoiceRepository = invoiceRepository;
+ }
+
+ public async Task Get(string storeId)
+ {
+ return await _storeRepository.GetSettingAsync(storeId,
+ nameof(DataErasureSettings));
+ }
+
+ public async Task Set(string storeId, DataErasureSettings settings)
+ {
+ await _runningLock.WaitAsync();
+ var existing = await Get(storeId);
+ settings.LastRunCutoff = existing?.LastRunCutoff;
+ await SetCore(storeId, settings);
+ _runningLock.Release();
+ }
+
+ private async Task SetCore(string storeId, DataErasureSettings settings)
+ {
+ await _storeRepository.UpdateSetting(storeId, nameof(DataErasureSettings), settings);
+ }
+
+ public bool IsRunning { get; private set; }
+ private readonly SemaphoreSlim _runningLock = new(1, 1);
+
+ private async Task Run(CancellationToken cancellationToken)
+ {
+ while (!cancellationToken.IsCancellationRequested)
+ {
+ await _runningLock.WaitAsync(cancellationToken);
+ IsRunning = true;
+
+
+ var settings =
+ await _storeRepository.GetSettingsAsync(nameof(DataErasureSettings));
+ foreach (var setting in settings.Where(setting => setting.Value.Enabled))
+ {
+ var skip = 0;
+ var count = 0;
+ var cutoffDate = DateTimeOffset.UtcNow.Subtract(TimeSpan.FromDays(setting.Value.DaysToKeep));
+ while (true)
+ {
+ var invoices = await _invoiceRepository.GetInvoices(new InvoiceQuery()
+ {
+ StartDate = setting.Value.LastRunCutoff,
+ EndDate = cutoffDate,
+ StoreId = new[] {setting.Key},
+ Skip = skip,
+ Take = 100
+ });
+ foreach (var invoice in invoices)
+ {
+ //replace all buyer info with "erased"
+ if (!string.IsNullOrEmpty(invoice.Metadata.BuyerAddress1))
+ invoice.Metadata.BuyerAddress1 = "erased";
+ if (!string.IsNullOrEmpty(invoice.Metadata.BuyerAddress2))
+ invoice.Metadata.BuyerAddress2 = "erased";
+ if (!string.IsNullOrEmpty(invoice.Metadata.BuyerCity))
+ invoice.Metadata.BuyerCity = "erased";
+ if (!string.IsNullOrEmpty(invoice.Metadata.BuyerCountry))
+ invoice.Metadata.BuyerCountry = "erased";
+ if (!string.IsNullOrEmpty(invoice.Metadata.BuyerEmail))
+ invoice.Metadata.BuyerEmail = "erased";
+ if (!string.IsNullOrEmpty(invoice.Metadata.BuyerName))
+ invoice.Metadata.BuyerName = "erased";
+ if (!string.IsNullOrEmpty(invoice.Metadata.BuyerPhone))
+ invoice.Metadata.BuyerPhone = "erased";
+ if (!string.IsNullOrEmpty(invoice.Metadata.BuyerState))
+ invoice.Metadata.BuyerState = "erased";
+ if (!string.IsNullOrEmpty(invoice.Metadata.BuyerZip))
+ invoice.Metadata.BuyerZip = "erased";
+ await _invoiceRepository.UpdateInvoiceMetadata(invoice.Id, invoice.StoreId,
+ invoice.Metadata.ToJObject());
+ count++;
+ }
+
+ if (invoices.Length < 100)
+ {
+ break;
+ }
+
+ skip += 100;
+ }
+
+ _logger.LogInformation($"Erased {count} invoice data for store {setting.Key}");
+ setting.Value.LastRunCutoff = cutoffDate;
+ await SetCore(setting.Key, setting.Value);
+ }
+
+ IsRunning = false;
+
+
+ _runningLock.Release();
+ await Task.Delay(TimeSpan.FromMinutes(1), cancellationToken);
+ }
+ }
+
+ public Task StartAsync(CancellationToken cancellationToken)
+ {
+ _ = Run(cancellationToken);
+ return Task.CompletedTask;
+ }
+
+ public Task StopAsync(CancellationToken cancellationToken)
+ {
+ return Task.CompletedTask;
+ }
+ }
+}
\ No newline at end of file
diff --git a/Plugins/BTCPayServer.Plugins.DataErasure/DataErasureSettings.cs b/Plugins/BTCPayServer.Plugins.DataErasure/DataErasureSettings.cs
new file mode 100644
index 0000000..94a2dbb
--- /dev/null
+++ b/Plugins/BTCPayServer.Plugins.DataErasure/DataErasureSettings.cs
@@ -0,0 +1,11 @@
+using System;
+
+namespace BTCPayServer.Plugins.DataErasure
+{
+ public class DataErasureSettings
+ {
+ public bool Enabled { get; set; }
+ public int DaysToKeep { get; set; }
+ public DateTimeOffset? LastRunCutoff { get; set; }
+ }
+}
diff --git a/Plugins/BTCPayServer.Plugins.DataErasure/Pack.ps1 b/Plugins/BTCPayServer.Plugins.DataErasure/Pack.ps1
new file mode 100644
index 0000000..258d46b
--- /dev/null
+++ b/Plugins/BTCPayServer.Plugins.DataErasure/Pack.ps1
@@ -0,0 +1,2 @@
+dotnet publish -c Release -o bin/publish/BTCPayServer.Plugins.DataErasure
+dotnet run -p ../../BTCPayServer.PluginPacker bin/publish/BTCPayServer.Plugins.DataErasure BTCPayServer.Plugins.DataErasure ../packed
diff --git a/Plugins/BTCPayServer.Plugins.DataErasure/Views/DataErasure/Update.cshtml b/Plugins/BTCPayServer.Plugins.DataErasure/Views/DataErasure/Update.cshtml
new file mode 100644
index 0000000..313e09a
--- /dev/null
+++ b/Plugins/BTCPayServer.Plugins.DataErasure/Views/DataErasure/Update.cshtml
@@ -0,0 +1,38 @@
+@using BTCPayServer.Abstractions.Extensions
+@using Microsoft.AspNetCore.Mvc.TagHelpers
+@model BTCPayServer.Plugins.DataErasure.DataErasureSettings
+@{
+ ViewData.SetActivePage("DataErasure", "Data Erasure", "DataErasure");
+}
+
+
+
+
@ViewData["Title"]
+
+
+
+ This plugin erases buyer information on your store's invoices based on when they were created. THERE IS NO UNDOING THIS ACTION ONCE IT HAS EXECUTED.
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Plugins/BTCPayServer.Plugins.DataErasure/Views/Shared/DataErasure/DataErasureNav.cshtml b/Plugins/BTCPayServer.Plugins.DataErasure/Views/Shared/DataErasure/DataErasureNav.cshtml
new file mode 100644
index 0000000..308a4ab
--- /dev/null
+++ b/Plugins/BTCPayServer.Plugins.DataErasure/Views/Shared/DataErasure/DataErasureNav.cshtml
@@ -0,0 +1,47 @@
+@using BTCPayServer.Abstractions.Contracts
+@using BTCPayServer.Abstractions.Extensions
+@using Microsoft.AspNetCore.Mvc.TagHelpers
+@inject IScopeProvider ScopeProvider
+@{
+ var storeId = ScopeProvider.GetCurrentStoreId();
+}
+@if (!string.IsNullOrEmpty(storeId))
+{
+