358 lines
12 KiB
Plaintext
358 lines
12 KiB
Plaintext
generator client {
|
|
provider = "prisma-client"
|
|
output = "../src/generated/prisma"
|
|
moduleFormat = "cjs"
|
|
// Compiled to CommonJS by tsc: imports without an extension. Set, not inferred: the client is generated during
|
|
// install, before the tsconfig it would be inferred from is there
|
|
importFileExtension = ""
|
|
}
|
|
|
|
datasource db {
|
|
provider = "postgresql"
|
|
}
|
|
|
|
enum Role {
|
|
admin
|
|
manager
|
|
superadmin
|
|
}
|
|
|
|
enum PaymentMethod {
|
|
cash
|
|
payconiq
|
|
other
|
|
}
|
|
|
|
enum StockMovementType {
|
|
add
|
|
count
|
|
}
|
|
|
|
enum ActorType {
|
|
user
|
|
device
|
|
system
|
|
}
|
|
|
|
enum TerminalEventType {
|
|
tab_opened
|
|
tab_renamed
|
|
tab_deleted
|
|
line_added
|
|
line_removed
|
|
settled
|
|
stock_set
|
|
product_created
|
|
product_updated
|
|
product_archived
|
|
product_restored
|
|
}
|
|
|
|
model Venue {
|
|
id String @id @default(uuid()) @db.Uuid
|
|
name String
|
|
// Who gets an email for every tab paid in cash, on a terminal or on the dashboard
|
|
cashPaymentEmails String[] @default([])
|
|
createdAt DateTime @default(now()) @db.Timestamptz(3)
|
|
|
|
users User[]
|
|
devices Device[]
|
|
registrationCodes RegistrationCode[]
|
|
customers Customer[]
|
|
tabs Tab[]
|
|
settlements Settlement[]
|
|
units TabUnit[]
|
|
products Product[]
|
|
stockMovements StockMovement[]
|
|
tabEvents TabEvent[]
|
|
reminders Reminder[]
|
|
auditLog AuditLog[]
|
|
}
|
|
|
|
// A venue's admin or manager, or a superadmin, who runs KoolTap itself and belongs to no venue: they view a venue to
|
|
// act in it as its admin
|
|
model User {
|
|
id String @id @default(uuid()) @db.Uuid
|
|
// Null exactly for a superadmin
|
|
venueId String? @db.Uuid
|
|
// Stored lowercased
|
|
email String @unique
|
|
passwordHash String
|
|
name String
|
|
role Role @default(manager)
|
|
// Access tokens issued before this are refused, so a forced logout applies at once instead of when they expire
|
|
sessionsRevokedAt DateTime? @db.Timestamptz(3)
|
|
createdAt DateTime @default(now()) @db.Timestamptz(3)
|
|
|
|
venue Venue? @relation(fields: [venueId], references: [id], onDelete: Cascade)
|
|
refreshTokens RefreshToken[]
|
|
settlements Settlement[]
|
|
stockMovements StockMovement[]
|
|
|
|
@@index([venueId])
|
|
}
|
|
|
|
model RefreshToken {
|
|
id String @id @default(uuid()) @db.Uuid
|
|
userId String @db.Uuid
|
|
tokenHash String @unique
|
|
expiresAt DateTime @db.Timestamptz(3)
|
|
revokedAt DateTime? @db.Timestamptz(3)
|
|
createdAt DateTime @default(now()) @db.Timestamptz(3)
|
|
|
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
|
|
@@index([userId])
|
|
}
|
|
|
|
// A bar terminal, paired with a registration code
|
|
model Device {
|
|
id String @id @default(uuid()) @db.Uuid
|
|
venueId String @db.Uuid
|
|
name String
|
|
tokenHash String @unique
|
|
tokenPrefix String
|
|
tokenExpiresAt DateTime @db.Timestamptz(3)
|
|
lastSeenAt DateTime @default(now()) @db.Timestamptz(3)
|
|
revokedAt DateTime? @db.Timestamptz(3)
|
|
createdAt DateTime @default(now()) @db.Timestamptz(3)
|
|
|
|
venue Venue @relation(fields: [venueId], references: [id], onDelete: Cascade)
|
|
tabEvents TabEvent[]
|
|
settlements Settlement[]
|
|
|
|
@@index([venueId, revokedAt])
|
|
}
|
|
|
|
model RegistrationCode {
|
|
id String @id @default(uuid()) @db.Uuid
|
|
venueId String @db.Uuid
|
|
code String @unique
|
|
expiresAt DateTime @db.Timestamptz(3)
|
|
usedAt DateTime? @db.Timestamptz(3)
|
|
createdAt DateTime @default(now()) @db.Timestamptz(3)
|
|
|
|
venue Venue @relation(fields: [venueId], references: [id], onDelete: Cascade)
|
|
|
|
@@index([venueId])
|
|
}
|
|
|
|
// A regular of the venue. Tabs are matched to customers by name: the next sync opens a tab for a customer that has
|
|
// none, and the customer's email is used for reminders
|
|
model Customer {
|
|
id String @id @default(uuid()) @db.Uuid
|
|
venueId String @db.Uuid
|
|
name String
|
|
// Lowercased, whitespace-collapsed name: customers are matched on this everywhere
|
|
nameKey String
|
|
email String?
|
|
position Int @default(0)
|
|
// Null until the next sync opens their tab
|
|
tabOpenedAt DateTime? @db.Timestamptz(3)
|
|
createdAt DateTime @default(now()) @db.Timestamptz(3)
|
|
|
|
venue Venue @relation(fields: [venueId], references: [id], onDelete: Cascade)
|
|
reminders Reminder[]
|
|
|
|
@@unique([venueId, nameKey])
|
|
}
|
|
|
|
// An open tab: the source of truth the terminals replace their own tabs with on every sync. Tabs are never closed,
|
|
// only emptied by settlements; deleting one drops the drinks still on it
|
|
model Tab {
|
|
// Generated by the terminal that opened it, or by the server for a customer's tab
|
|
id String @id @db.Uuid
|
|
venueId String @db.Uuid
|
|
customerName String
|
|
openedAt DateTime @db.Timestamptz(3)
|
|
// Bumped by every change, so the dashboard can refuse to settle a tab that changed since it was shown
|
|
version Int @default(0)
|
|
updatedAt DateTime @db.Timestamptz(3)
|
|
|
|
venue Venue @relation(fields: [venueId], references: [id], onDelete: Cascade)
|
|
units TabUnit[]
|
|
|
|
@@index([venueId])
|
|
}
|
|
|
|
// A paid tab, settled 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 @db.Uuid
|
|
sequence Int @unique @default(autoincrement())
|
|
venueId String @db.Uuid
|
|
// The tab may be deleted since
|
|
tabId String @db.Uuid
|
|
customerName String
|
|
settledAt DateTime @db.Timestamptz(3)
|
|
paymentMethod PaymentMethod
|
|
// What was charged; the units are what the server had on the tab
|
|
amountInCents Int
|
|
// Who settled it: a terminal, or a dashboard user
|
|
deviceId String? @db.Uuid
|
|
settledByUserId String? @db.Uuid
|
|
createdAt DateTime @default(now()) @db.Timestamptz(3)
|
|
|
|
venue Venue @relation(fields: [venueId], references: [id], onDelete: Cascade)
|
|
device Device? @relation(fields: [deviceId], references: [id], onDelete: SetNull)
|
|
settledBy User? @relation(fields: [settledByUserId], references: [id], onDelete: SetNull)
|
|
units TabUnit[]
|
|
|
|
@@index([venueId, sequence])
|
|
@@index([venueId, settledAt])
|
|
}
|
|
|
|
// One drink ever rung up. It is on an open tab until a settlement takes it; a drink taken off again keeps its
|
|
// removedAt and moves with the settlement too. Lines are the units sharing a lineId
|
|
model TabUnit {
|
|
// `<line_added event id>:<index>`, derived the same way on the terminals
|
|
id String @id
|
|
venueId String @db.Uuid
|
|
// Exactly one of tabId and settlementId is set
|
|
tabId String? @db.Uuid
|
|
settlementId String? @db.Uuid
|
|
lineId String
|
|
productId String? @db.Uuid
|
|
// As rung up: a later change to the product doesn't change what was sold
|
|
productName String
|
|
unitPriceInCents Int
|
|
// The order on the terminal it was rung up in; null only for drinks settled on a terminal the server didn't have
|
|
orderId String?
|
|
addedAt DateTime @db.Timestamptz(3)
|
|
removedAt DateTime? @db.Timestamptz(3)
|
|
|
|
venue Venue @relation(fields: [venueId], references: [id], onDelete: Cascade)
|
|
tab Tab? @relation(fields: [tabId], references: [id], onDelete: Cascade)
|
|
settlement Settlement? @relation(fields: [settlementId], references: [id], onDelete: Cascade)
|
|
product Product? @relation(fields: [productId], references: [id], onDelete: SetNull)
|
|
|
|
@@index([tabId])
|
|
@@index([settlementId])
|
|
@@index([venueId, addedAt])
|
|
@@index([productId])
|
|
}
|
|
|
|
// The venue's catalog and stock. The dashboard and the terminals both change products; per field the latest change
|
|
// wins. The stock follows the drinks rung up, the terminals' counts and the dashboard's stock movements
|
|
model Product {
|
|
// Generated by the server, or by the terminal that created it offline
|
|
id String @id @db.Uuid
|
|
venueId String @db.Uuid
|
|
name String
|
|
category String?
|
|
// The sell price: what the terminals charge
|
|
priceInCents Int
|
|
// What the bar pays for one. Dashboard only, the terminals never get it
|
|
buyPriceInCents Int?
|
|
// Null when stock is not tracked. Can be negative: a sale is never blocked
|
|
stock Int?
|
|
// When the stock was last counted; drinks rung up before it are already in the count
|
|
countedAt DateTime? @db.Timestamptz(3)
|
|
// Warn when the stock drops to this level
|
|
lowStockThreshold Int?
|
|
// How many the bar wants in stock. Dashboard only
|
|
desiredStock Int?
|
|
imageId String?
|
|
// Deleted: hidden on the terminals, kept because tabs and history refer to it
|
|
archivedAt DateTime? @db.Timestamptz(3)
|
|
// When each field was last changed ({ name: ISO date, ... }), so edits of different fields never overwrite each other
|
|
fieldUpdatedAt Json @default("{}")
|
|
createdAt DateTime @default(now()) @db.Timestamptz(3)
|
|
// The last change to what the terminals get (not the stock): part of the catalog version
|
|
updatedAt DateTime @db.Timestamptz(3)
|
|
|
|
venue Venue @relation(fields: [venueId], references: [id], onDelete: Cascade)
|
|
image ProductImage? @relation(fields: [imageId], references: [id], onDelete: SetNull)
|
|
units TabUnit[]
|
|
stockMovements StockMovement[]
|
|
|
|
@@index([venueId])
|
|
}
|
|
|
|
// A product picture, resized and cropped on upload and stored on disk as <id>.webp. The id is the SHA-256 of the
|
|
// bytes, so a picture's address never changes and the same picture is stored once
|
|
model ProductImage {
|
|
id String @id
|
|
contentType String
|
|
width Int
|
|
height Int
|
|
bytes Int
|
|
createdAt DateTime @default(now()) @db.Timestamptz(3)
|
|
|
|
products Product[]
|
|
}
|
|
|
|
// A stock change made on the dashboard: a delivery, breakage or count
|
|
model StockMovement {
|
|
id String @id @default(uuid()) @db.Uuid
|
|
venueId String @db.Uuid
|
|
productId String @db.Uuid
|
|
type StockMovementType
|
|
quantity Int
|
|
stockAfter Int
|
|
note String?
|
|
userId String? @db.Uuid
|
|
createdAt DateTime @default(now()) @db.Timestamptz(3)
|
|
|
|
venue Venue @relation(fields: [venueId], references: [id], onDelete: Cascade)
|
|
product Product @relation(fields: [productId], references: [id], onDelete: Cascade)
|
|
user User? @relation(fields: [userId], references: [id], onDelete: SetNull)
|
|
|
|
@@index([venueId, createdAt])
|
|
@@index([productId, createdAt])
|
|
}
|
|
|
|
// What a terminal reported, as it sent it. The id is generated by the terminal, so an event that is sent again is
|
|
// stored, and applied, only once
|
|
model TabEvent {
|
|
id String @id @db.Uuid
|
|
venueId String @db.Uuid
|
|
deviceId String @db.Uuid
|
|
type TerminalEventType
|
|
// The event as sent, after validation
|
|
payload Json
|
|
occurredAt DateTime @db.Timestamptz(3)
|
|
receivedAt DateTime @default(now()) @db.Timestamptz(3)
|
|
|
|
venue Venue @relation(fields: [venueId], references: [id], onDelete: Cascade)
|
|
device Device @relation(fields: [deviceId], references: [id], onDelete: Cascade)
|
|
|
|
@@index([venueId, occurredAt])
|
|
@@index([receivedAt])
|
|
}
|
|
|
|
model Reminder {
|
|
id String @id @default(uuid()) @db.Uuid
|
|
venueId String @db.Uuid
|
|
// Null when the customer was removed since
|
|
customerId String? @db.Uuid
|
|
customerName String
|
|
email String
|
|
tabId String @db.Uuid
|
|
amountInCents Int
|
|
sentAt DateTime @default(now()) @db.Timestamptz(3)
|
|
|
|
venue Venue @relation(fields: [venueId], references: [id], onDelete: Cascade)
|
|
customer Customer? @relation(fields: [customerId], references: [id], onDelete: SetNull)
|
|
|
|
@@index([venueId, sentAt])
|
|
@@index([customerId])
|
|
}
|
|
|
|
model AuditLog {
|
|
id String @id @default(uuid()) @db.Uuid
|
|
// Null for actions that are not about one venue (purging, signing everyone out)
|
|
venueId String? @db.Uuid
|
|
actorType ActorType
|
|
// A user or device id; null for the system
|
|
actorId String? @db.Uuid
|
|
action String
|
|
details Json?
|
|
createdAt DateTime @default(now()) @db.Timestamptz(3)
|
|
|
|
venue Venue? @relation(fields: [venueId], references: [id], onDelete: Cascade)
|
|
|
|
@@index([venueId, createdAt])
|
|
@@index([createdAt])
|
|
}
|