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:
@@ -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;
|
||||||
@@ -39,6 +39,24 @@ enum Role {
|
|||||||
CLIENT
|
CLIENT
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum AccountType {
|
||||||
|
ASSET
|
||||||
|
LIABILITY
|
||||||
|
EQUITY
|
||||||
|
REVENUE
|
||||||
|
EXPENSE
|
||||||
|
}
|
||||||
|
|
||||||
|
enum NormalBalance {
|
||||||
|
DEBIT
|
||||||
|
CREDIT
|
||||||
|
}
|
||||||
|
|
||||||
|
enum PeriodStatus {
|
||||||
|
OPEN
|
||||||
|
CLOSED
|
||||||
|
}
|
||||||
|
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
// MODELS
|
// MODELS
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
@@ -69,6 +87,56 @@ model Tenant {
|
|||||||
users User[]
|
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).
|
/// A User belongs to a Tenant (or is a super-admin with no tenant).
|
||||||
/// Email uniqueness is enforced per-tenant, not globally.
|
/// Email uniqueness is enforced per-tenant, not globally.
|
||||||
/// Super-admins have isSuperAdmin=true and tenantId=null.
|
/// Super-admins have isSuperAdmin=true and tenantId=null.
|
||||||
@@ -89,6 +157,9 @@ model User {
|
|||||||
isActive Boolean @default(true)
|
isActive Boolean @default(true)
|
||||||
isSuperAdmin Boolean @default(false)
|
isSuperAdmin Boolean @default(false)
|
||||||
|
|
||||||
|
/// Accounting periods this user has closed (as administrator)
|
||||||
|
closedPeriods AccountingPeriod[]
|
||||||
|
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
|
|
||||||
|
|||||||
131
src/lib/accounting/accounting-period.ts
Normal file
131
src/lib/accounting/accounting-period.ts
Normal file
@@ -0,0 +1,131 @@
|
|||||||
|
// =============================================================================
|
||||||
|
// Accounting Period Management
|
||||||
|
// =============================================================================
|
||||||
|
//
|
||||||
|
// Accounting periods represent calendar months. A period is OPEN by default.
|
||||||
|
// Once CLOSED, no journal entries may be posted to that period (enforced in 02-02).
|
||||||
|
//
|
||||||
|
// Periods are created on-demand — the first call to getOpenPeriod() for a given
|
||||||
|
// month/year creates the period record if it doesn't already exist.
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
import { PrismaClient, PeriodStatus } from "@prisma/client";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Minimal Prisma client interface that supports accountingPeriod operations.
|
||||||
|
* Accepts a full PrismaClient or a transaction client (Prisma.$transaction callback arg).
|
||||||
|
*/
|
||||||
|
type PrismaLike = Pick<PrismaClient, "accountingPeriod">;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Finds an existing OPEN period for the given month/year, or creates one if
|
||||||
|
* it doesn't exist yet. Throws if the period for that month is already CLOSED.
|
||||||
|
*
|
||||||
|
* @param db - A Prisma client or transaction client
|
||||||
|
* @param tenantId - The tenant scoping this period
|
||||||
|
* @param year - Calendar year (e.g., 2026)
|
||||||
|
* @param month - Calendar month, 1-12
|
||||||
|
* @returns The OPEN AccountingPeriod record
|
||||||
|
* @throws Error if period for month/year exists but is CLOSED
|
||||||
|
*/
|
||||||
|
export async function getOpenPeriod(
|
||||||
|
db: PrismaLike,
|
||||||
|
tenantId: string,
|
||||||
|
year: number,
|
||||||
|
month: number
|
||||||
|
) {
|
||||||
|
// Try to find an existing period for this month
|
||||||
|
const existing = await db.accountingPeriod.findFirst({
|
||||||
|
where: { tenantId, year, month },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (existing) {
|
||||||
|
if (existing.status === PeriodStatus.CLOSED) {
|
||||||
|
throw new Error(
|
||||||
|
`Accounting period ${year}-${String(month).padStart(2, "0")} is closed. ` +
|
||||||
|
`No journal entries may be posted to a closed period.`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return existing;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create the period if it doesn't exist yet
|
||||||
|
return db.accountingPeriod.create({
|
||||||
|
data: {
|
||||||
|
tenantId,
|
||||||
|
year,
|
||||||
|
month,
|
||||||
|
status: PeriodStatus.OPEN,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Closes an accounting period. Sets status to CLOSED, records the timestamp
|
||||||
|
* and the ID of the admin who performed the close.
|
||||||
|
*
|
||||||
|
* @param db - A Prisma client or transaction client
|
||||||
|
* @param periodId - The UUID of the AccountingPeriod to close
|
||||||
|
* @param closedById - The UUID of the User performing the close action
|
||||||
|
* @returns The updated (closed) AccountingPeriod record
|
||||||
|
* @throws Error if the period is already closed
|
||||||
|
*/
|
||||||
|
export async function closePeriod(
|
||||||
|
db: PrismaLike,
|
||||||
|
periodId: string,
|
||||||
|
closedById: string
|
||||||
|
) {
|
||||||
|
const period = await db.accountingPeriod.findFirst({
|
||||||
|
where: { id: periodId },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!period) {
|
||||||
|
throw new Error(`Accounting period not found: ${periodId}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (period.status === PeriodStatus.CLOSED) {
|
||||||
|
throw new Error(
|
||||||
|
`Accounting period ${period.year}-${String(period.month).padStart(2, "0")} ` +
|
||||||
|
`is already closed.`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return db.accountingPeriod.update({
|
||||||
|
where: { id: periodId },
|
||||||
|
data: {
|
||||||
|
status: PeriodStatus.CLOSED,
|
||||||
|
closedAt: new Date(),
|
||||||
|
closedById,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks whether the accounting period containing the given date is CLOSED.
|
||||||
|
*
|
||||||
|
* Used by journal entry posting logic (02-02) to prevent entries in closed periods.
|
||||||
|
*
|
||||||
|
* @param db - A Prisma client or transaction client
|
||||||
|
* @param tenantId - The tenant to check
|
||||||
|
* @param date - The date to check (uses its year and month)
|
||||||
|
* @returns true if the period is CLOSED, false if OPEN or not yet created
|
||||||
|
*/
|
||||||
|
export async function isDateInClosedPeriod(
|
||||||
|
db: PrismaLike,
|
||||||
|
tenantId: string,
|
||||||
|
date: Date
|
||||||
|
): Promise<boolean> {
|
||||||
|
const year = date.getFullYear();
|
||||||
|
const month = date.getMonth() + 1; // JS months are 0-indexed
|
||||||
|
|
||||||
|
const period = await db.accountingPeriod.findFirst({
|
||||||
|
where: { tenantId, year, month },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!period) {
|
||||||
|
// Period not created yet — it's implicitly open
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return period.status === PeriodStatus.CLOSED;
|
||||||
|
}
|
||||||
279
src/lib/accounting/chart-of-accounts.ts
Normal file
279
src/lib/accounting/chart-of-accounts.ts
Normal file
@@ -0,0 +1,279 @@
|
|||||||
|
// =============================================================================
|
||||||
|
// ISP Chart of Accounts Definition
|
||||||
|
// =============================================================================
|
||||||
|
//
|
||||||
|
// This module defines the standard Chart of Accounts (COA) for ISP businesses
|
||||||
|
// using NetForge. Every new tenant gets this COA auto-provisioned on signup.
|
||||||
|
//
|
||||||
|
// DOUBLE-ENTRY ACCOUNTING RULES:
|
||||||
|
// - DEBIT increases ASSET and EXPENSE accounts (normal balance = DEBIT)
|
||||||
|
// - CREDIT increases LIABILITY, EQUITY, and REVENUE accounts (normal balance = CREDIT)
|
||||||
|
// - Account balances are NEVER stored — always derived from journal entry sums
|
||||||
|
//
|
||||||
|
// ACCOUNT CODE RANGES:
|
||||||
|
// 1000s = Assets
|
||||||
|
// 2000s = Liabilities
|
||||||
|
// 3000s = Equity
|
||||||
|
// 4000s = Revenue
|
||||||
|
// 5000s = Expenses
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Account type enum mirroring the Prisma AccountType enum.
|
||||||
|
* Exported for use in code that runs outside the Prisma context (e.g., seeding scripts).
|
||||||
|
*/
|
||||||
|
export type AccountType = "ASSET" | "LIABILITY" | "EQUITY" | "REVENUE" | "EXPENSE";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Normal balance enum mirroring the Prisma NormalBalance enum.
|
||||||
|
* Determines whether a debit or credit increases the account balance.
|
||||||
|
* - DEBIT: assets and expenses increase with debits
|
||||||
|
* - CREDIT: liabilities, equity, and revenue increase with credits
|
||||||
|
*/
|
||||||
|
export type NormalBalance = "DEBIT" | "CREDIT";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A single account definition in the ISP Chart of Accounts.
|
||||||
|
*/
|
||||||
|
export interface COAAccountDefinition {
|
||||||
|
/** Account code (e.g., "1010"). Must be unique within a tenant. */
|
||||||
|
code: string;
|
||||||
|
/** Human-readable account name (e.g., "Cash on Hand"). */
|
||||||
|
name: string;
|
||||||
|
/** The broad classification of this account. */
|
||||||
|
accountType: AccountType;
|
||||||
|
/** Whether debits or credits increase this account's balance. */
|
||||||
|
normalBalance: NormalBalance;
|
||||||
|
/**
|
||||||
|
* Optional parent account code. If set, this account is a sub-account
|
||||||
|
* of the named parent. Parent must appear earlier in the array so it
|
||||||
|
* can be resolved to a parentId during seeding.
|
||||||
|
*/
|
||||||
|
parentCode?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The standard ISP Chart of Accounts.
|
||||||
|
*
|
||||||
|
* Organized by account type:
|
||||||
|
* - Assets (1000s): What the ISP owns or is owed
|
||||||
|
* - Liabilities (2000s): What the ISP owes to others
|
||||||
|
* - Equity (3000s): Owner's stake in the business
|
||||||
|
* - Revenue (4000s): Income from ISP operations
|
||||||
|
* - Expenses (5000s): Costs of running the ISP
|
||||||
|
*
|
||||||
|
* All 5 standard accounting types are represented.
|
||||||
|
* Subscriber Credits (1150) is a contra-asset (reduces AR).
|
||||||
|
*/
|
||||||
|
export const ISP_CHART_OF_ACCOUNTS: COAAccountDefinition[] = [
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// ASSETS (1000s) — Normal balance: DEBIT
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
{
|
||||||
|
code: "1000",
|
||||||
|
name: "Current Assets",
|
||||||
|
accountType: "ASSET",
|
||||||
|
normalBalance: "DEBIT",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: "1010",
|
||||||
|
name: "Cash on Hand",
|
||||||
|
accountType: "ASSET",
|
||||||
|
normalBalance: "DEBIT",
|
||||||
|
parentCode: "1000",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: "1020",
|
||||||
|
name: "Cash in Bank",
|
||||||
|
accountType: "ASSET",
|
||||||
|
normalBalance: "DEBIT",
|
||||||
|
parentCode: "1000",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: "1100",
|
||||||
|
name: "Accounts Receivable",
|
||||||
|
accountType: "ASSET",
|
||||||
|
normalBalance: "DEBIT",
|
||||||
|
parentCode: "1000",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: "1150",
|
||||||
|
name: "Subscriber Credits",
|
||||||
|
accountType: "ASSET",
|
||||||
|
normalBalance: "CREDIT", // Contra-asset: reduces accounts receivable
|
||||||
|
parentCode: "1000",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: "1200",
|
||||||
|
name: "Equipment Inventory",
|
||||||
|
accountType: "ASSET",
|
||||||
|
normalBalance: "DEBIT",
|
||||||
|
parentCode: "1000",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: "1300",
|
||||||
|
name: "Prepaid Expenses",
|
||||||
|
accountType: "ASSET",
|
||||||
|
normalBalance: "DEBIT",
|
||||||
|
parentCode: "1000",
|
||||||
|
},
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// LIABILITIES (2000s) — Normal balance: CREDIT
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
{
|
||||||
|
code: "2000",
|
||||||
|
name: "Current Liabilities",
|
||||||
|
accountType: "LIABILITY",
|
||||||
|
normalBalance: "CREDIT",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: "2010",
|
||||||
|
name: "Accounts Payable",
|
||||||
|
accountType: "LIABILITY",
|
||||||
|
normalBalance: "CREDIT",
|
||||||
|
parentCode: "2000",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: "2100",
|
||||||
|
name: "Unearned Revenue",
|
||||||
|
accountType: "LIABILITY",
|
||||||
|
normalBalance: "CREDIT", // Prepaid subscriber payments not yet earned
|
||||||
|
parentCode: "2000",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: "2200",
|
||||||
|
name: "Taxes Payable",
|
||||||
|
accountType: "LIABILITY",
|
||||||
|
normalBalance: "CREDIT",
|
||||||
|
parentCode: "2000",
|
||||||
|
},
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// EQUITY (3000s) — Normal balance: CREDIT
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
{
|
||||||
|
code: "3000",
|
||||||
|
name: "Owner's Equity",
|
||||||
|
accountType: "EQUITY",
|
||||||
|
normalBalance: "CREDIT",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: "3010",
|
||||||
|
name: "Owner's Capital",
|
||||||
|
accountType: "EQUITY",
|
||||||
|
normalBalance: "CREDIT",
|
||||||
|
parentCode: "3000",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: "3020",
|
||||||
|
name: "Retained Earnings",
|
||||||
|
accountType: "EQUITY",
|
||||||
|
normalBalance: "CREDIT",
|
||||||
|
parentCode: "3000",
|
||||||
|
},
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// REVENUE (4000s) — Normal balance: CREDIT
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
{
|
||||||
|
code: "4000",
|
||||||
|
name: "Operating Revenue",
|
||||||
|
accountType: "REVENUE",
|
||||||
|
normalBalance: "CREDIT",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: "4010",
|
||||||
|
name: "Subscription Revenue",
|
||||||
|
accountType: "REVENUE",
|
||||||
|
normalBalance: "CREDIT",
|
||||||
|
parentCode: "4000",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: "4020",
|
||||||
|
name: "Installation Fees",
|
||||||
|
accountType: "REVENUE",
|
||||||
|
normalBalance: "CREDIT",
|
||||||
|
parentCode: "4000",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: "4030",
|
||||||
|
name: "Reconnection Fees",
|
||||||
|
accountType: "REVENUE",
|
||||||
|
normalBalance: "CREDIT",
|
||||||
|
parentCode: "4000",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: "4090",
|
||||||
|
name: "Other Revenue",
|
||||||
|
accountType: "REVENUE",
|
||||||
|
normalBalance: "CREDIT",
|
||||||
|
parentCode: "4000",
|
||||||
|
},
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// EXPENSES (5000s) — Normal balance: DEBIT
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
{
|
||||||
|
code: "5000",
|
||||||
|
name: "Operating Expenses",
|
||||||
|
accountType: "EXPENSE",
|
||||||
|
normalBalance: "DEBIT",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: "5010",
|
||||||
|
name: "Salary Expense",
|
||||||
|
accountType: "EXPENSE",
|
||||||
|
normalBalance: "DEBIT",
|
||||||
|
parentCode: "5000",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: "5020",
|
||||||
|
name: "Technician Compensation",
|
||||||
|
accountType: "EXPENSE",
|
||||||
|
normalBalance: "DEBIT",
|
||||||
|
parentCode: "5000",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: "5030",
|
||||||
|
name: "Equipment Expense",
|
||||||
|
accountType: "EXPENSE",
|
||||||
|
normalBalance: "DEBIT",
|
||||||
|
parentCode: "5000",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: "5040",
|
||||||
|
name: "Internet Bandwidth",
|
||||||
|
accountType: "EXPENSE",
|
||||||
|
normalBalance: "DEBIT",
|
||||||
|
parentCode: "5000",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: "5050",
|
||||||
|
name: "Office Supplies",
|
||||||
|
accountType: "EXPENSE",
|
||||||
|
normalBalance: "DEBIT",
|
||||||
|
parentCode: "5000",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: "5060",
|
||||||
|
name: "Utilities",
|
||||||
|
accountType: "EXPENSE",
|
||||||
|
normalBalance: "DEBIT",
|
||||||
|
parentCode: "5000",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: "5070",
|
||||||
|
name: "Depreciation",
|
||||||
|
accountType: "EXPENSE",
|
||||||
|
normalBalance: "DEBIT",
|
||||||
|
parentCode: "5000",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: "5090",
|
||||||
|
name: "Other Expense",
|
||||||
|
accountType: "EXPENSE",
|
||||||
|
normalBalance: "DEBIT",
|
||||||
|
parentCode: "5000",
|
||||||
|
},
|
||||||
|
];
|
||||||
@@ -30,7 +30,7 @@ import { prisma } from "@/lib/prisma";
|
|||||||
* Extend this list as new models are added in later phases:
|
* Extend this list as new models are added in later phases:
|
||||||
* e.g., "subscriber", "invoice", "servicePlan", "payment"
|
* e.g., "subscriber", "invoice", "servicePlan", "payment"
|
||||||
*/
|
*/
|
||||||
export const TENANT_SCOPED_MODELS = ["user"] as const;
|
export const TENANT_SCOPED_MODELS = ["user", "account", "accountingPeriod"] as const;
|
||||||
|
|
||||||
export type TenantScopedModel = (typeof TENANT_SCOPED_MODELS)[number];
|
export type TenantScopedModel = (typeof TENANT_SCOPED_MODELS)[number];
|
||||||
|
|
||||||
@@ -162,6 +162,198 @@ export function withTenantContext(tenantId: string) {
|
|||||||
return query(args);
|
return query(args);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
account: {
|
||||||
|
async findMany({ args, query }) {
|
||||||
|
args.where = { ...args.where, tenantId };
|
||||||
|
return query(args);
|
||||||
|
},
|
||||||
|
|
||||||
|
async findFirst({ args, query }) {
|
||||||
|
args.where = { ...args.where, tenantId };
|
||||||
|
return query(args);
|
||||||
|
},
|
||||||
|
|
||||||
|
async findFirstOrThrow({ args, query }) {
|
||||||
|
args.where = { ...args.where, tenantId };
|
||||||
|
return query(args);
|
||||||
|
},
|
||||||
|
|
||||||
|
async findUnique({ args, query }) {
|
||||||
|
if (args.where && "id" in args.where && !("tenantId" in (args.where as object))) {
|
||||||
|
return prisma.account.findFirst({
|
||||||
|
...args,
|
||||||
|
where: { ...args.where, tenantId },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return query(args);
|
||||||
|
},
|
||||||
|
|
||||||
|
async findUniqueOrThrow({ args, query }) {
|
||||||
|
if (args.where && "id" in args.where && !("tenantId" in (args.where as object))) {
|
||||||
|
const result = await prisma.account.findFirst({
|
||||||
|
...args,
|
||||||
|
where: { ...args.where, tenantId },
|
||||||
|
});
|
||||||
|
if (!result) {
|
||||||
|
throw new Error("Record not found");
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
return query(args);
|
||||||
|
},
|
||||||
|
|
||||||
|
async create({ args, query }) {
|
||||||
|
args.data = { ...args.data, tenantId } as typeof args.data;
|
||||||
|
return query(args);
|
||||||
|
},
|
||||||
|
|
||||||
|
async createMany({ args, query }) {
|
||||||
|
if (Array.isArray(args.data)) {
|
||||||
|
args.data = args.data.map((item) => ({ ...item, tenantId })) as typeof args.data;
|
||||||
|
} else {
|
||||||
|
args.data = { ...args.data, tenantId } as typeof args.data;
|
||||||
|
}
|
||||||
|
return query(args);
|
||||||
|
},
|
||||||
|
|
||||||
|
async update({ args, query }) {
|
||||||
|
args.where = { ...args.where, tenantId };
|
||||||
|
return query(args);
|
||||||
|
},
|
||||||
|
|
||||||
|
async updateMany({ args, query }) {
|
||||||
|
args.where = { ...args.where, tenantId };
|
||||||
|
return query(args);
|
||||||
|
},
|
||||||
|
|
||||||
|
async delete({ args, query }) {
|
||||||
|
args.where = { ...args.where, tenantId };
|
||||||
|
return query(args);
|
||||||
|
},
|
||||||
|
|
||||||
|
async deleteMany({ args, query }) {
|
||||||
|
args.where = { ...args.where, tenantId };
|
||||||
|
return query(args);
|
||||||
|
},
|
||||||
|
|
||||||
|
async upsert({ args, query }) {
|
||||||
|
args.where = { ...args.where, tenantId } as typeof args.where;
|
||||||
|
args.create = { ...args.create, tenantId } as typeof args.create;
|
||||||
|
return query(args);
|
||||||
|
},
|
||||||
|
|
||||||
|
async count({ args, query }) {
|
||||||
|
args.where = { ...args.where, tenantId };
|
||||||
|
return query(args);
|
||||||
|
},
|
||||||
|
|
||||||
|
async aggregate({ args, query }) {
|
||||||
|
args.where = { ...args.where, tenantId };
|
||||||
|
return query(args);
|
||||||
|
},
|
||||||
|
|
||||||
|
async groupBy({ args, query }) {
|
||||||
|
args.where = { ...args.where, tenantId };
|
||||||
|
return query(args);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
accountingPeriod: {
|
||||||
|
async findMany({ args, query }) {
|
||||||
|
args.where = { ...args.where, tenantId };
|
||||||
|
return query(args);
|
||||||
|
},
|
||||||
|
|
||||||
|
async findFirst({ args, query }) {
|
||||||
|
args.where = { ...args.where, tenantId };
|
||||||
|
return query(args);
|
||||||
|
},
|
||||||
|
|
||||||
|
async findFirstOrThrow({ args, query }) {
|
||||||
|
args.where = { ...args.where, tenantId };
|
||||||
|
return query(args);
|
||||||
|
},
|
||||||
|
|
||||||
|
async findUnique({ args, query }) {
|
||||||
|
if (args.where && "id" in args.where && !("tenantId" in (args.where as object))) {
|
||||||
|
return prisma.accountingPeriod.findFirst({
|
||||||
|
...args,
|
||||||
|
where: { ...args.where, tenantId },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return query(args);
|
||||||
|
},
|
||||||
|
|
||||||
|
async findUniqueOrThrow({ args, query }) {
|
||||||
|
if (args.where && "id" in args.where && !("tenantId" in (args.where as object))) {
|
||||||
|
const result = await prisma.accountingPeriod.findFirst({
|
||||||
|
...args,
|
||||||
|
where: { ...args.where, tenantId },
|
||||||
|
});
|
||||||
|
if (!result) {
|
||||||
|
throw new Error("Record not found");
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
return query(args);
|
||||||
|
},
|
||||||
|
|
||||||
|
async create({ args, query }) {
|
||||||
|
args.data = { ...args.data, tenantId } as typeof args.data;
|
||||||
|
return query(args);
|
||||||
|
},
|
||||||
|
|
||||||
|
async createMany({ args, query }) {
|
||||||
|
if (Array.isArray(args.data)) {
|
||||||
|
args.data = args.data.map((item) => ({ ...item, tenantId })) as typeof args.data;
|
||||||
|
} else {
|
||||||
|
args.data = { ...args.data, tenantId } as typeof args.data;
|
||||||
|
}
|
||||||
|
return query(args);
|
||||||
|
},
|
||||||
|
|
||||||
|
async update({ args, query }) {
|
||||||
|
args.where = { ...args.where, tenantId };
|
||||||
|
return query(args);
|
||||||
|
},
|
||||||
|
|
||||||
|
async updateMany({ args, query }) {
|
||||||
|
args.where = { ...args.where, tenantId };
|
||||||
|
return query(args);
|
||||||
|
},
|
||||||
|
|
||||||
|
async delete({ args, query }) {
|
||||||
|
args.where = { ...args.where, tenantId };
|
||||||
|
return query(args);
|
||||||
|
},
|
||||||
|
|
||||||
|
async deleteMany({ args, query }) {
|
||||||
|
args.where = { ...args.where, tenantId };
|
||||||
|
return query(args);
|
||||||
|
},
|
||||||
|
|
||||||
|
async upsert({ args, query }) {
|
||||||
|
args.where = { ...args.where, tenantId } as typeof args.where;
|
||||||
|
args.create = { ...args.create, tenantId } as typeof args.create;
|
||||||
|
return query(args);
|
||||||
|
},
|
||||||
|
|
||||||
|
async count({ args, query }) {
|
||||||
|
args.where = { ...args.where, tenantId };
|
||||||
|
return query(args);
|
||||||
|
},
|
||||||
|
|
||||||
|
async aggregate({ args, query }) {
|
||||||
|
args.where = { ...args.where, tenantId };
|
||||||
|
return query(args);
|
||||||
|
},
|
||||||
|
|
||||||
|
async groupBy({ args, query }) {
|
||||||
|
args.where = { ...args.where, tenantId };
|
||||||
|
return query(args);
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user