/** * Dev/demo fixtures. Run with `npm run prisma:seed` (requires a reachable * DATABASE_URL — see .env.example). Intentionally small: enough to exercise * every relation in the schema once, not a full demo dataset. */ import "dotenv/config"; import { PrismaClient, Permission, Weekday, RewardType } from "../generated/prisma/client"; import { PrismaPg } from "@prisma/adapter-pg"; import bcrypt from "bcryptjs"; /** * All dates below are computed relative to "now" so the seed stays useful * (an upcoming appointment stays upcoming) no matter when it's re-run. */ function daysFromNow(days: number, hour: number, minute = 0): Date { const date = new Date(); date.setDate(date.getDate() + days); date.setHours(hour, minute, 0, 0); return date; } const adapter = new PrismaPg({ connectionString: process.env.DATABASE_URL }); const prisma = new PrismaClient({ adapter }); const OWNER_PERMISSIONS: Permission[] = [ Permission.MANAGE_BUSINESS_PROFILE, Permission.MANAGE_BOOKING_RULES, Permission.MANAGE_SERVICES, Permission.MANAGE_STAFF, Permission.MANAGE_LOYALTY_PROGRAMME, Permission.MANAGE_NOTIFICATION_SETTINGS, Permission.VIEW_ALL_STAFF_APPOINTMENTS, Permission.MANAGE_OWN_APPOINTMENTS, Permission.CHECK_IN_CUSTOMERS, Permission.MANAGE_LOYALTY_STAMPS, Permission.MANAGE_OWN_AVAILABILITY, Permission.VIEW_CUSTOMER_DIRECTORY, Permission.BOOK_ON_BEHALF_OF_CUSTOMER, ]; const BARBER_PERMISSIONS: Permission[] = [ Permission.MANAGE_OWN_APPOINTMENTS, Permission.CHECK_IN_CUSTOMERS, Permission.MANAGE_LOYALTY_STAMPS, Permission.MANAGE_OWN_AVAILABILITY, Permission.VIEW_CUSTOMER_DIRECTORY, Permission.BOOK_ON_BEHALF_OF_CUSTOMER, ]; const WEEKDAYS: Weekday[] = [ Weekday.MONDAY, Weekday.TUESDAY, Weekday.WEDNESDAY, Weekday.THURSDAY, Weekday.FRIDAY, Weekday.SATURDAY, ]; async function main() { const business = await prisma.business.upsert({ where: { slug: "blake-heritage-barbershop" }, update: {}, create: { slug: "blake-heritage-barbershop", name: "Blake Heritage Barbershop", description: "Blake Heritage has come to Northwood! Check our Instagram @Blake.Heritage.Barbershop for more info.", phone: "+44 1923 000000", email: "hello@blakeheritage.co.uk", timezone: "Europe/London", currency: "GBP", locale: "en-GB", locations: { create: { addressLine1: "79 Joel Street", city: "Northwood", postalCode: "HA6 1LW", country: "GB", }, }, settings: { create: { cancellationPolicy: "Free cancellation up to 24 hours before your appointment.", lateArrivalPolicy: "Arrivals more than 10 minutes late may need to be rescheduled.", }, }, bookingRule: { create: {} }, // all defaults from the schema hours: { create: [ ...WEEKDAYS.map((dayOfWeek) => ({ dayOfWeek, opensAtMinutes: 10 * 60, closesAtMinutes: 19 * 60, })), { dayOfWeek: Weekday.SUNDAY, isClosed: true }, ], }, loyaltyProgramme: { create: { isEnabled: true, stampsRequired: 10, rewardType: RewardType.PERCENTAGE, rewardValue: 50, rewardLabel: "50% Off Any Service", termsLine: "Valid for 12 months. One reward per visit.", }, }, }, }); const ownerRole = await prisma.role.upsert({ where: { businessId_name: { businessId: business.id, name: "Owner" } }, update: {}, create: { businessId: business.id, name: "Owner", isSystem: true, permissions: { create: OWNER_PERMISSIONS.map((permission) => ({ permission })) }, }, }); const barberRole = await prisma.role.upsert({ where: { businessId_name: { businessId: business.id, name: "Barber" } }, update: {}, create: { businessId: business.id, name: "Barber", isSystem: true, permissions: { create: BARBER_PERMISSIONS.map((permission) => ({ permission })) }, }, }); const category = await prisma.serviceCategory.upsert({ where: { id: `${business.id}-senior-barber` }, update: {}, create: { id: `${business.id}-senior-barber`, businessId: business.id, name: "Hair & Beard Services — Senior Barber", }, }); const beardCategory = await prisma.serviceCategory.upsert({ where: { id: `${business.id}-beard-services` }, update: {}, create: { id: `${business.id}-beard-services`, businessId: business.id, name: "Beard Services", }, }); const [haircut, skinfade, beardTrim] = await Promise.all([ prisma.service.upsert({ where: { id: `${business.id}-haircut-style` }, update: {}, create: { id: `${business.id}-haircut-style`, businessId: business.id, categoryId: category.id, name: "Haircut & Style", durationMinutes: 30, price: 22, }, }), prisma.service.upsert({ where: { id: `${business.id}-skinfade-style` }, update: {}, create: { id: `${business.id}-skinfade-style`, businessId: business.id, categoryId: category.id, name: "Skinfade / Taper & Style", durationMinutes: 30, price: 25, }, }), prisma.service.upsert({ where: { id: `${business.id}-beard-trim-shape` }, update: {}, create: { id: `${business.id}-beard-trim-shape`, businessId: business.id, categoryId: beardCategory.id, name: "Beard Trim & Shape", durationMinutes: 20, price: 15, }, }), ]); const ownerPasswordHash = await bcrypt.hash("password123", 12); const ownerUser = await prisma.user.upsert({ where: { email: "marcus@blakeheritage.co.uk" }, update: {}, create: { email: "marcus@blakeheritage.co.uk", name: "Marcus Reed", passwordHash: ownerPasswordHash, emailVerified: new Date(), staffProfile: { create: { businessId: business.id, roleId: ownerRole.id, bio: "Master barber, owner.", workingHours: { create: WEEKDAYS.map((dayOfWeek) => ({ dayOfWeek, startMinutes: 10 * 60, endMinutes: 19 * 60, })), }, services: { create: [{ serviceId: haircut.id }, { serviceId: skinfade.id }, { serviceId: beardTrim.id }], }, // Daily lunch break, Mon-Sat — exercises the "unavailable slot mid-day" path. breaks: { create: [ { label: "Lunch", startMinutes: 13 * 60, endMinutes: 13 * 60 + 30, days: { create: WEEKDAYS.map((dayOfWeek) => ({ dayOfWeek })) }, }, ], }, }, }, }, include: { staffProfile: true }, }); const barberPasswordHash = await bcrypt.hash("password123", 12); const barberUser = await prisma.user.upsert({ where: { email: "leo@blakeheritage.co.uk" }, update: {}, create: { email: "leo@blakeheritage.co.uk", name: "Leo Vance", passwordHash: barberPasswordHash, emailVerified: new Date(), staffProfile: { create: { businessId: business.id, roleId: barberRole.id, bio: "Senior specialist.", workingHours: { create: WEEKDAYS.map((dayOfWeek) => ({ dayOfWeek, startMinutes: 10 * 60, endMinutes: 19 * 60, })), }, services: { create: [{ serviceId: haircut.id }] }, // A week of approved leave starting next week — exercises the "staff on time off" path. timeOff: { create: [ { startDate: daysFromNow(7, 0), endDate: daysFromNow(11, 0), reason: "Annual leave", }, ], }, }, }, }, include: { staffProfile: true }, }); // Second barber: different days off (closed Sun+Mon rather than just Sun) // and a different service mix — exercises "not every barber can do every // service" and "any barber" resolution across staff with different rotas. const secondBarberPasswordHash = await bcrypt.hash("password123", 12); const secondBarberUser = await prisma.user.upsert({ where: { email: "jamie@blakeheritage.co.uk" }, update: {}, create: { email: "jamie@blakeheritage.co.uk", name: "Jamie Wallace", passwordHash: secondBarberPasswordHash, emailVerified: new Date(), staffProfile: { create: { businessId: business.id, roleId: barberRole.id, bio: "Fades and beard specialist.", workingHours: { create: [ { dayOfWeek: Weekday.TUESDAY, startMinutes: 9 * 60, endMinutes: 17 * 60 }, { dayOfWeek: Weekday.WEDNESDAY, startMinutes: 9 * 60, endMinutes: 17 * 60 }, { dayOfWeek: Weekday.THURSDAY, startMinutes: 9 * 60, endMinutes: 17 * 60 }, { dayOfWeek: Weekday.FRIDAY, startMinutes: 9 * 60, endMinutes: 17 * 60 }, { dayOfWeek: Weekday.SATURDAY, startMinutes: 9 * 60, endMinutes: 17 * 60 }, { dayOfWeek: Weekday.SUNDAY, isOff: true }, { dayOfWeek: Weekday.MONDAY, isOff: true }, ], }, services: { create: [{ serviceId: skinfade.id }, { serviceId: beardTrim.id }] }, }, }, }, include: { staffProfile: true }, }); const customerPasswordHash = await bcrypt.hash("password123", 12); const registeredCustomerUser = await prisma.user.upsert({ where: { email: "alex.chen@example.com" }, update: {}, create: { email: "alex.chen@example.com", name: "Alex Chen", passwordHash: customerPasswordHash, emailVerified: new Date(), customerProfile: { create: { firstName: "Alex", lastName: "Chen", email: "alex.chen@example.com", phone: "+44 7700 900123", isGuest: false, preferredBarberId: ownerUser.staffProfile?.id, }, }, }, include: { customerProfile: true }, }); // Guest customer — no linked User, matches the CustomerService guest-matching // rules (normalized email + exact phone) so re-running the booking flow with // these same details reuses this profile instead of creating a duplicate. const guestCustomer = await prisma.customerProfile.upsert({ where: { id: `${business.id}-guest-jordan-blake` }, update: {}, create: { id: `${business.id}-guest-jordan-blake`, firstName: "Jordan", lastName: "Blake", email: "jordan.blake@example.com", phone: "+44 7700 900456", isGuest: true, }, }); const registeredCustomer = registeredCustomerUser.customerProfile; if (registeredCustomer && ownerUser.staffProfile && barberUser.staffProfile) { // Upcoming, confirmed — registered customer with the owner/barber. const upcomingStart = daysFromNow(3, 11, 0); await prisma.appointment.upsert({ where: { id: `${business.id}-appt-upcoming-confirmed` }, update: {}, create: { id: `${business.id}-appt-upcoming-confirmed`, businessId: business.id, customerId: registeredCustomer.id, staffId: ownerUser.staffProfile.id, status: "CONFIRMED", startAt: upcomingStart, endAt: new Date(upcomingStart.getTime() + haircut.durationMinutes * 60_000), services: { create: [{ serviceId: haircut.id, priceAtBooking: haircut.price, durationAtBookingMinutes: haircut.durationMinutes }], }, }, }); // Completed — a past visit, used to power "Book Again" and appointment history. const completedStart = daysFromNow(-14, 15, 0); await prisma.appointment.upsert({ where: { id: `${business.id}-appt-completed` }, update: {}, create: { id: `${business.id}-appt-completed`, businessId: business.id, customerId: registeredCustomer.id, staffId: barberUser.staffProfile.id, status: "COMPLETED", startAt: completedStart, endAt: new Date(completedStart.getTime() + haircut.durationMinutes * 60_000), services: { create: [{ serviceId: haircut.id, priceAtBooking: haircut.price, durationAtBookingMinutes: haircut.durationMinutes }], }, }, }); } if (secondBarberUser.staffProfile) { // Cancelled — guest customer, cancelled by the guest themselves (no // cancelledByUserId, matching the Phase 4 guest-cancellation capability). const cancelledStart = daysFromNow(5, 10, 0); await prisma.appointment.upsert({ where: { id: `${business.id}-appt-cancelled` }, update: {}, create: { id: `${business.id}-appt-cancelled`, businessId: business.id, customerId: guestCustomer.id, staffId: secondBarberUser.staffProfile.id, status: "CANCELLED", startAt: cancelledStart, endAt: new Date(cancelledStart.getTime() + (skinfade.durationMinutes + beardTrim.durationMinutes) * 60_000), cancellationReason: "Schedule conflict", cancelledAt: daysFromNow(-1, 9, 0), services: { create: [ { serviceId: skinfade.id, priceAtBooking: skinfade.price, durationAtBookingMinutes: skinfade.durationMinutes }, { serviceId: beardTrim.id, priceAtBooking: beardTrim.price, durationAtBookingMinutes: beardTrim.durationMinutes }, ], }, }, }); } console.log("Seed complete:", { business: business.name, roles: [ownerRole.name, barberRole.name], services: [haircut.name, skinfade.name, beardTrim.name], staff: [ownerUser.name, barberUser.name, secondBarberUser.name], customers: [registeredCustomerUser.name, `${guestCustomer.firstName} ${guestCustomer.lastName} (guest)`], }); } main() .catch((error) => { console.error(error); process.exitCode = 1; }) .finally(async () => { await prisma.$disconnect(); });