- Add SubscriberStatus (ACTIVE/SUSPENDED/CANCELLED) and BillingType (PREPAID/POSTPAID) enums - Add ServicePlan model with name, speed, monthlyPrice, billingType, soft-delete - Add Subscriber model with accountNumber, billingDay, status lifecycle, creditBalance - Add TenantSettings model with autoSuspendDays and prepaidLeadDays - Migrate: 20260304145633_add_subscriber_models - Extend prisma-tenant.ts with subscriber, servicePlan, tenantSettings query scoping - Create service-plan-service.ts: createServicePlan, updateServicePlan, listServicePlans, deactivateServicePlan - Create subscriber-service.ts: createSubscriber, updateSubscriber, changeSubscriberStatus, searchSubscribers, getSubscriber, generateAccountNumber
263 lines
8.6 KiB
Plaintext
263 lines
8.6 KiB
Plaintext
// =============================================================================
|
|
// 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])
|
|
}
|