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
This commit is contained in:
kevin-asprec
2026-03-04 22:47:45 +08:00
parent bd8e83bf68
commit 7c0caf5244
5 changed files with 731 additions and 1 deletions

View File

@@ -0,0 +1,57 @@
-- CreateEnum
CREATE TYPE "AccountType" AS ENUM ('ASSET', 'LIABILITY', 'EQUITY', 'REVENUE', 'EXPENSE');
-- CreateEnum
CREATE TYPE "NormalBalance" AS ENUM ('DEBIT', 'CREDIT');
-- CreateEnum
CREATE TYPE "PeriodStatus" AS ENUM ('OPEN', 'CLOSED');
-- CreateTable
CREATE TABLE "Account" (
"id" TEXT NOT NULL,
"tenantId" TEXT NOT NULL,
"code" TEXT NOT NULL,
"name" TEXT NOT NULL,
"accountType" "AccountType" NOT NULL,
"normalBalance" "NormalBalance" NOT NULL,
"parentId" TEXT,
"isSystemAccount" BOOLEAN NOT NULL DEFAULT true,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "Account_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "AccountingPeriod" (
"id" TEXT NOT NULL,
"tenantId" TEXT NOT NULL,
"year" INTEGER NOT NULL,
"month" INTEGER NOT NULL,
"status" "PeriodStatus" NOT NULL DEFAULT 'OPEN',
"closedAt" TIMESTAMP(3),
"closedById" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "AccountingPeriod_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE INDEX "Account_tenantId_idx" ON "Account"("tenantId");
-- CreateIndex
CREATE UNIQUE INDEX "Account_tenantId_code_key" ON "Account"("tenantId", "code");
-- CreateIndex
CREATE INDEX "AccountingPeriod_tenantId_idx" ON "AccountingPeriod"("tenantId");
-- CreateIndex
CREATE UNIQUE INDEX "AccountingPeriod_tenantId_year_month_key" ON "AccountingPeriod"("tenantId", "year", "month");
-- AddForeignKey
ALTER TABLE "Account" ADD CONSTRAINT "Account_parentId_fkey" FOREIGN KEY ("parentId") REFERENCES "Account"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "AccountingPeriod" ADD CONSTRAINT "AccountingPeriod_closedById_fkey" FOREIGN KEY ("closedById") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE;

View File

@@ -39,6 +39,24 @@ enum Role {
CLIENT
}
enum AccountType {
ASSET
LIABILITY
EQUITY
REVENUE
EXPENSE
}
enum NormalBalance {
DEBIT
CREDIT
}
enum PeriodStatus {
OPEN
CLOSED
}
// =============================================================================
// MODELS
// =============================================================================
@@ -69,6 +87,56 @@ model Tenant {
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.
@@ -89,6 +157,9 @@ model User {
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