feat: database-backed COA with trust-path classification#84
Conversation
Add chart_of_accounts table, classification columns on transactions, and classification_audit trail. Implements L0-L4 trust path for executor/auditor segregation on financial classification. - chart_of_accounts: tenant-customizable with global defaults (60 REI accounts) - transactions: coa_code, suggested_coa_code, classification_confidence, classified_by/at, reconciled_by/at columns - classification_audit: full audit trail with trust level attribution - resolveScheduleELine() now prefers pre-classified coa_code over fuzzy match - Seed script for global COA defaults - CLAUDE.md updated with COA and trust-path documentation Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Deploying with
|
| Status | Name | Latest Commit | Updated (UTC) |
|---|---|---|---|
| ✅ Deployment successful! View logs |
chittyfinance | 10f6abe | Apr 08 2026, 04:38 AM |
|
@coderabbitai review Please evaluate:
|
📝 WalkthroughWalkthroughIntroduces a Chart of Accounts (COA) system with database tables for account definitions, transaction classification metadata, and audit trails. Adds a seed script to populate global COA entries and extends transaction storage/reporting to use pre-classified COA codes instead of fuzzy-matched descriptions. Changes
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 3✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Code Review: feat: database-backed COA with trust-path classificationGood foundation for a COA classification system. The trust-path model is well-documented and the backward-compatible 🔴 Bugs1. Duplicate index — // database/system.schema.ts
coaIdx: index('transactions_coa_idx').on(table.tenantId, table.coaCode),
unclassifiedIdx: index('transactions_unclassified_idx').on(table.tenantId, table.coaCode),Both index the exact same columns. CREATE INDEX transactions_unclassified_idx ON transactions (tenant_id) WHERE coa_code IS NULL;2. Global COA uniqueness not enforced at the DB level The comment in the schema acknowledges this:
The CREATE UNIQUE INDEX coa_global_code_idx ON chart_of_accounts (code) WHERE tenant_id IS NULL;🟡 Code Quality3. Type-cast hack in seed script // database/seeds/chart-of-accounts.ts:13
tenantId: null as unknown as undefined, // global accountThis works at runtime but indicates a type mismatch in the Drizzle schema. The column is defined as tenantId: undefined, // global account — field is optional/nullable4. Seed script inserts one row at a time with a try-catch instead of using for (const val of values) {
try {
await db.insert(schema.chartOfAccounts).values(val);
// ...
} catch (e: any) {
if (e.message?.includes('unique') || e.code === '23505') { ... }
throw e;
}
}Problems:
Prefer: await db.insert(schema.chartOfAccounts).values(values).onConflictDoNothing();5. if (import.meta.url.endsWith(process.argv[1]?.replace(/^file:\/\//, '') || '')) {The replacement only strips a leading import { fileURLToPath } from 'url';
const isMain = process.argv[1] === fileURLToPath(import.meta.url);
if (isMain) { ... }🟡 Schema Design6. No FK from Both columns on 7. Enum-like
export const insertClassificationAuditSchema = createInsertSchema(classificationAudit, {
trustLevel: z.enum(['L0', 'L1', 'L2', 'L3', 'L4']),
actorType: z.enum(['user', 'agent', 'system']),
action: z.enum(['classify', 'reclassify', 'reconcile', 'override']),
});8.
🟡 Architecture9. Trust-path described but not enforced The PR documents L0–L4 executor/auditor segregation but there are no routes, middleware, or storage methods that enforce it. An L0 ingest agent could call any existing 10. No L4 governance (add/retire accounts) has no API surface yet. Consider adding a stub route module ( ✅ What's Good
|
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 10f6abeab7
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
There was a problem hiding this comment.
Pull request overview
Introduces a database-backed Chart of Accounts (COA) and transaction-level COA classification/auditing to support a trust-path governed accounting workflow, and updates tax reporting to prefer authoritative classifications when present.
Changes:
- Adds
chart_of_accountsandclassification_audittables plus COA-related classification fields ontransactions. - Updates tax reporting (
resolveScheduleELine) to prefertransaction.coa_codeover fuzzy matching when available. - Adds a seed script to populate a global (tenant_id = NULL) default COA.
Reviewed changes
Copilot reviewed 6 out of 6 changed files in this pull request and generated 6 comments.
Show a summary per file
| File | Description |
|---|---|
| server/storage/system.ts | Includes coaCode in consolidated reporting transaction selects so downstream reporting can use pre-classified codes. |
| server/lib/tax-reporting.ts | Adds preClassifiedCoaCode support and routes tx coaCode through Schedule E / 1065 report builders. |
| server/lib/consolidated-reporting.ts | Extends ReportingTransactionRow to carry description and optional coaCode. |
| database/system.schema.ts | Defines chart_of_accounts, COA classification columns on transactions, and classification_audit table + indexes. |
| database/seeds/chart-of-accounts.ts | Seeds global COA defaults with keyword metadata. |
| CLAUDE.md | Documents the new COA schema and trust-path levels. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
There was a problem hiding this comment.
Actionable comments posted: 6
🧹 Nitpick comments (1)
database/system.schema.ts (1)
161-162:unclassifiedIdxis currently the same index twice.Both
coaIdxandunclassifiedIdxare(tenant_id, coa_code), so every transaction write pays for the same b-tree twice. Keep one, or make the second a true unclassified-only index if that was the intent.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@database/system.schema.ts` around lines 161 - 162, The two indexes coaIdx and unclassifiedIdx are identical (both on table.tenantId and table.coaCode) which duplicates the same b-tree; either remove unclassifiedIdx or change it to a true "unclassified-only" partial/index-on-null to avoid duplication. Fix by editing unclassifiedIdx (index name transactions_unclassified_idx) to use a different key or a predicate (e.g., index only on table.tenantId with WHERE table.coaCode IS NULL or WHERE table.coaCode = 'UNCLASSIFIED'), or delete the unclassifiedIdx line if you intend to keep just coaIdx; ensure the index name and definition reflect the chosen behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@CLAUDE.md`:
- Around line 225-232: The Markdown block titled "Trust Path for Classification"
contains a table that needs surrounding blank lines to satisfy MD058; update the
section around the "Trust Path for Classification" header and the table (the
pipe-delimited block starting with "| Level | Role | COA Permissions |") by
inserting one blank line immediately before the table and one blank line
immediately after the table so the markdownlint-cli2 check passes.
In `@database/seeds/chart-of-accounts.ts`:
- Around line 48-59: The deriveParentCode function currently computes parentNum
as Math.floor(num / 100) * 100 which yields the same code for x100 buckets
(e.g., 5100 → 5100) and creates self-parent cycles; change the logic in
deriveParentCode to first detect x000 top-level (keep the existing num % 1000
=== 0 check), then if num % 100 === 0 (but not %1000) compute the parent at the
x000 level (e.g., Math.floor(num/1000) * 1000) instead of the x100 level,
otherwise keep the existing Math.floor(num / 100) * 100 computation; also add a
guard that ensures parentCode !== code before checking REI_CHART_OF_ACCOUNTS and
returning parentCode only if parentExists.
In `@database/system.schema.ts`:
- Around line 457-478: classification_audit currently requires transactionId so
it cannot record COA-level actions (L4) that operate on the chart_of_accounts;
update the schema so COA changes are auditable by either adding nullable
COA-level reference fields to classification_audit (e.g., coaId:
uuid('coa_id').references(() => chart_of_accounts.id) and coaChangeType:
text('coa_change_type')) and make transactionId nullable for COA-level rows, or
create a new governance_audit (or chart_of_accounts_audit) table that records
coa_id, action (add/rename/retire), actorId/actorType, trustLevel, reason,
metadata and createdAt; ensure whichever path you choose uses the existing
symbols classification_audit, transactionId, chart_of_accounts and actorId to
maintain continuity and add appropriate indexes (coaId/coa_idx) for
tenant-scoped queries.
- Around line 89-93: The composite
uniqueIndex('coa_tenant_code_idx').on(table.tenantId, table.code) does not
prevent duplicate global accounts because PostgreSQL allows multiple NULLs;
replace it with two partial unique indexes so global accounts are enforced
separately: create a unique index coa_global_code_idx ON chart_of_accounts(code)
WHERE tenant_id IS NULL and a unique index coa_tenant_code_idx ON
chart_of_accounts(tenant_id, code) WHERE tenant_id IS NOT NULL; implement these
either via a migration or by emitting raw SQL from the schema setup (e.g., using
Drizzle.raw or your migration runner) and remove or adjust the existing
uniqueIndex call in database/system.schema.ts accordingly.
- Around line 147-149: Three confidence fields use decimal(4,3) which permits
>1.000; change the schema definitions for transactions.classificationConfidence,
propertyValuations.confidence, and classificationAudit.confidence to use decimal
with precision 3 and scale 3 (decimal(3,3)) or add a DB CHECK (<= 1.000) to
enforce the 0.000–1.000 range, and add corresponding Zod validators in the
application layer to reject values outside 0.0–1.0 before trust-path logic runs;
update the symbols classificationConfidence, confidence (in propertyValuations),
and confidence (in classificationAudit) accordingly so both DB and app-level
validation match.
In `@server/lib/tax-reporting.ts`:
- Around line 197-203: resolveScheduleELine currently trusts
preClassifiedCoaCode without validating it, allowing stale/tenant codes to
bypass fuzzy matching; change the logic to validate any preClassifiedCoaCode by
attempting to resolve it via getScheduleELine (or by looking it up in the
chart-of-accounts catalog) and only accept it if it yields a valid schedule
line/label, otherwise fall back to findAccountCode(description, category) to
compute a trusted coaCode; ensure the code paths reference resolveScheduleELine,
preClassifiedCoaCode, getScheduleELine, and findAccountCode so reviewers can
locate and verify the fix.
---
Nitpick comments:
In `@database/system.schema.ts`:
- Around line 161-162: The two indexes coaIdx and unclassifiedIdx are identical
(both on table.tenantId and table.coaCode) which duplicates the same b-tree;
either remove unclassifiedIdx or change it to a true "unclassified-only"
partial/index-on-null to avoid duplication. Fix by editing unclassifiedIdx
(index name transactions_unclassified_idx) to use a different key or a predicate
(e.g., index only on table.tenantId with WHERE table.coaCode IS NULL or WHERE
table.coaCode = 'UNCLASSIFIED'), or delete the unclassifiedIdx line if you
intend to keep just coaIdx; ensure the index name and definition reflect the
chosen behavior.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 3a1c8781-7a0b-4e37-aab7-d6ced6e02921
📒 Files selected for processing (6)
CLAUDE.mddatabase/seeds/chart-of-accounts.tsdatabase/system.schema.tsserver/lib/consolidated-reporting.tsserver/lib/tax-reporting.tsserver/storage/system.ts
Summary
chart_of_accountstable — database-backed, tenant-customizable COA with 60 REI account defaults (globaltenant_id = NULL)transactions—coa_code,suggested_coa_code,classification_confidence,classified_by/at,reconciled_by/atclassification_audittable — full audit trail with L0-L4 trust level attribution and actor type trackingresolveScheduleELine()to prefer pre-classifiedcoa_codeover fuzzy keyword match (backward compatible)Trust Path (Executor/Auditor Segregation)
suggested_coa_code, notcoa_codecoa_codeon unreconciled transactionsTest plan
npm run checkpasses (zero TS errors)npm run db:push:systemapplies schema cleanlynpx tsx database/seeds/chart-of-accounts.tsseeds 60 accountscoa_codeis NULL)🤖 Generated with Claude Code
Summary by CodeRabbit
Release Notes