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[] tabEvents TabEvent[] products Product[] stockAdjustments StockAdjustment[] } 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[] tabEvents TabEvent[] @@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 // The tab's lines as reported by the terminal: [{ productName, quantity, unitPriceInCents }]. // Null for terminals running an app version that doesn't report them. items Json? 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 customerName String? amountInCents Int? 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 // Lowercased, whitespace-collapsed name: customers are matched on this everywhere customerNameKey String email String? position Int @default(0) createdAt DateTime @default(now()) // Set once the terminal reports an open tab with this name; it is then no longer sent to devices deliveredAt DateTime? venue Venue @relation(fields: [venueId], references: [id]) @@unique([venueId, customerNameKey]) @@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]) } // What happened on a tab, as reported by the terminal: lines added or removed, and settlements. // The id is generated by the terminal so a re-sent event is stored only once. model TabEvent { id String @id venueId String deviceId String tabId String customerName String // line_added | line_removed | settled type String productName String? unitPriceInCents Int? // Signed: positive for line_added, negative for line_removed quantityDelta Int? // settled only amountInCents Int? paymentMethod String? commandId String? items Json? occurredAt DateTime receivedAt DateTime @default(now()) venue Venue @relation(fields: [venueId], references: [id]) device Device @relation(fields: [deviceId], references: [id]) @@index([venueId, occurredAt]) @@index([venueId, type, occurredAt]) } // The terminal's product catalog with its stock, as last reported by the terminal. The terminal owns the // stock: it counts sales down offline, so the dashboard only changes it through StockAdjustments. model Product { venueId String // Generated by the terminal productId String name String category String? priceInCents Int // Null when the terminal doesn't track stock for this product. Can be negative: a sale is never blocked. stock Int? // Dashboard-only setting, never sent to or overwritten by the terminal lowStockThreshold Int? reportedAt DateTime venue Venue @relation(fields: [venueId], references: [id]) @@id([venueId, productId]) } // A stock change requested from the dashboard. It is sent to the terminal on every sync until the terminal // reports it applied or rejected. model StockAdjustment { id String @id @default(uuid()) venueId String productId String productName String // add (signed: a delivery, or breakage when negative) | set (a stock count) type String quantity Int note String? requestedBy String requestedById String // pending | applied | rejected | cancelled status String @default("pending") // The terminal's stock right after applying it stockAfter Int? result String? createdAt DateTime @default(now()) processedAt DateTime? venue Venue @relation(fields: [venueId], references: [id]) @@index([venueId, status, createdAt]) @@index([venueId, productId, createdAt]) }