feat: replacement

This commit is contained in:
Ola Malmgren
2026-05-22 20:33:21 +02:00
parent 4d705a1005
commit f19e2d4e0d
26 changed files with 1951 additions and 20 deletions

Binary file not shown.

View File

@@ -46,17 +46,18 @@ model Product {
}
model PickupSlot {
id String @id @default(cuid())
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[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
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):
@@ -91,6 +92,8 @@ model Booking {
pickupSlotId String?
pickupSlot PickupSlot? @relation(fields: [pickupSlotId], references: [id])
replacements ReplacementRequest[]
notes String?
// Snapshots in öre at booking time
@@ -131,5 +134,72 @@ model BookingItem {
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])
}