UNPKG

chat-bet-parse

Version:

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

1,416 lines (1,407 loc) 51.1 kB
"use strict"; var __create = Object.create; var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __getProtoOf = Object.getPrototypeOf; var __hasOwnProp = Object.prototype.hasOwnProperty; var __export = (target, all) => { for (var name in all) __defProp(target, name, { get: all[name], enumerable: true }); }; var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") { for (let key of __getOwnPropNames(from)) if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } return to; }; var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps( // If the importer is in node compatibility mode or this is not an ESM // file that has been converted to a CommonJS file using a Babel- // compatible transform (i.e. "__esModule" has not been set), then set // "default" to the CommonJS "module.exports" for node compatibility. isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target, mod )); var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); // src/index.ts var index_exports = {}; __export(index_exports, { AmbiguousContractError: () => AmbiguousContractError, ChatBetGradingClient: () => ChatBetGradingClient, ChatBetParseError: () => ChatBetParseError, GradingConnectionError: () => GradingConnectionError, GradingDataError: () => GradingDataError, GradingError: () => GradingError, GradingQueryError: () => GradingQueryError, InvalidChatFormatError: () => InvalidChatFormatError, InvalidContractTypeError: () => InvalidContractTypeError, InvalidGameNumberError: () => InvalidGameNumberError, InvalidLineValueError: () => InvalidLineValueError, InvalidPeriodFormatError: () => InvalidPeriodFormatError, InvalidPriceFormatError: () => InvalidPriceFormatError, InvalidPropFormatError: () => InvalidPropFormatError, InvalidRotationNumberError: () => InvalidRotationNumberError, InvalidSeriesLengthError: () => InvalidSeriesLengthError, InvalidSizeFormatError: () => InvalidSizeFormatError, InvalidTeamFormatError: () => InvalidTeamFormatError, MissingSizeForFillError: () => MissingSizeForFillError, UnrecognizedChatPrefixError: () => UnrecognizedChatPrefixError, createGradingClient: () => createGradingClient, createGradingClientWithConfig: () => createGradingClientWithConfig, createPositionError: () => createPositionError, default: () => parseChat, detectContestantType: () => detectContestantType, detectPropType: () => detectPropType, inferSportAndLeague: () => inferSportAndLeague, isFill: () => isFill, isHandicapLine: () => isHandicapLine, isHandicapML: () => isHandicapML, isOrder: () => isOrder, isPropOU: () => isPropOU, isPropYN: () => isPropYN, isSeries: () => isSeries, isTotalPoints: () => isTotalPoints, isTotalPointsContestant: () => isTotalPointsContestant, mapParseResultToSqlParameters: () => mapParseResultToSqlParameters, matchesIgnoreCase: () => matchesIgnoreCase, parseChat: () => parseChat, parseChatFill: () => parseChatFill, parseChatOrder: () => parseChatOrder, parseFillSize: () => parseFillSize, parseGameNumber: () => parseGameNumber, parseLine: () => parseLine, parseOrderSize: () => parseOrderSize, parseOverUnder: () => parseOverUnder, parsePeriod: () => parsePeriod, parsePrice: () => parsePrice, parseRotationNumber: () => parseRotationNumber, parseTeam: () => parseTeam, parseTeams: () => parseTeams, validateGradingParameters: () => validateGradingParameters, validatePropFormat: () => validatePropFormat }); module.exports = __toCommonJS(index_exports); // 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 var sql = __toESM(require("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); } // Annotate the CommonJS export names for ESM import in node: 0 && (module.exports = { AmbiguousContractError, ChatBetGradingCli