From 12ea11fa9b081b6fe020391ebad313999ebda6b4 Mon Sep 17 00:00:00 2001 From: skoriop Date: Fri, 10 Jan 2025 06:34:00 +0530 Subject: [PATCH 01/11] feat: add cron hook to make bet payouts --- db/hooks/bet_status_update.pb.js | 43 ++++++++++++++++++++++++++++++++ docker-compose.yml | 1 + 2 files changed, 44 insertions(+) create mode 100644 db/hooks/bet_status_update.pb.js diff --git a/db/hooks/bet_status_update.pb.js b/db/hooks/bet_status_update.pb.js new file mode 100644 index 0000000..bfd2fab --- /dev/null +++ b/db/hooks/bet_status_update.pb.js @@ -0,0 +1,43 @@ +// run every minute +cronAdd("bet_status_update", "*/1 * * * *", () => { + // unfortunately pocketbase admin UI doesn't support showing timestamps in local time and defaults to UTC + // i don't think it's realistic or convenient to expect them to convert local time to UTC while adding events + // so this means a lot of reverse timezone shenanigans will be needed in the frontend too + let now = new Date(new Date().getTime() + 1000 * 60 * 330); + + let events = $app.findAllRecords( + "events", + $dbx.exp("endTime < {:now}", { now: now.toISOString().replace("T", " ") }) + ); + + for (const event of events) { + let winners = $app + .findAllRecords( + "standings", + $dbx.exp("position == 1 AND event == {:id}", { id: event.id }) + ) + .map((standing) => standing.get("team")); + + let bets = $app.findAllRecords( + "bets", + $dbx.exp("event == {:id}", { id: event.id }) + ); + + let wonBets = bets.filter((bet) => winners.includes(bet.get("team"))); + + // TODO: maintain these online using extra fields / collections instead of recomputing every time + let pool = bets.reduce((acc, bet) => acc + bet.getInt("amount"), 0); + let wonPool = wonBets.reduce((acc, bet) => acc + bet.getInt("amount"), 0); + + for (const bet of wonBets) { + let amount = bet.getInt("amount"); + let payout = Math.floor((amount * pool) / wonPool); + + // not using the user expands on bets since it might have stale data (which means money could be disappearing) + // does this make things safe from race conditions? i hope so + let user = $app.findRecordById("users", bet.get("user")); + user.set("balance", user.getInt("balance") + payout); + app.save(user); + } + } +}); diff --git a/docker-compose.yml b/docker-compose.yml index bd30257..bad091d 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -22,6 +22,7 @@ services: volumes: - ./db/migrations:/pb/pb_migrations - ./db/data:/pb/pb_data + - ./db/hooks:/pb/pb_hooks profiles: - dev From 42b0f526d7ab0a62737ad3c25633bef2343bc074 Mon Sep 17 00:00:00 2001 From: skoriop Date: Fri, 10 Jan 2025 06:43:17 +0530 Subject: [PATCH 02/11] feat: add `standingsUpdated` column --- db/migrations/1735284047_collections_snapshot.js | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/db/migrations/1735284047_collections_snapshot.js b/db/migrations/1735284047_collections_snapshot.js index 358dd46..ab9e9bf 100644 --- a/db/migrations/1735284047_collections_snapshot.js +++ b/db/migrations/1735284047_collections_snapshot.js @@ -926,6 +926,15 @@ migrate((app) => { "thumbs": [], "type": "file" }, + { + "hidden": false, + "id": "bool847678146", + "name": "standingsUpdated", + "presentable": false, + "required": false, + "system": false, + "type": "bool" + }, { "hidden": false, "id": "autodate2990389176", From 4469b7b40c4efde90c2637003ceada2e5025216d Mon Sep 17 00:00:00 2001 From: skoriop Date: Fri, 10 Jan 2025 07:00:43 +0530 Subject: [PATCH 03/11] feat: convert cron hook to update hook --- db/hooks/bet_status_update.pb.js | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/db/hooks/bet_status_update.pb.js b/db/hooks/bet_status_update.pb.js index bfd2fab..ced01e3 100644 --- a/db/hooks/bet_status_update.pb.js +++ b/db/hooks/bet_status_update.pb.js @@ -1,5 +1,8 @@ -// run every minute -cronAdd("bet_status_update", "*/1 * * * *", () => { +onRecordUpdate((e) => { + if (!e.record.getBool("standingsUpdated")) { + return e.next(); + } + // unfortunately pocketbase admin UI doesn't support showing timestamps in local time and defaults to UTC // i don't think it's realistic or convenient to expect them to convert local time to UTC while adding events // so this means a lot of reverse timezone shenanigans will be needed in the frontend too @@ -37,7 +40,9 @@ cronAdd("bet_status_update", "*/1 * * * *", () => { // does this make things safe from race conditions? i hope so let user = $app.findRecordById("users", bet.get("user")); user.set("balance", user.getInt("balance") + payout); - app.save(user); + $app.save(user); } } -}); + + e.next(); +}, "events"); From 91a5eb04a71c52979021722a722944da58131a3c Mon Sep 17 00:00:00 2001 From: skoriop Date: Fri, 10 Jan 2025 07:18:27 +0530 Subject: [PATCH 04/11] feat: add bet pool collection --- .../1735284047_collections_snapshot.js | 86 +++++++++++++++++++ 1 file changed, 86 insertions(+) diff --git a/db/migrations/1735284047_collections_snapshot.js b/db/migrations/1735284047_collections_snapshot.js index ab9e9bf..755b682 100644 --- a/db/migrations/1735284047_collections_snapshot.js +++ b/db/migrations/1735284047_collections_snapshot.js @@ -1239,6 +1239,92 @@ migrate((app) => { "type": "base", "updateRule": null, "viewRule": null + }, + { + "createRule": null, + "deleteRule": null, + "fields": [ + { + "autogeneratePattern": "[a-z0-9]{15}", + "hidden": false, + "id": "text3208210256", + "max": 15, + "min": 15, + "name": "id", + "pattern": "^[a-z0-9]+$", + "presentable": false, + "primaryKey": true, + "required": true, + "system": true, + "type": "text" + }, + { + "cascadeDelete": false, + "collectionId": "pbc_1687431684", + "hidden": false, + "id": "relation1001261735", + "maxSelect": 1, + "minSelect": 0, + "name": "event", + "presentable": false, + "required": true, + "system": false, + "type": "relation" + }, + { + "cascadeDelete": false, + "collectionId": "pbc_1568971955", + "hidden": false, + "id": "relation3303056927", + "maxSelect": 1, + "minSelect": 0, + "name": "team", + "presentable": false, + "required": true, + "system": false, + "type": "relation" + }, + { + "hidden": false, + "id": "number2392944706", + "max": null, + "min": 0, + "name": "amount", + "onlyInt": false, + "presentable": false, + "required": false, + "system": false, + "type": "number" + }, + { + "hidden": false, + "id": "autodate2990389176", + "name": "created", + "onCreate": true, + "onUpdate": false, + "presentable": false, + "system": false, + "type": "autodate" + }, + { + "hidden": false, + "id": "autodate3332085495", + "name": "updated", + "onCreate": true, + "onUpdate": true, + "presentable": false, + "system": false, + "type": "autodate" + } + ], + "id": "pbc_1788541978", + "indexes": [], + "listRule": null, + "name": "betPool", + "system": false, + "type": "base", + "updateRule": null, + "viewRule": null } ]; From e134f44b915279fa708aee2c1acd71345babd1f5 Mon Sep 17 00:00:00 2001 From: skoriop Date: Sat, 11 Jan 2025 06:04:16 +0530 Subject: [PATCH 05/11] fix: update collection types --- app/src/lib/types/pocketbase.d.ts | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/app/src/lib/types/pocketbase.d.ts b/app/src/lib/types/pocketbase.d.ts index 4c1c470..5cae4ea 100644 --- a/app/src/lib/types/pocketbase.d.ts +++ b/app/src/lib/types/pocketbase.d.ts @@ -11,6 +11,7 @@ export enum Collections { Mfas = '_mfas', Otps = '_otps', Superusers = '_superusers', + BetPool = 'betPool', Bets = 'bets', Events = 'events', Standings = 'standings', @@ -89,6 +90,15 @@ export type SuperusersRecord = { verified?: boolean; }; +export type BetPoolRecord = { + amount?: number; + created?: IsoDateString; + event: RecordIdString; + id: string; + team: RecordIdString; + updated?: IsoDateString; +}; + export type BetsRecord = { amount: number; created?: IsoDateString; @@ -128,6 +138,7 @@ export type EventsRecord = { id: string; location: string; sport: EventsSportOptions; + standingsUpdated?: boolean; startTime: IsoDateString; teams: RecordIdString[]; title: string; @@ -175,6 +186,8 @@ export type MfasResponse = Required & BaseSystemF export type OtpsResponse = Required & BaseSystemFields; export type SuperusersResponse = Required & AuthSystemFields; +export type BetPoolResponse = Required & + BaseSystemFields; export type BetsResponse = Required & BaseSystemFields; export type EventsResponse = Required & BaseSystemFields; export type StandingsResponse = Required & @@ -190,6 +203,7 @@ export type CollectionRecords = { _mfas: MfasRecord; _otps: OtpsRecord; _superusers: SuperusersRecord; + betPool: BetPoolRecord; bets: BetsRecord; events: EventsRecord; standings: StandingsRecord; @@ -203,6 +217,7 @@ export type CollectionResponses = { _mfas: MfasResponse; _otps: OtpsResponse; _superusers: SuperusersResponse; + betPool: BetPoolResponse; bets: BetsResponse; events: EventsResponse; standings: StandingsResponse; @@ -219,6 +234,7 @@ export type TypedPocketBase = PocketBase & { collection(idOrName: '_mfas'): RecordService; collection(idOrName: '_otps'): RecordService; collection(idOrName: '_superusers'): RecordService; + collection(idOrName: 'betPool'): RecordService; collection(idOrName: 'bets'): RecordService; collection(idOrName: 'events'): RecordService; collection(idOrName: 'standings'): RecordService; From f9dc558d4806db621853733e47d6089375f22fae Mon Sep 17 00:00:00 2001 From: skoriop Date: Sat, 11 Jan 2025 06:19:38 +0530 Subject: [PATCH 06/11] refactor: use bet pool collections --- app/src/routes/api/user/bet/+server.ts | 12 ++++++++++++ db/hooks/bet_status_update.pb.js | 19 +++++++++++++++---- 2 files changed, 27 insertions(+), 4 deletions(-) diff --git a/app/src/routes/api/user/bet/+server.ts b/app/src/routes/api/user/bet/+server.ts index 7617529..ca04724 100644 --- a/app/src/routes/api/user/bet/+server.ts +++ b/app/src/routes/api/user/bet/+server.ts @@ -52,6 +52,18 @@ const handlePOST: RequestHandler = async ({ request, locals }) => { .create({ user: locals.user.id, team: teamId, event: eventId, amount }); } + let betPool = ( + await pb.collection('betPool').getFullList({ + filter: `team="${teamId}" && event="${eventId}"` + }) + ).at(0); + + if (betPool) { + await pb.collection('betPool').update(betPool.id, { amount: betPool.amount + amount }); + } else { + await pb.collection('betPool').create({ event: eventId, team: teamId, amount }); + } + await pb.collection('users').update(userid, { balance: locals.user.balance - amount }); return json(newBet); }; diff --git a/db/hooks/bet_status_update.pb.js b/db/hooks/bet_status_update.pb.js index ced01e3..b993233 100644 --- a/db/hooks/bet_status_update.pb.js +++ b/db/hooks/bet_status_update.pb.js @@ -28,13 +28,24 @@ onRecordUpdate((e) => { let wonBets = bets.filter((bet) => winners.includes(bet.get("team"))); - // TODO: maintain these online using extra fields / collections instead of recomputing every time - let pool = bets.reduce((acc, bet) => acc + bet.getInt("amount"), 0); - let wonPool = wonBets.reduce((acc, bet) => acc + bet.getInt("amount"), 0); + let betPools = $app.findAllRecords( + "betPool", + $dbx.exp("event == {:id}", { id: event.id }) + ); + + let totalPool = 0; + let wonPool = 0; + + // one passTM + for (const betPool of betPools) { + totalPool += betPool.getInt("pool"); + if (winners.includes(betPool.get("team"))) + wonPool += betPool.getInt("pool"); + } for (const bet of wonBets) { let amount = bet.getInt("amount"); - let payout = Math.floor((amount * pool) / wonPool); + let payout = Math.floor((amount * totalPool) / wonPool); // not using the user expands on bets since it might have stale data (which means money could be disappearing) // does this make things safe from race conditions? i hope so From 9c1a60a81605360c796fb04edfddc3306900afe0 Mon Sep 17 00:00:00 2001 From: skoriop Date: Sat, 11 Jan 2025 07:14:50 +0530 Subject: [PATCH 07/11] fix: add some bug fixes also refactor code to remove unused bits --- db/hooks/bet_status_update.pb.js | 89 +++++++++++++++----------------- 1 file changed, 41 insertions(+), 48 deletions(-) diff --git a/db/hooks/bet_status_update.pb.js b/db/hooks/bet_status_update.pb.js index b993233..6ee9814 100644 --- a/db/hooks/bet_status_update.pb.js +++ b/db/hooks/bet_status_update.pb.js @@ -1,58 +1,51 @@ onRecordUpdate((e) => { - if (!e.record.getBool("standingsUpdated")) { + const event = e.record; + if (!event.getBool("standingsUpdated")) { return e.next(); } - // unfortunately pocketbase admin UI doesn't support showing timestamps in local time and defaults to UTC - // i don't think it's realistic or convenient to expect them to convert local time to UTC while adding events - // so this means a lot of reverse timezone shenanigans will be needed in the frontend too - let now = new Date(new Date().getTime() + 1000 * 60 * 330); + const winners = $app + .findAllRecords( + "standings", + $dbx.exp("position == 1 AND event == {:id}", { id: event.id }) + ) + .map((standing) => standing.get("team")); - let events = $app.findAllRecords( - "events", - $dbx.exp("endTime < {:now}", { now: now.toISOString().replace("T", " ") }) + const bets = $app.findAllRecords( + "bets", + $dbx.exp("event == {:id}", { id: event.id }) ); - for (const event of events) { - let winners = $app - .findAllRecords( - "standings", - $dbx.exp("position == 1 AND event == {:id}", { id: event.id }) - ) - .map((standing) => standing.get("team")); - - let bets = $app.findAllRecords( - "bets", - $dbx.exp("event == {:id}", { id: event.id }) - ); - - let wonBets = bets.filter((bet) => winners.includes(bet.get("team"))); - - let betPools = $app.findAllRecords( - "betPool", - $dbx.exp("event == {:id}", { id: event.id }) - ); - - let totalPool = 0; - let wonPool = 0; - - // one passTM - for (const betPool of betPools) { - totalPool += betPool.getInt("pool"); - if (winners.includes(betPool.get("team"))) - wonPool += betPool.getInt("pool"); - } - - for (const bet of wonBets) { - let amount = bet.getInt("amount"); - let payout = Math.floor((amount * totalPool) / wonPool); - - // not using the user expands on bets since it might have stale data (which means money could be disappearing) - // does this make things safe from race conditions? i hope so - let user = $app.findRecordById("users", bet.get("user")); - user.set("balance", user.getInt("balance") + payout); - $app.save(user); - } + const wonBets = bets.filter((bet) => winners.includes(bet.get("team"))); + + const betPools = $app.findAllRecords( + "betPool", + $dbx.exp("event == {:id}", { id: event.id }) + ); + + let totalPool = 0; + let wonPool = 0; + + // one passTM + for (const betPool of betPools) { + totalPool += betPool.getInt("amount"); + if (winners.includes(betPool.get("team"))) + wonPool += betPool.getInt("amount"); + } + + if (wonPool === 0) { + return e.next(); + } + + for (const bet of wonBets) { + const amount = bet.getInt("amount"); + const payout = Math.floor((amount * totalPool) / wonPool); + + // not using the user expands on bets since it might have stale data (which means money could be disappearing) + // does this make things safe from race conditions? i hope so + const user = $app.findRecordById("users", bet.get("user")); + user.set("balance", user.getInt("balance") + payout); + $app.save(user); } e.next(); From 88e7d04b18fb73107fb9aefa6e47de7b0a9191ab Mon Sep 17 00:00:00 2001 From: skoriop Date: Sat, 11 Jan 2025 19:05:13 +0530 Subject: [PATCH 08/11] refactor: remove variable --- app/src/routes/api/user/bet/+server.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/app/src/routes/api/user/bet/+server.ts b/app/src/routes/api/user/bet/+server.ts index ca04724..cfb21d1 100644 --- a/app/src/routes/api/user/bet/+server.ts +++ b/app/src/routes/api/user/bet/+server.ts @@ -9,7 +9,6 @@ const handlePOST: RequestHandler = async ({ request, locals }) => { } const { teamId, eventId, amount } = await request.json(); - const userid = locals.user.id; if (locals.user.balance < amount) { return error(400, 'Balance too low!'); @@ -64,7 +63,7 @@ const handlePOST: RequestHandler = async ({ request, locals }) => { await pb.collection('betPool').create({ event: eventId, team: teamId, amount }); } - await pb.collection('users').update(userid, { balance: locals.user.balance - amount }); + await pb.collection('users').update(locals.user.id, { balance: locals.user.balance - amount }); return json(newBet); }; From c4ad04d81ee79378d714e2d9c28d169b02f4257f Mon Sep 17 00:00:00 2001 From: skoriop Date: Sat, 11 Jan 2025 19:05:40 +0530 Subject: [PATCH 09/11] feat: add validation with zod --- app/package.json | 3 ++- app/pnpm-lock.yaml | 8 ++++++++ app/src/hooks.server.ts | 25 +++++++++++++++++++++++-- app/src/routes/api/user/bet/+server.ts | 7 +++++++ 4 files changed, 40 insertions(+), 3 deletions(-) diff --git a/app/package.json b/app/package.json index 34c994a..05005ec 100644 --- a/app/package.json +++ b/app/package.json @@ -35,6 +35,7 @@ "packageManager": "pnpm@9.15.1+sha512.1acb565e6193efbebda772702950469150cf12bcc764262e7587e71d19dc98a423dff9536e57ea44c49bdf790ff694e83c27be5faa23d67e0c033b583be4bfcf", "dependencies": { "@tabler/icons-svelte": "^3.26.0", - "pocketbase": "^0.24.0" + "pocketbase": "^0.24.0", + "zod": "^3.24.1" } } diff --git a/app/pnpm-lock.yaml b/app/pnpm-lock.yaml index a8612ca..495aa2a 100644 --- a/app/pnpm-lock.yaml +++ b/app/pnpm-lock.yaml @@ -14,6 +14,9 @@ importers: pocketbase: specifier: ^0.24.0 version: 0.24.0 + zod: + specifier: ^3.24.1 + version: 3.24.1 devDependencies: '@sveltejs/adapter-auto': specifier: ^3.0.0 @@ -1580,6 +1583,9 @@ packages: zimmerframe@1.1.2: resolution: {integrity: sha512-rAbqEGa8ovJy4pyBxZM70hg4pE6gDgaQ0Sl9M3enG3I0d6H4XSAM3GeNGLKnsBpuijUow064sf7ww1nutC5/3w==} + zod@3.24.1: + resolution: {integrity: sha512-muH7gBL9sI1nciMZV67X5fTKKBLtwpZ5VBp1vsOQzj1MhrBZ4wlVCm3gedKZWLp0Oyel8sIGfeiz54Su+OVT+A==} + snapshots: '@alloc/quick-lru@5.2.0': {} @@ -3052,3 +3058,5 @@ snapshots: yaml@2.6.1: {} zimmerframe@1.1.2: {} + + zod@3.24.1: {} diff --git a/app/src/hooks.server.ts b/app/src/hooks.server.ts index aa3ae2c..31c9718 100644 --- a/app/src/hooks.server.ts +++ b/app/src/hooks.server.ts @@ -1,8 +1,10 @@ import PocketBase from 'pocketbase'; -import { redirect, type Handle } from '@sveltejs/kit'; +import { error, redirect, type Handle } from '@sveltejs/kit'; import type { TypedPocketBase } from '$lib/types/pocketbase'; import { sequence } from '@sveltejs/kit/hooks'; import { INTERNAL_PB_URL } from '$env/static/private'; +import { _betCreateSchema } from './routes/api/user/bet/+server'; +import type { AnyZodObject } from 'zod'; const authentication: Handle = async ({ event, resolve }) => { event.locals.pb = new PocketBase(INTERNAL_PB_URL) as TypedPocketBase; @@ -42,4 +44,23 @@ const authorization: Handle = async ({ event, resolve }) => { return response; }; -export const handle = sequence(authentication, authorization); +const schemaMap: { [keys: string]: AnyZodObject } = { + '/api/user/bet': _betCreateSchema +}; + +const validation: Handle = async ({ event, resolve }) => { + const schema = schemaMap[event.url.pathname]; + if (schema) { + // apparently sveltekit doesn't like it if you read the request body twice, so we need to clone the request every time + const clonedRequest = event.request.clone(); + const result = schema.safeParse(await clonedRequest.json()); + if (!result.success) { + return error(400, result.error); + } + } + + const response = await resolve(event); + return response; +}; + +export const handle = sequence(authentication, authorization, validation); diff --git a/app/src/routes/api/user/bet/+server.ts b/app/src/routes/api/user/bet/+server.ts index cfb21d1..4dc555d 100644 --- a/app/src/routes/api/user/bet/+server.ts +++ b/app/src/routes/api/user/bet/+server.ts @@ -1,8 +1,15 @@ import { error, json, type RequestHandler } from '@sveltejs/kit'; import pb from '$lib/server/database'; +import { z } from 'zod'; import type { BetsResponse } from '$lib/types/pocketbase'; import type { BetExpand } from '$lib/types/expand'; +export const _betCreateSchema = z.object({ + teamId: z.string(), + eventId: z.string(), + amount: z.number().int() +}); + const handlePOST: RequestHandler = async ({ request, locals }) => { if (!locals.user) { return error(401, 'User not found'); From 9051288ce054be82aed189d734756238ed555ea0 Mon Sep 17 00:00:00 2001 From: skoriop Date: Sat, 11 Jan 2025 19:16:55 +0530 Subject: [PATCH 10/11] feat: validate team and event ids --- app/src/routes/api/user/bet/+server.ts | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/app/src/routes/api/user/bet/+server.ts b/app/src/routes/api/user/bet/+server.ts index 4dc555d..64f6cae 100644 --- a/app/src/routes/api/user/bet/+server.ts +++ b/app/src/routes/api/user/bet/+server.ts @@ -17,6 +17,15 @@ const handlePOST: RequestHandler = async ({ request, locals }) => { const { teamId, eventId, amount } = await request.json(); + const event = (await pb.collection('events').getFullList({ filter: `id="${eventId}"` })).at(0); + if (!event) { + return error(400, 'Event not found!'); + } + + if (!event.teams.includes(teamId)) { + return error(400, 'Team not found!'); + } + if (locals.user.balance < amount) { return error(400, 'Balance too low!'); } @@ -47,7 +56,6 @@ const handlePOST: RequestHandler = async ({ request, locals }) => { return error(400, 'Bet amount cannot be negative!'); } - const event = await pb.collection('events').getFirstListItem(`id="${eventId}"`); const startTime = new Date(event.startTime).getTime(); if (now > startTime) { return error(400, 'Bets are closed!'); From cbbb04ef1cdea3737d1a274776e6946b259b911f Mon Sep 17 00:00:00 2001 From: skoriop Date: Mon, 13 Jan 2025 05:05:15 +0530 Subject: [PATCH 11/11] feat: add some checks for bet payout --- db/hooks/bet_status_update.pb.js | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/db/hooks/bet_status_update.pb.js b/db/hooks/bet_status_update.pb.js index 6ee9814..8443268 100644 --- a/db/hooks/bet_status_update.pb.js +++ b/db/hooks/bet_status_update.pb.js @@ -4,12 +4,25 @@ onRecordUpdate((e) => { return e.next(); } + // unfortunately pocketbase admin UI doesn't support showing timestamps in local time and defaults to UTC + // i don't think it's realistic or convenient to expect them to convert local time to UTC while adding events + // so this means a lot of reverse timezone shenanigans will be needed in the frontend too + let now = new Date(new Date().getTime() + 1000 * 60 * 330); + + if (now / 1000 < e.record.getDateTime("endTime").unix()) { + throw new BadRequestError("Failed to trigger bet payout: Event has not ended yet!"); + } + const winners = $app .findAllRecords( "standings", $dbx.exp("position == 1 AND event == {:id}", { id: event.id }) ) .map((standing) => standing.get("team")); + + if (!winners.length) { + throw new BadRequestError("Failed to trigger bet payout: No winners found!"); + } const bets = $app.findAllRecords( "bets",