UNPKG

chat-bet-parse

Version:

TypeScript package for parsing sports betting contract text into structured data types compatible with SQL Server stored procedures

1,380 lines (1,372 loc) 47 kB
// src/errors/index.ts var ChatBetParseError = class extends Error { constructor(message, rawInput, position) { super(message.trimEnd()); this.name = "ChatBetParseError"; this.rawInput = rawInput; this.position = position; } }; var InvalidChatFormatError = class extends ChatBetParseError { constructor(rawInput, reason) { super(`Invalid chat format: ${reason}. Input: "${rawInput}"`, rawInput); this.name = "InvalidChatFormatError"; } }; var InvalidContractTypeError = class extends ChatBetParseError { constructor(rawInput, contractPortion) { super( `Unable to determine contract type from: "${contractPortion}". Input: "${rawInput}"`, rawInput ); this.name = "InvalidContractTypeError"; } }; var InvalidPriceFormatError = class extends ChatBetParseError { constructor(rawInput, priceStr) { super( `Invalid USA price format: "${priceStr}". Expected format: +150, -110, -115.5, ev, or even. Input: "${rawInput}"`, rawInput ); this.name = "InvalidPriceFormatError"; } }; var InvalidSizeFormatError = class extends ChatBetParseError { constructor(rawInput, sizeStr, expectedFormat) { super( `Invalid size format: "${sizeStr}". Expected: ${expectedFormat}. Input: "${rawInput}"`, rawInput ); this.name = "InvalidSizeFormatError"; } }; var InvalidLineValueError = class extends ChatBetParseError { constructor(rawInput, line) { super( `Invalid line value: ${line}. Line must be divisible by 0.5. Input: "${rawInput}"`, rawInput ); this.name = "InvalidLineValueError"; } }; var InvalidTeamFormatError = class extends ChatBetParseError { constructor(rawInput, teamStr, reason) { super(`Invalid team format: "${teamStr}". ${reason}. Input: "${rawInput}"`, rawInput); this.name = "InvalidTeamFormatError"; } }; var InvalidPeriodFormatError = class extends ChatBetParseError { constructor(rawInput, periodStr) { super( `Invalid period format: "${periodStr}". Expected formats: 1st inning, F5, 1H, Q1, etc. Input: "${rawInput}"`, rawInput ); this.name = "InvalidPeriodFormatError"; } }; var InvalidGameNumberError = class extends ChatBetParseError { constructor(rawInput, gameStr) { super( `Invalid game number format: "${gameStr}". Expected formats: G2, GM1, #2, etc. Input: "${rawInput}"`, rawInput ); this.name = "InvalidGameNumberError"; } }; var InvalidRotationNumberError = class extends ChatBetParseError { constructor(rawInput, rotationStr) { super( `Invalid rotation number: "${rotationStr}". Must be a positive integer. Input: "${rawInput}"`, rawInput ); this.name = "InvalidRotationNumberError"; } }; var InvalidPropFormatError = class extends ChatBetParseError { constructor(rawInput, propStr, availableProps) { super( `Invalid prop format: "${propStr}". Available props: ${availableProps.join(", ")}. Input: "${rawInput}"`, rawInput ); this.name = "InvalidPropFormatError"; } }; var InvalidSeriesLengthError = class extends ChatBetParseError { constructor(rawInput, lengthStr) { super( `Invalid series length: "${lengthStr}". Must be a positive integer. Input: "${rawInput}"`, rawInput ); this.name = "InvalidSeriesLengthError"; } }; var MissingSizeForFillError = class extends ChatBetParseError { constructor(rawInput) { super( `Missing size for fill (YG) bet. Fill bets must include size with "=" format. Input: "${rawInput}"`, rawInput ); this.name = "MissingSizeForFillError"; } }; var UnrecognizedChatPrefixError = class extends ChatBetParseError { constructor(rawInput, prefix) { super( `Unrecognized chat prefix: "${prefix}". Chat must be either a chat order (start with "IW" for "i want") or a chat fill (start with "YG", for "you got"). Input: "${rawInput}"`, rawInput ); this.name = "UnrecognizedChatPrefixError"; } }; var AmbiguousContractError = class extends ChatBetParseError { constructor(rawInput, possibleTypes) { super( `Ambiguous contract type. Could be: ${possibleTypes.join(", ")}. Please be more specific. Input: "${rawInput}"`, rawInput ); this.name = "AmbiguousContractError"; } }; function createPositionError(ErrorClass, rawInput, position, ...args) { const error = new ErrorClass(rawInput, ...args); error.position = position; return error; } // src/parsers/utils.ts function parsePrice(priceStr, rawInput) { const cleaned = priceStr.trim(); if (cleaned.toLowerCase() === "ev" || cleaned.toLowerCase() === "even") { return 100; } const match = cleaned.match(/^([+-])(\d+(?:\.\d+)?)$/); if (!match) { throw new InvalidPriceFormatError(rawInput, priceStr); } const sign = match[1]; const value = parseFloat(match[2]); return sign === "+" ? value : -value; } function parseOrderSize(sizeStr, rawInput) { const cleaned = sizeStr.trim(); if (cleaned.startsWith("$")) { const value2 = parseFloat(cleaned.substring(1)); if (isNaN(value2) || value2 < 0) { throw new InvalidSizeFormatError( rawInput, sizeStr, "positive dollar amount like $100 or $2.50" ); } return { value: value2, format: "dollar" }; } if (cleaned.toLowerCase().endsWith("k")) { const value2 = parseFloat(cleaned.slice(0, -1)); if (isNaN(value2) || value2 < 0) { throw new InvalidSizeFormatError(rawInput, sizeStr, "positive number with k like 4k or 2.5k"); } return { value: value2 * 1e3, format: "k_notation" }; } const value = parseFloat(cleaned); if (isNaN(value) || value < 0) { throw new InvalidSizeFormatError(rawInput, sizeStr, "positive decimal number like 2.0 or 0.50"); } return { value, format: "unit" }; } function parseFillSize(sizeStr, rawInput) { const cleaned = sizeStr.trim(); if (cleaned.startsWith("$")) { const value2 = parseFloat(cleaned.substring(1)); if (isNaN(value2) || value2 < 0) { throw new InvalidSizeFormatError( rawInput, sizeStr, "positive dollar amount like $100 or $2.00" ); } return { value: value2, format: "dollar" }; } if (cleaned.toLowerCase().endsWith("k")) { const value2 = parseFloat(cleaned.slice(0, -1)); if (isNaN(value2) || value2 < 0) { throw new InvalidSizeFormatError(rawInput, sizeStr, "positive number with k like 4k or 2.5k"); } return { value: value2 * 1e3, format: "k_notation" }; } const value = parseFloat(cleaned); if (isNaN(value) || value < 0) { throw new InvalidSizeFormatError( rawInput, sizeStr, "positive decimal number like 2.0 (=$2000) or 0.563 (=$563)" ); } return { value: value * 1e3, format: "decimal_thousands" }; } function parseLine(lineStr, rawInput) { const value = parseFloat(lineStr); if (isNaN(value)) { throw new InvalidLineValueError(rawInput, value); } if (value * 2 % 1 !== 0) { throw new InvalidLineValueError(rawInput, value); } return value; } function parsePeriod(periodStr, rawInput) { const cleaned = periodStr.toLowerCase().trim(); if (!cleaned || cleaned === "fg" || cleaned === "full game") { return { PeriodTypeCode: "M", PeriodNumber: 0 }; } if (cleaned === "f5" || cleaned === "h1" || cleaned === "1h" || cleaned === "first half" || cleaned === "1st half" || cleaned === "first h" || cleaned === "1st h" || cleaned === "first five" || cleaned === "1st five" || cleaned === "first 5" || cleaned === "1st 5") { return { PeriodTypeCode: "H", PeriodNumber: 1 }; } if (cleaned === "f3") { return { PeriodTypeCode: "H", PeriodNumber: 13 }; } if (cleaned === "h2" || cleaned === "2h" || cleaned === "second half" || cleaned === "2nd half" || cleaned === "second h" || cleaned === "2nd h") { return { PeriodTypeCode: "H", PeriodNumber: 2 }; } const quarterMatch = cleaned.match(/^(?:(\d+)(?:st|nd|rd|th)?\s*quarter?|q(\d+)|(\d+)q)$/); if (quarterMatch) { const qNum = parseInt(quarterMatch[1] || quarterMatch[2] || quarterMatch[3]); if (qNum >= 1 && qNum <= 4) { return { PeriodTypeCode: "Q", PeriodNumber: qNum }; } } const inningMatch = cleaned.match(/^(?:(\d+)(?:st|nd|rd|th)?\s*inning?|i(\d+)|(\d+)i)$/); if (inningMatch) { const iNum = parseInt(inningMatch[1] || inningMatch[2] || inningMatch[3]); if (iNum >= 1 && iNum <= 15) { return { PeriodTypeCode: "I", PeriodNumber: iNum }; } } const periodMatch = cleaned.match(/^(?:(\d+)(?:st|nd|rd|th)?\s*period?|p(\d+)|(\d+)p)$/); if (periodMatch) { const pNum = parseInt(periodMatch[1] || periodMatch[2] || periodMatch[3]); if (pNum >= 1 && pNum <= 4) { return { PeriodTypeCode: "P", PeriodNumber: pNum }; } } throw new InvalidPeriodFormatError(rawInput, periodStr); } function parseGameNumber(gameStr, rawInput) { const cleaned = gameStr.toLowerCase().trim(); const match = cleaned.match(/^(?:g(?:m)?(\d+)|#(\d+))$/); if (!match) { throw new InvalidGameNumberError(rawInput, gameStr); } const gameNum = parseInt(match[1] || match[2]); if (gameNum < 1 || gameNum > 10) { throw new InvalidGameNumberError(rawInput, gameStr); } return gameNum; } function parseRotationNumber(rotationStr, rawInput) { const value = parseInt(rotationStr.trim()); if (isNaN(value) || value < 1 || value > 9999) { throw new InvalidRotationNumberError(rawInput, rotationStr); } return value; } function parseTeam(teamStr, rawInput) { const cleaned = teamStr.trim(); if (!cleaned) { throw new InvalidTeamFormatError(rawInput, teamStr, "Team name cannot be empty"); } if (!/^[a-zA-Z0-9\s&\-.']+$/.test(cleaned)) { throw new InvalidTeamFormatError(rawInput, teamStr, "Team name contains invalid characters"); } if (cleaned.length > 50) { throw new InvalidTeamFormatError(rawInput, teamStr, "Team name too long (max 50 characters)"); } return cleaned; } function detectContestantType(contestant) { if (/^[A-Z]\.\s+[A-Za-z]+/.test(contestant)) { return "Individual"; } return void 0; } function parseTeams(teamsStr, rawInput) { const parts = teamsStr.split("/"); if (parts.length === 1) { return { team1: parseTeam(parts[0], rawInput) }; } else if (parts.length === 2) { return { team1: parseTeam(parts[0], rawInput), team2: parseTeam(parts[1], rawInput) }; } else { throw new InvalidTeamFormatError(rawInput, teamsStr, 'Too many "/" separators'); } } function inferSportAndLeague(rotationNumber) { if (rotationNumber) { if (rotationNumber >= 500 && rotationNumber < 600) { return { sport: "Basketball", league: "NBA" }; } if (rotationNumber >= 800 && rotationNumber < 900 || rotationNumber >= 9900 && rotationNumber < 1e4) { return { sport: "Baseball", league: "MLB" }; } } return {}; } function matchesIgnoreCase(text, pattern) { return text.toLowerCase().includes(pattern.toLowerCase()); } function parseOverUnder(ouStr, rawInput) { const match = ouStr.toLowerCase().match(/^([ou])(.+)$/); if (!match) { throw new InvalidLineValueError(rawInput, parseFloat(ouStr)); } const isOver = match[1] === "o"; const line = parseLine(match[2], rawInput); return { isOver, line }; } var PROP_TYPE_MAP = { // PropOU (Over/Under) - MUST have line "passing yards": { standardName: "PassingYards", category: "PropOU" }, passingyards: { standardName: "PassingYards", category: "PropOU" }, rbi: { standardName: "RBI", category: "PropOU" }, rbis: { standardName: "RBI", category: "PropOU" }, rebounds: { standardName: "Rebounds", category: "PropOU" }, rebs: { standardName: "Rebounds", category: "PropOU" }, "receiving yards": { standardName: "ReceivingYards", category: "PropOU" }, receivingyards: { standardName: "ReceivingYards", category: "PropOU" }, ks: { standardName: "Ks", category: "PropOU" }, strikeouts: { standardName: "Ks", category: "PropOU" }, // PropYN (Yes/No) - MAY NOT have line "first team to score": { standardName: "FirstToScore", category: "PropYN" }, "1st team to score": { standardName: "FirstToScore", category: "PropYN" }, "first to score": { standardName: "FirstToScore", category: "PropYN" }, "to score first": { standardName: "FirstToScore", category: "PropYN" }, "last team to score": { standardName: "LastToScore", category: "PropYN" }, "last to score": { standardName: "LastToScore", category: "PropYN" }, "to score last": { standardName: "LastToScore", category: "PropYN" } }; function detectPropType(propText) { const cleanText = propText.toLowerCase().trim(); for (const [keyword, info] of Object.entries(PROP_TYPE_MAP)) { const regex = new RegExp(`\\b${keyword.replace(/\s+/g, "\\s+")}\\b`); if (regex.test(cleanText)) { return info; } } return null; } function validatePropFormat(propText, hasLine, rawInput) { const propInfo = detectPropType(propText); if (!propInfo) { throw new InvalidContractTypeError(rawInput, `Unsupported prop type: ${propText}`); } if (propInfo.category === "PropOU" && !hasLine) { throw new InvalidContractTypeError( rawInput, `${propInfo.standardName} props require an over/under line (e.g., "o12.5")` ); } if (propInfo.category === "PropYN" && hasLine) { throw new InvalidContractTypeError( rawInput, `${propInfo.standardName} props cannot have a line - they are yes/no bets only` ); } } // src/parsers/index.ts function tokenizeChat(message) { const rawInput = message; const parts = message.trim().split(/\s+/); if (parts.length < 2) { throw new InvalidChatFormatError(rawInput, "Message too short"); } const prefix = parts[0].toUpperCase(); let chatType; if (prefix === "IW") { chatType = "order"; } else if (prefix === "YG") { chatType = "fill"; } else { throw new UnrecognizedChatPrefixError(rawInput, prefix); } let currentIndex = 1; let rotationNumber; let price; if (currentIndex < parts.length && /^\d+$/.test(parts[currentIndex])) { rotationNumber = parseRotationNumber(parts[currentIndex], rawInput); currentIndex++; } else if (currentIndex < parts.length && parts[currentIndex] === "abc") { throw new InvalidRotationNumberError(rawInput, parts[currentIndex]); } let priceIndex = -1; let sizeIndex = -1; let atSymbolCount = 0; for (let i = currentIndex; i < parts.length; i++) { if (parts[i] === "@") { atSymbolCount++; if (i + 1 < parts.length) { priceIndex = i + 1; } } if (parts[i] === "=" && i + 1 < parts.length) { sizeIndex = i + 1; } } if (atSymbolCount > 1) { if (chatType === "fill") { throw new InvalidChatFormatError( rawInput, 'Expected format for fills is: "YG" [rotation_number] contract ["@" usa_price] "=" fill_size' ); } else { throw new InvalidChatFormatError( rawInput, 'Expected format for orders is: "IW" [rotation_number] contract ["@" usa_price] ["=" unit_size]' ); } } for (let i = currentIndex; i < parts.length; i++) { if (parts[i] === "@") { if (i + 1 >= parts.length || parts[i + 1] === "" || parts[i + 1].trim() === "") { throw new InvalidChatFormatError(rawInput, "No contract details found"); } } } let contractEndIndex = parts.length; for (let i = currentIndex; i < parts.length; i++) { if (parts[i] === "@" || parts[i] === "=") { contractEndIndex = i; break; } } for (let i = currentIndex; i < contractEndIndex; i++) { if (/^[+-]\d+(?:\.\d+)?$/.test(parts[i])) { const value = parseFloat(parts[i].substring(1)); if (value === 0) { break; } else if (value > 50 && (value > 100 || value % 1 === 0)) { contractEndIndex = i; price = parsePrice(parts[i], rawInput); break; } } } if (contractEndIndex <= currentIndex) { throw new InvalidChatFormatError(rawInput, "No contract details found"); } const contractText = parts.slice(currentIndex, contractEndIndex).join(" "); if (price === void 0 && priceIndex > 0 && priceIndex < parts.length) { const priceStr = parts[priceIndex]; if (priceStr.toLowerCase().endsWith("k") || priceStr.startsWith("$")) { price = -110; if (sizeIndex === -1) { sizeIndex = priceIndex; } } else { price = parsePrice(priceStr, rawInput); } } let size; if (sizeIndex > 0 && sizeIndex < parts.length) { const sizeStr = parts[sizeIndex]; if (chatType === "order") { const parsed = parseOrderSize(sizeStr, rawInput); size = parsed.value; } else { const parsed = parseFillSize(sizeStr, rawInput); size = parsed.value; } } if (chatType === "fill" && size === void 0) { throw new MissingSizeForFillError(rawInput); } return { chatType, rotationNumber, contractText, price: price ?? -110, // Default price size, rawInput }; } function detectContractType(contractText, rawInput) { const text = contractText.toLowerCase().trim(); if (text.includes("series")) { return "Series"; } if (/\stt\s/i.test(contractText) || /\stt\s*[ou]/i.test(contractText) || /^tt\s/i.test(contractText)) { if (/^tt\s/i.test(contractText.trim())) { throw new InvalidTeamFormatError(rawInput, "", "Team name cannot be empty"); } return "TotalPointsContestant"; } const propInfo = detectPropType(text); if (propInfo) { const hasLine = /[ou]\d+(?:\.\d+)?/i.test(contractText); validatePropFormat(text, hasLine, rawInput); return propInfo.category; } if (/^[a-zA-Z0-9]+\s+[a-zA-Z\s]+(yards|rbi|rebounds|score|strikeouts|prop)/i.test(contractText)) { const hasLine = /[ou]\d+(?:\.\d+)?/i.test(contractText); validatePropFormat(text, hasLine, rawInput); } if (/[a-zA-Z]+(?:\s+[a-zA-Z0-9]+)*\s*[+-]\d+(?:\.\d+)?/i.test(contractText)) { const spreadMatch = contractText.match( /([a-zA-Z]+(?:\s+[a-zA-Z0-9]+)*)\s*([+-])(\d+(?:\.\d+)?)/i ); if (spreadMatch) { const value = parseFloat(spreadMatch[3]); if (value === 0) { return "HandicapContestantML"; } if (value <= 50 || value % 1 !== 0) { return "HandicapContestantLine"; } else if (value > 100 && value % 1 === 0) { return "HandicapContestantML"; } else { return "HandicapContestantLine"; } } } if (/\//.test(contractText) && /[ou]\d+(?:\.\d+)?(\s+runs)?/i.test(contractText)) { return "TotalPoints"; } if (/^[a-zA-Z\s&.-]+\s+(f5|f3|h1|1h|h2|2h|\d+(?:st|nd|rd|th)?\s*(?:inning|i|quarter|q|period|p))\s+[ou]\d+(?:\.\d+)?/i.test( contractText )) { return "TotalPoints"; } if (!text.includes("/") && !text.includes("o") && !text.includes("u") || /[a-zA-Z]+\s*[+-]0(?:\s|$)/i.test(contractText)) { return "HandicapContestantML"; } throw new InvalidContractTypeError(rawInput, contractText); } function parseGameTotal(contractText, rawInput, sport, league) { const ouMatch = contractText.match(/([ou])(\d+(?:\.\d+)?)(\s+runs)?/i); if (!ouMatch) { throw new InvalidContractTypeError(rawInput, contractText); } const { isOver, line } = parseOverUnder(ouMatch[1] + ouMatch[2], rawInput); const hasRunsSuffix = !!ouMatch[3]; const withoutOU = contractText.replace(/\s*[ou]\d+(?:\.\d+)?(\s+runs)?/i, "").trim(); const { period, match } = parseMatchInfo(withoutOU, rawInput, sport, league); let finalSport = sport; if ((hasRunsSuffix || period.PeriodTypeCode === "I") && !sport) { finalSport = "Baseball"; } return { Sport: finalSport, League: league, Match: match, Period: period, HasContestant: false, HasLine: true, ContractSportCompetitionMatchType: "TotalPoints", Line: line, IsOver: isOver }; } function parseTeamTotal(contractText, rawInput, sport, league) { const ouMatch = contractText.match(/([ou])(\d+(?:\.\d+)?)(\s+runs)?/i); if (!ouMatch) { throw new InvalidContractTypeError(rawInput, contractText); } const { isOver, line } = parseOverUnder(ouMatch[1] + ouMatch[2], rawInput); const hasRunsSuffix = !!ouMatch[3]; let finalSport = sport; if (hasRunsSuffix && !sport) { finalSport = "Baseball"; } const withoutOU = contractText.replace(/\s*[ou]\d+(?:\.\d+)?(\s+runs)?/i, "").replace(/\s*tt\s*/i, " ").trim(); const { teams, period, match } = parseMatchInfo(withoutOU, rawInput, finalSport, league); return { Sport: finalSport, League: league, Match: match, Period: period, HasContestant: true, HasLine: true, ContractSportCompetitionMatchType: "TotalPoints", Line: line, IsOver: isOver, Contestant: teams.team1 }; } function parseMoneyline(contractText, rawInput, sport, league) { const cleanedContractText = contractText.replace(/\s*[+-]0(?:\s|$)/i, "").trim(); const { teams, period, match } = parseMatchInfo(cleanedContractText, rawInput, sport, league); return { Sport: sport, League: league, Match: match, Period: period, HasContestant: true, HasLine: false, ContractSportCompetitionMatchType: "Handicap", Contestant: teams.team1, TiesLose: false // Default for MLB }; } function parseSpread(contractText, rawInput, sport, league) { const spreadMatch = contractText.match(/^(.*?)\s*([+-]\d+(?:\.\d+)?)$/); if (!spreadMatch) { throw new InvalidContractTypeError(rawInput, contractText); } const teamPart = spreadMatch[1].trim(); const lineStr = spreadMatch[2]; const sign = lineStr.startsWith("+") ? "+" : "-"; const lineValue = parseFloat(lineStr.substring(1)); const line = sign === "+" ? lineValue : -lineValue; const { teams, period, match } = parseMatchInfo(teamPart, rawInput, sport, league); return { Sport: sport, League: league, Match: match, Period: period, HasContestant: true, HasLine: true, ContractSportCompetitionMatchType: "Handicap", Contestant: teams.team1, Line: line }; } function parsePropOU(contractText, rawInput, sport, league) { const ouMatch = contractText.match(/([ou])(\d+(?:\.\d+)?)(\s+runs)?/i); if (!ouMatch) { throw new InvalidContractTypeError(rawInput, "PropOU requires an over/under line"); } const { isOver, line } = parseOverUnder(ouMatch[1] + ouMatch[2], rawInput); const hasRunsSuffix = !!ouMatch[3]; let finalSport = sport; if (hasRunsSuffix && !sport) { finalSport = "Baseball"; } const withoutOU = contractText.replace(/\s*[ou]\d+(?:\.\d+)?(\s+runs)?/i, "").trim(); const individualMatch = withoutOU.match(/^([A-Z]\.\s+[A-Za-z]+)\s+(.+)$/); let contestant; let propText; if (individualMatch) { contestant = individualMatch[1]; propText = individualMatch[2].toLowerCase(); } else { const parts = withoutOU.trim().split(/\s+/); if (parts.length < 2) { throw new InvalidContractTypeError(rawInput, contractText); } contestant = parts[0]; propText = parts.slice(1).join(" ").toLowerCase(); } const propInfo = detectPropType(propText); if (!propInfo || propInfo.category !== "PropOU") { throw new InvalidContractTypeError(rawInput, `Invalid PropOU type: ${propText}`); } const contestantType = detectContestantType(contestant); return { Sport: finalSport, League: league, Match: { Team1: contestant }, Period: { PeriodTypeCode: "M", PeriodNumber: 0 }, HasContestant: true, HasLine: true, ContractSportCompetitionMatchType: "Prop", ContestantType: contestantType, Prop: propInfo.standardName, Contestant: contestant, Line: line, IsOver: isOver }; } function parsePropYN(contractText, rawInput, sport, league) { const parts = contractText.trim().split(/\s+/); if (parts.length < 2) { throw new InvalidContractTypeError(rawInput, contractText); } const team = parts[0]; const propText = parts.slice(1).join(" ").toLowerCase(); const propInfo = detectPropType(propText); if (!propInfo || propInfo.category !== "PropYN") { throw new InvalidContractTypeError(rawInput, `Invalid PropYN type: ${propText}`); } const contestantType = detectContestantType(team); let isYes; if (propInfo.standardName === "FirstToScore") { isYes = true; } else if (propInfo.standardName === "LastToScore") { isYes = true; } else { isYes = true; } return { Sport: sport, League: league, Match: { Team1: team }, Period: { PeriodTypeCode: "M", PeriodNumber: 0 }, HasContestant: true, HasLine: false, ContractSportCompetitionMatchType: "Prop", ContestantType: contestantType, Prop: propInfo.standardName, Contestant: team, IsYes: isYes }; } function parseSeries(contractText, rawInput, sport, league) { const seriesSlashMatch = contractText.match(/series\/(\d+)/i); let seriesLength; if (seriesSlashMatch) { seriesLength = parseInt(seriesSlashMatch[1]); } else { const outOfMatch = contractText.match(/series\s+out\s+of\s+(\d+)/i); if (outOfMatch) { seriesLength = parseInt(outOfMatch[1]); } else { const lengthMatch = contractText.match(/(\d+)\s*game\s*series/i); if (lengthMatch) { seriesLength = parseInt(lengthMatch[1]); } else { const hyphenMatch = contractText.match(/(\d+)-game\s*series/i); if (hyphenMatch) { seriesLength = parseInt(hyphenMatch[1]); } else { seriesLength = 3; } } } } let teamMatch = contractText.match(/([a-zA-Z\s&.]+?)\s*\d+-game\s*series/i); if (!teamMatch) { teamMatch = contractText.match(/([a-zA-Z\s&.]+?)\s*series\/\d+/i); } if (!teamMatch) { teamMatch = contractText.match(/([a-zA-Z\s&.]+?)\s*(?:(?:\d+\s*game\s*)?series|series)/i); } if (!teamMatch) { throw new InvalidContractTypeError(rawInput, contractText); } const team = teamMatch[1].trim(); return { Sport: sport, League: league, Match: { Team1: team }, SeriesLength: seriesLength, Contestant: team }; } function parseMatchInfo(text, rawInput, _sport, _league) { let workingText = text.trim(); let daySequence; const gameMatch = workingText.match(/\s+(g(?:m)?\d+|#\d+)\s*/i); if (gameMatch) { daySequence = parseGameNumber(gameMatch[1], rawInput); workingText = workingText.replace(gameMatch[0], " ").trim(); } else { const invalidGameMatch = workingText.match(/\s+(g(?:m)?[a-zA-Z]+|#[a-zA-Z]+|g(?:m)?$|#$)\s*/i); if (invalidGameMatch) { throw new InvalidGameNumberError(rawInput, invalidGameMatch[1]); } } let period = { PeriodTypeCode: "M", PeriodNumber: 0 }; const periodPatterns = [ /\b(\d+(?:st|nd|rd|th)?\s*(?:inning|i))\b/i, /\b(f5|f3|h1|1h|h2|2h)\b/i, /\b(\d+(?:st|nd|rd|th)?\s*(?:quarter|q))\b/i, /\b(\d+(?:st|nd|rd|th)?\s*(?:period|p))\b/i, /\b(first\s*(?:half|five|5|inning|i))\b/i, /\b(second\s*(?:half|h))\b/i ]; for (const pattern of periodPatterns) { const match2 = workingText.match(pattern); if (match2) { period = parsePeriod(match2[1], rawInput); workingText = workingText.replace(match2[0], " ").trim(); break; } } const teams = parseTeams(workingText, rawInput); const match = { Team1: teams.team1, Team2: teams.team2, DaySequence: daySequence }; return { teams, period, match }; } function parseChatOrder(message) { const tokens = tokenizeChat(message); if (tokens.chatType !== "order") { throw new InvalidChatFormatError(tokens.rawInput, "Expected order (IW) message"); } const contractType = detectContractType(tokens.contractText, tokens.rawInput); const { sport, league } = inferSportAndLeague(tokens.rotationNumber); let contract; switch (contractType) { case "TotalPoints": contract = parseGameTotal(tokens.contractText, tokens.rawInput, sport, league); break; case "TotalPointsContestant": contract = parseTeamTotal(tokens.contractText, tokens.rawInput, sport, league); break; case "HandicapContestantML": contract = parseMoneyline(tokens.contractText, tokens.rawInput, sport, league); break; case "HandicapContestantLine": contract = parseSpread(tokens.contractText, tokens.rawInput, sport, league); break; case "PropOU": contract = parsePropOU(tokens.contractText, tokens.rawInput, sport, league); break; case "PropYN": contract = parsePropYN(tokens.contractText, tokens.rawInput, sport, league); break; case "Series": contract = parseSeries(tokens.contractText, tokens.rawInput, sport, league); break; default: throw new InvalidContractTypeError(tokens.rawInput, tokens.contractText); } if (tokens.rotationNumber && "RotationNumber" in contract) { contract.RotationNumber = tokens.rotationNumber; } return { chatType: "order", contractType, contract, rotationNumber: tokens.rotationNumber, bet: { Price: tokens.price, Size: tokens.size } }; } function parseChatFill(message) { const tokens = tokenizeChat(message); if (tokens.chatType !== "fill") { throw new InvalidChatFormatError(tokens.rawInput, "Expected fill (YG) message"); } const contractType = detectContractType(tokens.contractText, tokens.rawInput); const { sport, league } = inferSportAndLeague(tokens.rotationNumber); let contract; switch (contractType) { case "TotalPoints": contract = parseGameTotal(tokens.contractText, tokens.rawInput, sport, league); break; case "TotalPointsContestant": contract = parseTeamTotal(tokens.contractText, tokens.rawInput, sport, league); break; case "HandicapContestantML": contract = parseMoneyline(tokens.contractText, tokens.rawInput, sport, league); break; case "HandicapContestantLine": contract = parseSpread(tokens.contractText, tokens.rawInput, sport, league); break; case "PropOU": contract = parsePropOU(tokens.contractText, tokens.rawInput, sport, league); break; case "PropYN": contract = parsePropYN(tokens.contractText, tokens.rawInput, sport, league); break; case "Series": contract = parseSeries(tokens.contractText, tokens.rawInput, sport, league); break; default: throw new InvalidContractTypeError(tokens.rawInput, tokens.contractText); } if (tokens.rotationNumber && "RotationNumber" in contract) { contract.RotationNumber = tokens.rotationNumber; } return { chatType: "fill", contractType, contract, rotationNumber: tokens.rotationNumber, bet: { ExecutionDtm: /* @__PURE__ */ new Date(), // Current time for fills Price: tokens.price, Size: tokens.size } }; } function parseChat(message) { const trimmed = message.trim(); if (trimmed.toUpperCase().startsWith("IW")) { return parseChatOrder(message); } else if (trimmed.toUpperCase().startsWith("YG")) { return parseChatFill(message); } else { throw new UnrecognizedChatPrefixError(message, trimmed.split(/\s+/)[0] || ""); } } // src/types/index.ts function isOrder(result) { return result.chatType === "order"; } function isFill(result) { return result.chatType === "fill"; } function isTotalPoints(contract) { return "ContractSportCompetitionMatchType" in contract && contract.ContractSportCompetitionMatchType === "TotalPoints" && !contract.HasContestant; } function isTotalPointsContestant(contract) { return "ContractSportCompetitionMatchType" in contract && contract.ContractSportCompetitionMatchType === "TotalPoints" && contract.HasContestant; } function isHandicapML(contract) { return "ContractSportCompetitionMatchType" in contract && contract.ContractSportCompetitionMatchType === "Handicap" && contract.HasContestant && !contract.HasLine; } function isHandicapLine(contract) { return "ContractSportCompetitionMatchType" in contract && contract.ContractSportCompetitionMatchType === "Handicap" && contract.HasContestant && contract.HasLine; } function isPropOU(contract) { return "ContractSportCompetitionMatchType" in contract && contract.ContractSportCompetitionMatchType === "Prop" && contract.HasContestant && contract.HasLine; } function isPropYN(contract) { return "ContractSportCompetitionMatchType" in contract && contract.ContractSportCompetitionMatchType === "Prop" && contract.HasContestant && !contract.HasLine; } function isSeries(contract) { return "SeriesLength" in contract; } // src/grading/client.ts import * as sql from "mssql"; // src/grading/types.ts var GradingError = class extends Error { constructor(message, originalError) { super(message); this.originalError = originalError; this.name = "GradingError"; } }; var GradingConnectionError = class extends GradingError { constructor(message, originalError) { super(message, originalError); this.name = "GradingConnectionError"; } }; var GradingQueryError = class extends GradingError { constructor(message, originalError) { super(message, originalError); this.name = "GradingQueryError"; } }; var GradingDataError = class extends GradingError { constructor(message) { super(message); this.name = "GradingDataError"; } }; // src/grading/mappers.ts function mapParseResultToSqlParameters(result, options) { const contract = result.contract; let matchScheduledDate; if (options?.matchScheduledDate) { matchScheduledDate = options.matchScheduledDate; } else { throw new GradingDataError("MatchScheduledDate must be provided in options when grading"); } const matchInfo = extractMatchInfo(contract); const periodInfo = extractPeriodInfo(contract); const baseParams = { MatchScheduledDate: matchScheduledDate, Contestant1: matchInfo.Contestant1, Contestant2: matchInfo.Contestant2, DaySequence: matchInfo.DaySequence, PeriodTypeCode: periodInfo.PeriodTypeCode, PeriodNumber: periodInfo.PeriodNumber, TiesLose: false // Default assumption }; const { contractType } = result; let contractParams = {}; switch (contractType) { case "TotalPoints": contractParams = mapTotalPoints(contract); break; case "TotalPointsContestant": contractParams = mapTotalPointsContestant(contract); break; case "HandicapContestantML": contractParams = mapHandicapML(contract); break; case "HandicapContestantLine": contractParams = mapHandicapLine(contract); break; case "PropOU": contractParams = mapPropOU(contract); break; case "PropYN": contractParams = mapPropYN(contract); break; case "Series": contractParams = mapSeries(contract); break; default: throw new GradingDataError(`Unsupported contract type: ${contractType}`); } return { ...baseParams, ContractType: contractType, ...contractParams }; } function extractMatchInfo(contract) { return { Contestant1: contract.Match.Team1, Contestant2: contract.Match.Team2, DaySequence: contract.Match.DaySequence || void 0 }; } function extractPeriodInfo(contract) { if ("SeriesLength" in contract) { return { PeriodTypeCode: "M", // Default to match PeriodNumber: 0 }; } if ("Period" in contract) { const matchContract = contract; return { PeriodTypeCode: matchContract.Period.PeriodTypeCode, PeriodNumber: matchContract.Period.PeriodNumber }; } return { PeriodTypeCode: "FG", PeriodNumber: 1 }; } function mapTotalPoints(contract) { if (!("ContractSportCompetitionMatchType" in contract) || contract.ContractSportCompetitionMatchType !== "TotalPoints" || contract.HasContestant !== false) { throw new GradingDataError("Invalid TotalPoints contract structure"); } return { Line: contract.Line, IsOver: contract.IsOver }; } function mapTotalPointsContestant(contract) { if (!("ContractSportCompetitionMatchType" in contract) || contract.ContractSportCompetitionMatchType !== "TotalPoints" || contract.HasContestant !== true) { throw new GradingDataError("Invalid TotalPointsContestant contract structure"); } return { Line: contract.Line, IsOver: contract.IsOver, SelectedContestant: contract.Contestant }; } function mapHandicapML(contract) { if (!("ContractSportCompetitionMatchType" in contract) || contract.ContractSportCompetitionMatchType !== "Handicap" || contract.HasContestant !== true || contract.HasLine !== false) { throw new GradingDataError("Invalid HandicapContestantML contract structure"); } return { SelectedContestant: contract.Contestant, TiesLose: contract.TiesLose }; } function mapHandicapLine(contract) { if (!("ContractSportCompetitionMatchType" in contract) || contract.ContractSportCompetitionMatchType !== "Handicap" || contract.HasContestant !== true || contract.HasLine !== true) { throw new GradingDataError("Invalid HandicapContestantLine contract structure"); } return { SelectedContestant: contract.Contestant, Line: contract.Line }; } function mapPropOU(contract) { if (!("ContractSportCompetitionMatchType" in contract) || contract.ContractSportCompetitionMatchType !== "Prop" || contract.HasContestant !== true || contract.HasLine !== true) { throw new GradingDataError("Invalid PropOU contract structure"); } return { SelectedContestant: contract.Contestant, Line: contract.Line, IsOver: contract.IsOver, Prop: contract.Prop, PropContestantType: contract.ContestantType }; } function mapPropYN(contract) { if (!("ContractSportCompetitionMatchType" in contract) || contract.ContractSportCompetitionMatchType !== "Prop" || contract.HasContestant !== true || contract.HasLine !== false) { throw new GradingDataError("Invalid PropYN contract structure"); } return { SelectedContestant: contract.Contestant, IsYes: contract.IsYes, Prop: contract.Prop, PropContestantType: contract.ContestantType }; } function mapSeries(contract) { if (!("SeriesLength" in contract)) { throw new GradingDataError("Invalid Series contract structure"); } const seriesContract = contract; return { SeriesLength: seriesContract.SeriesLength, SelectedContestant: seriesContract.Contestant }; } function validateGradingParameters(params) { if (!params.MatchScheduledDate) { throw new GradingDataError("MatchScheduledDate is required"); } if (!params.Contestant1) { throw new GradingDataError("Contestant1 is required"); } if (!params.ContractType) { throw new GradingDataError("ContractType is required"); } switch (params.ContractType) { case "TotalPoints": case "TotalPointsContestant": if (params.Line === void 0 || params.IsOver === void 0) { throw new GradingDataError(`${params.ContractType} requires Line and IsOver`); } if (params.ContractType === "TotalPointsContestant" && !params.SelectedContestant) { throw new GradingDataError("TotalPointsContestant requires SelectedContestant"); } break; case "HandicapContestantML": if (!params.SelectedContestant) { throw new GradingDataError("HandicapContestantML requires SelectedContestant"); } break; case "HandicapContestantLine": if (!params.SelectedContestant || params.Line === void 0) { throw new GradingDataError("HandicapContestantLine requires SelectedContestant and Line"); } break; case "PropOU": if (!params.SelectedContestant || params.Line === void 0 || params.IsOver === void 0 || !params.Prop) { throw new GradingDataError("PropOU requires SelectedContestant, Line, IsOver, and Prop"); } break; case "PropYN": if (!params.SelectedContestant || params.IsYes === void 0 || !params.Prop) { throw new GradingDataError("PropYN requires SelectedContestant, IsYes, and Prop"); } break; case "Series": if (!params.SelectedContestant || !params.SeriesLength) { throw new GradingDataError("Series requires SelectedContestant and SeriesLength"); } break; default: throw new GradingDataError(`Unknown contract type: ${params.ContractType}`); } } // src/grading/client.ts var ChatBetGradingClient = class { constructor(config) { this.pool = null; this.isPoolConnected = false; if (typeof config === "string") { this.connectionString = config; } else { this.connectionString = config.connectionString; } } /** * Test the database connection * This will be called automatically on first use, but can be called explicitly */ async testConnection() { try { if (!this.pool) { this.pool = new sql.ConnectionPool(this.connectionString); } if (!this.isPoolConnected) { await this.pool.connect(); this.isPoolConnected = true; } const request = this.pool.request(); await request.query("SELECT 1 as test"); } catch (error) { this.isPoolConnected = false; const message = error instanceof Error ? error.message : "Unknown connection error"; throw new GradingConnectionError( `Failed to connect to SQL Server: ${message}`, error instanceof Error ? error : void 0 ); } } /** * Get connection status */ isConnected() { return this.isPoolConnected && this.pool?.connected === true; } /** * Grade a parsed chat result */ async grade(result, options) { try { if (!this.isConnected()) { await this.testConnection(); } const sqlParams = mapParseResultToSqlParameters(result, options); validateGradingParameters(sqlParams); const grade = await this.executeGradingFunction(sqlParams); return grade; } catch (error) { if (error instanceof GradingError) { throw error; } const message = error instanceof Error ? error.message : "Unknown grading error"; throw new GradingQueryError( `Failed to grade contract: ${message}`, error instanceof Error ? error : void 0 ); } } /** * Close the database connection and clean up resources */ async close() { try { if (this.pool && this.isPoolConnected) { await this.pool.close(); this.isPoolConnected = false; } } catch (error) { console.warn("Warning: Error closing database connection:", error); } } // ============================================================================== // PRIVATE METHODS // ============================================================================== /** * Execute the SQL Server grading function with parameters */ async executeGradingFunction(params) { if (!this.pool) { throw new GradingConnectionError("Database connection not established"); } try { const request = this.pool.request(); request.input("MatchScheduledDate", sql.Date, params.MatchScheduledDate); request.input("Contestant1", sql.Char(50), params.Contestant1); request.input("Contestant2", sql.Char(50), params.Contestant2 ?? null); request.input("DaySequence", sql.TinyInt, params.DaySequence ?? null); request.input("MatchContestantType", sql.Char(10), params.MatchContestantType ?? null); request.input("PeriodTypeCode", sql.Char(2), params.PeriodTypeCode); request.input("PeriodNumber", sql.TinyInt, params.PeriodNumber); request.input("ContractType", sql.VarChar(30), params.ContractType); request.input("Line", sql.Decimal(5, 2), params.Line ?? null); request.input("IsOver", sql.Bit, params.IsOver ?? null); request.input("SelectedContestant", sql.Char(50), params.SelectedContestant ?? null); request.input("TiesLose", sql.Bit, params.TiesLose ?? false); request.input("Prop", sql.VarChar(20), params.Prop ?? null); request.input("PropContestantType", sql.Char(10), params.PropContestantType ?? null); request.input("IsYes", sql.Bit, params.IsYes ?? null); request.input("SeriesLength", sql.TinyInt, params.SeriesLength ?? null); const result = await request.query(` SELECT dbo.Contract_CALCULATE_Grade_fn( @MatchScheduledDate, @Contestant1, @Contestant2, @DaySequence, @MatchContestantType, @PeriodTypeCode, @PeriodNumber, @ContractType, @Line, @IsOver, @SelectedContestant, @TiesLose, @Prop, @PropContestantType, @IsYes, @SeriesLength ) as Grade `); if (!result.recordset || result.recordset.length === 0) { throw new GradingQueryError("No result returned from grading function"); } const grade = result.recordset[0].Grade; if (!["W", "L", "P", "?"].includes(grade)) { throw new GradingQueryError(`Invalid grade result: ${grade}`); } return grade; } catch (error) { if (error instanceof GradingError) { throw error; } let message = error instanceof Error ? error.message : `Unknown SQL execution error: ${error}`; const conversionErrorPattern = /^Conversion failed when converting the (?:nvarchar|varchar) value '(.+?)' to data type tinyint\.?$/; const match = message.match(conversionErrorPattern); if (match) { message = match[1].trim(); } throw new GradingQueryError( `SQL execution failed: ${message}`, error instanceof Error ? error : void 0 ); } } }; function createGradingClient(connectionString) { return new ChatBetGradingClient(connectionString); } function createGradingClientWithConfig(config) { return new ChatBetGradingClient(config); } export { AmbiguousContractError, ChatBetGradingClient, ChatBetParseError, GradingConnectionError, GradingDataError, GradingError, GradingQueryError, InvalidChatFormatError, InvalidContractTypeError, InvalidGameNumberError, InvalidLineValueError, InvalidPeriodFormatError, InvalidPriceFormatError, InvalidPropFormatError, InvalidRotationNumberError, InvalidSeriesLengthError, InvalidSizeFormatError, InvalidTeamFormatError, MissingSizeForFillError, UnrecognizedChatPrefixError, createGradingClient, createGradingClientWithConfig, createPositionError, parseChat as default, detectContestantType, detectPropType, inferSportAndLeague, isFill, isHandicapLine, isHandicapML, isOrder, isPropOU, isPropYN, isSeries, isTotalPoints, isTotalPointsContestant, mapParseResultToSqlParameters, matchesIgnoreCase, parseChat, parseChatFill, parseChatOrder, parseFillSize, parseGameNumber, parseLine, parseOrderSize, parseOverUnder, parsePeriod, parsePrice, parseRotationNumber, parseTeam, parseTeams, validateGradingParameters, validatePropFormat };