// ============================================================================= // 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 } enum JournalEntryStatus { DRAFT PENDING_APPROVAL APPROVED POSTED REVERSED } enum JournalEntrySource { SYSTEM MANUAL } // ============================================================================= // 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 journalEntryLines JournalEntryLine[] /// 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[] /// Journal entries this user created (maker) createdJournalEntries JournalEntry[] @relation("JournalEntryCreatedBy") /// Journal entries this user approved (checker) approvedJournalEntries JournalEntry[] @relation("JournalEntryApprovedBy") 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]) } /// A JournalEntry records a double-entry accounting transaction. /// All financial events in the system flow through this model. /// Entries are IMMUTABLE — no updates or deletes. Corrections use reversing entries. model JournalEntry { id String @id @default(uuid()) tenantId String /// Auto-generated sequential identifier per tenant per year (e.g., "JE-2026-0001") entryNumber String /// The accounting date (when the economic event occurred, not necessarily createdAt) date DateTime description String source JournalEntrySource status JournalEntryStatus @default(DRAFT) /// Optional reference to source record (e.g., "Invoice", "Payment") referenceType String? /// ID of the source record (e.g., the invoice UUID) referenceId String? /// If this entry reverses another entry, this points to the original reversesEntryId String? @unique reversesEntry JournalEntry? @relation("JournalEntryReversal", fields: [reversesEntryId], references: [id]) /// If this entry has been reversed, this points to the reversing entry (back-reference) reversedByEntry JournalEntry? @relation("JournalEntryReversal") /// The user who created the entry (maker) createdById String createdBy User @relation("JournalEntryCreatedBy", fields: [createdById], references: [id]) /// The user who approved the entry (checker — for MANUAL entries) approvedById String? approvedBy User? @relation("JournalEntryApprovedBy", fields: [approvedById], references: [id]) approvedAt DateTime? createdAt DateTime @default(now()) updatedAt DateTime @updatedAt lines JournalEntryLine[] /// Entry numbers must be unique within a tenant @@unique([tenantId, entryNumber]) /// RLS-ready index — always present on tenant-scoped models @@index([tenantId]) @@index([tenantId, date]) @@index([referenceType, referenceId]) } /// A single line (debit or credit) within a JournalEntry. /// Every entry must have at least 2 lines, and sum(debit) = sum(credit). model JournalEntryLine { id String @id @default(uuid()) tenantId String journalEntryId String journalEntry JournalEntry @relation(fields: [journalEntryId], references: [id]) accountId String account Account @relation(fields: [accountId], references: [id]) /// Debit amount for this line (0 if this is a credit line) debit Decimal @default(0) @db.Decimal(15, 2) /// Credit amount for this line (0 if this is a debit line) credit Decimal @default(0) @db.Decimal(15, 2) /// Optional line-level memo description String? createdAt DateTime @default(now()) /// RLS-ready index — always present on tenant-scoped models @@index([tenantId]) @@index([accountId]) @@index([journalEntryId]) }