// 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]) }