UNPKG

chat-bet-parse

Version:

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

1,485 lines (1,477 loc) 64.2 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, InvalidWriteinDateError: () => InvalidWriteinDateError, InvalidWriteinDescriptionError: () => InvalidWriteinDescriptionError, InvalidWriteinFormatError: () => InvalidWriteinFormatError, 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, isWritein: () => isWritein, 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, parseWriteinDate: () => parseWriteinDate, validateGradingParameters: () => validateGradingParameters, validatePropFormat: () => validatePropFormat, validateWriteinDescription: () => validateWriteinDescription }); 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"; } }; var InvalidWriteinDateError = class extends ChatBetParseError { constructor(rawInput, dateStr, reason) { super( `Invalid writein date: "${dateStr}". ${reason}. Expected formats: YYYY-MM-DD, MM/DD/YYYY, YYYY/MM/DD, MM-DD-YYYY, or equivalents without year. Input: "${rawInput}"`, rawInput ); this.name = "InvalidWriteinDateError"; } }; var InvalidWriteinDescriptionError = class extends ChatBetParseError { constructor(rawInput, description, reason) { super( `Invalid writein description: "${description}". ${reason}. Input: "${rawInput}"`, rawInput ); this.name = "InvalidWriteinDescriptionError"; } }; var InvalidWriteinFormatError = class extends ChatBetParseError { constructor(rawInput, reason) { super( `Invalid writein format: ${reason}. Expected format: "IW/YG writein DATE DESCRIPTION [@ price] [= size]". Input: "${rawInput}"`, rawInput ); this.name = "InvalidWriteinFormatError"; } }; 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 === "f7") { return { PeriodTypeCode: "H", PeriodNumber: 17 }; } 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)?\s*(\d+)|#\s*(\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) { const team1 = parseTeam(parts[0], rawInput); const team2 = parseTeam(parts[1], rawInput); if (team1 === team2) { throw new InvalidTeamFormatError( rawInput, teamsStr, `Team1 and Team2 cannot be the same: "${team1}"` ); } return { team1, team2 }; } 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 lineAndPrice = match[2]; const attachedPriceMatch = lineAndPrice.match(/^(\d+(?:\.\d+)?)([+-]\d+(?:\.\d+)?)$/); if (attachedPriceMatch) { const line2 = parseLine(attachedPriceMatch[1], rawInput); const priceStr = attachedPriceMatch[2]; const attachedPrice = parsePrice(priceStr, rawInput); return { isOver, line: line2, attachedPrice }; } const line = parseLine(lineAndPrice, 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` ); } } function parseWriteinDate(dateString, rawInput) { const cleaned = dateString.trim(); if (!cleaned) { throw new InvalidWriteinDateError(rawInput, dateString, "Date cannot be empty"); } let parsedDate = null; const currentYear = (/* @__PURE__ */ new Date()).getFullYear(); const today = /* @__PURE__ */ new Date(); const patterns = [ // Full date patterns with year /^(\d{4})[/-](\d{1,2})[/-](\d{1,2})$/, // YYYY/MM/DD or YYYY-MM-DD /^(\d{1,2})[/-](\d{1,2})[/-](\d{4})$/, // MM/DD/YYYY or MM-DD-YYYY // Date patterns without year /^(\d{1,2})[/-](\d{1,2})$/ // MM/DD or MM-DD ]; for (const pattern of patterns) { const match = cleaned.match(pattern); if (match) { let year, month, day; if (match.length === 4) { if (pattern.source.startsWith("^(\\d{4})")) { year = parseInt(match[1]); month = parseInt(match[2]); day = parseInt(match[3]); } else { month = parseInt(match[1]); day = parseInt(match[2]); year = parseInt(match[3]); } } else { month = parseInt(match[1]); day = parseInt(match[2]); const dateThisYear = new Date(currentYear, month - 1, day); if (dateThisYear >= today) { year = currentYear; } else { year = currentYear + 1; } } const testDate = new Date(year, month - 1, day); if (testDate.getFullYear() === year && testDate.getMonth() === month - 1 && testDate.getDate() === day) { parsedDate = testDate; break; } else { throw new InvalidWriteinDateError( rawInput, dateString, `Invalid calendar date (e.g., February 30th doesn't exist)` ); } } } if (!parsedDate) { throw new InvalidWriteinDateError( rawInput, dateString, "Unable to parse date. Supported formats: YYYY-MM-DD, MM/DD/YYYY, YYYY/MM/DD, MM-DD-YYYY, MM/DD, MM-DD" ); } return parsedDate; } function validateWriteinDescription(description, rawInput) { const trimmed = description.trim(); if (!trimmed) { throw new InvalidWriteinDescriptionError(rawInput, description, "Description cannot be empty"); } if (trimmed.length < 10) { throw new InvalidWriteinDescriptionError( rawInput, description, `Description must be at least 10 characters long (currently ${trimmed.length})` ); } if (trimmed.length > 255) { throw new InvalidWriteinDescriptionError( rawInput, description, `Description cannot exceed 255 characters (currently ${trimmed.length})` ); } if (trimmed.includes("\n") || trimmed.includes("\r")) { throw new InvalidWriteinDescriptionError( rawInput, description, "Description cannot contain newlines" ); } return trimmed; } // src/parsers/index.ts function isWriteinTokens(tokens) { return "isWritein" in tokens; } function tokenizeWritein(parts, chatType, rawInput) { if (parts.length < 4) { throw new InvalidWriteinFormatError( rawInput, "Writein contracts require at least a date and description" ); } if (parts[1].toLowerCase() !== "writein") { throw new InvalidWriteinFormatError( rawInput, "Writein must be separated from date by whitespace" ); } let currentIndex = 2; const dateString = parts[currentIndex]; currentIndex++; let priceIndex = -1; let sizeIndex = -1; for (let i = currentIndex; i < parts.length; i++) { if (parts[i] === "@" && i + 1 < parts.length) { priceIndex = i + 1; } if (parts[i] === "=" && i + 1 < parts.length) { sizeIndex = i + 1; } } let descriptionEndIndex = parts.length; for (let i = currentIndex; i < parts.length; i++) { if (parts[i] === "@" || parts[i] === "=") { descriptionEndIndex = i; break; } } if (descriptionEndIndex <= currentIndex) { throw new InvalidWriteinFormatError(rawInput, "Writein contracts must include a description"); } const description = parts.slice(currentIndex, descriptionEndIndex).join(" "); let price; if (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, isWritein: true, dateString, description, price: price ?? -110, // Default price size, rawInput }; } function tokenizeChat(message) { const rawInput = message; let processedMessage = message.trim(); if (processedMessage.toUpperCase().startsWith("IWW ")) { processedMessage = "IW writein " + processedMessage.substring(4); } else if (processedMessage.toUpperCase().startsWith("YGW ")) { processedMessage = "YG writein " + processedMessage.substring(4); } processedMessage = processedMessage.replace(/([^=\s])=([^=\s])/g, "$1 = $2"); processedMessage = processedMessage.replace(/([^=\s])=(\s)/g, "$1 = $2"); processedMessage = processedMessage.replace(/(\s)=([^=\s])/g, "$1 = $2"); const parts = processedMessage.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); } if (parts.length >= 2 && parts[1].toLowerCase() === "writein") { return tokenizeWritein(parts, chatType, rawInput); } 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"); } let contractText = parts.slice(currentIndex, contractEndIndex).join(" "); let gameNumber; const gameNumberAtBeginningMatch = contractText.match(/^(g(?:m)?\s*\d+|#\s*\d+)\s+(.+)$/i); if (gameNumberAtBeginningMatch) { const gameNumberStr = gameNumberAtBeginningMatch[1]; const remainingContractText = gameNumberAtBeginningMatch[2]; try { gameNumber = parseGameNumber(gameNumberStr, rawInput); contractText = remainingContractText; } catch (error) { } } if (price === void 0) { const attachedPriceMatch = contractText.match(/([ou])(\d+(?:\.\d+)?)([+-]\d+(?:\.\d+)?)/i); if (attachedPriceMatch) { const attachedPriceStr = attachedPriceMatch[3]; price = parsePrice(attachedPriceStr, rawInput); contractText = contractText.replace( attachedPriceMatch[0], attachedPriceMatch[1] + attachedPriceMatch[2] ); } } 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, gameNumber, 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]\d+(?:\.\d+)?(?:[+-]\d+(?:\.\d+)?)?/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+)?(?:[+-]\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+)?(?:[+-]\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+)?(?:[+-]\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+)?(?:[+-]\d+(?:\.\d+)?)?/i.test( contractText )) { return "TotalPoints"; } if (!contractText.includes("/") && !/\s[ou]\d/i.test(contractText) && !/^[ou]\d/i.test(contractText) || /[a-zA-Z]+\s*[+-]0(?:\s|$)/i.test(contractText) || /\sml\s*$/i.test(contractText) || /\sml\s+/i.test(contractText) || /^[a-zA-Z]+\s+(f5|f3|h1|1h|h2|2h|\d+(?:st|nd|rd|th)?\s*(?:inning|i|quarter|q|period|p))\s*$/i.test( contractText )) { return "HandicapContestantML"; } throw new InvalidContractTypeError(rawInput, contractText); } function parseGameTotal(contractText, rawInput, sport, league, gameNumber) { 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+)?(?:[+-]\d+(?:\.\d+)?)?(\s+runs)?/i, "").trim(); const { period, match } = parseMatchInfo(withoutOU, rawInput, sport, league, gameNumber); 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, gameNumber) { 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+)?(?:[+-]\d+(?:\.\d+)?)?(\s+runs)?/i, "").replace(/\s*tt\s*/i, " ").trim(); const { teams, period, match } = parseMatchInfo( withoutOU, rawInput, finalSport, league, gameNumber ); 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, gameNumber) { const cleanedContractText = contractText.replace(/\s*[+-]0(?:\s|$)/i, "").replace(/\s+ml\s*$/i, "").replace(/\s+ml\s+/i, " ").trim(); const { teams, period, match } = parseMatchInfo( cleanedContractText, rawInput, sport, league, gameNumber ); 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, gameNumber) { 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, gameNumber); 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, gameNumber) { 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+)?(?:[+-]\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, DaySequence: gameNumber }, 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, gameNumber) { const propInfo = detectPropType(contractText.toLowerCase()); if (!propInfo || propInfo.category !== "PropYN") { throw new InvalidContractTypeError(rawInput, `Invalid PropYN type: ${contractText}`); } const propPatterns = [ /\s+(1st team to score|first team to score|to score first|first to score)$/i, /\s+(last team to score|to score last|last to score)$/i ]; let teamAndGameInfo = contractText; for (const pattern of propPatterns) { if (pattern.test(contractText)) { teamAndGameInfo = contractText.replace(pattern, "").trim(); break; } } if (!teamAndGameInfo) { throw new InvalidContractTypeError(rawInput, contractText); } const { teams, match } = parseMatchInfo(teamAndGameInfo, rawInput, sport, league, gameNumber); const contestantType = detectContestantType(teams.team1); let isYes; if (propInfo.standardName === "FirstToScore") { isYes = true; } else if (propInfo.standardName === "LastToScore") { isYes = true; } else { isYes = true; } return { Sport: sport, League: league, Match: match, Period: { PeriodTypeCode: "M", PeriodNumber: 0 }, HasContestant: true, HasLine: false, ContractSportCompetitionMatchType: "Prop", ContestantType: contestantType, Prop: propInfo.standardName, Contestant: teams.team1, IsYes: isYes }; } function parseSeries(contractText, rawInput, sport, league, gameNumber) { 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, DaySequence: gameNumber }, SeriesLength: seriesLength, Contestant: team }; } function parseWritein(dateString, description, rawInput) { const eventDate = parseWriteinDate(dateString, rawInput); const validatedDescription = validateWriteinDescription(description, rawInput); return { EventDate: eventDate, Description: validatedDescription }; } function parseMatchInfo(text, rawInput, _sport, _league, gameNumberFromTokens) { let workingText = text.trim(); let daySequence = gameNumberFromTokens; if (daySequence === void 0) { const gameMatch = workingText.match(/\s+(g(?:m)?\s*\d+|#\s*\d+)\s*/i); if (gameMatch) { daySequence = parseGameNumber(gameMatch[1], rawInput); workingText = workingText.replace(gameMatch[0], " ").trim(); } else { const invalidGameMatch = workingText.match( /\s+(g(?:m)?\s*[a-zA-Z]+|#\s*[a-zA-Z]+|g(?:m)?\s*$|#\s*$)\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|f7|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"); } if (isWriteinTokens(tokens)) { const contract2 = parseWritein(tokens.dateString, tokens.description, tokens.rawInput); return { chatType: "order", contractType: "Writein", contract: contract2, rotationNumber: void 0, bet: { Price: tokens.price, Size: tokens.size } }; } 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, tokens.gameNumber ); break; case "TotalPointsContestant": contract = parseTeamTotal( tokens.contractText, tokens.rawInput, sport, league, tokens.gameNumber ); break; case "HandicapContestantML": contract = parseMoneyline( tokens.contractText, tokens.rawInput, sport, league, tokens.gameNumber ); break; case "HandicapContestantLine": contract = parseSpread( tokens.contractText, tokens.rawInput, sport, league, tokens.gameNumber ); break; case "PropOU": contract = parsePropOU( tokens.contractText, tokens.rawInput, sport, league, tokens.gameNumber ); break; case "PropYN": contract = parsePropYN( tokens.contractText, tokens.rawInput, sport, league, tokens.gameNumber ); break; case "Series": contract = parseSeries( tokens.contractText, tokens.rawInput, sport, league, tokens.gameNumber ); break; case "Writein": throw new InvalidContractTypeError( tokens.rawInput, "Writein contracts should have been handled earlier" ); 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"); } if (isWriteinTokens(tokens)) { const contract2 = parseWritein(tokens.dateString, tokens.description, tokens.rawInput); return { chatType: "fill", contractType: "Writein", contract: contract2, rotationNumber: void 0, bet: { ExecutionDtm: /* @__PURE__ */ new Date(), // Current time for fills Price: tokens.price, Size: tokens.size } }; } 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, tokens.gameNumber ); break; case "TotalPointsContestant": contract = parseTeamTotal( tokens.contractText, tokens.rawInput, sport, league, tokens.gameNumber ); break; case "HandicapContestantML": contract = parseMoneyline( tokens.contractText, tokens.rawInput, sport, league, tokens.gameNumber ); break; case "HandicapContestantLine": contract = parseSpread( tokens.contractText, tokens.rawInput, sport, league, tokens.gameNumber ); break; case "PropOU": contract = parsePropOU( tokens.contractText, tokens.rawInput, sport, league, tokens.gameNumber ); break; case "PropYN": contract = parsePropYN( tokens.contractText, tokens.rawInput, sport, league, tokens.gameNumber ); break; case "Series": contract = parseSeries( tokens.contractText, tokens.rawInput, sport, league, tokens.gameNumber ); break; case "Writein": throw new InvalidContractTypeError( tokens.rawInput, "Writein contracts should have been handled earlier" ); 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(); const upperTrimmed = trimmed.toUpperCase(); if (upperTrimmed.startsWith("IW") || upperTrimmed.startsWith("IWW")) { return parseChatOrder(message); } else if (upperTrimmed.startsWith("YG") || upperTrimmed.startsWith("YGW")) { 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; } function isWritein(contract) { return "EventDate" in contract && "Description" 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 contractType = result.contractType; const matchInfo = contractType !== "Writein" ? extractMatchInfo(contract) : null; const periodInfo = extractPeriodInfo(c