@@ -0,0 +1,27 @@
|
||||
-- PlannedTab: match customers case-insensitively
|
||||
ALTER TABLE "PlannedTab" ADD COLUMN "customerNameKey" TEXT;
|
||||
|
||||
UPDATE "PlannedTab" SET "customerNameKey" = lower(trim(regexp_replace("customerName", '\s+', ' ', 'g')));
|
||||
|
||||
-- Names that only differ in case or spacing keep the oldest row as the match; later ones get a unique key
|
||||
UPDATE "PlannedTab" p SET "customerNameKey" = p."customerNameKey" || '#' || p."id"
|
||||
WHERE EXISTS (
|
||||
SELECT 1 FROM "PlannedTab" o
|
||||
WHERE o."venueId" = p."venueId"
|
||||
AND o."customerNameKey" = p."customerNameKey"
|
||||
AND (o."createdAt", o."id") < (p."createdAt", p."id")
|
||||
);
|
||||
|
||||
ALTER TABLE "PlannedTab" ALTER COLUMN "customerNameKey" SET NOT NULL;
|
||||
|
||||
DROP INDEX "PlannedTab_venueId_customerName_key";
|
||||
|
||||
CREATE UNIQUE INDEX "PlannedTab_venueId_customerNameKey_key" ON "PlannedTab"("venueId", "customerNameKey");
|
||||
|
||||
-- RemoteCommand: keep who the tab belonged to, the tab itself disappears once closed
|
||||
ALTER TABLE "RemoteCommand" ADD COLUMN "customerName" TEXT,
|
||||
ADD COLUMN "amountInCents" INTEGER;
|
||||
|
||||
UPDATE "RemoteCommand" c SET "customerName" = s."customerName", "amountInCents" = s."totalInCents"
|
||||
FROM "OpenTabSnapshot" s
|
||||
WHERE s."deviceId" = c."deviceId" AND s."tabId" = c."targetTabId";
|
||||
@@ -34,7 +34,7 @@ model Device {
|
||||
revokedAt DateTime?
|
||||
createdAt DateTime @default(now())
|
||||
|
||||
venue Venue @relation(fields: [venueId], references: [id])
|
||||
venue Venue @relation(fields: [venueId], references: [id])
|
||||
tabSnapshots OpenTabSnapshot[]
|
||||
tabOwnerships TabOwnership[]
|
||||
remoteCommands RemoteCommand[]
|
||||
@@ -57,12 +57,12 @@ model ClientUser {
|
||||
}
|
||||
|
||||
model RegistrationCode {
|
||||
id String @id @default(uuid())
|
||||
id String @id @default(uuid())
|
||||
venueId String
|
||||
code String @unique
|
||||
code String @unique
|
||||
expiresAt DateTime
|
||||
usedAt DateTime?
|
||||
createdAt DateTime @default(now())
|
||||
createdAt DateTime @default(now())
|
||||
|
||||
venue Venue @relation(fields: [venueId], references: [id])
|
||||
|
||||
@@ -106,6 +106,8 @@ model RemoteCommand {
|
||||
venueId String
|
||||
deviceId String
|
||||
targetTabId String
|
||||
customerName String?
|
||||
amountInCents Int?
|
||||
type String @default("close_tab")
|
||||
expectedVersion Int
|
||||
paymentMethod String
|
||||
@@ -138,44 +140,46 @@ model AuditLog {
|
||||
}
|
||||
|
||||
model RefreshToken {
|
||||
id String @id @default(uuid())
|
||||
id String @id @default(uuid())
|
||||
userId String
|
||||
tokenHash String @unique
|
||||
tokenHash String @unique
|
||||
expiresAt DateTime
|
||||
revokedAt DateTime?
|
||||
createdAt DateTime @default(now())
|
||||
createdAt DateTime @default(now())
|
||||
|
||||
@@index([userId])
|
||||
}
|
||||
|
||||
model PlannedTab {
|
||||
id String @id @default(uuid())
|
||||
venueId String
|
||||
customerName String
|
||||
email String?
|
||||
position Int @default(0)
|
||||
createdAt DateTime @default(now())
|
||||
id String @id @default(uuid())
|
||||
venueId String
|
||||
customerName String
|
||||
// Lowercased, whitespace-collapsed name: customers are matched on this everywhere
|
||||
customerNameKey String
|
||||
email String?
|
||||
position Int @default(0)
|
||||
createdAt DateTime @default(now())
|
||||
// Set once the terminal reports an open tab with this name; it is then no longer sent to devices
|
||||
deliveredAt DateTime?
|
||||
deliveredAt DateTime?
|
||||
|
||||
venue Venue @relation(fields: [venueId], references: [id])
|
||||
|
||||
@@unique([venueId, customerName])
|
||||
@@unique([venueId, customerNameKey])
|
||||
@@index([venueId])
|
||||
}
|
||||
|
||||
model TabReminder {
|
||||
id String @id @default(uuid())
|
||||
venueId String
|
||||
tabId String
|
||||
customerName String
|
||||
email String
|
||||
amountInCents Int
|
||||
sentAt DateTime @default(now())
|
||||
status String @default("sent")
|
||||
id String @id @default(uuid())
|
||||
venueId String
|
||||
tabId String
|
||||
customerName String
|
||||
email String
|
||||
amountInCents Int
|
||||
sentAt DateTime @default(now())
|
||||
status String @default("sent")
|
||||
|
||||
venue Venue @relation(fields: [venueId], references: [id])
|
||||
|
||||
@@index([venueId, sentAt])
|
||||
@@index([venueId, tabId])
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,158 +0,0 @@
|
||||
datasource db {
|
||||
provider = "sqlite"
|
||||
url = env("DATABASE_URL")
|
||||
}
|
||||
|
||||
generator client {
|
||||
provider = "prisma-client-js"
|
||||
}
|
||||
|
||||
model Venue {
|
||||
id String @id @default(uuid())
|
||||
name String
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
devices Device[]
|
||||
clientUsers ClientUser[]
|
||||
tabOwnerships TabOwnership[]
|
||||
remoteCommands RemoteCommand[]
|
||||
auditLogs AuditLog[]
|
||||
regCodes RegistrationCode[]
|
||||
plannedTabs PlannedTab[]
|
||||
}
|
||||
|
||||
model Device {
|
||||
id String @id @default(uuid())
|
||||
venueId String @unique
|
||||
name String
|
||||
tokenHash String @unique
|
||||
tokenPrefix String
|
||||
tokenExpiresAt DateTime
|
||||
lastSeenAt DateTime @default(now())
|
||||
revokedAt DateTime?
|
||||
createdAt DateTime @default(now())
|
||||
|
||||
venue Venue @relation(fields: [venueId], references: [id])
|
||||
tabSnapshots OpenTabSnapshot[]
|
||||
tabOwnerships TabOwnership[]
|
||||
remoteCommands RemoteCommand[]
|
||||
|
||||
@@index([venueId, revokedAt])
|
||||
}
|
||||
|
||||
model ClientUser {
|
||||
id String @id @default(uuid())
|
||||
venueId String
|
||||
email String @unique
|
||||
passwordHash String
|
||||
name String
|
||||
role String @default("viewer")
|
||||
createdAt DateTime @default(now())
|
||||
|
||||
venue Venue @relation(fields: [venueId], references: [id])
|
||||
|
||||
@@index([venueId])
|
||||
}
|
||||
|
||||
model RegistrationCode {
|
||||
id String @id @default(uuid())
|
||||
venueId String
|
||||
code String @unique
|
||||
expiresAt DateTime
|
||||
usedAt DateTime?
|
||||
createdAt DateTime @default(now())
|
||||
|
||||
venue Venue @relation(fields: [venueId], references: [id])
|
||||
|
||||
@@index([venueId])
|
||||
}
|
||||
|
||||
model OpenTabSnapshot {
|
||||
id String @id @default(uuid())
|
||||
deviceId String
|
||||
tabId String
|
||||
customerName String
|
||||
version Int
|
||||
totalInCents Int
|
||||
itemCount Int
|
||||
reportedAt DateTime
|
||||
|
||||
device Device @relation(fields: [deviceId], references: [id])
|
||||
|
||||
@@unique([deviceId, tabId])
|
||||
@@index([deviceId])
|
||||
@@index([tabId, reportedAt])
|
||||
}
|
||||
|
||||
model TabOwnership {
|
||||
tabId String
|
||||
venueId String
|
||||
deviceId String
|
||||
lastReportedAt DateTime
|
||||
|
||||
venue Venue @relation(fields: [venueId], references: [id])
|
||||
device Device @relation(fields: [deviceId], references: [id])
|
||||
|
||||
@@id([venueId, tabId])
|
||||
}
|
||||
|
||||
model RemoteCommand {
|
||||
id String @id @default(uuid())
|
||||
venueId String
|
||||
deviceId String
|
||||
targetTabId String
|
||||
type String @default("close_tab")
|
||||
expectedVersion Int
|
||||
paymentMethod String
|
||||
requestedBy String
|
||||
status String @default("pending")
|
||||
result String?
|
||||
acknowledgedAt DateTime?
|
||||
createdAt DateTime @default(now())
|
||||
expiresAt DateTime
|
||||
|
||||
venue Venue @relation(fields: [venueId], references: [id])
|
||||
device Device @relation(fields: [deviceId], references: [id])
|
||||
|
||||
@@index([deviceId, status, createdAt])
|
||||
@@index([venueId, status, createdAt])
|
||||
}
|
||||
|
||||
model AuditLog {
|
||||
id String @id @default(uuid())
|
||||
venueId String
|
||||
actorType String
|
||||
actorId String
|
||||
action String
|
||||
details String?
|
||||
createdAt DateTime @default(now())
|
||||
|
||||
venue Venue @relation(fields: [venueId], references: [id])
|
||||
|
||||
@@index([venueId, createdAt])
|
||||
}
|
||||
|
||||
model RefreshToken {
|
||||
id String @id @default(uuid())
|
||||
userId String
|
||||
tokenHash String @unique
|
||||
expiresAt DateTime
|
||||
revokedAt DateTime?
|
||||
createdAt DateTime @default(now())
|
||||
|
||||
@@index([userId])
|
||||
}
|
||||
|
||||
model PlannedTab {
|
||||
id String @id @default(uuid())
|
||||
venueId String
|
||||
customerName String
|
||||
position Int @default(0)
|
||||
createdAt DateTime @default(now())
|
||||
|
||||
venue Venue @relation(fields: [venueId], references: [id])
|
||||
|
||||
@@unique([venueId, customerName])
|
||||
@@index([venueId])
|
||||
}
|
||||
+51
-41
@@ -1,60 +1,70 @@
|
||||
const { PrismaClient } = require('@prisma/client');
|
||||
const bcrypt = require('bcrypt');
|
||||
const { v4: uuidv4 } = require('uuid');
|
||||
const crypto = require('crypto');
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
const VENUE_NAME = process.env.SETUP_VENUE_NAME || process.argv[2] || 'My Venue';
|
||||
const ADMIN_EMAIL = process.env.SETUP_ADMIN_EMAIL || process.argv[3] || 'admin@example.com';
|
||||
const ADMIN_PASSWORD = process.env.SETUP_ADMIN_PASSWORD || process.argv[4] || 'admin123';
|
||||
const args = process.argv.slice(2).filter((arg) => arg !== '--force');
|
||||
const FORCE = process.argv.includes('--force');
|
||||
const VENUE_NAME = process.env.SETUP_VENUE_NAME || args[0] || 'My Venue';
|
||||
const ADMIN_EMAIL = process.env.SETUP_ADMIN_EMAIL || args[1];
|
||||
const ADMIN_PASSWORD = process.env.SETUP_ADMIN_PASSWORD || args[2];
|
||||
|
||||
async function main() {
|
||||
console.log('=== KoolTap Production Setup ===\n');
|
||||
|
||||
// Wipe everything
|
||||
await prisma.auditLog.deleteMany();
|
||||
await prisma.remoteCommand.deleteMany();
|
||||
await prisma.openTabSnapshot.deleteMany();
|
||||
await prisma.tabOwnership.deleteMany();
|
||||
await prisma.refreshToken.deleteMany();
|
||||
await prisma.registrationCode.deleteMany();
|
||||
await prisma.device.deleteMany();
|
||||
await prisma.clientUser.deleteMany();
|
||||
await prisma.venue.deleteMany();
|
||||
console.log('✓ Cleared existing data');
|
||||
const venues = await prisma.venue.count();
|
||||
if (venues > 0 && !FORCE) {
|
||||
console.log(`The database already has ${venues} venue(s); nothing to do. Pass --force to wipe ALL data and start over.`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!ADMIN_EMAIL || !ADMIN_PASSWORD) {
|
||||
throw new Error('Usage: node prisma/setup.js "<venue name>" <admin email> <admin password> [--force]');
|
||||
}
|
||||
if (ADMIN_PASSWORD.length < 10) {
|
||||
throw new Error('The admin password must be at least 10 characters');
|
||||
}
|
||||
|
||||
// Create venue
|
||||
const venueId = uuidv4();
|
||||
await prisma.venue.create({ data: { id: venueId, name: VENUE_NAME } });
|
||||
console.log(`✓ Created venue: ${VENUE_NAME}`);
|
||||
|
||||
// Create admin user
|
||||
const hash = await bcrypt.hash(ADMIN_PASSWORD, 10);
|
||||
await prisma.clientUser.create({
|
||||
data: {
|
||||
id: uuidv4(),
|
||||
venueId,
|
||||
email: ADMIN_EMAIL,
|
||||
passwordHash: hash,
|
||||
name: 'Admin',
|
||||
role: 'admin',
|
||||
},
|
||||
});
|
||||
console.log(`✓ Created admin: ${ADMIN_EMAIL}`);
|
||||
|
||||
console.log('\n=== Setup complete ===');
|
||||
await prisma.$transaction(async (tx) => {
|
||||
await tx.auditLog.deleteMany();
|
||||
await tx.tabReminder.deleteMany();
|
||||
await tx.plannedTab.deleteMany();
|
||||
await tx.remoteCommand.deleteMany();
|
||||
await tx.openTabSnapshot.deleteMany();
|
||||
await tx.tabOwnership.deleteMany();
|
||||
await tx.refreshToken.deleteMany();
|
||||
await tx.registrationCode.deleteMany();
|
||||
await tx.device.deleteMany();
|
||||
await tx.clientUser.deleteMany();
|
||||
await tx.venue.deleteMany();
|
||||
|
||||
await tx.venue.create({ data: { id: venueId, name: VENUE_NAME } });
|
||||
await tx.clientUser.create({
|
||||
data: {
|
||||
id: uuidv4(),
|
||||
venueId,
|
||||
email: ADMIN_EMAIL,
|
||||
passwordHash: hash,
|
||||
name: 'Admin',
|
||||
role: 'admin',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
console.log('=== Setup complete ===');
|
||||
console.log(` Venue: ${VENUE_NAME}`);
|
||||
console.log(` Venue ID: ${venueId}`);
|
||||
console.log(` Admin: ${ADMIN_EMAIL}`);
|
||||
console.log(` Password: ${ADMIN_PASSWORD}`);
|
||||
console.log('\nSave the Venue ID — the Flutter app needs it to find your venue.');
|
||||
console.log('Login at https://kooltap.com with the admin credentials above.');
|
||||
|
||||
await prisma.$disconnect();
|
||||
console.log('\nLog in to the dashboard with the admin credentials you provided.');
|
||||
}
|
||||
|
||||
main().catch((err) => {
|
||||
console.error('Setup failed:', err);
|
||||
process.exit(1);
|
||||
});
|
||||
main()
|
||||
.catch((err) => {
|
||||
console.error('Setup failed:', err.message ?? err);
|
||||
process.exitCode = 1;
|
||||
})
|
||||
.finally(() => prisma.$disconnect());
|
||||
|
||||
Reference in New Issue
Block a user