Skip to main content

Overview

Sorteos (lottery draws) follow a strict state machine with four main states. Understanding the lifecycle and valid transitions is critical for managing lottery operations.

State Machine

Sorteo States

StateDescriptionTicket CreationEvaluation
SCHEDULEDFuture draw, not yet open❌ Blocked❌ Blocked
OPENAccepting bets✅ Allowed❌ Blocked
CLOSEDBetting closed, awaiting results❌ Blocked✅ Allowed
EVALUATEDResults published, tickets evaluated❌ Blocked✅ Can revert

State Transitions

SCHEDULED → OPEN

1

Activate sorteo

Manually open a scheduled sorteo for betting.
PATCH /api/v1/sorteos/:id/open
From src/api/v1/controllers/sorteo.controller.ts:22-25:
async open(req: AuthenticatedRequest, res: Response) {
  const s = await SorteoService.open(req.params.id, req.user!.id);
  res.json({ success: true, data: s });
}
2

Or activate and open in one call

Combine isActive = true and status transition:
PATCH /api/v1/sorteos/:id/activate-and-open
From src/api/v1/controllers/sorteo.controller.ts:32-35:
async activateAndOpen(req: AuthenticatedRequest, res: Response) {
  const s = await SorteoService.activateAndOpen(req.params.id, req.user!.id);
  res.json({ success: true, data: s });
}
Opening a sorteo makes it visible to vendedores and enables ticket creation.

OPEN → CLOSED

Close betting before evaluating results.
PATCH /api/v1/sorteos/:id/close
From src/api/v1/controllers/sorteo.controller.ts:37-40:
async close(req: AuthenticatedRequest, res: Response) {
  const s = await SorteoService.close(req.params.id, req.user!.id);
  res.json({ success: true, data: s });
}
Use case: Manual close before scheduled draw time.

CLOSED → EVALUATED

Evaluate sorteo with winning number and optional REVENTADO multiplier.
curl -X PATCH https://api.example.com/api/v1/sorteos/:id/evaluate \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "winningNumber": "42"
  }'
From src/api/v1/controllers/sorteo.controller.ts:42-50:
async evaluate(req: AuthenticatedRequest, res: Response) {
  const s = await SorteoService.evaluate(
    req.params.id,
    req.body,
    req.user!.id
  );
  res.json({ success: true, data: s });
}

Evaluation Rules

  • Must be exactly 2 digits (00-99) for standard lotteries
  • Must be 3 digits (000-999) for monazos
  • Validated against Loteria.rulesJson.numberRange
When the winning number has REVENTADO bets:
  • Required: extraMultiplierId of type REVENTADO
  • Must be active (isActive = true)
  • Must belong to same loteriaId
  • If appliesToSorteoId is set, must match current sorteo
  • System snapshots extraMultiplierX to sorteo
For each active ticket:
  1. NUMERO bets: Check if jugada.number === winningNumber
    • If match: isWinner = true, payout = amount × finalMultiplierX
  2. REVENTADO bets: Check if jugada.number === winningNumber
    • If match: isWinner = true, payout = amount × finalMultiplierX
    • Snapshot extraMultiplierX from sorteo to jugada
  3. Update ticket:
    • status = 'EVALUATED'
    • isActive = false
    • Calculate totalPayout
    • Set remainingAmount (for payment tracking)

EVALUATED → CLOSED (Revert)

Revert evaluation to fix errors.
POST /api/v1/sorteos/:id/revert-evaluation
From src/api/v1/controllers/sorteo.controller.ts:176-183:
async revertEvaluation(req: AuthenticatedRequest, res: Response) {
  const s = await SorteoService.revertEvaluation(
    req.params.id,
    req.user!.id,
    req.body?.reason
  );
  res.json({ success: true, data: s });
}
Reverting evaluation:
  • Resets all ticket statuses to pre-evaluation state
  • Clears winningNumber and extraMultiplierId from sorteo
  • Preserves original ticket data for re-evaluation
  • Requires admin privileges
  • Optional: Include reason in request body for audit trail

Creating Sorteos

Manual Creation

POST /api/v1/sorteos
{
  "loteriaId": "uuid-loteria",
  "name": "12:55 PM",
  "scheduledAt": "2025-01-20T18:55:00.000Z",  // UTC timestamp
  "isActive": true
}

Batch Creation (Seed)

Generate multiple sorteos based on loteria schedule rules.
1

Preview schedule

Get upcoming draw times without creating sorteos:
GET /api/v1/loterias/:id/preview_schedule?start=2025-01-20&days=7
Returns:
{
  "preview": [
    "2025-01-20T18:55:00.000Z",
    "2025-01-21T18:55:00.000Z",
    "2025-01-22T18:55:00.000Z"
  ],
  "count": 3
}
2

Seed sorteos

Create sorteos in batch:
POST /api/v1/loterias/:id/seed_sorteos?start=2025-01-20&days=7
With optional body for dry-run:
{
  "dryRun": true  // Preview without creating
}
Response:
{
  "created": ["2025-01-20T18:55:00.000Z", "2025-01-21T18:55:00.000Z"],
  "skipped": ["2025-01-22T18:55:00.000Z"],  // Already existed
  "alreadyExists": ["2025-01-22T18:55:00.000Z"],
  "processed": ["2025-01-20T18:55:00.000Z", "2025-01-21T18:55:00.000Z", "2025-01-22T18:55:00.000Z"]
}
Idempotency: The seed endpoint uses @@unique([loteriaId, scheduledAt]) constraint to prevent duplicates. Concurrent calls are safe.

Listing Sorteos

GET /api/v1/sorteos?loteriaId=uuid&status=OPEN&date=today
Query parameters:
  • loteriaId: Filter by lottery
  • status: SCHEDULED | OPEN | CLOSED | EVALUATED
  • isActive: true | false
  • date: today | tomorrow | week | month | range
  • fromDate, toDate: For date=range (ISO format)
  • search: Search by name or winning number
  • groupBy: hour | loteria-hour (grouped view)
  • page, pageSize: Pagination

Grouped View

From src/api/v1/controllers/sorteo.controller.ts:127-130:
const groupBy = typeof req.query.groupBy === "string" 
  ? (req.query.groupBy as "hour" | "loteria-hour" | undefined)
  : undefined;
Example with groupBy=hour:
{
  "data": {
    "10:00": [
      {"id": "uuid-1", "name": "10:00 AM", "loteriaName": "Nacional"},
      {"id": "uuid-2", "name": "10:00 AM", "loteriaName": "Popular"}
    ],
    "12:55": [...]
  },
  "meta": {
    "grouped": true,
    "groupBy": "hour",
    "total": 15
  }
}

Updating Sorteos

PUT /api/v1/sorteos/:id
# or
PATCH /api/v1/sorteos/:id
From src/api/v1/controllers/sorteo.controller.ts:11-14:
async update(req: AuthenticatedRequest, res: Response) {
  const s = await SorteoService.update(req.params.id, req.body, req.user!.id);
  res.json({ success: true, data: s });
}
Allowed updates:
  • name: Display name
  • scheduledAt: Reschedule draw time
  • isActive: Visibility toggle
Cannot change status or evaluation results via update endpoint. Use dedicated state transition endpoints.

Soft Delete and Restore

Delete (Soft)

DELETE /api/v1/sorteos/:id
Optional body:
{
  "reason": "Duplicate entry, correct sorteo is uuid-xyz"
}
From src/api/v1/controllers/sorteo.controller.ts:52-59:
async delete(req: AuthenticatedRequest, res: Response) {
  const s = await SorteoService.remove(
    req.params.id,
    req.user!.id,
    req.body?.reason
  );
  res.json({ success: true, data: s });
}

Restore

PATCH /api/v1/sorteos/:id/restore
From src/api/v1/controllers/sorteo.controller.ts:61-64:
async restore(req: AuthenticatedRequest, res: Response) {
  const s = await SorteoService.restore(req.params.id, req.user!.id);
  res.json({ success: true, data: s });
}

Reset to Scheduled

Reset sorteo back to SCHEDULED state (admin recovery tool).
POST /api/v1/sorteos/:id/reset-to-scheduled
From src/api/v1/controllers/sorteo.controller.ts:66-69:
async resetToScheduled(req: AuthenticatedRequest, res: Response) {
  const s = await SorteoService.resetToScheduled(req.params.id, req.user!.id);
  res.json({ success: true, data: s });
}

Force Open (Override)

Force open a sorteo even if in CLOSED or EVALUATED state.
PATCH /api/v1/sorteos/:id/force-open
From src/api/v1/controllers/sorteo.controller.ts:27-30:
async forceOpen(req: AuthenticatedRequest, res: Response) {
  const s = await SorteoService.forceOpen(req.params.id, req.user!.id);
  res.json({ success: true, data: s });
}
Use with caution. Force-opening an evaluated sorteo can create inconsistencies.

Evaluated Summary (Vendedor View)

Get summary of evaluated sorteos with win/loss statistics.
GET /api/v1/sorteos/evaluated-summary?scope=mine&date=today
From src/api/v1/controllers/sorteo.controller.ts:185-212:
async evaluatedSummary(req: AuthenticatedRequest, res: Response) {
  const { date, fromDate, toDate, scope, loteriaId, status, isActive } = req.query as any;
  const vendedorId = req.user!.id;
  
  const result = await SorteoService.evaluatedSummary(
    { date, fromDate, toDate, scope: scope || 'mine', loteriaId, status, isActive },
    vendedorId
  );
  res.json({ success: true, ...result });
}
Response structure:
{
  "sorteos": [
    {
      "id": "uuid-sorteo",
      "name": "12:55 PM",
      "winningNumber": "42",
      "myTotalBet": 5000,
      "myTotalPayout": 15000,
      "myNetResult": 10000,  // positive = won, negative = lost
      "myWinningTickets": 2,
      "myLosingTickets": 8
    }
  ],
  "summary": {
    "totalBet": 5000,
    "totalPayout": 15000,
    "netResult": 10000
  }
}

Best Practices

Always use dedicated transition endpoints (/open, /close, /evaluate) instead of trying to change status via update.
Check current state before calling transition endpoints to provide better UX error messages.
Use /seed_sorteos to pre-create sorteos for the week. This prevents last-minute issues.
Include reason field when reverting evaluations for audit compliance.
Use groupBy=hour for operational dashboards to see all draws at the same time across lotteries.

Build docs developers (and LLMs) love