Files
NetForge/.planning/phases/04-inventory-expenses-and-financial-reports/04-05-PLAN.md
kevin-asprec b5f2f3946b docs(04): create phase plan — Inventory, Expenses, and Financial Reports
Phase 04: 5 plans in 2 waves
- Wave 1: 04-01 (inventory event-ledger), 04-03 (expense tracking), 04-05 (financial reports) — parallel
- Wave 2: 04-02 (asset management), 04-04 (expense reports + audit trail) — sequential
- Ready for execution

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 10:00:55 +08:00

202 lines
11 KiB
Markdown

---
phase: 04-inventory-expenses-and-financial-reports
plan: 05
type: execute
wave: 1
depends_on: []
files_modified:
- src/lib/services/financial-report-service.ts
- src/app/api/reports/trial-balance/route.ts
- src/app/api/reports/income-statement/route.ts
- src/app/api/reports/balance-sheet/route.ts
- src/app/api/reports/accounts/[id]/entries/route.ts
- src/lib/__tests__/financial-report-service.test.ts
autonomous: true
must_haves:
truths:
- "Trial Balance totals of all debit balances equal all credit balances — books are self-verifying"
- "Income Statement shows revenue minus expenses for any date range"
- "Balance Sheet shows assets = liabilities + equity as of any date"
- "All three reports derived entirely from journal entry lines — no stored balances"
- "Drill-down: clicking an account shows the underlying journal entries for that account in the period"
artifacts:
- path: "src/lib/services/financial-report-service.ts"
provides: "getTrialBalance, getIncomeStatement, getBalanceSheet, getAccountEntries"
exports: ["FinancialReportService"]
- path: "src/lib/__tests__/financial-report-service.test.ts"
provides: "Tests for all 3 reports with balanced verification"
min_lines: 120
key_links:
- from: "src/lib/services/financial-report-service.ts"
to: "src/lib/accounting/journal-entry-service.ts"
via: "Uses getTrialBalance and getAccountBalance for data"
pattern: "JournalEntryService\\.(getTrialBalance|getAccountBalance)"
- from: "src/lib/services/financial-report-service.ts"
to: "prisma.journalEntryLine"
via: "Direct queries for income statement and balance sheet aggregation"
pattern: "journalEntryLine\\.(groupBy|findMany)"
---
<objective>
Build the financial report engine: Trial Balance, Income Statement, and Balance Sheet — all derived entirely from journal entry history. Includes drill-down capability to view underlying entries per account.
Purpose: This is the capstone of the accounting system. ISP owners can verify their books balance (Trial Balance), see profitability (Income Statement), and see financial position (Balance Sheet). All from the same JE data that every other module has been posting to.
Output: FinancialReportService with 3 report types + drill-down, API routes, comprehensive tests.
</objective>
<execution_context>
@C:\Users\KevinAsprec\.claude/get-shit-done/workflows/execute-plan.md
@C:\Users\KevinAsprec\.claude/get-shit-done/templates/summary.md
</execution_context>
<context>
@.planning/PROJECT.md
@.planning/ROADMAP.md
@.planning/STATE.md
@.planning/phases/04-inventory-expenses-and-financial-reports/04-CONTEXT.md
@prisma/schema.prisma
@src/lib/accounting/journal-entry-service.ts
@src/lib/accounting/chart-of-accounts.ts
</context>
<tasks>
<task type="auto">
<name>Task 1: FinancialReportService — Trial Balance, Income Statement, Balance Sheet</name>
<files>
src/lib/services/financial-report-service.ts
src/app/api/reports/trial-balance/route.ts
src/app/api/reports/income-statement/route.ts
src/app/api/reports/balance-sheet/route.ts
src/app/api/reports/accounts/[id]/entries/route.ts
</files>
<action>
**FinancialReportService** (`src/lib/services/financial-report-service.ts`):
Static class. All reports query POSTED journal entry lines only.
- `getTrialBalance(tenantPrisma, { asOfDate? })`:
- Delegates to JournalEntryService.getTrialBalance (already implemented)
- Enhances result with: accountType field per line, computed totalDebits and totalCredits
- Return: { asOfDate, lines: TrialBalanceLine[], totalDebits: Decimal, totalCredits: Decimal, isBalanced: boolean }
- isBalanced = totalDebits equals totalCredits (compare using integer cents to avoid floating point)
- `getIncomeStatement(tenantPrisma, { startDate, endDate })`:
- Query JE lines for REVENUE accounts (4xxx) and EXPENSE accounts (5xxx) within date range
- For each account: compute net balance = sum(credit) - sum(debit) for revenue accounts (normal CREDIT), sum(debit) - sum(credit) for expense accounts (normal DEBIT)
- Group by account, organized into sections:
- Revenue section: accounts where accountType=REVENUE, ordered by code. Show each account + subtotal.
- Expense section: accounts where accountType=EXPENSE, ordered by code. Show each account + subtotal.
- Net income = total revenue - total expenses
- Return: { startDate, endDate, revenue: { accounts: [{code, name, balance}], total }, expenses: { accounts: [{code, name, balance}], total }, netIncome }
- Only include leaf accounts (exclude category headers like 4000, 5000) — filter by: account has no children, OR use the convention that header codes end in "000"
- `getBalanceSheet(tenantPrisma, { asOfDate })`:
- Query JE lines for ASSET, LIABILITY, and EQUITY accounts as of asOfDate (all entries where date <= asOfDate)
- For each account: compute balance based on normalBalance direction
- Group into three sections:
- Assets: accountType=ASSET, each account + subtotal
- Liabilities: accountType=LIABILITY, each account + subtotal
- Equity: accountType=EQUITY, each account + subtotal. Include computed "Net Income" line (revenue - expenses as of date) added to equity section.
- Verify: totalAssets = totalLiabilities + totalEquity (including net income)
- Return: { asOfDate, assets: { accounts: [...], total }, liabilities: { accounts: [...], total }, equity: { accounts: [...], total, netIncome }, totalAssets, totalLiabilitiesAndEquity, isBalanced }
- Only include leaf accounts with non-zero balances
- `getAccountEntries(tenantPrisma, { accountId, startDate?, endDate? })`:
- Fetch all POSTED JE lines for the given account within date range
- Include the parent JE details: entryNumber, date, description, source, referenceType
- Return: { accountCode, accountName, entries: [{ entryNumber, date, description, debit, credit, runningBalance, referenceType }] }
- Running balance computed in order of date ASC, then createdAt ASC for same-date entries
- This is the drill-down capability per CONTEXT.md
**API Routes:**
- `GET /api/reports/trial-balance` — query: asOfDate? (ISO string). ADMIN only.
- `GET /api/reports/income-statement` — query: startDate, endDate (ISO strings). ADMIN only.
- `GET /api/reports/balance-sheet` — query: asOfDate (ISO string). ADMIN only.
- `GET /api/reports/accounts/[id]/entries` — query: startDate?, endDate?. ADMIN, OFFICE_STAFF.
All routes use withPermission() HOF. Dates parsed from query string ISO format.
</action>
<verify>API route files exist and export correct HTTP methods; TypeScript compiles</verify>
<done>FinancialReportService produces Trial Balance, Income Statement, Balance Sheet, and drill-down entries — all derived from JE lines</done>
</task>
<task type="auto">
<name>Task 2: Financial report tests — comprehensive verification of all 3 reports</name>
<files>src/lib/__tests__/financial-report-service.test.ts</files>
<action>
**Setup:** createTenant, create admin + office_staff users, create subscriber, create service plan. Then create known financial transactions that produce verifiable report numbers:
1. Generate an invoice (DR 1100 AR, CR 4010 Revenue) for 1000.00
2. Record a payment (DR 1010 Cash, CR 1100 AR) for 1000.00
3. Create and post an expense for bandwidth (DR 5040, CR 1010 Cash) for 300.00
4. Create and post an expense for fuel (DR 5080 or 5090, CR 1010 Cash) for 100.00
This gives known balances:
- Revenue: 1000 (4010)
- Expenses: 400 total (5040=300, 5080/5090=100)
- Net income: 600
- Cash: 1000 received - 300 - 100 = 600 (1010)
- AR: 1000 - 1000 = 0
NOTE: To create expenses in tests, you need ExpenseCategory and Vendor models from 04-03. If 04-03 is not yet complete (wave 1 parallel), use JournalEntryService.createEntry directly to simulate expense JEs. This keeps 04-05 independent. Create manual SYSTEM JEs with referenceType="Expense" to simulate.
**Test cases:**
*Trial Balance:*
1. Trial Balance — totalDebits equals totalCredits (isBalanced=true)
2. Trial Balance — each account shows correct debit or credit balance
3. Trial Balance with asOfDate filter — excludes entries after the date
*Income Statement:*
4. Income Statement — revenue section shows 4010 Subscription Revenue = 1000
5. Income Statement — expense section shows correct expense accounts
6. Income Statement — netIncome = revenue total - expense total = 600
7. Income Statement — excludes header accounts (4000, 5000 not in report lines)
8. Income Statement — date range filtering (entries outside range excluded)
*Balance Sheet:*
9. Balance Sheet — totalAssets = totalLiabilities + totalEquity (isBalanced=true)
10. Balance Sheet — assets section shows Cash on Hand = 600, AR = 0 (or omitted if zero)
11. Balance Sheet — equity section includes computed Net Income line
12. Balance Sheet — asOfDate filtering works
*Drill-down:*
13. Account entries drill-down — for Cash on Hand (1010): shows payment credit, expense debits with running balance
14. Account entries — includes entryNumber, description, referenceType for each line
*Edge cases:*
15. Empty tenant (no JEs) — Trial Balance returns all accounts with zero balances, isBalanced=true
16. All reports return only leaf accounts (no category headers)
Cleanup order: invoiceLines -> invoices -> journalEntryLines -> null reversesEntryId -> journalEntries -> subscribers -> servicePlans -> tenantSettings -> accountingPeriods -> accounts -> ticketCategories -> users -> tenant
**Important:** Use JournalEntryService.createEntry directly to create test JEs. This avoids dependency on ExpenseService (which is in 04-03, a parallel wave-1 plan). The financial reports read from JE lines regardless of what created them.
</action>
<verify>npx jest financial-report-service --verbose passes all tests</verify>
<done>Trial Balance proves books balance. Income Statement shows correct revenue/expenses/net income. Balance Sheet balances (A=L+E). Drill-down shows entries per account. All tests pass.</done>
</task>
</tasks>
<verification>
- `npx jest financial-report-service --verbose` — all tests pass
- Trial Balance: totalDebits === totalCredits
- Income Statement: netIncome = revenue - expenses
- Balance Sheet: totalAssets === totalLiabilitiesAndEquity
- Drill-down shows entries with running balance per account
- All reports use only POSTED JE lines
</verification>
<success_criteria>
- Trial Balance totals balance (debits = credits) — self-verifying books
- Income Statement shows revenue minus expenses for any date range
- Balance Sheet shows assets = liabilities + equity as of any date
- All three reports derived entirely from journal entry lines
- Drill-down capability returns underlying entries for any account
- All tests pass
</success_criteria>
<output>
After completion, create `.planning/phases/04-inventory-expenses-and-financial-reports/04-05-SUMMARY.md`
</output>