Extras (also called add-ons) allow you to offer optional enhancements to your tour. Customers can add these during checkout to customize their experience while you increase average order value.
Tours with extras see an average 25-40% increase in revenue per booking. Common extras include meals, equipment rentals, and premium experiences.
public record TourExtraRequest( string Name, // Extra name string Description, // What's included int ChargeMode, // 0=Free, 1=Flat, 2=Per Person decimal? BasePrice, // Base price (for flat rate) string CurrencyCode, // "USD", "DOP", "EUR" IEnumerable<TourExtraTicketPriceRequest> TicketPrices, // Per-person pricing int SortOrder); // Display orderpublic record TourExtraTicketPriceRequest( Guid TicketTypeId, // Frontend identifier string TicketName, // "Adult", "Child" decimal? Price); // Price for this ticket type
var lunchUpgrade = new TourExtraRequest( Name: "Premium Lunch", Description: "3-course meal at a local restaurant with wine pairing", ChargeMode: 2, // Per person BasePrice: null, // Not used for per-person CurrencyCode: "USD", TicketPrices: new[] { new TourExtraTicketPriceRequest( TicketTypeId: Guid.NewGuid(), // Links to adult ticket TicketName: "Adult", Price: 35.00m ), new TourExtraTicketPriceRequest( TicketTypeId: Guid.NewGuid(), // Links to child ticket TicketName: "Child", Price: 18.00m ) }, SortOrder: 2);
The UpsertExtrasAsync method handles extra creation:
public async Task UpsertExtrasAsync( int tourId, IEnumerable<TourExtraRequest> extras, CancellationToken cancellationToken = default){ if (extras is null) return; using var connection = CreateConnection(); await connection.OpenAsync(cancellationToken); foreach (var extra in extras) { using var command = new SqlCommand("sp_TourExtra_Insert", connection) { CommandType = CommandType.StoredProcedure }; command.Parameters.AddWithValue("@TourId", tourId); command.Parameters.AddWithValue("@Name", extra.Name ?? string.Empty); command.Parameters.AddWithValue("@Description", (object?)extra.Description ?? DBNull.Value); command.Parameters.AddWithValue("@ChargeMode", extra.ChargeMode); command.Parameters.AddWithValue("@BasePrice", (object?)extra.BasePrice ?? DBNull.Value); command.Parameters.AddWithValue("@CurrencyCode", extra.CurrencyCode ?? "USD"); command.Parameters.AddWithValue("@SortOrder", extra.SortOrder); var extraIdParameter = new SqlParameter("@NewExtraId", SqlDbType.Int) { Direction = ParameterDirection.Output }; command.Parameters.Add(extraIdParameter); await command.ExecuteNonQueryAsync(cancellationToken); // Get the generated TourExtraId var extraId = (int)(extraIdParameter.Value ?? 0); // If per-person pricing, store ticket-specific prices if (extra.ChargeMode == 2 && extra.TicketPrices is not null) { foreach (var price in extra.TicketPrices) { using var priceCommand = new SqlCommand("sp_TourExtraPerTicketType_Insert", connection) { CommandType = CommandType.StoredProcedure }; priceCommand.Parameters.AddWithValue("@TourExtraId", extraId); priceCommand.Parameters.AddWithValue("@TourTicketTypeId", 0); // Placeholder priceCommand.Parameters.AddWithValue("@Price", (object?)price.Price ?? DBNull.Value); await priceCommand.ExecuteNonQueryAsync(cancellationToken); } } }}
The current implementation uses a placeholder TourTicketTypeId of 0 for per-person pricing. In production, this should be mapped to the actual database ID of the ticket type.
public sealed record TourExtraDetailDto( int TourExtraId, string Name, string? Description, byte ChargeMode, // 0=Free, 1=Flat, 2=Per Person decimal? BasePrice, string CurrencyCode, bool IsActive, int SortOrder, IReadOnlyList<TourExtraPricePerTicketDetailDto> TicketPrices);public sealed record TourExtraPricePerTicketDetailDto( int TourExtraPerTicketTypeId, int TourExtraId, int TourTicketTypeId, decimal Price);
Current Limitation: The TourExtraTicketPriceRequest uses a Guid TicketTypeId from the frontend, but the database stores integer IDs. This mapping is not fully implemented in the current version.Workaround: The stored procedure uses a placeholder TourTicketTypeId of 0. In production, you’ll need to:
Capture the generated TourTicketTypeId when creating ticket types
Map the frontend Guid to the database int ID
Pass the correct ID to sp_TourExtraPerTicketType_Insert