mirror of
https://github.com/aljazceru/BTCPayServerPlugins.git
synced 2025-12-17 07:34:24 +01:00
Pimp the ticket tailor plugin
This commit is contained in:
@@ -1,4 +1,5 @@
|
|||||||
using System;
|
#nullable enable
|
||||||
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Net.Http;
|
using System.Net.Http;
|
||||||
@@ -6,11 +7,10 @@ using System.Net.Http.Headers;
|
|||||||
using System.Net.Http.Json;
|
using System.Net.Http.Json;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
|
using System.Text.Json.Nodes;
|
||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Microsoft.Extensions.Logging;
|
|
||||||
using NBitcoin.DataEncoders;
|
using NBitcoin.DataEncoders;
|
||||||
using NBitcoin.Logging;
|
|
||||||
|
|
||||||
namespace BTCPayServer.Plugins.TicketTailor;
|
namespace BTCPayServer.Plugins.TicketTailor;
|
||||||
|
|
||||||
@@ -37,10 +37,11 @@ public class TicketTailorClient : IDisposable
|
|||||||
return await _httpClient.GetFromJsonAsync<Event>($"/v1/events/{id}");
|
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 =>
|
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()));
|
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();
|
var error = await response.Content.ReadAsStringAsync();
|
||||||
return (null, error);
|
return (null, error);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (await response.Content.ReadFromJsonAsync<IssuedTicket>(), null);
|
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)
|
public async Task<IssuedTicket> GetTicket(string id)
|
||||||
{
|
{
|
||||||
@@ -72,13 +119,38 @@ public class TicketTailorClient : IDisposable
|
|||||||
public class IssueTicketRequest
|
public class IssueTicketRequest
|
||||||
{
|
{
|
||||||
[JsonPropertyName("event_id")] public string EventId { get; set; }
|
[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("email")] public string Email { get; set; }
|
||||||
[JsonPropertyName("full_name")] public string FullName { get; set; }
|
[JsonPropertyName("full_name")] public string FullName { get; set; }
|
||||||
[JsonPropertyName("reference")] public string Reference { 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
|
public class EventEnd
|
||||||
{
|
{
|
||||||
@@ -134,7 +206,7 @@ public class TicketTailorClient : IDisposable
|
|||||||
{
|
{
|
||||||
[JsonPropertyName("id")] public string Id { get; set; }
|
[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; }
|
[JsonPropertyName("name")] public string Name { get; set; }
|
||||||
|
|
||||||
@@ -149,7 +221,7 @@ public class TicketTailorClient : IDisposable
|
|||||||
|
|
||||||
[JsonPropertyName("id")] public string Id { get; set; }
|
[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; }
|
[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("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; }
|
[JsonPropertyName("name")] public string Name { get; set; }
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ using System.Threading.Tasks;
|
|||||||
using AngleSharp;
|
using AngleSharp;
|
||||||
using BTCPayServer.Abstractions.Contracts;
|
using BTCPayServer.Abstractions.Contracts;
|
||||||
using BTCPayServer.Abstractions.Extensions;
|
using BTCPayServer.Abstractions.Extensions;
|
||||||
|
using BTCPayServer.Abstractions.Models;
|
||||||
using BTCPayServer.Client;
|
using BTCPayServer.Client;
|
||||||
using BTCPayServer.Client.Models;
|
using BTCPayServer.Client.Models;
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
@@ -57,11 +58,9 @@ namespace BTCPayServer.Plugins.TicketTailor
|
|||||||
return NotFound();
|
return NotFound();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
[AllowAnonymous]
|
[AllowAnonymous]
|
||||||
[HttpPost("")]
|
[HttpPost("")]
|
||||||
public async Task<IActionResult> Purchase(string storeId, string ticketTypeId, string firstName,
|
public async Task<IActionResult> Purchase(string storeId, TicketTailorViewModel request)
|
||||||
string lastName, string email)
|
|
||||||
{
|
{
|
||||||
var config = await _ticketTailorService.GetTicketTailorForStore(storeId);
|
var config = await _ticketTailorService.GetTicketTailorForStore(storeId);
|
||||||
try
|
try
|
||||||
@@ -75,35 +74,87 @@ namespace BTCPayServer.Plugins.TicketTailor
|
|||||||
return NotFound();
|
return NotFound();
|
||||||
}
|
}
|
||||||
|
|
||||||
var ticketType = evt.TicketTypes.FirstOrDefault(type => type.Id == ticketTypeId);
|
var price = 0m;
|
||||||
|
foreach (var purchaseRequestItem in request.Items)
|
||||||
|
{
|
||||||
|
if (purchaseRequestItem.Quantity <= 0)
|
||||||
|
{
|
||||||
|
continue;;
|
||||||
|
}
|
||||||
|
var ticketType = evt.TicketTypes.FirstOrDefault(type => type.Id == purchaseRequestItem.TicketTypeId);
|
||||||
|
|
||||||
var specificTicket =
|
var specificTicket =
|
||||||
config.SpecificTickets?.SingleOrDefault(ticket => ticketType?.Id == ticket.TicketTypeId);
|
config.SpecificTickets?.SingleOrDefault(ticket => ticketType?.Id == ticket.TicketTypeId);
|
||||||
if (ticketType is not null && specificTicket is not null)
|
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)
|
||||||
{
|
{
|
||||||
ticketType.Price = specificTicket.Price.GetValueOrDefault(ticketType.Price);
|
TempData.SetStatusMessageModel(new StatusMessageModel
|
||||||
|
{
|
||||||
|
Severity = StatusMessageModel.StatusSeverity.Error,
|
||||||
|
Html = "The ticket was not found."
|
||||||
|
});
|
||||||
|
return RedirectToAction("View", new {storeId});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ticketType is null || (specificTicket is null && ticketType.Status != "on_sale") ||
|
if (purchaseRequestItem.Quantity > ticketType.MaxPerOrder ||
|
||||||
ticketType.Quantity <= 0)
|
purchaseRequestItem.Quantity < ticketType.MinPerOrder )
|
||||||
{
|
{
|
||||||
return NotFound();
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
var hold = await client.CreateHold(new TicketTailorClient.CreateHoldRequest()
|
||||||
|
{
|
||||||
|
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 btcpayClient = await CreateClient(storeId);
|
||||||
var redirectUrl = Request.GetAbsoluteUri(Url.Action("Receipt",
|
var redirectUrl = Request.GetAbsoluteUri(Url.Action("Receipt",
|
||||||
"TicketTailor", new {storeId, invoiceId = "kukkskukkskukks"}));
|
"TicketTailor", new {storeId, invoiceId = "kukkskukkskukks"}));
|
||||||
redirectUrl = redirectUrl.Replace("kukkskukkskukks", "{InvoiceId}");
|
redirectUrl = redirectUrl.Replace("kukkskukkskukks", "{InvoiceId}");
|
||||||
|
if(string.IsNullOrEmpty(request.Name))
|
||||||
|
{
|
||||||
|
request.Name = "Anonymous lizard";
|
||||||
|
}
|
||||||
var inv = await btcpayClient.CreateInvoice(storeId,
|
var inv = await btcpayClient.CreateInvoice(storeId,
|
||||||
new CreateInvoiceRequest()
|
new CreateInvoiceRequest()
|
||||||
{
|
{
|
||||||
Amount = ticketType.Price,
|
Amount = price,
|
||||||
Currency = evt.Currency,
|
Currency = evt.Currency,
|
||||||
Type = InvoiceType.Standard,
|
Type = InvoiceType.Standard,
|
||||||
AdditionalSearchTerms = new[] {"tickettailor", ticketTypeId, evt.Id},
|
AdditionalSearchTerms = new[] {"tickettailor", hold.Item1.Id, evt.Id},
|
||||||
Checkout =
|
Checkout =
|
||||||
{
|
{
|
||||||
RequiresRefundEmail = true,
|
RequiresRefundEmail = true,
|
||||||
RedirectAutomatically = ticketType.Price > 0,
|
RedirectAutomatically = price > 0,
|
||||||
RedirectURL = redirectUrl,
|
RedirectURL = redirectUrl,
|
||||||
},
|
},
|
||||||
Receipt = new InvoiceDataBase.ReceiptOptions()
|
Receipt = new InvoiceDataBase.ReceiptOptions()
|
||||||
@@ -112,7 +163,11 @@ namespace BTCPayServer.Plugins.TicketTailor
|
|||||||
},
|
},
|
||||||
Metadata = JObject.FromObject(new
|
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)
|
if (inv.Status == InvoiceStatus.Settled)
|
||||||
return RedirectToAction("Receipt", new {storeId, invoiceId = inv.Id});
|
return RedirectToAction("Receipt", new {storeId, invoiceId = inv.Id});
|
||||||
return Redirect(inv.CheckoutLink);
|
|
||||||
|
return RedirectToAction("Checkout","UIInvoice", new {invoiceId = inv.Id});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
@@ -145,21 +201,12 @@ namespace BTCPayServer.Plugins.TicketTailor
|
|||||||
var result = new TicketReceiptPage() {InvoiceId = invoiceId};
|
var result = new TicketReceiptPage() {InvoiceId = invoiceId};
|
||||||
var invoice = await btcpayClient.GetInvoice(storeId, invoiceId);
|
var invoice = await btcpayClient.GetInvoice(storeId, invoiceId);
|
||||||
result.Status = invoice.Status;
|
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);
|
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 settings = await _ticketTailorService.GetTicketTailorForStore(storeId);
|
||||||
var client = new TicketTailorClient(_httpClientFactory, settings.ApiKey);
|
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);
|
var evt = await client.GetEvent(settings.EventId);
|
||||||
result.Event = evt;
|
result.Event = evt;
|
||||||
result.TicketType =
|
result.Tickets = tickets;
|
||||||
evt.TicketTypes.FirstOrDefault(type => type.Id == result.Ticket.TicketTypeId);
|
|
||||||
result.Settings = settings;
|
result.Settings = settings;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -200,9 +246,8 @@ namespace BTCPayServer.Plugins.TicketTailor
|
|||||||
{
|
{
|
||||||
public string InvoiceId { get; set; }
|
public string InvoiceId { get; set; }
|
||||||
public InvoiceStatus Status { 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.Event Event { get; set; }
|
||||||
public TicketTailorClient.TicketType TicketType { get; set; }
|
|
||||||
public TicketTailorSettings Settings { get; set; }
|
public TicketTailorSettings Settings { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -210,28 +255,22 @@ namespace BTCPayServer.Plugins.TicketTailor
|
|||||||
private readonly IHttpClientFactory _httpClientFactory;
|
private readonly IHttpClientFactory _httpClientFactory;
|
||||||
private readonly TicketTailorService _ticketTailorService;
|
private readonly TicketTailorService _ticketTailorService;
|
||||||
private readonly IBTCPayServerClientFactory _btcPayServerClientFactory;
|
private readonly IBTCPayServerClientFactory _btcPayServerClientFactory;
|
||||||
private readonly IConfiguration _configuration;
|
|
||||||
private readonly LinkGenerator _linkGenerator;
|
|
||||||
|
|
||||||
public TicketTailorController(IHttpClientFactory httpClientFactory,
|
public TicketTailorController(IHttpClientFactory httpClientFactory,
|
||||||
TicketTailorService ticketTailorService,
|
TicketTailorService ticketTailorService,
|
||||||
IBTCPayServerClientFactory btcPayServerClientFactory,
|
IBTCPayServerClientFactory btcPayServerClientFactory)
|
||||||
IConfiguration configuration,
|
|
||||||
LinkGenerator linkGenerator )
|
|
||||||
{
|
{
|
||||||
|
|
||||||
_httpClientFactory = httpClientFactory;
|
_httpClientFactory = httpClientFactory;
|
||||||
_ticketTailorService = ticketTailorService;
|
_ticketTailorService = ticketTailorService;
|
||||||
_btcPayServerClientFactory = btcPayServerClientFactory;
|
_btcPayServerClientFactory = btcPayServerClientFactory;
|
||||||
_configuration = configuration;
|
|
||||||
_linkGenerator = linkGenerator;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet("update")]
|
[HttpGet("update")]
|
||||||
public async Task<IActionResult> UpdateTicketTailorSettings(string storeId)
|
public async Task<IActionResult> UpdateTicketTailorSettings(string storeId)
|
||||||
{
|
{
|
||||||
UpdateTicketTailorSettingsViewModel vm = new();
|
UpdateTicketTailorSettingsViewModel vm = new();
|
||||||
TicketTailorSettings TicketTailor = null;
|
TicketTailorSettings TicketTailor;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
TicketTailor = await _ticketTailorService.GetTicketTailorForStore(storeId);
|
TicketTailor = await _ticketTailorService.GetTicketTailorForStore(storeId);
|
||||||
@@ -304,8 +343,7 @@ namespace BTCPayServer.Plugins.TicketTailor
|
|||||||
[HttpPost("update")]
|
[HttpPost("update")]
|
||||||
public async Task<IActionResult> UpdateTicketTailorSettings(string storeId,
|
public async Task<IActionResult> UpdateTicketTailorSettings(string storeId,
|
||||||
UpdateTicketTailorSettingsViewModel vm,
|
UpdateTicketTailorSettingsViewModel vm,
|
||||||
string command,
|
string command)
|
||||||
[FromServices] BTCPayServerClient btcPayServerClient)
|
|
||||||
{
|
{
|
||||||
vm = await SetValues(vm);
|
vm = await SetValues(vm);
|
||||||
|
|
||||||
@@ -340,115 +378,6 @@ namespace BTCPayServer.Plugins.TicketTailor
|
|||||||
BypassAvailabilityCheck = vm.BypassAvailabilityCheck
|
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())
|
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();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,21 +1,25 @@
|
|||||||
using System;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
using System.Net.Http;
|
using System.Net.Http;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Channels;
|
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using BTCPayServer.Abstractions.Contracts;
|
using BTCPayServer.Abstractions.Contracts;
|
||||||
using BTCPayServer.Client;
|
using BTCPayServer.Client;
|
||||||
using BTCPayServer.Client.Models;
|
using BTCPayServer.Client.Models;
|
||||||
|
using BTCPayServer.Events;
|
||||||
|
using BTCPayServer.HostedServices;
|
||||||
|
using BTCPayServer.Services.Invoices;
|
||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
using Microsoft.AspNetCore.Routing;
|
using Microsoft.AspNetCore.Routing;
|
||||||
using Microsoft.Extensions.Caching.Memory;
|
using Microsoft.Extensions.Caching.Memory;
|
||||||
using Microsoft.Extensions.Hosting;
|
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using Newtonsoft.Json.Linq;
|
using Newtonsoft.Json.Linq;
|
||||||
|
using static System.String;
|
||||||
|
|
||||||
namespace BTCPayServer.Plugins.TicketTailor;
|
namespace BTCPayServer.Plugins.TicketTailor;
|
||||||
|
|
||||||
public class TicketTailorService : IHostedService
|
public class TicketTailorService : EventHostedServiceBase
|
||||||
{
|
{
|
||||||
private readonly ISettingsRepository _settingsRepository;
|
private readonly ISettingsRepository _settingsRepository;
|
||||||
private readonly IMemoryCache _memoryCache;
|
private readonly IMemoryCache _memoryCache;
|
||||||
@@ -28,7 +32,8 @@ public class TicketTailorService : IHostedService
|
|||||||
public TicketTailorService(ISettingsRepository settingsRepository, IMemoryCache memoryCache,
|
public TicketTailorService(ISettingsRepository settingsRepository, IMemoryCache memoryCache,
|
||||||
IHttpClientFactory httpClientFactory,
|
IHttpClientFactory httpClientFactory,
|
||||||
IStoreRepository storeRepository, ILogger<TicketTailorService> logger,
|
IStoreRepository storeRepository, ILogger<TicketTailorService> logger,
|
||||||
IBTCPayServerClientFactory btcPayServerClientFactory, LinkGenerator linkGenerator)
|
IBTCPayServerClientFactory btcPayServerClientFactory, LinkGenerator linkGenerator,
|
||||||
|
EventAggregator eventAggregator) : base(eventAggregator, logger)
|
||||||
{
|
{
|
||||||
_settingsRepository = settingsRepository;
|
_settingsRepository = settingsRepository;
|
||||||
_memoryCache = memoryCache;
|
_memoryCache = memoryCache;
|
||||||
@@ -68,54 +73,38 @@ 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
|
internal class IssueTicket
|
||||||
{
|
{
|
||||||
public string InvoiceId { get; set; }
|
public string InvoiceId { get; set; }
|
||||||
public string StoreId { get; set; }
|
public string StoreId { get; set; }
|
||||||
public TaskCompletionSource<InvoiceData?> Task { get; set; }
|
|
||||||
public Uri Host { get; set; }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected override void SubscribeToEvents()
|
||||||
readonly Channel<IssueTicket> _events = Channel.CreateUnbounded<IssueTicket>();
|
|
||||||
|
|
||||||
public Task StartAsync(CancellationToken cancellationToken)
|
|
||||||
{
|
{
|
||||||
_ = ProcessEvents(cancellationToken);
|
Subscribe<InvoiceEvent>();
|
||||||
return Task.CompletedTask;
|
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,
|
return;
|
||||||
Host = new HostString(host.Host),
|
|
||||||
Path = new PathString(host.AbsolutePath),
|
|
||||||
PathBase = new PathString(),
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task ProcessEvents(CancellationToken cancellationToken)
|
evt = new IssueTicket() {InvoiceId = invoiceEvent.InvoiceId, StoreId = invoiceEvent.Invoice.StoreId};
|
||||||
{
|
}
|
||||||
while (await _events.Reader.WaitToReadAsync(cancellationToken))
|
|
||||||
{
|
if (evt is not IssueTicket issueTicket)
|
||||||
if (!_events.Reader.TryRead(out var evt)) continue;
|
return;
|
||||||
|
|
||||||
async Task HandleIssueTicketError(JToken posData, string e, InvoiceData invoiceData,
|
async Task HandleIssueTicketError(JToken posData, string e, InvoiceData invoiceData,
|
||||||
BTCPayServerClient btcPayClient)
|
BTCPayServerClient btcPayClient)
|
||||||
@@ -123,44 +112,75 @@ public class TicketTailorService : IHostedService
|
|||||||
posData["Error"] =
|
posData["Error"] =
|
||||||
$"Ticket could not be created. You should refund customer.{Environment.NewLine}{e}";
|
$"Ticket could not be created. You should refund customer.{Environment.NewLine}{e}";
|
||||||
invoiceData.Metadata["posData"] = posData;
|
invoiceData.Metadata["posData"] = posData;
|
||||||
await btcPayClient.UpdateInvoice(evt.StoreId, invoiceData.Id,
|
await btcPayClient.UpdateInvoice(issueTicket.StoreId, invoiceData.Id,
|
||||||
new UpdateInvoiceRequest() {Metadata = invoiceData.Metadata}, cancellationToken);
|
new UpdateInvoiceRequest() {Metadata = invoiceData.Metadata}, cancellationToken);
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
await btcPayClient.MarkInvoiceStatus(evt.StoreId, invoiceData.Id,
|
await btcPayClient.MarkInvoiceStatus(issueTicket.StoreId, invoiceData.Id,
|
||||||
new MarkInvoiceStatusRequest() {Status = InvoiceStatus.Invalid}, cancellationToken);
|
new MarkInvoiceStatusRequest() {Status = InvoiceStatus.Invalid}, cancellationToken);
|
||||||
}
|
}
|
||||||
catch (Exception exception)
|
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");
|
_logger.LogError(exception,
|
||||||
|
$"Failed to update invoice {invoiceData.Id} status from {invoiceData.Status} to Invalid after failing to issue ticket from ticket tailor");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
InvoiceData invoice = null;
|
InvoiceData invoice = null;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var settings = await GetTicketTailorForStore(evt.StoreId);
|
var settings = await GetTicketTailorForStore(issueTicket.StoreId);
|
||||||
if (settings is null || settings.ApiKey is null)
|
if (settings is null || settings.ApiKey is null)
|
||||||
{
|
{
|
||||||
evt.Task.SetResult(null);
|
return;
|
||||||
continue;
|
}
|
||||||
|
|
||||||
|
var btcPayClient = await CreateClient(issueTicket.StoreId);
|
||||||
|
invoice = await btcPayClient.GetInvoice(issueTicket.StoreId, issueTicket.InvoiceId, cancellationToken);
|
||||||
|
|
||||||
|
if (new[] {InvoiceStatus.Invalid, InvoiceStatus.Expired}.Contains(invoice.Status))
|
||||||
|
{
|
||||||
|
if (invoice.Metadata.TryGetValue("holdId", out var jHoldIdx) &&
|
||||||
|
jHoldIdx.Value<string>() is { } holdIdx)
|
||||||
|
{
|
||||||
|
if (await new TicketTailorClient(_httpClientFactory, settings.ApiKey).DeleteHold(holdIdx))
|
||||||
|
{
|
||||||
|
invoice.Metadata.Remove("holdId");
|
||||||
|
invoice.Metadata.Add("holdId_deleted", holdIdx);
|
||||||
|
await btcPayClient.UpdateInvoice(issueTicket.StoreId, issueTicket.InvoiceId,
|
||||||
|
new UpdateInvoiceRequest()
|
||||||
|
{
|
||||||
|
Metadata = invoice.Metadata
|
||||||
|
}, cancellationToken);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var btcPayClient = await CreateClient(evt.StoreId, evt.Host);
|
|
||||||
invoice = await btcPayClient.GetInvoice(evt.StoreId, evt.InvoiceId, cancellationToken);
|
|
||||||
if (invoice.Status != InvoiceStatus.Settled)
|
if (invoice.Status != InvoiceStatus.Settled)
|
||||||
{
|
{
|
||||||
evt.Task.SetResult(null);
|
return;
|
||||||
continue;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (invoice.Metadata.ContainsKey("ticketId"))
|
if (invoice.Metadata.TryGetValue("ticketIds", out var jTicketId) &&
|
||||||
|
jTicketId.Values<string>() is { } ticketIds)
|
||||||
{
|
{
|
||||||
evt.Task.SetResult(null);
|
return;
|
||||||
continue;
|
}
|
||||||
|
|
||||||
|
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 ticketTypeId = invoice.Metadata["ticketTypeId"].ToString();
|
|
||||||
var email = invoice.Metadata["buyerEmail"].ToString();
|
var email = invoice.Metadata["buyerEmail"].ToString();
|
||||||
var name = invoice.Metadata["buyerName"]?.ToString();
|
var name = invoice.Metadata["buyerName"]?.ToString();
|
||||||
invoice.Metadata.TryGetValue("posData", out var posData);
|
invoice.Metadata.TryGetValue("posData", out var posData);
|
||||||
@@ -168,43 +188,74 @@ public class TicketTailorService : IHostedService
|
|||||||
var client = new TicketTailorClient(_httpClientFactory, settings.ApiKey);
|
var client = new TicketTailorClient(_httpClientFactory, settings.ApiKey);
|
||||||
try
|
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))
|
||||||
|
{
|
||||||
|
|
||||||
var ticketResult = await client.CreateTicket(new TicketTailorClient.IssueTicketRequest()
|
var ticketResult = await client.CreateTicket(new TicketTailorClient.IssueTicketRequest()
|
||||||
{
|
{
|
||||||
Reference = invoice.Id,
|
Reference = invoice.Id,
|
||||||
Email = email,
|
Email = email,
|
||||||
EventId = settings.EventId,
|
EventId = settings.EventId,
|
||||||
TicketTypeId = ticketTypeId,
|
HoldId = holdId,
|
||||||
FullName = name,
|
FullName = name,
|
||||||
|
TicketTypeId = tt.TicketTypeId
|
||||||
});
|
});
|
||||||
|
if (ticketResult.error is null)
|
||||||
if (ticketResult.Item2 is not null)
|
|
||||||
{
|
{
|
||||||
await HandleIssueTicketError(posData, ticketResult.Item2, invoice, btcPayClient);
|
tickets.Add(ticketResult.Item1);
|
||||||
|
|
||||||
continue;
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
|
||||||
|
errors.Add(ticketResult.error);
|
||||||
|
}
|
||||||
|
hold = await client.GetHold(holdId);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var ticket = ticketResult.Item1;
|
|
||||||
invoice.Metadata["ticketId"] = ticket.Id;
|
|
||||||
invoice.Metadata["orderId"] = $"tickettailor_{ticket.Id}";
|
|
||||||
|
|
||||||
posData["Ticket Code"] = ticket.Barcode;
|
if (tickets.Count != holdOriginalAmount)
|
||||||
posData["Ticket Id"] = ticket.Id;
|
{
|
||||||
|
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;
|
invoice.Metadata["posData"] = posData;
|
||||||
await btcPayClient.UpdateInvoice(evt.StoreId, invoice.Id,
|
await btcPayClient.UpdateInvoice(issueTicket.StoreId, invoice.Id,
|
||||||
new UpdateInvoiceRequest() {Metadata = invoice.Metadata}, cancellationToken);
|
new UpdateInvoiceRequest() {Metadata = invoice.Metadata}, cancellationToken);
|
||||||
|
|
||||||
|
var uri = new Uri(btcpayUrl);
|
||||||
var url =
|
var url =
|
||||||
_linkGenerator.GetUriByAction("Receipt",
|
_linkGenerator.GetUriByAction("Receipt",
|
||||||
"TicketTailor",
|
"TicketTailor",
|
||||||
new {evt.StoreId, invoiceId = invoice.Id},
|
new {issueTicket.StoreId, invoiceId = invoice.Id},
|
||||||
evt.Host.Scheme,
|
uri.Scheme,
|
||||||
new HostString(evt.Host.Host),
|
new HostString(uri.Host),
|
||||||
evt.Host.AbsolutePath);
|
uri.AbsolutePath);
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
await btcPayClient.SendEmail(evt.StoreId,
|
await btcPayClient.SendEmail(issueTicket.StoreId,
|
||||||
new SendEmailRequest()
|
new SendEmailRequest()
|
||||||
{
|
{
|
||||||
Subject = "Your ticket is available now.",
|
Subject = "Your ticket is available now.",
|
||||||
@@ -227,10 +278,5 @@ public class TicketTailorService : IHostedService
|
|||||||
{
|
{
|
||||||
_logger.LogError(ex, "Failed to issue ticket");
|
_logger.LogError(ex, "Failed to issue ticket");
|
||||||
}
|
}
|
||||||
finally
|
|
||||||
{
|
|
||||||
evt.Task.SetResult(invoice);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -30,4 +30,16 @@ public class TicketTailorViewModel
|
|||||||
{
|
{
|
||||||
public TicketTailorClient.Event Event { get; set; }
|
public TicketTailorClient.Event Event { get; set; }
|
||||||
public TicketTailorSettings Settings { 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; }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,6 +14,9 @@
|
|||||||
footer {
|
footer {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
.page-break {
|
||||||
|
page-break-after: always;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
<div class="container d-flex h-100">
|
<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">
|
<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.
|
The invoice is not settled.
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
else if (Model.Ticket is null)
|
else if (Model.Tickets?.Any() is not true)
|
||||||
{
|
{
|
||||||
|
|
||||||
reloadPage = true;
|
reloadPage = true;
|
||||||
@@ -47,42 +50,83 @@
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
var specificTicketName = Model.Settings?.SpecificTickets?.FirstOrDefault(ticket => ticket.TicketTypeId == Model.TicketType.Id)?.Name ?? Model.TicketType.Name;
|
|
||||||
|
|
||||||
<div class="row mb-4">
|
<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">
|
<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++)
|
||||||
|
{
|
||||||
|
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>
|
<h2 class="h4 mb-3">Ticket Details</h2>
|
||||||
<div class="d-flex mb-4">
|
<div class="d-flex mb-4">
|
||||||
<div class="col-6 px-1">
|
<div class="col-6 px-1">
|
||||||
<img src="@Model.Ticket.QrCodeUrl" alt="Please ensure you can see this QR barcode" class="w-100">
|
<img src="@ticket.QrCodeUrl" alt="Please ensure you can see this QR barcode" class="w-100">
|
||||||
</div>
|
</div>
|
||||||
<div class="col-6 px-1 py-4">
|
<div class="col-6 px-1 py-4">
|
||||||
<img src="@Model.Ticket.BarcodeUrl" alt="Please ensure you can see this barcode" class="w-100">
|
<img src="@ticket.BarcodeUrl" alt="Please ensure you can see this barcode" class="w-100">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="d-flex d-print-inline-block flex-column mb-4 ">
|
<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>
|
<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>
|
</div>
|
||||||
@if (!string.IsNullOrEmpty(Model.Ticket.Reference))
|
@if (!string.IsNullOrEmpty(ticket.Reference))
|
||||||
{
|
{
|
||||||
<div class="d-flex d-print-inline-block flex-column mb-4 px-4 ">
|
<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>
|
<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>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
@if (!string.IsNullOrEmpty(Model.Ticket.FullName))
|
@if (!string.IsNullOrEmpty(ticket.FullName))
|
||||||
{
|
{
|
||||||
<div class="d-flex d-print-inline-block flex-column ">
|
<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>
|
<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>
|
||||||
}
|
}
|
||||||
<div class="d-flex d-print-inline-block flex-column ">
|
<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>
|
<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>
|
||||||
|
|
||||||
</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>
|
||||||
<div class="col col-12 col-lg-6 mb-md-0 mb-sm-4">
|
<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>
|
||||||
|
<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">
|
<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>
|
<h2 class="h4 mb-3 d-print-none">Event Details</h2>
|
||||||
<div class="d-flex d-print-inline-block flex-column mb-4 ">
|
<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>
|
<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>
|
<dd class="order-1 order-sm-2 ">Date</dd>
|
||||||
</div>
|
</div>
|
||||||
|
@if (!string.IsNullOrEmpty(Model.Event.Venue.Name))
|
||||||
|
{
|
||||||
<div class="d-flex d-print-inline-block flex-column ">
|
<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>
|
<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>
|
<dd class="order-1 order-sm-2 ">Venue</dd>
|
||||||
</div>
|
</div>}
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -20,8 +20,7 @@
|
|||||||
{
|
{
|
||||||
<div class="alert alert-warning">
|
<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.
|
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>
|
||||||
}
|
}
|
||||||
<div class="row">
|
<div class="row">
|
||||||
|
|||||||
@@ -1,28 +1,24 @@
|
|||||||
@using Microsoft.AspNetCore.Routing
|
@using Microsoft.AspNetCore.Routing
|
||||||
@using BTCPayServer.Plugins.TicketTailor
|
@using BTCPayServer.Plugins.TicketTailor
|
||||||
|
@using Microsoft.AspNetCore.Mvc.TagHelpers
|
||||||
|
@using NBitcoin
|
||||||
@model BTCPayServer.Plugins.TicketTailor.TicketTailorViewModel
|
@model BTCPayServer.Plugins.TicketTailor.TicketTailorViewModel
|
||||||
|
|
||||||
@inject BTCPayServer.Security.ContentSecurityPolicies csp;
|
@inject BTCPayServer.Security.ContentSecurityPolicies csp;
|
||||||
@{
|
@{
|
||||||
|
|
||||||
|
var nonce = RandomUtils.GetUInt256().ToString().Substring(0, 32);
|
||||||
|
csp.Add("script-src", $"'nonce-{nonce}'");
|
||||||
|
csp.AllowUnsafeHashes();
|
||||||
Layout = "_LayoutSimple";
|
Layout = "_LayoutSimple";
|
||||||
var available = Model.Settings.BypassAvailabilityCheck || (Model.Event.Unavailable != "true" && Model.Event.TicketsAvailable == "true");
|
var available = Model.Settings.BypassAvailabilityCheck || (Model.Event.Unavailable != "true" && Model.Event.TicketsAvailable == "true");
|
||||||
Model.Settings.SpecificTickets ??= new List<SpecificTicket>();
|
Model.Settings.SpecificTickets ??= new List<SpecificTicket>();
|
||||||
|
Context.Request.Query.TryGetValue("accessCode", out var accessCode);
|
||||||
}
|
}
|
||||||
<style>
|
<style>
|
||||||
|
hr:last-child{
|
||||||
|
display: none;
|
||||||
.card-deck {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
|
||||||
grid-gap: 1.5rem;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.card-deck .card:only-of-type {
|
|
||||||
max-width: 320px;
|
|
||||||
margin: auto !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
footer {
|
footer {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
@@ -33,49 +29,86 @@ footer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
</style>
|
</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="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"/>
|
<partial name="_StatusMessage"/>
|
||||||
|
|
||||||
<h1 >@Model.Event.Title</h1>
|
<h1 class="text-center ">@Model.Event.Title</h1>
|
||||||
<h2 class="text-muted mb-4">@Model.Event.Start.Formatted - @Model.Event.EventEnd.Formatted</h2>
|
<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))
|
@if (Model.Settings.ShowDescription && !string.IsNullOrEmpty(Model.Event.Description))
|
||||||
{
|
{
|
||||||
<div class="row" id="ticket-tailor-description">
|
<div class="row" id="ticket-tailor-description text-center ">
|
||||||
<div class="overflow-hidden col-12">@Safe.Raw(Model.Event.Description)</div>
|
<div class="overflow-hidden col-12 ">@Safe.Raw(Model.Event.Description)</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
<form method="post" asp-controller="TicketTailor" asp-action="Purchase" asp-antiforgery="false" asp-route-storeId="@Context.GetRouteValue("storeId")">
|
<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">
|
<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-12 col-md-8">
|
<div class="col-sm-6 col-md-4">
|
||||||
<div class="form-floating">
|
<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>
|
<label >Email</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row g-2 justify-content-center mb-4">
|
||||||
|
<div class="col-sm-12 col-md-8">
|
||||||
|
|
||||||
</div>
|
@{
|
||||||
<div class="row g-2 justify-content-center" id="ticket-form-name-container">
|
var index = -1;
|
||||||
<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">
|
|
||||||
|
|
||||||
@for (int x = 0; x < Model.Event.TicketTypes.Count; x++)
|
foreach (var groupedTickets in Model.Event.TicketTypes.GroupBy(type => type.GroupId).OrderBy(groupedTickets => Model.Event.TicketGroups.FirstOrDefault(ticketGroup => ticketGroup.Id == groupedTickets.Key)?.SortOrder))
|
||||||
|
{
|
||||||
|
<div class="bg-tile w-100 p-4 mb-2">
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@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 item = Model.Event.TicketTypes[x];
|
|
||||||
var availableToShow = new[] {"on_sale", "sold_out", "unavailable"}.Contains(item.Status);
|
var availableToShow = new[] {"on_sale", "sold_out", "unavailable"}.Contains(item.Status);
|
||||||
|
if (!string.IsNullOrEmpty(item.AccessCode) && item.AccessCode.Equals(accessCode, StringComparison.InvariantCultureIgnoreCase))
|
||||||
|
{
|
||||||
|
availableToShow = true;
|
||||||
|
}
|
||||||
var specific = false;
|
var specific = false;
|
||||||
|
|
||||||
if (Model.Settings.SpecificTickets?.Any() is true)
|
if (Model.Settings.SpecificTickets?.Any() is true)
|
||||||
@@ -104,38 +137,61 @@ footer {
|
|||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
<div class="card px-0" data-id="@x">
|
index++;
|
||||||
|
|
||||||
@{ CardBody(item.Name, item.Description); }
|
<input type="hidden" asp-for="Items[index].TicketTypeId" value="@item.Id"/>
|
||||||
<div class="card-footer bg-transparent border-0 pb-3">
|
var purchasable = available && (specific || new[] {"on_sale", "locked"}.Contains(item.Status)) && item.Quantity > 0;
|
||||||
|
|
||||||
<div class="w-100 pt-2 text-center">
|
<div class="d-flex justify-content-between">
|
||||||
@if (available && (item.Status == "on_sale" || specific) && item.Quantity > 0)
|
<div style="flex:2">
|
||||||
|
<h5 >@item.Name</h5>
|
||||||
|
<p>@Safe.Raw(item.Description)</p>
|
||||||
|
</div>
|
||||||
|
<div style="flex:1">
|
||||||
|
@if (purchasable)
|
||||||
{
|
{
|
||||||
<button name="ticketTypeId" value="@item.Id" class="btn btn-primary text-nowrap" type="submit">
|
<div class="input-group">
|
||||||
@if (item.Price == 0)
|
<div class="form-floating">
|
||||||
{
|
<input type="number"
|
||||||
<span>FREE</span>
|
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
|
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>
|
||||||
</div>
|
</div>
|
||||||
|
<hr/>
|
||||||
|
}
|
||||||
|
|
||||||
</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>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
|
|
||||||
<div class="row">
|
<div class="row text-center">
|
||||||
<div class="col-12" id="fiat-page-link">
|
<div class="col-12" id="fiat-page-link">
|
||||||
<a href="@Model.Event.Url">Back to fiat ticket page</a>
|
<a href="@Model.Event.Url">Back to fiat ticket page</a>
|
||||||
</div>
|
</div>
|
||||||
@@ -145,19 +201,3 @@ footer {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</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>
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -92,15 +92,6 @@ public class BTCPayWallet : IWallet, IDestinationProvider
|
|||||||
_eventAggregator = eventAggregator;
|
_eventAggregator = eventAggregator;
|
||||||
Logger = loggerFactory.CreateLogger($"BTCPayWallet_{storeId}");
|
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; }
|
public string StoreId { get; set; }
|
||||||
|
|||||||
Reference in New Issue
Block a user