Datenmodell fuer Spieler, Guthaben-Ledger und CCs
Guthaben werden als Kontobuch gefuehrt: jede Bewegung ist eine Zeile, der Stand eines Topfes ist deren Summe. Zusammengehoerige Zeilen teilen sich eine groupId, damit ein CC-Paket als Ganzes storniert werden kann; ein unique Constraint auf reversalOfId verhindert doppelte Stornos. CCs bilden ueber lineageId und predecessorId eine Redesign-Kette. Der Grund einer Deaktivierung entscheidet spaeter ueber die Reaktivierungskosten.
This commit is contained in:
@@ -0,0 +1,259 @@
|
||||
generator client {
|
||||
provider = "prisma-client-js"
|
||||
binaryTargets = ["native", "debian-openssl-3.0.x"]
|
||||
}
|
||||
|
||||
datasource db {
|
||||
provider = "postgresql"
|
||||
url = env("DATABASE_URL")
|
||||
}
|
||||
|
||||
enum Role {
|
||||
ADMIN
|
||||
EDITOR
|
||||
VIEWER
|
||||
}
|
||||
|
||||
enum CreditKind {
|
||||
CC
|
||||
REDESIGN_PAID
|
||||
REDESIGN_KULANZ
|
||||
}
|
||||
|
||||
enum CreditSource {
|
||||
CC_PACKAGE
|
||||
REDESIGN_PACKAGE
|
||||
KULANZ
|
||||
ADJUSTMENT
|
||||
CONSUME
|
||||
REVERSAL
|
||||
}
|
||||
|
||||
enum CharacterStatus {
|
||||
ACTIVE
|
||||
INACTIVE
|
||||
}
|
||||
|
||||
enum InactiveReason {
|
||||
PLAYER_INACTIVITY
|
||||
REWORKED
|
||||
OTHER
|
||||
}
|
||||
|
||||
enum ProductTarget {
|
||||
CC_PACKAGE
|
||||
REDESIGN_PACKAGE
|
||||
IGNORE
|
||||
}
|
||||
|
||||
enum ImportStatus {
|
||||
PENDING
|
||||
COMMITTED
|
||||
DISCARDED
|
||||
}
|
||||
|
||||
model User {
|
||||
id Int @id @default(autoincrement())
|
||||
username String @unique
|
||||
passwordHash String
|
||||
role Role @default(EDITOR)
|
||||
totpSecret String?
|
||||
totpEnabled Boolean @default(false)
|
||||
isActive Boolean @default(true)
|
||||
mustChangePassword Boolean @default(false)
|
||||
lastLoginAt DateTime?
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
sessions Session[]
|
||||
auditEntries AuditLog[]
|
||||
ledgerEntries CreditLedger[] @relation("LedgerCreatedBy")
|
||||
charactersCreated Character[] @relation("CharacterCreatedBy")
|
||||
imports Import[]
|
||||
}
|
||||
|
||||
model Session {
|
||||
id String @id @default(uuid())
|
||||
tokenHash String @unique
|
||||
userId Int
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
expiresAt DateTime
|
||||
createdAt DateTime @default(now())
|
||||
lastSeenAt DateTime @default(now())
|
||||
ip String?
|
||||
userAgent String?
|
||||
|
||||
@@index([userId])
|
||||
@@index([expiresAt])
|
||||
}
|
||||
|
||||
model Player {
|
||||
id Int @id @default(autoincrement())
|
||||
displayName String
|
||||
steamId String? @unique
|
||||
forumName String?
|
||||
notes String?
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
ledger CreditLedger[]
|
||||
characters Character[]
|
||||
orders ImportedOrder[]
|
||||
|
||||
@@index([displayName])
|
||||
}
|
||||
|
||||
/// Kontobuch: jede Guthaben-Bewegung ist eine Zeile (+1 Zugang, -1 Verbrauch).
|
||||
/// Der offene Stand eines Topfes ist die Summe der amounts je kind.
|
||||
model CreditLedger {
|
||||
id Int @id @default(autoincrement())
|
||||
player Player @relation(fields: [playerId], references: [id], onDelete: Cascade)
|
||||
|
||||
playerId Int
|
||||
kind CreditKind
|
||||
amount Int
|
||||
source CreditSource
|
||||
|
||||
/// Klammert zusammengehoerige Zeilen: ein CC-Paket schreibt zwei Zeilen
|
||||
/// mit derselben groupId, damit ein Storno beide zurueckholt.
|
||||
groupId String @default(uuid())
|
||||
|
||||
characterId Int?
|
||||
character Character? @relation(fields: [characterId], references: [id], onDelete: SetNull)
|
||||
|
||||
importId Int?
|
||||
import Import? @relation(fields: [importId], references: [id], onDelete: SetNull)
|
||||
|
||||
note String?
|
||||
|
||||
/// Storno erfolgt als Gegenbuchung, nicht als Loeschung. Das @unique
|
||||
/// verhindert, dass eine Zeile zweimal storniert wird.
|
||||
reversalOfId Int? @unique
|
||||
reversalOf CreditLedger? @relation("LedgerReversal", fields: [reversalOfId], references: [id], onDelete: NoAction, onUpdate: NoAction)
|
||||
reversedBy CreditLedger? @relation("LedgerReversal")
|
||||
|
||||
createdById Int?
|
||||
createdBy User? @relation("LedgerCreatedBy", fields: [createdById], references: [id], onDelete: SetNull)
|
||||
createdAt DateTime @default(now())
|
||||
|
||||
@@index([playerId])
|
||||
@@index([playerId, kind])
|
||||
@@index([groupId])
|
||||
@@index([createdAt])
|
||||
}
|
||||
|
||||
model Character {
|
||||
id Int @id @default(autoincrement())
|
||||
playerId Int
|
||||
player Player @relation(fields: [playerId], references: [id], onDelete: Cascade)
|
||||
name String
|
||||
loreText String?
|
||||
status CharacterStatus @default(ACTIVE)
|
||||
inactiveReason InactiveReason?
|
||||
|
||||
/// Alle CCs einer Redesign-Kette teilen sich die lineageId.
|
||||
lineageId String @default(uuid())
|
||||
predecessorId Int?
|
||||
predecessor Character? @relation("CharacterLineage", fields: [predecessorId], references: [id], onDelete: NoAction, onUpdate: NoAction)
|
||||
successors Character[] @relation("CharacterLineage")
|
||||
|
||||
ledgerEntries CreditLedger[]
|
||||
|
||||
createdById Int?
|
||||
createdBy User? @relation("CharacterCreatedBy", fields: [createdById], references: [id], onDelete: SetNull)
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
@@index([playerId])
|
||||
@@index([playerId, status])
|
||||
@@index([lineageId])
|
||||
}
|
||||
|
||||
model Import {
|
||||
id Int @id @default(autoincrement())
|
||||
filename String
|
||||
status ImportStatus @default(PENDING)
|
||||
rowCount Int @default(0)
|
||||
bookedCount Int @default(0)
|
||||
mapping Json?
|
||||
createdAt DateTime @default(now())
|
||||
committedAt DateTime?
|
||||
|
||||
importedById Int?
|
||||
importedBy User? @relation(fields: [importedById], references: [id], onDelete: SetNull)
|
||||
|
||||
rows ImportRow[]
|
||||
orders ImportedOrder[]
|
||||
ledgerEntries CreditLedger[]
|
||||
|
||||
@@index([status])
|
||||
}
|
||||
|
||||
model ImportRow {
|
||||
id Int @id @default(autoincrement())
|
||||
importId Int
|
||||
import Import @relation(fields: [importId], references: [id], onDelete: Cascade)
|
||||
|
||||
lineNo Int
|
||||
externalId String?
|
||||
playerKey String?
|
||||
playerName String?
|
||||
productName String?
|
||||
purchasedAt DateTime?
|
||||
|
||||
target ProductTarget?
|
||||
resolvedPlayerId Int?
|
||||
|
||||
/// Was beim Buchen passieren wuerde: BOOK, CREATE_PLAYER_AND_BOOK,
|
||||
/// SKIP_DUPLICATE, SKIP_IGNORED, SKIP_UNMAPPED, SKIP_NO_PLAYER
|
||||
plan String
|
||||
raw Json
|
||||
|
||||
@@index([importId])
|
||||
}
|
||||
|
||||
/// Dedup-Anker: eine bereits gebuchte Bestell-ID wird nie ein zweites Mal gebucht.
|
||||
model ImportedOrder {
|
||||
id Int @id @default(autoincrement())
|
||||
externalId String @unique
|
||||
importId Int
|
||||
import Import @relation(fields: [importId], references: [id], onDelete: Cascade)
|
||||
playerId Int
|
||||
player Player @relation(fields: [playerId], references: [id], onDelete: Cascade)
|
||||
target ProductTarget
|
||||
groupId String
|
||||
createdAt DateTime @default(now())
|
||||
|
||||
@@index([playerId])
|
||||
}
|
||||
|
||||
model ProductMapping {
|
||||
id Int @id @default(autoincrement())
|
||||
matchPattern String
|
||||
target ProductTarget
|
||||
priority Int @default(100)
|
||||
isActive Boolean @default(true)
|
||||
note String?
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
@@index([priority])
|
||||
}
|
||||
|
||||
model AuditLog {
|
||||
id Int @id @default(autoincrement())
|
||||
actorUserId Int?
|
||||
actor User? @relation(fields: [actorUserId], references: [id], onDelete: SetNull)
|
||||
/// Name wird mitgeschrieben, damit der Eintrag lesbar bleibt, wenn der Account weg ist.
|
||||
actorName String?
|
||||
action String
|
||||
targetType String
|
||||
targetId String?
|
||||
details Json?
|
||||
ip String?
|
||||
createdAt DateTime @default(now())
|
||||
|
||||
@@index([createdAt])
|
||||
@@index([targetType, targetId])
|
||||
@@index([actorUserId])
|
||||
}
|
||||
Reference in New Issue
Block a user