Skip to content

feat: database-backed COA with trust-path classification#84

Merged
chitcommit merged 1 commit intomainfrom
feat/coa-schema-trust-path
Apr 8, 2026
Merged

feat: database-backed COA with trust-path classification#84
chitcommit merged 1 commit intomainfrom
feat/coa-schema-trust-path

Conversation

@chitcommit
Copy link
Copy Markdown
Contributor

@chitcommit chitcommit commented Apr 8, 2026

Summary

  • Adds chart_of_accounts table — database-backed, tenant-customizable COA with 60 REI account defaults (global tenant_id = NULL)
  • Adds classification columns on transactionscoa_code, suggested_coa_code, classification_confidence, classified_by/at, reconciled_by/at
  • Adds classification_audit table — full audit trail with L0-L4 trust level attribution and actor type tracking
  • Updates resolveScheduleELine() to prefer pre-classified coa_code over fuzzy keyword match (backward compatible)
  • Seed script for global COA defaults with TurboTenant keyword metadata
  • CLAUDE.md updated with COA schema and trust-path documentation

Trust Path (Executor/Auditor Segregation)

Level Role COA Permissions
L0 Ingest Executor Write to 9010 (suspense) only
L1 Suggest Executor Write suggested_coa_code, not coa_code
L2 Classify Executor Set coa_code on unreconciled transactions
L3 Reconcile Auditor Lock transactions, review L2 classifications
L4 Govern Auditor Modify the COA itself

Test plan

  • npm run check passes (zero TS errors)
  • npm run db:push:system applies schema cleanly
  • npx tsx database/seeds/chart-of-accounts.ts seeds 60 accounts
  • Existing tax reporting unchanged (fuzzy match fallback when coa_code is NULL)

🤖 Generated with Claude Code

Summary by CodeRabbit

Release Notes

  • New Features
    • Added Chart of Accounts system supporting global and tenant-specific account configurations
    • Transaction classification with suggested codes and confidence scoring
    • Classification audit trail recording all COA-related changes with trust levels and actor attribution
    • Enhanced tax reporting with pre-classified account code integration
    • Reconciliation metadata for tracking classified transaction reviews

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>
Copilot AI review requested due to automatic review settings April 8, 2026 05:33
@cloudflare-workers-and-pages
Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Updated (UTC)
✅ Deployment successful!
View logs
chittyfinance 10f6abe Apr 08 2026, 04:38 AM

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Apr 8, 2026

@coderabbitai review

Please evaluate:

  • Security implications
  • Credential exposure risk
  • Dependency supply chain concerns
  • Breaking API changes

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Apr 8, 2026

📝 Walkthrough

Walkthrough

Introduces 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

Cohort / File(s) Summary
Database Schema & Documentation
CLAUDE.md, database/system.schema.ts
Added chartOfAccounts and classificationAudit tables with tenant-scoping and indexing; extended transactions with COA classification fields (coaCode, suggestedCoaCode, classificationConfidence, classifiedBy/At, reconciledBy/At) and corresponding indexes.
Database Seeding
database/seeds/chart-of-accounts.ts
Seed script to populate global COA entries from REI_CHART_OF_ACCOUNTS, computing parent codes and mapping keywords; handles unique constraint conflicts gracefully.
Reporting & Storage Integration
server/lib/consolidated-reporting.ts, server/lib/tax-reporting.ts, server/storage/system.ts
Extended transaction row schema with description and coaCode fields; updated resolveScheduleELine to accept and use pre-classified coaCode parameter; added coaCode to transaction projection in storage layer.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Poem

🐰 A warren of accounts now organized neat,
With codes and trails making classification complete,
From REI seeds we plant them with care,
Trust levels and audits, transparent and fair,
No more fuzzy matching—just clarity bright!

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: database-backed Chart of Accounts with trust-path classification system across multiple new tables and updated logic.
Docstring Coverage ✅ Passed Docstring coverage is 83.33% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/coa-schema-trust-path

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@claude
Copy link
Copy Markdown

claude bot commented Apr 8, 2026

Code Review: feat: database-backed COA with trust-path classification

Good foundation for a COA classification system. The trust-path model is well-documented and the backward-compatible resolveScheduleELine change is clean. A few issues to address before merging:


🔴 Bugs

1. Duplicate index — unclassifiedIdx is identical to coaIdx

// 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. unclassifiedIdx was presumably intended to be a partial index for fast lookup of unclassified rows (WHERE coa_code IS NULL). Since Drizzle doesn't support partial indexes natively, either drop the duplicate or add a raw SQL migration for the partial index:

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:

Drizzle doesn't support partial indexes directly, so we use the composite above and enforce global uniqueness via application logic + seed script

The coa_tenant_code_idx unique index on (tenant_id, code) does not prevent two global accounts with the same code because PostgreSQL treats NULL != NULL. Two rows with tenant_id = NULL, code = '5070' will both pass the constraint. The seed script's try-catch only catches the conflict when there are prior rows — a concurrent insert would slip through. A partial unique index is the correct fix:

CREATE UNIQUE INDEX coa_global_code_idx ON chart_of_accounts (code) WHERE tenant_id IS NULL;

🟡 Code Quality

3. Type-cast hack in seed script

// database/seeds/chart-of-accounts.ts:13
tenantId: null as unknown as undefined, // global account

This works at runtime but indicates a type mismatch in the Drizzle schema. The column is defined as .references(() => tenants.id) without .notNull(), so Drizzle's insert type should accept null (or omitting the field). Use undefined to omit the field from the insert, or check if the inferred type is wrong:

tenantId: undefined, // global account — field is optional/nullable

4. Seed script inserts one row at a time with a try-catch instead of using onConflictDoNothing()

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:

  • N round-trips to the DB (60 for the full COA seed)
  • e.message?.includes('unique') is fragile — it depends on the driver's error message string
  • Any non-unique error (e.g. network blip, FK violation) during a loop iteration masks the real failure until throw e

Prefer:

await db.insert(schema.chartOfAccounts).values(values).onConflictDoNothing();

5. import.meta.url entry-point check is fragile

if (import.meta.url.endsWith(process.argv[1]?.replace(/^file:\/\//, '') || '')) {

The replacement only strips a leading file:// but on some systems the URL is file:///absolute/path. Use a cleaner pattern or just match on the basename:

import { fileURLToPath } from 'url';
const isMain = process.argv[1] === fileURLToPath(import.meta.url);
if (isMain) { ... }

🟡 Schema Design

6. No FK from coaCode / suggestedCoaCode to chart_of_accounts

Both columns on transactions and newCoaCode on classification_audit are plain text with no foreign key to chart_of_accounts.code. Invalid COA codes can be written without any DB-level rejection. A FK reference isn't always feasible (the COA code space differs per tenant), but at minimum the application layer should validate codes against the tenant's effective COA before writing.

7. Enum-like text columns have no constraints

type, action, trustLevel, and actorType accept arbitrary strings. Add Zod refinements to the insert schemas so invalid values are rejected at the application boundary:

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. classification_confidence decimal range is not enforced

decimal(4, 3) allows values from -9.999 to 9.999. The intent is 0.000–1.000. A DB check constraint or Zod .min(0).max(1) refinement would catch bad values early.


🟡 Architecture

9. 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 PATCH /api/transactions/:id and set coaCode directly. This is presumably follow-up work, but the CLAUDE.md section should note that the enforcement layer is not yet implemented to avoid misuse in the interim.

10. No chart_of_accounts routes

L4 governance (add/retire accounts) has no API surface yet. Consider adding a stub route module (server/routes/coa.ts) with GET /api/coa so consuming services can query the effective COA for a tenant — even if mutation endpoints come later.


✅ What's Good

  • Backward-compatible resolveScheduleELine — third parameter is optional, both call sites updated, fuzzy fallback preserved.
  • coaCode added to the getTransactionsForReporting select list in system.ts — correctly threads the pre-classified value through to tax reporting.
  • Audit table design (actor/actorType separation, denormalized tenantId for query performance, previousCoaCode for diffs) is solid.
  • CLAUDE.md update is clear and accurate.

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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_accounts and classification_audit tables plus COA-related classification fields on transactions.
  • Updates tax reporting (resolveScheduleELine) to prefer transaction.coa_code over 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.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

🧹 Nitpick comments (1)
database/system.schema.ts (1)

161-162: unclassifiedIdx is currently the same index twice.

Both coaIdx and unclassifiedIdx are (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

📥 Commits

Reviewing files that changed from the base of the PR and between d5399f1 and 10f6abe.

📒 Files selected for processing (6)
  • CLAUDE.md
  • database/seeds/chart-of-accounts.ts
  • database/system.schema.ts
  • server/lib/consolidated-reporting.ts
  • server/lib/tax-reporting.ts
  • server/storage/system.ts

@chitcommit chitcommit enabled auto-merge (squash) April 8, 2026 06:09
@chitcommit chitcommit merged commit b9e4b11 into main Apr 8, 2026
31 checks passed
@chitcommit chitcommit deleted the feat/coa-schema-trust-path branch April 8, 2026 06:10
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants