// ============================================================================= // NetForge Prisma Schema // ============================================================================= // // MULTI-TENANCY & RLS CONVENTION: // All tenant-scoped models MUST include a `tenantId` field. // This field is the foundation for Row-Level Security (RLS) policies. // When adding new models (Subscriber, Invoice, Plan, Payment, etc.), // always include: tenantId String + @@index([tenantId]) // // Super-admin models that span tenants (e.g., audit logs, platform config) // are the only exception to this rule. // ============================================================================= generator client { provider = "prisma-client-js" } datasource db { provider = "postgresql" url = env("DATABASE_URL") } // ============================================================================= // ENUMS // ============================================================================= enum TenantStatus { ACTIVE PENDING_SUSPENSION SUSPENDED } enum Role { ADMIN OFFICE_STAFF COLLECTOR TECHNICIAN CLIENT } enum AccountType { ASSET LIABILITY EQUITY REVENUE EXPENSE } enum NormalBalance { DEBIT CREDIT } enum PeriodStatus { OPEN CLOSED } enum SubscriberStatus { ACTIVE SUSPENDED CANCELLED } enum BillingType { PREPAID POSTPAID } // ============================================================================= // MODELS // ============================================================================= /// A Tenant represents a single ISP business using the NetForge platform. /// All tenant-scoped data is isolated by tenantId (RLS-ready). model Tenant { id String @id @default(uuid()) name String /// URL-friendly identifier, auto-generated from name (e.g., "my-isp" from "My ISP") slug String @unique ownerEmail String /// Physical address of the ISP business (optional) businessAddress String? /// Primary contact phone number (optional) contactPhone String? status TenantStatus @default(ACTIVE) /// Timestamp when suspension was triggered (starts grace period clock) suspendedAt DateTime? /// When actual service interruption occurs (suspendedAt + 7 days grace period) gracePeriodEndsAt DateTime? createdAt DateTime @default(now()) updatedAt DateTime @updatedAt users User[] } /// An Account represents a node in the Chart of Accounts tree. /// Account balances are NEVER stored — they are always derived from journal entry sums. /// System accounts (isSystemAccount=true) are auto-created from ISP_CHART_OF_ACCOUNTS /// and cannot be deleted by tenant admins. model Account { id String @id @default(uuid()) tenantId String /// Account code (e.g., "1010" for Cash on Hand). Unique per tenant. code String name String accountType AccountType normalBalance NormalBalance /// Optional parent account id for hierarchical COA (e.g., 1010 parent = 1000) parentId String? parent Account? @relation("AccountSubAccounts", fields: [parentId], references: [id]) children Account[] @relation("AccountSubAccounts") /// COA accounts seeded at tenant creation cannot be deleted by admins isSystemAccount Boolean @default(true) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt /// Account codes must be unique within a tenant @@unique([tenantId, code]) /// RLS-ready index — always present on tenant-scoped models @@index([tenantId]) } /// An AccountingPeriod represents one calendar month for a tenant. /// Once CLOSED, no journal entries may be posted to that period. /// Periods are created on-demand (first time an entry needs one). model AccountingPeriod { id String @id @default(uuid()) tenantId String year Int month Int status PeriodStatus @default(OPEN) /// Timestamp when the period was closed (null if OPEN) closedAt DateTime? /// The admin user who closed this period closedById String? closedBy User? @relation(fields: [closedById], references: [id]) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt /// One period per month per tenant @@unique([tenantId, year, month]) /// RLS-ready index — always present on tenant-scoped models @@index([tenantId]) } /// TenantSettings holds per-tenant configuration for billing and auto-suspension. /// One record per tenant, created on first access or at tenant provisioning. model TenantSettings { id String @id @default(uuid()) tenantId String @unique /// Days overdue before automatic subscriber suspension (default 30) autoSuspendDays Int @default(30) /// Days before billing date to generate prepaid invoices (default 7) prepaidLeadDays Int @default(7) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt /// RLS-ready index — always present on tenant-scoped models @@index([tenantId]) } /// A ServicePlan defines the internet service offering (speed, price, billing type). /// Plans are soft-deleted (isActive=false) to preserve subscriber history. model ServicePlan { id String @id @default(uuid()) tenantId String name String /// Human-readable speed description (e.g., "50 Mbps", "100/20 Mbps") speed String /// Monthly charge for this plan (2 decimal places) monthlyPrice Decimal @db.Decimal(10, 2) billingType BillingType description String? /// Soft-delete: inactive plans cannot be assigned to new subscribers isActive Boolean @default(true) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt subscribers Subscriber[] /// Plan names must be unique within a tenant @@unique([tenantId, name]) /// RLS-ready index — always present on tenant-scoped models @@index([tenantId]) } /// A Subscriber is a customer of the ISP — the core billing entity. /// All invoices, payments, and collections target subscribers. model Subscriber { id String @id @default(uuid()) tenantId String /// Auto-generated sequential identifier per tenant (e.g., "SUB-0001") accountNumber String firstName String lastName String email String? phone String? address String /// Zone for collector routing (Phase 3) zone String? servicePlanId String servicePlan ServicePlan @relation(fields: [servicePlanId], references: [id]) status SubscriberStatus @default(ACTIVE) /// Day of month for invoice generation — derived from signup date, capped at 28 billingDay Int activatedAt DateTime @default(now()) suspendedAt DateTime? cancelledAt DateTime? /// Per-subscriber auto-suspend override (null = use TenantSettings.autoSuspendDays) autoSuspendDays Int? notes String? /// Overpayment credit balance — always updated atomically with journal entries. /// This is NOT a stored ledger balance; it tracks credits for the FIFO allocation /// system (02-05) and is always updated within the same transaction as the journal entry. creditBalance Decimal @default(0) @db.Decimal(10, 2) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt /// Account numbers must be unique within a tenant @@unique([tenantId, accountNumber]) /// RLS-ready index — always present on tenant-scoped models @@index([tenantId]) @@index([tenantId, status]) @@index([tenantId, servicePlanId]) } /// A User belongs to a Tenant (or is a super-admin with no tenant). /// Email uniqueness is enforced per-tenant, not globally. /// Super-admins have isSuperAdmin=true and tenantId=null. model User { id String @id @default(uuid()) email String passwordHash String firstName String lastName String /// Nullable for super-admins who are not scoped to a specific tenant tenantId String? tenant Tenant? @relation(fields: [tenantId], references: [id], onDelete: Cascade) /// Multi-role support — a user can hold more than one role within a tenant roles Role[] isActive Boolean @default(true) isSuperAdmin Boolean @default(false) /// Accounting periods this user has closed (as administrator) closedPeriods AccountingPeriod[] createdAt DateTime @default(now()) updatedAt DateTime @updatedAt /// Email must be unique within a tenant (super-admins have tenantId=null) @@unique([email, tenantId]) /// RLS-ready index — always present on tenant-scoped models @@index([tenantId]) }