--- phase: 02-subscriber-and-billing-core plan: "01" subsystem: database tags: [prisma, postgresql, accounting, double-entry, chart-of-accounts, multi-tenancy] # Dependency graph requires: - phase: 01-foundation provides: Tenant model, User model, createTenant(), withTenantContext(), withPermission() HOF provides: - Account Prisma model with tenant scoping, code/name/accountType/normalBalance/parentId - AccountingPeriod Prisma model with year/month/status/closedAt/closedById - ISP_CHART_OF_ACCOUNTS: 28-account definition covering all 5 accounting types - seedChartOfAccounts(tx, tenantId): auto-provisions COA inside Prisma transaction - createTenant() now provisions full COA atomically with tenant+user creation - closePeriod, getOpenPeriod, isDateInClosedPeriod functions - GET /api/accounting/accounts — list COA for authenticated tenant - GET /api/accounting/periods — list accounting periods - POST /api/accounting/periods/[id]/close — close a period affects: - 02-02-journal-entry-service (posts to Account records created here; enforces closed periods) - 02-03-invoice-billing (uses Subscription Revenue 4010, Accounts Receivable 1100) - 02-04-payment-collection (uses Cash accounts 1010/1020, credits AR) - All future phases that touch financial data # Tech tracking tech-stack: added: [] patterns: - "COA auto-provisioning in Prisma $transaction — atomic tenant+user+COA creation" - "Zero mutable balance fields — all balances derived from journal entry sums" - "On-demand period creation via getOpenPeriod() — periods created at first use" - "TxClient pattern — seed functions accept transaction client, not full PrismaClient" - "withTenantContext() extended with account and accountingPeriod query blocks" key-files: created: - prisma/migrations/20260304144656_add_accounting_models/migration.sql - src/lib/accounting/chart-of-accounts.ts - src/lib/accounting/accounting-period.ts - src/lib/accounting/seed-coa.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 modified: - prisma/schema.prisma - src/lib/prisma-tenant.ts - src/lib/tenant.ts key-decisions: - "28 accounts in ISP COA — 5 parent headers (1000/2000/3000/4000/5000) + 23 leaf accounts" - "Subscriber Credits (1150) is contra-asset with CREDIT normal balance — reduces AR" - "seedChartOfAccounts receives Prisma tx client (not full PrismaClient) — works inside $transaction" - "Periods created on-demand via getOpenPeriod() — not pre-seeded at tenant creation" - "closedPeriods back-reference added on User model for Prisma relation integrity" - "close route uses closure pattern (withPermission wrapping inside POST fn) for param access" patterns-established: - "Accounting isolation: zero mutable balance fields — enforced at model level (no balanceColumn exists)" - "COA seeding: ISP_CHART_OF_ACCOUNTS array ordered parents-before-children for correct parentId resolution" # Metrics duration: 7min completed: 2026-03-04 --- # Phase 2 Plan 01: Chart of Accounts and Accounting Period Management Summary **ISP double-entry COA (28 accounts, all 5 types) auto-provisioned atomically on tenant signup, with period open/close management — zero mutable balance fields anywhere** ## Performance - **Duration:** 7 min - **Started:** 2026-03-04T14:44:47Z - **Completed:** 2026-03-04T14:51:56Z - **Tasks:** 2 - **Files modified:** 11 ## Accomplishments - Account and AccountingPeriod Prisma models added with full tenant scoping (RLS-ready indexes, @@unique per tenant) - 28-account ISP Chart of Accounts (1000-5000 ranges) with correct normal balances across all 5 types - Every new tenant signup atomically provisions a complete COA inside the existing $transaction - Accounting period close logic (closePeriod, getOpenPeriod, isDateInClosedPeriod) ready for journal entry enforcement in 02-02 - 28 new tests (121 total) covering COA definition purity, seeding correctness, period management, and createTenant integration ## Task Commits Each task was committed atomically: 1. **Task 1: Account and AccountingPeriod Prisma models + COA definition** - `7c0caf5` (feat) 2. **Task 2: COA auto-provisioning on tenant signup + API routes + tests** - `a53ee9c` (feat) ## Files Created/Modified - `prisma/schema.prisma` - Added AccountType/NormalBalance/PeriodStatus enums, Account model, AccountingPeriod model with User.closedPeriods back-reference - `prisma/migrations/20260304144656_add_accounting_models/migration.sql` - Migration creating account and accounting_period tables - `src/lib/accounting/chart-of-accounts.ts` - ISP_CHART_OF_ACCOUNTS (28 accounts), AccountType and NormalBalance TypeScript types - `src/lib/accounting/accounting-period.ts` - getOpenPeriod, closePeriod, isDateInClosedPeriod functions - `src/lib/accounting/seed-coa.ts` - seedChartOfAccounts(tx, tenantId) — resolves parentCode to parentId during seeding - `src/lib/prisma-tenant.ts` - Added "account" and "accountingPeriod" to TENANT_SCOPED_MODELS; full query extension blocks for both models - `src/lib/tenant.ts` - createTenant() now calls seedChartOfAccounts(tx, tenant.id) inside the existing $transaction - `src/app/api/accounting/accounts/route.ts` - GET /api/accounting/accounts (read:Account permission) - `src/app/api/accounting/periods/route.ts` - GET /api/accounting/periods (read:Account permission) - `src/app/api/accounting/periods/[id]/close/route.ts` - POST /api/accounting/periods/[id]/close (manage:Account permission) - `src/lib/__tests__/accounting-coa.test.ts` - 28 integration and unit tests ## Decisions Made - **28 accounts with parent headers:** 5 category headers (1000, 2000, 3000, 4000, 5000) plus 23 leaf accounts — provides hierarchical COA structure for reporting - **Contra-asset 1150:** Subscriber Credits has CREDIT normal balance despite being ASSET type — correctly models overpayments that reduce the AR balance - **TxClient via `any` cast in tests:** Prisma transaction client types don't align perfectly with interface extraction; using `any` in test is safe (the actual seedChartOfAccounts function is typed correctly via PrismaClient.$transaction parameter inference) - **Close route uses closure pattern:** `withPermission` HOF doesn't support route params directly — POST function closes over `params` from Next.js route context before invoking the HOF - **On-demand period creation:** Periods are created on first use (getOpenPeriod), not pre-seeded — avoids creating 12 periods per tenant on signup for months that may never have entries ## Deviations from Plan None — plan executed exactly as written. ## Issues Encountered None. ## User Setup Required None — no external service configuration required. ## Next Phase Readiness - Account and AccountingPeriod models in database — ready for journal entry posting (02-02) - All 28 standard ISP accounts provisioned for every new tenant — journal entries can reference accounts by code - closedPeriods enforcement hook ready — 02-02 JournalEntryService calls isDateInClosedPeriod before posting - CASL "Account" subject already defined in casl/types.ts — permissions are operational - withPermission("manage", "Account") guards period close endpoint — aligns with ADMIN-only restriction --- *Phase: 02-subscriber-and-billing-core* *Completed: 2026-03-04*