Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@
"test:run": "vitest run"
},
"dependencies": {
"@chittyos/schema": "file:../../CHITTYFOUNDATION/chittyschema",
"@hookform/resolvers": "^3.9.1",
"@jridgewell/trace-mapping": "^0.3.25",
"@modelcontextprotocol/sdk": "^1.27.1",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { Hono } from 'hono';
import type { HonoEnv } from '../env';
import { reportRoutes } from '../routes/reports';
import { reportRoutes } from '../accounting/reports';
import { integrationRoutes } from '../routes/integrations';

const env = {
Expand Down
2 changes: 1 addition & 1 deletion server/__tests__/routes-consumer-contract.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { describe, it, expect, vi } from 'vitest';
import { Hono } from 'hono';
import type { HonoEnv } from '../env';
import { accountRoutes } from '../routes/accounts';
import { accountRoutes } from '../accounting/accounts';
import { summaryRoutes } from '../routes/summary';

const mockStorage = {
Expand Down
2 changes: 1 addition & 1 deletion server/__tests__/scenario.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { serviceAuth } from '../middleware/auth';
import { tenantMiddleware } from '../middleware/tenant';
import { propertyRoutes } from '../routes/properties';
import { valuationRoutes } from '../routes/valuation';
import { importRoutes } from '../routes/import';
import { importRoutes } from '../books/import';

const TEST_TOKEN = 'test-service-token';
const TENANT_ID = 'b5fa96af-10eb-4d47-b9af-8fcb2ce24f81'; // IT CAN BE LLC
Expand Down
2 changes: 1 addition & 1 deletion server/__tests__/tax-reporting.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { describe, it, expect, vi } from 'vitest';
import { Hono } from 'hono';
import type { HonoEnv } from '../env';
import { taxRoutes } from '../routes/tax';
import { taxRoutes } from '../accounting/tax';
import {
buildScheduleEReport,
buildForm1065Report,
Expand Down
14 changes: 7 additions & 7 deletions server/__tests__/valuation.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ describe('valuation providers', () => {

describe('parseTurboTenantCSV', () => {
it('parses basic CSV data', async () => {
const { parseTurboTenantCSV } = await import('../routes/import');
const { parseTurboTenantCSV } = await import('../books/import');
const csv = `date,description,amount,category
2024-01-15,Rent Payment,1200,rent
2024-01-20,Maintenance,-85,maintenance`;
Expand All @@ -95,7 +95,7 @@ describe('parseTurboTenantCSV', () => {
});

it('handles quoted fields with commas', async () => {
const { parseTurboTenantCSV } = await import('../routes/import');
const { parseTurboTenantCSV } = await import('../books/import');
const csv = `date,description,amount,category
2024-01-15,"Rent, Unit 5A",1200,rent
2024-01-20,"Repair: sink, faucet",-150,maintenance`;
Expand All @@ -107,7 +107,7 @@ describe('parseTurboTenantCSV', () => {
});

it('handles quoted fields with escaped quotes', async () => {
const { parseTurboTenantCSV } = await import('../routes/import');
const { parseTurboTenantCSV } = await import('../books/import');
const csv = `date,description,amount,category
2024-01-15,"Payment for ""Studio""",1200,rent`;

Expand All @@ -117,7 +117,7 @@ describe('parseTurboTenantCSV', () => {
});

it('skips rows with missing date or invalid amount', async () => {
const { parseTurboTenantCSV } = await import('../routes/import');
const { parseTurboTenantCSV } = await import('../books/import');
const csv = `date,description,amount,category
,No Date,100,rent
2024-01-15,Valid,200,rent
Expand All @@ -129,23 +129,23 @@ describe('parseTurboTenantCSV', () => {
});

it('returns empty for single-line CSV', async () => {
const { parseTurboTenantCSV } = await import('../routes/import');
const { parseTurboTenantCSV } = await import('../books/import');
expect(parseTurboTenantCSV('date,description,amount')).toHaveLength(0);
expect(parseTurboTenantCSV('')).toHaveLength(0);
});
});

describe('deduplicationHash', () => {
it('produces consistent SHA-256 based hash', async () => {
const { deduplicationHash } = await import('../routes/import');
const { deduplicationHash } = await import('../books/import');
const hash1 = await deduplicationHash('2024-01-15', 1200, 'Rent Payment');
const hash2 = await deduplicationHash('2024-01-15', 1200, 'Rent Payment');
expect(hash1).toBe(hash2);
expect(hash1).toMatch(/^tt-[0-9a-f]{16}$/);
});

it('produces different hashes for different inputs', async () => {
const { deduplicationHash } = await import('../routes/import');
const { deduplicationHash } = await import('../books/import');
const hash1 = await deduplicationHash('2024-01-15', 1200, 'Rent');
const hash2 = await deduplicationHash('2024-01-15', 1200, 'Maintenance');
expect(hash1).not.toBe(hash2);
Expand Down
2 changes: 1 addition & 1 deletion server/__tests__/webhooks-mercury.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ const baseEnv = {

// Lazy-import the webhook routes so module mocks above take effect
async function getApp() {
const { webhookRoutes } = await import('../routes/webhooks');
const { webhookRoutes } = await import('../books/webhooks');
const app = new Hono<HonoEnv>();
app.route('/', webhookRoutes);
return app;
Expand Down
2 changes: 1 addition & 1 deletion server/__tests__/webhooks-wave-receiver.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
* configured to skip network in test env via CHITTY_LEDGER_BASE).
*/
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
import { webhookRoutes } from '../routes/webhooks';
import { webhookRoutes } from '../books/webhooks';

// Real ledger-client code runs; only the network boundary (fetch) is intercepted.
// Per project rule: no mocking of service modules — but stubbing the global
Expand Down
2 changes: 1 addition & 1 deletion server/__tests__/webhooks-wave-secret.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
* Tests use a Map-backed KV stand-in, matching the existing Mercury pattern.
*/
import { describe, it, expect, beforeEach } from 'vitest';
import { webhookRoutes } from '../routes/webhooks';
import { webhookRoutes } from '../books/webhooks';

const SERVICE_TOKEN = 'test-service-token';
const TENANT = '11111111-1111-1111-1111-111111111111';
Expand Down
7 changes: 7 additions & 0 deletions server/accounting/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# Accounting

Accounting **derives meaning** from the facts Books records.

Boundary: chart of accounts, reporting, tax, and allocations. It consumes the facts written by `server/books/` (transactions, imports, webhooks) and does not ingest or journal raw activity itself.

Owner: `chittyaccounting-agent` → coa / reporting / tax / allocations.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
14 changes: 7 additions & 7 deletions server/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,15 @@ import { callerContext } from './middleware/caller';
import { tenantMiddleware } from './middleware/tenant';
import { healthRoutes } from './routes/health';
import { docRoutes } from './routes/docs';
import { accountRoutes } from './routes/accounts';
import { accountRoutes } from './accounting/accounts';
import { summaryRoutes } from './routes/summary';
import { tenantRoutes } from './routes/tenants';
import { propertyRoutes } from './routes/properties';
import { transactionRoutes } from './routes/transactions';
import { transactionRoutes } from './books/transactions';
import { integrationRoutes } from './routes/integrations';
import { taskRoutes } from './routes/tasks';
import { aiRoutes } from './routes/ai';
import { webhookRoutes } from './routes/webhooks';
import { webhookRoutes } from './books/webhooks';
import { mercuryRoutes } from './routes/mercury';
import { githubRoutes } from './routes/github';
import { stripeRoutes } from './routes/stripe';
Expand All @@ -27,16 +27,16 @@ import { chargeRoutes } from './routes/charges';
import { forensicRoutes } from './routes/forensics';
import { valuationRoutes } from './routes/valuation';
import { portfolioRoutes } from './routes/portfolio';
import { importRoutes } from './routes/import';
import { importRoutes } from './books/import';
import { mcpRoutes } from './routes/mcp';
import { reportRoutes } from './routes/reports';
import { taxRoutes } from './routes/tax';
import { reportRoutes } from './accounting/reports';
import { taxRoutes } from './accounting/tax';
import { googleRoutes, googleCallbackRoute } from './routes/google';
import { commsRoutes } from './routes/comms';
import { workflowRoutes } from './routes/workflows';
import { leaseRoutes } from './routes/leases';
import { chittyIdAuthRoutes } from './routes/chittyid-auth';
import { allocationRoutes } from './routes/allocations';
import { allocationRoutes } from './accounting/allocations';
import { classificationRoutes } from './routes/classification';
import { emailRoutes } from './routes/email';
import { createDb } from './db/connection';
Expand Down
7 changes: 7 additions & 0 deletions server/books/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# Books

Books **writes facts**: ingest, categorize, and journal financial activity.

Boundary: Books records what happened (transactions, imports, webhook intake). It does not derive meaning — chart of accounts, reporting, tax, and allocations live in `server/accounting/`.

Owner: `chittybooks-agent` → ingest / categorize / journal.
File renamed without changes.
File renamed without changes.
File renamed without changes.
176 changes: 146 additions & 30 deletions server/lib/central-workflows.ts
Original file line number Diff line number Diff line change
@@ -1,59 +1,175 @@
/**
* ChittyFinance scope projector — thin adapter over @chittyos/schema/scope-projector.
* Fractal scope projector.
*
* Preserves the existing call signature (tenantId as top-level field)
* while delegating to the shared fractal scope library.
* Projects local workflow lifecycle events into the canonical `scopes`
* table in ChittyOS-Core (Neon). Uses the fractal scope primitive from
* migration 002_fractal_scopes.sql — self-similar via parent_scope_id,
* lifecycle via scope_status enum, domain taxonomy via scope_type.
*
* Fire-and-forget via waitUntil so local app flows remain authoritative.
* Fall-open: no CHITTYOS_CORE_DATABASE_URL = silent no-op.
*
* @canon: chittycanon://gov/governance#core-types
*/

import {
createScopeProjector,
type ScopeCharacterization,
type ScopeEnv,
type ScopeStatus,
SCOPE_TYPES,
} from '@chittyos/schema/scope-projector';
import { neon } from '@neondatabase/serverless';

// -- Canonical scope_status enum (002_fractal_scopes.sql) -----------------
type ScopeStatus =
| 'new'
| 'active'
| 'waiting'
| 'escalated'
| 'paused'
| 'resolved'
| 'closed'
| 'archived';

// Re-export shared types for downstream convenience
export { SCOPE_TYPES, type ScopeStatus, type ScopeCharacterization, type ScopeEnv };
// -- Canonical scope_characterization enum --------------------------------
type ScopeCharacterization =
| 'Case'
| 'Session'
| 'Transaction'
| 'Incident'
| 'Project'
| 'Engagement';

// -- Public interface for callers -----------------------------------------

export interface ScopeProjection {
/** Local workflow ID — becomes external_id for upsert dedup */
externalId: string;
/** Tenant slug or ID — stored in metadata for filtering */
tenantId: string;
/** Free-text domain taxonomy (e.g. 'maintenance_request', 'expense_approval') */
scopeType: string;
/** Canonical characterization — workflows are typically 'Project' */
characterization?: ScopeCharacterization;
/** Display title */
title: string;
/** Optional summary/description */
summary?: string | null;
/** Local workflow status — mapped to canonical scope_status */
localStatus: string;
/** Optional status reason */
statusReason?: string;
/** Domain-specific state (stored in metadata JSONB) */
metadata?: Record<string, unknown>;
}

const financeProjector = createScopeProjector('finance.chitty.cc', {
characterization: 'Project',
});
interface ScopeEnv {
CHITTYOS_CORE_DATABASE_URL?: string;
}

const SOURCE = 'finance.chitty.cc';
const CREATOR = 'service:finance.chitty.cc';

/**
* Fire-and-forget scope projection — drop-in compatible with existing callers.
* Injects tenantId into metadata (the shared library doesn't assume multi-tenancy).
* Map local workflow status strings to the canonical scope_status enum.
*
* scope_status: new | active | waiting | escalated | paused | resolved | closed | archived
*/
export function scopeLog(
c: { executionCtx: { waitUntil(p: Promise<unknown>): void } },
function toScopeStatus(localStatus: string): ScopeStatus {
switch (localStatus) {
case 'requested':
return 'new';
case 'approved':
case 'in_progress':
return 'active';
case 'completed':
return 'resolved';
case 'rejected':
return 'closed';
case 'blocked':
return 'waiting';
case 'cancelled':
return 'closed';
default:
return 'new';
}
}

/**
* Upsert a scope row in chittyos-core's public.scopes table.
*
* Uses the unique index on (source, external_id) for idempotent upsert.
* On conflict (same workflow projected again), updates status + metadata.
* The DB trigger `trg_scopes_transitions` auto-logs state changes to
* scope_events — no manual event inserts needed.
*/
export async function projectScope(
projection: ScopeProjection,
env: ScopeEnv,
): void {
financeProjector(c, env, {
externalId: projection.externalId,
): Promise<void> {
if (!env.CHITTYOS_CORE_DATABASE_URL) return;

const sql = neon(env.CHITTYOS_CORE_DATABASE_URL);
const status = toScopeStatus(projection.localStatus);
const characterization = projection.characterization ?? 'Project';
const metadata = JSON.stringify({
tenantId: projection.tenantId,
scopeType: projection.scopeType,
characterization: projection.characterization,
title: projection.title,
summary: projection.summary,
localStatus: projection.localStatus,
statusReason: projection.statusReason,
metadata: {
tenantId: projection.tenantId,
...(projection.metadata ?? {}),
},
...(projection.metadata ?? {}),
});

try {
await sql`
INSERT INTO public.scopes (
canon_type,
characterization,
scope_type,
status,
status_reason,
creator_id,
current_agent_id,
title,
summary,
source,
external_id,
metadata
) VALUES (
'E',
${characterization}::scope_characterization,
${projection.scopeType},
${status}::scope_status,
${projection.statusReason ?? null},
${CREATOR},
${CREATOR},
${projection.title},
${projection.summary ?? null},
${SOURCE},
${projection.externalId},
${metadata}::jsonb
)
ON CONFLICT (source, external_id)
WHERE external_id IS NOT NULL AND deleted_at IS NULL
DO UPDATE SET
status = ${status}::scope_status,
status_reason = ${projection.statusReason ?? null},
current_agent_id = ${CREATOR},
title = ${projection.title},
summary = ${projection.summary ?? null},
metadata = ${metadata}::jsonb
`;
} catch (err) {
console.warn('[scope-projector] upsert failed:', err);
}
}

/**
* Fire-and-forget scope projection via executionCtx.waitUntil.
* Drop-in replacement for the old centralWorkflowLog.
*/
export function scopeLog(
c: { executionCtx: { waitUntil(p: Promise<unknown>): void } },
projection: ScopeProjection,
env: ScopeEnv,
): void {
const promise = projectScope(projection, env);
try {
c.executionCtx.waitUntil(promise);
} catch {
// Test environment / non-Workers runtime — swallow.
}
}
Loading
Loading