Files
kooltab-backoffice/apps/api/prisma/schema.prisma
T
brammie15 e7f59f7583
CI / build-and-test (push) Canceled after 0s
feat: make products server managed
2026-10-20 23:55:40 +02:00

333 lines
10 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[]
tabEvents TabEvent[]
products Product[]
stockAdjustments StockAdjustment[]
tabs Tab[]
settlements Settlement[]
// Set on the first sync after the server started holding the tabs: the tabs were then taken over from the
// terminal's own open tabs, once
tabsSeededAt DateTime?
}
model Device {
id String @id @default(uuid())
// A venue can have several terminals
venueId String
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[]
settlements Settlement[]
@@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])
}
// A request from the dashboard. close_tab used to be executed by the owning terminal; the server now settles
// the tab itself and records the command as completed right away, for the history.
model RemoteCommand {
id String @id @default(uuid())
venueId String
// Null for a command the server carried out itself
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. The id is generated by the terminal so a re-sent event is
// stored, and applied to the tab state and stock, only once.
model TabEvent {
id String @id
venueId String
deviceId String
// Null for stock_set, which is not about a tab
tabId String?
customerName String?
// tab_opened | tab_renamed | tab_deleted | line_added | line_removed | settled | stock_set
type String
lineId String?
productId 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?
// stock_set: the counted stock, null to stop tracking
stockSetTo Int?
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 venue's products and their stock. The dashboard manages the catalog and the terminals get it on every sync;
// the first terminal's own catalog was taken over once. The stock follows the terminals' line events and stock
// counts and the dashboard's StockAdjustments.
model Product {
venueId String
// Generated by the server, or by the terminal that reported it before the dashboard managed the catalog
productId String
name String
category String?
priceInCents Int
// Null when stock is not tracked for this product. Can be negative: a sale is never blocked.
stock Int?
// When the stock was last counted; line events that happened before it are already in the count
countedAt DateTime?
// Warn when the stock drops to this level
lowStockThreshold Int?
// When it was created or last changed
reportedAt DateTime
// Deleted on the dashboard: hidden on the terminals, kept because tabs and history refer to it
archivedAt DateTime?
venue Venue @relation(fields: [venueId], references: [id])
@@id([venueId, productId])
}
// A stock change made on the dashboard. The server applies it right away. Older ones may still have been applied
// by a terminal, which reported the outcome.
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 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])
}
// An open tab of the venue: the source of truth the terminals replace their own tabs with on every sync. Tabs are
// never closed, only emptied by a settlement; tab_deleted removes one.
model Tab {
venueId String
// Generated by the terminal that opened it
id String
customerName String
openedAt DateTime
// Bumped by every change, so the dashboard can refuse to settle a tab that changed since it was shown
version Int @default(0)
// [{ id, productId, productName, unitPriceInCents, units: [{ id, addedAt, removedAt }] }]
lines Json
updatedAt DateTime @updatedAt
venue Venue @relation(fields: [venueId], references: [id])
@@id([venueId, id])
}
// A settlement made on a terminal (id = the settled event's id) or on the dashboard. Terminals read them as a feed
// after their cursor, which is the sequence number.
model Settlement {
id String @id
sequence Int @unique @default(autoincrement())
venueId String
tabId String
customerName String
settledAt DateTime
// cash | payconiq | payment_pending | other
paymentMethod String
amountInCents Int
// The settled lines, in the shape of Tab.lines with each line's settled and removed units
lines Json
// Who settled it: a terminal, or a dashboard user
deviceId String?
requestedBy String?
commandId String?
createdAt DateTime @default(now())
venue Venue @relation(fields: [venueId], references: [id])
device Device? @relation(fields: [deviceId], references: [id])
@@index([venueId, sequence])
@@index([venueId, settledAt])
}