Files
NetForge/prisma/schema.prisma
kevin-asprec 7c0caf5244 feat(02-01): Account and AccountingPeriod Prisma models + COA definition
- Add AccountType, NormalBalance, PeriodStatus enums to schema
- Add Account model with tenant scoping, code/name/type/normalBalance/parentId
- Add AccountingPeriod model with year/month/status/closedAt/closedById
- Create ISP_CHART_OF_ACCOUNTS with 28 accounts across all 5 types (1000-5000 ranges)
- Create accounting-period.ts with getOpenPeriod, closePeriod, isDateInClosedPeriod
- Extend TENANT_SCOPED_MODELS with account and accountingPeriod
- Add full query extension blocks for account and accountingPeriod in withTenantContext
- Run migration: 20260304144656_add_accounting_models
2026-03-04 22:47:45 +08:00

171 lines
5.3 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
}
// =============================================================================
// 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])
}
/// 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])
}