Ein CC, der nicht redesignt wurde, existiert unveraendert weiter und kommt
deshalb immer gratis zurueck - egal ob der Spieler inaktiv war oder die
Einheit voruebergehend aus dem Spiel genommen wurde. Bisher kostete der Fall
OTHER ein Guthaben, was die Regel unnoetig verkompliziert hat.
Damit der zweite Fall brauchbar wird, haelt inactiveNote den Grund im Klartext
fest ("Einheit bis Maerz entfernt"); die Notiz steht an der CC-Karte und wird
beim Reaktivieren wieder geleert.
262 lines
6.9 KiB
Plaintext
262 lines
6.9 KiB
Plaintext
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?
|
|
/// Klartext zum Grund, etwa "Einheit bis Maerz entfernt". Nur zur Anzeige.
|
|
inactiveNote String?
|
|
|
|
/// 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])
|
|
}
|