323 lines
10 KiB
Plaintext
323 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[]
|
|
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])
|
|
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
|
|
// admin (the owner: users and terminals) | manager (runs the venue: products, stock, tabs, reminders) |
|
|
// superadmin (runs KoolTap itself: every venue, and the admin page)
|
|
role String @default("manager")
|
|
createdAt DateTime @default(now())
|
|
// Access tokens issued before this are refused, so a forced logout applies at once instead of when they expire
|
|
sessionsRevokedAt DateTime?
|
|
|
|
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])
|
|
}
|
|
|
|
// 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?
|
|
// product_created / product_updated: the product's fields as sent by the terminal
|
|
productData 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 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?
|
|
imageId String?
|
|
// When each field was last changed ({ name: ISO date, ... }): a change from the dashboard or an (offline)
|
|
// terminal only wins over a later one per field, so edits of different fields never overwrite each other
|
|
fieldUpdatedAt Json?
|
|
|
|
image ProductImage? @relation(fields: [imageId], references: [id])
|
|
|
|
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])
|
|
}
|
|
|
|
// A product picture, resized and cropped on upload. The id is the SHA-256 of the stored bytes, so the picture at
|
|
// an address never changes and can be cached forever, and the same picture is stored once.
|
|
model ProductImage {
|
|
id String @id
|
|
contentType String
|
|
width Int
|
|
height Int
|
|
bytes Bytes
|
|
createdAt DateTime @default(now())
|
|
|
|
products Product[]
|
|
}
|