Features - Payment reminders via Resend (candidates, send, history, test email) - Bulk import of the customer list from Excel/CSV in the dashboard - Dashboard i18n (en/nl) with language selector Security - Enforce venue isolation on all /venues/:venueId routes - Require admin for planned-tab changes and pairing codes - Rate-limit login and device registration; opt-in TRUST_PROXY - Require JWT_SECRET in production; stop leaking internal errors - Escape customer names in reminder emails Sync and commands - Remove tabs the terminal no longer reports; validate nested sync payload - Return all pending commands on every sync (fixes missed-command race) - First command ack wins; atomic registration code claim - Reject commands for revoked devices, stale versions or duplicates - Remind only unpaid balances, with a 7-day cooldown Dashboard - Share a single token refresh across concurrent 401s (random logouts) - Keep the session alive for the refresh token lifetime Deployment - Replace `db push --accept-data-loss` with migrations (baselines legacy DBs) - Set NODE_ENV=production; pass Resend and TRUST_PROXY env in prod compose
176 lines
4.1 KiB
Plaintext
176 lines
4.1 KiB
Plaintext
datasource db {
|
|
provider = "postgresql"
|
|
url = env("DATABASE_URL")
|
|
}
|
|
|
|
generator client {
|
|
provider = "prisma-client-js"
|
|
}
|
|
|
|
model Venue {
|
|
id String @id @default(uuid())
|
|
name String
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
devices Device[]
|
|
clientUsers ClientUser[]
|
|
tabOwnerships TabOwnership[]
|
|
remoteCommands RemoteCommand[]
|
|
auditLogs AuditLog[]
|
|
regCodes RegistrationCode[]
|
|
plannedTabs PlannedTab[]
|
|
tabReminders TabReminder[]
|
|
}
|
|
|
|
model Device {
|
|
id String @id @default(uuid())
|
|
venueId String @unique
|
|
name String
|
|
tokenHash String @unique
|
|
tokenPrefix String
|
|
tokenExpiresAt DateTime
|
|
lastSeenAt DateTime @default(now())
|
|
revokedAt DateTime?
|
|
createdAt DateTime @default(now())
|
|
|
|
venue Venue @relation(fields: [venueId], references: [id])
|
|
tabSnapshots OpenTabSnapshot[]
|
|
tabOwnerships TabOwnership[]
|
|
remoteCommands RemoteCommand[]
|
|
|
|
@@index([venueId, revokedAt])
|
|
}
|
|
|
|
model ClientUser {
|
|
id String @id @default(uuid())
|
|
venueId String
|
|
email String @unique
|
|
passwordHash String
|
|
name String
|
|
role String @default("viewer")
|
|
createdAt DateTime @default(now())
|
|
|
|
venue Venue @relation(fields: [venueId], references: [id])
|
|
|
|
@@index([venueId])
|
|
}
|
|
|
|
model RegistrationCode {
|
|
id String @id @default(uuid())
|
|
venueId String
|
|
code String @unique
|
|
expiresAt DateTime
|
|
usedAt DateTime?
|
|
createdAt DateTime @default(now())
|
|
|
|
venue Venue @relation(fields: [venueId], references: [id])
|
|
|
|
@@index([venueId])
|
|
}
|
|
|
|
model OpenTabSnapshot {
|
|
id String @id @default(uuid())
|
|
deviceId String
|
|
tabId String
|
|
customerName String
|
|
version Int
|
|
totalInCents Int
|
|
itemCount Int
|
|
reportedAt DateTime
|
|
|
|
device Device @relation(fields: [deviceId], references: [id])
|
|
|
|
@@unique([deviceId, tabId])
|
|
@@index([deviceId])
|
|
@@index([tabId, reportedAt])
|
|
}
|
|
|
|
model TabOwnership {
|
|
tabId String
|
|
venueId String
|
|
deviceId String
|
|
lastReportedAt DateTime
|
|
|
|
venue Venue @relation(fields: [venueId], references: [id])
|
|
device Device @relation(fields: [deviceId], references: [id])
|
|
|
|
@@id([venueId, tabId])
|
|
}
|
|
|
|
model RemoteCommand {
|
|
id String @id @default(uuid())
|
|
venueId String
|
|
deviceId String
|
|
targetTabId String
|
|
type String @default("close_tab")
|
|
expectedVersion Int
|
|
paymentMethod String
|
|
requestedBy String
|
|
status String @default("pending")
|
|
result String?
|
|
acknowledgedAt DateTime?
|
|
createdAt DateTime @default(now())
|
|
expiresAt DateTime
|
|
|
|
venue Venue @relation(fields: [venueId], references: [id])
|
|
device Device @relation(fields: [deviceId], references: [id])
|
|
|
|
@@index([deviceId, status, createdAt])
|
|
@@index([venueId, status, createdAt])
|
|
}
|
|
|
|
model AuditLog {
|
|
id String @id @default(uuid())
|
|
venueId String
|
|
actorType String
|
|
actorId String
|
|
action String
|
|
details String?
|
|
createdAt DateTime @default(now())
|
|
|
|
venue Venue @relation(fields: [venueId], references: [id])
|
|
|
|
@@index([venueId, createdAt])
|
|
}
|
|
|
|
model RefreshToken {
|
|
id String @id @default(uuid())
|
|
userId String
|
|
tokenHash String @unique
|
|
expiresAt DateTime
|
|
revokedAt DateTime?
|
|
createdAt DateTime @default(now())
|
|
|
|
@@index([userId])
|
|
}
|
|
|
|
model PlannedTab {
|
|
id String @id @default(uuid())
|
|
venueId String
|
|
customerName String
|
|
email String?
|
|
position Int @default(0)
|
|
createdAt DateTime @default(now())
|
|
|
|
venue Venue @relation(fields: [venueId], references: [id])
|
|
|
|
@@unique([venueId, customerName])
|
|
@@index([venueId])
|
|
}
|
|
|
|
model TabReminder {
|
|
id String @id @default(uuid())
|
|
venueId String
|
|
tabId String
|
|
customerName String
|
|
email String
|
|
amountInCents Int
|
|
sentAt DateTime @default(now())
|
|
status String @default("sent")
|
|
|
|
venue Venue @relation(fields: [venueId], references: [id])
|
|
|
|
@@index([venueId, sentAt])
|
|
@@index([venueId, tabId])
|
|
} |