Files
boka-gasol247/prisma/schema.prisma
2026-05-22 20:33:21 +02:00

206 lines
5.8 KiB
Plaintext

// Boka Gasol247 — Prisma schema
// SQLite for MVP. Migrating to Postgres later: change provider + DATABASE_URL.
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "sqlite"
url = env("DATABASE_URL")
}
// Singleton settings row. Always accessed with id = "singleton" so we never
// have more than one row. Add new fields as feature flags grow.
model Settings {
id String @id @default("singleton")
pickupEnabled Boolean @default(true)
updatedAt DateTime @updatedAt
}
model Admin {
id String @id @default(cuid())
email String @unique
name String
passwordHash String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model Product {
id String @id @default(cuid())
sku String @unique
nameSv String
nameEn String
descriptionSv String
descriptionEn String
// Price in öre (SEK * 100) to avoid float issues.
priceOre Int
// VAT in basis points (e.g. 2500 = 25.00%).
vatBp Int @default(2500)
active Boolean @default(true)
sortOrder Int @default(0)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
bookingItems BookingItem[]
}
model PickupSlot {
id String @id @default(cuid())
// Human label fallback (e.g. "Måndag förmiddag").
labelSv String
labelEn String
startsAt DateTime
endsAt DateTime
capacity Int @default(50)
active Boolean @default(true)
bookings Booking[]
replacements ReplacementRequest[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
// BookingStatus values (string, since SQLite has no enums):
// PENDING | CONFIRMED
// DELIVERED_PARTIAL | DELIVERED (handout out, none returned)
// RETURNED_PARTIAL | RETURNED (everything has come back)
// INVOICED (billing done, terminal)
// CANCELLED (terminal)
// Derived statuses (DELIVERED*/RETURNED*) are normally auto-set from item
// counters; INVOICED and CANCELLED are set explicitly by admin.
model Booking {
id String @id @default(cuid())
bookingNumber String @unique
status String @default("CONFIRMED")
// Contact
contactName String
email String
phone String
// Organization
orgName String
orgNumber String
// Invoice address
address String
postalCode String
city String
country String @default("SE")
// Pickup
pickupSlotId String?
pickupSlot PickupSlot? @relation(fields: [pickupSlotId], references: [id])
replacements ReplacementRequest[]
notes String?
// Snapshots in öre at booking time
subtotalOre Int
vatOre Int
totalOre Int
locale String @default("sv")
items BookingItem[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([createdAt])
@@index([status])
}
model BookingItem {
id String @id @default(cuid())
bookingId String
booking Booking @relation(fields: [bookingId], references: [id], onDelete: Cascade)
productId String
product Product @relation(fields: [productId], references: [id])
// Snapshots so historical bookings don't break if a product changes
sku String
nameSv String
nameEn String
unitPriceOre Int
vatBp Int
quantity Int
lineTotalOre Int
// Fulfillment tracking — set by admin as cylinders go out and come back.
// Invariants: 0 <= deliveredQuantity <= quantity
// 0 <= returnedQuantity <= deliveredQuantity
deliveredQuantity Int @default(0)
returnedQuantity Int @default(0)
replacements ReplacementRequest[]
@@index([bookingId])
}
// Customer-side magic-link tokens. Raw token lives only in the email;
// DB stores SHA-256 hash. Single-use + 1h expiry.
model CustomerMagicLink {
id String @id @default(cuid())
tokenHash String @unique
email String
expiresAt DateTime
usedAt DateTime?
createdAt DateTime @default(now())
@@index([email])
@@index([expiresAt])
}
// Customer-side browser sessions, cookie value is hashed before storage.
// Email is the identity — magic link gives access to all bookings with this email.
model CustomerSession {
id String @id @default(cuid())
tokenHash String @unique
email String
expiresAt DateTime
createdAt DateTime @default(now())
@@index([email])
@@index([expiresAt])
}
// ReplacementRequest values for status (string, SQLite has no enums):
// REQUESTED — customer just asked
// SCHEDULED — admin assigned a slot / confirmed
// DELIVERED — exchange done
// CANCELLED — terminal
model ReplacementRequest {
id String @id @default(cuid())
bookingId String
booking Booking @relation(fields: [bookingId], references: [id])
bookingItemId String
bookingItem BookingItem @relation(fields: [bookingItemId], references: [id])
quantity Int
notes String?
// Price snapshot at request time — a swap is its own sale (full cylinder
// price). Snapshotted so future Product price changes don't rewrite history.
sku String
nameSv String
nameEn String
unitPriceOre Int
vatBp Int
lineTotalOre Int
// Pickup slot chosen by the customer (optional — admin can override)
pickupSlotId String?
pickupSlot PickupSlot? @relation(fields: [pickupSlotId], references: [id])
status String @default("REQUESTED")
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([bookingId])
@@index([status])
@@index([createdAt])
}