ed6502a880
Three submission methods: - POST /api/bets/quickslip — structured bet entry - POST /api/bets/screenshot — stub OCR with confirm flow - POST /api/bets/sync — coming soon stub Full bet lifecycle: - PATCH /api/bets/:id/settle — settle with outcome, recalculates performance - GET /api/bets — list with status/book/pagination filters - GET /api/bets/performance — ROI, win rate, profit (weekly/monthly/all_time) Payout calculator handles straight bets (American odds) and parlays (multiplied leg payouts). Performance service recalculates on each settlement and upserts into performance table. 33 new tests, 221 total (194 Node.js + 27 Python), all passing. All backend features for Phase 1 + Phase 2 now complete. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
309 lines
10 KiB
Markdown
309 lines
10 KiB
Markdown
# Feature 1.5 — Bet Submission
|
||
|
||
## Overview
|
||
Three ways for users to log their bets: quick slip (structured form), screenshot upload (OCR extraction), and sportsbook sync (stub for future OAuth integration). Every bet writes to the `bets` table, optionally links to a `scan_session`, and can be settled later with an outcome. Settled bets feed the `performance` table for ROI/win-rate tracking.
|
||
|
||
## Dependencies
|
||
- Feature 1.4 — Database Schema (`bets`, `outcomes`, `performance` tables)
|
||
- Feature 2.1 — Parlay Scan (`scan_sessions` for linking)
|
||
|
||
## Auth
|
||
All endpoints require Supabase JWT (same `requireAuth` middleware from Feature 2.1). All tiers can submit bets — no tier gating.
|
||
|
||
## Endpoints
|
||
|
||
### POST /api/bets/quickslip
|
||
Structured bet submission. User fills a form or sends structured JSON.
|
||
|
||
**Request body:**
|
||
```json
|
||
{
|
||
"legs": [
|
||
{
|
||
"player": "Nikola Jokic",
|
||
"stat_type": "points",
|
||
"line": 26.5,
|
||
"direction": "over",
|
||
"odds": -110
|
||
}
|
||
],
|
||
"amount": 20.00,
|
||
"book": "draftkings",
|
||
"bet_type": "straight",
|
||
"scan_session_id": "uuid-or-null",
|
||
"placed_at": "2026-03-21T18:00:00Z"
|
||
}
|
||
```
|
||
|
||
**Response (201):**
|
||
```json
|
||
{
|
||
"bet_id": "uuid",
|
||
"status": "pending",
|
||
"amount": 20.00,
|
||
"potential_payout": 38.18,
|
||
"bet_type": "straight",
|
||
"book": "draftkings",
|
||
"legs": 1,
|
||
"scan_session_id": "uuid-or-null",
|
||
"created_at": "2026-03-21T18:05:00Z"
|
||
}
|
||
```
|
||
|
||
### POST /api/bets/screenshot
|
||
Image upload. Server extracts bet data via OCR, then creates the bet.
|
||
|
||
**Request:** `multipart/form-data`
|
||
| Field | Type | Required | Description |
|
||
|---|---|---|---|
|
||
| image | file | yes | Screenshot image (JPEG, PNG, max 5MB) |
|
||
| book | string | yes | Sportsbook the screenshot is from |
|
||
| scan_session_id | string | no | Link to a prior scan |
|
||
|
||
**Response (201):**
|
||
```json
|
||
{
|
||
"bet_id": "uuid",
|
||
"status": "pending",
|
||
"extracted": {
|
||
"legs": [
|
||
{ "player": "Nikola Jokic", "stat_type": "points", "line": 26.5, "direction": "over", "odds": -110 }
|
||
],
|
||
"amount": 20.00,
|
||
"potential_payout": 38.18,
|
||
"bet_type": "parlay",
|
||
"confidence": 0.85
|
||
},
|
||
"needs_confirmation": true,
|
||
"message": "We extracted this from your screenshot. Confirm or edit before saving."
|
||
}
|
||
```
|
||
|
||
**Note:** For MVP, the OCR pipeline uses a stub that returns a structured placeholder. Full OCR (Tesseract or Claude Vision) is a Phase 3 enhancement. The endpoint accepts the image, stores the raw file reference, and returns extracted data for user confirmation.
|
||
|
||
### POST /api/bets/screenshot/confirm
|
||
User confirms or edits the OCR extraction, then saves the bet.
|
||
|
||
**Request body:**
|
||
```json
|
||
{
|
||
"extracted_bet_id": "uuid",
|
||
"legs": [
|
||
{ "player": "Nikola Jokic", "stat_type": "points", "line": 26.5, "direction": "over", "odds": -110 }
|
||
],
|
||
"amount": 20.00,
|
||
"book": "draftkings",
|
||
"bet_type": "parlay",
|
||
"scan_session_id": "uuid-or-null"
|
||
}
|
||
```
|
||
|
||
**Response (201):** Same shape as quickslip response.
|
||
|
||
### POST /api/bets/sync
|
||
Stub for future sportsbook OAuth sync. Returns a "coming soon" response.
|
||
|
||
**Response (200):**
|
||
```json
|
||
{
|
||
"status": "coming_soon",
|
||
"message": "Sportsbook sync is coming soon. Use quick slip or screenshot for now.",
|
||
"supported_books": ["draftkings", "fanduel", "betmgm"]
|
||
}
|
||
```
|
||
|
||
### PATCH /api/bets/:id/settle
|
||
Settle a bet with outcome. Updates the bet status and creates an outcome record for each leg.
|
||
|
||
**Request body:**
|
||
```json
|
||
{
|
||
"status": "won",
|
||
"leg_outcomes": [
|
||
{ "player": "Nikola Jokic", "stat_type": "points", "actual_value": 28.0, "result": "hit" }
|
||
]
|
||
}
|
||
```
|
||
|
||
**Response (200):**
|
||
```json
|
||
{
|
||
"bet_id": "uuid",
|
||
"status": "won",
|
||
"settled_at": "2026-03-22T06:00:00Z",
|
||
"amount": 20.00,
|
||
"potential_payout": 38.18,
|
||
"profit": 18.18
|
||
}
|
||
```
|
||
|
||
### GET /api/bets
|
||
List user's bets with optional filters.
|
||
|
||
**Query params:**
|
||
| Param | Type | Required | Default | Description |
|
||
|---|---|---|---|---|
|
||
| status | string | no | all | Filter: pending, won, lost, push, void |
|
||
| book | string | no | all | Filter by sportsbook |
|
||
| limit | int | no | 20 | Pagination limit (max 100) |
|
||
| offset | int | no | 0 | Pagination offset |
|
||
|
||
**Response (200):**
|
||
```json
|
||
{
|
||
"bets": [
|
||
{
|
||
"id": "uuid",
|
||
"amount": 20.00,
|
||
"potential_payout": 38.18,
|
||
"book": "draftkings",
|
||
"bet_type": "straight",
|
||
"status": "pending",
|
||
"slip_data": { "legs": [...], "total_odds": -110 },
|
||
"submission_method": "quickslip",
|
||
"scan_session_id": null,
|
||
"placed_at": "2026-03-21T18:00:00Z",
|
||
"settled_at": null
|
||
}
|
||
],
|
||
"total": 15,
|
||
"limit": 20,
|
||
"offset": 0
|
||
}
|
||
```
|
||
|
||
### GET /api/bets/performance
|
||
Returns aggregated performance stats. Recalculates from settled bets and upserts into `performance` table.
|
||
|
||
**Response (200):**
|
||
```json
|
||
{
|
||
"weekly": { "roi": 12.5, "win_rate": 60.0, "sample_size": 10, "total_wagered": 200.00, "total_profit": 25.00 },
|
||
"monthly": { "roi": 8.3, "win_rate": 55.0, "sample_size": 40, "total_wagered": 800.00, "total_profit": 66.40 },
|
||
"all_time": { "roi": 6.1, "win_rate": 52.0, "sample_size": 120, "total_wagered": 2400.00, "total_profit": 146.40 }
|
||
}
|
||
```
|
||
|
||
### Error Responses
|
||
| Status | When | Body |
|
||
|---|---|---|
|
||
| 400 | Missing required fields, invalid bet_type, empty legs | `{ "error": "legs array is required" }` |
|
||
| 401 | No auth token | `{ "error": "Authentication required" }` |
|
||
| 404 | Bet not found or not owned | `{ "error": "Bet not found" }` |
|
||
| 413 | Screenshot too large (> 5MB) | `{ "error": "Image must be under 5MB" }` |
|
||
| 422 | Invalid settlement (already settled, invalid status) | `{ "error": "Bet already settled" }` |
|
||
|
||
## Payout Calculation
|
||
|
||
For straight bets:
|
||
```
|
||
If odds < 0: payout = amount + (amount / (|odds| / 100))
|
||
If odds > 0: payout = amount + (amount * (odds / 100))
|
||
```
|
||
|
||
For parlays, multiply individual leg payouts:
|
||
```
|
||
parlay_multiplier = product of (1 + single_payout_ratio) for each leg
|
||
payout = amount * parlay_multiplier
|
||
```
|
||
|
||
## Performance Calculation
|
||
|
||
Triggered on each bet settlement. Recalculates from all settled bets for the user.
|
||
|
||
```
|
||
For each period (weekly, monthly, all_time):
|
||
- Filter settled bets within the period
|
||
- total_wagered = sum of amount for all settled bets
|
||
- total_profit = sum of (payout - amount) for won bets - sum of amount for lost bets
|
||
- roi = (total_profit / total_wagered) * 100
|
||
- win_rate = (won_count / (won_count + lost_count)) * 100
|
||
- sample_size = total settled bets
|
||
|
||
Upsert into performance table (unique index on user_id + period).
|
||
```
|
||
|
||
Period boundaries:
|
||
- weekly: current ISO week (Monday–Sunday)
|
||
- monthly: current calendar month
|
||
- all_time: all settled bets ever
|
||
|
||
## Scan Session Linking
|
||
|
||
When a user submits a bet with `scan_session_id`:
|
||
- Validate the session exists and belongs to the user
|
||
- Store the reference in `bets.slip_data.scan_session_id`
|
||
- This enables "how many bets followed BetonBLK grades" analytics later
|
||
|
||
## Service Architecture
|
||
|
||
```
|
||
src/
|
||
├── services/
|
||
│ ├── betService.js # Create bet, settle bet, list bets
|
||
│ ├── payoutCalculator.js # Payout math for straight/parlay/teaser
|
||
│ ├── performanceService.js # Recalculate and upsert performance stats
|
||
│ └── ocrStub.js # Stub OCR for screenshot extraction (MVP)
|
||
├── routes/
|
||
│ └── bets.js # All bet endpoints
|
||
```
|
||
|
||
- **betService.js** — Core CRUD: create from quickslip/screenshot, settle, list with filters.
|
||
- **payoutCalculator.js** — Pure function. Calculates potential payout from odds + amount + bet type.
|
||
- **performanceService.js** — Queries settled bets, computes ROI/win_rate/profit, upserts performance table.
|
||
- **ocrStub.js** — MVP stub. Returns a "needs_confirmation" response. Real OCR layered in Phase 3.
|
||
|
||
## Acceptance Criteria
|
||
|
||
1. `POST /api/bets/quickslip` creates a bet with correct slip_data, amount, payout, and status "pending"
|
||
2. `POST /api/bets/screenshot` accepts image upload and returns extracted data with `needs_confirmation: true`
|
||
3. `POST /api/bets/screenshot/confirm` saves the confirmed bet to the database
|
||
4. `POST /api/bets/sync` returns "coming_soon" stub response
|
||
5. `PATCH /api/bets/:id/settle` updates bet status and creates outcome records
|
||
6. `PATCH /api/bets/:id/settle` rejects settling an already-settled bet (422)
|
||
7. `GET /api/bets` returns user's bets with status/book/limit/offset filters
|
||
8. `GET /api/bets/performance` returns ROI, win_rate, sample_size for weekly/monthly/all_time
|
||
9. Payout calculation correct for straight bets (positive and negative odds)
|
||
10. Payout calculation correct for parlays (multiplied leg payouts)
|
||
11. Performance recalculated on each settlement
|
||
12. Scan session linking validated (session must exist and belong to user)
|
||
13. All endpoints require auth (401 without token)
|
||
14. Image upload rejects files > 5MB (413)
|
||
|
||
## Test Plan
|
||
|
||
### Unit Tests (payoutCalculator.js)
|
||
- Straight bet with negative odds (-110, $20) → correct payout
|
||
- Straight bet with positive odds (+150, $20) → correct payout
|
||
- Parlay payout with 2 legs → multiplied correctly
|
||
- Parlay payout with 3 legs → multiplied correctly
|
||
- Edge case: even odds (-100) → doubles the bet
|
||
|
||
### Unit Tests (performanceService.js)
|
||
- Calculates ROI correctly from mix of won/lost bets
|
||
- Win rate = won / (won + lost) * 100 (excludes push/void)
|
||
- Weekly period filters to current week only
|
||
- Empty settled bets returns zeroes
|
||
- Upserts into performance table (not duplicate rows)
|
||
|
||
### Unit Tests (ocrStub.js)
|
||
- Returns structured placeholder with needs_confirmation: true
|
||
- Includes book from request
|
||
|
||
### Integration Tests (routes/bets.js)
|
||
- Quickslip: create straight bet → 201 with correct payout
|
||
- Quickslip: create parlay → 201 with multiplied payout
|
||
- Screenshot: upload image → 201 with needs_confirmation
|
||
- Screenshot: confirm → 201 with saved bet
|
||
- Sync: returns coming_soon stub
|
||
- Settle: pending bet → won → 200 with settled_at
|
||
- Settle: already settled → 422
|
||
- List: returns user's bets with pagination
|
||
- List: status filter works
|
||
- Performance: returns stats for all 3 periods
|
||
- Auth: no token → 401
|
||
- Validation: missing legs → 400
|
||
|
||
## Open Questions
|
||
- **Screenshot storage:** For MVP, we don't actually store the image file — the stub extracts nothing and the user manually enters data via the confirm endpoint. When real OCR is added (Phase 3), we'll need file storage (Supabase Storage or S3). No infrastructure needed now.
|