fix: alot
CI / build-and-test (push) Canceled after 0s

This commit is contained in:
2026-10-19 21:41:07 +02:00
parent df90cf8a18
commit fa38ac7617
70 changed files with 1389 additions and 1023 deletions
@@ -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";
+28 -24
View File
@@ -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])
}
}
-158
View File
@@ -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
View File
@@ -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());