From 71d5a3c2f800aaa4dd3faef7b82050f69e26c2f2 Mon Sep 17 00:00:00 2001 From: Zee Aslam Date: Sat, 13 Apr 2024 15:09:32 -0400 Subject: [PATCH] (blissfest) Add ticket info, slight rephrasing. Fix some discord related bugs --- .envrc | 2 +- .gitignore | 1 + bot/bot.go | 2 +- bot/discord/manager.go | 1 + bot/handlers/blissfest.go | 63 ++++++++++--- bot/internal/config/config.go | 9 ++ bot/providers/app_command_handlers.go | 21 +++-- bot/providers/blissfest.go | 58 ++++++++++-- bot/providers/config.go | 3 +- bot/providers/discord.go | 4 +- bot/services/blissfest_service.go | 55 +++++++++++ bot/services/discord_service.go | 27 +++++- internal/models/feature_keys.go | 6 ++ internal/models/showclix.go | 23 +++++ sample_response_showclix_levels.json | 128 ++++++++++++++++++++++++++ 15 files changed, 367 insertions(+), 36 deletions(-) create mode 100755 internal/models/feature_keys.go create mode 100755 internal/models/showclix.go create mode 100755 sample_response_showclix_levels.json diff --git a/.envrc b/.envrc index 7d29b15..1ae51ed 100755 --- a/.envrc +++ b/.envrc @@ -1 +1 @@ -source .config.test +source .config.dev diff --git a/.gitignore b/.gitignore index d6db5bd..0fc80f4 100755 --- a/.gitignore +++ b/.gitignore @@ -13,6 +13,7 @@ *.env.* *.config.(^sample) +.config.dev config.yaml bloopyboi_dev bloopyboii_dev diff --git a/bot/bot.go b/bot/bot.go index 6869ede..ce859da 100755 --- a/bot/bot.go +++ b/bot/bot.go @@ -50,7 +50,7 @@ func (bot *BloopyBoi) Run(ctx context.Context) error { } bot.log.Debug(fmt.Sprintf("FeatureMap contains %d entries", len(providers.GetFeatures()))) - bot.log.Debug(fmt.Sprintf("Experimental is enabled: %v", providers.IsFeaturedConfigured("experimental"))) + bot.log.Debug(fmt.Sprintf("Experimental is enabled: %v", providers.IsFeatureEnabled("experimental"))) errGroup, ctx := errgroup.WithContext(ctx) errGroup.Go(func() error { diff --git a/bot/discord/manager.go b/bot/discord/manager.go index 18e1adb..17ca081 100755 --- a/bot/discord/manager.go +++ b/bot/discord/manager.go @@ -75,6 +75,7 @@ func (d *DiscordManager) Start(ctx context.Context) error { if err != nil { return fmt.Errorf("While opening a connection: %w", err) } + // d.discordSvc.GetSession().LogLevel = discordgo.LogDebug d.log.Info("Registering App Commands") for _, v := range providers.GetDiscordAppCommands(d.discordCfg.GuildConfigs) { diff --git a/bot/handlers/blissfest.go b/bot/handlers/blissfest.go index ec5c465..07915b9 100755 --- a/bot/handlers/blissfest.go +++ b/bot/handlers/blissfest.go @@ -20,8 +20,8 @@ type BlissfestCommand struct { blissSvc *services.BlissfestService // GuildID for which this command will be active // For global commands, set to "" - guildId string - roles []int64 + guildId string + roles []int64 } func NewBlissfestCommand(svc *services.BlissfestService) *BlissfestCommand { @@ -32,11 +32,12 @@ func NewBlissfestCommand(svc *services.BlissfestService) *BlissfestCommand { logger: log.NewZapLogger().Named("blissfest_command"), blissSvc: svc, guildId: "", - roles: []int64{}, + roles: []int64{}, } } func (p *BlissfestCommand) WithGuild(guildId string) *BlissfestCommand { + p.logger.Debug("setting guild", zap.String("guildId", guildId)) p.guildId = guildId return p } @@ -67,6 +68,7 @@ func (p *BlissfestCommand) GetAppCommand() *discordgo.ApplicationCommand { func (p *BlissfestCommand) GetAppCommandHandler() func(s *discordgo.Session, i *discordgo.InteractionCreate) { return func(s *discordgo.Session, i *discordgo.InteractionCreate) { + p.logger.Debug("received interaction", zap.String("interactionID", i.ID), zap.String("username", GetDiscordUserFromInteraction(i).Username)) getLineUp := false // Access options in the order provided by the user. options := i.ApplicationCommandData().Options @@ -79,30 +81,61 @@ func (p *BlissfestCommand) GetAppCommandHandler() func(s *discordgo.Session, i * bsvc := p.blissSvc var resData discordgo.InteractionResponseData - if getLineUp { - resData = discordgo.InteractionResponseData{ - Embeds: []*discordgo.MessageEmbed{ + resEmbeds := []*discordgo.MessageEmbed{} + + adultWeekendPriceLevel, err := bsvc.GetAdultWeekendPriceLevel() + var adultWeekendPriceLevelEmbed *discordgo.MessageEmbed + if err != nil { + p.logger.Warn("error getting adult weekend price level. Not including in response", zap.Error(err)) + } else { + adultWeekendPriceLevelEmbed = &discordgo.MessageEmbed{ + Title: "Adult Weekend (18+) Ticket Info", + Fields: []*discordgo.MessageEmbedField{ { - Author: &discordgo.MessageEmbedAuthor{}, - Image: &discordgo.MessageEmbedImage{ - URL: bsvc.GetLineupImageURI(), - }, + Name: "Active", + Value: fmt.Sprintf("%t", adultWeekendPriceLevel.Active == "1"), + }, + { + Name: "Price", + Value: adultWeekendPriceLevel.Price, //fmt.Sprintf("%.2f",adultWeekendPriceLevel.Price), + }, + { + Name: "Transaction Limit", + Value: adultWeekendPriceLevel.TransactionLimit, //fmt.Sprintf("%d", adultWeekendPriceLevel.TransactionLimit), }, }, - Title: "Blissfest", + } + resEmbeds = append(resEmbeds, adultWeekendPriceLevelEmbed) + } + + if getLineUp { + resEmbeds = append(resEmbeds, &discordgo.MessageEmbed{ + Title: fmt.Sprintf("%d Blissfest Lineup", bsvc.GetStartTime().Year()), + Author: &discordgo.MessageEmbedAuthor{}, + Image: &discordgo.MessageEmbedImage{ + URL: bsvc.GetLineupImageURI(), + }, + }) + } + if len(resEmbeds) > 0 { + resData = discordgo.InteractionResponseData{ + Embeds: resEmbeds, + Title: "Blissfest", // pending https://github.com/dustin/go-humanize/pull/92 // Content: fmt.Sprintf("%s left", humanize.Time(bsvc.GetTimeUntilStart(nil))), - Content: fmt.Sprintf("blissfest starts in %s", humanize.Time(*bsvc.GetStartTime())), + Content: fmt.Sprintf("blissfest starts %s", humanize.Time(*bsvc.GetStartTime())), } - } else { resData = discordgo.InteractionResponseData{ Title: "Blissfest", - Content: fmt.Sprintf("blissfest starts in %s", humanize.Time(*bsvc.GetStartTime())), + // pending https://github.com/dustin/go-humanize/pull/92 + // Content: fmt.Sprintf("%s left", humanize.Time(bsvc.GetTimeUntilStart(nil))), + Content: fmt.Sprintf("blissfest start %s", humanize.Time(*bsvc.GetStartTime())), } } + p.logger.Debug("finished constructing response", zap.Bool("getLineup", getLineUp)) - err := s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{ + err = s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{ Type: discordgo.InteractionResponseChannelMessageWithSource, Data: &resData, }) diff --git a/bot/internal/config/config.go b/bot/internal/config/config.go index 46fbcc5..7727c9d 100755 --- a/bot/internal/config/config.go +++ b/bot/internal/config/config.go @@ -142,3 +142,12 @@ func (myConfig *AppConfig) GetConfiguredFeatureNames() []string { } return names } + +// Checks FeatureConfigs for key +func (myConfig *AppConfig) IsFeaturedEnabled(key string) bool { + fCfg, ok := myConfig.FeatureMap[key] + if !ok { + return false + } + return fCfg.Enabled +} diff --git a/bot/providers/app_command_handlers.go b/bot/providers/app_command_handlers.go index 3b99775..ace8db3 100755 --- a/bot/providers/app_command_handlers.go +++ b/bot/providers/app_command_handlers.go @@ -4,6 +4,7 @@ import ( "github.com/h3mmy/bloopyboi/bot/handlers" "github.com/h3mmy/bloopyboi/bot/internal/config" "github.com/h3mmy/bloopyboi/bot/internal/models" + pkgmodels "github.com/h3mmy/bloopyboi/internal/models" "go.uber.org/zap" ) @@ -12,7 +13,7 @@ func GetDiscordAppCommands(cfgs []config.DiscordGuildConfig) []models.DiscordApp handls = append(handls, handlers.NewInspiroCommand(GetInspiroService())) handls = append(handls, GetGuildAppCommands(cfgs)...) logger.Debug("got discord commands", zap.Int("count", len(handls))) - return handls + return handls } func GetGuildAppCommands(cfgs []config.DiscordGuildConfig) []models.DiscordAppCommand { @@ -20,10 +21,10 @@ func GetGuildAppCommands(cfgs []config.DiscordGuildConfig) []models.DiscordAppCo logger.Debug("getting configs for guilds", zap.Int("count", len(cfgs))) for _, guild := range cfgs { logger.Debug("getting guild commands", zap.Int("count", len(guild.GuildCommandConfig))) - for _,v := range guild.GuildCommandConfig { + for _, v := range guild.GuildCommandConfig { logger.Debug("getting guild command", zap.String("name", v.Name), zap.Bool("enabled", v.Enabled)) if v.Enabled { - cmd := GetCommandWithConfig(guild.GuildId, v) + cmd := GetCommandWithConfig(guild.GuildId, v) if cmd != nil { handls = append(handls, cmd) } @@ -35,25 +36,29 @@ func GetGuildAppCommands(cfgs []config.DiscordGuildConfig) []models.DiscordAppCo } func GetCommandWithConfig(guildId string, cfg config.GuildCommandConfig) models.DiscordAppCommand { + flogger := logger.With(zap.String("guild_app_command", cfg.Name), zap.String("guild_id", guildId)) // get from repository TODO if cfg.Name == "blissfest" { - logger.Debug("adding blissfest command") - return handlers.NewBlissfestCommand(GetBlissfestService()).WithGuild(guildId).WithRoles(cfg.Roles...) + flogger.Debug("Checking if feature enabled") + if IsFeatureEnabled(pkgmodels.BlissfestFeatureKey) { + return handlers.NewBlissfestCommand(GetBlissfestService()).WithGuild(guildId).WithRoles(cfg.Roles...) + } + flogger.Warn("blissfest guild command exists but feature is disabled") } else if cfg.Name == "book" { bookSvc, err := GetBookService() if err != nil { - logger.Error("failed to create book svc", zap.Error(err)) + flogger.Error("failed to create book svc", zap.Error(err)) } else { return handlers.NewBookCommand(bookSvc).WithRoles(cfg.Roles...).WithGuild(guildId) } } else if cfg.Name == "requests" { bookSvc, err := GetBookService() if err != nil { - logger.Error("failed to create book svc", zap.Error(err)) + flogger.Error("failed to create book svc", zap.Error(err)) } else { return handlers.NewUserRequestCommand(bookSvc).WithRoles(cfg.Roles...).WithGuild(guildId) } } - logger.Warn("not adding command", zap.String("name", cfg.Name)) + flogger.Warn("not adding command", zap.String("name", cfg.Name)) return nil } diff --git a/bot/providers/blissfest.go b/bot/providers/blissfest.go index 3d62c9f..91b9dd4 100755 --- a/bot/providers/blissfest.go +++ b/bot/providers/blissfest.go @@ -1,18 +1,62 @@ package providers import ( + "fmt" "time" - pkgmodels "github.com/h3mmy/bloopyboi/internal/models" + "github.com/h3mmy/bloopyboi/bot/internal/config" "github.com/h3mmy/bloopyboi/bot/services" + pkgmodels "github.com/h3mmy/bloopyboi/internal/models" + "go.uber.org/zap" ) +const BlissfestStartDateKey = "start_date" +const BlissfestHomepage = "https://www.blissfest.org/" + +// Blissfest is in MI and bound to the US Eastern Timezone +// Since it always happens in the summer we can assume EDT +var blissfestTZ = time.FixedZone("UTC-4", -4*60*60) +var defaultBlissfestStartDate = time.Date(2024, 7, 12, 0, 0, 0, 0, blissfestTZ) + func GetBlissfestService() *services.BlissfestService { - location, _ := time.LoadLocation("America/Detroit") - config := pkgmodels.BlissfestConfig{ - Start: time.Date(2024, 7, 12, 9, 0, 0, 0, location), - End: time.Date(2024, 7, 14, 9, 0, 0, 0, location), - Homepage: "https://www.blissfest.org/", + // Blissfest is in MI and bound to the US Eastern Timezone + if IsFeatureEnabled(pkgmodels.BlissfestFeatureKey) { + // check for provided start date + cfg := GetFeatures()[pkgmodels.BlissfestFeatureKey] + startDate := getBlissfestStartDate(cfg) + + year, month, day := startDate.Date() + logger.Debug(fmt.Sprintf("startDate year: %d, month: %d, day: %d", year, month, day)) + // blissfest always starts at 9am on a Friday + finalStartDate := time.Date(year, month, day, 9, 0, 0, 0, blissfestTZ) + logger.Debug("finalized blissfest start date", zap.Time("startDate", finalStartDate)) + // everyone is supposed to be out by noon on the following Monday (3 days + 3 hours => 4500min => 75 hours) + finalEndDate := finalStartDate.Add(75 * time.Hour) + logger.Debug("finalized blissfest end date", zap.Time("endDate", finalEndDate)) + return services.NewBlissfestService(pkgmodels.BlissfestConfig{ + Start: finalStartDate, + End: finalEndDate, + Homepage: BlissfestHomepage, + }) + } + logger.Warn("blissfest feature not enabled") + + return nil +} + +func getBlissfestStartDate(cfg config.FeatureConfig) time.Time { + if cfg.Data != nil { + startDateString, ok := cfg.Data[BlissfestStartDateKey] + if ok { + logger.Debug("parsing start date", zap.String("providedDate", startDateString)) + startDate, err := time.Parse("2006-01-02", startDateString) + if err != nil { + logger.Error("error parsing configured start date", zap.Error(err)) + startDate = defaultBlissfestStartDate + } + logger.Debug("finished parsing start date", zap.String("providedDate", startDateString), zap.Time("startDate", startDate)) + return startDate + } } - return services.NewBlissfestService(config) + return defaultBlissfestStartDate } diff --git a/bot/providers/config.go b/bot/providers/config.go index c608b8f..94c5e20 100755 --- a/bot/providers/config.go +++ b/bot/providers/config.go @@ -8,7 +8,6 @@ import ( "go.uber.org/zap/zapcore" ) - func GetDiscordConfig() *config.DiscordConfig { AppConfig := config.GetConfig() return AppConfig.DiscordConfig @@ -28,7 +27,7 @@ func GetFeatures() map[string]config.FeatureConfig { } // Checks FeatureConfigs for key -func IsFeaturedConfigured(key string) bool { +func IsFeatureEnabled(key string) bool { AppConfig := config.GetConfig() fCfg, ok := AppConfig.FeatureMap[key] if !ok { diff --git a/bot/providers/discord.go b/bot/providers/discord.go index 3a8e88f..085860d 100755 --- a/bot/providers/discord.go +++ b/bot/providers/discord.go @@ -3,10 +3,12 @@ package providers import ( "github.com/h3mmy/bloopyboi/bot/internal/config" "github.com/h3mmy/bloopyboi/bot/services" + "go.uber.org/zap" ) func NewDiscordServiceWithConfig(cfg *config.DiscordConfig) (*services.DiscordService, error) { dsvc := services.NewDiscordService().WithConfig(cfg) err := dsvc.RefreshDBConnection() - return dsvc, err + logger.Warn("encountered error refreshing db connection. persistence may not be available", zap.Error(err)) + return dsvc, nil } diff --git a/bot/services/blissfest_service.go b/bot/services/blissfest_service.go index 1591630..d6d8210 100755 --- a/bot/services/blissfest_service.go +++ b/bot/services/blissfest_service.go @@ -1,6 +1,10 @@ package services import ( + "encoding/json" + "fmt" + "io" + "net/http" "time" "github.com/h3mmy/bloopyboi/bot/internal/log" @@ -14,8 +18,16 @@ import ( // 2024: "https://www.blissfestfestival.org/wp-content/uploads/2024/04/Bliss24_IGAnnouncement3-2048x2048.jpg" var lineupImageURI = "https://www.blissfestfestival.org/wp-content/uploads/2024/04/Bliss24_IGAnnouncement3-2048x2048.jpg" +// 2024 blissfest showclix "event_id": 9297272, "parent_event_id": 8615552, +// 2024 blissfest showclix venue_id = 64139 + +var blissfestShowclixEventID = 9297272 + +// 2024 blissfest logo art "https://www.blissfestfestival.org/wp-content/uploads/2024/01/Bliss_Logo_2024F3.png" + // var apiPrefix = "/wp-json/wp/v2" + type BlissfestService struct { bloopymeta models.BloopyMeta config pkgmodels.BlissfestConfig @@ -81,3 +93,46 @@ func (bs *BlissfestService) IsInProgress() bool { func (bs *BlissfestService) GetLineupImageURI() string { return lineupImageURI } + + +func (bs *BlissfestService) GetShowclixTicketData() (*[]pkgmodels.PriceLevel, error) { + resp, err := http.Get(fmt.Sprintf("%s%s/%d/all_levels",pkgmodels.ShowclixAPIURL, pkgmodels.ShowclixAPIEventPrefix, blissfestShowclixEventID)) + if err != nil { + bs.logger.Error("error getting showclix ticket data", zap.Error(err)) + return nil, err + } + + defer resp.Body.Close() + result, err := io.ReadAll(resp.Body) + if err != nil { + bs.logger.Error("error reading showclix ticket data", zap.Error(err)) + return nil, err + } + var priceLevels map[int]pkgmodels.PriceLevel + err = json.Unmarshal(result, &priceLevels) + if err != nil { + bs.logger.Error("error unmarshalling showclix ticket data", zap.Error(err), zap.ByteString("response", result)) + return nil, err + } + priceLevelSlice := []pkgmodels.PriceLevel{} + for _, priceLevel := range priceLevels { + priceLevelSlice = append(priceLevelSlice, priceLevel) + } + return &priceLevelSlice, nil +} + +func (bs *BlissfestService) GetAdultWeekendPriceLevel() (*pkgmodels.PriceLevel, error) { + priceLevelName := "Adult Weekend (18+)" + priceLevels, err := bs.GetShowclixTicketData() + if err != nil { + bs.logger.Error("error getting price levels", zap.Error(err)) + return nil, err + } + for _, priceLevel := range *priceLevels { + if priceLevel.Level == priceLevelName { + return &priceLevel, nil + } + } + bs.logger.Warn("no price level found", zap.String("priceLevelName", priceLevelName)) + return nil, nil +} diff --git a/bot/services/discord_service.go b/bot/services/discord_service.go index 8024bba..d78cfe9 100755 --- a/bot/services/discord_service.go +++ b/bot/services/discord_service.go @@ -54,6 +54,7 @@ func (d *DiscordService) WithSession(session *discordgo.Session) *DiscordService d.discordSession.Identify.Intents = d.intents return d } + // NewDiscordServiceWithToken creates a new DiscordService with a token // Oauth tokens need to be prefixed with "Bearer " instead so this won't work for that func (d *DiscordService) WithToken(token string) *DiscordService { @@ -167,6 +168,30 @@ func (d *DiscordService) AddInteractionHandlerProxy() { // De-registers all app commands registered with this service. // Intended for use by the shutdown handler. func (d *DiscordService) DeleteAppCommands() { + allGlobalCmds, err := d.discordSession.ApplicationCommands(d.discordSession.State.User.ID, "") + if err != nil { + d.logger.Error("error getting global commands", zap.Error(err)) + } else { + d.logger.Debug(fmt.Sprintf("found %d global commands", len(allGlobalCmds))) + for _, cmd := range allGlobalCmds { + flogger := d.logger.With(zap.String("command", cmd.Name), zap.String("commandID", cmd.ID)) + flogger.Debug(fmt.Sprintf("deleting global command: %v", cmd)) + err := d.discordSession.ApplicationCommandDelete(d.discordSession.State.User.ID, cmd.GuildID, cmd.ID) + if err != nil { + flogger.Error("error deleting global command", zap.Error(err)) + } else { + if d.commandRegistry[cmd.Name] != nil { + if d.commandRegistry[cmd.Name].ID == cmd.ID { + delete(d.commandRegistry, cmd.Name) + } else { + logger.Warn("commands with same name and different IDs!", zap.String("command", cmd.Name), zap.String("commandID 1", cmd.ID), zap.String("commandID 2", d.commandRegistry[cmd.Name].ID)) + } + } else { + d.logger.Warn("deleted command was not found in registry. Likely leftover from a previous instance", zap.String("command", cmd.Name)) + } + } + } + } d.logger.Debug("deleting app commands") for _, cmd := range d.commandRegistry { err := d.discordSession.ApplicationCommandDelete(d.discordSession.State.User.ID, cmd.GuildID, cmd.ID) @@ -176,7 +201,7 @@ func (d *DiscordService) DeleteAppCommands() { } } -// Gets all app commands registered with the discord session +// Gets all app commands registered with the discord session AND the discord Registry // Uses service registry for retrieval IDs and errors are logged func (d *DiscordService) GetCurrentAppCommands() []*discordgo.ApplicationCommand { var commands []*discordgo.ApplicationCommand diff --git a/internal/models/feature_keys.go b/internal/models/feature_keys.go new file mode 100755 index 0000000..3e6c7b1 --- /dev/null +++ b/internal/models/feature_keys.go @@ -0,0 +1,6 @@ +package models + +type FeatureKey string + +const BlissfestFeatureKey = "blissfest" +const InspirobotFeatureKey = "inspiro" diff --git a/internal/models/showclix.go b/internal/models/showclix.go new file mode 100755 index 0000000..74352a5 --- /dev/null +++ b/internal/models/showclix.go @@ -0,0 +1,23 @@ +package models + +const ShowclixAPIURL = "https://api.showclix.com" +const ShowclixAPIEventPrefix = "/Event" + +type PriceLevel struct { + LevelID string `json:"level_id"` //int + EventID string `json:"event_id"` //int + Price string `json:"price"` // float64 + MinPrice string `json:"min_price,omitempty"` // float64 + Level string `json:"level,omitempty"` // string + Active string `json:"active"` // int bool + Description string `json:"description,omitempty"` // string + Subheading string `json:"subheading,omitempty"` // string + ParentID string `json:"parent_id,omitempty"` // int + Position string `json:"position,omitempty"` // int + IncrementBy string `json:"increment_by,omitempty"` // int + TransactionLimit string `json:"transaction_limit,omitempty"` // int + TicketLayoutID string `json:"ticket_layout_id,omitempty"` // int + UpsellPrice string `json:"upsell_price,omitempty"` // float + DiscountType string `json:"discount_type,omitempty"` // string + DiscountVal string `json:"discount_val,omitempty"` // float64 +} diff --git a/sample_response_showclix_levels.json b/sample_response_showclix_levels.json new file mode 100755 index 0000000..3c6ff7a --- /dev/null +++ b/sample_response_showclix_levels.json @@ -0,0 +1,128 @@ +{ + "41693105": { + "level_id": "41693105", + "event_id": "9297272", + "price": "205.00", + "min_price": null, + "level": "Adult Weekend (18+)", + "active": "1", + "description": "", + "subheading": null, + "parent_id": null, + "position": "2", + "increment_by": null, + "transaction_limit": "12", + "ticket_layout_id": null, + "upsell_price": null, + "discount_type": "none", + "discount_val": "0.00" + }, + "41693106": { + "level_id": "41693106", + "event_id": "9297272", + "price": "100.00", + "min_price": null, + "level": "Saturday Adult (No Camping)", + "active": "1", + "description": "", + "subheading": null, + "parent_id": null, + "position": "6", + "increment_by": null, + "transaction_limit": "10", + "ticket_layout_id": null, + "upsell_price": null, + "discount_type": "none", + "discount_val": "0.00" + }, + "41693107": { + "level_id": "41693107", + "event_id": "9297272", + "price": "50.00", + "min_price": null, + "level": "Sunday Adult (No Camping)", + "active": "1", + "description": "", + "subheading": null, + "parent_id": null, + "position": "8", + "increment_by": null, + "transaction_limit": "10", + "ticket_layout_id": null, + "upsell_price": null, + "discount_type": "none", + "discount_val": "0.00" + }, + "41693108": { + "level_id": "41693108", + "event_id": "9297272", + "price": "85.00", + "min_price": null, + "level": "Teen Weekend (13-17)", + "active": "1", + "description": "", + "subheading": null, + "parent_id": null, + "position": "4", + "increment_by": null, + "transaction_limit": "10", + "ticket_layout_id": null, + "upsell_price": null, + "discount_type": "none", + "discount_val": "0.00" + }, + "41693109": { + "level_id": "41693109", + "event_id": "9297272", + "price": "50.00", + "min_price": null, + "level": "Teen Saturday Ticket (13-17 yrs)", + "active": "1", + "description": "", + "subheading": null, + "parent_id": null, + "position": "7", + "increment_by": null, + "transaction_limit": "10", + "ticket_layout_id": null, + "upsell_price": null, + "discount_type": "none", + "discount_val": "0.00" + }, + "41693110": { + "level_id": "41693110", + "event_id": "9297272", + "price": "0.00", + "min_price": null, + "level": "Child (12 & Under)", + "active": "1", + "description": "", + "subheading": null, + "parent_id": null, + "position": "5", + "increment_by": null, + "transaction_limit": "12", + "ticket_layout_id": null, + "upsell_price": null, + "discount_type": "none", + "discount_val": "0.00" + }, + "41693113": { + "level_id": "41693113", + "event_id": "9297272", + "price": "25.00", + "min_price": null, + "level": "Teen Sunday Ticket (13-17 yrs)", + "active": "1", + "description": "", + "subheading": null, + "parent_id": null, + "position": "9", + "increment_by": null, + "transaction_limit": "10", + "ticket_layout_id": null, + "upsell_price": null, + "discount_type": "none", + "discount_val": "0.00" + } +}