# User Pick Lock-Ins ## Project Overview This project allows users to lock-in a NFL schedule before a given week's games start, and will award a user one-point per game won. A weekly and seasonal leaderboard will exist for users to show on the site. ## System Enhancements ### User Pick Lock In #### Requirements 1. User picks become available at the close of the prior week (after Monday Night Football concludes) 2. User picks cannot be submitted after any NFL game transitions to "in progress" 3. User picks can be re-submitted until 1-hour before game start ```sql= CREATE SCHEMA resulting CREATE TABLE [resulting].PrivacyLevels ( PrivacyId int primary key identity, Name varchar(100) not null unique ) INSERT INTO [resulting].PrivacyLevels VALUES ('Unrestricted'), -- no restriction ('Private'), -- users can join with an invite link ('Closed') -- users with invite link cannot join create table resulting.GameResults ( GameResultId bigint primary key identity, GameScheduleNum int not null, [Week] int not null, Outcome int not null, TeamPicked int not null, GameClusterId bigint not null FOREIGN KEY REFERENCES Games(GameClusterId), LeagueGuid uniqueidentifier not null, INDEX IX_GameResults_League_Week NONCLUSTERED(LeagueGuid, [Week]), INDEX IX_GameResults_League NONCLUSTERED(LeagueGuid), --include is important because it will index scan instead of seek without it INDEX IX_GameResults_GameClusterId NONCLUSTERED(GameClusterId) INCLUDE (Outcome) ) CREATE TABLE [resulting].Groups ( GroupGuid uniqueidentifier default newid(), GroupId bigint identity, LeagueClusterId bigint not null, GroupAdministratorId nvarchar(128) not null, GroupName varchar(100), PrivacyLevel int not null default 1, CONSTRAINT FK_Groups_PrivacyLevels FOREIGN KEY (PrivacyLevel) REFERENCES [resulting].PrivacyLevels(PrivacyId), CONSTRAINT FK_Groups_GroupAdministratorId FOREIGN KEY (GroupAdministratorId) REFERENCES dbo.ASPNETUSERS(Id), CONSTRAINT FK_Groups_LeagueClusterId FOREIGN KEY (LeagueClusterId) REFERENCES Leagues(LeagueClusterId), CONSTRAINT PK_Groups_GroupGuid PRIMARY KEY NONCLUSTERED (GroupGuid), INDEX IX_Groups_GroupId CLUSTERED (GroupId) ) CREATE TABLE [resulting].UserPicks( UserPickId bigint identity primary key, LeagueClusterId bigint not null, GameClusterId bigint not null, UserGuid nvarchar(128) not null, TeamPicked int not null, GameWeek int not null, --will be far easier to query this directly PickSubmittedOn DATETIME not null default GETUTCDATE(), CONSTRAINT FK_UserPicks_UserGuid FOREIGN KEY (UserGuid) REFERENCES dbo.ASPNETUSERS(Id), CONSTRAINT FK_UserPicks_TeamPicked FOREIGN KEY (TeamPicked) REFERENCES dbo.Teams(TeamId), CONSTRAINT FK_UserPicks_GameClusterId FOREIGN KEY (GameClusterId) REFERENCES dbo.Games(GameClusterId), -- could get here from the GameClusterId table but useful to have easier queries CONSTRAINT FK_UserPicks_LeagueClusterId FOREIGN KEY (LeagueClusterId) REFERENCES Leagues(LeagueClusterId) , CONSTRAINT UQ_UserPicks_GameClusterId_UserGuid UNIQUE(GameClusterId, UserGuid) ) CREATE NONCLUSTERED INDEX IX_UserPicks_Week_League ON [resulting].UserPicks ([GameWeek], LeagueClusterId) INCLUDE (GameClusterId, UserGuid, TeamPicked, PickSubmittedOn) CREATE NONCLUSTERED INDEX IX_UserPicks_Week ON [resulting].UserPicks ([GameWeek]) CREATE NONCLUSTERED INDEX IX_UserPicks_User ON [resulting].UserPicks ([GameWeek], [UserGuid]) CREATE NONCLUSTERED INDEX IX_League_User_Game ON [resulting].UserPicks ([LeagueClusterId], [UserGuid], [GameClusterId]) CREATE TABLE [resulting].WeeklyRollups( WeeklyRollupId BIGINT PRIMARY KEY IDENTITY, UserGuid nvarchar(128) not null, LeagueClusterId bigint not null, PointsScored int not null, GameWeek int not null, --will be far easier to query this directly CONSTRAINT FK_WeeklyRollups_UserGuid FOREIGN KEY (UserGuid) REFERENCES dbo.ASPNETUSERS(Id), CONSTRAINT FK_WeeklyRollups_LeagueClusterId FOREIGN KEY (LeagueClusterId) REFERENCES Leagues(LeagueClusterId), CONSTRAINT UQ_WeeklyRollups_League_Week_User UNIQUE(LeagueClusterId,GameWeek,UserGuid) ) CREATE NONCLUSTERED INDEX IX_WeeklyRollups_User ON [resulting].WeeklyRollups (UserGuid) CREATE NONCLUSTERED INDEX IX_WeeklyRollups_LeagueClusterId ON [resulting].WeeklyRollups (LeagueClusterId) ALTER TABLE resulting.UserPicks ADD ProcessingState INT DEFAULT 0 NOT NULL CREATE NONCLUSTERED INDEX IX_UserPicks_ProcessingState ON resulting.UserPicks(ProcessingState) ``` #### Data generation for testing ```sql= declare userCursor cursor for select top 10 UserId = Id from ASPNETUSERS open userCursor declare @UserId varchar(128) fetch next from userCursor INTO @UserId ; WHILE @@FETCH_STATUS = 0 BEGIN INSERT INTO resulting.UserPicks (UserGuid, LeagueClusterId, GameClusterId, GameWeek, TeamPicked) (SELECT @UserId, LeagueClusterId, GameClusterId, [Week], (CASE WHEN ABS(CHECKSUM(NEWID()) % 2)+1 = 1 THEN HomeId ELSE AwayId END) AS TeamPicked FROM Games g join leagues l on g.LeagueGUID = l.LeagueGUID where g.LeagueGuid = '842CB5EC-193A-4D6D-8002-801297F04F53') FETCH NEXT FROM userCursor INTO @UserId END CLOSE userCursor DEALLOCATE userCursor ``` ### Pick Submission Save Pick `POST /save-pick` read userid from header: `x-playoffpredictors-userId` UserId for `pehrrobertson@fastmail.com` in sql-dev `6948bcbe-4e70-415d-958e-2b014e56960e` *extraction of userid is done via api management -- you can code this value into postman yourself* ```typescript= enum Outcomes{ NONE = 0, Home = 1, Away = 2, Tie = 3 } ``` ```javascript= { "LeagueClusterId": "842CB5EC-193A-4D6D-8002-801297F04F53", "week": 1, "results": [ { "GameGuid": "373A1135-AD2A-489E-B3CA-D7780A6FA26D", "outcome": 1, "teamPicked": 14 }, { "GameGuid": "6FE193FC-BE81-4C38-BF5D-D9E6C17CD190", "outcome": 2, "teamPicked": 32 }, { "GameGuid": "C32BCE53-794C-4164-8168-DA220EF6892C", "outcome": 1, "teamPicked": 28 }, { "GameGuid": "79B178C9-1F73-466A-AB7B-DBAD00B1A13A", "outcome": 1, "teamPicked": 11 }, { "GameGuid": "D3DA2722-0957-435A-9092-DF4F4A0E3CE5", "outcome": 1, "teamPicked": 11 }, { "GameGuid": "4BD3969A-2661-4BCD-BEB3-DFF5F07BFF07", "outcome": 2, "teamPicked": 19 }, { "GameGuid": "69BC595D-0D71-4476-9C5E-DFF8E0EF3CAE", "outcome": 2, "teamPicked": 6 } ] } ``` ```mermaid sequenceDiagram participant UI participant re as ResultingEngine participant le as LeagueEngine participant sb as ServiceBus participant sql as Sql alt UserLoggedIn UI->>le: IsWeekAvailable alt Submissions Available UI->>re: save-picks re->>le: IsSubmissionsAvailable (probably cache) re->>le: Get LeagueData (cache) re->>sb: SavePicks re->>UI: OK par async pick saves re->>sb: ServiceBusTrigger sb->>re: UserGamePick re->>sql: SavePicks end end end ``` * to take load off of the system, picks should be enqueued to service bus, and dequeued one-by-one to update the system asyncrhonously * the updates should never be accepted if the enqueued time occurs after a game start time for that week * Since ResultingEngine emits messages to itself, the contracts can be entirely defined in the project. We don't need to use the `PlayoffPredictors.Exports` projects for the service bus messages or the internal contracts ### Resulting 1. UpdateEngine can emit a message when a game is resulted 2. (new engine) ScoringEngine takes pick, pulls users that selected that game, awards them one point 1. could use event sourcing? worth a look 4. Users receive weekly email reports of their picks and leader board ranks (can schedule job to run on Wednesdays) ```mermaid sequenceDiagram participant le as LeagueEngine participant sb as ServiceBus participant se as ResultingEngine participant sql le->>sb: WeekConcludedEvent #durable function to orchestrate maybe? sb->>se: WeekConcludedHandler se->>sql: Select games for league+week se->>se: Select games user got correct outcome of se->>sql: Insert weekly rollup # could be a timer trigger to query/update the sql records in-line se->>sql: Any remaining scores for week? alt any remaining? se->>sb: Scoring Done else se->>se: Keep Processing end ``` Once there are no records remaining for the week, we need to move on to the next stage, which is to roll off the week's total results. Can be json file to blob or redis. ### User Profile 1. User Profiles need to have an external username entry, configurable from the account page. This will be what's shown on leaderboards 1. disallow entry of usernames with words like "playoffpredictors" in it