Pimp the ticket tailor plugin

This commit is contained in:
Kukks
2023-02-07 12:20:09 +01:00
parent 9786a03de3
commit 79a307859f
8 changed files with 590 additions and 490 deletions

View File

@@ -1,4 +1,5 @@
using System;
#nullable enable
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
@@ -6,11 +7,10 @@ using System.Net.Http.Headers;
using System.Net.Http.Json;
using System.Text;
using System.Text.Json;
using System.Text.Json.Nodes;
using System.Text.Json.Serialization;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using NBitcoin.DataEncoders;
using NBitcoin.Logging;
namespace BTCPayServer.Plugins.TicketTailor;
@@ -37,10 +37,11 @@ public class TicketTailorClient : IDisposable
return await _httpClient.GetFromJsonAsync<Event>($"/v1/events/{id}");
}
public async Task<(IssuedTicket, string)> CreateTicket(IssueTicketRequest request)
public async Task<(IssuedTicket?, string? error)> CreateTicket(IssueTicketRequest request)
{
var data = JsonSerializer.SerializeToElement(request).EnumerateObject().Select(property =>
new KeyValuePair<string, string>(property.Name, property.Value.GetString())).Where(pair =>pair.Value != null);
new KeyValuePair<string, string>(property.Name, property.Value.GetString()))
.Where(pair => pair.Value != null);
var response = await _httpClient.PostAsync($"/v1/issued_tickets", new FormUrlEncodedContent(data.ToArray()));
@@ -49,9 +50,55 @@ public class TicketTailorClient : IDisposable
var error = await response.Content.ReadAsStringAsync();
return (null, error);
}
return (await response.Content.ReadFromJsonAsync<IssuedTicket>(), null);
}
public async Task<(Hold?, string? error)> CreateHold(CreateHoldRequest request)
{
var data = new Dictionary<string, string>();
data.Add("note", request.Note);
data.Add("event_id", request.EventId);
foreach (var i in request.TicketTypeId.Where(pair => pair.Value > 0))
{
data.Add($"ticket_type_id[{i.Key}]", i.Value.ToString());
}
var response = await _httpClient.PostAsync($"/v1/holds", new FormUrlEncodedContent(data));
if (!response.IsSuccessStatusCode)
{
var error = await response.Content.ReadAsStringAsync();
return (null, error);
}
return (await response.Content.ReadFromJsonAsync<Hold>(), null);
}
public async Task<Hold?> GetHold(string holdId)
{
var response = await _httpClient.GetAsync($"/v1/holds/{holdId}");
if (!response.IsSuccessStatusCode)
{
return null;
}
return await response.Content.ReadFromJsonAsync<Hold>();
}
public async Task<bool> DeleteHold(string holdId)
{
var response = await _httpClient.DeleteAsync($"/v1/holds/{holdId}");
if (!response.IsSuccessStatusCode)
{
return false;
}
return (await response.Content.ReadFromJsonAsync<JsonObject>()).TryGetPropertyValue("deleted", out var jDeleted) &&
jDeleted.GetValue<string>() == "true";
}
public async Task<IssuedTicket> GetTicket(string id)
{
@@ -72,13 +119,38 @@ public class TicketTailorClient : IDisposable
public class IssueTicketRequest
{
[JsonPropertyName("event_id")] public string EventId { get; set; }
[JsonPropertyName("ticket_type_id")] public string TicketTypeId { get; set; }
[JsonPropertyName("email")] public string Email { get; set; }
[JsonPropertyName("full_name")] public string FullName { get; set; }
[JsonPropertyName("reference")] public string Reference { get; set; }
[JsonPropertyName("barcode")] public string BarCode { get; set; }
[JsonPropertyName("hold_id")] public string HoldId { get; set; }
[JsonPropertyName("ticket_type_id")] public string TicketTypeId { get; set; }
}
public class Hold
{
[JsonPropertyName("id")] public string Id { get; set; }
[JsonPropertyName("note")] public string Note { get; set; }
[JsonPropertyName("total_on_hold")] public int TotalOnHold { get; set; }
[JsonPropertyName("quantities")] public HoldQuantity[] Quantities { get; set; }
}
public class HoldQuantity
{
[JsonPropertyName("ticket_type_id")] public string TicketTypeId { get; set; }
[JsonPropertyName("quantity")] public int Quantity { get; set; }
}
public class CreateHoldRequest
{
[JsonPropertyName("event_id")] public string EventId { get; set; }
[JsonPropertyName("note")] public string Note { get; set; }
[JsonPropertyName("ticket_type_id")] public Dictionary<string, int> TicketTypeId { get; set; }
}
public class EventEnd
{
@@ -134,7 +206,7 @@ public class TicketTailorClient : IDisposable
{
[JsonPropertyName("id")] public string Id { get; set; }
[JsonPropertyName("max_per_order")] public object MaxPerOrder { get; set; }
[JsonPropertyName("max_per_order")] public int? MaxPerOrder { get; set; }
[JsonPropertyName("name")] public string Name { get; set; }
@@ -149,7 +221,7 @@ public class TicketTailorClient : IDisposable
[JsonPropertyName("id")] public string Id { get; set; }
[JsonPropertyName("access_code")] public object AccessCode { get; set; }
[JsonPropertyName("access_code")] public string AccessCode { get; set; }
[JsonPropertyName("booking_fee")] public int BookingFee { get; set; }
@@ -157,9 +229,9 @@ public class TicketTailorClient : IDisposable
[JsonPropertyName("group_id")] public string GroupId { get; set; }
[JsonPropertyName("max_per_order")] public int MaxPerOrder { get; set; }
[JsonPropertyName("max_per_order")] public int? MaxPerOrder { get; set; }
[JsonPropertyName("min_per_order")] public int MinPerOrder { get; set; }
[JsonPropertyName("min_per_order")] public int? MinPerOrder { get; set; }
[JsonPropertyName("name")] public string Name { get; set; }

View File

@@ -10,6 +10,7 @@ using System.Threading.Tasks;
using AngleSharp;
using BTCPayServer.Abstractions.Contracts;
using BTCPayServer.Abstractions.Extensions;
using BTCPayServer.Abstractions.Models;
using BTCPayServer.Client;
using BTCPayServer.Client.Models;
using Microsoft.AspNetCore.Authorization;
@@ -57,11 +58,9 @@ namespace BTCPayServer.Plugins.TicketTailor
return NotFound();
}
[AllowAnonymous]
[HttpPost("")]
public async Task<IActionResult> Purchase(string storeId, string ticketTypeId, string firstName,
string lastName, string email)
public async Task<IActionResult> Purchase(string storeId, TicketTailorViewModel request)
{
var config = await _ticketTailorService.GetTicketTailorForStore(storeId);
try
@@ -75,35 +74,87 @@ namespace BTCPayServer.Plugins.TicketTailor
return NotFound();
}
var ticketType = evt.TicketTypes.FirstOrDefault(type => type.Id == ticketTypeId);
var specificTicket =
config.SpecificTickets?.SingleOrDefault(ticket => ticketType?.Id == ticket.TicketTypeId);
if (ticketType is not null && specificTicket is not null)
var price = 0m;
foreach (var purchaseRequestItem in request.Items)
{
ticketType.Price = specificTicket.Price.GetValueOrDefault(ticketType.Price);
if (purchaseRequestItem.Quantity <= 0)
{
continue;;
}
var ticketType = evt.TicketTypes.FirstOrDefault(type => type.Id == purchaseRequestItem.TicketTypeId);
var specificTicket =
config.SpecificTickets?.SingleOrDefault(ticket => ticketType?.Id == ticket.TicketTypeId);
if ((config.SpecificTickets?.Any() is true && specificTicket is null) || ticketType is null ||
(!string.IsNullOrEmpty(ticketType.AccessCode) &&
!ticketType.AccessCode.Equals(request.AccessCode, StringComparison.InvariantCultureIgnoreCase)) ||
!new []{"on_sale" , "locked"}.Contains(ticketType.Status.ToLowerInvariant())
|| specificTicket?.Hidden is true)
{
TempData.SetStatusMessageModel(new StatusMessageModel
{
Severity = StatusMessageModel.StatusSeverity.Error,
Html = "The ticket was not found."
});
return RedirectToAction("View", new {storeId});
}
if (purchaseRequestItem.Quantity > ticketType.MaxPerOrder ||
purchaseRequestItem.Quantity < ticketType.MinPerOrder )
{
TempData.SetStatusMessageModel(new StatusMessageModel
{
Severity = StatusMessageModel.StatusSeverity.Error,
Html = "The amount of tickets was not allowed."
});
return RedirectToAction("View", new {storeId});
}
var ticketCost = ticketType.Price;
if (specificTicket is not null)
{
ticketCost =specificTicket.Price.GetValueOrDefault(ticketType.Price);
}
price += ticketCost * purchaseRequestItem.Quantity;
}
if (ticketType is null || (specificTicket is null && ticketType.Status != "on_sale") ||
ticketType.Quantity <= 0)
var hold = await client.CreateHold(new TicketTailorClient.CreateHoldRequest()
{
return NotFound();
EventId = evt.Id,
Note = "Created by BTCPay Server",
TicketTypeId = request.Items.ToDictionary(item => item.TicketTypeId, item => item.Quantity)
});
if (!string.IsNullOrEmpty(hold.error))
{
TempData.SetStatusMessageModel(new StatusMessageModel
{
Severity = StatusMessageModel.StatusSeverity.Error,
Html = $"Could not reserve tickets because {hold.error}"
});
return RedirectToAction("View", new {storeId});
}
var btcpayClient = await CreateClient(storeId);
var redirectUrl = Request.GetAbsoluteUri(Url.Action("Receipt",
"TicketTailor", new {storeId, invoiceId = "kukkskukkskukks"}));
redirectUrl = redirectUrl.Replace("kukkskukkskukks", "{InvoiceId}");
if(string.IsNullOrEmpty(request.Name))
{
request.Name = "Anonymous lizard";
}
var inv = await btcpayClient.CreateInvoice(storeId,
new CreateInvoiceRequest()
{
Amount = ticketType.Price,
Amount = price,
Currency = evt.Currency,
Type = InvoiceType.Standard,
AdditionalSearchTerms = new[] {"tickettailor", ticketTypeId, evt.Id},
AdditionalSearchTerms = new[] {"tickettailor", hold.Item1.Id, evt.Id},
Checkout =
{
RequiresRefundEmail = true,
RedirectAutomatically = ticketType.Price > 0,
RedirectAutomatically = price > 0,
RedirectURL = redirectUrl,
},
Receipt = new InvoiceDataBase.ReceiptOptions()
@@ -112,7 +163,11 @@ namespace BTCPayServer.Plugins.TicketTailor
},
Metadata = JObject.FromObject(new
{
buyerName = $"{firstName} {lastName}", buyerEmail = email, ticketTypeId,orderId="tickettailor"
btcpayUrl = Request.GetAbsoluteRoot(),
buyerName = request.Name,
buyerEmail = request.Email,
holdId = hold.Item1.Id,
orderId="tickettailor"
})
});
@@ -124,7 +179,8 @@ namespace BTCPayServer.Plugins.TicketTailor
if (inv.Status == InvoiceStatus.Settled)
return RedirectToAction("Receipt", new {storeId, invoiceId = inv.Id});
return Redirect(inv.CheckoutLink);
return RedirectToAction("Checkout","UIInvoice", new {invoiceId = inv.Id});
}
}
catch (Exception e)
@@ -145,21 +201,12 @@ namespace BTCPayServer.Plugins.TicketTailor
var result = new TicketReceiptPage() {InvoiceId = invoiceId};
var invoice = await btcpayClient.GetInvoice(storeId, invoiceId);
result.Status = invoice.Status;
if (invoice.Status == InvoiceStatus.Settled)
if (invoice.Status == InvoiceStatus.Settled &&
invoice.Metadata.TryGetValue("orderId", out var orderId) && orderId.Value<string>() == "tickettailor" &&
invoice.Metadata.TryGetValue("ticketIds", out var ticketIds))
{
await SetTicketTailorTicketResult(storeId, result, ticketIds.Values<string>());
if (invoice.Metadata.TryGetValue("ticketId", out var ticketId))
{
await SetTicketTailorTicketResult(storeId, result, ticketId);
}
else
{
invoice = await _ticketTailorService.Handle(invoice.Id, storeId, Request.GetAbsoluteRootUri());
if (invoice.Metadata.TryGetValue("ticketId", out ticketId))
{
await SetTicketTailorTicketResult(storeId, result, ticketId);
}
}
}
return View(result);
@@ -170,15 +217,14 @@ namespace BTCPayServer.Plugins.TicketTailor
}
}
private async Task SetTicketTailorTicketResult(string storeId, TicketReceiptPage result, JToken ticketId)
private async Task SetTicketTailorTicketResult(string storeId, TicketReceiptPage result, IEnumerable<string> ticketIds)
{
var settings = await _ticketTailorService.GetTicketTailorForStore(storeId);
var client = new TicketTailorClient(_httpClientFactory, settings.ApiKey);
result.Ticket = await client.GetTicket(ticketId.ToString());
var tickets = await Task.WhenAll(ticketIds.Select(s => client.GetTicket(s)));
var evt = await client.GetEvent(settings.EventId);
result.Event = evt;
result.TicketType =
evt.TicketTypes.FirstOrDefault(type => type.Id == result.Ticket.TicketTypeId);
result.Tickets = tickets;
result.Settings = settings;
}
@@ -200,9 +246,8 @@ namespace BTCPayServer.Plugins.TicketTailor
{
public string InvoiceId { get; set; }
public InvoiceStatus Status { get; set; }
public TicketTailorClient.IssuedTicket Ticket { get; set; }
public TicketTailorClient.IssuedTicket[] Tickets { get; set; }
public TicketTailorClient.Event Event { get; set; }
public TicketTailorClient.TicketType TicketType { get; set; }
public TicketTailorSettings Settings { get; set; }
}
@@ -210,28 +255,22 @@ namespace BTCPayServer.Plugins.TicketTailor
private readonly IHttpClientFactory _httpClientFactory;
private readonly TicketTailorService _ticketTailorService;
private readonly IBTCPayServerClientFactory _btcPayServerClientFactory;
private readonly IConfiguration _configuration;
private readonly LinkGenerator _linkGenerator;
public TicketTailorController(IHttpClientFactory httpClientFactory,
TicketTailorService ticketTailorService,
IBTCPayServerClientFactory btcPayServerClientFactory,
IConfiguration configuration,
LinkGenerator linkGenerator )
IBTCPayServerClientFactory btcPayServerClientFactory)
{
_httpClientFactory = httpClientFactory;
_ticketTailorService = ticketTailorService;
_btcPayServerClientFactory = btcPayServerClientFactory;
_configuration = configuration;
_linkGenerator = linkGenerator;
}
[HttpGet("update")]
public async Task<IActionResult> UpdateTicketTailorSettings(string storeId)
{
UpdateTicketTailorSettingsViewModel vm = new();
TicketTailorSettings TicketTailor = null;
TicketTailorSettings TicketTailor;
try
{
TicketTailor = await _ticketTailorService.GetTicketTailorForStore(storeId);
@@ -304,8 +343,7 @@ namespace BTCPayServer.Plugins.TicketTailor
[HttpPost("update")]
public async Task<IActionResult> UpdateTicketTailorSettings(string storeId,
UpdateTicketTailorSettingsViewModel vm,
string command,
[FromServices] BTCPayServerClient btcPayServerClient)
string command)
{
vm = await SetValues(vm);
@@ -340,115 +378,6 @@ namespace BTCPayServer.Plugins.TicketTailor
BypassAvailabilityCheck = vm.BypassAvailabilityCheck
};
var bindAddress = _configuration.GetValue("bind", IPAddress.Loopback);
if (Equals(bindAddress, IPAddress.Any))
{
bindAddress = IPAddress.Loopback;
}
if (Equals(bindAddress, IPAddress.IPv6Any))
{
bindAddress = IPAddress.IPv6Loopback;
}
int bindPort = _configuration.GetValue<int>("port", 443);
string rootPath = _configuration.GetValue<string>("rootpath", "/");
string attempt1 = null;
if (bindAddress is not null)
{
attempt1 = _linkGenerator.GetUriByAction("Callback",
"TicketTailor", new {storeId,test= true}, "https", new HostString(bindAddress?.ToString(), bindPort),
new PathString(rootPath));
}
var attempt2 = Request.GetAbsoluteUri(Url.Action("Callback",
"TicketTailor", new {storeId, test= true}));
HttpRequestMessage Create(string uri)
{
return new HttpRequestMessage(HttpMethod.Post, uri)
{
Content = new StringContent(
JsonConvert.SerializeObject(new WebhookInvoiceEvent(WebhookEventType.InvoiceSettled))
,Encoding.UTF8,
"application/json"),
};
}
HttpClient CreateClient(string uri)
{
var link = new Uri(uri);
if (link.IsLoopback)
{
return _httpClientFactory.CreateClient("greenfield-webhook.loopback");
}else if (link.Host.EndsWith("onion"))
{
return _httpClientFactory.CreateClient("greenfield-webhook.onion");
}
else
{
return _httpClientFactory.CreateClient("greenfield-webhook.clearnet");
}
}
HttpResponseMessage result = null;
if (attempt1 is not null)
{
try
{
result = await CreateClient(attempt1).SendAsync(Create(attempt1), CancellationToken.None);
}
catch (Exception e)
{
}
}
string webhookUrl = null;
if (result?.IsSuccessStatusCode is true)
{
webhookUrl = _linkGenerator.GetUriByAction("Callback",
"TicketTailor", new {storeId}, "http", new HostString(bindAddress.ToString(), bindPort),
new PathString(rootPath));;
}
else
{
try
{
result = null;
result = await CreateClient(attempt2).SendAsync(Create(attempt2), CancellationToken.None);
}
catch (Exception e)
{
}
if (result?.IsSuccessStatusCode is true)
{
webhookUrl = Request.GetAbsoluteUri(Url.Action("Callback",
"TicketTailor", new {storeId}));;
}
}
if (webhookUrl is null)
{
ModelState.AddModelError("", $"{attempt1} or {attempt2} was not reachable by BTCPayServer.");
return View(vm);
}else if (vm.ApiKey is not null && vm.EventId is not null)
{
var webhooks = await btcPayServerClient.GetWebhooks(storeId);
var webhook = webhooks.FirstOrDefault(data => data.Enabled && data.Url == webhookUrl && (data.AuthorizedEvents.Everything || data.AuthorizedEvents.SpecificEvents.Contains(WebhookEventType.InvoiceSettled)));
if (webhook is null)
{
await CreateWebhook(storeId, btcPayServerClient, webhookUrl);
}
}
switch (command?.ToLowerInvariant())
{
@@ -462,42 +391,6 @@ namespace BTCPayServer.Plugins.TicketTailor
}
}
private static async Task<string> CreateWebhook(string storeId, BTCPayServerClient btcPayServerClient,
string webhookUrl)
{
var wh = await btcPayServerClient.CreateWebhook(storeId,
new CreateStoreWebhookRequest()
{
Enabled = true,
Url = webhookUrl,
AuthorizedEvents = new StoreWebhookBaseData.AuthorizedEventsData()
{
Everything = false,
SpecificEvents = new[] {WebhookEventType.InvoiceSettled}
},
AutomaticRedelivery = true
});
return wh.Id;
}
[AllowAnonymous]
[HttpPost("callback")]
public async Task<IActionResult> Callback(string storeId, [FromBody] WebhookInvoiceSettledEvent response, [FromQuery ]bool test)
{
if (test)
{
return Ok();
}
if (response.StoreId != storeId && response.Type != WebhookEventType.InvoiceSettled)
{
return BadRequest();
}
await _ticketTailorService.Handle(response.InvoiceId, response.StoreId, Request.GetAbsoluteRootUri());
return Ok();
}
}
}

View File

@@ -1,21 +1,25 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Threading;
using System.Threading.Channels;
using System.Threading.Tasks;
using BTCPayServer.Abstractions.Contracts;
using BTCPayServer.Client;
using BTCPayServer.Client.Models;
using BTCPayServer.Events;
using BTCPayServer.HostedServices;
using BTCPayServer.Services.Invoices;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Routing;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json.Linq;
using static System.String;
namespace BTCPayServer.Plugins.TicketTailor;
public class TicketTailorService : IHostedService
public class TicketTailorService : EventHostedServiceBase
{
private readonly ISettingsRepository _settingsRepository;
private readonly IMemoryCache _memoryCache;
@@ -28,7 +32,8 @@ public class TicketTailorService : IHostedService
public TicketTailorService(ISettingsRepository settingsRepository, IMemoryCache memoryCache,
IHttpClientFactory httpClientFactory,
IStoreRepository storeRepository, ILogger<TicketTailorService> logger,
IBTCPayServerClientFactory btcPayServerClientFactory, LinkGenerator linkGenerator)
IBTCPayServerClientFactory btcPayServerClientFactory, LinkGenerator linkGenerator,
EventAggregator eventAggregator) : base(eventAggregator, logger)
{
_settingsRepository = settingsRepository;
_memoryCache = memoryCache;
@@ -68,169 +73,210 @@ public class TicketTailorService : IHostedService
}
public Task<InvoiceData> Handle(string invoiceId, string storeId, Uri host)
{
var tcs = new TaskCompletionSource<InvoiceData>();
_events.Writer.TryWrite(new IssueTicket() {Task = tcs, InvoiceId = invoiceId, StoreId = storeId, Host = host});
return tcs.Task;
}
internal class IssueTicket
{
public string InvoiceId { get; set; }
public string StoreId { get; set; }
public TaskCompletionSource<InvoiceData?> Task { get; set; }
public Uri Host { get; set; }
}
readonly Channel<IssueTicket> _events = Channel.CreateUnbounded<IssueTicket>();
public Task StartAsync(CancellationToken cancellationToken)
protected override void SubscribeToEvents()
{
_ = ProcessEvents(cancellationToken);
return Task.CompletedTask;
Subscribe<InvoiceEvent>();
Subscribe<IssueTicket>();
base.SubscribeToEvents();
}
public Task StopAsync(CancellationToken cancellationToken)
public async Task<BTCPayServerClient> CreateClient(string storeId)
{
return Task.CompletedTask;
return await _btcPayServerClientFactory.Create(null, new[] {storeId});
}
public async Task<BTCPayServerClient> CreateClient(string storeId, Uri host)
protected override async Task ProcessEvent(object evt, CancellationToken cancellationToken)
{
return await _btcPayServerClientFactory.Create(null, new []{storeId}, new DefaultHttpContext()
if (evt is InvoiceEvent invoiceEvent)
{
Request =
if (invoiceEvent.Invoice.Metadata.OrderId != "tickettailor" || !new []{InvoiceStatus.Settled, InvoiceStatus.Expired, InvoiceStatus.Invalid}.Contains(invoiceEvent.Invoice.GetInvoiceState().Status.ToModernStatus()))
{
Scheme = host.Scheme,
Host = new HostString(host.Host),
Path = new PathString(host.AbsolutePath),
PathBase = new PathString(),
}
});
}
private async Task ProcessEvents(CancellationToken cancellationToken)
{
while (await _events.Reader.WaitToReadAsync(cancellationToken))
{
if (!_events.Reader.TryRead(out var evt)) continue;
async Task HandleIssueTicketError(JToken posData, string e, InvoiceData invoiceData,
BTCPayServerClient btcPayClient)
{
posData["Error"] =
$"Ticket could not be created. You should refund customer.{Environment.NewLine}{e}";
invoiceData.Metadata["posData"] = posData;
await btcPayClient.UpdateInvoice(evt.StoreId, invoiceData.Id,
new UpdateInvoiceRequest() {Metadata = invoiceData.Metadata}, cancellationToken);
try
{
await btcPayClient.MarkInvoiceStatus(evt.StoreId, invoiceData.Id,
new MarkInvoiceStatusRequest() {Status = InvoiceStatus.Invalid}, cancellationToken);
}
catch (Exception exception)
{
_logger.LogError(exception, $"Failed to update invoice {invoiceData.Id} status from {invoiceData.Status} to Invalid after failing to issue ticket from ticket tailor");
}
return;
}
InvoiceData invoice = null;
evt = new IssueTicket() {InvoiceId = invoiceEvent.InvoiceId, StoreId = invoiceEvent.Invoice.StoreId};
}
if (evt is not IssueTicket issueTicket)
return;
async Task HandleIssueTicketError(JToken posData, string e, InvoiceData invoiceData,
BTCPayServerClient btcPayClient)
{
posData["Error"] =
$"Ticket could not be created. You should refund customer.{Environment.NewLine}{e}";
invoiceData.Metadata["posData"] = posData;
await btcPayClient.UpdateInvoice(issueTicket.StoreId, invoiceData.Id,
new UpdateInvoiceRequest() {Metadata = invoiceData.Metadata}, cancellationToken);
try
{
var settings = await GetTicketTailorForStore(evt.StoreId);
if (settings is null || settings.ApiKey is null)
{
evt.Task.SetResult(null);
continue;
}
await btcPayClient.MarkInvoiceStatus(issueTicket.StoreId, invoiceData.Id,
new MarkInvoiceStatusRequest() {Status = InvoiceStatus.Invalid}, cancellationToken);
}
catch (Exception exception)
{
_logger.LogError(exception,
$"Failed to update invoice {invoiceData.Id} status from {invoiceData.Status} to Invalid after failing to issue ticket from ticket tailor");
}
}
var btcPayClient = await CreateClient(evt.StoreId, evt.Host);
invoice = await btcPayClient.GetInvoice(evt.StoreId, evt.InvoiceId, cancellationToken);
if (invoice.Status != InvoiceStatus.Settled)
{
evt.Task.SetResult(null);
continue;
}
InvoiceData invoice = null;
try
{
var settings = await GetTicketTailorForStore(issueTicket.StoreId);
if (settings is null || settings.ApiKey is null)
{
return;
}
if (invoice.Metadata.ContainsKey("ticketId"))
{
evt.Task.SetResult(null);
continue;
}
var btcPayClient = await CreateClient(issueTicket.StoreId);
invoice = await btcPayClient.GetInvoice(issueTicket.StoreId, issueTicket.InvoiceId, cancellationToken);
var ticketTypeId = invoice.Metadata["ticketTypeId"].ToString();
var email = invoice.Metadata["buyerEmail"].ToString();
var name = invoice.Metadata["buyerName"]?.ToString();
invoice.Metadata.TryGetValue("posData", out var posData);
posData ??= new JObject();
var client = new TicketTailorClient(_httpClientFactory, settings.ApiKey);
try
if (new[] {InvoiceStatus.Invalid, InvoiceStatus.Expired}.Contains(invoice.Status))
{
if (invoice.Metadata.TryGetValue("holdId", out var jHoldIdx) &&
jHoldIdx.Value<string>() is { } holdIdx)
{
var ticketResult = await client.CreateTicket(new TicketTailorClient.IssueTicketRequest()
if (await new TicketTailorClient(_httpClientFactory, settings.ApiKey).DeleteHold(holdIdx))
{
Reference = invoice.Id,
Email = email,
EventId = settings.EventId,
TicketTypeId = ticketTypeId,
FullName = name,
});
if (ticketResult.Item2 is not null)
{
await HandleIssueTicketError(posData, ticketResult.Item2, invoice, btcPayClient);
continue;
}
var ticket = ticketResult.Item1;
invoice.Metadata["ticketId"] = ticket.Id;
invoice.Metadata["orderId"] = $"tickettailor_{ticket.Id}";
posData["Ticket Code"] = ticket.Barcode;
posData["Ticket Id"] = ticket.Id;
invoice.Metadata["posData"] = posData;
await btcPayClient.UpdateInvoice(evt.StoreId, invoice.Id,
new UpdateInvoiceRequest() {Metadata = invoice.Metadata}, cancellationToken);
var url =
_linkGenerator.GetUriByAction("Receipt",
"TicketTailor",
new {evt.StoreId, invoiceId = invoice.Id},
evt.Host.Scheme,
new HostString(evt.Host.Host),
evt.Host.AbsolutePath);
try
{
await btcPayClient.SendEmail(evt.StoreId,
new SendEmailRequest()
invoice.Metadata.Remove("holdId");
invoice.Metadata.Add("holdId_deleted", holdIdx);
await btcPayClient.UpdateInvoice(issueTicket.StoreId, issueTicket.InvoiceId,
new UpdateInvoiceRequest()
{
Subject = "Your ticket is available now.",
Email = email,
Body =
$"Your payment has been settled and the event ticket has been issued successfully. Please go to <a href='{url}'>{url}</a>"
Metadata = invoice.Metadata
}, cancellationToken);
}
catch (Exception e)
}
return;
}
if (invoice.Status != InvoiceStatus.Settled)
{
return;
}
if (invoice.Metadata.TryGetValue("ticketIds", out var jTicketId) &&
jTicketId.Values<string>() is { } ticketIds)
{
return;
}
if (!invoice.Metadata.TryGetValue("holdId", out var jHoldId) ||
jHoldId.Value<string>() is not { } holdId)
{
return;
}
if (!invoice.Metadata.TryGetValue("btcpayUrl", out var jbtcpayUrl) ||
jbtcpayUrl.Value<string>() is not { } btcpayUrl)
{
return;
}
var email = invoice.Metadata["buyerEmail"].ToString();
var name = invoice.Metadata["buyerName"]?.ToString();
invoice.Metadata.TryGetValue("posData", out var posData);
posData ??= new JObject();
var client = new TicketTailorClient(_httpClientFactory, settings.ApiKey);
try
{
var tickets = new List<TicketTailorClient.IssuedTicket>();
var errors = new List<string>();
var hold = await client.GetHold(holdId);
if (hold is null)
{
await HandleIssueTicketError(posData, "The hold created for this invoice was not found", invoice, btcPayClient);
return;
}
var holdOriginalAmount = hold?.TotalOnHold;
while (hold?.TotalOnHold > 0)
{
foreach (var tt in hold.Quantities.Where(quantity => quantity.Quantity > 0))
{
// ignored
var ticketResult = await client.CreateTicket(new TicketTailorClient.IssueTicketRequest()
{
Reference = invoice.Id,
Email = email,
EventId = settings.EventId,
HoldId = holdId,
FullName = name,
TicketTypeId = tt.TicketTypeId
});
if (ticketResult.error is null)
{
tickets.Add(ticketResult.Item1);
}
else
{
errors.Add(ticketResult.error);
}
hold = await client.GetHold(holdId);
}
}
if (tickets.Count != holdOriginalAmount)
{
await HandleIssueTicketError(posData, $"Not all the held tickets were issued because: {Join(",", errors)}", invoice, btcPayClient);
return;
}
invoice.Metadata["ticketIds"] =
new JArray(tickets.Select(issuedTicket => issuedTicket.Id));
posData["Ticket Id"] = invoice.Metadata["ticketIds"];
invoice.Metadata["posData"] = posData;
await btcPayClient.UpdateInvoice(issueTicket.StoreId, invoice.Id,
new UpdateInvoiceRequest() {Metadata = invoice.Metadata}, cancellationToken);
var uri = new Uri(btcpayUrl);
var url =
_linkGenerator.GetUriByAction("Receipt",
"TicketTailor",
new {issueTicket.StoreId, invoiceId = invoice.Id},
uri.Scheme,
new HostString(uri.Host),
uri.AbsolutePath);
try
{
await btcPayClient.SendEmail(issueTicket.StoreId,
new SendEmailRequest()
{
Subject = "Your ticket is available now.",
Email = email,
Body =
$"Your payment has been settled and the event ticket has been issued successfully. Please go to <a href='{url}'>{url}</a>"
}, cancellationToken);
}
catch (Exception e)
{
await HandleIssueTicketError(posData, e.Message, invoice, btcPayClient);
// ignored
}
}
catch (Exception ex)
catch (Exception e)
{
_logger.LogError(ex, "Failed to issue ticket");
}
finally
{
evt.Task.SetResult(invoice);
await HandleIssueTicketError(posData, e.Message, invoice, btcPayClient);
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to issue ticket");
}
}
}

View File

@@ -30,4 +30,16 @@ public class TicketTailorViewModel
{
public TicketTailorClient.Event Event { get; set; }
public TicketTailorSettings Settings { get; set; }
public string Name { get; set; }
public string Email { get; set; }
public PurchaseRequestItem[] Items { get; set; }
public string AccessCode { get; set; }
public class PurchaseRequestItem
{
public string TicketTypeId { get; set; }
public int Quantity { get; set; }
}
}

View File

@@ -14,6 +14,9 @@
footer {
display: none;
}
.page-break {
page-break-after: always;
}
</style>
<div class="container d-flex h-100">
<div class="justify-content-center align-self-center text-center mx-auto px-2 w-100 m-auto">
@@ -37,7 +40,7 @@
The invoice is not settled.
</div>
}
else if (Model.Ticket is null)
else if (Model.Tickets?.Any() is not true)
{
reloadPage = true;
@@ -47,42 +50,83 @@
}
else
{
var specificTicketName = Model.Settings?.SpecificTickets?.FirstOrDefault(ticket => ticket.TicketTypeId == Model.TicketType.Id)?.Name ?? Model.TicketType.Name;
<div class="row mb-4">
<div class="col col-12 col-lg-6 mb-md-0 mb-sm-4">
<div class="bg-tile h-100 m-0 p-3 p-sm-5 rounded">
<h2 class="h4 mb-3">Ticket Details</h2>
<div class="d-flex mb-4">
<div class="col-6 px-1">
<img src="@Model.Ticket.QrCodeUrl" alt="Please ensure you can see this QR barcode" class="w-100">
</div>
<div class="col-6 px-1 py-4">
<img src="@Model.Ticket.BarcodeUrl" alt="Please ensure you can see this barcode" class="w-100">
</div>
</div>
<div class="d-flex d-print-inline-block flex-column mb-4 ">
<dt class="h3 fw-semibold text-print-default order-2 order-sm-1 ">@Model.Ticket.Barcode</dt> <dd class="order-1 order-sm-2 ">TICKET CODE</dd>
</div>
@if (!string.IsNullOrEmpty(Model.Ticket.Reference))
{
<div class="d-flex d-print-inline-block flex-column mb-4 px-4 ">
<dt class="h3 fw-semibold text-print-default order-2 order-sm-1 ">@Model.Ticket.Reference</dt> <dd class="order-1 order-sm-2 ">REFERENCE</dd>
</div>
}
@if (!string.IsNullOrEmpty(Model.Ticket.FullName))
<div class="col col-12 col-lg-6 mb-md-0 mb-sm-4 bg-tile">
@{
for (var index = 0; index < Model.Tickets.Length; index++)
{
<div class="d-flex d-print-inline-block flex-column ">
<dt class="h3 fw-semibold text-print-default order-2 order-sm-1 ">@Model.Ticket.FullName</dt> <dd class="order-1 order-sm-2 ">ATTENDEE NAME</dd>
var ticket = Model.Tickets[index];
var ticketType = Model.Event.TicketTypes.First(type => type.Id == ticket.TicketTypeId);
var specificTicketType = Model.Settings?.SpecificTickets?
.FirstOrDefault(specificTicket => specificTicket.TicketTypeId == ticket.TicketTypeId);
ticketType.Name = specificTicketType?.Name ?? ticketType.Name;
ticketType.Description = specificTicketType?.Description ?? ticketType.Description;
<div class="bg-tile m-0 p-3 p-sm-5 rounded ">
<h2 class="h4 mb-3">Ticket Details</h2>
<div class="d-flex mb-4">
<div class="col-6 px-1">
<img src="@ticket.QrCodeUrl" alt="Please ensure you can see this QR barcode" class="w-100">
</div>
<div class="col-6 px-1 py-4">
<img src="@ticket.BarcodeUrl" alt="Please ensure you can see this barcode" class="w-100">
</div>
</div>
<div class="d-flex d-print-inline-block flex-column mb-4 ">
<dt class="h3 fw-semibold text-print-default order-2 order-sm-1 ">@ticket.Barcode</dt> <dd class="order-1 order-sm-2 ">TICKET CODE</dd>
</div>
@if (!string.IsNullOrEmpty(ticket.Reference))
{
<div class="d-flex d-print-inline-block flex-column mb-4 px-4 ">
<dt class="h3 fw-semibold text-print-default order-2 order-sm-1 ">@ticket.Reference</dt> <dd class="order-1 order-sm-2 ">REFERENCE</dd>
</div>
}
@if (!string.IsNullOrEmpty(ticket.FullName))
{
<div class="d-flex d-print-inline-block flex-column ">
<dt class="h3 fw-semibold text-print-default order-2 order-sm-1 ">@ticket.FullName</dt> <dd class="order-1 order-sm-2 ">ATTENDEE NAME</dd>
</div>
}
<div class="d-flex d-print-inline-block flex-column ">
<dt class="h3 fw-semibold text-print-default order-2 order-sm-1 ">@ticketType.Name</dt> <dd class="order-1 order-sm-2 ">TICKET TYPE</dd>
</div>
</div>
@if (Model.Tickets.Length > 1)
{
<div class="m-0 p-3 p-sm-5 d-none d-print-block ">
<div class="d-flex d-print-inline-block flex-column mb-4 ">
<dt class="h3 fw-semibold text-nowrap text-print-default order-2 order-sm-1 ">@Model.Event.Title</dt> <dd class="order-1 order-sm-2 ">EVENT</dd>
</div>
<div class="d-flex d-print-inline-block flex-column mb-4 ">
<dt class="h3 fw-semibold text-print-default order-2 order-sm-1 ">
<a href="@Model.Event.Url" style="word-wrap: break-word;" target="_blank">@Model.Event.Url</a>
</dt> <dd class="order-1 order-sm-2 ">EVENT URL</dd>
</div>
<div class="d-flex d-print-inline-block flex-column mb-4 ">
<dt class="h3 fw-semibold text-print-default order-2 order-sm-1 ">@Model.Event.Start.Formatted - @Model.Event.EventEnd.Formatted</dt>
<dd class="order-1 order-sm-2 ">Date</dd>
</div>
@if (!string.IsNullOrEmpty(Model.Event.Venue.Name))
{
<div class="d-flex d-print-inline-block flex-column ">
<dt class="h3 fw-semibold text-print-default order-2 order-sm-1 ">@Model.Event.Venue.Name</dt>
<dd class="order-1 order-sm-2 ">Venue</dd>
</div>}
</div>
<hr class="page-break"/>
}
}
<div class="d-flex d-print-inline-block flex-column ">
<dt class="h3 fw-semibold text-print-default order-2 order-sm-1 ">@specificTicketName</dt> <dd class="order-1 order-sm-2 ">TICKET TYPE</dd>
</div>
</div>
}
</div>
<div class="col col-12 col-lg-6 mb-md-0 mb-sm-4">
<div class="col col-12 col-lg-6 mb-md-0 mb-sm-4 @(Model.Tickets.Length > 1? "d-print-none": "")">
<div class="bg-tile h-100 m-0 p-3 p-sm-5 rounded">
<h2 class="h4 mb-3 d-print-none">Event Details</h2>
<div class="d-flex d-print-inline-block flex-column mb-4 ">
@@ -97,10 +141,13 @@
<dt class="h3 fw-semibold text-print-default order-2 order-sm-1 ">@Model.Event.Start.Formatted - @Model.Event.EventEnd.Formatted</dt>
<dd class="order-1 order-sm-2 ">Date</dd>
</div>
<div class="d-flex d-print-inline-block flex-column ">
<dt class="h3 fw-semibold text-print-default order-2 order-sm-1 ">@Model.Event.Venue.Name</dt>
<dd class="order-1 order-sm-2 ">Venue</dd>
</div>
@if (!string.IsNullOrEmpty(Model.Event.Venue.Name))
{
<div class="d-flex d-print-inline-block flex-column ">
<dt class="h3 fw-semibold text-print-default order-2 order-sm-1 ">@Model.Event.Venue.Name</dt>
<dd class="order-1 order-sm-2 ">Venue</dd>
</div>}
</div>
</div>
</div>

View File

@@ -20,8 +20,7 @@
{
<div class="alert alert-warning">
Please ensure that emails in your store are configured if you wish to send the tickets via email to customers as TicketTailor does not handle it for tickets issued via its API.
<br/>
Ensure that the url used for btcpayserver is accessible publicly.
</div>
}
<div class="row">

View File

@@ -1,28 +1,24 @@
@using Microsoft.AspNetCore.Routing
@using BTCPayServer.Plugins.TicketTailor
@using Microsoft.AspNetCore.Mvc.TagHelpers
@using NBitcoin
@model BTCPayServer.Plugins.TicketTailor.TicketTailorViewModel
@inject BTCPayServer.Security.ContentSecurityPolicies csp;
@{
var nonce = RandomUtils.GetUInt256().ToString().Substring(0, 32);
csp.Add("script-src", $"'nonce-{nonce}'");
csp.AllowUnsafeHashes();
Layout = "_LayoutSimple";
var available = Model.Settings.BypassAvailabilityCheck || (Model.Event.Unavailable != "true" && Model.Event.TicketsAvailable == "true");
Model.Settings.SpecificTickets ??= new List<SpecificTicket>();
Context.Request.Query.TryGetValue("accessCode", out var accessCode);
}
<style>
.card-deck {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
grid-gap: 1.5rem;
hr:last-child{
display: none;
}
.card-deck .card:only-of-type {
max-width: 320px;
margin: auto !important;
}
footer {
display: none;
}
@@ -33,109 +29,169 @@ footer {
}
</style>
<script nonce="@nonce">
document.addEventListener("DOMContentLoaded", ()=>{
const btn = document.querySelector("button[type='submit']");
document.querySelectorAll("input").forEach(value => value.addEventListener("change", (evt)=>{
if (!!evt.target.value && parseInt(evt.target.value) > 0){
btn.style.display = "block";
}
let total = 0;
document.querySelectorAll("[data-price]").forEach(value1 => {
if (!!value1.value){
const qty = parseInt(value1.value);
if (qty > 0){
const price = parseInt(value1.dataset.price);
total += price * qty;
}
}
});
btn.textContent = `Purchase for ${total}${@Safe.Json(@Model.Event.Currency)}`
}))
document.querySelector("form").addEventListener("submit", ()=>{
btn.setAttribute("disabled", "disabled");
})
})
</script>
<div class="container d-flex h-100">
<div class="justify-content-center align-self-center text-center mx-auto px-2 py-3 w-100 m-auto">
<div class="justify-content-center mx-auto px-2 py-3 w-100 m-auto">
<partial name="_StatusMessage"/>
<h1 >@Model.Event.Title</h1>
<h2 class="text-muted mb-4">@Model.Event.Start.Formatted - @Model.Event.EventEnd.Formatted</h2>
<h1 class="text-center ">@Model.Event.Title</h1>
<h2 class="text-muted mb-4 text-center ">@Model.Event.Start.Formatted - @Model.Event.EventEnd.Formatted</h2>
@if (Model.Settings.ShowDescription && !string.IsNullOrEmpty(Model.Event.Description))
{
<div class="row" id="ticket-tailor-description">
<div class="overflow-hidden col-12">@Safe.Raw(Model.Event.Description)</div>
<div class="row" id="ticket-tailor-description text-center ">
<div class="overflow-hidden col-12 ">@Safe.Raw(Model.Event.Description)</div>
</div>
}
<form method="post" asp-controller="TicketTailor" asp-action="Purchase" asp-antiforgery="false" asp-route-storeId="@Context.GetRouteValue("storeId")">
<div class="row g-2 mb-4 justify-content-center" id="ticket-form-email-container">
<div class="col-sm-12 col-md-8">
<input type="hidden" asp-for="AccessCode" value="@accessCode"/>
<div class="row g-2 justify-content-center mb-4" id="ticket-form-container">
<div class="col-sm-6 col-md-4">
<div class="form-floating">
<input required type="email" name="email" class="form-control"/>
<input type="text" minlength="3" asp-for="Name" class="form-control">
<label >Name</label>
</div>
</div>
<div class="col-sm-6 col-md-4">
<div class="form-floating">
<input required type="email" name="email" asp-for="Email" class="form-control"/>
<label >Email</label>
</div>
</div>
</div>
<div class="row g-2 justify-content-center" id="ticket-form-name-container">
<div class="col-sm-6 col-md-4">
<div class="form-floating">
<input type="text" minlength="3" name="firstName" required class="form-control">
<label >First name</label>
</div>
</div>
<div class="col-sm-6 col-md-4">
<div class="form-floating">
<input type="text" minlength="3" name="lastName" required class="form-control">
<label>Last name</label>
</div>
</div>
</div>
<div class="card-deck my-3 mx-auto" id="ticket-options-container">
<div class="row g-2 justify-content-center mb-4">
<div class="col-sm-12 col-md-8">
@for (int x = 0; x < Model.Event.TicketTypes.Count; x++)
{
var item = Model.Event.TicketTypes[x];
var availableToShow = new[] {"on_sale", "sold_out", "unavailable"}.Contains(item.Status);
var specific = false;
@{
var index = -1;
if (Model.Settings.SpecificTickets?.Any() is true)
{
var matched = Model.Settings.SpecificTickets.FirstOrDefault(ticket => ticket.TicketTypeId == item.Id);
if (matched is null || matched.Hidden)
foreach (var groupedTickets in Model.Event.TicketTypes.GroupBy(type => type.GroupId).OrderBy(groupedTickets => Model.Event.TicketGroups.FirstOrDefault(ticketGroup => ticketGroup.Id == groupedTickets.Key)?.SortOrder))
{
continue;
}
if (matched.Price is not null)
{
item.Price = matched.Price.Value;
}
if (!string.IsNullOrEmpty(matched.Name))
{
item.Name = matched.Name;
}
if (!string.IsNullOrEmpty(matched.Description))
{
item.Description = matched.Description;
}
availableToShow = true;
specific = true;
}
if (!availableToShow)
{
continue;
}
<div class="card px-0" data-id="@x">
<div class="bg-tile w-100 p-4 mb-2">
@{ CardBody(item.Name, item.Description); }
<div class="card-footer bg-transparent border-0 pb-3">
<div class="w-100 pt-2 text-center">
@if (available && (item.Status == "on_sale" || specific) && item.Quantity > 0)
@if (!string.IsNullOrEmpty(groupedTickets.Key))
{
var group = Model.Event.TicketGroups.First(ticketGroup => ticketGroup.Id == groupedTickets.Key);
<h4 class="mb-2 text-center ">@group.Name</h4>
}
@foreach (var item in groupedTickets)
{
var availableToShow = new[] {"on_sale", "sold_out", "unavailable"}.Contains(item.Status);
if (!string.IsNullOrEmpty(item.AccessCode) && item.AccessCode.Equals(accessCode, StringComparison.InvariantCultureIgnoreCase))
{
<button name="ticketTypeId" value="@item.Id" class="btn btn-primary text-nowrap" type="submit">
@if (item.Price == 0)
availableToShow = true;
}
var specific = false;
if (Model.Settings.SpecificTickets?.Any() is true)
{
var matched = Model.Settings.SpecificTickets.FirstOrDefault(ticket => ticket.TicketTypeId == item.Id);
if (matched is null || matched.Hidden)
{
continue;
}
if (matched.Price is not null)
{
item.Price = matched.Price.Value;
}
if (!string.IsNullOrEmpty(matched.Name))
{
item.Name = matched.Name;
}
if (!string.IsNullOrEmpty(matched.Description))
{
item.Description = matched.Description;
}
availableToShow = true;
specific = true;
}
if (!availableToShow)
{
continue;
}
index++;
<input type="hidden" asp-for="Items[index].TicketTypeId" value="@item.Id"/>
var purchasable = available && (specific || new[] {"on_sale", "locked"}.Contains(item.Status)) && item.Quantity > 0;
<div class="d-flex justify-content-between">
<div style="flex:2">
<h5 >@item.Name</h5>
<p>@Safe.Raw(item.Description)</p>
</div>
<div style="flex:1">
@if (purchasable)
{
<span>FREE</span>
<div class="input-group">
<div class="form-floating">
<input type="number"
class="form-control" asp-for="Items[index].Quantity" max="@item.MaxPerOrder"
min="0" data-price="@item.Price">
<label >Quantity</label>
</div>
<span class="input-group-text">
@(item.Price == 0 ? "FREE" : $"{item.Price} {Model.Event.Currency.ToUpperInvariant()}")
</span>
</div>
}
else
{
<span>Buy for @item.Price @Model.Event.Currency.ToUpperInvariant()</span>
<div >Unavailable</div>
}
</button>
}
else
{
<button class="btn btn-secondary text-nowrap" type="button" disabled="disabled">Unavailable</button>
}
</div>
</div>
<hr/>
}
</div>
</div>
</div>
}
}
}
</div>
<div class="col-sm-12 col-md-8">
<button class="btn btn-primary btn-lg m-auto" type="submit" style="display: none">Purchase</button>
</div>
</div>
</form>
<div class="row">
<div class="row text-center">
<div class="col-12" id="fiat-page-link">
<a href="@Model.Event.Url">Back to fiat ticket page</a>
</div>
@@ -145,19 +201,3 @@ footer {
</div>
</div>
</div>
@functions {
private void CardBody(string title, string description)
{
<div class="card-body my-auto pb-0">
<h5 class="card-title">@title</h5>
@if (!String.IsNullOrWhiteSpace(description))
{
<p class="card-text">@Html.Raw(description)</p>
}
</div>
}
}

View File

@@ -92,15 +92,6 @@ public class BTCPayWallet : IWallet, IDestinationProvider
_eventAggregator = eventAggregator;
Logger = loggerFactory.CreateLogger($"BTCPayWallet_{storeId}");
_eventAggregator.SubscribeAsync<NewTransactionEvent>(async evt =>
{
if (evt.DerivationStrategy != DerivationScheme)
{
return;
}
_smartifier.OnNewTransaction(evt.TransactionData.TransactionHash, evt);
});
}
public string StoreId { get; set; }