---
phase: 02-subscriber-and-billing-core
plan: "01"
type: execute
wave: 1
depends_on: []
files_modified:
- prisma/schema.prisma
- src/lib/accounting/chart-of-accounts.ts
- src/lib/accounting/seed-coa.ts
- src/lib/accounting/accounting-period.ts
- src/lib/tenant.ts
- src/app/api/accounting/accounts/route.ts
- src/app/api/accounting/periods/route.ts
- src/app/api/accounting/periods/[id]/close/route.ts
- prisma/migrations/*_add_accounting_models/migration.sql
- src/lib/__tests__/accounting-coa.test.ts
autonomous: true
must_haves:
truths:
- "A new tenant signup auto-provisions a complete ISP Chart of Accounts"
- "Account balances are never stored — only derived from journal entry sums"
- "Admin can close an accounting period, preventing future entries in that period"
- "COA accounts have correct normal balance types (debit/credit) for ISP operations"
artifacts:
- path: "prisma/schema.prisma"
provides: "Account, AccountingPeriod models with tenant scoping"
contains: "model Account"
- path: "src/lib/accounting/chart-of-accounts.ts"
provides: "ISP COA definition with account codes, types, normal balances"
exports: ["ISP_CHART_OF_ACCOUNTS", "AccountType", "NormalBalance"]
- path: "src/lib/accounting/seed-coa.ts"
provides: "Function to provision COA for a tenant"
exports: ["seedChartOfAccounts"]
- path: "src/lib/accounting/accounting-period.ts"
provides: "Accounting period open/close logic"
exports: ["closePeriod", "getOpenPeriod", "isDateInClosedPeriod"]
- path: "src/lib/tenant.ts"
provides: "Updated createTenant that calls seedChartOfAccounts"
key_links:
- from: "src/lib/tenant.ts"
to: "src/lib/accounting/seed-coa.ts"
via: "createTenant calls seedChartOfAccounts inside transaction"
pattern: "seedChartOfAccounts"
- from: "src/lib/accounting/seed-coa.ts"
to: "prisma/schema.prisma"
via: "creates Account records per ISP_CHART_OF_ACCOUNTS"
pattern: "account\\.create"
---
Create the Chart of Accounts (COA) data model and auto-provisioning system, plus accounting period management. Every new tenant gets a pre-configured ISP-specific COA on signup. No mutable balance fields exist anywhere — all balances will be derived from journal entries (built in 02-02).
Purpose: The COA is the foundation of the double-entry accounting system. Without accounts, journal entries have nowhere to post. This must exist before any financial transaction can be recorded.
Output: Account and AccountingPeriod Prisma models, ISP COA seed data, auto-provisioning on tenant signup, period close API.
@C:\Users\KevinAsprec\.claude/get-shit-done/workflows/execute-plan.md
@C:\Users\KevinAsprec\.claude/get-shit-done/templates/summary.md
@.planning/PROJECT.md
@.planning/ROADMAP.md
@.planning/STATE.md
@.planning/phases/02-subscriber-and-billing-core/02-CONTEXT.md
@prisma/schema.prisma
@src/lib/tenant.ts
@src/lib/prisma-tenant.ts
Task 1: Account and AccountingPeriod Prisma models + COA definition
prisma/schema.prisma
src/lib/accounting/chart-of-accounts.ts
src/lib/accounting/accounting-period.ts
src/lib/prisma-tenant.ts
1. Add enums to prisma/schema.prisma:
- `AccountType`: ASSET, LIABILITY, EQUITY, REVENUE, EXPENSE
- `NormalBalance`: DEBIT, CREDIT
- `PeriodStatus`: OPEN, CLOSED
2. Add `Account` model to prisma/schema.prisma:
- id (uuid), tenantId (String, required), code (String, e.g., "1000"), name (String), accountType (AccountType), normalBalance (NormalBalance), parentId (String?, self-relation for sub-accounts), isSystemAccount (Boolean, default true — COA accounts cannot be deleted), createdAt, updatedAt
- @@unique([tenantId, code]) — account codes unique per tenant
- @@index([tenantId])
3. Add `AccountingPeriod` model:
- id (uuid), tenantId (String), year (Int), month (Int), status (PeriodStatus, default OPEN), closedAt (DateTime?), closedById (String?, relation to User), createdAt, updatedAt
- @@unique([tenantId, year, month]) — one period per tenant per month
- @@index([tenantId])
4. Create src/lib/accounting/chart-of-accounts.ts:
- Export `ISP_CHART_OF_ACCOUNTS` as a typed array of account definitions with code, name, accountType, normalBalance, parentCode (optional). Standard ISP accounts:
- Assets (1000s): Cash on Hand (1010), Cash in Bank (1020), Accounts Receivable (1100), Subscriber Credits (1150, contra-receivable for overpayments), Equipment Inventory (1200), Prepaid Expenses (1300)
- Liabilities (2000s): Accounts Payable (2010), Unearned Revenue (2100, for prepaid subscriber payments), Taxes Payable (2200)
- Equity (3000s): Owner's Equity (3010), Retained Earnings (3020)
- Revenue (4000s): Subscription Revenue (4010), Installation Fees (4020), Reconnection Fees (4030), Other Revenue (4090)
- Expenses (5000s): Salary Expense (5010), Technician Compensation (5020), Equipment Expense (5030), Internet Bandwidth (5040), Office Supplies (5050), Utilities (5060), Depreciation (5070), Other Expense (5090)
- Export TypeScript types: AccountType, NormalBalance (mirrors the Prisma enums for use outside Prisma context)
5. Create src/lib/accounting/accounting-period.ts:
- `getOpenPeriod(tenantPrisma, year, month)` — finds or creates an OPEN period for the given month
- `closePeriod(tenantPrisma, periodId, closedById)` — sets status to CLOSED, records closedAt and closedById. Throws if already closed.
- `isDateInClosedPeriod(tenantPrisma, date)` — returns boolean, checks if the month/year of the date has a CLOSED period
6. Update TENANT_SCOPED_MODELS in src/lib/prisma-tenant.ts to include "account" and "accountingPeriod". Add the same query extension blocks (findMany, findFirst, create, update, delete, etc.) following the existing `user` pattern exactly.
7. Run `npx prisma migrate dev --name add_accounting_models` to generate and apply the migration.
- `npx prisma migrate status` shows no pending migrations
- `npx prisma generate` succeeds
- TypeScript compiles: `npx tsc --noEmit`
- ISP_CHART_OF_ACCOUNTS has entries covering all 5 account types
Account and AccountingPeriod models exist in database, COA definition exported, tenant scoping extensions updated, accounting period functions exported
Task 2: COA auto-provisioning on tenant signup + API routes + tests
src/lib/accounting/seed-coa.ts
src/lib/tenant.ts
src/app/api/accounting/accounts/route.ts
src/app/api/accounting/periods/route.ts
src/app/api/accounting/periods/[id]/close/route.ts
src/lib/__tests__/accounting-coa.test.ts
1. Create src/lib/accounting/seed-coa.ts:
- `seedChartOfAccounts(tx, tenantId)` — takes a Prisma transaction client and tenantId. Iterates ISP_CHART_OF_ACCOUNTS, creates Account records. Resolves parentCode to parentId by looking up already-created parent accounts. Returns count of accounts created.
- Must work inside an existing transaction (receives `tx` not a full PrismaClient)
2. Update src/lib/tenant.ts createTenant():
- Import seedChartOfAccounts
- Inside the existing $transaction block, after creating tenant and user, call `await seedChartOfAccounts(tx, tenant.id)`
- This means every new tenant signup gets a full COA automatically
3. Create API routes (all require ADMIN role via withPermission):
- GET /api/accounting/accounts — list all accounts for the tenant, ordered by code. Use withPermission("read", "Account"). Returns accounts with their type, code, name, normalBalance.
- GET /api/accounting/periods — list accounting periods for the tenant, ordered by year desc, month desc. Use withPermission("read", "Account").
- POST /api/accounting/periods/[id]/close — close an accounting period. Use withPermission("manage", "Account"). Calls closePeriod(). Returns updated period.
4. Write tests in src/lib/__tests__/accounting-coa.test.ts:
- Test seedChartOfAccounts creates the correct number of accounts for a tenant
- Test seedChartOfAccounts sets parentId correctly for sub-accounts
- Test all 5 account types are represented
- Test normal balances are correct (assets/expenses = DEBIT, liabilities/equity/revenue = CREDIT)
- Test closePeriod sets status to CLOSED and records timestamp
- Test closePeriod throws on already-closed period
- Test isDateInClosedPeriod returns true for closed month, false for open month
- Test createTenant now provisions COA (integration test — create tenant, verify accounts exist)
Run: `npx vitest run src/lib/__tests__/accounting-coa.test.ts`
- `npx vitest run src/lib/__tests__/accounting-coa.test.ts` — all tests pass
- `npx tsc --noEmit` — no type errors
- Creating a tenant via the signup API produces Account records in the database
New tenant signup auto-provisions ISP Chart of Accounts. Admin can list accounts and close accounting periods via API. All tests pass.
- `npx vitest run` — all existing + new tests pass
- `npx tsc --noEmit` — clean compilation
- Create a test tenant via POST /api/tenants/signup, verify Account records exist for that tenant
- GET /api/accounting/accounts returns the full COA for the authenticated tenant
- POST /api/accounting/periods/[id]/close successfully closes a period
- Account model exists with code, name, type, normalBalance, tenantId
- AccountingPeriod model exists with year, month, status, tenantId
- ISP COA has 20+ accounts across all 5 types
- createTenant() auto-provisions COA in same transaction
- Period close prevents entries in closed periods (enforcement in 02-02)
- Zero mutable balance fields anywhere
- All tests pass