diff --git a/Plugins/BTCPayServer.Plugins.TicketTailor/TicketTailorClient.cs b/Plugins/BTCPayServer.Plugins.TicketTailor/TicketTailorClient.cs index 83c1a68..96e5e1b 100644 --- a/Plugins/BTCPayServer.Plugins.TicketTailor/TicketTailorClient.cs +++ b/Plugins/BTCPayServer.Plugins.TicketTailor/TicketTailorClient.cs @@ -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($"/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(property.Name, property.Value.GetString())).Where(pair =>pair.Value != null); + new KeyValuePair(property.Name, property.Value.GetString())) + .Where(pair => pair.Value != null); var response = await _httpClient.PostAsync($"/v1/issued_tickets", new FormUrlEncodedContent(data.ToArray())); @@ -49,8 +50,54 @@ public class TicketTailorClient : IDisposable var error = await response.Content.ReadAsStringAsync(); return (null, error); } + return (await response.Content.ReadFromJsonAsync(), null); - } + } + + public async Task<(Hold?, string? error)> CreateHold(CreateHoldRequest request) + { + var data = new Dictionary(); + 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(), null); + } + + + + public async Task GetHold(string holdId) + { + var response = await _httpClient.GetAsync($"/v1/holds/{holdId}"); + if (!response.IsSuccessStatusCode) + { + return null; + } + + + return await response.Content.ReadFromJsonAsync(); + } + + public async Task DeleteHold(string holdId) + { + var response = await _httpClient.DeleteAsync($"/v1/holds/{holdId}"); + if (!response.IsSuccessStatusCode) + { + return false; + } + return (await response.Content.ReadFromJsonAsync()).TryGetPropertyValue("deleted", out var jDeleted) && + jDeleted.GetValue() == "true"; + } public async Task 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 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; } @@ -190,13 +262,13 @@ public class TicketTailorClient : IDisposable public class IssuedTicket { [JsonPropertyName("id")] public string Id { get; set; } - + [JsonPropertyName("reference")] public string Reference { get; set; } [JsonPropertyName("description")] public string Description { get; set; } [JsonPropertyName("status")] public string Status { get; set; } [JsonPropertyName("full_name")] public string FullName { get; set; } - + [JsonPropertyName("qr_code_url")] public string QrCodeUrl { get; set; } [JsonPropertyName("barcode_url")] public string BarcodeUrl { get; set; } diff --git a/Plugins/BTCPayServer.Plugins.TicketTailor/TicketTailorController.cs b/Plugins/BTCPayServer.Plugins.TicketTailor/TicketTailorController.cs index 8045f99..a8330c8 100644 --- a/Plugins/BTCPayServer.Plugins.TicketTailor/TicketTailorController.cs +++ b/Plugins/BTCPayServer.Plugins.TicketTailor/TicketTailorController.cs @@ -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; @@ -56,12 +57,10 @@ namespace BTCPayServer.Plugins.TicketTailor return NotFound(); } - - + [AllowAnonymous] [HttpPost("")] - public async Task Purchase(string storeId, string ticketTypeId, string firstName, - string lastName, string email) + public async Task 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() == "tickettailor" && + invoice.Metadata.TryGetValue("ticketIds", out var ticketIds)) { + await SetTicketTailorTicketResult(storeId, result, ticketIds.Values()); - 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 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 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 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("port", 443); - - string rootPath = _configuration.GetValue("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()) { @@ -461,43 +390,7 @@ namespace BTCPayServer.Plugins.TicketTailor return View(vm); } } - - private static async Task 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 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(); - } - + } } diff --git a/Plugins/BTCPayServer.Plugins.TicketTailor/TicketTailorService.cs b/Plugins/BTCPayServer.Plugins.TicketTailor/TicketTailorService.cs index 30c961c..b799b7b 100644 --- a/Plugins/BTCPayServer.Plugins.TicketTailor/TicketTailorService.cs +++ b/Plugins/BTCPayServer.Plugins.TicketTailor/TicketTailorService.cs @@ -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 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 Handle(string invoiceId, string storeId, Uri host) - { - var tcs = new TaskCompletionSource(); - _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 Task { get; set; } - public Uri Host { get; set; } } - - readonly Channel _events = Channel.CreateUnbounded(); - - public Task StartAsync(CancellationToken cancellationToken) + protected override void SubscribeToEvents() { - _ = ProcessEvents(cancellationToken); - return Task.CompletedTask; + Subscribe(); + Subscribe(); + base.SubscribeToEvents(); } - public Task StopAsync(CancellationToken cancellationToken) + public async Task CreateClient(string storeId) { - return Task.CompletedTask; + return await _btcPayServerClientFactory.Create(null, new[] {storeId}); } - public async Task 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() 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 {url}" + Metadata = invoice.Metadata }, cancellationToken); } - catch (Exception e) + } + + return; + } + + if (invoice.Status != InvoiceStatus.Settled) + { + return; + } + + if (invoice.Metadata.TryGetValue("ticketIds", out var jTicketId) && + jTicketId.Values() is { } ticketIds) + { + return; + } + + if (!invoice.Metadata.TryGetValue("holdId", out var jHoldId) || + jHoldId.Value() is not { } holdId) + { + return; + } + + if (!invoice.Metadata.TryGetValue("btcpayUrl", out var jbtcpayUrl) || + jbtcpayUrl.Value() 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(); + var errors = new List(); + + 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 {url}" + }, 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"); + } } -} +} \ No newline at end of file diff --git a/Plugins/BTCPayServer.Plugins.TicketTailor/UpdateTicketTailorSettingsViewModel.cs b/Plugins/BTCPayServer.Plugins.TicketTailor/UpdateTicketTailorSettingsViewModel.cs index e4e85fc..6e20426 100644 --- a/Plugins/BTCPayServer.Plugins.TicketTailor/UpdateTicketTailorSettingsViewModel.cs +++ b/Plugins/BTCPayServer.Plugins.TicketTailor/UpdateTicketTailorSettingsViewModel.cs @@ -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; } + } } diff --git a/Plugins/BTCPayServer.Plugins.TicketTailor/Views/TicketTailor/Receipt.cshtml b/Plugins/BTCPayServer.Plugins.TicketTailor/Views/TicketTailor/Receipt.cshtml index 77ab3fb..66887ff 100644 --- a/Plugins/BTCPayServer.Plugins.TicketTailor/Views/TicketTailor/Receipt.cshtml +++ b/Plugins/BTCPayServer.Plugins.TicketTailor/Views/TicketTailor/Receipt.cshtml @@ -14,6 +14,9 @@ footer { display: none; } + .page-break { + page-break-after: always; + }
@@ -37,7 +40,7 @@ The invoice is not settled.
} - 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; - +
-
-
-

Ticket Details

-
-
- Please ensure you can see this QR barcode -
-
- Please ensure you can see this barcode -
-
-
-
@Model.Ticket.Barcode
TICKET CODE
-
- @if (!string.IsNullOrEmpty(Model.Ticket.Reference)) + +
+ @{ + for (var index = 0; index < Model.Tickets.Length; index++) { -
-
@Model.Ticket.Reference
REFERENCE
+ 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; + +
+

Ticket Details

+
+
+ Please ensure you can see this QR barcode +
+
+ Please ensure you can see this barcode +
+
+
+
@ticket.Barcode
TICKET CODE
+
+ @if (!string.IsNullOrEmpty(ticket.Reference)) + { +
+
@ticket.Reference
REFERENCE
+
+ } + + @if (!string.IsNullOrEmpty(ticket.FullName)) + { +
+
@ticket.FullName
ATTENDEE NAME
+
+ } +
+
@ticketType.Name
TICKET TYPE
+
+
- } + @if (Model.Tickets.Length > 1) + { + +
+
+
@Model.Event.Title
EVENT
+
+
+
+ @Model.Event.Url +
EVENT URL
+
+
+
@Model.Event.Start.Formatted - @Model.Event.EventEnd.Formatted
+
Date
+
+ @if (!string.IsNullOrEmpty(Model.Event.Venue.Name)) + { +
+
@Model.Event.Venue.Name
+
Venue
+
} - @if (!string.IsNullOrEmpty(Model.Ticket.FullName)) - { -
-
@Model.Ticket.FullName
ATTENDEE NAME
-
+
+ +
+ } } -
-
@specificTicketName
TICKET TYPE
-
-
+ }
-
+

Event Details

@@ -97,10 +141,13 @@
@Model.Event.Start.Formatted - @Model.Event.EventEnd.Formatted
Date
-
-
@Model.Event.Venue.Name
-
Venue
-
+ @if (!string.IsNullOrEmpty(Model.Event.Venue.Name)) + { +
+
@Model.Event.Venue.Name
+
Venue
+
} +
diff --git a/Plugins/BTCPayServer.Plugins.TicketTailor/Views/TicketTailor/UpdateTicketTailorSettings.cshtml b/Plugins/BTCPayServer.Plugins.TicketTailor/Views/TicketTailor/UpdateTicketTailorSettings.cshtml index fa0e93a..a01bfe1 100644 --- a/Plugins/BTCPayServer.Plugins.TicketTailor/Views/TicketTailor/UpdateTicketTailorSettings.cshtml +++ b/Plugins/BTCPayServer.Plugins.TicketTailor/Views/TicketTailor/UpdateTicketTailorSettings.cshtml @@ -20,8 +20,7 @@ {
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. -
- Ensure that the url used for btcpayserver is accessible publicly. +
}
diff --git a/Plugins/BTCPayServer.Plugins.TicketTailor/Views/TicketTailor/View.cshtml b/Plugins/BTCPayServer.Plugins.TicketTailor/Views/TicketTailor/View.cshtml index 5f66d28..da4e872 100644 --- a/Plugins/BTCPayServer.Plugins.TicketTailor/Views/TicketTailor/View.cshtml +++ b/Plugins/BTCPayServer.Plugins.TicketTailor/Views/TicketTailor/View.cshtml @@ -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(); + Context.Request.Query.TryGetValue("accessCode", out var accessCode); } +
-
+
-

@Model.Event.Title

-

@Model.Event.Start.Formatted - @Model.Event.EventEnd.Formatted

+

@Model.Event.Title

+

@Model.Event.Start.Formatted - @Model.Event.EventEnd.Formatted

@if (Model.Settings.ShowDescription && !string.IsNullOrEmpty(Model.Event.Description)) { -
-
@Safe.Raw(Model.Event.Description)
+
+
@Safe.Raw(Model.Event.Description)
}
-
- -
+ +
+
- + + +
+
+
+
+
-
-
-
-
- - -
-
-
-
- - -
-
-
-
+
+
- @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; - } -
+
- @{ CardBody(item.Name, item.Description); } - +
+ } +
-
-
- } + } + + } + + +
+
+ + +
- -
+ +
@@ -144,20 +200,4 @@ footer {
-
- -@functions { - - - private void CardBody(string title, string description) - { -
-
@title
- @if (!String.IsNullOrWhiteSpace(description)) - { -

@Html.Raw(description)

- } -
- } - -} +
\ No newline at end of file diff --git a/Plugins/BTCPayServer.Plugins.Wabisabi/BTCPayWallet.cs b/Plugins/BTCPayServer.Plugins.Wabisabi/BTCPayWallet.cs index 261d27d..fa2f7f8 100644 --- a/Plugins/BTCPayServer.Plugins.Wabisabi/BTCPayWallet.cs +++ b/Plugins/BTCPayServer.Plugins.Wabisabi/BTCPayWallet.cs @@ -92,15 +92,6 @@ public class BTCPayWallet : IWallet, IDestinationProvider _eventAggregator = eventAggregator; Logger = loggerFactory.CreateLogger($"BTCPayWallet_{storeId}"); - _eventAggregator.SubscribeAsync(async evt => - { - if (evt.DerivationStrategy != DerivationScheme) - { - return; - } - - _smartifier.OnNewTransaction(evt.TransactionData.TransactionHash, evt); - }); } public string StoreId { get; set; }