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:
kevin-asprec
2026-03-05 07:24:58 +08:00
parent d54b517e2e
commit 56f5d071c4
5 changed files with 302 additions and 3 deletions

View File

@@ -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