feat: add plannedTabs
This commit is contained in:
@@ -1,152 +0,0 @@
|
||||
-- CreateTable
|
||||
CREATE TABLE "Venue" (
|
||||
"id" TEXT NOT NULL PRIMARY KEY,
|
||||
"name" TEXT NOT NULL,
|
||||
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" DATETIME NOT NULL
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "Device" (
|
||||
"id" TEXT NOT NULL PRIMARY KEY,
|
||||
"venueId" TEXT NOT NULL,
|
||||
"name" TEXT NOT NULL,
|
||||
"tokenHash" TEXT NOT NULL,
|
||||
"tokenPrefix" TEXT NOT NULL,
|
||||
"tokenExpiresAt" DATETIME NOT NULL,
|
||||
"lastSeenAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"revokedAt" DATETIME,
|
||||
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
CONSTRAINT "Device_venueId_fkey" FOREIGN KEY ("venueId") REFERENCES "Venue" ("id") ON DELETE RESTRICT ON UPDATE CASCADE
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "ClientUser" (
|
||||
"id" TEXT NOT NULL PRIMARY KEY,
|
||||
"venueId" TEXT NOT NULL,
|
||||
"email" TEXT NOT NULL,
|
||||
"passwordHash" TEXT NOT NULL,
|
||||
"name" TEXT NOT NULL,
|
||||
"role" TEXT NOT NULL DEFAULT 'viewer',
|
||||
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
CONSTRAINT "ClientUser_venueId_fkey" FOREIGN KEY ("venueId") REFERENCES "Venue" ("id") ON DELETE RESTRICT ON UPDATE CASCADE
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "RegistrationCode" (
|
||||
"id" TEXT NOT NULL PRIMARY KEY,
|
||||
"venueId" TEXT NOT NULL,
|
||||
"code" TEXT NOT NULL,
|
||||
"expiresAt" DATETIME NOT NULL,
|
||||
"usedAt" DATETIME,
|
||||
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
CONSTRAINT "RegistrationCode_venueId_fkey" FOREIGN KEY ("venueId") REFERENCES "Venue" ("id") ON DELETE RESTRICT ON UPDATE CASCADE
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "OpenTabSnapshot" (
|
||||
"id" TEXT NOT NULL PRIMARY KEY,
|
||||
"deviceId" TEXT NOT NULL,
|
||||
"tabId" TEXT NOT NULL,
|
||||
"customerName" TEXT NOT NULL,
|
||||
"version" INTEGER NOT NULL,
|
||||
"totalInCents" INTEGER NOT NULL,
|
||||
"itemCount" INTEGER NOT NULL,
|
||||
"reportedAt" DATETIME NOT NULL,
|
||||
CONSTRAINT "OpenTabSnapshot_deviceId_fkey" FOREIGN KEY ("deviceId") REFERENCES "Device" ("id") ON DELETE RESTRICT ON UPDATE CASCADE
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "TabOwnership" (
|
||||
"tabId" TEXT NOT NULL,
|
||||
"venueId" TEXT NOT NULL,
|
||||
"deviceId" TEXT NOT NULL,
|
||||
"lastReportedAt" DATETIME NOT NULL,
|
||||
|
||||
PRIMARY KEY ("venueId", "tabId"),
|
||||
CONSTRAINT "TabOwnership_venueId_fkey" FOREIGN KEY ("venueId") REFERENCES "Venue" ("id") ON DELETE RESTRICT ON UPDATE CASCADE,
|
||||
CONSTRAINT "TabOwnership_deviceId_fkey" FOREIGN KEY ("deviceId") REFERENCES "Device" ("id") ON DELETE RESTRICT ON UPDATE CASCADE
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "RemoteCommand" (
|
||||
"id" TEXT NOT NULL PRIMARY KEY,
|
||||
"venueId" TEXT NOT NULL,
|
||||
"deviceId" TEXT NOT NULL,
|
||||
"targetTabId" TEXT NOT NULL,
|
||||
"type" TEXT NOT NULL DEFAULT 'close_tab',
|
||||
"expectedVersion" INTEGER NOT NULL,
|
||||
"paymentMethod" TEXT NOT NULL,
|
||||
"requestedBy" TEXT NOT NULL,
|
||||
"status" TEXT NOT NULL DEFAULT 'pending',
|
||||
"result" TEXT,
|
||||
"acknowledgedAt" DATETIME,
|
||||
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"expiresAt" DATETIME NOT NULL,
|
||||
CONSTRAINT "RemoteCommand_venueId_fkey" FOREIGN KEY ("venueId") REFERENCES "Venue" ("id") ON DELETE RESTRICT ON UPDATE CASCADE,
|
||||
CONSTRAINT "RemoteCommand_deviceId_fkey" FOREIGN KEY ("deviceId") REFERENCES "Device" ("id") ON DELETE RESTRICT ON UPDATE CASCADE
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "AuditLog" (
|
||||
"id" TEXT NOT NULL PRIMARY KEY,
|
||||
"venueId" TEXT NOT NULL,
|
||||
"actorType" TEXT NOT NULL,
|
||||
"actorId" TEXT NOT NULL,
|
||||
"action" TEXT NOT NULL,
|
||||
"details" TEXT,
|
||||
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
CONSTRAINT "AuditLog_venueId_fkey" FOREIGN KEY ("venueId") REFERENCES "Venue" ("id") ON DELETE RESTRICT ON UPDATE CASCADE
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "RefreshToken" (
|
||||
"id" TEXT NOT NULL PRIMARY KEY,
|
||||
"userId" TEXT NOT NULL,
|
||||
"tokenHash" TEXT NOT NULL,
|
||||
"expiresAt" DATETIME NOT NULL,
|
||||
"revokedAt" DATETIME,
|
||||
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "Device_tokenHash_key" ON "Device"("tokenHash");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "Device_venueId_revokedAt_idx" ON "Device"("venueId", "revokedAt");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "ClientUser_email_key" ON "ClientUser"("email");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "ClientUser_venueId_idx" ON "ClientUser"("venueId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "RegistrationCode_code_key" ON "RegistrationCode"("code");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "RegistrationCode_venueId_idx" ON "RegistrationCode"("venueId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "OpenTabSnapshot_deviceId_idx" ON "OpenTabSnapshot"("deviceId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "OpenTabSnapshot_tabId_reportedAt_idx" ON "OpenTabSnapshot"("tabId", "reportedAt");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "OpenTabSnapshot_deviceId_tabId_key" ON "OpenTabSnapshot"("deviceId", "tabId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "RemoteCommand_deviceId_status_createdAt_idx" ON "RemoteCommand"("deviceId", "status", "createdAt");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "RemoteCommand_venueId_status_createdAt_idx" ON "RemoteCommand"("venueId", "status", "createdAt");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "AuditLog_venueId_createdAt_idx" ON "AuditLog"("venueId", "createdAt");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "RefreshToken_tokenHash_key" ON "RefreshToken"("tokenHash");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "RefreshToken_userId_idx" ON "RefreshToken"("userId");
|
||||
@@ -0,0 +1,189 @@
|
||||
-- CreateTable
|
||||
CREATE TABLE "Venue" (
|
||||
"id" TEXT NOT NULL,
|
||||
"name" TEXT NOT NULL,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "Venue_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "Device" (
|
||||
"id" TEXT NOT NULL,
|
||||
"venueId" TEXT NOT NULL,
|
||||
"name" TEXT NOT NULL,
|
||||
"tokenHash" TEXT NOT NULL,
|
||||
"tokenPrefix" TEXT NOT NULL,
|
||||
"tokenExpiresAt" TIMESTAMP(3) NOT NULL,
|
||||
"lastSeenAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"revokedAt" TIMESTAMP(3),
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "Device_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "ClientUser" (
|
||||
"id" TEXT NOT NULL,
|
||||
"venueId" TEXT NOT NULL,
|
||||
"email" TEXT NOT NULL,
|
||||
"passwordHash" TEXT NOT NULL,
|
||||
"name" TEXT NOT NULL,
|
||||
"role" TEXT NOT NULL DEFAULT 'viewer',
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "ClientUser_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "RegistrationCode" (
|
||||
"id" TEXT NOT NULL,
|
||||
"venueId" TEXT NOT NULL,
|
||||
"code" TEXT NOT NULL,
|
||||
"expiresAt" TIMESTAMP(3) NOT NULL,
|
||||
"usedAt" TIMESTAMP(3),
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "RegistrationCode_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "OpenTabSnapshot" (
|
||||
"id" TEXT NOT NULL,
|
||||
"deviceId" TEXT NOT NULL,
|
||||
"tabId" TEXT NOT NULL,
|
||||
"customerName" TEXT NOT NULL,
|
||||
"version" INTEGER NOT NULL,
|
||||
"totalInCents" INTEGER NOT NULL,
|
||||
"itemCount" INTEGER NOT NULL,
|
||||
"reportedAt" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "OpenTabSnapshot_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "TabOwnership" (
|
||||
"tabId" TEXT NOT NULL,
|
||||
"venueId" TEXT NOT NULL,
|
||||
"deviceId" TEXT NOT NULL,
|
||||
"lastReportedAt" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "TabOwnership_pkey" PRIMARY KEY ("venueId","tabId")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "RemoteCommand" (
|
||||
"id" TEXT NOT NULL,
|
||||
"venueId" TEXT NOT NULL,
|
||||
"deviceId" TEXT NOT NULL,
|
||||
"targetTabId" TEXT NOT NULL,
|
||||
"type" TEXT NOT NULL DEFAULT 'close_tab',
|
||||
"expectedVersion" INTEGER NOT NULL,
|
||||
"paymentMethod" TEXT NOT NULL,
|
||||
"requestedBy" TEXT NOT NULL,
|
||||
"status" TEXT NOT NULL DEFAULT 'pending',
|
||||
"result" TEXT,
|
||||
"acknowledgedAt" TIMESTAMP(3),
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"expiresAt" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "RemoteCommand_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "AuditLog" (
|
||||
"id" TEXT NOT NULL,
|
||||
"venueId" TEXT NOT NULL,
|
||||
"actorType" TEXT NOT NULL,
|
||||
"actorId" TEXT NOT NULL,
|
||||
"action" TEXT NOT NULL,
|
||||
"details" TEXT,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "AuditLog_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "RefreshToken" (
|
||||
"id" TEXT NOT NULL,
|
||||
"userId" TEXT NOT NULL,
|
||||
"tokenHash" TEXT NOT NULL,
|
||||
"expiresAt" TIMESTAMP(3) NOT NULL,
|
||||
"revokedAt" TIMESTAMP(3),
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "RefreshToken_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "Device_venueId_key" ON "Device"("venueId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "Device_tokenHash_key" ON "Device"("tokenHash");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "Device_venueId_revokedAt_idx" ON "Device"("venueId", "revokedAt");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "ClientUser_email_key" ON "ClientUser"("email");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "ClientUser_venueId_idx" ON "ClientUser"("venueId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "RegistrationCode_code_key" ON "RegistrationCode"("code");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "RegistrationCode_venueId_idx" ON "RegistrationCode"("venueId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "OpenTabSnapshot_deviceId_idx" ON "OpenTabSnapshot"("deviceId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "OpenTabSnapshot_tabId_reportedAt_idx" ON "OpenTabSnapshot"("tabId", "reportedAt");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "OpenTabSnapshot_deviceId_tabId_key" ON "OpenTabSnapshot"("deviceId", "tabId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "RemoteCommand_deviceId_status_createdAt_idx" ON "RemoteCommand"("deviceId", "status", "createdAt");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "RemoteCommand_venueId_status_createdAt_idx" ON "RemoteCommand"("venueId", "status", "createdAt");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "AuditLog_venueId_createdAt_idx" ON "AuditLog"("venueId", "createdAt");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "RefreshToken_tokenHash_key" ON "RefreshToken"("tokenHash");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "RefreshToken_userId_idx" ON "RefreshToken"("userId");
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Device" ADD CONSTRAINT "Device_venueId_fkey" FOREIGN KEY ("venueId") REFERENCES "Venue"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "ClientUser" ADD CONSTRAINT "ClientUser_venueId_fkey" FOREIGN KEY ("venueId") REFERENCES "Venue"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "RegistrationCode" ADD CONSTRAINT "RegistrationCode_venueId_fkey" FOREIGN KEY ("venueId") REFERENCES "Venue"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "OpenTabSnapshot" ADD CONSTRAINT "OpenTabSnapshot_deviceId_fkey" FOREIGN KEY ("deviceId") REFERENCES "Device"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "TabOwnership" ADD CONSTRAINT "TabOwnership_venueId_fkey" FOREIGN KEY ("venueId") REFERENCES "Venue"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "TabOwnership" ADD CONSTRAINT "TabOwnership_deviceId_fkey" FOREIGN KEY ("deviceId") REFERENCES "Device"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "RemoteCommand" ADD CONSTRAINT "RemoteCommand_venueId_fkey" FOREIGN KEY ("venueId") REFERENCES "Venue"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "RemoteCommand" ADD CONSTRAINT "RemoteCommand_deviceId_fkey" FOREIGN KEY ("deviceId") REFERENCES "Device"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "AuditLog" ADD CONSTRAINT "AuditLog_venueId_fkey" FOREIGN KEY ("venueId") REFERENCES "Venue"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
@@ -0,0 +1,19 @@
|
||||
-- CreateTable
|
||||
CREATE TABLE "PlannedTab" (
|
||||
"id" TEXT NOT NULL,
|
||||
"venueId" TEXT NOT NULL,
|
||||
"customerName" TEXT NOT NULL,
|
||||
"position" INTEGER NOT NULL DEFAULT 0,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "PlannedTab_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "PlannedTab_venueId_idx" ON "PlannedTab"("venueId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "PlannedTab_venueId_customerName_key" ON "PlannedTab"("venueId", "customerName");
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "PlannedTab" ADD CONSTRAINT "PlannedTab_venueId_fkey" FOREIGN KEY ("venueId") REFERENCES "Venue"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
@@ -1,3 +1,3 @@
|
||||
# Please do not edit this file manually
|
||||
# It should be added in your version-control system (i.e. Git)
|
||||
provider = "sqlite"
|
||||
provider = "postgresql"
|
||||
@@ -19,11 +19,12 @@ model Venue {
|
||||
remoteCommands RemoteCommand[]
|
||||
auditLogs AuditLog[]
|
||||
regCodes RegistrationCode[]
|
||||
plannedTabs PlannedTab[]
|
||||
}
|
||||
|
||||
model Device {
|
||||
id String @id @default(uuid())
|
||||
venueId String
|
||||
venueId String @unique
|
||||
name String
|
||||
tokenHash String @unique
|
||||
tokenPrefix String
|
||||
@@ -141,4 +142,17 @@ model RefreshToken {
|
||||
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])
|
||||
}
|
||||
@@ -19,11 +19,12 @@ model Venue {
|
||||
remoteCommands RemoteCommand[]
|
||||
auditLogs AuditLog[]
|
||||
regCodes RegistrationCode[]
|
||||
plannedTabs PlannedTab[]
|
||||
}
|
||||
|
||||
model Device {
|
||||
id String @id @default(uuid())
|
||||
venueId String
|
||||
venueId String @unique
|
||||
name String
|
||||
tokenHash String @unique
|
||||
tokenPrefix String
|
||||
@@ -141,4 +142,17 @@ model RefreshToken {
|
||||
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])
|
||||
}
|
||||
+18
-80
@@ -55,103 +55,43 @@ async function main() {
|
||||
});
|
||||
console.log('Created viewer user: viewer@example.com / Viewer123!');
|
||||
|
||||
// Create devices
|
||||
const device1Id = uuidv4();
|
||||
const device2Id = uuidv4();
|
||||
|
||||
const token1 = 'dev_' + crypto.randomBytes(48).toString('base64url');
|
||||
const token2 = 'dev_' + crypto.randomBytes(48).toString('base64url');
|
||||
// Create one device
|
||||
const deviceId = uuidv4();
|
||||
const token = 'dev_' + crypto.randomBytes(48).toString('base64url');
|
||||
|
||||
await prisma.device.create({
|
||||
data: {
|
||||
id: device1Id,
|
||||
id: deviceId,
|
||||
venueId,
|
||||
name: 'Bar terminal 1',
|
||||
tokenHash: crypto.createHash('sha256').update(token1).digest('hex'),
|
||||
tokenPrefix: token1.substring(0, 8),
|
||||
tokenHash: crypto.createHash('sha256').update(token).digest('hex'),
|
||||
tokenPrefix: token.substring(0, 8),
|
||||
tokenExpiresAt: new Date(Date.now() + 90 * 24 * 60 * 60 * 1000),
|
||||
lastSeenAt: new Date(Date.now() - 1 * 60 * 1000), // seen 1 minute ago (online)
|
||||
lastSeenAt: new Date(Date.now() - 1 * 60 * 1000),
|
||||
},
|
||||
});
|
||||
|
||||
await prisma.device.create({
|
||||
data: {
|
||||
id: device2Id,
|
||||
venueId,
|
||||
name: 'Bar terminal 2',
|
||||
tokenHash: crypto.createHash('sha256').update(token2).digest('hex'),
|
||||
tokenPrefix: token2.substring(0, 8),
|
||||
tokenExpiresAt: new Date(Date.now() + 90 * 24 * 60 * 60 * 1000),
|
||||
lastSeenAt: new Date(Date.now() - 15 * 60 * 1000), // seen 15 minutes ago (stale)
|
||||
},
|
||||
});
|
||||
console.log(`Created device: Bar terminal 1 (${deviceId})`);
|
||||
|
||||
console.log(`Created device: Bar terminal 1 (${device1Id})`);
|
||||
console.log(`Created device: Bar terminal 2 (${device2Id})`);
|
||||
console.log(`Device 1 token: ${token1}`);
|
||||
console.log(`Device 2 token: ${token2}`);
|
||||
|
||||
// Create tab ownership and snapshots for device 1
|
||||
const tabsData = [
|
||||
{ tabId: uuidv4(), customerName: 'Jan', version: 5, totalInCents: 2450, itemCount: 3, reportedAt: new Date(Date.now() - 2 * 60 * 1000) },
|
||||
{ tabId: uuidv4(), customerName: 'Marie', version: 3, totalInCents: 1800, itemCount: 2, reportedAt: new Date(Date.now() - 5 * 60 * 1000) },
|
||||
{ tabId: uuidv4(), customerName: 'Thomas', version: 7, totalInCents: 4200, itemCount: 5, reportedAt: new Date(Date.now() - 3 * 60 * 1000) },
|
||||
{ tabId: uuidv4(), customerName: 'Lisa', version: 2, totalInCents: 900, itemCount: 1, reportedAt: new Date(Date.now() - 4 * 60 * 1000) },
|
||||
];
|
||||
|
||||
for (const tab of tabsData) {
|
||||
await prisma.tabOwnership.create({
|
||||
data: {
|
||||
tabId: tab.tabId,
|
||||
venueId,
|
||||
deviceId: device1Id,
|
||||
lastReportedAt: tab.reportedAt,
|
||||
},
|
||||
});
|
||||
|
||||
await prisma.openTabSnapshot.create({
|
||||
data: {
|
||||
deviceId: device1Id,
|
||||
tabId: tab.tabId,
|
||||
customerName: tab.customerName,
|
||||
version: tab.version,
|
||||
totalInCents: tab.totalInCents,
|
||||
itemCount: tab.itemCount,
|
||||
reportedAt: tab.reportedAt,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// Create tab ownership and snapshots for device 2 (stale)
|
||||
const staleTabs = [
|
||||
{ tabId: uuidv4(), customerName: 'Emma', version: 4, totalInCents: 3200, itemCount: 4, reportedAt: new Date(Date.now() - 16 * 60 * 1000) },
|
||||
{ tabId: uuidv4(), customerName: 'David', version: 1, totalInCents: 1500, itemCount: 2, reportedAt: new Date(Date.now() - 20 * 60 * 1000) },
|
||||
];
|
||||
|
||||
for (const tab of staleTabs) {
|
||||
for (const tab of tabsData) {
|
||||
await prisma.tabOwnership.create({
|
||||
data: {
|
||||
tabId: tab.tabId,
|
||||
venueId,
|
||||
deviceId: device2Id,
|
||||
lastReportedAt: tab.reportedAt,
|
||||
},
|
||||
data: { tabId: tab.tabId, venueId, deviceId, lastReportedAt: tab.reportedAt },
|
||||
});
|
||||
|
||||
await prisma.openTabSnapshot.create({
|
||||
data: {
|
||||
deviceId: device2Id,
|
||||
tabId: tab.tabId,
|
||||
customerName: tab.customerName,
|
||||
version: tab.version,
|
||||
totalInCents: tab.totalInCents,
|
||||
itemCount: tab.itemCount,
|
||||
reportedAt: tab.reportedAt,
|
||||
},
|
||||
data: { deviceId, tabId: tab.tabId, customerName: tab.customerName, version: tab.version, totalInCents: tab.totalInCents, itemCount: tab.itemCount, reportedAt: tab.reportedAt },
|
||||
});
|
||||
}
|
||||
|
||||
console.log('Created 6 open tabs across 2 devices');
|
||||
console.log('Created 6 open tabs');
|
||||
|
||||
// Create sample commands
|
||||
const now = new Date();
|
||||
@@ -161,7 +101,7 @@ async function main() {
|
||||
data: {
|
||||
id: uuidv4(),
|
||||
venueId,
|
||||
deviceId: device1Id,
|
||||
deviceId,
|
||||
targetTabId: tabsData[1].tabId,
|
||||
type: 'close_tab',
|
||||
expectedVersion: 3,
|
||||
@@ -180,7 +120,7 @@ async function main() {
|
||||
data: {
|
||||
id: uuidv4(),
|
||||
venueId,
|
||||
deviceId: device1Id,
|
||||
deviceId,
|
||||
targetTabId: tabsData[0].tabId,
|
||||
type: 'close_tab',
|
||||
expectedVersion: 5,
|
||||
@@ -197,8 +137,8 @@ async function main() {
|
||||
data: {
|
||||
id: uuidv4(),
|
||||
venueId,
|
||||
deviceId: device2Id,
|
||||
targetTabId: staleTabs[0].tabId,
|
||||
deviceId,
|
||||
targetTabId: tabsData[4].tabId,
|
||||
type: 'close_tab',
|
||||
expectedVersion: 4,
|
||||
paymentMethod: 'payment_pending',
|
||||
@@ -216,7 +156,7 @@ async function main() {
|
||||
data: {
|
||||
id: uuidv4(),
|
||||
venueId,
|
||||
deviceId: device1Id,
|
||||
deviceId,
|
||||
targetTabId: tabsData[2].tabId,
|
||||
type: 'close_tab',
|
||||
expectedVersion: 7,
|
||||
@@ -249,9 +189,7 @@ async function main() {
|
||||
console.log('Demo credentials:');
|
||||
console.log(' Admin: admin@example.com / Admin123!');
|
||||
console.log(' Viewer: viewer@example.com / Viewer123!');
|
||||
console.log(`\nDevice tokens (for simulator):`);
|
||||
console.log(` Bar terminal 1: ${token1}`);
|
||||
console.log(` Bar terminal 2: ${token2}`);
|
||||
console.log(`\nDevice token (for simulator): ${token}`);
|
||||
}
|
||||
|
||||
main()
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
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';
|
||||
|
||||
async function main() {
|
||||
console.log('=== KoolTab 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');
|
||||
|
||||
// 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 ===');
|
||||
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://kooltab.com with the admin credentials above.');
|
||||
|
||||
await prisma.$disconnect();
|
||||
}
|
||||
|
||||
main().catch((err) => {
|
||||
console.error('Setup failed:', err);
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -10,6 +10,7 @@ import { TabsModule } from './tabs/tabs.module';
|
||||
import { CommandsModule } from './commands/commands.module';
|
||||
import { AuditModule } from './audit/audit.module';
|
||||
import { JobsModule } from './jobs/jobs.module';
|
||||
import { PlannedTabsModule } from './planned-tabs/planned-tabs.module';
|
||||
import { HealthController } from './health.controller';
|
||||
|
||||
@Module({
|
||||
@@ -24,6 +25,7 @@ import { HealthController } from './health.controller';
|
||||
CommandsModule,
|
||||
AuditModule,
|
||||
JobsModule,
|
||||
PlannedTabsModule,
|
||||
],
|
||||
controllers: [HealthController],
|
||||
providers: [
|
||||
|
||||
@@ -132,6 +132,15 @@ export class AuthService {
|
||||
const { rawToken, hash, prefix, expiresAt } = await this.generateDeviceToken();
|
||||
const deviceId = uuidv4();
|
||||
|
||||
// One device per venue — revoke any existing device
|
||||
const existing = await this.prisma.device.findFirst({ where: { venueId, revokedAt: null } });
|
||||
if (existing) {
|
||||
await this.prisma.device.update({
|
||||
where: { id: existing.id },
|
||||
data: { revokedAt: new Date() },
|
||||
});
|
||||
}
|
||||
|
||||
await this.prisma.device.create({
|
||||
data: {
|
||||
id: deviceId,
|
||||
@@ -153,6 +162,33 @@ export class AuthService {
|
||||
return { deviceId, token: rawToken, tokenExpiresAt: expiresAt };
|
||||
}
|
||||
|
||||
async createUser(venueId: string, email: string, password: string, name: string, role: string) {
|
||||
const venue = await this.prisma.venue.findUnique({ where: { id: venueId } });
|
||||
if (!venue) {
|
||||
throw new NotFoundException({ code: 'venue_not_found', message: 'Venue not found. Your session may be stale — try logging out and back in.' });
|
||||
}
|
||||
|
||||
const existingUser = await this.prisma.clientUser.findUnique({ where: { email } });
|
||||
if (existingUser) {
|
||||
throw new ConflictException({ code: 'email_exists', message: 'A user with this email already exists' });
|
||||
}
|
||||
|
||||
const passwordHash = await bcrypt.hash(password, 10);
|
||||
const user = await this.prisma.clientUser.create({
|
||||
data: {
|
||||
id: uuidv4(),
|
||||
venueId,
|
||||
email,
|
||||
passwordHash,
|
||||
name,
|
||||
role,
|
||||
},
|
||||
select: { id: true, email: true, name: true, role: true, venueId: true, createdAt: true },
|
||||
});
|
||||
|
||||
return user;
|
||||
}
|
||||
|
||||
async validateDevice(deviceId: string, token: string): Promise<any> {
|
||||
const hash = crypto.createHash('sha256').update(token).digest('hex');
|
||||
const device = await this.prisma.device.findUnique({ where: { id: deviceId } });
|
||||
|
||||
@@ -142,6 +142,13 @@ export class DevicesService {
|
||||
orderBy: { createdAt: 'asc' },
|
||||
});
|
||||
|
||||
// Fetch planned tab names for this venue
|
||||
const plannedTabs = await this.prisma.plannedTab.findMany({
|
||||
where: { venueId: device.venueId },
|
||||
orderBy: { position: 'asc' },
|
||||
select: { customerName: true },
|
||||
});
|
||||
|
||||
return {
|
||||
cursor,
|
||||
commands: commands.map((c) => ({
|
||||
@@ -153,6 +160,7 @@ export class DevicesService {
|
||||
requestedBy: c.requestedBy,
|
||||
createdAt: c.createdAt,
|
||||
})),
|
||||
plannedTabNames: plannedTabs.map((t) => t.customerName),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
import { Controller, Get, Post, Put, Delete, Param, Body, UseGuards } from '@nestjs/common';
|
||||
import { ApiTags, ApiOperation, ApiBearerAuth } from '@nestjs/swagger';
|
||||
import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard';
|
||||
import { PlannedTabsService } from './planned-tabs.service';
|
||||
|
||||
@ApiTags('Planned Tabs')
|
||||
@ApiBearerAuth()
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@Controller('venues/:venueId/planned-tabs')
|
||||
export class PlannedTabsController {
|
||||
constructor(private service: PlannedTabsService) {}
|
||||
|
||||
@Get()
|
||||
@ApiOperation({ summary: 'List planned tab names' })
|
||||
async findAll(@Param('venueId') venueId: string) {
|
||||
return this.service.findAll(venueId);
|
||||
}
|
||||
|
||||
@Post()
|
||||
@ApiOperation({ summary: 'Add a planned tab name' })
|
||||
async create(@Param('venueId') venueId: string, @Body() dto: { customerName: string }) {
|
||||
return this.service.create(venueId, dto.customerName);
|
||||
}
|
||||
|
||||
@Put(':id')
|
||||
@ApiOperation({ summary: 'Rename a planned tab' })
|
||||
async update(@Param('venueId') venueId: string, @Param('id') id: string, @Body() dto: { customerName: string }) {
|
||||
return this.service.update(venueId, id, dto.customerName);
|
||||
}
|
||||
|
||||
@Delete(':id')
|
||||
@ApiOperation({ summary: 'Remove a planned tab' })
|
||||
async remove(@Param('venueId') venueId: string, @Param('id') id: string) {
|
||||
return this.service.remove(venueId, id);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { PlannedTabsController } from './planned-tabs.controller';
|
||||
import { PlannedTabsService } from './planned-tabs.service';
|
||||
|
||||
@Module({
|
||||
controllers: [PlannedTabsController],
|
||||
providers: [PlannedTabsService],
|
||||
exports: [PlannedTabsService],
|
||||
})
|
||||
export class PlannedTabsModule {}
|
||||
@@ -0,0 +1,72 @@
|
||||
import { Injectable, NotFoundException, ConflictException } from '@nestjs/common';
|
||||
import { PrismaService } from '../prisma/prisma.service';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
@Injectable()
|
||||
export class PlannedTabsService {
|
||||
constructor(private prisma: PrismaService) {}
|
||||
|
||||
async findAll(venueId: string) {
|
||||
const tabs = await this.prisma.plannedTab.findMany({
|
||||
where: { venueId },
|
||||
orderBy: { position: 'asc' },
|
||||
select: { id: true, customerName: true, position: true, createdAt: true },
|
||||
});
|
||||
return { plannedTabs: tabs };
|
||||
}
|
||||
|
||||
async create(venueId: string, customerName: string) {
|
||||
const name = customerName.trim();
|
||||
if (!name) throw new NotFoundException({ code: 'name_required', message: 'Customer name is required' });
|
||||
|
||||
// Check for duplicate
|
||||
const existing = await this.prisma.plannedTab.findUnique({
|
||||
where: { venueId_customerName: { venueId, customerName: name } },
|
||||
});
|
||||
if (existing) {
|
||||
throw new ConflictException({ code: 'name_exists', message: 'This customer name already exists' });
|
||||
}
|
||||
|
||||
// Get next position
|
||||
const last = await this.prisma.plannedTab.findFirst({
|
||||
where: { venueId },
|
||||
orderBy: { position: 'desc' },
|
||||
});
|
||||
|
||||
const tab = await this.prisma.plannedTab.create({
|
||||
data: {
|
||||
id: uuidv4(),
|
||||
venueId,
|
||||
customerName: name,
|
||||
position: (last?.position ?? -1) + 1,
|
||||
},
|
||||
select: { id: true, customerName: true, position: true, createdAt: true },
|
||||
});
|
||||
|
||||
return tab;
|
||||
}
|
||||
|
||||
async update(venueId: string, id: string, customerName: string) {
|
||||
const name = customerName.trim();
|
||||
if (!name) throw new NotFoundException({ code: 'name_required', message: 'Customer name is required' });
|
||||
|
||||
const tab = await this.prisma.plannedTab.findFirst({ where: { id, venueId } });
|
||||
if (!tab) throw new NotFoundException({ code: 'not_found', message: 'Planned tab not found' });
|
||||
|
||||
const result = await this.prisma.plannedTab.update({
|
||||
where: { id },
|
||||
data: { customerName: name },
|
||||
select: { id: true, customerName: true, position: true, createdAt: true },
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
async remove(venueId: string, id: string) {
|
||||
const tab = await this.prisma.plannedTab.findFirst({ where: { id, venueId } });
|
||||
if (!tab) throw new NotFoundException({ code: 'not_found', message: 'Planned tab not found' });
|
||||
|
||||
await this.prisma.plannedTab.delete({ where: { id } });
|
||||
return { ok: true };
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,10 @@
|
||||
import { Controller, Get, Param, UseGuards } from '@nestjs/common';
|
||||
import { Controller, Get, Param, UseGuards, Post, Body } from '@nestjs/common';
|
||||
import { ApiTags, ApiOperation, ApiBearerAuth } from '@nestjs/swagger';
|
||||
import { VenuesService } from './venues.service';
|
||||
import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard';
|
||||
import { RolesGuard } from '../common/guards/roles.guard';
|
||||
import { Roles } from '../common/decorators/roles.decorator';
|
||||
import { AuthService } from '../auth/auth.service';
|
||||
import { PrismaService } from '../prisma/prisma.service';
|
||||
|
||||
@ApiTags('Venues')
|
||||
@@ -9,6 +12,7 @@ import { PrismaService } from '../prisma/prisma.service';
|
||||
export class VenuesController {
|
||||
constructor(
|
||||
private venuesService: VenuesService,
|
||||
private authService: AuthService,
|
||||
private prisma: PrismaService,
|
||||
) {}
|
||||
|
||||
@@ -26,24 +30,41 @@ export class VenuesController {
|
||||
return this.venuesService.findOne(venueId);
|
||||
}
|
||||
|
||||
@Get(':venueId/users')
|
||||
@ApiBearerAuth()
|
||||
@UseGuards(JwtAuthGuard, RolesGuard)
|
||||
@Roles('admin')
|
||||
@ApiOperation({ summary: 'List users for a venue' })
|
||||
async findUsers(@Param('venueId') venueId: string) {
|
||||
const users = await this.prisma.clientUser.findMany({
|
||||
where: { venueId },
|
||||
select: { id: true, email: true, name: true, role: true, createdAt: true },
|
||||
orderBy: { createdAt: 'desc' },
|
||||
});
|
||||
return { users };
|
||||
}
|
||||
|
||||
@Post(':venueId/users')
|
||||
@ApiBearerAuth()
|
||||
@UseGuards(JwtAuthGuard, RolesGuard)
|
||||
@Roles('admin')
|
||||
@ApiOperation({ summary: 'Create a new user for the venue' })
|
||||
async createUser(
|
||||
@Param('venueId') venueId: string,
|
||||
@Body() dto: { email: string; password: string; name: string; role: string },
|
||||
) {
|
||||
return this.authService.createUser(venueId, dto.email, dto.password, dto.name, dto.role);
|
||||
}
|
||||
|
||||
@Get(':venueId/devices')
|
||||
@ApiBearerAuth()
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@ApiOperation({ summary: 'List devices for a venue' })
|
||||
@ApiOperation({ summary: 'Get venue device info' })
|
||||
async findDevices(@Param('venueId') venueId: string) {
|
||||
const devices = await this.prisma.device.findMany({
|
||||
where: { venueId },
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
lastSeenAt: true,
|
||||
revokedAt: true,
|
||||
createdAt: true,
|
||||
tokenExpiresAt: true,
|
||||
tokenPrefix: true,
|
||||
},
|
||||
orderBy: { createdAt: 'desc' },
|
||||
const device = await this.prisma.device.findFirst({
|
||||
where: { venueId, revokedAt: null },
|
||||
select: { id: true, name: true, lastSeenAt: true, createdAt: true },
|
||||
});
|
||||
return { devices };
|
||||
return { device };
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,10 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { AuthModule } from '../auth/auth.module';
|
||||
import { VenuesController } from './venues.controller';
|
||||
import { VenuesService } from './venues.service';
|
||||
|
||||
@Module({
|
||||
imports: [AuthModule],
|
||||
controllers: [VenuesController],
|
||||
providers: [VenuesService],
|
||||
exports: [VenuesService],
|
||||
|
||||
Reference in New Issue
Block a user