feat(03-01): Zone schema, migration, and tenant scoping
- Add Zone model (id, tenantId, name, description, isActive) - Add ZoneAssignment model (collector-to-zone join table) - Replace Subscriber.zone String? with Subscriber.zoneId FK to Zone - Add Zone + ZoneAssignment to TENANT_SCOPED_MODELS with full operation blocks - Add "Zone" to AppSubjects in types.ts - Grant OFFICE_STAFF manage Zone, COLLECTOR read Zone in permissions.ts - Migration 20260305000000_add_zones applied to DB
This commit is contained in:
57
prisma/migrations/20260305000000_add_zones/migration.sql
Normal file
57
prisma/migrations/20260305000000_add_zones/migration.sql
Normal file
@@ -0,0 +1,57 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "Subscriber" DROP COLUMN "zone",
|
||||
ADD COLUMN "zoneId" TEXT;
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "Zone" (
|
||||
"id" TEXT NOT NULL,
|
||||
"tenantId" TEXT NOT NULL,
|
||||
"name" TEXT NOT NULL,
|
||||
"description" TEXT,
|
||||
"isActive" BOOLEAN NOT NULL DEFAULT true,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "Zone_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "ZoneAssignment" (
|
||||
"id" TEXT NOT NULL,
|
||||
"tenantId" TEXT NOT NULL,
|
||||
"userId" TEXT NOT NULL,
|
||||
"zoneId" TEXT NOT NULL,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "ZoneAssignment_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "Zone_tenantId_idx" ON "Zone"("tenantId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "Zone_tenantId_name_key" ON "Zone"("tenantId", "name");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "ZoneAssignment_tenantId_idx" ON "ZoneAssignment"("tenantId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "ZoneAssignment_userId_idx" ON "ZoneAssignment"("userId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "ZoneAssignment_zoneId_idx" ON "ZoneAssignment"("zoneId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "ZoneAssignment_tenantId_userId_zoneId_key" ON "ZoneAssignment"("tenantId", "userId", "zoneId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "Subscriber_tenantId_zoneId_idx" ON "Subscriber"("tenantId", "zoneId");
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Subscriber" ADD CONSTRAINT "Subscriber_zoneId_fkey" FOREIGN KEY ("zoneId") REFERENCES "Zone"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "ZoneAssignment" ADD CONSTRAINT "ZoneAssignment_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "ZoneAssignment" ADD CONSTRAINT "ZoneAssignment_zoneId_fkey" FOREIGN KEY ("zoneId") REFERENCES "Zone"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
@@ -235,8 +235,9 @@ model Subscriber {
|
||||
email String?
|
||||
phone String?
|
||||
address String
|
||||
/// Zone for collector routing (Phase 3)
|
||||
zone String?
|
||||
/// Zone FK for collector routing (Phase 3) — replaces old String? zone field
|
||||
zoneId String?
|
||||
zone Zone? @relation(fields: [zoneId], references: [id])
|
||||
servicePlanId String
|
||||
servicePlan ServicePlan @relation(fields: [servicePlanId], references: [id])
|
||||
status SubscriberStatus @default(ACTIVE)
|
||||
@@ -264,6 +265,48 @@ model Subscriber {
|
||||
@@index([tenantId])
|
||||
@@index([tenantId, status])
|
||||
@@index([tenantId, servicePlanId])
|
||||
@@index([tenantId, zoneId])
|
||||
}
|
||||
|
||||
/// A Zone groups subscribers geographically for collector routing.
|
||||
/// Collectors are assigned to zones and can only collect from subscribers in those zones.
|
||||
/// This is a security boundary enforced at the data layer.
|
||||
model Zone {
|
||||
id String @id @default(uuid())
|
||||
tenantId String
|
||||
name String
|
||||
description String?
|
||||
isActive Boolean @default(true)
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
subscribers Subscriber[]
|
||||
assignments ZoneAssignment[]
|
||||
|
||||
/// Zone names must be unique within a tenant
|
||||
@@unique([tenantId, name])
|
||||
/// RLS-ready index — always present on tenant-scoped models
|
||||
@@index([tenantId])
|
||||
}
|
||||
|
||||
/// A ZoneAssignment links a collector user to a zone.
|
||||
/// Collectors can be assigned to multiple zones; zones can have multiple collectors.
|
||||
model ZoneAssignment {
|
||||
id String @id @default(uuid())
|
||||
tenantId String
|
||||
/// The collector user assigned to this zone
|
||||
userId String
|
||||
user User @relation(fields: [userId], references: [id])
|
||||
zoneId String
|
||||
zone Zone @relation(fields: [zoneId], references: [id])
|
||||
createdAt DateTime @default(now())
|
||||
|
||||
/// One assignment per collector per zone per tenant
|
||||
@@unique([tenantId, userId, zoneId])
|
||||
/// RLS-ready index — always present on tenant-scoped models
|
||||
@@index([tenantId])
|
||||
@@index([userId])
|
||||
@@index([zoneId])
|
||||
}
|
||||
|
||||
/// A User belongs to a Tenant (or is a super-admin with no tenant).
|
||||
@@ -295,6 +338,8 @@ model User {
|
||||
approvedJournalEntries JournalEntry[] @relation("JournalEntryApprovedBy")
|
||||
/// Payments this user recorded
|
||||
recordedPayments Payment[] @relation("PaymentRecordedBy")
|
||||
/// Zone assignments for collector role (which zones this user can collect from)
|
||||
zoneAssignments ZoneAssignment[]
|
||||
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
Reference in New Issue
Block a user